diff --git a/native/package.json b/native/package.json index 76828f5..beba36b 100644 --- a/native/package.json +++ b/native/package.json @@ -1,6 +1,6 @@ { "name": "kanbangit-native", - "version": "0.2.2", + "version": "0.2.3", "private": true, "type": "module", "scripts": { @@ -25,5 +25,5 @@ "devDependencies": { "@tauri-apps/cli": "2.11.4" }, - "releaseNotes": "Add the stable Mac download for the KanbanGit website." + "releaseNotes": "Open saved workspaces directly in the dashboard on macOS and quit cleanly when closing the workspace window." } diff --git a/native/src-tauri/Cargo.lock b/native/src-tauri/Cargo.lock index 96e1f76..7723ab3 100644 --- a/native/src-tauri/Cargo.lock +++ b/native/src-tauri/Cargo.lock @@ -1770,7 +1770,7 @@ dependencies = [ [[package]] name = "kanbangit-native" -version = "0.2.2" +version = "0.2.3" dependencies = [ "semver", "serde", diff --git a/native/src-tauri/Cargo.toml b/native/src-tauri/Cargo.toml index c81e06f..2f8edd1 100644 --- a/native/src-tauri/Cargo.toml +++ b/native/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "kanbangit-native" -version = "0.2.2" +version = "0.2.3" description = "KanbanGit native application" edition = "2021" publish = false diff --git a/native/src-tauri/gen/apple/kanbangit-native.xcodeproj/project.pbxproj b/native/src-tauri/gen/apple/kanbangit-native.xcodeproj/project.pbxproj index 7d6c1ee..c339003 100644 --- a/native/src-tauri/gen/apple/kanbangit-native.xcodeproj/project.pbxproj +++ b/native/src-tauri/gen/apple/kanbangit-native.xcodeproj/project.pbxproj @@ -416,7 +416,7 @@ CODE_SIGN_ENTITLEMENTS = "kanbangit-native_iOS/kanbangit-native_iOS.entitlements"; CODE_SIGN_IDENTITY = "Apple Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = ZZX2KZGTHU; ENABLE_BITCODE = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -432,7 +432,7 @@ ); "LIBRARY_SEARCH_PATHS[arch=arm64]" = "$(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)"; "LIBRARY_SEARCH_PATHS[arch=x86_64]" = "$(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)"; - MARKETING_VERSION = 0.2.1; + MARKETING_VERSION = 0.2.3; PRODUCT_BUNDLE_IDENTIFIER = de.kanbangit.ios; PRODUCT_MODULE_NAME = KanbanGitNative; PRODUCT_NAME = KanbanGit; @@ -593,7 +593,7 @@ CODE_SIGN_ENTITLEMENTS = "kanbangit-native_iOS/kanbangit-native_iOS.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = ZZX2KZGTHU; ENABLE_BITCODE = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -609,7 +609,7 @@ ); "LIBRARY_SEARCH_PATHS[arch=arm64]" = "$(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)"; "LIBRARY_SEARCH_PATHS[arch=x86_64]" = "$(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)"; - MARKETING_VERSION = 0.2.1; + MARKETING_VERSION = 0.2.3; PRODUCT_BUNDLE_IDENTIFIER = de.kanbangit.ios; PRODUCT_MODULE_NAME = KanbanGitNative; PRODUCT_NAME = KanbanGit; diff --git a/native/src-tauri/gen/apple/project.yml b/native/src-tauri/gen/apple/project.yml index 4eadb84..3213bfb 100644 --- a/native/src-tauri/gen/apple/project.yml +++ b/native/src-tauri/gen/apple/project.yml @@ -18,8 +18,8 @@ settingGroups: DEVELOPMENT_TEAM: ZZX2KZGTHU ENABLE_USER_SCRIPT_SANDBOXING: false PRODUCT_BUNDLE_IDENTIFIER: de.kanbangit.ios - MARKETING_VERSION: "0.2.1" - CURRENT_PROJECT_VERSION: "1" + MARKETING_VERSION: "0.2.3" + CURRENT_PROJECT_VERSION: "3" configs: debug: CODE_SIGN_IDENTITY: Apple Development diff --git a/native/src-tauri/src/desktop.rs b/native/src-tauri/src/desktop.rs index 393764b..8387925 100644 --- a/native/src-tauri/src/desktop.rs +++ b/native/src-tauri/src/desktop.rs @@ -229,22 +229,17 @@ fn open_workspace(app: &AppHandle, input: &str) -> Result<(), &'static str> { .map_err(|_| "connectionError")?; let handle = app.clone(); window.on_window_event(move |event| { - if matches!(event, WindowEvent::Destroyed) { - let state = handle.state::>(); - let current = { - let mut state = state.lock().unwrap(); - if state.label.as_deref() == Some(&label) { - state.label = None; - state.ready = false; - state.revision += 1; - state.phase = "idle"; - true - } else { - false - } - }; + if let WindowEvent::CloseRequested { api, .. } = event { + let current = handle + .state::>() + .lock() + .unwrap() + .label + .as_deref() + == Some(&label); if current { - show_launcher(&handle); + api.prevent_close(); + handle.exit(0); } } }); @@ -308,6 +303,7 @@ pub fn builder() -> tauri::Builder { Ok(None) => (None, "idle"), Err(_) => (None, "storageError"), }; + let startup_server = server.clone(); app.manage(Mutex::new(Connection { server, phase, @@ -392,6 +388,7 @@ pub fn builder() -> tauri::Builder { .title(en::APP_NAME) .inner_size(560.0, 720.0) .min_inner_size(375.0, 640.0) + .visible(false) .center() .on_navigation(move |destination| { if !allows_launcher_navigation(destination) { @@ -414,25 +411,15 @@ pub fn builder() -> tauri::Builder { update_launcher(&app); #[cfg(target_os = "macos")] crate::updates::refresh(&app); - std::thread::spawn(move || { - let handle = app.clone(); - let _ = app.run_on_main_thread(move || { - let server = handle - .state::>() - .lock() - .unwrap() - .server - .clone(); - if let Some(server) = server { - if let Err(error) = open_workspace(&handle, server.url.as_str()) - { - handle.state::>().lock().unwrap().phase = - error; - show_launcher(&handle); - } - } - }); - }); + let needs_setup = app + .state::>() + .lock() + .unwrap() + .server + .is_none(); + if needs_setup { + show_launcher(&app); + } } }) .build()?; @@ -440,9 +427,25 @@ pub fn builder() -> tauri::Builder { launcher.on_window_event(move |event| { if let WindowEvent::CloseRequested { api, .. } = event { api.prevent_close(); - let _ = window.hide(); + let has_workspace = window + .app_handle() + .state::>() + .lock() + .unwrap() + .ready; + if has_workspace { + let _ = window.hide(); + } else { + window.app_handle().exit(0); + } } }); + if let Some(server) = startup_server { + if let Err(error) = open_workspace(app.handle(), server.url.as_str()) { + app.state::>().lock().unwrap().phase = error; + show_launcher(app.handle()); + } + } Ok(()) }) } diff --git a/native/src-tauri/tauri.conf.json b/native/src-tauri/tauri.conf.json index d04b8fc..2a19ad2 100644 --- a/native/src-tauri/tauri.conf.json +++ b/native/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "KanbanGit", - "version": "0.2.2", + "version": "0.2.3", "identifier": "de.kanbangit.desktop", "build": { "frontendDist": "../ui" @@ -25,7 +25,7 @@ ], "macOS": { "minimumSystemVersion": "14.0", - "bundleVersion": "8", + "bundleVersion": "9", "dmg": { "windowSize": { "width": 660, diff --git a/native/src-tauri/tauri.ios.conf.json b/native/src-tauri/tauri.ios.conf.json index c8d2157..882cb3e 100644 --- a/native/src-tauri/tauri.ios.conf.json +++ b/native/src-tauri/tauri.ios.conf.json @@ -5,7 +5,7 @@ "iOS": { "minimumSystemVersion": "17.0", "developmentTeam": "ZZX2KZGTHU", - "bundleVersion": "2" + "bundleVersion": "3" } } } diff --git a/native/src-tauri/tests/macos_smoke.rs b/native/src-tauri/tests/macos_smoke.rs index 18aebe0..6a0f028 100644 --- a/native/src-tauri/tests/macos_smoke.rs +++ b/native/src-tauri/tests/macos_smoke.rs @@ -6,15 +6,26 @@ fn main() { }; use tauri::{webview::PageLoadEvent, Manager}; + let startup = std::env::var("KANBANGIT_SMOKE_STARTUP").unwrap_or_default(); + let online = startup == "online"; + let needs_setup = matches!(startup.as_str(), "missing" | "invalid"); let passed = Arc::new(AtomicBool::new(false)); let stage = Arc::new(AtomicUsize::new(0)); let page_stage = stage.clone(); let result_passed = passed.clone(); + let setup_passed = passed.clone(); let plugin = tauri::plugin::Builder::::new("macos-smoke") - .setup(|app, _| { + .setup(move |app, _| { let directory = app.path().app_config_dir()?; std::fs::create_dir_all(&directory)?; - std::fs::write(directory.join("active-server"), "https://127.0.0.1:9/\n0123456789abcdef0123456789abcdef")?; + if needs_setup { + if startup == "invalid" { + std::fs::write(directory.join("active-server"), "invalid configuration")?; + } + return Ok(()); + } + let server = if online { "https://kanbangit.de/" } else { "https://127.0.0.1:9/" }; + std::fs::write(directory.join("active-server"), format!("{server}\n0123456789abcdef0123456789abcdef"))?; Ok(()) }) .js_init_script(r#" @@ -27,7 +38,7 @@ fn main() { }).observe(document, { subtree: true, attributes: true, childList: true }); } "#) - .js_init_script("window.addEventListener('error', event => { location.href = 'tauri://localhost/__native_smoke_result?passed=false&error=' + encodeURIComponent(event.message || event.target.src); }, true);") + .js_init_script("window.addEventListener('error', event => { location.href = 'tauri://localhost/?smoke=result&passed=false&error=' + encodeURIComponent(event.message || event.target.src); }, true);") .on_page_load(move |webview, payload| { eprintln!("Smoke load: {} {:?} {}", webview.label(), payload.event(), payload.url()); @@ -35,6 +46,24 @@ fn main() { return; } if webview.label() == "launcher" { + if needs_setup { + let app = webview.app_handle().clone(); + let passed = setup_passed.clone(); + std::thread::spawn(move || { + std::thread::sleep(std::time::Duration::from_millis(200)); + let handle = app.clone(); + app.run_on_main_thread(move || { + let launcher = handle.get_webview_window("launcher").unwrap(); + assert!(launcher.is_visible().unwrap(), "First launch and invalid configuration must show setup"); + assert!(!handle.webview_windows().keys().any(|label| label.starts_with("workspace-"))); + passed.store(true, Ordering::SeqCst); + launcher.close().unwrap(); + }).unwrap(); + }); + return; + } + assert!(!webview.app_handle().get_webview_window("launcher").unwrap().is_visible().unwrap(), "Saved startup must not show the native launcher window"); + if online { return; } webview.eval(r#" (async () => { const wait = async predicate => { @@ -62,6 +91,7 @@ fn main() { } input.value = 'https://127.0.0.1:9/'; form.requestSubmit(); + await wait(() => status.dataset.state === 'connecting'); await wait(() => status.dataset.state === 'connectionError'); if (input.value !== 'https://127.0.0.1:9/' || document.querySelector('.open-workspace').disabled) throw new Error('Offline recovery failed'); input.value = 'https://kanbangit.de/'; @@ -72,7 +102,7 @@ fn main() { form.requestSubmit(); document.querySelector('#confirm-switch').click(); } catch (error) { - location.href = 'tauri://localhost/__native_smoke_result?passed=false&reason=' + encodeURIComponent(error.message); + location.href = 'tauri://localhost/?smoke=result&passed=false&reason=' + encodeURIComponent(error.message); } })(); "#).unwrap(); @@ -110,7 +140,7 @@ fn main() { .on_navigation(move |webview, url| { eprintln!("Smoke navigation: {} {}", webview.label(), url); - if url.path() != "/__native_smoke_result" { + if url.path() != "/__native_smoke_result" && !url.query_pairs().any(|(key, value)| key == "smoke" && value == "result") { return true; } let success = url.query_pairs().any(|(key, value)| key == "passed" && value == "true"); @@ -139,7 +169,17 @@ fn main() { }); } else { result_passed.store(success, Ordering::SeqCst); - webview.app_handle().exit(if success { 0 } else { 1 }); + if success { + let app = webview.app_handle().clone(); + let label = webview.label().to_string(); + let handle = app.clone(); + app.run_on_main_thread(move || { + assert!(!handle.get_webview_window("launcher").unwrap().is_visible().unwrap()); + handle.get_webview_window(&label).unwrap().close().unwrap(); + }).unwrap(); + } else { + webview.app_handle().exit(1); + } } false }) @@ -150,6 +190,25 @@ fn main() { .plugin(plugin) .build(context) .expect("The macOS application must start"); + let visibility_handle = app.handle().clone(); + let visibility_stage = stage.clone(); + let visibility_check = std::thread::spawn(move || { + if needs_setup { + return; + } + for _ in 0..30 { + if visibility_stage.load(Ordering::SeqCst) > 0 { + break; + } + if let Some(launcher) = visibility_handle.get_webview_window("launcher") { + assert!( + !launcher.is_visible().unwrap(), + "Launcher flashed while the workspace was loading" + ); + } + std::thread::sleep(std::time::Duration::from_millis(100)); + } + }); let config_dir = app.path().app_config_dir().unwrap(); let handle = app.handle().clone(); std::thread::spawn(move || { @@ -157,7 +216,28 @@ fn main() { eprintln!("macOS smoke test timed out"); handle.exit(1); }); - app.run_return(|_, _| {}); + let closed = Arc::new(AtomicBool::new(false)); + let closed_result = closed.clone(); + let exit_code = app.run_return(move |app, event| { + if let tauri::RunEvent::ExitRequested { code: Some(0), .. } = event { + closed_result.store(true, Ordering::SeqCst); + assert!( + needs_setup + || !app + .get_webview_window("launcher") + .unwrap() + .is_visible() + .unwrap(), + "Closing the workspace must not reopen settings" + ); + } + }); + visibility_check.join().unwrap(); + assert_eq!(exit_code, 0); + assert!( + closed.load(Ordering::SeqCst), + "The close button must quit the application" + ); let _ = std::fs::remove_dir_all(config_dir); assert!( passed.load(Ordering::SeqCst), diff --git a/native/tests/macos-release.test.js b/native/tests/macos-release.test.js index 76a1da6..fe043db 100644 --- a/native/tests/macos-release.test.js +++ b/native/tests/macos-release.test.js @@ -578,3 +578,46 @@ for (const change of [ assert.equal(existsSync(f.destination), false); }); } + +test('release versions agree across native manifests', () => { + const read = (path) => readFileSync(new URL(path, import.meta.url), 'utf8'); + const config = JSON.parse(read('../src-tauri/tauri.conf.json')); + const ios = JSON.parse(read('../src-tauri/tauri.ios.conf.json')); + const packageInfo = JSON.parse(read('../package.json')); + assert.equal(packageInfo.version, config.version); + assert.equal( + read('../src-tauri/Cargo.toml').match(/^version = "([^"]+)"$/m)?.[1], + config.version, + ); + assert.equal( + read('../src-tauri/Cargo.lock').match( + /name = "kanbangit-native"\nversion = "([^"]+)"/, + )?.[1], + config.version, + ); + assert.equal( + read('../src-tauri/gen/apple/project.yml').match( + /MARKETING_VERSION: "([^"]+)"/, + )?.[1], + config.version, + ); + assert.equal( + read('../src-tauri/gen/apple/project.yml').match( + /CURRENT_PROJECT_VERSION: "([^"]+)"/, + )?.[1], + ios.bundle.iOS.bundleVersion, + ); + const project = read( + '../src-tauri/gen/apple/kanbangit-native.xcodeproj/project.pbxproj', + ); + for (const [, version] of project.matchAll( + /MARKETING_VERSION = ([^;]+);/g, + )) { + assert.equal(version, config.version); + } + for (const [, version] of project.matchAll( + /CURRENT_PROJECT_VERSION = ([^;]+);/g, + )) { + assert.equal(version, ios.bundle.iOS.bundleVersion); + } +});