mirror of
https://github.com/immich-app/immich
synced 2026-08-15 13:03:57 +00:00
Merge branch 'main' of https://github.com/immich-app/immich into chore/freezed-moderate-difficulty
This commit is contained in:
commit
425c09a64f
127 changed files with 854 additions and 197 deletions
|
|
@ -110,3 +110,17 @@ dart_code_metrics:
|
|||
- prefer-const-border-radius
|
||||
- prefer-declaring-const-constructor
|
||||
- prefer-switch-expression
|
||||
|
||||
# Riverpod
|
||||
- avoid-calling-notifier-members-inside-build
|
||||
- avoid-notifier-constructors
|
||||
- avoid-nullable-async-value-pattern
|
||||
- avoid-public-notifier-properties
|
||||
- avoid-ref-inside-state-dispose
|
||||
- avoid-ref-read-inside-build
|
||||
- avoid-ref-watch-outside-build
|
||||
- avoid-unnecessary-consumer-widgets
|
||||
- dispose-provided-instances
|
||||
- prefer-immutable-provider-arguments
|
||||
- use-ref-and-state-synchronously
|
||||
- use-ref-read-synchronously
|
||||
|
|
|
|||
|
|
@ -152,12 +152,6 @@
|
|||
This is used by the Flutter tool to
|
||||
generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data android:name="flutterEmbedding" android:value="2" />
|
||||
<!-- Disables default WorkManager initialization to use our custom initialization -->
|
||||
<provider
|
||||
android:name="androidx.startup.InitializationProvider"
|
||||
android:authorities="${applicationId}.androidx-startup"
|
||||
tools:node="remove" />
|
||||
|
||||
|
||||
<!-- Widgets -->
|
||||
<receiver
|
||||
|
|
|
|||
|
|
@ -3,17 +3,12 @@ package app.alextran.immich
|
|||
import android.app.Application
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.work.Configuration
|
||||
import androidx.work.WorkManager
|
||||
import app.alextran.immich.background.BackgroundEngineLock
|
||||
import app.alextran.immich.background.BackgroundWorkerApiImpl
|
||||
|
||||
class ImmichApp : Application() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
val config = Configuration.Builder().build()
|
||||
WorkManager.initialize(this, config)
|
||||
// always start BackupWorker after WorkManager init; this fixes the following bug:
|
||||
// After the process is killed (by user or system), the first trigger (taking a new picture) is lost.
|
||||
// Thus, the BackupWorker is not started. If the system kills the process after each initialization
|
||||
// (because of low memory etc.), the backup is never performed.
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ class LocalImageApiImpl: LocalImageApi {
|
|||
requestOptions.version = .current
|
||||
return requestOptions
|
||||
}()
|
||||
private static let maxPixelSize: CGFloat = 16384
|
||||
|
||||
private static let registry = RequestRegistry<ImageRequest>()
|
||||
|
||||
|
|
@ -108,11 +109,16 @@ class LocalImageApiImpl: LocalImageApi {
|
|||
]))
|
||||
}
|
||||
|
||||
let isOriginal = !(width > 0 && height > 0)
|
||||
let targetSize = isOriginal
|
||||
? CGSize(width: Self.maxPixelSize, height: Self.maxPixelSize)
|
||||
: CGSize(width: Double(width), height: Double(height))
|
||||
let contentMode: PHImageContentMode = isOriginal ? .aspectFit : .aspectFill
|
||||
var image: UIImage?
|
||||
Self.imageManager.requestImage(
|
||||
for: asset,
|
||||
targetSize: width > 0 && height > 0 ? CGSize(width: Double(width), height: Double(height)) : PHImageManagerMaximumSize,
|
||||
contentMode: .aspectFill,
|
||||
targetSize: targetSize,
|
||||
contentMode: contentMode,
|
||||
options: Self.requestOptions,
|
||||
resultHandler: { (_image, info) -> Void in
|
||||
image = _image
|
||||
|
|
@ -123,12 +129,38 @@ class LocalImageApiImpl: LocalImageApi {
|
|||
return request.completion(ImageProcessing.cancelledResult)
|
||||
}
|
||||
|
||||
guard let image = image,
|
||||
let cgImage = image.cgImage else {
|
||||
guard let fastImage = image,
|
||||
var cgImage = fastImage.cgImage else {
|
||||
Self.registry.remove(requestId: requestId)
|
||||
return request.completion(.failure(PigeonError(code: "", message: "Could not get pixel data for \(assetId)", details: nil)))
|
||||
}
|
||||
|
||||
// .fast can return larger than the target, so retry with .exact to guarantee the bound.
|
||||
if max(cgImage.width, cgImage.height) > Int(Self.maxPixelSize) {
|
||||
let exactOptions = Self.requestOptions.copy() as! PHImageRequestOptions
|
||||
exactOptions.resizeMode = .exact
|
||||
image = nil
|
||||
Self.imageManager.requestImage(
|
||||
for: asset,
|
||||
targetSize: targetSize,
|
||||
contentMode: contentMode,
|
||||
options: exactOptions,
|
||||
resultHandler: { (_image, info) -> Void in
|
||||
image = _image
|
||||
}
|
||||
)
|
||||
|
||||
if request.isCancelled {
|
||||
return request.completion(ImageProcessing.cancelledResult)
|
||||
}
|
||||
|
||||
guard let exactImage = image?.cgImage else {
|
||||
Self.registry.remove(requestId: requestId)
|
||||
return request.completion(.failure(PigeonError(code: "", message: "Could not resize image for \(assetId)", details: nil)))
|
||||
}
|
||||
cgImage = exactImage
|
||||
}
|
||||
|
||||
if request.isCancelled {
|
||||
return request.completion(ImageProcessing.cancelledResult)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'album.model.freezed.dart';
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'local_album.model.freezed.dart';
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'asset_face.model.freezed.dart';
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:immich_mobile/constants/colors.dart';
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'backup_config.freezed.dart';
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:immich_mobile/utils/semver.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'image_config.freezed.dart';
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:immich_mobile/constants/colors.dart';
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'viewer_config.freezed.dart';
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'exif.model.freezed.dart';
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'ocr.model.freezed.dart';
|
||||
|
|
|
|||
|
|
@ -2,6 +2,11 @@ import 'package:freezed_annotation/freezed_annotation.dart';
|
|||
|
||||
part 'person.model.freezed.dart';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'person.model.freezed.dart';
|
||||
|
||||
// TODO: Remove PersonDto once Isar is removed
|
||||
@freezed
|
||||
abstract class PersonDto with _$PersonDto {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'stack.model.freezed.dart';
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'auth_state.model.freezed.dart';
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:immich_mobile/models/server_info/server_config.model.dart';
|
||||
import 'package:immich_mobile/models/server_info/server_disk_info.model.dart';
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -81,8 +81,8 @@ class _DriftBackupPageState extends ConsumerState<DriftBackupPage> {
|
|||
|
||||
final error = ref.watch(driftBackupProvider.select((p) => p.error));
|
||||
|
||||
final backupNotifier = ref.read(driftBackupProvider.notifier);
|
||||
final backupSyncManager = ref.read(backgroundSyncProvider);
|
||||
final backupNotifier = ref.watch(driftBackupProvider.notifier);
|
||||
final backupSyncManager = ref.watch(backgroundSyncProvider);
|
||||
|
||||
Future<void> startBackup() async {
|
||||
final currentUser = Store.tryGet(StoreKey.currentUser);
|
||||
|
|
@ -143,7 +143,7 @@ class _DriftBackupPageState extends ConsumerState<DriftBackupPage> {
|
|||
onStart: () async => await startBackup(),
|
||||
onStop: () {
|
||||
syncSuccess = null;
|
||||
backupNotifier.stopForegroundBackup();
|
||||
backupNotifier.stopForegroundBackup(reason: "backup button toggled off");
|
||||
},
|
||||
),
|
||||
switch (error) {
|
||||
|
|
@ -321,7 +321,7 @@ class _BackupAlbumSelectionCard extends ConsumerWidget {
|
|||
Widget buildSelectedAlbumName() {
|
||||
String text = "backup_controller_page_backup_selected".tr();
|
||||
final albums = ref
|
||||
.watch(backupAlbumProvider)
|
||||
.read(backupAlbumProvider)
|
||||
.where((album) => album.backupSelection == BackupSelection.selected)
|
||||
.toList();
|
||||
|
||||
|
|
@ -355,7 +355,7 @@ class _BackupAlbumSelectionCard extends ConsumerWidget {
|
|||
Widget buildExcludedAlbumName() {
|
||||
String text = "backup_controller_page_excluded".tr();
|
||||
final albums = ref
|
||||
.watch(backupAlbumProvider)
|
||||
.read(backupAlbumProvider)
|
||||
.where((album) => album.backupSelection == BackupSelection.excluded)
|
||||
.toList();
|
||||
|
||||
|
|
@ -403,6 +403,10 @@ class _BackupAlbumSelectionCard extends ConsumerWidget {
|
|||
trailing: ElevatedButton(
|
||||
onPressed: () async {
|
||||
await context.pushRoute(const DriftBackupAlbumSelectionRoute());
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
final currentUser = ref.read(currentUserProvider);
|
||||
if (currentUser == null) {
|
||||
return;
|
||||
|
|
@ -562,6 +566,11 @@ class _PreparingStatusState extends ConsumerState {
|
|||
final currentUser = ref.read(currentUserProvider);
|
||||
if (currentUser != null && mounted) {
|
||||
await ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id);
|
||||
if (!context.mounted) {
|
||||
timer.cancel();
|
||||
_pollingTimer = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Stop polling if processing count reaches 0
|
||||
final updatedProcessingCount = ref.read(driftBackupProvider.select((p) => p.processingCount));
|
||||
|
|
|
|||
|
|
@ -102,6 +102,9 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState<DriftBackupAlbum
|
|||
onPopInvokedWithResult: (didPop, _) async {
|
||||
if (!didPop) {
|
||||
await _handlePagePopped();
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
final user = ref.read(currentUserProvider);
|
||||
if (user == null) {
|
||||
|
|
@ -110,6 +113,10 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState<DriftBackupAlbum
|
|||
|
||||
final isBackupEnabled = SettingsRepository.instance.appConfig.backup.enabled;
|
||||
await ref.read(driftBackupProvider.notifier).getBackupStatus(user.id);
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
final currentTotalAssetCount = ref.read(driftBackupProvider.select((p) => p.totalCount));
|
||||
final totalChanged = currentTotalAssetCount != _initialTotalAssetCount;
|
||||
final backupNotifier = ref.read(driftBackupProvider.notifier);
|
||||
|
|
@ -119,7 +126,7 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState<DriftBackupAlbum
|
|||
// Waits for hashing to be cancelled before starting a new one
|
||||
unawaited(nativeSync.cancelHashing().whenComplete(() => backgroundSync.hashAssets()));
|
||||
if (isBackupEnabled) {
|
||||
backupNotifier.stopForegroundBackup();
|
||||
backupNotifier.stopForegroundBackup(reason: "backup albums updated");
|
||||
unawaited(
|
||||
backgroundSync.syncRemote().then((success) {
|
||||
if (success) {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ class DriftBackupOptionsPage extends ConsumerWidget {
|
|||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
bool hasPopped = false;
|
||||
final previousBackup = ref.read(appConfigProvider).backup;
|
||||
final previousBackup = ref.watch(appConfigProvider.select((s) => s.backup));
|
||||
final previousCellularForVideos = previousBackup.useCellularForVideos;
|
||||
final previousCellularForPhotos = previousBackup.useCellularForPhotos;
|
||||
return PopScope(
|
||||
|
|
@ -59,7 +59,7 @@ class DriftBackupOptionsPage extends ConsumerWidget {
|
|||
|
||||
final backupNotifier = ref.read(driftBackupProvider.notifier);
|
||||
final backgroundSync = ref.read(backgroundSyncProvider);
|
||||
backupNotifier.stopForegroundBackup();
|
||||
backupNotifier.stopForegroundBackup(reason: "backup settings updated");
|
||||
unawaited(
|
||||
backgroundSync.syncRemote().then((success) {
|
||||
if (success) {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import 'package:auto_route/auto_route.dart';
|
|||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/log.model.dart';
|
||||
import 'package:immich_mobile/domain/services/log.service.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
|
|
@ -13,11 +12,11 @@ import 'package:immich_mobile/routing/router.dart';
|
|||
import 'package:immich_mobile/services/immich_logger.service.dart';
|
||||
|
||||
@RoutePage()
|
||||
class AppLogPage extends HookConsumerWidget {
|
||||
class AppLogPage extends HookWidget {
|
||||
const AppLogPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
Widget build(BuildContext context) {
|
||||
final immichLogger = LogService.I;
|
||||
final shouldReload = useState(false);
|
||||
final logMessages = useFuture(useMemoized(() => immichLogger.getMessages(), [shouldReload.value]));
|
||||
|
|
|
|||
|
|
@ -4,18 +4,18 @@ import 'package:auto_route/auto_route.dart';
|
|||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:immich_mobile/domain/models/log.model.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
|
||||
@RoutePage()
|
||||
class AppLogDetailPage extends HookConsumerWidget {
|
||||
class AppLogDetailPage extends HookWidget {
|
||||
const AppLogDetailPage({super.key, required this.logMessage});
|
||||
|
||||
final LogMessage logMessage;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
Widget build(BuildContext context) {
|
||||
Padding buildTextWithCopyButton(String header, String text) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ class DownloadPanel extends ConsumerWidget {
|
|||
final tasks = ref.watch(downloadStateProvider.select((state) => state.taskProgress)).entries.toList();
|
||||
|
||||
void onCancelDownload(String id) {
|
||||
unawaited(ref.watch(downloadStateProvider.notifier).cancelDownload(id));
|
||||
unawaited(ref.read(downloadStateProvider.notifier).cancelDownload(id));
|
||||
}
|
||||
|
||||
return Positioned(
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ class HeaderSettingsPage extends HookConsumerWidget {
|
|||
final headers = useState<List<SettingsHeader>>([]);
|
||||
final setInitialHeaders = useState(false);
|
||||
|
||||
final storedHeaders = ref.read(appConfigProvider).network.customHeaders;
|
||||
final storedHeaders = ref.watch(appConfigProvider.select((s) => s.network.customHeaders));
|
||||
if (!setInitialHeaders.value) {
|
||||
storedHeaders.forEach((k, v) {
|
||||
final header = SettingsHeader();
|
||||
|
|
@ -93,8 +93,9 @@ class HeaderSettingsPage extends HookConsumerWidget {
|
|||
headersMap[key] = value;
|
||||
}
|
||||
|
||||
final apiService = ref.read(apiServiceProvider);
|
||||
await ref.read(settingsProvider).write(.networkCustomHeaders, headersMap);
|
||||
await ref.read(apiServiceProvider).updateHeaders();
|
||||
await apiService.updateHeaders();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -353,11 +353,11 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
|
|||
},
|
||||
onError: (exception) {
|
||||
log.severe('Failed to update auth info with access token: $accessToken');
|
||||
unawaited(ref.read(authProvider.notifier).logout());
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
unawaited(ref.read(authProvider.notifier).logout());
|
||||
unawaited(context.router.replaceAll([const LoginRoute()]));
|
||||
},
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,14 +1,11 @@
|
|||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/widgets/forms/change_password_form.dart';
|
||||
|
||||
@RoutePage()
|
||||
class ChangePasswordPage extends HookConsumerWidget {
|
||||
class ChangePasswordPage extends StatelessWidget {
|
||||
const ChangePasswordPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return const Scaffold(body: ChangePasswordForm());
|
||||
}
|
||||
Widget build(BuildContext context) => const Scaffold(body: ChangePasswordForm());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import 'dart:async';
|
|||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
|
|
@ -11,11 +10,11 @@ import 'package:immich_mobile/widgets/forms/login/login_form.dart';
|
|||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
|
||||
@RoutePage()
|
||||
class LoginPage extends HookConsumerWidget {
|
||||
class LoginPage extends HookWidget {
|
||||
const LoginPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
Widget build(BuildContext context) {
|
||||
final appVersion = useState('0.0.0');
|
||||
|
||||
Future<void> getAppInfo() async {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import 'package:auto_route/auto_route.dart';
|
|||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/maplibrecontroller_extensions.dart';
|
||||
|
|
@ -14,13 +13,13 @@ import 'package:immich_mobile/widgets/map/map_theme_override.dart';
|
|||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
|
||||
@RoutePage()
|
||||
class MapLocationPickerPage extends HookConsumerWidget {
|
||||
class MapLocationPickerPage extends HookWidget {
|
||||
final LatLng initialLatLng;
|
||||
|
||||
const MapLocationPickerPage({super.key, this.initialLatLng = const LatLng(0, 0)});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
Widget build(BuildContext context) {
|
||||
final selectedLatLng = useValueNotifier<LatLng>(initialLatLng);
|
||||
final controller = useRef<MapLibreMapController?>(null);
|
||||
final marker = useRef<Symbol?>(null);
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ class DownloadInfoPage extends ConsumerWidget {
|
|||
final tasks = ref.watch(downloadStateProvider.select((state) => state.taskProgress)).entries.toList();
|
||||
|
||||
void onCancelDownload(String id) {
|
||||
unawaited(ref.watch(downloadStateProvider.notifier).cancelDownload(id));
|
||||
unawaited(ref.read(downloadStateProvider.notifier).cancelDownload(id));
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ class DriftActivitiesPage extends HookConsumerWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final activityNotifier = ref.read(albumActivityProvider((album.id, assetId)).notifier);
|
||||
final activityNotifier = ref.watch(albumActivityProvider((album.id, assetId)).notifier);
|
||||
final activities = ref.watch(albumActivityProvider((album.id, assetId)));
|
||||
final listViewScrollController = useScrollController();
|
||||
|
||||
|
|
|
|||
|
|
@ -75,6 +75,10 @@ class DriftAlbumOptionsPage extends HookConsumerWidget {
|
|||
}
|
||||
|
||||
try {
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
await ref.read(remoteAlbumProvider.notifier).addUsers(album.id, newUsers);
|
||||
ref.invalidate(remoteAlbumSharedUsersProvider(album.id));
|
||||
if (!context.mounted) {
|
||||
|
|
@ -137,7 +141,7 @@ class DriftAlbumOptionsPage extends HookConsumerWidget {
|
|||
|
||||
Widget buildOwnerInfo() {
|
||||
if (isOwner) {
|
||||
final owner = ref.watch(currentUserProvider);
|
||||
final owner = ref.read(currentUserProvider);
|
||||
return ListTile(
|
||||
leading: owner != null ? UserCircleAvatar(user: owner) : const SizedBox(),
|
||||
title: Text(album.ownerName, style: const TextStyle(fontWeight: FontWeight.w500)),
|
||||
|
|
@ -145,7 +149,7 @@ class DriftAlbumOptionsPage extends HookConsumerWidget {
|
|||
trailing: Text("owner", style: context.textTheme.labelLarge).t(context: context),
|
||||
);
|
||||
} else {
|
||||
final usersProvider = ref.watch(driftUsersProvider);
|
||||
final usersProvider = ref.read(driftUsersProvider);
|
||||
return usersProvider.maybeWhen(
|
||||
data: (users) {
|
||||
final user = users.firstWhereOrNull((u) => u.id == album.ownerId);
|
||||
|
|
|
|||
|
|
@ -7,12 +7,12 @@ import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
|||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||
|
||||
@RoutePage()
|
||||
class DriftAssetSelectionTimelinePage extends ConsumerWidget {
|
||||
class DriftAssetSelectionTimelinePage extends StatelessWidget {
|
||||
final Set<BaseAsset> lockedSelectionAssets;
|
||||
const DriftAssetSelectionTimelinePage({super.key, this.lockedSelectionAssets = const {}});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
Widget build(BuildContext context) {
|
||||
return ProviderScope(
|
||||
overrides: [
|
||||
multiSelectProvider.overrideWith(
|
||||
|
|
|
|||
|
|
@ -10,13 +10,13 @@ import 'package:immich_mobile/extensions/platform_extensions.dart';
|
|||
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||
|
||||
@RoutePage()
|
||||
class AssetTroubleshootPage extends ConsumerWidget {
|
||||
class AssetTroubleshootPage extends StatelessWidget {
|
||||
final BaseAsset asset;
|
||||
|
||||
const AssetTroubleshootPage({super.key, required this.asset});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text('asset_troubleshoot'.tr())),
|
||||
body: SingleChildScrollView(
|
||||
|
|
@ -29,13 +29,13 @@ class AssetTroubleshootPage extends ConsumerWidget {
|
|||
}
|
||||
}
|
||||
|
||||
class _AssetDetailsView extends ConsumerWidget {
|
||||
class _AssetDetailsView extends StatelessWidget {
|
||||
final BaseAsset asset;
|
||||
|
||||
const _AssetDetailsView({required this.asset});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
|
|
|||
|
|
@ -21,11 +21,11 @@ import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
|
|||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
|
||||
@RoutePage()
|
||||
class DriftLibraryPage extends ConsumerWidget {
|
||||
class DriftLibraryPage extends StatelessWidget {
|
||||
const DriftLibraryPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
Widget build(BuildContext context) {
|
||||
return const Scaffold(
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
|
|
|
|||
|
|
@ -76,6 +76,10 @@ class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
|
|||
}
|
||||
|
||||
try {
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
await ref.read(remoteAlbumProvider.notifier).addUsers(_album.id, newUsers);
|
||||
ref.invalidate(remoteAlbumSharedUsersProvider(_album.id));
|
||||
if (!context.mounted) {
|
||||
|
|
@ -133,6 +137,10 @@ class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
|
|||
|
||||
if (confirmed == true) {
|
||||
try {
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
await ref.read(remoteAlbumProvider.notifier).deleteAlbum(_album.id);
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
|
|
@ -431,7 +439,7 @@ class _AlbumKebabMenu extends ConsumerWidget {
|
|||
|
||||
return FutureBuilder<bool>(
|
||||
future: ref
|
||||
.read(remoteAlbumServiceProvider)
|
||||
.watch(remoteAlbumServiceProvider)
|
||||
.getUserRole(album.id, user?.id ?? '')
|
||||
.then((role) => role == AlbumUserRole.editor),
|
||||
builder: (context, snapshot) {
|
||||
|
|
|
|||
|
|
@ -300,7 +300,7 @@ class _DriftSlideshowPageState extends ConsumerState<DriftSlideshowPage> with Si
|
|||
borderRadius: BorderRadius.zero,
|
||||
minHeight: 5,
|
||||
value:
|
||||
ref.watch(videoPlayerProvider(asset.heroTag).select((s) => s.position)).inMilliseconds /
|
||||
ref.read(videoPlayerProvider(asset.heroTag).select((s) => s.position)).inMilliseconds /
|
||||
asset.duration.inMilliseconds,
|
||||
);
|
||||
}
|
||||
|
|
@ -374,7 +374,7 @@ class _DriftSlideshowPageState extends ConsumerState<DriftSlideshowPage> with Si
|
|||
builder: (context, value, _) => buildPhotoView(scale * (1.0 + value * _kenBurnsZoom)),
|
||||
);
|
||||
} else {
|
||||
final status = ref.watch(videoPlayerProvider(asset.heroTag).select((s) => s.status));
|
||||
final status = ref.read(videoPlayerProvider(asset.heroTag).select((s) => s.status));
|
||||
final position = ref.read(videoPlayerProvider(asset.heroTag)).position;
|
||||
|
||||
if (status == VideoPlaybackStatus.completed && isCurrent && position.inMicroseconds > 0) {
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@ class DriftUserSelectionPage extends HookConsumerWidget {
|
|||
body: suggestedShareUsers.widgetWhen(
|
||||
onData: (users) {
|
||||
// Get shared users for this album from the database
|
||||
final sharedUsers = ref.watch(remoteAlbumSharedUsersProvider(album.id));
|
||||
final sharedUsers = ref.read(remoteAlbumSharedUsersProvider(album.id));
|
||||
|
||||
return sharedUsers.when(
|
||||
data: (albumSharedUsers) {
|
||||
|
|
|
|||
|
|
@ -220,7 +220,7 @@ class _AspectRatioSelector extends ConsumerWidget {
|
|||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final editorState = ref.watch(editorStateProvider);
|
||||
final editorNotifier = ref.read(editorStateProvider.notifier);
|
||||
final editorNotifier = ref.watch(editorStateProvider.notifier);
|
||||
|
||||
return SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
|
|
@ -245,7 +245,7 @@ class _TransformControls extends ConsumerWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final editorNotifier = ref.read(editorStateProvider.notifier);
|
||||
final editorNotifier = ref.watch(editorStateProvider.notifier);
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
|
|
@ -333,7 +333,7 @@ class _ResetEditsButton extends ConsumerWidget {
|
|||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final editorState = ref.watch(editorStateProvider);
|
||||
final editorNotifier = ref.read(editorStateProvider.notifier);
|
||||
final editorNotifier = ref.watch(editorStateProvider.notifier);
|
||||
|
||||
return ImmichTextButton(
|
||||
labelText: 'reset'.tr(),
|
||||
|
|
@ -384,7 +384,7 @@ class _EditorPreviewState extends ConsumerState<_EditorPreview> with TickerProvi
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final editorState = ref.watch(editorStateProvider);
|
||||
final editorNotifier = ref.read(editorStateProvider.notifier);
|
||||
final editorNotifier = ref.watch(editorStateProvider.notifier);
|
||||
|
||||
ref.listen(editorStateProvider, (previous, current) {
|
||||
// Only re-apply the aspect ratio when it changes, otherwise the crop rect will shrink on every rotation
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
|
|
|||
|
|
@ -70,6 +70,10 @@ class _ProfilePictureCropPageState extends ConsumerState<ProfilePictureCropPage>
|
|||
final croppedImage = await _cropController.croppedImage();
|
||||
final pngBytes = await imageToUint8List(croppedImage);
|
||||
final xFile = XFile.fromData(pngBytes, mimeType: 'image/png');
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
final success = await ref
|
||||
.read(uploadProfileImageProvider.notifier)
|
||||
.upload(xFile, fileName: 'profile-picture.png');
|
||||
|
|
|
|||
|
|
@ -749,7 +749,7 @@ class _SearchResultGrid extends ConsumerWidget {
|
|||
}
|
||||
|
||||
Widget? _bottomWidget(BuildContext context, WidgetRef ref) {
|
||||
final isLoading = ref.watch(paginatedSearchProvider.select((s) => s.isLoading));
|
||||
final isLoading = ref.read(paginatedSearchProvider.select((s) => s.isLoading));
|
||||
|
||||
if (isLoading) {
|
||||
return const SliverFillRemaining(
|
||||
|
|
@ -761,7 +761,7 @@ class _SearchResultGrid extends ConsumerWidget {
|
|||
);
|
||||
}
|
||||
|
||||
final hasMore = ref.watch(paginatedSearchProvider.select((s) => s.nextPage != null));
|
||||
final hasMore = ref.read(paginatedSearchProvider.select((s) => s.nextPage != null));
|
||||
|
||||
if (hasMore) {
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
|
||||
class BaseActionButton extends ConsumerWidget {
|
||||
class BaseActionButton extends StatelessWidget {
|
||||
const BaseActionButton({
|
||||
super.key,
|
||||
required this.label,
|
||||
|
|
@ -31,7 +30,7 @@ class BaseActionButton extends ConsumerWidget {
|
|||
final void Function()? onLongPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
Widget build(BuildContext context) {
|
||||
final miniWidth = minWidth ?? (context.isMobile ? context.width / 4.5 : 75.0);
|
||||
final iconTheme = IconTheme.of(context);
|
||||
final iconSize = iconTheme.size ?? 24.0;
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ class MotionPhotoActionButton extends ConsumerWidget {
|
|||
return BaseActionButton(
|
||||
iconData: isPlaying ? Icons.motion_photos_pause_outlined : Icons.play_circle_outline_rounded,
|
||||
label: "play_motion_photo".t(context: context),
|
||||
onPressed: ref.read(isPlayingMotionVideoProvider.notifier).toggle,
|
||||
onPressed: ref.watch(isPlayingMotionVideoProvider.notifier).toggle,
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -669,7 +669,7 @@ class _GridAlbumCard extends ConsumerWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final albumThumbnailAsset = ref.read(assetServiceProvider).getRemoteAsset(album.thumbnailAssetId ?? "");
|
||||
final albumThumbnailAsset = ref.watch(assetServiceProvider).getRemoteAsset(album.thumbnailAssetId ?? "");
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => onAlbumSelected(album),
|
||||
|
|
@ -746,7 +746,7 @@ class AddToAlbumHeader extends ConsumerWidget {
|
|||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
Future<void> onCreateAlbum() async {
|
||||
final albumName = await showDialog<String?>(context: context, builder: (context) => const NewAlbumNameModal());
|
||||
if (albumName == null) {
|
||||
if (albumName == null || !context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ class AlbumTile extends ConsumerWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final albumThumbnailAsset = ref.read(assetServiceProvider).getRemoteAsset(album.thumbnailAssetId ?? "");
|
||||
final albumThumbnailAsset = ref.watch(assetServiceProvider).getRemoteAsset(album.thumbnailAssetId ?? "");
|
||||
|
||||
return LargeLeadingTile(
|
||||
title: Text(
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ class TechnicalDetails extends ConsumerWidget {
|
|||
final subtitleStyle = context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary);
|
||||
|
||||
if (asset is LocalAsset) {
|
||||
final assetMediaRepository = ref.watch(assetMediaRepositoryProvider);
|
||||
final assetMediaRepository = ref.read(assetMediaRepositoryProvider);
|
||||
return FutureBuilder<String?>(
|
||||
future: assetMediaRepository.getOriginalFilename(asset.id),
|
||||
builder: (context, snapshot) {
|
||||
|
|
|
|||
|
|
@ -401,7 +401,7 @@ class _AssetPageState extends ConsumerState<AssetPage> {
|
|||
_showingDetails = ref.watch(assetViewerProvider.select((s) => s.showingDetails));
|
||||
final stackIndex = ref.watch(assetViewerProvider.select((s) => s.stackIndex));
|
||||
final isPlayingMotionVideo = ref.watch(isPlayingMotionVideoProvider);
|
||||
final timelineOrigin = ref.read(timelineServiceProvider).origin;
|
||||
final timelineOrigin = ref.watch(timelineServiceProvider).origin;
|
||||
final showingOcr = ref.watch(assetViewerProvider.select((s) => s.showingOcr));
|
||||
|
||||
final asset = _asset;
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ class AssetStackRow extends ConsumerWidget {
|
|||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final hideAssetStack = ref.read(timelineServiceProvider).origin == TimelineOrigin.trash;
|
||||
final hideAssetStack = ref.watch(timelineServiceProvider).origin == TimelineOrigin.trash;
|
||||
if (hideAssetStack) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ class ViewerBottomBar extends ConsumerWidget {
|
|||
final isReadonlyModeEnabled = ref.watch(readonlyModeProvider);
|
||||
final showingDetails = ref.watch(assetViewerProvider.select((s) => s.showingDetails));
|
||||
final isInLockedView = ref.watch(inLockedViewProvider);
|
||||
final isInTrash = ref.read(timelineServiceProvider).origin == TimelineOrigin.trash;
|
||||
final isInTrash = ref.watch(timelineServiceProvider).origin == TimelineOrigin.trash;
|
||||
|
||||
final originalTheme = context.themeData;
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ class MotionPhotoPlayButton extends ConsumerWidget {
|
|||
child: Center(
|
||||
child: _MotionButton(
|
||||
isPlaying: isPlaying,
|
||||
onPressed: ref.read(isPlayingMotionVideoProvider.notifier).toggle,
|
||||
onPressed: ref.watch(isPlayingMotionVideoProvider.notifier).toggle,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ class OcrToggleButton extends ConsumerWidget {
|
|||
shape: const CircleBorder(),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: InkWell(
|
||||
onTap: ref.read(assetViewerProvider.notifier).toggleOcr,
|
||||
onTap: ref.watch(assetViewerProvider.notifier).toggleOcr,
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(10.0),
|
||||
child: Icon(Icons.text_fields_rounded, size: 22, color: Colors.white),
|
||||
|
|
|
|||
|
|
@ -150,6 +150,10 @@ class _NativeVideoViewerState extends ConsumerState<NativeVideoViewer> with Widg
|
|||
final remoteAsset = videoAsset as RemoteAsset;
|
||||
|
||||
final serverEndpoint = Store.get(StoreKey.serverEndpoint);
|
||||
if (!context.mounted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final isOriginalVideo = ref.read(appConfigProvider).viewer.loadOriginalVideo;
|
||||
final String postfixUrl = isOriginalVideo ? 'original' : 'video/playback';
|
||||
final String assetId = remoteAsset.livePhotoVideoId ?? remoteAsset.id;
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ class ViewerKebabMenu extends ConsumerWidget {
|
|||
final user = ref.watch(currentUserProvider);
|
||||
final isOwner = asset is RemoteAsset && asset.ownerId == user?.id;
|
||||
final isCasting = ref.watch(castProvider.select((c) => c.isCasting));
|
||||
final timelineOrigin = ref.read(timelineServiceProvider).origin;
|
||||
final timelineOrigin = ref.watch(timelineServiceProvider).origin;
|
||||
final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash));
|
||||
final isInLockedView = ref.watch(inLockedViewProvider);
|
||||
final currentAlbum = ref.watch(currentRemoteAlbumProvider);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.widget.dart';
|
||||
import 'package:immich_mobile/presentation/actions/delete.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/download.action.dart';
|
||||
|
|
@ -7,11 +6,11 @@ import 'package:immich_mobile/presentation/actions/lock.action.dart';
|
|||
import 'package:immich_mobile/presentation/actions/share.action.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
||||
|
||||
class LockedFolderBottomSheet extends ConsumerWidget {
|
||||
class LockedFolderBottomSheet extends StatelessWidget {
|
||||
const LockedFolderBottomSheet({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
Widget build(BuildContext context) {
|
||||
return const BaseBottomSheet(
|
||||
initialChildSize: 0.25,
|
||||
maxChildSize: 0.4,
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
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/presentation/actions/action.widget.dart';
|
||||
import 'package:immich_mobile/presentation/actions/delete.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/restore.action.dart';
|
||||
|
||||
class TrashBottomBar extends ConsumerWidget {
|
||||
class TrashBottomBar extends StatelessWidget {
|
||||
const TrashBottomBar({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
Widget build(BuildContext context) {
|
||||
return Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Container(
|
||||
|
|
|
|||
|
|
@ -162,7 +162,15 @@ ImageProvider getFullImageProvider(
|
|||
provider = FileImage(File(localFilePath));
|
||||
} else if (_shouldUseLocalAsset(asset)) {
|
||||
final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).localId!;
|
||||
provider = LocalFullImageProvider(id: id, size: size, assetType: asset.type, isAnimated: asset.isAnimatedImage);
|
||||
provider = LocalFullImageProvider(
|
||||
id: id,
|
||||
size: size,
|
||||
assetType: asset.type,
|
||||
isAnimated: asset.isAnimatedImage,
|
||||
width: asset.width,
|
||||
height: asset.height,
|
||||
checksum: asset.checksum,
|
||||
);
|
||||
} else {
|
||||
final String assetId;
|
||||
final String thumbhash;
|
||||
|
|
@ -190,7 +198,7 @@ ImageProvider getFullImageProvider(
|
|||
ImageProvider? getThumbnailImageProvider(BaseAsset asset, {Size size = kThumbnailResolution, bool edited = true}) {
|
||||
if (_shouldUseLocalAsset(asset)) {
|
||||
final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).localId!;
|
||||
return LocalThumbProvider(id: id, size: size, assetType: asset.type);
|
||||
return LocalThumbProvider(id: id, size: size, assetType: asset.type, checksum: asset.checksum);
|
||||
}
|
||||
|
||||
final assetId = asset is RemoteAsset ? asset.id : (asset as LocalAsset).remoteId;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
|
|
@ -8,13 +10,19 @@ import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
|
|||
import 'package:immich_mobile/presentation/widgets/images/one_frame_multi_image_stream_completer.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
|
||||
|
||||
// iOS GPU textures max out at 16384px; larger images squish.
|
||||
const _kMaxPixelSize = 16384;
|
||||
|
||||
class LocalThumbProvider extends CancellableImageProvider<LocalThumbProvider>
|
||||
with CancellableImageProviderMixin<LocalThumbProvider> {
|
||||
final String id;
|
||||
final Size size;
|
||||
final AssetType assetType;
|
||||
|
||||
LocalThumbProvider({required this.id, required this.assetType, this.size = kThumbnailResolution});
|
||||
// an edit on the device keeps the id and changes the bytes, so the checksum is what separates two renders
|
||||
final String? checksum;
|
||||
|
||||
LocalThumbProvider({required this.id, required this.assetType, this.checksum, this.size = kThumbnailResolution});
|
||||
|
||||
@override
|
||||
Future<LocalThumbProvider> obtainKey(ImageConfiguration configuration) {
|
||||
|
|
@ -44,13 +52,13 @@ class LocalThumbProvider extends CancellableImageProvider<LocalThumbProvider>
|
|||
return true;
|
||||
}
|
||||
if (other is LocalThumbProvider) {
|
||||
return id == other.id;
|
||||
return id == other.id && checksum == other.checksum;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => id.hashCode;
|
||||
int get hashCode => Object.hash(id, checksum);
|
||||
}
|
||||
|
||||
class LocalFullImageProvider extends CancellableImageProvider<LocalFullImageProvider>
|
||||
|
|
@ -59,8 +67,38 @@ class LocalFullImageProvider extends CancellableImageProvider<LocalFullImageProv
|
|||
final Size size;
|
||||
final AssetType assetType;
|
||||
final bool isAnimated;
|
||||
final int? width;
|
||||
final int? height;
|
||||
final String? checksum;
|
||||
|
||||
LocalFullImageProvider({required this.id, required this.assetType, required this.size, required this.isAnimated});
|
||||
LocalFullImageProvider({
|
||||
required this.id,
|
||||
required this.assetType,
|
||||
required this.size,
|
||||
required this.isAnimated,
|
||||
this.width,
|
||||
this.height,
|
||||
this.checksum,
|
||||
});
|
||||
|
||||
Size _previewTarget(double dpr, bool previewIsFinal) =>
|
||||
previewTargetSize(size.width * dpr, size.height * dpr, width, height, previewIsFinal: previewIsFinal);
|
||||
|
||||
// Use an aspect-correct target when aspectFill would exceed the texture limit.
|
||||
@visibleForTesting
|
||||
static Size previewTargetSize(double boxW, double boxH, int? width, int? height, {required bool previewIsFinal}) {
|
||||
if (width == null || height == null || width <= 0 || height <= 0) {
|
||||
return Size(boxW, boxH);
|
||||
}
|
||||
final imgLong = math.max(width, height).toDouble();
|
||||
final coverLong = imgLong * math.max(boxW / width, boxH / height);
|
||||
if (coverLong <= _kMaxPixelSize) {
|
||||
return Size(boxW, boxH);
|
||||
}
|
||||
final bound = previewIsFinal ? _kMaxPixelSize.toDouble() : math.max(boxW, boxH);
|
||||
final scale = math.min(1.0, bound / imgLong);
|
||||
return Size(math.max(1.0, width * scale), math.max(1.0, height * scale));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<LocalFullImageProvider> obtainKey(ImageConfiguration configuration) {
|
||||
|
|
@ -73,7 +111,7 @@ class LocalFullImageProvider extends CancellableImageProvider<LocalFullImageProv
|
|||
return AnimatedImageStreamCompleter(
|
||||
stream: _animatedCodec(key, decode),
|
||||
scale: 1.0,
|
||||
initialImage: getInitialImage(LocalThumbProvider(id: key.id, assetType: key.assetType)),
|
||||
initialImage: getInitialImage(LocalThumbProvider(id: key.id, assetType: key.assetType, checksum: key.checksum)),
|
||||
informationCollector: () => <DiagnosticsNode>[
|
||||
DiagnosticsProperty<ImageProvider>('Image provider', this),
|
||||
DiagnosticsProperty<String>('Id', key.id),
|
||||
|
|
@ -86,7 +124,7 @@ class LocalFullImageProvider extends CancellableImageProvider<LocalFullImageProv
|
|||
|
||||
return OneFramePlaceholderImageStreamCompleter(
|
||||
_codec(key, decode),
|
||||
initialImage: getInitialImage(LocalThumbProvider(id: key.id, assetType: key.assetType)),
|
||||
initialImage: getInitialImage(LocalThumbProvider(id: key.id, assetType: key.assetType, checksum: key.checksum)),
|
||||
informationCollector: () => <DiagnosticsNode>[
|
||||
DiagnosticsProperty<ImageProvider>('Image provider', this),
|
||||
DiagnosticsProperty<String>('Id', key.id),
|
||||
|
|
@ -108,7 +146,7 @@ class LocalFullImageProvider extends CancellableImageProvider<LocalFullImageProv
|
|||
final devicePixelRatio = PlatformDispatcher.instance.views.first.devicePixelRatio;
|
||||
var request = this.request = LocalImageRequest(
|
||||
localId: key.id,
|
||||
size: Size(size.width * devicePixelRatio, size.height * devicePixelRatio),
|
||||
size: _previewTarget(devicePixelRatio, !loadOriginal),
|
||||
assetType: key.assetType,
|
||||
);
|
||||
yield* loadRequest(request, decode, isFinal: !loadOriginal);
|
||||
|
|
@ -136,7 +174,7 @@ class LocalFullImageProvider extends CancellableImageProvider<LocalFullImageProv
|
|||
final devicePixelRatio = PlatformDispatcher.instance.views.first.devicePixelRatio;
|
||||
final previewRequest = request = LocalImageRequest(
|
||||
localId: key.id,
|
||||
size: Size(size.width * devicePixelRatio, size.height * devicePixelRatio),
|
||||
size: _previewTarget(devicePixelRatio, false),
|
||||
assetType: key.assetType,
|
||||
);
|
||||
yield* loadRequest(previewRequest, decode, isFinal: false);
|
||||
|
|
@ -163,11 +201,16 @@ class LocalFullImageProvider extends CancellableImageProvider<LocalFullImageProv
|
|||
return true;
|
||||
}
|
||||
if (other is LocalFullImageProvider) {
|
||||
return id == other.id && size == other.size && isAnimated == other.isAnimated;
|
||||
return id == other.id &&
|
||||
size == other.size &&
|
||||
isAnimated == other.isAnimated &&
|
||||
width == other.width &&
|
||||
height == other.height &&
|
||||
checksum == other.checksum;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => id.hashCode ^ size.hashCode ^ isAnimated.hashCode;
|
||||
int get hashCode => Object.hash(id, size, isAnimated, width, height, checksum);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,13 +45,13 @@ class DriftMemoryLane extends ConsumerWidget {
|
|||
}
|
||||
}
|
||||
|
||||
class DriftMemoryCard extends ConsumerWidget {
|
||||
class DriftMemoryCard extends StatelessWidget {
|
||||
const DriftMemoryCard({super.key, required this.memory});
|
||||
|
||||
final DriftMemory memory;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
Widget build(BuildContext context) {
|
||||
final yearsAgo = DateTime.now().year - memory.data.year;
|
||||
final title = 'years_ago'.t(context: context, args: {'years': yearsAgo.toString()});
|
||||
return Center(
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
|
||||
class PersonOptionSheet extends ConsumerWidget {
|
||||
class PersonOptionSheet extends StatelessWidget {
|
||||
const PersonOptionSheet({super.key, this.onEditName, this.onEditBirthday, this.birthdayExists = false});
|
||||
|
||||
final VoidCallback? onEditName;
|
||||
|
|
@ -10,7 +9,7 @@ class PersonOptionSheet extends ConsumerWidget {
|
|||
final bool birthdayExists;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
Widget build(BuildContext context) {
|
||||
final TextStyle textStyle = Theme.of(context).textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.w600);
|
||||
|
||||
return SafeArea(
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
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/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||
|
||||
class DriftRemoteAlbumOption extends ConsumerWidget {
|
||||
class DriftRemoteAlbumOption extends StatelessWidget {
|
||||
const DriftRemoteAlbumOption({
|
||||
super.key,
|
||||
this.onAddPhotos,
|
||||
|
|
@ -31,7 +30,7 @@ class DriftRemoteAlbumOption extends ConsumerWidget {
|
|||
final List<Shadow>? iconShadows;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.themeData;
|
||||
final menuChildren = <Widget>[];
|
||||
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ class _FixedSegmentRow extends ConsumerWidget {
|
|||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isScrubbing = ref.watch(timelineStateProvider.select((s) => s.isScrubbing));
|
||||
final timelineService = ref.read(timelineServiceProvider);
|
||||
final timelineService = ref.watch(timelineServiceProvider);
|
||||
final isDynamicLayout = columnCount <= (context.isMobile ? 2 : 3);
|
||||
|
||||
if (timelineService.hasRange(assetIndex, assetCount)) {
|
||||
|
|
@ -261,7 +261,7 @@ class _AssetTileWidget extends ConsumerWidget {
|
|||
final lockSelection = _getLockSelectionStatus(ref);
|
||||
final showStorageIndicator = ref.watch(timelineArgsProvider.select((args) => args.showStorageIndicator));
|
||||
final isReadonlyModeEnabled = ref.watch(readonlyModeProvider);
|
||||
final showStackIndicator = ref.read(timelineServiceProvider).origin != TimelineOrigin.trash;
|
||||
final showStackIndicator = ref.watch(timelineServiceProvider).origin != TimelineOrigin.trash;
|
||||
|
||||
return RepaintBoundary(
|
||||
child: GestureDetector(
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'dart:async';
|
|||
|
||||
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/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||
|
|
@ -12,7 +13,7 @@ import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.da
|
|||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||
|
||||
class TimelineHeader extends HookConsumerWidget {
|
||||
class TimelineHeader extends HookWidget {
|
||||
final Bucket bucket;
|
||||
final HeaderType header;
|
||||
final double height;
|
||||
|
|
@ -39,7 +40,7 @@ class TimelineHeader extends HookConsumerWidget {
|
|||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
Widget build(BuildContext context) {
|
||||
if (bucket is! TimeBucket || header == HeaderType.none) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||
|
|
|
|||
|
|
@ -81,6 +81,10 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
|||
await _ref.read(serverInfoProvider.notifier).getServerVersion();
|
||||
}
|
||||
|
||||
if (!_shouldContinueOperation()) {
|
||||
_wasPaused = true;
|
||||
return;
|
||||
}
|
||||
_ref.read(websocketProvider.notifier).connect();
|
||||
await _handleBetaTimelineResume();
|
||||
|
||||
|
|
@ -89,13 +93,13 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
|||
await _ref.read(galleryPermissionNotifier.notifier).getGalleryPermissionStatus();
|
||||
}
|
||||
|
||||
Future<void> _safeRun(Future<void> action, String debugName) async {
|
||||
Future<void> _safeRun(Future<void> Function() action, String debugName) async {
|
||||
if (!_shouldContinueOperation()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await action;
|
||||
await action();
|
||||
} catch (e, stackTrace) {
|
||||
_log.warning("Error during $debugName operation", e, stackTrace);
|
||||
}
|
||||
|
|
@ -113,13 +117,15 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
|||
try {
|
||||
bool syncSuccess = false;
|
||||
await Future.wait([
|
||||
_safeRun(backgroundManager.syncLocal(full: CurrentPlatform.isAndroid ? true : false), "syncLocal"),
|
||||
_safeRun(backgroundManager.syncRemote().then((success) => syncSuccess = success), "syncRemote"),
|
||||
_safeRun(() => backgroundManager.syncLocal(full: CurrentPlatform.isAndroid), "syncLocal"),
|
||||
_safeRun(() async {
|
||||
syncSuccess = await backgroundManager.syncRemote();
|
||||
}, "syncRemote"),
|
||||
]);
|
||||
_ref.invalidate(driftMemoryFutureProvider);
|
||||
if (syncSuccess) {
|
||||
await Future.wait([
|
||||
_safeRun(backgroundManager.hashAssets(), "hashAssets").then((_) {
|
||||
_safeRun(backgroundManager.hashAssets, "hashAssets").then((_) {
|
||||
unawaited(_resumeBackup());
|
||||
}),
|
||||
_resumeBackup(),
|
||||
|
|
@ -127,11 +133,11 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
|||
// _safeRun(backgroundManager.syncCloudIds(), "syncCloudIds"),
|
||||
]);
|
||||
} else {
|
||||
await _safeRun(backgroundManager.hashAssets(), "hashAssets");
|
||||
await _safeRun(backgroundManager.hashAssets, "hashAssets");
|
||||
}
|
||||
|
||||
if (isAlbumLinkedSyncEnable) {
|
||||
await _safeRun(backgroundManager.syncLinkedAlbum(), "syncLinkedAlbum");
|
||||
await _safeRun(backgroundManager.syncLinkedAlbum, "syncLinkedAlbum");
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
_log.severe("Error during background sync", e, stackTrace);
|
||||
|
|
@ -145,7 +151,7 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
|||
final currentUser = Store.tryGet(StoreKey.currentUser);
|
||||
if (currentUser != null) {
|
||||
await _safeRun(
|
||||
_ref.read(driftBackupProvider.notifier).startForegroundBackup(currentUser.id),
|
||||
() => _ref.read(driftBackupProvider.notifier).startForegroundBackup(currentUser.id),
|
||||
"handleBackupResume",
|
||||
);
|
||||
}
|
||||
|
|
@ -195,7 +201,7 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
|||
|
||||
Future<void> _performPause() {
|
||||
if (_ref.read(authProvider).isAuthenticated) {
|
||||
_ref.read(driftBackupProvider.notifier).stopForegroundBackup();
|
||||
_ref.read(driftBackupProvider.notifier).stopForegroundBackup(reason: "the app being sent to the background");
|
||||
|
||||
_ref.read(websocketProvider.notifier).disconnect();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ final backgroundSyncProvider = Provider<BackgroundSyncManager>((ref) {
|
|||
final manager = BackgroundSyncManager(
|
||||
onRemoteSyncStart: () {
|
||||
syncStatusNotifier.startRemoteSync();
|
||||
// ignore: dispose-provided-instances
|
||||
final backupProvider = ref.read(driftBackupProvider.notifier);
|
||||
if (backupProvider.mounted) {
|
||||
backupProvider.updateError(BackupError.none);
|
||||
|
|
@ -16,6 +17,7 @@ final backgroundSyncProvider = Provider<BackgroundSyncManager>((ref) {
|
|||
},
|
||||
onRemoteSyncComplete: (isSuccess) {
|
||||
syncStatusNotifier.completeRemoteSync();
|
||||
// ignore: dispose-provided-instances
|
||||
final backupProvider = ref.read(driftBackupProvider.notifier);
|
||||
if (backupProvider.mounted) {
|
||||
backupProvider.updateError(isSuccess == true ? BackupError.none : BackupError.syncFailed);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/constants.dart';
|
||||
|
|
@ -134,19 +135,25 @@ class DriftBackupNotifier extends StateNotifier<DriftBackupState> {
|
|||
state = state.copyWith(isSyncing: isSyncing);
|
||||
}
|
||||
|
||||
Future<void> startForegroundBackup(String userId) {
|
||||
Future<void> startForegroundBackup(String userId) async {
|
||||
// Cancel any existing backup before starting a new one
|
||||
if (_cancelToken != null) {
|
||||
stopForegroundBackup();
|
||||
stopForegroundBackup(reason: "restarting the backup");
|
||||
}
|
||||
|
||||
state = state.copyWith(error: BackupError.none);
|
||||
|
||||
_cancelToken = Completer<void>();
|
||||
// A pause during the recount below nulls _cancelToken, so the run keeps its own reference.
|
||||
final cancelToken = Completer<void>();
|
||||
_cancelToken = cancelToken;
|
||||
|
||||
// Re-baseline the counters against the same DB read that feeds this run's candidate list,
|
||||
// otherwise a resume counts duplicate successes against the old baseline (#26215).
|
||||
await getBackupStatus(userId);
|
||||
|
||||
return _foregroundUploadService.uploadCandidates(
|
||||
userId,
|
||||
_cancelToken!,
|
||||
cancelToken,
|
||||
callbacks: UploadCallbacks(
|
||||
onProgress: _handleForegroundBackupProgress,
|
||||
onSuccess: _handleForegroundBackupSuccess,
|
||||
|
|
@ -156,7 +163,10 @@ class DriftBackupNotifier extends StateNotifier<DriftBackupState> {
|
|||
);
|
||||
}
|
||||
|
||||
void stopForegroundBackup() {
|
||||
void stopForegroundBackup({required String reason}) {
|
||||
if (_cancelToken != null) {
|
||||
_logger.info("Foreground backup cancelled: $reason");
|
||||
}
|
||||
_cancelToken?.complete();
|
||||
_cancelToken = null;
|
||||
_uploadSpeedManager.clear();
|
||||
|
|
@ -212,6 +222,10 @@ class DriftBackupNotifier extends StateNotifier<DriftBackupState> {
|
|||
}
|
||||
|
||||
void _handleForegroundBackupSuccess(String localAssetId, String remoteAssetId) {
|
||||
if (!mounted) {
|
||||
_logger.warning("Skip _handleForegroundBackupSuccess: notifier disposed");
|
||||
return;
|
||||
}
|
||||
state = state.copyWith(backupCount: state.backupCount + 1, remainderCount: state.remainderCount - 1);
|
||||
_uploadSpeedManager.removeTask(localAssetId);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
// ignore_for_file: use-ref-and-state-synchronously
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
// ignore_for_file: use-ref-and-state-synchronously
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
// ignore_for_file: use-ref-and-state-synchronously
|
||||
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/setting.model.dart';
|
||||
import 'package:immich_mobile/domain/services/setting.service.dart';
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
// ignore_for_file: use-ref-and-state-synchronously
|
||||
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/tag.model.dart';
|
||||
import 'package:immich_mobile/domain/services/tag.service.dart';
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
// ignore_for_file: use-ref-and-state-synchronously
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
|
|
|
|||
|
|
@ -2,5 +2,7 @@ import 'package:flutter/widgets.dart';
|
|||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
final searchInputFocusProvider = Provider((ref) {
|
||||
return FocusNode();
|
||||
final focusNode = FocusNode();
|
||||
ref.onDispose(focusNode.dispose);
|
||||
return focusNode;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
// ignore_for_file: use-ref-and-state-synchronously
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
|
|
@ -38,14 +39,16 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
|||
@override
|
||||
void dispose() {
|
||||
_batchDebouncer.dispose();
|
||||
state.socket?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// Connects websocket to server unless already connected
|
||||
/// Connects websocket to server unless an active socket already exists
|
||||
void connect() {
|
||||
if (state.isConnected) {
|
||||
if (state.socket?.active == true) {
|
||||
return;
|
||||
}
|
||||
state.socket?.dispose();
|
||||
final authenticationState = _ref.read(authProvider);
|
||||
|
||||
if (authenticationState.isAuthenticated) {
|
||||
|
|
@ -66,6 +69,8 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
|||
.build(),
|
||||
);
|
||||
|
||||
state = WebsocketState(isConnected: false, socket: socket);
|
||||
|
||||
socket.onConnect((_) {
|
||||
dPrint(() => "Established Websocket Connection");
|
||||
state = WebsocketState(isConnected: true, socket: socket);
|
||||
|
|
@ -73,12 +78,12 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
|||
|
||||
socket.onDisconnect((_) {
|
||||
dPrint(() => "Disconnect to Websocket Connection");
|
||||
state = const WebsocketState(isConnected: false, socket: null);
|
||||
state = WebsocketState(isConnected: false, socket: socket);
|
||||
});
|
||||
|
||||
socket.on('error', (errorMessage) {
|
||||
_log.severe("Websocket Error - $errorMessage");
|
||||
state = const WebsocketState(isConnected: false, socket: null);
|
||||
state = WebsocketState(isConnected: false, socket: socket);
|
||||
});
|
||||
|
||||
socket.on('AssetUploadReadyV1', _handleSyncAssetUploadReadyV1);
|
||||
|
|
@ -87,6 +92,11 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
|||
socket.on('AssetEditReadyV2', _handleSyncAssetEditReadyV2);
|
||||
socket.on('on_album_update', _handleRemoteChange);
|
||||
socket.on('on_asset_stack_update', _handleRemoteChange);
|
||||
socket.on('on_asset_delete', _handleRemoteChange);
|
||||
socket.on('on_asset_trash', _handleRemoteChange);
|
||||
socket.on('on_asset_restore', _handleRemoteChange);
|
||||
socket.on('on_asset_hidden', _handleRemoteChange);
|
||||
socket.on('on_asset_update', _handleRemoteChange);
|
||||
socket.on('on_config_update', _handleOnConfigUpdate);
|
||||
socket.on('on_new_release', _handleReleaseUpdates);
|
||||
} catch (e) {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import 'package:immich_mobile/models/download/livephotos_medatada.model.dart';
|
|||
import 'package:immich_mobile/services/api.service.dart';
|
||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||
|
||||
// ignore: dispose-provided-instances
|
||||
final downloadRepositoryProvider = Provider((ref) => DownloadRepository());
|
||||
|
||||
class DownloadRepository {
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@ import 'package:maplibre_gl/maplibre_gl.dart';
|
|||
part 'router.gr.dart';
|
||||
|
||||
final appRouterProvider = Provider(
|
||||
// ignore: dispose-provided-instances
|
||||
(ref) => AppRouter(
|
||||
ref.watch(apiServiceProvider),
|
||||
ref.watch(authServiceProvider),
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ class UploadCallbacks {
|
|||
}
|
||||
|
||||
final foregroundUploadServiceProvider = Provider((ref) {
|
||||
// ignore: dispose-provided-instances
|
||||
return ForegroundUploadService(
|
||||
ref.watch(uploadRepositoryProvider),
|
||||
ref.watch(storageRepositoryProvider),
|
||||
|
|
@ -383,7 +384,6 @@ class ForegroundUploadService {
|
|||
if (result.isSuccess && result.remoteAssetId != null) {
|
||||
callbacks.onSuccess?.call(asset.localId!, result.remoteAssetId!);
|
||||
} else if (result.isCancelled) {
|
||||
_logger.warning(() => "Backup was cancelled by the user");
|
||||
shouldAbortUpload = true;
|
||||
} else if (result.errorMessage != null) {
|
||||
_logger.severe(
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ class CommentBubble extends ConsumerWidget {
|
|||
final isLike = activity.type == ActivityType.like;
|
||||
final bgColor = isOwn ? context.colorScheme.primaryContainer : context.colorScheme.surfaceContainer;
|
||||
|
||||
final activityNotifier = ref.read(
|
||||
final activityNotifier = ref.watch(
|
||||
albumActivityProvider((album.id, isAssetActivity ? activity.assetId : null)).notifier,
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ class CastDialog extends ConsumerWidget {
|
|||
width: 250,
|
||||
height: 250,
|
||||
child: FutureBuilder<List<(String, CastDestinationType, dynamic)>>(
|
||||
future: ref.read(castProvider.notifier).getDevices(),
|
||||
future: ref.watch(castProvider.notifier).getDevices(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return Text('error_saving_image'.tr(args: [snapshot.error.toString()]));
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ class _VideoControlsState extends ConsumerState<VideoControls> {
|
|||
});
|
||||
ref.listen(_provider.select((v) => v.status), (_, __) => _hideTimer.reset());
|
||||
|
||||
final notifier = ref.read(_provider.notifier);
|
||||
final notifier = ref.watch(_provider.notifier);
|
||||
final isLoaded = duration != Duration.zero;
|
||||
|
||||
return Padding(
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ class DriftAlbumInfoListTile extends HookConsumerWidget {
|
|||
|
||||
return GestureDetector(
|
||||
onDoubleTap: () {
|
||||
ref.watch(hapticFeedbackProvider.notifier).selectionClick();
|
||||
ref.read(hapticFeedbackProvider.notifier).selectionClick();
|
||||
|
||||
if (isExcluded) {
|
||||
unawaited(ref.read(backupAlbumProvider.notifier).deselectAlbum(album));
|
||||
|
|
|
|||
|
|
@ -124,6 +124,9 @@ class ImmichAppBarDialog extends HookConsumerWidget {
|
|||
onOk: () async {
|
||||
isLoggingOut.value = true;
|
||||
await ref.read(authProvider.notifier).logout().whenComplete(() => isLoggingOut.value = false);
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
ref.read(websocketProvider.notifier).disconnect();
|
||||
if (!context.mounted) {
|
||||
|
|
|
|||
|
|
@ -45,12 +45,12 @@ class AppBarProfileInfoBox extends HookConsumerWidget {
|
|||
Future<void> pickUserProfileImage() async {
|
||||
final XFile? image = await ImagePicker().pickImage(source: ImageSource.gallery, maxHeight: 1024, maxWidth: 1024);
|
||||
|
||||
if (image != null) {
|
||||
final success = await ref.watch(uploadProfileImageProvider.notifier).upload(image);
|
||||
if (image != null && context.mounted) {
|
||||
final success = await ref.read(uploadProfileImageProvider.notifier).upload(image);
|
||||
|
||||
if (success) {
|
||||
if (success && context.mounted) {
|
||||
final profileImagePath = ref.read(uploadProfileImageProvider).profileImagePath;
|
||||
ref.watch(authProvider.notifier).updateUserProfileImagePath(profileImagePath);
|
||||
ref.read(authProvider.notifier).updateUserProfileImagePath(profileImagePath);
|
||||
if (user != null) {
|
||||
unawaited(ref.read(currentUserProvider.notifier).refresh());
|
||||
}
|
||||
|
|
@ -61,7 +61,7 @@ class AppBarProfileInfoBox extends HookConsumerWidget {
|
|||
}
|
||||
|
||||
void toggleReadonlyMode() {
|
||||
final isReadonlyModeEnabled = ref.watch(readonlyModeProvider);
|
||||
final isReadonlyModeEnabled = ref.read(readonlyModeProvider);
|
||||
ref.read(readonlyModeProvider.notifier).toggleReadonlyMode();
|
||||
|
||||
context.scaffoldMessenger.showSnackBar(
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ class _ProfileIndicator extends ConsumerWidget {
|
|||
final isIpad = defaultTargetPlatform == TargetPlatform.iOS && !context.isMobile;
|
||||
|
||||
void toggleReadonlyMode() {
|
||||
final isReadonlyModeEnabled = ref.watch(readonlyModeProvider);
|
||||
final isReadonlyModeEnabled = ref.read(readonlyModeProvider);
|
||||
ref.read(readonlyModeProvider.notifier).toggleReadonlyMode();
|
||||
|
||||
context.scaffoldMessenger.showSnackBar(
|
||||
|
|
@ -193,11 +193,11 @@ class _BackupIndicator extends ConsumerWidget {
|
|||
}
|
||||
|
||||
Widget? _getBackupBadgeIcon(BuildContext context, WidgetRef ref) {
|
||||
final backupEnabled = ref.watch(appConfigProvider.select((c) => c.backup.enabled));
|
||||
final hasError = ref.watch(driftBackupProvider.select((state) => state.error != BackupError.none));
|
||||
final backupEnabled = ref.read(appConfigProvider.select((c) => c.backup.enabled));
|
||||
final hasError = ref.read(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));
|
||||
final isUploading = ref.read(driftBackupProvider.select((state) => state.uploadItems.isNotEmpty));
|
||||
|
||||
if (!backupEnabled) {
|
||||
return _BadgeLabel(
|
||||
|
|
|
|||
|
|
@ -17,13 +17,13 @@ Future<(Set<String>, Set<String>)?> showTagPickerModal({required BuildContext co
|
|||
);
|
||||
}
|
||||
|
||||
class _TagPickerModal extends HookConsumerWidget {
|
||||
class _TagPickerModal extends HookWidget {
|
||||
final Set<String>? initialSelection;
|
||||
|
||||
const _TagPickerModal({this.initialSelection});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
Widget build(BuildContext context) {
|
||||
final selectedTagIds = useState<Set<String>>(initialSelection ?? {});
|
||||
final newTagValues = useState<Set<String>>({});
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue