17749f7cc7
iOS and Android now run the same Web-URL clip flow as desktop. Paste
an article URL, the native side opens a full-screen WKWebView /
WebView with the same Chrome UA + fingerprint mask + "Saving to
Readest" overlay as the desktop hidden window, waits for load +
settle, captures `document.documentElement.outerHTML` via the
platform's `evaluateJavaScript`, and returns it through the existing
`convertToEpub` pipeline.
JS surface stays `invoke('clip_url', { url, options })` — no changes
in `library/page.tsx` or `send/page.tsx`. The platform branch lives
entirely in `clip_url.rs`.
Why not Tauri's `Window::add_child`
`add_child` is gated `#[cfg(any(test, all(desktop, feature =
"unstable")))]` in tauri 2.10. No public API for attaching a second
webview to the main window on mobile, so the clip flow can't be a
`#[cfg(mobile)]` branch of the existing `WebviewWindowBuilder` shape
— it needs native code. Extend `tauri-plugin-native-bridge` rather
than create a separate plugin: the Swift / Kotlin scaffolding +
Tauri IPC are already there.
Layout
- `src-tauri/src/clip_url.rs` — desktop branch unchanged; new
`#[cfg(mobile)]` `clip_url` command routes through
`app.native_bridge().clip_url(request)`. Shared `ClipOptions`
struct exposes its fields `pub` so the mobile branch can map into
the plugin's `ClipUrlRequest`.
- `plugins/tauri-plugin-native-bridge/src/models.rs` — `ClipUrlRequest`
+ `ClipUrlResponse` mirroring `ClipOptions` field-for-field so the
payload travels untouched from JS through to Swift/Kotlin.
- `plugins/tauri-plugin-native-bridge/src/{desktop,mobile}.rs` — desktop
returns an error (desktop has its own path); mobile dispatches via
`run_mobile_plugin("clip_url", payload)`.
- `ios/Sources/ClipUrlController.swift` — `UIViewController` hosting
`WKWebView` with the loading overlay drawn as native UIKit views
(not an injected user script, so the page's own hydration can't
wipe the spinner). 30 s hard timeout + 3 s settle window after
`didFinish`, same as desktop. Fingerprint mask injected as
`WKUserScript` at `.atDocumentStart`.
- `android/src/main/java/ClipUrlController.kt` — full-screen Dialog
hosting a `WebView`, mirrors the iOS controller's behaviour. JSON-
decodes the `evaluateJavascript` callback (raw return value is a
JSON-encoded string).
- `NativeBridgePlugin.{swift,kt}` — new `clip_url` method that parses
args via `invoke.parseArgs`, presents the controller, resolves the
invoke with `{ html }` on success or `invoke.reject` on failure.
Same rejection vocabulary as desktop (`"Invalid URL"`, `"Page took
too long to load"`, etc.) so the calling JS doesn't need a
platform branch.
- `build.rs` — adds `clip_url` to the plugin's `COMMANDS` array.
Notes
- The Swift overlay reserves the iOS safe-area-edge-to-edge so notch /
Dynamic Island devices don't see the underlying app peek through
during the brief capture window.
- The Android overlay's spinner tint follows the foreground theme
colour at 85 % alpha — same idea as the iOS controller.
- `WKWebView`'s JS keeps running while the controller is presented;
no off-screen / `isHidden` trick that would let iOS throttle the
page mid-capture.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
539 lines
21 KiB
Rust
539 lines
21 KiB
Rust
#[cfg(target_os = "macos")]
|
|
#[macro_use]
|
|
extern crate cocoa;
|
|
|
|
#[cfg(target_os = "macos")]
|
|
#[macro_use]
|
|
extern crate objc;
|
|
|
|
#[cfg(target_os = "windows")]
|
|
mod windows;
|
|
|
|
#[cfg(target_os = "android")]
|
|
mod android;
|
|
|
|
use tauri::utils::config::BackgroundThrottlingPolicy;
|
|
#[cfg(target_os = "macos")]
|
|
use tauri::TitleBarStyle;
|
|
|
|
use std::path::PathBuf;
|
|
use tauri::{AppHandle, Manager};
|
|
use tauri_plugin_fs::FsExt;
|
|
|
|
#[cfg(desktop)]
|
|
use tauri::{Listener, Url};
|
|
mod clip_url;
|
|
mod dir_scanner;
|
|
#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
|
|
mod discord_rpc;
|
|
#[cfg(target_os = "macos")]
|
|
mod macos;
|
|
mod transfer_file;
|
|
#[cfg(target_os = "windows")]
|
|
use tauri::webview::ScrollBarStyle;
|
|
use tauri::{command, Emitter, WebviewUrl, WebviewWindowBuilder, Window};
|
|
#[cfg(target_os = "android")]
|
|
use tauri_plugin_native_bridge::register_select_directory_callback;
|
|
#[cfg(target_os = "android")]
|
|
use tauri_plugin_native_bridge::{NativeBridgeExt, OpenExternalUrlRequest};
|
|
use tauri_plugin_oauth::start;
|
|
#[cfg(not(target_os = "android"))]
|
|
use tauri_plugin_opener::OpenerExt;
|
|
use transfer_file::{download_file, upload_file};
|
|
|
|
#[cfg(desktop)]
|
|
fn allow_file_in_scopes(app: &AppHandle, files: Vec<PathBuf>) {
|
|
let fs_scope = app.fs_scope();
|
|
let asset_protocol_scope = app.asset_protocol_scope();
|
|
for file in &files {
|
|
if let Err(e) = fs_scope.allow_file(file) {
|
|
log::error!("Failed to allow file in fs_scope: {e}");
|
|
} else {
|
|
log::debug!("Allowed file in fs_scope: {file:?}");
|
|
}
|
|
if let Err(e) = asset_protocol_scope.allow_file(file) {
|
|
log::error!("Failed to allow file in asset_protocol_scope: {e}");
|
|
} else {
|
|
log::debug!("Allowed file in asset_protocol_scope: {file:?}");
|
|
}
|
|
}
|
|
}
|
|
|
|
fn allow_dir_in_scopes(app: &AppHandle, dir: &PathBuf) {
|
|
let fs_scope = app.fs_scope();
|
|
let asset_protocol_scope = app.asset_protocol_scope();
|
|
if let Err(e) = fs_scope.allow_directory(dir, true) {
|
|
log::error!("Failed to allow directory in fs_scope: {e}");
|
|
} else {
|
|
log::info!("Allowed directory in fs_scope: {dir:?}");
|
|
}
|
|
if let Err(e) = asset_protocol_scope.allow_directory(dir, true) {
|
|
log::error!("Failed to allow directory in asset_protocol_scope: {e}");
|
|
} else {
|
|
log::info!("Allowed directory in asset_protocol_scope: {dir:?}");
|
|
}
|
|
}
|
|
|
|
/// Frontend-callable shim around [`allow_file_in_scopes`] /
|
|
/// [`allow_dir_in_scopes`]. Used after dialog-based file/folder pickers
|
|
/// because the Tauri `dialog` plugin only auto-grants `fs_scope`, not
|
|
/// `asset_protocol_scope` — and our importer relies on the asset
|
|
/// protocol (`RemoteFile`) to read user-selected files. Without this,
|
|
/// importing a book from e.g. `~/Downloads/...` fails with
|
|
/// "asset protocol not configured to allow the path".
|
|
///
|
|
/// Granted scopes are persisted across app restarts thanks to
|
|
/// `tauri_plugin_persisted_scope`, so re-picking the same file isn't
|
|
/// required after the first allow call.
|
|
///
|
|
/// Security: this command refuses to extend `asset_protocol_scope` for
|
|
/// any path that is not already allowed in `fs_scope`. The `fs_scope`
|
|
/// is populated only by the Tauri `dialog` plugin (when the user picks
|
|
/// through the OS picker) or by `tauri_plugin_persisted_scope` (which
|
|
/// restores prior dialog grants on startup). That gate constrains the
|
|
/// command to user-selected paths only — otherwise any frontend code
|
|
/// (including a future XSS via book content, OPDS HTML, dictionary
|
|
/// lookups, or a compromised dependency) could invoke it with an
|
|
/// arbitrary path like `/` or `~/.ssh` and gain persistent read access
|
|
/// to the entire user home directory via the asset protocol.
|
|
#[command]
|
|
fn allow_paths_in_scopes(_app: AppHandle, _paths: Vec<String>, _is_directory: bool) {
|
|
#[cfg(desktop)]
|
|
{
|
|
let fs_scope = _app.fs_scope();
|
|
for raw in _paths {
|
|
if raw.is_empty() {
|
|
continue;
|
|
}
|
|
let path = PathBuf::from(&raw);
|
|
if !fs_scope.is_allowed(&path) {
|
|
log::warn!("allow_paths_in_scopes refused (path not in fs_scope): {path:?}");
|
|
continue;
|
|
}
|
|
if _is_directory {
|
|
allow_dir_in_scopes(&_app, &path);
|
|
} else {
|
|
allow_file_in_scopes(&_app, vec![path]);
|
|
}
|
|
}
|
|
}
|
|
#[cfg(target_os = "android")]
|
|
{
|
|
// Android picker already routes through register_select_directory_callback
|
|
// for directories; files go through SAF / content-URIs and don't use
|
|
// asset_protocol_scope. Nothing to do here.
|
|
}
|
|
}
|
|
|
|
#[cfg(desktop)]
|
|
fn get_files_from_argv(argv: Vec<String>) -> Vec<PathBuf> {
|
|
let mut files = Vec::new();
|
|
// NOTICE: `args` may include URL protocol (`your-app-protocol://`)
|
|
// or arguments (`--`) if your app supports them.
|
|
// files may also be passed as `file://path/to/file`
|
|
for (_, maybe_file) in argv.iter().enumerate().skip(1) {
|
|
// skip flags like -f or --flag
|
|
if maybe_file.starts_with("-") {
|
|
continue;
|
|
}
|
|
// handle `file://` path urls and skip other urls
|
|
if let Ok(url) = Url::parse(maybe_file) {
|
|
if let Ok(path) = url.to_file_path() {
|
|
files.push(path);
|
|
} else {
|
|
files.push(PathBuf::from(maybe_file))
|
|
}
|
|
} else {
|
|
files.push(PathBuf::from(maybe_file))
|
|
}
|
|
}
|
|
files
|
|
}
|
|
|
|
#[cfg(desktop)]
|
|
fn set_window_open_with_files(app: &AppHandle, files: Vec<PathBuf>) {
|
|
let files = files
|
|
.into_iter()
|
|
.map(|f| {
|
|
let file = f
|
|
.to_string_lossy()
|
|
.replace("\\", "\\\\")
|
|
.replace("\"", "\\\"");
|
|
format!("\"{file}\"",)
|
|
})
|
|
.collect::<Vec<_>>()
|
|
.join(",");
|
|
let window = app.get_webview_window("main").unwrap();
|
|
let script = format!("window.OPEN_WITH_FILES = [{files}];");
|
|
if let Err(e) = window.eval(&script) {
|
|
eprintln!("Failed to set open files variable: {e}");
|
|
}
|
|
}
|
|
|
|
#[command]
|
|
async fn start_server(window: Window) -> Result<u16, String> {
|
|
start(move |url| {
|
|
// Because of the unprotected localhost port, you must verify the URL here.
|
|
// Preferebly send back only the token, or nothing at all if you can handle everything else in Rust.
|
|
let _ = window.emit("redirect_uri", url);
|
|
})
|
|
.map_err(|err| err.to_string())
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn get_environment_variable(name: &str) -> String {
|
|
std::env::var(String::from(name)).unwrap_or(String::from(""))
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn get_executable_dir() -> String {
|
|
std::env::current_exe()
|
|
.ok()
|
|
.and_then(|path| path.parent().map(|p| p.to_path_buf()))
|
|
.map(|p| p.to_string_lossy().to_string())
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
#[derive(Clone, serde::Serialize)]
|
|
#[allow(dead_code)]
|
|
struct SingleInstancePayload {
|
|
args: Vec<String>,
|
|
cwd: String,
|
|
}
|
|
|
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
|
pub fn run() {
|
|
let builder = tauri::Builder::default()
|
|
.plugin(
|
|
tauri_plugin_log::Builder::new()
|
|
.level(log::LevelFilter::Info)
|
|
.level_for("tracing", log::LevelFilter::Warn)
|
|
.level_for("tantivy", log::LevelFilter::Warn)
|
|
.build(),
|
|
)
|
|
.plugin(tauri_plugin_websocket::init())
|
|
.plugin(tauri_plugin_process::init())
|
|
.plugin(tauri_plugin_oauth::init())
|
|
.invoke_handler(tauri::generate_handler![
|
|
start_server,
|
|
download_file,
|
|
upload_file,
|
|
get_environment_variable,
|
|
get_executable_dir,
|
|
allow_paths_in_scopes,
|
|
dir_scanner::read_dir,
|
|
#[cfg(target_os = "macos")]
|
|
macos::safari_auth::auth_with_safari,
|
|
#[cfg(target_os = "macos")]
|
|
macos::apple_auth::start_apple_sign_in,
|
|
#[cfg(target_os = "macos")]
|
|
macos::traffic_light::set_traffic_lights,
|
|
#[cfg(target_os = "macos")]
|
|
macos::system_dictionary::show_lookup_popover,
|
|
#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
|
|
discord_rpc::update_book_presence,
|
|
#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
|
|
discord_rpc::clear_book_presence,
|
|
clip_url::clip_url,
|
|
])
|
|
.plugin(tauri_plugin_fs::init())
|
|
.plugin(tauri_plugin_persisted_scope::init())
|
|
.plugin(tauri_plugin_shell::init())
|
|
.plugin(tauri_plugin_opener::init())
|
|
.plugin(tauri_plugin_http::init())
|
|
.plugin(tauri_plugin_os::init())
|
|
.plugin(tauri_plugin_dialog::init())
|
|
.plugin(tauri_plugin_sharekit::init())
|
|
.plugin(tauri_plugin_device_info::init())
|
|
.plugin(tauri_plugin_turso::init())
|
|
.plugin(tauri_plugin_native_bridge::init())
|
|
.plugin(tauri_plugin_native_tts::init())
|
|
.plugin(tauri_plugin_webview_upgrade::init());
|
|
|
|
#[cfg(desktop)]
|
|
let builder = builder.plugin(
|
|
tauri_plugin_single_instance::Builder::new()
|
|
.callback(move |app, argv, cwd| {
|
|
if let Some(window) = app.get_webview_window("main") {
|
|
let _ = window.set_focus();
|
|
}
|
|
let files = get_files_from_argv(argv.clone());
|
|
if !files.is_empty() {
|
|
allow_file_in_scopes(app, files.clone());
|
|
}
|
|
app.emit("single-instance", SingleInstancePayload { args: argv, cwd })
|
|
.unwrap();
|
|
})
|
|
.dbus_id("com.bilingify.readest".to_owned())
|
|
.build(),
|
|
);
|
|
|
|
let builder = builder.plugin(tauri_plugin_deep_link::init());
|
|
|
|
#[cfg(desktop)]
|
|
let builder = builder.plugin(tauri_plugin_updater::Builder::new().build());
|
|
|
|
#[cfg(desktop)]
|
|
let builder = builder.plugin(tauri_plugin_window_state::Builder::default().build());
|
|
|
|
#[cfg(target_os = "macos")]
|
|
let builder = builder.plugin(macos::traffic_light::init());
|
|
|
|
#[cfg(target_os = "macos")]
|
|
let builder = builder.plugin(macos::safari_auth::init());
|
|
|
|
#[cfg(target_os = "ios")]
|
|
let builder = builder.plugin(tauri_plugin_sign_in_with_apple::init());
|
|
|
|
#[cfg(any(target_os = "ios", target_os = "android"))]
|
|
let builder = builder.plugin(tauri_plugin_haptics::init());
|
|
|
|
#[cfg(feature = "webdriver")]
|
|
let builder = builder.plugin(tauri_plugin_webdriver::init());
|
|
|
|
builder
|
|
.setup(|#[allow(unused_variables)] app| {
|
|
// When running with the webdriver feature (E2E/integration tests),
|
|
// grant all default permissions to remote URLs (http://127.0.0.1:*)
|
|
// so that Vitest browser-mode tests can call plugin commands.
|
|
#[cfg(feature = "webdriver")]
|
|
{
|
|
use tauri::Manager;
|
|
app.add_capability(include_str!("../capabilities-extra/webdriver.json"))?;
|
|
}
|
|
#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
|
|
{
|
|
use std::sync::{Arc, Mutex};
|
|
let discord_client = Arc::new(Mutex::new(discord_rpc::DiscordRpcClient::new()));
|
|
app.manage(discord_client);
|
|
}
|
|
|
|
#[cfg(desktop)]
|
|
{
|
|
let files = get_files_from_argv(std::env::args().collect());
|
|
if !files.is_empty() {
|
|
let app_handle = app.handle().clone();
|
|
allow_file_in_scopes(&app_handle, files.clone());
|
|
app.listen("window-ready", move |_| {
|
|
println!("Window is ready, proceeding to handle files.");
|
|
set_window_open_with_files(&app_handle, files.clone());
|
|
});
|
|
}
|
|
}
|
|
|
|
#[cfg(desktop)]
|
|
{
|
|
allow_dir_in_scopes(app.handle(), &PathBuf::from(get_executable_dir()));
|
|
}
|
|
|
|
#[cfg(target_os = "android")]
|
|
register_select_directory_callback(app.handle(), move |app, path| {
|
|
allow_dir_in_scopes(app, path);
|
|
});
|
|
|
|
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
|
{
|
|
use tauri_plugin_deep_link::DeepLinkExt;
|
|
let _ = app.deep_link().register_all();
|
|
}
|
|
|
|
#[cfg(desktop)]
|
|
{
|
|
app.handle().plugin(tauri_plugin_cli::init())?;
|
|
}
|
|
|
|
// Check for e-ink device on Android before building the window
|
|
#[cfg(target_os = "android")]
|
|
let is_eink = android::is_eink_device();
|
|
#[cfg(not(target_os = "android"))]
|
|
let is_eink = false;
|
|
|
|
#[cfg(desktop)]
|
|
let cli_access = true;
|
|
#[cfg(not(desktop))]
|
|
let cli_access = false;
|
|
|
|
#[cfg(target_os = "linux")]
|
|
let is_appimage = std::env::var("APPIMAGE").is_ok()
|
|
|| std::env::current_exe()
|
|
.map(|path| path.to_string_lossy().contains("/tmp/.mount_"))
|
|
.unwrap_or(false);
|
|
#[cfg(not(target_os = "linux"))]
|
|
let is_appimage = false;
|
|
|
|
#[cfg(desktop)]
|
|
let updater_disabled = std::env::var("READEST_DISABLE_UPDATER").is_ok();
|
|
#[cfg(not(desktop))]
|
|
let updater_disabled = false;
|
|
|
|
let init_script = format!(
|
|
r#"
|
|
if ({is_eink}) window.__READEST_IS_EINK = true;
|
|
if ({cli_access}) window.__READEST_CLI_ACCESS = true;
|
|
if ({is_appimage}) window.__READEST_IS_APPIMAGE = true;
|
|
if ({updater_disabled}) window.__READEST_UPDATER_DISABLED = true;
|
|
window.addEventListener('DOMContentLoaded', function() {{
|
|
document.documentElement.classList.add('edge-to-edge');
|
|
const isTauriLocal = window.location.protocol === 'tauri:' ||
|
|
window.location.protocol === 'about:' ||
|
|
window.location.hostname === 'tauri.localhost';
|
|
const needsSafeArea = !isTauriLocal;
|
|
if (needsSafeArea && !document.getElementById('safe-area-style')) {{
|
|
const style = document.createElement('style');
|
|
style.id = 'safe-area-style';
|
|
style.textContent = `
|
|
body {{
|
|
padding-top: env(safe-area-inset-top) !important;
|
|
padding-bottom: env(safe-area-inset-bottom) !important;
|
|
padding-left: env(safe-area-inset-left) !important;
|
|
padding-right: env(safe-area-inset-right) !important;
|
|
}}
|
|
`;
|
|
document.head.appendChild(style);
|
|
}}
|
|
}});
|
|
"#,
|
|
is_eink = is_eink,
|
|
cli_access = cli_access,
|
|
is_appimage = is_appimage,
|
|
updater_disabled = updater_disabled
|
|
);
|
|
|
|
let app_handle = app.handle().clone();
|
|
let win_builder = WebviewWindowBuilder::new(app, "main", WebviewUrl::default())
|
|
.background_throttling(BackgroundThrottlingPolicy::Disabled)
|
|
.background_color(if is_eink {
|
|
tauri::window::Color(255, 255, 255, 255)
|
|
} else {
|
|
tauri::window::Color(50, 49, 48, 255)
|
|
})
|
|
.initialization_script(&init_script)
|
|
.on_navigation(move |url| {
|
|
if url.scheme() == "alipays" || url.scheme() == "alipay" {
|
|
let url_str = url.as_str().to_string();
|
|
#[cfg(target_os = "android")]
|
|
{
|
|
let handle = app_handle.clone();
|
|
tauri::async_runtime::spawn(async move {
|
|
match handle
|
|
.native_bridge()
|
|
.open_external_url(OpenExternalUrlRequest { url: url_str })
|
|
{
|
|
Ok(result) => println!("Result: {:?}", result),
|
|
Err(e) => eprintln!("Error: {:?}", e),
|
|
}
|
|
});
|
|
}
|
|
#[cfg(not(target_os = "android"))]
|
|
{
|
|
let _ = app_handle.opener().open_url(url_str, None::<&str>);
|
|
}
|
|
return false;
|
|
}
|
|
true
|
|
});
|
|
|
|
#[cfg(target_os = "macos")]
|
|
let win_builder = win_builder.inner_size(1280.0, 800.0).resizable(true);
|
|
#[cfg(all(not(target_os = "macos"), desktop))]
|
|
let win_builder = win_builder.inner_size(800.0, 600.0).resizable(true);
|
|
|
|
#[cfg(target_os = "macos")]
|
|
let win_builder = win_builder
|
|
.decorations(true)
|
|
.title_bar_style(TitleBarStyle::Overlay)
|
|
.title("");
|
|
|
|
#[cfg(all(not(target_os = "macos"), desktop))]
|
|
let win_builder = {
|
|
let mut builder = win_builder
|
|
.decorations(false)
|
|
.visible(false)
|
|
.shadow(true)
|
|
.title("Readest");
|
|
|
|
#[cfg(target_os = "windows")]
|
|
{
|
|
builder = builder
|
|
.transparent(false)
|
|
.scroll_bar_style(ScrollBarStyle::FluentOverlay);
|
|
}
|
|
#[cfg(target_os = "linux")]
|
|
{
|
|
builder = builder
|
|
.transparent(true)
|
|
.background_color(tauri::window::Color(0, 0, 0, 0));
|
|
}
|
|
|
|
builder
|
|
};
|
|
|
|
#[cfg(not(target_os = "macos"))]
|
|
{
|
|
win_builder.build().unwrap();
|
|
}
|
|
// let win = win_builder.build().unwrap();
|
|
// win.open_devtools();
|
|
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
let window = win_builder.build().unwrap();
|
|
// On macOS, closing a window (via Cmd+W or the red traffic light) should
|
|
// not quit the app — only Cmd+Q should. Hide the window instead so the
|
|
// app keeps running in the dock, and restore it when the user reopens
|
|
// the app from the dock.
|
|
let window_for_close = window.clone();
|
|
window.on_window_event(move |event| {
|
|
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
|
|
api.prevent_close();
|
|
let _ = window_for_close.hide();
|
|
}
|
|
});
|
|
}
|
|
|
|
#[cfg(target_os = "macos")]
|
|
macos::menu::setup_macos_menu(app.handle())?;
|
|
|
|
app.handle().emit("window-ready", ()).unwrap();
|
|
|
|
Ok(())
|
|
})
|
|
.build(tauri::generate_context!())
|
|
.expect("error while running tauri application")
|
|
.run(
|
|
#[allow(unused_variables)]
|
|
|app_handle, event| {
|
|
#[cfg(target_os = "macos")]
|
|
match event {
|
|
tauri::RunEvent::Opened { urls } => {
|
|
let files = urls
|
|
.into_iter()
|
|
.filter_map(|url| url.to_file_path().ok())
|
|
.collect::<Vec<_>>();
|
|
|
|
let app_handler_clone = app_handle.clone();
|
|
allow_file_in_scopes(app_handle, files.clone());
|
|
app_handle.listen("window-ready", move |_| {
|
|
println!("Window is ready, proceeding to handle files.");
|
|
set_window_open_with_files(&app_handler_clone, files.clone());
|
|
});
|
|
}
|
|
// When the user reopens the app from the dock after closing all
|
|
// windows, re-show the main window instead of leaving the dock
|
|
// icon inert.
|
|
tauri::RunEvent::Reopen {
|
|
has_visible_windows: false,
|
|
..
|
|
} => {
|
|
if let Some(window) = app_handle.get_webview_window("main") {
|
|
let _ = window.show();
|
|
let _ = window.set_focus();
|
|
let _ = window.unminimize();
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
},
|
|
);
|
|
}
|