style(frontend): Prettier config + one-time repo-wide format pass
Prettier 3 (pinned), printWidth 100, double quotes — chosen to match the existing hand-formatting and minimise reflow. This commit is ONLY the mechanical format pass (`prettier --write`) plus the config/ignore/scripts, isolated so it never pollutes a feature diff. Verified behaviour-neutral: typecheck, eslint (0 errors), and vitest all green before and after. See .git-blame-ignore-revs — `git blame` skips this sha.
This commit is contained in:
@@ -1,7 +1,17 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { Crop, Eye, EyeOff, Pause, Play, Scissors, SplitSquareHorizontal, Loader2, X } from "lucide-react";
|
||||
import {
|
||||
Crop,
|
||||
Eye,
|
||||
EyeOff,
|
||||
Pause,
|
||||
Play,
|
||||
Scissors,
|
||||
SplitSquareHorizontal,
|
||||
Loader2,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import clsx from "clsx";
|
||||
import Modal from "./Modal";
|
||||
import { notify } from "../lib/notifications";
|
||||
@@ -19,7 +29,8 @@ function parseTc(v: string): number | null {
|
||||
if (!v) return null;
|
||||
if (v.includes(":")) {
|
||||
const [m, s] = v.split(":");
|
||||
const mm = Number(m), ss = Number(s);
|
||||
const mm = Number(m),
|
||||
ss = Number(s);
|
||||
if (!isFinite(mm) || !isFinite(ss)) return null;
|
||||
return mm * 60 + ss;
|
||||
}
|
||||
@@ -149,7 +160,7 @@ export default function VideoEditor({ job, onClose }: { job: DownloadJob; onClos
|
||||
const r = el.getBoundingClientRect();
|
||||
return Math.max(0, Math.min(1, (clientX - r.left) / r.width)) * duration;
|
||||
},
|
||||
[duration]
|
||||
[duration],
|
||||
);
|
||||
useEffect(() => {
|
||||
if (!drag) return;
|
||||
@@ -171,7 +182,9 @@ export default function VideoEditor({ job, onClose }: { job: DownloadJob; onClos
|
||||
|
||||
// --- crop drag ---
|
||||
const overlayRef = useRef<HTMLDivElement>(null);
|
||||
const cropDrag = useRef<null | { mode: "move" | "resize"; px: number; py: number; orig: Frac }>(null);
|
||||
const cropDrag = useRef<null | { mode: "move" | "resize"; px: number; py: number; orig: Frac }>(
|
||||
null,
|
||||
);
|
||||
const onCropDown = (e: React.PointerEvent, mode: "move" | "resize") => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@@ -233,8 +246,8 @@ export default function VideoEditor({ job, onClose }: { job: DownloadJob; onClos
|
||||
0,
|
||||
Math.min(
|
||||
Math.max(0, (trackW || HOVER_W) - HOVER_W),
|
||||
hoverX - (trackRef.current?.getBoundingClientRect().left ?? 0) - HOVER_W / 2
|
||||
)
|
||||
hoverX - (trackRef.current?.getBoundingClientRect().left ?? 0) - HOVER_W / 2,
|
||||
),
|
||||
)
|
||||
: 0;
|
||||
|
||||
@@ -242,7 +255,10 @@ export default function VideoEditor({ job, onClose }: { job: DownloadJob; onClos
|
||||
const kept = segments.filter((s) => s.keep);
|
||||
const keptDur = kept.reduce((a, s) => a + (s.end - s.start), 0);
|
||||
const isFullSingle =
|
||||
segments.length === 1 && kept.length === 1 && kept[0].start <= 0.01 && kept[0].end >= duration - 0.01;
|
||||
segments.length === 1 &&
|
||||
kept.length === 1 &&
|
||||
kept[0].start <= 0.01 &&
|
||||
kept[0].end >= duration - 0.01;
|
||||
const valid = kept.length >= 1 && keptDur >= MIN_SEG && (cropOn || !isFullSingle);
|
||||
const willJoin = output === "join" && kept.length > 1;
|
||||
const reencode = cropOn || accurate;
|
||||
@@ -253,7 +269,12 @@ export default function VideoEditor({ job, onClose }: { job: DownloadJob; onClos
|
||||
const vh = v?.videoHeight || job.asset?.height || 0;
|
||||
if (!cropOn || !vw || !vh) return undefined;
|
||||
const even = (n: number) => Math.max(2, Math.round(n / 2) * 2);
|
||||
return { x: even(crop.x * vw), y: even(crop.y * vh), w: even(crop.w * vw), h: even(crop.h * vh) };
|
||||
return {
|
||||
x: even(crop.x * vw),
|
||||
y: even(crop.y * vh),
|
||||
w: even(crop.w * vw),
|
||||
h: even(crop.h * vh),
|
||||
};
|
||||
};
|
||||
|
||||
const create = useMutation({
|
||||
@@ -266,7 +287,11 @@ export default function VideoEditor({ job, onClose }: { job: DownloadJob; onClos
|
||||
accurate,
|
||||
};
|
||||
if (cp) spec.crop = cp;
|
||||
await api.enqueueEdit({ source_job_id: job.id, edit_spec: spec, display_name: name.trim() || undefined });
|
||||
await api.enqueueEdit({
|
||||
source_job_id: job.id,
|
||||
edit_spec: spec,
|
||||
display_name: name.trim() || undefined,
|
||||
});
|
||||
return { files: 1 };
|
||||
}
|
||||
const N = kept.length;
|
||||
@@ -331,12 +356,21 @@ export default function VideoEditor({ job, onClose }: { job: DownloadJob; onClos
|
||||
|
||||
{/* transport */}
|
||||
<div className="flex items-center gap-3">
|
||||
<button onClick={playPause} className="p-2 rounded-lg bg-surface hover:bg-card transition" title={t("editor.playPause")}>
|
||||
<button
|
||||
onClick={playPause}
|
||||
className="p-2 rounded-lg bg-surface hover:bg-card transition"
|
||||
title={t("editor.playPause")}
|
||||
>
|
||||
{playing ? <Pause className="w-4 h-4" /> : <Play className="w-4 h-4" />}
|
||||
</button>
|
||||
<div className="text-xs text-muted tabular-nums">{tc(current)} / {tc(duration)}</div>
|
||||
<div className="text-xs text-muted tabular-nums">
|
||||
{tc(current)} / {tc(duration)}
|
||||
</div>
|
||||
<div className="flex-1" />
|
||||
<button onClick={splitAtPlayhead} className={clsx(btnCls, "bg-surface hover:bg-card inline-flex items-center gap-1.5")}>
|
||||
<button
|
||||
onClick={splitAtPlayhead}
|
||||
className={clsx(btnCls, "bg-surface hover:bg-card inline-flex items-center gap-1.5")}
|
||||
>
|
||||
<SplitSquareHorizontal className="w-4 h-4" /> {t("editor.splitHere")}
|
||||
</button>
|
||||
</div>
|
||||
@@ -360,7 +394,9 @@ export default function VideoEditor({ job, onClose }: { job: DownloadJob; onClos
|
||||
<div
|
||||
key={i}
|
||||
className="h-full flex-1"
|
||||
style={tileStyle(Math.round(((i + 0.5) / stripCells) * ((strip!.count ?? 1) - 1)))}
|
||||
style={tileStyle(
|
||||
Math.round(((i + 0.5) / stripCells) * ((strip!.count ?? 1) - 1)),
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -372,7 +408,7 @@ export default function VideoEditor({ job, onClose }: { job: DownloadJob; onClos
|
||||
className={clsx(
|
||||
"absolute inset-y-0 border-r border-black/40 last:border-r-0",
|
||||
s.keep ? "bg-accent/10" : "bg-black/60",
|
||||
s.id === selId && "ring-2 ring-inset ring-white/70"
|
||||
s.id === selId && "ring-2 ring-inset ring-white/70",
|
||||
)}
|
||||
style={{
|
||||
left: pct(s.start),
|
||||
@@ -395,7 +431,7 @@ export default function VideoEditor({ job, onClose }: { job: DownloadJob; onClos
|
||||
}}
|
||||
className={clsx(
|
||||
"absolute top-1 left-1 p-0.5 rounded bg-black/50 backdrop-blur-sm",
|
||||
s.keep ? "text-accent" : "text-muted"
|
||||
s.keep ? "text-accent" : "text-muted",
|
||||
)}
|
||||
title={s.keep ? t("editor.drop") : t("editor.keep")}
|
||||
>
|
||||
@@ -405,7 +441,11 @@ export default function VideoEditor({ job, onClose }: { job: DownloadJob; onClos
|
||||
))}
|
||||
{/* draggable boundary markers */}
|
||||
{boundaries.map((b, bi) => (
|
||||
<div key={bi} className="absolute inset-y-0 -ml-1.5 w-3 flex items-center justify-center z-base" style={{ left: pct(b) }}>
|
||||
<div
|
||||
key={bi}
|
||||
className="absolute inset-y-0 -ml-1.5 w-3 flex items-center justify-center z-base"
|
||||
style={{ left: pct(b) }}
|
||||
>
|
||||
<div
|
||||
className="w-1 h-full bg-white/90 cursor-ew-resize rounded"
|
||||
onPointerDown={(e) => {
|
||||
@@ -426,7 +466,10 @@ export default function VideoEditor({ job, onClose }: { job: DownloadJob; onClos
|
||||
</div>
|
||||
))}
|
||||
{/* playhead */}
|
||||
<div className="absolute inset-y-0 w-0.5 bg-white pointer-events-none z-menu" style={{ left: pct(current) }} />
|
||||
<div
|
||||
className="absolute inset-y-0 w-0.5 bg-white pointer-events-none z-menu"
|
||||
style={{ left: pct(current) }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* hover-scrub thumbnail */}
|
||||
@@ -435,8 +478,16 @@ export default function VideoEditor({ job, onClose }: { job: DownloadJob; onClos
|
||||
className="absolute bottom-full mb-2 pointer-events-none z-chrome rounded-md overflow-hidden border border-border shadow-xl bg-black"
|
||||
style={{ left: hoverLeft, width: HOVER_W }}
|
||||
>
|
||||
<div style={{ width: HOVER_W, height: HOVER_W / aspect, ...tileStyle(Math.floor(hoverTime / (strip.interval_s || 1))) }} />
|
||||
<div className="text-[10px] text-center text-muted py-0.5 tabular-nums">{tc(hoverTime)}</div>
|
||||
<div
|
||||
style={{
|
||||
width: HOVER_W,
|
||||
height: HOVER_W / aspect,
|
||||
...tileStyle(Math.floor(hoverTime / (strip.interval_s || 1))),
|
||||
}}
|
||||
/>
|
||||
<div className="text-[10px] text-center text-muted py-0.5 tabular-nums">
|
||||
{tc(hoverTime)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -449,7 +500,7 @@ export default function VideoEditor({ job, onClose }: { job: DownloadJob; onClos
|
||||
onClick={() => toggleKeep(sel.id)}
|
||||
className={clsx(
|
||||
"inline-flex items-center gap-1 px-2 py-1 rounded-md text-xs font-medium",
|
||||
sel.keep ? "bg-accent/15 text-accent" : "bg-surface text-muted"
|
||||
sel.keep ? "bg-accent/15 text-accent" : "bg-surface text-muted",
|
||||
)}
|
||||
>
|
||||
{sel.keep ? <Eye className="w-3.5 h-3.5" /> : <EyeOff className="w-3.5 h-3.5" />}
|
||||
@@ -482,7 +533,9 @@ export default function VideoEditor({ job, onClose }: { job: DownloadJob; onClos
|
||||
className={numCls}
|
||||
/>
|
||||
</label>
|
||||
<span className="text-xs text-muted tabular-nums w-16 text-right">{tc(sel.end - sel.start)}</span>
|
||||
<span className="text-xs text-muted tabular-nums w-16 text-right">
|
||||
{tc(sel.end - sel.start)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -496,7 +549,9 @@ export default function VideoEditor({ job, onClose }: { job: DownloadJob; onClos
|
||||
disabled={m === "join" && kept.length < 2}
|
||||
className={clsx(
|
||||
"px-3 py-1.5 rounded-lg border transition disabled:opacity-40",
|
||||
output === m ? "border-accent bg-accent/10 text-accent" : "border-border text-muted hover:text-fg"
|
||||
output === m
|
||||
? "border-accent bg-accent/10 text-accent"
|
||||
: "border-border text-muted hover:text-fg",
|
||||
)}
|
||||
>
|
||||
{t(`editor.output.${m}`)}
|
||||
@@ -514,7 +569,9 @@ export default function VideoEditor({ job, onClose }: { job: DownloadJob; onClos
|
||||
onClick={() => setCropOn((v) => !v)}
|
||||
className={clsx(
|
||||
"flex items-center gap-2 px-3 py-2 rounded-lg border text-sm transition",
|
||||
cropOn ? "border-accent text-accent bg-accent/10" : "border-border text-muted hover:text-fg"
|
||||
cropOn
|
||||
? "border-accent text-accent bg-accent/10"
|
||||
: "border-border text-muted hover:text-fg",
|
||||
)}
|
||||
>
|
||||
<Crop className="w-4 h-4" /> {cropOn ? t("editor.cropOn") : t("editor.cropOff")}
|
||||
@@ -529,37 +586,58 @@ export default function VideoEditor({ job, onClose }: { job: DownloadJob; onClos
|
||||
onClick={() => setAccurate(m === "accurate")}
|
||||
className={clsx(
|
||||
"flex-1 px-3 py-2 rounded-lg border text-left transition",
|
||||
on ? "border-accent bg-accent/10" : "border-border hover:border-muted"
|
||||
on ? "border-accent bg-accent/10" : "border-border hover:border-muted",
|
||||
)}
|
||||
title={t(`editor.${m}.hint`)}
|
||||
>
|
||||
<div className={clsx("text-sm font-medium", on ? "text-accent" : "text-fg")}>{t(`editor.${m}.label`)}</div>
|
||||
<div className={clsx("text-sm font-medium", on ? "text-accent" : "text-fg")}>
|
||||
{t(`editor.${m}.label`)}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center px-3 text-xs text-muted">{t("editor.cropReencode")}</div>
|
||||
<div className="flex items-center px-3 text-xs text-muted">
|
||||
{t("editor.cropReencode")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">{t("editor.nameLabel")}</label>
|
||||
<input value={name} onChange={(e) => setName(e.target.value)} placeholder={defaultName} className={inputCls} />
|
||||
<input
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder={defaultName}
|
||||
className={inputCls}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between pt-1">
|
||||
<span className="text-xs text-muted">{reencode ? t("editor.willReencode") : t("editor.willCopy")}</span>
|
||||
<span className="text-xs text-muted">
|
||||
{reencode ? t("editor.willReencode") : t("editor.willCopy")}
|
||||
</span>
|
||||
<div className="flex gap-2">
|
||||
<button onClick={onClose} className={clsx(btnCls, "text-muted hover:text-fg hover:bg-surface")}>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className={clsx(btnCls, "text-muted hover:text-fg hover:bg-surface")}
|
||||
>
|
||||
{t("common.cancel")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => create.mutate()}
|
||||
disabled={!valid || create.isPending}
|
||||
className={clsx(btnCls, "bg-accent text-accent-fg hover:opacity-90 inline-flex items-center gap-1.5")}
|
||||
className={clsx(
|
||||
btnCls,
|
||||
"bg-accent text-accent-fg hover:opacity-90 inline-flex items-center gap-1.5",
|
||||
)}
|
||||
>
|
||||
{create.isPending ? <Loader2 className="w-4 h-4 animate-spin" /> : <Scissors className="w-4 h-4" />}
|
||||
{create.isPending ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<Scissors className="w-4 h-4" />
|
||||
)}
|
||||
{willJoin
|
||||
? t("editor.createJoin")
|
||||
: kept.length > 1
|
||||
|
||||
Reference in New Issue
Block a user