platform clients

This commit is contained in:
mertalev 2025-08-29 18:26:42 -04:00
parent 873f7921da
commit c4bd24277a
No known key found for this signature in database
GPG key ID: DF6ABC77AAD98C95
12 changed files with 139 additions and 222 deletions

View file

@ -6,6 +6,9 @@ PODS:
- FlutterMacOS - FlutterMacOS
- connectivity_plus (0.0.1): - connectivity_plus (0.0.1):
- Flutter - Flutter
- cupertino_http (0.0.1):
- Flutter
- FlutterMacOS
- device_info_plus (0.0.1): - device_info_plus (0.0.1):
- Flutter - Flutter
- DKImagePickerController/Core (4.3.9): - DKImagePickerController/Core (4.3.9):
@ -77,6 +80,8 @@ PODS:
- Flutter - Flutter
- network_info_plus (0.0.1): - network_info_plus (0.0.1):
- Flutter - Flutter
- objective_c (0.0.1):
- Flutter
- package_info_plus (0.4.5): - package_info_plus (0.4.5):
- Flutter - Flutter
- path_provider_foundation (0.0.1): - path_provider_foundation (0.0.1):
@ -136,6 +141,7 @@ DEPENDENCIES:
- background_downloader (from `.symlinks/plugins/background_downloader/ios`) - background_downloader (from `.symlinks/plugins/background_downloader/ios`)
- bonsoir_darwin (from `.symlinks/plugins/bonsoir_darwin/darwin`) - bonsoir_darwin (from `.symlinks/plugins/bonsoir_darwin/darwin`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- cupertino_http (from `.symlinks/plugins/cupertino_http/darwin`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- file_picker (from `.symlinks/plugins/file_picker/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`)
- Flutter (from `Flutter`) - Flutter (from `Flutter`)
@ -154,6 +160,7 @@ DEPENDENCIES:
- maplibre_gl (from `.symlinks/plugins/maplibre_gl/ios`) - maplibre_gl (from `.symlinks/plugins/maplibre_gl/ios`)
- native_video_player (from `.symlinks/plugins/native_video_player/ios`) - native_video_player (from `.symlinks/plugins/native_video_player/ios`)
- network_info_plus (from `.symlinks/plugins/network_info_plus/ios`) - network_info_plus (from `.symlinks/plugins/network_info_plus/ios`)
- objective_c (from `.symlinks/plugins/objective_c/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
@ -184,6 +191,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/bonsoir_darwin/darwin" :path: ".symlinks/plugins/bonsoir_darwin/darwin"
connectivity_plus: connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/ios" :path: ".symlinks/plugins/connectivity_plus/ios"
cupertino_http:
:path: ".symlinks/plugins/cupertino_http/darwin"
device_info_plus: device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios" :path: ".symlinks/plugins/device_info_plus/ios"
file_picker: file_picker:
@ -220,6 +229,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/native_video_player/ios" :path: ".symlinks/plugins/native_video_player/ios"
network_info_plus: network_info_plus:
:path: ".symlinks/plugins/network_info_plus/ios" :path: ".symlinks/plugins/network_info_plus/ios"
objective_c:
:path: ".symlinks/plugins/objective_c/ios"
package_info_plus: package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios" :path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation: path_provider_foundation:
@ -249,6 +260,7 @@ SPEC CHECKSUMS:
background_downloader: 50e91d979067b82081aba359d7d916b3ba5fadad background_downloader: 50e91d979067b82081aba359d7d916b3ba5fadad
bonsoir_darwin: 29c7ccf356646118844721f36e1de4b61f6cbd0e bonsoir_darwin: 29c7ccf356646118844721f36e1de4b61f6cbd0e
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
cupertino_http: 94ac07f5ff090b8effa6c5e2c47871d48ab7c86c
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
@ -270,6 +282,7 @@ SPEC CHECKSUMS:
maplibre_gl: 3c924e44725147b03dda33430ad216005b40555f maplibre_gl: 3c924e44725147b03dda33430ad216005b40555f
native_video_player: b65c58951ede2f93d103a25366bdebca95081265 native_video_player: b65c58951ede2f93d103a25366bdebca95081265
network_info_plus: cf61925ab5205dce05a4f0895989afdb6aade5fc network_info_plus: cf61925ab5205dce05a4f0895989afdb6aade5fc
objective_c: 89e720c30d716b036faf9c9684022048eee1eee2
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d

View file

@ -1,15 +1,16 @@
import 'dart:async'; import 'dart:async';
import 'dart:ffi'; import 'dart:ffi';
import 'dart:io';
import 'dart:ui' as ui; import 'dart:ui' as ui;
import 'package:cronet_http/cronet_http.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:ffi/ffi.dart'; import 'package:ffi/ffi.dart';
import 'package:http/http.dart' as http;
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/providers/image/cache/remote_image_cache_manager.dart'; import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
import 'package:logging/logging.dart'; import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
part 'local_image_request.dart'; part 'local_image_request.dart';
part 'thumbhash_image_request.dart'; part 'thumbhash_image_request.dart';

View file

@ -1,14 +1,18 @@
part of 'image_request.dart'; part of 'image_request.dart';
class RemoteImageRequest extends ImageRequest { class RemoteImageRequest extends ImageRequest {
static final log = Logger('RemoteImageRequest'); static final _client = const NetworkRepository().getHttpClient(
static final client = HttpClient()..maxConnectionsPerHost = 16; directoryName: 'thumbnails',
final RemoteCacheManager? cacheManager; diskCapacity: kThumbnailDiskCacheSize,
memoryCapacity: 0,
maxConnections: 16,
cacheMode: CacheMode.disk,
);
final String uri; final String uri;
final Map<String, String> headers; final Map<String, String> headers;
HttpClientRequest? _request; final abortTrigger = Completer<void>();
RemoteImageRequest({required this.uri, required this.headers, this.cacheManager}); RemoteImageRequest({required this.uri, required this.headers});
@override @override
Future<ImageInfo?> load(ImageDecoderCallback decode, {double scale = 1.0}) async { Future<ImageInfo?> load(ImageDecoderCallback decode, {double scale = 1.0}) async {
@ -16,15 +20,8 @@ class RemoteImageRequest extends ImageRequest {
return null; return null;
} }
// TODO: the cache manager makes everything sequential with its DB calls and its operations cannot be cancelled,
// so it ends up being a bottleneck. We only prefer fetching from it when it can skip the DB call.
final cachedFileImage = await _loadCachedFile(uri, decode, scale, inMemoryOnly: true);
if (cachedFileImage != null) {
return cachedFileImage;
}
try { try {
final buffer = await _downloadImage(uri); final buffer = await _downloadImage();
if (buffer == null) { if (buffer == null) {
return null; return null;
} }
@ -35,57 +32,37 @@ class RemoteImageRequest extends ImageRequest {
return null; return null;
} }
final cachedFileImage = await _loadCachedFile(uri, decode, scale, inMemoryOnly: false);
if (cachedFileImage != null) {
return cachedFileImage;
}
rethrow; rethrow;
} finally {
_request = null;
} }
} }
Future<ImmutableBuffer?> _downloadImage(String url) async { Future<ImmutableBuffer?> _downloadImage() async {
if (_isCancelled) { if (_isCancelled) {
return null; return null;
} }
final request = _request = await client.getUrl(Uri.parse(url)); final req = http.AbortableRequest('get', Uri.parse(uri), abortTrigger: abortTrigger.future);
if (_isCancelled) { req.headers.addAll(headers);
request.abort(); final res = await _client.send(req);
return _request = null;
}
for (final entry in headers.entries) {
request.headers.set(entry.key, entry.value);
}
final response = await request.close();
if (_isCancelled) { if (_isCancelled) {
_onCancelled();
return null; return null;
} }
final cacheManager = this.cacheManager; final stream = res.stream.map((chunk) {
final streamController = StreamController<List<int>>(sync: true);
final Stream<List<int>> stream;
cacheManager?.putStreamedFile(url, streamController.stream);
stream = response.map((chunk) {
if (_isCancelled) { if (_isCancelled) {
throw StateError('Cancelled request'); throw StateError('Cancelled request');
} }
if (cacheManager != null) {
streamController.add(chunk);
}
return chunk; return chunk;
}); });
try { try {
final Uint8List bytes = await _downloadBytes(stream, response.contentLength); final Uint8List bytes = await _downloadBytes(stream, res.contentLength ?? -1);
streamController.close(); if (_isCancelled) {
return null;
}
return await ImmutableBuffer.fromUint8List(bytes); return await ImmutableBuffer.fromUint8List(bytes);
} catch (e) { } catch (e) {
streamController.addError(e);
streamController.close();
if (_isCancelled) { if (_isCancelled) {
return null; return null;
} }
@ -122,40 +99,6 @@ class RemoteImageRequest extends ImageRequest {
return bytes; return bytes;
} }
Future<ImageInfo?> _loadCachedFile(
String url,
ImageDecoderCallback decode,
double scale, {
required bool inMemoryOnly,
}) async {
final cacheManager = this.cacheManager;
if (_isCancelled || cacheManager == null) {
return null;
}
final file = await (inMemoryOnly ? cacheManager.getFileFromMemory(url) : cacheManager.getFileFromCache(url));
if (_isCancelled || file == null) {
return null;
}
try {
final buffer = await ImmutableBuffer.fromFilePath(file.file.path);
return await _decodeBuffer(buffer, decode, scale);
} catch (e) {
log.severe('Failed to decode cached image', e);
_evictFile(url);
return null;
}
}
Future<void> _evictFile(String url) async {
try {
await cacheManager?.removeFile(url);
} catch (e) {
log.severe('Failed to remove cached image', e);
}
}
Future<ImageInfo?> _decodeBuffer(ImmutableBuffer buffer, ImageDecoderCallback decode, scale) async { Future<ImageInfo?> _decodeBuffer(ImmutableBuffer buffer, ImageDecoderCallback decode, scale) async {
if (_isCancelled) { if (_isCancelled) {
buffer.dispose(); buffer.dispose();
@ -173,7 +116,6 @@ class RemoteImageRequest extends ImageRequest {
@override @override
void _onCancelled() { void _onCancelled() {
_request?.abort(); abortTrigger.complete();
_request = null;
} }
} }

View file

@ -0,0 +1,40 @@
import 'dart:io';
import 'package:cronet_http/cronet_http.dart';
import 'package:cupertino_http/cupertino_http.dart';
import 'package:http/http.dart' as http;
import 'package:path_provider/path_provider.dart';
class NetworkRepository {
static late Directory _cachePath;
static Future<void> init() async {
_cachePath = await getTemporaryDirectory();
}
const NetworkRepository();
http.Client getHttpClient({
required String directoryName,
required int diskCapacity,
required int memoryCapacity,
required int maxConnections,
required CacheMode cacheMode,
}) {
final directory = Directory('${_cachePath.path}/$directoryName');
directory.createSync(recursive: true);
if (Platform.isAndroid) {
final engine = CronetEngine.build(cacheMode: cacheMode, cacheMaxSize: diskCapacity, storagePath: directory.path);
return CronetClient.fromCronetEngine(engine, closeEngine: true);
}
final config = URLSessionConfiguration.defaultSessionConfiguration()
..httpMaximumConnectionsPerHost = maxConnections
..cache = URLCache.withCapacity(
diskCapacity: diskCapacity,
memoryCapacity: memoryCapacity,
directory: directory.uri,
);
return CupertinoClient.fromSessionConfiguration(config);
}
}

View file

@ -15,6 +15,7 @@ import 'package:immich_mobile/constants/locales.dart';
import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/generated/codegen_loader.g.dart'; import 'package:immich_mobile/generated/codegen_loader.g.dart';
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
import 'package:immich_mobile/providers/app_life_cycle.provider.dart'; import 'package:immich_mobile/providers/app_life_cycle.provider.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart';
@ -167,6 +168,7 @@ class ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserve
} }
SystemChrome.setSystemUIOverlayStyle(overlayStyle); SystemChrome.setSystemUIOverlayStyle(overlayStyle);
await ref.read(localNotificationService).setup(); await ref.read(localNotificationService).setup();
await NetworkRepository.init();
} }
Future<DeepLink> _deepLinkBuilder(PlatformDeepLink deepLink) async { Future<DeepLink> _deepLinkBuilder(PlatformDeepLink deepLink) async {

View file

@ -62,6 +62,11 @@ mixin CancellableImageProviderMixin<T extends Object> on CancellableImageProvide
return; return;
} }
yield image; yield image;
} catch (e) {
evict();
if (!isCancelled) {
_log.severe('Error loading image', e);
}
} finally { } finally {
this.request = null; this.request = null;
} }

View file

@ -7,13 +7,11 @@ import 'package:immich_mobile/domain/services/setting.service.dart';
import 'package:immich_mobile/infrastructure/loaders/image_request.dart'; import 'package:immich_mobile/infrastructure/loaders/image_request.dart';
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
import 'package:immich_mobile/presentation/widgets/images/one_frame_multi_image_stream_completer.dart'; import 'package:immich_mobile/presentation/widgets/images/one_frame_multi_image_stream_completer.dart';
import 'package:immich_mobile/providers/image/cache/remote_image_cache_manager.dart';
import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/utils/image_url_builder.dart'; import 'package:immich_mobile/utils/image_url_builder.dart';
class RemoteThumbProvider extends CancellableImageProvider<RemoteThumbProvider> class RemoteThumbProvider extends CancellableImageProvider<RemoteThumbProvider>
with CancellableImageProviderMixin<RemoteThumbProvider> { with CancellableImageProviderMixin<RemoteThumbProvider> {
static final cacheManager = RemoteThumbnailCacheManager();
final String assetId; final String assetId;
RemoteThumbProvider({required this.assetId}); RemoteThumbProvider({required this.assetId});
@ -39,7 +37,6 @@ class RemoteThumbProvider extends CancellableImageProvider<RemoteThumbProvider>
final request = this.request = RemoteImageRequest( final request = this.request = RemoteImageRequest(
uri: getThumbnailUrlForRemoteId(key.assetId), uri: getThumbnailUrlForRemoteId(key.assetId),
headers: ApiService.getRequestHeaders(), headers: ApiService.getRequestHeaders(),
cacheManager: cacheManager,
); );
return loadRequest(request, decode); return loadRequest(request, decode);
} }
@ -60,7 +57,6 @@ class RemoteThumbProvider extends CancellableImageProvider<RemoteThumbProvider>
class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImageProvider> class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImageProvider>
with CancellableImageProviderMixin<RemoteFullImageProvider> { with CancellableImageProviderMixin<RemoteFullImageProvider> {
static final cacheManager = RemoteThumbnailCacheManager();
final String assetId; final String assetId;
RemoteFullImageProvider({required this.assetId}); RemoteFullImageProvider({required this.assetId});
@ -92,11 +88,7 @@ class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImagePr
} }
final headers = ApiService.getRequestHeaders(); final headers = ApiService.getRequestHeaders();
final request = this.request = RemoteImageRequest( final request = this.request = RemoteImageRequest(uri: getPreviewUrlForRemoteId(key.assetId), headers: headers);
uri: getPreviewUrlForRemoteId(key.assetId),
headers: headers,
cacheManager: cacheManager,
);
yield* loadRequest(request, decode); yield* loadRequest(request, decode);
if (isCancelled) { if (isCancelled) {

View file

@ -2,9 +2,11 @@ import 'dart:ui';
const double kTimelineHeaderExtent = 80.0; const double kTimelineHeaderExtent = 80.0;
const Size kTimelineFixedTileExtent = Size.square(256); const Size kTimelineFixedTileExtent = Size.square(256);
const Size kThumbnailResolution = Size.square(320); // TODO: make the resolution vary based on actual tile size
const double kTimelineSpacing = 2.0; const double kTimelineSpacing = 2.0;
const int kTimelineColumnCount = 3; const int kTimelineColumnCount = 3;
const Duration kTimelineScrubberFadeInDuration = Duration(milliseconds: 300); const Duration kTimelineScrubberFadeInDuration = Duration(milliseconds: 300);
const Duration kTimelineScrubberFadeOutDuration = Duration(milliseconds: 800); const Duration kTimelineScrubberFadeOutDuration = Duration(milliseconds: 800);
const Size kThumbnailResolution = Size.square(320); // TODO: make the resolution vary based on actual tile size
const kThumbnailDiskCacheSize = 1024 << 20; // 1GiB

View file

@ -1,148 +1,25 @@
import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart';
// ignore: implementation_imports
import 'package:flutter_cache_manager/src/cache_store.dart';
import 'package:logging/logging.dart';
import 'package:uuid/uuid.dart';
abstract class RemoteCacheManager extends CacheManager { class RemoteImageCacheManager extends CacheManager {
static final _log = Logger('RemoteCacheManager');
RemoteCacheManager.custom(super.config, CacheStore store)
// Unfortunately, CacheStore is not a public API
// ignore: invalid_use_of_visible_for_testing_member
: super.custom(cacheStore: store);
Future<void> putStreamedFile(
String url,
Stream<List<int>> source, {
String? key,
String? eTag,
Duration maxAge = const Duration(days: 30),
String fileExtension = 'file',
});
// Unlike `putFileStream`, this method handles request cancellation,
// does not make a (slow) DB call checking if the file is already cached,
// does not synchronously check if a file exists,
// and deletes the file on cancellation without making these checks again.
Future<void> putStreamedFileToStore(
CacheStore store,
String url,
Stream<List<int>> source, {
String? key,
String? eTag,
Duration maxAge = const Duration(days: 30),
String fileExtension = 'file',
}) async {
final path = '${const Uuid().v1()}.$fileExtension';
final file = await store.fileSystem.createFile(path);
final sink = file.openWrite();
try {
await source.listen(sink.add, cancelOnError: true).asFuture();
} catch (e) {
try {
await sink.close();
await file.delete();
} catch (e) {
_log.severe('Failed to delete incomplete cache file: $e');
}
return;
}
try {
await sink.flush();
await sink.close();
} catch (e) {
try {
await file.delete();
} catch (e) {
_log.severe('Failed to delete incomplete cache file: $e');
}
return;
}
final cacheObject = CacheObject(
url,
key: key,
relativePath: path,
validTill: DateTime.now().add(maxAge),
eTag: eTag,
);
try {
await store.putFile(cacheObject);
} catch (e) {
try {
await file.delete();
} catch (e) {
_log.severe('Failed to delete untracked cache file: $e');
}
}
}
}
class RemoteImageCacheManager extends RemoteCacheManager {
static const key = 'remoteImageCacheKey'; static const key = 'remoteImageCacheKey';
static final RemoteImageCacheManager _instance = RemoteImageCacheManager._(); static final RemoteImageCacheManager _instance = RemoteImageCacheManager._();
static final _config = Config(key, maxNrOfCacheObjects: 500, stalePeriod: const Duration(days: 30)); static final _config = Config(key, maxNrOfCacheObjects: 500, stalePeriod: const Duration(days: 30));
static final _store = CacheStore(_config);
factory RemoteImageCacheManager() { factory RemoteImageCacheManager() {
return _instance; return _instance;
} }
RemoteImageCacheManager._() : super.custom(_config, _store); RemoteImageCacheManager._() : super(_config);
@override
Future<void> putStreamedFile(
String url,
Stream<List<int>> source, {
String? key,
String? eTag,
Duration maxAge = const Duration(days: 30),
String fileExtension = 'file',
}) {
return putStreamedFileToStore(
_store,
url,
source,
key: key,
eTag: eTag,
maxAge: maxAge,
fileExtension: fileExtension,
);
}
} }
/// The cache manager for full size images [ImmichRemoteImageProvider] class RemoteThumbnailCacheManager extends CacheManager {
class RemoteThumbnailCacheManager extends RemoteCacheManager {
static const key = 'remoteThumbnailCacheKey'; static const key = 'remoteThumbnailCacheKey';
static final RemoteThumbnailCacheManager _instance = RemoteThumbnailCacheManager._(); static final RemoteThumbnailCacheManager _instance = RemoteThumbnailCacheManager._();
static final _config = Config(key, maxNrOfCacheObjects: 5000, stalePeriod: const Duration(days: 30)); static final _config = Config(key, maxNrOfCacheObjects: 5000, stalePeriod: const Duration(days: 30));
static final _store = CacheStore(_config);
factory RemoteThumbnailCacheManager() { factory RemoteThumbnailCacheManager() {
return _instance; return _instance;
} }
RemoteThumbnailCacheManager._() : super.custom(_config, _store); RemoteThumbnailCacheManager._() : super(_config);
@override
Future<void> putStreamedFile(
String url,
Stream<List<int>> source, {
String? key,
String? eTag,
Duration maxAge = const Duration(days: 30),
String fileExtension = 'file',
}) {
return putStreamedFileToStore(
_store,
url,
source,
key: key,
eTag: eTag,
maxAge: maxAge,
fileExtension: fileExtension,
);
}
} }

View file

@ -337,6 +337,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.2" version: "3.1.2"
cronet_http:
dependency: "direct main"
description:
name: cronet_http
sha256: "1b99ad5ae81aa9d2f12900e5f17d3681f3828629bb7f7fe7ad88076a34209840"
url: "https://pub.dev"
source: hosted
version: "1.5.0"
crop_image: crop_image:
dependency: "direct main" dependency: "direct main"
description: description:
@ -369,6 +377,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.2" version: "1.0.2"
cupertino_http:
dependency: "direct main"
description:
name: cupertino_http
sha256: "72187f715837290a63479a5b0ae709f4fedad0ed6bd0441c275eceaa02d5abae"
url: "https://pub.dev"
source: hosted
version: "2.3.0"
custom_lint: custom_lint:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -899,10 +915,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: http name: http
sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.0" version: "1.5.0"
http_multi_server: http_multi_server:
dependency: transitive dependency: transitive
description: description:
@ -919,6 +935,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.1.2" version: "4.1.2"
http_profile:
dependency: transitive
description:
name: http_profile
sha256: "7e679e355b09aaee2ab5010915c932cce3f2d1c11c3b2dc177891687014ffa78"
url: "https://pub.dev"
source: hosted
version: "0.1.0"
image: image:
dependency: transitive dependency: transitive
description: description:
@ -1044,6 +1068,14 @@ packages:
url: "https://github.com/immich-app/isar" url: "https://github.com/immich-app/isar"
source: git source: git
version: "3.1.8" version: "3.1.8"
jni:
dependency: transitive
description:
name: jni
sha256: d2c361082d554d4593c3012e26f6b188f902acd291330f13d6427641a92b3da1
url: "https://pub.dev"
source: hosted
version: "0.14.2"
js: js:
dependency: transitive dependency: transitive
description: description:
@ -1237,6 +1269,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.5.0" version: "0.5.0"
objective_c:
dependency: transitive
description:
name: objective_c
sha256: "9f034ba1eeca53ddb339bc8f4813cb07336a849cd735559b60cdc068ecce2dc7"
url: "https://pub.dev"
source: hosted
version: "7.1.0"
octo_image: octo_image:
dependency: "direct main" dependency: "direct main"
description: description:

View file

@ -90,6 +90,8 @@ dependencies:
# DB # DB
drift: ^2.23.1 drift: ^2.23.1
drift_flutter: ^0.2.4 drift_flutter: ^0.2.4
cronet_http: ^1.5.0
cupertino_http: ^2.3.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View file

@ -34,7 +34,8 @@ type SendFile = Parameters<Response['sendFile']>;
type SendFileOptions = SendFile[1]; type SendFileOptions = SendFile[1];
const cacheControlHeaders: Record<CacheControl, string | null> = { const cacheControlHeaders: Record<CacheControl, string | null> = {
[CacheControl.PrivateWithCache]: 'private, max-age=86400, no-transform', [CacheControl.PrivateWithCache]:
'private, max-age=86400, no-transform, stale-while-revalidate=2592000, stale-if-error=2592000',
[CacheControl.PrivateWithoutCache]: 'private, no-cache, no-transform', [CacheControl.PrivateWithoutCache]: 'private, no-cache, no-transform',
[CacheControl.None]: null, // falsy value to prevent adding Cache-Control header [CacheControl.None]: null, // falsy value to prevent adding Cache-Control header
}; };