feat: move setup wizard to separate window (#179)

* feat: move setup wizard to separate window (MPA) with rust backend command

* fix: Remove forced GDK_SCALE exports to respect system DPI scaling

- Remove GDK_SCALE and GDK_DPI_SCALE forced exports from wrapper.sh
  These were forcing 1x scaling even on HiDPI displays with 2x scaling
- Add setup window closure handler to exit app if wizard not completed
- Fix TypeScript error with setInterval return type
- Update install.sh wrapper generation to match wrapper.sh changes

Fixes scaling issues on HiDPI displays (4K monitors with 2x scaling)

* chore: address code review comments

* chore: fix lint issues

* refactor: merge setup.html into index.html, fix race conditions and improve error handling
This commit is contained in:
Kinou
2026-02-15 04:34:55 +01:00
committed by GitHub
parent 0687af8a40
commit 24ffbc40b9
11 changed files with 180 additions and 66 deletions

18
package-lock.json generated
View File

@@ -23,6 +23,7 @@
"devDependencies": {
"@tailwindcss/postcss": "^4.1.18",
"@tauri-apps/cli": "^2.10.0",
"@types/node": "^25.2.3",
"@types/react": "^19.2.13",
"@types/react-dom": "^19.2.3",
"@types/react-window": "^2.0.0",
@@ -2039,6 +2040,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/node": {
"version": "25.2.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.3.tgz",
"integrity": "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~7.16.0"
}
},
"node_modules/@types/react": {
"version": "19.2.13",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.13.tgz",
@@ -4152,6 +4163,13 @@
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/undici-types": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
"dev": true,
"license": "MIT"
},
"node_modules/update-browserslist-db": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz",

1
package.json generated
View File

@@ -43,6 +43,7 @@
"devDependencies": {
"@tailwindcss/postcss": "^4.1.18",
"@tauri-apps/cli": "^2.10.0",
"@types/node": "^25.2.3",
"@types/react": "^19.2.13",
"@types/react-dom": "^19.2.3",
"@types/react-window": "^2.0.0",

View File

@@ -439,8 +439,6 @@ sanitize_xdg_data_dirs() {
}
sanitize_xdg_data_dirs
export GDK_SCALE="${GDK_SCALE:-1}"
export GDK_DPI_SCALE="${GDK_DPI_SCALE:-1}"
export NO_AT_BRIDGE=1
exec "$HOME/.local/bin/win11-clipboard-history.AppImage" "$@"

View File

@@ -139,8 +139,6 @@ fi
# ---------------------------------------------------------------------------
# Display & rendering defaults
# ---------------------------------------------------------------------------
export GDK_SCALE="${GDK_SCALE:-1}"
export GDK_DPI_SCALE="${GDK_DPI_SCALE:-1}"
export TAURI_TRAY="${TAURI_TRAY:-libayatana-appindicator3}"

View File

@@ -274,6 +274,30 @@ async fn copy_text_to_clipboard(_state: State<'_, AppState>, text: String) -> Re
Ok(())
}
#[tauri::command]
async fn finish_setup(app: AppHandle) -> Result<(), String> {
// 1. Mark first run as complete (redundant but safe)
win11_clipboard_history_lib::permission_checker::mark_first_run_complete()
.map_err(|e| e.to_string())?;
// 2. Close setup window
if let Some(setup_window) = app.get_webview_window("setup") {
let _ = setup_window.close();
}
// 3. Show main window
if let Some(main_window) = app.get_webview_window("main") {
// Ensure it's ready to be shown
WindowController::position_and_show(&main_window, &app);
}
// 4. Emit event to main window to update its state (stop waiting)
// We emit to all just in case, or specifically to main
let _ = app.emit("setup_complete", ());
Ok(())
}
// --- Helper for Paste Logic ---
struct PasteHelper;
@@ -773,6 +797,19 @@ fn main() {
config_manager: config_manager.clone(),
is_mouse_inside: is_mouse_inside.clone(),
})
.on_window_event(|window, event| {
if let tauri::WindowEvent::Destroyed = event {
if window.label() == "setup" {
// Check if setup was effectively finished.
// If the user clicked "Start Using", `finish_setup` would have been called.
// `finish_setup` calls `mark_first_run_complete`.
if win11_clipboard_history_lib::permission_checker::is_first_run() {
println!("[Setup] Setup window closed without completion. Exiting app.");
window.app_handle().exit(0);
}
}
}
})
.setup(move |app| {
let app_handle = app.handle().clone();
@@ -985,6 +1022,7 @@ fn main() {
get_recent_emojis,
paste_gif_from_url,
finish_paste,
finish_setup, // Register the new command
set_mouse_state,
get_user_settings,
set_user_settings,

View File

@@ -31,6 +31,22 @@
"alwaysOnTop": true,
"focus": true
},
{
"title": "Setup - Clipboard History",
"label": "setup",
"width": 550,
"height": 650,
"resizable": true,
"minWidth": 400,
"minHeight": 500,
"decorations": true,
"transparent": false,
"visible": false,
"skipTaskbar": false,
"alwaysOnTop": false,
"center": true,
"focus": true
},
{
"title": "Settings - Clipboard History",
"label": "settings",
@@ -52,7 +68,11 @@
},
"bundle": {
"active": true,
"targets": ["deb", "rpm", "appimage"],
"targets": [
"deb",
"rpm",
"appimage"
],
"icon": [
"icons/32x32.png",
"icons/64x64.png",
@@ -104,6 +124,8 @@
"preRemoveScript": "bundle/linux/postrm.sh"
}
},
"resources": ["bundle/linux/99-win11-clipboard-input.rules"]
"resources": [
"bundle/linux/99-win11-clipboard-input.rules"
]
}
}

