From 1513545fecd8834a4528ae8ebd9ec968f3b3bd7c Mon Sep 17 00:00:00 2001 From: "Calum H." Date: Tue, 24 Jun 2025 22:27:07 +0100 Subject: [PATCH] feat: drag and drop func --- .../components/ui/skin/UploadSkinModal.vue | 42 ++++++------------- apps/app-frontend/src/helpers/skins.ts | 4 ++ apps/app/build.rs | 1 + apps/app/src/api/minecraft_skins.rs | 11 +++++ packages/app-lib/src/api/minecraft_skins.rs | 41 ++++++++++++++++++ 5 files changed, 70 insertions(+), 29 deletions(-) diff --git a/apps/app-frontend/src/components/ui/skin/UploadSkinModal.vue b/apps/app-frontend/src/components/ui/skin/UploadSkinModal.vue index ac1e1d258..4436d0186 100644 --- a/apps/app-frontend/src/components/ui/skin/UploadSkinModal.vue +++ b/apps/app-frontend/src/components/ui/skin/UploadSkinModal.vue @@ -32,6 +32,7 @@ import { UploadIcon } from '@modrinth/assets' import { useNotifications } from '@/store/state' import { getCurrentWebview } from '@tauri-apps/api/webview' import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue' +import { get_dragged_skin_data } from '@/helpers/skins' const notifications = useNotifications() @@ -106,22 +107,24 @@ async function setupDragDropListener() { return } - // const filePath = event.payload.paths[0] + const filePath = event.payload.paths[0] try { - // TODO: Drag and drop support for local files - // const data = await readFile(filePath) - // - // const fileName = filePath.split('/').pop() || filePath.split('\\').pop() || 'skin.png' - // const fileBlob = new Blob([data], { type: 'image/png' }) - // const file = new File([fileBlob], fileName, { type: 'image/png' }) - // - // await processFile(file) + console.log(filePath); + const data = await get_dragged_skin_data(filePath).catch((err) => { + throw new Error(`Failed to read file: ${err.message || err}`) + }) + + const fileName = filePath.split('/').pop() || filePath.split('\\').pop() || 'skin.png' + const fileBlob = new Blob([data], { type: 'image/png' }) + const file = new File([fileBlob], fileName, { type: 'image/png' }) + + await processFile(file) } catch (error) { console.error(error) notifications.addNotification({ title: 'Error processing file', - text: 'Failed to read the dropped file.', + text: error.message || 'Failed to read the dropped file.', type: 'error', }) } @@ -140,25 +143,6 @@ async function cleanupDragDropListener() { } async function processFile(file: File) { - if (!file.name.toLowerCase().endsWith('.png') && file.type !== 'image/png') { - notifications.addNotification({ - title: 'Invalid file type.', - text: 'Only PNG files are accepted.', - type: 'error', - }) - return - } - - const isValidDimensions = await validateImageDimensions(file) - if (!isValidDimensions) { - notifications.addNotification({ - title: 'Invalid dimensions.', - text: 'Only 64x64 and 64x32 PNG files are accepted.', - type: 'error', - }) - return - } - emit('uploaded', file) hide() } diff --git a/apps/app-frontend/src/helpers/skins.ts b/apps/app-frontend/src/helpers/skins.ts index 71a466b86..f3c29be1e 100644 --- a/apps/app-frontend/src/helpers/skins.ts +++ b/apps/app-frontend/src/helpers/skins.ts @@ -156,3 +156,7 @@ export async function normalize_skin_texture(texture: Uint8Array | string): Prom export async function unequip_skin(): Promise { await invoke('plugin:minecraft-skins|unequip_skin') } + +export async function get_dragged_skin_data(path: string): Promise { + return invoke('plugin:minecraft-skins|get_dragged_skin_data', { path }) +} diff --git a/apps/app/build.rs b/apps/app/build.rs index 3a7891c52..7a4da8872 100644 --- a/apps/app/build.rs +++ b/apps/app/build.rs @@ -111,6 +111,7 @@ fn main() { "remove_custom_skin", "unequip_skin", "normalize_skin_texture", + "get_dragged_skin_data", ]) .default_permission( DefaultPermissionRule::AllowAllCommands, diff --git a/apps/app/src/api/minecraft_skins.rs b/apps/app/src/api/minecraft_skins.rs index d8e53caa4..a6d138fbd 100644 --- a/apps/app/src/api/minecraft_skins.rs +++ b/apps/app/src/api/minecraft_skins.rs @@ -1,5 +1,6 @@ use crate::api::Result; +use std::path::Path; use theseus::minecraft_skins::{ self, Bytes, Cape, MinecraftSkinVariant, Skin, UrlOrBlob, }; @@ -15,6 +16,7 @@ pub fn init() -> tauri::plugin::TauriPlugin { remove_custom_skin, unequip_skin, normalize_skin_texture, + get_dragged_skin_data, ]) .build() } @@ -91,3 +93,12 @@ pub async fn unequip_skin() -> Result<()> { pub async fn normalize_skin_texture(texture: UrlOrBlob) -> Result { Ok(minecraft_skins::normalize_skin_texture(&texture).await?) } + +/// `invoke('plugin:minecraft-skins|get_dragged_skin_data', path)` +/// +/// See also: [minecraft_skins::get_dragged_skin_data] +#[tauri::command] +pub async fn get_dragged_skin_data(path: String) -> Result { + let path = Path::new(&path); + Ok(minecraft_skins::get_dragged_skin_data(path).await?) +} diff --git a/packages/app-lib/src/api/minecraft_skins.rs b/packages/app-lib/src/api/minecraft_skins.rs index 67a40d876..1b53526d4 100644 --- a/packages/app-lib/src/api/minecraft_skins.rs +++ b/packages/app-lib/src/api/minecraft_skins.rs @@ -455,6 +455,47 @@ pub async fn normalize_skin_texture( png_util::normalize_skin_texture(texture).await } +/// Reads and validates a skin texture file from the given path. +/// Returns the file content as bytes if it's a valid skin texture (PNG with 64x64 or 64x32 dimensions). +#[tracing::instrument] +pub async fn get_dragged_skin_data(path: &std::path::Path) -> crate::Result { + if let Some(extension) = path.extension() { + if extension.to_string_lossy().to_lowercase() != "png" { + return Err(ErrorKind::InvalidSkinTexture.into()); + } + } else { + return Err(ErrorKind::InvalidSkinTexture.into()); + } + + tracing::debug!("Reading file: {:?}", path); + + if !path.exists() { + tracing::error!("File does not exist: {:?}", path); + return Err(ErrorKind::InvalidSkinTexture.into()); + } + + let data = match tokio::fs::read(path).await { + Ok(data) => { + tracing::debug!("File read successfully, size: {} bytes", data.len()); + data + }, + Err(err) => { + tracing::error!("Failed to read file: {}", err); + return Err(err.into()); + } + }; + + let url_or_blob = UrlOrBlob::Blob(data.clone().into()); + + match normalize_skin_texture(&url_or_blob).await { + Ok(_) => Ok(data.into()), + Err(err) => { + tracing::error!("Failed to normalize skin texture: {}", err); + Err(ErrorKind::InvalidSkinTexture.into()) + } + } +} + /// Synchronizes the equipped cape with the selected cape if necessary, taking into /// account the currently equipped cape, the default cape for the player, and if a /// cape override is provided.