feat: drag and drop func

This commit is contained in:
Calum H.
2025-06-24 22:27:07 +01:00
parent d2c64493f3
commit 1513545fec
5 changed files with 70 additions and 29 deletions

View File

@@ -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()
}

View File

@@ -156,3 +156,7 @@ export async function normalize_skin_texture(texture: Uint8Array | string): Prom
export async function unequip_skin(): Promise<void> {
await invoke('plugin:minecraft-skins|unequip_skin')
}
export async function get_dragged_skin_data(path: string): Promise<Uint8Array> {
return invoke('plugin:minecraft-skins|get_dragged_skin_data', { path })
}

View File

@@ -111,6 +111,7 @@ fn main() {
"remove_custom_skin",
"unequip_skin",
"normalize_skin_texture",
"get_dragged_skin_data",
])
.default_permission(
DefaultPermissionRule::AllowAllCommands,

View File

@@ -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<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
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<Bytes> {
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<Bytes> {
let path = Path::new(&path);
Ok(minecraft_skins::get_dragged_skin_data(path).await?)
}

View File

@@ -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<Bytes> {
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.