View File

@@ -666,22 +666,19 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
}
return (
<div className="fixed inset-0 flex items-center justify-center z-50 p-4">
{/* Backdrop */}
<div className="absolute inset-0 bg-black/50 backdrop-blur-sm" />
{/* Modal */}
<div
className={clsx(
'relative w-full max-w-sm rounded-win11-lg p-6 shadow-win11-elevated animate-scale-in',
isDark ? 'glass-effect' : 'glass-effect-light',
isDark ? 'text-win11-text-primary' : 'text-win11Light-text-primary'
'h-full w-full flex flex-col items-center justify-center p-6',
isDark
? 'bg-win11-bg-primary text-win11-text-primary'
: 'bg-win11Light-bg-primary text-win11Light-text-primary'
)}
>
<div className={clsx('w-full max-w-sm', 'animate-scale-in')}>
{steps[step]}
{/* Progress dots */}
<div className="flex justify-center gap-2 mt-6">
<div className="flex justify-center gap-2 mt-8">
{steps.map((_, i) => (
<button
key={`dot-${i}`}

View File

@@ -1,75 +1,81 @@
import React, { useState, useEffect } from 'react'
import ReactDOM from 'react-dom/client'
import { getCurrentWindow } from '@tauri-apps/api/window'
import { getCurrentWindow, getAllWindows } from '@tauri-apps/api/window'
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
import { invoke } from '@tauri-apps/api/core'
import { listen } from '@tauri-apps/api/event'
import ClipboardApp from './ClipboardApp'
import SettingsApp from './SettingsApp'
import { SetupWizard } from './components/SetupWizard'
import { SetupApp } from './setup'
import './index.css'
/**
* Main app wrapper that handles first-run setup wizard
* Main app wrapper that handles first-run check and launches setup window if needed
*/
function ClipboardAppWithSetup() {
const [showWizard, setShowWizard] = useState(false)
const [loading, setLoading] = useState(true)
const [waitingForSetup, setWaitingForSetup] = useState(false)
useEffect(() => {
// Check if this is first run
invoke<boolean>('is_first_run')
.then((isFirst) => {
setShowWizard(isFirst)
setLoading(false)
let unlistenSetup: (() => void) | undefined
const init = async () => {
// Listen for setup completion event from the setup window
// We set this up early to ensure we don't miss it
unlistenSetup = await listen('setup_complete', async () => {
console.log('Setup complete event received')
setWaitingForSetup(false)
})
.catch((err) => {
// Check if this is first run
try {
const isFirst = await invoke<boolean>('is_first_run')
if (isFirst) {
setWaitingForSetup(true)
// In Tauri v2, if the window is in tauri.conf.json, it's already created.
// We just need to find it and show it.
const windows = await getAllWindows()
const setupWin = windows.find((w) => w.label === 'setup')
if (setupWin) {
await setupWin.show()
await setupWin.setFocus()
} else {
console.error('Setup window not found in config')
// Attempt to create it if it somehow doesn't exist (fallback)
const newSetupWin = new WebviewWindow('setup')
newSetupWin.once('tauri://created', () => {
newSetupWin.show()
newSetupWin.setFocus()
})
}
}
setLoading(false)
} catch (err: unknown) {
console.error('Failed to check first run:', err)
setLoading(false)
})
// Listen for reset-to-defaults event from settings
let isMounted = true
let unlistenFn: (() => void) | null = null
listen('show-setup-wizard', () => {
if (isMounted) {
setShowWizard(true)
}
}).then((fn) => {
if (isMounted) {
unlistenFn = fn
} else {
// Component already unmounted, clean up immediately
fn()
}
})
init()
return () => {
isMounted = false
unlistenFn?.()
if (unlistenSetup) unlistenSetup()
}
}, [])
const handleWizardComplete = () => {
setShowWizard(false)
}
if (loading) {
// Show nothing while checking first run status
if (loading || waitingForSetup) {
// Show nothing while checking status or waiting for setup to complete
// This prevents the clipboard app from trying to initialize before permissions are granted
return null
}
return (
<>
{showWizard && <SetupWizard onComplete={handleWizardComplete} />}
<ClipboardApp />
</>
)
return <ClipboardApp />
}
/**
* Root component that routes to either ClipboardApp or SettingsApp
* based on the current window's label
* Root component that routes based on the current window's label
*/
export default function Root() {
const [windowLabel] = useState<string>(() => getCurrentWindow().label)
@@ -79,7 +85,11 @@ export default function Root() {
return <SettingsApp />
}
// Default to ClipboardApp with setup wizard for 'main' and any other window
if (windowLabel === 'setup') {
return <SetupApp />
}
// Default to ClipboardAppWithSetup for 'main'
return <ClipboardAppWithSetup />
}

27
src/setup.tsx Normal file
View File

@@ -0,0 +1,27 @@
import { SetupWizard } from './components/SetupWizard'
import './index.css'
import { invoke } from '@tauri-apps/api/core'
export function SetupApp() {
const handleComplete = async () => {
try {
console.log('SetupApp: invoking finish_setup command')
await invoke('finish_setup')
} catch (err) {
console.error('Failed to finish setup:', err)
// Do not close the window on failure; keep it open so the user can retry.
// If we close it while first_run is still true, the backend will exit the app.
if (typeof window !== 'undefined') {
alert('Failed to finish setup. Please try again or check the logs for details.')
}
}
}
return (
<div className="h-screen w-screen overflow-hidden bg-transparent">
{/* Pass handleComplete which invokes the backend finish_setup command
to mark setup as done, close this window, and show the main app. */}
<SetupWizard onComplete={handleComplete} />
</div>
)
}

View File

@@ -78,7 +78,7 @@ export function useSystemThemePreference(): boolean {
if (!hasEventListener) {
// Event listener not available, use polling fallback
checkInterval = setInterval(async () => {
checkInterval = window.setInterval(async () => {
const portalPrefersDark = await getSystemThemeFromPortal()
if (portalPrefersDark !== null) {
setSystemPrefersDark(portalPrefersDark)

View File

@@ -28,5 +28,10 @@ export default defineConfig({
minify: !process.env.TAURI_DEBUG ? 'esbuild' : false,
// Produce sourcemaps for debug builds
sourcemap: !!process.env.TAURI_DEBUG,
rollupOptions: {
input: {
main: 'index.html',
},
},
},
})