mirror of
https://github.com/immich-app/immich.git
synced 2026-05-21 23:26:31 -04:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f109e89d15 | |||
| 8ec828777c | |||
| e3736de0c1 | |||
| 0154b26870 | |||
| 915d865ce2 | |||
| c28e5f90b6 | |||
| 4383473ed6 | |||
| 77701dd5a3 |
@@ -159,14 +159,14 @@ jobs:
|
||||
|
||||
- name: Comment APK download link on PR
|
||||
if: ${{ github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork }}
|
||||
uses: mshick/add-pr-comment@8e4927817251f1ff60c001f04568532b38e0b4a0 # v3.11.0
|
||||
uses: immich-app/devtools/actions/sticky-comment@0135acd12ad9f3369b94a2aa3c0ae8c835a4e926 # sticky-comment-action-v1.0.0
|
||||
env:
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
APK_URL: ${{ steps.upload-apk.outputs.artifact-url }}
|
||||
with:
|
||||
github-token: ${{ steps.token.outputs.token }}
|
||||
message-id: 'mobile-android-apk'
|
||||
message: |
|
||||
id: mobile-android-apk
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
body: |
|
||||
📱 **Android release APK (universal)** — `${{ env.HEAD_SHA }}`
|
||||
|
||||
Download: ${{ env.APK_URL }}
|
||||
|
||||
@@ -213,12 +213,11 @@ jobs:
|
||||
run: 'mise run //deployment:tf apply'
|
||||
|
||||
- name: Comment
|
||||
uses: actions-cool/maintain-one-comment@909842216bc8e8658364c572ec52100f4c2cc50a # v3.3.0
|
||||
uses: immich-app/devtools/actions/sticky-comment@0135acd12ad9f3369b94a2aa3c0ae8c835a4e926 # sticky-comment-action-v1.0.0
|
||||
if: ${{ steps.parameters.outputs.event == 'pr' }}
|
||||
with:
|
||||
id: docs-pr-url
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
number: ${{ fromJson(needs.checks.outputs.parameters).pr_number }}
|
||||
body: |
|
||||
📖 Documentation deployed to [${{ steps.docs-output.outputs.subdomain }}](https://${{ steps.docs-output.outputs.subdomain }})
|
||||
emojis: 'rocket'
|
||||
body-include: '<!-- Docs PR URL -->'
|
||||
|
||||
@@ -44,9 +44,8 @@ jobs:
|
||||
run: 'mise run //deployment:tf destroy -- -refresh=false'
|
||||
|
||||
- name: Comment
|
||||
uses: actions-cool/maintain-one-comment@909842216bc8e8658364c572ec52100f4c2cc50a # v3.3.0
|
||||
uses: immich-app/devtools/actions/sticky-comment@0135acd12ad9f3369b94a2aa3c0ae8c835a4e926 # sticky-comment-action-v1.0.0
|
||||
with:
|
||||
id: docs-pr-url
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
number: ${{ github.event.number }}
|
||||
delete: true
|
||||
body-include: '<!-- Docs PR URL -->'
|
||||
|
||||
@@ -13,3 +13,4 @@ jobs:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
secrets: inherit
|
||||
|
||||
@@ -19,11 +19,11 @@ jobs:
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- uses: mshick/add-pr-comment@8e4927817251f1ff60c001f04568532b38e0b4a0 # v3.11.0
|
||||
- uses: immich-app/devtools/actions/sticky-comment@0135acd12ad9f3369b94a2aa3c0ae8c835a4e926 # sticky-comment-action-v1.0.0
|
||||
with:
|
||||
github-token: ${{ steps.token.outputs.token }}
|
||||
message-id: 'preview-status'
|
||||
message: 'Deploying preview environment to https://pr-${{ github.event.pull_request.number }}.preview.internal.immich.build/'
|
||||
id: preview-status
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
body: 'Deploying preview environment to https://pr-${{ github.event.pull_request.number }}.preview.internal.immich.build/'
|
||||
|
||||
remove-label:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -48,16 +48,16 @@ jobs:
|
||||
name: 'preview'
|
||||
})
|
||||
|
||||
- uses: mshick/add-pr-comment@8e4927817251f1ff60c001f04568532b38e0b4a0 # v3.11.0
|
||||
- uses: immich-app/devtools/actions/sticky-comment@0135acd12ad9f3369b94a2aa3c0ae8c835a4e926 # sticky-comment-action-v1.0.0
|
||||
if: ${{ github.event.pull_request.head.repo.fork }}
|
||||
with:
|
||||
github-token: ${{ steps.token.outputs.token }}
|
||||
message-id: 'preview-status'
|
||||
message: 'PRs from forks cannot have preview environments.'
|
||||
id: preview-status
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
body: 'PRs from forks cannot have preview environments.'
|
||||
|
||||
- uses: mshick/add-pr-comment@8e4927817251f1ff60c001f04568532b38e0b4a0 # v3.11.0
|
||||
- uses: immich-app/devtools/actions/sticky-comment@0135acd12ad9f3369b94a2aa3c0ae8c835a4e926 # sticky-comment-action-v1.0.0
|
||||
if: ${{ !github.event.pull_request.head.repo.fork }}
|
||||
with:
|
||||
github-token: ${{ steps.token.outputs.token }}
|
||||
message-id: 'preview-status'
|
||||
message: 'Preview environment has been removed.'
|
||||
id: preview-status
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
body: 'Preview environment has been removed.'
|
||||
|
||||
@@ -89,6 +89,13 @@ flutter {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
constraints {
|
||||
implementation("androidx.glance:glance-appwidget") {
|
||||
version { strictly libs.versions.glance.get() }
|
||||
because 'home_widget requests 1.+ which can resolve to pre-releases incompatible with our compileSdk/AGP'
|
||||
}
|
||||
}
|
||||
|
||||
implementation libs.okhttp
|
||||
implementation libs.cronet.embedded
|
||||
implementation libs.media3.datasource.okhttp
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:immich_mobile/domain/models/config/album_config.dart';
|
||||
import 'package:immich_mobile/domain/models/config/backup_config.dart';
|
||||
import 'package:immich_mobile/domain/models/config/cleanup_config.dart';
|
||||
import 'package:immich_mobile/domain/models/config/image_config.dart';
|
||||
import 'package:immich_mobile/domain/models/config/map_config.dart';
|
||||
@@ -16,6 +17,7 @@ class AppConfig {
|
||||
final ViewerConfig viewer;
|
||||
final SlideshowConfig slideshow;
|
||||
final AlbumConfig album;
|
||||
final BackupConfig backup;
|
||||
|
||||
const AppConfig({
|
||||
this.theme = const .new(),
|
||||
@@ -26,6 +28,7 @@ class AppConfig {
|
||||
this.viewer = const .new(),
|
||||
this.slideshow = const .new(),
|
||||
this.album = const .new(),
|
||||
this.backup = const .new(),
|
||||
});
|
||||
|
||||
AppConfig copyWith({
|
||||
@@ -37,6 +40,7 @@ class AppConfig {
|
||||
ViewerConfig? viewer,
|
||||
SlideshowConfig? slideshow,
|
||||
AlbumConfig? album,
|
||||
BackupConfig? backup,
|
||||
}) => .new(
|
||||
theme: theme ?? this.theme,
|
||||
cleanup: cleanup ?? this.cleanup,
|
||||
@@ -46,6 +50,7 @@ class AppConfig {
|
||||
viewer: viewer ?? this.viewer,
|
||||
slideshow: slideshow ?? this.slideshow,
|
||||
album: album ?? this.album,
|
||||
backup: backup ?? this.backup,
|
||||
);
|
||||
|
||||
@override
|
||||
@@ -59,12 +64,13 @@ class AppConfig {
|
||||
other.image == image &&
|
||||
other.viewer == viewer &&
|
||||
other.slideshow == slideshow &&
|
||||
other.album == album);
|
||||
other.album == album &&
|
||||
other.backup == backup);
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(theme, cleanup, map, timeline, image, viewer, slideshow, album);
|
||||
int get hashCode => Object.hash(theme, cleanup, map, timeline, image, viewer, slideshow, album, backup);
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'AppConfig(theme: $theme, cleanup: $cleanup, map: $map, timeline: $timeline, image: $image, viewer: $viewer, slideshow: $slideshow, album: $album)';
|
||||
'AppConfig(theme: $theme, cleanup: $cleanup, map: $map, timeline: $timeline, image: $image, viewer: $viewer, slideshow: $slideshow, album: $album, backup: $backup)';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
class BackupConfig {
|
||||
final bool enabled;
|
||||
final bool useCellularForVideos;
|
||||
final bool useCellularForPhotos;
|
||||
final bool requireCharging;
|
||||
final int triggerDelay;
|
||||
final bool syncAlbums;
|
||||
|
||||
const BackupConfig({
|
||||
this.enabled = false,
|
||||
this.useCellularForVideos = false,
|
||||
this.useCellularForPhotos = false,
|
||||
this.requireCharging = false,
|
||||
this.triggerDelay = 30,
|
||||
this.syncAlbums = false,
|
||||
});
|
||||
|
||||
BackupConfig copyWith({
|
||||
bool? enabled,
|
||||
bool? useCellularForVideos,
|
||||
bool? useCellularForPhotos,
|
||||
bool? requireCharging,
|
||||
int? triggerDelay,
|
||||
bool? syncAlbums,
|
||||
}) => BackupConfig(
|
||||
enabled: enabled ?? this.enabled,
|
||||
useCellularForVideos: useCellularForVideos ?? this.useCellularForVideos,
|
||||
useCellularForPhotos: useCellularForPhotos ?? this.useCellularForPhotos,
|
||||
requireCharging: requireCharging ?? this.requireCharging,
|
||||
triggerDelay: triggerDelay ?? this.triggerDelay,
|
||||
syncAlbums: syncAlbums ?? this.syncAlbums,
|
||||
);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
(other is BackupConfig &&
|
||||
other.enabled == enabled &&
|
||||
other.useCellularForVideos == useCellularForVideos &&
|
||||
other.useCellularForPhotos == useCellularForPhotos &&
|
||||
other.requireCharging == requireCharging &&
|
||||
other.triggerDelay == triggerDelay &&
|
||||
other.syncAlbums == syncAlbums);
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
Object.hash(enabled, useCellularForVideos, useCellularForPhotos, requireCharging, triggerDelay, syncAlbums);
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'BackupConfig(enabled: $enabled, useCellularForVideos: $useCellularForVideos, useCellularForPhotos: $useCellularForPhotos, requireCharging: $requireCharging, triggerDelay: $triggerDelay, syncAlbums: $syncAlbums)';
|
||||
}
|
||||
@@ -62,6 +62,14 @@ enum MetadataKey<T extends Object> {
|
||||
albumIsReverse<bool>(.appConfig, 'album.isReverse', true),
|
||||
albumIsGrid<bool>(.appConfig, 'album.isGrid', false),
|
||||
|
||||
// Backup
|
||||
backupEnabled<bool>(.appConfig, 'backup.enabled', false),
|
||||
backupUseCellularForVideos<bool>(.appConfig, 'backup.useCellularForVideos', false),
|
||||
backupUseCellularForPhotos<bool>(.appConfig, 'backup.useCellularForPhotos', false),
|
||||
backupRequireCharging<bool>(.appConfig, 'backup.requireCharging', false),
|
||||
backupTriggerDelay<int>(.appConfig, 'backup.triggerDelay', 30),
|
||||
backupSyncAlbums<bool>(.appConfig, 'backup.syncAlbums', false),
|
||||
|
||||
// Timeline
|
||||
timelineTilesPerRow<int>(.appConfig, 'timeline.tilesPerRow', 4),
|
||||
timelineGroupAssetsBy<GroupAssetsBy>(
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
|
||||
enum Setting<T> {
|
||||
advancedTroubleshooting<bool>(StoreKey.advancedTroubleshooting, false),
|
||||
enableBackup<bool>(StoreKey.enableBackup, false);
|
||||
advancedTroubleshooting<bool>(StoreKey.advancedTroubleshooting, false);
|
||||
|
||||
const Setting(this.storeKey, this.defaultValue);
|
||||
|
||||
|
||||
@@ -6,26 +6,25 @@ enum StoreKey<T> {
|
||||
version<int>._(0),
|
||||
currentUser<UserDto>._(2),
|
||||
deviceId<String>._(4),
|
||||
backupRequireCharging<bool>._(7),
|
||||
backupTriggerDelay<int>._(8),
|
||||
serverUrl<String>._(10),
|
||||
accessToken<String>._(11),
|
||||
serverEndpoint<String>._(12),
|
||||
advancedTroubleshooting<bool>._(114),
|
||||
enableHapticFeedback<bool>._(126),
|
||||
syncAlbums<bool>._(131),
|
||||
|
||||
manageLocalMediaAndroid<bool>._(137),
|
||||
// Read-only Mode settings
|
||||
readonlyModeEnabled<bool>._(138),
|
||||
|
||||
// Experimental stuff
|
||||
enableBackup<bool>._(1003),
|
||||
useWifiForUploadVideos<bool>._(1004),
|
||||
useWifiForUploadPhotos<bool>._(1005),
|
||||
syncMigrationStatus<String>._(1013),
|
||||
|
||||
// Legacy keys that have been migrated to the new metadata store
|
||||
legacyBackupRequireCharging<bool>._(7),
|
||||
legacyBackupTriggerDelay<int>._(8),
|
||||
legacySyncAlbums<bool>._(131),
|
||||
legacyEnableBackup<bool>._(1003),
|
||||
legacyUseWifiForUploadVideos<bool>._(1004),
|
||||
legacyUseWifiForUploadPhotos<bool>._(1005),
|
||||
legacySelectedAlbumSortOrder<int>._(113),
|
||||
legacySelectedAlbumSortReverse<bool>._(123),
|
||||
legacyAlbumGridView<bool>._(140),
|
||||
|
||||
@@ -11,15 +11,14 @@ import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
|
||||
import 'package:immich_mobile/platform/background_worker_api.g.dart';
|
||||
import 'package:immich_mobile/platform/background_worker_lock_api.g.dart';
|
||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart' show nativeSyncApiProvider;
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/services/auth.service.dart';
|
||||
import 'package:immich_mobile/services/foreground_upload.service.dart';
|
||||
import 'package:immich_mobile/services/localization.service.dart';
|
||||
@@ -39,16 +38,15 @@ class BackgroundWorkerFgService {
|
||||
Future<void> saveNotificationMessage(String title, String body) =>
|
||||
_foregroundHostApi.saveNotificationMessage(title, body);
|
||||
|
||||
Future<void> configure({int? minimumDelaySeconds, bool? requireCharging}) => _foregroundHostApi.configure(
|
||||
BackgroundWorkerSettings(
|
||||
minimumDelaySeconds:
|
||||
minimumDelaySeconds ??
|
||||
Store.get(AppSettingsEnum.backupTriggerDelay.storeKey, AppSettingsEnum.backupTriggerDelay.defaultValue),
|
||||
requiresCharging:
|
||||
requireCharging ??
|
||||
Store.get(AppSettingsEnum.backupRequireCharging.storeKey, AppSettingsEnum.backupRequireCharging.defaultValue),
|
||||
),
|
||||
);
|
||||
Future<void> configure({int? minimumDelaySeconds, bool? requireCharging}) {
|
||||
final backup = MetadataRepository.instance.appConfig.backup;
|
||||
return _foregroundHostApi.configure(
|
||||
BackgroundWorkerSettings(
|
||||
minimumDelaySeconds: minimumDelaySeconds ?? backup.triggerDelay,
|
||||
requiresCharging: requireCharging ?? backup.requireCharging,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> disable() => _foregroundHostApi.disable();
|
||||
}
|
||||
@@ -71,7 +69,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||
BackgroundWorkerFlutterApi.setUp(this);
|
||||
}
|
||||
|
||||
bool get _isBackupEnabled => _ref?.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup) ?? false;
|
||||
bool get _isBackupEnabled => MetadataRepository.instance.appConfig.backup.enabled;
|
||||
|
||||
Future<void> init() async {
|
||||
try {
|
||||
|
||||
@@ -192,13 +192,20 @@ class RemoteAlbumService {
|
||||
required UserDto uploader,
|
||||
required AlbumAssetCandidates candidates,
|
||||
UploadCallbacks uploadCallbacks = const UploadCallbacks(),
|
||||
Completer<void>? cancelToken,
|
||||
}) async {
|
||||
int addedCount = 0;
|
||||
if (candidates.remoteAssetIds.isNotEmpty) {
|
||||
addedCount += await addAssets(albumId: albumId, assetIds: candidates.remoteAssetIds);
|
||||
}
|
||||
if (candidates.localAssetsToUpload.isNotEmpty) {
|
||||
addedCount += await _uploadAndAddLocals(albumId, uploader, candidates.localAssetsToUpload, uploadCallbacks);
|
||||
addedCount += await _uploadAndAddLocals(
|
||||
albumId,
|
||||
uploader,
|
||||
candidates.localAssetsToUpload,
|
||||
uploadCallbacks,
|
||||
cancelToken,
|
||||
);
|
||||
}
|
||||
return addedCount;
|
||||
}
|
||||
@@ -228,8 +235,9 @@ class RemoteAlbumService {
|
||||
String albumId,
|
||||
UserDto uploader,
|
||||
List<LocalAsset> localAssets,
|
||||
UploadCallbacks userCallbacks,
|
||||
) async {
|
||||
UploadCallbacks userCallbacks, [
|
||||
Completer<void>? cancelToken,
|
||||
]) async {
|
||||
int addedCount = 0;
|
||||
final pendingAdds = <Future<void>>[];
|
||||
final localById = {for (final a in localAssets) a.id: a};
|
||||
@@ -258,7 +266,7 @@ class RemoteAlbumService {
|
||||
return;
|
||||
}
|
||||
pendingAdds.add(
|
||||
_linkUploadedAssetToAlbum(albumId, remoteId, uploader, source)
|
||||
linkUploadedAssetToAlbum(albumId, remoteId, uploader, source)
|
||||
.then<void>((added) {
|
||||
addedCount += added;
|
||||
})
|
||||
@@ -269,7 +277,7 @@ class RemoteAlbumService {
|
||||
},
|
||||
);
|
||||
|
||||
await _uploadService.uploadManual(localAssets, callbacks: wrappedCallbacks);
|
||||
await _uploadService.uploadManual(localAssets, callbacks: wrappedCallbacks, cancelToken: cancelToken);
|
||||
await Future.wait(pendingAdds);
|
||||
return addedCount;
|
||||
}
|
||||
@@ -288,7 +296,7 @@ class RemoteAlbumService {
|
||||
/// `remote_asset_entity` row from the local source so the FK-protected
|
||||
/// junction insert succeeds. Sync overwrites the placeholder later with
|
||||
/// the authoritative server data.
|
||||
Future<int> _linkUploadedAssetToAlbum(String albumId, String remoteId, UserDto uploader, LocalAsset source) async {
|
||||
Future<int> linkUploadedAssetToAlbum(String albumId, String remoteId, UserDto uploader, LocalAsset source) async {
|
||||
final result = await _albumApiRepository.addAssets(albumId, [remoteId]);
|
||||
if (result.added.isEmpty) {
|
||||
return 0;
|
||||
|
||||
@@ -152,6 +152,14 @@ extension<T extends Object> on MetadataDomain<T> {
|
||||
isReverse: repo._read(.albumIsReverse),
|
||||
isGrid: repo._read(.albumIsGrid),
|
||||
),
|
||||
backup: .new(
|
||||
enabled: repo._read(.backupEnabled),
|
||||
useCellularForVideos: repo._read(.backupUseCellularForVideos),
|
||||
useCellularForPhotos: repo._read(.backupUseCellularForPhotos),
|
||||
requireCharging: repo._read(.backupRequireCharging),
|
||||
triggerDelay: repo._read(.backupTriggerDelay),
|
||||
syncAlbums: repo._read(.backupSyncAlbums),
|
||||
),
|
||||
);
|
||||
case .systemConfig:
|
||||
repo._systemConfig = .new(
|
||||
|
||||
@@ -8,13 +8,13 @@ import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||
import 'package:immich_mobile/domain/services/sync_linked_album.service.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/infrastructure/repositories/metadata.repository.dart';
|
||||
import 'package:immich_mobile/providers/background_sync.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/infrastructure/metadata.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/widgets/backup/drift_album_info_list_tile.dart';
|
||||
import 'package:immich_mobile/widgets/common/search_field.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
@@ -43,7 +43,7 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState<DriftBackupAlbum
|
||||
_searchController = TextEditingController();
|
||||
_searchFocusNode = FocusNode();
|
||||
|
||||
_enableSyncUploadAlbum.value = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.syncAlbums);
|
||||
_enableSyncUploadAlbum.value = ref.read(metadataProvider).appConfig.backup.syncAlbums;
|
||||
ref.read(backupAlbumProvider.notifier).getAll();
|
||||
|
||||
_initialTotalAssetCount = ref.read(driftBackupProvider.select((p) => p.totalCount));
|
||||
@@ -55,7 +55,7 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState<DriftBackupAlbum
|
||||
return;
|
||||
}
|
||||
|
||||
final enableSyncUploadAlbum = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.syncAlbums);
|
||||
final enableSyncUploadAlbum = ref.read(metadataProvider).appConfig.backup.syncAlbums;
|
||||
final selectedAlbums = ref
|
||||
.read(backupAlbumProvider)
|
||||
.where((a) => a.backupSelection == BackupSelection.selected)
|
||||
@@ -103,7 +103,7 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState<DriftBackupAlbum
|
||||
return;
|
||||
}
|
||||
|
||||
final isBackupEnabled = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup);
|
||||
final isBackupEnabled = MetadataRepository.instance.appConfig.backup.enabled;
|
||||
await ref.read(driftBackupProvider.notifier).getBackupStatus(user.id);
|
||||
final currentTotalAssetCount = ref.read(driftBackupProvider.select((p) => p.totalCount));
|
||||
final totalChanged = currentTotalAssetCount != _initialTotalAssetCount;
|
||||
|
||||
@@ -3,14 +3,12 @@ import 'dart:async';
|
||||
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/infrastructure/repositories/metadata.repository.dart';
|
||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/metadata.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';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
@@ -21,18 +19,20 @@ class DriftBackupOptionsPage extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
bool hasPopped = false;
|
||||
final previousWifiReqForVideos = Store.tryGet(StoreKey.useWifiForUploadVideos) ?? false;
|
||||
final previousWifiReqForPhotos = Store.tryGet(StoreKey.useWifiForUploadPhotos) ?? false;
|
||||
final previousBackup = ref.read(metadataProvider).appConfig.backup;
|
||||
final previousCellularForVideos = previousBackup.useCellularForVideos;
|
||||
final previousCellularForPhotos = previousBackup.useCellularForPhotos;
|
||||
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;
|
||||
final currentBackup = ref.read(metadataProvider).appConfig.backup;
|
||||
final currentCellularForVideos = currentBackup.useCellularForVideos;
|
||||
final currentCellularForPhotos = currentBackup.useCellularForPhotos;
|
||||
|
||||
if (currentWifiReqForVideos == previousWifiReqForVideos &&
|
||||
currentWifiReqForPhotos == previousWifiReqForPhotos) {
|
||||
if (currentCellularForVideos == previousCellularForVideos &&
|
||||
currentCellularForPhotos == previousCellularForPhotos) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ class DriftBackupOptionsPage extends ConsumerWidget {
|
||||
}
|
||||
|
||||
await ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id);
|
||||
final isBackupEnabled = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup);
|
||||
final isBackupEnabled = MetadataRepository.instance.appConfig.backup.enabled;
|
||||
if (!isBackupEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/generated/codegen_loader.g.dart';
|
||||
import 'package:immich_mobile/generated/translations.g.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
|
||||
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
||||
@@ -340,7 +341,7 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
|
||||
await backgroundManager.hashAssets();
|
||||
}
|
||||
|
||||
if (Store.get(StoreKey.syncAlbums, false)) {
|
||||
if (MetadataRepository.instance.appConfig.backup.syncAlbums) {
|
||||
await backgroundManager.syncLinkedAlbum();
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -369,7 +370,7 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
|
||||
}
|
||||
|
||||
Future<void> _resumeBackup(DriftBackupNotifier notifier) async {
|
||||
final isEnableBackup = Store.get(StoreKey.enableBackup, false);
|
||||
final isEnableBackup = MetadataRepository.instance.appConfig.backup.enabled;
|
||||
|
||||
if (isEnableBackup) {
|
||||
final currentUser = Store.tryGet(StoreKey.currentUser);
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_bu
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/routes.provider.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
@@ -142,13 +143,18 @@ class _AddActionButtonState extends ConsumerState<AddActionButton> {
|
||||
return;
|
||||
}
|
||||
|
||||
final addedCount = await ref.read(remoteAlbumProvider.notifier).addAssets(album.id, [latest.remoteId!]);
|
||||
final result = await ref.read(actionProvider.notifier).addToAlbum(ActionSource.viewer, album);
|
||||
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (addedCount == 0) {
|
||||
if (!result.success) {
|
||||
ImmichToast.show(context: context, msg: 'scaffold_body_error_occurred'.tr(), toastType: ToastType.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.count == 0) {
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: 'add_to_album_bottom_sheet_already_exists'.tr(namedArgs: {'album': album.name}),
|
||||
@@ -159,7 +165,7 @@ class _AddActionButtonState extends ConsumerState<AddActionButton> {
|
||||
msg: 'add_to_album_bottom_sheet_added'.tr(namedArgs: {'album': album.name}),
|
||||
);
|
||||
|
||||
// Invalidate using the asset's remote ID to refresh the "Appears in" list
|
||||
// Refresh the "Appears in" list on the asset's info panel.
|
||||
ref.invalidate(albumsContainingAssetProvider(latest.remoteId!));
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
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/theme_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
@@ -747,12 +746,10 @@ class AddToAlbumHeader extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
Future<void> onCreateAlbum() async {
|
||||
final selectedAssets = ref.read(multiSelectProvider).selectedAssets;
|
||||
final newAlbum = await ref
|
||||
.read(remoteAlbumProvider.notifier)
|
||||
.createAlbum(
|
||||
title: "Untitled Album",
|
||||
assetIds: ref.read(multiSelectProvider).selectedAssets.map((e) => (e as RemoteAsset).id).toList(),
|
||||
);
|
||||
.createAlbumWithAssets(title: "Untitled Album", assets: selectedAssets);
|
||||
|
||||
if (newAlbum == null) {
|
||||
ImmichToast.show(context: context, toastType: ToastType.error, msg: 'errors.failed_to_create_album'.tr());
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
|
||||
import 'package:immich_mobile/providers/album/pending_album_uploads.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/asset_upload_progress.provider.dart';
|
||||
|
||||
/// Pinned banner sliver that surfaces in-flight album uploads directly under
|
||||
/// the album app bar. Renders nothing while the queue is empty. Tapping the
|
||||
@@ -165,6 +166,8 @@ class _PendingUploadsSheet extends ConsumerWidget {
|
||||
}
|
||||
|
||||
final failedCount = pending.where((p) => p.failed).length;
|
||||
final inFlightCount = pending.length - failedCount;
|
||||
final canAbort = inFlightCount > 0 && ref.watch(manualUploadCancelTokenProvider) != null;
|
||||
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
@@ -183,7 +186,21 @@ class _PendingUploadsSheet extends ConsumerWidget {
|
||||
style: context.textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
if (failedCount > 0)
|
||||
if (canAbort)
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
final cancelToken = ref.read(manualUploadCancelTokenProvider);
|
||||
if (cancelToken != null && !cancelToken.isCompleted) {
|
||||
cancelToken.complete();
|
||||
}
|
||||
ref.read(manualUploadCancelTokenProvider.notifier).state = null;
|
||||
ref.read(pendingAlbumUploadsProvider(albumId).notifier).clear();
|
||||
},
|
||||
icon: const Icon(Icons.stop_circle_outlined, size: 18),
|
||||
label: Text('cancel'.t(context: context)),
|
||||
style: TextButton.styleFrom(foregroundColor: context.colorScheme.error),
|
||||
)
|
||||
else if (failedCount > 0)
|
||||
TextButton.icon(
|
||||
onPressed: () => ref.read(pendingAlbumUploadsProvider(albumId).notifier).clearFailed(),
|
||||
icon: const Icon(Icons.clear_rounded, size: 18),
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/metadata_key.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/providers/backup/drift_backup.provider.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
|
||||
|
||||
class BackupToggleButton extends ConsumerStatefulWidget {
|
||||
final VoidCallback onStart;
|
||||
@@ -31,7 +31,7 @@ class BackupToggleButtonState extends ConsumerState<BackupToggleButton> with Sin
|
||||
end: 1,
|
||||
).animate(CurvedAnimation(parent: _animationController, curve: Curves.easeInOut));
|
||||
|
||||
_isEnabled = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup);
|
||||
_isEnabled = ref.read(metadataProvider).appConfig.backup.enabled;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -41,7 +41,7 @@ class BackupToggleButtonState extends ConsumerState<BackupToggleButton> with Sin
|
||||
}
|
||||
|
||||
Future<void> _onToggle(bool value) async {
|
||||
await ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.enableBackup, value);
|
||||
await ref.read(metadataProvider).write(MetadataKey.backupEnabled, value);
|
||||
|
||||
setState(() {
|
||||
_isEnabled = value;
|
||||
|
||||
@@ -3,9 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/setting.model.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/advanced_info_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/bulk_tag_assets_action_button.widget.dart';
|
||||
@@ -25,7 +23,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/user_metadata.provider.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
@@ -63,37 +61,23 @@ class _GeneralBottomSheetState extends ConsumerState<GeneralBottomSheet> {
|
||||
userMetadataPreferencesProvider.select((value) => value.valueOrNull?.tagsEnabled ?? false),
|
||||
);
|
||||
|
||||
Future<void> addAssetsToAlbum(RemoteAlbum album) async {
|
||||
final selectedAssets = multiselect.selectedAssets;
|
||||
if (selectedAssets.isEmpty) {
|
||||
Future<void> addToAlbum(RemoteAlbum album) async {
|
||||
final result = await ref.read(actionProvider.notifier).addToAlbum(ActionSource.timeline, album);
|
||||
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
final remoteAssets = selectedAssets.whereType<RemoteAsset>();
|
||||
final addedCount = await ref
|
||||
.read(remoteAlbumProvider.notifier)
|
||||
.addAssets(album.id, remoteAssets.map((e) => e.id).toList());
|
||||
|
||||
if (selectedAssets.length != remoteAssets.length) {
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: 'add_to_album_bottom_sheet_some_local_assets'.t(context: context),
|
||||
);
|
||||
if (!result.success) {
|
||||
ImmichToast.show(context: context, msg: 'scaffold_body_error_occurred'.tr(), toastType: ToastType.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (addedCount != remoteAssets.length) {
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: 'add_to_album_bottom_sheet_already_exists'.tr(namedArgs: {"album": album.name}),
|
||||
);
|
||||
} else {
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: 'add_to_album_bottom_sheet_added'.tr(namedArgs: {"album": album.name}),
|
||||
);
|
||||
}
|
||||
|
||||
ref.read(multiSelectProvider.notifier).reset();
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: result.count == 0
|
||||
? 'add_to_album_bottom_sheet_already_exists'.tr(namedArgs: {'album': album.name})
|
||||
: 'add_to_album_bottom_sheet_added'.tr(namedArgs: {'album': album.name}),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> onKeyboardExpand() {
|
||||
@@ -131,12 +115,10 @@ class _GeneralBottomSheetState extends ConsumerState<GeneralBottomSheet> {
|
||||
const DeleteLocalActionButton(source: ActionSource.timeline),
|
||||
if (multiselect.onlyLocal) const UploadActionButton(source: ActionSource.timeline),
|
||||
],
|
||||
slivers: multiselect.hasRemote
|
||||
? [
|
||||
const AddToAlbumHeader(),
|
||||
AlbumSelector(onAlbumSelected: addAssetsToAlbum, onKeyboardExpanded: onKeyboardExpand),
|
||||
]
|
||||
: [],
|
||||
slivers: [
|
||||
const AddToAlbumHeader(),
|
||||
AlbumSelector(onAlbumSelected: addToAlbum, onKeyboardExpanded: onKeyboardExpand),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,78 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_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/album/album_selector.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
|
||||
class LocalAlbumBottomSheet extends ConsumerWidget {
|
||||
class LocalAlbumBottomSheet extends ConsumerStatefulWidget {
|
||||
const LocalAlbumBottomSheet({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return const BaseBottomSheet(
|
||||
ConsumerState<LocalAlbumBottomSheet> createState() => _LocalAlbumBottomSheetState();
|
||||
}
|
||||
|
||||
class _LocalAlbumBottomSheetState extends ConsumerState<LocalAlbumBottomSheet> {
|
||||
late final DraggableScrollableController sheetController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
sheetController = DraggableScrollableController();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
sheetController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Future<void> addToAlbum(RemoteAlbum album) async {
|
||||
final result = await ref.read(actionProvider.notifier).addToAlbum(ActionSource.timeline, album);
|
||||
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result.success) {
|
||||
ImmichToast.show(context: context, msg: 'scaffold_body_error_occurred'.tr(), toastType: ToastType.error);
|
||||
return;
|
||||
}
|
||||
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: result.count == 0
|
||||
? 'add_to_album_bottom_sheet_already_exists'.tr(namedArgs: {'album': album.name})
|
||||
: 'add_to_album_bottom_sheet_added'.tr(namedArgs: {'album': album.name}),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> onKeyboardExpand() {
|
||||
return sheetController.animateTo(0.85, duration: const Duration(milliseconds: 200), curve: Curves.easeInOut);
|
||||
}
|
||||
|
||||
return BaseBottomSheet(
|
||||
controller: sheetController,
|
||||
initialChildSize: 0.25,
|
||||
maxChildSize: 0.4,
|
||||
maxChildSize: 0.85,
|
||||
shouldCloseOnMinExtent: false,
|
||||
actions: [
|
||||
actions: const [
|
||||
ShareActionButton(source: ActionSource.timeline),
|
||||
DeleteLocalActionButton(source: ActionSource.timeline),
|
||||
UploadActionButton(source: ActionSource.timeline),
|
||||
],
|
||||
slivers: [
|
||||
const AddToAlbumHeader(),
|
||||
AlbumSelector(onAlbumSelected: addToAlbum, onKeyboardExpanded: onKeyboardExpand),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+16
-21
@@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
|
||||
@@ -21,7 +20,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_b
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
@@ -56,29 +55,28 @@ class _RemoteAlbumBottomSheetState extends ConsumerState<RemoteAlbumBottomSheet>
|
||||
final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash));
|
||||
final ownsAlbum = ref.watch(currentUserProvider)?.id == widget.album.ownerId;
|
||||
|
||||
Future<void> addAssetsToAlbum(RemoteAlbum album) async {
|
||||
final selectedAssets = multiselect.selectedAssets;
|
||||
if (selectedAssets.isEmpty) {
|
||||
Future<void> addToAlbum(RemoteAlbum album) async {
|
||||
final result = await ref.read(actionProvider.notifier).addToAlbum(ActionSource.timeline, album);
|
||||
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
final addedCount = await ref
|
||||
.read(remoteAlbumProvider.notifier)
|
||||
.addAssets(album.id, selectedAssets.map((e) => (e as RemoteAsset).id).toList());
|
||||
|
||||
if (addedCount != selectedAssets.length) {
|
||||
if (!result.success) {
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: 'add_to_album_bottom_sheet_already_exists'.t(context: context, args: {"album": album.name}),
|
||||
);
|
||||
} else {
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: 'add_to_album_bottom_sheet_added'.t(context: context, args: {"album": album.name}),
|
||||
msg: 'scaffold_body_error_occurred'.t(context: context),
|
||||
toastType: ToastType.error,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
ref.read(multiSelectProvider.notifier).reset();
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: result.count == 0
|
||||
? 'add_to_album_bottom_sheet_already_exists'.t(context: context, args: {"album": album.name})
|
||||
: 'add_to_album_bottom_sheet_added'.t(context: context, args: {"album": album.name}),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> onKeyboardExpand() {
|
||||
@@ -118,10 +116,7 @@ class _RemoteAlbumBottomSheetState extends ConsumerState<RemoteAlbumBottomSheet>
|
||||
SetAlbumCoverActionButton(source: ActionSource.timeline, albumId: widget.album.id),
|
||||
],
|
||||
slivers: ownsAlbum
|
||||
? [
|
||||
const AddToAlbumHeader(),
|
||||
AlbumSelector(onAlbumSelected: addAssetsToAlbum, onKeyboardExpanded: onKeyboardExpand),
|
||||
]
|
||||
? [const AddToAlbumHeader(), AlbumSelector(onAlbumSelected: addToAlbum, onKeyboardExpanded: onKeyboardExpand)]
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -67,6 +67,11 @@ class AlbumPendingUploadsNotifier extends AutoDisposeFamilyNotifier<List<Pending
|
||||
_syncKeepAlive();
|
||||
}
|
||||
|
||||
void clear() {
|
||||
state = const [];
|
||||
_syncKeepAlive();
|
||||
}
|
||||
|
||||
void _syncKeepAlive() {
|
||||
if (state.isEmpty) {
|
||||
_keepAliveLink?.close();
|
||||
|
||||
@@ -5,16 +5,15 @@ import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/services/log.service.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
||||
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
|
||||
import 'package:immich_mobile/providers/notification_permission.provider.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/providers/websocket.provider.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
enum AppLifeCycleEnum { active, inactive, paused, resumed, detached, hidden }
|
||||
@@ -108,7 +107,7 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
final backgroundManager = _ref.read(backgroundSyncProvider);
|
||||
final isAlbumLinkedSyncEnable = _ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.syncAlbums);
|
||||
final isAlbumLinkedSyncEnable = _ref.read(metadataProvider).appConfig.backup.syncAlbums;
|
||||
|
||||
try {
|
||||
bool syncSuccess = false;
|
||||
@@ -138,7 +137,7 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
||||
}
|
||||
|
||||
Future<void> _resumeBackup() async {
|
||||
final isEnableBackup = _ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup);
|
||||
final isEnableBackup = _ref.read(metadataProvider).appConfig.backup.enabled;
|
||||
|
||||
if (isEnableBackup) {
|
||||
final currentUser = Store.tryGet(StoreKey.currentUser);
|
||||
|
||||
@@ -5,12 +5,15 @@ import 'package:background_downloader/background_downloader.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset_edit.model.dart';
|
||||
import 'package:immich_mobile/domain/services/asset.service.dart';
|
||||
import 'package:immich_mobile/domain/services/remote_album.service.dart';
|
||||
import 'package:immich_mobile/models/download/livephotos_medatada.model.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/asset_upload_progress.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provider.dart' show assetExifProvider;
|
||||
import 'package:immich_mobile/providers/infrastructure/tag.provider.dart';
|
||||
@@ -371,6 +374,52 @@ class ActionNotifier extends Notifier<void> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<ActionResult> addToAlbum(ActionSource source, RemoteAlbum album) async {
|
||||
final selected = _getAssets(source).toList(growable: false);
|
||||
if (selected.isEmpty) {
|
||||
return const ActionResult(count: 0, success: true);
|
||||
}
|
||||
|
||||
final candidates = RemoteAlbumService.categorizeCandidates(selected);
|
||||
final remoteIds = candidates.remoteAssetIds;
|
||||
final localAssets = candidates.localAssetsToUpload;
|
||||
final albumNotifier = ref.read(remoteAlbumProvider.notifier);
|
||||
|
||||
int addedRemote = 0;
|
||||
if (remoteIds.isNotEmpty) {
|
||||
try {
|
||||
addedRemote = await albumNotifier.addAssets(album.id, remoteIds);
|
||||
} catch (error, stack) {
|
||||
_logger.severe('Failed to add assets to album ${album.id}', error, stack);
|
||||
return ActionResult(count: 0, success: false, error: error.toString());
|
||||
}
|
||||
}
|
||||
|
||||
// Keep the selection available for retry if the remote add fails. Once the
|
||||
// album mutation succeeds, clear timeline selection so upload overlays can render.
|
||||
if (source == ActionSource.timeline) {
|
||||
ref.read(multiSelectProvider.notifier).reset();
|
||||
}
|
||||
|
||||
if (localAssets.isEmpty) {
|
||||
return ActionResult(count: addedRemote, success: true);
|
||||
}
|
||||
|
||||
final uploadResult = await upload(
|
||||
source,
|
||||
assets: localAssets,
|
||||
onAssetUploaded: (asset, remoteId) async {
|
||||
await albumNotifier.linkUploadedAssetToAlbum(album.id, asset, remoteId);
|
||||
},
|
||||
);
|
||||
|
||||
return ActionResult(
|
||||
count: addedRemote + uploadResult.count,
|
||||
success: uploadResult.success,
|
||||
error: uploadResult.error,
|
||||
);
|
||||
}
|
||||
|
||||
Future<ActionResult> removeFromAlbum(ActionSource source, String albumId) async {
|
||||
final ids = _getRemoteIdsForSource(source);
|
||||
try {
|
||||
@@ -487,8 +536,16 @@ class ActionNotifier extends Notifier<void> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<ActionResult> upload(ActionSource source, {List<LocalAsset>? assets}) async {
|
||||
Future<ActionResult> upload(
|
||||
ActionSource source, {
|
||||
List<LocalAsset>? assets,
|
||||
FutureOr<void> Function(LocalAsset asset, String remoteId)? onAssetUploaded,
|
||||
}) async {
|
||||
final assetsToUpload = assets ?? _getAssets(source).whereType<LocalAsset>().toList();
|
||||
final assetById = {for (final a in assetsToUpload) a.id: a};
|
||||
final uploadedAssetIds = <String>{};
|
||||
final failedAssetIds = <String>{};
|
||||
final postUploadTasks = <Future<void>>[];
|
||||
|
||||
final progressNotifier = ref.read(assetUploadProgressProvider.notifier);
|
||||
final cancelToken = Completer<void>();
|
||||
@@ -510,16 +567,43 @@ class ActionNotifier extends Notifier<void> {
|
||||
},
|
||||
onSuccess: (localAssetId, remoteAssetId) {
|
||||
progressNotifier.remove(localAssetId);
|
||||
uploadedAssetIds.add(localAssetId);
|
||||
final asset = assetById[localAssetId];
|
||||
final callback = onAssetUploaded;
|
||||
if (asset != null && callback != null) {
|
||||
postUploadTasks.add(
|
||||
Future.sync(() => callback(asset, remoteAssetId)).catchError((Object error, StackTrace stack) {
|
||||
failedAssetIds.add(localAssetId);
|
||||
progressNotifier.setError(localAssetId);
|
||||
_logger.warning('Post-upload callback failed for $localAssetId', error, stack);
|
||||
}),
|
||||
);
|
||||
}
|
||||
},
|
||||
onError: (localAssetId, errorMessage) {
|
||||
failedAssetIds.add(localAssetId);
|
||||
progressNotifier.setError(localAssetId);
|
||||
},
|
||||
),
|
||||
);
|
||||
return ActionResult(count: assetsToUpload.length, success: true);
|
||||
|
||||
await Future.wait(postUploadTasks);
|
||||
final successCount = uploadedAssetIds.difference(failedAssetIds).length;
|
||||
final isSuccess = successCount == assetsToUpload.length && failedAssetIds.isEmpty;
|
||||
|
||||
return ActionResult(
|
||||
count: successCount,
|
||||
success: isSuccess,
|
||||
error: isSuccess ? null : 'Failed to upload ${assetsToUpload.length - successCount} assets',
|
||||
);
|
||||
} catch (error, stack) {
|
||||
_logger.severe('Failed manually upload assets', error, stack);
|
||||
return ActionResult(count: assetsToUpload.length, success: false, error: error.toString());
|
||||
|
||||
return ActionResult(
|
||||
count: uploadedAssetIds.difference(failedAssetIds).length,
|
||||
success: false,
|
||||
error: error.toString(),
|
||||
);
|
||||
} finally {
|
||||
ref.read(manualUploadCancelTokenProvider.notifier).state = null;
|
||||
Future.delayed(const Duration(seconds: 2), () {
|
||||
|
||||
@@ -9,6 +9,7 @@ import 'package:immich_mobile/domain/services/remote_album.service.dart';
|
||||
import 'package:immich_mobile/models/albums/album_search.model.dart';
|
||||
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
||||
import 'package:immich_mobile/providers/album/pending_album_uploads.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/asset_upload_progress.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/services/foreground_upload.service.dart';
|
||||
@@ -207,6 +208,22 @@ class RemoteAlbumNotifier extends Notifier<RemoteAlbumState> {
|
||||
return added;
|
||||
}
|
||||
|
||||
/// Links a freshly-uploaded local asset to an album using its new remote ID,
|
||||
/// upserting a placeholder remote asset row so the local DB join survives
|
||||
/// until the next sync catches up.
|
||||
Future<int> linkUploadedAssetToAlbum(String albumId, LocalAsset source, String remoteId) async {
|
||||
final currentUser = ref.read(currentUserProvider);
|
||||
if (currentUser == null) {
|
||||
throw Exception('User not logged in');
|
||||
}
|
||||
|
||||
final added = await _remoteAlbumService.linkUploadedAssetToAlbum(albumId, remoteId, currentUser, source);
|
||||
if (added > 0) {
|
||||
await _refreshAlbumInState(albumId);
|
||||
}
|
||||
return added;
|
||||
}
|
||||
|
||||
/// Adds a heterogeneous asset selection to an album. Already-remote assets
|
||||
/// are linked immediately; local-only assets are queued in
|
||||
/// [pendingAlbumUploadsProvider] (so the album page can show them with
|
||||
@@ -221,11 +238,18 @@ class RemoteAlbumNotifier extends Notifier<RemoteAlbumState> {
|
||||
final pendingNotifier = ref.read(pendingAlbumUploadsProvider(albumId).notifier);
|
||||
pendingNotifier.enqueue(candidates.localAssetsToUpload);
|
||||
|
||||
Completer<void>? cancelToken;
|
||||
if (candidates.localAssetsToUpload.isNotEmpty) {
|
||||
cancelToken = Completer<void>();
|
||||
ref.read(manualUploadCancelTokenProvider.notifier).state = cancelToken;
|
||||
}
|
||||
|
||||
try {
|
||||
final added = await _remoteAlbumService.addAssetsToAlbum(
|
||||
albumId: albumId,
|
||||
uploader: currentUser,
|
||||
candidates: candidates,
|
||||
cancelToken: cancelToken,
|
||||
uploadCallbacks: UploadCallbacks(
|
||||
onProgress: (localAssetId, _, bytes, totalBytes) {
|
||||
final progress = totalBytes > 0 ? bytes / totalBytes : 0.0;
|
||||
@@ -245,6 +269,15 @@ class RemoteAlbumNotifier extends Notifier<RemoteAlbumState> {
|
||||
}
|
||||
_logger.severe('Failed to add assets to album $albumId', error, stack);
|
||||
rethrow;
|
||||
} finally {
|
||||
if (cancelToken != null) {
|
||||
if (cancelToken.isCompleted) {
|
||||
pendingNotifier.clear();
|
||||
}
|
||||
if (ref.read(manualUploadCancelTokenProvider) == cancelToken) {
|
||||
ref.read(manualUploadCancelTokenProvider.notifier).state = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:immich_mobile/infrastructure/repositories/network.repository.dar
|
||||
import 'package:immich_mobile/models/server_info/server_version.model.dart';
|
||||
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/utils/debounce.dart';
|
||||
import 'package:immich_mobile/utils/debug_print.dart';
|
||||
@@ -192,7 +193,7 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
||||
return;
|
||||
}
|
||||
|
||||
final isSyncAlbumEnabled = Store.get(StoreKey.syncAlbums, false);
|
||||
final isSyncAlbumEnabled = _ref.read(metadataProvider).appConfig.backup.syncAlbums;
|
||||
try {
|
||||
unawaited(
|
||||
_ref.read(backgroundSyncProvider).syncWebsocketBatchV1(_batchedAssetUploadReady.toList()).then((_) {
|
||||
@@ -213,7 +214,7 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
||||
return;
|
||||
}
|
||||
|
||||
final isSyncAlbumEnabled = Store.get(StoreKey.syncAlbums, false);
|
||||
final isSyncAlbumEnabled = _ref.read(metadataProvider).appConfig.backup.syncAlbums;
|
||||
try {
|
||||
unawaited(
|
||||
_ref.read(backgroundSyncProvider).syncWebsocketBatchV2(_batchedAssetUploadReady.toList()).then((_) {
|
||||
|
||||
@@ -5,13 +5,7 @@ enum AppSettingsEnum<T> {
|
||||
advancedTroubleshooting<bool>(StoreKey.advancedTroubleshooting, null, false),
|
||||
manageLocalMediaAndroid<bool>(StoreKey.manageLocalMediaAndroid, null, false),
|
||||
enableHapticFeedback<bool>(StoreKey.enableHapticFeedback, null, true),
|
||||
syncAlbums<bool>(StoreKey.syncAlbums, null, false),
|
||||
enableBackup<bool>(StoreKey.enableBackup, null, false),
|
||||
useCellularForUploadVideos<bool>(StoreKey.useWifiForUploadVideos, null, false),
|
||||
useCellularForUploadPhotos<bool>(StoreKey.useWifiForUploadPhotos, null, false),
|
||||
readonlyModeEnabled<bool>(StoreKey.readonlyModeEnabled, "readonlyModeEnabled", false),
|
||||
backupRequireCharging<bool>(StoreKey.backupRequireCharging, null, false),
|
||||
backupTriggerDelay<int>(StoreKey.backupTriggerDelay, null, 30);
|
||||
readonlyModeEnabled<bool>(StoreKey.readonlyModeEnabled, "readonlyModeEnabled", false);
|
||||
|
||||
const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue);
|
||||
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/metadata_key.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/utils/background_sync.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
|
||||
import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart';
|
||||
import 'package:immich_mobile/models/auth/login_response.model.dart';
|
||||
import 'package:immich_mobile/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||
import 'package:immich_mobile/repositories/auth.repository.dart';
|
||||
import 'package:immich_mobile/repositories/auth_api.repository.dart';
|
||||
import 'package:immich_mobile/services/api.service.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/services/network.service.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
@@ -25,7 +25,6 @@ final authServiceProvider = Provider(
|
||||
ref.watch(apiServiceProvider),
|
||||
ref.watch(networkServiceProvider),
|
||||
ref.watch(backgroundSyncProvider),
|
||||
ref.watch(appSettingsServiceProvider),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -35,7 +34,6 @@ class AuthService {
|
||||
final ApiService _apiService;
|
||||
final NetworkService _networkService;
|
||||
final BackgroundSyncManager _backgroundSyncManager;
|
||||
final AppSettingsService _appSettingsService;
|
||||
final _log = Logger("AuthService");
|
||||
|
||||
AuthService(
|
||||
@@ -44,7 +42,6 @@ class AuthService {
|
||||
this._apiService,
|
||||
this._networkService,
|
||||
this._backgroundSyncManager,
|
||||
this._appSettingsService,
|
||||
);
|
||||
|
||||
/// Validates the provided server URL by resolving and setting the endpoint.
|
||||
@@ -103,7 +100,7 @@ class AuthService {
|
||||
_log.severe("Error clearing local data", error, stackTrace);
|
||||
});
|
||||
|
||||
await _appSettingsService.setSetting(AppSettingsEnum.enableBackup, false);
|
||||
await MetadataRepository.instance.write(MetadataKey.backupEnabled, false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,14 +13,13 @@ import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/extensions/platform_extensions.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/metadata.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/infrastructure/asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/storage.provider.dart';
|
||||
import 'package:immich_mobile/repositories/asset_media.repository.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:immich_mobile/utils/debug_print.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
@@ -31,7 +30,6 @@ final backgroundUploadServiceProvider = Provider((ref) {
|
||||
ref.watch(storageRepositoryProvider),
|
||||
ref.watch(localAssetRepository),
|
||||
ref.watch(backupRepositoryProvider),
|
||||
ref.watch(appSettingsServiceProvider),
|
||||
ref.watch(assetMediaRepositoryProvider),
|
||||
);
|
||||
|
||||
@@ -105,7 +103,6 @@ class BackgroundUploadService {
|
||||
this._storageRepository,
|
||||
this._localAssetRepository,
|
||||
this._backupRepository,
|
||||
this._appSettingsService,
|
||||
this._assetMediaRepository,
|
||||
) {
|
||||
_uploadRepository.onUploadStatus = _onUploadCallback;
|
||||
@@ -116,7 +113,6 @@ class BackgroundUploadService {
|
||||
final StorageRepository _storageRepository;
|
||||
final DriftLocalAssetRepository _localAssetRepository;
|
||||
final DriftBackupRepository _backupRepository;
|
||||
final AppSettingsService _appSettingsService;
|
||||
final AssetMediaRepository _assetMediaRepository;
|
||||
final Logger _logger = Logger('BackgroundUploadService');
|
||||
|
||||
@@ -363,15 +359,14 @@ class BackgroundUploadService {
|
||||
}
|
||||
|
||||
bool _shouldRequireWiFi(LocalAsset asset) {
|
||||
bool requiresWiFi = true;
|
||||
|
||||
if (asset.isVideo && _appSettingsService.getSetting(AppSettingsEnum.useCellularForUploadVideos)) {
|
||||
requiresWiFi = false;
|
||||
} else if (!asset.isVideo && _appSettingsService.getSetting(AppSettingsEnum.useCellularForUploadPhotos)) {
|
||||
requiresWiFi = false;
|
||||
final backup = MetadataRepository.instance.appConfig.backup;
|
||||
if (asset.isVideo && backup.useCellularForVideos) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return requiresWiFi;
|
||||
if (!asset.isVideo && backup.useCellularForPhotos) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<UploadTask> buildUploadTask(
|
||||
|
||||
@@ -7,18 +7,17 @@ import 'package:immich_mobile/domain/models/asset/asset_metadata.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/network_capability_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
|
||||
import 'package:immich_mobile/platform/connectivity_api.g.dart';
|
||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/storage.provider.dart';
|
||||
import 'package:immich_mobile/repositories/asset_media.repository.dart';
|
||||
import 'package:immich_mobile/repositories/upload.repository.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:photo_manager/photo_manager.dart' show PMProgressHandler;
|
||||
@@ -39,7 +38,6 @@ final foregroundUploadServiceProvider = Provider((ref) {
|
||||
ref.watch(storageRepositoryProvider),
|
||||
ref.watch(backupRepositoryProvider),
|
||||
ref.watch(connectivityApiProvider),
|
||||
ref.watch(appSettingsServiceProvider),
|
||||
ref.watch(assetMediaRepositoryProvider),
|
||||
);
|
||||
});
|
||||
@@ -55,7 +53,6 @@ class ForegroundUploadService {
|
||||
this._storageRepository,
|
||||
this._backupRepository,
|
||||
this._connectivityApi,
|
||||
this._appSettingsService,
|
||||
this._assetMediaRepository,
|
||||
);
|
||||
|
||||
@@ -63,7 +60,6 @@ class ForegroundUploadService {
|
||||
final StorageRepository _storageRepository;
|
||||
final DriftBackupRepository _backupRepository;
|
||||
final ConnectivityApi _connectivityApi;
|
||||
final AppSettingsService _appSettingsService;
|
||||
final AssetMediaRepository _assetMediaRepository;
|
||||
final Logger _logger = Logger('ForegroundUploadService');
|
||||
|
||||
@@ -455,14 +451,13 @@ class ForegroundUploadService {
|
||||
}
|
||||
|
||||
bool _shouldRequireWiFi(LocalAsset asset) {
|
||||
bool requiresWiFi = true;
|
||||
|
||||
if (asset.isVideo && _appSettingsService.getSetting(AppSettingsEnum.useCellularForUploadVideos)) {
|
||||
requiresWiFi = false;
|
||||
} else if (!asset.isVideo && _appSettingsService.getSetting(AppSettingsEnum.useCellularForUploadPhotos)) {
|
||||
requiresWiFi = false;
|
||||
final backup = MetadataRepository.instance.appConfig.backup;
|
||||
if (asset.isVideo && backup.useCellularForVideos) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return requiresWiFi;
|
||||
if (!asset.isVideo && backup.useCellularForPhotos) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,6 +124,13 @@ Future<void> _migrateTo26(Drift drift) async {
|
||||
await _migrateAlbumSortMode(migrator);
|
||||
await migrator.migrateBool(StoreKey.legacySelectedAlbumSortReverse, MetadataKey.albumIsReverse);
|
||||
await migrator.migrateBool(StoreKey.legacyAlbumGridView, MetadataKey.albumIsGrid);
|
||||
// Backup
|
||||
await migrator.migrateBool(StoreKey.legacyEnableBackup, MetadataKey.backupEnabled);
|
||||
await migrator.migrateBool(StoreKey.legacyUseWifiForUploadVideos, MetadataKey.backupUseCellularForVideos);
|
||||
await migrator.migrateBool(StoreKey.legacyUseWifiForUploadPhotos, MetadataKey.backupUseCellularForPhotos);
|
||||
await migrator.migrateBool(StoreKey.legacyBackupRequireCharging, MetadataKey.backupRequireCharging);
|
||||
await migrator.migrateInt(StoreKey.legacyBackupTriggerDelay, MetadataKey.backupTriggerDelay);
|
||||
await migrator.migrateBool(StoreKey.legacySyncAlbums, MetadataKey.backupSyncAlbums);
|
||||
await migrator.complete();
|
||||
}
|
||||
|
||||
|
||||
@@ -6,13 +6,12 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/setting.model.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/models/server_info/server_info.model.dart';
|
||||
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
||||
import 'package:immich_mobile/providers/cast.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/providers/sync_status.provider.dart';
|
||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||
@@ -193,64 +192,51 @@ class _BackupIndicator extends ConsumerWidget {
|
||||
}
|
||||
|
||||
Widget? _getBackupBadgeIcon(BuildContext context, WidgetRef ref) {
|
||||
final backupStateStream = ref.watch(settingsProvider).watch(Setting.enableBackup);
|
||||
final backupEnabled = ref.watch(appConfigProvider.select((c) => c.backup.enabled));
|
||||
final hasError = ref.watch(driftBackupProvider.select((state) => state.error != BackupError.none));
|
||||
final isDarkTheme = context.isDarkTheme;
|
||||
final iconColor = isDarkTheme ? Colors.white : Colors.black;
|
||||
final isUploading = ref.watch(driftBackupProvider.select((state) => state.uploadItems.isNotEmpty));
|
||||
|
||||
return StreamBuilder(
|
||||
stream: backupStateStream,
|
||||
initialData: false,
|
||||
builder: (ctx, snapshot) {
|
||||
final backupEnabled = snapshot.data ?? false;
|
||||
if (!backupEnabled) {
|
||||
return _BadgeLabel(
|
||||
Icon(Icons.cloud_off_rounded, size: 9, color: iconColor, semanticLabel: 'backup_controller_page_backup'.tr()),
|
||||
);
|
||||
}
|
||||
|
||||
if (!backupEnabled) {
|
||||
return _BadgeLabel(
|
||||
Icon(
|
||||
Icons.cloud_off_rounded,
|
||||
size: 9,
|
||||
color: iconColor,
|
||||
semanticLabel: 'backup_controller_page_backup'.tr(),
|
||||
if (hasError) {
|
||||
return _BadgeLabel(
|
||||
Icon(
|
||||
Icons.warning_rounded,
|
||||
size: 12,
|
||||
color: context.colorScheme.error,
|
||||
semanticLabel: 'backup_controller_page_backup'.tr(),
|
||||
),
|
||||
backgroundColor: context.colorScheme.errorContainer,
|
||||
);
|
||||
}
|
||||
|
||||
if (isUploading) {
|
||||
return _BadgeLabel(
|
||||
Container(
|
||||
padding: const EdgeInsets.all(3.5),
|
||||
child: Theme(
|
||||
data: context.themeData.copyWith(
|
||||
progressIndicatorTheme: context.themeData.progressIndicatorTheme.copyWith(year2023: true),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (hasError) {
|
||||
return _BadgeLabel(
|
||||
Icon(
|
||||
Icons.warning_rounded,
|
||||
size: 12,
|
||||
color: context.colorScheme.error,
|
||||
semanticLabel: 'backup_controller_page_backup'.tr(),
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
strokeCap: StrokeCap.round,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(iconColor),
|
||||
semanticsLabel: 'backup_controller_page_backup'.tr(),
|
||||
),
|
||||
backgroundColor: context.colorScheme.errorContainer,
|
||||
);
|
||||
}
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (isUploading) {
|
||||
return _BadgeLabel(
|
||||
Container(
|
||||
padding: const EdgeInsets.all(3.5),
|
||||
child: Theme(
|
||||
data: context.themeData.copyWith(
|
||||
progressIndicatorTheme: context.themeData.progressIndicatorTheme.copyWith(year2023: true),
|
||||
),
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
strokeCap: StrokeCap.round,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(iconColor),
|
||||
semanticsLabel: 'backup_controller_page_backup'.tr(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return _BadgeLabel(
|
||||
Icon(Icons.check_outlined, size: 9, color: iconColor, semanticLabel: 'backup_controller_page_backup'.tr()),
|
||||
);
|
||||
},
|
||||
return _BadgeLabel(
|
||||
Icon(Icons.check_outlined, size: 9, color: iconColor, semanticLabel: 'backup_controller_page_backup'.tr()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ 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/infrastructure/repositories/metadata.repository.dart';
|
||||
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
||||
@@ -186,7 +187,7 @@ class LoginForm extends HookConsumerWidget {
|
||||
await backgroundManager.syncRemote();
|
||||
await backgroundManager.hashAssets();
|
||||
|
||||
if (Store.get(StoreKey.syncAlbums, false)) {
|
||||
if (MetadataRepository.instance.appConfig.backup.syncAlbums) {
|
||||
await backgroundManager.syncLinkedAlbum();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,18 +4,17 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.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/store.model.dart';
|
||||
import 'package:immich_mobile/domain/models/config/app_config.dart';
|
||||
import 'package:immich_mobile/domain/models/metadata_key.dart';
|
||||
import 'package:immich_mobile/domain/services/sync_linked_album.service.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/backup_album.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/platform.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/setting_group_title.dart';
|
||||
import 'package:immich_mobile/widgets/settings/setting_list_tile.dart';
|
||||
import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart';
|
||||
@@ -31,8 +30,8 @@ class DriftBackupSettings extends ConsumerWidget {
|
||||
title: "network_requirements".t(context: context),
|
||||
icon: Icons.cell_tower,
|
||||
),
|
||||
const _UseWifiForUploadVideosButton(),
|
||||
const _UseWifiForUploadPhotosButton(),
|
||||
const _UseCellularForVideosButton(),
|
||||
const _UseCellularForPhotosButton(),
|
||||
if (CurrentPlatform.isAndroid) ...[
|
||||
const Divider(),
|
||||
SettingGroupTitle(
|
||||
@@ -99,64 +98,58 @@ class _AlbumSyncActionButtonState extends ConsumerState<_AlbumSyncActionButton>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final albumSyncEnable = ref.watch(appConfigProvider.select((c) => c.backup.syncAlbums));
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
children: [
|
||||
StreamBuilder(
|
||||
stream: Store.watch(StoreKey.syncAlbums),
|
||||
initialData: Store.tryGet(StoreKey.syncAlbums) ?? false,
|
||||
builder: (context, snapshot) {
|
||||
final albumSyncEnable = snapshot.data ?? false;
|
||||
return Column(
|
||||
children: [
|
||||
SettingListTile(
|
||||
title: "sync_albums".t(context: context),
|
||||
subtitle: "sync_upload_album_setting_subtitle".t(context: context),
|
||||
trailing: Switch(
|
||||
value: albumSyncEnable,
|
||||
onChanged: (bool newValue) async {
|
||||
await ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.syncAlbums, newValue);
|
||||
Column(
|
||||
children: [
|
||||
SettingListTile(
|
||||
title: "sync_albums".t(context: context),
|
||||
subtitle: "sync_upload_album_setting_subtitle".t(context: context),
|
||||
trailing: Switch(
|
||||
value: albumSyncEnable,
|
||||
onChanged: (bool newValue) async {
|
||||
await ref.read(metadataProvider).write(MetadataKey.backupSyncAlbums, newValue);
|
||||
|
||||
if (newValue == true) {
|
||||
await _manageLinkedAlbums();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
AnimatedSize(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
child: AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
opacity: albumSyncEnable ? 1.0 : 0.0,
|
||||
child: albumSyncEnable
|
||||
? SettingListTile(
|
||||
onTap: _manualSyncAlbums,
|
||||
contentPadding: const EdgeInsets.only(left: 32, right: 16),
|
||||
title: "organize_into_albums".t(context: context),
|
||||
subtitle: "organize_into_albums_description".t(context: context),
|
||||
trailing: isAlbumSyncInProgress
|
||||
? const SizedBox(
|
||||
width: 32,
|
||||
height: 32,
|
||||
child: CircularProgressIndicator.adaptive(strokeWidth: 2),
|
||||
)
|
||||
: IconButton(
|
||||
onPressed: _manualSyncAlbums,
|
||||
icon: const Icon(Icons.sync_rounded),
|
||||
color: context.colorScheme.onSurface.withValues(alpha: 0.7),
|
||||
iconSize: 20,
|
||||
constraints: const BoxConstraints(minWidth: 32, minHeight: 32),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
if (newValue == true) {
|
||||
await _manageLinkedAlbums();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
AnimatedSize(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
child: AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
opacity: albumSyncEnable ? 1.0 : 0.0,
|
||||
child: albumSyncEnable
|
||||
? SettingListTile(
|
||||
onTap: _manualSyncAlbums,
|
||||
contentPadding: const EdgeInsets.only(left: 32, right: 16),
|
||||
title: "organize_into_albums".t(context: context),
|
||||
subtitle: "organize_into_albums_description".t(context: context),
|
||||
trailing: isAlbumSyncInProgress
|
||||
? const SizedBox(
|
||||
width: 32,
|
||||
height: 32,
|
||||
child: CircularProgressIndicator.adaptive(strokeWidth: 2),
|
||||
)
|
||||
: IconButton(
|
||||
onPressed: _manualSyncAlbums,
|
||||
icon: const Icon(Icons.sync_rounded),
|
||||
color: context.colorScheme.onSurface.withValues(alpha: 0.7),
|
||||
iconSize: 20,
|
||||
constraints: const BoxConstraints(minWidth: 32, minHeight: 32),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -164,60 +157,34 @@ class _AlbumSyncActionButtonState extends ConsumerState<_AlbumSyncActionButton>
|
||||
}
|
||||
}
|
||||
|
||||
class _SettingsSwitchTile extends ConsumerStatefulWidget {
|
||||
final AppSettingsEnum<bool> appSettingsEnum;
|
||||
class _BackupSwitchTile extends ConsumerWidget {
|
||||
final MetadataKey<bool> metadataKey;
|
||||
final bool Function(AppConfig) selector;
|
||||
final String titleKey;
|
||||
final String subtitleKey;
|
||||
final void Function(bool?)? onChanged;
|
||||
final void Function(bool)? onChanged;
|
||||
|
||||
const _SettingsSwitchTile({
|
||||
required this.appSettingsEnum,
|
||||
const _BackupSwitchTile({
|
||||
required this.metadataKey,
|
||||
required this.selector,
|
||||
required this.titleKey,
|
||||
required this.subtitleKey,
|
||||
this.onChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
ConsumerState createState() => _SettingsSwitchTileState();
|
||||
}
|
||||
|
||||
class _SettingsSwitchTileState extends ConsumerState<_SettingsSwitchTile> {
|
||||
late final Stream<bool?> valueStream;
|
||||
late final StreamSubscription<bool?> subscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
valueStream = Store.watch(widget.appSettingsEnum.storeKey).asBroadcastStream();
|
||||
subscription = valueStream.listen((value) {
|
||||
widget.onChanged?.call(value);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
subscription.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final value = ref.watch(appConfigProvider.select(selector));
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: SettingListTile(
|
||||
title: widget.titleKey.t(context: context),
|
||||
subtitle: widget.subtitleKey.t(context: context),
|
||||
trailing: StreamBuilder(
|
||||
stream: valueStream,
|
||||
initialData: Store.tryGet(widget.appSettingsEnum.storeKey) ?? widget.appSettingsEnum.defaultValue,
|
||||
builder: (context, snapshot) {
|
||||
final value = snapshot.data ?? false;
|
||||
return Switch(
|
||||
value: value,
|
||||
onChanged: (bool newValue) async {
|
||||
await ref.read(appSettingsServiceProvider).setSetting(widget.appSettingsEnum, newValue);
|
||||
},
|
||||
);
|
||||
title: titleKey.t(context: context),
|
||||
subtitle: subtitleKey.t(context: context),
|
||||
trailing: Switch(
|
||||
value: value,
|
||||
onChanged: (bool newValue) async {
|
||||
await ref.read(metadataProvider).write(metadataKey, newValue);
|
||||
onChanged?.call(newValue);
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -225,26 +192,28 @@ class _SettingsSwitchTileState extends ConsumerState<_SettingsSwitchTile> {
|
||||
}
|
||||
}
|
||||
|
||||
class _UseWifiForUploadVideosButton extends ConsumerWidget {
|
||||
const _UseWifiForUploadVideosButton();
|
||||
class _UseCellularForVideosButton extends StatelessWidget {
|
||||
const _UseCellularForVideosButton();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return const _SettingsSwitchTile(
|
||||
appSettingsEnum: AppSettingsEnum.useCellularForUploadVideos,
|
||||
Widget build(BuildContext context) {
|
||||
return _BackupSwitchTile(
|
||||
metadataKey: MetadataKey.backupUseCellularForVideos,
|
||||
selector: (c) => c.backup.useCellularForVideos,
|
||||
titleKey: "videos",
|
||||
subtitleKey: "network_requirement_videos_upload",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _UseWifiForUploadPhotosButton extends ConsumerWidget {
|
||||
const _UseWifiForUploadPhotosButton();
|
||||
class _UseCellularForPhotosButton extends StatelessWidget {
|
||||
const _UseCellularForPhotosButton();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return const _SettingsSwitchTile(
|
||||
appSettingsEnum: AppSettingsEnum.useCellularForUploadPhotos,
|
||||
Widget build(BuildContext context) {
|
||||
return _BackupSwitchTile(
|
||||
metadataKey: MetadataKey.backupUseCellularForPhotos,
|
||||
selector: (c) => c.backup.useCellularForPhotos,
|
||||
titleKey: "photos",
|
||||
subtitleKey: "network_requirement_photos_upload",
|
||||
);
|
||||
@@ -256,29 +225,22 @@ class _BackupOnlyWhenChargingButton extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return _SettingsSwitchTile(
|
||||
appSettingsEnum: AppSettingsEnum.backupRequireCharging,
|
||||
final fgService = ref.read(backgroundWorkerFgServiceProvider);
|
||||
return _BackupSwitchTile(
|
||||
metadataKey: MetadataKey.backupRequireCharging,
|
||||
selector: (c) => c.backup.requireCharging,
|
||||
titleKey: "charging",
|
||||
subtitleKey: "charging_requirement_mobile_backup",
|
||||
onChanged: (value) {
|
||||
ref.read(backgroundWorkerFgServiceProvider).configure(requireCharging: value ?? false);
|
||||
fgService.configure(requireCharging: value);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BackupDelaySlider extends ConsumerStatefulWidget {
|
||||
class _BackupDelaySlider extends ConsumerWidget {
|
||||
const _BackupDelaySlider();
|
||||
|
||||
@override
|
||||
ConsumerState<_BackupDelaySlider> createState() => _BackupDelaySliderState();
|
||||
}
|
||||
|
||||
class _BackupDelaySliderState extends ConsumerState<_BackupDelaySlider> {
|
||||
late final Stream<int?> valueStream;
|
||||
late final StreamSubscription<int?> subscription;
|
||||
late int currentValue;
|
||||
|
||||
static int backupDelayToSliderValue(int ms) => switch (ms) {
|
||||
5 => 0,
|
||||
30 => 1,
|
||||
@@ -301,30 +263,9 @@ class _BackupDelaySliderState extends ConsumerState<_BackupDelaySlider> {
|
||||
};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final initialValue =
|
||||
Store.tryGet(AppSettingsEnum.backupTriggerDelay.storeKey) ?? AppSettingsEnum.backupTriggerDelay.defaultValue;
|
||||
currentValue = backupDelayToSliderValue(initialValue);
|
||||
|
||||
valueStream = Store.watch(AppSettingsEnum.backupTriggerDelay.storeKey).asBroadcastStream();
|
||||
subscription = valueStream.listen((value) {
|
||||
if (mounted && value != null) {
|
||||
setState(() {
|
||||
currentValue = backupDelayToSliderValue(value);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
subscription.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final triggerDelay = ref.watch(appConfigProvider.select((c) => c.backup.triggerDelay));
|
||||
final currentValue = backupDelayToSliderValue(triggerDelay);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -339,14 +280,13 @@ class _BackupDelaySliderState extends ConsumerState<_BackupDelaySlider> {
|
||||
),
|
||||
Slider(
|
||||
value: currentValue.toDouble(),
|
||||
onChanged: (double v) {
|
||||
setState(() {
|
||||
currentValue = v.toInt();
|
||||
});
|
||||
onChanged: (double v) async {
|
||||
final seconds = backupDelayToSeconds(v.toInt());
|
||||
await ref.read(metadataProvider).write(MetadataKey.backupTriggerDelay, seconds);
|
||||
},
|
||||
onChangeEnd: (double v) async {
|
||||
final milliseconds = backupDelayToSeconds(v.toInt());
|
||||
await ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.backupTriggerDelay, milliseconds);
|
||||
final seconds = backupDelayToSeconds(v.toInt());
|
||||
await ref.read(metadataProvider).write(MetadataKey.backupTriggerDelay, seconds);
|
||||
},
|
||||
max: 3.0,
|
||||
min: 0.0,
|
||||
|
||||
@@ -9,7 +9,7 @@ import 'package:mocktail/mocktail.dart';
|
||||
import '../../infrastructure/repository.mock.dart';
|
||||
|
||||
const _kAccessToken = '#ThisIsAToken';
|
||||
const _kEnableBackup = false;
|
||||
const _kAdvancedTroubleshooting = false;
|
||||
const _kVersion = 2;
|
||||
|
||||
void main() {
|
||||
@@ -22,13 +22,13 @@ void main() {
|
||||
mockDriftStoreRepo = MockDriftStoreRepository();
|
||||
// For generics, we need to provide fallback to each concrete type to avoid runtime errors
|
||||
registerFallbackValue(StoreKey.accessToken);
|
||||
registerFallbackValue(StoreKey.backupTriggerDelay);
|
||||
registerFallbackValue(StoreKey.enableBackup);
|
||||
registerFallbackValue(StoreKey.version);
|
||||
registerFallbackValue(StoreKey.advancedTroubleshooting);
|
||||
|
||||
when(() => mockDriftStoreRepo.getAll()).thenAnswer(
|
||||
(_) async => [
|
||||
const StoreDto(StoreKey.accessToken, _kAccessToken),
|
||||
const StoreDto(StoreKey.enableBackup, _kEnableBackup),
|
||||
const StoreDto(StoreKey.advancedTroubleshooting, _kAdvancedTroubleshooting),
|
||||
const StoreDto(StoreKey.version, _kVersion),
|
||||
],
|
||||
);
|
||||
@@ -46,7 +46,7 @@ void main() {
|
||||
test('Populates the internal cache on init', () {
|
||||
verify(() => mockDriftStoreRepo.getAll()).called(1);
|
||||
expect(sut.tryGet(StoreKey.accessToken), _kAccessToken);
|
||||
expect(sut.tryGet(StoreKey.enableBackup), _kEnableBackup);
|
||||
expect(sut.tryGet(StoreKey.advancedTroubleshooting), _kAdvancedTroubleshooting);
|
||||
expect(sut.tryGet(StoreKey.version), _kVersion);
|
||||
// Other keys should be null
|
||||
expect(sut.tryGet(StoreKey.currentUser), isNull);
|
||||
@@ -147,7 +147,7 @@ void main() {
|
||||
await sut.clear();
|
||||
verify(() => mockDriftStoreRepo.deleteAll()).called(1);
|
||||
expect(sut.tryGet(StoreKey.accessToken), isNull);
|
||||
expect(sut.tryGet(StoreKey.enableBackup), isNull);
|
||||
expect(sut.tryGet(StoreKey.advancedTroubleshooting), isNull);
|
||||
expect(sut.tryGet(StoreKey.version), isNull);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ import '../../fixtures/user.stub.dart';
|
||||
|
||||
const _kTestAccessToken = "#TestToken";
|
||||
const _kTestVersion = 10;
|
||||
const _kTestBackupRequireCharging = false;
|
||||
const _kTestAdvancedTroubleshooting = false;
|
||||
final _kTestUser = UserStub.admin;
|
||||
|
||||
Future<void> _populateStore(Drift db) async {
|
||||
@@ -21,8 +21,8 @@ Future<void> _populateStore(Drift db) async {
|
||||
batch.insert(
|
||||
db.storeEntity,
|
||||
StoreEntityCompanion(
|
||||
id: Value(StoreKey.backupRequireCharging.id),
|
||||
intValue: const Value(_kTestBackupRequireCharging ? 1 : 0),
|
||||
id: Value(StoreKey.advancedTroubleshooting.id),
|
||||
intValue: const Value(_kTestAdvancedTroubleshooting ? 1 : 0),
|
||||
stringValue: const Value(null),
|
||||
),
|
||||
);
|
||||
@@ -76,11 +76,11 @@ void main() {
|
||||
});
|
||||
|
||||
test('converts bool', () async {
|
||||
bool? backupRequireCharging = await sut.tryGet(StoreKey.backupRequireCharging);
|
||||
expect(backupRequireCharging, isNull);
|
||||
await sut.upsert(StoreKey.backupRequireCharging, _kTestBackupRequireCharging);
|
||||
backupRequireCharging = await sut.tryGet(StoreKey.backupRequireCharging);
|
||||
expect(backupRequireCharging, _kTestBackupRequireCharging);
|
||||
bool? advancedTroubleshooting = await sut.tryGet(StoreKey.advancedTroubleshooting);
|
||||
expect(advancedTroubleshooting, isNull);
|
||||
await sut.upsert(StoreKey.advancedTroubleshooting, _kTestAdvancedTroubleshooting);
|
||||
advancedTroubleshooting = await sut.tryGet(StoreKey.advancedTroubleshooting);
|
||||
expect(advancedTroubleshooting, _kTestAdvancedTroubleshooting);
|
||||
});
|
||||
|
||||
test('converts user', () async {
|
||||
@@ -98,11 +98,11 @@ void main() {
|
||||
});
|
||||
|
||||
test('delete()', () async {
|
||||
bool? backupRequireCharging = await sut.tryGet(StoreKey.backupRequireCharging);
|
||||
expect(backupRequireCharging, isFalse);
|
||||
await sut.delete(StoreKey.backupRequireCharging);
|
||||
backupRequireCharging = await sut.tryGet(StoreKey.backupRequireCharging);
|
||||
expect(backupRequireCharging, isNull);
|
||||
bool? advancedTroubleshooting = await sut.tryGet(StoreKey.advancedTroubleshooting);
|
||||
expect(advancedTroubleshooting, isFalse);
|
||||
await sut.delete(StoreKey.advancedTroubleshooting);
|
||||
advancedTroubleshooting = await sut.tryGet(StoreKey.advancedTroubleshooting);
|
||||
expect(advancedTroubleshooting, isNull);
|
||||
});
|
||||
|
||||
test('deleteAll()', () async {
|
||||
@@ -147,13 +147,13 @@ void main() {
|
||||
emitsInOrder([
|
||||
[
|
||||
const StoreDto<Object>(StoreKey.version, _kTestVersion),
|
||||
const StoreDto<Object>(StoreKey.backupRequireCharging, _kTestBackupRequireCharging),
|
||||
const StoreDto<Object>(StoreKey.accessToken, _kTestAccessToken),
|
||||
const StoreDto<Object>(StoreKey.advancedTroubleshooting, _kTestAdvancedTroubleshooting),
|
||||
],
|
||||
[
|
||||
const StoreDto<Object>(StoreKey.version, _kTestVersion + 10),
|
||||
const StoreDto<Object>(StoreKey.backupRequireCharging, _kTestBackupRequireCharging),
|
||||
const StoreDto<Object>(StoreKey.accessToken, _kTestAccessToken),
|
||||
const StoreDto<Object>(StoreKey.advancedTroubleshooting, _kTestAdvancedTroubleshooting),
|
||||
],
|
||||
]),
|
||||
),
|
||||
|
||||
@@ -21,7 +21,6 @@ void main() {
|
||||
late MockApiService apiService;
|
||||
late MockNetworkService networkService;
|
||||
late MockBackgroundSyncManager backgroundSyncManager;
|
||||
late MockAppSettingService appSettingsService;
|
||||
late Drift db;
|
||||
|
||||
setUp(() async {
|
||||
@@ -30,15 +29,12 @@ void main() {
|
||||
apiService = MockApiService();
|
||||
networkService = MockNetworkService();
|
||||
backgroundSyncManager = MockBackgroundSyncManager();
|
||||
appSettingsService = MockAppSettingService();
|
||||
|
||||
sut = AuthService(
|
||||
authApiRepository,
|
||||
authRepository,
|
||||
apiService,
|
||||
networkService,
|
||||
backgroundSyncManager,
|
||||
appSettingsService,
|
||||
);
|
||||
|
||||
registerFallbackValue(Uri());
|
||||
|
||||
@@ -13,11 +13,9 @@ import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/services/background_upload.service.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
import '../domain/service.mock.dart';
|
||||
import '../fixtures/asset.stub.dart';
|
||||
import '../infrastructure/repository.mock.dart';
|
||||
import '../mocks/asset_entity.mock.dart';
|
||||
@@ -29,13 +27,10 @@ void main() {
|
||||
late MockStorageRepository mockStorageRepository;
|
||||
late MockDriftLocalAssetRepository mockLocalAssetRepository;
|
||||
late MockDriftBackupRepository mockBackupRepository;
|
||||
late MockAppSettingsService mockAppSettingsService;
|
||||
late MockAssetMediaRepository mockAssetMediaRepository;
|
||||
late Drift db;
|
||||
|
||||
setUpAll(() async {
|
||||
registerFallbackValue(AppSettingsEnum.useCellularForUploadPhotos);
|
||||
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
|
||||
const MethodChannel('plugins.flutter.io/path_provider'),
|
||||
@@ -54,18 +49,13 @@ void main() {
|
||||
mockStorageRepository = MockStorageRepository();
|
||||
mockLocalAssetRepository = MockDriftLocalAssetRepository();
|
||||
mockBackupRepository = MockDriftBackupRepository();
|
||||
mockAppSettingsService = MockAppSettingsService();
|
||||
mockAssetMediaRepository = MockAssetMediaRepository();
|
||||
|
||||
when(() => mockAppSettingsService.getSetting(AppSettingsEnum.useCellularForUploadVideos)).thenReturn(false);
|
||||
when(() => mockAppSettingsService.getSetting(AppSettingsEnum.useCellularForUploadPhotos)).thenReturn(false);
|
||||
|
||||
sut = BackgroundUploadService(
|
||||
mockUploadRepository,
|
||||
mockStorageRepository,
|
||||
mockLocalAssetRepository,
|
||||
mockBackupRepository,
|
||||
mockAppSettingsService,
|
||||
mockAssetMediaRepository,
|
||||
);
|
||||
|
||||
@@ -181,7 +171,6 @@ void main() {
|
||||
mockStorageRepository,
|
||||
mockLocalAssetRepository,
|
||||
mockBackupRepository,
|
||||
mockAppSettingsService,
|
||||
mockAssetMediaRepository,
|
||||
);
|
||||
addTearDown(() => sutWithV24.dispose());
|
||||
@@ -232,7 +221,6 @@ void main() {
|
||||
mockStorageRepository,
|
||||
mockLocalAssetRepository,
|
||||
mockBackupRepository,
|
||||
mockAppSettingsService,
|
||||
mockAssetMediaRepository,
|
||||
);
|
||||
addTearDown(() => sutAndroid.dispose());
|
||||
@@ -273,7 +261,6 @@ void main() {
|
||||
mockStorageRepository,
|
||||
mockLocalAssetRepository,
|
||||
mockBackupRepository,
|
||||
mockAppSettingsService,
|
||||
mockAssetMediaRepository,
|
||||
);
|
||||
addTearDown(() => sutWithV24.dispose());
|
||||
@@ -314,7 +301,6 @@ void main() {
|
||||
mockStorageRepository,
|
||||
mockLocalAssetRepository,
|
||||
mockBackupRepository,
|
||||
mockAppSettingsService,
|
||||
mockAssetMediaRepository,
|
||||
);
|
||||
addTearDown(() => sutWithV24.dispose());
|
||||
|
||||
@@ -11,9 +11,6 @@
|
||||
"required": true,
|
||||
"in": "query",
|
||||
"description": "Album ID",
|
||||
"x-nestjs_zod-parent-metadata": {
|
||||
"description": "Activity search"
|
||||
},
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$",
|
||||
@@ -25,9 +22,6 @@
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Asset ID (if activity is for an asset)",
|
||||
"x-nestjs_zod-parent-metadata": {
|
||||
"description": "Activity search"
|
||||
},
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$",
|
||||
@@ -38,9 +32,6 @@
|
||||
"name": "level",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"x-nestjs_zod-parent-metadata": {
|
||||
"description": "Activity search"
|
||||
},
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ReactionLevel"
|
||||
}
|
||||
@@ -49,9 +40,6 @@
|
||||
"name": "type",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"x-nestjs_zod-parent-metadata": {
|
||||
"description": "Activity search"
|
||||
},
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ReactionType"
|
||||
}
|
||||
@@ -61,9 +49,6 @@
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Filter by user ID",
|
||||
"x-nestjs_zod-parent-metadata": {
|
||||
"description": "Activity search"
|
||||
},
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$",
|
||||
@@ -187,9 +172,6 @@
|
||||
"required": true,
|
||||
"in": "query",
|
||||
"description": "Album ID",
|
||||
"x-nestjs_zod-parent-metadata": {
|
||||
"description": "Activity"
|
||||
},
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$",
|
||||
@@ -201,9 +183,6 @@
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Asset ID (if activity is for an asset)",
|
||||
"x-nestjs_zod-parent-metadata": {
|
||||
"description": "Activity"
|
||||
},
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$",
|
||||
|
||||
@@ -36,18 +36,16 @@ const ActivityStatisticsResponseSchema = z
|
||||
})
|
||||
.meta({ id: 'ActivityStatisticsResponseDto' });
|
||||
|
||||
const ActivitySchema = z
|
||||
.object({
|
||||
albumId: z.uuidv4().describe('Album ID'),
|
||||
assetId: z.uuidv4().optional().describe('Asset ID (if activity is for an asset)'),
|
||||
})
|
||||
.describe('Activity');
|
||||
const ActivitySchema = z.object({
|
||||
albumId: z.uuidv4().describe('Album ID'),
|
||||
assetId: z.uuidv4().optional().describe('Asset ID (if activity is for an asset)'),
|
||||
});
|
||||
|
||||
const ActivitySearchSchema = ActivitySchema.extend({
|
||||
type: ReactionTypeSchema.optional(),
|
||||
level: ReactionLevelSchema.optional(),
|
||||
userId: z.uuidv4().optional().describe('Filter by user ID'),
|
||||
}).describe('Activity search');
|
||||
});
|
||||
|
||||
const ActivityCreateSchema = ActivitySchema.extend({
|
||||
type: ReactionTypeSchema,
|
||||
|
||||
Reference in New Issue
Block a user