wip: rewrite playerlist

This commit is contained in:
pa
2026-01-07 16:20:52 +09:00
committed by Natsumi
parent 9ae8789d14
commit 6cfefb50ab
6 changed files with 641 additions and 346 deletions

View 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;
};