diff --git a/package-lock.json b/package-lock.json index 5b43ab82..505bf6bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -87,7 +87,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1555,6 +1554,7 @@ "dev": true, "license": "BSD-2-Clause", "optional": true, + "peer": true, "dependencies": { "cross-dirname": "^0.1.0", "debug": "^4.3.4", @@ -1576,6 +1576,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -1592,6 +1593,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "universalify": "^2.0.0" }, @@ -1606,6 +1608,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">= 10.0.0" } @@ -1698,6 +1701,7 @@ "os": [ "aix" ], + "peer": true, "engines": { "node": ">=18" } @@ -1715,6 +1719,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">=18" } @@ -1732,6 +1737,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">=18" } @@ -1749,6 +1755,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">=18" } @@ -1766,6 +1773,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">=18" } @@ -1783,6 +1791,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">=18" } @@ -1800,6 +1809,7 @@ "os": [ "freebsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -1817,6 +1827,7 @@ "os": [ "freebsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -1834,6 +1845,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -1851,6 +1863,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -1868,6 +1881,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -1885,6 +1899,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -1902,6 +1917,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -1919,6 +1935,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -1936,6 +1953,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -1953,6 +1971,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -1970,6 +1989,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -1987,6 +2007,7 @@ "os": [ "netbsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -2004,6 +2025,7 @@ "os": [ "netbsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -2021,6 +2043,7 @@ "os": [ "openbsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -2038,6 +2061,7 @@ "os": [ "openbsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -2055,6 +2079,7 @@ "os": [ "openharmony" ], + "peer": true, "engines": { "node": ">=18" } @@ -2072,6 +2097,7 @@ "os": [ "sunos" ], + "peer": true, "engines": { "node": ">=18" } @@ -2089,6 +2115,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=18" } @@ -2106,6 +2133,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=18" } @@ -2123,6 +2151,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=18" } @@ -5321,7 +5350,6 @@ "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/lodash": "*" } @@ -5958,7 +5986,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6019,7 +6046,6 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -6757,7 +6783,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -7705,7 +7730,8 @@ "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", "dev": true, "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/cross-env": { "version": "10.1.0", @@ -8404,6 +8430,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "@electron/asar": "^3.2.1", "debug": "^4.1.1", @@ -8424,6 +8451,7 @@ "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", @@ -8801,7 +8829,6 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -8862,7 +8889,6 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -13310,7 +13336,6 @@ "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", "dev": true, "license": "MPL-2.0", - "peer": true, "dependencies": { "detect-libc": "^2.0.3" }, @@ -13594,16 +13619,14 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/lodash-es": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/lodash-unified": { "version": "1.0.3", @@ -14812,7 +14835,6 @@ "integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vue/devtools-api": "^7.7.7" }, @@ -14983,6 +15005,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "commander": "^9.4.0" }, @@ -15000,6 +15023,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": "^12.20.0 || >=14" } @@ -15020,7 +15044,6 @@ "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -15410,6 +15433,7 @@ "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "glob": "^7.1.3" }, @@ -15424,6 +15448,7 @@ "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -15445,6 +15470,7 @@ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -17247,6 +17273,7 @@ "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "mkdirp": "^0.5.1", "rimraf": "~2.6.2" @@ -17310,6 +17337,7 @@ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "minimist": "^1.2.6" }, @@ -17428,7 +17456,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -17604,7 +17631,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -17950,7 +17976,6 @@ "integrity": "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -18528,7 +18553,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -18542,7 +18566,6 @@ "integrity": "sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.25", "@vue/compiler-sfc": "3.5.25", diff --git a/src/localization/en.json b/src/localization/en.json index d0bb04d8..c7eeb030 100644 --- a/src/localization/en.json +++ b/src/localization/en.json @@ -172,7 +172,21 @@ "local_favorites": "Local Favorites (Requires VRC+)", "new_group": "New Group", "refresh": "Refresh", - "cancel_refresh": "Cancel Refresh" + "cancel_refresh": "Cancel Refresh", + "check_invalid": "Check Invalid Avatars in This Group", + "check_description": "Detect and remove invalid avatars in this group", + "checking": "Checking invalid avatars...", + "check_progress": "Progress: {current}/{total}", + "check_complete": "Check complete!", + "check_summary": "Checked {total} avatars, found {invalid} invalid, removed {removed}", + "removed_list_header": "Removed avatar list:", + "copy_removed_ids": "Copy Removed Avatar IDs", + "checking_progress": "Checking avatar ({current}/{total})...", + "confirm_delete_invalid": "Delete Invalid Avatars?", + "confirm_delete_description": "Found {count} invalid avatars, delete them?", + "delete_summary": "Successfully deleted {removed} invalid avatars", + "no_invalid_found": "No invalid avatars found", + "delete_cancelled": "Delete operation cancelled" }, "edit_mode": "Edit Mode", "copy": "Copy", diff --git a/src/localization/ja.json b/src/localization/ja.json index a32db581..7491cba6 100644 --- a/src/localization/ja.json +++ b/src/localization/ja.json @@ -142,7 +142,21 @@ "local_favorites": "ローカルのお気に入り (VRC+が必要)", "new_group": "グループ作成", "refresh": "更新", - "cancel_refresh": "キャッシュ削除" + "cancel_refresh": "キャッシュ削除", + "check_invalid": "このグループの無効なアバターをチェック", + "check_description": "このグループ内の無効なアバターを検出して削除", + "checking": "無効なアバターをチェック中...", + "check_progress": "進行状況:{current}/{total}", + "check_complete": "チェック完了!", + "check_summary": "{total}個のアバターをチェックし、{invalid}個の無効なアバターを発見、{removed}個を削除しました", + "removed_list_header": "削除されたアバターリスト:", + "copy_removed_ids": "削除されたアバターIDをコピー", + "checking_progress": "アバター確認中 ({current}/{total})...", + "confirm_delete_invalid": "無効なアバターを削除しますか?", + "confirm_delete_description": "{count}個の無効なアバターが見つかりました。削除しますか?", + "delete_summary": "{removed}個の無効なアバターを正常に削除しました", + "no_invalid_found": "無効なアバターは見つかりませんでした", + "delete_cancelled": "削除操作がキャンセルされました" }, "edit_mode": "編集モード", "copy": "コピー", diff --git a/src/localization/ko.json b/src/localization/ko.json index 2c45d9c7..479d7608 100644 --- a/src/localization/ko.json +++ b/src/localization/ko.json @@ -97,7 +97,21 @@ "search": "검색", "vrchat_favorites": "VRChat 즐겨찾기", "local_favorites": "Local Favorites (Requires VRC+)", - "new_group": "새 그룹" + "new_group": "새 그룹", + "check_invalid": "이 그룹의 유효하지 않은 모델 확인", + "check_description": "이 그룹에서 유효하지 않은 모델을 감지하고 삭제", + "checking": "유효하지 않은 모델 확인 중...", + "check_progress": "확인 진행: {current}/{total}", + "check_complete": "확인 완료!", + "check_summary": "{total}개의 모델을 확인했고, {invalid}개의 유효하지 않은 모델을 발견했으며, {removed}개를 제거했습니다", + "removed_list_header": "제거된 모델 목록:", + "copy_removed_ids": "제거된 모델 ID 복사", + "checking_progress": "모델 확인 중 ({current}/{total})...", + "confirm_delete_invalid": "유효하지 않은 모델을 삭제하시겠습니까?", + "confirm_delete_description": "{count}개의 유효하지 않은 모델을 발견했습니다. 삭제하시겠습니까?", + "delete_summary": "{removed}개의 유효하지 않은 모델을 성공적으로 삭제했습니다", + "no_invalid_found": "유효하지 않은 모델을 찾을 수 없습니다", + "delete_cancelled": "삭제 작업이 취소되었습니다" }, "bulk_unfavorite_mode": "즐겨찾기 해제 모드", "bulk_unfavorite_selection": "선택한 즐겨찾기 해제", diff --git a/src/localization/zh-CN.json b/src/localization/zh-CN.json index 7bcef113..5170e838 100644 --- a/src/localization/zh-CN.json +++ b/src/localization/zh-CN.json @@ -172,7 +172,21 @@ "local_favorites": "本地收藏(需要 VRC+)", "new_group": "创建新的收藏夹", "refresh": "刷新", - "cancel_refresh": "清除缓存" + "cancel_refresh": "清除缓存", + "check_invalid": "检查这个分组的失效模型", + "check_description": "检测并删除这个分组中的失效模型", + "checking": "正在检查失效模型...", + "check_progress": "检查进度:{current}/{total}", + "checking_progress": "正在检查模型 ({current}/{total})...", + "confirm_delete_invalid": "删除失效模型?", + "confirm_delete_description": "发现 {count} 个失效模型,是否删除它们?", + "check_complete": "检查完成!", + "check_summary": "共检查 {total} 个模型,发现 {invalid} 个失效模型", + "delete_summary": "已成功删除 {removed} 个失效模型", + "removed_list_header": "已删除的模型列表:", + "copy_removed_ids": "复制已删除的模型ID", + "no_invalid_found": "未发现失效模型", + "delete_cancelled": "已取消删除操作" }, "edit_mode": "编辑模式", "copy": "复制", diff --git a/src/stores/favorite.js b/src/stores/favorite.js index b610a58e..45d578c0 100644 --- a/src/stores/favorite.js +++ b/src/stores/favorite.js @@ -9,7 +9,7 @@ import { replaceReactiveObject } from '../shared/utils'; import { database } from '../service/database'; -import { favoriteRequest } from '../api'; +import { avatarRequest, favoriteRequest } from '../api'; import { processBulk } from '../service/request'; import { useAppearanceSettingsStore } from './settings/appearance'; import { useAvatarStore } from './avatar'; @@ -1252,6 +1252,97 @@ export const useFavoriteStore = defineStore('Favorite', () => { } } + /** + * Check invalid local avatar favorites + * @param {string | null} targetGroup - Target group to check, null for all groups + * @param {Function | null} onProgress - Progress callback function, receives (current, total) parameters + * @returns {Promise<{total: number, invalid: number, invalidIds: string[]}>} + */ + async function checkInvalidLocalAvatars(targetGroup = null, onProgress = null) { + const result = { + total: 0, + invalid: 0, + invalidIds: [] + }; + + const groupsToCheck = targetGroup + ? [targetGroup] + : localAvatarFavoriteGroups.value; + + for (const group of groupsToCheck) { + const favoriteGroup = localAvatarFavorites[group]; + if (favoriteGroup && favoriteGroup.length > 0) { + result.total += favoriteGroup.length; + } + } + + let currentIndex = 0; + + for (const group of groupsToCheck) { + const favoriteGroup = localAvatarFavorites[group]; + if (!favoriteGroup || favoriteGroup.length === 0) { + continue; + } + + for (const favorite of favoriteGroup) { + currentIndex++; + + if (typeof onProgress === 'function') { + onProgress(currentIndex, result.total); + } + + try { + await avatarRequest.getAvatar({ + avatarId: favorite.id + }); + await new Promise(resolve => setTimeout(resolve, 500)); + } catch (err) { + result.invalid++; + result.invalidIds.push(favorite.id); + } + } + } + + return result; + } + + /** + * Remove invalid avatars from local favorites + * @param {string[]} avatarIds - Array of avatar IDs to remove + * @param {string | null} targetGroup - Target group, null for all groups + * @returns {Promise<{removed: number, removedIds: string[]}>} + */ + async function removeInvalidLocalAvatars(avatarIds, targetGroup = null) { + const result = { + removed: 0, + removedIds: [] + }; + + const groupsToCheck = targetGroup + ? [targetGroup] + : localAvatarFavoriteGroups.value; + + for (const group of groupsToCheck) { + const favoriteGroup = localAvatarFavorites[group]; + if (!favoriteGroup) { + continue; + } + + for (const avatarId of avatarIds) { + const index = favoriteGroup.findIndex(fav => fav.id === avatarId); + if (index !== -1) { + removeLocalAvatarFavorite(avatarId, group); + result.removed++; + if (!result.removedIds.includes(avatarId)) { + result.removedIds.push(avatarId); + } + } + } + } + + return result; + } + /** * * @param {string} newName @@ -1507,6 +1598,8 @@ export const useFavoriteStore = defineStore('Favorite', () => { handleFavoriteGroup, handleFavoriteDelete, handleFavoriteAdd, - getCachedFavoritesByObjectId + getCachedFavoritesByObjectId, + checkInvalidLocalAvatars, + removeInvalidLocalAvatars }; }); diff --git a/src/views/Favorites/FavoritesAvatar.vue b/src/views/Favorites/FavoritesAvatar.vue index fd986e8e..85209d63 100644 --- a/src/views/Favorites/FavoritesAvatar.vue +++ b/src/views/Favorites/FavoritesAvatar.vue @@ -240,6 +240,12 @@ @click="handleLocalRename(group)"> {{ t('view.favorite.rename_tooltip') }} +