feat(stage-tamagotchi): loading status
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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')
|
||||
|
||||
+10
-16
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -186,6 +186,7 @@ words:
|
||||
- turborepo
|
||||
- unbundle
|
||||
- unhead
|
||||
- Unlisten
|
||||
- unocss
|
||||
- unplugin
|
||||
- unspeech
|
||||
|
||||
Generated
+6
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user