mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-19 14:53:50 +02:00
wip: rewrite playerlist
This commit is contained in:
@@ -12,7 +12,8 @@
|
|||||||
<TableHead
|
<TableHead
|
||||||
v-for="header in headerGroup.headers"
|
v-for="header in headerGroup.headers"
|
||||||
:key="header.id"
|
:key="header.id"
|
||||||
:class="getHeaderClass(header)">
|
:class="getHeaderClass(header)"
|
||||||
|
:style="getPinnedStyle(header.column, true)">
|
||||||
<FlexRender
|
<FlexRender
|
||||||
v-if="!header.isPlaceholder"
|
v-if="!header.isPlaceholder"
|
||||||
:render="header.column.columnDef.header"
|
:render="header.column.columnDef.header"
|
||||||
@@ -23,11 +24,12 @@
|
|||||||
<TableBody>
|
<TableBody>
|
||||||
<template v-if="table.getRowModel().rows?.length">
|
<template v-if="table.getRowModel().rows?.length">
|
||||||
<template v-for="row in table.getRowModel().rows" :key="row.id">
|
<template v-for="row in table.getRowModel().rows" :key="row.id">
|
||||||
<TableRow>
|
<TableRow @click="handleRowClick(row)">
|
||||||
<TableCell
|
<TableCell
|
||||||
v-for="cell in row.getVisibleCells()"
|
v-for="cell in row.getVisibleCells()"
|
||||||
:key="cell.id"
|
:key="cell.id"
|
||||||
:class="getCellClass(cell)">
|
:class="getCellClass(cell)"
|
||||||
|
:style="getPinnedStyle(cell.column, false)">
|
||||||
<FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" />
|
<FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -56,7 +58,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4 flex w-full items-center gap-3">
|
<div v-if="showPagination" class="mt-4 flex w-full items-center gap-3">
|
||||||
<div v-if="pageSizes.length" class="inline-flex items-center flex-1 justify-end gap-2">
|
<div v-if="pageSizes.length" class="inline-flex items-center flex-1 justify-end gap-2">
|
||||||
<span class="text-xs text-muted-foreground">{{ t('table.pagination.rows_per_page') }}</span>
|
<span class="text-xs text-muted-foreground">{{ t('table.pagination.rows_per_page') }}</span>
|
||||||
<Select v-model="pageSizeValue">
|
<Select v-model="pageSizeValue">
|
||||||
@@ -137,6 +139,10 @@
|
|||||||
type: String,
|
type: String,
|
||||||
default: 'No results.'
|
default: 'No results.'
|
||||||
},
|
},
|
||||||
|
showPagination: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
onPageSizeChange: {
|
onPageSizeChange: {
|
||||||
type: Function,
|
type: Function,
|
||||||
default: null
|
default: null
|
||||||
@@ -144,6 +150,10 @@
|
|||||||
onPageChange: {
|
onPageChange: {
|
||||||
type: Function,
|
type: Function,
|
||||||
default: null
|
default: null
|
||||||
|
},
|
||||||
|
onRowClick: {
|
||||||
|
type: Function,
|
||||||
|
default: null
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -197,7 +207,9 @@
|
|||||||
const getCellClass = (cell) => {
|
const getCellClass = (cell) => {
|
||||||
const columnDef = cell?.column?.columnDef;
|
const columnDef = cell?.column?.columnDef;
|
||||||
const meta = columnDef?.meta ?? {};
|
const meta = columnDef?.meta ?? {};
|
||||||
|
const isPinned = Boolean(cell?.column?.getIsPinned?.());
|
||||||
return joinClasses(
|
return joinClasses(
|
||||||
|
isPinned ? 'bg-background' : null,
|
||||||
resolveClassValue(meta.class, cell?.getContext?.()),
|
resolveClassValue(meta.class, cell?.getContext?.()),
|
||||||
resolveClassValue(meta.cellClass, cell?.getContext?.()),
|
resolveClassValue(meta.cellClass, cell?.getContext?.()),
|
||||||
resolveClassValue(meta.tdClass, cell?.getContext?.()),
|
resolveClassValue(meta.tdClass, cell?.getContext?.()),
|
||||||
@@ -206,6 +218,31 @@
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getPinnedStyle = (column, isHeader) => {
|
||||||
|
const pinned = column?.getIsPinned?.();
|
||||||
|
if (!pinned) return null;
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
position: 'sticky',
|
||||||
|
zIndex: isHeader ? 30 : 20
|
||||||
|
};
|
||||||
|
|
||||||
|
const size = column?.getSize?.();
|
||||||
|
if (Number.isFinite(size)) {
|
||||||
|
style.width = `${size}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pinned === 'left') {
|
||||||
|
const left = column?.getStart?.('left');
|
||||||
|
if (Number.isFinite(left)) style.left = `${left}px`;
|
||||||
|
} else if (pinned === 'right') {
|
||||||
|
const right = column?.getAfter?.('right');
|
||||||
|
if (Number.isFinite(right)) style.right = `${right}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return style;
|
||||||
|
};
|
||||||
|
|
||||||
const handlePageSizeChange = (size) => {
|
const handlePageSizeChange = (size) => {
|
||||||
if (props.onPageSizeChange) {
|
if (props.onPageSizeChange) {
|
||||||
props.onPageSizeChange(size);
|
props.onPageSizeChange(size);
|
||||||
@@ -214,7 +251,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const pageSizeProxy = computed({
|
const pageSizeProxy = computed({
|
||||||
get: () => props.table.getState().pagination.pageSize,
|
get: () => props.table.getState?.().pagination?.pageSize ?? 0,
|
||||||
set: (size) => handlePageSizeChange(size)
|
set: (size) => handlePageSizeChange(size)
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -224,7 +261,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
const currentPage = computed({
|
const currentPage = computed({
|
||||||
get: () => props.table.getState().pagination.pageIndex + 1,
|
get: () => (props.table.getState?.().pagination?.pageIndex ?? 0) + 1,
|
||||||
set: (page) => {
|
set: (page) => {
|
||||||
props.table.setPageIndex(page - 1);
|
props.table.setPageIndex(page - 1);
|
||||||
if (props.onPageChange) {
|
if (props.onPageChange) {
|
||||||
@@ -232,4 +269,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handleRowClick = (row) => {
|
||||||
|
if (!props.onRowClick) return;
|
||||||
|
props.onRowClick(row);
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,13 +1,31 @@
|
|||||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||||
|
|
||||||
export function useDataTableScrollHeight(containerRef, options = {}) {
|
export function useDataTableScrollHeight(containerRef, options = {}) {
|
||||||
const offset = options.offset ?? 127;
|
const {
|
||||||
const toolbarHeight = options.toolbarHeight ?? 0;
|
offset = 127,
|
||||||
const paginationHeight = options.paginationHeight ?? 0;
|
toolbarHeight = 0,
|
||||||
|
paginationHeight = 0,
|
||||||
|
extraOffsetRefs = [],
|
||||||
|
subtractContainerPadding = false
|
||||||
|
} = options;
|
||||||
|
|
||||||
const maxHeight = ref(0);
|
const maxHeight = ref(0);
|
||||||
|
|
||||||
let resizeObserver;
|
let resizeObserver;
|
||||||
|
const observedElements = new Set();
|
||||||
|
|
||||||
|
const getPadding = (el) => {
|
||||||
|
if (!subtractContainerPadding || !el) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const style = getComputedStyle(el);
|
||||||
|
return (Number.parseFloat(style.paddingTop) || 0) + (Number.parseFloat(style.paddingBottom) || 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getHeight = (maybeRef) => {
|
||||||
|
const el = maybeRef?.value;
|
||||||
|
return el && typeof el.getBoundingClientRect === 'function' ? el.getBoundingClientRect().height : 0;
|
||||||
|
};
|
||||||
|
|
||||||
const recalc = () => {
|
const recalc = () => {
|
||||||
const containerEl = containerRef?.value;
|
const containerEl = containerRef?.value;
|
||||||
@@ -15,38 +33,68 @@ export function useDataTableScrollHeight(containerRef, options = {}) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const extraOffset = extraOffsetRefs.reduce((sum, ref) => sum + getHeight(ref), 0);
|
||||||
|
|
||||||
const available =
|
const available =
|
||||||
containerEl.clientHeight -
|
containerEl.clientHeight -
|
||||||
|
getPadding(containerEl) -
|
||||||
offset -
|
offset -
|
||||||
toolbarHeight -
|
toolbarHeight -
|
||||||
paginationHeight;
|
paginationHeight -
|
||||||
|
extraOffset;
|
||||||
|
|
||||||
maxHeight.value = Math.max(0, available);
|
maxHeight.value = Math.max(0, available);
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
const updateObservedElements = () => {
|
||||||
recalc();
|
if (!resizeObserver) {
|
||||||
|
return;
|
||||||
resizeObserver = new ResizeObserver(() => {
|
|
||||||
recalc();
|
|
||||||
});
|
|
||||||
|
|
||||||
if (containerRef?.value) {
|
|
||||||
resizeObserver.observe(containerRef.value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const nextObserved = new Set([
|
||||||
|
containerRef?.value,
|
||||||
|
...extraOffsetRefs.map((ref) => ref?.value)
|
||||||
|
].filter(Boolean));
|
||||||
|
|
||||||
|
for (const el of observedElements) {
|
||||||
|
if (!nextObserved.has(el)) {
|
||||||
|
resizeObserver.unobserve(el);
|
||||||
|
observedElements.delete(el);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const el of nextObserved) {
|
||||||
|
if (!observedElements.has(el)) {
|
||||||
|
resizeObserver.observe(el);
|
||||||
|
observedElements.add(el);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
resizeObserver = new ResizeObserver(recalc);
|
||||||
|
|
||||||
|
updateObservedElements();
|
||||||
|
recalc();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [containerRef?.value, ...extraOffsetRefs.map((r) => r?.value)],
|
||||||
|
() => {
|
||||||
|
updateObservedElements();
|
||||||
|
recalc();
|
||||||
|
},
|
||||||
|
{ flush: 'post' }
|
||||||
|
);
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
resizeObserver?.disconnect();
|
resizeObserver?.disconnect();
|
||||||
|
observedElements.clear();
|
||||||
});
|
});
|
||||||
|
|
||||||
const tableStyle = computed(() => {
|
const tableStyle = computed(() => {
|
||||||
if (!Number.isFinite(maxHeight.value) || maxHeight.value <= 0) {
|
if (!Number.isFinite(maxHeight.value) || maxHeight.value <= 0) return undefined;
|
||||||
return undefined;
|
return { maxHeight: `${maxHeight.value}px` };
|
||||||
}
|
|
||||||
return {
|
|
||||||
maxHeight: `${maxHeight.value}px`
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -84,23 +84,6 @@ export const useInstanceStore = defineStore('Instance', () => {
|
|||||||
|
|
||||||
const currentInstanceUsersData = ref([]);
|
const currentInstanceUsersData = ref([]);
|
||||||
|
|
||||||
const currentInstanceUsersTableProps = reactive({
|
|
||||||
stripe: true,
|
|
||||||
size: 'small',
|
|
||||||
defaultSort: {
|
|
||||||
prop: 'timer',
|
|
||||||
order: 'descending'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const currentInstanceUsersTable = computed(() => {
|
|
||||||
return {
|
|
||||||
data: currentInstanceWorld.value.ref.id
|
|
||||||
? currentInstanceUsersData.value
|
|
||||||
: [],
|
|
||||||
tableProps: currentInstanceUsersTableProps
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => watchState.isLoggedIn,
|
() => watchState.isLoggedIn,
|
||||||
(isLoggedIn) => {
|
(isLoggedIn) => {
|
||||||
@@ -1229,7 +1212,7 @@ export const useInstanceStore = defineStore('Instance', () => {
|
|||||||
previousInstancesInfoDialogVisible,
|
previousInstancesInfoDialogVisible,
|
||||||
previousInstancesInfoDialogInstanceId,
|
previousInstancesInfoDialogInstanceId,
|
||||||
instanceJoinHistory,
|
instanceJoinHistory,
|
||||||
currentInstanceUsersTable,
|
currentInstanceUsersData,
|
||||||
|
|
||||||
applyInstance,
|
applyInstance,
|
||||||
updateCurrentInstanceWorld,
|
updateCurrentInstanceWorld,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<ResizableHandle :disabled="isNavCollapsed" class="opacity-0"></ResizableHandle>
|
<ResizableHandle :disabled="isNavCollapsed" class="opacity-0"></ResizableHandle>
|
||||||
<ResizablePanel :default-size="mainDefaultSize" :order="2">
|
<ResizablePanel :default-size="mainDefaultSize" :order="2">
|
||||||
<RouterView v-slot="{ Component }">
|
<RouterView v-slot="{ Component }">
|
||||||
<KeepAlive include="Feed,GameLog,PlayerList">
|
<KeepAlive include="Feed,GameLog,Search">
|
||||||
<component :is="Component" />
|
<component :is="Component" />
|
||||||
</KeepAlive>
|
</KeepAlive>
|
||||||
</RouterView>
|
</RouterView>
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="x-container" ref="playerListRef">
|
<div class="x-container" ref="playerListRef">
|
||||||
<div style="display: flex; flex-direction: column; height: 100%">
|
<div class="flex h-full min-h-0 flex-col">
|
||||||
<div v-if="currentInstanceWorld.ref.id" style="display: flex; height: 120px">
|
<div
|
||||||
|
v-if="currentInstanceWorld.ref.id"
|
||||||
|
ref="playerListHeaderRef"
|
||||||
|
style="display: flex; height: 120px"
|
||||||
|
class="mb-7">
|
||||||
<img
|
<img
|
||||||
:src="currentInstanceWorld.ref.thumbnailImageUrl"
|
:src="currentInstanceWorld.ref.thumbnailImageUrl"
|
||||||
class="x-link"
|
class="x-link"
|
||||||
@@ -171,217 +175,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="photonLoggingEnabled" style="margin-bottom: 10px">
|
<div v-if="photonLoggingEnabled" ref="playerListPhotonRef" style="margin-bottom: 10px">
|
||||||
<PhotonEventTable @show-chatbox-blacklist="showChatboxBlacklistDialog" />
|
<PhotonEventTable @show-chatbox-blacklist="showChatboxBlacklistDialog" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="current-instance-table">
|
<div class="current-instance-table flex min-h-0 flex-1">
|
||||||
<DataTable
|
<DataTableLayout
|
||||||
v-bind="currentInstanceUsersTable"
|
class="[&_th]:px-2.5! [&_th]:py-0.75! [&_td]:px-2.5! [&_td]:py-0.75! [&_tr]:h-7!"
|
||||||
layout="table"
|
:table="playerListTable"
|
||||||
style="margin-top: 10px; cursor: pointer"
|
:table-style="playerListTableStyle"
|
||||||
@row-click="selectCurrentInstanceRow">
|
:loading="false"
|
||||||
<el-table-column :label="t('table.playerList.avatar')" width="70" prop="photo" fixed>
|
:total-items="playerListTotalItems"
|
||||||
<template #default="scope">
|
:show-pagination="false"
|
||||||
<div v-if="userImage(scope.row.ref)" class="flex items-center pl-2">
|
:on-row-click="handlePlayerListRowClick" />
|
||||||
<img :src="userImage(scope.row.ref)" class="friends-list-avatar" loading="lazy" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column :label="t('table.playerList.timer')" width="90" prop="timer" sortable fixed>
|
|
||||||
<template #default="scope">
|
|
||||||
<Timer :epoch="scope.row.timer" />
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column
|
|
||||||
class="table-user"
|
|
||||||
:label="t('table.playerList.displayName')"
|
|
||||||
width="200"
|
|
||||||
prop="displayName"
|
|
||||||
sortable
|
|
||||||
:sort-method="(a, b) => sortAlphabetically(a, b, 'displayName')"
|
|
||||||
fixed>
|
|
||||||
<template #default="scope">
|
|
||||||
<span
|
|
||||||
v-if="randomUserColours"
|
|
||||||
:style="{ color: scope.row.ref.$userColour }"
|
|
||||||
v-text="scope.row.ref.displayName"></span>
|
|
||||||
<span v-else v-text="scope.row.ref.displayName"></span>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
|
|
||||||
<el-table-column
|
|
||||||
:label="t('table.playerList.rank')"
|
|
||||||
width="110"
|
|
||||||
prop="$trustSortNum"
|
|
||||||
:sortable="true">
|
|
||||||
<template #default="scope">
|
|
||||||
<span
|
|
||||||
class="name"
|
|
||||||
:class="scope.row.ref.$trustClass"
|
|
||||||
v-text="scope.row.ref.$trustLevel"></span>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column :label="t('table.playerList.status')" min-width="200" prop="ref.status">
|
|
||||||
<template #default="scope">
|
|
||||||
<template v-if="scope.row.ref.status">
|
|
||||||
<i
|
|
||||||
class="x-user-status"
|
|
||||||
:class="statusClass(scope.row.ref.status)"
|
|
||||||
style="margin-right: 3px"></i>
|
|
||||||
<span v-text="scope.row.ref.statusDescription"></span>
|
|
||||||
<!--//- el-table-column(label="Group" min-width="180" prop="groupOnNameplate" sortable)-->
|
|
||||||
<!--//- template(v-once #default="scope")-->
|
|
||||||
<!--//- span(v-text="scope.row.groupOnNameplate")-->
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column
|
|
||||||
v-if="photonLoggingEnabled"
|
|
||||||
:label="t('table.playerList.photonId')"
|
|
||||||
width="110"
|
|
||||||
prop="photonId"
|
|
||||||
sortable>
|
|
||||||
<template #default="scope">
|
|
||||||
<template v-if="chatboxUserBlacklist.has(scope.row.ref.id)">
|
|
||||||
<TooltipWrapper side="left" content="Unblock chatbox messages">
|
|
||||||
<el-button
|
|
||||||
text
|
|
||||||
:icon="Mute"
|
|
||||||
size="small"
|
|
||||||
style="color: red; margin-right: 5px"
|
|
||||||
@click.stop="deleteChatboxUserBlacklist(scope.row.ref.id)"></el-button>
|
|
||||||
</TooltipWrapper>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<TooltipWrapper side="left" content="Block chatbox messages">
|
|
||||||
<el-button
|
|
||||||
text
|
|
||||||
:icon="Microphone"
|
|
||||||
size="small"
|
|
||||||
style="margin-right: 5px"
|
|
||||||
@click.stop="addChatboxUserBlacklist(scope.row.ref)"></el-button>
|
|
||||||
</TooltipWrapper>
|
|
||||||
</template>
|
|
||||||
<span v-text="scope.row.photonId"></span>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column
|
|
||||||
:label="t('table.playerList.icon')"
|
|
||||||
prop="isMaster"
|
|
||||||
width="90"
|
|
||||||
align="center"
|
|
||||||
sortable
|
|
||||||
:sort-method="sortInstanceIcon">
|
|
||||||
<template #default="scope">
|
|
||||||
<span></span>
|
|
||||||
<TooltipWrapper v-if="scope.row.isMaster" side="left" content="Instance Master">
|
|
||||||
<span>👑</span>
|
|
||||||
</TooltipWrapper>
|
|
||||||
<TooltipWrapper v-if="scope.row.isModerator" side="left" content="Moderator">
|
|
||||||
<span>⚔️</span>
|
|
||||||
</TooltipWrapper>
|
|
||||||
<TooltipWrapper v-if="scope.row.isFriend" side="left" content="Friend">
|
|
||||||
<span>💚</span>
|
|
||||||
</TooltipWrapper>
|
|
||||||
<TooltipWrapper v-if="scope.row.isBlocked" side="left" content="Blocked">
|
|
||||||
<el-icon style="color: red"><CircleClose /></el-icon>
|
|
||||||
</TooltipWrapper>
|
|
||||||
<TooltipWrapper v-if="scope.row.isMuted" side="left" content="Muted">
|
|
||||||
<el-icon style="color: var(--el-color-warning)"><Mute /></el-icon>
|
|
||||||
</TooltipWrapper>
|
|
||||||
<TooltipWrapper
|
|
||||||
v-if="scope.row.isAvatarInteractionDisabled"
|
|
||||||
side="left"
|
|
||||||
content="Avatar Interaction Disabled
|
|
||||||
">
|
|
||||||
<el-icon style="color: var(--el-color-warning)"><Pointer /></el-icon>
|
|
||||||
</TooltipWrapper>
|
|
||||||
<TooltipWrapper v-if="scope.row.isChatBoxMuted" side="left" content="Chatbox Muted">
|
|
||||||
<el-icon style="color: var(--el-color-warning)"><ChatLineRound /></el-icon>
|
|
||||||
</TooltipWrapper>
|
|
||||||
<TooltipWrapper v-if="scope.row.timeoutTime" side="left" content="Timeout">
|
|
||||||
<span style="color: var(--el-color-danger)">🔴{{ scope.row.timeoutTime }}s</span>
|
|
||||||
</TooltipWrapper>
|
|
||||||
<TooltipWrapper v-if="scope.row.ageVerified" side="left" content="18+ Verified">
|
|
||||||
<i class="ri-id-card-line"></i>
|
|
||||||
</TooltipWrapper>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column :label="t('table.playerList.platform')" prop="inVRMode" width="90">
|
|
||||||
<template #default="scope">
|
|
||||||
<template v-if="scope.row.ref.$platform">
|
|
||||||
<span
|
|
||||||
v-if="scope.row.ref.$platform === 'standalonewindows'"
|
|
||||||
style="color: var(--el-color-primary)"
|
|
||||||
><i class="ri-computer-line"></i
|
|
||||||
></span>
|
|
||||||
<span
|
|
||||||
v-else-if="scope.row.ref.$platform === 'android'"
|
|
||||||
style="color: var(--el-color-success)"
|
|
||||||
><i class="ri-android-line"></i
|
|
||||||
></span>
|
|
||||||
<span v-else-if="scope.row.ref.$platform === 'ios'" style="color: var(--el-color-info)"
|
|
||||||
><i class="ri-apple-line"></i
|
|
||||||
></span>
|
|
||||||
<span v-else>{{ scope.row.ref.$platform }}</span>
|
|
||||||
</template>
|
|
||||||
<template v-if="scope.row.inVRMode !== null">
|
|
||||||
<span v-if="scope.row.inVRMode">VR</span>
|
|
||||||
<span
|
|
||||||
v-else-if="
|
|
||||||
scope.row.ref.last_platform === 'android' ||
|
|
||||||
scope.row.ref.last_platform === 'ios'
|
|
||||||
"
|
|
||||||
>M</span
|
|
||||||
>
|
|
||||||
<span v-else>D</span>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column :label="t('table.playerList.language')" width="100" prop="ref.$languages">
|
|
||||||
<template #default="scope">
|
|
||||||
<TooltipWrapper v-for="item in scope.row.ref.$languages" :key="item.key" side="top">
|
|
||||||
<template #content>
|
|
||||||
<span>{{ item.value }} ({{ item.key }})</span>
|
|
||||||
</template>
|
|
||||||
<span
|
|
||||||
class="flags"
|
|
||||||
:class="languageClass(item.key)"
|
|
||||||
style="display: inline-block; margin-right: 5px"></span>
|
|
||||||
</TooltipWrapper>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column :label="t('table.playerList.bioLink')" width="100" prop="ref.bioLinks">
|
|
||||||
<template #default="scope">
|
|
||||||
<div style="display: flex; align-items: center">
|
|
||||||
<TooltipWrapper
|
|
||||||
v-for="(link, index) in scope.row.ref.bioLinks?.filter(Boolean)"
|
|
||||||
:key="index">
|
|
||||||
<template #content>
|
|
||||||
<span v-text="link"></span>
|
|
||||||
</template>
|
|
||||||
<img
|
|
||||||
:src="getFaviconUrl(link)"
|
|
||||||
style="
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
vertical-align: middle;
|
|
||||||
margin-right: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
"
|
|
||||||
@click.stop="openExternalLink(link)"
|
|
||||||
loading="lazy" />
|
|
||||||
</TooltipWrapper>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column :label="t('table.playerList.note')" width="400" prop="ref.note">
|
|
||||||
<template #default="scope">
|
|
||||||
<span v-text="scope.row.ref.note"></span>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</DataTable>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ChatboxBlacklistDialog
|
<ChatboxBlacklistDialog
|
||||||
@@ -391,20 +197,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ChatLineRound, CircleClose, HomeFilled, Microphone, Mute, Pointer } from '@element-plus/icons-vue';
|
import { computed, defineAsyncComponent, onActivated, onMounted, ref, watch } from 'vue';
|
||||||
import { computed, defineAsyncComponent, onMounted, onUnmounted, ref, watch } from 'vue';
|
import { getCoreRowModel, getPaginationRowModel, getSortedRowModel, useVueTable } from '@tanstack/vue-table';
|
||||||
|
import { HomeFilled } from '@element-plus/icons-vue';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
import {
|
|
||||||
commaNumber,
|
|
||||||
formatDateFilter,
|
|
||||||
getFaviconUrl,
|
|
||||||
languageClass,
|
|
||||||
openExternalLink,
|
|
||||||
statusClass,
|
|
||||||
userImage
|
|
||||||
} from '../../shared/utils';
|
|
||||||
import {
|
import {
|
||||||
useAppearanceSettingsStore,
|
useAppearanceSettingsStore,
|
||||||
useGalleryStore,
|
useGalleryStore,
|
||||||
@@ -414,8 +212,15 @@
|
|||||||
useUserStore,
|
useUserStore,
|
||||||
useWorldStore
|
useWorldStore
|
||||||
} from '../../stores';
|
} from '../../stores';
|
||||||
|
import { commaNumber, formatDateFilter } from '../../shared/utils';
|
||||||
|
import { DataTableLayout } from '../../components/ui/data-table';
|
||||||
|
import { createColumns } from './columns.jsx';
|
||||||
|
import { useDataTableScrollHeight } from '../../composables/useDataTableScrollHeight';
|
||||||
|
import { valueUpdater } from '../../components/ui/table/utils';
|
||||||
|
import { watchState } from '../../service/watchState';
|
||||||
|
|
||||||
import ChatboxBlacklistDialog from './dialogs/ChatboxBlacklistDialog.vue';
|
import ChatboxBlacklistDialog from './dialogs/ChatboxBlacklistDialog.vue';
|
||||||
|
import Timer from '../../components/Timer.vue';
|
||||||
|
|
||||||
const PhotonEventTable = defineAsyncComponent(() => import('./components/PhotonEventTable.vue'));
|
const PhotonEventTable = defineAsyncComponent(() => import('./components/PhotonEventTable.vue'));
|
||||||
|
|
||||||
@@ -428,65 +233,19 @@
|
|||||||
const { lastLocation } = storeToRefs(useLocationStore());
|
const { lastLocation } = storeToRefs(useLocationStore());
|
||||||
const { currentInstanceLocation, currentInstanceWorld } = storeToRefs(useInstanceStore());
|
const { currentInstanceLocation, currentInstanceWorld } = storeToRefs(useInstanceStore());
|
||||||
const { getCurrentInstanceUserList } = useInstanceStore();
|
const { getCurrentInstanceUserList } = useInstanceStore();
|
||||||
const { currentInstanceUsersTable } = storeToRefs(useInstanceStore());
|
const { currentInstanceUsersData } = storeToRefs(useInstanceStore());
|
||||||
const { showFullscreenImageDialog } = useGalleryStore();
|
const { showFullscreenImageDialog } = useGalleryStore();
|
||||||
const { currentUser } = storeToRefs(useUserStore());
|
const { currentUser } = storeToRefs(useUserStore());
|
||||||
|
|
||||||
const playerListRef = ref(null);
|
const playerListRef = ref(null);
|
||||||
const tableHeight = ref(0);
|
const playerListHeaderRef = ref(null);
|
||||||
|
const playerListPhotonRef = ref(null);
|
||||||
|
|
||||||
onMounted(() => {
|
const { tableStyle: playerListTableStyle } = useDataTableScrollHeight(playerListRef, {
|
||||||
if (playerListRef.value) {
|
offset: 30,
|
||||||
resizeObserver.observe(playerListRef.value);
|
paginationHeight: 0,
|
||||||
}
|
subtractContainerPadding: true,
|
||||||
});
|
extraOffsetRefs: [playerListHeaderRef, playerListPhotonRef]
|
||||||
|
|
||||||
const resizeObserver = new ResizeObserver(() => {
|
|
||||||
setPlayerListTableHeight();
|
|
||||||
});
|
|
||||||
|
|
||||||
function setPlayerListTableHeight() {
|
|
||||||
if (currentInstanceWorld.value.ref.id) {
|
|
||||||
tableHeight.value = playerListRef.value.clientHeight - 110;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (currentInstanceUsersTable.value.data.length === 0) {
|
|
||||||
tableHeight.value = playerListRef.value.clientHeight;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (playerListRef.value) {
|
|
||||||
tableHeight.value = playerListRef.value.clientHeight - 110;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => currentInstanceWorld.value.ref.id,
|
|
||||||
() => {
|
|
||||||
setPlayerListTableHeight();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
resizeObserver.disconnect();
|
|
||||||
});
|
|
||||||
|
|
||||||
const compactCellStyle = () => ({
|
|
||||||
padding: '4px 10px'
|
|
||||||
});
|
|
||||||
|
|
||||||
const compactInstanceUsersTable = computed(() => {
|
|
||||||
const baseTableConfig = currentInstanceUsersTable.value;
|
|
||||||
const tableProps = baseTableConfig.tableProps || {};
|
|
||||||
|
|
||||||
return {
|
|
||||||
...baseTableConfig,
|
|
||||||
tableProps: {
|
|
||||||
...tableProps,
|
|
||||||
cellStyle: compactCellStyle,
|
|
||||||
headerCellStyle: compactCellStyle,
|
|
||||||
height: tableHeight.value
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
@@ -525,25 +284,65 @@
|
|||||||
getCurrentInstanceUserList();
|
getCurrentInstanceUserList();
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortInstanceIcon(a, b) {
|
|
||||||
const getValue = (item) => {
|
|
||||||
let value = 0;
|
|
||||||
if (item.isMaster) value += 1000;
|
|
||||||
if (item.isModerator) value += 500;
|
|
||||||
if (item.isFriend) value += 200;
|
|
||||||
if (item.isBlocked) value -= 100;
|
|
||||||
if (item.isMuted) value -= 50;
|
|
||||||
if (item.isAvatarInteractionDisabled) value -= 20;
|
|
||||||
if (item.isChatBoxMuted) value -= 10;
|
|
||||||
return value;
|
|
||||||
};
|
|
||||||
return getValue(b) - getValue(a);
|
|
||||||
}
|
|
||||||
|
|
||||||
function sortAlphabetically(a, b, field) {
|
function sortAlphabetically(a, b, field) {
|
||||||
if (!a[field] || !b[field]) return 0;
|
if (!a[field] || !b[field]) return 0;
|
||||||
return a[field].toLowerCase().localeCompare(b[field].toLowerCase());
|
return a[field].toLowerCase().localeCompare(b[field].toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sorting = ref([]);
|
||||||
|
const pagination = ref({
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: 500
|
||||||
|
});
|
||||||
|
|
||||||
|
const columnPinning = ref({
|
||||||
|
left: ['avatar', 'timer', 'displayName'],
|
||||||
|
right: []
|
||||||
|
});
|
||||||
|
|
||||||
|
const playerListColumns = computed(() =>
|
||||||
|
createColumns({
|
||||||
|
randomUserColours,
|
||||||
|
photonLoggingEnabled,
|
||||||
|
chatboxUserBlacklist,
|
||||||
|
onBlockChatbox: addChatboxUserBlacklist,
|
||||||
|
onUnblockChatbox: deleteChatboxUserBlacklist,
|
||||||
|
sortAlphabetically
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const playerListTable = useVueTable({
|
||||||
|
data: currentInstanceUsersData.value,
|
||||||
|
columns: playerListColumns.value,
|
||||||
|
getRowId: (row) => `${row?.ref?.id ?? ''}:${row?.displayName ?? ''}`,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
|
getSortedRowModel: getSortedRowModel(),
|
||||||
|
onSortingChange: (updaterOrValue) => valueUpdater(updaterOrValue, sorting),
|
||||||
|
onPaginationChange: (updaterOrValue) => valueUpdater(updaterOrValue, pagination),
|
||||||
|
onColumnPinningChange: (updaterOrValue) => valueUpdater(updaterOrValue, columnPinning),
|
||||||
|
state: {
|
||||||
|
get sorting() {
|
||||||
|
return sorting.value;
|
||||||
|
},
|
||||||
|
get pagination() {
|
||||||
|
return pagination.value;
|
||||||
|
},
|
||||||
|
get columnPinning() {
|
||||||
|
return columnPinning.value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
initialState: {
|
||||||
|
columnPinning: columnPinning.value,
|
||||||
|
pagination: pagination.value
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const playerListTotalItems = computed(() => playerListTable.getRowModel().rows.length);
|
||||||
|
|
||||||
|
const handlePlayerListRowClick = (row) => {
|
||||||
|
selectCurrentInstanceRow(row?.original ?? null);
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -556,15 +355,4 @@
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
#x-app .current-instance-table .el-table .el-table__cell {
|
|
||||||
padding: 3px 10px !important;
|
|
||||||
}
|
|
||||||
.table-user {
|
|
||||||
color: var(--x-table-user-text-color);
|
|
||||||
}
|
|
||||||
.friends-list-avatar {
|
|
||||||
width: 16px !important;
|
|
||||||
height: 16px !important;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
434
src/views/PlayerList/columns.jsx
Normal file
434
src/views/PlayerList/columns.jsx
Normal file
@@ -0,0 +1,434 @@
|
|||||||
|
import Timer from '../../components/Timer.vue';
|
||||||
|
import { Button } from '../../components/ui/button';
|
||||||
|
import { TooltipWrapper } from '../../components/ui/tooltip';
|
||||||
|
import { ArrowUpDown } from 'lucide-vue-next';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getFaviconUrl,
|
||||||
|
languageClass,
|
||||||
|
openExternalLink,
|
||||||
|
statusClass,
|
||||||
|
userImage
|
||||||
|
} from '../../shared/utils';
|
||||||
|
import { i18n } from '../../plugin';
|
||||||
|
|
||||||
|
const { t } = i18n.global;
|
||||||
|
|
||||||
|
const sortButton = ({ column, label }) => (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
class="-ml-2 h-8 px-2"
|
||||||
|
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
<ArrowUpDown class="ml-1 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
|
||||||
|
const getInstanceIconWeight = (item) => {
|
||||||
|
if (!item) return 0;
|
||||||
|
let value = 0;
|
||||||
|
if (item.isMaster) value += 1000;
|
||||||
|
if (item.isModerator) value += 500;
|
||||||
|
if (item.isFriend) value += 200;
|
||||||
|
if (item.isBlocked) value -= 100;
|
||||||
|
if (item.isMuted) value -= 50;
|
||||||
|
if (item.isAvatarInteractionDisabled) value -= 20;
|
||||||
|
if (item.isChatBoxMuted) value -= 10;
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createColumns = ({
|
||||||
|
randomUserColours,
|
||||||
|
photonLoggingEnabled,
|
||||||
|
chatboxUserBlacklist,
|
||||||
|
onBlockChatbox,
|
||||||
|
onUnblockChatbox,
|
||||||
|
sortAlphabetically
|
||||||
|
}) => {
|
||||||
|
/** @type {import('@tanstack/vue-table').ColumnDef<any, any>[]} */
|
||||||
|
const cols = [
|
||||||
|
{
|
||||||
|
id: 'avatar',
|
||||||
|
accessorFn: (row) => row?.photo,
|
||||||
|
header: () => t('table.playerList.avatar'),
|
||||||
|
size: 70,
|
||||||
|
enableSorting: false,
|
||||||
|
meta: {
|
||||||
|
class: 'w-[70px]'
|
||||||
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const userRef = row.original?.ref;
|
||||||
|
const src = userImage(userRef);
|
||||||
|
if (!src) return null;
|
||||||
|
return (
|
||||||
|
<div class="flex items-center pl-2">
|
||||||
|
<img
|
||||||
|
src={src}
|
||||||
|
class="h-4 w-4 rounded-sm object-cover"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'timer',
|
||||||
|
accessorFn: (row) => row?.timer,
|
||||||
|
header: ({ column }) =>
|
||||||
|
sortButton({ column, label: t('table.playerList.timer') }),
|
||||||
|
size: 90,
|
||||||
|
meta: {
|
||||||
|
class: 'w-[90px]'
|
||||||
|
},
|
||||||
|
sortingFn: (rowA, rowB) =>
|
||||||
|
(rowA.original?.timer ?? 0) - (rowB.original?.timer ?? 0),
|
||||||
|
cell: ({ row }) => <Timer epoch={row.original?.timer} />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'displayName',
|
||||||
|
accessorFn: (row) => row?.displayName,
|
||||||
|
header: ({ column }) =>
|
||||||
|
sortButton({
|
||||||
|
column,
|
||||||
|
label: t('table.playerList.displayName')
|
||||||
|
}),
|
||||||
|
size: 200,
|
||||||
|
meta: {
|
||||||
|
class: 'w-[200px]'
|
||||||
|
},
|
||||||
|
sortingFn: (rowA, rowB) =>
|
||||||
|
sortAlphabetically(rowA.original, rowB.original, 'displayName'),
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const userRef = row.original?.ref;
|
||||||
|
const style = randomUserColours?.value
|
||||||
|
? { color: userRef?.$userColour }
|
||||||
|
: null;
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
class="text-(--x-table-user-text-color)"
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
{userRef?.displayName ?? ''}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'rank',
|
||||||
|
accessorFn: (row) => row?.ref?.$trustSortNum,
|
||||||
|
header: ({ column }) =>
|
||||||
|
sortButton({ column, label: t('table.playerList.rank') }),
|
||||||
|
size: 110,
|
||||||
|
meta: {
|
||||||
|
class: 'w-[110px]'
|
||||||
|
},
|
||||||
|
sortingFn: (rowA, rowB) =>
|
||||||
|
(rowA.original?.ref?.$trustSortNum ?? 0) -
|
||||||
|
(rowB.original?.ref?.$trustSortNum ?? 0),
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const userRef = row.original?.ref;
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
class={['name', userRef?.$trustClass]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')}
|
||||||
|
>
|
||||||
|
{userRef?.$trustLevel ?? ''}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'status',
|
||||||
|
accessorFn: (row) => row?.ref?.statusDescription,
|
||||||
|
header: () => t('table.playerList.status'),
|
||||||
|
minSize: 200,
|
||||||
|
enableSorting: false,
|
||||||
|
meta: {
|
||||||
|
class: 'min-w-[200px]'
|
||||||
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const userRef = row.original?.ref;
|
||||||
|
const status = userRef?.status;
|
||||||
|
return (
|
||||||
|
<span class="flex w-full min-w-0 items-center gap-2">
|
||||||
|
<i
|
||||||
|
class={[
|
||||||
|
'x-user-status',
|
||||||
|
'shrink-0',
|
||||||
|
status ? statusClass(status) : null
|
||||||
|
]}
|
||||||
|
></i>
|
||||||
|
<span class="min-w-0 truncate">
|
||||||
|
{userRef?.statusDescription ?? ''}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
if (photonLoggingEnabled?.value) {
|
||||||
|
cols.push({
|
||||||
|
id: 'photonId',
|
||||||
|
accessorFn: (row) => row?.photonId,
|
||||||
|
header: ({ column }) =>
|
||||||
|
sortButton({ column, label: t('table.playerList.photonId') }),
|
||||||
|
size: 110,
|
||||||
|
meta: {
|
||||||
|
class: 'w-[110px]'
|
||||||
|
},
|
||||||
|
sortingFn: (rowA, rowB) =>
|
||||||
|
(rowA.original?.photonId ?? 0) - (rowB.original?.photonId ?? 0),
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const userRef = row.original?.ref;
|
||||||
|
const userId = userRef?.id;
|
||||||
|
const isBlocked =
|
||||||
|
userId && chatboxUserBlacklist?.value?.has?.(userId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="flex items-center">
|
||||||
|
{userId ? (
|
||||||
|
<button
|
||||||
|
class={
|
||||||
|
isBlocked
|
||||||
|
? 'mr-1 text-xs underline text-destructive'
|
||||||
|
: 'mr-1 text-xs underline'
|
||||||
|
}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (isBlocked) {
|
||||||
|
onUnblockChatbox(userId);
|
||||||
|
} else {
|
||||||
|
onBlockChatbox(userRef);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isBlocked ? 'Unblock' : 'Block'}
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
<span>{String(row.original?.photonId ?? '')}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
cols.push(
|
||||||
|
{
|
||||||
|
id: 'icon',
|
||||||
|
header: ({ column }) =>
|
||||||
|
sortButton({ column, label: t('table.playerList.icon') }),
|
||||||
|
size: 90,
|
||||||
|
accessorFn: (row) => getInstanceIconWeight(row),
|
||||||
|
meta: {
|
||||||
|
class: 'w-[90px] text-center'
|
||||||
|
},
|
||||||
|
sortingFn: (rowA, rowB, columnId) => {
|
||||||
|
const a = rowA.getValue(columnId) ?? 0;
|
||||||
|
const b = rowB.getValue(columnId) ?? 0;
|
||||||
|
return b - a;
|
||||||
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const r = row.original;
|
||||||
|
return (
|
||||||
|
<div class="flex items-center justify-center gap-1">
|
||||||
|
{r?.isMaster ? (
|
||||||
|
<TooltipWrapper
|
||||||
|
side="left"
|
||||||
|
content="Instance Master"
|
||||||
|
>
|
||||||
|
<span>👑</span>
|
||||||
|
</TooltipWrapper>
|
||||||
|
) : null}
|
||||||
|
{r?.isModerator ? (
|
||||||
|
<TooltipWrapper side="left" content="Moderator">
|
||||||
|
<span>⚔️</span>
|
||||||
|
</TooltipWrapper>
|
||||||
|
) : null}
|
||||||
|
{r?.isFriend ? (
|
||||||
|
<TooltipWrapper side="left" content="Friend">
|
||||||
|
<span>💚</span>
|
||||||
|
</TooltipWrapper>
|
||||||
|
) : null}
|
||||||
|
{r?.isBlocked ? (
|
||||||
|
<TooltipWrapper side="left" content="Blocked">
|
||||||
|
<span class="text-destructive">⛔</span>
|
||||||
|
</TooltipWrapper>
|
||||||
|
) : null}
|
||||||
|
{r?.isMuted ? (
|
||||||
|
<TooltipWrapper side="left" content="Muted">
|
||||||
|
<span class="text-muted-foreground">🔇</span>
|
||||||
|
</TooltipWrapper>
|
||||||
|
) : null}
|
||||||
|
{r?.isAvatarInteractionDisabled ? (
|
||||||
|
<TooltipWrapper
|
||||||
|
side="left"
|
||||||
|
content="Avatar Interaction Disabled"
|
||||||
|
>
|
||||||
|
<span class="text-muted-foreground">🚫</span>
|
||||||
|
</TooltipWrapper>
|
||||||
|
) : null}
|
||||||
|
{r?.isChatBoxMuted ? (
|
||||||
|
<TooltipWrapper side="left" content="Chatbox Muted">
|
||||||
|
<span class="text-muted-foreground">💬</span>
|
||||||
|
</TooltipWrapper>
|
||||||
|
) : null}
|
||||||
|
{r?.timeoutTime ? (
|
||||||
|
<TooltipWrapper side="left" content="Timeout">
|
||||||
|
<span class="text-destructive">
|
||||||
|
🔴{r.timeoutTime}s
|
||||||
|
</span>
|
||||||
|
</TooltipWrapper>
|
||||||
|
) : null}
|
||||||
|
{r?.ageVerified ? (
|
||||||
|
<TooltipWrapper side="left" content="18+ Verified">
|
||||||
|
<i class="ri-id-card-line x-tag-age-verification"></i>
|
||||||
|
</TooltipWrapper>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'platform',
|
||||||
|
header: () => t('table.playerList.platform'),
|
||||||
|
size: 90,
|
||||||
|
enableSorting: false,
|
||||||
|
meta: {
|
||||||
|
class: 'w-[90px]'
|
||||||
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const userRef = row.original?.ref;
|
||||||
|
const platform = userRef?.$platform;
|
||||||
|
const inVRMode = row.original?.inVRMode;
|
||||||
|
|
||||||
|
const platformIcon =
|
||||||
|
platform === 'standalonewindows' ? (
|
||||||
|
<i class="ri-computer-line x-tag-platform-pc" />
|
||||||
|
) : platform === 'android' ? (
|
||||||
|
<i class="ri-android-line x-tag-platform-quest" />
|
||||||
|
) : platform === 'ios' ? (
|
||||||
|
<i class="ri-apple-line x-tag-platform-ios" />
|
||||||
|
) : platform ? (
|
||||||
|
<span>{String(platform)}</span>
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
const mode =
|
||||||
|
inVRMode === null || inVRMode === undefined
|
||||||
|
? null
|
||||||
|
: inVRMode
|
||||||
|
? 'VR'
|
||||||
|
: userRef?.last_platform === 'android' ||
|
||||||
|
userRef?.last_platform === 'ios'
|
||||||
|
? 'M'
|
||||||
|
: 'D';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
{platformIcon}
|
||||||
|
{mode ? <span>{mode}</span> : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'language',
|
||||||
|
header: () => t('table.playerList.language'),
|
||||||
|
size: 100,
|
||||||
|
enableSorting: false,
|
||||||
|
meta: {
|
||||||
|
class: 'w-[100px]'
|
||||||
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const userRef = row.original?.ref;
|
||||||
|
const langs = userRef?.$languages ?? [];
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{langs.map((item) => (
|
||||||
|
<TooltipWrapper
|
||||||
|
key={item.key}
|
||||||
|
side="top"
|
||||||
|
v-slots={{
|
||||||
|
content: () => (
|
||||||
|
<span>
|
||||||
|
{item.value} ({item.key})
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class={[
|
||||||
|
'flags',
|
||||||
|
'inline-block',
|
||||||
|
'mr-1',
|
||||||
|
languageClass(item.key)
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</TooltipWrapper>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bioLink',
|
||||||
|
header: () => t('table.playerList.bioLink'),
|
||||||
|
size: 100,
|
||||||
|
enableSorting: false,
|
||||||
|
meta: {
|
||||||
|
class: 'w-[100px]'
|
||||||
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const links =
|
||||||
|
row.original?.ref?.bioLinks?.filter(Boolean) ?? [];
|
||||||
|
return (
|
||||||
|
<div class="flex items-center">
|
||||||
|
{links.map((link, index) => (
|
||||||
|
<TooltipWrapper
|
||||||
|
key={index}
|
||||||
|
v-slots={{
|
||||||
|
content: () => (
|
||||||
|
<span>{String(link ?? '')}</span>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={getFaviconUrl(link)}
|
||||||
|
class="h-4 w-4 mr-1 align-middle cursor-pointer"
|
||||||
|
loading="lazy"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
openExternalLink(String(link));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</TooltipWrapper>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'note',
|
||||||
|
accessorFn: (row) => row?.ref?.note,
|
||||||
|
header: () => t('table.playerList.note'),
|
||||||
|
size: 400,
|
||||||
|
enableSorting: false,
|
||||||
|
meta: {
|
||||||
|
class: 'w-[150px]'
|
||||||
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const note = row.original?.ref?.note;
|
||||||
|
const text =
|
||||||
|
typeof note === 'string' || typeof note === 'number'
|
||||||
|
? String(note)
|
||||||
|
: '';
|
||||||
|
return <span>{text}</span>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return cols;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user