You've already forked immich
mirror of
https://github.com/immich-app/immich.git
synced 2025-08-08 23:07:06 +02:00
refactor(mobile): services and providers (#9232)
* refactor(mobile): services and provider * providers
This commit is contained in:
@ -0,0 +1,87 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/services/asset_description.service.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/exif_info.entity.dart';
|
||||
import 'package:immich_mobile/providers/db.provider.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
class AssetDescriptionNotifier extends StateNotifier<String> {
|
||||
final Isar _db;
|
||||
final AssetDescriptionService _service;
|
||||
final Asset _asset;
|
||||
|
||||
AssetDescriptionNotifier(
|
||||
this._db,
|
||||
this._service,
|
||||
this._asset,
|
||||
) : super('') {
|
||||
_fetchLocalDescription();
|
||||
_fetchRemoteDescription();
|
||||
}
|
||||
|
||||
String get description => state;
|
||||
|
||||
/// Fetches the local database value for description
|
||||
/// and writes it to [state]
|
||||
void _fetchLocalDescription() async {
|
||||
final localExifId = _asset.exifInfo?.id;
|
||||
|
||||
// Guard [localExifId] null
|
||||
if (localExifId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Subscribe to local changes
|
||||
final exifInfo = await _db.exifInfos.get(localExifId);
|
||||
|
||||
// Guard
|
||||
if (exifInfo?.description == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
state = exifInfo!.description!;
|
||||
}
|
||||
|
||||
/// Fetches the remote value and sets the state
|
||||
void _fetchRemoteDescription() async {
|
||||
final remoteAssetId = _asset.remoteId;
|
||||
final localExifId = _asset.exifInfo?.id;
|
||||
|
||||
// Guard [remoteAssetId] and [localExifId] null
|
||||
if (remoteAssetId == null || localExifId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Reads the latest from the remote and writes it to DB in the service
|
||||
final latest = await _service.readLatest(remoteAssetId, localExifId);
|
||||
|
||||
state = latest;
|
||||
}
|
||||
|
||||
/// Sets the description to [description]
|
||||
/// Uses the service to set the asset value
|
||||
Future<void> setDescription(String description) async {
|
||||
state = description;
|
||||
|
||||
final remoteAssetId = _asset.remoteId;
|
||||
final localExifId = _asset.exifInfo?.id;
|
||||
|
||||
// Guard [remoteAssetId] and [localExifId] null
|
||||
if (remoteAssetId == null || localExifId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
return _service.setDescription(description, remoteAssetId, localExifId);
|
||||
}
|
||||
}
|
||||
|
||||
final assetDescriptionProvider = StateNotifierProvider.autoDispose
|
||||
.family<AssetDescriptionNotifier, String, Asset>(
|
||||
(ref, asset) => AssetDescriptionNotifier(
|
||||
ref.watch(dbProvider),
|
||||
ref.watch(assetDescriptionServiceProvider),
|
||||
asset,
|
||||
),
|
||||
);
|
51
mobile/lib/providers/asset_viewer/asset_people.provider.dart
Normal file
51
mobile/lib/providers/asset_viewer/asset_people.provider.dart
Normal file
@ -0,0 +1,51 @@
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/services/asset.service.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'asset_people.provider.g.dart';
|
||||
|
||||
/// Maintains the list of people for an asset.
|
||||
@riverpod
|
||||
class AssetPeopleNotifier extends _$AssetPeopleNotifier {
|
||||
final log = Logger('AssetPeopleNotifier');
|
||||
|
||||
@override
|
||||
Future<List<PersonWithFacesResponseDto>> build(Asset asset) async {
|
||||
if (!asset.isRemote) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final list = await ref
|
||||
.watch(assetServiceProvider)
|
||||
.getRemotePeopleOfAsset(asset.remoteId!);
|
||||
if (list == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// explicitly a sorted slice to make it deterministic
|
||||
// named people will be at the beginning, and names are sorted
|
||||
// ascendingly
|
||||
list.sort((a, b) {
|
||||
final aNotEmpty = a.name.isNotEmpty;
|
||||
final bNotEmpty = b.name.isNotEmpty;
|
||||
if (aNotEmpty && !bNotEmpty) {
|
||||
return -1;
|
||||
} else if (!aNotEmpty && bNotEmpty) {
|
||||
return 1;
|
||||
} else if (!aNotEmpty && !bNotEmpty) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return a.name.compareTo(b.name);
|
||||
});
|
||||
return list;
|
||||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
// invalidate the state – this way we don't have to
|
||||
// duplicate the code from build.
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
}
|
189
mobile/lib/providers/asset_viewer/asset_people.provider.g.dart
generated
Normal file
189
mobile/lib/providers/asset_viewer/asset_people.provider.g.dart
generated
Normal file
@ -0,0 +1,189 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'asset_people.provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$assetPeopleNotifierHash() =>
|
||||
r'9835b180984a750c91e923e7b64dbda94f6d7574';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
_SystemHash._();
|
||||
|
||||
static int combine(int hash, int value) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + value);
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||
return hash ^ (hash >> 6);
|
||||
}
|
||||
|
||||
static int finish(int hash) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||
// ignore: parameter_assignments
|
||||
hash = hash ^ (hash >> 11);
|
||||
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _$AssetPeopleNotifier extends BuildlessAutoDisposeAsyncNotifier<
|
||||
List<PersonWithFacesResponseDto>> {
|
||||
late final Asset asset;
|
||||
|
||||
FutureOr<List<PersonWithFacesResponseDto>> build(
|
||||
Asset asset,
|
||||
);
|
||||
}
|
||||
|
||||
/// Maintains the list of people for an asset.
|
||||
///
|
||||
/// Copied from [AssetPeopleNotifier].
|
||||
@ProviderFor(AssetPeopleNotifier)
|
||||
const assetPeopleNotifierProvider = AssetPeopleNotifierFamily();
|
||||
|
||||
/// Maintains the list of people for an asset.
|
||||
///
|
||||
/// Copied from [AssetPeopleNotifier].
|
||||
class AssetPeopleNotifierFamily
|
||||
extends Family<AsyncValue<List<PersonWithFacesResponseDto>>> {
|
||||
/// Maintains the list of people for an asset.
|
||||
///
|
||||
/// Copied from [AssetPeopleNotifier].
|
||||
const AssetPeopleNotifierFamily();
|
||||
|
||||
/// Maintains the list of people for an asset.
|
||||
///
|
||||
/// Copied from [AssetPeopleNotifier].
|
||||
AssetPeopleNotifierProvider call(
|
||||
Asset asset,
|
||||
) {
|
||||
return AssetPeopleNotifierProvider(
|
||||
asset,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AssetPeopleNotifierProvider getProviderOverride(
|
||||
covariant AssetPeopleNotifierProvider provider,
|
||||
) {
|
||||
return call(
|
||||
provider.asset,
|
||||
);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'assetPeopleNotifierProvider';
|
||||
}
|
||||
|
||||
/// Maintains the list of people for an asset.
|
||||
///
|
||||
/// Copied from [AssetPeopleNotifier].
|
||||
class AssetPeopleNotifierProvider extends AutoDisposeAsyncNotifierProviderImpl<
|
||||
AssetPeopleNotifier, List<PersonWithFacesResponseDto>> {
|
||||
/// Maintains the list of people for an asset.
|
||||
///
|
||||
/// Copied from [AssetPeopleNotifier].
|
||||
AssetPeopleNotifierProvider(
|
||||
Asset asset,
|
||||
) : this._internal(
|
||||
() => AssetPeopleNotifier()..asset = asset,
|
||||
from: assetPeopleNotifierProvider,
|
||||
name: r'assetPeopleNotifierProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$assetPeopleNotifierHash,
|
||||
dependencies: AssetPeopleNotifierFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
AssetPeopleNotifierFamily._allTransitiveDependencies,
|
||||
asset: asset,
|
||||
);
|
||||
|
||||
AssetPeopleNotifierProvider._internal(
|
||||
super._createNotifier, {
|
||||
required super.name,
|
||||
required super.dependencies,
|
||||
required super.allTransitiveDependencies,
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.asset,
|
||||
}) : super.internal();
|
||||
|
||||
final Asset asset;
|
||||
|
||||
@override
|
||||
FutureOr<List<PersonWithFacesResponseDto>> runNotifierBuild(
|
||||
covariant AssetPeopleNotifier notifier,
|
||||
) {
|
||||
return notifier.build(
|
||||
asset,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Override overrideWith(AssetPeopleNotifier Function() create) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
override: AssetPeopleNotifierProvider._internal(
|
||||
() => create()..asset = asset,
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
asset: asset,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AutoDisposeAsyncNotifierProviderElement<AssetPeopleNotifier,
|
||||
List<PersonWithFacesResponseDto>> createElement() {
|
||||
return _AssetPeopleNotifierProviderElement(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is AssetPeopleNotifierProvider && other.asset == asset;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, asset.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
mixin AssetPeopleNotifierRef
|
||||
on AutoDisposeAsyncNotifierProviderRef<List<PersonWithFacesResponseDto>> {
|
||||
/// The parameter `asset` of this provider.
|
||||
Asset get asset;
|
||||
}
|
||||
|
||||
class _AssetPeopleNotifierProviderElement
|
||||
extends AutoDisposeAsyncNotifierProviderElement<AssetPeopleNotifier,
|
||||
List<PersonWithFacesResponseDto>> with AssetPeopleNotifierRef {
|
||||
_AssetPeopleNotifierProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
Asset get asset => (origin as AssetPeopleNotifierProvider).asset;
|
||||
}
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
59
mobile/lib/providers/asset_viewer/asset_stack.provider.dart
Normal file
59
mobile/lib/providers/asset_viewer/asset_stack.provider.dart
Normal file
@ -0,0 +1,59 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/providers/db.provider.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'asset_stack.provider.g.dart';
|
||||
|
||||
class AssetStackNotifier extends StateNotifier<List<Asset>> {
|
||||
final Asset _asset;
|
||||
final Ref _ref;
|
||||
|
||||
AssetStackNotifier(
|
||||
this._asset,
|
||||
this._ref,
|
||||
) : super([]) {
|
||||
fetchStackChildren();
|
||||
}
|
||||
|
||||
void fetchStackChildren() async {
|
||||
if (mounted) {
|
||||
state = await _ref.read(assetStackProvider(_asset).future);
|
||||
}
|
||||
}
|
||||
|
||||
void removeChild(int index) {
|
||||
if (index < state.length) {
|
||||
state.removeAt(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final assetStackStateProvider = StateNotifierProvider.autoDispose
|
||||
.family<AssetStackNotifier, List<Asset>, Asset>(
|
||||
(ref, asset) => AssetStackNotifier(asset, ref),
|
||||
);
|
||||
|
||||
final assetStackProvider =
|
||||
FutureProvider.autoDispose.family<List<Asset>, Asset>((ref, asset) async {
|
||||
// Guard [local asset]
|
||||
if (asset.remoteId == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return await ref
|
||||
.watch(dbProvider)
|
||||
.assets
|
||||
.filter()
|
||||
.isArchivedEqualTo(false)
|
||||
.isTrashedEqualTo(false)
|
||||
.stackParentIdEqualTo(asset.remoteId)
|
||||
.sortByFileCreatedAtDesc()
|
||||
.findAll();
|
||||
});
|
||||
|
||||
@riverpod
|
||||
int assetStackIndex(AssetStackIndexRef ref, Asset asset) {
|
||||
return -1;
|
||||
}
|
158
mobile/lib/providers/asset_viewer/asset_stack.provider.g.dart
generated
Normal file
158
mobile/lib/providers/asset_viewer/asset_stack.provider.g.dart
generated
Normal file
@ -0,0 +1,158 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'asset_stack.provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$assetStackIndexHash() => r'0f2df55e929767c8c698bd432b5e6e351d000a16';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
_SystemHash._();
|
||||
|
||||
static int combine(int hash, int value) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + value);
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||
return hash ^ (hash >> 6);
|
||||
}
|
||||
|
||||
static int finish(int hash) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||
// ignore: parameter_assignments
|
||||
hash = hash ^ (hash >> 11);
|
||||
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||
}
|
||||
}
|
||||
|
||||
/// See also [assetStackIndex].
|
||||
@ProviderFor(assetStackIndex)
|
||||
const assetStackIndexProvider = AssetStackIndexFamily();
|
||||
|
||||
/// See also [assetStackIndex].
|
||||
class AssetStackIndexFamily extends Family<int> {
|
||||
/// See also [assetStackIndex].
|
||||
const AssetStackIndexFamily();
|
||||
|
||||
/// See also [assetStackIndex].
|
||||
AssetStackIndexProvider call(
|
||||
Asset asset,
|
||||
) {
|
||||
return AssetStackIndexProvider(
|
||||
asset,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AssetStackIndexProvider getProviderOverride(
|
||||
covariant AssetStackIndexProvider provider,
|
||||
) {
|
||||
return call(
|
||||
provider.asset,
|
||||
);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'assetStackIndexProvider';
|
||||
}
|
||||
|
||||
/// See also [assetStackIndex].
|
||||
class AssetStackIndexProvider extends AutoDisposeProvider<int> {
|
||||
/// See also [assetStackIndex].
|
||||
AssetStackIndexProvider(
|
||||
Asset asset,
|
||||
) : this._internal(
|
||||
(ref) => assetStackIndex(
|
||||
ref as AssetStackIndexRef,
|
||||
asset,
|
||||
),
|
||||
from: assetStackIndexProvider,
|
||||
name: r'assetStackIndexProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$assetStackIndexHash,
|
||||
dependencies: AssetStackIndexFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
AssetStackIndexFamily._allTransitiveDependencies,
|
||||
asset: asset,
|
||||
);
|
||||
|
||||
AssetStackIndexProvider._internal(
|
||||
super._createNotifier, {
|
||||
required super.name,
|
||||
required super.dependencies,
|
||||
required super.allTransitiveDependencies,
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.asset,
|
||||
}) : super.internal();
|
||||
|
||||
final Asset asset;
|
||||
|
||||
@override
|
||||
Override overrideWith(
|
||||
int Function(AssetStackIndexRef provider) create,
|
||||
) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
override: AssetStackIndexProvider._internal(
|
||||
(ref) => create(ref as AssetStackIndexRef),
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
asset: asset,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AutoDisposeProviderElement<int> createElement() {
|
||||
return _AssetStackIndexProviderElement(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is AssetStackIndexProvider && other.asset == asset;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, asset.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
mixin AssetStackIndexRef on AutoDisposeProviderRef<int> {
|
||||
/// The parameter `asset` of this provider.
|
||||
Asset get asset;
|
||||
}
|
||||
|
||||
class _AssetStackIndexProviderElement extends AutoDisposeProviderElement<int>
|
||||
with AssetStackIndexRef {
|
||||
_AssetStackIndexProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
Asset get asset => (origin as AssetStackIndexProvider).asset;
|
||||
}
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
@ -0,0 +1,15 @@
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'current_asset.provider.g.dart';
|
||||
|
||||
@riverpod
|
||||
class CurrentAsset extends _$CurrentAsset {
|
||||
@override
|
||||
Asset? build() => null;
|
||||
|
||||
void set(Asset? a) => state = a;
|
||||
}
|
||||
|
||||
/// Mock class for testing
|
||||
abstract class CurrentAssetInternal extends _$CurrentAsset {}
|
25
mobile/lib/providers/asset_viewer/current_asset.provider.g.dart
generated
Normal file
25
mobile/lib/providers/asset_viewer/current_asset.provider.g.dart
generated
Normal file
@ -0,0 +1,25 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'current_asset.provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$currentAssetHash() => r'2def10ea594152c984ae2974d687ab6856d7bdd0';
|
||||
|
||||
/// See also [CurrentAsset].
|
||||
@ProviderFor(CurrentAsset)
|
||||
final currentAssetProvider =
|
||||
AutoDisposeNotifierProvider<CurrentAsset, Asset?>.internal(
|
||||
CurrentAsset.new,
|
||||
name: r'currentAssetProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$currentAssetHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$CurrentAsset = AutoDisposeNotifier<Asset?>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
@ -0,0 +1,95 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/services/album.service.dart';
|
||||
import 'package:immich_mobile/models/asset_viewer/asset_viewer_page_state.model.dart';
|
||||
import 'package:immich_mobile/services/image_viewer.service.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/services/share.service.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
||||
import 'package:immich_mobile/shared/ui/share_dialog.dart';
|
||||
|
||||
class ImageViewerStateNotifier extends StateNotifier<AssetViewerPageState> {
|
||||
final ImageViewerService _imageViewerService;
|
||||
final ShareService _shareService;
|
||||
final AlbumService _albumService;
|
||||
|
||||
ImageViewerStateNotifier(
|
||||
this._imageViewerService,
|
||||
this._shareService,
|
||||
this._albumService,
|
||||
) : super(
|
||||
AssetViewerPageState(
|
||||
downloadAssetStatus: DownloadAssetStatus.idle,
|
||||
),
|
||||
);
|
||||
|
||||
void downloadAsset(Asset asset, BuildContext context) async {
|
||||
state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.loading);
|
||||
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: 'image_viewer_page_state_provider_download_started'.tr(),
|
||||
toastType: ToastType.info,
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
);
|
||||
|
||||
bool isSuccess = await _imageViewerService.downloadAssetToDevice(asset);
|
||||
|
||||
if (isSuccess) {
|
||||
state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.success);
|
||||
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: 'image_viewer_page_state_provider_download_success'.tr(),
|
||||
toastType: ToastType.success,
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
);
|
||||
_albumService.refreshDeviceAlbums();
|
||||
} else {
|
||||
state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.error);
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: 'image_viewer_page_state_provider_download_error'.tr(),
|
||||
toastType: ToastType.error,
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
);
|
||||
}
|
||||
|
||||
state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.idle);
|
||||
}
|
||||
|
||||
void shareAsset(Asset asset, BuildContext context) async {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext buildContext) {
|
||||
_shareService.shareAsset(asset).then(
|
||||
(bool status) {
|
||||
if (!status) {
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: 'image_viewer_page_state_provider_share_error'.tr(),
|
||||
toastType: ToastType.error,
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
);
|
||||
}
|
||||
buildContext.pop();
|
||||
},
|
||||
);
|
||||
return const ShareDialog();
|
||||
},
|
||||
barrierDismissible: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final imageViewerStateProvider =
|
||||
StateNotifierProvider<ImageViewerStateNotifier, AssetViewerPageState>(
|
||||
((ref) => ImageViewerStateNotifier(
|
||||
ref.watch(imageViewerServiceProvider),
|
||||
ref.watch(shareServiceProvider),
|
||||
ref.watch(albumServiceProvider),
|
||||
)),
|
||||
);
|
32
mobile/lib/providers/asset_viewer/render_list.provider.dart
Normal file
32
mobile/lib/providers/asset_viewer/render_list.provider.dart
Normal file
@ -0,0 +1,32 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
|
||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/utils/renderlist_generator.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
final renderListProvider =
|
||||
FutureProvider.family<RenderList, List<Asset>>((ref, assets) {
|
||||
final settings = ref.watch(appSettingsServiceProvider);
|
||||
|
||||
return RenderList.fromAssets(
|
||||
assets,
|
||||
GroupAssetsBy.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)],
|
||||
);
|
||||
});
|
||||
|
||||
final renderListProviderWithGrouping =
|
||||
FutureProvider.family<RenderList, (List<Asset>, GroupAssetsBy?)>(
|
||||
(ref, args) {
|
||||
final settings = ref.watch(appSettingsServiceProvider);
|
||||
final grouping = args.$2 ??
|
||||
GroupAssetsBy.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)];
|
||||
return RenderList.fromAssets(args.$1, grouping);
|
||||
});
|
||||
|
||||
final renderListQueryProvider = StreamProvider.family<RenderList,
|
||||
QueryBuilder<Asset, Asset, QAfterSortBy>?>(
|
||||
(ref, query) =>
|
||||
query == null ? const Stream.empty() : renderListGenerator(query, ref),
|
||||
);
|
@ -0,0 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
final scrollToTopNotifierProvider = ScrollNotifier();
|
||||
|
||||
class ScrollNotifier with ChangeNotifier {
|
||||
void scrollToTop() {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
final scrollToDateNotifierProvider = ScrollToDateNotifier(null);
|
||||
|
||||
class ScrollToDateNotifier extends ValueNotifier<DateTime?> {
|
||||
ScrollToDateNotifier(super.value);
|
||||
|
||||
void scrollToDate(DateTime date) {
|
||||
value = date;
|
||||
|
||||
// Manually notify listeners to trigger the scroll, even if the value hasn't changed
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
final showControlsProvider = StateNotifierProvider<ShowControls, bool>((ref) {
|
||||
return ShowControls(ref);
|
||||
});
|
||||
|
||||
class ShowControls extends StateNotifier<bool> {
|
||||
ShowControls(this.ref) : super(true);
|
||||
|
||||
final Ref ref;
|
||||
|
||||
bool get show => state;
|
||||
|
||||
set show(bool value) {
|
||||
state = value;
|
||||
}
|
||||
|
||||
void toggle() {
|
||||
state = !state;
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
part 'video_player_controller_provider.g.dart';
|
||||
|
||||
@riverpod
|
||||
Future<VideoPlayerController> videoPlayerController(
|
||||
VideoPlayerControllerRef ref, {
|
||||
required Asset asset,
|
||||
}) async {
|
||||
late VideoPlayerController controller;
|
||||
if (asset.isLocal && asset.livePhotoVideoId == null) {
|
||||
// Use a local file for the video player controller
|
||||
final file = await asset.local!.file;
|
||||
if (file == null) {
|
||||
throw Exception('No file found for the video');
|
||||
}
|
||||
controller = VideoPlayerController.file(file);
|
||||
} else {
|
||||
// Use a network URL for the video player controller
|
||||
final serverEndpoint = Store.get(StoreKey.serverEndpoint);
|
||||
final String videoUrl = asset.livePhotoVideoId != null
|
||||
? '$serverEndpoint/asset/file/${asset.livePhotoVideoId}'
|
||||
: '$serverEndpoint/asset/file/${asset.remoteId}';
|
||||
|
||||
final url = Uri.parse(videoUrl);
|
||||
final accessToken = Store.get(StoreKey.accessToken);
|
||||
|
||||
controller = VideoPlayerController.networkUrl(
|
||||
url,
|
||||
httpHeaders: {"x-immich-user-token": accessToken},
|
||||
);
|
||||
}
|
||||
|
||||
await controller.initialize();
|
||||
|
||||
ref.onDispose(() {
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
return controller;
|
||||
}
|
164
mobile/lib/providers/asset_viewer/video_player_controller_provider.g.dart
generated
Normal file
164
mobile/lib/providers/asset_viewer/video_player_controller_provider.g.dart
generated
Normal file
@ -0,0 +1,164 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'video_player_controller_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$videoPlayerControllerHash() =>
|
||||
r'40b31f7b1a73fab84c311b0f06bedf5322143cd9';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
_SystemHash._();
|
||||
|
||||
static int combine(int hash, int value) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + value);
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||
return hash ^ (hash >> 6);
|
||||
}
|
||||
|
||||
static int finish(int hash) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||
// ignore: parameter_assignments
|
||||
hash = hash ^ (hash >> 11);
|
||||
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||
}
|
||||
}
|
||||
|
||||
/// See also [videoPlayerController].
|
||||
@ProviderFor(videoPlayerController)
|
||||
const videoPlayerControllerProvider = VideoPlayerControllerFamily();
|
||||
|
||||
/// See also [videoPlayerController].
|
||||
class VideoPlayerControllerFamily
|
||||
extends Family<AsyncValue<VideoPlayerController>> {
|
||||
/// See also [videoPlayerController].
|
||||
const VideoPlayerControllerFamily();
|
||||
|
||||
/// See also [videoPlayerController].
|
||||
VideoPlayerControllerProvider call({
|
||||
required Asset asset,
|
||||
}) {
|
||||
return VideoPlayerControllerProvider(
|
||||
asset: asset,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
VideoPlayerControllerProvider getProviderOverride(
|
||||
covariant VideoPlayerControllerProvider provider,
|
||||
) {
|
||||
return call(
|
||||
asset: provider.asset,
|
||||
);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'videoPlayerControllerProvider';
|
||||
}
|
||||
|
||||
/// See also [videoPlayerController].
|
||||
class VideoPlayerControllerProvider
|
||||
extends AutoDisposeFutureProvider<VideoPlayerController> {
|
||||
/// See also [videoPlayerController].
|
||||
VideoPlayerControllerProvider({
|
||||
required Asset asset,
|
||||
}) : this._internal(
|
||||
(ref) => videoPlayerController(
|
||||
ref as VideoPlayerControllerRef,
|
||||
asset: asset,
|
||||
),
|
||||
from: videoPlayerControllerProvider,
|
||||
name: r'videoPlayerControllerProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$videoPlayerControllerHash,
|
||||
dependencies: VideoPlayerControllerFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
VideoPlayerControllerFamily._allTransitiveDependencies,
|
||||
asset: asset,
|
||||
);
|
||||
|
||||
VideoPlayerControllerProvider._internal(
|
||||
super._createNotifier, {
|
||||
required super.name,
|
||||
required super.dependencies,
|
||||
required super.allTransitiveDependencies,
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.asset,
|
||||
}) : super.internal();
|
||||
|
||||
final Asset asset;
|
||||
|
||||
@override
|
||||
Override overrideWith(
|
||||
FutureOr<VideoPlayerController> Function(VideoPlayerControllerRef provider)
|
||||
create,
|
||||
) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
override: VideoPlayerControllerProvider._internal(
|
||||
(ref) => create(ref as VideoPlayerControllerRef),
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
asset: asset,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AutoDisposeFutureProviderElement<VideoPlayerController> createElement() {
|
||||
return _VideoPlayerControllerProviderElement(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is VideoPlayerControllerProvider && other.asset == asset;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, asset.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
mixin VideoPlayerControllerRef
|
||||
on AutoDisposeFutureProviderRef<VideoPlayerController> {
|
||||
/// The parameter `asset` of this provider.
|
||||
Asset get asset;
|
||||
}
|
||||
|
||||
class _VideoPlayerControllerProviderElement
|
||||
extends AutoDisposeFutureProviderElement<VideoPlayerController>
|
||||
with VideoPlayerControllerRef {
|
||||
_VideoPlayerControllerProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
Asset get asset => (origin as VideoPlayerControllerProvider).asset;
|
||||
}
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
@ -0,0 +1,96 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
class VideoPlaybackControls {
|
||||
VideoPlaybackControls({
|
||||
required this.position,
|
||||
required this.mute,
|
||||
required this.pause,
|
||||
});
|
||||
|
||||
final double position;
|
||||
final bool mute;
|
||||
final bool pause;
|
||||
}
|
||||
|
||||
final videoPlayerControlsProvider =
|
||||
StateNotifierProvider<VideoPlayerControls, VideoPlaybackControls>((ref) {
|
||||
return VideoPlayerControls(ref);
|
||||
});
|
||||
|
||||
class VideoPlayerControls extends StateNotifier<VideoPlaybackControls> {
|
||||
VideoPlayerControls(this.ref)
|
||||
: super(
|
||||
VideoPlaybackControls(
|
||||
position: 0,
|
||||
pause: false,
|
||||
mute: false,
|
||||
),
|
||||
);
|
||||
|
||||
final Ref ref;
|
||||
|
||||
VideoPlaybackControls get value => state;
|
||||
|
||||
set value(VideoPlaybackControls value) {
|
||||
state = value;
|
||||
}
|
||||
|
||||
void reset() {
|
||||
state = VideoPlaybackControls(
|
||||
position: 0,
|
||||
pause: false,
|
||||
mute: false,
|
||||
);
|
||||
}
|
||||
|
||||
double get position => state.position;
|
||||
bool get mute => state.mute;
|
||||
|
||||
set position(double value) {
|
||||
state = VideoPlaybackControls(
|
||||
position: value,
|
||||
mute: state.mute,
|
||||
pause: state.pause,
|
||||
);
|
||||
}
|
||||
|
||||
set mute(bool value) {
|
||||
state = VideoPlaybackControls(
|
||||
position: state.position,
|
||||
mute: value,
|
||||
pause: state.pause,
|
||||
);
|
||||
}
|
||||
|
||||
void toggleMute() {
|
||||
state = VideoPlaybackControls(
|
||||
position: state.position,
|
||||
mute: !state.mute,
|
||||
pause: state.pause,
|
||||
);
|
||||
}
|
||||
|
||||
void pause() {
|
||||
state = VideoPlaybackControls(
|
||||
position: state.position,
|
||||
mute: state.mute,
|
||||
pause: true,
|
||||
);
|
||||
}
|
||||
|
||||
void play() {
|
||||
state = VideoPlaybackControls(
|
||||
position: state.position,
|
||||
mute: state.mute,
|
||||
pause: false,
|
||||
);
|
||||
}
|
||||
|
||||
void togglePlay() {
|
||||
state = VideoPlaybackControls(
|
||||
position: state.position,
|
||||
mute: state.mute,
|
||||
pause: !state.pause,
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,92 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
enum VideoPlaybackState {
|
||||
initializing,
|
||||
paused,
|
||||
playing,
|
||||
buffering,
|
||||
completed,
|
||||
}
|
||||
|
||||
class VideoPlaybackValue {
|
||||
/// The current position of the video
|
||||
final Duration position;
|
||||
|
||||
/// The total duration of the video
|
||||
final Duration duration;
|
||||
|
||||
/// The current state of the video playback
|
||||
final VideoPlaybackState state;
|
||||
|
||||
/// The volume of the video
|
||||
final double volume;
|
||||
|
||||
VideoPlaybackValue({
|
||||
required this.position,
|
||||
required this.duration,
|
||||
required this.state,
|
||||
required this.volume,
|
||||
});
|
||||
|
||||
factory VideoPlaybackValue.fromController(VideoPlayerController? controller) {
|
||||
final video = controller?.value;
|
||||
late VideoPlaybackState s;
|
||||
if (video == null) {
|
||||
s = VideoPlaybackState.initializing;
|
||||
} else if (video.isCompleted) {
|
||||
s = VideoPlaybackState.completed;
|
||||
} else if (video.isPlaying) {
|
||||
s = VideoPlaybackState.playing;
|
||||
} else if (video.isBuffering) {
|
||||
s = VideoPlaybackState.buffering;
|
||||
} else {
|
||||
s = VideoPlaybackState.paused;
|
||||
}
|
||||
|
||||
return VideoPlaybackValue(
|
||||
position: video?.position ?? Duration.zero,
|
||||
duration: video?.duration ?? Duration.zero,
|
||||
state: s,
|
||||
volume: video?.volume ?? 0.0,
|
||||
);
|
||||
}
|
||||
|
||||
factory VideoPlaybackValue.uninitialized() {
|
||||
return VideoPlaybackValue(
|
||||
position: Duration.zero,
|
||||
duration: Duration.zero,
|
||||
state: VideoPlaybackState.initializing,
|
||||
volume: 0.0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final videoPlaybackValueProvider =
|
||||
StateNotifierProvider<VideoPlaybackValueState, VideoPlaybackValue>((ref) {
|
||||
return VideoPlaybackValueState(ref);
|
||||
});
|
||||
|
||||
class VideoPlaybackValueState extends StateNotifier<VideoPlaybackValue> {
|
||||
VideoPlaybackValueState(this.ref)
|
||||
: super(
|
||||
VideoPlaybackValue.uninitialized(),
|
||||
);
|
||||
|
||||
final Ref ref;
|
||||
|
||||
VideoPlaybackValue get value => state;
|
||||
|
||||
set value(VideoPlaybackValue value) {
|
||||
state = value;
|
||||
}
|
||||
|
||||
set position(Duration value) {
|
||||
state = VideoPlaybackValue(
|
||||
position: value,
|
||||
duration: state.duration,
|
||||
state: state.state,
|
||||
volume: state.volume,
|
||||
);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user