From 78a8c8e5b2cea7c29de284373153042e10b852b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20G=C3=B3mez?= Date: Sun, 21 Dec 2025 20:12:40 +0100 Subject: [PATCH] chore: apply prettier formatting fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/actions/delete-event.ts | 5 +- src/components/ConflictResolutionDialog.tsx | 18 +- src/components/LayoutControls.tsx | 154 +++++------ src/components/ReqViewer.tsx | 78 +++++- src/components/ShareSpellbookDialog.tsx | 9 +- src/components/SpellsViewer.tsx | 5 +- src/components/nostr/kinds/SpellRenderer.tsx | 4 +- .../nostr/kinds/SpellbookRenderer.tsx | 29 +- src/components/nostr/kinds/index.tsx | 6 + src/components/ui/alert.tsx | 24 +- src/core/state.ts | 249 ++++++++++++------ src/lib/imeta.ts | 22 ++ src/services/db.ts | 8 +- 13 files changed, 421 insertions(+), 190 deletions(-) diff --git a/src/actions/delete-event.ts b/src/actions/delete-event.ts index 7230a4e..bb2e2ab 100644 --- a/src/actions/delete-event.ts +++ b/src/actions/delete-event.ts @@ -12,7 +12,10 @@ export class DeleteEventAction { type = "delete-event"; label = "Delete Event"; - async execute(item: { event?: NostrEvent }, reason: string = ""): Promise { + async execute( + item: { event?: NostrEvent }, + reason: string = "", + ): Promise { if (!item.event) throw new Error("Item has no event to delete"); const account = accountManager.active; diff --git a/src/components/ConflictResolutionDialog.tsx b/src/components/ConflictResolutionDialog.tsx index 48c86e4..c2539be 100644 --- a/src/components/ConflictResolutionDialog.tsx +++ b/src/components/ConflictResolutionDialog.tsx @@ -39,7 +39,7 @@ export function ConflictResolutionDialog({ created_at: networkSpellbook.event?.created_at || 0, content: networkSpellbook.content, id: networkSpellbook.event?.id || "", - } + }, ); const authorProfile = useProfile(networkSpellbook.event?.pubkey); @@ -89,12 +89,16 @@ export function ConflictResolutionDialog({
- {formatDate(comparison.differences.lastModified.local)} + + {formatDate(comparison.differences.lastModified.local)} +
- {comparison.differences.workspaceCount.local} workspaces + + {comparison.differences.workspaceCount.local} workspaces +
@@ -128,7 +132,9 @@ export function ConflictResolutionDialog({
- {formatDate(comparison.differences.lastModified.network)} + + {formatDate(comparison.differences.lastModified.network)} +
@@ -140,7 +146,9 @@ export function ConflictResolutionDialog({
- {comparison.differences.windowCount.network} windows + + {comparison.differences.windowCount.network} windows +
{authorProfile && ( diff --git a/src/components/LayoutControls.tsx b/src/components/LayoutControls.tsx index f5fef91..30faee8 100644 --- a/src/components/LayoutControls.tsx +++ b/src/components/LayoutControls.tsx @@ -118,85 +118,85 @@ export function LayoutControls() { {/* Layouts Section */} -
- Layout Presets -
- {presets.map((preset) => { - const canApply = windowCount >= preset.minSlots; +
+ Layout Presets +
+ {presets.map((preset) => { + const canApply = windowCount >= preset.minSlots; - return ( - handleApplyPreset(preset.id)} - disabled={!canApply} - className="flex items-center gap-3 cursor-pointer" - > -
{getPresetIcon(preset.id)}
-
-
{preset.name}
-
-
- ); - })} - - - - {/* Placement Section */} -
-
- Placement -
-
Window insertion
-
- {insertionModes.map((mode) => { - const Icon = mode.icon; - const isActive = layoutConfig.insertionMode === mode.id; - return ( - updateLayoutConfig({ insertionMode: mode.id })} - className="flex items-center gap-2 cursor-pointer" - > - - {mode.label} - {isActive && ( -
- )} - - ); - })} - - - - {/* Split Ratio Section */} -
-
-
- - Split Ratio - - - {displayedSplitPercentage}/{100 - displayedSplitPercentage} - + return ( + handleApplyPreset(preset.id)} + disabled={!canApply} + className="flex items-center gap-3 cursor-pointer" + > +
{getPresetIcon(preset.id)}
+
+
{preset.name}
-
- Default split for new windows -
-
- setLocalSplitPercentage(value)} - onValueCommit={([value]) => { - updateLayoutConfig({ splitPercentage: value }); - setLocalSplitPercentage(null); // Clear local state after persist - }} - min={20} - max={80} - step={1} - className="w-full" - /> + + ); + })} + + + + {/* Placement Section */} +
+
+ Placement
- - +
Window insertion
+
+ {insertionModes.map((mode) => { + const Icon = mode.icon; + const isActive = layoutConfig.insertionMode === mode.id; + return ( + updateLayoutConfig({ insertionMode: mode.id })} + className="flex items-center gap-2 cursor-pointer" + > + + {mode.label} + {isActive && ( +
+ )} + + ); + })} + + + + {/* Split Ratio Section */} +
+
+
+ + Split Ratio + + + {displayedSplitPercentage}/{100 - displayedSplitPercentage} + +
+
+ Default split for new windows +
+
+ setLocalSplitPercentage(value)} + onValueCommit={([value]) => { + updateLayoutConfig({ splitPercentage: value }); + setLocalSplitPercentage(null); // Clear local state after persist + }} + min={20} + max={80} + step={1} + className="w-full" + /> +
+ + ); } diff --git a/src/components/ReqViewer.tsx b/src/components/ReqViewer.tsx index 8df7b67..c78d22c 100644 --- a/src/components/ReqViewer.tsx +++ b/src/components/ReqViewer.tsx @@ -16,6 +16,9 @@ import { Loader2, Mail, Send, + List, + Rows3, + Braces, } from "lucide-react"; import { Virtuoso } from "react-virtuoso"; import { useReqTimeline } from "@/hooks/useReqTimeline"; @@ -71,6 +74,12 @@ import { SyntaxHighlight } from "@/components/SyntaxHighlight"; import { getConnectionIcon, getAuthIcon } from "@/lib/relay-status-utils"; import { resolveFilterAliases, getTagValues } from "@/lib/nostr-utils"; import { useNostrEvent } from "@/hooks/useNostrEvent"; +import { cn } from "@/lib/utils"; +import { MemoizedCompactEventRow } from "./nostr/CompactEventRow"; +import { MemoizedJsonEventRow } from "./nostr/JsonEventRow"; + +// View mode type +type ViewMode = "list" | "compact" | "json"; // Memoized FeedEvent to prevent unnecessary re-renders during scroll const MemoizedFeedEvent = memo( @@ -747,6 +756,7 @@ export default function ReqViewer({ const [exportFilename, setExportFilename] = useState(""); const [isExporting, setIsExporting] = useState(false); const [exportProgress, setExportProgress] = useState(0); + const [viewMode, setViewMode] = useState("list"); // Freeze timeline after EOSE to prevent auto-scrolling on new events const [freezePoint, setFreezePoint] = useState(null); @@ -1105,6 +1115,61 @@ export default function ReqViewer({ + {/* View Mode Toggle */} +
+ + + + + List view + + + + + + Compact view + + + + + + JSON view + +
+ {/* Query (Clickable) */}
diff --git a/src/components/ShareSpellbookDialog.tsx b/src/components/ShareSpellbookDialog.tsx index b479166..55544f9 100644 --- a/src/components/ShareSpellbookDialog.tsx +++ b/src/components/ShareSpellbookDialog.tsx @@ -19,7 +19,11 @@ interface ShareFormat { id: string; label: string; description: string; - getValue: (event: NostrEvent, spellbook: ParsedSpellbook, actor: string) => string; + getValue: ( + event: NostrEvent, + spellbook: ParsedSpellbook, + actor: string, + ) => string; } interface ShareSpellbookDialogProps { @@ -48,7 +52,8 @@ export function ShareSpellbookDialog({ id: "web", label: "Web Link", description: "Share as a web URL that anyone can open", - getValue: (_e, s, a) => `${window.location.origin}/preview/${a}/${s.slug}`, + getValue: (_e, s, a) => + `${window.location.origin}/preview/${a}/${s.slug}`, }, { id: "naddr", diff --git a/src/components/SpellsViewer.tsx b/src/components/SpellsViewer.tsx index fc1b8b7..530b1fc 100644 --- a/src/components/SpellsViewer.tsx +++ b/src/components/SpellsViewer.tsx @@ -300,7 +300,10 @@ export function SpellsViewer() { // 1. If published, send Nostr Kind 5 if (isPublic && spell.event) { toast.promise( - new DeleteEventAction().execute({ event: spell.event }, "Deleted by user in Grimoire"), + new DeleteEventAction().execute( + { event: spell.event }, + "Deleted by user in Grimoire", + ), { loading: "Sending Nostr deletion request...", success: "Deletion request broadcasted", diff --git a/src/components/nostr/kinds/SpellRenderer.tsx b/src/components/nostr/kinds/SpellRenderer.tsx index 0207d4a..d3a5f71 100644 --- a/src/components/nostr/kinds/SpellRenderer.tsx +++ b/src/components/nostr/kinds/SpellRenderer.tsx @@ -224,8 +224,8 @@ export function SpellDetailRenderer({ event }: BaseEventProps) {
{spell.name && ( - {spell.name} diff --git a/src/components/nostr/kinds/SpellbookRenderer.tsx b/src/components/nostr/kinds/SpellbookRenderer.tsx index b863922..2f73539 100644 --- a/src/components/nostr/kinds/SpellbookRenderer.tsx +++ b/src/components/nostr/kinds/SpellbookRenderer.tsx @@ -145,7 +145,12 @@ function LayoutVisualizer({ } // Branch node - split - if (node && typeof node === "object" && "first" in node && "second" in node) { + if ( + node && + typeof node === "object" && + "first" in node && + "second" in node + ) { const isRow = node.direction === "row"; const splitPercentage = node.splitPercentage ?? 50; // Default to 50/50 if not specified @@ -160,10 +165,24 @@ function LayoutVisualizer({ minWidth: isRow ? "80px" : "40px", }} > -
+
{renderLayout(node.first)}
-
+
{renderLayout(node.second)}
@@ -365,9 +384,7 @@ export function SpellbookDetailRenderer({ event }: { event: NostrEvent }) { className="flex flex-col gap-3 p-4 rounded-xl border border-border bg-card/50" > {ws.label && ( - - {ws.label} - + {ws.label} )} {ws.layout && ( diff --git a/src/components/nostr/kinds/index.tsx b/src/components/nostr/kinds/index.tsx index e4e4581..b251e88 100644 --- a/src/components/nostr/kinds/index.tsx +++ b/src/components/nostr/kinds/index.tsx @@ -10,6 +10,7 @@ import { Kind9Renderer } from "./ChatMessageRenderer"; import { Kind20Renderer } from "./PictureRenderer"; import { Kind21Renderer } from "./VideoRenderer"; import { Kind22Renderer } from "./ShortVideoRenderer"; +import { VoiceMessageRenderer } from "./VoiceMessageRenderer"; import { Kind1063Renderer } from "./FileMetadataRenderer"; import { Kind1337Renderer } from "./CodeSnippetRenderer"; import { Kind1337DetailRenderer } from "./CodeSnippetDetailRenderer"; @@ -63,6 +64,8 @@ const kindRenderers: Record> = { 22: Kind22Renderer, // Short Video (NIP-71) 1063: Kind1063Renderer, // File Metadata (NIP-94) 1111: Kind1111Renderer, // Post (NIP-22) + 1222: VoiceMessageRenderer, // Voice Message (NIP-A0) + 1244: VoiceMessageRenderer, // Voice Message Reply (NIP-A0) 1337: Kind1337Renderer, // Code Snippet (NIP-C0) 1617: PatchRenderer, // Patch (NIP-34) 1618: PullRequestRenderer, // Pull Request (NIP-34) @@ -78,6 +81,8 @@ const kindRenderers: Record> = { 30002: GenericRelayListRenderer, // Relay Sets (NIP-51) 30023: Kind30023Renderer, // Long-form Article 30311: LiveActivityRenderer, // Live Streaming Event (NIP-53) + 34235: Kind21Renderer, // Horizontal Video (NIP-71 legacy) + 34236: Kind22Renderer, // Vertical Video (NIP-71 legacy) 30617: RepositoryRenderer, // Repository (NIP-34) 30618: RepositoryStateRenderer, // Repository State (NIP-34) 30777: SpellbookRenderer, // Spellbook (Grimoire) @@ -185,5 +190,6 @@ export { Kind9Renderer } from "./ChatMessageRenderer"; export { Kind20Renderer } from "./PictureRenderer"; export { Kind21Renderer } from "./VideoRenderer"; export { Kind22Renderer } from "./ShortVideoRenderer"; +export { VoiceMessageRenderer } from "./VoiceMessageRenderer"; export { Kind1063Renderer } from "./FileMetadataRenderer"; export { Kind9735Renderer } from "./ZapReceiptRenderer"; diff --git a/src/components/ui/alert.tsx b/src/components/ui/alert.tsx index 41fa7e0..13219e7 100644 --- a/src/components/ui/alert.tsx +++ b/src/components/ui/alert.tsx @@ -1,7 +1,7 @@ -import * as React from "react" -import { cva, type VariantProps } from "class-variance-authority" +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; const alertVariants = cva( "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", @@ -16,8 +16,8 @@ const alertVariants = cva( defaultVariants: { variant: "default", }, - } -) + }, +); const Alert = React.forwardRef< HTMLDivElement, @@ -29,8 +29,8 @@ const Alert = React.forwardRef< className={cn(alertVariants({ variant }), className)} {...props} /> -)) -Alert.displayName = "Alert" +)); +Alert.displayName = "Alert"; const AlertTitle = React.forwardRef< HTMLParagraphElement, @@ -41,8 +41,8 @@ const AlertTitle = React.forwardRef< className={cn("mb-1 font-medium leading-none tracking-tight", className)} {...props} /> -)) -AlertTitle.displayName = "AlertTitle" +)); +AlertTitle.displayName = "AlertTitle"; const AlertDescription = React.forwardRef< HTMLParagraphElement, @@ -53,7 +53,7 @@ const AlertDescription = React.forwardRef< className={cn("text-sm [&_p]:leading-relaxed", className)} {...props} /> -)) -AlertDescription.displayName = "AlertDescription" +)); +AlertDescription.displayName = "AlertDescription"; -export { Alert, AlertTitle, AlertDescription } +export { Alert, AlertTitle, AlertDescription }; diff --git a/src/core/state.ts b/src/core/state.ts index fb30a91..dd5e37f 100644 --- a/src/core/state.ts +++ b/src/core/state.ts @@ -47,7 +47,9 @@ const storage = createJSONStorage(() => ({ const storedVersion = parsed.__version || 5; if (storedVersion < CURRENT_VERSION) { - console.log(`[Storage] State version outdated (v${storedVersion}), migrating...`); + console.log( + `[Storage] State version outdated (v${storedVersion}), migrating...`, + ); const migrated = migrateState(parsed); localStorage.setItem(key, JSON.stringify(migrated)); toast.success("State Updated", { @@ -58,7 +60,9 @@ const storage = createJSONStorage(() => ({ } if (!validateState(parsed)) { - console.warn("[Storage] State validation failed, resetting to initial state"); + console.warn( + "[Storage] State validation failed, resetting to initial state", + ); toast.error("State Corrupted", { description: "Your state was corrupted and has been reset.", duration: 5000, @@ -98,39 +102,39 @@ export const grimoireStateAtom = atomWithStorage( const internalTemporaryStateAtom = atom(null); // Types for dispatch actions -type StateAction = - | { type: 'UPDATE', updater: (prev: GrimoireState) => GrimoireState } - | { type: 'START_TEMP', spellbook?: ParsedSpellbook } - | { type: 'APPLY_TEMP' } - | { type: 'DISCARD_TEMP' }; +type StateAction = + | { type: "UPDATE"; updater: (prev: GrimoireState) => GrimoireState } + | { type: "START_TEMP"; spellbook?: ParsedSpellbook } + | { type: "APPLY_TEMP" } + | { type: "DISCARD_TEMP" }; // Derived atom that handles the switching logic and updates const activeGrimoireStateAtom = atom( (get) => get(internalTemporaryStateAtom) || get(grimoireStateAtom), (get, set, action: StateAction) => { - if (action.type === 'UPDATE') { + if (action.type === "UPDATE") { const temp = get(internalTemporaryStateAtom); if (temp) { set(internalTemporaryStateAtom, action.updater(temp)); } else { set(grimoireStateAtom, action.updater); } - } else if (action.type === 'START_TEMP') { + } else if (action.type === "START_TEMP") { const current = get(grimoireStateAtom); - const next = action.spellbook + const next = action.spellbook ? SpellbookManager.loadSpellbook(current, action.spellbook) : { ...current }; set(internalTemporaryStateAtom, next); - } else if (action.type === 'APPLY_TEMP') { + } else if (action.type === "APPLY_TEMP") { const temp = get(internalTemporaryStateAtom); if (temp) { set(grimoireStateAtom, temp); set(internalTemporaryStateAtom, null); } - } else if (action.type === 'DISCARD_TEMP') { + } else if (action.type === "DISCARD_TEMP") { set(internalTemporaryStateAtom, null); } - } + }, ); // The Hook @@ -140,9 +144,12 @@ export const useGrimoire = () => { const isTemporary = useAtomValue(internalTemporaryStateAtom) !== null; const browserLocale = useLocale(); - const setState = useCallback((updater: (prev: GrimoireState) => GrimoireState) => { - dispatch({ type: 'UPDATE', updater }); - }, [dispatch]); + const setState = useCallback( + (updater: (prev: GrimoireState) => GrimoireState) => { + dispatch({ type: "UPDATE", updater }); + }, + [dispatch], + ); // Initialize locale from browser if not set useEffect(() => { @@ -153,96 +160,180 @@ export const useGrimoire = () => { const createWorkspace = useCallback(() => { setState((prev) => { - const nextNumber = Logic.findLowestAvailableWorkspaceNumber(prev.workspaces); + const nextNumber = Logic.findLowestAvailableWorkspaceNumber( + prev.workspaces, + ); return Logic.createWorkspace(prev, nextNumber); }); }, [setState]); - const createWorkspaceWithNumber = useCallback((number: number) => { - setState((prev) => { - const currentWorkspace = prev.workspaces[prev.activeWorkspaceId]; - const shouldDeleteCurrent = currentWorkspace && currentWorkspace.windowIds.length === 0 && Object.keys(prev.workspaces).length > 1; - const baseState = shouldDeleteCurrent ? Logic.deleteWorkspace(prev, prev.activeWorkspaceId) : prev; - return Logic.createWorkspace(baseState, number); - }); - }, [setState]); + const createWorkspaceWithNumber = useCallback( + (number: number) => { + setState((prev) => { + const currentWorkspace = prev.workspaces[prev.activeWorkspaceId]; + const shouldDeleteCurrent = + currentWorkspace && + currentWorkspace.windowIds.length === 0 && + Object.keys(prev.workspaces).length > 1; + const baseState = shouldDeleteCurrent + ? Logic.deleteWorkspace(prev, prev.activeWorkspaceId) + : prev; + return Logic.createWorkspace(baseState, number); + }); + }, + [setState], + ); - const addWindow = useCallback((appId: AppId, props: any, commandString?: string, customTitle?: string, spellId?: string) => { - setState((prev) => Logic.addWindow(prev, { appId, props, commandString, customTitle, spellId })); - }, [setState]); + const addWindow = useCallback( + ( + appId: AppId, + props: any, + commandString?: string, + customTitle?: string, + spellId?: string, + ) => { + setState((prev) => + Logic.addWindow(prev, { + appId, + props, + commandString, + customTitle, + spellId, + }), + ); + }, + [setState], + ); - const updateWindow = useCallback((windowId: string, updates: Partial>) => { - setState((prev) => Logic.updateWindow(prev, windowId, updates)); - }, [setState]); + const updateWindow = useCallback( + ( + windowId: string, + updates: Partial< + Pick< + WindowInstance, + "props" | "title" | "customTitle" | "commandString" | "appId" + > + >, + ) => { + setState((prev) => Logic.updateWindow(prev, windowId, updates)); + }, + [setState], + ); - const removeWindow = useCallback((id: string) => { - setState((prev) => Logic.removeWindow(prev, id)); - }, [setState]); + const removeWindow = useCallback( + (id: string) => { + setState((prev) => Logic.removeWindow(prev, id)); + }, + [setState], + ); - const moveWindowToWorkspace = useCallback((windowId: string, targetWorkspaceId: string) => { - setState((prev) => Logic.moveWindowToWorkspace(prev, windowId, targetWorkspaceId)); - }, [setState]); + const moveWindowToWorkspace = useCallback( + (windowId: string, targetWorkspaceId: string) => { + setState((prev) => + Logic.moveWindowToWorkspace(prev, windowId, targetWorkspaceId), + ); + }, + [setState], + ); - const updateLayout = useCallback((layout: any) => { - setState((prev) => Logic.updateLayout(prev, layout)); - }, [setState]); + const updateLayout = useCallback( + (layout: any) => { + setState((prev) => Logic.updateLayout(prev, layout)); + }, + [setState], + ); - const setActiveWorkspace = useCallback((id: string) => { - setState((prev) => { - if (!prev.workspaces[id] || prev.activeWorkspaceId === id) return prev; - const currentWorkspace = prev.workspaces[prev.activeWorkspaceId]; - const shouldDeleteCurrent = currentWorkspace && currentWorkspace.windowIds.length === 0 && Object.keys(prev.workspaces).length > 1; - const baseState = shouldDeleteCurrent ? Logic.deleteWorkspace(prev, prev.activeWorkspaceId) : prev; - return { ...baseState, activeWorkspaceId: id }; - }); - }, [setState]); + const setActiveWorkspace = useCallback( + (id: string) => { + setState((prev) => { + if (!prev.workspaces[id] || prev.activeWorkspaceId === id) return prev; + const currentWorkspace = prev.workspaces[prev.activeWorkspaceId]; + const shouldDeleteCurrent = + currentWorkspace && + currentWorkspace.windowIds.length === 0 && + Object.keys(prev.workspaces).length > 1; + const baseState = shouldDeleteCurrent + ? Logic.deleteWorkspace(prev, prev.activeWorkspaceId) + : prev; + return { ...baseState, activeWorkspaceId: id }; + }); + }, + [setState], + ); - const setActiveAccount = useCallback((pubkey: string | undefined) => { - setState((prev) => Logic.setActiveAccount(prev, pubkey)); - }, [setState]); + const setActiveAccount = useCallback( + (pubkey: string | undefined) => { + setState((prev) => Logic.setActiveAccount(prev, pubkey)); + }, + [setState], + ); - const setActiveAccountRelays = useCallback((relays: RelayInfo[]) => { - setState((prev) => Logic.setActiveAccountRelays(prev, relays)); - }, [setState]); + const setActiveAccountRelays = useCallback( + (relays: RelayInfo[]) => { + setState((prev) => Logic.setActiveAccountRelays(prev, relays)); + }, + [setState], + ); - const updateLayoutConfig = useCallback((layoutConfig: Partial) => { - setState((prev) => Logic.updateLayoutConfig(prev, layoutConfig)); - }, [setState]); + const updateLayoutConfig = useCallback( + (layoutConfig: Partial) => { + setState((prev) => Logic.updateLayoutConfig(prev, layoutConfig)); + }, + [setState], + ); - const applyPresetLayout = useCallback((preset: any) => { - setState((prev) => Logic.applyPresetLayout(prev, preset)); - }, [setState]); + const applyPresetLayout = useCallback( + (preset: any) => { + setState((prev) => Logic.applyPresetLayout(prev, preset)); + }, + [setState], + ); - const updateWorkspaceLabel = useCallback((workspaceId: string, label: string | undefined) => { - setState((prev) => Logic.updateWorkspaceLabel(prev, workspaceId, label)); - }, [setState]); + const updateWorkspaceLabel = useCallback( + (workspaceId: string, label: string | undefined) => { + setState((prev) => Logic.updateWorkspaceLabel(prev, workspaceId, label)); + }, + [setState], + ); - const reorderWorkspaces = useCallback((orderedIds: string[]) => { - setState((prev) => Logic.reorderWorkspaces(prev, orderedIds)); - }, [setState]); + const reorderWorkspaces = useCallback( + (orderedIds: string[]) => { + setState((prev) => Logic.reorderWorkspaces(prev, orderedIds)); + }, + [setState], + ); - const setCompactModeKinds = useCallback((kinds: number[]) => { - setState((prev) => Logic.setCompactModeKinds(prev, kinds)); - }, [setState]); + const setCompactModeKinds = useCallback( + (kinds: number[]) => { + setState((prev) => Logic.setCompactModeKinds(prev, kinds)); + }, + [setState], + ); - const loadSpellbook = useCallback((spellbook: ParsedSpellbook) => { - setState((prev) => SpellbookManager.loadSpellbook(prev, spellbook)); - }, [setState]); + const loadSpellbook = useCallback( + (spellbook: ParsedSpellbook) => { + setState((prev) => SpellbookManager.loadSpellbook(prev, spellbook)); + }, + [setState], + ); const clearActiveSpellbook = useCallback(() => { setState((prev) => Logic.clearActiveSpellbook(prev)); }, [setState]); - const switchToTemporary = useCallback((spellbook?: ParsedSpellbook) => { - dispatch({ type: 'START_TEMP', spellbook }); - }, [dispatch]); + const switchToTemporary = useCallback( + (spellbook?: ParsedSpellbook) => { + dispatch({ type: "START_TEMP", spellbook }); + }, + [dispatch], + ); const applyTemporaryToPersistent = useCallback(() => { - dispatch({ type: 'APPLY_TEMP' }); + dispatch({ type: "APPLY_TEMP" }); }, [dispatch]); const discardTemporary = useCallback(() => { - dispatch({ type: 'DISCARD_TEMP' }); + dispatch({ type: "DISCARD_TEMP" }); }, [dispatch]); return { @@ -271,4 +362,4 @@ export const useGrimoire = () => { applyTemporaryToPersistent, discardTemporary, }; -}; \ No newline at end of file +}; diff --git a/src/lib/imeta.ts b/src/lib/imeta.ts index f4de03f..526fdfe 100644 --- a/src/lib/imeta.ts +++ b/src/lib/imeta.ts @@ -13,6 +13,7 @@ export interface ImetaEntry { x?: string; // SHA-256 hash size?: string; // file size in bytes fallback?: string[]; // fallback URLs + duration?: number; // audio/video duration in seconds (NIP-A0) } /** @@ -37,6 +38,11 @@ export function parseImetaTag(tag: string[]): ImetaEntry | null { } else if (key === "fallback") { if (!entry.fallback) entry.fallback = []; entry.fallback.push(value); + } else if (key === "duration") { + const parsed = parseFloat(value); + if (!isNaN(parsed)) { + entry.duration = parsed; + } } else { (entry as any)[key] = value; } @@ -136,6 +142,22 @@ export function isAudioMime(mime?: string): boolean { return mime.startsWith("audio/"); } +/** + * Format duration in seconds to MM:SS or H:MM:SS format + */ +export function formatDuration(seconds?: number): string | null { + if (seconds === undefined || seconds < 0) return null; + + const hrs = Math.floor(seconds / 3600); + const mins = Math.floor((seconds % 3600) / 60); + const secs = Math.floor(seconds % 60); + + if (hrs > 0) { + return `${hrs}:${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`; + } + return `${mins}:${secs.toString().padStart(2, "0")}`; +} + /** * Format file size for display */ diff --git a/src/services/db.ts b/src/services/db.ts index 805519c..0bb2fa9 100644 --- a/src/services/db.ts +++ b/src/services/db.ts @@ -3,7 +3,11 @@ import { Dexie, Table } from "dexie"; import { RelayInformation } from "../types/nip11"; import { normalizeRelayURL } from "../lib/relay-url"; import type { NostrEvent } from "@/types/nostr"; -import type { SpellEvent, SpellbookContent, SpellbookEvent } from "@/types/spell"; +import type { + SpellEvent, + SpellbookContent, + SpellbookEvent, +} from "@/types/spell"; export interface Profile extends ProfileContent { pubkey: string; @@ -334,4 +338,4 @@ export const relayLivenessStorage = { }, }; -export default db; \ No newline at end of file +export default db;