feat: network requirement option for upload (#20302)

* wifi toggle

* feat: network requirement option for upload

* chore: put back holding queue previous config numbers

* options

* backup option page

* pr feedback
This commit is contained in:
Alex 2025-07-30 11:43:20 -05:00 committed by GitHub
parent 47a025f39f
commit 10e9c278ee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 220 additions and 14 deletions

View File

@ -580,8 +580,10 @@
"backup_manual_in_progress": "Upload already in progress. Try after sometime",
"backup_manual_success": "Success",
"backup_manual_title": "Upload status",
"backup_options": "Backup Options",
"backup_options_page_title": "Backup options",
"backup_setting_subtitle": "Manage background and foreground upload settings",
"backup_settings_subtitle": "Manage upload settings",
"backward": "Backward",
"beta_sync": "Beta Sync Status",
"beta_sync_subtitle": "Manage the new sync system",
@ -1312,6 +1314,9 @@
"my_albums": "My albums",
"name": "Name",
"name_or_nickname": "Name or nickname",
"network_requirement_photos_upload": "Use cellular data to backup photos",
"network_requirement_videos_upload": "Use cellular data to backup videos",
"network_requirements_updated": "Network requirements changed, resetting backup queue",
"networking_settings": "Networking",
"networking_subtitle": "Manage the server endpoint settings",
"never": "Never",

View File

@ -71,7 +71,9 @@ enum StoreKey<T> {
photoManagerCustomFilter<bool>._(1000),
betaPromptShown<bool>._(1001),
betaTimeline<bool>._(1002),
enableBackup<bool>._(1003);
enableBackup<bool>._(1003),
useWifiForUploadVideos<bool>._(1004),
useWifiForUploadPhotos<bool>._(1005);
const StoreKey._(this.id);
final int id;

View File

@ -91,10 +91,9 @@ Future<void> initApp() async {
initializeTimeZones();
// Initialize the file downloader
await FileDownloader().configure(
// maxConcurrent: 6, maxConcurrentByHost(server):6, maxConcurrentByGroup: 3
globalConfig: (Config.holdingQueue, (1000, 1000, 1000)),
globalConfig: (Config.holdingQueue, (6, 6, 3)),
);
await FileDownloader().trackTasksInGroup(kDownloadGroupLivePhoto, markDownloadedComplete: false);

View File

@ -65,6 +65,15 @@ class _DriftBackupPageState extends ConsumerState<DriftBackupPage> {
splashRadius: 24,
icon: const Icon(Icons.arrow_back_ios_rounded),
),
actions: [
IconButton(
onPressed: () {
context.pushRoute(const DriftBackupOptionsRoute());
},
icon: const Icon(Icons.settings_outlined),
tooltip: "backup_options".t(context: context),
),
],
),
body: Stack(
children: [

View File

@ -0,0 +1,68 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.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/providers/user.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/widgets/settings/backup_settings/drift_backup_settings.dart';
@RoutePage()
class DriftBackupOptionsPage extends ConsumerWidget {
const DriftBackupOptionsPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
bool hasPopped = false;
final previousWifiReqForVideos = Store.tryGet(StoreKey.useWifiForUploadVideos) ?? false;
final previousWifiReqForPhotos = Store.tryGet(StoreKey.useWifiForUploadPhotos) ?? false;
return PopScope(
onPopInvokedWithResult: (didPop, result) async {
// There is an issue with Flutter where the pop event
// can be triggered multiple times, so we guard it with _hasPopped
final currentWifiReqForVideos = Store.tryGet(StoreKey.useWifiForUploadVideos) ?? false;
final currentWifiReqForPhotos = Store.tryGet(StoreKey.useWifiForUploadPhotos) ?? false;
if (currentWifiReqForVideos == previousWifiReqForVideos &&
currentWifiReqForPhotos == previousWifiReqForPhotos) {
return;
}
if (didPop && !hasPopped) {
hasPopped = true;
final currentUser = ref.read(currentUserProvider);
if (currentUser == null) {
return;
}
await ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id);
final isBackupEnabled = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup);
if (!isBackupEnabled) {
return;
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text("network_requirements_updated".t(context: context)),
duration: const Duration(seconds: 4),
),
);
final backupNotifier = ref.read(driftBackupProvider.notifier);
backupNotifier.cancel().then((_) {
backupNotifier.startBackup(currentUser.id);
});
}
},
child: Scaffold(
appBar: AppBar(title: Text("backup_options".t(context: context))),
body: const DriftBackupSettings(),
),
);
}
}

View File

