Compare commits
10
Commits
09bb2a9fb5
...
2c1f321627
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c1f321627 | ||
|
|
e8c1fd297d | ||
|
|
8e3e3a48ae | ||
|
|
dc5553b239 | ||
|
|
32e02a05d2 | ||
|
|
c581bce715 | ||
|
|
dd23264385 | ||
|
|
6dbe72b6a1 | ||
|
|
72f96701f0 | ||
|
|
b4ae7f0dcf |
@@ -35,6 +35,7 @@ jobs:
|
||||
filters: |
|
||||
docs:
|
||||
- 'docs/**'
|
||||
- 'deployment/**'
|
||||
open-api:
|
||||
- 'open-api/immich-openapi-specs.json'
|
||||
force-filters: |
|
||||
|
||||
+18
-28
@@ -2,34 +2,24 @@
|
||||
# Manual edits may be lost in future updates.
|
||||
|
||||
provider "registry.opentofu.org/cloudflare/cloudflare" {
|
||||
version = "4.52.10"
|
||||
constraints = "4.52.10"
|
||||
version = "5.28.1"
|
||||
constraints = "5.28.1"
|
||||
hashes = [
|
||||
"h1:57oeYqz+pYEPAw2GvHARjcER0QYU0teS6DJzS26kCAY=",
|
||||
"h1:6bnAgXSboppQbj79XmRtQLj/58d+b4EozydI8XrqwRE=",
|
||||
"h1:6eP/jGOz1ioH17d3pygKhvsRa4AEKvedKqd+wgyiHYk=",
|
||||
"h1:84gJm57BBSvtfwt7t9ZiU7EKKDQARjsztoDuqB5DfqU=",
|
||||
"h1:99tME9L/Ug/dXElX3Ac7Kx+KD7nhWEEwOTqaQubW18w=",
|
||||
"h1:TnMGVaPpIH5NZX324v+y6j1iW3ym0MqqNekxHlzXT3A=",
|
||||
"h1:Z1L5jI4GiJxAAsfNyZHU4MGTluWEjjKtlB8Xn6cA0Dg=",
|
||||
"h1:dB/WsY27MLla/cwjYggolJhVeHUeS59rlOObAWK2e24=",
|
||||
"h1:gpffNocq9fHK+uQehZQK6YmL1aHeCGrVM1p53dcFP/0=",
|
||||
"h1:jvfw1A2Uck4AjNmmWXwGH6P0sGQrnNY/KUVlqgyqYUM=",
|
||||
"h1:ojvrl9uI1EPtAwKRHim9eSc4Lc88thU/2BFGdJLEwrc=",
|
||||
"h1:rJZJK06NYCcSZhY5fwsRUSznSmAmRWVg4sO0auvbURo=",
|
||||
"h1:y2m6i2SnQ3ZxiJHlMWNLHJrEbNqQT6OpDGyhFP+Nmds=",
|
||||
"zh:06c09cc50d6c61fb5f1ca79e4b22d5f383c6a848f885ed542dadef44ab4bfd55",
|
||||
"zh:205bdfdac49d6b25ec2ca7ecffd39bf47126340e1943066bdbbd64d6d72577d2",
|
||||
"zh:341ed688772d8e2bd9a148c57d56d5dfbe3d625bc50296a505cbae3d29694562",
|
||||
"zh:3790619bae55558ab4d93f53686f7cf55f35138f800ea07b0b9c20cce75b9c0d",
|
||||
"zh:44284a5b67e34f1381bbc47b6e765cee581037e25cd3b459caec069493b63898",
|
||||
"zh:493a173f3d5245db82782e8c1aba63e34c6fcf6b952b9288926b519bfe9d9725",
|
||||
"zh:4eb7846381fee6eefdb7e0aaf80153ed71d35547752f96987218a9429645844f",
|
||||
"zh:850b1d1da770f48def40223d754ff65c31fabb1a7f334ed6ebb40eba09c41572",
|
||||
"zh:9a29a08a9fd40d7c1beefe8597617c2fc475d62ec6d5ca8fcc2cd1a056a67a2e",
|
||||
"zh:9e57472f0a56863204f5b2eef125d4fb1222eca793c6a5ac9f4df6c1776edc48",
|
||||
"zh:a9eff98931da596c2d335359841bfd668f4b6cde257e15546a558c7f5d72773b",
|
||||
"zh:c9293fb3390c79e4f5072e019c0a2c4c7408a55a55e78ae37f739a1da75886ef",
|
||||
"zh:c9e98abec7f7a1e2669dd9c065896217125d774640e4673c1303a942736b3eb4",
|
||||
"h1:9YL6g+iEfg3OzX6qSZOYsictVO0fNP09D6jRwIZKwQ0=",
|
||||
"h1:GjOKfCmvcyE06fUp5WrAzfCLkTQaCg2Ko/kFlq8OfpM=",
|
||||
"h1:QTIohIPsVCqEmknrR2PdbVXyK1fjS805rMaZpEEpzFQ=",
|
||||
"h1:SFb952CVUFUoFjfor+GUqAV6DQ6bnUfaFcWL2yJlFBo=",
|
||||
"h1:aso3s6IuIQju5L/OLrywCfQl7aA5TF+sb5pZzo8JG20=",
|
||||
"h1:cxGa9LRKCzwNacgnLjXnHUygkvAEd6pb8g8sRwzPSRE=",
|
||||
"h1:kBW0qcDqNupOnsi7vvx/0TQRxXLtmDKAHwOwH5l03pc=",
|
||||
"h1:t6x20B8TpQAG0TsUlyWLrAWNO+V8JE8K3Ij0hW0feSU=",
|
||||
"zh:219d464dd727c0e5c90a97b44aa5d7988acc4eec47f3684f7ecd006b17c46d9c",
|
||||
"zh:2511f2380209c81121e6eb943d7e68d03602a4fcf4f0311dfc67e198179b9303",
|
||||
"zh:2ae200482bf592cd703981c4d4fe87a52c0dfae81a70261295f59b1bb0d01a6c",
|
||||
"zh:7df6fe6573379c2faf2e801fdbca58ac909266dbee604feaa993439b8616c9b3",
|
||||
"zh:9fb5a295588351ce565515dc2a770047e8bab786a4a9665511ab9a9ca2db5f1b",
|
||||
"zh:a182f4aec86c74e9db4affa22323c5a21f5d92c1fcf91b934570753d04f4daf8",
|
||||
"zh:a39bb8624fb6c714e38bc0e632aae117a9fede5a18a248ab15dd1b60013c0324",
|
||||
"zh:bef2ffbdb68569549fe432b550592e07456759a7288f84f7c37c9d933a493f54",
|
||||
]
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ terraform {
|
||||
required_providers {
|
||||
cloudflare = {
|
||||
source = "cloudflare/cloudflare"
|
||||
version = "4.52.10"
|
||||
version = "5.28.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+18
-28
@@ -2,34 +2,24 @@
|
||||
# Manual edits may be lost in future updates.
|
||||
|
||||
provider "registry.opentofu.org/cloudflare/cloudflare" {
|
||||
version = "4.52.10"
|
||||
constraints = "4.52.10"
|
||||
version = "5.28.1"
|
||||
constraints = "5.28.1"
|
||||
hashes = [
|
||||
"h1:57oeYqz+pYEPAw2GvHARjcER0QYU0teS6DJzS26kCAY=",
|
||||
"h1:6bnAgXSboppQbj79XmRtQLj/58d+b4EozydI8XrqwRE=",
|
||||
"h1:6eP/jGOz1ioH17d3pygKhvsRa4AEKvedKqd+wgyiHYk=",
|
||||
"h1:84gJm57BBSvtfwt7t9ZiU7EKKDQARjsztoDuqB5DfqU=",
|
||||
"h1:99tME9L/Ug/dXElX3Ac7Kx+KD7nhWEEwOTqaQubW18w=",
|
||||
"h1:TnMGVaPpIH5NZX324v+y6j1iW3ym0MqqNekxHlzXT3A=",
|
||||
"h1:Z1L5jI4GiJxAAsfNyZHU4MGTluWEjjKtlB8Xn6cA0Dg=",
|
||||
"h1:dB/WsY27MLla/cwjYggolJhVeHUeS59rlOObAWK2e24=",
|
||||
"h1:gpffNocq9fHK+uQehZQK6YmL1aHeCGrVM1p53dcFP/0=",
|
||||
"h1:jvfw1A2Uck4AjNmmWXwGH6P0sGQrnNY/KUVlqgyqYUM=",
|
||||
"h1:ojvrl9uI1EPtAwKRHim9eSc4Lc88thU/2BFGdJLEwrc=",
|
||||
"h1:rJZJK06NYCcSZhY5fwsRUSznSmAmRWVg4sO0auvbURo=",
|
||||
"h1:y2m6i2SnQ3ZxiJHlMWNLHJrEbNqQT6OpDGyhFP+Nmds=",
|
||||
"zh:06c09cc50d6c61fb5f1ca79e4b22d5f383c6a848f885ed542dadef44ab4bfd55",
|
||||
"zh:205bdfdac49d6b25ec2ca7ecffd39bf47126340e1943066bdbbd64d6d72577d2",
|
||||
"zh:341ed688772d8e2bd9a148c57d56d5dfbe3d625bc50296a505cbae3d29694562",
|
||||
"zh:3790619bae55558ab4d93f53686f7cf55f35138f800ea07b0b9c20cce75b9c0d",
|
||||
"zh:44284a5b67e34f1381bbc47b6e765cee581037e25cd3b459caec069493b63898",
|
||||
"zh:493a173f3d5245db82782e8c1aba63e34c6fcf6b952b9288926b519bfe9d9725",
|
||||
"zh:4eb7846381fee6eefdb7e0aaf80153ed71d35547752f96987218a9429645844f",
|
||||
"zh:850b1d1da770f48def40223d754ff65c31fabb1a7f334ed6ebb40eba09c41572",
|
||||
"zh:9a29a08a9fd40d7c1beefe8597617c2fc475d62ec6d5ca8fcc2cd1a056a67a2e",
|
||||
"zh:9e57472f0a56863204f5b2eef125d4fb1222eca793c6a5ac9f4df6c1776edc48",
|
||||
"zh:a9eff98931da596c2d335359841bfd668f4b6cde257e15546a558c7f5d72773b",
|
||||
"zh:c9293fb3390c79e4f5072e019c0a2c4c7408a55a55e78ae37f739a1da75886ef",
|
||||
"zh:c9e98abec7f7a1e2669dd9c065896217125d774640e4673c1303a942736b3eb4",
|
||||
"h1:9YL6g+iEfg3OzX6qSZOYsictVO0fNP09D6jRwIZKwQ0=",
|
||||
"h1:GjOKfCmvcyE06fUp5WrAzfCLkTQaCg2Ko/kFlq8OfpM=",
|
||||
"h1:QTIohIPsVCqEmknrR2PdbVXyK1fjS805rMaZpEEpzFQ=",
|
||||
"h1:SFb952CVUFUoFjfor+GUqAV6DQ6bnUfaFcWL2yJlFBo=",
|
||||
"h1:aso3s6IuIQju5L/OLrywCfQl7aA5TF+sb5pZzo8JG20=",
|
||||
"h1:cxGa9LRKCzwNacgnLjXnHUygkvAEd6pb8g8sRwzPSRE=",
|
||||
"h1:kBW0qcDqNupOnsi7vvx/0TQRxXLtmDKAHwOwH5l03pc=",
|
||||
"h1:t6x20B8TpQAG0TsUlyWLrAWNO+V8JE8K3Ij0hW0feSU=",
|
||||
"zh:219d464dd727c0e5c90a97b44aa5d7988acc4eec47f3684f7ecd006b17c46d9c",
|
||||
"zh:2511f2380209c81121e6eb943d7e68d03602a4fcf4f0311dfc67e198179b9303",
|
||||
"zh:2ae200482bf592cd703981c4d4fe87a52c0dfae81a70261295f59b1bb0d01a6c",
|
||||
"zh:7df6fe6573379c2faf2e801fdbca58ac909266dbee604feaa993439b8616c9b3",
|
||||
"zh:9fb5a295588351ce565515dc2a770047e8bab786a4a9665511ab9a9ca2db5f1b",
|
||||
"zh:a182f4aec86c74e9db4affa22323c5a21f5d92c1fcf91b934570753d04f4daf8",
|
||||
"zh:a39bb8624fb6c714e38bc0e632aae117a9fede5a18a248ab15dd1b60013c0324",
|
||||
"zh:bef2ffbdb68569549fe432b550592e07456759a7288f84f7c37c9d933a493f54",
|
||||
]
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ terraform {
|
||||
required_providers {
|
||||
cloudflare = {
|
||||
source = "cloudflare/cloudflare"
|
||||
version = "4.52.10"
|
||||
version = "5.28.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,8 +74,8 @@ class TimelineFactory {
|
||||
|
||||
TimelineService place(String place) => TimelineService(_timelineRepository.place(place, groupBy));
|
||||
|
||||
TimelineService person(List<String> userIds, String personId) =>
|
||||
TimelineService(_timelineRepository.person(userIds, personId, groupBy));
|
||||
TimelineService person(String userId, String personId) =>
|
||||
TimelineService(_timelineRepository.person(userId, personId, groupBy));
|
||||
|
||||
TimelineService fromAssets(List<BaseAsset> assets, TimelineOrigin type) =>
|
||||
TimelineService(_timelineRepository.fromAssets(assets, type));
|
||||
|
||||
@@ -366,10 +366,10 @@ class TimelineRepository extends DatabaseAccessor<Drift> with $TimelineRepositor
|
||||
origin: TimelineOrigin.place,
|
||||
);
|
||||
|
||||
TimelineQuery person(List<String> userIds, String personId, GroupAssetsBy groupBy) => (
|
||||
bucketSource: () => _watchPersonBucket(userIds, personId, groupBy: groupBy),
|
||||
TimelineQuery person(String userId, String personId, GroupAssetsBy groupBy) => (
|
||||
bucketSource: () => _watchPersonBucket(userId, personId, groupBy: groupBy),
|
||||
assetSource: (offset, count) =>
|
||||
_getPersonBucketAssets(userIds, personId, groupBy: groupBy, offset: offset, count: count),
|
||||
_getPersonBucketAssets(userId, personId, groupBy: groupBy, offset: offset, count: count),
|
||||
origin: TimelineOrigin.person,
|
||||
);
|
||||
|
||||
@@ -430,7 +430,7 @@ class TimelineRepository extends DatabaseAccessor<Drift> with $TimelineRepositor
|
||||
return query.map((row) => row.readTable(_db.remoteAssetEntity).toDto()).get();
|
||||
}
|
||||
|
||||
Expression<bool> _personAssetFilter(List<String> userIds, String personId) {
|
||||
Expression<bool> _personAssetFilter(String userId, String personId) {
|
||||
final faceAssetIds = _db.assetFaceEntity.selectOnly()
|
||||
..addColumns([_db.assetFaceEntity.assetId])
|
||||
..where(
|
||||
@@ -438,22 +438,23 @@ class TimelineRepository extends DatabaseAccessor<Drift> with $TimelineRepositor
|
||||
_db.assetFaceEntity.isVisible.equals(true) &
|
||||
_db.assetFaceEntity.deletedAt.isNull(),
|
||||
);
|
||||
final partnerIds = _db.partnerEntity.selectOnly()
|
||||
..addColumns([_db.partnerEntity.sharedById])
|
||||
..where(_db.partnerEntity.sharedWithId.equals(userId));
|
||||
final albumAssetIds = _db.remoteAlbumAssetEntity.selectOnly()..addColumns([_db.remoteAlbumAssetEntity.assetId]);
|
||||
return _db.remoteAssetEntity.id.isInQuery(faceAssetIds) &
|
||||
(_db.remoteAssetEntity.ownerId.isIn(userIds) | _db.remoteAssetEntity.id.isInQuery(albumAssetIds)) &
|
||||
(_db.remoteAssetEntity.ownerId.equals(userId) |
|
||||
_db.remoteAssetEntity.ownerId.isInQuery(partnerIds) |
|
||||
_db.remoteAssetEntity.id.isInQuery(albumAssetIds)) &
|
||||
_db.remoteAssetEntity.deletedAt.isNull() &
|
||||
_db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline);
|
||||
}
|
||||
|
||||
Stream<List<Bucket>> _watchPersonBucket(
|
||||
List<String> userIds,
|
||||
String personId, {
|
||||
GroupAssetsBy groupBy = GroupAssetsBy.day,
|
||||
}) {
|
||||
Stream<List<Bucket>> _watchPersonBucket(String userId, String personId, {GroupAssetsBy groupBy = GroupAssetsBy.day}) {
|
||||
if (groupBy == GroupAssetsBy.none) {
|
||||
final query = _db.remoteAssetEntity.selectOnly()
|
||||
..addColumns([_db.remoteAssetEntity.id.count()])
|
||||
..where(_personAssetFilter(userIds, personId));
|
||||
..where(_personAssetFilter(userId, personId));
|
||||
|
||||
return query.map((row) {
|
||||
final count = row.read(_db.remoteAssetEntity.id.count())!;
|
||||
@@ -466,7 +467,7 @@ class TimelineRepository extends DatabaseAccessor<Drift> with $TimelineRepositor
|
||||
|
||||
final query = _db.remoteAssetEntity.selectOnly()
|
||||
..addColumns([assetCountExp, dateExp])
|
||||
..where(_personAssetFilter(userIds, personId))
|
||||
..where(_personAssetFilter(userId, personId))
|
||||
..groupBy([dateExp])
|
||||
..orderBy([OrderingTerm.desc(dateExp)]);
|
||||
|
||||
@@ -478,14 +479,14 @@ class TimelineRepository extends DatabaseAccessor<Drift> with $TimelineRepositor
|
||||
}
|
||||
|
||||
Future<List<BaseAsset>> _getPersonBucketAssets(
|
||||
List<String> userIds,
|
||||
String userId,
|
||||
String personId, {
|
||||
required int offset,
|
||||
required int count,
|
||||
GroupAssetsBy groupBy = GroupAssetsBy.day,
|
||||
}) {
|
||||
final query = _db.remoteAssetEntity.select()
|
||||
..where((_) => _personAssetFilter(userIds, personId))
|
||||
..where((_) => _personAssetFilter(userId, personId))
|
||||
..orderBy(_assetDateOrder(groupBy))
|
||||
..limit(count, offset: offset);
|
||||
|
||||
|
||||
@@ -82,8 +82,7 @@ class _PersonPageState extends ConsumerState<PersonPage> {
|
||||
throw Exception('User must be logged in to view person timeline');
|
||||
}
|
||||
|
||||
final users = ref.watch(timelineUsersProvider).valueOrNull ?? [user.id];
|
||||
final timelineService = ref.watch(timelineFactoryProvider).person(users, _person.id);
|
||||
final timelineService = ref.watch(timelineFactoryProvider).person(user.id, _person.id);
|
||||
ref.onDispose(timelineService.dispose);
|
||||
return timelineService;
|
||||
}),
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ name: immich_mobile
|
||||
description: Immich - selfhosted backup media file on mobile phone
|
||||
|
||||
publish_to: 'none'
|
||||
version: 3.3.5-local.12+3030504
|
||||
version: 3.3.5-local.13+3030505
|
||||
|
||||
environment:
|
||||
sdk: '>=3.12.0 <4.0.0'
|
||||
|
||||
@@ -103,6 +103,22 @@ void main() {
|
||||
});
|
||||
|
||||
group('person assets', () {
|
||||
test('includes partner assets even when the partner is not shown in the timeline', () async {
|
||||
final user = await ctx.newUser();
|
||||
final partner = await ctx.newUser();
|
||||
final stranger = await ctx.newUser();
|
||||
await ctx.newPartner(sharedById: partner.id, sharedWithId: user.id, inTimeline: false);
|
||||
|
||||
final person = await ctx.newPerson(ownerId: user.id);
|
||||
final partnerAsset = await ctx.newRemoteAsset(ownerId: partner.id);
|
||||
final strangerAsset = await ctx.newRemoteAsset(ownerId: stranger.id);
|
||||
await ctx.newFace(assetId: partnerAsset.id, personId: person.id);
|
||||
await ctx.newFace(assetId: strangerAsset.id, personId: person.id);
|
||||
|
||||
final assets = await sut.person(user.id, person.id, .day).assetSource(0, 10);
|
||||
expect(assets.map((a) => (a as RemoteAsset).id), [partnerAsset.id]);
|
||||
});
|
||||
|
||||
test('does not duplicate an asset that has multiple face records for the same person', () async {
|
||||
// Regression check for #26723: an INNER JOIN between remote_asset_entity and asset_face_entity
|
||||
// fanned out one asset into N rows when N face records pointed at the same (asset, person) pair
|
||||
@@ -113,7 +129,7 @@ void main() {
|
||||
await ctx.newFace(assetId: asset.id, personId: person.id);
|
||||
await ctx.newFace(assetId: asset.id, personId: person.id);
|
||||
|
||||
final query = sut.person([user.id], person.id, .day);
|
||||
final query = sut.person(user.id, person.id, .day);
|
||||
|
||||
final buckets = await query.bucketSource().first;
|
||||
expect(buckets, hasLength(1));
|
||||
@@ -140,7 +156,7 @@ void main() {
|
||||
await ctx.newFace(assetId: shiftedLater.id, personId: person.id);
|
||||
await ctx.newFace(assetId: shiftedEarlier.id, personId: person.id);
|
||||
|
||||
final query = sut.person([user.id], person.id, .day);
|
||||
final query = sut.person(user.id, person.id, .day);
|
||||
|
||||
final buckets = await query.bucketSource().first;
|
||||
expect(buckets, hasLength(2));
|
||||
|
||||
@@ -6343,7 +6343,7 @@
|
||||
"state": "Added"
|
||||
}
|
||||
],
|
||||
"x-immich-permission": "clusterGroup.read"
|
||||
"x-immich-permission": "clusterGroup.admin"
|
||||
}
|
||||
},
|
||||
"/cluster-groups/{id}/requests": {
|
||||
@@ -25811,6 +25811,7 @@
|
||||
"backup.delete",
|
||||
"clusterGroup.read",
|
||||
"clusterGroup.leave",
|
||||
"clusterGroup.admin",
|
||||
"clusterGroupRequest.create",
|
||||
"clusterGroupRequest.read",
|
||||
"clusterGroupRequest.delete",
|
||||
@@ -28466,11 +28467,13 @@
|
||||
"ServerFeatureLevel": {
|
||||
"description": "Server feature level",
|
||||
"enum": [
|
||||
1
|
||||
1,
|
||||
2
|
||||
],
|
||||
"type": "integer",
|
||||
"x-enum-varnames": [
|
||||
"FeatureLevel"
|
||||
"FeatureLevel",
|
||||
"Passkeys"
|
||||
]
|
||||
},
|
||||
"ServerFeaturesDto": {
|
||||
|
||||
@@ -407,7 +407,7 @@
|
||||
"matchType": {
|
||||
"type": "string",
|
||||
"title": "Match type",
|
||||
"enum": ["contains", "startsWith", "exact", "regex"],
|
||||
"enum": ["contains", "startsWith", "exact", "regex", "present", "missing"],
|
||||
"default": "contains",
|
||||
"description": "Type of pattern matching to perform",
|
||||
"uiHint": {
|
||||
@@ -424,7 +424,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["property", "pattern"]
|
||||
"required": ["property"]
|
||||
},
|
||||
"uiHints": ["Filter"]
|
||||
},
|
||||
|
||||
@@ -3,13 +3,18 @@ import { AssetVisibility } from '@immich/sdk';
|
||||
import type { Manifest } from '../dist/index.d.ts';
|
||||
|
||||
type MatchValueConfig = {
|
||||
pattern: string;
|
||||
matchType?: 'contains' | 'exact' | 'regex' | 'startsWith';
|
||||
pattern?: string;
|
||||
matchType?: 'contains' | 'exact' | 'regex' | 'startsWith' | 'missing' | 'present';
|
||||
caseSensitive?: boolean;
|
||||
};
|
||||
|
||||
const matchValueResult = (value: string, config: MatchValueConfig) => {
|
||||
const { pattern, matchType = 'contains', caseSensitive = false } = config;
|
||||
const matchValueResult = (value: string | null, config: MatchValueConfig) => {
|
||||
const { pattern = '', matchType = 'contains', caseSensitive = false } = config;
|
||||
|
||||
if (value === null) {
|
||||
return { workflow: { continue: matchType === 'missing' } };
|
||||
}
|
||||
|
||||
const searchName = caseSensitive ? value : value.toLowerCase();
|
||||
const searchPattern = caseSensitive ? pattern : pattern.toLowerCase();
|
||||
|
||||
@@ -32,6 +37,15 @@ const matchValueResult = (value: string, config: MatchValueConfig) => {
|
||||
return { workflow: { continue: regex.test(value) } };
|
||||
}
|
||||
|
||||
case 'present': {
|
||||
return { workflow: { continue: true } };
|
||||
}
|
||||
|
||||
case 'missing': {
|
||||
// value is not null at this point
|
||||
return { workflow: { continue: false } };
|
||||
}
|
||||
|
||||
default: {
|
||||
return {};
|
||||
}
|
||||
@@ -135,11 +149,10 @@ const methods = wrapper<Manifest>({
|
||||
},
|
||||
|
||||
assetExifFilter: ({ config, data }) => {
|
||||
if (!data.asset.exifInfo || data.asset.exifInfo[config.property] === null) {
|
||||
return { workflow: { continue: false } };
|
||||
}
|
||||
let value = data.asset.exifInfo?.[config.property];
|
||||
|
||||
return matchValueResult(String(data.asset.exifInfo[config.property]), config);
|
||||
// Empty EXIF value is treated as missing to mirror behavior of the web UI
|
||||
return matchValueResult(value === null || value === undefined || value === '' ? null : String(value), config);
|
||||
},
|
||||
|
||||
assetDateFilter: ({ config, data }) => {
|
||||
|
||||
@@ -8351,6 +8351,7 @@ export enum Permission {
|
||||
BackupDelete = "backup.delete",
|
||||
ClusterGroupRead = "clusterGroup.read",
|
||||
ClusterGroupLeave = "clusterGroup.leave",
|
||||
ClusterGroupAdmin = "clusterGroup.admin",
|
||||
ClusterGroupRequestCreate = "clusterGroupRequest.create",
|
||||
ClusterGroupRequestRead = "clusterGroupRequest.read",
|
||||
ClusterGroupRequestDelete = "clusterGroupRequest.delete",
|
||||
@@ -8823,7 +8824,8 @@ export enum ReleaseType {
|
||||
Prerelease = "prerelease"
|
||||
}
|
||||
export enum ServerFeatureLevel {
|
||||
FeatureLevel = 1
|
||||
FeatureLevel = 1,
|
||||
Passkeys = 2
|
||||
}
|
||||
export enum UserMetadataKey {
|
||||
Preferences = "preferences",
|
||||
|
||||
Generated
+460
-445
File diff suppressed because it is too large
Load Diff
@@ -94,7 +94,7 @@ export class ClusterGroupController {
|
||||
}
|
||||
|
||||
@Post(':id/regenerate-people')
|
||||
@Authenticated({ permission: Permission.ClusterGroupRead })
|
||||
@Authenticated({ permission: Permission.ClusterGroupAdmin })
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
@Endpoint({
|
||||
summary: 'Regenerate people of users in cluster group',
|
||||
|
||||
@@ -193,6 +193,7 @@ export enum Permission {
|
||||
|
||||
ClusterGroupRead = 'clusterGroup.read',
|
||||
ClusterGroupLeave = 'clusterGroup.leave',
|
||||
ClusterGroupAdmin = 'clusterGroup.admin',
|
||||
ClusterGroupRequestCreate = 'clusterGroupRequest.create',
|
||||
ClusterGroupRequestRead = 'clusterGroupRequest.read',
|
||||
ClusterGroupRequestDelete = 'clusterGroupRequest.delete',
|
||||
@@ -1340,6 +1341,7 @@ export const SearchOrderFieldSchema = z.enum(SearchOrderField).meta({ id: 'Searc
|
||||
|
||||
export enum ServerFeatureLevel {
|
||||
FeatureLevel = 1,
|
||||
Passkeys = 2,
|
||||
}
|
||||
|
||||
export const ServerFeatureLevelSchema = z
|
||||
|
||||
@@ -38,7 +38,48 @@ select
|
||||
and "asset_file"."type" = $1
|
||||
) as agg
|
||||
) as "files",
|
||||
to_json("asset_exif") as "exifInfo"
|
||||
(
|
||||
select
|
||||
to_json(obj)
|
||||
from
|
||||
(
|
||||
select
|
||||
"asset_exif"."assetId",
|
||||
"asset_exif"."autoStackId",
|
||||
"asset_exif"."bitsPerSample",
|
||||
"asset_exif"."city",
|
||||
"asset_exif"."colorspace",
|
||||
"asset_exif"."country",
|
||||
"asset_exif"."dateTimeOriginal",
|
||||
"asset_exif"."description",
|
||||
"asset_exif"."exifImageHeight",
|
||||
"asset_exif"."exifImageWidth",
|
||||
"asset_exif"."exposureTime",
|
||||
"asset_exif"."fileSizeInByte",
|
||||
"asset_exif"."fNumber",
|
||||
"asset_exif"."focalLength",
|
||||
"asset_exif"."fps",
|
||||
"asset_exif"."iso",
|
||||
"asset_exif"."latitude",
|
||||
"asset_exif"."lensModel",
|
||||
"asset_exif"."livePhotoCID",
|
||||
"asset_exif"."longitude",
|
||||
"asset_exif"."make",
|
||||
"asset_exif"."model",
|
||||
"asset_exif"."modifyDate",
|
||||
"asset_exif"."orientation",
|
||||
"asset_exif"."profileDescription",
|
||||
"asset_exif"."projectionType",
|
||||
"asset_exif"."rating",
|
||||
"asset_exif"."state",
|
||||
"asset_exif"."tags",
|
||||
"asset_exif"."timeZone"
|
||||
from
|
||||
"asset_exif"
|
||||
where
|
||||
"asset_exif"."assetId" = "asset"."id"
|
||||
) as obj
|
||||
) as "exifInfo"
|
||||
from
|
||||
"asset"
|
||||
inner join "asset_exif" on "asset"."id" = "asset_exif"."assetId"
|
||||
@@ -239,7 +280,48 @@ select
|
||||
"asset_edit"."assetId" = "asset"."id"
|
||||
) as agg
|
||||
) as "edits",
|
||||
to_json("asset_exif") as "exifInfo",
|
||||
(
|
||||
select
|
||||
to_json(obj)
|
||||
from
|
||||
(
|
||||
select
|
||||
"asset_exif"."assetId",
|
||||
"asset_exif"."autoStackId",
|
||||
"asset_exif"."bitsPerSample",
|
||||
"asset_exif"."city",
|
||||
"asset_exif"."colorspace",
|
||||
"asset_exif"."country",
|
||||
"asset_exif"."dateTimeOriginal",
|
||||
"asset_exif"."description",
|
||||
"asset_exif"."exifImageHeight",
|
||||
"asset_exif"."exifImageWidth",
|
||||
"asset_exif"."exposureTime",
|
||||
"asset_exif"."fileSizeInByte",
|
||||
"asset_exif"."fNumber",
|
||||
"asset_exif"."focalLength",
|
||||
"asset_exif"."fps",
|
||||
"asset_exif"."iso",
|
||||
"asset_exif"."latitude",
|
||||
"asset_exif"."lensModel",
|
||||
"asset_exif"."livePhotoCID",
|
||||
"asset_exif"."longitude",
|
||||
"asset_exif"."make",
|
||||
"asset_exif"."model",
|
||||
"asset_exif"."modifyDate",
|
||||
"asset_exif"."orientation",
|
||||
"asset_exif"."profileDescription",
|
||||
"asset_exif"."projectionType",
|
||||
"asset_exif"."rating",
|
||||
"asset_exif"."state",
|
||||
"asset_exif"."tags",
|
||||
"asset_exif"."timeZone"
|
||||
from
|
||||
"asset_exif"
|
||||
where
|
||||
"asset_exif"."assetId" = "asset"."id"
|
||||
) as obj
|
||||
) as "exifInfo",
|
||||
(
|
||||
select
|
||||
to_json(obj)
|
||||
@@ -449,7 +531,48 @@ where
|
||||
select
|
||||
"asset"."id",
|
||||
"asset"."visibility",
|
||||
to_json("asset_exif") as "exifInfo",
|
||||
(
|
||||
select
|
||||
to_json(obj)
|
||||
from
|
||||
(
|
||||
select
|
||||
"asset_exif"."assetId",
|
||||
"asset_exif"."autoStackId",
|
||||
"asset_exif"."bitsPerSample",
|
||||
"asset_exif"."city",
|
||||
"asset_exif"."colorspace",
|
||||
"asset_exif"."country",
|
||||
"asset_exif"."dateTimeOriginal",
|
||||
"asset_exif"."description",
|
||||
"asset_exif"."exifImageHeight",
|
||||
"asset_exif"."exifImageWidth",
|
||||
"asset_exif"."exposureTime",
|
||||
"asset_exif"."fileSizeInByte",
|
||||
"asset_exif"."fNumber",
|
||||
"asset_exif"."focalLength",
|
||||
"asset_exif"."fps",
|
||||
"asset_exif"."iso",
|
||||
"asset_exif"."latitude",
|
||||
"asset_exif"."lensModel",
|
||||
"asset_exif"."livePhotoCID",
|
||||
"asset_exif"."longitude",
|
||||
"asset_exif"."make",
|
||||
"asset_exif"."model",
|
||||
"asset_exif"."modifyDate",
|
||||
"asset_exif"."orientation",
|
||||
"asset_exif"."profileDescription",
|
||||
"asset_exif"."projectionType",
|
||||
"asset_exif"."rating",
|
||||
"asset_exif"."state",
|
||||
"asset_exif"."tags",
|
||||
"asset_exif"."timeZone"
|
||||
from
|
||||
"asset_exif"
|
||||
where
|
||||
"asset_exif"."assetId" = "asset"."id"
|
||||
) as obj
|
||||
) as "exifInfo",
|
||||
(
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
|
||||
@@ -134,7 +134,48 @@ select
|
||||
"asset"."type",
|
||||
"asset"."width",
|
||||
"asset"."height",
|
||||
to_json("asset_exif") as "exifInfo"
|
||||
(
|
||||
select
|
||||
to_json(obj)
|
||||
from
|
||||
(
|
||||
select
|
||||
"asset_exif"."assetId",
|
||||
"asset_exif"."autoStackId",
|
||||
"asset_exif"."bitsPerSample",
|
||||
"asset_exif"."city",
|
||||
"asset_exif"."colorspace",
|
||||
"asset_exif"."country",
|
||||
"asset_exif"."dateTimeOriginal",
|
||||
"asset_exif"."description",
|
||||
"asset_exif"."exifImageHeight",
|
||||
"asset_exif"."exifImageWidth",
|
||||
"asset_exif"."exposureTime",
|
||||
"asset_exif"."fileSizeInByte",
|
||||
"asset_exif"."fNumber",
|
||||
"asset_exif"."focalLength",
|
||||
"asset_exif"."fps",
|
||||
"asset_exif"."iso",
|
||||
"asset_exif"."latitude",
|
||||
"asset_exif"."lensModel",
|
||||
"asset_exif"."livePhotoCID",
|
||||
"asset_exif"."longitude",
|
||||
"asset_exif"."make",
|
||||
"asset_exif"."model",
|
||||
"asset_exif"."modifyDate",
|
||||
"asset_exif"."orientation",
|
||||
"asset_exif"."profileDescription",
|
||||
"asset_exif"."projectionType",
|
||||
"asset_exif"."rating",
|
||||
"asset_exif"."state",
|
||||
"asset_exif"."tags",
|
||||
"asset_exif"."timeZone"
|
||||
from
|
||||
"asset_exif"
|
||||
where
|
||||
"asset_exif"."assetId" = "asset"."id"
|
||||
) as obj
|
||||
) as "exifInfo"
|
||||
from
|
||||
"asset"
|
||||
inner join "asset_exif" on "asset"."id" = "asset_exif"."assetId"
|
||||
@@ -1420,7 +1461,48 @@ offset
|
||||
|
||||
-- SearchRepository.searchMetadataV3 (order-rating-withExif)
|
||||
select
|
||||
to_json("asset_exif") as "exifInfo",
|
||||
(
|
||||
select
|
||||
to_json(obj)
|
||||
from
|
||||
(
|
||||
select
|
||||
"asset_exif"."assetId",
|
||||
"asset_exif"."autoStackId",
|
||||
"asset_exif"."bitsPerSample",
|
||||
"asset_exif"."city",
|
||||
"asset_exif"."colorspace",
|
||||
"asset_exif"."country",
|
||||
"asset_exif"."dateTimeOriginal",
|
||||
"asset_exif"."description",
|
||||
"asset_exif"."exifImageHeight",
|
||||
"asset_exif"."exifImageWidth",
|
||||
"asset_exif"."exposureTime",
|
||||
"asset_exif"."fileSizeInByte",
|
||||
"asset_exif"."fNumber",
|
||||
"asset_exif"."focalLength",
|
||||
"asset_exif"."fps",
|
||||
"asset_exif"."iso",
|
||||
"asset_exif"."latitude",
|
||||
"asset_exif"."lensModel",
|
||||
"asset_exif"."livePhotoCID",
|
||||
"asset_exif"."longitude",
|
||||
"asset_exif"."make",
|
||||
"asset_exif"."model",
|
||||
"asset_exif"."modifyDate",
|
||||
"asset_exif"."orientation",
|
||||
"asset_exif"."profileDescription",
|
||||
"asset_exif"."projectionType",
|
||||
"asset_exif"."rating",
|
||||
"asset_exif"."state",
|
||||
"asset_exif"."tags",
|
||||
"asset_exif"."timeZone"
|
||||
from
|
||||
"asset_exif"
|
||||
where
|
||||
"asset_exif"."assetId" = "asset"."id"
|
||||
) as obj
|
||||
) as "exifInfo",
|
||||
"asset"."id",
|
||||
"asset"."updateId",
|
||||
"asset"."createdAt",
|
||||
|
||||
@@ -73,7 +73,7 @@ export class ClusterGroupService extends BaseService {
|
||||
}
|
||||
|
||||
async regeneratePeople(auth: AuthDto, id: string) {
|
||||
await this.requireAccess({ auth, permission: Permission.ClusterGroupRead, ids: [id] });
|
||||
await this.requireAccess({ auth, permission: Permission.ClusterGroupAdmin, ids: [id] });
|
||||
|
||||
await this.jobRepository.queue({
|
||||
name: JobName.FacialRecognitionQueueAll,
|
||||
|
||||
@@ -356,6 +356,10 @@ const checkOtherAccess = async (access: AccessRepository, request: OtherAccessRe
|
||||
return access.person.checkFaceOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.ClusterGroupAdmin: {
|
||||
return access.clusterGroup.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.ClusterGroupRead: {
|
||||
const isMember = await access.clusterGroup.checkOwnerAccess(auth.user.id, ids);
|
||||
const isInvited = await access.clusterGroup.checkInviteAccess(auth.user.id, setDifference(ids, isMember));
|
||||
|
||||
@@ -123,10 +123,15 @@ export function withExif<O>(qb: SelectQueryBuilder<DB, 'asset', O>) {
|
||||
return qb.leftJoin('asset_exif', 'asset.id', 'asset_exif.assetId').select(selectExifInfo);
|
||||
}
|
||||
|
||||
const selectReturnedExifInfo = (eb: ExpressionBuilder<DB, 'asset'>) =>
|
||||
jsonObjectFrom(eb.selectFrom('asset_exif').select(columns.exif).whereRef('asset_exif.assetId', '=', 'asset.id')).as(
|
||||
'exifInfo',
|
||||
);
|
||||
|
||||
export function withExifInner<O>(qb: SelectQueryBuilder<DB, 'asset', O>) {
|
||||
return qb
|
||||
.innerJoin('asset_exif', 'asset.id', 'asset_exif.assetId')
|
||||
.select((eb) => eb.fn.toJson(eb.table('asset_exif')).as('exifInfo'))
|
||||
.select(selectReturnedExifInfo)
|
||||
.$narrowType<{ exifInfo: NotNull }>();
|
||||
}
|
||||
|
||||
@@ -816,7 +821,7 @@ export function searchAssetBuilder(kysely: Kysely<DB>, options: AssetSearchBuild
|
||||
.selectFrom('asset')
|
||||
// postgres eliminates the left join when no exif column is referenced, so unused joins are free
|
||||
.leftJoin('asset_exif', 'asset.id', 'asset_exif.assetId')
|
||||
.$if(!!options.withExif, (qb) => qb.select(selectExifInfo))
|
||||
.$if(!!options.withExif, (qb) => qb.select(selectReturnedExifInfo))
|
||||
.$if(scopeGlobally, (qb) => qb.where(ownershipPredicate))
|
||||
.where((eb) =>
|
||||
eb.or([eb('asset.visibility', '!=', AssetVisibility.Locked), eb('asset.ownerId', '=', scope.lockedOwnerId)]),
|
||||
|
||||
@@ -530,6 +530,112 @@ describe('core plugin', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('assetExifFilter', () => {
|
||||
describe('missing', () => {
|
||||
it('should favorite asset if it has no orientation', async () => {
|
||||
const { user } = await ctx.newUser();
|
||||
const { asset } = await ctx.newAsset({ ownerId: user.id, type: AssetType.Video });
|
||||
await ctx.newExif({ assetId: asset.id, orientation: null });
|
||||
|
||||
const workflow = await createWorkflow({
|
||||
ownerId: user.id,
|
||||
trigger: WorkflowTrigger.AssetMetadataExtraction,
|
||||
steps: [
|
||||
{
|
||||
method: 'immich-plugin-core#assetExifFilter',
|
||||
config: { property: 'orientation', matchType: 'missing' },
|
||||
},
|
||||
{
|
||||
method: 'immich-plugin-core#assetFavorite',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await expect(
|
||||
ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset.id }),
|
||||
).resolves.toBeUndefined();
|
||||
await expect(ctx.get(AssetRepository).getById(asset.id)).resolves.toMatchObject({ isFavorite: true });
|
||||
});
|
||||
|
||||
it('should not favorite asset if it has orientation', async () => {
|
||||
const { user } = await ctx.newUser();
|
||||
const { asset } = await ctx.newAsset({ ownerId: user.id, type: AssetType.Video });
|
||||
await ctx.newExif({ assetId: asset.id, orientation: '2' });
|
||||
|
||||
const workflow = await createWorkflow({
|
||||
ownerId: user.id,
|
||||
trigger: WorkflowTrigger.AssetMetadataExtraction,
|
||||
steps: [
|
||||
{
|
||||
method: 'immich-plugin-core#assetExifFilter',
|
||||
config: { property: 'orientation', matchType: 'missing' },
|
||||
},
|
||||
{
|
||||
method: 'immich-plugin-core#assetFavorite',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await expect(
|
||||
ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset.id }),
|
||||
).resolves.toBeUndefined();
|
||||
await expect(ctx.get(AssetRepository).getById(asset.id)).resolves.toMatchObject({ isFavorite: false });
|
||||
});
|
||||
});
|
||||
|
||||
describe('present', () => {
|
||||
it('should favorite asset if it has orientation', async () => {
|
||||
const { user } = await ctx.newUser();
|
||||
const { asset } = await ctx.newAsset({ ownerId: user.id, type: AssetType.Video });
|
||||
await ctx.newExif({ assetId: asset.id, orientation: '2' });
|
||||
|
||||
const workflow = await createWorkflow({
|
||||
ownerId: user.id,
|
||||
trigger: WorkflowTrigger.AssetMetadataExtraction,
|
||||
steps: [
|
||||
{
|
||||
method: 'immich-plugin-core#assetExifFilter',
|
||||
config: { property: 'orientation', matchType: 'present' },
|
||||
},
|
||||
{
|
||||
method: 'immich-plugin-core#assetFavorite',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await expect(
|
||||
ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset.id }),
|
||||
).resolves.toBeUndefined();
|
||||
await expect(ctx.get(AssetRepository).getById(asset.id)).resolves.toMatchObject({ isFavorite: true });
|
||||
});
|
||||
|
||||
it('should not favorite asset if it has no orientation', async () => {
|
||||
const { user } = await ctx.newUser();
|
||||
const { asset } = await ctx.newAsset({ ownerId: user.id, type: AssetType.Video });
|
||||
await ctx.newExif({ assetId: asset.id, orientation: null });
|
||||
|
||||
const workflow = await createWorkflow({
|
||||
ownerId: user.id,
|
||||
trigger: WorkflowTrigger.AssetMetadataExtraction,
|
||||
steps: [
|
||||
{
|
||||
method: 'immich-plugin-core#assetExifFilter',
|
||||
config: { property: 'orientation', matchType: 'present' },
|
||||
},
|
||||
{
|
||||
method: 'immich-plugin-core#assetFavorite',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await expect(
|
||||
ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset.id }),
|
||||
).resolves.toBeUndefined();
|
||||
await expect(ctx.get(AssetRepository).getById(asset.id)).resolves.toMatchObject({ isFavorite: false });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('assetDateFilter', () => {
|
||||
it('should favorite assets created during the first 7 days of a specific year and month', async () => {
|
||||
const { user } = await ctx.newUser();
|
||||
|
||||
@@ -61,16 +61,14 @@
|
||||
currentMemories.flatMap((memory) => {
|
||||
const asset = memory.assets[frame % memory.assets.length];
|
||||
return asset
|
||||
? [
|
||||
{
|
||||
id: memory.id,
|
||||
title: $memoryLaneTitle(memory),
|
||||
href: Route.viewMemory({ id: memory.id, assetId: asset.id }),
|
||||
alt: $t('memory_lane_title', { values: { title: $getAltText(toTimelineAsset(asset)) } }),
|
||||
src: getAssetMediaUrl({ id: asset.id }),
|
||||
type: memory.type,
|
||||
},
|
||||
]
|
||||
? {
|
||||
id: memory.id,
|
||||
title: $memoryLaneTitle(memory),
|
||||
href: Route.viewMemory({ id: memory.id, assetId: asset.id }),
|
||||
alt: $t('memory_lane_title', { values: { title: $getAltText(toTimelineAsset(asset)) } }),
|
||||
src: getAssetMediaUrl({ id: asset.id }),
|
||||
type: memory.type,
|
||||
}
|
||||
: [];
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -24,5 +24,5 @@ export function updateObject(target: any, source: any): boolean {
|
||||
return updated;
|
||||
}
|
||||
export function isMismatched<T>(option: T | undefined, value: T): boolean {
|
||||
return option === undefined ? false : option !== value;
|
||||
return option !== undefined && option !== value;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ export const getAdminConfigQuery = () =>
|
||||
queryOptions({
|
||||
queryKey: ['config', 'admin'],
|
||||
queryFn: ({ signal }) => getAdminConfig({ signal }),
|
||||
gcTime: Infinity,
|
||||
});
|
||||
|
||||
export const getAdminConfigDefaultsQuery = () =>
|
||||
|
||||
@@ -5,4 +5,5 @@ export const getPublicConfigQuery = () =>
|
||||
queryOptions({
|
||||
queryKey: ['config', 'public'],
|
||||
queryFn: ({ signal }) => getPublicConfig({ signal }),
|
||||
gcTime: Infinity,
|
||||
});
|
||||
|
||||
@@ -23,5 +23,6 @@ export const getUserConfigQuery = () =>
|
||||
queryOptions({
|
||||
queryKey: ['config', 'user'],
|
||||
queryFn: ({ signal }) => getUserConfig({ signal }),
|
||||
gcTime: Infinity,
|
||||
select: withEffectiveFlags,
|
||||
});
|
||||
|
||||
@@ -23,6 +23,7 @@ export const getServerConfigQuery = () =>
|
||||
queryOptions({
|
||||
queryKey: ['server', 'config'],
|
||||
queryFn: ({ signal }) => getServerConfig({ signal }),
|
||||
gcTime: Infinity,
|
||||
});
|
||||
|
||||
export const getServerStatisticsQuery = () =>
|
||||
|
||||
@@ -10,7 +10,7 @@ class SidebarStore {
|
||||
|
||||
/** Toggles the sidebar visibility, if available at the current screen width. */
|
||||
toggle() {
|
||||
this.isOpen = mediaQueryManager.isFullSidebar ? true : !this.isOpen;
|
||||
this.isOpen = mediaQueryManager.isFullSidebar || !this.isOpen;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { authManager } from '#lib/managers/auth-manager.svelte.ts';
|
||||
import { CastDestinationType, CastState, type ICastDestination } from '#lib/managers/cast-manager.svelte.ts';
|
||||
import 'chromecast-caf-sender';
|
||||
import { Duration } from 'luxon';
|
||||
|
||||
const FRAMEWORK_LINK = 'https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1';
|
||||
|
||||
@@ -121,7 +121,6 @@ export const calculateFittedFontSize = (
|
||||
return clamp(fontSize, MIN_FONT_SIZE, MAX_FONT_SIZE);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line tscompat/tscompat -- This assigns the Canvas 2D font property, not a font() method.
|
||||
context.font = getReferenceFont();
|
||||
|
||||
let maxCharWidth = 0;
|
||||
@@ -146,7 +145,6 @@ export const calculateFittedFontSize = (
|
||||
return clamp((1.4 * fitWidth) / text.length, MIN_FONT_SIZE, MAX_FONT_SIZE);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line tscompat/tscompat -- This assigns the Canvas 2D font property, not a font() method.
|
||||
context.font = getReferenceFont();
|
||||
|
||||
const metrics = context.measureText(text);
|
||||
|
||||
@@ -11,7 +11,6 @@ export async function acquireWakeLock() {
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line tscompat/tscompat -- The feature check above guards Wake Lock access.
|
||||
if (sentinel && !sentinel.released) {
|
||||
return;
|
||||
}
|
||||
@@ -20,7 +19,6 @@ export async function acquireWakeLock() {
|
||||
}
|
||||
acquiring = true;
|
||||
try {
|
||||
// eslint-disable-next-line tscompat/tscompat -- Browsers without Wake Lock return before this call.
|
||||
sentinel = await navigator.wakeLock.request('screen');
|
||||
} catch (error) {
|
||||
console.warn('Failed to acquire wake lock:', error);
|
||||
@@ -38,7 +36,6 @@ export async function releaseWakeLock() {
|
||||
// Unset first to avoid race condition after await
|
||||
sentinel = undefined;
|
||||
|
||||
// eslint-disable-next-line tscompat/tscompat -- A sentinel is only acquired on browsers supporting Wake Lock.
|
||||
await toReleaseSentinel.release();
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -226,7 +226,7 @@
|
||||
return { albumId, order: album.order };
|
||||
});
|
||||
|
||||
const isShared = $derived(viewMode === AlbumPageViewMode.SELECT_ASSETS ? false : album.albumUsers.length > 1);
|
||||
const isShared = $derived(viewMode !== AlbumPageViewMode.SELECT_ASSETS && album.albumUsers.length > 1);
|
||||
|
||||
$effect(() => {
|
||||
if (assetViewerManager.isViewing || !isShared) {
|
||||
@@ -248,10 +248,10 @@
|
||||
const isEditor = $derived(isAlbumEditor(album));
|
||||
|
||||
const isSelectionMode = $derived(
|
||||
viewMode === AlbumPageViewMode.SELECT_ASSETS ? true : viewMode === AlbumPageViewMode.SELECT_THUMBNAIL,
|
||||
viewMode === AlbumPageViewMode.SELECT_ASSETS || viewMode === AlbumPageViewMode.SELECT_THUMBNAIL,
|
||||
);
|
||||
const singleSelect = $derived(
|
||||
viewMode === AlbumPageViewMode.SELECT_ASSETS ? false : viewMode === AlbumPageViewMode.SELECT_THUMBNAIL,
|
||||
viewMode !== AlbumPageViewMode.SELECT_ASSETS && viewMode === AlbumPageViewMode.SELECT_THUMBNAIL,
|
||||
);
|
||||
const showArchiveIcon = $derived(viewMode !== AlbumPageViewMode.SELECT_ASSETS);
|
||||
const onSelect = ({ id }: { id: string }) => {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { onDestroy } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
let gCastEnabled = $state(authManager.authenticated ? authManager.preferences.cast.gCastEnabled : false);
|
||||
let gCastEnabled = $state(authManager.authenticated && authManager.preferences.cast.gCastEnabled);
|
||||
|
||||
onDestroy(async () => {
|
||||
try {
|
||||
|
||||
+2
-3
@@ -5,14 +5,13 @@
|
||||
"paths": {
|
||||
"#i18n/*": ["../i18n/*"],
|
||||
"#test-data/*": ["./src/test-data/*"],
|
||||
"#tests/*": ["./src/../tests/*"],
|
||||
"chromecast-caf-sender": ["./node_modules/@types/chromecast-caf-sender/index.d.ts"]
|
||||
"#tests/*": ["./src/../tests/*"]
|
||||
},
|
||||
"rootDir": ".",
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"target": "es2022",
|
||||
"types": ["$app/types", "vitest/globals"]
|
||||
"types": ["$app/types", "vitest/globals", "chromecast-caf-sender"]
|
||||
},
|
||||
"exclude": ["vite.config.ts", "src/service-worker/*"],
|
||||
"extends": "$app/tsconfig",
|
||||
|
||||
Reference in New Issue
Block a user