후속 개선: 원격 썸네일·자동재연결, RAW 썸네일, 마퀴 선택, 이동 강건성
P1 원격 소스 UX: - remote_thumbs: VFS 다운로드로 원격 썸네일 생성(이미지 EXIF 내장 우선, 영상은 미디어서버 bridge URL로 ffmpeg Range 추출) - 앱 시작 시 키체인 자격증명으로 원격 소스 자동 재연결 - fileops: physical_move에 Windows 공유위반 재시도 P3 그리드/포맷: - RAW(cr2/nef/arw/dng 등) 내장 JPEG 프리뷰 추출 썸네일(scan_largest_jpeg) - 그리드 마퀴(드래그 사각형) 다중선택 - .gitattributes 줄바꿈 정규화 Rust 테스트 51개 통과. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -13,10 +13,12 @@ import type { LayoutResult } from "./layout.worker";
|
||||
import LayoutWorker from "./layout.worker?worker";
|
||||
import {
|
||||
anchorIndex,
|
||||
clearSelection,
|
||||
info,
|
||||
openLightbox,
|
||||
selected,
|
||||
selectSingle,
|
||||
setSelectionFromIds,
|
||||
toggleSelect,
|
||||
selectRange,
|
||||
snapshot,
|
||||
@@ -53,6 +55,8 @@ export const JustifiedGrid: Component = () => {
|
||||
const [scrollTop, setScrollTop] = createSignal(0);
|
||||
const [viewportH, setViewportH] = createSignal(600);
|
||||
const [containerW, setContainerW] = createSignal(0);
|
||||
// 마퀴(드래그 사각형) 선택 — 콘텐츠 좌표계 [x1,y1,x2,y2]
|
||||
const [marquee, setMarquee] = createSignal<[number, number, number, number] | null>(null);
|
||||
|
||||
const worker = new LayoutWorker();
|
||||
let layoutSeq = 0;
|
||||
@@ -244,11 +248,75 @@ export const JustifiedGrid: Component = () => {
|
||||
if (e.dataTransfer) e.dataTransfer.effectAllowed = "move";
|
||||
};
|
||||
|
||||
// ── 마퀴(드래그 사각형) 선택 ──
|
||||
// 콘텐츠 좌표 = clientX - 컨테이너rect - 패딩(8) (+ y는 scrollTop)
|
||||
const toContent = (clientX: number, clientY: number): [number, number] => {
|
||||
const rect = containerRef!.getBoundingClientRect();
|
||||
return [
|
||||
clientX - rect.left - 8,
|
||||
clientY - rect.top - 8 + containerRef!.scrollTop,
|
||||
];
|
||||
};
|
||||
|
||||
const onBgMouseDown = (e: MouseEvent) => {
|
||||
// 배경(빈 공간) 좌클릭에서만 시작 — 셀 클릭은 제외
|
||||
if (e.button !== 0 || !(e.target as HTMLElement).dataset.gridbg) return;
|
||||
const [sx, sy] = toContent(e.clientX, e.clientY);
|
||||
setMarquee([sx, sy, sx, sy]);
|
||||
if (!e.ctrlKey && !e.metaKey) clearSelection();
|
||||
|
||||
const onMove = (me: MouseEvent) => {
|
||||
const [cx, cy] = toContent(me.clientX, me.clientY);
|
||||
const m = marquee();
|
||||
if (m) setMarquee([m[0], m[1], cx, cy]);
|
||||
};
|
||||
const onUp = () => {
|
||||
document.removeEventListener("mousemove", onMove);
|
||||
document.removeEventListener("mouseup", onUp);
|
||||
commitMarquee();
|
||||
setMarquee(null);
|
||||
};
|
||||
document.addEventListener("mousemove", onMove);
|
||||
document.addEventListener("mouseup", onUp);
|
||||
};
|
||||
|
||||
const commitMarquee = () => {
|
||||
const m = marquee();
|
||||
const l = layout();
|
||||
const snap = snapshot();
|
||||
if (!m || !l || !snap) return;
|
||||
const [x1, y1, x2, y2] = [Math.min(m[0], m[2]), Math.min(m[1], m[3]), Math.max(m[0], m[2]), Math.max(m[1], m[3])];
|
||||
// 너무 작은 드래그는 무시(클릭으로 간주)
|
||||
if (x2 - x1 < 4 && y2 - y1 < 4) return;
|
||||
const ids: number[] = [];
|
||||
for (let i = 0; i < snap.count; i++) {
|
||||
const bx = l.boxes[i * 4];
|
||||
const by = l.boxes[i * 4 + 1];
|
||||
const bw = l.boxes[i * 4 + 2];
|
||||
const bh = l.boxes[i * 4 + 3];
|
||||
// AABB 교차
|
||||
if (bx < x2 && bx + bw > x1 && by < y2 && by + bh > y1) ids.push(snap.id(i));
|
||||
}
|
||||
setSelectionFromIds(ids);
|
||||
};
|
||||
|
||||
const marqueeRect = createMemo(() => {
|
||||
const m = marquee();
|
||||
if (!m) return null;
|
||||
return {
|
||||
left: Math.min(m[0], m[2]),
|
||||
top: Math.min(m[1], m[3]),
|
||||
width: Math.abs(m[2] - m[0]),
|
||||
height: Math.abs(m[3] - m[1]),
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
onScroll={onScroll}
|
||||
onKeyDown={onKeyDown}
|
||||
onMouseDown={onBgMouseDown}
|
||||
tabindex={0}
|
||||
class="relative min-h-0 flex-1 overflow-y-auto overflow-x-hidden px-2 py-2 outline-none"
|
||||
>
|
||||
@@ -260,7 +328,24 @@ export const JustifiedGrid: Component = () => {
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div class="relative" style={{ height: `${layout()?.totalHeight ?? 0}px` }}>
|
||||
<div
|
||||
class="relative"
|
||||
data-gridbg="1"
|
||||
style={{ height: `${layout()?.totalHeight ?? 0}px` }}
|
||||
>
|
||||
<Show when={marqueeRect()}>
|
||||
{(r) => (
|
||||
<div
|
||||
class="pointer-events-none absolute z-10 border border-[var(--accent)] bg-[var(--accent-soft)]"
|
||||
style={{
|
||||
left: `${r().left}px`,
|
||||
top: `${r().top}px`,
|
||||
width: `${r().width}px`,
|
||||
height: `${r().height}px`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
<For each={indices()}>
|
||||
{(i) => {
|
||||
const snap = snapshot()!;
|
||||
|
||||
Reference in New Issue
Block a user