您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
无限收藏虚拟形象 Limitless Favorite Avatar
当前为
// ==UserScript== // @name [Neko0] VRChat Avatar 无限收藏夹 // @name:zh [Neko0] VRChat 无限虚拟形象收藏夹 // @name:en [Neko0] VRChat Limitless Favorite Avatar // @description 无限收藏虚拟形象 Limitless Favorite Avatar // @description:zh 无限收藏虚拟形象 // @description:en Limitless Favorite Avatar // @version 1.0.8 // @author Mitsuki Joe // @namespace neko0-web-tools // @icon https://assets.vrchat.com/www/favicons/favicon.ico // @homepageURL https://github.com/nekozero/neko0-web-tools // @supportURL https://t.me/+FANQrUGRV7A0YmM9 // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant GM_getResourceText // @run-at document-idle // @license AGPL-3.0-or-later // @require https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.8.1/js/solid.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.8.1/js/fontawesome.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.0/jquery.min.js // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/axios.min.js // @require https://cdn.jsdelivr.net/npm/[email protected] // @require https://unpkg.com/@popperjs/core@2 // @require https://unpkg.com/tippy.js@6 // @require https://cdn.jsdelivr.net/npm/[email protected]/build/alertify.min.js // @resource IMPORTED_CSS_1 https://cdn.jsdelivr.net/npm/[email protected]/build/css/alertify.rtl.min.css // @match *://vrchat.com/* // @resource IMPORTED_CSS_2 https://cdn.jsdelivr.net/gh/nekozero/[email protected]/convenience/vrchat/style.css // @resource html-avatar-btn https://cdn.jsdelivr.net/gh/nekozero/[email protected]/convenience/vrchat/html-avatar-btn.html // @resource html-avatar-list https://cdn.jsdelivr.net/gh/nekozero/[email protected]/convenience/vrchat/html-avatar-list.html // @resource html-btn-group https://cdn.jsdelivr.net/gh/nekozero/[email protected]/convenience/vrchat/html-btn-group.html // @resource language https://cdn.jsdelivr.net/gh/nekozero/[email protected]/convenience/vrchat/language.json // ==/UserScript== console.log('VLAF Start') /** 初始化设定 开始 */ // 设置项默认值 let setting = { lang: 'en', } // 判断是否存在设定 if (GM_getValue('VLAF_setting') === undefined) { GM_setValue('VLAF_setting', setting) } else { let store = GM_getValue('VLAF_setting') $.each(setting, function (i) { if (store[i] === undefined) { store[i] = setting[i] } }) GM_setValue('VLAF_setting', store) } // 示例模型列表 // prettier-ignore let avatars = [{"id":"avtr_bc6c06ec-fda2-4490-8db2-946f618dba2d","name":"贴贴小S-M子","description":"准备后续再更新几套衣服,还有道具,欢迎加群 80697161","authorId":"usr_6621262d-0005-4d37-8624-19ecd4c30a76","authorName":"铃月时雨","tags":[],"imageUrl":"https://api.vrchat.cloud/api/1/file/file_01d6551a-7cf6-4dcb-b93c-4aeaadba9815/2/file","thumbnailImageUrl":"https://api.vrchat.cloud/api/1/image/file_01d6551a-7cf6-4dcb-b93c-4aeaadba9815/2/256","releaseStatus":"public","version":30,"featured":false,"unityPackages":[{"id":"unp_8cbb9ab2-fa25-483f-8c73-850b8b993916","created_at":"2022-12-23T11:42:08.740Z","unityVersion":"2019.4.31f1c1","assetVersion":1,"platform":"standalonewindows","variant":"standard"},{"id":"unp_d1e81047-992a-4a3c-9959-c7dad0c3d246","created_at":"2023-01-04T03:08:05.858Z","assetVersion":1,"platform":"android","unityVersion":"2019.4.31f1c1","variant":"standard"}],"unityPackageUrl":"","unityPackageUrlObject":{},"created_at":"2022-12-23T11:42:08.741Z","updated_at":"2023-01-14T11:10:41.376Z","addTime":"2023-05-01T00:00:00.000Z"},{"id":"avtr_a20ffadf-49e5-464a-86f7-c567c173d801","name":"M小龙娘ver2 -M子","description":"和ver1版本有点不一样(可能负优化所以分了两个版本,但是加了些别的功能)欢迎加群 80697161","authorId":"usr_6621262d-0005-4d37-8624-19ecd4c30a76","authorName":"铃月时雨","tags":[],"imageUrl":"https://api.vrchat.cloud/api/1/file/file_5141b6b1-c9f9-4fb2-ab33-f8a19af8fb43/2/file","thumbnailImageUrl":"https://api.vrchat.cloud/api/1/image/file_5141b6b1-c9f9-4fb2-ab33-f8a19af8fb43/2/256","releaseStatus":"public","version":11,"featured":false,"unityPackages":[{"id":"unp_1e6fe9b1-e269-4360-8ce2-966deba9f023","created_at":"2022-11-12T14:08:19.586Z","unityVersion":"2019.4.31f1c1","assetVersion":1,"platform":"standalonewindows","variant":"standard"},{"id":"unp_d8db6daf-72e3-4796-a839-12fdf51891f4","created_at":"2022-12-12T14:45:01.127Z","assetVersion":1,"platform":"android","unityVersion":"2019.4.31f1c1","variant":"standard"}],"unityPackageUrl":"","unityPackageUrlObject":{},"created_at":"2022-11-12T14:08:19.586Z","updated_at":"2023-01-02T14:13:50.273Z","addTime":"2023-05-01T00:00:00.000Z"},{"id":"avtr_b895678c-690d-4163-8c7f-5c655c8a9492","name":"可爱小菲-M子","description":"加群加群,后续更新以后再说","authorId":"usr_6621262d-0005-4d37-8624-19ecd4c30a76","authorName":"铃月时雨","tags":[],"imageUrl":"https://api.vrchat.cloud/api/1/file/file_f0277fd4-6975-49c8-8dad-5fabefefc09c/2/file","thumbnailImageUrl":"https://api.vrchat.cloud/api/1/image/file_f0277fd4-6975-49c8-8dad-5fabefefc09c/2/256","releaseStatus":"public","version":12,"featured":false,"unityPackages":[{"id":"unp_93eabfce-7979-4c72-a0dd-527ce72e4ae3","created_at":"2023-03-26T08:57:49.023Z","unityVersion":"2019.4.31f1c1","assetVersion":1,"platform":"standalonewindows","variant":"standard"},{"id":"unp_7f09fccb-4997-4637-bcf1-f95d7a5c99c4","created_at":"2023-03-30T13:51:57.832Z","assetVersion":1,"platform":"android","unityVersion":"2019.4.31f1c1","variant":"standard"}],"unityPackageUrl":"","unityPackageUrlObject":{},"created_at":"2023-03-26T08:57:49.023Z","updated_at":"2023-04-14T10:46:13.121Z","addTime":"2023-05-01T00:00:00.000Z"},{"id":"avtr_8b6da3f5-bd3d-4ce3-900e-7f92d40b554e","name":"M子小猫","description":"还是test阶段,有什么想加的功能可以喊我,交流群80697161","authorId":"usr_6621262d-0005-4d37-8624-19ecd4c30a76","authorName":"铃月时雨","tags":[],"imageUrl":"https://api.vrchat.cloud/api/1/file/file_416a230c-3d62-4af3-8856-5293490f2ae5/2/file","thumbnailImageUrl":"https://api.vrchat.cloud/api/1/image/file_416a230c-3d62-4af3-8856-5293490f2ae5/2/256","releaseStatus":"public","version":20,"featured":false,"unityPackages":[{"id":"unp_30d85467-9921-4c81-9338-926219c85d34","created_at":"2022-11-27T01:57:38.875Z","unityVersion":"2019.4.31f1c1","assetVersion":1,"platform":"standalonewindows","variant":"standard"},{"id":"unp_48a49a77-66f7-4e0c-b6f0-01e3346dae5d","created_at":"2022-12-11T13:08:59.003Z","assetVersion":1,"platform":"android","unityVersion":"2019.4.31f1c1","variant":"standard"}],"unityPackageUrl":"","unityPackageUrlObject":{},"created_at":"2022-11-27T01:57:38.875Z","updated_at":"2023-01-01T04:30:30.359Z","addTime":"2023-05-01T00:00:00.000Z"},{"id":"avtr_3985e9d3-3af1-4f82-9b15-71d2d8ef02c5","name":"M理沙-M子","description":"密码加群哦~群号 80697161","authorId":"usr_6621262d-0005-4d37-8624-19ecd4c30a76","authorName":"铃月时雨","tags":[],"imageUrl":"https://api.vrchat.cloud/api/1/file/file_64c31eae-2df4-49a3-bf28-513ca0e66dd0/2/file","thumbnailImageUrl":"https://api.vrchat.cloud/api/1/image/file_64c31eae-2df4-49a3-bf28-513ca0e66dd0/2/256","releaseStatus":"public","version":23,"featured":false,"unityPackages":[{"id":"unp_99e840fd-962a-407c-954e-0f41edf173fe","created_at":"2023-01-26T14:07:58.703Z","unityVersion":"2019.4.31f1c1","assetVersion":1,"platform":"standalonewindows","variant":"standard"},{"id":"unp_3ef58cc9-6758-4960-adf1-7f9eada82539","created_at":"2023-02-22T14:43:58.158Z","assetVersion":1,"platform":"android","unityVersion":"2019.4.31f1c1","variant":"standard"}],"unityPackageUrl":"","unityPackageUrlObject":{},"created_at":"2023-01-26T14:07:58.704Z","updated_at":"2023-02-23T14:24:12.284Z","addTime":"2023-05-01T00:00:00.000Z"},{"id":"avtr_194c4d8e-58fe-408e-9c72-38b310250fde","name":"可爱小果冻 - M子","description":"随便改改,可以调发色大小之类的,大小需要重新载入下模型,有点bug,欢迎来群里玩 80697161","authorId":"usr_6621262d-0005-4d37-8624-19ecd4c30a76","authorName":"铃月时雨","tags":[],"imageUrl":"https://api.vrchat.cloud/api/1/file/file_ffeb727e-1fec-4376-98b0-7a801a0f6180/2/file","thumbnailImageUrl":"https://api.vrchat.cloud/api/1/image/file_ffeb727e-1fec-4376-98b0-7a801a0f6180/2/256","releaseStatus":"public","version":5,"featured":false,"unityPackages":[{"id":"unp_c8b796fa-7b07-40b1-ae3d-62dfe3e5ad05","created_at":"2022-09-30T12:54:22.956Z","unityVersion":"2019.4.31f1c1","assetVersion":1,"platform":"standalonewindows","variant":"standard"},{"id":"unp_371c8959-aca6-4f47-b1e0-3dfa434b7c62","created_at":"2022-12-11T14:22:30.390Z","assetVersion":1,"platform":"android","unityVersion":"2019.4.31f1c1","variant":"standard"}],"unityPackageUrl":"","unityPackageUrlObject":{},"created_at":"2022-09-30T12:54:22.956Z","updated_at":"2022-12-11T14:53:12.063Z","addTime":"2023-05-01T00:00:00.000Z"}] if (GM_getValue('VLAF_avatars') === undefined) { GM_setValue('VLAF_avatars', avatars) } /** 初始化设定 结束 */ // 提示框位置 alertify.set('notifier', 'position', 'top-center') // 实时获取最新设置 let getSet = () => { return GM_getValue('VLAF_setting') } // 更改设置 let setSet = (key, value) => { let store = GM_getValue('VLAF_setting') store[key] = value GM_setValue('VLAF_setting', store) } // 实时获取最新模型列表 let getAvtrs = () => { return GM_getValue('VLAF_avatars') } // 文本内容多语言替换 let text = JSON.parse(GM_getResourceText('language'))[getSet().lang] console.log('getSet()', getSet()) console.log('lang', getSet().lang) console.log('text', text) // 置入Style GM_addStyle(GM_getResourceText('IMPORTED_CSS_1')) GM_addStyle(GM_getResourceText('IMPORTED_CSS_2')) // 正则替换DOM内“变量” // From: https://gist.github.com/cybercase/2298e242e82d32b15787 if (!String.prototype.format) { String.prototype.format = function (dict) { return this.replace(/{(\w+)}/g, function (match, key) { return typeof dict[key] !== 'undefined' ? dict[key] : match }) } } // 左侧导航栏 ;(function () { // 置入DOM function domBtnGroup() { let html = GM_getResourceText('html-btn-group') let output = html.format(text) $('.leftbar .btn-group-vertical').prepend(output) } // 检测页面内容置入插件DOM var timer = setInterval(detection, 300) detection() function detection() { var neko0 = document.querySelector('.limitless') if (!neko0) { domBtnGroup() } else { clearInterval(timer) alertify.success(text.mounted) } } // 绑定点击事件 // 打开设置窗口 // $('.n-box .button.switch').click(() => { // $('.n-box').toggleClass('open') // }) // setTimeout(() => { // alertify.success("You've clicked OK") // window.alertify = alertify // console.log('alertify') // }, 1000) })() // 判断已收藏 let isInVLAF = avtr_id => { let store = getAvtrs() return store.find(obj => obj.id === avtr_id) } // 格式化当前时间 let getNowDate = () => { // 定义一个函数来补齐两位数 function pad(num) { return num < 10 ? '0' + num : num } // 获取当前时间的 Date 对象 let date = new Date() // 获取年月日时分秒毫秒 let year = date.getFullYear() let month = pad(date.getMonth() + 1) let day = pad(date.getDate()) let hour = pad(date.getHours()) let minute = pad(date.getMinutes()) let second = pad(date.getSeconds()) let millisecond = pad(date.getMilliseconds()) // 拼接成 2022-07-19T20:50:50.033Z 这种格式 let formatted = `${year}-${month}-${day}T${hour}:${minute}:${second}.${millisecond}Z` // 打印结果 return formatted } // Detect error types let detectError = msg => { console.log('detectError', msg) if (msg == "You already have 50 favorite avatars in group 'avatars1'") return alertify.error(text.avatars_full) if (msg == 'You already have that avatar favorited') return alertify.error(text.avatars_added) if (msg == 'This avatar is unavailableǃ') return alertify.error(text.error_unavailable) if (msg == "avatar isn't public and avatar is also not owned by you") return alertify.error(text.error_private) if (msg == "Can't find avatarǃ") return alertify.error(text.error_deleted) return alertify.error(text.operation_failed) } // 马上切换 let select = avtr_id => { url = window.location.origin + '/api/1/avatars/' + avtr_id + '/select' axios .put(url) .then(function (response) { console.log(response) alertify.success(text.operation_succeeded) }) .catch(function (error) { console.log(error) detectError(error.response.data.error.message) }) .finally(function () {}) } // 收藏到系统收藏夹 let favorites = avtr_id => { url = window.location.origin + '/api/1/favorites' val = { type: 'avatar', favoriteId: avtr_id, tags: ['avatars1'], } axios .post(url, val) .then(function (response) { console.log(response) alertify.success(text.operation_succeeded) }) .catch(function (error) { console.log(error) detectError(error.response.data.error.message) }) .finally(function () {}) } // 收藏到无限收藏夹 let limitless = avtr_id => { console.log('ƒ limitless') url = window.location.origin + '/api/1/avatars/' + avtr_id let store = getAvtrs() const result = isInVLAF(avtr_id) if (result) { console.log('存在') store = store.filter(function (obj) { return obj.id !== avtr_id }) $('#collect').text(text.btn_collect).removeClass('text-danger border-danger') GM_setValue('VLAF_avatars', store) } else { console.log('不存在') let data = null axios .get(url) .then(function (response) { console.log('limitless', response) alertify.success(text.operation_succeeded) data = response.data data.addTime = getNowDate() store.push(data) $('#collect').text(text.btn_collect_r).addClass('text-danger border-danger') GM_setValue('VLAF_avatars', store) }) .catch(function (error) { console.log(error) }) .finally(function () {}) } } // 不同页面 let page_is_avtr_own = () => { return document.location.pathname === '/home/avatars' } let page_is_avtr_details = () => { return document.location.pathname.indexOf('/home/avatar/avtr_') !== -1 } let page_is_limitless = () => { return document.location.pathname === '/home/limitless' } let pluginInject = () => { if (!page_is_limitless() && $('.neko0.limitless-list.row')[0]) { $('.neko0.limitless-list.row')[0].remove() } if (page_is_avtr_own()) { console.log('page_is_avtr_own') // 当前使用Avatar // let current_avtr_id = document.querySelector('[data-scrollkey]').getAttribute('data-scrollkey') // console.log(current_avtr_id) // let current_avtr_info = null // ;(function () { // url = // 'https://vrchat.com/api/1/users/' + // document.querySelector('[aria-label="User Status"]').getAttribute('href').substring(11) + // '/avatar' // axios // .get(url) // .then(function (response) { // console.log(response) // current_avtr_info = response.data // }) // .catch(function (error) { // console.log(error) // }) // .finally(function () { // }) // })() // 算了暂时先不改这个 } else if (page_is_avtr_details()) { // 当前浏览Avatar let current_avtr_id = window.location.pathname.substring(13) console.log('page_is_avtr_details', isInVLAF(current_avtr_id), getAvtrs()) // 置入DOM function domAvatar() { let html = GM_getResourceText('html-avatar-btn') let output = html.format(text) $('.col-xs-12.content-scroll .home-content .row:nth-child(2) .col-4 .btn-group-vertical') .attr('id', 'neko0') .append(output) if (isInVLAF(current_avtr_id)) { $('#collect').text(text.btn_collect_r).addClass('text-danger border-danger') } tippy('#transmit', { content: text.tippy_transmit, }) tippy('#use', { content: text.tippy_use, }) tippy('#collect', { content: text.tippy_collect, }) $('#transmit').click(() => { favorites(current_avtr_id) }) $('#use').click(() => { select(current_avtr_id) }) $('#collect').click(() => { limitless(current_avtr_id) }) } // 检测页面内容置入插件DOM var timer = setInterval(detection, 300) detection() function detection() { var neko0 = document.querySelector('.neko0') if (!neko0) { domAvatar() } else { clearInterval(timer) } } console.log(text.mounted) } else if (page_is_limitless()) { console.log('page_is_limitless', getAvtrs()) // 置入DOM function domLimitless() { let html = GM_getResourceText('html-avatar-list') let output = html $('.home-content').append(output) new Vue({ el: '#neko0', data: { text: text, items: getAvtrs(), }, methods: { // 语言切换 languageSwitch: function () { getSet().lang === 'en' ? setSet('lang', 'zh_cn') : setSet('lang', 'en') text = JSON.parse(GM_getResourceText('language'))[getSet().lang] location.reload() }, // 导出导入 exportList: function () { // 将 JSON 数据转换为字符串 const jsonString = JSON.stringify(getAvtrs()) // 创建一个 Blob 对象 const blob = new Blob([jsonString], { type: 'application/json' }) // 创建一个下载链接 const link = document.createElement('a') link.href = URL.createObjectURL(blob) link.download = 'LimitlessAvatars.json' // 模拟点击链接下载文件 document.body.appendChild(link) link.click() }, importList: function () { console.log('ƒ importList') const fileInput = document.getElementById('file-input') fileInput.click() }, fileUpload: function () { console.log('ƒ fileUpload') const fileInput = document.getElementById('file-input') const file = fileInput.files[0] const reader = new FileReader() reader.onload = event => { const fileContent = event.target.result const jsonData = JSON.parse(fileContent) console.log('import:', jsonData) const A = getAvtrs() const B = jsonData const diff = _.differenceBy(B, A, 'id') const merge = _.concat(A, diff) console.log('merge:', merge) GM_setValue('VLAF_avatars', merge) this.items = merge } reader.readAsText(file) }, // 格式化时间 formattedDate: function (str) { const dateStr = str const date = new Date(dateStr) const year = date.getFullYear() const month = date.getMonth() + 1 const day = date.getDate() const formattedDate = `${year}-${month.toString().padStart(2, '0')}-${day .toString() .padStart(2, '0')}` console.log(formattedDate) return formattedDate }, hasWindows: function (obj) { // 定义一个变量来存储检查结果 let hasWindows = false // 遍历对象中的 unityPackages 数组 for (let package of obj.unityPackages) { // 如果某个元素的 platform 属性等于 standalonewindows,就将结果设为 true,并跳出循环 if (package.platform === 'standalonewindows') { hasWindows = true break } } return hasWindows }, hasAndroid: function (obj) { // 定义一个变量来存储检查结果 let hasAndroid = false // 遍历对象中的 unityPackages 数组 for (let package of obj.unityPackages) { // 如果某个元素的 platform 属性等于 android,就将结果设为 true,并跳出循环 if (package.platform === 'android') { hasAndroid = true break } } return hasAndroid }, favorites: function (avtr_id) { favorites(avtr_id) }, select: function (avtr_id) { select(avtr_id) }, limitless: function (avtr_id) { limitless(avtr_id) $('[dat-a="' + avtr_id + '"]') .parents('.avatar-li') .remove() }, }, created: function () { let _this = this window.add_data = _this.add_data }, mounted() { tippy('.transmit', { content: text.tippy_transmit, }) tippy('.use', { content: text.tippy_use, }) tippy('.collect', { content: text.tippy_collect, }) tippy('.export', { content: text.tippy_export, }) tippy('.import', { content: text.tippy_import, }) // 检测Avatar的图片是否加载失败 const divs = document.querySelectorAll('.neko0 .img') console.log('divs', divs) for (let i = 0; i < divs.length; i++) { const div = divs[i] const img = new Image() img.src = div.style.backgroundImage.replace(/url\(['"]?([^'"]+)['"]?\)/i, '$1') img.addEventListener('load', () => { console.log(`Avatar image loaded successfully.`) }) img.addEventListener('error', () => { // 打印错误位置 console.log(`Avatar image in div ${i + 1} failed to load.`) // 更改图示 div.style.backgroundImage = `url("${text.broken_image}")` // 更改描述 div.parentElement.parentElement.nextElementSibling.querySelector( '.description .value' ).textContent = text.broken_description // 增加Class div.parentElement.parentElement.parentElement.classList.add('broken') }) } }, }) } // 检测页面内容置入插件DOM var timer = setInterval(detection, 300) detection() function detection() { var neko0 = document.querySelector('.neko0') if (!neko0) { domLimitless() } else { clearInterval(timer) } } console.log(text.mounted) } } pluginInject() // 监测页面变换 const _historyWrap = function (type) { const orig = history[type] const e = new Event(type) return function () { const rv = orig.apply(this, arguments) e.arguments = arguments window.dispatchEvent(e) return rv } } history.pushState = _historyWrap('pushState') history.replaceState = _historyWrap('replaceState') window.addEventListener('pushState', function (e) { console.log('change pushState') pluginInject() }) window.addEventListener('replaceState', function (e) { console.log('change replaceState') })