mirror of
https://github.com/gustavosett/Windows-11-Clipboard-History-For-Linux
synced 2026-04-25 17:15:35 +02:00
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:
18
package-lock.json
generated
18
package-lock.json
generated
@@ -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
1
package.json
generated
@@ -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",
|
||||
|
||||
@@ -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" "$@"
|
||||
|
||||
@@ -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}"
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
28
src-tauri/tauri.conf.json
generated
28
src-tauri/tauri.conf.json
generated
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'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}`}
|
||||
|
||||
98
src/main.tsx
98
src/main.tsx
@@ -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
27
src/setup.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -4,7 +4,7 @@ import react from '@vitejs/plugin-react'
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
|
||||
|
||||
// Tauri expects a fixed port for development
|
||||
server: {
|
||||
port: 1420,
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user