✨ chapter re-ordering
This commit is contained in:
@@ -78,3 +78,17 @@
|
||||
color: var(--accent);
|
||||
background: var(--active-bg);
|
||||
}
|
||||
|
||||
.tree-file.dragging,
|
||||
.tree-dir-header.dragging {
|
||||
opacity: 0.35;
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.tree-file.drag-over {
|
||||
box-shadow: inset 0 -2px 0 var(--accent);
|
||||
}
|
||||
|
||||
.tree-dir-header.drag-over {
|
||||
box-shadow: inset 0 -2px 0 var(--accent);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,13 @@ export function FileTree(): JSX.Element {
|
||||
<div className="file-tree-header">HOHOFF</div>
|
||||
<div className="file-tree-list">
|
||||
{fileTree.map((node) => (
|
||||
<FileTreeNode key={node.path} node={node} depth={0} />
|
||||
<FileTreeNode
|
||||
key={node.path}
|
||||
node={node}
|
||||
depth={0}
|
||||
dirPath="__root__"
|
||||
siblings={fileTree}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -2,14 +2,21 @@ import { useState } from 'react'
|
||||
import { useEditorStore } from '../../store/editorStore'
|
||||
import type { FileNode } from '../../types/editor'
|
||||
|
||||
interface DndPayload {
|
||||
name: string
|
||||
dirPath: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
node: FileNode
|
||||
depth: number
|
||||
dirPath: string
|
||||
siblings: FileNode[]
|
||||
}
|
||||
|
||||
export function FileTreeNode({ node, depth }: Props): JSX.Element {
|
||||
export function FileTreeNode({ node, depth, dirPath, siblings }: Props): JSX.Element {
|
||||
const [expanded, setExpanded] = useState(true)
|
||||
const { activeFilePath, setActiveFile } = useEditorStore()
|
||||
const { activeFilePath, setActiveFile, moveNode } = useEditorStore()
|
||||
|
||||
const openFile = async (): Promise<void> => {
|
||||
if (node.type === 'file') {
|
||||
@@ -18,6 +25,40 @@ export function FileTreeNode({ node, depth }: Props): JSX.Element {
|
||||
}
|
||||
}
|
||||
|
||||
function handleDragStart(e: React.DragEvent<HTMLElement>): void {
|
||||
e.dataTransfer.effectAllowed = 'move'
|
||||
e.dataTransfer.setData('dnd', JSON.stringify({ name: node.name, dirPath } satisfies DndPayload))
|
||||
e.currentTarget.classList.add('dragging')
|
||||
}
|
||||
|
||||
function handleDragEnd(e: React.DragEvent<HTMLElement>): void {
|
||||
e.currentTarget.classList.remove('dragging')
|
||||
}
|
||||
|
||||
function handleDragOver(e: React.DragEvent<HTMLElement>): void {
|
||||
e.preventDefault()
|
||||
e.dataTransfer.dropEffect = 'move'
|
||||
e.currentTarget.classList.add('drag-over')
|
||||
}
|
||||
|
||||
function handleDragLeave(e: React.DragEvent<HTMLElement>): void {
|
||||
e.currentTarget.classList.remove('drag-over')
|
||||
}
|
||||
|
||||
function handleDrop(e: React.DragEvent<HTMLElement>): void {
|
||||
e.preventDefault()
|
||||
e.currentTarget.classList.remove('drag-over')
|
||||
const raw = e.dataTransfer.getData('dnd')
|
||||
if (!raw) return
|
||||
const payload: DndPayload = JSON.parse(raw)
|
||||
if (payload.dirPath !== dirPath) return
|
||||
const fromIdx = siblings.findIndex((n) => n.name === payload.name)
|
||||
const toIdx = siblings.findIndex((n) => n.name === node.name)
|
||||
if (fromIdx !== -1 && toIdx !== -1 && fromIdx !== toIdx) {
|
||||
moveNode(dirPath, fromIdx, toIdx)
|
||||
}
|
||||
}
|
||||
|
||||
if (node.type === 'directory') {
|
||||
return (
|
||||
<div className="tree-dir">
|
||||
@@ -25,12 +66,24 @@ export function FileTreeNode({ node, depth }: Props): JSX.Element {
|
||||
className="tree-dir-header"
|
||||
style={{ paddingLeft: `${depth * 12 + 8}px` }}
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
draggable={true}
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
<span className="tree-arrow">{expanded ? '▾' : '▸'}</span>
|
||||
{node.name}
|
||||
</button>
|
||||
{expanded && node.children?.map((child) => (
|
||||
<FileTreeNode key={child.path} node={child} depth={depth + 1} />
|
||||
<FileTreeNode
|
||||
key={child.path}
|
||||
node={child}
|
||||
depth={depth + 1}
|
||||
dirPath={node.path}
|
||||
siblings={node.children!}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
@@ -44,6 +97,12 @@ export function FileTreeNode({ node, depth }: Props): JSX.Element {
|
||||
style={{ paddingLeft: `${depth * 12 + 20}px` }}
|
||||
onClick={openFile}
|
||||
title={node.name}
|
||||
draggable={true}
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
{node.name}
|
||||
</button>
|
||||
|
||||
@@ -35,6 +35,9 @@ interface EditorState {
|
||||
analysisMode: AnalysisMode
|
||||
setAnalysisMode: (mode: AnalysisMode) => void
|
||||
|
||||
// File tree reordering
|
||||
moveNode: (dirPath: string, fromIdx: number, toIdx: number) => void
|
||||
|
||||
// Word counts
|
||||
projectWordCount: number
|
||||
setProjectWordCount: (count: number) => void
|
||||
@@ -52,6 +55,37 @@ export const useEditorStore = create<EditorState>((set, get) => ({
|
||||
fileTree: [],
|
||||
setFileTree: (fileTree) => set({ fileTree }),
|
||||
|
||||
moveNode: (dirPath, fromIdx, toIdx) => {
|
||||
set((s) => {
|
||||
let newTree: typeof s.fileTree
|
||||
|
||||
if (dirPath === '__root__') {
|
||||
newTree = [...s.fileTree]
|
||||
const [moved] = newTree.splice(fromIdx, 1)
|
||||
newTree.splice(toIdx, 0, moved)
|
||||
} else {
|
||||
newTree = s.fileTree.map((node) => {
|
||||
if (node.path !== dirPath || !node.children) return node
|
||||
const children = [...node.children]
|
||||
const [moved] = children.splice(fromIdx, 1)
|
||||
children.splice(toIdx, 0, moved)
|
||||
return { ...node, children }
|
||||
})
|
||||
}
|
||||
|
||||
const orderMap: Record<string, string[]> = {}
|
||||
orderMap['__root__'] = newTree.map((n) => n.name)
|
||||
for (const node of newTree) {
|
||||
if (node.type === 'directory' && node.children) {
|
||||
orderMap[node.path] = node.children.map((c) => c.name)
|
||||
}
|
||||
}
|
||||
window.api.saveOrder(orderMap).catch(console.error)
|
||||
|
||||
return { fileTree: newTree }
|
||||
})
|
||||
},
|
||||
|
||||
activeFilePath: null,
|
||||
activeFileContent: '',
|
||||
isDirty: false,
|
||||
|
||||
1
src/renderer/types/global.d.ts
vendored
1
src/renderer/types/global.d.ts
vendored
@@ -12,6 +12,7 @@ declare global {
|
||||
) => Promise<void>
|
||||
removeAIListener: () => void
|
||||
getProjectWordCount: () => Promise<number>
|
||||
saveOrder: (order: Record<string, string[]>) => Promise<void>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user