diff --git a/i18n/en.json b/i18n/en.json index 06b3a5d599c36..876c53f0e7a78 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1781,6 +1781,7 @@ "stack_selected_photos": "Stack selected photos", "stacked_assets_count": "Stacked {count, plural, one {# asset} other {# assets}}", "stacktrace": "Stacktrace", + "star_rating_set_1_5_0": "Set rating to 1, 2, 3, 4, 5 or 0 star(s)", "start": "Start", "start_date": "Start date", "state": "State", @@ -1967,5 +1968,8 @@ "yes": "Yes", "you_dont_have_any_shared_links": "You don't have any shared links", "your_wifi_name": "Your Wi-Fi name", - "zoom_image": "Zoom Image" + "zoom_image": "Zoom Image", + "sort_by": "Sort by", + "sort_by_created_at": "Created date", + "sort_by_deleted_at": "Deleted date" } diff --git a/i18n/fr.json b/i18n/fr.json index bf574699449b7..ade78cfc58cae 100644 --- a/i18n/fr.json +++ b/i18n/fr.json @@ -1773,6 +1773,7 @@ "stack_selected_photos": "Empiler les photos sélectionnées", "stacked_assets_count": "{count, plural, one {# média empilé} other {# médias empilés}}", "stacktrace": "Trace de la pile", + "star_rating_set_1_5_0": "Définir la note à 1, 2, 3, 4, 5 ou 0 étoile(s)", "start": "Commencer", "start_date": "Date de début", "state": "Région", @@ -1955,5 +1956,8 @@ "yes": "Oui", "you_dont_have_any_shared_links": "Vous n'avez aucun lien partagé", "your_wifi_name": "Nom du réseau wifi", - "zoom_image": "Zoomer" + "zoom_image": "Zoomer", + "sort_by": "Trier par", + "sort_by_created_at": "Date de création", + "sort_by_deleted_at": "Date d'effacement" } diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 18204c21f357a..e7fdc669cff15 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -7686,6 +7686,15 @@ "type": "string" } }, + { + "name": "sortBy", + "required": false, + "in": "query", + "description": "Permet de choisir le champ de tri/groupe pour les buckets (createdAt ou deletedAt)", + "schema": { + "$ref": "#/components/schemas/TimeBucketSortBy" + } + }, { "name": "tagId", "required": false, @@ -7831,6 +7840,15 @@ "type": "string" } }, + { + "name": "sortBy", + "required": false, + "in": "query", + "description": "Permet de choisir le champ de tri/groupe pour les buckets (createdAt ou deletedAt)", + "schema": { + "$ref": "#/components/schemas/TimeBucketSortBy" + } + }, { "name": "tagId", "required": false, @@ -15457,6 +15475,13 @@ ], "type": "object" }, + "TimeBucketSortBy": { + "enum": [ + "createdAt", + "deletedAt" + ], + "type": "string" + }, "TimeBucketsResponseDto": { "properties": { "count": { diff --git a/server/src/dtos/time-bucket.dto.ts b/server/src/dtos/time-bucket.dto.ts index af2eae7e726cf..9386aa40927d6 100644 --- a/server/src/dtos/time-bucket.dto.ts +++ b/server/src/dtos/time-bucket.dto.ts @@ -47,6 +47,16 @@ export class TimeBucketDto { }) order?: AssetOrder; + @ApiProperty({ + enum: ['createdAt', 'deletedAt'], + enumName: 'TimeBucketSortBy', + required: false, + description: 'Permet de choisir le champ de tri/groupe pour les buckets (createdAt ou deletedAt)', + example: 'deletedAt', + }) + @Optional() + sortBy?: 'createdAt' | 'deletedAt'; + @ValidateAssetVisibility({ optional: true, description: 'Filter by asset visibility status (ARCHIVE, TIMELINE, HIDDEN, LOCKED)', diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index f3e798d2b48ca..2d046cf1db836 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -63,6 +63,8 @@ interface AssetBuilderOptions { export interface TimeBucketOptions extends AssetBuilderOptions { order?: AssetOrder; + /** Ajouté pour permettre le tri/groupe par createdAt ou deletedAt */ + sortBy?: 'createdAt' | 'deletedAt'; } export interface TimeBucketItem { @@ -491,11 +493,13 @@ export class AssetRepository { @GenerateSql({ params: [{}] }) async getTimeBuckets(options: TimeBucketOptions): Promise { + // Par défaut, on groupe/ordonne par fileCreatedAt, sinon par deletedAt si demandé + const groupField = options.sortBy === 'deletedAt' ? 'deletedAt' : 'fileCreatedAt'; return this.db .with('assets', (qb) => qb .selectFrom('assets') - .select(truncatedDate().as('timeBucket')) + .select(sql.raw(`date_trunc('day', assets."${groupField}")`).as('timeBucket')) .$if(!!options.isTrashed, (qb) => qb.where('assets.status', '!=', AssetStatus.DELETED)) .where('assets.deletedAt', options.isTrashed ? 'is not' : 'is', null) .$if(options.visibility === undefined, withDefaultVisibility) diff --git a/server/src/services/timeline.service.ts b/server/src/services/timeline.service.ts index abd536a97e21d..29b0c68ad55a6 100644 --- a/server/src/services/timeline.service.ts +++ b/server/src/services/timeline.service.ts @@ -12,6 +12,9 @@ export class TimelineService extends BaseService { async getTimeBuckets(auth: AuthDto, dto: TimeBucketDto): Promise { await this.timeBucketChecks(auth, dto); const timeBucketOptions = await this.buildTimeBucketOptions(auth, dto); + if (dto.sortBy) { + (timeBucketOptions as any).sortBy = dto.sortBy; + } return await this.assetRepository.getTimeBuckets(timeBucketOptions); } @@ -26,7 +29,7 @@ export class TimelineService extends BaseService { } private async buildTimeBucketOptions(auth: AuthDto, dto: TimeBucketDto): Promise { - const { userId, ...options } = dto; + const { userId, sortBy, ...options } = dto; let userIds: string[] | undefined = undefined; if (userId) { @@ -41,6 +44,7 @@ export class TimelineService extends BaseService { } } + // sortBy est propagé plus haut return { ...options, userIds }; } diff --git a/web/src/lib/components/asset-viewer/detail-panel-star-rating.svelte b/web/src/lib/components/asset-viewer/detail-panel-star-rating.svelte index 0ec8692180fbd..fb6dad62cb104 100644 --- a/web/src/lib/components/asset-viewer/detail-panel-star-rating.svelte +++ b/web/src/lib/components/asset-viewer/detail-panel-star-rating.svelte @@ -1,4 +1,5 @@ + handleShortcutRating('1') }, + { shortcut: { key: '2' }, onShortcut: () => handleShortcutRating('2') }, + { shortcut: { key: '3' }, onShortcut: () => handleShortcutRating('3') }, + { shortcut: { key: '4' }, onShortcut: () => handleShortcutRating('4') }, + { shortcut: { key: '5' }, onShortcut: () => handleShortcutRating('5') }, + { shortcut: { key: '0' }, onShortcut: () => handleShortcutRating('0') }, + ]} +/> + {#if !authManager.key && $preferences?.ratings.enabled}
handlePromiseError(handleChangeRating(rating))} /> diff --git a/web/src/lib/managers/timeline-manager/types.ts b/web/src/lib/managers/timeline-manager/types.ts index 8e5523758ba29..307f5bd2eadaa 100644 --- a/web/src/lib/managers/timeline-manager/types.ts +++ b/web/src/lib/managers/timeline-manager/types.ts @@ -6,6 +6,7 @@ export type AssetApiGetTimeBucketsRequest = Parameters & { timelineAlbumId?: string; deferInit?: boolean; + sortBy?: 'createdAt' | 'deletedAt'; }; export type AssetDescriptor = { id: string }; diff --git a/web/src/lib/modals/ShortcutsModal.svelte b/web/src/lib/modals/ShortcutsModal.svelte index c13d245107eea..fcb5c0556e6cf 100644 --- a/web/src/lib/modals/ShortcutsModal.svelte +++ b/web/src/lib/modals/ShortcutsModal.svelte @@ -44,6 +44,7 @@ { key: ['⇧', 'd'], action: $t('download') }, { key: ['Space'], action: $t('play_or_pause_video') }, { key: ['Del'], action: $t('trash_delete_asset'), info: $t('shift_to_permanent_delete') }, + { key: ['1', '2', '3', '4', '5', '0'], action: $t('star_rating_set_1_5_0') }, ], }, }: Props = $props(); diff --git a/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte index bae178689185e..66f7f1c17ca0a 100644 --- a/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -37,7 +37,14 @@ } const timelineManager = new TimelineManager(); - void timelineManager.updateOptions({ isTrashed: true }); + let sortBy = $state<'createdAt' | 'deletedAt'>('createdAt'); + void timelineManager.updateOptions({ isTrashed: true, sortBy: 'createdAt' }); + + function handleSortChange(event: Event) { + const value = (event.target as HTMLSelectElement).value as 'createdAt' | 'deletedAt'; + sortBy = value; + void timelineManager.updateOptions({ isTrashed: true, sortBy }); + } onDestroy(() => timelineManager.destroy()); const assetInteraction = new AssetInteraction(); @@ -92,7 +99,7 @@ {#if $featureFlags.loaded && $featureFlags.trash} - {#snippet buttons()} +
- {/snippet} - +
+ + +
+

{$t('trashed_items_will_be_permanently_deleted_after', { values: { days: $serverConfig.trashDays } })}