mirror of
https://github.com/immich-app/immich.git
synced 2026-06-16 11:52:16 -07:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f15a595759 | |||
| c5110c02fd | |||
| 0fb18ed241 | |||
| c0b3b08ce6 | |||
| e8a1084e5b |
@@ -1707,7 +1707,6 @@
|
||||
"not_available": "N/A",
|
||||
"not_in_any_album": "Not in any album",
|
||||
"not_selected": "Not selected",
|
||||
"not_set": "Not set",
|
||||
"notes": "Notes",
|
||||
"nothing_here_yet": "Nothing here yet",
|
||||
"notification_backup_reliability": "Enable notifications to improve background backup reliability",
|
||||
|
||||
@@ -82,9 +82,6 @@ url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/353224133"
|
||||
version = "7.1.3-6"
|
||||
backend = "github:jellyfin/jellyfin-ffmpeg"
|
||||
|
||||
[tools."github:jellyfin/jellyfin-ffmpeg".options]
|
||||
asset_pattern = "jellyfin-ffmpeg_*_portable_linuxarm64-gpl.tar.xz"
|
||||
|
||||
[[tools."github:webassembly/binaryen"]]
|
||||
version = "version_124"
|
||||
backend = "github:webassembly/binaryen"
|
||||
|
||||
-3603
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,8 @@ enum ActionSource { timeline, viewer }
|
||||
|
||||
enum ShareAssetType { original, preview }
|
||||
|
||||
enum ButtonPosition { bottomBar, kebabMenu }
|
||||
|
||||
enum CleanupStep { selectDate, scan, delete }
|
||||
|
||||
enum AssetKeepType { none, photosOnly, videosOnly }
|
||||
|
||||
@@ -101,7 +101,7 @@ class AppConfig {
|
||||
String toString() =>
|
||||
'AppConfig(logLevel: $logLevel, theme: $theme, cleanup: $cleanup, map: $map, timeline: $timeline, image: $image, viewer: $viewer, slideshow: $slideshow, album: $album, backup: $backup, network: $network, share: $share)';
|
||||
|
||||
T read<T>(SettingsKey<T> key) =>
|
||||
T read<T extends Object>(SettingsKey<T> key) =>
|
||||
(switch (key) {
|
||||
.logLevel => logLevel,
|
||||
.themePrimaryColor => theme.primaryColor,
|
||||
@@ -136,8 +136,6 @@ class AppConfig {
|
||||
.mapIncludeArchived => map.includeArchived,
|
||||
.mapThemeMode => map.themeMode,
|
||||
.mapWithPartners => map.withPartners,
|
||||
.mapCustomFrom => map.customFrom,
|
||||
.mapCustomTo => map.customTo,
|
||||
.cleanupKeepFavorites => cleanup.keepFavorites,
|
||||
.cleanupKeepMediaType => cleanup.keepMediaType,
|
||||
.cleanupKeepAlbumIds => cleanup.keepAlbumIds,
|
||||
@@ -152,10 +150,10 @@ class AppConfig {
|
||||
})
|
||||
as T;
|
||||
|
||||
factory AppConfig.fromEntries(Map<SettingsKey, Object?> overrides) =>
|
||||
factory AppConfig.fromEntries(Map<SettingsKey<Object>, Object> overrides) =>
|
||||
overrides.entries.fold(const AppConfig(), (config, entry) => config.write(entry.key, entry.value));
|
||||
|
||||
AppConfig write<T, U extends T>(SettingsKey<T> key, U value) {
|
||||
AppConfig write<T extends Object>(SettingsKey<T> key, T value) {
|
||||
return switch (key) {
|
||||
.logLevel => copyWith(logLevel: value as LogLevel),
|
||||
.themePrimaryColor => copyWith(theme: theme.copyWith(primaryColor: value as ImmichColorPreset)),
|
||||
@@ -169,10 +167,8 @@ class AppConfig {
|
||||
.viewerAutoPlayVideo => copyWith(viewer: viewer.copyWith(autoPlayVideo: value as bool)),
|
||||
.viewerTapToNavigate => copyWith(viewer: viewer.copyWith(tapToNavigate: value as bool)),
|
||||
.networkAutoEndpointSwitching => copyWith(network: network.copyWith(autoEndpointSwitching: value as bool)),
|
||||
.networkPreferredWifiName => copyWith(
|
||||
network: network.copyWith(preferredWifiName: .fromNullable((value as String?))),
|
||||
),
|
||||
.networkLocalEndpoint => copyWith(network: network.copyWith(localEndpoint: .fromNullable((value as String?)))),
|
||||
.networkPreferredWifiName => copyWith(network: network.copyWith(preferredWifiName: (value as String))),
|
||||
.networkLocalEndpoint => copyWith(network: network.copyWith(localEndpoint: (value as String))),
|
||||
.networkExternalEndpointList => copyWith(network: network.copyWith(externalEndpointList: value as List<String>)),
|
||||
.networkCustomHeaders => copyWith(network: network.copyWith(customHeaders: value as Map<String, String>)),
|
||||
.albumSortMode => copyWith(album: album.copyWith(sortMode: value as AlbumSortMode)),
|
||||
@@ -192,8 +188,6 @@ class AppConfig {
|
||||
.mapIncludeArchived => copyWith(map: map.copyWith(includeArchived: value as bool)),
|
||||
.mapThemeMode => copyWith(map: map.copyWith(themeMode: value as ThemeMode)),
|
||||
.mapWithPartners => copyWith(map: map.copyWith(withPartners: value as bool)),
|
||||
.mapCustomFrom => copyWith(map: map.copyWith(customFrom: .fromNullable(value as DateTime?))),
|
||||
.mapCustomTo => copyWith(map: map.copyWith(customTo: .fromNullable(value as DateTime?))),
|
||||
.cleanupKeepFavorites => copyWith(cleanup: cleanup.copyWith(keepFavorites: value as bool)),
|
||||
.cleanupKeepMediaType => copyWith(cleanup: cleanup.copyWith(keepMediaType: value as AssetKeepType)),
|
||||
.cleanupKeepAlbumIds => copyWith(cleanup: cleanup.copyWith(keepAlbumIds: value as List<String>)),
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/utils/option.dart';
|
||||
|
||||
class MapConfig {
|
||||
final int relativeDays;
|
||||
@@ -7,8 +6,6 @@ class MapConfig {
|
||||
final bool includeArchived;
|
||||
final ThemeMode themeMode;
|
||||
final bool withPartners;
|
||||
final DateTime? customFrom;
|
||||
final DateTime? customTo;
|
||||
|
||||
const MapConfig({
|
||||
this.relativeDays = 0,
|
||||
@@ -16,8 +13,6 @@ class MapConfig {
|
||||
this.includeArchived = false,
|
||||
this.themeMode = ThemeMode.system,
|
||||
this.withPartners = false,
|
||||
this.customFrom,
|
||||
this.customTo,
|
||||
});
|
||||
|
||||
MapConfig copyWith({
|
||||
@@ -26,16 +21,12 @@ class MapConfig {
|
||||
bool? includeArchived,
|
||||
ThemeMode? themeMode,
|
||||
bool? withPartners,
|
||||
Option<DateTime>? customFrom,
|
||||
Option<DateTime>? customTo,
|
||||
}) => MapConfig(
|
||||
relativeDays: relativeDays ?? this.relativeDays,
|
||||
favoritesOnly: favoritesOnly ?? this.favoritesOnly,
|
||||
includeArchived: includeArchived ?? this.includeArchived,
|
||||
themeMode: themeMode ?? this.themeMode,
|
||||
withPartners: withPartners ?? this.withPartners,
|
||||
customFrom: customFrom.patch(this.customFrom),
|
||||
customTo: customTo.patch(this.customTo),
|
||||
);
|
||||
|
||||
@override
|
||||
@@ -46,15 +37,12 @@ class MapConfig {
|
||||
other.favoritesOnly == favoritesOnly &&
|
||||
other.includeArchived == includeArchived &&
|
||||
other.themeMode == themeMode &&
|
||||
other.withPartners == withPartners &&
|
||||
other.customFrom == customFrom &&
|
||||
other.customTo == customTo);
|
||||
other.withPartners == withPartners);
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
Object.hash(relativeDays, favoritesOnly, includeArchived, themeMode, withPartners, customFrom, customTo);
|
||||
int get hashCode => Object.hash(relativeDays, favoritesOnly, includeArchived, themeMode, withPartners);
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'MapConfig(relativeDays: $relativeDays, favoritesOnly: $favoritesOnly, includeArchived: $includeArchived, themeMode: $themeMode, withPartners: $withPartners, customFrom: $customFrom, customTo: $customTo)';
|
||||
'MapConfig(relativeDays: $relativeDays, favoritesOnly: $favoritesOnly, includeArchived: $includeArchived, themeMode: $themeMode, withPartners: $withPartners)';
|
||||
}
|
||||
|
||||
@@ -1,31 +1,30 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:immich_mobile/utils/option.dart';
|
||||
|
||||
class NetworkConfig {
|
||||
final bool autoEndpointSwitching;
|
||||
final String? preferredWifiName;
|
||||
final String? localEndpoint;
|
||||
final String preferredWifiName;
|
||||
final String localEndpoint;
|
||||
final List<String> externalEndpointList;
|
||||
final Map<String, String> customHeaders;
|
||||
|
||||
const NetworkConfig({
|
||||
this.autoEndpointSwitching = false,
|
||||
this.preferredWifiName,
|
||||
this.localEndpoint,
|
||||
this.preferredWifiName = '',
|
||||
this.localEndpoint = '',
|
||||
this.externalEndpointList = const [],
|
||||
this.customHeaders = const {},
|
||||
});
|
||||
|
||||
NetworkConfig copyWith({
|
||||
bool? autoEndpointSwitching,
|
||||
Option<String>? preferredWifiName,
|
||||
Option<String>? localEndpoint,
|
||||
String? preferredWifiName,
|
||||
String? localEndpoint,
|
||||
List<String>? externalEndpointList,
|
||||
Map<String, String>? customHeaders,
|
||||
}) => NetworkConfig(
|
||||
autoEndpointSwitching: autoEndpointSwitching ?? this.autoEndpointSwitching,
|
||||
preferredWifiName: preferredWifiName.patch(this.preferredWifiName),
|
||||
localEndpoint: localEndpoint.patch(this.localEndpoint),
|
||||
preferredWifiName: preferredWifiName ?? this.preferredWifiName,
|
||||
localEndpoint: localEndpoint ?? this.localEndpoint,
|
||||
externalEndpointList: externalEndpointList ?? this.externalEndpointList,
|
||||
customHeaders: customHeaders ?? this.customHeaders,
|
||||
);
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import 'dart:convert';
|
||||
|
||||
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/timeline.model.dart';
|
||||
import 'package:immich_mobile/domain/models/value_codec.dart';
|
||||
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
||||
|
||||
enum SettingsKey<T> {
|
||||
enum SettingsKey<T extends Object> {
|
||||
// Theme
|
||||
themePrimaryColor<ImmichColorPreset>(codec: EnumCodec(ImmichColorPreset.values)),
|
||||
themeMode<ThemeMode>(codec: EnumCodec(ThemeMode.values)),
|
||||
themePrimaryColor<ImmichColorPreset>(codec: _EnumCodec(ImmichColorPreset.values)),
|
||||
themeMode<ThemeMode>(codec: _EnumCodec(ThemeMode.values)),
|
||||
themeDynamic<bool>(),
|
||||
themeColorfulInterface<bool>(),
|
||||
|
||||
@@ -25,13 +26,13 @@ enum SettingsKey<T> {
|
||||
|
||||
// Network
|
||||
networkAutoEndpointSwitching<bool>(),
|
||||
networkExternalEndpointList<List<String>>(codec: ListCodec(PrimitiveCodec.string)),
|
||||
networkCustomHeaders<Map<String, String>>(codec: MapCodec(PrimitiveCodec.string, PrimitiveCodec.string)),
|
||||
networkPreferredWifiName<String?>(),
|
||||
networkLocalEndpoint<String?>(),
|
||||
networkPreferredWifiName<String>(),
|
||||
networkLocalEndpoint<String>(),
|
||||
networkExternalEndpointList<List<String>>(codec: _ListCodec(_PrimitiveCodec.string)),
|
||||
networkCustomHeaders<Map<String, String>>(codec: _MapCodec(_PrimitiveCodec.string, _PrimitiveCodec.string)),
|
||||
|
||||
// Album
|
||||
albumSortMode<AlbumSortMode>(codec: EnumCodec(AlbumSortMode.values)),
|
||||
albumSortMode<AlbumSortMode>(codec: _EnumCodec(AlbumSortMode.values)),
|
||||
albumIsReverse<bool>(),
|
||||
albumIsGrid<bool>(),
|
||||
|
||||
@@ -45,45 +46,175 @@ enum SettingsKey<T> {
|
||||
|
||||
// Timeline
|
||||
timelineTilesPerRow<int>(),
|
||||
timelineGroupAssetsBy<GroupAssetsBy>(codec: EnumCodec(GroupAssetsBy.values)),
|
||||
timelineGroupAssetsBy<GroupAssetsBy>(codec: _EnumCodec(GroupAssetsBy.values)),
|
||||
timelineStorageIndicator<bool>(),
|
||||
|
||||
// Log
|
||||
logLevel<LogLevel>(codec: EnumCodec(LogLevel.values)),
|
||||
logLevel<LogLevel>(codec: _EnumCodec(LogLevel.values)),
|
||||
|
||||
// Map
|
||||
mapShowFavoriteOnly<bool>(),
|
||||
mapRelativeDate<int>(),
|
||||
mapIncludeArchived<bool>(),
|
||||
mapThemeMode<ThemeMode>(codec: EnumCodec(ThemeMode.values)),
|
||||
mapThemeMode<ThemeMode>(codec: _EnumCodec(ThemeMode.values)),
|
||||
mapWithPartners<bool>(),
|
||||
mapCustomFrom<DateTime?>(),
|
||||
mapCustomTo<DateTime?>(),
|
||||
|
||||
// Cleanup
|
||||
cleanupKeepFavorites<bool>(),
|
||||
cleanupKeepMediaType<AssetKeepType>(codec: EnumCodec(AssetKeepType.values)),
|
||||
cleanupKeepAlbumIds<List<String>>(codec: ListCodec(PrimitiveCodec.string)),
|
||||
cleanupKeepMediaType<AssetKeepType>(codec: _EnumCodec(AssetKeepType.values)),
|
||||
cleanupKeepAlbumIds<List<String>>(codec: _ListCodec(_PrimitiveCodec.string)),
|
||||
cleanupCutoffDaysAgo<int>(),
|
||||
cleanupDefaultsInitialized<bool>(),
|
||||
|
||||
// Share
|
||||
shareFileType<ShareAssetType>(codec: EnumCodec(ShareAssetType.values)),
|
||||
shareFileType<ShareAssetType>(codec: _EnumCodec(ShareAssetType.values)),
|
||||
|
||||
// Slideshow
|
||||
slideshowTransition<bool>(),
|
||||
slideshowRepeat<bool>(),
|
||||
slideshowDuration<int>(),
|
||||
slideshowLook<SlideshowLook>(codec: EnumCodec(SlideshowLook.values)),
|
||||
slideshowDirection<SlideshowDirection>(codec: EnumCodec(SlideshowDirection.values));
|
||||
slideshowLook<SlideshowLook>(codec: _EnumCodec(SlideshowLook.values)),
|
||||
slideshowDirection<SlideshowDirection>(codec: _EnumCodec(SlideshowDirection.values));
|
||||
|
||||
final ValueCodec<T>? _codecOverride;
|
||||
final _SettingsCodec<T>? _codecOverride;
|
||||
|
||||
const SettingsKey({ValueCodec<T>? codec}) : _codecOverride = codec;
|
||||
const SettingsKey({_SettingsCodec<T>? codec}) : _codecOverride = codec;
|
||||
|
||||
ValueCodec<T> get _codec => _codecOverride ?? ValueCodec.forType(T);
|
||||
_SettingsCodec<T> get _codec => _codecOverride ?? _SettingsCodec.forType(T);
|
||||
|
||||
String encode(T value) => _codec.encode(value);
|
||||
|
||||
T decode(String raw) => _codec.decode(raw);
|
||||
}
|
||||
|
||||
sealed class _SettingsCodec<T extends Object> {
|
||||
const _SettingsCodec();
|
||||
|
||||
String encode(T value);
|
||||
T decode(String raw);
|
||||
|
||||
static const Map<Type, _SettingsCodec<Object>> _primitives = {
|
||||
int: _PrimitiveCodec.integer,
|
||||
double: _PrimitiveCodec.real,
|
||||
bool: _PrimitiveCodec.boolean,
|
||||
String: _PrimitiveCodec.string,
|
||||
DateTime: _DateTimeCodec(),
|
||||
};
|
||||
|
||||
static _SettingsCodec<T> forType<T extends Object>(Type runtimeType) {
|
||||
final codec = _primitives[runtimeType];
|
||||
if (codec == null) {
|
||||
throw StateError('No primitive codec for $runtimeType. Provide an explicit codec when defining the SettingsKey.');
|
||||
}
|
||||
return codec as _SettingsCodec<T>;
|
||||
}
|
||||
}
|
||||
|
||||
final class _EnumCodec<T extends Enum> extends _SettingsCodec<T> {
|
||||
final List<T> values;
|
||||
|
||||
const _EnumCodec(this.values);
|
||||
|
||||
@override
|
||||
String encode(T value) => value.name;
|
||||
|
||||
@override
|
||||
T decode(String raw) => values.firstWhere((v) => v.name == raw);
|
||||
}
|
||||
|
||||
final class _DateTimeCodec extends _SettingsCodec<DateTime> {
|
||||
const _DateTimeCodec();
|
||||
|
||||
@override
|
||||
String encode(DateTime value) => value.toIso8601String();
|
||||
|
||||
@override
|
||||
DateTime decode(String raw) => DateTime.parse(raw);
|
||||
}
|
||||
|
||||
final class _MapCodec<K extends Object, V extends Object> extends _SettingsCodec<Map<K, V>> {
|
||||
final _SettingsCodec<K> _keyCodec;
|
||||
final _SettingsCodec<V> _valueCodec;
|
||||
|
||||
const _MapCodec(this._keyCodec, this._valueCodec);
|
||||
|
||||
@override
|
||||
String encode(Map<K, V> value) {
|
||||
final entries = <String, String>{};
|
||||
value.forEach((k, v) => entries[_keyCodec.encode(k)] = _valueCodec.encode(v));
|
||||
return jsonEncode(entries);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<K, V> decode(String raw) {
|
||||
try {
|
||||
final decoded = jsonDecode(raw);
|
||||
if (decoded is! Map) {
|
||||
return {};
|
||||
}
|
||||
final result = <K, V>{};
|
||||
for (final entry in decoded.entries) {
|
||||
final rawKey = entry.key;
|
||||
final rawValue = entry.value;
|
||||
if (rawKey is! String || rawValue is! String) {
|
||||
return {};
|
||||
}
|
||||
final k = _keyCodec.decode(rawKey);
|
||||
final v = _valueCodec.decode(rawValue);
|
||||
result[k] = v;
|
||||
}
|
||||
return result;
|
||||
} on FormatException {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class _ListCodec<T extends Object> extends _SettingsCodec<List<T>> {
|
||||
final _SettingsCodec<T> _elementCodec;
|
||||
|
||||
const _ListCodec(this._elementCodec);
|
||||
|
||||
@override
|
||||
String encode(List<T> value) => jsonEncode(value.map(_elementCodec.encode).toList());
|
||||
|
||||
@override
|
||||
List<T> decode(String raw) {
|
||||
try {
|
||||
final decoded = jsonDecode(raw);
|
||||
if (decoded is! List) {
|
||||
return [];
|
||||
}
|
||||
final result = <T>[];
|
||||
for (final item in decoded) {
|
||||
if (item is! String) {
|
||||
return [];
|
||||
}
|
||||
final element = _elementCodec.decode(item);
|
||||
result.add(element);
|
||||
}
|
||||
return result;
|
||||
} on FormatException {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class _PrimitiveCodec<T extends Object> extends _SettingsCodec<T> {
|
||||
final T Function(String) _parse;
|
||||
|
||||
const _PrimitiveCodec._(this._parse);
|
||||
|
||||
@override
|
||||
String encode(T value) => value.toString();
|
||||
|
||||
@override
|
||||
T decode(String raw) => _parse(raw);
|
||||
|
||||
static const integer = _PrimitiveCodec<int>._(int.parse);
|
||||
static const real = _PrimitiveCodec<double>._(double.parse);
|
||||
static const boolean = _PrimitiveCodec<bool>._(bool.parse);
|
||||
static const string = _PrimitiveCodec<String>._(_identity);
|
||||
|
||||
static String _identity(String s) => s;
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import 'package:immich_mobile/utils/option.dart';
|
||||
|
||||
class TimeRange {
|
||||
final DateTime? from;
|
||||
final DateTime? to;
|
||||
|
||||
const TimeRange({this.from, this.to});
|
||||
|
||||
TimeRange copyWith({Option<DateTime>? from, Option<DateTime>? to}) {
|
||||
return TimeRange(from: from.patch(this.from), to: to.patch(this.to));
|
||||
}
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
import 'dart:convert';
|
||||
|
||||
sealed class ValueCodec<T> {
|
||||
const ValueCodec();
|
||||
|
||||
String encode(T value);
|
||||
T decode(String raw);
|
||||
|
||||
static final Map<Type, ValueCodec<Object>> _primitives = {
|
||||
..._register<int>(PrimitiveCodec.integer),
|
||||
..._register<double>(PrimitiveCodec.real),
|
||||
..._register<bool>(PrimitiveCodec.boolean),
|
||||
..._register<String>(PrimitiveCodec.string),
|
||||
..._register<DateTime>(const DateTimeCodec()),
|
||||
};
|
||||
|
||||
static Map<Type, ValueCodec<Object>> _register<T>(ValueCodec<Object> codec) => {
|
||||
T: codec,
|
||||
// Reifies the nullable type T so it can be used as a key in the _primitives map
|
||||
_typeOf<T?>(): codec,
|
||||
};
|
||||
|
||||
static Type _typeOf<T>() => T;
|
||||
|
||||
static ValueCodec<T> forType<T>(Type runtimeType) {
|
||||
final codec = _primitives[runtimeType];
|
||||
if (codec == null) {
|
||||
throw StateError('No primitive codec for $runtimeType. Provide an explicit codec when defining the key.');
|
||||
}
|
||||
return codec as ValueCodec<T>;
|
||||
}
|
||||
}
|
||||
|
||||
final class EnumCodec<T extends Enum> extends ValueCodec<T> {
|
||||
final List<T> values;
|
||||
|
||||
const EnumCodec(this.values);
|
||||
|
||||
@override
|
||||
String encode(T value) => value.name;
|
||||
|
||||
@override
|
||||
T decode(String raw) => values.firstWhere((v) => v.name == raw);
|
||||
}
|
||||
|
||||
final class DateTimeCodec extends ValueCodec<DateTime> {
|
||||
const DateTimeCodec();
|
||||
|
||||
@override
|
||||
String encode(DateTime value) => value.toIso8601String();
|
||||
|
||||
@override
|
||||
DateTime decode(String raw) => DateTime.parse(raw);
|
||||
}
|
||||
|
||||
final class MapCodec<K extends Object, V extends Object> extends ValueCodec<Map<K, V>> {
|
||||
final ValueCodec<K> _keyCodec;
|
||||
final ValueCodec<V> _valueCodec;
|
||||
|
||||
const MapCodec(this._keyCodec, this._valueCodec);
|
||||
|
||||
@override
|
||||
String encode(Map<K, V> value) {
|
||||
final entries = <String, String>{};
|
||||
value.forEach((k, v) => entries[_keyCodec.encode(k)] = _valueCodec.encode(v));
|
||||
return jsonEncode(entries);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<K, V> decode(String raw) {
|
||||
try {
|
||||
final decoded = jsonDecode(raw);
|
||||
if (decoded is! Map) {
|
||||
return {};
|
||||
}
|
||||
final result = <K, V>{};
|
||||
for (final entry in decoded.entries) {
|
||||
final rawKey = entry.key;
|
||||
final rawValue = entry.value;
|
||||
if (rawKey is! String || rawValue is! String) {
|
||||
continue;
|
||||
}
|
||||
final k = _keyCodec.decode(rawKey);
|
||||
final v = _valueCodec.decode(rawValue);
|
||||
result[k] = v;
|
||||
}
|
||||
return result;
|
||||
} on FormatException {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class ListCodec<T extends Object> extends ValueCodec<List<T>> {
|
||||
final ValueCodec<T> _elementCodec;
|
||||
|
||||
const ListCodec(this._elementCodec);
|
||||
|
||||
@override
|
||||
String encode(List<T> value) => jsonEncode(value.map(_elementCodec.encode).toList());
|
||||
|
||||
@override
|
||||
List<T> decode(String raw) {
|
||||
try {
|
||||
final decoded = jsonDecode(raw);
|
||||
if (decoded is! List) {
|
||||
return [];
|
||||
}
|
||||
final result = <T>[];
|
||||
for (final item in decoded) {
|
||||
if (item is! String) {
|
||||
return [];
|
||||
}
|
||||
final element = _elementCodec.decode(item);
|
||||
result.add(element);
|
||||
}
|
||||
return result;
|
||||
} on FormatException {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class PrimitiveCodec<T extends Object> extends ValueCodec<T> {
|
||||
final T Function(String) _parse;
|
||||
|
||||
const PrimitiveCodec._(this._parse);
|
||||
|
||||
@override
|
||||
String encode(T value) => value.toString();
|
||||
|
||||
@override
|
||||
T decode(String raw) => _parse(raw);
|
||||
|
||||
static const integer = PrimitiveCodec<int>._(int.parse);
|
||||
static const real = PrimitiveCodec<double>._(double.parse);
|
||||
static const boolean = PrimitiveCodec<bool>._(bool.parse);
|
||||
static const string = PrimitiveCodec<String>._(_identity);
|
||||
|
||||
static String _identity(String s) => s;
|
||||
}
|
||||
@@ -6,7 +6,7 @@ class SettingsEntity extends Table with DriftDefaultsMixin {
|
||||
|
||||
TextColumn get key => text()();
|
||||
|
||||
TextColumn get value => text().nullable()();
|
||||
TextColumn get value => text()();
|
||||
|
||||
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
|
||||
|
||||
|
||||
+21
-20
@@ -10,13 +10,13 @@ import 'package:drift/src/runtime/query_builder/query_builder.dart' as i3;
|
||||
typedef $$SettingsEntityTableCreateCompanionBuilder =
|
||||
i1.SettingsEntityCompanion Function({
|
||||
required String key,
|
||||
i0.Value<String?> value,
|
||||
required String value,
|
||||
i0.Value<DateTime> updatedAt,
|
||||
});
|
||||
typedef $$SettingsEntityTableUpdateCompanionBuilder =
|
||||
i1.SettingsEntityCompanion Function({
|
||||
i0.Value<String> key,
|
||||
i0.Value<String?> value,
|
||||
i0.Value<String> value,
|
||||
i0.Value<DateTime> updatedAt,
|
||||
});
|
||||
|
||||
@@ -127,7 +127,7 @@ class $$SettingsEntityTableTableManager
|
||||
updateCompanionCallback:
|
||||
({
|
||||
i0.Value<String> key = const i0.Value.absent(),
|
||||
i0.Value<String?> value = const i0.Value.absent(),
|
||||
i0.Value<String> value = const i0.Value.absent(),
|
||||
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
|
||||
}) => i1.SettingsEntityCompanion(
|
||||
key: key,
|
||||
@@ -137,7 +137,7 @@ class $$SettingsEntityTableTableManager
|
||||
createCompanionCallback:
|
||||
({
|
||||
required String key,
|
||||
i0.Value<String?> value = const i0.Value.absent(),
|
||||
required String value,
|
||||
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
|
||||
}) => i1.SettingsEntityCompanion.insert(
|
||||
key: key,
|
||||
@@ -196,9 +196,9 @@ class $SettingsEntityTable extends i2.SettingsEntity
|
||||
late final i0.GeneratedColumn<String> value = i0.GeneratedColumn<String>(
|
||||
'value',
|
||||
aliasedName,
|
||||
true,
|
||||
false,
|
||||
type: i0.DriftSqlType.string,
|
||||
requiredDuringInsert: false,
|
||||
requiredDuringInsert: true,
|
||||
);
|
||||
static const i0.VerificationMeta _updatedAtMeta = const i0.VerificationMeta(
|
||||
'updatedAt',
|
||||
@@ -240,6 +240,8 @@ class $SettingsEntityTable extends i2.SettingsEntity
|
||||
_valueMeta,
|
||||
value.isAcceptableOrUnknown(data['value']!, _valueMeta),
|
||||
);
|
||||
} else if (isInserting) {
|
||||
context.missing(_valueMeta);
|
||||
}
|
||||
if (data.containsKey('updated_at')) {
|
||||
context.handle(
|
||||
@@ -263,7 +265,7 @@ class $SettingsEntityTable extends i2.SettingsEntity
|
||||
value: attachedDatabase.typeMapping.read(
|
||||
i0.DriftSqlType.string,
|
||||
data['${effectivePrefix}value'],
|
||||
),
|
||||
)!,
|
||||
updatedAt: attachedDatabase.typeMapping.read(
|
||||
i0.DriftSqlType.dateTime,
|
||||
data['${effectivePrefix}updated_at'],
|
||||
@@ -285,20 +287,18 @@ class $SettingsEntityTable extends i2.SettingsEntity
|
||||
class SettingsEntityData extends i0.DataClass
|
||||
implements i0.Insertable<i1.SettingsEntityData> {
|
||||
final String key;
|
||||
final String? value;
|
||||
final String value;
|
||||
final DateTime updatedAt;
|
||||
const SettingsEntityData({
|
||||
required this.key,
|
||||
this.value,
|
||||
required this.value,
|
||||
required this.updatedAt,
|
||||
});
|
||||
@override
|
||||
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, i0.Expression>{};
|
||||
map['key'] = i0.Variable<String>(key);
|
||||
if (!nullToAbsent || value != null) {
|
||||
map['value'] = i0.Variable<String>(value);
|
||||
}
|
||||
map['value'] = i0.Variable<String>(value);
|
||||
map['updated_at'] = i0.Variable<DateTime>(updatedAt);
|
||||
return map;
|
||||
}
|
||||
@@ -310,7 +310,7 @@ class SettingsEntityData extends i0.DataClass
|
||||
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
|
||||
return SettingsEntityData(
|
||||
key: serializer.fromJson<String>(json['key']),
|
||||
value: serializer.fromJson<String?>(json['value']),
|
||||
value: serializer.fromJson<String>(json['value']),
|
||||
updatedAt: serializer.fromJson<DateTime>(json['updatedAt']),
|
||||
);
|
||||
}
|
||||
@@ -319,18 +319,18 @@ class SettingsEntityData extends i0.DataClass
|
||||
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
|
||||
return <String, dynamic>{
|
||||
'key': serializer.toJson<String>(key),
|
||||
'value': serializer.toJson<String?>(value),
|
||||
'value': serializer.toJson<String>(value),
|
||||
'updatedAt': serializer.toJson<DateTime>(updatedAt),
|
||||
};
|
||||
}
|
||||
|
||||
i1.SettingsEntityData copyWith({
|
||||
String? key,
|
||||
i0.Value<String?> value = const i0.Value.absent(),
|
||||
String? value,
|
||||
DateTime? updatedAt,
|
||||
}) => i1.SettingsEntityData(
|
||||
key: key ?? this.key,
|
||||
value: value.present ? value.value : this.value,
|
||||
value: value ?? this.value,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
);
|
||||
SettingsEntityData copyWithCompanion(i1.SettingsEntityCompanion data) {
|
||||
@@ -365,7 +365,7 @@ class SettingsEntityData extends i0.DataClass
|
||||
class SettingsEntityCompanion
|
||||
extends i0.UpdateCompanion<i1.SettingsEntityData> {
|
||||
final i0.Value<String> key;
|
||||
final i0.Value<String?> value;
|
||||
final i0.Value<String> value;
|
||||
final i0.Value<DateTime> updatedAt;
|
||||
const SettingsEntityCompanion({
|
||||
this.key = const i0.Value.absent(),
|
||||
@@ -374,9 +374,10 @@ class SettingsEntityCompanion
|
||||
});
|
||||
SettingsEntityCompanion.insert({
|
||||
required String key,
|
||||
this.value = const i0.Value.absent(),
|
||||
required String value,
|
||||
this.updatedAt = const i0.Value.absent(),
|
||||
}) : key = i0.Value(key);
|
||||
}) : key = i0.Value(key),
|
||||
value = i0.Value(value);
|
||||
static i0.Insertable<i1.SettingsEntityData> custom({
|
||||
i0.Expression<String>? key,
|
||||
i0.Expression<String>? value,
|
||||
@@ -391,7 +392,7 @@ class SettingsEntityCompanion
|
||||
|
||||
i1.SettingsEntityCompanion copyWith({
|
||||
i0.Value<String>? key,
|
||||
i0.Value<String?>? value,
|
||||
i0.Value<String>? value,
|
||||
i0.Value<DateTime>? updatedAt,
|
||||
}) {
|
||||
return i1.SettingsEntityCompanion(
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
// ignore: depend_on_referenced_packages
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
abstract class CachedKeyValueRepository<K extends Enum, S> {
|
||||
CachedKeyValueRepository(this._snapshot);
|
||||
|
||||
S _snapshot;
|
||||
S get snapshot => _snapshot;
|
||||
@protected
|
||||
set snapshot(S value) => _snapshot = value;
|
||||
|
||||
List<K> get keys;
|
||||
|
||||
Object decodeValue(K key, String raw);
|
||||
|
||||
S buildSnapshot(Map<K, Object?> overrides);
|
||||
|
||||
Selectable<({String key, String? value})> selectable();
|
||||
|
||||
Future<void> refresh() async => _snapshot = _build(await selectable().get());
|
||||
|
||||
Stream<S> watchSnapshot() => selectable().watch().map((rows) => _snapshot = _build(rows));
|
||||
|
||||
S _build(List<({String key, String? value})> rows) => buildSnapshot(
|
||||
rows.fold({}, (overrides, row) {
|
||||
final key = keys.firstWhereOrNull((key) => key.name == row.key);
|
||||
if (key == null) {
|
||||
return overrides;
|
||||
}
|
||||
|
||||
Object? decodedValue;
|
||||
if (row.value != null) {
|
||||
decodedValue = decodeValue(key, row.value!);
|
||||
}
|
||||
|
||||
return {...overrides, key: decodedValue};
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -120,7 +120,7 @@ class Drift extends $Drift {
|
||||
}
|
||||
|
||||
@override
|
||||
int get schemaVersion => 30;
|
||||
int get schemaVersion => 29;
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration => MigrationStrategy(
|
||||
@@ -308,9 +308,6 @@ class Drift extends $Drift {
|
||||
await m.createTable(v29.assetOcrEntity);
|
||||
await m.createIndex(v29.idxAssetOcrAssetId);
|
||||
},
|
||||
from29To30: (m, v30) async {
|
||||
await m.alterTable(TableMigration(v30.settings));
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -15331,595 +15331,6 @@ i1.GeneratedColumn<String> _column_223(String aliasedName) =>
|
||||
type: i1.DriftSqlType.string,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
|
||||
final class Schema30 extends i0.VersionedSchema {
|
||||
Schema30({required super.database}) : super(version: 30);
|
||||
@override
|
||||
late final List<i1.DatabaseSchemaEntity> entities = [
|
||||
userEntity,
|
||||
remoteAssetEntity,
|
||||
stackEntity,
|
||||
localAssetEntity,
|
||||
remoteAlbumEntity,
|
||||
localAlbumEntity,
|
||||
localAlbumAssetEntity,
|
||||
idxLocalAlbumAssetAlbumAsset,
|
||||
idxLocalAssetChecksum,
|
||||
idxLocalAssetCloudId,
|
||||
idxLocalAssetCreatedAt,
|
||||
idxStackPrimaryAssetId,
|
||||
uQRemoteAssetsOwnerChecksum,
|
||||
uQRemoteAssetsOwnerLibraryChecksum,
|
||||
idxRemoteAssetChecksum,
|
||||
idxRemoteAssetStackId,
|
||||
idxRemoteAssetOwnerVisibilityDeletedCreated,
|
||||
authUserEntity,
|
||||
userMetadataEntity,
|
||||
partnerEntity,
|
||||
remoteExifEntity,
|
||||
remoteAlbumAssetEntity,
|
||||
remoteAlbumUserEntity,
|
||||
remoteAssetCloudIdEntity,
|
||||
memoryEntity,
|
||||
memoryAssetEntity,
|
||||
personEntity,
|
||||
assetFaceEntity,
|
||||
storeEntity,
|
||||
trashedLocalAssetEntity,
|
||||
assetEditEntity,
|
||||
settings,
|
||||
assetOcrEntity,
|
||||
idxPartnerSharedWithId,
|
||||
idxLatLng,
|
||||
idxRemoteExifCity,
|
||||
idxRemoteAlbumAssetAlbumAsset,
|
||||
idxRemoteAssetCloudId,
|
||||
idxPersonOwnerId,
|
||||
idxAssetFacePersonId,
|
||||
idxAssetFaceAssetId,
|
||||
idxAssetFaceVisiblePerson,
|
||||
idxTrashedLocalAssetChecksum,
|
||||
idxTrashedLocalAssetAlbum,
|
||||
idxAssetEditAssetId,
|
||||
idxAssetOcrAssetId,
|
||||
];
|
||||
late final Shape33 userEntity = Shape33(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'user_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_107,
|
||||
_column_108,
|
||||
_column_109,
|
||||
_column_110,
|
||||
_column_111,
|
||||
_column_112,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape50 remoteAssetEntity = Shape50(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'remote_asset_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_108,
|
||||
_column_113,
|
||||
_column_114,
|
||||
_column_115,
|
||||
_column_116,
|
||||
_column_117,
|
||||
_column_118,
|
||||
_column_107,
|
||||
_column_119,
|
||||
_column_120,
|
||||
_column_121,
|
||||
_column_122,
|
||||
_column_123,
|
||||
_column_124,
|
||||
_column_212,
|
||||
_column_125,
|
||||
_column_126,
|
||||
_column_127,
|
||||
_column_128,
|
||||
_column_129,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape35 stackEntity = Shape35(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'stack_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_107,
|
||||
_column_114,
|
||||
_column_115,
|
||||
_column_121,
|
||||
_column_130,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape36 localAssetEntity = Shape36(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'local_asset_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_108,
|
||||
_column_113,
|
||||
_column_114,
|
||||
_column_115,
|
||||
_column_116,
|
||||
_column_117,
|
||||
_column_118,
|
||||
_column_107,
|
||||
_column_131,
|
||||
_column_120,
|
||||
_column_132,
|
||||
_column_133,
|
||||
_column_134,
|
||||
_column_135,
|
||||
_column_136,
|
||||
_column_137,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape48 remoteAlbumEntity = Shape48(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'remote_album_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_107,
|
||||
_column_108,
|
||||
_column_138,
|
||||
_column_114,
|
||||
_column_115,
|
||||
_column_139,
|
||||
_column_140,
|
||||
_column_141,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape38 localAlbumEntity = Shape38(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'local_album_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_107,
|
||||
_column_108,
|
||||
_column_115,
|
||||
_column_142,
|
||||
_column_143,
|
||||
_column_144,
|
||||
_column_145,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape39 localAlbumAssetEntity = Shape39(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'local_album_asset_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
|
||||
columns: [_column_146, _column_147, _column_145],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
final i1.Index idxLocalAlbumAssetAlbumAsset = i1.Index(
|
||||
'idx_local_album_asset_album_asset',
|
||||
'CREATE INDEX IF NOT EXISTS idx_local_album_asset_album_asset ON local_album_asset_entity (album_id, asset_id)',
|
||||
);
|
||||
final i1.Index idxLocalAssetChecksum = i1.Index(
|
||||
'idx_local_asset_checksum',
|
||||
'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)',
|
||||
);
|
||||
final i1.Index idxLocalAssetCloudId = i1.Index(
|
||||
'idx_local_asset_cloud_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)',
|
||||
);
|
||||
final i1.Index idxLocalAssetCreatedAt = i1.Index(
|
||||
'idx_local_asset_created_at',
|
||||
'CREATE INDEX IF NOT EXISTS idx_local_asset_created_at ON local_asset_entity (created_at)',
|
||||
);
|
||||
final i1.Index idxStackPrimaryAssetId = i1.Index(
|
||||
'idx_stack_primary_asset_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)',
|
||||
);
|
||||
final i1.Index uQRemoteAssetsOwnerChecksum = i1.Index(
|
||||
'UQ_remote_assets_owner_checksum',
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)',
|
||||
);
|
||||
final i1.Index uQRemoteAssetsOwnerLibraryChecksum = i1.Index(
|
||||
'UQ_remote_assets_owner_library_checksum',
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)',
|
||||
);
|
||||
final i1.Index idxRemoteAssetChecksum = i1.Index(
|
||||
'idx_remote_asset_checksum',
|
||||
'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)',
|
||||
);
|
||||
final i1.Index idxRemoteAssetStackId = i1.Index(
|
||||
'idx_remote_asset_stack_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)',
|
||||
);
|
||||
final i1.Index idxRemoteAssetOwnerVisibilityDeletedCreated = i1.Index(
|
||||
'idx_remote_asset_owner_visibility_deleted_created',
|
||||
'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_visibility_deleted_created ON remote_asset_entity (owner_id, visibility, deleted_at, created_at DESC)',
|
||||
);
|
||||
late final Shape40 authUserEntity = Shape40(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'auth_user_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_107,
|
||||
_column_108,
|
||||
_column_109,
|
||||
_column_148,
|
||||
_column_110,
|
||||
_column_111,
|
||||
_column_149,
|
||||
_column_150,
|
||||
_column_151,
|
||||
_column_152,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape4 userMetadataEntity = Shape4(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'user_metadata_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(user_id, "key")'],
|
||||
columns: [_column_153, _column_154, _column_155],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape41 partnerEntity = Shape41(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'partner_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(shared_by_id, shared_with_id)'],
|
||||
columns: [_column_156, _column_157, _column_158],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape42 remoteExifEntity = Shape42(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'remote_exif_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(asset_id)'],
|
||||
columns: [
|
||||
_column_159,
|
||||
_column_160,
|
||||
_column_161,
|
||||
_column_162,
|
||||
_column_163,
|
||||
_column_164,
|
||||
_column_117,
|
||||
_column_116,
|
||||
_column_165,
|
||||
_column_166,
|
||||
_column_167,
|
||||
_column_168,
|
||||
_column_135,
|
||||
_column_136,
|
||||
_column_169,
|
||||
_column_170,
|
||||
_column_171,
|
||||
_column_172,
|
||||
_column_173,
|
||||
_column_174,
|
||||
_column_175,
|
||||
_column_176,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape7 remoteAlbumAssetEntity = Shape7(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'remote_album_asset_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
|
||||
columns: [_column_159, _column_177],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape10 remoteAlbumUserEntity = Shape10(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'remote_album_user_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(album_id, user_id)'],
|
||||
columns: [_column_177, _column_153, _column_178],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape43 remoteAssetCloudIdEntity = Shape43(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'remote_asset_cloud_id_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(asset_id)'],
|
||||
columns: [
|
||||
_column_159,
|
||||
_column_179,
|
||||
_column_180,
|
||||
_column_134,
|
||||
_column_135,
|
||||
_column_136,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape44 memoryEntity = Shape44(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'memory_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_107,
|
||||
_column_114,
|
||||
_column_115,
|
||||
_column_124,
|
||||
_column_121,
|
||||
_column_113,
|
||||
_column_181,
|
||||
_column_182,
|
||||
_column_183,
|
||||
_column_184,
|
||||
_column_185,
|
||||
_column_186,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape12 memoryAssetEntity = Shape12(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'memory_asset_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(asset_id, memory_id)'],
|
||||
columns: [_column_159, _column_187],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape45 personEntity = Shape45(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'person_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_107,
|
||||
_column_114,
|
||||
_column_115,
|
||||
_column_121,
|
||||
_column_108,
|
||||
_column_188,
|
||||
_column_189,
|
||||
_column_190,
|
||||
_column_191,
|
||||
_column_192,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape46 assetFaceEntity = Shape46(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'asset_face_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_107,
|
||||
_column_159,
|
||||
_column_193,
|
||||
_column_194,
|
||||
_column_195,
|
||||
_column_196,
|
||||
_column_197,
|
||||
_column_198,
|
||||
_column_199,
|
||||
_column_200,
|
||||
_column_201,
|
||||
_column_124,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape18 storeEntity = Shape18(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'store_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [_column_202, _column_203, _column_204],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape47 trashedLocalAssetEntity = Shape47(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'trashed_local_asset_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id, album_id)'],
|
||||
columns: [
|
||||
_column_108,
|
||||
_column_113,
|
||||
_column_114,
|
||||
_column_115,
|
||||
_column_116,
|
||||
_column_117,
|
||||
_column_118,
|
||||
_column_107,
|
||||
_column_205,
|
||||
_column_131,
|
||||
_column_120,
|
||||
_column_132,
|
||||
_column_206,
|
||||
_column_137,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape32 assetEditEntity = Shape32(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'asset_edit_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_107,
|
||||
_column_159,
|
||||
_column_207,
|
||||
_column_208,
|
||||
_column_209,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape49 settings = Shape49(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'settings',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY("key")'],
|
||||
columns: [_column_210, _column_224, _column_115],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape51 assetOcrEntity = Shape51(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'asset_ocr_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_107,
|
||||
_column_159,
|
||||
_column_213,
|
||||
_column_214,
|
||||
_column_215,
|
||||
_column_216,
|
||||
_column_217,
|
||||
_column_218,
|
||||
_column_219,
|
||||
_column_220,
|
||||
_column_221,
|
||||
_column_222,
|
||||
_column_223,
|
||||
_column_201,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
final i1.Index idxPartnerSharedWithId = i1.Index(
|
||||
'idx_partner_shared_with_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)',
|
||||
);
|
||||
final i1.Index idxLatLng = i1.Index(
|
||||
'idx_lat_lng',
|
||||
'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)',
|
||||
);
|
||||
final i1.Index idxRemoteExifCity = i1.Index(
|
||||
'idx_remote_exif_city',
|
||||
'CREATE INDEX IF NOT EXISTS idx_remote_exif_city ON remote_exif_entity (city) WHERE city IS NOT NULL',
|
||||
);
|
||||
final i1.Index idxRemoteAlbumAssetAlbumAsset = i1.Index(
|
||||
'idx_remote_album_asset_album_asset',
|
||||
'CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)',
|
||||
);
|
||||
final i1.Index idxRemoteAssetCloudId = i1.Index(
|
||||
'idx_remote_asset_cloud_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)',
|
||||
);
|
||||
final i1.Index idxPersonOwnerId = i1.Index(
|
||||
'idx_person_owner_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)',
|
||||
);
|
||||
final i1.Index idxAssetFacePersonId = i1.Index(
|
||||
'idx_asset_face_person_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)',
|
||||
);
|
||||
final i1.Index idxAssetFaceAssetId = i1.Index(
|
||||
'idx_asset_face_asset_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)',
|
||||
);
|
||||
final i1.Index idxAssetFaceVisiblePerson = i1.Index(
|
||||
'idx_asset_face_visible_person',
|
||||
'CREATE INDEX IF NOT EXISTS idx_asset_face_visible_person ON asset_face_entity (person_id, asset_id) WHERE is_visible = 1 AND deleted_at IS NULL',
|
||||
);
|
||||
final i1.Index idxTrashedLocalAssetChecksum = i1.Index(
|
||||
'idx_trashed_local_asset_checksum',
|
||||
'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)',
|
||||
);
|
||||
final i1.Index idxTrashedLocalAssetAlbum = i1.Index(
|
||||
'idx_trashed_local_asset_album',
|
||||
'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)',
|
||||
);
|
||||
final i1.Index idxAssetEditAssetId = i1.Index(
|
||||
'idx_asset_edit_asset_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_asset_edit_asset_id ON asset_edit_entity (asset_id)',
|
||||
);
|
||||
final i1.Index idxAssetOcrAssetId = i1.Index(
|
||||
'idx_asset_ocr_asset_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_asset_ocr_asset_id ON asset_ocr_entity (asset_id)',
|
||||
);
|
||||
}
|
||||
|
||||
i1.GeneratedColumn<String> _column_224(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>(
|
||||
'value',
|
||||
aliasedName,
|
||||
true,
|
||||
type: i1.DriftSqlType.string,
|
||||
$customConstraints: 'NULL',
|
||||
);
|
||||
i0.MigrationStepWithVersion migrationSteps({
|
||||
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
||||
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
||||
@@ -15949,7 +15360,6 @@ i0.MigrationStepWithVersion migrationSteps({
|
||||
required Future<void> Function(i1.Migrator m, Schema27 schema) from26To27,
|
||||
required Future<void> Function(i1.Migrator m, Schema28 schema) from27To28,
|
||||
required Future<void> Function(i1.Migrator m, Schema29 schema) from28To29,
|
||||
required Future<void> Function(i1.Migrator m, Schema30 schema) from29To30,
|
||||
}) {
|
||||
return (currentVersion, database) async {
|
||||
switch (currentVersion) {
|
||||
@@ -16093,11 +15503,6 @@ i0.MigrationStepWithVersion migrationSteps({
|
||||
final migrator = i1.Migrator(database, schema);
|
||||
await from28To29(migrator, schema);
|
||||
return 29;
|
||||
case 29:
|
||||
final schema = Schema30(database: database);
|
||||
final migrator = i1.Migrator(database, schema);
|
||||
await from29To30(migrator, schema);
|
||||
return 30;
|
||||
default:
|
||||
throw ArgumentError.value('Unknown migration from $currentVersion');
|
||||
}
|
||||
@@ -16133,7 +15538,6 @@ i1.OnUpgrade stepByStep({
|
||||
required Future<void> Function(i1.Migrator m, Schema27 schema) from26To27,
|
||||
required Future<void> Function(i1.Migrator m, Schema28 schema) from27To28,
|
||||
required Future<void> Function(i1.Migrator m, Schema29 schema) from28To29,
|
||||
required Future<void> Function(i1.Migrator m, Schema30 schema) from29To30,
|
||||
}) => i0.VersionedSchema.stepByStepHelper(
|
||||
step: migrationSteps(
|
||||
from1To2: from1To2,
|
||||
@@ -16164,6 +15568,5 @@ i1.OnUpgrade stepByStep({
|
||||
from26To27: from26To27,
|
||||
from27To28: from27To28,
|
||||
from28To29: from28To29,
|
||||
from29To30: from29To30,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -27,18 +27,7 @@ class DriftMapRepository extends DriftDatabaseRepository {
|
||||
condition = condition & _db.remoteAssetEntity.isFavorite.equals(true);
|
||||
}
|
||||
|
||||
final timeRange = options.timeRange;
|
||||
final hasCustomRange = timeRange.from != null || timeRange.to != null;
|
||||
|
||||
if (hasCustomRange) {
|
||||
if (timeRange.from != null) {
|
||||
condition = condition & _db.remoteAssetEntity.createdAt.isBiggerOrEqualValue(timeRange.from!);
|
||||
}
|
||||
|
||||
if (timeRange.to != null) {
|
||||
condition = condition & _db.remoteAssetEntity.createdAt.isSmallerOrEqualValue(timeRange.to!);
|
||||
}
|
||||
} else if (options.relativeDays > 0) {
|
||||
if (options.relativeDays != 0) {
|
||||
final cutoffDate = DateTime.now().toUtc().subtract(Duration(days: options.relativeDays));
|
||||
condition = condition & _db.remoteAssetEntity.createdAt.isBiggerOrEqualValue(cutoffDate);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/domain/models/config/app_config.dart';
|
||||
import 'package:immich_mobile/domain/models/settings_key.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/settings.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/cached_key_value_repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
|
||||
class SettingsRepository extends CachedKeyValueRepository<SettingsKey, AppConfig> {
|
||||
class SettingsRepository extends DriftDatabaseRepository {
|
||||
final Drift _db;
|
||||
|
||||
SettingsRepository._(this._db) : super(const .new());
|
||||
SettingsRepository._(this._db) : super(_db);
|
||||
|
||||
static SettingsRepository? _instance;
|
||||
|
||||
@@ -20,6 +20,9 @@ class SettingsRepository extends CachedKeyValueRepository<SettingsKey, AppConfig
|
||||
return instance;
|
||||
}
|
||||
|
||||
AppConfig _appConfig = const .new();
|
||||
AppConfig get appConfig => _appConfig;
|
||||
|
||||
static Future<SettingsRepository> ensureInitialized(Drift db) async {
|
||||
if (_instance == null) {
|
||||
final instance = SettingsRepository._(db);
|
||||
@@ -29,20 +32,7 @@ class SettingsRepository extends CachedKeyValueRepository<SettingsKey, AppConfig
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
@override
|
||||
List<SettingsKey> get keys => SettingsKey.values;
|
||||
|
||||
@override
|
||||
Object decodeValue(SettingsKey key, String raw) => key.decode(raw);
|
||||
|
||||
@override
|
||||
AppConfig buildSnapshot(Map<SettingsKey, Object?> overrides) => AppConfig.fromEntries(overrides);
|
||||
|
||||
@override
|
||||
Selectable<({String key, String? value})> selectable() =>
|
||||
_db.select(_db.settingsEntity).map((row) => (key: row.key, value: row.value));
|
||||
|
||||
AppConfig get appConfig => snapshot;
|
||||
Future<void> refresh() async => _applyOverrides(await _db.select(_db.settingsEntity).get());
|
||||
|
||||
Future<void> clear(Iterable<SettingsKey> keys) async {
|
||||
if (keys.isEmpty) {
|
||||
@@ -52,15 +42,13 @@ class SettingsRepository extends CachedKeyValueRepository<SettingsKey, AppConfig
|
||||
final names = keys.map((key) => key.name).toList();
|
||||
await (_db.delete(_db.settingsEntity)..where((row) => row.key.isIn(names))).go();
|
||||
|
||||
var config = snapshot;
|
||||
for (final key in keys) {
|
||||
config = config.write(key, defaultConfig.read(key));
|
||||
_appConfig = _appConfig.write(key, defaultConfig.read(key));
|
||||
}
|
||||
snapshot = config;
|
||||
}
|
||||
|
||||
Future<void> write<T, U extends T>(SettingsKey<T> key, U value) async {
|
||||
if (value == snapshot.read(key)) {
|
||||
Future<void> write<T extends Object, U extends T>(SettingsKey<T> key, U value) async {
|
||||
if (value == _appConfig.read(key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -68,18 +56,29 @@ class SettingsRepository extends CachedKeyValueRepository<SettingsKey, AppConfig
|
||||
return clear([key]);
|
||||
}
|
||||
|
||||
String? resolvedValue;
|
||||
if (value != null) {
|
||||
resolvedValue = key.encode(value);
|
||||
}
|
||||
|
||||
await _db
|
||||
.into(_db.settingsEntity)
|
||||
.insertOnConflictUpdate(
|
||||
SettingsEntityCompanion.insert(key: key.name, value: .new(resolvedValue), updatedAt: .new(DateTime.now())),
|
||||
SettingsEntityCompanion.insert(key: key.name, value: key.encode(value), updatedAt: Value(DateTime.now())),
|
||||
);
|
||||
snapshot = snapshot.write(key, value);
|
||||
_appConfig = _appConfig.write(key, value);
|
||||
}
|
||||
|
||||
Stream<AppConfig> watchConfig() => watchSnapshot();
|
||||
Stream<AppConfig> watchConfig() => _db.select(_db.settingsEntity).watch().map((rows) {
|
||||
_applyOverrides(rows);
|
||||
return _appConfig;
|
||||
});
|
||||
|
||||
void _applyOverrides(List<SettingsEntityData> rows) {
|
||||
_appConfig = AppConfig.fromEntries(
|
||||
rows.fold({}, (overrides, row) {
|
||||
final metadataKey = SettingsKey.values.firstWhereOrNull((key) => key.name == row.key);
|
||||
if (metadataKey == null) {
|
||||
return overrides;
|
||||
}
|
||||
|
||||
return {...overrides, metadataKey: metadataKey.decode(row.value)};
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:immich_mobile/constants/constants.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/time_range.model.dart';
|
||||
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
|
||||
@@ -22,7 +21,6 @@ class TimelineMapOptions {
|
||||
final bool includeArchived;
|
||||
final bool withPartners;
|
||||
final int relativeDays;
|
||||
final TimeRange timeRange;
|
||||
|
||||
const TimelineMapOptions({
|
||||
required this.bounds,
|
||||
@@ -30,7 +28,6 @@ class TimelineMapOptions {
|
||||
this.includeArchived = false,
|
||||
this.withPartners = false,
|
||||
this.relativeDays = 0,
|
||||
this.timeRange = const TimeRange(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -556,21 +553,8 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
||||
query.where(_db.remoteAssetEntity.isFavorite.equals(true));
|
||||
}
|
||||
|
||||
final timeRange = options.timeRange;
|
||||
|
||||
final hasCustomRange = timeRange.from != null || timeRange.to != null;
|
||||
|
||||
if (hasCustomRange) {
|
||||
if (timeRange.from != null) {
|
||||
query.where(_db.remoteAssetEntity.createdAt.isBiggerOrEqualValue(timeRange.from!));
|
||||
}
|
||||
|
||||
if (timeRange.to != null) {
|
||||
query.where(_db.remoteAssetEntity.createdAt.isSmallerOrEqualValue(timeRange.to!));
|
||||
}
|
||||
} else if (options.relativeDays > 0) {
|
||||
if (options.relativeDays != 0) {
|
||||
final cutoffDate = DateTime.now().toUtc().subtract(Duration(days: options.relativeDays));
|
||||
|
||||
query.where(_db.remoteAssetEntity.createdAt.isBiggerOrEqualValue(cutoffDate));
|
||||
}
|
||||
|
||||
@@ -611,21 +595,8 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
||||
query.where(_db.remoteAssetEntity.isFavorite.equals(true));
|
||||
}
|
||||
|
||||
final timeRange = options.timeRange;
|
||||
|
||||
final hasCustomRange = timeRange.from != null || timeRange.to != null;
|
||||
|
||||
if (hasCustomRange) {
|
||||
if (timeRange.from != null) {
|
||||
query.where(_db.remoteAssetEntity.createdAt.isBiggerOrEqualValue(timeRange.from!));
|
||||
}
|
||||
|
||||
if (timeRange.to != null) {
|
||||
query.where(_db.remoteAssetEntity.createdAt.isSmallerOrEqualValue(timeRange.to!));
|
||||
}
|
||||
} else if (options.relativeDays > 0) {
|
||||
if (options.relativeDays != 0) {
|
||||
final cutoffDate = DateTime.now().toUtc().subtract(Duration(days: options.relativeDays));
|
||||
|
||||
query.where(_db.remoteAssetEntity.createdAt.isBiggerOrEqualValue(cutoffDate));
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,10 @@ import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
|
||||
class EditImageActionButton extends ConsumerWidget {
|
||||
const EditImageActionButton({super.key});
|
||||
final bool iconOnly;
|
||||
final bool menuItem;
|
||||
|
||||
const EditImageActionButton({super.key, this.iconOnly = false, this.menuItem = false});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
@@ -54,6 +57,8 @@ class EditImageActionButton extends ConsumerWidget {
|
||||
iconData: Icons.tune,
|
||||
label: "edit".t(context: context),
|
||||
onPressed: onPress,
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
|
||||
class OpenActivityActionButton extends ConsumerWidget {
|
||||
const OpenActivityActionButton({super.key, this.iconOnly = false, this.menuItem = false});
|
||||
|
||||
final bool iconOnly;
|
||||
final bool menuItem;
|
||||
|
||||
void _onTap(BuildContext context, WidgetRef ref) {
|
||||
final album = ref.read(currentRemoteAlbumProvider);
|
||||
final asset = ref.read(assetViewerProvider).currentAsset;
|
||||
if (album == null || asset == null) {
|
||||
return;
|
||||
}
|
||||
context.router.push(
|
||||
DriftActivitiesRoute(album: album, assetId: asset is RemoteAsset ? asset.id : null, assetName: asset.name),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) => BaseActionButton(
|
||||
iconData: Icons.chat_outlined,
|
||||
label: "activity".t(context: context),
|
||||
onPressed: () => _onTap(context, ref),
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
);
|
||||
}
|
||||
@@ -2,24 +2,18 @@ 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/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
||||
import 'package:immich_mobile/domain/models/setting.model.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/add_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_image_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/restore_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/upload_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/ocr_toggle_button.widget.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||
import 'package:immich_mobile/providers/routes.provider.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/utils/semver.dart';
|
||||
import 'package:immich_mobile/utils/action_button.utils.dart';
|
||||
import 'package:immich_mobile/widgets/asset_viewer/video_controls.dart';
|
||||
|
||||
class ViewerBottomBar extends ConsumerWidget {
|
||||
@@ -37,35 +31,31 @@ class ViewerBottomBar extends ConsumerWidget {
|
||||
final isOwner = asset is RemoteAsset && asset.ownerId == user?.id;
|
||||
final showingDetails = ref.watch(assetViewerProvider.select((s) => s.showingDetails));
|
||||
final isInLockedView = ref.watch(inLockedViewProvider);
|
||||
final serverInfo = ref.watch(serverInfoProvider);
|
||||
final isInTrash = ref.read(timelineServiceProvider).origin == TimelineOrigin.trash;
|
||||
final album = ref.watch(currentRemoteAlbumProvider);
|
||||
final isArchived = asset is RemoteAsset && asset.visibility == AssetVisibility.archive;
|
||||
final advancedTroubleshooting = ref.watch(settingsProvider.notifier).get(Setting.advancedTroubleshooting);
|
||||
final timelineOrigin = ref.read(timelineServiceProvider).origin;
|
||||
final isTrashEnabled = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash));
|
||||
final serverVersion = ref.watch(serverInfoProvider.select((state) => state.serverVersion));
|
||||
|
||||
final originalTheme = context.themeData;
|
||||
|
||||
final actions = <Widget>[
|
||||
if (isInTrash && isOwner && asset.hasRemote)
|
||||
const RestoreActionButton(source: ActionSource.viewer)
|
||||
else
|
||||
const ShareActionButton(source: ActionSource.viewer),
|
||||
final buttonContext = ActionButtonContext(
|
||||
asset: asset,
|
||||
isOwner: isOwner,
|
||||
isArchived: isArchived,
|
||||
isTrashEnabled: isTrashEnabled,
|
||||
isStacked: asset is RemoteAsset && asset.stackId != null,
|
||||
isInLockedView: isInLockedView,
|
||||
currentAlbum: album,
|
||||
advancedTroubleshooting: advancedTroubleshooting,
|
||||
source: ActionSource.viewer,
|
||||
timelineOrigin: timelineOrigin,
|
||||
originalTheme: originalTheme,
|
||||
serverVersion: serverVersion,
|
||||
);
|
||||
|
||||
if (!isInLockedView) ...[
|
||||
if (!isInTrash) ...[
|
||||
if (asset.isLocalOnly) const UploadActionButton(source: ActionSource.viewer),
|
||||
// edit sync was added in 2.6.0
|
||||
if (asset.isEditable && serverInfo.serverVersion >= const SemVer(major: 2, minor: 6, patch: 0))
|
||||
const EditImageActionButton(),
|
||||
if (asset.hasRemote) AddActionButton(originalTheme: originalTheme),
|
||||
],
|
||||
if (isOwner) ...[
|
||||
if (asset.isLocalOnly)
|
||||
const DeleteLocalActionButton(source: ActionSource.viewer)
|
||||
else if (asset.isTrashed)
|
||||
const DeletePermanentActionButton(source: ActionSource.viewer, useShortLabel: true)
|
||||
else
|
||||
const DeleteActionButton(source: ActionSource.viewer, showConfirmation: true),
|
||||
],
|
||||
],
|
||||
];
|
||||
final actions = ActionButtonBuilder.buildViewerBottomBar(buttonContext, context);
|
||||
|
||||
return AnimatedSwitcher(
|
||||
duration: Durations.short4,
|
||||
|
||||
@@ -30,6 +30,7 @@ class ViewerKebabMenu extends ConsumerWidget {
|
||||
final isCasting = ref.watch(castProvider.select((c) => c.isCasting));
|
||||
final timelineOrigin = ref.read(timelineServiceProvider).origin;
|
||||
final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash));
|
||||
final serverVersion = ref.watch(serverInfoProvider.select((state) => state.serverVersion));
|
||||
final isInLockedView = ref.watch(inLockedViewProvider);
|
||||
final currentAlbum = ref.watch(currentRemoteAlbumProvider);
|
||||
final isArchived = asset is RemoteAsset && asset.visibility == AssetVisibility.archive;
|
||||
@@ -47,9 +48,10 @@ class ViewerKebabMenu extends ConsumerWidget {
|
||||
source: ActionSource.viewer,
|
||||
isCasting: isCasting,
|
||||
timelineOrigin: timelineOrigin,
|
||||
serverVersion: serverVersion,
|
||||
);
|
||||
|
||||
final menuChildren = ActionButtonBuilder.buildViewerKebabMenu(actionContext, context, ref);
|
||||
final menuChildren = ActionButtonBuilder.buildViewerKebabMenu(actionContext, context);
|
||||
|
||||
return MenuAnchor(
|
||||
consumeOutsideTap: true,
|
||||
|
||||
@@ -16,7 +16,6 @@ import 'package:immich_mobile/providers/infrastructure/current_album.provider.da
|
||||
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
|
||||
import 'package:immich_mobile/providers/routes.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/utils/timezone.dart';
|
||||
|
||||
class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
||||
@@ -49,20 +48,6 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
||||
|
||||
final actions = <Widget>[
|
||||
if (asset.isMotionPhoto) const MotionPhotoActionButton(iconOnly: true),
|
||||
if (album != null && album.isActivityEnabled && album.isShared)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.chat_outlined),
|
||||
onPressed: () {
|
||||
context.router.push(
|
||||
DriftActivitiesRoute(
|
||||
album: album,
|
||||
assetId: asset is RemoteAsset ? asset.id : null,
|
||||
assetName: asset.name,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
if (asset.hasRemote && isOwner && !asset.isFavorite)
|
||||
const FavoriteActionButton(source: ActionSource.viewer, iconOnly: true),
|
||||
if (asset.hasRemote && isOwner && asset.isFavorite)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/events.model.dart';
|
||||
import 'package:immich_mobile/domain/models/time_range.model.dart';
|
||||
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/map.provider.dart';
|
||||
@@ -16,7 +15,6 @@ class MapState {
|
||||
final bool includeArchived;
|
||||
final bool withPartners;
|
||||
final int relativeDays;
|
||||
final TimeRange timeRange;
|
||||
|
||||
const MapState({
|
||||
this.themeMode = ThemeMode.system,
|
||||
@@ -25,7 +23,6 @@ class MapState {
|
||||
this.includeArchived = false,
|
||||
this.withPartners = false,
|
||||
this.relativeDays = 0,
|
||||
this.timeRange = const TimeRange(),
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -43,7 +40,6 @@ class MapState {
|
||||
bool? includeArchived,
|
||||
bool? withPartners,
|
||||
int? relativeDays,
|
||||
TimeRange? timeRange,
|
||||
}) {
|
||||
return MapState(
|
||||
bounds: bounds ?? this.bounds,
|
||||
@@ -52,7 +48,6 @@ class MapState {
|
||||
includeArchived: includeArchived ?? this.includeArchived,
|
||||
withPartners: withPartners ?? this.withPartners,
|
||||
relativeDays: relativeDays ?? this.relativeDays,
|
||||
timeRange: timeRange ?? this.timeRange,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -62,7 +57,6 @@ class MapState {
|
||||
includeArchived: includeArchived,
|
||||
withPartners: withPartners,
|
||||
relativeDays: relativeDays,
|
||||
timeRange: timeRange,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -109,13 +103,6 @@ class MapStateNotifier extends Notifier<MapState> {
|
||||
EventStream.shared.emit(const MapMarkerReloadEvent());
|
||||
}
|
||||
|
||||
void setTimeRange(TimeRange range) {
|
||||
ref.read(settingsProvider).write(.mapCustomFrom, range.from);
|
||||
ref.read(settingsProvider).write(.mapCustomTo, range.to);
|
||||
state = state.copyWith(timeRange: range);
|
||||
EventStream.shared.emit(const MapMarkerReloadEvent());
|
||||
}
|
||||
|
||||
@override
|
||||
MapState build() {
|
||||
final mapConfig = ref.read(appConfigProvider.select((config) => config.map));
|
||||
@@ -124,9 +111,8 @@ class MapStateNotifier extends Notifier<MapState> {
|
||||
onlyFavorites: mapConfig.favoritesOnly,
|
||||
includeArchived: mapConfig.includeArchived,
|
||||
withPartners: mapConfig.withPartners,
|
||||
bounds: LatLngBounds(northeast: const LatLng(0, 0), southwest: const LatLng(0, 0)),
|
||||
relativeDays: mapConfig.relativeDays,
|
||||
timeRange: TimeRange(from: mapConfig.customFrom, to: mapConfig.customTo),
|
||||
bounds: LatLngBounds(northeast: const LatLng(0, 0), southwest: const LatLng(0, 0)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,39 +1,21 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/time_range.model.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/generated/translations.g.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/map/map.state.dart';
|
||||
import 'package:immich_mobile/widgets/map/map_settings/map_custom_time_range.dart';
|
||||
import 'package:immich_mobile/widgets/map/map_settings/map_settings_list_tile.dart';
|
||||
import 'package:immich_mobile/widgets/map/map_settings/map_settings_time_dropdown.dart';
|
||||
import 'package:immich_mobile/widgets/map/map_settings/map_theme_picker.dart';
|
||||
|
||||
class DriftMapSettingsSheet extends ConsumerStatefulWidget {
|
||||
class DriftMapSettingsSheet extends HookConsumerWidget {
|
||||
const DriftMapSettingsSheet({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<DriftMapSettingsSheet> createState() => _DriftMapSettingsSheetState();
|
||||
}
|
||||
|
||||
class _DriftMapSettingsSheetState extends ConsumerState<DriftMapSettingsSheet> {
|
||||
late bool useCustomRange;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final mapState = ref.read(mapStateProvider);
|
||||
final timeRange = mapState.timeRange;
|
||||
useCustomRange = timeRange.from != null || timeRange.to != null;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final mapState = ref.watch(mapStateProvider);
|
||||
|
||||
return DraggableScrollableSheet(
|
||||
expand: false,
|
||||
initialChildSize: useCustomRange ? 0.7 : 0.6,
|
||||
initialChildSize: 0.6,
|
||||
builder: (ctx, scrollController) => SingleChildScrollView(
|
||||
controller: scrollController,
|
||||
child: Card(
|
||||
@@ -65,41 +47,10 @@ class _DriftMapSettingsSheetState extends ConsumerState<DriftMapSettingsSheet> {
|
||||
selected: mapState.withPartners,
|
||||
onChanged: (withPartners) => ref.read(mapStateProvider.notifier).switchWithPartners(withPartners),
|
||||
),
|
||||
if (useCustomRange) ...[
|
||||
MapTimeRange(
|
||||
timeRange: mapState.timeRange,
|
||||
onChanged: (range) {
|
||||
ref.read(mapStateProvider.notifier).setTimeRange(range);
|
||||
},
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: TextButton(
|
||||
onPressed: () => setState(() {
|
||||
useCustomRange = false;
|
||||
ref.read(mapStateProvider.notifier).setRelativeTime(0);
|
||||
ref.read(mapStateProvider.notifier).setTimeRange(const TimeRange());
|
||||
}),
|
||||
child: Text(context.t.remove_custom_date_range),
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
MapTimeDropDown(
|
||||
relativeTime: mapState.relativeDays,
|
||||
onTimeChange: (time) => ref.read(mapStateProvider.notifier).setRelativeTime(time),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: TextButton(
|
||||
onPressed: () => setState(() {
|
||||
useCustomRange = true;
|
||||
ref.read(mapStateProvider.notifier).setRelativeTime(0);
|
||||
ref.read(mapStateProvider.notifier).setTimeRange(const TimeRange());
|
||||
}),
|
||||
child: Text(context.t.use_custom_date_range),
|
||||
),
|
||||
),
|
||||
],
|
||||
MapTimeDropDown(
|
||||
relativeTime: mapState.relativeDays,
|
||||
onTimeChange: (time) => ref.read(mapStateProvider.notifier).setRelativeTime(time),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -5,8 +5,8 @@ import 'dart:io';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
|
||||
import 'package:immich_mobile/utils/debug_print.dart';
|
||||
import 'package:immich_mobile/utils/url_helper.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
@@ -179,7 +179,7 @@ class ApiService {
|
||||
}
|
||||
final network = SettingsRepository.instance.appConfig.network;
|
||||
final localEndpoint = network.localEndpoint;
|
||||
if (localEndpoint != null && localEndpoint.isNotEmpty) {
|
||||
if (localEndpoint.isNotEmpty) {
|
||||
urls.add(localEndpoint);
|
||||
}
|
||||
for (final url in network.externalEndpointList) {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
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/domain/models/events.model.dart';
|
||||
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
||||
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||
import 'package:immich_mobile/utils/semver.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/advanced_info_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||
@@ -16,8 +16,11 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/add_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_image_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/like_activity_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/open_activity_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/open_in_browser_action_button.widget.dart';
|
||||
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';
|
||||
@@ -47,6 +50,8 @@ class ActionButtonContext {
|
||||
final bool isCasting;
|
||||
final TimelineOrigin timelineOrigin;
|
||||
final int selectedCount;
|
||||
final ThemeData? originalTheme;
|
||||
final SemVer serverVersion;
|
||||
|
||||
const ActionButtonContext({
|
||||
required this.asset,
|
||||
@@ -61,13 +66,17 @@ class ActionButtonContext {
|
||||
this.isCasting = false,
|
||||
this.timelineOrigin = TimelineOrigin.main,
|
||||
this.selectedCount = 1,
|
||||
this.originalTheme,
|
||||
this.serverVersion = const SemVer(major: 0, minor: 0, patch: 0),
|
||||
});
|
||||
}
|
||||
|
||||
enum ActionButtonType {
|
||||
openInfo,
|
||||
openActivity,
|
||||
likeActivity,
|
||||
share,
|
||||
editImage,
|
||||
shareLink,
|
||||
cast,
|
||||
setAlbumCover,
|
||||
@@ -89,8 +98,16 @@ enum ActionButtonType {
|
||||
deleteLocal,
|
||||
deletePermanent,
|
||||
delete,
|
||||
addTo,
|
||||
advancedInfo;
|
||||
|
||||
bool _isInActivityAlbum(ActionButtonContext context) {
|
||||
return !context.isInLockedView &&
|
||||
context.currentAlbum != null &&
|
||||
context.currentAlbum!.isActivityEnabled &&
|
||||
context.currentAlbum!.isShared;
|
||||
}
|
||||
|
||||
bool shouldShow(ActionButtonContext context) {
|
||||
return switch (this) {
|
||||
ActionButtonType.advancedInfo => context.advancedTroubleshooting,
|
||||
@@ -159,11 +176,7 @@ enum ActionButtonType {
|
||||
!context.isInLockedView && //
|
||||
context.isStacked,
|
||||
ActionButtonType.openInBrowser => context.asset.hasRemote && !context.isInLockedView,
|
||||
ActionButtonType.likeActivity =>
|
||||
!context.isInLockedView &&
|
||||
context.currentAlbum != null &&
|
||||
context.currentAlbum!.isActivityEnabled &&
|
||||
context.currentAlbum!.isShared,
|
||||
ActionButtonType.likeActivity => _isInActivityAlbum(context),
|
||||
ActionButtonType.similarPhotos =>
|
||||
!context.isInLockedView && //
|
||||
context.asset is RemoteAsset,
|
||||
@@ -181,11 +194,26 @@ enum ActionButtonType {
|
||||
context.timelineOrigin != TimelineOrigin.localAlbum &&
|
||||
context.isOwner,
|
||||
ActionButtonType.cast => context.isCasting || context.asset.hasRemote,
|
||||
ActionButtonType.editImage =>
|
||||
!context.isInLockedView &&
|
||||
context.asset.isEditable &&
|
||||
context.serverVersion >= const SemVer(major: 2, minor: 6, patch: 0),
|
||||
ActionButtonType.addTo =>
|
||||
!context.isInLockedView && //
|
||||
context.asset.hasRemote,
|
||||
ActionButtonType.openActivity => _isInActivityAlbum(context),
|
||||
ActionButtonType.slideshow => true,
|
||||
};
|
||||
}
|
||||
|
||||
ConsumerWidget buildButton(
|
||||
bool showsAt(ButtonPosition position, ActionButtonContext context) => switch (this) {
|
||||
ActionButtonType.openActivity || ActionButtonType.likeActivity =>
|
||||
position != ButtonPosition.bottomBar || context.timelineOrigin != TimelineOrigin.trash,
|
||||
ActionButtonType.editImage => position != ButtonPosition.bottomBar || context.currentAlbum?.isShared != true,
|
||||
_ => true,
|
||||
};
|
||||
|
||||
Widget buildButton(
|
||||
ActionButtonContext context, [
|
||||
BuildContext? buildContext,
|
||||
bool iconOnly = false,
|
||||
@@ -221,8 +249,14 @@ enum ActionButtonType {
|
||||
source: context.source,
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
useShortLabel: false,
|
||||
),
|
||||
ActionButtonType.delete => DeleteActionButton(
|
||||
source: context.source,
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
showConfirmation: true,
|
||||
),
|
||||
ActionButtonType.delete => DeleteActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem),
|
||||
ActionButtonType.moveToLockFolder => MoveToLockFolderActionButton(
|
||||
source: context.source,
|
||||
iconOnly: iconOnly,
|
||||
@@ -288,6 +322,9 @@ enum ActionButtonType {
|
||||
},
|
||||
),
|
||||
ActionButtonType.cast => CastActionButton(iconOnly: iconOnly, menuItem: menuItem),
|
||||
ActionButtonType.editImage => EditImageActionButton(iconOnly: iconOnly, menuItem: menuItem),
|
||||
ActionButtonType.addTo => AddActionButton(originalTheme: context.originalTheme),
|
||||
ActionButtonType.openActivity => OpenActivityActionButton(iconOnly: iconOnly, menuItem: menuItem),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -319,41 +356,73 @@ enum ActionButtonType {
|
||||
class ActionButtonBuilder {
|
||||
static const List<ActionButtonType> _actionTypes = ActionButtonType.values;
|
||||
static const List<ActionButtonType> defaultViewerKebabMenuOrder = _actionTypes;
|
||||
static const Set<ActionButtonType> defaultViewerBottomBarButtons = {
|
||||
static const List<ActionButtonType> _defaultViewerBottomBarOrder = [
|
||||
ActionButtonType.share,
|
||||
ActionButtonType.moveToLockFolder,
|
||||
ActionButtonType.upload,
|
||||
ActionButtonType.delete,
|
||||
ActionButtonType.archive,
|
||||
ActionButtonType.unarchive,
|
||||
ActionButtonType.editImage,
|
||||
ActionButtonType.addTo,
|
||||
ActionButtonType.openActivity,
|
||||
ActionButtonType.likeActivity,
|
||||
ActionButtonType.removeFromLockFolder,
|
||||
ActionButtonType.restoreTrash,
|
||||
ActionButtonType.deletePermanent,
|
||||
};
|
||||
ActionButtonType.delete,
|
||||
ActionButtonType.deleteLocal,
|
||||
];
|
||||
|
||||
static List<Widget> build(ActionButtonContext context) {
|
||||
return _actionTypes.where((type) => type.shouldShow(context)).map((type) => type.buildButton(context)).toList();
|
||||
}
|
||||
|
||||
static List<Widget> buildViewerKebabMenu(ActionButtonContext context, BuildContext buildContext, WidgetRef ref) {
|
||||
final visibleButtons = defaultViewerKebabMenuOrder
|
||||
.where((type) => !defaultViewerBottomBarButtons.contains(type) && type.shouldShow(context))
|
||||
static List<ActionButtonType> getViewerBottomBarTypes(ActionButtonContext context) {
|
||||
return _defaultViewerBottomBarOrder
|
||||
.where((type) => type.shouldShow(context) && type.showsAt(ButtonPosition.bottomBar, context))
|
||||
.take(4)
|
||||
.toList();
|
||||
}
|
||||
|
||||
if (visibleButtons.isEmpty) {
|
||||
static List<ActionButtonType> getViewerKebabMenuTypes(ActionButtonContext context) {
|
||||
final inBottomBar = getViewerBottomBarTypes(context).toSet();
|
||||
return defaultViewerKebabMenuOrder
|
||||
.where(
|
||||
(type) =>
|
||||
!inBottomBar.contains(type) &&
|
||||
type.shouldShow(context) &&
|
||||
type.showsAt(ButtonPosition.kebabMenu, context),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
static List<Widget> buildViewerKebabMenu(ActionButtonContext context, BuildContext buildContext) {
|
||||
return getViewerKebabMenuTypes(context).toKebabMenuWidgets(context, buildContext);
|
||||
}
|
||||
|
||||
static List<Widget> buildViewerBottomBar(ActionButtonContext context, BuildContext buildContext) {
|
||||
return getViewerBottomBarTypes(context).toBottomBarWidgets(context, buildContext);
|
||||
}
|
||||
}
|
||||
|
||||
extension ActionButtonTypeListExtension on List<ActionButtonType> {
|
||||
List<Widget> toKebabMenuWidgets(ActionButtonContext context, BuildContext buildContext) {
|
||||
if (isEmpty) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final List<Widget> result = [];
|
||||
int? lastGroup;
|
||||
|
||||
for (final type in visibleButtons) {
|
||||
for (final type in this) {
|
||||
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;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
List<Widget> toBottomBarWidgets(ActionButtonContext context, BuildContext buildContext) {
|
||||
return map((type) => type.buildButton(context, buildContext, false, false)).toList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,10 +118,8 @@ Future<void> _migrateTo26(Drift drift) async {
|
||||
await migrator.migrateBool(StoreKey.legacyTapToNavigate, SettingsKey.viewerTapToNavigate);
|
||||
// Network
|
||||
await migrator.migrateBool(StoreKey.legacyAutoEndpointSwitching, SettingsKey.networkAutoEndpointSwitching);
|
||||
final preferredWifiName = await migrator.readLegacyStoreString(StoreKey.legacyPreferredWifiName.id);
|
||||
migrator.stage(StoreKey.legacyPreferredWifiName, SettingsKey.networkPreferredWifiName, preferredWifiName);
|
||||
final localEndpoint = await migrator.readLegacyStoreString(StoreKey.legacyLocalEndpoint.id);
|
||||
migrator.stage(StoreKey.legacyLocalEndpoint, SettingsKey.networkLocalEndpoint, localEndpoint);
|
||||
await migrator.migrateString(StoreKey.legacyPreferredWifiName, SettingsKey.networkPreferredWifiName);
|
||||
await migrator.migrateString(StoreKey.legacyLocalEndpoint, SettingsKey.networkLocalEndpoint);
|
||||
await _migrateExternalEndpointList(migrator);
|
||||
await _migrateCustomHeaders(migrator);
|
||||
// Album
|
||||
@@ -197,7 +195,7 @@ Future<void> _migrateCustomHeaders(_StoreMigrator migrator) async {
|
||||
|
||||
class _StoreMigrator {
|
||||
final Drift _db;
|
||||
final Map<SettingsKey, Object?> _cache = {};
|
||||
final Map<SettingsKey<Object>, Object> _cache = {};
|
||||
final List<int> _migratedStoreIds = [];
|
||||
|
||||
_StoreMigrator(this._db);
|
||||
@@ -267,7 +265,7 @@ class _StoreMigrator {
|
||||
_migratedStoreIds.add(legacyKey.id);
|
||||
}
|
||||
|
||||
void stage<T, U extends T>(StoreKey legacyKey, SettingsKey<T> newKey, U value) {
|
||||
void stage<T extends Object>(StoreKey legacyKey, SettingsKey<T> newKey, T value) {
|
||||
_cache[newKey] = value;
|
||||
_migratedStoreIds.add(legacyKey.id);
|
||||
}
|
||||
@@ -278,15 +276,9 @@ class _StoreMigrator {
|
||||
if (entry.value == defaultConfig.read(entry.key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String? resolvedValue;
|
||||
if (entry.value != null) {
|
||||
resolvedValue = entry.key.encode(entry.value);
|
||||
}
|
||||
|
||||
batch.insert(
|
||||
_db.settingsEntity,
|
||||
SettingsEntityCompanion(key: Value(entry.key.name), value: Value(resolvedValue)),
|
||||
SettingsEntityCompanion(key: Value(entry.key.name), value: Value(entry.key.encode(entry.value))),
|
||||
mode: InsertMode.insertOrReplace,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -26,17 +26,6 @@ sealed class Option<T> {
|
||||
None() => onNone(),
|
||||
};
|
||||
|
||||
Option<U> flatMap<U>(Option<U> Function(T value) f) => switch (this) {
|
||||
Some(:final value) => f(value),
|
||||
None() => const Option.none(),
|
||||
};
|
||||
|
||||
void ifPresent(void Function(T value) f) {
|
||||
if (this case Some(:final value)) {
|
||||
f(value);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => switch (this) {
|
||||
Some(:final value) => 'Some($value)',
|
||||
@@ -66,8 +55,8 @@ final class None<T> extends Option<T> {
|
||||
int get hashCode => 0;
|
||||
}
|
||||
|
||||
extension NullableOptionExtension<T> on Option<T>? {
|
||||
T? patch(T? current) => this == null ? current : this!.unwrapOrNull;
|
||||
extension ObjectOptionExtension<T> on T? {
|
||||
Option<T> toOption() => Option.fromNullable(this);
|
||||
}
|
||||
|
||||
extension OptionToOptional<T> on Option<T> {
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/domain/models/time_range.model.dart';
|
||||
import 'package:immich_mobile/generated/translations.g.dart';
|
||||
import 'package:immich_mobile/utils/option.dart';
|
||||
|
||||
class MapTimeRange extends StatelessWidget {
|
||||
const MapTimeRange({super.key, required this.timeRange, required this.onChanged});
|
||||
|
||||
final TimeRange timeRange;
|
||||
final Function(TimeRange) onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(context.t.date_after),
|
||||
subtitle: Text(
|
||||
timeRange.from != null
|
||||
? DateFormat.yMMMd(context.locale.toLanguageTag()).add_jm().format(timeRange.from!)
|
||||
: context.t.not_set,
|
||||
),
|
||||
trailing: timeRange.from != null
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () => onChanged(timeRange.copyWith(from: const .none())),
|
||||
)
|
||||
: null,
|
||||
onTap: () async {
|
||||
final initial = timeRange.from ?? DateTime.now();
|
||||
final currentTo = timeRange.to;
|
||||
|
||||
final picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: currentTo != null && initial.isAfter(currentTo) ? currentTo : initial,
|
||||
firstDate: DateTime(1970),
|
||||
lastDate: currentTo ?? DateTime.now(),
|
||||
);
|
||||
|
||||
if (picked != null) {
|
||||
onChanged(timeRange.copyWith(from: Option.some(picked)));
|
||||
}
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text(context.t.date_before),
|
||||
subtitle: Text(
|
||||
timeRange.to != null
|
||||
? DateFormat.yMMMd(context.locale.toLanguageTag()).add_jm().format(timeRange.to!)
|
||||
: context.t.not_set,
|
||||
),
|
||||
trailing: timeRange.to != null
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () => onChanged(timeRange.copyWith(to: const .none())),
|
||||
)
|
||||
: null,
|
||||
onTap: () async {
|
||||
final initial = timeRange.to ?? DateTime.now();
|
||||
final currentFrom = timeRange.from;
|
||||
|
||||
final picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: currentFrom != null && initial.isBefore(currentFrom) ? currentFrom : initial,
|
||||
firstDate: currentFrom ?? DateTime(1970),
|
||||
lastDate: DateTime.now(),
|
||||
);
|
||||
|
||||
if (picked != null) {
|
||||
onChanged(timeRange.copyWith(to: Option.some(picked)));
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
-4
@@ -33,7 +33,6 @@ import 'schema_v26.dart' as v26;
|
||||
import 'schema_v27.dart' as v27;
|
||||
import 'schema_v28.dart' as v28;
|
||||
import 'schema_v29.dart' as v29;
|
||||
import 'schema_v30.dart' as v30;
|
||||
|
||||
class GeneratedHelper implements SchemaInstantiationHelper {
|
||||
@override
|
||||
@@ -97,8 +96,6 @@ class GeneratedHelper implements SchemaInstantiationHelper {
|
||||
return v28.DatabaseAtV28(db);
|
||||
case 29:
|
||||
return v29.DatabaseAtV29(db);
|
||||
case 30:
|
||||
return v30.DatabaseAtV30(db);
|
||||
default:
|
||||
throw MissingSchemaException(version, versions);
|
||||
}
|
||||
@@ -134,6 +131,5 @@ class GeneratedHelper implements SchemaInstantiationHelper {
|
||||
27,
|
||||
28,
|
||||
29,
|
||||
30,
|
||||
];
|
||||
}
|
||||
|
||||
-10027
File diff suppressed because it is too large
Load Diff
@@ -456,7 +456,7 @@ void main() {
|
||||
|
||||
test('does not update when longitude does not match', () async {
|
||||
final remoteAsset = await ctx.newRemoteAsset(ownerId: userId);
|
||||
final cloudIdAsset = await ctx.newRemoteAssetCloudId(id: remoteAsset.id, longitude: .fromNullable((-74.006)));
|
||||
final cloudIdAsset = await ctx.newRemoteAssetCloudId(id: remoteAsset.id, longitude: (-74.006).toOption());
|
||||
final localAsset = await ctx.newLocalAsset(
|
||||
checksumOption: const Option.none(),
|
||||
iCloudId: cloudIdAsset.cloudId,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/domain/models/log.model.dart';
|
||||
@@ -60,39 +61,6 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
group('null values', () {
|
||||
test('writing null to a nullable key clears the row and reverts the cache to null', () async {
|
||||
await sut.write(SettingsKey.networkPreferredWifiName, 'home-wifi');
|
||||
expect(sut.appConfig.network.preferredWifiName, 'home-wifi');
|
||||
expect(await ctx.db.select(ctx.db.settingsEntity).get(), hasLength(1));
|
||||
|
||||
await sut.write(SettingsKey.networkPreferredWifiName, null);
|
||||
|
||||
expect(await ctx.db.select(ctx.db.settingsEntity).get(), isEmpty);
|
||||
expect(sut.appConfig.network.preferredWifiName, isNull);
|
||||
});
|
||||
|
||||
test('writing null to an already-null key is a no-op', () async {
|
||||
await sut.write(SettingsKey.networkPreferredWifiName, null);
|
||||
expect(await ctx.db.select(ctx.db.settingsEntity).get(), isEmpty);
|
||||
});
|
||||
|
||||
test('a stored NULL value column decodes to null on refresh', () async {
|
||||
await ctx.db
|
||||
.into(ctx.db.settingsEntity)
|
||||
.insert(
|
||||
SettingsEntityCompanion.insert(
|
||||
key: SettingsKey.networkPreferredWifiName.name,
|
||||
value: const .new(null),
|
||||
updatedAt: .new(DateTime.now()),
|
||||
),
|
||||
);
|
||||
|
||||
await SettingsRepository.instance.refresh();
|
||||
expect(sut.appConfig.network.preferredWifiName, isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('delete', () {});
|
||||
|
||||
group('sync', () {
|
||||
@@ -102,8 +70,8 @@ void main() {
|
||||
.insert(
|
||||
SettingsEntityCompanion.insert(
|
||||
key: SettingsKey.themeMode.name,
|
||||
value: .new(ThemeMode.dark.name),
|
||||
updatedAt: .new(DateTime.now()),
|
||||
value: ThemeMode.dark.name,
|
||||
updatedAt: Value(DateTime.now()),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -130,8 +98,8 @@ void main() {
|
||||
.insert(
|
||||
SettingsEntityCompanion.insert(
|
||||
key: 'app-config.unknown.future-key',
|
||||
value: const .new('unknown'),
|
||||
updatedAt: .new(DateTime.now()),
|
||||
value: 'whatever',
|
||||
updatedAt: Value(DateTime.now()),
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -5,26 +5,12 @@ import 'package:immich_mobile/domain/models/settings_key.dart';
|
||||
void main() {
|
||||
group('SettingsKey', () {
|
||||
for (final key in SettingsKey.values) {
|
||||
final defaultValue = defaultConfig.read(key);
|
||||
// null is a valid value for some keys but we don't use the codec in that case
|
||||
if (defaultValue == null) {
|
||||
continue;
|
||||
}
|
||||
test('verify codec for $key', () {
|
||||
final defaultValue = defaultConfig.read(key);
|
||||
final encoded = key.encode(defaultValue);
|
||||
final decoded = key.decode(encoded);
|
||||
expect(decoded, defaultValue, reason: 'round-trip failed for ${key.name}');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
group('AppConfig.fromEntries', () {
|
||||
for (final key in SettingsKey.values) {
|
||||
test('routes the default value for $key through write/read', () {
|
||||
final value = defaultConfig.read(key);
|
||||
final config = AppConfig.fromEntries({key: value});
|
||||
expect(config.read(key), value, reason: 'write/read mismatch for ${key.name}');
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -100,23 +100,17 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
group('NullableOptionExtension', () {
|
||||
test('patch returns the current value when the option is null', () {
|
||||
const Option<String>? omitted = null;
|
||||
expect(omitted.patch('existing'), 'existing');
|
||||
group('ObjectOptionExtension', () {
|
||||
test('non-null value.toOption() returns Some', () {
|
||||
final option = 'hello'.toOption();
|
||||
expect(option, isA<Some<String>>());
|
||||
expect((option as Some).value, 'hello');
|
||||
});
|
||||
|
||||
test('patch preserves a null current when the option is null', () {
|
||||
const Option<String>? omitted = null;
|
||||
expect(omitted.patch(null), isNull);
|
||||
});
|
||||
|
||||
test('patch returns the wrapped value for Some, overriding current', () {
|
||||
expect(const Option.some('new').patch('existing'), 'new');
|
||||
});
|
||||
|
||||
test('patch returns null for None, clearing current', () {
|
||||
expect(const Option<String>.none().patch('existing'), isNull);
|
||||
test('null value.toOption() returns None', () {
|
||||
const String? value = null;
|
||||
final option = value.toOption();
|
||||
expect(option, isA<None<String>>());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/domain/models/value_codec.dart';
|
||||
|
||||
enum _Fruit { apple, banana, cherry }
|
||||
|
||||
void main() {
|
||||
group('MapCodec', () {
|
||||
group('encode', () {
|
||||
test('serializes an empty map to an empty JSON object', () {
|
||||
const codec = MapCodec<String, String>(PrimitiveCodec.string, PrimitiveCodec.string);
|
||||
expect(codec.encode({}), '{}');
|
||||
});
|
||||
|
||||
test('encodes a string-to-string map as a JSON object', () {
|
||||
const codec = MapCodec<String, String>(PrimitiveCodec.string, PrimitiveCodec.string);
|
||||
expect(codec.encode({'a': '1', 'b': '2'}), '{"a":"1","b":"2"}');
|
||||
});
|
||||
|
||||
test('stringifies non-string values via the value codec', () {
|
||||
const codec = MapCodec<String, int>(PrimitiveCodec.string, PrimitiveCodec.integer);
|
||||
expect(codec.encode({'x': 10, 'y': 20}), '{"x":"10","y":"20"}');
|
||||
});
|
||||
|
||||
test('stringifies non-string keys via the key codec', () {
|
||||
const codec = MapCodec<int, String>(PrimitiveCodec.integer, PrimitiveCodec.string);
|
||||
expect(codec.encode({1: 'one', 2: 'two'}), '{"1":"one","2":"two"}');
|
||||
});
|
||||
});
|
||||
|
||||
group('decode', () {
|
||||
test('reconstructs a string-to-string map', () {
|
||||
const codec = MapCodec<String, String>(PrimitiveCodec.string, PrimitiveCodec.string);
|
||||
expect(codec.decode('{"a":"1","b":"2"}'), {'a': '1', 'b': '2'});
|
||||
});
|
||||
|
||||
test('parses values back to their domain type', () {
|
||||
const codec = MapCodec<String, int>(PrimitiveCodec.string, PrimitiveCodec.integer);
|
||||
expect(codec.decode('{"x":"10","y":"20"}'), {'x': 10, 'y': 20});
|
||||
});
|
||||
|
||||
test('parses keys back to their domain type', () {
|
||||
const codec = MapCodec<int, String>(PrimitiveCodec.integer, PrimitiveCodec.string);
|
||||
expect(codec.decode('{"1":"one","2":"two"}'), {1: 'one', 2: 'two'});
|
||||
});
|
||||
|
||||
test('returns an empty map for an empty JSON object', () {
|
||||
const codec = MapCodec<String, String>(PrimitiveCodec.string, PrimitiveCodec.string);
|
||||
expect(codec.decode('{}'), isEmpty);
|
||||
});
|
||||
|
||||
test('returns an empty map when the payload is not valid JSON', () {
|
||||
const codec = MapCodec<String, String>(PrimitiveCodec.string, PrimitiveCodec.string);
|
||||
expect(codec.decode('not json'), isEmpty);
|
||||
});
|
||||
|
||||
test('returns an empty map when the JSON root is not an object', () {
|
||||
const codec = MapCodec<String, String>(PrimitiveCodec.string, PrimitiveCodec.string);
|
||||
expect(codec.decode('[]'), isEmpty);
|
||||
expect(codec.decode('"a string"'), isEmpty);
|
||||
expect(codec.decode('42'), isEmpty);
|
||||
});
|
||||
|
||||
test('skips entries whose value is not a JSON string, keeping the rest', () {
|
||||
const codec = MapCodec<String, int>(PrimitiveCodec.string, PrimitiveCodec.integer);
|
||||
expect(codec.decode('{"x":1,"y":"20"}'), {'y': 20});
|
||||
});
|
||||
|
||||
test('skips entries whose value is a nested object, keeping the rest', () {
|
||||
const codec = MapCodec<String, String>(PrimitiveCodec.string, PrimitiveCodec.string);
|
||||
expect(codec.decode('{"a":{"nested":"value"},"b":"ok"}'), {'b': 'ok'});
|
||||
});
|
||||
|
||||
test('returns an empty map when every entry is malformed', () {
|
||||
const codec = MapCodec<String, int>(PrimitiveCodec.string, PrimitiveCodec.integer);
|
||||
expect(codec.decode('{"x":1,"y":2}'), isEmpty);
|
||||
});
|
||||
});
|
||||
|
||||
group('round trip', () {
|
||||
test('preserves a primitive map through encode then decode', () {
|
||||
const codec = MapCodec<String, int>(PrimitiveCodec.string, PrimitiveCodec.integer);
|
||||
const original = {'one': 1, 'two': 2, 'three': 3};
|
||||
expect(codec.decode(codec.encode(original)), original);
|
||||
});
|
||||
|
||||
test('preserves an enum-valued map by composing with EnumCodec', () {
|
||||
const codec = MapCodec<String, _Fruit>(PrimitiveCodec.string, EnumCodec(_Fruit.values));
|
||||
const original = {'breakfast': _Fruit.banana, 'snack': _Fruit.apple};
|
||||
expect(codec.decode(codec.encode(original)), original);
|
||||
});
|
||||
|
||||
test('preserves a DateTime-valued map by composing with DateTimeCodec', () {
|
||||
const codec = MapCodec<String, DateTime>(PrimitiveCodec.string, DateTimeCodec());
|
||||
final original = {'created': DateTime.utc(2024, 1, 1, 12, 30)};
|
||||
expect(codec.decode(codec.encode(original)), original);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -5,6 +5,7 @@ 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/services/timeline.service.dart';
|
||||
import 'package:immich_mobile/utils/action_button.utils.dart';
|
||||
import 'package:immich_mobile/utils/semver.dart';
|
||||
|
||||
LocalAsset createLocalAsset({
|
||||
String? remoteId,
|
||||
@@ -1238,4 +1239,438 @@ void main() {
|
||||
expect(nonArchivedWidgets, isNotEmpty);
|
||||
});
|
||||
});
|
||||
|
||||
group('ActionButtonBuilder.getViewerBottomBarTypes', () {
|
||||
test('should return correct button types for shared album with activity', () {
|
||||
final remoteAsset = createRemoteAsset();
|
||||
final album = createRemoteAlbum(isActivityEnabled: true, isShared: true);
|
||||
final context = ActionButtonContext(
|
||||
asset: remoteAsset,
|
||||
isOwner: true,
|
||||
isArchived: false,
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: album,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.viewer,
|
||||
);
|
||||
|
||||
const expectedTypes = [
|
||||
ActionButtonType.share,
|
||||
ActionButtonType.addTo,
|
||||
ActionButtonType.openActivity,
|
||||
ActionButtonType.likeActivity,
|
||||
];
|
||||
|
||||
final bottomBarTypes = ActionButtonBuilder.getViewerBottomBarTypes(context);
|
||||
final kebabTypes = ActionButtonBuilder.getViewerKebabMenuTypes(context);
|
||||
|
||||
expect(bottomBarTypes, expectedTypes);
|
||||
expect(bottomBarTypes.any(kebabTypes.contains), isFalse);
|
||||
});
|
||||
|
||||
test('should return correct button types for local only asset', () {
|
||||
final localAsset = createLocalAsset();
|
||||
final context = ActionButtonContext(
|
||||
asset: localAsset,
|
||||
isOwner: true,
|
||||
isArchived: false,
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.viewer,
|
||||
serverVersion: const SemVer(major: 2, minor: 6, patch: 0),
|
||||
);
|
||||
|
||||
const expectedTypes = [ActionButtonType.share, ActionButtonType.upload, ActionButtonType.deleteLocal];
|
||||
|
||||
final bottomBarTypes = ActionButtonBuilder.getViewerBottomBarTypes(context);
|
||||
final kebabTypes = ActionButtonBuilder.getViewerKebabMenuTypes(context);
|
||||
|
||||
expect(bottomBarTypes, expectedTypes);
|
||||
expect(bottomBarTypes.any(kebabTypes.contains), isFalse);
|
||||
});
|
||||
|
||||
test('should return correct button types for locked view', () {
|
||||
final remoteAsset = createRemoteAsset();
|
||||
final context = ActionButtonContext(
|
||||
asset: remoteAsset,
|
||||
isOwner: true,
|
||||
isArchived: false,
|
||||
isTrashEnabled: false,
|
||||
isInLockedView: true,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.viewer,
|
||||
);
|
||||
|
||||
const expectedTypes = [
|
||||
ActionButtonType.share,
|
||||
ActionButtonType.removeFromLockFolder,
|
||||
ActionButtonType.deletePermanent,
|
||||
];
|
||||
|
||||
final bottomBarTypes = ActionButtonBuilder.getViewerBottomBarTypes(context);
|
||||
final kebabTypes = ActionButtonBuilder.getViewerKebabMenuTypes(context);
|
||||
|
||||
expect(bottomBarTypes, expectedTypes);
|
||||
expect(bottomBarTypes.any(kebabTypes.contains), isFalse);
|
||||
});
|
||||
|
||||
test('should return correct button types for remote only asset', () {
|
||||
final remoteAsset = createRemoteAsset();
|
||||
final context = ActionButtonContext(
|
||||
asset: remoteAsset,
|
||||
isOwner: true,
|
||||
isArchived: false,
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.viewer,
|
||||
serverVersion: const SemVer(major: 2, minor: 6, patch: 0),
|
||||
);
|
||||
|
||||
const expectedTypes = [
|
||||
ActionButtonType.share,
|
||||
ActionButtonType.editImage,
|
||||
ActionButtonType.addTo,
|
||||
ActionButtonType.delete,
|
||||
];
|
||||
|
||||
final bottomBarTypes = ActionButtonBuilder.getViewerBottomBarTypes(context);
|
||||
final kebabTypes = ActionButtonBuilder.getViewerKebabMenuTypes(context);
|
||||
|
||||
expect(bottomBarTypes, expectedTypes);
|
||||
expect(bottomBarTypes.any(kebabTypes.contains), isFalse);
|
||||
});
|
||||
|
||||
test('trashed asset (no album) keeps restore and permanent delete in bottom bar', () {
|
||||
final remoteAsset = createRemoteAsset(deletedAt: DateTime.now());
|
||||
final context = ActionButtonContext(
|
||||
asset: remoteAsset,
|
||||
isOwner: true,
|
||||
isArchived: false,
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.viewer,
|
||||
timelineOrigin: TimelineOrigin.trash,
|
||||
);
|
||||
|
||||
const expectedTypes = [
|
||||
ActionButtonType.share,
|
||||
ActionButtonType.addTo,
|
||||
ActionButtonType.restoreTrash,
|
||||
ActionButtonType.deletePermanent,
|
||||
];
|
||||
|
||||
expect(ActionButtonBuilder.getViewerBottomBarTypes(context), expectedTypes);
|
||||
});
|
||||
|
||||
test('trashed asset in shared activity album still surfaces restore and permanent delete', () {
|
||||
final remoteAsset = createRemoteAsset(deletedAt: DateTime.now());
|
||||
final album = createRemoteAlbum(isActivityEnabled: true, isShared: true);
|
||||
final context = ActionButtonContext(
|
||||
asset: remoteAsset,
|
||||
isOwner: true,
|
||||
isArchived: false,
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: album,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.viewer,
|
||||
timelineOrigin: TimelineOrigin.trash,
|
||||
);
|
||||
|
||||
final bottomBarTypes = ActionButtonBuilder.getViewerBottomBarTypes(context);
|
||||
|
||||
expect(bottomBarTypes, contains(ActionButtonType.restoreTrash));
|
||||
expect(bottomBarTypes, contains(ActionButtonType.deletePermanent));
|
||||
expect(bottomBarTypes, isNot(contains(ActionButtonType.openActivity)));
|
||||
expect(bottomBarTypes, isNot(contains(ActionButtonType.likeActivity)));
|
||||
});
|
||||
|
||||
test('5th bottom-bar candidate overflows into the kebab', () {
|
||||
final remoteAsset = createRemoteAsset();
|
||||
final album = createRemoteAlbum(isActivityEnabled: true, isShared: true);
|
||||
final context = ActionButtonContext(
|
||||
asset: remoteAsset,
|
||||
isOwner: true,
|
||||
isArchived: false,
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: album,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.viewer,
|
||||
serverVersion: const SemVer(major: 2, minor: 6, patch: 0),
|
||||
);
|
||||
|
||||
final bottomBarTypes = ActionButtonBuilder.getViewerBottomBarTypes(context);
|
||||
final kebabTypes = ActionButtonBuilder.getViewerKebabMenuTypes(context);
|
||||
|
||||
expect(bottomBarTypes.length, 4);
|
||||
expect(bottomBarTypes, isNot(contains(ActionButtonType.delete)));
|
||||
expect(kebabTypes, contains(ActionButtonType.delete));
|
||||
});
|
||||
});
|
||||
|
||||
group('ActionButtonType.shouldShow editImage button', () {
|
||||
final editableRemote = createRemoteAsset();
|
||||
|
||||
test('should show for editable remote asset on a recent server', () {
|
||||
final context = ActionButtonContext(
|
||||
asset: editableRemote,
|
||||
isOwner: true,
|
||||
isArchived: false,
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.viewer,
|
||||
serverVersion: const SemVer(major: 2, minor: 6, patch: 0),
|
||||
);
|
||||
|
||||
expect(ActionButtonType.editImage.shouldShow(context), isTrue);
|
||||
});
|
||||
|
||||
test('should not show when local-only (LocalAsset is not editable)', () {
|
||||
final context = ActionButtonContext(
|
||||
asset: createLocalAsset(),
|
||||
isOwner: true,
|
||||
isArchived: false,
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.viewer,
|
||||
serverVersion: const SemVer(major: 2, minor: 6, patch: 0),
|
||||
);
|
||||
|
||||
expect(ActionButtonType.editImage.shouldShow(context), isFalse);
|
||||
});
|
||||
|
||||
test('should not show when server version is below 2.6', () {
|
||||
final context = ActionButtonContext(
|
||||
asset: editableRemote,
|
||||
isOwner: true,
|
||||
isArchived: false,
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.viewer,
|
||||
serverVersion: const SemVer(major: 2, minor: 5, patch: 9),
|
||||
);
|
||||
|
||||
expect(ActionButtonType.editImage.shouldShow(context), isFalse);
|
||||
});
|
||||
|
||||
test('should not show in locked view', () {
|
||||
final context = ActionButtonContext(
|
||||
asset: editableRemote,
|
||||
isOwner: true,
|
||||
isArchived: false,
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: true,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.viewer,
|
||||
serverVersion: const SemVer(major: 2, minor: 6, patch: 0),
|
||||
);
|
||||
|
||||
expect(ActionButtonType.editImage.shouldShow(context), isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
group('ActionButtonType.shouldShow addTo button', () {
|
||||
test('should show when not locked and asset has remote', () {
|
||||
final context = ActionButtonContext(
|
||||
asset: createRemoteAsset(),
|
||||
isOwner: true,
|
||||
isArchived: false,
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.viewer,
|
||||
);
|
||||
|
||||
expect(ActionButtonType.addTo.shouldShow(context), isTrue);
|
||||
});
|
||||
|
||||
test('should not show in locked view', () {
|
||||
final context = ActionButtonContext(
|
||||
asset: createRemoteAsset(),
|
||||
isOwner: true,
|
||||
isArchived: false,
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: true,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.viewer,
|
||||
);
|
||||
|
||||
expect(ActionButtonType.addTo.shouldShow(context), isFalse);
|
||||
});
|
||||
|
||||
test('should not show for local-only asset', () {
|
||||
final context = ActionButtonContext(
|
||||
asset: createLocalAsset(),
|
||||
isOwner: true,
|
||||
isArchived: false,
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.viewer,
|
||||
);
|
||||
|
||||
expect(ActionButtonType.addTo.shouldShow(context), isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
group('ActionButtonType.shouldShow openActivity button', () {
|
||||
final asset = createRemoteAsset();
|
||||
|
||||
test('should show in a shared activity-enabled album', () {
|
||||
final context = ActionButtonContext(
|
||||
asset: asset,
|
||||
isOwner: true,
|
||||
isArchived: false,
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: createRemoteAlbum(isActivityEnabled: true, isShared: true),
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.viewer,
|
||||
);
|
||||
|
||||
expect(ActionButtonType.openActivity.shouldShow(context), isTrue);
|
||||
});
|
||||
|
||||
test('should not show when no album', () {
|
||||
final context = ActionButtonContext(
|
||||
asset: asset,
|
||||
isOwner: true,
|
||||
isArchived: false,
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.viewer,
|
||||
);
|
||||
|
||||
expect(ActionButtonType.openActivity.shouldShow(context), isFalse);
|
||||
});
|
||||
|
||||
test('should not show when album not shared', () {
|
||||
final context = ActionButtonContext(
|
||||
asset: asset,
|
||||
isOwner: true,
|
||||
isArchived: false,
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: createRemoteAlbum(isActivityEnabled: true, isShared: false),
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.viewer,
|
||||
);
|
||||
|
||||
expect(ActionButtonType.openActivity.shouldShow(context), isFalse);
|
||||
});
|
||||
|
||||
test('should not show when activity disabled', () {
|
||||
final context = ActionButtonContext(
|
||||
asset: asset,
|
||||
isOwner: true,
|
||||
isArchived: false,
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: createRemoteAlbum(isActivityEnabled: false, isShared: true),
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.viewer,
|
||||
);
|
||||
|
||||
expect(ActionButtonType.openActivity.shouldShow(context), isFalse);
|
||||
});
|
||||
|
||||
test('should not show in locked view', () {
|
||||
final context = ActionButtonContext(
|
||||
asset: asset,
|
||||
isOwner: true,
|
||||
isArchived: false,
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: true,
|
||||
currentAlbum: createRemoteAlbum(isActivityEnabled: true, isShared: true),
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.viewer,
|
||||
);
|
||||
|
||||
expect(ActionButtonType.openActivity.shouldShow(context), isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
group('ActionButtonType.showsAt', () {
|
||||
ActionButtonContext ctx({RemoteAlbum? album, TimelineOrigin origin = TimelineOrigin.main}) => ActionButtonContext(
|
||||
asset: createRemoteAsset(),
|
||||
isOwner: true,
|
||||
isArchived: false,
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: album,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.viewer,
|
||||
timelineOrigin: origin,
|
||||
);
|
||||
|
||||
group('editImage', () {
|
||||
test('hidden from bottom bar when album is shared', () {
|
||||
final context = ctx(album: createRemoteAlbum(isShared: true));
|
||||
expect(ActionButtonType.editImage.showsAt(ButtonPosition.bottomBar, context), isFalse);
|
||||
});
|
||||
|
||||
test('shown in bottom bar when album is not shared', () {
|
||||
final context = ctx(album: createRemoteAlbum(isShared: false));
|
||||
expect(ActionButtonType.editImage.showsAt(ButtonPosition.bottomBar, context), isTrue);
|
||||
});
|
||||
|
||||
test('shown in bottom bar when there is no album', () {
|
||||
expect(ActionButtonType.editImage.showsAt(ButtonPosition.bottomBar, ctx()), isTrue);
|
||||
});
|
||||
});
|
||||
|
||||
group('openActivity / likeActivity', () {
|
||||
test('hidden from bottom bar when viewing from trash', () {
|
||||
final context = ctx(origin: TimelineOrigin.trash);
|
||||
expect(ActionButtonType.openActivity.showsAt(ButtonPosition.bottomBar, context), isFalse);
|
||||
expect(ActionButtonType.likeActivity.showsAt(ButtonPosition.bottomBar, context), isFalse);
|
||||
});
|
||||
|
||||
test('shown in bottom bar outside trash', () {
|
||||
expect(ActionButtonType.openActivity.showsAt(ButtonPosition.bottomBar, ctx()), isTrue);
|
||||
expect(ActionButtonType.likeActivity.showsAt(ButtonPosition.bottomBar, ctx()), isTrue);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"@vitest/coverage-v8": "^4.0.0",
|
||||
"byte-size": "^9.0.0",
|
||||
"cli-progress": "^3.12.0",
|
||||
"commander": "^12.0.0",
|
||||
"commander": "^15.0.0",
|
||||
"eslint": "^10.0.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
|
||||
Generated
+21
-21
@@ -164,7 +164,7 @@ importers:
|
||||
version: 64.0.0(eslint@10.4.0(jiti@2.7.0))
|
||||
exiftool-vendored:
|
||||
specifier: ^35.0.0
|
||||
version: 35.20.0
|
||||
version: 35.21.0
|
||||
globals:
|
||||
specifier: ^17.0.0
|
||||
version: 17.6.0
|
||||
@@ -260,8 +260,8 @@ importers:
|
||||
specifier: ^3.12.0
|
||||
version: 3.12.0
|
||||
commander:
|
||||
specifier: ^12.0.0
|
||||
version: 12.1.0
|
||||
specifier: ^15.0.0
|
||||
version: 15.0.0
|
||||
eslint:
|
||||
specifier: ^10.0.0
|
||||
version: 10.4.0(jiti@2.7.0)
|
||||
@@ -481,7 +481,7 @@ importers:
|
||||
version: 4.4.0
|
||||
exiftool-vendored:
|
||||
specifier: ^35.20.0
|
||||
version: 35.20.0
|
||||
version: 35.21.0
|
||||
express:
|
||||
specifier: ^5.1.0
|
||||
version: 5.2.1
|
||||
@@ -6596,10 +6596,6 @@ packages:
|
||||
resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
commander@12.1.0:
|
||||
resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
commander@13.1.0:
|
||||
resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -6608,6 +6604,10 @@ packages:
|
||||
resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
commander@15.0.0:
|
||||
resolution: {integrity: sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==}
|
||||
engines: {node: '>=22.12.0'}
|
||||
|
||||
commander@2.20.3:
|
||||
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
|
||||
|
||||
@@ -7746,17 +7746,17 @@ packages:
|
||||
resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
exiftool-vendored.exe@13.58.0:
|
||||
resolution: {integrity: sha512-pV7SjQeOu4Q77DWuyF+hlRYWVlRcSAqfqTTujBZeGUy/Q9+RPAy877YgSZIxKOYW1TxmmL8KyBGxaG0JKYG8BQ==}
|
||||
exiftool-vendored.exe@13.59.0:
|
||||
resolution: {integrity: sha512-d+Glvl77sKAt3ybMyE2o9eLMtublnZIlii7QGFej5CwcPwvq9v440CmKnLvufzdUV3RGXU1q1/rZepONUNXJIQ==}
|
||||
os: [win32]
|
||||
|
||||
exiftool-vendored.pl@13.58.0:
|
||||
resolution: {integrity: sha512-+Z2xhZrYLMu/anO/s14AaS/K5HMJ5Cw9C3KefIeYNpkZRN4RRBJHm7R34yjj9Pv+elqYRZrQV9NcqvkBLn/68w==}
|
||||
exiftool-vendored.pl@13.59.0:
|
||||
resolution: {integrity: sha512-6YlBpi4pcfSmIYcsied6g4+XlDe7KSKMf5WPsLBpUCiD5PyO2fd1v/Ob37VZDj36+9XCRnXY98fqVfCfNIq8ug==}
|
||||
os: ['!win32']
|
||||
hasBin: true
|
||||
|
||||
exiftool-vendored@35.20.0:
|
||||
resolution: {integrity: sha512-Yn66dSBaWGcUaSbm5Nl4G28rxtceLlWf4PstqJMbLix9sN7w0okWHPEvdudiP56Q5Cjl7v3TLyKKwowUFlbD8g==}
|
||||
exiftool-vendored@35.21.0:
|
||||
resolution: {integrity: sha512-yZE8E8ZrRwAiD0z+MaAH5n6FdGsc2dbXbGHTJOaMWsQFsNOuKOkHgpgFzS9neB4MB8MirNlKDfEOEmg1KzdeTw==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
expand-template@2.0.3:
|
||||
@@ -19483,12 +19483,12 @@ snapshots:
|
||||
|
||||
commander@11.1.0: {}
|
||||
|
||||
commander@12.1.0: {}
|
||||
|
||||
commander@13.1.0: {}
|
||||
|
||||
commander@14.0.3: {}
|
||||
|
||||
commander@15.0.0: {}
|
||||
|
||||
commander@2.20.3: {}
|
||||
|
||||
commander@4.1.1: {}
|
||||
@@ -20826,21 +20826,21 @@ snapshots:
|
||||
signal-exit: 3.0.7
|
||||
strip-final-newline: 2.0.0
|
||||
|
||||
exiftool-vendored.exe@13.58.0:
|
||||
exiftool-vendored.exe@13.59.0:
|
||||
optional: true
|
||||
|
||||
exiftool-vendored.pl@13.58.0: {}
|
||||
exiftool-vendored.pl@13.59.0: {}
|
||||
|
||||
exiftool-vendored@35.20.0:
|
||||
exiftool-vendored@35.21.0:
|
||||
dependencies:
|
||||
'@photostructure/tz-lookup': 11.5.0
|
||||
'@types/luxon': 3.7.1
|
||||
batch-cluster: 17.3.1
|
||||
exiftool-vendored.pl: 13.58.0
|
||||
exiftool-vendored.pl: 13.59.0
|
||||
he: 1.2.0
|
||||
luxon: 3.7.2
|
||||
optionalDependencies:
|
||||
exiftool-vendored.exe: 13.58.0
|
||||
exiftool-vendored.exe: 13.59.0
|
||||
|
||||
expand-template@2.0.3:
|
||||
optional: true
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import type { CalendarHeatmapResponseDto } from '@immich/sdk';
|
||||
import { DateTime } from 'luxon';
|
||||
import { Text } from '@immich/ui';
|
||||
import { DateTime, Info } from 'luxon';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
type Props = {
|
||||
@@ -12,20 +13,8 @@
|
||||
|
||||
const { data, itemLabel, totalLabel }: Props = $props();
|
||||
|
||||
const { rows } = $derived.by(() => {
|
||||
const weeks = Array.from({ length: Math.ceil(data.series.length / 7) }, (_, index) =>
|
||||
data.series.slice(index * 7, index * 7 + 7),
|
||||
);
|
||||
|
||||
const rows = Array.from({ length: 7 }, (_, dayIndex) => weeks.map((week) => week[dayIndex]).filter(Boolean));
|
||||
const endDate = DateTime.fromISO(data.to, { zone: 'utc' });
|
||||
|
||||
const months = Array.from({ length: 4 }, (_, index) =>
|
||||
endDate.minus({ months: 11 - index * 4 }).toLocaleString({ month: 'short' }, { locale: $locale }),
|
||||
);
|
||||
|
||||
return { rows, months };
|
||||
});
|
||||
const startDate = $derived(DateTime.fromISO(data.from, { zone: 'utc' }));
|
||||
const padding = $derived(startDate.diff(startDate.startOf('week', { useLocaleWeeks: true })).as('days'));
|
||||
|
||||
const maxCount = $derived(Math.max(...data.series.map((item) => item.count), 0));
|
||||
|
||||
@@ -49,45 +38,47 @@
|
||||
return 'bg-immich-primary';
|
||||
};
|
||||
|
||||
// const dayLabels = $derived([
|
||||
// '',
|
||||
// dayOfWeek('monday', { locale: $locale, style: 'short' }),
|
||||
// '',
|
||||
// dayOfWeek('wednesday', { locale: $locale, style: 'short' }),
|
||||
// '',
|
||||
// dayOfWeek('friday', { locale: $locale, style: 'short' }),
|
||||
// '',
|
||||
// ]);
|
||||
const weekdays = $derived([
|
||||
Info.weekdays('short', { locale: $locale })[0],
|
||||
Info.weekdays('short', { locale: $locale })[2],
|
||||
Info.weekdays('short', { locale: $locale })[4],
|
||||
Info.weekdays('short', { locale: $locale })[6],
|
||||
]);
|
||||
</script>
|
||||
|
||||
<div class="mt-4 w-full">
|
||||
<div class="relative w-full">
|
||||
<!-- TODO -->
|
||||
<!-- <div class="absolute top-4 left-0 flex flex-col gap-0.5">
|
||||
{#each dayLabels as dayLabel, i (i)}
|
||||
<div class="relative flex h-3 w-6 items-center text-xs text-gray-500 dark:text-gray-400">
|
||||
{dayLabel}
|
||||
</div>
|
||||
{/each}
|
||||
</div> -->
|
||||
|
||||
<!-- <div class="mb-1 flex justify-between text-xs text-gray-500 dark:text-gray-400">
|
||||
{#each getUploadActivityMonths() as month (month)}
|
||||
<div>{month}</div>
|
||||
{/each}
|
||||
</div> -->
|
||||
|
||||
<div class="grid grid-rows-7 gap-0.5">
|
||||
{#each rows as row, dayIndex (dayIndex)}
|
||||
<div class="grid grid-cols-52 gap-0.5">
|
||||
{#each row as day (day.date)}
|
||||
<div
|
||||
class="aspect-square w-full min-w-0 rounded-sm {itemColors(day.count)}"
|
||||
title={itemLabel({ date: day.date, count: day.count })}
|
||||
aria-label={itemLabel({ date: day.date, count: day.count })}
|
||||
></div>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="grid grid-flow-col grid-rows-7 gap-0.5">
|
||||
<div class="row-span-7 grid grid-rows-subgrid">
|
||||
{#if Info.getStartOfWeek({ locale: $locale }) === 7}
|
||||
<div></div>
|
||||
{/if}
|
||||
<div class="row-span-2 -mt-1"><Text size="tiny" class="mr-0.5 font-mono">{weekdays[0]}</Text></div>
|
||||
<div class="row-span-2 -mt-1"><Text size="tiny" class="mr-0.5 font-mono">{weekdays[1]}</Text></div>
|
||||
<div class="row-span-2 -mt-1"><Text size="tiny" class="mr-0.5 font-mono">{weekdays[2]}</Text></div>
|
||||
{#if Info.getStartOfWeek({ locale: $locale }) === 1}
|
||||
<div class="-my-1"><Text size="tiny" class="mr-0.5 font-mono">{weekdays[3]}</Text></div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#each data.series as day, idx (day.date)}
|
||||
{@const date = DateTime.fromISO(day.date, { zone: 'utc' }).toLocaleString(
|
||||
{ month: 'short', day: 'numeric' },
|
||||
{ locale: $locale },
|
||||
)}
|
||||
<div
|
||||
class="aspect-square size-full rounded-sm {itemColors(day.count)} row-start-(--heatmap-row-start)"
|
||||
style:--heatmap-row-start={idx === 0 ? padding + 1 : undefined}
|
||||
title={itemLabel({ date, count: day.count })}
|
||||
aria-label={itemLabel({ date, count: day.count })}
|
||||
></div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user