refactor(mobile): services and providers (#9232)

* refactor(mobile): services and provider

* providers
This commit is contained in:
Alex
2024-05-02 15:59:14 -05:00
committed by GitHub
parent ec4eb7cd19
commit c1253663b7
242 changed files with 497 additions and 503 deletions

View File

@@ -1,50 +0,0 @@
import 'dart:async';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:immich_mobile/modules/asset_viewer/image_providers/exceptions/image_loading_exception.dart';
import 'package:immich_mobile/entities/store.entity.dart';
/// Loads the codec from the URI and sends the events to the [chunkEvents] stream
///
/// Credit to [flutter_cached_network_image](https://github.com/Baseflow/flutter_cached_network_image/blob/develop/cached_network_image/lib/src/image_provider/_image_loader.dart)
/// for this wonderful implementation of their image loader
class ImageLoader {
static Future<ui.Codec> loadImageFromCache(
String uri, {
required CacheManager cache,
required ImageDecoderCallback decode,
StreamController<ImageChunkEvent>? chunkEvents,
}) async {
final headers = {
'x-immich-user-token': Store.get(StoreKey.accessToken),
};
final stream = cache.getFileStream(
uri,
withProgress: chunkEvents != null,
headers: headers,
);
await for (final result in stream) {
if (result is DownloadProgress) {
// We are downloading the file, so update the [chunkEvents]
chunkEvents?.add(
ImageChunkEvent(
cumulativeBytesLoaded: result.downloaded,
expectedTotalBytes: result.totalSize,
),
);
} else if (result is FileInfo) {
// We have the file
final buffer = await ui.ImmutableBuffer.fromFilePath(result.file.path);
final decoded = await decode(buffer);
return decoded;
}
}
// If we get here, the image failed to load from the cache stream
throw ImageLoadingException('Could not load image from stream');
}
}

View File

@@ -1,20 +0,0 @@
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
/// The cache manager for full size images [ImmichRemoteImageProvider]
class RemoteImageCacheManager extends CacheManager {
static const key = 'remoteImageCacheKey';
static final RemoteImageCacheManager _instance = RemoteImageCacheManager._();
factory RemoteImageCacheManager() {
return _instance;
}
RemoteImageCacheManager._()
: super(
Config(
key,
maxNrOfCacheObjects: 500,
stalePeriod: const Duration(days: 30),
),
);
}

View File

@@ -1,21 +0,0 @@
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
/// The cache manager for thumbnail images [ImmichRemoteThumbnailProvider]
class ThumbnailImageCacheManager extends CacheManager {
static const key = 'thumbnailImageCacheKey';
static final ThumbnailImageCacheManager _instance =
ThumbnailImageCacheManager._();
factory ThumbnailImageCacheManager() {
return _instance;
}
ThumbnailImageCacheManager._()
: super(
Config(
key,
maxNrOfCacheObjects: 5000,
stalePeriod: const Duration(days: 30),
),
);
}

View File

@@ -1,5 +0,0 @@
/// An exception for the [ImageLoader] and the Immich image providers
class ImageLoadingException implements Exception {
final String message;
ImageLoadingException(this.message);
}

View File

@@ -1,106 +0,0 @@
import 'dart:async';
import 'dart:io';
import 'dart:ui' as ui;
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:photo_manager/photo_manager.dart';
/// The local image provider for an asset
class ImmichLocalImageProvider extends ImageProvider<ImmichLocalImageProvider> {
final Asset asset;
ImmichLocalImageProvider({
required this.asset,
}) : assert(asset.local != null, 'Only usable when asset.local is set');
/// Converts an [ImageProvider]'s settings plus an [ImageConfiguration] to a key
/// that describes the precise image to load.
@override
Future<ImmichLocalImageProvider> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture(this);
}
@override
ImageStreamCompleter loadImage(
ImmichLocalImageProvider key,
ImageDecoderCallback decode,
) {
final chunkEvents = StreamController<ImageChunkEvent>();
return MultiImageStreamCompleter(
codec: _codec(key.asset, decode, chunkEvents),
scale: 1.0,
chunkEvents: chunkEvents.stream,
informationCollector: () sync* {
yield ErrorDescription(asset.fileName);
},
);
}
// Streams in each stage of the image as we ask for it
Stream<ui.Codec> _codec(
Asset key,
ImageDecoderCallback decode,
StreamController<ImageChunkEvent> chunkEvents,
) async* {
// Load a small thumbnail
final thumbBytes = await asset.local?.thumbnailDataWithSize(
const ThumbnailSize.square(256),
quality: 80,
);
if (thumbBytes != null) {
final buffer = await ui.ImmutableBuffer.fromUint8List(thumbBytes);
final codec = await decode(buffer);
yield codec;
} else {
debugPrint("Loading thumb for ${asset.fileName} failed");
}
if (asset.isImage) {
/// Using 2K thumbnail for local iOS image to avoid double swiping issue
if (Platform.isIOS) {
final largeImageBytes = await asset.local
?.thumbnailDataWithSize(const ThumbnailSize(3840, 2160));
if (largeImageBytes == null) {
throw StateError(
"Loading thumb for local photo ${asset.fileName} failed",
);
}
final buffer = await ui.ImmutableBuffer.fromUint8List(largeImageBytes);
final codec = await decode(buffer);
yield codec;
} else {
// Use the original file for Android
final File? file = await asset.local?.originFile;
if (file == null) {
throw StateError("Opening file for asset ${asset.fileName} failed");
}
try {
final buffer = await ui.ImmutableBuffer.fromFilePath(file.path);
final codec = await decode(buffer);
yield codec;
} catch (error) {
throw StateError("Loading asset ${asset.fileName} failed");
}
}
}
chunkEvents.close();
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (other is ImmichLocalImageProvider) {
return asset == other.asset;
}
return false;
}
@override
int get hashCode => asset.hashCode;
}

