1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-01-01 08:31:59 +00:00

feat(mobile): Add to album from asset detail view (#1413)

* add to album from asset detail view

* layout and design

* added shared albums

* fixed remote, asset update, and hit test

* made static size

* fixed create album

* suppress shared expansion tile if there are no shared albums

* updates album

* padding on tile
This commit is contained in:
martyfuhry 2023-01-27 00:16:28 -05:00 committed by GitHub
parent 788b435f9b
commit d377cf0d02
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 309 additions and 12 deletions

View file

@ -0,0 +1,129 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
import 'package:immich_mobile/modules/album/services/album.service.dart';
import 'package:immich_mobile/modules/album/ui/album_thumbnail_listtile.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/ui/drag_sheet.dart';
import 'package:immich_mobile/shared/ui/immich_toast.dart';
import 'package:openapi/api.dart';
class AddToAlbumList extends HookConsumerWidget {
/// The asset to add to an album
final Asset asset;
const AddToAlbumList({
Key? key,
required this.asset,
}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final albums = ref.watch(albumProvider);
final albumService = ref.watch(albumServiceProvider);
final sharedAlbums = ref.watch(sharedAlbumProvider);
useEffect(
() {
// Fetch album updates, e.g., cover image
ref.read(albumProvider.notifier).getAllAlbums();
ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
return null;
},
[],
);
void addToAlbum(AlbumResponseDto album) async {
final result = await albumService.addAdditionalAssetToAlbum(
[asset],
album.id,
);
if (result != null) {
if (result.alreadyInAlbum.isNotEmpty) {
ImmichToast.show(
context: context,
msg: 'Already in ${album.albumName}',
);
} else {
ImmichToast.show(
context: context,
msg: 'Added to ${album.albumName}',
);
}
}
ref.read(albumProvider.notifier).getAllAlbums();
ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
Navigator.pop(context);
}
return Card(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(15),
topRight: Radius.circular(15),
),
),
child: ListView(
padding: const EdgeInsets.all(18.0),
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Align(
alignment: Alignment.center,
child: CustomDraggingHandle(),
),
const SizedBox(height: 12),
Text('Add to album',
style: Theme.of(context).textTheme.headline1,
),
TextButton.icon(
icon: const Icon(Icons.add),
label: const Text('New album'),
onPressed: () {
ref.watch(assetSelectionProvider.notifier).removeAll();
ref.watch(assetSelectionProvider.notifier).addNewAssets([asset]);
AutoRouter.of(context).push(
CreateAlbumRoute(
isSharedAlbum: false,
initialAssets: [asset],
),
);
},
),
],
),
if (sharedAlbums.isNotEmpty)
ExpansionTile(
title: const Text('Shared'),
tilePadding: const EdgeInsets.symmetric(horizontal: 10.0),
leading: const Icon(Icons.group),
children: sharedAlbums.map((album) =>
AlbumThumbnailListTile(
album: album,
onTap: () => addToAlbum(album),
),
).toList(),
),
const SizedBox(height: 12),
... albums.map((album) =>
AlbumThumbnailListTile(
album: album,
onTap: () => addToAlbum(album),
),
).toList(),
],
),
);
}
}

View file

