mirror of
https://github.com/immich-app/immich.git
synced 2025-12-15 01:00:44 -08:00
Compare commits
15 Commits
tmp/lcms
...
feat/mobil
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b84216180c | ||
|
|
e1c6813ee0 | ||
|
|
5054719f43 | ||
|
|
3423cf90bc | ||
|
|
f406ba1e6c | ||
|
|
df186cc326 | ||
|
|
11ebbe51a1 | ||
|
|
52fbf6fbc7 | ||
|
|
cf7a3a91c2 | ||
|
|
dc73a860cc | ||
|
|
88b6da5e0a | ||
|
|
740c50122e | ||
|
|
9836392fbe | ||
|
|
b1f3051608 | ||
|
|
87e1539912 |
@@ -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
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import 'dart:io';
|
|||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:background_downloader/background_downloader.dart';
|
import 'package:background_downloader/background_downloader.dart';
|
||||||
import 'package:cancellation_token_http/http.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/constants.dart';
|
import 'package:immich_mobile/constants/constants.dart';
|
||||||
@@ -63,7 +62,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
|||||||
final Drift _drift;
|
final Drift _drift;
|
||||||
final DriftLogger _driftLogger;
|
final DriftLogger _driftLogger;
|
||||||
final BackgroundWorkerBgHostApi _backgroundHostApi;
|
final BackgroundWorkerBgHostApi _backgroundHostApi;
|
||||||
final CancellationToken _cancellationToken = CancellationToken();
|
final Completer _cancellationToken = Completer();
|
||||||
final Logger _logger = Logger('BackgroundWorkerBgService');
|
final Logger _logger = Logger('BackgroundWorkerBgService');
|
||||||
|
|
||||||
bool _isCleanedUp = false;
|
bool _isCleanedUp = false;
|
||||||
@@ -188,7 +187,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
|||||||
_isCleanedUp = true;
|
_isCleanedUp = true;
|
||||||
_ref.dispose();
|
_ref.dispose();
|
||||||
|
|
||||||
_cancellationToken.cancel();
|
_cancellationToken.complete();
|
||||||
_logger.info("Cleaning up background worker");
|
_logger.info("Cleaning up background worker");
|
||||||
final cleanupFutures = [
|
final cleanupFutures = [
|
||||||
workerManager.dispose().catchError((_) async {
|
workerManager.dispose().catchError((_) async {
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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;
|
'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,41 @@ 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;
|
if (res.statusCode != 200) {
|
||||||
final streamController = StreamController<List<int>>(sync: true);
|
throw Exception('Failed to download $uri: ${res.statusCode}');
|
||||||
final Stream<List<int>> stream;
|
}
|
||||||
cacheManager?.putStreamedFile(url, streamController.stream);
|
|
||||||
stream = response.map((chunk) {
|
final stream = res.stream.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 +103,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 +120,6 @@ class RemoteImageRequest extends ImageRequest {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void _onCancelled() {
|
void _onCancelled() {
|
||||||
_request?.abort();
|
abortTrigger.complete();
|
||||||
_request = null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
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:immich_mobile/utils/user_agent.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|
||||||
|
class NetworkRepository {
|
||||||
|
static late Directory _cachePath;
|
||||||
|
static late String _userAgent;
|
||||||
|
static final _clients = <String, http.Client>{};
|
||||||
|
|
||||||
|
static Future<void> init() {
|
||||||
|
return (
|
||||||
|
getTemporaryDirectory().then((cachePath) => _cachePath = cachePath),
|
||||||
|
getUserAgentString().then((userAgent) => _userAgent = userAgent),
|
||||||
|
).wait;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void reset() {
|
||||||
|
Future.microtask(init);
|
||||||
|
for (final client in _clients.values) {
|
||||||
|
client.close();
|
||||||
|
}
|
||||||
|
_clients.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
const NetworkRepository();
|
||||||
|
|
||||||
|
/// Note: when disk caching is enabled, only one client may use a given directory at a time.
|
||||||
|
/// Different isolates or engines must use different directories.
|
||||||
|
http.Client getHttpClient(
|
||||||
|
String directoryName, {
|
||||||
|
CacheMode cacheMode = CacheMode.memory,
|
||||||
|
int diskCapacity = 0,
|
||||||
|
int maxConnections = 6,
|
||||||
|
int memoryCapacity = 10 << 20,
|
||||||
|
}) {
|
||||||
|
final cachedClient = _clients[directoryName];
|
||||||
|
if (cachedClient != null) {
|
||||||
|
return cachedClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
final directory = Directory('${_cachePath.path}/$directoryName');
|
||||||
|
directory.createSync(recursive: true);
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
final engine = CronetEngine.build(
|
||||||
|
cacheMode: cacheMode,
|
||||||
|
cacheMaxSize: diskCapacity,
|
||||||
|
storagePath: directory.path,
|
||||||
|
userAgent: _userAgent,
|
||||||
|
);
|
||||||
|
return _clients[directoryName] = CronetClient.fromCronetEngine(engine, closeEngine: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
final config = URLSessionConfiguration.defaultSessionConfiguration()
|
||||||
|
..httpMaximumConnectionsPerHost = maxConnections
|
||||||
|
..cache = URLCache.withCapacity(
|
||||||
|
diskCapacity: diskCapacity,
|
||||||
|
memoryCapacity: memoryCapacity,
|
||||||
|
directory: directory.uri,
|
||||||
|
)
|
||||||
|
..httpAdditionalHeaders = {'User-Agent': _userAgent};
|
||||||
|
return _clients[directoryName] = CupertinoClient.fromSessionConfiguration(config);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,11 +6,13 @@ import 'package:immich_mobile/constants/constants.dart';
|
|||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/sync_event.model.dart';
|
import 'package:immich_mobile/domain/models/sync_event.model.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
|
||||||
import 'package:immich_mobile/services/api.service.dart';
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class SyncApiRepository {
|
class SyncApiRepository {
|
||||||
|
static final _client = const NetworkRepository().getHttpClient('api');
|
||||||
final Logger _logger = Logger('SyncApiRepository');
|
final Logger _logger = Logger('SyncApiRepository');
|
||||||
final ApiService _api;
|
final ApiService _api;
|
||||||
SyncApiRepository(this._api);
|
SyncApiRepository(this._api);
|
||||||
@@ -26,7 +28,7 @@ class SyncApiRepository {
|
|||||||
http.Client? httpClient,
|
http.Client? httpClient,
|
||||||
}) async {
|
}) async {
|
||||||
final stopwatch = Stopwatch()..start();
|
final stopwatch = Stopwatch()..start();
|
||||||
final client = httpClient ?? http.Client();
|
final client = httpClient ?? _client;
|
||||||
final endpoint = "${_api.apiClient.basePath}/sync/stream";
|
final endpoint = "${_api.apiClient.basePath}/sync/stream";
|
||||||
|
|
||||||
final headers = {'Content-Type': 'application/json', 'Accept': 'application/jsonlines+json'};
|
final headers = {'Content-Type': 'application/json', 'Accept': 'application/jsonlines+json'};
|
||||||
@@ -112,8 +114,6 @@ class SyncApiRepository {
|
|||||||
} catch (error, stack) {
|
} catch (error, stack) {
|
||||||
_logger.severe("Error processing stream", error, stack);
|
_logger.severe("Error processing stream", error, stack);
|
||||||
return Future.error(error, stack);
|
return Future.error(error, stack);
|
||||||
} finally {
|
|
||||||
client.close();
|
|
||||||
}
|
}
|
||||||
stopwatch.stop();
|
stopwatch.stop();
|
||||||
_logger.info("Remote Sync completed in ${stopwatch.elapsed.inMilliseconds}ms");
|
_logger.info("Remote Sync completed in ${stopwatch.elapsed.inMilliseconds}ms");
|
||||||
|
|||||||
@@ -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/asset_viewer/share_intent_upload.provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart';
|
||||||
import 'package:immich_mobile/providers/db.provider.dart';
|
import 'package:immich_mobile/providers/db.provider.dart';
|
||||||
@@ -222,6 +223,14 @@ class ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserve
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void reassemble() {
|
||||||
|
if (kDebugMode) {
|
||||||
|
NetworkRepository.reset();
|
||||||
|
}
|
||||||
|
super.reassemble();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final router = ref.watch(appRouterProvider);
|
final router = ref.watch(appRouterProvider);
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:background_downloader/background_downloader.dart';
|
import 'package:background_downloader/background_downloader.dart';
|
||||||
import 'package:cancellation_token_http/http.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:http/http.dart';
|
||||||
import 'package:immich_mobile/constants/constants.dart';
|
import 'package:immich_mobile/constants/constants.dart';
|
||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:immich_mobile/utils/debug_print.dart';
|
import 'package:immich_mobile/utils/debug_print.dart';
|
||||||
|
|
||||||
@@ -20,6 +22,8 @@ class UploadTaskWithFile {
|
|||||||
final uploadRepositoryProvider = Provider((ref) => UploadRepository());
|
final uploadRepositoryProvider = Provider((ref) => UploadRepository());
|
||||||
|
|
||||||
class UploadRepository {
|
class UploadRepository {
|
||||||
|
static final _client = const NetworkRepository().getHttpClient('upload');
|
||||||
|
|
||||||
void Function(TaskStatusUpdate)? onUploadStatus;
|
void Function(TaskStatusUpdate)? onUploadStatus;
|
||||||
void Function(TaskProgressUpdate)? onTaskProgress;
|
void Function(TaskProgressUpdate)? onTaskProgress;
|
||||||
|
|
||||||
@@ -92,13 +96,12 @@ class UploadRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> backupWithDartClient(Iterable<UploadTaskWithFile> tasks, CancellationToken cancelToken) async {
|
Future<void> backupWithDartClient(Iterable<UploadTaskWithFile> tasks, Completer cancelToken) async {
|
||||||
final httpClient = Client();
|
|
||||||
final String savedEndpoint = Store.get(StoreKey.serverEndpoint);
|
final String savedEndpoint = Store.get(StoreKey.serverEndpoint);
|
||||||
|
|
||||||
Logger logger = Logger('UploadRepository');
|
Logger logger = Logger('UploadRepository');
|
||||||
for (final candidate in tasks) {
|
for (final candidate in tasks) {
|
||||||
if (cancelToken.isCancelled) {
|
if (cancelToken.isCompleted) {
|
||||||
logger.warning("Backup was cancelled by the user");
|
logger.warning("Backup was cancelled by the user");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -112,13 +115,17 @@ class UploadRepository {
|
|||||||
filename: candidate.task.filename,
|
filename: candidate.task.filename,
|
||||||
);
|
);
|
||||||
|
|
||||||
final baseRequest = MultipartRequest('POST', Uri.parse('$savedEndpoint/assets'));
|
final baseRequest = AbortableMultipartRequest(
|
||||||
|
'POST',
|
||||||
|
Uri.parse('$savedEndpoint/assets'),
|
||||||
|
abortTrigger: cancelToken.future,
|
||||||
|
)..headers['Accept'] = 'application/json';
|
||||||
|
|
||||||
baseRequest.headers.addAll(candidate.task.headers);
|
baseRequest.headers.addAll(candidate.task.headers);
|
||||||
baseRequest.fields.addAll(candidate.task.fields);
|
baseRequest.fields.addAll(candidate.task.fields);
|
||||||
baseRequest.files.add(assetRawUploadData);
|
baseRequest.files.add(assetRawUploadData);
|
||||||
|
|
||||||
final response = await httpClient.send(baseRequest, cancellationToken: cancelToken);
|
final response = await _client.send(baseRequest);
|
||||||
|
|
||||||
final responseBody = jsonDecode(await response.stream.bytesToString());
|
final responseBody = jsonDecode(await response.stream.bytesToString());
|
||||||
|
|
||||||
@@ -131,7 +138,7 @@ class UploadRepository {
|
|||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
} on CancelledException {
|
} on RequestAbortedException {
|
||||||
logger.warning("Backup was cancelled by the user");
|
logger.warning("Backup was cancelled by the user");
|
||||||
break;
|
break;
|
||||||
} catch (error, stackTrace) {
|
} catch (error, stackTrace) {
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import 'dart:convert';
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:device_info_plus/device_info_plus.dart';
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
import 'package:http/http.dart';
|
|
||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
|
||||||
import 'package:immich_mobile/utils/url_helper.dart';
|
import 'package:immich_mobile/utils/url_helper.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
@@ -13,6 +13,7 @@ import 'package:immich_mobile/utils/user_agent.dart';
|
|||||||
import 'package:immich_mobile/utils/debug_print.dart';
|
import 'package:immich_mobile/utils/debug_print.dart';
|
||||||
|
|
||||||
class ApiService implements Authentication {
|
class ApiService implements Authentication {
|
||||||
|
static final _client = const NetworkRepository().getHttpClient('api');
|
||||||
late ApiClient _apiClient;
|
late ApiClient _apiClient;
|
||||||
|
|
||||||
late UsersApi usersApi;
|
late UsersApi usersApi;
|
||||||
@@ -50,6 +51,7 @@ class ApiService implements Authentication {
|
|||||||
|
|
||||||
setEndpoint(String endpoint) {
|
setEndpoint(String endpoint) {
|
||||||
_apiClient = ApiClient(basePath: endpoint, authentication: this);
|
_apiClient = ApiClient(basePath: endpoint, authentication: this);
|
||||||
|
_apiClient.client = _client;
|
||||||
_setUserAgentHeader();
|
_setUserAgentHeader();
|
||||||
if (_accessToken != null) {
|
if (_accessToken != null) {
|
||||||
setAccessToken(_accessToken!);
|
setAccessToken(_accessToken!);
|
||||||
@@ -134,13 +136,11 @@ class ApiService implements Authentication {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<String> _getWellKnownEndpoint(String baseUrl) async {
|
Future<String> _getWellKnownEndpoint(String baseUrl) async {
|
||||||
final Client client = Client();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var headers = {"Accept": "application/json"};
|
var headers = {"Accept": "application/json"};
|
||||||
headers.addAll(getRequestHeaders());
|
headers.addAll(getRequestHeaders());
|
||||||
|
|
||||||
final res = await client
|
final res = await _client
|
||||||
.get(Uri.parse("$baseUrl/.well-known/immich"), headers: headers)
|
.get(Uri.parse("$baseUrl/.well-known/immich"), headers: headers)
|
||||||
.timeout(const Duration(seconds: 5));
|
.timeout(const Duration(seconds: 5));
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import 'dart:convert';
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:background_downloader/background_downloader.dart';
|
import 'package:background_downloader/background_downloader.dart';
|
||||||
import 'package:cancellation_token_http/http.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/constants.dart';
|
import 'package:immich_mobile/constants/constants.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
@@ -158,7 +157,7 @@ class UploadService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> startBackupWithHttpClient(String userId, bool hasWifi, CancellationToken token) async {
|
Future<void> startBackupWithHttpClient(String userId, bool hasWifi, Completer token) async {
|
||||||
await _storageRepository.clearCache();
|
await _storageRepository.clearCache();
|
||||||
|
|
||||||
shouldAbortQueuingTasks = false;
|
shouldAbortQueuingTasks = false;
|
||||||
@@ -170,7 +169,7 @@ class UploadService {
|
|||||||
|
|
||||||
const batchSize = 100;
|
const batchSize = 100;
|
||||||
for (int i = 0; i < candidates.length; i += batchSize) {
|
for (int i = 0; i < candidates.length; i += batchSize) {
|
||||||
if (shouldAbortQueuingTasks || token.isCancelled) {
|
if (shouldAbortQueuingTasks || token.isCompleted) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
|||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
@@ -106,5 +107,7 @@ abstract final class Bootstrap {
|
|||||||
storeRepository: storeRepo,
|
storeRepository: storeRepo,
|
||||||
shouldBuffer: shouldBufferLogs,
|
shouldBuffer: shouldBufferLogs,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await NetworkRepository.init();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -89,6 +89,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:
|
||||||
|
|||||||
@@ -118,7 +118,6 @@ void main() {
|
|||||||
expect(onDataCallCount, 1);
|
expect(onDataCallCount, 1);
|
||||||
expect(abortWasCalledInCallback, isTrue);
|
expect(abortWasCalledInCallback, isTrue);
|
||||||
expect(receivedEventsBatch1.length, testBatchSize);
|
expect(receivedEventsBatch1.length, testBatchSize);
|
||||||
verify(() => mockHttpClient.close()).called(1);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('streamChanges does not process remaining lines in finally block if aborted', () async {
|
test('streamChanges does not process remaining lines in finally block if aborted', () async {
|
||||||
@@ -159,7 +158,6 @@ void main() {
|
|||||||
|
|
||||||
expect(onDataCallCount, 1);
|
expect(onDataCallCount, 1);
|
||||||
expect(abortWasCalledInCallback, isTrue);
|
expect(abortWasCalledInCallback, isTrue);
|
||||||
verify(() => mockHttpClient.close()).called(1);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('streamChanges processes remaining lines in finally block if not aborted', () async {
|
test('streamChanges processes remaining lines in finally block if not aborted', () async {
|
||||||
@@ -204,7 +202,6 @@ void main() {
|
|||||||
expect(onDataCallCount, 2);
|
expect(onDataCallCount, 2);
|
||||||
expect(receivedEventsBatch1.length, testBatchSize);
|
expect(receivedEventsBatch1.length, testBatchSize);
|
||||||
expect(receivedEventsBatch2.length, 1);
|
expect(receivedEventsBatch2.length, 1);
|
||||||
verify(() => mockHttpClient.close()).called(1);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('streamChanges handles stream error gracefully', () async {
|
test('streamChanges handles stream error gracefully', () async {
|
||||||
@@ -229,7 +226,6 @@ void main() {
|
|||||||
await expectLater(streamChangesFuture, throwsA(streamError));
|
await expectLater(streamChangesFuture, throwsA(streamError));
|
||||||
|
|
||||||
expect(onDataCallCount, 0);
|
expect(onDataCallCount, 0);
|
||||||
verify(() => mockHttpClient.close()).called(1);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('streamChanges throws ApiException on non-200 status code', () async {
|
test('streamChanges throws ApiException on non-200 status code', () async {
|
||||||
@@ -257,6 +253,5 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(onDataCallCount, 0);
|
expect(onDataCallCount, 0);
|
||||||
verify(() => mockHttpClient.close()).called(1);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM ghcr.io/immich-app/base-server-dev:pr-272 AS builder
|
FROM ghcr.io/immich-app/base-server-dev:202509091104@sha256:4f9275330f1e49e7ce9840758ea91839052fe6ed40972d5bb97a9af857fa956a AS builder
|
||||||
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
|
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
|
||||||
CI=1 \
|
CI=1 \
|
||||||
COREPACK_HOME=/tmp
|
COREPACK_HOME=/tmp
|
||||||
@@ -33,7 +33,7 @@ RUN pnpm --filter @immich/sdk --filter @immich/cli --frozen-lockfile install &&
|
|||||||
pnpm --filter @immich/sdk --filter @immich/cli build && \
|
pnpm --filter @immich/sdk --filter @immich/cli build && \
|
||||||
pnpm --filter @immich/cli --prod --no-optional deploy /output/cli-pruned
|
pnpm --filter @immich/cli --prod --no-optional deploy /output/cli-pruned
|
||||||
|
|
||||||
FROM ghcr.io/immich-app/base-server-prod:pr-272
|
FROM ghcr.io/immich-app/base-server-prod:202509091104@sha256:d1ccbac24c84f2f8277cf85281edfca62d85d7daed6a62b8efd3a81bcd3c5e0e
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
ENV NODE_ENV=production \
|
ENV NODE_ENV=production \
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user