feat: publish HoloLake model-native living system source
This commit is contained in:
parent
6ad10edde1
commit
c395dd3a99
2467 changed files with 615073 additions and 0 deletions
|
|
@ -0,0 +1,204 @@
|
|||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { isTauri, mockInvoke } from '../../mock-tauri'
|
||||
import type { SidebarSelection, VaultEntry } from '../../types'
|
||||
|
||||
export interface FolderRenameResult {
|
||||
old_path: string
|
||||
new_path: string
|
||||
}
|
||||
|
||||
export interface FolderTab {
|
||||
entry: VaultEntry
|
||||
content: string
|
||||
}
|
||||
|
||||
export interface ConfirmFolderDeleteState {
|
||||
path: string
|
||||
title: string
|
||||
message: string
|
||||
confirmLabel: string
|
||||
}
|
||||
|
||||
export const DEFAULT_SELECTION: SidebarSelection = { kind: 'filter', filter: 'all' }
|
||||
|
||||
function trimTrailingSlash(path: string): string {
|
||||
return path.endsWith('/') ? path.slice(0, -1) : path
|
||||
}
|
||||
|
||||
export function folderAbsolutePath(params: { vaultPath: string; folderPath: string }): string {
|
||||
const normalizedVaultPath = trimTrailingSlash(params.vaultPath)
|
||||
const normalizedFolderPath = params.folderPath.replace(/^\/+/, '')
|
||||
return normalizedFolderPath ? `${normalizedVaultPath}/${normalizedFolderPath}` : normalizedVaultPath
|
||||
}
|
||||
|
||||
export function isWithinPrefix(params: { path: string; prefix: string }): boolean {
|
||||
return params.path === params.prefix || params.path.startsWith(`${params.prefix}/`)
|
||||
}
|
||||
|
||||
export function replaceFolderPrefix(params: {
|
||||
path: string
|
||||
oldPrefix: string
|
||||
newPrefix: string
|
||||
}): string {
|
||||
if (!isWithinPrefix({ path: params.path, prefix: params.oldPrefix })) return params.path
|
||||
return `${params.newPrefix}${params.path.slice(params.oldPrefix.length)}`
|
||||
}
|
||||
|
||||
export function replaceRelativeFolderPrefix(params: {
|
||||
path: string
|
||||
oldPrefix: string
|
||||
newPrefix: string
|
||||
}): string {
|
||||
if (!isWithinPrefix({ path: params.path, prefix: params.oldPrefix })) return params.path
|
||||
return `${params.newPrefix}${params.path.slice(params.oldPrefix.length)}`
|
||||
}
|
||||
|
||||
export function folderLabel(params: { folderPath: string }): string {
|
||||
return params.folderPath.split('/').filter(Boolean).at(-1) ?? params.folderPath
|
||||
}
|
||||
|
||||
export async function invokeRenameFolder(params: {
|
||||
vaultPath: string
|
||||
folderPath: string
|
||||
newName: string
|
||||
}): Promise<FolderRenameResult> {
|
||||
if (isTauri()) {
|
||||
return invoke<FolderRenameResult>('rename_vault_folder', params)
|
||||
}
|
||||
return mockInvoke<FolderRenameResult>('rename_vault_folder', params)
|
||||
}
|
||||
|
||||
export async function invokeDeleteFolder(params: { vaultPath: string; folderPath: string }): Promise<string> {
|
||||
if (isTauri()) {
|
||||
return invoke<string>('delete_vault_folder', params)
|
||||
}
|
||||
return mockInvoke<string>('delete_vault_folder', params)
|
||||
}
|
||||
|
||||
export function updateSelectionAfterFolderRename(params: {
|
||||
refreshedEntries: VaultEntry[]
|
||||
renameResult: FolderRenameResult
|
||||
selection: SidebarSelection
|
||||
setSelection: (selection: SidebarSelection) => void
|
||||
vaultPath: string
|
||||
}) {
|
||||
const {
|
||||
refreshedEntries,
|
||||
renameResult,
|
||||
selection,
|
||||
setSelection,
|
||||
vaultPath,
|
||||
} = params
|
||||
|
||||
if (selection.kind === 'folder') {
|
||||
if (!isWithinPrefix({ path: selection.path, prefix: renameResult.old_path })) return
|
||||
setSelection({
|
||||
kind: 'folder',
|
||||
path: replaceRelativeFolderPrefix({
|
||||
path: selection.path,
|
||||
oldPrefix: renameResult.old_path,
|
||||
newPrefix: renameResult.new_path,
|
||||
}),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (selection.kind !== 'entity') return
|
||||
|
||||
const oldPrefix = folderAbsolutePath({ vaultPath, folderPath: renameResult.old_path })
|
||||
if (!isWithinPrefix({ path: selection.entry.path, prefix: oldPrefix })) return
|
||||
|
||||
const renamedPath = replaceFolderPrefix({
|
||||
path: selection.entry.path,
|
||||
oldPrefix,
|
||||
newPrefix: folderAbsolutePath({ vaultPath, folderPath: renameResult.new_path }),
|
||||
})
|
||||
const refreshedEntry = refreshedEntries.find((entry) => entry.path === renamedPath)
|
||||
setSelection(refreshedEntry ? { kind: 'entity', entry: refreshedEntry } : DEFAULT_SELECTION)
|
||||
}
|
||||
|
||||
export function updateTabsAfterFolderRename(params: {
|
||||
activeTabPathRef: React.MutableRefObject<string | null>
|
||||
handleSwitchTab: (path: string) => void
|
||||
refreshedEntries: VaultEntry[]
|
||||
renameResult: FolderRenameResult
|
||||
setTabs: React.Dispatch<React.SetStateAction<FolderTab[]>>
|
||||
vaultPath: string
|
||||
}) {
|
||||
const {
|
||||
activeTabPathRef,
|
||||
handleSwitchTab,
|
||||
refreshedEntries,
|
||||
renameResult,
|
||||
setTabs,
|
||||
vaultPath,
|
||||
} = params
|
||||
const oldPrefix = folderAbsolutePath({ vaultPath, folderPath: renameResult.old_path })
|
||||
const newPrefix = folderAbsolutePath({ vaultPath, folderPath: renameResult.new_path })
|
||||
|
||||
setTabs((currentTabs) => currentTabs.map((tab) => {
|
||||
if (!isWithinPrefix({ path: tab.entry.path, prefix: oldPrefix })) return tab
|
||||
const nextPath = replaceFolderPrefix({ path: tab.entry.path, oldPrefix, newPrefix })
|
||||
const refreshedEntry = refreshedEntries.find((entry) => entry.path === nextPath)
|
||||
return refreshedEntry ? { ...tab, entry: refreshedEntry } : { ...tab, entry: { ...tab.entry, path: nextPath } }
|
||||
}))
|
||||
|
||||
const activeTabPath = activeTabPathRef.current
|
||||
if (!activeTabPath || !isWithinPrefix({ path: activeTabPath, prefix: oldPrefix })) return
|
||||
handleSwitchTab(replaceFolderPrefix({ path: activeTabPath, oldPrefix, newPrefix }))
|
||||
}
|
||||
|
||||
export function clearDeletedFolderTabs(params: {
|
||||
activeTabPathRef: React.MutableRefObject<string | null>
|
||||
closeAllTabs: () => void
|
||||
folderPath: string
|
||||
setTabs: React.Dispatch<React.SetStateAction<FolderTab[]>>
|
||||
vaultPath: string
|
||||
}) {
|
||||
const {
|
||||
activeTabPathRef,
|
||||
closeAllTabs,
|
||||
folderPath,
|
||||
setTabs,
|
||||
vaultPath,
|
||||
} = params
|
||||
const folderPrefix = folderAbsolutePath({ vaultPath, folderPath })
|
||||
const hasActiveTabInFolder = !!activeTabPathRef.current
|
||||
&& isWithinPrefix({ path: activeTabPathRef.current, prefix: folderPrefix })
|
||||
|
||||
if (hasActiveTabInFolder) {
|
||||
closeAllTabs()
|
||||
return
|
||||
}
|
||||
|
||||
setTabs((currentTabs) => currentTabs.filter((tab) => !isWithinPrefix({ path: tab.entry.path, prefix: folderPrefix })))
|
||||
}
|
||||
|
||||
export function resetSelectionIfFolderDeleted(params: {
|
||||
folderPath: string
|
||||
refreshedEntries: VaultEntry[]
|
||||
selection: SidebarSelection
|
||||
setSelection: (selection: SidebarSelection) => void
|
||||
vaultPath: string
|
||||
}) {
|
||||
const {
|
||||
folderPath,
|
||||
refreshedEntries,
|
||||
selection,
|
||||
setSelection,
|
||||
vaultPath,
|
||||
} = params
|
||||
|
||||
if (selection.kind === 'folder' && isWithinPrefix({ path: selection.path, prefix: folderPath })) {
|
||||
setSelection(DEFAULT_SELECTION)
|
||||
return
|
||||
}
|
||||
|
||||
if (selection.kind !== 'entity') return
|
||||
|
||||
const folderPrefix = folderAbsolutePath({ vaultPath, folderPath })
|
||||
if (!isWithinPrefix({ path: selection.entry.path, prefix: folderPrefix })) return
|
||||
|
||||
const replacement = refreshedEntries.find((entry) => entry.path === selection.entry.path)
|
||||
setSelection(replacement ? { kind: 'entity', entry: replacement } : DEFAULT_SELECTION)
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
import { useCallback, useState } from 'react'
|
||||
import type { SidebarSelection, VaultEntry } from '../../types'
|
||||
import {
|
||||
clearDeletedFolderTabs,
|
||||
type ConfirmFolderDeleteState,
|
||||
type FolderTab,
|
||||
folderLabel,
|
||||
invokeDeleteFolder,
|
||||
resetSelectionIfFolderDeleted,
|
||||
} from './folderActionUtils'
|
||||
|
||||
interface UseFolderDeleteInput {
|
||||
activeTabPathRef: React.MutableRefObject<string | null>
|
||||
clearFolderRename: () => void
|
||||
closeAllTabs: () => void
|
||||
reloadFolders: () => Promise<unknown>
|
||||
reloadVault: () => Promise<VaultEntry[]>
|
||||
selection: SidebarSelection
|
||||
setSelection: (selection: SidebarSelection) => void
|
||||
setTabs: React.Dispatch<React.SetStateAction<FolderTab[]>>
|
||||
setToastMessage: (message: string | null) => void
|
||||
vaultPath: string
|
||||
}
|
||||
|
||||
export function useFolderDelete({
|
||||
activeTabPathRef,
|
||||
clearFolderRename,
|
||||
closeAllTabs,
|
||||
reloadFolders,
|
||||
reloadVault,
|
||||
selection,
|
||||
setSelection,
|
||||
setTabs,
|
||||
setToastMessage,
|
||||
vaultPath,
|
||||
}: UseFolderDeleteInput) {
|
||||
const [confirmDeleteFolder, setConfirmDeleteFolder] = useState<ConfirmFolderDeleteState | null>(null)
|
||||
|
||||
const requestDeleteFolder = useCallback((folderPath: string) => {
|
||||
clearFolderRename()
|
||||
setConfirmDeleteFolder({
|
||||
path: folderPath,
|
||||
title: `Delete "${folderLabel({ folderPath })}" and everything inside it?`,
|
||||
message: 'This permanently removes the folder, all nested folders, and every note or file inside it. This cannot be undone.',
|
||||
confirmLabel: 'Delete folder',
|
||||
})
|
||||
}, [clearFolderRename])
|
||||
|
||||
const cancelDeleteFolder = useCallback(() => setConfirmDeleteFolder(null), [])
|
||||
|
||||
const confirmDeleteSelectedFolder = useCallback(async () => {
|
||||
if (!confirmDeleteFolder) return
|
||||
|
||||
const folderPath = confirmDeleteFolder.path
|
||||
try {
|
||||
setConfirmDeleteFolder(null)
|
||||
await invokeDeleteFolder({ vaultPath, folderPath })
|
||||
clearDeletedFolderTabs({
|
||||
activeTabPathRef,
|
||||
closeAllTabs,
|
||||
folderPath,
|
||||
setTabs,
|
||||
vaultPath,
|
||||
})
|
||||
await reloadFolders()
|
||||
const refreshedEntries = await reloadVault()
|
||||
resetSelectionIfFolderDeleted({
|
||||
folderPath,
|
||||
refreshedEntries,
|
||||
selection,
|
||||
setSelection,
|
||||
vaultPath,
|
||||
})
|
||||
setToastMessage(`Deleted folder "${folderLabel({ folderPath })}"`)
|
||||
} catch (error) {
|
||||
setToastMessage(`Failed to delete folder: ${error}`)
|
||||
}
|
||||
}, [activeTabPathRef, closeAllTabs, confirmDeleteFolder, reloadFolders, reloadVault, selection, setSelection, setTabs, setToastMessage, vaultPath])
|
||||
|
||||
const deleteSelectedFolder = useCallback(() => {
|
||||
if (selection.kind !== 'folder' || !selection.path) return
|
||||
requestDeleteFolder(selection.path)
|
||||
}, [requestDeleteFolder, selection])
|
||||
|
||||
return {
|
||||
cancelDeleteFolder,
|
||||
confirmDeleteFolder,
|
||||
confirmDeleteSelectedFolder,
|
||||
deleteSelectedFolder,
|
||||
requestDeleteFolder,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
import { useCallback, useState } from 'react'
|
||||
import type { SidebarSelection, VaultEntry } from '../../types'
|
||||
import {
|
||||
folderLabel,
|
||||
invokeRenameFolder,
|
||||
type FolderTab,
|
||||
updateSelectionAfterFolderRename,
|
||||
updateTabsAfterFolderRename,
|
||||
} from './folderActionUtils'
|
||||
|
||||
interface UseFolderRenameInput {
|
||||
activeTabPathRef: React.MutableRefObject<string | null>
|
||||
handleSwitchTab: (path: string) => void
|
||||
reloadFolders: () => Promise<unknown>
|
||||
reloadVault: () => Promise<VaultEntry[]>
|
||||
selection: SidebarSelection
|
||||
setSelection: (selection: SidebarSelection) => void
|
||||
setTabs: React.Dispatch<React.SetStateAction<FolderTab[]>>
|
||||
setToastMessage: (message: string | null) => void
|
||||
vaultPath: string
|
||||
}
|
||||
|
||||
export function useFolderRename({
|
||||
activeTabPathRef,
|
||||
handleSwitchTab,
|
||||
reloadFolders,
|
||||
reloadVault,
|
||||
selection,
|
||||
setSelection,
|
||||
setTabs,
|
||||
setToastMessage,
|
||||
vaultPath,
|
||||
}: UseFolderRenameInput) {
|
||||
const [renamingFolderPath, setRenamingFolderPath] = useState<string | null>(null)
|
||||
|
||||
const cancelFolderRename = useCallback(() => setRenamingFolderPath(null), [])
|
||||
const startFolderRename = useCallback((folderPath: string) => setRenamingFolderPath(folderPath), [])
|
||||
|
||||
const renameFolder = useCallback(async (folderPath: string, nextName: string) => {
|
||||
const trimmedName = nextName.trim()
|
||||
if (trimmedName === folderLabel({ folderPath })) {
|
||||
setRenamingFolderPath(null)
|
||||
return true
|
||||
}
|
||||
|
||||
try {
|
||||
const renameResult = await invokeRenameFolder({ vaultPath, folderPath, newName: trimmedName })
|
||||
setRenamingFolderPath(null)
|
||||
await reloadFolders()
|
||||
const refreshedEntries = await reloadVault()
|
||||
updateTabsAfterFolderRename({
|
||||
activeTabPathRef,
|
||||
handleSwitchTab,
|
||||
refreshedEntries,
|
||||
renameResult,
|
||||
setTabs,
|
||||
vaultPath,
|
||||
})
|
||||
updateSelectionAfterFolderRename({
|
||||
refreshedEntries,
|
||||
renameResult,
|
||||
selection,
|
||||
setSelection,
|
||||
vaultPath,
|
||||
})
|
||||
setToastMessage(`Renamed folder to "${trimmedName}"`)
|
||||
return true
|
||||
} catch (error) {
|
||||
setToastMessage(`Failed to rename folder: ${error}`)
|
||||
return false
|
||||
}
|
||||
}, [activeTabPathRef, handleSwitchTab, reloadFolders, reloadVault, selection, setSelection, setTabs, setToastMessage, vaultPath])
|
||||
|
||||
const renameSelectedFolder = useCallback(() => {
|
||||
if (selection.kind !== 'folder' || !selection.path) return
|
||||
startFolderRename(selection.path)
|
||||
}, [selection, startFolderRename])
|
||||
|
||||
return {
|
||||
cancelFolderRename,
|
||||
renameFolder,
|
||||
renameSelectedFolder,
|
||||
renamingFolderPath,
|
||||
startFolderRename,
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue