feat(mobile): use shared native client (#25942)

* use shared client in dart

fix android

* websocket integration

platform-side headers

update comment

consistent platform check

tweak websocket handling

support streaming

* redundant logging

* fix proguard

* formatting

* handle onProgress

* support videos on ios

* inline return

* improved ios impl

* cleanup

* sync stopForegroundBackup

* voidify

* future already completed

* stream request on android

* outdated ios ws code

* use `choosePrivateKeyAlias`

* return result

* formatting

* update tests

* redundant check

* handle custom headers

* move completer outside of state

* persist auth

* dispose old socket

* use group id for cookies

* redundant headers

* cache global ref

* handle network switching

* handle basic auth

* apply custom headers immediately

* video player update

* fix

* persist url

* potential logout fix

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
Mert
2026-03-05 12:04:45 -05:00
committed by GitHub
parent 35a521c6ec
commit a05c8c6087
57 changed files with 880 additions and 855 deletions
+13 -40
View File
@@ -2,14 +2,16 @@ import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:cancellation_token_http/http.dart' as http;
import 'package:collection/collection.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:http/http.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/backup_album.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
import 'package:immich_mobile/repositories/upload.repository.dart';
import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
import 'package:immich_mobile/models/backup/current_upload_asset.model.dart';
import 'package:immich_mobile/models/backup/error_upload_asset.model.dart';
@@ -43,7 +45,6 @@ final backupServiceProvider = Provider(
);
class BackupService {
final httpClient = http.Client();
final ApiService _apiService;
final Logger _log = Logger("BackupService");
final AppSettingsService _appSetting;
@@ -233,7 +234,7 @@ class BackupService {
Future<bool> backupAsset(
Iterable<BackupCandidate> assets,
http.CancellationToken cancelToken, {
Completer<void> cancelToken, {
bool isBackground = false,
PMProgressHandler? pmProgressHandler,
required void Function(SuccessUploadAsset result) onSuccess,
@@ -306,20 +307,20 @@ class BackupService {
}
final fileStream = file.openRead();
final assetRawUploadData = http.MultipartFile(
final assetRawUploadData = MultipartFile(
"assetData",
fileStream,
file.lengthSync(),
filename: originalFileName,
);
final baseRequest = MultipartRequest(
final baseRequest = ProgressMultipartRequest(
'POST',
Uri.parse('$savedEndpoint/assets'),
abortTrigger: cancelToken.future,
onProgress: ((bytes, totalBytes) => onProgress(bytes, totalBytes)),
);
baseRequest.headers.addAll(ApiService.getRequestHeaders());
baseRequest.fields['deviceAssetId'] = asset.localId!;
baseRequest.fields['deviceId'] = deviceId;
baseRequest.fields['fileCreatedAt'] = asset.fileCreatedAt.toUtc().toIso8601String();
@@ -348,7 +349,7 @@ class BackupService {
baseRequest.fields['livePhotoVideoId'] = livePhotoVideoId;
}
final response = await httpClient.send(baseRequest, cancellationToken: cancelToken);
final response = await NetworkRepository.client.send(baseRequest);
final responseBody = jsonDecode(await response.stream.bytesToString());
@@ -398,7 +399,7 @@ class BackupService {
await _albumService.syncUploadAlbums(candidate.albumNames, [responseBody['id'] as String]);
}
}
} on http.CancelledException {
} on RequestAbortedException {
dPrint(() => "Backup was cancelled by the user");
anyErrors = true;
break;
@@ -429,26 +430,26 @@ class BackupService {
String originalFileName,
File? livePhotoVideoFile,
MultipartRequest baseRequest,
http.CancellationToken cancelToken,
Completer cancelToken,
) async {
if (livePhotoVideoFile == null) {
return null;
}
final livePhotoTitle = p.setExtension(originalFileName, p.extension(livePhotoVideoFile.path));
final fileStream = livePhotoVideoFile.openRead();
final livePhotoRawUploadData = http.MultipartFile(
final livePhotoRawUploadData = MultipartFile(
"assetData",
fileStream,
livePhotoVideoFile.lengthSync(),
filename: livePhotoTitle,
);
final livePhotoReq = MultipartRequest(baseRequest.method, baseRequest.url, onProgress: baseRequest.onProgress)
final livePhotoReq = ProgressMultipartRequest(baseRequest.method, baseRequest.url, abortTrigger: cancelToken.future)
..headers.addAll(baseRequest.headers)
..fields.addAll(baseRequest.fields);
livePhotoReq.files.add(livePhotoRawUploadData);
var response = await httpClient.send(livePhotoReq, cancellationToken: cancelToken);
var response = await NetworkRepository.client.send(livePhotoReq);
var responseBody = jsonDecode(await response.stream.bytesToString());
@@ -470,31 +471,3 @@ class BackupService {
AssetType.other => "OTHER",
};
}
class MultipartRequest extends http.MultipartRequest {
/// Creates a new [MultipartRequest].
MultipartRequest(super.method, super.url, {required this.onProgress});
final void Function(int bytes, int totalBytes) onProgress;
/// Freezes all mutable fields and returns a
/// single-subscription [http.ByteStream]
/// that will emit the request body.
@override
http.ByteStream finalize() {
final byteStream = super.finalize();
final total = contentLength;
var bytes = 0;
final t = StreamTransformer.fromHandlers(
handleData: (List<int> data, EventSink<List<int>> sink) {
bytes += data.length;
onProgress.call(bytes, total);
sink.add(data);
},
);
final stream = byteStream.transform(t);
return http.ByteStream(stream);
}
}