Share a card's image from the preview
A share button beside the preview's close button sends the face being shown at Scryfall's large size: on desktop the image is decoded and put on the clipboard (tauri-plugin-clipboard-manager); on Android it's saved to the cache and handed to the system share sheet by a small in-repo plugin (SharePlugin.kt) through the app's FileProvider. The preview's corner buttons are also a little larger.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
mod commands;
|
||||
mod libraries;
|
||||
mod news;
|
||||
mod share;
|
||||
mod state;
|
||||
mod sync;
|
||||
|
||||
@@ -38,7 +39,10 @@ pub(crate) fn logged<T>(what: &str, result: Result<T, String>) -> Result<T, Stri
|
||||
pub fn run() {
|
||||
let builder = tauri::Builder::default().plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_fs::init())
|
||||
.plugin(tauri_plugin_opener::init());
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.plugin(share::init());
|
||||
#[cfg(desktop)]
|
||||
let builder = builder.plugin(tauri_plugin_clipboard_manager::init());
|
||||
// Android gets sync sign-ins back through the `mtgc://` scheme; desktop
|
||||
// uses a loopback port instead and needs no scheme registered.
|
||||
#[cfg(target_os = "android")]
|
||||
@@ -66,6 +70,7 @@ pub fn run() {
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
commands::get_status,
|
||||
share::share_card_image,
|
||||
commands::update_card_data,
|
||||
commands::cancel_update_card_data,
|
||||
commands::import_reference_db,
|
||||
|
||||
92
src-tauri/src/share.rs
Normal file
92
src-tauri/src/share.rs
Normal file
@@ -0,0 +1,92 @@
|
||||
//! 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()
|
||||
}
|
||||
Reference in New Issue
Block a user