mirror of
https://github.com/immich-app/immich.git
synced 2024-12-25 10:43:13 +02:00
feat(mobile): lazy loading of assets (#2413)
This commit is contained in:
parent
93863b0629
commit
0dde76bbbc
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@ -115,7 +115,7 @@ jobs:
|
|||||||
flutter-version: "3.10.0"
|
flutter-version: "3.10.0"
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
working-directory: ./mobile
|
working-directory: ./mobile
|
||||||
run: flutter test
|
run: flutter test -j 1
|
||||||
|
|
||||||
generated-api-up-to-date:
|
generated-api-up-to-date:
|
||||||
name: Check generated files are up-to-date
|
name: Check generated files are up-to-date
|
||||||
|
@ -21,6 +21,7 @@
|
|||||||
"asset_list_layout_settings_group_by": "Group assets by",
|
"asset_list_layout_settings_group_by": "Group assets by",
|
||||||
"asset_list_layout_settings_group_by_month": "Month",
|
"asset_list_layout_settings_group_by_month": "Month",
|
||||||
"asset_list_layout_settings_group_by_month_day": "Month + day",
|
"asset_list_layout_settings_group_by_month_day": "Month + day",
|
||||||
|
"asset_list_layout_settings_group_automatically": "Automatic",
|
||||||
"asset_list_settings_subtitle": "Photo grid layout settings",
|
"asset_list_settings_subtitle": "Photo grid layout settings",
|
||||||
"asset_list_settings_title": "Photo Grid",
|
"asset_list_settings_title": "Photo Grid",
|
||||||
"backup_album_selection_page_albums_device": "Albums on device ({})",
|
"backup_album_selection_page_albums_device": "Albums on device ({})",
|
||||||
@ -276,4 +277,4 @@
|
|||||||
"description_input_hint_text": "Add description...",
|
"description_input_hint_text": "Add description...",
|
||||||
"archive_page_title": "Archive ({})",
|
"archive_page_title": "Archive ({})",
|
||||||
"archive_page_no_archived_assets": "No archived assets found"
|
"archive_page_no_archived_assets": "No archived assets found"
|
||||||
}
|
}
|
@ -2,47 +2,20 @@ import 'package:collection/collection.dart';
|
|||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
|
|
||||||
class AssetSelectionPageResult {
|
class AssetSelectionPageResult {
|
||||||
final Set<Asset> selectedNewAsset;
|
final Set<Asset> selectedAssets;
|
||||||
final Set<Asset> selectedAdditionalAsset;
|
|
||||||
final bool isAlbumExist;
|
|
||||||
|
|
||||||
AssetSelectionPageResult({
|
AssetSelectionPageResult({
|
||||||
required this.selectedNewAsset,
|
required this.selectedAssets,
|
||||||
required this.selectedAdditionalAsset,
|
|
||||||
required this.isAlbumExist,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
AssetSelectionPageResult copyWith({
|
|
||||||
Set<Asset>? selectedNewAsset,
|
|
||||||
Set<Asset>? selectedAdditionalAsset,
|
|
||||||
bool? isAlbumExist,
|
|
||||||
}) {
|
|
||||||
return AssetSelectionPageResult(
|
|
||||||
selectedNewAsset: selectedNewAsset ?? this.selectedNewAsset,
|
|
||||||
selectedAdditionalAsset:
|
|
||||||
selectedAdditionalAsset ?? this.selectedAdditionalAsset,
|
|
||||||
isAlbumExist: isAlbumExist ?? this.isAlbumExist,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() =>
|
|
||||||
'AssetSelectionPageResult(selectedNewAsset: $selectedNewAsset, selectedAdditionalAsset: $selectedAdditionalAsset, isAlbumExist: $isAlbumExist)';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
if (identical(this, other)) return true;
|
if (identical(this, other)) return true;
|
||||||
final setEquals = const DeepCollectionEquality().equals;
|
final setEquals = const DeepCollectionEquality().equals;
|
||||||
|
|
||||||
return other is AssetSelectionPageResult &&
|
return other is AssetSelectionPageResult &&
|
||||||
setEquals(other.selectedNewAsset, selectedNewAsset) &&
|
setEquals(other.selectedAssets, selectedAssets);
|
||||||
setEquals(other.selectedAdditionalAsset, selectedAdditionalAsset) &&
|
|
||||||
other.isAlbumExist == isAlbumExist;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode => selectedAssets.hashCode;
|
||||||
selectedNewAsset.hashCode ^
|
|
||||||
selectedAdditionalAsset.hashCode ^
|
|
||||||
isAlbumExist.hashCode;
|
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import 'package:collection/collection.dart';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
@ -9,50 +10,38 @@ import 'package:immich_mobile/shared/providers/db.provider.dart';
|
|||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
|
|
||||||
class AlbumNotifier extends StateNotifier<List<Album>> {
|
class AlbumNotifier extends StateNotifier<List<Album>> {
|
||||||
AlbumNotifier(this._albumService, this._db) : super([]);
|
AlbumNotifier(this._albumService, Isar db) : super([]) {
|
||||||
|
final query = db.albums
|
||||||
|
.filter()
|
||||||
|
.owner((q) => q.isarIdEqualTo(Store.get(StoreKey.currentUser).isarId));
|
||||||
|
query.findAll().then((value) => state = value);
|
||||||
|
_streamSub = query.watch().listen((data) => state = data);
|
||||||
|
}
|
||||||
final AlbumService _albumService;
|
final AlbumService _albumService;
|
||||||
final Isar _db;
|
late final StreamSubscription<List<Album>> _streamSub;
|
||||||
|
|
||||||
Future<void> getAllAlbums() async {
|
Future<void> getAllAlbums() => Future.wait([
|
||||||
final User me = Store.get(StoreKey.currentUser);
|
_albumService.refreshDeviceAlbums(),
|
||||||
List<Album> albums = await _db.albums
|
_albumService.refreshRemoteAlbums(isShared: false),
|
||||||
.filter()
|
]);
|
||||||
.owner((q) => q.isarIdEqualTo(me.isarId))
|
|
||||||
.findAll();
|
|
||||||
if (!const ListEquality().equals(albums, state)) {
|
|
||||||
state = albums;
|
|
||||||
}
|
|
||||||
await Future.wait([
|
|
||||||
_albumService.refreshDeviceAlbums(),
|
|
||||||
_albumService.refreshRemoteAlbums(isShared: false),
|
|
||||||
]);
|
|
||||||
albums = await _db.albums
|
|
||||||
.filter()
|
|
||||||
.owner((q) => q.isarIdEqualTo(me.isarId))
|
|
||||||
.findAll();
|
|
||||||
if (!const ListEquality().equals(albums, state)) {
|
|
||||||
state = albums;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> deleteAlbum(Album album) async {
|
Future<bool> deleteAlbum(Album album) => _albumService.deleteAlbum(album);
|
||||||
state = state.where((a) => a.id != album.id).toList();
|
|
||||||
return _albumService.deleteAlbum(album);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Album?> createAlbum(
|
Future<Album?> createAlbum(
|
||||||
String albumTitle,
|
String albumTitle,
|
||||||
Set<Asset> assets,
|
Set<Asset> assets,
|
||||||
) async {
|
) =>
|
||||||
Album? album = await _albumService.createAlbum(albumTitle, assets, []);
|
_albumService.createAlbum(albumTitle, assets, []);
|
||||||
if (album != null) {
|
|
||||||
state = [...state, album];
|
@override
|
||||||
}
|
void dispose() {
|
||||||
return album;
|
_streamSub.cancel();
|
||||||
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final albumProvider = StateNotifierProvider<AlbumNotifier, List<Album>>((ref) {
|
final albumProvider =
|
||||||
|
StateNotifierProvider.autoDispose<AlbumNotifier, List<Album>>((ref) {
|
||||||
return AlbumNotifier(
|
return AlbumNotifier(
|
||||||
ref.watch(albumServiceProvider),
|
ref.watch(albumServiceProvider),
|
||||||
ref.watch(dbProvider),
|
ref.watch(dbProvider),
|
||||||
|
@ -1,134 +0,0 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:immich_mobile/modules/album/models/asset_selection_state.model.dart';
|
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
|
||||||
|
|
||||||
class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
|
|
||||||
AssetSelectionNotifier()
|
|
||||||
: super(
|
|
||||||
AssetSelectionState(
|
|
||||||
selectedNewAssetsForAlbum: {},
|
|
||||||
selectedMonths: {},
|
|
||||||
selectedAdditionalAssetsForAlbum: {},
|
|
||||||
selectedAssetsInAlbumViewer: {},
|
|
||||||
isAlbumExist: false,
|
|
||||||
isMultiselectEnable: false,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
void setIsAlbumExist(bool isAlbumExist) {
|
|
||||||
state = state.copyWith(isAlbumExist: isAlbumExist);
|
|
||||||
}
|
|
||||||
|
|
||||||
void removeAssetsInMonth(
|
|
||||||
String removedMonth,
|
|
||||||
List<Asset> assetsInMonth,
|
|
||||||
) {
|
|
||||||
Set<Asset> currentAssetList = state.selectedNewAssetsForAlbum;
|
|
||||||
Set<String> currentMonthList = state.selectedMonths;
|
|
||||||
|
|
||||||
currentMonthList
|
|
||||||
.removeWhere((selectedMonth) => selectedMonth == removedMonth);
|
|
||||||
|
|
||||||
for (Asset asset in assetsInMonth) {
|
|
||||||
currentAssetList.removeWhere((e) => e.id == asset.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
state = state.copyWith(
|
|
||||||
selectedNewAssetsForAlbum: currentAssetList,
|
|
||||||
selectedMonths: currentMonthList,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void addAdditionalAssets(List<Asset> assets) {
|
|
||||||
state = state.copyWith(
|
|
||||||
selectedAdditionalAssetsForAlbum: {
|
|
||||||
...state.selectedAdditionalAssetsForAlbum,
|
|
||||||
...assets
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void addAllAssetsInMonth(String month, List<Asset> assetsInMonth) {
|
|
||||||
state = state.copyWith(
|
|
||||||
selectedMonths: {...state.selectedMonths, month},
|
|
||||||
selectedNewAssetsForAlbum: {
|
|
||||||
...state.selectedNewAssetsForAlbum,
|
|
||||||
...assetsInMonth
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void addNewAssets(Iterable<Asset> assets) {
|
|
||||||
state = state.copyWith(
|
|
||||||
selectedNewAssetsForAlbum: {
|
|
||||||
...state.selectedNewAssetsForAlbum,
|
|
||||||
...assets
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void removeSelectedNewAssets(List<Asset> assets) {
|
|
||||||
Set<Asset> currentList = state.selectedNewAssetsForAlbum;
|
|
||||||
|
|
||||||
for (Asset asset in assets) {
|
|
||||||
currentList.removeWhere((e) => e.id == asset.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
state = state.copyWith(selectedNewAssetsForAlbum: currentList);
|
|
||||||
}
|
|
||||||
|
|
||||||
void removeSelectedAdditionalAssets(List<Asset> assets) {
|
|
||||||
Set<Asset> currentList = state.selectedAdditionalAssetsForAlbum;
|
|
||||||
|
|
||||||
for (Asset asset in assets) {
|
|
||||||
currentList.removeWhere((e) => e.id == asset.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
state = state.copyWith(selectedAdditionalAssetsForAlbum: currentList);
|
|
||||||
}
|
|
||||||
|
|
||||||
void removeAll() {
|
|
||||||
state = state.copyWith(
|
|
||||||
selectedNewAssetsForAlbum: {},
|
|
||||||
selectedMonths: {},
|
|
||||||
selectedAdditionalAssetsForAlbum: {},
|
|
||||||
selectedAssetsInAlbumViewer: {},
|
|
||||||
isAlbumExist: false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void enableMultiselection() {
|
|
||||||
state = state.copyWith(isMultiselectEnable: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
void disableMultiselection() {
|
|
||||||
state = state.copyWith(
|
|
||||||
isMultiselectEnable: false,
|
|
||||||
selectedAssetsInAlbumViewer: {},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void addAssetsInAlbumViewer(List<Asset> assets) {
|
|
||||||
state = state.copyWith(
|
|
||||||
selectedAssetsInAlbumViewer: {
|
|
||||||
...state.selectedAssetsInAlbumViewer,
|
|
||||||
...assets
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void removeAssetsInAlbumViewer(List<Asset> assets) {
|
|
||||||
Set<Asset> currentList = state.selectedAssetsInAlbumViewer;
|
|
||||||
|
|
||||||
for (Asset asset in assets) {
|
|
||||||
currentList.removeWhere((e) => e.id == asset.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
state = state.copyWith(selectedAssetsInAlbumViewer: currentList);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final assetSelectionProvider =
|
|
||||||
StateNotifierProvider<AssetSelectionNotifier, AssetSelectionState>((ref) {
|
|
||||||
return AssetSelectionNotifier();
|
|
||||||
});
|
|
@ -1,7 +1,9 @@
|
|||||||
import 'package:collection/collection.dart';
|
import 'dart:async';
|
||||||
|
|
||||||
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/services/album.service.dart';
|
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
||||||
|
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
|
||||||
import 'package:immich_mobile/shared/models/album.dart';
|
import 'package:immich_mobile/shared/models/album.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/models/user.dart';
|
import 'package:immich_mobile/shared/models/user.dart';
|
||||||
@ -9,10 +11,14 @@ import 'package:immich_mobile/shared/providers/db.provider.dart';
|
|||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
|
|
||||||
class SharedAlbumNotifier extends StateNotifier<List<Album>> {
|
class SharedAlbumNotifier extends StateNotifier<List<Album>> {
|
||||||
SharedAlbumNotifier(this._albumService, this._db) : super([]);
|
SharedAlbumNotifier(this._albumService, Isar db) : super([]) {
|
||||||
|
final query = db.albums.filter().sharedEqualTo(true).sortByCreatedAtDesc();
|
||||||
|
query.findAll().then((value) => state = value);
|
||||||
|
_streamSub = query.watch().listen((data) => state = data);
|
||||||
|
}
|
||||||
|
|
||||||
final AlbumService _albumService;
|
final AlbumService _albumService;
|
||||||
final Isar _db;
|
late final StreamSubscription<List<Album>> _streamSub;
|
||||||
|
|
||||||
Future<Album?> createSharedAlbum(
|
Future<Album?> createSharedAlbum(
|
||||||
String albumName,
|
String albumName,
|
||||||
@ -20,46 +26,21 @@ class SharedAlbumNotifier extends StateNotifier<List<Album>> {
|
|||||||
Iterable<User> sharedUsers,
|
Iterable<User> sharedUsers,
|
||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
final Album? newAlbum = await _albumService.createAlbum(
|
return await _albumService.createAlbum(
|
||||||
albumName,
|
albumName,
|
||||||
assets,
|
assets,
|
||||||
sharedUsers,
|
sharedUsers,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (newAlbum != null) {
|
|
||||||
state = [...state, newAlbum];
|
|
||||||
return newAlbum;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Error createSharedAlbum ${e.toString()}");
|
debugPrint("Error createSharedAlbum ${e.toString()}");
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> getAllSharedAlbums() async {
|
Future<void> getAllSharedAlbums() =>
|
||||||
var albums = await _db.albums
|
_albumService.refreshRemoteAlbums(isShared: true);
|
||||||
.filter()
|
|
||||||
.sharedEqualTo(true)
|
|
||||||
.sortByCreatedAtDesc()
|
|
||||||
.findAll();
|
|
||||||
if (!const ListEquality().equals(albums, state)) {
|
|
||||||
state = albums;
|
|
||||||
}
|
|
||||||
await _albumService.refreshRemoteAlbums(isShared: true);
|
|
||||||
albums = await _db.albums
|
|
||||||
.filter()
|
|
||||||
.sharedEqualTo(true)
|
|
||||||
.sortByCreatedAtDesc()
|
|
||||||
.findAll();
|
|
||||||
if (!const ListEquality().equals(albums, state)) {
|
|
||||||
state = albums;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> deleteAlbum(Album album) {
|
Future<bool> deleteAlbum(Album album) => _albumService.deleteAlbum(album);
|
||||||
state = state.where((a) => a.id != album.id).toList();
|
|
||||||
return _albumService.deleteAlbum(album);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> leaveAlbum(Album album) async {
|
Future<bool> leaveAlbum(Album album) async {
|
||||||
var res = await _albumService.leaveAlbum(album);
|
var res = await _albumService.leaveAlbum(album);
|
||||||
@ -75,10 +56,16 @@ class SharedAlbumNotifier extends StateNotifier<List<Album>> {
|
|||||||
Future<bool> removeAssetFromAlbum(Album album, Iterable<Asset> assets) {
|
Future<bool> removeAssetFromAlbum(Album album, Iterable<Asset> assets) {
|
||||||
return _albumService.removeAssetFromAlbum(album, assets);
|
return _albumService.removeAssetFromAlbum(album, assets);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_streamSub.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final sharedAlbumProvider =
|
final sharedAlbumProvider =
|
||||||
StateNotifierProvider<SharedAlbumNotifier, List<Album>>((ref) {
|
StateNotifierProvider.autoDispose<SharedAlbumNotifier, List<Album>>((ref) {
|
||||||
return SharedAlbumNotifier(
|
return SharedAlbumNotifier(
|
||||||
ref.watch(albumServiceProvider),
|
ref.watch(albumServiceProvider),
|
||||||
ref.watch(dbProvider),
|
ref.watch(dbProvider),
|
||||||
@ -86,10 +73,15 @@ final sharedAlbumProvider =
|
|||||||
});
|
});
|
||||||
|
|
||||||
final sharedAlbumDetailProvider =
|
final sharedAlbumDetailProvider =
|
||||||
FutureProvider.autoDispose.family<Album?, int>((ref, albumId) async {
|
StreamProvider.autoDispose.family<Album, int>((ref, albumId) async* {
|
||||||
final AlbumService sharedAlbumService = ref.watch(albumServiceProvider);
|
final AlbumService sharedAlbumService = ref.watch(albumServiceProvider);
|
||||||
|
|
||||||
final Album? a = await sharedAlbumService.getAlbumDetail(albumId);
|
await for (final a in sharedAlbumService.watchAlbum(albumId)) {
|
||||||
await a?.loadSortedAssets();
|
if (a == null) {
|
||||||
return a;
|
throw Exception("Album with ID=$albumId does not exist anymore!");
|
||||||
|
}
|
||||||
|
await for (final _ in a.watchRenderList(GroupAssetsBy.none)) {
|
||||||
|
yield a;
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
@ -214,8 +214,9 @@ class AlbumService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Album?> getAlbumDetail(int albumId) {
|
Stream<Album?> watchAlbum(int albumId) async* {
|
||||||
return _db.albums.get(albumId);
|
yield await _db.albums.get(albumId);
|
||||||
|
yield* _db.albums.watchObject(albumId);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<AddAssetsResponseDto?> addAdditionalAssetToAlbum(
|
Future<AddAssetsResponseDto?> addAdditionalAssetToAlbum(
|
||||||
|
@ -4,7 +4,6 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.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.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/providers/shared_album.provider.dart';
|
||||||
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
||||||
import 'package:immich_mobile/modules/album/ui/add_to_album_sliverlist.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),
|
TextStyle(color: Theme.of(context).primaryColor),
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ref
|
|
||||||
.watch(assetSelectionProvider.notifier)
|
|
||||||
.removeAll();
|
|
||||||
ref
|
|
||||||
.watch(assetSelectionProvider.notifier)
|
|
||||||
.addNewAssets(assets);
|
|
||||||
AutoRouter.of(context).push(
|
AutoRouter.of(context).push(
|
||||||
CreateAlbumRoute(
|
CreateAlbumRoute(
|
||||||
isSharedAlbum: false,
|
isSharedAlbum: false,
|
||||||
|
@ -9,12 +9,14 @@ class AddToAlbumSliverList extends HookConsumerWidget {
|
|||||||
final List<Album> albums;
|
final List<Album> albums;
|
||||||
final List<Album> sharedAlbums;
|
final List<Album> sharedAlbums;
|
||||||
final void Function(Album) onAddToAlbum;
|
final void Function(Album) onAddToAlbum;
|
||||||
|
final bool enabled;
|
||||||
|
|
||||||
const AddToAlbumSliverList({
|
const AddToAlbumSliverList({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.onAddToAlbum,
|
required this.onAddToAlbum,
|
||||||
required this.albums,
|
required this.albums,
|
||||||
required this.sharedAlbums,
|
required this.sharedAlbums,
|
||||||
|
this.enabled = true,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -28,14 +30,14 @@ class AddToAlbumSliverList extends HookConsumerWidget {
|
|||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 8),
|
padding: const EdgeInsets.only(bottom: 8),
|
||||||
child: ExpansionTile(
|
child: ExpansionTile(
|
||||||
title: Text('common_shared'.tr()),
|
title: Text('common_shared'.tr()),
|
||||||
tilePadding: const EdgeInsets.symmetric(horizontal: 10.0),
|
tilePadding: const EdgeInsets.symmetric(horizontal: 10.0),
|
||||||
leading: const Icon(Icons.group),
|
leading: const Icon(Icons.group),
|
||||||
children: sharedAlbums
|
children: sharedAlbums
|
||||||
.map(
|
.map(
|
||||||
(album) => AlbumThumbnailListTile(
|
(album) => AlbumThumbnailListTile(
|
||||||
album: album,
|
album: album,
|
||||||
onTap: () => onAddToAlbum(album),
|
onTap: enabled ? () => onAddToAlbum(album) : () {},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
@ -48,7 +50,7 @@ class AddToAlbumSliverList extends HookConsumerWidget {
|
|||||||
final album = albums[offset];
|
final album = albums[offset];
|
||||||
return AlbumThumbnailListTile(
|
return AlbumThumbnailListTile(
|
||||||
album: album,
|
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:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/album/providers/album.provider.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/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/modules/album/providers/shared_album.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/shared/models/album.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/ui/immich_toast.dart';
|
||||||
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
|
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
|
||||||
|
|
||||||
@ -18,17 +18,19 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
|||||||
Key? key,
|
Key? key,
|
||||||
required this.album,
|
required this.album,
|
||||||
required this.userId,
|
required this.userId,
|
||||||
|
required this.selected,
|
||||||
|
required this.selectionDisabled,
|
||||||
|
required this.titleFocusNode,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final Album album;
|
final Album album;
|
||||||
final String userId;
|
final String userId;
|
||||||
|
final Set<Asset> selected;
|
||||||
|
final void Function() selectionDisabled;
|
||||||
|
final FocusNode titleFocusNode;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
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 newAlbumTitle = ref.watch(albumViewerProvider).editTitleText;
|
||||||
final isEditAlbum = ref.watch(albumViewerProvider).isEditAlbum;
|
final isEditAlbum = ref.watch(albumViewerProvider).isEditAlbum;
|
||||||
|
|
||||||
@ -86,12 +88,12 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
|||||||
bool isSuccess =
|
bool isSuccess =
|
||||||
await ref.watch(sharedAlbumProvider.notifier).removeAssetFromAlbum(
|
await ref.watch(sharedAlbumProvider.notifier).removeAssetFromAlbum(
|
||||||
album,
|
album,
|
||||||
selectedAssetsInAlbum,
|
selected,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
ref.watch(assetSelectionProvider.notifier).disableMultiselection();
|
selectionDisabled();
|
||||||
ref.watch(albumProvider.notifier).getAllAlbums();
|
ref.watch(albumProvider.notifier).getAllAlbums();
|
||||||
ref.invalidate(sharedAlbumDetailProvider(album.id));
|
ref.invalidate(sharedAlbumDetailProvider(album.id));
|
||||||
} else {
|
} else {
|
||||||
@ -108,7 +110,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
|||||||
}
|
}
|
||||||
|
|
||||||
buildBottomSheetActionButton() {
|
buildBottomSheetActionButton() {
|
||||||
if (isMultiSelectionEnable) {
|
if (selected.isNotEmpty) {
|
||||||
if (album.ownerId == userId) {
|
if (album.ownerId == userId) {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: const Icon(Icons.delete_sweep_rounded),
|
leading: const Icon(Icons.delete_sweep_rounded),
|
||||||
@ -163,11 +165,9 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
|||||||
}
|
}
|
||||||
|
|
||||||
buildLeadingButton() {
|
buildLeadingButton() {
|
||||||
if (isMultiSelectionEnable) {
|
if (selected.isNotEmpty) {
|
||||||
return IconButton(
|
return IconButton(
|
||||||
onPressed: () => ref
|
onPressed: selectionDisabled,
|
||||||
.watch(assetSelectionProvider.notifier)
|
|
||||||
.disableMultiselection(),
|
|
||||||
icon: const Icon(Icons.close_rounded),
|
icon: const Icon(Icons.close_rounded),
|
||||||
splashRadius: 25,
|
splashRadius: 25,
|
||||||
);
|
);
|
||||||
@ -202,9 +202,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
|||||||
return AppBar(
|
return AppBar(
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
leading: buildLeadingButton(),
|
leading: buildLeadingButton(),
|
||||||
title: isMultiSelectionEnable
|
title: selected.isNotEmpty ? Text('${selected.length}') : null,
|
||||||
? Text('${selectedAssetsInAlbum.length}')
|
|
||||||
: null,
|
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
actions: [
|
actions: [
|
||||||
if (album.isRemote)
|
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,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -5,21 +5,18 @@ import 'package:easy_localization/easy_localization.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:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
|
|
||||||
import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart';
|
|
||||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
|
||||||
import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart';
|
import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart';
|
||||||
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/album.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/services/album.service.dart';
|
||||||
import 'package:immich_mobile/modules/album/ui/album_action_outlined_button.dart';
|
import 'package:immich_mobile/modules/album/ui/album_action_outlined_button.dart';
|
||||||
import 'package:immich_mobile/modules/album/ui/album_viewer_appbar.dart';
|
|
||||||
import 'package:immich_mobile/modules/album/ui/album_viewer_editable_title.dart';
|
import 'package:immich_mobile/modules/album/ui/album_viewer_editable_title.dart';
|
||||||
import 'package:immich_mobile/modules/album/ui/album_viewer_thumbnail.dart';
|
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
|
||||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
|
||||||
|
import 'package:immich_mobile/modules/album/ui/album_viewer_appbar.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/shared/models/album.dart';
|
import 'package:immich_mobile/shared/models/album.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_sliver_persistent_app_bar_delegate.dart';
|
import 'package:immich_mobile/shared/ui/immich_sliver_persistent_app_bar_delegate.dart';
|
||||||
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
|
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
|
||||||
@ -32,33 +29,51 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
FocusNode titleFocusNode = useFocusNode();
|
FocusNode titleFocusNode = useFocusNode();
|
||||||
ScrollController scrollController = useScrollController();
|
|
||||||
final album = ref.watch(sharedAlbumDetailProvider(albumId));
|
final album = ref.watch(sharedAlbumDetailProvider(albumId));
|
||||||
|
|
||||||
final userId = ref.watch(authenticationProvider).userId;
|
final userId = ref.watch(authenticationProvider).userId;
|
||||||
|
final selection = useState<Set<Asset>>({});
|
||||||
|
final multiSelectEnabled = useState(false);
|
||||||
|
bool? isTop;
|
||||||
|
|
||||||
|
Future<bool> onWillPop() async {
|
||||||
|
if (multiSelectEnabled.value) {
|
||||||
|
selection.value = {};
|
||||||
|
multiSelectEnabled.value = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void selectionListener(bool active, Set<Asset> selected) {
|
||||||
|
selection.value = selected;
|
||||||
|
multiSelectEnabled.value = selected.isNotEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
|
void disableSelection() {
|
||||||
|
selection.value = {};
|
||||||
|
multiSelectEnabled.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
/// Find out if the assets in album exist on the device
|
/// Find out if the assets in album exist on the device
|
||||||
/// If they exist, add to selected asset state to show they are already selected.
|
/// If they exist, add to selected asset state to show they are already selected.
|
||||||
void onAddPhotosPressed(Album albumInfo) async {
|
void onAddPhotosPressed(Album albumInfo) async {
|
||||||
if (albumInfo.assets.isNotEmpty == true) {
|
AssetSelectionPageResult? returnPayload =
|
||||||
ref.watch(assetSelectionProvider.notifier).addNewAssets(
|
await AutoRouter.of(context).push<AssetSelectionPageResult?>(
|
||||||
albumInfo.assets,
|
AssetSelectionRoute(
|
||||||
);
|
existingAssets: albumInfo.assets,
|
||||||
}
|
isNewAlbum: false,
|
||||||
|
),
|
||||||
ref.watch(assetSelectionProvider.notifier).setIsAlbumExist(true);
|
);
|
||||||
|
|
||||||
AssetSelectionPageResult? returnPayload = await AutoRouter.of(context)
|
|
||||||
.push<AssetSelectionPageResult?>(const AssetSelectionRoute());
|
|
||||||
|
|
||||||
if (returnPayload != null) {
|
if (returnPayload != null) {
|
||||||
// Check if there is new assets add
|
// Check if there is new assets add
|
||||||
if (returnPayload.selectedAdditionalAsset.isNotEmpty) {
|
if (returnPayload.selectedAssets.isNotEmpty) {
|
||||||
ImmichLoadingOverlayController.appLoader.show();
|
ImmichLoadingOverlayController.appLoader.show();
|
||||||
|
|
||||||
var addAssetsResult =
|
var addAssetsResult =
|
||||||
await ref.watch(albumServiceProvider).addAdditionalAssetToAlbum(
|
await ref.watch(albumServiceProvider).addAdditionalAssetToAlbum(
|
||||||
returnPayload.selectedAdditionalAsset,
|
returnPayload.selectedAssets,
|
||||||
albumInfo,
|
albumInfo,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -70,10 +85,6 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
ImmichLoadingOverlayController.appLoader.hide();
|
ImmichLoadingOverlayController.appLoader.hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
ref.watch(assetSelectionProvider.notifier).removeAll();
|
|
||||||
} else {
|
|
||||||
ref.watch(assetSelectionProvider.notifier).removeAll();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,13 +102,38 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
.addAdditionalUserToAlbum(sharedUserIds, album);
|
.addAdditionalUserToAlbum(sharedUserIds, album);
|
||||||
|
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
ref.invalidate(sharedAlbumDetailProvider(albumId));
|
ref.invalidate(sharedAlbumDetailProvider(album.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
ImmichLoadingOverlayController.appLoader.hide();
|
ImmichLoadingOverlayController.appLoader.hide();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget buildControlButton(Album album) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 16.0, top: 8, bottom: 8),
|
||||||
|
child: SizedBox(
|
||||||
|
height: 40,
|
||||||
|
child: ListView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
children: [
|
||||||
|
AlbumActionOutlinedButton(
|
||||||
|
iconData: Icons.add_photo_alternate_outlined,
|
||||||
|
onPressed: () => onAddPhotosPressed(album),
|
||||||
|
labelText: "share_add_photos".tr(),
|
||||||
|
),
|
||||||
|
if (userId == album.ownerId)
|
||||||
|
AlbumActionOutlinedButton(
|
||||||
|
iconData: Icons.person_add_alt_rounded,
|
||||||
|
onPressed: () => onAddUsersPressed(album),
|
||||||
|
labelText: "album_viewer_page_share_add_users".tr(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget buildTitle(Album album) {
|
Widget buildTitle(Album album) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(left: 8, right: 8, top: 16),
|
padding: const EdgeInsets.only(left: 8, right: 8, top: 16),
|
||||||
@ -146,171 +182,104 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget buildHeader(Album album) {
|
Widget buildHeader(Album album) {
|
||||||
return SliverToBoxAdapter(
|
return Column(
|
||||||
child: Column(
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
buildTitle(album),
|
buildTitle(album),
|
||||||
if (album.assets.isNotEmpty == true) buildAlbumDateRange(album),
|
if (album.assets.isNotEmpty == true) buildAlbumDateRange(album),
|
||||||
if (album.shared)
|
if (album.shared)
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 60,
|
height: 50,
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
padding: const EdgeInsets.only(left: 16),
|
padding: const EdgeInsets.only(left: 16),
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
itemBuilder: ((context, index) {
|
itemBuilder: ((context, index) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(right: 8.0),
|
padding: const EdgeInsets.only(right: 8.0),
|
||||||
child: CircleAvatar(
|
child: CircleAvatar(
|
||||||
backgroundColor: Colors.grey[300],
|
backgroundColor: Colors.grey[300],
|
||||||
radius: 18,
|
radius: 18,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(2.0),
|
padding: const EdgeInsets.all(2.0),
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(50.0),
|
borderRadius: BorderRadius.circular(50.0),
|
||||||
child: Image.asset(
|
child: Image.asset(
|
||||||
'assets/immich-logo-no-outline.png',
|
'assets/immich-logo-no-outline.png',
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
|
||||||
}),
|
|
||||||
itemCount: album.sharedUsers.length,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget buildImageGrid(Album album) {
|
|
||||||
final appSettingService = ref.watch(appSettingsServiceProvider);
|
|
||||||
final bool showStorageIndicator =
|
|
||||||
appSettingService.getSetting(AppSettingsEnum.storageIndicator);
|
|
||||||
|
|
||||||
if (album.sortedAssets.isNotEmpty) {
|
|
||||||
return SliverPadding(
|
|
||||||
padding: const EdgeInsets.only(top: 10.0),
|
|
||||||
sliver: SliverGrid(
|
|
||||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
|
||||||
crossAxisCount:
|
|
||||||
appSettingService.getSetting(AppSettingsEnum.tilesPerRow),
|
|
||||||
crossAxisSpacing: 5.0,
|
|
||||||
mainAxisSpacing: 5,
|
|
||||||
),
|
|
||||||
delegate: SliverChildBuilderDelegate(
|
|
||||||
(BuildContext context, int index) {
|
|
||||||
return AlbumViewerThumbnail(
|
|
||||||
asset: album.sortedAssets[index],
|
|
||||||
assetList: album.sortedAssets,
|
|
||||||
showStorageIndicator: showStorageIndicator,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
childCount: album.assetCount,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return const SliverToBoxAdapter();
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget buildControlButton(Album album) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 16.0, top: 8, bottom: 8),
|
|
||||||
child: SizedBox(
|
|
||||||
height: 40,
|
|
||||||
child: ListView(
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
children: [
|
|
||||||
AlbumActionOutlinedButton(
|
|
||||||
iconData: Icons.add_photo_alternate_outlined,
|
|
||||||
onPressed: () => onAddPhotosPressed(album),
|
|
||||||
labelText: "share_add_photos".tr(),
|
|
||||||
),
|
|
||||||
if (userId == album.ownerId)
|
|
||||||
AlbumActionOutlinedButton(
|
|
||||||
iconData: Icons.person_add_alt_rounded,
|
|
||||||
onPressed: () => onAddUsersPressed(album),
|
|
||||||
labelText: "album_viewer_page_share_add_users".tr(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> onWillPop() async {
|
|
||||||
final isMultiselectEnable = ref
|
|
||||||
.read(assetSelectionProvider)
|
|
||||||
.selectedAssetsInAlbumViewer
|
|
||||||
.isNotEmpty;
|
|
||||||
if (isMultiselectEnable) {
|
|
||||||
ref.watch(assetSelectionProvider.notifier).removeAll();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget buildBody(Album album) {
|
|
||||||
return WillPopScope(
|
|
||||||
onWillPop: onWillPop,
|
|
||||||
child: GestureDetector(
|
|
||||||
onTap: () {
|
|
||||||
titleFocusNode.unfocus();
|
|
||||||
},
|
|
||||||
child: DraggableScrollbar.semicircle(
|
|
||||||
backgroundColor: Theme.of(context).hintColor,
|
|
||||||
controller: scrollController,
|
|
||||||
heightScrollThumb: 48.0,
|
|
||||||
child: CustomScrollView(
|
|
||||||
controller: scrollController,
|
|
||||||
slivers: [
|
|
||||||
buildHeader(album),
|
|
||||||
if (album.isRemote)
|
|
||||||
SliverPersistentHeader(
|
|
||||||
pinned: true,
|
|
||||||
delegate: ImmichSliverPersistentAppBarDelegate(
|
|
||||||
minHeight: 50,
|
|
||||||
maxHeight: 50,
|
|
||||||
child: Container(
|
|
||||||
color: Theme.of(context).scaffoldBackgroundColor,
|
|
||||||
child: buildControlButton(album),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
SliverSafeArea(
|
}),
|
||||||
sliver: buildImageGrid(album),
|
itemCount: album.sharedUsers.length,
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final scroll = ScrollController();
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: album.when(
|
appBar: album.when(
|
||||||
data: (Album? data) {
|
data: (data) => AlbumViewerAppbar(
|
||||||
if (data != null) {
|
titleFocusNode: titleFocusNode,
|
||||||
return AlbumViewerAppbar(
|
album: data,
|
||||||
album: data,
|
userId: userId,
|
||||||
userId: userId,
|
selected: selection.value,
|
||||||
);
|
selectionDisabled: disableSelection,
|
||||||
}
|
),
|
||||||
return null;
|
error: (error, stackTrace) => AppBar(title: const Text("Error")),
|
||||||
},
|
loading: () => AppBar(),
|
||||||
error: (e, _) => null,
|
|
||||||
loading: () => null,
|
|
||||||
),
|
),
|
||||||
body: album.when(
|
body: album.when(
|
||||||
data: (albumInfo) => albumInfo != null
|
data: (data) => WillPopScope(
|
||||||
? buildBody(albumInfo)
|
onWillPop: onWillPop,
|
||||||
: const Center(
|
child: GestureDetector(
|
||||||
child: CircularProgressIndicator(),
|
onTap: () {
|
||||||
|
titleFocusNode.unfocus();
|
||||||
|
},
|
||||||
|
child: NestedScrollView(
|
||||||
|
controller: scroll,
|
||||||
|
floatHeaderSlivers: true,
|
||||||
|
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
||||||
|
SliverToBoxAdapter(child: buildHeader(data)),
|
||||||
|
SliverPersistentHeader(
|
||||||
|
pinned: true,
|
||||||
|
delegate: ImmichSliverPersistentAppBarDelegate(
|
||||||
|
minHeight: 50,
|
||||||
|
maxHeight: 50,
|
||||||
|
child: Container(
|
||||||
|
color: Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
child: buildControlButton(data),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
body: ImmichAssetGrid(
|
||||||
|
renderList: data.renderList,
|
||||||
|
listener: selectionListener,
|
||||||
|
selectionActive: multiSelectEnabled.value,
|
||||||
|
showMultiSelectIndicator: false,
|
||||||
|
visibleItemsListener: (start, end) {
|
||||||
|
final top = start.index == 0 && start.itemLeadingEdge == 0.0;
|
||||||
|
if (top != isTop) {
|
||||||
|
isTop = top;
|
||||||
|
scroll.animateTo(
|
||||||
|
top
|
||||||
|
? scroll.position.minScrollExtent
|
||||||
|
: scroll.position.maxScrollExtent,
|
||||||
|
duration: const Duration(milliseconds: 500),
|
||||||
|
curve: top ? Curves.easeOut : Curves.easeIn,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
),
|
),
|
||||||
error: (e, _) => Center(child: Text("Error loading album info $e")),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
error: (e, _) => Center(child: Text("Error loading album info!\n$e")),
|
||||||
loading: () => const Center(
|
loading: () => const Center(
|
||||||
child: ImmichLoadingIndicator(),
|
child: ImmichLoadingIndicator(),
|
||||||
),
|
),
|
||||||
|
@ -4,54 +4,42 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart';
|
import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart';
|
||||||
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
|
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
|
||||||
import 'package:immich_mobile/modules/album/ui/asset_grid_by_month.dart';
|
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
|
||||||
import 'package:immich_mobile/modules/album/ui/month_group_title.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart';
|
|
||||||
|
|
||||||
class AssetSelectionPage extends HookConsumerWidget {
|
class AssetSelectionPage extends HookConsumerWidget {
|
||||||
const AssetSelectionPage({Key? key}) : super(key: key);
|
const AssetSelectionPage({
|
||||||
|
Key? key,
|
||||||
|
required this.existingAssets,
|
||||||
|
this.isNewAlbum = false,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final Set<Asset> existingAssets;
|
||||||
|
final bool isNewAlbum;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
ScrollController scrollController = useScrollController();
|
final renderList = ref.watch(remoteAssetsProvider);
|
||||||
var assetGroupMonthYear = ref.watch(assetGroupByMonthYearProvider);
|
final selected = useState<Set<Asset>>(existingAssets);
|
||||||
final selectedAssets =
|
final selectionEnabledHook = useState(true);
|
||||||
ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum;
|
|
||||||
final newAssetsForAlbum =
|
|
||||||
ref.watch(assetSelectionProvider).selectedAdditionalAssetsForAlbum;
|
|
||||||
final isAlbumExist = ref.watch(assetSelectionProvider).isAlbumExist;
|
|
||||||
|
|
||||||
List<Widget> imageGridGroup = [];
|
|
||||||
|
|
||||||
String buildAssetCountText() {
|
String buildAssetCountText() {
|
||||||
if (isAlbumExist) {
|
return selected.value.length.toString();
|
||||||
return (selectedAssets.length + newAssetsForAlbum.length).toString();
|
|
||||||
} else {
|
|
||||||
return selectedAssets.length.toString();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildBody() {
|
Widget buildBody(RenderList renderList) {
|
||||||
assetGroupMonthYear.forEach((monthYear, assetGroup) {
|
return ImmichAssetGrid(
|
||||||
imageGridGroup
|
renderList: renderList,
|
||||||
.add(MonthGroupTitle(month: monthYear, assetGroup: assetGroup));
|
listener: (active, assets) {
|
||||||
imageGridGroup.add(AssetGridByMonth(assetGroup: assetGroup));
|
selectionEnabledHook.value = active;
|
||||||
});
|
selected.value = assets;
|
||||||
|
},
|
||||||
return Stack(
|
selectionActive: true,
|
||||||
children: [
|
preselectedAssets: isNewAlbum ? selected.value : existingAssets,
|
||||||
DraggableScrollbar.semicircle(
|
canDeselect: isNewAlbum,
|
||||||
backgroundColor: Theme.of(context).hintColor,
|
showMultiSelectIndicator: false,
|
||||||
controller: scrollController,
|
|
||||||
heightScrollThumb: 48.0,
|
|
||||||
child: CustomScrollView(
|
|
||||||
controller: scrollController,
|
|
||||||
slivers: [...imageGridGroup],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,11 +49,10 @@ class AssetSelectionPage extends HookConsumerWidget {
|
|||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const Icon(Icons.close_rounded),
|
icon: const Icon(Icons.close_rounded),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ref.watch(assetSelectionProvider.notifier).removeAll();
|
AutoRouter.of(context).popForced(null);
|
||||||
AutoRouter.of(context).pop(null);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
title: selectedAssets.isEmpty
|
title: selected.value.isEmpty
|
||||||
? const Text(
|
? const Text(
|
||||||
'share_add_photos',
|
'share_add_photos',
|
||||||
style: TextStyle(fontSize: 18),
|
style: TextStyle(fontSize: 18),
|
||||||
@ -76,16 +63,13 @@ class AssetSelectionPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
actions: [
|
actions: [
|
||||||
if ((!isAlbumExist && selectedAssets.isNotEmpty) ||
|
if (selected.value.isNotEmpty)
|
||||||
(isAlbumExist && newAssetsForAlbum.isNotEmpty))
|
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
var payload = AssetSelectionPageResult(
|
var payload =
|
||||||
isAlbumExist: isAlbumExist,
|
AssetSelectionPageResult(selectedAssets: selected.value);
|
||||||
selectedAdditionalAsset: newAssetsForAlbum,
|
AutoRouter.of(context)
|
||||||
selectedNewAsset: selectedAssets,
|
.popForced<AssetSelectionPageResult>(payload);
|
||||||
);
|
|
||||||
AutoRouter.of(context).pop(payload);
|
|
||||||
},
|
},
|
||||||
child: const Text(
|
child: const Text(
|
||||||
"share_add",
|
"share_add",
|
||||||
@ -94,7 +78,13 @@ class AssetSelectionPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: buildBody(),
|
body: renderList.when(
|
||||||
|
data: (data) => buildBody(data),
|
||||||
|
error: (error, stackTrace) => Center(
|
||||||
|
child: Text(error.toString()),
|
||||||
|
),
|
||||||
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart';
|
import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart';
|
||||||
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
|
||||||
import 'package:immich_mobile/modules/album/providers/album_title.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/album_title.provider.dart';
|
||||||
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
|
|
||||||
import 'package:immich_mobile/modules/album/ui/album_action_outlined_button.dart';
|
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/album_title_text_field.dart';
|
||||||
import 'package:immich_mobile/modules/album/ui/shared_album_thumbnail_image.dart';
|
import 'package:immich_mobile/modules/album/ui/shared_album_thumbnail_image.dart';
|
||||||
@ -31,12 +30,15 @@ class CreateAlbumPage extends HookConsumerWidget {
|
|||||||
final albumTitleTextFieldFocusNode = useFocusNode();
|
final albumTitleTextFieldFocusNode = useFocusNode();
|
||||||
final isAlbumTitleTextFieldFocus = useState(false);
|
final isAlbumTitleTextFieldFocus = useState(false);
|
||||||
final isAlbumTitleEmpty = useState(true);
|
final isAlbumTitleEmpty = useState(true);
|
||||||
final selectedAssets =
|
final selectedAssets = useState<Set<Asset>>(const {});
|
||||||
ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum;
|
|
||||||
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
|
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
|
||||||
showSelectUserPage() {
|
showSelectUserPage() async {
|
||||||
AutoRouter.of(context).push(const SelectUserForSharingRoute());
|
final bool? ok = await AutoRouter.of(context)
|
||||||
|
.push<bool?>(SelectUserForSharingRoute(assets: selectedAssets.value));
|
||||||
|
if (ok == true) {
|
||||||
|
selectedAssets.value = {};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void onBackgroundTapped() {
|
void onBackgroundTapped() {
|
||||||
@ -52,13 +54,17 @@ class CreateAlbumPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onSelectPhotosButtonPressed() async {
|
onSelectPhotosButtonPressed() async {
|
||||||
ref.watch(assetSelectionProvider.notifier).setIsAlbumExist(false);
|
AssetSelectionPageResult? selectedAsset =
|
||||||
|
await AutoRouter.of(context).push<AssetSelectionPageResult?>(
|
||||||
AssetSelectionPageResult? selectedAsset = await AutoRouter.of(context)
|
AssetSelectionRoute(
|
||||||
.push<AssetSelectionPageResult?>(const AssetSelectionRoute());
|
existingAssets: selectedAssets.value,
|
||||||
|
isNewAlbum: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
if (selectedAsset == null) {
|
if (selectedAsset == null) {
|
||||||
ref.watch(assetSelectionProvider.notifier).removeAll();
|
selectedAssets.value = const {};
|
||||||
|
} else {
|
||||||
|
selectedAssets.value = selectedAsset.selectedAssets;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,7 +84,7 @@ class CreateAlbumPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
buildTitle() {
|
buildTitle() {
|
||||||
if (selectedAssets.isEmpty) {
|
if (selectedAssets.value.isEmpty) {
|
||||||
return SliverToBoxAdapter(
|
return SliverToBoxAdapter(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(top: 200, left: 18),
|
padding: const EdgeInsets.only(top: 200, left: 18),
|
||||||
@ -97,7 +103,7 @@ class CreateAlbumPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
buildSelectPhotosButton() {
|
buildSelectPhotosButton() {
|
||||||
if (selectedAssets.isEmpty) {
|
if (selectedAssets.value.isEmpty) {
|
||||||
return SliverToBoxAdapter(
|
return SliverToBoxAdapter(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(top: 16, left: 18, right: 18),
|
padding: const EdgeInsets.only(top: 16, left: 18, right: 18),
|
||||||
@ -158,7 +164,7 @@ class CreateAlbumPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
buildSelectedImageGrid() {
|
buildSelectedImageGrid() {
|
||||||
if (selectedAssets.isNotEmpty) {
|
if (selectedAssets.value.isNotEmpty) {
|
||||||
return SliverPadding(
|
return SliverPadding(
|
||||||
padding: const EdgeInsets.only(top: 16),
|
padding: const EdgeInsets.only(top: 16),
|
||||||
sliver: SliverGrid(
|
sliver: SliverGrid(
|
||||||
@ -172,11 +178,11 @@ class CreateAlbumPage extends HookConsumerWidget {
|
|||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: onBackgroundTapped,
|
onTap: onBackgroundTapped,
|
||||||
child: SharedAlbumThumbnailImage(
|
child: SharedAlbumThumbnailImage(
|
||||||
asset: selectedAssets.elementAt(index),
|
asset: selectedAssets.value.elementAt(index),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
childCount: selectedAssets.length,
|
childCount: selectedAssets.value.length,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -188,12 +194,12 @@ class CreateAlbumPage extends HookConsumerWidget {
|
|||||||
createNonSharedAlbum() async {
|
createNonSharedAlbum() async {
|
||||||
var newAlbum = await ref.watch(albumProvider.notifier).createAlbum(
|
var newAlbum = await ref.watch(albumProvider.notifier).createAlbum(
|
||||||
ref.watch(albumTitleProvider),
|
ref.watch(albumTitleProvider),
|
||||||
ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum,
|
selectedAssets.value,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (newAlbum != null) {
|
if (newAlbum != null) {
|
||||||
ref.watch(albumProvider.notifier).getAllAlbums();
|
ref.watch(albumProvider.notifier).getAllAlbums();
|
||||||
ref.watch(assetSelectionProvider.notifier).removeAll();
|
selectedAssets.value = {};
|
||||||
ref.watch(albumTitleProvider.notifier).clearAlbumTitle();
|
ref.watch(albumTitleProvider.notifier).clearAlbumTitle();
|
||||||
|
|
||||||
AutoRouter.of(context).replace(AlbumViewerRoute(albumId: newAlbum.id));
|
AutoRouter.of(context).replace(AlbumViewerRoute(albumId: newAlbum.id));
|
||||||
@ -207,7 +213,7 @@ class CreateAlbumPage extends HookConsumerWidget {
|
|||||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ref.watch(assetSelectionProvider.notifier).removeAll();
|
selectedAssets.value = {};
|
||||||
AutoRouter.of(context).pop();
|
AutoRouter.of(context).pop();
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.close_rounded),
|
icon: const Icon(Icons.close_rounded),
|
||||||
@ -237,7 +243,7 @@ class CreateAlbumPage extends HookConsumerWidget {
|
|||||||
if (!isSharedAlbum)
|
if (!isSharedAlbum)
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: albumTitleController.text.isNotEmpty &&
|
onPressed: albumTitleController.text.isNotEmpty &&
|
||||||
selectedAssets.isNotEmpty
|
selectedAssets.value.isNotEmpty
|
||||||
? createNonSharedAlbum
|
? createNonSharedAlbum
|
||||||
: null,
|
: null,
|
||||||
child: Text(
|
child: Text(
|
||||||
@ -264,7 +270,7 @@ class CreateAlbumPage extends HookConsumerWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
buildTitleInputField(),
|
buildTitleInputField(),
|
||||||
if (selectedAssets.isNotEmpty) buildControlButton(),
|
if (selectedAssets.value.isNotEmpty) buildControlButton(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -4,15 +4,18 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/album/providers/album_title.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/album_title.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/providers/shared_album.provider.dart';
|
||||||
import 'package:immich_mobile/modules/album/providers/suggested_shared_users.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/suggested_shared_users.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/models/user.dart';
|
import 'package:immich_mobile/shared/models/user.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||||
|
|
||||||
class SelectUserForSharingPage extends HookConsumerWidget {
|
class SelectUserForSharingPage extends HookConsumerWidget {
|
||||||
const SelectUserForSharingPage({Key? key}) : super(key: key);
|
const SelectUserForSharingPage({Key? key, required this.assets})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
|
final Set<Asset> assets;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
@ -24,15 +27,15 @@ class SelectUserForSharingPage extends HookConsumerWidget {
|
|||||||
var newAlbum =
|
var newAlbum =
|
||||||
await ref.watch(sharedAlbumProvider.notifier).createSharedAlbum(
|
await ref.watch(sharedAlbumProvider.notifier).createSharedAlbum(
|
||||||
ref.watch(albumTitleProvider),
|
ref.watch(albumTitleProvider),
|
||||||
ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum,
|
assets,
|
||||||
sharedUsersList.value,
|
sharedUsersList.value,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (newAlbum != null) {
|
if (newAlbum != null) {
|
||||||
await ref.watch(sharedAlbumProvider.notifier).getAllSharedAlbums();
|
await ref.watch(sharedAlbumProvider.notifier).getAllSharedAlbums();
|
||||||
ref.watch(assetSelectionProvider.notifier).removeAll();
|
// ref.watch(assetSelectionProvider.notifier).removeAll();
|
||||||
ref.watch(albumTitleProvider.notifier).clearAlbumTitle();
|
ref.watch(albumTitleProvider.notifier).clearAlbumTitle();
|
||||||
|
AutoRouter.of(context).pop(true);
|
||||||
AutoRouter.of(context)
|
AutoRouter.of(context)
|
||||||
.navigate(const TabControllerRoute(children: [SharingRoute()]));
|
.navigate(const TabControllerRoute(children: [SharingRoute()]));
|
||||||
}
|
}
|
||||||
|
@ -1,55 +1,25 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.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/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
import 'package:immich_mobile/shared/models/store.dart';
|
||||||
import 'package:immich_mobile/shared/providers/db.provider.dart';
|
import 'package:immich_mobile/shared/providers/db.provider.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
|
|
||||||
class ArchiveSelectionNotifier extends StateNotifier<Set<int>> {
|
final archiveProvider = StreamProvider<RenderList>((ref) async* {
|
||||||
ArchiveSelectionNotifier(this.db, this.assetNotifier) : super({}) {
|
final query = ref
|
||||||
state = db.assets
|
.watch(dbProvider)
|
||||||
.filter()
|
.assets
|
||||||
.isArchivedEqualTo(true)
|
.filter()
|
||||||
.findAllSync()
|
.ownerIdEqualTo(Store.get(StoreKey.currentUser).isarId)
|
||||||
.map((e) => e.id)
|
.isArchivedEqualTo(true)
|
||||||
.toSet();
|
.sortByFileCreatedAt();
|
||||||
|
final settings = ref.watch(appSettingsServiceProvider);
|
||||||
|
final groupBy =
|
||||||
|
GroupAssetsBy.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)];
|
||||||
|
yield await RenderList.fromQuery(query, groupBy);
|
||||||
|
await for (final _ in query.watchLazy()) {
|
||||||
|
yield await RenderList.fromQuery(query, groupBy);
|
||||||
}
|
}
|
||||||
|
|
||||||
final Isar db;
|
|
||||||
final AssetNotifier assetNotifier;
|
|
||||||
|
|
||||||
void _setArchiveForAssetId(int id, bool archive) {
|
|
||||||
if (!archive) {
|
|
||||||
state = state.difference({id});
|
|
||||||
} else {
|
|
||||||
state = state.union({id});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _isArchive(int id) {
|
|
||||||
return state.contains(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> toggleArchive(Asset asset) async {
|
|
||||||
if (asset.storage == AssetState.local) return;
|
|
||||||
|
|
||||||
_setArchiveForAssetId(asset.id, !_isArchive(asset.id));
|
|
||||||
|
|
||||||
await assetNotifier.toggleArchive(
|
|
||||||
[asset],
|
|
||||||
state.contains(asset.id),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> addToArchives(Iterable<Asset> assets) {
|
|
||||||
state = state.union(assets.map((a) => a.id).toSet());
|
|
||||||
return assetNotifier.toggleArchive(assets, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final archiveProvider =
|
|
||||||
StateNotifierProvider<ArchiveSelectionNotifier, Set<int>>((ref) {
|
|
||||||
return ArchiveSelectionNotifier(
|
|
||||||
ref.watch(dbProvider),
|
|
||||||
ref.watch(assetProvider.notifier),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
@ -1,46 +1,25 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.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_hooks/flutter_hooks.dart' hide Store;
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/archive/providers/archive_asset_provider.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
|
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/models/store.dart';
|
|
||||||
import 'package:immich_mobile/shared/models/user.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/db.provider.dart';
|
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
||||||
import 'package:isar/isar.dart';
|
|
||||||
|
|
||||||
class ArchivePage extends HookConsumerWidget {
|
class ArchivePage extends HookConsumerWidget {
|
||||||
const ArchivePage({super.key});
|
const ArchivePage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final User me = Store.get(StoreKey.currentUser);
|
final archivedAssets = ref.watch(archiveProvider);
|
||||||
final query = ref
|
|
||||||
.watch(dbProvider)
|
|
||||||
.assets
|
|
||||||
.filter()
|
|
||||||
.ownerIdEqualTo(me.isarId)
|
|
||||||
.isArchivedEqualTo(true);
|
|
||||||
final stream = query.watch();
|
|
||||||
final archivedAssets = useState<List<Asset>>([]);
|
|
||||||
final selectionEnabledHook = useState(false);
|
final selectionEnabledHook = useState(false);
|
||||||
final selection = useState(<Asset>{});
|
final selection = useState(<Asset>{});
|
||||||
|
final processing = useState(false);
|
||||||
useEffect(
|
|
||||||
() {
|
|
||||||
query.findAll().then((value) => archivedAssets.value = value);
|
|
||||||
final subscription = stream.listen((e) {
|
|
||||||
archivedAssets.value = e;
|
|
||||||
});
|
|
||||||
// Cancel the subscription when the widget is disposed
|
|
||||||
return subscription.cancel;
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
void selectionListener(
|
void selectionListener(
|
||||||
bool multiselect,
|
bool multiselect,
|
||||||
@ -50,7 +29,7 @@ class ArchivePage extends HookConsumerWidget {
|
|||||||
selection.value = selectedAssets;
|
selection.value = selectedAssets;
|
||||||
}
|
}
|
||||||
|
|
||||||
AppBar buildAppBar() {
|
AppBar buildAppBar(String count) {
|
||||||
return AppBar(
|
return AppBar(
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
onPressed: () => AutoRouter.of(context).pop(),
|
onPressed: () => AutoRouter.of(context).pop(),
|
||||||
@ -60,7 +39,7 @@ class ArchivePage extends HookConsumerWidget {
|
|||||||
automaticallyImplyLeading: false,
|
automaticallyImplyLeading: false,
|
||||||
title: const Text(
|
title: const Text(
|
||||||
'archive_page_title',
|
'archive_page_title',
|
||||||
).tr(args: [archivedAssets.value.length.toString()]),
|
).tr(args: [count]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,24 +63,34 @@ class ArchivePage extends HookConsumerWidget {
|
|||||||
'control_bottom_app_bar_unarchive'.tr(),
|
'control_bottom_app_bar_unarchive'.tr(),
|
||||||
style: const TextStyle(fontSize: 14),
|
style: const TextStyle(fontSize: 14),
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: processing.value
|
||||||
if (selection.value.isNotEmpty) {
|
? null
|
||||||
ref
|
: () async {
|
||||||
.watch(assetProvider.notifier)
|
processing.value = true;
|
||||||
.toggleArchive(selection.value, false);
|
try {
|
||||||
|
if (selection.value.isNotEmpty) {
|
||||||
|
await ref
|
||||||
|
.watch(assetProvider.notifier)
|
||||||
|
.toggleArchive(
|
||||||
|
selection.value.toList(),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
final assetOrAssets =
|
final assetOrAssets = selection.value.length > 1
|
||||||
selection.value.length > 1 ? 'assets' : 'asset';
|
? 'assets'
|
||||||
ImmichToast.show(
|
: 'asset';
|
||||||
context: context,
|
ImmichToast.show(
|
||||||
msg:
|
context: context,
|
||||||
'Moved ${selection.value.length} $assetOrAssets to library',
|
msg:
|
||||||
gravity: ToastGravity.CENTER,
|
'Moved ${selection.value.length} $assetOrAssets to library',
|
||||||
);
|
gravity: ToastGravity.CENTER,
|
||||||
}
|
);
|
||||||
|
}
|
||||||
selectionEnabledHook.value = false;
|
} finally {
|
||||||
},
|
processing.value = false;
|
||||||
|
selectionEnabledHook.value = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -111,22 +100,34 @@ class ArchivePage extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return archivedAssets.when(
|
||||||
appBar: buildAppBar(),
|
loading: () => Scaffold(
|
||||||
body: archivedAssets.value.isEmpty
|
appBar: buildAppBar("?"),
|
||||||
? Center(
|
body: const Center(child: CircularProgressIndicator()),
|
||||||
child: Text('archive_page_no_archived_assets'.tr()),
|
),
|
||||||
)
|
error: (error, stackTrace) => Scaffold(
|
||||||
: Stack(
|
appBar: buildAppBar("Error"),
|
||||||
children: [
|
body: Center(child: Text(error.toString())),
|
||||||
ImmichAssetGrid(
|
),
|
||||||
assets: archivedAssets.value,
|
data: (data) => Scaffold(
|
||||||
listener: selectionListener,
|
appBar: buildAppBar(data.totalAssets.toString()),
|
||||||
selectionActive: selectionEnabledHook.value,
|
body: data.isEmpty
|
||||||
),
|
? Center(
|
||||||
if (selectionEnabledHook.value) buildBottomBar()
|
child: Text('archive_page_no_archived_assets'.tr()),
|
||||||
],
|
)
|
||||||
),
|
: Stack(
|
||||||
|
children: [
|
||||||
|
ImmichAssetGrid(
|
||||||
|
renderList: data,
|
||||||
|
listener: selectionListener,
|
||||||
|
selectionActive: selectionEnabledHook.value,
|
||||||
|
),
|
||||||
|
if (selectionEnabledHook.value) buildBottomBar(),
|
||||||
|
if (processing.value)
|
||||||
|
const Center(child: ImmichLoadingIndicator())
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
class RequestDownloadAssetInfo {
|
|
||||||
final String assetId;
|
|
||||||
final String deviceId;
|
|
||||||
|
|
||||||
RequestDownloadAssetInfo(this.assetId, this.deviceId);
|
|
||||||
}
|
|
@ -4,14 +4,12 @@ import 'package:immich_mobile/modules/settings/providers/app_settings.provider.d
|
|||||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
|
|
||||||
final renderListProvider = FutureProvider.family<RenderList, List<Asset>>((ref, assets) {
|
final renderListProvider =
|
||||||
var settings = ref.watch(appSettingsServiceProvider);
|
FutureProvider.family<RenderList, List<Asset>>((ref, assets) {
|
||||||
|
final settings = ref.watch(appSettingsServiceProvider);
|
||||||
|
|
||||||
final layout = AssetGridLayoutParameters(
|
return RenderList.fromAssets(
|
||||||
settings.getSetting(AppSettingsEnum.tilesPerRow),
|
assets,
|
||||||
settings.getSetting(AppSettingsEnum.dynamicLayout),
|
|
||||||
GroupAssetsBy.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)],
|
GroupAssetsBy.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)],
|
||||||
);
|
);
|
||||||
|
|
||||||
return RenderList.fromAssets(assets, layout);
|
|
||||||
});
|
});
|
||||||
|
@ -21,7 +21,7 @@ class TopControlAppBar extends HookConsumerWidget {
|
|||||||
final VoidCallback? onDownloadPressed;
|
final VoidCallback? onDownloadPressed;
|
||||||
final VoidCallback onToggleMotionVideo;
|
final VoidCallback onToggleMotionVideo;
|
||||||
final VoidCallback onAddToAlbumPressed;
|
final VoidCallback onAddToAlbumPressed;
|
||||||
final VoidCallback onFavorite;
|
final VoidCallback? onFavorite;
|
||||||
final bool isPlayingMotionVideo;
|
final bool isPlayingMotionVideo;
|
||||||
final bool isFavorite;
|
final bool isFavorite;
|
||||||
|
|
||||||
@ -31,9 +31,7 @@ class TopControlAppBar extends HookConsumerWidget {
|
|||||||
|
|
||||||
Widget buildFavoriteButton() {
|
Widget buildFavoriteButton() {
|
||||||
return IconButton(
|
return IconButton(
|
||||||
onPressed: () {
|
onPressed: onFavorite,
|
||||||
onFavorite();
|
|
||||||
},
|
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
isFavorite ? Icons.favorite : Icons.favorite_border,
|
isFavorite ? Icons.favorite : Icons.favorite_border,
|
||||||
color: Colors.grey[200],
|
color: Colors.grey[200],
|
||||||
|
@ -12,9 +12,7 @@ import 'package:immich_mobile/modules/asset_viewer/ui/advanced_bottom_sheet.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/top_control_app_bar.dart';
|
import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart';
|
import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart';
|
||||||
import 'package:immich_mobile/modules/favorite/providers/favorite_provider.dart';
|
|
||||||
import 'package:immich_mobile/shared/models/store.dart';
|
import 'package:immich_mobile/shared/models/store.dart';
|
||||||
import 'package:immich_mobile/shared/services/asset.service.dart';
|
|
||||||
import 'package:immich_mobile/modules/home/ui/delete_dialog.dart';
|
import 'package:immich_mobile/modules/home/ui/delete_dialog.dart';
|
||||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.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/modules/settings/services/app_settings.service.dart';
|
||||||
@ -32,16 +30,16 @@ import 'package:openapi/api.dart' as api;
|
|||||||
|
|
||||||
// ignore: must_be_immutable
|
// ignore: must_be_immutable
|
||||||
class GalleryViewerPage extends HookConsumerWidget {
|
class GalleryViewerPage extends HookConsumerWidget {
|
||||||
final List<Asset> assetList;
|
final Asset Function(int index) loadAsset;
|
||||||
final Asset asset;
|
final int totalAssets;
|
||||||
|
final int initialIndex;
|
||||||
|
|
||||||
GalleryViewerPage({
|
GalleryViewerPage({
|
||||||
super.key,
|
super.key,
|
||||||
required this.assetList,
|
required this.initialIndex,
|
||||||
required this.asset,
|
required this.loadAsset,
|
||||||
}) : controller = PageController(initialPage: assetList.indexOf(asset));
|
required this.totalAssets,
|
||||||
|
}) : controller = PageController(initialPage: initialIndex);
|
||||||
Asset? assetDetail;
|
|
||||||
|
|
||||||
final PageController controller;
|
final PageController controller;
|
||||||
|
|
||||||
@ -52,11 +50,15 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
final isLoadOriginal = useState(AppSettingsEnum.loadOriginal.defaultValue);
|
final isLoadOriginal = useState(AppSettingsEnum.loadOriginal.defaultValue);
|
||||||
final isZoomed = useState<bool>(false);
|
final isZoomed = useState<bool>(false);
|
||||||
final showAppBar = useState<bool>(true);
|
final showAppBar = useState<bool>(true);
|
||||||
final indexOfAsset = useState(assetList.indexOf(asset));
|
|
||||||
final isPlayingMotionVideo = useState(false);
|
final isPlayingMotionVideo = useState(false);
|
||||||
final isPlayingVideo = useState(false);
|
final isPlayingVideo = useState(false);
|
||||||
late Offset localPosition;
|
late Offset localPosition;
|
||||||
final authToken = 'Bearer ${Store.get(StoreKey.accessToken)}';
|
final authToken = 'Bearer ${Store.get(StoreKey.accessToken)}';
|
||||||
|
final currentIndex = useState(initialIndex);
|
||||||
|
final currentAsset = loadAsset(currentIndex.value);
|
||||||
|
final watchedAsset = ref.watch(assetDetailProvider(currentAsset));
|
||||||
|
|
||||||
|
Asset asset() => watchedAsset.value ?? currentAsset;
|
||||||
|
|
||||||
showAppBar.addListener(() {
|
showAppBar.addListener(() {
|
||||||
// Change to and from immersive mode, hiding navigation and app bar
|
// Change to and from immersive mode, hiding navigation and app bar
|
||||||
@ -79,16 +81,9 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
void toggleFavorite(Asset asset) {
|
void toggleFavorite(Asset asset) => ref
|
||||||
ref.watch(favoriteProvider.notifier).toggleFavorite(asset);
|
.watch(assetProvider.notifier)
|
||||||
}
|
.toggleFavorite([asset], !asset.isFavorite);
|
||||||
|
|
||||||
void getAssetExif() async {
|
|
||||||
assetDetail = assetList[indexOfAsset.value];
|
|
||||||
assetDetail = await ref
|
|
||||||
.watch(assetServiceProvider)
|
|
||||||
.loadExif(assetList[indexOfAsset.value]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Thumbnail image of a remote asset. Required asset.isRemote
|
/// Thumbnail image of a remote asset. Required asset.isRemote
|
||||||
ImageProvider remoteThumbnailImageProvider(
|
ImageProvider remoteThumbnailImageProvider(
|
||||||
@ -138,8 +133,8 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void precacheNextImage(int index) {
|
void precacheNextImage(int index) {
|
||||||
if (index < assetList.length && index >= 0) {
|
if (index < totalAssets && index >= 0) {
|
||||||
final asset = assetList[index];
|
final asset = loadAsset(index);
|
||||||
|
|
||||||
if (asset.isLocal) {
|
if (asset.isLocal) {
|
||||||
// Preload the local asset
|
// Preload the local asset
|
||||||
@ -193,13 +188,13 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
if (ref
|
if (ref
|
||||||
.watch(appSettingsServiceProvider)
|
.watch(appSettingsServiceProvider)
|
||||||
.getSetting<bool>(AppSettingsEnum.advancedTroubleshooting)) {
|
.getSetting<bool>(AppSettingsEnum.advancedTroubleshooting)) {
|
||||||
return AdvancedBottomSheet(assetDetail: assetDetail!);
|
return AdvancedBottomSheet(assetDetail: asset());
|
||||||
}
|
}
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||||
),
|
),
|
||||||
child: ExifBottomSheet(asset: assetDetail!),
|
child: ExifBottomSheet(asset: asset()),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -211,7 +206,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
builder: (BuildContext _) {
|
builder: (BuildContext _) {
|
||||||
return DeleteDialog(
|
return DeleteDialog(
|
||||||
onDelete: () {
|
onDelete: () {
|
||||||
if (assetList.length == 1) {
|
if (totalAssets == 1) {
|
||||||
// Handle only one asset
|
// Handle only one asset
|
||||||
AutoRouter.of(context).pop();
|
AutoRouter.of(context).pop();
|
||||||
} else {
|
} else {
|
||||||
@ -221,7 +216,6 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
curve: Curves.fastLinearToSlowEaseIn,
|
curve: Curves.fastLinearToSlowEaseIn,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
assetList.remove(deleteAsset);
|
|
||||||
ref.watch(assetProvider.notifier).deleteAssets({deleteAsset});
|
ref.watch(assetProvider.notifier).deleteAssets({deleteAsset});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -267,9 +261,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
shareAsset() {
|
shareAsset() {
|
||||||
ref
|
ref.watch(imageViewerStateProvider.notifier).shareAsset(asset(), context);
|
||||||
.watch(imageViewerStateProvider.notifier)
|
|
||||||
.shareAsset(assetList[indexOfAsset.value], context);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleArchive(Asset asset) {
|
handleArchive(Asset asset) {
|
||||||
@ -291,30 +283,21 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
color: Colors.black.withOpacity(0.4),
|
color: Colors.black.withOpacity(0.4),
|
||||||
child: TopControlAppBar(
|
child: TopControlAppBar(
|
||||||
isPlayingMotionVideo: isPlayingMotionVideo.value,
|
isPlayingMotionVideo: isPlayingMotionVideo.value,
|
||||||
asset: assetList[indexOfAsset.value],
|
asset: asset(),
|
||||||
isFavorite: ref.watch(favoriteProvider).contains(
|
isFavorite: asset().isFavorite,
|
||||||
assetList[indexOfAsset.value].id,
|
onMoreInfoPressed: showInfo,
|
||||||
),
|
onFavorite: asset().isRemote ? () => toggleFavorite(asset()) : null,
|
||||||
onMoreInfoPressed: () {
|
onDownloadPressed: asset().storage == AssetState.local
|
||||||
showInfo();
|
|
||||||
},
|
|
||||||
onFavorite: () {
|
|
||||||
toggleFavorite(assetList[indexOfAsset.value]);
|
|
||||||
},
|
|
||||||
onDownloadPressed: assetList[indexOfAsset.value].storage ==
|
|
||||||
AssetState.local
|
|
||||||
? null
|
? null
|
||||||
: () {
|
: () =>
|
||||||
ref.watch(imageViewerStateProvider.notifier).downloadAsset(
|
ref.watch(imageViewerStateProvider.notifier).downloadAsset(
|
||||||
assetList[indexOfAsset.value],
|
asset(),
|
||||||
context,
|
context,
|
||||||
);
|
),
|
||||||
},
|
|
||||||
onToggleMotionVideo: (() {
|
onToggleMotionVideo: (() {
|
||||||
isPlayingMotionVideo.value = !isPlayingMotionVideo.value;
|
isPlayingMotionVideo.value = !isPlayingMotionVideo.value;
|
||||||
}),
|
}),
|
||||||
onAddToAlbumPressed: () =>
|
onAddToAlbumPressed: () => addToAlbum(asset()),
|
||||||
addToAlbum(assetList[indexOfAsset.value]),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -324,8 +307,6 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
final show = (showAppBar.value || // onTap has the final say
|
final show = (showAppBar.value || // onTap has the final say
|
||||||
(showAppBar.value && !isZoomed.value)) &&
|
(showAppBar.value && !isZoomed.value)) &&
|
||||||
!isPlayingVideo.value;
|
!isPlayingVideo.value;
|
||||||
final currentAsset = assetList[indexOfAsset.value];
|
|
||||||
|
|
||||||
return AnimatedOpacity(
|
return AnimatedOpacity(
|
||||||
duration: const Duration(milliseconds: 100),
|
duration: const Duration(milliseconds: 100),
|
||||||
opacity: show ? 1.0 : 0.0,
|
opacity: show ? 1.0 : 0.0,
|
||||||
@ -343,7 +324,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
label: 'control_bottom_app_bar_share'.tr(),
|
label: 'control_bottom_app_bar_share'.tr(),
|
||||||
tooltip: 'control_bottom_app_bar_share'.tr(),
|
tooltip: 'control_bottom_app_bar_share'.tr(),
|
||||||
),
|
),
|
||||||
currentAsset.isArchived
|
asset().isArchived
|
||||||
? BottomNavigationBarItem(
|
? BottomNavigationBarItem(
|
||||||
icon: const Icon(Icons.unarchive_rounded),
|
icon: const Icon(Icons.unarchive_rounded),
|
||||||
label: 'control_bottom_app_bar_unarchive'.tr(),
|
label: 'control_bottom_app_bar_unarchive'.tr(),
|
||||||
@ -366,10 +347,10 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
shareAsset();
|
shareAsset();
|
||||||
break;
|
break;
|
||||||
case 1:
|
case 1:
|
||||||
handleArchive(assetList[indexOfAsset.value]);
|
handleArchive(asset());
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
handleDelete(assetList[indexOfAsset.value]);
|
handleDelete(asset());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -399,33 +380,33 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
? const ScrollPhysics() // Use bouncing physics for iOS
|
? const ScrollPhysics() // Use bouncing physics for iOS
|
||||||
: const ClampingScrollPhysics() // Use heavy physics for Android
|
: const ClampingScrollPhysics() // Use heavy physics for Android
|
||||||
),
|
),
|
||||||
itemCount: assetList.length,
|
itemCount: totalAssets,
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
onPageChanged: (value) {
|
onPageChanged: (value) {
|
||||||
// Precache image
|
// Precache image
|
||||||
if (indexOfAsset.value < value) {
|
if (currentIndex.value < value) {
|
||||||
// Moving forwards, so precache the next asset
|
// Moving forwards, so precache the next asset
|
||||||
precacheNextImage(value + 1);
|
precacheNextImage(value + 1);
|
||||||
} else {
|
} else {
|
||||||
// Moving backwards, so precache previous asset
|
// Moving backwards, so precache previous asset
|
||||||
precacheNextImage(value - 1);
|
precacheNextImage(value - 1);
|
||||||
}
|
}
|
||||||
indexOfAsset.value = value;
|
currentIndex.value = value;
|
||||||
HapticFeedback.selectionClick();
|
HapticFeedback.selectionClick();
|
||||||
},
|
},
|
||||||
loadingBuilder: isLoadPreview.value
|
loadingBuilder: isLoadPreview.value
|
||||||
? (context, event) {
|
? (context, event) {
|
||||||
final asset = assetList[indexOfAsset.value];
|
final a = asset();
|
||||||
if (!asset.isLocal) {
|
if (!a.isLocal) {
|
||||||
// Use the WEBP Thumbnail as a placeholder for the JPEG thumbnail to achieve
|
// Use the WEBP Thumbnail as a placeholder for the JPEG thumbnail to achieve
|
||||||
// Three-Stage Loading (WEBP -> JPEG -> Original)
|
// Three-Stage Loading (WEBP -> JPEG -> Original)
|
||||||
final webPThumbnail = CachedNetworkImage(
|
final webPThumbnail = CachedNetworkImage(
|
||||||
imageUrl: getThumbnailUrl(
|
imageUrl: getThumbnailUrl(
|
||||||
asset,
|
a,
|
||||||
type: api.ThumbnailFormat.WEBP,
|
type: api.ThumbnailFormat.WEBP,
|
||||||
),
|
),
|
||||||
cacheKey: getThumbnailCacheKey(
|
cacheKey: getThumbnailCacheKey(
|
||||||
asset,
|
a,
|
||||||
type: api.ThumbnailFormat.WEBP,
|
type: api.ThumbnailFormat.WEBP,
|
||||||
),
|
),
|
||||||
httpHeaders: {'Authorization': authToken},
|
httpHeaders: {'Authorization': authToken},
|
||||||
@ -444,11 +425,11 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
// makes sense if the original is loaded in the builder
|
// makes sense if the original is loaded in the builder
|
||||||
return CachedNetworkImage(
|
return CachedNetworkImage(
|
||||||
imageUrl: getThumbnailUrl(
|
imageUrl: getThumbnailUrl(
|
||||||
asset,
|
a,
|
||||||
type: api.ThumbnailFormat.JPEG,
|
type: api.ThumbnailFormat.JPEG,
|
||||||
),
|
),
|
||||||
cacheKey: getThumbnailCacheKey(
|
cacheKey: getThumbnailCacheKey(
|
||||||
asset,
|
a,
|
||||||
type: api.ThumbnailFormat.JPEG,
|
type: api.ThumbnailFormat.JPEG,
|
||||||
),
|
),
|
||||||
httpHeaders: {'Authorization': authToken},
|
httpHeaders: {'Authorization': authToken},
|
||||||
@ -462,30 +443,30 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return Image(
|
return Image(
|
||||||
image: localThumbnailImageProvider(asset),
|
image: localThumbnailImageProvider(a),
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
builder: (context, index) {
|
builder: (context, index) {
|
||||||
getAssetExif();
|
final asset = loadAsset(index);
|
||||||
if (assetList[index].isImage && !isPlayingMotionVideo.value) {
|
if (asset.isImage && !isPlayingMotionVideo.value) {
|
||||||
// Show photo
|
// Show photo
|
||||||
final ImageProvider provider;
|
final ImageProvider provider;
|
||||||
if (assetList[index].isLocal) {
|
if (asset.isLocal) {
|
||||||
provider = localImageProvider(assetList[index]);
|
provider = localImageProvider(asset);
|
||||||
} else {
|
} else {
|
||||||
if (isLoadOriginal.value) {
|
if (isLoadOriginal.value) {
|
||||||
provider = originalImageProvider(assetList[index]);
|
provider = originalImageProvider(asset);
|
||||||
} else if (isLoadPreview.value) {
|
} else if (isLoadPreview.value) {
|
||||||
provider = remoteThumbnailImageProvider(
|
provider = remoteThumbnailImageProvider(
|
||||||
assetList[index],
|
asset,
|
||||||
api.ThumbnailFormat.JPEG,
|
api.ThumbnailFormat.JPEG,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
provider = remoteThumbnailImageProvider(
|
provider = remoteThumbnailImageProvider(
|
||||||
assetList[index],
|
asset,
|
||||||
api.ThumbnailFormat.WEBP,
|
api.ThumbnailFormat.WEBP,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -499,13 +480,13 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
showAppBar.value = !showAppBar.value,
|
showAppBar.value = !showAppBar.value,
|
||||||
imageProvider: provider,
|
imageProvider: provider,
|
||||||
heroAttributes: PhotoViewHeroAttributes(
|
heroAttributes: PhotoViewHeroAttributes(
|
||||||
tag: assetList[index].id,
|
tag: asset.id,
|
||||||
),
|
),
|
||||||
filterQuality: FilterQuality.high,
|
filterQuality: FilterQuality.high,
|
||||||
tightMode: true,
|
tightMode: true,
|
||||||
minScale: PhotoViewComputedScale.contained,
|
minScale: PhotoViewComputedScale.contained,
|
||||||
errorBuilder: (context, error, stackTrace) => ImmichImage(
|
errorBuilder: (context, error, stackTrace) => ImmichImage(
|
||||||
assetList[indexOfAsset.value],
|
asset,
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -516,7 +497,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
onDragUpdate: (_, details, __) =>
|
onDragUpdate: (_, details, __) =>
|
||||||
handleSwipeUpDown(details),
|
handleSwipeUpDown(details),
|
||||||
heroAttributes: PhotoViewHeroAttributes(
|
heroAttributes: PhotoViewHeroAttributes(
|
||||||
tag: assetList[index].id,
|
tag: asset.id,
|
||||||
),
|
),
|
||||||
filterQuality: FilterQuality.high,
|
filterQuality: FilterQuality.high,
|
||||||
maxScale: 1.0,
|
maxScale: 1.0,
|
||||||
@ -526,7 +507,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
child: VideoViewerPage(
|
child: VideoViewerPage(
|
||||||
onPlaying: () => isPlayingVideo.value = true,
|
onPlaying: () => isPlayingVideo.value = true,
|
||||||
onPaused: () => isPlayingVideo.value = false,
|
onPaused: () => isPlayingVideo.value = false,
|
||||||
asset: assetList[index],
|
asset: asset,
|
||||||
isMotionVideo: isPlayingMotionVideo.value,
|
isMotionVideo: isPlayingMotionVideo.value,
|
||||||
onVideoEnded: () {
|
onVideoEnded: () {
|
||||||
if (isPlayingMotionVideo.value) {
|
if (isPlayingMotionVideo.value) {
|
||||||
|
@ -1,68 +1,25 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.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/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
import 'package:immich_mobile/shared/models/store.dart';
|
||||||
|
import 'package:immich_mobile/shared/providers/db.provider.dart';
|
||||||
|
import 'package:isar/isar.dart';
|
||||||
|
|
||||||
class FavoriteSelectionNotifier extends StateNotifier<Set<int>> {
|
final favoriteAssetsProvider = StreamProvider<RenderList>((ref) async* {
|
||||||
FavoriteSelectionNotifier(this.assetsState, this.assetNotifier) : super({}) {
|
final query = ref
|
||||||
state = assetsState.allAssets
|
.watch(dbProvider)
|
||||||
.where((asset) => asset.isFavorite)
|
.assets
|
||||||
.map((asset) => asset.id)
|
.filter()
|
||||||
.toSet();
|
.ownerIdEqualTo(Store.get(StoreKey.currentUser).isarId)
|
||||||
|
.isFavoriteEqualTo(true)
|
||||||
|
.sortByFileCreatedAt();
|
||||||
|
final settings = ref.watch(appSettingsServiceProvider);
|
||||||
|
final groupBy =
|
||||||
|
GroupAssetsBy.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)];
|
||||||
|
yield await RenderList.fromQuery(query, groupBy);
|
||||||
|
await for (final _ in query.watchLazy()) {
|
||||||
|
yield await RenderList.fromQuery(query, groupBy);
|
||||||
}
|
}
|
||||||
|
|
||||||
final AssetsState assetsState;
|
|
||||||
final AssetNotifier assetNotifier;
|
|
||||||
|
|
||||||
void _setFavoriteForAssetId(int id, bool favorite) {
|
|
||||||
if (!favorite) {
|
|
||||||
state = state.difference({id});
|
|
||||||
} else {
|
|
||||||
state = state.union({id});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _isFavorite(int id) {
|
|
||||||
return state.contains(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> toggleFavorite(Asset asset) async {
|
|
||||||
// TODO support local favorite assets
|
|
||||||
if (asset.storage == AssetState.local) return;
|
|
||||||
_setFavoriteForAssetId(asset.id, !_isFavorite(asset.id));
|
|
||||||
|
|
||||||
await assetNotifier.toggleFavorite(
|
|
||||||
asset,
|
|
||||||
state.contains(asset.id),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> addToFavorites(Iterable<Asset> assets) {
|
|
||||||
state = state.union(assets.map((a) => a.id).toSet());
|
|
||||||
final futures = assets.map(
|
|
||||||
(a) => assetNotifier.toggleFavorite(
|
|
||||||
a,
|
|
||||||
true,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
return Future.wait(futures);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final favoriteProvider =
|
|
||||||
StateNotifierProvider<FavoriteSelectionNotifier, Set<int>>((ref) {
|
|
||||||
return FavoriteSelectionNotifier(
|
|
||||||
ref.watch(assetProvider),
|
|
||||||
ref.watch(assetProvider.notifier),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
final favoriteAssetProvider = StateProvider((ref) {
|
|
||||||
final favorites = ref.watch(favoriteProvider);
|
|
||||||
|
|
||||||
return ref
|
|
||||||
.watch(assetProvider)
|
|
||||||
.allAssets
|
|
||||||
.where((element) => favorites.contains(element.id))
|
|
||||||
.toList();
|
|
||||||
});
|
});
|
||||||
|
@ -1,36 +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/routing/router.dart';
|
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
|
||||||
import 'package:immich_mobile/shared/ui/immich_image.dart';
|
|
||||||
|
|
||||||
class FavoriteImage extends HookConsumerWidget {
|
|
||||||
final Asset asset;
|
|
||||||
final List<Asset> assets;
|
|
||||||
|
|
||||||
const FavoriteImage(this.asset, this.assets, {super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
void viewAsset() {
|
|
||||||
AutoRouter.of(context).push(
|
|
||||||
GalleryViewerRoute(
|
|
||||||
asset: asset,
|
|
||||||
assetList: assets,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return GestureDetector(
|
|
||||||
onTap: viewAsset,
|
|
||||||
child: ImmichImage(
|
|
||||||
asset,
|
|
||||||
width: 300,
|
|
||||||
height: 300,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,15 +1,32 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.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_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/favorite/providers/favorite_provider.dart';
|
import 'package:immich_mobile/modules/favorite/providers/favorite_provider.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
|
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
|
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
||||||
|
|
||||||
class FavoritesPage extends HookConsumerWidget {
|
class FavoritesPage extends HookConsumerWidget {
|
||||||
const FavoritesPage({Key? key}) : super(key: key);
|
const FavoritesPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final selectionEnabledHook = useState(false);
|
||||||
|
final selection = useState(<Asset>{});
|
||||||
|
final processing = useState(false);
|
||||||
|
|
||||||
|
void selectionListener(
|
||||||
|
bool multiselect,
|
||||||
|
Set<Asset> selectedAssets,
|
||||||
|
) {
|
||||||
|
selectionEnabledHook.value = multiselect;
|
||||||
|
selection.value = selectedAssets;
|
||||||
|
}
|
||||||
|
|
||||||
AppBar buildAppBar() {
|
AppBar buildAppBar() {
|
||||||
return AppBar(
|
return AppBar(
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
@ -24,15 +41,77 @@ class FavoritesPage extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void unfavorite() async {
|
||||||
|
try {
|
||||||
|
if (selection.value.isNotEmpty) {
|
||||||
|
await ref.watch(assetProvider.notifier).toggleFavorite(
|
||||||
|
selection.value.toList(),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
final assetOrAssets = selection.value.length > 1 ? 'assets' : 'asset';
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg:
|
||||||
|
'Removed ${selection.value.length} $assetOrAssets from favorites',
|
||||||
|
gravity: ToastGravity.CENTER,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
processing.value = false;
|
||||||
|
selectionEnabledHook.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildBottomBar() {
|
||||||
|
return SafeArea(
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: SizedBox(
|
||||||
|
height: 64,
|
||||||
|
child: Card(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
leading: const Icon(
|
||||||
|
Icons.star_border,
|
||||||
|
),
|
||||||
|
title: const Text(
|
||||||
|
"Unfavorite",
|
||||||
|
style: TextStyle(fontSize: 14),
|
||||||
|
),
|
||||||
|
onTap: processing.value ? null : unfavorite,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: buildAppBar(),
|
appBar: buildAppBar(),
|
||||||
body: ref.watch(favoriteAssetProvider).isEmpty
|
body: ref.watch(favoriteAssetsProvider).when(
|
||||||
? Center(
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
child: Text('favorites_page_no_favorites'.tr()),
|
error: (error, stackTrace) => Center(child: Text(error.toString())),
|
||||||
)
|
data: (data) => data.isEmpty
|
||||||
: ImmichAssetGrid(
|
? Center(
|
||||||
assets: ref.watch(favoriteAssetProvider),
|
child: Text('favorites_page_no_favorites'.tr()),
|
||||||
),
|
)
|
||||||
|
: Stack(
|
||||||
|
children: [
|
||||||
|
ImmichAssetGrid(
|
||||||
|
renderList: data,
|
||||||
|
selectionActive: selectionEnabledHook.value,
|
||||||
|
listener: selectionListener,
|
||||||
|
),
|
||||||
|
if (selectionEnabledHook.value) buildBottomBar()
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,212 +2,313 @@ import 'dart:math';
|
|||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
|
import 'package:isar/isar.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
final log = Logger('AssetGridDataStructure');
|
final log = Logger('AssetGridDataStructure');
|
||||||
|
|
||||||
enum RenderAssetGridElementType {
|
enum RenderAssetGridElementType {
|
||||||
|
assets,
|
||||||
assetRow,
|
assetRow,
|
||||||
groupDividerTitle,
|
groupDividerTitle,
|
||||||
monthTitle;
|
monthTitle;
|
||||||
}
|
}
|
||||||
|
|
||||||
class RenderAssetGridRow {
|
|
||||||
final List<Asset> assets;
|
|
||||||
final List<double> widthDistribution;
|
|
||||||
|
|
||||||
RenderAssetGridRow(this.assets, this.widthDistribution);
|
|
||||||
}
|
|
||||||
|
|
||||||
class RenderAssetGridElement {
|
class RenderAssetGridElement {
|
||||||
final RenderAssetGridElementType type;
|
final RenderAssetGridElementType type;
|
||||||
final RenderAssetGridRow? assetRow;
|
|
||||||
final String? title;
|
final String? title;
|
||||||
final DateTime date;
|
final DateTime date;
|
||||||
final List<Asset>? relatedAssetList;
|
final int count;
|
||||||
|
final int offset;
|
||||||
|
final int totalCount;
|
||||||
|
|
||||||
RenderAssetGridElement(
|
RenderAssetGridElement(
|
||||||
this.type, {
|
this.type, {
|
||||||
this.assetRow,
|
|
||||||
this.title,
|
this.title,
|
||||||
required this.date,
|
required this.date,
|
||||||
this.relatedAssetList,
|
this.count = 0,
|
||||||
|
this.offset = 0,
|
||||||
|
this.totalCount = 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
enum GroupAssetsBy {
|
enum GroupAssetsBy {
|
||||||
day,
|
day,
|
||||||
month;
|
month,
|
||||||
}
|
auto,
|
||||||
|
none,
|
||||||
class AssetGridLayoutParameters {
|
;
|
||||||
final int perRow;
|
|
||||||
final bool dynamicLayout;
|
|
||||||
final GroupAssetsBy groupBy;
|
|
||||||
|
|
||||||
AssetGridLayoutParameters(
|
|
||||||
this.perRow,
|
|
||||||
this.dynamicLayout,
|
|
||||||
this.groupBy,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AssetGroupsToRenderListComputeParameters {
|
|
||||||
final List<Asset> assets;
|
|
||||||
final AssetGridLayoutParameters layout;
|
|
||||||
|
|
||||||
_AssetGroupsToRenderListComputeParameters(
|
|
||||||
this.assets,
|
|
||||||
this.layout,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class RenderList {
|
class RenderList {
|
||||||
final List<RenderAssetGridElement> elements;
|
final List<RenderAssetGridElement> elements;
|
||||||
|
final List<Asset>? allAssets;
|
||||||
|
final QueryBuilder<Asset, Asset, QAfterSortBy>? query;
|
||||||
|
final int totalAssets;
|
||||||
|
|
||||||
RenderList(this.elements);
|
/// reference to batch of assets loaded from DB with offset [_bufOffset]
|
||||||
|
List<Asset> _buf = [];
|
||||||
|
|
||||||
static Map<DateTime, List<Asset>> _groupAssets(
|
/// global offset of assets in [_buf]
|
||||||
List<Asset> assets,
|
int _bufOffset = 0;
|
||||||
GroupAssetsBy groupBy,
|
|
||||||
) {
|
RenderList(this.elements, this.query, this.allAssets)
|
||||||
if (groupBy == GroupAssetsBy.day) {
|
: totalAssets = allAssets?.length ?? query!.countSync();
|
||||||
return assets.groupListsBy(
|
|
||||||
(element) {
|
bool get isEmpty => totalAssets == 0;
|
||||||
final date = element.fileCreatedAt.toLocal();
|
|
||||||
return DateTime(date.year, date.month, date.day);
|
/// Loads the requested assets from the database to an internal buffer if not cached
|
||||||
},
|
/// and returns a slice of that buffer
|
||||||
);
|
List<Asset> loadAssets(int offset, int count) {
|
||||||
} else if (groupBy == GroupAssetsBy.month) {
|
assert(offset >= 0);
|
||||||
return assets.groupListsBy(
|
assert(count > 0);
|
||||||
(element) {
|
assert(offset + count <= totalAssets);
|
||||||
final date = element.fileCreatedAt.toLocal();
|
if (allAssets != null) {
|
||||||
return DateTime(date.year, date.month);
|
// if we already loaded all assets (e.g. from search result)
|
||||||
},
|
// simply return the requested slice of that array
|
||||||
);
|
return allAssets!.slice(offset, offset + count);
|
||||||
|
} else if (query != null) {
|
||||||
|
// general case: we have the query to load assets via offset from the DB on demand
|
||||||
|
if (offset < _bufOffset || offset + count > _bufOffset + _buf.length) {
|
||||||
|
// the requested slice (offset:offset+count) is not contained in the cache buffer `_buf`
|
||||||
|
// thus, fill the buffer with a new batch of assets that at least contains the requested
|
||||||
|
// assets and some more
|
||||||
|
|
||||||
|
final bool forward = _bufOffset < offset;
|
||||||
|
// if the requested offset is greater than the cached offset, the user scrolls forward "down"
|
||||||
|
const batchSize = 256;
|
||||||
|
const oppositeSize = 64;
|
||||||
|
|
||||||
|
// make sure to load a meaningful amount of data (and not only the requested slice)
|
||||||
|
// otherwise, each call to [loadAssets] would result in DB call trashing performance
|
||||||
|
// fills small requests to [batchSize], adds some legroom into the opposite scroll direction for large requests
|
||||||
|
final len = max(batchSize, count + oppositeSize);
|
||||||
|
// when scrolling forward, start shortly before the requested offset...
|
||||||
|
// when scrolling backward, end shortly after the requested offset...
|
||||||
|
// ... to guard against the user scrolling in the other direction
|
||||||
|
// a tiny bit resulting in a another required load from the DB
|
||||||
|
final start = max(
|
||||||
|
0,
|
||||||
|
forward
|
||||||
|
? offset - oppositeSize
|
||||||
|
: (len > batchSize ? offset : offset + count - len),
|
||||||
|
);
|
||||||
|
// load the calculated batch (start:start+len) from the DB and put it into the buffer
|
||||||
|
_buf = query!.offset(start).limit(len).findAllSync();
|
||||||
|
_bufOffset = start;
|
||||||
|
}
|
||||||
|
assert(_bufOffset <= offset);
|
||||||
|
assert(_bufOffset + _buf.length >= offset + count);
|
||||||
|
// return the requested slice from the buffer (we made sure before that the assets are loaded!)
|
||||||
|
return _buf.slice(offset - _bufOffset, offset - _bufOffset + count);
|
||||||
}
|
}
|
||||||
|
throw Exception("RenderList has neither assets nor query");
|
||||||
return {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<RenderList> _processAssetGroupData(
|
/// Returns the requested asset either from cached buffer or directly from the database
|
||||||
_AssetGroupsToRenderListComputeParameters data,
|
Asset loadAsset(int index) {
|
||||||
|
if (allAssets != null) {
|
||||||
|
// all assets are already loaded (e.g. from search result)
|
||||||
|
return allAssets![index];
|
||||||
|
} else if (query != null) {
|
||||||
|
// general case: we have the DB query to load asset(s) on demand
|
||||||
|
if (index >= _bufOffset && index < _bufOffset + _buf.length) {
|
||||||
|
// lucky case: the requested asset is already cached in the buffer!
|
||||||
|
return _buf[index - _bufOffset];
|
||||||
|
}
|
||||||
|
// request the asset from the database (not changing the buffer!)
|
||||||
|
final asset = query!.offset(index).findFirstSync();
|
||||||
|
if (asset == null) {
|
||||||
|
throw Exception(
|
||||||
|
"Asset at index $index does no longer exist in database",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return asset;
|
||||||
|
}
|
||||||
|
throw Exception("RenderList has neither assets nor query");
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<RenderList> fromQuery(
|
||||||
|
QueryBuilder<Asset, Asset, QAfterSortBy> query,
|
||||||
|
GroupAssetsBy groupBy,
|
||||||
|
) =>
|
||||||
|
_buildRenderList(null, query, groupBy);
|
||||||
|
|
||||||
|
static Future<RenderList> _buildRenderList(
|
||||||
|
List<Asset>? assets,
|
||||||
|
QueryBuilder<Asset, Asset, QAfterSortBy>? query,
|
||||||
|
GroupAssetsBy groupBy,
|
||||||
) async {
|
) async {
|
||||||
// TODO: Make DateFormat use the configured locale.
|
final List<RenderAssetGridElement> elements = [];
|
||||||
final monthFormat = DateFormat.yMMM();
|
|
||||||
final dayFormatSameYear = DateFormat.MMMEd();
|
|
||||||
final dayFormatOtherYear = DateFormat.yMMMEd();
|
|
||||||
final allAssets = data.assets;
|
|
||||||
final perRow = data.layout.perRow;
|
|
||||||
final dynamicLayout = data.layout.dynamicLayout;
|
|
||||||
final groupBy = data.layout.groupBy;
|
|
||||||
|
|
||||||
List<RenderAssetGridElement> elements = [];
|
const pageSize = 500;
|
||||||
DateTime? lastDate;
|
const sectionSize = 60; // divides evenly by 2,3,4,5,6
|
||||||
|
|
||||||
final groups = _groupAssets(allAssets, groupBy);
|
|
||||||
|
|
||||||
groups.entries.sortedBy((e) => e.key).reversed.forEach((entry) {
|
|
||||||
final date = entry.key;
|
|
||||||
final assets = entry.value;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Month title
|
|
||||||
if (groupBy == GroupAssetsBy.day &&
|
|
||||||
(lastDate == null || lastDate!.month != date.month)) {
|
|
||||||
elements.add(
|
|
||||||
RenderAssetGridElement(
|
|
||||||
RenderAssetGridElementType.monthTitle,
|
|
||||||
title: monthFormat.format(date),
|
|
||||||
date: date,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Group divider title (day or month)
|
|
||||||
var formatDate = dayFormatOtherYear;
|
|
||||||
|
|
||||||
if (DateTime.now().year == date.year) {
|
|
||||||
formatDate = dayFormatSameYear;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (groupBy == GroupAssetsBy.month) {
|
|
||||||
formatDate = monthFormat;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (groupBy == GroupAssetsBy.none) {
|
||||||
|
final int total = assets?.length ?? query!.countSync();
|
||||||
|
for (int i = 0; i < total; i += sectionSize) {
|
||||||
|
final date = assets != null
|
||||||
|
? assets[i].fileCreatedAt
|
||||||
|
: await query!.offset(i).fileCreatedAtProperty().findFirst();
|
||||||
|
final int count = i + sectionSize > total ? total - i : sectionSize;
|
||||||
|
if (date == null) break;
|
||||||
elements.add(
|
elements.add(
|
||||||
RenderAssetGridElement(
|
RenderAssetGridElement(
|
||||||
RenderAssetGridElementType.groupDividerTitle,
|
RenderAssetGridElementType.assets,
|
||||||
title: formatDate.format(date),
|
|
||||||
date: date,
|
date: date,
|
||||||
relatedAssetList: assets,
|
count: count,
|
||||||
|
totalCount: total,
|
||||||
|
offset: i,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add rows
|
|
||||||
int cursor = 0;
|
|
||||||
while (cursor < assets.length) {
|
|
||||||
int rowElements = min(assets.length - cursor, perRow);
|
|
||||||
final rowAssets = assets.sublist(cursor, cursor + rowElements);
|
|
||||||
|
|
||||||
// Default: All assets have the same width
|
|
||||||
var widthDistribution = List.filled(rowElements, 1.0);
|
|
||||||
|
|
||||||
if (dynamicLayout) {
|
|
||||||
final aspectRatios =
|
|
||||||
rowAssets.map((e) => (e.width ?? 1) / (e.height ?? 1)).toList();
|
|
||||||
final meanAspectRatio = aspectRatios.sum / rowElements;
|
|
||||||
|
|
||||||
// 1: mean width
|
|
||||||
// 0.5: width < mean - threshold
|
|
||||||
// 1.5: width > mean + threshold
|
|
||||||
final arConfiguration = aspectRatios.map((e) {
|
|
||||||
if (e - meanAspectRatio > 0.3) return 1.5;
|
|
||||||
if (e - meanAspectRatio < -0.3) return 0.5;
|
|
||||||
return 1.0;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Normalize:
|
|
||||||
final sum = arConfiguration.sum;
|
|
||||||
widthDistribution =
|
|
||||||
arConfiguration.map((e) => (e * rowElements) / sum).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
final rowElement = RenderAssetGridElement(
|
|
||||||
RenderAssetGridElementType.assetRow,
|
|
||||||
date: date,
|
|
||||||
assetRow: RenderAssetGridRow(
|
|
||||||
rowAssets,
|
|
||||||
widthDistribution,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
elements.add(rowElement);
|
|
||||||
cursor += rowElements;
|
|
||||||
}
|
|
||||||
|
|
||||||
lastDate = date;
|
|
||||||
} catch (e, stackTrace) {
|
|
||||||
log.severe(e, stackTrace);
|
|
||||||
}
|
}
|
||||||
});
|
return RenderList(elements, query, assets);
|
||||||
|
}
|
||||||
|
|
||||||
return RenderList(elements);
|
final formatSameYear =
|
||||||
|
groupBy == GroupAssetsBy.month ? DateFormat.MMMM() : DateFormat.MMMEd();
|
||||||
|
final formatOtherYear = groupBy == GroupAssetsBy.month
|
||||||
|
? DateFormat.yMMMM()
|
||||||
|
: DateFormat.yMMMEd();
|
||||||
|
final currentYear = DateTime.now().year;
|
||||||
|
final formatMergedSameYear = DateFormat.MMMd();
|
||||||
|
final formatMergedOtherYear = DateFormat.yMMMd();
|
||||||
|
|
||||||
|
int offset = 0;
|
||||||
|
DateTime? last;
|
||||||
|
DateTime? current;
|
||||||
|
int lastOffset = 0;
|
||||||
|
int count = 0;
|
||||||
|
int monthCount = 0;
|
||||||
|
int lastMonthIndex = 0;
|
||||||
|
|
||||||
|
String formatDateRange(DateTime from, DateTime to) {
|
||||||
|
final startDate = (from.year == currentYear
|
||||||
|
? formatMergedSameYear
|
||||||
|
: formatMergedOtherYear)
|
||||||
|
.format(from);
|
||||||
|
final endDate = (to.year == currentYear
|
||||||
|
? formatMergedSameYear
|
||||||
|
: formatMergedOtherYear)
|
||||||
|
.format(to);
|
||||||
|
if (DateTime(from.year, from.month, from.day) ==
|
||||||
|
DateTime(to.year, to.month, to.day)) {
|
||||||
|
// format range with time when both dates are on the same day
|
||||||
|
final startTime = DateFormat.Hm().format(from);
|
||||||
|
final endTime = DateFormat.Hm().format(to);
|
||||||
|
return "$startDate $startTime - $endTime";
|
||||||
|
}
|
||||||
|
return "$startDate - $endDate";
|
||||||
|
}
|
||||||
|
|
||||||
|
void mergeMonth() {
|
||||||
|
if (last != null &&
|
||||||
|
groupBy == GroupAssetsBy.auto &&
|
||||||
|
monthCount <= 30 &&
|
||||||
|
elements.length > lastMonthIndex + 1) {
|
||||||
|
// merge all days into a single section
|
||||||
|
assert(elements[lastMonthIndex].date.month == last.month);
|
||||||
|
final e = elements[lastMonthIndex];
|
||||||
|
|
||||||
|
elements[lastMonthIndex] = RenderAssetGridElement(
|
||||||
|
RenderAssetGridElementType.monthTitle,
|
||||||
|
date: e.date,
|
||||||
|
count: monthCount,
|
||||||
|
totalCount: monthCount,
|
||||||
|
offset: e.offset,
|
||||||
|
title: formatDateRange(e.date, elements.last.date),
|
||||||
|
);
|
||||||
|
elements.removeRange(lastMonthIndex + 1, elements.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void addElems(DateTime d, DateTime? prevDate) {
|
||||||
|
final bool newMonth =
|
||||||
|
last == null || last.year != d.year || last.month != d.month;
|
||||||
|
if (newMonth) {
|
||||||
|
mergeMonth();
|
||||||
|
lastMonthIndex = elements.length;
|
||||||
|
monthCount = 0;
|
||||||
|
}
|
||||||
|
for (int j = 0; j < count; j += sectionSize) {
|
||||||
|
final type = j == 0
|
||||||
|
? (groupBy != GroupAssetsBy.month && newMonth
|
||||||
|
? RenderAssetGridElementType.monthTitle
|
||||||
|
: RenderAssetGridElementType.groupDividerTitle)
|
||||||
|
: (groupBy == GroupAssetsBy.auto
|
||||||
|
? RenderAssetGridElementType.groupDividerTitle
|
||||||
|
: RenderAssetGridElementType.assets);
|
||||||
|
final sectionCount = j + sectionSize > count ? count - j : sectionSize;
|
||||||
|
assert(sectionCount > 0 && sectionCount <= sectionSize);
|
||||||
|
elements.add(
|
||||||
|
RenderAssetGridElement(
|
||||||
|
type,
|
||||||
|
date: d,
|
||||||
|
count: sectionCount,
|
||||||
|
totalCount: groupBy == GroupAssetsBy.auto ? sectionCount : count,
|
||||||
|
offset: lastOffset + j,
|
||||||
|
title: j == 0
|
||||||
|
? (d.year == currentYear
|
||||||
|
? formatSameYear.format(d)
|
||||||
|
: formatOtherYear.format(d))
|
||||||
|
: (groupBy == GroupAssetsBy.auto
|
||||||
|
? formatDateRange(d, prevDate ?? d)
|
||||||
|
: null),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
monthCount += count;
|
||||||
|
}
|
||||||
|
|
||||||
|
DateTime? prevDate;
|
||||||
|
while (true) {
|
||||||
|
// this iterates all assets (only their createdAt property) in batches
|
||||||
|
// memory usage is okay, however runtime is linear with number of assets
|
||||||
|
// TODO replace with groupBy once Isar supports such queries
|
||||||
|
final dates = assets != null
|
||||||
|
? assets.map((a) => a.fileCreatedAt)
|
||||||
|
: await query!
|
||||||
|
.offset(offset)
|
||||||
|
.limit(pageSize)
|
||||||
|
.fileCreatedAtProperty()
|
||||||
|
.findAll();
|
||||||
|
int i = 0;
|
||||||
|
for (final date in dates) {
|
||||||
|
final d = DateTime(
|
||||||
|
date.year,
|
||||||
|
date.month,
|
||||||
|
groupBy == GroupAssetsBy.month ? 1 : date.day,
|
||||||
|
);
|
||||||
|
current ??= d;
|
||||||
|
if (current != d) {
|
||||||
|
addElems(current, prevDate);
|
||||||
|
last = current;
|
||||||
|
current = d;
|
||||||
|
lastOffset = offset + i;
|
||||||
|
count = 0;
|
||||||
|
}
|
||||||
|
prevDate = date;
|
||||||
|
count++;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (assets != null || dates.length != pageSize) break;
|
||||||
|
offset += pageSize;
|
||||||
|
}
|
||||||
|
if (count > 0 && current != null) {
|
||||||
|
addElems(current, prevDate);
|
||||||
|
mergeMonth();
|
||||||
|
}
|
||||||
|
assert(elements.every((e) => e.count <= sectionSize), "too large section");
|
||||||
|
return RenderList(elements, query, assets);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static RenderList empty() => RenderList([], null, []);
|
||||||
|
|
||||||
static Future<RenderList> fromAssets(
|
static Future<RenderList> fromAssets(
|
||||||
List<Asset> assets,
|
List<Asset> assets,
|
||||||
AssetGridLayoutParameters layout,
|
GroupAssetsBy groupBy,
|
||||||
) async {
|
) =>
|
||||||
// Compute only allows for one parameter. Therefore we pass all parameters in a map
|
_buildRenderList(assets, null, groupBy);
|
||||||
return compute(
|
|
||||||
_processAssetGroupData,
|
|
||||||
_AssetGroupsToRenderListComputeParameters(
|
|
||||||
assets,
|
|
||||||
layout,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -396,8 +396,8 @@ class DraggableScrollbarState extends State<DraggableScrollbar>
|
|||||||
widget.scrollStateListener(true);
|
widget.scrollStateListener(true);
|
||||||
|
|
||||||
dragHaltTimer = Timer(
|
dragHaltTimer = Timer(
|
||||||
const Duration(milliseconds: 200),
|
const Duration(milliseconds: 500),
|
||||||
() {
|
() {
|
||||||
widget.scrollStateListener(false);
|
widget.scrollStateListener(false);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -19,8 +19,6 @@ class GroupDividerTitle extends ConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
|
||||||
|
|
||||||
void handleTitleIconClick() {
|
void handleTitleIconClick() {
|
||||||
if (selected) {
|
if (selected) {
|
||||||
onDeselect();
|
onDeselect();
|
||||||
@ -32,7 +30,7 @@ class GroupDividerTitle extends ConsumerWidget {
|
|||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.only(
|
||||||
top: 29.0,
|
top: 29.0,
|
||||||
bottom: 29.0,
|
bottom: 10.0,
|
||||||
left: 12.0,
|
left: 12.0,
|
||||||
right: 12.0,
|
right: 12.0,
|
||||||
),
|
),
|
||||||
|
@ -8,6 +8,7 @@ import 'package:immich_mobile/modules/settings/providers/app_settings.provider.d
|
|||||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||||
|
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||||
|
|
||||||
class ImmichAssetGrid extends HookConsumerWidget {
|
class ImmichAssetGrid extends HookConsumerWidget {
|
||||||
final int? assetsPerRow;
|
final int? assetsPerRow;
|
||||||
@ -15,13 +16,19 @@ class ImmichAssetGrid extends HookConsumerWidget {
|
|||||||
final bool? showStorageIndicator;
|
final bool? showStorageIndicator;
|
||||||
final ImmichAssetGridSelectionListener? listener;
|
final ImmichAssetGridSelectionListener? listener;
|
||||||
final bool selectionActive;
|
final bool selectionActive;
|
||||||
final List<Asset> assets;
|
final List<Asset>? assets;
|
||||||
final RenderList? renderList;
|
final RenderList? renderList;
|
||||||
final Future<void> Function()? onRefresh;
|
final Future<void> Function()? onRefresh;
|
||||||
|
final Set<Asset>? preselectedAssets;
|
||||||
|
final bool canDeselect;
|
||||||
|
final bool? dynamicLayout;
|
||||||
|
final bool showMultiSelectIndicator;
|
||||||
|
final void Function(ItemPosition start, ItemPosition end)?
|
||||||
|
visibleItemsListener;
|
||||||
|
|
||||||
const ImmichAssetGrid({
|
const ImmichAssetGrid({
|
||||||
super.key,
|
super.key,
|
||||||
required this.assets,
|
this.assets,
|
||||||
this.onRefresh,
|
this.onRefresh,
|
||||||
this.renderList,
|
this.renderList,
|
||||||
this.assetsPerRow,
|
this.assetsPerRow,
|
||||||
@ -29,12 +36,16 @@ class ImmichAssetGrid extends HookConsumerWidget {
|
|||||||
this.listener,
|
this.listener,
|
||||||
this.margin = 5.0,
|
this.margin = 5.0,
|
||||||
this.selectionActive = false,
|
this.selectionActive = false,
|
||||||
|
this.preselectedAssets,
|
||||||
|
this.canDeselect = true,
|
||||||
|
this.dynamicLayout,
|
||||||
|
this.showMultiSelectIndicator = true,
|
||||||
|
this.visibleItemsListener,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
var settings = ref.watch(appSettingsServiceProvider);
|
var settings = ref.watch(appSettingsServiceProvider);
|
||||||
final renderListFuture = ref.watch(renderListProvider(assets));
|
|
||||||
|
|
||||||
// Needs to suppress hero animations when navigating to this widget
|
// Needs to suppress hero animations when navigating to this widget
|
||||||
final enableHeroAnimations = useState(false);
|
final enableHeroAnimations = useState(false);
|
||||||
@ -64,34 +75,12 @@ class ImmichAssetGrid extends HookConsumerWidget {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (renderList != null) {
|
Widget buildAssetGridView(RenderList renderList) {
|
||||||
return WillPopScope(
|
return WillPopScope(
|
||||||
onWillPop: onWillPop,
|
onWillPop: onWillPop,
|
||||||
child: HeroMode(
|
child: HeroMode(
|
||||||
enabled: enableHeroAnimations.value,
|
enabled: enableHeroAnimations.value,
|
||||||
child: ImmichAssetGridView(
|
child: ImmichAssetGridView(
|
||||||
allAssets: assets,
|
|
||||||
onRefresh: onRefresh,
|
|
||||||
assetsPerRow: assetsPerRow ??
|
|
||||||
settings.getSetting(AppSettingsEnum.tilesPerRow),
|
|
||||||
listener: listener,
|
|
||||||
showStorageIndicator: showStorageIndicator ??
|
|
||||||
settings.getSetting(AppSettingsEnum.storageIndicator),
|
|
||||||
renderList: renderList!,
|
|
||||||
margin: margin,
|
|
||||||
selectionActive: selectionActive,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return renderListFuture.when(
|
|
||||||
data: (renderList) => WillPopScope(
|
|
||||||
onWillPop: onWillPop,
|
|
||||||
child: HeroMode(
|
|
||||||
enabled: enableHeroAnimations.value,
|
|
||||||
child: ImmichAssetGridView(
|
|
||||||
allAssets: assets,
|
|
||||||
onRefresh: onRefresh,
|
onRefresh: onRefresh,
|
||||||
assetsPerRow: assetsPerRow ??
|
assetsPerRow: assetsPerRow ??
|
||||||
settings.getSetting(AppSettingsEnum.tilesPerRow),
|
settings.getSetting(AppSettingsEnum.tilesPerRow),
|
||||||
@ -101,9 +90,22 @@ class ImmichAssetGrid extends HookConsumerWidget {
|
|||||||
renderList: renderList,
|
renderList: renderList,
|
||||||
margin: margin,
|
margin: margin,
|
||||||
selectionActive: selectionActive,
|
selectionActive: selectionActive,
|
||||||
|
preselectedAssets: preselectedAssets,
|
||||||
|
canDeselect: canDeselect,
|
||||||
|
dynamicLayout: dynamicLayout ??
|
||||||
|
settings.getSetting(AppSettingsEnum.dynamicLayout),
|
||||||
|
showMultiSelectIndicator: showMultiSelectIndicator,
|
||||||
|
visibleItemsListener: visibleItemsListener,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (renderList != null) return buildAssetGridView(renderList!);
|
||||||
|
|
||||||
|
final renderListFuture = ref.watch(renderListProvider(assets!));
|
||||||
|
return renderListFuture.when(
|
||||||
|
data: (renderList) => buildAssetGridView(renderList),
|
||||||
error: (err, stack) => Center(child: Text("$err")),
|
error: (err, stack) => Center(child: Text("$err")),
|
||||||
loading: () => const Center(
|
loading: () => const Center(
|
||||||
child: ImmichLoadingIndicator(),
|
child: ImmichLoadingIndicator(),
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import 'dart:collection';
|
import 'dart:collection';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
@ -6,6 +7,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:immich_mobile/modules/asset_viewer/providers/scroll_notifier.provider.dart';
|
import 'package:immich_mobile/modules/asset_viewer/providers/scroll_notifier.provider.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_image.dart';
|
import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_image.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
|
import 'package:immich_mobile/utils/builtin_extensions.dart';
|
||||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||||
import 'asset_grid_data_structure.dart';
|
import 'asset_grid_data_structure.dart';
|
||||||
import 'group_divider_title.dart';
|
import 'group_divider_title.dart';
|
||||||
@ -23,13 +25,11 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
|
|||||||
ItemPositionsListener.create();
|
ItemPositionsListener.create();
|
||||||
|
|
||||||
bool _scrolling = false;
|
bool _scrolling = false;
|
||||||
final Set<int> _selectedAssets = HashSet();
|
final Set<Asset> _selectedAssets =
|
||||||
|
HashSet(equals: (a, b) => a.id == b.id, hashCode: (a) => a.id);
|
||||||
|
|
||||||
Set<Asset> _getSelectedAssets() {
|
Set<Asset> _getSelectedAssets() {
|
||||||
return _selectedAssets
|
return Set.from(_selectedAssets);
|
||||||
.map((e) => widget.allAssets.firstWhereOrNull((a) => a.id == e))
|
|
||||||
.whereNotNull()
|
|
||||||
.toSet();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _callSelectionListener(bool selectionActive) {
|
void _callSelectionListener(bool selectionActive) {
|
||||||
@ -38,18 +38,14 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
|
|||||||
|
|
||||||
void _selectAssets(List<Asset> assets) {
|
void _selectAssets(List<Asset> assets) {
|
||||||
setState(() {
|
setState(() {
|
||||||
for (var e in assets) {
|
_selectedAssets.addAll(assets);
|
||||||
_selectedAssets.add(e.id);
|
|
||||||
}
|
|
||||||
_callSelectionListener(true);
|
_callSelectionListener(true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _deselectAssets(List<Asset> assets) {
|
void _deselectAssets(List<Asset> assets) {
|
||||||
setState(() {
|
setState(() {
|
||||||
for (var e in assets) {
|
_selectedAssets.removeAll(assets);
|
||||||
_selectedAssets.remove(e.id);
|
|
||||||
}
|
|
||||||
_callSelectionListener(_selectedAssets.isNotEmpty);
|
_callSelectionListener(_selectedAssets.isNotEmpty);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -57,64 +53,86 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
|
|||||||
void _deselectAll() {
|
void _deselectAll() {
|
||||||
setState(() {
|
setState(() {
|
||||||
_selectedAssets.clear();
|
_selectedAssets.clear();
|
||||||
|
if (!widget.canDeselect &&
|
||||||
|
widget.preselectedAssets != null &&
|
||||||
|
widget.preselectedAssets!.isNotEmpty) {
|
||||||
|
_selectedAssets.addAll(widget.preselectedAssets!);
|
||||||
|
}
|
||||||
|
_callSelectionListener(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
_callSelectionListener(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _allAssetsSelected(List<Asset> assets) {
|
bool _allAssetsSelected(List<Asset> assets) {
|
||||||
return widget.selectionActive &&
|
return widget.selectionActive &&
|
||||||
assets.firstWhereOrNull((e) => !_selectedAssets.contains(e.id)) == null;
|
assets.firstWhereOrNull((e) => !_selectedAssets.contains(e)) == null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildThumbnailOrPlaceholder(
|
Widget _buildThumbnailOrPlaceholder(Asset asset, int index) {
|
||||||
Asset asset,
|
|
||||||
bool placeholder,
|
|
||||||
) {
|
|
||||||
if (placeholder) {
|
|
||||||
return const DecoratedBox(
|
|
||||||
decoration: BoxDecoration(color: Colors.grey),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return ThumbnailImage(
|
return ThumbnailImage(
|
||||||
asset: asset,
|
asset: asset,
|
||||||
assetList: widget.allAssets,
|
index: index,
|
||||||
|
loadAsset: widget.renderList.loadAsset,
|
||||||
|
totalAssets: widget.renderList.totalAssets,
|
||||||
multiselectEnabled: widget.selectionActive,
|
multiselectEnabled: widget.selectionActive,
|
||||||
isSelected: widget.selectionActive && _selectedAssets.contains(asset.id),
|
isSelected: widget.selectionActive && _selectedAssets.contains(asset),
|
||||||
onSelect: () => _selectAssets([asset]),
|
onSelect: () => _selectAssets([asset]),
|
||||||
onDeselect: () => _deselectAssets([asset]),
|
onDeselect: widget.canDeselect ||
|
||||||
|
widget.preselectedAssets == null ||
|
||||||
|
!widget.preselectedAssets!.contains(asset)
|
||||||
|
? () => _deselectAssets([asset])
|
||||||
|
: null,
|
||||||
useGrayBoxPlaceholder: true,
|
useGrayBoxPlaceholder: true,
|
||||||
showStorageIndicator: widget.showStorageIndicator,
|
showStorageIndicator: widget.showStorageIndicator,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildAssetRow(
|
Widget _buildAssetRow(
|
||||||
|
Key key,
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
RenderAssetGridRow row,
|
List<Asset> assets,
|
||||||
bool scrolling,
|
int absoluteOffset,
|
||||||
|
double width,
|
||||||
) {
|
) {
|
||||||
return LayoutBuilder(
|
// Default: All assets have the same width
|
||||||
builder: (context, constraints) {
|
final widthDistribution = List.filled(assets.length, 1.0);
|
||||||
final size = constraints.maxWidth / widget.assetsPerRow -
|
|
||||||
widget.margin * (widget.assetsPerRow - 1) / widget.assetsPerRow;
|
|
||||||
return Row(
|
|
||||||
key: Key("asset-row-${row.assets.first.id}"),
|
|
||||||
children: row.assets.mapIndexed((int index, Asset asset) {
|
|
||||||
bool last = asset.id == row.assets.last.id;
|
|
||||||
|
|
||||||
return Container(
|
if (widget.dynamicLayout) {
|
||||||
key: Key("asset-${asset.id}"),
|
final aspectRatios =
|
||||||
width: size * row.widthDistribution[index],
|
assets.map((e) => (e.width ?? 1) / (e.height ?? 1)).toList();
|
||||||
height: size,
|
final meanAspectRatio = aspectRatios.sum / assets.length;
|
||||||
margin: EdgeInsets.only(
|
|
||||||
top: widget.margin,
|
// 1: mean width
|
||||||
right: last ? 0.0 : widget.margin,
|
// 0.5: width < mean - threshold
|
||||||
),
|
// 1.5: width > mean + threshold
|
||||||
child: _buildThumbnailOrPlaceholder(asset, scrolling),
|
final arConfiguration = aspectRatios.map((e) {
|
||||||
);
|
if (e - meanAspectRatio > 0.3) return 1.5;
|
||||||
}).toList(),
|
if (e - meanAspectRatio < -0.3) return 0.5;
|
||||||
|
return 1.0;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Normalize:
|
||||||
|
final sum = arConfiguration.sum;
|
||||||
|
widthDistribution.setRange(
|
||||||
|
0,
|
||||||
|
widthDistribution.length,
|
||||||
|
arConfiguration.map((e) => (e * assets.length) / sum),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Row(
|
||||||
|
key: key,
|
||||||
|
children: assets.mapIndexed((int index, Asset asset) {
|
||||||
|
final bool last = index + 1 == widget.assetsPerRow;
|
||||||
|
return Container(
|
||||||
|
key: ValueKey(index),
|
||||||
|
width: width * widthDistribution[index],
|
||||||
|
height: width,
|
||||||
|
margin: EdgeInsets.only(
|
||||||
|
top: widget.margin,
|
||||||
|
right: last ? 0.0 : widget.margin,
|
||||||
|
),
|
||||||
|
child: _buildThumbnailOrPlaceholder(asset, absoluteOffset + index),
|
||||||
);
|
);
|
||||||
},
|
}).toList(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,10 +150,14 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildMonthTitle(BuildContext context, String title) {
|
Widget _buildMonthTitle(BuildContext context, DateTime date) {
|
||||||
|
final monthFormat = DateTime.now().year == date.year
|
||||||
|
? DateFormat.MMMM()
|
||||||
|
: DateFormat.yMMMM();
|
||||||
|
final String title = monthFormat.format(date);
|
||||||
return Padding(
|
return Padding(
|
||||||
key: Key("month-$title"),
|
key: Key("month-$title"),
|
||||||
padding: const EdgeInsets.only(left: 12.0, top: 32),
|
padding: const EdgeInsets.only(left: 12.0, top: 30),
|
||||||
child: Text(
|
child: Text(
|
||||||
title,
|
title,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
@ -147,18 +169,84 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildPlaceHolderRow(Key key, int num, double width, double height) {
|
||||||
|
return Row(
|
||||||
|
key: key,
|
||||||
|
children: [
|
||||||
|
for (int i = 0; i < num; i++)
|
||||||
|
Container(
|
||||||
|
key: ValueKey(i),
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
margin: EdgeInsets.only(
|
||||||
|
top: widget.margin,
|
||||||
|
right: i + 1 == num ? 0.0 : widget.margin,
|
||||||
|
),
|
||||||
|
color: Colors.grey,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSection(
|
||||||
|
BuildContext context,
|
||||||
|
RenderAssetGridElement section,
|
||||||
|
bool scrolling,
|
||||||
|
) {
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
final width = constraints.maxWidth / widget.assetsPerRow -
|
||||||
|
widget.margin * (widget.assetsPerRow - 1) / widget.assetsPerRow;
|
||||||
|
final rows =
|
||||||
|
(section.count + widget.assetsPerRow - 1) ~/ widget.assetsPerRow;
|
||||||
|
final List<Asset> assetsToRender = scrolling
|
||||||
|
? []
|
||||||
|
: widget.renderList.loadAssets(section.offset, section.count);
|
||||||
|
return Column(
|
||||||
|
key: ValueKey(section.offset),
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (section.type == RenderAssetGridElementType.monthTitle)
|
||||||
|
_buildMonthTitle(context, section.date),
|
||||||
|
if (section.type == RenderAssetGridElementType.groupDividerTitle ||
|
||||||
|
section.type == RenderAssetGridElementType.monthTitle)
|
||||||
|
_buildTitle(
|
||||||
|
context,
|
||||||
|
section.title!,
|
||||||
|
scrolling
|
||||||
|
? []
|
||||||
|
: widget.renderList
|
||||||
|
.loadAssets(section.offset, section.totalCount),
|
||||||
|
),
|
||||||
|
for (int i = 0; i < rows; i++)
|
||||||
|
scrolling
|
||||||
|
? _buildPlaceHolderRow(
|
||||||
|
ValueKey(i),
|
||||||
|
i + 1 == rows
|
||||||
|
? section.count - i * widget.assetsPerRow
|
||||||
|
: widget.assetsPerRow,
|
||||||
|
width,
|
||||||
|
width,
|
||||||
|
)
|
||||||
|
: _buildAssetRow(
|
||||||
|
ValueKey(i),
|
||||||
|
context,
|
||||||
|
assetsToRender.nestedSlice(
|
||||||
|
i * widget.assetsPerRow,
|
||||||
|
min((i + 1) * widget.assetsPerRow, section.count),
|
||||||
|
),
|
||||||
|
section.offset + i * widget.assetsPerRow,
|
||||||
|
width,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _itemBuilder(BuildContext c, int position) {
|
Widget _itemBuilder(BuildContext c, int position) {
|
||||||
final item = widget.renderList.elements[position];
|
final item = widget.renderList.elements[position];
|
||||||
|
return _buildSection(c, item, _scrolling);
|
||||||
if (item.type == RenderAssetGridElementType.groupDividerTitle) {
|
|
||||||
return _buildTitle(c, item.title!, item.relatedAssetList!);
|
|
||||||
} else if (item.type == RenderAssetGridElementType.monthTitle) {
|
|
||||||
return _buildMonthTitle(c, item.title!);
|
|
||||||
} else if (item.type == RenderAssetGridElementType.assetRow) {
|
|
||||||
return _buildAssetRow(c, item.assetRow!, _scrolling);
|
|
||||||
}
|
|
||||||
|
|
||||||
return const Text("Invalid widget type!");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Text _labelBuilder(int pos) {
|
Text _labelBuilder(int pos) {
|
||||||
@ -180,7 +268,7 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildAssetGrid() {
|
Widget _buildAssetGrid() {
|
||||||
final useDragScrolling = widget.allAssets.length >= 20;
|
final useDragScrolling = widget.renderList.totalAssets >= 20;
|
||||||
|
|
||||||
void dragScrolling(bool active) {
|
void dragScrolling(bool active) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -225,6 +313,10 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_selectedAssets.clear();
|
_selectedAssets.clear();
|
||||||
});
|
});
|
||||||
|
} else if (widget.preselectedAssets != null) {
|
||||||
|
setState(() {
|
||||||
|
_selectedAssets.addAll(widget.preselectedAssets!);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -241,14 +333,33 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
scrollToTopNotifierProvider.addListener(_scrollToTop);
|
scrollToTopNotifierProvider.addListener(_scrollToTop);
|
||||||
|
if (widget.visibleItemsListener != null) {
|
||||||
|
_itemPositionsListener.itemPositions.addListener(_positionListener);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
scrollToTopNotifierProvider.removeListener(_scrollToTop);
|
scrollToTopNotifierProvider.removeListener(_scrollToTop);
|
||||||
|
if (widget.visibleItemsListener != null) {
|
||||||
|
_itemPositionsListener.itemPositions.removeListener(_positionListener);
|
||||||
|
}
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _positionListener() {
|
||||||
|
final values = _itemPositionsListener.itemPositions.value;
|
||||||
|
final start = values.firstOrNull;
|
||||||
|
final end = values.lastOrNull;
|
||||||
|
if (start != null && end != null) {
|
||||||
|
if (start.index <= end.index) {
|
||||||
|
widget.visibleItemsListener?.call(start, end);
|
||||||
|
} else {
|
||||||
|
widget.visibleItemsListener?.call(end, start);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _scrollToTop() {
|
void _scrollToTop() {
|
||||||
// for some reason, this is necessary as well in order
|
// for some reason, this is necessary as well in order
|
||||||
// to correctly reposition the drag thumb scroll bar
|
// to correctly reposition the drag thumb scroll bar
|
||||||
@ -268,7 +379,8 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
|
|||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
_buildAssetGrid(),
|
_buildAssetGrid(),
|
||||||
if (widget.selectionActive) _buildMultiSelectIndicator(),
|
if (widget.showMultiSelectIndicator && widget.selectionActive)
|
||||||
|
_buildMultiSelectIndicator(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -282,19 +394,28 @@ class ImmichAssetGridView extends StatefulWidget {
|
|||||||
final bool showStorageIndicator;
|
final bool showStorageIndicator;
|
||||||
final ImmichAssetGridSelectionListener? listener;
|
final ImmichAssetGridSelectionListener? listener;
|
||||||
final bool selectionActive;
|
final bool selectionActive;
|
||||||
final List<Asset> allAssets;
|
|
||||||
final Future<void> Function()? onRefresh;
|
final Future<void> Function()? onRefresh;
|
||||||
|
final Set<Asset>? preselectedAssets;
|
||||||
|
final bool canDeselect;
|
||||||
|
final bool dynamicLayout;
|
||||||
|
final bool showMultiSelectIndicator;
|
||||||
|
final void Function(ItemPosition start, ItemPosition end)?
|
||||||
|
visibleItemsListener;
|
||||||
|
|
||||||
const ImmichAssetGridView({
|
const ImmichAssetGridView({
|
||||||
super.key,
|
super.key,
|
||||||
required this.renderList,
|
required this.renderList,
|
||||||
required this.allAssets,
|
|
||||||
required this.assetsPerRow,
|
required this.assetsPerRow,
|
||||||
required this.showStorageIndicator,
|
required this.showStorageIndicator,
|
||||||
this.listener,
|
this.listener,
|
||||||
this.margin = 5.0,
|
this.margin = 5.0,
|
||||||
this.selectionActive = false,
|
this.selectionActive = false,
|
||||||
this.onRefresh,
|
this.onRefresh,
|
||||||
|
this.preselectedAssets,
|
||||||
|
this.canDeselect = true,
|
||||||
|
this.dynamicLayout = true,
|
||||||
|
this.showMultiSelectIndicator = true,
|
||||||
|
this.visibleItemsListener,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -2,7 +2,6 @@ import 'package:auto_route/auto_route.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/favorite/providers/favorite_provider.dart';
|
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_image.dart';
|
import 'package:immich_mobile/shared/ui/immich_image.dart';
|
||||||
@ -10,7 +9,9 @@ import 'package:immich_mobile/utils/storage_indicator.dart';
|
|||||||
|
|
||||||
class ThumbnailImage extends HookConsumerWidget {
|
class ThumbnailImage extends HookConsumerWidget {
|
||||||
final Asset asset;
|
final Asset asset;
|
||||||
final List<Asset> assetList;
|
final int index;
|
||||||
|
final Asset Function(int index) loadAsset;
|
||||||
|
final int totalAssets;
|
||||||
final bool showStorageIndicator;
|
final bool showStorageIndicator;
|
||||||
final bool useGrayBoxPlaceholder;
|
final bool useGrayBoxPlaceholder;
|
||||||
final bool isSelected;
|
final bool isSelected;
|
||||||
@ -21,7 +22,9 @@ class ThumbnailImage extends HookConsumerWidget {
|
|||||||
const ThumbnailImage({
|
const ThumbnailImage({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.asset,
|
required this.asset,
|
||||||
required this.assetList,
|
required this.index,
|
||||||
|
required this.loadAsset,
|
||||||
|
required this.totalAssets,
|
||||||
this.showStorageIndicator = true,
|
this.showStorageIndicator = true,
|
||||||
this.useGrayBoxPlaceholder = false,
|
this.useGrayBoxPlaceholder = false,
|
||||||
this.isSelected = false,
|
this.isSelected = false,
|
||||||
@ -57,8 +60,9 @@ class ThumbnailImage extends HookConsumerWidget {
|
|||||||
} else {
|
} else {
|
||||||
AutoRouter.of(context).push(
|
AutoRouter.of(context).push(
|
||||||
GalleryViewerRoute(
|
GalleryViewerRoute(
|
||||||
assetList: assetList,
|
initialIndex: index,
|
||||||
asset: asset,
|
loadAsset: loadAsset,
|
||||||
|
totalAssets: totalAssets,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -100,7 +104,9 @@ class ThumbnailImage extends HookConsumerWidget {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: multiselectEnabled && isSelected
|
border: multiselectEnabled && isSelected
|
||||||
? Border.all(
|
? Border.all(
|
||||||
color: Theme.of(context).primaryColorLight,
|
color: onDeselect == null
|
||||||
|
? Colors.grey
|
||||||
|
: Theme.of(context).primaryColorLight,
|
||||||
width: 10,
|
width: 10,
|
||||||
)
|
)
|
||||||
: const Border(),
|
: const Border(),
|
||||||
@ -130,7 +136,7 @@ class ThumbnailImage extends HookConsumerWidget {
|
|||||||
size: 18,
|
size: 18,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (ref.watch(favoriteProvider).contains(asset.id))
|
if (asset.isFavorite)
|
||||||
const Positioned(
|
const Positioned(
|
||||||
left: 10,
|
left: 10,
|
||||||
bottom: 5,
|
bottom: 5,
|
||||||
|
@ -7,15 +7,16 @@ import 'package:immich_mobile/shared/ui/drag_sheet.dart';
|
|||||||
import 'package:immich_mobile/shared/models/album.dart';
|
import 'package:immich_mobile/shared/models/album.dart';
|
||||||
|
|
||||||
class ControlBottomAppBar extends ConsumerWidget {
|
class ControlBottomAppBar extends ConsumerWidget {
|
||||||
final Function onShare;
|
final void Function() onShare;
|
||||||
final Function onFavorite;
|
final void Function() onFavorite;
|
||||||
final Function onArchive;
|
final void Function() onArchive;
|
||||||
final Function onDelete;
|
final void Function() onDelete;
|
||||||
final Function(Album album) onAddToAlbum;
|
final Function(Album album) onAddToAlbum;
|
||||||
final void Function() onCreateNewAlbum;
|
final void Function() onCreateNewAlbum;
|
||||||
|
|
||||||
final List<Album> albums;
|
final List<Album> albums;
|
||||||
final List<Album> sharedAlbums;
|
final List<Album> sharedAlbums;
|
||||||
|
final bool enabled;
|
||||||
|
|
||||||
const ControlBottomAppBar({
|
const ControlBottomAppBar({
|
||||||
Key? key,
|
Key? key,
|
||||||
@ -27,6 +28,7 @@ class ControlBottomAppBar extends ConsumerWidget {
|
|||||||
required this.albums,
|
required this.albums,
|
||||||
required this.onAddToAlbum,
|
required this.onAddToAlbum,
|
||||||
required this.onCreateNewAlbum,
|
required this.onCreateNewAlbum,
|
||||||
|
this.enabled = true,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -39,35 +41,31 @@ class ControlBottomAppBar extends ConsumerWidget {
|
|||||||
ControlBoxButton(
|
ControlBoxButton(
|
||||||
iconData: Icons.ios_share_rounded,
|
iconData: Icons.ios_share_rounded,
|
||||||
label: "control_bottom_app_bar_share".tr(),
|
label: "control_bottom_app_bar_share".tr(),
|
||||||
onPressed: () {
|
onPressed: enabled ? onShare : null,
|
||||||
onShare();
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
ControlBoxButton(
|
ControlBoxButton(
|
||||||
iconData: Icons.favorite_border_rounded,
|
iconData: Icons.favorite_border_rounded,
|
||||||
label: "control_bottom_app_bar_favorite".tr(),
|
label: "control_bottom_app_bar_favorite".tr(),
|
||||||
onPressed: () {
|
onPressed: enabled ? onFavorite : null,
|
||||||
onFavorite();
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
ControlBoxButton(
|
ControlBoxButton(
|
||||||
iconData: Icons.delete_outline_rounded,
|
iconData: Icons.delete_outline_rounded,
|
||||||
label: "control_bottom_app_bar_delete".tr(),
|
label: "control_bottom_app_bar_delete".tr(),
|
||||||
onPressed: () {
|
onPressed: enabled
|
||||||
showDialog(
|
? () => showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
return DeleteDialog(
|
return DeleteDialog(
|
||||||
onDelete: onDelete,
|
onDelete: onDelete,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
)
|
||||||
},
|
: null,
|
||||||
),
|
),
|
||||||
ControlBoxButton(
|
ControlBoxButton(
|
||||||
iconData: Icons.archive,
|
iconData: Icons.archive,
|
||||||
label: "control_bottom_app_bar_archive".tr(),
|
label: "control_bottom_app_bar_archive".tr(),
|
||||||
onPressed: () => onArchive(),
|
onPressed: enabled ? onArchive : null,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@ -108,7 +106,9 @@ class ControlBottomAppBar extends ConsumerWidget {
|
|||||||
endIndent: 16,
|
endIndent: 16,
|
||||||
thickness: 1,
|
thickness: 1,
|
||||||
),
|
),
|
||||||
AddToAlbumTitleRow(onCreateNewAlbum: onCreateNewAlbum),
|
AddToAlbumTitleRow(
|
||||||
|
onCreateNewAlbum: enabled ? onCreateNewAlbum : null,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -118,6 +118,7 @@ class ControlBottomAppBar extends ConsumerWidget {
|
|||||||
albums: albums,
|
albums: albums,
|
||||||
sharedAlbums: sharedAlbums,
|
sharedAlbums: sharedAlbums,
|
||||||
onAddToAlbum: onAddToAlbum,
|
onAddToAlbum: onAddToAlbum,
|
||||||
|
enabled: enabled,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SliverToBoxAdapter(
|
const SliverToBoxAdapter(
|
||||||
@ -137,7 +138,7 @@ class AddToAlbumTitleRow extends StatelessWidget {
|
|||||||
required this.onCreateNewAlbum,
|
required this.onCreateNewAlbum,
|
||||||
});
|
});
|
||||||
|
|
||||||
final VoidCallback onCreateNewAlbum;
|
final VoidCallback? onCreateNewAlbum;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
@ -10,14 +10,11 @@ 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.provider.dart';
|
||||||
import 'package:immich_mobile/modules/album/providers/shared_album.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/services/album.service.dart';
|
||||||
import 'package:immich_mobile/modules/favorite/providers/favorite_provider.dart';
|
|
||||||
import 'package:immich_mobile/modules/home/providers/multiselect.provider.dart';
|
import 'package:immich_mobile/modules/home/providers/multiselect.provider.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
|
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/control_bottom_app_bar.dart';
|
import 'package:immich_mobile/modules/home/ui/control_bottom_app_bar.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/home_page_app_bar.dart';
|
import 'package:immich_mobile/modules/home/ui/home_page_app_bar.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/profile_drawer/profile_drawer.dart';
|
import 'package:immich_mobile/modules/home/ui/profile_drawer/profile_drawer.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/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/shared/models/album.dart';
|
import 'package:immich_mobile/shared/models/album.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
@ -34,7 +31,6 @@ class HomePage extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final appSettingService = ref.watch(appSettingsServiceProvider);
|
|
||||||
final multiselectEnabled = ref.watch(multiselectProvider.notifier);
|
final multiselectEnabled = ref.watch(multiselectProvider.notifier);
|
||||||
final selectionEnabledHook = useState(false);
|
final selectionEnabledHook = useState(false);
|
||||||
|
|
||||||
@ -45,6 +41,7 @@ class HomePage extends HookConsumerWidget {
|
|||||||
|
|
||||||
final tipOneOpacity = useState(0.0);
|
final tipOneOpacity = useState(0.0);
|
||||||
final refreshCount = useState(0);
|
final refreshCount = useState(0);
|
||||||
|
final processing = useState(false);
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() {
|
() {
|
||||||
@ -97,7 +94,7 @@ class HomePage extends HookConsumerWidget {
|
|||||||
selectionEnabledHook.value = false;
|
selectionEnabledHook.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Iterable<Asset> remoteOnlySelection({String? localErrorMessage}) {
|
List<Asset> remoteOnlySelection({String? localErrorMessage}) {
|
||||||
final Set<Asset> assets = selection.value;
|
final Set<Asset> assets = selection.value;
|
||||||
final bool onlyRemote = assets.every((e) => e.isRemote);
|
final bool onlyRemote = assets.every((e) => e.isRemote);
|
||||||
if (!onlyRemote) {
|
if (!onlyRemote) {
|
||||||
@ -108,113 +105,139 @@ class HomePage extends HookConsumerWidget {
|
|||||||
gravity: ToastGravity.BOTTOM,
|
gravity: ToastGravity.BOTTOM,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return assets.where((a) => a.isRemote);
|
return assets.where((a) => a.isRemote).toList();
|
||||||
}
|
}
|
||||||
return assets;
|
return assets.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
void onFavoriteAssets() {
|
void onFavoriteAssets() async {
|
||||||
final remoteAssets = remoteOnlySelection(
|
processing.value = true;
|
||||||
localErrorMessage: 'home_page_favorite_err_local'.tr(),
|
try {
|
||||||
);
|
final remoteAssets = remoteOnlySelection(
|
||||||
if (remoteAssets.isNotEmpty) {
|
localErrorMessage: 'home_page_favorite_err_local'.tr(),
|
||||||
ref.watch(favoriteProvider.notifier).addToFavorites(remoteAssets);
|
|
||||||
|
|
||||||
final assetOrAssets = remoteAssets.length > 1 ? 'assets' : 'asset';
|
|
||||||
ImmichToast.show(
|
|
||||||
context: context,
|
|
||||||
msg: 'Added ${remoteAssets.length} $assetOrAssets to favorites',
|
|
||||||
gravity: ToastGravity.BOTTOM,
|
|
||||||
);
|
);
|
||||||
}
|
if (remoteAssets.isNotEmpty) {
|
||||||
|
await ref
|
||||||
|
.watch(assetProvider.notifier)
|
||||||
|
.toggleFavorite(remoteAssets, true);
|
||||||
|
|
||||||
selectionEnabledHook.value = false;
|
final assetOrAssets = remoteAssets.length > 1 ? 'assets' : 'asset';
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: 'Added ${remoteAssets.length} $assetOrAssets to favorites',
|
||||||
|
gravity: ToastGravity.BOTTOM,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
processing.value = false;
|
||||||
|
selectionEnabledHook.value = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void onArchiveAsset() {
|
void onArchiveAsset() async {
|
||||||
final remoteAssets = remoteOnlySelection(
|
processing.value = true;
|
||||||
localErrorMessage: 'home_page_archive_err_local'.tr(),
|
try {
|
||||||
);
|
final remoteAssets = remoteOnlySelection(
|
||||||
if (remoteAssets.isNotEmpty) {
|
localErrorMessage: 'home_page_archive_err_local'.tr(),
|
||||||
ref.watch(assetProvider.notifier).toggleArchive(remoteAssets, true);
|
|
||||||
|
|
||||||
final assetOrAssets = remoteAssets.length > 1 ? 'assets' : 'asset';
|
|
||||||
ImmichToast.show(
|
|
||||||
context: context,
|
|
||||||
msg: 'Moved ${remoteAssets.length} $assetOrAssets to archive',
|
|
||||||
gravity: ToastGravity.CENTER,
|
|
||||||
);
|
);
|
||||||
}
|
if (remoteAssets.isNotEmpty) {
|
||||||
|
await ref
|
||||||
|
.watch(assetProvider.notifier)
|
||||||
|
.toggleArchive(remoteAssets, true);
|
||||||
|
|
||||||
selectionEnabledHook.value = false;
|
final assetOrAssets = remoteAssets.length > 1 ? 'assets' : 'asset';
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: 'Moved ${remoteAssets.length} $assetOrAssets to archive',
|
||||||
|
gravity: ToastGravity.CENTER,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
processing.value = false;
|
||||||
|
selectionEnabledHook.value = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void onDelete() {
|
void onDelete() async {
|
||||||
ref.watch(assetProvider.notifier).deleteAssets(selection.value);
|
processing.value = true;
|
||||||
selectionEnabledHook.value = false;
|
try {
|
||||||
|
await ref.watch(assetProvider.notifier).deleteAssets(selection.value);
|
||||||
|
selectionEnabledHook.value = false;
|
||||||
|
} finally {
|
||||||
|
processing.value = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void onAddToAlbum(Album album) async {
|
void onAddToAlbum(Album album) async {
|
||||||
final Iterable<Asset> assets = remoteOnlySelection(
|
processing.value = true;
|
||||||
localErrorMessage: "home_page_add_to_album_err_local".tr(),
|
try {
|
||||||
);
|
final Iterable<Asset> assets = remoteOnlySelection(
|
||||||
if (assets.isEmpty) {
|
localErrorMessage: "home_page_add_to_album_err_local".tr(),
|
||||||
return;
|
);
|
||||||
}
|
if (assets.isEmpty) {
|
||||||
final result = await albumService.addAdditionalAssetToAlbum(
|
return;
|
||||||
assets,
|
|
||||||
album,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result != null) {
|
|
||||||
if (result.alreadyInAlbum.isNotEmpty) {
|
|
||||||
ImmichToast.show(
|
|
||||||
context: context,
|
|
||||||
msg: "home_page_add_to_album_conflicts".tr(
|
|
||||||
namedArgs: {
|
|
||||||
"album": album.name,
|
|
||||||
"added": result.successfullyAdded.toString(),
|
|
||||||
"failed": result.alreadyInAlbum.length.toString()
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
ImmichToast.show(
|
|
||||||
context: context,
|
|
||||||
msg: "home_page_add_to_album_success".tr(
|
|
||||||
namedArgs: {
|
|
||||||
"album": album.name,
|
|
||||||
"added": result.successfullyAdded.toString(),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
toastType: ToastType.success,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
final result = await albumService.addAdditionalAssetToAlbum(
|
||||||
|
assets,
|
||||||
|
album,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result != null) {
|
||||||
|
if (result.alreadyInAlbum.isNotEmpty) {
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: "home_page_add_to_album_conflicts".tr(
|
||||||
|
namedArgs: {
|
||||||
|
"album": album.name,
|
||||||
|
"added": result.successfullyAdded.toString(),
|
||||||
|
"failed": result.alreadyInAlbum.length.toString()
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: "home_page_add_to_album_success".tr(
|
||||||
|
namedArgs: {
|
||||||
|
"album": album.name,
|
||||||
|
"added": result.successfullyAdded.toString(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
toastType: ToastType.success,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
processing.value = false;
|
||||||
selectionEnabledHook.value = false;
|
selectionEnabledHook.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void onCreateNewAlbum() async {
|
void onCreateNewAlbum() async {
|
||||||
final Iterable<Asset> assets = remoteOnlySelection(
|
processing.value = true;
|
||||||
localErrorMessage: "home_page_add_to_album_err_local".tr(),
|
try {
|
||||||
);
|
final Iterable<Asset> assets = remoteOnlySelection(
|
||||||
if (assets.isEmpty) {
|
localErrorMessage: "home_page_add_to_album_err_local".tr(),
|
||||||
return;
|
);
|
||||||
}
|
if (assets.isEmpty) {
|
||||||
final result = await albumService.createAlbumWithGeneratedName(assets);
|
return;
|
||||||
|
}
|
||||||
|
final result =
|
||||||
|
await albumService.createAlbumWithGeneratedName(assets);
|
||||||
|
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
ref.watch(albumProvider.notifier).getAllAlbums();
|
ref.watch(albumProvider.notifier).getAllAlbums();
|
||||||
ref.watch(sharedAlbumProvider.notifier).getAllSharedAlbums();
|
ref.watch(sharedAlbumProvider.notifier).getAllSharedAlbums();
|
||||||
selectionEnabledHook.value = false;
|
selectionEnabledHook.value = false;
|
||||||
|
|
||||||
AutoRouter.of(context).push(AlbumViewerRoute(albumId: result.id));
|
AutoRouter.of(context).push(AlbumViewerRoute(albumId: result.id));
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
processing.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> refreshAssets() async {
|
Future<void> refreshAssets() async {
|
||||||
debugPrint("refreshCount.value ${refreshCount.value}");
|
|
||||||
final fullRefresh = refreshCount.value > 0;
|
final fullRefresh = refreshCount.value > 0;
|
||||||
await ref.read(assetProvider.notifier).getAllAsset(clear: fullRefresh);
|
await ref.read(assetProvider.notifier).getAllAsset(clear: fullRefresh);
|
||||||
if (fullRefresh) {
|
if (fullRefresh) {
|
||||||
@ -277,20 +300,18 @@ class HomePage extends HookConsumerWidget {
|
|||||||
bottom: false,
|
bottom: false,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
ref.watch(assetProvider).renderList == null ||
|
ref.watch(assetsProvider).when(
|
||||||
ref.watch(assetProvider).allAssets.isEmpty
|
data: (data) => data.isEmpty
|
||||||
? buildLoadingIndicator()
|
? buildLoadingIndicator()
|
||||||
: ImmichAssetGrid(
|
: ImmichAssetGrid(
|
||||||
renderList: ref.watch(assetProvider).renderList!,
|
renderList: data,
|
||||||
assets: ref.read(assetProvider).allAssets,
|
listener: selectionListener,
|
||||||
assetsPerRow: appSettingService
|
selectionActive: selectionEnabledHook.value,
|
||||||
.getSetting(AppSettingsEnum.tilesPerRow),
|
onRefresh: refreshAssets,
|
||||||
showStorageIndicator: appSettingService
|
),
|
||||||
.getSetting(AppSettingsEnum.storageIndicator),
|
error: (error, _) => Center(child: Text(error.toString())),
|
||||||
listener: selectionListener,
|
loading: buildLoadingIndicator,
|
||||||
selectionActive: selectionEnabledHook.value,
|
),
|
||||||
onRefresh: refreshAssets,
|
|
||||||
),
|
|
||||||
if (selectionEnabledHook.value)
|
if (selectionEnabledHook.value)
|
||||||
ControlBottomAppBar(
|
ControlBottomAppBar(
|
||||||
onShare: onShareAssets,
|
onShare: onShareAssets,
|
||||||
@ -301,7 +322,9 @@ class HomePage extends HookConsumerWidget {
|
|||||||
albums: albums,
|
albums: albums,
|
||||||
sharedAlbums: sharedAlbums,
|
sharedAlbums: sharedAlbums,
|
||||||
onCreateNewAlbum: onCreateNewAlbum,
|
onCreateNewAlbum: onCreateNewAlbum,
|
||||||
|
enabled: !processing.value,
|
||||||
),
|
),
|
||||||
|
if (processing.value) const Center(child: ImmichLoadingIndicator())
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -9,13 +9,17 @@ import 'package:immich_mobile/shared/models/store.dart';
|
|||||||
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
|
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
|
||||||
import 'package:immich_mobile/shared/models/user.dart';
|
import 'package:immich_mobile/shared/models/user.dart';
|
||||||
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/providers/db.provider.dart';
|
||||||
import 'package:immich_mobile/shared/services/api.service.dart';
|
import 'package:immich_mobile/shared/services/api.service.dart';
|
||||||
|
import 'package:immich_mobile/utils/db.dart';
|
||||||
import 'package:immich_mobile/utils/hash.dart';
|
import 'package:immich_mobile/utils/hash.dart';
|
||||||
|
import 'package:isar/isar.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
||||||
AuthenticationNotifier(
|
AuthenticationNotifier(
|
||||||
this._apiService,
|
this._apiService,
|
||||||
|
this._db,
|
||||||
) : super(
|
) : super(
|
||||||
AuthenticationState(
|
AuthenticationState(
|
||||||
deviceId: "",
|
deviceId: "",
|
||||||
@ -31,6 +35,7 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
final ApiService _apiService;
|
final ApiService _apiService;
|
||||||
|
final Isar _db;
|
||||||
|
|
||||||
Future<bool> login(
|
Future<bool> login(
|
||||||
String email,
|
String email,
|
||||||
@ -91,7 +96,7 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
|||||||
try {
|
try {
|
||||||
await Future.wait([
|
await Future.wait([
|
||||||
_apiService.authenticationApi.logout(),
|
_apiService.authenticationApi.logout(),
|
||||||
Store.delete(StoreKey.assetETag),
|
clearAssetsAndAlbums(_db),
|
||||||
Store.delete(StoreKey.currentUser),
|
Store.delete(StoreKey.currentUser),
|
||||||
Store.delete(StoreKey.accessToken),
|
Store.delete(StoreKey.accessToken),
|
||||||
]);
|
]);
|
||||||
@ -170,5 +175,6 @@ final authenticationProvider =
|
|||||||
StateNotifierProvider<AuthenticationNotifier, AuthenticationState>((ref) {
|
StateNotifierProvider<AuthenticationNotifier, AuthenticationState>((ref) {
|
||||||
return AuthenticationNotifier(
|
return AuthenticationNotifier(
|
||||||
ref.watch(apiServiceProvider),
|
ref.watch(apiServiceProvider),
|
||||||
|
ref.watch(dbProvider),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -8,6 +8,8 @@ class SearchResultGrid extends HookConsumerWidget {
|
|||||||
|
|
||||||
final List<Asset> assets;
|
final List<Asset> assets;
|
||||||
|
|
||||||
|
Asset _loadAsset(int index) => assets[index];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return GridView.builder(
|
return GridView.builder(
|
||||||
@ -22,7 +24,9 @@ class SearchResultGrid extends HookConsumerWidget {
|
|||||||
final asset = assets[index];
|
final asset = assets[index];
|
||||||
return ThumbnailImage(
|
return ThumbnailImage(
|
||||||
asset: asset,
|
asset: asset,
|
||||||
assetList: assets,
|
index: index,
|
||||||
|
loadAsset: _loadAsset,
|
||||||
|
totalAssets: assets.length,
|
||||||
useGrayBoxPlaceholder: true,
|
useGrayBoxPlaceholder: true,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -5,7 +5,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
|
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
|
||||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.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/modules/settings/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
|
||||||
|
|
||||||
class LayoutSettings extends HookConsumerWidget {
|
class LayoutSettings extends HookConsumerWidget {
|
||||||
const LayoutSettings({
|
const LayoutSettings({
|
||||||
@ -22,14 +21,17 @@ class LayoutSettings extends HookConsumerWidget {
|
|||||||
void switchChanged(bool value) {
|
void switchChanged(bool value) {
|
||||||
appSettingService.setSetting(AppSettingsEnum.dynamicLayout, value);
|
appSettingService.setSetting(AppSettingsEnum.dynamicLayout, value);
|
||||||
useDynamicLayout.value = value;
|
useDynamicLayout.value = value;
|
||||||
ref.watch(assetProvider.notifier).rebuildAssetGridDataStructure();
|
ref.invalidate(appSettingsServiceProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
void changeGroupValue(GroupAssetsBy? value) {
|
void changeGroupValue(GroupAssetsBy? value) {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
appSettingService.setSetting(AppSettingsEnum.groupAssetsBy, value.index);
|
appSettingService.setSetting(
|
||||||
|
AppSettingsEnum.groupAssetsBy,
|
||||||
|
value.index,
|
||||||
|
);
|
||||||
groupBy.value = value;
|
groupBy.value = value;
|
||||||
ref.watch(assetProvider.notifier).rebuildAssetGridDataStructure();
|
ref.invalidate(appSettingsServiceProvider);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -37,8 +39,8 @@ class LayoutSettings extends HookConsumerWidget {
|
|||||||
() {
|
() {
|
||||||
useDynamicLayout.value =
|
useDynamicLayout.value =
|
||||||
appSettingService.getSetting<bool>(AppSettingsEnum.dynamicLayout);
|
appSettingService.getSetting<bool>(AppSettingsEnum.dynamicLayout);
|
||||||
groupBy.value =
|
groupBy.value = GroupAssetsBy.values[
|
||||||
GroupAssetsBy.values[appSettingService.getSetting<int>(AppSettingsEnum.groupAssetsBy)];
|
appSettingService.getSetting<int>(AppSettingsEnum.groupAssetsBy)];
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
@ -93,6 +95,19 @@ class LayoutSettings extends HookConsumerWidget {
|
|||||||
onChanged: changeGroupValue,
|
onChanged: changeGroupValue,
|
||||||
controlAffinity: ListTileControlAffinity.trailing,
|
controlAffinity: ListTileControlAffinity.trailing,
|
||||||
),
|
),
|
||||||
|
RadioListTile(
|
||||||
|
activeColor: Theme.of(context).primaryColor,
|
||||||
|
title: const Text(
|
||||||
|
"asset_list_layout_settings_group_automatically",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
).tr(),
|
||||||
|
value: GroupAssetsBy.auto,
|
||||||
|
groupValue: groupBy.value,
|
||||||
|
onChanged: changeGroupValue,
|
||||||
|
controlAffinity: ListTileControlAffinity.trailing,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.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/modules/settings/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
|
||||||
|
|
||||||
class StorageIndicator extends HookConsumerWidget {
|
class StorageIndicator extends HookConsumerWidget {
|
||||||
const StorageIndicator({
|
const StorageIndicator({
|
||||||
@ -20,12 +19,13 @@ class StorageIndicator extends HookConsumerWidget {
|
|||||||
void switchChanged(bool value) {
|
void switchChanged(bool value) {
|
||||||
appSettingService.setSetting(AppSettingsEnum.storageIndicator, value);
|
appSettingService.setSetting(AppSettingsEnum.storageIndicator, value);
|
||||||
showStorageIndicator.value = value;
|
showStorageIndicator.value = value;
|
||||||
ref.watch(assetProvider.notifier).rebuildAssetGridDataStructure();
|
ref.invalidate(appSettingsServiceProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() {
|
() {
|
||||||
showStorageIndicator.value = appSettingService.getSetting<bool>(AppSettingsEnum.storageIndicator);
|
showStorageIndicator.value = appSettingService
|
||||||
|
.getSetting<bool>(AppSettingsEnum.storageIndicator);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
@ -4,7 +4,6 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.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/modules/settings/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
|
||||||
|
|
||||||
class TilesPerRow extends HookConsumerWidget {
|
class TilesPerRow extends HookConsumerWidget {
|
||||||
const TilesPerRow({
|
const TilesPerRow({
|
||||||
@ -20,10 +19,7 @@ class TilesPerRow extends HookConsumerWidget {
|
|||||||
void sliderChanged(double value) {
|
void sliderChanged(double value) {
|
||||||
appSettingService.setSetting(AppSettingsEnum.tilesPerRow, value.toInt());
|
appSettingService.setSetting(AppSettingsEnum.tilesPerRow, value.toInt());
|
||||||
itemsValue.value = value;
|
itemsValue.value = value;
|
||||||
}
|
ref.invalidate(appSettingsServiceProvider);
|
||||||
|
|
||||||
void sliderChangedEnd(double _) {
|
|
||||||
ref.watch(assetProvider.notifier).rebuildAssetGridDataStructure();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
@ -49,7 +45,6 @@ class TilesPerRow extends HookConsumerWidget {
|
|||||||
).tr(args: ["${itemsValue.value.toInt()}"]),
|
).tr(args: ["${itemsValue.value.toInt()}"]),
|
||||||
),
|
),
|
||||||
Slider(
|
Slider(
|
||||||
onChangeEnd: sliderChangedEnd,
|
|
||||||
onChanged: sliderChanged,
|
onChanged: sliderChanged,
|
||||||
value: itemsValue.value,
|
value: itemsValue.value,
|
||||||
min: 2,
|
min: 2,
|
||||||
|
@ -67,8 +67,9 @@ class _$AppRouter extends RootStackRouter {
|
|||||||
routeData: routeData,
|
routeData: routeData,
|
||||||
child: GalleryViewerPage(
|
child: GalleryViewerPage(
|
||||||
key: args.key,
|
key: args.key,
|
||||||
assetList: args.assetList,
|
initialIndex: args.initialIndex,
|
||||||
asset: args.asset,
|
loadAsset: args.loadAsset,
|
||||||
|
totalAssets: args.totalAssets,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -150,18 +151,27 @@ class _$AppRouter extends RootStackRouter {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
AssetSelectionRoute.name: (routeData) {
|
AssetSelectionRoute.name: (routeData) {
|
||||||
|
final args = routeData.argsAs<AssetSelectionRouteArgs>();
|
||||||
return CustomPage<AssetSelectionPageResult?>(
|
return CustomPage<AssetSelectionPageResult?>(
|
||||||
routeData: routeData,
|
routeData: routeData,
|
||||||
child: const AssetSelectionPage(),
|
child: AssetSelectionPage(
|
||||||
|
key: args.key,
|
||||||
|
existingAssets: args.existingAssets,
|
||||||
|
isNewAlbum: args.isNewAlbum,
|
||||||
|
),
|
||||||
transitionsBuilder: TransitionsBuilders.slideBottom,
|
transitionsBuilder: TransitionsBuilders.slideBottom,
|
||||||
opaque: true,
|
opaque: true,
|
||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
SelectUserForSharingRoute.name: (routeData) {
|
SelectUserForSharingRoute.name: (routeData) {
|
||||||
|
final args = routeData.argsAs<SelectUserForSharingRouteArgs>();
|
||||||
return CustomPage<List<String>>(
|
return CustomPage<List<String>>(
|
||||||
routeData: routeData,
|
routeData: routeData,
|
||||||
child: const SelectUserForSharingPage(),
|
child: SelectUserForSharingPage(
|
||||||
|
key: args.key,
|
||||||
|
assets: args.assets,
|
||||||
|
),
|
||||||
transitionsBuilder: TransitionsBuilders.slideBottom,
|
transitionsBuilder: TransitionsBuilders.slideBottom,
|
||||||
opaque: true,
|
opaque: true,
|
||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
@ -582,15 +592,17 @@ class TabControllerRoute extends PageRouteInfo<void> {
|
|||||||
class GalleryViewerRoute extends PageRouteInfo<GalleryViewerRouteArgs> {
|
class GalleryViewerRoute extends PageRouteInfo<GalleryViewerRouteArgs> {
|
||||||
GalleryViewerRoute({
|
GalleryViewerRoute({
|
||||||
Key? key,
|
Key? key,
|
||||||
required List<Asset> assetList,
|
required int initialIndex,
|
||||||
required Asset asset,
|
required Asset Function(int) loadAsset,
|
||||||
|
required int totalAssets,
|
||||||
}) : super(
|
}) : super(
|
||||||
GalleryViewerRoute.name,
|
GalleryViewerRoute.name,
|
||||||
path: '/gallery-viewer-page',
|
path: '/gallery-viewer-page',
|
||||||
args: GalleryViewerRouteArgs(
|
args: GalleryViewerRouteArgs(
|
||||||
key: key,
|
key: key,
|
||||||
assetList: assetList,
|
initialIndex: initialIndex,
|
||||||
asset: asset,
|
loadAsset: loadAsset,
|
||||||
|
totalAssets: totalAssets,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -600,19 +612,22 @@ class GalleryViewerRoute extends PageRouteInfo<GalleryViewerRouteArgs> {
|
|||||||
class GalleryViewerRouteArgs {
|
class GalleryViewerRouteArgs {
|
||||||
const GalleryViewerRouteArgs({
|
const GalleryViewerRouteArgs({
|
||||||
this.key,
|
this.key,
|
||||||
required this.assetList,
|
required this.initialIndex,
|
||||||
required this.asset,
|
required this.loadAsset,
|
||||||
|
required this.totalAssets,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Key? key;
|
final Key? key;
|
||||||
|
|
||||||
final List<Asset> assetList;
|
final int initialIndex;
|
||||||
|
|
||||||
final Asset asset;
|
final Asset Function(int) loadAsset;
|
||||||
|
|
||||||
|
final int totalAssets;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'GalleryViewerRouteArgs{key: $key, assetList: $assetList, asset: $asset}';
|
return 'GalleryViewerRouteArgs{key: $key, initialIndex: $initialIndex, loadAsset: $loadAsset, totalAssets: $totalAssets}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -623,9 +638,9 @@ class VideoViewerRoute extends PageRouteInfo<VideoViewerRouteArgs> {
|
|||||||
Key? key,
|
Key? key,
|
||||||
required Asset asset,
|
required Asset asset,
|
||||||
required bool isMotionVideo,
|
required bool isMotionVideo,
|
||||||
required void Function() onVideoEnded,
|
required dynamic onVideoEnded,
|
||||||
void Function()? onPlaying,
|
dynamic onPlaying,
|
||||||
void Function()? onPaused,
|
dynamic onPaused,
|
||||||
}) : super(
|
}) : super(
|
||||||
VideoViewerRoute.name,
|
VideoViewerRoute.name,
|
||||||
path: '/video-viewer-page',
|
path: '/video-viewer-page',
|
||||||
@ -658,11 +673,11 @@ class VideoViewerRouteArgs {
|
|||||||
|
|
||||||
final bool isMotionVideo;
|
final bool isMotionVideo;
|
||||||
|
|
||||||
final void Function() onVideoEnded;
|
final dynamic onVideoEnded;
|
||||||
|
|
||||||
final void Function()? onPlaying;
|
final dynamic onPlaying;
|
||||||
|
|
||||||
final void Function()? onPaused;
|
final dynamic onPaused;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
@ -829,28 +844,78 @@ class RecentlyAddedRoute extends PageRouteInfo<void> {
|
|||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [AssetSelectionPage]
|
/// [AssetSelectionPage]
|
||||||
class AssetSelectionRoute extends PageRouteInfo<void> {
|
class AssetSelectionRoute extends PageRouteInfo<AssetSelectionRouteArgs> {
|
||||||
const AssetSelectionRoute()
|
AssetSelectionRoute({
|
||||||
: super(
|
Key? key,
|
||||||
|
required Set<Asset> existingAssets,
|
||||||
|
bool isNewAlbum = false,
|
||||||
|
}) : super(
|
||||||
AssetSelectionRoute.name,
|
AssetSelectionRoute.name,
|
||||||
path: '/asset-selection-page',
|
path: '/asset-selection-page',
|
||||||
|
args: AssetSelectionRouteArgs(
|
||||||
|
key: key,
|
||||||
|
existingAssets: existingAssets,
|
||||||
|
isNewAlbum: isNewAlbum,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
static const String name = 'AssetSelectionRoute';
|
static const String name = 'AssetSelectionRoute';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class AssetSelectionRouteArgs {
|
||||||
|
const AssetSelectionRouteArgs({
|
||||||
|
this.key,
|
||||||
|
required this.existingAssets,
|
||||||
|
this.isNewAlbum = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Key? key;
|
||||||
|
|
||||||
|
final Set<Asset> existingAssets;
|
||||||
|
|
||||||
|
final bool isNewAlbum;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'AssetSelectionRouteArgs{key: $key, existingAssets: $existingAssets, isNewAlbum: $isNewAlbum}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [SelectUserForSharingPage]
|
/// [SelectUserForSharingPage]
|
||||||
class SelectUserForSharingRoute extends PageRouteInfo<void> {
|
class SelectUserForSharingRoute
|
||||||
const SelectUserForSharingRoute()
|
extends PageRouteInfo<SelectUserForSharingRouteArgs> {
|
||||||
: super(
|
SelectUserForSharingRoute({
|
||||||
|
Key? key,
|
||||||
|
required Set<Asset> assets,
|
||||||
|
}) : super(
|
||||||
SelectUserForSharingRoute.name,
|
SelectUserForSharingRoute.name,
|
||||||
path: '/select-user-for-sharing-page',
|
path: '/select-user-for-sharing-page',
|
||||||
|
args: SelectUserForSharingRouteArgs(
|
||||||
|
key: key,
|
||||||
|
assets: assets,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
static const String name = 'SelectUserForSharingRoute';
|
static const String name = 'SelectUserForSharingRoute';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SelectUserForSharingRouteArgs {
|
||||||
|
const SelectUserForSharingRouteArgs({
|
||||||
|
this.key,
|
||||||
|
required this.assets,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Key? key;
|
||||||
|
|
||||||
|
final Set<Asset> assets;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'SelectUserForSharingRouteArgs{key: $key, assets: $assets}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [AlbumViewerPage]
|
/// [AlbumViewerPage]
|
||||||
class AlbumViewerRoute extends PageRouteInfo<AlbumViewerRouteArgs> {
|
class AlbumViewerRoute extends PageRouteInfo<AlbumViewerRouteArgs> {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/models/store.dart';
|
import 'package:immich_mobile/shared/models/store.dart';
|
||||||
import 'package:immich_mobile/shared/models/user.dart';
|
import 'package:immich_mobile/shared/models/user.dart';
|
||||||
@ -34,10 +35,10 @@ class Album {
|
|||||||
final IsarLinks<User> sharedUsers = IsarLinks<User>();
|
final IsarLinks<User> sharedUsers = IsarLinks<User>();
|
||||||
final IsarLinks<Asset> assets = IsarLinks<Asset>();
|
final IsarLinks<Asset> assets = IsarLinks<Asset>();
|
||||||
|
|
||||||
List<Asset> _sortedAssets = [];
|
RenderList _renderList = RenderList.empty();
|
||||||
|
|
||||||
@ignore
|
@ignore
|
||||||
List<Asset> get sortedAssets => _sortedAssets;
|
RenderList get renderList => _renderList;
|
||||||
|
|
||||||
@ignore
|
@ignore
|
||||||
bool get isRemote => remoteId != null;
|
bool get isRemote => remoteId != null;
|
||||||
@ -69,8 +70,14 @@ class Album {
|
|||||||
return name.join(' ');
|
return name.join(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> loadSortedAssets() async {
|
Stream<void> watchRenderList(GroupAssetsBy groupAssetsBy) async* {
|
||||||
_sortedAssets = await assets.filter().sortByFileCreatedAt().findAll();
|
final query = assets.filter().sortByFileCreatedAt();
|
||||||
|
_renderList = await RenderList.fromQuery(query, groupAssetsBy);
|
||||||
|
yield _renderList;
|
||||||
|
await for (final _ in query.watchLazy()) {
|
||||||
|
_renderList = await RenderList.fromQuery(query, groupAssetsBy);
|
||||||
|
yield _renderList;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -225,7 +225,6 @@ class Asset {
|
|||||||
a.isLocal && !isLocal ||
|
a.isLocal && !isLocal ||
|
||||||
width == null && a.width != null ||
|
width == null && a.width != null ||
|
||||||
height == null && a.height != null ||
|
height == null && a.height != null ||
|
||||||
exifInfo == null && a.exifInfo != null ||
|
|
||||||
livePhotoVideoId == null && a.livePhotoVideoId != null ||
|
livePhotoVideoId == null && a.livePhotoVideoId != null ||
|
||||||
!isRemote && a.isRemote && isFavorite != a.isFavorite ||
|
!isRemote && a.isRemote && isFavorite != a.isFavorite ||
|
||||||
!isRemote && a.isRemote && isArchived != a.isArchived;
|
!isRemote && a.isRemote && isArchived != a.isArchived;
|
||||||
|
@ -114,6 +114,45 @@ class ExifInfo {
|
|||||||
country: country ?? this.country,
|
country: country ?? this.country,
|
||||||
description: description ?? this.description,
|
description: description ?? this.description,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(other) {
|
||||||
|
if (other is! ExifInfo) return false;
|
||||||
|
return id == other.id &&
|
||||||
|
fileSize == other.fileSize &&
|
||||||
|
make == other.make &&
|
||||||
|
model == other.model &&
|
||||||
|
lens == other.lens &&
|
||||||
|
f == other.f &&
|
||||||
|
mm == other.mm &&
|
||||||
|
iso == other.iso &&
|
||||||
|
exposureSeconds == other.exposureSeconds &&
|
||||||
|
lat == other.lat &&
|
||||||
|
long == other.long &&
|
||||||
|
city == other.city &&
|
||||||
|
state == other.state &&
|
||||||
|
country == other.country &&
|
||||||
|
description == other.description;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@ignore
|
||||||
|
int get hashCode =>
|
||||||
|
id.hashCode ^
|
||||||
|
fileSize.hashCode ^
|
||||||
|
make.hashCode ^
|
||||||
|
model.hashCode ^
|
||||||
|
lens.hashCode ^
|
||||||
|
f.hashCode ^
|
||||||
|
mm.hashCode ^
|
||||||
|
iso.hashCode ^
|
||||||
|
exposureSeconds.hashCode ^
|
||||||
|
lat.hashCode ^
|
||||||
|
long.hashCode ^
|
||||||
|
city.hashCode ^
|
||||||
|
state.hashCode ^
|
||||||
|
country.hashCode ^
|
||||||
|
description.hashCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
double? _exposureTimeToSeconds(String? s) {
|
double? _exposureTimeToSeconds(String? s) {
|
||||||
|
@ -35,6 +35,10 @@ class Store {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Watches a specific key for changes
|
||||||
|
static Stream<T?> watch<T>(StoreKey<T> key) =>
|
||||||
|
_db.storeValues.watchObject(key.id).map((e) => e?._extract(key));
|
||||||
|
|
||||||
/// Returns the stored value for the given key (possibly null)
|
/// Returns the stored value for the given key (possibly null)
|
||||||
static T? tryGet<T>(StoreKey<T> key) => _cache[key.id];
|
static T? tryGet<T>(StoreKey<T> key) => _cache[key.id];
|
||||||
|
|
||||||
|
@ -3,18 +3,14 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
||||||
import 'package:immich_mobile/shared/models/exif_info.dart';
|
import 'package:immich_mobile/shared/models/exif_info.dart';
|
||||||
import 'package:immich_mobile/shared/models/store.dart';
|
import 'package:immich_mobile/shared/models/store.dart';
|
||||||
import 'package:immich_mobile/shared/models/user.dart';
|
|
||||||
import 'package:immich_mobile/shared/providers/db.provider.dart';
|
import 'package:immich_mobile/shared/providers/db.provider.dart';
|
||||||
import 'package:immich_mobile/shared/services/asset.service.dart';
|
import 'package:immich_mobile/shared/services/asset.service.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
|
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
|
||||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.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/modules/settings/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:immich_mobile/shared/services/sync.service.dart';
|
import 'package:immich_mobile/shared/services/sync.service.dart';
|
||||||
import 'package:immich_mobile/utils/async_mutex.dart';
|
|
||||||
import 'package:immich_mobile/utils/db.dart';
|
import 'package:immich_mobile/utils/db.dart';
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
@ -22,72 +18,23 @@ import 'package:photo_manager/photo_manager.dart';
|
|||||||
|
|
||||||
/// State does not contain archived assets.
|
/// State does not contain archived assets.
|
||||||
/// Use database provider if you want to access the isArchived assets
|
/// Use database provider if you want to access the isArchived assets
|
||||||
class AssetsState {
|
class AssetsState {}
|
||||||
final List<Asset> allAssets;
|
|
||||||
final RenderList? renderList;
|
|
||||||
|
|
||||||
AssetsState(this.allAssets, {this.renderList});
|
|
||||||
|
|
||||||
Future<AssetsState> withRenderDataStructure(
|
|
||||||
AssetGridLayoutParameters layout,
|
|
||||||
) async {
|
|
||||||
return AssetsState(
|
|
||||||
allAssets,
|
|
||||||
renderList: await RenderList.fromAssets(
|
|
||||||
allAssets,
|
|
||||||
layout,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
AssetsState withAdditionalAssets(List<Asset> toAdd) {
|
|
||||||
return AssetsState([...allAssets, ...toAdd]);
|
|
||||||
}
|
|
||||||
|
|
||||||
static AssetsState fromAssetList(List<Asset> assets) {
|
|
||||||
return AssetsState(assets);
|
|
||||||
}
|
|
||||||
|
|
||||||
static AssetsState empty() {
|
|
||||||
return AssetsState([]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class AssetNotifier extends StateNotifier<AssetsState> {
|
class AssetNotifier extends StateNotifier<AssetsState> {
|
||||||
final AssetService _assetService;
|
final AssetService _assetService;
|
||||||
final AppSettingsService _settingsService;
|
|
||||||
final AlbumService _albumService;
|
final AlbumService _albumService;
|
||||||
final SyncService _syncService;
|
final SyncService _syncService;
|
||||||
final Isar _db;
|
final Isar _db;
|
||||||
final log = Logger('AssetNotifier');
|
final log = Logger('AssetNotifier');
|
||||||
bool _getAllAssetInProgress = false;
|
bool _getAllAssetInProgress = false;
|
||||||
bool _deleteInProgress = false;
|
bool _deleteInProgress = false;
|
||||||
final AsyncMutex _stateUpdateLock = AsyncMutex();
|
|
||||||
|
|
||||||
AssetNotifier(
|
AssetNotifier(
|
||||||
this._assetService,
|
this._assetService,
|
||||||
this._settingsService,
|
|
||||||
this._albumService,
|
this._albumService,
|
||||||
this._syncService,
|
this._syncService,
|
||||||
this._db,
|
this._db,
|
||||||
) : super(AssetsState.fromAssetList([]));
|
) : super(AssetsState());
|
||||||
|
|
||||||
Future<void> _updateAssetsState(List<Asset> newAssetList) async {
|
|
||||||
final layout = AssetGridLayoutParameters(
|
|
||||||
_settingsService.getSetting(AppSettingsEnum.tilesPerRow),
|
|
||||||
_settingsService.getSetting(AppSettingsEnum.dynamicLayout),
|
|
||||||
GroupAssetsBy
|
|
||||||
.values[_settingsService.getSetting(AppSettingsEnum.groupAssetsBy)],
|
|
||||||
);
|
|
||||||
|
|
||||||
state = await AssetsState.fromAssetList(newAssetList)
|
|
||||||
.withRenderDataStructure(layout);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Just a little helper to trigger a rebuild of the state object
|
|
||||||
Future<void> rebuildAssetGridDataStructure() async {
|
|
||||||
await _updateAssetsState(state.allAssets);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> getAllAsset({bool clear = false}) async {
|
Future<void> getAllAsset({bool clear = false}) async {
|
||||||
if (_getAllAssetInProgress || _deleteInProgress) {
|
if (_getAllAssetInProgress || _deleteInProgress) {
|
||||||
@ -97,79 +44,32 @@ class AssetNotifier extends StateNotifier<AssetsState> {
|
|||||||
final stopwatch = Stopwatch()..start();
|
final stopwatch = Stopwatch()..start();
|
||||||
try {
|
try {
|
||||||
_getAllAssetInProgress = true;
|
_getAllAssetInProgress = true;
|
||||||
final User me = Store.get(StoreKey.currentUser);
|
|
||||||
if (clear) {
|
if (clear) {
|
||||||
await clearAssetsAndAlbums(_db);
|
await clearAssetsAndAlbums(_db);
|
||||||
log.info("Manual refresh requested, cleared assets and albums from db");
|
log.info("Manual refresh requested, cleared assets and albums from db");
|
||||||
} else if (_stateUpdateLock.enqueued <= 1) {
|
|
||||||
final int cachedCount = await _userAssetQuery(me.isarId).count();
|
|
||||||
if (cachedCount > 0 && cachedCount != state.allAssets.length) {
|
|
||||||
await _stateUpdateLock.run(
|
|
||||||
() async => _updateAssetsState(await _getUserAssets(me.isarId)),
|
|
||||||
);
|
|
||||||
log.info(
|
|
||||||
"Reading assets ${state.allAssets.length} from DB: ${stopwatch.elapsedMilliseconds}ms",
|
|
||||||
);
|
|
||||||
stopwatch.reset();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
final bool newRemote = await _assetService.refreshRemoteAssets();
|
final bool newRemote = await _assetService.refreshRemoteAssets();
|
||||||
final bool newLocal = await _albumService.refreshDeviceAlbums();
|
final bool newLocal = await _albumService.refreshDeviceAlbums();
|
||||||
debugPrint("newRemote: $newRemote, newLocal: $newLocal");
|
debugPrint("newRemote: $newRemote, newLocal: $newLocal");
|
||||||
log.info("Load assets: ${stopwatch.elapsedMilliseconds}ms");
|
log.info("Load assets: ${stopwatch.elapsedMilliseconds}ms");
|
||||||
stopwatch.reset();
|
|
||||||
if (!newRemote &&
|
|
||||||
!newLocal &&
|
|
||||||
state.allAssets.length == await _userAssetQuery(me.isarId).count()) {
|
|
||||||
log.info("state is already up-to-date");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
stopwatch.reset();
|
|
||||||
if (_stateUpdateLock.enqueued <= 1) {
|
|
||||||
_stateUpdateLock.run(() async {
|
|
||||||
final assets = await _getUserAssets(me.isarId);
|
|
||||||
if (!const ListEquality().equals(assets, state.allAssets)) {
|
|
||||||
log.info("setting new asset state");
|
|
||||||
await _updateAssetsState(assets);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
_getAllAssetInProgress = false;
|
_getAllAssetInProgress = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Asset>> _getUserAssets(int userId) =>
|
|
||||||
_userAssetQuery(userId).sortByFileCreatedAtDesc().findAll();
|
|
||||||
|
|
||||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> _userAssetQuery(
|
|
||||||
int userId,
|
|
||||||
) =>
|
|
||||||
_db.assets.filter().ownerIdEqualTo(userId).isArchivedEqualTo(false);
|
|
||||||
|
|
||||||
Future<void> clearAllAsset() {
|
Future<void> clearAllAsset() {
|
||||||
state = AssetsState.empty();
|
|
||||||
return clearAssetsAndAlbums(_db);
|
return clearAssetsAndAlbums(_db);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> onNewAssetUploaded(Asset newAsset) async {
|
Future<void> onNewAssetUploaded(Asset newAsset) async {
|
||||||
final bool ok = await _syncService.syncNewAssetToDb(newAsset);
|
// eTag on device is not valid after partially modifying the assets
|
||||||
if (ok && _stateUpdateLock.enqueued <= 1) {
|
Store.delete(StoreKey.assetETag);
|
||||||
// run this sequentially if there is at most 1 other task waiting
|
await _syncService.syncNewAssetToDb(newAsset);
|
||||||
await _stateUpdateLock.run(() async {
|
|
||||||
final userId = Store.get(StoreKey.currentUser).isarId;
|
|
||||||
final assets = await _getUserAssets(userId);
|
|
||||||
await _updateAssetsState(assets);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> deleteAssets(Set<Asset> deleteAssets) async {
|
Future<void> deleteAssets(Set<Asset> deleteAssets) async {
|
||||||
_deleteInProgress = true;
|
_deleteInProgress = true;
|
||||||
try {
|
try {
|
||||||
_updateAssetsState(
|
|
||||||
state.allAssets.whereNot(deleteAssets.contains).toList(),
|
|
||||||
);
|
|
||||||
final localDeleted = await _deleteLocalAssets(deleteAssets);
|
final localDeleted = await _deleteLocalAssets(deleteAssets);
|
||||||
final remoteDeleted = await _deleteRemoteAssets(deleteAssets);
|
final remoteDeleted = await _deleteRemoteAssets(deleteAssets);
|
||||||
if (localDeleted.isNotEmpty || remoteDeleted.isNotEmpty) {
|
if (localDeleted.isNotEmpty || remoteDeleted.isNotEmpty) {
|
||||||
@ -201,7 +101,7 @@ class AssetNotifier extends StateNotifier<AssetsState> {
|
|||||||
}
|
}
|
||||||
if (local.isNotEmpty) {
|
if (local.isNotEmpty) {
|
||||||
try {
|
try {
|
||||||
await PhotoManager.editor.deleteWithIds(local);
|
return await PhotoManager.editor.deleteWithIds(local);
|
||||||
} catch (e, stack) {
|
} catch (e, stack) {
|
||||||
log.severe("Failed to delete asset from device", e, stack);
|
log.severe("Failed to delete asset from device", e, stack);
|
||||||
}
|
}
|
||||||
@ -220,53 +120,25 @@ class AssetNotifier extends StateNotifier<AssetsState> {
|
|||||||
.map((a) => a.id);
|
.map((a) => a.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> toggleFavorite(Asset asset, bool status) async {
|
Future<void> toggleFavorite(List<Asset> assets, bool status) async {
|
||||||
final newAsset = await _assetService.changeFavoriteStatus(asset, status);
|
final newAssets = await _assetService.changeFavoriteStatus(assets, status);
|
||||||
|
for (Asset? newAsset in newAssets) {
|
||||||
if (newAsset == null) {
|
if (newAsset == null) {
|
||||||
log.severe("Change favorite status failed for asset ${asset.id}");
|
log.severe("Change favorite status failed for asset");
|
||||||
return asset.isFavorite;
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final index = state.allAssets.indexWhere((a) => asset.id == a.id);
|
|
||||||
if (index != -1) {
|
|
||||||
state.allAssets[index] = newAsset;
|
|
||||||
_updateAssetsState(state.allAssets);
|
|
||||||
}
|
|
||||||
|
|
||||||
return newAsset.isFavorite;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> toggleArchive(Iterable<Asset> assets, bool status) async {
|
Future<void> toggleArchive(List<Asset> assets, bool status) async {
|
||||||
final newAssets = await Future.wait(
|
final newAssets = await _assetService.changeArchiveStatus(assets, status);
|
||||||
assets.map((a) => _assetService.changeArchiveStatus(a, status)),
|
|
||||||
);
|
|
||||||
int i = 0;
|
int i = 0;
|
||||||
bool unArchived = false;
|
|
||||||
for (Asset oldAsset in assets) {
|
for (Asset oldAsset in assets) {
|
||||||
final newAsset = newAssets[i++];
|
final newAsset = newAssets[i++];
|
||||||
if (newAsset == null) {
|
if (newAsset == null) {
|
||||||
log.severe("Change archive status failed for asset ${oldAsset.id}");
|
log.severe("Change archive status failed for asset ${oldAsset.id}");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
final index = state.allAssets.indexWhere((a) => oldAsset.id == a.id);
|
|
||||||
if (newAsset.isArchived) {
|
|
||||||
// remove from state
|
|
||||||
if (index != -1) {
|
|
||||||
state.allAssets.removeAt(index);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// add to state is difficult because the list is sorted
|
|
||||||
unArchived = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (unArchived) {
|
|
||||||
final User me = Store.get(StoreKey.currentUser);
|
|
||||||
await _stateUpdateLock.run(
|
|
||||||
() async => _updateAssetsState(await _getUserAssets(me.isarId)),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
_updateAssetsState(state.allAssets);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -274,26 +146,53 @@ class AssetNotifier extends StateNotifier<AssetsState> {
|
|||||||
final assetProvider = StateNotifierProvider<AssetNotifier, AssetsState>((ref) {
|
final assetProvider = StateNotifierProvider<AssetNotifier, AssetsState>((ref) {
|
||||||
return AssetNotifier(
|
return AssetNotifier(
|
||||||
ref.watch(assetServiceProvider),
|
ref.watch(assetServiceProvider),
|
||||||
ref.watch(appSettingsServiceProvider),
|
|
||||||
ref.watch(albumServiceProvider),
|
ref.watch(albumServiceProvider),
|
||||||
ref.watch(syncServiceProvider),
|
ref.watch(syncServiceProvider),
|
||||||
ref.watch(dbProvider),
|
ref.watch(dbProvider),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
final assetGroupByMonthYearProvider = StateProvider((ref) {
|
final assetDetailProvider =
|
||||||
// TODO: remove `where` once temporary workaround is no longer needed (to only
|
StreamProvider.autoDispose.family<Asset, Asset>((ref, asset) async* {
|
||||||
// allow remote assets to be added to album). Keep `toList()` as to NOT sort
|
yield await ref.watch(assetServiceProvider).loadExif(asset);
|
||||||
// the original list/state
|
final db = ref.watch(dbProvider);
|
||||||
final assets =
|
await for (final a in db.assets.watchObject(asset.id)) {
|
||||||
ref.watch(assetProvider).allAssets.where((e) => e.isRemote).toList();
|
if (a != null) yield await ref.watch(assetServiceProvider).loadExif(a);
|
||||||
|
}
|
||||||
assets.sortByCompare<DateTime>(
|
});
|
||||||
(e) => e.fileCreatedAt,
|
|
||||||
(a, b) => b.compareTo(a),
|
final assetsProvider = StreamProvider.autoDispose<RenderList>((ref) async* {
|
||||||
);
|
final query = ref
|
||||||
|
.watch(dbProvider)
|
||||||
return assets.groupListsBy(
|
.assets
|
||||||
(element) => DateFormat('MMMM, y').format(element.fileCreatedAt.toLocal()),
|
.filter()
|
||||||
);
|
.ownerIdEqualTo(Store.get(StoreKey.currentUser).isarId)
|
||||||
|
.isArchivedEqualTo(false)
|
||||||
|
.sortByFileCreatedAtDesc();
|
||||||
|
final settings = ref.watch(appSettingsServiceProvider);
|
||||||
|
final groupBy =
|
||||||
|
GroupAssetsBy.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)];
|
||||||
|
yield await RenderList.fromQuery(query, groupBy);
|
||||||
|
await for (final _ in query.watchLazy()) {
|
||||||
|
yield await RenderList.fromQuery(query, groupBy);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
final remoteAssetsProvider =
|
||||||
|
StreamProvider.autoDispose<RenderList>((ref) async* {
|
||||||
|
final query = ref
|
||||||
|
.watch(dbProvider)
|
||||||
|
.assets
|
||||||
|
.where()
|
||||||
|
.remoteIdIsNotNull()
|
||||||
|
.filter()
|
||||||
|
.ownerIdEqualTo(Store.get(StoreKey.currentUser).isarId)
|
||||||
|
.sortByFileCreatedAt();
|
||||||
|
final settings = ref.watch(appSettingsServiceProvider);
|
||||||
|
final groupBy =
|
||||||
|
GroupAssetsBy.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)];
|
||||||
|
yield await RenderList.fromQuery(query, groupBy);
|
||||||
|
await for (final _ in query.watchLazy()) {
|
||||||
|
yield await RenderList.fromQuery(query, groupBy);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
@ -97,15 +97,18 @@ class AssetService {
|
|||||||
/// the exif info from the server (remote assets only)
|
/// the exif info from the server (remote assets only)
|
||||||
Future<Asset> loadExif(Asset a) async {
|
Future<Asset> loadExif(Asset a) async {
|
||||||
a.exifInfo ??= await _db.exifInfos.get(a.id);
|
a.exifInfo ??= await _db.exifInfos.get(a.id);
|
||||||
if (a.exifInfo?.iso == null) {
|
// fileSize is always filled on the server but not set on client
|
||||||
|
if (a.exifInfo?.fileSize == null) {
|
||||||
if (a.isRemote) {
|
if (a.isRemote) {
|
||||||
final dto = await _apiService.assetApi.getAssetById(a.remoteId!);
|
final dto = await _apiService.assetApi.getAssetById(a.remoteId!);
|
||||||
if (dto != null && dto.exifInfo != null) {
|
if (dto != null && dto.exifInfo != null) {
|
||||||
a.exifInfo = Asset.remote(dto).exifInfo!.copyWith(id: a.id);
|
final newExif = Asset.remote(dto).exifInfo!.copyWith(id: a.id);
|
||||||
if (a.isInDb) {
|
if (newExif != a.exifInfo) {
|
||||||
_db.writeTxn(() => a.put(_db));
|
if (a.isInDb) {
|
||||||
} else {
|
_db.writeTxn(() => a.put(_db));
|
||||||
debugPrint("[loadExif] parameter Asset is not from DB!");
|
} else {
|
||||||
|
debugPrint("[loadExif] parameter Asset is not from DB!");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -115,27 +118,39 @@ class AssetService {
|
|||||||
return a;
|
return a;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Asset?> updateAsset(
|
Future<List<Asset?>> updateAssets(
|
||||||
Asset asset,
|
List<Asset> assets,
|
||||||
UpdateAssetDto updateAssetDto,
|
UpdateAssetDto updateAssetDto,
|
||||||
) async {
|
) async {
|
||||||
final dto =
|
final List<AssetResponseDto?> dtos = await Future.wait(
|
||||||
await _apiService.assetApi.updateAsset(asset.remoteId!, updateAssetDto);
|
assets.map(
|
||||||
if (dto != null) {
|
(a) => _apiService.assetApi.updateAsset(a.remoteId!, updateAssetDto),
|
||||||
final updated = asset.updatedCopy(Asset.remote(dto));
|
),
|
||||||
if (updated.isInDb) {
|
);
|
||||||
await _db.writeTxn(() => updated.put(_db));
|
bool allInDb = true;
|
||||||
|
for (int i = 0; i < assets.length; i++) {
|
||||||
|
final dto = dtos[i], old = assets[i];
|
||||||
|
if (dto != null) {
|
||||||
|
final remote = Asset.remote(dto);
|
||||||
|
if (old.canUpdate(remote)) {
|
||||||
|
assets[i] = old.updatedCopy(remote);
|
||||||
|
}
|
||||||
|
allInDb &= assets[i].isInDb;
|
||||||
}
|
}
|
||||||
return updated;
|
|
||||||
}
|
}
|
||||||
return null;
|
final toUpdate = allInDb ? assets : assets.where((e) => e.isInDb).toList();
|
||||||
|
await _syncService.upsertAssetsWithExif(toUpdate);
|
||||||
|
return assets;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Asset?> changeFavoriteStatus(Asset asset, bool isFavorite) {
|
Future<List<Asset?>> changeFavoriteStatus(
|
||||||
return updateAsset(asset, UpdateAssetDto(isFavorite: isFavorite));
|
List<Asset> assets,
|
||||||
|
bool isFavorite,
|
||||||
|
) {
|
||||||
|
return updateAssets(assets, UpdateAssetDto(isFavorite: isFavorite));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Asset?> changeArchiveStatus(Asset asset, bool isArchive) {
|
Future<List<Asset?>> changeArchiveStatus(List<Asset> assets, bool isArchive) {
|
||||||
return updateAsset(asset, UpdateAssetDto(isArchived: isArchive));
|
return updateAssets(assets, UpdateAssetDto(isArchived: isArchive));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -172,7 +172,7 @@ class SyncService {
|
|||||||
final idsToDelete = diff.third.map((e) => e.id).toList();
|
final idsToDelete = diff.third.map((e) => e.id).toList();
|
||||||
try {
|
try {
|
||||||
await _db.writeTxn(() => _db.assets.deleteAll(idsToDelete));
|
await _db.writeTxn(() => _db.assets.deleteAll(idsToDelete));
|
||||||
await _upsertAssetsWithExif(diff.first + diff.second);
|
await upsertAssetsWithExif(diff.first + diff.second);
|
||||||
} on IsarError catch (e) {
|
} on IsarError catch (e) {
|
||||||
_log.severe("Failed to sync remote assets to db: $e");
|
_log.severe("Failed to sync remote assets to db: $e");
|
||||||
}
|
}
|
||||||
@ -272,7 +272,7 @@ class SyncService {
|
|||||||
|
|
||||||
// for shared album: put missing album assets into local DB
|
// for shared album: put missing album assets into local DB
|
||||||
final resultPair = await _linkWithExistingFromDb(toAdd);
|
final resultPair = await _linkWithExistingFromDb(toAdd);
|
||||||
await _upsertAssetsWithExif(resultPair.second);
|
await upsertAssetsWithExif(resultPair.second);
|
||||||
final assetsToLink = resultPair.first + resultPair.second;
|
final assetsToLink = resultPair.first + resultPair.second;
|
||||||
final usersToLink = (await _db.users.getAllById(userIdsToAdd)).cast<User>();
|
final usersToLink = (await _db.users.getAllById(userIdsToAdd)).cast<User>();
|
||||||
|
|
||||||
@ -329,7 +329,7 @@ class SyncService {
|
|||||||
// put missing album assets into local DB
|
// put missing album assets into local DB
|
||||||
final result = await _linkWithExistingFromDb(dto.getAssets());
|
final result = await _linkWithExistingFromDb(dto.getAssets());
|
||||||
existing.addAll(result.first);
|
existing.addAll(result.first);
|
||||||
await _upsertAssetsWithExif(result.second);
|
await upsertAssetsWithExif(result.second);
|
||||||
|
|
||||||
final Album a = await Album.remote(dto);
|
final Album a = await Album.remote(dto);
|
||||||
await _db.writeTxn(() => _db.albums.store(a));
|
await _db.writeTxn(() => _db.albums.store(a));
|
||||||
@ -540,7 +540,7 @@ class SyncService {
|
|||||||
_log.info(
|
_log.info(
|
||||||
"${result.first.length} assets already existed in DB, to upsert ${result.second.length}",
|
"${result.first.length} assets already existed in DB, to upsert ${result.second.length}",
|
||||||
);
|
);
|
||||||
await _upsertAssetsWithExif(result.second);
|
await upsertAssetsWithExif(result.second);
|
||||||
existing.addAll(result.first);
|
existing.addAll(result.first);
|
||||||
a.assets.addAll(result.first);
|
a.assets.addAll(result.first);
|
||||||
a.assets.addAll(result.second);
|
a.assets.addAll(result.second);
|
||||||
@ -600,7 +600,7 @@ class SyncService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Inserts or updates the assets in the database with their ExifInfo (if any)
|
/// Inserts or updates the assets in the database with their ExifInfo (if any)
|
||||||
Future<void> _upsertAssetsWithExif(List<Asset> assets) async {
|
Future<void> upsertAssetsWithExif(List<Asset> assets) async {
|
||||||
if (assets.isEmpty) {
|
if (assets.isEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -21,19 +21,19 @@ class ControlBoxButton extends StatelessWidget {
|
|||||||
Key? key,
|
Key? key,
|
||||||
required this.label,
|
required this.label,
|
||||||
required this.iconData,
|
required this.iconData,
|
||||||
required this.onPressed,
|
this.onPressed,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final String label;
|
final String label;
|
||||||
final IconData iconData;
|
final IconData iconData;
|
||||||
final Function onPressed;
|
final void Function()? onPressed;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MaterialButton(
|
return MaterialButton(
|
||||||
padding: const EdgeInsets.all(10),
|
padding: const EdgeInsets.all(10),
|
||||||
shape: const CircleBorder(),
|
shape: const CircleBorder(),
|
||||||
onPressed: () => onPressed(),
|
onPressed: onPressed,
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
extension DurationExtension on String {
|
extension DurationExtension on String {
|
||||||
Duration? toDuration() {
|
Duration? toDuration() {
|
||||||
try {
|
try {
|
||||||
@ -34,4 +36,12 @@ extension ListExtension<E> on List<E> {
|
|||||||
length = length == 0 ? 0 : j;
|
length = length == 0 ? 0 : j;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ListSlice<E> nestedSlice(int start, int end) {
|
||||||
|
if (this is ListSlice) {
|
||||||
|
final ListSlice<E> self = this as ListSlice<E>;
|
||||||
|
return ListSlice<E>(self.source, self.start + start, self.start + end);
|
||||||
|
}
|
||||||
|
return ListSlice<E>(this, start, end);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -60,11 +60,7 @@ void main() {
|
|||||||
test('test grouped check months', () async {
|
test('test grouped check months', () async {
|
||||||
final renderList = await RenderList.fromAssets(
|
final renderList = await RenderList.fromAssets(
|
||||||
assets,
|
assets,
|
||||||
AssetGridLayoutParameters(
|
GroupAssetsBy.day,
|
||||||
3,
|
|
||||||
false,
|
|
||||||
GroupAssetsBy.day,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Oct
|
// Oct
|
||||||
@ -78,32 +74,33 @@ void main() {
|
|||||||
// 5 Assets => 2 Rows
|
// 5 Assets => 2 Rows
|
||||||
// Day 1
|
// Day 1
|
||||||
// 5 Assets => 2 Rows
|
// 5 Assets => 2 Rows
|
||||||
expect(renderList.elements.length, 18);
|
expect(renderList.elements.length, 4);
|
||||||
expect(
|
expect(
|
||||||
renderList.elements[0].type,
|
renderList.elements[0].type,
|
||||||
RenderAssetGridElementType.monthTitle,
|
RenderAssetGridElementType.monthTitle,
|
||||||
);
|
);
|
||||||
expect(renderList.elements[0].date.month, 10);
|
expect(renderList.elements[0].date.month, 1);
|
||||||
expect(
|
expect(
|
||||||
renderList.elements[7].type,
|
renderList.elements[1].type,
|
||||||
|
RenderAssetGridElementType.groupDividerTitle,
|
||||||
|
);
|
||||||
|
expect(renderList.elements[1].date.month, 1);
|
||||||
|
expect(
|
||||||
|
renderList.elements[2].type,
|
||||||
RenderAssetGridElementType.monthTitle,
|
RenderAssetGridElementType.monthTitle,
|
||||||
);
|
);
|
||||||
expect(renderList.elements[7].date.month, 2);
|
expect(renderList.elements[2].date.month, 2);
|
||||||
expect(
|
expect(
|
||||||
renderList.elements[11].type,
|
renderList.elements[3].type,
|
||||||
RenderAssetGridElementType.monthTitle,
|
RenderAssetGridElementType.monthTitle,
|
||||||
);
|
);
|
||||||
expect(renderList.elements[11].date.month, 1);
|
expect(renderList.elements[3].date.month, 10);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('test grouped check types', () async {
|
test('test grouped check types', () async {
|
||||||
final renderList = await RenderList.fromAssets(
|
final renderList = await RenderList.fromAssets(
|
||||||
assets,
|
assets,
|
||||||
AssetGridLayoutParameters(
|
GroupAssetsBy.day,
|
||||||
5,
|
|
||||||
false,
|
|
||||||
GroupAssetsBy.day,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Oct
|
// Oct
|
||||||
@ -120,17 +117,8 @@ void main() {
|
|||||||
final types = [
|
final types = [
|
||||||
RenderAssetGridElementType.monthTitle,
|
RenderAssetGridElementType.monthTitle,
|
||||||
RenderAssetGridElementType.groupDividerTitle,
|
RenderAssetGridElementType.groupDividerTitle,
|
||||||
RenderAssetGridElementType.assetRow,
|
|
||||||
RenderAssetGridElementType.assetRow,
|
|
||||||
RenderAssetGridElementType.assetRow,
|
|
||||||
RenderAssetGridElementType.monthTitle,
|
RenderAssetGridElementType.monthTitle,
|
||||||
RenderAssetGridElementType.groupDividerTitle,
|
|
||||||
RenderAssetGridElementType.assetRow,
|
|
||||||
RenderAssetGridElementType.monthTitle,
|
RenderAssetGridElementType.monthTitle,
|
||||||
RenderAssetGridElementType.groupDividerTitle,
|
|
||||||
RenderAssetGridElementType.assetRow,
|
|
||||||
RenderAssetGridElementType.groupDividerTitle,
|
|
||||||
RenderAssetGridElementType.assetRow,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
expect(renderList.elements.length, types.length);
|
expect(renderList.elements.length, types.length);
|
||||||
|
@ -1,112 +0,0 @@
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:immich_mobile/modules/favorite/providers/favorite_provider.dart';
|
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
|
||||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
|
||||||
import 'package:mockito/annotations.dart';
|
|
||||||
import 'package:mockito/mockito.dart';
|
|
||||||
|
|
||||||
@GenerateNiceMocks([
|
|
||||||
MockSpec<AssetsState>(),
|
|
||||||
MockSpec<AssetNotifier>(),
|
|
||||||
])
|
|
||||||
import 'favorite_provider_test.mocks.dart';
|
|
||||||
|
|
||||||
Asset _getTestAsset(int id, bool favorite) {
|
|
||||||
final Asset a = Asset(
|
|
||||||
remoteId: id.toString(),
|
|
||||||
localId: id.toString(),
|
|
||||||
deviceId: 1,
|
|
||||||
ownerId: 1,
|
|
||||||
fileCreatedAt: DateTime.now(),
|
|
||||||
fileModifiedAt: DateTime.now(),
|
|
||||||
updatedAt: DateTime.now(),
|
|
||||||
isLocal: false,
|
|
||||||
durationInSeconds: 0,
|
|
||||||
type: AssetType.image,
|
|
||||||
fileName: '',
|
|
||||||
isFavorite: favorite,
|
|
||||||
isArchived: false,
|
|
||||||
);
|
|
||||||
a.id = id;
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
group("Test favoriteProvider", () {
|
|
||||||
late MockAssetsState assetsState;
|
|
||||||
late MockAssetNotifier assetNotifier;
|
|
||||||
late ProviderContainer container;
|
|
||||||
late StateNotifierProvider<FavoriteSelectionNotifier, Set<int>>
|
|
||||||
testFavoritesProvider;
|
|
||||||
|
|
||||||
setUp(
|
|
||||||
() {
|
|
||||||
assetsState = MockAssetsState();
|
|
||||||
assetNotifier = MockAssetNotifier();
|
|
||||||
container = ProviderContainer();
|
|
||||||
|
|
||||||
testFavoritesProvider =
|
|
||||||
StateNotifierProvider<FavoriteSelectionNotifier, Set<int>>((ref) {
|
|
||||||
return FavoriteSelectionNotifier(
|
|
||||||
assetsState,
|
|
||||||
assetNotifier,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
test("Empty favorites provider", () {
|
|
||||||
when(assetsState.allAssets).thenReturn([]);
|
|
||||||
expect(<int>{}, container.read(testFavoritesProvider));
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Non-empty favorites provider", () {
|
|
||||||
when(assetsState.allAssets).thenReturn([
|
|
||||||
_getTestAsset(1, false),
|
|
||||||
_getTestAsset(2, true),
|
|
||||||
_getTestAsset(3, false),
|
|
||||||
_getTestAsset(4, false),
|
|
||||||
_getTestAsset(5, true),
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(<int>{2, 5}, container.read(testFavoritesProvider));
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Toggle favorite", () {
|
|
||||||
when(assetNotifier.toggleFavorite(null, false))
|
|
||||||
.thenAnswer((_) async => false);
|
|
||||||
|
|
||||||
final testAsset1 = _getTestAsset(1, false);
|
|
||||||
final testAsset2 = _getTestAsset(2, true);
|
|
||||||
|
|
||||||
when(assetsState.allAssets).thenReturn([testAsset1, testAsset2]);
|
|
||||||
|
|
||||||
expect(<int>{2}, container.read(testFavoritesProvider));
|
|
||||||
|
|
||||||
container.read(testFavoritesProvider.notifier).toggleFavorite(testAsset2);
|
|
||||||
expect(<int>{}, container.read(testFavoritesProvider));
|
|
||||||
|
|
||||||
container.read(testFavoritesProvider.notifier).toggleFavorite(testAsset1);
|
|
||||||
expect(<int>{1}, container.read(testFavoritesProvider));
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Add favorites", () {
|
|
||||||
when(assetNotifier.toggleFavorite(null, false))
|
|
||||||
.thenAnswer((_) async => false);
|
|
||||||
|
|
||||||
when(assetsState.allAssets).thenReturn([]);
|
|
||||||
|
|
||||||
expect(<int>{}, container.read(testFavoritesProvider));
|
|
||||||
|
|
||||||
container.read(testFavoritesProvider.notifier).addToFavorites(
|
|
||||||
[
|
|
||||||
_getTestAsset(1, false),
|
|
||||||
_getTestAsset(2, false),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(<int>{1, 2}, container.read(testFavoritesProvider));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
@ -1,298 +0,0 @@
|
|||||||
// Mocks generated by Mockito 5.3.2 from annotations
|
|
||||||
// in immich_mobile/test/favorite_provider_test.dart.
|
|
||||||
// Do not manually edit this file.
|
|
||||||
|
|
||||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
|
||||||
import 'dart:async' as _i5;
|
|
||||||
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart' as _i7;
|
|
||||||
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart'
|
|
||||||
as _i6;
|
|
||||||
import 'package:immich_mobile/shared/models/asset.dart' as _i4;
|
|
||||||
import 'package:immich_mobile/shared/providers/asset.provider.dart' as _i2;
|
|
||||||
import 'package:logging/logging.dart' as _i3;
|
|
||||||
import 'package:mockito/mockito.dart' as _i1;
|
|
||||||
import 'package:state_notifier/state_notifier.dart' as _i8;
|
|
||||||
|
|
||||||
// ignore_for_file: type=lint
|
|
||||||
// ignore_for_file: avoid_redundant_argument_values
|
|
||||||
// ignore_for_file: avoid_setters_without_getters
|
|
||||||
// ignore_for_file: comment_references
|
|
||||||
// ignore_for_file: implementation_imports
|
|
||||||
// ignore_for_file: invalid_use_of_visible_for_testing_member
|
|
||||||
// ignore_for_file: prefer_const_constructors
|
|
||||||
// ignore_for_file: unnecessary_parenthesis
|
|
||||||
// ignore_for_file: camel_case_types
|
|
||||||
// ignore_for_file: subtype_of_sealed_class
|
|
||||||
|
|
||||||
class _FakeAssetsState_0 extends _i1.SmartFake implements _i2.AssetsState {
|
|
||||||
_FakeAssetsState_0(
|
|
||||||
Object parent,
|
|
||||||
Invocation parentInvocation,
|
|
||||||
) : super(
|
|
||||||
parent,
|
|
||||||
parentInvocation,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class _FakeLogger_1 extends _i1.SmartFake implements _i3.Logger {
|
|
||||||
_FakeLogger_1(
|
|
||||||
Object parent,
|
|
||||||
Invocation parentInvocation,
|
|
||||||
) : super(
|
|
||||||
parent,
|
|
||||||
parentInvocation,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A class which mocks [AssetsState].
|
|
||||||
///
|
|
||||||
/// See the documentation for Mockito's code generation for more information.
|
|
||||||
class MockAssetsState extends _i1.Mock implements _i2.AssetsState {
|
|
||||||
@override
|
|
||||||
List<_i4.Asset> get allAssets => (super.noSuchMethod(
|
|
||||||
Invocation.getter(#allAssets),
|
|
||||||
returnValue: <_i4.Asset>[],
|
|
||||||
returnValueForMissingStub: <_i4.Asset>[],
|
|
||||||
) as List<_i4.Asset>);
|
|
||||||
@override
|
|
||||||
_i5.Future<_i2.AssetsState> withRenderDataStructure(
|
|
||||||
_i6.AssetGridLayoutParameters? layout) =>
|
|
||||||
(super.noSuchMethod(
|
|
||||||
Invocation.method(
|
|
||||||
#withRenderDataStructure,
|
|
||||||
[layout],
|
|
||||||
),
|
|
||||||
returnValue: _i5.Future<_i2.AssetsState>.value(_FakeAssetsState_0(
|
|
||||||
this,
|
|
||||||
Invocation.method(
|
|
||||||
#withRenderDataStructure,
|
|
||||||
[layout],
|
|
||||||
),
|
|
||||||
)),
|
|
||||||
returnValueForMissingStub:
|
|
||||||
_i5.Future<_i2.AssetsState>.value(_FakeAssetsState_0(
|
|
||||||
this,
|
|
||||||
Invocation.method(
|
|
||||||
#withRenderDataStructure,
|
|
||||||
[layout],
|
|
||||||
),
|
|
||||||
)),
|
|
||||||
) as _i5.Future<_i2.AssetsState>);
|
|
||||||
@override
|
|
||||||
_i2.AssetsState withAdditionalAssets(List<_i4.Asset>? toAdd) =>
|
|
||||||
(super.noSuchMethod(
|
|
||||||
Invocation.method(
|
|
||||||
#withAdditionalAssets,
|
|
||||||
[toAdd],
|
|
||||||
),
|
|
||||||
returnValue: _FakeAssetsState_0(
|
|
||||||
this,
|
|
||||||
Invocation.method(
|
|
||||||
#withAdditionalAssets,
|
|
||||||
[toAdd],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
returnValueForMissingStub: _FakeAssetsState_0(
|
|
||||||
this,
|
|
||||||
Invocation.method(
|
|
||||||
#withAdditionalAssets,
|
|
||||||
[toAdd],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
) as _i2.AssetsState);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A class which mocks [AssetNotifier].
|
|
||||||
///
|
|
||||||
/// See the documentation for Mockito's code generation for more information.
|
|
||||||
class MockAssetNotifier extends _i1.Mock implements _i2.AssetNotifier {
|
|
||||||
@override
|
|
||||||
_i3.Logger get log => (super.noSuchMethod(
|
|
||||||
Invocation.getter(#log),
|
|
||||||
returnValue: _FakeLogger_1(
|
|
||||||
this,
|
|
||||||
Invocation.getter(#log),
|
|
||||||
),
|
|
||||||
returnValueForMissingStub: _FakeLogger_1(
|
|
||||||
this,
|
|
||||||
Invocation.getter(#log),
|
|
||||||
),
|
|
||||||
) as _i3.Logger);
|
|
||||||
@override
|
|
||||||
set onError(_i7.ErrorListener? _onError) => super.noSuchMethod(
|
|
||||||
Invocation.setter(
|
|
||||||
#onError,
|
|
||||||
_onError,
|
|
||||||
),
|
|
||||||
returnValueForMissingStub: null,
|
|
||||||
);
|
|
||||||
@override
|
|
||||||
bool get mounted => (super.noSuchMethod(
|
|
||||||
Invocation.getter(#mounted),
|
|
||||||
returnValue: false,
|
|
||||||
returnValueForMissingStub: false,
|
|
||||||
) as bool);
|
|
||||||
@override
|
|
||||||
_i5.Stream<_i2.AssetsState> get stream => (super.noSuchMethod(
|
|
||||||
Invocation.getter(#stream),
|
|
||||||
returnValue: _i5.Stream<_i2.AssetsState>.empty(),
|
|
||||||
returnValueForMissingStub: _i5.Stream<_i2.AssetsState>.empty(),
|
|
||||||
) as _i5.Stream<_i2.AssetsState>);
|
|
||||||
@override
|
|
||||||
_i2.AssetsState get state => (super.noSuchMethod(
|
|
||||||
Invocation.getter(#state),
|
|
||||||
returnValue: _FakeAssetsState_0(
|
|
||||||
this,
|
|
||||||
Invocation.getter(#state),
|
|
||||||
),
|
|
||||||
returnValueForMissingStub: _FakeAssetsState_0(
|
|
||||||
this,
|
|
||||||
Invocation.getter(#state),
|
|
||||||
),
|
|
||||||
) as _i2.AssetsState);
|
|
||||||
@override
|
|
||||||
set state(_i2.AssetsState? value) => super.noSuchMethod(
|
|
||||||
Invocation.setter(
|
|
||||||
#state,
|
|
||||||
value,
|
|
||||||
),
|
|
||||||
returnValueForMissingStub: null,
|
|
||||||
);
|
|
||||||
@override
|
|
||||||
_i2.AssetsState get debugState => (super.noSuchMethod(
|
|
||||||
Invocation.getter(#debugState),
|
|
||||||
returnValue: _FakeAssetsState_0(
|
|
||||||
this,
|
|
||||||
Invocation.getter(#debugState),
|
|
||||||
),
|
|
||||||
returnValueForMissingStub: _FakeAssetsState_0(
|
|
||||||
this,
|
|
||||||
Invocation.getter(#debugState),
|
|
||||||
),
|
|
||||||
) as _i2.AssetsState);
|
|
||||||
@override
|
|
||||||
bool get hasListeners => (super.noSuchMethod(
|
|
||||||
Invocation.getter(#hasListeners),
|
|
||||||
returnValue: false,
|
|
||||||
returnValueForMissingStub: false,
|
|
||||||
) as bool);
|
|
||||||
@override
|
|
||||||
_i5.Future<void> rebuildAssetGridDataStructure() => (super.noSuchMethod(
|
|
||||||
Invocation.method(
|
|
||||||
#rebuildAssetGridDataStructure,
|
|
||||||
[],
|
|
||||||
),
|
|
||||||
returnValue: _i5.Future<void>.value(),
|
|
||||||
returnValueForMissingStub: _i5.Future<void>.value(),
|
|
||||||
) as _i5.Future<void>);
|
|
||||||
@override
|
|
||||||
_i5.Future<void> getAllAsset({bool? clear = false}) => (super.noSuchMethod(
|
|
||||||
Invocation.method(
|
|
||||||
#getAllAsset,
|
|
||||||
[],
|
|
||||||
{#clear: clear},
|
|
||||||
),
|
|
||||||
returnValue: _i5.Future<void>.value(),
|
|
||||||
returnValueForMissingStub: _i5.Future<void>.value(),
|
|
||||||
) as _i5.Future<void>);
|
|
||||||
@override
|
|
||||||
_i5.Future<void> clearAllAsset() => (super.noSuchMethod(
|
|
||||||
Invocation.method(
|
|
||||||
#clearAllAsset,
|
|
||||||
[],
|
|
||||||
),
|
|
||||||
returnValue: _i5.Future<void>.value(),
|
|
||||||
returnValueForMissingStub: _i5.Future<void>.value(),
|
|
||||||
) as _i5.Future<void>);
|
|
||||||
@override
|
|
||||||
_i5.Future<void> onNewAssetUploaded(_i4.Asset? newAsset) =>
|
|
||||||
(super.noSuchMethod(
|
|
||||||
Invocation.method(
|
|
||||||
#onNewAssetUploaded,
|
|
||||||
[newAsset],
|
|
||||||
),
|
|
||||||
returnValue: _i5.Future<void>.value(),
|
|
||||||
returnValueForMissingStub: _i5.Future<void>.value(),
|
|
||||||
) as _i5.Future<void>);
|
|
||||||
@override
|
|
||||||
_i5.Future<void> deleteAssets(Set<_i4.Asset>? deleteAssets) =>
|
|
||||||
(super.noSuchMethod(
|
|
||||||
Invocation.method(
|
|
||||||
#deleteAssets,
|
|
||||||
[deleteAssets],
|
|
||||||
),
|
|
||||||
returnValue: _i5.Future<void>.value(),
|
|
||||||
returnValueForMissingStub: _i5.Future<void>.value(),
|
|
||||||
) as _i5.Future<void>);
|
|
||||||
@override
|
|
||||||
_i5.Future<bool> toggleFavorite(
|
|
||||||
_i4.Asset? asset,
|
|
||||||
bool? status,
|
|
||||||
) =>
|
|
||||||
(super.noSuchMethod(
|
|
||||||
Invocation.method(
|
|
||||||
#toggleFavorite,
|
|
||||||
[
|
|
||||||
asset,
|
|
||||||
status,
|
|
||||||
],
|
|
||||||
),
|
|
||||||
returnValue: _i5.Future<bool>.value(false),
|
|
||||||
returnValueForMissingStub: _i5.Future<bool>.value(false),
|
|
||||||
) as _i5.Future<bool>);
|
|
||||||
@override
|
|
||||||
_i5.Future<void> toggleArchive(
|
|
||||||
Iterable<_i4.Asset>? assets,
|
|
||||||
bool? status,
|
|
||||||
) =>
|
|
||||||
(super.noSuchMethod(
|
|
||||||
Invocation.method(
|
|
||||||
#toggleArchive,
|
|
||||||
[
|
|
||||||
assets,
|
|
||||||
status,
|
|
||||||
],
|
|
||||||
),
|
|
||||||
returnValue: _i5.Future<void>.value(),
|
|
||||||
returnValueForMissingStub: _i5.Future<void>.value(),
|
|
||||||
) as _i5.Future<void>);
|
|
||||||
@override
|
|
||||||
bool updateShouldNotify(
|
|
||||||
_i2.AssetsState? old,
|
|
||||||
_i2.AssetsState? current,
|
|
||||||
) =>
|
|
||||||
(super.noSuchMethod(
|
|
||||||
Invocation.method(
|
|
||||||
#updateShouldNotify,
|
|
||||||
[
|
|
||||||
old,
|
|
||||||
current,
|
|
||||||
],
|
|
||||||
),
|
|
||||||
returnValue: false,
|
|
||||||
returnValueForMissingStub: false,
|
|
||||||
) as bool);
|
|
||||||
@override
|
|
||||||
_i7.RemoveListener addListener(
|
|
||||||
_i8.Listener<_i2.AssetsState>? listener, {
|
|
||||||
bool? fireImmediately = true,
|
|
||||||
}) =>
|
|
||||||
(super.noSuchMethod(
|
|
||||||
Invocation.method(
|
|
||||||
#addListener,
|
|
||||||
[listener],
|
|
||||||
{#fireImmediately: fireImmediately},
|
|
||||||
),
|
|
||||||
returnValue: () {},
|
|
||||||
returnValueForMissingStub: () {},
|
|
||||||
) as _i7.RemoveListener);
|
|
||||||
@override
|
|
||||||
void dispose() => super.noSuchMethod(
|
|
||||||
Invocation.method(
|
|
||||||
#dispose,
|
|
||||||
[],
|
|
||||||
),
|
|
||||||
returnValueForMissingStub: null,
|
|
||||||
);
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user