- // ==UserScript==
- // @name VRChat Web Pages Extender
- // @name:ja VRChat Webページ拡張
- // @description Add features into VRChat Web Pages and improve user experience.
- // @description:ja VRChatのWebページに機能を追加し、また使い勝手を改善します。
- // @namespace https://greasyfork.org/users/137
- // @version 2.19.0
- // @match https://vrchat.com/home
- // @match https://vrchat.com/home?*
- // @match https://vrchat.com/home#*
- // @match https://vrchat.com/home/*
- // @require https://greasyfork.org/scripts/19616/code/utilities.js?version=895049
- // @license MPL-2.0
- // @contributionURL https://pokemori.booth.pm/items/969835
- // @compatible Edge
- // @compatible Firefox Firefoxを推奨 / Firefox is recommended
- // @compatible Opera
- // @compatible Chrome
- // @grant dummy
- // @run-at document-start
- // @icon https://images.squarespace-cdn.com/content/v1/5f0770791aaf57311515b23d/1599678606410-4QMTB25DHF87E8EFFKXY/ke17ZwdGBToddI8pDm48kGfiFqkITS6axXxhYYUCnlRZw-zPPgdn4jUwVcJE1ZvWQUxwkmyExglNqGp0IvTJZUJFbgE-7XRK3dMEBRBhUpxQ1ibo-zdhORxWnJtmNCajDe36aQmu-4Z4SFOss0oowgxUaachD66r8Ra2gwuBSqM/favicon.ico
- // @author 100の人
- // @homepageURL https://greasyfork.org/scripts/371331
- // ==/UserScript==
-
- /*global Gettext, _, h, GreasemonkeyUtils */
-
- 'use strict';
-
- // L10N
- Gettext.setLocalizedTexts({
- /*eslint-disable quote-props, max-len */
- 'en': {
- 'エラーが発生しました': 'Error occurred',
- },
- /*eslint-enable quote-props, max-len */
- });
-
- Gettext.setLocale(navigator.language);
-
-
-
- if (typeof content !== 'undefined') {
- // For Greasemonkey 4
- fetch = content.fetch.bind(content); //eslint-disable-line no-global-assign, no-native-reassign, no-undef
- }
-
-
-
- /**
- * ページ上部にエラー内容を表示します。
- * @param {Error} exception
- * @returns {void}
- */
- function showError(exception)
- {
- console.error(exception);
- try {
- const errorMessage = _('エラーが発生しました') + ': ' + exception
- + ('stack' in exception ? '\n\n' + exception.stack : '');
- const homeContent = document.getElementsByClassName('home-content')[0];
- if (homeContent) {
- homeContent.firstElementChild.firstElementChild.insertAdjacentHTML('afterbegin', h`<div class="row">
- <div class="alert alert-danger fade show" role="alert"
- style="white-space: pre-wrap; font-size: 1rem; font-weight: normal;">${errorMessage}</div>
- </div>`);
- } else {
- alert(errorMessage); //eslint-disable-line no-alert
- }
- } catch (e) {
- alert(_('エラーが発生しました') + ': ' + e); //eslint-disable-line no-alert
- }
- }
-
- const ID = 'vrchat-web-pages-extender-137';
-
- /**
- * 一度に取得できる最大の要素数。
- * @constant {number}
- */
- const MAX_ITEMS_COUNT = 100;
-
- /**
- * 一つのブックマークグループの最大登録数。
- * @constant {Object.<number>}
- */
- const MAX_FRIEND_FAVORITE_COUNT_PER_GROUP = 150;
-
- /**
- * @type {Function}
- * @access private
- */
- let setUserDetails;
-
- /**
- * @type {Promise.<Object>}
- * @access private
- */
- let userDetails = new Promise(function (resolve) {
- let settled = false;
- setUserDetails = function (details) {
- if (settled) {
- userDetails = Promise.resolve(details);
- } else {
- settled = true;
- resolve(details);
- }
- };
- });
-
- /**
- * キーにワールドIDを持つ連想配列。
- * @type {Object.<(string|string[]|boolean|number|Object.<(string|number)>[]|(string|number)[][]|null)>}
- */
- const worlds = { };
-
- /**
- * キーにグループIDを持つ連想配列。
- * @type {Object.<string,(string|string[]|number|boolean|Object.<string,(string|string[]|boolean)?>)?>[]}
- */
- const groups = { };
-
- addEventListener('message', function (event) {
- if (event.origin !== location.origin || typeof event.data !== 'object' || event.data === null
- || event.data.id !== ID) {
- return;
- }
-
- if (event.data.userDetails) {
- setUserDetails(event.data.userDetails);
- } else if (event.data.world) {
- worlds[event.data.world.id] = event.data.world;
- const locations = document.getElementsByClassName('locations')[0];
- if (!locations) {
- return;
- }
- for (const [ instanceId ] of event.data.world.instances) {
- const locationLink = locations.querySelector(`.locations
- [href*=${CSS.escape(`/home/launch?worldId=${event.data.world.id}&instanceId=${instanceId}`)}]`);
- if (!locationLink) {
- continue;
- }
- insertInstanceUserCountAndCapacity(locationLink.closest('.locations > *'), event.data.world.id, instanceId);
- }
- } else if (event.data.group) {
- groups[event.data.group.id] = event.data.group;
- }
- });
-
- /**
- * ログインしているユーザーの情報を取得します。
- * @see [User Info — VRChat API Documentation]{@link https://vrchatapi.github.io/#/UserAPI/CurrentUserDetails}
- * @returns {Promise.<?Object.<(string|string[]|boolean|number|Object)>>}
- */
- async function getUserDetails()
- {
- return await userDetails;
- }
-
- /**
- * JSONファイルをオブジェクトとして取得します。
- * @param {string} url
- * @returns {Promise.<(Object|Array)>} OKステータスでなければ失敗します。
- */
- async function fetchJSON(url)
- {
- const response = await fetch(url, {credentials: 'same-origin'});
- return response.ok
- ? response.json()
- : Promise.reject(new Error(`${response.status} ${response.statusText}\n${await response.text()}`));
- }
-
- let friendFavoriteGroupNameDisplayNamePairs;
-
- /**
- * ログインしているユーザーのフレンドfavoriteのグループ名を取得します。
- * @returns {Promise.<Object.<string>[]>}
- */
- function getFriendFavoriteGroupNameDisplayNamePairs()
- {
- if (!friendFavoriteGroupNameDisplayNamePairs) {
- friendFavoriteGroupNameDisplayNamePairs
- = fetchJSON('/api/1/favorite/groups?type=friend', {credentials: 'same-origin'}).then(function (groups) {
- const groupNameDisplayNamePairs = {};
- for (const group of groups) {
- groupNameDisplayNamePairs[group.name] = group.displayName;
- }
- return groupNameDisplayNamePairs;
- });
- }
- return friendFavoriteGroupNameDisplayNamePairs;
- }
-
- /**
- * @type {Promise.<Object.<(string|string[])>[]>}
- * @access private
- */
- let friendFavoritesPromise;
-
- /**
- * ブックマークを全件取得します。
- * @see [List Favorites — VRChat API Documentation]{@link https://vrchatapi.github.io/#/FavoritesAPI/ListAllFavorites}
- * @returns {Promise.<Object.<(string|string[])>[]>}
- */
- function getFriendFavorites()
- {
- return friendFavoritesPromise || (friendFavoritesPromise = async function () {
- const allFavorites = [];
- let offset = 0;
-
- while (true) { //eslint-disable-line no-constant-condition
- const favorites = await fetchJSON(
- `/api/1/favorites/?type=friend&n=${MAX_ITEMS_COUNT}&offset=${offset}`,
- ).catch(showError);
-
- allFavorites.push(...favorites);
-
- if (favorites.length < MAX_ITEMS_COUNT) {
- break;
- }
-
- offset += favorites.length;
- }
- return allFavorites;
- }());
- }
-
- /**
- * 自分のユーザーページの編集ダイアログのステータスメッセージ入力欄へ履歴を追加します。
- * @returns {Promise.<void>}
- */
- async function insertStatusMessageHistory()
- {
- if (document.getElementById('input-status-message-history')) {
- // すでに挿入済みなら
- return;
- }
-
- const inputStatusMessage = document.getElementById('input-status-message');
- if (!inputStatusMessage) {
- return;
- }
-
- // ステータスメッセージ入力欄へ履歴を追加
- inputStatusMessage.insertAdjacentHTML('afterend', `<datalist id="input-status-message-history">
- ${(await getUserDetails())
- .statusHistory.map(statusDescription => h`<option>${statusDescription}</option>`).join('')}
- </datalist>`);
- inputStatusMessage.setAttribute('list', inputStatusMessage.nextElementSibling.id);
- }
-
- /**
- * フレンドのブックマーク登録/解除ボタンの登録数表示を更新します。
- * @returns {Promise.<void>}
- */
- async function updateFriendFavoriteCounts()
- {
- const counts = {};
- for (const favorite of await getFriendFavorites()) {
- for (const tag of favorite.tags) {
- if (!(tag in counts)) {
- counts[tag] = 0;
- }
- counts[tag]++;
- }
- }
-
- for (const button of document.getElementsByName('favorite-friend')) {
- button.getElementsByClassName('count')[0].textContent = counts[button.value] || 0;
- }
- }
-
- /**
- * ユーザーページへブックマーク登録/解除ボタンを追加します。
- * @returns {Promise.<void>}
- */
- async function insertFriendFavoriteButtons()
- {
- const homeContent = document.getElementsByClassName('home-content')[0];
- const unfriendButton = homeContent.querySelector('[aria-label="Unfriend"]');
- if (!unfriendButton) {
- return;
- }
-
- const id = getUserIdFromLocation();
- if (!id) {
- return;
- }
-
- const buttons = document.getElementsByName('favorite-friend');
- const groupNameDisplayNamePairs = await getFriendFavoriteGroupNameDisplayNamePairs();
- const groupNames = Object.keys(groupNameDisplayNamePairs);
- const buttonsParent = buttons[0] && buttons[0].closest('[role="group"]');
- if (buttonsParent) {
- // 多重挿入の防止
- if (buttonsParent.dataset.id === id) {
- return;
- } else {
- buttonsParent.remove();
- }
- }
- unfriendButton.parentElement.parentElement.parentElement.parentElement.nextElementSibling.firstElementChild
- .insertAdjacentHTML('beforeend', `<div role="group" class="mx-2 btn-group-lg btn-group-vertical"
- style="margin-top: -60px;"
- data-id="${h(id)}">
- ${groupNames.sort().map(tag => h`<button type="button"
- class="btn btn-secondary" name="favorite-friend" value="${tag}" disabled="">
- <span aria-hidden="true" class="fa fa-star"></span>
-  <span class="name">${groupNameDisplayNamePairs[tag]}</span>
-  <span class="count">‒</span>⁄${MAX_FRIEND_FAVORITE_COUNT_PER_GROUP}
- </button>`).join('')}
- </div>`);
-
- await updateFriendFavoriteCounts();
-
- const tags = [].concat(
- ...(await getFriendFavorites()).filter(favorite => favorite.favoriteId === id).map(favorite => favorite.tags),
- );
-
- for (const button of buttons) {
- button.dataset.id = id;
- if (tags.includes(button.value)) {
- button.classList.remove('btn-secondary');
- button.classList.add('btn-primary');
- }
- if (button.classList.contains('btn-primary')
- || button.getElementsByClassName('count')[0].textContent < MAX_FRIEND_FAVORITE_COUNT_PER_GROUP) {
- button.disabled = false;
- }
- }
-
- buttons[0].closest('[role="group"]').addEventListener('click', async function (event) {
- const button = event.target.closest('button');
- if (!button || button.name !== 'favorite-friend') {
- return;
- }
-
- const buttons = document.getElementsByName('favorite-friend');
- for (const button of buttons) {
- button.disabled = true;
- }
-
- const id = button.dataset.id;
- const newTags = button.classList.contains('btn-secondary') ? [button.value] : [];
-
- const favorites = await getFriendFavorites();
- for (let i = favorites.length - 1; i >= 0; i--) {
- if (favorites[i].favoriteId === id) {
- await fetch(
- '/api/1/favorites/' + favorites[i].id,
- {method: 'DELETE', credentials: 'same-origin'},
- );
-
- for (const button of buttons) {
- if (favorites[i].tags.includes(button.value)) {
- button.classList.remove('btn-primary');
- button.classList.add('btn-secondary');
- }
- }
-
- favorites.splice(i, 1);
- }
- }
-
- if (newTags.length > 0) {
- await fetch('/api/1/favorites', {
- method: 'POST',
- headers: { 'content-type': 'application/json' },
- credentials: 'same-origin',
- body: JSON.stringify({type: 'friend', favoriteId: id, tags: newTags}),
- })
- .then(async response => response.ok ? response.json() : Promise.reject(
- new Error(`${response.status} ${response.statusText}\n${await response.text()}`),
- ))
- .then(function (favorite) {
- favorites.push(favorite);
- for (const button of buttons) {
- if (favorite.tags.includes(button.value)) {
- button.classList.remove('btn-secondary');
- button.classList.add('btn-primary');
- }
- }
- })
- .catch(showError);
- }
-
- await updateFriendFavoriteCounts();
-
- for (const button of buttons) {
- if (button.getElementsByClassName('count')[0].textContent < MAX_FRIEND_FAVORITE_COUNT_PER_GROUP) {
- button.disabled = false;
- }
- }
- });
- }
-
- /**
- * ログイン中のユーザーのグループ一覧。
- * @type {(string|boolean|number)[]?}
- */
- let authUserGroups;
-
- /**
- * 指定したユーザーが参加しているグループを取得します。
- * @param {*} userId
- * @returns {Promise.<(string|boolean|number)[]>}
- */
- function fetchUserGroups(userId)
- {
- return fetchJSON(`https://vrchat.com/api/1/users/${userId}/groups`);
- }
-
- /**
- * {@link location} からユーザーIDを抽出します。
- * @see {@link https://github.com/vrcx-team/VRCX/issues/429#issuecomment-1302920703}
- * @returns {string?}
- */
- function getUserIdFromLocation()
- {
- return /\/home\/user\/(usr_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|[0-9A-Za-z]{10})/
- .exec(location.pathname)?.[1];
- }
-
- /**
- * ユーザーページへブグループへのinviteボタンを追加します。
- * @returns {void}
- */
- function insertInvitingToGroupButton()
- {
- const userId = getUserIdFromLocation();
- if (!userId) {
- return;
- }
-
- const groupsHeading = Array.from(document.querySelectorAll('.home-content h2'))
- .find(heading => heading.lastChild?.data === '\'s Groups');
- if (!groupsHeading) {
- return;
- }
-
- if (document.getElementsByName('open-inviting-to-group')[0]) {
- return;
- }
-
- const displayName = document.querySelector('.home-content h2').textContent;
-
- /*eslint-disable max-len */
- groupsHeading.insertAdjacentHTML('beforeend', h`
- <button type="button" name="open-inviting-to-group" class="btn btn-primary">
- <svg aria-hidden="true" class="svg-inline--fa fa-envelope" role="presentation"
- xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
- <path fill="currentColor" d="M464 64C490.5 64 512 85.49 512 112C512 127.1 504.9 141.3 492.8 150.4L275.2 313.6C263.8 322.1 248.2 322.1 236.8 313.6L19.2 150.4C7.113 141.3 0 127.1 0 112C0 85.49 21.49 64 48 64H464zM217.6 339.2C240.4 356.3 271.6 356.3 294.4 339.2L512 176V384C512 419.3 483.3 448 448 448H64C28.65 448 0 419.3 0 384V176L217.6 339.2z"></path>
- </svg>
- Invite to Group
- </button>
- <div id="user-page-inviting-to-group-dialog" tabindex="-1" hidden=""
- style="font-size: 1rem; line-height: initial; position: relative; z-index: 1050; display: block;"><div>
- <div class="modal fade show" style="display: block;" role="dialog" tabindex="-1">
- <div class="modal-dialog" role="document"><div class="modal-content">
- <div class="modal-header">
- <h5 class="modal-title"><h4 class="m-0">Invite to Group</h4></h5>
- <div><button name="close-inviting-to-group-dialog" aria-label="Close Button"
- style="padding: 5px; border-radius: 4px; border: 2px solid #333333; background: #333333;
- color: white;">
- <svg role="presentation" aria-hidden="true" class="svg-inline--fa fa-xmark fa-fw"
- xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512" width="20">
- <path fill="currentColor" d="M310.6 361.4c12.5 12.5 12.5 32.75 0 45.25C304.4 412.9 296.2 416 288 416s-16.38-3.125-22.62-9.375L160 301.3L54.63 406.6C48.38 412.9 40.19 416 32 416S15.63 412.9 9.375 406.6c-12.5-12.5-12.5-32.75 0-45.25l105.4-105.4L9.375 150.6c-12.5-12.5-12.5-32.75 0-45.25s32.75-12.5 45.25 0L160 210.8l105.4-105.4c12.5-12.5 32.75-12.5 45.25 0s12.5 32.75 0 45.25l-105.4 105.4L310.6 361.4z"></path>
- </svg>
- </button></div>
- </div>
- <div class="modal-body"></div>
- </div></div>
- </div>
- <div class="modal-backdrop fade show"></div>
- </div></div>
- `);
- /*eslint-enable max-len */
-
- const dialog = document.getElementById('user-page-inviting-to-group-dialog');
-
- groupsHeading.addEventListener('click', async function (event) {
- const button = event.target.closest('button');
- if (!button) {
- return;
- }
-
- switch (button.name) {
- case 'open-inviting-to-group': {
- dialog.hidden = false;
-
- const modalBody = dialog.getElementsByClassName('modal-body')[0];
- if (modalBody.firstElementChild) {
- break;
- }
-
- if (!authUserGroups) {
- authUserGroups = await fetchUserGroups((await getUserDetails()).id);
- }
- const groupIds
- = Array.from(groupsHeading.nextElementSibling.querySelectorAll('[aria-label="Group Card"]'))
- .map(groupCard => /grp_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/
- .exec(groupCard.pathname)[0]);
-
- if (!document.getElementById('invite-to-group-style')) {
- document.head.insertAdjacentHTML('beforeend', `<style id="invite-to-group-style">
- [name="invite-to-group"] {
- --icon-size: 30px;
- --padding: 5px;
- padding: var(--padding) calc(var(--icon-size) + 2 * var(--padding));
- font-size: 1.2em;
- border: 2px solid #064B5C;
- border-radius: 4px;
- position: relative;
- color: #6AE3F9;
- background: #064B5C;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- width: 100%;
- }
-
- [name="invite-to-group"]:hover {
- border-color: #086C84;
- }
-
- [name="invite-to-group"]:disabled {
- border: 2px solid #333333;
- background: #333333;
- color: #999999;
- }
-
- [name="invite-to-group"] img {
- width: var(--icon-size);
- height: var(--icon-size);
- border-radius: 100%;
- border: 1px solid #181B1F;
- background-color: #181B1F;
- position: absolute;
- left: var(--padding);
- }
-
- [role="alert"] {
- display: flex;
- flex-direction: column;
- background-color: #541D22BF;
- margin-top: 10px;
- border-radius: 3px;
- padding: 10px;
- border-left: 3px solid red;
- }
-
- [role="alert"] > div:first-of-type {
- display: flex;
- align-items: center;
- }
-
- [role="alert"] > div:first-of-type > div:first-of-type {
- font-size: 1.2rem;
- font-weight: bold;
- }
- </style>`);
- }
- /*eslint-disable indent */
- modalBody.innerHTML = authUserGroups.map(group => h`<div
- class="mt-2 mb-2 d-flex flex-column justify-content-center">
- <div style="position: relative; border-radius: 4px;">
- <button name="invite-to-group" value="${h(group.groupId)}"` + (groupIds.includes(group.groupId)
- ? h` disabled="" title="${displayName} is already a member of this group․"`
- : '') + h`>
- <img src="${group.iconUrl}">
- ${group.name}
- </button>
- </div>
- </div>`).join('');
- /*eslint-enable indent */
- break;
- }
- case 'invite-to-group': {
- const enabledButtons = Array.from(dialog.querySelectorAll('button:enabled'));
- try {
- for (const button of enabledButtons) {
- button.disabled = true;
- }
-
- const response = await fetch(`/api/1/groups/${button.value}/invites`, {
- method: 'POST',
- headers: { 'content-type': 'application/json' },
- credentials: 'same-origin',
- body: JSON.stringify({ userId, confirmOverrideBlock: true }),
- });
- if (!response.ok) {
- const { error: { message } } = await response.json();
- /*eslint-disable max-len */
- button.parentElement.insertAdjacentHTML('beforebegin', h`<div role="alert"
- aria-label="Couldn't invite user">
- <div>
- <svg aria-hidden="true" class="svg-inline--fa fa-circle-exclamation me-2"
- role="presentation"
- xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" color="red">
- <path fill="currentColor" d="M256 0C114.6 0 0 114.6 0 256s114.6 256 256 256s256-114.6 256-256S397.4 0 256 0zM232 152C232 138.8 242.8 128 256 128s24 10.75 24 24v128c0 13.25-10.75 24-24 24S232 293.3 232 280V152zM256 400c-17.36 0-31.44-14.08-31.44-31.44c0-17.36 14.07-31.44 31.44-31.44s31.44 14.08 31.44 31.44C287.4 385.9 273.4 400 256 400z"></path>
- </svg>
- <div>Couldn't invite user</div>
- </div>
- <div>${response.statusText}: ${message}</div>
- </div>`);
- /*eslint-enable max-len */
- }
- enabledButtons.splice(enabledButtons.indexOf(button), 1);
- } finally {
- for (const button of enabledButtons) {
- button.disabled = false;
- }
- }
- break;
- }
- case 'close-inviting-to-group-dialog':
- dialog.hidden = true;
- break;
- }
- });
- }
-
- /**
- * Friend Locationsページのインスタンスへ、現在のインスタンス人数と上限を表示します。
- * @param {HTMLDivElement} location
- * @returns {void}
- */
- function insertInstanceUserCountAndCapacity(location, worldId, instanceId)
- {
- const world = worlds[worldId];
- const instanceUserCount = world?.instances?.find(([ id ]) => id === instanceId)?.[1];
-
- /** @type {HTMLElement} */
- let counts = location.getElementsByClassName('instance-user-count-and-capacity')[0];
- if (!counts) {
- const button = location.querySelector('[aria-label="Invite Me"]');
- const friendCount = location.querySelector('[aria-label="Invite Me"]').parentElement.previousElementSibling;
- counts = friendCount.cloneNode();
- counts.classList.add('instance-user-count-and-capacity');
- const reloadButton = button.cloneNode();
- reloadButton.setAttribute('aria-label', 'Reload');
- reloadButton.textContent = '↺';
- reloadButton.addEventListener('click', async function (event) {
- const instance
- = await fetchJSON(`/api/1/instances/${worldId}:${instanceId}`, { credentials: 'same-origin' });
- event.target.previousSibling.data = instance.userCount + ' / ' + instance.capacity;
- });
- counts.append('', reloadButton);
- friendCount.before(counts);
- }
- counts.firstChild.data = (instanceUserCount ?? '?') + ' / ' + (world?.capacity ?? '?');
- }
-
- /**
- * ページ読み込み後に一度だけ実行する処理をすでに行っていれば `true`。
- * @type {boolean}
- * @access private
- */
- let headChildrenInserted = false;
-
- const homeContents = document.getElementsByClassName('home-content');
-
- new MutationObserver(function (mutations, observer) {
- if (document.head && !headChildrenInserted) {
- headChildrenInserted = true;
- document.head.insertAdjacentHTML('beforeend', `<style>
- /*====================================
- Friend Locations
- */
- .instance-user-count-and-capacity {
- white-space: nowrap;
- }
-
- .instance-user-count-and-capacity button {
- margin: 0 0 0 0.5em !important;
- padding: unset;
- line-height: 1;
- }
-
- /*====================================
- フレンドのユーザーページ
- */
- .btn[name^="favorite-"] {
- white-space: unset;
- }
- </style>`);
-
- // ユーザー情報・ワールド情報・グループ情報を取得
- GreasemonkeyUtils.executeOnUnsafeContext(function (id) {
- Response.prototype.text = new Proxy(Response.prototype.text, {
- apply(get, thisArgument, argumentList)
- {
- const textPromise = Reflect.apply(get, thisArgument, argumentList);
- (async function () {
- const data = { id };
- const pathname = new URL(thisArgument.url).pathname;
- if (pathname === '/api/1/auth/user') {
- data.userDetails = JSON.parse(await textPromise);
- } else if (pathname.startsWith('/api/1/worlds/wrld_')) {
- data.world = JSON.parse(await textPromise);
- } else if (pathname.startsWith('/api/1/groups/grp_')) {
- data.group = JSON.parse(await textPromise);
- } else {
- return;
- }
- postMessage(data, location.origin);
- })();
- return textPromise;
- },
- });
- }, [ ID ]);
- }
-
- if (!homeContents[0]) {
- return;
- }
-
- const locationsList = homeContents[0].getElementsByClassName('locations');
- const instanceUserCountAndCapacityList = homeContents[0].getElementsByClassName('instance-user-count-and-capacity');
-
- new MutationObserver(async function (mutations) {
- for (const mutation of mutations) {
- if (locationsList[0]) {
- if (locationsList[0].children.length !== instanceUserCountAndCapacityList.length) {
- // Friend Locationsへインスタンス人数を追加
- for (const location of locationsList[0].children) {
- if (location.getElementsByClassName('instance-user-count-and-capacity')[0]) {
- continue;
- }
-
- const launchLink = location.querySelector('[href*="/home/launch?"]');
- if (!launchLink) {
- continue;
- }
- const params = new URLSearchParams(launchLink.search);
- insertInstanceUserCountAndCapacity(location, params.get('worldId'), params.get('instanceId'));
- }
- }
- } else if (mutation.addedNodes.length > 0 && mutation.target.nodeType === Node.ELEMENT_NODE
- && (/* ユーザーページを開いたとき */ mutation.target.classList.contains('home-content')
- || mutation.target.localName === 'div'
- && mutation.addedNodes.length === 1 && mutation.addedNodes[0].localName === 'div'
- && mutation.addedNodes[0]
- .querySelector('[aria-label="Add Friend"], [aria-label="Unfriend"]')
- || /* ワールドページを開いたとき */ mutation.target.parentElement.classList.contains('home-content')
- || /* グループページでタブ移動したとき */ mutation.target.parentElement.parentElement
- .classList.contains('home-content'))
- || /* ユーザーページ間を移動したとき */ mutation.type === 'characterData'
- && mutation.target.nextSibling?.data === '\'s Profile') {
- if (location.pathname.startsWith('/home/user/')) {
- // ユーザーページ
- await insertStatusMessageHistory();
- insertInvitingToGroupButton();
- await insertFriendFavoriteButtons('friend');
- } else if (location.pathname.startsWith('/home/world/')) {
- // ワールドページ
- const heading = document.querySelector('.home-content h2');
- const name = heading.firstChild.data;
- const author
- = heading.nextElementSibling.querySelector('[href^="/home/user/"]').firstChild.data;
- document.title = `${name} By ${author} - VRChat`;
- } else if (location.pathname.startsWith('/home/avatar/')) {
- // アバターページ
- const name = document.querySelector('.home-content h3').textContent;
- const author = document.querySelector('.home-content [href^="/home/user/"]').text;
- document.title = `${name} By ${author} - VRChat`;
- } else if (location.pathname.startsWith('/home/group/')) {
- // グループページ
- const name = document.querySelector('.home-content h2').textContent;
- const groupLink = document.querySelector('[href^="https://vrc.group/"]');
- const shortCodeAndDiscriminator = groupLink.textContent;
- document.title = `${name} ⁂ ${shortCodeAndDiscriminator} - VRChat`;
-
- // グループオーナーへのリンクを追加
- setTimeout(function () {
- if (!document.getElementById('group-owner-link')) {
- const groupLinkColumn = groupLink.closest('div');
- groupLinkColumn.style.marginLeft = '1em';
- const column = groupLinkColumn.cloneNode();
- const ownerId = groups[/^\/home\/group\/([^/]+)/.exec(location.pathname)[1]].ownerId;
- column.innerHTML = h`<a id="group-owner-link" href="/home/user/${ownerId}">
- Group Owner
- </a>`;
- groupLinkColumn.after(column);
- }
- });
- }
- break;
- }
- }
- }).observe(homeContents[0], {childList: true, characterData: true, subtree: true });
-
- observer.disconnect();
- }).observe(document, {childList: true, subtree: true});