KAN-51: Add Tauri scaffold and macOS development build #147
@@ -43,3 +43,7 @@ storage/framework/sessions/*
|
|||||||
storage/framework/testing/*
|
storage/framework/testing/*
|
||||||
storage/framework/views/*
|
storage/framework/views/*
|
||||||
storage/logs/*
|
storage/logs/*
|
||||||
|
|
||||||
|
# Native build artifacts are independent of the Laravel image.
|
||||||
|
native/src-tauri/target
|
||||||
|
native/src-tauri/gen
|
||||||
|
|||||||
@@ -28,3 +28,7 @@ yarn-error.log
|
|||||||
/.vscode
|
/.vscode
|
||||||
/.zed
|
/.zed
|
||||||
.codex/config.toml
|
.codex/config.toml
|
||||||
|
|
||||||
|
/native/node_modules
|
||||||
|
/native/src-tauri/target
|
||||||
|
/native/src-tauri/gen
|
||||||
|
|||||||
+3
-1
@@ -27,7 +27,9 @@ const eslintConfig = [
|
|||||||
ignores: [
|
ignores: [
|
||||||
'bootstrap/ssr/**',
|
'bootstrap/ssr/**',
|
||||||
'**/dist/**',
|
'**/dist/**',
|
||||||
'node_modules/**',
|
'**/node_modules/**',
|
||||||
|
'native/src-tauri/target/**',
|
||||||
|
'native/src-tauri/gen/**',
|
||||||
'.github/**',
|
'.github/**',
|
||||||
'**/*.yml',
|
'**/*.yml',
|
||||||
'**/*.yaml',
|
'**/*.yaml',
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "kanbangit-native",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "tauri dev",
|
||||||
|
"build": "tauri build",
|
||||||
|
"build:macos": "tauri build --debug --bundles app",
|
||||||
|
"test": "node --test tests/*.test.js && cargo test --manifest-path src-tauri/Cargo.toml --locked",
|
||||||
|
"check": "cargo fmt --manifest-path src-tauri/Cargo.toml --check && cargo clippy --manifest-path src-tauri/Cargo.toml --locked --all-targets --all-features -- -D warnings",
|
||||||
|
"smoke:macos": "cargo test --manifest-path src-tauri/Cargo.toml --locked --features smoke-test,custom-protocol --test macos_smoke"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tauri-apps/cli": "2.11.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+140
@@ -0,0 +1,140 @@
|
|||||||
|
lockfileVersion: '9.0'
|
||||||
|
|
||||||
|
settings:
|
||||||
|
autoInstallPeers: true
|
||||||
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
|
importers:
|
||||||
|
|
||||||
|
.:
|
||||||
|
devDependencies:
|
||||||
|
'@tauri-apps/cli':
|
||||||
|
specifier: 2.11.4
|
||||||
|
version: 2.11.4
|
||||||
|
|
||||||
|
packages:
|
||||||
|
|
||||||
|
'@tauri-apps/cli-darwin-arm64@2.11.4':
|
||||||
|
resolution: {integrity: sha512-1ryOF3ZhpZ/nemHV5zVwBQBz9jDGKmKPvWPADOhc83ig0P4bMc2iER4NbC6r9sjeIZ6RVQ4g3RZIYvezhcl4TQ==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@tauri-apps/cli-darwin-x64@2.11.4':
|
||||||
|
resolution: {integrity: sha512-uFsGQAAfuyz1k/yGLmkWfkBlgKAqZfxqlHmLWx81QU27RJWfmbNHCIq8T8w1e+VClleIuZUjpHWfoE4E3DLo3A==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@tauri-apps/cli-linux-arm-gnueabihf@2.11.4':
|
||||||
|
resolution: {integrity: sha512-IaHZn5CdBL21oUmjiVOS1ctw6Ip1O0pjp70FwOWmYz1myWe0SY96ZIj2FYf7pT0m8bI2h/hrs5ZbEXXh44/MkQ==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@tauri-apps/cli-linux-arm64-gnu@2.11.4':
|
||||||
|
resolution: {integrity: sha512-N41/ukTRVe6XSuUTESuFdGeOW2i7k62tK+6gHK5Kd5/q5RPvvi19GaWAVPPb9u95HSGmTChSolBfzynUsssFaA==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@tauri-apps/cli-linux-arm64-musl@2.11.4':
|
||||||
|
resolution: {integrity: sha512-v277UnT/fB64xAfSroL5N3Km3tLmvATWqJJw/wRI+g6o+HkeD0slyE7gOhNs1MbjE41R7bQOTxMVoL3aomUJmw==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
|
'@tauri-apps/cli-linux-riscv64-gnu@2.11.4':
|
||||||
|
resolution: {integrity: sha512-qqgNkQ2u1yZHxjhxsZaxUtRDW8dIqIYm33rx/mzwQv0SfY9x1B+iraj8vWeFiXjjSVVhEMepXSOts1TqPzvXNQ==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [riscv64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@tauri-apps/cli-linux-x64-gnu@2.11.4':
|
||||||
|
resolution: {integrity: sha512-2VRNWl84FOH0m2giiDkO2h0QXlcMJeX+zJDpI5kDIQAx6s+geF3v48F4DXfJez4GS/FdoDGnPnw1C2iYGbQ7bQ==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@tauri-apps/cli-linux-x64-musl@2.11.4':
|
||||||
|
resolution: {integrity: sha512-o9GyhYor/nc7xarmwDE3ka2szuW3uuZzXjHWh64Q8YX5AtSgxdQkFWzrY4O8KiGtVNvFBI14H3Q49Qj5TOIP/A==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
|
'@tauri-apps/cli-win32-arm64-msvc@2.11.4':
|
||||||
|
resolution: {integrity: sha512-ld5Ehb598m0VkYyylRPNeCFsBe/km0jxis6KgMpl3IGY6I/i1RwQXO05I1AsXUXO2WC6AvB/Lw4qTf/asiuEiQ==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@tauri-apps/cli-win32-ia32-msvc@2.11.4':
|
||||||
|
resolution: {integrity: sha512-12Hxi0XX/H5VFxO/bGgHkFWhml9VMgEOu9CidjeCeTNQ1l6fpUlbiGgSP7CLI3PFtW9/FfbeHieZ+kyWK5H7CA==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [ia32]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@tauri-apps/cli-win32-x64-msvc@2.11.4':
|
||||||
|
resolution: {integrity: sha512-+vDiqBIU5dMISg/wNvX3sF+ZHfgJGJ5T0AcO+EHNXV9GGAG+P5fzodlDXD3QdKCRgZxMoCm5PPvj3BqLNjBthw==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@tauri-apps/cli@2.11.4':
|
||||||
|
resolution: {integrity: sha512-R8xGtMpwyetawSqm9kYOuMmEqkhUbvcUy8n0aNXIxollKBLESUu5f4Fx+64hgASYm1H+jSWq6jCW6zqTnH6hqQ==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
snapshots:
|
||||||
|
|
||||||
|
'@tauri-apps/cli-darwin-arm64@2.11.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@tauri-apps/cli-darwin-x64@2.11.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@tauri-apps/cli-linux-arm-gnueabihf@2.11.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@tauri-apps/cli-linux-arm64-gnu@2.11.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@tauri-apps/cli-linux-arm64-musl@2.11.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@tauri-apps/cli-linux-riscv64-gnu@2.11.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@tauri-apps/cli-linux-x64-gnu@2.11.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@tauri-apps/cli-linux-x64-musl@2.11.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@tauri-apps/cli-win32-arm64-msvc@2.11.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@tauri-apps/cli-win32-ia32-msvc@2.11.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@tauri-apps/cli-win32-x64-msvc@2.11.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@tauri-apps/cli@2.11.4':
|
||||||
|
optionalDependencies:
|
||||||
|
'@tauri-apps/cli-darwin-arm64': 2.11.4
|
||||||
|
'@tauri-apps/cli-darwin-x64': 2.11.4
|
||||||
|
'@tauri-apps/cli-linux-arm-gnueabihf': 2.11.4
|
||||||
|
'@tauri-apps/cli-linux-arm64-gnu': 2.11.4
|
||||||
|
'@tauri-apps/cli-linux-arm64-musl': 2.11.4
|
||||||
|
'@tauri-apps/cli-linux-riscv64-gnu': 2.11.4
|
||||||
|
'@tauri-apps/cli-linux-x64-gnu': 2.11.4
|
||||||
|
'@tauri-apps/cli-linux-x64-musl': 2.11.4
|
||||||
|
'@tauri-apps/cli-win32-arm64-msvc': 2.11.4
|
||||||
|
'@tauri-apps/cli-win32-ia32-msvc': 2.11.4
|
||||||
|
'@tauri-apps/cli-win32-x64-msvc': 2.11.4
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
minimumReleaseAge: 10080
|
||||||
Generated
+4509
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
|||||||
|
[package]
|
||||||
|
name = "kanbangit-native"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "KanbanGit native application"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "kanbangit_native"
|
||||||
|
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
tauri-build = { version = "2", features = [] }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tauri = { version = "2", features = [] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tauri = { version = "2", features = ["test"] }
|
||||||
|
serde_json = "1"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
smoke-test = []
|
||||||
|
custom-protocol = ["tauri/custom-protocol"]
|
||||||
|
|
||||||
|
[[test]]
|
||||||
|
name = "macos_smoke"
|
||||||
|
harness = false
|
||||||
|
required-features = ["smoke-test"]
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
tauri_build::build();
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 5.9 KiB |
@@ -0,0 +1,214 @@
|
|||||||
|
use tauri::{
|
||||||
|
webview::{NewWindowResponse, PageLoadEvent},
|
||||||
|
AppHandle, Manager, Url, WebviewUrl, WebviewWindowBuilder, WindowEvent,
|
||||||
|
};
|
||||||
|
|
||||||
|
const SERVER_URL: &str = "https://kanbangit.de/";
|
||||||
|
|
||||||
|
fn allows_server_navigation(server: &Url, destination: &Url) -> bool {
|
||||||
|
destination.scheme() == "https"
|
||||||
|
&& destination.origin() == server.origin()
|
||||||
|
&& destination.username().is_empty()
|
||||||
|
&& destination.password().is_none()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn allows_launcher_navigation(destination: &Url) -> bool {
|
||||||
|
destination.scheme() == "tauri"
|
||||||
|
&& destination.host_str() == Some("localhost")
|
||||||
|
&& destination.port().is_none()
|
||||||
|
&& destination.username().is_empty()
|
||||||
|
&& destination.password().is_none()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_launcher(app: &AppHandle) {
|
||||||
|
if let Some(window) = app.get_webview_window("launcher") {
|
||||||
|
let _ = window.show();
|
||||||
|
let _ = window.set_focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_workspace(app: &AppHandle) -> tauri::Result<()> {
|
||||||
|
if let Some(window) = app.get_webview_window("workspace") {
|
||||||
|
window.show()?;
|
||||||
|
window.set_focus()?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let server = Url::parse(SERVER_URL).expect("The built-in server URL must be valid");
|
||||||
|
let window = WebviewWindowBuilder::new(app, "workspace", WebviewUrl::External(server.clone()))
|
||||||
|
.title("KanbanGit")
|
||||||
|
.inner_size(1280.0, 850.0)
|
||||||
|
.min_inner_size(375.0, 600.0)
|
||||||
|
.center()
|
||||||
|
.on_navigation(move |destination| allows_server_navigation(&server, destination))
|
||||||
|
.on_new_window(|_, _| NewWindowResponse::Deny)
|
||||||
|
.on_page_load(|window, payload| {
|
||||||
|
if payload.event() == PageLoadEvent::Finished {
|
||||||
|
if let Some(launcher) = window.app_handle().get_webview_window("launcher") {
|
||||||
|
let _ = launcher.hide();
|
||||||
|
}
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
eprintln!(
|
||||||
|
"KanbanGit workspace page loaded: {}",
|
||||||
|
payload.url().origin().ascii_serialization()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build()?;
|
||||||
|
|
||||||
|
let handle = app.clone();
|
||||||
|
window.on_window_event(move |event| {
|
||||||
|
if matches!(event, WindowEvent::Destroyed) {
|
||||||
|
show_launcher(&handle);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn context<R: tauri::Runtime>() -> tauri::Context<R> {
|
||||||
|
tauri::generate_context!()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn builder() -> tauri::Builder<tauri::Wry> {
|
||||||
|
tauri::Builder::default().setup(|app| {
|
||||||
|
let handle = app.handle().clone();
|
||||||
|
WebviewWindowBuilder::new(app, "launcher", WebviewUrl::App("index.html".into()))
|
||||||
|
.title("KanbanGit")
|
||||||
|
.inner_size(560.0, 520.0)
|
||||||
|
.min_inner_size(375.0, 480.0)
|
||||||
|
.center()
|
||||||
|
.on_navigation(move |destination| {
|
||||||
|
if allows_launcher_navigation(destination) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if destination.as_str() == SERVER_URL {
|
||||||
|
let handle = handle.clone();
|
||||||
|
// Defer window creation until the navigation callback has returned.
|
||||||
|
let _ = handle.clone().run_on_main_thread(move || {
|
||||||
|
if let Err(error) = open_workspace(&handle) {
|
||||||
|
eprintln!("Could not open the workspace: {error}");
|
||||||
|
show_launcher(&handle);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
false
|
||||||
|
})
|
||||||
|
.on_new_window(|_, _| NewWindowResponse::Deny)
|
||||||
|
.build()?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
|
pub fn run() {
|
||||||
|
builder().run(context()).expect("Could not start KanbanGit");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn server_allows_application_routes_on_the_same_https_origin() {
|
||||||
|
let server = Url::parse(SERVER_URL).unwrap();
|
||||||
|
for path in [
|
||||||
|
"/",
|
||||||
|
"/login",
|
||||||
|
"/two-factor-challenge",
|
||||||
|
"/projects/1?ticket=51#details",
|
||||||
|
] {
|
||||||
|
assert!(allows_server_navigation(
|
||||||
|
&server,
|
||||||
|
&server.join(path).unwrap()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn server_rejects_origin_changes_downgrades_credentials_and_local_content() {
|
||||||
|
let server = Url::parse(SERVER_URL).unwrap();
|
||||||
|
for destination in [
|
||||||
|
"http://kanbangit.de/",
|
||||||
|
"https://kanbangit.de:444/",
|
||||||
|
"https://kanbangit.de.example.com/",
|
||||||
|
"https://example.com/",
|
||||||
|
"https://user:password@kanbangit.de/",
|
||||||
|
"https://user@kanbangit.de/",
|
||||||
|
"tauri://localhost/index.html",
|
||||||
|
"file:///etc/passwd",
|
||||||
|
"data:text/html,hello",
|
||||||
|
"javascript:alert(1)",
|
||||||
|
] {
|
||||||
|
assert!(
|
||||||
|
!allows_server_navigation(&server, &Url::parse(destination).unwrap()),
|
||||||
|
"{destination}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn launcher_only_loads_bundled_pages() {
|
||||||
|
assert!(allows_launcher_navigation(
|
||||||
|
&Url::parse("tauri://localhost/index.html").unwrap()
|
||||||
|
));
|
||||||
|
for destination in [
|
||||||
|
SERVER_URL,
|
||||||
|
"https://localhost/",
|
||||||
|
"http://localhost:1420/",
|
||||||
|
"tauri://other/index.html",
|
||||||
|
"tauri://user@localhost/index.html",
|
||||||
|
] {
|
||||||
|
assert!(!allows_launcher_navigation(
|
||||||
|
&Url::parse(destination).unwrap()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn remote_pages_cannot_invoke_native_window_webview_or_path_commands() {
|
||||||
|
let app = tauri::test::mock_builder().build(context()).unwrap();
|
||||||
|
let window = WebviewWindowBuilder::new(
|
||||||
|
&app,
|
||||||
|
"workspace",
|
||||||
|
WebviewUrl::External(Url::parse(SERVER_URL).unwrap()),
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
for command in [
|
||||||
|
"plugin:window|close",
|
||||||
|
"plugin:webview|create_webview_window",
|
||||||
|
"plugin:path|resolve_directory",
|
||||||
|
] {
|
||||||
|
let response = tauri::test::get_ipc_response(
|
||||||
|
&window,
|
||||||
|
tauri::webview::InvokeRequest {
|
||||||
|
cmd: command.into(),
|
||||||
|
callback: tauri::ipc::CallbackFn(0),
|
||||||
|
error: tauri::ipc::CallbackFn(1),
|
||||||
|
url: Url::parse(SERVER_URL).unwrap(),
|
||||||
|
body: tauri::ipc::InvokeBody::default(),
|
||||||
|
headers: Default::default(),
|
||||||
|
invoke_key: tauri::test::INVOKE_KEY.into(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let error = response.expect_err("Remote native commands must be rejected");
|
||||||
|
assert!(error.as_str().unwrap().contains("not allowed"), "{error}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn packaged_app_exposes_no_native_capabilities_or_global_api() {
|
||||||
|
let config: serde_json::Value =
|
||||||
|
serde_json::from_str(include_str!("../tauri.conf.json")).unwrap();
|
||||||
|
assert_eq!(config["app"]["withGlobalTauri"], false);
|
||||||
|
assert_eq!(
|
||||||
|
config["app"]["security"]["capabilities"],
|
||||||
|
serde_json::json!([])
|
||||||
|
);
|
||||||
|
let capabilities = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("capabilities");
|
||||||
|
assert!(
|
||||||
|
!capabilities.exists() || std::fs::read_dir(capabilities).unwrap().next().is_none()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
kanbangit_native::run();
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
|
"productName": "KanbanGit",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"identifier": "de.kanbangit.desktop",
|
||||||
|
"build": {
|
||||||
|
"frontendDist": "../ui"
|
||||||
|
},
|
||||||
|
"app": {
|
||||||
|
"windows": [],
|
||||||
|
"withGlobalTauri": false,
|
||||||
|
"security": {
|
||||||
|
"csp": "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self'; connect-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'; frame-src 'none'",
|
||||||
|
"capabilities": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bundle": {
|
||||||
|
"active": true,
|
||||||
|
"targets": ["app"],
|
||||||
|
"icon": ["icons/icon.png", "icons/icon.icns"],
|
||||||
|
"macOS": {
|
||||||
|
"minimumSystemVersion": "11.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
fn main() {
|
||||||
|
use std::sync::{
|
||||||
|
atomic::{AtomicBool, Ordering},
|
||||||
|
Arc,
|
||||||
|
};
|
||||||
|
use tauri::{webview::PageLoadEvent, Manager};
|
||||||
|
|
||||||
|
let complete = Arc::new(AtomicBool::new(false));
|
||||||
|
let passed = Arc::new(AtomicBool::new(false));
|
||||||
|
let navigation_complete = complete.clone();
|
||||||
|
let navigation_passed = passed.clone();
|
||||||
|
let plugin = tauri::plugin::Builder::<tauri::Wry>::new("macos-smoke")
|
||||||
|
.js_init_script("window.__nativeSmokeErrors = []; window.addEventListener('error', event => window.__nativeSmokeErrors.push(event.message || event.target.src), true);")
|
||||||
|
.on_page_load(|webview, payload| {
|
||||||
|
if payload.event() != PageLoadEvent::Finished {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if webview.label() == "launcher" {
|
||||||
|
webview.eval(r#"
|
||||||
|
(async () => {
|
||||||
|
for (let attempt = 0; attempt < 100; attempt++) {
|
||||||
|
if (document.querySelector('.open-workspace')?.textContent === 'Open workspace') break;
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
}
|
||||||
|
const link = document.querySelector('.open-workspace');
|
||||||
|
const appearance = document.querySelector('#appearance');
|
||||||
|
const translated = link?.textContent === 'Open workspace';
|
||||||
|
appearance.value = 'dark';
|
||||||
|
appearance.dispatchEvent(new Event('change'));
|
||||||
|
const dark = getComputedStyle(document.documentElement).backgroundColor === 'rgb(15, 23, 42)';
|
||||||
|
appearance.value = 'light';
|
||||||
|
appearance.dispatchEvent(new Event('change'));
|
||||||
|
const light = getComputedStyle(document.documentElement).backgroundColor === 'rgb(247, 249, 251)';
|
||||||
|
appearance.value = 'system';
|
||||||
|
appearance.dispatchEvent(new Event('change'));
|
||||||
|
if (translated && dark && light) {
|
||||||
|
link.click();
|
||||||
|
} else {
|
||||||
|
location.href = 'tauri://localhost/__native_smoke_result?passed=false&stage=launcher&translated=' + translated + '&dark=' + dark + '&light=' + light + '&background=' + encodeURIComponent(getComputedStyle(document.documentElement).backgroundColor) + '&label=' + encodeURIComponent(link?.textContent) + '&errors=' + encodeURIComponent(JSON.stringify(window.__nativeSmokeErrors));
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
"#).unwrap();
|
||||||
|
} else if webview.label() == "workspace" {
|
||||||
|
webview.eval(r#"
|
||||||
|
(async () => {
|
||||||
|
for (let attempt = 0; attempt < 100; attempt++) {
|
||||||
|
if (document.querySelector('#app')?.innerText.trim().length > 100) break;
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
}
|
||||||
|
const root = document.querySelector('#app');
|
||||||
|
const rendered = root?.innerText.trim().length > 100 && root.getBoundingClientRect().height > 0;
|
||||||
|
let denied = true;
|
||||||
|
for (const cmd of ['plugin:path|resolve_directory', 'plugin:window|get_all_windows']) {
|
||||||
|
try {
|
||||||
|
await window.__TAURI_INTERNALS__.invoke(cmd);
|
||||||
|
denied = false;
|
||||||
|
} catch (error) {
|
||||||
|
denied = denied && String(error).includes('not allowed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const noGlobalApi = typeof window.__TAURI__ === 'undefined';
|
||||||
|
const report = new URL('/__native_smoke_result', location.origin);
|
||||||
|
report.searchParams.set('passed', String(rendered && denied && noGlobalApi));
|
||||||
|
report.searchParams.set('rendered', String(rendered));
|
||||||
|
report.searchParams.set('nativeDenied', String(denied));
|
||||||
|
report.searchParams.set('globalAbsent', String(noGlobalApi));
|
||||||
|
location.href = report.href;
|
||||||
|
})();
|
||||||
|
"#).unwrap();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on_navigation(move |webview, url| {
|
||||||
|
if url.path() != "/__native_smoke_result" {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
let success = url.query_pairs().any(|(key, value)| key == "passed" && value == "true");
|
||||||
|
println!("macOS WebView smoke test: {}", url.query().unwrap_or_default());
|
||||||
|
navigation_passed.store(success, Ordering::SeqCst);
|
||||||
|
navigation_complete.store(true, Ordering::SeqCst);
|
||||||
|
webview.app_handle().exit(if success { 0 } else { 1 });
|
||||||
|
false
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
let app = kanbangit_native::builder()
|
||||||
|
.plugin(plugin)
|
||||||
|
.build(kanbangit_native::context())
|
||||||
|
.expect("The macOS application must start");
|
||||||
|
let handle = app.handle().clone();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
std::thread::sleep(std::time::Duration::from_secs(30));
|
||||||
|
if !complete.load(Ordering::SeqCst) {
|
||||||
|
eprintln!("macOS smoke test timed out before the server UI rendered");
|
||||||
|
handle.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
app.run_return(|_, _| {});
|
||||||
|
assert!(
|
||||||
|
passed.load(Ordering::SeqCst),
|
||||||
|
"The native smoke test must complete successfully"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
fn main() {
|
||||||
|
panic!("Run this smoke test on macOS with an active graphical session");
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
import test from 'node:test';
|
||||||
|
import { en } from '../ui/locales/en.js';
|
||||||
|
|
||||||
|
const html = readFileSync(new URL('../ui/index.html', import.meta.url), 'utf8');
|
||||||
|
|
||||||
|
test('every launcher text resolves to a nonempty English translation', () => {
|
||||||
|
for (const [, key] of html.matchAll(/data-i18n="([^"]+)"/g)) {
|
||||||
|
assert.equal(typeof en[key], 'string', key);
|
||||||
|
assert.ok(en[key].trim(), key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the launcher opens only the configured HTTPS workspace without an IPC bridge', () => {
|
||||||
|
const links = [...html.matchAll(/<a\s[^>]*href="([^"]+)"/g)];
|
||||||
|
assert.equal(links.length, 1);
|
||||||
|
assert.equal(links[0][1], 'https://kanbangit.de/');
|
||||||
|
const main = readFileSync(
|
||||||
|
new URL('../ui/main.js', import.meta.url),
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
assert.doesNotMatch(main, /__TAURI__|invoke\(/);
|
||||||
|
});
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta name="color-scheme" content="light dark" />
|
||||||
|
<title>KanbanGit</title>
|
||||||
|
<link rel="stylesheet" href="./styles.css" />
|
||||||
|
<script type="module" src="./main.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<img class="brand" src="./mark.svg" alt="" width="48" height="48" />
|
||||||
|
<p class="eyebrow" data-i18n="appName"></p>
|
||||||
|
<h1 data-i18n="title"></h1>
|
||||||
|
<p class="description" data-i18n="description"></p>
|
||||||
|
<a class="open-workspace" href="https://kanbangit.de/" data-i18n="openWorkspace"></a>
|
||||||
|
<p class="server">kanbangit.de</p>
|
||||||
|
<label class="appearance">
|
||||||
|
<span data-i18n="appearance"></span>
|
||||||
|
<select id="appearance">
|
||||||
|
<option value="system" data-i18n="system"></option>
|
||||||
|
<option value="light" data-i18n="light"></option>
|
||||||
|
<option value="dark" data-i18n="dark"></option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
export const en = {
|
||||||
|
appName: 'KanbanGit',
|
||||||
|
title: 'Your workspace, one click away.',
|
||||||
|
description: 'Open your projects and keep work moving.',
|
||||||
|
openWorkspace: 'Open workspace',
|
||||||
|
appearance: 'Appearance',
|
||||||
|
system: 'System',
|
||||||
|
light: 'Light',
|
||||||
|
dark: 'Dark',
|
||||||
|
};
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { en } from './locales/en.js';
|
||||||
|
|
||||||
|
for (const element of document.querySelectorAll('[data-i18n]')) {
|
||||||
|
element.textContent = en[element.dataset.i18n];
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelector('#appearance').addEventListener('change', (event) => {
|
||||||
|
document.documentElement.dataset.appearance = event.target.value;
|
||||||
|
});
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 64 64">
|
||||||
|
<rect width="64" height="64" rx="12" fill="#22c55e" />
|
||||||
|
<g fill="none" stroke="#004b1e" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect x="16" y="16" width="32" height="32" rx="3" />
|
||||||
|
<path d="M27 16v32M37 16v32" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 363 B |
@@ -0,0 +1,127 @@
|
|||||||
|
:root {
|
||||||
|
--surface: #f7f9fb;
|
||||||
|
--foreground: #191c1e;
|
||||||
|
--muted: #505f76;
|
||||||
|
--primary: #006e2f;
|
||||||
|
--border: #bccbb9;
|
||||||
|
--control: #ffffff;
|
||||||
|
font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
|
color-scheme: light dark;
|
||||||
|
color: var(--foreground);
|
||||||
|
background: var(--surface);
|
||||||
|
font-synthesis: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-appearance='light'] {
|
||||||
|
color-scheme: light;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-appearance='dark'] {
|
||||||
|
color-scheme: dark;
|
||||||
|
--surface: #0f172a;
|
||||||
|
--foreground: #f8fafc;
|
||||||
|
--muted: #b7c8e1;
|
||||||
|
--primary: #4ae176;
|
||||||
|
--border: #334155;
|
||||||
|
--control: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root:not([data-appearance='light']) {
|
||||||
|
--surface: #0f172a;
|
||||||
|
--foreground: #f8fafc;
|
||||||
|
--muted: #b7c8e1;
|
||||||
|
--primary: #4ae176;
|
||||||
|
--border: #334155;
|
||||||
|
--control: #1e293b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
width: min(100%, 380px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eyebrow {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin: 12px 0;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.2;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description,
|
||||||
|
.server,
|
||||||
|
.appearance {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.open-workspace {
|
||||||
|
display: block;
|
||||||
|
margin-top: 28px;
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #004b1e;
|
||||||
|
background: #22c55e;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.open-workspace:hover {
|
||||||
|
background: #4ae176;
|
||||||
|
}
|
||||||
|
|
||||||
|
:focus-visible {
|
||||||
|
outline: 2px solid var(--primary);
|
||||||
|
outline-offset: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.appearance {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
min-height: 36px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
font: inherit;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: inherit;
|
||||||
|
background: var(--control);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user