feat(mobile): preserve mobile album info on upload (#11965)

* curating assets with albums to upload

* sorting for background backup

* background upload works

* transform fields string array to javascript array

* send json array

* generate sql

* refactor upload callback

* remove albums info from upload payload

* mechanism to create album on album selection

* album creation

* Sync to upload album

* Remove unused service

* unify name changes

* Add mechanism to sync uploaded assets to albums

* Put add to album operation after updating the UI state

* clean up

* background album sync

* add to album in background context

* remove add to album in callback

* refactor

* refactor

* refactor

* fix: make sure all selected albums are selected for building upload candidate

* clean up

* add manual sync button

* lint

* revert server changes

* pr feedback

* revert time filtering

* const

* sync album on manual upload

* linting

* pr feedback and proper time filtering

* wording
This commit is contained in:
Alex 2024-08-26 13:21:19 -05:00 committed by GitHub
parent f4371578f5
commit 6b6d2a6621
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 657 additions and 233 deletions

View file

@ -5,15 +5,21 @@ import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/models/backup/available_album.model.dart';
import 'package:immich_mobile/providers/album/album.provider.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/providers/backup/backup.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
class AlbumInfoCard extends HookConsumerWidget {
final AvailableAlbum album;
const AlbumInfoCard({super.key, required this.album});
const AlbumInfoCard({
super.key,
required this.album,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
@ -21,6 +27,9 @@ class AlbumInfoCard extends HookConsumerWidget {
ref.watch(backupProvider).selectedBackupAlbums.contains(album);
final bool isExcluded =
ref.watch(backupProvider).excludedBackupAlbums.contains(album);
final syncAlbum = ref
.watch(appSettingsServiceProvider)
.getSetting(AppSettingsEnum.syncAlbums);
final isDarkTheme = context.isDarkTheme;
@ -85,6 +94,9 @@ class AlbumInfoCard extends HookConsumerWidget {
ref.read(backupProvider.notifier).removeAlbumForBackup(album);
} else {
ref.read(backupProvider.notifier).addAlbumForBackup(album);
if (syncAlbum) {
ref.read(albumProvider.notifier).createSyncAlbum(album.name);
}
}
},
onDoubleTap: () {

View file

@ -5,9 +5,12 @@ import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/models/backup/available_album.model.dart';
import 'package:immich_mobile/providers/album/album.provider.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/providers/backup/backup.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
class AlbumInfoListTile extends HookConsumerWidget {
@ -21,7 +24,10 @@ class AlbumInfoListTile extends HookConsumerWidget {
ref.watch(backupProvider).selectedBackupAlbums.contains(album);
final bool isExcluded =
ref.watch(backupProvider).excludedBackupAlbums.contains(album);
var assetCount = useState(0);
final assetCount = useState(0);
final syncAlbum = ref
.watch(appSettingsServiceProvider)
.getSetting(AppSettingsEnum.syncAlbums);
useEffect(
() {
@ -98,6 +104,9 @@ class AlbumInfoListTile extends HookConsumerWidget {
ref.read(backupProvider.notifier).removeAlbumForBackup(album);
} else {
ref.read(backupProvider.notifier).addAlbumForBackup(album);
if (syncAlbum) {
ref.read(albumProvider.notifier).createSyncAlbum(album.name);
}
}
},
leading: buildIcon(),

View file

@ -1,9 +1,12 @@
import 'dart:io';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/providers/backup/backup_verification.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/services/asset.service.dart';
import 'package:immich_mobile/widgets/settings/backup_settings/background_settings.dart';
import 'package:immich_mobile/widgets/settings/backup_settings/foreground_settings.dart';
import 'package:immich_mobile/widgets/settings/settings_button_list_tile.dart';
@ -23,7 +26,21 @@ class BackupSettings extends HookConsumerWidget {
useAppSettingsState(AppSettingsEnum.ignoreIcloudAssets);
final isAdvancedTroubleshooting =
useAppSettingsState(AppSettingsEnum.advancedTroubleshooting);
final albumSync = useAppSettingsState(AppSettingsEnum.syncAlbums);
final isCorruptCheckInProgress = ref.watch(backupVerificationProvider);
final isAlbumSyncInProgress = useState(false);
syncAlbums() async {
isAlbumSyncInProgress.value = true;
try {
await ref.read(assetServiceProvider).syncUploadedAssetToAlbums();
} catch (_) {
} finally {
Future.delayed(const Duration(seconds: 1), () {
isAlbumSyncInProgress.value = false;
});
}
}
final backupSettings = [
const ForegroundBackupSettings(),
@ -58,6 +75,23 @@ class BackupSettings extends HookConsumerWidget {
.performBackupCheck(context)
: null,
),
if (albumSync.value)
SettingsButtonListTile(
icon: Icons.photo_album_outlined,
title: 'sync_albums'.tr(),
subtitle: Text(
"sync_albums_manual_subtitle".tr(),
),
buttonText: 'sync_albums'.tr(),
child: isAlbumSyncInProgress.value
? const CircularProgressIndicator.adaptive(
strokeWidth: 2,
)
: ElevatedButton(
onPressed: syncAlbums,
child: Text('sync'.tr()),
),
),
];
return SettingsSubPageScaffold(

View file

@ -9,6 +9,7 @@ class SettingsButtonListTile extends StatelessWidget {
final Widget? subtitle;
final String? subtileText;
final String buttonText;
final Widget? child;
final void Function()? onButtonTap;
const SettingsButtonListTile({
@ -18,6 +19,7 @@ class SettingsButtonListTile extends StatelessWidget {
this.subtileText,
this.subtitle,
required this.buttonText,
this.child,
this.onButtonTap,
super.key,
});
@ -48,7 +50,8 @@ class SettingsButtonListTile extends StatelessWidget {
),
if (subtitle != null) subtitle!,
const SizedBox(height: 6),
ElevatedButton(onPressed: onButtonTap, child: Text(buttonText)),
child ??
ElevatedButton(onPressed: onButtonTap, child: Text(buttonText)),
],
),
);

View file

@ -9,6 +9,9 @@ class SettingsSwitchListTile extends StatelessWidget {
final String? subtitle;
final IconData? icon;
final Function(bool)? onChanged;
final EdgeInsets? contentPadding;
final TextStyle? titleStyle;
final TextStyle? subtitleStyle;
const SettingsSwitchListTile({
required this.valueNotifier,
@ -17,6 +20,9 @@ class SettingsSwitchListTile extends StatelessWidget {
this.icon,
this.enabled = true,
this.onChanged,
this.contentPadding = const EdgeInsets.symmetric(horizontal: 20),
this.titleStyle,
this.subtitleStyle,
super.key,
});
@ -30,7 +36,7 @@ class SettingsSwitchListTile extends StatelessWidget {
}
return SwitchListTile.adaptive(
contentPadding: const EdgeInsets.symmetric(horizontal: 20),
contentPadding: contentPadding,
selectedTileColor: enabled ? null : context.themeData.disabledColor,
value: valueNotifier.value,
onChanged: onSwitchChanged,
@ -45,20 +51,22 @@ class SettingsSwitchListTile extends StatelessWidget {
: null,
title: Text(
title,
style: context.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w500,
color: enabled ? null : context.themeData.disabledColor,
height: 1.5,
),
style: titleStyle ??
context.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w500,
color: enabled ? null : context.themeData.disabledColor,
height: 1.5,
),
),
subtitle: subtitle != null
? Text(
subtitle!,
style: context.textTheme.bodyMedium?.copyWith(
color: enabled
? context.colorScheme.onSurfaceSecondary
: context.themeData.disabledColor,
),
style: subtitleStyle ??
context.textTheme.bodyMedium?.copyWith(
color: enabled
? context.colorScheme.onSurfaceSecondary
: context.themeData.disabledColor,
),
)
: null,
);