mirror of
https://github.com/immich-app/immich.git
synced 2026-06-04 05:45:24 -04:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 19fbc11c33 | |||
| 932302c5ab | |||
| 1bb7517da0 | |||
| 814c2e32e4 | |||
| 92841f311f | |||
| 9d2e576630 | |||
| 936418a464 | |||
| 84c75d95c7 |
@@ -1 +0,0 @@
|
|||||||
custom: ['https://buy.immich.app', 'https://immich.store']
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
# Contributor Covenant Code of Conduct
|
|
||||||
|
|
||||||
## Our Pledge
|
|
||||||
|
|
||||||
We as members, contributors, and leaders pledge to make participation
|
|
||||||
in our community a harassment-free experience for everyone, regardless
|
|
||||||
of age, body size, visible or invisible disability, ethnicity, sex
|
|
||||||
characteristics, gender identity and expression, level of experience,
|
|
||||||
education, socio-economic status, nationality, personal appearance,
|
|
||||||
race, religion, or sexual identity and orientation.
|
|
||||||
|
|
||||||
We pledge to act and interact in ways that contribute to an open,
|
|
||||||
welcoming, diverse, inclusive, and healthy community.
|
|
||||||
|
|
||||||
## Our Standards
|
|
||||||
|
|
||||||
Examples of behavior that contributes to a positive environment for
|
|
||||||
our community include:
|
|
||||||
|
|
||||||
- Demonstrating empathy and kindness toward other people
|
|
||||||
- Being respectful of differing opinions, viewpoints, and experiences
|
|
||||||
- Giving and gracefully accepting constructive feedback
|
|
||||||
- Accepting responsibility and apologizing to those affected by our
|
|
||||||
mistakes, and learning from the experience
|
|
||||||
- Focusing on what is best not just for us as individuals, but for the
|
|
||||||
overall community
|
|
||||||
|
|
||||||
Examples of unacceptable behavior include:
|
|
||||||
|
|
||||||
- The use of sexualized language or imagery, and sexual attention or
|
|
||||||
advances of any kind
|
|
||||||
- Trolling, insulting or derogatory comments, and personal or
|
|
||||||
political attacks
|
|
||||||
- Public or private harassment
|
|
||||||
- Publishing others' private information, such as a physical or email
|
|
||||||
address, without their explicit permission
|
|
||||||
- Other conduct which could reasonably be considered inappropriate in
|
|
||||||
a professional setting
|
|
||||||
|
|
||||||
## Enforcement Responsibilities
|
|
||||||
|
|
||||||
Community leaders are responsible for clarifying and enforcing our
|
|
||||||
standards of acceptable behavior and will take appropriate and fair
|
|
||||||
corrective action in response to any behavior that they deem
|
|
||||||
inappropriate, threatening, offensive, or harmful.
|
|
||||||
|
|
||||||
Community leaders have the right and responsibility to remove, edit,
|
|
||||||
or reject comments, commits, code, wiki edits, issues, and other
|
|
||||||
contributions that are not aligned to this Code of Conduct, and will
|
|
||||||
communicate reasons for moderation decisions when appropriate.
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
|
|
||||||
This Code of Conduct applies within all community spaces, and also
|
|
||||||
applies when an individual is officially representing the community in
|
|
||||||
public spaces. Examples of representing our community include using an
|
|
||||||
official e-mail address, posting via an official social media account,
|
|
||||||
or acting as an appointed representative at an online or offline
|
|
||||||
event.
|
|
||||||
|
|
||||||
## Enforcement
|
|
||||||
|
|
||||||
Instances of abusive, harassing, or otherwise unacceptable behavior
|
|
||||||
may be reported to the community leaders responsible for enforcement
|
|
||||||
at our Discord channel. All complaints
|
|
||||||
will be reviewed and investigated promptly and fairly.
|
|
||||||
|
|
||||||
All community leaders are obligated to respect the privacy and
|
|
||||||
security of the reporter of any incident.
|
|
||||||
|
|
||||||
## Enforcement Guidelines
|
|
||||||
|
|
||||||
Community leaders will follow these Community Impact Guidelines in
|
|
||||||
determining the consequences for any action they deem in violation of
|
|
||||||
this Code of Conduct:
|
|
||||||
|
|
||||||
### 1. Correction
|
|
||||||
|
|
||||||
**Community Impact**: Use of inappropriate language or other behavior
|
|
||||||
deemed unprofessional or unwelcome in the community.
|
|
||||||
|
|
||||||
**Consequence**: A private, written warning from community leaders,
|
|
||||||
providing clarity around the nature of the violation and an
|
|
||||||
explanation of why the behavior was inappropriate. A public apology
|
|
||||||
may be requested.
|
|
||||||
|
|
||||||
### 2. Warning
|
|
||||||
|
|
||||||
**Community Impact**: A violation through a single incident or series
|
|
||||||
of actions.
|
|
||||||
|
|
||||||
**Consequence**: A warning with consequences for continued
|
|
||||||
behavior. No interaction with the people involved, including
|
|
||||||
unsolicited interaction with those enforcing the Code of Conduct, for
|
|
||||||
a specified period of time. This includes avoiding interactions in
|
|
||||||
community spaces as well as external channels like social
|
|
||||||
media. Violating these terms may lead to a temporary or permanent ban.
|
|
||||||
|
|
||||||
### 3. Temporary Ban
|
|
||||||
|
|
||||||
**Community Impact**: A serious violation of community standards,
|
|
||||||
including sustained inappropriate behavior.
|
|
||||||
|
|
||||||
**Consequence**: A temporary ban from any sort of interaction or
|
|
||||||
public communication with the community for a specified period of
|
|
||||||
time. No public or private interaction with the people involved,
|
|
||||||
including unsolicited interaction with those enforcing the Code of
|
|
||||||
Conduct, is allowed during this period. Violating these terms may lead
|
|
||||||
to a permanent ban.
|
|
||||||
|
|
||||||
### 4. Permanent Ban
|
|
||||||
|
|
||||||
**Community Impact**: Demonstrating a pattern of violation of
|
|
||||||
community standards, including sustained inappropriate behavior,
|
|
||||||
harassment of an individual, or aggression toward or disparagement of
|
|
||||||
classes of individuals.
|
|
||||||
|
|
||||||
**Consequence**: A permanent ban from any sort of public interaction
|
|
||||||
within the community.
|
|
||||||
|
|
||||||
## Attribution
|
|
||||||
|
|
||||||
This Code of Conduct is adapted from the [Contributor
|
|
||||||
Covenant][homepage], version 2.0, available at
|
|
||||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
|
||||||
|
|
||||||
Community Impact Guidelines were inspired by [Mozilla's code of
|
|
||||||
conduct enforcement ladder](https://github.com/mozilla/diversity).
|
|
||||||
|
|
||||||
[homepage]: https://www.contributor-covenant.org
|
|
||||||
|
|
||||||
For answers to common questions about this code of conduct, see the
|
|
||||||
FAQ at https://www.contributor-covenant.org/faq. Translations are
|
|
||||||
available at https://www.contributor-covenant.org/translations.
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
# Security Policy
|
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
|
||||||
|
|
||||||
Please report security issues to `security@immich.app`
|
|
||||||
@@ -1,30 +1,30 @@
|
|||||||
# @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html
|
# @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html
|
||||||
|
|
||||||
[[tools."aqua:flutter/flutter"]]
|
[[tools."aqua:flutter/flutter"]]
|
||||||
version = "3.44.0"
|
version = "3.44.1"
|
||||||
backend = "aqua:flutter/flutter"
|
backend = "aqua:flutter/flutter"
|
||||||
|
|
||||||
[tools."aqua:flutter/flutter"."platforms.linux-arm64"]
|
[tools."aqua:flutter/flutter"."platforms.linux-arm64"]
|
||||||
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.0-stable.tar.xz"
|
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.1-stable.tar.xz"
|
||||||
|
|
||||||
[tools."aqua:flutter/flutter"."platforms.linux-arm64-musl"]
|
[tools."aqua:flutter/flutter"."platforms.linux-arm64-musl"]
|
||||||
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.0-stable.tar.xz"
|
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.1-stable.tar.xz"
|
||||||
|
|
||||||
[tools."aqua:flutter/flutter"."platforms.linux-x64"]
|
[tools."aqua:flutter/flutter"."platforms.linux-x64"]
|
||||||
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.0-stable.tar.xz"
|
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.1-stable.tar.xz"
|
||||||
|
|
||||||
[tools."aqua:flutter/flutter"."platforms.linux-x64-musl"]
|
[tools."aqua:flutter/flutter"."platforms.linux-x64-musl"]
|
||||||
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.0-stable.tar.xz"
|
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.1-stable.tar.xz"
|
||||||
|
|
||||||
[tools."aqua:flutter/flutter"."platforms.macos-arm64"]
|
[tools."aqua:flutter/flutter"."platforms.macos-arm64"]
|
||||||
checksum = "blake3:fb03aa5d9790205c948922ec3f0751c16e4575b09d6ae9dd4fbeb664a69f0e00"
|
checksum = "blake3:15069c982a30ca0189a83edb5627b69d91485ad94fb74d2de8585b43364e9e8e"
|
||||||
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_macos_arm64_3.44.0-stable.zip"
|
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_macos_arm64_3.44.1-stable.zip"
|
||||||
|
|
||||||
[tools."aqua:flutter/flutter"."platforms.macos-x64"]
|
[tools."aqua:flutter/flutter"."platforms.macos-x64"]
|
||||||
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_macos_3.44.0-stable.zip"
|
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_macos_3.44.1-stable.zip"
|
||||||
|
|
||||||
[tools."aqua:flutter/flutter"."platforms.windows-x64"]
|
[tools."aqua:flutter/flutter"."platforms.windows-x64"]
|
||||||
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/windows/flutter_windows_3.44.0-stable.zip"
|
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/windows/flutter_windows_3.44.1-stable.zip"
|
||||||
|
|
||||||
[[tools.flutter]]
|
[[tools.flutter]]
|
||||||
version = "3.41.9-stable"
|
version = "3.41.9-stable"
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ config_roots = [
|
|||||||
|
|
||||||
[tools]
|
[tools]
|
||||||
node = "24.15.0"
|
node = "24.15.0"
|
||||||
"aqua:flutter/flutter" = "3.44.0"
|
"aqua:flutter/flutter" = "3.44.1"
|
||||||
pnpm = "10.33.4"
|
pnpm = "10.33.4"
|
||||||
terragrunt = "1.0.3"
|
terragrunt = "1.0.3"
|
||||||
opentofu = "1.11.6"
|
opentofu = "1.11.6"
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:immich_mobile/constants/colors.dart';
|
import 'package:immich_mobile/constants/colors.dart';
|
||||||
import 'package:immich_mobile/constants/enums.dart';
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
import 'package:immich_mobile/domain/models/log.model.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/timeline.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/value_codec.dart';
|
||||||
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
||||||
|
|
||||||
enum SettingsKey<T extends Object> {
|
enum SettingsKey<T extends Object> {
|
||||||
// Theme
|
// Theme
|
||||||
themePrimaryColor<ImmichColorPreset>(codec: _EnumCodec(ImmichColorPreset.values)),
|
themePrimaryColor<ImmichColorPreset>(codec: EnumCodec(ImmichColorPreset.values)),
|
||||||
themeMode<ThemeMode>(codec: _EnumCodec(ThemeMode.values)),
|
themeMode<ThemeMode>(codec: EnumCodec(ThemeMode.values)),
|
||||||
themeDynamic<bool>(),
|
themeDynamic<bool>(),
|
||||||
themeColorfulInterface<bool>(),
|
themeColorfulInterface<bool>(),
|
||||||
|
|
||||||
@@ -28,11 +27,11 @@ enum SettingsKey<T extends Object> {
|
|||||||
networkAutoEndpointSwitching<bool>(),
|
networkAutoEndpointSwitching<bool>(),
|
||||||
networkPreferredWifiName<String>(),
|
networkPreferredWifiName<String>(),
|
||||||
networkLocalEndpoint<String>(),
|
networkLocalEndpoint<String>(),
|
||||||
networkExternalEndpointList<List<String>>(codec: _ListCodec(_PrimitiveCodec.string)),
|
networkExternalEndpointList<List<String>>(codec: ListCodec(PrimitiveCodec.string)),
|
||||||
networkCustomHeaders<Map<String, String>>(codec: _MapCodec(_PrimitiveCodec.string, _PrimitiveCodec.string)),
|
networkCustomHeaders<Map<String, String>>(codec: MapCodec(PrimitiveCodec.string, PrimitiveCodec.string)),
|
||||||
|
|
||||||
// Album
|
// Album
|
||||||
albumSortMode<AlbumSortMode>(codec: _EnumCodec(AlbumSortMode.values)),
|
albumSortMode<AlbumSortMode>(codec: EnumCodec(AlbumSortMode.values)),
|
||||||
albumIsReverse<bool>(),
|
albumIsReverse<bool>(),
|
||||||
albumIsGrid<bool>(),
|
albumIsGrid<bool>(),
|
||||||
|
|
||||||
@@ -46,23 +45,23 @@ enum SettingsKey<T extends Object> {
|
|||||||
|
|
||||||
// Timeline
|
// Timeline
|
||||||
timelineTilesPerRow<int>(),
|
timelineTilesPerRow<int>(),
|
||||||
timelineGroupAssetsBy<GroupAssetsBy>(codec: _EnumCodec(GroupAssetsBy.values)),
|
timelineGroupAssetsBy<GroupAssetsBy>(codec: EnumCodec(GroupAssetsBy.values)),
|
||||||
timelineStorageIndicator<bool>(),
|
timelineStorageIndicator<bool>(),
|
||||||
|
|
||||||
// Log
|
// Log
|
||||||
logLevel<LogLevel>(codec: _EnumCodec(LogLevel.values)),
|
logLevel<LogLevel>(codec: EnumCodec(LogLevel.values)),
|
||||||
|
|
||||||
// Map
|
// Map
|
||||||
mapShowFavoriteOnly<bool>(),
|
mapShowFavoriteOnly<bool>(),
|
||||||
mapRelativeDate<int>(),
|
mapRelativeDate<int>(),
|
||||||
mapIncludeArchived<bool>(),
|
mapIncludeArchived<bool>(),
|
||||||
mapThemeMode<ThemeMode>(codec: _EnumCodec(ThemeMode.values)),
|
mapThemeMode<ThemeMode>(codec: EnumCodec(ThemeMode.values)),
|
||||||
mapWithPartners<bool>(),
|
mapWithPartners<bool>(),
|
||||||
|
|
||||||
// Cleanup
|
// Cleanup
|
||||||
cleanupKeepFavorites<bool>(),
|
cleanupKeepFavorites<bool>(),
|
||||||
cleanupKeepMediaType<AssetKeepType>(codec: _EnumCodec(AssetKeepType.values)),
|
cleanupKeepMediaType<AssetKeepType>(codec: EnumCodec(AssetKeepType.values)),
|
||||||
cleanupKeepAlbumIds<List<String>>(codec: _ListCodec(_PrimitiveCodec.string)),
|
cleanupKeepAlbumIds<List<String>>(codec: ListCodec(PrimitiveCodec.string)),
|
||||||
cleanupCutoffDaysAgo<int>(),
|
cleanupCutoffDaysAgo<int>(),
|
||||||
cleanupDefaultsInitialized<bool>(),
|
cleanupDefaultsInitialized<bool>(),
|
||||||
|
|
||||||
@@ -70,148 +69,16 @@ enum SettingsKey<T extends Object> {
|
|||||||
slideshowTransition<bool>(),
|
slideshowTransition<bool>(),
|
||||||
slideshowRepeat<bool>(),
|
slideshowRepeat<bool>(),
|
||||||
slideshowDuration<int>(),
|
slideshowDuration<int>(),
|
||||||
slideshowLook<SlideshowLook>(codec: _EnumCodec(SlideshowLook.values)),
|
slideshowLook<SlideshowLook>(codec: EnumCodec(SlideshowLook.values)),
|
||||||
slideshowDirection<SlideshowDirection>(codec: _EnumCodec(SlideshowDirection.values));
|
slideshowDirection<SlideshowDirection>(codec: EnumCodec(SlideshowDirection.values));
|
||||||
|
|
||||||
final _SettingsCodec<T>? _codecOverride;
|
final ValueCodec<T>? _codecOverride;
|
||||||
|
|
||||||
const SettingsKey({_SettingsCodec<T>? codec}) : _codecOverride = codec;
|
const SettingsKey({ValueCodec<T>? codec}) : _codecOverride = codec;
|
||||||
|
|
||||||
_SettingsCodec<T> get _codec => _codecOverride ?? _SettingsCodec.forType(T);
|
ValueCodec<T> get _codec => _codecOverride ?? ValueCodec.forType(T);
|
||||||
|
|
||||||
String encode(T value) => _codec.encode(value);
|
String encode(T value) => _codec.encode(value);
|
||||||
|
|
||||||
T decode(String raw) => _codec.decode(raw);
|
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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
sealed class ValueCodec<T extends Object> {
|
||||||
|
const ValueCodec();
|
||||||
|
|
||||||
|
String encode(T value);
|
||||||
|
T decode(String raw);
|
||||||
|
|
||||||
|
static const Map<Type, ValueCodec<Object>> _primitives = {
|
||||||
|
int: PrimitiveCodec.integer,
|
||||||
|
double: PrimitiveCodec.real,
|
||||||
|
bool: PrimitiveCodec.boolean,
|
||||||
|
String: PrimitiveCodec.string,
|
||||||
|
DateTime: DateTimeCodec(),
|
||||||
|
};
|
||||||
|
|
||||||
|
static ValueCodec<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 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) {
|
||||||
|
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 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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
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) {
|
||||||
|
final overrides = <K, Object>{};
|
||||||
|
for (final row in rows) {
|
||||||
|
final key = keys.firstWhereOrNull((k) => k.name == row.key);
|
||||||
|
if (key == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
overrides[key] = decodeValue(key, row.value);
|
||||||
|
}
|
||||||
|
return buildSnapshot(overrides);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:immich_mobile/domain/models/config/app_config.dart';
|
import 'package:immich_mobile/domain/models/config/app_config.dart';
|
||||||
import 'package:immich_mobile/domain/models/settings_key.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/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';
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
|
|
||||||
class SettingsRepository extends DriftDatabaseRepository {
|
class SettingsRepository extends CachedKeyValueRepository<SettingsKey, AppConfig> {
|
||||||
final Drift _db;
|
final Drift _db;
|
||||||
|
|
||||||
SettingsRepository._(this._db) : super(_db);
|
SettingsRepository._(this._db) : super(const .new());
|
||||||
|
|
||||||
static SettingsRepository? _instance;
|
static SettingsRepository? _instance;
|
||||||
|
|
||||||
@@ -20,9 +20,6 @@ class SettingsRepository extends DriftDatabaseRepository {
|
|||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
AppConfig _appConfig = const .new();
|
|
||||||
AppConfig get appConfig => _appConfig;
|
|
||||||
|
|
||||||
static Future<SettingsRepository> ensureInitialized(Drift db) async {
|
static Future<SettingsRepository> ensureInitialized(Drift db) async {
|
||||||
if (_instance == null) {
|
if (_instance == null) {
|
||||||
final instance = SettingsRepository._(db);
|
final instance = SettingsRepository._(db);
|
||||||
@@ -32,7 +29,20 @@ class SettingsRepository extends DriftDatabaseRepository {
|
|||||||
return _instance!;
|
return _instance!;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> refresh() async => _applyOverrides(await _db.select(_db.settingsEntity).get());
|
@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> clear(Iterable<SettingsKey> keys) async {
|
Future<void> clear(Iterable<SettingsKey> keys) async {
|
||||||
if (keys.isEmpty) {
|
if (keys.isEmpty) {
|
||||||
@@ -42,13 +52,15 @@ class SettingsRepository extends DriftDatabaseRepository {
|
|||||||
final names = keys.map((key) => key.name).toList();
|
final names = keys.map((key) => key.name).toList();
|
||||||
await (_db.delete(_db.settingsEntity)..where((row) => row.key.isIn(names))).go();
|
await (_db.delete(_db.settingsEntity)..where((row) => row.key.isIn(names))).go();
|
||||||
|
|
||||||
|
var config = snapshot;
|
||||||
for (final key in keys) {
|
for (final key in keys) {
|
||||||
_appConfig = _appConfig.write(key, defaultConfig.read(key));
|
config = config.write(key, defaultConfig.read(key));
|
||||||
}
|
}
|
||||||
|
snapshot = config;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> write<T extends Object, U extends T>(SettingsKey<T> key, U value) async {
|
Future<void> write<T extends Object, U extends T>(SettingsKey<T> key, U value) async {
|
||||||
if (value == _appConfig.read(key)) {
|
if (value == snapshot.read(key)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,24 +73,8 @@ class SettingsRepository extends DriftDatabaseRepository {
|
|||||||
.insertOnConflictUpdate(
|
.insertOnConflictUpdate(
|
||||||
SettingsEntityCompanion.insert(key: key.name, value: key.encode(value), updatedAt: Value(DateTime.now())),
|
SettingsEntityCompanion.insert(key: key.name, value: key.encode(value), updatedAt: Value(DateTime.now())),
|
||||||
);
|
);
|
||||||
_appConfig = _appConfig.write(key, value);
|
snapshot = snapshot.write(key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream<AppConfig> watchConfig() => _db.select(_db.settingsEntity).watch().map((rows) {
|
Stream<AppConfig> watchConfig() => watchSnapshot();
|
||||||
_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)};
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ dynamic upgradeDto(dynamic value, String targetType) {
|
|||||||
if (value is Map) {
|
if (value is Map) {
|
||||||
addDefault(value, 'mapLightStyleUrl', 'https://tiles.immich.cloud/v1/style/light.json');
|
addDefault(value, 'mapLightStyleUrl', 'https://tiles.immich.cloud/v1/style/light.json');
|
||||||
addDefault(value, 'mapDarkStyleUrl', 'https://tiles.immich.cloud/v1/style/dark.json');
|
addDefault(value, 'mapDarkStyleUrl', 'https://tiles.immich.cloud/v1/style/dark.json');
|
||||||
|
addDefault(value, 'minFaces', 3);
|
||||||
}
|
}
|
||||||
case 'UserResponseDto':
|
case 'UserResponseDto':
|
||||||
if (value is Map) {
|
if (value is Map) {
|
||||||
@@ -54,6 +55,7 @@ dynamic upgradeDto(dynamic value, String targetType) {
|
|||||||
case 'ServerFeaturesDto':
|
case 'ServerFeaturesDto':
|
||||||
if (value is Map) {
|
if (value is Map) {
|
||||||
addDefault(value, 'ocr', false);
|
addDefault(value, 'ocr', false);
|
||||||
|
addDefault(value, 'realtimeTranscoding', false);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'MemoriesResponse':
|
case 'MemoriesResponse':
|
||||||
|
|||||||
+1
-1
@@ -1997,4 +1997,4 @@ packages:
|
|||||||
version: "3.1.3"
|
version: "3.1.3"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.12.0 <4.0.0"
|
dart: ">=3.12.0 <4.0.0"
|
||||||
flutter: "3.44.0"
|
flutter: "3.44.1"
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ version: 3.0.0+3047
|
|||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.12.0 <4.0.0'
|
sdk: '>=3.12.0 <4.0.0'
|
||||||
flutter: 3.44.0
|
flutter: 3.44.1
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
async: ^2.13.1
|
async: ^2.13.1
|
||||||
|
|||||||
Reference in New Issue
Block a user