mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
Merge branch 'feature/kebab-menu-2' into feature/rearrange-buttons-2
This commit is contained in:
@@ -5,6 +5,10 @@ class LocalAsset extends BaseAsset {
|
||||
final String? remoteAssetId;
|
||||
final int orientation;
|
||||
|
||||
final DateTime? adjustmentTime;
|
||||
final double? latitude;
|
||||
final double? longitude;
|
||||
|
||||
const LocalAsset({
|
||||
required this.id,
|
||||
String? remoteId,
|
||||
@@ -19,6 +23,9 @@ class LocalAsset extends BaseAsset {
|
||||
super.isFavorite = false,
|
||||
super.livePhotoVideoId,
|
||||
this.orientation = 0,
|
||||
this.adjustmentTime,
|
||||
this.latitude,
|
||||
this.longitude,
|
||||
}) : remoteAssetId = remoteId;
|
||||
|
||||
@override
|
||||
@@ -33,6 +40,8 @@ class LocalAsset extends BaseAsset {
|
||||
@override
|
||||
String get heroTag => '${id}_${remoteId ?? checksum}';
|
||||
|
||||
bool get hasCoordinates => latitude != null && longitude != null && latitude != 0 && longitude != 0;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '''LocalAsset {
|
||||
@@ -47,6 +56,9 @@ class LocalAsset extends BaseAsset {
|
||||
remoteId: ${remoteId ?? "<NA>"}
|
||||
isFavorite: $isFavorite,
|
||||
orientation: $orientation,
|
||||
adjustmentTime: $adjustmentTime,
|
||||
latitude: ${latitude ?? "<NA>"},
|
||||
longitude: ${longitude ?? "<NA>"},
|
||||
}''';
|
||||
}
|
||||
|
||||
@@ -55,11 +67,23 @@ class LocalAsset extends BaseAsset {
|
||||
bool operator ==(Object other) {
|
||||
if (other is! LocalAsset) return false;
|
||||
if (identical(this, other)) return true;
|
||||
return super == other && id == other.id && orientation == other.orientation;
|
||||
return super == other &&
|
||||
id == other.id &&
|
||||
orientation == other.orientation &&
|
||||
adjustmentTime == other.adjustmentTime &&
|
||||
latitude == other.latitude &&
|
||||
longitude == other.longitude;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => super.hashCode ^ id.hashCode ^ remoteId.hashCode ^ orientation.hashCode;
|
||||
int get hashCode =>
|
||||
super.hashCode ^
|
||||
id.hashCode ^
|
||||
remoteId.hashCode ^
|
||||
orientation.hashCode ^
|
||||
adjustmentTime.hashCode ^
|
||||
latitude.hashCode ^
|
||||
longitude.hashCode;
|
||||
|
||||
LocalAsset copyWith({
|
||||
String? id,
|
||||
@@ -74,6 +98,9 @@ class LocalAsset extends BaseAsset {
|
||||
int? durationInSeconds,
|
||||
bool? isFavorite,
|
||||
int? orientation,
|
||||
DateTime? adjustmentTime,
|
||||
double? latitude,
|
||||
double? longitude,
|
||||
}) {
|
||||
return LocalAsset(
|
||||
id: id ?? this.id,
|
||||
@@ -88,6 +115,9 @@ class LocalAsset extends BaseAsset {
|
||||
durationInSeconds: durationInSeconds ?? this.durationInSeconds,
|
||||
isFavorite: isFavorite ?? this.isFavorite,
|
||||
orientation: orientation ?? this.orientation,
|
||||
adjustmentTime: adjustmentTime ?? this.adjustmentTime,
|
||||
latitude: latitude ?? this.latitude,
|
||||
longitude: longitude ?? this.longitude,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||
|
||||
// Timeline Events
|
||||
class TimelineReloadEvent extends Event {
|
||||
const TimelineReloadEvent();
|
||||
}
|
||||
|
||||
class ScrollToTopEvent extends Event {
|
||||
const ScrollToTopEvent();
|
||||
}
|
||||
|
||||
class ScrollToDateEvent extends Event {
|
||||
final DateTime date;
|
||||
|
||||
const ScrollToDateEvent(this.date);
|
||||
}
|
||||
|
||||
// Asset Viewer Events
|
||||
class ViewerOpenBottomSheetEvent extends Event {
|
||||
final bool activitiesMode;
|
||||
const ViewerOpenBottomSheetEvent({this.activitiesMode = false});
|
||||
}
|
||||
|
||||
class ViewerReloadAssetEvent extends Event {
|
||||
const ViewerReloadAssetEvent();
|
||||
}
|
||||
|
||||
// Multi-Select Events
|
||||
class MultiSelectToggleEvent extends Event {
|
||||
final bool isEnabled;
|
||||
const MultiSelectToggleEvent(this.isEnabled);
|
||||
}
|
||||
@@ -3,8 +3,6 @@ class ExifInfo {
|
||||
final int? fileSize;
|
||||
final String? description;
|
||||
final bool isFlipped;
|
||||
final double? width;
|
||||
final double? height;
|
||||
final String? orientation;
|
||||
final String? timeZone;
|
||||
final DateTime? dateTimeOriginal;
|
||||
@@ -39,15 +37,13 @@ class ExifInfo {
|
||||
|
||||
String get fNumber => f == null ? "" : f!.toStringAsFixed(1);
|
||||
|
||||
String get focalLength => mm == null ? "" : mm!.toStringAsFixed(1);
|
||||
String get focalLength => mm == null ? "" : mm!.toStringAsFixed(3);
|
||||
|
||||
const ExifInfo({
|
||||
this.assetId,
|
||||
this.fileSize,
|
||||
this.description,
|
||||
this.orientation,
|
||||
this.width,
|
||||
this.height,
|
||||
this.timeZone,
|
||||
this.dateTimeOriginal,
|
||||
this.isFlipped = false,
|
||||
@@ -72,8 +68,6 @@ class ExifInfo {
|
||||
return other.fileSize == fileSize &&
|
||||
other.description == description &&
|
||||
other.isFlipped == isFlipped &&
|
||||
other.width == width &&
|
||||
other.height == height &&
|
||||
other.orientation == orientation &&
|
||||
other.timeZone == timeZone &&
|
||||
other.dateTimeOriginal == dateTimeOriginal &&
|
||||
@@ -98,8 +92,6 @@ class ExifInfo {
|
||||
description.hashCode ^
|
||||
orientation.hashCode ^
|
||||
isFlipped.hashCode ^
|
||||
width.hashCode ^
|
||||
height.hashCode ^
|
||||
timeZone.hashCode ^
|
||||
dateTimeOriginal.hashCode ^
|
||||
latitude.hashCode ^
|
||||
@@ -123,8 +115,6 @@ class ExifInfo {
|
||||
fileSize: ${fileSize ?? 'NA'},
|
||||
description: ${description ?? 'NA'},
|
||||
orientation: ${orientation ?? 'NA'},
|
||||
width: ${width ?? 'NA'},
|
||||
height: ${height ?? 'NA'},
|
||||
isFlipped: $isFlipped,
|
||||
timeZone: ${timeZone ?? 'NA'},
|
||||
dateTimeOriginal: ${dateTimeOriginal ?? 'NA'},
|
||||
|
||||
@@ -71,7 +71,8 @@ enum StoreKey<T> {
|
||||
readonlyModeEnabled<bool>._(138),
|
||||
|
||||
autoPlayVideo<bool>._(139),
|
||||
viewerQuickActionOrder<String>._(140),
|
||||
albumGridView<bool>._(140),
|
||||
viewerQuickActionOrder<String>._(141),
|
||||
|
||||
// Experimental stuff
|
||||
photoManagerCustomFilter<bool>._(1000),
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||
|
||||
enum GroupAssetsBy { day, month, auto, none }
|
||||
|
||||
enum HeaderType { none, month, day, monthAndDay }
|
||||
@@ -31,17 +29,3 @@ class TimeBucket extends Bucket {
|
||||
@override
|
||||
int get hashCode => super.hashCode ^ date.hashCode;
|
||||
}
|
||||
|
||||
class TimelineReloadEvent extends Event {
|
||||
const TimelineReloadEvent();
|
||||
}
|
||||
|
||||
class ScrollToTopEvent extends Event {
|
||||
const ScrollToTopEvent();
|
||||
}
|
||||
|
||||
class ScrollToDateEvent extends Event {
|
||||
final DateTime date;
|
||||
|
||||
const ScrollToDateEvent(this.date);
|
||||
}
|
||||
|
||||
@@ -65,8 +65,8 @@ class AssetService {
|
||||
if (asset.hasRemote) {
|
||||
final exif = await getExif(asset);
|
||||
isFlipped = ExifDtoConverter.isOrientationFlipped(exif?.orientation);
|
||||
width = exif?.width ?? asset.width?.toDouble();
|
||||
height = exif?.height ?? asset.height?.toDouble();
|
||||
width = asset.width?.toDouble();
|
||||
height = asset.height?.toDouble();
|
||||
} else if (asset is LocalAsset) {
|
||||
isFlipped = CurrentPlatform.isAndroid && (asset.orientation == 90 || asset.orientation == 270);
|
||||
width = asset.width?.toDouble();
|
||||
@@ -75,6 +75,20 @@ class AssetService {
|
||||
isFlipped = false;
|
||||
}
|
||||
|
||||
if (width == null || height == null) {
|
||||
if (asset.hasRemote) {
|
||||
final id = asset is LocalAsset ? asset.remoteId! : (asset as RemoteAsset).id;
|
||||
final remoteAsset = await _remoteAssetRepository.get(id);
|
||||
width = remoteAsset?.width?.toDouble();
|
||||
height = remoteAsset?.height?.toDouble();
|
||||
} else {
|
||||
final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).localId!;
|
||||
final localAsset = await _localAssetRepository.get(id);
|
||||
width = localAsset?.width?.toDouble();
|
||||
height = localAsset?.height?.toDouble();
|
||||
}
|
||||
}
|
||||
|
||||
final orientedWidth = isFlipped ? height : width;
|
||||
final orientedHeight = isFlipped ? width : height;
|
||||
if (orientedWidth != null && orientedHeight != null && orientedHeight > 0) {
|
||||
|
||||
@@ -177,6 +177,12 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||
}
|
||||
|
||||
Future<void> _cleanup() async {
|
||||
await runZonedGuarded(_handleCleanup, (error, stack) {
|
||||
dPrint(() => "Error during background worker cleanup: $error, $stack");
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _handleCleanup() async {
|
||||
// If ref is null, it means the service was never initialized properly
|
||||
if (_isCleanedUp || _ref == null) {
|
||||
return;
|
||||
@@ -186,11 +192,16 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||
_isCleanedUp = true;
|
||||
final backgroundSyncManager = _ref?.read(backgroundSyncProvider);
|
||||
final nativeSyncApi = _ref?.read(nativeSyncApiProvider);
|
||||
|
||||
await _drift.close();
|
||||
await _driftLogger.close();
|
||||
|
||||
_ref?.dispose();
|
||||
_ref = null;
|
||||
|
||||
_cancellationToken.cancel();
|
||||
_logger.info("Cleaning up background worker");
|
||||
|
||||
final cleanupFutures = [
|
||||
nativeSyncApi?.cancelHashing(),
|
||||
workerManagerPatch.dispose().catchError((_) async {
|
||||
@@ -199,8 +210,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||
}),
|
||||
LogService.I.dispose(),
|
||||
Store.dispose(),
|
||||
_drift.close(),
|
||||
_driftLogger.close(),
|
||||
|
||||
backgroundSyncManager?.cancel(),
|
||||
];
|
||||
|
||||
|
||||
@@ -286,11 +286,23 @@ class LocalSyncService {
|
||||
}
|
||||
|
||||
bool _assetsEqual(LocalAsset a, LocalAsset b) {
|
||||
return a.updatedAt.isAtSameMomentAs(b.updatedAt) &&
|
||||
if (CurrentPlatform.isAndroid) {
|
||||
return a.updatedAt.isAtSameMomentAs(b.updatedAt) &&
|
||||
a.createdAt.isAtSameMomentAs(b.createdAt) &&
|
||||
a.width == b.width &&
|
||||
a.height == b.height &&
|
||||
a.durationInSeconds == b.durationInSeconds;
|
||||
}
|
||||
|
||||
final firstAdjustment = a.adjustmentTime?.millisecondsSinceEpoch ?? 0;
|
||||
final secondAdjustment = b.adjustmentTime?.millisecondsSinceEpoch ?? 0;
|
||||
return firstAdjustment == secondAdjustment &&
|
||||
a.createdAt.isAtSameMomentAs(b.createdAt) &&
|
||||
a.width == b.width &&
|
||||
a.height == b.height &&
|
||||
a.durationInSeconds == b.durationInSeconds;
|
||||
a.durationInSeconds == b.durationInSeconds &&
|
||||
a.latitude == b.latitude &&
|
||||
a.longitude == b.longitude;
|
||||
}
|
||||
|
||||
bool _albumsEqual(LocalAlbum a, LocalAlbum b) {
|
||||
@@ -363,18 +375,21 @@ extension on Iterable<PlatformAsset> {
|
||||
}
|
||||
}
|
||||
|
||||
extension on PlatformAsset {
|
||||
extension PlatformToLocalAsset on PlatformAsset {
|
||||
LocalAsset toLocalAsset() => LocalAsset(
|
||||
id: id,
|
||||
name: name,
|
||||
checksum: null,
|
||||
type: AssetType.values.elementAtOrNull(type) ?? AssetType.other,
|
||||
createdAt: tryFromSecondsSinceEpoch(createdAt, isUtc: true) ?? DateTime.timestamp(),
|
||||
updatedAt: tryFromSecondsSinceEpoch(createdAt, isUtc: true) ?? DateTime.timestamp(),
|
||||
updatedAt: tryFromSecondsSinceEpoch(updatedAt, isUtc: true) ?? DateTime.timestamp(),
|
||||
width: width,
|
||||
height: height,
|
||||
durationInSeconds: durationInSeconds,
|
||||
isFavorite: isFavorite,
|
||||
orientation: orientation,
|
||||
adjustmentTime: tryFromSecondsSinceEpoch(adjustmentTime, isUtc: true),
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
|
||||
import 'package:immich_mobile/models/albums/album_search.model.dart';
|
||||
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
|
||||
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
||||
|
||||
class RemoteAlbumService {
|
||||
final DriftRemoteAlbumRepository _repository;
|
||||
@@ -32,16 +33,16 @@ class RemoteAlbumService {
|
||||
|
||||
Future<List<RemoteAlbum>> sortAlbums(
|
||||
List<RemoteAlbum> albums,
|
||||
RemoteAlbumSortMode sortMode, {
|
||||
AlbumSortMode sortMode, {
|
||||
bool isReverse = false,
|
||||
}) async {
|
||||
final List<RemoteAlbum> sorted = switch (sortMode) {
|
||||
RemoteAlbumSortMode.created => albums.sortedBy((album) => album.createdAt),
|
||||
RemoteAlbumSortMode.title => albums.sortedBy((album) => album.name),
|
||||
RemoteAlbumSortMode.lastModified => albums.sortedBy((album) => album.updatedAt),
|
||||
RemoteAlbumSortMode.assetCount => albums.sortedBy((album) => album.assetCount),
|
||||
RemoteAlbumSortMode.mostRecent => await _sortByNewestAsset(albums),
|
||||
RemoteAlbumSortMode.mostOldest => await _sortByOldestAsset(albums),
|
||||
AlbumSortMode.created => albums.sortedBy((album) => album.createdAt),
|
||||
AlbumSortMode.title => albums.sortedBy((album) => album.name),
|
||||
AlbumSortMode.lastModified => albums.sortedBy((album) => album.updatedAt),
|
||||
AlbumSortMode.assetCount => albums.sortedBy((album) => album.assetCount),
|
||||
AlbumSortMode.mostRecent => await _sortByNewestAsset(albums),
|
||||
AlbumSortMode.mostOldest => await _sortByOldestAsset(albums),
|
||||
};
|
||||
|
||||
return (isReverse ? sorted.reversed : sorted).toList();
|
||||
@@ -211,16 +212,3 @@ class RemoteAlbumService {
|
||||
return sorted.reversed.toList();
|
||||
}
|
||||
}
|
||||
|
||||
enum RemoteAlbumSortMode {
|
||||
title("library_page_sort_title"),
|
||||
assetCount("library_page_sort_asset_count"),
|
||||
lastModified("library_page_sort_last_modified"),
|
||||
created("library_page_sort_created"),
|
||||
mostRecent("sort_newest"),
|
||||
mostOldest("sort_oldest");
|
||||
|
||||
final String key;
|
||||
|
||||
const RemoteAlbumSortMode(this.key);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'dart:math' as math;
|
||||
import 'package:collection/collection.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/events.model.dart';
|
||||
import 'package:immich_mobile/domain/models/setting.model.dart';
|
||||
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||
import 'package:immich_mobile/domain/services/setting.service.dart';
|
||||
|
||||
Reference in New Issue
Block a user