mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-14 04:13:52 +02:00
391 lines
14 KiB
Vue
391 lines
14 KiB
Vue
<template>
|
|
<div class="flex items-center" v-bind="$attrs">
|
|
<div v-if="showLaunchButton" class="inline-block">
|
|
<TooltipWrapper side="top" :content="t('dialog.user.info.launch_invite_tooltip')">
|
|
<Button
|
|
class="rounded-full w-6 h-6 text-xs text-muted-foreground hover:text-foreground"
|
|
size="icon-sm"
|
|
variant="outline"
|
|
@click="confirmLaunch">
|
|
<LogIn />
|
|
</Button>
|
|
</TooltipWrapper>
|
|
</div>
|
|
<div v-if="showInviteYourself" class="inline-block" :style="inviteStyle">
|
|
<TooltipWrapper
|
|
v-if="!canOpenInstanceInGame"
|
|
side="top"
|
|
:content="t('dialog.user.info.self_invite_tooltip')">
|
|
<Button
|
|
class="rounded-full h-6 w-6 text-xs text-muted-foreground hover:text-foreground"
|
|
size="icon-sm"
|
|
variant="outline"
|
|
@click="confirmInvite">
|
|
<Mail class="h-4 w-4" />
|
|
</Button>
|
|
</TooltipWrapper>
|
|
<TooltipWrapper v-else side="top" :content="t('dialog.user.info.open_in_vrchat_tooltip')">
|
|
<Button
|
|
class="rounded-full h-6 w-6 text-xs text-muted-foreground hover:text-foreground"
|
|
size="icon-sm"
|
|
variant="outline"
|
|
v-if="isOpeningInstance">
|
|
<Loader2 class="h-4 w-4 animate-spin" />
|
|
</Button>
|
|
<Button
|
|
class="rounded-full h-6 w-6 text-xs text-muted-foreground hover:text-foreground"
|
|
size="icon-sm"
|
|
variant="outline"
|
|
v-else
|
|
@click="openInstance">
|
|
<Mail class="h-4 w-4" />
|
|
</Button>
|
|
</TooltipWrapper>
|
|
</div>
|
|
<TooltipWrapper v-if="showRefreshButton" side="top" :content="refreshTooltip">
|
|
<Button
|
|
class="rounded-full ml-1 w-6 h-6 text-xs text-muted-foreground hover:text-foreground"
|
|
size="icon"
|
|
variant="outline"
|
|
@click="handleRefresh">
|
|
<RefreshCw class="h-4 w-4" />
|
|
</Button>
|
|
</TooltipWrapper>
|
|
<TooltipWrapper v-if="showHistoryButton" side="top" :content="historyTooltip">
|
|
<Button
|
|
class="rounded-full w-6 h-6 text-xs text-muted-foreground hover:text-foreground"
|
|
size="icon-sm"
|
|
variant="outline"
|
|
style="margin-left: 5px"
|
|
@click="handleHistory">
|
|
<History class="h-4 w-4" />
|
|
</Button>
|
|
</TooltipWrapper>
|
|
|
|
<div v-if="showInstanceInfo" class="flex items-center ml-2">
|
|
<TooltipWrapper v-if="instanceInfoState.isValidInstance" side="top">
|
|
<template #content>
|
|
<div>
|
|
<span v-if="instance?.closedAt">
|
|
Closed At: {{ formatDateFilter(instance.closedAt, 'long') }}<br />
|
|
</span>
|
|
<template v-if="instanceInfoState.canCloseInstance">
|
|
<Button
|
|
class="mt-1"
|
|
size="xs"
|
|
:disabled="!!instance?.closedAt"
|
|
@click="closeInstance(resolvedInstanceLocation)">
|
|
{{ t('dialog.user.info.close_instance') }}
|
|
</Button>
|
|
<br /><br />
|
|
</template>
|
|
<span>
|
|
<span class="x-tag-platform-pc">PC: </span>{{ instance?.platforms?.standalonewindows }}
|
|
</span>
|
|
<br />
|
|
<span>
|
|
<span class="x-tag-platform-quest">Android: </span>{{ instance?.platforms?.android }}
|
|
</span>
|
|
<br />
|
|
<span><span>iOS: </span>{{ instance?.platforms?.ios }}</span>
|
|
<br />
|
|
<span>{{ t('dialog.user.info.instance_game_version') }} {{ instance?.gameServerVersion }}</span>
|
|
<br />
|
|
<span v-if="instance?.queueEnabled"
|
|
>{{ t('dialog.user.info.instance_queuing_enabled') }}<br
|
|
/></span>
|
|
<span v-if="instanceInfoState.disabledContentSettings">
|
|
{{ t('dialog.user.info.instance_disabled_content') }}
|
|
{{ instanceInfoState.disabledContentSettings }}<br />
|
|
</span>
|
|
<span v-if="instance?.users?.length">{{ t('dialog.user.info.instance_users') }}<br /></span>
|
|
<template v-for="user in instance?.users || []" :key="user.id">
|
|
<span style="cursor: pointer; margin-right: 5px" @click="showUserDialog(user.id)">
|
|
{{ user.displayName }}
|
|
</span>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
<div class="mr-1 text-muted-foreground">
|
|
<span v-if="resolvedInstanceLocation === locationStore.lastLocation.location">
|
|
{{ locationStore.lastLocation.playerList.size }}/{{ instance?.capacity }}
|
|
</span>
|
|
|
|
<span v-else-if="instance?.userCount"> {{ instance.userCount }}/{{ instance?.capacity }} </span>
|
|
</div>
|
|
</TooltipWrapper>
|
|
|
|
<TooltipWrapper v-if="friendcount" side="top" :content="t('dialog.user.info.instance_friends_tooltip')">
|
|
<span class="ml-1 flex items-center text-muted-foreground"><UsersRound />{{ friendcount }}</span>
|
|
</TooltipWrapper>
|
|
<span v-if="showLastJoinIndicator" class="inline-block ml-1">
|
|
<TooltipWrapper side="top">
|
|
<template #content>
|
|
<span>{{ t('dialog.user.info.last_join') }} </span>
|
|
</template>
|
|
<span class="flex items-center ml-1">
|
|
<MapPin class="h-4 w-4 text-muted-foreground" />
|
|
<Timer class="text-muted-foreground" :epoch="lastJoin" />
|
|
</span>
|
|
</TooltipWrapper>
|
|
</span>
|
|
<span v-if="instanceInfoState.isValidInstance && !instance?.hasCapacityForYou" class="ml-1">
|
|
{{ t('dialog.user.info.instance_full') }}
|
|
</span>
|
|
<span v-if="instance?.queueSize" class="ml-1">
|
|
{{ t('dialog.user.info.instance_queue') }} {{ instance.queueSize }}
|
|
</span>
|
|
<span v-if="instanceInfoState.isAgeGated" class="ml-1">
|
|
{{ t('dialog.user.info.instance_age_gated') }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { History, Loader2, LogIn, Mail, MapPin, RefreshCw, UsersRound } from 'lucide-vue-next';
|
|
import { computed, reactive, ref, watch } from 'vue';
|
|
import { Button } from '@/components/ui/button';
|
|
import { storeToRefs } from 'pinia';
|
|
import { toast } from 'vue-sonner';
|
|
import { useI18n } from 'vue-i18n';
|
|
|
|
import {
|
|
useGroupStore,
|
|
useInstanceStore,
|
|
useInviteStore,
|
|
useLaunchStore,
|
|
useLocationStore,
|
|
useModalStore,
|
|
useUserStore
|
|
} from '../stores';
|
|
import { checkCanInviteSelf, formatDateFilter, hasGroupPermission, parseLocation } from '../shared/utils';
|
|
import { instanceRequest, miscRequest } from '../api';
|
|
|
|
defineOptions({
|
|
inheritAttrs: false
|
|
});
|
|
|
|
const { t } = useI18n();
|
|
|
|
const locationStore = useLocationStore();
|
|
const userStore = useUserStore();
|
|
const groupStore = useGroupStore();
|
|
const instanceStore = useInstanceStore();
|
|
const modalStore = useModalStore();
|
|
const launchStore = useLaunchStore();
|
|
const inviteStore = useInviteStore();
|
|
|
|
const { instanceJoinHistory } = storeToRefs(instanceStore);
|
|
const { canOpenInstanceInGame } = storeToRefs(inviteStore);
|
|
const { isOpeningInstance } = storeToRefs(launchStore);
|
|
|
|
const props = defineProps({
|
|
location: {
|
|
type: String,
|
|
default: ''
|
|
},
|
|
launchLocation: {
|
|
type: String,
|
|
default: ''
|
|
},
|
|
inviteLocation: {
|
|
type: String,
|
|
default: ''
|
|
},
|
|
lastJoinLocation: {
|
|
type: String,
|
|
default: ''
|
|
},
|
|
instanceLocation: {
|
|
type: String,
|
|
default: ''
|
|
},
|
|
shortname: {
|
|
type: String,
|
|
default: ''
|
|
},
|
|
instance: {
|
|
type: Object,
|
|
default: null
|
|
},
|
|
friendcount: {
|
|
type: Number,
|
|
default: undefined
|
|
},
|
|
currentlocation: {
|
|
type: String,
|
|
default: ''
|
|
},
|
|
showLaunch: {
|
|
type: Boolean,
|
|
default: true
|
|
},
|
|
showInvite: {
|
|
type: Boolean,
|
|
default: true
|
|
},
|
|
showRefresh: {
|
|
type: Boolean,
|
|
default: true
|
|
},
|
|
showHistory: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
showLastJoin: {
|
|
type: Boolean,
|
|
default: true
|
|
},
|
|
showInstanceInfo: {
|
|
type: Boolean,
|
|
default: true
|
|
},
|
|
refreshTooltip: {
|
|
type: String,
|
|
default: ''
|
|
},
|
|
historyTooltip: {
|
|
type: String,
|
|
default: ''
|
|
},
|
|
onRefresh: {
|
|
type: Function,
|
|
default: null
|
|
},
|
|
onHistory: {
|
|
type: Function,
|
|
default: null
|
|
}
|
|
});
|
|
|
|
const resolvedLaunchLocation = computed(() => props.launchLocation || props.location);
|
|
const resolvedInviteLocation = computed(() => props.inviteLocation || props.location);
|
|
const resolvedLastJoinLocation = computed(() => props.lastJoinLocation || props.location);
|
|
const resolvedInstanceLocation = computed(() => props.instanceLocation || props.location);
|
|
|
|
const showLaunchButton = computed(() => props.showLaunch && checkCanInviteSelf(resolvedLaunchLocation.value));
|
|
const showInviteYourself = computed(() => props.showInvite && checkCanInviteSelf(resolvedInviteLocation.value));
|
|
|
|
const inviteStyle = computed(() => (showLaunchButton.value ? 'margin-left: 5px' : ''));
|
|
const showRefreshButton = computed(() => props.showRefresh && typeof props.onRefresh === 'function');
|
|
const showHistoryButton = computed(() => props.showHistory && typeof props.onHistory === 'function');
|
|
|
|
const lastJoin = ref(null);
|
|
const showLastJoinIndicator = computed(() => props.showLastJoin && lastJoin.value);
|
|
|
|
const instanceInfoState = reactive({
|
|
isValidInstance: false,
|
|
canCloseInstance: false,
|
|
isAgeGated: false,
|
|
disabledContentSettings: ''
|
|
});
|
|
|
|
const handleRefresh = () => {
|
|
if (typeof props.onRefresh === 'function') {
|
|
props.onRefresh();
|
|
}
|
|
};
|
|
|
|
const handleHistory = () => {
|
|
if (typeof props.onHistory === 'function') {
|
|
props.onHistory();
|
|
}
|
|
};
|
|
|
|
const confirmLaunch = () => {
|
|
launchStore.showLaunchDialog(resolvedLaunchLocation.value);
|
|
};
|
|
|
|
const confirmInvite = () => {
|
|
const L = parseLocation(resolvedInviteLocation.value);
|
|
if (!L.isRealInstance) {
|
|
return;
|
|
}
|
|
|
|
instanceRequest
|
|
.selfInvite({
|
|
instanceId: L.instanceId,
|
|
worldId: L.worldId,
|
|
shortName: props.shortname
|
|
})
|
|
.then((args) => {
|
|
toast.success(t('message.invite.self_sent'));
|
|
return args;
|
|
});
|
|
};
|
|
|
|
const openInstance = () => {
|
|
const L = parseLocation(resolvedInviteLocation.value);
|
|
if (!L.isRealInstance) {
|
|
return;
|
|
}
|
|
|
|
launchStore.tryOpenInstanceInVrc(L.tag, props.shortname);
|
|
};
|
|
|
|
const parseLastJoin = () => {
|
|
lastJoin.value = instanceJoinHistory.value.get(resolvedLastJoinLocation.value);
|
|
};
|
|
|
|
const parseInstanceInfo = () => {
|
|
Object.assign(instanceInfoState, {
|
|
isValidInstance: false,
|
|
canCloseInstance: false,
|
|
isAgeGated: false,
|
|
disabledContentSettings: ''
|
|
});
|
|
|
|
if (!resolvedInstanceLocation.value || !props.instance || Object.keys(props.instance).length === 0) return;
|
|
|
|
instanceInfoState.isValidInstance = true;
|
|
if (props.instance.ownerId === userStore.currentUser.id) {
|
|
instanceInfoState.canCloseInstance = true;
|
|
} else if (props.instance.ownerId?.startsWith('grp_')) {
|
|
const group = groupStore.cachedGroups.get(props.instance.ownerId);
|
|
instanceInfoState.canCloseInstance = hasGroupPermission(group, 'group-instance-moderate');
|
|
}
|
|
instanceInfoState.isAgeGated = props.instance.ageGate === true;
|
|
if (resolvedInstanceLocation.value?.includes('~ageGate')) instanceInfoState.isAgeGated = true;
|
|
if (props.instance.$disabledContentSettings?.length) {
|
|
instanceInfoState.disabledContentSettings = props.instance.$disabledContentSettings.join(', ');
|
|
}
|
|
};
|
|
|
|
const showUserDialog = (userId) => {
|
|
userStore.showUserDialog(userId);
|
|
};
|
|
|
|
const closeInstance = (location) => {
|
|
modalStore
|
|
.confirm({
|
|
description: t('confirm.close_instance'),
|
|
title: t('confirm.title')
|
|
})
|
|
.then(async ({ ok }) => {
|
|
if (!ok) return;
|
|
const args = await miscRequest.closeInstance({ location, hardClose: false });
|
|
if (args.json) {
|
|
toast.success(t('message.instance.closed'));
|
|
instanceStore.applyInstance(args.json);
|
|
}
|
|
})
|
|
.catch(() => {});
|
|
};
|
|
|
|
watch(() => resolvedLastJoinLocation.value, parseLastJoin, { immediate: true });
|
|
watch(() => props.currentlocation, parseLastJoin);
|
|
watch([resolvedInstanceLocation, () => props.instance, () => props.friendcount], parseInstanceInfo, {
|
|
immediate: true
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
.inline-block {
|
|
display: inline-block;
|
|
}
|
|
.ml-5 {
|
|
margin-left: 5px;
|
|
}
|
|
</style>
|