Merge branch 'main' into fix/notification-from-native
This commit is contained in:
@@ -84,8 +84,8 @@ class AssetService {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
Future<List<(String, String)>> getPlaces() {
|
||||
return _remoteAssetRepository.getPlaces();
|
||||
Future<List<(String, String)>> getPlaces(String userId) {
|
||||
return _remoteAssetRepository.getPlaces(userId);
|
||||
}
|
||||
|
||||
Future<(int local, int remote)> getAssetCounts() async {
|
||||
|
||||
@@ -59,7 +59,8 @@ class TimelineFactory {
|
||||
|
||||
TimelineService fromAssets(List<BaseAsset> assets) => TimelineService(_timelineRepository.fromAssets(assets));
|
||||
|
||||
TimelineService map(LatLngBounds bounds) => TimelineService(_timelineRepository.map(bounds, groupBy));
|
||||
TimelineService map(String userId, LatLngBounds bounds) =>
|
||||
TimelineService(_timelineRepository.map(userId, bounds, groupBy));
|
||||
}
|
||||
|
||||
class TimelineService {
|
||||
|
||||
@@ -6,11 +6,12 @@ import 'package:immich_mobile/utils/isolate.dart';
|
||||
import 'package:worker_manager/worker_manager.dart';
|
||||
|
||||
typedef SyncCallback = void Function();
|
||||
typedef SyncCallbackWithResult<T> = void Function(T result);
|
||||
typedef SyncErrorCallback = void Function(String error);
|
||||
|
||||
class BackgroundSyncManager {
|
||||
final SyncCallback? onRemoteSyncStart;
|
||||
final SyncCallback? onRemoteSyncComplete;
|
||||
final SyncCallbackWithResult<bool?>? onRemoteSyncComplete;
|
||||
final SyncErrorCallback? onRemoteSyncError;
|
||||
|
||||
final SyncCallback? onLocalSyncStart;
|
||||
@@ -156,15 +157,18 @@ class BackgroundSyncManager {
|
||||
debugLabel: 'remote-sync',
|
||||
);
|
||||
return _syncTask!
|
||||
.then((result) => result ?? false)
|
||||
.whenComplete(() {
|
||||
onRemoteSyncComplete?.call();
|
||||
_syncTask = null;
|
||||
.then((result) {
|
||||
final success = result ?? false;
|
||||
onRemoteSyncComplete?.call(success);
|
||||
return success;
|
||||
})
|
||||
.catchError((error) {
|
||||
onRemoteSyncError?.call(error.toString());
|
||||
_syncTask = null;
|
||||
return false;
|
||||
})
|
||||
.whenComplete(() {
|
||||
_syncTask = null;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
||||
.toDto(
|
||||
assetCount: row.read(assetCount) ?? 0,
|
||||
ownerName: row.read(_db.userEntity.name)!,
|
||||
isShared: row.read(_db.remoteAlbumUserEntity.userId.count(distinct: true))! > 2,
|
||||
isShared: row.read(_db.remoteAlbumUserEntity.userId.count(distinct: true))! > 0,
|
||||
),
|
||||
)
|
||||
.get();
|
||||
@@ -107,7 +107,7 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
||||
.toDto(
|
||||
assetCount: row.read(assetCount) ?? 0,
|
||||
ownerName: row.read(_db.userEntity.name)!,
|
||||
isShared: row.read(_db.remoteAlbumUserEntity.userId.count(distinct: true))! > 2,
|
||||
isShared: row.read(_db.remoteAlbumUserEntity.userId.count(distinct: true))! > 0,
|
||||
),
|
||||
)
|
||||
.getSingleOrNull();
|
||||
@@ -305,8 +305,9 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
||||
.readTable(_db.remoteAlbumEntity)
|
||||
.toDto(
|
||||
ownerName: row.read(_db.userEntity.name)!,
|
||||
isShared: row.read(_db.remoteAlbumUserEntity.userId.count(distinct: true))! > 2,
|
||||
isShared: row.read(_db.remoteAlbumUserEntity.userId.count(distinct: true))! > 0,
|
||||
);
|
||||
|
||||
return album;
|
||||
}).watchSingleOrNull();
|
||||
}
|
||||
|
||||
@@ -81,9 +81,11 @@ class RemoteAssetRepository extends DriftDatabaseRepository {
|
||||
.getSingleOrNull();
|
||||
}
|
||||
|
||||
Future<List<(String, String)>> getPlaces() {
|
||||
Future<List<(String, String)>> getPlaces(String userId) {
|
||||
final asset = Subquery(
|
||||
_db.remoteAssetEntity.select()..orderBy([(row) => OrderingTerm.desc(row.createdAt)]),
|
||||
_db.remoteAssetEntity.select()
|
||||
..where((row) => row.ownerId.equals(userId))
|
||||
..orderBy([(row) => OrderingTerm.desc(row.createdAt)]),
|
||||
"asset",
|
||||
);
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
||||
await _db.remoteAssetEntity.deleteAll();
|
||||
await _db.remoteExifEntity.deleteAll();
|
||||
await _db.stackEntity.deleteAll();
|
||||
await _db.authUserEntity.deleteAll();
|
||||
await _db.userEntity.deleteAll();
|
||||
await _db.userMetadataEntity.deleteAll();
|
||||
});
|
||||
|
||||
@@ -431,12 +431,16 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
||||
return query.map((row) => row.readTable(_db.remoteAssetEntity).toDto()).get();
|
||||
}
|
||||
|
||||
TimelineQuery map(LatLngBounds bounds, GroupAssetsBy groupBy) => (
|
||||
bucketSource: () => _watchMapBucket(bounds, groupBy: groupBy),
|
||||
assetSource: (offset, count) => _getMapBucketAssets(bounds, offset: offset, count: count),
|
||||
TimelineQuery map(String userId, LatLngBounds bounds, GroupAssetsBy groupBy) => (
|
||||
bucketSource: () => _watchMapBucket(userId, bounds, groupBy: groupBy),
|
||||
assetSource: (offset, count) => _getMapBucketAssets(userId, bounds, offset: offset, count: count),
|
||||
);
|
||||
|
||||
Stream<List<Bucket>> _watchMapBucket(LatLngBounds bounds, {GroupAssetsBy groupBy = GroupAssetsBy.day}) {
|
||||
Stream<List<Bucket>> _watchMapBucket(
|
||||
String userId,
|
||||
LatLngBounds bounds, {
|
||||
GroupAssetsBy groupBy = GroupAssetsBy.day,
|
||||
}) {
|
||||
if (groupBy == GroupAssetsBy.none) {
|
||||
// TODO: Support GroupAssetsBy.none
|
||||
throw UnsupportedError("GroupAssetsBy.none is not supported for _watchMapBucket");
|
||||
@@ -455,7 +459,8 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
||||
),
|
||||
])
|
||||
..where(
|
||||
_db.remoteExifEntity.inBounds(bounds) &
|
||||
_db.remoteAssetEntity.ownerId.equals(userId) &
|
||||
_db.remoteExifEntity.inBounds(bounds) &
|
||||
_db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline) &
|
||||
_db.remoteAssetEntity.deletedAt.isNull(),
|
||||
)
|
||||
@@ -469,7 +474,12 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
||||
}).watch();
|
||||
}
|
||||
|
||||
Future<List<BaseAsset>> _getMapBucketAssets(LatLngBounds bounds, {required int offset, required int count}) {
|
||||
Future<List<BaseAsset>> _getMapBucketAssets(
|
||||
String userId,
|
||||
LatLngBounds bounds, {
|
||||
required int offset,
|
||||
required int count,
|
||||
}) {
|
||||
final query =
|
||||
_db.remoteAssetEntity.select().join([
|
||||
innerJoin(
|
||||
@@ -479,7 +489,8 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
||||
),
|
||||
])
|
||||
..where(
|
||||
_db.remoteExifEntity.inBounds(bounds) &
|
||||
_db.remoteAssetEntity.ownerId.equals(userId) &
|
||||
_db.remoteExifEntity.inBounds(bounds) &
|
||||
_db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline) &
|
||||
_db.remoteAssetEntity.deletedAt.isNull(),
|
||||
)
|
||||
|
||||
@@ -49,9 +49,6 @@ class _DriftBackupPageState extends ConsumerState<DriftBackupPage> {
|
||||
|
||||
ref.read(driftBackupProvider.notifier).updateSyncing(true);
|
||||
syncSuccess = await ref.read(backgroundSyncProvider).syncRemote();
|
||||
ref
|
||||
.read(driftBackupProvider.notifier)
|
||||
.updateError(syncSuccess == true ? BackupError.none : BackupError.syncFailed);
|
||||
ref.read(driftBackupProvider.notifier).updateSyncing(false);
|
||||
|
||||
if (mounted) {
|
||||
@@ -94,7 +91,6 @@ class _DriftBackupPageState extends ConsumerState<DriftBackupPage> {
|
||||
|
||||
if (syncSuccess == false) {
|
||||
Logger("DriftBackupPage").warning("Remote sync did not complete successfully, skipping backup");
|
||||
backupNotifier.updateError(BackupError.syncFailed);
|
||||
return;
|
||||
}
|
||||
await backupNotifier.startBackup(currentUser.id);
|
||||
|
||||
@@ -120,7 +120,6 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState<DriftBackupAlbum
|
||||
return backupNotifier.startBackup(user.id);
|
||||
} else {
|
||||
Logger('DriftBackupAlbumSelectionPage').warning('Background sync failed, not starting backup');
|
||||
backupNotifier.updateError(BackupError.syncFailed);
|
||||
}
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -66,7 +66,6 @@ class DriftBackupOptionsPage extends ConsumerWidget {
|
||||
return backupNotifier.startBackup(currentUser.id);
|
||||
} else {
|
||||
Logger('DriftBackupOptionsPage').warning('Background sync failed, not starting backup');
|
||||
backupNotifier.updateError(BackupError.syncFailed);
|
||||
}
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -141,14 +141,9 @@ class SettingsSubPage extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
context.locale;
|
||||
return SafeArea(
|
||||
bottom: true,
|
||||
top: false,
|
||||
right: true,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(centerTitle: false, title: Text(section.title).tr()),
|
||||
body: section.widget,
|
||||
),
|
||||
return Scaffold(
|
||||
appBar: AppBar(centerTitle: false, title: Text(section.title).tr()),
|
||||
body: section.widget,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
|
||||
if (Store.isBetaTimelineEnabled) {
|
||||
bool syncSuccess = false;
|
||||
await Future.wait([
|
||||
backgroundManager.syncLocal(full: true),
|
||||
backgroundManager.syncLocal(),
|
||||
backgroundManager.syncRemote().then((success) => syncSuccess = success),
|
||||
]);
|
||||
|
||||
@@ -76,7 +76,6 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
|
||||
_resumeBackup(backupProvider),
|
||||
]);
|
||||
} else {
|
||||
backupProvider.updateError(BackupError.syncFailed);
|
||||
await backgroundManager.hashAssets();
|
||||
}
|
||||
|
||||
|
||||
@@ -512,7 +512,7 @@ class _AlbumList extends ConsumerWidget {
|
||||
}
|
||||
|
||||
return SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
padding: const EdgeInsets.only(left: 16.0, right: 16, bottom: 64),
|
||||
sliver: SliverList.builder(
|
||||
itemBuilder: (_, index) {
|
||||
final album = albums[index];
|
||||
|
||||
@@ -186,6 +186,7 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
|
||||
color: context.textTheme.bodyMedium?.color?.withAlpha(155),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 64),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
+9
-6
@@ -11,8 +11,8 @@ import 'package:immich_mobile/providers/infrastructure/people.provider.dart';
|
||||
import 'package:immich_mobile/providers/routes.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/services/api.service.dart';
|
||||
import 'package:immich_mobile/utils/people.utils.dart';
|
||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||
import 'package:immich_mobile/utils/people.utils.dart';
|
||||
|
||||
class SheetPeopleDetails extends ConsumerStatefulWidget {
|
||||
const SheetPeopleDetails({super.key});
|
||||
@@ -158,11 +158,14 @@ class _PeopleAvatar extends StatelessWidget {
|
||||
maxLines: 1,
|
||||
),
|
||||
if (person.birthDate != null)
|
||||
Text(
|
||||
formatAge(person.birthDate!, assetFileCreatedAt),
|
||||
textAlign: TextAlign.center,
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color: context.textTheme.bodyMedium?.color?.withAlpha(175),
|
||||
FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
formatAge(person.birthDate!, assetFileCreatedAt),
|
||||
textAlign: TextAlign.center,
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color: context.textTheme.bodyMedium?.color?.withAlpha(175),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
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';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
|
||||
@@ -15,9 +18,12 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_b
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_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/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
|
||||
class FavoriteBottomSheet extends ConsumerWidget {
|
||||
const FavoriteBottomSheet({super.key});
|
||||
@@ -27,9 +33,42 @@ class FavoriteBottomSheet extends ConsumerWidget {
|
||||
final multiselect = ref.watch(multiSelectProvider);
|
||||
final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash));
|
||||
|
||||
Future<void> addAssetsToAlbum(RemoteAlbum album) async {
|
||||
final selectedAssets = multiselect.selectedAssets;
|
||||
if (selectedAssets.isEmpty) {
|
||||
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 (addedCount != remoteAssets.length) {
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: 'add_to_album_bottom_sheet_already_exists'.t(args: {"album": album.name}),
|
||||
);
|
||||
} else {
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: 'add_to_album_bottom_sheet_added'.t(args: {"album": album.name}),
|
||||
);
|
||||
}
|
||||
|
||||
ref.read(multiSelectProvider.notifier).reset();
|
||||
}
|
||||
|
||||
return BaseBottomSheet(
|
||||
initialChildSize: 0.25,
|
||||
maxChildSize: 0.4,
|
||||
initialChildSize: 0.4,
|
||||
maxChildSize: 0.7,
|
||||
shouldCloseOnMinExtent: false,
|
||||
actions: [
|
||||
const ShareActionButton(source: ActionSource.timeline),
|
||||
@@ -52,6 +91,9 @@ class FavoriteBottomSheet extends ConsumerWidget {
|
||||
const UploadActionButton(source: ActionSource.timeline),
|
||||
],
|
||||
],
|
||||
slivers: multiselect.hasRemote
|
||||
? [const AddToAlbumHeader(), AlbumSelector(onAlbumSelected: addAssetsToAlbum)]
|
||||
: [],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_shee
|
||||
import 'package:immich_mobile/presentation/widgets/map/map.state.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
|
||||
class MapBottomSheet extends StatelessWidget {
|
||||
const MapBottomSheet({super.key});
|
||||
@@ -32,8 +33,13 @@ class _ScopedMapTimeline extends StatelessWidget {
|
||||
return ProviderScope(
|
||||
overrides: [
|
||||
timelineServiceProvider.overrideWith((ref) {
|
||||
final user = ref.watch(currentUserProvider);
|
||||
if (user == null) {
|
||||
throw Exception('User must be logged in to access archive');
|
||||
}
|
||||
|
||||
final bounds = ref.watch(mapStateProvider).bounds;
|
||||
final timelineService = ref.watch(timelineFactoryProvider).map(bounds);
|
||||
final timelineService = ref.watch(timelineFactoryProvider).map(user.id, bounds);
|
||||
ref.onDispose(timelineService.dispose);
|
||||
return timelineService;
|
||||
}),
|
||||
|
||||
@@ -16,7 +16,7 @@ class ThumbnailTile extends ConsumerWidget {
|
||||
this.asset, {
|
||||
this.size = kThumbnailResolution,
|
||||
this.fit = BoxFit.cover,
|
||||
this.showStorageIndicator,
|
||||
this.showStorageIndicator = false,
|
||||
this.lockSelection = false,
|
||||
this.heroOffset,
|
||||
super.key,
|
||||
@@ -25,7 +25,7 @@ class ThumbnailTile extends ConsumerWidget {
|
||||
final BaseAsset? asset;
|
||||
final Size size;
|
||||
final BoxFit fit;
|
||||
final bool? showStorageIndicator;
|
||||
final bool showStorageIndicator;
|
||||
final bool lockSelection;
|
||||
final int? heroOffset;
|
||||
|
||||
@@ -55,7 +55,7 @@ class ThumbnailTile extends ConsumerWidget {
|
||||
: const BoxDecoration();
|
||||
|
||||
final bool storageIndicator =
|
||||
showStorageIndicator ?? ref.watch(settingsProvider.select((s) => s.get(Setting.showStorageIndicator)));
|
||||
ref.watch(settingsProvider.select((s) => s.get(Setting.showStorageIndicator))) && showStorageIndicator;
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
|
||||
@@ -185,6 +185,8 @@ class _Map extends StatelessWidget {
|
||||
initialCameraPosition: initialLocation == null
|
||||
? const CameraPosition(target: LatLng(0, 0), zoom: 0)
|
||||
: CameraPosition(target: initialLocation, zoom: MapUtils.mapZoomToAssetLevel),
|
||||
compassEnabled: false,
|
||||
rotateGesturesEnabled: false,
|
||||
styleString: style,
|
||||
onMapCreated: onMapCreated,
|
||||
onStyleLoadedCallback: onMapReady,
|
||||
|
||||
@@ -14,7 +14,7 @@ class TimelineArgs {
|
||||
final double maxHeight;
|
||||
final double spacing;
|
||||
final int columnCount;
|
||||
final bool? showStorageIndicator;
|
||||
final bool showStorageIndicator;
|
||||
final bool withStack;
|
||||
final GroupAssetsBy? groupBy;
|
||||
|
||||
@@ -23,7 +23,7 @@ class TimelineArgs {
|
||||
required this.maxHeight,
|
||||
this.spacing = kTimelineSpacing,
|
||||
this.columnCount = kTimelineColumnCount,
|
||||
this.showStorageIndicator,
|
||||
this.showStorageIndicator = false,
|
||||
this.withStack = false,
|
||||
this.groupBy,
|
||||
});
|
||||
|
||||
@@ -36,7 +36,7 @@ class Timeline extends StatelessWidget {
|
||||
this.showStorageIndicator = false,
|
||||
this.withStack = false,
|
||||
this.appBar = const ImmichSliverAppBar(floating: true, pinned: false, snap: false),
|
||||
this.bottomSheet = const GeneralBottomSheet(minChildSize: 0.18),
|
||||
this.bottomSheet = const GeneralBottomSheet(minChildSize: 0.23),
|
||||
this.groupBy,
|
||||
this.withScrubber = true,
|
||||
this.snapToMonth = true,
|
||||
|
||||
@@ -161,7 +161,6 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
||||
_resumeBackup(),
|
||||
]);
|
||||
} else {
|
||||
_ref.read(driftBackupProvider.notifier).updateError(BackupError.syncFailed);
|
||||
await _safeRun(backgroundManager.hashAssets(), "hashAssets");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/utils/background_sync.dart';
|
||||
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
||||
import 'package:immich_mobile/providers/sync_status.provider.dart';
|
||||
|
||||
final backgroundSyncProvider = Provider<BackgroundSyncManager>((ref) {
|
||||
final syncStatusNotifier = ref.read(syncStatusProvider.notifier);
|
||||
final backupProvider = ref.read(driftBackupProvider.notifier);
|
||||
|
||||
final manager = BackgroundSyncManager(
|
||||
onRemoteSyncStart: syncStatusNotifier.startRemoteSync,
|
||||
onRemoteSyncComplete: syncStatusNotifier.completeRemoteSync,
|
||||
onRemoteSyncStart: () {
|
||||
syncStatusNotifier.startRemoteSync();
|
||||
backupProvider.updateError(BackupError.none);
|
||||
},
|
||||
onRemoteSyncComplete: (isSuccess) {
|
||||
syncStatusNotifier.completeRemoteSync();
|
||||
backupProvider.updateError(isSuccess == true ? BackupError.none : BackupError.syncFailed);
|
||||
},
|
||||
onRemoteSyncError: syncStatusNotifier.errorRemoteSync,
|
||||
onLocalSyncStart: syncStatusNotifier.startLocalSync,
|
||||
onLocalSyncComplete: syncStatusNotifier.completeLocalSync,
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:immich_mobile/domain/services/asset.service.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
|
||||
final localAssetRepository = Provider<DriftLocalAssetRepository>(
|
||||
(ref) => DriftLocalAssetRepository(ref.watch(driftProvider)),
|
||||
@@ -19,9 +20,13 @@ final assetServiceProvider = Provider(
|
||||
),
|
||||
);
|
||||
|
||||
final placesProvider = FutureProvider<List<(String, String)>>(
|
||||
(ref) => AssetService(
|
||||
remoteAssetRepository: ref.watch(remoteAssetRepositoryProvider),
|
||||
localAssetRepository: ref.watch(localAssetRepository),
|
||||
).getPlaces(),
|
||||
);
|
||||
final placesProvider = FutureProvider<List<(String, String)>>((ref) {
|
||||
final assetService = ref.watch(assetServiceProvider);
|
||||
final auth = ref.watch(currentUserProvider);
|
||||
|
||||
if (auth == null) {
|
||||
return Future.value(const []);
|
||||
}
|
||||
|
||||
return assetService.getPlaces(auth.id);
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart' as asset_entity;
|
||||
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/response_extensions.dart';
|
||||
import 'package:immich_mobile/repositories/asset_api.repository.dart';
|
||||
import 'package:immich_mobile/utils/hash.dart';
|
||||
@@ -72,6 +73,7 @@ class AssetMediaRepository {
|
||||
// TODO: make this more efficient
|
||||
Future<int> shareAssets(List<BaseAsset> assets, BuildContext context) async {
|
||||
final downloadedXFiles = <XFile>[];
|
||||
final tempFiles = <File>[];
|
||||
|
||||
for (var asset in assets) {
|
||||
final localId = (asset is LocalAsset)
|
||||
@@ -82,6 +84,9 @@ class AssetMediaRepository {
|
||||
if (localId != null) {
|
||||
File? f = await AssetEntity(id: localId, width: 1, height: 1, typeInt: 0).originFile;
|
||||
downloadedXFiles.add(XFile(f!.path));
|
||||
if (CurrentPlatform.isIOS) {
|
||||
tempFiles.add(f);
|
||||
}
|
||||
} else if (asset is RemoteAsset) {
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
final name = asset.name;
|
||||
@@ -95,6 +100,7 @@ class AssetMediaRepository {
|
||||
|
||||
await tempFile.writeAsBytes(res.bodyBytes);
|
||||
downloadedXFiles.add(XFile(tempFile.path));
|
||||
tempFiles.add(tempFile);
|
||||
} else {
|
||||
_log.warning("Asset type not supported for sharing: $asset");
|
||||
continue;
|
||||
@@ -113,9 +119,9 @@ class AssetMediaRepository {
|
||||
downloadedXFiles,
|
||||
sharePositionOrigin: Rect.fromPoints(Offset.zero, Offset(size.width / 3, size.height)),
|
||||
).then((result) async {
|
||||
for (var file in downloadedXFiles) {
|
||||
for (var file in tempFiles) {
|
||||
try {
|
||||
await File(file.path).delete();
|
||||
await file.delete();
|
||||
} catch (e) {
|
||||
_log.warning("Failed to delete temporary file: ${file.path}", e);
|
||||
}
|
||||
|
||||
@@ -162,48 +162,32 @@ class _ProfileIndicator extends ConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
const double _kBadgeWidgetSize = 30.0;
|
||||
|
||||
class _BackupIndicator extends ConsumerWidget {
|
||||
const _BackupIndicator();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
const widgetSize = 30.0;
|
||||
final hasError = ref.watch(driftBackupProvider.select((state) => state.error != BackupError.none));
|
||||
final indicatorIcon = hasError
|
||||
? Icon(
|
||||
Icons.warning_rounded,
|
||||
size: 12,
|
||||
color: context.colorScheme.error,
|
||||
semanticLabel: 'backup_controller_page_backup'.tr(),
|
||||
)
|
||||
: _getBackupBadgeIcon(context, ref);
|
||||
final badgeBackground = hasError ? context.colorScheme.errorContainer : context.colorScheme.surfaceContainer;
|
||||
final indicatorIcon = _getBackupBadgeIcon(context, ref);
|
||||
|
||||
return InkWell(
|
||||
onTap: () => context.pushRoute(const DriftBackupRoute()),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||
child: Badge(
|
||||
label: Container(
|
||||
width: widgetSize / 2,
|
||||
height: widgetSize / 2,
|
||||
decoration: BoxDecoration(
|
||||
color: badgeBackground,
|
||||
border: Border.all(color: context.colorScheme.outline.withValues(alpha: .3)),
|
||||
borderRadius: BorderRadius.circular(widgetSize / 2),
|
||||
),
|
||||
child: indicatorIcon,
|
||||
),
|
||||
label: indicatorIcon,
|
||||
backgroundColor: Colors.transparent,
|
||||
alignment: Alignment.bottomRight,
|
||||
isLabelVisible: indicatorIcon != null,
|
||||
offset: const Offset(-2, -12),
|
||||
child: Icon(Icons.backup_rounded, size: widgetSize, color: context.primaryColor),
|
||||
child: Icon(Icons.backup_rounded, size: _kBadgeWidgetSize, color: context.primaryColor),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget? _getBackupBadgeIcon(BuildContext context, WidgetRef ref) {
|
||||
final backupStateStream = ref.watch(settingsProvider).watch(Setting.enableBackup);
|
||||
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));
|
||||
@@ -215,42 +199,76 @@ class _BackupIndicator extends ConsumerWidget {
|
||||
final backupEnabled = snapshot.data ?? false;
|
||||
|
||||
if (!backupEnabled) {
|
||||
return Icon(
|
||||
Icons.cloud_off_rounded,
|
||||
size: 9,
|
||||
color: iconColor,
|
||||
semanticLabel: 'backup_controller_page_backup'.tr(),
|
||||
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 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(
|
||||
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 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()),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BadgeLabel extends StatelessWidget {
|
||||
final Widget indicator;
|
||||
final Color? backgroundColor;
|
||||
|
||||
const _BadgeLabel(this.indicator, {this.backgroundColor});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: _kBadgeWidgetSize / 2,
|
||||
height: _kBadgeWidgetSize / 2,
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor ?? context.colorScheme.surfaceContainer,
|
||||
border: Border.all(color: context.colorScheme.outline.withValues(alpha: .3)),
|
||||
borderRadius: BorderRadius.circular(_kBadgeWidgetSize / 2),
|
||||
),
|
||||
child: indicator,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SyncStatusIndicator extends ConsumerStatefulWidget {
|
||||
const _SyncStatusIndicator();
|
||||
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.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/auth.provider.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
|
||||
@@ -16,10 +14,9 @@ class BetaTimelineListTile extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final betaTimelineValue = ref.watch(appSettingsServiceProvider).getSetting<bool>(AppSettingsEnum.betaTimeline);
|
||||
final serverInfo = ref.watch(serverInfoProvider);
|
||||
final auth = ref.watch(authProvider);
|
||||
|
||||
if (!auth.isAuthenticated || (serverInfo.serverVersion.minor < 136 && kReleaseMode)) {
|
||||
if (!auth.isAuthenticated) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user