This commit is contained in:
Natsumi
2025-10-17 16:57:09 +11:00
parent 9e95e1734c
commit dc51d156e4
13 changed files with 386 additions and 330 deletions

View File

@@ -63,6 +63,8 @@
<VRChatConfigDialog></VRChatConfigDialog> <VRChatConfigDialog></VRChatConfigDialog>
<PrimaryPasswordDialog></PrimaryPasswordDialog> <PrimaryPasswordDialog></PrimaryPasswordDialog>
<SendBoopDialog></SendBoopDialog>
</template> </template>
</div> </div>
</el-config-provider> </el-config-provider>
@@ -109,6 +111,7 @@
import NavMenu from './components/NavMenu.vue'; import NavMenu from './components/NavMenu.vue';
import PreviousInstancesInfoDialog from './components/dialogs/PreviousInstancesDialog/PreviousInstancesInfoDialog.vue'; import PreviousInstancesInfoDialog from './components/dialogs/PreviousInstancesDialog/PreviousInstancesInfoDialog.vue';
import PrimaryPasswordDialog from './views/Settings/dialogs/PrimaryPasswordDialog.vue'; import PrimaryPasswordDialog from './views/Settings/dialogs/PrimaryPasswordDialog.vue';
import SendBoopDialog from './components/dialogs/SendBoopDialog.vue';
import Sidebar from './views/Sidebar/Sidebar.vue'; import Sidebar from './views/Sidebar/Sidebar.vue';
import UserDialog from './components/dialogs/UserDialog/UserDialog.vue'; import UserDialog from './components/dialogs/UserDialog/UserDialog.vue';
import VRCXUpdateDialog from './components/dialogs/VRCXUpdateDialog.vue'; import VRCXUpdateDialog from './components/dialogs/VRCXUpdateDialog.vue';

View File

@@ -194,28 +194,29 @@ const miscReq = {
}; };
return args; return args;
}); });
} },
// /** /**
// * @params {{ * @params {{
// userId: string, userId: string,
// emojiId: string emojiId: string
// }} params }} params
// * @returns {Promise<{json: any, params}>} * @returns {Promise<{json: any, params}>}
// */ */
// sendBoop(params) { sendBoop(params) {
// return request(`users/${params.userId}/boop`, { return request(`users/${params.userId}/boop`, {
// method: 'POST', method: 'POST',
// params params: {
// }).then((json) => { emojiId: params.emojiId
// const args = { }
// json, }).then((json) => {
// params const args = {
// }; json,
// this.$emit('BOOP:SEND', args); params
// return args; };
// }); return args;
// } });
}
}; };
export default miscReq; export default miscReq;

View File

