Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
621504fcda | ||
|
|
919a4fd9ef | ||
|
|
2ed9207c3f | ||
|
|
c48394aa6e | ||
|
|
86c8b9e04d | ||
|
|
1190d0cc80 | ||
|
|
9669f7cc6e |
@@ -14,7 +14,7 @@ dependencies = [
|
|||||||
"onnx>=1.22.0",
|
"onnx>=1.22.0",
|
||||||
"opencv-python-headless>=4.7.0.72,<5.0",
|
"opencv-python-headless>=4.7.0.72,<5.0",
|
||||||
"orjson>=3.9.5",
|
"orjson>=3.9.5",
|
||||||
"pillow>=12.2,<13",
|
"pillow>=13,<14",
|
||||||
"pydantic>=2.0.0,<3",
|
"pydantic>=2.0.0,<3",
|
||||||
"pydantic-settings>=2.5.2,<3",
|
"pydantic-settings>=2.5.2,<3",
|
||||||
"python-multipart>=0.0.6,<1.0",
|
"python-multipart>=0.0.6,<1.0",
|
||||||
@@ -64,7 +64,7 @@ compile-bytecode = true
|
|||||||
override-dependencies = ["opencv-python ; sys_platform == 'never'"] # only install the headless version
|
override-dependencies = ["opencv-python ; sys_platform == 'never'"] # only install the headless version
|
||||||
|
|
||||||
[tool.uv.sources]
|
[tool.uv.sources]
|
||||||
immich-model = { git = "https://github.com/immich-app/ml-models.git", rev = "8bf3fd1b44b0c8608f52fb6e50ddff52826576d8" }
|
immich-model = { git = "https://github.com/immich-app/ml-models.git", rev = "a1eeff2c2118d76067efecf4cc4630998eac519a" }
|
||||||
onnxruntime-gpu = { index = "onnxruntime-cuda-12" }
|
onnxruntime-gpu = { index = "onnxruntime-cuda-12" }
|
||||||
openvino = { index = "openvino-nightly" }
|
openvino = { index = "openvino-nightly" }
|
||||||
|
|
||||||
|
|||||||
Generated
+719
-531
File diff suppressed because it is too large
Load Diff
@@ -41,6 +41,23 @@ class RemoteAssetRepository extends DatabaseAccessor<Drift> with $RemoteAssetRep
|
|||||||
return _assetSelectable(id).watchSingleOrNull();
|
return _assetSelectable(id).watchSingleOrNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Stream<Set<String>> watchDeletedAssetIds(Iterable<String> ids, AssetVisibility visibility) {
|
||||||
|
final idList = ids.toList(growable: false);
|
||||||
|
if (idList.isEmpty) {
|
||||||
|
return Stream.value(const <String>{});
|
||||||
|
}
|
||||||
|
|
||||||
|
final query = _db.remoteAssetEntity.selectOnly()
|
||||||
|
..addColumns([_db.remoteAssetEntity.id])
|
||||||
|
..where(
|
||||||
|
_db.remoteAssetEntity.id.isIn(idList) &
|
||||||
|
(_db.remoteAssetEntity.deletedAt.isNotNull() |
|
||||||
|
_db.remoteAssetEntity.visibility.equalsValue(visibility).not()),
|
||||||
|
);
|
||||||
|
|
||||||
|
return query.map((row) => row.read(_db.remoteAssetEntity.id)!).watch().map((rows) => rows.toSet());
|
||||||
|
}
|
||||||
|
|
||||||
Future<RemoteAsset?> get(String id) {
|
Future<RemoteAsset?> get(String id) {
|
||||||
return _assetSelectable(id).getSingleOrNull();
|
return _assetSelectable(id).getSingleOrNull();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import 'dart:async';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.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/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/domain/services/search.service.dart';
|
import 'package:immich_mobile/domain/services/search.service.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart';
|
||||||
import 'package:immich_mobile/models/search/search_filter.model.dart';
|
import 'package:immich_mobile/models/search/search_filter.model.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/search.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/search.provider.dart';
|
||||||
|
|
||||||
final searchPreFilterProvider = NotifierProvider<SearchFilterProvider, SearchFilter?>(SearchFilterProvider.new);
|
final searchPreFilterProvider = NotifierProvider<SearchFilterProvider, SearchFilter?>(SearchFilterProvider.new);
|
||||||
@@ -32,16 +34,21 @@ class SearchState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final paginatedSearchProvider = StateNotifierProvider<PaginatedSearchNotifier, SearchState>(
|
final paginatedSearchProvider = StateNotifierProvider<PaginatedSearchNotifier, SearchState>(
|
||||||
(ref) => PaginatedSearchNotifier(ref.watch(searchServiceProvider)),
|
(ref) => PaginatedSearchNotifier(ref.watch(searchServiceProvider), ref.watch(driftProvider).remoteAssetRepository),
|
||||||
);
|
);
|
||||||
|
|
||||||
class PaginatedSearchNotifier extends StateNotifier<SearchState> {
|
class PaginatedSearchNotifier extends StateNotifier<SearchState> {
|
||||||
final SearchService _searchService;
|
final SearchService _searchService;
|
||||||
|
final RemoteAssetRepository _remoteAssetRepository;
|
||||||
final _assetCountController = StreamController<int>.broadcast();
|
final _assetCountController = StreamController<int>.broadcast();
|
||||||
|
|
||||||
PaginatedSearchNotifier(this._searchService) : super(const SearchState());
|
StreamSubscription<Set<String>>? _deletedIdsSubscription;
|
||||||
|
List<BaseAsset> _results = const [];
|
||||||
|
int? _nextPage = 1;
|
||||||
|
|
||||||
Stream<int> get assetCount => _assetCountController.stream;
|
PaginatedSearchNotifier(this._searchService, this._remoteAssetRepository) : super(const SearchState());
|
||||||
|
|
||||||
|
Stream<int> get assetCount => _assetCountController.stream.distinct();
|
||||||
|
|
||||||
Future<void> search(SearchFilter filter) async {
|
Future<void> search(SearchFilter filter) async {
|
||||||
if (state.nextPage == null || state.isLoading) {
|
if (state.nextPage == null || state.isLoading) {
|
||||||
@@ -52,24 +59,47 @@ class PaginatedSearchNotifier extends StateNotifier<SearchState> {
|
|||||||
|
|
||||||
final result = await _searchService.search(filter, state.nextPage!);
|
final result = await _searchService.search(filter, state.nextPage!);
|
||||||
|
|
||||||
|
if (!mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
state = SearchState(assets: state.assets, nextPage: state.nextPage);
|
state = SearchState(assets: state.assets, nextPage: state.nextPage);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final assets = [...state.assets, ...result.assets];
|
_results = [..._results, ...result.assets];
|
||||||
state = SearchState(assets: assets, nextPage: result.nextPage);
|
_nextPage = result.nextPage;
|
||||||
|
_watchDeletedIds(filter.display.isArchive ? AssetVisibility.archive : AssetVisibility.timeline);
|
||||||
_assetCountController.add(assets.length);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void clear() {
|
void clear() {
|
||||||
state = const SearchState();
|
unawaited(_deletedIdsSubscription?.cancel());
|
||||||
_assetCountController.add(0);
|
_deletedIdsSubscription = null;
|
||||||
|
_results = const [];
|
||||||
|
_nextPage = 1;
|
||||||
|
_emit(const {});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _watchDeletedIds(AssetVisibility visibility) {
|
||||||
|
unawaited(_deletedIdsSubscription?.cancel());
|
||||||
|
|
||||||
|
final ids = _results.whereType<RemoteAsset>().map((asset) => asset.id);
|
||||||
|
_deletedIdsSubscription = _remoteAssetRepository.watchDeletedAssetIds(ids, visibility).listen(_emit);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _emit(Set<String> deletedIds) {
|
||||||
|
final visible = deletedIds.isEmpty
|
||||||
|
? _results
|
||||||
|
: _results.where((asset) => asset is! RemoteAsset || !deletedIds.contains(asset.id)).toList(growable: false);
|
||||||
|
|
||||||
|
state = SearchState(assets: visible, nextPage: _nextPage);
|
||||||
|
_assetCountController.add(visible.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
unawaited(_deletedIdsSubscription?.cancel());
|
||||||
unawaited(_assetCountController.close());
|
unawaited(_assetCountController.close());
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@ name: immich_mobile
|
|||||||
description: Immich - selfhosted backup media file on mobile phone
|
description: Immich - selfhosted backup media file on mobile phone
|
||||||
|
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
version: 3.3.5-local.9+3030502
|
version: 3.3.5-local.11+3030503
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.12.0 <4.0.0'
|
sdk: '>=3.12.0 <4.0.0'
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import 'package:async/async.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart';
|
||||||
|
|
||||||
import '../repository_context.dart';
|
import '../repository_context.dart';
|
||||||
@@ -64,4 +66,53 @@ void main() {
|
|||||||
expect(result[0].id, remote.id);
|
expect(result[0].id, remote.id);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('watchDeletedAssetIds', () {
|
||||||
|
late String userId;
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
final user = await ctx.newUser();
|
||||||
|
userId = user.id;
|
||||||
|
await ctx.newAuthUser(id: userId);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('hides trashed and archived assets but keeps matching and unsynced ones', () async {
|
||||||
|
final timeline = await ctx.newRemoteAsset(ownerId: userId);
|
||||||
|
final trashed = await ctx.newRemoteAsset(ownerId: userId, deletedAt: DateTime(2020));
|
||||||
|
final archived = await ctx.newRemoteAsset(ownerId: userId, visibility: AssetVisibility.archive);
|
||||||
|
|
||||||
|
final result = await sut.watchDeletedAssetIds([
|
||||||
|
timeline.id,
|
||||||
|
trashed.id,
|
||||||
|
archived.id,
|
||||||
|
'never-synced',
|
||||||
|
], AssetVisibility.timeline).first;
|
||||||
|
|
||||||
|
expect(result, {trashed.id, archived.id});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('hides assets not matching the requested visibility for archive searches', () async {
|
||||||
|
final timeline = await ctx.newRemoteAsset(ownerId: userId);
|
||||||
|
final archived = await ctx.newRemoteAsset(ownerId: userId, visibility: AssetVisibility.archive);
|
||||||
|
|
||||||
|
final result = await sut.watchDeletedAssetIds([timeline.id, archived.id], AssetVisibility.archive).first;
|
||||||
|
|
||||||
|
expect(result, {timeline.id});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('emits when an asset is trashed and again when it is restored', () async {
|
||||||
|
final asset = await ctx.newRemoteAsset(ownerId: userId);
|
||||||
|
final queue = StreamQueue(sut.watchDeletedAssetIds([asset.id], AssetVisibility.timeline));
|
||||||
|
|
||||||
|
expect(await queue.next, isEmpty);
|
||||||
|
|
||||||
|
await sut.trash([asset.id]);
|
||||||
|
expect(await queue.next, {asset.id});
|
||||||
|
|
||||||
|
await sut.restoreTrash([asset.id]);
|
||||||
|
expect(await queue.next, isEmpty);
|
||||||
|
|
||||||
|
await queue.cancel();
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use std::sync::Arc;
|
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::sync::{Arc, LazyLock};
|
||||||
|
|
||||||
use jxl::api::{
|
use jxl::api::{
|
||||||
AlphaMode, Endianness, Event, ExtraChannel, JxlColorEncoding, JxlColorProfile, JxlColorType,
|
AlphaMode, Endianness, Event, ExtraChannel, JxlColorEncoding, JxlColorProfile, JxlColorType,
|
||||||
@@ -7,6 +7,7 @@ use jxl::api::{
|
|||||||
JxlParallelRunnerFun, JxlPixelFormat, JxlPrimaries, JxlTransferFunction, JxlWhitePoint,
|
JxlParallelRunnerFun, JxlPixelFormat, JxlPrimaries, JxlTransferFunction, JxlWhitePoint,
|
||||||
};
|
};
|
||||||
use moxcms::{Layout, MatrixCoefficients, Transform16BitExecutor};
|
use moxcms::{Layout, MatrixCoefficients, Transform16BitExecutor};
|
||||||
|
use quick_cache::sync::Cache;
|
||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
|
|
||||||
use crate::color::{self, CicpColorPrimaries, CicpProfile, Target, TransferCharacteristics};
|
use crate::color::{self, CicpColorPrimaries, CicpProfile, Target, TransferCharacteristics};
|
||||||
@@ -98,7 +99,7 @@ impl<'a> Jxl<'a> {
|
|||||||
transform: None,
|
transform: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let (transform, layout) = color::to_srgb(&profile.try_as_icc()?)?;
|
let (transform, layout) = srgb_transform(profile)?;
|
||||||
decoder
|
decoder
|
||||||
.set_pixel_format(JxlPixelFormat {
|
.set_pixel_format(JxlPixelFormat {
|
||||||
color_type: match layout {
|
color_type: match layout {
|
||||||
@@ -233,3 +234,23 @@ fn cicp(profile: &JxlColorProfile) -> Option<CicpProfile> {
|
|||||||
full_range: true,
|
full_range: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reuse the transform to sRGB for a given source profile.
|
||||||
|
type Transforms<K> = Cache<K, (Arc<Transform16BitExecutor>, Layout)>;
|
||||||
|
|
||||||
|
/// [`color::to_srgb`] for `profile`, built once per distinct profile.
|
||||||
|
fn srgb_transform(profile: &JxlColorProfile) -> Option<(Arc<Transform16BitExecutor>, Layout)> {
|
||||||
|
const TRANSFORMS: usize = 8;
|
||||||
|
static ENCODINGS: LazyLock<Transforms<String>> = LazyLock::new(|| Cache::new(TRANSFORMS));
|
||||||
|
static ICCS: LazyLock<Transforms<Vec<u8>>> = LazyLock::new(|| Cache::new(TRANSFORMS));
|
||||||
|
match profile {
|
||||||
|
JxlColorProfile::Simple(encoding) => ENCODINGS
|
||||||
|
.get_or_insert_with(&encoding.get_color_encoding_description(), || {
|
||||||
|
color::to_srgb(&profile.try_as_icc().ok_or(())?).ok_or(())
|
||||||
|
}),
|
||||||
|
JxlColorProfile::Icc(icc) => {
|
||||||
|
ICCS.get_or_insert_with(icc.as_slice(), || color::to_srgb(icc).ok_or(()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ok()
|
||||||
|
}
|
||||||
|
|||||||
@@ -83,6 +83,36 @@ fn converts_lossy_icc_to_srgb() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn concurrent_decodes_keep_profile_colors_when_transforms_are_reused() {
|
||||||
|
let cases: [(&[u8], [u8; 8]); 3] = [
|
||||||
|
(
|
||||||
|
include_bytes!("fixtures/p3.jxl"),
|
||||||
|
[255, 0, 0, 255, 255, 255, 255, 255],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
include_bytes!("fixtures/gray_icc.jxl"),
|
||||||
|
[81, 81, 81, 255, 203, 203, 203, 255],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
include_bytes!("fixtures/srgb_alpha.jxl"),
|
||||||
|
[128, 0, 0, 128, 0, 0, 255, 255],
|
||||||
|
),
|
||||||
|
];
|
||||||
|
std::thread::scope(|scope| {
|
||||||
|
for _ in 0..4 {
|
||||||
|
scope.spawn(|| {
|
||||||
|
for _ in 0..8 {
|
||||||
|
for (data, expected) in cases {
|
||||||
|
let rgba = decode_srgb(data).unwrap();
|
||||||
|
assert_eq!(rgba.pixels().as_flattened(), &expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn tone_maps_hdr() {
|
fn tone_maps_hdr() {
|
||||||
let rgba = decode_srgb(include_bytes!("fixtures/pq_grays.jxl")).unwrap();
|
let rgba = decode_srgb(include_bytes!("fixtures/pq_grays.jxl")).unwrap();
|
||||||
|
|||||||
Generated
+5
-5
@@ -1177,8 +1177,8 @@ importers:
|
|||||||
specifier: ^5.5.6
|
specifier: ^5.5.6
|
||||||
version: 5.5.6(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.11.0(jiti@2.7.0)(supports-color@8.1.1)))(eslint@10.11.0(jiti@2.7.0)(supports-color@8.1.1))(prettier@3.9.9)
|
version: 5.5.6(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.11.0(jiti@2.7.0)(supports-color@8.1.1)))(eslint@10.11.0(jiti@2.7.0)(supports-color@8.1.1))(prettier@3.9.9)
|
||||||
eslint-plugin-svelte:
|
eslint-plugin-svelte:
|
||||||
specifier: ^3.12.4
|
specifier: ^3.23.1
|
||||||
version: 3.23.0(eslint@10.11.0(jiti@2.7.0)(supports-color@8.1.1))(svelte@5.57.1(@typescript-eslint/types@8.70.1))
|
version: 3.23.1(eslint@10.11.0(jiti@2.7.0)(supports-color@8.1.1))(svelte@5.57.1(@typescript-eslint/types@8.70.1))
|
||||||
eslint-plugin-unicorn:
|
eslint-plugin-unicorn:
|
||||||
specifier: ^75.0.0
|
specifier: ^75.0.0
|
||||||
version: 75.0.0(eslint@10.11.0(jiti@2.7.0)(supports-color@8.1.1))
|
version: 75.0.0(eslint@10.11.0(jiti@2.7.0)(supports-color@8.1.1))
|
||||||
@@ -7821,8 +7821,8 @@ packages:
|
|||||||
eslint-config-prettier:
|
eslint-config-prettier:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
eslint-plugin-svelte@3.23.0:
|
eslint-plugin-svelte@3.23.1:
|
||||||
resolution: {integrity: sha512-n9jRklDqy0+W834568a4ZIZTK7DHXxvvHHxxGP8nNq7N//pOZMubttskHOquyGgfTR4Z05q2NdGNlsTpIEA48w==}
|
resolution: {integrity: sha512-+w86Jlx4IRUuuvtqD6j4njql+akx+81JnpvbN5de3AV5iq2SexvST/Jr51CiTlKeY0i+vhoKjPO/ywFKIP75sA==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
eslint: ^8.57.1 || ^9.0.0 || ^10.0.0
|
eslint: ^8.57.1 || ^9.0.0 || ^10.0.0
|
||||||
@@ -21185,7 +21185,7 @@ snapshots:
|
|||||||
'@types/eslint': 9.6.1
|
'@types/eslint': 9.6.1
|
||||||
eslint-config-prettier: 10.1.8(eslint@10.11.0(jiti@2.7.0)(supports-color@8.1.1))
|
eslint-config-prettier: 10.1.8(eslint@10.11.0(jiti@2.7.0)(supports-color@8.1.1))
|
||||||
|
|
||||||
eslint-plugin-svelte@3.23.0(eslint@10.11.0(jiti@2.7.0)(supports-color@8.1.1))(svelte@5.57.1(@typescript-eslint/types@8.70.1)):
|
eslint-plugin-svelte@3.23.1(eslint@10.11.0(jiti@2.7.0)(supports-color@8.1.1))(svelte@5.57.1(@typescript-eslint/types@8.70.1)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/eslint-utils': 4.10.1(eslint@10.11.0(jiti@2.7.0)(supports-color@8.1.1))
|
'@eslint-community/eslint-utils': 4.10.1(eslint@10.11.0(jiti@2.7.0)(supports-color@8.1.1))
|
||||||
'@jridgewell/sourcemap-codec': 1.6.0
|
'@jridgewell/sourcemap-codec': 1.6.0
|
||||||
|
|||||||
@@ -818,6 +818,7 @@ export class AssetRepository {
|
|||||||
})
|
})
|
||||||
getTimeBucket(timeBucket: string, options: TimeBucketOptions, auth: AuthDto) {
|
getTimeBucket(timeBucket: string, options: TimeBucketOptions, auth: AuthDto) {
|
||||||
const order = options.order ?? 'desc';
|
const order = options.order ?? 'desc';
|
||||||
|
const bucket = timeBucket.replace(/^[+-]/, '');
|
||||||
const query = this.db
|
const query = this.db
|
||||||
.with('cte', (qb) =>
|
.with('cte', (qb) =>
|
||||||
qb
|
qb
|
||||||
@@ -869,7 +870,14 @@ export class AssetRepository {
|
|||||||
|
|
||||||
return withBoundingBox(withBoundingCircle, bbox);
|
return withBoundingBox(withBoundingCircle, bbox);
|
||||||
})
|
})
|
||||||
.where(truncatedDate(options.orderBy), '=', timeBucket.replace(/^[+-]/, ''))
|
.$if(options.orderBy === AssetOrderBy.CreatedAt, (qb) =>
|
||||||
|
qb
|
||||||
|
.where('asset.createdAt', '>=', sql<Date>`${bucket}::timestamp AT TIME ZONE 'UTC'`)
|
||||||
|
.where('asset.createdAt', '<', sql<Date>`(${bucket}::timestamp + interval '1 month') AT TIME ZONE 'UTC'`),
|
||||||
|
)
|
||||||
|
.$if(options.orderBy !== AssetOrderBy.CreatedAt, (qb) =>
|
||||||
|
qb.where(truncatedDate(options.orderBy), '=', bucket),
|
||||||
|
)
|
||||||
.$if(!!options.albumId, (qb) =>
|
.$if(!!options.albumId, (qb) =>
|
||||||
qb.where((eb) =>
|
qb.where((eb) =>
|
||||||
eb.exists(
|
eb.exists(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { BadRequestException } from '@nestjs/common';
|
import { BadRequestException } from '@nestjs/common';
|
||||||
import { Kysely } from 'kysely';
|
import { Kysely } from 'kysely';
|
||||||
import { AssetVisibility, SharedLinkType } from 'src/enum.js';
|
import { AssetOrderBy, AssetVisibility, SharedLinkType } from 'src/enum.js';
|
||||||
import { AccessRepository } from 'src/repositories/access.repository.js';
|
import { AccessRepository } from 'src/repositories/access.repository.js';
|
||||||
import { AssetRepository } from 'src/repositories/asset.repository.js';
|
import { AssetRepository } from 'src/repositories/asset.repository.js';
|
||||||
import { LoggingRepository } from 'src/repositories/logging.repository.js';
|
import { LoggingRepository } from 'src/repositories/logging.repository.js';
|
||||||
@@ -162,6 +162,34 @@ describe(TimelineService.name, () => {
|
|||||||
const rawResponse = await sut.getTimeBucket(factory.auth(), { timeBucket: '012345-01-01' });
|
const rawResponse = await sut.getTimeBucket(factory.auth(), { timeBucket: '012345-01-01' });
|
||||||
const response = JSON.parse(rawResponse);
|
const response = JSON.parse(rawResponse);
|
||||||
expect(response).toEqual(expect.objectContaining({ id: [] }));
|
expect(response).toEqual(expect.objectContaining({ id: [] }));
|
||||||
|
|
||||||
|
const createdAtResponse = await sut.getTimeBucket(factory.auth(), {
|
||||||
|
timeBucket: '012345-01-01',
|
||||||
|
orderBy: AssetOrderBy.CreatedAt,
|
||||||
|
});
|
||||||
|
expect(JSON.parse(createdAtResponse)).toEqual(expect.objectContaining({ id: [] }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should bucket by upload month when ordering by createdAt', async () => {
|
||||||
|
const { sut, ctx } = setup();
|
||||||
|
const { user } = await ctx.newUser();
|
||||||
|
const auth = factory.auth({ user: { id: user.id } });
|
||||||
|
const dates = [
|
||||||
|
'1970-01-31T23:59:59.999Z',
|
||||||
|
'1970-02-01T00:00:00.000Z',
|
||||||
|
'1970-02-28T23:59:59.999Z',
|
||||||
|
'1970-03-01T00:00:00.000Z',
|
||||||
|
];
|
||||||
|
const ids: string[] = [];
|
||||||
|
for (const createdAt of dates) {
|
||||||
|
const { asset } = await ctx.newAsset({ ownerId: user.id, createdAt, localDateTime: new Date('2000-01-01') });
|
||||||
|
await ctx.newExif({ assetId: asset.id, make: 'Canon' });
|
||||||
|
ids.push(asset.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawResponse = await sut.getTimeBucket(auth, { timeBucket: '1970-02-01', orderBy: AssetOrderBy.CreatedAt });
|
||||||
|
const response = JSON.parse(rawResponse);
|
||||||
|
expect(response.id).toEqual([ids[2], ids[1]]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return time bucket in trash', async () => {
|
it('should return time bucket in trash', async () => {
|
||||||
|
|||||||
@@ -164,6 +164,7 @@ export default defineConfig(
|
|||||||
'@typescript-eslint/switch-exhaustiveness-check': ['error', { considerDefaultExhaustiveForUnions: true }],
|
'@typescript-eslint/switch-exhaustiveness-check': ['error', { considerDefaultExhaustiveForUnions: true }],
|
||||||
'svelte/button-has-type': 'error',
|
'svelte/button-has-type': 'error',
|
||||||
'object-shorthand': ['error', 'always'],
|
'object-shorthand': ['error', 'always'],
|
||||||
|
'svelte/no-at-const-tags': 'error',
|
||||||
'svelte/no-navigation-without-resolve': 'off',
|
'svelte/no-navigation-without-resolve': 'off',
|
||||||
'unicorn/prefer-early-return': 'off',
|
'unicorn/prefer-early-return': 'off',
|
||||||
'unicorn/prefer-ternary': 'off',
|
'unicorn/prefer-ternary': 'off',
|
||||||
|
|||||||
+2
-2
@@ -36,8 +36,8 @@
|
|||||||
"@photo-sphere-viewer/resolution-plugin": "^5.14.0",
|
"@photo-sphere-viewer/resolution-plugin": "^5.14.0",
|
||||||
"@photo-sphere-viewer/settings-plugin": "^5.14.0",
|
"@photo-sphere-viewer/settings-plugin": "^5.14.0",
|
||||||
"@photo-sphere-viewer/video-plugin": "^5.14.0",
|
"@photo-sphere-viewer/video-plugin": "^5.14.0",
|
||||||
"@tanstack/svelte-query": "^6.3.2",
|
|
||||||
"@simplewebauthn/browser": "14.0.0",
|
"@simplewebauthn/browser": "14.0.0",
|
||||||
|
"@tanstack/svelte-query": "^6.3.2",
|
||||||
"@types/geojson": "^7946.0.16",
|
"@types/geojson": "^7946.0.16",
|
||||||
"@zoom-image/core": "^0.43.0",
|
"@zoom-image/core": "^0.43.0",
|
||||||
"@zoom-image/svelte": "^0.3.0",
|
"@zoom-image/svelte": "^0.3.0",
|
||||||
@@ -101,7 +101,7 @@
|
|||||||
"eslint-plugin-better-tailwindcss": "^4.5.0",
|
"eslint-plugin-better-tailwindcss": "^4.5.0",
|
||||||
"eslint-plugin-compat": "^7.0.0",
|
"eslint-plugin-compat": "^7.0.0",
|
||||||
"eslint-plugin-prettier": "^5.5.6",
|
"eslint-plugin-prettier": "^5.5.6",
|
||||||
"eslint-plugin-svelte": "^3.12.4",
|
"eslint-plugin-svelte": "^3.23.1",
|
||||||
"eslint-plugin-unicorn": "^75.0.0",
|
"eslint-plugin-unicorn": "^75.0.0",
|
||||||
"factory.ts": "^1.4.1",
|
"factory.ts": "^1.4.1",
|
||||||
"globals": "^17.0.0",
|
"globals": "^17.0.0",
|
||||||
|
|||||||
@@ -70,9 +70,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#each data.series as day, idx (day.date)}
|
{#each data.series as day, idx (day.date)}
|
||||||
{@const date = DateTime.fromISO(day.date, { zone: 'utc' }).toLocaleString(
|
{const date = $derived(
|
||||||
{ month: 'short', day: 'numeric' },
|
DateTime.fromISO(day.date, { zone: 'utc' }).toLocaleString(
|
||||||
{ locale: $locale },
|
{ month: 'short', day: 'numeric' },
|
||||||
|
{ locale: $locale },
|
||||||
|
),
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
class="aspect-square size-full rounded-sm {itemColors(day.count)} row-start-(--heatmap-row-start)"
|
class="aspect-square size-full rounded-sm {itemColors(day.count)} row-start-(--heatmap-row-start)"
|
||||||
|
|||||||
@@ -49,7 +49,7 @@
|
|||||||
{@render button()}
|
{@render button()}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
{@const albumId = albumIds[0]}
|
{const albumId = $derived(albumIds[0])}
|
||||||
{#if albumId}
|
{#if albumId}
|
||||||
<AlbumThumbnail {albumId} onDelete={() => (albumIds = [])} />
|
<AlbumThumbnail {albumId} onDelete={() => (albumIds = [])} />
|
||||||
{:else}
|
{:else}
|
||||||
|
|||||||
@@ -92,7 +92,7 @@
|
|||||||
<Select options={schema.enum} bind:value={getString, setValue} />
|
<Select options={schema.enum} bind:value={getString, setValue} />
|
||||||
</Field>
|
</Field>
|
||||||
{:else if schema.array}
|
{:else if schema.array}
|
||||||
{@const values = getValue<string[]>([])}
|
{const values = $derived(getValue<string[]>([]))}
|
||||||
<Field {label} {description}>
|
<Field {label} {description}>
|
||||||
<Input value={values.join(',')} disabled />
|
<Input value={values.join(',')} disabled />
|
||||||
<HelperText>{$t('unsupported_field_type')}</HelperText>
|
<HelperText>{$t('unsupported_field_type')}</HelperText>
|
||||||
|
|||||||
@@ -23,6 +23,8 @@
|
|||||||
preload = false,
|
preload = false,
|
||||||
contextMenuItems,
|
contextMenuItems,
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
|
const owner = $derived(album.albumUsers[0].user);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -74,7 +76,6 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if showOwner}
|
{#if showOwner}
|
||||||
{@const owner = album.albumUsers[0].user}
|
|
||||||
{#if owner.id === authManager.user.id}
|
{#if owner.id === authManager.user.id}
|
||||||
<p>{$t('owned')}</p>
|
<p>{$t('owned')}</p>
|
||||||
{:else}
|
{:else}
|
||||||
|
|||||||
@@ -66,8 +66,8 @@
|
|||||||
{#if !isCollapsed}
|
{#if !isCollapsed}
|
||||||
<div class="grid grid-auto-fill-56 gap-y-4" transition:slide={{ duration: 300 }}>
|
<div class="grid grid-auto-fill-56 gap-y-4" transition:slide={{ duration: 300 }}>
|
||||||
{#each albums as album, index (album.id)}
|
{#each albums as album, index (album.id)}
|
||||||
{@const { Edit, Share, Download, Leave, Delete } = getAlbumActions($t, album)}
|
{const { Edit, Share, Download, Leave, Delete } = $derived(getAlbumActions($t, album))}
|
||||||
{@const items = [Edit, Share, Download, Leave, Delete]}
|
{const items = $derived([Edit, Share, Download, Leave, Delete])}
|
||||||
<a
|
<a
|
||||||
href={Route.viewAlbum(album)}
|
href={Route.viewAlbum(album)}
|
||||||
class="h-fit"
|
class="h-fit"
|
||||||
|
|||||||
@@ -39,8 +39,8 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
{:else}
|
{:else}
|
||||||
{#each groupedAlbums as albumGroup (albumGroup.id)}
|
{#each groupedAlbums as albumGroup (albumGroup.id)}
|
||||||
{@const isCollapsed = isAlbumGroupCollapsed($albumViewSettings, albumGroup.id)}
|
{const isCollapsed = $derived(isAlbumGroupCollapsed($albumViewSettings, albumGroup.id))}
|
||||||
{@const iconRotation = isCollapsed ? 'rotate-0' : 'rotate-90'}
|
{const iconRotation = $derived(isCollapsed ? 'rotate-0' : 'rotate-90')}
|
||||||
<tbody
|
<tbody
|
||||||
class="mt-4 block w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray dark:text-immich-dark-fg"
|
class="mt-4 block w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray dark:text-immich-dark-fg"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -649,10 +649,9 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if stack && withStacked && !assetViewerManager.isShowEditor && $slideshowState === SlideshowState.None}
|
{#if stack && withStacked && !assetViewerManager.isShowEditor && $slideshowState === SlideshowState.None}
|
||||||
{@const stackedAssets = stack.assets}
|
|
||||||
<div id="stack-slideshow" class="absolute bottom-0 col-span-4 col-start-1 w-fit max-w-full">
|
<div id="stack-slideshow" class="absolute bottom-0 col-span-4 col-start-1 w-fit max-w-full">
|
||||||
<div class="no-wrap horizontal-scrollbar relative flex flex-row overflow-x-auto overflow-y-hidden">
|
<div class="no-wrap horizontal-scrollbar relative flex flex-row overflow-x-auto overflow-y-hidden">
|
||||||
{#each stackedAssets as stackedAsset (stackedAsset.id)}
|
{#each stack.assets as stackedAsset (stackedAsset.id)}
|
||||||
<div
|
<div
|
||||||
class={['relative inline-block px-1 pb-2 transition-all']}
|
class={['relative inline-block px-1 pb-2 transition-all']}
|
||||||
style:bottom={stackedAsset.id === asset.id ? '0' : '-10px'}
|
style:bottom={stackedAsset.id === asset.id ? '0' : '-10px'}
|
||||||
|
|||||||
@@ -142,11 +142,11 @@
|
|||||||
<ActionMenuItem action={StackActions.RemoveAsset} />
|
<ActionMenuItem action={StackActions.RemoveAsset} />
|
||||||
|
|
||||||
{#if album}
|
{#if album}
|
||||||
{@const { SetCover } = getAlbumAssetActions($t, album, asset)}
|
{const { SetCover } = $derived(getAlbumAssetActions($t, album, asset))}
|
||||||
<ActionMenuItem action={SetCover} />
|
<ActionMenuItem action={SetCover} />
|
||||||
{/if}
|
{/if}
|
||||||
{#if person}
|
{#if person}
|
||||||
{@const { SetFeaturedPhoto } = getPersonAssetActions($t, person, asset)}
|
{const { SetFeaturedPhoto } = $derived(getPersonAssetActions($t, person, asset))}
|
||||||
<ActionMenuItem action={SetFeaturedPhoto} />
|
<ActionMenuItem action={SetFeaturedPhoto} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|||||||
@@ -186,7 +186,7 @@
|
|||||||
{getMegapixel(asset.exifInfo.exifImageHeight, asset.exifInfo.exifImageWidth)} MP
|
{getMegapixel(asset.exifInfo.exifImageHeight, asset.exifInfo.exifImageWidth)} MP
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
{@const { width, height } = getDimensions(asset.exifInfo)}
|
{const { width, height } = $derived(getDimensions(asset.exifInfo))}
|
||||||
<p>{width} × {height}</p>
|
<p>{width} × {height}</p>
|
||||||
{/if}
|
{/if}
|
||||||
{#if asset.exifInfo?.fileSizeInByte}
|
{#if asset.exifInfo?.fileSizeInByte}
|
||||||
@@ -295,8 +295,8 @@
|
|||||||
onOpenInMapView={() => goto(Route.map({ ...latlng, zoom: 12.5 }))}
|
onOpenInMapView={() => goto(Route.map({ ...latlng, zoom: 12.5 }))}
|
||||||
>
|
>
|
||||||
{#snippet popup({ marker })}
|
{#snippet popup({ marker })}
|
||||||
{@const { lat, lon } = marker}
|
|
||||||
<div class="flex flex-col items-center gap-1">
|
<div class="flex flex-col items-center gap-1">
|
||||||
|
{const { lat, lon } = $derived(marker)}
|
||||||
<Text fontWeight="bold">{lat.toPrecision(6)}, {lon.toPrecision(6)}</Text>
|
<Text fontWeight="bold">{lat.toPrecision(6)}, {lon.toPrecision(6)}</Text>
|
||||||
<Link
|
<Link
|
||||||
href="https://www.openstreetmap.org/?mlat={lat}&mlon={lon}&zoom=13#map=15/{lat}/{lon}"
|
href="https://www.openstreetmap.org/?mlat={lat}&mlon={lon}&zoom=13#map=15/{lat}/{lon}"
|
||||||
|
|||||||
@@ -103,8 +103,10 @@
|
|||||||
|
|
||||||
<div class="mt-2 grid {visiblePeople.length <= 6 ? 'grid-cols-3 gap-3' : 'grid-cols-4 gap-2'}">
|
<div class="mt-2 grid {visiblePeople.length <= 6 ? 'grid-cols-3 gap-3' : 'grid-cols-4 gap-2'}">
|
||||||
{#each visiblePeople as person (person.id)}
|
{#each visiblePeople as person (person.id)}
|
||||||
{@const personFaces = faceManager.facesByPersonId.get(person.id) ?? []}
|
{const personFaces = $derived(faceManager.facesByPersonId.get(person.id) ?? [])}
|
||||||
{@const isHighlighted = personFaces.some((f) => assetViewerManager.highlightedFaces.some((b) => b.id === f.id))}
|
{const isHighlighted = $derived(
|
||||||
|
personFaces.some((f) => assetViewerManager.highlightedFaces.some((b) => b.id === f.id)),
|
||||||
|
)}
|
||||||
<a
|
<a
|
||||||
class="group outline-none"
|
class="group outline-none"
|
||||||
href={Route.viewPerson(person, { previousRoute })}
|
href={Route.viewPerson(person, { previousRoute })}
|
||||||
|
|||||||
@@ -265,7 +265,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
{#each boundingBoxes as boundingbox (boundingbox.id)}
|
{#each boundingBoxes as boundingbox (boundingbox.id)}
|
||||||
{@const isActive = assetViewerManager.highlightedFaces.some((f) => f.id === boundingbox.id)}
|
{const isActive = $derived(assetViewerManager.highlightedFaces.some((f) => f.id === boundingbox.id))}
|
||||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
<div
|
<div
|
||||||
class="pointer-events-auto absolute rounded-lg {isActive && 'border-3 border-solid border-white'}"
|
class="pointer-events-auto absolute rounded-lg {isActive && 'border-3 border-solid border-white'}"
|
||||||
@@ -274,9 +274,10 @@
|
|||||||
onpointerleave={() => assetViewerManager.clearHighlightedFaces()}
|
onpointerleave={() => assetViewerManager.clearHighlightedFaces()}
|
||||||
>
|
>
|
||||||
{#if isActive && boundingbox.name}
|
{#if isActive && boundingbox.name}
|
||||||
{@const labelAbove =
|
{const labelAbove = $derived(
|
||||||
boundingbox.top + boundingbox.height + faceLabelOffset > overlaySize.height &&
|
boundingbox.top + boundingbox.height + faceLabelOffset > overlaySize.height &&
|
||||||
boundingbox.top >= faceLabelOffset}
|
boundingbox.top >= faceLabelOffset,
|
||||||
|
)}
|
||||||
<div
|
<div
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
class="absolute rounded-sm bg-white/90 px-2 py-1 text-sm font-medium whitespace-nowrap text-black shadow-lg"
|
class="absolute rounded-sm bg-white/90 px-2 py-1 text-sm font-medium whitespace-nowrap text-black shadow-lg"
|
||||||
|
|||||||
@@ -93,7 +93,7 @@
|
|||||||
></div>
|
></div>
|
||||||
|
|
||||||
{#each edges as edge (edge)}
|
{#each edges as edge (edge)}
|
||||||
{@const rotatedEdge = rotateBoundary(edges, edge, transformManager.normalizedRotation / 90)}
|
{const rotatedEdge = $derived(rotateBoundary(edges, edge, transformManager.normalizedRotation / 90))}
|
||||||
<button
|
<button
|
||||||
class={['absolute', edge]}
|
class={['absolute', edge]}
|
||||||
style={`${edge}: -10px`}
|
style={`${edge}: -10px`}
|
||||||
@@ -104,7 +104,7 @@
|
|||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
{#each corners as corner (corner)}
|
{#each corners as corner (corner)}
|
||||||
{@const rotatedCorner = rotateBoundary(corners, corner, transformManager.normalizedRotation / 90)}
|
{const rotatedCorner = $derived(rotateBoundary(corners, corner, transformManager.normalizedRotation / 90))}
|
||||||
<button
|
<button
|
||||||
class={['corner', corner]}
|
class={['corner', corner]}
|
||||||
onmousedown={(e) => transformManager.handleMouseDownOn(e, corner)}
|
onmousedown={(e) => transformManager.handleMouseDownOn(e, corner)}
|
||||||
|
|||||||
@@ -238,8 +238,8 @@
|
|||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
{#each peopleWithFaces as face, index (face.id)}
|
{#each peopleWithFaces as face, index (face.id)}
|
||||||
{@const personName = face.person ? face.person.name : $t('face_unassigned')}
|
{const personName = face.person ? face.person.name : $t('face_unassigned')}
|
||||||
{@const isHighlighted = assetViewerManager.highlightedFaces.some((b) => b.id === face.id)}
|
{const isHighlighted = $derived(assetViewerManager.highlightedFaces.some((b) => b.id === face.id))}
|
||||||
<div class="relative h-29 w-24">
|
<div class="relative h-29 w-24">
|
||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
|
|||||||
@@ -348,8 +348,8 @@
|
|||||||
>
|
>
|
||||||
{#each assets as asset, index (asset.id + '-' + index)}
|
{#each assets as asset, index (asset.id + '-' + index)}
|
||||||
{#if isInOrNearViewport(index)}
|
{#if isInOrNearViewport(index)}
|
||||||
{@const currentAsset = toTimelineAsset(asset)}
|
|
||||||
<div class="absolute" style:overflow="clip" style={getStyle(index)}>
|
<div class="absolute" style:overflow="clip" style={getStyle(index)}>
|
||||||
|
{const currentAsset = toTimelineAsset(asset)}
|
||||||
<Thumbnail
|
<Thumbnail
|
||||||
readonly={disableAssetSelect}
|
readonly={disableAssetSelect}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@@ -100,9 +100,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#each filteredSearchTerms as savedSearchTerm, i (i)}
|
{#each filteredSearchTerms as savedSearchTerm, i (i)}
|
||||||
{@const index = showClearAll ? i + 1 : i}
|
|
||||||
<div class="flex w-full items-center justify-between text-black dark:text-gray-300">
|
<div class="flex w-full items-center justify-between text-black dark:text-gray-300">
|
||||||
<div class="relative w-full items-center">
|
<div class="relative w-full items-center">
|
||||||
|
{const index = $derived(showClearAll ? i + 1 : i)}
|
||||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||||
<div
|
<div
|
||||||
id={getId(index)}
|
id={getId(index)}
|
||||||
|
|||||||
@@ -46,14 +46,14 @@
|
|||||||
</div>
|
</div>
|
||||||
{:then people}
|
{:then people}
|
||||||
{#if people && people.length > 0}
|
{#if people && people.length > 0}
|
||||||
{@const peopleList = showAllPeople
|
|
||||||
? filterPeople(people, name)
|
|
||||||
: filterPeople(people, name).slice(0, numberOfPeople)}
|
|
||||||
|
|
||||||
<div id="people-selection" class="max-h-80 immich-scrollbar overflow-y-auto">
|
<div id="people-selection" class="max-h-80 immich-scrollbar overflow-y-auto">
|
||||||
<Text class="pb-5">{$t('people_search_description')}</Text>
|
<Text class="pb-5">{$t('people_search_description')}</Text>
|
||||||
<SearchBar bind:name placeholder={$t('filter_people')} showLoadingSpinner={false} />
|
<SearchBar bind:name placeholder={$t('filter_people')} showLoadingSpinner={false} />
|
||||||
|
|
||||||
|
{const peopleList = $derived(
|
||||||
|
showAllPeople ? filterPeople(people, name) : filterPeople(people, name).slice(0, numberOfPeople),
|
||||||
|
)}
|
||||||
|
|
||||||
<SingleGridRow
|
<SingleGridRow
|
||||||
class="space-between grid immich-scrollbar grid-auto-fill-20 gap-5 overflow-y-auto pt-5"
|
class="space-between grid immich-scrollbar grid-auto-fill-20 gap-5 overflow-y-auto pt-5"
|
||||||
bind:itemCount={numberOfPeople}
|
bind:itemCount={numberOfPeople}
|
||||||
|
|||||||
@@ -81,7 +81,7 @@
|
|||||||
{#if selectedTags?.size}
|
{#if selectedTags?.size}
|
||||||
<section class="flex flex-wrap gap-2 pt-5">
|
<section class="flex flex-wrap gap-2 pt-5">
|
||||||
{#each selectedTags as tagId (tagId)}
|
{#each selectedTags as tagId (tagId)}
|
||||||
{@const tag = tagMap[tagId]}
|
{const tag = $derived(tagMap[tagId])}
|
||||||
{#if tag}
|
{#if tag}
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
|
|||||||
@@ -35,8 +35,8 @@
|
|||||||
<!-- Image grid -->
|
<!-- Image grid -->
|
||||||
<div data-image-grid class="relative overflow-clip" style:height={height + 'px'} style:width={width + 'px'}>
|
<div data-image-grid class="relative overflow-clip" style:height={height + 'px'} style:width={width + 'px'}>
|
||||||
{#each viewerAssets as viewerAsset (viewerAsset.id)}
|
{#each viewerAssets as viewerAsset (viewerAsset.id)}
|
||||||
{@const position = viewerAsset.position!}
|
{const position = viewerAsset.position!}
|
||||||
{@const asset = viewerAsset.asset!}
|
{const asset = viewerAsset.asset!}
|
||||||
|
|
||||||
<!-- note: don't remove data-asset-id - its used by web e2e tests -->
|
<!-- note: don't remove data-asset-id - its used by web e2e tests -->
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -58,7 +58,6 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#each filterIsInOrNearViewport(timelineMonth.timelineDays) as timelineDay, groupIndex (timelineDay.day)}
|
{#each filterIsInOrNearViewport(timelineMonth.timelineDays) as timelineDay, groupIndex (timelineDay.day)}
|
||||||
{@const isTimelineDaySelected = assetInteraction.selectedGroup.has(timelineDay.groupTitle)}
|
|
||||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
<section
|
<section
|
||||||
class={[{ 'transition-all': !timelineMonth.timelineManager.suspendTransitions }]}
|
class={[{ 'transition-all': !timelineMonth.timelineManager.suspendTransitions }]}
|
||||||
@@ -76,10 +75,10 @@
|
|||||||
style:width={timelineDay.width + 'px'}
|
style:width={timelineDay.width + 'px'}
|
||||||
>
|
>
|
||||||
{#if !singleSelect}
|
{#if !singleSelect}
|
||||||
|
{const isTimelineDaySelected = $derived(assetInteraction.selectedGroup.has(timelineDay.groupTitle))}
|
||||||
<div
|
<div
|
||||||
class="w-0 overflow-hidden transition-all duration-200 ease-out hover:cursor-pointer"
|
class="w-0 overflow-hidden transition-all duration-200 ease-out hover:cursor-pointer"
|
||||||
class:w-8={hoveredTimelineDay === timelineDay.groupTitle ||
|
class:w-8={hoveredTimelineDay === timelineDay.groupTitle || isTimelineDaySelected}
|
||||||
assetInteraction.selectedGroup.has(timelineDay.groupTitle)}
|
|
||||||
onclick={() => onTimelineDaySelect(timelineDay, assetsSnapshot(timelineDay.getAssets()))}
|
onclick={() => onTimelineDaySelect(timelineDay, assetsSnapshot(timelineDay.getAssets()))}
|
||||||
onkeydown={() => onTimelineDaySelect(timelineDay, assetsSnapshot(timelineDay.getAssets()))}
|
onkeydown={() => onTimelineDaySelect(timelineDay, assetsSnapshot(timelineDay.getAssets()))}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -634,24 +634,21 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{#each timelineManager.months as timelineMonth (timelineMonth.viewId)}
|
{#each timelineManager.months as timelineMonth (timelineMonth.viewId)}
|
||||||
{@const isInOrNearViewport = timelineMonth.isInOrNearViewport}
|
|
||||||
{@const absoluteHeight = timelineMonth.top}
|
|
||||||
|
|
||||||
{#if !timelineMonth.isLoaded}
|
{#if !timelineMonth.isLoaded}
|
||||||
<div
|
<div
|
||||||
style:height={timelineMonth.height + 'px'}
|
style:height={timelineMonth.height + 'px'}
|
||||||
style:position="absolute"
|
style:position="absolute"
|
||||||
style:transform={`translate3d(0,${absoluteHeight}px,0)`}
|
style:transform={`translate3d(0,${timelineMonth.top}px,0)`}
|
||||||
style:width="100%"
|
style:width="100%"
|
||||||
>
|
>
|
||||||
<Skeleton {invisible} height={timelineMonth.height} title={timelineMonth.title} />
|
<Skeleton {invisible} height={timelineMonth.height} title={timelineMonth.title} />
|
||||||
</div>
|
</div>
|
||||||
{:else if isInOrNearViewport}
|
{:else if timelineMonth.isInOrNearViewport}
|
||||||
<div
|
<div
|
||||||
class="timeline-month"
|
class="timeline-month"
|
||||||
style:height={timelineMonth.height + 'px'}
|
style:height={timelineMonth.height + 'px'}
|
||||||
style:position="absolute"
|
style:position="absolute"
|
||||||
style:transform={`translate3d(0,${absoluteHeight}px,0)`}
|
style:transform={`translate3d(0,${timelineMonth.top}px,0)`}
|
||||||
style:width="100%"
|
style:width="100%"
|
||||||
>
|
>
|
||||||
<Month
|
<Month
|
||||||
@@ -663,10 +660,11 @@
|
|||||||
onTimelineDaySelect={handleGroupSelect}
|
onTimelineDaySelect={handleGroupSelect}
|
||||||
>
|
>
|
||||||
{#snippet thumbnail({ asset, position, timelineDay, groupIndex })}
|
{#snippet thumbnail({ asset, position, timelineDay, groupIndex })}
|
||||||
{@const isAssetSelectionCandidate = assetInteraction.hasSelectionCandidate(asset.id)}
|
{const isAssetSelectionCandidate = $derived(assetInteraction.hasSelectionCandidate(asset.id))}
|
||||||
{@const isAssetSelected =
|
{const isAssetSelected = $derived(
|
||||||
assetInteraction.hasSelectedAsset(asset.id) || timelineManager.albumAssets.has(asset.id)}
|
assetInteraction.hasSelectedAsset(asset.id) || timelineManager.albumAssets.has(asset.id),
|
||||||
{@const isAssetDisabled = timelineManager.albumAssets.has(asset.id)}
|
)}
|
||||||
|
{const isAssetDisabled = $derived(timelineManager.albumAssets.has(asset.id))}
|
||||||
<Thumbnail
|
<Thumbnail
|
||||||
showStackedIcon={withStacked}
|
showStackedIcon={withStacked}
|
||||||
{showArchiveIcon}
|
{showArchiveIcon}
|
||||||
|
|||||||
@@ -112,8 +112,8 @@
|
|||||||
)}"
|
)}"
|
||||||
>
|
>
|
||||||
{#each options as option (option)}
|
{#each options as option (option)}
|
||||||
{@const renderedOption = renderOption(option)}
|
{const renderedOption = renderOption(option)}
|
||||||
{@const buttonStyle = renderedOption.disabled ? '' : 'transition-all hover:bg-gray-300 dark:hover:bg-gray-800'}
|
{const buttonStyle = renderedOption.disabled ? '' : 'transition-all hover:bg-gray-300 dark:hover:bg-gray-800'}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="grid grid-cols-[36px_1fr] place-items-center p-2 disabled:opacity-40 {buttonStyle}"
|
class="grid grid-cols-[36px_1fr] place-items-center p-2 disabled:opacity-40 {buttonStyle}"
|
||||||
|
|||||||
@@ -70,9 +70,9 @@
|
|||||||
<legend class="sr-only">{$t('rating')}</legend>
|
<legend class="sr-only">{$t('rating')}</legend>
|
||||||
<div class="flex flex-row" data-testid="star-container">
|
<div class="flex flex-row" data-testid="star-container">
|
||||||
{#each { length: count } as _, index (index)}
|
{#each { length: count } as _, index (index)}
|
||||||
{@const value = index + 1}
|
{const value = index + 1}
|
||||||
{@const filled = hoverRating === null ? (ratingSelection ?? 0) >= value : hoverRating >= value}
|
{const filled = $derived(hoverRating === null ? (ratingSelection ?? 0) >= value : hoverRating >= value)}
|
||||||
{@const starId = `${id}-${value}`}
|
{const starId = `${id}-${value}`}
|
||||||
<!-- svelte-ignore a11y_mouse_events_have_key_events -->
|
<!-- svelte-ignore a11y_mouse_events_have_key_events -->
|
||||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
||||||
<label
|
<label
|
||||||
|
|||||||
@@ -79,7 +79,7 @@
|
|||||||
|
|
||||||
<section class="flex flex-wrap gap-1 pt-2">
|
<section class="flex flex-wrap gap-1 pt-2">
|
||||||
{#each selectedIds as tagId (tagId)}
|
{#each selectedIds as tagId (tagId)}
|
||||||
{@const tag = tagMap[tagId]}
|
{const tag = $derived(tagMap[tagId])}
|
||||||
{#if tag}
|
{#if tag}
|
||||||
<TagPill label={tag.value} onRemove={() => handleRemove(tagId)} />
|
<TagPill label={tag.value} onRemove={() => handleRemove(tagId)} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -82,7 +82,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#snippet row(id: string, label: string, userId: string, onPick: () => Promise<void>)}
|
{#snippet row(id: string, label: string, userId: string, onPick: () => Promise<void>)}
|
||||||
{@const user = getUser(userId)}
|
{const user = $derived(getUser(userId))}
|
||||||
<div class="flex items-center justify-between gap-2">
|
<div class="flex items-center justify-between gap-2">
|
||||||
<Label id="{id}-label" for={id}>{label}</Label>
|
<Label id="{id}-label" for={id}>{label}</Label>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -72,7 +72,7 @@
|
|||||||
{:else if filteredPeople.length > 0}
|
{:else if filteredPeople.length > 0}
|
||||||
<div class="grid grid-cols-3 gap-4 p-2">
|
<div class="grid grid-cols-3 gap-4 p-2">
|
||||||
{#each filteredPeople as person (person.id)}
|
{#each filteredPeople as person (person.id)}
|
||||||
{@const isSelected = selectedPeople.some((p) => p.id === person.id)}
|
{const isSelected = $derived(selectedPeople.some((p) => p.id === person.id))}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onclick={() => togglePerson(person)}
|
onclick={() => togglePerson(person)}
|
||||||
|
|||||||
@@ -74,7 +74,7 @@
|
|||||||
<Label size="small" color="primary" for="version-history">{$t('version_history')}</Label>
|
<Label size="small" color="primary" for="version-history">{$t('version_history')}</Label>
|
||||||
<ul id="version-history" class="list-none">
|
<ul id="version-history" class="list-none">
|
||||||
{#each versions.slice(0, 5) as item (item.id)}
|
{#each versions.slice(0, 5) as item (item.id)}
|
||||||
{@const createdAt = DateTime.fromISO(item.createdAt)}
|
{const createdAt = DateTime.fromISO(item.createdAt)}
|
||||||
<li>
|
<li>
|
||||||
<span
|
<span
|
||||||
class="pb-2 text-xs immich-form-label"
|
class="pb-2 text-xs immich-form-label"
|
||||||
|
|||||||
+1
-1
@@ -390,7 +390,7 @@
|
|||||||
|
|
||||||
{#if assetMultiSelectManager.selectionActive}
|
{#if assetMultiSelectManager.selectionActive}
|
||||||
<AssetSelectControlBar>
|
<AssetSelectControlBar>
|
||||||
{@const Actions = getAssetBulkActions($t, album)}
|
{const Actions = $derived(getAssetBulkActions($t, album))}
|
||||||
<CommandPaletteDefaultProvider name={$t('assets')} actions={Object.values(Actions)} />
|
<CommandPaletteDefaultProvider name={$t('assets')} actions={Object.values(Actions)} />
|
||||||
<ActionButton action={Actions.CreateSharedLink} />
|
<ActionButton action={Actions.CreateSharedLink} />
|
||||||
<SelectAllAssets {timelineManager} assetInteraction={assetMultiSelectManager} />
|
<SelectAllAssets {timelineManager} assetInteraction={assetMultiSelectManager} />
|
||||||
|
|||||||
@@ -61,7 +61,7 @@
|
|||||||
|
|
||||||
{#if assetMultiSelectManager.selectionActive}
|
{#if assetMultiSelectManager.selectionActive}
|
||||||
<AssetSelectControlBar>
|
<AssetSelectControlBar>
|
||||||
{@const Actions = getAssetBulkActions($t)}
|
{const Actions = $derived(getAssetBulkActions($t))}
|
||||||
<CommandPaletteDefaultProvider name={$t('assets')} actions={Object.values(Actions)} />
|
<CommandPaletteDefaultProvider name={$t('assets')} actions={Object.values(Actions)} />
|
||||||
<ArchiveAction
|
<ArchiveAction
|
||||||
unarchive
|
unarchive
|
||||||
|
|||||||
@@ -64,7 +64,7 @@
|
|||||||
<!-- Multiselection mode app bar -->
|
<!-- Multiselection mode app bar -->
|
||||||
{#if assetMultiSelectManager.selectionActive}
|
{#if assetMultiSelectManager.selectionActive}
|
||||||
<AssetSelectControlBar>
|
<AssetSelectControlBar>
|
||||||
{@const Actions = getAssetBulkActions($t)}
|
{const Actions = $derived(getAssetBulkActions($t))}
|
||||||
<CommandPaletteDefaultProvider name={$t('assets')} actions={Object.values(Actions)} />
|
<CommandPaletteDefaultProvider name={$t('assets')} actions={Object.values(Actions)} />
|
||||||
<FavoriteAction removeFavorite onFavorite={(assetIds) => timelineManager.removeAssets(assetIds)} />
|
<FavoriteAction removeFavorite onFavorite={(assetIds) => timelineManager.removeAssets(assetIds)} />
|
||||||
<ActionButton action={Actions.CreateSharedLink} />
|
<ActionButton action={Actions.CreateSharedLink} />
|
||||||
|
|||||||
@@ -133,7 +133,7 @@
|
|||||||
{#if assetMultiSelectManager.selectionActive}
|
{#if assetMultiSelectManager.selectionActive}
|
||||||
<div class="fixed inset-s-0 top-0 w-full">
|
<div class="fixed inset-s-0 top-0 w-full">
|
||||||
<AssetSelectControlBar>
|
<AssetSelectControlBar>
|
||||||
{@const Actions = getAssetBulkActions($t)}
|
{const Actions = $derived(getAssetBulkActions($t))}
|
||||||
<CommandPaletteDefaultProvider name={$t('assets')} actions={Object.values(Actions)} />
|
<CommandPaletteDefaultProvider name={$t('assets')} actions={Object.values(Actions)} />
|
||||||
<ActionButton action={Actions.CreateSharedLink} />
|
<ActionButton action={Actions.CreateSharedLink} />
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|||||||
@@ -115,8 +115,8 @@
|
|||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
{#if assetMultiSelectManager.selectionActive}
|
{#if assetMultiSelectManager.selectionActive}
|
||||||
{@const Actions = getAssetBulkActions($t)}
|
{const Actions = $derived(getAssetBulkActions($t))}
|
||||||
{@const StackActions = getStackBulkActions($t)}
|
{const StackActions = $derived(getStackBulkActions($t))}
|
||||||
<CommandPaletteDefaultProvider name={$t('assets')} actions={Object.values(Actions)} />
|
<CommandPaletteDefaultProvider name={$t('assets')} actions={Object.values(Actions)} />
|
||||||
|
|
||||||
<Portal target="body">
|
<Portal target="body">
|
||||||
|
|||||||
+1
-1
@@ -277,7 +277,7 @@
|
|||||||
{#if assetMultiSelectManager.selectionActive}
|
{#if assetMultiSelectManager.selectionActive}
|
||||||
<div class="dark sticky top-0 z-1">
|
<div class="dark sticky top-0 z-1">
|
||||||
<AssetSelectControlBar>
|
<AssetSelectControlBar>
|
||||||
{@const Actions = getAssetBulkActions($t)}
|
{const Actions = $derived(getAssetBulkActions($t))}
|
||||||
<CommandPaletteDefaultProvider name={$t('assets')} actions={Object.values(Actions)} />
|
<CommandPaletteDefaultProvider name={$t('assets')} actions={Object.values(Actions)} />
|
||||||
|
|
||||||
<ActionButton action={Actions.CreateSharedLink} />
|
<ActionButton action={Actions.CreateSharedLink} />
|
||||||
|
|||||||
+1
-1
@@ -41,7 +41,7 @@
|
|||||||
|
|
||||||
{#if assetMultiSelectManager.selectionActive}
|
{#if assetMultiSelectManager.selectionActive}
|
||||||
<AssetSelectControlBar>
|
<AssetSelectControlBar>
|
||||||
{@const Actions = getAssetBulkActions($t)}
|
{const Actions = $derived(getAssetBulkActions($t))}
|
||||||
<CommandPaletteDefaultProvider name={$t('assets')} actions={Object.values(Actions)} />
|
<CommandPaletteDefaultProvider name={$t('assets')} actions={Object.values(Actions)} />
|
||||||
|
|
||||||
<ActionButton action={Actions.CreateSharedLink} />
|
<ActionButton action={Actions.CreateSharedLink} />
|
||||||
|
|||||||
+1
-1
@@ -476,7 +476,7 @@
|
|||||||
<header>
|
<header>
|
||||||
{#if assetMultiSelectManager.selectionActive}
|
{#if assetMultiSelectManager.selectionActive}
|
||||||
<AssetSelectControlBar>
|
<AssetSelectControlBar>
|
||||||
{@const Actions = getAssetBulkActions($t)}
|
{const Actions = $derived(getAssetBulkActions($t))}
|
||||||
<CommandPaletteDefaultProvider name={$t('assets')} actions={Object.values(Actions)} />
|
<CommandPaletteDefaultProvider name={$t('assets')} actions={Object.values(Actions)} />
|
||||||
<ActionButton action={Actions.CreateSharedLink} />
|
<ActionButton action={Actions.CreateSharedLink} />
|
||||||
<SelectAllAssets {timelineManager} assetInteraction={assetMultiSelectManager} />
|
<SelectAllAssets {timelineManager} assetInteraction={assetMultiSelectManager} />
|
||||||
|
|||||||
@@ -148,7 +148,7 @@
|
|||||||
<div class="flex flex-wrap gap-1 p-2 pb-8 md:px-8">
|
<div class="flex flex-wrap gap-1 p-2 pb-8 md:px-8">
|
||||||
<PeopleInfiniteScroll {people} hasNextPage={nextPage !== null} {loadNextPage}>
|
<PeopleInfiniteScroll {people} hasNextPage={nextPage !== null} {loadNextPage}>
|
||||||
{#snippet children({ person })}
|
{#snippet children({ person })}
|
||||||
{@const hidden = overrides.get(person.id) ?? person.isHidden}
|
{const hidden = $derived(overrides.get(person.id) ?? person.isHidden)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="group relative size-full"
|
class="group relative size-full"
|
||||||
|
|||||||
@@ -123,8 +123,8 @@
|
|||||||
|
|
||||||
{#if assetMultiSelectManager.selectionActive}
|
{#if assetMultiSelectManager.selectionActive}
|
||||||
<AssetSelectControlBar>
|
<AssetSelectControlBar>
|
||||||
{@const Actions = getAssetBulkActions($t)}
|
{const Actions = $derived(getAssetBulkActions($t))}
|
||||||
{@const StackActions = getStackBulkActions($t)}
|
{const StackActions = $derived(getStackBulkActions($t))}
|
||||||
<CommandPaletteDefaultProvider name={$t('assets')} actions={Object.values(Actions)} />
|
<CommandPaletteDefaultProvider name={$t('assets')} actions={Object.values(Actions)} />
|
||||||
|
|
||||||
<ActionButton action={Actions.CreateSharedLink} />
|
<ActionButton action={Actions.CreateSharedLink} />
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
{#if !isCollapsed}
|
{#if !isCollapsed}
|
||||||
<div class="flex flex-row flex-wrap gap-4">
|
<div class="flex flex-row flex-wrap gap-4">
|
||||||
{#each places as item (item.id)}
|
{#each places as item (item.id)}
|
||||||
{@const city = item.exifInfo?.city}
|
{const city = item.exifInfo?.city}
|
||||||
<a class="relative" href={Route.search({ city })} draggable="false">
|
<a class="relative" href={Route.search({ city })} draggable="false">
|
||||||
<div
|
<div
|
||||||
class="flex w-[calc((100vw-(72px+5rem))/2)] max-w-39 justify-center overflow-hidden rounded-xl brightness-75 filter"
|
class="flex w-[calc((100vw-(72px+5rem))/2)] max-w-39 justify-center overflow-hidden rounded-xl brightness-75 filter"
|
||||||
|
|||||||
@@ -98,8 +98,8 @@
|
|||||||
|
|
||||||
{#if assetMultiSelectManager.selectionActive}
|
{#if assetMultiSelectManager.selectionActive}
|
||||||
<AssetSelectControlBar>
|
<AssetSelectControlBar>
|
||||||
{@const Actions = getAssetBulkActions($t)}
|
{const Actions = $derived(getAssetBulkActions($t))}
|
||||||
{@const StackActions = getStackBulkActions($t)}
|
{const StackActions = $derived(getStackBulkActions($t))}
|
||||||
<CommandPaletteDefaultProvider name={$t('assets')} actions={Object.values(Actions)} />
|
<CommandPaletteDefaultProvider name={$t('assets')} actions={Object.values(Actions)} />
|
||||||
|
|
||||||
<ActionButton action={Actions.CreateSharedLink} />
|
<ActionButton action={Actions.CreateSharedLink} />
|
||||||
|
|||||||
@@ -259,7 +259,7 @@
|
|||||||
<section id="search-chips" class="mx-auto mt-24 w-full max-w-7xl px-4 sm:px-8 lg:px-12">
|
<section id="search-chips" class="mx-auto mt-24 w-full max-w-7xl px-4 sm:px-8 lg:px-12">
|
||||||
<div class="flex w-full flex-wrap place-content-center place-items-center gap-2.5 sm:gap-3">
|
<div class="flex w-full flex-wrap place-content-center place-items-center gap-2.5 sm:gap-3">
|
||||||
{#each searchTermKeys as searchKey (searchKey)}
|
{#each searchTermKeys as searchKey (searchKey)}
|
||||||
{@const value = terms[searchKey]}
|
{const value = terms[searchKey]}
|
||||||
<div
|
<div
|
||||||
class="inline-flex max-w-full items-center rounded-full bg-primary/10 py-1 ps-1 pe-1 text-xs text-primary ring-1 ring-primary/15 transition-shadow hover:ring-primary/25 dark:bg-immich-dark-primary/15 dark:text-immich-dark-primary dark:ring-immich-dark-primary/20 dark:hover:ring-immich-dark-primary/30"
|
class="inline-flex max-w-full items-center rounded-full bg-primary/10 py-1 ps-1 pe-1 text-xs text-primary ring-1 ring-primary/15 transition-shadow hover:ring-primary/25 dark:bg-immich-dark-primary/15 dark:text-immich-dark-primary dark:ring-immich-dark-primary/20 dark:hover:ring-immich-dark-primary/30"
|
||||||
>
|
>
|
||||||
@@ -344,7 +344,7 @@
|
|||||||
{#if assetMultiSelectManager.selectionActive}
|
{#if assetMultiSelectManager.selectionActive}
|
||||||
<div class="fixed inset-s-0 top-0 z-2 w-full">
|
<div class="fixed inset-s-0 top-0 z-2 w-full">
|
||||||
<AssetSelectControlBar>
|
<AssetSelectControlBar>
|
||||||
{@const Actions = getAssetBulkActions($t)}
|
{const Actions = $derived(getAssetBulkActions($t))}
|
||||||
<CommandPaletteDefaultProvider name={$t('assets')} actions={Object.values(Actions)} />
|
<CommandPaletteDefaultProvider name={$t('assets')} actions={Object.values(Actions)} />
|
||||||
|
|
||||||
<ActionButton action={Actions.CreateSharedLink} />
|
<ActionButton action={Actions.CreateSharedLink} />
|
||||||
|
|||||||
@@ -120,7 +120,7 @@
|
|||||||
{#if assetMultiSelectManager.selectionActive}
|
{#if assetMultiSelectManager.selectionActive}
|
||||||
<div class="fixed inset-s-0 top-0 w-full">
|
<div class="fixed inset-s-0 top-0 w-full">
|
||||||
<AssetSelectControlBar>
|
<AssetSelectControlBar>
|
||||||
{@const Actions = getAssetBulkActions($t)}
|
{const Actions = $derived(getAssetBulkActions($t))}
|
||||||
<CommandPaletteDefaultProvider name={$t('assets')} actions={Object.values(Actions)} />
|
<CommandPaletteDefaultProvider name={$t('assets')} actions={Object.values(Actions)} />
|
||||||
<ActionButton action={Actions.CreateSharedLink} />
|
<ActionButton action={Actions.CreateSharedLink} />
|
||||||
<SelectAllAssets {timelineManager} assetInteraction={assetMultiSelectManager} />
|
<SelectAllAssets {timelineManager} assetInteraction={assetMultiSelectManager} />
|
||||||
|
|||||||
@@ -251,10 +251,12 @@
|
|||||||
<Card class="mt-4">
|
<Card class="mt-4">
|
||||||
<CardBody>
|
<CardBody>
|
||||||
{#each users as user, index (user.id)}
|
{#each users as user, index (user.id)}
|
||||||
{@const { ManageAccess } = getPeopleUserActions($t, [
|
{const { ManageAccess } = $derived(
|
||||||
user,
|
getPeopleUserActions($t, [
|
||||||
...users.filter(({ id }) => id !== authManager.user.id && id !== user.id),
|
user,
|
||||||
])}
|
...users.filter(({ id }) => id !== authManager.user.id && id !== user.id),
|
||||||
|
]),
|
||||||
|
)}
|
||||||
<div class="flex items-center justify-between gap-4" class:mt-4={index > 0}>
|
<div class="flex items-center justify-between gap-4" class:mt-4={index > 0}>
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<UserAvatar {user} size="md" />
|
<UserAvatar {user} size="md" />
|
||||||
@@ -300,7 +302,7 @@
|
|||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
{#each sentRequests as request, index (request.id)}
|
{#each sentRequests as request, index (request.id)}
|
||||||
{@const user = candidates[request.userId]}
|
{const user = $derived(candidates[request.userId])}
|
||||||
<div
|
<div
|
||||||
class="flex items-center justify-between gap-4"
|
class="flex items-center justify-between gap-4"
|
||||||
class:mt-4={index > 0 || receivedRequests.length > 0}
|
class:mt-4={index > 0 || receivedRequests.length > 0}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
|
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{#each keys as key (key.id)}
|
{#each keys as key (key.id)}
|
||||||
{@const { Update, Rotate, Delete } = getApiKeyActions($t, key)}
|
{const { Update, Rotate, Delete } = $derived(getApiKeyActions($t, key))}
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell>{key.name}</TableCell>
|
<TableCell>{key.name}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
|
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{#each passkeys as passkey (passkey.id)}
|
{#each passkeys as passkey (passkey.id)}
|
||||||
{@const { Update, Delete } = getPasskeyActions($t, passkey)}
|
{const { Update, Delete } = $derived(getPasskeyActions($t, passkey))}
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell>{passkey.name ?? $t('passkeys', { values: { count: 1 } })}</TableCell>
|
<TableCell>{passkey.name ?? $t('passkeys', { values: { count: 1 } })}</TableCell>
|
||||||
<TableCell>{new Date(passkey.createdAt).toLocaleDateString($locale, dateFormats.settings)}</TableCell>
|
<TableCell>{new Date(passkey.createdAt).toLocaleDateString($locale, dateFormats.settings)}</TableCell>
|
||||||
|
|||||||
@@ -61,7 +61,7 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<div class="my-6 flex flex-col gap-3">
|
<div class="my-6 flex flex-col gap-3">
|
||||||
{#each workflows as workflow (workflow.id)}
|
{#each workflows as workflow (workflow.id)}
|
||||||
{@const { ToggleEnabled, Duplicate, Logs, Edit, Delete } = getWorkflowActions($t, workflow)}
|
{const { ToggleEnabled, Duplicate, Logs, Edit, Delete } = $derived(getWorkflowActions($t, workflow))}
|
||||||
|
|
||||||
<Card class="group shadow-none transition-colors hover:border-primary">
|
<Card class="group shadow-none transition-colors hover:border-primary">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
<Link href={Route.viewUser(owner)}>{owner.name}</Link>
|
<Link href={Route.viewUser(owner)}>{owner.name}</Link>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
{#if stats}
|
{#if stats}
|
||||||
{@const [diskUsage, diskUsageUnit] = getBytesWithUnit(stats.usage, 0)}
|
{const [diskUsage, diskUsageUnit] = $derived(getBytesWithUnit(stats.usage, 0))}
|
||||||
<TableCell class={libraryTableClasses.column3}>
|
<TableCell class={libraryTableClasses.column3}>
|
||||||
{stats.photos.toLocaleString($locale)}
|
{stats.photos.toLocaleString($locale)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|||||||
@@ -63,7 +63,7 @@
|
|||||||
<table class="w-full">
|
<table class="w-full">
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each library.importPaths as folder (folder)}
|
{#each library.importPaths as folder (folder)}
|
||||||
{@const { Edit, Delete } = getLibraryFolderActions($t, library, folder)}
|
{const { Edit, Delete } = $derived(getLibraryFolderActions($t, library, folder))}
|
||||||
<tr class="h-12">
|
<tr class="h-12">
|
||||||
<td>
|
<td>
|
||||||
<Code>{folder}</Code>
|
<Code>{folder}</Code>
|
||||||
@@ -83,7 +83,7 @@
|
|||||||
<table class="w-full">
|
<table class="w-full">
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each library.exclusionPatterns as exclusionPattern (exclusionPattern)}
|
{#each library.exclusionPatterns as exclusionPattern (exclusionPattern)}
|
||||||
{@const { Edit, Delete } = getLibraryExclusionPatternActions($t, library, exclusionPattern)}
|
{const { Edit, Delete } = $derived(getLibraryExclusionPatternActions($t, library, exclusionPattern))}
|
||||||
<tr class="h-12">
|
<tr class="h-12">
|
||||||
<td>
|
<td>
|
||||||
<Code>{exclusionPattern}</Code>
|
<Code>{exclusionPattern}</Code>
|
||||||
|
|||||||
@@ -86,8 +86,8 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if description}
|
{#if description}
|
||||||
{@const SvelteComponent = description}
|
|
||||||
<div class="text-sm dark:text-white">
|
<div class="text-sm dark:text-white">
|
||||||
|
{const SvelteComponent = $derived(description)}
|
||||||
<SvelteComponent />
|
<SvelteComponent />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -139,10 +139,8 @@
|
|||||||
</QueueCardButton>
|
</QueueCardButton>
|
||||||
{/if}
|
{/if}
|
||||||
{#if queue.isPaused}
|
{#if queue.isPaused}
|
||||||
{@const size = waitingCount > 0 ? '24' : '48'}
|
|
||||||
<QueueCardButton color="light-gray" onClick={() => onCommand({ command: QueueCommand.Resume, force: false })}>
|
<QueueCardButton color="light-gray" onClick={() => onCommand({ command: QueueCommand.Resume, force: false })}>
|
||||||
<!-- size property is not reactive, so have to use width and height -->
|
<Icon icon={mdiFastForward} size={waitingCount > 0 ? '24' : '48'} />
|
||||||
<Icon icon={mdiFastForward} {size} />
|
|
||||||
<span>{$t('resume')}</span>
|
<span>{$t('resume')}</span>
|
||||||
</QueueCardButton>
|
</QueueCardButton>
|
||||||
{:else}
|
{:else}
|
||||||
|
|||||||
@@ -127,7 +127,7 @@
|
|||||||
|
|
||||||
<div class="mt-10 flex flex-col gap-7">
|
<div class="mt-10 flex flex-col gap-7">
|
||||||
{#each queueList as [queueName, props] (queueName)}
|
{#each queueList as [queueName, props] (queueName)}
|
||||||
{@const queue = queues.find(({ name }) => name === queueName)}
|
{const queue = $derived(queues.find(({ name }) => name === queueName))}
|
||||||
{#if queue}
|
{#if queue}
|
||||||
<QueueCard {queue} onCommand={(command) => handleCommand(queueName, command)} {...props} />
|
<QueueCard {queue} onCommand={(command) => handleCommand(queueName, command)} {...props} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -100,7 +100,7 @@
|
|||||||
|
|
||||||
<div class="relative flex text-center font-mono text-2xl font-medium">
|
<div class="relative flex text-center font-mono text-2xl font-medium">
|
||||||
{#if stats}
|
{#if stats}
|
||||||
{@const storageUsageWithUnit = getStorageUsageWithUnit(stats.usage)}
|
{const storageUsageWithUnit = $derived(getStorageUsageWithUnit(stats.usage))}
|
||||||
<span class="text-light-300">{zeros(storageUsageWithUnit[0])}</span><span class="text-primary"
|
<span class="text-light-300">{zeros(storageUsageWithUnit[0])}</span><span class="text-primary"
|
||||||
>{storageUsageWithUnit[0]}</span
|
>{storageUsageWithUnit[0]}</span
|
||||||
>
|
>
|
||||||
@@ -130,7 +130,7 @@
|
|||||||
{#each users as user (user.id)}
|
{#each users as user (user.id)}
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell class="w-1/4">{user.name}</TableCell>
|
<TableCell class="w-1/4">{user.name}</TableCell>
|
||||||
{@const userStats = getUserStats(user.id)}
|
{const userStats = $derived(getUserStats(user.id))}
|
||||||
{#if userStats}
|
{#if userStats}
|
||||||
<TableCell class="w-1/4">
|
<TableCell class="w-1/4">
|
||||||
{userStats.photos.toLocaleString($locale)} (<FormatBytes bytes={userStats.usagePhotos} />)</TableCell
|
{userStats.photos.toLocaleString($locale)} (<FormatBytes bytes={userStats.usagePhotos} />)</TableCell
|
||||||
|
|||||||
Reference in New Issue
Block a user