mirror of
https://github.com/KevinMidboe/immich.git
synced 2025-10-29 17:40:28 +00:00
feat(web,server): explore (#1926)
* feat: explore * chore: generate open api * styling explore page * styling no result page * style overlay * style: bluring text on thumbnail card for readability * explore page tweaks * fix(web): search urls * feat(web): use objects for things * feat(server): filter by motion, sort by createdAt * More styling * better navigation --------- Co-authored-by: Alex Tran <alex.tran1502@gmail.com> Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
This commit is contained in:
13
web/src/routes/(user)/explore/+page.server.ts
Normal file
13
web/src/routes/(user)/explore/+page.server.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load = (async ({ locals, parent }) => {
|
||||
const { user } = await parent();
|
||||
if (!user) {
|
||||
throw redirect(302, '/auth/login');
|
||||
}
|
||||
|
||||
const { data: items } = await locals.api.searchApi.getExploreData();
|
||||
|
||||
return { user, items };
|
||||
}) satisfies PageServerLoad;
|
||||
173
web/src/routes/(user)/explore/+page.svelte
Normal file
173
web/src/routes/(user)/explore/+page.svelte
Normal file
@@ -0,0 +1,173 @@
|
||||
<script lang="ts">
|
||||
import ImmichThumbnail from '$lib/components/shared-components/immich-thumbnail.svelte';
|
||||
import NavigationBar from '$lib/components/shared-components/navigation-bar/navigation-bar.svelte';
|
||||
import SideBar from '$lib/components/shared-components/side-bar/side-bar.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { AssetTypeEnum, SearchExploreItem } from '@api';
|
||||
import ClockOutline from 'svelte-material-icons/ClockOutline.svelte';
|
||||
import MotionPlayOutline from 'svelte-material-icons/MotionPlayOutline.svelte';
|
||||
import PlayCircleOutline from 'svelte-material-icons/PlayCircleOutline.svelte';
|
||||
import StarOutline from 'svelte-material-icons/StarOutline.svelte';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
enum Field {
|
||||
CITY = 'exifInfo.city',
|
||||
TAGS = 'smartInfo.tags',
|
||||
OBJECTS = 'smartInfo.objects'
|
||||
}
|
||||
|
||||
const MAX_ITEMS = 12;
|
||||
|
||||
let things: SearchExploreItem[] = [];
|
||||
let places: SearchExploreItem[] = [];
|
||||
|
||||
for (const item of data.items) {
|
||||
switch (item.fieldName) {
|
||||
case Field.OBJECTS:
|
||||
things = item.items;
|
||||
break;
|
||||
|
||||
case Field.CITY:
|
||||
places = item.items;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
things = things.slice(0, MAX_ITEMS);
|
||||
places = places.slice(0, MAX_ITEMS);
|
||||
</script>
|
||||
|
||||
<section>
|
||||
<NavigationBar user={data.user} shouldShowUploadButton={false} />
|
||||
</section>
|
||||
|
||||
<section
|
||||
class="grid grid-cols-[250px_auto] relative pt-[72px] h-screen bg-immich-bg dark:bg-immich-dark-bg"
|
||||
>
|
||||
<SideBar />
|
||||
|
||||
<section class="overflow-y-auto relative immich-scrollbar">
|
||||
<section
|
||||
id="album-content"
|
||||
class="relative pt-8 pl-4 mb-12 bg-immich-bg dark:bg-immich-dark-bg"
|
||||
>
|
||||
<!-- Main Section -->
|
||||
<div class="px-4 flex justify-between place-items-center dark:text-immich-dark-fg">
|
||||
<div>
|
||||
<p class="font-medium">Explore</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="my-4">
|
||||
<hr class="dark:border-immich-dark-gray" />
|
||||
</div>
|
||||
|
||||
<div class="mx-4 flex flex-col">
|
||||
{#if places.length > 0}
|
||||
<div class="mb-6 mt-2">
|
||||
<div>
|
||||
<p class="mb-4 dark:text-immich-dark-fg font-medium">Places</p>
|
||||
</div>
|
||||
<div class="flex flex-row flex-wrap gap-4">
|
||||
{#each places as item}
|
||||
<a class="relative" href="/search?{Field.CITY}={item.value}" draggable="false">
|
||||
<div class="filter brightness-75 rounded-xl overflow-hidden">
|
||||
<ImmichThumbnail
|
||||
isRoundedCorner={true}
|
||||
thumbnailSize={156}
|
||||
asset={item.data}
|
||||
readonly={true}
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
class="capitalize absolute bottom-2 w-full text-center text-sm font-medium text-white text-ellipsis w-100 px-1 hover:cursor-pointer backdrop-blur-[1px]"
|
||||
>
|
||||
{item.value}
|
||||
</span>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if things.length > 0}
|
||||
<div class="mb-6 mt-2">
|
||||
<div>
|
||||
<p class="mb-4 dark:text-immich-dark-fg font-medium">Things</p>
|
||||
</div>
|
||||
<div class="flex flex-row flex-wrap gap-4">
|
||||
{#each things as item}
|
||||
<a class="relative" href="/search?{Field.OBJECTS}={item.value}" draggable="false">
|
||||
<div class="filter brightness-75 rounded-xl overflow-hidden">
|
||||
<ImmichThumbnail
|
||||
isRoundedCorner={true}
|
||||
thumbnailSize={156}
|
||||
asset={item.data}
|
||||
readonly={true}
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
class="capitalize absolute bottom-2 w-full text-center text-sm font-medium text-white text-ellipsis w-100 px-1 hover:cursor-pointer backdrop-blur-[1px]"
|
||||
>
|
||||
{item.value}
|
||||
</span>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<hr class="dark:border-immich-dark-gray mb-4" />
|
||||
|
||||
<div
|
||||
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-8"
|
||||
>
|
||||
<div class="flex flex-col gap-6 dark:text-immich-dark-fg">
|
||||
<p class="text-sm">YOUR ACTIVITY</p>
|
||||
<div class="flex flex-col gap-4 dark:text-immich-dark-fg/80">
|
||||
<a
|
||||
href={AppRoute.FAVORITES}
|
||||
class="w-full flex text-sm font-medium hover:text-immich-primary dark:hover:text-immich-dark-primary content-center gap-2"
|
||||
draggable="false"
|
||||
>
|
||||
<StarOutline size={24} />
|
||||
<span>Favorites</span>
|
||||
</a>
|
||||
<a
|
||||
href="/search?recent=true"
|
||||
class="w-full flex text-sm font-medium hover:text-immich-primary dark:hover:text-immich-dark-primary content-center gap-2"
|
||||
draggable="false"
|
||||
>
|
||||
<ClockOutline size={24} />
|
||||
<span>Recently added</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-6 dark:text-immich-dark-fg">
|
||||
<p class="text-sm">CATEGORIES</p>
|
||||
<div class="flex flex-col gap-4 dark:text-immich-dark-fg/80">
|
||||
<a
|
||||
href="/search?type={AssetTypeEnum.Video}"
|
||||
class="w-full flex text-sm font-medium hover:text-immich-primary dark:hover:text-immich-dark-primary items-center gap-2"
|
||||
>
|
||||
<PlayCircleOutline size={24} />
|
||||
<span>Videos</span>
|
||||
</a>
|
||||
<div>
|
||||
<a
|
||||
href="/search?motion=true"
|
||||
class="w-full flex text-sm font-medium hover:text-immich-primary dark:hover:text-immich-dark-primary items-center gap-2"
|
||||
>
|
||||
<MotionPlayOutline size={24} />
|
||||
<span>Motion photos</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</section>
|
||||
@@ -8,7 +8,6 @@ export const load = (async ({ locals, parent, url }) => {
|
||||
}
|
||||
|
||||
const term = url.searchParams.get('q') || undefined;
|
||||
|
||||
const { data: results } = await locals.api.searchApi.search(
|
||||
term,
|
||||
undefined,
|
||||
@@ -20,6 +19,8 @@ export const load = (async ({ locals, parent, url }) => {
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
{ params: url.searchParams }
|
||||
);
|
||||
return { user, term, results };
|
||||
|
||||
@@ -1,16 +1,34 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
|
||||
import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte';
|
||||
import NavigationBar from '$lib/components/shared-components/navigation-bar/navigation-bar.svelte';
|
||||
import type { PageData } from './$types';
|
||||
import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte';
|
||||
import ImageOffOutline from 'svelte-material-icons/ImageOffOutline.svelte';
|
||||
import { afterNavigate, goto } from '$app/navigation';
|
||||
|
||||
export let data: PageData;
|
||||
const term = $page.url.searchParams.get('q') || data.term || '';
|
||||
|
||||
const term = $page.url.searchParams.get('q') || '';
|
||||
let goBackRoute = '/explore';
|
||||
afterNavigate((r) => {
|
||||
if (r.from) {
|
||||
goBackRoute = r.from.url.href;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<section>
|
||||
<NavigationBar {term} user={data.user} shouldShowUploadButton={false} />
|
||||
<ControlAppBar on:close-button-click={() => goto(goBackRoute)} backIcon={ArrowLeft}>
|
||||
<svelte:fragment slot="leading">
|
||||
<p class="text-xl capitalize">
|
||||
Search
|
||||
{#if term}
|
||||
- {term}
|
||||
{/if}
|
||||
</p>
|
||||
</svelte:fragment>
|
||||
</ControlAppBar>
|
||||
</section>
|
||||
|
||||
<section class="relative pt-[72px] h-screen bg-immich-bg dark:bg-immich-dark-bg">
|
||||
@@ -19,8 +37,16 @@
|
||||
id="search-content"
|
||||
class="relative pt-8 pl-4 mb-12 bg-immich-bg dark:bg-immich-dark-bg"
|
||||
>
|
||||
{#if data.results?.assets?.items}
|
||||
{#if data.results?.assets?.items.length != 0}
|
||||
<GalleryViewer assets={data.results.assets.items} />
|
||||
{:else}
|
||||
<div class="w-full text-center dark:text-white ">
|
||||
<div class="mt-60 flex flex-col place-content-center place-items-center">
|
||||
<ImageOffOutline size="56" />
|
||||
<p class="font-medium text-3xl mt-5">No results</p>
|
||||
<p class="text-base font-normal">Try a synonym or more general keyword</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user