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)