v1.2 - collections, etc

This commit is contained in:
2026-04-25 22:57:08 +12:00
parent e68a8b1622
commit cd563d67b6
28 changed files with 20276 additions and 270 deletions
+73 -33
View File
@@ -2,7 +2,7 @@
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
export let config = { embyUrl: '', apiKey: '', dbPath: '' };
export let config = { embyUrl: '', apiKey: '', tmdbApiKey: '', dbPath: '' };
let localConfig = { ...config };
let status = '';
@@ -14,6 +14,19 @@
statusType = type;
}
async function fetchEmbyUsers() {
const res = await fetch('/api/emby-users');
if (!res.ok) {
const err = await res.json().catch(() => ({ message: res.statusText }));
throw new Error(err.message || res.statusText);
}
const payload = await res.json();
return Array.isArray(payload)
? { users: payload, source: 'live', lastSyncedAt: null, message: '' }
: payload;
}
async function saveConfig() {
busy = true;
setStatus('Saving…');
@@ -48,13 +61,15 @@
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(localConfig)
});
const res = await fetch('/api/emby-users');
if (!res.ok) {
const err = await res.json().catch(() => ({ message: res.statusText }));
throw new Error(err.message || res.statusText);
const payload = await fetchEmbyUsers();
if (payload.source === 'cache') {
setStatus(
`${payload.message} Loaded ${payload.users.length} cached users from ${payload.lastSyncedAt || 'the last successful sync'}.`,
'ok'
);
} else {
setStatus(`Connected — ${payload.users.length} users found and cached locally.`, 'ok');
}
const users = await res.json();
setStatus(`Connected — ${users.length} users found.`, 'ok');
} catch (e) {
setStatus(`Connection failed: ${e.message}`, 'error');
} finally {
@@ -66,14 +81,16 @@
busy = true;
setStatus('Fetching user names from Emby…');
try {
const res = await fetch('/api/emby-users');
if (!res.ok) {
const err = await res.json().catch(() => ({ message: res.statusText }));
throw new Error(err.message || res.statusText);
const payload = await fetchEmbyUsers();
dispatch('namesRefreshed', payload);
if (payload.source === 'cache') {
setStatus(
`${payload.message} Refreshed ${payload.users.length} users from the local cache.`,
'ok'
);
} else {
setStatus(`Names refreshed — ${payload.users.length} users from Emby and saved locally.`, 'ok');
}
const embyUsers = await res.json();
dispatch('namesRefreshed', embyUsers);
setStatus(`Names refreshed — ${embyUsers.length} users from Emby.`, 'ok');
} catch (e) {
setStatus(`Failed: ${e.message}`, 'error');
} finally {
@@ -148,6 +165,25 @@
</div>
</section>
<section>
<h4>Recommendations</h4>
<label>
<span>TMDB API key</span>
<input
type="password"
bind:value={localConfig.tmdbApiKey}
placeholder="Paste your TMDB v3 API key"
/>
</label>
<p class="hint">
Stored locally in the app config so recommendation features can reuse it without re-entering
the key each time.
</p>
<div class="row">
<button class="btn ghost" on:click={saveConfig} disabled={busy}>Save TMDB key</button>
</div>
</section>
<section>
<h4>Database file</h4>
<label>
@@ -182,9 +218,9 @@
padding: 0 4px;
}
h3 {
font-size: 15px;
font-size: 18px;
font-weight: 700;
margin: 0 0 16px;
margin: 0 0 18px;
color: var(--text);
}
h4 {
@@ -196,12 +232,15 @@
margin: 0 0 10px;
}
section {
margin-bottom: 24px;
padding-bottom: 20px;
border-bottom: 1px solid var(--border);
margin-bottom: 18px;
padding: 16px;
border: 1px solid var(--border);
border-radius: 18px;
background: var(--surface);
box-shadow: 0 16px 32px rgba(0, 0, 0, 0.16);
}
section:last-of-type {
border-bottom: none;
margin-bottom: 12px;
}
label {
display: flex;
@@ -215,10 +254,10 @@
font-weight: 500;
}
input {
background: var(--bg);
background: #0b0f14;
border: 1px solid var(--border);
border-radius: 6px;
padding: 7px 10px;
border-radius: 12px;
padding: 10px 12px;
font-size: 13px;
color: var(--text);
font-family: inherit;
@@ -227,7 +266,7 @@
}
input:focus {
outline: none;
border-color: var(--accent);
border-color: var(--border-strong);
}
.hint {
font-size: 11px;
@@ -237,9 +276,9 @@
}
code {
font-size: 11px;
background: var(--bg);
padding: 1px 4px;
border-radius: 3px;
background: #0b0f14;
padding: 2px 5px;
border-radius: 6px;
}
.row {
display: flex;
@@ -249,16 +288,17 @@
.btn {
all: unset;
cursor: pointer;
padding: 6px 14px;
border-radius: 6px;
padding: 9px 14px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
font-family: inherit;
transition: all 0.12s;
}
.btn.ghost {
color: var(--text-muted);
color: #d9e7f8;
border: 1px solid var(--border);
background: var(--bg-secondary);
}
.btn.ghost:hover:not(:disabled) {
background: var(--surface-hover);
@@ -277,10 +317,10 @@
}
.status {
margin-top: 8px;
padding: 8px 12px;
border-radius: 6px;
padding: 10px 12px;
border-radius: 14px;
font-size: 12px;
background: var(--surface-hover);
background: var(--surface);
color: var(--text-muted);
border-left: 3px solid var(--border);
}