mirror of
https://github.com/immich-app/immich.git
synced 2026-07-22 05:44:03 +03:00
Compare commits
19 Commits
fix/ios-ex
...
refactor/s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ab8c6e3db | ||
|
|
865adba8c1 | ||
|
|
9136a72712 | ||
|
|
1b1b14e3e4 | ||
|
|
5c5b41500b | ||
|
|
00542011fb | ||
|
|
f8be7dc573 | ||
|
|
899f547053 | ||
|
|
4938fd4c89 | ||
|
|
e81a79224c | ||
|
|
7b0f58b2c1 | ||
|
|
ae8398ffe4 | ||
|
|
aa08dad1f5 | ||
|
|
ce022233ae | ||
|
|
1b4d41324e | ||
|
|
8365cdd59e | ||
|
|
1cec7021e8 | ||
|
|
1b0f473457 | ||
|
|
e918658cc1 |
@@ -154,7 +154,7 @@ services:
|
||||
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
image: docker.io/valkey/valkey:9@sha256:4963247afc4cd33c7d3b2d2816b9f7f8eeebab148d29056c2ca4d7cbc966f2d9
|
||||
image: docker.io/valkey/valkey:9@sha256:8e8d64b405ce18f41b8e5ee20aa4687a8ed0022d1298f2ce31cdcf3a76e09411
|
||||
healthcheck:
|
||||
test: redis-cli ping || exit 1
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ services:
|
||||
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
image: docker.io/valkey/valkey:9@sha256:4963247afc4cd33c7d3b2d2816b9f7f8eeebab148d29056c2ca4d7cbc966f2d9
|
||||
image: docker.io/valkey/valkey:9@sha256:8e8d64b405ce18f41b8e5ee20aa4687a8ed0022d1298f2ce31cdcf3a76e09411
|
||||
healthcheck:
|
||||
test: redis-cli ping || exit 1
|
||||
restart: always
|
||||
|
||||
@@ -61,7 +61,7 @@ services:
|
||||
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
image: docker.io/valkey/valkey:9@sha256:4963247afc4cd33c7d3b2d2816b9f7f8eeebab148d29056c2ca4d7cbc966f2d9
|
||||
image: docker.io/valkey/valkey:9@sha256:8e8d64b405ce18f41b8e5ee20aa4687a8ed0022d1298f2ce31cdcf3a76e09411
|
||||
user: '1000:1000'
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
|
||||
@@ -49,7 +49,7 @@ services:
|
||||
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
image: docker.io/valkey/valkey:9@sha256:4963247afc4cd33c7d3b2d2816b9f7f8eeebab148d29056c2ca4d7cbc966f2d9
|
||||
image: docker.io/valkey/valkey:9@sha256:8e8d64b405ce18f41b8e5ee20aa4687a8ed0022d1298f2ce31cdcf3a76e09411
|
||||
healthcheck:
|
||||
test: redis-cli ping || exit 1
|
||||
restart: always
|
||||
|
||||
@@ -28,4 +28,4 @@ run = "prettier --write ."
|
||||
run = "wrangler pages deploy build --project-name=${PROJECT_NAME} --branch=${BRANCH_NAME}"
|
||||
|
||||
[tools]
|
||||
wrangler = "4.110.0"
|
||||
wrangler = "4.111.0"
|
||||
|
||||
@@ -40,7 +40,8 @@
|
||||
"@docusaurus/tsconfig": "^3.10.0",
|
||||
"@docusaurus/types": "^3.10.0",
|
||||
"prettier": "^3.7.4",
|
||||
"typescript": "^6.0.0"
|
||||
"@typescript/native": "npm:typescript@^7.0.2",
|
||||
"typescript": "npm:@typescript/typescript6@^6.0.2"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
|
||||
@@ -45,7 +45,7 @@ services:
|
||||
|
||||
redis:
|
||||
container_name: immich-e2e-redis
|
||||
image: docker.io/valkey/valkey:9@sha256:4963247afc4cd33c7d3b2d2816b9f7f8eeebab148d29056c2ca4d7cbc966f2d9
|
||||
image: docker.io/valkey/valkey:9@sha256:8e8d64b405ce18f41b8e5ee20aa4687a8ed0022d1298f2ce31cdcf3a76e09411
|
||||
healthcheck:
|
||||
test: redis-cli ping || exit 1
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ export default typescriptEslint.config([
|
||||
'unicorn/isolated-functions': 'off',
|
||||
'unicorn/prefer-promise-with-resolvers': 'off',
|
||||
'unicorn/no-declarations-before-early-exit': 'off',
|
||||
'unicorn/prefer-simple-condition-first': 'off',
|
||||
curly: 2,
|
||||
'prettier/prettier': 0,
|
||||
'unicorn/name-replacements': 'off',
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
"eslint": "^10.0.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-unicorn": "^70.0.0",
|
||||
"eslint-plugin-unicorn": "^72.0.0",
|
||||
"exiftool-vendored": "^35.0.0",
|
||||
"globals": "^17.0.0",
|
||||
"luxon": "^3.4.4",
|
||||
@@ -51,7 +51,8 @@
|
||||
"sharp": "^0.34.5",
|
||||
"socket.io-client": "^4.7.4",
|
||||
"supertest": "^7.0.0",
|
||||
"typescript": "^6.0.0",
|
||||
"@typescript/native": "npm:typescript@^7.0.2",
|
||||
"typescript": "npm:@typescript/typescript6@^6.0.2",
|
||||
"typescript-eslint": "^8.28.0",
|
||||
"utimes": "^5.2.1",
|
||||
"vitest": "^4.0.0"
|
||||
|
||||
@@ -38,7 +38,7 @@ test.describe('asset-viewer', () => {
|
||||
assets.push(...timeBucket);
|
||||
}
|
||||
for (const yearMonth of timelineRestData.buckets.keys()) {
|
||||
const [year, month] = yearMonth.split('-');
|
||||
const [year, month] = yearMonth.split('-', 2);
|
||||
yearMonths.push(`${year}-${Number(month)}`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -49,7 +49,7 @@ test.describe('Timeline', () => {
|
||||
assets.push(...timeBucket);
|
||||
}
|
||||
for (const yearMonth of timelineRestData.buckets.keys()) {
|
||||
const [year, month] = yearMonth.split('-');
|
||||
const [year, month] = yearMonth.split('-', 2);
|
||||
yearMonths.push(`${year}-${Number(month)}`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@ export const sleep = (ms: number) => {
|
||||
};
|
||||
|
||||
export const padYearMonth = (yearMonth: string) => {
|
||||
const [year, month] = yearMonth.split('-');
|
||||
const [year, month] = yearMonth.split('-', 2);
|
||||
return `${year}-${month.padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
|
||||
38
mise.lock
38
mise.lock
@@ -287,43 +287,43 @@ url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.
|
||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/391536406"
|
||||
|
||||
[[tools.pnpm]]
|
||||
version = "11.11.0"
|
||||
version = "11.13.1"
|
||||
backend = "aqua:pnpm/pnpm"
|
||||
|
||||
[tools.pnpm."platforms.linux-arm64"]
|
||||
checksum = "sha256:4871093439f036b3082df6102255ac119f3b6f4f50c51c5711fc7c358b8b22d7"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.11.0/pnpm-linux-arm64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/471734816"
|
||||
checksum = "sha256:b52db99d215ed7dc9563aed815953c62a6c1ffd7cd75803d3a07ad7e4f246aed"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.13.1/pnpm-linux-arm64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/478563983"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.pnpm."platforms.linux-arm64-musl"]
|
||||
checksum = "sha256:f97316cee0deb3ae3fd64e8a09e1c8c88a76da33373a2b465bd6a4c3e85e0690"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.11.0/pnpm-linux-arm64-musl.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/471734819"
|
||||
checksum = "sha256:2cadd4fc815c591f498a0a84c9e74a836e3e8c1236275f6e1cfd355bae6ae957"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.13.1/pnpm-linux-arm64-musl.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/478563985"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.pnpm."platforms.linux-x64"]
|
||||
checksum = "sha256:df4699e897012ab14df2cc6eaa942910e830eb7fcaa420a2a1421a9461fd9108"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.11.0/pnpm-linux-x64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/471734818"
|
||||
checksum = "sha256:bd6d4b0e14536207ad76bc838f5980cecd968da15f69aae0b207380cca3f2e98"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.13.1/pnpm-linux-x64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/478563986"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.pnpm."platforms.linux-x64-musl"]
|
||||
checksum = "sha256:aad186f8a8ae72d827d5efe63e99e801456715fb7f2798949405fa33f20260db"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.11.0/pnpm-linux-x64-musl.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/471734821"
|
||||
checksum = "sha256:ba19690f4ed1b64f1203ade14e9216352b46232d5582468b26a0160e0c9618c5"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.13.1/pnpm-linux-x64-musl.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/478563980"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.pnpm."platforms.macos-arm64"]
|
||||
checksum = "sha256:ad46ad16c2376c2b78354ac488c1b166e076aa59bfcbcfd567d55957d755a690"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.11.0/pnpm-darwin-arm64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/471734814"
|
||||
checksum = "sha256:765c2bf04e8129cb58c0f946e324262e418370b35a203b50b1f06a0567ef8bc1"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.13.1/pnpm-darwin-arm64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/478563984"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.pnpm."platforms.windows-x64"]
|
||||
checksum = "sha256:13ad8a9b139c4c829fea33462cf04d061d72f0926b2b76a92ac03c9a36e405f9"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.11.0/pnpm-win32-x64.zip"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/471734820"
|
||||
checksum = "sha256:d8bebbc71df2702961c1d34a5e61196bc0aa3bbde33c33253f6afa3dd4546a6d"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.13.1/pnpm-win32-x64.zip"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/478563981"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[[tools.terragrunt]]
|
||||
|
||||
@@ -16,7 +16,7 @@ config_roots = [
|
||||
|
||||
[tools]
|
||||
node = "24.15.0"
|
||||
pnpm = "11.11.0"
|
||||
pnpm = "11.13.1"
|
||||
terragrunt = "1.0.3"
|
||||
opentofu = "1.11.6"
|
||||
"npm:oazapfts" = "7.5.0"
|
||||
|
||||
@@ -23,6 +23,6 @@ class ImmichApp : Application() {
|
||||
// as the previous start might have been killed without unlocking.
|
||||
if (BackgroundEngineLock.connectEngines > 0) return@postDelayed
|
||||
BackgroundWorkerApiImpl.enqueueBackgroundWorker(this)
|
||||
}, 5000)
|
||||
}, 15000)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import androidx.work.ListenableWorker
|
||||
import androidx.work.WorkerParameters
|
||||
import app.alextran.immich.MainActivity
|
||||
import app.alextran.immich.R
|
||||
import com.google.common.util.concurrent.Futures
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.common.util.concurrent.SettableFuture
|
||||
import io.flutter.FlutterInjector
|
||||
@@ -61,6 +62,11 @@ class BackgroundWorker(context: Context, params: WorkerParameters) :
|
||||
}
|
||||
|
||||
override fun startWork(): ListenableFuture<Result> {
|
||||
if (BackgroundWorkerPreferences(ctx).isLocked() && BackgroundEngineLock.connectEngines > 0) {
|
||||
Log.i(TAG, "Foreground engine active, skipping background worker")
|
||||
return Futures.immediateFuture(Result.success())
|
||||
}
|
||||
|
||||
Log.i(TAG, "Starting background upload worker")
|
||||
|
||||
if (!loader.initialized()) {
|
||||
@@ -77,6 +83,10 @@ class BackgroundWorker(context: Context, params: WorkerParameters) :
|
||||
showNotification(notificationConfig.first, notificationConfig.second)
|
||||
|
||||
loader.ensureInitializationCompleteAsync(ctx, null, Handler(Looper.getMainLooper())) {
|
||||
if (isStopped || isComplete) {
|
||||
return@ensureInitializationCompleteAsync
|
||||
}
|
||||
|
||||
engine = FlutterEngine(ctx)
|
||||
FlutterEngineCache.getInstance().put(BackgroundWorkerApiImpl.ENGINE_CACHE_KEY, engine!!)
|
||||
|
||||
@@ -143,11 +153,17 @@ class BackgroundWorker(context: Context, params: WorkerParameters) :
|
||||
return
|
||||
}
|
||||
|
||||
val api = flutterApi
|
||||
if (api == null) {
|
||||
Handler(Looper.getMainLooper()).postAtFrontOfQueue {
|
||||
complete(Result.failure())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
Handler(Looper.getMainLooper()).postAtFrontOfQueue {
|
||||
if (flutterApi != null) {
|
||||
flutterApi?.cancel {
|
||||
complete(Result.failure())
|
||||
}
|
||||
api.cancel {
|
||||
complete(Result.failure())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
3683
mobile/drift_schemas/main/drift_schema_v32.json
generated
Normal file
3683
mobile/drift_schemas/main/drift_schema_v32.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -40,7 +40,7 @@ void main() {
|
||||
tearDown(() async {
|
||||
await workerManagerPatch.dispose();
|
||||
await server.close();
|
||||
await Store.delete(StoreKey.serverEndpoint);
|
||||
await Store.delete(StoreKey.legacyServerEndpoint);
|
||||
await Store.delete(StoreKey.syncMigrationStatus);
|
||||
});
|
||||
|
||||
@@ -119,7 +119,9 @@ void main() {
|
||||
final releaseTxn = Completer<void>();
|
||||
final txnHeld = Completer<void>();
|
||||
final txn = drift.transaction(() async {
|
||||
await drift.into(drift.userEntity).insert(
|
||||
await drift
|
||||
.into(drift.userEntity)
|
||||
.insert(
|
||||
UserEntityCompanion.insert(
|
||||
id: 'holder',
|
||||
name: 'holder',
|
||||
|
||||
63
mobile/lib/domain/models/session.model.dart
Normal file
63
mobile/lib/domain/models/session.model.dart
Normal file
@@ -0,0 +1,63 @@
|
||||
import 'package:immich_mobile/domain/models/value_codec.dart';
|
||||
import 'package:immich_mobile/utils/option.dart';
|
||||
|
||||
enum SessionKey<T> {
|
||||
serverUrl<String?>(),
|
||||
accessToken<String?>(),
|
||||
serverEndpoint<String?>();
|
||||
|
||||
ValueCodec<T> get _codec => ValueCodec.forType(T);
|
||||
|
||||
String encode(T value) => _codec.encode(value);
|
||||
|
||||
T decode(String raw) => _codec.decode(raw);
|
||||
}
|
||||
|
||||
const defaultSession = Session();
|
||||
|
||||
class Session {
|
||||
final String? serverUrl;
|
||||
final String? accessToken;
|
||||
final String? serverEndpoint;
|
||||
|
||||
const Session({this.serverUrl, this.accessToken, this.serverEndpoint});
|
||||
|
||||
Session copyWith({Option<String>? serverUrl, Option<String>? accessToken, Option<String>? serverEndpoint}) => .new(
|
||||
serverUrl: serverUrl.patch(this.serverUrl),
|
||||
accessToken: accessToken.patch(this.accessToken),
|
||||
serverEndpoint: serverEndpoint.patch(this.serverEndpoint),
|
||||
);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
(other is Session &&
|
||||
other.serverUrl == serverUrl &&
|
||||
other.accessToken == accessToken &&
|
||||
other.serverEndpoint == serverEndpoint);
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(serverUrl, accessToken, serverEndpoint);
|
||||
|
||||
@override
|
||||
String toString() => 'Session(serverUrl: $serverUrl, accessToken: $accessToken, serverEndpoint: $serverEndpoint)';
|
||||
|
||||
T read<T>(SessionKey<T> key) =>
|
||||
(switch (key) {
|
||||
.serverUrl => serverUrl,
|
||||
.accessToken => accessToken,
|
||||
.serverEndpoint => serverEndpoint,
|
||||
})
|
||||
as T;
|
||||
|
||||
factory Session.fromEntries(Map<SessionKey, Object?> overrides) =>
|
||||
overrides.entries.fold(const Session(), (session, entry) => session.write(entry.key, entry.value));
|
||||
|
||||
Session write<T, U extends T>(SessionKey<T> key, U value) {
|
||||
return switch (key) {
|
||||
.serverUrl => copyWith(serverUrl: .fromNullable(value as String?)),
|
||||
.accessToken => copyWith(accessToken: .fromNullable(value as String?)),
|
||||
.serverEndpoint => copyWith(serverEndpoint: .fromNullable(value as String?)),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,17 +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';
|
||||
import 'package:immich_mobile/utils/semver.dart';
|
||||
|
||||
enum SettingsKey<T> {
|
||||
// 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>(),
|
||||
|
||||
@@ -27,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?>(),
|
||||
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>(),
|
||||
|
||||
@@ -47,195 +46,45 @@ 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>(),
|
||||
|
||||
// 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
|
||||
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)),
|
||||
|
||||
// Feature message
|
||||
featureMessageSeenRelease<SemVer>(codec: _SemVerCodec());
|
||||
featureMessageSeenRelease<SemVer>(codec: SemVerCodec());
|
||||
|
||||
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);
|
||||
|
||||
T decode(String raw) => _codec.decode(raw);
|
||||
}
|
||||
|
||||
sealed class _SettingsCodec<T> {
|
||||
const _SettingsCodec();
|
||||
|
||||
String encode(T value);
|
||||
T decode(String raw);
|
||||
|
||||
static final Map<Type, _SettingsCodec<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, _SettingsCodec<Object>> _register<T>(_SettingsCodec<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 _SettingsCodec<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 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 _SemVerCodec extends _SettingsCodec<SemVer> {
|
||||
const _SemVerCodec();
|
||||
|
||||
@override
|
||||
String encode(SemVer value) => value.toString();
|
||||
|
||||
@override
|
||||
SemVer decode(String raw) => SemVer.fromString(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;
|
||||
}
|
||||
|
||||
@@ -6,9 +6,6 @@ enum StoreKey<T> {
|
||||
version<int>._(0),
|
||||
currentUser<UserDto>._(2),
|
||||
deviceId<String>._(4),
|
||||
serverUrl<String>._(10),
|
||||
accessToken<String>._(11),
|
||||
serverEndpoint<String>._(12),
|
||||
advancedTroubleshooting<bool>._(114),
|
||||
enableHapticFeedback<bool>._(126),
|
||||
|
||||
@@ -19,6 +16,9 @@ enum StoreKey<T> {
|
||||
syncMigrationStatus<String>._(1013),
|
||||
|
||||
// Legacy keys that have been migrated to the new metadata store
|
||||
legacyServerUrl<String>._(10),
|
||||
legacyAccessToken<String>._(11),
|
||||
legacyServerEndpoint<String>._(12),
|
||||
legacyBackupRequireCharging<bool>._(7),
|
||||
legacyBackupTriggerDelay<int>._(8),
|
||||
legacySyncAlbums<bool>._(131),
|
||||
|
||||
153
mobile/lib/domain/models/value_codec.dart
Normal file
153
mobile/lib/domain/models/value_codec.dart
Normal file
@@ -0,0 +1,153 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:immich_mobile/utils/semver.dart';
|
||||
|
||||
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 SemVerCodec extends ValueCodec<SemVer> {
|
||||
const SemVerCodec();
|
||||
|
||||
@override
|
||||
String encode(SemVer value) => value.toString();
|
||||
|
||||
@override
|
||||
SemVer decode(String raw) => SemVer.fromString(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;
|
||||
}
|
||||
18
mobile/lib/infrastructure/entities/session.entity.dart
Normal file
18
mobile/lib/infrastructure/entities/session.entity.dart
Normal file
@@ -0,0 +1,18 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
||||
|
||||
class SessionEntity extends Table with DriftDefaultsMixin {
|
||||
const SessionEntity();
|
||||
|
||||
TextColumn get key => text()();
|
||||
|
||||
TextColumn get value => text().nullable()();
|
||||
|
||||
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {key};
|
||||
|
||||
@override
|
||||
String get tableName => "session";
|
||||
}
|
||||
427
mobile/lib/infrastructure/entities/session.entity.drift.dart
generated
Normal file
427
mobile/lib/infrastructure/entities/session.entity.drift.dart
generated
Normal file
@@ -0,0 +1,427 @@
|
||||
// dart format width=80
|
||||
// ignore_for_file: type=lint
|
||||
import 'package:drift/drift.dart' as i0;
|
||||
import 'package:immich_mobile/infrastructure/entities/session.entity.drift.dart'
|
||||
as i1;
|
||||
import 'package:immich_mobile/infrastructure/entities/session.entity.dart'
|
||||
as i2;
|
||||
import 'package:drift/src/runtime/query_builder/query_builder.dart' as i3;
|
||||
|
||||
typedef $$SessionEntityTableCreateCompanionBuilder =
|
||||
i1.SessionEntityCompanion Function({
|
||||
required String key,
|
||||
i0.Value<String?> value,
|
||||
i0.Value<DateTime> updatedAt,
|
||||
});
|
||||
typedef $$SessionEntityTableUpdateCompanionBuilder =
|
||||
i1.SessionEntityCompanion Function({
|
||||
i0.Value<String> key,
|
||||
i0.Value<String?> value,
|
||||
i0.Value<DateTime> updatedAt,
|
||||
});
|
||||
|
||||
class $$SessionEntityTableFilterComposer
|
||||
extends i0.Composer<i0.GeneratedDatabase, i1.$SessionEntityTable> {
|
||||
$$SessionEntityTableFilterComposer({
|
||||
required super.$db,
|
||||
required super.$table,
|
||||
super.joinBuilder,
|
||||
super.$addJoinBuilderToRootComposer,
|
||||
super.$removeJoinBuilderFromRootComposer,
|
||||
});
|
||||
i0.ColumnFilters<String> get key => $composableBuilder(
|
||||
column: $table.key,
|
||||
builder: (column) => i0.ColumnFilters(column),
|
||||
);
|
||||
|
||||
i0.ColumnFilters<String> get value => $composableBuilder(
|
||||
column: $table.value,
|
||||
builder: (column) => i0.ColumnFilters(column),
|
||||
);
|
||||
|
||||
i0.ColumnFilters<DateTime> get updatedAt => $composableBuilder(
|
||||
column: $table.updatedAt,
|
||||
builder: (column) => i0.ColumnFilters(column),
|
||||
);
|
||||
}
|
||||
|
||||
class $$SessionEntityTableOrderingComposer
|
||||
extends i0.Composer<i0.GeneratedDatabase, i1.$SessionEntityTable> {
|
||||
$$SessionEntityTableOrderingComposer({
|
||||
required super.$db,
|
||||
required super.$table,
|
||||
super.joinBuilder,
|
||||
super.$addJoinBuilderToRootComposer,
|
||||
super.$removeJoinBuilderFromRootComposer,
|
||||
});
|
||||
i0.ColumnOrderings<String> get key => $composableBuilder(
|
||||
column: $table.key,
|
||||
builder: (column) => i0.ColumnOrderings(column),
|
||||
);
|
||||
|
||||
i0.ColumnOrderings<String> get value => $composableBuilder(
|
||||
column: $table.value,
|
||||
builder: (column) => i0.ColumnOrderings(column),
|
||||
);
|
||||
|
||||
i0.ColumnOrderings<DateTime> get updatedAt => $composableBuilder(
|
||||
column: $table.updatedAt,
|
||||
builder: (column) => i0.ColumnOrderings(column),
|
||||
);
|
||||
}
|
||||
|
||||
class $$SessionEntityTableAnnotationComposer
|
||||
extends i0.Composer<i0.GeneratedDatabase, i1.$SessionEntityTable> {
|
||||
$$SessionEntityTableAnnotationComposer({
|
||||
required super.$db,
|
||||
required super.$table,
|
||||
super.joinBuilder,
|
||||
super.$addJoinBuilderToRootComposer,
|
||||
super.$removeJoinBuilderFromRootComposer,
|
||||
});
|
||||
i0.GeneratedColumn<String> get key =>
|
||||
$composableBuilder(column: $table.key, builder: (column) => column);
|
||||
|
||||
i0.GeneratedColumn<String> get value =>
|
||||
$composableBuilder(column: $table.value, builder: (column) => column);
|
||||
|
||||
i0.GeneratedColumn<DateTime> get updatedAt =>
|
||||
$composableBuilder(column: $table.updatedAt, builder: (column) => column);
|
||||
}
|
||||
|
||||
class $$SessionEntityTableTableManager
|
||||
extends
|
||||
i0.RootTableManager<
|
||||
i0.GeneratedDatabase,
|
||||
i1.$SessionEntityTable,
|
||||
i1.SessionEntityData,
|
||||
i1.$$SessionEntityTableFilterComposer,
|
||||
i1.$$SessionEntityTableOrderingComposer,
|
||||
i1.$$SessionEntityTableAnnotationComposer,
|
||||
$$SessionEntityTableCreateCompanionBuilder,
|
||||
$$SessionEntityTableUpdateCompanionBuilder,
|
||||
(
|
||||
i1.SessionEntityData,
|
||||
i0.BaseReferences<
|
||||
i0.GeneratedDatabase,
|
||||
i1.$SessionEntityTable,
|
||||
i1.SessionEntityData
|
||||
>,
|
||||
),
|
||||
i1.SessionEntityData,
|
||||
i0.PrefetchHooks Function()
|
||||
> {
|
||||
$$SessionEntityTableTableManager(
|
||||
i0.GeneratedDatabase db,
|
||||
i1.$SessionEntityTable table,
|
||||
) : super(
|
||||
i0.TableManagerState(
|
||||
db: db,
|
||||
table: table,
|
||||
createFilteringComposer: () =>
|
||||
i1.$$SessionEntityTableFilterComposer($db: db, $table: table),
|
||||
createOrderingComposer: () =>
|
||||
i1.$$SessionEntityTableOrderingComposer($db: db, $table: table),
|
||||
createComputedFieldComposer: () =>
|
||||
i1.$$SessionEntityTableAnnotationComposer($db: db, $table: table),
|
||||
updateCompanionCallback:
|
||||
({
|
||||
i0.Value<String> key = const i0.Value.absent(),
|
||||
i0.Value<String?> value = const i0.Value.absent(),
|
||||
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
|
||||
}) => i1.SessionEntityCompanion(
|
||||
key: key,
|
||||
value: value,
|
||||
updatedAt: updatedAt,
|
||||
),
|
||||
createCompanionCallback:
|
||||
({
|
||||
required String key,
|
||||
i0.Value<String?> value = const i0.Value.absent(),
|
||||
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
|
||||
}) => i1.SessionEntityCompanion.insert(
|
||||
key: key,
|
||||
value: value,
|
||||
updatedAt: updatedAt,
|
||||
),
|
||||
withReferenceMapper: (p0) => p0
|
||||
.map((e) => (e.readTable(table), i0.BaseReferences(db, table, e)))
|
||||
.toList(),
|
||||
prefetchHooksCallback: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
typedef $$SessionEntityTableProcessedTableManager =
|
||||
i0.ProcessedTableManager<
|
||||
i0.GeneratedDatabase,
|
||||
i1.$SessionEntityTable,
|
||||
i1.SessionEntityData,
|
||||
i1.$$SessionEntityTableFilterComposer,
|
||||
i1.$$SessionEntityTableOrderingComposer,
|
||||
i1.$$SessionEntityTableAnnotationComposer,
|
||||
$$SessionEntityTableCreateCompanionBuilder,
|
||||
$$SessionEntityTableUpdateCompanionBuilder,
|
||||
(
|
||||
i1.SessionEntityData,
|
||||
i0.BaseReferences<
|
||||
i0.GeneratedDatabase,
|
||||
i1.$SessionEntityTable,
|
||||
i1.SessionEntityData
|
||||
>,
|
||||
),
|
||||
i1.SessionEntityData,
|
||||
i0.PrefetchHooks Function()
|
||||
>;
|
||||
|
||||
class $SessionEntityTable extends i2.SessionEntity
|
||||
with i0.TableInfo<$SessionEntityTable, i1.SessionEntityData> {
|
||||
@override
|
||||
final i0.GeneratedDatabase attachedDatabase;
|
||||
final String? _alias;
|
||||
$SessionEntityTable(this.attachedDatabase, [this._alias]);
|
||||
static const i0.VerificationMeta _keyMeta = const i0.VerificationMeta('key');
|
||||
@override
|
||||
late final i0.GeneratedColumn<String> key = i0.GeneratedColumn<String>(
|
||||
'key',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i0.DriftSqlType.string,
|
||||
requiredDuringInsert: true,
|
||||
);
|
||||
static const i0.VerificationMeta _valueMeta = const i0.VerificationMeta(
|
||||
'value',
|
||||
);
|
||||
@override
|
||||
late final i0.GeneratedColumn<String> value = i0.GeneratedColumn<String>(
|
||||
'value',
|
||||
aliasedName,
|
||||
true,
|
||||
type: i0.DriftSqlType.string,
|
||||
requiredDuringInsert: false,
|
||||
);
|
||||
static const i0.VerificationMeta _updatedAtMeta = const i0.VerificationMeta(
|
||||
'updatedAt',
|
||||
);
|
||||
@override
|
||||
late final i0.GeneratedColumn<DateTime> updatedAt =
|
||||
i0.GeneratedColumn<DateTime>(
|
||||
'updated_at',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i0.DriftSqlType.dateTime,
|
||||
requiredDuringInsert: false,
|
||||
defaultValue: i3.currentDateAndTime,
|
||||
);
|
||||
@override
|
||||
List<i0.GeneratedColumn> get $columns => [key, value, updatedAt];
|
||||
@override
|
||||
String get aliasedName => _alias ?? actualTableName;
|
||||
@override
|
||||
String get actualTableName => $name;
|
||||
static const String $name = 'session';
|
||||
@override
|
||||
i0.VerificationContext validateIntegrity(
|
||||
i0.Insertable<i1.SessionEntityData> instance, {
|
||||
bool isInserting = false,
|
||||
}) {
|
||||
final context = i0.VerificationContext();
|
||||
final data = instance.toColumns(true);
|
||||
if (data.containsKey('key')) {
|
||||
context.handle(
|
||||
_keyMeta,
|
||||
key.isAcceptableOrUnknown(data['key']!, _keyMeta),
|
||||
);
|
||||
} else if (isInserting) {
|
||||
context.missing(_keyMeta);
|
||||
}
|
||||
if (data.containsKey('value')) {
|
||||
context.handle(
|
||||
_valueMeta,
|
||||
value.isAcceptableOrUnknown(data['value']!, _valueMeta),
|
||||
);
|
||||
}
|
||||
if (data.containsKey('updated_at')) {
|
||||
context.handle(
|
||||
_updatedAtMeta,
|
||||
updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta),
|
||||
);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
@override
|
||||
Set<i0.GeneratedColumn> get $primaryKey => {key};
|
||||
@override
|
||||
i1.SessionEntityData map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||
return i1.SessionEntityData(
|
||||
key: attachedDatabase.typeMapping.read(
|
||||
i0.DriftSqlType.string,
|
||||
data['${effectivePrefix}key'],
|
||||
)!,
|
||||
value: attachedDatabase.typeMapping.read(
|
||||
i0.DriftSqlType.string,
|
||||
data['${effectivePrefix}value'],
|
||||
),
|
||||
updatedAt: attachedDatabase.typeMapping.read(
|
||||
i0.DriftSqlType.dateTime,
|
||||
data['${effectivePrefix}updated_at'],
|
||||
)!,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
$SessionEntityTable createAlias(String alias) {
|
||||
return $SessionEntityTable(attachedDatabase, alias);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get withoutRowId => true;
|
||||
@override
|
||||
bool get isStrict => true;
|
||||
}
|
||||
|
||||
class SessionEntityData extends i0.DataClass
|
||||
implements i0.Insertable<i1.SessionEntityData> {
|
||||
final String key;
|
||||
final String? value;
|
||||
final DateTime updatedAt;
|
||||
const SessionEntityData({
|
||||
required this.key,
|
||||
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['updated_at'] = i0.Variable<DateTime>(updatedAt);
|
||||
return map;
|
||||
}
|
||||
|
||||
factory SessionEntityData.fromJson(
|
||||
Map<String, dynamic> json, {
|
||||
i0.ValueSerializer? serializer,
|
||||
}) {
|
||||
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
|
||||
return SessionEntityData(
|
||||
key: serializer.fromJson<String>(json['key']),
|
||||
value: serializer.fromJson<String?>(json['value']),
|
||||
updatedAt: serializer.fromJson<DateTime>(json['updatedAt']),
|
||||
);
|
||||
}
|
||||
@override
|
||||
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
|
||||
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
|
||||
return <String, dynamic>{
|
||||
'key': serializer.toJson<String>(key),
|
||||
'value': serializer.toJson<String?>(value),
|
||||
'updatedAt': serializer.toJson<DateTime>(updatedAt),
|
||||
};
|
||||
}
|
||||
|
||||
i1.SessionEntityData copyWith({
|
||||
String? key,
|
||||
i0.Value<String?> value = const i0.Value.absent(),
|
||||
DateTime? updatedAt,
|
||||
}) => i1.SessionEntityData(
|
||||
key: key ?? this.key,
|
||||
value: value.present ? value.value : this.value,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
);
|
||||
SessionEntityData copyWithCompanion(i1.SessionEntityCompanion data) {
|
||||
return SessionEntityData(
|
||||
key: data.key.present ? data.key.value : this.key,
|
||||
value: data.value.present ? data.value.value : this.value,
|
||||
updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('SessionEntityData(')
|
||||
..write('key: $key, ')
|
||||
..write('value: $value, ')
|
||||
..write('updatedAt: $updatedAt')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(key, value, updatedAt);
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
(other is i1.SessionEntityData &&
|
||||
other.key == this.key &&
|
||||
other.value == this.value &&
|
||||
other.updatedAt == this.updatedAt);
|
||||
}
|
||||
|
||||
class SessionEntityCompanion extends i0.UpdateCompanion<i1.SessionEntityData> {
|
||||
final i0.Value<String> key;
|
||||
final i0.Value<String?> value;
|
||||
final i0.Value<DateTime> updatedAt;
|
||||
const SessionEntityCompanion({
|
||||
this.key = const i0.Value.absent(),
|
||||
this.value = const i0.Value.absent(),
|
||||
this.updatedAt = const i0.Value.absent(),
|
||||
});
|
||||
SessionEntityCompanion.insert({
|
||||
required String key,
|
||||
this.value = const i0.Value.absent(),
|
||||
this.updatedAt = const i0.Value.absent(),
|
||||
}) : key = i0.Value(key);
|
||||
static i0.Insertable<i1.SessionEntityData> custom({
|
||||
i0.Expression<String>? key,
|
||||
i0.Expression<String>? value,
|
||||
i0.Expression<DateTime>? updatedAt,
|
||||
}) {
|
||||
return i0.RawValuesInsertable({
|
||||
if (key != null) 'key': key,
|
||||
if (value != null) 'value': value,
|
||||
if (updatedAt != null) 'updated_at': updatedAt,
|
||||
});
|
||||
}
|
||||
|
||||
i1.SessionEntityCompanion copyWith({
|
||||
i0.Value<String>? key,
|
||||
i0.Value<String?>? value,
|
||||
i0.Value<DateTime>? updatedAt,
|
||||
}) {
|
||||
return i1.SessionEntityCompanion(
|
||||
key: key ?? this.key,
|
||||
value: value ?? this.value,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, i0.Expression>{};
|
||||
if (key.present) {
|
||||
map['key'] = i0.Variable<String>(key.value);
|
||||
}
|
||||
if (value.present) {
|
||||
map['value'] = i0.Variable<String>(value.value);
|
||||
}
|
||||
if (updatedAt.present) {
|
||||
map['updated_at'] = i0.Variable<DateTime>(updatedAt.value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('SessionEntityCompanion(')
|
||||
..write('key: $key, ')
|
||||
..write('value: $value, ')
|
||||
..write('updatedAt: $updatedAt')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
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());
|
||||
|
||||
@protected
|
||||
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};
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.d
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_asset_cloud_id.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/session.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/settings.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/stack.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
|
||||
@@ -67,6 +68,7 @@ import 'package:sqlite_async/sqlite_async.dart';
|
||||
AssetEditEntity,
|
||||
SettingsEntity,
|
||||
AssetOcrEntity,
|
||||
SessionEntity,
|
||||
],
|
||||
include: {'package:immich_mobile/infrastructure/entities/merged_asset.drift'},
|
||||
)
|
||||
@@ -120,7 +122,7 @@ class Drift extends $Drift {
|
||||
}
|
||||
|
||||
@override
|
||||
int get schemaVersion => 31;
|
||||
int get schemaVersion => 32;
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration => MigrationStrategy(
|
||||
@@ -317,6 +319,10 @@ class Drift extends $Drift {
|
||||
},
|
||||
from30To31: (m, v31) async {
|
||||
await m.createIndex(v31.idxRemoteAssetUploaded);
|
||||
// await m.createTable(v31.session);
|
||||
},
|
||||
from31To32: (m, v32) async {
|
||||
await m.createTable(v32.session);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
@@ -47,9 +47,11 @@ import 'package:immich_mobile/infrastructure/entities/settings.entity.drift.dart
|
||||
as i22;
|
||||
import 'package:immich_mobile/infrastructure/entities/asset_ocr.entity.drift.dart'
|
||||
as i23;
|
||||
import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart'
|
||||
import 'package:immich_mobile/infrastructure/entities/session.entity.drift.dart'
|
||||
as i24;
|
||||
import 'package:drift/internal/modular.dart' as i25;
|
||||
import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart'
|
||||
as i25;
|
||||
import 'package:drift/internal/modular.dart' as i26;
|
||||
|
||||
abstract class $Drift extends i0.GeneratedDatabase {
|
||||
$Drift(i0.QueryExecutor e) : super(e);
|
||||
@@ -99,9 +101,12 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
||||
late final i23.$AssetOcrEntityTable assetOcrEntity = i23.$AssetOcrEntityTable(
|
||||
this,
|
||||
);
|
||||
i24.MergedAssetDrift get mergedAssetDrift => i25.ReadDatabaseContainer(
|
||||
late final i24.$SessionEntityTable sessionEntity = i24.$SessionEntityTable(
|
||||
this,
|
||||
).accessor<i24.MergedAssetDrift>(i24.MergedAssetDrift.new);
|
||||
);
|
||||
i25.MergedAssetDrift get mergedAssetDrift => i26.ReadDatabaseContainer(
|
||||
this,
|
||||
).accessor<i25.MergedAssetDrift>(i25.MergedAssetDrift.new);
|
||||
@override
|
||||
Iterable<i0.TableInfo<i0.Table, Object?>> get allTables =>
|
||||
allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>();
|
||||
@@ -141,6 +146,7 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
||||
assetEditEntity,
|
||||
settingsEntity,
|
||||
assetOcrEntity,
|
||||
sessionEntity,
|
||||
i10.idxPartnerSharedWithId,
|
||||
i11.idxLatLng,
|
||||
i11.idxRemoteExifCity,
|
||||
@@ -415,4 +421,6 @@ class $DriftManager {
|
||||
i22.$$SettingsEntityTableTableManager(_db, _db.settingsEntity);
|
||||
i23.$$AssetOcrEntityTableTableManager get assetOcrEntity =>
|
||||
i23.$$AssetOcrEntityTableTableManager(_db, _db.assetOcrEntity);
|
||||
i24.$$SessionEntityTableTableManager get sessionEntity =>
|
||||
i24.$$SessionEntityTableTableManager(_db, _db.sessionEntity);
|
||||
}
|
||||
|
||||
@@ -16506,6 +16506,603 @@ final class Schema31 extends i0.VersionedSchema {
|
||||
);
|
||||
}
|
||||
|
||||
final class Schema32 extends i0.VersionedSchema {
|
||||
Schema32({required super.database}) : super(version: 32);
|
||||
@override
|
||||
late final List<i1.DatabaseSchemaEntity> entities = [
|
||||
userEntity,
|
||||
remoteAssetEntity,
|
||||
stackEntity,
|
||||
localAssetEntity,
|
||||
remoteAlbumEntity,
|
||||
localAlbumEntity,
|
||||
localAlbumAssetEntity,
|
||||
idxLocalAlbumAssetAlbumAsset,
|
||||
idxLocalAssetChecksum,
|
||||
idxLocalAssetCloudId,
|
||||
idxLocalAssetCreatedAt,
|
||||
idxStackPrimaryAssetId,
|
||||
uQRemoteAssetsOwnerChecksum,
|
||||
uQRemoteAssetsOwnerLibraryChecksum,
|
||||
idxRemoteAssetChecksum,
|
||||
idxRemoteAssetStackId,
|
||||
idxRemoteAssetOwnerVisibilityDeletedCreated,
|
||||
idxRemoteAssetUploaded,
|
||||
authUserEntity,
|
||||
userMetadataEntity,
|
||||
partnerEntity,
|
||||
remoteExifEntity,
|
||||
remoteAlbumAssetEntity,
|
||||
remoteAlbumUserEntity,
|
||||
remoteAssetCloudIdEntity,
|
||||
memoryEntity,
|
||||
memoryAssetEntity,
|
||||
personEntity,
|
||||
assetFaceEntity,
|
||||
storeEntity,
|
||||
trashedLocalAssetEntity,
|
||||
assetEditEntity,
|
||||
settings,
|
||||
assetOcrEntity,
|
||||
session,
|
||||
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)',
|
||||
);
|
||||
final i1.Index idxRemoteAssetUploaded = i1.Index(
|
||||
'idx_remote_asset_uploaded',
|
||||
'CREATE INDEX IF NOT EXISTS idx_remote_asset_uploaded ON remote_asset_entity (uploaded_at)',
|
||||
);
|
||||
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,
|
||||
);
|
||||
late final Shape49 session = Shape49(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'session',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY("key")'],
|
||||
columns: [_column_210, _column_224, _column_115],
|
||||
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)',
|
||||
);
|
||||
}
|
||||
|
||||
i0.MigrationStepWithVersion migrationSteps({
|
||||
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
||||
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
||||
@@ -16537,6 +17134,7 @@ i0.MigrationStepWithVersion migrationSteps({
|
||||
required Future<void> Function(i1.Migrator m, Schema29 schema) from28To29,
|
||||
required Future<void> Function(i1.Migrator m, Schema30 schema) from29To30,
|
||||
required Future<void> Function(i1.Migrator m, Schema31 schema) from30To31,
|
||||
required Future<void> Function(i1.Migrator m, Schema32 schema) from31To32,
|
||||
}) {
|
||||
return (currentVersion, database) async {
|
||||
switch (currentVersion) {
|
||||
@@ -16690,6 +17288,11 @@ i0.MigrationStepWithVersion migrationSteps({
|
||||
final migrator = i1.Migrator(database, schema);
|
||||
await from30To31(migrator, schema);
|
||||
return 31;
|
||||
case 31:
|
||||
final schema = Schema32(database: database);
|
||||
final migrator = i1.Migrator(database, schema);
|
||||
await from31To32(migrator, schema);
|
||||
return 32;
|
||||
default:
|
||||
throw ArgumentError.value('Unknown migration from $currentVersion');
|
||||
}
|
||||
@@ -16727,6 +17330,7 @@ i1.OnUpgrade stepByStep({
|
||||
required Future<void> Function(i1.Migrator m, Schema29 schema) from28To29,
|
||||
required Future<void> Function(i1.Migrator m, Schema30 schema) from29To30,
|
||||
required Future<void> Function(i1.Migrator m, Schema31 schema) from30To31,
|
||||
required Future<void> Function(i1.Migrator m, Schema32 schema) from31To32,
|
||||
}) => i0.VersionedSchema.stepByStepHelper(
|
||||
step: migrationSteps(
|
||||
from1To2: from1To2,
|
||||
@@ -16759,5 +17363,6 @@ i1.OnUpgrade stepByStep({
|
||||
from28To29: from28To29,
|
||||
from29To30: from29To30,
|
||||
from30To31: from30To31,
|
||||
from31To32: from31To32,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -210,17 +210,19 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
|
||||
await _deleteAssets(deletes);
|
||||
|
||||
await _upsertAssets(updates);
|
||||
// Drop every existing album link for each changed asset before re-adding the
|
||||
// ones the native side reports. A moved asset only reports its new album here,
|
||||
// so leaving the old link around makes the per-album delete sweep wipe the
|
||||
// asset entirely (it is still linked to a bucket it no longer lives in).
|
||||
await _db.batch((batch) async {
|
||||
for (final assetId in assetAlbums.keys) {
|
||||
batch.deleteWhere(_db.localAlbumAssetEntity, (f) => f.assetId.equals(assetId));
|
||||
}
|
||||
});
|
||||
// The ugly casting below is required for now because the generated code
|
||||
// casts the returned values from the platform during decoding them
|
||||
// and iterating over them causes the type to be List<Object?> instead of
|
||||
// List<String>
|
||||
await _db.batch((batch) async {
|
||||
assetAlbums.cast<String, List<Object?>>().forEach((assetId, albumIds) {
|
||||
for (final albumId in albumIds.cast<String?>().nonNulls) {
|
||||
batch.deleteWhere(_db.localAlbumAssetEntity, (f) => f.albumId.equals(albumId) & f.assetId.equals(assetId));
|
||||
}
|
||||
});
|
||||
});
|
||||
await _db.batch((batch) async {
|
||||
assetAlbums.cast<String, List<Object?>>().forEach((assetId, albumIds) {
|
||||
batch.insertAll(
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/domain/models/session.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/session.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/cached_key_value_repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
|
||||
class SessionRepository extends CachedKeyValueRepository<SessionKey, Session> {
|
||||
final Drift _db;
|
||||
|
||||
SessionRepository._(this._db) : super(const .new());
|
||||
|
||||
static SessionRepository? _instance;
|
||||
|
||||
static SessionRepository get instance {
|
||||
final instance = _instance;
|
||||
if (instance == null) {
|
||||
throw StateError('SessionRepository not initialized. Call ensureInitialized() first');
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
static Future<SessionRepository> ensureInitialized(Drift db) async {
|
||||
if (_instance == null) {
|
||||
final instance = SessionRepository._(db);
|
||||
await instance.refresh();
|
||||
_instance = instance;
|
||||
}
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
@override
|
||||
List<SessionKey> get keys => SessionKey.values;
|
||||
|
||||
@override
|
||||
Object decodeValue(SessionKey key, String raw) => key.decode(raw);
|
||||
|
||||
@override
|
||||
Session buildSnapshot(Map<SessionKey, Object?> overrides) => Session.fromEntries(overrides);
|
||||
|
||||
@override
|
||||
@protected
|
||||
Selectable<({String key, String? value})> selectable() =>
|
||||
_db.select(_db.sessionEntity).map((row) => (key: row.key, value: row.value));
|
||||
|
||||
Session get session => snapshot;
|
||||
|
||||
Future<void> clear(Iterable<SessionKey> keys) async {
|
||||
if (keys.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final names = keys.map((key) => key.name).toList();
|
||||
await (_db.delete(_db.sessionEntity)..where((row) => row.key.isIn(names))).go();
|
||||
|
||||
var session = snapshot;
|
||||
for (final key in keys) {
|
||||
session = session.write(key, defaultSession.read(key));
|
||||
}
|
||||
snapshot = session;
|
||||
}
|
||||
|
||||
Future<void> write<T, U extends T>(SessionKey<T> key, U value) async {
|
||||
if (value == snapshot.read(key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String? resolvedValue;
|
||||
if (value != null) {
|
||||
resolvedValue = key.encode(value);
|
||||
}
|
||||
|
||||
await _db
|
||||
.into(_db.sessionEntity)
|
||||
.insertOnConflictUpdate(
|
||||
SessionEntityCompanion.insert(key: key.name, value: .new(resolvedValue), updatedAt: .new(DateTime.now())),
|
||||
);
|
||||
snapshot = snapshot.write(key, value);
|
||||
}
|
||||
|
||||
Stream<Session> watch() => watchSnapshot();
|
||||
}
|
||||
@@ -1,13 +1,15 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:flutter/material.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 DriftDatabaseRepository {
|
||||
class SettingsRepository extends CachedKeyValueRepository<SettingsKey, AppConfig> {
|
||||
final Drift _db;
|
||||
|
||||
SettingsRepository._(this._db) : super(_db);
|
||||
SettingsRepository._(this._db) : super(const .new());
|
||||
|
||||
static SettingsRepository? _instance;
|
||||
|
||||
@@ -19,9 +21,6 @@ class SettingsRepository extends DriftDatabaseRepository {
|
||||
return instance;
|
||||
}
|
||||
|
||||
AppConfig _appConfig = const .new();
|
||||
AppConfig get appConfig => _appConfig;
|
||||
|
||||
static Future<SettingsRepository> ensureInitialized(Drift db) async {
|
||||
if (_instance == null) {
|
||||
final instance = SettingsRepository._(db);
|
||||
@@ -31,7 +30,21 @@ class SettingsRepository extends DriftDatabaseRepository {
|
||||
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
|
||||
@protected
|
||||
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 {
|
||||
if (keys.isEmpty) {
|
||||
@@ -41,13 +54,15 @@ class SettingsRepository extends DriftDatabaseRepository {
|
||||
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) {
|
||||
_appConfig = _appConfig.write(key, defaultConfig.read(key));
|
||||
config = config.write(key, defaultConfig.read(key));
|
||||
}
|
||||
snapshot = config;
|
||||
}
|
||||
|
||||
Future<void> write<T, U extends T>(SettingsKey<T> key, U value) async {
|
||||
if (value == _appConfig.read(key)) {
|
||||
if (value == snapshot.read(key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -65,29 +80,8 @@ class SettingsRepository extends DriftDatabaseRepository {
|
||||
.insertOnConflictUpdate(
|
||||
SettingsEntityCompanion.insert(key: key.name, value: .new(resolvedValue), updatedAt: .new(DateTime.now())),
|
||||
);
|
||||
_appConfig = _appConfig.write(key, value);
|
||||
snapshot = snapshot.write(key, value);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
Object? decodedValue;
|
||||
if (row.value != null) {
|
||||
decodedValue = metadataKey.decode(row.value!);
|
||||
}
|
||||
|
||||
return {...overrides, metadataKey: decodedValue};
|
||||
}),
|
||||
);
|
||||
}
|
||||
Stream<AppConfig> watch() => watchSnapshot();
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import 'package:immich_mobile/infrastructure/repositories/settings.repository.da
|
||||
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/session.provider.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/providers/view_intent/view_intent_handler.provider.dart';
|
||||
import 'package:immich_mobile/providers/websocket.provider.dart';
|
||||
@@ -298,9 +299,10 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
|
||||
}
|
||||
|
||||
void resumeSession() async {
|
||||
final serverUrl = Store.tryGet(StoreKey.serverUrl);
|
||||
final endpoint = Store.tryGet(StoreKey.serverEndpoint);
|
||||
final accessToken = Store.tryGet(StoreKey.accessToken);
|
||||
final session = ref.read(sessionProvider);
|
||||
final serverUrl = session.serverUrl;
|
||||
final endpoint = session.serverEndpoint;
|
||||
final accessToken = session.accessToken;
|
||||
|
||||
if (accessToken != null && serverUrl != null && endpoint != null) {
|
||||
final infoProvider = ref.read(serverInfoProvider.notifier);
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/session.repository.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
@@ -22,7 +21,7 @@ class OpenInBrowserActionButton extends ConsumerWidget {
|
||||
});
|
||||
|
||||
void _onTap() async {
|
||||
final serverEndpoint = Store.get(StoreKey.serverEndpoint).replaceFirst('/api', '');
|
||||
final serverEndpoint = SessionRepository.instance.session.serverEndpoint!.replaceFirst('/api', '');
|
||||
|
||||
String originPath = '';
|
||||
switch (origin) {
|
||||
|
||||
@@ -174,6 +174,12 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
||||
return;
|
||||
}
|
||||
|
||||
// The viewer is closing; don't flip the current asset now. Flipping it swaps
|
||||
// the grid tile hero keys mid pop and animates the close on two tiles (#23779).
|
||||
if (!mounted || !(ModalRoute.of(context)?.isActive ?? true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
AssetViewer._setAsset(ref, asset);
|
||||
_preloader.preload(index, context.sizeData);
|
||||
_handleCasting();
|
||||
|
||||
@@ -4,8 +4,6 @@ import 'dart:io';
|
||||
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/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
|
||||
@@ -13,6 +11,7 @@ import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.pro
|
||||
import 'package:immich_mobile/providers/asset_viewer/video_player_provider.dart';
|
||||
import 'package:immich_mobile/providers/cast.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/session.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
|
||||
import 'package:immich_mobile/services/api.service.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
@@ -148,7 +147,7 @@ class _NativeVideoViewerState extends ConsumerState<NativeVideoViewer> with Widg
|
||||
|
||||
final remoteAsset = videoAsset as RemoteAsset;
|
||||
|
||||
final serverEndpoint = Store.get(StoreKey.serverEndpoint);
|
||||
final serverEndpoint = ref.read(sessionProvider).serverEndpoint!;
|
||||
final isOriginalVideo = ref.read(appConfigProvider).viewer.loadOriginalVideo;
|
||||
final String postfixUrl = isOriginalVideo ? 'original' : 'video/playback';
|
||||
final String assetId = remoteAsset.livePhotoVideoId ?? remoteAsset.id;
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/session.provider.dart';
|
||||
|
||||
class PartnerUserAvatar extends StatelessWidget {
|
||||
class PartnerUserAvatar extends ConsumerWidget {
|
||||
const PartnerUserAvatar({super.key, required this.userId, required this.name});
|
||||
|
||||
final String userId;
|
||||
final String name;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final url = "${Store.get(StoreKey.serverEndpoint)}/users/$userId/profile-image";
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final url = "${ref.read(sessionProvider).serverEndpoint}/users/$userId/profile-image";
|
||||
final nameFirstLetter = name.isNotEmpty ? name[0] : "";
|
||||
return CircleAvatar(
|
||||
radius: 16,
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:convert';
|
||||
import 'package:flutter_udid/flutter_udid.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/constants.dart';
|
||||
import 'package:immich_mobile/domain/models/session.model.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/domain/services/user.service.dart';
|
||||
@@ -10,6 +11,7 @@ import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/models/auth/auth_state.model.dart';
|
||||
import 'package:immich_mobile/models/auth/login_response.model.dart';
|
||||
import 'package:immich_mobile/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/session.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
|
||||
import 'package:immich_mobile/services/api.service.dart';
|
||||
@@ -125,10 +127,10 @@ class AuthNotifier extends StateNotifier<AuthState> {
|
||||
}
|
||||
|
||||
Future<bool> saveAuthInfo({required String accessToken}) async {
|
||||
await Store.put(StoreKey.accessToken, accessToken);
|
||||
await _ref.read(sessionRepository).write(SessionKey.accessToken, accessToken);
|
||||
await _apiService.updateHeaders();
|
||||
|
||||
final serverEndpoint = Store.get(StoreKey.serverEndpoint);
|
||||
final serverEndpoint = _ref.read(sessionProvider).serverEndpoint!;
|
||||
final headerMap = _ref.read(appConfigProvider).network.customHeaders;
|
||||
final customHeaders = headerMap.isEmpty ? null : jsonEncode(headerMap);
|
||||
await _widgetService.writeCredentials(serverEndpoint, accessToken, customHeaders);
|
||||
@@ -194,9 +196,9 @@ class AuthNotifier extends StateNotifier<AuthState> {
|
||||
return _ref.read(appConfigProvider).network.localEndpoint;
|
||||
}
|
||||
|
||||
/// Returns the current server endpoint (with /api) URL from the store
|
||||
/// Returns the current server endpoint (with /api) URL from the session
|
||||
String? getServerEndpoint() {
|
||||
return Store.tryGet(StoreKey.serverEndpoint);
|
||||
return _ref.read(sessionProvider).serverEndpoint;
|
||||
}
|
||||
|
||||
Future<String?> setOpenApiServiceEndpoint() {
|
||||
|
||||
12
mobile/lib/providers/infrastructure/session.provider.dart
Normal file
12
mobile/lib/providers/infrastructure/session.provider.dart
Normal file
@@ -0,0 +1,12 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/session.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/session.repository.dart';
|
||||
|
||||
final sessionRepository = Provider.autoDispose<SessionRepository>((_) => SessionRepository.instance);
|
||||
|
||||
final sessionProvider = Provider.autoDispose<Session>((ref) {
|
||||
final repo = ref.watch(sessionRepository);
|
||||
final subscription = repo.watch().listen((event) => ref.state = event);
|
||||
ref.onDispose(subscription.cancel);
|
||||
return repo.session;
|
||||
});
|
||||
@@ -6,7 +6,7 @@ final settingsProvider = Provider.autoDispose<SettingsRepository>((_) => Setting
|
||||
|
||||
final appConfigProvider = Provider.autoDispose<AppConfig>((ref) {
|
||||
final repo = ref.watch(settingsProvider);
|
||||
final subscription = repo.watchConfig().listen((event) => ref.state = event);
|
||||
final subscription = repo.watch().listen((event) => ref.state = event);
|
||||
ref.onDispose(subscription.cancel);
|
||||
return repo.appConfig;
|
||||
});
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:hooks_riverpod/hooks_riverpod.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/models/server_info/server_version.model.dart';
|
||||
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/session.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/utils/debounce.dart';
|
||||
@@ -68,7 +67,7 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
||||
|
||||
if (authenticationState.isAuthenticated) {
|
||||
try {
|
||||
final endpoint = Uri.parse(Store.get(StoreKey.serverEndpoint));
|
||||
final endpoint = Uri.parse(_ref.read(sessionProvider).serverEndpoint!);
|
||||
dPrint(() => "Attempting to connect to websocket");
|
||||
// Configure socket transports must be specified
|
||||
Socket socket = io(
|
||||
|
||||
@@ -5,9 +5,8 @@ import 'dart:io';
|
||||
import 'package:background_downloader/background_downloader.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/constants.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/session.repository.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:immich_mobile/utils/debug_print.dart';
|
||||
@@ -96,7 +95,7 @@ class UploadRepository {
|
||||
void Function(int bytes, int totalBytes)? onProgress,
|
||||
required String logContext,
|
||||
}) async {
|
||||
final String savedEndpoint = Store.get(StoreKey.serverEndpoint);
|
||||
final String savedEndpoint = SessionRepository.instance.session.serverEndpoint!;
|
||||
final baseRequest = ProgressMultipartRequest(
|
||||
'POST',
|
||||
Uri.parse('$savedEndpoint/assets'),
|
||||
|
||||
@@ -2,9 +2,7 @@ import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/services/store.service.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/session.repository.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/services/api.service.dart';
|
||||
import 'package:immich_mobile/services/auth.service.dart';
|
||||
@@ -23,10 +21,8 @@ class AuthGuard extends AutoRouteGuard {
|
||||
// guards, so we keep this function fully sync and validate the token in
|
||||
// the background — otherwise a slow validateAccessToken() request would
|
||||
// block the route transition for as long as the OS-level HTTP timeout.
|
||||
try {
|
||||
Store.get(StoreKey.accessToken);
|
||||
} on StoreKeyNotFoundException catch (_) {
|
||||
_log.warning('No access token in the store.');
|
||||
if (SessionRepository.instance.session.accessToken == null) {
|
||||
_log.warning('No access token in the session.');
|
||||
resolver.next(false);
|
||||
unawaited(router.replaceAll([const LoginRoute()]));
|
||||
return;
|
||||
@@ -40,7 +36,7 @@ class AuthGuard extends AutoRouteGuard {
|
||||
if (_validateInFlight) {
|
||||
return;
|
||||
}
|
||||
final token = Store.tryGet(StoreKey.accessToken);
|
||||
final token = SessionRepository.instance.session.accessToken;
|
||||
if (token == null) {
|
||||
return;
|
||||
}
|
||||
@@ -50,7 +46,7 @@ class AuthGuard extends AutoRouteGuard {
|
||||
if (res == null || res.authStatus != true) {
|
||||
// Token may have changed during validation (user logged out + logged in
|
||||
// again); only act if it still applies to the current session.
|
||||
if (Store.tryGet(StoreKey.accessToken) != token) {
|
||||
if (SessionRepository.instance.session.accessToken != token) {
|
||||
return;
|
||||
}
|
||||
_log.fine('User token is invalid. Redirecting to login');
|
||||
@@ -61,7 +57,7 @@ class AuthGuard extends AutoRouteGuard {
|
||||
if (e.code != HttpStatus.unauthorized) {
|
||||
return;
|
||||
}
|
||||
if (Store.tryGet(StoreKey.accessToken) != token) {
|
||||
if (SessionRepository.instance.session.accessToken != token) {
|
||||
return;
|
||||
}
|
||||
_log.warning("Unauthorized access token.");
|
||||
|
||||
@@ -3,9 +3,9 @@ import 'dart:convert';
|
||||
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/domain/models/session.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/session.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
|
||||
import 'package:immich_mobile/utils/debug_print.dart';
|
||||
import 'package:immich_mobile/utils/url_helper.dart';
|
||||
@@ -41,7 +41,7 @@ class ApiService {
|
||||
// The below line ensures that the api clients are initialized when the service is instantiated
|
||||
// This is required to avoid late initialization errors when the clients are access before the endpoint is resolved
|
||||
setEndpoint('');
|
||||
final endpoint = Store.tryGet(StoreKey.serverEndpoint);
|
||||
final endpoint = SessionRepository.instance.session.serverEndpoint;
|
||||
if (endpoint != null && endpoint.isNotEmpty) {
|
||||
setEndpoint(endpoint);
|
||||
}
|
||||
@@ -84,7 +84,7 @@ class ApiService {
|
||||
setEndpoint(endpoint);
|
||||
|
||||
// Save in local database for next startup
|
||||
await Store.put(StoreKey.serverEndpoint, endpoint);
|
||||
await SessionRepository.instance.write(SessionKey.serverEndpoint, endpoint);
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
@@ -173,7 +173,7 @@ class ApiService {
|
||||
|
||||
static List<String> getServerUrls() {
|
||||
final urls = <String>[];
|
||||
final serverEndpoint = Store.tryGet(StoreKey.serverEndpoint);
|
||||
final serverEndpoint = SessionRepository.instance.session.serverEndpoint;
|
||||
if (serverEndpoint != null && serverEndpoint.isNotEmpty) {
|
||||
urls.add(serverEndpoint);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/session.model.dart';
|
||||
import 'package:immich_mobile/domain/models/settings_key.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/utils/background_sync.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/session.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
|
||||
import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart';
|
||||
import 'package:immich_mobile/models/auth/login_response.model.dart';
|
||||
import 'package:immich_mobile/providers/api.provider.dart';
|
||||
@@ -55,7 +57,7 @@ class AuthService {
|
||||
Future<String> validateServerUrl(String url) async {
|
||||
final validUrl = await _apiService.resolveAndSetEndpoint(url);
|
||||
await _apiService.setDeviceInfoHeader();
|
||||
await Store.put(StoreKey.serverUrl, validUrl);
|
||||
await SessionRepository.instance.write(SessionKey.serverUrl, validUrl);
|
||||
|
||||
return validUrl;
|
||||
}
|
||||
@@ -119,7 +121,7 @@ class AuthService {
|
||||
await Future.wait([
|
||||
_authRepository.clearLocalData(),
|
||||
Store.delete(StoreKey.currentUser),
|
||||
Store.delete(StoreKey.accessToken),
|
||||
SessionRepository.instance.clear([SessionKey.accessToken]),
|
||||
SettingsRepository.instance.clear(const [
|
||||
.networkAutoEndpointSwitching,
|
||||
.networkPreferredWifiName,
|
||||
|
||||
@@ -13,6 +13,7 @@ import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/session.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||
@@ -389,7 +390,7 @@ class BackgroundUploadService {
|
||||
String? latitude,
|
||||
String? longitude,
|
||||
}) async {
|
||||
final serverEndpoint = Store.get(StoreKey.serverEndpoint);
|
||||
final serverEndpoint = SessionRepository.instance.session.serverEndpoint!;
|
||||
final url = Uri.parse('$serverEndpoint/assets').toString();
|
||||
final headers = ApiService.getRequestHeaders();
|
||||
final deviceId = Store.get(StoreKey.deviceId);
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/session.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
|
||||
import 'package:immich_mobile/utils/debug_print.dart';
|
||||
@@ -53,6 +54,8 @@ abstract final class Bootstrap {
|
||||
|
||||
await StoreService.init(storeRepository: storeRepo, listenUpdates: listenStoreUpdates);
|
||||
|
||||
await SessionRepository.ensureInitialized(drift);
|
||||
|
||||
final settingsRepo = await SettingsRepository.ensureInitialized(drift);
|
||||
final logDb = await _initLogger(settingsRepository: settingsRepo, shouldBufferLogs: shouldBufferLogs);
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/session.repository.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
String getOriginalUrlForRemoteId(final String id, {bool edited = true}) {
|
||||
return '${Store.get(StoreKey.serverEndpoint)}/assets/$id/original?edited=$edited';
|
||||
return '${SessionRepository.instance.session.serverEndpoint!}/assets/$id/original?edited=$edited';
|
||||
}
|
||||
|
||||
String getThumbnailUrlForRemoteId(
|
||||
@@ -12,14 +11,14 @@ String getThumbnailUrlForRemoteId(
|
||||
bool edited = true,
|
||||
String? thumbhash,
|
||||
}) {
|
||||
final url = '${Store.get(StoreKey.serverEndpoint)}/assets/$id/thumbnail?size=${type.toString()}&edited=$edited';
|
||||
final url = '${SessionRepository.instance.session.serverEndpoint!}/assets/$id/thumbnail?size=$type&edited=$edited';
|
||||
return thumbhash != null ? '$url&c=${Uri.encodeComponent(thumbhash)}' : url;
|
||||
}
|
||||
|
||||
String getPlaybackUrlForRemoteId(final String id) {
|
||||
return '${Store.get(StoreKey.serverEndpoint)}/assets/$id/video/playback?';
|
||||
return '${SessionRepository.instance.session.serverEndpoint!}/assets/$id/video/playback?';
|
||||
}
|
||||
|
||||
String getFaceThumbnailUrl(final String personId) {
|
||||
return '${Store.get(StoreKey.serverEndpoint)}/people/$personId/thumbnail';
|
||||
return '${SessionRepository.instance.session.serverEndpoint!}/people/$personId/thumbnail';
|
||||
}
|
||||
|
||||
@@ -8,19 +8,22 @@ import 'package:immich_mobile/constants/colors.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/config/app_config.dart';
|
||||
import 'package:immich_mobile/domain/models/log.model.dart';
|
||||
import 'package:immich_mobile/domain/models/session.model.dart';
|
||||
import 'package:immich_mobile/domain/models/settings_key.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||
import 'package:immich_mobile/domain/services/feature_message.service.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/session.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/settings.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/session.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
|
||||
import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart';
|
||||
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
||||
|
||||
const int targetVersion = 26;
|
||||
const int targetVersion = 27;
|
||||
|
||||
Future<void> migrateDatabaseIfNeeded(Drift drift) async {
|
||||
final int? storedVersion = Store.tryGet(StoreKey.version);
|
||||
@@ -37,19 +40,22 @@ Future<void> migrateDatabaseIfNeeded(Drift drift) async {
|
||||
if (storedVersion == null) {
|
||||
await FeatureMessageService(SettingsRepository.instance).markSeen();
|
||||
}
|
||||
if (version < 27) {
|
||||
await _migrateTo27(drift);
|
||||
}
|
||||
|
||||
await Store.put(StoreKey.version, targetVersion);
|
||||
return;
|
||||
}
|
||||
|
||||
Future<void> _migrateTo25() async {
|
||||
final accessToken = Store.tryGet(StoreKey.accessToken);
|
||||
final accessToken = Store.tryGet(StoreKey.legacyAccessToken);
|
||||
if (accessToken == null || accessToken.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final urls = <String>[];
|
||||
final serverEndpoint = Store.tryGet(StoreKey.serverEndpoint);
|
||||
final serverEndpoint = Store.tryGet(StoreKey.legacyServerEndpoint);
|
||||
if (serverEndpoint != null && serverEndpoint.isNotEmpty) {
|
||||
urls.add(serverEndpoint);
|
||||
}
|
||||
@@ -80,7 +86,7 @@ Future<void> _migrateTo25() async {
|
||||
}
|
||||
|
||||
Future<void> _migrateTo26(Drift drift) async {
|
||||
final migrator = _StoreMigrator(drift);
|
||||
final migrator = _StoreMigrator.settings(drift);
|
||||
await migrator.migrateEnumIndex(StoreKey.legacyLogLevel, SettingsKey.logLevel, LogLevel.values);
|
||||
// Theme
|
||||
await migrator.migrateEnumName(StoreKey.legacyThemeMode, SettingsKey.themeMode, ThemeMode.values);
|
||||
@@ -145,7 +151,17 @@ Future<void> _migrateTo26(Drift drift) async {
|
||||
await migrator.complete();
|
||||
}
|
||||
|
||||
Future<void> _migrateAlbumSortMode(_StoreMigrator migrator) async {
|
||||
Future<void> _migrateTo27(Drift drift) async {
|
||||
final migrator = _StoreMigrator.session(drift);
|
||||
await migrator.migrateString(StoreKey.legacyServerUrl, SessionKey.serverUrl);
|
||||
await migrator.migrateString(StoreKey.legacyAccessToken, SessionKey.accessToken);
|
||||
await migrator.migrateString(StoreKey.legacyServerEndpoint, SessionKey.serverEndpoint);
|
||||
await migrator.complete();
|
||||
|
||||
await SessionRepository.instance.refresh();
|
||||
}
|
||||
|
||||
Future<void> _migrateAlbumSortMode(_StoreMigrator<SettingsKey> migrator) async {
|
||||
final raw = await migrator.readLegacyStoreInt(StoreKey.legacySelectedAlbumSortOrder.id);
|
||||
final mode = AlbumSortMode.values.firstWhereOrNull((e) => raw != null && e.storeIndex == raw);
|
||||
if (mode == null) {
|
||||
@@ -155,7 +171,7 @@ Future<void> _migrateAlbumSortMode(_StoreMigrator migrator) async {
|
||||
migrator.stage(StoreKey.legacySelectedAlbumSortOrder, SettingsKey.albumSortMode, mode);
|
||||
}
|
||||
|
||||
Future<void> _migrateExternalEndpointList(_StoreMigrator migrator) async {
|
||||
Future<void> _migrateExternalEndpointList(_StoreMigrator<SettingsKey> migrator) async {
|
||||
final raw = await migrator.readLegacyStoreString(StoreKey.legacyExternalEndpointList.id);
|
||||
if (raw == null) {
|
||||
return;
|
||||
@@ -179,7 +195,7 @@ Future<void> _migrateExternalEndpointList(_StoreMigrator migrator) async {
|
||||
migrator.stage(StoreKey.legacyExternalEndpointList, SettingsKey.networkExternalEndpointList, urls);
|
||||
}
|
||||
|
||||
Future<void> _migrateCustomHeaders(_StoreMigrator migrator) async {
|
||||
Future<void> _migrateCustomHeaders(_StoreMigrator<SettingsKey> migrator) async {
|
||||
final raw = await migrator.readLegacyStoreString(StoreKey.legacyCustomHeaders.id);
|
||||
if (raw == null) {
|
||||
return;
|
||||
@@ -202,14 +218,39 @@ Future<void> _migrateCustomHeaders(_StoreMigrator migrator) async {
|
||||
migrator.stage(StoreKey.legacyCustomHeaders, SettingsKey.networkCustomHeaders, headers);
|
||||
}
|
||||
|
||||
class _StoreMigrator {
|
||||
class _StoreMigrator<K extends Enum> {
|
||||
_StoreMigrator._(this._db, {required this.encode, required this.readDefault, required this.insertRow});
|
||||
|
||||
static _StoreMigrator<SettingsKey> settings(Drift db) => _StoreMigrator<SettingsKey>._(
|
||||
db,
|
||||
encode: (key, value) => key.encode(value),
|
||||
readDefault: (key) => defaultConfig.read(key),
|
||||
insertRow: (batch, name, value) => batch.insert(
|
||||
db.settingsEntity,
|
||||
SettingsEntityCompanion(key: Value(name), value: Value(value)),
|
||||
mode: InsertMode.insertOrReplace,
|
||||
),
|
||||
);
|
||||
|
||||
static _StoreMigrator<SessionKey> session(Drift db) => _StoreMigrator<SessionKey>._(
|
||||
db,
|
||||
encode: (key, value) => key.encode(value),
|
||||
readDefault: (key) => defaultSession.read(key),
|
||||
insertRow: (batch, name, value) => batch.insert(
|
||||
db.sessionEntity,
|
||||
SessionEntityCompanion(key: Value(name), value: Value(value)),
|
||||
mode: InsertMode.insertOrReplace,
|
||||
),
|
||||
);
|
||||
|
||||
final Drift _db;
|
||||
final Map<SettingsKey, Object?> _cache = {};
|
||||
final String Function(K key, Object value) encode;
|
||||
final Object? Function(K key) readDefault;
|
||||
final void Function(Batch batch, String name, String? value) insertRow;
|
||||
final Map<K, Object?> _cache = {};
|
||||
final List<int> _migratedStoreIds = [];
|
||||
|
||||
_StoreMigrator(this._db);
|
||||
|
||||
Future<void> migrateEnumIndex<T extends Enum>(StoreKey<int> legacyKey, SettingsKey<T> newKey, List<T> values) async {
|
||||
Future<void> migrateEnumIndex<T extends Enum>(StoreKey<int> legacyKey, K newKey, List<T> values) async {
|
||||
final index = await readLegacyStoreInt(legacyKey.id);
|
||||
if (index == null) {
|
||||
return;
|
||||
@@ -224,11 +265,7 @@ class _StoreMigrator {
|
||||
_migratedStoreIds.add(legacyKey.id);
|
||||
}
|
||||
|
||||
Future<void> migrateEnumName<T extends Enum>(
|
||||
StoreKey<String> legacyKey,
|
||||
SettingsKey<T> newKey,
|
||||
List<T> values,
|
||||
) async {
|
||||
Future<void> migrateEnumName<T extends Enum>(StoreKey<String> legacyKey, K newKey, List<T> values) async {
|
||||
final name = await readLegacyStoreString(legacyKey.id);
|
||||
if (name == null) {
|
||||
return;
|
||||
@@ -243,18 +280,17 @@ class _StoreMigrator {
|
||||
_migratedStoreIds.add(legacyKey.id);
|
||||
}
|
||||
|
||||
Future<void> migrateBool(StoreKey<bool> legacyKey, SettingsKey<bool> newKey) async {
|
||||
Future<void> migrateBool(StoreKey<bool> legacyKey, K newKey) async {
|
||||
final intValue = await readLegacyStoreInt(legacyKey.id);
|
||||
if (intValue == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final boolValue = intValue != 0;
|
||||
_cache[newKey] = boolValue;
|
||||
_cache[newKey] = intValue != 0;
|
||||
_migratedStoreIds.add(legacyKey.id);
|
||||
}
|
||||
|
||||
Future<void> migrateInt(StoreKey<int> legacyKey, SettingsKey<int> newKey) async {
|
||||
Future<void> migrateInt(StoreKey<int> legacyKey, K newKey) async {
|
||||
final intValue = await readLegacyStoreInt(legacyKey.id);
|
||||
if (intValue == null) {
|
||||
return;
|
||||
@@ -264,9 +300,9 @@ class _StoreMigrator {
|
||||
_migratedStoreIds.add(legacyKey.id);
|
||||
}
|
||||
|
||||
Future<void> migrateString(StoreKey<String> legacyKey, SettingsKey<String> newKey) async {
|
||||
Future<void> migrateString(StoreKey<String> legacyKey, K newKey) async {
|
||||
final value = await readLegacyStoreString(legacyKey.id);
|
||||
if (value == null) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -274,7 +310,12 @@ class _StoreMigrator {
|
||||
_migratedStoreIds.add(legacyKey.id);
|
||||
}
|
||||
|
||||
void stage<T, U extends T>(StoreKey legacyKey, SettingsKey<T> newKey, U value) {
|
||||
Future<void> migrateNullableString(StoreKey<String> legacyKey, K newKey) async {
|
||||
_cache[newKey] = await readLegacyStoreString(legacyKey.id);
|
||||
_migratedStoreIds.add(legacyKey.id);
|
||||
}
|
||||
|
||||
void stage(StoreKey legacyKey, K newKey, Object? value) {
|
||||
_cache[newKey] = value;
|
||||
_migratedStoreIds.add(legacyKey.id);
|
||||
}
|
||||
@@ -282,20 +323,12 @@ class _StoreMigrator {
|
||||
Future<void> complete() async {
|
||||
await _db.batch((batch) {
|
||||
for (final entry in _cache.entries) {
|
||||
if (entry.value == defaultConfig.read(entry.key)) {
|
||||
if (entry.value == readDefault(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)),
|
||||
mode: InsertMode.insertOrReplace,
|
||||
);
|
||||
final value = entry.value;
|
||||
insertRow(batch, entry.key.name, value == null ? null : encode(entry.key, value));
|
||||
}
|
||||
});
|
||||
await deleteLegacyStoreRows(_migratedStoreIds);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/session.repository.dart';
|
||||
import 'package:punycode/punycode.dart';
|
||||
|
||||
String sanitizeUrl(String url) {
|
||||
@@ -11,7 +10,7 @@ String sanitizeUrl(String url) {
|
||||
}
|
||||
|
||||
String? getServerUrl() {
|
||||
final serverUrl = punycodeDecodeUrl(Store.tryGet(StoreKey.serverEndpoint));
|
||||
final serverUrl = punycodeDecodeUrl(SessionRepository.instance.session.serverEndpoint);
|
||||
final serverUri = serverUrl != null ? Uri.tryParse(serverUrl) : null;
|
||||
if (serverUri == null) {
|
||||
return null;
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/session.repository.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart';
|
||||
|
||||
Widget userAvatar(BuildContext context, UserDto u, {double? radius}) {
|
||||
final url = "${Store.get(StoreKey.serverEndpoint)}/users/${u.id}/profile-image";
|
||||
final url = "${SessionRepository.instance.session.serverEndpoint!}/users/${u.id}/profile-image";
|
||||
final nameFirstLetter = u.name.isNotEmpty ? u.name[0] : "";
|
||||
return CircleAvatar(
|
||||
radius: radius,
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/session.provider.dart';
|
||||
|
||||
// ignore: must_be_immutable
|
||||
class UserCircleAvatar extends ConsumerWidget {
|
||||
@@ -18,7 +17,7 @@ class UserCircleAvatar extends ConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final userAvatarColor = user.avatarColor.toColor().withValues(alpha: opacity);
|
||||
final profileImageUrl =
|
||||
'${Store.get(StoreKey.serverEndpoint)}/users/${user.id}/profile-image?d=${user.profileChangedAt.millisecondsSinceEpoch}';
|
||||
'${ref.read(sessionProvider).serverEndpoint}/users/${user.id}/profile-image?d=${user.profileChangedAt.millisecondsSinceEpoch}';
|
||||
|
||||
final textColor = (user.avatarColor.toColor().computeLuminance() > 0.5 ? Colors.black : Colors.white).withValues(
|
||||
alpha: opacity,
|
||||
|
||||
@@ -21,13 +21,13 @@ void main() {
|
||||
controller = StreamController<List<StoreDto<Object>>>.broadcast();
|
||||
mockDriftStoreRepo = MockDriftStoreRepository();
|
||||
// For generics, we need to provide fallback to each concrete type to avoid runtime errors
|
||||
registerFallbackValue(StoreKey.accessToken);
|
||||
registerFallbackValue(StoreKey.legacyAccessToken);
|
||||
registerFallbackValue(StoreKey.version);
|
||||
registerFallbackValue(StoreKey.advancedTroubleshooting);
|
||||
|
||||
when(() => mockDriftStoreRepo.getAll()).thenAnswer(
|
||||
(_) async => [
|
||||
const StoreDto(StoreKey.accessToken, _kAccessToken),
|
||||
const StoreDto(StoreKey.legacyAccessToken, _kAccessToken),
|
||||
const StoreDto(StoreKey.advancedTroubleshooting, _kAdvancedTroubleshooting),
|
||||
const StoreDto(StoreKey.version, _kVersion),
|
||||
],
|
||||
@@ -45,7 +45,7 @@ void main() {
|
||||
group("Store Service Init:", () {
|
||||
test('Populates the internal cache on init', () {
|
||||
verify(() => mockDriftStoreRepo.getAll()).called(1);
|
||||
expect(sut.tryGet(StoreKey.accessToken), _kAccessToken);
|
||||
expect(sut.tryGet(StoreKey.legacyAccessToken), _kAccessToken);
|
||||
expect(sut.tryGet(StoreKey.advancedTroubleshooting), _kAdvancedTroubleshooting);
|
||||
expect(sut.tryGet(StoreKey.version), _kVersion);
|
||||
// Other keys should be null
|
||||
@@ -53,19 +53,19 @@ void main() {
|
||||
});
|
||||
|
||||
test('Listens to stream of store updates', () async {
|
||||
final event = StoreDto(StoreKey.accessToken, _kAccessToken.toUpperCase());
|
||||
final event = StoreDto(StoreKey.legacyAccessToken, _kAccessToken.toUpperCase());
|
||||
controller.add([event]);
|
||||
|
||||
await pumpEventQueue();
|
||||
|
||||
verify(() => mockDriftStoreRepo.watchAll()).called(1);
|
||||
expect(sut.tryGet(StoreKey.accessToken), _kAccessToken.toUpperCase());
|
||||
expect(sut.tryGet(StoreKey.legacyAccessToken), _kAccessToken.toUpperCase());
|
||||
});
|
||||
});
|
||||
|
||||
group('Store Service get:', () {
|
||||
test('Returns the stored value for the given key', () {
|
||||
expect(sut.get(StoreKey.accessToken), _kAccessToken);
|
||||
expect(sut.get(StoreKey.legacyAccessToken), _kAccessToken);
|
||||
});
|
||||
|
||||
test('Throws StoreKeyNotFoundException for nonexistent keys', () {
|
||||
@@ -83,15 +83,15 @@ void main() {
|
||||
});
|
||||
|
||||
test('Skip insert when value is not modified', () async {
|
||||
await sut.put(StoreKey.accessToken, _kAccessToken);
|
||||
verifyNever(() => mockDriftStoreRepo.upsert<String>(StoreKey.accessToken, any()));
|
||||
await sut.put(StoreKey.legacyAccessToken, _kAccessToken);
|
||||
verifyNever(() => mockDriftStoreRepo.upsert<String>(StoreKey.legacyAccessToken, any()));
|
||||
});
|
||||
|
||||
test('Insert value when modified', () async {
|
||||
final newAccessToken = _kAccessToken.toUpperCase();
|
||||
await sut.put(StoreKey.accessToken, newAccessToken);
|
||||
verify(() => mockDriftStoreRepo.upsert<String>(StoreKey.accessToken, newAccessToken)).called(1);
|
||||
expect(sut.tryGet(StoreKey.accessToken), newAccessToken);
|
||||
await sut.put(StoreKey.legacyAccessToken, newAccessToken);
|
||||
verify(() => mockDriftStoreRepo.upsert<String>(StoreKey.legacyAccessToken, newAccessToken)).called(1);
|
||||
expect(sut.tryGet(StoreKey.legacyAccessToken), newAccessToken);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -108,7 +108,7 @@ void main() {
|
||||
});
|
||||
|
||||
test('Watches a specific key for changes', () async {
|
||||
final stream = sut.watch(StoreKey.accessToken);
|
||||
final stream = sut.watch(StoreKey.legacyAccessToken);
|
||||
final events = <String?>[_kAccessToken, _kAccessToken.toUpperCase(), null, _kAccessToken.toLowerCase()];
|
||||
|
||||
unawaited(expectLater(stream, emitsInOrder(events)));
|
||||
@@ -118,7 +118,7 @@ void main() {
|
||||
}
|
||||
|
||||
await pumpEventQueue();
|
||||
verify(() => mockDriftStoreRepo.watch<String>(StoreKey.accessToken)).called(1);
|
||||
verify(() => mockDriftStoreRepo.watch<String>(StoreKey.legacyAccessToken)).called(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -128,13 +128,13 @@ void main() {
|
||||
});
|
||||
|
||||
test('Removes the value from the DB', () async {
|
||||
await sut.delete(StoreKey.accessToken);
|
||||
verify(() => mockDriftStoreRepo.delete<String>(StoreKey.accessToken)).called(1);
|
||||
await sut.delete(StoreKey.legacyAccessToken);
|
||||
verify(() => mockDriftStoreRepo.delete<String>(StoreKey.legacyAccessToken)).called(1);
|
||||
});
|
||||
|
||||
test('Removes the value from the cache', () async {
|
||||
await sut.delete(StoreKey.accessToken);
|
||||
expect(sut.tryGet(StoreKey.accessToken), isNull);
|
||||
await sut.delete(StoreKey.legacyAccessToken);
|
||||
expect(sut.tryGet(StoreKey.legacyAccessToken), isNull);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -146,7 +146,7 @@ void main() {
|
||||
test('Clears all values from the store', () async {
|
||||
await sut.clear();
|
||||
verify(() => mockDriftStoreRepo.deleteAll()).called(1);
|
||||
expect(sut.tryGet(StoreKey.accessToken), isNull);
|
||||
expect(sut.tryGet(StoreKey.legacyAccessToken), isNull);
|
||||
expect(sut.tryGet(StoreKey.advancedTroubleshooting), isNull);
|
||||
expect(sut.tryGet(StoreKey.version), isNull);
|
||||
});
|
||||
|
||||
4
mobile/test/drift/main/generated/schema.dart
generated
4
mobile/test/drift/main/generated/schema.dart
generated
@@ -35,6 +35,7 @@ import 'schema_v28.dart' as v28;
|
||||
import 'schema_v29.dart' as v29;
|
||||
import 'schema_v30.dart' as v30;
|
||||
import 'schema_v31.dart' as v31;
|
||||
import 'schema_v32.dart' as v32;
|
||||
|
||||
class GeneratedHelper implements SchemaInstantiationHelper {
|
||||
@override
|
||||
@@ -102,6 +103,8 @@ class GeneratedHelper implements SchemaInstantiationHelper {
|
||||
return v30.DatabaseAtV30(db);
|
||||
case 31:
|
||||
return v31.DatabaseAtV31(db);
|
||||
case 32:
|
||||
return v32.DatabaseAtV32(db);
|
||||
default:
|
||||
throw MissingSchemaException(version, versions);
|
||||
}
|
||||
@@ -139,5 +142,6 @@ class GeneratedHelper implements SchemaInstantiationHelper {
|
||||
29,
|
||||
30,
|
||||
31,
|
||||
32,
|
||||
];
|
||||
}
|
||||
|
||||
10246
mobile/test/drift/main/generated/schema_v32.dart
generated
Normal file
10246
mobile/test/drift/main/generated/schema_v32.dart
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@ import 'package:drift/drift.dart';
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
|
||||
|
||||
@@ -35,4 +36,75 @@ void main() {
|
||||
expect(albums[3].id, '2'); // excluded
|
||||
});
|
||||
});
|
||||
|
||||
group('processDelta', () {
|
||||
// Regression for #22844: an asset moved (not copied) into a backup album on
|
||||
// Android was dropped. The delta reports only the asset's new album, and the
|
||||
// stale link to its old album made the per-album delete sweep wipe the asset.
|
||||
test('keeps an asset moved to another album that still holds other assets', () async {
|
||||
final localAlbumRepo = mediumFactory.getRepository<DriftLocalAlbumRepository>();
|
||||
|
||||
final moved = _localAsset('moved');
|
||||
final other = _localAsset('other');
|
||||
await localAlbumRepo.upsert(
|
||||
mediumFactory.localAlbum(id: 'src', backupSelection: BackupSelection.none),
|
||||
toUpsert: [moved, other],
|
||||
);
|
||||
final anchor = _localAsset('anchor');
|
||||
await localAlbumRepo.upsert(
|
||||
mediumFactory.localAlbum(id: 'dst', backupSelection: BackupSelection.selected),
|
||||
toUpsert: [anchor],
|
||||
);
|
||||
|
||||
// Delta reports the moved asset now living only in the destination album.
|
||||
await localAlbumRepo.processDelta(
|
||||
updates: [moved],
|
||||
deletes: [],
|
||||
assetAlbums: {
|
||||
'moved': ['dst'],
|
||||
},
|
||||
);
|
||||
|
||||
// Per-album delete sweep, as sync() runs on Android. src now physically
|
||||
// holds only `other`; dst holds `anchor` and the moved asset.
|
||||
await localAlbumRepo.syncDeletes('src', ['other']);
|
||||
await localAlbumRepo.syncDeletes('dst', ['anchor', 'moved']);
|
||||
|
||||
final dstIds = (await localAlbumRepo.getAssets('dst')).map((a) => a.id).toSet();
|
||||
final srcIds = await localAlbumRepo.getAssetIds('src');
|
||||
|
||||
expect(dstIds, contains('moved')); // survived and linked to the backed-up album
|
||||
expect(srcIds, isNot(contains('moved'))); // stale source link cleared
|
||||
expect(srcIds, contains('other')); // untouched asset stays put
|
||||
});
|
||||
|
||||
test('replaces album membership with exactly what the delta reports', () async {
|
||||
final localAlbumRepo = mediumFactory.getRepository<DriftLocalAlbumRepository>();
|
||||
|
||||
final moved = _localAsset('moved');
|
||||
await localAlbumRepo.upsert(mediumFactory.localAlbum(id: 'src'), toUpsert: [moved]);
|
||||
await localAlbumRepo.upsert(mediumFactory.localAlbum(id: 'dst'));
|
||||
|
||||
await localAlbumRepo.processDelta(
|
||||
updates: [moved],
|
||||
deletes: [],
|
||||
assetAlbums: {
|
||||
'moved': ['dst'],
|
||||
},
|
||||
);
|
||||
|
||||
expect(await localAlbumRepo.getAssetIds('src'), isEmpty);
|
||||
expect(await localAlbumRepo.getAssetIds('dst'), ['moved']);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
LocalAsset _localAsset(String id) => LocalAsset(
|
||||
id: id,
|
||||
name: '$id.jpg',
|
||||
type: AssetType.image,
|
||||
createdAt: DateTime(2024),
|
||||
updatedAt: DateTime(2024),
|
||||
playbackStyle: AssetPlaybackStyle.image,
|
||||
isEdited: false,
|
||||
);
|
||||
|
||||
@@ -29,7 +29,7 @@ Future<void> _populateStore(Drift db) async {
|
||||
batch.insert(
|
||||
db.storeEntity,
|
||||
StoreEntityCompanion(
|
||||
id: Value(StoreKey.accessToken.id),
|
||||
id: Value(StoreKey.legacyAccessToken.id),
|
||||
intValue: const Value(null),
|
||||
stringValue: const Value(_kTestAccessToken),
|
||||
),
|
||||
@@ -68,10 +68,10 @@ void main() {
|
||||
});
|
||||
|
||||
test('converts string', () async {
|
||||
String? accessToken = await sut.tryGet(StoreKey.accessToken);
|
||||
String? accessToken = await sut.tryGet(StoreKey.legacyAccessToken);
|
||||
expect(accessToken, isNull);
|
||||
await sut.upsert(StoreKey.accessToken, _kTestAccessToken);
|
||||
accessToken = await sut.tryGet(StoreKey.accessToken);
|
||||
await sut.upsert(StoreKey.legacyAccessToken, _kTestAccessToken);
|
||||
accessToken = await sut.tryGet(StoreKey.legacyAccessToken);
|
||||
expect(accessToken, _kTestAccessToken);
|
||||
});
|
||||
|
||||
@@ -147,12 +147,12 @@ void main() {
|
||||
emitsInOrder([
|
||||
[
|
||||
const StoreDto<Object>(StoreKey.version, _kTestVersion),
|
||||
const StoreDto<Object>(StoreKey.accessToken, _kTestAccessToken),
|
||||
const StoreDto<Object>(StoreKey.legacyAccessToken, _kTestAccessToken),
|
||||
const StoreDto<Object>(StoreKey.advancedTroubleshooting, _kTestAdvancedTroubleshooting),
|
||||
],
|
||||
[
|
||||
const StoreDto<Object>(StoreKey.version, _kTestVersion + 10),
|
||||
const StoreDto<Object>(StoreKey.accessToken, _kTestAccessToken),
|
||||
const StoreDto<Object>(StoreKey.legacyAccessToken, _kTestAccessToken),
|
||||
const StoreDto<Object>(StoreKey.advancedTroubleshooting, _kTestAdvancedTroubleshooting),
|
||||
],
|
||||
]),
|
||||
|
||||
118
mobile/test/medium/repositories/session_repository_test.dart
Normal file
118
mobile/test/medium/repositories/session_repository_test.dart
Normal file
@@ -0,0 +1,118 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/domain/models/session.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/session.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/session.repository.dart';
|
||||
|
||||
import '../repository_context.dart';
|
||||
|
||||
void main() {
|
||||
late MediumRepositoryContext ctx;
|
||||
late SessionRepository sut;
|
||||
|
||||
setUpAll(() async {
|
||||
ctx = MediumRepositoryContext();
|
||||
sut = await SessionRepository.ensureInitialized(ctx.db);
|
||||
});
|
||||
|
||||
tearDownAll(() async {
|
||||
await ctx.dispose();
|
||||
});
|
||||
|
||||
setUp(() async {
|
||||
await ctx.db.delete(ctx.db.sessionEntity).go();
|
||||
await SessionRepository.instance.refresh();
|
||||
});
|
||||
|
||||
group('defaults', () {
|
||||
test('session returns null fields when DB is empty', () {
|
||||
expect(sut.session.serverUrl, isNull);
|
||||
expect(sut.session.accessToken, isNull);
|
||||
expect(sut.session.serverEndpoint, isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('write', () {
|
||||
test('persists a value and reflects it in the composed view', () async {
|
||||
await sut.write(.serverEndpoint, 'https://demo.immich.app/api');
|
||||
expect(sut.session.serverEndpoint, 'https://demo.immich.app/api');
|
||||
});
|
||||
|
||||
test('persists across keys independently', () async {
|
||||
await sut.write(.serverUrl, 'https://demo.immich.app');
|
||||
await sut.write(.accessToken, 'token-123');
|
||||
expect(sut.session.serverUrl, 'https://demo.immich.app');
|
||||
expect(sut.session.accessToken, 'token-123');
|
||||
expect(sut.session.serverEndpoint, isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('null values', () {
|
||||
test('a stored NULL value column decodes to null on refresh', () async {
|
||||
await ctx.db
|
||||
.into(ctx.db.sessionEntity)
|
||||
.insert(
|
||||
SessionEntityCompanion.insert(
|
||||
key: SessionKey.accessToken.name,
|
||||
value: const .new(null),
|
||||
updatedAt: .new(DateTime.now()),
|
||||
),
|
||||
);
|
||||
|
||||
await SessionRepository.instance.refresh();
|
||||
expect(sut.session.accessToken, isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('sync', () {
|
||||
test('picks up rows that were inserted directly into the DB', () async {
|
||||
await ctx.db
|
||||
.into(ctx.db.sessionEntity)
|
||||
.insert(
|
||||
SessionEntityCompanion.insert(
|
||||
key: SessionKey.serverEndpoint.name,
|
||||
value: const .new('https://demo.immich.app/api'),
|
||||
updatedAt: .new(DateTime.now()),
|
||||
),
|
||||
);
|
||||
expect(sut.session.serverEndpoint, isNull);
|
||||
|
||||
await SessionRepository.instance.refresh();
|
||||
expect(sut.session.serverEndpoint, 'https://demo.immich.app/api');
|
||||
});
|
||||
|
||||
test('drops cached values for rows that were deleted out from under the repo', () async {
|
||||
await sut.write(.serverEndpoint, 'https://demo.immich.app/api');
|
||||
await ctx.db.delete(ctx.db.sessionEntity).go();
|
||||
expect(sut.session.serverEndpoint, 'https://demo.immich.app/api');
|
||||
|
||||
await SessionRepository.instance.refresh();
|
||||
expect(sut.session.serverEndpoint, isNull);
|
||||
});
|
||||
|
||||
test('skips rows whose key is unknown to SessionKey', () async {
|
||||
await ctx.db
|
||||
.into(ctx.db.sessionEntity)
|
||||
.insert(
|
||||
SessionEntityCompanion.insert(
|
||||
key: 'session.unknown.future-key',
|
||||
value: const .new('unknown'),
|
||||
updatedAt: .new(DateTime.now()),
|
||||
),
|
||||
);
|
||||
|
||||
await SessionRepository.instance.refresh();
|
||||
expect(sut.session.serverEndpoint, isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('watch', () {
|
||||
test('watchSession emits the new value after a write', () async {
|
||||
final expectation = expectLater(
|
||||
sut.watch().map((s) => s.serverEndpoint),
|
||||
emitsThrough('https://demo.immich.app/api/watch'),
|
||||
);
|
||||
await sut.write(SessionKey.serverEndpoint, 'https://demo.immich.app/api/watch');
|
||||
await expectation;
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -142,13 +142,13 @@ void main() {
|
||||
|
||||
group('watch', () {
|
||||
test('watchAppConfig emits the new value after a write', () async {
|
||||
final expectation = expectLater(sut.watchConfig().map((c) => c.theme.mode), emitsThrough(ThemeMode.dark));
|
||||
final expectation = expectLater(sut.watch().map((c) => c.theme.mode), emitsThrough(ThemeMode.dark));
|
||||
await sut.write(SettingsKey.themeMode, ThemeMode.dark);
|
||||
await expectation;
|
||||
});
|
||||
|
||||
test('watchConfig emits the new value after a write', () async {
|
||||
final expectation = expectLater(sut.watchConfig().map((c) => c.logLevel), emitsThrough(LogLevel.warning));
|
||||
final expectation = expectLater(sut.watch().map((c) => c.logLevel), emitsThrough(LogLevel.warning));
|
||||
await sut.write(SettingsKey.logLevel, LogLevel.warning);
|
||||
await expectation;
|
||||
});
|
||||
|
||||
@@ -25,6 +25,8 @@ class _FrozenBucketService implements TimelineService {
|
||||
}
|
||||
|
||||
class _EmptyBucketService implements TimelineService {
|
||||
const _EmptyBucketService();
|
||||
|
||||
@override
|
||||
Stream<List<Bucket>> Function() get watchBuckets =>
|
||||
() => Stream.value(const []);
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/domain/services/store.service.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/session.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
|
||||
import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart';
|
||||
import 'package:immich_mobile/services/auth.service.dart';
|
||||
@@ -44,6 +45,7 @@ void main() {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true));
|
||||
await StoreService.init(storeRepository: DriftStoreRepository(db));
|
||||
await SessionRepository.ensureInitialized(db);
|
||||
});
|
||||
|
||||
tearDownAll(() async {
|
||||
|
||||
@@ -7,10 +7,12 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/session.model.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/services/store.service.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/session.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
|
||||
import 'package:immich_mobile/services/background_upload.service.dart';
|
||||
@@ -39,8 +41,8 @@ void main() {
|
||||
db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true));
|
||||
await StoreService.init(storeRepository: DriftStoreRepository(db));
|
||||
await SettingsRepository.ensureInitialized(db);
|
||||
|
||||
await Store.put(StoreKey.serverEndpoint, 'http://test-server.com');
|
||||
await SessionRepository.ensureInitialized(db);
|
||||
await SessionRepository.instance.write(SessionKey.serverEndpoint, 'https://demo.immich.app');
|
||||
await Store.put(StoreKey.deviceId, 'test-device-id');
|
||||
});
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/services/store.service.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/session.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
|
||||
import 'package:immich_mobile/repositories/upload.repository.dart';
|
||||
@@ -38,8 +39,8 @@ void main() {
|
||||
db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true));
|
||||
await StoreService.init(storeRepository: DriftStoreRepository(db));
|
||||
await SettingsRepository.ensureInitialized(db);
|
||||
|
||||
await Store.put(StoreKey.serverEndpoint, 'http://demo.immich.app');
|
||||
await SessionRepository.ensureInitialized(db);
|
||||
await SessionRepository.instance.write(.serverEndpoint, 'http://demo.immich.app');
|
||||
await Store.put(StoreKey.deviceId, 'device-id');
|
||||
|
||||
registerFallbackValue(File('file'));
|
||||
|
||||
@@ -5,11 +5,11 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/locales.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/domain/services/store.service.dart';
|
||||
import 'package:immich_mobile/generated/codegen_loader.g.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/session.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.widget.dart';
|
||||
@@ -50,7 +50,8 @@ class PresentationContext {
|
||||
if (_db == null) {
|
||||
final db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true));
|
||||
await StoreService.init(storeRepository: DriftStoreRepository(db), listenUpdates: false);
|
||||
await StoreService.I.put(StoreKey.serverEndpoint, serverEndpoint);
|
||||
await SessionRepository.ensureInitialized(db);
|
||||
await SessionRepository.instance.write(.serverEndpoint, serverEndpoint);
|
||||
_db = db;
|
||||
}
|
||||
return PresentationContext._(user: UserFactory.createDto());
|
||||
|
||||
99
mobile/test/unit/utils/value_codec_test.dart
Normal file
99
mobile/test/unit/utils/value_codec_test.dart
Normal file
@@ -0,0 +1,99 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
"format": "prettier --cache --check i18n/",
|
||||
"format:fix": "prettier --cache --write --list-different i18n"
|
||||
},
|
||||
"packageManager": "pnpm@11.11.0",
|
||||
"packageManager": "pnpm@11.13.1",
|
||||
"engines": {
|
||||
"pnpm": ">=10.0.0"
|
||||
},
|
||||
|
||||
@@ -46,6 +46,7 @@ export default typescriptEslint.config([
|
||||
'unicorn/no-process-exit': 'off',
|
||||
'unicorn/import-style': 'off',
|
||||
'unicorn/consistent-class-member-order': 'off',
|
||||
'unicorn/prefer-simple-condition-first': 'off',
|
||||
curly: 2,
|
||||
// prefer the typescript-eslint type-aware version
|
||||
'unicorn/require-array-sort-compare': 'off',
|
||||
|
||||
@@ -33,12 +33,13 @@
|
||||
"eslint": "^10.0.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-unicorn": "^70.0.0",
|
||||
"eslint-plugin-unicorn": "^72.0.0",
|
||||
"globals": "^17.0.0",
|
||||
"mock-fs": "^5.2.0",
|
||||
"prettier": "^3.7.4",
|
||||
"prettier-plugin-organize-imports": "^4.0.0",
|
||||
"typescript": "^6.0.0",
|
||||
"@typescript/native": "npm:typescript@^7.0.2",
|
||||
"typescript": "npm:@typescript/typescript6@^6.0.2",
|
||||
"typescript-eslint": "^8.58.0",
|
||||
"vite": "^8.0.0",
|
||||
"vitest": "^4.0.0",
|
||||
|
||||
@@ -13,5 +13,5 @@
|
||||
"oidc-provider": "^9.0.0",
|
||||
"tsx": "^4.20.6"
|
||||
},
|
||||
"packageManager": "pnpm@11.11.0"
|
||||
"packageManager": "pnpm@11.13.1"
|
||||
}
|
||||
|
||||
@@ -16,6 +16,6 @@
|
||||
"@immich/sdk": "workspace:*",
|
||||
"@immich/plugin-sdk": "workspace:*",
|
||||
"esbuild": "^0.28.0",
|
||||
"typescript": "^6.0.0"
|
||||
"typescript": "^7.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,14 +27,14 @@
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"packageManager": "pnpm@11.11.0",
|
||||
"packageManager": "pnpm@11.13.1",
|
||||
"devDependencies": {
|
||||
"@extism/js-pdk": "^1.1.1",
|
||||
"@immich/sdk": "workspace:*",
|
||||
"@types/node": "^24.13.3",
|
||||
"esbuild": "^0.28.0",
|
||||
"tsc-alias": "^1.8.16",
|
||||
"typescript": "^6.0.0"
|
||||
"typescript": "^7.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@extism/js-pdk": "^1.1.1"
|
||||
|
||||
@@ -32,5 +32,5 @@
|
||||
"vite": "^8.0.16",
|
||||
"vitest": "^4.1.8"
|
||||
},
|
||||
"packageManager": "pnpm@11.11.0"
|
||||
"packageManager": "pnpm@11.13.1"
|
||||
}
|
||||
|
||||
@@ -25,6 +25,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.13.3",
|
||||
"typescript": "^6.0.0"
|
||||
"typescript": "^7.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
1409
pnpm-lock.yaml
generated
1409
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
FROM ghcr.io/immich-app/base-server-dev:202607132058@sha256:968709a3668167f11ef8ece5962e72ea8a4231e841f98bd5e0f35da4cc295cc7 AS builder
|
||||
FROM ghcr.io/immich-app/base-server-dev:202607211135@sha256:83c9ff3f7390111596a2dcd24a746c3f8618d58259dbc7aae36618d153462f18 AS builder
|
||||
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
|
||||
CI=1 \
|
||||
COREPACK_HOME=/tmp \
|
||||
@@ -56,7 +56,7 @@ FROM builder AS plugins
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
COPY --from=ghcr.io/jdx/mise:2026.7.7@sha256:ead960a5f0d2834a78e4640b8e1579d0fcf08cc52a16810dba33b06abf13282e /usr/local/bin/mise /usr/local/bin/mise
|
||||
COPY --from=ghcr.io/jdx/mise:2026.7.11@sha256:6599c81b0da6206cbf5151df97a36794d70aaec4574ceb92d13783b42080b055 /usr/local/bin/mise /usr/local/bin/mise
|
||||
|
||||
WORKDIR /app
|
||||
COPY ./mise.toml ./mise.toml
|
||||
@@ -80,7 +80,7 @@ RUN --mount=type=cache,id=pnpm-packages,target=/buildcache/pnpm-store \
|
||||
--mount=type=cache,id=mise-tools-${TARGETPLATFORM},target=/buildcache/mise \
|
||||
mise //:plugins
|
||||
|
||||
FROM ghcr.io/immich-app/base-server-prod:202607132058@sha256:85cca651310842265015f26a6710bb7420a54a8fd1ec22d519b7b620f5f93bb1
|
||||
FROM ghcr.io/immich-app/base-server-prod:202607211135@sha256:ced131da7523544fe975cfd25abd67386e712e39496340b437cd95a08d0a18f3
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
ENV NODE_ENV=production \
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# dev build
|
||||
FROM ghcr.io/immich-app/base-server-dev:202607132058@sha256:968709a3668167f11ef8ece5962e72ea8a4231e841f98bd5e0f35da4cc295cc7 AS dev
|
||||
FROM ghcr.io/immich-app/base-server-dev:202607211135@sha256:83c9ff3f7390111596a2dcd24a746c3f8618d58259dbc7aae36618d153462f18 AS dev
|
||||
|
||||
|
||||
COPY --from=ghcr.io/jdx/mise:2026.7.7@sha256:ead960a5f0d2834a78e4640b8e1579d0fcf08cc52a16810dba33b06abf13282e /usr/local/bin/mise /usr/local/bin/mise
|
||||
COPY --from=ghcr.io/jdx/mise:2026.7.11@sha256:6599c81b0da6206cbf5151df97a36794d70aaec4574ceb92d13783b42080b055 /usr/local/bin/mise /usr/local/bin/mise
|
||||
|
||||
RUN echo "devdir=/buildcache/node-gyp" >> /usr/local/etc/npmrc && \
|
||||
echo "store-dir=/buildcache/pnpm-store" >> /usr/local/etc/npmrc && \
|
||||
|
||||
@@ -64,6 +64,7 @@ export default typescriptEslint.config([
|
||||
'unicorn/consistent-boolean-name': 'off',
|
||||
'unicorn/no-computed-property-existence-check': 'off',
|
||||
'unicorn/no-non-function-verb-prefix': 'off',
|
||||
'unicorn/prefer-simple-condition-first': 'off',
|
||||
// prefer the typescript-eslint type-aware version
|
||||
'unicorn/require-array-sort-compare': 'off',
|
||||
'@typescript-eslint/require-array-sort-compare': 'error',
|
||||
|
||||
@@ -151,7 +151,7 @@
|
||||
"eslint": "^10.0.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-unicorn": "^70.0.0",
|
||||
"eslint-plugin-unicorn": "^72.0.0",
|
||||
"globals": "^17.0.0",
|
||||
"mock-fs": "^5.2.0",
|
||||
"pngjs": "^7.0.0",
|
||||
@@ -161,7 +161,8 @@
|
||||
"supertest": "^7.1.0",
|
||||
"tailwindcss": "^3.4.0",
|
||||
"testcontainers": "^12.0.0",
|
||||
"typescript": "^6.0.0",
|
||||
"@typescript/native": "npm:typescript@^7.0.2",
|
||||
"typescript": "npm:@typescript/typescript6@^6.0.2",
|
||||
"typescript-eslint": "^8.28.0",
|
||||
"unplugin-swc": "^1.4.5",
|
||||
"vite-tsconfig-paths": "^6.0.0",
|
||||
|
||||
@@ -128,7 +128,7 @@ export class AssetMediaController {
|
||||
this.logger.deprecate(
|
||||
'Calling the thumbnail endpoint with size=original is deprecated. Use the :id/original endpoint instead',
|
||||
);
|
||||
const [_, reqSearch] = req.url.split('?');
|
||||
const [_, reqSearch] = req.url.split('?', 2);
|
||||
const redirSearchParams = new URLSearchParams(reqSearch);
|
||||
redirSearchParams.delete('size');
|
||||
return res.redirect('original?' + redirSearchParams.toString());
|
||||
@@ -142,7 +142,7 @@ export class AssetMediaController {
|
||||
// viewThumbnailRes is a AssetMediaRedirectResponse
|
||||
// which redirects to the original asset or a specific size to make better use of caching
|
||||
const { targetSize } = viewThumbnailRes;
|
||||
const [reqPath, reqSearch] = req.url.split('?');
|
||||
const [reqPath, reqSearch] = req.url.split('?', 2);
|
||||
let redirPath: string;
|
||||
const redirSearchParams = new URLSearchParams(reqSearch);
|
||||
if (targetSize === 'original') {
|
||||
|
||||
@@ -313,7 +313,7 @@ export class MediaRepository {
|
||||
if (!line) {
|
||||
return;
|
||||
}
|
||||
const [ptsStr, durationStr, flags] = line.split(',');
|
||||
const [ptsStr, durationStr, flags] = line.split(',', 3);
|
||||
const pts = Number.parseInt(ptsStr);
|
||||
const duration = Number.parseInt(durationStr);
|
||||
if (Number.isNaN(pts) || Number.isNaN(duration) || !flags) {
|
||||
|
||||
@@ -269,6 +269,10 @@ describe(AssetMediaService.name, () => {
|
||||
'random-uuid.jpg',
|
||||
);
|
||||
});
|
||||
|
||||
it('should accept filenames with just an extension', () => {
|
||||
expect(sut.getUploadFilename(uploadFile.filename(UploadFieldName.ASSET_DATA, '.jpg'))).toEqual('random-uuid.jpg');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUploadFolder', () => {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { BadRequestException, Injectable, InternalServerErrorException, NotFoundException } from '@nestjs/common';
|
||||
import { extname } from 'node:path';
|
||||
import sanitize from 'sanitize-filename';
|
||||
import { StorageCore } from 'src/cores/storage.core';
|
||||
import { AuthSharedLink } from 'src/database';
|
||||
@@ -92,8 +91,7 @@ export class AssetMediaService extends BaseService {
|
||||
getUploadFilename({ auth, fieldName, file, body }: UploadRequest): string {
|
||||
requireUploadAccess(auth);
|
||||
|
||||
const extension = extname(body.filename || file.originalName);
|
||||
|
||||
const extension = getFilenameExtension(body.filename || file.originalName);
|
||||
const lookup = {
|
||||
[UploadFieldName.ASSET_DATA]: extension,
|
||||
[UploadFieldName.SIDECAR_DATA]: '.xmp',
|
||||
|
||||
@@ -461,7 +461,7 @@ export class AuthService extends BaseService {
|
||||
}
|
||||
|
||||
private getBearerToken(headers: IncomingHttpHeaders): string | null {
|
||||
const [type, token] = (headers.authorization || '').split(' ');
|
||||
const [type, token] = (headers.authorization || '').split(' ', 2);
|
||||
if (type.toLowerCase() === 'bearer') {
|
||||
return token;
|
||||
}
|
||||
|
||||
@@ -214,7 +214,6 @@ export class DatabaseBackupService {
|
||||
bin: `/usr/lib/postgresql/${databaseMajorVersion}/bin/${bin}`,
|
||||
args,
|
||||
databaseUsername,
|
||||
// eslint-disable-next-line unicorn/prefer-minimal-ternary
|
||||
databasePassword: isUrlConnection ? new URL(databaseConfig.url).password : databaseConfig.password,
|
||||
databaseVersion,
|
||||
databaseMajorVersion,
|
||||
|
||||
@@ -15,6 +15,7 @@ import { makeStream, newTestService, ServiceMocks } from 'test/utils';
|
||||
import { vitest } from 'vitest';
|
||||
|
||||
async function* mockWalk() {
|
||||
// eslint-disable-next-line unicorn/no-useless-promise-resolve-reject
|
||||
yield await Promise.resolve(['/data/user1/photo.jpg']);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ import { ArgOf } from 'src/repositories/event.repository';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { JobOf, StorageAsset } from 'src/types';
|
||||
import { getAssetFile } from 'src/utils/asset.util';
|
||||
import { getLivePhotoMotionFilename } from 'src/utils/file';
|
||||
import { getFilenameExtension, getLivePhotoMotionFilename } from 'src/utils/file';
|
||||
|
||||
const storageTokens = {
|
||||
secondOptions: ['s', 'ss', 'SSS'],
|
||||
@@ -267,10 +267,10 @@ export class StorageTemplateService extends BaseService {
|
||||
const { storageLabel, filename } = metadata;
|
||||
|
||||
try {
|
||||
const filenameWithoutExtension = path.basename(filename, path.extname(filename));
|
||||
const filenameWithoutExtension = path.basename(filename, getFilenameExtension(filename));
|
||||
|
||||
const source = asset.originalPath;
|
||||
let extension = path.extname(source).split('.').pop() as string;
|
||||
let extension = getFilenameExtension(source).split('.').pop() as string;
|
||||
const sanitized = sanitize(path.basename(filenameWithoutExtension, `.${extension}`));
|
||||
extension = extension?.toLowerCase();
|
||||
const rootPath = StorageCore.getLibraryFolder({ id: asset.ownerId, storageLabel });
|
||||
|
||||
@@ -51,7 +51,7 @@ export class SystemConfigService extends BaseService {
|
||||
@OnEvent({ name: 'ConfigValidate' })
|
||||
onConfigValidate({ newConfig, oldConfig }: ArgOf<'ConfigValidate'>) {
|
||||
const { logLevel } = this.configRepository.getEnv();
|
||||
if (!_.isEqual(toPlainObject(newConfig.logging), oldConfig.logging) && logLevel) {
|
||||
if (logLevel && !_.isEqual(toPlainObject(newConfig.logging), oldConfig.logging)) {
|
||||
throw new Error('Logging cannot be changed while the environment variable IMMICH_LOG_LEVEL is set.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,15 +9,19 @@ import { ImmichReadStream } from 'src/repositories/storage.repository';
|
||||
import { isConnectionAborted } from 'src/utils/misc';
|
||||
|
||||
export function getFileNameWithoutExtension(path: string): string {
|
||||
return basename(path, extname(path));
|
||||
return basename(path, getFilenameExtension(path));
|
||||
}
|
||||
|
||||
export function getFilenameExtension(path: string): string {
|
||||
return extname(path);
|
||||
export function getFilenameExtension(path: string) {
|
||||
const extension = extname(path);
|
||||
if (!extension && path.startsWith('.') && !path.includes('.', 1)) {
|
||||
return path;
|
||||
}
|
||||
return extension;
|
||||
}
|
||||
|
||||
export function getLivePhotoMotionFilename(stillName: string, motionName: string) {
|
||||
return getFileNameWithoutExtension(stillName) + extname(motionName);
|
||||
return getFileNameWithoutExtension(stillName) + getFilenameExtension(motionName);
|
||||
}
|
||||
|
||||
export class ImmichFileResponse {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { extname } from 'node:path';
|
||||
import { AssetType } from 'src/enum';
|
||||
import { getFilenameExtension } from 'src/utils/file';
|
||||
|
||||
const raw = {
|
||||
'.3fr': ['image/3fr', 'image/x-hasselblad-3fr'],
|
||||
@@ -132,10 +132,12 @@ const sidecar: Record<string, string[]> = {
|
||||
|
||||
const types = { ...image, ...video, ...sidecar };
|
||||
|
||||
const isType = (filename: string, record: Record<string, string[]>) =>
|
||||
Object.hasOwn(record, extname(filename).toLowerCase());
|
||||
const isType = (filename: string, r: Record<string, string[]>) =>
|
||||
Object.hasOwn(r, getFilenameExtension(filename).toLowerCase());
|
||||
|
||||
const lookup = (filename: string) =>
|
||||
types[getFilenameExtension(filename).toLowerCase()]?.[0] ?? 'application/octet-stream';
|
||||
|
||||
const lookup = (filename: string) => types[extname(filename).toLowerCase()]?.[0] ?? 'application/octet-stream';
|
||||
const toExtension = (mimeType: string) => {
|
||||
return (
|
||||
extensionOverrides[mimeType] || Object.entries(types).find(([, mimeTypes]) => mimeTypes.includes(mimeType))?.[0]
|
||||
@@ -158,7 +160,8 @@ export const mimeTypes = {
|
||||
isProfile: (filename: string) => isType(filename, profile),
|
||||
isSidecar: (filename: string) => isType(filename, sidecar),
|
||||
isVideo: (filename: string) => isType(filename, video),
|
||||
canBeTransparent: (filename: string) => transparentCapableExtensions.has(extname(filename).toLowerCase()),
|
||||
canBeTransparent: (filename: string) =>
|
||||
transparentCapableExtensions.has(getFilenameExtension(filename).toLowerCase()),
|
||||
isRaw: (filename: string) => isType(filename, raw),
|
||||
lookup,
|
||||
/** return an extension (including a leading `.`) for a mime-type */
|
||||
@@ -169,7 +172,7 @@ export const mimeTypes = {
|
||||
return AssetType.Image;
|
||||
}
|
||||
|
||||
if (contentType.startsWith('video/') || contentType === 'application/mxf') {
|
||||
if (contentType === 'application/mxf' || contentType.startsWith('video/')) {
|
||||
return AssetType.Video;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ type Impossible<K extends keyof any> = {
|
||||
type Exact<T, U extends T = T> = U & Impossible<Exclude<keyof U, keyof T>>;
|
||||
|
||||
export const fromAck = (ack: string): SyncAck => {
|
||||
const [type, updateId, extraId] = ack.split('|');
|
||||
const [type, updateId, extraId] = ack.split('|', 3);
|
||||
return { type: type as SyncEntityType, updateId, extraId };
|
||||
};
|
||||
|
||||
|
||||
@@ -146,6 +146,7 @@ export default typescriptEslint.config(
|
||||
'unicorn/no-non-function-verb-prefix': 'off',
|
||||
'unicorn/prefer-minimal-ternary': 'off',
|
||||
'unicorn/no-empty-file': 'off',
|
||||
'unicorn/prefer-simple-condition-first': 'off',
|
||||
// prefer the typescript-eslint type-aware version
|
||||
'unicorn/require-array-sort-compare': 'off',
|
||||
'@typescript-eslint/require-array-sort-compare': 'error',
|
||||
|
||||
@@ -97,7 +97,7 @@
|
||||
"eslint-plugin-better-tailwindcss": "^4.5.0",
|
||||
"eslint-plugin-compat": "^7.0.0",
|
||||
"eslint-plugin-svelte": "^3.12.4",
|
||||
"eslint-plugin-unicorn": "^70.0.0",
|
||||
"eslint-plugin-unicorn": "^72.0.0",
|
||||
"factory.ts": "^1.4.1",
|
||||
"globals": "^17.0.0",
|
||||
"happy-dom": "^20.0.0",
|
||||
@@ -105,11 +105,12 @@
|
||||
"prettier-plugin-sort-json": "^4.1.1",
|
||||
"prettier-plugin-svelte": "^4.0.0",
|
||||
"rollup-plugin-visualizer": "^7.0.0",
|
||||
"svelte": "5.56.4",
|
||||
"svelte": "5.56.5",
|
||||
"svelte-check": "^4.4.6",
|
||||
"svelte-eslint-parser": "^1.3.3",
|
||||
"tailwindcss": "^4.2.4",
|
||||
"typescript": "^6.0.0",
|
||||
"@typescript/native": "npm:typescript@^7.0.2",
|
||||
"typescript": "npm:@typescript/typescript6@^6.0.2",
|
||||
"typescript-eslint": "^8.45.0",
|
||||
"vite": "^8.0.0",
|
||||
"vitest": "^4.0.0"
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
continue;
|
||||
}
|
||||
|
||||
const [group] = permission.split('.');
|
||||
const [group] = permission.split('.', 1);
|
||||
if (!Object.hasOwn(permissions, group)) {
|
||||
permissions[group] = [];
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
}
|
||||
|
||||
if (isFirefox && ref) {
|
||||
ref.decode().then(completeLoad, completeLoad);
|
||||
ref.decode().then(completeLoad).catch(completeLoad);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
}
|
||||
|
||||
if (isRotated) {
|
||||
let [width, height] = ratio.value.split(':');
|
||||
let [width, height] = ratio.value.split(':', 2);
|
||||
return `${height}:${width}`;
|
||||
}
|
||||
return ratio.value;
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
{#if session.deviceType || session.deviceOS}
|
||||
<span
|
||||
>{session.deviceOS || $t('unknown')} • {session.deviceType || $t('unknown')}{session.appVersion
|
||||
? `(v${session.appVersion})`
|
||||
? ` (v${session.appVersion})`
|
||||
: ''}</span
|
||||
>
|
||||
{:else}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { describe } from 'vitest';
|
||||
import FormatMessage from '$lib/elements/FormatMessage.svelte';
|
||||
import FormatTagB from '$lib/elements/__test__/FormatTagB.svelte';
|
||||
|
||||
const getSanitizedHTML = (container: HTMLElement) => container.innerHTML.replaceAll('<!---->', '');
|
||||
const getSanitizedHTML = (container: HTMLElement) => container.getHTML().replaceAll('<!---->', '');
|
||||
|
||||
describe('FormatMessage component', () => {
|
||||
beforeAll(async () => {
|
||||
|
||||
@@ -124,7 +124,7 @@ class AuthManager {
|
||||
}
|
||||
|
||||
for (const cookie of document.cookie.split('; ')) {
|
||||
const [name] = cookie.split('=');
|
||||
const [name] = cookie.split('=', 1);
|
||||
if (name === 'immich_is_authenticated') {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -834,7 +834,7 @@ class TransformManager implements EditToolManager {
|
||||
return;
|
||||
}
|
||||
|
||||
const [widthRatio, heightRatio] = aspectRatio.split(':');
|
||||
const [widthRatio, heightRatio] = aspectRatio.split(':', 2);
|
||||
this.setAspectRatio(`${heightRatio}:${widthRatio}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -140,7 +140,7 @@
|
||||
city: emptyStringToNull(filter.location.city),
|
||||
make: emptyStringToNull(filter.camera.make),
|
||||
model: emptyStringToNull(filter.camera.model),
|
||||
lensModel: filter.camera.lensModel,
|
||||
lensModel: emptyStringToNull(filter.camera.lensModel),
|
||||
takenAfter: filter.date.takenAfter
|
||||
? asLocalTimeISO(filter.date.takenAfter.startOf('day') as DateTime<true>)
|
||||
: undefined,
|
||||
|
||||
@@ -281,8 +281,9 @@
|
||||
};
|
||||
|
||||
afterNavigate(({ from, to }) => {
|
||||
memoryManager.ready().then(
|
||||
() => {
|
||||
memoryManager
|
||||
.ready()
|
||||
.then(() => {
|
||||
let target;
|
||||
if (to?.params?.assetId) {
|
||||
target = to;
|
||||
@@ -294,11 +295,10 @@
|
||||
|
||||
init(target);
|
||||
initPlayer();
|
||||
},
|
||||
(error) => {
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(`Error loading memories: ${error}`);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
|
||||
Reference in New Issue
Block a user