mirror of
https://github.com/immich-app/immich.git
synced 2025-07-31 15:08:44 -04:00
feat: new upload (cont) (#20029)
* new upload button * wip * pr feedback * fix: updateAll override album selection value * feat: status box * feat: handle upload resume * re-enable websocket event * fix: update state condition and upload status * Better backup detail page --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
This commit is contained in:
parent
1dc62fce5f
commit
4d27f187ea
@ -406,6 +406,7 @@
|
|||||||
"album_options": "Album options",
|
"album_options": "Album options",
|
||||||
"album_remove_user": "Remove user?",
|
"album_remove_user": "Remove user?",
|
||||||
"album_remove_user_confirmation": "Are you sure you want to remove {user}?",
|
"album_remove_user_confirmation": "Are you sure you want to remove {user}?",
|
||||||
|
"album_search_not_found": "No albums found matching your search",
|
||||||
"album_share_no_users": "Looks like you have shared this album with all users or you don't have any user to share with.",
|
"album_share_no_users": "Looks like you have shared this album with all users or you don't have any user to share with.",
|
||||||
"album_updated": "Album updated",
|
"album_updated": "Album updated",
|
||||||
"album_updated_setting_description": "Receive an email notification when a shared album has new assets",
|
"album_updated_setting_description": "Receive an email notification when a shared album has new assets",
|
||||||
@ -425,6 +426,7 @@
|
|||||||
"albums_default_sort_order": "Default album sort order",
|
"albums_default_sort_order": "Default album sort order",
|
||||||
"albums_default_sort_order_description": "Initial asset sort order when creating new albums.",
|
"albums_default_sort_order_description": "Initial asset sort order when creating new albums.",
|
||||||
"albums_feature_description": "Collections of assets that can be shared with other users.",
|
"albums_feature_description": "Collections of assets that can be shared with other users.",
|
||||||
|
"albums_on_device_count": "Albums on device ({count})",
|
||||||
"all": "All",
|
"all": "All",
|
||||||
"all_albums": "All albums",
|
"all_albums": "All albums",
|
||||||
"all_people": "All people",
|
"all_people": "All people",
|
||||||
@ -605,6 +607,7 @@
|
|||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"cancel_search": "Cancel search",
|
"cancel_search": "Cancel search",
|
||||||
"canceled": "Canceled",
|
"canceled": "Canceled",
|
||||||
|
"canceling": "Canceling",
|
||||||
"cannot_merge_people": "Cannot merge people",
|
"cannot_merge_people": "Cannot merge people",
|
||||||
"cannot_undo_this_action": "You cannot undo this action!",
|
"cannot_undo_this_action": "You cannot undo this action!",
|
||||||
"cannot_update_the_description": "Cannot update the description",
|
"cannot_update_the_description": "Cannot update the description",
|
||||||
@ -765,6 +768,7 @@
|
|||||||
"description": "Description",
|
"description": "Description",
|
||||||
"description_input_hint_text": "Add description...",
|
"description_input_hint_text": "Add description...",
|
||||||
"description_input_submit_error": "Error updating description, check the log for more details",
|
"description_input_submit_error": "Error updating description, check the log for more details",
|
||||||
|
"deselect_all": "Deselect All",
|
||||||
"details": "Details",
|
"details": "Details",
|
||||||
"direction": "Direction",
|
"direction": "Direction",
|
||||||
"disabled": "Disabled",
|
"disabled": "Disabled",
|
||||||
@ -839,6 +843,7 @@
|
|||||||
"empty_trash": "Empty trash",
|
"empty_trash": "Empty trash",
|
||||||
"empty_trash_confirmation": "Are you sure you want to empty the trash? This will remove all the assets in trash permanently from Immich.\nYou cannot undo this action!",
|
"empty_trash_confirmation": "Are you sure you want to empty the trash? This will remove all the assets in trash permanently from Immich.\nYou cannot undo this action!",
|
||||||
"enable": "Enable",
|
"enable": "Enable",
|
||||||
|
"enable_backup": "Enable Backup",
|
||||||
"enable_biometric_auth_description": "Enter your PIN code to enable biometric authentication",
|
"enable_biometric_auth_description": "Enter your PIN code to enable biometric authentication",
|
||||||
"enabled": "Enabled",
|
"enabled": "Enabled",
|
||||||
"end_date": "End date",
|
"end_date": "End date",
|
||||||
@ -1485,6 +1490,7 @@
|
|||||||
"purchase_server_description_2": "Supporter status",
|
"purchase_server_description_2": "Supporter status",
|
||||||
"purchase_server_title": "Server",
|
"purchase_server_title": "Server",
|
||||||
"purchase_settings_server_activated": "The server product key is managed by the admin",
|
"purchase_settings_server_activated": "The server product key is managed by the admin",
|
||||||
|
"queue_status": "Queuing {count}/{total}",
|
||||||
"rating": "Star rating",
|
"rating": "Star rating",
|
||||||
"rating_clear": "Clear rating",
|
"rating_clear": "Clear rating",
|
||||||
"rating_count": "{count, plural, one {# star} other {# stars}}",
|
"rating_count": "{count, plural, one {# star} other {# stars}}",
|
||||||
@ -1915,6 +1921,7 @@
|
|||||||
"updated_password": "Updated password",
|
"updated_password": "Updated password",
|
||||||
"upload": "Upload",
|
"upload": "Upload",
|
||||||
"upload_concurrency": "Upload concurrency",
|
"upload_concurrency": "Upload concurrency",
|
||||||
|
"upload_details": "Upload Details",
|
||||||
"upload_dialog_info": "Do you want to backup the selected Asset(s) to the server?",
|
"upload_dialog_info": "Do you want to backup the selected Asset(s) to the server?",
|
||||||
"upload_dialog_title": "Upload Asset",
|
"upload_dialog_title": "Upload Asset",
|
||||||
"upload_errors": "Upload completed with {count, plural, one {# error} other {# errors}}, refresh the page to see new upload assets.",
|
"upload_errors": "Upload completed with {count, plural, one {# error} other {# errors}}, refresh the page to see new upload assets.",
|
||||||
@ -1965,6 +1972,7 @@
|
|||||||
"view_album": "View Album",
|
"view_album": "View Album",
|
||||||
"view_all": "View All",
|
"view_all": "View All",
|
||||||
"view_all_users": "View all users",
|
"view_all_users": "View all users",
|
||||||
|
"view_details": "View Details",
|
||||||
"view_in_timeline": "View in timeline",
|
"view_in_timeline": "View in timeline",
|
||||||
"view_link": "View link",
|
"view_link": "View link",
|
||||||
"view_links": "View links",
|
"view_links": "View links",
|
||||||
|
@ -83,6 +83,7 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
|
|||||||
|
|
||||||
flutterLoader.ensureInitializationCompleteAsync(ctx, null, Handler(Looper.getMainLooper())) {
|
flutterLoader.ensureInitializationCompleteAsync(ctx, null, Handler(Looper.getMainLooper())) {
|
||||||
runDart()
|
runDart()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return resolvableFuture
|
return resolvableFuture
|
||||||
|
@ -38,3 +38,6 @@ const List<(String, String)> kWidgetNames = [
|
|||||||
('com.immich.widget.random', 'app.alextran.immich.widget.RandomReceiver'),
|
('com.immich.widget.random', 'app.alextran.immich.widget.RandomReceiver'),
|
||||||
('com.immich.widget.memory', 'app.alextran.immich.widget.MemoryReceiver'),
|
('com.immich.widget.memory', 'app.alextran.immich.widget.MemoryReceiver'),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const double kUploadStatusFailed = -1.0;
|
||||||
|
const double kUploadStatusCanceled = -2.0;
|
||||||
|
@ -70,7 +70,8 @@ enum StoreKey<T> {
|
|||||||
// Experimental stuff
|
// Experimental stuff
|
||||||
photoManagerCustomFilter<bool>._(1000),
|
photoManagerCustomFilter<bool>._(1000),
|
||||||
betaPromptShown<bool>._(1001),
|
betaPromptShown<bool>._(1001),
|
||||||
betaTimeline<bool>._(1002);
|
betaTimeline<bool>._(1002),
|
||||||
|
enableBackup<bool>._(1003);
|
||||||
|
|
||||||
const StoreKey._(this.id);
|
const StoreKey._(this.id);
|
||||||
final int id;
|
final int id;
|
||||||
|
@ -7,8 +7,8 @@ class LocalAlbumService {
|
|||||||
|
|
||||||
const LocalAlbumService(this._repository);
|
const LocalAlbumService(this._repository);
|
||||||
|
|
||||||
Future<List<LocalAlbum>> getAll() {
|
Future<List<LocalAlbum>> getAll({Set<SortLocalAlbumsBy> sortBy = const {}}) {
|
||||||
return _repository.getAll();
|
return _repository.getAll(sortBy: sortBy);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<LocalAsset?> getThumbnail(String albumId) {
|
Future<LocalAsset?> getThumbnail(String albumId) {
|
||||||
|
@ -101,14 +101,18 @@ class BackgroundSyncManager {
|
|||||||
if (_syncWebsocketTask != null) {
|
if (_syncWebsocketTask != null) {
|
||||||
return _syncWebsocketTask!.future;
|
return _syncWebsocketTask!.future;
|
||||||
}
|
}
|
||||||
|
_syncWebsocketTask = _handleWsAssetUploadReadyV1Batch(batchData);
|
||||||
_syncWebsocketTask = runInIsolateGentle(
|
|
||||||
computation: (ref) => ref
|
|
||||||
.read(syncStreamServiceProvider)
|
|
||||||
.handleWsAssetUploadReadyV1Batch(batchData),
|
|
||||||
);
|
|
||||||
return _syncWebsocketTask!.whenComplete(() {
|
return _syncWebsocketTask!.whenComplete(() {
|
||||||
_syncWebsocketTask = null;
|
_syncWebsocketTask = null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Cancelable<void> _handleWsAssetUploadReadyV1Batch(
|
||||||
|
List<dynamic> batchData,
|
||||||
|
) =>
|
||||||
|
runInIsolateGentle(
|
||||||
|
computation: (ref) => ref
|
||||||
|
.read(syncStreamServiceProvider)
|
||||||
|
.handleWsAssetUploadReadyV1Batch(batchData),
|
||||||
|
);
|
||||||
|
@ -33,6 +33,10 @@ extension ContextHelper on BuildContext {
|
|||||||
|
|
||||||
// Returns the current Primary color of the Theme
|
// Returns the current Primary color of the Theme
|
||||||
Color get primaryColor => themeData.colorScheme.primary;
|
Color get primaryColor => themeData.colorScheme.primary;
|
||||||
|
Color get logoYellow => const Color.fromARGB(255, 255, 184, 0);
|
||||||
|
Color get logoRed => const Color.fromARGB(255, 230, 65, 30);
|
||||||
|
Color get logoPink => const Color.fromARGB(255, 222, 127, 179);
|
||||||
|
Color get logoGreen => const Color.fromARGB(255, 49, 164, 82);
|
||||||
|
|
||||||
// Returns the Scaffold background color of the Theme
|
// Returns the Scaffold background color of the Theme
|
||||||
Color get scaffoldBackgroundColor => colorScheme.surface;
|
Color get scaffoldBackgroundColor => colorScheme.surface;
|
||||||
|
@ -52,9 +52,7 @@ class DriftBackupRepository extends DriftDatabaseRepository {
|
|||||||
|
|
||||||
Future<int> getRemainderCount() async {
|
Future<int> getRemainderCount() async {
|
||||||
final query = _db.localAlbumAssetEntity.selectOnly(distinct: true)
|
final query = _db.localAlbumAssetEntity.selectOnly(distinct: true)
|
||||||
..addColumns(
|
..addColumns([_db.localAlbumAssetEntity.assetId])
|
||||||
[_db.localAlbumAssetEntity.assetId],
|
|
||||||
)
|
|
||||||
..join([
|
..join([
|
||||||
innerJoin(
|
innerJoin(
|
||||||
_db.localAlbumEntity,
|
_db.localAlbumEntity,
|
||||||
@ -147,6 +145,11 @@ class DriftBackupRepository extends DriftDatabaseRepository {
|
|||||||
),
|
),
|
||||||
) &
|
) &
|
||||||
lae.id.isNotInQuery(_getExcludedSubquery()),
|
lae.id.isNotInQuery(_getExcludedSubquery()),
|
||||||
|
)
|
||||||
|
..orderBy(
|
||||||
|
[
|
||||||
|
(localAsset) => OrderingTerm.desc(localAsset.createdAt),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
return query.map((localAsset) => localAsset.toDto()).get();
|
return query.map((localAsset) => localAsset.toDto()).get();
|
||||||
|
@ -8,7 +8,13 @@ import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
|||||||
import 'package:immich_mobile/utils/database.utils.dart';
|
import 'package:immich_mobile/utils/database.utils.dart';
|
||||||
import 'package:platform/platform.dart';
|
import 'package:platform/platform.dart';
|
||||||
|
|
||||||
enum SortLocalAlbumsBy { id, backupSelection, isIosSharedAlbum }
|
enum SortLocalAlbumsBy {
|
||||||
|
id,
|
||||||
|
backupSelection,
|
||||||
|
isIosSharedAlbum,
|
||||||
|
name,
|
||||||
|
assetCount
|
||||||
|
}
|
||||||
|
|
||||||
class DriftLocalAlbumRepository extends DriftDatabaseRepository {
|
class DriftLocalAlbumRepository extends DriftDatabaseRepository {
|
||||||
final Drift _db;
|
final Drift _db;
|
||||||
@ -41,6 +47,9 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
|
|||||||
OrderingTerm.asc(_db.localAlbumEntity.backupSelection),
|
OrderingTerm.asc(_db.localAlbumEntity.backupSelection),
|
||||||
SortLocalAlbumsBy.isIosSharedAlbum =>
|
SortLocalAlbumsBy.isIosSharedAlbum =>
|
||||||
OrderingTerm.asc(_db.localAlbumEntity.isIosSharedAlbum),
|
OrderingTerm.asc(_db.localAlbumEntity.isIosSharedAlbum),
|
||||||
|
SortLocalAlbumsBy.name =>
|
||||||
|
OrderingTerm.asc(_db.localAlbumEntity.name),
|
||||||
|
SortLocalAlbumsBy.assetCount => OrderingTerm.desc(assetCount),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -151,7 +160,15 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
|
|||||||
batch.insert(
|
batch.insert(
|
||||||
_db.localAlbumEntity,
|
_db.localAlbumEntity,
|
||||||
companion,
|
companion,
|
||||||
onConflict: DoUpdate((_) => companion),
|
onConflict: DoUpdate(
|
||||||
|
(old) => LocalAlbumEntityCompanion(
|
||||||
|
id: companion.id,
|
||||||
|
name: companion.name,
|
||||||
|
updatedAt: companion.updatedAt,
|
||||||
|
isIosSharedAlbum: companion.isIosSharedAlbum,
|
||||||
|
marker_: companion.marker_,
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -96,8 +96,8 @@ Future<void> initApp() async {
|
|||||||
// Initialize the file downloader
|
// Initialize the file downloader
|
||||||
|
|
||||||
await FileDownloader().configure(
|
await FileDownloader().configure(
|
||||||
// maxConcurrent: 5, maxConcurrentByHost: 2, maxConcurrentByGroup: 3
|
// maxConcurrent: 6, maxConcurrentByHost(server):6, maxConcurrentByGroup: 3
|
||||||
globalConfig: (Config.holdingQueue, (5, 2, 3)),
|
globalConfig: (Config.holdingQueue, (6, 6, 3)),
|
||||||
);
|
);
|
||||||
|
|
||||||
await FileDownloader().trackTasksInGroup(
|
await FileDownloader().trackTasksInGroup(
|
||||||
|
@ -1,83 +1,61 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/backup/backup_toggle_button.widget.dart';
|
||||||
import 'package:immich_mobile/providers/backup/backup_album.provider.dart';
|
import 'package:immich_mobile/providers/backup/backup_album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
||||||
import 'package:immich_mobile/providers/websocket.provider.dart';
|
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/widgets/backup/backup_info_card.dart';
|
import 'package:immich_mobile/widgets/backup/backup_info_card.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class DriftBackupPage extends HookConsumerWidget {
|
class DriftBackupPage extends ConsumerStatefulWidget {
|
||||||
const DriftBackupPage({super.key});
|
const DriftBackupPage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
ConsumerState<DriftBackupPage> createState() => _DriftBackupPageState();
|
||||||
useEffect(
|
}
|
||||||
() {
|
|
||||||
ref.read(driftBackupProvider.notifier).getBackupStatus();
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget buildControlButtons() {
|
class _DriftBackupPageState extends ConsumerState<DriftBackupPage> {
|
||||||
return Padding(
|
@override
|
||||||
padding: const EdgeInsets.only(
|
void initState() {
|
||||||
top: 24,
|
super.initState();
|
||||||
),
|
ref.read(driftBackupProvider.notifier).getBackupStatus();
|
||||||
child: Column(
|
}
|
||||||
children: [
|
|
||||||
ElevatedButton(
|
Future<void> startBackup() async {
|
||||||
onPressed: () => ref.read(driftBackupProvider.notifier).backup(),
|
await ref.read(driftBackupProvider.notifier).getBackupStatus();
|
||||||
child: const Text(
|
await ref.read(driftBackupProvider.notifier).backup();
|
||||||
"backup_controller_page_start_backup",
|
}
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16,
|
Future<void> stopBackup() async {
|
||||||
fontWeight: FontWeight.bold,
|
await ref.read(driftBackupProvider.notifier).cancel();
|
||||||
),
|
}
|
||||||
).tr(),
|
|
||||||
),
|
@override
|
||||||
OutlinedButton(
|
Widget build(BuildContext context) {
|
||||||
onPressed: () => ref.read(driftBackupProvider.notifier).cancel(),
|
final selectedAlbum = ref
|
||||||
child: const Text(
|
.watch(backupAlbumProvider)
|
||||||
"cancel",
|
.where(
|
||||||
style: TextStyle(
|
(album) => album.backupSelection == BackupSelection.selected,
|
||||||
fontSize: 16,
|
)
|
||||||
fontWeight: FontWeight.bold,
|
.toList();
|
||||||
),
|
final uploadItems = ref.watch(
|
||||||
).tr(),
|
driftBackupProvider.select((state) => state.uploadItems),
|
||||||
),
|
);
|
||||||
OutlinedButton(
|
|
||||||
onPressed: () =>
|
|
||||||
ref.read(driftBackupProvider.notifier).getDataInfo(),
|
|
||||||
child: const Text(
|
|
||||||
"Get database info",
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
).tr(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
title: const Text(
|
title: Text(
|
||||||
"Backup (Experimental)",
|
"backup_controller_page_backup".t(),
|
||||||
),
|
),
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ref.watch(websocketProvider.notifier).listenUploadEvent();
|
|
||||||
context.maybePop(true);
|
context.maybePop(true);
|
||||||
},
|
},
|
||||||
splashRadius: 24,
|
splashRadius: 24,
|
||||||
@ -85,18 +63,6 @@ class DriftBackupPage extends HookConsumerWidget {
|
|||||||
Icons.arrow_back_ios_rounded,
|
Icons.arrow_back_ios_rounded,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
actions: [
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(right: 8.0),
|
|
||||||
child: IconButton(
|
|
||||||
onPressed: () => context.pushRoute(const BackupOptionsRoute()),
|
|
||||||
splashRadius: 24,
|
|
||||||
icon: const Icon(
|
|
||||||
Icons.settings_outlined,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
body: Stack(
|
body: Stack(
|
||||||
children: [
|
children: [
|
||||||
@ -110,11 +76,24 @@ class DriftBackupPage extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
const _BackupAlbumSelectionCard(),
|
const _BackupAlbumSelectionCard(),
|
||||||
const _TotalCard(),
|
if (selectedAlbum.isNotEmpty) ...[
|
||||||
const _BackupCard(),
|
const _TotalCard(),
|
||||||
const _RemainderCard(),
|
const _BackupCard(),
|
||||||
const Divider(),
|
const _RemainderCard(),
|
||||||
buildControlButtons(),
|
const Divider(),
|
||||||
|
BackupToggleButton(
|
||||||
|
onStart: () async => await startBackup(),
|
||||||
|
onStop: () async => await stopBackup(),
|
||||||
|
),
|
||||||
|
if (uploadItems.isNotEmpty)
|
||||||
|
TextButton.icon(
|
||||||
|
icon: const Icon(Icons.info_outline_rounded),
|
||||||
|
onPressed: () => context.pushRoute(
|
||||||
|
const DriftUploadDetailRoute(),
|
||||||
|
),
|
||||||
|
label: Text("view_details".t(context: context)),
|
||||||
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -1,25 +1,67 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||||
|
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/backup/backup_album.provider.dart';
|
import 'package:immich_mobile/providers/backup/backup_album.provider.dart';
|
||||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
|
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||||
import 'package:immich_mobile/widgets/backup/drift_album_info_list_tile.dart';
|
import 'package:immich_mobile/widgets/backup/drift_album_info_list_tile.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
|
import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/search_field.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class DriftBackupAlbumSelectionPage extends HookConsumerWidget {
|
class DriftBackupAlbumSelectionPage extends ConsumerStatefulWidget {
|
||||||
const DriftBackupAlbumSelectionPage({super.key});
|
const DriftBackupAlbumSelectionPage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
ConsumerState<DriftBackupAlbumSelectionPage> createState() =>
|
||||||
|
_DriftBackupAlbumSelectionPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DriftBackupAlbumSelectionPageState
|
||||||
|
extends ConsumerState<DriftBackupAlbumSelectionPage> {
|
||||||
|
String _searchQuery = '';
|
||||||
|
bool _isSearchMode = false;
|
||||||
|
late ValueNotifier<bool> _enableSyncUploadAlbum;
|
||||||
|
late TextEditingController _searchController;
|
||||||
|
late FocusNode _searchFocusNode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_enableSyncUploadAlbum = ValueNotifier<bool>(false);
|
||||||
|
_searchController = TextEditingController();
|
||||||
|
_searchFocusNode = FocusNode();
|
||||||
|
|
||||||
|
_enableSyncUploadAlbum.value = ref
|
||||||
|
.read(appSettingsServiceProvider)
|
||||||
|
.getSetting(AppSettingsEnum.syncAlbums);
|
||||||
|
ref.read(backupAlbumProvider.notifier).getAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_enableSyncUploadAlbum.dispose();
|
||||||
|
_searchController.dispose();
|
||||||
|
_searchFocusNode.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
final albums = ref.watch(backupAlbumProvider);
|
final albums = ref.watch(backupAlbumProvider);
|
||||||
|
final albumCount = albums.length;
|
||||||
|
// Filter albums based on search query
|
||||||
|
final filteredAlbums = albums.where((album) {
|
||||||
|
if (_searchQuery.isEmpty) return true;
|
||||||
|
return album.name.toLowerCase().contains(_searchQuery.toLowerCase());
|
||||||
|
}).toList();
|
||||||
|
|
||||||
final selectedBackupAlbums = albums
|
final selectedBackupAlbums = albums
|
||||||
.where((album) => album.backupSelection == BackupSelection.selected)
|
.where((album) => album.backupSelection == BackupSelection.selected)
|
||||||
@ -27,133 +69,6 @@ class DriftBackupAlbumSelectionPage extends HookConsumerWidget {
|
|||||||
final excludedBackupAlbums = albums
|
final excludedBackupAlbums = albums
|
||||||
.where((album) => album.backupSelection == BackupSelection.excluded)
|
.where((album) => album.backupSelection == BackupSelection.excluded)
|
||||||
.toList();
|
.toList();
|
||||||
final enableSyncUploadAlbum =
|
|
||||||
useAppSettingsState(AppSettingsEnum.syncAlbums);
|
|
||||||
final isDarkTheme = context.isDarkTheme;
|
|
||||||
|
|
||||||
useEffect(
|
|
||||||
() {
|
|
||||||
ref.watch(backupProvider.notifier).getBackupInfo();
|
|
||||||
ref.watch(backupAlbumProvider.notifier).getAll();
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
buildAlbumSelectionList() {
|
|
||||||
if (albums.isEmpty) {
|
|
||||||
return const SliverToBoxAdapter(
|
|
||||||
child: Center(
|
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return SliverPadding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12.0),
|
|
||||||
sliver: SliverList(
|
|
||||||
delegate: SliverChildBuilderDelegate(
|
|
||||||
((context, index) {
|
|
||||||
return DriftAlbumInfoListTile(
|
|
||||||
album: albums[index],
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
childCount: albums.length,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
buildAlbumSelectionGrid() {
|
|
||||||
if (albums.isEmpty) {
|
|
||||||
return const SliverToBoxAdapter(
|
|
||||||
child: Center(
|
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return SliverPadding(
|
|
||||||
padding: const EdgeInsets.all(12.0),
|
|
||||||
sliver: SliverGrid.builder(
|
|
||||||
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
|
||||||
maxCrossAxisExtent: 300,
|
|
||||||
mainAxisSpacing: 12,
|
|
||||||
crossAxisSpacing: 12,
|
|
||||||
),
|
|
||||||
itemCount: albums.length,
|
|
||||||
itemBuilder: ((context, index) {
|
|
||||||
return DriftAlbumInfoListTile(
|
|
||||||
album: albums[index],
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
buildSelectedAlbumNameChip() {
|
|
||||||
return selectedBackupAlbums.map((album) {
|
|
||||||
void removeSelection() {
|
|
||||||
ref.read(backupAlbumProvider.notifier).deselectAlbum(album);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(right: 8.0),
|
|
||||||
child: GestureDetector(
|
|
||||||
onTap: removeSelection,
|
|
||||||
child: Chip(
|
|
||||||
label: Text(
|
|
||||||
album.name,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: isDarkTheme ? Colors.black : Colors.white,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
backgroundColor: context.primaryColor,
|
|
||||||
deleteIconColor: isDarkTheme ? Colors.black : Colors.white,
|
|
||||||
deleteIcon: const Icon(
|
|
||||||
Icons.cancel_rounded,
|
|
||||||
size: 15,
|
|
||||||
),
|
|
||||||
onDeleted: removeSelection,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}).toSet();
|
|
||||||
}
|
|
||||||
|
|
||||||
buildExcludedAlbumNameChip() {
|
|
||||||
return excludedBackupAlbums.map((album) {
|
|
||||||
void removeSelection() {
|
|
||||||
ref.read(backupAlbumProvider.notifier).deselectAlbum(album);
|
|
||||||
}
|
|
||||||
|
|
||||||
return GestureDetector(
|
|
||||||
onTap: removeSelection,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(right: 8.0),
|
|
||||||
child: Chip(
|
|
||||||
label: Text(
|
|
||||||
album.name,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: context.scaffoldBackgroundColor,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
backgroundColor: Colors.red[300],
|
|
||||||
deleteIconColor: context.scaffoldBackgroundColor,
|
|
||||||
deleteIcon: const Icon(
|
|
||||||
Icons.cancel_rounded,
|
|
||||||
size: 15,
|
|
||||||
),
|
|
||||||
onDeleted: removeSelection,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}).toSet();
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSyncAlbumToggle(bool isEnable) async {
|
handleSyncAlbumToggle(bool isEnable) async {
|
||||||
if (isEnable) {
|
if (isEnable) {
|
||||||
@ -170,138 +85,439 @@ class DriftBackupAlbumSelectionPage extends HookConsumerWidget {
|
|||||||
onPressed: () => context.maybePop(),
|
onPressed: () => context.maybePop(),
|
||||||
icon: const Icon(Icons.arrow_back_ios_rounded),
|
icon: const Icon(Icons.arrow_back_ios_rounded),
|
||||||
),
|
),
|
||||||
title: const Text(
|
title: _isSearchMode
|
||||||
"backup_album_selection_page_select_albums",
|
? SearchField(
|
||||||
).tr(),
|
hintText: 'search_albums'.t(context: context),
|
||||||
|
autofocus: true,
|
||||||
|
controller: _searchController,
|
||||||
|
focusNode: _searchFocusNode,
|
||||||
|
onChanged: (value) =>
|
||||||
|
setState(() => _searchQuery = value.trim()),
|
||||||
|
)
|
||||||
|
: const Text(
|
||||||
|
"backup_album_selection_page_select_albums",
|
||||||
|
).t(context: context),
|
||||||
|
actions: [
|
||||||
|
if (!_isSearchMode)
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.search),
|
||||||
|
onPressed: () => setState(() {
|
||||||
|
_isSearchMode = true;
|
||||||
|
_searchQuery = '';
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.close),
|
||||||
|
onPressed: () => setState(() {
|
||||||
|
_isSearchMode = false;
|
||||||
|
_searchQuery = '';
|
||||||
|
_searchController.clear();
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
],
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
),
|
),
|
||||||
body: SafeArea(
|
body: CustomScrollView(
|
||||||
child: CustomScrollView(
|
physics: const ClampingScrollPhysics(),
|
||||||
physics: const ClampingScrollPhysics(),
|
slivers: [
|
||||||
slivers: [
|
SliverToBoxAdapter(
|
||||||
SliverToBoxAdapter(
|
child: Column(
|
||||||
child: Column(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
children: [
|
||||||
children: [
|
Padding(
|
||||||
Padding(
|
padding: const EdgeInsets.symmetric(
|
||||||
padding: const EdgeInsets.symmetric(
|
vertical: 8.0,
|
||||||
vertical: 8.0,
|
horizontal: 16.0,
|
||||||
horizontal: 16.0,
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
"backup_album_selection_page_selection_info",
|
|
||||||
style: context.textTheme.titleSmall,
|
|
||||||
).tr(),
|
|
||||||
),
|
),
|
||||||
// Selected Album Chips
|
child: Text(
|
||||||
|
"backup_album_selection_page_selection_info",
|
||||||
|
style: context.textTheme.titleSmall,
|
||||||
|
).t(context: context),
|
||||||
|
),
|
||||||
|
// Selected Album Chips
|
||||||
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
child: Wrap(
|
child: Wrap(
|
||||||
children: [
|
children: [
|
||||||
...buildSelectedAlbumNameChip(),
|
_SelectedAlbumNameChips(
|
||||||
...buildExcludedAlbumNameChip(),
|
selectedBackupAlbums: selectedBackupAlbums,
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
SettingsSwitchListTile(
|
|
||||||
valueNotifier: enableSyncUploadAlbum,
|
|
||||||
title: "sync_albums".tr(),
|
|
||||||
subtitle: "sync_upload_album_setting_subtitle".tr(),
|
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
titleStyle: context.textTheme.bodyLarge?.copyWith(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
subtitleStyle: context.textTheme.labelLarge?.copyWith(
|
|
||||||
color: context.colorScheme.primary,
|
|
||||||
),
|
|
||||||
onChanged: handleSyncAlbumToggle,
|
|
||||||
),
|
|
||||||
|
|
||||||
ListTile(
|
|
||||||
title: Text(
|
|
||||||
"backup_album_selection_page_albums_device".tr(
|
|
||||||
namedArgs: {
|
|
||||||
'count': ref
|
|
||||||
.watch(backupProvider)
|
|
||||||
.availableAlbums
|
|
||||||
.length
|
|
||||||
.toString(),
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
style: context.textTheme.titleSmall,
|
_ExcludedAlbumNameChips(
|
||||||
|
excludedBackupAlbums: excludedBackupAlbums,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
SettingsSwitchListTile(
|
||||||
|
valueNotifier: _enableSyncUploadAlbum,
|
||||||
|
title: "sync_albums".t(context: context),
|
||||||
|
subtitle:
|
||||||
|
"sync_upload_album_setting_subtitle".t(context: context),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
titleStyle: context.textTheme.bodyLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
subtitleStyle: context.textTheme.labelLarge?.copyWith(
|
||||||
|
color: context.colorScheme.primary,
|
||||||
|
),
|
||||||
|
onChanged: handleSyncAlbumToggle,
|
||||||
|
),
|
||||||
|
|
||||||
|
ListTile(
|
||||||
|
title: Text(
|
||||||
|
"albums_on_device_count".t(
|
||||||
|
context: context,
|
||||||
|
args: {'count': albumCount.toString()},
|
||||||
),
|
),
|
||||||
subtitle: Padding(
|
style: context.textTheme.titleSmall,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
),
|
||||||
child: Text(
|
subtitle: Padding(
|
||||||
"backup_album_selection_page_albums_tap",
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
style: context.textTheme.labelLarge?.copyWith(
|
child: Text(
|
||||||
color: context.primaryColor,
|
"backup_album_selection_page_albums_tap",
|
||||||
),
|
style: context.textTheme.labelLarge?.copyWith(
|
||||||
).tr(),
|
|
||||||
),
|
|
||||||
trailing: IconButton(
|
|
||||||
splashRadius: 16,
|
|
||||||
icon: Icon(
|
|
||||||
Icons.info,
|
|
||||||
size: 20,
|
|
||||||
color: context.primaryColor,
|
color: context.primaryColor,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
).t(context: context),
|
||||||
// show the dialog
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (BuildContext context) {
|
|
||||||
return AlertDialog(
|
|
||||||
shape: const RoundedRectangleBorder(
|
|
||||||
borderRadius:
|
|
||||||
BorderRadius.all(Radius.circular(10)),
|
|
||||||
),
|
|
||||||
elevation: 5,
|
|
||||||
title: Text(
|
|
||||||
'backup_album_selection_page_selection_info',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: context.primaryColor,
|
|
||||||
),
|
|
||||||
).tr(),
|
|
||||||
content: SingleChildScrollView(
|
|
||||||
child: ListBody(
|
|
||||||
children: [
|
|
||||||
const Text(
|
|
||||||
'backup_album_selection_page_assets_scatter',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
),
|
|
||||||
).tr(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
trailing: IconButton(
|
||||||
|
splashRadius: 16,
|
||||||
|
icon: Icon(
|
||||||
|
Icons.info,
|
||||||
|
size: 20,
|
||||||
|
color: context.primaryColor,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
// show the dialog
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius.all(Radius.circular(10)),
|
||||||
|
),
|
||||||
|
elevation: 5,
|
||||||
|
title: Text(
|
||||||
|
'backup_album_selection_page_selection_info',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: context.primaryColor,
|
||||||
|
),
|
||||||
|
).t(context: context),
|
||||||
|
content: SingleChildScrollView(
|
||||||
|
child: ListBody(
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'backup_album_selection_page_assets_scatter',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
).t(context: context),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
// buildSearchBar(),
|
if (Platform.isAndroid)
|
||||||
],
|
_SelectAllButton(
|
||||||
),
|
filteredAlbums: filteredAlbums,
|
||||||
|
selectedBackupAlbums: selectedBackupAlbums,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
SliverLayoutBuilder(
|
),
|
||||||
builder: (context, constraints) {
|
SliverLayoutBuilder(
|
||||||
if (constraints.crossAxisExtent > 600) {
|
builder: (context, constraints) {
|
||||||
return buildAlbumSelectionGrid();
|
if (constraints.crossAxisExtent > 600) {
|
||||||
} else {
|
return _AlbumSelectionGrid(
|
||||||
return buildAlbumSelectionList();
|
filteredAlbums: filteredAlbums,
|
||||||
}
|
searchQuery: _searchQuery,
|
||||||
},
|
);
|
||||||
),
|
} else {
|
||||||
],
|
return _AlbumSelectionList(
|
||||||
|
filteredAlbums: filteredAlbums,
|
||||||
|
searchQuery: _searchQuery,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AlbumSelectionList extends StatelessWidget {
|
||||||
|
final List<LocalAlbum> filteredAlbums;
|
||||||
|
final String searchQuery;
|
||||||
|
|
||||||
|
const _AlbumSelectionList({
|
||||||
|
required this.filteredAlbums,
|
||||||
|
required this.searchQuery,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (filteredAlbums.isEmpty && searchQuery.isNotEmpty) {
|
||||||
|
return SliverToBoxAdapter(
|
||||||
|
child: Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(24.0),
|
||||||
|
child: Text('album_search_not_found'.t(context: context)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filteredAlbums.isEmpty) {
|
||||||
|
return const SliverToBoxAdapter(
|
||||||
|
child: Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SliverPadding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12.0),
|
||||||
|
sliver: SliverList(
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
((context, index) {
|
||||||
|
return DriftAlbumInfoListTile(
|
||||||
|
album: filteredAlbums[index],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
childCount: filteredAlbums.length,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _AlbumSelectionGrid extends StatelessWidget {
|
||||||
|
final List<LocalAlbum> filteredAlbums;
|
||||||
|
final String searchQuery;
|
||||||
|
|
||||||
|
const _AlbumSelectionGrid({
|
||||||
|
required this.filteredAlbums,
|
||||||
|
required this.searchQuery,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (filteredAlbums.isEmpty && searchQuery.isNotEmpty) {
|
||||||
|
return SliverToBoxAdapter(
|
||||||
|
child: Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(24.0),
|
||||||
|
child: Text('album_search_not_found'.t(context: context)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filteredAlbums.isEmpty) {
|
||||||
|
return const SliverToBoxAdapter(
|
||||||
|
child: Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SliverPadding(
|
||||||
|
padding: const EdgeInsets.all(12.0),
|
||||||
|
sliver: SliverGrid.builder(
|
||||||
|
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||||
|
maxCrossAxisExtent: 300,
|
||||||
|
mainAxisSpacing: 12,
|
||||||
|
crossAxisSpacing: 12,
|
||||||
|
),
|
||||||
|
itemCount: filteredAlbums.length,
|
||||||
|
itemBuilder: ((context, index) {
|
||||||
|
return DriftAlbumInfoListTile(
|
||||||
|
album: filteredAlbums[index],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SelectedAlbumNameChips extends ConsumerWidget {
|
||||||
|
final List<LocalAlbum> selectedBackupAlbums;
|
||||||
|
|
||||||
|
const _SelectedAlbumNameChips({
|
||||||
|
required this.selectedBackupAlbums,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return Wrap(
|
||||||
|
children: selectedBackupAlbums.asMap().entries.map((entry) {
|
||||||
|
final album = entry.value;
|
||||||
|
|
||||||
|
void removeSelection() {
|
||||||
|
ref.read(backupAlbumProvider.notifier).deselectAlbum(album);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 8.0),
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: removeSelection,
|
||||||
|
child: AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
child: Chip(
|
||||||
|
label: Text(
|
||||||
|
album.name,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: context.isDarkTheme ? Colors.black : Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
backgroundColor: context.primaryColor,
|
||||||
|
deleteIconColor:
|
||||||
|
context.isDarkTheme ? Colors.black : Colors.white,
|
||||||
|
deleteIcon: const Icon(
|
||||||
|
Icons.cancel_rounded,
|
||||||
|
size: 15,
|
||||||
|
),
|
||||||
|
onDeleted: removeSelection,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ExcludedAlbumNameChips extends ConsumerWidget {
|
||||||
|
final List<LocalAlbum> excludedBackupAlbums;
|
||||||
|
|
||||||
|
const _ExcludedAlbumNameChips({
|
||||||
|
required this.excludedBackupAlbums,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return Wrap(
|
||||||
|
children: excludedBackupAlbums.asMap().entries.map((entry) {
|
||||||
|
final album = entry.value;
|
||||||
|
|
||||||
|
void removeSelection() {
|
||||||
|
ref.read(backupAlbumProvider.notifier).deselectAlbum(album);
|
||||||
|
}
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: removeSelection,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 8.0),
|
||||||
|
child: AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
child: Chip(
|
||||||
|
label: Text(
|
||||||
|
album.name,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: context.scaffoldBackgroundColor,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
backgroundColor: Colors.red[300],
|
||||||
|
deleteIconColor: context.scaffoldBackgroundColor,
|
||||||
|
deleteIcon: const Icon(
|
||||||
|
Icons.cancel_rounded,
|
||||||
|
size: 15,
|
||||||
|
),
|
||||||
|
onDeleted: removeSelection,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SelectAllButton extends ConsumerWidget {
|
||||||
|
final List<LocalAlbum> filteredAlbums;
|
||||||
|
final List<LocalAlbum> selectedBackupAlbums;
|
||||||
|
|
||||||
|
const _SelectAllButton({
|
||||||
|
required this.filteredAlbums,
|
||||||
|
required this.selectedBackupAlbums,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final canSelectAll = filteredAlbums
|
||||||
|
.where((album) => album.backupSelection != BackupSelection.selected)
|
||||||
|
.isNotEmpty;
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: canSelectAll
|
||||||
|
? () {
|
||||||
|
for (final album in filteredAlbums) {
|
||||||
|
if (album.backupSelection != BackupSelection.selected) {
|
||||||
|
ref
|
||||||
|
.read(backupAlbumProvider.notifier)
|
||||||
|
.selectAlbum(album);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
icon: const Icon(Icons.select_all),
|
||||||
|
label: AnimatedSwitcher(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
child: Text(
|
||||||
|
"select_all".t(context: context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12.0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8.0),
|
||||||
|
Expanded(
|
||||||
|
child: OutlinedButton.icon(
|
||||||
|
onPressed: selectedBackupAlbums.isNotEmpty
|
||||||
|
? () {
|
||||||
|
for (final album in filteredAlbums) {
|
||||||
|
if (album.backupSelection == BackupSelection.selected) {
|
||||||
|
ref
|
||||||
|
.read(backupAlbumProvider.notifier)
|
||||||
|
.deselectAlbum(album);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
icon: const Icon(Icons.deselect),
|
||||||
|
label: Text('deselect_all'.t(context: context)),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12.0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
428
mobile/lib/pages/backup/drift_upload_detail.page.dart
Normal file
428
mobile/lib/pages/backup/drift_upload_detail.page.dart
Normal file
@ -0,0 +1,428 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
|
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
|
||||||
|
import 'package:immich_mobile/utils/bytes_units.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
|
@RoutePage()
|
||||||
|
class DriftUploadDetailPage extends ConsumerWidget {
|
||||||
|
const DriftUploadDetailPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final uploadItems = ref.watch(
|
||||||
|
driftBackupProvider.select((state) => state.uploadItems),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text("upload_details".t(context: context)),
|
||||||
|
backgroundColor: context.colorScheme.surface,
|
||||||
|
elevation: 0,
|
||||||
|
scrolledUnderElevation: 1,
|
||||||
|
),
|
||||||
|
body: uploadItems.isEmpty
|
||||||
|
? _buildEmptyState(context)
|
||||||
|
: _buildUploadList(uploadItems),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildEmptyState(BuildContext context) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.cloud_upload_outlined,
|
||||||
|
size: 80,
|
||||||
|
color: context.colorScheme.onSurface.withValues(alpha: 0.3),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
"no_uploads_in_progress".t(context: context),
|
||||||
|
style: context.textTheme.titleMedium?.copyWith(
|
||||||
|
color: context.colorScheme.onSurface.withValues(alpha: 0.6),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildUploadList(
|
||||||
|
Map<String, DriftUploadStatus> uploadItems,
|
||||||
|
) {
|
||||||
|
return ListView.separated(
|
||||||
|
addAutomaticKeepAlives: true,
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
itemCount: uploadItems.length,
|
||||||
|
separatorBuilder: (context, index) => const SizedBox(height: 4),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final item = uploadItems.values.elementAt(index);
|
||||||
|
return _buildUploadCard(context, item);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildUploadCard(
|
||||||
|
BuildContext context,
|
||||||
|
DriftUploadStatus item,
|
||||||
|
) {
|
||||||
|
final isCompleted = item.progress >= 1.0;
|
||||||
|
final double progressPercentage = (item.progress * 100).clamp(0, 100);
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
elevation: 0,
|
||||||
|
color: context.colorScheme.surfaceContainer,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(16),
|
||||||
|
),
|
||||||
|
side: BorderSide(
|
||||||
|
color: context.colorScheme.outline.withValues(alpha: 0.1),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () => _showFileDetailDialog(context, item),
|
||||||
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(16),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
path.basename(item.filename),
|
||||||
|
style: context.textTheme.titleSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'Tap for more details',
|
||||||
|
style: context.textTheme.bodySmall?.copyWith(
|
||||||
|
color: context.colorScheme.onSurface
|
||||||
|
.withValues(alpha: 0.6),
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_buildProgressIndicator(
|
||||||
|
context,
|
||||||
|
item.progress,
|
||||||
|
progressPercentage,
|
||||||
|
isCompleted,
|
||||||
|
item.networkSpeedAsString,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildProgressIndicator(
|
||||||
|
BuildContext context,
|
||||||
|
double progress,
|
||||||
|
double percentage,
|
||||||
|
bool isCompleted,
|
||||||
|
String networkSpeedAsString,
|
||||||
|
) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Stack(
|
||||||
|
alignment: AlignmentDirectional.center,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
child: TweenAnimationBuilder(
|
||||||
|
tween: Tween<double>(begin: 0.0, end: progress),
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
builder: (context, value, _) => CircularProgressIndicator(
|
||||||
|
backgroundColor:
|
||||||
|
context.colorScheme.outline.withValues(alpha: 0.2),
|
||||||
|
strokeWidth: 3,
|
||||||
|
value: value,
|
||||||
|
color: isCompleted
|
||||||
|
? context.colorScheme.primary
|
||||||
|
: context.colorScheme.secondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (isCompleted)
|
||||||
|
Icon(
|
||||||
|
Icons.check_circle_rounded,
|
||||||
|
size: 28,
|
||||||
|
color: context.colorScheme.primary,
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Text(
|
||||||
|
percentage.toStringAsFixed(0),
|
||||||
|
style: context.textTheme.labelSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 10,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
networkSpeedAsString,
|
||||||
|
style: context.textTheme.labelSmall?.copyWith(
|
||||||
|
color: context.colorScheme.onSurface.withValues(alpha: 0.6),
|
||||||
|
fontSize: 10,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _showFileDetailDialog(
|
||||||
|
BuildContext context,
|
||||||
|
DriftUploadStatus item,
|
||||||
|
) async {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => FileDetailDialog(uploadStatus: item),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FileDetailDialog extends ConsumerWidget {
|
||||||
|
final DriftUploadStatus uploadStatus;
|
||||||
|
|
||||||
|
const FileDetailDialog({
|
||||||
|
super.key,
|
||||||
|
required this.uploadStatus,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return AlertDialog(
|
||||||
|
insetPadding: const EdgeInsets.all(20),
|
||||||
|
backgroundColor: context.colorScheme.surfaceContainerLow,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(16),
|
||||||
|
),
|
||||||
|
side: BorderSide(
|
||||||
|
color: context.colorScheme.outline.withValues(alpha: 0.2),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.info_outline,
|
||||||
|
color: context.primaryColor,
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
"details".t(context: context),
|
||||||
|
style: context.textTheme.titleMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: context.primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
content: SizedBox(
|
||||||
|
width: double.maxFinite,
|
||||||
|
child: FutureBuilder<LocalAsset?>(
|
||||||
|
future: _getAssetDetails(ref, uploadStatus.taskId),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
|
return const SizedBox(
|
||||||
|
height: 200,
|
||||||
|
child: Center(child: CircularProgressIndicator()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final asset = snapshot.data;
|
||||||
|
return SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
// Thumbnail at the top center
|
||||||
|
Center(
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||||
|
child: Container(
|
||||||
|
width: 128,
|
||||||
|
height: 128,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
color: context.colorScheme.outline
|
||||||
|
.withValues(alpha: 0.2),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
borderRadius:
|
||||||
|
const BorderRadius.all(Radius.circular(12)),
|
||||||
|
),
|
||||||
|
child: asset != null
|
||||||
|
? Thumbnail(
|
||||||
|
asset: asset,
|
||||||
|
size: const Size(512, 512),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
if (asset != null) ...[
|
||||||
|
_buildInfoSection(context, [
|
||||||
|
_buildInfoRow(
|
||||||
|
context,
|
||||||
|
"Filename",
|
||||||
|
path.basename(uploadStatus.filename),
|
||||||
|
),
|
||||||
|
_buildInfoRow(
|
||||||
|
context,
|
||||||
|
"Local ID",
|
||||||
|
asset.id,
|
||||||
|
),
|
||||||
|
_buildInfoRow(
|
||||||
|
context,
|
||||||
|
"File Size",
|
||||||
|
formatHumanReadableBytes(uploadStatus.fileSize, 2),
|
||||||
|
),
|
||||||
|
if (asset.width != null)
|
||||||
|
_buildInfoRow(context, "Width", "${asset.width}px"),
|
||||||
|
if (asset.height != null)
|
||||||
|
_buildInfoRow(
|
||||||
|
context,
|
||||||
|
"Height",
|
||||||
|
"${asset.height}px",
|
||||||
|
),
|
||||||
|
_buildInfoRow(
|
||||||
|
context,
|
||||||
|
"Created At",
|
||||||
|
asset.createdAt.toString(),
|
||||||
|
),
|
||||||
|
_buildInfoRow(
|
||||||
|
context,
|
||||||
|
"Updated At",
|
||||||
|
asset.updatedAt.toString(),
|
||||||
|
),
|
||||||
|
if (asset.checksum != null)
|
||||||
|
_buildInfoRow(
|
||||||
|
context,
|
||||||
|
"Checksum",
|
||||||
|
asset.checksum!,
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: Text(
|
||||||
|
"close".t(),
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: context.primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildInfoSection(
|
||||||
|
BuildContext context,
|
||||||
|
List<Widget> children,
|
||||||
|
) {
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: context.colorScheme.surfaceContainer,
|
||||||
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(12),
|
||||||
|
),
|
||||||
|
border: Border.all(
|
||||||
|
color: context.colorScheme.outline.withValues(alpha: 0.1),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
...children,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildInfoRow(BuildContext context, String label, String value) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 4),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 100,
|
||||||
|
child: Text(
|
||||||
|
"$label:",
|
||||||
|
style: context.textTheme.labelMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: context.colorScheme.onSurface.withValues(alpha: 0.7),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
value,
|
||||||
|
style: context.textTheme.labelMedium?.copyWith(),
|
||||||
|
maxLines: 3,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<LocalAsset?> _getAssetDetails(
|
||||||
|
WidgetRef ref,
|
||||||
|
String localAssetId,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
final repository = ref.read(localAssetRepository);
|
||||||
|
return await repository.getById(localAssetId);
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,7 +3,9 @@ import 'package:easy_localization/easy_localization.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/scroll_notifier.provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/scroll_notifier.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
||||||
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/search/search_input_focus.provider.dart';
|
import 'package:immich_mobile/providers/search/search_input_focus.provider.dart';
|
||||||
@ -11,6 +13,7 @@ import 'package:immich_mobile/providers/tab.provider.dart';
|
|||||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
import 'package:immich_mobile/providers/websocket.provider.dart';
|
import 'package:immich_mobile/providers/websocket.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/utils/migration.dart';
|
import 'package:immich_mobile/utils/migration.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
@ -25,9 +28,19 @@ class _TabShellPageState extends ConsumerState<TabShellPage> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
ref.read(websocketProvider.notifier).connect();
|
ref.read(websocketProvider.notifier).connect();
|
||||||
runNewSync(ref, full: true);
|
|
||||||
|
final isEnableBackup = ref
|
||||||
|
.read(appSettingsServiceProvider)
|
||||||
|
.getSetting(AppSettingsEnum.enableBackup);
|
||||||
|
|
||||||
|
await runNewSync(ref, full: true).then((_) async {
|
||||||
|
if (isEnableBackup) {
|
||||||
|
await ref.read(driftBackupProvider.notifier).handleBackupResume();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,242 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
|
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
||||||
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
|
|
||||||
|
class BackupToggleButton extends ConsumerStatefulWidget {
|
||||||
|
final VoidCallback onStart;
|
||||||
|
final VoidCallback onStop;
|
||||||
|
|
||||||
|
const BackupToggleButton({
|
||||||
|
super.key,
|
||||||
|
required this.onStart,
|
||||||
|
required this.onStop,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<BackupToggleButton> createState() => BackupToggleButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class BackupToggleButtonState extends ConsumerState<BackupToggleButton>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late AnimationController _animationController;
|
||||||
|
late Animation<double> _gradientAnimation;
|
||||||
|
bool _isEnabled = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_animationController = AnimationController(
|
||||||
|
duration: const Duration(seconds: 8),
|
||||||
|
vsync: this,
|
||||||
|
);
|
||||||
|
|
||||||
|
_gradientAnimation = Tween<double>(begin: 0, end: 1).animate(
|
||||||
|
CurvedAnimation(
|
||||||
|
parent: _animationController,
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
_isEnabled = ref
|
||||||
|
.read(appSettingsServiceProvider)
|
||||||
|
.getSetting(AppSettingsEnum.enableBackup);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_animationController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onToggle(bool value) async {
|
||||||
|
await ref
|
||||||
|
.read(appSettingsServiceProvider)
|
||||||
|
.setSetting(AppSettingsEnum.enableBackup, value);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isEnabled = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
widget.onStart.call();
|
||||||
|
} else {
|
||||||
|
widget.onStop.call();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final enqueueCount = ref.watch(
|
||||||
|
driftBackupProvider.select((state) => state.enqueueCount),
|
||||||
|
);
|
||||||
|
|
||||||
|
final enqueueTotalCount = ref.watch(
|
||||||
|
driftBackupProvider.select((state) => state.enqueueTotalCount),
|
||||||
|
);
|
||||||
|
|
||||||
|
final isCanceling = ref.watch(
|
||||||
|
driftBackupProvider.select((state) => state.isCanceling),
|
||||||
|
);
|
||||||
|
|
||||||
|
final uploadTasks = ref.watch(
|
||||||
|
driftBackupProvider.select((state) => state.uploadItems),
|
||||||
|
);
|
||||||
|
|
||||||
|
final isUploading = uploadTasks.isNotEmpty;
|
||||||
|
|
||||||
|
return AnimatedBuilder(
|
||||||
|
animation: _animationController,
|
||||||
|
builder: (context, child) {
|
||||||
|
final gradientColors = [
|
||||||
|
Color.lerp(
|
||||||
|
context.primaryColor.withValues(alpha: 0.5),
|
||||||
|
context.primaryColor.withValues(alpha: 0.3),
|
||||||
|
_gradientAnimation.value,
|
||||||
|
)!,
|
||||||
|
Color.lerp(
|
||||||
|
context.primaryColor.withValues(alpha: 0.2),
|
||||||
|
context.primaryColor.withValues(alpha: 0.4),
|
||||||
|
_gradientAnimation.value,
|
||||||
|
)!,
|
||||||
|
Color.lerp(
|
||||||
|
context.primaryColor.withValues(alpha: 0.3),
|
||||||
|
context.primaryColor.withValues(alpha: 0.5),
|
||||||
|
_gradientAnimation.value,
|
||||||
|
)!,
|
||||||
|
];
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(20)),
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: gradientColors,
|
||||||
|
stops: const [0.0, 0.5, 1.0],
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: context.primaryColor.withValues(alpha: 0.1),
|
||||||
|
blurRadius: 12,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.all(1.5),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(18.5)),
|
||||||
|
color: context.colorScheme.surfaceContainerLow,
|
||||||
|
),
|
||||||
|
child: Material(
|
||||||
|
color: context.colorScheme.surfaceContainerLow,
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(20.5)),
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(20.5)),
|
||||||
|
onTap: () => isCanceling ? null : _onToggle(!_isEnabled),
|
||||||
|
child: Padding(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [
|
||||||
|
context.primaryColor.withValues(alpha: 0.2),
|
||||||
|
context.primaryColor.withValues(alpha: 0.1),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: isUploading
|
||||||
|
? const SizedBox(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Icon(
|
||||||
|
Icons.cloud_upload_outlined,
|
||||||
|
color: context.primaryColor,
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"enable_backup".t(context: context),
|
||||||
|
style:
|
||||||
|
context.textTheme.titleMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: context.primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (enqueueCount != enqueueTotalCount)
|
||||||
|
Text(
|
||||||
|
"queue_status".t(
|
||||||
|
context: context,
|
||||||
|
args: {
|
||||||
|
'count': enqueueCount.toString(),
|
||||||
|
'total': enqueueTotalCount.toString(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
style: context.textTheme.labelLarge?.copyWith(
|
||||||
|
color: context.colorScheme.onSurfaceSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (isCanceling)
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"canceling".t(),
|
||||||
|
style: context.textTheme.labelLarge,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
SizedBox(
|
||||||
|
width: 18,
|
||||||
|
height: 18,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
backgroundColor: context
|
||||||
|
.colorScheme.onSurface
|
||||||
|
.withValues(alpha: 0.2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Switch.adaptive(
|
||||||
|
value: _isEnabled,
|
||||||
|
onChanged: (value) =>
|
||||||
|
isCanceling ? null : _onToggle(value),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -6,10 +6,12 @@ import 'package:immich_mobile/domain/services/log.service.dart';
|
|||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/models/backup/backup_state.model.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/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/asset.provider.dart';
|
||||||
import 'package:immich_mobile/providers/auth.provider.dart';
|
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||||
import 'package:immich_mobile/providers/background_sync.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/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/ios_background_settings.provider.dart';
|
||||||
import 'package:immich_mobile/providers/backup/manual_upload.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/gallery_permission.provider.dart';
|
||||||
@ -18,6 +20,7 @@ import 'package:immich_mobile/providers/notification_permission.provider.dart';
|
|||||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||||
import 'package:immich_mobile/providers/tab.provider.dart';
|
import 'package:immich_mobile/providers/tab.provider.dart';
|
||||||
import 'package:immich_mobile/providers/websocket.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:immich_mobile/services/background.service.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
@ -69,25 +72,18 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await _ref.read(serverInfoProvider.notifier).getServerVersion();
|
await _ref.read(serverInfoProvider.notifier).getServerVersion();
|
||||||
|
|
||||||
// TODO: Need to decide on how we want to handle uploads once the app is resumed
|
|
||||||
// await FileDownloader().start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Store.isBetaTimelineEnabled) {
|
if (!Store.isBetaTimelineEnabled) {
|
||||||
switch (_ref.read(tabProvider)) {
|
switch (_ref.read(tabProvider)) {
|
||||||
case TabEnum.home:
|
case TabEnum.home:
|
||||||
await _ref.read(assetProvider.notifier).getAllAsset();
|
await _ref.read(assetProvider.notifier).getAllAsset();
|
||||||
break;
|
|
||||||
case TabEnum.search:
|
|
||||||
// nothing to do
|
|
||||||
break;
|
|
||||||
|
|
||||||
case TabEnum.albums:
|
case TabEnum.albums:
|
||||||
await _ref.read(albumProvider.notifier).refreshRemoteAlbums();
|
await _ref.read(albumProvider.notifier).refreshRemoteAlbums();
|
||||||
break;
|
|
||||||
case TabEnum.library:
|
case TabEnum.library:
|
||||||
// nothing to do
|
case TabEnum.search:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -108,7 +104,15 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
backgroundManager.syncRemote(),
|
backgroundManager.syncRemote(),
|
||||||
]);
|
]).then((_) async {
|
||||||
|
final isEnableBackup = _ref
|
||||||
|
.read(appSettingsServiceProvider)
|
||||||
|
.getSetting(AppSettingsEnum.enableBackup);
|
||||||
|
|
||||||
|
if (isEnableBackup) {
|
||||||
|
await _ref.read(driftBackupProvider.notifier).handleBackupResume();
|
||||||
|
}
|
||||||
|
});
|
||||||
} catch (e, stackTrace) {
|
} catch (e, stackTrace) {
|
||||||
Logger("AppLifeCycleNotifier").severe(
|
Logger("AppLifeCycleNotifier").severe(
|
||||||
"Error during background sync",
|
"Error during background sync",
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||||
import 'package:immich_mobile/domain/services/local_album.service.dart';
|
import 'package:immich_mobile/domain/services/local_album.service.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||||
|
|
||||||
final backupAlbumProvider =
|
final backupAlbumProvider =
|
||||||
@ -18,7 +19,8 @@ class BackupAlbumNotifier extends StateNotifier<List<LocalAlbum>> {
|
|||||||
final LocalAlbumService _localAlbumService;
|
final LocalAlbumService _localAlbumService;
|
||||||
|
|
||||||
Future<void> getAll() async {
|
Future<void> getAll() async {
|
||||||
state = await _localAlbumService.getAll();
|
state =
|
||||||
|
await _localAlbumService.getAll(sortBy: {SortLocalAlbumsBy.assetCount});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> selectAlbum(LocalAlbum album) async {
|
Future<void> selectAlbum(LocalAlbum album) async {
|
||||||
|
@ -1,39 +1,75 @@
|
|||||||
|
// ignore_for_file: public_member_api_docs, sort_constructors_first
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:background_downloader/background_downloader.dart';
|
import 'package:background_downloader/background_downloader.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
import 'package:immich_mobile/constants/constants.dart';
|
import 'package:immich_mobile/constants/constants.dart';
|
||||||
import 'package:immich_mobile/services/drift_backup.service.dart';
|
import 'package:immich_mobile/services/drift_backup.service.dart';
|
||||||
import 'package:immich_mobile/services/upload.service.dart';
|
import 'package:immich_mobile/services/upload.service.dart';
|
||||||
|
|
||||||
|
class EnqueueStatus {
|
||||||
|
final int enqueueCount;
|
||||||
|
final int totalCount;
|
||||||
|
|
||||||
|
const EnqueueStatus({
|
||||||
|
required this.enqueueCount,
|
||||||
|
required this.totalCount,
|
||||||
|
});
|
||||||
|
|
||||||
|
EnqueueStatus copyWith({
|
||||||
|
int? enqueueCount,
|
||||||
|
int? totalCount,
|
||||||
|
}) {
|
||||||
|
return EnqueueStatus(
|
||||||
|
enqueueCount: enqueueCount ?? this.enqueueCount,
|
||||||
|
totalCount: totalCount ?? this.totalCount,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() =>
|
||||||
|
'EnqueueStatus(enqueueCount: $enqueueCount, totalCount: $totalCount)';
|
||||||
|
}
|
||||||
|
|
||||||
class DriftUploadStatus {
|
class DriftUploadStatus {
|
||||||
final String taskId;
|
final String taskId;
|
||||||
final String filename;
|
final String filename;
|
||||||
final double progress;
|
final double progress;
|
||||||
|
final int fileSize;
|
||||||
|
final String networkSpeedAsString;
|
||||||
|
|
||||||
const DriftUploadStatus({
|
const DriftUploadStatus({
|
||||||
required this.taskId,
|
required this.taskId,
|
||||||
required this.filename,
|
required this.filename,
|
||||||
required this.progress,
|
required this.progress,
|
||||||
|
required this.fileSize,
|
||||||
|
required this.networkSpeedAsString,
|
||||||
});
|
});
|
||||||
|
|
||||||
DriftUploadStatus copyWith({
|
DriftUploadStatus copyWith({
|
||||||
String? taskId,
|
String? taskId,
|
||||||
String? filename,
|
String? filename,
|
||||||
double? progress,
|
double? progress,
|
||||||
|
int? fileSize,
|
||||||
|
String? networkSpeedAsString,
|
||||||
}) {
|
}) {
|
||||||
return DriftUploadStatus(
|
return DriftUploadStatus(
|
||||||
taskId: taskId ?? this.taskId,
|
taskId: taskId ?? this.taskId,
|
||||||
filename: filename ?? this.filename,
|
filename: filename ?? this.filename,
|
||||||
progress: progress ?? this.progress,
|
progress: progress ?? this.progress,
|
||||||
|
fileSize: fileSize ?? this.fileSize,
|
||||||
|
networkSpeedAsString: networkSpeedAsString ?? this.networkSpeedAsString,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() =>
|
String toString() {
|
||||||
'ExpUploadStatus(taskId: $taskId, filename: $filename, progress: $progress)';
|
return 'DriftUploadStatus(taskId: $taskId, filename: $filename, progress: $progress, fileSize: $fileSize, networkSpeedAsString: $networkSpeedAsString)';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(covariant DriftUploadStatus other) {
|
bool operator ==(covariant DriftUploadStatus other) {
|
||||||
@ -41,23 +77,65 @@ class DriftUploadStatus {
|
|||||||
|
|
||||||
return other.taskId == taskId &&
|
return other.taskId == taskId &&
|
||||||
other.filename == filename &&
|
other.filename == filename &&
|
||||||
other.progress == progress;
|
other.progress == progress &&
|
||||||
|
other.fileSize == fileSize &&
|
||||||
|
other.networkSpeedAsString == networkSpeedAsString;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => taskId.hashCode ^ filename.hashCode ^ progress.hashCode;
|
int get hashCode {
|
||||||
|
return taskId.hashCode ^
|
||||||
|
filename.hashCode ^
|
||||||
|
progress.hashCode ^
|
||||||
|
fileSize.hashCode ^
|
||||||
|
networkSpeedAsString.hashCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() {
|
||||||
|
return <String, dynamic>{
|
||||||
|
'taskId': taskId,
|
||||||
|
'filename': filename,
|
||||||
|
'progress': progress,
|
||||||
|
'fileSize': fileSize,
|
||||||
|
'networkSpeedAsString': networkSpeedAsString,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
factory DriftUploadStatus.fromMap(Map<String, dynamic> map) {
|
||||||
|
return DriftUploadStatus(
|
||||||
|
taskId: map['taskId'] as String,
|
||||||
|
filename: map['filename'] as String,
|
||||||
|
progress: map['progress'] as double,
|
||||||
|
fileSize: map['fileSize'] as int,
|
||||||
|
networkSpeedAsString: map['networkSpeedAsString'] as String,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String toJson() => json.encode(toMap());
|
||||||
|
|
||||||
|
factory DriftUploadStatus.fromJson(String source) =>
|
||||||
|
DriftUploadStatus.fromMap(json.decode(source) as Map<String, dynamic>);
|
||||||
}
|
}
|
||||||
|
|
||||||
class DriftBackupState {
|
class DriftBackupState {
|
||||||
final int totalCount;
|
final int totalCount;
|
||||||
final int backupCount;
|
final int backupCount;
|
||||||
final int remainderCount;
|
final int remainderCount;
|
||||||
|
|
||||||
|
final int enqueueCount;
|
||||||
|
final int enqueueTotalCount;
|
||||||
|
|
||||||
|
final bool isCanceling;
|
||||||
|
|
||||||
final Map<String, DriftUploadStatus> uploadItems;
|
final Map<String, DriftUploadStatus> uploadItems;
|
||||||
|
|
||||||
const DriftBackupState({
|
const DriftBackupState({
|
||||||
required this.totalCount,
|
required this.totalCount,
|
||||||
required this.backupCount,
|
required this.backupCount,
|
||||||
required this.remainderCount,
|
required this.remainderCount,
|
||||||
|
required this.enqueueCount,
|
||||||
|
required this.enqueueTotalCount,
|
||||||
|
required this.isCanceling,
|
||||||
required this.uploadItems,
|
required this.uploadItems,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -65,19 +143,25 @@ class DriftBackupState {
|
|||||||
int? totalCount,
|
int? totalCount,
|
||||||
int? backupCount,
|
int? backupCount,
|
||||||
int? remainderCount,
|
int? remainderCount,
|
||||||
|
int? enqueueCount,
|
||||||
|
int? enqueueTotalCount,
|
||||||
|
bool? isCanceling,
|
||||||
Map<String, DriftUploadStatus>? uploadItems,
|
Map<String, DriftUploadStatus>? uploadItems,
|
||||||
}) {
|
}) {
|
||||||
return DriftBackupState(
|
return DriftBackupState(
|
||||||
totalCount: totalCount ?? this.totalCount,
|
totalCount: totalCount ?? this.totalCount,
|
||||||
backupCount: backupCount ?? this.backupCount,
|
backupCount: backupCount ?? this.backupCount,
|
||||||
remainderCount: remainderCount ?? this.remainderCount,
|
remainderCount: remainderCount ?? this.remainderCount,
|
||||||
|
enqueueCount: enqueueCount ?? this.enqueueCount,
|
||||||
|
enqueueTotalCount: enqueueTotalCount ?? this.enqueueTotalCount,
|
||||||
|
isCanceling: isCanceling ?? this.isCanceling,
|
||||||
uploadItems: uploadItems ?? this.uploadItems,
|
uploadItems: uploadItems ?? this.uploadItems,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'ExpBackupState(totalCount: $totalCount, backupCount: $backupCount, remainderCount: $remainderCount, uploadItems: $uploadItems)';
|
return 'DriftBackupState(totalCount: $totalCount, backupCount: $backupCount, remainderCount: $remainderCount, enqueueCount: $enqueueCount, enqueueTotalCount: $enqueueTotalCount, isCanceling: $isCanceling, uploadItems: $uploadItems)';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -88,6 +172,9 @@ class DriftBackupState {
|
|||||||
return other.totalCount == totalCount &&
|
return other.totalCount == totalCount &&
|
||||||
other.backupCount == backupCount &&
|
other.backupCount == backupCount &&
|
||||||
other.remainderCount == remainderCount &&
|
other.remainderCount == remainderCount &&
|
||||||
|
other.enqueueCount == enqueueCount &&
|
||||||
|
other.enqueueTotalCount == enqueueTotalCount &&
|
||||||
|
other.isCanceling == isCanceling &&
|
||||||
mapEquals(other.uploadItems, uploadItems);
|
mapEquals(other.uploadItems, uploadItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,6 +183,9 @@ class DriftBackupState {
|
|||||||
return totalCount.hashCode ^
|
return totalCount.hashCode ^
|
||||||
backupCount.hashCode ^
|
backupCount.hashCode ^
|
||||||
remainderCount.hashCode ^
|
remainderCount.hashCode ^
|
||||||
|
enqueueCount.hashCode ^
|
||||||
|
enqueueTotalCount.hashCode ^
|
||||||
|
isCanceling.hashCode ^
|
||||||
uploadItems.hashCode;
|
uploadItems.hashCode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -117,6 +207,9 @@ class ExpBackupNotifier extends StateNotifier<DriftBackupState> {
|
|||||||
totalCount: 0,
|
totalCount: 0,
|
||||||
backupCount: 0,
|
backupCount: 0,
|
||||||
remainderCount: 0,
|
remainderCount: 0,
|
||||||
|
enqueueCount: 0,
|
||||||
|
enqueueTotalCount: 0,
|
||||||
|
isCanceling: false,
|
||||||
uploadItems: {},
|
uploadItems: {},
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
@ -131,13 +224,39 @@ class ExpBackupNotifier extends StateNotifier<DriftBackupState> {
|
|||||||
StreamSubscription<TaskStatusUpdate>? _statusSubscription;
|
StreamSubscription<TaskStatusUpdate>? _statusSubscription;
|
||||||
StreamSubscription<TaskProgressUpdate>? _progressSubscription;
|
StreamSubscription<TaskProgressUpdate>? _progressSubscription;
|
||||||
|
|
||||||
|
/// Remove upload item from state
|
||||||
|
void _removeUploadItem(String taskId) {
|
||||||
|
if (state.uploadItems.containsKey(taskId)) {
|
||||||
|
final updatedItems =
|
||||||
|
Map<String, DriftUploadStatus>.from(state.uploadItems);
|
||||||
|
updatedItems.remove(taskId);
|
||||||
|
state = state.copyWith(uploadItems: updatedItems);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _handleTaskStatusUpdate(TaskStatusUpdate update) {
|
void _handleTaskStatusUpdate(TaskStatusUpdate update) {
|
||||||
switch (update.status) {
|
switch (update.status) {
|
||||||
case TaskStatus.complete:
|
case TaskStatus.complete:
|
||||||
state = state.copyWith(
|
if (update.task.group == kBackupGroup) {
|
||||||
backupCount: state.backupCount + 1,
|
state = state.copyWith(
|
||||||
remainderCount: state.remainderCount - 1,
|
backupCount: state.backupCount + 1,
|
||||||
);
|
remainderCount: state.remainderCount - 1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the completed task from the upload items
|
||||||
|
final taskId = update.task.taskId;
|
||||||
|
if (state.uploadItems.containsKey(taskId)) {
|
||||||
|
Future.delayed(const Duration(milliseconds: 500), () {
|
||||||
|
_removeUploadItem(taskId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
case TaskStatus.failed:
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TaskStatus.canceled:
|
||||||
|
_removeUploadItem(update.task.taskId);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@ -145,7 +264,48 @@ class ExpBackupNotifier extends StateNotifier<DriftBackupState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleTaskProgressUpdate(TaskProgressUpdate update) {}
|
void _handleTaskProgressUpdate(TaskProgressUpdate update) {
|
||||||
|
final taskId = update.task.taskId;
|
||||||
|
final filename = update.task.displayName;
|
||||||
|
final progress = update.progress;
|
||||||
|
final currentItem = state.uploadItems[taskId];
|
||||||
|
if (currentItem != null) {
|
||||||
|
if (progress == kUploadStatusCanceled) {
|
||||||
|
_removeUploadItem(update.task.taskId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state = state.copyWith(
|
||||||
|
uploadItems: {
|
||||||
|
...state.uploadItems,
|
||||||
|
taskId: update.hasExpectedFileSize
|
||||||
|
? currentItem.copyWith(
|
||||||
|
progress: progress,
|
||||||
|
fileSize: update.expectedFileSize,
|
||||||
|
networkSpeedAsString: update.networkSpeedAsString,
|
||||||
|
)
|
||||||
|
: currentItem.copyWith(
|
||||||
|
progress: progress,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state = state.copyWith(
|
||||||
|
uploadItems: {
|
||||||
|
...state.uploadItems,
|
||||||
|
taskId: DriftUploadStatus(
|
||||||
|
taskId: taskId,
|
||||||
|
filename: filename,
|
||||||
|
progress: progress,
|
||||||
|
fileSize: update.expectedFileSize,
|
||||||
|
networkSpeedAsString: update.networkSpeedAsString,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> getBackupStatus() async {
|
Future<void> getBackupStatus() async {
|
||||||
final [totalCount, backupCount, remainderCount] = await Future.wait([
|
final [totalCount, backupCount, remainderCount] = await Future.wait([
|
||||||
@ -162,27 +322,50 @@ class ExpBackupNotifier extends StateNotifier<DriftBackupState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> backup() {
|
Future<void> backup() {
|
||||||
return _backupService.backup();
|
return _backupService.backup(_updateEnqueueCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateEnqueueCount(EnqueueStatus status) {
|
||||||
|
state = state.copyWith(
|
||||||
|
enqueueCount: status.enqueueCount,
|
||||||
|
enqueueTotalCount: status.totalCount,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> cancel() async {
|
Future<void> cancel() async {
|
||||||
|
state = state.copyWith(
|
||||||
|
enqueueCount: 0,
|
||||||
|
enqueueTotalCount: 0,
|
||||||
|
isCanceling: true,
|
||||||
|
);
|
||||||
|
|
||||||
await _backupService.cancel();
|
await _backupService.cancel();
|
||||||
await getDataInfo();
|
|
||||||
|
// Check if there are any tasks left in the queue
|
||||||
|
final tasks = await FileDownloader().allTasks(group: kBackupGroup);
|
||||||
|
|
||||||
|
debugPrint("Tasks left to cancel: ${tasks.length}");
|
||||||
|
|
||||||
|
if (tasks.isNotEmpty) {
|
||||||
|
await cancel();
|
||||||
|
} else {
|
||||||
|
// Clear all upload items when cancellation is complete
|
||||||
|
state = state.copyWith(
|
||||||
|
isCanceling: false,
|
||||||
|
uploadItems: {},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> getDataInfo() async {
|
Future<void> handleBackupResume() async {
|
||||||
final a = await FileDownloader().database.allRecordsWithStatus(
|
final tasks = await FileDownloader().allTasks(group: kBackupGroup);
|
||||||
TaskStatus.enqueued,
|
if (tasks.isEmpty) {
|
||||||
group: kBackupGroup,
|
// Start a new backup queue
|
||||||
);
|
await backup();
|
||||||
|
}
|
||||||
|
|
||||||
final b = await FileDownloader().allTasks(
|
debugPrint("Tasks to resume: ${tasks.length}");
|
||||||
group: kBackupGroup,
|
await FileDownloader().start();
|
||||||
);
|
|
||||||
|
|
||||||
debugPrint(
|
|
||||||
"Enqueued tasks: ${a.length}, All tasks: ${b.length}",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -12,6 +12,7 @@ import 'package:immich_mobile/models/server_info/server_version.model.dart';
|
|||||||
import 'package:immich_mobile/providers/asset.provider.dart';
|
import 'package:immich_mobile/providers/asset.provider.dart';
|
||||||
import 'package:immich_mobile/providers/auth.provider.dart';
|
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||||
|
// import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||||
import 'package:immich_mobile/providers/db.provider.dart';
|
import 'package:immich_mobile/providers/db.provider.dart';
|
||||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||||
import 'package:immich_mobile/services/api.service.dart';
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
@ -177,9 +178,15 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!Store.isBetaTimelineEnabled) {
|
if (!Store.isBetaTimelineEnabled) {
|
||||||
startListeningToOldEvents();
|
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 {
|
} else {
|
||||||
startListeningToBetaEvents();
|
socket.on('AssetUploadReadyV1', _handleSyncAssetUploadReady);
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.on('on_config_update', _handleOnConfigUpdate);
|
socket.on('on_config_update', _handleOnConfigUpdate);
|
||||||
@ -207,7 +214,6 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void stopListenToEvent(String eventName) {
|
void stopListenToEvent(String eventName) {
|
||||||
debugPrint("Stop listening to event $eventName");
|
|
||||||
state.socket?.off(eventName);
|
state.socket?.off(eventName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,6 +27,7 @@ import 'package:immich_mobile/pages/backup/drift_backup.page.dart';
|
|||||||
import 'package:immich_mobile/pages/backup/backup_album_selection.page.dart';
|
import 'package:immich_mobile/pages/backup/backup_album_selection.page.dart';
|
||||||
import 'package:immich_mobile/pages/backup/backup_controller.page.dart';
|
import 'package:immich_mobile/pages/backup/backup_controller.page.dart';
|
||||||
import 'package:immich_mobile/pages/backup/backup_options.page.dart';
|
import 'package:immich_mobile/pages/backup/backup_options.page.dart';
|
||||||
|
import 'package:immich_mobile/pages/backup/drift_upload_detail.page.dart';
|
||||||
import 'package:immich_mobile/pages/backup/failed_backup_status.page.dart';
|
import 'package:immich_mobile/pages/backup/failed_backup_status.page.dart';
|
||||||
import 'package:immich_mobile/pages/common/activities.page.dart';
|
import 'package:immich_mobile/pages/common/activities.page.dart';
|
||||||
import 'package:immich_mobile/pages/common/app_log.page.dart';
|
import 'package:immich_mobile/pages/common/app_log.page.dart';
|
||||||
@ -484,6 +485,10 @@ class AppRouter extends RootStackRouter {
|
|||||||
page: ChangeExperienceRoute.page,
|
page: ChangeExperienceRoute.page,
|
||||||
guards: [_authGuard, _duplicateGuard],
|
guards: [_authGuard, _duplicateGuard],
|
||||||
),
|
),
|
||||||
|
AutoRoute(
|
||||||
|
page: DriftUploadDetailRoute.page,
|
||||||
|
guards: [_authGuard, _duplicateGuard],
|
||||||
|
),
|
||||||
// required to handle all deeplinks in deep_link.service.dart
|
// required to handle all deeplinks in deep_link.service.dart
|
||||||
// auto_route_library#1722
|
// auto_route_library#1722
|
||||||
RedirectRoute(path: '*', redirectTo: '/'),
|
RedirectRoute(path: '*', redirectTo: '/'),
|
||||||
|
@ -1078,6 +1078,22 @@ class DriftTrashRoute extends PageRouteInfo<void> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [DriftUploadDetailPage]
|
||||||
|
class DriftUploadDetailRoute extends PageRouteInfo<void> {
|
||||||
|
const DriftUploadDetailRoute({List<PageRouteInfo>? children})
|
||||||
|
: super(DriftUploadDetailRoute.name, initialChildren: children);
|
||||||
|
|
||||||
|
static const String name = 'DriftUploadDetailRoute';
|
||||||
|
|
||||||
|
static PageInfo page = PageInfo(
|
||||||
|
name,
|
||||||
|
builder: (data) {
|
||||||
|
return const DriftUploadDetailPage();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [DriftUserSelectionPage]
|
/// [DriftUserSelectionPage]
|
||||||
class DriftUserSelectionRoute
|
class DriftUserSelectionRoute
|
||||||
|
@ -91,6 +91,7 @@ enum AppSettingsEnum<T> {
|
|||||||
true,
|
true,
|
||||||
),
|
),
|
||||||
betaTimeline<bool>(StoreKey.betaTimeline, null, false),
|
betaTimeline<bool>(StoreKey.betaTimeline, null, false),
|
||||||
|
enableBackup<bool>(StoreKey.enableBackup, null, false),
|
||||||
;
|
;
|
||||||
|
|
||||||
const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue);
|
const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue);
|
||||||
|
@ -9,6 +9,7 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
|||||||
import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
|
||||||
|
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/storage.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/storage.provider.dart';
|
||||||
import 'package:immich_mobile/services/upload.service.dart';
|
import 'package:immich_mobile/services/upload.service.dart';
|
||||||
@ -55,7 +56,9 @@ class DriftBackupService {
|
|||||||
return _backupRepository.getBackupCount();
|
return _backupRepository.getBackupCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> backup() async {
|
Future<void> backup(
|
||||||
|
void Function(EnqueueStatus status) onEnqueueTasks,
|
||||||
|
) async {
|
||||||
shouldCancel = false;
|
shouldCancel = false;
|
||||||
|
|
||||||
final candidates = await _backupRepository.getCandidates();
|
final candidates = await _backupRepository.getCandidates();
|
||||||
@ -83,8 +86,12 @@ class DriftBackupService {
|
|||||||
if (tasks.isNotEmpty && !shouldCancel) {
|
if (tasks.isNotEmpty && !shouldCancel) {
|
||||||
count += tasks.length;
|
count += tasks.length;
|
||||||
_uploadService.enqueueTasks(tasks);
|
_uploadService.enqueueTasks(tasks);
|
||||||
debugPrint(
|
|
||||||
"Enqueued $count/${candidates.length} tasks for backup",
|
onEnqueueTasks(
|
||||||
|
EnqueueStatus(
|
||||||
|
enqueueCount: count,
|
||||||
|
totalCount: candidates.length,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -213,7 +220,7 @@ class DriftBackupService {
|
|||||||
deviceAssetId: asset.id,
|
deviceAssetId: asset.id,
|
||||||
fields: fields,
|
fields: fields,
|
||||||
group: kBackupLivePhotoGroup,
|
group: kBackupLivePhotoGroup,
|
||||||
priority: 0,
|
priority: 0, // Highest priority to get upload immediately
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -134,6 +134,7 @@ class UploadService {
|
|||||||
group: group,
|
group: group,
|
||||||
priority: priority ?? 5,
|
priority: priority ?? 5,
|
||||||
updates: Updates.statusAndProgress,
|
updates: Updates.statusAndProgress,
|
||||||
|
retries: 3,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_svg/svg.dart';
|
import 'package:flutter_svg/svg.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
@ -83,6 +84,11 @@ class ImmichSliverAppBar extends ConsumerWidget {
|
|||||||
child: action,
|
child: action,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (kDebugMode || kProfileMode)
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.science_rounded),
|
||||||
|
onPressed: () => context.pushRoute(const FeatInDevRoute()),
|
||||||
|
),
|
||||||
if (showUploadButton)
|
if (showUploadButton)
|
||||||
const Padding(
|
const Padding(
|
||||||
padding: EdgeInsets.only(right: 20),
|
padding: EdgeInsets.only(right: 20),
|
||||||
|
@ -90,6 +90,19 @@ class _BetaTimelineListTileState extends ConsumerState<BetaTimelineListTile>
|
|||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
context.pop();
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
"cancel".t(context: context),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: context.colorScheme.outline,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
await ref.read(appSettingsServiceProvider).setSetting(
|
await ref.read(appSettingsServiceProvider).setSetting(
|
||||||
@ -101,25 +114,7 @@ class _BetaTimelineListTileState extends ConsumerState<BetaTimelineListTile>
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
"YES",
|
"ok".t(context: context),
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: context.primaryColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
context.pop();
|
|
||||||
},
|
|
||||||
child: Text(
|
|
||||||
"NO",
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: context.colorScheme.outline,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -135,13 +130,13 @@ class _BetaTimelineListTileState extends ConsumerState<BetaTimelineListTile>
|
|||||||
_gradientAnimation.value,
|
_gradientAnimation.value,
|
||||||
)!,
|
)!,
|
||||||
Color.lerp(
|
Color.lerp(
|
||||||
context.primaryColor.withValues(alpha: 0.4),
|
context.logoPink.withValues(alpha: 0.2),
|
||||||
context.primaryColor.withValues(alpha: 0.6),
|
context.logoPink.withValues(alpha: 0.4),
|
||||||
_gradientAnimation.value,
|
_gradientAnimation.value,
|
||||||
)!,
|
)!,
|
||||||
Color.lerp(
|
Color.lerp(
|
||||||
context.primaryColor.withValues(alpha: 0.3),
|
context.logoRed.withValues(alpha: 0.3),
|
||||||
context.primaryColor.withValues(alpha: 0.5),
|
context.logoRed.withValues(alpha: 0.5),
|
||||||
_gradientAnimation.value,
|
_gradientAnimation.value,
|
||||||
)!,
|
)!,
|
||||||
];
|
];
|
||||||
@ -155,7 +150,7 @@ class _BetaTimelineListTileState extends ConsumerState<BetaTimelineListTile>
|
|||||||
stops: const [0.0, 0.5, 1.0],
|
stops: const [0.0, 0.5, 1.0],
|
||||||
begin: Alignment.topLeft,
|
begin: Alignment.topLeft,
|
||||||
end: Alignment.bottomRight,
|
end: Alignment.bottomRight,
|
||||||
transform: GradientRotation(_rotationAnimation.value * 0.1),
|
transform: GradientRotation(_rotationAnimation.value * 0.5),
|
||||||
),
|
),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
@ -166,13 +161,12 @@ class _BetaTimelineListTileState extends ConsumerState<BetaTimelineListTile>
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: Container(
|
child: Container(
|
||||||
margin: const EdgeInsets.all(1.5),
|
margin: const EdgeInsets.all(2),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(10.5)),
|
borderRadius: const BorderRadius.all(Radius.circular(10.5)),
|
||||||
color: context.scaffoldBackgroundColor,
|
color: context.scaffoldBackgroundColor,
|
||||||
),
|
),
|
||||||
child: Material(
|
child: Material(
|
||||||
color: Colors.transparent,
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(10.5)),
|
borderRadius: const BorderRadius.all(Radius.circular(10.5)),
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(10.5)),
|
borderRadius: const BorderRadius.all(Radius.circular(10.5)),
|
||||||
@ -205,7 +199,7 @@ class _BetaTimelineListTileState extends ConsumerState<BetaTimelineListTile>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 28),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@ -259,8 +253,9 @@ class _BetaTimelineListTileState extends ConsumerState<BetaTimelineListTile>
|
|||||||
.t(context: context),
|
.t(context: context),
|
||||||
style: context.textTheme.labelLarge?.copyWith(
|
style: context.textTheme.labelLarge?.copyWith(
|
||||||
color: context.textTheme.labelLarge?.color
|
color: context.textTheme.labelLarge?.color
|
||||||
?.withValues(alpha: 0.7),
|
?.withValues(alpha: 0.9),
|
||||||
),
|
),
|
||||||
|
maxLines: 2,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user