mirror of
https://github.com/KevinMidboe/immich.git
synced 2025-10-29 17:40:28 +00:00
feat(mobile): Manual asset upload (#3445)
* fix: exclude albums filter in backup provider * refactor: Separate builder methods for Top Control App Bar buttons * fix: Show download button only for Remote only assets * fix(mobile): Force Refresh duration is too low to trigger it consistently * feat(mobile): Make Buttons dynamic in Home Selection DraggableScrollableSheet * feat(mobile): Manual Asset upload * refactor(mobile): Replace _showToast with ImmichToast calls * refactor(mobile): home_page selectionAssetState handling * chore(mobile): min and initial size of DraggableScrollState increased This is to prevent the buttons in the bottom sheet getting clipped behind the 3 way navigation buttons in the default density of Android devices * feat(mobile): notifications for manual upload progress * wording --------- Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
@@ -13,11 +13,13 @@ class TopControlAppBar extends HookConsumerWidget {
|
||||
required this.onToggleMotionVideo,
|
||||
required this.isPlayingMotionVideo,
|
||||
required this.onFavorite,
|
||||
required this.onUploadPressed,
|
||||
required this.isFavorite,
|
||||
}) : super(key: key);
|
||||
|
||||
final Asset asset;
|
||||
final Function onMoreInfoPressed;
|
||||
final VoidCallback? onUploadPressed;
|
||||
final VoidCallback? onDownloadPressed;
|
||||
final VoidCallback onToggleMotionVideo;
|
||||
final VoidCallback onAddToAlbumPressed;
|
||||
@@ -39,10 +41,69 @@ class TopControlAppBar extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
return AppBar(
|
||||
foregroundColor: Colors.grey[100],
|
||||
backgroundColor: Colors.transparent,
|
||||
leading: IconButton(
|
||||
Widget buildLivePhotoButton() {
|
||||
return IconButton(
|
||||
onPressed: () {
|
||||
onToggleMotionVideo();
|
||||
},
|
||||
icon: isPlayingMotionVideo
|
||||
? Icon(
|
||||
Icons.motion_photos_pause_outlined,
|
||||
color: Colors.grey[200],
|
||||
)
|
||||
: Icon(
|
||||
Icons.play_circle_outline_rounded,
|
||||
color: Colors.grey[200],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildMoreInfoButton() {
|
||||
return IconButton(
|
||||
onPressed: () {
|
||||
onMoreInfoPressed();
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.info_outline_rounded,
|
||||
color: Colors.grey[200],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildDownloadButton() {
|
||||
return IconButton(
|
||||
onPressed: onDownloadPressed,
|
||||
icon: Icon(
|
||||
Icons.cloud_download_outlined,
|
||||
color: Colors.grey[200],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildAddToAlbumButtom() {
|
||||
return IconButton(
|
||||
onPressed: () {
|
||||
onAddToAlbumPressed();
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.add,
|
||||
color: Colors.grey[200],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildUploadButton() {
|
||||
return IconButton(
|
||||
onPressed: onUploadPressed,
|
||||
icon: Icon(
|
||||
Icons.backup_outlined,
|
||||
color: Colors.grey[200],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildBackButton() {
|
||||
return IconButton(
|
||||
onPressed: () {
|
||||
AutoRouter.of(context).pop();
|
||||
},
|
||||
@@ -51,54 +112,23 @@ class TopControlAppBar extends HookConsumerWidget {
|
||||
size: 20.0,
|
||||
color: Colors.grey[200],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return AppBar(
|
||||
foregroundColor: Colors.grey[100],
|
||||
backgroundColor: Colors.transparent,
|
||||
leading: buildBackButton(),
|
||||
actionsIconTheme: const IconThemeData(
|
||||
size: iconSize,
|
||||
),
|
||||
actions: [
|
||||
if (asset.isRemote) buildFavoriteButton(),
|
||||
if (asset.livePhotoVideoId != null)
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
onToggleMotionVideo();
|
||||
},
|
||||
icon: isPlayingMotionVideo
|
||||
? Icon(
|
||||
Icons.motion_photos_pause_outlined,
|
||||
color: Colors.grey[200],
|
||||
)
|
||||
: Icon(
|
||||
Icons.play_circle_outline_rounded,
|
||||
color: Colors.grey[200],
|
||||
),
|
||||
),
|
||||
if (asset.storage == AssetState.remote)
|
||||
IconButton(
|
||||
onPressed: onDownloadPressed,
|
||||
icon: Icon(
|
||||
Icons.cloud_download_outlined,
|
||||
color: Colors.grey[200],
|
||||
),
|
||||
),
|
||||
if (asset.isRemote)
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
onAddToAlbumPressed();
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.add,
|
||||
color: Colors.grey[200],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
onMoreInfoPressed();
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.info_outline_rounded,
|
||||
color: Colors.grey[200],
|
||||
),
|
||||
),
|
||||
if (asset.livePhotoVideoId != null) buildLivePhotoButton(),
|
||||
if (asset.isLocal && !asset.isRemote) buildUploadButton(),
|
||||
if (asset.isRemote && !asset.isLocal) buildDownloadButton(),
|
||||
if (asset.isRemote) buildAddToAlbumButtom(),
|
||||
buildMoreInfoButton()
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ import 'package:immich_mobile/modules/asset_viewer/ui/advanced_bottom_sheet.dart
|
||||
import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart';
|
||||
import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart';
|
||||
import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/manual_upload.provider.dart';
|
||||
import 'package:immich_mobile/modules/home/ui/upload_dialog.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/modules/home/ui/delete_dialog.dart';
|
||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
||||
@@ -276,6 +278,21 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
AutoRouter.of(context).pop();
|
||||
}
|
||||
|
||||
handleUpload(Asset asset) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext _) {
|
||||
return UploadDialog(
|
||||
onUpload: () {
|
||||
ref
|
||||
.read(manualUploadProvider.notifier)
|
||||
.uploadAssets(context, [asset]);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
buildAppBar() {
|
||||
return IgnorePointer(
|
||||
ignoring: !ref.watch(showControlsProvider),
|
||||
@@ -291,6 +308,8 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
onMoreInfoPressed: showInfo,
|
||||
onFavorite:
|
||||
asset().isRemote ? () => toggleFavorite(asset()) : null,
|
||||
onUploadPressed:
|
||||
asset().isLocal ? () => handleUpload(asset()) : null,
|
||||
onDownloadPressed: asset().isLocal
|
||||
? null
|
||||
: () => ref
|
||||
|
||||
@@ -18,6 +18,7 @@ import 'package:immich_mobile/modules/backup/services/backup.service.dart';
|
||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/shared/services/api.service.dart';
|
||||
import 'package:immich_mobile/utils/backup_progress.dart';
|
||||
import 'package:immich_mobile/utils/diff.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:path_provider_ios/path_provider_ios.dart';
|
||||
@@ -34,7 +35,6 @@ class BackgroundService {
|
||||
MethodChannel('immich/foregroundChannel');
|
||||
static const MethodChannel _backgroundChannel =
|
||||
MethodChannel('immich/backgroundChannel');
|
||||
static final NumberFormat numberFormat = NumberFormat("###0.##");
|
||||
static const notifyInterval = Duration(milliseconds: 400);
|
||||
bool _isBackgroundInitialized = false;
|
||||
CancellationToken? _cancellationToken;
|
||||
@@ -48,10 +48,10 @@ class BackgroundService {
|
||||
int _assetsToUploadCount = 0;
|
||||
String _lastPrintedDetailContent = "";
|
||||
String? _lastPrintedDetailTitle;
|
||||
late final _Throttle _throttledNotifiy =
|
||||
_Throttle(_updateProgress, notifyInterval);
|
||||
late final _Throttle _throttledDetailNotify =
|
||||
_Throttle(_updateDetailProgress, notifyInterval);
|
||||
late final ThrottleProgressUpdate _throttledNotifiy =
|
||||
ThrottleProgressUpdate(_updateProgress, notifyInterval);
|
||||
late final ThrottleProgressUpdate _throttledDetailNotify =
|
||||
ThrottleProgressUpdate(_updateDetailProgress, notifyInterval);
|
||||
|
||||
bool get isBackgroundInitialized {
|
||||
return _isBackgroundInitialized;
|
||||
@@ -439,7 +439,12 @@ class BackgroundService {
|
||||
_uploadedAssetsCount = 0;
|
||||
_updateNotification(
|
||||
title: "backup_background_service_in_progress_notification".tr(),
|
||||
content: notifyTotalProgress ? _formatAssetBackupProgress() : null,
|
||||
content: notifyTotalProgress
|
||||
? formatAssetBackupProgress(
|
||||
_uploadedAssetsCount,
|
||||
_assetsToUploadCount,
|
||||
)
|
||||
: null,
|
||||
progress: 0,
|
||||
max: notifyTotalProgress ? _assetsToUploadCount : 0,
|
||||
indeterminate: !notifyTotalProgress,
|
||||
@@ -464,11 +469,6 @@ class BackgroundService {
|
||||
return ok;
|
||||
}
|
||||
|
||||
String _formatAssetBackupProgress() {
|
||||
final int percent = (_uploadedAssetsCount * 100) ~/ _assetsToUploadCount;
|
||||
return "$percent% ($_uploadedAssetsCount/$_assetsToUploadCount)";
|
||||
}
|
||||
|
||||
void _onAssetUploaded(String deviceAssetId, String deviceId, bool isDup) {
|
||||
_uploadedAssetsCount++;
|
||||
_throttledNotifiy();
|
||||
@@ -480,7 +480,7 @@ class BackgroundService {
|
||||
|
||||
void _updateDetailProgress(String? title, int progress, int total) {
|
||||
final String msg =
|
||||
total > 0 ? _humanReadableBytesProgress(progress, total) : "";
|
||||
total > 0 ? humanReadableBytesProgress(progress, total) : "";
|
||||
// only update if message actually differs (to stop many useless notification updates on large assets or slow connections)
|
||||
if (msg != _lastPrintedDetailContent || _lastPrintedDetailTitle != title) {
|
||||
_lastPrintedDetailContent = msg;
|
||||
@@ -500,7 +500,10 @@ class BackgroundService {
|
||||
progress: _uploadedAssetsCount,
|
||||
max: _assetsToUploadCount,
|
||||
title: title,
|
||||
content: _formatAssetBackupProgress(),
|
||||
content: formatAssetBackupProgress(
|
||||
_uploadedAssetsCount,
|
||||
_assetsToUploadCount,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -546,26 +549,6 @@ class BackgroundService {
|
||||
return true;
|
||||
}
|
||||
|
||||
/// prints percentage and absolute progress in useful (kilo/mega/giga)bytes
|
||||
static String _humanReadableBytesProgress(int bytes, int bytesTotal) {
|
||||
String unit = "KB"; // Kilobyte
|
||||
if (bytesTotal >= 0x40000000) {
|
||||
unit = "GB"; // Gigabyte
|
||||
bytes >>= 20;
|
||||
bytesTotal >>= 20;
|
||||
} else if (bytesTotal >= 0x100000) {
|
||||
unit = "MB"; // Megabyte
|
||||
bytes >>= 10;
|
||||
bytesTotal >>= 10;
|
||||
} else if (bytesTotal < 0x400) {
|
||||
return "$bytes / $bytesTotal B";
|
||||
}
|
||||
final int percent = (bytes * 100) ~/ bytesTotal;
|
||||
final String done = numberFormat.format(bytes / 1024.0);
|
||||
final String total = numberFormat.format(bytesTotal / 1024.0);
|
||||
return "$percent% ($done/$total$unit)";
|
||||
}
|
||||
|
||||
Future<DateTime?> getIOSBackupLastRun(IosBackgroundTask task) async {
|
||||
if (!Platform.isIOS) {
|
||||
return null;
|
||||
@@ -598,43 +581,6 @@ class BackgroundService {
|
||||
|
||||
enum IosBackgroundTask { fetch, processing }
|
||||
|
||||
class _Throttle {
|
||||
_Throttle(this._fun, Duration interval) : _interval = interval.inMicroseconds;
|
||||
final void Function(String?, int, int) _fun;
|
||||
final int _interval;
|
||||
int _invokedAt = 0;
|
||||
Timer? _timer;
|
||||
|
||||
String? title;
|
||||
int progress = 0;
|
||||
int total = 0;
|
||||
|
||||
void call({
|
||||
final String? title,
|
||||
final int progress = 0,
|
||||
final int total = 0,
|
||||
}) {
|
||||
final time = Timeline.now;
|
||||
this.title = title ?? this.title;
|
||||
this.progress = progress;
|
||||
this.total = total;
|
||||
if (time > _invokedAt + _interval) {
|
||||
_timer?.cancel();
|
||||
_onTimeElapsed();
|
||||
} else {
|
||||
_timer ??= Timer(Duration(microseconds: _interval), _onTimeElapsed);
|
||||
}
|
||||
}
|
||||
|
||||
void _onTimeElapsed() {
|
||||
_invokedAt = Timeline.now;
|
||||
_fun(title, progress, total);
|
||||
_timer = null;
|
||||
// clear title to not send/overwrite it next time if unchanged
|
||||
title = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// entry point called by Kotlin/Java code; needs to be a top-level function
|
||||
@pragma('vm:entry-point')
|
||||
void _nativeEntry() {
|
||||
|
||||
@@ -6,7 +6,13 @@ import 'package:photo_manager/photo_manager.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/available_album.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.dart';
|
||||
|
||||
enum BackUpProgressEnum { idle, inProgress, inBackground, done }
|
||||
enum BackUpProgressEnum {
|
||||
idle,
|
||||
inProgress,
|
||||
manualInProgress,
|
||||
inBackground,
|
||||
done
|
||||
}
|
||||
|
||||
class BackUpState {
|
||||
// enum
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import 'package:cancellation_token_http/http.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.dart';
|
||||
|
||||
class ManualUploadState {
|
||||
final CancellationToken cancelToken;
|
||||
|
||||
final double progressInPercentage;
|
||||
|
||||
// Current Backup Asset
|
||||
final CurrentUploadAsset currentUploadAsset;
|
||||
|
||||
/// Manual Upload
|
||||
final int manualUploadsTotal;
|
||||
final int manualUploadFailures;
|
||||
final int manualUploadSuccess;
|
||||
|
||||
const ManualUploadState({
|
||||
required this.progressInPercentage,
|
||||
required this.cancelToken,
|
||||
required this.currentUploadAsset,
|
||||
required this.manualUploadsTotal,
|
||||
required this.manualUploadFailures,
|
||||
required this.manualUploadSuccess,
|
||||
});
|
||||
|
||||
ManualUploadState copyWith({
|
||||
double? progressInPercentage,
|
||||
CancellationToken? cancelToken,
|
||||
CurrentUploadAsset? currentUploadAsset,
|
||||
int? manualUploadsTotal,
|
||||
int? manualUploadFailures,
|
||||
int? manualUploadSuccess,
|
||||
}) {
|
||||
return ManualUploadState(
|
||||
progressInPercentage: progressInPercentage ?? this.progressInPercentage,
|
||||
cancelToken: cancelToken ?? this.cancelToken,
|
||||
currentUploadAsset: currentUploadAsset ?? this.currentUploadAsset,
|
||||
manualUploadsTotal: manualUploadsTotal ?? this.manualUploadsTotal,
|
||||
manualUploadFailures: manualUploadFailures ?? this.manualUploadFailures,
|
||||
manualUploadSuccess: manualUploadSuccess ?? this.manualUploadSuccess,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ManualUploadState(progressInPercentage: $progressInPercentage, cancelToken: $cancelToken, currentUploadAsset: $currentUploadAsset, manualUploadsTotal: $manualUploadsTotal, manualUploadSuccess: $manualUploadSuccess, manualUploadFailures: $manualUploadFailures)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is ManualUploadState &&
|
||||
other.progressInPercentage == progressInPercentage &&
|
||||
other.cancelToken == cancelToken &&
|
||||
other.currentUploadAsset == currentUploadAsset &&
|
||||
other.manualUploadsTotal == manualUploadsTotal &&
|
||||
other.manualUploadFailures == manualUploadFailures &&
|
||||
other.manualUploadSuccess == manualUploadSuccess;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return progressInPercentage.hashCode ^
|
||||
cancelToken.hashCode ^
|
||||
currentUploadAsset.hashCode ^
|
||||
manualUploadsTotal.hashCode ^
|
||||
manualUploadFailures.hashCode ^
|
||||
manualUploadSuccess.hashCode;
|
||||
}
|
||||
}
|
||||
@@ -388,7 +388,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
|
||||
if (state.backupProgress != BackUpProgressEnum.inBackground) {
|
||||
await _getBackupAlbumsInfo();
|
||||
await _updateServerInfo();
|
||||
await updateServerInfo();
|
||||
await _updateBackupAssetCount();
|
||||
}
|
||||
}
|
||||
@@ -465,7 +465,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
_onSetCurrentBackupAsset,
|
||||
_onBackupError,
|
||||
);
|
||||
await _notifyBackgroundServiceCanRun();
|
||||
await notifyBackgroundServiceCanRun();
|
||||
} else {
|
||||
openAppSettings();
|
||||
}
|
||||
@@ -487,7 +487,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
|
||||
void cancelBackup() {
|
||||
if (state.backupProgress != BackUpProgressEnum.inProgress) {
|
||||
_notifyBackgroundServiceCanRun();
|
||||
notifyBackgroundServiceCanRun();
|
||||
}
|
||||
state.cancelToken.cancel();
|
||||
state = state.copyWith(
|
||||
@@ -537,7 +537,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
_updatePersistentAlbumsSelection();
|
||||
}
|
||||
|
||||
_updateServerInfo();
|
||||
updateServerInfo();
|
||||
}
|
||||
|
||||
void _onUploadProgress(int sent, int total) {
|
||||
@@ -546,7 +546,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _updateServerInfo() async {
|
||||
Future<void> updateServerInfo() async {
|
||||
final serverInfo = await _serverInfoService.getServerInfo();
|
||||
|
||||
// Update server info
|
||||
@@ -569,9 +569,9 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
|
||||
// Check if this device is enable backup by the user
|
||||
if (state.autoBackup) {
|
||||
// check if backup is alreayd in process - then return
|
||||
// check if backup is already in process - then return
|
||||
if (state.backupProgress == BackUpProgressEnum.inProgress) {
|
||||
log.info("[_resumeBackup] Backup is already in progress - abort");
|
||||
log.info("[_resumeBackup] Auto Backup is already in progress - abort");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -580,6 +580,11 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.backupProgress == BackUpProgressEnum.manualInProgress) {
|
||||
log.info("[_resumeBackup] Manual upload is running - abort");
|
||||
return;
|
||||
}
|
||||
|
||||
// Run backup
|
||||
log.info("[_resumeBackup] Start back up");
|
||||
await startBackupProcess();
|
||||
@@ -594,7 +599,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
.findAll();
|
||||
final List<BackupAlbum> excludedBackupAlbums = await _db.backupAlbums
|
||||
.filter()
|
||||
.selectionEqualTo(BackupSelection.select)
|
||||
.selectionEqualTo(BackupSelection.exclude)
|
||||
.findAll();
|
||||
Set<AvailableAlbum> selectedAlbums = state.selectedBackupAlbums;
|
||||
Set<AvailableAlbum> excludedAlbums = state.excludedBackupAlbums;
|
||||
@@ -646,7 +651,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<void> _notifyBackgroundServiceCanRun() async {
|
||||
Future<void> notifyBackgroundServiceCanRun() async {
|
||||
const allowedStates = [
|
||||
AppStateEnum.inactive,
|
||||
AppStateEnum.paused,
|
||||
@@ -656,6 +661,11 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
_backgroundService.releaseLock();
|
||||
}
|
||||
}
|
||||
|
||||
BackUpProgressEnum get backupProgress => state.backupProgress;
|
||||
void updateBackupProgress(BackUpProgressEnum backupProgress) {
|
||||
state = state.copyWith(backupProgress: backupProgress);
|
||||
}
|
||||
}
|
||||
|
||||
final backupProvider =
|
||||
|
||||
300
mobile/lib/modules/backup/providers/manual_upload.provider.dart
Normal file
300
mobile/lib/modules/backup/providers/manual_upload.provider.dart
Normal file
@@ -0,0 +1,300 @@
|
||||
import 'package:cancellation_token_http/http.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/backup/background_service/background.service.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/error_upload_asset.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/manual_upload_state.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
|
||||
import 'package:immich_mobile/modules/backup/services/backup.service.dart';
|
||||
import 'package:immich_mobile/modules/onboarding/providers/gallery_permission.provider.dart';
|
||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/services/local_notification.service.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
||||
import 'package:immich_mobile/utils/backup_progress.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
|
||||
final manualUploadProvider =
|
||||
StateNotifierProvider<ManualUploadNotifier, ManualUploadState>((ref) {
|
||||
return ManualUploadNotifier(
|
||||
ref.watch(localNotificationService),
|
||||
ref.watch(backgroundServiceProvider),
|
||||
ref.watch(backupServiceProvider),
|
||||
ref.watch(backupProvider.notifier),
|
||||
ref,
|
||||
);
|
||||
});
|
||||
|
||||
class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
|
||||
final LocalNotificationService _localNotificationService;
|
||||
final BackgroundService _backgroundService;
|
||||
final BackupService _backupService;
|
||||
final BackupNotifier _backupProvider;
|
||||
final Ref ref;
|
||||
|
||||
ManualUploadNotifier(
|
||||
this._localNotificationService,
|
||||
this._backgroundService,
|
||||
this._backupService,
|
||||
this._backupProvider,
|
||||
this.ref,
|
||||
) : super(
|
||||
ManualUploadState(
|
||||
progressInPercentage: 0,
|
||||
cancelToken: CancellationToken(),
|
||||
currentUploadAsset: CurrentUploadAsset(
|
||||
id: '...',
|
||||
fileCreatedAt: DateTime.parse('2020-10-04'),
|
||||
fileName: '...',
|
||||
fileType: '...',
|
||||
),
|
||||
manualUploadsTotal: 0,
|
||||
manualUploadSuccess: 0,
|
||||
manualUploadFailures: 0,
|
||||
),
|
||||
);
|
||||
|
||||
int get _uploadedAssetsCount =>
|
||||
state.manualUploadSuccess + state.manualUploadFailures;
|
||||
|
||||
String _lastPrintedDetailContent = '';
|
||||
String? _lastPrintedDetailTitle;
|
||||
|
||||
static const notifyInterval = Duration(milliseconds: 500);
|
||||
late final ThrottleProgressUpdate _throttledNotifiy =
|
||||
ThrottleProgressUpdate(_updateProgress, notifyInterval);
|
||||
late final ThrottleProgressUpdate _throttledDetailNotify =
|
||||
ThrottleProgressUpdate(_updateDetailProgress, notifyInterval);
|
||||
|
||||
void _updateProgress(String? title, int progress, int total) {
|
||||
// Guard against throttling calling this method after the upload is done
|
||||
if (_backupProvider.backupProgress == BackUpProgressEnum.manualInProgress) {
|
||||
_localNotificationService.showOrUpdateManualUploadStatus(
|
||||
"backup_background_service_in_progress_notification".tr(),
|
||||
formatAssetBackupProgress(
|
||||
_uploadedAssetsCount,
|
||||
state.manualUploadsTotal,
|
||||
),
|
||||
maxProgress: state.manualUploadsTotal,
|
||||
progress: _uploadedAssetsCount,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _updateDetailProgress(String? title, int progress, int total) {
|
||||
// Guard against throttling calling this method after the upload is done
|
||||
if (_backupProvider.backupProgress == BackUpProgressEnum.manualInProgress) {
|
||||
final String msg =
|
||||
total > 0 ? humanReadableBytesProgress(progress, total) : "";
|
||||
// only update if message actually differs (to stop many useless notification updates on large assets or slow connections)
|
||||
if (msg != _lastPrintedDetailContent ||
|
||||
title != _lastPrintedDetailTitle) {
|
||||
_lastPrintedDetailContent = msg;
|
||||
_lastPrintedDetailTitle = title;
|
||||
_localNotificationService.showOrUpdateManualUploadStatus(
|
||||
title ?? 'Uploading',
|
||||
msg,
|
||||
progress: total > 0 ? (progress * 1000) ~/ total : 0,
|
||||
maxProgress: 1000,
|
||||
isDetailed: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _onManualAssetUploaded(
|
||||
String deviceAssetId,
|
||||
String deviceId,
|
||||
bool isDuplicated,
|
||||
) {
|
||||
state = state.copyWith(manualUploadSuccess: state.manualUploadSuccess + 1);
|
||||
_backupProvider.updateServerInfo();
|
||||
if (state.manualUploadsTotal > 1) {
|
||||
_throttledNotifiy();
|
||||
}
|
||||
}
|
||||
|
||||
void _onManualBackupError(ErrorUploadAsset errorAssetInfo) {
|
||||
state =
|
||||
state.copyWith(manualUploadFailures: state.manualUploadFailures + 1);
|
||||
if (state.manualUploadsTotal > 1) {
|
||||
_throttledNotifiy();
|
||||
}
|
||||
}
|
||||
|
||||
void _onProgress(int sent, int total) {
|
||||
final title = "backup_background_service_current_upload_notification"
|
||||
.tr(args: [state.currentUploadAsset.fileName]);
|
||||
_throttledDetailNotify(title: title, progress: sent, total: total);
|
||||
}
|
||||
|
||||
void _onSetCurrentBackupAsset(CurrentUploadAsset currentUploadAsset) {
|
||||
state = state.copyWith(currentUploadAsset: currentUploadAsset);
|
||||
_throttledDetailNotify.title =
|
||||
"backup_background_service_current_upload_notification"
|
||||
.tr(args: [currentUploadAsset.fileName]);
|
||||
_throttledDetailNotify.progress = 0;
|
||||
_throttledDetailNotify.total = 0;
|
||||
}
|
||||
|
||||
Future<bool> _startUpload(Iterable<Asset> allManualUploads) async {
|
||||
try {
|
||||
_backupProvider.updateBackupProgress(BackUpProgressEnum.manualInProgress);
|
||||
|
||||
if (ref.read(galleryPermissionNotifier.notifier).hasPermission) {
|
||||
await PhotoManager.clearFileCache();
|
||||
|
||||
Set<AssetEntity> allUploadAssets = allManualUploads
|
||||
.where((e) => e.isLocal && e.local != null)
|
||||
.map((e) => e.local!)
|
||||
.toSet();
|
||||
|
||||
if (allUploadAssets.isEmpty) {
|
||||
debugPrint("[_startUpload] No Assets to upload - Abort Process");
|
||||
_backupProvider.updateBackupProgress(BackUpProgressEnum.idle);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reset state
|
||||
state = state.copyWith(
|
||||
manualUploadsTotal: allManualUploads.length,
|
||||
manualUploadSuccess: 0,
|
||||
manualUploadFailures: 0,
|
||||
currentUploadAsset: CurrentUploadAsset(
|
||||
id: '...',
|
||||
fileCreatedAt: DateTime.parse('2020-10-04'),
|
||||
fileName: '...',
|
||||
fileType: '...',
|
||||
),
|
||||
cancelToken: CancellationToken(),
|
||||
);
|
||||
|
||||
if (state.manualUploadsTotal > 1) {
|
||||
_throttledNotifiy();
|
||||
}
|
||||
|
||||
// Show detailed asset if enabled in settings or if a single asset is uploaded
|
||||
bool showDetailedNotification =
|
||||
ref.read(appSettingsServiceProvider).getSetting<bool>(
|
||||
AppSettingsEnum.backgroundBackupSingleProgress,
|
||||
) ||
|
||||
state.manualUploadsTotal == 1;
|
||||
|
||||
final bool ok = await _backupService.backupAsset(
|
||||
allUploadAssets,
|
||||
state.cancelToken,
|
||||
_onManualAssetUploaded,
|
||||
showDetailedNotification ? _onProgress : (sent, total) {},
|
||||
showDetailedNotification ? _onSetCurrentBackupAsset : (asset) {},
|
||||
_onManualBackupError,
|
||||
);
|
||||
|
||||
// Close detailed notification
|
||||
await _localNotificationService.closeNotification(
|
||||
LocalNotificationService.manualUploadDetailedNotificationID,
|
||||
);
|
||||
|
||||
bool hasErrors = false;
|
||||
if ((state.manualUploadFailures != 0 &&
|
||||
state.manualUploadSuccess == 0) ||
|
||||
(!ok && !state.cancelToken.isCancelled)) {
|
||||
await _localNotificationService.showOrUpdateManualUploadStatus(
|
||||
"backup_manual_title".tr(),
|
||||
"backup_manual_failed".tr(),
|
||||
presentBanner: true,
|
||||
);
|
||||
hasErrors = true;
|
||||
} else if (state.manualUploadSuccess != 0) {
|
||||
await _localNotificationService.showOrUpdateManualUploadStatus(
|
||||
"backup_manual_title".tr(),
|
||||
"backup_manual_success".tr(),
|
||||
presentBanner: true,
|
||||
);
|
||||
}
|
||||
|
||||
_backupProvider.updateBackupProgress(BackUpProgressEnum.idle);
|
||||
await _backupProvider.notifyBackgroundServiceCanRun();
|
||||
return !hasErrors;
|
||||
} else {
|
||||
openAppSettings();
|
||||
debugPrint("[_startUpload] Do not have permission to the gallery");
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("ERROR _startUpload: ${e.toString()}");
|
||||
}
|
||||
await _localNotificationService.closeNotification(
|
||||
LocalNotificationService.manualUploadDetailedNotificationID,
|
||||
);
|
||||
_backupProvider.updateBackupProgress(BackUpProgressEnum.idle);
|
||||
await _backupProvider.notifyBackgroundServiceCanRun();
|
||||
return false;
|
||||
}
|
||||
|
||||
void cancelBackup() {
|
||||
if (_backupProvider.backupProgress != BackUpProgressEnum.manualInProgress) {
|
||||
_backupProvider.notifyBackgroundServiceCanRun();
|
||||
}
|
||||
state.cancelToken.cancel();
|
||||
_backupProvider.updateBackupProgress(BackUpProgressEnum.idle);
|
||||
}
|
||||
|
||||
Future<bool> uploadAssets(
|
||||
BuildContext context,
|
||||
Iterable<Asset> allManualUploads,
|
||||
) async {
|
||||
// assumes the background service is currently running and
|
||||
// waits until it has stopped to start the backup.
|
||||
final bool hasLock = await _backgroundService.acquireLock();
|
||||
if (!hasLock) {
|
||||
debugPrint("[uploadAssets] could not acquire lock, exiting");
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: "backup_manual_failed".tr(),
|
||||
toastType: ToastType.info,
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
durationInSecond: 3,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool showInProgress = false;
|
||||
|
||||
// check if backup is already in process - then return
|
||||
if (_backupProvider.backupProgress == BackUpProgressEnum.manualInProgress) {
|
||||
debugPrint("[uploadAssets] Manual upload is already running - abort");
|
||||
showInProgress = true;
|
||||
}
|
||||
|
||||
if (_backupProvider.backupProgress == BackUpProgressEnum.inProgress) {
|
||||
debugPrint("[uploadAssets] Auto Backup is already in progress - abort");
|
||||
showInProgress = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_backupProvider.backupProgress == BackUpProgressEnum.inBackground) {
|
||||
debugPrint("[uploadAssets] Background backup is running - abort");
|
||||
showInProgress = true;
|
||||
}
|
||||
|
||||
if (showInProgress) {
|
||||
if (context.mounted) {
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: "backup_manual_in_progress".tr(),
|
||||
toastType: ToastType.info,
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
durationInSecond: 3,
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return _startUpload(allManualUploads);
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,8 @@ class BackupControllerPage extends HookConsumerWidget {
|
||||
|
||||
useEffect(
|
||||
() {
|
||||
if (backupState.backupProgress != BackUpProgressEnum.inProgress) {
|
||||
if (backupState.backupProgress != BackUpProgressEnum.inProgress &&
|
||||
backupState.backupProgress != BackUpProgressEnum.manualInProgress) {
|
||||
ref.watch(backupProvider.notifier).getBackupInfo();
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/album/ui/add_to_album_sliverlist.dart';
|
||||
import 'package:immich_mobile/modules/home/ui/delete_dialog.dart';
|
||||
import 'package:immich_mobile/modules/home/ui/upload_dialog.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/ui/drag_sheet.dart';
|
||||
import 'package:immich_mobile/shared/models/album.dart';
|
||||
|
||||
@@ -15,10 +17,12 @@ class ControlBottomAppBar extends ConsumerWidget {
|
||||
final void Function() onDelete;
|
||||
final Function(Album album) onAddToAlbum;
|
||||
final void Function() onCreateNewAlbum;
|
||||
final void Function() onUpload;
|
||||
|
||||
final List<Album> albums;
|
||||
final List<Album> sharedAlbums;
|
||||
final bool enabled;
|
||||
final AssetState selectionAssetState;
|
||||
|
||||
const ControlBottomAppBar({
|
||||
Key? key,
|
||||
@@ -30,12 +34,15 @@ class ControlBottomAppBar extends ConsumerWidget {
|
||||
required this.albums,
|
||||
required this.onAddToAlbum,
|
||||
required this.onCreateNewAlbum,
|
||||
required this.onUpload,
|
||||
this.selectionAssetState = AssetState.remote,
|
||||
this.enabled = true,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
var isDarkMode = Theme.of(context).brightness == Brightness.dark;
|
||||
var hasRemote = selectionAssetState == AssetState.remote;
|
||||
|
||||
Widget renderActionButtons() {
|
||||
return Row(
|
||||
@@ -47,11 +54,12 @@ class ControlBottomAppBar extends ConsumerWidget {
|
||||
label: "control_bottom_app_bar_share".tr(),
|
||||
onPressed: enabled ? onShare : null,
|
||||
),
|
||||
ControlBoxButton(
|
||||
iconData: Icons.favorite_border_rounded,
|
||||
label: "control_bottom_app_bar_favorite".tr(),
|
||||
onPressed: enabled ? onFavorite : null,
|
||||
),
|
||||
if (hasRemote)
|
||||
ControlBoxButton(
|
||||
iconData: Icons.favorite_border_rounded,
|
||||
label: "control_bottom_app_bar_favorite".tr(),
|
||||
onPressed: enabled ? onFavorite : null,
|
||||
),
|
||||
ControlBoxButton(
|
||||
iconData: Icons.delete_outline_rounded,
|
||||
label: "control_bottom_app_bar_delete".tr(),
|
||||
@@ -66,19 +74,35 @@ class ControlBottomAppBar extends ConsumerWidget {
|
||||
)
|
||||
: null,
|
||||
),
|
||||
ControlBoxButton(
|
||||
iconData: Icons.archive,
|
||||
label: "control_bottom_app_bar_archive".tr(),
|
||||
onPressed: enabled ? onArchive : null,
|
||||
),
|
||||
if (!hasRemote)
|
||||
ControlBoxButton(
|
||||
iconData: Icons.backup_outlined,
|
||||
label: "Upload",
|
||||
onPressed: enabled
|
||||
? () => showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return UploadDialog(
|
||||
onUpload: onUpload,
|
||||
);
|
||||
},
|
||||
)
|
||||
: null,
|
||||
),
|
||||
if (hasRemote)
|
||||
ControlBoxButton(
|
||||
iconData: Icons.archive,
|
||||
label: "control_bottom_app_bar_archive".tr(),
|
||||
onPressed: enabled ? onArchive : null,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return DraggableScrollableSheet(
|
||||
initialChildSize: 0.30,
|
||||
minChildSize: 0.15,
|
||||
maxChildSize: 0.57,
|
||||
initialChildSize: hasRemote ? 0.30 : 0.18,
|
||||
minChildSize: 0.18,
|
||||
maxChildSize: hasRemote ? 0.57 : 0.18,
|
||||
snap: true,
|
||||
builder: (
|
||||
BuildContext context,
|
||||
@@ -105,29 +129,33 @@ class ControlBottomAppBar extends ConsumerWidget {
|
||||
const CustomDraggingHandle(),
|
||||
const SizedBox(height: 12),
|
||||
renderActionButtons(),
|
||||
const Divider(
|
||||
indent: 16,
|
||||
endIndent: 16,
|
||||
thickness: 1,
|
||||
),
|
||||
AddToAlbumTitleRow(
|
||||
onCreateNewAlbum: enabled ? onCreateNewAlbum : null,
|
||||
),
|
||||
if (hasRemote)
|
||||
const Divider(
|
||||
indent: 16,
|
||||
endIndent: 16,
|
||||
thickness: 1,
|
||||
),
|
||||
if (hasRemote)
|
||||
AddToAlbumTitleRow(
|
||||
onCreateNewAlbum: enabled ? onCreateNewAlbum : null,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
sliver: AddToAlbumSliverList(
|
||||
albums: albums,
|
||||
sharedAlbums: sharedAlbums,
|
||||
onAddToAlbum: onAddToAlbum,
|
||||
enabled: enabled,
|
||||
if (hasRemote)
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
sliver: AddToAlbumSliverList(
|
||||
albums: albums,
|
||||
sharedAlbums: sharedAlbums,
|
||||
onAddToAlbum: onAddToAlbum,
|
||||
enabled: enabled,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SliverToBoxAdapter(
|
||||
child: SizedBox(height: 200),
|
||||
)
|
||||
if (hasRemote)
|
||||
const SliverToBoxAdapter(
|
||||
child: SizedBox(height: 200),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/manual_upload.provider.dart';
|
||||
import 'package:immich_mobile/modules/home/ui/profile_drawer/profile_drawer_header.dart';
|
||||
import 'package:immich_mobile/modules/home/ui/profile_drawer/server_info_box.dart';
|
||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||
@@ -35,6 +36,7 @@ class ProfileDrawer extends HookConsumerWidget {
|
||||
onTap: () async {
|
||||
await ref.watch(authenticationProvider.notifier).logout();
|
||||
|
||||
ref.read(manualUploadProvider.notifier).cancelBackup();
|
||||
ref.watch(backupProvider.notifier).cancelBackup();
|
||||
ref.watch(assetProvider.notifier).clearAllAsset();
|
||||
ref.watch(websocketProvider.notifier).disconnect();
|
||||
|
||||
16
mobile/lib/modules/home/ui/upload_dialog.dart
Normal file
16
mobile/lib/modules/home/ui/upload_dialog.dart
Normal file
@@ -0,0 +1,16 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/shared/ui/confirm_dialog.dart';
|
||||
|
||||
class UploadDialog extends ConfirmDialog {
|
||||
final Function onUpload;
|
||||
|
||||
const UploadDialog({Key? key, required this.onUpload})
|
||||
: super(
|
||||
key: key,
|
||||
title: 'upload_dialog_title',
|
||||
content: 'upload_dialog_info',
|
||||
cancel: 'upload_dialog_cancel',
|
||||
ok: 'upload_dialog_ok',
|
||||
onOk: onUpload,
|
||||
);
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import 'package:immich_mobile/modules/album/providers/album.provider.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/album_detail.provider.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
|
||||
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/manual_upload.provider.dart';
|
||||
import 'package:immich_mobile/modules/home/providers/multiselect.provider.dart';
|
||||
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
|
||||
import 'package:immich_mobile/modules/home/ui/control_bottom_app_bar.dart';
|
||||
@@ -36,6 +37,7 @@ class HomePage extends HookConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final multiselectEnabled = ref.watch(multiselectProvider.notifier);
|
||||
final selectionEnabledHook = useState(false);
|
||||
final selectionAssetState = useState(AssetState.remote);
|
||||
|
||||
final selection = useState(<Asset>{});
|
||||
final albums = ref.watch(albumProvider).where((a) => a.isRemote).toList();
|
||||
@@ -80,6 +82,9 @@ class HomePage extends HookConsumerWidget {
|
||||
) {
|
||||
selectionEnabledHook.value = multiselect;
|
||||
selection.value = selectedAssets;
|
||||
selectionAssetState.value = selectedAssets.any((e) => e.isRemote)
|
||||
? AssetState.remote
|
||||
: AssetState.local;
|
||||
}
|
||||
|
||||
void onShareAssets() {
|
||||
@@ -172,6 +177,28 @@ class HomePage extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
void onUpload() async {
|
||||
processing.value = true;
|
||||
try {
|
||||
final Set<Asset> assets = selection.value;
|
||||
if (assets.length > 30) {
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: 'home_page_upload_err_limit'.tr(),
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
);
|
||||
} else {
|
||||
processing.value = false;
|
||||
selectionEnabledHook.value = false;
|
||||
await ref
|
||||
.read(manualUploadProvider.notifier)
|
||||
.uploadAssets(context, assets);
|
||||
}
|
||||
} finally {
|
||||
processing.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
void onAddToAlbum(Album album) async {
|
||||
processing.value = true;
|
||||
try {
|
||||
@@ -253,7 +280,7 @@ class HomePage extends HookConsumerWidget {
|
||||
} else {
|
||||
refreshCount.value++;
|
||||
// set counter back to 0 if user does not request refresh again
|
||||
Timer(const Duration(seconds: 2), () {
|
||||
Timer(const Duration(seconds: 4), () {
|
||||
refreshCount.value = 0;
|
||||
});
|
||||
}
|
||||
@@ -330,7 +357,9 @@ class HomePage extends HookConsumerWidget {
|
||||
albums: albums,
|
||||
sharedAlbums: sharedAlbums,
|
||||
onCreateNewAlbum: onCreateNewAlbum,
|
||||
onUpload: onUpload,
|
||||
enabled: !processing.value,
|
||||
selectionAssetState: selectionAssetState.value,
|
||||
),
|
||||
if (processing.value) const Center(child: ImmichLoadingIndicator())
|
||||
],
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/manual_upload.provider.dart';
|
||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||
@@ -79,6 +80,9 @@ class ChangePasswordForm extends HookConsumerWidget {
|
||||
.read(authenticationProvider.notifier)
|
||||
.logout();
|
||||
|
||||
ref
|
||||
.read(manualUploadProvider.notifier)
|
||||
.cancelBackup();
|
||||
ref.read(backupProvider.notifier).cancelBackup();
|
||||
ref.read(assetProvider.notifier).clearAllAsset();
|
||||
ref.read(websocketProvider.notifier).disconnect();
|
||||
|
||||
Reference in New Issue
Block a user