mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-25 01:33:51 +02:00
replace el-tabs
This commit is contained in:
@@ -325,8 +325,12 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-tabs v-model="avatarDialogLastActiveTab" @tab-click="avatarDialogTabClick">
|
||||
<el-tab-pane name="Info" :label="t('dialog.avatar.info.header')">
|
||||
<TabsUnderline
|
||||
v-model="avatarDialogLastActiveTab"
|
||||
:items="avatarDialogTabs"
|
||||
:unmount-on-hide="false"
|
||||
@update:modelValue="avatarDialogTabClick">
|
||||
<template #Info>
|
||||
<div class="x-friend-list" style="max-height: unset">
|
||||
<div
|
||||
v-if="avatarDialog.galleryImages.length || avatarDialog.ref.authorId === currentUser.id"
|
||||
@@ -479,8 +483,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane name="JSON" :label="t('dialog.avatar.json.header')" style="max-height: 50vh" lazy>
|
||||
</template>
|
||||
<template #JSON>
|
||||
<Button
|
||||
class="rounded-full h-6 w-6 mr-2"
|
||||
size="icon-sm"
|
||||
@@ -503,8 +507,8 @@
|
||||
:deep="2"
|
||||
:theme="isDarkMode ? 'dark' : 'light'"
|
||||
show-icon />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</template>
|
||||
</TabsUnderline>
|
||||
</div>
|
||||
<template v-if="avatarDialog.visible">
|
||||
<SetAvatarTagsDialog v-model:setAvatarTagsDialog="setAvatarTagsDialog" />
|
||||
@@ -535,6 +539,7 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
import { InputGroupTextareaField } from '@/components/ui/input-group';
|
||||
import { TabsUnderline } from '@/components/ui/tabs';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { toast } from 'vue-sonner';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
@@ -595,6 +600,10 @@
|
||||
const modalStore = useModalStore();
|
||||
|
||||
const { t } = useI18n();
|
||||
const avatarDialogTabs = computed(() => [
|
||||
{ value: 'Info', label: t('dialog.avatar.info.header') },
|
||||
{ value: 'JSON', label: t('dialog.avatar.json.header') }
|
||||
]);
|
||||
|
||||
const avatarDialogIndex = ref(2000);
|
||||
const avatarDialogLastActiveTab = ref('Info');
|
||||
@@ -680,12 +689,11 @@
|
||||
handleAvatarDialogTab(avatarDialogLastActiveTab.value);
|
||||
}
|
||||
|
||||
function avatarDialogTabClick(obj) {
|
||||
if (obj.props.name === avatarDialogLastActiveTab.value) {
|
||||
function avatarDialogTabClick(tabName) {
|
||||
if (tabName === avatarDialogLastActiveTab.value) {
|
||||
return;
|
||||
}
|
||||
handleAvatarDialogTab(obj.props.name);
|
||||
avatarDialogLastActiveTab.value = obj.props.name;
|
||||
handleAvatarDialogTab(tabName);
|
||||
}
|
||||
|
||||
function getImageUrlFromImageId(imageId) {
|
||||
|
||||
@@ -335,8 +335,12 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-tabs v-model="groupDialogLastActiveTab" @tab-click="groupDialogTabClick">
|
||||
<el-tab-pane name="Info" :label="t('dialog.group.info.header')">
|
||||
<TabsUnderline
|
||||
v-model="groupDialogLastActiveTab"
|
||||
:items="groupDialogTabs"
|
||||
:unmount-on-hide="false"
|
||||
@update:modelValue="groupDialogTabClick">
|
||||
<template #Info>
|
||||
<div class="group-banner-image-info">
|
||||
<img
|
||||
:src="groupDialog.ref.bannerUrl"
|
||||
@@ -706,8 +710,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane name="Posts" :label="t('dialog.group.posts.header')" lazy>
|
||||
</template>
|
||||
<template #Posts>
|
||||
<template v-if="groupDialog.visible">
|
||||
<span style="margin-right: 10px; vertical-align: top"
|
||||
>{{ t('dialog.group.posts.posts_count') }} {{ groupDialog.posts.length }}</span
|
||||
@@ -822,8 +826,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane name="Members" :label="t('dialog.group.members.header')" lazy>
|
||||
</template>
|
||||
<template #Members>
|
||||
<template v-if="groupDialog.visible">
|
||||
<span
|
||||
v-if="hasGroupPermission(groupDialog.ref, 'group-members-viewall')"
|
||||
@@ -1039,8 +1043,8 @@
|
||||
</div>
|
||||
</ul>
|
||||
</template>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane name="Photos" :label="t('dialog.group.gallery.header')" lazy>
|
||||
</template>
|
||||
<template #Photos>
|
||||
<Button
|
||||
class="rounded-full"
|
||||
variant="outline"
|
||||
@@ -1050,48 +1054,51 @@
|
||||
<Spinner v-if="isGroupGalleryLoading" />
|
||||
<Refresh v-else />
|
||||
</Button>
|
||||
<el-tabs
|
||||
<TabsUnderline
|
||||
v-model="groupDialogGalleryCurrentName"
|
||||
:items="groupGalleryTabs"
|
||||
:unmount-on-hide="false"
|
||||
v-loading="isGroupGalleryLoading"
|
||||
style="margin-top: 10px">
|
||||
<template v-for="(gallery, index) in groupDialog.ref.galleries" :key="index">
|
||||
<el-tab-pane>
|
||||
<template #label>
|
||||
<span style="font-weight: bold; font-size: 16px" v-text="gallery.name" />
|
||||
<i
|
||||
class="x-status-icon"
|
||||
style="margin-left: 5px"
|
||||
:class="groupGalleryStatus(gallery)" />
|
||||
<span style="color: #909399; font-size: 12px; margin-left: 5px">{{
|
||||
groupDialog.galleries[gallery.id] ? groupDialog.galleries[gallery.id].length : 0
|
||||
}}</span>
|
||||
</template>
|
||||
<span style="color: #c7c7c7; padding: 10px" v-text="gallery.description" />
|
||||
<div
|
||||
style="
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
margin-top: 10px;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
">
|
||||
<Card
|
||||
v-for="image in groupDialog.galleries[gallery.id]"
|
||||
:key="image.id"
|
||||
class="p-0 overflow-hidden transition-shadow hover:shadow-md">
|
||||
<img
|
||||
:src="image.imageUrl"
|
||||
:class="['x-link', 'x-popover-image']"
|
||||
@click="showFullscreenImageDialog(image.imageUrl)"
|
||||
loading="lazy" />
|
||||
</Card>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
class="mt-2.5">
|
||||
<template
|
||||
v-for="(gallery, index) in groupDialog.ref.galleries"
|
||||
:key="`label-${index}`"
|
||||
v-slot:[`label-${index}`]>
|
||||
<span style="font-weight: bold; font-size: 16px" v-text="gallery.name" />
|
||||
<i class="x-status-icon" style="margin-left: 5px" :class="groupGalleryStatus(gallery)" />
|
||||
<span style="color: #909399; font-size: 12px; margin-left: 5px">{{
|
||||
groupDialog.galleries[gallery.id] ? groupDialog.galleries[gallery.id].length : 0
|
||||
}}</span>
|
||||
</template>
|
||||
</el-tabs>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane name="JSON" :label="t('dialog.group.json.header')" lazy>
|
||||
<template
|
||||
v-for="(gallery, index) in groupDialog.ref.galleries"
|
||||
:key="`content-${index}`"
|
||||
v-slot:[String(index)]>
|
||||
<span style="color: #c7c7c7; padding: 10px" v-text="gallery.description" />
|
||||
<div
|
||||
style="
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
margin-top: 10px;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
">
|
||||
<Card
|
||||
v-for="image in groupDialog.galleries[gallery.id]"
|
||||
:key="image.id"
|
||||
class="p-0 overflow-hidden transition-shadow hover:shadow-md">
|
||||
<img
|
||||
:src="image.imageUrl"
|
||||
:class="['x-link', 'x-popover-image']"
|
||||
@click="showFullscreenImageDialog(image.imageUrl)"
|
||||
loading="lazy" />
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
</TabsUnderline>
|
||||
</template>
|
||||
<template #JSON>
|
||||
<Button
|
||||
class="rounded-full h-6 w-6 mr-2"
|
||||
size="icon-sm"
|
||||
@@ -1111,8 +1118,8 @@
|
||||
:deep="2"
|
||||
:theme="isDarkMode ? 'dark' : 'light'"
|
||||
show-icon />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</template>
|
||||
</TabsUnderline>
|
||||
</div>
|
||||
<GroupPostEditDialog :dialog-data="groupPostEditDialog" :selected-gallery-file="selectedGalleryFile" />
|
||||
<PreviousInstancesGroupDialog
|
||||
@@ -1153,6 +1160,7 @@
|
||||
import { InputGroupField } from '@/components/ui/input-group';
|
||||
import { RefreshCcw } from 'lucide-vue-next';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
import { TabsUnderline } from '@/components/ui/tabs';
|
||||
import { VirtualCombobox } from '@/components/ui/virtual-combobox';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { toast } from 'vue-sonner';
|
||||
@@ -1202,6 +1210,19 @@
|
||||
import * as workerTimers from 'worker-timers';
|
||||
|
||||
const { t } = useI18n();
|
||||
const groupDialogTabs = computed(() => [
|
||||
{ value: 'Info', label: t('dialog.group.info.header') },
|
||||
{ value: 'Posts', label: t('dialog.group.posts.header') },
|
||||
{ value: 'Members', label: t('dialog.group.members.header') },
|
||||
{ value: 'Photos', label: t('dialog.group.gallery.header') },
|
||||
{ value: 'JSON', label: t('dialog.group.json.header') }
|
||||
]);
|
||||
const groupGalleryTabs = computed(() =>
|
||||
(groupDialog.value?.ref?.galleries || []).map((gallery, index) => ({
|
||||
value: String(index),
|
||||
label: gallery?.name ?? ''
|
||||
}))
|
||||
);
|
||||
|
||||
const modalStore = useModalStore();
|
||||
|
||||
@@ -1654,12 +1675,12 @@
|
||||
handleGroupDialogTab(groupDialogLastActiveTab.value);
|
||||
}
|
||||
|
||||
function groupDialogTabClick(obj) {
|
||||
if (obj.props.name === groupDialogTabCurrentName.value) {
|
||||
function groupDialogTabClick(tabName) {
|
||||
if (tabName === groupDialogTabCurrentName.value) {
|
||||
return;
|
||||
}
|
||||
handleGroupDialogTab(obj.props.name);
|
||||
groupDialogTabCurrentName.value = obj.props.name;
|
||||
handleGroupDialogTab(tabName);
|
||||
groupDialogTabCurrentName.value = tabName;
|
||||
}
|
||||
|
||||
function showGroupPostEditDialog(groupId, post) {
|
||||
|
||||
@@ -7,8 +7,12 @@
|
||||
width="90vw">
|
||||
<div>
|
||||
<h3>{{ groupMemberModeration.groupRef.name }}</h3>
|
||||
<el-tabs style="height: 100%">
|
||||
<el-tab-pane :label="t('dialog.group_member_moderation.members')">
|
||||
<TabsUnderline
|
||||
default-value="members"
|
||||
:items="groupModerationTabs"
|
||||
:unmount-on-hide="false"
|
||||
style="height: 100%">
|
||||
<template #members>
|
||||
<div style="margin-top: 10px">
|
||||
<Button
|
||||
class="rounded-full"
|
||||
@@ -124,11 +128,9 @@
|
||||
:page-sizes="pageSizes"
|
||||
:total-items="groupMemberModerationTotalItems" />
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</template>
|
||||
|
||||
<el-tab-pane
|
||||
:label="t('dialog.group_member_moderation.bans')"
|
||||
:disabled="!hasGroupPermission(groupMemberModeration.groupRef, 'group-bans-manage')">
|
||||
<template #bans>
|
||||
<div style="margin-top: 10px">
|
||||
<Button
|
||||
class="rounded-full"
|
||||
@@ -159,11 +161,9 @@
|
||||
:page-sizes="pageSizes"
|
||||
:total-items="groupBansModerationTotalItems" />
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</template>
|
||||
|
||||
<el-tab-pane
|
||||
:label="t('dialog.group_member_moderation.invites')"
|
||||
:disabled="!hasGroupPermission(groupMemberModeration.groupRef, 'group-invites-manage')">
|
||||
<template #invites>
|
||||
<div style="margin-top: 10px">
|
||||
<Button
|
||||
class="rounded-full"
|
||||
@@ -175,16 +175,32 @@
|
||||
<Refresh v-else />
|
||||
</Button>
|
||||
<br />
|
||||
<el-tabs>
|
||||
<el-tab-pane>
|
||||
<template #label>
|
||||
<span style="font-weight: bold; font-size: 16px">{{
|
||||
t('dialog.group_member_moderation.sent_invites')
|
||||
}}</span>
|
||||
<span style="color: #909399; font-size: 12px; margin-left: 5px">{{
|
||||
groupInvitesModerationTable.data.length
|
||||
}}</span>
|
||||
</template>
|
||||
<TabsUnderline default-value="sent" :items="groupInvitesTabs" :unmount-on-hide="false">
|
||||
<template #label-sent>
|
||||
<span style="font-weight: bold; font-size: 16px">{{
|
||||
t('dialog.group_member_moderation.sent_invites')
|
||||
}}</span>
|
||||
<span style="color: #909399; font-size: 12px; margin-left: 5px">{{
|
||||
groupInvitesModerationTable.data.length
|
||||
}}</span>
|
||||
</template>
|
||||
<template #label-join>
|
||||
<span style="font-weight: bold; font-size: 16px">{{
|
||||
t('dialog.group_member_moderation.join_requests')
|
||||
}}</span>
|
||||
<span style="color: #909399; font-size: 12px; margin-left: 5px">{{
|
||||
groupJoinRequestsModerationTable.data.length
|
||||
}}</span>
|
||||
</template>
|
||||
<template #label-blocked>
|
||||
<span style="font-weight: bold; font-size: 16px">{{
|
||||
t('dialog.group_member_moderation.blocked_requests')
|
||||
}}</span>
|
||||
<span style="color: #909399; font-size: 12px; margin-left: 5px">{{
|
||||
groupBlockedModerationTable.data.length
|
||||
}}</span>
|
||||
</template>
|
||||
<template #sent>
|
||||
<Button size="sm" variant="outline" @click="selectAllGroupInvites">{{
|
||||
t('dialog.group_member_moderation.select_all')
|
||||
}}</Button>
|
||||
@@ -206,17 +222,9 @@
|
||||
@click="groupMembersDeleteSentInvite"
|
||||
>{{ t('dialog.group_member_moderation.delete_sent_invite') }}</Button
|
||||
>
|
||||
</el-tab-pane>
|
||||
</template>
|
||||
|
||||
<el-tab-pane>
|
||||
<template #label>
|
||||
<span style="font-weight: bold; font-size: 16px">{{
|
||||
t('dialog.group_member_moderation.join_requests')
|
||||
}}</span>
|
||||
<span style="color: #909399; font-size: 12px; margin-left: 5px">{{
|
||||
groupJoinRequestsModerationTable.data.length
|
||||
}}</span>
|
||||
</template>
|
||||
<template #join>
|
||||
<Button size="sm" variant="outline" @click="selectAllGroupJoinRequests">{{
|
||||
t('dialog.group_member_moderation.select_all')
|
||||
}}</Button>
|
||||
@@ -262,17 +270,9 @@
|
||||
@click="groupMembersBlockJoinRequest"
|
||||
>{{ t('dialog.group_member_moderation.block_join_requests') }}</Button
|
||||
>
|
||||
</el-tab-pane>
|
||||
</template>
|
||||
|
||||
<el-tab-pane>
|
||||
<template #label>
|
||||
<span style="font-weight: bold; font-size: 16px">{{
|
||||
t('dialog.group_member_moderation.blocked_requests')
|
||||
}}</span>
|
||||
<span style="color: #909399; font-size: 12px; margin-left: 5px">{{
|
||||
groupBlockedModerationTable.data.length
|
||||
}}</span>
|
||||
</template>
|
||||
<template #blocked>
|
||||
<Button size="sm" variant="outline" @click="selectAllGroupBlocked">{{
|
||||
t('dialog.group_member_moderation.select_all')
|
||||
}}</Button>
|
||||
@@ -294,14 +294,12 @@
|
||||
@click="groupMembersDeleteBlockedRequest"
|
||||
>{{ t('dialog.group_member_moderation.delete_blocked_requests') }}</Button
|
||||
>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</template>
|
||||
</TabsUnderline>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</template>
|
||||
|
||||
<el-tab-pane
|
||||
:label="t('dialog.group_member_moderation.logs')"
|
||||
:disabled="!hasGroupPermission(groupMemberModeration.groupRef, 'group-audit-view')">
|
||||
<template #logs>
|
||||
<div style="margin-top: 10px">
|
||||
<Button
|
||||
class="rounded-full"
|
||||
@@ -352,8 +350,8 @@
|
||||
:page-sizes="pageSizes"
|
||||
:total-items="groupLogsModerationTotalItems" />
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</template>
|
||||
</TabsUnderline>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
@@ -533,6 +531,7 @@
|
||||
import { InputGroupField, InputGroupTextareaField } from '@/components/ui/input-group';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
import { TabsUnderline } from '@/components/ui/tabs';
|
||||
import { Trash2 } from 'lucide-vue-next';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { toast } from 'vue-sonner';
|
||||
@@ -566,6 +565,29 @@
|
||||
const { applyGroupMember, handleGroupMember, handleGroupMemberProps } = useGroupStore();
|
||||
const { showFullscreenImageDialog } = useGalleryStore();
|
||||
const { t } = useI18n();
|
||||
const groupModerationTabs = computed(() => [
|
||||
{ value: 'members', label: t('dialog.group_member_moderation.members') },
|
||||
{
|
||||
value: 'bans',
|
||||
label: t('dialog.group_member_moderation.bans'),
|
||||
disabled: !hasGroupPermission(groupMemberModeration.value?.groupRef, 'group-bans-manage')
|
||||
},
|
||||
{
|
||||
value: 'invites',
|
||||
label: t('dialog.group_member_moderation.invites'),
|
||||
disabled: !hasGroupPermission(groupMemberModeration.value?.groupRef, 'group-invites-manage')
|
||||
},
|
||||
{
|
||||
value: 'logs',
|
||||
label: t('dialog.group_member_moderation.logs'),
|
||||
disabled: !hasGroupPermission(groupMemberModeration.value?.groupRef, 'group-audit-view')
|
||||
}
|
||||
]);
|
||||
const groupInvitesTabs = computed(() => [
|
||||
{ value: 'sent', label: t('dialog.group_member_moderation.sent_invites') },
|
||||
{ value: 'join', label: t('dialog.group_member_moderation.join_requests') },
|
||||
{ value: 'blocked', label: t('dialog.group_member_moderation.blocked_requests') }
|
||||
]);
|
||||
const selectedUsers = reactive({});
|
||||
const selectedUsersArray = ref([]);
|
||||
const isGroupMembersLoading = ref(false);
|
||||
|
||||
@@ -5,8 +5,12 @@
|
||||
:title="t('dialog.new_instance.header')"
|
||||
width="650px"
|
||||
append-to-body>
|
||||
<el-tabs v-model="newInstanceDialog.selectedTab" @tab-click="newInstanceTabClick">
|
||||
<el-tab-pane name="Normal" :label="t('dialog.new_instance.normal')">
|
||||
<TabsUnderline
|
||||
v-model="newInstanceDialog.selectedTab"
|
||||
:items="newInstanceTabs"
|
||||
:unmount-on-hide="false"
|
||||
@update:modelValue="newInstanceTabClick">
|
||||
<template #Normal>
|
||||
<FieldGroup class="gap-4">
|
||||
<Field>
|
||||
<FieldLabel>{{ t('dialog.new_instance.access_type') }}</FieldLabel>
|
||||
@@ -221,8 +225,8 @@
|
||||
</Field>
|
||||
</template>
|
||||
</FieldGroup>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane name="Legacy" :label="t('dialog.new_instance.legacy')">
|
||||
</template>
|
||||
<template #Legacy>
|
||||
<FieldGroup class="gap-4">
|
||||
<Field>
|
||||
<FieldLabel>{{ t('dialog.new_instance.access_type') }}</FieldLabel>
|
||||
@@ -429,8 +433,8 @@
|
||||
</FieldContent>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</template>
|
||||
</TabsUnderline>
|
||||
<template v-if="newInstanceDialog.selectedTab === 'Normal'" #footer>
|
||||
<template v-if="newInstanceDialog.instanceCreated">
|
||||
<Button variant="outline" class="mr-2" @click="copyInstanceUrl(newInstanceDialog.location)">{{
|
||||
@@ -514,6 +518,7 @@
|
||||
import { Check as CheckIcon } from 'lucide-vue-next';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { InputGroupField } from '@/components/ui/input-group';
|
||||
import { TabsUnderline } from '@/components/ui/tabs';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { toast } from 'vue-sonner';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
@@ -551,6 +556,10 @@
|
||||
required: true
|
||||
}
|
||||
});
|
||||
const newInstanceTabs = computed(() => [
|
||||
{ value: 'Normal', label: t('dialog.new_instance.normal') },
|
||||
{ value: 'Legacy', label: t('dialog.new_instance.legacy') }
|
||||
]);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -858,8 +867,8 @@
|
||||
configRepository.setBool('instanceDialogAgeGate', ageGate);
|
||||
configRepository.setString('instanceDialogDisplayName', displayName);
|
||||
}
|
||||
function newInstanceTabClick(obj) {
|
||||
if (obj.props.name === 'Normal') {
|
||||
function newInstanceTabClick(tabName) {
|
||||
if (tabName === 'Normal') {
|
||||
buildInstance();
|
||||
} else {
|
||||
buildLegacyInstance();
|
||||
|
||||
@@ -14,8 +14,12 @@
|
||||
:toggle-badge-showcased="toggleBadgeShowcased"
|
||||
:user-dialog-command="userDialogCommand" />
|
||||
|
||||
<el-tabs v-model="userDialogLastActiveTab" @tab-click="userDialogTabClick">
|
||||
<el-tab-pane name="Info" :label="t('dialog.user.info.header')">
|
||||
<TabsUnderline
|
||||
v-model="userDialogLastActiveTab"
|
||||
:items="userDialogTabs"
|
||||
:unmount-on-hide="false"
|
||||
@update:modelValue="userDialogTabClick">
|
||||
<template #Info>
|
||||
<template v-if="isFriendOnline(userDialog.friend) || currentUser.id === userDialog.id">
|
||||
<div
|
||||
v-if="userDialog.ref.location"
|
||||
@@ -546,13 +550,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</template>
|
||||
|
||||
<el-tab-pane
|
||||
name="Mutual Friends"
|
||||
v-if="userDialog.id !== currentUser.id && !currentUser.hasSharedConnectionsOptOut"
|
||||
:label="t('dialog.user.mutual_friends.header')"
|
||||
lazy>
|
||||
<template v-if="userDialog.id !== currentUser.id && !currentUser.hasSharedConnectionsOptOut" #mutual>
|
||||
<div style="display: flex; align-items: center; justify-content: space-between">
|
||||
<div style="display: flex; align-items: center">
|
||||
<Button
|
||||
@@ -621,9 +621,9 @@
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</el-tab-pane>
|
||||
</template>
|
||||
|
||||
<el-tab-pane name="Groups" :label="t('dialog.user.groups.header')" lazy>
|
||||
<template #Groups>
|
||||
<div style="display: flex; align-items: center; justify-content: space-between">
|
||||
<div style="display: flex; align-items: center">
|
||||
<Button
|
||||
@@ -1012,9 +1012,9 @@
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</template>
|
||||
|
||||
<el-tab-pane name="Worlds" :label="t('dialog.user.worlds.header')" lazy>
|
||||
<template #Worlds>
|
||||
<div style="display: flex; align-items: center; justify-content: space-between">
|
||||
<div style="display: flex; align-items: center">
|
||||
<Button
|
||||
@@ -1085,9 +1085,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</template>
|
||||
|
||||
<el-tab-pane name="Favorite Worlds" :label="t('dialog.user.favorite_worlds.header')" lazy>
|
||||
<template #favorite-worlds>
|
||||
<!-- <Button
|
||||
variant="outline"
|
||||
v-if="userFavoriteWorlds && userFavoriteWorlds.length > 0"
|
||||
@@ -1099,33 +1099,35 @@
|
||||
style="position: absolute; right: 15px; bottom: 15px; z-index: 99"
|
||||
@click="getUserFavoriteWorlds(userDialog.id)">
|
||||
</Button> -->
|
||||
<el-tabs
|
||||
ref="favoriteWorldsRef"
|
||||
v-loading="userDialog.isFavoriteWorldsLoading"
|
||||
class="zero-margin-tabs"
|
||||
type="card"
|
||||
stretch
|
||||
style="margin-top: 10px; height: 50vh">
|
||||
<template v-if="userFavoriteWorlds && userFavoriteWorlds.length > 0">
|
||||
<el-tab-pane v-for="(list, index) in userFavoriteWorlds" :key="index" lazy>
|
||||
<template #label>
|
||||
<span>
|
||||
<i
|
||||
class="x-status-icon"
|
||||
style="margin-right: 6px"
|
||||
:class="userFavoriteWorldsStatus(list[1])">
|
||||
</i>
|
||||
<span style="font-weight: bold; font-size: 14px" v-text="list[0]"></span>
|
||||
<span
|
||||
style="
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 10px;
|
||||
margin-left: 5px;
|
||||
"
|
||||
>{{ list[2].length }}/{{ favoriteLimits.maxFavoritesPerGroup.world }}</span
|
||||
>
|
||||
</span>
|
||||
</template>
|
||||
<template v-if="userFavoriteWorlds && userFavoriteWorlds.length > 0">
|
||||
<TabsUnderline
|
||||
v-model="favoriteWorldsTab"
|
||||
:items="favoriteWorldTabs"
|
||||
:unmount-on-hide="false"
|
||||
v-loading="userDialog.isFavoriteWorldsLoading"
|
||||
class="zero-margin-tabs"
|
||||
style="margin-top: 10px; height: 50vh">
|
||||
<template
|
||||
v-for="(list, index) in userFavoriteWorlds"
|
||||
:key="`favorite-worlds-label-${index}`"
|
||||
v-slot:[`label-${index}`]>
|
||||
<span>
|
||||
<i
|
||||
class="x-status-icon"
|
||||
style="margin-right: 6px"
|
||||
:class="userFavoriteWorldsStatus(list[1])">
|
||||
</i>
|
||||
<span style="font-weight: bold; font-size: 14px" v-text="list[0]"></span>
|
||||
<span
|
||||
style="color: var(--el-text-color-secondary); font-size: 10px; margin-left: 5px"
|
||||
>{{ list[2].length }}/{{ favoriteLimits.maxFavoritesPerGroup.world }}</span
|
||||
>
|
||||
</span>
|
||||
</template>
|
||||
<template
|
||||
v-for="(list, index) in userFavoriteWorlds"
|
||||
:key="`favorite-worlds-content-${index}`"
|
||||
v-slot:[String(index)]>
|
||||
<div
|
||||
class="x-friend-list"
|
||||
style="margin-top: 10px; margin-bottom: 15px; min-height: 60px; max-height: none">
|
||||
@@ -1143,17 +1145,17 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</template>
|
||||
<template v-else-if="!userDialog.isFavoriteWorldsLoading">
|
||||
<div style="display: flex; justify-content: center; align-items: center; height: 100%">
|
||||
<span style="font-size: 16px">No favorite worlds found.</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-tabs>
|
||||
</el-tab-pane>
|
||||
</template>
|
||||
</TabsUnderline>
|
||||
</template>
|
||||
<template v-else-if="!userDialog.isFavoriteWorldsLoading">
|
||||
<div style="display: flex; justify-content: center; align-items: center; height: 100%">
|
||||
<span style="font-size: 16px">No favorite worlds found.</span>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<el-tab-pane name="Avatars" :label="t('dialog.user.avatars.header')" lazy>
|
||||
<template #Avatars>
|
||||
<div style="display: flex; align-items: center; justify-content: space-between">
|
||||
<div style="display: flex; align-items: center">
|
||||
<Button
|
||||
@@ -1250,9 +1252,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</template>
|
||||
|
||||
<el-tab-pane name="JSON" :label="t('dialog.user.json.header')" lazy style="height: 50vh">
|
||||
<template #JSON>
|
||||
<Button
|
||||
class="rounded-full h-6 w-6 mr-2"
|
||||
size="icon-sm"
|
||||
@@ -1272,8 +1274,8 @@
|
||||
:deep="2"
|
||||
:theme="isDarkMode ? 'dark' : 'light'"
|
||||
show-icon />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</template>
|
||||
</TabsUnderline>
|
||||
</div>
|
||||
<SendInviteDialog
|
||||
v-model:sendInviteDialogVisible="sendInviteDialogVisible"
|
||||
@@ -1319,6 +1321,7 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
import { TabsUnderline } from '@/components/ui/tabs';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { toast } from 'vue-sonner';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
@@ -1392,6 +1395,26 @@
|
||||
const EditNoteAndMemoDialog = defineAsyncComponent(() => import('./EditNoteAndMemoDialog.vue'));
|
||||
|
||||
const { t } = useI18n();
|
||||
const userDialogTabs = computed(() => {
|
||||
const tabs = [
|
||||
{ value: 'Info', label: t('dialog.user.info.header') },
|
||||
{ value: 'Groups', label: t('dialog.user.groups.header') },
|
||||
{ value: 'Worlds', label: t('dialog.user.worlds.header') },
|
||||
{ value: 'favorite-worlds', label: t('dialog.user.favorite_worlds.header') },
|
||||
{ value: 'Avatars', label: t('dialog.user.avatars.header') },
|
||||
{ value: 'JSON', label: t('dialog.user.json.header') }
|
||||
];
|
||||
if (userDialog.value.id !== currentUser.value.id && !currentUser.value.hasSharedConnectionsOptOut) {
|
||||
tabs.splice(1, 0, { value: 'mutual', label: t('dialog.user.mutual_friends.header') });
|
||||
}
|
||||
return tabs;
|
||||
});
|
||||
const favoriteWorldTabs = computed(() =>
|
||||
(userFavoriteWorlds.value || []).map((list, index) => ({
|
||||
value: String(index),
|
||||
label: list?.[0] ?? ''
|
||||
}))
|
||||
);
|
||||
|
||||
const modalStore = useModalStore();
|
||||
|
||||
@@ -1476,7 +1499,7 @@
|
||||
remainingGroups: []
|
||||
});
|
||||
|
||||
const favoriteWorldsRef = ref(null);
|
||||
const favoriteWorldsTab = ref('0');
|
||||
|
||||
const sendInviteDialogVisible = ref(false);
|
||||
const sendInviteDialog = ref({
|
||||
@@ -1588,7 +1611,7 @@
|
||||
if (vrchatCredit.value === null) {
|
||||
getVRChatCredits();
|
||||
}
|
||||
} else if (tabName === 'Mutual Friends') {
|
||||
} else if (tabName === 'mutual') {
|
||||
if (userId === currentUser.value.id) {
|
||||
userDialogLastActiveTab.value = 'Info';
|
||||
return;
|
||||
@@ -1618,7 +1641,7 @@
|
||||
userDialogLastWorld.value = userId;
|
||||
refreshUserDialogWorlds();
|
||||
}
|
||||
} else if (tabName === 'Favorite Worlds') {
|
||||
} else if (tabName === 'favorite-worlds') {
|
||||
if (userDialogLastFavoriteWorld.value !== userId) {
|
||||
userDialogLastFavoriteWorld.value = userId;
|
||||
getUserFavoriteWorlds(userId);
|
||||
@@ -1632,12 +1655,11 @@
|
||||
handleUserDialogTab(userDialogLastActiveTab.value);
|
||||
}
|
||||
|
||||
function userDialogTabClick(obj) {
|
||||
if (obj.props.name === userDialogLastActiveTab.value) {
|
||||
function userDialogTabClick(tabName) {
|
||||
if (tabName === userDialogLastActiveTab.value) {
|
||||
return;
|
||||
}
|
||||
handleUserDialogTab(obj.props.name);
|
||||
userDialogLastActiveTab.value = obj.props.name;
|
||||
handleUserDialogTab(tabName);
|
||||
}
|
||||
|
||||
function showPronounsDialog() {
|
||||
@@ -2318,9 +2340,7 @@
|
||||
|
||||
async function getUserFavoriteWorlds(userId) {
|
||||
userDialog.value.isFavoriteWorldsLoading = true;
|
||||
if (favoriteWorldsRef.value) {
|
||||
favoriteWorldsRef.value.currentName = '0'; // select first tab
|
||||
}
|
||||
favoriteWorldsTab.value = '0';
|
||||
userFavoriteWorlds.value = [];
|
||||
const worldLists = [];
|
||||
let params = {
|
||||
|
||||
@@ -314,8 +314,12 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-tabs v-model="worldDialogLastActiveTab" @tab-click="worldDialogTabClick">
|
||||
<el-tab-pane name="Instances" :label="t('dialog.world.instances.header')">
|
||||
<TabsUnderline
|
||||
v-model="worldDialogLastActiveTab"
|
||||
:items="worldDialogTabs"
|
||||
:unmount-on-hide="false"
|
||||
@update:modelValue="worldDialogTabClick">
|
||||
<template #Instances>
|
||||
<div class="">
|
||||
<el-icon><User /></el-icon>
|
||||
{{ t('dialog.world.instances.public_count', { count: worldDialog.ref.publicOccupants }) }}
|
||||
@@ -429,8 +433,8 @@
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane name="Info" :label="t('dialog.world.info.header')" lazy>
|
||||
</template>
|
||||
<template #Info>
|
||||
<div class="x-friend-list" style="max-height: none">
|
||||
<div class="x-friend-item" style="width: 100%; cursor: default">
|
||||
<div class="detail">
|
||||
@@ -695,8 +699,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane name="JSON" :label="t('dialog.world.json.header')" style="max-height: 50vh" lazy>
|
||||
</template>
|
||||
<template #JSON>
|
||||
<Button
|
||||
class="rounded-full h-6 w-6 mr-2"
|
||||
size="icon-sm"
|
||||
@@ -719,8 +723,8 @@
|
||||
:deep="2"
|
||||
:theme="isDarkMode ? 'dark' : 'light'"
|
||||
show-icon />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</template>
|
||||
</TabsUnderline>
|
||||
</div>
|
||||
|
||||
<template v-if="isDialogVisible">
|
||||
@@ -768,6 +772,7 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
import { InputGroupTextareaField } from '@/components/ui/input-group';
|
||||
import { TabsUnderline } from '@/components/ui/tabs';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { toast } from 'vue-sonner';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
@@ -834,6 +839,11 @@
|
||||
const { isGameRunning } = storeToRefs(useGameStore());
|
||||
const { showFullscreenImageDialog } = useGalleryStore();
|
||||
const { t } = useI18n();
|
||||
const worldDialogTabs = computed(() => [
|
||||
{ value: 'Instances', label: t('dialog.world.instances.header') },
|
||||
{ value: 'Info', label: t('dialog.world.info.header') },
|
||||
{ value: 'JSON', label: t('dialog.world.json.header') }
|
||||
]);
|
||||
|
||||
const treeData = ref({});
|
||||
const worldAllowedDomainsDialog = ref({
|
||||
@@ -954,12 +964,11 @@
|
||||
handleWorldDialogTab(worldDialogLastActiveTab.value);
|
||||
}
|
||||
|
||||
function worldDialogTabClick(obj) {
|
||||
if (obj.props.name === worldDialogLastActiveTab.value) {
|
||||
function worldDialogTabClick(tabName) {
|
||||
if (tabName === worldDialogLastActiveTab.value) {
|
||||
return;
|
||||
}
|
||||
handleWorldDialogTab(obj.props.name);
|
||||
worldDialogLastActiveTab.value = obj.props.name;
|
||||
handleWorldDialogTab(tabName);
|
||||
}
|
||||
|
||||
function handleDialogOpen() {
|
||||
|
||||
109
src/components/ui/tabs/TabsUnderline.vue
Normal file
109
src/components/ui/tabs/TabsUnderline.vue
Normal file
@@ -0,0 +1,109 @@
|
||||
<script setup>
|
||||
import { TabsContent, TabsIndicator, TabsList, TabsRoot, TabsTrigger } from 'reka-ui';
|
||||
import { computed, ref, toRefs, watch } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: String,
|
||||
defaultValue: String,
|
||||
items: {
|
||||
type: Array,
|
||||
required: true,
|
||||
validator: (value) =>
|
||||
Array.isArray(value) &&
|
||||
value.every(
|
||||
(item) =>
|
||||
item &&
|
||||
(typeof item.value === 'string' || typeof item.value === 'number') &&
|
||||
typeof item.label === 'string'
|
||||
)
|
||||
},
|
||||
ariaLabel: { type: String, default: '' },
|
||||
|
||||
variant: { type: String, default: 'fit' },
|
||||
unmountOnHide: { type: Boolean, default: false }
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
const { modelValue, defaultValue, items, ariaLabel, variant, unmountOnHide } = toRefs(props);
|
||||
|
||||
const resolvedDefault = computed(() => {
|
||||
return defaultValue.value ?? items.value?.[0]?.value;
|
||||
});
|
||||
|
||||
const isValueValid = (value) => items.value?.some((item) => item?.value === value);
|
||||
|
||||
const innerValue = ref(isValueValid(modelValue.value) ? modelValue.value : resolvedDefault.value);
|
||||
|
||||
watch(modelValue, (v) => {
|
||||
if (isValueValid(v)) {
|
||||
innerValue.value = v;
|
||||
}
|
||||
});
|
||||
|
||||
watch([items, defaultValue], () => {
|
||||
if (!isValueValid(innerValue.value)) {
|
||||
innerValue.value = resolvedDefault.value;
|
||||
return;
|
||||
}
|
||||
if (!isValueValid(modelValue.value)) {
|
||||
innerValue.value = resolvedDefault.value;
|
||||
}
|
||||
});
|
||||
|
||||
function onValueChange(v) {
|
||||
innerValue.value = v;
|
||||
emit('update:modelValue', v);
|
||||
}
|
||||
|
||||
const triggerClass = computed(() => {
|
||||
return [
|
||||
'relative inline-flex h-10 items-center justify-center px-3 text-sm font-medium',
|
||||
'text-muted-foreground transition-colors hover:text-foreground',
|
||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ring-offset-background',
|
||||
'disabled:pointer-events-none disabled:opacity-50',
|
||||
'data-[state=active]:text-primary',
|
||||
variant.value === 'equal' ? 'flex-1' : '',
|
||||
variant.value === 'pill' ? 'rounded-full' : ''
|
||||
].join(' ');
|
||||
});
|
||||
|
||||
const listClass = computed(() => {
|
||||
return [
|
||||
'relative flex w-full items-center gap-1 border-b border-border',
|
||||
variant.value === 'pill' ? 'rounded-full bg-muted p-1' : ''
|
||||
].join(' ');
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TabsRoot
|
||||
:model-value="innerValue"
|
||||
:default-value="resolvedDefault"
|
||||
class="w-full"
|
||||
:unmount-on-hide="unmountOnHide"
|
||||
@update:modelValue="onValueChange">
|
||||
<TabsList :class="listClass" :aria-label="ariaLabel || undefined">
|
||||
<TabsIndicator
|
||||
class="pointer-events-none absolute left-0 bottom-0 z-20 h-0.5 w-(--reka-tabs-indicator-size) translate-x-(--reka-tabs-indicator-position) transition-[width,translate] duration-200 ease-out">
|
||||
<div class="h-full w-full rounded-full bg-primary" />
|
||||
</TabsIndicator>
|
||||
|
||||
<TabsTrigger
|
||||
v-for="it in items"
|
||||
:key="it.value"
|
||||
:value="it.value"
|
||||
:disabled="it.disabled"
|
||||
:class="triggerClass">
|
||||
<slot :name="`label-${it.value}`">{{ it.label }}</slot>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent
|
||||
v-for="it in items"
|
||||
:key="it.value"
|
||||
:value="it.value"
|
||||
class="pt-4 outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ring-offset-background">
|
||||
<slot :name="it.value" />
|
||||
</TabsContent>
|
||||
</TabsRoot>
|
||||
</template>
|
||||
@@ -2,3 +2,4 @@ export { default as Tabs } from './Tabs.vue';
|
||||
export { default as TabsContent } from './TabsContent.vue';
|
||||
export { default as TabsList } from './TabsList.vue';
|
||||
export { default as TabsTrigger } from './TabsTrigger.vue';
|
||||
export { default as TabsUnderline } from './TabsUnderline.vue';
|
||||
|
||||
Reference in New Issue
Block a user