@ -0,0 +1,115 @@
import 'package:auto_route/auto_route.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:openapi/api.dart';
class AlbumThumbnailListTile extends StatelessWidget {
const AlbumThumbnailListTile({
Key? key,
required this.album,
this.onTap,
}) : super(key: key);
final AlbumResponseDto album;
final void Function()? onTap;
@override
Widget build(BuildContext context) {
var box = Hive.box(userInfoBox);
var cardSize = 68.0;
var isDarkMode = Theme.of(context).brightness == Brightness.dark;
buildEmptyThumbnail() {
return Container(
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[800] : Colors.grey[200],
),
child: SizedBox(
height: cardSize,
width: cardSize,
child: const Center(
child: Icon(Icons.no_photography),
),
),
);
}
buildAlbumThumbnail() {
return CachedNetworkImage(
width: cardSize,
height: cardSize,
fit: BoxFit.cover,
fadeInDuration: const Duration(milliseconds: 200),
imageUrl: getAlbumThumbnailUrl(
album,
type: ThumbnailFormat.JPEG,
),
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
cacheKey: getAlbumThumbNailCacheKey(album, type: ThumbnailFormat.JPEG),
);
}
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onTap ?? () {
AutoRouter.of(context).push(AlbumViewerRoute(albumId: album.id));
},
child: Padding(
padding: const EdgeInsets.only(bottom: 12.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: album.albumThumbnailAssetId == null
? buildEmptyThumbnail()
: buildAlbumThumbnail(),
),
Padding(
padding: const EdgeInsets.only(
left: 8.0,
right: 8.0,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
album.albumName,
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
album.assetCount == 1
? 'album_thumbnail_card_item'
: 'album_thumbnail_card_items',
style: const TextStyle(
fontSize: 12,
),
).tr(args: ['${album.assetCount}']),
if (album.shared)
const Text(
'album_thumbnail_card_shared',
style: TextStyle(
fontSize: 12,
),
).tr()
],
)
],
),
),
],
),
),
);
}
}

View file

