feat: module lorebook sort, import, export (#822)

# PR Checklist
- [ ] Have you checked if it works normally in all models? *Ignore this
if it doesn't use models.*
- [ ] Have you checked if it works normally in all web, local, and node
hosted versions? If it doesn't, have you blocked it in those versions?
- [x] Have you added type definitions?

# Description
This PR adds sorting, import, and export functionality to the Module
Lorebook list.

I focused on preserving the existing code structure as much as possible
while implementing these changes.

Thank you for your time and review!
This commit is contained in:
kwaroran
2025-04-28 15:42:40 +09:00
committed by GitHub
3 changed files with 80 additions and 13 deletions

View File

@@ -2,6 +2,9 @@
import { language } from "src/lang";
import TextInput from "src/lib/UI/GUI/TextInput.svelte";
import LoreBookData from "src/lib/SideBars/LoreBook/LoreBookData.svelte";
import type { loreBook } from "src/ts/storage/database.svelte";
import LoreBookList from "src/lib/SideBars/LoreBook/LoreBookList.svelte";
import { type CCLorebook, convertExternalLorebook } from "src/ts/process/lorebook.svelte";
import type { RisuModule } from "src/ts/process/modules";
import { DownloadIcon, FolderUpIcon, PlusIcon, TrashIcon } from "lucide-svelte";
import RegexList from "src/lib/SideBars/Scripts/RegexList.svelte";
@@ -9,7 +12,8 @@
import Check from "src/lib/UI/GUI/CheckInput.svelte";
import Help from "src/lib/Others/Help.svelte";
import TextAreaInput from "src/lib/UI/GUI/TextAreaInput.svelte";
import { getFileSrc, openURL, saveAsset } from "src/ts/globalApi.svelte";
import { getFileSrc, openURL, saveAsset, downloadFile } from "src/ts/globalApi.svelte";
import { alertNormal, alertError } from "src/ts/alert";
import { exportRegex, importRegex } from "src/ts/process/scripts";
import { selectMultipleFile } from "src/ts/util";
@@ -57,6 +61,48 @@
}
}
async function exportLoreBook(){
try {
const lore = currentModule.lorebook
const stringl = Buffer.from(JSON.stringify({
type: 'risu',
ver: 1,
data: lore
}), 'utf-8')
await downloadFile(`lorebook_export.json`, stringl)
alertNormal(language.successExport)
} catch (error) {
alertError(`${error}`)
}
}
async function importLoreBook(){
let lore = currentModule.lorebook
const lorebook = (await selectMultipleFile(['json', 'lorebook']))
if(!lorebook){
return
}
try {
for(const f of lorebook){
const importedlore = JSON.parse(Buffer.from(f.data).toString('utf-8'))
if(importedlore.type === 'risu' && importedlore.data){
const datas:loreBook[] = importedlore.data
for(const data of datas){
lore.push(data)
}
}
else if(importedlore.entries){
const entries:{[key:string]:CCLorebook} = importedlore.entries
lore.push(...convertExternalLorebook(entries))
}
}
} catch (error) {
alertError(`${error}`)
}
}
function addRegex(){
if(Array.isArray(currentModule.regex)){
currentModule.regex.push({
@@ -150,17 +196,18 @@
<TextAreaInput bind:value={currentModule.customModuleToggle}/>
{/if}
{#if submenu === 1 && (Array.isArray(currentModule.lorebook))}
<div class="border border-selected p-2 flex flex-col rounded-md mt-2">
{#each currentModule.lorebook as lore, i}
<LoreBookData bind:value={currentModule.lorebook[i]} idx={i} onRemove={() => {
currentModule.lorebook.splice(i, 1)
currentModule.lorebook = currentModule.lorebook
}}/>
{/each}
<LoreBookList externalLoreBooks={currentModule.lorebook} />
<div class="text-textcolor2 mt-2 flex">
<button onclick={() => {addLorebook()}} class="hover:text-textcolor cursor-pointer ml-1">
<PlusIcon />
</button>
<button onclick={() => {exportLoreBook()}} class="hover:text-textcolor cursor-pointer ml-2">
<DownloadIcon />
</button>
<button onclick={() => {importLoreBook()}} class="hover:text-textcolor cursor-pointer ml-2">
<FolderUpIcon />
</button>
</div>
<button onclick={() => {addLorebook()}} class="hover:text-textcolor cursor-pointer">
<PlusIcon />
</button>
{/if}
{#if submenu === 2 && (Array.isArray(currentModule.regex))}

View File

@@ -11,9 +11,10 @@
globalMode?: boolean;
submenu?: number;
lorePlus?: boolean;
externalLoreBooks?: loreBook[];
}
let { globalMode = false, submenu = 0, lorePlus = false }: Props = $props();
let { globalMode = false, submenu = 0, lorePlus = false, externalLoreBooks = null }: Props = $props();
let stb: Sortable = null
let ele: HTMLDivElement = $state()
let sorted = $state(0)
@@ -31,6 +32,13 @@
})
DBState.db.loreBook[DBState.db.loreBookPage].data = newLore
}
else if(externalLoreBooks){
let newLore:loreBook[] = []
idx.forEach((i) => {
newLore.push(externalLoreBooks[i])
})
externalLoreBooks = newLore
}
else if(submenu === 1){
let newLore:loreBook[] = []
idx.forEach((i) => {
@@ -97,6 +105,18 @@
}} onOpen={onOpen} onClose={onClose}/>
{/each}
{/if}
{:else if externalLoreBooks}
{#if externalLoreBooks.length === 0}
<span class="text-textcolor2">No Lorebook</span>
{:else}
{#each externalLoreBooks as book, i}
<LoreBookData bind:value={externalLoreBooks[i]} idx={i} onRemove={() => {
let lore = externalLoreBooks
lore.splice(i, 1)
externalLoreBooks = lore
}} onOpen={onOpen} onClose={onClose}/>
{/each}
{/if}
{:else if submenu === 0}
{#if DBState.db.characters[$selectedCharID].globalLore.length === 0}
<span class="text-textcolor2">No Lorebook</span>

View File

@@ -516,7 +516,7 @@ export async function importLoreBook(mode:'global'|'local'|'sglobal'){
}
}
interface CCLorebook{
export interface CCLorebook{
key:string[]
comment:string
content:string