mirror of
https://github.com/immich-app/immich.git
synced 2024-12-25 10:43:13 +02:00
feat(mobile): Home screen customization options (#1563)
* Try staggered layout for home page * Introduce setting for dynamic layout * Fix some provider related bugs * Make asset grouping configurable * Add translation keys, refactor group title * Rename enum values * Fix enum names * Reformat long if statement * Fix timezone related bug * Minor clean up * Fix unit test * Add second assets check back to home screen
This commit is contained in:
parent
911c35a7f1
commit
fd13265131
@ -12,6 +12,10 @@
|
|||||||
"album_viewer_appbar_share_leave": "Leave album",
|
"album_viewer_appbar_share_leave": "Leave album",
|
||||||
"album_viewer_appbar_share_remove": "Remove from album",
|
"album_viewer_appbar_share_remove": "Remove from album",
|
||||||
"album_viewer_page_share_add_users": "Add users",
|
"album_viewer_page_share_add_users": "Add users",
|
||||||
|
"asset_list_layout_settings_dynamic_layout_title": "Dynamic layout",
|
||||||
|
"asset_list_layout_settings_group_by": "Group assets by",
|
||||||
|
"asset_list_layout_settings_group_by_month_day": "Month + day",
|
||||||
|
"asset_list_layout_settings_group_by_month": "Month",
|
||||||
"asset_list_settings_subtitle": "Photo grid layout settings",
|
"asset_list_settings_subtitle": "Photo grid layout settings",
|
||||||
"asset_list_settings_title": "Photo Grid",
|
"asset_list_settings_title": "Photo Grid",
|
||||||
"backup_album_selection_page_albums_device": "Albums on device ({})",
|
"backup_album_selection_page_albums_device": "Albums on device ({})",
|
||||||
@ -199,4 +203,4 @@
|
|||||||
"version_announcement_overlay_text_2": "please take your time to visit the ",
|
"version_announcement_overlay_text_2": "please take your time to visit the ",
|
||||||
"version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.",
|
"version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.",
|
||||||
"version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89"
|
"version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89"
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
@ -9,14 +10,15 @@ final log = Logger('AssetGridDataStructure');
|
|||||||
|
|
||||||
enum RenderAssetGridElementType {
|
enum RenderAssetGridElementType {
|
||||||
assetRow,
|
assetRow,
|
||||||
dayTitle,
|
groupDividerTitle,
|
||||||
monthTitle;
|
monthTitle;
|
||||||
}
|
}
|
||||||
|
|
||||||
class RenderAssetGridRow {
|
class RenderAssetGridRow {
|
||||||
final List<Asset> assets;
|
final List<Asset> assets;
|
||||||
|
final List<double> widthDistribution;
|
||||||
|
|
||||||
RenderAssetGridRow(this.assets);
|
RenderAssetGridRow(this.assets, this.widthDistribution);
|
||||||
}
|
}
|
||||||
|
|
||||||
class RenderAssetGridElement {
|
class RenderAssetGridElement {
|
||||||
@ -35,19 +37,36 @@ class RenderAssetGridElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum GroupAssetsBy {
|
||||||
|
day,
|
||||||
|
month;
|
||||||
|
}
|
||||||
|
|
||||||
|
class AssetGridLayoutParameters {
|
||||||
|
final int perRow;
|
||||||
|
final bool dynamicLayout;
|
||||||
|
final GroupAssetsBy groupBy;
|
||||||
|
|
||||||
|
AssetGridLayoutParameters(
|
||||||
|
this.perRow,
|
||||||
|
this.dynamicLayout,
|
||||||
|
this.groupBy,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
class _AssetGroupsToRenderListComputeParameters {
|
class _AssetGroupsToRenderListComputeParameters {
|
||||||
final String monthFormat;
|
final String monthFormat;
|
||||||
final String dayFormat;
|
final String dayFormat;
|
||||||
final String dayFormatYear;
|
final String dayFormatYear;
|
||||||
final Map<String, List<Asset>> groups;
|
final List<Asset> assets;
|
||||||
final int perRow;
|
final AssetGridLayoutParameters layout;
|
||||||
|
|
||||||
_AssetGroupsToRenderListComputeParameters(
|
_AssetGroupsToRenderListComputeParameters(
|
||||||
this.monthFormat,
|
this.monthFormat,
|
||||||
this.dayFormat,
|
this.dayFormat,
|
||||||
this.dayFormatYear,
|
this.dayFormatYear,
|
||||||
this.groups,
|
this.assets,
|
||||||
this.perRow,
|
this.layout,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,62 +75,75 @@ class RenderList {
|
|||||||
|
|
||||||
RenderList(this.elements);
|
RenderList(this.elements);
|
||||||
|
|
||||||
|
static Map<String, List<Asset>> _groupAssets(
|
||||||
|
List<Asset> assets,
|
||||||
|
GroupAssetsBy groupBy,
|
||||||
|
) {
|
||||||
|
assets.sortByCompare<DateTime>(
|
||||||
|
(e) => e.createdAt,
|
||||||
|
(a, b) => b.compareTo(a),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (groupBy == GroupAssetsBy.day) {
|
||||||
|
return assets.groupListsBy(
|
||||||
|
(element) => DateFormat('y-MM-dd').format(element.createdAt.toLocal()),
|
||||||
|
);
|
||||||
|
} else if (groupBy == GroupAssetsBy.month) {
|
||||||
|
return assets.groupListsBy(
|
||||||
|
(element) => DateFormat('y-MM').format(element.createdAt.toLocal()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
static Future<RenderList> _processAssetGroupData(
|
static Future<RenderList> _processAssetGroupData(
|
||||||
_AssetGroupsToRenderListComputeParameters data,
|
_AssetGroupsToRenderListComputeParameters data,
|
||||||
) async {
|
) async {
|
||||||
final monthFormat = DateFormat(data.monthFormat);
|
final monthFormat = DateFormat(data.monthFormat);
|
||||||
final dayFormatSameYear = DateFormat(data.dayFormat);
|
final dayFormatSameYear = DateFormat(data.dayFormat);
|
||||||
final dayFormatOtherYear = DateFormat(data.dayFormatYear);
|
final dayFormatOtherYear = DateFormat(data.dayFormatYear);
|
||||||
final groups = data.groups;
|
final allAssets = data.assets;
|
||||||
final perRow = data.perRow;
|
final perRow = data.layout.perRow;
|
||||||
|
final dynamicLayout = data.layout.dynamicLayout;
|
||||||
|
final groupBy = data.layout.groupBy;
|
||||||
|
|
||||||
List<RenderAssetGridElement> elements = [];
|
List<RenderAssetGridElement> elements = [];
|
||||||
DateTime? lastDate;
|
DateTime? lastDate;
|
||||||
|
|
||||||
|
final groups = _groupAssets(allAssets, groupBy);
|
||||||
|
|
||||||
groups.forEach((groupName, assets) {
|
groups.forEach((groupName, assets) {
|
||||||
try {
|
try {
|
||||||
final date = DateTime.parse(groupName);
|
final date = assets.first.createdAt.toLocal();
|
||||||
|
|
||||||
if (lastDate == null || lastDate!.month != date.month) {
|
|
||||||
// Month title
|
|
||||||
|
|
||||||
var monthTitleText = groupName;
|
|
||||||
|
|
||||||
var groupDate = DateTime.tryParse(groupName);
|
|
||||||
if (groupDate != null) {
|
|
||||||
monthTitleText = monthFormat.format(groupDate);
|
|
||||||
} else {
|
|
||||||
log.severe("Failed to format date for day title: $groupName");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Month title
|
||||||
|
if (groupBy == GroupAssetsBy.day &&
|
||||||
|
(lastDate == null || lastDate!.month != date.month)) {
|
||||||
elements.add(
|
elements.add(
|
||||||
RenderAssetGridElement(
|
RenderAssetGridElement(
|
||||||
RenderAssetGridElementType.monthTitle,
|
RenderAssetGridElementType.monthTitle,
|
||||||
title: monthTitleText,
|
title: monthFormat.format(date),
|
||||||
date: date,
|
date: date,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add group title
|
// Group divider title (day or month)
|
||||||
var currentYear = DateTime.now().year;
|
var formatDate = dayFormatOtherYear;
|
||||||
var groupYear = DateTime.parse(groupName).year;
|
|
||||||
var formatDate =
|
|
||||||
currentYear == groupYear ? dayFormatSameYear : dayFormatOtherYear;
|
|
||||||
|
|
||||||
var dateText = groupName;
|
if (DateTime.now().year == date.year) {
|
||||||
|
formatDate = dayFormatSameYear;
|
||||||
|
}
|
||||||
|
|
||||||
var groupDate = DateTime.tryParse(groupName);
|
if (groupBy == GroupAssetsBy.month) {
|
||||||
if (groupDate != null) {
|
formatDate = monthFormat;
|
||||||
dateText = formatDate.format(groupDate);
|
|
||||||
} else {
|
|
||||||
log.severe("Failed to format date for day title: $groupName");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
elements.add(
|
elements.add(
|
||||||
RenderAssetGridElement(
|
RenderAssetGridElement(
|
||||||
RenderAssetGridElementType.dayTitle,
|
RenderAssetGridElementType.groupDividerTitle,
|
||||||
title: dateText,
|
title: formatDate.format(date),
|
||||||
date: date,
|
date: date,
|
||||||
relatedAssetList: assets,
|
relatedAssetList: assets,
|
||||||
),
|
),
|
||||||
@ -121,12 +153,37 @@ class RenderList {
|
|||||||
int cursor = 0;
|
int cursor = 0;
|
||||||
while (cursor < assets.length) {
|
while (cursor < assets.length) {
|
||||||
int rowElements = min(assets.length - cursor, perRow);
|
int rowElements = min(assets.length - cursor, perRow);
|
||||||
|
final rowAssets = assets.sublist(cursor, cursor + rowElements);
|
||||||
|
|
||||||
|
// Default: All assets have the same width
|
||||||
|
var widthDistribution = List.filled(rowElements, 1.0);
|
||||||
|
|
||||||
|
if (dynamicLayout) {
|
||||||
|
final aspectRatios =
|
||||||
|
rowAssets.map((e) => (e.width ?? 1) / (e.height ?? 1)).toList();
|
||||||
|
final meanAspectRatio = aspectRatios.sum / rowElements;
|
||||||
|
|
||||||
|
// 1: mean width
|
||||||
|
// 0.5: width < mean - threshold
|
||||||
|
// 1.5: width > mean + threshold
|
||||||
|
final arConfiguration = aspectRatios.map((e) {
|
||||||
|
if (e - meanAspectRatio > 0.3) return 1.5;
|
||||||
|
if (e - meanAspectRatio < -0.3) return 0.5;
|
||||||
|
return 1.0;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Normalize:
|
||||||
|
final sum = arConfiguration.sum;
|
||||||
|
widthDistribution =
|
||||||
|
arConfiguration.map((e) => (e * rowElements) / sum).toList();
|
||||||
|
}
|
||||||
|
|
||||||
final rowElement = RenderAssetGridElement(
|
final rowElement = RenderAssetGridElement(
|
||||||
RenderAssetGridElementType.assetRow,
|
RenderAssetGridElementType.assetRow,
|
||||||
date: date,
|
date: date,
|
||||||
assetRow: RenderAssetGridRow(
|
assetRow: RenderAssetGridRow(
|
||||||
assets.sublist(cursor, cursor + rowElements),
|
rowAssets,
|
||||||
|
widthDistribution,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -143,9 +200,9 @@ class RenderList {
|
|||||||
return RenderList(elements);
|
return RenderList(elements);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<RenderList> fromAssetGroups(
|
static Future<RenderList> fromAssets(
|
||||||
Map<String, List<Asset>> assetGroups,
|
List<Asset> assets,
|
||||||
int assetsPerRow,
|
AssetGridLayoutParameters layout,
|
||||||
) async {
|
) async {
|
||||||
// Compute only allows for one parameter. Therefore we pass all parameters in a map
|
// Compute only allows for one parameter. Therefore we pass all parameters in a map
|
||||||
return compute(
|
return compute(
|
||||||
@ -154,8 +211,8 @@ class RenderList {
|
|||||||
"monthly_title_text_date_format".tr(),
|
"monthly_title_text_date_format".tr(),
|
||||||
"daily_title_text_date".tr(),
|
"daily_title_text_date".tr(),
|
||||||
"daily_title_text_date_year".tr(),
|
"daily_title_text_date_year".tr(),
|
||||||
assetGroups,
|
assets,
|
||||||
assetsPerRow,
|
layout,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
class DailyTitleText extends ConsumerWidget {
|
class GroupDividerTitle extends ConsumerWidget {
|
||||||
const DailyTitleText({
|
const GroupDividerTitle({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.text,
|
required this.text,
|
||||||
required this.multiselectEnabled,
|
required this.multiselectEnabled,
|
@ -7,7 +7,7 @@ import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_image.dart';
|
|||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||||
import 'asset_grid_data_structure.dart';
|
import 'asset_grid_data_structure.dart';
|
||||||
import 'daily_title_text.dart';
|
import 'group_divider_title.dart';
|
||||||
import 'disable_multi_select_button.dart';
|
import 'disable_multi_select_button.dart';
|
||||||
import 'draggable_scrollbar_custom.dart';
|
import 'draggable_scrollbar_custom.dart';
|
||||||
|
|
||||||
@ -99,12 +99,12 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
|
|||||||
widget.margin * (widget.assetsPerRow - 1) / widget.assetsPerRow;
|
widget.margin * (widget.assetsPerRow - 1) / widget.assetsPerRow;
|
||||||
return Row(
|
return Row(
|
||||||
key: Key("asset-row-${row.assets.first.id}"),
|
key: Key("asset-row-${row.assets.first.id}"),
|
||||||
children: row.assets.map((Asset asset) {
|
children: row.assets.mapIndexed((int index, Asset asset) {
|
||||||
bool last = asset.id == row.assets.last.id;
|
bool last = asset.id == row.assets.last.id;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
key: Key("asset-${asset.id}"),
|
key: Key("asset-${asset.id}"),
|
||||||
width: size,
|
width: size * row.widthDistribution[index],
|
||||||
height: size,
|
height: size,
|
||||||
margin: EdgeInsets.only(
|
margin: EdgeInsets.only(
|
||||||
top: widget.margin,
|
top: widget.margin,
|
||||||
@ -123,7 +123,7 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
|
|||||||
String title,
|
String title,
|
||||||
List<Asset> assets,
|
List<Asset> assets,
|
||||||
) {
|
) {
|
||||||
return DailyTitleText(
|
return GroupDividerTitle(
|
||||||
text: title,
|
text: title,
|
||||||
multiselectEnabled: widget.selectionActive,
|
multiselectEnabled: widget.selectionActive,
|
||||||
onSelect: () => _selectAssets(assets),
|
onSelect: () => _selectAssets(assets),
|
||||||
@ -150,7 +150,7 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
|
|||||||
Widget _itemBuilder(BuildContext c, int position) {
|
Widget _itemBuilder(BuildContext c, int position) {
|
||||||
final item = widget.renderList.elements[position];
|
final item = widget.renderList.elements[position];
|
||||||
|
|
||||||
if (item.type == RenderAssetGridElementType.dayTitle) {
|
if (item.type == RenderAssetGridElementType.groupDividerTitle) {
|
||||||
return _buildTitle(c, item.title!, item.relatedAssetList!);
|
return _buildTitle(c, item.title!, item.relatedAssetList!);
|
||||||
} else if (item.type == RenderAssetGridElementType.monthTitle) {
|
} else if (item.type == RenderAssetGridElementType.monthTitle) {
|
||||||
return _buildMonthTitle(c, item.title!);
|
return _buildMonthTitle(c, item.title!);
|
||||||
|
@ -220,8 +220,8 @@ class HomePage extends HookConsumerWidget {
|
|||||||
top: true,
|
top: true,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
ref.watch(assetProvider).renderList == null ||
|
ref.watch(assetProvider).renderList == null
|
||||||
ref.watch(assetProvider).allAssets.isEmpty
|
|| ref.watch(assetProvider).allAssets.isEmpty
|
||||||
? buildLoadingIndicator()
|
? buildLoadingIndicator()
|
||||||
: ImmichAssetGrid(
|
: ImmichAssetGrid(
|
||||||
renderList: ref.watch(assetProvider).renderList!,
|
renderList: ref.watch(assetProvider).renderList!,
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
|
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
|
||||||
import 'package:immich_mobile/modules/search/models/search_result_page_state.model.dart';
|
import 'package:immich_mobile/modules/search/models/search_result_page_state.model.dart';
|
||||||
@ -7,7 +6,6 @@ import 'package:immich_mobile/modules/search/services/search.service.dart';
|
|||||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
||||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
|
|
||||||
class SearchResultPageNotifier extends StateNotifier<SearchResultPageState> {
|
class SearchResultPageNotifier extends StateNotifier<SearchResultPageState> {
|
||||||
SearchResultPageNotifier(this._searchService)
|
SearchResultPageNotifier(this._searchService)
|
||||||
@ -56,23 +54,16 @@ final searchResultPageProvider =
|
|||||||
return SearchResultPageNotifier(ref.watch(searchServiceProvider));
|
return SearchResultPageNotifier(ref.watch(searchServiceProvider));
|
||||||
});
|
});
|
||||||
|
|
||||||
final searchResultGroupByDateTimeProvider = StateProvider((ref) {
|
|
||||||
var assets = ref.watch(searchResultPageProvider).searchResult;
|
|
||||||
|
|
||||||
assets.sortByCompare<DateTime>(
|
|
||||||
(e) => e.createdAt,
|
|
||||||
(a, b) => b.compareTo(a),
|
|
||||||
);
|
|
||||||
return assets.groupListsBy(
|
|
||||||
(element) => DateFormat('y-MM-dd').format(element.createdAt.toLocal()),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
final searchRenderListProvider = FutureProvider((ref) {
|
final searchRenderListProvider = FutureProvider((ref) {
|
||||||
var assetGroups = ref.watch(searchResultGroupByDateTimeProvider);
|
|
||||||
|
|
||||||
var settings = ref.watch(appSettingsServiceProvider);
|
var settings = ref.watch(appSettingsServiceProvider);
|
||||||
final assetsPerRow = settings.getSetting(AppSettingsEnum.tilesPerRow);
|
|
||||||
|
|
||||||
return RenderList.fromAssetGroups(assetGroups, assetsPerRow);
|
final assets = ref.watch(searchResultPageProvider).searchResult;
|
||||||
|
|
||||||
|
final layout = AssetGridLayoutParameters(
|
||||||
|
settings.getSetting(AppSettingsEnum.tilesPerRow),
|
||||||
|
settings.getSetting(AppSettingsEnum.dynamicLayout),
|
||||||
|
GroupAssetsBy.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)],
|
||||||
|
);
|
||||||
|
|
||||||
|
return RenderList.fromAssets(assets, layout);
|
||||||
});
|
});
|
||||||
|
@ -6,6 +6,8 @@ enum AppSettingsEnum<T> {
|
|||||||
loadOriginal<bool>("loadOriginal", false),
|
loadOriginal<bool>("loadOriginal", false),
|
||||||
themeMode<String>("themeMode", "system"), // "light","dark","system"
|
themeMode<String>("themeMode", "system"), // "light","dark","system"
|
||||||
tilesPerRow<int>("tilesPerRow", 4),
|
tilesPerRow<int>("tilesPerRow", 4),
|
||||||
|
dynamicLayout<bool>("dynamicLayout", false),
|
||||||
|
groupAssetsBy<int>("groupBy", 0),
|
||||||
uploadErrorNotificationGracePeriod<int>(
|
uploadErrorNotificationGracePeriod<int>(
|
||||||
"uploadErrorNotificationGracePeriod",
|
"uploadErrorNotificationGracePeriod",
|
||||||
2,
|
2,
|
||||||
|
@ -0,0 +1,99 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
|
||||||
|
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
||||||
|
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||||
|
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||||
|
|
||||||
|
class LayoutSettings extends HookConsumerWidget {
|
||||||
|
const LayoutSettings({
|
||||||
|
Key? key,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final appSettingService = ref.watch(appSettingsServiceProvider);
|
||||||
|
|
||||||
|
final useDynamicLayout = useState(true);
|
||||||
|
final groupBy = useState(GroupAssetsBy.day);
|
||||||
|
|
||||||
|
void switchChanged(bool value) {
|
||||||
|
appSettingService.setSetting(AppSettingsEnum.dynamicLayout, value);
|
||||||
|
useDynamicLayout.value = value;
|
||||||
|
ref.watch(assetProvider.notifier).rebuildAssetGridDataStructure();
|
||||||
|
}
|
||||||
|
|
||||||
|
void changeGroupValue(GroupAssetsBy? value) {
|
||||||
|
if (value != null) {
|
||||||
|
appSettingService.setSetting(AppSettingsEnum.groupAssetsBy, value.index);
|
||||||
|
groupBy.value = value;
|
||||||
|
ref.watch(assetProvider.notifier).rebuildAssetGridDataStructure();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() {
|
||||||
|
useDynamicLayout.value =
|
||||||
|
appSettingService.getSetting<bool>(AppSettingsEnum.dynamicLayout);
|
||||||
|
groupBy.value =
|
||||||
|
GroupAssetsBy.values[appSettingService.getSetting<int>(AppSettingsEnum.groupAssetsBy)];
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
SwitchListTile.adaptive(
|
||||||
|
activeColor: Theme.of(context).primaryColor,
|
||||||
|
title: const Text(
|
||||||
|
"asset_list_layout_settings_dynamic_layout_title",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
).tr(),
|
||||||
|
onChanged: switchChanged,
|
||||||
|
value: useDynamicLayout.value,
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
title: const Text(
|
||||||
|
"asset_list_layout_settings_group_by",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
).tr(),
|
||||||
|
),
|
||||||
|
RadioListTile(
|
||||||
|
activeColor: Theme.of(context).primaryColor,
|
||||||
|
title: const Text(
|
||||||
|
"asset_list_layout_settings_group_by_month_day",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
).tr(),
|
||||||
|
value: GroupAssetsBy.day,
|
||||||
|
groupValue: groupBy.value,
|
||||||
|
onChanged: changeGroupValue,
|
||||||
|
controlAffinity: ListTileControlAffinity.trailing,
|
||||||
|
),
|
||||||
|
RadioListTile(
|
||||||
|
activeColor: Theme.of(context).primaryColor,
|
||||||
|
title: const Text(
|
||||||
|
"asset_list_layout_settings_group_by_month",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
).tr(),
|
||||||
|
value: GroupAssetsBy.month,
|
||||||
|
groupValue: groupBy.value,
|
||||||
|
onChanged: changeGroupValue,
|
||||||
|
controlAffinity: ListTileControlAffinity.trailing,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:immich_mobile/modules/settings/ui/asset_list_settings/asset_list_layout_settings.dart';
|
||||||
import 'package:immich_mobile/modules/settings/ui/asset_list_settings/asset_list_storage_indicator.dart';
|
import 'package:immich_mobile/modules/settings/ui/asset_list_settings/asset_list_storage_indicator.dart';
|
||||||
import 'asset_list_tiles_per_row.dart';
|
import 'asset_list_tiles_per_row.dart';
|
||||||
|
|
||||||
@ -27,6 +28,7 @@ class AssetListSettings extends StatelessWidget {
|
|||||||
children: const [
|
children: const [
|
||||||
TilesPerRow(),
|
TilesPerRow(),
|
||||||
StorageIndicator(),
|
StorageIndicator(),
|
||||||
|
LayoutSettings(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -20,8 +20,7 @@ class StorageIndicator extends HookConsumerWidget {
|
|||||||
void switchChanged(bool value) {
|
void switchChanged(bool value) {
|
||||||
appSettingService.setSetting(AppSettingsEnum.storageIndicator, value);
|
appSettingService.setSetting(AppSettingsEnum.storageIndicator, value);
|
||||||
showStorageIndicator.value = value;
|
showStorageIndicator.value = value;
|
||||||
|
ref.watch(assetProvider.notifier).rebuildAssetGridDataStructure();
|
||||||
ref.invalidate(assetProvider);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
|
@ -23,8 +23,7 @@ class TilesPerRow extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void sliderChangedEnd(double _) {
|
void sliderChangedEnd(double _) {
|
||||||
ref.invalidate(assetProvider);
|
ref.watch(assetProvider.notifier).rebuildAssetGridDataStructure();
|
||||||
ref.watch(assetProvider.notifier).getAllAsset();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
|
@ -25,11 +25,15 @@ class AssetsState {
|
|||||||
|
|
||||||
AssetsState(this.allAssets, {this.renderList});
|
AssetsState(this.allAssets, {this.renderList});
|
||||||
|
|
||||||
Future<AssetsState> withRenderDataStructure(int groupSize) async {
|
Future<AssetsState> withRenderDataStructure(
|
||||||
|
AssetGridLayoutParameters layout,
|
||||||
|
) async {
|
||||||
return AssetsState(
|
return AssetsState(
|
||||||
allAssets,
|
allAssets,
|
||||||
renderList:
|
renderList: await RenderList.fromAssets(
|
||||||
await RenderList.fromAssetGroups(await _groupByDate(), groupSize),
|
allAssets,
|
||||||
|
layout,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -37,20 +41,6 @@ class AssetsState {
|
|||||||
return AssetsState([...allAssets, ...toAdd]);
|
return AssetsState([...allAssets, ...toAdd]);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, List<Asset>>> _groupByDate() async {
|
|
||||||
sortCompare(List<Asset> assets) {
|
|
||||||
assets.sortByCompare<DateTime>(
|
|
||||||
(e) => e.createdAt,
|
|
||||||
(a, b) => b.compareTo(a),
|
|
||||||
);
|
|
||||||
return assets.groupListsBy(
|
|
||||||
(element) => DateFormat('y-MM-dd').format(element.createdAt.toLocal()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await compute(sortCompare, allAssets.toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
static AssetsState fromAssetList(List<Asset> assets) {
|
static AssetsState fromAssetList(List<Asset> assets) {
|
||||||
return AssetsState(assets);
|
return AssetsState(assets);
|
||||||
}
|
}
|
||||||
@ -91,10 +81,19 @@ class AssetNotifier extends StateNotifier<AssetsState> {
|
|||||||
_assetCacheService.put(newAssetList);
|
_assetCacheService.put(newAssetList);
|
||||||
}
|
}
|
||||||
|
|
||||||
state =
|
final layout = AssetGridLayoutParameters(
|
||||||
await AssetsState.fromAssetList(newAssetList).withRenderDataStructure(
|
|
||||||
_settingsService.getSetting(AppSettingsEnum.tilesPerRow),
|
_settingsService.getSetting(AppSettingsEnum.tilesPerRow),
|
||||||
|
_settingsService.getSetting(AppSettingsEnum.dynamicLayout),
|
||||||
|
GroupAssetsBy.values[_settingsService.getSetting(AppSettingsEnum.groupAssetsBy)],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
state = await AssetsState.fromAssetList(newAssetList)
|
||||||
|
.withRenderDataStructure(layout);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Just a little helper to trigger a rebuild of the state object
|
||||||
|
Future<void> rebuildAssetGridDataStructure() async {
|
||||||
|
await _updateAssetsState(state.allAssets, cache: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
getAllAsset() async {
|
getAllAsset() async {
|
||||||
|
@ -25,46 +25,61 @@ void main() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final Map<String, List<Asset>> groups = {
|
final List<Asset> assets = [];
|
||||||
'2022-01-05': testAssets.sublist(0, 5).map((e) {
|
|
||||||
|
assets.addAll(
|
||||||
|
testAssets.sublist(0, 5).map((e) {
|
||||||
e.createdAt = DateTime(2022, 1, 5);
|
e.createdAt = DateTime(2022, 1, 5);
|
||||||
return e;
|
return e;
|
||||||
}).toList(),
|
}).toList(),
|
||||||
'2022-01-10': testAssets.sublist(5, 10).map((e) {
|
);
|
||||||
|
assets.addAll(
|
||||||
|
testAssets.sublist(5, 10).map((e) {
|
||||||
e.createdAt = DateTime(2022, 1, 10);
|
e.createdAt = DateTime(2022, 1, 10);
|
||||||
return e;
|
return e;
|
||||||
}).toList(),
|
}).toList(),
|
||||||
'2022-02-17': testAssets.sublist(10, 15).map((e) {
|
);
|
||||||
|
assets.addAll(
|
||||||
|
testAssets.sublist(10, 15).map((e) {
|
||||||
e.createdAt = DateTime(2022, 2, 17);
|
e.createdAt = DateTime(2022, 2, 17);
|
||||||
return e;
|
return e;
|
||||||
}).toList(),
|
}).toList(),
|
||||||
'2022-10-15': testAssets.sublist(15, 30).map((e) {
|
);
|
||||||
|
assets.addAll(
|
||||||
|
testAssets.sublist(15, 30).map((e) {
|
||||||
e.createdAt = DateTime(2022, 10, 15);
|
e.createdAt = DateTime(2022, 10, 15);
|
||||||
return e;
|
return e;
|
||||||
}).toList()
|
}).toList(),
|
||||||
};
|
);
|
||||||
|
|
||||||
group('Test grouped', () {
|
group('Test grouped', () {
|
||||||
test('test grouped check months', () async {
|
test('test grouped check months', () async {
|
||||||
final renderList = await RenderList.fromAssetGroups(groups, 3);
|
final renderList = await RenderList.fromAssets(
|
||||||
|
assets,
|
||||||
|
AssetGridLayoutParameters(
|
||||||
|
3,
|
||||||
|
false,
|
||||||
|
GroupAssetsBy.day,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
// Jan
|
|
||||||
// Day 1
|
|
||||||
// 5 Assets => 2 Rows
|
|
||||||
// Day 2
|
|
||||||
// 5 Assets => 2 Rows
|
|
||||||
// Feb
|
|
||||||
// Day 1
|
|
||||||
// 5 Assets => 2 Rows
|
|
||||||
// Oct
|
// Oct
|
||||||
// Day 1
|
// Day 1
|
||||||
// 15 Assets => 5 Rows
|
// 15 Assets => 5 Rows
|
||||||
|
// Feb
|
||||||
|
// Day 1
|
||||||
|
// 5 Assets => 2 Rows
|
||||||
|
// Jan
|
||||||
|
// Day 2
|
||||||
|
// 5 Assets => 2 Rows
|
||||||
|
// Day 1
|
||||||
|
// 5 Assets => 2 Rows
|
||||||
expect(renderList.elements.length, 18);
|
expect(renderList.elements.length, 18);
|
||||||
expect(
|
expect(
|
||||||
renderList.elements[0].type,
|
renderList.elements[0].type,
|
||||||
RenderAssetGridElementType.monthTitle,
|
RenderAssetGridElementType.monthTitle,
|
||||||
);
|
);
|
||||||
expect(renderList.elements[0].date.month, 1);
|
expect(renderList.elements[0].date.month, 10);
|
||||||
expect(
|
expect(
|
||||||
renderList.elements[7].type,
|
renderList.elements[7].type,
|
||||||
RenderAssetGridElementType.monthTitle,
|
RenderAssetGridElementType.monthTitle,
|
||||||
@ -74,38 +89,44 @@ void main() {
|
|||||||
renderList.elements[11].type,
|
renderList.elements[11].type,
|
||||||
RenderAssetGridElementType.monthTitle,
|
RenderAssetGridElementType.monthTitle,
|
||||||
);
|
);
|
||||||
expect(renderList.elements[11].date.month, 10);
|
expect(renderList.elements[11].date.month, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('test grouped check types', () async {
|
test('test grouped check types', () async {
|
||||||
final renderList = await RenderList.fromAssetGroups(groups, 5);
|
final renderList = await RenderList.fromAssets(
|
||||||
|
assets,
|
||||||
|
AssetGridLayoutParameters(
|
||||||
|
5,
|
||||||
|
false,
|
||||||
|
GroupAssetsBy.day,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
// Jan
|
|
||||||
// Day 1
|
|
||||||
// 5 Assets
|
|
||||||
// Day 2
|
|
||||||
// 5 Assets
|
|
||||||
// Feb
|
|
||||||
// Day 1
|
|
||||||
// 5 Assets
|
|
||||||
// Oct
|
// Oct
|
||||||
// Day 1
|
// Day 1
|
||||||
// 15 Assets => 3 Rows
|
// 15 Assets => 3 Rows
|
||||||
|
// Feb
|
||||||
|
// Day 1
|
||||||
|
// 5 Assets => 1 Row
|
||||||
|
// Jan
|
||||||
|
// Day 2
|
||||||
|
// 5 Assets => 1 Row
|
||||||
|
// Day 1
|
||||||
|
// 5 Assets => 1 Row
|
||||||
final types = [
|
final types = [
|
||||||
RenderAssetGridElementType.monthTitle,
|
RenderAssetGridElementType.monthTitle,
|
||||||
RenderAssetGridElementType.dayTitle,
|
RenderAssetGridElementType.groupDividerTitle,
|
||||||
|
RenderAssetGridElementType.assetRow,
|
||||||
RenderAssetGridElementType.assetRow,
|
RenderAssetGridElementType.assetRow,
|
||||||
RenderAssetGridElementType.dayTitle,
|
|
||||||
RenderAssetGridElementType.assetRow,
|
RenderAssetGridElementType.assetRow,
|
||||||
RenderAssetGridElementType.monthTitle,
|
RenderAssetGridElementType.monthTitle,
|
||||||
RenderAssetGridElementType.dayTitle,
|
RenderAssetGridElementType.groupDividerTitle,
|
||||||
RenderAssetGridElementType.assetRow,
|
RenderAssetGridElementType.assetRow,
|
||||||
RenderAssetGridElementType.monthTitle,
|
RenderAssetGridElementType.monthTitle,
|
||||||
RenderAssetGridElementType.dayTitle,
|
RenderAssetGridElementType.groupDividerTitle,
|
||||||
RenderAssetGridElementType.assetRow,
|
RenderAssetGridElementType.assetRow,
|
||||||
|
RenderAssetGridElementType.groupDividerTitle,
|
||||||
RenderAssetGridElementType.assetRow,
|
RenderAssetGridElementType.assetRow,
|
||||||
RenderAssetGridElementType.assetRow
|
|
||||||
];
|
];
|
||||||
|
|
||||||
expect(renderList.elements.length, types.length);
|
expect(renderList.elements.length, types.length);
|
||||||
|
Loading…
Reference in New Issue
Block a user