diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index ecdbc6bf4..3d1517e99 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -171,8 +171,5 @@ "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", "experimental_settings_title": "Experimental", - "experimental_settings_subtitle": "Use at your own risk!", - "experimental_settings_new_asset_list_title": "Enable experimental photo grid", - "experimental_settings_new_asset_list_subtitle": "Work in progress", - "settings_require_restart": "Please restart Immich to apply this setting" + "experimental_settings_subtitle": "Use at your own risk!" } diff --git a/mobile/lib/modules/home/models/home_page_state.model.dart b/mobile/lib/modules/home/models/home_page_state.model.dart deleted file mode 100644 index 1701ac3ff..000000000 --- a/mobile/lib/modules/home/models/home_page_state.model.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'package:collection/collection.dart'; - -import 'package:openapi/api.dart'; - -class HomePageState { - final bool isMultiSelectEnable; - final Set selectedItems; - final Set selectedDateGroup; - HomePageState({ - required this.isMultiSelectEnable, - required this.selectedItems, - required this.selectedDateGroup, - }); - - HomePageState copyWith({ - bool? isMultiSelectEnable, - Set? selectedItems, - Set? selectedDateGroup, - }) { - return HomePageState( - isMultiSelectEnable: isMultiSelectEnable ?? this.isMultiSelectEnable, - selectedItems: selectedItems ?? this.selectedItems, - selectedDateGroup: selectedDateGroup ?? this.selectedDateGroup, - ); - } - - @override - String toString() => - 'HomePageState(isMultiSelectEnable: $isMultiSelectEnable, selectedItems: $selectedItems, selectedDateGroup: $selectedDateGroup)'; - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - final setEquals = const DeepCollectionEquality().equals; - - return other is HomePageState && - other.isMultiSelectEnable == isMultiSelectEnable && - setEquals(other.selectedItems, selectedItems) && - setEquals(other.selectedDateGroup, selectedDateGroup); - } - - @override - int get hashCode => - isMultiSelectEnable.hashCode ^ - selectedItems.hashCode ^ - selectedDateGroup.hashCode; -} diff --git a/mobile/lib/modules/home/providers/home_page_render_list_provider.dart b/mobile/lib/modules/home/providers/home_page_render_list_provider.dart index 276aef9f7..f97fd537e 100644 --- a/mobile/lib/modules/home/providers/home_page_render_list_provider.dart +++ b/mobile/lib/modules/home/providers/home_page_render_list_provider.dart @@ -1,95 +1,14 @@ -import 'dart:math'; - -import 'package:flutter/material.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'; -import 'package:openapi/api.dart'; - -enum RenderAssetGridElementType { - assetRow, - dayTitle, - monthTitle; -} - -class RenderAssetGridRow { - final List assets; - - RenderAssetGridRow(this.assets); -} - -class RenderAssetGridElement { - final RenderAssetGridElementType type; - final RenderAssetGridRow? assetRow; - final String? title; - final DateTime date; - final List? relatedAssetList; - - RenderAssetGridElement( - this.type, { - this.assetRow, - this.title, - required this.date, - this.relatedAssetList, - }); -} final renderListProvider = StateProvider((ref) { var assetGroups = ref.watch(assetGroupByDateTimeProvider); - var settings = ref.watch(appSettingsServiceProvider); + var settings = ref.watch(appSettingsServiceProvider); final assetsPerRow = settings.getSetting(AppSettingsEnum.tilesPerRow); - List elements = []; - DateTime? lastDate; - - assetGroups.forEach((groupName, assets) { - try { - final date = DateTime.parse(groupName); - - if (lastDate == null || lastDate!.month != date.month) { - elements.add( - RenderAssetGridElement( - RenderAssetGridElementType.monthTitle, - title: groupName, - date: date, - ), - ); - } - - // Add group title - elements.add( - RenderAssetGridElement( - RenderAssetGridElementType.dayTitle, - title: groupName, - date: date, - relatedAssetList: assets, - ), - ); - - // Add rows - int cursor = 0; - while (cursor < assets.length) { - int rowElements = min(assets.length - cursor, assetsPerRow); - - final rowElement = RenderAssetGridElement( - RenderAssetGridElementType.assetRow, - date: date, - assetRow: RenderAssetGridRow( - assets.sublist(cursor, cursor + rowElements), - ), - ); - - elements.add(rowElement); - cursor += rowElements; - } - - lastDate = date; - } catch (e) { - debugPrint(e.toString()); - } - }); - - return elements; + return assetGroupsToRenderList(assetGroups, assetsPerRow); }); diff --git a/mobile/lib/modules/home/providers/home_page_state.provider.dart b/mobile/lib/modules/home/providers/home_page_state.provider.dart deleted file mode 100644 index 26cee6d5a..000000000 --- a/mobile/lib/modules/home/providers/home_page_state.provider.dart +++ /dev/null @@ -1,90 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/modules/home/models/home_page_state.model.dart'; -import 'package:immich_mobile/shared/services/share.service.dart'; -import 'package:immich_mobile/shared/ui/share_dialog.dart'; -import 'package:openapi/api.dart'; - -class HomePageStateNotifier extends StateNotifier { - final ShareService _shareService; - - HomePageStateNotifier(this._shareService) - : super( - HomePageState( - isMultiSelectEnable: false, - selectedItems: {}, - selectedDateGroup: {}, - ), - ); - - void addSelectedDateGroup(String dateGroupTitle) { - state = state.copyWith( - selectedDateGroup: {...state.selectedDateGroup, dateGroupTitle}, - ); - } - - void removeSelectedDateGroup(String dateGroupTitle) { - var currentDateGroup = state.selectedDateGroup; - - currentDateGroup.removeWhere((e) => e == dateGroupTitle); - - state = state.copyWith(selectedDateGroup: currentDateGroup); - } - - void enableMultiSelect(Set selectedItems) { - state = - state.copyWith(isMultiSelectEnable: true, selectedItems: selectedItems); - } - - void disableMultiSelect() { - state = state.copyWith( - isMultiSelectEnable: false, - selectedItems: {}, - selectedDateGroup: {}, - ); - } - - void addSingleSelectedItem(AssetResponseDto asset) { - state = state.copyWith(selectedItems: {...state.selectedItems, asset}); - } - - void addMultipleSelectedItems(List assets) { - state = state.copyWith(selectedItems: {...state.selectedItems, ...assets}); - } - - void removeSingleSelectedItem(AssetResponseDto asset) { - Set currentList = state.selectedItems; - - currentList.removeWhere((e) => e.id == asset.id); - - state = state.copyWith(selectedItems: currentList); - } - - void removeMultipleSelectedItem(List assets) { - Set currentList = state.selectedItems; - - for (AssetResponseDto asset in assets) { - currentList.removeWhere((e) => e.id == asset.id); - } - - state = state.copyWith(selectedItems: currentList); - } - - void shareAssets(List assets, BuildContext context) { - showDialog( - context: context, - builder: (BuildContext buildContext) { - _shareService - .shareAssets(assets) - .then((_) => Navigator.of(buildContext).pop()); - return const ShareDialog(); - }, - barrierDismissible: false, - ); - } -} - -final homePageStateProvider = - StateNotifierProvider( - ((ref) => HomePageStateNotifier(ref.watch(shareServiceProvider))), -); diff --git a/mobile/lib/modules/home/providers/multiselect.provider.dart b/mobile/lib/modules/home/providers/multiselect.provider.dart new file mode 100644 index 000000000..22210e88c --- /dev/null +++ b/mobile/lib/modules/home/providers/multiselect.provider.dart @@ -0,0 +1,5 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +final multiselectProvider = StateProvider((ref) { + return false; +}); diff --git a/mobile/lib/modules/home/ui/asset_grid/asset_grid_data_structure.dart b/mobile/lib/modules/home/ui/asset_grid/asset_grid_data_structure.dart new file mode 100644 index 000000000..934385959 --- /dev/null +++ b/mobile/lib/modules/home/ui/asset_grid/asset_grid_data_structure.dart @@ -0,0 +1,103 @@ +import 'dart:math'; + +import 'package:openapi/api.dart'; + +enum RenderAssetGridElementType { + assetRow, + dayTitle, + monthTitle; +} + +class RenderAssetGridRow { + final List assets; + + RenderAssetGridRow(this.assets); +} + +class RenderAssetGridElement { + final RenderAssetGridElementType type; + final RenderAssetGridRow? assetRow; + final String? title; + final DateTime date; + final List? relatedAssetList; + + RenderAssetGridElement( + this.type, { + this.assetRow, + this.title, + required this.date, + this.relatedAssetList, + }); +} + +List assetsToRenderList( + List assets, int assetsPerRow) { + List elements = []; + + int cursor = 0; + while (cursor < assets.length) { + int rowElements = min(assets.length - cursor, assetsPerRow); + final date = DateTime.parse(assets[cursor].createdAt); + + final rowElement = RenderAssetGridElement( + RenderAssetGridElementType.assetRow, + date: date, + assetRow: RenderAssetGridRow( + assets.sublist(cursor, cursor + rowElements), + ), + ); + + elements.add(rowElement); + cursor += rowElements; + } + + return elements; +} + +List assetGroupsToRenderList( + Map> assetGroups, int assetsPerRow) { + List elements = []; + DateTime? lastDate; + + assetGroups.forEach((groupName, assets) { + final date = DateTime.parse(groupName); + + if (lastDate == null || lastDate!.month != date.month) { + elements.add( + RenderAssetGridElement(RenderAssetGridElementType.monthTitle, + title: groupName, date: date), + ); + } + + // Add group title + elements.add( + RenderAssetGridElement( + RenderAssetGridElementType.dayTitle, + title: groupName, + date: date, + relatedAssetList: assets, + ), + ); + + // Add rows + int cursor = 0; + while (cursor < assets.length) { + int rowElements = min(assets.length - cursor, assetsPerRow); + + final rowElement = RenderAssetGridElement( + RenderAssetGridElementType.assetRow, + date: date, + assetRow: RenderAssetGridRow( + assets.sublist(cursor, cursor + rowElements), + ), + ); + + elements.add(rowElement); + cursor += rowElements; + } + + lastDate = date; + }); + + return elements; +} diff --git a/mobile/lib/modules/home/ui/asset_grid/daily_title_text.dart b/mobile/lib/modules/home/ui/asset_grid/daily_title_text.dart new file mode 100644 index 000000000..78e033ce5 --- /dev/null +++ b/mobile/lib/modules/home/ui/asset_grid/daily_title_text.dart @@ -0,0 +1,72 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +class DailyTitleText extends ConsumerWidget { + const DailyTitleText({ + Key? key, + required this.isoDate, + required this.multiselectEnabled, + required this.onSelect, + required this.onDeselect, + required this.selected, + }) : super(key: key); + + final String isoDate; + final bool multiselectEnabled; + final Function onSelect; + final Function onDeselect; + final bool selected; + + @override + Widget build(BuildContext context, WidgetRef ref) { + var currentYear = DateTime.now().year; + var groupYear = DateTime.parse(isoDate).year; + var formatDateTemplate = currentYear == groupYear + ? "daily_title_text_date".tr() + : "daily_title_text_date_year".tr(); + var dateText = + DateFormat(formatDateTemplate).format(DateTime.parse(isoDate)); + + void handleTitleIconClick() { + if (selected) { + onDeselect(); + } else { + onSelect(); + } + } + + return Padding( + padding: const EdgeInsets.only( + top: 29.0, + bottom: 29.0, + left: 12.0, + right: 12.0, + ), + child: Row( + children: [ + Text( + dateText, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + const Spacer(), + GestureDetector( + onTap: handleTitleIconClick, + child: multiselectEnabled && selected + ? Icon( + Icons.check_circle_rounded, + color: Theme.of(context).primaryColor, + ) + : const Icon( + Icons.check_circle_outline_rounded, + color: Colors.grey, + ), + ) + ], + ), + ); + } +} diff --git a/mobile/lib/modules/home/ui/disable_multi_select_button.dart b/mobile/lib/modules/home/ui/asset_grid/disable_multi_select_button.dart similarity index 83% rename from mobile/lib/modules/home/ui/disable_multi_select_button.dart rename to mobile/lib/modules/home/ui/asset_grid/disable_multi_select_button.dart index 2d450832e..cca2e0b20 100644 --- a/mobile/lib/modules/home/ui/disable_multi_select_button.dart +++ b/mobile/lib/modules/home/ui/asset_grid/disable_multi_select_button.dart @@ -1,40 +1,36 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; - -class DisableMultiSelectButton extends ConsumerWidget { - const DisableMultiSelectButton({ - Key? key, - required this.onPressed, - required this.selectedItemCount, - }) : super(key: key); - - final Function onPressed; - final int selectedItemCount; - - @override - Widget build(BuildContext context, WidgetRef ref) { - return Positioned( - top: 10, - left: 0, - child: Padding( - padding: const EdgeInsets.only(left: 16.0, top: 46), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 4.0), - child: ElevatedButton.icon( - onPressed: () { - onPressed(); - }, - icon: const Icon(Icons.close_rounded), - label: Text( - '$selectedItemCount', - style: const TextStyle( - fontWeight: FontWeight.w600, - fontSize: 18, - ), - ), - ), - ), - ), - ); - } -} +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +class DisableMultiSelectButton extends ConsumerWidget { + const DisableMultiSelectButton({ + Key? key, + required this.onPressed, + required this.selectedItemCount, + }) : super(key: key); + + final Function onPressed; + final int selectedItemCount; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Padding( + padding: const EdgeInsets.only(left: 16.0, top: 15), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: ElevatedButton.icon( + onPressed: () { + onPressed(); + }, + icon: const Icon(Icons.close_rounded), + label: Text( + '$selectedItemCount', + style: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 18, + ), + ), + ), + ), + ); + } +} diff --git a/mobile/lib/modules/home/ui/asset_list_v2/draggable_scrollbar_custom.dart b/mobile/lib/modules/home/ui/asset_grid/draggable_scrollbar_custom.dart similarity index 100% rename from mobile/lib/modules/home/ui/asset_list_v2/draggable_scrollbar_custom.dart rename to mobile/lib/modules/home/ui/asset_grid/draggable_scrollbar_custom.dart diff --git a/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart b/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart new file mode 100644 index 000000000..98ca796a6 --- /dev/null +++ b/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart @@ -0,0 +1,274 @@ +import 'dart:collection'; + +import 'package:collection/collection.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_image.dart'; +import 'package:openapi/api.dart'; +import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; +import 'asset_grid_data_structure.dart'; +import 'daily_title_text.dart'; +import 'disable_multi_select_button.dart'; +import 'draggable_scrollbar_custom.dart'; + +typedef ImmichAssetGridSelectionListener = void Function( + bool, + Set, +); + +class ImmichAssetGridState extends State { + final ItemScrollController _itemScrollController = ItemScrollController(); + final ItemPositionsListener _itemPositionsListener = + ItemPositionsListener.create(); + + bool _scrolling = false; + final Set _selectedAssets = HashSet(); + + List get _assets { + return widget.renderList + .map((e) { + if (e.type == RenderAssetGridElementType.assetRow) { + return e.assetRow!.assets; + } else { + return List.empty(); + } + }) + .flattened + .toList(); + } + + Set _getSelectedAssets() { + return _selectedAssets + .map((e) => _assets.firstWhereOrNull((a) => a.id == e)) + .whereNotNull() + .toSet(); + } + + void _callSelectionListener(bool selectionActive) { + widget.listener?.call(selectionActive, _getSelectedAssets()); + } + + void _selectAssets(List assets) { + setState(() { + for (var e in assets) { + _selectedAssets.add(e.id); + } + _callSelectionListener(true); + }); + } + + void _deselectAssets(List assets) { + setState(() { + for (var e in assets) { + _selectedAssets.remove(e.id); + } + _callSelectionListener(_selectedAssets.isNotEmpty); + }); + } + + void _deselectAll() { + setState(() { + _selectedAssets.clear(); + }); + + _callSelectionListener(false); + } + + bool _allAssetsSelected(List assets) { + return widget.selectionActive && + assets.firstWhereOrNull((e) => !_selectedAssets.contains(e.id)) == null; + } + + double _getItemSize(BuildContext context) { + return MediaQuery.of(context).size.width / widget.assetsPerRow - + widget.margin * (widget.assetsPerRow - 1) / widget.assetsPerRow; + } + + Widget _buildThumbnailOrPlaceholder( + AssetResponseDto asset, + bool placeholder, + ) { + if (placeholder) { + return const DecoratedBox( + decoration: BoxDecoration(color: Colors.grey), + ); + } + return ThumbnailImage( + asset: asset, + assetList: _assets, + multiselectEnabled: widget.selectionActive, + isSelected: _selectedAssets.contains(asset.id), + onSelect: () => _selectAssets([asset]), + onDeselect: () => _deselectAssets([asset]), + useGrayBoxPlaceholder: true, + showStorageIndicator: widget.showStorageIndicator, + ); + } + + Widget _buildAssetRow( + BuildContext context, + RenderAssetGridRow row, + bool scrolling, + ) { + double size = _getItemSize(context); + + return Row( + key: Key("asset-row-${row.assets.first.id}"), + children: row.assets.map((AssetResponseDto asset) { + bool last = asset == row.assets.last; + + return Container( + key: Key("asset-${asset.id}"), + width: size, + height: size, + margin: EdgeInsets.only( + top: widget.margin, + right: last ? 0.0 : widget.margin, + ), + child: _buildThumbnailOrPlaceholder(asset, scrolling), + ); + }).toList(), + ); + } + + Widget _buildTitle( + BuildContext context, + String title, + List assets, + ) { + return DailyTitleText( + isoDate: title, + multiselectEnabled: widget.selectionActive, + onSelect: () => _selectAssets(assets), + onDeselect: () => _deselectAssets(assets), + selected: _allAssetsSelected(assets), + ); + } + + Widget _buildMonthTitle(BuildContext context, String title) { + var monthTitleText = DateFormat("monthly_title_text_date_format".tr()) + .format(DateTime.parse(title)); + + return Padding( + key: Key("month-$title"), + padding: const EdgeInsets.only(left: 12.0, top: 32), + child: Text( + monthTitleText, + style: TextStyle( + fontSize: 26, + fontWeight: FontWeight.bold, + color: Theme.of(context).textTheme.headline1?.color, + ), + ), + ); + } + + Widget _itemBuilder(BuildContext c, int position) { + final item = widget.renderList[position]; + + if (item.type == RenderAssetGridElementType.dayTitle) { + return _buildTitle(c, item.title!, item.relatedAssetList!); + } else if (item.type == RenderAssetGridElementType.monthTitle) { + return _buildMonthTitle(c, item.title!); + } else if (item.type == RenderAssetGridElementType.assetRow) { + return _buildAssetRow(c, item.assetRow!, _scrolling); + } + + return const Text("Invalid widget type!"); + } + + Text _labelBuilder(int pos) { + final date = widget.renderList[pos].date; + return Text( + DateFormat.yMMMd().format(date), + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ); + } + + Widget _buildMultiSelectIndicator() { + return DisableMultiSelectButton( + onPressed: () => _deselectAll(), + selectedItemCount: _selectedAssets.length, + ); + } + + Widget _buildAssetGrid() { + final useDragScrolling = _assets.length >= 20; + + void dragScrolling(bool active) { + setState(() { + _scrolling = active; + }); + } + + final listWidget = ScrollablePositionedList.builder( + itemBuilder: _itemBuilder, + itemPositionsListener: _itemPositionsListener, + itemScrollController: _itemScrollController, + itemCount: widget.renderList.length, + ); + + if (!useDragScrolling) { + return listWidget; + } + + return DraggableScrollbar.semicircle( + scrollStateListener: dragScrolling, + itemPositionsListener: _itemPositionsListener, + controller: _itemScrollController, + backgroundColor: Theme.of(context).hintColor, + labelTextBuilder: _labelBuilder, + labelConstraints: const BoxConstraints(maxHeight: 28), + scrollbarAnimationDuration: const Duration(seconds: 1), + scrollbarTimeToFade: const Duration(seconds: 4), + child: listWidget, + ); + } + + @override + void didUpdateWidget(ImmichAssetGrid oldWidget) { + super.didUpdateWidget(oldWidget); + if (!widget.selectionActive) { + setState(() { + _selectedAssets.clear(); + }); + } + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + _buildAssetGrid(), + if (widget.selectionActive) _buildMultiSelectIndicator(), + ], + ); + } +} + +class ImmichAssetGrid extends StatefulWidget { + final List renderList; + final int assetsPerRow; + final double margin; + final bool showStorageIndicator; + final ImmichAssetGridSelectionListener? listener; + final bool selectionActive; + + const ImmichAssetGrid({ + super.key, + required this.renderList, + required this.assetsPerRow, + required this.showStorageIndicator, + this.listener, + this.margin = 5.0, + this.selectionActive = false, + }); + + @override + State createState() { + return ImmichAssetGridState(); + } +} diff --git a/mobile/lib/modules/home/ui/monthly_title_text.dart b/mobile/lib/modules/home/ui/asset_grid/monthly_title_text.dart similarity index 100% rename from mobile/lib/modules/home/ui/monthly_title_text.dart rename to mobile/lib/modules/home/ui/asset_grid/monthly_title_text.dart diff --git a/mobile/lib/modules/home/ui/thumbnail_image.dart b/mobile/lib/modules/home/ui/asset_grid/thumbnail_image.dart similarity index 79% rename from mobile/lib/modules/home/ui/thumbnail_image.dart rename to mobile/lib/modules/home/ui/asset_grid/thumbnail_image.dart index 1eb165f62..fecdf66eb 100644 --- a/mobile/lib/modules/home/ui/thumbnail_image.dart +++ b/mobile/lib/modules/home/ui/asset_grid/thumbnail_image.dart @@ -1,176 +1,172 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:hive_flutter/hive_flutter.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/hive_box.dart'; -import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart'; -import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/utils/image_url_builder.dart'; -import 'package:openapi/api.dart'; - -class ThumbnailImage extends HookConsumerWidget { - final AssetResponseDto asset; - final List assetList; - final bool showStorageIndicator; - final bool useGrayBoxPlaceholder; - - const ThumbnailImage({ - Key? key, - required this.asset, - required this.assetList, - this.showStorageIndicator = true, - this.useGrayBoxPlaceholder = false, - }) : super(key: key); - - @override - Widget build(BuildContext context, WidgetRef ref) { - var box = Hive.box(userInfoBox); - var thumbnailRequestUrl = getThumbnailUrl(asset); - var selectedAsset = ref.watch(homePageStateProvider).selectedItems; - var isMultiSelectEnable = - ref.watch(homePageStateProvider).isMultiSelectEnable; - var deviceId = ref.watch(authenticationProvider).deviceId; - - Widget buildSelectionIcon(AssetResponseDto asset) { - if (selectedAsset.contains(asset)) { - return Icon( - Icons.check_circle, - color: Theme.of(context).primaryColor, - ); - } else { - return const Icon( - Icons.circle_outlined, - color: Colors.white, - ); - } - } - - return GestureDetector( - onTap: () { - if (isMultiSelectEnable && - selectedAsset.contains(asset) && - selectedAsset.length == 1) { - ref.watch(homePageStateProvider.notifier).disableMultiSelect(); - } else if (isMultiSelectEnable && - selectedAsset.contains(asset) && - selectedAsset.length > 1) { - ref - .watch(homePageStateProvider.notifier) - .removeSingleSelectedItem(asset); - } else if (isMultiSelectEnable && !selectedAsset.contains(asset)) { - ref - .watch(homePageStateProvider.notifier) - .addSingleSelectedItem(asset); - } else { - AutoRouter.of(context).push( - GalleryViewerRoute( - assetList: assetList, - asset: asset, - ), - ); - } - }, - onLongPress: () { - // Enable multi select function - ref.watch(homePageStateProvider.notifier).enableMultiSelect({asset}); - HapticFeedback.heavyImpact(); - }, - child: Hero( - tag: asset.id, - child: Stack( - children: [ - Container( - decoration: BoxDecoration( - border: isMultiSelectEnable && selectedAsset.contains(asset) - ? Border.all( - color: Theme.of(context).primaryColorLight, - width: 10, - ) - : const Border(), - ), - child: CachedNetworkImage( - cacheKey: 'thumbnail-image-${asset.id}', - width: 300, - height: 300, - memCacheHeight: 200, - maxWidthDiskCache: 200, - maxHeightDiskCache: 200, - fit: BoxFit.cover, - imageUrl: thumbnailRequestUrl, - httpHeaders: { - "Authorization": "Bearer ${box.get(accessTokenKey)}" - }, - fadeInDuration: const Duration(milliseconds: 250), - progressIndicatorBuilder: (context, url, downloadProgress) { - if (useGrayBoxPlaceholder) { - return const DecoratedBox( - decoration: BoxDecoration(color: Colors.grey), - ); - } - return Transform.scale( - scale: 0.2, - child: CircularProgressIndicator( - value: downloadProgress.progress, - ), - ); - }, - errorWidget: (context, url, error) { - debugPrint("Error getting thumbnail $url = $error"); - CachedNetworkImage.evictFromCache(thumbnailRequestUrl); - - return Icon( - Icons.image_not_supported_outlined, - color: Theme.of(context).primaryColor, - ); - }, - ), - ), - if (isMultiSelectEnable) - Padding( - padding: const EdgeInsets.all(3.0), - child: Align( - alignment: Alignment.topLeft, - child: buildSelectionIcon(asset), - ), - ), - if (showStorageIndicator) - Positioned( - right: 10, - bottom: 5, - child: Icon( - (deviceId != asset.deviceId) - ? Icons.cloud_done_outlined - : Icons.photo_library_rounded, - color: Colors.white, - size: 18, - ), - ), - if (asset.type != AssetTypeEnum.IMAGE) - Positioned( - top: 5, - right: 5, - child: Row( - children: [ - Text( - asset.duration.toString().substring(0, 7), - style: const TextStyle( - color: Colors.white, - fontSize: 10, - ), - ), - const Icon( - Icons.play_circle_outline_rounded, - color: Colors.white, - ), - ], - ), - ), - ], - ), - ), - ); - } -} +import 'package:auto_route/auto_route.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:hive_flutter/hive_flutter.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/hive_box.dart'; +import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/utils/image_url_builder.dart'; +import 'package:openapi/api.dart'; + +class ThumbnailImage extends HookConsumerWidget { + final AssetResponseDto asset; + final List assetList; + final bool showStorageIndicator; + final bool useGrayBoxPlaceholder; + final bool isSelected; + final bool multiselectEnabled; + final Function? onSelect; + final Function? onDeselect; + + const ThumbnailImage({ + Key? key, + required this.asset, + required this.assetList, + this.showStorageIndicator = true, + this.useGrayBoxPlaceholder = false, + this.isSelected = false, + this.multiselectEnabled = false, + this.onDeselect, + this.onSelect, + }) : super(key: key); + + @override + Widget build(BuildContext context, WidgetRef ref) { + var box = Hive.box(userInfoBox); + var thumbnailRequestUrl = getThumbnailUrl(asset); + var deviceId = ref.watch(authenticationProvider).deviceId; + + + Widget buildSelectionIcon(AssetResponseDto asset) { + if (isSelected) { + return Icon( + Icons.check_circle, + color: Theme.of(context).primaryColor, + ); + } else { + return const Icon( + Icons.circle_outlined, + color: Colors.white, + ); + } + } + + return GestureDetector( + onTap: () { + if (multiselectEnabled) { + if (isSelected) { + onDeselect?.call(); + } else { + onSelect?.call(); + } + } else { + AutoRouter.of(context).push( + GalleryViewerRoute( + assetList: assetList, + asset: asset, + ), + ); + } + }, + onLongPress: () { + onSelect?.call(); + HapticFeedback.heavyImpact(); + }, + child: Hero( + tag: asset.id, + child: Stack( + children: [ + Container( + decoration: BoxDecoration( + border: multiselectEnabled && isSelected + ? Border.all( + color: Theme.of(context).primaryColorLight, + width: 10, + ) + : const Border(), + ), + child: CachedNetworkImage( + cacheKey: 'thumbnail-image-${asset.id}', + width: 300, + height: 300, + memCacheHeight: 200, + maxWidthDiskCache: 200, + maxHeightDiskCache: 200, + fit: BoxFit.cover, + imageUrl: thumbnailRequestUrl, + httpHeaders: { + "Authorization": "Bearer ${box.get(accessTokenKey)}" + }, + fadeInDuration: const Duration(milliseconds: 250), + progressIndicatorBuilder: (context, url, downloadProgress) { + if (useGrayBoxPlaceholder) { + return const DecoratedBox( + decoration: BoxDecoration(color: Colors.grey), + ); + } + return Transform.scale( + scale: 0.2, + child: CircularProgressIndicator( + value: downloadProgress.progress, + ), + ); + }, + errorWidget: (context, url, error) { + debugPrint("Error getting thumbnail $url = $error"); + CachedNetworkImage.evictFromCache(thumbnailRequestUrl); + + return Icon( + Icons.image_not_supported_outlined, + color: Theme.of(context).primaryColor, + ); + }, + ), + ), + if (multiselectEnabled) + Padding( + padding: const EdgeInsets.all(3.0), + child: Align( + alignment: Alignment.topLeft, + child: buildSelectionIcon(asset), + ), + ), + if (showStorageIndicator) + Positioned( + right: 10, + bottom: 5, + child: Icon( + (deviceId != asset.deviceId) + ? Icons.cloud_done_outlined + : Icons.photo_library_rounded, + color: Colors.white, + size: 18, + ), + ), + if (asset.type != AssetTypeEnum.IMAGE) + Positioned( + top: 5, + right: 5, + child: Row( + children: [ + Text( + asset.duration.toString().substring(0, 7), + style: const TextStyle( + color: Colors.white, + fontSize: 10, + ), + ), + const Icon( + Icons.play_circle_outline_rounded, + color: Colors.white, + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/mobile/lib/modules/home/ui/asset_list_v2/daily_title_text.dart b/mobile/lib/modules/home/ui/asset_list_v2/daily_title_text.dart deleted file mode 100644 index e16bea2b3..000000000 --- a/mobile/lib/modules/home/ui/asset_list_v2/daily_title_text.dart +++ /dev/null @@ -1,107 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart'; -import 'package:openapi/api.dart'; - -class DailyTitleText extends ConsumerWidget { - const DailyTitleText({ - Key? key, - required this.isoDate, - required this.assetGroup, - }) : super(key: key); - - final String isoDate; - final List assetGroup; - - @override - Widget build(BuildContext context, WidgetRef ref) { - var currentYear = DateTime.now().year; - var groupYear = DateTime.parse(isoDate).year; - var formatDateTemplate = currentYear == groupYear - ? "daily_title_text_date".tr() - : "daily_title_text_date_year".tr(); - var dateText = - DateFormat(formatDateTemplate).format(DateTime.parse(isoDate)); - var isMultiSelectEnable = - ref.watch(homePageStateProvider).isMultiSelectEnable; - var selectedDateGroup = ref.watch(homePageStateProvider).selectedDateGroup; - var selectedItems = ref.watch(homePageStateProvider).selectedItems; - - void _handleTitleIconClick() { - if (isMultiSelectEnable && - selectedDateGroup.contains(dateText) && - selectedDateGroup.length == 1 && - selectedItems.length <= assetGroup.length) { - // Multi select is active - click again on the icon while it is the only active group -> disable multi select - ref.watch(homePageStateProvider.notifier).disableMultiSelect(); - } else if (isMultiSelectEnable && - selectedDateGroup.contains(dateText) && - selectedItems.length != assetGroup.length) { - // Multi select is active - click again on the icon while it is not the only active group -> remove that group from selected group/items - ref - .watch(homePageStateProvider.notifier) - .removeSelectedDateGroup(dateText); - ref - .watch(homePageStateProvider.notifier) - .removeMultipleSelectedItem(assetGroup); - } else if (isMultiSelectEnable && - selectedDateGroup.contains(dateText) && - selectedDateGroup.length > 1) { - ref - .watch(homePageStateProvider.notifier) - .removeSelectedDateGroup(dateText); - ref - .watch(homePageStateProvider.notifier) - .removeMultipleSelectedItem(assetGroup); - } else if (isMultiSelectEnable && !selectedDateGroup.contains(dateText)) { - ref - .watch(homePageStateProvider.notifier) - .addSelectedDateGroup(dateText); - ref - .watch(homePageStateProvider.notifier) - .addMultipleSelectedItems(assetGroup); - } else { - ref - .watch(homePageStateProvider.notifier) - .enableMultiSelect(assetGroup.toSet()); - ref - .watch(homePageStateProvider.notifier) - .addSelectedDateGroup(dateText); - } - } - - return Padding( - padding: const EdgeInsets.only( - top: 29.0, - bottom: 29.0, - left: 12.0, - right: 12.0, - ), - child: Row( - children: [ - Text( - dateText, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - ), - ), - const Spacer(), - GestureDetector( - onTap: _handleTitleIconClick, - child: isMultiSelectEnable && selectedDateGroup.contains(dateText) - ? Icon( - Icons.check_circle_rounded, - color: Theme.of(context).primaryColor, - ) - : const Icon( - Icons.check_circle_outline_rounded, - color: Colors.grey, - ), - ) - ], - ), - ); - } -} diff --git a/mobile/lib/modules/home/ui/asset_list_v2/immich_asset_grid.dart b/mobile/lib/modules/home/ui/asset_list_v2/immich_asset_grid.dart deleted file mode 100644 index 6a935fb5e..000000000 --- a/mobile/lib/modules/home/ui/asset_list_v2/immich_asset_grid.dart +++ /dev/null @@ -1,173 +0,0 @@ -import 'package:collection/collection.dart'; -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/providers/home_page_render_list_provider.dart'; -import 'package:immich_mobile/modules/home/ui/asset_list_v2/daily_title_text.dart'; -import 'package:immich_mobile/modules/home/ui/asset_list_v2/draggable_scrollbar_custom.dart'; -import 'package:openapi/api.dart'; -import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; - -import '../thumbnail_image.dart'; - -class ImmichAssetGrid extends HookConsumerWidget { - final ItemScrollController _itemScrollController = ItemScrollController(); - final ItemPositionsListener _itemPositionsListener = - ItemPositionsListener.create(); - - final List renderList; - final int assetsPerRow; - final double margin; - final bool showStorageIndicator; - - ImmichAssetGrid({ - super.key, - required this.renderList, - required this.assetsPerRow, - required this.showStorageIndicator, - this.margin = 5.0, - }); - - List get _assets { - return renderList - .map((e) { - if (e.type == RenderAssetGridElementType.assetRow) { - return e.assetRow!.assets; - } else { - return List.empty(); - } - }) - .flattened - .toList(); - } - - double _getItemSize(BuildContext context) { - return MediaQuery.of(context).size.width / assetsPerRow - - margin * (assetsPerRow - 1) / assetsPerRow; - } - - Widget _buildThumbnailOrPlaceholder( - AssetResponseDto asset, - bool placeholder, - ) { - if (placeholder) { - return const DecoratedBox( - decoration: BoxDecoration(color: Colors.grey), - ); - } - return ThumbnailImage( - asset: asset, - assetList: _assets, - showStorageIndicator: showStorageIndicator, - useGrayBoxPlaceholder: true, - ); - } - - Widget _buildAssetRow( - BuildContext context, - RenderAssetGridRow row, - bool scrolling, - ) { - double size = _getItemSize(context); - - return Row( - key: Key("asset-row-${row.assets.first.id}"), - children: row.assets.map((AssetResponseDto asset) { - bool last = asset == row.assets.last; - - return Container( - key: Key("asset-${asset.id}"), - width: size, - height: size, - margin: EdgeInsets.only(top: margin, right: last ? 0.0 : margin), - child: _buildThumbnailOrPlaceholder(asset, scrolling), - ); - }).toList(), - ); - } - - Widget _buildTitle( - BuildContext context, - String title, - List assets, - ) { - return DailyTitleText( - isoDate: title, - assetGroup: assets, - ); - } - - Widget _buildMonthTitle(BuildContext context, String title) { - var monthTitleText = DateFormat("monthly_title_text_date_format".tr()) - .format(DateTime.parse(title)); - - return Padding( - key: Key("month-$title"), - padding: const EdgeInsets.only(left: 12.0, top: 32), - child: Text( - monthTitleText, - style: TextStyle( - fontSize: 26, - fontWeight: FontWeight.bold, - color: Theme.of(context).textTheme.headline1?.color, - ), - ), - ); - } - - Widget _itemBuilder(BuildContext c, int position, bool scrolling) { - final item = renderList[position]; - - if (item.type == RenderAssetGridElementType.dayTitle) { - return _buildTitle(c, item.title!, item.relatedAssetList!); - } else if (item.type == RenderAssetGridElementType.monthTitle) { - return _buildMonthTitle(c, item.title!); - } else if (item.type == RenderAssetGridElementType.assetRow) { - return _buildAssetRow(c, item.assetRow!, scrolling); - } - - return const Text("Invalid widget type!"); - } - - Text _labelBuilder(int pos) { - final date = renderList[pos].date; - return Text( - DateFormat.yMMMd().format(date), - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - ), - ); - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - final scrolling = useState(false); - - void dragScrolling(bool active) { - scrolling.value = active; - } - - Widget itemBuilder(BuildContext c, int position) { - return _itemBuilder(c, position, scrolling.value); - } - - return DraggableScrollbar.semicircle( - scrollStateListener: dragScrolling, - itemPositionsListener: _itemPositionsListener, - controller: _itemScrollController, - backgroundColor: Theme.of(context).hintColor, - labelTextBuilder: _labelBuilder, - labelConstraints: const BoxConstraints(maxHeight: 28), - scrollbarAnimationDuration: const Duration(seconds: 1), - scrollbarTimeToFade: const Duration(seconds: 4), - child: ScrollablePositionedList.builder( - itemBuilder: itemBuilder, - itemPositionsListener: _itemPositionsListener, - itemScrollController: _itemScrollController, - itemCount: renderList.length, - ), - ); - } -} diff --git a/mobile/lib/modules/home/ui/control_bottom_app_bar.dart b/mobile/lib/modules/home/ui/control_bottom_app_bar.dart index 875647e73..dceaea4ac 100644 --- a/mobile/lib/modules/home/ui/control_bottom_app_bar.dart +++ b/mobile/lib/modules/home/ui/control_bottom_app_bar.dart @@ -1,11 +1,15 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart'; import 'package:immich_mobile/modules/home/ui/delete_diaglog.dart'; class ControlBottomAppBar extends ConsumerWidget { - const ControlBottomAppBar({Key? key}) : super(key: key); + final Function onShare; + final Function onDelete; + + const ControlBottomAppBar( + {Key? key, required this.onShare, required this.onDelete}) + : super(key: key); @override Widget build(BuildContext context, WidgetRef ref) { @@ -36,7 +40,9 @@ class ControlBottomAppBar extends ConsumerWidget { showDialog( context: context, builder: (BuildContext context) { - return const DeleteDialog(); + return DeleteDialog( + onDelete: onDelete, + ); }, ); }, @@ -45,14 +51,7 @@ class ControlBottomAppBar extends ConsumerWidget { iconData: Icons.share, label: "control_bottom_app_bar_share".tr(), onPressed: () { - final homePageState = ref.watch(homePageStateProvider); - ref.watch(homePageStateProvider.notifier).shareAssets( - homePageState.selectedItems.toList(), - context, - ); - ref - .watch(homePageStateProvider.notifier) - .disableMultiSelect(); + onShare(); }, ), ], diff --git a/mobile/lib/modules/home/ui/daily_title_text.dart b/mobile/lib/modules/home/ui/daily_title_text.dart deleted file mode 100644 index f083cc6e6..000000000 --- a/mobile/lib/modules/home/ui/daily_title_text.dart +++ /dev/null @@ -1,109 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart'; -import 'package:openapi/api.dart'; - -class DailyTitleText extends ConsumerWidget { - const DailyTitleText({ - Key? key, - required this.isoDate, - required this.assetGroup, - }) : super(key: key); - - final String isoDate; - final List assetGroup; - - @override - Widget build(BuildContext context, WidgetRef ref) { - var currentYear = DateTime.now().year; - var groupYear = DateTime.parse(isoDate).year; - var formatDateTemplate = currentYear == groupYear - ? "daily_title_text_date".tr() - : "daily_title_text_date_year".tr(); - var dateText = DateFormat(formatDateTemplate) - .format(DateTime.parse(isoDate).toLocal()); - var isMultiSelectEnable = - ref.watch(homePageStateProvider).isMultiSelectEnable; - var selectedDateGroup = ref.watch(homePageStateProvider).selectedDateGroup; - var selectedItems = ref.watch(homePageStateProvider).selectedItems; - - void _handleTitleIconClick() { - if (isMultiSelectEnable && - selectedDateGroup.contains(dateText) && - selectedDateGroup.length == 1 && - selectedItems.length <= assetGroup.length) { - // Multi select is active - click again on the icon while it is the only active group -> disable multi select - ref.watch(homePageStateProvider.notifier).disableMultiSelect(); - } else if (isMultiSelectEnable && - selectedDateGroup.contains(dateText) && - selectedItems.length != assetGroup.length) { - // Multi select is active - click again on the icon while it is not the only active group -> remove that group from selected group/items - ref - .watch(homePageStateProvider.notifier) - .removeSelectedDateGroup(dateText); - ref - .watch(homePageStateProvider.notifier) - .removeMultipleSelectedItem(assetGroup); - } else if (isMultiSelectEnable && - selectedDateGroup.contains(dateText) && - selectedDateGroup.length > 1) { - ref - .watch(homePageStateProvider.notifier) - .removeSelectedDateGroup(dateText); - ref - .watch(homePageStateProvider.notifier) - .removeMultipleSelectedItem(assetGroup); - } else if (isMultiSelectEnable && !selectedDateGroup.contains(dateText)) { - ref - .watch(homePageStateProvider.notifier) - .addSelectedDateGroup(dateText); - ref - .watch(homePageStateProvider.notifier) - .addMultipleSelectedItems(assetGroup); - } else { - ref - .watch(homePageStateProvider.notifier) - .enableMultiSelect(assetGroup.toSet()); - ref - .watch(homePageStateProvider.notifier) - .addSelectedDateGroup(dateText); - } - } - - return SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.only( - top: 29.0, - bottom: 29.0, - left: 12.0, - right: 12.0, - ), - child: Row( - children: [ - Text( - dateText, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - ), - ), - const Spacer(), - GestureDetector( - onTap: _handleTitleIconClick, - child: isMultiSelectEnable && selectedDateGroup.contains(dateText) - ? Icon( - Icons.check_circle_rounded, - color: Theme.of(context).primaryColor, - ) - : const Icon( - Icons.check_circle_outline_rounded, - color: Colors.grey, - ), - ) - ], - ), - ), - ); - } -} diff --git a/mobile/lib/modules/home/ui/delete_diaglog.dart b/mobile/lib/modules/home/ui/delete_diaglog.dart index 8c8454269..f1ba86864 100644 --- a/mobile/lib/modules/home/ui/delete_diaglog.dart +++ b/mobile/lib/modules/home/ui/delete_diaglog.dart @@ -1,15 +1,14 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/shared/providers/asset.provider.dart'; -import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart'; class DeleteDialog extends ConsumerWidget { - const DeleteDialog({Key? key}) : super(key: key); + final Function onDelete; + + const DeleteDialog({Key? key, required this.onDelete}) : super(key: key); @override Widget build(BuildContext context, WidgetRef ref) { - final homePageState = ref.watch(homePageStateProvider); return AlertDialog( // backgroundColor: Colors.grey[200], @@ -31,11 +30,7 @@ class DeleteDialog extends ConsumerWidget { ), TextButton( onPressed: () { - ref - .watch(assetProvider.notifier) - .deleteAssets(homePageState.selectedItems); - ref.watch(homePageStateProvider.notifier).disableMultiSelect(); - + onDelete(); Navigator.of(context).pop(); }, child: Text( diff --git a/mobile/lib/modules/home/ui/image_grid.dart b/mobile/lib/modules/home/ui/image_grid.dart deleted file mode 100644 index 093d5c613..000000000 --- a/mobile/lib/modules/home/ui/image_grid.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/modules/home/ui/thumbnail_image.dart'; -import 'package:openapi/api.dart'; - -// ignore: must_be_immutable -class ImageGrid extends ConsumerWidget { - final List assetGroup; - final List sortedAssetGroup; - final int tilesPerRow; - final bool showStorageIndicator; - - ImageGrid({ - Key? key, - required this.assetGroup, - required this.sortedAssetGroup, - this.tilesPerRow = 4, - this.showStorageIndicator = true, - }) : super(key: key); - - List imageSortedList = []; - - @override - Widget build(BuildContext context, WidgetRef ref) { - return SliverGrid( - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: tilesPerRow, - crossAxisSpacing: 5.0, - mainAxisSpacing: 5, - ), - delegate: SliverChildBuilderDelegate( - (BuildContext context, int index) { - return GestureDetector( - onTap: () {}, - child: ThumbnailImage( - asset: assetGroup[index], - assetList: sortedAssetGroup, - showStorageIndicator: showStorageIndicator, - ), - ); - }, - childCount: assetGroup.length, - ), - ); - } -} diff --git a/mobile/lib/modules/home/views/home_page.dart b/mobile/lib/modules/home/views/home_page.dart index a1d8f46b2..82045c687 100644 --- a/mobile/lib/modules/home/views/home_page.dart +++ b/mobile/lib/modules/home/views/home_page.dart @@ -2,22 +2,17 @@ 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/providers/home_page_render_list_provider.dart'; -import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart'; +import 'package:immich_mobile/modules/home/providers/multiselect.provider.dart'; +import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart'; import 'package:immich_mobile/modules/home/ui/control_bottom_app_bar.dart'; -import 'package:immich_mobile/modules/home/ui/daily_title_text.dart'; -import 'package:immich_mobile/modules/home/ui/disable_multi_select_button.dart'; -import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart'; -import 'package:immich_mobile/modules/home/ui/image_grid.dart'; -import 'package:immich_mobile/modules/home/ui/asset_list_v2/immich_asset_grid.dart'; import 'package:immich_mobile/modules/home/ui/immich_sliver_appbar.dart'; -import 'package:immich_mobile/modules/home/ui/monthly_title_text.dart'; import 'package:immich_mobile/modules/home/ui/profile_drawer/profile_drawer.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'; import 'package:immich_mobile/shared/providers/server_info.provider.dart'; import 'package:immich_mobile/shared/providers/websocket.provider.dart'; +import 'package:immich_mobile/shared/services/share.service.dart'; import 'package:openapi/api.dart'; class HomePage extends HookConsumerWidget { @@ -26,22 +21,9 @@ class HomePage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final appSettingService = ref.watch(appSettingsServiceProvider); - var renderList = ref.watch(renderListProvider); - - ScrollController scrollController = useScrollController(); - var assetGroupByDateTime = ref.watch(assetGroupByDateTimeProvider); - List imageGridGroup = []; - var isMultiSelectEnable = - ref.watch(homePageStateProvider).isMultiSelectEnable; - var homePageState = ref.watch(homePageStateProvider); - List sortedAssetList = []; - // set sorted List - for (var group in assetGroupByDateTime.values) { - for (var value in group) { - sortedAssetList.add(value); - } - } + final multiselectEnabled = ref.watch(multiselectProvider.notifier); + final selection = useState({}); useEffect( () { @@ -57,115 +39,61 @@ class HomePage extends HookConsumerWidget { ref.read(assetProvider.notifier).getAllAsset(); } - _buildSelectedItemCountIndicator() { - return DisableMultiSelectButton( - onPressed: ref.watch(homePageStateProvider.notifier).disableMultiSelect, - selectedItemCount: homePageState.selectedItems.length, - ); - } - - Widget _buildBody() { - if (assetGroupByDateTime.isNotEmpty) { - int? lastMonth; - - assetGroupByDateTime.forEach((dateGroup, immichAssetList) { - try { - DateTime parseDateGroup = DateTime.parse(dateGroup); - int currentMonth = parseDateGroup.month; - - if (lastMonth != null) { - if (currentMonth - lastMonth! != 0) { - imageGridGroup.add( - MonthlyTitleText( - isoDate: dateGroup, - ), - ); - } - } - - imageGridGroup.add( - DailyTitleText( - key: Key('${dateGroup.toString()}title'), - isoDate: dateGroup, - assetGroup: immichAssetList, - ), - ); - - imageGridGroup.add( - ImageGrid( - assetGroup: immichAssetList, - sortedAssetGroup: sortedAssetList, - tilesPerRow: - appSettingService.getSetting(AppSettingsEnum.tilesPerRow), - showStorageIndicator: appSettingService - .getSetting(AppSettingsEnum.storageIndicator), - ), - ); - - lastMonth = currentMonth; - } catch (e) { - debugPrint( - "[ERROR] Cannot parse $dateGroup - Wrong create date format : ${immichAssetList.map((asset) => asset.createdAt).toList()}", - ); - } - }); + Widget buildBody() { + void selectionListener( + bool multiselect, + Set selectedAssets, + ) { + multiselectEnabled.state = multiselect; + selection.value = selectedAssets; } - _buildSliverAppBar() { - return isMultiSelectEnable - ? const SliverToBoxAdapter( - child: SizedBox( - height: 70, - child: null, - ), - ) - : ImmichSliverAppBar( - onPopBack: reloadAllAsset, - ); + void onShareAssets() { + ref.watch(shareServiceProvider).shareAssets(selection.value.toList()); + multiselectEnabled.state = false; } - _buildAssetGrid() { - if (appSettingService - .getSetting(AppSettingsEnum.useExperimentalAssetGrid)) { - return ImmichAssetGrid( - renderList: renderList, - assetsPerRow: - appSettingService.getSetting(AppSettingsEnum.tilesPerRow), - showStorageIndicator: appSettingService - .getSetting(AppSettingsEnum.storageIndicator), - ); - } else { - return DraggableScrollbar.semicircle( - backgroundColor: Theme.of(context).hintColor, - controller: scrollController, - heightScrollThumb: 48.0, - child: CustomScrollView( - controller: scrollController, - slivers: [ - ...imageGridGroup, - ], - ), - ); - } + void onDelete() { + ref.watch(assetProvider.notifier).deleteAssets(selection.value); + multiselectEnabled.state = false; } return SafeArea( - bottom: !isMultiSelectEnable, - top: !isMultiSelectEnable, + bottom: !multiselectEnabled.state, + top: !multiselectEnabled.state, child: Stack( children: [ CustomScrollView( slivers: [ - _buildSliverAppBar(), + multiselectEnabled.state + ? const SliverToBoxAdapter( + child: SizedBox( + height: 70, + child: null, + ), + ) + : ImmichSliverAppBar( + onPopBack: reloadAllAsset, + ), ], ), Padding( padding: const EdgeInsets.only(top: 60.0, bottom: 0.0), - child: _buildAssetGrid(), + child: ImmichAssetGrid( + renderList: renderList, + assetsPerRow: + appSettingService.getSetting(AppSettingsEnum.tilesPerRow), + showStorageIndicator: appSettingService + .getSetting(AppSettingsEnum.storageIndicator), + listener: selectionListener, + selectionActive: multiselectEnabled.state, + ), ), - if (isMultiSelectEnable) ...[ - _buildSelectedItemCountIndicator(), - const ControlBottomAppBar(), + if (multiselectEnabled.state) ...[ + ControlBottomAppBar( + onShare: onShareAssets, + onDelete: onDelete, + ), ], ], ), @@ -174,7 +102,7 @@ class HomePage extends HookConsumerWidget { return Scaffold( drawer: const ProfileDrawer(), - body: _buildBody(), + body: buildBody(), ); } } diff --git a/mobile/lib/modules/search/providers/search_result_page.provider.dart b/mobile/lib/modules/search/providers/search_result_page.provider.dart index 66c7a4419..c152be25e 100644 --- a/mobile/lib/modules/search/providers/search_result_page.provider.dart +++ b/mobile/lib/modules/search/providers/search_result_page.provider.dart @@ -1,8 +1,11 @@ import 'package:collection/collection.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/search/models/search_result_page_state.model.dart'; 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/services/app_settings.service.dart'; import 'package:intl/intl.dart'; import 'package:openapi/api.dart'; @@ -66,3 +69,12 @@ final searchResultGroupByDateTimeProvider = StateProvider((ref) { .format(DateTime.parse(element.createdAt).toLocal()), ); }); + +final searchRenderListProvider = StateProvider((ref) { + var assetGroups = ref.watch(searchResultGroupByDateTimeProvider); + + var settings = ref.watch(appSettingsServiceProvider); + final assetsPerRow = settings.getSetting(AppSettingsEnum.tilesPerRow); + + return assetGroupsToRenderList(assetGroups, assetsPerRow); +}); diff --git a/mobile/lib/modules/search/views/search_result_page.dart b/mobile/lib/modules/search/views/search_result_page.dart index 09f38ffb0..5f213cdce 100644 --- a/mobile/lib/modules/search/views/search_result_page.dart +++ b/mobile/lib/modules/search/views/search_result_page.dart @@ -4,14 +4,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/modules/home/ui/daily_title_text.dart'; -import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart'; -import 'package:immich_mobile/modules/home/ui/image_grid.dart'; -import 'package:immich_mobile/modules/home/ui/monthly_title_text.dart'; +import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart'; import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart'; import 'package:immich_mobile/modules/search/providers/search_result_page.provider.dart'; import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart'; -import 'package:openapi/api.dart'; +import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart'; +import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; class SearchResultPage extends HookConsumerWidget { const SearchResultPage({Key? key, required this.searchTerm}) @@ -21,17 +19,12 @@ class SearchResultPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - ScrollController scrollController = useScrollController(); final searchTermController = useTextEditingController(text: ""); final isNewSearch = useState(false); final currentSearchTerm = useState(searchTerm); - final List imageGridGroup = []; - FocusNode? searchFocusNode; - List sortedAssetList = []; - useEffect( () { searchFocusNode = FocusNode(); @@ -117,7 +110,12 @@ class SearchResultPage extends HookConsumerWidget { _buildSearchResult() { var searchResultPageState = ref.watch(searchResultPageProvider); - var assetGroupByDateTime = ref.watch(searchResultGroupByDateTimeProvider); + var searchResultRenderList = ref.watch(searchRenderListProvider); + + var settings = ref.watch(appSettingsServiceProvider); + final assetsPerRow = settings.getSetting(AppSettingsEnum.tilesPerRow); + final showStorageIndicator = + settings.getSetting(AppSettingsEnum.storageIndicator); if (searchResultPageState.isError) { return const Text("Error"); @@ -132,57 +130,11 @@ class SearchResultPage extends HookConsumerWidget { } if (searchResultPageState.isSuccess) { - if (searchResultPageState.searchResult.isNotEmpty) { - int? lastMonth; - // set sorted List - for (var group in assetGroupByDateTime.values) { - for (var value in group) { - sortedAssetList.add(value); - } - } - assetGroupByDateTime.forEach((dateGroup, immichAssetList) { - DateTime parseDateGroup = DateTime.parse(dateGroup); - int currentMonth = parseDateGroup.month; - - if (lastMonth != null) { - if (currentMonth - lastMonth! != 0) { - imageGridGroup.add( - MonthlyTitleText( - isoDate: dateGroup, - ), - ); - } - } - - imageGridGroup.add( - DailyTitleText( - isoDate: dateGroup, - assetGroup: immichAssetList, - ), - ); - - imageGridGroup.add( - ImageGrid( - assetGroup: immichAssetList, - sortedAssetGroup: sortedAssetList, - ), - ); - - lastMonth = currentMonth; - }); - - return DraggableScrollbar.semicircle( - backgroundColor: Theme.of(context).hintColor, - controller: scrollController, - heightScrollThumb: 48.0, - child: CustomScrollView( - controller: scrollController, - slivers: [...imageGridGroup], - ), - ); - } else { - return const Text("No assets found"); - } + return ImmichAssetGrid( + renderList: searchResultRenderList, + assetsPerRow: assetsPerRow, + showStorageIndicator: showStorageIndicator, + ); } return const SizedBox(); diff --git a/mobile/lib/modules/settings/ui/experimental_settings/experimental_settings.dart b/mobile/lib/modules/settings/ui/experimental_settings/experimental_settings.dart index 2043d0d24..f6db82494 100644 --- a/mobile/lib/modules/settings/ui/experimental_settings/experimental_settings.dart +++ b/mobile/lib/modules/settings/ui/experimental_settings/experimental_settings.dart @@ -1,11 +1,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.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/ui/immich_toast.dart'; class ExperimentalSettings extends HookConsumerWidget { const ExperimentalSettings({ @@ -14,33 +9,6 @@ class ExperimentalSettings extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final appSettingService = ref.watch(appSettingsServiceProvider); - - final useExperimentalAssetGrid = useState(false); - - useEffect( - () { - useExperimentalAssetGrid.value = appSettingService - .getSetting(AppSettingsEnum.useExperimentalAssetGrid); - return null; - }, - [], - ); - - void changeUseExperimentalAssetGrid(bool status) { - useExperimentalAssetGrid.value = status; - appSettingService.setSetting( - AppSettingsEnum.useExperimentalAssetGrid, - status, - ); - - ImmichToast.show( - context: context, - msg: "settings_require_restart".tr(), - gravity: ToastGravity.BOTTOM, - ); - } - return ExpansionTile( textColor: Theme.of(context).primaryColor, title: const Text( @@ -55,25 +23,25 @@ class ExperimentalSettings extends HookConsumerWidget { fontSize: 13, ), ).tr(), - children: [ - SwitchListTile.adaptive( - activeColor: Theme.of(context).primaryColor, - title: const Text( - "experimental_settings_new_asset_list_title", - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - ), - ).tr(), - subtitle: const Text( - "experimental_settings_new_asset_list_subtitle", - style: TextStyle( - fontSize: 12, - ), - ).tr(), - value: useExperimentalAssetGrid.value, - onChanged: changeUseExperimentalAssetGrid, - ), + children: const [ + // SwitchListTile.adaptive( + // activeColor: Theme.of(context).primaryColor, + // title: const Text( + // "experimental_settings_new_asset_list_title", + // style: TextStyle( + // fontSize: 12, + // fontWeight: FontWeight.bold, + // ), + // ).tr(), + // subtitle: const Text( + // "experimental_settings_new_asset_list_subtitle", + // style: TextStyle( + // fontSize: 12, + // ), + // ).tr(), + // value: useExperimentalAssetGrid.value, + // onChanged: changeUseExperimentalAssetGrid, + // ), ], ); } diff --git a/mobile/lib/modules/settings/views/settings_page.dart b/mobile/lib/modules/settings/views/settings_page.dart index 4176629dd..9a1679342 100644 --- a/mobile/lib/modules/settings/views/settings_page.dart +++ b/mobile/lib/modules/settings/views/settings_page.dart @@ -43,7 +43,7 @@ class SettingsPage extends HookConsumerWidget { const ThemeSetting(), const AssetListSettings(), if (Platform.isAndroid) const NotificationSetting(), - const ExperimentalSettings(), + //const ExperimentalSettings(), ], ).toList(), ], diff --git a/mobile/lib/shared/views/tab_controller_page.dart b/mobile/lib/shared/views/tab_controller_page.dart index 51a13fea5..0b0634ae0 100644 --- a/mobile/lib/shared/views/tab_controller_page.dart +++ b/mobile/lib/shared/views/tab_controller_page.dart @@ -2,7 +2,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart'; +import 'package:immich_mobile/modules/home/providers/multiselect.provider.dart'; import 'package:immich_mobile/routing/router.dart'; class TabControllerPage extends ConsumerWidget { @@ -10,8 +10,7 @@ class TabControllerPage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - var isMultiSelectEnable = - ref.watch(homePageStateProvider).isMultiSelectEnable; + final multiselectEnabled = ref.watch(multiselectProvider); return AutoTabsRouter( routes: [ @@ -32,7 +31,7 @@ class TabControllerPage extends ConsumerWidget { opacity: animation, child: child, ), - bottomNavigationBar: isMultiSelectEnable + bottomNavigationBar: multiselectEnabled ? null : BottomNavigationBar( selectedLabelStyle: const TextStyle( diff --git a/mobile/test/asset_grid_data_structure_test.dart b/mobile/test/asset_grid_data_structure_test.dart new file mode 100644 index 000000000..94d646ea0 --- /dev/null +++ b/mobile/test/asset_grid_data_structure_test.dart @@ -0,0 +1,159 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart'; +import 'package:openapi/api.dart'; + +void main() { + final List testAssets = []; + + for (int i = 0; i < 150; i++) { + int month = i ~/ 31; + int day = (i % 31).toInt(); + + DateTime date = DateTime(2022, month, day); + + testAssets.add(AssetResponseDto( + type: AssetTypeEnum.IMAGE, + id: '$i', + deviceAssetId: '', + ownerId: '', + deviceId: '', + originalPath: '', + resizePath: '', + createdAt: date.toIso8601String(), + modifiedAt: date.toIso8601String(), + isFavorite: false, + mimeType: 'image/jpeg', + duration: '', + webpPath: '', + encodedVideoPath: '', + )); + } + + final Map> groups = { + '2022-01-05': testAssets.sublist(0, 5).map((e) { + e.createdAt = DateTime(2022, 1, 5).toIso8601String(); + return e; + }).toList(), + '2022-01-10': testAssets.sublist(5, 10).map((e) { + e.createdAt = DateTime(2022, 1, 10).toIso8601String(); + return e; + }).toList(), + '2022-02-17': testAssets.sublist(10, 15).map((e) { + e.createdAt = DateTime(2022, 2, 17).toIso8601String(); + return e; + }).toList(), + '2022-10-15': testAssets.sublist(15, 30).map((e) { + e.createdAt = DateTime(2022, 10, 15).toIso8601String(); + return e; + }).toList() + }; + + group('Asset only list', () { + test('items < itemsPerRow', () { + final assets = testAssets.sublist(0, 2); + final renderList = assetsToRenderList(assets, 3); + + expect(renderList.length, 1); + expect(renderList[0].assetRow!.assets.length, 2); + }); + + test('items = itemsPerRow', () { + final assets = testAssets.sublist(0, 3); + final renderList = assetsToRenderList(assets, 3); + + expect(renderList.length, 1); + expect(renderList[0].assetRow!.assets.length, 3); + }); + + test('items > itemsPerRow', () { + final assets = testAssets.sublist(0, 20); + final renderList = assetsToRenderList(assets, 3); + + expect(renderList.length, 7); + expect(renderList[6].assetRow!.assets.length, 2); + }); + + test('items > itemsPerRow partition 4', () { + final assets = testAssets.sublist(0, 21); + final renderList = assetsToRenderList(assets, 4); + + expect(renderList.length, 6); + expect(renderList[5].assetRow!.assets.length, 1); + }); + + test('items > itemsPerRow check ids', () { + final assets = testAssets.sublist(0, 21); + final renderList = assetsToRenderList(assets, 3); + + expect(renderList.length, 7); + expect(renderList[6].assetRow!.assets.length, 3); + expect(renderList[0].assetRow!.assets[0].id, '0'); + expect(renderList[1].assetRow!.assets[1].id, '4'); + expect(renderList[3].assetRow!.assets[2].id, '11'); + expect(renderList[6].assetRow!.assets[2].id, '20'); + }); + }); + + group('Test grouped', () { + test('test grouped check months', () { + final renderList = assetGroupsToRenderList(groups, 3); + + // Jan + // Day 1 + // 5 Assets => 2 Rows + // Day 2 + // 5 Assets => 2 Rows + // Feb + // Day 1 + // 5 Assets => 2 Rows + // Oct + // Day 1 + // 15 Assets => 5 Rows + expect(renderList.length, 18); + expect(renderList[0].type, RenderAssetGridElementType.monthTitle); + expect(renderList[0].date.month, 1); + expect(renderList[7].type, RenderAssetGridElementType.monthTitle); + expect(renderList[7].date.month, 2); + expect(renderList[11].type, RenderAssetGridElementType.monthTitle); + expect(renderList[11].date.month, 10); + }); + + test('test grouped check types', () { + final renderList = assetGroupsToRenderList(groups, 5); + + // Jan + // Day 1 + // 5 Assets + // Day 2 + // 5 Assets + // Feb + // Day 1 + // 5 Assets + // Oct + // Day 1 + // 15 Assets => 3 Rows + + final types = [ + RenderAssetGridElementType.monthTitle, + RenderAssetGridElementType.dayTitle, + RenderAssetGridElementType.assetRow, + RenderAssetGridElementType.dayTitle, + RenderAssetGridElementType.assetRow, + RenderAssetGridElementType.monthTitle, + RenderAssetGridElementType.dayTitle, + RenderAssetGridElementType.assetRow, + RenderAssetGridElementType.monthTitle, + RenderAssetGridElementType.dayTitle, + RenderAssetGridElementType.assetRow, + RenderAssetGridElementType.assetRow, + RenderAssetGridElementType.assetRow + ]; + + expect(renderList.length, types.length); + + for (int i = 0; i < renderList.length; i++) { + expect(renderList[i].type, types[i]); + } + }); + }); +}