View File

@@ -1,92 +0,0 @@
import 'dart:async';
import 'dart:ui' as ui;
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:photo_manager/photo_manager.dart';
/// The local image provider for an asset
/// Only viable
class ImmichLocalThumbnailProvider
extends ImageProvider<ImmichLocalThumbnailProvider> {
final Asset asset;
final int height;
final int width;
ImmichLocalThumbnailProvider({
required this.asset,
this.height = 256,
this.width = 256,
}) : assert(asset.local != null, 'Only usable when asset.local is set');
/// Converts an [ImageProvider]'s settings plus an [ImageConfiguration] to a key
/// that describes the precise image to load.
@override
Future<ImmichLocalThumbnailProvider> obtainKey(
ImageConfiguration configuration,
) {
return SynchronousFuture(this);
}
@override
ImageStreamCompleter loadImage(
ImmichLocalThumbnailProvider key,
ImageDecoderCallback decode,
) {
final chunkEvents = StreamController<ImageChunkEvent>();
return MultiImageStreamCompleter(
codec: _codec(key.asset, decode, chunkEvents),
scale: 1.0,
chunkEvents: chunkEvents.stream,
informationCollector: () sync* {
yield ErrorDescription(asset.fileName);
},
);
}
// Streams in each stage of the image as we ask for it
Stream<ui.Codec> _codec(
Asset key,
ImageDecoderCallback decode,
StreamController<ImageChunkEvent> chunkEvents,
) async* {
// Load a small thumbnail
final thumbBytes = await asset.local?.thumbnailDataWithSize(
const ThumbnailSize.square(32),
quality: 75,
);
if (thumbBytes != null) {
final buffer = await ui.ImmutableBuffer.fromUint8List(thumbBytes);
final codec = await decode(buffer);
yield codec;
} else {
debugPrint("Loading thumb for ${asset.fileName} failed");
}
final normalThumbBytes =
await asset.local?.thumbnailDataWithSize(ThumbnailSize(width, height));
if (normalThumbBytes == null) {
throw StateError(
"Loading thumb for local photo ${asset.fileName} failed",
);
}
final buffer = await ui.ImmutableBuffer.fromUint8List(normalThumbBytes);
final codec = await decode(buffer);
yield codec;
chunkEvents.close();
}
@override
bool operator ==(Object other) {
if (other is! ImmichLocalThumbnailProvider) return false;
if (identical(this, other)) return true;
return asset == other.asset;
}
@override
int get hashCode => asset.hashCode;
}

View File

