feat: desktop tamagotchi (#7)
* rebase origin main * exclude packages/desktop/out * tmp commit * use mobile ui instead of desktop ui * fix lockfile * send ipc event to move window * load zip file * rename folder * remove `less` * add correct prefix for script * use airi logo instead of electron default logo * remove duplicate icon * use import.meta instead of __dirname
This commit is contained in:
@@ -15,7 +15,9 @@
|
||||
"typecheck": "pnpm -r --filter=./packages/* run build",
|
||||
"dev": "pnpm packages:dev",
|
||||
"build": "pnpm packages:build",
|
||||
"dev:tamagotchi": "pnpm packages:dev:tamagotchi",
|
||||
"packages:dev": "pnpm -r --filter=./packages/* --parallel run dev",
|
||||
"packages:dev:tamagotchi": "pnpm -r --filter=./packages/* --parallel run dev:tamagotchi",
|
||||
"packages:stub": "pnpm -r --filter=./packages/* run stub",
|
||||
"packages:build": "pnpm -r --filter=./packages/* run build",
|
||||
"packages:publish": "pnpm -r --filter=./packages/* run package:publish",
|
||||
|
||||
@@ -15,7 +15,9 @@
|
||||
"dev": "vite",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview",
|
||||
"typecheck": "vue-tsc --noEmit"
|
||||
"typecheck": "vue-tsc --noEmit",
|
||||
"dev:tamagotchi": "vite --mode tamagotchi",
|
||||
"build:tamagotchi": "vite build --mode tamagotchi"
|
||||
},
|
||||
"dependencies": {
|
||||
"@11labs/client": "^0.0.4",
|
||||
@@ -54,6 +56,7 @@
|
||||
"@xsai/shared-chat": "^0.0.22",
|
||||
"@xsai/stream-text": "^0.0.22",
|
||||
"defu": "^6.1.4",
|
||||
"jszip": "^3.10.1",
|
||||
"nprogress": "^0.2.0",
|
||||
"ofetch": "^1.4.1",
|
||||
"onnxruntime-web": "^1.20.1",
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useMicVAD } from '../../composables/micvad'
|
||||
// import { useAudioContext } from '../../stores/audio'
|
||||
import { useChatStore } from '../../stores/chat'
|
||||
import { useSettings } from '../../stores/settings'
|
||||
import BasicTextarea from '../BasicTextarea.vue'
|
||||
import MobileChatHistory from '../Widgets/MobileChatHistory.vue'
|
||||
import MobileSettings from '../Widgets/MobileSettings.vue'
|
||||
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
<script setup lang="ts">
|
||||
// import { useDevicesList } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
import { DrawerContent, DrawerPortal, DrawerRoot, DrawerTrigger } from 'vaul-vue'
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useMicVAD } from '../../composables/micvad'
|
||||
// import { useAudioContext } from '../../stores/audio'
|
||||
import { useChatStore } from '../../stores/chat'
|
||||
import { useSettings } from '../../stores/settings'
|
||||
import BasicTextarea from '../BasicTextarea.vue'
|
||||
import TamagotchiChatHistory from '../Widgets/TamagotchiChatHistory.vue'
|
||||
import TamagotchiSettings from '../Widgets/TamagotchiSettings.vue'
|
||||
|
||||
const messageInput = ref('')
|
||||
const listening = ref(false)
|
||||
|
||||
// const { audioInputs } = useDevicesList({ constraints: { audio: true }, requestPermissions: true })
|
||||
// const { selectedAudioDevice, isAudioInputOn, selectedAudioDeviceId } = storeToRefs(useSettings())
|
||||
const { isAudioInputOn, selectedAudioDeviceId } = storeToRefs(useSettings())
|
||||
const { send, onAfterSend } = useChatStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
async function handleSend() {
|
||||
if (!messageInput.value.trim()) {
|
||||
return
|
||||
}
|
||||
|
||||
await send(messageInput.value)
|
||||
}
|
||||
|
||||
const { destroy, start } = useMicVAD(selectedAudioDeviceId, {
|
||||
onSpeechStart: () => {
|
||||
// TODO: interrupt the playback
|
||||
// TODO: interrupt any of the ongoing TTS
|
||||
// TODO: interrupt any of the ongoing LLM requests
|
||||
// TODO: interrupt any of the ongoing animation of Live2D or VRM
|
||||
// TODO: once interrupted, we should somehow switch to listen or thinking
|
||||
// emotion / expression?
|
||||
listening.value = true
|
||||
},
|
||||
// VAD misfire means while speech end is detected but
|
||||
// the frames of the segment of the audio buffer
|
||||
// is not enough to be considered as a speech segment
|
||||
// which controlled by the `minSpeechFrames` parameter
|
||||
onVADMisfire: () => {
|
||||
// TODO: do audio buffer send to whisper
|
||||
listening.value = false
|
||||
},
|
||||
onSpeechEnd: (buffer) => {
|
||||
// TODO: do audio buffer send to whisper
|
||||
listening.value = false
|
||||
handleTranscription(buffer)
|
||||
},
|
||||
auto: false,
|
||||
})
|
||||
|
||||
function handleTranscription(_buffer: Float32Array<ArrayBufferLike>) {
|
||||
// eslint-disable-next-line no-alert
|
||||
alert('Transcription is not implemented yet')
|
||||
}
|
||||
|
||||
// async function handleAudioInputChange(event: Event) {
|
||||
// const target = event.target as HTMLSelectElement
|
||||
// const found = audioInputs.value.find(d => d.deviceId === target.value)
|
||||
// if (!found) {
|
||||
// selectedAudioDevice.value = undefined
|
||||
// return
|
||||
// }
|
||||
|
||||
// selectedAudioDevice.value = found
|
||||
// }
|
||||
|
||||
watch(isAudioInputOn, async (value) => {
|
||||
if (value === 'false') {
|
||||
destroy()
|
||||
}
|
||||
})
|
||||
|
||||
onAfterSend(async () => {
|
||||
messageInput.value = ''
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
start()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div relative w-full flex gap-1>
|
||||
<TamagotchiChatHistory absolute left-0 top-0 transform="translate-y-[-100%]" w-full />
|
||||
<div flex flex-1>
|
||||
<BasicTextarea
|
||||
v-model="messageInput"
|
||||
:placeholder="t('stage.message')"
|
||||
border="solid 2 pink-100"
|
||||
text="pink-400 hover:pink-600 placeholder:pink-400 placeholder:hover:pink-600"
|
||||
bg="pink-50 dark:[#3c2632]" max-h="[10lh]" min-h="[1lh]"
|
||||
w-full resize-none overflow-y-scroll rounded-l-xl p-2 font-medium outline-none
|
||||
transition="all duration-250 ease-in-out placeholder:all placeholder:duration-250 placeholder:ease-in-out"
|
||||
@submit="handleSend"
|
||||
/>
|
||||
</div>
|
||||
<DrawerRoot should-scale-background>
|
||||
<DrawerTrigger
|
||||
class="px-4 py-2.5"
|
||||
border="solid 2 pink-100 "
|
||||
text="lg pink-400 hover:pink-600 placeholder:pink-400 placeholder:hover:pink-600"
|
||||
bg="pink-50 dark:[#3c2632]" max-h="[10lh]" min-h="[1lh]" rounded-r-xl
|
||||
>
|
||||
<div i-solar:settings-bold-duotone />
|
||||
</DrawerTrigger>
|
||||
<DrawerPortal>
|
||||
<DrawerContent
|
||||
max-h="[90%]"
|
||||
fixed bottom-0 left-0 right-0 z-50 mt-24 h-full flex flex-col rounded-t-lg bg="[#fffbff] dark:[#1f1a1d]"
|
||||
>
|
||||
<div class="flex flex-1 flex-col rounded-t-lg p-5" bg="[#fffbff] dark:[#1f1a1d]" gap-2>
|
||||
<TamagotchiSettings />
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</DrawerPortal>
|
||||
</DrawerRoot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -9,15 +9,18 @@ import { useQueue } from '../../composables/queue'
|
||||
import { useDelayMessageQueue, useEmotionsMessageQueue, useMessageContentQueue } from '../../composables/queues'
|
||||
import { llmInferenceEndToken } from '../../constants'
|
||||
import { Voice } from '../../constants/elevenlabs'
|
||||
import { EMOTION_EmotionMotionName_value, EMOTION_VRMExpressionName_value, EmotionThinkMotionName } from '../../constants/emotions'
|
||||
|
||||
import { EMOTION_EmotionMotionName_value, EMOTION_VRMExpressionName_value, EmotionThinkMotionName } from '../../constants/emotions'
|
||||
import { useAudioContext, useSpeakingStore } from '../../stores/audio'
|
||||
import { useChatStore } from '../../stores/chat'
|
||||
import { useLLM } from '../../stores/llm'
|
||||
import { useSettings } from '../../stores/settings'
|
||||
import Live2DScene from '../Scenes/Live2D.vue'
|
||||
|
||||
import VRMScene from '../Scenes/VRM.vue'
|
||||
|
||||
import '../../utils/live2d-zip-loader'
|
||||
|
||||
const live2DViewerRef = ref<{ setMotion: (motionName: string) => Promise<void> }>()
|
||||
const vrmViewerRef = ref<{ setExpression: (expression: string) => void }>()
|
||||
|
||||
@@ -183,7 +186,7 @@ onUnmounted(() => {
|
||||
v-if="stageView === '2d'"
|
||||
ref="live2DViewerRef"
|
||||
:mouth-open-size="mouthOpenSize"
|
||||
model="/assets/live2d/models/hiyori_pro_zh/runtime/hiyori_pro_t11.model3.json"
|
||||
model="./assets/live2d/models/hiyori_pro_zh.zip"
|
||||
min-w="50% <lg:full" min-h="100 sm:100" h-full w-full flex-1
|
||||
/>
|
||||
<VRMScene
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
<script setup lang="ts">
|
||||
import { useElementBounding, useScroll } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
import { nextTick, ref } from 'vue'
|
||||
import { useMarkdown } from '../../composables/markdown'
|
||||
import { useChatStore } from '../../stores/chat'
|
||||
|
||||
const chatHistoryRef = ref<HTMLDivElement>()
|
||||
|
||||
const { messages } = storeToRefs(useChatStore())
|
||||
const bounding = useElementBounding(chatHistoryRef, { immediate: true, windowScroll: true, windowResize: true })
|
||||
const { y: chatHistoryContainerY } = useScroll(chatHistoryRef)
|
||||
|
||||
const { process } = useMarkdown()
|
||||
const { onBeforeMessageComposed, onTokenLiteral } = useChatStore()
|
||||
|
||||
onBeforeMessageComposed(async () => {
|
||||
// Scroll down to the new sent message
|
||||
nextTick().then(() => {
|
||||
bounding.update()
|
||||
chatHistoryContainerY.value = bounding.height.value
|
||||
})
|
||||
})
|
||||
|
||||
onTokenLiteral(async () => {
|
||||
// Scroll down to the new responding message
|
||||
nextTick().then(() => {
|
||||
bounding.update()
|
||||
chatHistoryContainerY.value = bounding.height.value
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div py="1" flex="~ col" rounded="lg" overflow-hidden>
|
||||
<div flex-1 /> <!-- spacer -->
|
||||
<div ref="chatHistoryRef" v-auto-animate h-full w-full max-h="30vh" flex="~ col" overflow-scroll>
|
||||
<div flex-1 /> <!-- spacer -->
|
||||
<div v-for="(message, index) in messages" :key="index" mb-2>
|
||||
<div v-if="message.role === 'assistant'" flex mr="12">
|
||||
<div
|
||||
flex="~ col"
|
||||
border="4 solid pink-200"
|
||||
shadow="md pink-200/50"
|
||||
min-w-20 rounded-lg px-2 py-1
|
||||
h="fit"
|
||||
bg="pink-100"
|
||||
>
|
||||
<div>
|
||||
<span text-xs text="pink-400/90" font-semibold class="inline hidden">Airi</span>
|
||||
</div>
|
||||
<div v-if="message.content" class="markdown-content" text="xs pink-400" v-html="process(message.content as string)" />
|
||||
<div v-else i-eos-icons:three-dots-loading />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="message.role === 'user'" flex="~">
|
||||
<div
|
||||
flex="~ col"
|
||||
border="4 solid cyan-200"
|
||||
shadow="md cyan-200/50"
|
||||
px="2"
|
||||
h="fit" min-w-20 rounded-lg px-2 py-1
|
||||
bg="cyan-100"
|
||||
>
|
||||
<div>
|
||||
<span text-xs text="cyan-600/90" font-semibold class="hidden">You</span>
|
||||
</div>
|
||||
<div v-if="message.content" class="markdown-content" text="xs cyan-600" v-html="process(message.content as string)" />
|
||||
<div v-else />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,202 @@
|
||||
<script setup lang="ts">
|
||||
import type { Voice } from '../../constants/elevenlabs'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { voiceList } from '../../constants/elevenlabs'
|
||||
import { useLLM } from '../../stores/llm'
|
||||
import { useSettings } from '../../stores/settings'
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
|
||||
const settings = useSettings()
|
||||
const supportedModels = ref<{ id: string, name?: string }[]>([])
|
||||
const { models } = useLLM()
|
||||
const { openAiModel, openAiApiBaseURL, openAiApiKey, elevenlabsVoiceEnglish, elevenlabsVoiceJapanese } = storeToRefs(settings)
|
||||
|
||||
function handleModelChange(event: Event) {
|
||||
const target = event.target as HTMLSelectElement
|
||||
const found = supportedModels.value.find(m => m.id === target.value)
|
||||
if (!found) {
|
||||
openAiModel.value = undefined
|
||||
return
|
||||
}
|
||||
|
||||
openAiModel.value = found
|
||||
}
|
||||
|
||||
function handleViewChange(event: Event) {
|
||||
const target = event.target as HTMLSelectElement
|
||||
settings.stageView = target.value
|
||||
}
|
||||
|
||||
function handleVoiceChange(event: Event) {
|
||||
const value = (event.target as HTMLSelectElement).value as Voice
|
||||
switch (locale.value) {
|
||||
case 'en':
|
||||
case 'en-US':
|
||||
elevenlabsVoiceEnglish.value = value
|
||||
break
|
||||
case 'zh':
|
||||
case 'zh-CN':
|
||||
case 'zh-TW':
|
||||
case 'zh-HK':
|
||||
elevenlabsVoiceEnglish.value = value
|
||||
break
|
||||
case 'jp':
|
||||
case 'jp-JP':
|
||||
elevenlabsVoiceJapanese.value = value
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
watch([openAiApiBaseURL, openAiApiKey], async ([baseUrl, apiKey]) => {
|
||||
if (!baseUrl || !apiKey) {
|
||||
supportedModels.value = []
|
||||
return
|
||||
}
|
||||
|
||||
supportedModels.value = await models(baseUrl, apiKey)
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
if (!openAiApiBaseURL.value || !openAiApiKey.value)
|
||||
return
|
||||
|
||||
supportedModels.value = await models(openAiApiBaseURL.value, openAiApiKey.value)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h2 text="slate-800/80" font-bold>
|
||||
Settings
|
||||
</h2>
|
||||
<div>
|
||||
<div
|
||||
grid="~ cols-[140px_1fr]" my-2 items-center gap-1.5 rounded-lg
|
||||
bg="[#fff6fc]" px-2 py-1 text="pink-400"
|
||||
>
|
||||
<div text="xs pink-500">
|
||||
<span>{{ t('settings.openai-base-url.label') }}</span>
|
||||
</div>
|
||||
<div flex="~ row" w-full text="xs">
|
||||
<input
|
||||
v-model="settings.openAiApiBaseURL"
|
||||
type="text"
|
||||
:placeholder="t('settings.openai-base-url.placeholder_mobile')"
|
||||
h-6 w-full rounded-md bg-transparent px-2 py-1 text-right font-mono outline-none
|
||||
>
|
||||
</div>
|
||||
<div text="xs pink-500">
|
||||
<span>{{ t('settings.openai-api-key.label') }}</span>
|
||||
</div>
|
||||
<div flex="~ row" w-full text="xs">
|
||||
<input
|
||||
v-model="settings.openAiApiKey"
|
||||
type="text"
|
||||
:placeholder="t('settings.openai-api-key.placeholder_mobile')"
|
||||
h-6 w-full rounded-md bg-transparent px-2 py-1 text-right font-mono outline-none
|
||||
>
|
||||
</div>
|
||||
<div text="xs pink-500">
|
||||
<span>{{ t('settings.elevenlabs-api-key.label') }}</span>
|
||||
</div>
|
||||
<div flex="~ row" w-full text="xs">
|
||||
<input
|
||||
v-model="settings.elevenLabsApiKey"
|
||||
type="text"
|
||||
:placeholder="t('settings.elevenlabs-api-key.placeholder_mobile')"
|
||||
h-6 w-full rounded-md bg-transparent px-2 py-1 text-right font-mono outline-none
|
||||
>
|
||||
</div>
|
||||
<div text="xs pink-500">
|
||||
<span>{{ t('settings.language') }}</span>
|
||||
</div>
|
||||
<div flex="~ row" w-full text="xs">
|
||||
<select
|
||||
v-model="settings.language"
|
||||
h-6 w-full rounded-md bg-transparent px-2 py-1 text-right font-mono outline-none
|
||||
>
|
||||
<option value="en-US">
|
||||
English
|
||||
</option>
|
||||
<option value="zh-CN">
|
||||
简体中文
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div text="xs pink-500">
|
||||
<span>{{ t('settings.models') }}</span>
|
||||
</div>
|
||||
<div flex="~ row" w-full text="xs">
|
||||
<select
|
||||
h-6 w-full rounded-md bg-transparent px-2 py-1 text-right font-mono outline-none
|
||||
@change="handleModelChange"
|
||||
>
|
||||
<option disabled class="bg-white">
|
||||
{{ t('stage.select-a-model') }}
|
||||
</option>
|
||||
<option v-if="settings.openAiModel" :value="settings.openAiModel.id">
|
||||
{{ 'name' in settings.openAiModel ? `${settings.openAiModel.name} (${settings.openAiModel.id})` : settings.openAiModel.id }}
|
||||
</option>
|
||||
<option v-for="m in supportedModels" :key="m.id" :value="m.id">
|
||||
{{ 'name' in m ? `${m.name} (${m.id})` : m.id }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div text="xs pink-500">
|
||||
<span>{{ t('settings.voices') }}</span>
|
||||
</div>
|
||||
<div flex="~ row" w-full text="xs">
|
||||
<select
|
||||
h-6 w-full rounded-md bg-transparent px-2 py-1 text-right font-mono outline-none
|
||||
@change="handleVoiceChange"
|
||||
>
|
||||
<option disabled class="bg-white">
|
||||
{{ t('stage.select-a-voice') }}
|
||||
</option>
|
||||
<option v-if="['en', 'en-US'].indexOf(locale) !== -1 && elevenlabsVoiceEnglish" :value="elevenlabsVoiceEnglish">
|
||||
{{ elevenlabsVoiceEnglish }}
|
||||
</option>
|
||||
<!-- TODO -->
|
||||
<option v-if="['zh', 'zh-CN', 'zh-TW', 'zh-HK'].indexOf(locale) !== -1 && elevenlabsVoiceEnglish" :value="elevenlabsVoiceEnglish">
|
||||
{{ elevenlabsVoiceEnglish }}
|
||||
</option>
|
||||
<option v-if="['jp', 'jp-JP'].indexOf(locale) !== -1 && elevenlabsVoiceJapanese" :value="elevenlabsVoiceJapanese">
|
||||
{{ elevenlabsVoiceJapanese }}
|
||||
</option>
|
||||
<option v-for="(m, index) in voiceList[locale]" :key="index" :value="m">
|
||||
{{ m }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2 text="slate-800/80" font-bold>
|
||||
View
|
||||
</h2>
|
||||
<div>
|
||||
<div
|
||||
grid="~ cols-[140px_1fr]" my-2 items-center gap-1.5 rounded-lg
|
||||
bg="[#fff6fc]" px-2 py-1 text="pink-400"
|
||||
>
|
||||
<div text="xs pink-500">
|
||||
<span>Viewer</span>
|
||||
</div>
|
||||
<select
|
||||
h-6 w-full rounded-md bg-transparent px-2 py-1 text-right text-xs font-mono outline-none
|
||||
@change="handleViewChange"
|
||||
>
|
||||
<option value="2d">
|
||||
2D
|
||||
</option>
|
||||
<option value="3d">
|
||||
3D
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -18,13 +18,14 @@ export enum Emotion {
|
||||
|
||||
export const EMOTION_VALUES = Object.values(Emotion)
|
||||
|
||||
export const EmotionHappyMotionName = 'EmotionHappy'
|
||||
// FIXME: need a editor to remap the motion
|
||||
export const EmotionHappyMotionName = 'Tap'
|
||||
export const EmotionSadMotionName = 'EmotionSad'
|
||||
export const EmotionAngryMotionName = 'EmotionAngry'
|
||||
export const EmotionAwkwardMotionName = 'EmotionAwkward'
|
||||
export const EmotionThinkMotionName = 'EmotionThink'
|
||||
export const EmotionSurpriseMotionName = 'EmotionSurprise'
|
||||
export const EmotionQuestionMotionName = 'EmotionQuestion'
|
||||
export const EmotionAngryMotionName = 'Tap@Body'
|
||||
export const EmotionAwkwardMotionName = 'FlickDown'
|
||||
export const EmotionThinkMotionName = 'Flick'
|
||||
export const EmotionSurpriseMotionName = 'Flick'
|
||||
export const EmotionQuestionMotionName = 'Flick@Body'
|
||||
|
||||
export const EMOTION_EmotionMotionName_value = {
|
||||
[Emotion.Happy]: EmotionHappyMotionName,
|
||||
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
declare interface Window {
|
||||
electron: {
|
||||
ipcRenderer: {
|
||||
send: (channel: string, ...args: any[]) => void
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import JSZip from 'jszip'
|
||||
import { ZipLoader } from 'pixi-live2d-display/cubism4'
|
||||
|
||||
ZipLoader.zipReader = (data: Blob, _url: string) => JSZip.loadAsync(data)
|
||||
|
||||
ZipLoader.readText = (jsZip: JSZip, path: string) => {
|
||||
const file = jsZip.file(path)
|
||||
|
||||
if (!file) {
|
||||
throw new Error(`Cannot find file: ${path}`)
|
||||
}
|
||||
|
||||
return file.async('text')
|
||||
}
|
||||
|
||||
ZipLoader.getFilePaths = (jsZip: JSZip) => {
|
||||
const paths: string[] = []
|
||||
|
||||
jsZip.forEach(relativePath => paths.push(relativePath))
|
||||
|
||||
return Promise.resolve(paths)
|
||||
}
|
||||
|
||||
ZipLoader.getFiles = (jsZip: JSZip, paths: string[]) =>
|
||||
Promise.all(paths.map(
|
||||
async (path) => {
|
||||
const fileName = path.slice(path.lastIndexOf('/') + 1)
|
||||
|
||||
const blob = await jsZip.file(path)!.async('blob')
|
||||
|
||||
return new File([blob], fileName)
|
||||
},
|
||||
))
|
||||
@@ -0,0 +1,3 @@
|
||||
export function isPlatformTamagotchi() {
|
||||
return import.meta.env.MODE === 'tamagotchi'
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import TamagotchiInteractiveArea from '../src/components/Layouts/TamagotchiInteractiveArea.vue'
|
||||
import Stage from '../src/components/Widgets/Stage.vue'
|
||||
|
||||
const dragDelay = ref(0)
|
||||
const isDragging = ref(false)
|
||||
|
||||
function handleMouseDown() {
|
||||
dragDelay.value = window.setTimeout(() => {
|
||||
isDragging.value = true
|
||||
}, 500)
|
||||
}
|
||||
|
||||
function handleMouseUp() {
|
||||
clearTimeout(dragDelay.value)
|
||||
isDragging.value = false
|
||||
}
|
||||
|
||||
function handleMouseLeave() {
|
||||
isDragging.value = false
|
||||
}
|
||||
|
||||
function handleMouseMove(event: MouseEvent) {
|
||||
if (isDragging.value) {
|
||||
window.electron.ipcRenderer.send('move-window', event.movementX, event.movementY)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div relative max-h="[100vh]" max-w="[100vw]" p="2" flex="~ col" z-2 h-full overflow-hidden @mousedown="handleMouseDown" @mouseup="handleMouseUp" @mousemove="handleMouseMove" @mouseleave="handleMouseLeave">
|
||||
<div relative h-full w-full items-end gap-2 class="view">
|
||||
<Stage h-full w-full flex-1 mb="<md:18" />
|
||||
<TamagotchiInteractiveArea class="interaction-area block" pointer-events-none absolute bottom-0 w-full opacity-0 transition="opacity duration-250" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.view {
|
||||
&:hover {
|
||||
.interaction-area {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,19 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>アイリ</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=0" />
|
||||
<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="shortcut icon" href="/favicon.ico" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<meta name="apple-mobile-web-app-title" content="アイリ" />
|
||||
<script src="/assets/js/CubismSdkForWeb-5-r.1/Core/live2dcubismcore.min.js"></script>
|
||||
</head>
|
||||
<body class="font-sans">
|
||||
<div id="app"></div>
|
||||
<script type="module" src="./main.ts"></script>
|
||||
<noscript> This website requires JavaScript to function properly. Please enable JavaScript to continue. </noscript>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,30 @@
|
||||
@import './themes.css';
|
||||
@import './transitions.css';
|
||||
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
|
||||
html {
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
#nprogress {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#nprogress .bar {
|
||||
background: rgb(13, 148, 136);
|
||||
opacity: 0.75;
|
||||
position: fixed;
|
||||
z-index: 1031;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { autoAnimatePlugin } from '@formkit/auto-animate/vue'
|
||||
import Tres from '@tresjs/core'
|
||||
import { MotionPlugin } from '@vueuse/motion'
|
||||
import { createPinia } from 'pinia'
|
||||
import { createApp } from 'vue'
|
||||
import { i18n } from '../src/modules/i18n'
|
||||
import App from './App.vue'
|
||||
|
||||
import '@unocss/reset/tailwind.css'
|
||||
import 'uno.css'
|
||||
import './main.css'
|
||||
|
||||
const pinia = createPinia()
|
||||
|
||||
createApp(App)
|
||||
.use(MotionPlugin)
|
||||
.use(autoAnimatePlugin)
|
||||
.use(pinia)
|
||||
.use(i18n)
|
||||
.use(Tres)
|
||||
.mount('#app')
|
||||
@@ -0,0 +1,13 @@
|
||||
:root {
|
||||
--airi-theme-primary-50: #fff0f2;
|
||||
--airi-theme-primary-100: #ffe3e6;
|
||||
--airi-theme-primary-200: #ffcad4;
|
||||
--airi-theme-primary-300: #ff9fb0;
|
||||
--airi-theme-primary-400: #ff6988;
|
||||
--airi-theme-primary-500: #fe456e;
|
||||
--airi-theme-primary-600: #ec124d;
|
||||
--airi-theme-primary-700: #c70941;
|
||||
--airi-theme-primary-800: #a70a3e;
|
||||
--airi-theme-primary-900: #8e0d3b;
|
||||
--airi-theme-primary-950: #50011b;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
.slide-away-enter-active,
|
||||
.slide-away-leave-active {
|
||||
transition:
|
||||
transform 0.3s ease-in-out,
|
||||
opacity 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.slide-away-enter,
|
||||
.slide-away-leave-to {
|
||||
transform: translateY(-10px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.slide-away-enter-from,
|
||||
.slide-away-leave {
|
||||
transform: translateY(10px);
|
||||
opacity: 0;
|
||||
}
|
||||
+274
-248
@@ -1,7 +1,7 @@
|
||||
import { Buffer } from 'node:buffer'
|
||||
import { copyFile, cp, mkdir, readFile, writeFile } from 'node:fs/promises'
|
||||
import { copyFile, cp, mkdir, writeFile } from 'node:fs/promises'
|
||||
import path, { join, resolve } from 'node:path'
|
||||
import { env } from 'node:process'
|
||||
import { cwd, env } from 'node:process'
|
||||
|
||||
import VueI18n from '@intlify/unplugin-vue-i18n/vite'
|
||||
import { templateCompilerOptions } from '@tresjs/core'
|
||||
@@ -21,295 +21,321 @@ import { exists } from './scripts/fs'
|
||||
import { unzip } from './scripts/unzip'
|
||||
import { appName } from './src/constants'
|
||||
|
||||
export default defineConfig({
|
||||
optimizeDeps: {
|
||||
exclude: [
|
||||
'public/assets/*',
|
||||
'@framework/live2dcubismframework',
|
||||
'@framework/math/cubismmatrix44',
|
||||
'@framework/type/csmvector',
|
||||
'@framework/math/cubismviewmatrix',
|
||||
'@framework/cubismdefaultparameterid',
|
||||
'@framework/cubismmodelsettingjson',
|
||||
'@framework/effect/cubismbreath',
|
||||
'@framework/effect/cubismeyeblink',
|
||||
'@framework/model/cubismusermodel',
|
||||
'@framework/motion/acubismmotion',
|
||||
'@framework/motion/cubismmotionqueuemanager',
|
||||
'@framework/type/csmmap',
|
||||
'@framework/utils/cubismdebug',
|
||||
'@framework/model/cubismmoc',
|
||||
],
|
||||
},
|
||||
|
||||
build: {
|
||||
rollupOptions: {
|
||||
external: [
|
||||
'virtual:pwa-register',
|
||||
export default defineConfig(({ mode }) => {
|
||||
return {
|
||||
optimizeDeps: {
|
||||
exclude: [
|
||||
'public/assets/*',
|
||||
'@framework/live2dcubismframework',
|
||||
'@framework/math/cubismmatrix44',
|
||||
'@framework/type/csmvector',
|
||||
'@framework/math/cubismviewmatrix',
|
||||
'@framework/cubismdefaultparameterid',
|
||||
'@framework/cubismmodelsettingjson',
|
||||
'@framework/effect/cubismbreath',
|
||||
'@framework/effect/cubismeyeblink',
|
||||
'@framework/model/cubismusermodel',
|
||||
'@framework/motion/acubismmotion',
|
||||
'@framework/motion/cubismmotionqueuemanager',
|
||||
'@framework/type/csmmap',
|
||||
'@framework/utils/cubismdebug',
|
||||
'@framework/model/cubismmoc',
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
plugins: [
|
||||
VueMacros({
|
||||
plugins: {
|
||||
vue: Vue({
|
||||
include: [/\.vue$/, /\.md$/],
|
||||
...templateCompilerOptions,
|
||||
}),
|
||||
build: {
|
||||
rollupOptions: {
|
||||
external: [
|
||||
'virtual:pwa-register',
|
||||
],
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
// https://github.com/posva/unplugin-vue-router
|
||||
VueRouter({
|
||||
extensions: ['.vue', '.md'],
|
||||
dts: path.resolve(__dirname, 'src/typed-router.d.ts'),
|
||||
}),
|
||||
base: mode === 'tamagotchi' ? './' : '',
|
||||
root: mode === 'tamagotchi' ? 'tamagotchi' : '',
|
||||
|
||||
// https://github.com/JohnCampionJr/vite-plugin-vue-layouts
|
||||
Layouts(),
|
||||
plugins: [
|
||||
VueMacros({
|
||||
plugins: {
|
||||
vue: Vue({
|
||||
include: [/\.vue$/, /\.md$/],
|
||||
...templateCompilerOptions,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
|
||||
// https://github.com/antfu/unplugin-vue-components
|
||||
Components({
|
||||
// https://github.com/posva/unplugin-vue-router
|
||||
VueRouter({
|
||||
extensions: ['.vue', '.md'],
|
||||
dts: path.resolve(__dirname, 'src/typed-router.d.ts'),
|
||||
}),
|
||||
|
||||
// https://github.com/JohnCampionJr/vite-plugin-vue-layouts
|
||||
Layouts(),
|
||||
|
||||
// https://github.com/antfu/unplugin-vue-components
|
||||
Components({
|
||||
// allow auto load markdown components under `./src/components/`
|
||||
extensions: ['vue', 'md'],
|
||||
// allow auto import and register components used in markdown
|
||||
include: [/\.vue$/, /\.vue\?vue/, /\.md$/],
|
||||
dts: 'src/components.d.ts',
|
||||
}),
|
||||
extensions: ['vue', 'md'],
|
||||
// allow auto import and register components used in markdown
|
||||
include: [/\.vue$/, /\.vue\?vue/, /\.md$/],
|
||||
dts: 'src/components.d.ts',
|
||||
}),
|
||||
|
||||
// https://github.com/antfu/unocss
|
||||
// see uno.config.ts for config
|
||||
Unocss(),
|
||||
// https://github.com/antfu/unocss
|
||||
// see uno.config.ts for config
|
||||
Unocss(),
|
||||
|
||||
// https://github.com/antfu/vite-plugin-pwa
|
||||
...(env.TARGET_HUGGINGFACE_SPACE
|
||||
? []
|
||||
: [VitePWA({
|
||||
registerType: 'autoUpdate',
|
||||
includeAssets: ['favicon.svg', 'apple-touch-icon.png'],
|
||||
manifest: {
|
||||
name: appName,
|
||||
short_name: appName,
|
||||
theme_color: '#ffffff',
|
||||
icons: [
|
||||
{
|
||||
src: '/web-app-manifest-192x192.png',
|
||||
sizes: '192x192',
|
||||
type: 'image/png',
|
||||
},
|
||||
{
|
||||
src: '/web-app-manifest-512x512.png',
|
||||
sizes: '512x512',
|
||||
type: 'image/png',
|
||||
},
|
||||
],
|
||||
},
|
||||
})]),
|
||||
// https://github.com/antfu/vite-plugin-pwa
|
||||
...(env.TARGET_HUGGINGFACE_SPACE || mode === 'tamagotchi'
|
||||
? []
|
||||
: [VitePWA({
|
||||
registerType: 'autoUpdate',
|
||||
includeAssets: ['favicon.svg', 'apple-touch-icon.png'],
|
||||
manifest: {
|
||||
name: appName,
|
||||
short_name: appName,
|
||||
theme_color: '#ffffff',
|
||||
icons: [
|
||||
{
|
||||
src: '/web-app-manifest-192x192.png',
|
||||
sizes: '192x192',
|
||||
type: 'image/png',
|
||||
},
|
||||
{
|
||||
src: '/web-app-manifest-512x512.png',
|
||||
sizes: '512x512',
|
||||
type: 'image/png',
|
||||
},
|
||||
],
|
||||
},
|
||||
})]),
|
||||
|
||||
// https://github.com/intlify/bundle-tools/tree/main/packages/unplugin-vue-i18n
|
||||
VueI18n({
|
||||
runtimeOnly: true,
|
||||
compositionOnly: true,
|
||||
fullInstall: true,
|
||||
include: [path.resolve(__dirname, 'locales/**')],
|
||||
}),
|
||||
// https://github.com/intlify/bundle-tools/tree/main/packages/unplugin-vue-i18n
|
||||
VueI18n({
|
||||
runtimeOnly: true,
|
||||
compositionOnly: true,
|
||||
fullInstall: true,
|
||||
include: [path.resolve(__dirname, 'locales/**')],
|
||||
}),
|
||||
|
||||
// https://github.com/webfansplz/vite-plugin-vue-devtools
|
||||
VueDevTools(),
|
||||
// https://github.com/webfansplz/vite-plugin-vue-devtools
|
||||
VueDevTools(),
|
||||
|
||||
{
|
||||
name: 'live2d-cubism-sdk',
|
||||
async configResolved(config) {
|
||||
const cacheDir = resolve(join(config.root, '.cache'))
|
||||
const publicDir = resolve(join(config.root, 'public'))
|
||||
{
|
||||
name: 'live2d-cubism-sdk',
|
||||
async configResolved(config) {
|
||||
const cacheDir = resolve(join(config.root, '.cache'))
|
||||
const publicDir = resolve(join(config.root, 'public'))
|
||||
|
||||
try {
|
||||
if (!(await exists(resolve(join(cacheDir, 'assets/js/CubismSdkForWeb-5-r.1'))))) {
|
||||
console.log('Downloading Cubism SDK...')
|
||||
const stream = await ofetch('https://dist.ayaka.moe/npm/live2d-cubism/CubismSdkForWeb-5-r.1.zip', { responseType: 'arrayBuffer' })
|
||||
try {
|
||||
if (!(await exists(resolve(join(cacheDir, 'assets/js/CubismSdkForWeb-5-r.1'))))) {
|
||||
console.log('Downloading Cubism SDK...')
|
||||
const stream = await ofetch('https://dist.ayaka.moe/npm/live2d-cubism/CubismSdkForWeb-5-r.1.zip', { responseType: 'arrayBuffer' })
|
||||
|
||||
console.log('Unzipping Cubism SDK...')
|
||||
await mkdir(join(cacheDir, 'assets/js'), { recursive: true })
|
||||
await unzip(Buffer.from(stream), join(cacheDir, 'assets/js'))
|
||||
console.log('Unzipping Cubism SDK...')
|
||||
await mkdir(join(cacheDir, 'assets/js'), { recursive: true })
|
||||
await unzip(Buffer.from(stream), join(cacheDir, 'assets/js'))
|
||||
|
||||
console.log('Cubism SDK downloaded and unzipped.')
|
||||
console.log('Cubism SDK downloaded and unzipped.')
|
||||
}
|
||||
|
||||
if (!(await exists(resolve(join(publicDir, 'assets/js/CubismSdkForWeb-5-r.1'))))) {
|
||||
await mkdir(join(publicDir, 'assets/js/CubismSdkForWeb-5-r.1/Core'), { recursive: true }).catch(() => {})
|
||||
await copyFile(join(cacheDir, 'assets/js/CubismSdkForWeb-5-r.1/Core/live2dcubismcore.min.js'), join(publicDir, 'assets/js/CubismSdkForWeb-5-r.1/Core/live2dcubismcore.min.js'))
|
||||
}
|
||||
}
|
||||
|
||||
if (!(await exists(resolve(join(publicDir, 'assets/js/CubismSdkForWeb-5-r.1'))))) {
|
||||
await mkdir(join(publicDir, 'assets/js/CubismSdkForWeb-5-r.1/Core'), { recursive: true }).catch(() => {})
|
||||
await copyFile(join(cacheDir, 'assets/js/CubismSdkForWeb-5-r.1/Core/live2dcubismcore.min.js'), join(publicDir, 'assets/js/CubismSdkForWeb-5-r.1/Core/live2dcubismcore.min.js'))
|
||||
catch (err) {
|
||||
console.error(err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err)
|
||||
throw err
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'live2d-models-hiyori-free',
|
||||
async configResolved(config) {
|
||||
const cacheDir = resolve(join(config.root, '.cache'))
|
||||
const publicDir = resolve(join(config.root, 'public'))
|
||||
{
|
||||
name: 'live2d-models-hiyori-free',
|
||||
async configResolved(config) {
|
||||
const cacheDir = resolve(join(config.root, '.cache'))
|
||||
const publicDir = resolve(join(config.root, 'public'))
|
||||
|
||||
try {
|
||||
if (!(await exists(resolve(join(cacheDir, 'assets/live2d/models/hiyori_free_zh'))))) {
|
||||
console.log('Downloading Demo Live2D Model - Hiyori Free...')
|
||||
const stream = await ofetch('https://dist.ayaka.moe/live2d-models/hiyori_free_zh.zip', { responseType: 'arrayBuffer' })
|
||||
try {
|
||||
if (!(await exists(resolve(join(cacheDir, 'assets/live2d/models/hiyori_free_zh'))))) {
|
||||
console.log('Downloading Demo Live2D Model - Hiyori Free...')
|
||||
const stream = await ofetch('https://dist.ayaka.moe/live2d-models/hiyori_free_zh.zip', { responseType: 'arrayBuffer' })
|
||||
|
||||
console.log('Unzipping Demo Live2D Model - Hiyori Free...')
|
||||
await mkdir(join(cacheDir, 'assets/live2d/models'), { recursive: true })
|
||||
await unzip(Buffer.from(stream), join(cacheDir, 'assets/live2d/models'))
|
||||
console.log('Unzipping Demo Live2D Model - Hiyori Free...')
|
||||
await mkdir(join(cacheDir, 'assets/live2d/models'), { recursive: true })
|
||||
await unzip(Buffer.from(stream), join(cacheDir, 'assets/live2d/models'))
|
||||
console.log('Demo Live2D Model - Hiyori Free downloaded and unzipped.')
|
||||
}
|
||||
|
||||
console.log('Demo Live2D Model - Hiyori Free downloaded and unzipped.')
|
||||
if (!(await exists(resolve(join(publicDir, 'assets/live2d/models/hiyori_free_zh'))))) {
|
||||
await mkdir(join(publicDir, 'assets/live2d/models'), { recursive: true }).catch(() => { })
|
||||
await cp(join(cacheDir, 'assets/live2d/models/hiyori_free_zh'), join(publicDir, 'assets/live2d/models/hiyori_free_zh'), { recursive: true })
|
||||
}
|
||||
}
|
||||
|
||||
if (!(await exists(resolve(join(publicDir, 'assets/live2d/models/hiyori_free_zh'))))) {
|
||||
await mkdir(join(publicDir, 'assets/live2d/models'), { recursive: true }).catch(() => { })
|
||||
await cp(join(cacheDir, 'assets/live2d/models/hiyori_free_zh'), join(publicDir, 'assets/live2d/models/hiyori_free_zh'), { recursive: true })
|
||||
catch (err) {
|
||||
console.error(err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err)
|
||||
throw err
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'live2d-models-hiyori-pro',
|
||||
async configResolved(config) {
|
||||
const cacheDir = resolve(join(config.root, '.cache'))
|
||||
const publicDir = resolve(join(config.root, 'public'))
|
||||
{
|
||||
name: 'live2d-models-hiyori-pro',
|
||||
async configResolved(config) {
|
||||
const cacheDir = resolve(join(config.root, '.cache'))
|
||||
const publicDir = resolve(join(config.root, 'public'))
|
||||
|
||||
try {
|
||||
if (!(await exists(resolve(join(cacheDir, 'assets/live2d/models/hiyori_pro_zh'))))) {
|
||||
console.log('Downloading Demo Live2D Model - Hiyori Pro...')
|
||||
const stream = await ofetch('https://dist.ayaka.moe/live2d-models/hiyori_pro_zh.zip', { responseType: 'arrayBuffer' })
|
||||
try {
|
||||
if (!(await exists(resolve(join(cacheDir, 'assets/live2d/models/hiyori_pro_zh'))))) {
|
||||
console.log('Downloading Demo Live2D Model - Hiyori Pro...')
|
||||
const stream = await ofetch('https://dist.ayaka.moe/live2d-models/hiyori_pro_zh.zip', { responseType: 'arrayBuffer' })
|
||||
|
||||
console.log('Unzipping Demo Live2D Model - Hiyori Pro...')
|
||||
await mkdir(join(cacheDir, 'assets/live2d/models'), { recursive: true })
|
||||
await unzip(Buffer.from(stream), join(cacheDir, 'assets/live2d/models'))
|
||||
console.log('Unzipping Demo Live2D Model - Hiyori Pro...')
|
||||
await mkdir(join(cacheDir, 'assets/live2d/models'), { recursive: true })
|
||||
// await unzip(Buffer.from(stream), join(cacheDir, 'assets/live2d/models'))
|
||||
await writeFile(join(cacheDir, 'assets/live2d/models/hiyori_pro_zh.zip'), Buffer.from(stream))
|
||||
|
||||
console.log('Demo Live2D Model - Hiyori Pro downloaded and unzipped.')
|
||||
console.log('Demo Live2D Model - Hiyori Pro downloaded.')
|
||||
}
|
||||
|
||||
if (!(await exists(resolve(join(publicDir, 'assets/live2d/models/hiyori_pro_zh.zip'))))) {
|
||||
await mkdir(join(publicDir, 'assets/live2d/models'), { recursive: true }).catch(() => { })
|
||||
await cp(join(cacheDir, 'assets/live2d/models/hiyori_pro_zh.zip'), join(publicDir, 'assets/live2d/models/hiyori_pro_zh.zip'), { recursive: true })
|
||||
}
|
||||
|
||||
// TODO: use motion editor to remap emotions
|
||||
// const hiyoriEmotions = {
|
||||
// Idle: [
|
||||
// {
|
||||
// File: 'motion/hiyori_m01.motion3.json',
|
||||
// },
|
||||
// {
|
||||
// File: 'motion/hiyori_m05.motion3.json',
|
||||
// },
|
||||
// ],
|
||||
// EmotionHappy: [{ File: 'motion/hiyori_m08.motion3.json' }],
|
||||
// EmotionSad: [{ File: 'motion/hiyori_m10.motion3.json' }],
|
||||
// EmotionAngry: [{ File: 'motion/hiyori_m09.motion3.json' }],
|
||||
// EmotionAwkward: [{ File: 'motion/hiyori_m04.motion3.json' }],
|
||||
// EmotionThink: [{ File: 'motion/hiyori_m03.motion3.json' }],
|
||||
// EmotionSurprise: [{ File: 'motion/hiyori_m03.motion3.json' }],
|
||||
// EmotionQuestion: [{ File: 'motion/hiyori_m10.motion3.json' }],
|
||||
// }
|
||||
|
||||
// const read = await readFile(join(publicDir, 'assets/live2d/models/hiyori_pro_zh/runtime/hiyori_pro_t11.model3.json'), 'utf-8')
|
||||
// const model = JSON.parse(read.toString()) as { FileReferences: { Motions: Record<string, { File: string }[]> } }
|
||||
// Object.assign(model.FileReferences.Motions, hiyoriEmotions)
|
||||
// await writeFile(join(publicDir, 'assets/live2d/models/hiyori_pro_zh/runtime/hiyori_pro_t11.model3.json'), JSON.stringify(model, null, 4), 'utf-8')
|
||||
}
|
||||
|
||||
if (!(await exists(resolve(join(publicDir, 'assets/live2d/models/hiyori_pro_zh'))))) {
|
||||
await mkdir(join(publicDir, 'assets/live2d/models'), { recursive: true }).catch(() => { })
|
||||
await cp(join(cacheDir, 'assets/live2d/models/hiyori_pro_zh'), join(publicDir, 'assets/live2d/models/hiyori_pro_zh'), { recursive: true })
|
||||
catch (err) {
|
||||
console.error(err)
|
||||
throw err
|
||||
}
|
||||
|
||||
const hiyoriEmotions = {
|
||||
Idle: [
|
||||
{
|
||||
File: 'motion/hiyori_m01.motion3.json',
|
||||
},
|
||||
{
|
||||
File: 'motion/hiyori_m05.motion3.json',
|
||||
},
|
||||
],
|
||||
EmotionHappy: [{ File: 'motion/hiyori_m08.motion3.json' }],
|
||||
EmotionSad: [{ File: 'motion/hiyori_m10.motion3.json' }],
|
||||
EmotionAngry: [{ File: 'motion/hiyori_m09.motion3.json' }],
|
||||
EmotionAwkward: [{ File: 'motion/hiyori_m04.motion3.json' }],
|
||||
EmotionThink: [{ File: 'motion/hiyori_m03.motion3.json' }],
|
||||
EmotionSurprise: [{ File: 'motion/hiyori_m03.motion3.json' }],
|
||||
EmotionQuestion: [{ File: 'motion/hiyori_m10.motion3.json' }],
|
||||
}
|
||||
|
||||
const read = await readFile(join(publicDir, 'assets/live2d/models/hiyori_pro_zh/runtime/hiyori_pro_t11.model3.json'), 'utf-8')
|
||||
const model = JSON.parse(read.toString()) as { FileReferences: { Motions: Record<string, { File: string }[]> } }
|
||||
Object.assign(model.FileReferences.Motions, hiyoriEmotions)
|
||||
await writeFile(join(publicDir, 'assets/live2d/models/hiyori_pro_zh/runtime/hiyori_pro_t11.model3.json'), JSON.stringify(model, null, 4), 'utf-8')
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err)
|
||||
throw err
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'vrm-models-sample-a',
|
||||
async configResolved(config) {
|
||||
const cacheDir = resolve(join(config.root, '.cache'))
|
||||
const publicDir = resolve(join(config.root, 'public'))
|
||||
{
|
||||
name: 'vrm-models-sample-a',
|
||||
async configResolved(config) {
|
||||
const cacheDir = resolve(join(config.root, '.cache'))
|
||||
const publicDir = resolve(join(config.root, 'public'))
|
||||
|
||||
try {
|
||||
if (!(await exists(resolve(join(cacheDir, 'assets/vrm/models/AvatarSample-A'))))) {
|
||||
await mkdir(join(cacheDir, 'assets/vrm/models/AvatarSample-A'), { recursive: true })
|
||||
try {
|
||||
if (!(await exists(resolve(join(cacheDir, 'assets/vrm/models/AvatarSample-A'))))) {
|
||||
await mkdir(join(cacheDir, 'assets/vrm/models/AvatarSample-A'), { recursive: true })
|
||||
|
||||
console.log('Downloading VRM Model - Avatar Sample A...')
|
||||
const res = await ofetch('https://dist.ayaka.moe/vrm-models/VRoid-Hub/AvatarSample-A/AvatarSample_A.vrm', { responseType: 'arrayBuffer' })
|
||||
console.log('Downloading VRM Model - Avatar Sample A...')
|
||||
const res = await ofetch('https://dist.ayaka.moe/vrm-models/VRoid-Hub/AvatarSample-A/AvatarSample_A.vrm', { responseType: 'arrayBuffer' })
|
||||
|
||||
console.log('Saving VRM Model - Avatar Sample A...')
|
||||
await writeFile(join(cacheDir, 'assets/vrm/models/AvatarSample-A/AvatarSample_A.vrm'), Buffer.from(res))
|
||||
console.log('Saving VRM Model - Avatar Sample A...')
|
||||
await writeFile(join(cacheDir, 'assets/vrm/models/AvatarSample-A/AvatarSample_A.vrm'), Buffer.from(res))
|
||||
|
||||
console.log('VRM Model - Avatar Sample A downloaded and saved.')
|
||||
console.log('VRM Model - Avatar Sample A downloaded and saved.')
|
||||
}
|
||||
|
||||
if (!(await exists(resolve(join(publicDir, 'assets/vrm/models/AvatarSample-A'))))) {
|
||||
await mkdir(join(publicDir, 'assets/vrm/models/AvatarSample-A'), { recursive: true }).catch(() => { })
|
||||
await cp(join(cacheDir, 'assets/vrm/models/AvatarSample-A'), join(publicDir, 'assets/vrm/models/AvatarSample-A'), { recursive: true })
|
||||
}
|
||||
}
|
||||
|
||||
if (!(await exists(resolve(join(publicDir, 'assets/vrm/models/AvatarSample-A'))))) {
|
||||
await mkdir(join(publicDir, 'assets/vrm/models/AvatarSample-A'), { recursive: true }).catch(() => { })
|
||||
await cp(join(cacheDir, 'assets/vrm/models/AvatarSample-A'), join(publicDir, 'assets/vrm/models/AvatarSample-A'), { recursive: true })
|
||||
catch (err) {
|
||||
console.error(err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err)
|
||||
throw err
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'vrm-models-sample-b',
|
||||
async configResolved(config) {
|
||||
const cacheDir = resolve(join(config.root, '.cache'))
|
||||
const publicDir = resolve(join(config.root, 'public'))
|
||||
{
|
||||
name: 'vrm-models-sample-b',
|
||||
async configResolved(config) {
|
||||
const cacheDir = resolve(join(config.root, '.cache'))
|
||||
const publicDir = resolve(join(config.root, 'public'))
|
||||
|
||||
try {
|
||||
if (!(await exists(resolve(join(cacheDir, 'assets/vrm/models/AvatarSample-B'))))) {
|
||||
await mkdir(join(cacheDir, 'assets/vrm/models/AvatarSample-B'), { recursive: true })
|
||||
try {
|
||||
if (!(await exists(resolve(join(cacheDir, 'assets/vrm/models/AvatarSample-B'))))) {
|
||||
await mkdir(join(cacheDir, 'assets/vrm/models/AvatarSample-B'), { recursive: true })
|
||||
|
||||
console.log('Downloading VRM Model - Avatar Sample B...')
|
||||
const res = await ofetch('https://dist.ayaka.moe/vrm-models/VRoid-Hub/AvatarSample-B/AvatarSample_B.vrm', { responseType: 'arrayBuffer' })
|
||||
console.log('Downloading VRM Model - Avatar Sample B...')
|
||||
const res = await ofetch('https://dist.ayaka.moe/vrm-models/VRoid-Hub/AvatarSample-B/AvatarSample_B.vrm', { responseType: 'arrayBuffer' })
|
||||
|
||||
console.log('Saving VRM Model - Avatar Sample B...')
|
||||
await writeFile(join(cacheDir, 'assets/vrm/models/AvatarSample-B/AvatarSample_B.vrm'), Buffer.from(res))
|
||||
console.log('Saving VRM Model - Avatar Sample B...')
|
||||
await writeFile(join(cacheDir, 'assets/vrm/models/AvatarSample-B/AvatarSample_B.vrm'), Buffer.from(res))
|
||||
}
|
||||
|
||||
if (!(await exists(resolve(join(publicDir, 'assets/vrm/models/AvatarSample-B'))))) {
|
||||
await mkdir(join(publicDir, 'assets/vrm/models/AvatarSample-B'), { recursive: true }).catch(() => { })
|
||||
await cp(join(cacheDir, 'assets/vrm/models/AvatarSample-B'), join(publicDir, 'assets/vrm/models/AvatarSample-B'), { recursive: true })
|
||||
}
|
||||
}
|
||||
|
||||
if (!(await exists(resolve(join(publicDir, 'assets/vrm/models/AvatarSample-B'))))) {
|
||||
await mkdir(join(publicDir, 'assets/vrm/models/AvatarSample-B'), { recursive: true }).catch(() => { })
|
||||
await cp(join(cacheDir, 'assets/vrm/models/AvatarSample-B'), join(publicDir, 'assets/vrm/models/AvatarSample-B'), { recursive: true })
|
||||
catch (err) {
|
||||
console.error(err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err)
|
||||
throw err
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// HuggingFace Spaces
|
||||
LFS({
|
||||
extraGlobs: [
|
||||
'*.vrm',
|
||||
'*.cmo3',
|
||||
'*.png',
|
||||
'*.jpg',
|
||||
'*.jpeg',
|
||||
'*.gif',
|
||||
'*.webp',
|
||||
'*.bmp',
|
||||
],
|
||||
}),
|
||||
SpaceCard({
|
||||
title: 'アイリ VTuber',
|
||||
emoji: '🧸',
|
||||
colorFrom: 'pink',
|
||||
colorTo: 'pink',
|
||||
sdk: 'static',
|
||||
pinned: false,
|
||||
license: 'mit',
|
||||
models: ['onnx-community/whisper-base'],
|
||||
short_description: 'アイリ VTuber. LLM powered Live2D/VRM living character.',
|
||||
}),
|
||||
],
|
||||
// HuggingFace Spaces
|
||||
LFS({
|
||||
extraGlobs: [
|
||||
'*.vrm',
|
||||
'*.cmo3',
|
||||
'*.png',
|
||||
'*.jpg',
|
||||
'*.jpeg',
|
||||
'*.gif',
|
||||
'*.webp',
|
||||
'*.bmp',
|
||||
],
|
||||
}),
|
||||
SpaceCard({
|
||||
title: 'アイリ VTuber',
|
||||
emoji: '🧸',
|
||||
colorFrom: 'pink',
|
||||
colorTo: 'pink',
|
||||
sdk: 'static',
|
||||
pinned: false,
|
||||
license: 'mit',
|
||||
models: ['onnx-community/whisper-base'],
|
||||
short_description: 'アイリ VTuber. LLM powered Live2D/VRM living character.',
|
||||
}),
|
||||
{
|
||||
name: 'write-port-number-to-file-when-dev',
|
||||
apply: 'serve',
|
||||
configureServer(server) {
|
||||
if (mode !== 'tamagotchi')
|
||||
return
|
||||
|
||||
if (!server.httpServer)
|
||||
return
|
||||
|
||||
server.httpServer.once('listening', () => {
|
||||
const address = server.httpServer!.address()
|
||||
if (!address || typeof address !== 'object' || !('port' in address))
|
||||
return
|
||||
writeFile(join(cwd(), 'stage.dev.json'), JSON.stringify({
|
||||
address,
|
||||
}))
|
||||
})
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
out/
|
||||
@@ -0,0 +1,38 @@
|
||||
# たまごっち アイリ
|
||||
|
||||
A desktop application for たまごっち アイリ.
|
||||
|
||||
## Project Setup
|
||||
|
||||
### Install
|
||||
|
||||
```bash
|
||||
$ pnpm install
|
||||
```
|
||||
|
||||
### Development
|
||||
|
||||
```bash
|
||||
$ cd /packages/stage
|
||||
$ pnpm dev:tamagotchi
|
||||
```
|
||||
|
||||
Then open another terminal and run:
|
||||
|
||||
```bash
|
||||
$ cd /packages/tamagotchi
|
||||
$ pnpm dev:tamagotchi
|
||||
```
|
||||
|
||||
### Build
|
||||
|
||||
```bash
|
||||
# For windows
|
||||
$ pnpm build:win
|
||||
|
||||
# For macOS
|
||||
$ pnpm build:mac
|
||||
|
||||
# For Linux
|
||||
$ pnpm build:linux
|
||||
```
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 7.7 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,43 @@
|
||||
appId: com.github.moeru-ai.airi-tamagotchi
|
||||
productName: airi
|
||||
directories:
|
||||
buildResources: build
|
||||
files:
|
||||
- '!**/.vscode/*'
|
||||
- '!src/*'
|
||||
- '!electron.vite.config.{js,ts,mjs,cjs}'
|
||||
- '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
|
||||
- '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
|
||||
- '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
|
||||
asarUnpack:
|
||||
- resources/**
|
||||
win:
|
||||
executableName: tamagotchi
|
||||
nsis:
|
||||
artifactName: ${name}-${version}-setup.${ext}
|
||||
shortcutName: ${productName}
|
||||
uninstallDisplayName: ${productName}
|
||||
createDesktopShortcut: always
|
||||
mac:
|
||||
entitlementsInherit: build/entitlements.mac.plist
|
||||
extendInfo:
|
||||
- NSCameraUsageDescription: Application requests access to the device's camera.
|
||||
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
|
||||
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
|
||||
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
|
||||
notarize: false
|
||||
dmg:
|
||||
artifactName: ${name}-${version}.${ext}
|
||||
linux:
|
||||
target:
|
||||
- AppImage
|
||||
- snap
|
||||
- deb
|
||||
maintainer: github.com/moeru-ai Contributors
|
||||
category: Entertainment
|
||||
appImage:
|
||||
artifactName: ${name}-${version}.${ext}
|
||||
npmRebuild: false
|
||||
# publish:
|
||||
# provider: generic
|
||||
# url: https://example.com/auto-updates
|
||||
@@ -0,0 +1,20 @@
|
||||
import { join, resolve } from 'node:path'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
|
||||
|
||||
export default defineConfig({
|
||||
main: {
|
||||
plugins: [externalizeDepsPlugin()],
|
||||
},
|
||||
preload: {
|
||||
plugins: [externalizeDepsPlugin()],
|
||||
},
|
||||
renderer: {
|
||||
resolve: {
|
||||
alias: {
|
||||
'@renderer': resolve(join('src', 'renderer', 'src')),
|
||||
},
|
||||
},
|
||||
plugins: [vue()],
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "@proj-airi/stage-tamagotchi",
|
||||
"version": "1.0.0",
|
||||
"description": "An Electron application with Vue and TypeScript",
|
||||
"author": "LemonNekoGH",
|
||||
"homepage": "https://electron-vite.org",
|
||||
"main": "./out/main/index.js",
|
||||
"scripts": {
|
||||
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
|
||||
"typecheck:web": "vue-tsc --noEmit -p tsconfig.web.json --composite false",
|
||||
"typecheck": "npm run typecheck:node && npm run typecheck:web",
|
||||
"start": "electron-vite preview",
|
||||
"dev:tamagotchi": "npm run build:tamagotchi && electron .",
|
||||
"build:tamagotchi": "npm run typecheck && electron-vite build && rm -rf ./out/renderer && cp -r ../stage/tamagotchi/dist/ ./out/renderer",
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
"build:unpack": "npm run build:tamagotchi && electron-builder --dir",
|
||||
"build:win": "npm run build:tamagotchi && electron-builder --win",
|
||||
"build:mac": "npm run build:tamagotchi && electron-builder --mac",
|
||||
"build:linux": "npm run build:tamagotchi && electron-builder --linux"
|
||||
},
|
||||
"dependencies": {
|
||||
"@electron-toolkit/preload": "^3.0.0",
|
||||
"@electron-toolkit/utils": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron-toolkit/eslint-config": "^1.0.2",
|
||||
"@electron-toolkit/eslint-config-ts": "^2.0.0",
|
||||
"@electron-toolkit/tsconfig": "^1.0.1",
|
||||
"@rushstack/eslint-patch": "^1.10.3",
|
||||
"@types/node": "^20.14.8",
|
||||
"@vitejs/plugin-vue": "^5.0.5",
|
||||
"electron": "^31.0.2",
|
||||
"electron-builder": "^24.13.3",
|
||||
"electron-vite": "^2.3.0",
|
||||
"typescript": "^5.5.2",
|
||||
"vite": "^5.3.1",
|
||||
"vue": "^3.4.30",
|
||||
"vue-tsc": "^2.0.22"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { platform } from 'node:process'
|
||||
import { electronApp, is, optimizer } from '@electron-toolkit/utils'
|
||||
import { app, BrowserWindow, ipcMain, shell } from 'electron'
|
||||
import icon from '../../build/icon.png?asset'
|
||||
|
||||
function createWindow(): void {
|
||||
// Create the browser window.
|
||||
const mainWindow = new BrowserWindow({
|
||||
width: 300,
|
||||
height: 400,
|
||||
show: false,
|
||||
autoHideMenuBar: true,
|
||||
frame: false,
|
||||
transparent: true,
|
||||
resizable: false,
|
||||
hasShadow: false,
|
||||
alwaysOnTop: true,
|
||||
...(platform === 'linux' ? { icon } : {}),
|
||||
webPreferences: {
|
||||
preload: join(import.meta.dirname, '..', 'preload', 'index.js'),
|
||||
sandbox: false,
|
||||
},
|
||||
})
|
||||
|
||||
mainWindow.on('ready-to-show', () => {
|
||||
mainWindow.show()
|
||||
})
|
||||
|
||||
mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||
shell.openExternal(details.url)
|
||||
return { action: 'deny' }
|
||||
})
|
||||
|
||||
mainWindow.show()
|
||||
|
||||
// HMR for renderer base on electron-vite cli.
|
||||
// Load the remote URL for development or the local html file for production.
|
||||
|
||||
if (is.dev) {
|
||||
// try to read port number from stage.dev.json
|
||||
const devFile = readFileSync(join(import.meta.dirname, '../../../stage/stage.dev.json'), 'utf-8')
|
||||
const devInfo = JSON.parse(devFile) as { address: { address: string, family: string, port: number } }
|
||||
mainWindow.loadURL(`http://localhost:${devInfo.address.port}`).catch((e) => {
|
||||
console.error('Failed to load URL', e)
|
||||
})
|
||||
}
|
||||
else {
|
||||
mainWindow.loadFile(join(import.meta.dirname, '..', '..', 'out', 'renderer', 'index.html'))
|
||||
}
|
||||
|
||||
ipcMain.on('move-window', (_, dx, dy) => {
|
||||
const [currentX, currentY] = mainWindow.getPosition()
|
||||
mainWindow.setPosition(currentX + dx, currentY + dy)
|
||||
})
|
||||
}
|
||||
|
||||
// This method will be called when Electron has finished
|
||||
// initialization and is ready to create browser windows.
|
||||
// Some APIs can only be used after this event occurs.
|
||||
app.whenReady().then(() => {
|
||||
// Set app user model id for windows
|
||||
electronApp.setAppUserModelId('com.github.moeru-ai.airi-tamagotchi')
|
||||
|
||||
// Default open or close DevTools by F12 in development
|
||||
// and ignore CommandOrControl + R in production.
|
||||
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
|
||||
app.on('browser-window-created', (_, window) => {
|
||||
optimizer.watchWindowShortcuts(window)
|
||||
})
|
||||
|
||||
// IPC test
|
||||
ipcMain.on('quit', () => app.quit())
|
||||
|
||||
createWindow()
|
||||
|
||||
app.on('activate', () => {
|
||||
// On macOS it's common to re-create a window in the app when the
|
||||
// dock icon is clicked and there are no other windows open.
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// Quit when all windows are closed, except on macOS. There, it's common
|
||||
// for applications and their menu bar to stay active until the user quits
|
||||
// explicitly with Cmd + Q.
|
||||
app.on('window-all-closed', () => {
|
||||
if (platform !== 'darwin') {
|
||||
app.quit()
|
||||
}
|
||||
})
|
||||
|
||||
// In this file you can include the rest of your app"s specific main process
|
||||
// code. You can also put them in separate files and require them here.
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
import type { ElectronAPI } from '@electron-toolkit/preload'
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
electron: ElectronAPI
|
||||
api: unknown
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { contextIsolated } from 'node:process'
|
||||
import { electronAPI } from '@electron-toolkit/preload'
|
||||
import { contextBridge } from 'electron'
|
||||
|
||||
// Custom APIs for renderer
|
||||
const api = {}
|
||||
|
||||
// Use `contextBridge` APIs to expose Electron APIs to
|
||||
// renderer only if context isolation is enabled, otherwise
|
||||
// just add to the DOM global.
|
||||
if (contextIsolated) {
|
||||
try {
|
||||
contextBridge.exposeInMainWorld('electron', electronAPI)
|
||||
contextBridge.exposeInMainWorld('api', api)
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
else {
|
||||
// @ts-expect-error (define in dts)
|
||||
window.electron = electronAPI
|
||||
// @ts-expect-error (define in dts)
|
||||
window.api = api
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>アイリ</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=0" />
|
||||
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:"
|
||||
/>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>PLACE HOLDER, YOU SHOULD NOT SEE THIS DURING DEVELOPMENT</h1>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"references": [{ "path": "./tsconfig.node.json" }, { "path": "./tsconfig.web.json" }],
|
||||
"files": []
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "@electron-toolkit/tsconfig/tsconfig.node.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"types": ["electron-vite/node"]
|
||||
},
|
||||
"include": ["electron.vite.config.*", "src/main/**/*", "src/preload/**/*"]
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"extends": "@electron-toolkit/tsconfig/tsconfig.web.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@renderer/*": [
|
||||
"src/renderer/src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"src/renderer/src/env.d.ts",
|
||||
"src/renderer/src/**/*",
|
||||
"src/renderer/src/**/*.vue",
|
||||
"src/preload/*.d.ts"
|
||||
]
|
||||
}
|
||||
Generated
+1740
-99
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user