1
0
mirror of https://github.com/immich-app/immich.git synced 2024-12-25 10:43:13 +02:00

Enable swiping between assets (#381)

Enable swiping between assets (#381)

Co-authored-by: Alex <alex.tran1502@gmail.com>
Co-authored-by: Malte Kiefer <59220985+MalteKiefer@users.noreply.github.com>
Co-authored-by: Matthias Rupp <matthias.rupp@posteo.de>
This commit is contained in:
Stevenson Chittumuri 2022-08-03 16:36:12 -04:00 committed by GitHub
parent e8d1f89a47
commit 8c184dc4d4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 372 additions and 189 deletions

2
mobile/.gitignore vendored
View File

@ -24,7 +24,7 @@
# Flutter/Dart/Pub related # Flutter/Dart/Pub related
**/doc/api/ **/doc/api/
**/ios/Flutter/.last_build_id **/ios/
.dart_tool/ .dart_tool/
.flutter-plugins .flutter-plugins
.flutter-plugins-dependencies .flutter-plugins-dependencies

View File

@ -17,7 +17,6 @@ import 'package:immich_mobile/shared/providers/server_info.provider.dart';
import 'package:immich_mobile/shared/providers/websocket.provider.dart'; import 'package:immich_mobile/shared/providers/websocket.provider.dart';
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart'; import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
import 'package:immich_mobile/shared/views/version_announcement_overlay.dart'; import 'package:immich_mobile/shared/views/version_announcement_overlay.dart';
import 'constants/hive_box.dart'; import 'constants/hive_box.dart';
void main() async { void main() async {

View File

@ -20,7 +20,9 @@ class AlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
} }
Future<AlbumResponseDto?> createAlbum( Future<AlbumResponseDto?> createAlbum(
String albumTitle, Set<AssetResponseDto> assets) async { String albumTitle,
Set<AssetResponseDto> assets,
) async {
AlbumResponseDto? album = AlbumResponseDto? album =
await _albumService.createAlbum(albumTitle, assets, []); await _albumService.createAlbum(albumTitle, assets, []);

View File

@ -12,8 +12,13 @@ import 'package:openapi/api.dart';
class AlbumViewerThumbnail extends HookConsumerWidget { class AlbumViewerThumbnail extends HookConsumerWidget {
final AssetResponseDto asset; final AssetResponseDto asset;
final List<AssetResponseDto> assetList;
const AlbumViewerThumbnail({Key? key, required this.asset}) : super(key: key); const AlbumViewerThumbnail({
Key? key,
required this.asset,
required this.assetList,
}) : super(key: key);
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@ -28,25 +33,13 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
ref.watch(assetSelectionProvider).isMultiselectEnable; ref.watch(assetSelectionProvider).isMultiselectEnable;
_viewAsset() { _viewAsset() {
if (asset.type == AssetTypeEnum.IMAGE) { AutoRouter.of(context).push(
AutoRouter.of(context).push( GalleryViewerRoute(
ImageViewerRoute( asset: asset,
imageUrl: assetList: assetList,
'${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=false', thumbnailRequestUrl: thumbnailRequestUrl,
heroTag: asset.id, ),
thumbnailUrl: thumbnailRequestUrl, );
asset: asset,
),
);
} else {
AutoRouter.of(context).push(
VideoViewerRoute(
videoUrl:
'${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}',
asset: asset,
),
);
}
} }
BoxBorder drawBorderColor() { BoxBorder drawBorderColor() {

View File

@ -29,9 +29,7 @@ class AlbumViewerPage extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
FocusNode titleFocusNode = useFocusNode(); FocusNode titleFocusNode = useFocusNode();
ScrollController scrollController = useScrollController(); ScrollController scrollController = useScrollController();
var albumInfo = ref.watch(sharedAlbumDetailProvider(albumId));
AsyncValue<AlbumResponseDto?> albumInfo =
ref.watch(sharedAlbumDetailProvider(albumId));
final userId = ref.watch(authenticationProvider).userId; final userId = ref.watch(authenticationProvider).userId;
@ -200,7 +198,10 @@ class AlbumViewerPage extends HookConsumerWidget {
), ),
delegate: SliverChildBuilderDelegate( delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) { (BuildContext context, int index) {
return AlbumViewerThumbnail(asset: albumInfo.assets[index]); return AlbumViewerThumbnail(
asset: albumInfo.assets[index],
assetList: albumInfo.assets,
);
}, },
childCount: albumInfo.assets.length, childCount: albumInfo.assets.length,
), ),

View File

@ -15,7 +15,6 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
bool allowMoving = _status == _RemoteImageStatus.full; bool allowMoving = _status == _RemoteImageStatus.full;
return PhotoView( return PhotoView(
imageProvider: _imageProvider, imageProvider: _imageProvider,
minScale: PhotoViewComputedScale.contained, minScale: PhotoViewComputedScale.contained,
@ -32,8 +31,9 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
PhotoViewControllerValue controllerValue, PhotoViewControllerValue controllerValue,
) { ) {
// Disable swipe events when zoomed in // Disable swipe events when zoomed in
if (_zoomedIn) return; if (_zoomedIn) {
return;
}
if (controllerValue.position.dy > swipeThreshold) { if (controllerValue.position.dy > swipeThreshold) {
widget.onSwipeDown(); widget.onSwipeDown();
} else if (controllerValue.position.dy < -swipeThreshold) { } else if (controllerValue.position.dy < -swipeThreshold) {
@ -42,7 +42,14 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
} }
void _scaleStateChanged(PhotoViewScaleState state) { void _scaleStateChanged(PhotoViewScaleState state) {
_zoomedIn = state == PhotoViewScaleState.zoomedIn; // _onScaleListener;
_zoomedIn = state != PhotoViewScaleState.initial;
if (_zoomedIn) {
widget.isZoomedListener.value = true;
} else {
widget.isZoomedListener.value = false;
}
widget.isZoomedFunction();
} }
CachedNetworkImageProvider _authorizedImageProvider(String url) { CachedNetworkImageProvider _authorizedImageProvider(String url) {
@ -107,6 +114,8 @@ class RemotePhotoView extends StatefulWidget {
required this.thumbnailUrl, required this.thumbnailUrl,
required this.imageUrl, required this.imageUrl,
required this.authToken, required this.authToken,
required this.isZoomedFunction,
required this.isZoomedListener,
required this.onSwipeDown, required this.onSwipeDown,
required this.onSwipeUp, required this.onSwipeUp,
}) : super(key: key); }) : super(key: key);
@ -117,6 +126,9 @@ class RemotePhotoView extends StatefulWidget {
final void Function() onSwipeDown; final void Function() onSwipeDown;
final void Function() onSwipeUp; final void Function() onSwipeUp;
final void Function() isZoomedFunction;
final ValueNotifier<bool> isZoomedListener;
@override @override
State<StatefulWidget> createState() { State<StatefulWidget> createState() {

View File

@ -0,0 +1,134 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_swipe_detector/flutter_swipe_detector.dart';
import 'package:hive/hive.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart';
import 'package:immich_mobile/modules/asset_viewer/views/image_viewer_page.dart';
import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart';
import 'package:immich_mobile/modules/home/services/asset.service.dart';
import 'package:openapi/api.dart';
// ignore: must_be_immutable
class GalleryViewerPage extends HookConsumerWidget {
late List<AssetResponseDto> assetList;
final AssetResponseDto asset;
final String thumbnailRequestUrl;
GalleryViewerPage({
Key? key,
required this.assetList,
required this.asset,
required this.thumbnailRequestUrl,
}) : super(key: key);
AssetResponseDto? assetDetail;
@override
Widget build(BuildContext context, WidgetRef ref) {
final Box<dynamic> box = Hive.box(userInfoBox);
int indexOfAsset = assetList.indexOf(asset);
@override
void initState(int index) {
indexOfAsset = index;
}
PageController controller =
PageController(initialPage: assetList.indexOf(asset));
getAssetExif() async {
assetDetail = await ref
.watch(assetServiceProvider)
.getAssetById(assetList[indexOfAsset].id);
}
void showInfo() {
showModalBottomSheet(
backgroundColor: Colors.black,
barrierColor: Colors.transparent,
isScrollControlled: false,
context: context,
builder: (context) {
return ExifBottomSheet(assetDetail: assetDetail!);
},
);
}
final isZoomed = useState<bool>(false);
ValueNotifier<bool> isZoomedListener = ValueNotifier<bool>(false);
//make isZoomed listener call instead
void isZoomedMethod() {
if (isZoomedListener.value) {
isZoomed.value = true;
} else {
isZoomed.value = false;
}
}
return Scaffold(
backgroundColor: Colors.black,
appBar: TopControlAppBar(
asset: assetList[indexOfAsset],
onMoreInfoPressed: () {
showInfo();
},
onDownloadPressed: () {
ref
.watch(imageViewerStateProvider.notifier)
.downloadAsset(assetList[indexOfAsset], context);
},
),
body: SafeArea(
child: PageView.builder(
controller: controller,
pageSnapping: true,
physics: isZoomed.value
? const NeverScrollableScrollPhysics()
: const BouncingScrollPhysics(),
itemCount: assetList.length,
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) {
initState(index);
getAssetExif();
if (assetList[index].type == AssetTypeEnum.IMAGE) {
return ImageViewerPage(
thumbnailUrl:
'${box.get(serverEndpointKey)}/asset/thumbnail/${assetList[index].id}',
imageUrl:
'${box.get(serverEndpointKey)}/asset/file?aid=${assetList[index].deviceAssetId}&did=${assetList[index].deviceId}&isThumb=false',
authToken: 'Bearer ${box.get(accessTokenKey)}',
isZoomedFunction: isZoomedMethod,
isZoomedListener: isZoomedListener,
asset: assetList[index],
heroTag: assetList[index].id,
);
} else {
return SwipeDetector(
onSwipeDown: (_) {
AutoRouter.of(context).pop();
},
onSwipeUp: (_) {
showInfo();
},
child: Hero(
tag: assetList[index].id,
child: VideoViewerPage(
asset: assetList[index],
videoUrl:
'${box.get(serverEndpointKey)}/asset/file?aid=${assetList[index].deviceAssetId}&did=${assetList[index].deviceId}',
),
),
);
}
},
),
),
);
}
}

View File

@ -1,15 +1,12 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hive/hive.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart'; import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart'; import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/download_loading_indicator.dart'; import 'package:immich_mobile/modules/asset_viewer/ui/download_loading_indicator.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart'; import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/remote_photo_view.dart'; import 'package:immich_mobile/modules/asset_viewer/ui/remote_photo_view.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart';
import 'package:immich_mobile/modules/home/services/asset.service.dart'; import 'package:immich_mobile/modules/home/services/asset.service.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
@ -19,8 +16,9 @@ class ImageViewerPage extends HookConsumerWidget {
final String heroTag; final String heroTag;
final String thumbnailUrl; final String thumbnailUrl;
final AssetResponseDto asset; final AssetResponseDto asset;
final String authToken;
AssetResponseDto? assetDetail; final ValueNotifier<bool> isZoomedListener;
final void Function() isZoomedFunction;
ImageViewerPage({ ImageViewerPage({
Key? key, Key? key,
@ -28,31 +26,22 @@ class ImageViewerPage extends HookConsumerWidget {
required this.heroTag, required this.heroTag,
required this.thumbnailUrl, required this.thumbnailUrl,
required this.asset, required this.asset,
required this.authToken,
required this.isZoomedFunction,
required this.isZoomedListener,
}) : super(key: key); }) : super(key: key);
AssetResponseDto? assetDetail;
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final downloadAssetStatus = final downloadAssetStatus =
ref.watch(imageViewerStateProvider).downloadAssetStatus; ref.watch(imageViewerStateProvider).downloadAssetStatus;
var box = Hive.box(userInfoBox);
getAssetExif() async { getAssetExif() async {
assetDetail = assetDetail =
await ref.watch(assetServiceProvider).getAssetById(asset.id); await ref.watch(assetServiceProvider).getAssetById(asset.id);
} }
showInfo() {
showModalBottomSheet(
backgroundColor: Colors.black,
barrierColor: Colors.transparent,
isScrollControlled: false,
context: context,
builder: (context) {
return ExifBottomSheet(assetDetail: assetDetail!);
},
);
}
useEffect( useEffect(
() { () {
getAssetExif(); getAssetExif();
@ -61,39 +50,39 @@ class ImageViewerPage extends HookConsumerWidget {
[], [],
); );
return Scaffold( showInfo() {
backgroundColor: Colors.black, showModalBottomSheet(
appBar: TopControlAppBar( backgroundColor: Colors.black,
asset: asset, barrierColor: Colors.transparent,
onMoreInfoPressed: showInfo, isScrollControlled: false,
onDownloadPressed: () { context: context,
ref builder: (context) {
.watch(imageViewerStateProvider.notifier) return ExifBottomSheet(assetDetail: assetDetail ?? asset);
.downloadAsset(asset, context);
}, },
), );
body: SafeArea( }
child: Stack(
children: [ return Stack(
Center( children: [
child: Hero( Center(
tag: heroTag, child: Hero(
child: RemotePhotoView( tag: heroTag,
thumbnailUrl: thumbnailUrl, child: RemotePhotoView(
imageUrl: imageUrl, thumbnailUrl: thumbnailUrl,
authToken: "Bearer ${box.get(accessTokenKey)}", imageUrl: imageUrl,
onSwipeDown: () => AutoRouter.of(context).pop(), authToken: authToken,
onSwipeUp: () => showInfo(), isZoomedFunction: isZoomedFunction,
), isZoomedListener: isZoomedListener,
), onSwipeDown: () => AutoRouter.of(context).pop(),
onSwipeUp: () => showInfo(),
), ),
if (downloadAssetStatus == DownloadAssetStatus.loading) ),
const Center(
child: DownloadLoadingIndicator(),
),
],
), ),
), if (downloadAssetStatus == DownloadAssetStatus.loading)
const Center(
child: DownloadLoadingIndicator(),
),
],
); );
} }
} }

View File

@ -1,7 +1,4 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_swipe_detector/flutter_swipe_detector.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart'; import 'package:immich_mobile/constants/hive_box.dart';
@ -9,9 +6,6 @@ import 'package:chewie/chewie.dart';
import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart'; import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart'; import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/download_loading_indicator.dart'; import 'package:immich_mobile/modules/asset_viewer/ui/download_loading_indicator.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart';
import 'package:immich_mobile/modules/home/services/asset.service.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
import 'package:video_player/video_player.dart'; import 'package:video_player/video_player.dart';
@ -31,66 +25,17 @@ class VideoViewerPage extends HookConsumerWidget {
String jwtToken = Hive.box(userInfoBox).get(accessTokenKey); String jwtToken = Hive.box(userInfoBox).get(accessTokenKey);
void showInfo() { return Stack(
showModalBottomSheet( children: [
backgroundColor: Colors.black, VideoThumbnailPlayer(
barrierColor: Colors.transparent, url: videoUrl,
isScrollControlled: false, jwtToken: jwtToken,
context: context,
builder: (context) {
return ExifBottomSheet(assetDetail: assetDetail!);
},
);
}
getAssetExif() async {
assetDetail =
await ref.watch(assetServiceProvider).getAssetById(asset.id);
}
useEffect(
() {
getAssetExif();
return null;
},
[],
);
return Scaffold(
backgroundColor: Colors.black,
appBar: TopControlAppBar(
asset: asset,
onMoreInfoPressed: () {
showInfo();
},
onDownloadPressed: () {
ref
.watch(imageViewerStateProvider.notifier)
.downloadAsset(asset, context);
},
),
body: SwipeDetector(
onSwipeDown: (_) {
AutoRouter.of(context).pop();
},
onSwipeUp: (_) {
showInfo();
},
child: SafeArea(
child: Stack(
children: [
VideoThumbnailPlayer(
url: videoUrl,
jwtToken: jwtToken,
),
if (downloadAssetStatus == DownloadAssetStatus.loading)
const Center(
child: DownloadLoadingIndicator(),
),
],
),
), ),
), if (downloadAssetStatus == DownloadAssetStatus.loading)
const Center(
child: DownloadLoadingIndicator(),
),
],
); );
} }
} }
@ -134,10 +79,13 @@ class _VideoThumbnailPlayerState extends State<VideoThumbnailPlayer> {
_createChewieController() { _createChewieController() {
chewieController = ChewieController( chewieController = ChewieController(
showOptions: true, showOptions: true,
showControlsOnInitialize: false, showControlsOnInitialize: true,
videoPlayerController: videoPlayerController, videoPlayerController: videoPlayerController,
autoPlay: true, autoPlay: true,
autoInitialize: false, autoInitialize: true,
allowFullScreen: true,
showControls: true,
hideControlsTimer: const Duration(seconds: 5),
); );
} }
@ -157,11 +105,13 @@ class _VideoThumbnailPlayerState extends State<VideoThumbnailPlayer> {
controller: chewieController!, controller: chewieController!,
), ),
) )
: const SizedBox( : const Center(
width: 75, child: SizedBox(
height: 75, width: 75,
child: CircularProgressIndicator.adaptive( height: 75,
strokeWidth: 2, child: CircularProgressIndicator.adaptive(
strokeWidth: 2,
),
), ),
); );
} }

View File

@ -162,6 +162,10 @@ class BackupNotifier extends StateNotifier<BackUpState> {
onlyAll: true, onlyAll: true,
type: RequestType.common, type: RequestType.common,
); );
if (list.isEmpty) {
return;
}
AssetPathEntity albumHasAllAssets = list.first; AssetPathEntity albumHasAllAssets = list.first;
backupAlbumInfoBox.put( backupAlbumInfoBox.put(

View File

@ -3,10 +3,18 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/home/ui/thumbnail_image.dart'; import 'package:immich_mobile/modules/home/ui/thumbnail_image.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
// ignore: must_be_immutable
class ImageGrid extends ConsumerWidget { class ImageGrid extends ConsumerWidget {
final List<AssetResponseDto> assetGroup; final List<AssetResponseDto> assetGroup;
final List<AssetResponseDto> sortedAssetGroup;
const ImageGrid({Key? key, required this.assetGroup}) : super(key: key); ImageGrid({
Key? key,
required this.assetGroup,
required this.sortedAssetGroup,
}) : super(key: key);
List<AssetResponseDto> imageSortedList = [];
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@ -19,12 +27,14 @@ class ImageGrid extends ConsumerWidget {
delegate: SliverChildBuilderDelegate( delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) { (BuildContext context, int index) {
var assetType = assetGroup[index].type; var assetType = assetGroup[index].type;
return GestureDetector( return GestureDetector(
onTap: () {}, onTap: () {},
child: Stack( child: Stack(
children: [ children: [
ThumbnailImage(asset: assetGroup[index]), ThumbnailImage(
asset: assetGroup[index],
assetList: sortedAssetGroup,
),
if (assetType != AssetTypeEnum.IMAGE) if (assetType != AssetTypeEnum.IMAGE)
Positioned( Positioned(
top: 5, top: 5,

View File

@ -13,8 +13,10 @@ import 'package:openapi/api.dart';
class ThumbnailImage extends HookConsumerWidget { class ThumbnailImage extends HookConsumerWidget {
final AssetResponseDto asset; final AssetResponseDto asset;
final List<AssetResponseDto> assetList;
const ThumbnailImage({Key? key, required this.asset}) : super(key: key); const ThumbnailImage({Key? key, required this.asset, required this.assetList})
: super(key: key);
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@ -60,29 +62,17 @@ class ThumbnailImage extends HookConsumerWidget {
.watch(homePageStateProvider.notifier) .watch(homePageStateProvider.notifier)
.addSingleSelectedItem(asset); .addSingleSelectedItem(asset);
} else { } else {
if (asset.type == AssetTypeEnum.IMAGE) { AutoRouter.of(context).push(
AutoRouter.of(context).push( GalleryViewerRoute(
ImageViewerRoute( assetList: assetList,
imageUrl: thumbnailRequestUrl: thumbnailRequestUrl,
'${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=false', asset: asset,
heroTag: asset.id, ),
thumbnailUrl: thumbnailRequestUrl, );
asset: asset,
),
);
} else {
AutoRouter.of(context).push(
VideoViewerRoute(
videoUrl:
'${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}',
asset: asset,
),
);
}
} }
}, },
onLongPress: () { onLongPress: () {
// Enable multi selecte function // Enable multi select function
ref.watch(homePageStateProvider.notifier).enableMultiSelect({asset}); ref.watch(homePageStateProvider.notifier).enableMultiSelect({asset});
HapticFeedback.heavyImpact(); HapticFeedback.heavyImpact();
}, },

View File

@ -10,9 +10,11 @@ import 'package:immich_mobile/modules/home/ui/image_grid.dart';
import 'package:immich_mobile/modules/home/ui/immich_sliver_appbar.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/monthly_title_text.dart';
import 'package:immich_mobile/modules/home/ui/profile_drawer.dart'; import 'package:immich_mobile/modules/home/ui/profile_drawer.dart';
import 'package:immich_mobile/shared/providers/asset.provider.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/server_info.provider.dart';
import 'package:immich_mobile/shared/providers/websocket.provider.dart'; import 'package:immich_mobile/shared/providers/websocket.provider.dart';
import 'package:openapi/api.dart';
class HomePage extends HookConsumerWidget { class HomePage extends HookConsumerWidget {
const HomePage({Key? key}) : super(key: key); const HomePage({Key? key}) : super(key: key);
@ -25,6 +27,13 @@ class HomePage extends HookConsumerWidget {
var isMultiSelectEnable = var isMultiSelectEnable =
ref.watch(homePageStateProvider).isMultiSelectEnable; ref.watch(homePageStateProvider).isMultiSelectEnable;
var homePageState = ref.watch(homePageStateProvider); var homePageState = ref.watch(homePageStateProvider);
List<AssetResponseDto> sortedAssetList = [];
// set sorted List
for (var group in assetGroupByDateTime.values) {
for (var value in group) {
sortedAssetList.add(value);
}
}
useEffect( useEffect(
() { () {
@ -73,7 +82,10 @@ class HomePage extends HookConsumerWidget {
); );
imageGridGroup.add( imageGridGroup.add(
ImageGrid(assetGroup: immichAssetList), ImageGrid(
assetGroup: immichAssetList,
sortedAssetGroup: sortedAssetList,
),
); );
lastMonth = currentMonth; lastMonth = currentMonth;

View File

@ -11,6 +11,7 @@ import 'package:immich_mobile/modules/home/ui/monthly_title_text.dart';
import 'package:immich_mobile/modules/search/providers/search_page_state.provider.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/providers/search_result_page.provider.dart';
import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart'; import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart';
import 'package:openapi/api.dart';
class SearchResultPage extends HookConsumerWidget { class SearchResultPage extends HookConsumerWidget {
const SearchResultPage({Key? key, required this.searchTerm}) const SearchResultPage({Key? key, required this.searchTerm})
@ -27,7 +28,9 @@ class SearchResultPage extends HookConsumerWidget {
final List<Widget> imageGridGroup = []; final List<Widget> imageGridGroup = [];
late FocusNode searchFocusNode; FocusNode? searchFocusNode;
List<AssetResponseDto> sortedAssetList = [];
useEffect( useEffect(
() { () {
@ -37,14 +40,14 @@ class SearchResultPage extends HookConsumerWidget {
Duration.zero, Duration.zero,
() => ref.read(searchResultPageProvider.notifier).search(searchTerm), () => ref.read(searchResultPageProvider.notifier).search(searchTerm),
); );
return () => searchFocusNode.dispose(); return () => searchFocusNode?.dispose();
}, },
[], [],
); );
_onSearchSubmitted(String newSearchTerm) { _onSearchSubmitted(String newSearchTerm) {
debugPrint("Re-Search with $newSearchTerm"); debugPrint("Re-Search with $newSearchTerm");
searchFocusNode.unfocus(); searchFocusNode?.unfocus();
isNewSearch.value = false; isNewSearch.value = false;
currentSearchTerm.value = newSearchTerm; currentSearchTerm.value = newSearchTerm;
ref.watch(searchResultPageProvider.notifier).search(newSearchTerm); ref.watch(searchResultPageProvider.notifier).search(newSearchTerm);
@ -58,7 +61,7 @@ class SearchResultPage extends HookConsumerWidget {
onTap: () { onTap: () {
searchTermController.clear(); searchTermController.clear();
ref.watch(searchPageStateProvider.notifier).setSearchTerm(""); ref.watch(searchPageStateProvider.notifier).setSearchTerm("");
searchFocusNode.requestFocus(); searchFocusNode?.requestFocus();
}, },
textInputAction: TextInputAction.search, textInputAction: TextInputAction.search,
onSubmitted: (searchTerm) { onSubmitted: (searchTerm) {
@ -131,7 +134,12 @@ class SearchResultPage extends HookConsumerWidget {
if (searchResultPageState.isSuccess) { if (searchResultPageState.isSuccess) {
if (searchResultPageState.searchResult.isNotEmpty) { if (searchResultPageState.searchResult.isNotEmpty) {
int? lastMonth; int? lastMonth;
// set sorted List
for (var group in assetGroupByDateTime.values) {
for (var value in group) {
sortedAssetList.add(value);
}
}
assetGroupByDateTime.forEach((dateGroup, immichAssetList) { assetGroupByDateTime.forEach((dateGroup, immichAssetList) {
DateTime parseDateGroup = DateTime.parse(dateGroup); DateTime parseDateGroup = DateTime.parse(dateGroup);
int currentMonth = parseDateGroup.month; int currentMonth = parseDateGroup.month;
@ -154,7 +162,10 @@ class SearchResultPage extends HookConsumerWidget {
); );
imageGridGroup.add( imageGridGroup.add(
ImageGrid(assetGroup: immichAssetList), ImageGrid(
assetGroup: immichAssetList,
sortedAssetGroup: sortedAssetList,
),
); );
lastMonth = currentMonth; lastMonth = currentMonth;
@ -193,7 +204,7 @@ class SearchResultPage extends HookConsumerWidget {
title: GestureDetector( title: GestureDetector(
onTap: () { onTap: () {
isNewSearch.value = true; isNewSearch.value = true;
searchFocusNode.requestFocus(); searchFocusNode?.requestFocus();
}, },
child: isNewSearch.value ? _buildTextField() : _buildChip(), child: isNewSearch.value ? _buildTextField() : _buildChip(),
), ),
@ -201,7 +212,10 @@ class SearchResultPage extends HookConsumerWidget {
), ),
body: GestureDetector( body: GestureDetector(
onTap: () { onTap: () {
searchFocusNode.unfocus(); if (searchFocusNode != null) {
searchFocusNode?.unfocus();
}
ref.watch(searchPageStateProvider.notifier).disableSearch(); ref.watch(searchPageStateProvider.notifier).disableSearch();
}, },
child: Stack( child: Stack(

View File

@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/album/views/library_page.dart'; import 'package:immich_mobile/modules/album/views/library_page.dart';
import 'package:immich_mobile/modules/asset_viewer/views/gallery_viewer.dart';
import 'package:immich_mobile/modules/backup/views/album_preview_page.dart'; import 'package:immich_mobile/modules/backup/views/album_preview_page.dart';
import 'package:immich_mobile/modules/backup/views/backup_album_selection_page.dart'; import 'package:immich_mobile/modules/backup/views/backup_album_selection_page.dart';
import 'package:immich_mobile/modules/backup/views/failed_backup_status_page.dart'; import 'package:immich_mobile/modules/backup/views/failed_backup_status_page.dart';
@ -47,6 +48,7 @@ part 'router.gr.dart';
], ],
transitionsBuilder: TransitionsBuilders.fadeIn, transitionsBuilder: TransitionsBuilders.fadeIn,
), ),
AutoRoute(page: GalleryViewerPage, guards: [AuthGuard]),
AutoRoute(page: ImageViewerPage, guards: [AuthGuard]), AutoRoute(page: ImageViewerPage, guards: [AuthGuard]),
AutoRoute(page: VideoViewerPage, guards: [AuthGuard]), AutoRoute(page: VideoViewerPage, guards: [AuthGuard]),
AutoRoute(page: BackupControllerPage, guards: [AuthGuard]), AutoRoute(page: BackupControllerPage, guards: [AuthGuard]),
@ -78,6 +80,7 @@ part 'router.gr.dart';
], ],
) )
class AppRouter extends _$AppRouter { class AppRouter extends _$AppRouter {
// ignore: unused_field
final ApiService _apiService; final ApiService _apiService;
AppRouter(this._apiService) : super(authGuard: AuthGuard(_apiService)); AppRouter(this._apiService) : super(authGuard: AuthGuard(_apiService));

View File

@ -41,6 +41,16 @@ class _$AppRouter extends RootStackRouter {
opaque: true, opaque: true,
barrierDismissible: false); barrierDismissible: false);
}, },
GalleryViewerRoute.name: (routeData) {
final args = routeData.argsAs<GalleryViewerRouteArgs>();
return MaterialPageX<dynamic>(
routeData: routeData,
child: GalleryViewerPage(
key: args.key,
assetList: args.assetList,
asset: args.asset,
thumbnailRequestUrl: args.thumbnailRequestUrl));
},
ImageViewerRoute.name: (routeData) { ImageViewerRoute.name: (routeData) {
final args = routeData.argsAs<ImageViewerRouteArgs>(); final args = routeData.argsAs<ImageViewerRouteArgs>();
return MaterialPageX<dynamic>( return MaterialPageX<dynamic>(
@ -50,7 +60,10 @@ class _$AppRouter extends RootStackRouter {
imageUrl: args.imageUrl, imageUrl: args.imageUrl,
heroTag: args.heroTag, heroTag: args.heroTag,
thumbnailUrl: args.thumbnailUrl, thumbnailUrl: args.thumbnailUrl,
asset: args.asset)); asset: args.asset,
authToken: args.authToken,
isZoomedFunction: args.isZoomedFunction,
isZoomedListener: args.isZoomedListener));
}, },
VideoViewerRoute.name: (routeData) { VideoViewerRoute.name: (routeData) {
final args = routeData.argsAs<VideoViewerRouteArgs>(); final args = routeData.argsAs<VideoViewerRouteArgs>();
@ -174,6 +187,8 @@ class _$AppRouter extends RootStackRouter {
parent: TabControllerRoute.name, parent: TabControllerRoute.name,
guards: [authGuard]) guards: [authGuard])
]), ]),
RouteConfig(GalleryViewerRoute.name,
path: '/gallery-viewer-page', guards: [authGuard]),
RouteConfig(ImageViewerRoute.name, RouteConfig(ImageViewerRoute.name,
path: '/image-viewer-page', guards: [authGuard]), path: '/image-viewer-page', guards: [authGuard]),
RouteConfig(VideoViewerRoute.name, RouteConfig(VideoViewerRoute.name,
@ -237,6 +252,46 @@ class TabControllerRoute extends PageRouteInfo<void> {
static const String name = 'TabControllerRoute'; static const String name = 'TabControllerRoute';
} }
/// generated route for
/// [GalleryViewerPage]
class GalleryViewerRoute extends PageRouteInfo<GalleryViewerRouteArgs> {
GalleryViewerRoute(
{Key? key,
required List<AssetResponseDto> assetList,
required AssetResponseDto asset,
required String thumbnailRequestUrl})
: super(GalleryViewerRoute.name,
path: '/gallery-viewer-page',
args: GalleryViewerRouteArgs(
key: key,
assetList: assetList,
asset: asset,
thumbnailRequestUrl: thumbnailRequestUrl));
static const String name = 'GalleryViewerRoute';
}
class GalleryViewerRouteArgs {
const GalleryViewerRouteArgs(
{this.key,
required this.assetList,
required this.asset,
required this.thumbnailRequestUrl});
final Key? key;
final List<AssetResponseDto> assetList;
final AssetResponseDto asset;
final String thumbnailRequestUrl;
@override
String toString() {
return 'GalleryViewerRouteArgs{key: $key, assetList: $assetList, asset: $asset, thumbnailRequestUrl: $thumbnailRequestUrl}';
}
}
/// generated route for /// generated route for
/// [ImageViewerPage] /// [ImageViewerPage]
class ImageViewerRoute extends PageRouteInfo<ImageViewerRouteArgs> { class ImageViewerRoute extends PageRouteInfo<ImageViewerRouteArgs> {
@ -245,7 +300,10 @@ class ImageViewerRoute extends PageRouteInfo<ImageViewerRouteArgs> {
required String imageUrl, required String imageUrl,
required String heroTag, required String heroTag,
required String thumbnailUrl, required String thumbnailUrl,
required AssetResponseDto asset}) required AssetResponseDto asset,
required String authToken,
required void Function() isZoomedFunction,
required ValueNotifier<bool> isZoomedListener})
: super(ImageViewerRoute.name, : super(ImageViewerRoute.name,
path: '/image-viewer-page', path: '/image-viewer-page',
args: ImageViewerRouteArgs( args: ImageViewerRouteArgs(
@ -253,7 +311,10 @@ class ImageViewerRoute extends PageRouteInfo<ImageViewerRouteArgs> {
imageUrl: imageUrl, imageUrl: imageUrl,
heroTag: heroTag, heroTag: heroTag,
thumbnailUrl: thumbnailUrl, thumbnailUrl: thumbnailUrl,
asset: asset)); asset: asset,
authToken: authToken,
isZoomedFunction: isZoomedFunction,
isZoomedListener: isZoomedListener));
static const String name = 'ImageViewerRoute'; static const String name = 'ImageViewerRoute';
} }
@ -264,7 +325,10 @@ class ImageViewerRouteArgs {
required this.imageUrl, required this.imageUrl,
required this.heroTag, required this.heroTag,
required this.thumbnailUrl, required this.thumbnailUrl,
required this.asset}); required this.asset,
required this.authToken,
required this.isZoomedFunction,
required this.isZoomedListener});
final Key? key; final Key? key;
@ -276,9 +340,15 @@ class ImageViewerRouteArgs {
final AssetResponseDto asset; final AssetResponseDto asset;
final String authToken;
final void Function() isZoomedFunction;
final ValueNotifier<bool> isZoomedListener;
@override @override
String toString() { String toString() {
return 'ImageViewerRouteArgs{key: $key, imageUrl: $imageUrl, heroTag: $heroTag, thumbnailUrl: $thumbnailUrl, asset: $asset}'; return 'ImageViewerRouteArgs{key: $key, imageUrl: $imageUrl, heroTag: $heroTag, thumbnailUrl: $thumbnailUrl, asset: $asset, authToken: $authToken, isZoomedFunction: $isZoomedFunction, isZoomedListener: $isZoomedListener}';
} }
} }