@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/routing/router.dart';
@ -9,6 +10,7 @@ import 'package:immich_mobile/widgets/settings/advanced_settings.dart';
import 'package:immich_mobile/widgets/settings/asset_list_settings/asset_list_settings.dart';
import 'package:immich_mobile/widgets/settings/asset_viewer_settings/asset_viewer_settings.dart';
import 'package:immich_mobile/widgets/settings/backup_settings/backup_settings.dart';
import 'package:immich_mobile/widgets/settings/backup_settings/drift_backup_settings.dart';
import 'package:immich_mobile/widgets/settings/beta_sync_settings/beta_sync_settings.dart';
import 'package:immich_mobile/widgets/settings/beta_timeline_list_tile.dart';
import 'package:immich_mobile/widgets/settings/language_settings.dart';
@ -21,7 +23,7 @@ enum SettingSection {
beta('beta_sync', Icons.sync_outlined, "beta_sync_subtitle"),
advanced('advanced', Icons.build_outlined, "advanced_settings_tile_subtitle"),
assetViewer('asset_viewer_settings_title', Icons.image_outlined, "asset_viewer_settings_subtitle"),
backup('backup', Icons.cloud_upload_outlined, "backup_setting_subtitle"),
backup('backup', Icons.cloud_upload_outlined, "backup_settings_subtitle"),
languages('language', Icons.language, "setting_languages_subtitle"),
networking('networking_settings', Icons.wifi, "networking_subtitle"),
notifications('notifications', Icons.notifications_none_rounded, "setting_notifications_subtitle"),
@ -36,7 +38,8 @@ enum SettingSection {
SettingSection.beta => const _BetaLandscapeToggle(),
SettingSection.advanced => const AdvancedSettings(),
SettingSection.assetViewer => const AssetViewerSettings(),
SettingSection.backup => const BackupSettings(),
SettingSection.backup =>
Store.tryGet(StoreKey.betaTimeline) ?? false ? const DriftBackupSettings() : const BackupSettings(),
SettingSection.languages => const LanguageSettings(),
SettingSection.networking => const NetworkingSettings(),
SettingSection.notifications => const NotificationSetting(),

View File

@ -187,12 +187,12 @@ class DriftBackupState {
}
}
final driftBackupProvider = StateNotifierProvider<ExpBackupNotifier, DriftBackupState>((ref) {
return ExpBackupNotifier(ref.watch(uploadServiceProvider));
final driftBackupProvider = StateNotifierProvider<DriftBackupNotifier, DriftBackupState>((ref) {
return DriftBackupNotifier(ref.watch(uploadServiceProvider));
});
class ExpBackupNotifier extends StateNotifier<DriftBackupState> {
ExpBackupNotifier(this._uploadService)
class DriftBackupNotifier extends StateNotifier<DriftBackupState> {
DriftBackupNotifier(this._uploadService)
: super(
const DriftBackupState(
totalCount: 0,

View File

@ -28,6 +28,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_controller.page.dart';
import 'package:immich_mobile/pages/backup/backup_options.page.dart';
import 'package:immich_mobile/pages/backup/drift_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/common/activities.page.dart';
@ -322,13 +323,12 @@ class AppRouter extends RootStackRouter {
AutoRoute(page: DriftPlaceDetailRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: DriftUserSelectionRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: ChangeExperienceRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: DriftPartnerRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: DriftUploadDetailRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: BetaSyncSettingsRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: DriftPeopleCollectionRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: DriftPersonRoute.page, guards: [_authGuard]),
AutoRoute(page: DriftBackupOptionsRoute.page, guards: [_authGuard, _duplicateGuard]),
// required to handle all deeplinks in deep_link.service.dart
// auto_route_library#1722
RedirectRoute(path: '*', redirectTo: '/'),

View File

@ -764,6 +764,22 @@ class DriftBackupAlbumSelectionRoute extends PageRouteInfo<void> {
);
}
/// generated route for
/// [DriftBackupOptionsPage]
class DriftBackupOptionsRoute extends PageRouteInfo<void> {
const DriftBackupOptionsRoute({List<PageRouteInfo>? children})
: super(DriftBackupOptionsRoute.name, initialChildren: children);
static const String name = 'DriftBackupOptionsRoute';
static PageInfo page = PageInfo(
name,
builder: (data) {
return const DriftBackupOptionsPage();
},
);
}
/// generated route for
/// [DriftBackupPage]
class DriftBackupRoute extends PageRouteInfo<void> {

View File

@ -47,7 +47,9 @@ enum AppSettingsEnum<T> {
autoEndpointSwitching<bool>(StoreKey.autoEndpointSwitching, null, false),
photoManagerCustomFilter<bool>(StoreKey.photoManagerCustomFilter, null, true),
betaTimeline<bool>(StoreKey.betaTimeline, null, false),
enableBackup<bool>(StoreKey.enableBackup, null, false);
enableBackup<bool>(StoreKey.enableBackup, null, false),
useCellularForUploadVideos<bool>(StoreKey.useWifiForUploadVideos, null, false),
useCellularForUploadPhotos<bool>(StoreKey.useWifiForUploadPhotos, null, false);
const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue);

View File

@ -12,11 +12,13 @@ import 'package:immich_mobile/entities/store.entity.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/storage.repository.dart';
import 'package:immich_mobile/providers/app_settings.provider.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/storage.provider.dart';
import 'package:immich_mobile/repositories/upload.repository.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:path/path.dart' as p;
final uploadServiceProvider = Provider((ref) {
@ -25,6 +27,7 @@ final uploadServiceProvider = Provider((ref) {
ref.watch(backupRepositoryProvider),
ref.watch(storageRepositoryProvider),
ref.watch(localAssetRepository),
ref.watch(appSettingsServiceProvider),
);
ref.onDispose(service.dispose);
@ -32,7 +35,13 @@ final uploadServiceProvider = Provider((ref) {
});
class UploadService {
UploadService(this._uploadRepository, this._backupRepository, this._storageRepository, this._localAssetRepository) {
UploadService(
this._uploadRepository,
this._backupRepository,
this._storageRepository,
this._localAssetRepository,
this._appSettingsService,
) {
_uploadRepository.onUploadStatus = _onUploadCallback;
_uploadRepository.onTaskProgress = _onTaskProgressCallback;
}
@ -41,6 +50,7 @@ class UploadService {
final DriftBackupRepository _backupRepository;
final StorageRepository _storageRepository;
final DriftLocalAssetRepository _localAssetRepository;
final AppSettingsService _appSettingsService;
final StreamController<TaskStatusUpdate> _taskStatusController = StreamController<TaskStatusUpdate>.broadcast();
final StreamController<TaskProgressUpdate> _taskProgressController = StreamController<TaskProgressUpdate>.broadcast();
@ -240,6 +250,14 @@ class UploadService {
livePhotoVideoId: '',
).toJson();
bool requiresWiFi = true;
if (asset.isVideo && _appSettingsService.getSetting(AppSettingsEnum.useCellularForUploadVideos)) {
requiresWiFi = false;
} else if (!asset.isVideo && _appSettingsService.getSetting(AppSettingsEnum.useCellularForUploadPhotos)) {
requiresWiFi = false;
}
return buildUploadTask(
file,
originalFileName: originalFileName,
@ -248,6 +266,7 @@ class UploadService {
group: group,
priority: priority,
isFavorite: asset.isFavorite,
requiresWiFi: requiresWiFi,
);
}
@ -284,12 +303,12 @@ class UploadService {
String? metadata,
int? priority,
bool? isFavorite,
bool requiresWiFi = true,
}) async {
final serverEndpoint = Store.get(StoreKey.serverEndpoint);
final url = Uri.parse('$serverEndpoint/assets').toString();
final headers = ApiService.getRequestHeaders();
final deviceId = Store.get(StoreKey.deviceId);
final (baseDirectory, directory, filename) = await Task.split(filePath: file.path);
final stats = await file.stat();
final fileCreatedAt = stats.changed;
@ -318,6 +337,7 @@ class UploadService {
fileField: 'assetData',
metaData: metadata ?? '',
group: group,
requiresWiFi: requiresWiFi,
priority: priority ?? 5,
updates: Updates.statusAndProgress,
retries: 3,

View File

@ -0,0 +1,82 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart';
class DriftBackupSettings extends StatelessWidget {
const DriftBackupSettings({super.key});
@override
Widget build(BuildContext context) {
return const SettingsSubPageScaffold(settings: [_UseWifiForUploadVideosButton(), _UseWifiForUploadPhotosButton()]);
}
}
class _UseWifiForUploadVideosButton extends ConsumerWidget {
const _UseWifiForUploadVideosButton();
@override
Widget build(BuildContext context, WidgetRef ref) {
final valueStream = Store.watch(StoreKey.useWifiForUploadVideos);
return ListTile(
title: Text(
"videos".t(context: context),
style: context.textTheme.titleMedium?.copyWith(color: context.primaryColor),
),
subtitle: Text("network_requirement_videos_upload".t(context: context), style: context.textTheme.labelLarge),
trailing: StreamBuilder(
stream: valueStream,
initialData: Store.tryGet(StoreKey.useWifiForUploadVideos) ?? false,
builder: (context, snapshot) {
final value = snapshot.data ?? false;
return Switch(
value: value,
onChanged: (bool newValue) async {
await ref
.read(appSettingsServiceProvider)
.setSetting(AppSettingsEnum.useCellularForUploadVideos, newValue);
},
);
},
),
);
}
}
class _UseWifiForUploadPhotosButton extends ConsumerWidget {
const _UseWifiForUploadPhotosButton();
@override
Widget build(BuildContext context, WidgetRef ref) {
final valueStream = Store.watch(StoreKey.useWifiForUploadPhotos);
return ListTile(
title: Text(
"photos".t(context: context),
style: context.textTheme.titleMedium?.copyWith(color: context.primaryColor),
),
subtitle: Text("network_requirement_photos_upload".t(context: context), style: context.textTheme.labelLarge),
trailing: StreamBuilder(
stream: valueStream,
initialData: Store.tryGet(StoreKey.useWifiForUploadPhotos) ?? false,
builder: (context, snapshot) {
final value = snapshot.data ?? false;
return Switch(
value: value,
onChanged: (bool newValue) async {
await ref
.read(appSettingsServiceProvider)
.setSetting(AppSettingsEnum.useCellularForUploadPhotos, newValue);
},
);
},
),
);
}
}