diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index fc8fbfe3..56bb1d13 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -323,6 +323,14 @@ "settingsScreenBrightnessDescription": "Override system brightness level", "settingsForceRotationTitle": "Ignore rotation lock", "settingsForceRotationDescription": "Rotate the screen even when auto rotation is disabled", + "settingsAlbumTitle": "Album", + "settingsAlbumDescription": "Customize albums", + "settingsAlbumPageTitle": "Album settings", + "@settingsAlbumPageTitle": { + "description": "Dedicated page for album settings" + }, + "settingsShowDateInAlbumTitle": "Group photos by date", + "settingsShowDateInAlbumDescription": "Apply only when the album is sorted by time", "settingsThemeTitle": "Theme", "settingsThemeDescription": "Customize the appearance of the app", "settingsThemePageTitle": "Theme settings", @@ -844,7 +852,6 @@ "description": "Create a password protected share link on server and share it" }, "shareMethodPasswordLinkDescription": "Create a new password protected link on the server", - "errorUnauthenticated": "Unauthenticated access. Please sign-in again if the problem continues", "@errorUnauthenticated": { "description": "Error message when server responds with HTTP401" @@ -869,4 +876,4 @@ "@errorServerError": { "description": "HTTP 500" } -} +} \ No newline at end of file diff --git a/lib/l10n/untranslated-messages.txt b/lib/l10n/untranslated-messages.txt index f8a58fe7..42133861 100644 --- a/lib/l10n/untranslated-messages.txt +++ b/lib/l10n/untranslated-messages.txt @@ -1,5 +1,18 @@ { + "cs": [ + "settingsAlbumTitle", + "settingsAlbumDescription", + "settingsAlbumPageTitle", + "settingsShowDateInAlbumTitle", + "settingsShowDateInAlbumDescription" + ], + "de": [ + "settingsAlbumTitle", + "settingsAlbumDescription", + "settingsAlbumPageTitle", + "settingsShowDateInAlbumTitle", + "settingsShowDateInAlbumDescription", "timeSecondInputHint", "slideshowTooltip", "slideshowSetupDialogTitle", @@ -25,6 +38,11 @@ "settingsScreenBrightnessDescription", "settingsForceRotationTitle", "settingsForceRotationDescription", + "settingsAlbumTitle", + "settingsAlbumDescription", + "settingsAlbumPageTitle", + "settingsShowDateInAlbumTitle", + "settingsShowDateInAlbumDescription", "settingsThemeTitle", "settingsThemeDescription", "settingsThemePageTitle", @@ -88,6 +106,14 @@ "shareMethodPasswordLinkDescription" ], + "es": [ + "settingsAlbumTitle", + "settingsAlbumDescription", + "settingsAlbumPageTitle", + "settingsShowDateInAlbumTitle", + "settingsShowDateInAlbumDescription" + ], + "fr": [ "collectionsTooltip", "settingsViewerTitle", @@ -97,6 +123,11 @@ "settingsScreenBrightnessDescription", "settingsForceRotationTitle", "settingsForceRotationDescription", + "settingsAlbumTitle", + "settingsAlbumDescription", + "settingsAlbumPageTitle", + "settingsShowDateInAlbumTitle", + "settingsShowDateInAlbumDescription", "settingsThemeTitle", "settingsThemeDescription", "settingsThemePageTitle", @@ -141,6 +172,11 @@ ], "ru": [ + "settingsAlbumTitle", + "settingsAlbumDescription", + "settingsAlbumPageTitle", + "settingsShowDateInAlbumTitle", + "settingsShowDateInAlbumDescription", "settingsCaptureLogsTitle", "settingsCaptureLogsDescription", "captureLogDetails", diff --git a/lib/pref.dart b/lib/pref.dart index b09c652a..88ba5df2 100644 --- a/lib/pref.dart +++ b/lib/pref.dart @@ -114,6 +114,13 @@ class Pref { Future setSlideshowRepeat(bool value) => _setBool(PrefKey.isSlideshowRepeat, value); + bool? isAlbumBrowserShowDate() => + _pref.getBool(_toKey(PrefKey.isAlbumBrowserShowDate)); + bool isAlbumBrowserShowDateOr([bool def = false]) => + isAlbumBrowserShowDate() ?? def; + Future setAlbumBrowserShowDate(bool value) => + _setBool(PrefKey.isAlbumBrowserShowDate, value); + bool? hasNewSharedAlbum() => _pref.getBool(_toKey(PrefKey.newSharedAlbum)); bool hasNewSharedAlbumOr(bool def) => hasNewSharedAlbum() ?? def; Future setNewSharedAlbum(bool value) => @@ -189,6 +196,8 @@ class Pref { return "isSlideshowShuffle"; case PrefKey.isSlideshowRepeat: return "isSlideshowRepeat"; + case PrefKey.isAlbumBrowserShowDate: + return "isAlbumBrowserShowDate"; } } @@ -216,6 +225,7 @@ enum PrefKey { slideshowDuration, isSlideshowShuffle, isSlideshowRepeat, + isAlbumBrowserShowDate, } extension PrefExtension on Pref { diff --git a/lib/widget/album_browser.dart b/lib/widget/album_browser.dart index f13e12f9..1754874f 100644 --- a/lib/widget/album_browser.dart +++ b/lib/widget/album_browser.dart @@ -19,6 +19,7 @@ import 'package:nc_photos/k.dart' as k; import 'package:nc_photos/list_extension.dart'; import 'package:nc_photos/or_null.dart'; import 'package:nc_photos/platform/k.dart' as platform_k; +import 'package:nc_photos/pref.dart'; import 'package:nc_photos/session_storage.dart'; import 'package:nc_photos/share_handler.dart'; import 'package:nc_photos/snack_bar_manager.dart'; @@ -30,6 +31,7 @@ import 'package:nc_photos/use_case/update_album_with_actual_items.dart'; import 'package:nc_photos/widget/album_browser_mixin.dart'; import 'package:nc_photos/widget/draggable_item_list_mixin.dart'; import 'package:nc_photos/widget/fancy_option_picker.dart'; +import 'package:nc_photos/widget/photo_list_helper.dart'; import 'package:nc_photos/widget/photo_list_item.dart'; import 'package:nc_photos/widget/selectable_item_stream_list_mixin.dart'; import 'package:nc_photos/widget/simple_input_dialog.dart'; @@ -571,6 +573,9 @@ class _AlbumBrowserState extends State .map((e) => e.file) .where((element) => file_util.isSupportedFormat(element)) .toList(); + final dateHelper = PhotoListDateGroupHelper( + isMonthOnly: false, + ); final items = () sync* { for (int i = 0; i < _sortedItems.length; ++i) { @@ -582,6 +587,14 @@ class _AlbumBrowserState extends State width: k.photoThumbSize, height: k.photoThumbSize, ); + if ((_editAlbum ?? _album)?.sortProvider is AlbumTimeSortProvider && + Pref.inst().isAlbumBrowserShowDateOr()) { + final date = dateHelper.onFile(item.file); + if (date != null) { + yield _DateListItem(date: date); + } + } + if (file_util.isSupportedImageFormat(item.file)) { yield _ImageListItem( index: i, @@ -705,7 +718,7 @@ enum _SelectionMenuOption { } abstract class _ListItem implements SelectableItem, DraggableItem { - _ListItem({ + const _ListItem({ required this.index, VoidCallback? onTap, DragTargetAccept? onDropBefore, @@ -911,3 +924,24 @@ class _EditLabelListItem extends _LabelListItem { final VoidCallback? onEditPressed; } + +class _DateListItem extends _ListItem { + const _DateListItem({ + required this.date, + }) : super(index: -1); + + @override + get isSelectable => false; + + @override + get staggeredTile => const StaggeredTile.extent(99, 32); + + @override + buildWidget(BuildContext context) { + return PhotoListDate( + date: date, + ); + } + + final DateTime date; +} diff --git a/lib/widget/dynamic_album_browser.dart b/lib/widget/dynamic_album_browser.dart index 1cdd4b81..f840cf58 100644 --- a/lib/widget/dynamic_album_browser.dart +++ b/lib/widget/dynamic_album_browser.dart @@ -22,6 +22,7 @@ import 'package:nc_photos/iterable_extension.dart'; import 'package:nc_photos/k.dart' as k; import 'package:nc_photos/or_null.dart'; import 'package:nc_photos/platform/k.dart' as platform_k; +import 'package:nc_photos/pref.dart'; import 'package:nc_photos/share_handler.dart'; import 'package:nc_photos/snack_bar_manager.dart'; import 'package:nc_photos/theme.dart'; @@ -31,6 +32,7 @@ import 'package:nc_photos/use_case/update_album.dart'; import 'package:nc_photos/use_case/update_album_with_actual_items.dart'; import 'package:nc_photos/widget/album_browser_mixin.dart'; import 'package:nc_photos/widget/fancy_option_picker.dart'; +import 'package:nc_photos/widget/photo_list_helper.dart'; import 'package:nc_photos/widget/photo_list_item.dart'; import 'package:nc_photos/widget/selectable_item_stream_list_mixin.dart'; import 'package:nc_photos/widget/viewer.dart'; @@ -535,6 +537,10 @@ class _DynamicAlbumBrowserState extends State .map((e) => e.file) .where((element) => file_util.isSupportedFormat(element)) .toList(); + final dateHelper = PhotoListDateGroupHelper( + isMonthOnly: false, + ); + itemStreamListItems = () sync* { for (int i = 0; i < _sortedItems.length; ++i) { final item = _sortedItems[i]; @@ -545,6 +551,14 @@ class _DynamicAlbumBrowserState extends State width: k.photoThumbSize, height: k.photoThumbSize, ); + if ((_editAlbum ?? _album)?.sortProvider is AlbumTimeSortProvider && + Pref.inst().isAlbumBrowserShowDateOr()) { + final date = dateHelper.onFile(item.file); + if (date != null) { + yield _DateListItem(date: date); + } + } + if (file_util.isSupportedImageFormat(item.file)) { yield _ImageListItem( index: i, @@ -596,7 +610,7 @@ enum _SelectionMenuOption { } abstract class _ListItem implements SelectableItem { - _ListItem({ + const _ListItem({ required this.index, VoidCallback? onTap, }) : _onTap = onTap; @@ -685,3 +699,24 @@ class _VideoListItem extends _FileListItem { final Account account; final String previewUrl; } + +class _DateListItem extends _ListItem { + const _DateListItem({ + required this.date, + }) : super(index: -1); + + @override + get isSelectable => false; + + @override + get staggeredTile => const StaggeredTile.extent(99, 32); + + @override + buildWidget(BuildContext context) { + return PhotoListDate( + date: date, + ); + } + + final DateTime date; +} diff --git a/lib/widget/home_photos.dart b/lib/widget/home_photos.dart index d993640e..e42c1dfa 100644 --- a/lib/widget/home_photos.dart +++ b/lib/widget/home_photos.dart @@ -6,7 +6,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; -import 'package:intl/intl.dart'; import 'package:kiwi/kiwi.dart'; import 'package:logging/logging.dart'; import 'package:nc_photos/account.dart'; @@ -40,6 +39,7 @@ import 'package:nc_photos/widget/album_picker_dialog.dart'; import 'package:nc_photos/widget/home_app_bar.dart'; import 'package:nc_photos/widget/measure.dart'; import 'package:nc_photos/widget/page_visibility_mixin.dart'; +import 'package:nc_photos/widget/photo_list_helper.dart'; import 'package:nc_photos/widget/photo_list_item.dart'; import 'package:nc_photos/widget/selectable_item_stream_list_mixin.dart'; import 'package:nc_photos/widget/selection_app_bar.dart'; @@ -547,18 +547,16 @@ class _HomePhotosState extends State file_util.isSupportedFormat(element) && element.isArchived != true) .sorted(compareFileDateTimeDescending); - DateTime? currentDate; final isMonthOnly = _thumbZoomLevel < 0; + final dateHelper = PhotoListDateGroupHelper( + isMonthOnly: isMonthOnly, + ); itemStreamListItems = () sync* { for (int i = 0; i < _backingFiles.length; ++i) { final f = _backingFiles[i]; - - final newDate = f.bestDateTime.toLocal(); - if (newDate.year != currentDate?.year || - newDate.month != currentDate?.month || - (!isMonthOnly && newDate.day != currentDate?.day)) { - yield _DateListItem(date: newDate, isMonthOnly: isMonthOnly); - currentDate = newDate; + final date = dateHelper.onFile(f); + if (date != null) { + yield _DateListItem(date: date, isMonthOnly: isMonthOnly); } final previewUrl = api_util.getFilePreviewUrl(widget.account, f, @@ -744,30 +742,13 @@ class _DateListItem extends _ListItem { @override buildWidget(BuildContext context) { - String subtitle = ""; - if (date != null) { - final pattern = - isMonthOnly ? DateFormat.YEAR_MONTH : DateFormat.YEAR_MONTH_DAY; - subtitle = - DateFormat(pattern, Localizations.localeOf(context).languageCode) - .format(date!.toLocal()); - } - return Align( - alignment: AlignmentDirectional.centerStart, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Text( - subtitle, - style: Theme.of(context).textTheme.caption!.copyWith( - color: AppTheme.getPrimaryTextColor(context), - fontWeight: FontWeight.bold, - ), - ), - ), + return PhotoListDate( + date: date, + isMonthOnly: isMonthOnly, ); } - final DateTime? date; + final DateTime date; final bool isMonthOnly; } diff --git a/lib/widget/photo_list_helper.dart b/lib/widget/photo_list_helper.dart new file mode 100644 index 00000000..1c6346e2 --- /dev/null +++ b/lib/widget/photo_list_helper.dart @@ -0,0 +1,20 @@ +import 'package:nc_photos/entity/file.dart'; + +class PhotoListDateGroupHelper { + PhotoListDateGroupHelper({ + required this.isMonthOnly, + }); + + DateTime? onFile(File file) { + final newDate = file.bestDateTime.toLocal(); + if (newDate.year != _currentDate?.year || + newDate.month != _currentDate?.month || + (!isMonthOnly && newDate.day != _currentDate?.day)) { + _currentDate = newDate; + return newDate; + } + } + + final bool isMonthOnly; + DateTime? _currentDate; +} diff --git a/lib/widget/photo_list_item.dart b/lib/widget/photo_list_item.dart index d7310841..fee086de 100644 --- a/lib/widget/photo_list_item.dart +++ b/lib/widget/photo_list_item.dart @@ -1,6 +1,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; +import 'package:intl/intl.dart'; import 'package:nc_photos/account.dart'; import 'package:nc_photos/api/api.dart'; import 'package:nc_photos/app_localizations.dart'; @@ -192,3 +193,36 @@ class PhotoListLabelEdit extends PhotoListLabel { final VoidCallback? onEditPressed; } + +class PhotoListDate extends StatelessWidget { + const PhotoListDate({ + Key? key, + required this.date, + this.isMonthOnly = false, + }) : super(key: key); + + @override + build(BuildContext context) { + final pattern = + isMonthOnly ? DateFormat.YEAR_MONTH : DateFormat.YEAR_MONTH_DAY; + final subtitle = + DateFormat(pattern, Localizations.localeOf(context).languageCode) + .format(date.toLocal()); + return Align( + alignment: AlignmentDirectional.centerStart, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Text( + subtitle, + style: Theme.of(context).textTheme.caption!.copyWith( + color: AppTheme.getPrimaryTextColor(context), + fontWeight: FontWeight.bold, + ), + ), + ), + ); + } + + final DateTime date; + final bool isMonthOnly; +} diff --git a/lib/widget/settings.dart b/lib/widget/settings.dart index 3952cab2..1a3b67ee 100644 --- a/lib/widget/settings.dart +++ b/lib/widget/settings.dart @@ -99,6 +99,12 @@ class _SettingsState extends State { description: L10n.global().settingsViewerDescription, builder: () => _ViewerSettings(), ), + _buildSubSettings( + context, + label: L10n.global().settingsAlbumTitle, + description: L10n.global().settingsAlbumDescription, + builder: () => _AlbumSettings(), + ), _buildSubSettings( context, label: L10n.global().settingsThemeTitle, @@ -512,6 +518,75 @@ class _ViewerSettingsState extends State<_ViewerSettings> { static final _log = Logger("widget.settings._ViewerSettingsState"); } +class _AlbumSettings extends StatefulWidget { + @override + createState() => _AlbumSettingsState(); +} + +class _AlbumSettingsState extends State<_AlbumSettings> { + @override + initState() { + super.initState(); + _isBrowserShowDate = Pref.inst().isAlbumBrowserShowDateOr(); + } + + @override + build(BuildContext context) { + return AppTheme( + child: Scaffold( + body: Builder( + builder: (context) => _buildContent(context), + ), + ), + ); + } + + Widget _buildContent(BuildContext context) { + return CustomScrollView( + slivers: [ + SliverAppBar( + pinned: true, + title: Text(L10n.global().settingsAlbumPageTitle), + ), + SliverList( + delegate: SliverChildListDelegate( + [ + SwitchListTile( + title: Text(L10n.global().settingsShowDateInAlbumTitle), + subtitle: + Text(L10n.global().settingsShowDateInAlbumDescription), + value: _isBrowserShowDate, + onChanged: (value) => _onBrowserShowDateChanged(value), + ), + ], + ), + ), + ], + ); + } + + Future _onBrowserShowDateChanged(bool value) async { + final oldValue = _isBrowserShowDate; + setState(() { + _isBrowserShowDate = value; + }); + if (!await Pref.inst().setAlbumBrowserShowDate(value)) { + _log.severe("[_onBrowserShowDateChanged] Failed writing pref"); + SnackBarManager().showSnackBar(SnackBar( + content: Text(L10n.global().writePreferenceFailureNotification), + duration: k.snackBarDurationNormal, + )); + setState(() { + _isBrowserShowDate = oldValue; + }); + } + } + + late bool _isBrowserShowDate; + + static final _log = Logger("widget.settings._AlbumSettingsState"); +} + class _ThemeSettings extends StatefulWidget { @override createState() => _ThemeSettingsState();