mirror of
https://github.com/KevinMidboe/immich.git
synced 2025-10-29 17:40:28 +00: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:
@@ -1,5 +1,6 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
@@ -9,14 +10,15 @@ final log = Logger('AssetGridDataStructure');
|
||||
|
||||
enum RenderAssetGridElementType {
|
||||
assetRow,
|
||||
dayTitle,
|
||||
groupDividerTitle,
|
||||
monthTitle;
|
||||
}
|
||||
|
||||
class RenderAssetGridRow {
|
||||
final List<Asset> assets;
|
||||
final List<double> widthDistribution;
|
||||
|
||||
RenderAssetGridRow(this.assets);
|
||||
RenderAssetGridRow(this.assets, this.widthDistribution);
|
||||
}
|
||||
|
||||
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 {
|
||||
final String monthFormat;
|
||||
final String dayFormat;
|
||||
final String dayFormatYear;
|
||||
final Map<String, List<Asset>> groups;
|
||||
final int perRow;
|
||||
final List<Asset> assets;
|
||||
final AssetGridLayoutParameters layout;
|
||||
|
||||
_AssetGroupsToRenderListComputeParameters(
|
||||
this.monthFormat,
|
||||
this.dayFormat,
|
||||
this.dayFormatYear,
|
||||
this.groups,
|
||||
this.perRow,
|
||||
this.assets,
|
||||
this.layout,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -56,62 +75,75 @@ class RenderList {
|
||||
|
||||
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(
|
||||
_AssetGroupsToRenderListComputeParameters data,
|
||||
) async {
|
||||
final monthFormat = DateFormat(data.monthFormat);
|
||||
final dayFormatSameYear = DateFormat(data.dayFormat);
|
||||
final dayFormatOtherYear = DateFormat(data.dayFormatYear);
|
||||
final groups = data.groups;
|
||||
final perRow = data.perRow;
|
||||
final allAssets = data.assets;
|
||||
final perRow = data.layout.perRow;
|
||||
final dynamicLayout = data.layout.dynamicLayout;
|
||||
final groupBy = data.layout.groupBy;
|
||||
|
||||
List<RenderAssetGridElement> elements = [];
|
||||
DateTime? lastDate;
|
||||
|
||||
final groups = _groupAssets(allAssets, groupBy);
|
||||
|
||||
groups.forEach((groupName, assets) {
|
||||
try {
|
||||
final date = DateTime.parse(groupName);
|
||||
|
||||
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");
|
||||
}
|
||||
final date = assets.first.createdAt.toLocal();
|
||||
|
||||
// Month title
|
||||
if (groupBy == GroupAssetsBy.day &&
|
||||
(lastDate == null || lastDate!.month != date.month)) {
|
||||
elements.add(
|
||||
RenderAssetGridElement(
|
||||
RenderAssetGridElementType.monthTitle,
|
||||
title: monthTitleText,
|
||||
title: monthFormat.format(date),
|
||||
date: date,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Add group title
|
||||
var currentYear = DateTime.now().year;
|
||||
var groupYear = DateTime.parse(groupName).year;
|
||||
var formatDate =
|
||||
currentYear == groupYear ? dayFormatSameYear : dayFormatOtherYear;
|
||||
// Group divider title (day or month)
|
||||
var formatDate = dayFormatOtherYear;
|
||||
|
||||
var dateText = groupName;
|
||||
if (DateTime.now().year == date.year) {
|
||||
formatDate = dayFormatSameYear;
|
||||
}
|
||||
|
||||
var groupDate = DateTime.tryParse(groupName);
|
||||
if (groupDate != null) {
|
||||
dateText = formatDate.format(groupDate);
|
||||
} else {
|
||||
log.severe("Failed to format date for day title: $groupName");
|
||||
if (groupBy == GroupAssetsBy.month) {
|
||||
formatDate = monthFormat;
|
||||
}
|
||||
|
||||
elements.add(
|
||||
RenderAssetGridElement(
|
||||
RenderAssetGridElementType.dayTitle,
|
||||
title: dateText,
|
||||
RenderAssetGridElementType.groupDividerTitle,
|
||||
title: formatDate.format(date),
|
||||
date: date,
|
||||
relatedAssetList: assets,
|
||||
),
|
||||
@@ -121,12 +153,37 @@ class RenderList {
|
||||
int cursor = 0;
|
||||
while (cursor < assets.length) {
|
||||
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(
|
||||
RenderAssetGridElementType.assetRow,
|
||||
date: date,
|
||||
assetRow: RenderAssetGridRow(
|
||||
assets.sublist(cursor, cursor + rowElements),
|
||||
rowAssets,
|
||||
widthDistribution,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -143,9 +200,9 @@ class RenderList {
|
||||
return RenderList(elements);
|
||||
}
|
||||
|
||||
static Future<RenderList> fromAssetGroups(
|
||||
Map<String, List<Asset>> assetGroups,
|
||||
int assetsPerRow,
|
||||
static Future<RenderList> fromAssets(
|
||||
List<Asset> assets,
|
||||
AssetGridLayoutParameters layout,
|
||||
) async {
|
||||
// Compute only allows for one parameter. Therefore we pass all parameters in a map
|
||||
return compute(
|
||||
@@ -154,8 +211,8 @@ class RenderList {
|
||||
"monthly_title_text_date_format".tr(),
|
||||
"daily_title_text_date".tr(),
|
||||
"daily_title_text_date_year".tr(),
|
||||
assetGroups,
|
||||
assetsPerRow,
|
||||
assets,
|
||||
layout,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
class DailyTitleText extends ConsumerWidget {
|
||||
const DailyTitleText({
|
||||
class GroupDividerTitle extends ConsumerWidget {
|
||||
const GroupDividerTitle({
|
||||
Key? key,
|
||||
required this.text,
|
||||
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:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
import 'asset_grid_data_structure.dart';
|
||||
import 'daily_title_text.dart';
|
||||
import 'group_divider_title.dart';
|
||||
import 'disable_multi_select_button.dart';
|
||||
import 'draggable_scrollbar_custom.dart';
|
||||
|
||||
@@ -99,12 +99,12 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
|
||||
widget.margin * (widget.assetsPerRow - 1) / widget.assetsPerRow;
|
||||
return Row(
|
||||
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;
|
||||
|
||||
return Container(
|
||||
key: Key("asset-${asset.id}"),
|
||||
width: size,
|
||||
width: size * row.widthDistribution[index],
|
||||
height: size,
|
||||
margin: EdgeInsets.only(
|
||||
top: widget.margin,
|
||||
@@ -123,7 +123,7 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
|
||||
String title,
|
||||
List<Asset> assets,
|
||||
) {
|
||||
return DailyTitleText(
|
||||
return GroupDividerTitle(
|
||||
text: title,
|
||||
multiselectEnabled: widget.selectionActive,
|
||||
onSelect: () => _selectAssets(assets),
|
||||
@@ -150,7 +150,7 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
|
||||
Widget _itemBuilder(BuildContext c, int position) {
|
||||
final item = widget.renderList.elements[position];
|
||||
|
||||
if (item.type == RenderAssetGridElementType.dayTitle) {
|
||||
if (item.type == RenderAssetGridElementType.groupDividerTitle) {
|
||||
return _buildTitle(c, item.title!, item.relatedAssetList!);
|
||||
} else if (item.type == RenderAssetGridElementType.monthTitle) {
|
||||
return _buildMonthTitle(c, item.title!);
|
||||
|
||||
@@ -220,8 +220,8 @@ class HomePage extends HookConsumerWidget {
|
||||
top: true,
|
||||
child: Stack(
|
||||
children: [
|
||||
ref.watch(assetProvider).renderList == null ||
|
||||
ref.watch(assetProvider).allAssets.isEmpty
|
||||
ref.watch(assetProvider).renderList == null
|
||||
|| ref.watch(assetProvider).allAssets.isEmpty
|
||||
? buildLoadingIndicator()
|
||||
: ImmichAssetGrid(
|
||||
renderList: ref.watch(assetProvider).renderList!,
|
||||
|
||||
Reference in New Issue
Block a user