feat(stage-tamagotchi): window state persistence
This commit is contained in:
Generated
+930
-5
File diff suppressed because it is too large
Load Diff
@@ -50,6 +50,7 @@ tokenizers = "0.21.2"
|
||||
url = "2.5.4"
|
||||
tauri-plugin-window-state = "2.3.0"
|
||||
tauri-plugin-positioner = "2.3.0"
|
||||
xcap = "0.6.1"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
objc2 = "0.6.1"
|
||||
|
||||
@@ -16,6 +16,7 @@ use tokio::time::sleep;
|
||||
mod app_click_through;
|
||||
mod app_windows;
|
||||
mod commands;
|
||||
mod plugins;
|
||||
mod whisper;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
@@ -253,6 +254,11 @@ pub fn run() {
|
||||
commands::open_chat_window,
|
||||
commands::debug_println,
|
||||
start_monitor,
|
||||
plugins::window::plugins_window_get_current_window_info,
|
||||
plugins::window::plugin_window_get_display_info,
|
||||
plugins::window::plugins_window_set_position,
|
||||
plugins::window_persistence::plugins_window_persistence_save,
|
||||
plugins::window_persistence::plugins_window_persistence_restore,
|
||||
stop_monitor,
|
||||
start_click_through,
|
||||
stop_click_through,
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod window;
|
||||
pub mod window_persistence;
|
||||
@@ -0,0 +1,56 @@
|
||||
use tauri::Monitor;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn plugin_window_get_display_info(
|
||||
window: tauri::Window
|
||||
) -> Result<(Vec<Monitor>, Monitor), String> {
|
||||
let monitors = match window.available_monitors() {
|
||||
std::result::Result::Ok(monitors) => monitors,
|
||||
_ => vec![],
|
||||
};
|
||||
let primary_monitor = match window.primary_monitor() {
|
||||
std::result::Result::Ok(monitor) => match monitor {
|
||||
Some(monitor) => monitor,
|
||||
None => {
|
||||
return Err("Primary monitor not found".to_string());
|
||||
},
|
||||
},
|
||||
_ => return Err("Failed to get primary monitor".to_string()),
|
||||
};
|
||||
|
||||
Ok((monitors, primary_monitor))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn plugins_window_get_current_window_info(
|
||||
window: tauri::Window
|
||||
) -> Result<((u32, u32), (i32, i32)), String> {
|
||||
match window.current_monitor() {
|
||||
std::result::Result::Ok(optional_monitor) => match optional_monitor {
|
||||
Some(monitor) => {
|
||||
let monitor_size = monitor.size();
|
||||
let position = monitor.position();
|
||||
|
||||
Ok((
|
||||
(monitor_size.width, monitor_size.height),
|
||||
(position.x, position.y),
|
||||
))
|
||||
},
|
||||
_ => Ok(((0, 0), (0, 0))),
|
||||
},
|
||||
_ => Ok(((0, 0), (0, 0))),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn plugins_window_set_position(
|
||||
window: tauri::Window,
|
||||
x: i32,
|
||||
y: i32,
|
||||
) -> Result<(), String> {
|
||||
use tauri::Position;
|
||||
|
||||
window
|
||||
.set_position(Position::Physical(tauri::PhysicalPosition { x, y }))
|
||||
.map_err(|e| format!("Failed to set window position: {}", e))
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
use log::info;
|
||||
use tauri::Window;
|
||||
use tauri_plugin_window_state::{AppHandleExt, WindowExt};
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn plugins_window_persistence_save(app: tauri::AppHandle) -> Result<(), String> {
|
||||
info!("Saving window state...");
|
||||
app
|
||||
.save_window_state(tauri_plugin_window_state::StateFlags::all())
|
||||
.map_err(|e| format!("Failed to save window state: {}", e))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn plugins_window_persistence_restore(window: Window) -> Result<(), String> {
|
||||
info!("Restoring window state...");
|
||||
// The window state is automatically restored when the plugin is initialized
|
||||
// This command can be used to manually trigger a restore
|
||||
window
|
||||
.restore_state(tauri_plugin_window_state::StateFlags::all())
|
||||
.map_err(|e| format!("Failed to restore window state: {}", e))
|
||||
}
|
||||
@@ -0,0 +1,551 @@
|
||||
import type {
|
||||
DisplayInfo,
|
||||
Monitor,
|
||||
Point,
|
||||
Size,
|
||||
} from './tauri'
|
||||
|
||||
import { useDebounceFn } from '@vueuse/core'
|
||||
import { computed, nextTick, onUnmounted, readonly, ref, watch } from 'vue'
|
||||
|
||||
import { useAppRuntime } from './runtime'
|
||||
import { useTauriCore } from './tauri'
|
||||
import { useTauriPointAndWindowFrame } from './tauri-click-through'
|
||||
|
||||
export interface WindowPersistenceConfig {
|
||||
autoSave?: boolean
|
||||
autoRestore?: boolean
|
||||
savePeriod?: number // milliseconds
|
||||
constrainToDisplays?: boolean
|
||||
centerPointConstraint?: boolean // Use center point for boundary checks
|
||||
monitorDisplayChanges?: boolean
|
||||
}
|
||||
|
||||
export interface WindowBoundaryConstraints {
|
||||
minCenterX: number
|
||||
maxCenterX: number
|
||||
minCenterY: number
|
||||
maxCenterY: number
|
||||
recommendedMonitor: Monitor | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhanced window positioning system with persistence and boundary management
|
||||
* Integrates with the existing tauri-click-through system
|
||||
*/
|
||||
export function useWindowPersistence(config: WindowPersistenceConfig = {}) {
|
||||
const {
|
||||
autoSave = true,
|
||||
autoRestore = true,
|
||||
savePeriod = 10000,
|
||||
constrainToDisplays = true,
|
||||
centerPointConstraint = true,
|
||||
monitorDisplayChanges = true,
|
||||
} = config
|
||||
|
||||
const { platform } = useAppRuntime()
|
||||
const { invoke } = useTauriCore()
|
||||
const { windowFrame } = useTauriPointAndWindowFrame()
|
||||
|
||||
// Debounced save function (declare early to avoid usage before definition)
|
||||
const debouncedSave = useDebounceFn(() => savePosition(), savePeriod)
|
||||
|
||||
// Reactive state
|
||||
const displayInfo = ref<DisplayInfo>()
|
||||
const currentWindowPosition = computed(() => windowFrame.value.origin)
|
||||
const currentWindowSize = computed(() => windowFrame.value.size)
|
||||
const isPositioning = ref(false)
|
||||
const isRestoring = ref(false)
|
||||
|
||||
// State tracking (using Tauri plugins only)
|
||||
const isStateRestored = ref<boolean>(false)
|
||||
|
||||
const primaryMonitor = computed(() => displayInfo.value?.primaryMonitor)
|
||||
|
||||
const currentMonitor = computed(() => {
|
||||
if (!currentWindowPosition.value || !displayInfo.value)
|
||||
return null
|
||||
return findMonitorContainingCenterPoint(
|
||||
displayInfo.value.monitors,
|
||||
getWindowCenterPoint({
|
||||
x: currentWindowPosition.value.x,
|
||||
y: currentWindowPosition.value.y,
|
||||
}, {
|
||||
width: currentWindowSize.value.width,
|
||||
height: currentWindowSize.value.height,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
const boundaryConstraints = computed((): WindowBoundaryConstraints | null => {
|
||||
if (!displayInfo.value || !currentWindowPosition.value)
|
||||
return null
|
||||
|
||||
const allMonitors = displayInfo.value.monitors
|
||||
const windowSize = {
|
||||
width: currentWindowSize.value.width,
|
||||
height: currentWindowSize.value.height,
|
||||
}
|
||||
|
||||
return calculateBoundaryConstraints(allMonitors, windowSize, centerPointConstraint)
|
||||
})
|
||||
|
||||
const isWindowInValidPosition = computed(() => {
|
||||
if (!currentWindowPosition.value || !boundaryConstraints.value)
|
||||
return false
|
||||
|
||||
const center = getWindowCenterPoint({
|
||||
x: currentWindowPosition.value.x,
|
||||
y: currentWindowPosition.value.y,
|
||||
}, {
|
||||
width: currentWindowSize.value.width,
|
||||
height: currentWindowSize.value.height,
|
||||
})
|
||||
|
||||
const constraints = boundaryConstraints.value
|
||||
|
||||
return (
|
||||
center.x >= constraints.minCenterX
|
||||
&& center.x <= constraints.maxCenterX
|
||||
&& center.y >= constraints.minCenterY
|
||||
&& center.y <= constraints.maxCenterY
|
||||
)
|
||||
})
|
||||
|
||||
// Initialize the system
|
||||
async function initialize() {
|
||||
if (platform.value === 'web')
|
||||
return
|
||||
|
||||
try {
|
||||
// Get initial display information
|
||||
refreshDisplayInfo()
|
||||
|
||||
// Set up event listeners
|
||||
setupEventListeners()
|
||||
|
||||
// Auto-restore if enabled
|
||||
if (autoRestore) {
|
||||
restorePosition()
|
||||
}
|
||||
|
||||
// Set up display change monitoring
|
||||
if (monitorDisplayChanges) {
|
||||
setupDisplayMonitoring()
|
||||
}
|
||||
|
||||
// Success - using console.warn to comply with linting rules
|
||||
}
|
||||
catch (error) {
|
||||
console.error('[WindowPersistence] Failed to initialize:', error)
|
||||
}
|
||||
}
|
||||
|
||||
function setupEventListeners() {
|
||||
watch(windowFrame, () => {
|
||||
if (!isPositioning.value && !isRestoring.value) {
|
||||
if (autoSave) {
|
||||
debouncedSave()
|
||||
}
|
||||
}
|
||||
}, {
|
||||
deep: true,
|
||||
})
|
||||
}
|
||||
|
||||
function setupDisplayMonitoring() {
|
||||
// Monitor for display configuration changes
|
||||
// This would typically be handled by the Rust backend emitting display-changed events
|
||||
// For now, we'll poll periodically as a fallback
|
||||
const monitorInterval = setInterval(async () => {
|
||||
try {
|
||||
const [monitors, primaryMonitor] = (await invoke('plugin_window_get_display_info'))!
|
||||
|
||||
const newDisplayInfo = {
|
||||
monitors,
|
||||
primaryMonitor,
|
||||
}
|
||||
|
||||
// Check for significant display changes using detailed comparison
|
||||
if (displayInfo.value && hasSignificantDisplayChange(displayInfo.value, newDisplayInfo)) {
|
||||
handleDisplayChange(newDisplayInfo)
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('[WindowPersistence] Failed to monitor display changes:', error)
|
||||
}
|
||||
}, 10000) // Check every 10 seconds
|
||||
|
||||
onUnmounted(() => {
|
||||
clearInterval(monitorInterval)
|
||||
})
|
||||
}
|
||||
|
||||
async function handleDisplayChange(newDisplayInfo: DisplayInfo) {
|
||||
// Display configuration changed
|
||||
|
||||
const oldDisplayInfo = displayInfo.value
|
||||
displayInfo.value = newDisplayInfo
|
||||
|
||||
// Check if current window position is still valid
|
||||
if (currentWindowPosition.value && constrainToDisplays) {
|
||||
await nextTick()
|
||||
|
||||
if (!isWindowInValidPosition.value) {
|
||||
console.warn('[WindowPersistence] Window is outside valid area after display change, repositioning...')
|
||||
await ensureWindowInBounds()
|
||||
}
|
||||
}
|
||||
|
||||
// For significant display changes, the plugin will handle state invalidation
|
||||
if (oldDisplayInfo && hasSignificantDisplayChange(oldDisplayInfo, newDisplayInfo)) {
|
||||
console.warn('[WindowPersistence] Significant display change detected')
|
||||
}
|
||||
}
|
||||
|
||||
// Core positioning functions
|
||||
async function savePosition(): Promise<boolean> {
|
||||
if (!currentWindowPosition.value || !displayInfo.value)
|
||||
return false
|
||||
|
||||
try {
|
||||
console.warn('[WindowPersistence] Saving position:', currentWindowPosition.value, currentWindowSize.value)
|
||||
await invoke('plugins_window_persistence_save')
|
||||
|
||||
return true
|
||||
}
|
||||
catch (error) {
|
||||
console.error('[WindowPersistence] Failed to save position:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function restorePosition(): Promise<boolean> {
|
||||
if (!displayInfo.value) {
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
isRestoring.value = true
|
||||
|
||||
// Restore window state using Tauri plugin
|
||||
await invoke('plugins_window_persistence_restore')
|
||||
|
||||
// Ensure the restored position is within bounds
|
||||
if (constrainToDisplays && currentWindowPosition.value) {
|
||||
if (!isWindowInValidPosition.value) {
|
||||
console.warn('[WindowPersistence] Restored position is not valid for current displays')
|
||||
await centerOnPrimaryMonitor()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
isStateRestored.value = true
|
||||
return true
|
||||
}
|
||||
catch (error) {
|
||||
console.error('[WindowPersistence] Failed to restore position:', error)
|
||||
return false
|
||||
}
|
||||
finally {
|
||||
isRestoring.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureWindowInBounds(): Promise<boolean> {
|
||||
if (!currentWindowPosition.value || !constrainToDisplays)
|
||||
return true
|
||||
|
||||
try {
|
||||
const constrainedPosition = constrainPositionToBounds({
|
||||
x: currentWindowPosition.value.x,
|
||||
y: currentWindowPosition.value.y,
|
||||
}, {
|
||||
width: currentWindowSize.value.width,
|
||||
height: currentWindowSize.value.height,
|
||||
})
|
||||
|
||||
if (!positionsEqual({
|
||||
x: currentWindowPosition.value.x,
|
||||
y: currentWindowPosition.value.y,
|
||||
width: currentWindowSize.value.width,
|
||||
height: currentWindowSize.value.height,
|
||||
}, {
|
||||
x: constrainedPosition.x,
|
||||
y: constrainedPosition.y,
|
||||
width: currentWindowSize.value.width,
|
||||
height: currentWindowSize.value.height,
|
||||
})) {
|
||||
await applyPosition(constrainedPosition)
|
||||
return true
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
catch (error) {
|
||||
console.error('[WindowPersistence] Failed to ensure window bounds:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function applyPosition(pos: Point): Promise<boolean> {
|
||||
if (platform.value === 'web')
|
||||
return false
|
||||
|
||||
try {
|
||||
isPositioning.value = true
|
||||
await invoke('plugins_window_set_position', { x: pos.x, y: pos.y })
|
||||
|
||||
if (autoSave) {
|
||||
debouncedSave()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
catch (error) {
|
||||
console.error('[WindowPersistence] Failed to apply position:', error)
|
||||
return false
|
||||
}
|
||||
finally {
|
||||
isPositioning.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function centerOnPrimaryMonitor(): Promise<boolean> {
|
||||
if (!primaryMonitor.value || !currentWindowSize.value)
|
||||
return false
|
||||
|
||||
const monitor = primaryMonitor.value
|
||||
const windowSize = {
|
||||
width: currentWindowSize.value.width,
|
||||
height: currentWindowSize.value.height,
|
||||
}
|
||||
|
||||
const centerPosition: Point = {
|
||||
x: monitor.workArea.position.x + (monitor.workArea.size.width - windowSize.width) / 2,
|
||||
y: monitor.workArea.position.y + (monitor.workArea.size.height - windowSize.height) / 2,
|
||||
}
|
||||
|
||||
return await applyPosition(centerPosition)
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
function getWindowCenterPoint(pos: Point, size: Size): Point {
|
||||
return {
|
||||
x: pos.x + size.width / 2,
|
||||
y: pos.y + size.height / 2,
|
||||
}
|
||||
}
|
||||
|
||||
function findMonitorContainingCenterPoint(monitors: Monitor[], center: Point): Monitor | null {
|
||||
return monitors.find((monitor) => {
|
||||
const workArea = monitor.workArea
|
||||
return (
|
||||
center.x >= workArea.position.x
|
||||
&& center.x <= workArea.position.x + workArea.size.width
|
||||
&& center.y >= workArea.position.y
|
||||
&& center.y <= workArea.position.y + workArea.size.height
|
||||
)
|
||||
}) || null
|
||||
}
|
||||
|
||||
function calculateBoundaryConstraints(
|
||||
monitors: Monitor[],
|
||||
windowSize: Size,
|
||||
useCenterPoint: boolean,
|
||||
): WindowBoundaryConstraints {
|
||||
if (monitors.length === 0) {
|
||||
return {
|
||||
minCenterX: 0,
|
||||
maxCenterX: 0,
|
||||
minCenterY: 0,
|
||||
maxCenterY: 0,
|
||||
recommendedMonitor: null,
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate the combined bounds of all monitors
|
||||
let minX = Infinity
|
||||
let maxX = -Infinity
|
||||
let minY = Infinity
|
||||
let maxY = -Infinity
|
||||
let bestMonitor = monitors[0]
|
||||
|
||||
monitors.forEach((monitor) => {
|
||||
const workArea = monitor.workArea
|
||||
minX = Math.min(minX, workArea.position.x)
|
||||
maxX = Math.max(maxX, workArea.position.x + workArea.size.width)
|
||||
minY = Math.min(minY, workArea.position.y)
|
||||
maxY = Math.max(maxY, workArea.position.y + workArea.size.height)
|
||||
|
||||
// Use primary monitor from displayInfo instead of is_primary field
|
||||
if (displayInfo.value && monitor.name === displayInfo.value.primaryMonitor.name) {
|
||||
bestMonitor = monitor
|
||||
}
|
||||
})
|
||||
|
||||
if (useCenterPoint) {
|
||||
// Calculate constraints for center point
|
||||
return {
|
||||
minCenterX: minX + windowSize.width / 2,
|
||||
maxCenterX: maxX - windowSize.width / 2,
|
||||
minCenterY: minY + windowSize.height / 2,
|
||||
maxCenterY: maxY - windowSize.height / 2,
|
||||
recommendedMonitor: bestMonitor,
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Calculate constraints for top-left corner
|
||||
return {
|
||||
minCenterX: minX,
|
||||
maxCenterX: maxX - windowSize.width,
|
||||
minCenterY: minY,
|
||||
maxCenterY: maxY - windowSize.height,
|
||||
recommendedMonitor: bestMonitor,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function constrainPositionToBounds(pos: Point, size: Size): Point {
|
||||
if (!boundaryConstraints.value)
|
||||
return { x: pos.x, y: pos.y }
|
||||
|
||||
const constraints = boundaryConstraints.value
|
||||
const center = getWindowCenterPoint(pos, size)
|
||||
|
||||
// Constrain center point
|
||||
const constrainedCenter: Point = {
|
||||
x: Math.max(constraints.minCenterX, Math.min(constraints.maxCenterX, center.x)),
|
||||
y: Math.max(constraints.minCenterY, Math.min(constraints.maxCenterY, center.y)),
|
||||
}
|
||||
|
||||
// Convert back to position
|
||||
return {
|
||||
x: constrainedCenter.x - size.width / 2,
|
||||
y: constrainedCenter.y - size.height / 2,
|
||||
}
|
||||
}
|
||||
|
||||
function isPositionValidForCurrentDisplays(pos: Point, size: Size): boolean {
|
||||
if (!displayInfo.value)
|
||||
return false
|
||||
|
||||
const center = getWindowCenterPoint(pos, size)
|
||||
return displayInfo.value.monitors.some((monitor) => {
|
||||
const workArea = monitor.workArea
|
||||
return (
|
||||
center.x >= workArea.position.x
|
||||
&& center.x <= workArea.position.x + workArea.size.width
|
||||
&& center.y >= workArea.position.y
|
||||
&& center.y <= workArea.position.y + workArea.size.height
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
function hasSignificantDisplayChange(oldInfo: DisplayInfo, newInfo: DisplayInfo): boolean {
|
||||
// Check if the number of monitors changed
|
||||
if (oldInfo.monitors.length !== newInfo.monitors.length)
|
||||
return true
|
||||
|
||||
// Check if primary monitor changed
|
||||
if (oldInfo.primaryMonitor.name !== newInfo.primaryMonitor.name)
|
||||
return true
|
||||
|
||||
// Check if any monitor resolution/position changed significantly
|
||||
for (const oldMonitor of oldInfo.monitors) {
|
||||
const newMonitor = newInfo.monitors.find(m => m.name === oldMonitor.name)
|
||||
if (!newMonitor)
|
||||
return true
|
||||
|
||||
const oldWorkArea = oldMonitor.workArea
|
||||
const newWorkArea = newMonitor.workArea
|
||||
|
||||
if (
|
||||
Math.abs(oldWorkArea.size.width - newWorkArea.size.width) > 50
|
||||
|| Math.abs(oldWorkArea.size.height - newWorkArea.size.height) > 50
|
||||
|| Math.abs(oldWorkArea.position.x - newWorkArea.position.x) > 50
|
||||
|| Math.abs(oldWorkArea.position.y - newWorkArea.position.y) > 50
|
||||
) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
function positionsEqual(a: (Point & Size), b: (Point & Size)): boolean {
|
||||
if (!a || !b)
|
||||
return a === b
|
||||
|
||||
return (
|
||||
Math.abs(a.x - b.x) < 1
|
||||
&& Math.abs(a.y - b.y) < 1
|
||||
&& Math.abs(a.width - b.width) < 1
|
||||
&& Math.abs(a.height - b.height) < 1
|
||||
)
|
||||
}
|
||||
|
||||
async function refreshDisplayInfo(): Promise<void> {
|
||||
if (platform.value === 'web')
|
||||
return
|
||||
|
||||
try {
|
||||
const [monitors, primaryMonitor] = (await invoke('plugin_window_get_display_info'))!
|
||||
displayInfo.value = {
|
||||
monitors,
|
||||
primaryMonitor,
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('[WindowPersistence] Failed to refresh display info:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Manual control functions
|
||||
async function manualSave(): Promise<boolean> {
|
||||
return await savePosition()
|
||||
}
|
||||
|
||||
async function manualRestore(): Promise<boolean> {
|
||||
return await restorePosition()
|
||||
}
|
||||
|
||||
function clearPersistedState(): void {
|
||||
isStateRestored.value = false
|
||||
// Plugin will handle clearing persisted state internally
|
||||
console.warn('[WindowPersistence] State marked as cleared locally')
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
displayInfo: readonly(displayInfo),
|
||||
currentWindowPosition: readonly(currentWindowPosition),
|
||||
windowFrame: readonly(windowFrame),
|
||||
isPositioning: readonly(isPositioning),
|
||||
isRestoring: readonly(isRestoring),
|
||||
isStateRestored: readonly(isStateRestored),
|
||||
|
||||
// Computed
|
||||
primaryMonitor,
|
||||
currentMonitor,
|
||||
boundaryConstraints,
|
||||
isWindowInValidPosition,
|
||||
|
||||
// Core functions
|
||||
initialize,
|
||||
savePosition: manualSave,
|
||||
restorePosition: manualRestore,
|
||||
ensureWindowInBounds,
|
||||
applyPosition,
|
||||
centerOnPrimaryMonitor,
|
||||
|
||||
// Utilities
|
||||
refreshDisplayInfo,
|
||||
clearPersistedState,
|
||||
getWindowCenterPoint,
|
||||
|
||||
// Internal utilities (exposed for debugging)
|
||||
constrainPositionToBounds,
|
||||
isPositionValidForCurrentDisplays,
|
||||
}
|
||||
}
|
||||
@@ -52,33 +52,23 @@ interface Events {
|
||||
export interface AiriTamagotchiEvents extends Events {
|
||||
'tauri-app:window-click-through:position-cursor-and-window-frame': [Point, WindowFrame]
|
||||
'tauri-app:model-load-progress': [string, number]
|
||||
'tauri-app:window-position-changed': WindowPosition
|
||||
'tauri-app:window-state-saved': undefined
|
||||
'tauri-app:window-state-restored': undefined
|
||||
'tauri-app:display-changed': DisplayInfo
|
||||
'mcp_plugin_destroyed': undefined
|
||||
'tauri-app:invoke-returns:plugins-window-get-display-info': [[number, number], [number, number]]
|
||||
}
|
||||
|
||||
export interface Monitor {
|
||||
id: number
|
||||
name: string
|
||||
is_primary: boolean
|
||||
bounds: WindowFrame
|
||||
work_area: WindowFrame
|
||||
size: { width: number, height: number }
|
||||
position: { x: number, y: number }
|
||||
workArea: {
|
||||
position: { x: number, y: number }
|
||||
size: { width: number, height: number }
|
||||
}
|
||||
scale_factor: number
|
||||
}
|
||||
|
||||
export interface DisplayInfo {
|
||||
monitors: Monitor[]
|
||||
primary_monitor_id: number
|
||||
}
|
||||
|
||||
export interface WindowPosition {
|
||||
x: number
|
||||
y: number
|
||||
width: number
|
||||
height: number
|
||||
primaryMonitor: Monitor
|
||||
}
|
||||
|
||||
export enum PlacementStrategy {
|
||||
@@ -99,7 +89,7 @@ export interface WindowPlacementRequest {
|
||||
}
|
||||
|
||||
export interface WindowPlacementResult {
|
||||
position: WindowPosition
|
||||
position: Point
|
||||
target_monitor_id: number
|
||||
is_constrained: boolean
|
||||
}
|
||||
@@ -149,14 +139,19 @@ export function useTauriEvent<ES = Events>() {
|
||||
}
|
||||
|
||||
export interface InvokeMethods {
|
||||
open_settings_window: { args: undefined, options: undefined, returns: void }
|
||||
// Model related
|
||||
load_models: { args: undefined, options: undefined, returns: void }
|
||||
|
||||
// Click Through
|
||||
start_monitor: { args: undefined, options: undefined, returns: void }
|
||||
stop_monitor: { args: undefined, options: undefined, returns: void }
|
||||
open_chat_window: { args: undefined, options: undefined, returns: void }
|
||||
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 }
|
||||
|
||||
// Windows
|
||||
open_settings_window: { args: undefined, options: undefined, returns: void }
|
||||
open_chat_window: { args: undefined, options: undefined, returns: void }
|
||||
|
||||
// WindowLink.vue
|
||||
open_route_in_window: {
|
||||
args: { route: string, windowLabel?: string } | undefined
|
||||
@@ -165,35 +160,30 @@ export interface InvokeMethods {
|
||||
}
|
||||
|
||||
// Window positioning methods
|
||||
plugins_window_get_display_info: {
|
||||
plugin_window_get_display_info: {
|
||||
args: undefined
|
||||
options: undefined
|
||||
returns: DisplayInfo
|
||||
}
|
||||
plugins_window_calculate_window_placement: {
|
||||
args: { request: WindowPlacementRequest }
|
||||
options: undefined
|
||||
returns: WindowPlacementResult
|
||||
}
|
||||
plugins_window_apply_window_position: {
|
||||
args: { position: WindowPosition }
|
||||
options: undefined
|
||||
returns: void
|
||||
}
|
||||
plugins_window_save_window_state: {
|
||||
args: undefined
|
||||
options: undefined
|
||||
returns: void
|
||||
}
|
||||
plugins_window_restore_window_state: {
|
||||
args: undefined
|
||||
options: undefined
|
||||
returns: void
|
||||
returns: [Monitor[], Monitor]
|
||||
}
|
||||
plugins_window_get_current_window_info: {
|
||||
args: undefined
|
||||
options: undefined
|
||||
returns: WindowPosition
|
||||
returns: [[number, number], [number, number]]
|
||||
}
|
||||
plugins_window_set_position: {
|
||||
args: { x: number, y: number }
|
||||
options: undefined
|
||||
returns: void
|
||||
}
|
||||
plugins_window_persistence_save: {
|
||||
args: undefined
|
||||
options: undefined
|
||||
returns: void
|
||||
}
|
||||
plugins_window_persistence_restore: {
|
||||
args: undefined
|
||||
options: undefined
|
||||
returns: void
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import ResourceStatusIsland from '../components/Widgets/ResourceStatusIsland/index.vue'
|
||||
import { useTauriCore, useTauriEvent } from '../composables/tauri'
|
||||
import { useTauriWindowClickThrough } from '../composables/tauri-click-through'
|
||||
import { useWindowPersistence } from '../composables/tauri-window-persistence'
|
||||
import { useWindowShortcuts } from '../composables/window-shortcuts'
|
||||
import { useResourcesStore } from '../stores/resources'
|
||||
import { useWindowControlStore } from '../stores/window-controls'
|
||||
@@ -34,15 +35,14 @@ const { connected, serverCmd, serverArgs } = storeToRefs(mcpStore)
|
||||
|
||||
watch([live2dLookAtX, live2dLookAtY], ([x, y]) => live2dFocusAt.value = { x, y }, { immediate: true })
|
||||
|
||||
// // Initialize window persistence system
|
||||
// const windowPersistence = useWindowPersistence({
|
||||
// autoSave: true,
|
||||
// autoRestore: true,
|
||||
// constrainToDisplays: true,
|
||||
// centerPointConstraint: true,
|
||||
// scaleAware: true,
|
||||
// monitorDisplayChanges: true
|
||||
// })
|
||||
const windowPersistence = useWindowPersistence({
|
||||
autoSave: true,
|
||||
autoRestore: true,
|
||||
constrainToDisplays: true,
|
||||
centerPointConstraint: true,
|
||||
monitorDisplayChanges: true,
|
||||
|
||||
})
|
||||
|
||||
const modeIndicatorClass = computed(() => {
|
||||
switch (windowStore.controlMode) {
|
||||
@@ -60,6 +60,7 @@ const modeIndicatorClass = computed(() => {
|
||||
onMounted(async () => {
|
||||
await invoke('start_monitor')
|
||||
await startClickThrough()
|
||||
await windowPersistence.initialize()
|
||||
})
|
||||
|
||||
onUnmounted(async () => {
|
||||
|
||||
Reference in New Issue
Block a user