feat(stage-tamagotchi): loading status

This commit is contained in:
Neko Ayaka
2025-06-29 02:01:13 +08:00
parent 2dcf36feb7
commit de326cf72c
10 changed files with 181 additions and 43 deletions
+2
View File
@@ -18,6 +18,7 @@
"dependencies": {
"@formkit/auto-animate": "^0.8.2",
"@huggingface/transformers": "^3.5.2",
"@moeru/std": "catalog:",
"@pixiv/three-vrm": "^3.4.1",
"@pixiv/three-vrm-animation": "^3.4.1",
"@pixiv/three-vrm-core": "^3.4.1",
@@ -57,6 +58,7 @@
"popmotion": "^11.0.5",
"radix-vue": "^1.9.17",
"rehype-stringify": "^10.0.1",
"reka-ui": "^2.3.1",
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.2",
"shiki": "^3.7.0",
+8 -3
View File
@@ -1,6 +1,7 @@
<script setup lang="ts">
import type { AiriTamagotchiEvents } from './composables/tauri'
import { useMcpStore, useSettings } from '@proj-airi/stage-ui/stores'
import { listen } from '@tauri-apps/api/event'
import { Window } from '@tauri-apps/api/window'
import { useEventListener } from '@vueuse/core'
import { storeToRefs } from 'pinia'
@@ -9,6 +10,7 @@ import { useI18n } from 'vue-i18n'
import { RouterView } from 'vue-router'
import { useAppRuntime } from './composables/runtime'
import { useTauriEvent } from './composables/tauri'
import { useWindowControlStore } from './stores/window-controls'
const { language, themeColorsHue, themeColorsHueDynamic, allowVisibleOnAllWorkspaces } = storeToRefs(useSettings())
@@ -16,6 +18,7 @@ const i18n = useI18n()
const windowControlStore = useWindowControlStore()
const mcpStore = useMcpStore()
const { platform } = useAppRuntime()
const { listen } = useTauriEvent<AiriTamagotchiEvents>()
useEventListener(window, 'resize', () => {
windowControlStore.size.width = window.innerWidth
@@ -38,8 +41,10 @@ watch(themeColorsHueDynamic, () => {
document.documentElement.classList.toggle('dynamic-hue', themeColorsHueDynamic.value)
}, { immediate: true })
listen('mcp_plugin_destroyed', () => {
mcpStore.connected = false
onMounted(() => {
listen('mcp_plugin_destroyed', () => {
mcpStore.connected = false
})
})
const isMac = computed(() => platform.value === 'macos')
@@ -15,18 +15,12 @@ const totalProgress = computed(() => {
</script>
<template>
<div
left="0" fixed top-0 m-3 rounded-xl p-3
shadow-md bg="white/80 dark:neutral-900/80"
backdrop-blur="md"
w="[calc(100%-1.5rem)]"
space-y-3
>
<div flex flex-col gap-3>
<div>
<div flex items-center gap-2>
<div i-solar:danger-triangle-bold-duotone text="orange" />
<div flex items-center gap-2 text="sm orange-600 dark:orange-400">
<div i-solar:danger-triangle-bold-duotone />
<div>
Some modules are not available yet...
Some modules haven't been available yet...
</div>
</div>
<ul ml-4 mt-3>
@@ -45,23 +39,23 @@ const totalProgress = computed(() => {
</li>
</ul>
</div>
<div flex items-center gap-2>
<div flex items-center gap-2 text-sm>
<div v-if="totalProgress < 100" i-svg-spinners:pulse-ring />
<div v-else i-solar:check-circle-bold />
<div v-else i-solar:check-circle-bold-duotone />
<div>
Preparing external resources...
</div>
</div>
<div w-full flex flex-col gap-2>
<div v-if="props.progressInfo.size > 0" w-full flex flex-col gap-2>
<div
v-for="file in Array.from(progressInfo.values())"
:key="file.value.filename"
max-w-full flex flex-col gap="1 sm:2"
>
<div flex justify-between text="xs sm:sm neutral-500 dark:neutral-400">
<div flex justify-between text="xs sm:sm neutral-600 dark:neutral-400">
<div flex items-center gap-1>
<div v-if="file.value.progress < 100" i-svg-spinners:pulse-ring />
<div v-else i-solar:check-circle-bold text="green-600 dark:green-400" />
<div v-else i-solar:check-circle-bold-duotone text="green-600 dark:green-400" />
<div>
{{ file.value.filename }}
</div>
@@ -73,7 +67,7 @@ const totalProgress = computed(() => {
</div>
<div relative>
<div
bg="primary-300 dark:primary-400"
bg="primary-300 dark:primary-300/50"
absolute h-4 rounded-md will-change-width
:style="{ width: `${file.value.progress}%` }"
transition="width duration-500 ease-in-out"
@@ -1,12 +1,57 @@
<script setup lang="ts">
import { TransitionVertical } from '@proj-airi/ui'
import { storeToRefs } from 'pinia'
import { TooltipContent, TooltipPortal, TooltipProvider, TooltipRoot, TooltipTrigger } from 'reka-ui'
import { ref } from 'vue'
import { useResourcesStore } from '../../../stores/resources'
import LoadingProgress from './LoadingProgress.vue'
const { resources } = storeToRefs(useResourcesStore())
const loadingProgressOpen = ref(false)
function handleClick() {
loadingProgressOpen.value = !loadingProgressOpen.value
}
</script>
<template>
<LoadingProgress :progress-info="resources" />
<div fixed left-0 top-3 w-full flex flex-col items-center>
<TooltipProvider :delay-duration="150">
<TooltipRoot :open="loadingProgressOpen" disable-closing-trigger @update:open="(state) => loadingProgressOpen = state">
<TooltipTrigger>
<div
w="fit"
bg="white/80 dark:neutral-900/80"
mb-1 flex cursor-pointer items-center gap-2 rounded-full px-2 py-1 text-sm shadow-md backdrop-blur-md
@click="handleClick"
>
<div i-svg-spinners:pulse-ring pointer-events-none />
<div pointer-events-none select-none pr-2>
Downloading...
</div>
</div>
</TooltipTrigger>
<TooltipPortal>
<TransitionVertical>
<TooltipContent class="resource-status-island-tooltip">
<LoadingProgress
w="[calc(100%-1.5rem)]" max-w="500px sm:600px md:700px"
bg="white/80 dark:neutral-900/80"
mx-3 rounded-xl p-3 shadow-md backdrop-blur-md will-change-transform
:progress-info="resources"
/>
</TooltipContent>
</TransitionVertical>
</TooltipPortal>
</TooltipRoot>
</TooltipProvider>
</div>
</template>
<style>
[data-reka-popper-content-wrapper=""]:has(.resource-status-island-tooltip) {
z-index: 1000 !important;
}
</style>
@@ -0,0 +1,85 @@
import type { EventCallback, EventName, UnlistenFn } from '@tauri-apps/api/event'
import { withRetry } from '@moeru/std'
import { computedAsync, until } from '@vueuse/core'
async function untilNoError<T>(fn: () => Promise<T>, onError?: (err?: unknown | null) => void): Promise<T> {
const fnRetry = withRetry(fn, { retryDelay: 5000, retry: Number.MAX_SAFE_INTEGER - 2, onError })
return await fnRetry()
}
async function untilImported<T>(fn: () => Promise<T>, onError?: (err?: unknown | null) => void): Promise<T> {
return await untilNoError(fn, onError)
}
export interface Point {
x: number
y: number
}
export interface Size {
width: number
height: number
}
export interface WindowFrame {
origin: Point
size: Size
}
interface Events {
'tauri://resize': unknown
'tauri://move': unknown
'tauri://close-requested': unknown
'tauri://destroyed': unknown
'tauri://focus': unknown
'tauri://blur': unknown
'tauri://scale-change': unknown
'tauri://theme-changed': unknown
'tauri://window-created': unknown
'tauri://webview-created': unknown
'tauri://drag-enter': unknown
'tauri://drag-over': unknown
'tauri://drag-drop': unknown
'tauri://drag-leave': unknown
}
export interface AiriTamagotchiEvents extends Events {
'tauri-app:window-click-through:position-cursor-and-window-frame': [Point, WindowFrame]
'tauri-app:model-load-progress': [string, number]
'mcp_plugin_destroyed': undefined
}
export type Event<E extends { [key: string]: any }> = {
[K in keyof E]: E[K];
}[keyof E]
export function useTauriEvent<ES = Events>() {
const tauriEventApi = computedAsync(() => untilImported(() => import('@tauri-apps/api/event'), console.warn))
async function _listen<E extends keyof ES>(event: E, callback: EventCallback<ES[E]>) {
await until(tauriEventApi).toBeTruthy()
const imported = await tauriEventApi.value
if (!imported) {
throw new Error('Tauri event API not available')
}
return untilNoError(() => imported.listen(event as EventName, callback), console.warn)
}
async function listen<E extends keyof ES>(event: E, callback: EventCallback<ES[E]>) {
let cleanupListener: UnlistenFn | undefined
_listen(event, callback).then((listener) => {
cleanupListener = listener
})
return () => {
cleanupListener?.()
}
}
return {
listen,
}
}
@@ -117,7 +117,9 @@ const routeHeaderMetadataMap = computed(() => {
})
// const activeSettingsTutorial = ref('default')
const routeHeaderMetadata = computed(() => routeHeaderMetadataMap.value[route.path])
const routeHeaderMetadata = computed(() => {
return routeHeaderMetadataMap.value[route.path] || routeHeaderMetadataMap.value[`${route.path}/`]
})
</script>
<template>
+13 -22
View File
@@ -1,26 +1,29 @@
<script setup lang="ts">
import type { AiriTamagotchiEvents, Point, WindowFrame } from '../composables/tauri'
import { WidgetStage } from '@proj-airi/stage-ui/components'
import { useMcpStore } from '@proj-airi/stage-ui/stores'
import { connectServer } from '@proj-airi/tauri-plugin-mcp'
import { invoke } from '@tauri-apps/api/core'
import { listen } from '@tauri-apps/api/event'
import { storeToRefs } from 'pinia'
import { computed, onMounted, onUnmounted, ref } from 'vue'
import ResourceStatusIsland from '../components/Widgets/ResourceStatusIsland/index.vue'
import { useAppRuntime } from '../composables/runtime'
import { useTauriEvent } from '../composables/tauri'
import { useWindowShortcuts } from '../composables/window-shortcuts'
import { useResourcesStore } from '../stores/resources'
import { useWindowControlStore } from '../stores/window-controls'
import { WindowControlMode } from '../types/window-controls'
import { startClickThrough, stopClickThrough } from '../utils/windows'
useWindowShortcuts()
const { platform } = useAppRuntime()
const windowStore = useWindowControlStore()
useWindowShortcuts()
const isCursorInside = ref(false)
const mcpStore = useMcpStore()
const { listen } = useTauriEvent<AiriTamagotchiEvents>()
const isCursorInside = ref(false)
const { connected, serverCmd, serverArgs } = storeToRefs(mcpStore)
const modeIndicatorClass = computed(() => {
@@ -62,21 +65,6 @@ const shouldHideView = computed(() => {
const live2dFocusAt = ref<Point>({ x: window.innerWidth / 2, y: window.innerHeight / 2 })
interface Point {
x: number
y: number
}
interface Size {
width: number
height: number
}
interface WindowFrame {
origin: Point
size: Size
}
function onTauriPositionCursorAndWindowFrameEvent(event: { payload: [Point, WindowFrame] }) {
const [mouseLocation, windowFrame] = event.payload
isCursorInside.value = mouseLocation.x >= windowFrame.origin.x && mouseLocation.x <= windowFrame.origin.x + windowFrame.size.width && mouseLocation.y >= windowFrame.origin.y && mouseLocation.y <= windowFrame.origin.y + windowFrame.size.height
@@ -195,9 +183,12 @@ if (import.meta.hot) { // For better DX
data-tauri-drag-region
class="drag-region absolute left-0 top-0 z-999 h-full w-full flex items-center justify-center overflow-hidden"
>
<div class="absolute h-32 w-full flex items-center justify-center b-2 b-primary bg-white">
<div
class="absolute h-32 w-full flex items-center justify-center overflow-hidden rounded-xl"
bg="white/85 dark:neutral-900/95" backdrop-blur="md"
>
<div class="wall absolute top-0 h-8" />
<div data-tauri-drag-region class="absolute left-0 top-0 h-full w-full flex animate-flash animate-duration-5s animate-count-infinite items-center justify-center text-1.5rem text-primary-300 font-normal">
<div data-tauri-drag-region class="absolute left-0 top-0 h-full w-full flex animate-flash animate-duration-5s animate-count-infinite items-center justify-center text-1.5rem text-primary-400 font-normal">
DRAG HERE TO MOVE
</div>
<div data-tauri-drag-region class="wall absolute bottom-0 h-8" />
@@ -247,7 +238,7 @@ if (import.meta.hot) { // For better DX
}
.wall {
--at-apply: text-primary-200;
--at-apply: text-primary-300;
--wall-width: 8px;
animation: wall-move 1s linear infinite;
@@ -20,6 +20,13 @@ html {
transition: all 0.3s ease-in-out;
}
.dark {
--bg-color-light: rgb(255 255 255);
--bg-color-dark: rgb(18 18 18);
--progress-bar-color: rgb(244 114 182);
--bg-color: var(--bg-color-dark);
}
html.dark {
color-scheme: dark;
}
+1
View File
@@ -186,6 +186,7 @@ words:
- turborepo
- unbundle
- unhead
- Unlisten
- unocss
- unplugin
- unspeech
+6
View File
@@ -270,6 +270,9 @@ importers:
'@huggingface/transformers':
specifier: ^3.5.2
version: 3.5.2
'@moeru/std':
specifier: 'catalog:'
version: 0.1.0-beta.4
'@pixiv/three-vrm':
specifier: ^3.4.1
version: 3.4.1(three@0.177.0)
@@ -387,6 +390,9 @@ importers:
rehype-stringify:
specifier: ^10.0.1
version: 10.0.1
reka-ui:
specifier: ^2.3.1
version: 2.3.1(typescript@5.8.3)(vue@3.5.17(typescript@5.8.3))
remark-parse:
specifier: ^11.0.0
version: 11.0.0