@@ -611,6 +611,7 @@
} from '../../../shared/utils'; } from '../../../shared/utils';
import { useAvatarStore, useFavoriteStore, useGalleryStore, useGameStore, useUserStore } from '../../../stores'; import { useAvatarStore, useFavoriteStore, useGalleryStore, useGameStore, useUserStore } from '../../../stores';
import { avatarModerationRequest, avatarRequest, favoriteRequest, miscRequest } from '../../../api'; import { avatarModerationRequest, avatarRequest, favoriteRequest, miscRequest } from '../../../api';
import { AppDebug } from '../../../service/appConfig.js';
import { database } from '../../../service/database'; import { database } from '../../../service/database';
import { getNextDialogIndex } from '../../../shared/utils/base/ui'; import { getNextDialogIndex } from '../../../shared/utils/base/ui';
import { handleImageUploadInput } from '../../../shared/utils/imageUpload'; import { handleImageUploadInput } from '../../../shared/utils/imageUpload';
@@ -727,7 +728,7 @@
} }
function getImageUrlFromImageId(imageId) { function getImageUrlFromImageId(imageId) {
return `https://api.vrchat.cloud/api/1/file/${imageId}/1/`; return `${AppDebug.endpointDomain}/file/${imageId}/1/`;
} }
function handleDialogOpen() { function handleDialogOpen() {

View File

@@ -1,277 +1,250 @@
<!--<template>--> <template>
<!-- <el-dialog--> <el-dialog
<!-- class="x-dialog"--> class="x-dialog"
<!-- :model-value="sendBoopDialog.visible"--> v-model="sendBoopDialog.visible"
<!-- :title="t('dialog.boop_dialog.header')"--> :title="t('dialog.boop_dialog.header')"
<!-- width="450px"--> width="450px"
<!-- @close="closeDialog">--> @close="closeDialog">
<!-- <el-select--> <el-select
<!-- v-model="sendBoopDialog.userId"--> v-if="sendBoopDialog.visible"
<!-- :placeholder="t('dialog.new_instance.instance_creator_placeholder')"--> v-model="sendBoopDialog.userId"
<!-- filterable--> :placeholder="t('dialog.new_instance.instance_creator_placeholder')"
<!-- style="width: 100%">--> filterable
<!-- <el-option-group v-if="vipFriends.length" :label="t('side_panel.favorite')">--> style="width: 100%">
<!-- <el-option--> <el-option-group v-if="vipFriends.length" :label="t('side_panel.favorite')">
<!-- v-for="friend in vipFriends"--> <el-option
<!-- :key="friend.id"--> v-for="friend in vipFriends"
<!-- class="x-friend-item"--> :key="friend.id"
<!-- :label="friend.name"--> :label="friend.name"
<!-- :value="friend.id"--> :value="friend.id"
<!-- style="height: auto">--> style="height: auto"
<!-- <template v-if="friend.ref">--> class="x-friend-item">
<!-- <div class="avatar" :class="userStatusClass(friend.ref)">--> <template v-if="friend.ref">
<!-- <img :src="userImage(friend.ref)" loading="lazy">--> <div class="avatar" :class="userStatusClass(friend.ref)">
<!-- </div>--> <img :src="userImage(friend.ref)" loading="lazy" />
<!-- <div class="detail">--> </div>
<!-- <span--> <div class="detail">
<!-- class="name"--> <span
<!-- :style="{ color: friend.ref.$userColour }"--> class="name"
<!-- v-text="friend.ref.displayName"></span>--> :style="{ color: friend.ref.$userColour }"
<!-- </div>--> v-text="friend.ref.displayName"></span>
<!-- </template>--> </div>
<!-- <span v-else v-text="friend.id"></span>--> </template>
<!-- </el-option>--> <span v-else v-text="friend.id"></span>
<!-- </el-option-group>--> </el-option>
<!-- <el-option-group v-if="onlineFriends.length" :label="t('side_panel.online')">--> </el-option-group>
<!-- <el-option--> <el-option-group v-if="onlineFriends.length" :label="t('side_panel.online')">
<!-- v-for="friend in onlineFriends"--> <el-option
<!-- :key="friend.id"--> v-for="friend in onlineFriends"
<!-- class="x-friend-item"--> :key="friend.id"
<!-- :label="friend.name"--> :label="friend.name"
<!-- :value="friend.id"--> :value="friend.id"
<!-- style="height: auto">--> style="height: auto"
<!-- <template v-if="friend.ref">--> class="x-friend-item">
<!-- <div class="avatar" :class="userStatusClass(friend.ref)">--> <template v-if="friend.ref">
<!-- <img :src="userImage(friend.ref)" loading="lazy">--> <div class="avatar" :class="userStatusClass(friend.ref)">
<!-- </div>--> <img :src="userImage(friend.ref)" loading="lazy" />
<!-- <div class="detail">--> </div>
<!-- <span--> <div class="detail">
<!-- class="name"--> <span
<!-- :style="{ color: friend.ref.$userColour }"--> class="name"
<!-- v-text="friend.ref.displayName"></span>--> :style="{ color: friend.ref.$userColour }"
<!-- </div>--> v-text="friend.ref.displayName"></span>
<!-- </template>--> </div>
<!-- <span v-else v-text="friend.id"></span>--> </template>
<!-- </el-option>--> <span v-else v-text="friend.id"></span>
<!-- </el-option-group>--> </el-option>
<!-- <el-option-group v-if="activeFriends.length" :label="t('side_panel.active')">--> </el-option-group>
<!-- <el-option--> <el-option-group v-if="activeFriends.length" :label="t('side_panel.active')">
<!-- v-for="friend in activeFriends"--> <el-option
<!-- :key="friend.id"--> v-for="friend in activeFriends"
<!-- class="x-friend-item"--> :key="friend.id"
<!-- :label="friend.name"--> :label="friend.name"
<!-- :value="friend.id"--> :value="friend.id"
<!-- style="height: auto">--> style="height: auto"
<!-- <template v-if="friend.ref">--> class="x-friend-item">
<!-- <div class="avatar">--> <template v-if="friend.ref">
<!-- <img :src="userImage(friend.ref)" loading="lazy">--> <div class="avatar">
<!-- </div>--> <img :src="userImage(friend.ref)" loading="lazy" />
<!-- <div class="detail">--> </div>
<!-- <span--> <div class="detail">
<!-- class="name"--> <span
<!-- :style="{ color: friend.ref.$userColour }"--> class="name"
<!-- v-text="friend.ref.displayName"></span>--> :style="{ color: friend.ref.$userColour }"
<!-- </div>--> v-text="friend.ref.displayName"></span>
<!-- </template>--> </div>
<!-- <span v-else v-text="friend.id"></span>--> </template>
<!-- </el-option>--> <span v-else v-text="friend.id"></span>
<!-- </el-option-group>--> </el-option>
<!-- <el-option-group v-if="offlineFriends.length" :label="t('side_panel.offline')">--> </el-option-group>
<!-- <el-option--> <el-option-group v-if="offlineFriends.length" :label="t('side_panel.offline')">
<!-- v-for="friend in offlineFriends"--> <el-option
<!-- :key="friend.id"--> v-for="friend in offlineFriends"
<!-- class="x-friend-item"--> :key="friend.id"
<!-- :label="friend.name"--> :label="friend.name"
<!-- :value="friend.id"--> :value="friend.id"
<!-- style="height: auto">--> style="height: auto"
<!-- <template v-if="friend.ref">--> class="x-friend-item">
<!-- <div class="avatar">--> <template v-if="friend.ref">
<!-- <img :src="userImage(friend.ref)" loading="lazy">--> <div class="avatar">
<!-- </div>--> <img :src="userImage(friend.ref)" loading="lazy" />
<!-- <div class="detail">--> </div>
<!-- <span--> <div class="detail">
<!-- class="name"--> <span
<!-- :style="{ color: friend.ref.$userColour }"--> class="name"
<!-- v-text="friend.ref.displayName"></span>--> :style="{ color: friend.ref.$userColour }"
<!-- </div>--> v-text="friend.ref.displayName"></span>
<!-- </template>--> </div>
<!-- <span v-else v-text="friend.id"></span>--> </template>
<!-- </el-option>--> <span v-else v-text="friend.id"></span>
<!-- </el-option-group>--> </el-option>
<!-- </el-select>--> </el-option-group>
</el-select>
<!-- <br />--> <br />
<!-- <br />--> <br />
<!-- <el-select--> <el-select
<!-- v-model="fileId"--> v-if="sendBoopDialog.visible"
<!-- clearable--> v-model="fileId"
<!-- :placeholder="t('dialog.boop_dialog.select_emoji')"--> clearable
<!-- size="small"--> :placeholder="t('dialog.boop_dialog.select_emoji')"
<!-- style="width: 100%"--> size="small"
<!-- popper-class="max-height-el-select">--> style="width: 100%"
<!-- <el-option-group :label="t('dialog.boop_dialog.my_emojis')">--> popper-class="max-height-el-select">
<!-- <el-option--> <el-option-group :label="t('dialog.boop_dialog.my_emojis')">
<!-- v-for="image in emojiTable"--> <el-option
<!-- v-if="image.versions && image.versions.length > 0"--> v-for="image in emojiTable"
<!-- :key="image.id"--> :key="image.id"
<!-- :value="image.id"--> :value="image.id"
<!-- style="width: 100%; height: 100%">--> style="width: 100%; height: 100%">
<!-- <div--> <div
<!-- v-if="image.versions[image.versions.length - 1].file.url"--> v-if="
<!-- class="vrcplus-icon"--> image.versions &&
<!-- style="overflow: hidden; width: 200px; height: 200px; padding: 10px">--> image.versions.length > 0 &&
<!-- <template v-if="image.frames">--> image.versions[image.versions.length - 1].file.url
<!-- <div--> "
<!-- class="avatar"--> class="vrcplus-icon"
<!-- :style="--> style="overflow: hidden; width: 200px; height: 200px; padding: 10px">
<!-- generateEmojiStyle(--> <template v-if="image.frames">
<!-- image.versions[image.versions.length - 1].file.url,--> <div
<!-- image.framesOverTime,--> class="avatar"
<!-- image.frames,--> :style="
<!-- image.loopStyle--> generateEmojiStyle(
<!-- )--> image.versions[image.versions.length - 1].file.url,
<!-- "></div>--> image.framesOverTime,
<!-- </template>--> image.frames,
<!-- <template v-else>--> image.loopStyle
<!-- <img--> )
<!-- :src="image.versions[image.versions.length - 1].file.url"--> "></div>
<!-- class="avatar"--> </template>
<!-- style="width: 200px; height: 200px" />--> <template v-else>
<!-- </template>--> <img
<!-- </div>--> :src="image.versions[image.versions.length - 1].file.url"
<!-- </el-option>--> class="avatar"
<!-- </el-option-group>--> style="width: 200px; height: 200px" />
<!-- <el-option-group :label="t('dialog.boop_dialog.default_emojis')">--> </template>
<!-- <el-option--> </div>
<!-- v-for="emojiName in photonEmojis"--> </el-option>
<!-- :key="emojiName"--> </el-option-group>
<!-- :value="getEmojiValue(emojiName)"--> <el-option-group :label="t('dialog.boop_dialog.default_emojis')">
<!-- style="width: 100%; height: 100%">--> <el-option
<!-- <span v-text="emojiName"></span>--> v-for="emojiName in photonEmojis"
<!-- </el-option>--> :key="emojiName"
<!-- </el-option-group>--> :value="getEmojiValue(emojiName)"
<!-- </el-select>--> style="width: 100%; height: 100%">
<span v-text="emojiName"></span>
</el-option>
</el-option-group>
</el-select>
<!-- <template #footer>--> <template #footer>
<!-- <el-button size="small" @click="showGalleryDialog(2)">{{--> <el-button
<!-- t('dialog.boop_dialog.emoji_manager')--> size="small"
<!-- }}</el-button>--> @click="
<!-- <el-button size="small" @click="closeDialog">{{ t('dialog.boop_dialog.cancel') }}</el-button>--> redirectToToolsTab();
<!-- <el-button size="small" :disabled="!sendBoopDialog.userId" @click="sendBoop">{{--> showGalleryDialog();
<!-- t('dialog.boop_dialog.send')--> "
<!-- }}</el-button>--> >{{ t('dialog.boop_dialog.emoji_manager') }}</el-button
<!-- </template>--> >
<!-- </el-dialog>--> <el-button size="small" @click="closeDialog">{{ t('dialog.boop_dialog.cancel') }}</el-button>
<!--</template>--> <el-button size="small" :disabled="!sendBoopDialog.userId" @click="sendBoop">{{
t('dialog.boop_dialog.send')
}}</el-button>
</template>
</el-dialog>
</template>
<!--<script setup>--> <script setup>
<!-- import { inject, ref } from 'vue';--> import { ref, watch } from 'vue';
<!-- import { useI18n } from 'vue-i18n-bridge';--> import { storeToRefs } from 'pinia';
<!-- import { photonEmojis } from '../../composables/shared/constants/photon.js';--> import { useI18n } from 'vue-i18n';
<!-- import { notificationRequest } from '../../api';-->
<!-- // import { miscRequest } from '../../api';-->
<!-- const { t } = useI18n();--> import { generateEmojiStyle, userImage, userStatusClass } from '../../shared/utils';
import { miscRequest } from '../../api';
import { notificationRequest } from '../../api';
import { photonEmojis } from '../../shared/constants/photon.js';
import { redirectToToolsTab } from '../../shared/utils/base/ui';
import { useFriendStore } from '../../stores';
import { useGalleryStore } from '../../stores';
import { useNotificationStore } from '../../stores';
import { useUserStore } from '../../stores/user.js';
<!-- const userStatusClass = inject('userStatusClass');--> const { t } = useI18n();
<!-- const userImage = inject('userImage');-->
<!-- const showGalleryDialog = inject('showGalleryDialog');-->
<!-- const props = defineProps({--> const { sendBoopDialog } = storeToRefs(useUserStore());
<!-- sendBoopDialog: {--> const { notificationTable } = storeToRefs(useNotificationStore());
<!-- type: Object,--> const { showGalleryDialog, refreshEmojiTable } = useGalleryStore();
<!-- required: true--> const { emojiTable } = storeToRefs(useGalleryStore());
<!-- },--> const { vipFriends, onlineFriends, activeFriends, offlineFriends } = useFriendStore();
<!-- emojiTable: {-->
<!-- type: Array,-->
<!-- required: true-->
<!-- },-->
<!-- vipFriends: {-->
<!-- type: Array,-->
<!-- required: true-->
<!-- },-->
<!-- onlineFriends: {-->
<!-- type: Array,-->
<!-- required: true-->
<!-- },-->
<!-- activeFriends: {-->
<!-- type: Array,-->
<!-- required: true-->
<!-- },-->
<!-- offlineFriends: {-->
<!-- type: Array,-->
<!-- required: true-->
<!-- },-->
<!-- generateEmojiStyle: {-->
<!-- type: Function,-->
<!-- required: true-->
<!-- },-->
<!-- notificationTable: {-->
<!-- type: Object,-->
<!-- required: true-->
<!-- }-->
<!-- });-->
<!-- const emit = defineEmits(['update:sendBoopDialog']);--> const fileId = ref('');
<!-- const fileId = ref('');--> watch(
() => sendBoopDialog.value.visible,
(visible) => {
if (visible && emojiTable.value.length === 0) {
refreshEmojiTable();
}
}
);
<!-- // $app.data.sendBoopDialog = {--> function closeDialog() {
<!-- // visible: false,--> sendBoopDialog.value.visible = false;
<!-- // userId: ''--> }
<!-- // };--> function getEmojiValue(emojiName) {
<!-- // $app.methods.showSendBoopDialog = function (userId) {--> if (!emojiName) {
<!-- // this.$nextTick(() =>--> return '';
<!-- // $app.adjustDialogZ(this.$refs.sendBoopDialog.$el)--> }
<!-- // );--> return `default_${emojiName.replace(/ /g, '_').toLowerCase()}`;
<!-- // const D = this.sendBoopDialog;--> }
<!-- // D.userId = userId;-->
<!-- // D.visible = true;-->
<!-- // if (this.emojiTable.length === 0 && API.currentUser.$isVRCPlus) {-->
<!-- // this.refreshEmojiTable();-->
<!-- // }-->
<!-- // };-->
<!-- function closeDialog() {--> function sendBoop() {
<!-- emit('update:sendBoopDialog', {--> const D = sendBoopDialog.value;
<!-- ...props.sendBoopDialog,--> dismissBoop(D.userId);
<!-- visible: false--> const params = {
<!-- });--> userId: D.userId
<!-- }--> };
<!-- function getEmojiValue(emojiName) {--> if (fileId.value) {
<!-- if (!emojiName) {--> params.emojiId = fileId.value;
<!-- return '';--> }
<!-- }--> miscRequest.sendBoop(params);
<!-- return `vrchat_${emojiName.replace(/ /g, '_').toLowerCase()}`;--> D.visible = false;
<!-- }--> }
<!-- function sendBoop() {--> function dismissBoop(userId) {
<!-- const D = props.sendBoopDialog;--> // JANK: This is a hack to remove boop notifications when responding
<!-- dismissBoop(D.userId);--> const array = notificationTable.value.data;
<!-- const params = {--> for (let i = array.length - 1; i >= 0; i--) {
<!-- userId: D.userId--> const ref = array[i];
<!-- };--> if (ref.type !== 'boop' || ref.$isExpired || ref.senderUserId !== userId) {
<!-- if (fileId.value) {--> continue;
<!-- params.emojiId = fileId.value;--> }
<!-- }--> notificationRequest.sendNotificationResponse({
<!-- // miscRequest.sendBoop(params);--> notificationId: ref.id,
<!-- D.visible = false;--> responseType: 'delete',
<!-- }--> responseData: ''
});
<!-- function dismissBoop(userId) {--> }
<!-- // JANK: This is a hack to remove boop notifications when responding--> }
<!-- const array = props.notificationTable.data;--> </script>
<!-- for (let i = array.length - 1; i >= 0; i&#45;&#45;) {-->
<!-- const ref = array[i];-->
<!-- if (ref.type !== 'boop' || ref.$isExpired || ref.senderUserId !== userId) {-->
<!-- continue;-->
<!-- }-->
<!-- notificationRequest.sendNotificationResponse({-->
<!-- notificationId: ref.id,-->
<!-- responseType: 'delete',-->
<!-- responseData: ''-->
<!-- });-->
<!-- }-->
<!-- }-->
<!--</script>-->

View File

@@ -108,6 +108,12 @@
<el-dropdown-item v-else :icon="Plus" command="Send Friend Request">{{ <el-dropdown-item v-else :icon="Plus" command="Send Friend Request">{{
t('dialog.user.actions.send_friend_request') t('dialog.user.actions.send_friend_request')
}}</el-dropdown-item> }}</el-dropdown-item>
<el-dropdown-item
:disabled="!currentUser.isBoopingEnabled"
:icon="Pointer"
command="Send Boop"
>{{ t('dialog.user.actions.send_boop') }}</el-dropdown-item
>
<el-dropdown-item :icon="Message" command="Invite To Group">{{ <el-dropdown-item :icon="Message" command="Invite To Group">{{
t('dialog.user.actions.invite_to_group') t('dialog.user.actions.invite_to_group')
}}</el-dropdown-item> }}</el-dropdown-item>

View File

@@ -423,11 +423,17 @@
}}</span> }}</span>
</div> </div>
</div> </div>
<!--//- .x-friend-item(@click="toggleAllowBooping")--> <div class="x-friend-item" @click="toggleAllowBooping">
<!--//- .detail--> <div class="detail">
<!--//- span.name {{ t('dialog.user.info.booping') }}--> <span class="name">{{ t('dialog.user.info.booping') }}</span>
<!--//- span.extra(v-if="currentUser.isBoopingEnabled" style="color:#67C23A") {{ t('dialog.user.info.avatar_cloning_allow') }}--> <span v-if="currentUser.isBoopingEnabled" class="extra" style="color: #67c23a">{{
<!--//- span.extra(v-else style="color:#F56C6C") {{ t('dialog.user.info.avatar_cloning_deny') }}--> t('dialog.user.info.avatar_cloning_allow')
}}</span>
<span v-else class="extra" style="color: #f56c6c">{{
t('dialog.user.info.avatar_cloning_deny')
}}</span>
</div>
</div>
</template> </template>
<template v-else> <template v-else>
<div class="x-friend-item" style="cursor: default"> <div class="x-friend-item" style="cursor: default">
@@ -1302,8 +1308,14 @@
const { hideUserNotes, hideUserMemos } = storeToRefs(useAppearanceSettingsStore()); const { hideUserNotes, hideUserMemos } = storeToRefs(useAppearanceSettingsStore());
const { avatarRemoteDatabase } = storeToRefs(useAdvancedSettingsStore()); const { avatarRemoteDatabase } = storeToRefs(useAdvancedSettingsStore());
const { userDialog, languageDialog, currentUser, isLocalUserVrcPlusSupporter } = storeToRefs(useUserStore()); const { userDialog, languageDialog, currentUser, isLocalUserVrcPlusSupporter } = storeToRefs(useUserStore());
const { cachedUsers, showUserDialog, sortUserDialogAvatars, refreshUserDialogAvatars, refreshUserDialogTreeData } = const {
useUserStore(); cachedUsers,
showUserDialog,
sortUserDialogAvatars,
refreshUserDialogAvatars,
refreshUserDialogTreeData,
showSendBoopDialog
} = useUserStore();
const { favoriteLimits } = storeToRefs(useFavoriteStore()); const { favoriteLimits } = storeToRefs(useFavoriteStore());
const { showFavoriteDialog, handleFavoriteWorldList } = useFavoriteStore(); const { showFavoriteDialog, handleFavoriteWorldList } = useFavoriteStore();
const { showAvatarDialog, lookupAvatars, showAvatarAuthorDialog } = useAvatarStore(); const { showAvatarDialog, lookupAvatars, showAvatarAuthorDialog } = useAvatarStore();
@@ -1749,8 +1761,8 @@
redirectToToolsTab(); redirectToToolsTab();
} else if (command === 'Invite To Group') { } else if (command === 'Invite To Group') {
showInviteGroupDialog('', D.id); showInviteGroupDialog('', D.id);
// } else if (command === 'Send Boop') { } else if (command === 'Send Boop') {
// this.showSendBoopDialog(D.id); showSendBoopDialog(D.id);
} else if (command === 'Group Moderation') { } else if (command === 'Group Moderation') {
showModerateGroupDialog(D.id); showModerateGroupDialog(D.id);
} else if (command === 'Hide Avatar') { } else if (command === 'Hide Avatar') {
@@ -2273,6 +2285,12 @@
}); });
} }
function toggleAllowBooping() {
userRequest.saveCurrentUser({
isBoopingEnabled: !currentUser.value.isBoopingEnabled
});
}
function resetHome() { function resetHome() {
ElMessageBox.confirm('Continue? Reset Home', 'Confirm', { ElMessageBox.confirm('Continue? Reset Home', 'Confirm', {
confirmButtonText: 'Confirm', confirmButtonText: 'Confirm',

View File

@@ -9,6 +9,7 @@ import {
useSearchStore, useSearchStore,
useWorldStore useWorldStore
} from '../../stores'; } from '../../stores';
import { AppDebug } from '../../service/appConfig.js';
import { compareUnityVersion } from './avatar'; import { compareUnityVersion } from './avatar';
import { escapeTag } from './base/string'; import { escapeTag } from './base/string';
import { miscRequest } from '../../api'; import { miscRequest } from '../../api';
@@ -213,7 +214,7 @@ function convertFileUrlToImageUrl(url, resolution = 128) {
if (match) { if (match) {
const fileId = match[1]; const fileId = match[1];
const version = match[2]; const version = match[2];
return `https://api.vrchat.cloud/api/1/image/file_${fileId}/${version}/${resolution}`; return `${AppDebug.endpointDomain}/image/file_${fileId}/${version}/${resolution}`;
} }
// no match return origin url // no match return origin url
return url; return url;

View File

@@ -54,4 +54,34 @@ function getEmojiFileName(emoji) {
} }
} }
export { getPrintLocalDate, getPrintFileName, getEmojiFileName }; /**
* @param {string} url
* @param {number} fps
* @param {number} frameCount
* @param {string} loopStyle
*/
function generateEmojiStyle(url, fps, frameCount, loopStyle) {
let framesPerLine = 2;
if (frameCount > 4) framesPerLine = 4;
if (frameCount > 16) framesPerLine = 8;
const animationDurationMs = (1000 / fps) * frameCount;
const frameSize = 1024 / framesPerLine;
const scale = 100 / (frameSize / 200);
const animStyle = loopStyle === 'pingpong' ? 'alternate' : 'none';
const style = `
transform: scale(${scale / 100});
transform-origin: top left;
width: ${frameSize}px;
height: ${frameSize}px;
background: url('${url}') 0 0;
animation: ${animationDurationMs}ms steps(1) 0s infinite ${animStyle} running animated-emoji-${frameCount};
`;
return style;
}
export {
getPrintLocalDate,
getPrintFileName,
getEmojiFileName,
generateEmojiStyle
};

View File

@@ -413,6 +413,15 @@ export const useNotificationStore = defineStore('Notification', () => {
} }
ref.details = details; ref.details = details;
} }
if (ref.type === 'boop') {
ref.message = ref.title;
if (ref.details?.emojiId.startsWith('default_')) {
ref.details.imageUrl = ref.details.emojiId;
ref.message += ` ${ref.details.emojiId.replace('default_', '')}`;
} else {
ref.details.imageUrl = `${AppDebug.endpointDomain}/file/${ref.details.emojiId}/${ref.details.emojiVersion}`;
}
}
return ref; return ref;
} }

