v1
This commit is contained in:
@@ -0,0 +1,295 @@
|
||||
<script>
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let config = { embyUrl: '', apiKey: '', dbPath: '' };
|
||||
|
||||
let localConfig = { ...config };
|
||||
let status = '';
|
||||
let statusType = ''; // 'ok' | 'error' | 'info'
|
||||
let busy = false;
|
||||
|
||||
function setStatus(msg, type = 'info') {
|
||||
status = msg;
|
||||
statusType = type;
|
||||
}
|
||||
|
||||
async function saveConfig() {
|
||||
busy = true;
|
||||
setStatus('Saving…');
|
||||
try {
|
||||
const res = await fetch('/api/config', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(localConfig)
|
||||
});
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
config = { ...localConfig };
|
||||
dispatch('configSaved', { ...localConfig });
|
||||
setStatus('Settings saved.', 'ok');
|
||||
} catch (e) {
|
||||
setStatus(`Save failed: ${e.message}`, 'error');
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function testConnection() {
|
||||
if (!localConfig.embyUrl || !localConfig.apiKey) {
|
||||
setStatus('Enter Emby URL and API key first.', 'error');
|
||||
return;
|
||||
}
|
||||
busy = true;
|
||||
setStatus('Connecting to Emby…');
|
||||
try {
|
||||
// Save config first so the server endpoint can read it
|
||||
await fetch('/api/config', {
|
||||
method: 'POST',
|
||||
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 users = await res.json();
|
||||
setStatus(`Connected — ${users.length} users found.`, 'ok');
|
||||
} catch (e) {
|
||||
setStatus(`Connection failed: ${e.message}`, 'error');
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshNames() {
|
||||
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 embyUsers = await res.json();
|
||||
dispatch('namesRefreshed', embyUsers);
|
||||
setStatus(`Names refreshed — ${embyUsers.length} users from Emby.`, 'ok');
|
||||
} catch (e) {
|
||||
setStatus(`Failed: ${e.message}`, 'error');
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadFromDb() {
|
||||
if (!localConfig.dbPath) {
|
||||
setStatus('Enter the DB path first.', 'error');
|
||||
return;
|
||||
}
|
||||
busy = true;
|
||||
setStatus('Reading database…');
|
||||
try {
|
||||
const res = await fetch('/api/db-read', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ dbPath: localConfig.dbPath })
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({ message: res.statusText }));
|
||||
throw new Error(err.message || res.statusText);
|
||||
}
|
||||
const { users, validation } = await res.json();
|
||||
dispatch('usersLoaded', { users, validation });
|
||||
|
||||
if (validation?.mismatchedUsers > 0 || validation?.missingSectionUserIds > 0) {
|
||||
setStatus(
|
||||
`Loaded ${users.length} users from ${validation.userSource}. ` +
|
||||
`${validation.mismatchedUsers} user(s) had mismatched section UserIds and ` +
|
||||
`${validation.normalizedUsers} user(s) were normalized on load.`,
|
||||
'ok'
|
||||
);
|
||||
} else {
|
||||
setStatus(
|
||||
`Loaded ${users.length} users from ${validation?.userSource || 'database'}. ` +
|
||||
'UserSettings IDs and section UserIds match.',
|
||||
'ok'
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
setStatus(`Failed: ${e.message}`, 'error');
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="panel">
|
||||
<h3>Settings</h3>
|
||||
|
||||
<section>
|
||||
<h4>Emby connection</h4>
|
||||
<label>
|
||||
<span>Server URL</span>
|
||||
<input type="text" bind:value={localConfig.embyUrl} placeholder="http://localhost:8096" />
|
||||
</label>
|
||||
<label>
|
||||
<span>API key</span>
|
||||
<input type="password" bind:value={localConfig.apiKey} placeholder="Paste your API key" />
|
||||
</label>
|
||||
<p class="hint">
|
||||
Get your API key from Emby: Dashboard → Advanced → API Keys → New API Key.
|
||||
</p>
|
||||
<div class="row">
|
||||
<button class="btn ghost" on:click={saveConfig} disabled={busy}>Save</button>
|
||||
<button class="btn ghost" on:click={testConnection} disabled={busy}>Test connection</button>
|
||||
<button class="btn ghost" on:click={refreshNames} disabled={busy || !config.apiKey}>
|
||||
Refresh user names
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h4>Database file</h4>
|
||||
<label>
|
||||
<span>Path to users.db</span>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={localConfig.dbPath}
|
||||
placeholder="C:\ProgramData\Emby-Server\data\users.db"
|
||||
/>
|
||||
</label>
|
||||
<p class="hint">
|
||||
Stop Emby before loading or writing to avoid corruption. Typical path:
|
||||
<code>C:\ProgramData\Emby-Server\data\users.db</code>
|
||||
</p>
|
||||
<div class="row">
|
||||
<button class="btn ghost" on:click={saveConfig} disabled={busy}>Save path</button>
|
||||
<button class="btn accent" on:click={loadFromDb} disabled={busy || !localConfig.dbPath}>
|
||||
Load from DB
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{#if status}
|
||||
<div class="status" class:ok={statusType === 'ok'} class:error={statusType === 'error'}>
|
||||
{status}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.panel {
|
||||
padding: 0 4px;
|
||||
}
|
||||
h3 {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
margin: 0 0 16px;
|
||||
color: var(--text);
|
||||
}
|
||||
h4 {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--text-muted);
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
section {
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
section:last-of-type {
|
||||
border-bottom: none;
|
||||
}
|
||||
label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
label span {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
input {
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
padding: 7px 10px;
|
||||
font-size: 13px;
|
||||
color: var(--text);
|
||||
font-family: inherit;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
.hint {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
margin: 0 0 10px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
code {
|
||||
font-size: 11px;
|
||||
background: var(--bg);
|
||||
padding: 1px 4px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.btn {
|
||||
all: unset;
|
||||
cursor: pointer;
|
||||
padding: 6px 14px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
font-family: inherit;
|
||||
transition: all 0.12s;
|
||||
}
|
||||
.btn.ghost {
|
||||
color: var(--text-muted);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.btn.ghost:hover:not(:disabled) {
|
||||
background: var(--surface-hover);
|
||||
color: var(--text);
|
||||
}
|
||||
.btn.accent {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
.btn.accent:hover:not(:disabled) {
|
||||
opacity: 0.9;
|
||||
}
|
||||
.btn:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: default;
|
||||
}
|
||||
.status {
|
||||
margin-top: 8px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
background: var(--surface-hover);
|
||||
color: var(--text-muted);
|
||||
border-left: 3px solid var(--border);
|
||||
}
|
||||
.status.ok {
|
||||
border-left-color: #22c55e;
|
||||
color: #86efac;
|
||||
}
|
||||
.status.error {
|
||||
border-left-color: var(--danger);
|
||||
color: #fca5a5;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user