add: android preview to main video
Signed-off-by: pakintada@gmail.com <Pakin>
This commit is contained in:
parent
1156b5e6c3
commit
af9a4a2be5
1 changed files with 237 additions and 64 deletions
|
|
@ -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 & brewing-page videos, scheduled by date</p>
|
<p class="text-sm text-muted-foreground">
|
||||||
|
Main-page & 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<N></code> (next free 1–40, never overwrites a video in use).
|
<code class="font-mono">brewing_adv<N></code> (next free 1–40, 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 & the <code class="font-mono">…VideoEnable</code> variable.</p>
|
<p class="text-xs text-muted-foreground">
|
||||||
|
Used for the comment & 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<N>.mp4</Badge>
|
<Badge variant="outline" class="ml-auto font-mono text-[10px]"
|
||||||
|
>brewing_adv<N>.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<N>_long.mp4</Badge>
|
<Badge variant="outline" class="ml-auto font-mono text-[10px]"
|
||||||
|
>brewing_adv<N>_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 & Push{/if}
|
{#if submitting}<Spinner class="mr-2 h-4 w-4" />Saving...{:else}<Upload
|
||||||
|
class="mr-2 h-4 w-4"
|
||||||
|
/>Create & 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 & Push{/if}
|
{#if editSaving}<Spinner class="mr-2 h-4 w-4" />Saving...{:else}<Upload
|
||||||
|
class="mr-2 h-4 w-4"
|
||||||
|
/>Save & Push{/if}
|
||||||
</Button>
|
</Button>
|
||||||
</Dialog.Footer>
|
</Dialog.Footer>
|
||||||
</Dialog.Content>
|
</Dialog.Content>
|
||||||
</Dialog.Root>
|
</Dialog.Root>
|
||||||
|
|
||||||
|
<ScrcpyDialog bind:open={showScreenMirror} />
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue