mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-01-23 01:06:21 +01:00
Group photos by date in album browser
This commit is contained in:
parent
dfe1acc6e4
commit
5557b591c9
9 changed files with 266 additions and 34 deletions
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
20
lib/widget/photo_list_helper.dart
Normal file
20
lib/widget/photo_list_helper.dart
Normal 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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Reference in a new issue