mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
Merge remote-tracking branch 'origin/main' into feat/integrity-checks-izzy
This commit is contained in:
@@ -35,8 +35,8 @@ platform :android do
|
||||
task: 'bundle',
|
||||
build_type: 'Release',
|
||||
properties: {
|
||||
"android.injected.version.code" => 3036,
|
||||
"android.injected.version.name" => "2.5.5",
|
||||
"android.injected.version.code" => 3037,
|
||||
"android.injected.version.name" => "2.5.6",
|
||||
}
|
||||
)
|
||||
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2.5.5</string>
|
||||
<string>2.5.6</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
enum SortOrder { asc, desc }
|
||||
enum SortOrder {
|
||||
asc,
|
||||
desc;
|
||||
|
||||
SortOrder reverse() {
|
||||
return this == SortOrder.asc ? SortOrder.desc : SortOrder.asc;
|
||||
}
|
||||
}
|
||||
|
||||
enum TextSearchType { context, filename, description, ocr }
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
@@ -36,6 +37,7 @@ class RemoteAlbumService {
|
||||
AlbumSortMode sortMode, {
|
||||
bool isReverse = false,
|
||||
}) async {
|
||||
// list of albums sorted ascendingly according to the selected sort mode
|
||||
final List<RemoteAlbum> sorted = switch (sortMode) {
|
||||
AlbumSortMode.created => albums.sortedBy((album) => album.createdAt),
|
||||
AlbumSortMode.title => albums.sortedBy((album) => album.name),
|
||||
@@ -44,8 +46,9 @@ class RemoteAlbumService {
|
||||
AlbumSortMode.mostRecent => await _sortByNewestAsset(albums),
|
||||
AlbumSortMode.mostOldest => await _sortByOldestAsset(albums),
|
||||
};
|
||||
final effectiveOrder = isReverse ? sortMode.defaultOrder.reverse() : sortMode.defaultOrder;
|
||||
|
||||
return (isReverse ? sorted.reversed : sorted).toList();
|
||||
return (effectiveOrder == SortOrder.asc ? sorted : sorted.reversed).toList();
|
||||
}
|
||||
|
||||
List<RemoteAlbum> searchAlbums(
|
||||
@@ -209,6 +212,6 @@ class RemoteAlbumService {
|
||||
return aDate.compareTo(bDate);
|
||||
});
|
||||
|
||||
return sorted.reversed.toList();
|
||||
return sorted;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,8 +90,14 @@ FROM
|
||||
(
|
||||
SELECT
|
||||
CASE
|
||||
WHEN :group_by = 0 THEN STRFTIME('%Y-%m-%d', rae.local_date_time)
|
||||
WHEN :group_by = 1 THEN STRFTIME('%Y-%m', rae.local_date_time)
|
||||
WHEN :group_by = 0 THEN COALESCE(
|
||||
STRFTIME('%Y-%m-%d', rae.local_date_time),
|
||||
STRFTIME('%Y-%m-%d', rae.created_at, 'localtime')
|
||||
)
|
||||
WHEN :group_by = 1 THEN COALESCE(
|
||||
STRFTIME('%Y-%m', rae.local_date_time),
|
||||
STRFTIME('%Y-%m', rae.created_at, 'localtime')
|
||||
)
|
||||
END as bucket_date
|
||||
FROM
|
||||
remote_asset_entity rae
|
||||
|
||||
+1
-1
@@ -79,7 +79,7 @@ class MergedAssetDrift extends i1.ModularAccessor {
|
||||
final expandeduserIds = $expandVar($arrayStartIndex, userIds.length);
|
||||
$arrayStartIndex += userIds.length;
|
||||
return customSelect(
|
||||
'SELECT COUNT(*) AS asset_count, bucket_date FROM (SELECT CASE WHEN ?1 = 0 THEN STRFTIME(\'%Y-%m-%d\', rae.local_date_time) WHEN ?1 = 1 THEN STRFTIME(\'%Y-%m\', rae.local_date_time) END AS bucket_date FROM remote_asset_entity AS rae LEFT JOIN stack_entity AS se ON rae.stack_id = se.id WHERE rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id IN ($expandeduserIds) AND(rae.stack_id IS NULL OR rae.id = se.primary_asset_id)UNION ALL SELECT CASE WHEN ?1 = 0 THEN STRFTIME(\'%Y-%m-%d\', lae.created_at, \'localtime\') WHEN ?1 = 1 THEN STRFTIME(\'%Y-%m\', lae.created_at, \'localtime\') END AS bucket_date FROM local_asset_entity AS lae WHERE NOT EXISTS (SELECT 1 FROM remote_asset_entity AS rae WHERE rae.checksum = lae.checksum AND rae.owner_id IN ($expandeduserIds)) AND EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 0) AND NOT EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 2)) GROUP BY bucket_date ORDER BY bucket_date DESC',
|
||||
'SELECT COUNT(*) AS asset_count, bucket_date FROM (SELECT CASE WHEN ?1 = 0 THEN COALESCE(STRFTIME(\'%Y-%m-%d\', rae.local_date_time), STRFTIME(\'%Y-%m-%d\', rae.created_at, \'localtime\')) WHEN ?1 = 1 THEN COALESCE(STRFTIME(\'%Y-%m\', rae.local_date_time), STRFTIME(\'%Y-%m\', rae.created_at, \'localtime\')) END AS bucket_date FROM remote_asset_entity AS rae LEFT JOIN stack_entity AS se ON rae.stack_id = se.id WHERE rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id IN ($expandeduserIds) AND(rae.stack_id IS NULL OR rae.id = se.primary_asset_id)UNION ALL SELECT CASE WHEN ?1 = 0 THEN STRFTIME(\'%Y-%m-%d\', lae.created_at, \'localtime\') WHEN ?1 = 1 THEN STRFTIME(\'%Y-%m\', lae.created_at, \'localtime\') END AS bucket_date FROM local_asset_entity AS lae WHERE NOT EXISTS (SELECT 1 FROM remote_asset_entity AS rae WHERE rae.checksum = lae.checksum AND rae.owner_id IN ($expandeduserIds)) AND EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 0) AND NOT EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 2)) GROUP BY bucket_date ORDER BY bucket_date DESC',
|
||||
variables: [
|
||||
i0.Variable<int>(groupBy),
|
||||
for (var $ in userIds) i0.Variable<String>($),
|
||||
|
||||
@@ -75,7 +75,7 @@ Future<void> initApp() async {
|
||||
await EasyLocalization.ensureInitialized();
|
||||
await initializeDateFormatting();
|
||||
|
||||
if (kReleaseMode && Platform.isAndroid) {
|
||||
if (Platform.isAndroid) {
|
||||
try {
|
||||
await FlutterDisplayMode.setHighRefreshRate();
|
||||
dPrint(() => "Enabled high refresh mode");
|
||||
@@ -114,7 +114,7 @@ Future<void> initApp() async {
|
||||
|
||||
await FileDownloader().trackTasksInGroup(kDownloadGroupLivePhoto, markDownloadedComplete: false);
|
||||
|
||||
await FileDownloader().trackTasks();
|
||||
unawaited(FileDownloader().trackTasks());
|
||||
|
||||
LicenseRegistry.addLicense(() async* {
|
||||
for (final license in nonPubLicenses.entries) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
@@ -281,6 +282,8 @@ class _SortButtonState extends ConsumerState<_SortButton> {
|
||||
setState(() {
|
||||
albumSortOption = sortMode;
|
||||
isSorting = true;
|
||||
// reset sort order to default state when switching option
|
||||
albumSortIsReverse = false;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -293,6 +296,7 @@ class _SortButtonState extends ConsumerState<_SortButton> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final effectiveOrder = albumSortOption.effectiveOrder(albumSortIsReverse);
|
||||
return MenuAnchor(
|
||||
controller: widget.controller,
|
||||
style: MenuStyle(
|
||||
@@ -307,7 +311,7 @@ class _SortButtonState extends ConsumerState<_SortButton> {
|
||||
.map(
|
||||
(sortMode) => MenuItemButton(
|
||||
leadingIcon: albumSortOption == sortMode
|
||||
? albumSortIsReverse
|
||||
? effectiveOrder == SortOrder.desc
|
||||
? Icon(
|
||||
Icons.keyboard_arrow_down,
|
||||
color: albumSortOption == sortMode
|
||||
@@ -355,7 +359,7 @@ class _SortButtonState extends ConsumerState<_SortButton> {
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 5),
|
||||
child: albumSortIsReverse
|
||||
child: effectiveOrder == SortOrder.desc
|
||||
? Icon(Icons.keyboard_arrow_down, color: context.colorScheme.onSurface)
|
||||
: Icon(Icons.keyboard_arrow_up_rounded, color: context.colorScheme.onSurface),
|
||||
),
|
||||
|
||||
@@ -62,6 +62,9 @@ mixin CancellableImageProviderMixin<T extends Object> on CancellableImageProvide
|
||||
return;
|
||||
}
|
||||
yield image;
|
||||
} catch (e) {
|
||||
PaintingBinding.instance.imageCache.evict(this);
|
||||
rethrow;
|
||||
} finally {
|
||||
this.request = null;
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ class LocalThumbProvider extends CancellableImageProvider<LocalThumbProvider>
|
||||
DiagnosticsProperty<String>('Id', key.id),
|
||||
DiagnosticsProperty<Size>('Size', key.size),
|
||||
],
|
||||
onDispose: cancel,
|
||||
onLastListenerRemoved: cancel,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ class LocalFullImageProvider extends CancellableImageProvider<LocalFullImageProv
|
||||
DiagnosticsProperty<String>('Id', key.id),
|
||||
DiagnosticsProperty<Size>('Size', key.size),
|
||||
],
|
||||
onDispose: cancel,
|
||||
onLastListenerRemoved: cancel,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+29
-10
@@ -9,7 +9,10 @@ import 'package:flutter/painting.dart';
|
||||
|
||||
/// An ImageStreamCompleter with support for loading multiple images.
|
||||
class OneFramePlaceholderImageStreamCompleter extends ImageStreamCompleter {
|
||||
void Function()? _onDispose;
|
||||
void Function()? _onLastListenerRemoved;
|
||||
int _listenerCount = 0;
|
||||
// True once setImage() has been called at least once.
|
||||
bool didProvideImage = false;
|
||||
|
||||
/// The constructor to create an OneFramePlaceholderImageStreamCompleter. The [images]
|
||||
/// should be the primary images to display (typically asynchronously as they load).
|
||||
@@ -19,14 +22,18 @@ class OneFramePlaceholderImageStreamCompleter extends ImageStreamCompleter {
|
||||
Stream<ImageInfo> images, {
|
||||
ImageInfo? initialImage,
|
||||
InformationCollector? informationCollector,
|
||||
void Function()? onDispose,
|
||||
void Function()? onLastListenerRemoved,
|
||||
}) {
|
||||
if (initialImage != null) {
|
||||
didProvideImage = true;
|
||||
setImage(initialImage);
|
||||
}
|
||||
_onDispose = onDispose;
|
||||
_onLastListenerRemoved = onLastListenerRemoved;
|
||||
images.listen(
|
||||
setImage,
|
||||
(image) {
|
||||
didProvideImage = true;
|
||||
setImage(image);
|
||||
},
|
||||
onError: (Object error, StackTrace stack) {
|
||||
reportError(
|
||||
context: ErrorDescription('resolving a single-frame image stream'),
|
||||
@@ -40,12 +47,24 @@ class OneFramePlaceholderImageStreamCompleter extends ImageStreamCompleter {
|
||||
}
|
||||
|
||||
@override
|
||||
void onDisposed() {
|
||||
final onDispose = _onDispose;
|
||||
if (onDispose != null) {
|
||||
_onDispose = null;
|
||||
onDispose();
|
||||
void addListener(ImageStreamListener listener) {
|
||||
super.addListener(listener);
|
||||
_listenerCount = _listenerCount + 1;
|
||||
}
|
||||
|
||||
@override
|
||||
void removeListener(ImageStreamListener listener) {
|
||||
super.removeListener(listener);
|
||||
_listenerCount = _listenerCount - 1;
|
||||
|
||||
final bool onlyCacheListenerLeft = _listenerCount == 1 && !didProvideImage;
|
||||
final bool noListenersAfterImage = _listenerCount == 0 && didProvideImage;
|
||||
|
||||
final onLastListenerRemoved = _onLastListenerRemoved;
|
||||
|
||||
if (onLastListenerRemoved != null && (noListenersAfterImage || onlyCacheListenerLeft)) {
|
||||
_onLastListenerRemoved = null;
|
||||
onLastListenerRemoved();
|
||||
}
|
||||
super.onDisposed();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ class RemoteImageProvider extends CancellableImageProvider<RemoteImageProvider>
|
||||
DiagnosticsProperty<ImageProvider>('Image provider', this),
|
||||
DiagnosticsProperty<String>('URL', key.url),
|
||||
],
|
||||
onDispose: cancel,
|
||||
onLastListenerRemoved: cancel,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImagePr
|
||||
DiagnosticsProperty<ImageProvider>('Image provider', this),
|
||||
DiagnosticsProperty<String>('Asset Id', key.assetId),
|
||||
],
|
||||
onDispose: cancel,
|
||||
onLastListenerRemoved: cancel,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ class ThumbHashProvider extends CancellableImageProvider<ThumbHashProvider>
|
||||
|
||||
@override
|
||||
ImageStreamCompleter loadImage(ThumbHashProvider key, ImageDecoderCallback decode) {
|
||||
return OneFramePlaceholderImageStreamCompleter(_loadCodec(key, decode), onDispose: cancel);
|
||||
return OneFramePlaceholderImageStreamCompleter(_loadCodec(key, decode), onLastListenerRemoved: cancel);
|
||||
}
|
||||
|
||||
Stream<ImageInfo> _loadCodec(ThumbHashProvider key, ImageDecoderCallback decode) {
|
||||
|
||||
@@ -233,16 +233,6 @@ class _ThumbnailState extends State<Thumbnail> with SingleTickerProviderStateMix
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
final imageProvider = widget.imageProvider;
|
||||
if (imageProvider is CancellableImageProvider) {
|
||||
imageProvider.cancel();
|
||||
}
|
||||
|
||||
final thumbhashProvider = widget.thumbhashProvider;
|
||||
if (thumbhashProvider is CancellableImageProvider) {
|
||||
thumbhashProvider.cancel();
|
||||
}
|
||||
|
||||
_fadeController.removeStatusListener(_onAnimationStatusChanged);
|
||||
_fadeController.dispose();
|
||||
_stopListeningToStream();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/entities/album.entity.dart';
|
||||
@@ -73,18 +74,21 @@ class _AlbumSortHandlers {
|
||||
|
||||
// Store index allows us to re-arrange the values without affecting the saved prefs
|
||||
enum AlbumSortMode {
|
||||
title(1, "library_page_sort_title", _AlbumSortHandlers.title),
|
||||
assetCount(4, "library_page_sort_asset_count", _AlbumSortHandlers.assetCount),
|
||||
lastModified(3, "library_page_sort_last_modified", _AlbumSortHandlers.lastModified),
|
||||
created(0, "library_page_sort_created", _AlbumSortHandlers.created),
|
||||
mostRecent(2, "sort_recent", _AlbumSortHandlers.mostRecent),
|
||||
mostOldest(5, "sort_oldest", _AlbumSortHandlers.mostOldest);
|
||||
title(1, "library_page_sort_title", _AlbumSortHandlers.title, SortOrder.asc),
|
||||
assetCount(4, "library_page_sort_asset_count", _AlbumSortHandlers.assetCount, SortOrder.desc),
|
||||
lastModified(3, "library_page_sort_last_modified", _AlbumSortHandlers.lastModified, SortOrder.desc),
|
||||
created(0, "library_page_sort_created", _AlbumSortHandlers.created, SortOrder.desc),
|
||||
mostRecent(2, "sort_recent", _AlbumSortHandlers.mostRecent, SortOrder.desc),
|
||||
mostOldest(5, "sort_oldest", _AlbumSortHandlers.mostOldest, SortOrder.asc);
|
||||
|
||||
final int storeIndex;
|
||||
final String label;
|
||||
final AlbumSortFn sortFn;
|
||||
final SortOrder defaultOrder;
|
||||
|
||||
const AlbumSortMode(this.storeIndex, this.label, this.sortFn);
|
||||
const AlbumSortMode(this.storeIndex, this.label, this.sortFn, this.defaultOrder);
|
||||
|
||||
SortOrder effectiveOrder(bool isReverse) => isReverse ? defaultOrder.reverse() : defaultOrder;
|
||||
}
|
||||
|
||||
@riverpod
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||
import 'package:immich_mobile/repositories/asset_media.repository.dart';
|
||||
@@ -9,6 +10,8 @@ final cleanupServiceProvider = Provider<CleanupService>((ref) {
|
||||
});
|
||||
|
||||
class CleanupService {
|
||||
static final int _deleteBatchSize = CurrentPlatform.isAndroid ? 2000 : 10000;
|
||||
|
||||
final DriftLocalAssetRepository _localAssetRepository;
|
||||
final AssetMediaRepository _assetMediaRepository;
|
||||
|
||||
@@ -35,13 +38,20 @@ class CleanupService {
|
||||
return 0;
|
||||
}
|
||||
|
||||
final deletedIds = await _assetMediaRepository.deleteAll(localIds);
|
||||
if (deletedIds.isNotEmpty) {
|
||||
await _localAssetRepository.delete(deletedIds);
|
||||
return deletedIds.length;
|
||||
int deletedCount = 0;
|
||||
|
||||
for (int index = 0; index < localIds.length; index += _deleteBatchSize) {
|
||||
final end = index + _deleteBatchSize < localIds.length ? index + _deleteBatchSize : localIds.length;
|
||||
final batch = localIds.sublist(index, end);
|
||||
|
||||
final deletedIds = await _assetMediaRepository.deleteAll(batch);
|
||||
if (deletedIds.isNotEmpty) {
|
||||
await _localAssetRepository.delete(deletedIds);
|
||||
deletedCount += deletedIds.length;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
return deletedCount;
|
||||
}
|
||||
|
||||
/// Returns album IDs that should be kept by default (e.g., messaging app albums)
|
||||
|
||||
@@ -27,19 +27,17 @@ import 'package:isar/isar.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
void configureFileDownloaderNotifications() {
|
||||
final fileName = 'file_name'.t(args: {'file_name': '{filename}'});
|
||||
|
||||
FileDownloader().configureNotificationForGroup(
|
||||
kDownloadGroupImage,
|
||||
running: TaskNotification('downloading_media'.t(), fileName),
|
||||
complete: TaskNotification('download_finished'.t(), fileName),
|
||||
running: TaskNotification('downloading_media'.t(), '${'file_name_text'.t()}: {filename}'),
|
||||
complete: TaskNotification('download_finished'.t(), '${'file_name_text'.t()}: {filename}'),
|
||||
progressBar: true,
|
||||
);
|
||||
|
||||
FileDownloader().configureNotificationForGroup(
|
||||
kDownloadGroupVideo,
|
||||
running: TaskNotification('downloading_media'.t(), fileName),
|
||||
complete: TaskNotification('download_finished'.t(), fileName),
|
||||
running: TaskNotification('downloading_media'.t(), '${'file_name_text'.t()}: {filename}'),
|
||||
complete: TaskNotification('download_finished'.t(), '${'file_name_text'.t()}: {filename}'),
|
||||
progressBar: true,
|
||||
);
|
||||
|
||||
|
||||
@@ -128,6 +128,9 @@ class _ProfileIndicator extends ConsumerWidget {
|
||||
|
||||
const widgetSize = 30.0;
|
||||
|
||||
// TODO: remove this when update Flutter version newer than 3.35.7
|
||||
final isIpad = defaultTargetPlatform == TargetPlatform.iOS && !context.isMobile;
|
||||
|
||||
void toggleReadonlyMode() {
|
||||
final isReadonlyModeEnabled = ref.watch(readonlyModeProvider);
|
||||
ref.read(readonlyModeProvider.notifier).toggleReadonlyMode();
|
||||
@@ -144,7 +147,12 @@ class _ProfileIndicator extends ConsumerWidget {
|
||||
}
|
||||
|
||||
return InkWell(
|
||||
onTap: () => showDialog(context: context, useRootNavigator: false, builder: (ctx) => const ImmichAppBarDialog()),
|
||||
onTap: () => showDialog(
|
||||
context: context,
|
||||
useRootNavigator: false,
|
||||
barrierDismissible: !isIpad,
|
||||
builder: (ctx) => const ImmichAppBarDialog(),
|
||||
),
|
||||
onLongPress: () => toggleReadonlyMode(),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||
child: Badge(
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
@@ -20,7 +18,7 @@ class UserCircleAvatar extends ConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final userAvatarColor = user.avatarColor.toColor();
|
||||
final profileImageUrl =
|
||||
'${Store.get(StoreKey.serverEndpoint)}/users/${user.id}/profile-image?d=${Random().nextInt(1024)}';
|
||||
'${Store.get(StoreKey.serverEndpoint)}/users/${user.id}/profile-image?d=${user.profileChangedAt.millisecondsSinceEpoch}';
|
||||
|
||||
final textIcon = DefaultTextStyle(
|
||||
style: TextStyle(
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ name: immich_mobile
|
||||
description: Immich - selfhosted backup media file on mobile phone
|
||||
|
||||
publish_to: 'none'
|
||||
version: 2.5.5+3036
|
||||
version: 2.5.6+3037
|
||||
|
||||
environment:
|
||||
sdk: '>=3.8.0 <4.0.0'
|
||||
|
||||
@@ -85,35 +85,47 @@ void main() {
|
||||
final albums = [albumB, albumA];
|
||||
|
||||
final result = await sut.sortAlbums(albums, AlbumSortMode.created);
|
||||
expect(result, [albumA, albumB]);
|
||||
expect(result, [albumB, albumA]);
|
||||
});
|
||||
|
||||
test('should sort correctly based on updatedAt', () async {
|
||||
final albums = [albumB, albumA];
|
||||
|
||||
final result = await sut.sortAlbums(albums, AlbumSortMode.lastModified);
|
||||
expect(result, [albumA, albumB]);
|
||||
expect(result, [albumB, albumA]);
|
||||
});
|
||||
|
||||
test('should sort correctly based on assetCount', () async {
|
||||
final albums = [albumB, albumA];
|
||||
|
||||
final result = await sut.sortAlbums(albums, AlbumSortMode.assetCount);
|
||||
expect(result, [albumA, albumB]);
|
||||
expect(result, [albumB, albumA]);
|
||||
});
|
||||
|
||||
test('should sort correctly based on newestAssetTimestamp', () async {
|
||||
final albums = [albumB, albumA];
|
||||
|
||||
final result = await sut.sortAlbums(albums, AlbumSortMode.mostRecent);
|
||||
expect(result, [albumA, albumB]);
|
||||
expect(result, [albumB, albumA]);
|
||||
});
|
||||
|
||||
test('should sort correctly based on oldestAssetTimestamp', () async {
|
||||
final albums = [albumB, albumA];
|
||||
|
||||
final result = await sut.sortAlbums(albums, AlbumSortMode.mostOldest);
|
||||
expect(result, [albumB, albumA]);
|
||||
expect(result, [albumA, albumB]);
|
||||
});
|
||||
|
||||
test('should flip order when isReverse is true for all modes', () async {
|
||||
final albums = [albumB, albumA];
|
||||
|
||||
for (final mode in AlbumSortMode.values) {
|
||||
final normal = await sut.sortAlbums(albums, mode, isReverse: false);
|
||||
final reversed = await sut.sortAlbums(albums, mode, isReverse: true);
|
||||
|
||||
// reversed should be the exact inverse of normal
|
||||
expect(reversed, normal.reversed.toList(), reason: 'Mode: $mode');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
|
||||
void main() {
|
||||
late Drift db;
|
||||
|
||||
setUp(() {
|
||||
db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true));
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
test('mergedBucket falls back to createdAt when localDateTime is null', () async {
|
||||
const userId = 'user-1';
|
||||
final createdAt = DateTime(2024, 1, 1, 12);
|
||||
|
||||
await db
|
||||
.into(db.userEntity)
|
||||
.insert(UserEntityCompanion.insert(id: userId, email: 'user-1@test.dev', name: 'User 1'));
|
||||
|
||||
await db
|
||||
.into(db.remoteAssetEntity)
|
||||
.insert(
|
||||
RemoteAssetEntityCompanion.insert(
|
||||
id: 'asset-1',
|
||||
name: 'asset-1.jpg',
|
||||
type: AssetType.image,
|
||||
checksum: 'checksum-1',
|
||||
ownerId: userId,
|
||||
visibility: AssetVisibility.timeline,
|
||||
createdAt: Value(createdAt),
|
||||
updatedAt: Value(createdAt),
|
||||
localDateTime: const Value(null),
|
||||
),
|
||||
);
|
||||
|
||||
final buckets = await db.mergedAssetDrift.mergedBucket(groupBy: GroupAssetsBy.day.index, userIds: [userId]).get();
|
||||
|
||||
expect(buckets, hasLength(1));
|
||||
expect(buckets.single.assetCount, 1);
|
||||
expect(buckets.single.bucketDate, isNotEmpty);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||
import 'package:immich_mobile/services/cleanup.service.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
import '../infrastructure/repository.mock.dart';
|
||||
import '../repository.mocks.dart';
|
||||
|
||||
void main() {
|
||||
late CleanupService sut;
|
||||
|
||||
late MockDriftLocalAssetRepository localAssetRepository;
|
||||
late MockAssetMediaRepository assetMediaRepository;
|
||||
|
||||
setUp(() {
|
||||
localAssetRepository = MockDriftLocalAssetRepository();
|
||||
assetMediaRepository = MockAssetMediaRepository();
|
||||
sut = CleanupService(localAssetRepository, assetMediaRepository);
|
||||
});
|
||||
|
||||
group('CleanupService.deleteLocalAssets', () {
|
||||
test('returns 0 and does nothing for empty input', () async {
|
||||
final result = await sut.deleteLocalAssets([]);
|
||||
|
||||
expect(result, 0);
|
||||
verifyNever(() => assetMediaRepository.deleteAll(any()));
|
||||
verifyNever(() => localAssetRepository.delete(any()));
|
||||
});
|
||||
|
||||
test('deletes in a single batch when under limit', () async {
|
||||
final ids = List.generate(999, (i) => 'asset-$i');
|
||||
|
||||
when(() => assetMediaRepository.deleteAll(any())).thenAnswer((invocation) async {
|
||||
return (invocation.positionalArguments.first as List<String>).toList();
|
||||
});
|
||||
when(() => localAssetRepository.delete(any())).thenAnswer((_) async {});
|
||||
|
||||
final result = await sut.deleteLocalAssets(ids);
|
||||
|
||||
expect(result, ids.length);
|
||||
verify(() => assetMediaRepository.deleteAll(ids)).called(1);
|
||||
verify(() => localAssetRepository.delete(ids)).called(1);
|
||||
});
|
||||
|
||||
test('deletes in platform-specific batches when over limit', () async {
|
||||
final batchSize = CurrentPlatform.isAndroid ? 2000 : 10000;
|
||||
final ids = List.generate(batchSize * 2 + 501, (i) => 'asset-$i');
|
||||
final capturedBatches = <List<String>>[];
|
||||
|
||||
when(() => assetMediaRepository.deleteAll(any())).thenAnswer((invocation) async {
|
||||
final batch = (invocation.positionalArguments.first as List<String>).toList();
|
||||
capturedBatches.add(batch);
|
||||
return batch;
|
||||
});
|
||||
when(() => localAssetRepository.delete(any())).thenAnswer((_) async {});
|
||||
|
||||
final result = await sut.deleteLocalAssets(ids);
|
||||
|
||||
expect(result, ids.length);
|
||||
expect(capturedBatches.length, 3);
|
||||
expect(capturedBatches[0].length, batchSize);
|
||||
expect(capturedBatches[1].length, batchSize);
|
||||
expect(capturedBatches[2].length, 501);
|
||||
expect(capturedBatches[0].first, 'asset-0');
|
||||
expect(capturedBatches[0].last, 'asset-${batchSize - 1}');
|
||||
expect(capturedBatches[1].first, 'asset-$batchSize');
|
||||
expect(capturedBatches[1].last, 'asset-${batchSize * 2 - 1}');
|
||||
expect(capturedBatches[2].first, 'asset-${batchSize * 2}');
|
||||
expect(capturedBatches[2].last, 'asset-${batchSize * 2 + 500}');
|
||||
verify(() => localAssetRepository.delete(any())).called(3);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user