aves/lib/widgets/dialogs/edit_entry_date_dialog.dart
2021-09-09 14:16:46 +09:00

421 lines
12 KiB
Dart

import 'package:aves/model/entry.dart';
import 'package:aves/model/metadata/date_modifier.dart';
import 'package:aves/model/metadata/enums.dart';
import 'package:aves/theme/format.dart';
import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/material.dart';
import 'aves_dialog.dart';
class EditEntryDateDialog extends StatefulWidget {
final AvesEntry entry;
const EditEntryDateDialog({
Key? key,
required this.entry,
}) : super(key: key);
@override
_EditEntryDateDialogState createState() => _EditEntryDateDialogState();
}
class _EditEntryDateDialogState extends State<EditEntryDateDialog> {
DateEditAction _action = DateEditAction.set;
late Set<MetadataField> _fields;
late DateTime _dateTime;
int _shiftMinutes = 60;
bool _showOptions = false;
AvesEntry get entry => widget.entry;
@override
void initState() {
super.initState();
_fields = {
MetadataField.exifDate,
MetadataField.exifDateDigitized,
MetadataField.exifDateOriginal,
};
_dateTime = entry.bestDate ?? DateTime.now();
}
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
void _updateAction(DateEditAction? action) {
if (action == null) return;
setState(() => _action = action);
}
Widget _tileText(String text) => Text(
text,
softWrap: false,
overflow: TextOverflow.fade,
maxLines: 1,
);
final setTile = Row(
children: [
Expanded(
child: RadioListTile<DateEditAction>(
value: DateEditAction.set,
groupValue: _action,
onChanged: _updateAction,
title: _tileText(l10n.editEntryDateDialogSet),
subtitle: Text(formatDateTime(_dateTime, l10n.localeName)),
),
),
Padding(
padding: const EdgeInsetsDirectional.only(end: 12),
child: IconButton(
icon: const Icon(AIcons.edit),
onPressed: _action == DateEditAction.set ? _editDate : null,
tooltip: context.l10n.changeTooltip,
),
),
],
);
final shiftTile = Row(
children: [
Expanded(
child: RadioListTile<DateEditAction>(
value: DateEditAction.shift,
groupValue: _action,
onChanged: _updateAction,
title: _tileText(l10n.editEntryDateDialogShift),
subtitle: Text(_formatShiftDuration()),
),
),
Padding(
padding: const EdgeInsetsDirectional.only(end: 12),
child: IconButton(
icon: const Icon(AIcons.edit),
onPressed: _action == DateEditAction.shift ? _editShift : null,
tooltip: context.l10n.changeTooltip,
),
),
],
);
final clearTile = RadioListTile<DateEditAction>(
value: DateEditAction.clear,
groupValue: _action,
onChanged: _updateAction,
title: _tileText(l10n.editEntryDateDialogClear),
);
final theme = Theme.of(context);
return Theme(
data: theme.copyWith(
textTheme: theme.textTheme.copyWith(
// dense style font for tile subtitles, without modifying title font
bodyText2: const TextStyle(fontSize: 12),
),
),
child: AvesDialog(
context: context,
title: context.l10n.editEntryDateDialogTitle,
scrollableContent: [
setTile,
shiftTile,
clearTile,
Padding(
padding: const EdgeInsets.only(bottom: 1),
child: ExpansionPanelList(
expansionCallback: (index, isExpanded) {
setState(() => _showOptions = !isExpanded);
},
expandedHeaderPadding: EdgeInsets.zero,
elevation: 0,
children: [
ExpansionPanel(
headerBuilder: (context, isExpanded) => ListTile(
title: Text(l10n.editEntryDateDialogFieldSelection),
),
body: Column(
children: DateModifier.allDateFields
.map((field) => SwitchListTile(
value: _fields.contains(field),
onChanged: (selected) => setState(() => selected ? _fields.add(field) : _fields.remove(field)),
title: Text(_fieldTitle(field)),
))
.toList(),
),
isExpanded: _showOptions,
canTapOnHeader: true,
backgroundColor: Theme.of(context).dialogBackgroundColor,
),
],
),
),
],
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
),
TextButton(
onPressed: () => _submit(context),
child: Text(context.l10n.applyButtonLabel),
),
],
),
);
}
String _formatShiftDuration() {
final abs = _shiftMinutes.abs();
final h = abs ~/ 60;
final m = abs % 60;
return '${_shiftMinutes.isNegative ? '-' : '+'}$h:${m.toString().padLeft(2, '0')}';
}
String _fieldTitle(MetadataField field) {
switch (field) {
case MetadataField.exifDate:
return 'Exif date';
case MetadataField.exifDateOriginal:
return 'Exif original date';
case MetadataField.exifDateDigitized:
return 'Exif digitized date';
case MetadataField.exifGpsDate:
return 'Exif GPS date';
}
}
Future<void> _editDate() async {
final _date = await showDatePicker(
context: context,
initialDate: _dateTime,
firstDate: DateTime(0),
lastDate: DateTime.now(),
confirmText: context.l10n.nextButtonLabel,
);
if (_date == null) return;
final _time = await showTimePicker(
context: context,
initialTime: TimeOfDay.fromDateTime(_dateTime),
);
if (_time == null) return;
setState(() => _dateTime = DateTime(
_date.year,
_date.month,
_date.day,
_time.hour,
_time.minute,
));
}
void _editShift() async {
final picked = await showDialog<int>(
context: context,
builder: (context) => TimeShiftDialog(
initialShiftMinutes: _shiftMinutes,
),
);
if (picked == null) return;
setState(() => _shiftMinutes = picked);
}
void _submit(BuildContext context) {
late DateModifier modifier;
switch (_action) {
case DateEditAction.set:
modifier = DateModifier(_action, _fields, dateTime: _dateTime);
break;
case DateEditAction.shift:
modifier = DateModifier(_action, _fields, shiftMinutes: _shiftMinutes);
break;
case DateEditAction.clear:
modifier = DateModifier(_action, _fields);
break;
}
Navigator.pop(context, modifier);
}
}
class TimeShiftDialog extends StatefulWidget {
final int initialShiftMinutes;
const TimeShiftDialog({
Key? key,
required this.initialShiftMinutes,
}) : super(key: key);
@override
_TimeShiftDialogState createState() => _TimeShiftDialogState();
}
class _TimeShiftDialogState extends State<TimeShiftDialog> {
late ValueNotifier<int> _hour, _minute;
late ValueNotifier<String> _sign;
@override
void initState() {
super.initState();
final initial = widget.initialShiftMinutes;
final abs = initial.abs();
_hour = ValueNotifier(abs ~/ 60);
_minute = ValueNotifier(abs % 60);
_sign = ValueNotifier(initial.isNegative ? '-' : '+');
}
@override
Widget build(BuildContext context) {
const textStyle = TextStyle(fontSize: 34);
return AvesDialog(
context: context,
scrollableContent: [
Center(
child: Padding(
padding: const EdgeInsets.only(top: 8),
child: Table(
children: [
TableRow(
children: [
const SizedBox(),
Center(child: Text(context.l10n.editEntryDateDialogHours)),
const SizedBox(),
Center(child: Text(context.l10n.editEntryDateDialogMinutes)),
],
),
TableRow(
children: [
_Wheel(
valueNotifier: _sign,
values: const ['+', '-'],
textStyle: textStyle,
textAlign: TextAlign.center,
),
Align(
alignment: Alignment.centerRight,
child: _Wheel(
valueNotifier: _hour,
values: List.generate(24, (i) => i),
textStyle: textStyle,
textAlign: TextAlign.end,
),
),
const Padding(
padding: EdgeInsets.only(bottom: 2),
child: Text(
':',
style: textStyle,
),
),
Align(
alignment: Alignment.centerLeft,
child: _Wheel(
valueNotifier: _minute,
values: List.generate(60, (i) => i),
textStyle: textStyle,
textAlign: TextAlign.end,
),
),
],
)
],
defaultColumnWidth: const IntrinsicColumnWidth(),
defaultVerticalAlignment: TableCellVerticalAlignment.middle,
),
),
),
],
hasScrollBar: false,
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
),
TextButton(
onPressed: () => Navigator.pop(context, (_hour.value * 60 + _minute.value) * (_sign.value == '+' ? 1 : -1)),
child: Text(MaterialLocalizations.of(context).okButtonLabel),
),
],
);
}
}
class _Wheel<T> extends StatefulWidget {
final ValueNotifier<T> valueNotifier;
final List<T> values;
final TextStyle textStyle;
final TextAlign textAlign;
const _Wheel({
Key? key,
required this.valueNotifier,
required this.values,
required this.textStyle,
required this.textAlign,
}) : super(key: key);
@override
_WheelState createState() => _WheelState<T>();
}
class _WheelState<T> extends State<_Wheel<T>> {
late final ScrollController _controller;
static const itemSize = Size(40, 40);
ValueNotifier<T> get valueNotifier => widget.valueNotifier;
List<T> get values => widget.values;
@override
void initState() {
super.initState();
var indexOf = values.indexOf(valueNotifier.value);
_controller = FixedExtentScrollController(
initialItem: indexOf,
);
}
@override
Widget build(BuildContext context) {
final background = Theme.of(context).dialogBackgroundColor;
final foreground = DefaultTextStyle.of(context).style.color!;
return Padding(
padding: const EdgeInsets.all(8),
child: SizedBox(
width: itemSize.width,
height: itemSize.height * 3,
child: ShaderMask(
shaderCallback: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
background,
foreground,
foreground,
background,
],
).createShader,
child: ListWheelScrollView(
controller: _controller,
physics: const FixedExtentScrollPhysics(parent: BouncingScrollPhysics()),
diameterRatio: 1.2,
itemExtent: itemSize.height,
squeeze: 1.3,
onSelectedItemChanged: (i) => valueNotifier.value = values[i],
children: values
.map((i) => SizedBox.fromSize(
size: itemSize,
child: Text(
'$i',
textAlign: widget.textAlign,
style: widget.textStyle,
),
))
.toList(),
),
),
),
);
}
}