93 lines
3.4 KiB
Rust
93 lines
3.4 KiB
Rust
|
|
//! Sharing a card's image: onto the clipboard on desktop, through the system
|
||
|
|
//! share sheet on Android.
|
||
|
|
//!
|
||
|
|
//! Android has no official Tauri share plugin, so this registers a small one
|
||
|
|
//! of our own whose Kotlin half (`SharePlugin.kt`, in the Android app module)
|
||
|
|
//! hands the image to `ACTION_SEND` via the app's FileProvider.
|
||
|
|
|
||
|
|
use tauri::AppHandle;
|
||
|
|
|
||
|
|
type Res<T> = Result<T, String>;
|
||
|
|
|
||
|
|
/// Scryfall serves each scan at several sizes under the same path; the
|
||
|
|
/// previews show "normal", but a shared or pasted image deserves "large".
|
||
|
|
fn large_variant(url: &str) -> String {
|
||
|
|
if url.starts_with("https://cards.scryfall.io/normal/") {
|
||
|
|
url.replacen("/normal/", "/large/", 1)
|
||
|
|
} else {
|
||
|
|
url.to_string()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Copy (desktop) or share (Android) the card image at `url`. Returns what
|
||
|
|
/// happened — "copied" or "shared" — for the UI to confirm.
|
||
|
|
#[tauri::command]
|
||
|
|
pub async fn share_card_image(app: AppHandle, url: String, name: String) -> Res<&'static str> {
|
||
|
|
crate::sync::blocking(move || {
|
||
|
|
let bytes = mtgc_core::fetch_image(&large_variant(&url)).map_err(|e| e.to_string())?;
|
||
|
|
deliver(&app, &bytes, &name)
|
||
|
|
})
|
||
|
|
.await
|
||
|
|
}
|
||
|
|
|
||
|
|
#[cfg(desktop)]
|
||
|
|
fn deliver(app: &AppHandle, bytes: &[u8], _name: &str) -> Res<&'static str> {
|
||
|
|
use tauri_plugin_clipboard_manager::ClipboardExt;
|
||
|
|
// The clipboard takes raw pixels, and Scryfall's scans are JPEGs.
|
||
|
|
let rgba = image::load_from_memory(bytes).map_err(|e| e.to_string())?.to_rgba8();
|
||
|
|
let (width, height) = rgba.dimensions();
|
||
|
|
app.clipboard()
|
||
|
|
.write_image(&tauri::image::Image::new(rgba.as_raw(), width, height))
|
||
|
|
.map_err(|e| e.to_string())?;
|
||
|
|
Ok("copied")
|
||
|
|
}
|
||
|
|
|
||
|
|
#[cfg(target_os = "android")]
|
||
|
|
fn deliver(app: &AppHandle, bytes: &[u8], name: &str) -> Res<&'static str> {
|
||
|
|
use tauri::Manager;
|
||
|
|
// The cache dir is what the FileProvider exposes (res/xml/file_paths.xml).
|
||
|
|
// Named after the card, since that's what the receiving app shows.
|
||
|
|
let dir = app.path().app_cache_dir().map_err(|e| e.to_string())?.join("shared");
|
||
|
|
std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
|
||
|
|
let file_name: String = name
|
||
|
|
.chars()
|
||
|
|
.map(|c| if c.is_alphanumeric() || c == ' ' || c == '-' { c } else { '_' })
|
||
|
|
.collect();
|
||
|
|
let path = dir.join(format!("{}.jpg", file_name.trim()));
|
||
|
|
std::fs::write(&path, bytes).map_err(|e| e.to_string())?;
|
||
|
|
|
||
|
|
let plugin = app.state::<SharePlugin>();
|
||
|
|
plugin
|
||
|
|
.0
|
||
|
|
.run_mobile_plugin::<()>(
|
||
|
|
"shareImage",
|
||
|
|
serde_json::json!({ "path": path.to_string_lossy(), "title": name }),
|
||
|
|
)
|
||
|
|
.map_err(|e| e.to_string())?;
|
||
|
|
Ok("shared")
|
||
|
|
}
|
||
|
|
|
||
|
|
#[cfg(target_os = "ios")]
|
||
|
|
fn deliver(_app: &AppHandle, _bytes: &[u8], _name: &str) -> Res<&'static str> {
|
||
|
|
Err("Sharing isn't available on iOS yet.".into())
|
||
|
|
}
|
||
|
|
|
||
|
|
/// The Kotlin half's handle, for `run_mobile_plugin`.
|
||
|
|
#[cfg(target_os = "android")]
|
||
|
|
struct SharePlugin(tauri::plugin::PluginHandle<tauri::Wry>);
|
||
|
|
|
||
|
|
/// Registers the Android share plugin. A no-op plugin elsewhere.
|
||
|
|
pub fn init() -> tauri::plugin::TauriPlugin<tauri::Wry> {
|
||
|
|
tauri::plugin::Builder::new("share")
|
||
|
|
.setup(|_app, _api| {
|
||
|
|
#[cfg(target_os = "android")]
|
||
|
|
{
|
||
|
|
use tauri::Manager;
|
||
|
|
let handle = _api.register_android_plugin("com.garrettmills.mtgc", "SharePlugin")?;
|
||
|
|
_app.manage(SharePlugin(handle));
|
||
|
|
}
|
||
|
|
Ok(())
|
||
|
|
})
|
||
|
|
.build()
|
||
|
|
}
|