diff --git a/lib/model/actions/entry_info_actions.dart b/lib/model/actions/entry_info_actions.dart new file mode 100644 index 000000000..77e5a557a --- /dev/null +++ b/lib/model/actions/entry_info_actions.dart @@ -0,0 +1,4 @@ +enum SettingsAction { + export, + import, +} diff --git a/lib/model/entry.dart b/lib/model/entry.dart index 5d2812de2..e593bc719 100644 --- a/lib/model/entry.dart +++ b/lib/model/entry.dart @@ -12,8 +12,8 @@ import 'package:aves/services/geocoding_service.dart'; import 'package:aves/services/service_policy.dart'; import 'package:aves/services/services.dart'; import 'package:aves/services/svg_metadata_service.dart'; +import 'package:aves/theme/format.dart'; import 'package:aves/utils/change_notifier.dart'; -import 'package:aves/utils/time_utils.dart'; import 'package:collection/collection.dart'; import 'package:country_code/country_code.dart'; import 'package:flutter/foundation.dart'; diff --git a/lib/model/video/metadata.dart b/lib/model/video/metadata.dart index f906ac1a7..bdb23dbb3 100644 --- a/lib/model/video/metadata.dart +++ b/lib/model/video/metadata.dart @@ -11,10 +11,10 @@ import 'package:aves/model/video/profiles/hevc.dart'; import 'package:aves/ref/languages.dart'; import 'package:aves/ref/mp4.dart'; import 'package:aves/services/services.dart'; +import 'package:aves/theme/format.dart'; import 'package:aves/utils/file_utils.dart'; import 'package:aves/utils/math_utils.dart'; import 'package:aves/utils/string_utils.dart'; -import 'package:aves/utils/time_utils.dart'; import 'package:aves/widgets/viewer/video/fijkplayer.dart'; import 'package:collection/collection.dart'; import 'package:fijkplayer/fijkplayer.dart'; diff --git a/lib/services/global_search.dart b/lib/services/global_search.dart index c4a785196..6464c12a7 100644 --- a/lib/services/global_search.dart +++ b/lib/services/global_search.dart @@ -1,10 +1,10 @@ import 'dart:ui'; import 'package:aves/services/services.dart'; +import 'package:aves/theme/format.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:intl/date_symbol_data_local.dart'; -import 'package:intl/intl.dart'; class GlobalSearch { static const platform = MethodChannel('deckers.thibault/aves/global_search'); @@ -55,7 +55,7 @@ Future>> _getSuggestions(dynamic args) async { 'data': entry.uri, 'mimeType': entry.mimeType, 'title': entry.bestTitle, - 'subtitle': date != null ? '${DateFormat.yMMMd(locale).format(date)} • ${DateFormat.Hm(locale).format(date)}' : null, + 'subtitle': date != null ? formatDateTime(date, locale) : null, 'iconUri': entry.uri, }; })); diff --git a/lib/theme/format.dart b/lib/theme/format.dart new file mode 100644 index 000000000..48de7d153 --- /dev/null +++ b/lib/theme/format.dart @@ -0,0 +1,23 @@ +import 'package:intl/intl.dart'; + +String formatDay(DateTime date, String locale) => DateFormat.yMMMd(locale).format(date); + +String formatTime(DateTime date, String locale) => DateFormat.Hm(locale).format(date); + +String formatDateTime(DateTime date, String locale) => '${formatDay(date, locale)} • ${formatTime(date, locale)}'; + +String formatFriendlyDuration(Duration d) { + final seconds = (d.inSeconds.remainder(Duration.secondsPerMinute)).toString().padLeft(2, '0'); + if (d.inHours == 0) return '${d.inMinutes}:$seconds'; + + final minutes = (d.inMinutes.remainder(Duration.minutesPerHour)).toString().padLeft(2, '0'); + return '${d.inHours}:$minutes:$seconds'; +} + +String formatPreciseDuration(Duration d) { + final millis = ((d.inMicroseconds / 1000.0).round() % 1000).toString().padLeft(3, '0'); + final seconds = (d.inSeconds.remainder(Duration.secondsPerMinute)).toString().padLeft(2, '0'); + final minutes = (d.inMinutes.remainder(Duration.minutesPerHour)).toString().padLeft(2, '0'); + final hours = (d.inHours).toString().padLeft(2, '0'); + return '$hours:$minutes:$seconds.$millis'; +} diff --git a/lib/utils/time_utils.dart b/lib/utils/time_utils.dart index 4769b2557..505387930 100644 --- a/lib/utils/time_utils.dart +++ b/lib/utils/time_utils.dart @@ -1,19 +1,3 @@ -String formatFriendlyDuration(Duration d) { - final seconds = (d.inSeconds.remainder(Duration.secondsPerMinute)).toString().padLeft(2, '0'); - if (d.inHours == 0) return '${d.inMinutes}:$seconds'; - - final minutes = (d.inMinutes.remainder(Duration.minutesPerHour)).toString().padLeft(2, '0'); - return '${d.inHours}:$minutes:$seconds'; -} - -String formatPreciseDuration(Duration d) { - final millis = ((d.inMicroseconds / 1000.0).round() % 1000).toString().padLeft(3, '0'); - final seconds = (d.inSeconds.remainder(Duration.secondsPerMinute)).toString().padLeft(2, '0'); - final minutes = (d.inMinutes.remainder(Duration.minutesPerHour)).toString().padLeft(2, '0'); - final hours = (d.inHours).toString().padLeft(2, '0'); - return '$hours:$minutes:$seconds.$millis'; -} - extension ExtraDateTime on DateTime { bool isAtSameYearAs(DateTime? other) => year == other?.year; diff --git a/lib/widgets/common/grid/draggable_thumb_label.dart b/lib/widgets/common/grid/draggable_thumb_label.dart index 6650405f8..ea9e85a3e 100644 --- a/lib/widgets/common/grid/draggable_thumb_label.dart +++ b/lib/widgets/common/grid/draggable_thumb_label.dart @@ -1,3 +1,4 @@ +import 'package:aves/theme/format.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/grid/section_layout.dart'; import 'package:flutter/material.dart'; @@ -62,6 +63,6 @@ class DraggableThumbLabel extends StatelessWidget { static String formatDayThumbLabel(BuildContext context, DateTime? date) { final l10n = context.l10n; if (date == null) return l10n.sectionUnknown; - return DateFormat.yMMMd(l10n.localeName).format(date); + return formatDay(date, l10n.localeName); } } diff --git a/lib/widgets/dialogs/cover_selection_dialog.dart b/lib/widgets/dialogs/cover_selection_dialog.dart index 4177ee42f..3bafd9a88 100644 --- a/lib/widgets/dialogs/cover_selection_dialog.dart +++ b/lib/widgets/dialogs/cover_selection_dialog.dart @@ -73,15 +73,17 @@ class _CoverSelectionDialogState extends State { setState(() {}); }, title: isCustom - ? Row(children: [ - title, - const Spacer(), - IconButton( - icon: const Icon(AIcons.setCover), - onPressed: _isCustom ? _pickEntry : null, - tooltip: context.l10n.changeTooltip, - ), - ]) + ? Row( + children: [ + title, + const Spacer(), + IconButton( + icon: const Icon(AIcons.setCover), + onPressed: _isCustom ? _pickEntry : null, + tooltip: context.l10n.changeTooltip, + ), + ], + ) : title, ); }, diff --git a/lib/widgets/dialogs/edit_entry_date_dialog.dart b/lib/widgets/dialogs/edit_entry_date_dialog.dart new file mode 100644 index 000000000..80b859ef8 --- /dev/null +++ b/lib/widgets/dialogs/edit_entry_date_dialog.dart @@ -0,0 +1,86 @@ +import 'dart:io'; + +import 'package:aves/model/entry.dart'; +import 'package:aves/services/services.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:flutter/material.dart'; + +import 'aves_dialog.dart'; + +class RenameEntryDialog extends StatefulWidget { + final AvesEntry entry; + + const RenameEntryDialog({ + Key? key, + required this.entry, + }) : super(key: key); + + @override + _RenameEntryDialogState createState() => _RenameEntryDialogState(); +} + +class _RenameEntryDialogState extends State { + final TextEditingController _nameController = TextEditingController(); + final ValueNotifier _isValidNotifier = ValueNotifier(false); + + AvesEntry get entry => widget.entry; + + @override + void initState() { + super.initState(); + _nameController.text = entry.filenameWithoutExtension ?? entry.sourceTitle ?? ''; + _validate(); + } + + @override + void dispose() { + _nameController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AvesDialog( + context: context, + content: TextField( + controller: _nameController, + decoration: InputDecoration( + labelText: context.l10n.renameEntryDialogLabel, + suffixText: entry.extension, + ), + autofocus: true, + onChanged: (_) => _validate(), + onSubmitted: (_) => _submit(context), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(MaterialLocalizations.of(context).cancelButtonLabel), + ), + ValueListenableBuilder( + valueListenable: _isValidNotifier, + builder: (context, isValid, child) { + return TextButton( + onPressed: isValid ? () => _submit(context) : null, + child: Text(context.l10n.applyButtonLabel), + ); + }, + ) + ], + ); + } + + String _buildEntryPath(String name) { + if (name.isEmpty) return ''; + return pContext.join(entry.directory!, name + entry.extension!); + } + + Future _validate() async { + final newName = _nameController.text; + final path = _buildEntryPath(newName); + final exists = newName.isNotEmpty && await FileSystemEntity.type(path) != FileSystemEntityType.notFound; + _isValidNotifier.value = newName.isNotEmpty && !exists; + } + + void _submit(BuildContext context) => Navigator.pop(context, _nameController.text); +} diff --git a/lib/widgets/viewer/info/basic_section.dart b/lib/widgets/viewer/info/basic_section.dart index 9fe1daf08..2b7d10767 100644 --- a/lib/widgets/viewer/info/basic_section.dart +++ b/lib/widgets/viewer/info/basic_section.dart @@ -8,6 +8,7 @@ import 'package:aves/model/filters/tag.dart'; import 'package:aves/model/filters/type.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/services/services.dart'; +import 'package:aves/theme/format.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/file_utils.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; @@ -16,7 +17,6 @@ import 'package:aves/widgets/viewer/info/common.dart'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; class BasicSection extends StatelessWidget { final AvesEntry entry; @@ -42,7 +42,7 @@ class BasicSection extends StatelessWidget { final infoUnknown = l10n.viewerInfoUnknown; final date = entry.bestDate; final locale = l10n.localeName; - final dateText = date != null ? '${DateFormat.yMMMd(locale).format(date)} • ${DateFormat.Hm(locale).format(date)}' : infoUnknown; + final dateText = date != null ? formatDateTime(date, locale) : infoUnknown; // TODO TLAD line break on all characters for the following fields when this is fixed: https://github.com/flutter/flutter/issues/61081 // inserting ZWSP (\u200B) between characters does help, but it messes with width and height computation (another Flutter issue) diff --git a/lib/widgets/viewer/overlay/bottom/common.dart b/lib/widgets/viewer/overlay/bottom/common.dart index 181d69277..6b1552424 100644 --- a/lib/widgets/viewer/overlay/bottom/common.dart +++ b/lib/widgets/viewer/overlay/bottom/common.dart @@ -7,6 +7,7 @@ import 'package:aves/model/settings/coordinate_format.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/services/services.dart'; import 'package:aves/theme/durations.dart'; +import 'package:aves/theme/format.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; @@ -18,7 +19,6 @@ import 'package:aves/widgets/viewer/page_entry_builder.dart'; import 'package:decorated_icon/decorated_icon.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; @@ -387,7 +387,7 @@ class _DateRow extends StatelessWidget { Widget build(BuildContext context) { final locale = context.l10n.localeName; final date = entry.bestDate; - final dateText = date != null ? '${DateFormat.yMMMd(locale).format(date)} • ${DateFormat.Hm(locale).format(date)}' : Constants.overlayUnknown; + final dateText = date != null ? formatDateTime(date, locale) : Constants.overlayUnknown; final resolutionText = entry.isSvg ? entry.aspectRatioText : entry.isSized diff --git a/lib/widgets/viewer/overlay/bottom/video.dart b/lib/widgets/viewer/overlay/bottom/video.dart index 8014e9cff..addcdf028 100644 --- a/lib/widgets/viewer/overlay/bottom/video.dart +++ b/lib/widgets/viewer/overlay/bottom/video.dart @@ -5,9 +5,9 @@ import 'package:aves/model/entry.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/services/android_app_service.dart'; import 'package:aves/theme/durations.dart'; +import 'package:aves/theme/format.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/utils/constants.dart'; -import 'package:aves/utils/time_utils.dart'; import 'package:aves/widgets/common/basic/menu.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/fx/blurred.dart';