mirror of
https://github.com/immich-app/immich.git
synced 2025-12-09 09:13:08 +03:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55fa3234fd | ||
|
|
f094ff2aa1 | ||
|
|
a13052e24c | ||
|
|
bc2c73e499 | ||
|
|
bcb885422a | ||
|
|
c438e17954 | ||
|
|
c46e82561e | ||
|
|
5c0821330f | ||
|
|
69e0db56b3 | ||
|
|
e8bf498236 | ||
|
|
9cf40afaf0 | ||
|
|
28a15365d6 | ||
|
|
d49b353c49 | ||
|
|
8b966a0f15 | ||
|
|
30e9763888 |
2
.github/workflows/cli-release.yml
vendored
2
.github/workflows/cli-release.yml
vendored
@@ -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:
|
||||
|
||||
2
cli/src/api/open-api/api.ts
generated
2
cli/src/api/open-api/api.ts
generated
@@ -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).
|
||||
|
||||
2
cli/src/api/open-api/base.ts
generated
2
cli/src/api/open-api/base.ts
generated
@@ -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).
|
||||
|
||||
2
cli/src/api/open-api/common.ts
generated
2
cli/src/api/open-api/common.ts
generated
@@ -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).
|
||||
|
||||
2
cli/src/api/open-api/configuration.ts
generated
2
cli/src/api/open-api/configuration.ts
generated
@@ -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).
|
||||
|
||||
2
cli/src/api/open-api/index.ts
generated
2
cli/src/api/open-api/index.ts
generated
@@ -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).
|
||||
|
||||
@@ -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!',
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
2
mobile/openapi/README.md
generated
2
mobile/openapi/README.md
generated
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -6052,7 +6052,7 @@
|
||||
"info": {
|
||||
"title": "Immich",
|
||||
"description": "Immich API",
|
||||
"version": "1.88.0",
|
||||
"version": "1.88.2",
|
||||
"contact": {}
|
||||
},
|
||||
"tags": [],
|
||||
|
||||
4
server/package-lock.json
generated
4
server/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich",
|
||||
"version": "1.88.0",
|
||||
"version": "1.88.2",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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[]>;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
30
server/test/fixtures/album.stub.ts
vendored
30
server/test/fixtures/album.stub.ts
vendored
@@ -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,
|
||||
}),
|
||||
};
|
||||
|
||||
3
server/test/fixtures/shared-link.stub.ts
vendored
3
server/test/fixtures/shared-link.stub.ts
vendored
@@ -181,9 +181,6 @@ export const sharedLinkStub = {
|
||||
sharedUsers: [],
|
||||
sharedLinks: [],
|
||||
isActivityEnabled: true,
|
||||
startDate: today,
|
||||
endDate: today,
|
||||
assetCount: 1,
|
||||
assets: [
|
||||
{
|
||||
id: 'id_1',
|
||||
|
||||
@@ -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(),
|
||||
|
||||
2
web/src/api/open-api/api.ts
generated
2
web/src/api/open-api/api.ts
generated
@@ -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).
|
||||
|
||||
2
web/src/api/open-api/base.ts
generated
2
web/src/api/open-api/base.ts
generated
@@ -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).
|
||||
|
||||
2
web/src/api/open-api/common.ts
generated
2
web/src/api/open-api/common.ts
generated
@@ -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).
|
||||
|
||||
2
web/src/api/open-api/configuration.ts
generated
2
web/src/api/open-api/configuration.ts
generated
@@ -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).
|
||||
|
||||
2
web/src/api/open-api/index.ts
generated
2
web/src/api/open-api/index.ts
generated
@@ -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).
|
||||
|
||||
@@ -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}
|
||||
↓
|
||||
{:else}
|
||||
↑
|
||||
{/if}
|
||||
{/if}{option.title}</button
|
||||
{/if}{option.table}</button
|
||||
></th
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
|
||||
showBigSearchBar = false;
|
||||
$isSearchEnabled = false;
|
||||
goto(`${AppRoute.SEARCH}?${params}`);
|
||||
goto(`${AppRoute.SEARCH}?${params}`, { invalidateAll: true });
|
||||
}
|
||||
|
||||
const clearSearchTerm = (searchTerm: string) => {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}
|
||||
❌
|
||||
{/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}
|
||||
❌
|
||||
{/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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user