Add PWA support

This commit is contained in:
kwaroran
2024-09-10 22:09:58 +09:00
parent 17af14b1ee
commit 3ae2c11ca4
8 changed files with 300 additions and 84 deletions

View File

@@ -5,6 +5,7 @@
<link rel="icon" type="image/png" sizes="16x16" href="/logo_16.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/logo_32.png" />
<link rel="icon" type="image/png" sizes="256x256" href="/logo_256.png" />
<link rel="manifest" href="manifest.json" />
<meta name="description" content="An AI frontend for both light and core users.">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link href="https://fonts.googleapis.com/css2?family=Tilt+Prism&family=Yellowtail&display=swap" rel="stylesheet">

63
manifest.json Normal file
View File

@@ -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"]
}
}
]
}

BIN
public/logo_192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

BIN
public/logo_512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -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(

View File

@@ -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{

View File

@@ -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,20 +81,17 @@ export async function exportModule(module:RisuModule){
writeByte(0) //end of file
if(saveData){
await downloadFile(module.name + '.risum', apb.buffer)
}
if(alertEnd){
alertNormal(language.successExport)
}
return apb.buffer
}
export async function importModule(){
const f = await selectSingleFile(['json', 'lorebook', 'risum'])
if(!f){
return
}
let fileData = f.data
const db = get(DataBase)
if(f.name.endsWith('.risum')){
try {
const buf = Buffer.from(fileData)
export async function readModule(buf:Buffer):Promise<RisuModule> {
let pos = 0
const readLength = () => {
@@ -162,6 +164,20 @@ export async function importModule(){
})
module.id = v4()
return module
}
export async function importModule(){
const f = await selectSingleFile(['json', 'lorebook', 'risum'])
if(!f){
return
}
let fileData = f.data
const db = get(DataBase)
if(f.name.endsWith('.risum')){
try {
const buf = Buffer.from(fileData)
const module = await readModule(buf)
db.modules.push(module)
setDatabase(db)
return

View File

@@ -81,6 +81,7 @@ export class CharXReader{
assetPromises:Promise<void>[] = []
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)