From 802e2f2aefd56af13575c4cdf5a81722a8bc6aa5 Mon Sep 17 00:00:00 2001 From: lshw54 Date: Sat, 11 Jul 2026 02:08:21 +0800 Subject: [PATCH 1/2] fix(window): drop leftover --force-device-scale-factor=1 so the window matches content at >100% scaling #257 forced the webview to 1:1 physical pixels for its physical-px layout, but #326 switched the router's fitWindow to size the OS window with LogicalSize (Tauri multiplies logical px by the OS scale factor). With the webview still rendering 1:1, at >100% Windows scaling the window came out scale-times larger than the content -> the 'window too big, empty space around it' bug. Let the webview devicePixelRatio follow the OS scale so LogicalSize and fitWindow's CSS-px measurements agree and the window matches content at every scaling. Keep --force-text-scale-factor=1 (orthogonal accessibility guard). --- src-tauri/src/lib.rs | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 7145fa7..0918ecc 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -347,16 +347,28 @@ pub fn run() { .unwrap_or_default() .eq_ignore_ascii_case("true"); + // NOTE: we deliberately do NOT pass `--force-device-scale-factor=1`. + // + // That flag (from #257's physical-px "DPI-immune" layout) pinned the + // webview to 1:1 physical pixels. But #326 switched the router's + // `fitWindow` to size the OS window with `LogicalSize`, which Tauri + // multiplies by the window's OS scale factor. With the webview still + // rendering 1:1, at >100% Windows scaling the window came out + // `scale`× larger than the 1:1 content → the reported "window too big, + // empty space around the content" bug. Letting the webview's + // `devicePixelRatio` follow the OS scale makes `LogicalSize` and the + // CSS-px measurements in `fitWindow` (which already assume + // `screen.availWidth` is CSS px) consistent, so the window matches the + // content at every scaling. + // + // `--force-text-scale-factor=1` stays: it's an orthogonal accessibility + // guard (Windows "Text size") and does not touch device scaling. let mut args = vec![ - // Keep the existing DPI/resolution behaviour: render at - // 1:1 physical pixels and let the router's fitWindow - // compensate the logical window size. - "--force-device-scale-factor=1".to_string(), - // Treat Windows Accessibility "Text size" as 100% so - // user text-size changes do not inflate app content. + // Treat Windows Accessibility "Text size" as 100% so a user's + // text-size setting does not inflate the app layout. "--force-text-scale-factor=1".to_string(), ]; - tracing::info!("forcing WebView2 device scale and text scale factors"); + tracing::info!("forcing WebView2 text scale factor to 1 (device scale left to the OS)"); if disable_hw_accel { args.push("--disable-gpu".to_string()); From 8f72faaeb7a7137271590318058348cc046d0ec7 Mon Sep 17 00:00:00 2001 From: lshw54 Date: Sat, 11 Jul 2026 02:32:59 +0800 Subject: [PATCH 2/2] =?UTF-8?q?feat(settings):=20add=20Maintenance=20secti?= =?UTF-8?q?on=20=E2=80=94=20reset=20window=20position=20+=20clear=20WebVie?= =?UTF-8?q?w2=20cache?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a Settings -> Maintenance section with two recovery controls for the 'window too big / empty space / off-screen' reports: - Reset window position: re-centers the OS window and deletes the persisted tauri-plugin-window-state position (POSITION-only), fixing a window stranded off-screen or at a bad spot. - Clear WebView2 cache: clears the main webview's browsing data (incl. the persisted zoom that can mis-size the window). Confirmed first; does NOT sign the user out (the session lives in the backend HTTP client, not the webview). Backend: system::reset_window_position + system::clear_webview2_cache commands (bindings.ts regenerated). i18n keys added to all three locales. --- src-tauri/src/commands/mod.rs | 4 ++ src-tauri/src/commands/system.rs | 89 ++++++++++++++++++++++++++++++++ src/i18n/messages.ts | 27 ++++++++++ src/pages/Settings.vue | 80 ++++++++++++++++++++++++++++ src/types/bindings.ts | 45 ++++++++++++++++ 5 files changed, 245 insertions(+) diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index b0adc97..de0aba5 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -275,6 +275,10 @@ pub fn build_specta_builder() -> Builder { // see the `auth::login_gamepass_start` comment for the full // E0401 / runtime-agnostic bindings rationale. system::minimize_main_window::, + // system (Settings → Maintenance: window/cache recovery). Generic over + // `R` for the same `AppHandle` reason as `minimize_main_window`. + system::reset_window_position::, + system::clear_webview2_cache::, // config (P10.3 — D2) config::get_config_value, config::get_all_config, diff --git a/src-tauri/src/commands/system.rs b/src-tauri/src/commands/system.rs index 69e1b40..0290caf 100644 --- a/src-tauri/src/commands/system.rs +++ b/src-tauri/src/commands/system.rs @@ -241,6 +241,95 @@ pub async fn minimize_main_window( Ok(()) } +/// Filename the `tauri-plugin-window-state` plugin persists the window +/// position into, under `app_config_dir()` (Windows: +/// `%APPDATA%\tw.beanfun.app\.window-state.json`). The plugin's documented +/// default; kept as a named constant so [`reset_window_position`] and the +/// plugin stay in sync if it is ever customised in `lib.rs`. +const WINDOW_STATE_FILENAME: &str = ".window-state.json"; + +/// Reset the main window to a safe, on-screen position. +/// +/// `tauri-plugin-window-state` persists the window **position** (see +/// `lib.rs`), so a window dragged off-screen — or stranded by a disconnected +/// monitor — restores to that bad spot on the next launch. This re-centers the +/// live window and deletes the saved state file so the next launch also starts +/// centered. +/// +/// # Errors +/// +/// - `system.window_not_found` — the `main` window is not registered. +/// - `system.reset_window_failed` — `window.center()` returned an OS error. +#[tauri::command] +#[specta::specta] +pub async fn reset_window_position( + app_handle: AppHandle, +) -> Result<(), CommandError> { + let win = app_handle.get_webview_window("main").ok_or_else(|| { + CommandError::new( + "system.window_not_found", + "main webview window is not currently registered", + ) + })?; + win.center().map_err(|err| { + CommandError::new( + "system.reset_window_failed", + format!("failed to center main window: {err}"), + ) + })?; + + // Best-effort: drop the persisted position so a future launch starts from + // the centered spot rather than restoring the old (possibly off-screen) + // one. Non-fatal — the live window is already centered, and the plugin + // re-saves the good position on the next clean exit. + if let Ok(config_dir) = app_handle.path().app_config_dir() { + let state_file = config_dir.join(WINDOW_STATE_FILENAME); + match std::fs::remove_file(&state_file) { + Ok(()) => {} + Err(err) if err.kind() == std::io::ErrorKind::NotFound => {} + Err(err) => tracing::warn!( + step = "ResetWindow.RemoveState", + error = %err, + "could not delete persisted window-state file (non-fatal)" + ), + } + } + Ok(()) +} + +/// Clear the main webview's browsing data (the WebView2 cache on Windows). +/// +/// WebView2 persists its cache — including the page **zoom factor** — in the +/// user-data folder (`%APPDATA%\tw.beanfun.app\EBWebView`). A stale/corrupt +/// cache can leave the UI mis-rendered; this clears it. The app's login +/// session lives in the backend HTTP client's cookie jar (not the webview), so +/// this does **not** sign the user out. A restart is recommended so the cleared +/// state fully takes effect. +/// +/// # Errors +/// +/// - `system.window_not_found` — the `main` window is not registered. +/// - `system.clear_cache_failed` — the WebView2 clear call returned an error. +#[tauri::command] +#[specta::specta] +pub async fn clear_webview2_cache( + app_handle: AppHandle, +) -> Result<(), CommandError> { + let win = app_handle.get_webview_window("main").ok_or_else(|| { + CommandError::new( + "system.window_not_found", + "main webview window is not currently registered", + ) + })?; + win.clear_all_browsing_data().map_err(|err| { + CommandError::new( + "system.clear_cache_failed", + format!("failed to clear webview browsing data: {err}"), + ) + })?; + Ok(()) +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/i18n/messages.ts b/src/i18n/messages.ts index d5b2f77..b30b293 100644 --- a/src/i18n/messages.ts +++ b/src/i18n/messages.ts @@ -436,6 +436,15 @@ const zhTW = { tradLoginTip: '使用傳統登入流程(多次跳轉),\n適合自動登入失敗或無法跳出登入視窗時使用。', killPatcherTip: '阻止 beanfun! 啟動的更新程式 (Patcher.aspx) 自動執行。', skipPlayWindowTip: '直接啟動遊戲,跳過 Play 視窗確認步驟。', + maintenance: '維護', + maintenanceTip: '視窗顯示異常(過大、四周空白、位置跑掉)時可嘗試。', + resetWindowPosition: '重設視窗位置', + resetWindowPositionDone: '視窗位置已重設並置中。', + clearWebviewCache: '清理 WebView2 快取', + clearWebviewCacheTitle: '清理 WebView2 快取', + clearWebviewCacheConfirm: + '這會清除 WebView2 的快取與縮放狀態,不會登出你的帳號。清理後建議重新啟動 Beanfun。要繼續嗎?', + clearWebviewCacheDone: 'WebView2 快取已清理,建議重新啟動 Beanfun。', }, inAppBrowser: { fallbackToSystem: '此網址不在 Beanfun 內建瀏覽器允許清單,將改以系統預設瀏覽器開啟。', @@ -675,6 +684,15 @@ const zhCN = { tradLoginTip: '使用传统登录流程(多次跳转),\n适合自动登录失败或无法跳出登录窗口时使用。', killPatcherTip: '阻止 beanfun! 启动的更新程序 (Patcher.aspx) 自动执行。', skipPlayWindowTip: '直接启动游戏,跳过 Play 窗口确认步骤。', + maintenance: '维护', + maintenanceTip: '窗口显示异常(过大、四周空白、位置跑掉)时可尝试。', + resetWindowPosition: '重置窗口位置', + resetWindowPositionDone: '窗口位置已重置并居中。', + clearWebviewCache: '清理 WebView2 缓存', + clearWebviewCacheTitle: '清理 WebView2 缓存', + clearWebviewCacheConfirm: + '这会清除 WebView2 的缓存与缩放状态,不会登出你的账号。清理后建议重新启动 Beanfun。要继续吗?', + clearWebviewCacheDone: 'WebView2 缓存已清理,建议重新启动 Beanfun。', }, inAppBrowser: { fallbackToSystem: '此网址不在 Beanfun 内建浏览器允许列表,将改以系统默认浏览器开启。', @@ -926,6 +944,15 @@ const enUS = { 'Use the traditional multi-redirect login flow.\nHandy when auto-login fails or the login window does not appear.', killPatcherTip: 'Prevent the beanfun! launcher (Patcher.aspx) from auto-running.', skipPlayWindowTip: 'Launch the game directly, skipping the Play window confirmation step.', + maintenance: 'Maintenance', + maintenanceTip: 'Try these if the window looks wrong (too large, empty space, off-screen).', + resetWindowPosition: 'Reset window position', + resetWindowPositionDone: 'Window position reset and centered.', + clearWebviewCache: 'Clear WebView2 cache', + clearWebviewCacheTitle: 'Clear WebView2 cache', + clearWebviewCacheConfirm: + 'This clears the WebView2 cache and zoom state. It will not sign you out. A Beanfun restart is recommended afterwards. Continue?', + clearWebviewCacheDone: 'WebView2 cache cleared. Restarting Beanfun is recommended.', }, inAppBrowser: { fallbackToSystem: diff --git a/src/pages/Settings.vue b/src/pages/Settings.vue index a526fb6..98c1c8b 100644 --- a/src/pages/Settings.vue +++ b/src/pages/Settings.vue @@ -118,10 +118,13 @@ import { } from 'element-plus' import { ArrowLeft, + Delete, FolderOpened, InfoFilled, Operation, + Refresh, Setting as SettingIcon, + Tools, User, } from '@element-plus/icons-vue' import { open as openFileDialog } from '@tauri-apps/plugin-dialog' @@ -640,6 +643,41 @@ function handleBack(): void { void router.push('/login') } +/* --------------- Maintenance — window / cache recovery --------------- */ + +/** + * Re-center the OS window and clear the persisted position. Recovery for a + * window that restored off-screen or at a bad spot (the `POSITION`-only + * `tauri-plugin-window-state` can strand it there). `safeInvoke` toasts on + * failure; we add a success toast so the click has visible feedback even + * though the window jump is usually obvious. + */ +async function handleResetWindowPosition(): Promise { + const result = await safeInvoke(commands.resetWindowPosition()) + if (result.ok) ElMessage.success(t('settings.resetWindowPositionDone')) +} + +/** + * Clear the WebView2 cache (incl. the persisted zoom that could mis-size the + * window). Confirmed first because a restart is recommended afterwards. Does + * not sign the user out — the login session lives in the backend HTTP client, + * not the webview. + */ +async function handleClearWebviewCache(): Promise { + try { + await ElMessageBox.confirm( + t('settings.clearWebviewCacheConfirm'), + t('settings.clearWebviewCacheTitle'), + { type: 'warning' }, + ) + } catch { + // User dismissed the confirm dialog — no-op. + return + } + const result = await safeInvoke(commands.clearWebview2Cache()) + if (result.ok) ElMessage.success(t('settings.clearWebviewCacheDone')) +} + /* --------------- mount --------------- */ onMounted(() => { @@ -930,6 +968,35 @@ onMounted(() => {

{{ t('settings.gameSectionEmpty') }}

+ +
+
+ + {{ t('settings.maintenance') }} +
+

{{ t('settings.maintenanceTip') }}

+
+ + + {{ t('settings.resetWindowPosition') }} + + + + {{ t('settings.clearWebviewCache') }} + +
+
+