feat: ship v0.3.0 with P2.1 editor polish and P3 AI collaboration
Add chapter reorder, search replace, inspiration capture, and AI chat with context editing, settings, offline handling, and naming checks. Fix dev black screen by keeping process.env reads in the main process only. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { useAppStore } from '@renderer/stores/useAppStore'
|
||||
@@ -122,6 +122,55 @@ export function EditorLayout(): React.JSX.Element {
|
||||
setActiveVolume(vol.id)
|
||||
}
|
||||
|
||||
const [dragOverChapterId, setDragOverChapterId] = useState<string | null>(null)
|
||||
|
||||
const handleChapterDropOnItem = async (
|
||||
draggedId: string,
|
||||
targetVolumeId: string,
|
||||
targetChapterId: string
|
||||
): Promise<void> => {
|
||||
if (!currentBookId || draggedId === targetChapterId) return
|
||||
await flushEditorSave()
|
||||
const dragged = chapters.find((c) => c.id === draggedId)
|
||||
if (!dragged) return
|
||||
const volChapters = chapters
|
||||
.filter((c) => c.volumeId === targetVolumeId)
|
||||
.sort((a, b) => a.sortOrder - b.sortOrder)
|
||||
const targetIdx = volChapters.findIndex((c) => c.id === targetChapterId)
|
||||
if (dragged.volumeId === targetVolumeId) {
|
||||
const ids = volChapters.map((c) => c.id)
|
||||
const from = ids.indexOf(draggedId)
|
||||
if (from < 0) return
|
||||
ids.splice(from, 1)
|
||||
const insertAt = ids.indexOf(targetChapterId)
|
||||
ids.splice(insertAt, 0, draggedId)
|
||||
await ipcCall(() => window.electronAPI.chapter.reorder(currentBookId, targetVolumeId, ids))
|
||||
} else {
|
||||
await ipcCall(() =>
|
||||
window.electronAPI.chapter.moveToVolume(
|
||||
currentBookId,
|
||||
draggedId,
|
||||
targetVolumeId,
|
||||
Math.max(0, targetIdx)
|
||||
)
|
||||
)
|
||||
}
|
||||
await refreshChapters()
|
||||
}
|
||||
|
||||
const handleChapterDropOnVolume = async (draggedId: string, targetVolumeId: string): Promise<void> => {
|
||||
if (!currentBookId) return
|
||||
await flushEditorSave()
|
||||
const dragged = chapters.find((c) => c.id === draggedId)
|
||||
if (!dragged) return
|
||||
const targetLen = chapters.filter((c) => c.volumeId === targetVolumeId).length
|
||||
if (dragged.volumeId === targetVolumeId) return
|
||||
await ipcCall(() =>
|
||||
window.electronAPI.chapter.moveToVolume(currentBookId, draggedId, targetVolumeId, targetLen)
|
||||
)
|
||||
await refreshChapters()
|
||||
}
|
||||
|
||||
const handleInsertLandmark = async (): Promise<void> => {
|
||||
if (!currentBookId || target?.kind !== 'chapter') {
|
||||
showToast(t('landmark.chapterOnly'))
|
||||
@@ -163,7 +212,14 @@ export function EditorLayout(): React.JSX.Element {
|
||||
<div key={vol.id}>
|
||||
<div
|
||||
className="vol-header"
|
||||
data-volume-id={vol.id}
|
||||
onClick={() => setActiveVolume(vol.id)}
|
||||
onDragOver={(e) => e.preventDefault()}
|
||||
onDrop={(e) => {
|
||||
e.preventDefault()
|
||||
const draggedId = e.dataTransfer.getData('application/x-bilin-chapter')
|
||||
if (draggedId) void handleChapterDropOnVolume(draggedId, vol.id)
|
||||
}}
|
||||
style={{ color: activeVolumeId === vol.id ? 'var(--accent-light)' : undefined }}
|
||||
>
|
||||
{vol.name}
|
||||
@@ -173,7 +229,24 @@ export function EditorLayout(): React.JSX.Element {
|
||||
.map((ch, idx) => (
|
||||
<div
|
||||
key={ch.id}
|
||||
className={`chapter-item ${target?.kind === 'chapter' && target.id === ch.id ? 'active' : ''}`}
|
||||
className={`chapter-item ${target?.kind === 'chapter' && target.id === ch.id ? 'active' : ''} ${dragOverChapterId === ch.id ? 'drag-over' : ''}`}
|
||||
draggable
|
||||
data-testid={`chapter-item-${ch.id}`}
|
||||
onDragStart={(e) => {
|
||||
e.dataTransfer.setData('application/x-bilin-chapter', ch.id)
|
||||
e.dataTransfer.effectAllowed = 'move'
|
||||
}}
|
||||
onDragOver={(e) => {
|
||||
e.preventDefault()
|
||||
setDragOverChapterId(ch.id)
|
||||
}}
|
||||
onDragLeave={() => setDragOverChapterId(null)}
|
||||
onDrop={(e) => {
|
||||
e.preventDefault()
|
||||
setDragOverChapterId(null)
|
||||
const draggedId = e.dataTransfer.getData('application/x-bilin-chapter')
|
||||
if (draggedId) void handleChapterDropOnItem(draggedId, vol.id, ch.id)
|
||||
}}
|
||||
onClick={() => void handleSelectChapter(ch.id)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && void handleSelectChapter(ch.id)}
|
||||
role="button"
|
||||
|
||||
Reference in New Issue
Block a user