Refine clip editor flow
This commit is contained in:
@@ -16,6 +16,7 @@ const MEDIA_ACCEPT =
|
|||||||
".mp3,.m4a,.aac,.wav,.ogg,.oga,.flac,.mp4,.m4v,.mov,audio/*,video/*,application/octet-stream";
|
".mp3,.m4a,.aac,.wav,.ogg,.oga,.flac,.mp4,.m4v,.mov,audio/*,video/*,application/octet-stream";
|
||||||
const DEFAULT_CLIP_LENGTH_MS = 30_000;
|
const DEFAULT_CLIP_LENGTH_MS = 30_000;
|
||||||
const END_SHORTCUT_LENGTH_MS = 90_000;
|
const END_SHORTCUT_LENGTH_MS = 90_000;
|
||||||
|
const SAVE_FADE_OUT_MS = 1000;
|
||||||
const TRIM_NUDGE_MS = 100;
|
const TRIM_NUDGE_MS = 100;
|
||||||
const TRIM_STEP_MS = 100;
|
const TRIM_STEP_MS = 100;
|
||||||
const TRIM_ZOOM_WINDOW_MS = 3_000;
|
const TRIM_ZOOM_WINDOW_MS = 3_000;
|
||||||
@@ -64,6 +65,7 @@ export function LibraryPage() {
|
|||||||
const {
|
const {
|
||||||
activeKey: previewKey,
|
activeKey: previewKey,
|
||||||
currentTimeMs: previewTimeMs,
|
currentTimeMs: previewTimeMs,
|
||||||
|
fadeOutClip,
|
||||||
playClip: playClipPreview,
|
playClip: playClipPreview,
|
||||||
stopClip: stopPreview,
|
stopClip: stopPreview,
|
||||||
} = useClipPlayback();
|
} = useClipPlayback();
|
||||||
@@ -315,6 +317,7 @@ export function LibraryPage() {
|
|||||||
teamId={teamId}
|
teamId={teamId}
|
||||||
playerId={playerId}
|
playerId={playerId}
|
||||||
previewTimeMs={previewTimeMs}
|
previewTimeMs={previewTimeMs}
|
||||||
|
fadeOutPreview={fadeOutClip}
|
||||||
playPreview={playPreview}
|
playPreview={playPreview}
|
||||||
onClose={closeCreateWalkupClip}
|
onClose={closeCreateWalkupClip}
|
||||||
stopPreview={stopPreview}
|
stopPreview={stopPreview}
|
||||||
@@ -374,6 +377,7 @@ function WalkupClipModal({
|
|||||||
teamId,
|
teamId,
|
||||||
playerId,
|
playerId,
|
||||||
previewTimeMs,
|
previewTimeMs,
|
||||||
|
fadeOutPreview,
|
||||||
playPreview,
|
playPreview,
|
||||||
onClose,
|
onClose,
|
||||||
stopPreview,
|
stopPreview,
|
||||||
@@ -385,6 +389,7 @@ function WalkupClipModal({
|
|||||||
teamId: string;
|
teamId: string;
|
||||||
playerId: string;
|
playerId: string;
|
||||||
previewTimeMs: number | null;
|
previewTimeMs: number | null;
|
||||||
|
fadeOutPreview: (durationMs?: number) => void;
|
||||||
playPreview: (clip: AudioClip, startMs?: number, endMs?: number) => Promise<void>;
|
playPreview: (clip: AudioClip, startMs?: number, endMs?: number) => Promise<void>;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
stopPreview: () => void;
|
stopPreview: () => void;
|
||||||
@@ -594,23 +599,19 @@ function WalkupClipModal({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="walkup-modal-body">
|
<div className="walkup-modal-body">
|
||||||
<div className="walkup-stepper">
|
<nav aria-label="Walkup clip steps">
|
||||||
<div className={`walkup-step${step === "source" ? " is-active" : " is-complete"}`}>1. Source</div>
|
<ol className="breadcrumb walkup-step-breadcrumb mb-0">
|
||||||
<div className={`walkup-step${step === "editor" ? " is-active" : ""}`}>2. Trim and metadata</div>
|
<li className={`breadcrumb-item${step === "source" ? " active" : ""}`} aria-current={step === "source" ? "page" : undefined}>
|
||||||
</div>
|
Source
|
||||||
|
</li>
|
||||||
|
<li className={`breadcrumb-item${step === "editor" ? " active" : ""}`} aria-current={step === "editor" ? "page" : undefined}>
|
||||||
|
Trim and metadata
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
|
||||||
{step === "source" ? (
|
{step === "source" ? (
|
||||||
<form className="stack" onSubmit={handleSourceSubmit} aria-busy={createSourceMutation.isPending}>
|
<form className="stack" onSubmit={handleSourceSubmit} aria-busy={createSourceMutation.isPending}>
|
||||||
<label className="field">
|
|
||||||
Walkup clip name
|
|
||||||
<input
|
|
||||||
value={draftLabel}
|
|
||||||
onChange={(event) => setDraftLabel(event.target.value)}
|
|
||||||
placeholder="Optional clip name"
|
|
||||||
autoComplete="off"
|
|
||||||
disabled={createSourceMutation.isPending}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<ul className="nav nav-tabs" role="tablist" aria-label="Walkup clip source">
|
<ul className="nav nav-tabs" role="tablist" aria-label="Walkup clip source">
|
||||||
<li className="nav-item" role="presentation">
|
<li className="nav-item" role="presentation">
|
||||||
<button
|
<button
|
||||||
@@ -623,7 +624,7 @@ function WalkupClipModal({
|
|||||||
disabled={createSourceMutation.isPending}
|
disabled={createSourceMutation.isPending}
|
||||||
onClick={() => setSourceMode("upload")}
|
onClick={() => setSourceMode("upload")}
|
||||||
>
|
>
|
||||||
Upload file
|
File
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li className="nav-item" role="presentation">
|
<li className="nav-item" role="presentation">
|
||||||
@@ -637,7 +638,7 @@ function WalkupClipModal({
|
|||||||
disabled={createSourceMutation.isPending}
|
disabled={createSourceMutation.isPending}
|
||||||
onClick={() => setSourceMode("url")}
|
onClick={() => setSourceMode("url")}
|
||||||
>
|
>
|
||||||
Import URL
|
URL
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li className="nav-item" role="presentation">
|
<li className="nav-item" role="presentation">
|
||||||
@@ -651,7 +652,7 @@ function WalkupClipModal({
|
|||||||
disabled={createSourceMutation.isPending}
|
disabled={createSourceMutation.isPending}
|
||||||
onClick={() => setSourceMode("existing")}
|
onClick={() => setSourceMode("existing")}
|
||||||
>
|
>
|
||||||
Existing media
|
Existing
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -663,6 +664,7 @@ function WalkupClipModal({
|
|||||||
className={`tab-pane fade${sourceMode === "upload" ? " show active" : ""}`}
|
className={`tab-pane fade${sourceMode === "upload" ? " show active" : ""}`}
|
||||||
>
|
>
|
||||||
<div className="stack">
|
<div className="stack">
|
||||||
|
<div className="muted">Upload a local audio file to create a new walkup clip.</div>
|
||||||
<label className="field">
|
<label className="field">
|
||||||
Media title
|
Media title
|
||||||
<input
|
<input
|
||||||
@@ -692,6 +694,7 @@ function WalkupClipModal({
|
|||||||
className={`tab-pane fade${sourceMode === "url" ? " show active" : ""}`}
|
className={`tab-pane fade${sourceMode === "url" ? " show active" : ""}`}
|
||||||
>
|
>
|
||||||
<div className="stack">
|
<div className="stack">
|
||||||
|
<div className="muted">Paste a link and we will download the audio for clip creation.</div>
|
||||||
<label className="field">
|
<label className="field">
|
||||||
Media title
|
Media title
|
||||||
<input
|
<input
|
||||||
@@ -723,6 +726,7 @@ function WalkupClipModal({
|
|||||||
className={`tab-pane fade${sourceMode === "existing" ? " show active" : ""}`}
|
className={`tab-pane fade${sourceMode === "existing" ? " show active" : ""}`}
|
||||||
>
|
>
|
||||||
<div className="stack">
|
<div className="stack">
|
||||||
|
<div className="muted">Pick an existing media file to turn into a walkup clip.</div>
|
||||||
<label className="field">
|
<label className="field">
|
||||||
Existing media file
|
Existing media file
|
||||||
<select
|
<select
|
||||||
@@ -775,11 +779,17 @@ function WalkupClipModal({
|
|||||||
previewTimeMs={previewTimeMs}
|
previewTimeMs={previewTimeMs}
|
||||||
playerId={playerId}
|
playerId={playerId}
|
||||||
onSaveComplete={async () => {
|
onSaveComplete={async () => {
|
||||||
|
if (previewTimeMs !== null) {
|
||||||
|
fadeOutPreview(SAVE_FADE_OUT_MS);
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
window.setTimeout(resolve, SAVE_FADE_OUT_MS);
|
||||||
|
});
|
||||||
|
}
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
queryClient.invalidateQueries({ queryKey: ["assets", teamId, playerId] }),
|
queryClient.invalidateQueries({ queryKey: ["assets", teamId, playerId] }),
|
||||||
queryClient.invalidateQueries({ queryKey: clipsQueryPrefix(teamId, playerId) }),
|
queryClient.invalidateQueries({ queryKey: clipsQueryPrefix(teamId, playerId) }),
|
||||||
]);
|
]);
|
||||||
handleClose();
|
onClose();
|
||||||
}}
|
}}
|
||||||
saveButtonLabel={isCreateMode ? "Save walk up clip" : "Save changes"}
|
saveButtonLabel={isCreateMode ? "Save walk up clip" : "Save changes"}
|
||||||
introText={
|
introText={
|
||||||
|
|||||||
@@ -174,28 +174,29 @@ select {
|
|||||||
padding: 1.25rem;
|
padding: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.walkup-stepper {
|
.walkup-step-breadcrumb {
|
||||||
display: flex;
|
--bs-breadcrumb-divider: "›";
|
||||||
gap: 0.75rem;
|
align-items: center;
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.walkup-step {
|
|
||||||
padding: 0.4rem 0.7rem;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: rgba(19, 34, 56, 0.08);
|
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 0.85rem;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.walkup-step.is-active {
|
.walkup-step-breadcrumb .breadcrumb-item {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.walkup-step-breadcrumb .breadcrumb-item + .breadcrumb-item::before {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.walkup-step-breadcrumb .breadcrumb-item.active {
|
||||||
|
padding: 0.32rem 0.65rem;
|
||||||
|
border-radius: 999px;
|
||||||
background: var(--accent-soft);
|
background: var(--accent-soft);
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
}
|
font-weight: 600;
|
||||||
|
|
||||||
.walkup-step.is-complete {
|
|
||||||
background: rgba(47, 158, 68, 0.14);
|
|
||||||
color: #25643b;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.walkup-modal-actions {
|
.walkup-modal-actions {
|
||||||
|
|||||||
Reference in New Issue
Block a user