1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-01-01 08:31:59 +00:00

feat(web,server): search people (#5703)

* feat: search peoples

* fix: responsive design

* use existing count

* generate sql file

* fix: tests

* remove visible people

* fix: merge, hide...

* use component

* fix: linter

* chore: regenerate api

* fix: change name when searching for a face

* save search

* remove duplicate

* use enums for query parameters

* fix: increase to 20 for the local search

* use constants

* simplify

* fix: number of people more visible

* fix: merge

* fix: search

* fix: loading spinner position

* pr feedback
This commit is contained in:
martin 2024-01-28 01:54:31 +01:00 committed by GitHub
parent 2249f7d42a
commit fa0913120d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
37 changed files with 283 additions and 131 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -8523,15 +8523,11 @@
}, },
"total": { "total": {
"type": "integer" "type": "integer"
},
"visible": {
"type": "integer"
} }
}, },
"required": [ "required": [
"people", "people",
"total", "total"
"visible"
], ],
"type": "object" "type": "object"
}, },

View file

@ -2577,12 +2577,6 @@ export interface PeopleResponseDto {
* @memberof PeopleResponseDto * @memberof PeopleResponseDto
*/ */
'total': number; 'total': number;
/**
*
* @type {number}
* @memberof PeopleResponseDto
*/
'visible': number;
} }
/** /**
* *

View file

@ -80,7 +80,6 @@ describe(`${PersonController.name}`, () => {
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toEqual({ expect(body).toEqual({
total: 2, total: 2,
visible: 1,
people: [ people: [
expect.objectContaining({ name: 'visible_person' }), expect.objectContaining({ name: 'visible_person' }),
expect.objectContaining({ name: 'hidden_person' }), expect.objectContaining({ name: 'hidden_person' }),
@ -93,8 +92,7 @@ describe(`${PersonController.name}`, () => {
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toEqual({ expect(body).toEqual({
total: 1, total: 2,
visible: 1,
people: [expect.objectContaining({ name: 'visible_person' })], people: [expect.objectContaining({ name: 'visible_person' })],
}); });
}); });

View file

@ -128,9 +128,6 @@ export class PeopleResponseDto {
@ApiProperty({ type: 'integer' }) @ApiProperty({ type: 'integer' })
total!: number; total!: number;
@ApiProperty({ type: 'integer' })
visible!: number;
people!: PersonResponseDto[]; people!: PersonResponseDto[];
} }

View file

@ -116,9 +116,9 @@ describe(PersonService.name, () => {
describe('getAll', () => { describe('getAll', () => {
it('should get all people with thumbnails', async () => { it('should get all people with thumbnails', async () => {
personMock.getAllForUser.mockResolvedValue([personStub.withName, personStub.noThumbnail]); personMock.getAllForUser.mockResolvedValue([personStub.withName, personStub.noThumbnail]);
personMock.getNumberOfPeople.mockResolvedValue(1);
await expect(sut.getAll(authStub.admin, { withHidden: undefined })).resolves.toEqual({ await expect(sut.getAll(authStub.admin, { withHidden: undefined })).resolves.toEqual({
total: 1, total: 1,
visible: 1,
people: [responseDto], people: [responseDto],
}); });
expect(personMock.getAllForUser).toHaveBeenCalledWith(authStub.admin.user.id, { expect(personMock.getAllForUser).toHaveBeenCalledWith(authStub.admin.user.id, {
@ -128,9 +128,9 @@ describe(PersonService.name, () => {
}); });
it('should get all visible people with thumbnails', async () => { it('should get all visible people with thumbnails', async () => {
personMock.getAllForUser.mockResolvedValue([personStub.withName, personStub.hidden]); personMock.getAllForUser.mockResolvedValue([personStub.withName, personStub.hidden]);
personMock.getNumberOfPeople.mockResolvedValue(2);
await expect(sut.getAll(authStub.admin, { withHidden: false })).resolves.toEqual({ await expect(sut.getAll(authStub.admin, { withHidden: false })).resolves.toEqual({
total: 2, total: 2,
visible: 1,
people: [responseDto], people: [responseDto],
}); });
expect(personMock.getAllForUser).toHaveBeenCalledWith(authStub.admin.user.id, { expect(personMock.getAllForUser).toHaveBeenCalledWith(authStub.admin.user.id, {
@ -140,9 +140,9 @@ describe(PersonService.name, () => {
}); });
it('should get all hidden and visible people with thumbnails', async () => { it('should get all hidden and visible people with thumbnails', async () => {
personMock.getAllForUser.mockResolvedValue([personStub.withName, personStub.hidden]); personMock.getAllForUser.mockResolvedValue([personStub.withName, personStub.hidden]);
personMock.getNumberOfPeople.mockResolvedValue(2);
await expect(sut.getAll(authStub.admin, { withHidden: true })).resolves.toEqual({ await expect(sut.getAll(authStub.admin, { withHidden: true })).resolves.toEqual({
total: 2, total: 2,
visible: 1,
people: [ people: [
responseDto, responseDto,
{ {

View file

@ -82,6 +82,7 @@ export class PersonService {
minimumFaceCount: machineLearning.facialRecognition.minFaces, minimumFaceCount: machineLearning.facialRecognition.minFaces,
withHidden: dto.withHidden || false, withHidden: dto.withHidden || false,
}); });
const total = await this.repository.getNumberOfPeople(auth.user.id);
const persons: PersonResponseDto[] = people const persons: PersonResponseDto[] = people
// with thumbnails // with thumbnails
.filter((person) => !!person.thumbnailPath) .filter((person) => !!person.thumbnailPath)
@ -89,8 +90,7 @@ export class PersonService {
return { return {
people: persons.filter((person) => dto.withHidden || !person.isHidden), people: persons.filter((person) => dto.withHidden || !person.isHidden),
total: persons.length, total,
visible: persons.filter((person: PersonResponseDto) => !person.isHidden).length,
}; };
} }

View file

@ -54,6 +54,7 @@ export interface IPersonRepository {
getRandomFace(personId: string): Promise<AssetFaceEntity | null>; getRandomFace(personId: string): Promise<AssetFaceEntity | null>;
getStatistics(personId: string): Promise<PersonStatistics>; getStatistics(personId: string): Promise<PersonStatistics>;
reassignFace(assetFaceId: string, newPersonId: string): Promise<number>; reassignFace(assetFaceId: string, newPersonId: string): Promise<number>;
getNumberOfPeople(userId: string): Promise<number>;
reassignFaces(data: UpdateFacesData): Promise<number>; reassignFaces(data: UpdateFacesData): Promise<number>;
update(entity: Partial<PersonEntity>): Promise<PersonEntity>; update(entity: Partial<PersonEntity>): Promise<PersonEntity>;
} }

View file

@ -211,6 +211,18 @@ export class PersonRepository implements IPersonRepository {
}); });
} }
@GenerateSql({ params: [DummyValue.UUID] })
async getNumberOfPeople(userId: string): Promise<number> {
return this.personRepository
.createQueryBuilder('person')
.leftJoin('person.faces', 'face')
.where('person.ownerId = :userId', { userId })
.having('COUNT(face.assetId) != 0')
.groupBy('person.id')
.withDeleted()
.getCount();
}
create(entity: Partial<PersonEntity>): Promise<PersonEntity> { create(entity: Partial<PersonEntity>): Promise<PersonEntity> {
return this.personRepository.save(entity); return this.personRepository.save(entity);
} }

View file

@ -338,6 +338,17 @@ ORDER BY
LIMIT LIMIT
1000 1000
-- PersonRepository.getNumberOfPeople
SELECT
COUNT(DISTINCT ("person"."id")) AS "cnt"
FROM
"person" "person"
LEFT JOIN "asset_faces" "face" ON "face"."personId" = "person"."id"
WHERE
"person"."ownerId" = $1
HAVING
COUNT("face"."assetId") != 0
-- PersonRepository.getFacesByIds -- PersonRepository.getFacesByIds
SELECT SELECT
"AssetFaceEntity"."id" AS "AssetFaceEntity_id", "AssetFaceEntity"."id" AS "AssetFaceEntity_id",

View file

@ -27,5 +27,6 @@ export const newPersonRepositoryMock = (): jest.Mocked<IPersonRepository> => {
reassignFace: jest.fn(), reassignFace: jest.fn(),
getFaceById: jest.fn(), getFaceById: jest.fn(),
getFaceByIdWithAssets: jest.fn(), getFaceByIdWithAssets: jest.fn(),
getNumberOfPeople: jest.fn(),
}; };
}; };

View file

@ -13,6 +13,7 @@
import SettingSwitch from '../setting-switch.svelte'; import SettingSwitch from '../setting-switch.svelte';
import SupportedDatetimePanel from './supported-datetime-panel.svelte'; import SupportedDatetimePanel from './supported-datetime-panel.svelte';
import SupportedVariablesPanel from './supported-variables-panel.svelte'; import SupportedVariablesPanel from './supported-variables-panel.svelte';
import { AppRoute } from '$lib/constants';
export let savedConfig: SystemConfigDto; export let savedConfig: SystemConfigDto;
export let defaultConfig: SystemConfigDto; export let defaultConfig: SystemConfigDto;
@ -185,7 +186,7 @@
<p> <p>
Template changes will only apply to new assets. To retroactively apply the template to previously Template changes will only apply to new assets. To retroactively apply the template to previously
uploaded assets, run the uploaded assets, run the
<a href="/admin/jobs-status" class="text-immich-primary dark:text-immich-dark-primary" <a href={AppRoute.ADMIN_JOBS} class="text-immich-primary dark:text-immich-dark-primary"
>Storage Migration Job</a >Storage Migration Job</a
>. >.
</p> </p>
@ -193,7 +194,7 @@
The template variable <span class="font-mono">{`{{album}}`}</span> will always be empty for new The template variable <span class="font-mono">{`{{album}}`}</span> will always be empty for new
assets, so manually running the assets, so manually running the
<a href="/admin/jobs-status" class="text-immich-primary dark:text-immich-dark-primary" <a href={AppRoute.ADMIN_JOBS} class="text-immich-primary dark:text-immich-dark-primary"
>Storage Migration Job</a >Storage Migration Job</a
> >
is required in order to successfully use the variable. is required in order to successfully use the variable.

View file

@ -19,6 +19,7 @@
import { NotificationType, notificationController } from '../shared-components/notification/notification'; import { NotificationType, notificationController } from '../shared-components/notification/notification';
import { getAssetType } from '$lib/utils/asset-utils'; import { getAssetType } from '$lib/utils/asset-utils';
import * as luxon from 'luxon'; import * as luxon from 'luxon';
import { timeBeforeShowLoadingSpinner } from '$lib/constants';
import { autoGrowHeight } from '$lib/utils/autogrow'; import { autoGrowHeight } from '$lib/utils/autogrow';
const units: Intl.RelativeTimeFormatUnit[] = ['year', 'month', 'week', 'day', 'hour', 'minute', 'second']; const units: Intl.RelativeTimeFormatUnit[] = ['year', 'month', 'week', 'day', 'hour', 'minute', 'second'];
@ -132,7 +133,7 @@
if (!message) { if (!message) {
return; return;
} }
const timeout = setTimeout(() => (isSendingMessage = true), 100); const timeout = setTimeout(() => (isSendingMessage = true), timeBeforeShowLoadingSpinner);
try { try {
const { data } = await api.activityApi.createActivity({ const { data } = await api.activityApi.createActivity({
activityCreateDto: { albumId, assetId, type: ReactionType.Comment, comment: message }, activityCreateDto: { albumId, assetId, type: ReactionType.Comment, comment: message },

View file

@ -27,7 +27,7 @@
import Map from '../shared-components/map/map.svelte'; import Map from '../shared-components/map/map.svelte';
import { boundingBoxesArray } from '$lib/stores/people.store'; import { boundingBoxesArray } from '$lib/stores/people.store';
import { websocketStore } from '$lib/stores/websocket'; import { websocketStore } from '$lib/stores/websocket';
import { AppRoute } from '$lib/constants'; import { AppRoute, QueryParameter } from '$lib/constants';
import ChangeLocation from '../shared-components/change-location.svelte'; import ChangeLocation from '../shared-components/change-location.svelte';
import { handleError } from '../../utils/handle-error'; import { handleError } from '../../utils/handle-error';
import { user } from '$lib/stores/user.store'; import { user } from '$lib/stores/user.store';
@ -274,7 +274,7 @@
on:mouseleave={() => ($boundingBoxesArray = [])} on:mouseleave={() => ($boundingBoxesArray = [])}
> >
<a <a
href="{AppRoute.PEOPLE}/{person.id}?previousRoute={albumId href="{AppRoute.PEOPLE}/{person.id}?{QueryParameter.PREVIOUS_ROUTE}={albumId
? `${AppRoute.ALBUMS}/${albumId}` ? `${AppRoute.ALBUMS}/${albumId}`
: AppRoute.PHOTOS}" : AppRoute.PHOTOS}"
on:click={() => dispatch('closeViewer')} on:click={() => dispatch('closeViewer')}

View file

@ -10,6 +10,7 @@
import { getPersonNameWithHiddenValue, searchNameLocal } from '$lib/utils/person'; import { getPersonNameWithHiddenValue, searchNameLocal } from '$lib/utils/person';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { photoViewer } from '$lib/stores/assets.store'; import { photoViewer } from '$lib/stores/assets.store';
import { maximumLengthSearchPeople, timeBeforeShowLoadingSpinner } from '$lib/constants';
export let peopleWithFaces: AssetFaceResponseDto[]; export let peopleWithFaces: AssetFaceResponseDto[];
export let allPeople: PersonResponseDto[]; export let allPeople: PersonResponseDto[];
@ -90,7 +91,7 @@
}; };
const handleCreatePerson = async () => { const handleCreatePerson = async () => {
const timeout = setTimeout(() => (isShowLoadingNewPerson = true), 100); const timeout = setTimeout(() => (isShowLoadingNewPerson = true), timeBeforeShowLoadingSpinner);
const personToUpdate = peopleWithFaces.find((person) => person.id === peopleWithFaces[editedPersonIndex].id); const personToUpdate = peopleWithFaces.find((person) => person.id === peopleWithFaces[editedPersonIndex].id);
const newFeaturePhoto = personToUpdate ? await zoomImageToBase64(personToUpdate) : null; const newFeaturePhoto = personToUpdate ? await zoomImageToBase64(personToUpdate) : null;
@ -103,10 +104,10 @@
}; };
const searchPeople = async () => { const searchPeople = async () => {
if ((searchedPeople.length < 20 && searchName.startsWith(searchWord)) || searchName === '') { if ((searchedPeople.length < maximumLengthSearchPeople && searchName.startsWith(searchWord)) || searchName === '') {
return; return;
} }
const timeout = setTimeout(() => (isShowLoadingSearch = true), 100); const timeout = setTimeout(() => (isShowLoadingSearch = true), timeBeforeShowLoadingSpinner);
try { try {
const { data } = await api.searchApi.searchPerson({ name: searchName }); const { data } = await api.searchApi.searchPerson({ name: searchName });
searchedPeople = data; searchedPeople = data;
@ -122,7 +123,7 @@
}; };
$: { $: {
searchedPeople = searchNameLocal(searchName, searchedPeopleCopy, 10); searchedPeople = searchNameLocal(searchName, searchedPeopleCopy, 20);
} }
const initInput = (element: HTMLInputElement) => { const initInput = (element: HTMLInputElement) => {

View file

@ -11,7 +11,7 @@
import ConfirmDialogue from '../shared-components/confirm-dialogue.svelte'; import ConfirmDialogue from '../shared-components/confirm-dialogue.svelte';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { AppRoute } from '$lib/constants'; import { ActionQueryParameterValue, AppRoute, QueryParameter } from '$lib/constants';
import { mdiCallMerge, mdiMerge, mdiSwapHorizontal } from '@mdi/js'; import { mdiCallMerge, mdiMerge, mdiSwapHorizontal } from '@mdi/js';
import Icon from '$lib/components/elements/icon.svelte'; import Icon from '$lib/components/elements/icon.svelte';
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte'; import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
@ -45,7 +45,7 @@
const handleSwapPeople = () => { const handleSwapPeople = () => {
[person, selectedPeople[0]] = [selectedPeople[0], person]; [person, selectedPeople[0]] = [selectedPeople[0], person];
$page.url.searchParams.set('action', 'merge'); $page.url.searchParams.set(QueryParameter.ACTION, ActionQueryParameterValue.MERGE);
goto(`${AppRoute.PEOPLE}/${person.id}?${$page.url.searchParams.toString()}`); goto(`${AppRoute.PEOPLE}/${person.id}?${$page.url.searchParams.toString()}`);
}; };

View file

@ -7,7 +7,7 @@
import MenuOption from '../shared-components/context-menu/menu-option.svelte'; import MenuOption from '../shared-components/context-menu/menu-option.svelte';
import Portal from '../shared-components/portal/portal.svelte'; import Portal from '../shared-components/portal/portal.svelte';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
import { AppRoute } from '$lib/constants'; import { AppRoute, QueryParameter } from '$lib/constants';
import { mdiDotsVertical } from '@mdi/js'; import { mdiDotsVertical } from '@mdi/js';
import Icon from '$lib/components/elements/icon.svelte'; import Icon from '$lib/components/elements/icon.svelte';
@ -45,7 +45,7 @@
on:mouseleave={() => (showVerticalDots = false)} on:mouseleave={() => (showVerticalDots = false)}
role="group" role="group"
> >
<a href="{AppRoute.PEOPLE}/{person.id}?previousRoute={AppRoute.PEOPLE}" draggable="false"> <a href="{AppRoute.PEOPLE}/{person.id}?{QueryParameter.PREVIOUS_ROUTE}={AppRoute.PEOPLE}" draggable="false">
<div class="w-full h-full rounded-xl brightness-95 filter"> <div class="w-full h-full rounded-xl brightness-95 filter">
<ImageThumbnail <ImageThumbnail
shadow shadow

View file

@ -2,11 +2,10 @@
import { api, type PersonResponseDto } from '@api'; import { api, type PersonResponseDto } from '@api';
import FaceThumbnail from './face-thumbnail.svelte'; import FaceThumbnail from './face-thumbnail.svelte';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
import Icon from '../elements/icon.svelte';
import { mdiClose, mdiMagnify } from '@mdi/js';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
import { searchNameLocal } from '$lib/utils/person'; import { searchNameLocal } from '$lib/utils/person';
import SearchBar from './search-bar.svelte';
import { maximumLengthSearchPeople, timeBeforeShowLoadingSpinner } from '$lib/constants';
export let screenHeight: number; export let screenHeight: number;
export let people: PersonResponseDto[]; export let people: PersonResponseDto[];
@ -21,17 +20,12 @@
select: PersonResponseDto; select: PersonResponseDto;
}>(); }>();
const resetSearch = () => {
name = '';
people = peopleCopy;
};
$: { $: {
people = peopleCopy.filter( people = peopleCopy.filter(
(person) => !unselectedPeople.some((unselectedPerson) => unselectedPerson.id === person.id), (person) => !unselectedPeople.some((unselectedPerson) => unselectedPerson.id === person.id),
); );
if (name) { if (name) {
people = searchNameLocal(name, people, 10); people = searchNameLocal(name, people, maximumLengthSearchPeople);
} }
} }
@ -41,12 +35,12 @@
return; return;
} }
if (!force) { if (!force) {
if (people.length < 20 && name.startsWith(searchWord)) { if (people.length < maximumLengthSearchPeople && name.startsWith(searchWord)) {
return; return;
} }
} }
const timeout = setTimeout(() => (isSearchingPeople = true), 100); const timeout = setTimeout(() => (isSearchingPeople = true), timeBeforeShowLoadingSpinner);
try { try {
const { data } = await api.searchApi.searchPerson({ name }); const { data } = await api.searchApi.searchPerson({ name });
people = data; people = data;
@ -61,31 +55,15 @@
}; };
</script> </script>
<div class="flex w-40 sm:w-48 md:w-96 h-14 rounded-lg bg-gray-100 p-2 dark:bg-gray-700 mb-8 gap-2 place-items-center"> <div class=" w-40 sm:w-48 md:w-96 h-14 mb-8">
<button on:click={() => searchPeople(true)}> <SearchBar
<div class="w-fit"> bind:name
<Icon path={mdiMagnify} size="24" /> {isSearchingPeople}
</div> on:reset={() => {
</button> people = peopleCopy;
<!-- svelte-ignore a11y-autofocus --> }}
<input on:search={({ detail }) => searchPeople(detail.force ?? false)}
autofocus
class="w-full gap-2 bg-gray-100 dark:bg-gray-700 dark:text-white"
type="text"
placeholder="Search names"
bind:value={name}
on:input={() => searchPeople(false)}
/> />
{#if name}
<button on:click={resetSearch}>
<Icon path={mdiClose} />
</button>
{/if}
{#if isSearchingPeople}
<div class="flex place-items-center">
<LoadingSpinner />
</div>
{/if}
</div> </div>
<div <div

View file

@ -13,6 +13,7 @@
import { websocketStore } from '$lib/stores/websocket'; import { websocketStore } from '$lib/stores/websocket';
import AssignFaceSidePanel from './assign-face-side-panel.svelte'; import AssignFaceSidePanel from './assign-face-side-panel.svelte';
import { getPersonNameWithHiddenValue } from '$lib/utils/person'; import { getPersonNameWithHiddenValue } from '$lib/utils/person';
import { timeBeforeShowLoadingSpinner } from '$lib/constants';
export let assetId: string; export let assetId: string;
export let assetType: AssetTypeEnum; export let assetType: AssetTypeEnum;
@ -65,7 +66,7 @@
} }
onMount(async () => { onMount(async () => {
const timeout = setTimeout(() => (isShowLoadingPeople = true), 100); const timeout = setTimeout(() => (isShowLoadingPeople = true), timeBeforeShowLoadingSpinner);
try { try {
const { data } = await api.personApi.getAllPeople({ withHidden: true }); const { data } = await api.personApi.getAllPeople({ withHidden: true });
allPeople = data.people; allPeople = data.people;
@ -99,7 +100,7 @@
}; };
const handleEditFaces = async () => { const handleEditFaces = async () => {
loaderLoadingDoneTimeout = setTimeout(() => (isShowLoadingDone = true), 100); loaderLoadingDoneTimeout = setTimeout(() => (isShowLoadingDone = true), timeBeforeShowLoadingSpinner);
const numberOfChanges = const numberOfChanges =
selectedPersonToCreate.filter((person) => person !== null).length + selectedPersonToCreate.filter((person) => person !== null).length +
selectedPersonToReassign.filter((person) => person !== null).length; selectedPersonToReassign.filter((person) => person !== null).length;

View file

@ -0,0 +1,44 @@
<script lang="ts">
import { mdiClose, mdiMagnify } from '@mdi/js';
import Icon from '../elements/icon.svelte';
import { createEventDispatcher } from 'svelte';
import type { SearchOptions } from '$lib/utils/dipatch';
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
export let name: string;
export let isSearchingPeople: boolean;
const dispatch = createEventDispatcher<{ search: SearchOptions; reset: void }>();
const resetSearch = () => {
name = '';
dispatch('reset');
};
</script>
<div class="flex items-center text-sm rounded-lg bg-gray-100 p-2 dark:bg-gray-700 gap-2 place-items-center h-full">
<button on:click={() => dispatch('search', { force: true })}>
<div class="w-fit">
<Icon path={mdiMagnify} size="24" />
</div>
</button>
<!-- svelte-ignore a11y-autofocus -->
<input
autofocus
class="w-full gap-2 bg-gray-100 dark:bg-gray-700 dark:text-white"
type="text"
placeholder="Search names"
bind:value={name}
on:input={() => dispatch('search', { force: false })}
/>
{#if isSearchingPeople}
<div class="flex place-items-center">
<LoadingSpinner />
</div>
{/if}
{#if name}
<button on:click={resetSearch}>
<Icon path={mdiClose} />
</button>
{/if}
</div>

View file

@ -24,7 +24,7 @@
class="absolute left-0 top-0 z-[9999] h-full w-full bg-immich-bg dark:bg-immich-dark-bg" class="absolute left-0 top-0 z-[9999] h-full w-full bg-immich-bg dark:bg-immich-dark-bg"
> >
<div <div
class="sticky top-0 z-10 flex h-16 w-full items-center justify-between border-b bg-white p-1 dark:border-immich-dark-gray dark:bg-black dark:text-immich-dark-fg md:p-8" class="fixed top-0 z-10 flex h-16 w-full items-center justify-between border-b bg-white p-1 dark:border-immich-dark-gray dark:bg-black dark:text-immich-dark-fg md:p-8"
> >
<div class="flex items-center"> <div class="flex items-center">
<CircleIconButton icon={mdiClose} on:click={() => dispatch('close')} /> <CircleIconButton icon={mdiClose} on:click={() => dispatch('close')} />
@ -47,7 +47,7 @@
</div> </div>
</div> </div>
<div class="flex w-full flex-wrap gap-1 bg-immich-bg p-2 pb-8 dark:bg-immich-dark-bg md:px-8 md:pt-4"> <div class="flex w-full flex-wrap gap-1 bg-immich-bg p-2 pb-8 dark:bg-immich-dark-bg md:px-8 mt-16">
<slot /> <slot />
</div> </div>
</section> </section>

View file

@ -12,6 +12,7 @@
import { notificationController, NotificationType } from '../shared-components/notification/notification'; import { notificationController, NotificationType } from '../shared-components/notification/notification';
import PeopleList from './people-list.svelte'; import PeopleList from './people-list.svelte';
import Icon from '$lib/components/elements/icon.svelte'; import Icon from '$lib/components/elements/icon.svelte';
import { timeBeforeShowLoadingSpinner } from '$lib/constants';
export let assetIds: string[]; export let assetIds: string[];
export let personAssets: PersonResponseDto; export let personAssets: PersonResponseDto;
@ -63,7 +64,7 @@
}; };
const handleCreate = async () => { const handleCreate = async () => {
const timeout = setTimeout(() => (showLoadingSpinnerCreate = true), 100); const timeout = setTimeout(() => (showLoadingSpinnerCreate = true), timeBeforeShowLoadingSpinner);
try { try {
disableButtons = true; disableButtons = true;
@ -88,7 +89,7 @@
}; };
const handleReassign = async () => { const handleReassign = async () => {
const timeout = setTimeout(() => (showLoadingSpinnerReassign = true), 100); const timeout = setTimeout(() => (showLoadingSpinnerReassign = true), timeBeforeShowLoadingSpinner);
try { try {
disableButtons = true; disableButtons = true;
if (selectedPerson) { if (selectedPerson) {

View file

@ -7,6 +7,7 @@
export let hideNavbar = false; export let hideNavbar = false;
export let showUploadButton = false; export let showUploadButton = false;
export let title: string | undefined = undefined; export let title: string | undefined = undefined;
export let description: string | undefined = undefined;
export let scrollbar = true; export let scrollbar = true;
export let admin = false; export let admin = false;
@ -37,7 +38,12 @@
<div <div
class="absolute flex h-16 w-full place-items-center justify-between border-b p-4 dark:border-immich-dark-gray dark:text-immich-dark-fg" class="absolute flex h-16 w-full place-items-center justify-between border-b p-4 dark:border-immich-dark-gray dark:text-immich-dark-fg"
> >
<p class="font-medium">{title}</p> <div class="flex gap-2 items-center">
<div class="font-medium">{title}</div>
{#if description}
<p class="text-sm text-gray-400 dark:text-gray-600">{description}</p>
{/if}
</div>
<slot name="buttons" /> <slot name="buttons" />
</div> </div>
{/if} {/if}

View file

@ -6,7 +6,7 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte'; import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
import { fromLocalDateTime } from '$lib/utils/timeline-util'; import { fromLocalDateTime } from '$lib/utils/timeline-util';
import { AppRoute } from '$lib/constants'; import { AppRoute, QueryParameter } from '$lib/constants';
import { page } from '$app/stores'; import { page } from '$app/stores';
import noThumbnailUrl from '$lib/assets/no-thumbnail.png'; import noThumbnailUrl from '$lib/assets/no-thumbnail.png';
import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte'; import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte';
@ -18,8 +18,8 @@
const parseIndex = (s: string | null, max: number | null) => Math.max(Math.min(parseInt(s ?? '') || 0, max ?? 0), 0); const parseIndex = (s: string | null, max: number | null) => Math.max(Math.min(parseInt(s ?? '') || 0, max ?? 0), 0);
$: memoryIndex = parseIndex($page.url.searchParams.get('memory'), $memoryStore?.length - 1); $: memoryIndex = parseIndex($page.url.searchParams.get(QueryParameter.MEMORY_INDEX), $memoryStore?.length - 1);
$: assetIndex = parseIndex($page.url.searchParams.get('asset'), currentMemory?.assets.length - 1); $: assetIndex = parseIndex($page.url.searchParams.get(QueryParameter.ASSET_INDEX), currentMemory?.assets.length - 1);
$: previousMemory = $memoryStore?.[memoryIndex - 1]; $: previousMemory = $memoryStore?.[memoryIndex - 1];
$: currentMemory = $memoryStore?.[memoryIndex]; $: currentMemory = $memoryStore?.[memoryIndex];
@ -32,11 +32,13 @@
$: canGoForward = !!(nextMemory || nextAsset); $: canGoForward = !!(nextMemory || nextAsset);
$: canGoBack = !!(previousMemory || previousAsset); $: canGoBack = !!(previousMemory || previousAsset);
const toNextMemory = () => goto(`?memory=${memoryIndex + 1}`); const toNextMemory = () => goto(`?${QueryParameter.MEMORY_INDEX}=${memoryIndex + 1}`);
const toPreviousMemory = () => goto(`?memory=${memoryIndex - 1}`); const toPreviousMemory = () => goto(`?${QueryParameter.MEMORY_INDEX}=${memoryIndex - 1}`);
const toNextAsset = () => goto(`?memory=${memoryIndex}&asset=${assetIndex + 1}`); const toNextAsset = () =>
const toPreviousAsset = () => goto(`?memory=${memoryIndex}&asset=${assetIndex - 1}`); goto(`?${QueryParameter.MEMORY_INDEX}=${memoryIndex}&${QueryParameter.ASSET_INDEX}=${assetIndex + 1}`);
const toPreviousAsset = () =>
goto(`?${QueryParameter.MEMORY_INDEX}=${memoryIndex}&${QueryParameter.ASSET_INDEX}=${assetIndex - 1}`);
const toNext = () => (nextAsset ? toNextAsset() : toNextMemory()); const toNext = () => (nextAsset ? toNextAsset() : toNextMemory());
const toPrevious = () => (previousAsset ? toPreviousAsset() : toPreviousMemory()); const toPrevious = () => (previousAsset ? toPreviousAsset() : toPreviousMemory());
@ -113,7 +115,10 @@
<CircleIconButton icon={paused ? mdiPlay : mdiPause} forceDark on:click={() => (paused = !paused)} /> <CircleIconButton icon={paused ? mdiPlay : mdiPause} forceDark on:click={() => (paused = !paused)} />
{#each currentMemory.assets as _, i} {#each currentMemory.assets as _, i}
<button class="relative w-full py-2" on:click={() => goto(`?memory=${memoryIndex}&asset=${i}`)}> <button
class="relative w-full py-2"
on:click={() => goto(`?${QueryParameter.MEMORY_INDEX}=${memoryIndex}&${QueryParameter.ASSET_INDEX}=${i}`)}
>
<span class="absolute left-0 h-[2px] w-full bg-gray-500" /> <span class="absolute left-0 h-[2px] w-full bg-gray-500" />
{#await resetPromise} {#await resetPromise}
<span class="absolute left-0 h-[2px] bg-white" style:width={`${i < assetIndex ? 100 : 0}%`} /> <span class="absolute left-0 h-[2px] bg-white" style:width={`${i < assetIndex ? 100 : 0}%`} />

View file

@ -6,6 +6,7 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import { mdiChevronLeft, mdiChevronRight } from '@mdi/js'; import { mdiChevronLeft, mdiChevronRight } from '@mdi/js';
import { AppRoute, QueryParameter } from '$lib/constants';
$: shouldRender = $memoryStore?.length > 0; $: shouldRender = $memoryStore?.length > 0;
@ -71,7 +72,7 @@
{#each $memoryStore as memory, i (memory.title)} {#each $memoryStore as memory, i (memory.title)}
<button <button
class="memory-card relative mr-8 inline-block aspect-video h-[215px] rounded-xl" class="memory-card relative mr-8 inline-block aspect-video h-[215px] rounded-xl"
on:click={() => goto(`/memory?memory=${i}`)} on:click={() => goto(`${AppRoute.MEMORY}?${QueryParameter.MEMORY_INDEX}=${i}`)}
> >
<img <img
class="h-full w-full rounded-xl object-cover" class="h-full w-full rounded-xl object-cover"

View file

@ -8,6 +8,7 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { mdiCircleEditOutline, mdiContentCopy, mdiDelete, mdiOpenInNew } from '@mdi/js'; import { mdiCircleEditOutline, mdiContentCopy, mdiDelete, mdiOpenInNew } from '@mdi/js';
import noThumbnailUrl from '$lib/assets/no-thumbnail.png'; import noThumbnailUrl from '$lib/assets/no-thumbnail.png';
import { AppRoute } from '$lib/constants';
export let link: SharedLinkResponseDto; export let link: SharedLinkResponseDto;
@ -120,8 +121,8 @@
<div <div
class="hover:cursor-pointer" class="hover:cursor-pointer"
title="Go to share page" title="Go to share page"
on:click={() => goto(`/share/${link.key}`)} on:click={() => goto(`${AppRoute.SHARE}/${link.key}`)}
on:keydown={() => goto(`/share/${link.key}`)} on:keydown={() => goto(`${AppRoute.SHARE}/${link.key}`)}
> >
<Icon path={mdiOpenInNew} /> <Icon path={mdiOpenInNew} />
</div> </div>

View file

@ -14,6 +14,7 @@
import UserAPIKeyList from './user-api-key-list.svelte'; import UserAPIKeyList from './user-api-key-list.svelte';
import UserProfileSettings from './user-profile-settings.svelte'; import UserProfileSettings from './user-profile-settings.svelte';
import { user } from '$lib/stores/user.store'; import { user } from '$lib/stores/user.store';
import { OpenSettingQueryParameterValue, QueryParameter } from '$lib/constants';
import AppearanceSettings from './appearance-settings.svelte'; import AppearanceSettings from './appearance-settings.svelte';
import TrashSettings from './trash-settings.svelte'; import TrashSettings from './trash-settings.svelte';
@ -54,7 +55,8 @@
<SettingAccordion <SettingAccordion
title="OAuth" title="OAuth"
subtitle="Manage your OAuth connection" subtitle="Manage your OAuth connection"
isOpen={oauthOpen || $page.url.searchParams.get('open') === 'oauth'} isOpen={oauthOpen ||
$page.url.searchParams.get(QueryParameter.OPEN_SETTING) === OpenSettingQueryParameterValue.OAUTH}
> >
<OAuthSettings user={$user} /> <OAuthSettings user={$user} />
</SettingAccordion> </SettingAccordion>

View file

@ -24,6 +24,7 @@ export enum AppRoute {
PLACES = '/places', PLACES = '/places',
PHOTOS = '/photos', PHOTOS = '/photos',
EXPLORE = '/explore', EXPLORE = '/explore',
SHARE = '/share',
SHARING = '/sharing', SHARING = '/sharing',
SHARED_LINKS = '/sharing/sharedlinks', SHARED_LINKS = '/sharing/sharedlinks',
SEARCH = '/search', SEARCH = '/search',
@ -59,6 +60,32 @@ export const dateFormats = {
}, },
}; };
export enum QueryParameter {
ACTION = 'action',
ASSET_INDEX = 'assetIndex',
CLIP = 'clip',
MEMORY_INDEX = 'memoryIndex',
ONBOARDING_STEP = 'step',
OPEN_SETTING = 'openSetting',
QUERY = 'query',
PREVIOUS_ROUTE = 'previousRoute',
SEARCHED_PEOPLE = 'searchedPeople',
SEARCH_TERM = 'q',
}
export enum OpenSettingQueryParameterValue {
OAUTH = 'oauth',
JOB = 'job',
STORAGE_TEMPLATE = 'storageTemplate',
}
export enum ActionQueryParameterValue {
MERGE = 'merge',
}
export const maximumLengthSearchPeople: number = 20;
export const timeBeforeShowLoadingSpinner: number = 100;
// should be the same values as the ones in the app.html // should be the same values as the ones in the app.html
export enum Theme { export enum Theme {
LIGHT = 'light', LIGHT = 'light',

View file

@ -1,3 +1,7 @@
export interface ResetOptions { export interface ResetOptions {
default?: boolean; default?: boolean;
} }
export interface SearchOptions {
force?: boolean;
}

View file

@ -6,7 +6,13 @@
import Button from '$lib/components/elements/buttons/button.svelte'; import Button from '$lib/components/elements/buttons/button.svelte';
import { api, type PeopleUpdateItem, type PersonResponseDto } from '@api'; import { api, type PeopleUpdateItem, type PersonResponseDto } from '@api';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { AppRoute } from '$lib/constants'; import {
ActionQueryParameterValue,
AppRoute,
QueryParameter,
maximumLengthSearchPeople,
timeBeforeShowLoadingSpinner,
} from '$lib/constants';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { import {
NotificationType, NotificationType,
@ -22,16 +28,23 @@
import { shouldIgnoreShortcut } from '$lib/utils/shortcut'; import { shouldIgnoreShortcut } from '$lib/utils/shortcut';
import { mdiAccountOff, mdiEyeOutline } from '@mdi/js'; import { mdiAccountOff, mdiEyeOutline } from '@mdi/js';
import Icon from '$lib/components/elements/icon.svelte'; import Icon from '$lib/components/elements/icon.svelte';
import { searchNameLocal } from '$lib/utils/person';
import SearchBar from '$lib/components/faces-page/search-bar.svelte';
import { page } from '$app/stores';
export let data: PageData; export let data: PageData;
let selectHidden = false;
let initialHiddenValues: Record<string, boolean> = {};
let eyeColorMap: Record<string, 'black' | 'white'> = {};
let people = data.people.people; let people = data.people.people;
let countTotalPeople = data.people.total; let countTotalPeople = data.people.total;
let countVisiblePeople = data.people.visible;
let selectHidden = false;
let initialHiddenValues: Record<string, boolean> = {};
let eyeColorMap: Record<string, 'black' | 'white'> = {};
let searchedPeople: PersonResponseDto[] = [];
let searchName = '';
let searchWord: string;
let isSearchingPeople = false;
let showLoadingSpinner = false; let showLoadingSpinner = false;
let toggleVisibility = false; let toggleVisibility = false;
@ -47,14 +60,23 @@
let innerHeight: number; let innerHeight: number;
people.forEach((person: PersonResponseDto) => { for (const person of people) {
initialHiddenValues[person.id] = person.isHidden; initialHiddenValues[person.id] = person.isHidden;
}); }
$: searchedPeopleLocal = searchName ? searchNameLocal(searchName, searchedPeople, maximumLengthSearchPeople) : [];
$: countVisiblePeople = people.filter((person) => !person.isHidden).length;
const onKeyboardPress = (event: KeyboardEvent) => handleKeyboardPress(event); const onKeyboardPress = (event: KeyboardEvent) => handleKeyboardPress(event);
onMount(() => { onMount(() => {
document.addEventListener('keydown', onKeyboardPress); document.addEventListener('keydown', onKeyboardPress);
const getSearchedPeople = $page.url.searchParams.get(QueryParameter.SEARCHED_PEOPLE);
if (getSearchedPeople) {
searchName = getSearchedPeople;
searchPeople(true);
}
}); });
onDestroy(() => { onDestroy(() => {
@ -74,6 +96,12 @@
} }
}; };
const handleSearch = (force: boolean) => {
$page.url.searchParams.set(QueryParameter.SEARCHED_PEOPLE, searchName);
goto($page.url);
searchPeople(force);
};
const handleCloseClick = () => { const handleCloseClick = () => {
for (const person of people) { for (const person of people) {
person.isHidden = initialHiddenValues[person.id]; person.isHidden = initialHiddenValues[person.id];
@ -117,9 +145,6 @@
// Update the initial hidden values // Update the initial hidden values
initialHiddenValues[person.id] = person.isHidden; initialHiddenValues[person.id] = person.isHidden;
// Update the count of hidden/visible people
countVisiblePeople += person.isHidden ? -1 : 1;
} }
} }
@ -188,6 +213,7 @@
*/ */
try { try {
await api.personApi.updatePerson({ id: personToBeMergedIn.id, personUpdateDto: { name: personName } }); await api.personApi.updatePerson({ id: personToBeMergedIn.id, personUpdateDto: { name: personName } });
for (const person of people) { for (const person of people) {
if (person.id === personToBeMergedIn.id) { if (person.id === personToBeMergedIn.id) {
person.name = personName; person.name = personName;
@ -233,11 +259,9 @@
return person; return person;
}); });
people.forEach((person: PersonResponseDto) => { for (const person of people) {
initialHiddenValues[person.id] = person.isHidden; initialHiddenValues[person.id] = person.isHidden;
}); }
countVisiblePeople--;
showChangeNameModal = false; showChangeNameModal = false;
@ -251,7 +275,38 @@
}; };
const handleMergePeople = (detail: PersonResponseDto) => { const handleMergePeople = (detail: PersonResponseDto) => {
goto(`${AppRoute.PEOPLE}/${detail.id}?action=merge&previousRoute=${AppRoute.PEOPLE}`); goto(
`${AppRoute.PEOPLE}/${detail.id}?${QueryParameter.ACTION}=${ActionQueryParameterValue.MERGE}&${QueryParameter.PREVIOUS_ROUTE}=${AppRoute.PEOPLE}`,
);
};
const searchPeople = async (force: boolean) => {
if (searchName === '') {
if ($page.url.searchParams.has(QueryParameter.SEARCHED_PEOPLE)) {
$page.url.searchParams.delete(QueryParameter.SEARCHED_PEOPLE);
goto($page.url);
}
return;
}
if (!force) {
if (people.length < maximumLengthSearchPeople && searchName.startsWith(searchWord)) {
return;
}
}
const timeout = setTimeout(() => (isSearchingPeople = true), timeBeforeShowLoadingSpinner);
try {
const { data } = await api.searchApi.searchPerson({ name: searchName, withHidden: false });
searchedPeople = data;
searchWord = searchName;
} catch (error) {
handleError(error, "Can't search people");
} finally {
clearTimeout(timeout);
}
isSearchingPeople = false;
}; };
const submitNameChange = async () => { const submitNameChange = async () => {
@ -310,7 +365,6 @@
} }
return person; return person;
}); });
notificationController.show({ notificationController.show({
message: 'Date of birth saved succesfully', message: 'Date of birth saved succesfully',
type: NotificationType.Info, type: NotificationType.Info,
@ -332,14 +386,12 @@
id: edittingPerson.id, id: edittingPerson.id,
personUpdateDto: { name: personName }, personUpdateDto: { name: personName },
}); });
people = people.map((person: PersonResponseDto) => { people = people.map((person: PersonResponseDto) => {
if (person.id === updatedPerson.id) { if (person.id === updatedPerson.id) {
return updatedPerson; return updatedPerson;
} }
return person; return person;
}); });
notificationController.show({ notificationController.show({
message: 'Change name succesfully', message: 'Change name succesfully',
type: NotificationType.Info, type: NotificationType.Info,
@ -365,22 +417,36 @@
</FullScreenModal> </FullScreenModal>
{/if} {/if}
<UserPageLayout title="People"> <UserPageLayout title="People" description={countTotalPeople !== 0 ? `(${countTotalPeople.toString()})` : undefined}>
<svelte:fragment slot="buttons"> <svelte:fragment slot="buttons">
{#if countTotalPeople > 0} {#if countTotalPeople > 0}
<IconButton on:click={() => (selectHidden = !selectHidden)}> <div class="flex gap-2 items-center justify-center">
<div class="flex flex-wrap place-items-center justify-center gap-x-1 text-sm"> <div class="hidden sm:block">
<Icon path={mdiEyeOutline} size="18" /> <div class="w-40 lg:w-80 h-10">
<p class="ml-2">Show & hide people</p> <SearchBar
bind:name={searchName}
{isSearchingPeople}
on:reset={() => {
searchedPeople = [];
}}
on:search={({ detail }) => handleSearch(detail.force ?? false)}
/>
</div>
</div> </div>
</IconButton> <IconButton on:click={() => (selectHidden = !selectHidden)}>
<div class="flex flex-wrap place-items-center justify-center gap-x-1 text-sm">
<Icon path={mdiEyeOutline} size="18" />
<p class="ml-2">Show & hide people</p>
</div>
</IconButton>
</div>
{/if} {/if}
</svelte:fragment> </svelte:fragment>
{#if countVisiblePeople > 0} {#if countVisiblePeople > 0}
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 xl:grid-cols-7 2xl:grid-cols-9 gap-1"> <div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 xl:grid-cols-7 2xl:grid-cols-9 gap-1">
{#each people as person, idx (person.id)} {#each people as person, idx (person.id)}
{#if !person.isHidden} {#if !person.isHidden && (searchName ? searchedPeopleLocal.some((searchedPerson) => searchedPerson.id === person.id) : true)}
<PeopleCard <PeopleCard
{person} {person}
preload={idx < 20} preload={idx < 20}
@ -461,7 +527,7 @@
on:mouseleave={() => (eyeColorMap[person.id] = 'white')} on:mouseleave={() => (eyeColorMap[person.id] = 'white')}
> >
<ImageThumbnail <ImageThumbnail
preload={idx < 20} preload={searchName !== '' || idx < 20}
bind:hidden={person.isHidden} bind:hidden={person.isHidden}
shadow shadow
url={api.getPeopleThumbnailUrl(person.id)} url={api.getPeopleThumbnailUrl(person.id)}

View file

@ -25,7 +25,7 @@
NotificationType, NotificationType,
notificationController, notificationController,
} from '$lib/components/shared-components/notification/notification'; } from '$lib/components/shared-components/notification/notification';
import { AppRoute } from '$lib/constants'; import { AppRoute, QueryParameter, maximumLengthSearchPeople, timeBeforeShowLoadingSpinner } from '$lib/constants';
import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store'; import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store';
import { AssetStore } from '$lib/stores/assets.store'; import { AssetStore } from '$lib/stores/assets.store';
import { websocketStore } from '$lib/stores/websocket'; import { websocketStore } from '$lib/stores/websocket';
@ -90,10 +90,10 @@
let isSearchingPeople = false; let isSearchingPeople = false;
const searchPeople = async () => { const searchPeople = async () => {
if ((people.length < 20 && name.startsWith(searchWord)) || name === '') { if ((people.length < maximumLengthSearchPeople && name.startsWith(searchWord)) || name === '') {
return; return;
} }
const timeout = setTimeout(() => (isSearchingPeople = true), 100); const timeout = setTimeout(() => (isSearchingPeople = true), timeBeforeShowLoadingSpinner);
try { try {
const { data } = await api.searchApi.searchPerson({ name }); const { data } = await api.searchApi.searchPerson({ name });
people = data; people = data;
@ -120,8 +120,8 @@
} }
onMount(() => { onMount(() => {
const action = $page.url.searchParams.get('action'); const action = $page.url.searchParams.get(QueryParameter.ACTION);
const getPreviousRoute = $page.url.searchParams.get('previousRoute'); const getPreviousRoute = $page.url.searchParams.get(QueryParameter.PREVIOUS_ROUTE);
if (getPreviousRoute && !isExternalUrl(getPreviousRoute)) { if (getPreviousRoute && !isExternalUrl(getPreviousRoute)) {
previousRoute = getPreviousRoute; previousRoute = getPreviousRoute;
} }
@ -343,8 +343,8 @@
const handleGoBack = () => { const handleGoBack = () => {
viewMode = ViewMode.VIEW_ASSETS; viewMode = ViewMode.VIEW_ASSETS;
if ($page.url.searchParams.has('action')) { if ($page.url.searchParams.has(QueryParameter.ACTION)) {
$page.url.searchParams.delete('action'); $page.url.searchParams.delete(QueryParameter.ACTION);
goto($page.url); goto($page.url);
} }
}; };

View file

@ -18,7 +18,7 @@
import type { PageData } from './$types'; import type { PageData } from './$types';
import Icon from '$lib/components/elements/icon.svelte'; import Icon from '$lib/components/elements/icon.svelte';
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import { AppRoute } from '$lib/constants'; import { AppRoute, QueryParameter } from '$lib/constants';
import AlbumCard from '$lib/components/album-page/album-card.svelte'; import AlbumCard from '$lib/components/album-page/album-card.svelte';
import { flip } from 'svelte/animate'; import { flip } from 'svelte/animate';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
@ -86,8 +86,8 @@
}); });
$: term = (() => { $: term = (() => {
let term = $page.url.searchParams.get('q') || data.term || ''; let term = $page.url.searchParams.get(QueryParameter.SEARCH_TERM) || data.term || '';
const isMetadataSearch = $page.url.searchParams.get('clip') === 'false'; const isMetadataSearch = $page.url.searchParams.get(QueryParameter.CLIP) === 'false';
if (isMetadataSearch && term !== '') { if (isMetadataSearch && term !== '') {
term = `m:${term}`; term = `m:${term}`;
} }

View file

@ -1,11 +1,13 @@
import { authenticate } from '$lib/utils/auth'; import { authenticate } from '$lib/utils/auth';
import { type SearchResponseDto, api } from '@api'; import { type SearchResponseDto, api } from '@api';
import type { PageLoad } from './$types'; import type { PageLoad } from './$types';
import { QueryParameter } from '$lib/constants';
export const load = (async (data) => { export const load = (async (data) => {
await authenticate(); await authenticate();
const url = new URL(data.url.href); const url = new URL(data.url.href);
const term = url.searchParams.get('q') || url.searchParams.get('query') || undefined; const term =
url.searchParams.get(QueryParameter.SEARCH_TERM) || url.searchParams.get(QueryParameter.QUERY) || undefined;
let results: SearchResponseDto | null = null; let results: SearchResponseDto | null = null;
if (term) { if (term) {
const { data } = await api.searchApi.search({}, { params: url.searchParams }); const { data } = await api.searchApi.search({}, { params: url.searchParams });

View file

@ -2,6 +2,7 @@
import AssetViewer from '$lib/components/asset-viewer/asset-viewer.svelte'; import AssetViewer from '$lib/components/asset-viewer/asset-viewer.svelte';
import type { PageData } from './$types'; import type { PageData } from './$types';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { AppRoute } from '$lib/constants';
export let data: PageData; export let data: PageData;
</script> </script>
@ -11,6 +12,6 @@
showNavigation={false} showNavigation={false}
on:previous={() => null} on:previous={() => null}
on:next={() => null} on:next={() => null}
on:close={() => goto(`/share/${data.key}`)} on:close={() => goto(`${AppRoute.SHARE}/${data.key}`)}
/> />
{/if} {/if}

View file

@ -4,7 +4,7 @@
import OnboadingStorageTemplate from '$lib/components/onboarding-page/onboarding-storage-template.svelte'; import OnboadingStorageTemplate from '$lib/components/onboarding-page/onboarding-storage-template.svelte';
import { api } from '@api'; import { api } from '@api';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { AppRoute } from '$lib/constants'; import { AppRoute, QueryParameter } from '$lib/constants';
import { page } from '$app/stores'; import { page } from '$app/stores';
let index = 0; let index = 0;
@ -32,14 +32,14 @@
goto(AppRoute.PHOTOS); goto(AppRoute.PHOTOS);
} else { } else {
index++; index++;
goto(`${AppRoute.AUTH_ONBOARDING}?step=${onboardingSteps[index].name}`); goto(`${AppRoute.AUTH_ONBOARDING}?${QueryParameter.ONBOARDING_STEP}=${onboardingSteps[index].name}`);
} }
}; };
const handlePrevious = () => { const handlePrevious = () => {
if (index >= 1) { if (index >= 1) {
index--; index--;
goto(`${AppRoute.AUTH_ONBOARDING}?step=${onboardingSteps[index].name}`); goto(`${AppRoute.AUTH_ONBOARDING}?${QueryParameter.ONBOARDING_STEP}=${onboardingSteps[index].name}`);
} }
}; };
</script> </script>