mirror of
https://github.com/KevinMidboe/immich.git
synced 2025-10-29 17:40:28 +00:00
feat(mobile): lazy loading of assets (#2413)
This commit is contained in:
committed by
GitHub
parent
93863b0629
commit
0dde76bbbc
@@ -4,7 +4,6 @@ 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/add_to_album_sliverlist.dart';
|
||||
@@ -110,12 +109,6 @@ class AddToAlbumBottomSheet extends HookConsumerWidget {
|
||||
TextStyle(color: Theme.of(context).primaryColor),
|
||||
),
|
||||
onPressed: () {
|
||||
ref
|
||||
.watch(assetSelectionProvider.notifier)
|
||||
.removeAll();
|
||||
ref
|
||||
.watch(assetSelectionProvider.notifier)
|
||||
.addNewAssets(assets);
|
||||
AutoRouter.of(context).push(
|
||||
CreateAlbumRoute(
|
||||
isSharedAlbum: false,
|
||||
|
||||
@@ -9,12 +9,14 @@ class AddToAlbumSliverList extends HookConsumerWidget {
|
||||
final List<Album> albums;
|
||||
final List<Album> sharedAlbums;
|
||||
final void Function(Album) onAddToAlbum;
|
||||
final bool enabled;
|
||||
|
||||
const AddToAlbumSliverList({
|
||||
Key? key,
|
||||
required this.onAddToAlbum,
|
||||
required this.albums,
|
||||
required this.sharedAlbums,
|
||||
this.enabled = true,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
@@ -28,14 +30,14 @@ class AddToAlbumSliverList extends HookConsumerWidget {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: ExpansionTile(
|
||||
title: Text('common_shared'.tr()),
|
||||
title: Text('common_shared'.tr()),
|
||||
tilePadding: const EdgeInsets.symmetric(horizontal: 10.0),
|
||||
leading: const Icon(Icons.group),
|
||||
children: sharedAlbums
|
||||
.map(
|
||||
(album) => AlbumThumbnailListTile(
|
||||
album: album,
|
||||
onTap: () => onAddToAlbum(album),
|
||||
onTap: enabled ? () => onAddToAlbum(album) : () {},
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
@@ -48,7 +50,7 @@ class AddToAlbumSliverList extends HookConsumerWidget {
|
||||
final album = albums[offset];
|
||||
return AlbumThumbnailListTile(
|
||||
album: album,
|
||||
onTap: () => onAddToAlbum(album),
|
||||
onTap: enabled ? () => onAddToAlbum(album) : () {},
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -5,10 +5,10 @@ import 'package:fluttertoast/fluttertoast.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/album_viewer.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/routing/router.dart';
|
||||
import 'package:immich_mobile/shared/models/album.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
||||
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
|
||||
|
||||
@@ -18,17 +18,19 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
||||
Key? key,
|
||||
required this.album,
|
||||
required this.userId,
|
||||
required this.selected,
|
||||
required this.selectionDisabled,
|
||||
required this.titleFocusNode,
|
||||
}) : super(key: key);
|
||||
|
||||
final Album album;
|
||||
final String userId;
|
||||
final Set<Asset> selected;
|
||||
final void Function() selectionDisabled;
|
||||
final FocusNode titleFocusNode;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isMultiSelectionEnable =
|
||||
ref.watch(assetSelectionProvider).isMultiselectEnable;
|
||||
final selectedAssetsInAlbum =
|
||||
ref.watch(assetSelectionProvider).selectedAssetsInAlbumViewer;
|
||||
final newAlbumTitle = ref.watch(albumViewerProvider).editTitleText;
|
||||
final isEditAlbum = ref.watch(albumViewerProvider).isEditAlbum;
|
||||
|
||||
@@ -86,12 +88,12 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
||||
bool isSuccess =
|
||||
await ref.watch(sharedAlbumProvider.notifier).removeAssetFromAlbum(
|
||||
album,
|
||||
selectedAssetsInAlbum,
|
||||
selected,
|
||||
);
|
||||
|
||||
if (isSuccess) {
|
||||
Navigator.pop(context);
|
||||
ref.watch(assetSelectionProvider.notifier).disableMultiselection();
|
||||
selectionDisabled();
|
||||
ref.watch(albumProvider.notifier).getAllAlbums();
|
||||
ref.invalidate(sharedAlbumDetailProvider(album.id));
|
||||
} else {
|
||||
@@ -108,7 +110,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
||||
}
|
||||
|
||||
buildBottomSheetActionButton() {
|
||||
if (isMultiSelectionEnable) {
|
||||
if (selected.isNotEmpty) {
|
||||
if (album.ownerId == userId) {
|
||||
return ListTile(
|
||||
leading: const Icon(Icons.delete_sweep_rounded),
|
||||
@@ -163,11 +165,9 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
||||
}
|
||||
|
||||
buildLeadingButton() {
|
||||
if (isMultiSelectionEnable) {
|
||||
if (selected.isNotEmpty) {
|
||||
return IconButton(
|
||||
onPressed: () => ref
|
||||
.watch(assetSelectionProvider.notifier)
|
||||
.disableMultiselection(),
|
||||
onPressed: selectionDisabled,
|
||||
icon: const Icon(Icons.close_rounded),
|
||||
splashRadius: 25,
|
||||
);
|
||||
@@ -202,9 +202,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
||||
return AppBar(
|
||||
elevation: 0,
|
||||
leading: buildLeadingButton(),
|
||||
title: isMultiSelectionEnable
|
||||
? Text('${selectedAssetsInAlbum.length}')
|
||||
: null,
|
||||
title: selected.isNotEmpty ? Text('${selected.length}') : null,
|
||||
centerTitle: false,
|
||||
actions: [
|
||||
if (album.isRemote)
|
||||
|
||||
@@ -1,163 +0,0 @@
|
||||
import 'package:auto_route/auto_route.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/album/providers/asset_selection.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_image.dart';
|
||||
import 'package:immich_mobile/utils/storage_indicator.dart';
|
||||
|
||||
class AlbumViewerThumbnail extends HookConsumerWidget {
|
||||
final Asset asset;
|
||||
final List<Asset> assetList;
|
||||
final bool showStorageIndicator;
|
||||
|
||||
const AlbumViewerThumbnail({
|
||||
Key? key,
|
||||
required this.asset,
|
||||
required this.assetList,
|
||||
this.showStorageIndicator = true,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final selectedAssetsInAlbumViewer =
|
||||
ref.watch(assetSelectionProvider).selectedAssetsInAlbumViewer;
|
||||
final isMultiSelectionEnable =
|
||||
ref.watch(assetSelectionProvider).isMultiselectEnable;
|
||||
final isFavorite = ref.watch(favoriteProvider).contains(asset.id);
|
||||
|
||||
viewAsset() {
|
||||
AutoRouter.of(context).push(
|
||||
GalleryViewerRoute(
|
||||
asset: asset,
|
||||
assetList: assetList,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
BoxBorder drawBorderColor() {
|
||||
if (selectedAssetsInAlbumViewer.contains(asset)) {
|
||||
return Border.all(
|
||||
color: Theme.of(context).primaryColorLight,
|
||||
width: 10,
|
||||
);
|
||||
} else {
|
||||
return const Border();
|
||||
}
|
||||
}
|
||||
|
||||
enableMultiSelection() {
|
||||
ref.watch(assetSelectionProvider.notifier).enableMultiselection();
|
||||
ref
|
||||
.watch(assetSelectionProvider.notifier)
|
||||
.addAssetsInAlbumViewer([asset]);
|
||||
}
|
||||
|
||||
disableMultiSelection() {
|
||||
ref.watch(assetSelectionProvider.notifier).disableMultiselection();
|
||||
}
|
||||
|
||||
buildVideoLabel() {
|
||||
return Positioned(
|
||||
top: 5,
|
||||
right: 5,
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
asset.duration.toString().substring(0, 7),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
const Icon(
|
||||
Icons.play_circle_outline_rounded,
|
||||
color: Colors.white,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
buildAssetStoreLocationIcon() {
|
||||
return Positioned(
|
||||
right: 10,
|
||||
bottom: 5,
|
||||
child: Icon(
|
||||
storageIcon(asset),
|
||||
color: Colors.white,
|
||||
size: 18,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
buildAssetFavoriteIcon() {
|
||||
return const Positioned(
|
||||
left: 10,
|
||||
bottom: 5,
|
||||
child: Icon(
|
||||
Icons.favorite,
|
||||
color: Colors.white,
|
||||
size: 18,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
buildAssetSelectionIcon() {
|
||||
bool isSelected = selectedAssetsInAlbumViewer.contains(asset);
|
||||
|
||||
return Positioned(
|
||||
left: 10,
|
||||
top: 5,
|
||||
child: isSelected
|
||||
? Icon(
|
||||
Icons.check_circle_rounded,
|
||||
color: Theme.of(context).primaryColor,
|
||||
)
|
||||
: const Icon(
|
||||
Icons.check_circle_outline_rounded,
|
||||
color: Colors.white,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
buildThumbnailImage() {
|
||||
return Container(
|
||||
decoration: BoxDecoration(border: drawBorderColor()),
|
||||
child: ImmichImage(asset, width: 300, height: 300),
|
||||
);
|
||||
}
|
||||
|
||||
handleSelectionGesture() {
|
||||
if (selectedAssetsInAlbumViewer.contains(asset)) {
|
||||
ref
|
||||
.watch(assetSelectionProvider.notifier)
|
||||
.removeAssetsInAlbumViewer([asset]);
|
||||
|
||||
if (selectedAssetsInAlbumViewer.isEmpty) {
|
||||
disableMultiSelection();
|
||||
}
|
||||
} else {
|
||||
ref
|
||||
.watch(assetSelectionProvider.notifier)
|
||||
.addAssetsInAlbumViewer([asset]);
|
||||
}
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
onTap: isMultiSelectionEnable ? handleSelectionGesture : viewAsset,
|
||||
onLongPress: enableMultiSelection,
|
||||
child: Stack(
|
||||
children: [
|
||||
buildThumbnailImage(),
|
||||
if (isFavorite) buildAssetFavoriteIcon(),
|
||||
if (showStorageIndicator) buildAssetStoreLocationIcon(),
|
||||
if (!asset.isImage) buildVideoLabel(),
|
||||
if (isMultiSelectionEnable) buildAssetSelectionIcon(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/album/ui/selection_thumbnail_image.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
|
||||
class AssetGridByMonth extends HookConsumerWidget {
|
||||
final List<Asset> assetGroup;
|
||||
const AssetGridByMonth({Key? key, required this.assetGroup})
|
||||
: super(key: key);
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return SliverGrid(
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 4,
|
||||
crossAxisSpacing: 5.0,
|
||||
mainAxisSpacing: 5,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(BuildContext context, int index) {
|
||||
return SelectionThumbnailImage(asset: assetGroup[index]);
|
||||
},
|
||||
childCount: assetGroup.length,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
|
||||
class MonthGroupTitle extends HookConsumerWidget {
|
||||
final String month;
|
||||
final List<Asset> assetGroup;
|
||||
|
||||
const MonthGroupTitle({
|
||||
Key? key,
|
||||
required this.month,
|
||||
required this.assetGroup,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final selectedDateGroup = ref.watch(assetSelectionProvider).selectedMonths;
|
||||
final selectedAssets =
|
||||
ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum;
|
||||
final isAlbumExist = ref.watch(assetSelectionProvider).isAlbumExist;
|
||||
|
||||
handleTitleIconClick() {
|
||||
HapticFeedback.heavyImpact();
|
||||
|
||||
if (isAlbumExist) {
|
||||
if (selectedDateGroup.contains(month)) {
|
||||
ref
|
||||
.watch(assetSelectionProvider.notifier)
|
||||
.removeAssetsInMonth(month, []);
|
||||
ref
|
||||
.watch(assetSelectionProvider.notifier)
|
||||
.removeSelectedAdditionalAssets(assetGroup);
|
||||
} else {
|
||||
ref
|
||||
.watch(assetSelectionProvider.notifier)
|
||||
.addAllAssetsInMonth(month, []);
|
||||
|
||||
// Deep clone assetGroup
|
||||
var assetGroupWithNewItems = [...assetGroup];
|
||||
|
||||
for (var selectedAsset in selectedAssets) {
|
||||
assetGroupWithNewItems.removeWhere((a) => a.id == selectedAsset.id);
|
||||
}
|
||||
|
||||
ref
|
||||
.watch(assetSelectionProvider.notifier)
|
||||
.addAdditionalAssets(assetGroupWithNewItems);
|
||||
}
|
||||
} else {
|
||||
if (selectedDateGroup.contains(month)) {
|
||||
ref
|
||||
.watch(assetSelectionProvider.notifier)
|
||||
.removeAssetsInMonth(month, assetGroup);
|
||||
} else {
|
||||
ref
|
||||
.watch(assetSelectionProvider.notifier)
|
||||
.addAllAssetsInMonth(month, assetGroup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getSimplifiedMonth() {
|
||||
var monthAndYear = month.split(',');
|
||||
var yearText = monthAndYear[1].trim();
|
||||
var monthText = monthAndYear[0].trim();
|
||||
var currentYear = DateTime.now().year.toString();
|
||||
|
||||
if (yearText == currentYear) {
|
||||
return monthText;
|
||||
} else {
|
||||
return month;
|
||||
}
|
||||
}
|
||||
|
||||
return SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 29.0,
|
||||
bottom: 29.0,
|
||||
left: 14.0,
|
||||
right: 8.0,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: handleTitleIconClick,
|
||||
child: selectedDateGroup.contains(month)
|
||||
? Icon(
|
||||
Icons.check_circle_rounded,
|
||||
color: Theme.of(context).primaryColor,
|
||||
)
|
||||
: const Icon(
|
||||
Icons.circle_outlined,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: handleTitleIconClick,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: Text(
|
||||
getSimplifiedMonth(),
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_image.dart';
|
||||
|
||||
class SelectionThumbnailImage extends HookConsumerWidget {
|
||||
final Asset asset;
|
||||
|
||||
const SelectionThumbnailImage({Key? key, required this.asset})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
var selectedAsset =
|
||||
ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum;
|
||||
var newAssetsForAlbum =
|
||||
ref.watch(assetSelectionProvider).selectedAdditionalAssetsForAlbum;
|
||||
var isAlbumExist = ref.watch(assetSelectionProvider).isAlbumExist;
|
||||
|
||||
Widget buildSelectionIcon(Asset asset) {
|
||||
var isSelected = selectedAsset.map((item) => item.id).contains(asset.id);
|
||||
var isNewlySelected =
|
||||
newAssetsForAlbum.map((item) => item.id).contains(asset.id);
|
||||
|
||||
if (isSelected && !isAlbumExist) {
|
||||
return Icon(
|
||||
Icons.check_circle,
|
||||
color: Theme.of(context).primaryColor,
|
||||
);
|
||||
} else if (isSelected && isAlbumExist) {
|
||||
return const Icon(
|
||||
Icons.check_circle,
|
||||
color: Color.fromARGB(255, 233, 233, 233),
|
||||
);
|
||||
} else if (isNewlySelected && isAlbumExist) {
|
||||
return Icon(
|
||||
Icons.check_circle,
|
||||
color: Theme.of(context).primaryColor,
|
||||
);
|
||||
} else {
|
||||
return const Icon(
|
||||
Icons.circle_outlined,
|
||||
color: Colors.white,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
BoxBorder drawBorderColor() {
|
||||
var isSelected = selectedAsset.map((item) => item.id).contains(asset.id);
|
||||
var isNewlySelected =
|
||||
newAssetsForAlbum.map((item) => item.id).contains(asset.id);
|
||||
|
||||
if (isSelected && !isAlbumExist) {
|
||||
return Border.all(
|
||||
color: Theme.of(context).primaryColorLight,
|
||||
width: 10,
|
||||
);
|
||||
} else if (isSelected && isAlbumExist) {
|
||||
return Border.all(
|
||||
color: const Color.fromARGB(255, 190, 190, 190),
|
||||
width: 10,
|
||||
);
|
||||
} else if (isNewlySelected && isAlbumExist) {
|
||||
return Border.all(
|
||||
color: Theme.of(context).primaryColorLight,
|
||||
width: 10,
|
||||
);
|
||||
}
|
||||
return const Border();
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
var isSelected =
|
||||
selectedAsset.map((item) => item.id).contains(asset.id);
|
||||
var isNewlySelected =
|
||||
newAssetsForAlbum.map((item) => item.id).contains(asset.id);
|
||||
|
||||
if (isAlbumExist) {
|
||||
// Operation for existing album
|
||||
if (!isSelected) {
|
||||
if (isNewlySelected) {
|
||||
ref
|
||||
.watch(assetSelectionProvider.notifier)
|
||||
.removeSelectedAdditionalAssets([asset]);
|
||||
} else {
|
||||
ref
|
||||
.watch(assetSelectionProvider.notifier)
|
||||
.addAdditionalAssets([asset]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Operation for new album
|
||||
if (isSelected) {
|
||||
ref
|
||||
.watch(assetSelectionProvider.notifier)
|
||||
.removeSelectedNewAssets([asset]);
|
||||
} else {
|
||||
ref.watch(assetSelectionProvider.notifier).addNewAssets([asset]);
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(border: drawBorderColor()),
|
||||
child: ImmichImage(asset, width: 150, height: 150),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(3.0),
|
||||
child: Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: buildSelectionIcon(asset),
|
||||
),
|
||||
),
|
||||
if (!asset.isImage)
|
||||
Positioned(
|
||||
bottom: 5,
|
||||
right: 5,
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
asset.duration.toString().substring(0, 7),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
const Icon(
|
||||
Icons.play_circle_outline_rounded,
|
||||
color: Colors.white,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user