diff --git a/index.html b/index.html index 0459e154..972d7f37 100644 --- a/index.html +++ b/index.html @@ -5,6 +5,7 @@ + diff --git a/manifest.json b/manifest.json new file mode 100644 index 00000000..be6e8ca5 --- /dev/null +++ b/manifest.json @@ -0,0 +1,63 @@ +{ + "name": "RisuAI", + "icons": [ + { + "src": "logo_512.png", + "type": "image/png", + "sizes": "512x512" + }, + { + "src": "logo_192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo_16.png", + "type": "image/png", + "sizes": "16x16" + }, + { + "src": "logo_32.png", + "type": "image/png", + "sizes": "32x32" + }, + { + "src": "logo_256.png", + "type": "image/png", + "sizes": "256x256" + } + ], + "start_url": "/", + "display": "standalone", + "theme_color": "#4682B4", + "share_target": { + "action": "/receive-files/", + "method": "POST", + "enctype": "multipart/form-data", + "params": { + "files": [ + { + "name": "character", + "accept": [".charx"] + }, + { + "name": "preset", + "accept": [".risup"] + }, + { + "name": "module", + "accept": [".risum"] + } + ] + } + }, + "file_handlers": [ + { + "action": "/", + "accept": { + "application/octet-stream": [".charx", ".risup", ".risum"] + } + } + ] + +} \ No newline at end of file diff --git a/public/logo_192.png b/public/logo_192.png new file mode 100644 index 00000000..17db294e Binary files /dev/null and b/public/logo_192.png differ diff --git a/public/logo_512.png b/public/logo_512.png new file mode 100644 index 00000000..76306c27 Binary files /dev/null and b/public/logo_512.png differ diff --git a/public/sw.js b/public/sw.js index a94ac67d..ed1172cb 100644 --- a/public/sw.js +++ b/public/sw.js @@ -35,6 +35,36 @@ self.addEventListener('fetch', (event) => { } case "init":{ event.respondWith(new Response("v2")) + break + } + case 'share':{ + event.respondWith((async () => { + const formData = await event.request.formData(); + /** + * @type {File} + */ + const character = formData.get('character') + const preset = formData.get('preset') + const module = formData.get('module') + if(character){ + const buf = await character.arrayBuffer() + await registerCache(`/sw/share/character`, buf, true) + return Response.redirect("/#share_character", 303) + } + if(preset){ + const buf = await preset.arrayBuffer() + await registerCache(`/sw/share/preset`, buf, true) + return Response.redirect("/#share_preset", 303) + } + if(module){ + const buf = await module.arrayBuffer() + await registerCache(`/sw/share/module`, buf, true) + return Response.redirect("/#share_module", 303) + } + return Response.redirect("/", 303) + + })()) + break } default: { event.respondWith(new Response( diff --git a/src/ts/characterCards.ts b/src/ts/characterCards.ts index ffe37d8e..cffde8a4 100644 --- a/src/ts/characterCards.ts +++ b/src/ts/characterCards.ts @@ -14,6 +14,7 @@ import { PngChunk } from "./pngChunk" import type { OnnxModelFiles } from "./process/transformers" import { CharXReader, CharXWriter } from "./process/processzip" import { Capacitor } from "@capacitor/core" +import { exportModule, readModule, type RisuModule } from "./process/modules" export const hubURL = "https://sv.risuai.xyz" @@ -65,6 +66,7 @@ async function importCharacterProcess(f:{ } } + if(f.name.endsWith('charx')){ console.log('reading charx') alertStore.set({ @@ -80,11 +82,18 @@ async function importCharacterProcess(f:{ alertError(language.errors.noData) return } - const card = JSON.parse(cardData) + const card:CharacterCardV3 = JSON.parse(cardData) if(CCardLib.character.check(card) !== 'v3'){ alertError(language.errors.noData) return } + if(reader.moduleData){ + const md = await readModule(Buffer.from(reader.moduleData)) + card.data.extensions ??= {} + card.data.extensions.risuai ??= {} + card.data.extensions.risuai.triggerscript = md.trigger ?? [] + card.data.extensions.risuai.customScripts = md.regex ?? [] + } await importCharacterCardSpec(card, undefined, 'normal', reader.assets) let db = get(DataBase) return db.characters.length - 1 @@ -133,7 +142,7 @@ async function importCharacterProcess(f:{ continue } if(chunk.key.startsWith('chara-ext-asset_')){ - const assetIndex = (chunk.key.replace('chara-ext-asset_', '')) + const assetIndex = chunk.key.replace('chara-ext-asset_:', '').replace('chara-ext-asset_', '') alertWait('Loading... (Reading Asset ' + assetIndex + ')' ) const assetData = Buffer.from(chunk.value, 'base64') const assetId = await saveAsset(assetData) @@ -304,7 +313,7 @@ export async function characterURLImport() { db.modules.push(importData) setDatabase(db) alertNormal(language.successImport) - SettingsMenuIndex.set(1) + SettingsMenuIndex.set(14) settingsOpen.set(true) return } @@ -315,11 +324,90 @@ export async function characterURLImport() { name: 'imported.risupreset', data: importData }) - SettingsMenuIndex.set(14) + SettingsMenuIndex.set(1) settingsOpen.set(true) return - } + if(hash.startsWith('#share_character')){ + const data = await fetch("/sw/share/character") + if(data.status !== 200){ + return + } + const charx = new Uint8Array(await data.arrayBuffer()) + await importCharacterProcess({ + name: 'shared.charx', + data: charx + }) + } + if(hash.startsWith('#share_module')){ + const data = await fetch("/sw/share/module") + if(data.status !== 200){ + return + } + const module = new Uint8Array(await data.arrayBuffer()) + const md = await readModule(Buffer.from(module)) + md.id = v4() + const db = get(DataBase) + db.modules.push(md) + setDatabase(db) + alertNormal(language.successImport) + SettingsMenuIndex.set(14) + settingsOpen.set(true) + } + if(hash.startsWith('#share_preset')){ + const data = await fetch("/sw/share/preset") + if(data.status !== 200){ + return + } + const preset = new Uint8Array(await data.arrayBuffer()) + await importPreset({ + name: 'shared.risup', + data: preset + }) + SettingsMenuIndex.set(1) + settingsOpen.set(true) + } + if ("launchQueue" in window) { + const handleFiles = async (files:FileSystemFileHandle[]) => { + for(const f of files){ + const file = await f.getFile() + const data = new Uint8Array(await file.arrayBuffer()) + if(f.name.endsWith('.charx')){ + await importCharacterProcess({ + name: f.name, + data: data + }) + } + if(f.name.endsWith('.risupreset') || f.name.endsWith('.risup')){ + await importPreset({ + name: f.name, + data: data + }) + SettingsMenuIndex.set(1) + settingsOpen.set(true) + alertNormal(language.successImport) + } + if(f.name.endsWith('risum')){ + const md = await readModule(Buffer.from(data)) + md.id = v4() + const db = get(DataBase) + db.modules.push(md) + setDatabase(db) + alertNormal(language.successImport) + SettingsMenuIndex.set(14) + settingsOpen.set(true) + } + } + } + //@ts-ignore + window.launchQueue.setConsumer((launchParams) => { + if (launchParams.files && launchParams.files.length) { + const files = launchParams.files as FileSystemFileHandle[] + handleFiles(files) + } + }); + } + } @@ -850,7 +938,7 @@ export async function exportCharacterCard(char:character, type:'png'|'json'|'cha const b64encoded = Buffer.from(await convertImage(rData)).toString('base64') assetIndex++ card.data.extensions.risuai.emotions[i][1] = `__asset:${assetIndex}` - await writer.write("chara-ext-asset_" + assetIndex, b64encoded) + await writer.write("chara-ext-asset_:" + assetIndex, b64encoded) } } @@ -866,7 +954,7 @@ export async function exportCharacterCard(char:character, type:'png'|'json'|'cha const b64encoded = Buffer.from(await convertImage(rData)).toString('base64') assetIndex++ card.data.extensions.risuai.additionalAssets[i][1] = `__asset:${assetIndex}` - await writer.write("chara-ext-asset_" + assetIndex, b64encoded) + await writer.write("chara-ext-asset_:" + assetIndex, b64encoded) } } @@ -882,7 +970,7 @@ export async function exportCharacterCard(char:character, type:'png'|'json'|'cha const b64encoded = Buffer.from(rData).toString('base64') assetIndex++ card.data.extensions.risuai.vits[key] = `__asset:${assetIndex}` - await writer.write("chara-ext-asset_" + assetIndex, b64encoded) + await writer.write("chara-ext-asset_:" + assetIndex, b64encoded) } } if(type === 'json'){ @@ -923,7 +1011,7 @@ export async function exportCharacterCard(char:character, type:'png'|'json'|'cha if(type === 'png'){ const b64encoded = Buffer.from(await convertImage(rData)).toString('base64') card.data.assets[i].uri = `__asset:${assetIndex}` - await writer.write("chara-ext-asset_" + assetIndex, b64encoded) + await writer.write("chara-ext-asset_:" + assetIndex, b64encoded) } else if(type === 'json'){ const b64encoded = Buffer.from(await convertImage(rData)).toString('base64') @@ -1015,6 +1103,20 @@ export async function exportCharacterCard(char:character, type:'png'|'json'|'cha }) if(type === 'charx'){ + const md:RisuModule = { + name: `${char.name} Module`, + description: "Module for " + char.name, + id: v4(), + trigger: card.data.extensions.risuai.triggerscript ?? [], + regex: card.data.extensions.risuai.customScripts ?? [], + lorebook: char.globalLore ?? [], + } + delete card.data.extensions.risuai.triggerscript + delete card.data.extensions.risuai.customScripts + await writer.write("module.risum", await exportModule(md, { + alertEnd: false, + saveData: false + })) await writer.write("card.json", Buffer.from(JSON.stringify(card, null, 4))) } else{ diff --git a/src/ts/process/modules.ts b/src/ts/process/modules.ts index 0d4800a2..a7b9c198 100644 --- a/src/ts/process/modules.ts +++ b/src/ts/process/modules.ts @@ -27,7 +27,12 @@ export interface RisuModule{ namespace?:string } -export async function exportModule(module:RisuModule){ +export async function exportModule(module:RisuModule, arg:{ + alertEnd?:boolean + saveData?:boolean +} = {}){ + const alertEnd = arg.alertEnd ?? true + const saveData = arg.saveData ?? true const apb = new AppendableBuffer() const writeLength = (len:number) => { const lenbuf = Buffer.alloc(4) @@ -76,8 +81,90 @@ export async function exportModule(module:RisuModule){ writeByte(0) //end of file - await downloadFile(module.name + '.risum', apb.buffer) - alertNormal(language.successExport) + if(saveData){ + await downloadFile(module.name + '.risum', apb.buffer) + } + if(alertEnd){ + alertNormal(language.successExport) + } + + return apb.buffer +} + +export async function readModule(buf:Buffer):Promise { + let pos = 0 + + const readLength = () => { + const len = buf.readUInt32LE(pos) + pos += 4 + return len + } + const readByte = () => { + const byte = buf.readUInt8(pos) + pos += 1 + return byte + } + const readData = (len:number) => { + const data = buf.subarray(pos, pos + len) + pos += len + return data + } + + if(readByte() !== 111){ + console.error("Invalid magic number") + alertError(language.errors.noData) + return + } + if(readByte() !== 0){ //Version check + console.error("Invalid version") + alertError(language.errors.noData) + return + } + + const mainLen = readLength() + const mainData = readData(mainLen) + const main:{ + type:'risuModule' + module:RisuModule + } = JSON.parse(Buffer.from(await decodeRPack(mainData)).toString()) + + if(main.type !== 'risuModule'){ + console.error("Invalid module type") + alertError(language.errors.noData) + return + } + + let module = main.module + + let i = 0 + while(true){ + const mark = readByte() + if(mark === 0){ + break + } + if(mark !== 1){ + alertError(language.errors.noData) + return + } + const len = readLength() + const data = readData(len) + module.assets[i][1] = await saveAsset(Buffer.from(await decodeRPack(data))) + alertStore.set({ + type: 'wait', + msg: `Loading... (Adding Assets ${i} / ${module.assets.length})` + }) + if(!isTauri && !Capacitor.isNativePlatform() &&!isNodeServer){ + await sleep(100) + } + i++ + } + alertStore.set({ + type: 'none', + msg: '' + }) + + module.id = v4() + return module } export async function importModule(){ @@ -90,78 +177,7 @@ export async function importModule(){ if(f.name.endsWith('.risum')){ try { const buf = Buffer.from(fileData) - let pos = 0 - - const readLength = () => { - const len = buf.readUInt32LE(pos) - pos += 4 - return len - } - const readByte = () => { - const byte = buf.readUInt8(pos) - pos += 1 - return byte - } - const readData = (len:number) => { - const data = buf.subarray(pos, pos + len) - pos += len - return data - } - - if(readByte() !== 111){ - console.error("Invalid magic number") - alertError(language.errors.noData) - return - } - if(readByte() !== 0){ //Version check - console.error("Invalid version") - alertError(language.errors.noData) - return - } - - const mainLen = readLength() - const mainData = readData(mainLen) - const main:{ - type:'risuModule' - module:RisuModule - } = JSON.parse(Buffer.from(await decodeRPack(mainData)).toString()) - - if(main.type !== 'risuModule'){ - console.error("Invalid module type") - alertError(language.errors.noData) - return - } - - let module = main.module - - let i = 0 - while(true){ - const mark = readByte() - if(mark === 0){ - break - } - if(mark !== 1){ - alertError(language.errors.noData) - return - } - const len = readLength() - const data = readData(len) - module.assets[i][1] = await saveAsset(Buffer.from(await decodeRPack(data))) - alertStore.set({ - type: 'wait', - msg: `Loading... (Adding Assets ${i} / ${module.assets.length})` - }) - if(!isTauri && !Capacitor.isNativePlatform() &&!isNodeServer){ - await sleep(100) - } - i++ - } - alertStore.set({ - type: 'none', - msg: '' - }) - - module.id = v4() + const module = await readModule(buf) db.modules.push(module) setDatabase(db) return diff --git a/src/ts/process/processzip.ts b/src/ts/process/processzip.ts index a74d0c5f..a3f85f6d 100644 --- a/src/ts/process/processzip.ts +++ b/src/ts/process/processzip.ts @@ -81,6 +81,7 @@ export class CharXReader{ assetPromises:Promise[] = [] excludedFiles:string[] = [] cardData:string|undefined + moduleData:Uint8Array|undefined constructor(){ this.unzip = new fflate.Unzip() this.unzip.register(fflate.UnzipInflate) @@ -98,6 +99,9 @@ export class CharXReader{ else if(file.name === 'card.json'){ this.cardData = new TextDecoder().decode(assetData) } + else if(file.name === 'module.risum'){ + this.moduleData = assetData + } else{ this.assetPromises.push((async () => { const assetId = await saveAsset(assetData)