@@ -1,128 +0,0 @@
import 'dart:async';
import 'dart:ui' as ui;
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:immich_mobile/modules/asset_viewer/image_providers/cache/image_loader.dart';
import 'package:immich_mobile/modules/asset_viewer/image_providers/cache/remote_image_cache_manager.dart';
import 'package:openapi/api.dart' as api;
import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
/// The remote image provider for full size remote images
class ImmichRemoteImageProvider
extends ImageProvider<ImmichRemoteImageProvider> {
/// The [Asset.remoteId] of the asset to fetch
final String assetId;
/// The image cache manager
final CacheManager? cacheManager;
ImmichRemoteImageProvider({
required this.assetId,
this.cacheManager,
});
/// Converts an [ImageProvider]'s settings plus an [ImageConfiguration] to a key
/// that describes the precise image to load.
@override
Future<ImmichRemoteImageProvider> obtainKey(
ImageConfiguration configuration,
) {
return SynchronousFuture(this);
}
@override
ImageStreamCompleter loadImage(
ImmichRemoteImageProvider key,
ImageDecoderCallback decode,
) {
final cache = cacheManager ?? RemoteImageCacheManager();
final chunkEvents = StreamController<ImageChunkEvent>();
return MultiImageStreamCompleter(
codec: _codec(key, cache, decode, chunkEvents),
scale: 1.0,
chunkEvents: chunkEvents.stream,
);
}
/// Whether to show the original file or load a compressed version
bool get _useOriginal => Store.get(
AppSettingsEnum.loadOriginal.storeKey,
AppSettingsEnum.loadOriginal.defaultValue,
);
/// Whether to load the preview thumbnail first or not
bool get _loadPreview => Store.get(
AppSettingsEnum.loadPreview.storeKey,
AppSettingsEnum.loadPreview.defaultValue,
);
// Streams in each stage of the image as we ask for it
Stream<ui.Codec> _codec(
ImmichRemoteImageProvider key,
CacheManager cache,
ImageDecoderCallback decode,
StreamController<ImageChunkEvent> chunkEvents,
) async* {
// Load a preview to the chunk events
if (_loadPreview) {
final preview = getThumbnailUrlForRemoteId(
key.assetId,
type: api.ThumbnailFormat.WEBP,
);
yield await ImageLoader.loadImageFromCache(
preview,
cache: cache,
decode: decode,
chunkEvents: chunkEvents,
);
}
// Load the higher resolution version of the image
final url = getThumbnailUrlForRemoteId(
key.assetId,
type: api.ThumbnailFormat.JPEG,
);
final codec = await ImageLoader.loadImageFromCache(
url,
cache: cache,
decode: decode,
chunkEvents: chunkEvents,
);
yield codec;
// Load the final remote image
if (_useOriginal) {
// Load the original image
final url = getImageUrlFromId(key.assetId);
final codec = await ImageLoader.loadImageFromCache(
url,
cache: cache,
decode: decode,
chunkEvents: chunkEvents,
);
yield codec;
}
await chunkEvents.close();
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (other is ImmichRemoteImageProvider) {
return assetId == other.assetId;
}
return false;
}
@override
int get hashCode => assetId.hashCode;
}

View File

@@ -1,86 +0,0 @@
import 'dart:async';
import 'dart:ui' as ui;
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:immich_mobile/modules/asset_viewer/image_providers/cache/image_loader.dart';
import 'package:immich_mobile/modules/asset_viewer/image_providers/cache/thumbnail_image_cache_manager.dart';
import 'package:openapi/api.dart' as api;
import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
/// The remote image provider
class ImmichRemoteThumbnailProvider
extends ImageProvider<ImmichRemoteThumbnailProvider> {
/// The [Asset.remoteId] of the asset to fetch
final String assetId;
final int? height;
final int? width;
/// The image cache manager
final CacheManager? cacheManager;
ImmichRemoteThumbnailProvider({
required this.assetId,
this.height,
this.width,
this.cacheManager,
});
/// Converts an [ImageProvider]'s settings plus an [ImageConfiguration] to a key
/// that describes the precise image to load.
@override
Future<ImmichRemoteThumbnailProvider> obtainKey(
ImageConfiguration configuration,
) {
return SynchronousFuture(this);
}
@override
ImageStreamCompleter loadImage(
ImmichRemoteThumbnailProvider key,
ImageDecoderCallback decode,
) {
final cache = cacheManager ?? ThumbnailImageCacheManager();
return MultiImageStreamCompleter(
codec: _codec(key, cache, decode),
scale: 1.0,
);
}
// Streams in each stage of the image as we ask for it
Stream<ui.Codec> _codec(
ImmichRemoteThumbnailProvider key,
CacheManager cache,
ImageDecoderCallback decode,
) async* {
// Load a preview to the chunk events
final preview = getThumbnailUrlForRemoteId(
key.assetId,
type: api.ThumbnailFormat.WEBP,
);
yield await ImageLoader.loadImageFromCache(
preview,
cache: cache,
decode: decode,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (other is ImmichRemoteThumbnailProvider) {
return assetId == other.assetId;
}
return false;
}
@override
int get hashCode => assetId.hashCode;
}

View File

@@ -1,87 +0,0 @@
import 'dart:async';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/asset_viewer/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/shared/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,
),
);

