mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-06 14:39:35 +08:00
✨ feat(window): 实现窗口尺寸位置与侧边栏宽度持久化记忆
- 窗口状态:新增 windowState 记录全屏/最大化/普通状态,关闭后重开自动恢复 - 窗口尺寸:普通窗口模式下每2秒自动保存宽高和坐标位置 - 侧边栏宽度:sidebarWidth 从 useState 迁移至 zustand store 持久化 - 状态恢复:启动时根据保存的状态决定全屏/最大化/恢复具体尺寸位置 - 数据校验:新增 sanitizeWindowBounds/sanitizeWindowState/sanitizeSidebarWidth 校验函数 - 兼容处理:startupFullscreen 设置优先级高于自动记忆的窗口状态 - refs #259
This commit is contained in:
@@ -2,7 +2,7 @@ import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { Layout, Button, ConfigProvider, theme, message, Modal, Spin, Slider, Progress, Switch, Input, InputNumber, Select } from 'antd';
|
||||
import zhCN from 'antd/locale/zh_CN';
|
||||
import { PlusOutlined, ConsoleSqlOutlined, UploadOutlined, DownloadOutlined, CloudDownloadOutlined, BugOutlined, ToolOutlined, GlobalOutlined, InfoCircleOutlined, GithubOutlined, SkinOutlined, CheckOutlined, MinusOutlined, BorderOutlined, CloseOutlined, SettingOutlined, LinkOutlined, BgColorsOutlined, AppstoreOutlined } from '@ant-design/icons';
|
||||
import { BrowserOpenURL, Environment, EventsOn, Quit, WindowFullscreen, WindowGetSize, WindowIsFullscreen, WindowIsMaximised, WindowMaximise, WindowMinimise, WindowSetSize, WindowToggleMaximise, WindowUnfullscreen } from '../wailsjs/runtime';
|
||||
import { BrowserOpenURL, Environment, EventsOn, Quit, WindowFullscreen, WindowGetPosition, WindowGetSize, WindowIsFullscreen, WindowIsMaximised, WindowMaximise, WindowMinimise, WindowSetPosition, WindowSetSize, WindowToggleMaximise, WindowUnfullscreen } from '../wailsjs/runtime';
|
||||
import Sidebar from './components/Sidebar';
|
||||
import TabManager from './components/TabManager';
|
||||
import ConnectionModal from './components/ConnectionModal';
|
||||
@@ -89,7 +89,8 @@ function App() {
|
||||
const [runtimePlatform, setRuntimePlatform] = useState('');
|
||||
const [isLinuxRuntime, setIsLinuxRuntime] = useState(false);
|
||||
const [isStoreHydrated, setIsStoreHydrated] = useState(() => useStore.persist.hasHydrated());
|
||||
const [sidebarWidth, setSidebarWidth] = useState(330);
|
||||
const sidebarWidth = useStore(state => state.sidebarWidth);
|
||||
const setSidebarWidth = useStore(state => state.setSidebarWidth);
|
||||
const globalProxyInvalidHintShownRef = React.useRef(false);
|
||||
|
||||
// 同步 macOS 窗口透明度:opacity=1.0 且 blur=0 时关闭 NSVisualEffectView,
|
||||
@@ -285,14 +286,43 @@ function App() {
|
||||
}, applyRetryDelayMs);
|
||||
};
|
||||
|
||||
const restoreWindowState = async () => {
|
||||
if (cancelled) return;
|
||||
const state = useStore.getState();
|
||||
// startupFullscreen 设置优先
|
||||
if (state.startupFullscreen) {
|
||||
applyStartupWindowPreference(1);
|
||||
return;
|
||||
}
|
||||
// 根据上次保存的窗口状态恢复
|
||||
const savedState = state.windowState;
|
||||
if (savedState === 'fullscreen') {
|
||||
applyStartupWindowPreference(1);
|
||||
return;
|
||||
}
|
||||
if (savedState === 'maximized') {
|
||||
try { await WindowMaximise(); } catch (_) {}
|
||||
return;
|
||||
}
|
||||
// 普通窗口:恢复尺寸和位置
|
||||
const bounds = state.windowBounds;
|
||||
if (!bounds || bounds.width < 400 || bounds.height < 300) return;
|
||||
try {
|
||||
WindowSetSize(bounds.width, bounds.height);
|
||||
WindowSetPosition(bounds.x, bounds.y);
|
||||
} catch (e) {
|
||||
console.warn('Failed to restore window bounds', e);
|
||||
}
|
||||
};
|
||||
|
||||
if (useStore.persist.hasHydrated()) {
|
||||
applyStartupWindowPreference(1);
|
||||
void restoreWindowState();
|
||||
}
|
||||
const unsubscribeHydration = useStore.persist.onFinishHydration(() => {
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
applyStartupWindowPreference(1);
|
||||
void restoreWindowState();
|
||||
});
|
||||
|
||||
return () => {
|
||||
@@ -304,6 +334,52 @@ function App() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 定时保存窗口状态、尺寸与位置
|
||||
useEffect(() => {
|
||||
const SAVE_INTERVAL_MS = 2000;
|
||||
let lastSaved = '';
|
||||
|
||||
const saveWindowState = async () => {
|
||||
try {
|
||||
const [isFs, isMax] = await Promise.all([
|
||||
WindowIsFullscreen().catch(() => false),
|
||||
WindowIsMaximised().catch(() => false),
|
||||
]);
|
||||
|
||||
// 保存窗口状态
|
||||
const store = useStore.getState();
|
||||
const newState = isFs ? 'fullscreen' : (isMax ? 'maximized' : 'normal');
|
||||
if (store.windowState !== newState) {
|
||||
store.setWindowState(newState);
|
||||
}
|
||||
|
||||
// 只在普通窗口模式下保存尺寸和位置
|
||||
if (isFs || isMax) return;
|
||||
|
||||
const [size, pos] = await Promise.all([
|
||||
WindowGetSize().catch(() => null),
|
||||
WindowGetPosition().catch(() => null),
|
||||
]);
|
||||
if (!size || !pos) return;
|
||||
const w = Math.trunc(Number(size.w || 0));
|
||||
const h = Math.trunc(Number(size.h || 0));
|
||||
const x = Math.trunc(Number(pos.x || 0));
|
||||
const y = Math.trunc(Number(pos.y || 0));
|
||||
if (w < 400 || h < 300) return;
|
||||
|
||||
const key = `${w},${h},${x},${y}`;
|
||||
if (key === lastSaved) return;
|
||||
lastSaved = key;
|
||||
store.setWindowBounds({ width: w, height: h, x, y });
|
||||
} catch (e) {
|
||||
// 静默忽略
|
||||
}
|
||||
};
|
||||
|
||||
const timer = window.setInterval(saveWindowState, SAVE_INTERVAL_MS);
|
||||
return () => window.clearInterval(timer);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isWindowsPlatform()) {
|
||||
return;
|
||||
|
||||
@@ -420,6 +420,9 @@ interface AppState {
|
||||
enableColumnOrderMemory: boolean;
|
||||
tableHiddenColumns: Record<string, string[]>;
|
||||
enableHiddenColumnMemory: boolean;
|
||||
windowBounds: { width: number; height: number; x: number; y: number } | null;
|
||||
windowState: 'normal' | 'fullscreen' | 'maximized';
|
||||
sidebarWidth: number;
|
||||
|
||||
addConnection: (conn: SavedConnection) => void;
|
||||
updateConnection: (conn: SavedConnection) => void;
|
||||
@@ -469,6 +472,9 @@ interface AppState {
|
||||
setTableHiddenColumns: (connectionId: string, dbName: string, tableName: string, hiddenColumns: string[]) => void;
|
||||
setEnableHiddenColumnMemory: (enabled: boolean) => void;
|
||||
clearTableHiddenColumns: (connectionId: string, dbName: string, tableName: string) => void;
|
||||
setWindowBounds: (bounds: { width: number; height: number; x: number; y: number }) => void;
|
||||
setWindowState: (state: 'normal' | 'fullscreen' | 'maximized') => void;
|
||||
setSidebarWidth: (width: number) => void;
|
||||
}
|
||||
|
||||
const sanitizeSavedQueries = (value: unknown): SavedQuery[] => {
|
||||
@@ -599,6 +605,29 @@ const sanitizeGlobalProxy = (value: unknown): GlobalProxyConfig => {
|
||||
};
|
||||
};
|
||||
|
||||
const sanitizeWindowState = (value: unknown): 'normal' | 'fullscreen' | 'maximized' => {
|
||||
if (value === 'fullscreen' || value === 'maximized') return value;
|
||||
return 'normal';
|
||||
};
|
||||
|
||||
const sanitizeSidebarWidth = (value: unknown): number => {
|
||||
const parsed = Number(value);
|
||||
if (!Number.isFinite(parsed)) return 330;
|
||||
return Math.max(200, Math.min(600, Math.trunc(parsed)));
|
||||
};
|
||||
|
||||
const sanitizeWindowBounds = (value: unknown): { width: number; height: number; x: number; y: number } | null => {
|
||||
if (!value || typeof value !== 'object') return null;
|
||||
const raw = value as Record<string, unknown>;
|
||||
const width = Number(raw.width);
|
||||
const height = Number(raw.height);
|
||||
const x = Number(raw.x);
|
||||
const y = Number(raw.y);
|
||||
if (!Number.isFinite(width) || !Number.isFinite(height) || !Number.isFinite(x) || !Number.isFinite(y)) return null;
|
||||
if (width < 400 || height < 300) return null;
|
||||
return { width: Math.trunc(width), height: Math.trunc(height), x: Math.trunc(x), y: Math.trunc(y) };
|
||||
};
|
||||
|
||||
const unwrapPersistedAppState = (persistedState: unknown): Record<string, unknown> => {
|
||||
if (!persistedState || typeof persistedState !== 'object') {
|
||||
return {};
|
||||
@@ -635,6 +664,9 @@ export const useStore = create<AppState>()(
|
||||
enableColumnOrderMemory: true,
|
||||
tableHiddenColumns: {},
|
||||
enableHiddenColumnMemory: true,
|
||||
windowBounds: null,
|
||||
windowState: 'normal' as const,
|
||||
sidebarWidth: 330,
|
||||
|
||||
addConnection: (conn) => set((state) => ({ connections: [...state.connections, conn] })),
|
||||
updateConnection: (conn) => set((state) => ({
|
||||
@@ -875,6 +907,19 @@ export const useStore = create<AppState>()(
|
||||
}),
|
||||
|
||||
setEnableHiddenColumnMemory: (enabled) => set({ enableHiddenColumnMemory: !!enabled }),
|
||||
|
||||
setWindowBounds: (bounds) => set({
|
||||
windowBounds: {
|
||||
width: Math.max(400, Math.trunc(bounds.width)),
|
||||
height: Math.max(300, Math.trunc(bounds.height)),
|
||||
x: Math.trunc(bounds.x),
|
||||
y: Math.trunc(bounds.y),
|
||||
}
|
||||
}),
|
||||
|
||||
setWindowState: (state) => set({ windowState: state }),
|
||||
|
||||
setSidebarWidth: (width) => set({ sidebarWidth: Math.max(200, Math.min(600, Math.trunc(width))) }),
|
||||
}),
|
||||
{
|
||||
name: 'lite-db-storage', // name of the item in the storage (must be unique)
|
||||
@@ -906,7 +951,10 @@ export const useStore = create<AppState>()(
|
||||
nextState.enableColumnOrderMemory = state.enableColumnOrderMemory !== false;
|
||||
const safeHidden = sanitizeTableHiddenColumns(state.tableHiddenColumns);
|
||||
nextState.tableHiddenColumns = safeHidden;
|
||||
nextState.enableHiddenColumnMemory = state.enableHiddenColumnMemory !== false;
|
||||
nextState.enableHiddenColumnMemory = state.enableHiddenColumnMemory !== false;
|
||||
nextState.windowBounds = sanitizeWindowBounds(state.windowBounds);
|
||||
nextState.windowState = sanitizeWindowState(state.windowState);
|
||||
nextState.sidebarWidth = sanitizeSidebarWidth(state.sidebarWidth);
|
||||
return nextState as AppState;
|
||||
},
|
||||
merge: (persistedState, currentState) => {
|
||||
@@ -928,6 +976,9 @@ export const useStore = create<AppState>()(
|
||||
enableColumnOrderMemory: state.enableColumnOrderMemory !== false,
|
||||
tableHiddenColumns: sanitizeTableHiddenColumns(state.tableHiddenColumns),
|
||||
enableHiddenColumnMemory: state.enableHiddenColumnMemory !== false,
|
||||
windowBounds: sanitizeWindowBounds(state.windowBounds),
|
||||
windowState: sanitizeWindowState(state.windowState),
|
||||
sidebarWidth: sanitizeSidebarWidth(state.sidebarWidth),
|
||||
|
||||
sqlFormatOptions: sanitizeSqlFormatOptions(state.sqlFormatOptions),
|
||||
queryOptions: sanitizeQueryOptions(state.queryOptions),
|
||||
@@ -953,7 +1004,10 @@ export const useStore = create<AppState>()(
|
||||
tableColumnOrders: state.tableColumnOrders,
|
||||
enableColumnOrderMemory: state.enableColumnOrderMemory,
|
||||
tableHiddenColumns: state.tableHiddenColumns,
|
||||
enableHiddenColumnMemory: state.enableHiddenColumnMemory
|
||||
enableHiddenColumnMemory: state.enableHiddenColumnMemory,
|
||||
windowBounds: state.windowBounds,
|
||||
windowState: state.windowState,
|
||||
sidebarWidth: state.sidebarWidth,
|
||||
}), // Don't persist logs
|
||||
}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user