feat: chapter function

This commit is contained in:
2025-07-15 18:17:48 +08:00
parent e46a110720
commit dbfe057292
6 changed files with 6829 additions and 99 deletions

View File

@@ -4,36 +4,89 @@ import { Code } from "@nextui-org/code";
import { Icon } from "@iconify/react";
import {
Button,
Card,
CardBody,
CardHeader,
Chip,
Divider,
ScrollShadow,
Select,
SelectItem,
} from "@nextui-org/react";
import { cn } from "@nextui-org/theme";
import React from "react";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { title } from "@/components/primitives";
import { ChapterItem, Chapters, readFile, splitChapter } from "./utils/chapter";
import { ChapterCard } from "@/components/ChapterCard";
export default function Home() {
const [text,setText] = useState("")
const [chapters,setChapters] = useState<Chapters>([])
const [activeChapter,setActiveChapter] = useState<number|null>(null)
const textareaRef = useRef<HTMLTextAreaElement|null>(null)
const onSplit = useCallback(async ()=>{
const dom = textareaRef.current
if(!dom) return
const index = dom.selectionStart
if(index<0) return
console.log("bookmark",index)
const content = '====SPLIT CHAPTER===='
setText(txt=>{
return txt.substring(0,index)+'\n'+content+"\n"+txt.substring(15)
})
},[])
const onMergeNext = useCallback((chapter:ChapterItem)=>{
const current = chapters.findIndex(detail=>detail.key===chapter.key)
if(current==-1) return
const next = current+1
const nextChapter = chapters[next]
if(!nextChapter) return
chapter.text += ["",nextChapter.title,nextChapter.text].join("\n")
const newChapters = chapters.slice()
newChapters.splice(next,1)
const newText = newChapters.reduce<string>((txt,chapter)=>{
txt += chapter.rawTitle+'\n'
txt += chapter.text+"\n"
return txt
},"")
setText(newText)
},[chapters])
const selectFile = useCallback(()=>{
const input = document.createElement("input")
input.type = 'file'
input.accept = '.txt'
input.addEventListener("change",async function(event){
setActiveChapter(null)
const [file] = input.files||[]
if(!file) {
setText("")
return
}
console.time("read-txt")
setText(await readFile(file))
console.timeEnd("read-txt")
})
input.click()
},[])
useEffect(()=>{
console.time("split-chapter")
const chapters = splitChapter(text)
setChapters(chapters)
if(chapters.length>0) {
setActiveChapter(1)
}
console.timeEnd("split-chapter")
},[text])
return (
<section className="flex flex-col items-center justify-center gap-4 py-8 md:py-10">
<div className="inline-block max-w-xl text-center justify-center">
<span className={title()}>Place your changes here</span>
</div>
<div className="mt-8 gap-16">
<Snippet hideCopyButton hideSymbol className="gap-4" variant="bordered">
<span>
Get started by editing <Code color="primary">app/page.tsx</Code>
</span>
<span>Please feel free to use the example components below.</span>
</Snippet>
</div>
<div className="pt-6 w-48">
{/* <div className="flex flex-row pt-6 w-48"> */}
<div className="flex flex-row pt-6 w-auto">
<Select
className="flex-auto"
items={[
{ key: "story-1", label: "story-1" },
{ key: "story-2", label: "story-2" },
@@ -44,6 +97,8 @@ export default function Home() {
>
{(story) => <SelectItem key={story.key}>{story.label}</SelectItem>}
</Select>
<Button className="flex-auto w-auto" onPress={selectFile}>Select</Button>
</div>
<div className="pt-6">
@@ -67,84 +122,28 @@ export default function Home() {
id="menu-scroll"
>
<div className="flex flex-col gap-4 py-3 pr-4">
<Card
key="card-1"
isPressable
className={`max-w-[384px] border-1 border-divider/15 bg-themeBlue/20`}
shadow="none"
>
<CardHeader className="flex items-center justify-between">
<div className="flex gap-1.5">
<Chip
className="mr-1 text-themeBlue bg-themeBlue/20"
radius="sm"
size="sm"
variant="flat"
>
Editing
</Chip>
<p className="text-left mr-1">
Chapter 1 - Chapter 1 title
</p>
</div>
</CardHeader>
<Divider />
<CardBody>
<p className="line-clamp-2">
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Sed do eiusmod tempor incididunt ut labore et dolore magna
aliqua. Ut enim ad minim veniam, quis nostrud exercitation
ullamco laboris nisi ut aliquip
</p>
</CardBody>
</Card>
<Card
key="card-2"
isPressable
className={`max-w-[384px] border-1 border-divider/15`}
shadow="none"
>
<CardHeader className="flex items-center justify-between">
<div className="flex gap-1.5">
<p className="text-left mr-1">
Chapter 2 - Chapter 2 title
</p>
</div>
</CardHeader>
<Divider />
<CardBody>
<p className="line-clamp-2">
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Sed do eiusmod tempor incididunt ut labore et dolore magna
aliqua. Ut enim ad minim veniam, quis nostrud exercitation
ullamco laboris nisi ut aliquip
</p>
</CardBody>
</Card>
<Card
key="card-3"
isPressable
className={`max-w-[384px] border-1 border-divider/15`}
shadow="none"
>
<CardHeader className="flex items-center justify-between">
<div className="flex gap-1.5">
<p className="text-left mr-1">
Chapter 3 - Chapter 3 title
</p>
</div>
</CardHeader>
<Divider />
<CardBody>
<p className="line-clamp-2">
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Sed do eiusmod tempor incididunt ut labore et dolore magna
aliqua. Ut enim ad minim veniam, quis nostrud exercitation
ullamco laboris nisi ut aliquip
</p>
</CardBody>
</Card>
{
chapters.map((chapter,index)=>{
return <ChapterCard
chapter={chapter}
active={activeChapter===chapter.number}
key={`chapter-${chapter.key}`}
onPress={async ()=>{
setActiveChapter(chapter.number)
if(!textareaRef.current) return
textareaRef.current.focus()
await new Promise<void>(resolve=>{
requestAnimationFrame(()=>{
resolve()
})
})
textareaRef.current.selectionStart = chapter.indexOf
textareaRef.current.selectionEnd = chapter.indexOf
}}
onMergeNext={index===chapters.length-1?undefined:onMergeNext}
/>
})
}
</div>
</ScrollShadow>
</div>
@@ -197,8 +196,9 @@ export default function Home() {
/>
}
variant="flat"
onPress={onSplit}
>
button-2
Split
</Button>
</div>
<div>
@@ -207,6 +207,11 @@ export default function Home() {
{/* Adjusted to use flex display for layout */}
<textarea
className="flex-1 p-3 resize-none rounded-md border border-transparent bg-slate-50 dark:bg-gray-200 text-gray-900" // Use flex-1 to allow the textarea to fill available space
value={text}
onChange={(event)=>{
setText(event.target.value)
}}
ref={textareaRef}
/>
<div className="bg-gray-100 p-1 rounded-md self-end ml-2">
{/* Added margin-left to separate from textarea, align-self to position at the bottom */}