View File

@@ -1,51 +0,0 @@
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/shared/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();
}
}

View File

@@ -1,189 +0,0 @@
// 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

View File

@@ -1,59 +0,0 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/shared/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;
}

View File

@@ -1,158 +0,0 @@
// 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

View File

@@ -1,15 +0,0 @@
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 {}

View File

@@ -1,25 +0,0 @@
// 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

View File

@@ -1,95 +0,0 @@
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/modules/album/services/album.service.dart';
import 'package:immich_mobile/models/asset_viewer/asset_viewer_page_state.model.dart';
import 'package:immich_mobile/modules/asset_viewer/services/image_viewer.service.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/shared/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),
)),
);

View File

@@ -1,32 +0,0 @@
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/modules/settings/providers/app_settings.provider.dart';
import 'package:immich_mobile/modules/settings/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),
);

View File

@@ -1,9 +0,0 @@
import 'package:flutter/material.dart';
final scrollToTopNotifierProvider = ScrollNotifier();
class ScrollNotifier with ChangeNotifier {
void scrollToTop() {
notifyListeners();
}
}

View File

@@ -1,14 +0,0 @@
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();
}
}

View File

@@ -1,21 +0,0 @@
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;
}
}

View File

@@ -1,44 +0,0 @@
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;
}

View File

@@ -1,164 +0,0 @@
// 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

View File

@@ -1,96 +0,0 @@
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,
);
}
}

View File

@@ -1,92 +0,0 @@
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,
);
}
}

View File

@@ -1,62 +0,0 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/exif_info.entity.dart';
import 'package:immich_mobile/shared/providers/api.provider.dart';
import 'package:immich_mobile/shared/providers/db.provider.dart';
import 'package:immich_mobile/shared/services/api.service.dart';
import 'package:isar/isar.dart';
import 'package:openapi/api.dart';
class AssetDescriptionService {
AssetDescriptionService(this._db, this._api);
final Isar _db;
final ApiService _api;
setDescription(
String description,
String remoteAssetId,
int localExifId,
) async {
final result = await _api.assetApi.updateAsset(
remoteAssetId,
UpdateAssetDto(description: description),
);
if (result?.exifInfo?.description != null) {
var exifInfo = await _db.exifInfos.get(localExifId);
if (exifInfo != null) {
exifInfo.description = result!.exifInfo!.description;
await _db.writeTxn(
() => _db.exifInfos.put(exifInfo),
);
}
}
}
Future<String> readLatest(String assetRemoteId, int localExifId) async {
final latestAssetFromServer =
await _api.assetApi.getAssetInfo(assetRemoteId);
final localExifInfo = await _db.exifInfos.get(localExifId);
if (latestAssetFromServer != null && localExifInfo != null) {
localExifInfo.description =
latestAssetFromServer.exifInfo?.description ?? '';
await _db.writeTxn(
() => _db.exifInfos.put(localExifInfo),
);
return localExifInfo.description!;
}
return "";
}
}
final assetDescriptionServiceProvider = Provider(
(ref) => AssetDescriptionService(
ref.watch(dbProvider),
ref.watch(apiServiceProvider),
),
);

View File

@@ -1,72 +0,0 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/shared/providers/api.provider.dart';
import 'package:immich_mobile/shared/services/api.service.dart';
import 'package:openapi/api.dart';
class AssetStackService {
AssetStackService(this._api);
final ApiService _api;
Future<void> updateStack(
Asset parentAsset, {
List<Asset>? childrenToAdd,
List<Asset>? childrenToRemove,
}) async {
// Guard [local asset]
if (parentAsset.remoteId == null) {
return;
}
try {
if (childrenToAdd != null) {
final toAdd = childrenToAdd
.where((e) => e.isRemote)
.map((e) => e.remoteId!)
.toList();
await _api.assetApi.updateAssets(
AssetBulkUpdateDto(ids: toAdd, stackParentId: parentAsset.remoteId),
);
}
if (childrenToRemove != null) {
final toRemove = childrenToRemove
.where((e) => e.isRemote)
.map((e) => e.remoteId!)
.toList();
await _api.assetApi.updateAssets(
AssetBulkUpdateDto(ids: toRemove, removeParent: true),
);
}
} catch (error) {
debugPrint("Error while updating stack children: ${error.toString()}");
}
}
Future<void> updateStackParent(Asset oldParent, Asset newParent) async {
// Guard [local asset]
if (oldParent.remoteId == null || newParent.remoteId == null) {
return;
}
try {
await _api.assetApi.updateStackParent(
UpdateStackParentDto(
oldParentId: oldParent.remoteId!,
newParentId: newParent.remoteId!,
),
);
} catch (error) {
debugPrint("Error while updating stack parent: ${error.toString()}");
}
}
}
final assetStackServiceProvider = Provider(
(ref) => AssetStackService(
ref.watch(apiServiceProvider),
),
);

