Compare commits

...

3 Commits

Author SHA1 Message Date
Alex
782a089da0 fix: scrubber scroll error when page is not long enough 2025-07-07 23:15:25 -05:00
Alex
d5eff115e8 Merge branch 'main' of github.com:immich-app/immich 2025-07-07 15:44:28 -05:00
Alex
f8ab533acb use mutex 2025-07-05 14:32:46 -05:00
3 changed files with 106 additions and 46 deletions

View File

@@ -9,8 +9,57 @@ class BackgroundSyncManager {
Cancelable<void>? _deviceAlbumSyncTask;
Cancelable<void>? _hashTask;
Completer<void>? _localSyncMutex;
Completer<void>? _remoteSyncMutex;
Completer<void>? _hashMutex;
BackgroundSyncManager();
Future<T> _withMutex<T>(
Completer<void>? Function() getMutex,
void Function(Completer<void>?) setMutex,
Future<T> Function() operation,
) async {
while (getMutex() != null) {
await getMutex()!.future;
}
final mutex = Completer<void>();
setMutex(mutex);
try {
final result = await operation();
return result;
} finally {
setMutex(null);
mutex.complete();
}
}
Future<T> _withLocalSyncMutex<T>(Future<T> Function() operation) {
return _withMutex(
() => _localSyncMutex,
(mutex) => _localSyncMutex = mutex,
operation,
);
}
Future<T> _withRemoteSyncMutex<T>(Future<T> Function() operation) {
return _withMutex(
() => _remoteSyncMutex,
(mutex) => _remoteSyncMutex = mutex,
operation,
);
}
Future<T> _withHashMutex<T>(Future<T> Function() operation) {
return _withMutex(
() => _hashMutex,
(mutex) => _hashMutex = mutex,
operation,
);
}
Future<void> cancel() {
final futures = <Future>[];
@@ -25,51 +74,57 @@ class BackgroundSyncManager {
// No need to cancel the task, as it can also be run when the user logs out
Future<void> syncLocal({bool full = false}) {
if (_deviceAlbumSyncTask != null) {
return _deviceAlbumSyncTask!.future;
}
return _withLocalSyncMutex(() async {
if (_deviceAlbumSyncTask != null) {
return _deviceAlbumSyncTask!.future;
}
// We use a ternary operator to avoid [_deviceAlbumSyncTask] from being
// captured by the closure passed to [runInIsolateGentle].
_deviceAlbumSyncTask = full
? runInIsolateGentle(
computation: (ref) =>
ref.read(localSyncServiceProvider).sync(full: true),
)
: runInIsolateGentle(
computation: (ref) =>
ref.read(localSyncServiceProvider).sync(full: false),
);
// We use a ternary operator to avoid [_deviceAlbumSyncTask] from being
// captured by the closure passed to [runInIsolateGentle].
_deviceAlbumSyncTask = full
? runInIsolateGentle(
computation: (ref) =>
ref.read(localSyncServiceProvider).sync(full: true),
)
: runInIsolateGentle(
computation: (ref) =>
ref.read(localSyncServiceProvider).sync(full: false),
);
return _deviceAlbumSyncTask!.whenComplete(() {
_deviceAlbumSyncTask = null;
return _deviceAlbumSyncTask!.whenComplete(() {
_deviceAlbumSyncTask = null;
});
});
}
// No need to cancel the task, as it can also be run when the user logs out
Future<void> hashAssets() {
if (_hashTask != null) {
return _hashTask!.future;
}
return _withHashMutex(() async {
if (_hashTask != null) {
return _hashTask!.future;
}
_hashTask = runInIsolateGentle(
computation: (ref) => ref.read(hashServiceProvider).hashAssets(),
);
return _hashTask!.whenComplete(() {
_hashTask = null;
_hashTask = runInIsolateGentle(
computation: (ref) => ref.read(hashServiceProvider).hashAssets(),
);
return _hashTask!.whenComplete(() {
_hashTask = null;
});
});
}
Future<void> syncRemote() {
if (_syncTask != null) {
return _syncTask!.future;
}
return _withRemoteSyncMutex(() async {
if (_syncTask != null) {
return _syncTask!.future;
}
_syncTask = runInIsolateGentle(
computation: (ref) => ref.read(syncStreamServiceProvider).sync(),
);
return _syncTask!.whenComplete(() {
_syncTask = null;
_syncTask = runInIsolateGentle(
computation: (ref) => ref.read(syncStreamServiceProvider).sync(),
);
return _syncTask!.whenComplete(() {
_syncTask = null;
});
});
}
}

View File

@@ -354,22 +354,24 @@ class ScrubberState extends ConsumerState<Scrubber>
isDragging: _isDragging,
),
),
PositionedDirectional(
top: _thumbTopOffset + widget.topPadding,
end: 0,
child: RepaintBoundary(
child: GestureDetector(
onVerticalDragStart: _onDragStart,
onVerticalDragUpdate: _onDragUpdate,
onVerticalDragEnd: _onDragEnd,
child: _Scrubber(
thumbAnimation: _thumbAnimation,
labelAnimation: _labelAnimation,
label: label,
if (_scrollController.hasClients &&
_scrollController.position.maxScrollExtent > 0)
PositionedDirectional(
top: _thumbTopOffset + widget.topPadding,
end: 0,
child: RepaintBoundary(
child: GestureDetector(
onVerticalDragStart: _onDragStart,
onVerticalDragUpdate: _onDragUpdate,
onVerticalDragEnd: _onDragEnd,
child: _Scrubber(
thumbAnimation: _thumbAnimation,
labelAnimation: _labelAnimation,
label: label,
),
),
),
),
),
],
),
);

View File

@@ -73,7 +73,10 @@ class ImmichSliverAppBar extends ConsumerWidget {
onPressed: () => context.pop(),
),
IconButton(
onPressed: () => ref.read(backgroundSyncProvider).syncRemote(),
onPressed: () {
ref.read(backgroundSyncProvider).syncLocal(full: true);
ref.read(backgroundSyncProvider).syncRemote();
},
icon: const Icon(
Icons.sync,
),