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"
|
||||
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]]
|
||||
name = "iterator-sorted"
|
||||
version = "0.1.0"
|
||||
@@ -2735,6 +2754,7 @@ dependencies = [
|
||||
"tauri-plugin-dialog",
|
||||
"tauri-plugin-keyring-store",
|
||||
"tauri-plugin-notification",
|
||||
"tauri-plugin-shell",
|
||||
"tauri-plugin-updater",
|
||||
"tauri-plugin-window-state",
|
||||
"tokio",
|
||||
@@ -3286,6 +3306,17 @@ version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "openssl"
|
||||
version = "0.10.81"
|
||||
@@ -3345,6 +3376,16 @@ dependencies = [
|
||||
"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]]
|
||||
name = "osakit"
|
||||
version = "0.3.1"
|
||||
@@ -4526,12 +4567,44 @@ dependencies = [
|
||||
"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]]
|
||||
name = "shlex"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.8"
|
||||
@@ -5087,6 +5160,27 @@ dependencies = [
|
||||
"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]]
|
||||
name = "tauri-plugin-updater"
|
||||
version = "2.10.1"
|
||||
|
||||
@@ -22,6 +22,7 @@ tauri-plugin-notification = "2"
|
||||
tauri-plugin-dialog = "2"
|
||||
tauri-plugin-updater = "2"
|
||||
tauri-plugin-window-state = "2"
|
||||
tauri-plugin-shell = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
|
||||
@@ -444,6 +444,28 @@ async fn pick_and_upload_image(
|
||||
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
|
||||
/// `application/octet-stream` when the extension is unrecognised —
|
||||
/// the PDS's magic-byte sniffer will take over from there.
|
||||
@@ -529,6 +551,7 @@ pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_notification::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
.plugin(tauri_plugin_window_state::Builder::default().build())
|
||||
.manage(state)
|
||||
@@ -551,6 +574,14 @@ pub fn run() {
|
||||
None::<&str>,
|
||||
)
|
||||
.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(
|
||||
app,
|
||||
"tray_compose",
|
||||
@@ -559,6 +590,22 @@ pub fn run() {
|
||||
None::<&str>,
|
||||
)
|
||||
.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(
|
||||
app,
|
||||
"tray_quit",
|
||||
@@ -572,7 +619,15 @@ pub fn run() {
|
||||
|
||||
let tray_menu = tauri::menu::Menu::with_items(
|
||||
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}"))?;
|
||||
|
||||
@@ -586,9 +641,18 @@ pub fn run() {
|
||||
"tray_show" => {
|
||||
let _ = tauri::Emitter::emit(app, "app://show", ());
|
||||
}
|
||||
"tray_home" => {
|
||||
let _ = tauri::Emitter::emit(app, "app://navigate", "home");
|
||||
}
|
||||
"tray_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" => {
|
||||
app.exit(0);
|
||||
}
|
||||
@@ -634,6 +698,7 @@ pub fn run() {
|
||||
fetch_blob,
|
||||
pick_and_upload_image,
|
||||
show_notification,
|
||||
open_external_url,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running maarcadetweet");
|
||||
|
||||
@@ -51,7 +51,7 @@ function createSessionStore() {
|
||||
return {
|
||||
subscribe,
|
||||
async load() {
|
||||
const s = await tauriCall<Session | null>("current_session", undefined, null);
|
||||
const s = await tauriCall<Session | null>("current_session", null);
|
||||
set(s);
|
||||
},
|
||||
async login(handle: string, password: string) {
|
||||
@@ -355,18 +355,43 @@ export async function showNotification(
|
||||
/// The handler receives the event payload (empty object for these
|
||||
/// cases). Returns an unsubscribe function.
|
||||
export async function listenTrayEvents(
|
||||
handler: (event: "show" | "compose") => void,
|
||||
handler: (event: "show" | "home" | "compose" | "profile" | "search") => void,
|
||||
): Promise<() => void> {
|
||||
const { listen } = await import("@tauri-apps/api/event");
|
||||
const unlisteners: Array<() => void> = [];
|
||||
const u1 = await listen("app://show", () => handler("show"));
|
||||
const u2 = await listen("app://compose", () => handler("compose"));
|
||||
unlisteners.push(u1, u2);
|
||||
const u2 = await listen("app://navigate", (e) => {
|
||||
handler(e.payload as "home" | "profile" | "search");
|
||||
});
|
||||
const u3 = await listen("app://compose", () => handler("compose"));
|
||||
unlisteners.push(u1, u2, u3);
|
||||
return () => {
|
||||
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
|
||||
/// `show_notification` command. Used to focus the window and
|
||||
/// navigate when the user clicks the notification.
|
||||
|
||||
Reference in New Issue
Block a user