mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-18 22:33:50 +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';
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
<template>
|
||||
<div id="chart" class="x-container">
|
||||
<el-tabs v-model="activeTab" class="charts-tabs">
|
||||
<el-tab-pane :label="t('view.charts.instance_activity.header')" name="instance"></el-tab-pane>
|
||||
<el-tab-pane :label="t('view.charts.mutual_friend.tab_label')" name="mutual"></el-tab-pane>
|
||||
</el-tabs>
|
||||
<div v-show="activeTab === 'instance'">
|
||||
<InstanceActivity />
|
||||
</div>
|
||||
<div v-show="activeTab === 'mutual'">
|
||||
<MutualFriends />
|
||||
</div>
|
||||
<TabsUnderline v-model="activeTab" :items="chartTabs" :unmount-on-hide="false" class="charts-tabs">
|
||||
<template #instance>
|
||||
<InstanceActivity />
|
||||
</template>
|
||||
<template #mutual>
|
||||
<MutualFriends />
|
||||
</template>
|
||||
</TabsUnderline>
|
||||
<el-backtop target="#chart" :right="30" :bottom="30"></el-backtop>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
import { computed, defineAsyncComponent } from 'vue';
|
||||
import { TabsUnderline } from '@/components/ui/tabs';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
@@ -27,10 +26,14 @@
|
||||
const { t } = useI18n();
|
||||
const chartsStore = useChartsStore();
|
||||
const { activeTab } = storeToRefs(chartsStore);
|
||||
const chartTabs = computed(() => [
|
||||
{ value: 'instance', label: t('view.charts.instance_activity.header') },
|
||||
{ value: 'mutual', label: t('view.charts.mutual_friend.tab_label') }
|
||||
]);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.el-tabs__header) {
|
||||
:deep(.charts-tabs [data-slot='tabs-list']) {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
<template>
|
||||
<div class="friend-view x-container">
|
||||
<div v-if="settingsReady" class="friend-view__toolbar">
|
||||
<el-segmented v-model="activeSegment" :options="segmentedOptions" />
|
||||
<Tabs v-model="activeSegment" class="friend-view__tabs">
|
||||
<TabsList>
|
||||
<TabsTrigger v-for="option in segmentedOptions" :key="option.value" :value="option.value">
|
||||
{{ option.label }}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
<div class="friend-view__actions">
|
||||
<InputGroupSearch
|
||||
v-model="searchTerm"
|
||||
class="friend-view__search"
|
||||
placeholder="Search Friend" />
|
||||
<InputGroupSearch v-model="searchTerm" class="friend-view__search" placeholder="Search Friend" />
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<div>
|
||||
@@ -161,9 +164,10 @@
|
||||
|
||||
<script setup>
|
||||
import { computed, nextTick, onBeforeMount, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||
import { Loading } from '@element-plus/icons-vue';
|
||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { InputGroupSearch } from '@/components/ui/input-group';
|
||||
import { Loading } from '@element-plus/icons-vue';
|
||||
import { Settings } from 'lucide-vue-next';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
@@ -724,6 +728,10 @@
|
||||
padding: 6px 2px 0 2px;
|
||||
}
|
||||
|
||||
.friend-view__tabs {
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.friend-view__toolbar--loading {
|
||||
justify-content: flex-end;
|
||||
color: var(--el-text-color-secondary);
|
||||
|
||||
@@ -36,8 +36,8 @@
|
||||
</div>
|
||||
</TooltipWrapper>
|
||||
</div>
|
||||
<el-tabs type="card">
|
||||
<el-tab-pane :label="t('view.player_list.photon.current')">
|
||||
<TabsUnderline default-value="current" :items="photonTabs" :unmount-on-hide="false">
|
||||
<template #current>
|
||||
<DataTableLayout
|
||||
class="min-w-0 w-full"
|
||||
:table="currentTable"
|
||||
@@ -47,8 +47,8 @@
|
||||
:total-items="currentTotal"
|
||||
:on-page-size-change="handleCurrentPageSizeChange"
|
||||
style="margin-bottom: 10px" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="t('view.player_list.photon.previous')">
|
||||
</template>
|
||||
<template #previous>
|
||||
<DataTableLayout
|
||||
class="min-w-0 w-full"
|
||||
:table="previousTable"
|
||||
@@ -58,8 +58,8 @@
|
||||
:total-items="previousTotal"
|
||||
:on-page-size-change="handlePreviousPageSizeChange"
|
||||
style="margin-bottom: 10px" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</template>
|
||||
</TabsUnderline>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { DataTableLayout } from '@/components/ui/data-table';
|
||||
import { InputGroupField } from '@/components/ui/input-group';
|
||||
import { TabsUnderline } from '@/components/ui/tabs';
|
||||
import { localeIncludes } from '@/shared/utils';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
@@ -91,6 +92,10 @@
|
||||
const { t } = useI18n();
|
||||
|
||||
const photonStore = usePhotonStore();
|
||||
const photonTabs = computed(() => [
|
||||
{ value: 'current', label: t('view.player_list.photon.current') },
|
||||
{ value: 'previous', label: t('view.player_list.photon.previous') }
|
||||
]);
|
||||
const {
|
||||
photonEventTableTypeFilter,
|
||||
photonEventTableFilter,
|
||||
|
||||
@@ -14,329 +14,341 @@
|
||||
/></Button>
|
||||
</TooltipWrapper>
|
||||
</div>
|
||||
<el-tabs ref="searchTabRef" style="margin-top: 15px" @tab-click="searchText = ''">
|
||||
<el-tab-pane v-loading="isSearchUserLoading" :label="t('view.search.user.header')" style="min-height: 60px">
|
||||
<label class="inline-flex items-center gap-2" style="margin-left: 10px">
|
||||
<Checkbox v-model="searchUserByBio" />
|
||||
<span>{{ t('view.search.user.search_by_bio') }}</span>
|
||||
</label>
|
||||
<label class="inline-flex items-center gap-2" style="margin-left: 10px">
|
||||
<Checkbox v-model="searchUserSortByLastLoggedIn" />
|
||||
<span>{{ t('view.search.user.sort_by_last_logged_in') }}</span>
|
||||
</label>
|
||||
<div class="x-friend-list" style="min-height: 500px">
|
||||
<div
|
||||
v-for="user in searchUserResults"
|
||||
:key="user.id"
|
||||
class="x-friend-item"
|
||||
@click="showUserDialog(user.id)">
|
||||
<div class="avatar">
|
||||
<img :src="userImage(user, true)" loading="lazy" />
|
||||
</div>
|
||||
<div class="detail">
|
||||
<span class="name" v-text="user.displayName"></span>
|
||||
<span
|
||||
v-if="randomUserColours"
|
||||
class="extra"
|
||||
:class="user.$trustClass"
|
||||
v-text="user.$trustLevel"></span>
|
||||
<span
|
||||
v-else
|
||||
class="extra"
|
||||
:style="{ color: user.$userColour }"
|
||||
v-text="user.$trustLevel"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ButtonGroup v-if="searchUserResults.length" style="margin-top: 15px">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
:disabled="!searchUserParams.offset"
|
||||
@click="handleMoreSearchUser(-1)">
|
||||
<Back />
|
||||
{{ t('view.search.prev_page') }}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
:disabled="searchUserResults.length < 10"
|
||||
@click="handleMoreSearchUser(1)">
|
||||
<Right />
|
||||
{{ t('view.search.next_page') }}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane
|
||||
v-loading="isSearchWorldLoading"
|
||||
:label="t('view.search.world.header')"
|
||||
style="min-height: 60px">
|
||||
<div class="inline-flex justify-between mb-4 w-full">
|
||||
<Select
|
||||
:model-value="searchWorldCategoryIndex"
|
||||
@update:modelValue="handleSearchWorldCategorySelect"
|
||||
style="margin-bottom: 15px">
|
||||
<SelectTrigger size="sm">
|
||||
<SelectValue :placeholder="t('view.search.world.category')" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem
|
||||
v-for="row in cachedConfig.dynamicWorldRows"
|
||||
:key="row.index"
|
||||
:value="row.index">
|
||||
{{ row.name }}
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<TabsUnderline
|
||||
v-model="activeSearchTab"
|
||||
:items="searchTabs"
|
||||
aria-label="Search tabs"
|
||||
:unmount-on-hide="false"
|
||||
style="margin-top: 15px">
|
||||
<template #user>
|
||||
<div v-loading="isSearchUserLoading" style="min-height: 60px">
|
||||
<label class="inline-flex items-center gap-2" style="margin-left: 10px">
|
||||
<Checkbox v-model="searchWorldLabs" />
|
||||
<span>{{ t('view.search.world.community_lab') }}</span>
|
||||
<Checkbox v-model="searchUserByBio" />
|
||||
<span>{{ t('view.search.user.search_by_bio') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="x-friend-list" style="min-height: 500px">
|
||||
<div
|
||||
v-for="world in searchWorldResults"
|
||||
:key="world.id"
|
||||
class="x-friend-item"
|
||||
@click="showWorldDialog(world.id)">
|
||||
<div class="avatar">
|
||||
<img :src="world.thumbnailImageUrl" loading="lazy" />
|
||||
</div>
|
||||
<div class="detail">
|
||||
<span class="name" v-text="world.name"></span>
|
||||
<span v-if="world.occupants" class="extra"
|
||||
>{{ world.authorName }} ({{ world.occupants }})</span
|
||||
>
|
||||
<span v-else class="extra" v-text="world.authorName"></span>
|
||||
<label class="inline-flex items-center gap-2" style="margin-left: 10px">
|
||||
<Checkbox v-model="searchUserSortByLastLoggedIn" />
|
||||
<span>{{ t('view.search.user.sort_by_last_logged_in') }}</span>
|
||||
</label>
|
||||
<div class="x-friend-list" style="min-height: 500px">
|
||||
<div
|
||||
v-for="user in searchUserResults"
|
||||
:key="user.id"
|
||||
class="x-friend-item"
|
||||
@click="showUserDialog(user.id)">
|
||||
<div class="avatar">
|
||||
<img :src="userImage(user, true)" loading="lazy" />
|
||||
</div>
|
||||
<div class="detail">
|
||||
<span class="name" v-text="user.displayName"></span>
|
||||
<span
|
||||
v-if="randomUserColours"
|
||||
class="extra"
|
||||
:class="user.$trustClass"
|
||||
v-text="user.$trustLevel"></span>
|
||||
<span
|
||||
v-else
|
||||
class="extra"
|
||||
:style="{ color: user.$userColour }"
|
||||
v-text="user.$trustLevel"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ButtonGroup v-if="searchUserResults.length" style="margin-top: 15px">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
:disabled="!searchUserParams.offset"
|
||||
@click="handleMoreSearchUser(-1)">
|
||||
<Back />
|
||||
{{ t('view.search.prev_page') }}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
:disabled="searchUserResults.length < 10"
|
||||
@click="handleMoreSearchUser(1)">
|
||||
<Right />
|
||||
{{ t('view.search.next_page') }}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
<ButtonGroup v-if="searchWorldResults.length" style="margin-top: 15px">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
:disabled="!searchWorldParams.offset"
|
||||
@click="moreSearchWorld(-1)">
|
||||
<Back />
|
||||
{{ t('view.search.prev_page') }}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
:disabled="searchWorldResults.length < 10"
|
||||
@click="moreSearchWorld(1)">
|
||||
<Right />
|
||||
{{ t('view.search.next_page') }}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane
|
||||
v-loading="isSearchAvatarLoading"
|
||||
:label="t('view.search.avatar.header')"
|
||||
style="min-height: 60px">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between">
|
||||
<div style="display: flex; align-items: center">
|
||||
</template>
|
||||
<template #world>
|
||||
<div v-loading="isSearchWorldLoading" style="min-height: 60px">
|
||||
<div class="inline-flex justify-between mb-4 w-full">
|
||||
<Select
|
||||
v-if="avatarRemoteDatabaseProviderList.length > 1"
|
||||
:model-value="avatarRemoteDatabaseProvider"
|
||||
@update:modelValue="setAvatarProvider"
|
||||
style="margin-right: 5px">
|
||||
:model-value="searchWorldCategoryIndex"
|
||||
@update:modelValue="handleSearchWorldCategorySelect"
|
||||
style="margin-bottom: 15px">
|
||||
<SelectTrigger size="sm">
|
||||
<SelectValue :placeholder="t('view.search.avatar.search_provider')" />
|
||||
<SelectValue :placeholder="t('view.search.world.category')" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem
|
||||
v-for="provider in avatarRemoteDatabaseProviderList"
|
||||
:key="provider"
|
||||
:value="provider">
|
||||
{{ provider }}
|
||||
v-for="row in cachedConfig.dynamicWorldRows"
|
||||
:key="row.index"
|
||||
:value="row.index">
|
||||
{{ row.name }}
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<TooltipWrapper side="bottom" :content="t('view.search.avatar.refresh_tooltip')">
|
||||
<Button
|
||||
class="rounded-full ml-1"
|
||||
variant="outline"
|
||||
size="icon-sm"
|
||||
:disabled="userDialog.isAvatarsLoading"
|
||||
@click="refreshUserDialogAvatars">
|
||||
<Spinner v-if="userDialog.isAvatarsLoading" />
|
||||
<Refresh v-else />
|
||||
</Button>
|
||||
</TooltipWrapper>
|
||||
<span style="font-size: 14px; margin-left: 5px; margin-right: 5px">{{
|
||||
t('view.search.avatar.result_count', {
|
||||
count: searchAvatarResults.length
|
||||
})
|
||||
}}</span>
|
||||
<label class="inline-flex items-center gap-2" style="margin-left: 10px">
|
||||
<Checkbox v-model="searchWorldLabs" />
|
||||
<span>{{ t('view.search.world.community_lab') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center">
|
||||
<RadioGroup
|
||||
:model-value="searchAvatarFilter"
|
||||
class="flex items-center gap-4"
|
||||
style="margin: 5px"
|
||||
@update:modelValue="handleSearchAvatarFilterChange">
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroupItem id="searchAvatarFilter-all" value="all" />
|
||||
<label for="searchAvatarFilter-all">{{ t('view.search.avatar.all') }}</label>
|
||||
<div class="x-friend-list" style="min-height: 500px">
|
||||
<div
|
||||
v-for="world in searchWorldResults"
|
||||
:key="world.id"
|
||||
class="x-friend-item"
|
||||
@click="showWorldDialog(world.id)">
|
||||
<div class="avatar">
|
||||
<img :src="world.thumbnailImageUrl" loading="lazy" />
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroupItem id="searchAvatarFilter-public" value="public" />
|
||||
<label for="searchAvatarFilter-public">{{ t('view.search.avatar.public') }}</label>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroupItem id="searchAvatarFilter-private" value="private" />
|
||||
<label for="searchAvatarFilter-private">{{ t('view.search.avatar.private') }}</label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
<el-divider direction="vertical"></el-divider>
|
||||
<RadioGroup
|
||||
:model-value="searchAvatarFilterRemote"
|
||||
class="flex items-center gap-4"
|
||||
style="margin: 5px"
|
||||
@update:modelValue="handleSearchAvatarFilterRemoteChange">
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroupItem id="searchAvatarFilterRemote-all" value="all" />
|
||||
<label for="searchAvatarFilterRemote-all">{{ t('view.search.avatar.all') }}</label>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroupItem id="searchAvatarFilterRemote-local" value="local" />
|
||||
<label for="searchAvatarFilterRemote-local">{{ t('view.search.avatar.local') }}</label>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroupItem
|
||||
id="searchAvatarFilterRemote-remote"
|
||||
value="remote"
|
||||
:disabled="!avatarRemoteDatabase" />
|
||||
<label for="searchAvatarFilterRemote-remote">{{
|
||||
t('view.search.avatar.remote')
|
||||
}}</label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: end">
|
||||
<RadioGroup
|
||||
:model-value="searchAvatarSort"
|
||||
:disabled="searchAvatarFilterRemote !== 'local'"
|
||||
class="flex items-center gap-4"
|
||||
style="margin: 5px"
|
||||
@update:modelValue="handleSearchAvatarSortChange">
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroupItem id="searchAvatarSort-name" value="name" />
|
||||
<label for="searchAvatarSort-name">{{ t('view.search.avatar.sort_name') }}</label>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroupItem id="searchAvatarSort-update" value="update" />
|
||||
<label for="searchAvatarSort-update">{{ t('view.search.avatar.sort_update') }}</label>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroupItem id="searchAvatarSort-created" value="created" />
|
||||
<label for="searchAvatarSort-created">{{ t('view.search.avatar.sort_created') }}</label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
<div class="x-friend-list" style="margin-top: 20px; min-height: 500px">
|
||||
<div
|
||||
v-for="avatar in searchAvatarPage"
|
||||
:key="avatar.id"
|
||||
class="x-friend-item"
|
||||
@click="showAvatarDialog(avatar.id)">
|
||||
<div class="avatar">
|
||||
<img v-if="avatar.thumbnailImageUrl" :src="avatar.thumbnailImageUrl" loading="lazy" />
|
||||
<img v-else-if="avatar.imageUrl" :src="avatar.imageUrl" loading="lazy" />
|
||||
</div>
|
||||
<div class="detail">
|
||||
<span class="name" v-text="avatar.name"></span>
|
||||
<span
|
||||
v-if="avatar.releaseStatus === 'public'"
|
||||
class="extra"
|
||||
style="color: var(--el-color-success)"
|
||||
v-text="avatar.releaseStatus"></span>
|
||||
<span
|
||||
v-else-if="avatar.releaseStatus === 'private'"
|
||||
class="extra"
|
||||
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="avatar.authorName"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ButtonGroup v-if="searchAvatarPage.length" style="margin-top: 15px">
|
||||
<Button variant="outline" size="sm" :disabled="!searchAvatarPageNum" @click="moreSearchAvatar(-1)">
|
||||
<Back />
|
||||
{{ t('view.search.prev_page') }}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
:disabled="
|
||||
searchAvatarResults.length < 10 ||
|
||||
(searchAvatarPageNum + 1) * 10 >= searchAvatarResults.length
|
||||
"
|
||||
@click="moreSearchAvatar(1)">
|
||||
<Right />
|
||||
{{ t('view.search.next_page') }}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane
|
||||
v-loading="isSearchGroupLoading"
|
||||
:label="t('view.search.group.header')"
|
||||
style="min-height: 60px">
|
||||
<div class="x-friend-list" style="min-height: 500px">
|
||||
<div
|
||||
v-for="group in searchGroupResults"
|
||||
:key="group.id"
|
||||
class="x-friend-item"
|
||||
@click="showGroupDialog(group.id)">
|
||||
<div class="avatar">
|
||||
<img :src="getSmallThumbnailUrl(group.iconUrl)" loading="lazy" />
|
||||
</div>
|
||||
<div class="detail">
|
||||
<span class="name">
|
||||
<span v-text="group.name"></span>
|
||||
<span style="margin-left: 5px; font-weight: normal">({{ group.memberCount }})</span>
|
||||
<span
|
||||
style="
|
||||
margin-left: 5px;
|
||||
color: #909399;
|
||||
font-weight: normal;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
"
|
||||
>{{ group.shortCode }}.{{ group.discriminator }}</span
|
||||
<div class="detail">
|
||||
<span class="name" v-text="world.name"></span>
|
||||
<span v-if="world.occupants" class="extra"
|
||||
>{{ world.authorName }} ({{ world.occupants }})</span
|
||||
>
|
||||
</span>
|
||||
<span class="extra" v-text="group.description"></span>
|
||||
<span v-else class="extra" v-text="world.authorName"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ButtonGroup v-if="searchWorldResults.length" style="margin-top: 15px">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
:disabled="!searchWorldParams.offset"
|
||||
@click="moreSearchWorld(-1)">
|
||||
<Back />
|
||||
{{ t('view.search.prev_page') }}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
:disabled="searchWorldResults.length < 10"
|
||||
@click="moreSearchWorld(1)">
|
||||
<Right />
|
||||
{{ t('view.search.next_page') }}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
<ButtonGroup v-if="searchGroupResults.length" style="margin-top: 15px">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
:disabled="!searchGroupParams.offset"
|
||||
@click="moreSearchGroup(-1)">
|
||||
<Back />
|
||||
{{ t('view.search.prev_page') }}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
:disabled="searchGroupResults.length < 10"
|
||||
@click="moreSearchGroup(1)">
|
||||
<Right />
|
||||
{{ t('view.search.next_page') }}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</template>
|
||||
<template #avatar>
|
||||
<div v-loading="isSearchAvatarLoading" style="min-height: 60px">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between">
|
||||
<div style="display: flex; align-items: center">
|
||||
<Select
|
||||
v-if="avatarRemoteDatabaseProviderList.length > 1"
|
||||
:model-value="avatarRemoteDatabaseProvider"
|
||||
@update:modelValue="setAvatarProvider"
|
||||
style="margin-right: 5px">
|
||||
<SelectTrigger size="sm">
|
||||
<SelectValue :placeholder="t('view.search.avatar.search_provider')" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem
|
||||
v-for="provider in avatarRemoteDatabaseProviderList"
|
||||
:key="provider"
|
||||
:value="provider">
|
||||
{{ provider }}
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<TooltipWrapper side="bottom" :content="t('view.search.avatar.refresh_tooltip')">
|
||||
<Button
|
||||
class="rounded-full ml-1"
|
||||
variant="outline"
|
||||
size="icon-sm"
|
||||
:disabled="userDialog.isAvatarsLoading"
|
||||
@click="refreshUserDialogAvatars">
|
||||
<Spinner v-if="userDialog.isAvatarsLoading" />
|
||||
<Refresh v-else />
|
||||
</Button>
|
||||
</TooltipWrapper>
|
||||
<span style="font-size: 14px; margin-left: 5px; margin-right: 5px">{{
|
||||
t('view.search.avatar.result_count', {
|
||||
count: searchAvatarResults.length
|
||||
})
|
||||
}}</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center">
|
||||
<RadioGroup
|
||||
:model-value="searchAvatarFilter"
|
||||
class="flex items-center gap-4"
|
||||
style="margin: 5px"
|
||||
@update:modelValue="handleSearchAvatarFilterChange">
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroupItem id="searchAvatarFilter-all" value="all" />
|
||||
<label for="searchAvatarFilter-all">{{ t('view.search.avatar.all') }}</label>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroupItem id="searchAvatarFilter-public" value="public" />
|
||||
<label for="searchAvatarFilter-public">{{ t('view.search.avatar.public') }}</label>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroupItem id="searchAvatarFilter-private" value="private" />
|
||||
<label for="searchAvatarFilter-private">{{
|
||||
t('view.search.avatar.private')
|
||||
}}</label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
<el-divider direction="vertical"></el-divider>
|
||||
<RadioGroup
|
||||
:model-value="searchAvatarFilterRemote"
|
||||
class="flex items-center gap-4"
|
||||
style="margin: 5px"
|
||||
@update:modelValue="handleSearchAvatarFilterRemoteChange">
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroupItem id="searchAvatarFilterRemote-all" value="all" />
|
||||
<label for="searchAvatarFilterRemote-all">{{ t('view.search.avatar.all') }}</label>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroupItem id="searchAvatarFilterRemote-local" value="local" />
|
||||
<label for="searchAvatarFilterRemote-local">{{
|
||||
t('view.search.avatar.local')
|
||||
}}</label>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroupItem
|
||||
id="searchAvatarFilterRemote-remote"
|
||||
value="remote"
|
||||
:disabled="!avatarRemoteDatabase" />
|
||||
<label for="searchAvatarFilterRemote-remote">{{
|
||||
t('view.search.avatar.remote')
|
||||
}}</label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: end">
|
||||
<RadioGroup
|
||||
:model-value="searchAvatarSort"
|
||||
:disabled="searchAvatarFilterRemote !== 'local'"
|
||||
class="flex items-center gap-4"
|
||||
style="margin: 5px"
|
||||
@update:modelValue="handleSearchAvatarSortChange">
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroupItem id="searchAvatarSort-name" value="name" />
|
||||
<label for="searchAvatarSort-name">{{ t('view.search.avatar.sort_name') }}</label>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroupItem id="searchAvatarSort-update" value="update" />
|
||||
<label for="searchAvatarSort-update">{{ t('view.search.avatar.sort_update') }}</label>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroupItem id="searchAvatarSort-created" value="created" />
|
||||
<label for="searchAvatarSort-created">{{ t('view.search.avatar.sort_created') }}</label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
<div class="x-friend-list" style="margin-top: 20px; min-height: 500px">
|
||||
<div
|
||||
v-for="avatar in searchAvatarPage"
|
||||
:key="avatar.id"
|
||||
class="x-friend-item"
|
||||
@click="showAvatarDialog(avatar.id)">
|
||||
<div class="avatar">
|
||||
<img v-if="avatar.thumbnailImageUrl" :src="avatar.thumbnailImageUrl" loading="lazy" />
|
||||
<img v-else-if="avatar.imageUrl" :src="avatar.imageUrl" loading="lazy" />
|
||||
</div>
|
||||
<div class="detail">
|
||||
<span class="name" v-text="avatar.name"></span>
|
||||
<span
|
||||
v-if="avatar.releaseStatus === 'public'"
|
||||
class="extra"
|
||||
style="color: var(--el-color-success)"
|
||||
v-text="avatar.releaseStatus"></span>
|
||||
<span
|
||||
v-else-if="avatar.releaseStatus === 'private'"
|
||||
class="extra"
|
||||
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="avatar.authorName"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ButtonGroup v-if="searchAvatarPage.length" style="margin-top: 15px">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
:disabled="!searchAvatarPageNum"
|
||||
@click="moreSearchAvatar(-1)">
|
||||
<Back />
|
||||
{{ t('view.search.prev_page') }}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
:disabled="
|
||||
searchAvatarResults.length < 10 ||
|
||||
(searchAvatarPageNum + 1) * 10 >= searchAvatarResults.length
|
||||
"
|
||||
@click="moreSearchAvatar(1)">
|
||||
<Right />
|
||||
{{ t('view.search.next_page') }}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</template>
|
||||
<template #group>
|
||||
<div v-loading="isSearchGroupLoading" style="min-height: 60px">
|
||||
<div class="x-friend-list" style="min-height: 500px">
|
||||
<div
|
||||
v-for="group in searchGroupResults"
|
||||
:key="group.id"
|
||||
class="x-friend-item"
|
||||
@click="showGroupDialog(group.id)">
|
||||
<div class="avatar">
|
||||
<img :src="getSmallThumbnailUrl(group.iconUrl)" loading="lazy" />
|
||||
</div>
|
||||
<div class="detail">
|
||||
<span class="name">
|
||||
<span v-text="group.name"></span>
|
||||
<span style="margin-left: 5px; font-weight: normal">({{ group.memberCount }})</span>
|
||||
<span
|
||||
style="
|
||||
margin-left: 5px;
|
||||
color: #909399;
|
||||
font-weight: normal;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
"
|
||||
>{{ group.shortCode }}.{{ group.discriminator }}</span
|
||||
>
|
||||
</span>
|
||||
<span class="extra" v-text="group.description"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ButtonGroup v-if="searchGroupResults.length" style="margin-top: 15px">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
:disabled="!searchGroupParams.offset"
|
||||
@click="moreSearchGroup(-1)">
|
||||
<Back />
|
||||
{{ t('view.search.prev_page') }}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
:disabled="searchGroupResults.length < 10"
|
||||
@click="moreSearchGroup(1)">
|
||||
<Right />
|
||||
{{ t('view.search.next_page') }}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</template>
|
||||
</TabsUnderline>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -344,13 +356,14 @@
|
||||
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Back, Refresh, Right } from '@element-plus/icons-vue';
|
||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
||||
import { computed, ref } from 'vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ButtonGroup } from '@/components/ui/button-group';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { InputGroupField } from '@/components/ui/input-group';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
import { TabsUnderline } from '@/components/ui/tabs';
|
||||
import { Trash2 } from 'lucide-vue-next';
|
||||
import { ref } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
@@ -390,7 +403,13 @@
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const searchTabRef = ref(null);
|
||||
const activeSearchTab = ref('user');
|
||||
const searchTabs = computed(() => [
|
||||
{ value: 'user', label: t('view.search.user.header') },
|
||||
{ value: 'world', label: t('view.search.world.header') },
|
||||
{ value: 'avatar', label: t('view.search.avatar.header') },
|
||||
{ value: 'group', label: t('view.search.group.header') }
|
||||
]);
|
||||
|
||||
const searchUserParams = ref({});
|
||||
const searchUserByBio = ref(false);
|
||||
@@ -453,18 +472,23 @@
|
||||
searchText.value = text;
|
||||
}
|
||||
|
||||
function handleSearchTabChange(tabName) {
|
||||
searchText.value = '';
|
||||
activeSearchTab.value = tabName;
|
||||
}
|
||||
|
||||
function search() {
|
||||
switch (searchTabRef.value.currentName) {
|
||||
case '0':
|
||||
switch (activeSearchTab.value) {
|
||||
case 'user':
|
||||
searchUser();
|
||||
break;
|
||||
case '1':
|
||||
case 'world':
|
||||
searchWorld({});
|
||||
break;
|
||||
case '2':
|
||||
case 'avatar':
|
||||
searchAvatar();
|
||||
break;
|
||||
case '3':
|
||||
case 'group':
|
||||
searchGroup();
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -3,34 +3,39 @@
|
||||
<div class="options-container" style="margin-top: 0; padding: 5px">
|
||||
<span class="header">{{ t('view.settings.header') }}</span>
|
||||
</div>
|
||||
<el-tabs style="height: calc(100% - 51px)">
|
||||
<el-tab-pane :label="t('view.settings.category.general')">
|
||||
<TabsUnderline
|
||||
default-value="general"
|
||||
:items="settingsTabs"
|
||||
:unmount-on-hide="false"
|
||||
style="height: calc(100% - 51px)">
|
||||
<template #general>
|
||||
<GeneralTab />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane lazy :label="t('view.settings.category.appearance')">
|
||||
</template>
|
||||
<template #appearance>
|
||||
<AppearanceTab />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane lazy :label="t('view.settings.category.notifications')">
|
||||
</template>
|
||||
<template #notifications>
|
||||
<NotificationsTab />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane lazy :label="t('view.settings.category.wrist_overlay')">
|
||||
</template>
|
||||
<template #wrist-overlay>
|
||||
<WristOverlayTab />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane lazy :label="t('view.settings.category.discord_presence')">
|
||||
</template>
|
||||
<template #discord>
|
||||
<DiscordPresenceTab />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane lazy :label="t('view.settings.category.pictures')">
|
||||
</template>
|
||||
<template #pictures>
|
||||
<PicturesTab />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane lazy :label="t('view.settings.category.advanced')">
|
||||
</template>
|
||||
<template #advanced>
|
||||
<AdvancedTab />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</template>
|
||||
</TabsUnderline>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onBeforeMount } from 'vue';
|
||||
import { computed, onBeforeMount } from 'vue';
|
||||
import { TabsUnderline } from '@/components/ui/tabs';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import AdvancedTab from './components/Tabs/AdvancedTab.vue';
|
||||
@@ -42,6 +47,15 @@
|
||||
import WristOverlayTab from './components/Tabs/WristOverlayTab.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const settingsTabs = computed(() => [
|
||||
{ value: 'general', label: t('view.settings.category.general') },
|
||||
{ value: 'appearance', label: t('view.settings.category.appearance') },
|
||||
{ value: 'notifications', label: t('view.settings.category.notifications') },
|
||||
{ value: 'wrist-overlay', label: t('view.settings.category.wrist_overlay') },
|
||||
{ value: 'discord', label: t('view.settings.category.discord_presence') },
|
||||
{ value: 'pictures', label: t('view.settings.category.pictures') },
|
||||
{ value: 'advanced', label: t('view.settings.category.advanced') }
|
||||
]);
|
||||
|
||||
onBeforeMount(() => {
|
||||
const menuItem = document.querySelector('li[role="menuitem"].is-active');
|
||||
|
||||
@@ -73,33 +73,44 @@
|
||||
</TooltipWrapper>
|
||||
</div>
|
||||
</div>
|
||||
<el-tabs class="zero-margin-tabs" stretch style="height: calc(100% - 70px); margin-top: 5px">
|
||||
<el-tab-pane>
|
||||
<template #label>
|
||||
<span>{{ t('side_panel.friends') }}</span>
|
||||
<span class="sidebar-tab-count"> ({{ onlineFriendCount }}/{{ friends.size }}) </span>
|
||||
</template>
|
||||
<el-backtop target=".zero-margin-tabs .el-tabs__content" :bottom="20" :right="20"></el-backtop>
|
||||
<FriendsSidebar @confirm-delete-friend="confirmDeleteFriend" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane lazy>
|
||||
<template #label>
|
||||
<span>{{ t('side_panel.groups') }}</span>
|
||||
<span class="sidebar-tab-count"> ({{ groupInstances.length }}) </span>
|
||||
</template>
|
||||
<GroupsSidebar :group-instances="groupInstances" :group-order="inGameGroupOrder" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<TabsUnderline
|
||||
default-value="friends"
|
||||
:items="sidebarTabs"
|
||||
:unmount-on-hide="false"
|
||||
variant="equal"
|
||||
class="zero-margin-tabs"
|
||||
style="height: calc(100% - 70px); margin-top: 5px">
|
||||
<template #label-friends>
|
||||
<span>{{ t('side_panel.friends') }}</span>
|
||||
<span class="sidebar-tab-count"> ({{ onlineFriendCount }}/{{ friends.size }}) </span>
|
||||
</template>
|
||||
<template #label-groups>
|
||||
<span>{{ t('side_panel.groups') }}</span>
|
||||
<span class="sidebar-tab-count"> ({{ groupInstances.length }}) </span>
|
||||
</template>
|
||||
<template #friends>
|
||||
<div class="el-tabs__content">
|
||||
<el-backtop target=".zero-margin-tabs .el-tabs__content" :bottom="20" :right="20"></el-backtop>
|
||||
<FriendsSidebar @confirm-delete-friend="confirmDeleteFriend" />
|
||||
</div>
|
||||
</template>
|
||||
<template #groups>
|
||||
<div class="el-tabs__content">
|
||||
<GroupsSidebar :group-instances="groupInstances" :group-order="inGameGroupOrder" />
|
||||
</div>
|
||||
</template>
|
||||
</TabsUnderline>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import { ref, watch } from 'vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Refresh } from '@element-plus/icons-vue';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
import { TabsUnderline } from '@/components/ui/tabs';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
@@ -115,6 +126,10 @@
|
||||
const { quickSearchItems } = storeToRefs(useSearchStore());
|
||||
const { inGameGroupOrder, groupInstances } = storeToRefs(useGroupStore());
|
||||
const { t } = useI18n();
|
||||
const sidebarTabs = computed(() => [
|
||||
{ value: 'friends', label: t('side_panel.friends') },
|
||||
{ value: 'groups', label: t('side_panel.groups') }
|
||||
]);
|
||||
|
||||
const quickSearchQuery = ref('');
|
||||
const isQuickSearchOpen = ref(false);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,40 +5,40 @@
|
||||
:title="t('dialog.edit_invite_messages.header')"
|
||||
width="1000px"
|
||||
@close="closeDialog">
|
||||
<el-tabs v-model="activeTab" style="margin-top: 10px">
|
||||
<el-tab-pane :label="t('dialog.edit_invite_messages.invite_message_tab')" name="message">
|
||||
<TabsUnderline v-model="activeTab" :items="editInviteTabs" :unmount-on-hide="false" class="mt-2.5">
|
||||
<template #message>
|
||||
<DataTableLayout
|
||||
style="margin-top: 10px; cursor: pointer"
|
||||
:table="inviteMessageTanstackTable"
|
||||
:loading="false"
|
||||
:show-pagination="false"
|
||||
:on-row-click="handleEditInviteMessageRowClick" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="t('dialog.edit_invite_messages.invite_request_tab')" name="request">
|
||||
</template>
|
||||
<template #request>
|
||||
<DataTableLayout
|
||||
style="margin-top: 10px; cursor: pointer"
|
||||
:table="inviteRequestTanstackTable"
|
||||
:loading="false"
|
||||
:show-pagination="false"
|
||||
:on-row-click="handleEditInviteMessageRowClick" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="t('dialog.edit_invite_messages.invite_request_response_tab')" name="requestResponse">
|
||||
</template>
|
||||
<template #requestResponse>
|
||||
<DataTableLayout
|
||||
style="margin-top: 10px; cursor: pointer"
|
||||
:table="inviteRequestResponseTanstackTable"
|
||||
:loading="false"
|
||||
:show-pagination="false"
|
||||
:on-row-click="handleEditInviteMessageRowClick" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="t('dialog.edit_invite_messages.invite_response_tab')" name="response">
|
||||
</template>
|
||||
<template #response>
|
||||
<DataTableLayout
|
||||
style="margin-top: 10px; cursor: pointer"
|
||||
:table="inviteResponseTanstackTable"
|
||||
:loading="false"
|
||||
:show-pagination="false"
|
||||
:on-row-click="handleEditInviteMessageRowClick" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</template>
|
||||
</TabsUnderline>
|
||||
</el-dialog>
|
||||
<template v-if="isEditInviteMessagesDialogVisible">
|
||||
<EditInviteMessageDialog
|
||||
@@ -52,6 +52,7 @@
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { DataTableLayout } from '@/components/ui/data-table';
|
||||
import { TabsUnderline } from '@/components/ui/tabs';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { toast } from 'vue-sonner';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
@@ -82,6 +83,12 @@
|
||||
});
|
||||
|
||||
const activeTab = ref('message');
|
||||
const editInviteTabs = computed(() => [
|
||||
{ value: 'message', label: t('dialog.edit_invite_messages.invite_message_tab') },
|
||||
{ value: 'request', label: t('dialog.edit_invite_messages.invite_request_tab') },
|
||||
{ value: 'requestResponse', label: t('dialog.edit_invite_messages.invite_request_response_tab') },
|
||||
{ value: 'response', label: t('dialog.edit_invite_messages.invite_response_tab') }
|
||||
]);
|
||||
|
||||
const isEditInviteMessageDialogVisible = ref(false);
|
||||
const inviteMessage = ref({});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<el-dialog :title="t('dialog.export_friends_list.header')" v-model="isVisible" width="650px">
|
||||
<el-tabs>
|
||||
<el-tab-pane :label="t('dialog.export_friends_list.csv')">
|
||||
<TabsUnderline default-value="csv" :items="exportFriendsTabs" :unmount-on-hide="false" class="mt-2.5">
|
||||
<template #csv>
|
||||
<InputGroupTextareaField
|
||||
v-model="exportFriendsListCsv"
|
||||
:rows="15"
|
||||
@@ -9,8 +9,8 @@
|
||||
style="margin-top: 15px"
|
||||
input-class="resize-none"
|
||||
@click="$event.target.tagName === 'TEXTAREA' && $event.target.select()" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="t('dialog.export_friends_list.json')">
|
||||
</template>
|
||||
<template #json>
|
||||
<InputGroupTextareaField
|
||||
v-model="exportFriendsListJson"
|
||||
:rows="15"
|
||||
@@ -18,15 +18,16 @@
|
||||
style="margin-top: 15px"
|
||||
input-class="resize-none"
|
||||
@click="$event.target.tagName === 'TEXTAREA' && $event.target.select()" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</template>
|
||||
</TabsUnderline>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { InputGroupTextareaField } from '@/components/ui/input-group';
|
||||
import { TabsUnderline } from '@/components/ui/tabs';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import { useUserStore } from '../../../stores';
|
||||
@@ -49,6 +50,10 @@
|
||||
|
||||
const exportFriendsListCsv = ref('');
|
||||
const exportFriendsListJson = ref('');
|
||||
const exportFriendsTabs = computed(() => [
|
||||
{ value: 'csv', label: t('dialog.export_friends_list.csv') },
|
||||
{ value: 'json', label: t('dialog.export_friends_list.json') }
|
||||
]);
|
||||
|
||||
const isVisible = computed({
|
||||
get() {
|
||||
|
||||
Reference in New Issue
Block a user