android: tap-to-sync, the per-track auto-sync switch, the vocal-reduced copy
The editor the desktop drives with a mouse is better under a thumb: the song plays, the line to time sits large in the middle of the screen with the one behind and the one ahead faint around it, and a tap anywhere stamps it. One layer the back gesture peels, and a set left mid-tap is offered back by the row that opened it; Save writes the track's own document, lines never tapped left out of it and the button saying how many are kept. The lyrics sheet also gains the per-track auto-sync switch and the desktop's reduced-vocals copy — the same title, artist and duration, so the words keep following it, and one row puts the original back.
This commit is contained in:
1 parent
e693dfb868
commit
24c6857847
9 files changed
+434
-11
No files matched your search
@@ -0,0 +1,110 @@
|
||||
<script>
|
||||
/* The tap-to-sync room — the desktop's correction editor
|
||||
(LyricsSyncEditor.svelte) in the phone's idiom. The song plays and you
|
||||
tap once as each line begins; the taps become the timing. One line
|
||||
large, the line behind and the line ahead faint around it, and the
|
||||
whole middle of the screen is the tap. Saving writes the track's own
|
||||
document; lines never tapped are left out of it, and the button says
|
||||
how many are kept. */
|
||||
import Icon from "../components/Icon.svelte";
|
||||
import { nav } from "./nav.svelte.js";
|
||||
import { player, toggle, seekSecs } from "./player.svelte.js";
|
||||
import { tapSync, saveTapSync, closeTapSync } from "./lyrics.svelte.js";
|
||||
import { mmss } from "./util.js";
|
||||
|
||||
/* the ✕ asks twice once there is work to lose; the back gesture keeps
|
||||
the taps instead (the lyrics sheet offers them back). The room is
|
||||
mounted for the life of the shell, so the question is put down when
|
||||
it closes — otherwise the next visit opens already asking. */
|
||||
let confirmDiscard = $state(false);
|
||||
$effect(() => { if (!nav.timing) confirmDiscard = false; });
|
||||
|
||||
/* position anchors land a couple of times a second, so a tap between
|
||||
them reads the last anchor plus the wall clock — the stamp carries
|
||||
the tap's own precision rather than the tick's. Plain lets, never
|
||||
read back inside the effect that writes them. */
|
||||
let lastPos = 0;
|
||||
let lastAt = 0;
|
||||
$effect(() => {
|
||||
void player.status;
|
||||
lastPos = player.position ?? 0;
|
||||
lastAt = performance.now();
|
||||
});
|
||||
function nowMs() {
|
||||
const base = player.status === "playing"
|
||||
? lastPos + (performance.now() - lastAt) / 1000
|
||||
: (player.position ?? 0);
|
||||
return Math.max(0, Math.round(base * 1000));
|
||||
}
|
||||
|
||||
const rows = $derived(tapSync.rows);
|
||||
const done = $derived(rows.filter((r) => r.time_ms != null).length);
|
||||
const at = $derived(Math.min(tapSync.cursor, rows.length));
|
||||
const playing = $derived(player.status === "playing" || player.status === "buffering");
|
||||
/* playback moved on: a stamp would land on another song's clock */
|
||||
const wrong = $derived(!!tapSync.identity && !!player.track && player.track.title !== tapSync.identity.title);
|
||||
|
||||
function stamp() {
|
||||
if (wrong || tapSync.saving || at >= rows.length) return;
|
||||
tapSync.rows[at].time_ms = nowMs();
|
||||
tapSync.cursor = at + 1;
|
||||
confirmDiscard = false;
|
||||
}
|
||||
function undo() {
|
||||
const last = tapSync.cursor - 1;
|
||||
if (last < 0) return;
|
||||
tapSync.rows[last].time_ms = null;
|
||||
tapSync.cursor = last;
|
||||
}
|
||||
function back5() { seekSecs(Math.max(0, (player.position ?? 0) - 5)); }
|
||||
function close() {
|
||||
if (done > 0 && !confirmDiscard) { confirmDiscard = true; return; }
|
||||
closeTapSync();
|
||||
}
|
||||
/* a stamp reads to a tenth — the precision a thumb actually has */
|
||||
function mark(ms) {
|
||||
if (ms == null) return "";
|
||||
return `${mmss(Math.floor(ms / 1000))}.${Math.floor((ms % 1000) / 100)}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if nav.timing}
|
||||
<div class="syncroom">
|
||||
<div class="sy-head">
|
||||
<button class="iconbtn sy-close" onclick={close} aria-label={confirmDiscard ? "Discard the timing" : "Close"}>
|
||||
{#if confirmDiscard}<span class="sy-discard">Discard?</span>{:else}<Icon name="x" size={22} />{/if}
|
||||
</button>
|
||||
<span class="sy-who"><b>Tap in time</b><i>{tapSync.identity?.title ?? ""}</i></span>
|
||||
<span class="sy-count">{done} / {rows.length}</span>
|
||||
</div>
|
||||
|
||||
{#if wrong}
|
||||
<div class="sy-warn">Another track is playing — a tap follows what plays. Play “{tapSync.identity?.title}” again to keep going.</div>
|
||||
{:else if tapSync.error}
|
||||
<div class="sy-warn">{tapSync.error}</div>
|
||||
{/if}
|
||||
|
||||
<button class="sy-pad" onclick={stamp} disabled={wrong}>
|
||||
<span class="sy-prev">{at > 0 ? rows[at - 1].text : ""}</span>
|
||||
{#if at < rows.length}
|
||||
<span class="sy-now">{rows[at].text}</span>
|
||||
<span class="sy-next">{at + 1 < rows.length ? rows[at + 1].text : ""}</span>
|
||||
<span class="sy-hint">Tap anywhere as the line begins</span>
|
||||
{:else}
|
||||
<span class="sy-now">That’s every line — save the timing.</span>
|
||||
<span class="sy-next"></span>
|
||||
<span class="sy-hint">Undo puts the last one back</span>
|
||||
{/if}
|
||||
<span class="sy-mark">{at > 0 ? mark(rows[at - 1].time_ms) : mmss(player.position)}</span>
|
||||
</button>
|
||||
|
||||
<div class="sy-foot">
|
||||
<button class="iconbtn sy-undo" onclick={undo} disabled={tapSync.cursor === 0} aria-label="Undo the last tap"><Icon name="history" size={22} /></button>
|
||||
<button class="iconbtn sy-back5" onclick={back5} aria-label="Back five seconds">−5 s</button>
|
||||
<button class="disc" onclick={toggle} aria-label={playing ? "Pause" : "Play"}><Icon name={playing ? "pause" : "play"} size={22} /></button>
|
||||
<button class="btn btn-fill sy-save" onclick={() => { void saveTapSync(); }} disabled={!done || tapSync.saving}>
|
||||
{tapSync.saving ? "Saving…" : (done && done < rows.length ? `Save ${done} line${done === 1 ? "" : "s"}` : "Save timing")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
Reference in new issue
Block a user