Merge branch 'main' into feat/custom-date-range

This commit is contained in:
Yaros
2026-05-13 18:46:08 +02:00
426 changed files with 18698 additions and 4342 deletions
+3 -8
View File
@@ -22,10 +22,10 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/open_in_browse
import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_lock_folder_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/set_album_cover.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/set_profile_picture_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/similar_photos_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/set_profile_picture_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart';
@@ -44,7 +44,6 @@ class ActionButtonContext {
final ActionSource source;
final bool isCasting;
final TimelineOrigin timelineOrigin;
final ThemeData? originalTheme;
final int selectedCount;
const ActionButtonContext({
@@ -59,7 +58,6 @@ class ActionButtonContext {
required this.source,
this.isCasting = false,
this.timelineOrigin = TimelineOrigin.main,
this.originalTheme,
this.selectedCount = 1,
});
}
@@ -244,7 +242,6 @@ enum ActionButtonType {
origin: context.timelineOrigin,
iconOnly: iconOnly,
menuItem: menuItem,
iconColor: context.originalTheme?.iconTheme.color,
),
ActionButtonType.similarPhotos => SimilarPhotosActionButton(
assetId: (context.asset as RemoteAsset).id,
@@ -259,14 +256,12 @@ enum ActionButtonType {
ActionButtonType.openInfo => BaseActionButton(
label: 'info'.tr(),
iconData: Icons.info_outline,
iconColor: context.originalTheme?.iconTheme.color,
menuItem: true,
onPressed: () => EventStream.shared.emit(const ViewerShowDetailsEvent()),
),
ActionButtonType.viewInTimeline => BaseActionButton(
label: 'view_in_timeline'.tr(),
iconData: Icons.image_search,
iconColor: context.originalTheme?.iconTheme.color,
iconOnly: iconOnly,
menuItem: menuItem,
onPressed: buildContext == null
@@ -320,7 +315,7 @@ class ActionButtonBuilder {
return _actionTypes.where((type) => type.shouldShow(context)).map((type) => type.buildButton(context)).toList();
}
static List<Widget> buildViewerKebabMenu(ActionButtonContext context, BuildContext buildContext, WidgetRef ref) {
static List<Widget> buildViewerKebabMenu(ActionButtonContext context, BuildContext buildContext) {
final visibleButtons = defaultViewerKebabMenuOrder
.where((type) => !defaultViewerBottomBarButtons.contains(type) && type.shouldShow(context))
.toList();
@@ -336,7 +331,7 @@ class ActionButtonBuilder {
if (lastGroup != null && type.kebabMenuGroup != lastGroup) {
result.add(const Divider(height: 1));
}
result.add(type.buildButton(context, buildContext, false, true).build(buildContext, ref));
result.add(type.buildButton(context, buildContext, false, true));
lastGroup = type.kebabMenuGroup;
}
+3 -1
View File
@@ -18,7 +18,9 @@ String formatBytes(int bytes) {
}
String formatHumanReadableBytes(int bytes, int decimals) {
if (bytes <= 0) return "0 B";
if (bytes <= 0) {
return "0 B";
}
const suffixes = ["B", "KiB", "MiB", "GiB", "TiB"];
var i = (log(bytes) / log(1024)).floor();
return '${(bytes / pow(1024, i)).toStringAsFixed(decimals)} ${suffixes[i]}';
+85 -17
View File
@@ -3,9 +3,11 @@ import 'dart:async';
import 'package:drift/drift.dart';
import 'package:flutter/material.dart';
import 'package:immich_mobile/constants/colors.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/log.model.dart';
import 'package:immich_mobile/domain/models/metadata_key.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/timeline.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/entities/metadata.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
@@ -31,21 +33,69 @@ Future<void> migrateDatabaseIfNeeded(Drift drift) async {
Future<void> _migrateTo25() async {
final accessToken = Store.tryGet(StoreKey.accessToken);
if (accessToken == null || accessToken.isEmpty) return;
if (accessToken == null || accessToken.isEmpty) {
return;
}
final serverUrls = ApiService.getServerUrls();
if (serverUrls.isEmpty) return;
if (serverUrls.isEmpty) {
return;
}
await NetworkRepository.setHeaders(ApiService.getRequestHeaders(), serverUrls, token: accessToken);
}
Future<void> _migrateTo26(Drift drift) async {
final migrator = _StoreMigrator(drift);
await migrator.migrateEnumName(StoreKey.legacyThemeMode, MetadataKey.themeMode, ThemeMode.values);
await migrator.migrateEnumIndex(StoreKey.legacyLogLevel, MetadataKey.logLevel, LogLevel.values);
await migrator.migrateEnumName(StoreKey.legacyPrimaryColor, MetadataKey.primaryColor, ImmichColorPreset.values);
await migrator.migrateBool(StoreKey.legacyDynamicTheme, MetadataKey.dynamicTheme);
await migrator.migrateBool(StoreKey.legacyColorfulInterface, MetadataKey.colorfulInterface);
// Theme
await migrator.migrateEnumName(StoreKey.legacyThemeMode, MetadataKey.themeMode, ThemeMode.values);
await migrator.migrateEnumName(StoreKey.legacyPrimaryColor, MetadataKey.themePrimaryColor, ImmichColorPreset.values);
await migrator.migrateBool(StoreKey.legacyDynamicTheme, MetadataKey.themeDynamic);
await migrator.migrateBool(StoreKey.legacyColorfulInterface, MetadataKey.themeColorfulInterface);
// Cleanup
final cleanupKeepAlbumIds = await migrator.readLegacyStoreString(StoreKey.legacyCleanupKeepAlbumIds.id);
if (cleanupKeepAlbumIds != null) {
final ids = cleanupKeepAlbumIds.split(',').where((id) => id.isNotEmpty).toList();
await drift.metadataEntity.insertOnConflictUpdate(
MetadataEntityCompanion.insert(
key: MetadataKey.cleanupKeepAlbumIds.key,
value: MetadataKey.cleanupKeepAlbumIds.encode(ids),
updatedAt: Value(DateTime.now()),
),
);
await migrator.deleteLegacyStoreRows([StoreKey.legacyCleanupKeepAlbumIds.id]);
}
await migrator.migrateBool(StoreKey.legacyCleanupKeepFavorites, MetadataKey.cleanupKeepFavorites);
await migrator.migrateEnumIndex(
StoreKey.legacyCleanupKeepMediaType,
MetadataKey.cleanupKeepMediaType,
AssetKeepType.values,
);
await migrator.migrateInt(StoreKey.legacyCleanupCutoffDaysAgo, MetadataKey.cleanupCutoffDaysAgo);
await migrator.migrateBool(StoreKey.legacyCleanupDefaultsInitialized, MetadataKey.cleanupDefaultsInitialized);
// Map
await migrator.migrateBool(StoreKey.legacyMapShowFavoriteOnly, MetadataKey.mapShowFavoriteOnly);
await migrator.migrateInt(StoreKey.legacyMapRelativeDate, MetadataKey.mapRelativeDate);
await migrator.migrateBool(StoreKey.legacyMapIncludeArchived, MetadataKey.mapIncludeArchived);
await migrator.migrateEnumIndex(StoreKey.legacyMapThemeMode, MetadataKey.mapThemeMode, ThemeMode.values);
await migrator.migrateBool(StoreKey.legacyMapwithPartners, MetadataKey.mapWithPartners);
// Timeline
await migrator.migrateInt(StoreKey.legacyTilesPerRow, MetadataKey.timelineTilesPerRow);
await migrator.migrateEnumIndex(
StoreKey.legacyGroupAssetsBy,
MetadataKey.timelineGroupAssetsBy,
GroupAssetsBy.values,
);
await migrator.migrateBool(StoreKey.legacyStorageIndicator, MetadataKey.timelineStorageIndicator);
// Image
await migrator.migrateBool(StoreKey.legacyPreferRemoteImage, MetadataKey.imagePreferRemote);
await migrator.migrateBool(StoreKey.legacyLoadOriginal, MetadataKey.imageLoadOriginal);
// Viewer
await migrator.migrateBool(StoreKey.legacyLoopVideo, MetadataKey.viewerLoopVideo);
await migrator.migrateBool(StoreKey.legacyLoadOriginalVideo, MetadataKey.viewerLoadOriginalVideo);
await migrator.migrateBool(StoreKey.legacyAutoPlayVideo, MetadataKey.viewerAutoPlayVideo);
await migrator.migrateBool(StoreKey.legacyTapToNavigate, MetadataKey.viewerTapToNavigate);
await migrator.complete();
}
@@ -57,8 +107,10 @@ class _StoreMigrator {
_StoreMigrator(this._db);
Future<void> migrateEnumIndex<T extends Enum>(StoreKey<int> legacyKey, MetadataKey<T> newKey, List<T> values) async {
final index = await _readLegacyStoreInt(legacyKey.id);
if (index == null) return;
final index = await readLegacyStoreInt(legacyKey.id);
if (index == null) {
return;
}
final enumValue = values.elementAtOrNull(index) ?? newKey.defaultValue;
_cache[newKey] = enumValue;
@@ -70,8 +122,10 @@ class _StoreMigrator {
MetadataKey<T> newKey,
List<T> values,
) async {
final name = await _readLegacyStoreString(legacyKey.id);
if (name == null) return;
final name = await readLegacyStoreString(legacyKey.id);
if (name == null) {
return;
}
final enumValue = values.firstWhere((e) => e.name == name, orElse: () => newKey.defaultValue);
_cache[newKey] = enumValue;
@@ -79,14 +133,26 @@ class _StoreMigrator {
}
Future<void> migrateBool(StoreKey<bool> legacyKey, MetadataKey<bool> newKey) async {
final intValue = await _readLegacyStoreInt(legacyKey.id);
if (intValue == null) return;
final intValue = await readLegacyStoreInt(legacyKey.id);
if (intValue == null) {
return;
}
final boolValue = intValue != 0;
_cache[newKey] = boolValue;
_migratedStoreIds.add(legacyKey.id);
}
Future<void> migrateInt(StoreKey<int> legacyKey, MetadataKey<int> newKey) async {
final intValue = await readLegacyStoreInt(legacyKey.id);
if (intValue == null) {
return;
}
_cache[newKey] = intValue;
_migratedStoreIds.add(legacyKey.id);
}
Future<void> complete() async {
await _db.batch((batch) {
for (final entry in _cache.entries) {
@@ -97,21 +163,23 @@ class _StoreMigrator {
);
}
});
await _deleteLegacyStoreRows(_migratedStoreIds);
await deleteLegacyStoreRows(_migratedStoreIds);
}
Future<String?> _readLegacyStoreString(int id) async {
Future<String?> readLegacyStoreString(int id) async {
final row = await (_db.storeEntity.select()..where((t) => t.id.equals(id))).getSingleOrNull();
return row?.stringValue;
}
Future<int?> _readLegacyStoreInt(int id) async {
Future<int?> readLegacyStoreInt(int id) async {
final row = await (_db.storeEntity.select()..where((t) => t.id.equals(id))).getSingleOrNull();
return row?.intValue;
}
Future<void> _deleteLegacyStoreRows(List<int> ids) async {
if (ids.isEmpty) return;
Future<void> deleteLegacyStoreRows(List<int> ids) async {
if (ids.isEmpty) {
return;
}
await (_db.storeEntity.delete()..where((t) => t.id.isIn(ids))).go();
}
}
+3 -1
View File
@@ -63,7 +63,9 @@ class SemVer {
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other is SemVer && other.major == major && other.minor == minor && other.patch == patch;
}
+9 -3
View File
@@ -42,13 +42,17 @@ String? getServerUrl() {
///
String punycodeEncodeUrl(String serverUrl) {
final serverUri = Uri.tryParse(serverUrl);
if (serverUri == null || serverUri.host.isEmpty) return '';
if (serverUri == null || serverUri.host.isEmpty) {
return '';
}
final encodedHost = Uri.decodeComponent(serverUri.host)
.split('.')
.map((segment) {
// If segment is already ASCII, then return as it is.
if (segment.runes.every((c) => c < 0x80)) return segment;
if (segment.runes.every((c) => c < 0x80)) {
return segment;
}
return 'xn--${punycodeEncode(segment)}';
})
.join('.');
@@ -75,7 +79,9 @@ String punycodeEncodeUrl(String serverUrl) {
///
String? punycodeDecodeUrl(String? serverUrl) {
final serverUri = serverUrl != null ? Uri.tryParse(serverUrl) : null;
if (serverUri == null || serverUri.host.isEmpty) return null;
if (serverUri == null || serverUri.host.isEmpty) {
return null;
}
final decodedHost = serverUri.host
.split('.')