View File

@@ -1,109 +0,0 @@
import 'dart:io';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/response_extensions.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/shared/providers/api.provider.dart';
import 'package:immich_mobile/shared/services/api.service.dart';
import 'package:logging/logging.dart';
import 'package:photo_manager/photo_manager.dart';
import 'package:path_provider/path_provider.dart';
final imageViewerServiceProvider =
Provider((ref) => ImageViewerService(ref.watch(apiServiceProvider)));
class ImageViewerService {
final ApiService _apiService;
final Logger _log = Logger("ImageViewerService");
ImageViewerService(this._apiService);
Future<bool> downloadAssetToDevice(Asset asset) async {
File? imageFile;
File? videoFile;
try {
// Download LivePhotos image and motion part
if (asset.isImage && asset.livePhotoVideoId != null && Platform.isIOS) {
var imageResponse =
await _apiService.downloadApi.downloadFileWithHttpInfo(
asset.remoteId!,
);
var motionReponse =
await _apiService.downloadApi.downloadFileWithHttpInfo(
asset.livePhotoVideoId!,
);
if (imageResponse.statusCode != 200 ||
motionReponse.statusCode != 200) {
final failedResponse =
imageResponse.statusCode != 200 ? imageResponse : motionReponse;
_log.severe(
"Motion asset download failed",
failedResponse.toLoggerString(),
);
return false;
}
AssetEntity? entity;
final tempDir = await getTemporaryDirectory();
videoFile = await File('${tempDir.path}/livephoto.mov').create();
imageFile = await File('${tempDir.path}/livephoto.heic').create();
videoFile.writeAsBytesSync(motionReponse.bodyBytes);
imageFile.writeAsBytesSync(imageResponse.bodyBytes);
entity = await PhotoManager.editor.darwin.saveLivePhoto(
imageFile: imageFile,
videoFile: videoFile,
title: asset.fileName,
);
if (entity == null) {
_log.warning(
"Asset cannot be saved as a live photo. This is most likely a motion photo. Saving only the image file",
);
entity = await PhotoManager.editor.saveImage(
imageResponse.bodyBytes,
title: asset.fileName,
);
}
return entity != null;
} else {
var res = await _apiService.downloadApi
.downloadFileWithHttpInfo(asset.remoteId!);
if (res.statusCode != 200) {
_log.severe("Asset download failed", res.toLoggerString());
return false;
}
final AssetEntity? entity;
if (asset.isImage) {
entity = await PhotoManager.editor.saveImage(
res.bodyBytes,
title: asset.fileName,
);
} else {
final tempDir = await getTemporaryDirectory();
videoFile = await File('${tempDir.path}/${asset.fileName}').create();
videoFile.writeAsBytesSync(res.bodyBytes);
entity = await PhotoManager.editor
.saveVideo(videoFile, title: asset.fileName);
}
return entity != null;
}
} catch (error, stack) {
_log.severe("Error saving downloaded asset", error, stack);
return false;
} finally {
// Clear temp files
imageFile?.delete();
videoFile?.delete();
}
}
}

View File

