hololake-platform/src/hooks/useNoteWindowLifecycle.ts

188 lines
5.7 KiB
TypeScript
Raw Normal View History

import { useEffect, useRef, type RefObject } from 'react'
import { invoke } from '@tauri-apps/api/core'
import { isTauri, mockInvoke } from '../mock-tauri'
import type { VaultEntry } from '../types'
import {
getNoteWindowPathCandidates,
type NoteWindowParams,
} from '../utils/windowMode'
interface NoteWindowActions {
handleSelectNote: (entry: VaultEntry) => unknown
openTabWithContent: (entry: VaultEntry, content: string) => unknown
}
interface UseNoteWindowLifecycleArgs extends NoteWindowActions {
activeTabPath: string | null
noteWindowParams: NoteWindowParams | null
setToastMessage: (message: string) => void
tabs: Array<{ entry: VaultEntry }>
}
interface OpenNoteWindowArgs {
actionsRef: RefObject<NoteWindowActions>
missingPathRef: RefObject<string | null>
noteWindowParams: NoteWindowParams
openedRef: RefObject<boolean>
setToastMessage: (message: string) => void
}
interface OpenResolvedNoteWindowEntryArgs extends Omit<OpenNoteWindowArgs, 'setToastMessage'> {
entry: VaultEntry
}
async function resolveNoteWindowEntry(noteWindowParams: NoteWindowParams): Promise<VaultEntry | undefined> {
for (const path of getNoteWindowPathCandidates(noteWindowParams)) {
try {
const request = { path, vaultPath: noteWindowParams.vaultPath }
const entry = isTauri()
? await invoke<VaultEntry | null>('reload_vault_entry', request)
: await mockInvoke<VaultEntry | null>('reload_vault_entry', request)
if (entry) return entry
} catch (error) {
console.warn('Failed to resolve note window candidate:', error)
}
}
}
async function loadNoteWindowContent(path: string, vaultPath: string): Promise<string> {
const request = { path, vaultPath }
if (!isTauri()) return mockInvoke<string>('get_note_content', request)
await syncVaultAssetScope(vaultPath)
return invoke<string>('get_note_content', request)
}
export async function syncVaultAssetScope(vaultPath: string): Promise<void> {
if (!isTauri() || !vaultPath.trim()) return
await invoke('sync_vault_asset_scope_for_window', { vaultPath })
}
async function openNoteWindowEntry({
actionsRef,
entry,
missingPathRef,
noteWindowParams,
openedRef,
}: OpenResolvedNoteWindowEntryArgs): Promise<void> {
try {
const content = await loadNoteWindowContent(entry.path, noteWindowParams.vaultPath)
if (openedRef.current) return
openedRef.current = true
missingPathRef.current = null
actionsRef.current.openTabWithContent(entry, content)
} catch (error) {
console.warn('Failed to load note window content before opening fallback:', error)
if (openedRef.current) return
openedRef.current = true
missingPathRef.current = null
void Promise.resolve(actionsRef.current.handleSelectNote(entry)).catch((selectError) => {
console.warn('Failed to select note after note-window content fallback:', selectError)
})
}
}
function reportMissingNoteWindowPath({
missingPathRef,
noteWindowParams,
setToastMessage,
}: Pick<OpenNoteWindowArgs, 'missingPathRef' | 'noteWindowParams' | 'setToastMessage'>): void {
if (missingPathRef.current === noteWindowParams.notePath) return
missingPathRef.current = noteWindowParams.notePath
setToastMessage(`Could not open "${noteWindowParams.noteTitle}" in this window`)
}
async function resolveAndOpenNoteWindow({
actionsRef,
missingPathRef,
noteWindowParams,
openedRef,
setToastMessage,
}: OpenNoteWindowArgs): Promise<void> {
const entry = await resolveNoteWindowEntry(noteWindowParams)
if (openedRef.current) return
if (!entry) {
reportMissingNoteWindowPath({ missingPathRef, noteWindowParams, setToastMessage })
return
}
await openNoteWindowEntry({
actionsRef,
entry,
missingPathRef,
noteWindowParams,
openedRef,
})
}
function useNoteWindowActionsRef({
handleSelectNote,
openTabWithContent,
}: NoteWindowActions): RefObject<NoteWindowActions> {
const actionsRef = useRef<NoteWindowActions>({ handleSelectNote, openTabWithContent })
useEffect(() => {
actionsRef.current = { handleSelectNote, openTabWithContent }
}, [handleSelectNote, openTabWithContent])
return actionsRef
}
function useOpenNoteWindowOnMount(
noteWindowParams: NoteWindowParams | null,
actionsRef: RefObject<NoteWindowActions>,
setToastMessage: (message: string) => void,
): void {
const openedRef = useRef(false)
const missingPathRef = useRef<string | null>(null)
useEffect(() => {
if (!noteWindowParams || openedRef.current) return
void resolveAndOpenNoteWindow({
actionsRef,
missingPathRef,
noteWindowParams,
openedRef,
setToastMessage,
})
}, [actionsRef, noteWindowParams, setToastMessage])
}
function useNoteWindowTitle(
noteWindowParams: NoteWindowParams | null,
tabs: Array<{ entry: VaultEntry }>,
activeTabPath: string | null,
): void {
useEffect(() => {
if (!noteWindowParams) return
const activeEntry = tabs.find((tab) => tab.entry.path === activeTabPath)?.entry
const title = activeEntry?.title ?? noteWindowParams.noteTitle
if (!isTauri()) {
document.title = title
return
}
import('@tauri-apps/api/window')
.then(({ getCurrentWindow }) => {
getCurrentWindow().setTitle(title)
})
.catch((err) => console.warn('[window] Failed to update note window title:', err))
}, [activeTabPath, noteWindowParams, tabs])
}
export function useNoteWindowLifecycle({
activeTabPath,
handleSelectNote,
noteWindowParams,
openTabWithContent,
setToastMessage,
tabs,
}: UseNoteWindowLifecycleArgs): void {
const actionsRef = useNoteWindowActionsRef({ handleSelectNote, openTabWithContent })
useOpenNoteWindowOnMount(noteWindowParams, actionsRef, setToastMessage)
useNoteWindowTitle(noteWindowParams, tabs, activeTabPath)
}