From 501b96baf7564ecc8374fc53bcbeac0ec8766522 Mon Sep 17 00:00:00 2001 From: martyfuhry Date: Fri, 24 Mar 2023 23:44:53 -0400 Subject: [PATCH] feat(mobile): Explore favorites, recently added, videos, and motion photos (#2076) * Added placeholder for search explore * refactor immich asset grid to use ref and provider * all videos page * got favorites, recently added, videos, and motion videos all using the immich grid * Fixed issue with hero animations * theming * localization * delete empty file * style text * Styling icons * more styling --------- Co-authored-by: Alex Tran --- mobile/assets/i18n/en-US.json | 11 +- .../providers/render_list.provider.dart | 17 + .../favorite/views/favorites_page.dart | 40 +- .../home/ui/asset_grid/immich_asset_grid.dart | 370 ++++-------------- .../ui/asset_grid/immich_asset_grid_view.dart | 300 ++++++++++++++ mobile/lib/modules/home/views/home_page.dart | 2 +- .../providers/all_motion_photos.provider.dart | 29 ++ .../providers/all_video_assets.provider.dart | 28 ++ .../providers/recently_added.provider.dart | 20 + .../search_result_page.provider.dart | 15 +- mobile/lib/modules/search/ui/curated_row.dart | 67 ++++ .../search/views/all_motion_videos_page.dart | 35 ++ .../modules/search/views/all_videos_page.dart | 35 ++ .../search/views/curated_location_page.dart | 5 + .../search/views/curated_object_page.dart | 5 + .../search/views/recently_added_page.dart | 35 ++ .../lib/modules/search/views/search_page.dart | 237 ++++++----- .../search/views/search_result_page.dart | 26 +- mobile/lib/routing/router.dart | 6 + mobile/lib/routing/router.gr.dart | 83 ++++ .../lib/shared/views/tab_controller_page.dart | 5 +- mobile/lib/utils/immich_app_theme.dart | 27 ++ .../openapi/lib/model/asset_response_dto.dart | Bin 9638 -> 9843 bytes 23 files changed, 938 insertions(+), 460 deletions(-) create mode 100644 mobile/lib/modules/asset_viewer/providers/render_list.provider.dart create mode 100644 mobile/lib/modules/home/ui/asset_grid/immich_asset_grid_view.dart create mode 100644 mobile/lib/modules/search/providers/all_motion_photos.provider.dart create mode 100644 mobile/lib/modules/search/providers/all_video_assets.provider.dart create mode 100644 mobile/lib/modules/search/providers/recently_added.provider.dart create mode 100644 mobile/lib/modules/search/ui/curated_row.dart create mode 100644 mobile/lib/modules/search/views/all_motion_videos_page.dart create mode 100644 mobile/lib/modules/search/views/all_videos_page.dart create mode 100644 mobile/lib/modules/search/views/recently_added_page.dart diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index cb39ddc331..787cebcf45 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -249,5 +249,14 @@ "album_thumbnail_owned": "Owned", "curated_object_page_title": "Things", "curated_location_page_title": "Places", - "search_page_view_all_button": "View All" + "search_page_view_all_button": "View all", + "search_page_your_activity": "Your activity", + "search_page_favorites": "Favorites", + "search_page_videos": "Videos", + "all_videos_page_title": "Videos", + "recently_added_page_title": "Recently Added", + "motion_photos_page_title": "Motion Photos", + "search_page_motion_photos": "Motion Photos", + "search_page_recently_added": "Recently added", + "search_page_categories": "Categories" } diff --git a/mobile/lib/modules/asset_viewer/providers/render_list.provider.dart b/mobile/lib/modules/asset_viewer/providers/render_list.provider.dart new file mode 100644 index 0000000000..e106e97ed1 --- /dev/null +++ b/mobile/lib/modules/asset_viewer/providers/render_list.provider.dart @@ -0,0 +1,17 @@ +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/models/asset.dart'; + +final renderListProvider = FutureProvider.family>((ref, assets) { + var settings = ref.watch(appSettingsServiceProvider); + + final layout = AssetGridLayoutParameters( + settings.getSetting(AppSettingsEnum.tilesPerRow), + settings.getSetting(AppSettingsEnum.dynamicLayout), + GroupAssetsBy.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)], + ); + + return RenderList.fromAssets(assets, layout); +}); diff --git a/mobile/lib/modules/favorite/views/favorites_page.dart b/mobile/lib/modules/favorite/views/favorites_page.dart index 657bfe7939..3448073e7d 100644 --- a/mobile/lib/modules/favorite/views/favorites_page.dart +++ b/mobile/lib/modules/favorite/views/favorites_page.dart @@ -3,9 +3,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/modules/favorite/providers/favorite_provider.dart'; -import 'package:immich_mobile/modules/favorite/ui/favorite_image.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/home/ui/asset_grid/immich_asset_grid.dart'; class FavoritesPage extends HookConsumerWidget { const FavoritesPage({Key? key}) : super(key: key); @@ -22,46 +20,14 @@ class FavoritesPage extends HookConsumerWidget { automaticallyImplyLeading: false, title: const Text( 'favorites_page_title', - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ).tr(), ); } - Widget buildImageGrid() { - final appSettingService = ref.watch(appSettingsServiceProvider); - - if (ref.watch(favoriteAssetProvider).isNotEmpty) { - return SliverPadding( - padding: const EdgeInsets.only(top: 10.0), - sliver: SliverGrid( - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: - appSettingService.getSetting(AppSettingsEnum.tilesPerRow), - crossAxisSpacing: 5.0, - mainAxisSpacing: 5, - ), - delegate: SliverChildBuilderDelegate( - ( - BuildContext context, - int index, - ) { - return FavoriteImage( - ref.watch(favoriteAssetProvider)[index], - ref.watch(favoriteAssetProvider), - ); - }, - childCount: ref.watch(favoriteAssetProvider).length, - ), - ), - ); - } - return const SliverToBoxAdapter(); - } - return Scaffold( appBar: buildAppBar(), - body: CustomScrollView( - slivers: [buildImageGrid()], + body: ImmichAssetGrid( + assets: ref.watch(favoriteAssetProvider), ), ); } 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 index 1c6eb5a04d..c4f9558018 100644 --- a/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart +++ b/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart @@ -1,300 +1,104 @@ -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/asset_viewer/providers/scroll_notifier.provider.dart'; -import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_image.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/modules/asset_viewer/providers/render_list.provider.dart'; +import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart'; +import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid_view.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/models/asset.dart'; -import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; -import 'asset_grid_data_structure.dart'; -import 'group_divider_title.dart'; -import 'disable_multi_select_button.dart'; -import 'draggable_scrollbar_custom.dart'; +import 'package:immich_mobile/shared/ui/immich_loading_indicator.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(); - - Set _getSelectedAssets() { - return _selectedAssets - .map((e) => widget.allAssets.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; - } - - Widget _buildThumbnailOrPlaceholder( - Asset asset, - bool placeholder, - ) { - if (placeholder) { - return const DecoratedBox( - decoration: BoxDecoration(color: Colors.grey), - ); - } - return ThumbnailImage( - asset: asset, - assetList: widget.allAssets, - multiselectEnabled: widget.selectionActive, - isSelected: widget.selectionActive && _selectedAssets.contains(asset.id), - onSelect: () => _selectAssets([asset]), - onDeselect: () => _deselectAssets([asset]), - useGrayBoxPlaceholder: true, - showStorageIndicator: widget.showStorageIndicator, - ); - } - - Widget _buildAssetRow( - BuildContext context, - RenderAssetGridRow row, - bool scrolling, - ) { - return LayoutBuilder( - builder: (context, constraints) { - final size = constraints.maxWidth / widget.assetsPerRow - - widget.margin * (widget.assetsPerRow - 1) / widget.assetsPerRow; - return Row( - key: Key("asset-row-${row.assets.first.id}"), - 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 * row.widthDistribution[index], - 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 GroupDividerTitle( - text: title, - multiselectEnabled: widget.selectionActive, - onSelect: () => _selectAssets(assets), - onDeselect: () => _deselectAssets(assets), - selected: _allAssetsSelected(assets), - ); - } - - Widget _buildMonthTitle(BuildContext context, String title) { - return Padding( - key: Key("month-$title"), - padding: const EdgeInsets.only(left: 12.0, top: 32), - child: Text( - title, - style: TextStyle( - fontSize: 26, - fontWeight: FontWeight.bold, - color: Theme.of(context).textTheme.displayLarge?.color, - ), - ), - ); - } - - Widget _itemBuilder(BuildContext c, int position) { - final item = widget.renderList.elements[position]; - - if (item.type == RenderAssetGridElementType.groupDividerTitle) { - 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.elements[pos].date; - return Text( - DateFormat.yMMMM().format(date), - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - ), - ); - } - - Widget _buildMultiSelectIndicator() { - return DisableMultiSelectButton( - onPressed: () => _deselectAll(), - selectedItemCount: _selectedAssets.length, - ); - } - - Widget _buildAssetGrid() { - final useDragScrolling = widget.allAssets.length >= 20; - - void dragScrolling(bool active) { - setState(() { - _scrolling = active; - }); - } - - final listWidget = ScrollablePositionedList.builder( - padding: const EdgeInsets.only( - bottom: 220, - ), - itemBuilder: _itemBuilder, - itemPositionsListener: _itemPositionsListener, - itemScrollController: _itemScrollController, - itemCount: widget.renderList.elements.length, - addRepaintBoundaries: true, - ); - - 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(); - }); - } - } - - Future onWillPop() async { - if (widget.selectionActive && _selectedAssets.isNotEmpty) { - _deselectAll(); - return false; - } - - return true; - } - - @override - void initState() { - super.initState(); - scrollToTopNotifierProvider.addListener(_scrollToTop); - } - - @override - void dispose() { - scrollToTopNotifierProvider.removeListener(_scrollToTop); - super.dispose(); - } - - void _scrollToTop() { - // for some reason, this is necessary as well in order - // to correctly reposition the drag thumb scroll bar - _itemScrollController.jumpTo( - index: 0, - ); - _itemScrollController.scrollTo( - index: 0, - duration: const Duration(milliseconds: 200), - ); - } - - @override - Widget build(BuildContext context) { - return WillPopScope( - onWillPop: onWillPop, - child: Stack( - children: [ - _buildAssetGrid(), - if (widget.selectionActive) _buildMultiSelectIndicator(), - ], - ), - ); - } -} - -class ImmichAssetGrid extends StatefulWidget { - final RenderList renderList; - final int assetsPerRow; +class ImmichAssetGrid extends HookConsumerWidget { + final int? assetsPerRow; final double margin; - final bool showStorageIndicator; + final bool? showStorageIndicator; final ImmichAssetGridSelectionListener? listener; final bool selectionActive; - final List allAssets; + final List assets; + final RenderList? renderList; const ImmichAssetGrid({ super.key, - required this.renderList, - required this.allAssets, - required this.assetsPerRow, - required this.showStorageIndicator, + required this.assets, + this.renderList, + this.assetsPerRow, + this.showStorageIndicator, this.listener, this.margin = 5.0, this.selectionActive = false, }); @override - State createState() { - return ImmichAssetGridState(); + Widget build(BuildContext context, WidgetRef ref) { + var settings = ref.watch(appSettingsServiceProvider); + final renderListFuture = ref.watch(renderListProvider(assets)); + + // Needs to suppress hero animations when navigating to this widget + final enableHeroAnimations = useState(false); + + // Wait for transition to complete, then re-enable + ModalRoute.of(context)?.animation?.addListener(() { + // If we've already enabled, we are done + if (enableHeroAnimations.value) { + return; + } + final animation = ModalRoute.of(context)?.animation; + if (animation != null) { + // When the animation is complete, re-enable hero animations + enableHeroAnimations.value = animation.isCompleted; + } + }); + + Future onWillPop() async { + enableHeroAnimations.value = false; + return true; + } + + if (renderList != null) { + return WillPopScope( + onWillPop: onWillPop, + child: HeroMode( + enabled: enableHeroAnimations.value, + child: ImmichAssetGridView( + allAssets: assets, + assetsPerRow: assetsPerRow + ?? settings.getSetting(AppSettingsEnum.tilesPerRow), + listener: listener, + showStorageIndicator: showStorageIndicator + ?? settings.getSetting(AppSettingsEnum.storageIndicator), + renderList: renderList!, + margin: margin, + selectionActive: selectionActive, + ), + ), + ); + } + + return renderListFuture.when( + data: (renderList) => + WillPopScope( + onWillPop: onWillPop, + child: HeroMode( + enabled: enableHeroAnimations.value, + child: ImmichAssetGridView( + allAssets: assets, + assetsPerRow: assetsPerRow + ?? settings.getSetting(AppSettingsEnum.tilesPerRow), + listener: listener, + showStorageIndicator: showStorageIndicator + ?? settings.getSetting(AppSettingsEnum.storageIndicator), + renderList: renderList, + margin: margin, + selectionActive: selectionActive, + ), + ), + ), + error: (err, stack) => + Center(child: Text("$err")), + loading: () => const Center( + child: ImmichLoadingIndicator(), + ), + ); } } diff --git a/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid_view.dart b/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid_view.dart new file mode 100644 index 0000000000..8c3026f462 --- /dev/null +++ b/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid_view.dart @@ -0,0 +1,300 @@ +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/asset_viewer/providers/scroll_notifier.provider.dart'; +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 'group_divider_title.dart'; +import 'disable_multi_select_button.dart'; +import 'draggable_scrollbar_custom.dart'; + +typedef ImmichAssetGridSelectionListener = void Function( + bool, + Set, +); + +class ImmichAssetGridViewState extends State { + final ItemScrollController _itemScrollController = ItemScrollController(); + final ItemPositionsListener _itemPositionsListener = + ItemPositionsListener.create(); + + bool _scrolling = false; + final Set _selectedAssets = HashSet(); + + Set _getSelectedAssets() { + return _selectedAssets + .map((e) => widget.allAssets.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; + } + + Widget _buildThumbnailOrPlaceholder( + Asset asset, + bool placeholder, + ) { + if (placeholder) { + return const DecoratedBox( + decoration: BoxDecoration(color: Colors.grey), + ); + } + return ThumbnailImage( + asset: asset, + assetList: widget.allAssets, + multiselectEnabled: widget.selectionActive, + isSelected: widget.selectionActive && _selectedAssets.contains(asset.id), + onSelect: () => _selectAssets([asset]), + onDeselect: () => _deselectAssets([asset]), + useGrayBoxPlaceholder: true, + showStorageIndicator: widget.showStorageIndicator, + ); + } + + Widget _buildAssetRow( + BuildContext context, + RenderAssetGridRow row, + bool scrolling, + ) { + return LayoutBuilder( + builder: (context, constraints) { + final size = constraints.maxWidth / widget.assetsPerRow - + widget.margin * (widget.assetsPerRow - 1) / widget.assetsPerRow; + return Row( + key: Key("asset-row-${row.assets.first.id}"), + 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 * row.widthDistribution[index], + 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 GroupDividerTitle( + text: title, + multiselectEnabled: widget.selectionActive, + onSelect: () => _selectAssets(assets), + onDeselect: () => _deselectAssets(assets), + selected: _allAssetsSelected(assets), + ); + } + + Widget _buildMonthTitle(BuildContext context, String title) { + return Padding( + key: Key("month-$title"), + padding: const EdgeInsets.only(left: 12.0, top: 32), + child: Text( + title, + style: TextStyle( + fontSize: 26, + fontWeight: FontWeight.bold, + color: Theme.of(context).textTheme.displayLarge?.color, + ), + ), + ); + } + + Widget _itemBuilder(BuildContext c, int position) { + final item = widget.renderList.elements[position]; + + if (item.type == RenderAssetGridElementType.groupDividerTitle) { + 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.elements[pos].date; + return Text( + DateFormat.yMMMM().format(date), + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ); + } + + Widget _buildMultiSelectIndicator() { + return DisableMultiSelectButton( + onPressed: () => _deselectAll(), + selectedItemCount: _selectedAssets.length, + ); + } + + Widget _buildAssetGrid() { + final useDragScrolling = widget.allAssets.length >= 20; + + void dragScrolling(bool active) { + setState(() { + _scrolling = active; + }); + } + + final listWidget = ScrollablePositionedList.builder( + padding: const EdgeInsets.only( + bottom: 220, + ), + itemBuilder: _itemBuilder, + itemPositionsListener: _itemPositionsListener, + itemScrollController: _itemScrollController, + itemCount: widget.renderList.elements.length, + addRepaintBoundaries: true, + ); + + 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(ImmichAssetGridView oldWidget) { + super.didUpdateWidget(oldWidget); + if (!widget.selectionActive) { + setState(() { + _selectedAssets.clear(); + }); + } + } + + Future onWillPop() async { + if (widget.selectionActive && _selectedAssets.isNotEmpty) { + _deselectAll(); + return false; + } + + return true; + } + + @override + void initState() { + super.initState(); + scrollToTopNotifierProvider.addListener(_scrollToTop); + } + + @override + void dispose() { + scrollToTopNotifierProvider.removeListener(_scrollToTop); + super.dispose(); + } + + void _scrollToTop() { + // for some reason, this is necessary as well in order + // to correctly reposition the drag thumb scroll bar + _itemScrollController.jumpTo( + index: 0, + ); + _itemScrollController.scrollTo( + index: 0, + duration: const Duration(milliseconds: 200), + ); + } + + @override + Widget build(BuildContext context) { + return WillPopScope( + onWillPop: onWillPop, + child: Stack( + children: [ + _buildAssetGrid(), + if (widget.selectionActive) _buildMultiSelectIndicator(), + ], + ), + ); + } +} + +class ImmichAssetGridView extends StatefulWidget { + final RenderList renderList; + final int assetsPerRow; + final double margin; + final bool showStorageIndicator; + final ImmichAssetGridSelectionListener? listener; + final bool selectionActive; + final List allAssets; + + const ImmichAssetGridView({ + super.key, + required this.renderList, + required this.allAssets, + required this.assetsPerRow, + required this.showStorageIndicator, + this.listener, + this.margin = 5.0, + this.selectionActive = false, + }); + + @override + State createState() { + return ImmichAssetGridViewState(); + } +} diff --git a/mobile/lib/modules/home/views/home_page.dart b/mobile/lib/modules/home/views/home_page.dart index 663ab47d64..b428e5023d 100644 --- a/mobile/lib/modules/home/views/home_page.dart +++ b/mobile/lib/modules/home/views/home_page.dart @@ -234,7 +234,7 @@ class HomePage extends HookConsumerWidget { ? buildLoadingIndicator() : ImmichAssetGrid( renderList: ref.watch(assetProvider).renderList!, - allAssets: ref.watch(assetProvider).allAssets, + assets: ref.watch(assetProvider).allAssets, assetsPerRow: appSettingService .getSetting(AppSettingsEnum.tilesPerRow), showStorageIndicator: appSettingService diff --git a/mobile/lib/modules/search/providers/all_motion_photos.provider.dart b/mobile/lib/modules/search/providers/all_motion_photos.provider.dart new file mode 100644 index 0000000000..7e20fa793f --- /dev/null +++ b/mobile/lib/modules/search/providers/all_motion_photos.provider.dart @@ -0,0 +1,29 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/shared/models/asset.dart'; +import 'package:immich_mobile/shared/providers/api.provider.dart'; +import 'package:immich_mobile/shared/providers/db.provider.dart'; + +final allMotionPhotosProvider = FutureProvider>( (ref) async { + final search = await ref.watch(apiServiceProvider).searchApi.search( + motion: true, + ); + + if (search == null) { + return []; + } + + return ref.watch(dbProvider) + .assets + .getAllByRemoteId( + search.assets.items.map((e) => e.id), + ); + + + /// This works offline, but we use the above + /* + return ref.watch(dbProvider).assets + .filter() + .livePhotoVideoIdIsNotNull() + .findAll(); + */ +}); diff --git a/mobile/lib/modules/search/providers/all_video_assets.provider.dart b/mobile/lib/modules/search/providers/all_video_assets.provider.dart new file mode 100644 index 0000000000..eb46ad2596 --- /dev/null +++ b/mobile/lib/modules/search/providers/all_video_assets.provider.dart @@ -0,0 +1,28 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/shared/models/asset.dart'; +import 'package:immich_mobile/shared/providers/api.provider.dart'; +import 'package:immich_mobile/shared/providers/db.provider.dart'; + +final allVideoAssetsProvider = FutureProvider>( (ref) async { + final search = await ref.watch(apiServiceProvider).searchApi.search( + type: 'VIDEO', + ); + + if (search == null) { + return []; + } + + return ref.watch(dbProvider) + .assets + .getAllByRemoteId( + search.assets.items.map((e) => e.id), + ); + + /// This works offline, but we use the above + /* + return ref.watch(dbProvider).assets + .filter() + .durationInSecondsGreaterThan(0) + .findAll(); + */ +}); diff --git a/mobile/lib/modules/search/providers/recently_added.provider.dart b/mobile/lib/modules/search/providers/recently_added.provider.dart new file mode 100644 index 0000000000..a4c7a829b4 --- /dev/null +++ b/mobile/lib/modules/search/providers/recently_added.provider.dart @@ -0,0 +1,20 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/shared/models/asset.dart'; +import 'package:immich_mobile/shared/providers/api.provider.dart'; +import 'package:immich_mobile/shared/providers/db.provider.dart'; + +final recentlyAddedProvider = FutureProvider>( (ref) async { + final search = await ref.watch(apiServiceProvider).searchApi.search( + recent: true, + ); + + if (search == null) { + return []; + } + + return ref.watch(dbProvider) + .assets + .getAllByRemoteId( + search.assets.items.map((e) => e.id), + ); +}); 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 102573bcad..ab93f0670a 100644 --- a/mobile/lib/modules/search/providers/search_result_page.provider.dart +++ b/mobile/lib/modules/search/providers/search_result_page.provider.dart @@ -1,10 +1,8 @@ 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/asset_viewer/providers/render_list.provider.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:immich_mobile/shared/models/asset.dart'; class SearchResultPageNotifier extends StateNotifier { @@ -55,15 +53,6 @@ final searchResultPageProvider = }); final searchRenderListProvider = FutureProvider((ref) { - var settings = ref.watch(appSettingsServiceProvider); - 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); + return ref.watch(renderListProvider(assets)); }); diff --git a/mobile/lib/modules/search/ui/curated_row.dart b/mobile/lib/modules/search/ui/curated_row.dart new file mode 100644 index 0000000000..2c09828679 --- /dev/null +++ b/mobile/lib/modules/search/ui/curated_row.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import 'package:immich_mobile/modules/search/models/curated_content.dart'; +import 'package:immich_mobile/modules/search/ui/thumbnail_with_info.dart'; +import 'package:immich_mobile/shared/models/store.dart'; + +class CuratedRow extends StatelessWidget { + final List content; + final double imageSize; + + /// Callback with the content and the index when tapped + final Function(CuratedContent, int)? onTap; + + const CuratedRow({ + super.key, + required this.content, + this.imageSize = 200, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + + // Guard empty [content] + if (content.isEmpty) { + // Return empty thumbnail + return Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: SizedBox( + width: imageSize, + height: imageSize, + child: ThumbnailWithInfo( + textInfo: '', + onTap: () {}, + ), + ), + ), + ); + } + + return ListView.builder( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric( + horizontal: 16, + ), + itemBuilder: (context, index) { + final object = content[index]; + final thumbnailRequestUrl = + '${Store.get(StoreKey.serverEndpoint)}/asset/thumbnail/${object.id}'; + return SizedBox( + width: imageSize, + height: imageSize, + child: Padding( + padding: const EdgeInsets.only(right: 4.0), + child: ThumbnailWithInfo( + imageUrl: thumbnailRequestUrl, + textInfo: object.label, + onTap: () => onTap?.call(object, index), + ), + ), + ); + }, + itemCount: content.length, + ); + } +} diff --git a/mobile/lib/modules/search/views/all_motion_videos_page.dart b/mobile/lib/modules/search/views/all_motion_videos_page.dart new file mode 100644 index 0000000000..ba990a7da4 --- /dev/null +++ b/mobile/lib/modules/search/views/all_motion_videos_page.dart @@ -0,0 +1,35 @@ +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/ui/asset_grid/immich_asset_grid.dart'; +import 'package:immich_mobile/modules/search/providers/all_motion_photos.provider.dart'; +import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; + +class AllMotionPhotosPage extends HookConsumerWidget { + const AllMotionPhotosPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final motionPhotos = ref.watch(allMotionPhotosProvider); + + return Scaffold( + appBar: AppBar( + title: const Text('motion_photos_page_title').tr(), + leading: IconButton( + onPressed: () => AutoRouter.of(context).pop(), + icon: const Icon(Icons.arrow_back_ios_rounded), + ), + ), + body: motionPhotos.when( + data: (assets) => ImmichAssetGrid( + assets: assets, + ), + error: (e, s) => Text(e.toString()), + loading: () => const Center( + child: ImmichLoadingIndicator(), + ), + ), + ); + } +} diff --git a/mobile/lib/modules/search/views/all_videos_page.dart b/mobile/lib/modules/search/views/all_videos_page.dart new file mode 100644 index 0000000000..9461869aed --- /dev/null +++ b/mobile/lib/modules/search/views/all_videos_page.dart @@ -0,0 +1,35 @@ +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/ui/asset_grid/immich_asset_grid.dart'; +import 'package:immich_mobile/modules/search/providers/all_video_assets.provider.dart'; +import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; + +class AllVideosPage extends HookConsumerWidget { + const AllVideosPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final videos = ref.watch(allVideoAssetsProvider); + + return Scaffold( + appBar: AppBar( + title: const Text('all_videos_page_title').tr(), + leading: IconButton( + onPressed: () => AutoRouter.of(context).pop(), + icon: const Icon(Icons.arrow_back_ios_rounded), + ), + ), + body: videos.when( + data: (assets) => ImmichAssetGrid( + assets: assets, + ), + error: (e, s) => Text(e.toString()), + loading: () => const Center( + child: ImmichLoadingIndicator(), + ), + ), + ); + } +} diff --git a/mobile/lib/modules/search/views/curated_location_page.dart b/mobile/lib/modules/search/views/curated_location_page.dart index d4038a9651..59e4c3a875 100644 --- a/mobile/lib/modules/search/views/curated_location_page.dart +++ b/mobile/lib/modules/search/views/curated_location_page.dart @@ -1,3 +1,4 @@ +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'; @@ -25,6 +26,10 @@ class CuratedLocationPage extends HookConsumerWidget { fontSize: 16.0, ), ).tr(), + leading: IconButton( + onPressed: () => AutoRouter.of(context).pop(), + icon: const Icon(Icons.arrow_back_ios_rounded), + ), ), body: curatedLocation.when( loading: () => const Center(child: ImmichLoadingIndicator()), diff --git a/mobile/lib/modules/search/views/curated_object_page.dart b/mobile/lib/modules/search/views/curated_object_page.dart index 7823950e6f..15f44bf41a 100644 --- a/mobile/lib/modules/search/views/curated_object_page.dart +++ b/mobile/lib/modules/search/views/curated_object_page.dart @@ -1,3 +1,4 @@ +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'; @@ -28,6 +29,10 @@ class CuratedObjectPage extends HookConsumerWidget { fontSize: 16.0, ), ).tr(), + leading: IconButton( + onPressed: () => AutoRouter.of(context).pop(), + icon: const Icon(Icons.arrow_back_ios_rounded), + ), ), body: curatedObjects.when( loading: () => const Center(child: ImmichLoadingIndicator()), diff --git a/mobile/lib/modules/search/views/recently_added_page.dart b/mobile/lib/modules/search/views/recently_added_page.dart new file mode 100644 index 0000000000..fc079fbfc2 --- /dev/null +++ b/mobile/lib/modules/search/views/recently_added_page.dart @@ -0,0 +1,35 @@ +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/ui/asset_grid/immich_asset_grid.dart'; +import 'package:immich_mobile/modules/search/providers/recently_added.provider.dart'; +import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; + +class RecentlyAddedPage extends HookConsumerWidget { + const RecentlyAddedPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final recents = ref.watch(recentlyAddedProvider); + + return Scaffold( + appBar: AppBar( + title: const Text('recently_added_page_title').tr(), + leading: IconButton( + onPressed: () => AutoRouter.of(context).pop(), + icon: const Icon(Icons.arrow_back_ios_rounded), + ), + ), + body: recents.when( + data: (searchResponse) => ImmichAssetGrid( + assets: searchResponse, + ), + error: (e, s) => Text(e.toString()), + loading: () => const Center( + child: ImmichLoadingIndicator(), + ), + ), + ); + } +} diff --git a/mobile/lib/modules/search/views/search_page.dart b/mobile/lib/modules/search/views/search_page.dart index 73ef4cf32b..06cc5e687b 100644 --- a/mobile/lib/modules/search/views/search_page.dart +++ b/mobile/lib/modules/search/views/search_page.dart @@ -3,14 +3,13 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/modules/search/models/curated_content.dart'; import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart'; +import 'package:immich_mobile/modules/search/ui/curated_row.dart'; import 'package:immich_mobile/modules/search/ui/search_bar.dart'; import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart'; -import 'package:immich_mobile/modules/search/ui/thumbnail_with_info.dart'; import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/shared/models/store.dart'; import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; -import 'package:immich_mobile/utils/capitalize_first_letter.dart'; import 'package:openapi/api.dart'; // ignore: must_be_immutable @@ -26,8 +25,14 @@ class SearchPage extends HookConsumerWidget { ref.watch(getCuratedLocationProvider); AsyncValue> curatedObjects = ref.watch(getCuratedObjectProvider); - + var isDarkTheme = Theme.of(context).brightness == Brightness.dark; double imageSize = MediaQuery.of(context).size.width / 3; + TextStyle categoryTitleStyle = const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14.0, + ); + + Color categoryIconColor = isDarkTheme ? Colors.white : Colors.black; useEffect( () { @@ -50,100 +55,55 @@ class SearchPage extends HookConsumerWidget { child: curatedLocation.when( loading: () => const Center(child: ImmichLoadingIndicator()), error: (err, stack) => Center(child: Text('Error: $err')), - data: (curatedLocations) => ListView.builder( - padding: const EdgeInsets.symmetric( - horizontal: 16, - ), - scrollDirection: Axis.horizontal, - itemBuilder: (context, index) { - final locationInfo = curatedLocations[index]; - final thumbnailRequestUrl = - '${Store.get(StoreKey.serverEndpoint)}/asset/thumbnail/${locationInfo.id}'; - return SizedBox( - width: imageSize, - child: Padding( - padding: const EdgeInsets.only(right: 4.0), - child: ThumbnailWithInfo( - imageUrl: thumbnailRequestUrl, - textInfo: locationInfo.city, - onTap: () { - AutoRouter.of(context).push( - SearchResultRoute(searchTerm: locationInfo.city), - ); - }, + data: (locations) => CuratedRow( + content: locations + .map( + (o) => CuratedContent( + id: o.id, + label: o.city, ), - ), + ) + .toList(), + imageSize: imageSize, + onTap: (content, index) { + AutoRouter.of(context).push( + SearchResultRoute(searchTerm: content.label), ); }, - itemCount: curatedLocations.length.clamp(0, 10), - ), - ), - ); - } - - buildEmptyThumbnail() { - return Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: SizedBox( - width: imageSize, - height: imageSize, - child: ThumbnailWithInfo( - textInfo: '', - onTap: () {}, - ), ), ), ); } buildThings() { - return curatedObjects.when( - loading: () => SizedBox( - height: imageSize, - child: const Center(child: ImmichLoadingIndicator()), - ), - error: (err, stack) => SizedBox( - height: imageSize, - child: Center(child: Text('Error: $err')), - ), - data: (objects) => objects.isEmpty - ? buildEmptyThumbnail() - : SizedBox( - height: imageSize, - child: ListView.builder( - shrinkWrap: true, - scrollDirection: Axis.horizontal, - padding: const EdgeInsets.symmetric( - horizontal: 16, + return SizedBox( + height: imageSize, + child: curatedObjects.when( + loading: () => SizedBox( + height: imageSize, + child: const Center(child: ImmichLoadingIndicator()), + ), + error: (err, stack) => SizedBox( + height: imageSize, + child: Center(child: Text('Error: $err')), + ), + data: (objects) => CuratedRow( + content: objects + .map( + (o) => CuratedContent( + id: o.id, + label: o.object, ), - itemBuilder: (context, index) { - final curatedObjectInfo = objects[index]; - final thumbnailRequestUrl = - '${Store.get(StoreKey.serverEndpoint)}/asset/thumbnail/${curatedObjectInfo.id}'; - return SizedBox( - width: imageSize, - child: Padding( - padding: const EdgeInsets.only(right: 4.0), - child: ThumbnailWithInfo( - imageUrl: thumbnailRequestUrl, - textInfo: curatedObjectInfo.object, - onTap: () { - AutoRouter.of(context).push( - SearchResultRoute( - searchTerm: curatedObjectInfo.object - .capitalizeFirstLetter(), - ), - ); - }, - ), - ), - ); - }, - itemCount: objects.length.clamp(0, 10), - ), - ), + ) + .toList(), + imageSize: imageSize, + onTap: (content, index) { + AutoRouter.of(context).push( + SearchResultRoute(searchTerm: content.label), + ); + }, + ), + ), ); } @@ -169,12 +129,9 @@ class SearchPage extends HookConsumerWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text( + Text( "search_page_places", - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - ), + style: Theme.of(context).textTheme.titleMedium, ).tr(), TextButton( child: Text( @@ -194,19 +151,18 @@ class SearchPage extends HookConsumerWidget { ), buildPlaces(), Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 4.0, + padding: const EdgeInsets.only( + top: 24.0, + bottom: 4.0, + left: 16.0, + right: 16.0, ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text( + Text( "search_page_things", - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - ), + style: Theme.of(context).textTheme.titleMedium, ).tr(), TextButton( child: Text( @@ -225,6 +181,85 @@ class SearchPage extends HookConsumerWidget { ), ), buildThings(), + const SizedBox(height: 24.0), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + 'search_page_your_activity', + style: Theme.of(context).textTheme.titleMedium, + ).tr(), + ), + ListTile( + leading: Icon( + Icons.star_outline, + color: categoryIconColor, + ), + title: + Text('search_page_favorites', style: categoryTitleStyle) + .tr(), + onTap: () => AutoRouter.of(context).push( + const FavoritesRoute(), + ), + ), + const Padding( + padding: EdgeInsets.only( + left: 72, + right: 16, + ), + child: Divider(), + ), + ListTile( + leading: Icon( + Icons.schedule_outlined, + color: categoryIconColor, + ), + title: Text( + 'search_page_recently_added', + style: categoryTitleStyle, + ).tr(), + onTap: () => AutoRouter.of(context).push( + const RecentlyAddedRoute(), + ), + ), + const SizedBox(height: 24.0), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Text( + 'search_page_categories', + style: Theme.of(context).textTheme.titleMedium, + ).tr(), + ), + ListTile( + title: Text('search_page_videos', style: categoryTitleStyle) + .tr(), + leading: Icon( + Icons.play_circle_outline, + color: categoryIconColor, + ), + onTap: () => AutoRouter.of(context).push( + const AllVideosRoute(), + ), + ), + const Padding( + padding: EdgeInsets.only( + left: 72, + right: 16, + ), + child: Divider(), + ), + ListTile( + title: Text( + 'search_page_motion_photos', + style: categoryTitleStyle, + ).tr(), + leading: Icon( + Icons.motion_photos_on_outlined, + color: categoryIconColor, + ), + onTap: () => AutoRouter.of(context).push( + const AllMotionPhotosRoute(), + ), + ), ], ), if (isSearchEnabled) diff --git a/mobile/lib/modules/search/views/search_result_page.dart b/mobile/lib/modules/search/views/search_result_page.dart index 40f3305871..83b4b0deac 100644 --- a/mobile/lib/modules/search/views/search_result_page.dart +++ b/mobile/lib/modules/search/views/search_result_page.dart @@ -7,8 +7,6 @@ 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: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_loading_indicator.dart'; class SearchResultPage extends HookConsumerWidget { @@ -110,14 +108,8 @@ class SearchResultPage extends HookConsumerWidget { buildSearchResult() { var searchResultPageState = ref.watch(searchResultPageProvider); - var searchResultRenderList = ref.watch(searchRenderListProvider); var allSearchAssets = ref.watch(searchResultPageProvider).searchResult; - var settings = ref.watch(appSettingsServiceProvider); - final assetsPerRow = settings.getSetting(AppSettingsEnum.tilesPerRow); - final showStorageIndicator = - settings.getSetting(AppSettingsEnum.storageIndicator); - if (searchResultPageState.isError) { return Padding( padding: const EdgeInsets.all(12), @@ -129,22 +121,10 @@ class SearchResultPage extends HookConsumerWidget { return const Center(child: ImmichLoadingIndicator()); } + if (searchResultPageState.isSuccess) { - return searchResultRenderList.when( - data: (result) { - return ImmichAssetGrid( - allAssets: allSearchAssets, - renderList: result, - assetsPerRow: assetsPerRow, - showStorageIndicator: showStorageIndicator, - ); - }, - error: (err, stack) { - return Text("$err"); - }, - loading: () { - return const CircularProgressIndicator(); - }, + return ImmichAssetGrid( + assets: allSearchAssets, ); } diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index b85e01e864..389fc5c900 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -21,8 +21,11 @@ import 'package:immich_mobile/modules/login/views/change_password_page.dart'; import 'package:immich_mobile/modules/login/views/login_page.dart'; import 'package:immich_mobile/modules/onboarding/providers/gallery_permission.provider.dart'; import 'package:immich_mobile/modules/onboarding/views/permission_onboarding_page.dart'; +import 'package:immich_mobile/modules/search/views/all_motion_videos_page.dart'; +import 'package:immich_mobile/modules/search/views/all_videos_page.dart'; import 'package:immich_mobile/modules/search/views/curated_location_page.dart'; import 'package:immich_mobile/modules/search/views/curated_object_page.dart'; +import 'package:immich_mobile/modules/search/views/recently_added_page.dart'; import 'package:immich_mobile/modules/search/views/search_page.dart'; import 'package:immich_mobile/modules/search/views/search_result_page.dart'; import 'package:immich_mobile/modules/settings/views/settings_page.dart'; @@ -70,6 +73,9 @@ part 'router.gr.dart'; AutoRoute(page: CuratedObjectPage, guards: [AuthGuard, DuplicateGuard]), AutoRoute(page: CreateAlbumPage, guards: [AuthGuard, DuplicateGuard]), AutoRoute(page: FavoritesPage, guards: [AuthGuard, DuplicateGuard]), + AutoRoute(page: AllVideosPage, guards: [AuthGuard, DuplicateGuard]), + AutoRoute(page: AllMotionPhotosPage, guards: [AuthGuard, DuplicateGuard]), + AutoRoute(page: RecentlyAddedPage, guards: [AuthGuard, DuplicateGuard],), CustomRoute( page: AssetSelectionPage, guards: [AuthGuard, DuplicateGuard], diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index 8af37a2f0b..d8a9ada72d 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -131,6 +131,29 @@ class _$AppRouter extends RootStackRouter { child: const FavoritesPage(), ); }, + AllVideosRoute.name: (routeData) { + return MaterialPageX( + routeData: routeData, + child: const AllVideosPage(), + ); + }, + AllMotionPhotosRoute.name: (routeData) { + return MaterialPageX( + routeData: routeData, + child: const AllMotionPhotosPage(), + ); + }, + RecentlyAddedRoute.name: (routeData) { + return CustomPage( + routeData: routeData, + child: const RecentlyAddedPage(), + transitionsBuilder: TransitionsBuilders.noTransition, + durationInMilliseconds: 200, + reverseDurationInMilliseconds: 200, + opaque: true, + barrierDismissible: false, + ); + }, AssetSelectionRoute.name: (routeData) { return CustomPage( routeData: routeData, @@ -375,6 +398,30 @@ class _$AppRouter extends RootStackRouter { duplicateGuard, ], ), + RouteConfig( + AllVideosRoute.name, + path: '/all-videos-page', + guards: [ + authGuard, + duplicateGuard, + ], + ), + RouteConfig( + AllMotionPhotosRoute.name, + path: '/all-motion-photos-page', + guards: [ + authGuard, + duplicateGuard, + ], + ), + RouteConfig( + RecentlyAddedRoute.name, + path: '/recently-added-page', + guards: [ + authGuard, + duplicateGuard, + ], + ), RouteConfig( AssetSelectionRoute.name, path: '/asset-selection-page', @@ -721,6 +768,42 @@ class FavoritesRoute extends PageRouteInfo { static const String name = 'FavoritesRoute'; } +/// generated route for +/// [AllVideosPage] +class AllVideosRoute extends PageRouteInfo { + const AllVideosRoute() + : super( + AllVideosRoute.name, + path: '/all-videos-page', + ); + + static const String name = 'AllVideosRoute'; +} + +/// generated route for +/// [AllMotionPhotosPage] +class AllMotionPhotosRoute extends PageRouteInfo { + const AllMotionPhotosRoute() + : super( + AllMotionPhotosRoute.name, + path: '/all-motion-photos-page', + ); + + static const String name = 'AllMotionPhotosRoute'; +} + +/// generated route for +/// [RecentlyAddedPage] +class RecentlyAddedRoute extends PageRouteInfo { + const RecentlyAddedRoute() + : super( + RecentlyAddedRoute.name, + path: '/recently-added-page', + ); + + static const String name = 'RecentlyAddedRoute'; +} + /// generated route for /// [AssetSelectionPage] class AssetSelectionRoute extends PageRouteInfo { diff --git a/mobile/lib/shared/views/tab_controller_page.dart b/mobile/lib/shared/views/tab_controller_page.dart index 9881b58c2a..d0b3c01ac3 100644 --- a/mobile/lib/shared/views/tab_controller_page.dart +++ b/mobile/lib/shared/views/tab_controller_page.dart @@ -169,7 +169,10 @@ class TabControllerPage extends ConsumerWidget { ); } return Scaffold( - body: body, + body: HeroControllerScope( + controller: HeroController(), + child: body, + ), bottomNavigationBar: multiselectEnabled ? null : bottom, ); }, diff --git a/mobile/lib/utils/immich_app_theme.dart b/mobile/lib/utils/immich_app_theme.dart index c60b14c233..77571781f8 100644 --- a/mobile/lib/utils/immich_app_theme.dart +++ b/mobile/lib/utils/immich_app_theme.dart @@ -41,6 +41,8 @@ ThemeData immichLightTheme = ThemeData( titleTextStyle: const TextStyle( fontFamily: 'WorkSans', color: Colors.indigo, + fontWeight: FontWeight.bold, + fontSize: 18, ), backgroundColor: immichBackgroundColor, foregroundColor: Colors.indigo, @@ -75,6 +77,17 @@ ThemeData immichLightTheme = ThemeData( fontWeight: FontWeight.bold, color: Colors.indigo, ), + titleSmall: TextStyle( + fontSize: 16.0, + ), + titleMedium: TextStyle( + fontSize: 18.0, + fontWeight: FontWeight.bold, + ), + titleLarge: TextStyle( + fontSize: 26.0, + fontWeight: FontWeight.bold, + ), ), elevatedButtonTheme: ElevatedButtonThemeData( style: ElevatedButton.styleFrom( @@ -127,6 +140,8 @@ ThemeData immichDarkTheme = ThemeData( titleTextStyle: TextStyle( fontFamily: 'WorkSans', color: immichDarkThemePrimaryColor, + fontWeight: FontWeight.bold, + fontSize: 18, ), backgroundColor: const Color.fromARGB(255, 32, 33, 35), foregroundColor: immichDarkThemePrimaryColor, @@ -159,6 +174,18 @@ ThemeData immichDarkTheme = ThemeData( fontWeight: FontWeight.bold, color: immichDarkThemePrimaryColor, ), + titleSmall: const TextStyle( + fontSize: 16.0, + ), + titleMedium: const TextStyle( + fontSize: 18.0, + fontWeight: FontWeight.bold, + ), + titleLarge: const TextStyle( + fontSize: 26.0, + fontWeight: FontWeight.bold, + ), + ), cardColor: Colors.grey[900], elevatedButtonTheme: ElevatedButtonThemeData( diff --git a/mobile/openapi/lib/model/asset_response_dto.dart b/mobile/openapi/lib/model/asset_response_dto.dart index 013ac00ff4b9bdd20e83e59dd56543cc015d5b19..e6b728566dae81cbcd1c8be5a82cbd8f6c477122 100644 GIT binary patch delta 538 zcmZ4H{n=-OD~B4F0vKebq~?`mCMV`-lw@QU>nP-xWTX~pD%8|KyuCZsxL> zybVa-vM6JW6apgGvihCKvFCPd>mSH~9{a$z(oWnaLWwCX=Ih zT_(=~(&u>1CNuH5Om^WjV}n?-xr$GOY4QXC`N$VsYN9k8k!2#PzUSlPtKQ6V**lB zBr-wNABjvsph^XWT1{(&ngU5NpycLO$tYGyuxLtUUSe)$vO-pIex450w8`(}#F&Ay zn_1+WnTb%gMv0fv4r&J|E`bqQRH;{%m{XdngCv|@lwY2hl#^NN()*}SBiSS zrD|!CVTcdeQ2%&n<;0L|Et{H+UM`A7E{;vE9!Fd!p1CAKJPFC}f~q(W_$mzWb(rGY zVzTPmiep&j-?C%A6EA!pGT}B%gxj$j?!#@k-HTRK29XjP5`@eYEpybC3{@H@-Nez2 zV$M(t{x*yY9oP{hoC;~&2^T;lkIP60-Xj^1=#h67eGPhNv2+kmH4(hmq>!xDu~Do0 zw^q}i72mKm$F)u3*sk_tdWrZ*d4*asm3d?8YeAN*U@IB(4wCC-{{~|sA0V3P*rnW( aGUsYpGHqMS%H*8Z^{cYpLF08Gz*2a