chore: apply prettier formatting fixes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Alejandro Gómez
2025-12-21 20:12:40 +01:00
parent f4d0e86f09
commit 78a8c8e5b2
13 changed files with 421 additions and 190 deletions

View File

@@ -12,7 +12,10 @@ export class DeleteEventAction {
type = "delete-event";
label = "Delete Event";
async execute(item: { event?: NostrEvent }, reason: string = ""): Promise<void> {
async execute(
item: { event?: NostrEvent },
reason: string = "",
): Promise<void> {
if (!item.event) throw new Error("Item has no event to delete");
const account = accountManager.active;

View File

@@ -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({
<div className="space-y-2 text-sm">
<div className="flex items-center gap-2 text-muted-foreground">
<Clock className="size-4" />
<span>{formatDate(comparison.differences.lastModified.local)}</span>
<span>
{formatDate(comparison.differences.lastModified.local)}
</span>
</div>
<div className="flex items-center gap-2 text-muted-foreground">
<Layers className="size-4" />
<span>{comparison.differences.workspaceCount.local} workspaces</span>
<span>
{comparison.differences.workspaceCount.local} workspaces
</span>
</div>
<div className="flex items-center gap-2 text-muted-foreground">
@@ -128,7 +132,9 @@ export function ConflictResolutionDialog({
<div className="space-y-2 text-sm">
<div className="flex items-center gap-2 text-muted-foreground">
<Clock className="size-4" />
<span>{formatDate(comparison.differences.lastModified.network)}</span>
<span>
{formatDate(comparison.differences.lastModified.network)}
</span>
</div>
<div className="flex items-center gap-2 text-muted-foreground">
@@ -140,7 +146,9 @@ export function ConflictResolutionDialog({
<div className="flex items-center gap-2 text-muted-foreground">
<Layout className="size-4" />
<span>{comparison.differences.windowCount.network} windows</span>
<span>
{comparison.differences.windowCount.network} windows
</span>
</div>
{authorProfile && (

View File

@@ -118,85 +118,85 @@ export function LayoutControls() {
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-64">
{/* Layouts Section */}
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground">
Layout Presets
</div>
{presets.map((preset) => {
const canApply = windowCount >= preset.minSlots;
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground">
Layout Presets
</div>
{presets.map((preset) => {
const canApply = windowCount >= preset.minSlots;
return (
<DropdownMenuItem
key={preset.id}
onClick={() => handleApplyPreset(preset.id)}
disabled={!canApply}
className="flex items-center gap-3 cursor-pointer"
>
<div className="flex-shrink-0">{getPresetIcon(preset.id)}</div>
<div className="flex-1 min-w-0">
<div className="font-medium text-sm">{preset.name}</div>
</div>
</DropdownMenuItem>
);
})}
<DropdownMenuSeparator />
{/* Placement Section */}
<div className="px-2 py-1.5 space-y-0.5">
<div className="text-xs font-semibold text-muted-foreground">
Placement
</div>
<div className="text-xs text-muted-foreground">Window insertion</div>
</div>
{insertionModes.map((mode) => {
const Icon = mode.icon;
const isActive = layoutConfig.insertionMode === mode.id;
return (
<DropdownMenuItem
key={mode.id}
onClick={() => updateLayoutConfig({ insertionMode: mode.id })}
className="flex items-center gap-2 cursor-pointer"
>
<Icon className="h-3.5 w-3.5 text-muted-foreground" />
<span className="flex-1">{mode.label}</span>
{isActive && (
<div className="h-1.5 w-1.5 rounded-full bg-accent" />
)}
</DropdownMenuItem>
);
})}
<DropdownMenuSeparator />
{/* Split Ratio Section */}
<div className="px-2 py-2 space-y-2">
<div className="space-y-0.5">
<div className="flex items-center justify-between text-xs">
<span className="font-semibold text-muted-foreground">
Split Ratio
</span>
<span className="text-foreground">
{displayedSplitPercentage}/{100 - displayedSplitPercentage}
</span>
return (
<DropdownMenuItem
key={preset.id}
onClick={() => handleApplyPreset(preset.id)}
disabled={!canApply}
className="flex items-center gap-3 cursor-pointer"
>
<div className="flex-shrink-0">{getPresetIcon(preset.id)}</div>
<div className="flex-1 min-w-0">
<div className="font-medium text-sm">{preset.name}</div>
</div>
<div className="text-xs text-muted-foreground">
Default split for new windows
</div>
</div>
<Slider
value={[displayedSplitPercentage]}
onValueChange={([value]) => setLocalSplitPercentage(value)}
onValueCommit={([value]) => {
updateLayoutConfig({ splitPercentage: value });
setLocalSplitPercentage(null); // Clear local state after persist
}}
min={20}
max={80}
step={1}
className="w-full"
/>
</DropdownMenuItem>
);
})}
<DropdownMenuSeparator />
{/* Placement Section */}
<div className="px-2 py-1.5 space-y-0.5">
<div className="text-xs font-semibold text-muted-foreground">
Placement
</div>
</DropdownMenuContent>
</DropdownMenu>
<div className="text-xs text-muted-foreground">Window insertion</div>
</div>
{insertionModes.map((mode) => {
const Icon = mode.icon;
const isActive = layoutConfig.insertionMode === mode.id;
return (
<DropdownMenuItem
key={mode.id}
onClick={() => updateLayoutConfig({ insertionMode: mode.id })}
className="flex items-center gap-2 cursor-pointer"
>
<Icon className="h-3.5 w-3.5 text-muted-foreground" />
<span className="flex-1">{mode.label}</span>
{isActive && (
<div className="h-1.5 w-1.5 rounded-full bg-accent" />
)}
</DropdownMenuItem>
);
})}
<DropdownMenuSeparator />
{/* Split Ratio Section */}
<div className="px-2 py-2 space-y-2">
<div className="space-y-0.5">
<div className="flex items-center justify-between text-xs">
<span className="font-semibold text-muted-foreground">
Split Ratio
</span>
<span className="text-foreground">
{displayedSplitPercentage}/{100 - displayedSplitPercentage}
</span>
</div>
<div className="text-xs text-muted-foreground">
Default split for new windows
</div>
</div>
<Slider
value={[displayedSplitPercentage]}
onValueChange={([value]) => setLocalSplitPercentage(value)}
onValueCommit={([value]) => {
updateLayoutConfig({ splitPercentage: value });
setLocalSplitPercentage(null); // Clear local state after persist
}}
min={20}
max={80}
step={1}
className="w-full"
/>
</div>
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@@ -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<ViewMode>("list");
// Freeze timeline after EOSE to prevent auto-scrolling on new events
const [freezePoint, setFreezePoint] = useState<string | null>(null);
@@ -1105,6 +1115,61 @@ export default function ReqViewer({
</DropdownMenuContent>
</DropdownMenu>
{/* View Mode Toggle */}
<div className="flex items-center gap-0.5 border-l border-border pl-3">
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={() => setViewMode("list")}
className={cn(
"p-1 rounded transition-colors",
viewMode === "list"
? "text-foreground bg-muted"
: "text-muted-foreground hover:text-foreground",
)}
aria-label="List view"
>
<List className="size-3" />
</button>
</TooltipTrigger>
<TooltipContent>List view</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={() => setViewMode("compact")}
className={cn(
"p-1 rounded transition-colors",
viewMode === "compact"
? "text-foreground bg-muted"
: "text-muted-foreground hover:text-foreground",
)}
aria-label="Compact view"
>
<Rows3 className="size-3" />
</button>
</TooltipTrigger>
<TooltipContent>Compact view</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={() => setViewMode("json")}
className={cn(
"p-1 rounded transition-colors",
viewMode === "json"
? "text-foreground bg-muted"
: "text-muted-foreground hover:text-foreground",
)}
aria-label="JSON view"
>
<Braces className="size-3" />
</button>
</TooltipTrigger>
<TooltipContent>JSON view</TooltipContent>
</Tooltip>
</div>
{/* Query (Clickable) */}
<button
onClick={() => setShowQuery(!showQuery)}
@@ -1198,9 +1263,16 @@ export default function ReqViewer({
style={{ height: "100%" }}
data={visibleEvents}
computeItemKey={(_index, item) => item.id}
itemContent={(_index, event) => (
<MemoizedFeedEvent event={event} />
)}
itemContent={(_index, event) => {
switch (viewMode) {
case "compact":
return <MemoizedCompactEventRow event={event} />;
case "json":
return <MemoizedJsonEventRow event={event} />;
default:
return <MemoizedFeedEvent event={event} />;
}
}}
/>
)}
</div>

View File

@@ -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",

View File

@@ -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",

View File

@@ -224,8 +224,8 @@ export function SpellDetailRenderer({ event }: BaseEventProps) {
<div className="flex flex-col gap-6 p-4">
<div className="flex flex-col gap-2">
{spell.name && (
<ClickableEventTitle
event={event}
<ClickableEventTitle
event={event}
className="text-2xl font-bold hover:underline cursor-pointer"
>
{spell.name}

View File

@@ -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",
}}
>
<div style={{ flexGrow: splitPercentage, minHeight: "40px", minWidth: "40px", display: "flex" }}>
<div
style={{
flexGrow: splitPercentage,
minHeight: "40px",
minWidth: "40px",
display: "flex",
}}
>
{renderLayout(node.first)}
</div>
<div style={{ flexGrow: 100 - splitPercentage, minHeight: "40px", minWidth: "40px", display: "flex" }}>
<div
style={{
flexGrow: 100 - splitPercentage,
minHeight: "40px",
minWidth: "40px",
display: "flex",
}}
>
{renderLayout(node.second)}
</div>
</div>
@@ -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 && (
<span className="font-bold text-sm">
{ws.label}
</span>
<span className="font-bold text-sm">{ws.label}</span>
)}
{ws.layout && (

View File

@@ -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<number, React.ComponentType<BaseEventProps>> = {
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<number, React.ComponentType<BaseEventProps>> = {
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";

View File

@@ -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 };

View File

@@ -47,7 +47,9 @@ const storage = createJSONStorage<GrimoireState>(() => ({
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<GrimoireState>(() => ({
}
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<GrimoireState>(
const internalTemporaryStateAtom = atom<GrimoireState | null>(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<Pick<WindowInstance, "props" | "title" | "customTitle" | "commandString" | "appId">>) => {
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<LayoutConfig>) => {
setState((prev) => Logic.updateLayoutConfig(prev, layoutConfig));
}, [setState]);
const updateLayoutConfig = useCallback(
(layoutConfig: Partial<LayoutConfig>) => {
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,
};
};
};

View File

@@ -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
*/

View File

@@ -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;
export default db;