mirror of
https://github.com/maxdorninger/MediaManager.git
synced 2026-04-17 15:43:28 +02:00
Address various different fixes regarding search UI experience.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
services:
|
||||
mediamanager:
|
||||
container_name: mediamanager_server
|
||||
image: ghcr.io/maxdorninger/mediamanager/mediamanager:latest
|
||||
ports:
|
||||
- "8000:8000"
|
||||
@@ -16,6 +17,7 @@ services:
|
||||
db:
|
||||
condition: service_healthy
|
||||
db:
|
||||
container_name: mediamanager_postgres
|
||||
image: postgres:17
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
|
||||
@@ -10,3 +10,4 @@ class MetaDataProviderSearchResult(BaseModel):
|
||||
metadata_provider: str
|
||||
added: bool
|
||||
vote_average: float | None = None
|
||||
id: str | None = None # Internal ID if already added
|
||||
|
||||
@@ -228,6 +228,15 @@ class MovieService:
|
||||
external_id=result.external_id, metadata_provider=metadata_provider.name
|
||||
):
|
||||
result.added = True
|
||||
|
||||
# Fetch the internal movie ID.
|
||||
try:
|
||||
movie = self.movie_repository.get_movie_by_external_id(
|
||||
external_id=result.external_id, metadata_provider=metadata_provider.name
|
||||
)
|
||||
result.id = str(movie.id)
|
||||
except Exception:
|
||||
log.error(f"Unable to find internal movie ID for {result.external_id} on {metadata_provider.name}")
|
||||
return results
|
||||
|
||||
def get_popular_movies(
|
||||
|
||||
@@ -241,6 +241,15 @@ class TvService:
|
||||
external_id=result.external_id, metadata_provider=metadata_provider.name
|
||||
):
|
||||
result.added = True
|
||||
|
||||
# Fetch the internal show ID.
|
||||
try:
|
||||
show = self.tv_repository.get_show_by_external_id(
|
||||
external_id=result.external_id, metadata_provider=metadata_provider.name
|
||||
)
|
||||
result.id = str(show.id)
|
||||
except Exception:
|
||||
log.error(f"Unable to find internal show ID for {result.external_id} on {metadata_provider.name}")
|
||||
return results
|
||||
|
||||
def get_popular_shows(
|
||||
|
||||
8296
web/src/lib/api/api.d.ts
vendored
8296
web/src/lib/api/api.d.ts
vendored
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import * as Card from '$lib/components/ui/card/index.js';
|
||||
import { ImageOff } from 'lucide-svelte';
|
||||
import { ImageOff, LoaderCircle } from 'lucide-svelte';
|
||||
import { goto, invalidateAll } from '$app/navigation';
|
||||
import { resolve } from '$app/paths';
|
||||
import type { components } from '$lib/api/api';
|
||||
@@ -76,17 +76,29 @@
|
||||
{/if}
|
||||
</Card.Content>
|
||||
<Card.Footer class="flex flex-col items-start gap-2 rounded-b-lg border-t bg-card p-4">
|
||||
<Button
|
||||
class="w-full font-semibold"
|
||||
disabled={result.added || loading}
|
||||
onclick={() => addMedia()}
|
||||
>
|
||||
{#if loading}
|
||||
<span class="animate-pulse">Loading...</span>
|
||||
{:else}
|
||||
{result.added ? 'Show already exists' : `Add ${isShow ? 'Show' : 'Movie'}`}
|
||||
{/if}
|
||||
</Button>
|
||||
{#if result.added}
|
||||
<Button
|
||||
class="w-full font-semibold"
|
||||
variant="secondary"
|
||||
href={resolve(isShow ? '/dashboard/tv/[showId]' : '/dashboard/movies/[movieId]',
|
||||
isShow ? { showId: result.id ?? '' } : { movieId: result.id ?? '' })}
|
||||
>
|
||||
{isShow ? 'Show already exists' : 'Movie already exists'}
|
||||
</Button>
|
||||
{:else}
|
||||
<Button
|
||||
class="w-full font-semibold"
|
||||
disabled={loading}
|
||||
onclick={() => addMedia()}
|
||||
>
|
||||
{#if loading}
|
||||
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
|
||||
<span class="animate-pulse">Loading...</span>
|
||||
{:else}
|
||||
{`Add ${isShow ? 'Show' : 'Movie'}`}
|
||||
{/if}
|
||||
</Button>
|
||||
{/if}
|
||||
<div class="flex w-full items-center gap-2">
|
||||
{#if result.vote_average != null}
|
||||
<span class="flex items-center text-sm font-medium text-yellow-600">
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { type VariantProps, tv } from 'tailwind-variants';
|
||||
|
||||
export const buttonVariants = tv({
|
||||
base: 'focus-visible:ring-ring inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
||||
base: 'focus-visible:ring-ring inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 cursor-pointer',
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground hover:bg-primary/90 shadow',
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { ChevronDown } from 'lucide-svelte';
|
||||
import { ChevronDown, LoaderCircle } from 'lucide-svelte';
|
||||
import * as Collapsible from '$lib/components/ui/collapsible/index.js';
|
||||
import * as RadioGroup from '$lib/components/ui/radio-group/index.js';
|
||||
import AddMediaCard from '$lib/components/add-media-card.svelte';
|
||||
@@ -18,29 +18,35 @@
|
||||
let searchTerm: string = $state('');
|
||||
let metadataProvider: 'tmdb' | 'tvdb' = $state('tmdb');
|
||||
let results: components['schemas']['MetaDataProviderSearchResult'][] | null = $state(null);
|
||||
let isSearching: boolean = $state(false);
|
||||
|
||||
onMount(() => {
|
||||
search('');
|
||||
});
|
||||
|
||||
async function search(query: string) {
|
||||
const { data } =
|
||||
query.length > 0
|
||||
? await client.GET('/api/v1/movies/search', {
|
||||
params: {
|
||||
query: {
|
||||
query: query,
|
||||
metadata_provider: metadataProvider
|
||||
isSearching = true;
|
||||
try {
|
||||
const { data } =
|
||||
query.length > 0
|
||||
? await client.GET('/api/v1/movies/search', {
|
||||
params: {
|
||||
query: {
|
||||
query: query,
|
||||
metadata_provider: metadataProvider
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
: await client.GET('/api/v1/movies/recommended');
|
||||
if (data && data.length > 0) {
|
||||
results = data as components['schemas']['MetaDataProviderSearchResult'][];
|
||||
} else {
|
||||
results = null;
|
||||
})
|
||||
: await client.GET('/api/v1/movies/recommended');
|
||||
if (data && data.length > 0) {
|
||||
results = data as components['schemas']['MetaDataProviderSearchResult'][];
|
||||
} else {
|
||||
results = null;
|
||||
}
|
||||
handleQueryNotificationToast(data?.length ?? 0, query);
|
||||
} finally {
|
||||
isSearching = false;
|
||||
}
|
||||
handleQueryNotificationToast(data?.length ?? 0, query);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -82,7 +88,17 @@
|
||||
</h1>
|
||||
<section>
|
||||
<Label for="search-box">Movie Name</Label>
|
||||
<Input bind:value={searchTerm} id="search-box" placeholder="Show Name" type="text" />
|
||||
<Input
|
||||
bind:value={searchTerm}
|
||||
id="search-box"
|
||||
placeholder="Movie Name"
|
||||
type="text"
|
||||
onkeydown={(e) => {
|
||||
if (e.key === 'Enter' && !isSearching) {
|
||||
search(searchTerm);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<p class="text-sm text-muted-foreground">Search for a Movie to add.</p>
|
||||
</section>
|
||||
<section>
|
||||
@@ -112,14 +128,21 @@
|
||||
</Collapsible.Root>
|
||||
</section>
|
||||
<section>
|
||||
<Button onclick={() => search(searchTerm)} type="submit">Search</Button>
|
||||
<Button onclick={() => search(searchTerm)} type="submit" disabled={isSearching}>
|
||||
{#if isSearching}
|
||||
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
|
||||
<span class="animate-pulse">Searching...</span>
|
||||
{:else}
|
||||
Search
|
||||
{/if}
|
||||
</Button>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<Separator class="my-8" />
|
||||
|
||||
{#if results && results.length === 0}
|
||||
<h3 class="mx-auto">No Shows found.</h3>
|
||||
<h3 class="mx-auto">No Movie found.</h3>
|
||||
{:else if results}
|
||||
<div
|
||||
class="grid w-full auto-rows-min gap-4 sm:grid-cols-1
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { ChevronDown } from 'lucide-svelte';
|
||||
import { ChevronDown, LoaderCircle } from 'lucide-svelte';
|
||||
import * as Collapsible from '$lib/components/ui/collapsible/index.js';
|
||||
import * as RadioGroup from '$lib/components/ui/radio-group/index.js';
|
||||
import AddMediaCard from '$lib/components/add-media-card.svelte';
|
||||
@@ -14,6 +14,7 @@
|
||||
let searchTerm: string = $state('');
|
||||
let metadataProvider: 'tmdb' | 'tvdb' = $state('tmdb');
|
||||
let data: components['schemas']['MetaDataProviderSearchResult'][] | null = $state(null);
|
||||
let isSearching: boolean = $state(false);
|
||||
import { resolve } from '$app/paths';
|
||||
import client from '$lib/api';
|
||||
import type { components } from '$lib/api/api';
|
||||
@@ -24,23 +25,28 @@
|
||||
});
|
||||
|
||||
async function search(query: string) {
|
||||
const results =
|
||||
query.length > 0
|
||||
? await client.GET('/api/v1/tv/search', {
|
||||
params: {
|
||||
query: {
|
||||
query: query,
|
||||
metadata_provider: metadataProvider
|
||||
isSearching = true;
|
||||
try {
|
||||
const results =
|
||||
query.length > 0
|
||||
? await client.GET('/api/v1/tv/search', {
|
||||
params: {
|
||||
query: {
|
||||
query: query,
|
||||
metadata_provider: metadataProvider
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
: await client.GET('/api/v1/tv/recommended');
|
||||
if (results.data && results.data.length > 0) {
|
||||
handleQueryNotificationToast(results.data.length, query);
|
||||
data = results.data as components['schemas']['MetaDataProviderSearchResult'][];
|
||||
} else {
|
||||
handleQueryNotificationToast(0, query);
|
||||
data = null;
|
||||
})
|
||||
: await client.GET('/api/v1/tv/recommended');
|
||||
if (results.data && results.data.length > 0) {
|
||||
handleQueryNotificationToast(results.data.length, query);
|
||||
data = results.data as components['schemas']['MetaDataProviderSearchResult'][];
|
||||
} else {
|
||||
handleQueryNotificationToast(0, query);
|
||||
data = null;
|
||||
}
|
||||
} finally {
|
||||
isSearching = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -83,7 +89,17 @@
|
||||
</h1>
|
||||
<section>
|
||||
<Label for="search-box">Show Name</Label>
|
||||
<Input bind:value={searchTerm} id="search-box" placeholder="Show Name" type="text" />
|
||||
<Input
|
||||
bind:value={searchTerm}
|
||||
id="search-box"
|
||||
placeholder="Show Name"
|
||||
type="text"
|
||||
onkeydown={(e) => {
|
||||
if (e.key === 'Enter' && !isSearching) {
|
||||
search(searchTerm);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<p class="text-sm text-muted-foreground">Search for a Show to add.</p>
|
||||
</section>
|
||||
<section>
|
||||
@@ -113,7 +129,14 @@
|
||||
</Collapsible.Root>
|
||||
</section>
|
||||
<section>
|
||||
<Button onclick={() => search(searchTerm)} type="submit">Search</Button>
|
||||
<Button onclick={() => search(searchTerm)} type="submit" disabled={isSearching}>
|
||||
{#if isSearching}
|
||||
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
|
||||
<span class="animate-pulse">Searching...</span>
|
||||
{:else}
|
||||
Search
|
||||
{/if}
|
||||
</Button>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user