mirror of
https://github.com/immich-app/immich.git
synced 2025-07-31 15:08:44 -04:00
feat: drift manual upload (#20101)
This commit is contained in:
parent
08122d6871
commit
1a35a01149
@ -1941,11 +1941,13 @@
|
||||
"updated_at": "Updated",
|
||||
"updated_password": "Updated password",
|
||||
"upload": "Upload",
|
||||
"upload_action_prompt": "{count} queued for upload",
|
||||
"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_title": "Upload Asset",
|
||||
"upload_errors": "Upload completed with {count, plural, one {# error} other {# errors}}, refresh the page to see new upload assets.",
|
||||
"upload_finished": "Upload finished",
|
||||
"upload_progress": "Remaining {remaining, number} - Processed {processed, number}/{total, number}",
|
||||
"upload_skipped_duplicates": "Skipped {count, plural, one {# duplicate asset} other {# duplicate assets}}",
|
||||
"upload_status_duplicates": "Duplicates",
|
||||
@ -1954,6 +1956,7 @@
|
||||
"upload_success": "Upload success, refresh the page to see new upload assets.",
|
||||
"upload_to_immich": "Upload to Immich ({count})",
|
||||
"uploading": "Uploading",
|
||||
"uploading_media": "Uploading media",
|
||||
"url": "URL",
|
||||
"usage": "Usage",
|
||||
"use_biometric": "Use biometric",
|
||||
|
@ -141,6 +141,11 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
||||
_db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id),
|
||||
useColumns: false,
|
||||
),
|
||||
leftOuterJoin(
|
||||
_db.remoteAssetEntity,
|
||||
_db.localAssetEntity.checksum.equalsExp(_db.remoteAssetEntity.checksum),
|
||||
useColumns: false,
|
||||
),
|
||||
])
|
||||
..addColumns([assetCountExp, dateExp])
|
||||
..where(_db.localAlbumAssetEntity.albumId.equals(albumId))
|
||||
|
@ -203,6 +203,19 @@ class ImmichAppState extends ConsumerState<ImmichApp>
|
||||
),
|
||||
progressBar: true,
|
||||
);
|
||||
|
||||
FileDownloader().configureNotificationForGroup(
|
||||
kManualUploadGroup,
|
||||
running: TaskNotification(
|
||||
'uploading_media'.tr(),
|
||||
'${'file_name'.tr()}: {displayName}',
|
||||
),
|
||||
complete: TaskNotification(
|
||||
'upload_finished'.tr(),
|
||||
'${'file_name'.tr()}: {displayName}',
|
||||
),
|
||||
progressBar: true,
|
||||
);
|
||||
}
|
||||
|
||||
Future<DeepLink> _deepLinkBuilder(PlatformDeepLink deepLink) async {
|
||||
|
@ -1,16 +1,50 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
|
||||
class UploadActionButton extends ConsumerWidget {
|
||||
const UploadActionButton({super.key});
|
||||
final ActionSource source;
|
||||
|
||||
const UploadActionButton({super.key, required this.source});
|
||||
|
||||
void _onTap(BuildContext context, WidgetRef ref) async {
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
final result = await ref.read(actionProvider.notifier).upload(source);
|
||||
|
||||
final successMessage = 'upload_action_prompt'.t(
|
||||
context: context,
|
||||
args: {'count': result.count.toString()},
|
||||
);
|
||||
|
||||
if (context.mounted) {
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: result.success
|
||||
? successMessage
|
||||
: 'scaffold_body_error_occurred'.t(context: context),
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
toastType: result.success ? ToastType.success : ToastType.error,
|
||||
);
|
||||
|
||||
ref.read(multiSelectProvider.notifier).reset();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return BaseActionButton(
|
||||
iconData: Icons.backup_outlined,
|
||||
label: "upload".t(context: context),
|
||||
onPressed: () => _onTap(context, ref),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
@ -37,6 +38,8 @@ class ViewerBottomBar extends ConsumerWidget {
|
||||
|
||||
final actions = <Widget>[
|
||||
const ShareActionButton(source: ActionSource.viewer),
|
||||
if (asset.isLocalOnly)
|
||||
const UploadActionButton(source: ActionSource.viewer),
|
||||
if (asset.hasRemote && isOwner)
|
||||
const ArchiveActionButton(source: ActionSource.viewer),
|
||||
];
|
||||
|
@ -63,7 +63,7 @@ class AssetDetailBottomSheet extends ConsumerWidget {
|
||||
],
|
||||
if (asset.storage == AssetState.local) ...[
|
||||
const DeleteLocalActionButton(source: ActionSource.viewer),
|
||||
const UploadActionButton(),
|
||||
const UploadActionButton(source: ActionSource.timeline),
|
||||
],
|
||||
];
|
||||
|
||||
|
@ -53,7 +53,7 @@ class ArchiveBottomSheet extends ConsumerWidget {
|
||||
],
|
||||
if (multiselect.hasLocal) ...[
|
||||
const DeleteLocalActionButton(source: ActionSource.timeline),
|
||||
const UploadActionButton(),
|
||||
const UploadActionButton(source: ActionSource.timeline),
|
||||
],
|
||||
],
|
||||
);
|
||||
|
@ -53,7 +53,7 @@ class FavoriteBottomSheet extends ConsumerWidget {
|
||||
],
|
||||
if (multiselect.hasLocal) ...[
|
||||
const DeleteLocalActionButton(source: ActionSource.timeline),
|
||||
const UploadActionButton(),
|
||||
const UploadActionButton(source: ActionSource.timeline),
|
||||
],
|
||||
],
|
||||
);
|
||||
|
@ -56,7 +56,7 @@ class GeneralBottomSheet extends ConsumerWidget {
|
||||
],
|
||||
if (multiselect.hasLocal) ...[
|
||||
const DeleteLocalActionButton(source: ActionSource.timeline),
|
||||
const UploadActionButton(),
|
||||
const UploadActionButton(source: ActionSource.timeline),
|
||||
],
|
||||
],
|
||||
);
|
||||
|
@ -18,7 +18,7 @@ class LocalAlbumBottomSheet extends ConsumerWidget {
|
||||
actions: [
|
||||
ShareActionButton(source: ActionSource.timeline),
|
||||
DeleteLocalActionButton(source: ActionSource.timeline),
|
||||
UploadActionButton(),
|
||||
UploadActionButton(source: ActionSource.timeline),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
@ -56,7 +56,7 @@ class RemoteAlbumBottomSheet extends ConsumerWidget {
|
||||
],
|
||||
if (multiselect.hasLocal) ...[
|
||||
const DeleteLocalActionButton(source: ActionSource.timeline),
|
||||
const UploadActionButton(),
|
||||
const UploadActionButton(source: ActionSource.timeline),
|
||||
],
|
||||
RemoveFromAlbumActionButton(
|
||||
source: ActionSource.timeline,
|
||||
|
@ -5,6 +5,7 @@ import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asse
|
||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/services/action.service.dart';
|
||||
import 'package:immich_mobile/services/drift_backup.service.dart';
|
||||
import 'package:immich_mobile/services/timeline.service.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
@ -32,12 +33,14 @@ class ActionResult {
|
||||
class ActionNotifier extends Notifier<void> {
|
||||
final Logger _logger = Logger('ActionNotifier');
|
||||
late ActionService _service;
|
||||
late DriftBackupService _backupService;
|
||||
|
||||
ActionNotifier() : super();
|
||||
|
||||
@override
|
||||
void build() {
|
||||
_service = ref.watch(actionServiceProvider);
|
||||
_backupService = ref.watch(driftBackupServiceProvider);
|
||||
}
|
||||
|
||||
List<String> _getRemoteIdsForSource(ActionSource source) {
|
||||
@ -368,6 +371,21 @@ class ActionNotifier extends Notifier<void> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<ActionResult> upload(ActionSource source) async {
|
||||
final assets = _getAssets(source).whereType<LocalAsset>().toList();
|
||||
try {
|
||||
await _backupService.manualBackup(assets);
|
||||
return ActionResult(count: assets.length, success: true);
|
||||
} catch (error, stack) {
|
||||
_logger.severe('Failed manually upload assets', error, stack);
|
||||
return ActionResult(
|
||||
count: assets.length,
|
||||
success: false,
|
||||
error: error.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension on Iterable<RemoteAsset> {
|
||||
|
@ -20,6 +20,11 @@ class UploadRepository {
|
||||
taskStatusCallback: (update) => onUploadStatus?.call(update),
|
||||
taskProgressCallback: (update) => onTaskProgress?.call(update),
|
||||
);
|
||||
FileDownloader().registerCallbacks(
|
||||
group: kManualUploadGroup,
|
||||
taskStatusCallback: (_) => {},
|
||||
taskProgressCallback: (_) => {},
|
||||
);
|
||||
}
|
||||
|
||||
void enqueueAll(List<UploadTask> tasks) {
|
||||
|
@ -26,6 +26,7 @@ final driftBackupServiceProvider = Provider<DriftBackupService>(
|
||||
),
|
||||
);
|
||||
|
||||
// TODO: Rename to UploadService after removing Isar
|
||||
class DriftBackupService {
|
||||
DriftBackupService(
|
||||
this._backupRepository,
|
||||
@ -56,6 +57,24 @@ class DriftBackupService {
|
||||
return _backupRepository.getBackupCount(userId);
|
||||
}
|
||||
|
||||
Future<void> manualBackup(List<LocalAsset> localAssets) async {
|
||||
List<UploadTask> tasks = [];
|
||||
for (final asset in localAssets) {
|
||||
final task = await _getUploadTask(
|
||||
asset,
|
||||
group: kManualUploadGroup,
|
||||
priority: 1, // High priority after upload motion photo part
|
||||
);
|
||||
if (task != null) {
|
||||
tasks.add(task);
|
||||
}
|
||||
}
|
||||
|
||||
if (tasks.isNotEmpty) {
|
||||
_uploadService.enqueueTasks(tasks);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> backup(
|
||||
String userId,
|
||||
void Function(EnqueueStatus status) onEnqueueTasks,
|
||||
@ -147,7 +166,11 @@ class DriftBackupService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<UploadTask?> _getUploadTask(LocalAsset asset) async {
|
||||
Future<UploadTask?> _getUploadTask(
|
||||
LocalAsset asset, {
|
||||
String group = kBackupGroup,
|
||||
int? priority,
|
||||
}) async {
|
||||
final entity = await _storageRepository.getAssetEntityForAsset(asset);
|
||||
if (entity == null) {
|
||||
return null;
|
||||
@ -193,7 +216,8 @@ class DriftBackupService {
|
||||
originalFileName: originalFileName,
|
||||
deviceAssetId: asset.id,
|
||||
metadata: metadata,
|
||||
group: kBackupGroup,
|
||||
group: group,
|
||||
priority: priority,
|
||||
);
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user