feat(tauri-app): expand tray menu + open_external_url + Profile/Search items
Tray menu now has:
Show maarcadetweet
Home
Compose
Profile
Search
----
Quit
The Home/Profile/Search items emit 'app://navigate' events
which the frontend's listenTrayEvents translates to view
switches. The compose and show events continue to be
separate event types ('app://compose', 'app://show').
open_external_url Tauri command takes a URL, validates it's
http(s), and uses tauri-plugin-shell to open it in the user's
default browser. The frontend's openExternalUrl falls back
to window.open in the browser preview (no Tauri runtime).
svelte-check error fix: tauriCall<T>(cmd, fallback, args?)
had the second call argument as 'undefined' instead of 'null'
on the call site for session.load(). The TypeScript compiler
correctly noted that the fallback type 'T' (here Session |
null) couldn't be undefined. Replaced with 'null' and
dropped the trailing null args argument (it's optional).
This commit is contained in:
Generated
+94
@@ -2435,6 +2435,25 @@ version = "2.12.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
|
checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "is-docker"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "is-wsl"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5"
|
||||||
|
dependencies = [
|
||||||
|
"is-docker",
|
||||||
|
"once_cell",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iterator-sorted"
|
name = "iterator-sorted"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -2735,6 +2754,7 @@ dependencies = [
|
|||||||
"tauri-plugin-dialog",
|
"tauri-plugin-dialog",
|
||||||
"tauri-plugin-keyring-store",
|
"tauri-plugin-keyring-store",
|
||||||
"tauri-plugin-notification",
|
"tauri-plugin-notification",
|
||||||
|
"tauri-plugin-shell",
|
||||||
"tauri-plugin-updater",
|
"tauri-plugin-updater",
|
||||||
"tauri-plugin-window-state",
|
"tauri-plugin-window-state",
|
||||||
"tokio",
|
"tokio",
|
||||||
@@ -3286,6 +3306,17 @@ version = "0.3.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "open"
|
||||||
|
version = "5.3.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cd8d3b65c44123a56e0133d2cd06ce4361bd3ca99d41198b2f25e3c3db9b8b4a"
|
||||||
|
dependencies = [
|
||||||
|
"dunce",
|
||||||
|
"is-wsl",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl"
|
name = "openssl"
|
||||||
version = "0.10.81"
|
version = "0.10.81"
|
||||||
@@ -3345,6 +3376,16 @@ dependencies = [
|
|||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "os_pipe"
|
||||||
|
version = "1.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "osakit"
|
name = "osakit"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
@@ -4526,12 +4567,44 @@ dependencies = [
|
|||||||
"lazy_static",
|
"lazy_static",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shared_child"
|
||||||
|
version = "1.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"sigchld",
|
||||||
|
"windows-sys 0.60.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shlex"
|
name = "shlex"
|
||||||
version = "2.0.1"
|
version = "2.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sigchld"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"os_pipe",
|
||||||
|
"signal-hook",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signal-hook"
|
||||||
|
version = "0.3.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"signal-hook-registry",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "signal-hook-registry"
|
name = "signal-hook-registry"
|
||||||
version = "1.4.8"
|
version = "1.4.8"
|
||||||
@@ -5087,6 +5160,27 @@ dependencies = [
|
|||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-shell"
|
||||||
|
version = "2.3.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8457dbf9e2bab1edd8df22bb2c20857a59a9868e79cb3eac5ed639eec4d0c73b"
|
||||||
|
dependencies = [
|
||||||
|
"encoding_rs",
|
||||||
|
"log",
|
||||||
|
"open",
|
||||||
|
"os_pipe",
|
||||||
|
"regex",
|
||||||
|
"schemars 0.8.22",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"shared_child",
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-updater"
|
name = "tauri-plugin-updater"
|
||||||
version = "2.10.1"
|
version = "2.10.1"
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ tauri-plugin-notification = "2"
|
|||||||
tauri-plugin-dialog = "2"
|
tauri-plugin-dialog = "2"
|
||||||
tauri-plugin-updater = "2"
|
tauri-plugin-updater = "2"
|
||||||
tauri-plugin-window-state = "2"
|
tauri-plugin-window-state = "2"
|
||||||
|
tauri-plugin-shell = "2"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
|||||||
@@ -444,6 +444,28 @@ async fn pick_and_upload_image(
|
|||||||
Ok(Some(resp))
|
Ok(Some(resp))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Open an external URL in the user's default browser. Returns
|
||||||
|
/// silently on success. Only accepts `http://` and `https://` —
|
||||||
|
/// any other scheme (e.g. `file://`, `mailto:`, `javascript:`)
|
||||||
|
/// is rejected so the Rust side is the single source of truth for
|
||||||
|
/// which schemes are allowed.
|
||||||
|
#[tauri::command]
|
||||||
|
async fn open_external_url(
|
||||||
|
url: String,
|
||||||
|
app: tauri::AppHandle,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
use tauri_plugin_shell::ShellExt;
|
||||||
|
let lower = url.to_ascii_lowercase();
|
||||||
|
if !(lower.starts_with("http://") || lower.starts_with("https://")) {
|
||||||
|
return Err(format!(
|
||||||
|
"open_external_url: refusing non-http(s) scheme in {url:?}"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
app.shell()
|
||||||
|
.open(url, None)
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
/// Map a file extension to a MIME type. Returns
|
/// Map a file extension to a MIME type. Returns
|
||||||
/// `application/octet-stream` when the extension is unrecognised —
|
/// `application/octet-stream` when the extension is unrecognised —
|
||||||
/// the PDS's magic-byte sniffer will take over from there.
|
/// the PDS's magic-byte sniffer will take over from there.
|
||||||
@@ -529,6 +551,7 @@ pub fn run() {
|
|||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_notification::init())
|
.plugin(tauri_plugin_notification::init())
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
|
.plugin(tauri_plugin_shell::init())
|
||||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||||
.plugin(tauri_plugin_window_state::Builder::default().build())
|
.plugin(tauri_plugin_window_state::Builder::default().build())
|
||||||
.manage(state)
|
.manage(state)
|
||||||
@@ -551,6 +574,14 @@ pub fn run() {
|
|||||||
None::<&str>,
|
None::<&str>,
|
||||||
)
|
)
|
||||||
.map_err(|e| format!("failed to build show menu item: {e}"))?;
|
.map_err(|e| format!("failed to build show menu item: {e}"))?;
|
||||||
|
let home_item = tauri::menu::MenuItem::with_id(
|
||||||
|
app,
|
||||||
|
"tray_home",
|
||||||
|
"Home",
|
||||||
|
true,
|
||||||
|
None::<&str>,
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("failed to build home menu item: {e}"))?;
|
||||||
let compose_item = tauri::menu::MenuItem::with_id(
|
let compose_item = tauri::menu::MenuItem::with_id(
|
||||||
app,
|
app,
|
||||||
"tray_compose",
|
"tray_compose",
|
||||||
@@ -559,6 +590,22 @@ pub fn run() {
|
|||||||
None::<&str>,
|
None::<&str>,
|
||||||
)
|
)
|
||||||
.map_err(|e| format!("failed to build compose menu item: {e}"))?;
|
.map_err(|e| format!("failed to build compose menu item: {e}"))?;
|
||||||
|
let profile_item = tauri::menu::MenuItem::with_id(
|
||||||
|
app,
|
||||||
|
"tray_profile",
|
||||||
|
"Profile",
|
||||||
|
true,
|
||||||
|
None::<&str>,
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("failed to build profile menu item: {e}"))?;
|
||||||
|
let search_item = tauri::menu::MenuItem::with_id(
|
||||||
|
app,
|
||||||
|
"tray_search",
|
||||||
|
"Search",
|
||||||
|
true,
|
||||||
|
None::<&str>,
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("failed to build search menu item: {e}"))?;
|
||||||
let quit_item = tauri::menu::MenuItem::with_id(
|
let quit_item = tauri::menu::MenuItem::with_id(
|
||||||
app,
|
app,
|
||||||
"tray_quit",
|
"tray_quit",
|
||||||
@@ -572,7 +619,15 @@ pub fn run() {
|
|||||||
|
|
||||||
let tray_menu = tauri::menu::Menu::with_items(
|
let tray_menu = tauri::menu::Menu::with_items(
|
||||||
app,
|
app,
|
||||||
&[&show_item, &compose_item, &separator, &quit_item],
|
&[
|
||||||
|
&show_item,
|
||||||
|
&home_item,
|
||||||
|
&compose_item,
|
||||||
|
&profile_item,
|
||||||
|
&search_item,
|
||||||
|
&separator,
|
||||||
|
&quit_item,
|
||||||
|
],
|
||||||
)
|
)
|
||||||
.map_err(|e| format!("failed to build tray menu: {e}"))?;
|
.map_err(|e| format!("failed to build tray menu: {e}"))?;
|
||||||
|
|
||||||
@@ -586,9 +641,18 @@ pub fn run() {
|
|||||||
"tray_show" => {
|
"tray_show" => {
|
||||||
let _ = tauri::Emitter::emit(app, "app://show", ());
|
let _ = tauri::Emitter::emit(app, "app://show", ());
|
||||||
}
|
}
|
||||||
|
"tray_home" => {
|
||||||
|
let _ = tauri::Emitter::emit(app, "app://navigate", "home");
|
||||||
|
}
|
||||||
"tray_compose" => {
|
"tray_compose" => {
|
||||||
let _ = tauri::Emitter::emit(app, "app://compose", ());
|
let _ = tauri::Emitter::emit(app, "app://compose", ());
|
||||||
}
|
}
|
||||||
|
"tray_profile" => {
|
||||||
|
let _ = tauri::Emitter::emit(app, "app://navigate", "profile");
|
||||||
|
}
|
||||||
|
"tray_search" => {
|
||||||
|
let _ = tauri::Emitter::emit(app, "app://navigate", "search");
|
||||||
|
}
|
||||||
"tray_quit" => {
|
"tray_quit" => {
|
||||||
app.exit(0);
|
app.exit(0);
|
||||||
}
|
}
|
||||||
@@ -634,6 +698,7 @@ pub fn run() {
|
|||||||
fetch_blob,
|
fetch_blob,
|
||||||
pick_and_upload_image,
|
pick_and_upload_image,
|
||||||
show_notification,
|
show_notification,
|
||||||
|
open_external_url,
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running maarcadetweet");
|
.expect("error while running maarcadetweet");
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ function createSessionStore() {
|
|||||||
return {
|
return {
|
||||||
subscribe,
|
subscribe,
|
||||||
async load() {
|
async load() {
|
||||||
const s = await tauriCall<Session | null>("current_session", undefined, null);
|
const s = await tauriCall<Session | null>("current_session", null);
|
||||||
set(s);
|
set(s);
|
||||||
},
|
},
|
||||||
async login(handle: string, password: string) {
|
async login(handle: string, password: string) {
|
||||||
@@ -355,18 +355,43 @@ export async function showNotification(
|
|||||||
/// The handler receives the event payload (empty object for these
|
/// The handler receives the event payload (empty object for these
|
||||||
/// cases). Returns an unsubscribe function.
|
/// cases). Returns an unsubscribe function.
|
||||||
export async function listenTrayEvents(
|
export async function listenTrayEvents(
|
||||||
handler: (event: "show" | "compose") => void,
|
handler: (event: "show" | "home" | "compose" | "profile" | "search") => void,
|
||||||
): Promise<() => void> {
|
): Promise<() => void> {
|
||||||
const { listen } = await import("@tauri-apps/api/event");
|
const { listen } = await import("@tauri-apps/api/event");
|
||||||
const unlisteners: Array<() => void> = [];
|
const unlisteners: Array<() => void> = [];
|
||||||
const u1 = await listen("app://show", () => handler("show"));
|
const u1 = await listen("app://show", () => handler("show"));
|
||||||
const u2 = await listen("app://compose", () => handler("compose"));
|
const u2 = await listen("app://navigate", (e) => {
|
||||||
unlisteners.push(u1, u2);
|
handler(e.payload as "home" | "profile" | "search");
|
||||||
|
});
|
||||||
|
const u3 = await listen("app://compose", () => handler("compose"));
|
||||||
|
unlisteners.push(u1, u2, u3);
|
||||||
return () => {
|
return () => {
|
||||||
for (const u of unlisteners) u();
|
for (const u of unlisteners) u();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Open an external URL in the user's default browser. The Rust
|
||||||
|
/// `open_external_url` command enforces http(s) only; in the
|
||||||
|
/// browser preview where no Tauri runtime is present, fall back
|
||||||
|
/// to `window.open` and treat a popup-blocker denial as
|
||||||
|
/// "fine, user can copy the URL themselves".
|
||||||
|
export async function openExternalUrl(url: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
if (isTauri()) {
|
||||||
|
await safeInvoke("open_external_url", { url });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("open_external_url failed", e);
|
||||||
|
}
|
||||||
|
// Browser fallback (vite dev preview).
|
||||||
|
try {
|
||||||
|
window.open(url, "_blank", "noopener,noreferrer");
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("window.open failed (popup blocker?)", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Subscribe to `app://notification` events emitted by the Rust
|
/// Subscribe to `app://notification` events emitted by the Rust
|
||||||
/// `show_notification` command. Used to focus the window and
|
/// `show_notification` command. Used to focus the window and
|
||||||
/// navigate when the user clicks the notification.
|
/// navigate when the user clicks the notification.
|
||||||
|
|||||||
Reference in New Issue
Block a user