diff --git a/ASSETS.md b/ASSETS.md index 5d55ae2..de68865 100644 --- a/ASSETS.md +++ b/ASSETS.md @@ -5,7 +5,6 @@ Contributors and are available, at your option, under either the MIT License or the Apache License, Version 2.0: - all files in `sprites/british-blue/`; -- all files in `sprites/douhua-cat/`; - all files in `sprites/huajuan-cat/`; - all files in `sprites/tabby-cat/`; - the corresponding `.fpack` archives in `sprites/`; diff --git a/Cargo.lock b/Cargo.lock index 2b34729..d78315c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1370,7 +1370,7 @@ checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" [[package]] name = "familiar-api" -version = "2.2.0" +version = "2.2.1" dependencies = [ "anyhow", "axum", @@ -1386,7 +1386,7 @@ dependencies = [ [[package]] name = "familiar-app" -version = "2.2.0" +version = "2.2.1" dependencies = [ "anyhow", "axum", @@ -1415,7 +1415,7 @@ dependencies = [ [[package]] name = "familiar-cli" -version = "2.2.0" +version = "2.2.1" dependencies = [ "anyhow", "axum", @@ -1434,7 +1434,7 @@ dependencies = [ [[package]] name = "familiar-core" -version = "2.2.0" +version = "2.2.1" dependencies = [ "anyhow", "chrono", @@ -1453,7 +1453,7 @@ dependencies = [ [[package]] name = "familiar-hooks" -version = "2.2.0" +version = "2.2.1" dependencies = [ "anyhow", "async-trait", @@ -2078,6 +2078,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http-range" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" + [[package]] name = "http-range-header" version = "0.4.2" @@ -4522,6 +4528,7 @@ dependencies = [ "gtk", "heck 0.5.0", "http", + "http-range", "image", "jni", "libc", diff --git a/Cargo.toml b/Cargo.toml index 7e343ae..063b889 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ resolver = "2" [workspace.package] -version = "2.2.0" +version = "2.2.1" edition = "2021" rust-version = "1.88" license = "MIT OR Apache-2.0" diff --git a/app/package-lock.json b/app/package-lock.json index a724fc6..22755c3 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -1,12 +1,12 @@ { "name": "familiar-app-ui", - "version": "2.2.0", + "version": "2.2.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "familiar-app-ui", - "version": "2.2.0", + "version": "2.2.1", "license": "MIT OR Apache-2.0", "devDependencies": { "@tauri-apps/api": "^2.11.1", diff --git a/app/package.json b/app/package.json index 5064723..c97ae9b 100644 --- a/app/package.json +++ b/app/package.json @@ -1,6 +1,6 @@ { "name": "familiar-app-ui", - "version": "2.2.0", + "version": "2.2.1", "private": true, "license": "MIT OR Apache-2.0", "type": "module", diff --git a/app/src-tauri/Cargo.toml b/app/src-tauri/Cargo.toml index 3e698a0..e738e75 100644 --- a/app/src-tauri/Cargo.toml +++ b/app/src-tauri/Cargo.toml @@ -11,7 +11,7 @@ publish.workspace = true [dependencies] familiar-core = { path = "../../crates/familiar-core" } familiar-api = { path = "../../crates/familiar-api" } -tauri = { version = "2.0.0-rc", features = ["macos-private-api", "tray-icon", "image-png"] } +tauri = { version = "2.0.0-rc", features = ["protocol-asset", "macos-private-api", "tray-icon", "image-png"] } tauri-plugin-single-instance = "2" tokio = { workspace = true } tokio-tungstenite = { workspace = true, features = ["native-tls"] } diff --git a/app/src-tauri/src/commands.rs b/app/src-tauri/src/commands.rs index d06d147..2c61ed5 100644 --- a/app/src-tauri/src/commands.rs +++ b/app/src-tauri/src/commands.rs @@ -1095,7 +1095,10 @@ pub fn get_sprite_packs(app_handle: tauri::AppHandle) -> Result) -> Result { +pub async fn import_sprite_pack( + app_handle: tauri::AppHandle, + path: Option, +) -> Result { let import_path = if let Some(p) = path.filter(|s| !s.is_empty()) { std::path::PathBuf::from(p) } else { @@ -1111,7 +1114,22 @@ pub async fn import_sprite_pack(path: Option) -> Result>, + id: String, +) -> Result<(), String> { + if config_state.get_config().renderer.desktop_pet.sprite == id { + return Err("Cannot delete the active sprite pack; switch packs first".to_string()); + } + + SpritePackManager::delete_user_pack(&id).map_err(|error| error.to_string()) } #[tauri::command] diff --git a/app/src-tauri/src/main.rs b/app/src-tauri/src/main.rs index ed431dd..29d9971 100644 --- a/app/src-tauri/src/main.rs +++ b/app/src-tauri/src/main.rs @@ -600,6 +600,7 @@ fn main() { commands::test_hook_point, commands::get_sprite_packs, commands::import_sprite_pack, + commands::delete_sprite_pack, commands::get_active_sprite_pack, commands::open_sprite_dir, commands::quit_app, diff --git a/app/src-tauri/tauri.conf.json b/app/src-tauri/tauri.conf.json index a821e6f..3ebc1d1 100644 --- a/app/src-tauri/tauri.conf.json +++ b/app/src-tauri/tauri.conf.json @@ -8,6 +8,15 @@ "app": { "withGlobalTauri": true, "macOSPrivateApi": true, + "security": { + "assetProtocol": { + "enable": true, + "scope": [ + "$HOME/.config/familiar/sprites/**/*", + "$APPDATA/familiar/sprites/**/*" + ] + } + }, "windows": [ { "label": "main", diff --git a/app/src/i18n.js b/app/src/i18n.js index 40562cb..e63add6 100644 --- a/app/src/i18n.js +++ b/app/src/i18n.js @@ -172,12 +172,16 @@ export const translations = { 'lbl_pack_email': '联系邮箱', 'lbl_builtin': '系统自带', 'lbl_custom': '自定义导入', - 'btn_use_pack': '使用此素材包', + 'btn_use_pack': '使用', 'btn_preview_pack': '预览动作', + 'btn_delete_pack': '删除素材包', 'title_preview_modal': '素材动作预览', 'lbl_in_use': '已使用', 'msg_import_success': '素材包导入成功!', 'msg_import_failed': '素材包导入失败: ', + 'msg_delete_pack_confirm': '再次点击删除按钮以删除“{name}”', + 'msg_delete_pack_success': '素材包已删除', + 'msg_delete_pack_failed': '素材包删除失败: ', 'state_idle': '待命 (idle)', 'state_working': '工作中 (working)', 'state_thinking': '思考中 (thinking)', @@ -484,12 +488,16 @@ export const translations = { 'lbl_pack_email': 'Email', 'lbl_builtin': 'Built-in', 'lbl_custom': 'Custom', - 'btn_use_pack': 'Use Pack', + 'btn_use_pack': 'Use', 'btn_preview_pack': 'Preview', + 'btn_delete_pack': 'Delete Pack', 'title_preview_modal': 'Action Preview', 'lbl_in_use': 'In Use', 'msg_import_success': 'Sprite pack imported successfully!', 'msg_import_failed': 'Failed to import sprite pack: ', + 'msg_delete_pack_confirm': 'Click delete again to remove “{name}”', + 'msg_delete_pack_success': 'Sprite pack deleted', + 'msg_delete_pack_failed': 'Failed to delete sprite pack: ', 'state_idle': 'Idle', 'state_working': 'Working', 'state_thinking': 'Thinking', diff --git a/app/src/main.js b/app/src/main.js index 8465206..baa60a4 100644 --- a/app/src/main.js +++ b/app/src/main.js @@ -276,12 +276,18 @@ function setupContextMenu() { } let currentSpriteId = null; +let currentSpriteRevision = null; -async function loadActiveSpritePack() { +async function loadActiveSpritePack(force = true) { try { const packInfo = await invoke("get_active_sprite_pack"); if (packInfo && packInfo.manifest) { + const samePack = currentSpriteId === packInfo.manifest.id + && currentSpriteRevision === packInfo.asset_revision; + if (!force && samePack) return; + currentSpriteId = packInfo.manifest.id; + currentSpriteRevision = packInfo.asset_revision; await renderer.loadSpritePack(packInfo); renderer.playAnimation("idle"); } @@ -373,8 +379,17 @@ async function init() { }); // Listen for config changes from Rust Backend - await listen("config_changed", (event) => { - applyConfigToWindow(event.payload); + await listen("config_changed", async (event) => { + await applyConfigToWindow(event.payload); + }); + + await listen("sprite_pack_imported", async (event) => { + const imported = event.payload; + if (!imported || !imported.manifest || imported.manifest.id !== currentSpriteId) return; + + currentSpriteRevision = imported.asset_revision; + await renderer.loadSpritePack(imported); + renderer.playAnimation("idle"); }); } @@ -431,8 +446,8 @@ async function applyConfigToWindow(config) { const petConf = config.renderer['desktop-pet']; - if (petConf.sprite && currentSpriteId && petConf.sprite !== currentSpriteId) { - loadActiveSpritePack(); + if (petConf.sprite && currentSpriteId) { + await loadActiveSpritePack(false); } baseOpacity = petConf.opacity !== undefined ? petConf.opacity : 1; diff --git a/app/src/pet/PixelSpriteRenderer.js b/app/src/pet/PixelSpriteRenderer.js index 109242b..2a6f903 100644 --- a/app/src/pet/PixelSpriteRenderer.js +++ b/app/src/pet/PixelSpriteRenderer.js @@ -104,13 +104,20 @@ export class PixelSpriteRenderer extends SpriteRenderer { const packId = this.manifest.id || this.manifest.name || 'british-blue'; if (this.packInfo && !this.packInfo.is_builtin && this.packInfo.path) { try { - return convertFileSrc(`${this.packInfo.path}/${fileName}`); + return this._withAssetRevision(convertFileSrc(`${this.packInfo.path}/${fileName}`)); } catch (e) { - return `/sprites/${packId}/${fileName}`; + return this._withAssetRevision(`/sprites/${packId}/${fileName}`); } } - return `/sprites/${packId}/${fileName}`; + return this._withAssetRevision(`/sprites/${packId}/${fileName}`); + } + + _withAssetRevision(url) { + const revision = this.packInfo && this.packInfo.asset_revision; + if (!revision) return url; + const separator = url.includes('?') ? '&' : '?'; + return `${url}${separator}v=${encodeURIComponent(revision)}`; } _resizeElement(element, width, height) { diff --git a/app/src/settings.css b/app/src/settings.css index 592cd6a..731e010 100644 --- a/app/src/settings.css +++ b/app/src/settings.css @@ -483,6 +483,11 @@ input:checked + .slider:before { .danger-btn:hover { background-color: rgba(255, 59, 48, 0.2); } +.btn-delete-pack.confirming { + color: #fff; + background-color: #FF3B30; + border-color: #FF3B30; +} /* --- Hook Agent Collapsible Cards --- */ .hook-agent-card { background-color: var(--bg-card); diff --git a/app/src/settings.js b/app/src/settings.js index 4fa3f0b..5d554a8 100644 --- a/app/src/settings.js +++ b/app/src/settings.js @@ -837,6 +837,7 @@ document.addEventListener('DOMContentLoaded', async () => { packs.forEach(pack => { const manifest = pack.manifest; const isActive = manifest.id === activeSprite; + const description = simplifyPackDescription(manifest.description); let previewSrc = ''; const previewFile = manifest.preview || 'idle.png'; @@ -845,6 +846,7 @@ document.addEventListener('DOMContentLoaded', async () => { } else { previewSrc = convertFileSrc(`${pack.path}/${previewFile}`); } + previewSrc = withAssetRevision(previewSrc, pack); const card = document.createElement('div'); card.className = `sprite-pack-card ${isActive ? 'active' : ''}`; @@ -863,19 +865,22 @@ document.addEventListener('DOMContentLoaded', async () => {
${t('lbl_pack_author', lang)}: ${manifest.author || 'Unknown'}
-
${manifest.description || ''}
+
${description}
${badgeText} ${manifest.created_at ? `${t('lbl_pack_created', lang)}: ${manifest.created_at}` : ''} ${manifest.email ? `${t('lbl_pack_email', lang)}: ${manifest.email}` : ''}