Files
Immich/mobile/lib/providers/asset_viewer/asset_viewer.provider.dart
T
Santo ShakilandGitHub 9862e50aab fix(mobile): decode remote thumbnails at displayed size (#29965)
* fix(mobile): decode remote thumbnails at displayed size

* use nullable decode size and reuse the tapped thumbnail in the viewer

* clean up decode size naming and guards
2026-08-10 21:14:23 +06:00

118 lines
3.4 KiB
Dart

import 'dart:async';
import 'dart:ui';
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';
import 'package:immich_mobile/providers/asset_viewer/video_player_provider.dart';
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
part 'asset_viewer.provider.freezed.dart';
@freezed
abstract class AssetViewerState with _$AssetViewerState {
const factory AssetViewerState({
@Default(1.0) double backgroundOpacity,
@Default(false) bool showingDetails,
@Default(true) bool showingControls,
@Default(false) bool isZoomed,
@Default(false) bool showingOcr,
BaseAsset? currentAsset,
/// Physical thumbnail size retained while paging through the viewer.
Size? thumbnailSize,
@Default(0) int stackIndex,
}) = _AssetViewerState;
}
class AssetViewerStateNotifier extends Notifier<AssetViewerState> {
StreamSubscription<BaseAsset?>? _assetSubscription;
@override
AssetViewerState build() {
ref.onDispose(() => _assetSubscription?.cancel());
return const AssetViewerState();
}
void reset() {
unawaited(_assetSubscription?.cancel());
_assetSubscription = null;
state = const AssetViewerState();
}
void setAsset(BaseAsset asset, {Size? thumbnailSize}) {
if (asset == state.currentAsset) {
return;
}
// Swiping to a neighbor passes no size; keep the tapped tile's so neighbors reuse it.
state = state.copyWith(
currentAsset: asset,
thumbnailSize: thumbnailSize ?? state.thumbnailSize,
stackIndex: 0,
showingOcr: false,
);
_watchCurrentAsset(asset);
}
void _watchCurrentAsset(BaseAsset asset) {
unawaited(_assetSubscription?.cancel());
_assetSubscription = ref.read(assetServiceProvider).watchAsset(asset).listen((updated) {
if (updated != null) {
state = state.copyWith(currentAsset: updated);
}
});
}
void setOpacity(double opacity) {
if (opacity == state.backgroundOpacity) {
return;
}
state = state.copyWith(backgroundOpacity: opacity, showingControls: opacity >= 1.0 ? true : state.showingControls);
}
void setShowingDetails(bool showing) {
if (showing == state.showingDetails) {
return;
}
state = state.copyWith(showingDetails: showing, showingControls: showing ? true : state.showingControls);
final id = state.currentAsset?.id;
if (id != null) {
final notifier = ref.read(videoPlayerProvider(id).notifier);
showing ? notifier.hold() : notifier.release();
}
}
void setControls(bool isShowing) {
if (isShowing == state.showingControls) {
return;
}
state = state.copyWith(showingControls: isShowing);
}
void toggleControls() {
state = state.copyWith(showingControls: !state.showingControls);
}
void setZoomed(bool isZoomed) {
if (isZoomed == state.isZoomed) {
return;
}
state = state.copyWith(isZoomed: isZoomed);
}
void setStackIndex(int index) {
if (index == state.stackIndex) {
return;
}
state = state.copyWith(stackIndex: index);
}
void toggleOcr() {
state = state.copyWith(showingOcr: !state.showingOcr);
}
}
final assetViewerProvider = NotifierProvider<AssetViewerStateNotifier, AssetViewerState>(AssetViewerStateNotifier.new);