fix(mobile): video state (#26574)

Consolidate video state into a single asset-scoped provider, and reduce
dependency on global state generally. Overall this should fix a few
timing issues and race conditions with videos specifically, and make
future changes in this area easier.
This commit is contained in:
Thomas
2026-03-03 16:28:07 +00:00
committed by GitHub
parent 0560f98c2d
commit 4eb08eee18
40 changed files with 823 additions and 1182 deletions
@@ -8,27 +8,25 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/theme_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/album/album_tile.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/sheet_tile.widget.dart';
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/routing/router.dart';
class AppearsInDetails extends ConsumerWidget {
const AppearsInDetails({super.key});
final BaseAsset asset;
const AppearsInDetails({super.key, required this.asset});
@override
Widget build(BuildContext context, WidgetRef ref) {
final asset = ref.watch(currentAssetNotifier);
if (asset == null || !asset.hasRemote) return const SizedBox.shrink();
if (!asset.hasRemote) return const SizedBox.shrink();
String? remoteAssetId;
if (asset is RemoteAsset) {
remoteAssetId = asset.id;
} else if (asset is LocalAsset) {
remoteAssetId = asset.remoteAssetId;
}
final remoteAssetId = switch (asset) {
RemoteAsset(:final id) => id,
LocalAsset(:final remoteAssetId) => remoteAssetId,
};
if (remoteAssetId == null) return const SizedBox.shrink();
@@ -10,7 +10,6 @@ import 'package:immich_mobile/extensions/duration_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/sheet_tile.widget.dart';
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/utils/timezone.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
@@ -18,14 +17,15 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart';
const _kSeparator = '';
class DateTimeDetails extends ConsumerWidget {
const DateTimeDetails({super.key});
final BaseAsset asset;
final ExifInfo? exifInfo;
const DateTimeDetails({super.key, required this.asset, this.exifInfo});
@override
Widget build(BuildContext context, WidgetRef ref) {
final asset = ref.watch(currentAssetNotifier);
if (asset == null) return const SizedBox.shrink();
final exifInfo = ref.watch(currentAssetExifProvider).valueOrNull;
final asset = this.asset;
final exifInfo = this.exifInfo;
final isOwner = ref.watch(currentUserProvider)?.id == (asset is RemoteAsset ? asset.ownerId : null);
return Column(
@@ -106,9 +106,7 @@ class _SheetAssetDescriptionState extends ConsumerState<_SheetAssetDescription>
@override
Widget build(BuildContext context) {
final currentExifInfo = ref.watch(currentAssetExifProvider).valueOrNull;
final currentDescription = currentExifInfo?.description ?? '';
final currentDescription = widget.exif.description ?? '';
final hintText = (widget.isEditable ? 'exif_bottom_sheet_description' : 'exif_bottom_sheet_no_description').t(
context: context,
);
@@ -134,7 +132,7 @@ class _SheetAssetDescriptionState extends ConsumerState<_SheetAssetDescription>
errorBorder: InputBorder.none,
focusedErrorBorder: InputBorder.none,
),
onTapOutside: (_) => saveDescription(currentExifInfo?.description),
onTapOutside: (_) => saveDescription(widget.exif.description),
),
),
);
@@ -8,12 +8,14 @@ import 'package:immich_mobile/extensions/theme_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/sheet_tile.widget.dart';
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provider.dart';
import 'package:immich_mobile/widgets/asset_viewer/detail_panel/exif_map.dart';
import 'package:maplibre_gl/maplibre_gl.dart';
class LocationDetails extends ConsumerStatefulWidget {
const LocationDetails({super.key});
final BaseAsset asset;
final ExifInfo? exifInfo;
const LocationDetails({super.key, required this.asset, this.exifInfo});
@override
ConsumerState createState() => _LocationDetailsState();
@@ -40,17 +42,15 @@ class _LocationDetailsState extends ConsumerState<LocationDetails> {
_mapController = controller;
}
void _onExifChanged(AsyncValue<ExifInfo?>? previous, AsyncValue<ExifInfo?> current) {
final currentExif = current.valueOrNull;
if (currentExif != null && currentExif.hasCoordinates) {
_mapController?.moveCamera(CameraUpdate.newLatLng(LatLng(currentExif.latitude!, currentExif.longitude!)));
}
}
@override
void initState() {
super.initState();
ref.listenManual(currentAssetExifProvider, _onExifChanged, fireImmediately: true);
void didUpdateWidget(LocationDetails oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.exifInfo != oldWidget.exifInfo) {
final exif = widget.exifInfo;
if (exif != null && exif.hasCoordinates) {
_mapController?.moveCamera(CameraUpdate.newLatLng(LatLng(exif.latitude!, exif.longitude!)));
}
}
}
void editLocation() async {
@@ -59,8 +59,8 @@ class _LocationDetailsState extends ConsumerState<LocationDetails> {
@override
Widget build(BuildContext context) {
final asset = ref.watch(currentAssetNotifier);
final exifInfo = ref.watch(currentAssetExifProvider).valueOrNull;
final asset = widget.asset;
final exifInfo = widget.exifInfo;
final hasCoordinates = exifInfo?.hasCoordinates ?? false;
// Guard local assets
@@ -7,7 +7,6 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/theme_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/people/person_edit_name_modal.widget.dart';
import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provider.dart';
import 'package:immich_mobile/providers/infrastructure/people.provider.dart';
import 'package:immich_mobile/providers/routes.provider.dart';
import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart';
@@ -15,17 +14,14 @@ import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:immich_mobile/utils/people.utils.dart';
class PeopleDetails extends ConsumerStatefulWidget {
const PeopleDetails({super.key});
class PeopleDetails extends ConsumerWidget {
final BaseAsset asset;
const PeopleDetails({super.key, required this.asset});
@override
ConsumerState createState() => _PeopleDetailsState();
}
class _PeopleDetailsState extends ConsumerState<PeopleDetails> {
@override
Widget build(BuildContext context) {
final asset = ref.watch(currentAssetNotifier);
Widget build(BuildContext context, WidgetRef ref) {
final asset = this.asset;
if (asset is! RemoteAsset) {
return const SizedBox.shrink();
}
@@ -1,16 +1,18 @@
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/exif.model.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/theme_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/rating_bar.widget.dart';
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provider.dart';
import 'package:immich_mobile/providers/infrastructure/user_metadata.provider.dart';
class RatingDetails extends ConsumerWidget {
const RatingDetails({super.key});
final ExifInfo? exifInfo;
const RatingDetails({super.key, this.exifInfo});
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -20,8 +22,6 @@ class RatingDetails extends ConsumerWidget {
if (!isRatingEnabled) return const SizedBox.shrink();
final exifInfo = ref.watch(currentAssetExifProvider).valueOrNull;
return Padding(
padding: const EdgeInsets.only(left: 16.0, top: 16.0),
child: Column(
@@ -6,21 +6,20 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/theme_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/sheet_tile.widget.dart';
import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provider.dart';
import 'package:immich_mobile/repositories/asset_media.repository.dart';
import 'package:immich_mobile/utils/bytes_units.dart';
const _kSeparator = '';
class TechnicalDetails extends ConsumerWidget {
const TechnicalDetails({super.key});
final BaseAsset asset;
final ExifInfo? exifInfo;
const TechnicalDetails({super.key, required this.asset, this.exifInfo});
@override
Widget build(BuildContext context, WidgetRef ref) {
final asset = ref.watch(currentAssetNotifier);
if (asset == null) return const SizedBox.shrink();
final exifInfo = ref.watch(currentAssetExifProvider).valueOrNull;
final exifInfo = this.exifInfo;
final cameraTitle = _getCameraInfoTitle(exifInfo);
final lensTitle = exifInfo?.lens != null && exifInfo!.lens!.isNotEmpty ? exifInfo.lens : null;