422 lines
12 KiB
TypeScript
422 lines
12 KiB
TypeScript
import { get, writable, type Writable } from "svelte/store"
|
|
import type { Database, Message } from "./storage/database"
|
|
import { DataBase } from "./storage/database"
|
|
import { selectedCharID } from "./stores"
|
|
import {open} from '@tauri-apps/api/dialog'
|
|
import { readBinaryFile } from "@tauri-apps/api/fs"
|
|
import { basename } from "@tauri-apps/api/path"
|
|
import { createBlankChar, getCharImage } from "./characters"
|
|
import { appWindow } from '@tauri-apps/api/window';
|
|
import { isTauri } from "./storage/globalApi"
|
|
|
|
export interface Messagec extends Message{
|
|
index: number
|
|
}
|
|
|
|
export function messageForm(arg:Message[], loadPages:number){
|
|
let db = get(DataBase)
|
|
let selectedChar = get(selectedCharID)
|
|
function reformatContent(data:string){
|
|
return data.trim()
|
|
}
|
|
|
|
let a:Messagec[] = []
|
|
for(let i=0;i<arg.length;i++){
|
|
const m = arg[i]
|
|
a.unshift({
|
|
role: m.role,
|
|
data: reformatContent(m.data),
|
|
index: i,
|
|
saying: m.saying,
|
|
chatId: m.chatId ?? 'none'
|
|
})
|
|
}
|
|
return a.slice(0, loadPages)
|
|
}
|
|
|
|
export function sleep(ms: number) {
|
|
return new Promise( resolve => setTimeout(resolve, ms) );
|
|
}
|
|
|
|
export function checkNullish(data:any){
|
|
return data === undefined || data === null
|
|
}
|
|
|
|
const domSelect = true
|
|
export async function selectSingleFile(ext:string[]){
|
|
if(domSelect){
|
|
const v = await selectFileByDom(ext, 'single')
|
|
const file = v[0]
|
|
return {name: file.name,data:await readFileAsUint8Array(file)}
|
|
}
|
|
|
|
const selected = await open({
|
|
filters: [{
|
|
name: ext.join(', '),
|
|
extensions: ext
|
|
}]
|
|
});
|
|
if (Array.isArray(selected)) {
|
|
return null
|
|
} else if (selected === null) {
|
|
return null
|
|
} else {
|
|
return {name: await basename(selected),data:await readBinaryFile(selected)}
|
|
}
|
|
}
|
|
|
|
export async function selectMultipleFile(ext:string[]){
|
|
if(!isTauri){
|
|
const v = await selectFileByDom(ext, 'multiple')
|
|
let arr:{name:string, data:Uint8Array}[] = []
|
|
for(const file of v){
|
|
arr.push({name: file.name,data:await readFileAsUint8Array(file)})
|
|
}
|
|
return arr
|
|
}
|
|
|
|
const selected = await open({
|
|
filters: [{
|
|
name: ext.join(', '),
|
|
extensions: ext,
|
|
}],
|
|
multiple: true
|
|
});
|
|
if (Array.isArray(selected)) {
|
|
let arr:{name:string, data:Uint8Array}[] = []
|
|
for(const file of selected){
|
|
arr.push({name: await basename(file),data:await readBinaryFile(file)})
|
|
}
|
|
return arr
|
|
} else if (selected === null) {
|
|
return null
|
|
} else {
|
|
return [{name: await basename(selected),data:await readBinaryFile(selected)}]
|
|
}
|
|
}
|
|
|
|
export const replacePlaceholders = (msg:string, name:string) => {
|
|
let db = get(DataBase)
|
|
let selectedChar = get(selectedCharID)
|
|
let currentChar = db.characters[selectedChar]
|
|
return msg .replace(/({{char}})|({{Char}})|(<Char>)|(<char>)/gi, currentChar.name)
|
|
.replace(/({{user}})|({{User}})|(<User>)|(<user>)/gi, db.username)
|
|
.replace(/(\{\{((set)|(get))var::.+?\}\})/gu,'')
|
|
}
|
|
|
|
export function checkIsIos(){
|
|
return /(iPad|iPhone|iPod)/g.test(navigator.userAgent)
|
|
}
|
|
function selectFileByDom(allowedExtensions:string[], multiple:'multiple'|'single' = 'single') {
|
|
return new Promise<null|File[]>((resolve) => {
|
|
const fileInput = document.createElement('input');
|
|
fileInput.type = 'file';
|
|
fileInput.multiple = multiple === 'multiple';
|
|
|
|
if(!(get(DataBase).allowAllExtentionFiles || checkIsIos())){
|
|
if (allowedExtensions && allowedExtensions.length) {
|
|
fileInput.accept = allowedExtensions.map(ext => `.${ext}`).join(',');
|
|
}
|
|
}
|
|
|
|
|
|
fileInput.addEventListener('change', (event) => {
|
|
if (fileInput.files.length === 0) {
|
|
resolve([]);
|
|
return;
|
|
}
|
|
|
|
const files = Array.from(fileInput.files).filter(file => {
|
|
const fileExtension = file.name.split('.').pop().toLowerCase();
|
|
return !allowedExtensions || allowedExtensions.includes(fileExtension);
|
|
});
|
|
|
|
fileInput.remove()
|
|
resolve(files);
|
|
});
|
|
|
|
document.body.appendChild(fileInput);
|
|
fileInput.click();
|
|
fileInput.style.display = 'none'; // Hide the file input element
|
|
});
|
|
}
|
|
|
|
function readFileAsUint8Array(file) {
|
|
return new Promise<Uint8Array>((resolve, reject) => {
|
|
const reader = new FileReader();
|
|
|
|
reader.onload = (event) => {
|
|
const buffer = event.target.result;
|
|
const uint8Array = new Uint8Array(buffer as ArrayBuffer);
|
|
resolve(uint8Array);
|
|
};
|
|
|
|
reader.onerror = (error) => {
|
|
reject(error);
|
|
};
|
|
|
|
reader.readAsArrayBuffer(file);
|
|
});
|
|
}
|
|
|
|
export async function changeFullscreen(){
|
|
const db = get(DataBase)
|
|
const isFull = await appWindow.isFullscreen()
|
|
if(db.fullScreen && (!isFull)){
|
|
await appWindow.setFullscreen(true)
|
|
}
|
|
if((!db.fullScreen) && (isFull)){
|
|
await appWindow.setFullscreen(false)
|
|
}
|
|
}
|
|
|
|
export async function getCustomBackground(db:string){
|
|
if(db.length < 2){
|
|
return ''
|
|
}
|
|
else{
|
|
const filesrc = await getCharImage(db, 'plain')
|
|
return `background: url("${filesrc}"); background-size: cover;`
|
|
}
|
|
}
|
|
|
|
export function findCharacterbyId(id:string) {
|
|
const db = get(DataBase)
|
|
for(const char of db.characters){
|
|
if(char.type !== 'group'){
|
|
if(char.chaId === id){
|
|
return char
|
|
}
|
|
}
|
|
}
|
|
let unknown =createBlankChar()
|
|
unknown.name = 'Unknown Character'
|
|
return unknown
|
|
}
|
|
|
|
export function findCharacterIndexbyId(id:string) {
|
|
const db = get(DataBase)
|
|
let i=0;
|
|
for(const char of db.characters){
|
|
if(char.chaId === id){
|
|
return i
|
|
}
|
|
i += 1
|
|
}
|
|
return -1
|
|
}
|
|
|
|
export function getCharacterIndexObject() {
|
|
const db = get(DataBase)
|
|
let i=0;
|
|
let result:{[key:string]:number} = {}
|
|
for(const char of db.characters){
|
|
result[char.chaId] = i
|
|
i += 1
|
|
}
|
|
return result
|
|
}
|
|
|
|
export function defaultEmotion(em:[string,string][]){
|
|
if(!em){
|
|
return ''
|
|
}
|
|
for(const v of em){
|
|
if(v[0] === 'neutral'){
|
|
return v[1]
|
|
}
|
|
}
|
|
return ''
|
|
}
|
|
|
|
export async function getEmotion(db:Database,chaEmotion:{[key:string]: [string, string, number][]}, type:'contain'|'plain'|'css'){
|
|
const selectedChar = get(selectedCharID)
|
|
const currentDat = db.characters[selectedChar]
|
|
if(!currentDat){
|
|
return []
|
|
}
|
|
let charIdList:string[] = []
|
|
|
|
if(currentDat.type === 'group'){
|
|
if(currentDat.characters.length === 0){
|
|
return []
|
|
}
|
|
switch(currentDat.viewScreen){
|
|
case "multiple":
|
|
charIdList = currentDat.characters
|
|
break
|
|
case "single":{
|
|
let newist:[string,string,number] = ['', '', 0]
|
|
let newistChar = currentDat.characters[0]
|
|
for(const currentChar of currentDat.characters){
|
|
const cha = chaEmotion[currentChar]
|
|
if(cha){
|
|
const latestEmotion = cha[cha.length - 1]
|
|
if(latestEmotion && latestEmotion[2] > newist[2]){
|
|
newist = latestEmotion
|
|
newistChar = currentChar
|
|
}
|
|
}
|
|
}
|
|
charIdList = [newistChar]
|
|
break
|
|
}
|
|
case "emp":{
|
|
charIdList = currentDat.characters
|
|
break
|
|
}
|
|
}
|
|
}
|
|
else{
|
|
charIdList = [currentDat.chaId]
|
|
}
|
|
|
|
let datas: string[] = [currentDat.viewScreen === 'emp' ? 'emp' : 'normal' as const]
|
|
for(const chaid of charIdList){
|
|
const currentChar = findCharacterbyId(chaid)
|
|
if(currentChar.viewScreen === 'emotion'){
|
|
const currEmotion = chaEmotion[currentChar.chaId]
|
|
let im = ''
|
|
if(!currEmotion || currEmotion.length === 0){
|
|
im = (await getCharImage(defaultEmotion(currentChar?.emotionImages),type))
|
|
}
|
|
else{
|
|
im = (await getCharImage(currEmotion[currEmotion.length - 1][1], type))
|
|
}
|
|
if(im && im.length > 2){
|
|
datas.push(im)
|
|
}
|
|
}
|
|
else if(currentChar.viewScreen === 'imggen'){
|
|
const currEmotion = chaEmotion[currentChar.chaId]
|
|
if(!currEmotion || currEmotion.length === 0){
|
|
datas.push(await getCharImage(currentChar.image ?? '', 'plain'))
|
|
}
|
|
else{
|
|
datas.push(currEmotion[currEmotion.length - 1][1])
|
|
}
|
|
}
|
|
}
|
|
return datas
|
|
}
|
|
|
|
export function getAuthorNoteDefaultText(){
|
|
const db = get(DataBase)
|
|
const template = db.promptTemplate
|
|
if(!template){
|
|
return ''
|
|
}
|
|
|
|
for(const v of template){
|
|
if(v.type === 'authornote'){
|
|
return v.defaultText ?? ''
|
|
}
|
|
}
|
|
return ''
|
|
|
|
}
|
|
|
|
export async function encryptBuffer(data:Uint8Array, keys:string){
|
|
// hash the key to get a fixed length key value
|
|
const keyArray = await window.crypto.subtle.digest("SHA-256", new TextEncoder().encode(keys))
|
|
|
|
const key = await window.crypto.subtle.importKey(
|
|
"raw",
|
|
keyArray,
|
|
"AES-GCM",
|
|
false,
|
|
["encrypt", "decrypt"]
|
|
)
|
|
|
|
// use web crypto api to encrypt the data
|
|
const result = await window.crypto.subtle.encrypt(
|
|
{
|
|
name: "AES-GCM",
|
|
iv: new Uint8Array(12),
|
|
},
|
|
key,
|
|
data
|
|
)
|
|
|
|
return result
|
|
}
|
|
|
|
export async function decryptBuffer(data:Uint8Array, keys:string){
|
|
// hash the key to get a fixed length key value
|
|
const keyArray = await window.crypto.subtle.digest("SHA-256", new TextEncoder().encode(keys))
|
|
|
|
const key = await window.crypto.subtle.importKey(
|
|
"raw",
|
|
keyArray,
|
|
"AES-GCM",
|
|
false,
|
|
["encrypt", "decrypt"]
|
|
)
|
|
|
|
// use web crypto api to encrypt the data
|
|
const result = await window.crypto.subtle.decrypt(
|
|
{
|
|
name: "AES-GCM",
|
|
iv: new Uint8Array(12),
|
|
},
|
|
key,
|
|
data
|
|
)
|
|
|
|
return result
|
|
}
|
|
|
|
export function getCurrentCharacter(){
|
|
const db = get(DataBase)
|
|
const selectedChar = get(selectedCharID)
|
|
return db.characters[selectedChar]
|
|
}
|
|
|
|
export function toState<T>(t:T):Writable<T>{
|
|
return writable(t)
|
|
}
|
|
|
|
export function BufferToText(data:Uint8Array){
|
|
if(!TextDecoder){
|
|
return Buffer.from(data).toString('utf-8')
|
|
}
|
|
return new TextDecoder().decode(data)
|
|
}
|
|
|
|
export function encodeMultilangString(data:{[code:string]:string}){
|
|
let result = ''
|
|
if(data.xx){
|
|
result = data.xx
|
|
}
|
|
for(const key in data){
|
|
result = `${result}\n# \`${key}\`\n${data[key]}`
|
|
}
|
|
return result
|
|
}
|
|
|
|
export function parseMultilangString(data:string){
|
|
let result:{[code:string]:string} = {}
|
|
const regex = /# `(.+?)`\n([\s\S]+?)(?=\n# `|$)/g
|
|
let m:RegExpExecArray
|
|
while ((m = regex.exec(data)) !== null) {
|
|
if (m.index === regex.lastIndex) {
|
|
regex.lastIndex++;
|
|
}
|
|
result[m[1]] = m[2]
|
|
}
|
|
result.xx = data.replace(regex, '')
|
|
return result
|
|
}
|
|
|
|
export const toLangName = (code:string) => {
|
|
switch(code){
|
|
case 'xx':{ //Special case for unknown language
|
|
return 'Unknown Language'
|
|
}
|
|
default:{
|
|
return new Intl.DisplayNames([code, 'en'], {type: 'language'}).of(code)
|
|
}
|
|
}
|
|
}
|
|
|
|
export const languageCodes = ["af","ak","am","an","ar","as","ay","az","be","bg","bh","bm","bn","br","bs","ca","co","cs","cy","da","de","dv","ee","el","en","eo","es","et","eu","fa","fi","fo","fr","fy","ga","gd","gl","gn","gu","ha","he","hi","hr","ht","hu","hy","ia","id","ig","is","it","iu","ja","jv","ka","kk","km","kn","ko","ku","ky","la","lb","lg","ln","lo","lt","lv","mg","mi","mk","ml","mn","mr","ms","mt","my","nb","ne","nl","nn","no","ny","oc","om","or","pa","pl","ps","pt","qu","rm","ro","ru","rw","sa","sd","si","sk","sl","sm","sn","so","sq","sr","st","su","sv","sw","ta","te","tg","th","ti","tk","tl","tn","to","tr","ts","tt","tw","ug","uk","ur","uz","vi","wa","wo","xh","yi","yo","zh","zu"] |