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_at": "Updated",
|
||||||
"updated_password": "Updated password",
|
"updated_password": "Updated password",
|
||||||
"upload": "Upload",
|
"upload": "Upload",
|
||||||
|
"upload_action_prompt": "{count} queued for upload",
|
||||||
"upload_concurrency": "Upload concurrency",
|
"upload_concurrency": "Upload concurrency",
|
||||||
"upload_details": "Upload Details",
|
"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.",
|
||||||
|
"upload_finished": "Upload finished",
|
||||||
"upload_progress": "Remaining {remaining, number} - Processed {processed, number}/{total, number}",
|
"upload_progress": "Remaining {remaining, number} - Processed {processed, number}/{total, number}",
|
||||||
"upload_skipped_duplicates": "Skipped {count, plural, one {# duplicate asset} other {# duplicate assets}}",
|
"upload_skipped_duplicates": "Skipped {count, plural, one {# duplicate asset} other {# duplicate assets}}",
|
||||||
"upload_status_duplicates": "Duplicates",
|
"upload_status_duplicates": "Duplicates",
|
||||||
@ -1954,6 +1956,7 @@
|
|||||||
"upload_success": "Upload success, refresh the page to see new upload assets.",
|
"upload_success": "Upload success, refresh the page to see new upload assets.",
|
||||||
"upload_to_immich": "Upload to Immich ({count})",
|
"upload_to_immich": "Upload to Immich ({count})",
|
||||||
"uploading": "Uploading",
|
"uploading": "Uploading",
|
||||||
|
"uploading_media": "Uploading media",
|
||||||
"url": "URL",
|
"url": "URL",
|
||||||
"usage": "Usage",
|
"usage": "Usage",
|
||||||
"use_biometric": "Use biometric",
|
"use_biometric": "Use biometric",
|
||||||
|
@ -141,6 +141,11 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
|||||||
_db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id),
|
_db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id),
|
||||||
useColumns: false,
|
useColumns: false,
|
||||||
),
|
),
|
||||||
|
leftOuterJoin(
|
||||||
|
_db.remoteAssetEntity,
|
||||||
|
_db.localAssetEntity.checksum.equalsExp(_db.remoteAssetEntity.checksum),
|
||||||
|
useColumns: false,
|
||||||
|
),
|
||||||
])
|
])
|
||||||
..addColumns([assetCountExp, dateExp])
|
..addColumns([assetCountExp, dateExp])
|
||||||
..where(_db.localAlbumAssetEntity.albumId.equals(albumId))
|
..where(_db.localAlbumAssetEntity.albumId.equals(albumId))
|
||||||
|
@ -203,6 +203,19 @@ class ImmichAppState extends ConsumerState<ImmichApp>
|
|||||||
),
|
),
|
||||||
progressBar: true,
|
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 {
|
Future<DeepLink> _deepLinkBuilder(PlatformDeepLink deepLink) async {
|
||||||
|
@ -1,16 +1,50 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.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/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.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 {
|
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
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return BaseActionButton(
|
return BaseActionButton(
|
||||||
iconData: Icons.backup_outlined,
|
iconData: Icons.backup_outlined,
|
||||||
label: "upload".t(context: context),
|
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/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/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/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/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/infrastructure/asset_viewer/current_asset.provider.dart';
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
@ -37,6 +38,8 @@ class ViewerBottomBar extends ConsumerWidget {
|
|||||||
|
|
||||||
final actions = <Widget>[
|
final actions = <Widget>[
|
||||||
const ShareActionButton(source: ActionSource.viewer),
|
const ShareActionButton(source: ActionSource.viewer),
|
||||||
|
if (asset.isLocalOnly)
|
||||||
|
const UploadActionButton(source: ActionSource.viewer),
|
||||||
if (asset.hasRemote && isOwner)
|
if (asset.hasRemote && isOwner)
|
||||||
const ArchiveActionButton(source: ActionSource.viewer),
|
const ArchiveActionButton(source: ActionSource.viewer),
|
||||||
];
|
];
|
||||||
|
@ -63,7 +63,7 @@ class AssetDetailBottomSheet extends ConsumerWidget {
|
|||||||
],
|
],
|
||||||
if (asset.storage == AssetState.local) ...[
|
if (asset.storage == AssetState.local) ...[
|
||||||
const DeleteLocalActionButton(source: ActionSource.viewer),
|
const DeleteLocalActionButton(source: ActionSource.viewer),
|
||||||
const UploadActionButton(),
|
const UploadActionButton(source: ActionSource.timeline),
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -53,7 +53,7 @@ class ArchiveBottomSheet extends ConsumerWidget {
|
|||||||
],
|
],
|
||||||
if (multiselect.hasLocal) ...[
|
if (multiselect.hasLocal) ...[
|
||||||
const DeleteLocalActionButton(source: ActionSource.timeline),
|
const DeleteLocalActionButton(source: ActionSource.timeline),
|
||||||
const UploadActionButton(),
|
const UploadActionButton(source: ActionSource.timeline),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
@ -53,7 +53,7 @@ class FavoriteBottomSheet extends ConsumerWidget {
|
|||||||
],
|
],
|
||||||
if (multiselect.hasLocal) ...[
|
if (multiselect.hasLocal) ...[
|
||||||
const DeleteLocalActionButton(source: ActionSource.timeline),
|
const DeleteLocalActionButton(source: ActionSource.timeline),
|
||||||
const UploadActionButton(),
|
const UploadActionButton(source: ActionSource.timeline),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
@ -56,7 +56,7 @@ class GeneralBottomSheet extends ConsumerWidget {
|
|||||||
],
|
],
|
||||||
if (multiselect.hasLocal) ...[
|
if (multiselect.hasLocal) ...[
|
||||||
const DeleteLocalActionButton(source: ActionSource.timeline),
|
const DeleteLocalActionButton(source: ActionSource.timeline),
|
||||||
const UploadActionButton(),
|
const UploadActionButton(source: ActionSource.timeline),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
@ -18,7 +18,7 @@ class LocalAlbumBottomSheet extends ConsumerWidget {
|
|||||||
actions: [
|
actions: [
|
||||||
ShareActionButton(source: ActionSource.timeline),
|
ShareActionButton(source: ActionSource.timeline),
|
||||||
DeleteLocalActionButton(source: ActionSource.timeline),
|
DeleteLocalActionButton(source: ActionSource.timeline),
|
||||||
UploadActionButton(),
|
UploadActionButton(source: ActionSource.timeline),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -56,7 +56,7 @@ class RemoteAlbumBottomSheet extends ConsumerWidget {
|
|||||||
],
|
],
|
||||||
if (multiselect.hasLocal) ...[
|
if (multiselect.hasLocal) ...[
|
||||||
const DeleteLocalActionButton(source: ActionSource.timeline),
|
const DeleteLocalActionButton(source: ActionSource.timeline),
|
||||||
const UploadActionButton(),
|
const UploadActionButton(source: ActionSource.timeline),
|
||||||
],
|
],
|
||||||
RemoveFromAlbumActionButton(
|
RemoveFromAlbumActionButton(
|
||||||
source: ActionSource.timeline,
|
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/timeline/multiselect.provider.dart';
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
import 'package:immich_mobile/services/action.service.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:immich_mobile/services/timeline.service.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
@ -32,12 +33,14 @@ class ActionResult {
|
|||||||
class ActionNotifier extends Notifier<void> {
|
class ActionNotifier extends Notifier<void> {
|
||||||
final Logger _logger = Logger('ActionNotifier');
|
final Logger _logger = Logger('ActionNotifier');
|
||||||
late ActionService _service;
|
late ActionService _service;
|
||||||
|
late DriftBackupService _backupService;
|
||||||
|
|
||||||
ActionNotifier() : super();
|
ActionNotifier() : super();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void build() {
|
void build() {
|
||||||
_service = ref.watch(actionServiceProvider);
|
_service = ref.watch(actionServiceProvider);
|
||||||
|
_backupService = ref.watch(driftBackupServiceProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String> _getRemoteIdsForSource(ActionSource source) {
|
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> {
|
extension on Iterable<RemoteAsset> {
|
||||||
|
@ -20,6 +20,11 @@ class UploadRepository {
|
|||||||
taskStatusCallback: (update) => onUploadStatus?.call(update),
|
taskStatusCallback: (update) => onUploadStatus?.call(update),
|
||||||
taskProgressCallback: (update) => onTaskProgress?.call(update),
|
taskProgressCallback: (update) => onTaskProgress?.call(update),
|
||||||
);
|
);
|
||||||
|
FileDownloader().registerCallbacks(
|
||||||
|
group: kManualUploadGroup,
|
||||||
|
taskStatusCallback: (_) => {},
|
||||||
|
taskProgressCallback: (_) => {},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void enqueueAll(List<UploadTask> tasks) {
|
void enqueueAll(List<UploadTask> tasks) {
|
||||||
|
@ -26,6 +26,7 @@ final driftBackupServiceProvider = Provider<DriftBackupService>(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// TODO: Rename to UploadService after removing Isar
|
||||||
class DriftBackupService {
|
class DriftBackupService {
|
||||||
DriftBackupService(
|
DriftBackupService(
|
||||||
this._backupRepository,
|
this._backupRepository,
|
||||||
@ -56,6 +57,24 @@ class DriftBackupService {
|
|||||||
return _backupRepository.getBackupCount(userId);
|
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(
|
Future<void> backup(
|
||||||
String userId,
|
String userId,
|
||||||
void Function(EnqueueStatus status) onEnqueueTasks,
|
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);
|
final entity = await _storageRepository.getAssetEntityForAsset(asset);
|
||||||
if (entity == null) {
|
if (entity == null) {
|
||||||
return null;
|
return null;
|
||||||
@ -193,7 +216,8 @@ class DriftBackupService {
|
|||||||
originalFileName: originalFileName,
|
originalFileName: originalFileName,
|
||||||
deviceAssetId: asset.id,
|
deviceAssetId: asset.id,
|
||||||
metadata: metadata,
|
metadata: metadata,
|
||||||
group: kBackupGroup,
|
group: group,
|
||||||
|
priority: priority,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user