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 <alex.tran1502@gmail.com>
This commit is contained in:
martyfuhry
2023-03-24 23:44:53 -04:00
committed by GitHub
parent d2600e0ddd
commit 501b96baf7
23 changed files with 1013 additions and 524 deletions

View File

@@ -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(),
),
),
);
}
}

View File

@@ -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(),
),
),
);
}
}

View File

@@ -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()),

View File

@@ -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()),

View File

@@ -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(),
),
),
);
}
}

View File

@@ -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<List<CuratedObjectsResponseDto>> 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)

View File

@@ -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,
);
}