@ -11,12 +11,18 @@ import 'package:immich_mobile/modules/album/ui/album_action_outlined_button.dart
import 'package:immich_mobile/modules/album/ui/album_title_text_field.dart';
import 'package:immich_mobile/modules/album/ui/shared_album_thumbnail_image.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/models/asset.dart';
// ignore: must_be_immutable
class CreateAlbumPage extends HookConsumerWidget {
bool isSharedAlbum;
final bool isSharedAlbum;
final List<Asset>? initialAssets;
CreateAlbumPage({Key? key, required this.isSharedAlbum}) : super(key: key);
const CreateAlbumPage({
Key? key,
required this.isSharedAlbum,
this.initialAssets,
}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {

View file

@ -11,6 +11,7 @@ class TopControlAppBar extends HookConsumerWidget with PreferredSizeWidget {
required this.onDownloadPressed,
required this.onSharePressed,
required this.onDeletePressed,
required this.onAddToAlbumPressed,
required this.onToggleMotionVideo,
required this.isPlayingMotionVideo,
}) : super(key: key);
@ -20,6 +21,7 @@ class TopControlAppBar extends HookConsumerWidget with PreferredSizeWidget {
final VoidCallback? onDownloadPressed;
final VoidCallback onToggleMotionVideo;
final VoidCallback onDeletePressed;
final VoidCallback onAddToAlbumPressed;
final Function onSharePressed;
final bool isPlayingMotionVideo;
@ -80,6 +82,18 @@ class TopControlAppBar extends HookConsumerWidget with PreferredSizeWidget {
color: Colors.grey[200],
),
),
if (asset.isRemote)
IconButton(
iconSize: iconSize,
splashRadius: iconSize,
onPressed: () {
onAddToAlbumPressed();
},
icon: Icon(
Icons.add,
color: Colors.grey[200],
),
),
IconButton(
iconSize: iconSize,
splashRadius: iconSize,

View file

@ -5,6 +5,7 @@ import 'package:flutter_hooks/flutter_hooks.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/album/ui/add_to_album_list.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';
@ -105,6 +106,22 @@ class GalleryViewerPage extends HookConsumerWidget {
);
}
void addToAlbum(Asset addToAlbumAsset) {
showModalBottomSheet(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15.0),
),
barrierColor: Colors.transparent,
backgroundColor: Colors.transparent,
context: context,
builder: (BuildContext _) {
return AddToAlbumList(
asset: addToAlbumAsset,
);
},
);
}
return Scaffold(
backgroundColor: Colors.black,
appBar: TopControlAppBar(
@ -130,6 +147,7 @@ class GalleryViewerPage extends HookConsumerWidget {
isPlayingMotionVideo.value = !isPlayingMotionVideo.value;
}),
onDeletePressed: () => handleDelete((assetList[indexOfAsset.value])),
onAddToAlbumPressed: () => addToAlbum(assetList[indexOfAsset.value]),
),
body: SafeArea(
child: PageView.builder(

View file

@ -60,7 +60,8 @@ class _$AppRouter extends RootStackRouter {
isZoomedFunction: args.isZoomedFunction,
isZoomedListener: args.isZoomedListener,
loadPreview: args.loadPreview,
loadOriginal: args.loadOriginal));
loadOriginal: args.loadOriginal,
showExifSheet: args.showExifSheet));
},
VideoViewerRoute.name: (routeData) {
final args = routeData.argsAs<VideoViewerRouteArgs>();
@ -87,7 +88,9 @@ class _$AppRouter extends RootStackRouter {
return MaterialPageX<dynamic>(
routeData: routeData,
child: CreateAlbumPage(
key: args.key, isSharedAlbum: args.isSharedAlbum));
key: args.key,
isSharedAlbum: args.isSharedAlbum,
initialAssets: args.initialAssets));
},
AssetSelectionRoute.name: (routeData) {
return CustomPage<AssetSelectionPageResult?>(
@ -307,7 +310,8 @@ class ImageViewerRoute extends PageRouteInfo<ImageViewerRouteArgs> {
required void Function() isZoomedFunction,
required ValueNotifier<bool> isZoomedListener,
required bool loadPreview,
required bool loadOriginal})
required bool loadOriginal,
void Function()? showExifSheet})
: super(ImageViewerRoute.name,
path: '/image-viewer-page',
args: ImageViewerRouteArgs(
@ -318,7 +322,8 @@ class ImageViewerRoute extends PageRouteInfo<ImageViewerRouteArgs> {
isZoomedFunction: isZoomedFunction,
isZoomedListener: isZoomedListener,
loadPreview: loadPreview,
loadOriginal: loadOriginal));
loadOriginal: loadOriginal,
showExifSheet: showExifSheet));
static const String name = 'ImageViewerRoute';
}
@ -332,7 +337,8 @@ class ImageViewerRouteArgs {
required this.isZoomedFunction,
required this.isZoomedListener,
required this.loadPreview,
required this.loadOriginal});
required this.loadOriginal,
this.showExifSheet});
final Key? key;
@ -350,9 +356,11 @@ class ImageViewerRouteArgs {
final bool loadOriginal;
final void Function()? showExifSheet;
@override
String toString() {
return 'ImageViewerRouteArgs{key: $key, heroTag: $heroTag, asset: $asset, authToken: $authToken, isZoomedFunction: $isZoomedFunction, isZoomedListener: $isZoomedListener, loadPreview: $loadPreview, loadOriginal: $loadOriginal}';
return 'ImageViewerRouteArgs{key: $key, heroTag: $heroTag, asset: $asset, authToken: $authToken, isZoomedFunction: $isZoomedFunction, isZoomedListener: $isZoomedListener, loadPreview: $loadPreview, loadOriginal: $loadOriginal, showExifSheet: $showExifSheet}';
}
}
@ -432,24 +440,31 @@ class SearchResultRouteArgs {
/// generated route for
/// [CreateAlbumPage]
class CreateAlbumRoute extends PageRouteInfo<CreateAlbumRouteArgs> {
CreateAlbumRoute({Key? key, required bool isSharedAlbum})
CreateAlbumRoute(
{Key? key, required bool isSharedAlbum, List<Asset>? initialAssets})
: super(CreateAlbumRoute.name,
path: '/create-album-page',
args: CreateAlbumRouteArgs(key: key, isSharedAlbum: isSharedAlbum));
args: CreateAlbumRouteArgs(
key: key,
isSharedAlbum: isSharedAlbum,
initialAssets: initialAssets));
static const String name = 'CreateAlbumRoute';
}
class CreateAlbumRouteArgs {
const CreateAlbumRouteArgs({this.key, required this.isSharedAlbum});
const CreateAlbumRouteArgs(
{this.key, required this.isSharedAlbum, this.initialAssets});
final Key? key;
final bool isSharedAlbum;
final List<Asset>? initialAssets;
@override
String toString() {
return 'CreateAlbumRouteArgs{key: $key, isSharedAlbum: $isSharedAlbum}';
return 'CreateAlbumRouteArgs{key: $key, isSharedAlbum: $isSharedAlbum, initialAssets: $initialAssets}';
}
}