feat(stage-tamagotchi): WindowLink for opening window in Tauri

This commit is contained in:
Neko Ayaka
2025-06-29 04:39:52 +08:00
parent 94048b0f71
commit 6e7e9d02d8
11 changed files with 101 additions and 34 deletions
Generated
+1
View File
@@ -206,6 +206,7 @@ dependencies = [
"tauri-plugin-prevent-default",
"tokenizers",
"tokio",
"url",
"windows 0.61.3",
]
@@ -47,6 +47,7 @@ rubato = "0.16.2"
byteorder = "1.5.0"
clap = { version = "4.5.40", features = ["derive"] }
tokenizers = "0.21.2"
url = "2.5.4"
[target.'cfg(target_os = "macos")'.dependencies]
objc2 = "0.6.1"
@@ -4,7 +4,7 @@ use std::path::Path;
use tauri::TitleBarStyle;
use tauri::{WebviewUrl, WebviewWindowBuilder};
pub fn new_chat_window(app: &tauri::AppHandle) -> Result<(), tauri::Error> {
pub fn new_chat_window(app: &tauri::AppHandle) -> Result<tauri::WebviewWindow, tauri::Error> {
let mut builder = WebviewWindowBuilder::new(
app,
"chat",
@@ -28,6 +28,5 @@ pub fn new_chat_window(app: &tauri::AppHandle) -> Result<(), tauri::Error> {
builder = builder.traffic_light_position(tauri::LogicalPosition::new(14.0, 20.0));
}
builder.build()?;
Ok(())
return builder.build();
}
@@ -4,7 +4,7 @@ use std::path::Path;
use tauri::TitleBarStyle;
use tauri::{WebviewUrl, WebviewWindowBuilder};
pub fn new_settings_window(app: &tauri::AppHandle) -> Result<(), tauri::Error> {
pub fn new_settings_window(app: &tauri::AppHandle) -> Result<tauri::WebviewWindow, tauri::Error> {
let mut builder = WebviewWindowBuilder::new(
app,
"settings",
@@ -28,6 +28,5 @@ pub fn new_settings_window(app: &tauri::AppHandle) -> Result<(), tauri::Error> {
builder = builder.traffic_light_position(tauri::LogicalPosition::new(14.0, 20.0));
}
builder.build()?;
Ok(())
return builder.build();
}
+38 -1
View File
@@ -1,4 +1,4 @@
use std::{sync::atomic::Ordering, time::Duration};
use std::{str::FromStr, sync::atomic::Ordering, time::Duration};
use log::info;
use tauri::{
@@ -126,6 +126,42 @@ async fn load_models(window: tauri::Window) -> Result<(), String> {
Ok(())
}
#[tauri::command]
async fn open_route_in_window(
window: tauri::Window,
route: String,
window_label: String,
) -> Result<(), String> {
let app = window.app_handle();
let target_window = match window_label.as_str() {
"chat" => match app.get_webview_window("chat") {
Some(window) => window,
None => app_windows::chat::new_chat_window(app)
.map_err(|e| format!("Failed to create chat window: {}", e))?,
},
"settings" => match app.get_webview_window("settings") {
Some(window) => window,
None => app_windows::settings::new_settings_window(app)
.map_err(|e| format!("Failed to create settings window: {}", e))?,
},
_ => {
return Err(format!("Unknown window label: {}", window_label));
},
};
let mut current_url = target_window
.url()
.map_err(|e| format!("Failed to get current URL: {}", e))?;
let route: String = "/".to_string() + route.trim_start_matches('/');
current_url.set_fragment(Some(route.to_string().as_str()));
let _ = target_window.show();
let _ = target_window.navigate(current_url);
Ok(())
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
#[allow(clippy::missing_panics_doc)]
pub fn run() {
@@ -238,6 +274,7 @@ pub fn run() {
start_click_through,
stop_click_through,
load_models,
open_route_in_window,
])
.build(tauri::generate_context!())
.expect("error while building tauri application")
@@ -0,0 +1,23 @@
<script setup lang="ts">
import { useTauriCore } from '../../composables/tauri'
const props = defineProps<{
to: string
label?: string
}>()
const { invoke } = useTauriCore()
function handleClick() {
invoke('open_route_in_window', {
route: props.to,
windowLabel: props.label,
})
}
</script>
<template>
<a cursor-pointer @click="handleClick">
<slot />
</a>
</template>
@@ -3,6 +3,8 @@ import type { ProgressInfo } from '../../../stores/resources'
import { computed } from 'vue'
import WindowLink from '../../Tauri/WindowLink.vue'
const props = defineProps<{
progressInfo: ProgressInfo
}>()
@@ -25,7 +27,7 @@ const totalProgress = computed(() => {
</div>
<ul ml-4 mt-3>
<li>
<RouterLink to="/settings/modules/hearing">
<WindowLink to="/settings/modules/hearing" label="settings">
<div flex items-center gap-1>
<div flex items-center gap-1>
<div i-solar:microphone-3-bold-duotone />
@@ -35,7 +37,7 @@ const totalProgress = computed(() => {
due to loading inference models...
</div>
</div>
</RouterLink>
</WindowLink>
</li>
</ul>
</div>
@@ -1,5 +1,5 @@
import { computedAsync } from '@vueuse/core'
import { onMounted, ref } from 'vue'
import { computed, ref } from 'vue'
async function getTauri() {
try {
@@ -11,27 +11,25 @@ async function getTauri() {
}
}
async function getTauriOSPluginInternal() {
const os = await import('@tauri-apps/plugin-os')
return os
}
export function useAppRuntime() {
const isTauri = ref<boolean>(false)
const isInitialized = ref(false)
const platform = computedAsync(async () => {
if (!isTauri.value) {
return 'web'
const res = (await getTauri())?.platform?.() || 'web'
if (!isInitialized.value) {
isInitialized.value = true
}
return (await getTauriOSPluginInternal())?.platform?.() || 'web'
})
return res
}, 'web')
onMounted(async () => {
isTauri.value = (await getTauri()) != null
const isTauri = computed(() => {
return platform.value !== 'web'
})
return {
platform,
isInitialized,
isTauri,
}
}
+16 -7
View File
@@ -7,7 +7,7 @@ import { computedAsync, until } from '@vueuse/core'
import { useAppRuntime } from './runtime'
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 })
const fnRetry = withRetry(fn, { retryDelay: 5000, retry: 5, onError })
return await fnRetry()
}
@@ -54,7 +54,7 @@ export interface AiriTamagotchiEvents extends Events {
}
export function useTauriEvent<ES = Events>() {
const { platform } = useAppRuntime()
const { platform, isInitialized } = useAppRuntime()
const tauriEventApi = computedAsync(() => {
if (platform.value !== 'web') {
@@ -63,6 +63,8 @@ export function useTauriEvent<ES = Events>() {
})
async function _listen<E extends keyof ES>(event: E, callback: EventCallback<ES[E]>) {
await until(isInitialized).toBeTruthy()
if (platform.value === 'web') {
return () => {}
}
@@ -106,6 +108,13 @@ export interface InvokeMethods {
load_models: { args: undefined, options: undefined, returns: void }
stop_click_through: { args: undefined, options: undefined, returns: void }
start_click_through: { args: undefined, options: undefined, returns: void }
// WindowLink.vue
open_route_in_window: {
args: { route: string, windowLabel?: string } | undefined
options: undefined
returns: void
}
}
interface InvokeMethodShape {
@@ -115,7 +124,7 @@ interface InvokeMethodShape {
}
export function useTauriCore<IM extends Record<keyof IM, InvokeMethodShape> = InvokeMethods>() {
const { platform } = useAppRuntime()
const { platform, isInitialized } = useAppRuntime()
const tauriCoreApi = computedAsync(() => {
if (platform.value !== 'web') {
@@ -123,15 +132,15 @@ export function useTauriCore<IM extends Record<keyof IM, InvokeMethodShape> = In
}
})
async function invoke<
C extends keyof IM,
>(
async function invoke<C extends keyof IM>(
command: C,
args?: IM[C]['args'],
options?: IM[C]['options'],
): Promise<IM[C]['returns'] | undefined> {
await until(isInitialized).toBeTruthy()
if (platform.value === 'web') {
console.warn(`Attempted to invoke Tauri command "${String(command)}" in web platform, however, currently we are not in a Tauri environment.`)
console.warn(`Attempted to invoke Tauri command "${String(command)}" in web platform`)
return
}
+2 -2
View File
@@ -4,12 +4,13 @@ import type { AiriTamagotchiEvents, Point, WindowFrame } from '../composables/ta
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 { storeToRefs } from 'pinia'
import { computed, onMounted, onUnmounted, ref } from 'vue'
import ResourceStatusIsland from '../components/Widgets/ResourceStatusIsland/index.vue'
import { useAppRuntime } from '../composables/runtime'
import { useTauriCore, useTauriEvent } from '../composables/tauri'
import { useTauriEvent } from '../composables/tauri'
import { useWindowShortcuts } from '../composables/window-shortcuts'
import { useResourcesStore } from '../stores/resources'
import { useWindowControlStore } from '../stores/window-controls'
@@ -21,7 +22,6 @@ const { platform } = useAppRuntime()
const windowStore = useWindowControlStore()
const mcpStore = useMcpStore()
const { listen } = useTauriEvent<AiriTamagotchiEvents>()
const { invoke } = useTauriCore()
const isCursorInside = ref(false)
const { connected, serverCmd, serverArgs } = storeToRefs(mcpStore)
+1 -3
View File
@@ -1,11 +1,9 @@
import { useTauriCore } from '../composables/tauri'
import { invoke } from '@tauri-apps/api/core'
export async function startClickThrough() {
const { invoke } = useTauriCore()
await invoke('start_click_through')
}
export async function stopClickThrough() {
const { invoke } = useTauriCore()
await invoke('stop_click_through')
}