mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
Merge branch 'main' into feature/bottom-buttons-order
This commit is contained in:
@@ -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/log.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:isar/isar.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
@@ -106,5 +107,7 @@ abstract final class Bootstrap {
|
||||
storeRepository: storeRepo,
|
||||
shouldBuffer: shouldBufferLogs,
|
||||
);
|
||||
|
||||
await NetworkRepository.init();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ String formatBytes(int bytes) {
|
||||
|
||||
String formatHumanReadableBytes(int bytes, int decimals) {
|
||||
if (bytes <= 0) return "0 B";
|
||||
const suffixes = ["B", "KB", "MB", "GB", "TB"];
|
||||
const suffixes = ["B", "KiB", "MiB", "GiB", "TiB"];
|
||||
var i = (log(bytes) / log(1024)).floor();
|
||||
return '${(bytes / pow(1024, i)).toStringAsFixed(decimals)} ${suffixes[i]}';
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'package:immich_mobile/constants/constants.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';
|
||||
@@ -10,14 +9,18 @@ String getThumbnailUrl(final Asset asset, {AssetMediaSize type = AssetMediaSize.
|
||||
}
|
||||
|
||||
String getThumbnailCacheKey(final Asset asset, {AssetMediaSize type = AssetMediaSize.thumbnail}) {
|
||||
return getThumbnailCacheKeyForRemoteId(asset.remoteId!, type: type);
|
||||
return getThumbnailCacheKeyForRemoteId(asset.remoteId!, asset.thumbhash!, type: type);
|
||||
}
|
||||
|
||||
String getThumbnailCacheKeyForRemoteId(final String id, {AssetMediaSize type = AssetMediaSize.thumbnail}) {
|
||||
String getThumbnailCacheKeyForRemoteId(
|
||||
final String id,
|
||||
final String thumbhash, {
|
||||
AssetMediaSize type = AssetMediaSize.thumbnail,
|
||||
}) {
|
||||
if (type == AssetMediaSize.thumbnail) {
|
||||
return 'thumbnail-image-$id';
|
||||
return 'thumbnail-image-$id-$thumbhash';
|
||||
} else {
|
||||
return '${id}_previewStage';
|
||||
return '${id}_${thumbhash}_previewStage';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,26 +35,27 @@ String getAlbumThumbNailCacheKey(final Album album, {AssetMediaSize type = Asset
|
||||
if (album.thumbnail.value?.remoteId == null) {
|
||||
return '';
|
||||
}
|
||||
return getThumbnailCacheKeyForRemoteId(album.thumbnail.value!.remoteId!, type: type);
|
||||
return getThumbnailCacheKeyForRemoteId(
|
||||
album.thumbnail.value!.remoteId!,
|
||||
album.thumbnail.value!.thumbhash!,
|
||||
type: type,
|
||||
);
|
||||
}
|
||||
|
||||
String getOriginalUrlForRemoteId(final String id) {
|
||||
return '${Store.get(StoreKey.serverEndpoint)}/assets/$id/original';
|
||||
String getOriginalUrlForRemoteId(final String id, {bool edited = true}) {
|
||||
return '${Store.get(StoreKey.serverEndpoint)}/assets/$id/original?edited=$edited';
|
||||
}
|
||||
|
||||
String getImageCacheKey(final Asset asset) {
|
||||
// Assets from response DTOs do not have an isar id, querying which would give us the default autoIncrement id
|
||||
final isFromDto = asset.id == noDbId;
|
||||
return '${isFromDto ? asset.remoteId : asset.id}_fullStage';
|
||||
String getThumbnailUrlForRemoteId(
|
||||
final String id, {
|
||||
AssetMediaSize type = AssetMediaSize.thumbnail,
|
||||
bool edited = true,
|
||||
String? thumbhash,
|
||||
}) {
|
||||
final url = '${Store.get(StoreKey.serverEndpoint)}/assets/$id/thumbnail?size=${type.value}&edited=$edited';
|
||||
return thumbhash != null ? '$url&c=${Uri.encodeComponent(thumbhash)}' : url;
|
||||
}
|
||||
|
||||
String getThumbnailUrlForRemoteId(final String id, {AssetMediaSize type = AssetMediaSize.thumbnail}) {
|
||||
return '${Store.get(StoreKey.serverEndpoint)}/assets/$id/thumbnail?size=${type.value}';
|
||||
}
|
||||
|
||||
String getPreviewUrlForRemoteId(final String id) =>
|
||||
'${Store.get(StoreKey.serverEndpoint)}/assets/$id/thumbnail?size=${AssetMediaSize.preview}';
|
||||
|
||||
String getPlaybackUrlForRemoteId(final String id) {
|
||||
return '${Store.get(StoreKey.serverEndpoint)}/assets/$id/video/playback?';
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import 'package:immich_mobile/utils/datetime_helpers.dart';
|
||||
import 'package:immich_mobile/utils/debug_print.dart';
|
||||
import 'package:immich_mobile/utils/diff.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
// ignore: import_rule_photo_manager
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ dynamic upgradeDto(dynamic value, String targetType) {
|
||||
if (value is Map) {
|
||||
addDefault(value, 'visibility', 'timeline');
|
||||
addDefault(value, 'createdAt', DateTime.now().toIso8601String());
|
||||
addDefault(value, 'isEdited', false);
|
||||
}
|
||||
break;
|
||||
case 'UserAdminResponseDto':
|
||||
@@ -46,6 +47,10 @@ dynamic upgradeDto(dynamic value, String targetType) {
|
||||
addDefault(value, 'profileChangedAt', DateTime.now().toIso8601String());
|
||||
addDefault(value, 'hasProfileImage', false);
|
||||
}
|
||||
case 'SyncAssetV1':
|
||||
if (value is Map) {
|
||||
addDefault(value, 'isEdited', false);
|
||||
}
|
||||
case 'ServerFeaturesDto':
|
||||
if (value is Map) {
|
||||
addDefault(value, 'ocr', false);
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
/// A class to calculate upload speed based on progress updates.
|
||||
///
|
||||
/// Tracks bytes transferred over time and calculates average speed
|
||||
/// using a sliding window approach to smooth out fluctuations.
|
||||
class UploadSpeedCalculator {
|
||||
/// Creates an UploadSpeedCalculator with the given window size.
|
||||
///
|
||||
/// [windowSize] determines how many recent samples to use for
|
||||
/// calculating the average speed. Default is 5 samples.
|
||||
UploadSpeedCalculator({this.windowSize = 5});
|
||||
|
||||
/// The number of samples to keep in the sliding window.
|
||||
final int windowSize;
|
||||
|
||||
/// List of recent speed samples (bytes per second).
|
||||
final List<double> _speedSamples = [];
|
||||
|
||||
/// The timestamp of the last progress update.
|
||||
DateTime? _lastUpdateTime;
|
||||
|
||||
/// The bytes transferred at the last progress update.
|
||||
int _lastBytes = 0;
|
||||
|
||||
/// The total file size being uploaded.
|
||||
int _totalBytes = 0;
|
||||
|
||||
/// Resets the calculator for a new upload.
|
||||
void reset() {
|
||||
_speedSamples.clear();
|
||||
_lastUpdateTime = null;
|
||||
_lastBytes = 0;
|
||||
_totalBytes = 0;
|
||||
}
|
||||
|
||||
/// Updates the calculator with the current progress.
|
||||
///
|
||||
/// [currentBytes] is the number of bytes transferred so far.
|
||||
/// [totalBytes] is the total size of the file being uploaded.
|
||||
///
|
||||
/// Returns the calculated speed in MB/s, or -1 if not enough data.
|
||||
double update(int currentBytes, int totalBytes) {
|
||||
final now = DateTime.now();
|
||||
_totalBytes = totalBytes;
|
||||
|
||||
if (_lastUpdateTime == null) {
|
||||
_lastUpdateTime = now;
|
||||
_lastBytes = currentBytes;
|
||||
return -1;
|
||||
}
|
||||
|
||||
final elapsed = now.difference(_lastUpdateTime!);
|
||||
|
||||
// Only calculate if at least 100ms has passed to avoid division by very small numbers
|
||||
if (elapsed.inMilliseconds < 100) {
|
||||
return _currentSpeed;
|
||||
}
|
||||
|
||||
final bytesTransferred = currentBytes - _lastBytes;
|
||||
final elapsedSeconds = elapsed.inMilliseconds / 1000.0;
|
||||
|
||||
// Calculate bytes per second, then convert to MB/s
|
||||
final bytesPerSecond = bytesTransferred / elapsedSeconds;
|
||||
final mbPerSecond = bytesPerSecond / (1024 * 1024);
|
||||
|
||||
// Add to sliding window
|
||||
_speedSamples.add(mbPerSecond);
|
||||
if (_speedSamples.length > windowSize) {
|
||||
_speedSamples.removeAt(0);
|
||||
}
|
||||
|
||||
_lastUpdateTime = now;
|
||||
_lastBytes = currentBytes;
|
||||
|
||||
return _currentSpeed;
|
||||
}
|
||||
|
||||
/// Returns the current calculated speed in MB/s.
|
||||
///
|
||||
/// Returns -1 if no valid speed has been calculated yet.
|
||||
double get _currentSpeed {
|
||||
if (_speedSamples.isEmpty) {
|
||||
return -1;
|
||||
}
|
||||
// Calculate average of all samples in the window
|
||||
final sum = _speedSamples.fold(0.0, (prev, speed) => prev + speed);
|
||||
return sum / _speedSamples.length;
|
||||
}
|
||||
|
||||
/// Returns the current speed in MB/s, or -1 if not available.
|
||||
double get speed => _currentSpeed;
|
||||
|
||||
/// Returns a human-readable string representation of the current speed.
|
||||
///
|
||||
/// Returns '-- MB/s' if N/A, otherwise in MB/s or kB/s format.
|
||||
String get speedAsString {
|
||||
final s = _currentSpeed;
|
||||
return switch (s) {
|
||||
<= 0 => '-- MB/s',
|
||||
>= 1 => '${s.round()} MB/s',
|
||||
_ => '${(s * 1000).round()} kB/s',
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns the estimated time remaining as a Duration.
|
||||
///
|
||||
/// Returns Duration with negative seconds if not calculable.
|
||||
Duration get timeRemaining {
|
||||
final s = _currentSpeed;
|
||||
if (s <= 0 || _totalBytes <= 0 || _lastBytes >= _totalBytes) {
|
||||
return const Duration(seconds: -1);
|
||||
}
|
||||
|
||||
final remainingBytes = _totalBytes - _lastBytes;
|
||||
final bytesPerSecond = s * 1024 * 1024;
|
||||
final secondsRemaining = remainingBytes / bytesPerSecond;
|
||||
|
||||
return Duration(seconds: secondsRemaining.round());
|
||||
}
|
||||
|
||||
/// Returns a human-readable string representation of time remaining.
|
||||
///
|
||||
/// Returns '--:--' if N/A, otherwise HH:MM:SS or MM:SS format.
|
||||
String get timeRemainingAsString {
|
||||
final remaining = timeRemaining;
|
||||
return switch (remaining.inSeconds) {
|
||||
<= 0 => '--:--',
|
||||
< 3600 =>
|
||||
'${remaining.inMinutes.toString().padLeft(2, "0")}'
|
||||
':${remaining.inSeconds.remainder(60).toString().padLeft(2, "0")}',
|
||||
_ =>
|
||||
'${remaining.inHours}'
|
||||
':${remaining.inMinutes.remainder(60).toString().padLeft(2, "0")}'
|
||||
':${remaining.inSeconds.remainder(60).toString().padLeft(2, "0")}',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Manager for tracking upload speeds for multiple concurrent uploads.
|
||||
///
|
||||
/// Each upload is identified by a unique task ID.
|
||||
class UploadSpeedManager {
|
||||
/// Map of task IDs to their speed calculators.
|
||||
final Map<String, UploadSpeedCalculator> _calculators = {};
|
||||
|
||||
/// Gets or creates a speed calculator for the given task ID.
|
||||
UploadSpeedCalculator getCalculator(String taskId) {
|
||||
return _calculators.putIfAbsent(taskId, () => UploadSpeedCalculator());
|
||||
}
|
||||
|
||||
/// Updates progress for a specific task and returns the speed string.
|
||||
///
|
||||
/// [taskId] is the unique identifier for the upload task.
|
||||
/// [currentBytes] is the number of bytes transferred so far.
|
||||
/// [totalBytes] is the total size of the file being uploaded.
|
||||
///
|
||||
/// Returns the human-readable speed string.
|
||||
String updateProgress(String taskId, int currentBytes, int totalBytes) {
|
||||
final calculator = getCalculator(taskId);
|
||||
calculator.update(currentBytes, totalBytes);
|
||||
return calculator.speedAsString;
|
||||
}
|
||||
|
||||
/// Gets the current speed string for a specific task.
|
||||
String getSpeedAsString(String taskId) {
|
||||
return _calculators[taskId]?.speedAsString ?? '-- MB/s';
|
||||
}
|
||||
|
||||
/// Gets the time remaining string for a specific task.
|
||||
String getTimeRemainingAsString(String taskId) {
|
||||
return _calculators[taskId]?.timeRemainingAsString ?? '--:--';
|
||||
}
|
||||
|
||||
/// Removes a task from tracking.
|
||||
void removeTask(String taskId) {
|
||||
_calculators.remove(taskId);
|
||||
}
|
||||
|
||||
/// Clears all tracked tasks.
|
||||
void clear() {
|
||||
_calculators.clear();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user