mirror of
https://github.com/immich-app/immich.git
synced 2025-09-29 15:31:13 -04:00
feat: show "appears in" albums on asset viewer bottom sheet
fix: multiple RemoteAlbumPages in navigation stack this also allows us to not have to set the current album before navigating to RemoteAlbumPage chore: clarification comments handle nested album pages fix: hide "appears in" when an asset is not in any albums fix: way more bottom padding for some reason we can't query the safe area here :/
This commit is contained in:
parent
aaeac2ab73
commit
a437a947c3
@ -160,6 +160,10 @@ class RemoteAlbumService {
|
|||||||
return _repository.getCount();
|
return _repository.getCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<RemoteAlbum>> getAlbumsContainingAsset(String assetId) {
|
||||||
|
return _repository.getAlbumsContainingAsset(assetId);
|
||||||
|
}
|
||||||
|
|
||||||
Future<List<RemoteAlbum>> _sortByNewestAsset(List<RemoteAlbum> albums) async {
|
Future<List<RemoteAlbum>> _sortByNewestAsset(List<RemoteAlbum> albums) async {
|
||||||
// map album IDs to their newest asset dates
|
// map album IDs to their newest asset dates
|
||||||
final Map<String, Future<DateTime?>> assetTimestampFutures = {};
|
final Map<String, Future<DateTime?>> assetTimestampFutures = {};
|
||||||
|
@ -372,6 +372,18 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
|||||||
|
|
||||||
return query.map((row) => row.read(_db.remoteAssetEntity.id)!).get();
|
return query.map((row) => row.read(_db.remoteAssetEntity.id)!).get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<RemoteAlbum>> getAlbumsContainingAsset(String assetId) async {
|
||||||
|
final albumIdsQuery = _db.remoteAlbumAssetEntity.select()..where((row) => row.assetId.equals(assetId));
|
||||||
|
|
||||||
|
final albumIds = (await albumIdsQuery.get()).map((e) => e.albumId).toSet();
|
||||||
|
|
||||||
|
if (albumIds.isEmpty) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return getAll().then((albums) => albums.where((album) => albumIds.contains(album.id)).toList());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension on RemoteAlbumEntityData {
|
extension on RemoteAlbumEntityData {
|
||||||
|
@ -32,6 +32,7 @@ import 'package:immich_mobile/theme/dynamic_theme.dart';
|
|||||||
import 'package:immich_mobile/theme/theme_data.dart';
|
import 'package:immich_mobile/theme/theme_data.dart';
|
||||||
import 'package:immich_mobile/utils/bootstrap.dart';
|
import 'package:immich_mobile/utils/bootstrap.dart';
|
||||||
import 'package:immich_mobile/utils/cache/widgets_binding.dart';
|
import 'package:immich_mobile/utils/cache/widgets_binding.dart';
|
||||||
|
import 'package:immich_mobile/utils/debug_print.dart';
|
||||||
import 'package:immich_mobile/utils/http_ssl_options.dart';
|
import 'package:immich_mobile/utils/http_ssl_options.dart';
|
||||||
import 'package:immich_mobile/utils/licenses.dart';
|
import 'package:immich_mobile/utils/licenses.dart';
|
||||||
import 'package:immich_mobile/utils/migration.dart';
|
import 'package:immich_mobile/utils/migration.dart';
|
||||||
@ -39,7 +40,6 @@ import 'package:intl/date_symbol_data_local.dart';
|
|||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:timezone/data/latest.dart';
|
import 'package:timezone/data/latest.dart';
|
||||||
import 'package:worker_manager/worker_manager.dart';
|
import 'package:worker_manager/worker_manager.dart';
|
||||||
import 'package:immich_mobile/utils/debug_print.dart';
|
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
ImmichWidgetsBinding();
|
ImmichWidgetsBinding();
|
||||||
@ -240,7 +240,7 @@ class ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserve
|
|||||||
theme: getThemeData(colorScheme: immichTheme.light, locale: context.locale),
|
theme: getThemeData(colorScheme: immichTheme.light, locale: context.locale),
|
||||||
routerConfig: router.config(
|
routerConfig: router.config(
|
||||||
deepLinkBuilder: _deepLinkBuilder,
|
deepLinkBuilder: _deepLinkBuilder,
|
||||||
navigatorObservers: () => [AppNavigationObserver(ref: ref)],
|
navigatorObservers: () => [AppNavigationObserver(ref: ref), HeroController()],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -221,14 +221,15 @@ class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return PopScope(
|
return PopScope(
|
||||||
|
canPop: false,
|
||||||
onPopInvokedWithResult: (didPop, _) {
|
onPopInvokedWithResult: (didPop, _) {
|
||||||
if (didPop) {
|
if (didPop || !mounted) {
|
||||||
Future.microtask(() {
|
return;
|
||||||
if (mounted) {
|
|
||||||
ref.read(currentRemoteAlbumProvider.notifier).dispose();
|
|
||||||
ref.read(remoteAlbumProvider.notifier).refresh();
|
|
||||||
}
|
}
|
||||||
});
|
final hasAncestor = context.findAncestorWidgetOfExactType<RemoteAlbumPage>() != null;
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
if (!hasAncestor) {
|
||||||
|
ref.read(currentRemoteAlbumProvider.notifier).dispose();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: ProviderScope(
|
child: ProviderScope(
|
||||||
|
@ -12,7 +12,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
|||||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/models/albums/album_search.model.dart';
|
import 'package:immich_mobile/models/albums/album_search.model.dart';
|
||||||
import 'package:immich_mobile/pages/common/large_leading_tile.dart';
|
import 'package:immich_mobile/presentation/widgets/album/album_tile.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
|
||||||
@ -516,38 +516,6 @@ class _AlbumList extends ConsumerWidget {
|
|||||||
sliver: SliverList.builder(
|
sliver: SliverList.builder(
|
||||||
itemBuilder: (_, index) {
|
itemBuilder: (_, index) {
|
||||||
final album = albums[index];
|
final album = albums[index];
|
||||||
final albumTile = LargeLeadingTile(
|
|
||||||
title: Text(
|
|
||||||
album.name,
|
|
||||||
maxLines: 2,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600),
|
|
||||||
),
|
|
||||||
subtitle: Text(
|
|
||||||
'${'items_count'.t(context: context, args: {'count': album.assetCount})} • ${album.ownerId != userId ? 'shared_by_user'.t(context: context, args: {'user': album.ownerName}) : 'owned'.t(context: context)}',
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary),
|
|
||||||
),
|
|
||||||
onTap: () => onAlbumSelected(album),
|
|
||||||
leadingPadding: const EdgeInsets.only(right: 16),
|
|
||||||
leading: album.thumbnailAssetId != null
|
|
||||||
? ClipRRect(
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(15)),
|
|
||||||
child: SizedBox(width: 80, height: 80, child: Thumbnail.remote(remoteId: album.thumbnailAssetId!)),
|
|
||||||
)
|
|
||||||
: SizedBox(
|
|
||||||
width: 80,
|
|
||||||
height: 80,
|
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: context.colorScheme.surfaceContainer,
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
|
||||||
border: Border.all(color: context.colorScheme.outline.withAlpha(50), width: 1),
|
|
||||||
),
|
|
||||||
child: const Icon(Icons.photo_album_rounded, size: 24, color: Colors.grey),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
final isOwner = album.ownerId == userId;
|
final isOwner = album.ownerId == userId;
|
||||||
|
|
||||||
if (isOwner) {
|
if (isOwner) {
|
||||||
@ -576,11 +544,14 @@ class _AlbumList extends ConsumerWidget {
|
|||||||
onDismissed: (direction) async {
|
onDismissed: (direction) async {
|
||||||
await ref.read(remoteAlbumProvider.notifier).deleteAlbum(album.id);
|
await ref.read(remoteAlbumProvider.notifier).deleteAlbum(album.id);
|
||||||
},
|
},
|
||||||
child: albumTile,
|
child: AlbumTile(album: album, isOwner: isOwner, onAlbumSelected: onAlbumSelected),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return Padding(padding: const EdgeInsets.only(bottom: 8.0), child: albumTile);
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 8.0),
|
||||||
|
child: AlbumTile(album: album, isOwner: isOwner, onAlbumSelected: onAlbumSelected),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
itemCount: albums.length,
|
itemCount: albums.length,
|
||||||
|
51
mobile/lib/presentation/widgets/album/album_tile.dart
Normal file
51
mobile/lib/presentation/widgets/album/album_tile.dart
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
|
import 'package:immich_mobile/pages/common/large_leading_tile.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
|
||||||
|
|
||||||
|
class AlbumTile extends StatelessWidget {
|
||||||
|
const AlbumTile({super.key, required this.album, required this.isOwner, this.onAlbumSelected});
|
||||||
|
|
||||||
|
final RemoteAlbum album;
|
||||||
|
final bool isOwner;
|
||||||
|
final Function(RemoteAlbum)? onAlbumSelected;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return LargeLeadingTile(
|
||||||
|
title: Text(
|
||||||
|
album.name,
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
'${'items_count'.t(context: context, args: {'count': album.assetCount})} • ${isOwner ? 'owned'.t(context: context) : 'shared_by_user'.t(context: context, args: {'user': album.ownerName})}',
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary),
|
||||||
|
),
|
||||||
|
onTap: () => onAlbumSelected?.call(album),
|
||||||
|
leadingPadding: const EdgeInsets.only(right: 16),
|
||||||
|
leading: album.thumbnailAssetId != null
|
||||||
|
? ClipRRect(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(15)),
|
||||||
|
child: SizedBox(width: 80, height: 80, child: Thumbnail.remote(remoteId: album.thumbnailAssetId!)),
|
||||||
|
)
|
||||||
|
: SizedBox(
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: context.colorScheme.surfaceContainer,
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
||||||
|
border: Border.all(color: context.colorScheme.outline.withAlpha(50), width: 1),
|
||||||
|
),
|
||||||
|
child: const Icon(Icons.photo_album_rounded, size: 24, color: Colors.grey),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,5 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
@ -8,17 +10,20 @@ import 'package:immich_mobile/domain/models/exif.model.dart';
|
|||||||
import 'package:immich_mobile/domain/models/setting.model.dart';
|
import 'package:immich_mobile/domain/models/setting.model.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/album/album_tile.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet/sheet_location_details.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet/sheet_location_details.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet/sheet_people_details.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet/sheet_people_details.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
||||||
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
|
||||||
import 'package:immich_mobile/providers/routes.provider.dart';
|
import 'package:immich_mobile/providers/routes.provider.dart';
|
||||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/utils/action_button.utils.dart';
|
import 'package:immich_mobile/utils/action_button.utils.dart';
|
||||||
import 'package:immich_mobile/utils/bytes_units.dart';
|
import 'package:immich_mobile/utils/bytes_units.dart';
|
||||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
@ -130,6 +135,61 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
|
|||||||
await ref.read(actionProvider.notifier).editDateTime(ActionSource.viewer, context);
|
await ref.read(actionProvider.notifier).editDateTime(ActionSource.viewer, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildAppearsInList(WidgetRef ref, BuildContext context) {
|
||||||
|
final isRemote = ref.watch(currentAssetNotifier)?.hasRemote ?? false;
|
||||||
|
if (!isRemote) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
final remoteAsset = ref.watch(currentAssetNotifier) as RemoteAsset;
|
||||||
|
final albums = ref.watch(remoteAlbumServiceProvider).getAlbumsContainingAsset(remoteAsset.id);
|
||||||
|
final userId = ref.watch(currentUserProvider)?.id;
|
||||||
|
|
||||||
|
return FutureBuilder(
|
||||||
|
future: albums,
|
||||||
|
builder: (_, snap) {
|
||||||
|
final albums = snap.data ?? [];
|
||||||
|
if (albums.isEmpty) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
albums.sortBy((a) => a.name);
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 16, right: 16, top: 8),
|
||||||
|
child: Column(
|
||||||
|
spacing: 12,
|
||||||
|
children: [
|
||||||
|
if (albums.isNotEmpty)
|
||||||
|
_SheetTile(
|
||||||
|
title: 'appears_in'.t(context: context).toUpperCase(),
|
||||||
|
titleStyle: context.textTheme.labelMedium?.copyWith(
|
||||||
|
color: context.textTheme.labelMedium?.color?.withAlpha(200),
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
...albums.map((album) {
|
||||||
|
final isOwner = album.ownerId == userId;
|
||||||
|
return AlbumTile(
|
||||||
|
album: album,
|
||||||
|
isOwner: isOwner,
|
||||||
|
onAlbumSelected: (album) async {
|
||||||
|
final prevAlbum = ref.read(currentRemoteAlbumProvider);
|
||||||
|
ref.read(currentRemoteAlbumProvider.notifier).setAlbum(album);
|
||||||
|
await context.router.push(RemoteAlbumRoute(album: album));
|
||||||
|
if (prevAlbum != null) {
|
||||||
|
ref.read(currentRemoteAlbumProvider.notifier).setAlbum(prevAlbum);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final asset = ref.watch(currentAssetNotifier);
|
final asset = ref.watch(currentAssetNotifier);
|
||||||
@ -185,6 +245,10 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
|
|||||||
color: context.textTheme.bodyMedium?.color?.withAlpha(155),
|
color: context.textTheme.bodyMedium?.color?.withAlpha(155),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
// Appears in (Albums)
|
||||||
|
_buildAppearsInList(ref, context),
|
||||||
|
// padding at the bottom to avoid cut-off
|
||||||
|
const SizedBox(height: 100),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,6 @@ import 'package:immich_mobile/providers/infrastructure/current_album.provider.da
|
|||||||
import 'package:immich_mobile/providers/infrastructure/remote_album.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/remote_album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
|
||||||
import 'package:immich_mobile/widgets/album/remote_album_shared_user_icons.dart';
|
import 'package:immich_mobile/widgets/album/remote_album_shared_user_icons.dart';
|
||||||
|
|
||||||
class RemoteAlbumSliverAppBar extends ConsumerStatefulWidget {
|
class RemoteAlbumSliverAppBar extends ConsumerStatefulWidget {
|
||||||
@ -89,7 +88,7 @@ class _MesmerizingSliverAppBarState extends ConsumerState<RemoteAlbumSliverAppBa
|
|||||||
color: actionIconColor,
|
color: actionIconColor,
|
||||||
shadows: actionIconShadows,
|
shadows: actionIconShadows,
|
||||||
),
|
),
|
||||||
onPressed: () => context.navigateTo(const TabShellRoute(children: [DriftAlbumsRoute()])),
|
onPressed: () => context.maybePop(),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
if (widget.onToggleAlbumOrder != null)
|
if (widget.onToggleAlbumOrder != null)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user