add: android preview to main video

Signed-off-by: pakintada@gmail.com <Pakin>
This commit is contained in:
pakintada@gmail.com 2026-07-16 15:13:24 +07:00
parent 1156b5e6c3
commit af9a4a2be5

View file

@ -27,6 +27,7 @@
import * as adb from '$lib/core/adb/adb'; import * as adb from '$lib/core/adb/adb';
import { env } from '$env/dynamic/public'; import { env } from '$env/dynamic/public';
import { AdbInstance } from '../../../state.svelte'; import { AdbInstance } from '../../../state.svelte';
import ScrcpyDialog from '$lib/components/scrcpy-dialog.svelte';
const CREATE_ENDPOINT = '/api/video-mainpage'; const CREATE_ENDPOINT = '/api/video-mainpage';
const LIST_ENDPOINT = '/api/video-mainpage/list'; const LIST_ENDPOINT = '/api/video-mainpage/list';
@ -121,6 +122,8 @@
let editBrewingTxtEnFile = $state<File | null>(null); let editBrewingTxtEnFile = $state<File | null>(null);
let editSaving = $state(false); let editSaving = $state(false);
let showScreenMirror = $state(false);
const editBrewingPlaySeconds = $derived(Math.max(1, Math.round(editBrewingRaw) - DURATION_TRIM)); const editBrewingPlaySeconds = $derived(Math.max(1, Math.round(editBrewingRaw) - DURATION_TRIM));
function toIso(d: string): string { function toIso(d: string): string {
@ -203,7 +206,8 @@
const file = input.files?.[0]; const file = input.files?.[0];
input.value = ''; input.value = '';
if (!file) return; if (!file) return;
if (!file.name.toLowerCase().endsWith('.png')) return addNotification('WARN:Text overlay must be .png'); if (!file.name.toLowerCase().endsWith('.png'))
return addNotification('WARN:Text overlay must be .png');
set(file, URL.createObjectURL(file)); set(file, URL.createObjectURL(file));
} }
function pickBrewingTxt(e: Event) { function pickBrewingTxt(e: Event) {
@ -286,7 +290,14 @@
const user = $auth; const user = $auth;
if (!user) return addNotification('ERR:Not logged in'); if (!user) return addNotification('ERR:Not logged in');
if (!AdbInstance.instance) return addNotification('ERR:Connect a machine first'); if (!AdbInstance.instance) return addNotification('ERR:Connect a machine first');
if (!name.trim() || !startDate || !mainFile || !brewingFile || !brewingTxtFile || !brewingTxtEnFile) if (
!name.trim() ||
!startDate ||
!mainFile ||
!brewingFile ||
!brewingTxtFile ||
!brewingTxtEnFile
)
return addNotification( return addNotification(
'ERR:Need a name, start date, both videos, and both brewing text overlays (TH + EN)' 'ERR:Need a name, start date, both videos, and both brewing text overlays (TH + EN)'
); );
@ -355,7 +366,9 @@
managed = data.managed ?? []; managed = data.managed ?? [];
readonlyList = data.readonly ?? []; readonlyList = data.readonly ?? [];
} catch (error) { } catch (error) {
addNotification(`ERR:Load list failed: ${error instanceof Error ? error.message : 'unknown'}`); addNotification(
`ERR:Load list failed: ${error instanceof Error ? error.message : 'unknown'}`
);
} finally { } finally {
loadingList = false; loadingList = false;
} }
@ -395,7 +408,8 @@
const file = input.files?.[0]; const file = input.files?.[0];
input.value = ''; input.value = '';
if (!file) return; if (!file) return;
if (!file.name.toLowerCase().endsWith('.png')) return addNotification('WARN:Text overlay must be .png'); if (!file.name.toLowerCase().endsWith('.png'))
return addNotification('WARN:Text overlay must be .png');
if (en) editBrewingTxtEnFile = file; if (en) editBrewingTxtEnFile = file;
else editBrewingTxtFile = file; else editBrewingTxtFile = file;
} }
@ -418,7 +432,8 @@
fd.append('start', toIso(editStart)); fd.append('start', toIso(editStart));
fd.append('end', editEnd ? toIso(editEnd) : 'NONE'); fd.append('end', editEnd ? toIso(editEnd) : 'NONE');
if (editBrewingFile) fd.append('brewing_duration', String(editBrewingPlaySeconds)); if (editBrewingFile) fd.append('brewing_duration', String(editBrewingPlaySeconds));
else if (target.brewing?.duration) fd.append('brewing_duration', String(target.brewing.duration)); else if (target.brewing?.duration)
fd.append('brewing_duration', String(target.brewing.duration));
if (editMainFile) fd.append('video', editMainFile); if (editMainFile) fd.append('video', editMainFile);
if (editBrewingFile) fd.append('brewing_video', editBrewingFile); if (editBrewingFile) fd.append('brewing_video', editBrewingFile);
if (editBrewingTxtFile) fd.append('brewing_txt', editBrewingTxtFile); if (editBrewingTxtFile) fd.append('brewing_txt', editBrewingTxtFile);
@ -476,7 +491,9 @@
<div class="flex items-center justify-between px-8 py-4"> <div class="flex items-center justify-between px-8 py-4">
<div> <div>
<h1 class="text-2xl font-bold">Advertisement Videos</h1> <h1 class="text-2xl font-bold">Advertisement Videos</h1>
<p class="text-sm text-muted-foreground">Main-page &amp; brewing-page videos, scheduled by date</p> <p class="text-sm text-muted-foreground">
Main-page &amp; brewing-page videos, scheduled by date
</p>
</div> </div>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<Badge variant={isAdbConnected ? 'default' : 'secondary'}> <Badge variant={isAdbConnected ? 'default' : 'secondary'}>
@ -484,7 +501,13 @@
</Badge> </Badge>
{#if !isAdbConnected} {#if !isAdbConnected}
<Button variant="outline" onclick={connectMachine} disabled={connecting}> <Button variant="outline" onclick={connectMachine} disabled={connecting}>
{#if connecting}<Spinner class="mr-2 h-4 w-4" />Connecting...{:else}<MonitorPlay class="mr-2 h-4 w-4" />Connect Machine{/if} {#if connecting}<Spinner class="mr-2 h-4 w-4" />Connecting...{:else}<MonitorPlay
class="mr-2 h-4 w-4"
/>Connect Machine{/if}
</Button>
{:else}
<Button variant="outline" onclick={() => (showScreenMirror = true)}>
Show Android Screen
</Button> </Button>
{/if} {/if}
</div> </div>
@ -502,7 +525,8 @@
<Card.Description> <Card.Description>
Upload the same clip twice — the main-page version and the brewing-page Upload the same clip twice — the main-page version and the brewing-page
<code class="font-mono">_long</code> version. Auto-named <code class="font-mono">_long</code> version. Auto-named
<code class="font-mono">brewing_adv&lt;N&gt;</code> (next free 140, never overwrites a video in use). <code class="font-mono">brewing_adv&lt;N&gt;</code> (next free 140, never overwrites a video
in use).
</Card.Description> </Card.Description>
</Card.Header> </Card.Header>
<Card.Content class="space-y-6"> <Card.Content class="space-y-6">
@ -518,20 +542,29 @@
</Select.Content> </Select.Content>
</Select.Root> </Select.Root>
<p class="text-xs text-muted-foreground"> <p class="text-xs text-muted-foreground">
Writes to <code class="font-mono">inter/{country}/video</code>{country === 'tha' ? ' + flat video/' : ''}. Writes to <code class="font-mono">inter/{country}/video</code>{country === 'tha'
? ' + flat video/'
: ''}.
</p> </p>
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<Label for="v-name">Name</Label> <Label for="v-name">Name</Label>
<Input id="v-name" bind:value={name} placeholder="e.g. Bas Bew Bow Brewing" /> <Input id="v-name" bind:value={name} placeholder="e.g. Bas Bew Bow Brewing" />
<p class="text-xs text-muted-foreground">Used for the comment &amp; the <code class="font-mono">…VideoEnable</code> variable.</p> <p class="text-xs text-muted-foreground">
Used for the comment &amp; the <code class="font-mono">…VideoEnable</code> variable.
</p>
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<Label for="v-start" class="flex items-center gap-1.5"><CalendarDays class="h-3.5 w-3.5" /> Start date</Label> <Label for="v-start" class="flex items-center gap-1.5"
><CalendarDays class="h-3.5 w-3.5" /> Start date</Label
>
<Input id="v-start" type="date" bind:value={startDate} /> <Input id="v-start" type="date" bind:value={startDate} />
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<Label for="v-end" class="flex items-center gap-1.5"><CalendarDays class="h-3.5 w-3.5" /> End date <span class="text-muted-foreground">(optional)</span></Label> <Label for="v-end" class="flex items-center gap-1.5"
><CalendarDays class="h-3.5 w-3.5" /> End date
<span class="text-muted-foreground">(optional)</span></Label
>
<Input id="v-end" type="date" bind:value={endDate} /> <Input id="v-end" type="date" bind:value={endDate} />
{#if !endDate}<p class="text-xs text-muted-foreground">Blank = open-ended</p>{/if} {#if !endDate}<p class="text-xs text-muted-foreground">Blank = open-ended</p>{/if}
</div> </div>
@ -543,17 +576,27 @@
<div class="rounded-xl border p-3"> <div class="rounded-xl border p-3">
<div class="mb-2 flex items-center gap-2 text-sm font-semibold"> <div class="mb-2 flex items-center gap-2 text-sm font-semibold">
<Film class="h-4 w-4 text-muted-foreground" /> Main-page video <Film class="h-4 w-4 text-muted-foreground" /> Main-page video
<Badge variant="outline" class="ml-auto font-mono text-[10px]">brewing_adv&lt;N&gt;.mp4</Badge> <Badge variant="outline" class="ml-auto font-mono text-[10px]"
>brewing_adv&lt;N&gt;.mp4</Badge
>
</div> </div>
{#if mainFile} {#if mainFile}
<div class="relative aspect-video overflow-hidden rounded-lg bg-black"> <div class="relative aspect-video overflow-hidden rounded-lg bg-black">
<!-- svelte-ignore a11y_media_has_caption --> <!-- svelte-ignore a11y_media_has_caption -->
<video src={mainPreview} class="h-full w-full object-contain" muted controls></video> <video src={mainPreview} class="h-full w-full object-contain" muted controls
<button class="absolute right-1.5 top-1.5 rounded-full bg-black/60 p-1 text-white" onclick={clearMain}><X class="h-3.5 w-3.5" /></button> ></video>
<button
class="absolute top-1.5 right-1.5 rounded-full bg-black/60 p-1 text-white"
onclick={clearMain}><X class="h-3.5 w-3.5" /></button
>
</div> </div>
<p class="mt-2 truncate text-xs text-muted-foreground" title={mainFile.name}>{mainFile.name} · {fmtMB(mainFile.size)}</p> <p class="mt-2 truncate text-xs text-muted-foreground" title={mainFile.name}>
{mainFile.name} · {fmtMB(mainFile.size)}
</p>
{:else} {:else}
<label class="flex aspect-video cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed text-muted-foreground transition hover:bg-muted/50 hover:text-foreground"> <label
class="flex aspect-video cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed text-muted-foreground transition hover:bg-muted/50 hover:text-foreground"
>
<input type="file" accept=".mp4,video/mp4" class="hidden" onchange={pickMain} /> <input type="file" accept=".mp4,video/mp4" class="hidden" onchange={pickMain} />
<Film class="mb-2 h-8 w-8" /> <Film class="mb-2 h-8 w-8" />
<span class="text-sm font-medium">Click to select .mp4</span> <span class="text-sm font-medium">Click to select .mp4</span>
@ -565,23 +608,40 @@
<div class="rounded-xl border p-3"> <div class="rounded-xl border p-3">
<div class="mb-2 flex items-center gap-2 text-sm font-semibold"> <div class="mb-2 flex items-center gap-2 text-sm font-semibold">
<CoffeeIcon class="h-4 w-4 text-muted-foreground" /> Brewing-page video <CoffeeIcon class="h-4 w-4 text-muted-foreground" /> Brewing-page video
<Badge variant="outline" class="ml-auto font-mono text-[10px]">brewing_adv&lt;N&gt;_long.mp4</Badge> <Badge variant="outline" class="ml-auto font-mono text-[10px]"
>brewing_adv&lt;N&gt;_long.mp4</Badge
>
</div> </div>
{#if brewingFile} {#if brewingFile}
<div class="relative aspect-video overflow-hidden rounded-lg bg-black"> <div class="relative aspect-video overflow-hidden rounded-lg bg-black">
<!-- svelte-ignore a11y_media_has_caption --> <!-- svelte-ignore a11y_media_has_caption -->
<video src={brewingPreview} class="h-full w-full object-contain" muted controls></video> <video src={brewingPreview} class="h-full w-full object-contain" muted controls
<button class="absolute right-1.5 top-1.5 rounded-full bg-black/60 p-1 text-white" onclick={clearBrewing}><X class="h-3.5 w-3.5" /></button> ></video>
<button
class="absolute top-1.5 right-1.5 rounded-full bg-black/60 p-1 text-white"
onclick={clearBrewing}><X class="h-3.5 w-3.5" /></button
>
</div> </div>
<p class="mt-2 truncate text-xs text-muted-foreground" title={brewingFile.name}>{brewingFile.name} · {fmtMB(brewingFile.size)}</p> <p class="mt-2 truncate text-xs text-muted-foreground" title={brewingFile.name}>
{brewingFile.name} · {fmtMB(brewingFile.size)}
</p>
<p class="mt-1 flex items-center gap-1.5 text-xs font-medium"> <p class="mt-1 flex items-center gap-1.5 text-xs font-medium">
<Clock class="h-3.5 w-3.5 text-muted-foreground" /> <Clock class="h-3.5 w-3.5 text-muted-foreground" />
Plays {brewingPlaySeconds}s Plays {brewingPlaySeconds}s
<span class="text-muted-foreground">(length {Math.round(brewingRawSeconds)}s {DURATION_TRIM})</span> <span class="text-muted-foreground"
>(length {Math.round(brewingRawSeconds)}s {DURATION_TRIM})</span
>
</p> </p>
{:else} {:else}
<label class="flex aspect-video cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed text-muted-foreground transition hover:bg-muted/50 hover:text-foreground"> <label
<input type="file" accept=".mp4,video/mp4" class="hidden" onchange={pickBrewing} /> class="flex aspect-video cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed text-muted-foreground transition hover:bg-muted/50 hover:text-foreground"
>
<input
type="file"
accept=".mp4,video/mp4"
class="hidden"
onchange={pickBrewing}
/>
<CoffeeIcon class="mb-2 h-8 w-8" /> <CoffeeIcon class="mb-2 h-8 w-8" />
<span class="text-sm font-medium">Click to select _long .mp4</span> <span class="text-sm font-medium">Click to select _long .mp4</span>
</label> </label>
@ -599,13 +659,29 @@
{#if t.file} {#if t.file}
<div class="relative overflow-hidden rounded-md border bg-muted/30"> <div class="relative overflow-hidden rounded-md border bg-muted/30">
<img src={t.preview} alt={t.label} class="h-20 w-full object-contain" /> <img src={t.preview} alt={t.label} class="h-20 w-full object-contain" />
<button class="absolute right-1 top-1 rounded-full bg-black/60 p-0.5 text-white" onclick={t.clear}><X class="h-3 w-3" /></button> <button
class="absolute top-1 right-1 rounded-full bg-black/60 p-0.5 text-white"
onclick={t.clear}><X class="h-3 w-3" /></button
>
</div> </div>
<p class="mt-1 truncate text-[10px] text-muted-foreground" title={t.file.name}>{t.file.name}</p> <p
class="mt-1 truncate text-[10px] text-muted-foreground"
title={t.file.name}
>
{t.file.name}
</p>
{:else} {:else}
<label class="flex h-20 cursor-pointer flex-col items-center justify-center gap-1 rounded-md border border-dashed text-[11px] text-muted-foreground transition hover:bg-muted/50"> <label
<input type="file" accept=".png,image/png" class="hidden" onchange={t.pick} /> class="flex h-20 cursor-pointer flex-col items-center justify-center gap-1 rounded-md border border-dashed text-[11px] text-muted-foreground transition hover:bg-muted/50"
<ImageIcon class="h-4 w-4" /> {t.label} .png >
<input
type="file"
accept=".png,image/png"
class="hidden"
onchange={t.pick}
/>
<ImageIcon class="h-4 w-4" />
{t.label} .png
</label> </label>
{/if} {/if}
</div> </div>
@ -618,13 +694,17 @@
{#if pushProgress.active && submitting} {#if pushProgress.active && submitting}
<div class="space-y-1"> <div class="space-y-1">
<Progress value={pushProgress.percent} max={100} class="h-2" /> <Progress value={pushProgress.percent} max={100} class="h-2" />
<p class="text-center text-xs text-muted-foreground">Pushing {pushProgress.name} ({pushProgress.percent}%)</p> <p class="text-center text-xs text-muted-foreground">
Pushing {pushProgress.name} ({pushProgress.percent}%)
</p>
</div> </div>
{/if} {/if}
</Card.Content> </Card.Content>
<Card.Footer class="justify-end gap-2 border-t bg-muted/30 py-4"> <Card.Footer class="justify-end gap-2 border-t bg-muted/30 py-4">
<Button size="lg" onclick={handleSubmit} disabled={submitting || !isAdbConnected}> <Button size="lg" onclick={handleSubmit} disabled={submitting || !isAdbConnected}>
{#if submitting}<Spinner class="mr-2 h-4 w-4" />Saving...{:else}<Upload class="mr-2 h-4 w-4" />Create &amp; Push{/if} {#if submitting}<Spinner class="mr-2 h-4 w-4" />Saving...{:else}<Upload
class="mr-2 h-4 w-4"
/>Create &amp; Push{/if}
</Button> </Button>
</Card.Footer> </Card.Footer>
</Card.Root> </Card.Root>
@ -634,7 +714,9 @@
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<h2 class="text-lg font-semibold">Existing videos</h2> <h2 class="text-lg font-semibold">Existing videos</h2>
<Button variant="ghost" size="sm" onclick={loadList} disabled={loadingList}> <Button variant="ghost" size="sm" onclick={loadList} disabled={loadingList}>
{#if loadingList}<Spinner class="mr-2 h-3.5 w-3.5" />{:else}<RefreshCw class="mr-2 h-3.5 w-3.5" />{/if}Refresh {#if loadingList}<Spinner class="mr-2 h-3.5 w-3.5" />{:else}<RefreshCw
class="mr-2 h-3.5 w-3.5"
/>{/if}Refresh
</Button> </Button>
</div> </div>
@ -651,29 +733,59 @@
<div class="bg-black"> <div class="bg-black">
{#if v.main} {#if v.main}
<!-- svelte-ignore a11y_media_has_caption --> <!-- svelte-ignore a11y_media_has_caption -->
<video src={videoUrl(v.main.video)} class="aspect-video w-full object-contain" preload="metadata" muted controls></video> <video
src={videoUrl(v.main.video)}
class="aspect-video w-full object-contain"
preload="metadata"
muted
controls
></video>
{:else} {:else}
<div class="flex aspect-video items-center justify-center text-xs text-muted-foreground">no main</div> <div
class="flex aspect-video items-center justify-center text-xs text-muted-foreground"
>
no main
</div>
{/if} {/if}
</div> </div>
<div class="bg-black"> <div class="bg-black">
{#if v.brewing} {#if v.brewing}
<!-- svelte-ignore a11y_media_has_caption --> <!-- svelte-ignore a11y_media_has_caption -->
<video src={videoUrl(v.brewing.video)} class="aspect-video w-full object-contain" preload="metadata" muted controls></video> <video
src={videoUrl(v.brewing.video)}
class="aspect-video w-full object-contain"
preload="metadata"
muted
controls
></video>
{:else} {:else}
<div class="flex aspect-video items-center justify-center text-xs text-muted-foreground">no brewing</div> <div
class="flex aspect-video items-center justify-center text-xs text-muted-foreground"
>
no brewing
</div>
{/if} {/if}
</div> </div>
</div> </div>
<div class="flex items-center justify-between gap-2 p-3"> <div class="flex items-center justify-between gap-2 p-3">
<div class="min-w-0"> <div class="min-w-0">
<p class="truncate text-sm font-semibold" title={v.name}>{v.name}</p> <p class="truncate text-sm font-semibold" title={v.name}>{v.name}</p>
<div class="mt-1 flex flex-wrap items-center gap-1.5 text-[11px] text-muted-foreground"> <div
class="mt-1 flex flex-wrap items-center gap-1.5 text-[11px] text-muted-foreground"
>
<Badge variant="secondary" class="font-mono">brewing_adv{v.n}</Badge> <Badge variant="secondary" class="font-mono">brewing_adv{v.n}</Badge>
<span class="flex items-center gap-1"><CalendarDays class="h-3 w-3" />{v.range_label}</span> <span class="flex items-center gap-1"
{#if v.brewing?.duration}<span class="flex items-center gap-1"><Clock class="h-3 w-3" />{v.brewing.duration}s</span>{/if} ><CalendarDays class="h-3 w-3" />{v.range_label}</span
<Badge variant={v.main ? 'default' : 'outline'} class="text-[10px]">main</Badge> >
<Badge variant={v.brewing ? 'default' : 'outline'} class="text-[10px]">brewing</Badge> {#if v.brewing?.duration}<span class="flex items-center gap-1"
><Clock class="h-3 w-3" />{v.brewing.duration}s</span
>{/if}
<Badge variant={v.main ? 'default' : 'outline'} class="text-[10px]"
>main</Badge
>
<Badge variant={v.brewing ? 'default' : 'outline'} class="text-[10px]"
>brewing</Badge
>
</div> </div>
</div> </div>
<Button variant="outline" size="sm" onclick={() => openEdit(v)}> <Button variant="outline" size="sm" onclick={() => openEdit(v)}>
@ -692,19 +804,31 @@
class="flex w-full items-center gap-2 p-3 text-sm font-semibold text-muted-foreground transition hover:bg-muted/40" class="flex w-full items-center gap-2 p-3 text-sm font-semibold text-muted-foreground transition hover:bg-muted/40"
onclick={() => (showReadonly = !showReadonly)} onclick={() => (showReadonly = !showReadonly)}
> >
<ChevronDown class="h-4 w-4 shrink-0 transition-transform {showReadonly ? 'rotate-180' : ''}" /> <ChevronDown
class="h-4 w-4 shrink-0 transition-transform {showReadonly ? 'rotate-180' : ''}"
/>
<Lock class="h-3.5 w-3.5" /> <Lock class="h-3.5 w-3.5" />
Hand-maintained videos ({readonlyList.length}) Hand-maintained videos ({readonlyList.length})
<Badge variant="secondary" class="ml-1 text-[10px]">read-only</Badge> <Badge variant="secondary" class="ml-1 text-[10px]">read-only</Badge>
<span class="ml-auto text-xs font-normal">{showReadonly ? 'Click to hide' : 'Click to show'}</span> <span class="ml-auto text-xs font-normal"
>{showReadonly ? 'Click to hide' : 'Click to show'}</span
>
</button> </button>
{#if showReadonly} {#if showReadonly}
<div class="grid grid-cols-2 gap-3 p-3 pt-0 sm:grid-cols-3 lg:grid-cols-5"> <div class="grid grid-cols-2 gap-3 p-3 pt-0 sm:grid-cols-3 lg:grid-cols-5">
{#each readonlyList as v (v.filename)} {#each readonlyList as v (v.filename)}
<div class="overflow-hidden rounded-md border bg-muted/30"> <div class="overflow-hidden rounded-md border bg-muted/30">
<!-- svelte-ignore a11y_media_has_caption --> <!-- svelte-ignore a11y_media_has_caption -->
<video src={videoUrl(v.video)} class="aspect-video w-full bg-black object-contain" preload="metadata" muted controls></video> <video
<p class="truncate px-1.5 py-1 font-mono text-[10px]" title={v.filename}>{v.filename}</p> src={videoUrl(v.video)}
class="aspect-video w-full bg-black object-contain"
preload="metadata"
muted
controls
></video>
<p class="truncate px-1.5 py-1 font-mono text-[10px]" title={v.filename}>
{v.filename}
</p>
</div> </div>
{/each} {/each}
</div> </div>
@ -721,7 +845,8 @@
<Dialog.Header> <Dialog.Header>
<Dialog.Title>Edit video</Dialog.Title> <Dialog.Title>Edit video</Dialog.Title>
<Dialog.Description> <Dialog.Description>
{#if editTarget}<code class="font-mono">brewing_adv{editTarget.n}</code> · change dates, rename, or replace either clip{/if} {#if editTarget}<code class="font-mono">brewing_adv{editTarget.n}</code> · change dates, rename,
or replace either clip{/if}
</Dialog.Description> </Dialog.Description>
</Dialog.Header> </Dialog.Header>
@ -743,37 +868,77 @@
<div class="grid gap-3 sm:grid-cols-2"> <div class="grid gap-3 sm:grid-cols-2">
<div class="rounded-lg border p-2"> <div class="rounded-lg border p-2">
<p class="mb-1.5 flex items-center gap-1.5 text-xs font-semibold"><Film class="h-3.5 w-3.5 text-muted-foreground" /> Main-page</p> <p class="mb-1.5 flex items-center gap-1.5 text-xs font-semibold">
<Film class="h-3.5 w-3.5 text-muted-foreground" /> Main-page
</p>
{#if editTarget?.main} {#if editTarget?.main}
<!-- svelte-ignore a11y_media_has_caption --> <!-- svelte-ignore a11y_media_has_caption -->
<video src={videoUrl(editTarget.main.video)} class="aspect-video w-full rounded bg-black object-contain" preload="metadata" muted controls></video> <video
src={videoUrl(editTarget.main.video)}
class="aspect-video w-full rounded bg-black object-contain"
preload="metadata"
muted
controls
></video>
{/if} {/if}
<label class="mt-2 flex cursor-pointer items-center justify-center gap-1.5 rounded border border-dashed p-1.5 text-xs text-muted-foreground hover:bg-muted/50"> <label
class="mt-2 flex cursor-pointer items-center justify-center gap-1.5 rounded border border-dashed p-1.5 text-xs text-muted-foreground hover:bg-muted/50"
>
<input type="file" accept=".mp4,video/mp4" class="hidden" onchange={pickEditMain} /> <input type="file" accept=".mp4,video/mp4" class="hidden" onchange={pickEditMain} />
<Upload class="h-3.5 w-3.5" /> {editMainFile ? editMainFile.name : 'Replace (optional)'} <Upload class="h-3.5 w-3.5" />
{editMainFile ? editMainFile.name : 'Replace (optional)'}
</label> </label>
</div> </div>
<div class="rounded-lg border p-2"> <div class="rounded-lg border p-2">
<p class="mb-1.5 flex items-center gap-1.5 text-xs font-semibold"><CoffeeIcon class="h-3.5 w-3.5 text-muted-foreground" /> Brewing-page</p> <p class="mb-1.5 flex items-center gap-1.5 text-xs font-semibold">
<CoffeeIcon class="h-3.5 w-3.5 text-muted-foreground" /> Brewing-page
</p>
{#if editTarget?.brewing} {#if editTarget?.brewing}
<!-- svelte-ignore a11y_media_has_caption --> <!-- svelte-ignore a11y_media_has_caption -->
<video src={videoUrl(editTarget.brewing.video)} class="aspect-video w-full rounded bg-black object-contain" preload="metadata" muted controls></video> <video
src={videoUrl(editTarget.brewing.video)}
class="aspect-video w-full rounded bg-black object-contain"
preload="metadata"
muted
controls
></video>
{/if} {/if}
<label class="mt-2 flex cursor-pointer items-center justify-center gap-1.5 rounded border border-dashed p-1.5 text-xs text-muted-foreground hover:bg-muted/50"> <label
class="mt-2 flex cursor-pointer items-center justify-center gap-1.5 rounded border border-dashed p-1.5 text-xs text-muted-foreground hover:bg-muted/50"
>
<input type="file" accept=".mp4,video/mp4" class="hidden" onchange={pickEditBrewing} /> <input type="file" accept=".mp4,video/mp4" class="hidden" onchange={pickEditBrewing} />
<Upload class="h-3.5 w-3.5" /> {editBrewingFile ? editBrewingFile.name : 'Replace (optional)'} <Upload class="h-3.5 w-3.5" />
{editBrewingFile ? editBrewingFile.name : 'Replace (optional)'}
</label> </label>
{#if editBrewingFile} {#if editBrewingFile}
<p class="mt-1 flex items-center gap-1 text-[11px] text-muted-foreground"><Clock class="h-3 w-3" />Plays {editBrewingPlaySeconds}s</p> <p class="mt-1 flex items-center gap-1 text-[11px] text-muted-foreground">
<Clock class="h-3 w-3" />Plays {editBrewingPlaySeconds}s
</p>
{/if} {/if}
<div class="mt-2 grid grid-cols-2 gap-1.5"> <div class="mt-2 grid grid-cols-2 gap-1.5">
<label class="flex cursor-pointer items-center justify-center gap-1 rounded border border-dashed p-1.5 text-[11px] text-muted-foreground hover:bg-muted/50"> <label
<input type="file" accept=".png,image/png" class="hidden" onchange={(e) => pickEditTxt(e, false)} /> class="flex cursor-pointer items-center justify-center gap-1 rounded border border-dashed p-1.5 text-[11px] text-muted-foreground hover:bg-muted/50"
<ImageIcon class="h-3 w-3" /> {editBrewingTxtFile ? 'TH ✓' : 'Text TH (.png)'} >
<input
type="file"
accept=".png,image/png"
class="hidden"
onchange={(e) => pickEditTxt(e, false)}
/>
<ImageIcon class="h-3 w-3" />
{editBrewingTxtFile ? 'TH ✓' : 'Text TH (.png)'}
</label> </label>
<label class="flex cursor-pointer items-center justify-center gap-1 rounded border border-dashed p-1.5 text-[11px] text-muted-foreground hover:bg-muted/50"> <label
<input type="file" accept=".png,image/png" class="hidden" onchange={(e) => pickEditTxt(e, true)} /> class="flex cursor-pointer items-center justify-center gap-1 rounded border border-dashed p-1.5 text-[11px] text-muted-foreground hover:bg-muted/50"
<ImageIcon class="h-3 w-3" /> {editBrewingTxtEnFile ? 'EN ✓' : 'Text EN (.png)'} >
<input
type="file"
accept=".png,image/png"
class="hidden"
onchange={(e) => pickEditTxt(e, true)}
/>
<ImageIcon class="h-3 w-3" />
{editBrewingTxtEnFile ? 'EN ✓' : 'Text EN (.png)'}
</label> </label>
</div> </div>
</div> </div>
@ -782,16 +947,24 @@
{#if pushProgress.active} {#if pushProgress.active}
<div class="space-y-1"> <div class="space-y-1">
<Progress value={pushProgress.percent} max={100} class="h-2" /> <Progress value={pushProgress.percent} max={100} class="h-2" />
<p class="text-center text-xs text-muted-foreground">Pushing {pushProgress.name} ({pushProgress.percent}%)</p> <p class="text-center text-xs text-muted-foreground">
Pushing {pushProgress.name} ({pushProgress.percent}%)
</p>
</div> </div>
{/if} {/if}
</div> </div>
<Dialog.Footer> <Dialog.Footer>
<Button variant="outline" onclick={() => (editOpen = false)} disabled={editSaving}>Cancel</Button> <Button variant="outline" onclick={() => (editOpen = false)} disabled={editSaving}
>Cancel</Button
>
<Button onclick={submitEdit} disabled={editSaving || !isAdbConnected}> <Button onclick={submitEdit} disabled={editSaving || !isAdbConnected}>
{#if editSaving}<Spinner class="mr-2 h-4 w-4" />Saving...{:else}<Upload class="mr-2 h-4 w-4" />Save &amp; Push{/if} {#if editSaving}<Spinner class="mr-2 h-4 w-4" />Saving...{:else}<Upload
class="mr-2 h-4 w-4"
/>Save &amp; Push{/if}
</Button> </Button>
</Dialog.Footer> </Dialog.Footer>
</Dialog.Content> </Dialog.Content>
</Dialog.Root> </Dialog.Root>
<ScrcpyDialog bind:open={showScreenMirror} />