mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-18 22:33:50 +02:00
UI Refresh
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div @click="confirm" class="avatar-info">
|
||||
<span style="margin-right: 5px">{{ avatarName }}</span>
|
||||
<span v-if="avatarType" :class="color" style="margin-right: 5px"><i :class="avatarTypeIcons" /></span>
|
||||
<span v-if="avatarTags" style="color: #909399; font-family: monospace; font-size: 12px">{{ avatarTags }}</span>
|
||||
<span v-if="avatarType" :class="color" class="mr-2"><i :class="avatarTypeIcons" /></span>
|
||||
<span class="mr-2">{{ avatarName }}</span>
|
||||
<span v-if="avatarTags" style="color: var(--el-text-color-secondary); font-size: 12px">{{ avatarTags }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
imageurl: String,
|
||||
userid: String,
|
||||
hintownerid: String,
|
||||
hintavatarname: String,
|
||||
hintavatarname: [String, Object],
|
||||
avatartags: Array
|
||||
});
|
||||
|
||||
@@ -45,7 +45,9 @@
|
||||
if (!props.imageurl) {
|
||||
avatarName.value = '';
|
||||
} else if (props.hintownerid) {
|
||||
avatarName.value = props.hintavatarname;
|
||||
if (typeof props.hintavatarname === 'string') {
|
||||
avatarName.value = props.hintavatarname;
|
||||
}
|
||||
ownerId = props.hintownerid;
|
||||
} else {
|
||||
try {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
v-loading="loading"
|
||||
:data="paginatedData"
|
||||
v-bind="mergedTableProps"
|
||||
:stripe="false"
|
||||
:default-sort="resolvedDefaultSort"
|
||||
@row-click="handleRowClick">
|
||||
<slot></slot>
|
||||
@@ -102,7 +103,6 @@
|
||||
delete rest.defaultSort;
|
||||
}
|
||||
return {
|
||||
stripe: true,
|
||||
...rest
|
||||
};
|
||||
});
|
||||
|
||||
@@ -168,7 +168,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
<style scoped>
|
||||
.toolbar-icon:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@@ -15,9 +15,13 @@
|
||||
{{ t('dialog.user.info.close_instance') }} </el-button
|
||||
><br /><br />
|
||||
</template>
|
||||
<span><span style="color: #409eff">PC: </span>{{ props.instance.platforms.standalonewindows }}</span
|
||||
<span
|
||||
><span style="color: var(--el-color-primary)">PC: </span
|
||||
>{{ props.instance.platforms.standalonewindows }}</span
|
||||
><br />
|
||||
<span><span style="color: #67c23a">Android: </span>{{ props.instance.platforms.android }}</span
|
||||
<span
|
||||
><span style="color: var(--el-color-success)">Android: </span
|
||||
>{{ props.instance.platforms.android }}</span
|
||||
><br />
|
||||
<span>{{ t('dialog.user.info.instance_game_version') }} {{ props.instance.gameServerVersion }}</span
|
||||
><br />
|
||||
@@ -46,13 +50,13 @@
|
||||
<span v-if="props.friendcount" style="margin-left: 5px">({{ props.friendcount }})</span>
|
||||
<span
|
||||
v-if="state.isValidInstance && !props.instance.hasCapacityForYou"
|
||||
style="margin-left: 5px; color: lightcoral"
|
||||
style="margin-left: 5px; color: var(--el-color-danger)"
|
||||
>{{ t('dialog.user.info.instance_full') }}</span
|
||||
>
|
||||
<span v-if="props.instance.queueSize" style="margin-left: 5px"
|
||||
>{{ t('dialog.user.info.instance_queue') }} {{ props.instance.queueSize }}</span
|
||||
>
|
||||
<span v-if="state.isAgeGated" style="margin-left: 5px; color: lightcoral">{{
|
||||
<span v-if="state.isAgeGated" style="margin-left: 5px; color: var(--el-color-danger)">{{
|
||||
t('dialog.user.info.instance_age_gated')
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
@@ -1,24 +1,33 @@
|
||||
<template>
|
||||
<div>
|
||||
<span v-if="!text" class="transparent">-</span>
|
||||
<span v-show="text">
|
||||
<span
|
||||
:class="{ 'x-link': link && location !== 'private' && location !== 'offline' }"
|
||||
@click="handleShowWorldDialog">
|
||||
<el-icon :class="['is-loading', 'inline-block']" style="margin-right: 3px" v-if="isTraveling"
|
||||
><Loading
|
||||
/></el-icon>
|
||||
<span>{{ text }}</span>
|
||||
</span>
|
||||
<span v-if="groupName" :class="{ 'x-link': link }" @click="handleShowGroupDialog">({{ groupName }})</span>
|
||||
<span
|
||||
v-if="region"
|
||||
:class="['flags', 'inline-block', 'ml-5', region, 'transform-[translateY(3px)]']"></span>
|
||||
<NativeTooltip v-if="isClosed" :content="t('dialog.user.info.instance_closed')">
|
||||
<div v-if="!text" class="transparent">-</div>
|
||||
<div v-show="text" class="flex items-center">
|
||||
<div v-if="region" :class="['flags', 'mr-1.5', region]"></div>
|
||||
<el-tooltip
|
||||
:content="`${t('dialog.new_instance.instance_id')}: #${instanceName}`"
|
||||
:disabled="!instanceName"
|
||||
:show-after="300"
|
||||
placement="top">
|
||||
<div
|
||||
:class="['x-location', { 'x-link': link && location !== 'private' && location !== 'offline' }]"
|
||||
class="inline-flex min-w-0 flex-nowrap items-center overflow-hidden"
|
||||
@click="handleShowWorldDialog">
|
||||
<el-icon :class="['is-loading']" class="mr-1" v-if="isTraveling"><Loading /></el-icon>
|
||||
<span class="min-w-0 truncate">{{ text }}</span>
|
||||
<span
|
||||
v-if="groupName"
|
||||
class="ml-0.5 whitespace-nowrap"
|
||||
:class="{ 'x-link': link }"
|
||||
@click.stop="handleShowGroupDialog">
|
||||
({{ groupName }})
|
||||
</span>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="isClosed" :content="t('dialog.user.info.instance_closed')">
|
||||
<el-icon :class="['inline-block', 'ml-5']" style="color: lightcoral"><WarnTriangleFilled /></el-icon>
|
||||
</NativeTooltip>
|
||||
</el-tooltip>
|
||||
<el-icon v-if="strict" :class="['inline-block', 'ml-5']"><Lock /></el-icon>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -30,6 +39,7 @@
|
||||
|
||||
import { useGroupStore, useInstanceStore, useSearchStore, useWorldStore } from '../stores';
|
||||
import { getGroupName, getWorldName, parseLocation } from '../shared/utils';
|
||||
import { accessTypeLocaleKeyMap } from '../shared/constants';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -67,6 +77,7 @@
|
||||
const isTraveling = ref(false);
|
||||
const groupName = ref('');
|
||||
const isClosed = ref(false);
|
||||
const instanceName = ref('');
|
||||
|
||||
let isDisposed = false;
|
||||
onBeforeUnmount(() => {
|
||||
@@ -108,7 +119,8 @@
|
||||
isTraveling.value = true;
|
||||
}
|
||||
const L = parseLocation(instanceId);
|
||||
setText(L, L.instanceName);
|
||||
setText(L);
|
||||
instanceName.value = L.instanceName;
|
||||
if (!L.isRealInstance) {
|
||||
return;
|
||||
}
|
||||
@@ -116,7 +128,8 @@
|
||||
const instanceRef = cachedInstances.get(L.tag);
|
||||
if (typeof instanceRef !== 'undefined') {
|
||||
if (instanceRef.displayName) {
|
||||
setText(L, instanceRef.displayName);
|
||||
setText(L);
|
||||
instanceName.value = instanceRef.displayName;
|
||||
}
|
||||
if (instanceRef.closedAt) {
|
||||
isClosed.value = true;
|
||||
@@ -147,7 +160,9 @@
|
||||
strict.value = L.strict;
|
||||
}
|
||||
|
||||
function setText(L, instanceName) {
|
||||
function setText(L) {
|
||||
const accessTypeLabel = translateAccessType(L.accessTypeName);
|
||||
|
||||
if (L.isOffline) {
|
||||
text.value = 'Offline';
|
||||
} else if (L.isPrivate) {
|
||||
@@ -156,13 +171,13 @@
|
||||
text.value = 'Traveling';
|
||||
} else if (typeof props.hint === 'string' && props.hint !== '') {
|
||||
if (L.instanceId) {
|
||||
text.value = `${props.hint} #${instanceName} ${L.accessTypeName}`;
|
||||
text.value = `${props.hint} · ${accessTypeLabel}`;
|
||||
} else {
|
||||
text.value = props.hint;
|
||||
}
|
||||
} else if (L.worldId) {
|
||||
if (L.instanceId) {
|
||||
text.value = `${L.worldId} #${instanceName} ${L.accessTypeName}`;
|
||||
text.value = `${L.worldId} · ${accessTypeLabel}`;
|
||||
} else {
|
||||
text.value = L.worldId;
|
||||
}
|
||||
@@ -172,7 +187,7 @@
|
||||
.then((name) => {
|
||||
if (!isDisposed && name && currentInstanceId() === L.tag) {
|
||||
if (L.instanceId) {
|
||||
text.value = `${name} #${instanceName} ${L.accessTypeName}`;
|
||||
text.value = `${name} · ${translateAccessType(L.accessTypeName)}`;
|
||||
} else {
|
||||
text.value = name;
|
||||
}
|
||||
@@ -182,13 +197,21 @@
|
||||
console.error(e);
|
||||
});
|
||||
} else if (L.instanceId) {
|
||||
text.value = `${ref.name} #${instanceName} ${L.accessTypeName}`;
|
||||
text.value = `${ref.name} · ${accessTypeLabel}`;
|
||||
} else {
|
||||
text.value = ref.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function translateAccessType(accessTypeName) {
|
||||
const key = accessTypeLocaleKeyMap[accessTypeName];
|
||||
if (!key) {
|
||||
return accessTypeName;
|
||||
}
|
||||
return t(key);
|
||||
}
|
||||
|
||||
function handleShowWorldDialog() {
|
||||
if (props.link) {
|
||||
let instanceId = currentInstanceId();
|
||||
@@ -218,15 +241,13 @@
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.inline-block {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.ml-5 {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.transparent {
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
:global(html.dark .x-location),
|
||||
:global(:root.dark .x-location),
|
||||
:global(:root[data-theme='dark'] .x-location) {
|
||||
color: var(--color-zinc-300);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<template>
|
||||
<span>
|
||||
<span class="x-location-world">
|
||||
<span v-if="region" :class="['flags', 'inline-block', 'mr-1.25', region]"></span>
|
||||
<span @click="showLaunchDialog" class="x-link">
|
||||
<el-icon v-if="isUnlocked" :class="['inline-block', 'mr-5']"><Unlock /></el-icon>
|
||||
<span>#{{ instanceName }} {{ accessTypeName }}</span>
|
||||
<el-icon v-if="isUnlocked" :class="['inline-block', 'mr-1.25']"><Unlock /></el-icon>
|
||||
<span> {{ accessTypeName }} #{{ instanceName }}</span>
|
||||
</span>
|
||||
<span v-if="groupName" @click="showGroupDialog" class="x-link">({{ groupName }})</span>
|
||||
<span v-if="region" :class="['flags', 'inline-block', 'ml-5', region, 'transform-[translateY(3px)]']"></span>
|
||||
<NativeTooltip v-if="isClosed" :content="t('dialog.user.info.instance_closed')">
|
||||
<el-tooltip v-if="isClosed" :content="t('dialog.user.info.instance_closed')">
|
||||
<el-icon :class="['inline-block', 'ml-5']" style="color: lightcoral"><WarnTriangleFilled /></el-icon>
|
||||
</NativeTooltip>
|
||||
</el-tooltip>
|
||||
<el-icon v-if="strict" style="display: inline-block; margin-left: 5px"><Lock /></el-icon>
|
||||
</span>
|
||||
</template>
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
import { useGroupStore, useInstanceStore, useLaunchStore } from '../stores';
|
||||
import { getGroupName, parseLocation } from '../shared/utils';
|
||||
import { accessTypeLocaleKeyMap } from '../shared/constants';
|
||||
|
||||
const { t } = useI18n();
|
||||
const { cachedInstances } = useInstanceStore();
|
||||
@@ -52,7 +53,7 @@
|
||||
function parse() {
|
||||
const locObj = props.locationobject;
|
||||
location.value = locObj.tag;
|
||||
accessTypeName.value = locObj.accessTypeName;
|
||||
accessTypeName.value = translateAccessType(locObj.accessTypeName);
|
||||
strict.value = locObj.strict;
|
||||
shortName.value = locObj.shortName;
|
||||
|
||||
@@ -97,6 +98,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
function translateAccessType(accessTypeNameRaw) {
|
||||
const key = accessTypeLocaleKeyMap[accessTypeNameRaw];
|
||||
if (!key) {
|
||||
return accessTypeNameRaw;
|
||||
}
|
||||
return t(key);
|
||||
}
|
||||
|
||||
watch(() => props.locationobject, parse, { immediate: true });
|
||||
|
||||
watch(
|
||||
@@ -126,11 +135,9 @@
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.ml-5 {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.mr-5 {
|
||||
margin-right: 5px;
|
||||
:global(html.dark .x-location-world),
|
||||
:global(:root.dark .x-location-world),
|
||||
:global(:root[data-theme='dark'] .x-location-world) {
|
||||
color: var(--color-zinc-100);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,537 +0,0 @@
|
||||
<template>
|
||||
<span
|
||||
ref="triggerEl"
|
||||
class="vrcx-native-tooltip__trigger"
|
||||
:style="triggerStyle"
|
||||
@mouseenter="onEnter"
|
||||
@mouseleave="onLeave"
|
||||
@focusin="onEnter"
|
||||
@focusout="onLeave"
|
||||
@keydown.esc="close">
|
||||
<slot />
|
||||
</span>
|
||||
|
||||
<div
|
||||
ref="tooltipEl"
|
||||
class="vrcx-native-tooltip__content"
|
||||
:class="[
|
||||
placementClass,
|
||||
{
|
||||
'has-arrow': props.showArrow,
|
||||
'is-open': isOpen,
|
||||
'is-closing': isClosing
|
||||
}
|
||||
]"
|
||||
:style="contentStyle"
|
||||
popover="manual"
|
||||
role="tooltip"
|
||||
@mouseenter="cancelClose"
|
||||
@mouseleave="onLeave">
|
||||
<slot name="content">
|
||||
<span class="vrcx-native-tooltip__text" v-text="content" />
|
||||
</slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onBeforeUnmount, ref } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
content: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
showAfter: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
placement: {
|
||||
type: String,
|
||||
default: 'top'
|
||||
},
|
||||
enterMs: {
|
||||
type: Number,
|
||||
default: 120
|
||||
},
|
||||
exitMs: {
|
||||
type: Number,
|
||||
default: 100
|
||||
},
|
||||
offset: {
|
||||
type: Number,
|
||||
default: 8
|
||||
},
|
||||
maxWidth: {
|
||||
type: String,
|
||||
default: '260px'
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showArrow: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
});
|
||||
|
||||
const ARROW_SIZE_PX = 10;
|
||||
|
||||
const triggerEl = ref(null);
|
||||
const tooltipEl = ref(null);
|
||||
|
||||
const isOpen = ref(false);
|
||||
const isClosing = ref(false);
|
||||
|
||||
const anchorName = `--vrcx-tt-${Math.random().toString(36).slice(2, 10)}`;
|
||||
|
||||
const triggerStyle = computed(() => {
|
||||
return {
|
||||
'anchor-name': anchorName
|
||||
};
|
||||
});
|
||||
|
||||
const contentStyle = computed(() => {
|
||||
const effectiveOffsetPx = props.offset + (props.showArrow ? ARROW_SIZE_PX / 2 : 0);
|
||||
return {
|
||||
'position-anchor': anchorName,
|
||||
'--vrcx-tt-enter': `${props.enterMs}ms`,
|
||||
'--vrcx-tt-exit': `${props.exitMs}ms`,
|
||||
'--vrcx-tt-offset': `${effectiveOffsetPx}px`,
|
||||
'--vrcx-tt-max-width': props.maxWidth,
|
||||
'--vrcx-tt-shift-x': `${shiftX.value}px`,
|
||||
'--vrcx-tt-shift-y': `${shiftY.value}px`,
|
||||
'--vrcx-tt-arrow-x': `${arrowX.value}px`,
|
||||
'--vrcx-tt-arrow-y': `${arrowY.value}px`
|
||||
};
|
||||
});
|
||||
|
||||
const placementClass = computed(() => {
|
||||
const normalized = String(props.placement || 'top').toLowerCase();
|
||||
return `is-${normalized}`;
|
||||
});
|
||||
|
||||
const shiftX = ref(0);
|
||||
const shiftY = ref(0);
|
||||
const arrowX = ref(0);
|
||||
const arrowY = ref(0);
|
||||
|
||||
const timers = {
|
||||
open: 0,
|
||||
close: 0,
|
||||
hide: 0
|
||||
};
|
||||
|
||||
function clearTimer(key) {
|
||||
const id = timers[key];
|
||||
if (id) {
|
||||
window.clearTimeout(id);
|
||||
timers[key] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function clearAllTimers() {
|
||||
clearTimer('open');
|
||||
clearTimer('close');
|
||||
clearTimer('hide');
|
||||
}
|
||||
|
||||
function resetOffsets() {
|
||||
shiftX.value = 0;
|
||||
shiftY.value = 0;
|
||||
arrowX.value = 0;
|
||||
arrowY.value = 0;
|
||||
}
|
||||
|
||||
function isPopoverOpen(el) {
|
||||
return Boolean(el?.matches?.(':popover-open'));
|
||||
}
|
||||
|
||||
function updateViewportShift() {
|
||||
const el = tooltipEl.value;
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
|
||||
shiftX.value = 0;
|
||||
shiftY.value = 0;
|
||||
|
||||
const rect = el.getBoundingClientRect();
|
||||
const margin = 8;
|
||||
const vw = window.innerWidth;
|
||||
const vh = window.innerHeight;
|
||||
|
||||
let dx = 0;
|
||||
let dy = 0;
|
||||
|
||||
if (rect.left < margin) {
|
||||
dx += margin - rect.left;
|
||||
}
|
||||
if (rect.right > vw - margin) {
|
||||
dx -= rect.right - (vw - margin);
|
||||
}
|
||||
if (rect.top < margin) {
|
||||
dy += margin - rect.top;
|
||||
}
|
||||
if (rect.bottom > vh - margin) {
|
||||
dy -= rect.bottom - (vh - margin);
|
||||
}
|
||||
|
||||
shiftX.value = Math.round(dx);
|
||||
shiftY.value = Math.round(dy);
|
||||
}
|
||||
|
||||
function updateArrowPosition() {
|
||||
if (!props.showArrow) {
|
||||
return;
|
||||
}
|
||||
|
||||
const trigger = triggerEl.value;
|
||||
const tooltip = tooltipEl.value;
|
||||
if (!trigger || !tooltip) {
|
||||
return;
|
||||
}
|
||||
|
||||
const placement = String(props.placement || 'top').toLowerCase();
|
||||
|
||||
const tr = trigger.getBoundingClientRect();
|
||||
const tt = tooltip.getBoundingClientRect();
|
||||
|
||||
const cs = window.getComputedStyle(tooltip);
|
||||
const padLeft = Number.parseFloat(cs.paddingLeft) || 0;
|
||||
const padRight = Number.parseFloat(cs.paddingRight) || 0;
|
||||
const padTop = Number.parseFloat(cs.paddingTop) || 0;
|
||||
const padBottom = Number.parseFloat(cs.paddingBottom) || 0;
|
||||
|
||||
const padding = 12;
|
||||
const half = ARROW_SIZE_PX / 2;
|
||||
|
||||
if (placement.startsWith('top') || placement.startsWith('bottom')) {
|
||||
const desired = tr.left + tr.width / 2 - tt.left;
|
||||
|
||||
const edgeLeft = Math.max(padding, padLeft) + half;
|
||||
const edgeRight = Math.max(padding, padRight) + half;
|
||||
const min = edgeLeft;
|
||||
const max = tt.width - edgeRight;
|
||||
|
||||
const clamped = min > max ? tt.width / 2 : Math.min(Math.max(desired, min), max);
|
||||
arrowX.value = Math.round(clamped);
|
||||
arrowY.value = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (placement.startsWith('left') || placement.startsWith('right')) {
|
||||
const desired = tr.top + tr.height / 2 - tt.top;
|
||||
|
||||
const edgeTop = Math.max(padding, padTop) + half;
|
||||
const edgeBottom = Math.max(padding, padBottom) + half;
|
||||
const min = edgeTop;
|
||||
const max = tt.height - edgeBottom;
|
||||
|
||||
const clamped = min > max ? tt.height / 2 : Math.min(Math.max(desired, min), max);
|
||||
arrowY.value = Math.round(clamped);
|
||||
arrowX.value = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function open() {
|
||||
if (props.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const el = tooltipEl.value;
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearAllTimers();
|
||||
|
||||
const doOpen = () => {
|
||||
timers.open = 0;
|
||||
|
||||
const tooltip = tooltipEl.value;
|
||||
if (!tooltip) {
|
||||
return;
|
||||
}
|
||||
|
||||
const alreadyOpen = isPopoverOpen(tooltip);
|
||||
|
||||
isClosing.value = false;
|
||||
if (!alreadyOpen) {
|
||||
isOpen.value = false;
|
||||
tooltip.showPopover();
|
||||
}
|
||||
|
||||
window.requestAnimationFrame(() => {
|
||||
updateViewportShift();
|
||||
window.requestAnimationFrame(() => {
|
||||
updateArrowPosition();
|
||||
isOpen.value = true;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
if (props.showAfter > 0) {
|
||||
timers.open = window.setTimeout(doOpen, props.showAfter);
|
||||
return;
|
||||
}
|
||||
|
||||
doOpen();
|
||||
}
|
||||
|
||||
function close(immediate = false) {
|
||||
const el = tooltipEl.value;
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearAllTimers();
|
||||
|
||||
if (immediate) {
|
||||
isOpen.value = false;
|
||||
isClosing.value = false;
|
||||
resetOffsets();
|
||||
if (isPopoverOpen(el)) {
|
||||
el.hidePopover();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
isOpen.value = false;
|
||||
isClosing.value = true;
|
||||
timers.hide = window.setTimeout(() => {
|
||||
timers.hide = 0;
|
||||
isClosing.value = false;
|
||||
resetOffsets();
|
||||
if (isPopoverOpen(el)) {
|
||||
el.hidePopover();
|
||||
}
|
||||
}, props.exitMs);
|
||||
}
|
||||
|
||||
function onEnter() {
|
||||
open();
|
||||
}
|
||||
|
||||
function onLeave() {
|
||||
clearTimer('open');
|
||||
clearTimer('close');
|
||||
timers.close = window.setTimeout(() => {
|
||||
timers.close = 0;
|
||||
close();
|
||||
}, 80);
|
||||
}
|
||||
|
||||
function cancelClose() {
|
||||
clearTimer('close');
|
||||
clearTimer('hide');
|
||||
if (isPopoverOpen(tooltipEl.value)) {
|
||||
isClosing.value = false;
|
||||
isOpen.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
close(true);
|
||||
clearAllTimers();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.vrcx-native-tooltip__trigger {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.vrcx-native-tooltip__content {
|
||||
position: fixed;
|
||||
inset: auto;
|
||||
|
||||
overflow: visible;
|
||||
clip-path: none;
|
||||
|
||||
inline-size: max-content;
|
||||
max-inline-size: min(var(--vrcx-tt-max-width), calc(100vw - 16px));
|
||||
min-inline-size: 0;
|
||||
padding: 6px 10px;
|
||||
|
||||
border-radius: var(--el-border-radius-base);
|
||||
border: 0;
|
||||
|
||||
background: var(--el-tooltip-bg-color, color-mix(in srgb, var(--el-color-black) 88%, transparent));
|
||||
color: var(--el-tooltip-text-color, var(--el-color-white));
|
||||
|
||||
box-shadow: none;
|
||||
|
||||
font-size: 12px;
|
||||
line-height: 1.35;
|
||||
|
||||
white-space: pre-line;
|
||||
word-break: break-word;
|
||||
overflow-wrap: anywhere;
|
||||
|
||||
opacity: 0;
|
||||
|
||||
transition-property: opacity;
|
||||
transition-duration: var(--vrcx-tt-exit);
|
||||
transition-timing-function: linear;
|
||||
transition-behavior: allow-discrete;
|
||||
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
:global(html.dark) .vrcx-native-tooltip__content {
|
||||
background: var(--el-tooltip-bg-color, color-mix(in srgb, var(--el-color-black) 96%, transparent));
|
||||
}
|
||||
|
||||
.vrcx-native-tooltip__content.has-arrow::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: var(--el-tooltip-bg-color, color-mix(in srgb, var(--el-color-black) 88%, transparent));
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
:global(html.dark) .vrcx-native-tooltip__content.has-arrow::before {
|
||||
background: var(--el-tooltip-bg-color, color-mix(in srgb, var(--el-color-black) 96%, transparent));
|
||||
}
|
||||
|
||||
.vrcx-native-tooltip__content.has-arrow.is-top::before,
|
||||
.vrcx-native-tooltip__content.has-arrow.is-top-start::before,
|
||||
.vrcx-native-tooltip__content.has-arrow.is-top-end::before {
|
||||
left: var(--vrcx-tt-arrow-x, 50%);
|
||||
bottom: -5px;
|
||||
translate: -50% 0;
|
||||
}
|
||||
|
||||
.vrcx-native-tooltip__content.has-arrow.is-bottom::before,
|
||||
.vrcx-native-tooltip__content.has-arrow.is-bottom-start::before,
|
||||
.vrcx-native-tooltip__content.has-arrow.is-bottom-end::before {
|
||||
left: var(--vrcx-tt-arrow-x, 50%);
|
||||
top: -5px;
|
||||
translate: -50% 0;
|
||||
}
|
||||
|
||||
.vrcx-native-tooltip__content.has-arrow.is-left::before,
|
||||
.vrcx-native-tooltip__content.has-arrow.is-left-start::before,
|
||||
.vrcx-native-tooltip__content.has-arrow.is-left-end::before {
|
||||
top: var(--vrcx-tt-arrow-y, 50%);
|
||||
right: -5px;
|
||||
translate: 0 -50%;
|
||||
}
|
||||
|
||||
.vrcx-native-tooltip__content.has-arrow.is-right::before,
|
||||
.vrcx-native-tooltip__content.has-arrow.is-right-start::before,
|
||||
.vrcx-native-tooltip__content.has-arrow.is-right-end::before {
|
||||
top: var(--vrcx-tt-arrow-y, 50%);
|
||||
left: -5px;
|
||||
translate: 0 -50%;
|
||||
}
|
||||
|
||||
.vrcx-native-tooltip__content:popover-open.is-open {
|
||||
opacity: 1;
|
||||
transition-duration: var(--vrcx-tt-enter);
|
||||
}
|
||||
|
||||
.vrcx-native-tooltip__content:popover-open.is-closing {
|
||||
opacity: 0;
|
||||
transition-duration: var(--vrcx-tt-exit);
|
||||
}
|
||||
|
||||
.vrcx-native-tooltip__content.is-top {
|
||||
left: anchor(center);
|
||||
bottom: anchor(top);
|
||||
translate: calc(-50% + var(--vrcx-tt-shift-x)) calc((-1 * var(--vrcx-tt-offset)) + var(--vrcx-tt-shift-y));
|
||||
transform-origin: bottom center;
|
||||
}
|
||||
|
||||
.vrcx-native-tooltip__content.is-top-start {
|
||||
left: anchor(left);
|
||||
bottom: anchor(top);
|
||||
translate: var(--vrcx-tt-shift-x) calc((-1 * var(--vrcx-tt-offset)) + var(--vrcx-tt-shift-y));
|
||||
transform-origin: bottom left;
|
||||
}
|
||||
|
||||
.vrcx-native-tooltip__content.is-top-end {
|
||||
right: anchor(right);
|
||||
bottom: anchor(top);
|
||||
translate: var(--vrcx-tt-shift-x) calc((-1 * var(--vrcx-tt-offset)) + var(--vrcx-tt-shift-y));
|
||||
transform-origin: bottom right;
|
||||
}
|
||||
|
||||
.vrcx-native-tooltip__content.is-bottom {
|
||||
left: anchor(center);
|
||||
top: anchor(bottom);
|
||||
translate: calc(-50% + var(--vrcx-tt-shift-x)) calc(var(--vrcx-tt-offset) + var(--vrcx-tt-shift-y));
|
||||
transform-origin: top center;
|
||||
}
|
||||
|
||||
.vrcx-native-tooltip__content.is-bottom-start {
|
||||
left: anchor(left);
|
||||
top: anchor(bottom);
|
||||
translate: var(--vrcx-tt-shift-x) calc(var(--vrcx-tt-offset) + var(--vrcx-tt-shift-y));
|
||||
transform-origin: top left;
|
||||
}
|
||||
|
||||
.vrcx-native-tooltip__content.is-bottom-end {
|
||||
right: anchor(right);
|
||||
top: anchor(bottom);
|
||||
translate: var(--vrcx-tt-shift-x) calc(var(--vrcx-tt-offset) + var(--vrcx-tt-shift-y));
|
||||
transform-origin: top right;
|
||||
}
|
||||
|
||||
.vrcx-native-tooltip__content.is-left {
|
||||
right: anchor(left);
|
||||
top: anchor(center);
|
||||
translate: calc((-1 * var(--vrcx-tt-offset)) + var(--vrcx-tt-shift-x)) calc(-50% + var(--vrcx-tt-shift-y));
|
||||
transform-origin: center right;
|
||||
}
|
||||
|
||||
.vrcx-native-tooltip__content.is-left-start {
|
||||
right: anchor(left);
|
||||
top: anchor(top);
|
||||
translate: calc((-1 * var(--vrcx-tt-offset)) + var(--vrcx-tt-shift-x)) var(--vrcx-tt-shift-y);
|
||||
transform-origin: top right;
|
||||
}
|
||||
|
||||
.vrcx-native-tooltip__content.is-left-end {
|
||||
right: anchor(left);
|
||||
bottom: anchor(bottom);
|
||||
translate: calc((-1 * var(--vrcx-tt-offset)) + var(--vrcx-tt-shift-x)) var(--vrcx-tt-shift-y);
|
||||
transform-origin: bottom right;
|
||||
}
|
||||
|
||||
.vrcx-native-tooltip__content.is-right {
|
||||
left: anchor(right);
|
||||
top: anchor(center);
|
||||
translate: calc(var(--vrcx-tt-offset) + var(--vrcx-tt-shift-x)) calc(-50% + var(--vrcx-tt-shift-y));
|
||||
transform-origin: center left;
|
||||
}
|
||||
|
||||
.vrcx-native-tooltip__content.is-right-start {
|
||||
left: anchor(right);
|
||||
top: anchor(top);
|
||||
translate: calc(var(--vrcx-tt-offset) + var(--vrcx-tt-shift-x)) var(--vrcx-tt-shift-y);
|
||||
transform-origin: top left;
|
||||
}
|
||||
|
||||
.vrcx-native-tooltip__content.is-right-end {
|
||||
left: anchor(right);
|
||||
bottom: anchor(bottom);
|
||||
translate: calc(var(--vrcx-tt-offset) + var(--vrcx-tt-shift-x)) var(--vrcx-tt-shift-y);
|
||||
transform-origin: bottom left;
|
||||
}
|
||||
|
||||
.vrcx-native-tooltip__content:not([class*='is-']) {
|
||||
left: anchor(center);
|
||||
bottom: anchor(top);
|
||||
translate: calc(-50% + var(--vrcx-tt-shift-x)) calc((-1 * var(--vrcx-tt-offset)) + var(--vrcx-tt-shift-y));
|
||||
}
|
||||
|
||||
.vrcx-native-tooltip__text {
|
||||
display: block;
|
||||
white-space: pre-line;
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="x-menu-container nav-menu-container">
|
||||
<div class="x-menu-container nav-menu-container" :class="{ 'is-collapsed': isCollapsed }">
|
||||
<template v-if="navLayoutReady">
|
||||
<div>
|
||||
<div class="nav-menu-body mt-5">
|
||||
<div v-if="updateInProgress" class="pending-update" @click="showVRCXUpdateDialog">
|
||||
<el-progress
|
||||
type="circle"
|
||||
@@ -16,80 +16,71 @@
|
||||
type="success"
|
||||
plain
|
||||
style="font-size: 19px; height: 36px; width: 44px; margin: 10px"
|
||||
@click="showVRCXUpdateDialog"
|
||||
><i class="ri-download-line"></i
|
||||
></el-button>
|
||||
@click="showVRCXUpdateDialog">
|
||||
<i class="ri-download-line"></i>
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-menu collapse :default-active="activeMenuIndex" :collapse-transition="false" ref="navMenuRef">
|
||||
<el-popover
|
||||
v-for="item in navMenuItems"
|
||||
:disabled="!item.entries?.length"
|
||||
:key="item.index"
|
||||
:ref="(el) => setNavPopoverRef(el, item.index)"
|
||||
placement="right-start"
|
||||
trigger="hover"
|
||||
:hide-after="isSteamVRRunning ? 400 : 150"
|
||||
:show-arrow="false"
|
||||
:offset="0"
|
||||
:width="navPopoverWidth"
|
||||
transition="nav-menu-slide"
|
||||
@before-enter="handleSubMenuBeforeEnter()"
|
||||
:popper-style="navPopoverStyle"
|
||||
popper-class="nav-menu-popover-popper">
|
||||
<div class="nav-menu-popover">
|
||||
<div class="nav-menu-popover__header">
|
||||
<i :class="item.icon"></i>
|
||||
<el-menu ref="navMenuRef" class="nav-menu" :collapse="isCollapsed" :collapse-transition="false">
|
||||
<template v-for="item in menuItems" :key="item.index">
|
||||
<el-menu-item
|
||||
v-if="!item.children?.length"
|
||||
:index="item.index"
|
||||
:class="{ notify: isNavItemNotified(item) }"
|
||||
@click="handleMenuItemClick(item)">
|
||||
<i :class="item.icon"></i>
|
||||
<template #title>
|
||||
<span>{{ item.titleIsCustom ? item.title : t(item.title || '') }}</span>
|
||||
</div>
|
||||
|
||||
<div class="nav-menu-popover__menu">
|
||||
<button
|
||||
v-for="entry in item.entries"
|
||||
:key="entry.label"
|
||||
type="button"
|
||||
:class="['nav-menu-popover__menu-item', { notify: isEntryNotified(entry) }]"
|
||||
@click="handleSubmenuClick(entry, item.index)">
|
||||
<i v-if="entry.icon" :class="entry.icon" class="nav-menu-popover__menu-icon"></i>
|
||||
<span class="nav-menu-popover__menu-label">{{ t(entry.label) }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<template #reference>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
<el-sub-menu v-else :index="item.index">
|
||||
<template #title>
|
||||
<div :class="{ notify: isNavItemNotified(item) }">
|
||||
<i :class="item.icon"></i>
|
||||
<span v-show="!isCollapsed">{{
|
||||
item.titleIsCustom ? item.title : t(item.title || '')
|
||||
}}</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-menu-item
|
||||
:index="item.index"
|
||||
:class="{ notify: isNavItemNotified(item) }"
|
||||
@click="handleMenuItemClick(item)">
|
||||
<i :class="item.icon"></i>
|
||||
<template #title v-if="item.tooltip">
|
||||
<span>{{ item.tooltipIsCustom ? item.tooltip : t(item.tooltip) }}</span>
|
||||
v-for="entry in item.children"
|
||||
:key="entry.index"
|
||||
:index="entry.index"
|
||||
class="pl-8!"
|
||||
:class="{ notify: isEntryNotified(entry) }"
|
||||
@click="handleSubmenuClick(entry, item.index)">
|
||||
<i v-show="entry.icon" :class="entry.icon"></i>
|
||||
<template #title>
|
||||
<span>{{ t(entry.label) }}</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
</el-popover>
|
||||
</el-sub-menu>
|
||||
</template>
|
||||
</el-menu>
|
||||
<el-divider style="width: calc(100% - 18px); margin-left: 9px"></el-divider>
|
||||
<NativeTooltip :content="t('prompt.direct_access_omni.header')" placement="right">
|
||||
<div class="bottom-button" @click="directAccessPaste"><i class="ri-compass-3-line"></i></div>
|
||||
</NativeTooltip>
|
||||
</div>
|
||||
|
||||
<div class="nav-menu-container-bottom">
|
||||
<NativeTooltip v-if="branch === 'Nightly'" :show-after="150" :content="'Feedback'" placement="right">
|
||||
<div class="nav-menu-container-bottom mb-4">
|
||||
<el-tooltip
|
||||
v-if="branch === 'Nightly'"
|
||||
:show-after="150"
|
||||
:content="'Feedback'"
|
||||
:disabled="!isCollapsed"
|
||||
placement="right">
|
||||
<div
|
||||
class="bottom-button"
|
||||
id="feedback"
|
||||
@click="!sentryErrorReporting && setSentryErrorReporting()">
|
||||
<i class="ri-feedback-line"></i>
|
||||
<span v-show="!isCollapsed" class="bottom-button__label">Feedback</span>
|
||||
</div>
|
||||
</NativeTooltip>
|
||||
</el-tooltip>
|
||||
|
||||
<el-popover
|
||||
v-model:visible="supportMenuVisible"
|
||||
placement="right"
|
||||
trigger="click"
|
||||
popper-style="padding:4px;border-radius:8px;"
|
||||
:offset="4"
|
||||
:offset="-10"
|
||||
:show-arrow="false"
|
||||
:width="200"
|
||||
:hide-after="0">
|
||||
@@ -119,11 +110,18 @@
|
||||
</div>
|
||||
<template #reference>
|
||||
<div>
|
||||
<NativeTooltip :show-after="150" :content="t('nav_tooltip.help_support')" placement="right">
|
||||
<el-tooltip
|
||||
:show-after="150"
|
||||
:content="t('nav_tooltip.help_support')"
|
||||
placement="right"
|
||||
:disabled="!isCollapsed">
|
||||
<div class="bottom-button">
|
||||
<i class="ri-question-line"></i>
|
||||
<span v-show="!isCollapsed" class="bottom-button__label">{{
|
||||
t('nav_tooltip.help_support')
|
||||
}}</span>
|
||||
</div>
|
||||
</NativeTooltip>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</el-popover>
|
||||
@@ -133,7 +131,7 @@
|
||||
placement="right"
|
||||
trigger="click"
|
||||
popper-style="padding:4px;border-radius:8px;"
|
||||
:offset="4"
|
||||
:offset="-10"
|
||||
:show-arrow="false"
|
||||
:width="200"
|
||||
:hide-after="0">
|
||||
@@ -143,7 +141,7 @@
|
||||
<div class="nav-menu-settings__meta">
|
||||
<span class="nav-menu-settings__title" @click="openGithub"
|
||||
>VRCX
|
||||
<i class="ri-heart-3-fill" style="color: #64cd8a; font-size: 14px"></i>
|
||||
<i class="ri-heart-3-fill nav-menu-settings__heart"></i>
|
||||
</span>
|
||||
<span class="nav-menu-settings__version">{{ version }}</span>
|
||||
</div>
|
||||
@@ -170,7 +168,7 @@
|
||||
:class="{ 'is-active': themeMode === theme }"
|
||||
@click="handleThemeSelect(theme)">
|
||||
<span class="nav-menu-theme__label">{{ themeDisplayName(theme) }}</span>
|
||||
<span v-if="themeMode === theme" class="nav-menu-theme__check">✔</span>
|
||||
<span v-if="themeMode === theme" class="nav-menu-theme__check">✓</span>
|
||||
</button>
|
||||
</div>
|
||||
<template #reference>
|
||||
@@ -190,9 +188,24 @@
|
||||
<template #reference>
|
||||
<div class="bottom-button">
|
||||
<i class="ri-settings-3-line"></i>
|
||||
<span v-show="!isCollapsed" class="bottom-button__label">{{
|
||||
t('nav_tooltip.manage')
|
||||
}}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-popover>
|
||||
<el-tooltip
|
||||
:show-after="150"
|
||||
:content="t('nav_tooltip.expand_menu')"
|
||||
:disabled="!isCollapsed"
|
||||
placement="right">
|
||||
<div class="bottom-button" @click="toggleNavCollapse">
|
||||
<i class="ri-side-bar-line"></i>
|
||||
<span v-show="!isCollapsed" class="bottom-button__label">{{
|
||||
t('nav_tooltip.collapse_menu')
|
||||
}}</span>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -215,9 +228,9 @@
|
||||
useAdvancedSettingsStore,
|
||||
useAppearanceSettingsStore,
|
||||
useAuthStore,
|
||||
useGameStore,
|
||||
useSearchStore,
|
||||
useUiStore,
|
||||
useUserStore,
|
||||
useVRCXUpdaterStore
|
||||
} from '../stores';
|
||||
import { THEME_CONFIG, links, navDefinitions } from '../shared/constants';
|
||||
@@ -226,8 +239,6 @@
|
||||
|
||||
import configRepository from '../service/config';
|
||||
|
||||
import 'remixicon/fonts/remixicon.css';
|
||||
|
||||
const CustomNavDialog = defineAsyncComponent(() => import('./dialogs/CustomNavDialog.vue'));
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
@@ -257,26 +268,13 @@
|
||||
},
|
||||
{ type: 'item', key: 'notification' },
|
||||
{ type: 'item', key: 'charts' },
|
||||
{ type: 'item', key: 'tools' }
|
||||
{ type: 'item', key: 'tools' },
|
||||
{ type: 'item', key: 'direct-access' }
|
||||
];
|
||||
|
||||
const navDefinitionMap = new Map(navDefinitions.map((item) => [item.key, item]));
|
||||
const DEFAULT_FOLDER_ICON = 'ri-menu-fold-line';
|
||||
|
||||
const navPopoverWidth = 250;
|
||||
const navPopoverStyle = {
|
||||
zIndex: 500,
|
||||
borderRadius: '0',
|
||||
border: '1px solid var(--el-border-color)',
|
||||
borderLeft: 'none',
|
||||
borderBottom: 'none',
|
||||
borderTop: 'none',
|
||||
boxShadow: '0 8px 20px rgba(0,0,0,0.05)',
|
||||
padding: '0',
|
||||
background: 'var(--el-bg-color)',
|
||||
height: '100vh'
|
||||
};
|
||||
|
||||
const VRCXUpdaterStore = useVRCXUpdaterStore();
|
||||
const { pendingVRCXUpdate, pendingVRCXInstall, updateInProgress, updateProgress, branch, appVersion } =
|
||||
storeToRefs(VRCXUpdaterStore);
|
||||
@@ -288,18 +286,19 @@
|
||||
const { setSentryErrorReporting } = useAdvancedSettingsStore();
|
||||
const { logout } = useAuthStore();
|
||||
const appearanceSettingsStore = useAppearanceSettingsStore();
|
||||
const { themeMode } = storeToRefs(appearanceSettingsStore);
|
||||
const { isSteamVRRunning } = storeToRefs(useGameStore());
|
||||
const { themeMode, isNavCollapsed: isCollapsed } = storeToRefs(appearanceSettingsStore);
|
||||
const userStore = useUserStore();
|
||||
const { currentUser } = storeToRefs(userStore);
|
||||
const { showUserDialog } = userStore;
|
||||
|
||||
const settingsMenuVisible = ref(false);
|
||||
const themeMenuVisible = ref(false);
|
||||
const supportMenuVisible = ref(false);
|
||||
const navMenuRef = ref(null);
|
||||
const navPopoverRefs = new Map();
|
||||
const navLayout = ref([]);
|
||||
const navLayoutReady = ref(false);
|
||||
|
||||
const navMenuItems = computed(() => {
|
||||
const menuItems = computed(() => {
|
||||
const items = [];
|
||||
navLayout.value.forEach((entry) => {
|
||||
if (entry.type === 'item') {
|
||||
@@ -310,7 +309,7 @@
|
||||
items.push({
|
||||
...definition,
|
||||
index: definition.key,
|
||||
tooltipIsCustom: false,
|
||||
title: definition.tooltip || definition.labelKey,
|
||||
titleIsCustom: false
|
||||
});
|
||||
return;
|
||||
@@ -324,7 +323,6 @@
|
||||
items.push({
|
||||
...definition,
|
||||
index: definition.key,
|
||||
tooltipIsCustom: false,
|
||||
titleIsCustom: false
|
||||
});
|
||||
});
|
||||
@@ -334,83 +332,41 @@
|
||||
const folderEntries = folderDefinitions.map((definition) => ({
|
||||
label: definition.labelKey,
|
||||
routeName: definition.routeName,
|
||||
key: definition.key,
|
||||
icon: definition.icon
|
||||
index: definition.key,
|
||||
icon: definition.icon,
|
||||
action: definition.action
|
||||
}));
|
||||
|
||||
items.push({
|
||||
index: entry.id,
|
||||
icon: entry.icon || DEFAULT_FOLDER_ICON,
|
||||
tooltip: entry.name?.trim() || t('nav_menu.custom_nav.folder_name_placeholder'),
|
||||
tooltipIsCustom: true,
|
||||
title: entry.name?.trim() || t('nav_menu.custom_nav.folder_name_placeholder'),
|
||||
titleIsCustom: true,
|
||||
entries: folderEntries
|
||||
children: folderEntries
|
||||
});
|
||||
}
|
||||
});
|
||||
return items;
|
||||
});
|
||||
|
||||
const folderCyclePointers = new Map();
|
||||
|
||||
const navigateToFolderEntry = (folderIndex, entry) => {
|
||||
if (!entry) {
|
||||
return;
|
||||
}
|
||||
if (entry.routeName) {
|
||||
handleRouteChange(entry.routeName, folderIndex);
|
||||
return;
|
||||
}
|
||||
if (entry.path) {
|
||||
router.push(entry.path);
|
||||
if (folderIndex) {
|
||||
navMenuRef.value?.updateActiveIndex(folderIndex);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleFolderCycleNavigation = (item) => {
|
||||
if (!item?.entries?.length) {
|
||||
return;
|
||||
}
|
||||
const entries = item.entries.filter((entry) => Boolean(entry?.routeName || entry?.path));
|
||||
if (!entries.length) {
|
||||
return;
|
||||
}
|
||||
let pointer = folderCyclePointers.get(item.index) ?? 0;
|
||||
if (pointer >= entries.length || pointer < 0) {
|
||||
pointer = 0;
|
||||
}
|
||||
const entry = entries[pointer];
|
||||
folderCyclePointers.set(item.index, (pointer + 1) % entries.length);
|
||||
navigateToFolderEntry(item.index, entry);
|
||||
};
|
||||
|
||||
const activeMenuIndex = computed(() => {
|
||||
const currentRouteName = router.currentRoute.value?.name;
|
||||
if (!currentRouteName) {
|
||||
const firstEntry = navLayout.value[0];
|
||||
if (!firstEntry) {
|
||||
return 'feed';
|
||||
}
|
||||
return firstEntry.type === 'folder' ? firstEntry.id : firstEntry.key;
|
||||
const currentRoute = router.currentRoute.value;
|
||||
const currentRouteName = currentRoute?.name;
|
||||
const navKey = currentRoute?.meta?.navKey || currentRouteName;
|
||||
if (!navKey) {
|
||||
return getFirstNavRoute(navLayout.value) || 'feed';
|
||||
}
|
||||
|
||||
for (const entry of navLayout.value) {
|
||||
if (entry.type === 'item' && entry.key === currentRouteName) {
|
||||
if (entry.type === 'item' && entry.key === navKey) {
|
||||
return entry.key;
|
||||
}
|
||||
if (entry.type === 'folder' && entry.items?.includes(currentRouteName)) {
|
||||
return entry.id;
|
||||
if (entry.type === 'folder' && entry.items?.includes(navKey)) {
|
||||
return navKey;
|
||||
}
|
||||
}
|
||||
|
||||
const fallback = navLayout.value[0];
|
||||
if (!fallback) {
|
||||
return 'feed';
|
||||
}
|
||||
return fallback.type === 'folder' ? fallback.id : fallback.key;
|
||||
return getFirstNavRoute(navLayout.value) || 'feed';
|
||||
});
|
||||
|
||||
const version = computed(() => appVersion.value?.split('VRCX ')?.[1] || '-');
|
||||
@@ -448,6 +404,10 @@
|
||||
|
||||
const generateFolderId = () => `nav-folder-${dayjs().toISOString()}-${Math.random().toString().slice(2, 4)}`;
|
||||
|
||||
const showCurrentUserDialog = () => {
|
||||
showUserDialog(currentUser.value?.id);
|
||||
};
|
||||
|
||||
const sanitizeLayout = (layout) => {
|
||||
const usedKeys = new Set();
|
||||
const normalized = [];
|
||||
@@ -627,52 +587,47 @@
|
||||
if (notifiedMenus.value.includes(item.index)) {
|
||||
return true;
|
||||
}
|
||||
if (item.entries?.length) {
|
||||
return item.entries.some((entry) => isEntryNotified(entry));
|
||||
if (item.children?.length) {
|
||||
return item.children.some((entry) => isEntryNotified(entry));
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const setNavPopoverRef = (el, index) => {
|
||||
if (!index) {
|
||||
return;
|
||||
}
|
||||
if (el) {
|
||||
navPopoverRefs.set(index, el);
|
||||
} else {
|
||||
navPopoverRefs.delete(index);
|
||||
}
|
||||
};
|
||||
|
||||
const closeNavPopover = (index) => {
|
||||
navPopoverRefs.get(index)?.hide?.();
|
||||
};
|
||||
|
||||
const handleSubmenuClick = (entry, index) => {
|
||||
if (!entry) {
|
||||
return;
|
||||
}
|
||||
const entries = navMenuItems.value.find((item) => item.index === index)?.entries || [];
|
||||
const indexOfEntry = entries.findIndex((e) => e.label === entry.label);
|
||||
folderCyclePointers.set(index, (indexOfEntry + 1) % entries.length);
|
||||
|
||||
if (entry.routeName) {
|
||||
handleRouteChange(entry.routeName, index || entry.routeName);
|
||||
} else if (entry.path) {
|
||||
router.push(entry.path);
|
||||
if (index) {
|
||||
navMenuRef.value?.updateActiveIndex(index);
|
||||
}
|
||||
}
|
||||
closeNavPopover(index);
|
||||
};
|
||||
|
||||
const handleSubMenuBeforeEnter = () => {
|
||||
const closeNavFlyouts = () => {
|
||||
settingsMenuVisible.value = false;
|
||||
supportMenuVisible.value = false;
|
||||
themeMenuVisible.value = false;
|
||||
};
|
||||
|
||||
const triggerNavAction = (entry, navIndex = entry?.index) => {
|
||||
if (!entry) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (entry.action === 'direct-access') {
|
||||
closeNavFlyouts();
|
||||
directAccessPaste();
|
||||
if (navIndex) {
|
||||
navMenuRef.value?.updateActiveIndex(navIndex);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (entry.routeName) {
|
||||
handleRouteChange(entry.routeName, navIndex);
|
||||
closeNavFlyouts();
|
||||
return;
|
||||
}
|
||||
|
||||
if (entry.path) {
|
||||
router.push(entry.path);
|
||||
if (navIndex) {
|
||||
navMenuRef.value?.updateActiveIndex(navIndex);
|
||||
}
|
||||
closeNavFlyouts();
|
||||
}
|
||||
};
|
||||
|
||||
const handleRouteChange = (routeName, navIndex = routeName) => {
|
||||
if (!routeName) {
|
||||
return;
|
||||
@@ -697,17 +652,23 @@
|
||||
}
|
||||
});
|
||||
|
||||
const getFirstNavRoute = (layout) => {
|
||||
function getFirstNavRoute(layout) {
|
||||
for (const entry of layout) {
|
||||
if (entry.type === 'item') {
|
||||
return entry.key;
|
||||
const definition = navDefinitionMap.get(entry.key);
|
||||
if (definition?.routeName) {
|
||||
return definition.routeName;
|
||||
}
|
||||
}
|
||||
if (entry.type === 'folder' && entry.items?.length) {
|
||||
return entry.items[0];
|
||||
const definition = entry.items.map((key) => navDefinitionMap.get(key)).find((def) => def?.routeName);
|
||||
if (definition?.routeName) {
|
||||
return definition.routeName;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
let hasNavigatedToInitialRoute = false;
|
||||
const navigateToFirstNavEntry = () => {
|
||||
@@ -724,15 +685,17 @@
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmenuClick = (entry, index) => {
|
||||
const navIndex = index || entry?.index;
|
||||
triggerNavAction(entry, navIndex);
|
||||
};
|
||||
|
||||
const handleMenuItemClick = (item) => {
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
if (item.entries?.length) {
|
||||
handleFolderCycleNavigation(item);
|
||||
return;
|
||||
}
|
||||
handleRouteChange(item.routeName, item.index);
|
||||
triggerNavAction(item, item?.index);
|
||||
};
|
||||
|
||||
const toggleNavCollapse = () => {
|
||||
appearanceSettingsStore.toggleNavCollapsed();
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
@@ -755,146 +718,147 @@
|
||||
:deep(.el-divider) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.nav-menu-container {
|
||||
position: relative;
|
||||
width: 240px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex: 0 0 240px;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
align-items: stretch;
|
||||
justify-content: flex-start;
|
||||
z-index: 600;
|
||||
background-color: var(--el-bg-color);
|
||||
border-right: 1px solid var(--el-border-color);
|
||||
background-color: var(--el-bg-color-page);
|
||||
box-shadow: none;
|
||||
.el-menu {
|
||||
background: 0;
|
||||
border: 0;
|
||||
}
|
||||
.el-menu-item i[class*='ri-'] {
|
||||
font-size: 19px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.bottom-button {
|
||||
font-size: 19px;
|
||||
width: 64px;
|
||||
height: 56px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
}
|
||||
.bottom-button:hover {
|
||||
background-color: var(--el-menu-hover-bg-color);
|
||||
transition:
|
||||
border-color var(--el-transition-duration),
|
||||
background-color var(--el-transition-duration),
|
||||
color var(--el-transition-duration);
|
||||
}
|
||||
.nav-menu-container-bottom {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
backdrop-filter: blur(14px) saturate(130%);
|
||||
}
|
||||
|
||||
.nav-menu-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden auto;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nav-menu {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nav-menu :deep(.el-menu-item),
|
||||
.nav-menu :deep(.el-sub-menu__title) {
|
||||
height: 46px;
|
||||
line-height: 46px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 10px;
|
||||
font-size: 13px;
|
||||
padding: 0 20px !important;
|
||||
}
|
||||
|
||||
.nav-menu :deep(.el-menu-item i[class*='ri-']),
|
||||
.nav-menu :deep(.el-sub-menu__title i[class*='ri-']) {
|
||||
font-size: 19px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
line-height: 1;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.nav-menu :deep(.el-sub-menu__title > div) {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.nav-menu :deep(.el-sub-menu__icon-arrow) {
|
||||
right: 8px;
|
||||
}
|
||||
|
||||
.bottom-button {
|
||||
font-size: 19px;
|
||||
width: 100%;
|
||||
height: 46px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 10px;
|
||||
padding: 0 20px;
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
& > span {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-menu-popover {
|
||||
.bottom-button i {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.bottom-button__label {
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-regular);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.bottom-button:hover {
|
||||
background-color: var(--el-menu-hover-bg-color);
|
||||
transition:
|
||||
border-color var(--el-transition-duration),
|
||||
background-color var(--el-transition-duration),
|
||||
color var(--el-transition-duration);
|
||||
}
|
||||
|
||||
.nav-menu-container-bottom {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-width: 240px;
|
||||
background-color: var(--el-bg-color);
|
||||
border-left: 1px solid var(--el-border-color);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.nav-menu-popover__header {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-height: 52px;
|
||||
padding: 0 20px;
|
||||
border-bottom: 1px solid var(--el-border-color-light, var(--el-border-color));
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
.nav-menu-container.is-collapsed .nav-menu :deep(.el-menu-item),
|
||||
.nav-menu-container.is-collapsed .nav-menu :deep(.el-sub-menu__title) {
|
||||
column-gap: 0;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.nav-menu-popover__header i {
|
||||
font-size: 18px;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
.nav-menu-container.is-collapsed {
|
||||
width: 64px;
|
||||
flex-basis: 64px;
|
||||
}
|
||||
|
||||
.nav-menu-popover__menu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
gap: 6px;
|
||||
padding: 12px 12px 16px;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
.nav-menu-container.is-collapsed .nav-menu :deep(.el-sub-menu__title > div) {
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.nav-menu-popover__menu::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
.nav-menu-container.is-collapsed .bottom-button {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
gap: 0;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.nav-menu-popover__menu::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(0, 0, 0, 0.18);
|
||||
border-radius: 3px;
|
||||
}
|
||||
:deep(.el-menu-item .el-menu-tooltip__trigger) {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.nav-menu-popover__menu::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.nav-menu-popover__menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
text-align: left;
|
||||
color: var(--el-text-color-primary);
|
||||
font-size: 13px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background-color var(--el-transition-duration);
|
||||
}
|
||||
|
||||
.nav-menu-popover__menu-item.notify::after {
|
||||
content: '';
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--el-text-color-primary);
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.nav-menu-popover__menu-item:hover {
|
||||
background-color: var(--el-menu-hover-bg-color);
|
||||
}
|
||||
|
||||
.nav-menu-popover__menu-item:focus-visible {
|
||||
outline: 2px solid var(--el-color-primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.nav-menu-popover__menu-icon {
|
||||
font-size: 16px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.nav-menu-popover__menu-label {
|
||||
font-weight: 600;
|
||||
}
|
||||
:deep(.el-button.is-text:not(.is-disabled):hover) {
|
||||
background-color: var(--el-menu-hover-bg-color);
|
||||
}
|
||||
|
||||
.nav-menu-settings {
|
||||
@@ -930,6 +894,11 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nav-menu-settings__heart {
|
||||
font-size: 14px;
|
||||
color: var(--el-color-success);
|
||||
}
|
||||
|
||||
.nav-menu-settings__version {
|
||||
font-size: 11px;
|
||||
}
|
||||
@@ -942,8 +911,8 @@
|
||||
width: 100%;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--el-text-color-primary);
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-regular);
|
||||
font-size: 13px;
|
||||
border-radius: 4px;
|
||||
transition: background-color var(--el-transition-duration);
|
||||
cursor: pointer;
|
||||
@@ -962,7 +931,7 @@
|
||||
}
|
||||
|
||||
.nav-menu-settings__item--danger:hover {
|
||||
background-color: rgba(245, 108, 108, 0.18);
|
||||
background-color: color-mix(in oklch, var(--el-color-danger) 18%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -971,23 +940,6 @@
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.nav-menu-support__search {
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
background: var(--el-fill-color-light);
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.nav-menu-support__heading {
|
||||
padding: 4px 12px 0;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.nav-menu-support__section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -1015,7 +967,6 @@
|
||||
padding: 6px 10px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--el-text-color-primary);
|
||||
font-size: 13px;
|
||||
border-radius: 6px;
|
||||
transition: background-color var(--el-transition-duration);
|
||||
@@ -1031,18 +982,4 @@
|
||||
background-color: var(--el-menu-hover-bg-color);
|
||||
}
|
||||
}
|
||||
|
||||
:global(.nav-menu-slide-enter-active),
|
||||
:global(.nav-menu-slide-leave-active) {
|
||||
transition:
|
||||
opacity 0.1s ease,
|
||||
transform 0.1s ease;
|
||||
transform-origin: left center;
|
||||
}
|
||||
|
||||
:global(.nav-menu-slide-enter-from),
|
||||
:global(.nav-menu-slide-leave-to) {
|
||||
opacity: 0;
|
||||
transform: translateX(-12px);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
class="x-dialog x-avatar-dialog"
|
||||
v-model="avatarDialog.visible"
|
||||
:show-close="false"
|
||||
width="700px">
|
||||
top="10vh"
|
||||
width="930px">
|
||||
<div v-loading="avatarDialog.loading">
|
||||
<div style="display: flex">
|
||||
<img
|
||||
@@ -246,11 +247,7 @@
|
||||
style="margin-left: 5px"
|
||||
@click="selectAvatarWithoutConfirmation(avatarDialog.id)"></el-button>
|
||||
</el-tooltip>
|
||||
<el-dropdown
|
||||
trigger="click"
|
||||
size="small"
|
||||
style="margin-left: 5px"
|
||||
@command="avatarDialogCommand">
|
||||
<el-dropdown trigger="click" style="margin-left: 5px" @command="avatarDialogCommand">
|
||||
<el-button
|
||||
:type="avatarDialog.isBlocked ? 'danger' : 'default'"
|
||||
:icon="MoreFilled"
|
||||
|
||||
@@ -321,7 +321,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
<style scoped>
|
||||
.img-size {
|
||||
width: 500px;
|
||||
height: 375px;
|
||||
|
||||
@@ -68,12 +68,12 @@
|
||||
<span
|
||||
v-if="avatar.releaseStatus === 'public'"
|
||||
class="extra"
|
||||
style="color: #67c23a"
|
||||
style="color: var(--el-color-success)"
|
||||
v-text="avatar.releaseStatus"></span>
|
||||
<span
|
||||
v-else-if="avatar.releaseStatus === 'private'"
|
||||
class="extra"
|
||||
style="color: #f56c6c"
|
||||
style="color: var(--el-color-danger)"
|
||||
v-text="avatar.releaseStatus"></span>
|
||||
<span v-else class="extra" v-text="avatar.releaseStatus"></span>
|
||||
<span class="extra" v-text="avatarTagStrings.get(avatar.id)"></span>
|
||||
|
||||
@@ -329,8 +329,9 @@
|
||||
);
|
||||
|
||||
const openFolderEditor = (index) => {
|
||||
folderEditor.isEditing = !!index;
|
||||
folderEditor.index = folderEditor.isEditing ? index : -1;
|
||||
const isEditing = index !== undefined && index !== null;
|
||||
folderEditor.isEditing = isEditing;
|
||||
folderEditor.index = isEditing ? index : -1;
|
||||
if (folderEditor.isEditing) {
|
||||
const entry = localLayout.value[index];
|
||||
folderEditor.data = {
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
:z-index="groupDialogIndex"
|
||||
v-model="groupDialog.visible"
|
||||
:show-close="false"
|
||||
width="770px"
|
||||
top="10vh"
|
||||
width="930px"
|
||||
class="x-dialog x-group-dialog">
|
||||
<div v-loading="groupDialog.loading" class="group-body">
|
||||
<div style="display: flex">
|
||||
@@ -258,11 +259,7 @@
|
||||
@click="joinGroup(groupDialog.id)"></el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<el-dropdown
|
||||
trigger="click"
|
||||
size="small"
|
||||
style="margin-left: 5px"
|
||||
@command="groupDialogCommand">
|
||||
<el-dropdown trigger="click" style="margin-left: 5px" @command="groupDialogCommand">
|
||||
<el-button
|
||||
:type="groupDialog.ref.membershipStatus === 'userblocked' ? 'danger' : 'default'"
|
||||
:icon="MoreFilled"
|
||||
@@ -616,7 +613,8 @@
|
||||
<span class="name">{{ t('dialog.group.info.links') }}</span>
|
||||
<div
|
||||
v-if="groupDialog.ref.links && groupDialog.ref.links.length > 0"
|
||||
style="margin-top: 5px">
|
||||
style="margin-top: 5px"
|
||||
class="flex">
|
||||
<template v-for="(link, index) in groupDialog.ref.links" :key="index">
|
||||
<el-tooltip v-if="link">
|
||||
<template #content>
|
||||
@@ -1086,7 +1084,6 @@
|
||||
<el-tabs
|
||||
v-model="groupDialogGalleryCurrentName"
|
||||
v-loading="isGroupGalleryLoading"
|
||||
type="card"
|
||||
style="margin-top: 10px">
|
||||
<template v-for="(gallery, index) in groupDialog.ref.galleries" :key="index">
|
||||
<el-tab-pane>
|
||||
@@ -1839,7 +1836,7 @@
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
<style scoped>
|
||||
.time-group-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
width="90vw">
|
||||
<div>
|
||||
<h3>{{ groupMemberModeration.groupRef.name }}</h3>
|
||||
<el-tabs type="card" style="height: 100%">
|
||||
<el-tabs style="height: 100%">
|
||||
<el-tab-pane :label="t('dialog.group_member_moderation.members')">
|
||||
<div style="margin-top: 10px">
|
||||
<el-button
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
:title="t('dialog.new_instance.header')"
|
||||
width="650px"
|
||||
append-to-body>
|
||||
<el-tabs v-model="newInstanceDialog.selectedTab" type="card" @tab-click="newInstanceTabClick">
|
||||
<el-tabs v-model="newInstanceDialog.selectedTab" @tab-click="newInstanceTabClick">
|
||||
<el-tab-pane name="Normal" :label="t('dialog.new_instance.normal')">
|
||||
<el-form :model="newInstanceDialog" label-width="150px">
|
||||
<el-form-item :label="t('dialog.new_instance.access_type')">
|
||||
|
||||
@@ -63,14 +63,7 @@
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button
|
||||
size="small"
|
||||
@click="
|
||||
redirectToToolsTab();
|
||||
showGalleryDialog();
|
||||
"
|
||||
>{{ t('dialog.boop_dialog.emoji_manager') }}</el-button
|
||||
>
|
||||
<el-button size="small" @click="showGalleryPage">{{ t('dialog.boop_dialog.emoji_manager') }}</el-button>
|
||||
<el-button size="small" @click="closeDialog">{{ t('dialog.boop_dialog.cancel') }}</el-button>
|
||||
<el-button size="small" :disabled="!sendBoopDialog.userId" @click="sendBoop">{{
|
||||
t('dialog.boop_dialog.send')
|
||||
@@ -87,7 +80,6 @@
|
||||
import { notificationRequest, userRequest } from '../../api';
|
||||
import { miscRequest } from '../../api';
|
||||
import { photonEmojis } from '../../shared/constants/photon.js';
|
||||
import { redirectToToolsTab } from '../../shared/utils/base/ui';
|
||||
import { useGalleryStore } from '../../stores';
|
||||
import { useNotificationStore } from '../../stores';
|
||||
import { useUserStore } from '../../stores/user.js';
|
||||
@@ -98,7 +90,7 @@
|
||||
|
||||
const { sendBoopDialog } = storeToRefs(useUserStore());
|
||||
const { notificationTable } = storeToRefs(useNotificationStore());
|
||||
const { showGalleryDialog, refreshEmojiTable } = useGalleryStore();
|
||||
const { showGalleryPage, refreshEmojiTable } = useGalleryStore();
|
||||
const { emojiTable } = storeToRefs(useGalleryStore());
|
||||
const { isLocalUserVrcPlusSupporter } = storeToRefs(useUserStore());
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
@click="userDialogCommand('Add Favorite')"></el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<el-dropdown trigger="click" size="small" @command="onCommand">
|
||||
<el-dropdown trigger="click" @command="onCommand">
|
||||
<el-button
|
||||
:type="
|
||||
userDialog.incomingRequest || userDialog.outgoingRequest
|
||||
@@ -132,7 +132,7 @@
|
||||
:icon="CircleCheck"
|
||||
command="Moderation Unblock"
|
||||
divided
|
||||
style="color: #f56c6c">
|
||||
style="color: var(--el-color-danger)">
|
||||
{{ t('dialog.user.actions.moderation_unblock') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
@@ -147,7 +147,7 @@
|
||||
v-if="userDialog.isMute"
|
||||
:icon="Microphone"
|
||||
command="Moderation Unmute"
|
||||
style="color: #f56c6c">
|
||||
style="color: var(--el-color-danger)">
|
||||
{{ t('dialog.user.actions.moderation_unmute') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
@@ -161,7 +161,7 @@
|
||||
v-if="userDialog.isMuteChat"
|
||||
:icon="ChatLineRound"
|
||||
command="Moderation Enable Chatbox"
|
||||
style="color: #f56c6c">
|
||||
style="color: var(--el-color-danger)">
|
||||
{{ t('dialog.user.actions.moderation_enable_chatbox') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-else :icon="ChatDotRound" command="Moderation Disable Chatbox">
|
||||
@@ -179,7 +179,7 @@
|
||||
v-if="userDialog.isInteractOff"
|
||||
:icon="Pointer"
|
||||
command="Moderation Enable Avatar Interaction"
|
||||
style="color: #f56c6c">
|
||||
style="color: var(--el-color-danger)">
|
||||
{{ t('dialog.user.actions.moderation_enable_avatar_interaction') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-else :icon="CircleClose" command="Moderation Disable Avatar Interaction">
|
||||
@@ -189,7 +189,11 @@
|
||||
{{ t('dialog.user.actions.report_hacking') }}
|
||||
</el-dropdown-item>
|
||||
<template v-if="userDialog.isFriend">
|
||||
<el-dropdown-item :icon="Delete" command="Unfriend" divided style="color: #f56c6c">
|
||||
<el-dropdown-item
|
||||
:icon="Delete"
|
||||
command="Unfriend"
|
||||
divided
|
||||
style="color: var(--el-color-danger)">
|
||||
{{ t('dialog.user.actions.unfriend') }}
|
||||
</el-dropdown-item>
|
||||
</template>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
v-model="userDialog.visible"
|
||||
:show-close="false"
|
||||
top="10vh"
|
||||
width="940px">
|
||||
width="930px">
|
||||
<div v-loading="userDialog.loading">
|
||||
<UserSummaryHeader
|
||||
:get-user-state-text="getUserStateText"
|
||||
@@ -252,7 +252,7 @@
|
||||
style="margin-left: 5px; padding: 0"
|
||||
@click="showBioDialog"></el-button>
|
||||
</div>
|
||||
<div style="margin-top: 5px" class="flex">
|
||||
<div style="margin-top: 5px" class="flex items-center">
|
||||
<el-tooltip v-for="(link, index) in userDialog.ref.bioLinks" :key="index">
|
||||
<template #content>
|
||||
<span v-text="link"></span>
|
||||
@@ -426,10 +426,13 @@
|
||||
<div class="x-friend-item" @click="toggleAvatarCopying">
|
||||
<div class="detail">
|
||||
<span class="name">{{ t('dialog.user.info.avatar_cloning') }}</span>
|
||||
<span v-if="currentUser.allowAvatarCopying" class="extra" style="color: #67c23a">{{
|
||||
t('dialog.user.info.avatar_cloning_allow')
|
||||
}}</span>
|
||||
<span v-else class="extra" style="color: #f56c6c">{{
|
||||
<span
|
||||
v-if="currentUser.allowAvatarCopying"
|
||||
class="extra"
|
||||
style="color: var(--el-color-success)"
|
||||
>{{ t('dialog.user.info.avatar_cloning_allow') }}</span
|
||||
>
|
||||
<span v-else class="extra" style="color: var(--el-color-danger)">{{
|
||||
t('dialog.user.info.avatar_cloning_deny')
|
||||
}}</span>
|
||||
</div>
|
||||
@@ -437,10 +440,13 @@
|
||||
<div class="x-friend-item" @click="toggleAllowBooping">
|
||||
<div class="detail">
|
||||
<span class="name">{{ t('dialog.user.info.booping') }}</span>
|
||||
<span v-if="currentUser.isBoopingEnabled" class="extra" style="color: #67c23a">{{
|
||||
t('dialog.user.info.avatar_cloning_allow')
|
||||
}}</span>
|
||||
<span v-else class="extra" style="color: #f56c6c">{{
|
||||
<span
|
||||
v-if="currentUser.isBoopingEnabled"
|
||||
class="extra"
|
||||
style="color: var(--el-color-success)"
|
||||
>{{ t('dialog.user.info.avatar_cloning_allow') }}</span
|
||||
>
|
||||
<span v-else class="extra" style="color: var(--el-color-danger)">{{
|
||||
t('dialog.user.info.avatar_cloning_deny')
|
||||
}}</span>
|
||||
</div>
|
||||
@@ -451,10 +457,10 @@
|
||||
<span
|
||||
v-if="!currentUser.hasSharedConnectionsOptOut"
|
||||
class="extra"
|
||||
style="color: #67c23a"
|
||||
style="color: var(--el-color-success)"
|
||||
>{{ t('dialog.user.info.avatar_cloning_allow') }}</span
|
||||
>
|
||||
<span v-else class="extra" style="color: #f56c6c">{{
|
||||
<span v-else class="extra" style="color: var(--el-color-danger)">{{
|
||||
t('dialog.user.info.avatar_cloning_deny')
|
||||
}}</span>
|
||||
</div>
|
||||
@@ -467,10 +473,10 @@
|
||||
<span
|
||||
v-if="userDialog.ref.allowAvatarCopying"
|
||||
class="extra"
|
||||
style="color: #67c23a"
|
||||
style="color: var(--el-color-success)"
|
||||
>{{ t('dialog.user.info.avatar_cloning_allow') }}</span
|
||||
>
|
||||
<span v-else class="extra" style="color: #f56c6c">{{
|
||||
<span v-else class="extra" style="color: var(--el-color-danger)">{{
|
||||
t('dialog.user.info.avatar_cloning_deny')
|
||||
}}</span>
|
||||
</div>
|
||||
@@ -634,9 +640,10 @@
|
||||
t('dialog.user.groups.total_count', { count: userGroups.groups.length })
|
||||
}}</span>
|
||||
<template v-if="userDialogGroupEditMode">
|
||||
<span style="margin-left: 10px; color: #909399; font-size: 10px">{{
|
||||
t('dialog.user.groups.hold_shift')
|
||||
}}</span>
|
||||
<span
|
||||
style="margin-left: 10px; color: var(--el-text-color-secondary); font-size: 10px"
|
||||
>{{ t('dialog.user.groups.hold_shift') }}</span
|
||||
>
|
||||
</template>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center">
|
||||
@@ -872,7 +879,7 @@
|
||||
size="small"
|
||||
:icon="Close"
|
||||
circle
|
||||
style="color: #f56c6c; margin-left: 5px"
|
||||
style="color: var(--el-color-danger); margin-left: 5px"
|
||||
@click.stop="leaveGroup(group.id)">
|
||||
</el-button>
|
||||
<el-button
|
||||
@@ -892,7 +899,7 @@
|
||||
<span style="font-weight: bold; font-size: 16px">{{
|
||||
t('dialog.user.groups.own_groups')
|
||||
}}</span>
|
||||
<span style="color: #909399; font-size: 12px; margin-left: 5px"
|
||||
<span style="color: var(--el-text-color-secondary); font-size: 12px; margin-left: 5px"
|
||||
>{{ userGroups.ownGroups.length }}/{{
|
||||
cachedConfig?.constants?.GROUPS?.MAX_OWNED
|
||||
}}</span
|
||||
@@ -936,9 +943,10 @@
|
||||
<span style="font-weight: bold; font-size: 16px">{{
|
||||
t('dialog.user.groups.mutual_groups')
|
||||
}}</span>
|
||||
<span style="color: #909399; font-size: 12px; margin-left: 5px">{{
|
||||
userGroups.mutualGroups.length
|
||||
}}</span>
|
||||
<span
|
||||
style="color: var(--el-text-color-secondary); font-size: 12px; margin-left: 5px"
|
||||
>{{ userGroups.mutualGroups.length }}</span
|
||||
>
|
||||
<div
|
||||
class="x-friend-list"
|
||||
style="margin-top: 10px; margin-bottom: 15px; min-height: 60px">
|
||||
@@ -978,7 +986,7 @@
|
||||
<span style="font-weight: bold; font-size: 16px">{{
|
||||
t('dialog.user.groups.groups')
|
||||
}}</span>
|
||||
<span style="color: #909399; font-size: 12px; margin-left: 5px">
|
||||
<span style="color: var(--el-text-color-secondary); font-size: 12px; margin-left: 5px">
|
||||
{{ userGroups.remainingGroups.length }}
|
||||
<template v-if="currentUser.id === userDialog.id">
|
||||
/
|
||||
@@ -1143,7 +1151,12 @@
|
||||
:class="userFavoriteWorldsStatus(list[1])">
|
||||
</i>
|
||||
<span style="font-weight: bold; font-size: 14px" v-text="list[0]"></span>
|
||||
<span style="color: #909399; font-size: 10px; margin-left: 5px"
|
||||
<span
|
||||
style="
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 10px;
|
||||
margin-left: 5px;
|
||||
"
|
||||
>{{ list[2].length }}/{{ favoriteLimits.maxFavoritesPerGroup.world }}</span
|
||||
>
|
||||
</span>
|
||||
@@ -1272,13 +1285,13 @@
|
||||
<span
|
||||
v-if="avatar.releaseStatus === 'public'"
|
||||
class="extra"
|
||||
style="color: #67c23a"
|
||||
style="color: var(--el-color-success)"
|
||||
v-text="avatar.releaseStatus">
|
||||
</span>
|
||||
<span
|
||||
v-else-if="avatar.releaseStatus === 'private'"
|
||||
class="extra"
|
||||
style="color: #f56c6c"
|
||||
style="color: var(--el-color-danger)"
|
||||
v-text="avatar.releaseStatus">
|
||||
</span>
|
||||
<span v-else class="extra" v-text="avatar.releaseStatus"></span>
|
||||
@@ -1400,11 +1413,11 @@
|
||||
userRequest,
|
||||
worldRequest
|
||||
} from '../../../api';
|
||||
import { getNextDialogIndex, redirectToToolsTab } from '../../../shared/utils/base/ui';
|
||||
import { processBulk, request } from '../../../service/request';
|
||||
import { userDialogGroupSortingOptions, userDialogMutualFriendSortingOptions } from '../../../shared/constants';
|
||||
import { userDialogWorldOrderOptions, userDialogWorldSortingOptions } from '../../../shared/constants/';
|
||||
import { database } from '../../../service/database';
|
||||
import { getNextDialogIndex } from '../../../shared/utils/base/ui';
|
||||
|
||||
import SendInviteDialog from '../InviteDialog/SendInviteDialog.vue';
|
||||
import UserSummaryHeader from './UserSummaryHeader.vue';
|
||||
@@ -1455,7 +1468,7 @@
|
||||
const { refreshInviteMessageTableData } = useInviteStore();
|
||||
const { friendLogTable } = storeToRefs(useFriendStore());
|
||||
const { getFriendRequest, handleFriendDelete } = useFriendStore();
|
||||
const { clearInviteImageUpload, showFullscreenImageDialog } = useGalleryStore();
|
||||
const { clearInviteImageUpload, showFullscreenImageDialog, showGalleryPage } = useGalleryStore();
|
||||
|
||||
const { logout } = useAuthStore();
|
||||
const { cachedConfig } = storeToRefs(useAuthStore());
|
||||
@@ -1896,6 +1909,9 @@
|
||||
}
|
||||
} else if (command === 'Previous Instances') {
|
||||
showPreviousInstancesUserDialog(D.ref);
|
||||
} else if (command === 'Manage Gallery') {
|
||||
userDialog.value.visible = false;
|
||||
showGalleryPage();
|
||||
} else if (command === 'Invite To Group') {
|
||||
showInviteGroupDialog('', D.id);
|
||||
} else if (command === 'Send Boop') {
|
||||
|
||||
@@ -316,7 +316,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
<style scoped>
|
||||
.img-size {
|
||||
width: 500px;
|
||||
height: 375px;
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
:z-index="worldDialogIndex"
|
||||
class="x-dialog x-world-dialog"
|
||||
v-model="isDialogVisible"
|
||||
top="10vh"
|
||||
:show-close="false"
|
||||
width="770px">
|
||||
width="930px">
|
||||
<div v-loading="worldDialog.loading">
|
||||
<div style="display: flex">
|
||||
<img
|
||||
@@ -204,11 +205,7 @@
|
||||
style="margin-left: 5px"
|
||||
@click="worldDialogCommand('Add Favorite')" />
|
||||
</el-tooltip>
|
||||
<el-dropdown
|
||||
trigger="click"
|
||||
size="small"
|
||||
style="margin-left: 5px"
|
||||
@command="worldDialogCommand">
|
||||
<el-dropdown trigger="click" style="margin-left: 5px" @command="worldDialogCommand">
|
||||
<el-button type="default" :icon="MoreFilled" size="large" circle />
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
@@ -302,7 +299,10 @@
|
||||
command="Delete Persistent Data">
|
||||
{{ t('dialog.world.actions.delete_persistent_data') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item :icon="Delete" command="Delete" style="color: #f56c6c">
|
||||
<el-dropdown-item
|
||||
:icon="Delete"
|
||||
command="Delete"
|
||||
style="color: var(--el-color-danger)">
|
||||
{{ t('dialog.world.actions.delete') }}
|
||||
</el-dropdown-item>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user