mirror of
https://github.com/immich-app/immich.git
synced 2026-05-21 15:16:31 -04:00
refactor: yeet old timeline (#27666)
* refactor: yank old timeline # Conflicts: # mobile/lib/presentation/pages/editing/drift_edit.page.dart # mobile/lib/providers/websocket.provider.dart # mobile/lib/routing/router.dart * more cleanup * remove native code * chore: bump sqlite-data version * remove old background tasks from BGTaskSchedulerPermittedIdentifiers * rebase --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
This commit is contained in:
@@ -1,151 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/entities/album.entity.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/models/albums/album_search.model.dart';
|
||||
import 'package:immich_mobile/services/album.service.dart';
|
||||
|
||||
final isRefreshingRemoteAlbumProvider = StateProvider<bool>((ref) => false);
|
||||
|
||||
class AlbumNotifier extends StateNotifier<List<Album>> {
|
||||
AlbumNotifier(this.albumService, this.ref) : super([]) {
|
||||
albumService.getAllRemoteAlbums().then((value) {
|
||||
if (mounted) {
|
||||
state = value;
|
||||
}
|
||||
});
|
||||
|
||||
_streamSub = albumService.watchRemoteAlbums().listen((data) => state = data);
|
||||
}
|
||||
|
||||
final AlbumService albumService;
|
||||
final Ref ref;
|
||||
late final StreamSubscription<List<Album>> _streamSub;
|
||||
|
||||
Future<void> refreshRemoteAlbums() async {
|
||||
ref.read(isRefreshingRemoteAlbumProvider.notifier).state = true;
|
||||
await albumService.refreshRemoteAlbums();
|
||||
ref.read(isRefreshingRemoteAlbumProvider.notifier).state = false;
|
||||
}
|
||||
|
||||
Future<void> refreshDeviceAlbums() => albumService.refreshDeviceAlbums();
|
||||
|
||||
Future<bool> deleteAlbum(Album album) => albumService.deleteAlbum(album);
|
||||
|
||||
Future<Album?> createAlbum(String albumTitle, Set<Asset> assets) => albumService.createAlbum(albumTitle, assets, []);
|
||||
|
||||
Future<Album?> getAlbumByName(String albumName, {bool? remote, bool? shared, bool? owner}) =>
|
||||
albumService.getAlbumByName(albumName, remote: remote, shared: shared, owner: owner);
|
||||
|
||||
/// Create an album on the server with the same name as the selected album for backup
|
||||
/// First this will check if the album already exists on the server with name
|
||||
/// If it does not exist, it will create the album on the server
|
||||
Future<void> createSyncAlbum(String albumName) async {
|
||||
final album = await getAlbumByName(albumName, remote: true, owner: true);
|
||||
if (album != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await createAlbum(albumName, {});
|
||||
}
|
||||
|
||||
Future<bool> leaveAlbum(Album album) async {
|
||||
var res = await albumService.leaveAlbum(album);
|
||||
|
||||
if (res) {
|
||||
await deleteAlbum(album);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void searchAlbums(String searchTerm, QuickFilterMode filterMode) async {
|
||||
state = await albumService.search(searchTerm, filterMode);
|
||||
}
|
||||
|
||||
Future<void> addUsers(Album album, List<String> userIds) async {
|
||||
await albumService.addUsers(album, userIds);
|
||||
}
|
||||
|
||||
Future<bool> removeUser(Album album, UserDto user) async {
|
||||
final isRemoved = await albumService.removeUser(album, user);
|
||||
|
||||
if (isRemoved && album.sharedUsers.isEmpty) {
|
||||
state = state.where((element) => element.id != album.id).toList();
|
||||
}
|
||||
|
||||
return isRemoved;
|
||||
}
|
||||
|
||||
Future<void> addAssets(Album album, Iterable<Asset> assets) async {
|
||||
await albumService.addAssets(album, assets);
|
||||
}
|
||||
|
||||
Future<bool> removeAsset(Album album, Iterable<Asset> assets) async {
|
||||
return await albumService.removeAsset(album, assets);
|
||||
}
|
||||
|
||||
Future<bool> setActivitystatus(Album album, bool enabled) {
|
||||
return albumService.setActivityStatus(album, enabled);
|
||||
}
|
||||
|
||||
Future<Album?> toggleSortOrder(Album album) {
|
||||
final order = album.sortOrder == SortOrder.asc ? SortOrder.desc : SortOrder.asc;
|
||||
|
||||
return albumService.updateSortOrder(album, order);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_streamSub.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
final albumProvider = StateNotifierProvider.autoDispose<AlbumNotifier, List<Album>>((ref) {
|
||||
return AlbumNotifier(ref.watch(albumServiceProvider), ref);
|
||||
});
|
||||
|
||||
final albumWatcher = StreamProvider.autoDispose.family<Album, int>((ref, id) async* {
|
||||
final albumService = ref.watch(albumServiceProvider);
|
||||
|
||||
final album = await albumService.getAlbumById(id);
|
||||
if (album != null) {
|
||||
yield album;
|
||||
}
|
||||
|
||||
await for (final album in albumService.watchAlbum(id)) {
|
||||
if (album != null) {
|
||||
yield album;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
class LocalAlbumsNotifier extends StateNotifier<List<Album>> {
|
||||
LocalAlbumsNotifier(this.albumService) : super([]) {
|
||||
albumService.getAllLocalAlbums().then((value) {
|
||||
if (mounted) {
|
||||
state = value;
|
||||
}
|
||||
});
|
||||
|
||||
_streamSub = albumService.watchLocalAlbums().listen((data) => state = data);
|
||||
}
|
||||
|
||||
final AlbumService albumService;
|
||||
late final StreamSubscription<List<Album>> _streamSub;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_streamSub.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
final localAlbumsProvider = StateNotifierProvider.autoDispose<LocalAlbumsNotifier, List<Album>>((ref) {
|
||||
return LocalAlbumsNotifier(ref.watch(albumServiceProvider));
|
||||
});
|
||||
@@ -1,119 +1,19 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/entities/album.entity.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'album_sort_by_options.provider.g.dart';
|
||||
|
||||
typedef AlbumSortFn = List<Album> Function(List<Album> albums, bool isReverse);
|
||||
|
||||
class _AlbumSortHandlers {
|
||||
const _AlbumSortHandlers._();
|
||||
|
||||
static const AlbumSortFn created = _sortByCreated;
|
||||
static List<Album> _sortByCreated(List<Album> albums, bool isReverse) {
|
||||
final sorted = albums.sortedBy((album) => album.createdAt);
|
||||
return (isReverse ? sorted.reversed : sorted).toList();
|
||||
}
|
||||
|
||||
static const AlbumSortFn title = _sortByTitle;
|
||||
static List<Album> _sortByTitle(List<Album> albums, bool isReverse) {
|
||||
final sorted = albums.sortedBy((album) => album.name);
|
||||
return (isReverse ? sorted.reversed : sorted).toList();
|
||||
}
|
||||
|
||||
static const AlbumSortFn lastModified = _sortByLastModified;
|
||||
static List<Album> _sortByLastModified(List<Album> albums, bool isReverse) {
|
||||
final sorted = albums.sortedBy((album) => album.modifiedAt);
|
||||
return (isReverse ? sorted.reversed : sorted).toList();
|
||||
}
|
||||
|
||||
static const AlbumSortFn assetCount = _sortByAssetCount;
|
||||
static List<Album> _sortByAssetCount(List<Album> albums, bool isReverse) {
|
||||
final sorted = albums.sorted((a, b) => a.assetCount.compareTo(b.assetCount));
|
||||
return (isReverse ? sorted.reversed : sorted).toList();
|
||||
}
|
||||
|
||||
static const AlbumSortFn mostRecent = _sortByMostRecent;
|
||||
static List<Album> _sortByMostRecent(List<Album> albums, bool isReverse) {
|
||||
final sorted = albums.sorted((a, b) {
|
||||
if (a.endDate == null && b.endDate == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (a.endDate == null) {
|
||||
// Put nulls at the end for recent sorting
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (b.endDate == null) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Sort by descending recent date
|
||||
return b.endDate!.compareTo(a.endDate!);
|
||||
});
|
||||
return (isReverse ? sorted.reversed : sorted).toList();
|
||||
}
|
||||
|
||||
static const AlbumSortFn mostOldest = _sortByMostOldest;
|
||||
static List<Album> _sortByMostOldest(List<Album> albums, bool isReverse) {
|
||||
final sorted = albums.sorted((a, b) {
|
||||
if (a.startDate != null && b.startDate != null) {
|
||||
return a.startDate!.compareTo(b.startDate!);
|
||||
}
|
||||
if (a.startDate == null) return 1;
|
||||
if (b.startDate == null) return -1;
|
||||
return 0;
|
||||
});
|
||||
return (isReverse ? sorted.reversed : sorted).toList();
|
||||
}
|
||||
}
|
||||
|
||||
// Store index allows us to re-arrange the values without affecting the saved prefs
|
||||
enum AlbumSortMode {
|
||||
title(1, "library_page_sort_title", _AlbumSortHandlers.title, SortOrder.asc),
|
||||
assetCount(4, "library_page_sort_asset_count", _AlbumSortHandlers.assetCount, SortOrder.desc),
|
||||
lastModified(3, "library_page_sort_last_modified", _AlbumSortHandlers.lastModified, SortOrder.desc),
|
||||
created(0, "library_page_sort_created", _AlbumSortHandlers.created, SortOrder.desc),
|
||||
mostRecent(2, "sort_recent", _AlbumSortHandlers.mostRecent, SortOrder.desc),
|
||||
mostOldest(5, "sort_oldest", _AlbumSortHandlers.mostOldest, SortOrder.asc);
|
||||
title(1, "library_page_sort_title", SortOrder.asc),
|
||||
assetCount(4, "library_page_sort_asset_count", SortOrder.desc),
|
||||
lastModified(3, "library_page_sort_last_modified", SortOrder.desc),
|
||||
created(0, "library_page_sort_created", SortOrder.desc),
|
||||
mostRecent(2, "sort_recent", SortOrder.desc),
|
||||
mostOldest(5, "sort_oldest", SortOrder.asc);
|
||||
|
||||
final int storeIndex;
|
||||
final String label;
|
||||
final AlbumSortFn sortFn;
|
||||
final SortOrder defaultOrder;
|
||||
|
||||
const AlbumSortMode(this.storeIndex, this.label, this.sortFn, this.defaultOrder);
|
||||
const AlbumSortMode(this.storeIndex, this.label, this.defaultOrder);
|
||||
|
||||
SortOrder effectiveOrder(bool isReverse) => isReverse ? defaultOrder.reverse() : defaultOrder;
|
||||
}
|
||||
|
||||
@riverpod
|
||||
class AlbumSortByOptions extends _$AlbumSortByOptions {
|
||||
@override
|
||||
AlbumSortMode build() {
|
||||
final sortOpt = ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.selectedAlbumSortOrder);
|
||||
return AlbumSortMode.values.firstWhere((e) => e.storeIndex == sortOpt, orElse: () => AlbumSortMode.title);
|
||||
}
|
||||
|
||||
void changeSortMode(AlbumSortMode sortOption) {
|
||||
state = sortOption;
|
||||
ref.watch(appSettingsServiceProvider).setSetting(AppSettingsEnum.selectedAlbumSortOrder, sortOption.storeIndex);
|
||||
}
|
||||
}
|
||||
|
||||
@riverpod
|
||||
class AlbumSortOrder extends _$AlbumSortOrder {
|
||||
@override
|
||||
bool build() {
|
||||
return ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.selectedAlbumSortReverse);
|
||||
}
|
||||
|
||||
void changeSortDirection(bool isReverse) {
|
||||
state = isReverse;
|
||||
ref.watch(appSettingsServiceProvider).setSetting(AppSettingsEnum.selectedAlbumSortReverse, isReverse);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'album_sort_by_options.provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$albumSortByOptionsHash() =>
|
||||
r'dd8da5e730af555de1b86c3b157b6c93183523ac';
|
||||
|
||||
/// See also [AlbumSortByOptions].
|
||||
@ProviderFor(AlbumSortByOptions)
|
||||
final albumSortByOptionsProvider =
|
||||
AutoDisposeNotifierProvider<AlbumSortByOptions, AlbumSortMode>.internal(
|
||||
AlbumSortByOptions.new,
|
||||
name: r'albumSortByOptionsProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$albumSortByOptionsHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$AlbumSortByOptions = AutoDisposeNotifier<AlbumSortMode>;
|
||||
String _$albumSortOrderHash() => r'573dea45b4519e69386fc7104c72522e35713440';
|
||||
|
||||
/// See also [AlbumSortOrder].
|
||||
@ProviderFor(AlbumSortOrder)
|
||||
final albumSortOrderProvider =
|
||||
AutoDisposeNotifierProvider<AlbumSortOrder, bool>.internal(
|
||||
AlbumSortOrder.new,
|
||||
name: r'albumSortOrderProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$albumSortOrderHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$AlbumSortOrder = AutoDisposeNotifier<bool>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||
@@ -1,74 +0,0 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/album.entity.dart';
|
||||
import 'package:immich_mobile/models/albums/album_viewer_page_state.model.dart';
|
||||
import 'package:immich_mobile/services/album.service.dart';
|
||||
|
||||
class AlbumViewerNotifier extends StateNotifier<AlbumViewerPageState> {
|
||||
AlbumViewerNotifier(this.ref)
|
||||
: super(const AlbumViewerPageState(editTitleText: "", isEditAlbum: false, editDescriptionText: ""));
|
||||
|
||||
final Ref ref;
|
||||
|
||||
void enableEditAlbum() {
|
||||
state = state.copyWith(isEditAlbum: true);
|
||||
}
|
||||
|
||||
void disableEditAlbum() {
|
||||
state = state.copyWith(isEditAlbum: false);
|
||||
}
|
||||
|
||||
void setEditTitleText(String newTitle) {
|
||||
state = state.copyWith(editTitleText: newTitle);
|
||||
}
|
||||
|
||||
void setEditDescriptionText(String newDescription) {
|
||||
state = state.copyWith(editDescriptionText: newDescription);
|
||||
}
|
||||
|
||||
void remoteEditTitleText() {
|
||||
state = state.copyWith(editTitleText: "");
|
||||
}
|
||||
|
||||
void remoteEditDescriptionText() {
|
||||
state = state.copyWith(editDescriptionText: "");
|
||||
}
|
||||
|
||||
void resetState() {
|
||||
state = state.copyWith(editTitleText: "", isEditAlbum: false, editDescriptionText: "");
|
||||
}
|
||||
|
||||
Future<bool> changeAlbumTitle(Album album, String newAlbumTitle) async {
|
||||
AlbumService service = ref.watch(albumServiceProvider);
|
||||
|
||||
bool isSuccess = await service.changeTitleAlbum(album, newAlbumTitle);
|
||||
|
||||
if (isSuccess) {
|
||||
state = state.copyWith(editTitleText: "", isEditAlbum: false);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
state = state.copyWith(editTitleText: "", isEditAlbum: false);
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<bool> changeAlbumDescription(Album album, String newAlbumDescription) async {
|
||||
AlbumService service = ref.watch(albumServiceProvider);
|
||||
|
||||
bool isSuccess = await service.changeDescriptionAlbum(album, newAlbumDescription);
|
||||
|
||||
if (isSuccess) {
|
||||
state = state.copyWith(editDescriptionText: "", isEditAlbum: false);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
state = state.copyWith(editDescriptionText: "", isEditAlbum: false);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
final albumViewerProvider = StateNotifierProvider<AlbumViewerNotifier, AlbumViewerPageState>((ref) {
|
||||
return AlbumViewerNotifier(ref);
|
||||
});
|
||||
@@ -1,15 +0,0 @@
|
||||
import 'package:immich_mobile/entities/album.entity.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'current_album.provider.g.dart';
|
||||
|
||||
@riverpod
|
||||
class CurrentAlbum extends _$CurrentAlbum {
|
||||
@override
|
||||
Album? build() => null;
|
||||
|
||||
void set(Album? a) => state = a;
|
||||
}
|
||||
|
||||
/// Mock class for testing
|
||||
abstract class CurrentAlbumInternal extends _$CurrentAlbum {}
|
||||
@@ -1,14 +0,0 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/domain/services/user.service.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
|
||||
final otherUsersProvider = FutureProvider.autoDispose<List<UserDto>>((ref) async {
|
||||
UserService userService = ref.watch(userServiceProvider);
|
||||
final currentUser = ref.watch(currentUserProvider);
|
||||
|
||||
final allUsers = await userService.getAll();
|
||||
allUsers.removeWhere((u) => currentUser?.id == u.id);
|
||||
return allUsers;
|
||||
});
|
||||
@@ -5,28 +5,17 @@ import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/services/log.service.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||
import 'package:immich_mobile/models/backup/backup_state.model.dart';
|
||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/ios_background_settings.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
|
||||
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
|
||||
import 'package:immich_mobile/providers/memory.provider.dart';
|
||||
import 'package:immich_mobile/providers/notification_permission.provider.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/providers/tab.provider.dart';
|
||||
import 'package:immich_mobile/providers/websocket.provider.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/services/background.service.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
enum AppLifeCycleEnum { active, inactive, paused, resumed, detached, hidden }
|
||||
|
||||
@@ -87,43 +76,15 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
||||
final endpoint = await _ref.read(authProvider.notifier).setOpenApiServiceEndpoint();
|
||||
_log.info("Using server URL: $endpoint");
|
||||
|
||||
if (!Store.isBetaTimelineEnabled) {
|
||||
final permission = _ref.watch(galleryPermissionNotifier);
|
||||
if (permission.isGranted || permission.isLimited) {
|
||||
await _ref.read(backupProvider.notifier).resumeBackup();
|
||||
await _ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
|
||||
}
|
||||
}
|
||||
|
||||
await _ref.read(serverInfoProvider.notifier).getServerVersion();
|
||||
}
|
||||
|
||||
if (!Store.isBetaTimelineEnabled) {
|
||||
switch (_ref.read(tabProvider)) {
|
||||
case TabEnum.home:
|
||||
await _ref.read(assetProvider.notifier).getAllAsset();
|
||||
|
||||
case TabEnum.albums:
|
||||
await _ref.read(albumProvider.notifier).refreshRemoteAlbums();
|
||||
|
||||
case TabEnum.library:
|
||||
case TabEnum.search:
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
_ref.read(websocketProvider.notifier).connect();
|
||||
await _handleBetaTimelineResume();
|
||||
}
|
||||
_ref.read(websocketProvider.notifier).connect();
|
||||
await _handleBetaTimelineResume();
|
||||
|
||||
await _ref.read(notificationPermissionProvider.notifier).getNotificationPermission();
|
||||
|
||||
await _ref.read(galleryPermissionNotifier.notifier).getGalleryPermissionStatus();
|
||||
|
||||
if (!Store.isBetaTimelineEnabled) {
|
||||
await _ref.read(iOSBackgroundSettingsProvider.notifier).refresh();
|
||||
|
||||
_ref.invalidate(memoryFutureProvider);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _safeRun(Future<void> action, String debugName) async {
|
||||
@@ -139,7 +100,6 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
||||
}
|
||||
|
||||
Future<void> _handleBetaTimelineResume() async {
|
||||
_ref.read(backupProvider.notifier).cancelBackup();
|
||||
unawaited(_ref.read(backgroundWorkerLockServiceProvider).lock());
|
||||
|
||||
// Give isolates time to complete any ongoing database transactions
|
||||
@@ -218,9 +178,7 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
||||
_pauseOperation = Completer<void>();
|
||||
|
||||
try {
|
||||
if (Store.isBetaTimelineEnabled) {
|
||||
unawaited(_ref.read(backgroundWorkerLockServiceProvider).unlock());
|
||||
}
|
||||
unawaited(_ref.read(backgroundWorkerLockServiceProvider).unlock());
|
||||
await _performPause();
|
||||
} catch (e, stackTrace) {
|
||||
_log.severe("Error during app pause", e, stackTrace);
|
||||
@@ -234,14 +192,7 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
||||
|
||||
Future<void> _performPause() {
|
||||
if (_ref.read(authProvider).isAuthenticated) {
|
||||
if (!Store.isBetaTimelineEnabled) {
|
||||
// Do not cancel backup if manual upload is in progress
|
||||
if (_ref.read(backupProvider.notifier).backupProgress != BackUpProgressEnum.manualInProgress) {
|
||||
_ref.read(backupProvider.notifier).cancelBackup();
|
||||
}
|
||||
} else {
|
||||
_ref.read(driftBackupProvider.notifier).stopForegroundBackup();
|
||||
}
|
||||
_ref.read(driftBackupProvider.notifier).stopForegroundBackup();
|
||||
|
||||
_ref.read(websocketProvider.notifier).disconnect();
|
||||
}
|
||||
@@ -252,31 +203,12 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
||||
Future<void> handleAppDetached() async {
|
||||
state = AppLifeCycleEnum.detached;
|
||||
|
||||
if (Store.isBetaTimelineEnabled) {
|
||||
unawaited(_ref.read(backgroundWorkerLockServiceProvider).unlock());
|
||||
}
|
||||
unawaited(_ref.read(backgroundWorkerLockServiceProvider).unlock());
|
||||
|
||||
// Flush logs before closing database
|
||||
try {
|
||||
await LogService.I.flush();
|
||||
} catch (_) {}
|
||||
|
||||
// Close Isar database safely
|
||||
try {
|
||||
final isar = Isar.getInstance();
|
||||
if (isar != null && isar.isOpen) {
|
||||
await isar.close();
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
if (Store.isBetaTimelineEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// no guarantee this is called at all
|
||||
try {
|
||||
_ref.read(manualUploadProvider.notifier).cancelBackup();
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
void handleAppHidden() {
|
||||
|
||||
@@ -1,182 +0,0 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/services/user.service.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
|
||||
import 'package:immich_mobile/providers/memory.provider.dart';
|
||||
import 'package:immich_mobile/services/album.service.dart';
|
||||
import 'package:immich_mobile/services/asset.service.dart';
|
||||
import 'package:immich_mobile/services/etag.service.dart';
|
||||
import 'package:immich_mobile/services/exif.service.dart';
|
||||
import 'package:immich_mobile/services/sync.service.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:immich_mobile/utils/debug_print.dart';
|
||||
|
||||
final assetProvider = StateNotifierProvider<AssetNotifier, bool>((ref) {
|
||||
return AssetNotifier(
|
||||
ref.watch(assetServiceProvider),
|
||||
ref.watch(albumServiceProvider),
|
||||
ref.watch(userServiceProvider),
|
||||
ref.watch(syncServiceProvider),
|
||||
ref.watch(etagServiceProvider),
|
||||
ref.watch(exifServiceProvider),
|
||||
ref,
|
||||
);
|
||||
});
|
||||
|
||||
class AssetNotifier extends StateNotifier<bool> {
|
||||
final AssetService _assetService;
|
||||
final AlbumService _albumService;
|
||||
final UserService _userService;
|
||||
final SyncService _syncService;
|
||||
final ETagService _etagService;
|
||||
final ExifService _exifService;
|
||||
final Ref _ref;
|
||||
final log = Logger('AssetNotifier');
|
||||
bool _getAllAssetInProgress = false;
|
||||
bool _deleteInProgress = false;
|
||||
|
||||
AssetNotifier(
|
||||
this._assetService,
|
||||
this._albumService,
|
||||
this._userService,
|
||||
this._syncService,
|
||||
this._etagService,
|
||||
this._exifService,
|
||||
this._ref,
|
||||
) : super(false);
|
||||
|
||||
Future<void> getAllAsset({bool clear = false}) async {
|
||||
if (_getAllAssetInProgress || _deleteInProgress) {
|
||||
// guard against multiple calls to this method while it's still working
|
||||
return;
|
||||
}
|
||||
final stopwatch = Stopwatch()..start();
|
||||
try {
|
||||
_getAllAssetInProgress = true;
|
||||
state = true;
|
||||
if (clear) {
|
||||
await clearAllAssets();
|
||||
log.info("Manual refresh requested, cleared assets and albums from db");
|
||||
}
|
||||
final users = await _syncService.getUsersFromServer();
|
||||
bool changedUsers = false;
|
||||
if (users != null) {
|
||||
changedUsers = await _syncService.syncUsersFromServer(users);
|
||||
}
|
||||
final bool newRemote = await _assetService.refreshRemoteAssets();
|
||||
final bool newLocal = await _albumService.refreshDeviceAlbums();
|
||||
dPrint(() => "changedUsers: $changedUsers, newRemote: $newRemote, newLocal: $newLocal");
|
||||
if (newRemote) {
|
||||
_ref.invalidate(memoryFutureProvider);
|
||||
}
|
||||
|
||||
log.info("Load assets: ${stopwatch.elapsedMilliseconds}ms");
|
||||
} catch (error) {
|
||||
// If there is error in getting the remote assets, still showing the new local assets
|
||||
await _albumService.refreshDeviceAlbums();
|
||||
} finally {
|
||||
_getAllAssetInProgress = false;
|
||||
if (mounted) {
|
||||
state = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> clearAllAssets() async {
|
||||
await Store.delete(StoreKey.assetETag);
|
||||
await Future.wait([
|
||||
_assetService.clearTable(),
|
||||
_exifService.clearTable(),
|
||||
_albumService.clearTable(),
|
||||
_userService.deleteAll(),
|
||||
_etagService.clearTable(),
|
||||
]);
|
||||
}
|
||||
|
||||
Future<void> onNewAssetUploaded(Asset newAsset) async {
|
||||
// eTag on device is not valid after partially modifying the assets
|
||||
await Store.delete(StoreKey.assetETag);
|
||||
await _syncService.syncNewAssetToDb(newAsset);
|
||||
}
|
||||
|
||||
Future<bool> deleteLocalAssets(List<Asset> assets) async {
|
||||
_deleteInProgress = true;
|
||||
state = true;
|
||||
try {
|
||||
await _assetService.deleteLocalAssets(assets);
|
||||
return true;
|
||||
} catch (error) {
|
||||
log.severe("Failed to delete local assets", error);
|
||||
return false;
|
||||
} finally {
|
||||
_deleteInProgress = false;
|
||||
state = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete remote asset only
|
||||
///
|
||||
/// Default behavior is trashing the asset
|
||||
Future<bool> deleteRemoteAssets(Iterable<Asset> deleteAssets, {bool shouldDeletePermanently = false}) async {
|
||||
_deleteInProgress = true;
|
||||
state = true;
|
||||
try {
|
||||
await _assetService.deleteRemoteAssets(deleteAssets, shouldDeletePermanently: shouldDeletePermanently);
|
||||
return true;
|
||||
} catch (error) {
|
||||
log.severe("Failed to delete remote assets", error);
|
||||
return false;
|
||||
} finally {
|
||||
_deleteInProgress = false;
|
||||
state = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> deleteAssets(Iterable<Asset> deleteAssets, {bool force = false}) async {
|
||||
_deleteInProgress = true;
|
||||
state = true;
|
||||
try {
|
||||
await _assetService.deleteAssets(deleteAssets, shouldDeletePermanently: force);
|
||||
return true;
|
||||
} catch (error) {
|
||||
log.severe("Failed to delete assets", error);
|
||||
return false;
|
||||
} finally {
|
||||
_deleteInProgress = false;
|
||||
state = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> toggleFavorite(List<Asset> assets, [bool? status]) {
|
||||
status ??= !assets.every((a) => a.isFavorite);
|
||||
return _assetService.changeFavoriteStatus(assets, status);
|
||||
}
|
||||
|
||||
Future<void> toggleArchive(List<Asset> assets, [bool? status]) {
|
||||
status ??= !assets.every((a) => a.isArchived);
|
||||
return _assetService.changeArchiveStatus(assets, status);
|
||||
}
|
||||
|
||||
Future<void> setLockedView(List<Asset> selection, AssetVisibilityEnum visibility) {
|
||||
return _assetService.setVisibility(selection, visibility);
|
||||
}
|
||||
}
|
||||
|
||||
final assetDetailProvider = StreamProvider.autoDispose.family<Asset, Asset>((ref, asset) async* {
|
||||
final assetService = ref.watch(assetServiceProvider);
|
||||
yield await assetService.loadExif(asset);
|
||||
|
||||
await for (final asset in assetService.watchAsset(asset.id)) {
|
||||
if (asset != null) {
|
||||
yield await ref.watch(assetServiceProvider).loadExif(asset);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
final assetWatcher = StreamProvider.autoDispose.family<Asset?, Asset>((ref, asset) {
|
||||
final assetService = ref.watch(assetServiceProvider);
|
||||
return assetService.watchAsset(asset.id, fireImmediately: true);
|
||||
});
|
||||
@@ -1,49 +0,0 @@
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/services/asset.service.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'asset_people.provider.g.dart';
|
||||
|
||||
/// Maintains the list of people for an asset.
|
||||
@riverpod
|
||||
class AssetPeopleNotifier extends _$AssetPeopleNotifier {
|
||||
final log = Logger('AssetPeopleNotifier');
|
||||
|
||||
@override
|
||||
Future<List<PersonWithFacesResponseDto>> build(Asset asset) async {
|
||||
if (!asset.isRemote) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final list = await ref.watch(assetServiceProvider).getRemotePeopleOfAsset(asset.remoteId!);
|
||||
if (list == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// explicitly a sorted slice to make it deterministic
|
||||
// named people will be at the beginning, and names are sorted
|
||||
// ascendingly
|
||||
list.sort((a, b) {
|
||||
final aNotEmpty = a.name.isNotEmpty;
|
||||
final bNotEmpty = b.name.isNotEmpty;
|
||||
if (aNotEmpty && !bNotEmpty) {
|
||||
return -1;
|
||||
} else if (!aNotEmpty && bNotEmpty) {
|
||||
return 1;
|
||||
} else if (!aNotEmpty && !bNotEmpty) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return a.name.compareTo(b.name);
|
||||
});
|
||||
return list;
|
||||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
// invalidate the state – this way we don't have to
|
||||
// duplicate the code from build.
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
}
|
||||
@@ -1,192 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'asset_people.provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$assetPeopleNotifierHash() =>
|
||||
r'9835b180984a750c91e923e7b64dbda94f6d7574';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
_SystemHash._();
|
||||
|
||||
static int combine(int hash, int value) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + value);
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||
return hash ^ (hash >> 6);
|
||||
}
|
||||
|
||||
static int finish(int hash) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||
// ignore: parameter_assignments
|
||||
hash = hash ^ (hash >> 11);
|
||||
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _$AssetPeopleNotifier
|
||||
extends
|
||||
BuildlessAutoDisposeAsyncNotifier<List<PersonWithFacesResponseDto>> {
|
||||
late final Asset asset;
|
||||
|
||||
FutureOr<List<PersonWithFacesResponseDto>> build(Asset asset);
|
||||
}
|
||||
|
||||
/// Maintains the list of people for an asset.
|
||||
///
|
||||
/// Copied from [AssetPeopleNotifier].
|
||||
@ProviderFor(AssetPeopleNotifier)
|
||||
const assetPeopleNotifierProvider = AssetPeopleNotifierFamily();
|
||||
|
||||
/// Maintains the list of people for an asset.
|
||||
///
|
||||
/// Copied from [AssetPeopleNotifier].
|
||||
class AssetPeopleNotifierFamily
|
||||
extends Family<AsyncValue<List<PersonWithFacesResponseDto>>> {
|
||||
/// Maintains the list of people for an asset.
|
||||
///
|
||||
/// Copied from [AssetPeopleNotifier].
|
||||
const AssetPeopleNotifierFamily();
|
||||
|
||||
/// Maintains the list of people for an asset.
|
||||
///
|
||||
/// Copied from [AssetPeopleNotifier].
|
||||
AssetPeopleNotifierProvider call(Asset asset) {
|
||||
return AssetPeopleNotifierProvider(asset);
|
||||
}
|
||||
|
||||
@override
|
||||
AssetPeopleNotifierProvider getProviderOverride(
|
||||
covariant AssetPeopleNotifierProvider provider,
|
||||
) {
|
||||
return call(provider.asset);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'assetPeopleNotifierProvider';
|
||||
}
|
||||
|
||||
/// Maintains the list of people for an asset.
|
||||
///
|
||||
/// Copied from [AssetPeopleNotifier].
|
||||
class AssetPeopleNotifierProvider
|
||||
extends
|
||||
AutoDisposeAsyncNotifierProviderImpl<
|
||||
AssetPeopleNotifier,
|
||||
List<PersonWithFacesResponseDto>
|
||||
> {
|
||||
/// Maintains the list of people for an asset.
|
||||
///
|
||||
/// Copied from [AssetPeopleNotifier].
|
||||
AssetPeopleNotifierProvider(Asset asset)
|
||||
: this._internal(
|
||||
() => AssetPeopleNotifier()..asset = asset,
|
||||
from: assetPeopleNotifierProvider,
|
||||
name: r'assetPeopleNotifierProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$assetPeopleNotifierHash,
|
||||
dependencies: AssetPeopleNotifierFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
AssetPeopleNotifierFamily._allTransitiveDependencies,
|
||||
asset: asset,
|
||||
);
|
||||
|
||||
AssetPeopleNotifierProvider._internal(
|
||||
super._createNotifier, {
|
||||
required super.name,
|
||||
required super.dependencies,
|
||||
required super.allTransitiveDependencies,
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.asset,
|
||||
}) : super.internal();
|
||||
|
||||
final Asset asset;
|
||||
|
||||
@override
|
||||
FutureOr<List<PersonWithFacesResponseDto>> runNotifierBuild(
|
||||
covariant AssetPeopleNotifier notifier,
|
||||
) {
|
||||
return notifier.build(asset);
|
||||
}
|
||||
|
||||
@override
|
||||
Override overrideWith(AssetPeopleNotifier Function() create) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
override: AssetPeopleNotifierProvider._internal(
|
||||
() => create()..asset = asset,
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
asset: asset,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AutoDisposeAsyncNotifierProviderElement<
|
||||
AssetPeopleNotifier,
|
||||
List<PersonWithFacesResponseDto>
|
||||
>
|
||||
createElement() {
|
||||
return _AssetPeopleNotifierProviderElement(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is AssetPeopleNotifierProvider && other.asset == asset;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, asset.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
mixin AssetPeopleNotifierRef
|
||||
on AutoDisposeAsyncNotifierProviderRef<List<PersonWithFacesResponseDto>> {
|
||||
/// The parameter `asset` of this provider.
|
||||
Asset get asset;
|
||||
}
|
||||
|
||||
class _AssetPeopleNotifierProviderElement
|
||||
extends
|
||||
AutoDisposeAsyncNotifierProviderElement<
|
||||
AssetPeopleNotifier,
|
||||
List<PersonWithFacesResponseDto>
|
||||
>
|
||||
with AssetPeopleNotifierRef {
|
||||
_AssetPeopleNotifierProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
Asset get asset => (origin as AssetPeopleNotifierProvider).asset;
|
||||
}
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||
@@ -1,42 +0,0 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/services/asset.service.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'asset_stack.provider.g.dart';
|
||||
|
||||
class AssetStackNotifier extends StateNotifier<List<Asset>> {
|
||||
final AssetService assetService;
|
||||
final String _stackId;
|
||||
|
||||
AssetStackNotifier(this.assetService, this._stackId) : super([]) {
|
||||
_fetchStack(_stackId);
|
||||
}
|
||||
|
||||
void _fetchStack(String stackId) async {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
final stack = await assetService.getStackAssets(stackId);
|
||||
if (stack.isNotEmpty) {
|
||||
state = stack;
|
||||
}
|
||||
}
|
||||
|
||||
void removeChild(int index) {
|
||||
if (index < state.length) {
|
||||
state.removeAt(index);
|
||||
state = List<Asset>.from(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final assetStackStateProvider = StateNotifierProvider.autoDispose.family<AssetStackNotifier, List<Asset>, String>(
|
||||
(ref, stackId) => AssetStackNotifier(ref.watch(assetServiceProvider), stackId),
|
||||
);
|
||||
|
||||
@riverpod
|
||||
int assetStackIndex(Ref _) {
|
||||
return -1;
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'asset_stack.provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$assetStackIndexHash() => r'086ddb782e3eb38b80d755666fe35be8fe7322d7';
|
||||
|
||||
/// See also [assetStackIndex].
|
||||
@ProviderFor(assetStackIndex)
|
||||
final assetStackIndexProvider = AutoDisposeProvider<int>.internal(
|
||||
assetStackIndex,
|
||||
name: r'assetStackIndexProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$assetStackIndexHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
typedef AssetStackIndexRef = AutoDisposeProviderRef<int>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||
@@ -1,15 +0,0 @@
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'current_asset.provider.g.dart';
|
||||
|
||||
@riverpod
|
||||
class CurrentAsset extends _$CurrentAsset {
|
||||
@override
|
||||
Asset? build() => null;
|
||||
|
||||
void set(Asset? a) => state = a;
|
||||
}
|
||||
|
||||
/// Mock class for testing
|
||||
abstract class CurrentAssetInternal extends _$CurrentAsset {}
|
||||
@@ -1,26 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'current_asset.provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$currentAssetHash() => r'2def10ea594152c984ae2974d687ab6856d7bdd0';
|
||||
|
||||
/// See also [CurrentAsset].
|
||||
@ProviderFor(CurrentAsset)
|
||||
final currentAssetProvider =
|
||||
AutoDisposeNotifierProvider<CurrentAsset, Asset?>.internal(
|
||||
CurrentAsset.new,
|
||||
name: r'currentAssetProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$currentAssetHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$CurrentAsset = AutoDisposeNotifier<Asset?>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||
@@ -1,26 +1,15 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:background_downloader/background_downloader.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/models/download/download_state.model.dart';
|
||||
import 'package:immich_mobile/models/download/livephotos_medatada.model.dart';
|
||||
import 'package:immich_mobile/services/album.service.dart';
|
||||
import 'package:immich_mobile/services/download.service.dart';
|
||||
import 'package:immich_mobile/services/share.service.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
import 'package:immich_mobile/widgets/common/share_dialog.dart';
|
||||
|
||||
class DownloadStateNotifier extends StateNotifier<DownloadState> {
|
||||
final DownloadService _downloadService;
|
||||
final ShareService _shareService;
|
||||
final AlbumService _albumService;
|
||||
|
||||
DownloadStateNotifier(this._downloadService, this._shareService, this._albumService)
|
||||
DownloadStateNotifier(this._downloadService)
|
||||
: super(
|
||||
const DownloadState(
|
||||
downloadStatus: TaskStatus.complete,
|
||||
@@ -132,18 +121,9 @@ class DownloadStateNotifier extends StateNotifier<DownloadState> {
|
||||
if (state.taskProgress.isEmpty) {
|
||||
state = state.copyWith(showProgress: false);
|
||||
}
|
||||
_albumService.refreshDeviceAlbums();
|
||||
});
|
||||
}
|
||||
|
||||
Future<List<bool>> downloadAllAsset(List<Asset> assets) async {
|
||||
return await _downloadService.downloadAll(assets);
|
||||
}
|
||||
|
||||
void downloadAsset(Asset asset) async {
|
||||
await _downloadService.download(asset);
|
||||
}
|
||||
|
||||
void cancelDownload(String id) async {
|
||||
final isCanceled = await _downloadService.cancelDownload(id);
|
||||
|
||||
@@ -159,36 +139,8 @@ class DownloadStateNotifier extends StateNotifier<DownloadState> {
|
||||
state = state.copyWith(showProgress: false);
|
||||
}
|
||||
}
|
||||
|
||||
void shareAsset(Asset asset, BuildContext context) async {
|
||||
unawaited(
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext buildContext) {
|
||||
_shareService.shareAsset(asset, context).then((bool status) {
|
||||
if (!status) {
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: 'image_viewer_page_state_provider_share_error'.tr(),
|
||||
toastType: ToastType.error,
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
);
|
||||
}
|
||||
buildContext.pop();
|
||||
});
|
||||
return const ShareDialog();
|
||||
},
|
||||
barrierDismissible: false,
|
||||
useRootNavigator: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final downloadStateProvider = StateNotifierProvider<DownloadStateNotifier, DownloadState>(
|
||||
((ref) => DownloadStateNotifier(
|
||||
ref.watch(downloadServiceProvider),
|
||||
ref.watch(shareServiceProvider),
|
||||
ref.watch(albumServiceProvider),
|
||||
)),
|
||||
((ref) => DownloadStateNotifier(ref.watch(downloadServiceProvider))),
|
||||
);
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
enum RenderListStatusEnum { complete, empty, error, loading }
|
||||
|
||||
final renderListStatusProvider = StateNotifierProvider<RenderListStatus, RenderListStatusEnum>((ref) {
|
||||
return RenderListStatus(ref);
|
||||
});
|
||||
|
||||
class RenderListStatus extends StateNotifier<RenderListStatusEnum> {
|
||||
RenderListStatus(this.ref) : super(RenderListStatusEnum.complete);
|
||||
|
||||
final Ref ref;
|
||||
|
||||
RenderListStatusEnum get status => state;
|
||||
|
||||
set status(RenderListStatusEnum value) {
|
||||
state = value;
|
||||
}
|
||||
}
|
||||
@@ -1,672 +1,23 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/entities/album.entity.dart';
|
||||
import 'package:immich_mobile/entities/backup_album.entity.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/models/auth/auth_state.model.dart';
|
||||
import 'package:immich_mobile/models/backup/available_album.model.dart';
|
||||
import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
|
||||
import 'package:immich_mobile/models/backup/backup_state.model.dart';
|
||||
import 'package:immich_mobile/models/backup/current_upload_asset.model.dart';
|
||||
import 'package:immich_mobile/models/backup/error_upload_asset.model.dart';
|
||||
import 'package:immich_mobile/models/backup/success_upload_asset.model.dart';
|
||||
import 'package:immich_mobile/models/server_info/server_disk_info.model.dart';
|
||||
import 'package:immich_mobile/providers/app_life_cycle.provider.dart';
|
||||
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart';
|
||||
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
||||
import 'package:immich_mobile/repositories/album_media.repository.dart';
|
||||
import 'package:immich_mobile/repositories/backup.repository.dart';
|
||||
import 'package:immich_mobile/repositories/file_media.repository.dart';
|
||||
import 'package:immich_mobile/services/background.service.dart';
|
||||
import 'package:immich_mobile/services/backup.service.dart';
|
||||
import 'package:immich_mobile/services/backup_album.service.dart';
|
||||
import 'package:immich_mobile/services/server_info.service.dart';
|
||||
import 'package:immich_mobile/utils/backup_progress.dart';
|
||||
import 'package:immich_mobile/utils/diff.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:photo_manager/photo_manager.dart' show PMProgressHandler;
|
||||
import 'package:immich_mobile/utils/debug_print.dart';
|
||||
|
||||
final backupProvider = StateNotifierProvider<BackupNotifier, BackUpState>((ref) {
|
||||
return BackupNotifier(
|
||||
ref.watch(backupServiceProvider),
|
||||
ref.watch(serverInfoServiceProvider),
|
||||
ref.watch(authProvider),
|
||||
ref.watch(backgroundServiceProvider),
|
||||
ref.watch(galleryPermissionNotifier.notifier),
|
||||
ref.watch(albumMediaRepositoryProvider),
|
||||
ref.watch(fileMediaRepositoryProvider),
|
||||
ref.watch(backupAlbumServiceProvider),
|
||||
ref,
|
||||
);
|
||||
final backupProvider = StateNotifierProvider<BackupNotifier, ServerDiskInfo>((ref) {
|
||||
return BackupNotifier(ref.watch(serverInfoServiceProvider));
|
||||
});
|
||||
|
||||
class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
BackupNotifier(
|
||||
this._backupService,
|
||||
this._serverInfoService,
|
||||
this._authState,
|
||||
this._backgroundService,
|
||||
this._galleryPermissionNotifier,
|
||||
this._albumMediaRepository,
|
||||
this._fileMediaRepository,
|
||||
this._backupAlbumService,
|
||||
this.ref,
|
||||
) : super(
|
||||
BackUpState(
|
||||
backupProgress: BackUpProgressEnum.idle,
|
||||
allAssetsInDatabase: const [],
|
||||
progressInPercentage: 0,
|
||||
progressInFileSize: "0 B / 0 B",
|
||||
progressInFileSpeed: 0,
|
||||
progressInFileSpeeds: const [],
|
||||
progressInFileSpeedUpdateTime: DateTime.now(),
|
||||
progressInFileSpeedUpdateSentBytes: 0,
|
||||
autoBackup: Store.get(StoreKey.autoBackup, false),
|
||||
backgroundBackup: Store.get(StoreKey.backgroundBackup, false),
|
||||
backupRequireWifi: Store.get(StoreKey.backupRequireWifi, true),
|
||||
backupRequireCharging: Store.get(StoreKey.backupRequireCharging, false),
|
||||
backupTriggerDelay: Store.get(StoreKey.backupTriggerDelay, 5000),
|
||||
serverInfo: const ServerDiskInfo(diskAvailable: "0", diskSize: "0", diskUse: "0", diskUsagePercentage: 0),
|
||||
availableAlbums: const [],
|
||||
selectedBackupAlbums: const {},
|
||||
excludedBackupAlbums: const {},
|
||||
allUniqueAssets: const {},
|
||||
selectedAlbumsBackupAssetsIds: const {},
|
||||
currentUploadAsset: CurrentUploadAsset(
|
||||
id: '...',
|
||||
fileCreatedAt: DateTime.parse('2020-10-04'),
|
||||
fileName: '...',
|
||||
fileType: '...',
|
||||
fileSize: 0,
|
||||
iCloudAsset: false,
|
||||
),
|
||||
iCloudDownloadProgress: 0.0,
|
||||
),
|
||||
);
|
||||
class BackupNotifier extends StateNotifier<ServerDiskInfo> {
|
||||
BackupNotifier(this._serverInfoService)
|
||||
: super(const ServerDiskInfo(diskAvailable: "0", diskSize: "0", diskUse: "0", diskUsagePercentage: 0));
|
||||
|
||||
final log = Logger('BackupNotifier');
|
||||
final BackupService _backupService;
|
||||
final ServerInfoService _serverInfoService;
|
||||
final AuthState _authState;
|
||||
final BackgroundService _backgroundService;
|
||||
final GalleryPermissionNotifier _galleryPermissionNotifier;
|
||||
final AlbumMediaRepository _albumMediaRepository;
|
||||
final FileMediaRepository _fileMediaRepository;
|
||||
final BackupAlbumService _backupAlbumService;
|
||||
final Ref ref;
|
||||
Completer<void>? _cancelToken;
|
||||
|
||||
///
|
||||
/// UI INTERACTION
|
||||
///
|
||||
/// Album selection
|
||||
/// Due to the overlapping assets across multiple albums on the device
|
||||
/// We have method to include and exclude albums
|
||||
/// The total unique assets will be used for backing mechanism
|
||||
///
|
||||
void addAlbumForBackup(AvailableAlbum album) {
|
||||
if (state.excludedBackupAlbums.contains(album)) {
|
||||
removeExcludedAlbumForBackup(album);
|
||||
}
|
||||
|
||||
state = state.copyWith(selectedBackupAlbums: {...state.selectedBackupAlbums, album});
|
||||
}
|
||||
|
||||
void addExcludedAlbumForBackup(AvailableAlbum album) {
|
||||
if (state.selectedBackupAlbums.contains(album)) {
|
||||
removeAlbumForBackup(album);
|
||||
}
|
||||
state = state.copyWith(excludedBackupAlbums: {...state.excludedBackupAlbums, album});
|
||||
}
|
||||
|
||||
void removeAlbumForBackup(AvailableAlbum album) {
|
||||
Set<AvailableAlbum> currentSelectedAlbums = state.selectedBackupAlbums;
|
||||
|
||||
currentSelectedAlbums.removeWhere((a) => a == album);
|
||||
|
||||
state = state.copyWith(selectedBackupAlbums: currentSelectedAlbums);
|
||||
}
|
||||
|
||||
void removeExcludedAlbumForBackup(AvailableAlbum album) {
|
||||
Set<AvailableAlbum> currentExcludedAlbums = state.excludedBackupAlbums;
|
||||
|
||||
currentExcludedAlbums.removeWhere((a) => a == album);
|
||||
|
||||
state = state.copyWith(excludedBackupAlbums: currentExcludedAlbums);
|
||||
}
|
||||
|
||||
Future<void> backupAlbumSelectionDone() {
|
||||
if (state.selectedBackupAlbums.isEmpty) {
|
||||
// disable any backup
|
||||
cancelBackup();
|
||||
setAutoBackup(false);
|
||||
configureBackgroundBackup(enabled: false, onError: (msg) {}, onBatteryInfo: () {});
|
||||
}
|
||||
return _updateBackupAssetCount();
|
||||
}
|
||||
|
||||
void setAutoBackup(bool enabled) {
|
||||
Store.put(StoreKey.autoBackup, enabled);
|
||||
state = state.copyWith(autoBackup: enabled);
|
||||
}
|
||||
|
||||
void configureBackgroundBackup({
|
||||
bool? enabled,
|
||||
bool? requireWifi,
|
||||
bool? requireCharging,
|
||||
int? triggerDelay,
|
||||
required void Function(String msg) onError,
|
||||
required void Function() onBatteryInfo,
|
||||
}) async {
|
||||
assert(enabled != null || requireWifi != null || requireCharging != null || triggerDelay != null);
|
||||
final bool wasEnabled = state.backgroundBackup;
|
||||
final bool wasWifi = state.backupRequireWifi;
|
||||
final bool wasCharging = state.backupRequireCharging;
|
||||
final int oldTriggerDelay = state.backupTriggerDelay;
|
||||
state = state.copyWith(
|
||||
backgroundBackup: enabled,
|
||||
backupRequireWifi: requireWifi,
|
||||
backupRequireCharging: requireCharging,
|
||||
backupTriggerDelay: triggerDelay,
|
||||
);
|
||||
|
||||
if (state.backgroundBackup) {
|
||||
bool success = true;
|
||||
if (!wasEnabled) {
|
||||
if (!await _backgroundService.isIgnoringBatteryOptimizations()) {
|
||||
onBatteryInfo();
|
||||
}
|
||||
success &= await _backgroundService.enableService(immediate: true);
|
||||
}
|
||||
success &=
|
||||
success &&
|
||||
await _backgroundService.configureService(
|
||||
requireUnmetered: state.backupRequireWifi,
|
||||
requireCharging: state.backupRequireCharging,
|
||||
triggerUpdateDelay: state.backupTriggerDelay,
|
||||
triggerMaxDelay: state.backupTriggerDelay * 10,
|
||||
);
|
||||
if (success) {
|
||||
await Store.put(StoreKey.backupRequireWifi, state.backupRequireWifi);
|
||||
await Store.put(StoreKey.backupRequireCharging, state.backupRequireCharging);
|
||||
await Store.put(StoreKey.backupTriggerDelay, state.backupTriggerDelay);
|
||||
await Store.put(StoreKey.backgroundBackup, state.backgroundBackup);
|
||||
} else {
|
||||
state = state.copyWith(
|
||||
backgroundBackup: wasEnabled,
|
||||
backupRequireWifi: wasWifi,
|
||||
backupRequireCharging: wasCharging,
|
||||
backupTriggerDelay: oldTriggerDelay,
|
||||
);
|
||||
onError("backup_controller_page_background_configure_error");
|
||||
}
|
||||
} else {
|
||||
final bool success = await _backgroundService.disableService();
|
||||
if (!success) {
|
||||
state = state.copyWith(backgroundBackup: wasEnabled);
|
||||
onError("backup_controller_page_background_configure_error");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// Get all album on the device
|
||||
/// Get all selected and excluded album from the user's persistent storage
|
||||
/// If this is the first time performing backup - set the default selected album to be
|
||||
/// the one that has all assets (`Recent` on Android, `Recents` on iOS)
|
||||
///
|
||||
Future<void> _getBackupAlbumsInfo() async {
|
||||
Stopwatch stopwatch = Stopwatch()..start();
|
||||
// Get all albums on the device
|
||||
List<AvailableAlbum> availableAlbums = [];
|
||||
List<Album> albums = await _albumMediaRepository.getAll();
|
||||
|
||||
// Map of id -> album for quick album lookup later on.
|
||||
Map<String, Album> albumMap = {};
|
||||
|
||||
log.info('Found ${albums.length} local albums');
|
||||
|
||||
for (Album album in albums) {
|
||||
AvailableAlbum availableAlbum = AvailableAlbum(
|
||||
album: album,
|
||||
assetCount: await ref.read(albumMediaRepositoryProvider).getAssetCount(album.localId!),
|
||||
);
|
||||
|
||||
availableAlbums.add(availableAlbum);
|
||||
|
||||
albumMap[album.localId!] = album;
|
||||
}
|
||||
state = state.copyWith(availableAlbums: availableAlbums);
|
||||
|
||||
final List<BackupAlbum> excludedBackupAlbums = await _backupAlbumService.getAllBySelection(BackupSelection.exclude);
|
||||
final List<BackupAlbum> selectedBackupAlbums = await _backupAlbumService.getAllBySelection(BackupSelection.select);
|
||||
|
||||
final Set<AvailableAlbum> selectedAlbums = {};
|
||||
for (final BackupAlbum ba in selectedBackupAlbums) {
|
||||
final albumAsset = albumMap[ba.id];
|
||||
|
||||
if (albumAsset != null) {
|
||||
selectedAlbums.add(
|
||||
AvailableAlbum(
|
||||
album: albumAsset,
|
||||
assetCount: await _albumMediaRepository.getAssetCount(albumAsset.localId!),
|
||||
lastBackup: ba.lastBackup,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
log.severe('Selected album not found');
|
||||
}
|
||||
}
|
||||
|
||||
final Set<AvailableAlbum> excludedAlbums = {};
|
||||
for (final BackupAlbum ba in excludedBackupAlbums) {
|
||||
final albumAsset = albumMap[ba.id];
|
||||
|
||||
if (albumAsset != null) {
|
||||
excludedAlbums.add(
|
||||
AvailableAlbum(
|
||||
album: albumAsset,
|
||||
assetCount: await ref.read(albumMediaRepositoryProvider).getAssetCount(albumAsset.localId!),
|
||||
lastBackup: ba.lastBackup,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
log.severe('Excluded album not found');
|
||||
}
|
||||
}
|
||||
|
||||
state = state.copyWith(selectedBackupAlbums: selectedAlbums, excludedBackupAlbums: excludedAlbums);
|
||||
|
||||
log.info("_getBackupAlbumsInfo: Found ${availableAlbums.length} available albums");
|
||||
dPrint(() => "_getBackupAlbumsInfo takes ${stopwatch.elapsedMilliseconds}ms");
|
||||
}
|
||||
|
||||
///
|
||||
/// From all the selected and albums assets
|
||||
/// Find the assets that are not overlapping between the two sets
|
||||
/// Those assets are unique and are used as the total assets
|
||||
///
|
||||
Future<void> _updateBackupAssetCount() async {
|
||||
// Save to persistent storage
|
||||
await _updatePersistentAlbumsSelection();
|
||||
|
||||
final duplicatedAssetIds = await _backupService.getDuplicatedAssetIds();
|
||||
final Set<BackupCandidate> assetsFromSelectedAlbums = {};
|
||||
final Set<BackupCandidate> assetsFromExcludedAlbums = {};
|
||||
|
||||
for (final album in state.selectedBackupAlbums) {
|
||||
final assetCount = await ref.read(albumMediaRepositoryProvider).getAssetCount(album.album.localId!);
|
||||
|
||||
if (assetCount == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final assets = await ref.read(albumMediaRepositoryProvider).getAssets(album.album.localId!);
|
||||
|
||||
// Add album's name to the asset info
|
||||
for (final asset in assets) {
|
||||
List<String> albumNames = [album.name];
|
||||
|
||||
final existingAsset = assetsFromSelectedAlbums.firstWhereOrNull((a) => a.asset.localId == asset.localId);
|
||||
|
||||
if (existingAsset != null) {
|
||||
albumNames.addAll(existingAsset.albumNames);
|
||||
assetsFromSelectedAlbums.remove(existingAsset);
|
||||
}
|
||||
|
||||
assetsFromSelectedAlbums.add(BackupCandidate(asset: asset, albumNames: albumNames));
|
||||
}
|
||||
}
|
||||
|
||||
for (final album in state.excludedBackupAlbums) {
|
||||
final assetCount = await ref.read(albumMediaRepositoryProvider).getAssetCount(album.album.localId!);
|
||||
|
||||
if (assetCount == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final assets = await ref.read(albumMediaRepositoryProvider).getAssets(album.album.localId!);
|
||||
|
||||
for (final asset in assets) {
|
||||
assetsFromExcludedAlbums.add(BackupCandidate(asset: asset, albumNames: [album.name]));
|
||||
}
|
||||
}
|
||||
|
||||
final Set<BackupCandidate> allUniqueAssets = assetsFromSelectedAlbums.difference(assetsFromExcludedAlbums);
|
||||
|
||||
final allAssetsInDatabase = await _backupService.getDeviceBackupAsset();
|
||||
|
||||
if (allAssetsInDatabase == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find asset that were backup from selected albums
|
||||
final Set<String> selectedAlbumsBackupAssets = Set.from(allUniqueAssets.map((e) => e.asset.localId));
|
||||
|
||||
selectedAlbumsBackupAssets.removeWhere((assetId) => !allAssetsInDatabase.contains(assetId));
|
||||
|
||||
// Remove duplicated asset from all unique assets
|
||||
allUniqueAssets.removeWhere((candidate) => duplicatedAssetIds.contains(candidate.asset.localId));
|
||||
|
||||
if (allUniqueAssets.isEmpty) {
|
||||
log.info("No assets are selected for back up");
|
||||
state = state.copyWith(
|
||||
backupProgress: BackUpProgressEnum.idle,
|
||||
allAssetsInDatabase: allAssetsInDatabase,
|
||||
allUniqueAssets: {},
|
||||
selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssets,
|
||||
);
|
||||
} else {
|
||||
state = state.copyWith(
|
||||
allAssetsInDatabase: allAssetsInDatabase,
|
||||
allUniqueAssets: allUniqueAssets,
|
||||
selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssets,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all necessary information for calculating the available albums,
|
||||
/// which albums are selected or excluded
|
||||
/// and then update the UI according to those information
|
||||
Future<void> getBackupInfo() async {
|
||||
final isEnabled = await _backgroundService.isBackgroundBackupEnabled();
|
||||
|
||||
state = state.copyWith(backgroundBackup: isEnabled);
|
||||
if (isEnabled != Store.get(StoreKey.backgroundBackup, !isEnabled)) {
|
||||
await Store.put(StoreKey.backgroundBackup, isEnabled);
|
||||
}
|
||||
|
||||
if (state.backupProgress != BackUpProgressEnum.inBackground) {
|
||||
await _getBackupAlbumsInfo();
|
||||
await updateDiskInfo();
|
||||
await _updateBackupAssetCount();
|
||||
} else {
|
||||
log.warning("cannot get backup info - background backup is in progress!");
|
||||
}
|
||||
}
|
||||
|
||||
/// Save user selection of selected albums and excluded albums to database
|
||||
Future<void> _updatePersistentAlbumsSelection() async {
|
||||
final epoch = DateTime.fromMillisecondsSinceEpoch(0, isUtc: true);
|
||||
final selected = state.selectedBackupAlbums.map(
|
||||
(e) => BackupAlbum(e.id, e.lastBackup ?? epoch, BackupSelection.select),
|
||||
);
|
||||
final excluded = state.excludedBackupAlbums.map(
|
||||
(e) => BackupAlbum(e.id, e.lastBackup ?? epoch, BackupSelection.exclude),
|
||||
);
|
||||
final candidates = selected.followedBy(excluded).toList();
|
||||
candidates.sortBy((e) => e.id);
|
||||
|
||||
final savedBackupAlbums = await _backupAlbumService.getAll(sort: BackupAlbumSort.id);
|
||||
final List<int> toDelete = [];
|
||||
final List<BackupAlbum> toUpsert = [];
|
||||
|
||||
diffSortedListsSync(
|
||||
savedBackupAlbums,
|
||||
candidates,
|
||||
compare: (BackupAlbum a, BackupAlbum b) => a.id.compareTo(b.id),
|
||||
both: (BackupAlbum a, BackupAlbum b) {
|
||||
b.lastBackup = a.lastBackup.isAfter(b.lastBackup) ? a.lastBackup : b.lastBackup;
|
||||
toUpsert.add(b);
|
||||
return true;
|
||||
},
|
||||
onlyFirst: (BackupAlbum a) => toDelete.add(a.isarId),
|
||||
onlySecond: (BackupAlbum b) => toUpsert.add(b),
|
||||
);
|
||||
|
||||
await _backupAlbumService.deleteAll(toDelete);
|
||||
await _backupAlbumService.updateAll(toUpsert);
|
||||
}
|
||||
|
||||
/// Invoke backup process
|
||||
Future<void> startBackupProcess() async {
|
||||
dPrint(() => "Start backup process");
|
||||
assert(state.backupProgress == BackUpProgressEnum.idle);
|
||||
state = state.copyWith(backupProgress: BackUpProgressEnum.inProgress);
|
||||
|
||||
await getBackupInfo();
|
||||
|
||||
final hasPermission = _galleryPermissionNotifier.hasPermission;
|
||||
if (hasPermission) {
|
||||
await _fileMediaRepository.clearFileCache();
|
||||
|
||||
if (state.allUniqueAssets.isEmpty) {
|
||||
log.info("No Asset On Device - Abort Backup Process");
|
||||
state = state.copyWith(backupProgress: BackUpProgressEnum.idle);
|
||||
return;
|
||||
}
|
||||
|
||||
Set<BackupCandidate> assetsWillBeBackup = Set.from(state.allUniqueAssets);
|
||||
// Remove item that has already been backed up
|
||||
for (final assetId in state.allAssetsInDatabase) {
|
||||
assetsWillBeBackup.removeWhere((e) => e.asset.localId == assetId);
|
||||
}
|
||||
|
||||
if (assetsWillBeBackup.isEmpty) {
|
||||
state = state.copyWith(backupProgress: BackUpProgressEnum.idle);
|
||||
}
|
||||
|
||||
// Perform Backup
|
||||
_cancelToken?.complete();
|
||||
_cancelToken = Completer<void>();
|
||||
|
||||
final pmProgressHandler = Platform.isIOS ? PMProgressHandler() : null;
|
||||
|
||||
pmProgressHandler?.stream.listen((event) {
|
||||
final double progress = event.progress;
|
||||
state = state.copyWith(iCloudDownloadProgress: progress);
|
||||
});
|
||||
|
||||
await _backupService.backupAsset(
|
||||
assetsWillBeBackup,
|
||||
_cancelToken!,
|
||||
pmProgressHandler: pmProgressHandler,
|
||||
onSuccess: _onAssetUploaded,
|
||||
onProgress: _onUploadProgress,
|
||||
onCurrentAsset: _onSetCurrentBackupAsset,
|
||||
onError: _onBackupError,
|
||||
);
|
||||
await notifyBackgroundServiceCanRun();
|
||||
} else {
|
||||
await openAppSettings();
|
||||
}
|
||||
}
|
||||
|
||||
void setAvailableAlbums(availableAlbums) {
|
||||
state = state.copyWith(availableAlbums: availableAlbums);
|
||||
}
|
||||
|
||||
void _onBackupError(ErrorUploadAsset errorAssetInfo) {
|
||||
ref.watch(errorBackupListProvider.notifier).add(errorAssetInfo);
|
||||
}
|
||||
|
||||
void _onSetCurrentBackupAsset(CurrentUploadAsset currentUploadAsset) {
|
||||
state = state.copyWith(currentUploadAsset: currentUploadAsset);
|
||||
}
|
||||
|
||||
void cancelBackup() {
|
||||
if (state.backupProgress != BackUpProgressEnum.inProgress) {
|
||||
notifyBackgroundServiceCanRun();
|
||||
}
|
||||
_cancelToken?.complete();
|
||||
_cancelToken = null;
|
||||
state = state.copyWith(
|
||||
backupProgress: BackUpProgressEnum.idle,
|
||||
progressInPercentage: 0.0,
|
||||
progressInFileSize: "0 B / 0 B",
|
||||
progressInFileSpeed: 0,
|
||||
progressInFileSpeedUpdateTime: DateTime.now(),
|
||||
progressInFileSpeedUpdateSentBytes: 0,
|
||||
);
|
||||
}
|
||||
|
||||
void _onAssetUploaded(SuccessUploadAsset result) async {
|
||||
if (result.isDuplicate) {
|
||||
state = state.copyWith(
|
||||
allUniqueAssets: state.allUniqueAssets
|
||||
.where((candidate) => candidate.asset.localId != result.candidate.asset.localId)
|
||||
.toSet(),
|
||||
);
|
||||
} else {
|
||||
state = state.copyWith(
|
||||
selectedAlbumsBackupAssetsIds: {...state.selectedAlbumsBackupAssetsIds, result.candidate.asset.localId!},
|
||||
allAssetsInDatabase: [...state.allAssetsInDatabase, result.candidate.asset.localId!],
|
||||
);
|
||||
}
|
||||
|
||||
if (state.allUniqueAssets.length - state.selectedAlbumsBackupAssetsIds.length == 0) {
|
||||
final latestAssetBackup = state.allUniqueAssets
|
||||
.map((candidate) => candidate.asset.fileModifiedAt)
|
||||
.reduce((v, e) => e.isAfter(v) ? e : v);
|
||||
state = state.copyWith(
|
||||
selectedBackupAlbums: state.selectedBackupAlbums.map((e) => e.copyWith(lastBackup: latestAssetBackup)).toSet(),
|
||||
excludedBackupAlbums: state.excludedBackupAlbums.map((e) => e.copyWith(lastBackup: latestAssetBackup)).toSet(),
|
||||
backupProgress: BackUpProgressEnum.done,
|
||||
progressInPercentage: 0.0,
|
||||
progressInFileSize: "0 B / 0 B",
|
||||
progressInFileSpeed: 0,
|
||||
progressInFileSpeedUpdateTime: DateTime.now(),
|
||||
progressInFileSpeedUpdateSentBytes: 0,
|
||||
);
|
||||
await _updatePersistentAlbumsSelection();
|
||||
}
|
||||
|
||||
await updateDiskInfo();
|
||||
}
|
||||
|
||||
void _onUploadProgress(int sent, int total) {
|
||||
double lastUploadSpeed = state.progressInFileSpeed;
|
||||
List<double> lastUploadSpeeds = state.progressInFileSpeeds.toList();
|
||||
DateTime lastUpdateTime = state.progressInFileSpeedUpdateTime;
|
||||
int lastSentBytes = state.progressInFileSpeedUpdateSentBytes;
|
||||
|
||||
final now = DateTime.now();
|
||||
final duration = now.difference(lastUpdateTime);
|
||||
|
||||
// Keep the upload speed average span limited, to keep it somewhat relevant
|
||||
if (lastUploadSpeeds.length > 10) {
|
||||
lastUploadSpeeds.removeAt(0);
|
||||
}
|
||||
|
||||
if (duration.inSeconds > 0) {
|
||||
lastUploadSpeeds.add(((sent - lastSentBytes) / duration.inSeconds).abs().roundToDouble());
|
||||
|
||||
lastUploadSpeed = lastUploadSpeeds.average.abs().roundToDouble();
|
||||
lastUpdateTime = now;
|
||||
lastSentBytes = sent;
|
||||
}
|
||||
|
||||
state = state.copyWith(
|
||||
progressInPercentage: (sent.toDouble() / total.toDouble() * 100),
|
||||
progressInFileSize: humanReadableFileBytesProgress(sent, total),
|
||||
progressInFileSpeed: lastUploadSpeed,
|
||||
progressInFileSpeeds: lastUploadSpeeds,
|
||||
progressInFileSpeedUpdateTime: lastUpdateTime,
|
||||
progressInFileSpeedUpdateSentBytes: lastSentBytes,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> updateDiskInfo() async {
|
||||
final diskInfo = await _serverInfoService.getDiskInfo();
|
||||
|
||||
// Update server info
|
||||
if (diskInfo != null) {
|
||||
state = state.copyWith(serverInfo: diskInfo);
|
||||
state = diskInfo;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _resumeBackup() async {
|
||||
// Check if user is login
|
||||
final accessKey = Store.tryGet(StoreKey.accessToken);
|
||||
|
||||
// User has been logged out return
|
||||
if (accessKey == null || !_authState.isAuthenticated) {
|
||||
log.info("[_resumeBackup] not authenticated - abort");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this device is enable backup by the user
|
||||
if (state.autoBackup) {
|
||||
// check if backup is already in process - then return
|
||||
if (state.backupProgress == BackUpProgressEnum.inProgress) {
|
||||
log.info("[_resumeBackup] Auto Backup is already in progress - abort");
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.backupProgress == BackUpProgressEnum.inBackground) {
|
||||
log.info("[_resumeBackup] Background backup is running - abort");
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.backupProgress == BackUpProgressEnum.manualInProgress) {
|
||||
log.info("[_resumeBackup] Manual upload is running - abort");
|
||||
return;
|
||||
}
|
||||
|
||||
// Run backup
|
||||
log.info("[_resumeBackup] Start back up");
|
||||
await startBackupProcess();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Future<void> resumeBackup() async {
|
||||
final List<BackupAlbum> selectedBackupAlbums = await _backupAlbumService.getAllBySelection(BackupSelection.select);
|
||||
final List<BackupAlbum> excludedBackupAlbums = await _backupAlbumService.getAllBySelection(BackupSelection.exclude);
|
||||
Set<AvailableAlbum> selectedAlbums = state.selectedBackupAlbums;
|
||||
Set<AvailableAlbum> excludedAlbums = state.excludedBackupAlbums;
|
||||
if (selectedAlbums.isNotEmpty) {
|
||||
selectedAlbums = _updateAlbumsBackupTime(selectedAlbums, selectedBackupAlbums);
|
||||
}
|
||||
|
||||
if (excludedAlbums.isNotEmpty) {
|
||||
excludedAlbums = _updateAlbumsBackupTime(excludedAlbums, excludedBackupAlbums);
|
||||
}
|
||||
final BackUpProgressEnum previous = state.backupProgress;
|
||||
state = state.copyWith(
|
||||
backupProgress: BackUpProgressEnum.inBackground,
|
||||
selectedBackupAlbums: selectedAlbums,
|
||||
excludedBackupAlbums: excludedAlbums,
|
||||
);
|
||||
// assumes the background service is currently running
|
||||
// if true, waits until it has stopped to start the backup
|
||||
final bool hasLock = await _backgroundService.acquireLock();
|
||||
if (hasLock) {
|
||||
state = state.copyWith(backupProgress: previous);
|
||||
}
|
||||
return _resumeBackup();
|
||||
}
|
||||
|
||||
Set<AvailableAlbum> _updateAlbumsBackupTime(Set<AvailableAlbum> albums, List<BackupAlbum> backupAlbums) {
|
||||
Set<AvailableAlbum> result = {};
|
||||
for (BackupAlbum ba in backupAlbums) {
|
||||
try {
|
||||
AvailableAlbum a = albums.firstWhere((e) => e.id == ba.id);
|
||||
result.add(a.copyWith(lastBackup: ba.lastBackup));
|
||||
} on StateError {
|
||||
log.severe("[_updateAlbumBackupTime] failed to find album in state", "State Error", StackTrace.current);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<void> notifyBackgroundServiceCanRun() async {
|
||||
const allowedStates = [AppLifeCycleEnum.inactive, AppLifeCycleEnum.paused, AppLifeCycleEnum.detached];
|
||||
if (allowedStates.contains(ref.read(appStateProvider.notifier).state)) {
|
||||
_backgroundService.releaseLock();
|
||||
}
|
||||
}
|
||||
|
||||
BackUpProgressEnum get backupProgress => state.backupProgress;
|
||||
|
||||
void updateBackupProgress(BackUpProgressEnum backupProgress) {
|
||||
state = state.copyWith(backupProgress: backupProgress);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/providers/asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||
import 'package:immich_mobile/services/backup_verification.service.dart';
|
||||
import 'package:immich_mobile/widgets/common/confirm_dialog.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
|
||||
part 'backup_verification.provider.g.dart';
|
||||
|
||||
@riverpod
|
||||
class BackupVerification extends _$BackupVerification {
|
||||
@override
|
||||
bool build() => false;
|
||||
|
||||
void performBackupCheck(BuildContext context) async {
|
||||
try {
|
||||
state = true;
|
||||
final backupState = ref.read(backupProvider);
|
||||
|
||||
if (backupState.allUniqueAssets.length > backupState.selectedAlbumsBackupAssetsIds.length) {
|
||||
if (context.mounted) {
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: "Backup all assets before starting this check!",
|
||||
toastType: ToastType.error,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
final connection = await Connectivity().checkConnectivity();
|
||||
if (!connection.contains(ConnectivityResult.wifi)) {
|
||||
if (context.mounted) {
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: "Make sure to be connected to unmetered Wi-Fi",
|
||||
toastType: ToastType.error,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
unawaited(WakelockPlus.enable());
|
||||
|
||||
const limit = 100;
|
||||
final toDelete = await ref.read(backupVerificationServiceProvider).findWronglyBackedUpAssets(limit: limit);
|
||||
if (toDelete.isEmpty) {
|
||||
if (context.mounted) {
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: "Did not find any corrupt asset backups!",
|
||||
toastType: ToastType.success,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (context.mounted) {
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => ConfirmDialog(
|
||||
onOk: () => _performDeletion(context, toDelete),
|
||||
title: "Corrupt backups!",
|
||||
ok: "Delete",
|
||||
content:
|
||||
"Found ${toDelete.length} (max $limit at once) corrupt asset backups. "
|
||||
"Run the check again to find more.\n"
|
||||
"Do you want to delete the corrupt asset backups now?",
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
unawaited(WakelockPlus.disable());
|
||||
state = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _performDeletion(BuildContext context, List<Asset> assets) async {
|
||||
try {
|
||||
state = true;
|
||||
if (context.mounted) {
|
||||
ImmichToast.show(context: context, msg: "Deleting ${assets.length} assets on the server...");
|
||||
}
|
||||
await ref.read(assetProvider.notifier).deleteAssets(assets, force: true);
|
||||
if (context.mounted) {
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg:
|
||||
"Deleted ${assets.length} assets on the server. "
|
||||
"You can now start a manual backup",
|
||||
toastType: ToastType.success,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
state = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'backup_verification.provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$backupVerificationHash() =>
|
||||
r'b4b34909ed1af3f28877ea457d53a4a18b6417f8';
|
||||
|
||||
/// See also [BackupVerification].
|
||||
@ProviderFor(BackupVerification)
|
||||
final backupVerificationProvider =
|
||||
AutoDisposeNotifierProvider<BackupVerification, bool>.internal(
|
||||
BackupVerification.new,
|
||||
name: r'backupVerificationProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$backupVerificationHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$BackupVerification = AutoDisposeNotifier<bool>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||
@@ -1,22 +0,0 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/models/backup/error_upload_asset.model.dart';
|
||||
|
||||
class ErrorBackupListNotifier extends StateNotifier<Set<ErrorUploadAsset>> {
|
||||
ErrorBackupListNotifier() : super({});
|
||||
|
||||
add(ErrorUploadAsset errorAsset) {
|
||||
state = state.union({errorAsset});
|
||||
}
|
||||
|
||||
remove(ErrorUploadAsset errorAsset) {
|
||||
state = state.difference({errorAsset});
|
||||
}
|
||||
|
||||
empty() {
|
||||
state = {};
|
||||
}
|
||||
}
|
||||
|
||||
final errorBackupListProvider = StateNotifierProvider<ErrorBackupListNotifier, Set<ErrorUploadAsset>>(
|
||||
(ref) => ErrorBackupListNotifier(),
|
||||
);
|
||||
@@ -1,54 +0,0 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/services/background.service.dart';
|
||||
|
||||
class IOSBackgroundSettings {
|
||||
final bool appRefreshEnabled;
|
||||
final int numberOfBackgroundTasksQueued;
|
||||
final DateTime? timeOfLastFetch;
|
||||
final DateTime? timeOfLastProcessing;
|
||||
|
||||
const IOSBackgroundSettings({
|
||||
required this.appRefreshEnabled,
|
||||
required this.numberOfBackgroundTasksQueued,
|
||||
this.timeOfLastFetch,
|
||||
this.timeOfLastProcessing,
|
||||
});
|
||||
}
|
||||
|
||||
class IOSBackgroundSettingsNotifier extends StateNotifier<IOSBackgroundSettings?> {
|
||||
final BackgroundService _service;
|
||||
IOSBackgroundSettingsNotifier(this._service) : super(null);
|
||||
|
||||
IOSBackgroundSettings? get settings => state;
|
||||
|
||||
Future<IOSBackgroundSettings> refresh() async {
|
||||
final lastFetchTime = await _service.getIOSBackupLastRun(IosBackgroundTask.fetch);
|
||||
final lastProcessingTime = await _service.getIOSBackupLastRun(IosBackgroundTask.processing);
|
||||
int numberOfProcesses = await _service.getIOSBackupNumberOfProcesses();
|
||||
final appRefreshEnabled = await _service.getIOSBackgroundAppRefreshEnabled();
|
||||
|
||||
// If this is enabled and there are no background processes,
|
||||
// the user just enabled app refresh in Settings.
|
||||
// But we don't have any background services running, since it was disabled
|
||||
// before.
|
||||
if (await _service.isBackgroundBackupEnabled() && numberOfProcesses == 0) {
|
||||
// We need to restart the background service
|
||||
await _service.enableService();
|
||||
numberOfProcesses = await _service.getIOSBackupNumberOfProcesses();
|
||||
}
|
||||
|
||||
final settings = IOSBackgroundSettings(
|
||||
appRefreshEnabled: appRefreshEnabled,
|
||||
numberOfBackgroundTasksQueued: numberOfProcesses,
|
||||
timeOfLastFetch: lastFetchTime,
|
||||
timeOfLastProcessing: lastProcessingTime,
|
||||
);
|
||||
|
||||
state = settings;
|
||||
return settings;
|
||||
}
|
||||
}
|
||||
|
||||
final iOSBackgroundSettingsProvider = StateNotifierProvider<IOSBackgroundSettingsNotifier, IOSBackgroundSettings?>(
|
||||
(ref) => IOSBackgroundSettingsNotifier(ref.watch(backgroundServiceProvider)),
|
||||
);
|
||||
@@ -1,391 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/backup_album.entity.dart';
|
||||
import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
|
||||
import 'package:immich_mobile/models/backup/backup_state.model.dart';
|
||||
import 'package:immich_mobile/models/backup/current_upload_asset.model.dart';
|
||||
import 'package:immich_mobile/models/backup/error_upload_asset.model.dart';
|
||||
import 'package:immich_mobile/models/backup/manual_upload_state.model.dart';
|
||||
import 'package:immich_mobile/models/backup/success_upload_asset.model.dart';
|
||||
import 'package:immich_mobile/providers/app_life_cycle.provider.dart';
|
||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart';
|
||||
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
||||
import 'package:immich_mobile/repositories/file_media.repository.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/services/background.service.dart';
|
||||
import 'package:immich_mobile/services/backup.service.dart';
|
||||
import 'package:immich_mobile/services/backup_album.service.dart';
|
||||
import 'package:immich_mobile/services/local_notification.service.dart';
|
||||
import 'package:immich_mobile/utils/backup_progress.dart';
|
||||
import 'package:immich_mobile/utils/debug_print.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:photo_manager/photo_manager.dart' show PMProgressHandler;
|
||||
|
||||
final manualUploadProvider = StateNotifierProvider<ManualUploadNotifier, ManualUploadState>((ref) {
|
||||
return ManualUploadNotifier(
|
||||
ref.watch(localNotificationService),
|
||||
ref.watch(backupProvider.notifier),
|
||||
ref.watch(backupServiceProvider),
|
||||
ref.watch(backupAlbumServiceProvider),
|
||||
ref,
|
||||
);
|
||||
});
|
||||
|
||||
class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
|
||||
final Logger _log = Logger("ManualUploadNotifier");
|
||||
final LocalNotificationService _localNotificationService;
|
||||
final BackupNotifier _backupProvider;
|
||||
final BackupService _backupService;
|
||||
final BackupAlbumService _backupAlbumService;
|
||||
final Ref ref;
|
||||
Completer<void>? _cancelToken;
|
||||
|
||||
ManualUploadNotifier(
|
||||
this._localNotificationService,
|
||||
this._backupProvider,
|
||||
this._backupService,
|
||||
this._backupAlbumService,
|
||||
this.ref,
|
||||
) : super(
|
||||
ManualUploadState(
|
||||
progressInPercentage: 0,
|
||||
progressInFileSize: "0 B / 0 B",
|
||||
progressInFileSpeed: 0,
|
||||
progressInFileSpeeds: const [],
|
||||
progressInFileSpeedUpdateTime: DateTime.now(),
|
||||
progressInFileSpeedUpdateSentBytes: 0,
|
||||
currentUploadAsset: CurrentUploadAsset(
|
||||
id: '...',
|
||||
fileCreatedAt: DateTime.parse('2020-10-04'),
|
||||
fileName: '...',
|
||||
fileType: '...',
|
||||
),
|
||||
totalAssetsToUpload: 0,
|
||||
successfulUploads: 0,
|
||||
currentAssetIndex: 0,
|
||||
showDetailedNotification: false,
|
||||
),
|
||||
);
|
||||
|
||||
String _lastPrintedDetailContent = '';
|
||||
String? _lastPrintedDetailTitle;
|
||||
|
||||
static const notifyInterval = Duration(milliseconds: 500);
|
||||
late final ThrottleProgressUpdate _throttledNotifiy = ThrottleProgressUpdate(_updateProgress, notifyInterval);
|
||||
late final ThrottleProgressUpdate _throttledDetailNotify = ThrottleProgressUpdate(
|
||||
_updateDetailProgress,
|
||||
notifyInterval,
|
||||
);
|
||||
|
||||
void _updateProgress(String? title, int progress, int total) {
|
||||
// Guard against throttling calling this method after the upload is done
|
||||
if (_backupProvider.backupProgress == BackUpProgressEnum.manualInProgress) {
|
||||
_localNotificationService.showOrUpdateManualUploadStatus(
|
||||
"backup_background_service_in_progress_notification".tr(),
|
||||
formatAssetBackupProgress(state.currentAssetIndex, state.totalAssetsToUpload),
|
||||
maxProgress: state.totalAssetsToUpload,
|
||||
progress: state.currentAssetIndex,
|
||||
showActions: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _updateDetailProgress(String? title, int progress, int total) {
|
||||
// Guard against throttling calling this method after the upload is done
|
||||
if (_backupProvider.backupProgress == BackUpProgressEnum.manualInProgress) {
|
||||
final String msg = total > 0 ? humanReadableBytesProgress(progress, total) : "";
|
||||
// only update if message actually differs (to stop many useless notification updates on large assets or slow connections)
|
||||
if (msg != _lastPrintedDetailContent || title != _lastPrintedDetailTitle) {
|
||||
_lastPrintedDetailContent = msg;
|
||||
_lastPrintedDetailTitle = title;
|
||||
_localNotificationService.showOrUpdateManualUploadStatus(
|
||||
title ?? 'Uploading',
|
||||
msg,
|
||||
progress: total > 0 ? (progress * 1000) ~/ total : 0,
|
||||
maxProgress: 1000,
|
||||
isDetailed: true,
|
||||
// Detailed noitifcation is displayed for Single asset uploads. Show actions for such case
|
||||
showActions: state.totalAssetsToUpload == 1,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _onAssetUploaded(SuccessUploadAsset result) {
|
||||
state = state.copyWith(successfulUploads: state.successfulUploads + 1);
|
||||
_backupProvider.updateDiskInfo();
|
||||
}
|
||||
|
||||
void _onAssetUploadError(ErrorUploadAsset errorAssetInfo) {
|
||||
ref.watch(errorBackupListProvider.notifier).add(errorAssetInfo);
|
||||
}
|
||||
|
||||
void _onProgress(int sent, int total) {
|
||||
double lastUploadSpeed = state.progressInFileSpeed;
|
||||
List<double> lastUploadSpeeds = state.progressInFileSpeeds.toList();
|
||||
DateTime lastUpdateTime = state.progressInFileSpeedUpdateTime;
|
||||
int lastSentBytes = state.progressInFileSpeedUpdateSentBytes;
|
||||
|
||||
final now = DateTime.now();
|
||||
final duration = now.difference(lastUpdateTime);
|
||||
|
||||
// Keep the upload speed average span limited, to keep it somewhat relevant
|
||||
if (lastUploadSpeeds.length > 10) {
|
||||
lastUploadSpeeds.removeAt(0);
|
||||
}
|
||||
|
||||
if (duration.inSeconds > 0) {
|
||||
lastUploadSpeeds.add(((sent - lastSentBytes) / duration.inSeconds).abs().roundToDouble());
|
||||
|
||||
lastUploadSpeed = lastUploadSpeeds.average.abs().roundToDouble();
|
||||
lastUpdateTime = now;
|
||||
lastSentBytes = sent;
|
||||
}
|
||||
|
||||
state = state.copyWith(
|
||||
progressInPercentage: (sent.toDouble() / total.toDouble() * 100),
|
||||
progressInFileSize: humanReadableFileBytesProgress(sent, total),
|
||||
progressInFileSpeed: lastUploadSpeed,
|
||||
progressInFileSpeeds: lastUploadSpeeds,
|
||||
progressInFileSpeedUpdateTime: lastUpdateTime,
|
||||
progressInFileSpeedUpdateSentBytes: lastSentBytes,
|
||||
);
|
||||
|
||||
if (state.showDetailedNotification) {
|
||||
final title = "backup_background_service_current_upload_notification".tr(
|
||||
namedArgs: {'filename': state.currentUploadAsset.fileName},
|
||||
);
|
||||
_throttledDetailNotify(title: title, progress: sent, total: total);
|
||||
}
|
||||
}
|
||||
|
||||
void _onSetCurrentBackupAsset(CurrentUploadAsset currentUploadAsset) {
|
||||
state = state.copyWith(currentUploadAsset: currentUploadAsset, currentAssetIndex: state.currentAssetIndex + 1);
|
||||
if (state.totalAssetsToUpload > 1) {
|
||||
_throttledNotifiy();
|
||||
}
|
||||
if (state.showDetailedNotification) {
|
||||
_throttledDetailNotify.title = "backup_background_service_current_upload_notification".tr(
|
||||
namedArgs: {'filename': currentUploadAsset.fileName},
|
||||
);
|
||||
_throttledDetailNotify.progress = 0;
|
||||
_throttledDetailNotify.total = 0;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _startUpload(Iterable<Asset> allManualUploads) async {
|
||||
bool hasErrors = false;
|
||||
try {
|
||||
_backupProvider.updateBackupProgress(BackUpProgressEnum.manualInProgress);
|
||||
|
||||
if (ref.read(galleryPermissionNotifier.notifier).hasPermission) {
|
||||
await ref.read(fileMediaRepositoryProvider).clearFileCache();
|
||||
|
||||
final allAssetsFromDevice = allManualUploads.where((e) => e.isLocal && !e.isRemote).toList();
|
||||
|
||||
if (allAssetsFromDevice.length != allManualUploads.length) {
|
||||
_log.warning(
|
||||
'[_startUpload] Refreshed upload list -> ${allManualUploads.length - allAssetsFromDevice.length} asset will not be uploaded',
|
||||
);
|
||||
}
|
||||
|
||||
final selectedBackupAlbums = await _backupAlbumService.getAllBySelection(BackupSelection.select);
|
||||
final excludedBackupAlbums = await _backupAlbumService.getAllBySelection(BackupSelection.exclude);
|
||||
|
||||
// Get candidates from selected albums and excluded albums
|
||||
Set<BackupCandidate> candidates = await _backupService.buildUploadCandidates(
|
||||
selectedBackupAlbums,
|
||||
excludedBackupAlbums,
|
||||
useTimeFilter: false,
|
||||
);
|
||||
|
||||
// Extrack candidate from allAssetsFromDevice
|
||||
final uploadAssets = candidates.where(
|
||||
(candidate) =>
|
||||
allAssetsFromDevice.firstWhereOrNull((asset) => asset.localId == candidate.asset.localId) != null,
|
||||
);
|
||||
|
||||
if (uploadAssets.isEmpty) {
|
||||
dPrint(() => "[_startUpload] No Assets to upload - Abort Process");
|
||||
_backupProvider.updateBackupProgress(BackUpProgressEnum.idle);
|
||||
return false;
|
||||
}
|
||||
|
||||
state = state.copyWith(
|
||||
progressInPercentage: 0,
|
||||
progressInFileSize: "0 B / 0 B",
|
||||
progressInFileSpeed: 0,
|
||||
totalAssetsToUpload: uploadAssets.length,
|
||||
successfulUploads: 0,
|
||||
currentAssetIndex: 0,
|
||||
currentUploadAsset: CurrentUploadAsset(
|
||||
id: '...',
|
||||
fileCreatedAt: DateTime.parse('2020-10-04'),
|
||||
fileName: '...',
|
||||
fileType: '...',
|
||||
),
|
||||
);
|
||||
// Reset Error List
|
||||
ref.watch(errorBackupListProvider.notifier).empty();
|
||||
|
||||
if (state.totalAssetsToUpload > 1) {
|
||||
_throttledNotifiy();
|
||||
}
|
||||
|
||||
// Show detailed asset if enabled in settings or if a single asset is uploaded
|
||||
bool showDetailedNotification =
|
||||
ref.read(appSettingsServiceProvider).getSetting<bool>(AppSettingsEnum.backgroundBackupSingleProgress) ||
|
||||
state.totalAssetsToUpload == 1;
|
||||
state = state.copyWith(showDetailedNotification: showDetailedNotification);
|
||||
final pmProgressHandler = Platform.isIOS ? PMProgressHandler() : null;
|
||||
|
||||
_cancelToken?.complete();
|
||||
_cancelToken = Completer<void>();
|
||||
final bool ok = await ref
|
||||
.read(backupServiceProvider)
|
||||
.backupAsset(
|
||||
uploadAssets,
|
||||
_cancelToken!,
|
||||
pmProgressHandler: pmProgressHandler,
|
||||
onSuccess: _onAssetUploaded,
|
||||
onProgress: _onProgress,
|
||||
onCurrentAsset: _onSetCurrentBackupAsset,
|
||||
onError: _onAssetUploadError,
|
||||
);
|
||||
|
||||
// Close detailed notification
|
||||
await _localNotificationService.closeNotification(LocalNotificationService.manualUploadDetailedNotificationID);
|
||||
|
||||
_log.info(
|
||||
'[_startUpload] Manual Upload Completed - success: ${state.successfulUploads},'
|
||||
' failed: ${state.totalAssetsToUpload - state.successfulUploads}',
|
||||
);
|
||||
|
||||
// User cancelled upload
|
||||
if (!ok && _cancelToken == null) {
|
||||
await _localNotificationService.showOrUpdateManualUploadStatus(
|
||||
"backup_manual_title".tr(),
|
||||
"backup_manual_cancelled".tr(),
|
||||
presentBanner: true,
|
||||
);
|
||||
hasErrors = true;
|
||||
} else if (state.successfulUploads == 0 || (!ok && _cancelToken != null)) {
|
||||
await _localNotificationService.showOrUpdateManualUploadStatus(
|
||||
"backup_manual_title".tr(),
|
||||
"failed".tr(),
|
||||
presentBanner: true,
|
||||
);
|
||||
hasErrors = true;
|
||||
} else {
|
||||
await _localNotificationService.showOrUpdateManualUploadStatus(
|
||||
"backup_manual_title".tr(),
|
||||
"backup_manual_success".tr(),
|
||||
presentBanner: true,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
unawaited(openAppSettings());
|
||||
dPrint(() => "[_startUpload] Do not have permission to the gallery");
|
||||
}
|
||||
} catch (e) {
|
||||
dPrint(() => "ERROR _startUpload: ${e.toString()}");
|
||||
hasErrors = true;
|
||||
} finally {
|
||||
_backupProvider.updateBackupProgress(BackUpProgressEnum.idle);
|
||||
_handleAppInActivity();
|
||||
await _localNotificationService.closeNotification(LocalNotificationService.manualUploadDetailedNotificationID);
|
||||
await _backupProvider.notifyBackgroundServiceCanRun();
|
||||
}
|
||||
return !hasErrors;
|
||||
}
|
||||
|
||||
void _handleAppInActivity() {
|
||||
final appState = ref.read(appStateProvider.notifier).getAppState();
|
||||
// The app is currently in background. Perform the necessary cleanups which
|
||||
// are on-hold for upload completion
|
||||
if (appState != AppLifeCycleEnum.active && appState != AppLifeCycleEnum.resumed) {
|
||||
ref.read(backupProvider.notifier).cancelBackup();
|
||||
}
|
||||
}
|
||||
|
||||
void cancelBackup() {
|
||||
if (_backupProvider.backupProgress != BackUpProgressEnum.inProgress &&
|
||||
_backupProvider.backupProgress != BackUpProgressEnum.manualInProgress) {
|
||||
_backupProvider.notifyBackgroundServiceCanRun();
|
||||
}
|
||||
_cancelToken?.complete();
|
||||
_cancelToken = null;
|
||||
if (_backupProvider.backupProgress != BackUpProgressEnum.manualInProgress) {
|
||||
_backupProvider.updateBackupProgress(BackUpProgressEnum.idle);
|
||||
}
|
||||
state = state.copyWith(
|
||||
progressInPercentage: 0,
|
||||
progressInFileSize: "0 B / 0 B",
|
||||
progressInFileSpeed: 0,
|
||||
progressInFileSpeedUpdateTime: DateTime.now(),
|
||||
progressInFileSpeedUpdateSentBytes: 0,
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> uploadAssets(BuildContext context, Iterable<Asset> allManualUploads) async {
|
||||
// assumes the background service is currently running and
|
||||
// waits until it has stopped to start the backup.
|
||||
final bool hasLock = await ref.read(backgroundServiceProvider).acquireLock();
|
||||
if (!hasLock) {
|
||||
dPrint(() => "[uploadAssets] could not acquire lock, exiting");
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: "failed".tr(),
|
||||
toastType: ToastType.info,
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
durationInSecond: 3,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool showInProgress = false;
|
||||
|
||||
// check if backup is already in process - then return
|
||||
if (_backupProvider.backupProgress == BackUpProgressEnum.manualInProgress) {
|
||||
dPrint(() => "[uploadAssets] Manual upload is already running - abort");
|
||||
showInProgress = true;
|
||||
}
|
||||
|
||||
if (_backupProvider.backupProgress == BackUpProgressEnum.inProgress) {
|
||||
dPrint(() => "[uploadAssets] Auto Backup is already in progress - abort");
|
||||
showInProgress = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_backupProvider.backupProgress == BackUpProgressEnum.inBackground) {
|
||||
dPrint(() => "[uploadAssets] Background backup is running - abort");
|
||||
showInProgress = true;
|
||||
}
|
||||
|
||||
if (showInProgress) {
|
||||
if (context.mounted) {
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: "backup_manual_in_progress".tr(),
|
||||
toastType: ToastType.info,
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
durationInSecond: 3,
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return _startUpload(allManualUploads);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart' as old_asset_entity;
|
||||
import 'package:immich_mobile/models/cast/cast_manager_state.dart';
|
||||
import 'package:immich_mobile/services/gcast.service.dart';
|
||||
|
||||
@@ -55,26 +54,6 @@ class CastNotifier extends StateNotifier<CastManagerState> {
|
||||
_gCastService.loadMedia(asset, reload);
|
||||
}
|
||||
|
||||
// TODO: remove this when we migrate to new timeline
|
||||
void loadMediaOld(old_asset_entity.Asset asset, bool reload) {
|
||||
final remoteAsset = RemoteAsset(
|
||||
id: asset.remoteId.toString(),
|
||||
name: asset.name,
|
||||
ownerId: asset.ownerId.toString(),
|
||||
checksum: asset.checksum,
|
||||
type: asset.type == old_asset_entity.AssetType.image
|
||||
? AssetType.image
|
||||
: asset.type == old_asset_entity.AssetType.video
|
||||
? AssetType.video
|
||||
: AssetType.other,
|
||||
createdAt: asset.fileCreatedAt,
|
||||
updatedAt: asset.updatedAt,
|
||||
isEdited: false,
|
||||
);
|
||||
|
||||
_gCastService.loadMedia(remoteAsset, reload);
|
||||
}
|
||||
|
||||
Future<void> connect(CastDestinationType type, dynamic device) async {
|
||||
switch (type) {
|
||||
case CastDestinationType.googleCast:
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
// overwritten in main.dart due to async loading
|
||||
final dbProvider = Provider<Isar>((_) => throw UnimplementedError());
|
||||
@@ -1,5 +0,0 @@
|
||||
/// An exception for the [ImageLoader] and the Immich image providers
|
||||
class ImageLoadingException implements Exception {
|
||||
final String message;
|
||||
const ImageLoadingException(this.message);
|
||||
}
|
||||
@@ -19,16 +19,12 @@ import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/services/action.service.dart';
|
||||
import 'package:immich_mobile/services/download.service.dart';
|
||||
import 'package:immich_mobile/services/foreground_upload.service.dart';
|
||||
import 'package:immich_mobile/services/timeline.service.dart';
|
||||
import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
final actionProvider = NotifierProvider<ActionNotifier, void>(
|
||||
ActionNotifier.new,
|
||||
dependencies: [multiSelectProvider, timelineServiceProvider],
|
||||
);
|
||||
final actionProvider = NotifierProvider<ActionNotifier, void>(ActionNotifier.new, dependencies: [multiSelectProvider]);
|
||||
|
||||
class ActionResult {
|
||||
final int count;
|
||||
|
||||
@@ -2,13 +2,6 @@ import 'dart:async';
|
||||
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'db.provider.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
Isar isar(Ref ref) => throw UnimplementedError('isar');
|
||||
|
||||
Drift Function(Ref ref) driftOverride(Drift drift) => (ref) {
|
||||
ref.onDispose(() => unawaited(drift.close()));
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'db.provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$isarHash() => r'69d3a06aa7e69a4381478e03f7956eb07d7f7feb';
|
||||
|
||||
/// See also [isar].
|
||||
@ProviderFor(isar)
|
||||
final isarProvider = Provider<Isar>.internal(
|
||||
isar,
|
||||
name: r'isarProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$isarHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
typedef IsarRef = ProviderRef<Isar>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||
@@ -1,7 +0,0 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/device_asset.repository.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||
|
||||
final deviceAssetRepositoryProvider = Provider<IsarDeviceAssetRepository>(
|
||||
(ref) => IsarDeviceAssetRepository(ref.watch(isarProvider)),
|
||||
);
|
||||
@@ -1,9 +0,0 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/exif.repository.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'exif.provider.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
IsarExifRepository exifRepository(Ref ref) => IsarExifRepository(ref.watch(isarProvider));
|
||||
@@ -1,27 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'exif.provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$exifRepositoryHash() => r'bf4a3f6a50d954a23d317659b4f3e2f381066463';
|
||||
|
||||
/// See also [exifRepository].
|
||||
@ProviderFor(exifRepository)
|
||||
final exifRepositoryProvider = Provider<IsarExifRepository>.internal(
|
||||
exifRepository,
|
||||
name: r'exifRepositoryProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$exifRepositoryHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
typedef ExifRepositoryRef = ProviderRef<IsarExifRepository>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||
@@ -1,13 +1,8 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/services/store.service.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'store.provider.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
IsarStoreRepository storeRepository(Ref ref) => IsarStoreRepository(ref.watch(isarProvider));
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
StoreService storeService(Ref _) => StoreService.I;
|
||||
|
||||
@@ -6,23 +6,6 @@ part of 'store.provider.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$storeRepositoryHash() => r'659cb134466e4b0d5f04e2fc93e426350d99545f';
|
||||
|
||||
/// See also [storeRepository].
|
||||
@ProviderFor(storeRepository)
|
||||
final storeRepositoryProvider = Provider<IsarStoreRepository>.internal(
|
||||
storeRepository,
|
||||
name: r'storeRepositoryProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$storeRepositoryHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
typedef StoreRepositoryRef = ProviderRef<IsarStoreRepository>;
|
||||
String _$storeServiceHash() => r'250e10497c42df360e9e1f9a618d0b19c1b5b0a0';
|
||||
|
||||
/// See also [storeService].
|
||||
|
||||
@@ -3,7 +3,6 @@ import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/domain/services/partner.service.dart';
|
||||
import 'package:immich_mobile/domain/services/user.service.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/partner.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/user.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart';
|
||||
import 'package:immich_mobile/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||
@@ -14,18 +13,12 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'user.provider.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
IsarUserRepository userRepository(Ref ref) => IsarUserRepository(ref.watch(isarProvider));
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
UserApiRepository userApiRepository(Ref ref) => UserApiRepository(ref.watch(apiServiceProvider).usersApi);
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
UserService userService(Ref ref) => UserService(
|
||||
isarUserRepository: ref.watch(userRepositoryProvider),
|
||||
userApiRepository: ref.watch(userApiRepositoryProvider),
|
||||
storeService: ref.watch(storeServiceProvider),
|
||||
);
|
||||
UserService userService(Ref ref) =>
|
||||
UserService(userApiRepository: ref.watch(userApiRepositoryProvider), storeService: ref.watch(storeServiceProvider));
|
||||
|
||||
/// Drifts
|
||||
final driftPartnerRepositoryProvider = Provider<DriftPartnerRepository>(
|
||||
|
||||
+1
-18
@@ -6,23 +6,6 @@ part of 'user.provider.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$userRepositoryHash() => r'538791a4ad126ed086c9db682c67fc5c654d54f3';
|
||||
|
||||
/// See also [userRepository].
|
||||
@ProviderFor(userRepository)
|
||||
final userRepositoryProvider = Provider<IsarUserRepository>.internal(
|
||||
userRepository,
|
||||
name: r'userRepositoryProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$userRepositoryHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
typedef UserRepositoryRef = ProviderRef<IsarUserRepository>;
|
||||
String _$userApiRepositoryHash() => r'8a7340ca4544c8c6b20225c65bff2abb9e96baa2';
|
||||
|
||||
/// See also [userApiRepository].
|
||||
@@ -40,7 +23,7 @@ final userApiRepositoryProvider = Provider<UserApiRepository>.internal(
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
typedef UserApiRepositoryRef = ProviderRef<UserApiRepository>;
|
||||
String _$userServiceHash() => r'181414dddc7891be6237e13d568c287a804228d1';
|
||||
String _$userServiceHash() => r'47e607f3b484b51bcb634d47e3cbf1f6ef25da97';
|
||||
|
||||
/// See also [userService].
|
||||
@ProviderFor(userService)
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/models/memories/memory.model.dart';
|
||||
import 'package:immich_mobile/services/memory.service.dart';
|
||||
|
||||
final memoryFutureProvider = FutureProvider.autoDispose<List<Memory>?>((ref) async {
|
||||
final service = ref.watch(memoryServiceProvider);
|
||||
|
||||
return await service.getMemoryLane();
|
||||
});
|
||||
@@ -1,89 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/providers/album/suggested_shared_users.provider.dart';
|
||||
import 'package:immich_mobile/services/partner.service.dart';
|
||||
|
||||
class PartnerSharedWithNotifier extends StateNotifier<List<UserDto>> {
|
||||
final PartnerService _partnerService;
|
||||
late final StreamSubscription<List<UserDto>> streamSub;
|
||||
|
||||
PartnerSharedWithNotifier(this._partnerService) : super([]) {
|
||||
Function eq = const ListEquality<UserDto>().equals;
|
||||
_partnerService
|
||||
.getSharedWith()
|
||||
.then((partners) {
|
||||
if (!eq(state, partners)) {
|
||||
state = partners;
|
||||
}
|
||||
})
|
||||
.then((_) {
|
||||
streamSub = _partnerService.watchSharedWith().listen((partners) {
|
||||
if (!eq(state, partners)) {
|
||||
state = partners;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Future<bool> updatePartner(UserDto partner, {required bool inTimeline}) {
|
||||
return _partnerService.updatePartner(partner, inTimeline: inTimeline);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (mounted) {
|
||||
streamSub.cancel();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
final partnerSharedWithProvider = StateNotifierProvider<PartnerSharedWithNotifier, List<UserDto>>((ref) {
|
||||
return PartnerSharedWithNotifier(ref.watch(partnerServiceProvider));
|
||||
});
|
||||
|
||||
class PartnerSharedByNotifier extends StateNotifier<List<UserDto>> {
|
||||
final PartnerService _partnerService;
|
||||
late final StreamSubscription<List<UserDto>> streamSub;
|
||||
|
||||
PartnerSharedByNotifier(this._partnerService) : super([]) {
|
||||
Function eq = const ListEquality<UserDto>().equals;
|
||||
_partnerService
|
||||
.getSharedBy()
|
||||
.then((partners) {
|
||||
if (!eq(state, partners)) {
|
||||
state = partners;
|
||||
}
|
||||
})
|
||||
.then((_) {
|
||||
streamSub = _partnerService.watchSharedBy().listen((partners) {
|
||||
if (!eq(state, partners)) {
|
||||
state = partners;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (mounted) {
|
||||
streamSub.cancel();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
final partnerSharedByProvider = StateNotifierProvider<PartnerSharedByNotifier, List<UserDto>>((ref) {
|
||||
return PartnerSharedByNotifier(ref.watch(partnerServiceProvider));
|
||||
});
|
||||
|
||||
final partnerAvailableProvider = FutureProvider.autoDispose<List<UserDto>>((ref) async {
|
||||
final otherUsers = await ref.watch(otherUsersProvider.future);
|
||||
final currentPartners = ref.watch(partnerSharedByProvider);
|
||||
final available = Set<UserDto>.of(otherUsers);
|
||||
available.removeAll(currentPartners);
|
||||
return available.toList();
|
||||
});
|
||||
@@ -1,7 +0,0 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/services/asset.service.dart';
|
||||
|
||||
final allMotionPhotosProvider = FutureProvider<List<Asset>>((ref) async {
|
||||
return ref.watch(assetServiceProvider).getMotionAssets();
|
||||
});
|
||||
@@ -1,46 +0,0 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/models/search/search_result.model.dart';
|
||||
import 'package:immich_mobile/services/timeline.service.dart';
|
||||
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
|
||||
import 'package:immich_mobile/models/search/search_filter.model.dart';
|
||||
import 'package:immich_mobile/services/search.service.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'paginated_search.provider.g.dart';
|
||||
|
||||
final paginatedSearchProvider = StateNotifierProvider<PaginatedSearchNotifier, SearchResult>(
|
||||
(ref) => PaginatedSearchNotifier(ref.watch(searchServiceProvider)),
|
||||
);
|
||||
|
||||
class PaginatedSearchNotifier extends StateNotifier<SearchResult> {
|
||||
final SearchService _searchService;
|
||||
|
||||
PaginatedSearchNotifier(this._searchService) : super(const SearchResult(assets: [], nextPage: 1));
|
||||
|
||||
Future<bool> search(SearchFilter filter) async {
|
||||
if (state.nextPage == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final result = await _searchService.search(filter, state.nextPage!);
|
||||
|
||||
if (result == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
state = SearchResult(assets: [...state.assets, ...result.assets], nextPage: result.nextPage);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
clear() {
|
||||
state = const SearchResult(assets: [], nextPage: 1);
|
||||
}
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Future<RenderList> paginatedSearchRenderList(Ref ref) {
|
||||
final result = ref.watch(paginatedSearchProvider);
|
||||
final timelineService = ref.watch(timelineServiceProvider);
|
||||
return timelineService.getTimelineFromAssets(result.assets, GroupAssetsBy.none);
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'paginated_search.provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$paginatedSearchRenderListHash() =>
|
||||
r'22d715ff7864e5a946be38322ce7813616f899c2';
|
||||
|
||||
/// See also [paginatedSearchRenderList].
|
||||
@ProviderFor(paginatedSearchRenderList)
|
||||
final paginatedSearchRenderListProvider =
|
||||
AutoDisposeFutureProvider<RenderList>.internal(
|
||||
paginatedSearchRenderList,
|
||||
name: r'paginatedSearchRenderListProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$paginatedSearchRenderListHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
typedef PaginatedSearchRenderListRef = AutoDisposeFutureProviderRef<RenderList>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||
@@ -1,9 +1,6 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/person.model.dart';
|
||||
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
|
||||
import 'package:immich_mobile/services/person.service.dart';
|
||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'people.provider.g.dart';
|
||||
@@ -17,16 +14,6 @@ Future<List<PersonDto>> getAllPeople(Ref ref) async {
|
||||
return people;
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Future<RenderList> personAssets(Ref ref, String personId) async {
|
||||
final PersonService personService = ref.read(personServiceProvider);
|
||||
final assets = await personService.getPersonAssets(personId);
|
||||
|
||||
final settings = ref.read(appSettingsServiceProvider);
|
||||
final groupBy = GroupAssetsBy.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)];
|
||||
return await RenderList.fromAssets(assets, groupBy);
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Future<bool> updatePersonName(Ref ref, String personId, String updatedName) async {
|
||||
final PersonService personService = ref.read(personServiceProvider);
|
||||
|
||||
+1
-121
@@ -24,7 +24,7 @@ final getAllPeopleProvider =
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
typedef GetAllPeopleRef = AutoDisposeFutureProviderRef<List<PersonDto>>;
|
||||
String _$personAssetsHash() => r'c1d35ee0e024bd6915e21bc724be4b458a14bc24';
|
||||
String _$updatePersonNameHash() => r'45f7693172de522a227406d8198811434cf2bbbc';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
@@ -47,126 +47,6 @@ class _SystemHash {
|
||||
}
|
||||
}
|
||||
|
||||
/// See also [personAssets].
|
||||
@ProviderFor(personAssets)
|
||||
const personAssetsProvider = PersonAssetsFamily();
|
||||
|
||||
/// See also [personAssets].
|
||||
class PersonAssetsFamily extends Family<AsyncValue<RenderList>> {
|
||||
/// See also [personAssets].
|
||||
const PersonAssetsFamily();
|
||||
|
||||
/// See also [personAssets].
|
||||
PersonAssetsProvider call(String personId) {
|
||||
return PersonAssetsProvider(personId);
|
||||
}
|
||||
|
||||
@override
|
||||
PersonAssetsProvider getProviderOverride(
|
||||
covariant PersonAssetsProvider provider,
|
||||
) {
|
||||
return call(provider.personId);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'personAssetsProvider';
|
||||
}
|
||||
|
||||
/// See also [personAssets].
|
||||
class PersonAssetsProvider extends AutoDisposeFutureProvider<RenderList> {
|
||||
/// See also [personAssets].
|
||||
PersonAssetsProvider(String personId)
|
||||
: this._internal(
|
||||
(ref) => personAssets(ref as PersonAssetsRef, personId),
|
||||
from: personAssetsProvider,
|
||||
name: r'personAssetsProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$personAssetsHash,
|
||||
dependencies: PersonAssetsFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
PersonAssetsFamily._allTransitiveDependencies,
|
||||
personId: personId,
|
||||
);
|
||||
|
||||
PersonAssetsProvider._internal(
|
||||
super._createNotifier, {
|
||||
required super.name,
|
||||
required super.dependencies,
|
||||
required super.allTransitiveDependencies,
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.personId,
|
||||
}) : super.internal();
|
||||
|
||||
final String personId;
|
||||
|
||||
@override
|
||||
Override overrideWith(
|
||||
FutureOr<RenderList> Function(PersonAssetsRef provider) create,
|
||||
) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
override: PersonAssetsProvider._internal(
|
||||
(ref) => create(ref as PersonAssetsRef),
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
personId: personId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AutoDisposeFutureProviderElement<RenderList> createElement() {
|
||||
return _PersonAssetsProviderElement(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is PersonAssetsProvider && other.personId == personId;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, personId.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
mixin PersonAssetsRef on AutoDisposeFutureProviderRef<RenderList> {
|
||||
/// The parameter `personId` of this provider.
|
||||
String get personId;
|
||||
}
|
||||
|
||||
class _PersonAssetsProviderElement
|
||||
extends AutoDisposeFutureProviderElement<RenderList>
|
||||
with PersonAssetsRef {
|
||||
_PersonAssetsProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
String get personId => (origin as PersonAssetsProvider).personId;
|
||||
}
|
||||
|
||||
String _$updatePersonNameHash() => r'45f7693172de522a227406d8198811434cf2bbbc';
|
||||
|
||||
/// See also [updatePersonName].
|
||||
@ProviderFor(updatePersonName)
|
||||
const updatePersonNameProvider = UpdatePersonNameFamily();
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/services/asset.service.dart';
|
||||
|
||||
final recentlyTakenAssetProvider = FutureProvider<List<Asset>>((ref) async {
|
||||
final assetService = ref.read(assetServiceProvider);
|
||||
|
||||
return assetService.getRecentlyTakenAssets();
|
||||
});
|
||||
@@ -1,68 +0,0 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/locale_provider.dart';
|
||||
import 'package:immich_mobile/services/timeline.service.dart';
|
||||
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
|
||||
|
||||
final singleUserTimelineProvider = StreamProvider.family<RenderList, String?>((ref, userId) {
|
||||
if (userId == null) {
|
||||
return const Stream.empty();
|
||||
}
|
||||
|
||||
ref.watch(localeProvider);
|
||||
final timelineService = ref.watch(timelineServiceProvider);
|
||||
return timelineService.watchHomeTimeline(userId);
|
||||
}, dependencies: [localeProvider]);
|
||||
|
||||
final multiUsersTimelineProvider = StreamProvider.family<RenderList, List<String>>((ref, userIds) {
|
||||
ref.watch(localeProvider);
|
||||
final timelineService = ref.watch(timelineServiceProvider);
|
||||
return timelineService.watchMultiUsersTimeline(userIds);
|
||||
}, dependencies: [localeProvider]);
|
||||
|
||||
final albumTimelineProvider = StreamProvider.autoDispose.family<RenderList, int>((ref, id) {
|
||||
final album = ref.watch(albumWatcher(id)).value;
|
||||
final timelineService = ref.watch(timelineServiceProvider);
|
||||
|
||||
if (album != null) {
|
||||
return timelineService.watchAlbumTimeline(album);
|
||||
}
|
||||
|
||||
return const Stream.empty();
|
||||
});
|
||||
|
||||
final archiveTimelineProvider = StreamProvider<RenderList>((ref) {
|
||||
final timelineService = ref.watch(timelineServiceProvider);
|
||||
return timelineService.watchArchiveTimeline();
|
||||
});
|
||||
|
||||
final favoriteTimelineProvider = StreamProvider<RenderList>((ref) {
|
||||
final timelineService = ref.watch(timelineServiceProvider);
|
||||
return timelineService.watchFavoriteTimeline();
|
||||
});
|
||||
|
||||
final trashTimelineProvider = StreamProvider<RenderList>((ref) {
|
||||
final timelineService = ref.watch(timelineServiceProvider);
|
||||
return timelineService.watchTrashTimeline();
|
||||
});
|
||||
|
||||
final allVideosTimelineProvider = StreamProvider<RenderList>((ref) {
|
||||
final timelineService = ref.watch(timelineServiceProvider);
|
||||
return timelineService.watchAllVideosTimeline();
|
||||
});
|
||||
|
||||
final assetSelectionTimelineProvider = StreamProvider<RenderList>((ref) {
|
||||
final timelineService = ref.watch(timelineServiceProvider);
|
||||
return timelineService.watchAssetSelectionTimeline();
|
||||
});
|
||||
|
||||
final assetsTimelineProvider = FutureProvider.family<RenderList, List<Asset>>((ref, assets) {
|
||||
final timelineService = ref.watch(timelineServiceProvider);
|
||||
return timelineService.getTimelineFromAssets(assets, null);
|
||||
});
|
||||
|
||||
final lockedTimelineProvider = StreamProvider<RenderList>((ref) {
|
||||
final timelineService = ref.watch(timelineServiceProvider);
|
||||
return timelineService.watchLockedTimelineProvider();
|
||||
});
|
||||
@@ -1,45 +0,0 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/services/trash.service.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
class TrashNotifier extends StateNotifier<bool> {
|
||||
final TrashService _trashService;
|
||||
final _log = Logger('TrashNotifier');
|
||||
|
||||
TrashNotifier(this._trashService) : super(false);
|
||||
|
||||
Future<void> emptyTrash() async {
|
||||
try {
|
||||
await _trashService.emptyTrash();
|
||||
state = true;
|
||||
} catch (error, stack) {
|
||||
_log.severe("Cannot empty trash", error, stack);
|
||||
state = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> restoreAssets(Iterable<Asset> assetList) async {
|
||||
try {
|
||||
await _trashService.restoreAssets(assetList);
|
||||
return true;
|
||||
} catch (error, stack) {
|
||||
_log.severe("Cannot restore assets", error, stack);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> restoreTrash() async {
|
||||
try {
|
||||
await _trashService.restoreTrash();
|
||||
state = true;
|
||||
} catch (error, stack) {
|
||||
_log.severe("Cannot restore trash", error, stack);
|
||||
state = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final trashProvider = StateNotifierProvider<TrashNotifier, bool>((ref) {
|
||||
return TrashNotifier(ref.watch(trashServiceProvider));
|
||||
});
|
||||
@@ -1,11 +1,9 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/domain/services/user.service.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
|
||||
import 'package:immich_mobile/services/timeline.service.dart';
|
||||
|
||||
class CurrentUserProvider extends StateNotifier<UserDto?> {
|
||||
CurrentUserProvider(this._userService) : super(null) {
|
||||
@@ -32,28 +30,3 @@ class CurrentUserProvider extends StateNotifier<UserDto?> {
|
||||
final currentUserProvider = StateNotifierProvider<CurrentUserProvider, UserDto?>((ref) {
|
||||
return CurrentUserProvider(ref.watch(userServiceProvider));
|
||||
});
|
||||
|
||||
class TimelineUserIdsProvider extends StateNotifier<List<String>> {
|
||||
TimelineUserIdsProvider(this._timelineService) : super([]) {
|
||||
final listEquality = const ListEquality();
|
||||
_timelineService.getTimelineUserIds().then((users) => state = users);
|
||||
streamSub = _timelineService.watchTimelineUserIds().listen((users) {
|
||||
if (!listEquality.equals(state, users)) {
|
||||
state = users;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
late final StreamSubscription<List<String>> streamSub;
|
||||
final TimelineService _timelineService;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
streamSub.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
final timelineUsersIdsProvider = StateNotifierProvider<TimelineUserIdsProvider, List<String>>((ref) {
|
||||
return TimelineUserIdsProvider(ref.watch(timelineServiceProvider));
|
||||
});
|
||||
|
||||
@@ -1,60 +1,27 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
|
||||
import 'package:immich_mobile/models/server_info/server_version.model.dart';
|
||||
import 'package:immich_mobile/providers/asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||
import 'package:immich_mobile/providers/db.provider.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/services/sync.service.dart';
|
||||
import 'package:immich_mobile/utils/debounce.dart';
|
||||
import 'package:immich_mobile/utils/debug_print.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:socket_io_client/socket_io_client.dart';
|
||||
|
||||
enum PendingAction { assetDelete, assetUploaded, assetHidden, assetTrash }
|
||||
|
||||
class PendingChange {
|
||||
final String id;
|
||||
final PendingAction action;
|
||||
final dynamic value;
|
||||
|
||||
const PendingChange(this.id, this.action, this.value);
|
||||
|
||||
@override
|
||||
String toString() => 'PendingChange(id: $id, action: $action, value: $value)';
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is PendingChange && other.id == id && other.action == action;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => id.hashCode ^ action.hashCode;
|
||||
}
|
||||
|
||||
class WebsocketState {
|
||||
final Socket? socket;
|
||||
final bool isConnected;
|
||||
final List<PendingChange> pendingChanges;
|
||||
|
||||
const WebsocketState({this.socket, required this.isConnected, required this.pendingChanges});
|
||||
const WebsocketState({this.socket, required this.isConnected});
|
||||
|
||||
WebsocketState copyWith({Socket? socket, bool? isConnected, List<PendingChange>? pendingChanges}) {
|
||||
return WebsocketState(
|
||||
socket: socket ?? this.socket,
|
||||
isConnected: isConnected ?? this.isConnected,
|
||||
pendingChanges: pendingChanges ?? this.pendingChanges,
|
||||
);
|
||||
WebsocketState copyWith({Socket? socket, bool? isConnected}) {
|
||||
return WebsocketState(socket: socket ?? this.socket, isConnected: isConnected ?? this.isConnected);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -72,11 +39,10 @@ class WebsocketState {
|
||||
}
|
||||
|
||||
class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
||||
WebsocketNotifier(this._ref) : super(const WebsocketState(socket: null, isConnected: false, pendingChanges: []));
|
||||
WebsocketNotifier(this._ref) : super(const WebsocketState(socket: null, isConnected: false));
|
||||
|
||||
final _log = Logger('WebsocketNotifier');
|
||||
final Ref _ref;
|
||||
final Debouncer _debounce = Debouncer(interval: const Duration(milliseconds: 500));
|
||||
|
||||
final Debouncer _batchDebouncer = Debouncer(
|
||||
interval: const Duration(seconds: 5),
|
||||
@@ -115,32 +81,21 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
||||
|
||||
socket.onConnect((_) {
|
||||
dPrint(() => "Established Websocket Connection");
|
||||
state = WebsocketState(isConnected: true, socket: socket, pendingChanges: state.pendingChanges);
|
||||
state = WebsocketState(isConnected: true, socket: socket);
|
||||
});
|
||||
|
||||
socket.onDisconnect((_) {
|
||||
dPrint(() => "Disconnect to Websocket Connection");
|
||||
state = WebsocketState(isConnected: false, socket: null, pendingChanges: state.pendingChanges);
|
||||
state = const WebsocketState(isConnected: false, socket: null);
|
||||
});
|
||||
|
||||
socket.on('error', (errorMessage) {
|
||||
_log.severe("Websocket Error - $errorMessage");
|
||||
state = WebsocketState(isConnected: false, socket: null, pendingChanges: state.pendingChanges);
|
||||
state = const WebsocketState(isConnected: false, socket: null);
|
||||
});
|
||||
|
||||
if (!Store.isBetaTimelineEnabled) {
|
||||
socket.on('on_upload_success', _handleOnUploadSuccess);
|
||||
socket.on('on_asset_delete', _handleOnAssetDelete);
|
||||
socket.on('on_asset_trash', _handleOnAssetTrash);
|
||||
socket.on('on_asset_restore', _handleServerUpdates);
|
||||
socket.on('on_asset_update', _handleServerUpdates);
|
||||
socket.on('on_asset_stack_update', _handleServerUpdates);
|
||||
socket.on('on_asset_hidden', _handleOnAssetHidden);
|
||||
} else {
|
||||
socket.on('AssetUploadReadyV1', _handleSyncAssetUploadReady);
|
||||
socket.on('AssetEditReadyV1', _handleSyncAssetEditReady);
|
||||
}
|
||||
|
||||
socket.on('AssetUploadReadyV1', _handleSyncAssetUploadReady);
|
||||
socket.on('AssetEditReadyV1', _handleSyncAssetEditReady);
|
||||
socket.on('on_config_update', _handleOnConfigUpdate);
|
||||
socket.on('on_new_release', _handleReleaseUpdates);
|
||||
} catch (e) {
|
||||
@@ -155,46 +110,7 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
||||
_batchedAssetUploadReady.clear();
|
||||
|
||||
state.socket?.dispose();
|
||||
state = WebsocketState(isConnected: false, socket: null, pendingChanges: state.pendingChanges);
|
||||
}
|
||||
|
||||
void stopListenToEvent(String eventName) {
|
||||
state.socket?.off(eventName);
|
||||
}
|
||||
|
||||
void stopListenToOldEvents() {
|
||||
state.socket?.off('on_upload_success');
|
||||
state.socket?.off('on_asset_delete');
|
||||
state.socket?.off('on_asset_trash');
|
||||
state.socket?.off('on_asset_restore');
|
||||
state.socket?.off('on_asset_update');
|
||||
state.socket?.off('on_asset_stack_update');
|
||||
state.socket?.off('on_asset_hidden');
|
||||
}
|
||||
|
||||
void startListeningToOldEvents() {
|
||||
state.socket?.on('on_upload_success', _handleOnUploadSuccess);
|
||||
state.socket?.on('on_asset_delete', _handleOnAssetDelete);
|
||||
state.socket?.on('on_asset_trash', _handleOnAssetTrash);
|
||||
state.socket?.on('on_asset_restore', _handleServerUpdates);
|
||||
state.socket?.on('on_asset_update', _handleServerUpdates);
|
||||
state.socket?.on('on_asset_stack_update', _handleServerUpdates);
|
||||
state.socket?.on('on_asset_hidden', _handleOnAssetHidden);
|
||||
}
|
||||
|
||||
void stopListeningToBetaEvents() {
|
||||
state.socket?.off('AssetUploadReadyV1');
|
||||
state.socket?.off('AssetEditReadyV1');
|
||||
}
|
||||
|
||||
void startListeningToBetaEvents() {
|
||||
state.socket?.on('AssetUploadReadyV1', _handleSyncAssetUploadReady);
|
||||
state.socket?.on('AssetEditReadyV1', _handleSyncAssetEditReady);
|
||||
}
|
||||
|
||||
void listenUploadEvent() {
|
||||
dPrint(() => "Start listening to event on_upload_success");
|
||||
state.socket?.on('on_upload_success', _handleOnUploadSuccess);
|
||||
state = const WebsocketState(isConnected: false, socket: null);
|
||||
}
|
||||
|
||||
Future<void> waitForEvent(String event, bool Function(dynamic)? predicate, Duration timeout) {
|
||||
@@ -218,89 +134,11 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
||||
);
|
||||
}
|
||||
|
||||
void addPendingChange(PendingAction action, dynamic value) {
|
||||
final now = DateTime.now();
|
||||
state = state.copyWith(
|
||||
pendingChanges: [...state.pendingChanges, PendingChange(now.millisecondsSinceEpoch.toString(), action, value)],
|
||||
);
|
||||
_debounce.run(handlePendingChanges);
|
||||
}
|
||||
|
||||
Future<void> _handlePendingTrashes() async {
|
||||
final trashChanges = state.pendingChanges.where((c) => c.action == PendingAction.assetTrash).toList();
|
||||
if (trashChanges.isNotEmpty) {
|
||||
List<String> remoteIds = trashChanges.expand((a) => (a.value as List).map((e) => e.toString())).toList();
|
||||
|
||||
await _ref.read(syncServiceProvider).handleRemoteAssetRemoval(remoteIds);
|
||||
await _ref.read(assetProvider.notifier).getAllAsset();
|
||||
|
||||
state = state.copyWith(pendingChanges: state.pendingChanges.whereNot((c) => trashChanges.contains(c)).toList());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handlePendingDeletes() async {
|
||||
final deleteChanges = state.pendingChanges.where((c) => c.action == PendingAction.assetDelete).toList();
|
||||
if (deleteChanges.isNotEmpty) {
|
||||
List<String> remoteIds = deleteChanges.map((a) => a.value.toString()).toList();
|
||||
await _ref.read(syncServiceProvider).handleRemoteAssetRemoval(remoteIds);
|
||||
state = state.copyWith(pendingChanges: state.pendingChanges.whereNot((c) => deleteChanges.contains(c)).toList());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handlePendingUploaded() async {
|
||||
final uploadedChanges = state.pendingChanges.where((c) => c.action == PendingAction.assetUploaded).toList();
|
||||
if (uploadedChanges.isNotEmpty) {
|
||||
List<AssetResponseDto?> remoteAssets = uploadedChanges.map((a) => AssetResponseDto.fromJson(a.value)).toList();
|
||||
for (final dto in remoteAssets) {
|
||||
if (dto != null) {
|
||||
final newAsset = Asset.remote(dto);
|
||||
await _ref.watch(assetProvider.notifier).onNewAssetUploaded(newAsset);
|
||||
}
|
||||
}
|
||||
state = state.copyWith(
|
||||
pendingChanges: state.pendingChanges.whereNot((c) => uploadedChanges.contains(c)).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handlingPendingHidden() async {
|
||||
final hiddenChanges = state.pendingChanges.where((c) => c.action == PendingAction.assetHidden).toList();
|
||||
if (hiddenChanges.isNotEmpty) {
|
||||
List<String> remoteIds = hiddenChanges.map((a) => a.value.toString()).toList();
|
||||
final db = _ref.watch(dbProvider);
|
||||
await db.writeTxn(() => db.assets.deleteAllByRemoteId(remoteIds));
|
||||
|
||||
state = state.copyWith(pendingChanges: state.pendingChanges.whereNot((c) => hiddenChanges.contains(c)).toList());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> handlePendingChanges() async {
|
||||
await _handlePendingUploaded();
|
||||
await _handlePendingDeletes();
|
||||
await _handlingPendingHidden();
|
||||
await _handlePendingTrashes();
|
||||
}
|
||||
|
||||
void _handleOnConfigUpdate(dynamic _) {
|
||||
_ref.read(serverInfoProvider.notifier).getServerFeatures();
|
||||
_ref.read(serverInfoProvider.notifier).getServerConfig();
|
||||
}
|
||||
|
||||
// Refresh updated assets
|
||||
void _handleServerUpdates(dynamic _) {
|
||||
_ref.read(assetProvider.notifier).getAllAsset();
|
||||
}
|
||||
|
||||
void _handleOnUploadSuccess(dynamic data) => addPendingChange(PendingAction.assetUploaded, data);
|
||||
|
||||
void _handleOnAssetDelete(dynamic data) => addPendingChange(PendingAction.assetDelete, data);
|
||||
|
||||
void _handleOnAssetTrash(dynamic data) {
|
||||
addPendingChange(PendingAction.assetTrash, data);
|
||||
}
|
||||
|
||||
void _handleOnAssetHidden(dynamic data) => addPendingChange(PendingAction.assetHidden, data);
|
||||
|
||||
_handleReleaseUpdates(dynamic data) {
|
||||
// Json guard
|
||||
if (data is! Map) {
|
||||
|
||||
Reference in New Issue
Block a user