diff --git a/packages/app/src-tauri/Cargo.lock b/packages/app/src-tauri/Cargo.lock index 0dd5505..e4d27fa 100644 --- a/packages/app/src-tauri/Cargo.lock +++ b/packages/app/src-tauri/Cargo.lock @@ -31,7 +31,7 @@ dependencies = [ [[package]] name = "aibubu" -version = "0.4.1" +version = "0.4.3" dependencies = [ "chrono", "dirs 6.0.0", diff --git a/packages/app/src-tauri/src/lib.rs b/packages/app/src-tauri/src/lib.rs index 4c4df1c..c5a6953 100644 --- a/packages/app/src-tauri/src/lib.rs +++ b/packages/app/src-tauri/src/lib.rs @@ -84,6 +84,8 @@ pub fn run() { #[cfg(target_os = "macos")] let builder = builder.plugin(tauri_nspanel::init()); + let builder = skin_import::register_skin_protocol(builder); + builder .setup(|app| { tray::setup_tray(app)?; diff --git a/packages/app/src-tauri/src/skin_import.rs b/packages/app/src-tauri/src/skin_import.rs index 36153ed..0515ad1 100644 --- a/packages/app/src-tauri/src/skin_import.rs +++ b/packages/app/src-tauri/src/skin_import.rs @@ -2,7 +2,80 @@ use serde::Serialize; use std::fs; use std::io; use std::path::{Path, PathBuf}; -use tauri::Manager; +use tauri::http; +use tauri::{Manager, Runtime}; + +fn mime_from_ext(path: &str) -> &'static str { + match path.rsplit('.').next().unwrap_or("") { + "json" => "application/json", + "png" => "image/png", + "gif" => "image/gif", + "jpg" | "jpeg" => "image/jpeg", + "webp" => "image/webp", + "svg" => "image/svg+xml", + _ => "application/octet-stream", + } +} + +fn file_response(file_path: &Path, uri_path: &str) -> http::Response> { + match fs::read(file_path) { + Ok(data) => http::Response::builder() + .header(http::header::CONTENT_TYPE, mime_from_ext(uri_path)) + .header(http::header::ACCESS_CONTROL_ALLOW_ORIGIN, "*") + .body(data) + .unwrap(), + Err(_) => http::Response::builder() + .status(http::StatusCode::INTERNAL_SERVER_ERROR) + .body(Vec::new()) + .unwrap(), + } +} + +pub fn register_skin_protocol(builder: tauri::Builder) -> tauri::Builder { + builder.register_uri_scheme_protocol("skin", |ctx, request| { + let path = request.uri().path().trim_start_matches('/'); + + if path.is_empty() || path.contains("..") { + return http::Response::builder() + .status(http::StatusCode::BAD_REQUEST) + .body(Vec::new()) + .unwrap(); + } + + if cfg!(debug_assertions) { + let dev_path = Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .unwrap_or(Path::new(".")) + .join("public") + .join("skins") + .join(path); + if dev_path.is_file() { + return file_response(&dev_path, path); + } + } else { + if let Ok(data_dir) = ctx.app_handle().path().app_data_dir() { + let user_path = data_dir.join("skins").join(path); + if user_path.is_file() { + return file_response(&user_path, path); + } + } + + let asset_path = format!("skins/{}", path); + if let Some(asset) = ctx.app_handle().asset_resolver().get(asset_path) { + return http::Response::builder() + .header(http::header::CONTENT_TYPE, &asset.mime_type) + .header(http::header::ACCESS_CONTROL_ALLOW_ORIGIN, "*") + .body(asset.bytes) + .unwrap(); + } + } + + http::Response::builder() + .status(http::StatusCode::NOT_FOUND) + .body(Vec::new()) + .unwrap() + }) +} include!(concat!(env!("OUT_DIR"), "/builtin_skins.rs")); diff --git a/packages/app/src-tauri/tauri.conf.json b/packages/app/src-tauri/tauri.conf.json index ebefc6c..d90ef66 100644 --- a/packages/app/src-tauri/tauri.conf.json +++ b/packages/app/src-tauri/tauri.conf.json @@ -45,7 +45,7 @@ } ], "security": { - "csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' asset: https://asset.localhost data: blob:; connect-src 'self' ipc: http://ipc.localhost https://github.com https://api.github.com" + "csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' asset: https://asset.localhost skin: https://skin.localhost data: blob:; connect-src 'self' ipc: http://ipc.localhost skin: https://skin.localhost https://github.com https://api.github.com" }, "trayIcon": null }, diff --git a/packages/app/src/panels/AboutView.vue b/packages/app/src/panels/AboutView.vue index 00d9110..6cb1d67 100644 --- a/packages/app/src/panels/AboutView.vue +++ b/packages/app/src/panels/AboutView.vue @@ -2,6 +2,7 @@ import { openUrl } from '@tauri-apps/plugin-opener' import { useI18n } from '@/composables/useI18n' import { useAutoUpdater } from '@/composables/useAutoUpdater' +import { SKIN_BASE } from '@/stores/skin' const { t } = useI18n() const appVersion = __APP_VERSION__ @@ -30,7 +31,7 @@ async function open(url: string) {
- +

{{ t('brand') }}

{{ t('aboutDesc') }}

diff --git a/packages/app/src/panels/PeerAvatar.vue b/packages/app/src/panels/PeerAvatar.vue index 0cc6cf4..61646b4 100644 --- a/packages/app/src/panels/PeerAvatar.vue +++ b/packages/app/src/panels/PeerAvatar.vue @@ -4,6 +4,7 @@ import type { PeerInfo } from '@/types' import { MOVEMENT_COLORS } from '@/utils/activity' import { useI18n } from '@/composables/useI18n' import { MOVEMENT_STATE_KEYS } from '@/utils/movement' +import { SKIN_BASE } from '@/stores/skin' const { t } = useI18n() @@ -20,7 +21,7 @@ const rankDisplay = computed(() => (props.rank ? String(props.rank) : '')) const avatarFailed = ref(false) const avatarSrc = computed(() => { if (props.peer.petSkin && !avatarFailed.value) { - return `/skins/${props.peer.petSkin}/pet.png` + return `${SKIN_BASE}/${props.peer.petSkin}/pet.png` } return '' }) diff --git a/packages/app/src/panels/SkinMarket.vue b/packages/app/src/panels/SkinMarket.vue index ba4420f..7265c2f 100644 --- a/packages/app/src/panels/SkinMarket.vue +++ b/packages/app/src/panels/SkinMarket.vue @@ -1,7 +1,7 @@