@@ -6,17 +6,17 @@ 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/modules/asset_viewer/providers/asset_stack.provider.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/show_controls.provider.dart';
import 'package:immich_mobile/modules/asset_viewer/services/asset_stack.service.dart';
import 'package:immich_mobile/providers/asset_viewer/asset_stack.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/image_viewer_page_state.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart';
import 'package:immich_mobile/services/asset_stack.service.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/video_controls.dart';
import 'package:immich_mobile/modules/home/ui/delete_dialog.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
import 'package:immich_mobile/shared/providers/user.provider.dart';
import 'package:immich_mobile/providers/asset.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/shared/ui/immich_toast.dart';
class BottomGalleryBar extends ConsumerWidget {

View File

@@ -1,9 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/show_controls.provider.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/video_player_controls_provider.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/video_player_value_provider.dart';
import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart';
import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/center_play_button.dart';
import 'package:immich_mobile/shared/ui/delayed_loading_indicator.dart';
import 'package:immich_mobile/shared/ui/hooks/timer_hook.dart';

View File

@@ -3,9 +3,9 @@ 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/modules/asset_viewer/providers/asset_description.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/asset_description.provider.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/shared/providers/user.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/shared/ui/immich_toast.dart';
import 'package:logging/logging.dart';

View File

@@ -11,7 +11,7 @@ import 'package:immich_mobile/modules/asset_viewer/ui/exif_sheet/exif_location.d
import 'package:immich_mobile/modules/asset_viewer/ui/exif_sheet/exif_people.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/exif_info.entity.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:immich_mobile/providers/asset.provider.dart';
import 'package:immich_mobile/utils/selection_handlers.dart';
class ExifBottomSheet extends HookConsumerWidget {

View File

@@ -5,7 +5,7 @@ import 'package:easy_localization/easy_localization.dart';
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/modules/asset_viewer/providers/asset_people.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/asset_people.provider.dart';
import 'package:immich_mobile/models/search/search_curated_content.model.dart';
import 'package:immich_mobile/modules/search/ui/curated_people_row.dart';
import 'package:immich_mobile/modules/search/ui/person_name_edit_form.dart';

View File

@@ -2,19 +2,19 @@ import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/album/providers/current_album.provider.dart';
import 'package:immich_mobile/providers/album/current_album.provider.dart';
import 'package:immich_mobile/modules/album/ui/add_to_album_bottom_sheet.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/show_controls.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/image_viewer_page_state.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart';
import 'package:immich_mobile/modules/backup/providers/manual_upload.provider.dart';
import 'package:immich_mobile/modules/trash/providers/trashed_asset.provider.dart';
import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
import 'package:immich_mobile/providers/trash.provider.dart';
import 'package:immich_mobile/modules/home/ui/upload_dialog.dart';
import 'package:immich_mobile/modules/partner/providers/partner.provider.dart';
import 'package:immich_mobile/providers/partner.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:immich_mobile/shared/providers/user.provider.dart';
import 'package:immich_mobile/providers/asset.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/shared/ui/immich_toast.dart';
class GalleryAppBar extends ConsumerWidget {

View File

@@ -1,10 +1,10 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/activities/providers/activity_statistics.provider.dart';
import 'package:immich_mobile/modules/album/providers/current_album.provider.dart';
import 'package:immich_mobile/providers/activity_statistics.provider.dart';
import 'package:immich_mobile/providers/album/current_album.provider.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:immich_mobile/providers/asset.provider.dart';
class TopControlAppBar extends HookConsumerWidget {
const TopControlAppBar({

View File

@@ -2,9 +2,9 @@ import 'dart:math';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/show_controls.provider.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/video_player_controls_provider.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/video_player_value_provider.dart';
import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart';
import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart';
/// The video controls for the [videPlayerControlsProvider]
class VideoControls extends ConsumerWidget {

View File

@@ -8,19 +8,19 @@ import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/asset_viewer/image_providers/immich_remote_image_provider.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/asset_stack.provider.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/current_asset.provider.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/show_controls.provider.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/video_player_value_provider.dart';
import 'package:immich_mobile/providers/image/immich_remote_image_provider.dart';
import 'package:immich_mobile/providers/asset_viewer/asset_stack.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/advanced_bottom_sheet.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/bottom_gallery_bar.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/exif_sheet/exif_bottom_sheet.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/gallery_app_bar.dart';
import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart';
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
import 'package:immich_mobile/shared/providers/haptic_feedback.provider.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
import 'package:immich_mobile/shared/ui/immich_image.dart';
import 'package:immich_mobile/shared/ui/immich_thumbnail.dart';
import 'package:immich_mobile/shared/ui/photo_view/photo_view_gallery.dart';

View File

@@ -2,10 +2,10 @@ 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/modules/asset_viewer/providers/show_controls.provider.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/video_player_controller_provider.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/video_player_controls_provider.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/video_player_value_provider.dart';
import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/video_player_controller_provider.dart';
import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart';
import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/video_player.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/shared/ui/delayed_loading_indicator.dart';