refactor(frontend): migrate JS modules to TypeScript
Wholesale JS -> TS migration of the frontend — stores, utils, actions,
and all Svelte component scripts adopt type annotations. Compile-time
surfaces (app.d.ts, lib/types/) added for shared DTO types.
Build plumbing:
- package.json: dev:frontend script that runs svelte-kit sync first
- tauri.conf.json: beforeDevCommand points at dev:frontend
- run.sh: dropped the sed-hack that temporarily blanked beforeDevCommand;
now relies on npm run dev:frontend to avoid double-Vite
- jsconfig.json: allowImportingTsExtensions
Preserves all Group 1 behaviour:
- page.svelte.ts keeps loadHistory / loadTasks Tauri-first, no
localStorage; saveTranscriptMeta + mapTranscriptRow + mapTaskRow
intact; update_task_cmd and update_transcript_meta_cmd invocations
carry the correct payload shape.
- Toasts, preferences stores typed without behaviour change.
- Viewer still routes segment edits through saveTranscriptMeta; the
Task 1.5 TODO markers are gone.
taskExtractor.ts is functionally improved during the migration:
- multi-task matches in the same sentence
- list-style shopping-verb expansion (get bread, milk, and cheese)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
// @ts-nocheck
|
||||
import "../app.css";
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
// @ts-nocheck
|
||||
import "../../app.css";
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import { tick } from "svelte";
|
||||
import type { TaskList } from "$lib/types/app";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import {
|
||||
tasks, addTask, completeTask, uncompleteTask, deleteTask,
|
||||
@@ -12,12 +14,14 @@
|
||||
let showCompleted = $state(false);
|
||||
let newListName = $state("");
|
||||
let showNewList = $state(false);
|
||||
let contextMenuListId = $state(null);
|
||||
let editingListId = $state(null);
|
||||
let contextMenuListId = $state<string | null>(null);
|
||||
let editingListId = $state<string | null>(null);
|
||||
let editingName = $state("");
|
||||
let showSortMenu = $state(false);
|
||||
let draggingTaskId = $state(null);
|
||||
let dropHighlightId = $state(null);
|
||||
let draggingTaskId = $state<string | null>(null);
|
||||
let dropHighlightId = $state<string | null>(null);
|
||||
let editingInputEl = $state<HTMLInputElement | null>(null);
|
||||
let newListInputEl = $state<HTMLInputElement | null>(null);
|
||||
|
||||
// Filtered tasks for active list
|
||||
let tasksForActiveList = $derived.by(() => {
|
||||
@@ -34,7 +38,7 @@
|
||||
return done.filter((t) => t.listId === activeListId).slice(0, 10);
|
||||
});
|
||||
|
||||
function countForList(listId) {
|
||||
function countForList(listId: string) {
|
||||
if (listId === "all") return tasks.filter((t) => !t.done).length;
|
||||
if (listId === "inbox") return tasks.filter((t) => !t.done && !t.listId).length;
|
||||
return tasks.filter((t) => !t.done && t.listId === listId).length;
|
||||
@@ -48,7 +52,7 @@
|
||||
let builtInLists = $derived(taskLists.filter((l) => l.builtIn));
|
||||
let customLists = $derived(taskLists.filter((l) => !l.builtIn));
|
||||
|
||||
function handleQuickAdd(e) {
|
||||
function handleQuickAdd(e: KeyboardEvent) {
|
||||
if (e.key === "Enter" && quickInput.trim()) {
|
||||
const listId = (activeListId === "all" || activeListId === "inbox") ? null : activeListId;
|
||||
addTask({ text: quickInput.trim(), bucket: "inbox", listId });
|
||||
@@ -56,11 +60,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
function handleDragStart(e) {
|
||||
function handleDragStart(e: PointerEvent) {
|
||||
if (e.button !== 0) return;
|
||||
if (e.target.closest("button")) return;
|
||||
if (e.target.closest("input")) return;
|
||||
try { e.currentTarget?.setPointerCapture?.(e.pointerId); } catch {}
|
||||
if ((e.target as HTMLElement | null)?.closest("button")) return;
|
||||
if ((e.target as HTMLElement | null)?.closest("input")) return;
|
||||
try { (e.currentTarget as HTMLElement | null)?.setPointerCapture?.(e.pointerId); } catch {}
|
||||
getCurrentWindow().startDragging();
|
||||
}
|
||||
|
||||
@@ -74,7 +78,7 @@
|
||||
}
|
||||
|
||||
// List management
|
||||
function handleCreateList(e) {
|
||||
function handleCreateList(e: KeyboardEvent) {
|
||||
if (e.key === "Enter" && newListName.trim()) {
|
||||
addTaskList(newListName.trim());
|
||||
newListName = "";
|
||||
@@ -86,10 +90,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
function startRenaming(list) {
|
||||
async function startRenaming(list: TaskList) {
|
||||
editingListId = list.id;
|
||||
editingName = list.name;
|
||||
contextMenuListId = null;
|
||||
await tick();
|
||||
editingInputEl?.focus();
|
||||
editingInputEl?.select();
|
||||
}
|
||||
|
||||
function finishRenaming() {
|
||||
@@ -100,28 +107,28 @@
|
||||
editingName = "";
|
||||
}
|
||||
|
||||
function handleRenameKey(e) {
|
||||
function handleRenameKey(e: KeyboardEvent) {
|
||||
if (e.key === "Enter") finishRenaming();
|
||||
if (e.key === "Escape") { editingListId = null; editingName = ""; }
|
||||
}
|
||||
|
||||
function handleDeleteList(id) {
|
||||
function handleDeleteList(id: string) {
|
||||
if (activeListId === id) activeListId = "all";
|
||||
deleteTaskList(id);
|
||||
contextMenuListId = null;
|
||||
}
|
||||
|
||||
function toggleContextMenu(e, listId) {
|
||||
function toggleContextMenu(e: MouseEvent, listId: string) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
contextMenuListId = contextMenuListId === listId ? null : listId;
|
||||
}
|
||||
|
||||
// Drag-and-drop: tasks between lists
|
||||
function handleTaskDragStart(e, taskId) {
|
||||
function handleTaskDragStart(e: DragEvent, taskId: string) {
|
||||
draggingTaskId = taskId;
|
||||
e.dataTransfer.setData("text/plain", taskId);
|
||||
e.dataTransfer.effectAllowed = "move";
|
||||
e.dataTransfer?.setData("text/plain", taskId);
|
||||
if (e.dataTransfer) e.dataTransfer.effectAllowed = "move";
|
||||
}
|
||||
|
||||
function handleTaskDragEnd() {
|
||||
@@ -129,24 +136,24 @@
|
||||
dropHighlightId = null;
|
||||
}
|
||||
|
||||
function handleListDragOver(e) {
|
||||
function handleListDragOver(e: DragEvent) {
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = "move";
|
||||
if (e.dataTransfer) e.dataTransfer.dropEffect = "move";
|
||||
}
|
||||
|
||||
function handleListDragEnter(e, listId) {
|
||||
function handleListDragEnter(e: DragEvent, listId: string) {
|
||||
e.preventDefault();
|
||||
dropHighlightId = listId;
|
||||
}
|
||||
|
||||
function handleListDragLeave(e, listId) {
|
||||
function handleListDragLeave(_e: DragEvent, listId: string) {
|
||||
if (dropHighlightId === listId) dropHighlightId = null;
|
||||
}
|
||||
|
||||
function handleListDrop(e, listId) {
|
||||
function handleListDrop(e: DragEvent, listId: string) {
|
||||
e.preventDefault();
|
||||
dropHighlightId = null;
|
||||
const taskId = e.dataTransfer.getData("text/plain");
|
||||
const taskId = e.dataTransfer?.getData("text/plain");
|
||||
if (taskId) {
|
||||
moveTaskToList(taskId, listId);
|
||||
}
|
||||
@@ -157,10 +164,20 @@
|
||||
if (contextMenuListId) contextMenuListId = null;
|
||||
if (showSortMenu) showSortMenu = false;
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (showNewList) {
|
||||
tick().then(() => {
|
||||
newListInputEl?.focus();
|
||||
newListInputEl?.select();
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:window onclick={handleWindowClick} />
|
||||
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="flex flex-col h-full bg-bg">
|
||||
<!-- Drag handle with title -->
|
||||
<div
|
||||
@@ -239,6 +256,7 @@
|
||||
{#each customLists as list (list.id)}
|
||||
{#if editingListId === list.id}
|
||||
<input
|
||||
bind:this={editingInputEl}
|
||||
type="text"
|
||||
class="bg-bg-input border border-accent rounded px-2 py-0.5 text-[11px] text-text
|
||||
focus:outline-none w-[100px]"
|
||||
@@ -246,7 +264,6 @@
|
||||
onkeydown={handleRenameKey}
|
||||
onblur={finishRenaming}
|
||||
data-no-transition
|
||||
autofocus
|
||||
/>
|
||||
{:else}
|
||||
<div class="relative flex-shrink-0">
|
||||
@@ -280,7 +297,7 @@
|
||||
<!-- Context menu -->
|
||||
{#if contextMenuListId === list.id}
|
||||
<div class="absolute left-0 top-full mt-1 bg-bg-card border border-border rounded-lg shadow-lg py-1 z-20 animate-fade-in min-w-[120px]"
|
||||
onclick={(e) => e.stopPropagation()}>
|
||||
onpointerdown={(e) => e.stopPropagation()}>
|
||||
<button
|
||||
class="w-full text-left px-3 py-1 text-[10px] text-text-secondary hover:bg-hover hover:text-text"
|
||||
onclick={() => startRenaming(list)}
|
||||
@@ -322,6 +339,7 @@
|
||||
<!-- New list button -->
|
||||
{#if showNewList}
|
||||
<input
|
||||
bind:this={newListInputEl}
|
||||
type="text"
|
||||
class="bg-bg-input border border-border rounded px-2 py-0.5 text-[10px] text-text
|
||||
placeholder:text-text-tertiary focus:outline-none focus:border-accent w-[100px] flex-shrink-0"
|
||||
@@ -330,7 +348,6 @@
|
||||
onkeydown={handleCreateList}
|
||||
onblur={() => { showNewList = false; newListName = ""; }}
|
||||
data-no-transition
|
||||
autofocus
|
||||
/>
|
||||
{:else}
|
||||
<button
|
||||
@@ -345,6 +362,7 @@
|
||||
<button
|
||||
class="px-1.5 py-1 rounded text-[10px] text-text-tertiary hover:text-text-secondary"
|
||||
onclick={(e) => { e.stopPropagation(); showSortMenu = !showSortMenu; }}
|
||||
aria-label="Sort task lists"
|
||||
>
|
||||
<svg class="w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M3 6h18M3 12h12M3 18h6" stroke-linecap="round" />
|
||||
@@ -352,7 +370,7 @@
|
||||
</button>
|
||||
{#if showSortMenu}
|
||||
<div class="absolute right-0 top-full mt-1 bg-bg-card border border-border rounded-lg shadow-lg py-1 z-20 animate-fade-in min-w-[110px]"
|
||||
onclick={(e) => e.stopPropagation()}>
|
||||
onpointerdown={(e) => e.stopPropagation()}>
|
||||
<button
|
||||
class="w-full text-left px-3 py-1 text-[10px] text-text-secondary hover:bg-hover hover:text-text"
|
||||
onclick={() => { sortTaskLists("alpha"); showSortMenu = false; }}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
// @ts-nocheck
|
||||
import "../../app.css";
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
<script>
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy, tick } from "svelte";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { convertFileSrc, invoke } from "@tauri-apps/api/core";
|
||||
import type { TranscriptEntry, ViewerSegment } from "$lib/types/app";
|
||||
import { formatTime, formatDuration } from "$lib/utils/time.js";
|
||||
import { PLAYBACK_SPEEDS } from "$lib/utils/constants.js";
|
||||
import { errorMessage } from "$lib/utils/errors.js";
|
||||
import { parseStoredJson } from "$lib/utils/storage.js";
|
||||
import { saveTranscriptMeta } from "$lib/stores/page.svelte.js";
|
||||
|
||||
let item = $state(null);
|
||||
let audioEl = $state(null);
|
||||
let item = $state<TranscriptEntry | null>(null);
|
||||
let audioEl = $state<HTMLAudioElement | null>(null);
|
||||
let playing = $state(false);
|
||||
let currentTime = $state(0);
|
||||
let duration = $state(0);
|
||||
@@ -18,41 +21,62 @@
|
||||
let editingText = $state("");
|
||||
let showTimestamps = $state(true);
|
||||
let showStarredOnly = $state(false);
|
||||
let animFrameId = null;
|
||||
let segmentRefs = [];
|
||||
let viewerMode = $state("view"); // "view" | "edit"
|
||||
let animFrameId: number | null = null;
|
||||
let segmentRefs = $state<Array<HTMLElement | null>>([]);
|
||||
let viewerMode = $state<"view" | "edit">("view");
|
||||
let textDraft = $state("");
|
||||
let textDirty = $state(false);
|
||||
let textSaveTimer = null;
|
||||
let textSaveTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let editingTextareaEl = $state<HTMLTextAreaElement | null>(null);
|
||||
|
||||
function stopAudio() {
|
||||
if (audioEl) audioEl.pause();
|
||||
playing = false;
|
||||
if (animFrameId !== null) cancelAnimationFrame(animFrameId);
|
||||
animFrameId = null;
|
||||
}
|
||||
|
||||
function buildAudio(audioPath: string | null) {
|
||||
stopAudio();
|
||||
audioEl = null;
|
||||
duration = 0;
|
||||
currentTime = 0;
|
||||
if (!audioPath) return;
|
||||
|
||||
const src = convertFileSrc(audioPath);
|
||||
const audio = new Audio(src);
|
||||
audio.playbackRate = playbackRate;
|
||||
audio.onloadedmetadata = () => { duration = audio.duration; };
|
||||
audio.onended = () => {
|
||||
playing = false;
|
||||
if (animFrameId !== null) cancelAnimationFrame(animFrameId);
|
||||
animFrameId = null;
|
||||
};
|
||||
audio.onerror = () => { playing = false; };
|
||||
audioEl = audio;
|
||||
}
|
||||
|
||||
function loadViewerItem(nextItem: TranscriptEntry | null) {
|
||||
item = nextItem;
|
||||
textDraft = nextItem?.text ?? "";
|
||||
activeSegmentIdx = -1;
|
||||
editingIdx = -1;
|
||||
editingText = "";
|
||||
segmentRefs.length = 0;
|
||||
buildAudio(nextItem?.audioPath ?? null);
|
||||
}
|
||||
|
||||
// Load item data from localStorage (set by HistoryPage before opening this window)
|
||||
onMount(() => {
|
||||
try {
|
||||
const raw = localStorage.getItem("kon_viewer_item");
|
||||
if (raw) {
|
||||
item = JSON.parse(raw);
|
||||
textDraft = item?.text || "";
|
||||
if (item.audioPath) {
|
||||
const src = convertFileSrc(item.audioPath);
|
||||
const audio = new Audio(src);
|
||||
audio.onloadedmetadata = () => { duration = audio.duration; };
|
||||
audio.onended = () => { playing = false; cancelAnimationFrame(animFrameId); };
|
||||
audio.onerror = () => { playing = false; };
|
||||
audioEl = audio;
|
||||
}
|
||||
}
|
||||
const mode = localStorage.getItem("kon_viewer_mode");
|
||||
if (mode === "edit" || mode === "view") viewerMode = mode;
|
||||
} catch {}
|
||||
loadViewerItem(parseStoredJson<TranscriptEntry>(localStorage.getItem("kon_viewer_item")));
|
||||
const mode = localStorage.getItem("kon_viewer_mode");
|
||||
if (mode === "edit" || mode === "view") viewerMode = mode;
|
||||
|
||||
// Listen for new items via storage events
|
||||
window.addEventListener("storage", handleStorageChange);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (audioEl) audioEl.pause();
|
||||
cancelAnimationFrame(animFrameId);
|
||||
stopAudio();
|
||||
window.removeEventListener("storage", handleStorageChange);
|
||||
// Flush any pending text edit before the window tears down.
|
||||
if (textSaveTimer) {
|
||||
@@ -62,23 +86,9 @@
|
||||
if (textDirty) commitTextEdit();
|
||||
});
|
||||
|
||||
function handleStorageChange(e) {
|
||||
function handleStorageChange(e: StorageEvent) {
|
||||
if (e.key === "kon_viewer_item" && e.newValue) {
|
||||
try {
|
||||
if (audioEl) { audioEl.pause(); }
|
||||
cancelAnimationFrame(animFrameId);
|
||||
playing = false;
|
||||
currentTime = 0;
|
||||
|
||||
item = JSON.parse(e.newValue);
|
||||
if (item.audioPath) {
|
||||
const src = convertFileSrc(item.audioPath);
|
||||
const audio = new Audio(src);
|
||||
audio.onloadedmetadata = () => { duration = audio.duration; };
|
||||
audio.onended = () => { playing = false; cancelAnimationFrame(animFrameId); };
|
||||
audioEl = audio;
|
||||
}
|
||||
} catch {}
|
||||
loadViewerItem(parseStoredJson<TranscriptEntry>(e.newValue));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +97,7 @@
|
||||
if (playing) {
|
||||
audioEl.pause();
|
||||
playing = false;
|
||||
cancelAnimationFrame(animFrameId);
|
||||
if (animFrameId !== null) cancelAnimationFrame(animFrameId);
|
||||
} else {
|
||||
audioEl.play();
|
||||
playing = true;
|
||||
@@ -114,17 +124,15 @@
|
||||
if (activeSegmentIdx !== i) {
|
||||
activeSegmentIdx = i;
|
||||
// Auto-scroll to active segment
|
||||
if (segmentRefs[i]) {
|
||||
segmentRefs[i].scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
}
|
||||
segmentRefs[i]?.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function seekTo(e) {
|
||||
const val = parseFloat(e.target.value);
|
||||
function seekTo(e: Event) {
|
||||
const val = parseFloat((e.currentTarget as HTMLInputElement).value);
|
||||
if (audioEl) {
|
||||
audioEl.currentTime = val;
|
||||
currentTime = val;
|
||||
@@ -132,7 +140,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
function seekToTime(time) {
|
||||
function seekToTime(time: number) {
|
||||
if (audioEl) {
|
||||
audioEl.currentTime = time;
|
||||
currentTime = time;
|
||||
@@ -145,7 +153,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
function setSpeed(speed) {
|
||||
function setSpeed(speed: number) {
|
||||
playbackRate = speed;
|
||||
if (audioEl) audioEl.playbackRate = speed;
|
||||
}
|
||||
@@ -153,9 +161,9 @@
|
||||
|
||||
// Search: find matching segments
|
||||
let matchingSegments = $derived.by(() => {
|
||||
if (!searchQuery.trim() || !item?.segments) return new Set();
|
||||
if (!searchQuery.trim() || !item?.segments) return new Set<number>();
|
||||
const q = searchQuery.toLowerCase();
|
||||
const matches = new Set();
|
||||
const matches = new Set<number>();
|
||||
item.segments.forEach((seg, i) => {
|
||||
if (seg.text.toLowerCase().includes(q)) matches.add(i);
|
||||
});
|
||||
@@ -165,7 +173,7 @@
|
||||
let matchCount = $derived.by(() => matchingSegments.size);
|
||||
|
||||
// Highlight search matches in text (XSS-safe: escapes HTML entities first)
|
||||
function highlightText(text) {
|
||||
function highlightText(text: string) {
|
||||
if (!searchQuery.trim()) return text;
|
||||
const escaped = text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
||||
const q = searchQuery.trim();
|
||||
@@ -174,9 +182,13 @@
|
||||
}
|
||||
|
||||
// Segment editing
|
||||
function startEditing(idx) {
|
||||
async function startEditing(idx: number) {
|
||||
if (!item?.segments?.[idx]) return;
|
||||
editingIdx = idx;
|
||||
editingText = item.segments[idx].text;
|
||||
await tick();
|
||||
editingTextareaEl?.focus();
|
||||
editingTextareaEl?.select();
|
||||
}
|
||||
|
||||
function finishEditing() {
|
||||
@@ -194,7 +206,7 @@
|
||||
editingText = "";
|
||||
}
|
||||
|
||||
function deleteSegment(idx) {
|
||||
function deleteSegment(idx: number) {
|
||||
if (item?.segments) {
|
||||
item.segments.splice(idx, 1);
|
||||
item.text = item.segments.map((s) => s.text).join(" ").trim();
|
||||
@@ -205,7 +217,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
function toggleStar(idx) {
|
||||
function toggleStar(idx: number) {
|
||||
if (item?.segments) {
|
||||
item.segments[idx].starred = !item.segments[idx].starred;
|
||||
// Task 2.5 — persist the updated segments array (incl. starred flags)
|
||||
@@ -223,12 +235,10 @@
|
||||
// Task 2.5, tracked by the file's 80-odd pre-existing TS errors.
|
||||
function persistSegments() {
|
||||
if (!item) return;
|
||||
/** @type {any} */
|
||||
const snap = item;
|
||||
saveTranscriptMeta(snap.id, { segments: snap.segments });
|
||||
saveTranscriptMeta(item.id, { segments: item.segments });
|
||||
}
|
||||
|
||||
function copySegment(idx) {
|
||||
function copySegment(idx: number) {
|
||||
if (item?.segments?.[idx]) {
|
||||
const text = item.segments[idx].text.trim();
|
||||
navigator.clipboard.writeText(text).catch(() => {
|
||||
@@ -259,13 +269,13 @@
|
||||
text: item.text ?? null,
|
||||
title: item.title ?? null,
|
||||
}).catch((err) => {
|
||||
console.warn("viewer saveItemToHistory: update_transcript failed", err);
|
||||
console.warn("viewer saveItemToHistory: update_transcript failed", errorMessage(err));
|
||||
});
|
||||
}
|
||||
|
||||
function scheduleTextSave() {
|
||||
textDirty = true;
|
||||
clearTimeout(textSaveTimer);
|
||||
if (textSaveTimer) clearTimeout(textSaveTimer);
|
||||
textSaveTimer = setTimeout(() => {
|
||||
commitTextEdit();
|
||||
}, 400);
|
||||
@@ -288,8 +298,8 @@
|
||||
|
||||
// Filtered segments (starred filter + search)
|
||||
let visibleSegments = $derived.by(() => {
|
||||
if (!item?.segments) return [];
|
||||
let segs = item.segments.map((seg, i) => ({ ...seg, _idx: i }));
|
||||
if (!item?.segments) return [] as ViewerSegment[];
|
||||
let segs: ViewerSegment[] = item.segments.map((seg, i) => ({ ...seg, _idx: i }));
|
||||
if (showStarredOnly) {
|
||||
segs = segs.filter((s) => s.starred);
|
||||
}
|
||||
@@ -298,11 +308,11 @@
|
||||
|
||||
// Window drag — pointerdown + setPointerCapture avoids the mousedown
|
||||
// latency that makes KWin's initial grab feel draggy.
|
||||
function handleDragStart(e) {
|
||||
function handleDragStart(e: PointerEvent) {
|
||||
if (e.button !== 0) return;
|
||||
if (e.target.closest("button")) return;
|
||||
if (e.target.closest("input")) return;
|
||||
try { e.currentTarget?.setPointerCapture?.(e.pointerId); } catch {}
|
||||
if ((e.target as HTMLElement | null)?.closest("button")) return;
|
||||
if ((e.target as HTMLElement | null)?.closest("input")) return;
|
||||
try { (e.currentTarget as HTMLElement | null)?.setPointerCapture?.(e.pointerId); } catch {}
|
||||
getCurrentWindow().startDragging();
|
||||
}
|
||||
|
||||
@@ -310,6 +320,7 @@
|
||||
|
||||
<div class="flex flex-col h-full bg-bg">
|
||||
<!-- Drag handle -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="flex items-center h-[36px] bg-bg-elevated select-none px-3"
|
||||
onpointerdown={handleDragStart}
|
||||
@@ -469,6 +480,7 @@
|
||||
<!-- Text (editable or display) -->
|
||||
{#if editingIdx === seg._idx}
|
||||
<textarea
|
||||
bind:this={editingTextareaEl}
|
||||
class="flex-1 bg-bg-input border border-accent rounded-lg px-2 py-1 text-[13px] text-text
|
||||
leading-relaxed resize-none focus:outline-none min-h-[40px]"
|
||||
bind:value={editingText}
|
||||
@@ -476,7 +488,6 @@
|
||||
onblur={finishEditing}
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
data-no-transition
|
||||
autofocus
|
||||
></textarea>
|
||||
{:else}
|
||||
<p class="text-[13px] text-text leading-relaxed flex-1">
|
||||
|
||||
Reference in New Issue
Block a user