Refactor frontend components and remove FlareSolverr service from Docker setup
- Removed FlareSolverr service from docker-compose.yml and its related environment variables. - Refactored App.svelte to utilize new components for header, login form, and title management. - Introduced new components: AppHeader, LoginForm, AddTitleForm, ChapterList, TitleCard, TitleList, JobsStrip, and ErrorBanner for better modularity and maintainability. - Updated API handling in App.svelte to streamline authentication and title addition processes. - Enhanced user interface with improved error handling and loading states across components.
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
<script>
|
||||
/**
|
||||
* @typedef {object} Props
|
||||
* @property {boolean} [busy]
|
||||
* @property {(url: string) => void | Promise<void>} onSubmit
|
||||
*/
|
||||
|
||||
/** @type {Props} */
|
||||
let { busy = false, onSubmit } = $props();
|
||||
|
||||
let url = $state("");
|
||||
|
||||
async function handleSubmit(event) {
|
||||
event.preventDefault();
|
||||
const value = url.trim();
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
await onSubmit(value);
|
||||
url = "";
|
||||
}
|
||||
</script>
|
||||
|
||||
<form class="add-title" onsubmit={handleSubmit}>
|
||||
<label>
|
||||
Series URL
|
||||
<input type="url" bind:value={url} placeholder="" required disabled={busy} />
|
||||
</label>
|
||||
<button type="submit" disabled={busy || !url.trim()}>
|
||||
{busy ? "Working…" : "Add & watch"}
|
||||
</button>
|
||||
</form>
|
||||
@@ -0,0 +1,23 @@
|
||||
<script>
|
||||
/**
|
||||
* @typedef {object} Props
|
||||
* @property {boolean} showLogout
|
||||
* @property {string} [username]
|
||||
* @property {() => void} [onLogout]
|
||||
*/
|
||||
|
||||
/** @type {Props} */
|
||||
let { showLogout = false, username = "", onLogout } = $props();
|
||||
</script>
|
||||
|
||||
<header class="app-header">
|
||||
<h1>Watch & Download</h1>
|
||||
{#if showLogout}
|
||||
<div class="header-auth">
|
||||
{#if username}
|
||||
<span class="muted">{username}</span>
|
||||
{/if}
|
||||
<button type="button" class="outline contrast" onclick={onLogout}>Log out</button>
|
||||
</div>
|
||||
{/if}
|
||||
</header>
|
||||
@@ -0,0 +1,87 @@
|
||||
<script>
|
||||
/**
|
||||
* @typedef {object} Chapter
|
||||
* @property {number} id
|
||||
* @property {string} label
|
||||
* @property {string} status
|
||||
* @property {string} [error]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} Props
|
||||
* @property {Chapter[]} chapters
|
||||
* @property {number} titleId
|
||||
* @property {(chapterId: number, titleId: number) => void | Promise<void>} onCancel
|
||||
* @property {(chapterId: number, titleId: number) => void | Promise<void>} onRedownload
|
||||
*/
|
||||
|
||||
/** @type {Props} */
|
||||
let { chapters = [], titleId, onCancel, onRedownload } = $props();
|
||||
</script>
|
||||
|
||||
<ul class="chapter-list">
|
||||
{#each chapters as ch (ch.id)}
|
||||
<li class="chapter-card">
|
||||
<div class="chapter-label">{ch.label}</div>
|
||||
<span class={`status status-${ch.status}`}>{ch.status}</span>
|
||||
{#if ch.error}
|
||||
<div class="muted chapter-error">{ch.error}</div>
|
||||
{/if}
|
||||
<div class="chapter-actions">
|
||||
{#if ch.status === "downloading"}
|
||||
<button
|
||||
type="button"
|
||||
class="outline contrast"
|
||||
onclick={() => onCancel(ch.id, titleId)}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
{/if}
|
||||
<button type="button" class="outline" onclick={() => onRedownload(ch.id, titleId)}>
|
||||
Redownload
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Chapter</th>
|
||||
<th>Status</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each chapters as ch (ch.id)}
|
||||
<tr>
|
||||
<td>{ch.label}</td>
|
||||
<td>
|
||||
<span class={`status status-${ch.status}`}>{ch.status}</span>
|
||||
{#if ch.error}
|
||||
<div class="muted">{ch.error}</div>
|
||||
{/if}
|
||||
</td>
|
||||
<td>
|
||||
<div class="chapter-actions">
|
||||
{#if ch.status === "downloading"}
|
||||
<button
|
||||
type="button"
|
||||
class="outline contrast"
|
||||
onclick={() => onCancel(ch.id, titleId)}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
{/if}
|
||||
<button type="button" class="outline" onclick={() => onRedownload(ch.id, titleId)}>
|
||||
Redownload
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -0,0 +1,13 @@
|
||||
<script>
|
||||
/**
|
||||
* @typedef {object} Props
|
||||
* @property {string} message
|
||||
*/
|
||||
|
||||
/** @type {Props} */
|
||||
let { message } = $props();
|
||||
</script>
|
||||
|
||||
{#if message}
|
||||
<p class="error-banner" role="alert">{message}</p>
|
||||
{/if}
|
||||
@@ -0,0 +1,62 @@
|
||||
<script>
|
||||
/**
|
||||
* @typedef {object} Job
|
||||
* @property {number} id
|
||||
* @property {string} status
|
||||
* @property {string} [message]
|
||||
* @property {string} [type]
|
||||
* @property {string} [title_name]
|
||||
* @property {number} [chapter_id]
|
||||
* @property {number} [title_id]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} Props
|
||||
* @property {Job[]} jobs
|
||||
* @property {() => void | Promise<void>} onClear
|
||||
* @property {(chapterId: number, titleId: number|undefined) => void | Promise<void>} onCancel
|
||||
*/
|
||||
|
||||
/** @type {Props} */
|
||||
let { jobs = [], onClear, onCancel } = $props();
|
||||
</script>
|
||||
|
||||
<section class="jobs-strip" aria-live="polite">
|
||||
<div class="section-heading">
|
||||
<h2>Activity</h2>
|
||||
<button
|
||||
type="button"
|
||||
class="outline contrast"
|
||||
disabled={jobs.length === 0}
|
||||
onclick={onClear}
|
||||
>
|
||||
Clear queue
|
||||
</button>
|
||||
</div>
|
||||
{#if jobs.length === 0}
|
||||
<p class="muted">No active jobs.</p>
|
||||
{:else}
|
||||
<ul class="job-list">
|
||||
{#each jobs as job (job.id)}
|
||||
<li class="job-row">
|
||||
<div class="job-info">
|
||||
<strong>{job.status}</strong>
|
||||
— {job.message || job.type}
|
||||
{#if job.title_name}
|
||||
<span class="muted">({job.title_name})</span>
|
||||
{/if}
|
||||
</div>
|
||||
{#if job.chapter_id}
|
||||
<button
|
||||
type="button"
|
||||
class="outline contrast job-cancel"
|
||||
onclick={() => onCancel(job.chapter_id, job.title_id)}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</section>
|
||||
@@ -0,0 +1,50 @@
|
||||
<script>
|
||||
/**
|
||||
* @typedef {object} Props
|
||||
* @property {string} [error]
|
||||
* @property {boolean} [busy]
|
||||
* @property {(payload: { username: string, password: string }) => void | Promise<void>} onSubmit
|
||||
*/
|
||||
|
||||
/** @type {Props} */
|
||||
let { error = "", busy = false, onSubmit } = $props();
|
||||
|
||||
let username = $state("admin");
|
||||
let password = $state("");
|
||||
|
||||
async function handleSubmit(event) {
|
||||
event.preventDefault();
|
||||
await onSubmit({ username: username.trim(), password });
|
||||
password = "";
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if error}
|
||||
<p class="error-banner" role="alert">{error}</p>
|
||||
{/if}
|
||||
|
||||
<form class="login-form" onsubmit={handleSubmit}>
|
||||
<label>
|
||||
Username
|
||||
<input
|
||||
type="text"
|
||||
bind:value={username}
|
||||
autocomplete="username"
|
||||
required
|
||||
disabled={busy}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Password
|
||||
<input
|
||||
type="password"
|
||||
bind:value={password}
|
||||
autocomplete="current-password"
|
||||
required
|
||||
disabled={busy}
|
||||
/>
|
||||
</label>
|
||||
<button type="submit" disabled={busy || !password}>
|
||||
{busy ? "Signing in…" : "Sign in"}
|
||||
</button>
|
||||
</form>
|
||||
@@ -0,0 +1,99 @@
|
||||
<script>
|
||||
import ChapterList from "./ChapterList.svelte";
|
||||
|
||||
/**
|
||||
* @typedef {object} Title
|
||||
* @property {number} id
|
||||
* @property {string} title
|
||||
* @property {string} source
|
||||
* @property {string} url
|
||||
* @property {boolean} enabled
|
||||
* @property {number} downloaded
|
||||
* @property {number} total
|
||||
* @property {number} downloading
|
||||
* @property {number} failed
|
||||
* @property {string} [last_checked_at]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} Props
|
||||
* @property {Title} title
|
||||
* @property {boolean} expanded
|
||||
* @property {boolean} loading
|
||||
* @property {any[]} chapters
|
||||
* @property {boolean} busy
|
||||
* @property {() => void | Promise<void>} onToggleExpand
|
||||
* @property {() => void | Promise<void>} onSync
|
||||
* @property {() => void | Promise<void>} onToggleEnabled
|
||||
* @property {() => void | Promise<void>} onRemove
|
||||
* @property {(chapterId: number, titleId: number) => void | Promise<void>} onCancelChapter
|
||||
* @property {(chapterId: number, titleId: number) => void | Promise<void>} onRedownload
|
||||
*/
|
||||
|
||||
/** @type {Props} */
|
||||
let {
|
||||
title,
|
||||
expanded = false,
|
||||
loading = false,
|
||||
chapters = [],
|
||||
busy = false,
|
||||
onToggleExpand,
|
||||
onSync,
|
||||
onToggleEnabled,
|
||||
onRemove,
|
||||
onCancelChapter,
|
||||
onRedownload,
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<article>
|
||||
<header>
|
||||
<div class="title-meta">
|
||||
<strong>{title.title}</strong>
|
||||
<span class="muted">{title.source}</span>
|
||||
<span class="progress-text">
|
||||
{title.downloaded} / {title.total} downloaded
|
||||
{#if title.downloading > 0}
|
||||
· {title.downloading} downloading
|
||||
{/if}
|
||||
{#if title.failed > 0}
|
||||
· {title.failed} failed
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
<p class="muted url-line">
|
||||
<a href={title.url} target="_blank" rel="noreferrer">{title.url}</a>
|
||||
{#if title.last_checked_at}
|
||||
· last checked {title.last_checked_at}
|
||||
{/if}
|
||||
· watch {title.enabled ? "on" : "off"}
|
||||
</p>
|
||||
<div class="row-actions">
|
||||
<button type="button" class="outline" onclick={onToggleExpand}>
|
||||
{expanded ? "Hide chapters" : "Show chapters"}
|
||||
</button>
|
||||
<button type="button" class="outline" disabled={busy} onclick={onSync}>
|
||||
Sync now
|
||||
</button>
|
||||
<button type="button" class="outline secondary" onclick={onToggleEnabled}>
|
||||
{title.enabled ? "Disable watch" : "Enable watch"}
|
||||
</button>
|
||||
<button type="button" class="outline contrast" onclick={onRemove}>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{#if expanded}
|
||||
{#if loading && chapters.length === 0}
|
||||
<p class="muted">Loading chapters…</p>
|
||||
{:else}
|
||||
<ChapterList
|
||||
{chapters}
|
||||
titleId={title.id}
|
||||
onCancel={onCancelChapter}
|
||||
{onRedownload}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
</article>
|
||||
@@ -0,0 +1,56 @@
|
||||
<script>
|
||||
import TitleCard from "./TitleCard.svelte";
|
||||
|
||||
/**
|
||||
* @typedef {object} Props
|
||||
* @property {any[]} titles
|
||||
* @property {Record<string|number, boolean>} expanded
|
||||
* @property {Record<string|number, boolean>} loadingChapters
|
||||
* @property {Record<string|number, any[]>} chaptersByTitle
|
||||
* @property {boolean} busy
|
||||
* @property {(titleId: number) => void | Promise<void>} onToggleExpand
|
||||
* @property {(titleId: number) => void | Promise<void>} onSync
|
||||
* @property {(title: any) => void | Promise<void>} onToggleEnabled
|
||||
* @property {(titleId: number) => void | Promise<void>} onRemove
|
||||
* @property {(chapterId: number, titleId: number) => void | Promise<void>} onCancelChapter
|
||||
* @property {(chapterId: number, titleId: number) => void | Promise<void>} onRedownload
|
||||
*/
|
||||
|
||||
/** @type {Props} */
|
||||
let {
|
||||
titles = [],
|
||||
expanded = {},
|
||||
loadingChapters = {},
|
||||
chaptersByTitle = {},
|
||||
busy = false,
|
||||
onToggleExpand,
|
||||
onSync,
|
||||
onToggleEnabled,
|
||||
onRemove,
|
||||
onCancelChapter,
|
||||
onRedownload,
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<section>
|
||||
<h2>Titles</h2>
|
||||
{#if titles.length === 0}
|
||||
<p class="muted">No titles yet. Add a series URL above.</p>
|
||||
{:else}
|
||||
{#each titles as title (title.id)}
|
||||
<TitleCard
|
||||
{title}
|
||||
expanded={Boolean(expanded[title.id])}
|
||||
loading={Boolean(loadingChapters[title.id])}
|
||||
chapters={chaptersByTitle[title.id] || []}
|
||||
{busy}
|
||||
onToggleExpand={() => onToggleExpand(title.id)}
|
||||
onSync={() => onSync(title.id)}
|
||||
onToggleEnabled={() => onToggleEnabled(title)}
|
||||
onRemove={() => onRemove(title.id)}
|
||||
{onCancelChapter}
|
||||
{onRedownload}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
</section>
|
||||
Reference in New Issue
Block a user