Add Custom Character Folder Image Feature Using Global Module Assets (#697)

# PR Checklist
- [ ] Have you checked if it works normally in all models? *Ignore this
if it doesn't use models.*
- [x] 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 a new feature that allows users to add custom images to
character folders. Instead of uploading images directly, this feature
utilizes the existing image pool from **the global module**. Users can
either select from existing images in the global module or upload new
images to the module.

The users can use a context menu on a folder to update its image.

## Why Use the Global Module?

The global module was chosen for several reasons:  
1. It allows the reuse of existing functions and code, minimizing the
need for new implementations.
2. Users can easily select existing images without re-uploading them.  
3. The global module provides reliable support for image upload and
deletion.
4. Images uploaded via the global module are accessible across all
platforms where the shared data is available.

## Note

To retrieve the list of assets from the global module, I used the
following code:

```typescript
let assetPaths: { [key: string]: { path: string } } = {};

const moduleAssets = getModuleAssets();
if (moduleAssets.length > 0) {
  for (const asset of moduleAssets) {
    const assetPath = await getFileSrc(asset[1]);
    assetPaths[asset[0].toLocaleLowerCase()] = {
      path: assetPath,
    };
  }
}
```

I reused the `parseAdditionalAssets` code from
`/src/ts/parser.svelte.ts` because it seemed simple and practical for
this feature. I thought about refactoring it into a shared function but
decided to keep it simple for now. I’m not sure if this is the best
approach, but it can always be changed later if needed.

---

If you are already working on a similar feature, find any issues with
this code, or feel this PR does not align with the project's direction,
I fully understand if this PR is not accepted.

Thank you!

---

One last note: With *transparent background images* and the existing
folder color functionality, users can differentiate categories even when
using the same image by applying different colors!
This commit is contained in:
kwaroran
2025-01-02 02:56:49 +09:00
committed by GitHub
6 changed files with 63 additions and 7 deletions

View File

@@ -490,6 +490,7 @@ export const languageEnglish = {
cancel: "Cancel",
renameFolder: "Rename Folder",
changeFolderColor: "Change Folder Color",
changeFolderImage: "Change Folder Image",
fullWordMatching: "Full Word Matching",
botSettingAtStart: "Bot Menu when Launch",
triggerStart: "On chat Send",

View File

@@ -441,6 +441,7 @@ export const languageKorean = {
"cancel": "취소",
"renameFolder": "폴더 이름 변경하기",
"changeFolderColor": "폴더 색상 변경하기",
"changeFolderImage": "폴더 이미지 변경하기",
"fullWordMatching": "단어 단위 매칭",
"botSettingAtStart": "실행 시 봇 설정으로 시작하기",
"triggerStart": "채팅 보낼 시",

View File

@@ -37,9 +37,9 @@
import SidebarAvatar from "./SidebarAvatar.svelte";
import BaseRoundedButton from "../UI/BaseRoundedButton.svelte";
import { get } from "svelte/store";
import { getCharacterIndexObject } from "src/ts/util";
import { getCharacterIndexObject, selectSingleFile } from "src/ts/util";
import { v4 } from "uuid";
import { checkCharOrder } from "src/ts/globalApi.svelte";
import { checkCharOrder, getFileSrc, saveAsset } from "src/ts/globalApi.svelte";
import { alertInput, alertSelect } from "src/ts/alert";
import SideChatList from "./SideChatList.svelte";
import { ConnectionIsHost, ConnectionOpenStore, RoomIdStore } from "src/ts/sync/multiuser";
@@ -59,7 +59,7 @@
}
type sortTypeNormal = { type:'normal',img: string, index: number, name:string }
type sortType = sortTypeNormal|{type:'folder',folder:sortTypeNormal[],id:string, name:string, color:string}
type sortType = sortTypeNormal|{type:'folder',folder:sortTypeNormal[],id:string, name:string, color:string, img?:string}
let charImages: sortType[] = $state([]);
let IconRounded = $state(false)
let openFolders:string[] = $state([])
@@ -109,7 +109,8 @@
type: "folder",
id: folder.id,
name: folder.name,
color: folder.color
color: folder.color,
img: folder.img,
});
}
}
@@ -472,10 +473,10 @@
{:else if char.type === "folder"}
{#key char.color}
{#key char.name}
<SidebarAvatar src="slot" size="56" rounded={IconRounded} bordered name={char.name} color={char.color}
<SidebarAvatar src="slot" size="56" rounded={IconRounded} bordered name={char.name} color={char.color} backgroundimg={char.img}
oncontextmenu={async (e) => {
e.preventDefault()
const sel = parseInt(await alertSelect([language.renameFolder,language.changeFolderColor,language.cancel]))
const sel = parseInt(await alertSelect([language.renameFolder,language.changeFolderColor,language.changeFolderImage,language.cancel]))
if(sel === 0){
const v = await alertInput(language.changeFolderName)
const db = DBState.db
@@ -501,6 +502,40 @@
db.characterOrder[ind] = oder
setDatabase(db)
}
else if(sel === 2) {
const sel = parseInt(await alertSelect(['Reset to Default Image', 'Select Image File']))
const db = DBState.db
const oder = db.characterOrder[ind]
if(typeof(oder) === 'string'){
return
}
switch (sel) {
case 0:
oder.imgFile = null
oder.img = ''
break;
case 1:
const folderImage = await selectSingleFile([
'png',
'jpg',
'webp',
])
if(!folderImage) {
return
}
const folderImageData = await saveAsset(folderImage.data)
oder.imgFile = folderImageData
oder.img = await getFileSrc(folderImageData)
db.characterOrder[ind] = oder
setDatabase(db)
break;
}
}
}}
onClick={() => {
if(char.type !== 'folder'){

View File

@@ -9,6 +9,7 @@
onClick?: any;
bordered?: boolean;
color?: string;
backgroundimg?: string;
children?: import('svelte').Snippet;
oncontextmenu?: (event: MouseEvent & {
currentTarget: EventTarget & HTMLDivElement;
@@ -23,6 +24,7 @@
onClick = () => {},
bordered = false,
color = '',
backgroundimg = '',
children,
oncontextmenu
}: Props = $props();
@@ -55,8 +57,15 @@
style:width={size + "px"}
style:height={size + "px"}
style:minWidth={size + "px"}
style:background-image={backgroundimg ? `url('${backgroundimg}')` : undefined}
style:background-size={backgroundimg ? "cover" : undefined}
style:background-position={backgroundimg ? "center" : undefined}
class:rounded-md={!rounded} class:rounded-full={rounded}
>{@render children?.()}</div>
>
{#if !backgroundimg}
{@render children?.()}
{/if}
</div>
{:else}
{#await src}
<div

View File

@@ -1081,6 +1081,14 @@ export function getUnpargeables(db: Database, uptype: 'basename' | 'pure' = 'bas
addUnparge(v.icon);
});
}
if(db.characterOrder){
db.characterOrder.forEach((item) => {
if (typeof item === 'object' && 'imgFile' in item) {
addUnparge(item.imgFile);
}
})
}
return unpargeable;
}

View File

@@ -1203,6 +1203,8 @@ export interface folder{
data:string[]
color:string
id:string
imgFile?:string
img?:string
}