Compare commits

...

15 Commits

Author SHA1 Message Date
Alex The Bot
55fa3234fd Version v1.88.2 2023-11-21 16:09:18 +00:00
Alex
f094ff2aa1 fix(server): album perf query (#5232)
* Revert "fix: album performances (#5224)"

This reverts commit c438e17954.

* Revert "fix: album sorting options (#5127)"

This reverts commit 725f30c494.
2023-11-21 10:07:49 -06:00
Alex Tran
a13052e24c Revert "Version v1.88.2"
This reverts commit bc2c73e499.
2023-11-21 09:07:50 -06:00
Alex The Bot
bc2c73e499 Version v1.88.2 2023-11-21 15:01:06 +00:00
bo0tzz
bcb885422a feat(docs): Add no-ssr milestone (#5220)
* Add no-ssr milestone

* chore: formatting

* feat: activity

---------

Co-authored-by: martabal <74269598+martabal@users.noreply.github.com>
2023-11-21 08:50:56 -06:00
martin
c438e17954 fix: album performances (#5224)
* fix: album performances

* fix: tests
2023-11-21 08:49:53 -06:00
dependabot[bot]
c46e82561e chore(deps): bump actions/checkout from 2 to 4 (#5219)
Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-21 09:37:28 -05:00
Jason Rasmussen
5c0821330f chore(deps): cli grouping (#5193) 2023-11-21 11:44:59 +01:00
Alex Tran
69e0db56b3 Merge branch 'main' of github.com:immich-app/immich 2023-11-20 21:20:51 -06:00
Alex Tran
e8bf498236 fix(web): non-null error 2023-11-20 21:19:37 -06:00
Alex The Bot
9cf40afaf0 Version v1.88.1 2023-11-21 03:00:19 +00:00
Alex
28a15365d6 fix(web): search returns all value (#5210) 2023-11-20 20:58:22 -06:00
Alex
d49b353c49 fix(web): Fix year label calculation (#5211)
* fix(web): revert, fix year label overlapse

* not too bold

* fix
2023-11-20 20:58:05 -06:00
Jason Rasmussen
8b966a0f15 fix(server): date time calculation (#5204) 2023-11-20 17:26:53 -05:00
Alex
30e9763888 chore: post release tasks 2023-11-20 15:11:56 -06:00
42 changed files with 175 additions and 221 deletions

View File

@@ -10,7 +10,7 @@ jobs:
working-directory: ./cli
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
# Setup .npmrc file to publish to npm
- uses: actions/setup-node@v4
with:

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.88.0
* The version of the OpenAPI document: 1.88.2
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.88.0
* The version of the OpenAPI document: 1.88.2
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.88.0
* The version of the OpenAPI document: 1.88.2
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.88.0
* The version of the OpenAPI document: 1.88.2
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.88.0
* The version of the OpenAPI document: 1.88.2
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -16,6 +16,7 @@ import {
mdiFile,
mdiFileSearch,
mdiFolder,
mdiForum,
mdiHeart,
mdiImage,
mdiImageAlbum,
@@ -42,6 +43,7 @@ import {
mdiText,
mdiThemeLightDark,
mdiTrashCanOutline,
mdiVectorCombine,
mdiVideo,
mdiWeb,
} from '@mdi/js';
@@ -50,6 +52,16 @@ import React from 'react';
import Timeline, { DateType, Item } from '../components/timeline';
const items: Item[] = [
{
icon: mdiVectorCombine,
description:
'The serving of the web app is merged into the server image, allowing us to remove two containers from the stack.',
title: 'Container consolidation',
release: 'v1.88.0',
tag: 'v1.88.0',
date: new Date(2023, 10, 20),
dateType: DateType.RELEASE,
},
{
icon: mdiBash,
description: 'Version 2 of the Immich CLI is released, replacing the legacy v1 CLI.',
@@ -59,6 +71,15 @@ const items: Item[] = [
date: new Date(2023, 10, 19),
dateType: DateType.RELEASE,
},
{
icon: mdiForum,
description: 'Comment a photo or a video in a shared album',
title: 'Activity',
release: 'v1.84.0',
tag: 'v1.84.0',
date: new Date(2023, 10, 1),
dateType: DateType.RELEASE,
},
{
icon: mdiStar,
description: 'Reach 20K Stars on GitHub!',

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "machine-learning"
version = "1.88.0"
version = "1.88.2"
description = ""
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
readme = "README.md"

View File

@@ -36,7 +36,7 @@ platform :android do
build_type: 'Release',
properties: {
"android.injected.version.code" => 112,
"android.injected.version.name" => "1.88.0",
"android.injected.version.name" => "1.88.2",
}
)
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')

View File

@@ -5,17 +5,17 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000219">
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000263">
</testcase>
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="67.071569">
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="80.37488">
</testcase>
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="29.991184">
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="25.830358">
</testcase>

View File

@@ -379,7 +379,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 127;
CURRENT_PROJECT_VERSION = 128;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -515,7 +515,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 127;
CURRENT_PROJECT_VERSION = 128;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -543,7 +543,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 127;
CURRENT_PROJECT_VERSION = 128;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;

View File

@@ -54,11 +54,11 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.87.0</string>
<string>1.88.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>127</string>
<string>128</string>
<key>FLTEnableImpeller</key>
<true />
<key>ITSAppUsesNonExemptEncryption</key>

View File

@@ -19,7 +19,7 @@ platform :ios do
desc "iOS Beta"
lane :beta do
increment_version_number(
version_number: "1.88.0"
version_number: "1.88.2"
)
increment_build_number(
build_number: latest_testflight_build_number + 1,

View File

@@ -5,32 +5,32 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000245">
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000267">
</testcase>
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.162192">
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.193021">
</testcase>
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="5.082136">
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="5.987435">
</testcase>
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.181105">
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.181886">
</testcase>
<testcase classname="fastlane.lanes" name="4: build_app" time="99.633247">
<testcase classname="fastlane.lanes" name="4: build_app" time="105.510332">
</testcase>
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="62.690406">
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="65.714015">
</testcase>

View File

@@ -3,7 +3,7 @@ Immich API
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
- API version: 1.88.0
- API version: 1.88.2
- Build package: org.openapitools.codegen.languages.DartClientCodegen
## Requirements

View File

@@ -2,7 +2,7 @@ name: immich_mobile
description: Immich - selfhosted backup media file on mobile phone
publish_to: "none"
version: 1.88.0+112
version: 1.88.2+112
isar_version: &isar_version 3.1.0+1
environment:

View File

@@ -2,6 +2,11 @@
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:base"],
"packageRules": [
{
"matchFileNames": ["cli/**"],
"groupName": "@immich/cli",
"matchUpdateTypes": ["minor", "patch"]
},
{
"matchFileNames": ["mobile/**"],
"groupName": "mobile",

View File

@@ -6052,7 +6052,7 @@
"info": {
"title": "Immich",
"description": "Immich API",
"version": "1.88.0",
"version": "1.88.2",
"contact": {}
},
"tags": [],

View File

@@ -1,12 +1,12 @@
{
"name": "immich",
"version": "1.88.0",
"version": "1.88.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "immich",
"version": "1.88.0",
"version": "1.88.2",
"license": "UNLICENSED",
"dependencies": {
"@babel/runtime": "^7.22.11",

View File

@@ -1,6 +1,6 @@
{
"name": "immich",
"version": "1.88.0",
"version": "1.88.2",
"description": "",
"author": "",
"private": true,

View File

@@ -37,6 +37,15 @@ export const mapAlbum = (entity: AlbumEntity, withAssets: boolean): AlbumRespons
const hasSharedLink = entity.sharedLinks?.length > 0;
const hasSharedUser = sharedUsers.length > 0;
let startDate = assets.at(0)?.fileCreatedAt || undefined;
let endDate = assets.at(-1)?.fileCreatedAt || undefined;
// Swap dates if start date is greater than end date.
if (startDate && endDate && startDate > endDate) {
const temp = startDate;
startDate = endDate;
endDate = temp;
}
return {
albumName: entity.albumName,
description: entity.description,
@@ -49,10 +58,10 @@ export const mapAlbum = (entity: AlbumEntity, withAssets: boolean): AlbumRespons
sharedUsers,
shared: hasSharedUser || hasSharedLink,
hasSharedLink,
startDate: entity.startDate ? entity.startDate : undefined,
endDate: entity.endDate ? entity.endDate : undefined,
startDate,
endDate,
assets: (withAssets ? assets : []).map((asset) => mapAsset(asset)),
assetCount: entity.assetCount,
assetCount: entity.assets?.length || 0,
isActivityEnabled: entity.isActivityEnabled,
};
};

View File

@@ -58,6 +58,10 @@ describe(AlbumService.name, () => {
describe('getAll', () => {
it('gets list of albums for auth user', async () => {
albumMock.getOwned.mockResolvedValue([albumStub.empty, albumStub.sharedWithUser]);
albumMock.getAssetCountForIds.mockResolvedValue([
{ albumId: albumStub.empty.id, assetCount: 0 },
{ albumId: albumStub.sharedWithUser.id, assetCount: 0 },
]);
albumMock.getInvalidThumbnail.mockResolvedValue([]);
const result = await sut.getAll(authStub.admin, {});
@@ -68,6 +72,7 @@ describe(AlbumService.name, () => {
it('gets list of albums that have a specific asset', async () => {
albumMock.getByAssetId.mockResolvedValue([albumStub.oneAsset]);
albumMock.getAssetCountForIds.mockResolvedValue([{ albumId: albumStub.oneAsset.id, assetCount: 1 }]);
albumMock.getInvalidThumbnail.mockResolvedValue([]);
const result = await sut.getAll(authStub.admin, { assetId: albumStub.oneAsset.id });
@@ -78,6 +83,7 @@ describe(AlbumService.name, () => {
it('gets list of albums that are shared', async () => {
albumMock.getShared.mockResolvedValue([albumStub.sharedWithUser]);
albumMock.getAssetCountForIds.mockResolvedValue([{ albumId: albumStub.sharedWithUser.id, assetCount: 0 }]);
albumMock.getInvalidThumbnail.mockResolvedValue([]);
const result = await sut.getAll(authStub.admin, { shared: true });
@@ -88,6 +94,7 @@ describe(AlbumService.name, () => {
it('gets list of albums that are NOT shared', async () => {
albumMock.getNotShared.mockResolvedValue([albumStub.empty]);
albumMock.getAssetCountForIds.mockResolvedValue([{ albumId: albumStub.empty.id, assetCount: 0 }]);
albumMock.getInvalidThumbnail.mockResolvedValue([]);
const result = await sut.getAll(authStub.admin, { shared: false });
@@ -99,6 +106,7 @@ describe(AlbumService.name, () => {
it('counts assets correctly', async () => {
albumMock.getOwned.mockResolvedValue([albumStub.oneAsset]);
albumMock.getAssetCountForIds.mockResolvedValue([{ albumId: albumStub.oneAsset.id, assetCount: 1 }]);
albumMock.getInvalidThumbnail.mockResolvedValue([]);
const result = await sut.getAll(authStub.admin, {});
@@ -110,6 +118,9 @@ describe(AlbumService.name, () => {
it('updates the album thumbnail by listing all albums', async () => {
albumMock.getOwned.mockResolvedValue([albumStub.oneAssetInvalidThumbnail]);
albumMock.getAssetCountForIds.mockResolvedValue([
{ albumId: albumStub.oneAssetInvalidThumbnail.id, assetCount: 1 },
]);
albumMock.getInvalidThumbnail.mockResolvedValue([albumStub.oneAssetInvalidThumbnail.id]);
albumMock.update.mockResolvedValue(albumStub.oneAssetValidThumbnail);
assetMock.getFirstAssetForAlbumId.mockResolvedValue(albumStub.oneAssetInvalidThumbnail.assets[0]);
@@ -123,6 +134,9 @@ describe(AlbumService.name, () => {
it('removes the thumbnail for an empty album', async () => {
albumMock.getOwned.mockResolvedValue([albumStub.emptyWithInvalidThumbnail]);
albumMock.getAssetCountForIds.mockResolvedValue([
{ albumId: albumStub.emptyWithInvalidThumbnail.id, assetCount: 1 },
]);
albumMock.getInvalidThumbnail.mockResolvedValue([albumStub.emptyWithInvalidThumbnail.id]);
albumMock.update.mockResolvedValue(albumStub.emptyWithValidThumbnail);
assetMock.getFirstAssetForAlbumId.mockResolvedValue(null);

View File

@@ -66,12 +66,21 @@ export class AlbumService {
albums = await this.albumRepository.getOwned(ownerId);
}
// Get asset count for each album. Then map the result to an object:
// { [albumId]: assetCount }
const albumsAssetCount = await this.albumRepository.getAssetCountForIds(albums.map((album) => album.id));
const albumsAssetCountObj = albumsAssetCount.reduce((obj: Record<string, number>, { albumId, assetCount }) => {
obj[albumId] = assetCount;
return obj;
}, {});
return Promise.all(
albums.map(async (album) => {
const lastModifiedAsset = await this.assetRepository.getLastUpdatedAssetForAlbumId(album.id);
return {
...mapAlbumWithoutAssets(album),
sharedLinks: undefined,
assetCount: albumsAssetCountObj[album.id],
lastModifiedAssetTimestamp: lastModifiedAsset?.fileModifiedAt,
};
}),

View File

@@ -39,7 +39,7 @@ interface DirectoryEntry {
type ExifEntityWithoutGeocodeAndTypeOrm = Omit<
ExifEntity,
'city' | 'state' | 'country' | 'description' | 'exifTextSearchableColumn'
>;
> & { dateTimeOriginal: Date };
const exifDate = (dt: ExifDateTime | string | undefined) => (dt instanceof ExifDateTime ? dt?.toDate() : null);
const tzOffset = (dt: ExifDateTime | string | undefined) => (dt instanceof ExifDateTime ? dt?.tzoffsetMinutes : null);
@@ -181,7 +181,7 @@ export class MetadataService {
await this.applyReverseGeocoding(asset, exifData);
await this.assetRepository.upsertExif(exifData);
const dateTimeOriginal = exifDate(firstDateTime(tags as Tags)) ?? exifData.dateTimeOriginal;
const dateTimeOriginal = exifData.dateTimeOriginal;
let localDateTime = dateTimeOriginal ?? undefined;
const timeZoneOffset = tzOffset(firstDateTime(tags as Tags)) ?? 0;

View File

@@ -30,6 +30,7 @@ export interface IAlbumRepository {
hasAsset(asset: AlbumAsset): Promise<boolean>;
removeAsset(assetId: string): Promise<void>;
removeAssets(assets: AlbumAssets): Promise<void>;
getAssetCountForIds(ids: string[]): Promise<AlbumAssetCount[]>;
getInvalidThumbnail(): Promise<string[]>;
getOwned(ownerId: string): Promise<AlbumEntity[]>;
getShared(ownerId: string): Promise<AlbumEntity[]>;

View File

@@ -40,7 +40,7 @@ export function mapSharedLink(sharedLink: SharedLinkEntity): SharedLinkResponseD
createdAt: sharedLink.createdAt,
expiresAt: sharedLink.expiresAt,
assets: assets.map((asset) => mapAsset(asset)),
album: sharedLink.album?.id ? mapAlbumWithoutAssets(sharedLink.album) : undefined,
album: sharedLink.album ? mapAlbumWithoutAssets(sharedLink.album) : undefined,
allowUpload: sharedLink.allowUpload,
allowDownload: sharedLink.allowDownload,
showMetadata: sharedLink.showExif,

View File

@@ -9,7 +9,6 @@ import {
OneToMany,
PrimaryGeneratedColumn,
UpdateDateColumn,
VirtualColumn,
} from 'typeorm';
import { AssetEntity } from './asset.entity';
import { SharedLinkEntity } from './shared-link.entity';
@@ -60,34 +59,4 @@ export class AlbumEntity {
@Column({ default: true })
isActivityEnabled!: boolean;
@VirtualColumn({
query: (alias) => `
SELECT MIN(assets."fileCreatedAt")
FROM "assets" assets
JOIN "albums_assets_assets" aa ON aa."assetsId" = assets.id
WHERE aa."albumsId" = ${alias}.id
`,
})
startDate!: Date | null;
@VirtualColumn({
query: (alias) => `
SELECT MAX(assets."fileCreatedAt")
FROM "assets" assets
JOIN "albums_assets_assets" aa ON aa."assetsId" = assets.id
WHERE aa."albumsId" = ${alias}.id
`,
})
endDate!: Date | null;
@VirtualColumn({
query: (alias) => `
SELECT COUNT(assets."id")
FROM "assets" assets
JOIN "albums_assets_assets" aa ON aa."assetsId" = assets.id
WHERE aa."albumsId" = ${alias}.id
`,
})
assetCount!: number;
}

View File

@@ -1,4 +1,4 @@
import { AlbumAsset, AlbumAssets, AlbumInfoOptions, IAlbumRepository } from '@app/domain';
import { AlbumAsset, AlbumAssetCount, AlbumAssets, AlbumInfoOptions, IAlbumRepository } from '@app/domain';
import { Injectable } from '@nestjs/common';
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { DataSource, FindOptionsOrder, FindOptionsRelations, In, IsNull, Not, Repository } from 'typeorm';
@@ -59,6 +59,28 @@ export class AlbumRepository implements IAlbumRepository {
});
}
async getAssetCountForIds(ids: string[]): Promise<AlbumAssetCount[]> {
// Guard against running invalid query when ids list is empty.
if (!ids.length) {
return [];
}
// Only possible with query builder because of GROUP BY.
const countByAlbums = await this.repository
.createQueryBuilder('album')
.select('album.id')
.addSelect('COUNT(albums_assets.assetsId)', 'asset_count')
.leftJoin('albums_assets_assets', 'albums_assets', 'albums_assets.albumsId = album.id')
.where('album.id IN (:...ids)', { ids })
.groupBy('album.id')
.getRawMany();
return countByAlbums.map<AlbumAssetCount>((albumCount) => ({
albumId: albumCount['album_id'],
assetCount: Number(albumCount['asset_count']),
}));
}
/**
* Returns the album IDs that have an invalid thumbnail, when:
* - Thumbnail references an asset outside the album

View File

@@ -19,9 +19,6 @@ export const albumStub = {
sharedLinks: [],
sharedUsers: [],
isActivityEnabled: true,
startDate: null,
endDate: null,
assetCount: 0,
}),
sharedWithUser: Object.freeze<AlbumEntity>({
id: 'album-2',
@@ -38,9 +35,6 @@ export const albumStub = {
sharedLinks: [],
sharedUsers: [userStub.user1],
isActivityEnabled: true,
startDate: null,
endDate: null,
assetCount: 0,
}),
sharedWithMultiple: Object.freeze<AlbumEntity>({
id: 'album-3',
@@ -57,9 +51,6 @@ export const albumStub = {
sharedLinks: [],
sharedUsers: [userStub.user1, userStub.user2],
isActivityEnabled: true,
startDate: null,
endDate: null,
assetCount: 0,
}),
sharedWithAdmin: Object.freeze<AlbumEntity>({
id: 'album-3',
@@ -76,9 +67,6 @@ export const albumStub = {
sharedLinks: [],
sharedUsers: [userStub.admin],
isActivityEnabled: true,
startDate: null,
endDate: null,
assetCount: 0,
}),
oneAsset: Object.freeze<AlbumEntity>({
id: 'album-4',
@@ -95,9 +83,6 @@ export const albumStub = {
sharedLinks: [],
sharedUsers: [],
isActivityEnabled: true,
startDate: assetStub.image.fileCreatedAt,
endDate: assetStub.image.fileCreatedAt,
assetCount: 1,
}),
twoAssets: Object.freeze<AlbumEntity>({
id: 'album-4a',
@@ -114,9 +99,6 @@ export const albumStub = {
sharedLinks: [],
sharedUsers: [],
isActivityEnabled: true,
startDate: assetStub.withLocation.fileCreatedAt,
endDate: assetStub.image.fileCreatedAt,
assetCount: 2,
}),
emptyWithInvalidThumbnail: Object.freeze<AlbumEntity>({
id: 'album-5',
@@ -133,9 +115,6 @@ export const albumStub = {
sharedLinks: [],
sharedUsers: [],
isActivityEnabled: true,
startDate: null,
endDate: null,
assetCount: 0,
}),
emptyWithValidThumbnail: Object.freeze<AlbumEntity>({
id: 'album-5',
@@ -152,9 +131,6 @@ export const albumStub = {
sharedLinks: [],
sharedUsers: [],
isActivityEnabled: true,
startDate: null,
endDate: null,
assetCount: 0,
}),
oneAssetInvalidThumbnail: Object.freeze<AlbumEntity>({
id: 'album-6',
@@ -171,9 +147,6 @@ export const albumStub = {
sharedLinks: [],
sharedUsers: [],
isActivityEnabled: true,
startDate: assetStub.image.fileCreatedAt,
endDate: assetStub.image.fileCreatedAt,
assetCount: 1,
}),
oneAssetValidThumbnail: Object.freeze<AlbumEntity>({
id: 'album-6',
@@ -190,8 +163,5 @@ export const albumStub = {
sharedLinks: [],
sharedUsers: [],
isActivityEnabled: true,
startDate: assetStub.image.fileCreatedAt,
endDate: assetStub.image.fileCreatedAt,
assetCount: 1,
}),
};

View File

@@ -181,9 +181,6 @@ export const sharedLinkStub = {
sharedUsers: [],
sharedLinks: [],
isActivityEnabled: true,
startDate: today,
endDate: today,
assetCount: 1,
assets: [
{
id: 'id_1',

View File

@@ -5,6 +5,7 @@ export const newAlbumRepositoryMock = (): jest.Mocked<IAlbumRepository> => {
getById: jest.fn(),
getByIds: jest.fn(),
getByAssetId: jest.fn(),
getAssetCountForIds: jest.fn(),
getInvalidThumbnail: jest.fn(),
getOwned: jest.fn(),
getShared: jest.fn(),

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.88.0
* The version of the OpenAPI document: 1.88.2
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.88.0
* The version of the OpenAPI document: 1.88.2
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.88.0
* The version of the OpenAPI document: 1.88.2
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.88.0
* The version of the OpenAPI document: 1.88.2
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.88.0
* The version of the OpenAPI document: 1.88.2
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -5,10 +5,10 @@
export let option: Sort;
const handleSort = () => {
if (albumViewSettings === option.title) {
if (albumViewSettings === option.sortTitle) {
option.sortDesc = !option.sortDesc;
} else {
albumViewSettings = option.title;
albumViewSettings = option.sortTitle;
}
};
</script>
@@ -18,12 +18,12 @@
class="rounded-lg p-2 hover:bg-immich-dark-primary hover:dark:bg-immich-dark-primary/50"
on:click={() => handleSort()}
>
{#if albumViewSettings === option.title}
{#if albumViewSettings === option.sortTitle}
{#if option.sortDesc}
&#8595;
{:else}
&#8593;
{/if}
{/if}{option.title}</button
{/if}{option.table}</button
></th
>

View File

@@ -42,8 +42,8 @@
segment.timeGroup = bucket.bucketDate;
segment.date = fromLocalDateTime(segment.timeGroup);
if (prev?.date.year !== segment.date.year && (!prev || height > MIN_YEAR_LABEL_DISTANCE)) {
segment.hasLabel = true;
if (prev?.date.year !== segment.date.year && height > MIN_YEAR_LABEL_DISTANCE) {
prev.hasLabel = true;
height = 0;
}
@@ -134,7 +134,7 @@
{#if segment.hasLabel}
<div
aria-label={segment.timeGroup + ' ' + segment.count}
class="absolute right-0 bottom-0 z-10 pr-5 text-xs dark:text-immich-dark-fg font-immich-mono font-semibold"
class="absolute right-0 bottom-0 z-10 pr-5 text-[12px] dark:text-immich-dark-fg font-immich-mono"
>
{segment.date.year}
</div>

View File

@@ -33,7 +33,7 @@
showBigSearchBar = false;
$isSearchEnabled = false;
goto(`${AppRoute.SEARCH}?${params}`);
goto(`${AppRoute.SEARCH}?${params}`, { invalidateAll: true });
}
const clearSearchTerm = (searchTerm: string) => {

View File

@@ -7,7 +7,6 @@
import { createEventDispatcher } from 'svelte';
import { goto } from '$app/navigation';
import { mdiCircleEditOutline, mdiContentCopy, mdiDelete, mdiOpenInNew } from '@mdi/js';
import noThumbnailUrl from '$lib/assets/no-thumbnail.png';
export let link: SharedLinkResponseDto;
@@ -61,7 +60,6 @@
class="flex w-full gap-4 border-b border-gray-200 py-4 transition-all hover:border-immich-primary dark:border-gray-600 dark:text-immich-gray dark:hover:border-immich-dark-primary"
>
<div>
{#if (link?.album?.assetCount && link?.album?.assetCount > 0) || link.assets.length > 0}
{#await getAssetInfo()}
<LoadingSpinner />
{:then asset}
@@ -74,15 +72,6 @@
draggable="false"
/>
{/await}
{:else}
<img
src={noThumbnailUrl}
alt={'Album without assets'}
class="h-[100px] w-[100px] rounded-lg object-cover"
loading="lazy"
draggable="false"
/>
{/if}
</div>
<div class="flex flex-col justify-between">

View File

@@ -1,6 +1,9 @@
<script lang="ts" context="module">
// table is the text printed in the table and sortTitle is the text printed in the dropDow menu
export interface Sort {
title: string;
table: string;
sortTitle: string;
sortDesc: boolean;
widthClass: string;
sortFn: (reverse: boolean, albums: AlbumResponseDto[]) => AlbumResponseDto[];
@@ -51,75 +54,46 @@
let sortByOptions: Record<string, Sort> = {
albumTitle: {
title: 'Album title',
table: 'Album title',
sortTitle: 'Album title',
sortDesc: $albumViewSettings.sortDesc, // Load Sort Direction
widthClass: 'text-left w-8/12 sm:w-4/12 md:w-4/12 md:w-4/12 xl:w-[30%] 2xl:w-[40%]',
widthClass: 'w-8/12 text-left sm:w-4/12 md:w-4/12 md:w-4/12 2xl:w-6/12',
sortFn: (reverse, albums) => {
return orderBy(albums, 'albumName', [reverse ? 'desc' : 'asc']);
},
},
numberOfAssets: {
title: 'Number of assets',
table: 'Assets',
sortTitle: 'Number of assets',
sortDesc: $albumViewSettings.sortDesc,
widthClass: 'text-center w-4/12 m:w-2/12 md:w-2/12 xl:w-[15%] 2xl:w-[12%]',
widthClass: 'w-4/12 text-center sm:w-2/12 2xl:w-1/12',
sortFn: (reverse, albums) => {
return orderBy(albums, 'assetCount', [reverse ? 'desc' : 'asc']);
},
},
lastModified: {
title: 'Last modified',
table: 'Updated date',
sortTitle: 'Last modified',
sortDesc: $albumViewSettings.sortDesc,
widthClass: 'text-center hidden sm:block w-3/12 xl:w-[15%] 2xl:w-[12%]',
widthClass: 'text-center hidden sm:block w-3/12 lg:w-2/12',
sortFn: (reverse, albums) => {
return orderBy(albums, [(album) => new Date(album.updatedAt)], [reverse ? 'desc' : 'asc']);
},
},
created: {
title: 'Created date',
sortDesc: $albumViewSettings.sortDesc,
widthClass: 'text-center hidden sm:block w-3/12 xl:w-[15%] 2xl:w-[12%]',
sortFn: (reverse, albums) => {
return orderBy(albums, [(album) => new Date(album.createdAt)], [reverse ? 'desc' : 'asc']);
},
},
mostRecent: {
title: 'Most recent photo',
table: 'Created date',
sortTitle: 'Most recent photo',
sortDesc: $albumViewSettings.sortDesc,
widthClass: 'text-center hidden xl:block xl:w-[15%] 2xl:w-[12%]',
widthClass: 'text-center hidden sm:block w-3/12 lg:w-2/12',
sortFn: (reverse, albums) => {
return orderBy(
albums,
[(album) => (album.endDate ? new Date(album.endDate) : '')],
[
(album) =>
album.lastModifiedAssetTimestamp ? new Date(album.lastModifiedAssetTimestamp) : new Date(album.updatedAt),
],
[reverse ? 'desc' : 'asc'],
).sort((a, b) => {
if (a.endDate === undefined) {
return 1;
}
if (b.endDate === undefined) {
return -1;
}
return 0;
});
},
},
mostOld: {
title: 'Oldest photo',
sortDesc: $albumViewSettings.sortDesc,
widthClass: 'text-center hidden xl:block xl:w-[15%] 2xl:w-[12%]',
sortFn: (reverse, albums) => {
return orderBy(
albums,
[(album) => (album.startDate ? new Date(album.startDate) : null)],
[reverse ? 'desc' : 'asc'],
).sort((a, b) => {
if (a.startDate === undefined) {
return 1;
}
if (b.startDate === undefined) {
return -1;
}
return 0;
});
);
},
},
};
@@ -170,25 +144,16 @@
};
$: {
const { sortBy } = $albumViewSettings;
for (const key in sortByOptions) {
if (sortByOptions[key].title === $albumViewSettings.sortBy) {
if (sortByOptions[key].sortTitle === sortBy) {
$albums = sortByOptions[key].sortFn(sortByOptions[key].sortDesc, $unsortedAlbums);
$albumViewSettings.sortDesc = sortByOptions[key].sortDesc; // "Save" sortDesc
$albumViewSettings.sortBy = sortByOptions[key].title;
break;
}
}
}
const test = (searched: string): Sort => {
for (const key in sortByOptions) {
if (sortByOptions[key].title === searched) {
return sortByOptions[key];
}
}
return sortByOptions[0];
};
const handleCreateAlbum = async () => {
const newAlbum = await createAlbum();
if (newAlbum) {
@@ -255,20 +220,19 @@
<Dropdown
options={Object.values(sortByOptions)}
selectedOption={test($albumViewSettings.sortBy)}
render={(option) => {
return {
title: option.title,
title: option.sortTitle,
icon: option.sortDesc ? mdiArrowDownThin : mdiArrowUpThin,
};
}}
on:select={(event) => {
for (const key in sortByOptions) {
if (sortByOptions[key].title === event.detail.title) {
if (sortByOptions[key].sortTitle === event.detail.sortTitle) {
sortByOptions[key].sortDesc = !sortByOptions[key].sortDesc;
$albumViewSettings.sortBy = sortByOptions[key].title;
}
}
$albumViewSettings.sortBy = event.detail.sortTitle;
}}
/>
@@ -307,7 +271,7 @@
{#each Object.keys(sortByOptions) as key (key)}
<TableHeader bind:albumViewSettings={$albumViewSettings.sortBy} bind:option={sortByOptions[key]} />
{/each}
<th class="hidden text-center text-sm font-medium 2xl:block 2xl:w-[12%]">Action</th>
<th class="hidden w-2/12 text-center text-sm font-medium lg:block 2xl:w-1/12">Action</th>
</tr>
</thead>
<tbody
@@ -320,34 +284,18 @@
on:keydown={(event) => event.key === 'Enter' && goto(`albums/${album.id}`)}
tabindex="0"
>
<td class="text-md text-ellipsis text-left w-8/12 sm:w-4/12 md:w-4/12 xl:w-[30%] 2xl:w-[40%]"
>{album.albumName}</td
>
<td class="text-md text-ellipsis text-center sm:w-2/12 md:w-2/12 xl:w-[15%] 2xl:w-[12%]">
<td class="text-md w-8/12 text-ellipsis text-left sm:w-4/12 md:w-4/12 2xl:w-6/12">{album.albumName}</td>
<td class="text-md w-4/12 text-ellipsis text-center sm:w-2/12 md:w-2/12 2xl:w-1/12">
{album.assetCount}
{album.assetCount > 1 ? `items` : `item`}
{album.assetCount == 1 ? `item` : `items`}
</td>
<td class="text-md hidden text-ellipsis text-center sm:block w-3/12 xl:w-[15%] 2xl:w-[12%]"
>{dateLocaleString(album.updatedAt)}
</td>
<td class="text-md hidden text-ellipsis text-center sm:block w-3/12 xl:w-[15%] 2xl:w-[12%]"
<td class="text-md hidden w-3/12 text-ellipsis text-center sm:block lg:w-2/12"
>{dateLocaleString(album.updatedAt)}</td
>
<td class="text-md hidden w-3/12 text-ellipsis text-center sm:block lg:w-2/12"
>{dateLocaleString(album.createdAt)}</td
>
<td class="text-md text-ellipsis text-center hidden xl:block xl:w-[15%] 2xl:w-[12%]">
{#if album.endDate}
{dateLocaleString(album.endDate)}
{:else}
&#10060;
{/if}</td
>
<td class="text-md text-ellipsis text-center hidden xl:block xl:w-[15%] 2xl:w-[12%]"
>{#if album.startDate}
{dateLocaleString(album.startDate)}
{:else}
&#10060;
{/if}</td
>
<td class="text-md hidden text-ellipsis text-center 2xl:block xl:w-[15%] 2xl:w-[12%]">
<td class="text-md hidden w-2/12 text-ellipsis text-center lg:block 2xl:w-1/12">
<button
on:click|stopPropagation={() => handleEdit(album)}
class="rounded-full bg-immich-primary p-3 text-gray-100 transition-all duration-150 hover:bg-immich-primary/75 dark:bg-immich-dark-primary dark:text-gray-700"

View File

@@ -2,11 +2,10 @@ import { authenticate } from '$lib/utils/auth';
import { api } from '@api';
import type { PageLoad } from './$types';
export const load = (async () => {
export const load = (async (data) => {
const user = await authenticate();
const url = new URL(location.href);
const url = new URL(data.url.href);
const term = url.searchParams.get('q') || url.searchParams.get('query') || undefined;
const { data: results } = await api.searchApi.search({}, { params: url.searchParams });
return {