Group photos by date in album browser

This commit is contained in:
Ming Ming 2021-10-04 21:53:03 +08:00
parent dfe1acc6e4
commit 5557b591c9
9 changed files with 266 additions and 34 deletions

View file

@ -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"
}
}
}

View file

@ -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",

View file

@ -114,6 +114,13 @@ class Pref {
Future<bool> setSlideshowRepeat(bool value) =>
_setBool(PrefKey.isSlideshowRepeat, value);
bool? isAlbumBrowserShowDate() =>
_pref.getBool(_toKey(PrefKey.isAlbumBrowserShowDate));
bool isAlbumBrowserShowDateOr([bool def = false]) =>
isAlbumBrowserShowDate() ?? def;
Future<bool> setAlbumBrowserShowDate(bool value) =>
_setBool(PrefKey.isAlbumBrowserShowDate, value);
bool? hasNewSharedAlbum() => _pref.getBool(_toKey(PrefKey.newSharedAlbum));
bool hasNewSharedAlbumOr(bool def) => hasNewSharedAlbum() ?? def;
Future<bool> 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 {

View file

@ -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<AlbumBrowser>
.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<AlbumBrowser>
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<DraggableItem>? 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;
}

View file

@ -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<DynamicAlbumBrowser>
.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<DynamicAlbumBrowser>
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;
}

View file

@ -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<HomePhotos>
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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -99,6 +99,12 @@ class _SettingsState extends State<Settings> {
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<void> _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();