View File

@@ -1115,7 +1115,7 @@ export const usePhotonStore = defineStore('Photon', () => {
} else if (type === 1) { } else if (type === 1) {
emojiName = 'Custom'; emojiName = 'Custom';
var fileId = data.Parameters[245][1]; var fileId = data.Parameters[245][1];
imageUrl = `https://api.vrchat.cloud/api/1/file/${fileId}/1/`; imageUrl = `${AppDebug.endpointDomain}/file/${fileId}/1/`;
} }
addEntryPhotonEvent({ addEntryPhotonEvent({
photonId, photonId,

View File

@@ -269,6 +269,10 @@ export const useUserStore = defineStore('User', () => {
languageChoice: false, languageChoice: false,
languages: [] languages: []
}); });
const sendBoopDialog = ref({
visible: false,
userId: ''
});
const pastDisplayNameTable = ref({ const pastDisplayNameTable = ref({
data: [], data: [],
tableProps: { tableProps: {
@@ -1960,6 +1964,11 @@ export const useUserStore = defineStore('User', () => {
return ref; return ref;
} }
function showSendBoopDialog(userId) {
sendBoopDialog.value.userId = userId;
sendBoopDialog.value.visible = true;
}
return { return {
state, state,
@@ -1968,6 +1977,7 @@ export const useUserStore = defineStore('User', () => {
userDialog, userDialog,
subsetOfLanguages, subsetOfLanguages,
languageDialog, languageDialog,
sendBoopDialog,
pastDisplayNameTable, pastDisplayNameTable,
showUserDialogHistory, showUserDialogHistory,
customUserTags, customUserTags,
@@ -1986,7 +1996,7 @@ export const useUserStore = defineStore('User', () => {
initUserNotes, initUserNotes,
getCurrentUser, getCurrentUser,
handleConfig, handleConfig,
showSendBoopDialog,
checkNote checkNote
}; };
}); });

View File

@@ -125,7 +125,16 @@
<el-table-column :label="t('table.notification.photo')" width="100" prop="photo"> <el-table-column :label="t('table.notification.photo')" width="100" prop="photo">
<template #default="scope"> <template #default="scope">
<template v-if="scope.row.details && scope.row.details.imageUrl"> <template v-if="scope.row.type === 'boop'">
<img
v-if="!scope.row.details.imageUrl.startsWith('default_')"
class="x-link"
:src="getSmallThumbnailUrl(scope.row.details.imageUrl)"
style="flex: none; height: 50px; border-radius: 4px"
@click="showFullscreenImageDialog(scope.row.details.imageUrl)"
loading="lazy" />
</template>
<template v-else-if="scope.row.details && scope.row.details.imageUrl">
<img <img
class="x-link" class="x-link"
:src="getSmallThumbnailUrl(scope.row.details.imageUrl)" :src="getSmallThumbnailUrl(scope.row.details.imageUrl)"
@@ -224,7 +233,14 @@
<template v-for="response in scope.row.responses" :key="response.text"> <template v-for="response in scope.row.responses" :key="response.text">
<el-tooltip placement="top" :content="response.text"> <el-tooltip placement="top" :content="response.text">
<el-button <el-button
v-if="response.icon === 'check'" v-if="response.type === 'link'"
type="text"
:icon="Link"
size="small"
:class="['button-pd-0', 'ml-5']"
@click="openNotificationLink(response.data)" />
<el-button
v-else-if="response.icon === 'check'"
type="text" type="text"
:icon="Check" :icon="Check"
size="small" size="small"
@@ -259,13 +275,13 @@
@click=" @click="
sendNotificationResponse(scope.row.id, scope.row.responses, response.type) sendNotificationResponse(scope.row.id, scope.row.responses, response.type)
" /> " />
<!--//el-button(--> <el-button
<!--// v-else-if='response.icon === "reply" && scope.row.type === "boop"'--> v-else-if="response.icon === 'reply' && scope.row.type === 'boop'"
<!--// type='text'--> type="text"
<!--// icon='el-icon-chat-line-square'--> :icon="ChatLineSquare"
<!--// size='mini'--> size="small"
<!--// style='margin-left: 5px'--> :class="['button-pd-0', 'ml-5']"
<!--// @click='showSendBoopDialog(scope.row.senderUserId)')--> @click="showSendBoopDialog(scope.row.senderUserId)" />
<el-button <el-button
v-else-if="response.icon === 'reply'" v-else-if="response.icon === 'reply'"
type="text" type="text"
@@ -384,6 +400,7 @@
Close, Close,
CollectionTag, CollectionTag,
Delete, Delete,
Link,
Refresh Refresh
} from '@element-plus/icons-vue'; } from '@element-plus/icons-vue';
import { ElMessage, ElMessageBox } from 'element-plus'; import { ElMessage, ElMessageBox } from 'element-plus';
@@ -419,7 +436,7 @@
import SendInviteResponseDialog from './dialogs/SendInviteResponseDialog.vue'; import SendInviteResponseDialog from './dialogs/SendInviteResponseDialog.vue';
import configRepository from '../../service/config'; import configRepository from '../../service/config';
const { showUserDialog } = useUserStore(); const { showUserDialog, showSendBoopDialog } = useUserStore();
const { showWorldDialog } = useWorldStore(); const { showWorldDialog } = useWorldStore();
const { showGroupDialog } = useGroupStore(); const { showGroupDialog } = useGroupStore();
const { lastLocation, lastLocationDestination } = storeToRefs(useLocationStore()); const { lastLocation, lastLocationDestination } = storeToRefs(useLocationStore());

View File

@@ -550,8 +550,14 @@
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import {
extractFileId,
formatDateFilter,
generateEmojiStyle,
getEmojiFileName,
getPrintFileName
} from '../../../shared/utils';
import { inventoryRequest, miscRequest, userRequest, vrcPlusIconRequest, vrcPlusImageRequest } from '../../../api'; import { inventoryRequest, miscRequest, userRequest, vrcPlusIconRequest, vrcPlusImageRequest } from '../../../api';
import { extractFileId, formatDateFilter, getEmojiFileName, getPrintFileName } from '../../../shared/utils';
import { useAdvancedSettingsStore, useAuthStore, useGalleryStore, useUserStore } from '../../../stores'; import { useAdvancedSettingsStore, useAuthStore, useGalleryStore, useUserStore } from '../../../stores';
import { emojiAnimationStyleList, emojiAnimationStyleUrl } from '../../../shared/constants'; import { emojiAnimationStyleList, emojiAnimationStyleUrl } from '../../../shared/constants';
import { AppDebug } from '../../../service/appConfig'; import { AppDebug } from '../../../service/appConfig';
@@ -905,25 +911,6 @@
document.getElementById('EmojiUploadButton').click(); document.getElementById('EmojiUploadButton').click();
} }
function generateEmojiStyle(url, fps, frameCount, loopStyle) {
let framesPerLine = 2;
if (frameCount > 4) framesPerLine = 4;
if (frameCount > 16) framesPerLine = 8;
const animationDurationMs = (1000 / fps) * frameCount;
const frameSize = 1024 / framesPerLine;
const scale = 100 / (frameSize / 200);
const animStyle = loopStyle === 'pingpong' ? 'alternate' : 'none';
const style = `
transform: scale(${scale / 100});
transform-origin: top left;
width: ${frameSize}px;
height: ${frameSize}px;
background: url('${url}') 0 0;
animation: ${animationDurationMs}ms steps(1) 0s infinite ${animStyle} running animated-emoji-${frameCount};
`;
return style;
}
function deleteEmoji(fileId) { function deleteEmoji(fileId) {
miscRequest.deleteFile(fileId).then((args) => { miscRequest.deleteFile(fileId).then((args) => {
const array = emojiTable.value; const array = emojiTable.value;