mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
Merge branch 'main' into feat/custom-date-range
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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]}';
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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('.')
|
||||
|
||||
Reference in New Issue
Block a user