Merge tag 'v3.3.3' into codex/immich-release-20261014
# Conflicts: # mobile/pubspec.yaml
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich-e2e",
|
||||
"version": "3.3.1",
|
||||
"version": "3.3.3",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "immich-ml"
|
||||
version = "3.3.1"
|
||||
version = "3.3.3"
|
||||
description = ""
|
||||
authors = [{ name = "Hau Tran", email = "alex.tran1502@gmail.com" }]
|
||||
requires-python = ">=3.12,<4.0"
|
||||
|
||||
Generated
+1
-1
@@ -676,7 +676,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "immich-ml"
|
||||
version = "3.3.1"
|
||||
version = "3.3.3"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "fastapi" },
|
||||
|
||||
@@ -22,8 +22,8 @@ platform :android do
|
||||
task: 'bundle',
|
||||
build_type: 'Release',
|
||||
properties: {
|
||||
"android.injected.version.code" => 3030199,
|
||||
"android.injected.version.name" => "3.3.1",
|
||||
"android.injected.version.code" => 3030399,
|
||||
"android.injected.version.name" => "3.3.3",
|
||||
}
|
||||
)
|
||||
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab', track: 'beta')
|
||||
@@ -35,8 +35,8 @@ platform :android do
|
||||
task: 'bundle',
|
||||
build_type: 'Release',
|
||||
properties: {
|
||||
"android.injected.version.code" => 3030199,
|
||||
"android.injected.version.name" => "3.3.1",
|
||||
"android.injected.version.code" => 3030399,
|
||||
"android.injected.version.name" => "3.3.3",
|
||||
}
|
||||
)
|
||||
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>3.3.1</string>
|
||||
<string>3.3.3</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/memory.model.dart';
|
||||
import 'package:immich_mobile/extensions/datetime_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/object_extensions.dart';
|
||||
import 'package:immich_mobile/generated/translations.g.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
@@ -10,7 +11,7 @@ String getMemoryTitle(Translations t, Memory memory, {RemoteAsset? asset, bool p
|
||||
switch (memory.type) {
|
||||
MemoryTypeEnum.onThisDay =>
|
||||
preferDate
|
||||
? DateFormat.yMMMMd().format(memory.memoryAt)
|
||||
? DateFormat.yMMMMd(resolvedDateTimeLocale()).format(memory.memoryAt)
|
||||
: t.years_ago(years: DateTime.now().year - memory.data.year),
|
||||
MemoryTypeEnum.birthday => _getBirthdayTitle(t, memory.data, asset),
|
||||
};
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ name: immich_mobile
|
||||
description: Immich - selfhosted backup media file on mobile phone
|
||||
|
||||
publish_to: 'none'
|
||||
version: 3.3.1-local.3+3030201
|
||||
version: 3.3.3-local.1+3030400
|
||||
|
||||
environment:
|
||||
sdk: '>=3.12.0 <4.0.0'
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/domain/models/memory.model.dart';
|
||||
import 'package:immich_mobile/presentation/pages/memory_list.page.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/memory.provider.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../factories/remote_asset_factory.dart';
|
||||
import 'presentation_context.dart';
|
||||
@@ -44,6 +45,15 @@ void main() {
|
||||
expect(find.textContaining('ago'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('shows the date when intl has no date formats for the app language', (tester) async {
|
||||
Intl.defaultLocale = 'kab';
|
||||
addTearDown(() => Intl.defaultLocale = null);
|
||||
await pumpPage(tester, [newMemory(type: MemoryTypeEnum.onThisDay, memoryAt: DateTime(2025, 10, 7))]);
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('October 7, 2025'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('shows the person name for a birthday memory', (tester) async {
|
||||
await pumpPage(tester, [
|
||||
newMemory(type: MemoryTypeEnum.birthday, memoryAt: DateTime(2025, 10, 7), personName: 'Alice'),
|
||||
|
||||
@@ -18211,7 +18211,7 @@
|
||||
"info": {
|
||||
"title": "Immich",
|
||||
"description": "Immich API",
|
||||
"version": "3.3.1",
|
||||
"version": "3.3.3",
|
||||
"contact": {}
|
||||
},
|
||||
"tags": [
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich-monorepo",
|
||||
"version": "3.3.1",
|
||||
"version": "3.3.3",
|
||||
"description": "Monorepo for Immich",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@immich/cli",
|
||||
"version": "3.3.1",
|
||||
"version": "3.3.3",
|
||||
"description": "Command Line Interface (CLI) for Immich",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@immich/sdk",
|
||||
"version": "3.3.1",
|
||||
"version": "3.3.3",
|
||||
"description": "Auto-generated TypeScript SDK for the Immich API",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Immich
|
||||
* 3.3.1
|
||||
* 3.3.3
|
||||
* DO NOT MODIFY - This file has been generated using oazapfts.
|
||||
* See https://www.npmjs.com/package/oazapfts
|
||||
*/
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich",
|
||||
"version": "3.3.1",
|
||||
"version": "3.3.3",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
|
||||
@@ -239,38 +239,52 @@ from
|
||||
"person_group"
|
||||
inner join "person" as "owned" on "owned"."personGroupId" = "person_group"."id"
|
||||
and "owned"."ownerId" = $5
|
||||
inner join "asset_face" on "asset_face"."personGroupId" = "person_group"."id"
|
||||
and "asset_face"."deletedAt" is null
|
||||
and "asset_face"."isVisible" is true
|
||||
inner join "asset" on "asset"."id" = "asset_face"."assetId"
|
||||
and "asset"."visibility" = 'timeline'
|
||||
and "asset"."deletedAt" is null
|
||||
and (
|
||||
"asset"."ownerId" = any ($6::uuid[])
|
||||
or exists (
|
||||
select
|
||||
1 as "exists"
|
||||
from
|
||||
"album_asset"
|
||||
inner join "album" on "album"."id" = "album_asset"."albumId"
|
||||
and "album"."deletedAt" is null
|
||||
inner join "album_user" on "album_user"."albumId" = "album"."id"
|
||||
and "album_user"."userId" = $7::uuid
|
||||
where
|
||||
"album_asset"."assetId" = "asset"."id"
|
||||
)
|
||||
)
|
||||
inner join (
|
||||
select
|
||||
"asset_face"."personGroupId",
|
||||
count("asset"."id") as "faceCount"
|
||||
from
|
||||
"asset_face"
|
||||
inner join "asset" on "asset"."id" = "asset_face"."assetId"
|
||||
and "asset"."visibility" = 'timeline'
|
||||
and "asset"."deletedAt" is null
|
||||
and (
|
||||
"asset"."ownerId" = any ($6::uuid[])
|
||||
or exists (
|
||||
select
|
||||
1 as "exists"
|
||||
from
|
||||
"album_asset"
|
||||
inner join "album" on "album"."id" = "album_asset"."albumId"
|
||||
and "album"."deletedAt" is null
|
||||
inner join "album_user" on "album_user"."albumId" = "album"."id"
|
||||
and "album_user"."userId" = $7::uuid
|
||||
where
|
||||
"album_asset"."assetId" = "asset"."id"
|
||||
)
|
||||
)
|
||||
where
|
||||
"asset_face"."personGroupId" = any (
|
||||
array(
|
||||
(
|
||||
select
|
||||
"person"."personGroupId"
|
||||
from
|
||||
"person"
|
||||
where
|
||||
"person"."ownerId" = $8
|
||||
)
|
||||
)
|
||||
)
|
||||
and "asset_face"."deletedAt" is null
|
||||
and "asset_face"."isVisible" is true
|
||||
group by
|
||||
"asset_face"."personGroupId"
|
||||
) as "faces" on "faces"."personGroupId" = "person_group"."id"
|
||||
where
|
||||
"owned"."isHidden" = $8
|
||||
and 1 = 1
|
||||
group by
|
||||
"person_group"."id",
|
||||
"owned"."ownerId",
|
||||
"owned"."personGroupId"
|
||||
having
|
||||
(
|
||||
"owned"."name" != $9
|
||||
or count("asset"."id") >= COALESCE(
|
||||
or "faces"."faceCount" >= COALESCE(
|
||||
(
|
||||
SELECT
|
||||
value -> 'people' ->> 'minimumFaces'
|
||||
@@ -283,17 +297,19 @@ having
|
||||
'3'
|
||||
)::int
|
||||
)
|
||||
and "owned"."isHidden" = $11
|
||||
and 1 = 1
|
||||
order by
|
||||
"owned"."isHidden" asc,
|
||||
"owned"."isFavorite" desc,
|
||||
NULLIF("owned"."name", '') is null asc,
|
||||
count("asset"."id") desc,
|
||||
"faces"."faceCount" desc,
|
||||
NULLIF("owned"."name", '') asc nulls last,
|
||||
"owned"."createdAt"
|
||||
limit
|
||||
$11
|
||||
offset
|
||||
$12
|
||||
offset
|
||||
$13
|
||||
|
||||
-- PersonRepository.getAllWithoutFaces
|
||||
select
|
||||
@@ -901,49 +917,65 @@ from
|
||||
"person_group"
|
||||
inner join "person" as "owned" on "owned"."personGroupId" = "person_group"."id"
|
||||
and "owned"."ownerId" = $2
|
||||
inner join "asset_face" on "asset_face"."personGroupId" = "person_group"."id"
|
||||
and "asset_face"."deletedAt" is null
|
||||
and "asset_face"."isVisible" is true
|
||||
inner join "asset" on "asset"."id" = "asset_face"."assetId"
|
||||
and "asset"."visibility" = 'timeline'
|
||||
and "asset"."deletedAt" is null
|
||||
and (
|
||||
"asset"."ownerId" = any ($3::uuid[])
|
||||
or exists (
|
||||
select
|
||||
1 as "exists"
|
||||
from
|
||||
"album_asset"
|
||||
inner join "album" on "album"."id" = "album_asset"."albumId"
|
||||
and "album"."deletedAt" is null
|
||||
inner join "album_user" on "album_user"."albumId" = "album"."id"
|
||||
and "album_user"."userId" = $4::uuid
|
||||
where
|
||||
"album_asset"."assetId" = "asset"."id"
|
||||
)
|
||||
)
|
||||
inner join (
|
||||
select
|
||||
"asset_face"."personGroupId",
|
||||
count("asset"."id") as "faceCount"
|
||||
from
|
||||
"asset_face"
|
||||
inner join "asset" on "asset"."id" = "asset_face"."assetId"
|
||||
and "asset"."visibility" = 'timeline'
|
||||
and "asset"."deletedAt" is null
|
||||
and (
|
||||
"asset"."ownerId" = any ($3::uuid[])
|
||||
or exists (
|
||||
select
|
||||
1 as "exists"
|
||||
from
|
||||
"album_asset"
|
||||
inner join "album" on "album"."id" = "album_asset"."albumId"
|
||||
and "album"."deletedAt" is null
|
||||
inner join "album_user" on "album_user"."albumId" = "album"."id"
|
||||
and "album_user"."userId" = $4::uuid
|
||||
where
|
||||
"album_asset"."assetId" = "asset"."id"
|
||||
)
|
||||
)
|
||||
where
|
||||
"asset_face"."personGroupId" = any (
|
||||
array(
|
||||
(
|
||||
select
|
||||
"person"."personGroupId"
|
||||
from
|
||||
"person"
|
||||
where
|
||||
"person"."ownerId" = $5
|
||||
)
|
||||
)
|
||||
)
|
||||
and "asset_face"."deletedAt" is null
|
||||
and "asset_face"."isVisible" is true
|
||||
group by
|
||||
"asset_face"."personGroupId"
|
||||
) as "faces" on "faces"."personGroupId" = "person_group"."id"
|
||||
where
|
||||
1 = 1
|
||||
group by
|
||||
"person_group"."id",
|
||||
"owned"."ownerId",
|
||||
"owned"."personGroupId"
|
||||
having
|
||||
(
|
||||
"owned"."name" != $5
|
||||
or count("asset"."id") >= COALESCE(
|
||||
"owned"."name" != $6
|
||||
or "faces"."faceCount" >= COALESCE(
|
||||
(
|
||||
SELECT
|
||||
value -> 'people' ->> 'minimumFaces'
|
||||
FROM
|
||||
user_metadata
|
||||
WHERE
|
||||
"userId" = $6
|
||||
"userId" = $7
|
||||
AND key = 'preferences'
|
||||
),
|
||||
'3'
|
||||
)::int
|
||||
)
|
||||
and 1 = 1
|
||||
) as "people"
|
||||
|
||||
-- PersonRepository.createGroup
|
||||
|
||||
@@ -224,8 +224,6 @@ const asPerson = ({ ownedPerson, otherPeople, sharedBy, sharedWith }: PersonGrou
|
||||
sharedWith,
|
||||
});
|
||||
|
||||
const faceCount = (eb: ExpressionBuilder<DB, 'asset'>) => eb.fn.count('asset.id');
|
||||
|
||||
const withPerson = ({ viewingUserId }: WithPersonOptions) => {
|
||||
return (eb: ExpressionBuilder<DB, 'asset_face'>) =>
|
||||
jsonObjectFrom(
|
||||
@@ -448,25 +446,40 @@ export class PersonRepository {
|
||||
.innerJoin('person as owned', (join) =>
|
||||
join.onRef('owned.personGroupId', '=', 'person_group.id').on('owned.ownerId', '=', userId),
|
||||
)
|
||||
.innerJoin('asset_face', (join) =>
|
||||
join
|
||||
.onRef('asset_face.personGroupId', '=', 'person_group.id')
|
||||
.on('asset_face.deletedAt', 'is', null)
|
||||
.on('asset_face.isVisible', 'is', true),
|
||||
.innerJoin(
|
||||
(eb) =>
|
||||
eb
|
||||
.selectFrom('asset_face')
|
||||
.innerJoin('asset', (join) =>
|
||||
join
|
||||
.onRef('asset.id', '=', 'asset_face.assetId')
|
||||
.on('asset.visibility', '=', sql.lit(AssetVisibility.Timeline))
|
||||
.on('asset.deletedAt', 'is', null)
|
||||
.on((eb) =>
|
||||
eb.or([eb('asset.ownerId', '=', anyUuid([userId, ...partnerIds])), inSharedAlbum(eb, userId)]),
|
||||
),
|
||||
)
|
||||
.select('asset_face.personGroupId')
|
||||
.select((eb) => eb.fn.count<number>('asset.id').as('faceCount'))
|
||||
// an array instead of a join or `in`, which the planner may turn into a scan of every face in the instance
|
||||
.where('asset_face.personGroupId', '=', (eb) => {
|
||||
const ownedGroupIds = eb
|
||||
.selectFrom('person')
|
||||
.select('person.personGroupId')
|
||||
.where('person.ownerId', '=', userId);
|
||||
return eb.fn.any(eb.fn<string[]>('array', [ownedGroupIds]));
|
||||
})
|
||||
.where('asset_face.deletedAt', 'is', null)
|
||||
.where('asset_face.isVisible', 'is', true)
|
||||
.groupBy('asset_face.personGroupId')
|
||||
.as('faces'),
|
||||
(join) => join.onRef('faces.personGroupId', '=', 'person_group.id'),
|
||||
)
|
||||
.innerJoin('asset', (join) =>
|
||||
join
|
||||
.onRef('asset.id', '=', 'asset_face.assetId')
|
||||
.on('asset.visibility', '=', sql.lit(AssetVisibility.Timeline))
|
||||
.on('asset.deletedAt', 'is', null)
|
||||
.on((eb) => eb.or([eb('asset.ownerId', '=', anyUuid([userId, ...partnerIds])), inSharedAlbum(eb, userId)])),
|
||||
)
|
||||
.groupBy(['person_group.id', 'owned.ownerId', 'owned.personGroupId'])
|
||||
.having((eb) =>
|
||||
.where((eb) =>
|
||||
eb.or([
|
||||
eb('owned.name', '!=', ''),
|
||||
eb(
|
||||
faceCount,
|
||||
'faces.faceCount',
|
||||
'>=',
|
||||
sql<number>`COALESCE(
|
||||
(SELECT value -> 'people' ->> 'minimumFaces'
|
||||
@@ -509,7 +522,7 @@ export class PersonRepository {
|
||||
.$if(!options.closestFaceAssetId, (qb) =>
|
||||
qb
|
||||
.orderBy(sql`NULLIF("owned"."name", '') is null`, 'asc')
|
||||
.orderBy(faceCount, 'desc')
|
||||
.orderBy('faces.faceCount', 'desc')
|
||||
.orderBy(sql`NULLIF("owned"."name", '')`, (om) => om.asc().nullsLast())
|
||||
.orderBy('owned.createdAt'),
|
||||
)
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich-web",
|
||||
"version": "3.3.1",
|
||||
"version": "3.3.3",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
Reference in New Issue
Block a user