mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-11 10:53:52 +02:00
1592 lines
76 KiB
Vue
1592 lines
76 KiB
Vue
<template>
|
|
<div class="w-223">
|
|
<DialogHeader class="sr-only">
|
|
<DialogTitle>{{ worldDialog.ref?.name || t('dialog.world.info.header') }}</DialogTitle>
|
|
<DialogDescription>
|
|
{{ worldDialog.ref?.description || worldDialog.ref?.name || t('dialog.world.info.header') }}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<div>
|
|
<div style="display: flex">
|
|
<div style="flex: none; width: 160px; height: 120px">
|
|
<img
|
|
v-if="!worldDialog.loading"
|
|
:src="worldDialog.ref.thumbnailImageUrl"
|
|
class="cursor-pointer"
|
|
style="width: 160px; height: 120px; border-radius: 12px"
|
|
@click="showFullscreenImageDialog(worldDialog.ref.imageUrl)"
|
|
loading="lazy" />
|
|
</div>
|
|
<div style="flex: 1; display: flex; align-items: flex-start; margin-left: 15px">
|
|
<div style="flex: 1">
|
|
<div>
|
|
<span class="font-bold" style="margin-right: 5px; cursor: pointer" @click="copyWorldName">
|
|
<Home
|
|
v-if="
|
|
currentUser.$homeLocation &&
|
|
currentUser.$homeLocation.worldId === worldDialog.id
|
|
"
|
|
class="inline-block" />
|
|
{{ worldDialog.ref.name }}
|
|
</span>
|
|
</div>
|
|
<div style="margin-top: 5px">
|
|
<span
|
|
class="cursor-pointer x-grey"
|
|
style="font-family: monospace"
|
|
@click="showUserDialog(worldDialog.ref.authorId)"
|
|
v-text="worldDialog.ref.authorName" />
|
|
</div>
|
|
<div>
|
|
<Badge
|
|
v-if="worldDialog.ref.$isLabs"
|
|
variant="outline"
|
|
style="margin-right: 5px; margin-top: 5px">
|
|
{{ t('dialog.world.tags.labs') }}
|
|
</Badge>
|
|
<Badge
|
|
v-else-if="worldDialog.ref.releaseStatus === 'public'"
|
|
variant="outline"
|
|
style="margin-right: 5px; margin-top: 5px">
|
|
{{ t('dialog.world.tags.public') }}
|
|
</Badge>
|
|
<Badge v-else variant="outline" style="margin-right: 5px; margin-top: 5px">
|
|
{{ t('dialog.world.tags.private') }}
|
|
</Badge>
|
|
<TooltipWrapper v-if="worldDialog.isPC" side="top" content="PC">
|
|
<Badge
|
|
class="x-tag-platform-pc"
|
|
variant="outline"
|
|
style="margin-right: 5px; margin-top: 5px">
|
|
<Monitor class="h-4 w-4 x-tag-platform-pc" />
|
|
<span
|
|
v-if="worldDialog.fileAnalysis.standalonewindows?._fileSize"
|
|
:class="['x-grey', 'x-tag-platform-pc', 'x-tag-border-left']">
|
|
{{ worldDialog.fileAnalysis.standalonewindows._fileSize }}
|
|
</span>
|
|
</Badge>
|
|
</TooltipWrapper>
|
|
|
|
<TooltipWrapper v-if="worldDialog.isQuest" side="top" content="Quest">
|
|
<Badge
|
|
class="x-tag-platform-quest"
|
|
variant="outline"
|
|
style="margin-right: 5px; margin-top: 5px">
|
|
<Smartphone class="h-4 w-4 x-tag-platform-quest" />
|
|
<span
|
|
v-if="worldDialog.fileAnalysis.android?._fileSize"
|
|
:class="['x-grey', 'x-tag-platform-quest', 'x-tag-border-left']">
|
|
{{ worldDialog.fileAnalysis.android._fileSize }}
|
|
</span>
|
|
</Badge>
|
|
</TooltipWrapper>
|
|
|
|
<TooltipWrapper v-if="worldDialog.isIos" side="top" content="iOS">
|
|
<Badge
|
|
class="text-platform-ios border-platform-ios"
|
|
variant="outline"
|
|
style="margin-right: 5px; margin-top: 5px">
|
|
<Apple class="h-4 w-4 text-platform-ios" />
|
|
<span
|
|
v-if="worldDialog.fileAnalysis.ios?._fileSize"
|
|
:class="[
|
|
'x-grey',
|
|
'x-tag-border-left',
|
|
'text-platform-ios',
|
|
'border-platform-ios'
|
|
]">
|
|
{{ worldDialog.fileAnalysis.ios._fileSize }}
|
|
</span>
|
|
</Badge>
|
|
</TooltipWrapper>
|
|
|
|
<Badge
|
|
v-if="worldDialog.avatarScalingDisabled"
|
|
variant="outline"
|
|
style="margin-right: 5px; margin-top: 5px">
|
|
{{ t('dialog.world.tags.avatar_scaling_disabled') }}
|
|
</Badge>
|
|
<Badge
|
|
v-if="worldDialog.focusViewDisabled"
|
|
variant="outline"
|
|
style="margin-right: 5px; margin-top: 5px">
|
|
{{ t('dialog.world.tags.focus_view_disabled') }}
|
|
</Badge>
|
|
<Badge
|
|
v-if="worldDialog.ref.unityPackageUrl"
|
|
variant="outline"
|
|
style="margin-right: 5px; margin-top: 5px">
|
|
{{ t('dialog.world.tags.future_proofing') }}
|
|
</Badge>
|
|
<Badge
|
|
v-if="worldDialog.inCache"
|
|
variant="outline"
|
|
class="cursor-pointer"
|
|
style="margin-right: 5px; margin-top: 5px"
|
|
@click="openFolderGeneric(worldDialog.cachePath)">
|
|
<span v-text="worldDialog.cacheSize" />
|
|
| {{ t('dialog.world.tags.cache') }}
|
|
</Badge>
|
|
</div>
|
|
<div>
|
|
<template v-for="tag in worldDialog.ref.tags" :key="tag">
|
|
<Badge
|
|
v-if="tag.startsWith('content_')"
|
|
variant="outline"
|
|
style="margin-right: 5px; margin-top: 5px">
|
|
<span v-if="tag === 'content_horror'">
|
|
{{ t('dialog.world.tags.content_horror') }}
|
|
</span>
|
|
<span v-else-if="tag === 'content_gore'">
|
|
{{ t('dialog.world.tags.content_gore') }}
|
|
</span>
|
|
<span v-else-if="tag === 'content_violence'">
|
|
{{ t('dialog.world.tags.content_violence') }}
|
|
</span>
|
|
<span v-else-if="tag === 'content_adult'">
|
|
{{ t('dialog.world.tags.content_adult') }}
|
|
</span>
|
|
<span v-else-if="tag === 'content_sex'">
|
|
{{ t('dialog.world.tags.content_sex') }}
|
|
</span>
|
|
<span v-else>
|
|
{{ tag.replace('content_', '') }}
|
|
</span>
|
|
</Badge>
|
|
</template>
|
|
</div>
|
|
<div style="margin-top: 5px; display: flex; align-items: center">
|
|
<span
|
|
v-show="worldDialog.ref.name !== worldDialog.ref.description"
|
|
style="font-size: 12px; flex: 1; margin-right: 0.5em"
|
|
>{{ translatedDescription || worldDialog.ref.description }}</span
|
|
>
|
|
<Button
|
|
v-if="
|
|
translationApi &&
|
|
worldDialog.ref.description &&
|
|
worldDialog.ref.name !== worldDialog.ref.description
|
|
"
|
|
class="w-3 h-6 text-xs"
|
|
size="icon-sm"
|
|
variant="ghost"
|
|
@click="translateDescription">
|
|
<Spinner v-if="isTranslating" class="size-1" />
|
|
<Languages v-else class="h-3 w-3" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
<div class="ml-2 mt-12">
|
|
<TooltipWrapper
|
|
v-if="worldDialog.inCache"
|
|
side="top"
|
|
:content="t('dialog.world.actions.delete_cache_tooltip')">
|
|
<Button
|
|
class="rounded-full mr-2"
|
|
size="icon-lg"
|
|
variant="outline"
|
|
:disabled="isGameRunning && worldDialog.cacheLocked"
|
|
@click="deleteVRChatCache(worldDialog.ref)"
|
|
><Trash2
|
|
/></Button>
|
|
</TooltipWrapper>
|
|
<TooltipWrapper
|
|
v-if="worldDialog.isFavorite"
|
|
side="top"
|
|
:content="t('dialog.world.actions.favorites_tooltip')">
|
|
<Button class="rounded-full" size="icon-lg" @click="worldDialogCommand('Add Favorite')"
|
|
><Star
|
|
/></Button>
|
|
</TooltipWrapper>
|
|
<TooltipWrapper v-else side="top" :content="t('dialog.world.actions.favorites_tooltip')">
|
|
<Button
|
|
class="rounded-full"
|
|
size="icon-lg"
|
|
variant="outline"
|
|
@click="worldDialogCommand('Add Favorite')"
|
|
><Star
|
|
/></Button>
|
|
</TooltipWrapper>
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger as-child>
|
|
<Button variant="outline" size="icon-lg" class="rounded-full ml-2">
|
|
<Ellipsis />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent>
|
|
<DropdownMenuItem @click="worldDialogCommand('Refresh')">
|
|
<RefreshCw class="size-4" />
|
|
{{ t('dialog.world.actions.refresh') }}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem @click="worldDialogCommand('Share')">
|
|
<Share2 class="size-4" />
|
|
{{ t('dialog.world.actions.share') }}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem @click="worldDialogCommand('New Instance')">
|
|
<Flag class="size-4" />
|
|
{{ t('dialog.world.actions.new_instance') }}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem @click="worldDialogCommand('New Instance and Self Invite')">
|
|
<MessageSquare class="size-4" />
|
|
{{
|
|
canOpenInstanceInGame
|
|
? t('dialog.world.actions.new_instance_and_open_ingame')
|
|
: t('dialog.world.actions.new_instance_and_self_invite')
|
|
}}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem
|
|
v-if="
|
|
currentUser.$homeLocation &&
|
|
currentUser.$homeLocation.worldId === worldDialog.id
|
|
"
|
|
@click="worldDialogCommand('Reset Home')">
|
|
<Wand2 class="size-4" />
|
|
{{ t('dialog.world.actions.reset_home') }}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem v-else @click="worldDialogCommand('Make Home')">
|
|
<Home class="size-4" />
|
|
{{ t('dialog.world.actions.make_home') }}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem @click="worldDialogCommand('Previous Instances')">
|
|
<LineChart class="size-4" />
|
|
{{ t('dialog.world.actions.show_previous_instances') }}
|
|
</DropdownMenuItem>
|
|
<template v-if="currentUser.id !== worldDialog.ref.authorId">
|
|
<DropdownMenuItem
|
|
:disabled="!worldDialog.hasPersistData"
|
|
@click="worldDialogCommand('Delete Persistent Data')">
|
|
<Upload class="size-4" />
|
|
{{ t('dialog.world.actions.delete_persistent_data') }}
|
|
</DropdownMenuItem>
|
|
</template>
|
|
<template v-else>
|
|
<DropdownMenuItem @click="worldDialogCommand('Rename')">
|
|
<Pencil class="size-4" />
|
|
{{ t('dialog.world.actions.rename') }}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem @click="worldDialogCommand('Change Description')">
|
|
<Pencil class="size-4" />
|
|
{{ t('dialog.world.actions.change_description') }}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem @click="worldDialogCommand('Change Capacity')">
|
|
<Pencil class="size-4" />
|
|
{{ t('dialog.world.actions.change_capacity') }}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem @click="worldDialogCommand('Change Recommended Capacity')">
|
|
<Pencil class="size-4" />
|
|
{{ t('dialog.world.actions.change_recommended_capacity') }}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem @click="worldDialogCommand('Change YouTube Preview')">
|
|
<Pencil class="size-4" />
|
|
{{ t('dialog.world.actions.change_preview') }}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem @click="worldDialogCommand('Change Tags')">
|
|
<Pencil class="size-4" />
|
|
{{ t('dialog.world.actions.change_warnings_settings_tags') }}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem @click="worldDialogCommand('Change Allowed Domains')">
|
|
<Pencil class="size-4" />
|
|
{{ t('dialog.world.actions.change_allowed_video_player_domains') }}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem v-if="isWindows" @click="worldDialogCommand('Change Image')">
|
|
<Image class="size-4" />
|
|
{{ t('dialog.world.actions.change_image') }}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem
|
|
v-if="worldDialog.ref.unityPackageUrl"
|
|
@click="worldDialogCommand('Download Unity Package')">
|
|
<Download class="size-4" />
|
|
{{ t('dialog.world.actions.download_package') }}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem
|
|
v-if="
|
|
worldDialog.ref?.tags?.includes('system_approved') ||
|
|
worldDialog.ref?.tags?.includes('system_labs')
|
|
"
|
|
@click="worldDialogCommand('Unpublish')">
|
|
<Eye class="size-4" />
|
|
{{ t('dialog.world.actions.unpublish') }}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem v-else @click="worldDialogCommand('Publish')">
|
|
<Eye class="size-4" />
|
|
{{ t('dialog.world.actions.publish_to_labs') }}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem
|
|
:disabled="!worldDialog.hasPersistData"
|
|
@click="worldDialogCommand('Delete Persistent Data')">
|
|
<Upload class="size-4" />
|
|
{{ t('dialog.world.actions.delete_persistent_data') }}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem variant="destructive" @click="worldDialogCommand('Delete')">
|
|
<Trash2 class="size-4" />
|
|
{{ t('dialog.world.actions.delete') }}
|
|
</DropdownMenuItem>
|
|
</template>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<TabsUnderline
|
|
v-model="worldDialog.activeTab"
|
|
:items="worldDialogTabs"
|
|
:unmount-on-hide="false"
|
|
@update:modelValue="worldDialogTabClick">
|
|
<template #Instances>
|
|
<div class="flex items-center text-sm">
|
|
<User />
|
|
{{ t('dialog.world.instances.public_count', { count: worldDialog.ref.publicOccupants }) }}
|
|
<User style="margin-left: 10px" />
|
|
{{
|
|
t('dialog.world.instances.private_count', {
|
|
count: worldDialog.ref.privateOccupants
|
|
})
|
|
}}
|
|
<Check style="margin-left: 10px" />
|
|
{{
|
|
t('dialog.world.instances.capacity_count', {
|
|
count: worldDialog.ref.recommendedCapacity,
|
|
max: worldDialog.ref.capacity
|
|
})
|
|
}}
|
|
</div>
|
|
<div v-for="room in worldDialog.rooms" :key="room.id">
|
|
<template
|
|
v-if="isAgeGatedInstancesVisible || !(room.ageGate || room.location?.includes('~ageGate'))">
|
|
<div style="margin: 5px 0">
|
|
<div class="flex items-center">
|
|
<LocationWorld
|
|
class="text-sm"
|
|
:locationobject="room.$location"
|
|
:currentuserid="currentUser.id"
|
|
:worlddialogshortname="worldDialog.$location.shortName" />
|
|
<InstanceActionBar
|
|
class="ml-1 text-sm"
|
|
:location="room.$location.tag"
|
|
:launch-location="room.tag"
|
|
:instance-location="room.tag"
|
|
:shortname="room.$location.shortName"
|
|
:currentlocation="lastLocation.location"
|
|
:instance="room.ref"
|
|
:friendcount="room.friendCount"
|
|
:refresh-tooltip="t('dialog.world.instances.refresh_instance_info')"
|
|
:show-history="!!instanceJoinHistory.get(room.$location.tag)"
|
|
:history-tooltip="t('dialog.previous_instances.info')"
|
|
:on-refresh="() => refreshInstancePlayerCount(room.tag)"
|
|
:on-history="() => showPreviousInstancesInfoDialog(room.location)" />
|
|
</div>
|
|
<div
|
|
v-if="room.$location.userId || room.users.length"
|
|
class="flex flex-wrap items-start"
|
|
style="margin: 10px 0; max-height: unset">
|
|
<div
|
|
v-if="room.$location.userId"
|
|
class="box-border flex items-center p-1.5 text-[13px] cursor-pointer w-[167px] hover:rounded-[25px_5px_5px_25px]"
|
|
@click="showUserDialog(room.$location.userId)">
|
|
<template v-if="room.$location.user">
|
|
<div
|
|
class="relative inline-block flex-none size-9 mr-2.5"
|
|
:class="userStatusClass(room.$location.user)">
|
|
<img
|
|
class="size-full rounded-full object-cover"
|
|
:src="userImage(room.$location.user, true)"
|
|
loading="lazy" />
|
|
</div>
|
|
<div class="flex-1 overflow-hidden">
|
|
<span
|
|
class="block truncate font-medium leading-[18px]"
|
|
:style="{ color: room.$location.user.$userColour }"
|
|
v-text="room.$location.user.displayName" />
|
|
<span class="block truncate text-xs">
|
|
{{ t('dialog.world.instances.instance_creator') }}
|
|
</span>
|
|
</div>
|
|
</template>
|
|
<span v-else v-text="room.$location.userId" />
|
|
</div>
|
|
<div
|
|
v-for="user in room.users"
|
|
:key="user.id"
|
|
class="box-border flex items-center p-1.5 text-[13px] cursor-pointer w-[167px] hover:rounded-[25px_5px_5px_25px]"
|
|
@click="showUserDialog(user.id)">
|
|
<div
|
|
class="relative inline-block flex-none size-9 mr-2.5"
|
|
:class="userStatusClass(user)">
|
|
<img
|
|
class="size-full rounded-full object-cover"
|
|
:src="userImage(user, true)"
|
|
loading="lazy" />
|
|
</div>
|
|
<div class="flex-1 overflow-hidden">
|
|
<span
|
|
class="block truncate font-medium leading-[18px]"
|
|
:style="{ color: user.$userColour }"
|
|
v-text="user.displayName" />
|
|
<span v-if="user.location === 'traveling'" class="block truncate text-xs">
|
|
<Spinner class="inline-block mr-1" />
|
|
<Timer :epoch="user.$travelingToTime" />
|
|
</span>
|
|
<span v-else class="block truncate text-xs">
|
|
<Timer :epoch="user.$location_at" />
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
<template #Info>
|
|
<div class="flex flex-wrap items-start px-2.5" style="max-height: none">
|
|
<div class="box-border flex items-center p-1.5 text-[13px] w-full cursor-default">
|
|
<div class="flex-1 overflow-hidden">
|
|
<span class="block truncate font-medium leading-[18px]">
|
|
{{ t('dialog.world.info.memo') }}
|
|
</span>
|
|
<InputGroupTextareaField
|
|
v-model="memo"
|
|
class="text-xs"
|
|
:rows="2"
|
|
:placeholder="t('dialog.world.info.memo_placeholder')"
|
|
input-class="resize-none min-h-0"
|
|
@change="onWorldMemoChange" />
|
|
</div>
|
|
</div>
|
|
<div style="width: 100%; display: flex">
|
|
<div class="box-border flex items-center p-1.5 text-[13px] w-full cursor-default">
|
|
<div class="flex-1 overflow-hidden">
|
|
<span class="block truncate font-medium leading-[18px]">
|
|
{{ t('dialog.world.info.id') }}
|
|
</span>
|
|
<span class="block truncate text-xs" style="display: inline">
|
|
{{ worldDialog.id }}
|
|
</span>
|
|
<TooltipWrapper side="top" :content="t('dialog.world.info.id_tooltip')">
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger as-child>
|
|
<Button
|
|
class="rounded-full text-xs"
|
|
size="icon-sm"
|
|
variant="ghost"
|
|
@click.stop
|
|
><Copy class="h-4 w-4" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent>
|
|
<DropdownMenuItem @click="copyWorldId()">
|
|
{{ t('dialog.world.info.copy_id') }}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem @click="copyWorldUrl()">
|
|
{{ t('dialog.world.info.copy_url') }}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem @click="copyWorldName()">
|
|
{{ t('dialog.world.info.copy_name') }}
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</TooltipWrapper>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div
|
|
v-if="worldDialog.ref.previewYoutubeId"
|
|
class="box-border flex items-center p-1.5 text-[13px] cursor-pointer"
|
|
style="width: 350px"
|
|
@click="
|
|
openExternalLink(`https://www.youtube.com/watch?v=${worldDialog.ref.previewYoutubeId}`)
|
|
">
|
|
<div class="flex-1 overflow-hidden">
|
|
<span class="block truncate font-medium leading-[18px]">
|
|
{{ t('dialog.world.info.youtube_preview') }}
|
|
</span>
|
|
<span class="block truncate text-xs">
|
|
https://www.youtube.com/watch?v={{ worldDialog.ref.previewYoutubeId }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div class="box-border flex items-center p-1.5 text-[13px] w-full cursor-default">
|
|
<div class="flex-1 overflow-hidden">
|
|
<span class="block truncate font-medium leading-[18px]">
|
|
{{ t('dialog.world.info.author_tags') }}
|
|
</span>
|
|
<span
|
|
v-if="
|
|
worldDialog.ref.tags?.filter((tag) => tag.startsWith('author_tag')).length > 0
|
|
"
|
|
class="block truncate text-xs">
|
|
{{ worldTags }}
|
|
</span>
|
|
<span v-else class="block truncate text-xs"> - </span>
|
|
</div>
|
|
</div>
|
|
<div class="box-border flex items-center p-1.5 text-[13px] cursor-default w-[167px]">
|
|
<div class="flex-1 overflow-hidden">
|
|
<span class="block truncate font-medium leading-[18px]">
|
|
{{ t('dialog.world.info.players') }}
|
|
</span>
|
|
<span class="block truncate text-xs">
|
|
{{ commaNumber(worldDialog.ref.occupants) }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div class="box-border flex items-center p-1.5 text-[13px] cursor-default w-[167px]">
|
|
<div class="flex-1 overflow-hidden">
|
|
<span class="block truncate font-medium leading-[18px]">
|
|
{{ t('dialog.world.info.favorites') }}
|
|
</span>
|
|
<span class="block truncate text-xs">
|
|
{{ commaNumber(worldDialog.ref.favorites)
|
|
}}<span
|
|
v-if="worldDialog.ref?.favorites > 0 && worldDialog.ref?.visits > 0"
|
|
class="text-xs">
|
|
({{ favoriteRate }}%)
|
|
</span>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div class="box-border flex items-center p-1.5 text-[13px] cursor-default w-[167px]">
|
|
<div class="flex-1 overflow-hidden">
|
|
<span class="block truncate font-medium leading-[18px]">
|
|
{{ t('dialog.world.info.visits') }}
|
|
</span>
|
|
<span class="block truncate text-xs">
|
|
{{ commaNumber(worldDialog.ref.visits) }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div class="box-border flex items-center p-1.5 text-[13px] cursor-default w-[167px]">
|
|
<div class="flex-1 overflow-hidden">
|
|
<span class="block truncate font-medium leading-[18px]">
|
|
{{ t('dialog.world.info.capacity') }}
|
|
</span>
|
|
<span class="block truncate text-xs">
|
|
{{ commaNumber(worldDialog.ref.recommendedCapacity) }} ({{
|
|
commaNumber(worldDialog.ref.capacity)
|
|
}})
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div class="box-border flex items-center p-1.5 text-[13px] cursor-default w-[167px]">
|
|
<div class="flex-1 overflow-hidden">
|
|
<span class="block truncate font-medium leading-[18px]">
|
|
{{ t('dialog.world.info.created_at') }}
|
|
</span>
|
|
<span class="block truncate text-xs">
|
|
{{ formatDateFilter(worldDialog.ref.created_at, 'long') }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div class="box-border flex items-center p-1.5 text-[13px] cursor-default w-[167px]">
|
|
<div class="flex-1 overflow-hidden">
|
|
<span class="block truncate font-medium leading-[18px]" style="display: inline">
|
|
{{ t('dialog.world.info.last_updated') }}
|
|
</span>
|
|
<TooltipWrapper
|
|
v-if="Object.keys(worldDialog.fileAnalysis).length"
|
|
side="top"
|
|
style="margin-left: 5px">
|
|
<template #content>
|
|
<template
|
|
v-for="(created_at, platform) in worldDialogPlatformCreatedAt"
|
|
:key="platform">
|
|
<div class="flex justify-between w-full">
|
|
<span class="mr-1">{{ platform }}:</span>
|
|
<span>{{ formatDateFilter(created_at, 'long') }}</span>
|
|
</div>
|
|
</template>
|
|
</template>
|
|
<ChevronDown class="inline-block" />
|
|
</TooltipWrapper>
|
|
<span class="block truncate text-xs">
|
|
{{ formatDateFilter(worldDialog.ref.updated_at, 'long') }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div
|
|
v-if="worldDialog.ref.labsPublicationDate !== 'none'"
|
|
class="box-border flex items-center p-1.5 text-[13px] cursor-default w-[167px]">
|
|
<div class="flex-1 overflow-hidden">
|
|
<span class="block truncate font-medium leading-[18px]">
|
|
{{ t('dialog.world.info.labs_publication_date') }}
|
|
</span>
|
|
<span class="block truncate text-xs">
|
|
{{ formatDateFilter(worldDialog.ref.labsPublicationDate, 'long') }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div
|
|
v-if="worldDialog.ref.publicationDate !== 'none'"
|
|
class="box-border flex items-center p-1.5 text-[13px] cursor-default w-[167px]">
|
|
<div class="flex-1 overflow-hidden">
|
|
<span class="block truncate font-medium leading-[18px]" style="display: inline">
|
|
{{ t('dialog.world.info.publication_date') }}
|
|
</span>
|
|
<TooltipWrapper v-if="isTimeInLabVisible" side="top" style="margin-left: 5px">
|
|
<template #content>
|
|
<span>
|
|
{{ t('dialog.world.info.time_in_labs') }}
|
|
{{ timeInLab }}
|
|
</span>
|
|
</template>
|
|
<ChevronDown class="inline-block" />
|
|
</TooltipWrapper>
|
|
<span class="block truncate text-xs">
|
|
{{ formatDateFilter(worldDialog.ref.publicationDate, 'long') }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div class="box-border flex items-center p-1.5 text-[13px] cursor-default w-[167px]">
|
|
<div class="flex-1 overflow-hidden">
|
|
<span class="block truncate font-medium leading-[18px]">
|
|
{{ t('dialog.world.info.version') }}
|
|
</span>
|
|
<span class="block truncate text-xs" v-text="worldDialog.ref.version" />
|
|
</div>
|
|
</div>
|
|
<div class="box-border flex items-center p-1.5 text-[13px] cursor-default w-[167px]">
|
|
<div class="flex-1 overflow-hidden">
|
|
<span class="block truncate font-medium leading-[18px]">
|
|
{{ t('dialog.world.info.heat') }}
|
|
</span>
|
|
<span class="block truncate text-xs">
|
|
{{ commaNumber(worldDialog.ref.heat) }} {{ '🔥'.repeat(worldDialog.ref.heat) }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div class="box-border flex items-center p-1.5 text-[13px] cursor-default w-[167px]">
|
|
<div class="flex-1 overflow-hidden">
|
|
<span class="block truncate font-medium leading-[18px]">
|
|
{{ t('dialog.world.info.popularity') }}
|
|
</span>
|
|
<span class="block truncate text-xs">
|
|
{{ commaNumber(worldDialog.ref.popularity) }}
|
|
{{ '💖'.repeat(worldDialog.ref.popularity) }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div class="box-border flex items-center p-1.5 text-[13px] w-full cursor-default">
|
|
<div class="flex-1 overflow-hidden">
|
|
<span class="block truncate font-medium leading-[18px]">
|
|
{{ t('dialog.world.info.platform') }}
|
|
</span>
|
|
<span class="block truncate text-xs" style="white-space: normal">{{
|
|
worldDialogPlatform
|
|
}}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="box-border flex items-center p-1.5 text-[13px] cursor-default w-[167px]">
|
|
<div class="flex-1 overflow-hidden">
|
|
<span class="block truncate font-medium leading-[18px]">
|
|
{{ t('dialog.world.info.last_visited') }}
|
|
</span>
|
|
<span class="block truncate text-xs">{{
|
|
formatDateFilter(worldDialog.lastVisit, 'long')
|
|
}}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
class="box-border flex items-center p-1.5 text-[13px] cursor-pointer w-[167px]"
|
|
@click="showPreviousInstancesListDialog(worldDialog.ref)">
|
|
<div class="flex-1 overflow-hidden">
|
|
<div
|
|
class="block truncate font-medium leading-[18px]"
|
|
style="display: flex; justify-content: space-between; align-items: center">
|
|
<div>
|
|
{{ t('dialog.world.info.visit_count') }}
|
|
</div>
|
|
|
|
<TooltipWrapper side="top" :content="t('dialog.user.info.open_previous_instance')">
|
|
<MoreHorizontal style="margin-right: 16px" />
|
|
</TooltipWrapper>
|
|
</div>
|
|
<span v-if="worldDialog.visitCount === 0" class="block truncate text-xs">-</span>
|
|
<span v-else class="block truncate text-xs" v-text="worldDialog.visitCount"></span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="box-border flex items-center p-1.5 text-[13px] cursor-default w-[167px]">
|
|
<div class="flex-1 overflow-hidden">
|
|
<span class="block truncate font-medium leading-[18px]">
|
|
{{ t('dialog.world.info.time_spent') }}
|
|
</span>
|
|
<span class="block truncate text-xs">
|
|
{{ worldDialog.timeSpent === 0 ? ' - ' : timeSpent }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<template #JSON>
|
|
<Button
|
|
class="rounded-full mr-2"
|
|
size="icon-sm"
|
|
variant="ghost"
|
|
@click="refreshWorldDialogTreeData()">
|
|
<RefreshCw />
|
|
</Button>
|
|
<Button
|
|
class="rounded-full"
|
|
size="icon-sm"
|
|
variant="ghost"
|
|
@click="downloadAndSaveJson(worldDialog.id, worldDialog.ref)">
|
|
<Download />
|
|
</Button>
|
|
<vue-json-pretty
|
|
:key="treeData?.id"
|
|
:data="treeData"
|
|
:deep="2"
|
|
:theme="isDarkMode ? 'dark' : 'light'"
|
|
show-icon />
|
|
<br />
|
|
<vue-json-pretty
|
|
v-if="Object.keys(worldDialog.fileAnalysis).length"
|
|
:data="worldDialog.fileAnalysis"
|
|
:deep="2"
|
|
:theme="isDarkMode ? 'dark' : 'light'"
|
|
show-icon />
|
|
</template>
|
|
</TabsUnderline>
|
|
</div>
|
|
|
|
<template v-if="isDialogVisible">
|
|
<WorldAllowedDomainsDialog :world-allowed-domains-dialog="worldAllowedDomainsDialog" />
|
|
<SetWorldTagsDialog
|
|
v-model:is-set-world-tags-dialog-visible="isSetWorldTagsDialogVisible"
|
|
:old-tags="worldDialog.ref?.tags"
|
|
:world-id="worldDialog.id"
|
|
:is-world-dialog-visible="worldDialog.visible" />
|
|
<NewInstanceDialog
|
|
:new-instance-dialog-location-tag="newInstanceDialogLocationTag"
|
|
:last-location="lastLocation" />
|
|
<input
|
|
id="WorldImageUploadButton"
|
|
type="file"
|
|
accept="image/*"
|
|
style="display: none"
|
|
@change="onFileChangeWorldImage" />
|
|
<ImageCropDialog
|
|
:open="cropDialogOpen"
|
|
:title="t('dialog.change_content_image.world')"
|
|
:aspect-ratio="4 / 3"
|
|
:file="cropDialogFile"
|
|
@update:open="cropDialogOpen = $event"
|
|
@confirm="onCropConfirmWorld" />
|
|
</template>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import {
|
|
Apple,
|
|
Check,
|
|
ChevronDown,
|
|
Copy,
|
|
Download,
|
|
Ellipsis,
|
|
Eye,
|
|
Flag,
|
|
Home,
|
|
Image,
|
|
Languages,
|
|
LineChart,
|
|
MessageSquare,
|
|
Monitor,
|
|
MoreHorizontal,
|
|
Pencil,
|
|
RefreshCw,
|
|
Share2,
|
|
Smartphone,
|
|
Star,
|
|
Trash2,
|
|
Upload,
|
|
User,
|
|
Wand2
|
|
} from 'lucide-vue-next';
|
|
import { computed, defineAsyncComponent, nextTick, ref, watch } from 'vue';
|
|
import { DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
|
import { Button } from '@/components/ui/button';
|
|
import { InputGroupTextareaField } from '@/components/ui/input-group';
|
|
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';
|
|
|
|
import VueJsonPretty from 'vue-json-pretty';
|
|
|
|
import {
|
|
commaNumber,
|
|
compareUnityVersion,
|
|
deleteVRChatCache,
|
|
downloadAndSaveJson,
|
|
formatDateFilter,
|
|
openExternalLink,
|
|
openFolderGeneric,
|
|
refreshInstancePlayerCount,
|
|
replaceVrcPackageUrl,
|
|
timeToText,
|
|
userImage,
|
|
userStatusClass
|
|
} from '../../../shared/utils';
|
|
import {
|
|
useAdvancedSettingsStore,
|
|
useAppearanceSettingsStore,
|
|
useFavoriteStore,
|
|
useGalleryStore,
|
|
useGameStore,
|
|
useInstanceStore,
|
|
useInviteStore,
|
|
useLocationStore,
|
|
useModalStore,
|
|
useUserStore,
|
|
useWorldStore
|
|
} from '../../../stores';
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger
|
|
} from '../../ui/dropdown-menu';
|
|
import {
|
|
handleImageUploadInput,
|
|
readFileAsBase64,
|
|
resizeImageToFitLimits,
|
|
uploadImageLegacy,
|
|
withUploadTimeout
|
|
} from '../../../shared/utils/imageUpload';
|
|
import { favoriteRequest, miscRequest, userRequest, worldRequest } from '../../../api';
|
|
import { Badge } from '../../ui/badge';
|
|
import { database } from '../../../service/database';
|
|
import { formatJsonVars } from '../../../shared/utils/base/ui';
|
|
|
|
import ImageCropDialog from '../ImageCropDialog.vue';
|
|
import InstanceActionBar from '../../InstanceActionBar.vue';
|
|
|
|
const SetWorldTagsDialog = defineAsyncComponent(() => import('./SetWorldTagsDialog.vue'));
|
|
const WorldAllowedDomainsDialog = defineAsyncComponent(() => import('./WorldAllowedDomainsDialog.vue'));
|
|
const NewInstanceDialog = defineAsyncComponent(() => import('../NewInstanceDialog.vue'));
|
|
|
|
const { isAgeGatedInstancesVisible, isDarkMode } = storeToRefs(useAppearanceSettingsStore());
|
|
const { showUserDialog } = useUserStore();
|
|
const { currentUser, userDialog } = storeToRefs(useUserStore());
|
|
const { worldDialog } = storeToRefs(useWorldStore());
|
|
const { cachedWorlds, showWorldDialog } = useWorldStore();
|
|
const { lastLocation } = storeToRefs(useLocationStore());
|
|
const { newInstanceSelfInvite, canOpenInstanceInGame } = useInviteStore();
|
|
const { showFavoriteDialog } = useFavoriteStore();
|
|
const { showPreviousInstancesInfoDialog, showPreviousInstancesListDialog: openPreviousInstancesListDialog } =
|
|
useInstanceStore();
|
|
const { instanceJoinHistory } = storeToRefs(useInstanceStore());
|
|
const { isGameRunning } = storeToRefs(useGameStore());
|
|
const { showFullscreenImageDialog } = useGalleryStore();
|
|
const { translationApi } = storeToRefs(useAdvancedSettingsStore());
|
|
const modalStore = useModalStore();
|
|
|
|
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({
|
|
visible: false,
|
|
worldId: '',
|
|
urlList: []
|
|
});
|
|
const isSetWorldTagsDialogVisible = ref(false);
|
|
const newInstanceDialogLocationTag = ref('');
|
|
const cropDialogOpen = ref(false);
|
|
const cropDialogFile = ref(null);
|
|
const changeWorldImageLoading = ref(false);
|
|
const translatedDescription = ref('');
|
|
const isTranslating = ref(false);
|
|
|
|
const isDialogVisible = computed({
|
|
get() {
|
|
return worldDialog.value.visible;
|
|
},
|
|
set(value) {
|
|
worldDialog.value.visible = value;
|
|
}
|
|
});
|
|
|
|
const isWindows = computed(() => WINDOWS);
|
|
|
|
const memo = computed({
|
|
get() {
|
|
return worldDialog.value.memo;
|
|
},
|
|
set(value) {
|
|
worldDialog.value.memo = value;
|
|
}
|
|
});
|
|
|
|
const isTimeInLabVisible = computed(() => {
|
|
return (
|
|
worldDialog.value.ref.publicationDate &&
|
|
worldDialog.value.ref.publicationDate !== 'none' &&
|
|
worldDialog.value.ref.labsPublicationDate &&
|
|
worldDialog.value.ref.labsPublicationDate !== 'none'
|
|
);
|
|
});
|
|
|
|
const timeInLab = computed(() => {
|
|
return timeToText(
|
|
new Date(worldDialog.value.ref.publicationDate).getTime() -
|
|
new Date(worldDialog.value.ref.labsPublicationDate).getTime()
|
|
);
|
|
});
|
|
|
|
const favoriteRate = computed(() => {
|
|
return (
|
|
Math.round(
|
|
(((worldDialog.value.ref?.favorites - worldDialog.value.ref?.visits) / worldDialog.value.ref?.visits) *
|
|
100 +
|
|
100) *
|
|
100
|
|
) / 100
|
|
);
|
|
});
|
|
|
|
const worldTags = computed(() => {
|
|
return worldDialog.value.ref?.tags
|
|
.filter((tag) => tag.startsWith('author_tag'))
|
|
.map((tag) => tag.replace('author_tag_', ''))
|
|
.join(', ');
|
|
});
|
|
|
|
const timeSpent = computed(() => {
|
|
return timeToText(worldDialog.value.timeSpent);
|
|
});
|
|
|
|
const worldDialogPlatform = computed(() => {
|
|
const { ref } = worldDialog.value;
|
|
const platforms = [];
|
|
if (ref.unityPackages) {
|
|
for (const unityPackage of ref.unityPackages) {
|
|
if (!compareUnityVersion(unityPackage.unitySortNumber)) {
|
|
continue;
|
|
}
|
|
let platform = 'PC';
|
|
if (unityPackage.platform === 'standalonewindows') {
|
|
platform = 'PC';
|
|
} else if (unityPackage.platform === 'android') {
|
|
platform = 'Android';
|
|
} else if (unityPackage.platform) {
|
|
platform = unityPackage.platform;
|
|
}
|
|
platforms.unshift(`${platform}/${unityPackage.unityVersion}`);
|
|
}
|
|
}
|
|
return platforms.join(', ');
|
|
});
|
|
|
|
const worldDialogPlatformCreatedAt = computed(() => {
|
|
const { ref } = worldDialog.value;
|
|
if (!ref.unityPackages) {
|
|
return null;
|
|
}
|
|
let newest = {};
|
|
for (const unityPackage of ref.unityPackages) {
|
|
if (unityPackage.variant && unityPackage.variant !== 'standard' && unityPackage.variant !== 'security') {
|
|
continue;
|
|
}
|
|
const platform = unityPackage.platform;
|
|
const createdAt = unityPackage.created_at;
|
|
if (!newest[platform] || new Date(createdAt) > new Date(newest[platform])) {
|
|
newest[platform] = createdAt;
|
|
}
|
|
}
|
|
return newest;
|
|
});
|
|
|
|
watch(
|
|
() => worldDialog.value.loading,
|
|
() => {
|
|
if (worldDialog.value.visible) {
|
|
handleDialogOpen();
|
|
!worldDialog.value.loading && loadLastActiveTab();
|
|
}
|
|
}
|
|
);
|
|
|
|
/**
|
|
*
|
|
* @param tabName
|
|
*/
|
|
function handleWorldDialogTab(tabName) {
|
|
worldDialog.value.lastActiveTab = tabName;
|
|
if (tabName === 'JSON') {
|
|
refreshWorldDialogTreeData();
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
*/
|
|
function loadLastActiveTab() {
|
|
handleWorldDialogTab(worldDialog.value.lastActiveTab);
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param tabName
|
|
*/
|
|
function worldDialogTabClick(tabName) {
|
|
if (tabName === worldDialog.value.lastActiveTab) {
|
|
if (tabName === 'JSON') {
|
|
refreshWorldDialogTreeData();
|
|
}
|
|
return;
|
|
}
|
|
handleWorldDialogTab(tabName);
|
|
}
|
|
|
|
/**
|
|
*
|
|
*/
|
|
function handleDialogOpen() {
|
|
treeData.value = {};
|
|
}
|
|
|
|
/**
|
|
*
|
|
*/
|
|
function showChangeWorldImageDialog() {
|
|
document.getElementById('WorldImageUploadButton').click();
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param e
|
|
*/
|
|
function onFileChangeWorldImage(e) {
|
|
const { file, clearInput } = handleImageUploadInput(e, {
|
|
inputSelector: '#WorldImageUploadButton',
|
|
tooLargeMessage: () => t('message.file.too_large'),
|
|
invalidTypeMessage: () => t('message.file.not_image')
|
|
});
|
|
if (!file) {
|
|
return;
|
|
}
|
|
if (!worldDialog.value.visible || worldDialog.value.loading) {
|
|
clearInput();
|
|
return;
|
|
}
|
|
clearInput();
|
|
cropDialogFile.value = file;
|
|
cropDialogOpen.value = true;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param blob
|
|
*/
|
|
async function onCropConfirmWorld(blob) {
|
|
changeWorldImageLoading.value = true;
|
|
try {
|
|
await withUploadTimeout(
|
|
(async () => {
|
|
const base64Body = await readFileAsBase64(blob);
|
|
const base64File = await resizeImageToFitLimits(base64Body);
|
|
await uploadImageLegacy('world', {
|
|
entityId: worldDialog.value.id,
|
|
imageUrl: worldDialog.value.ref.imageUrl,
|
|
base64File,
|
|
blob
|
|
});
|
|
})()
|
|
);
|
|
toast.success(t('message.upload.success'));
|
|
} catch (error) {
|
|
console.error('World image upload process failed:', error);
|
|
toast.error(t('message.upload.error'));
|
|
} finally {
|
|
changeWorldImageLoading.value = false;
|
|
cropDialogOpen.value = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param tag
|
|
*/
|
|
function showNewInstanceDialog(tag) {
|
|
// trigger watcher
|
|
newInstanceDialogLocationTag.value = '';
|
|
nextTick(() => (newInstanceDialogLocationTag.value = tag));
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param command
|
|
*/
|
|
function worldDialogCommand(command) {
|
|
const D = worldDialog.value;
|
|
if (D.visible === false) {
|
|
return;
|
|
}
|
|
switch (command) {
|
|
case 'Delete Favorite':
|
|
case 'Make Home':
|
|
case 'Reset Home':
|
|
case 'Publish':
|
|
case 'Unpublish':
|
|
case 'Delete Persistent Data':
|
|
case 'Delete':
|
|
const commandLabelMap = {
|
|
'Delete Favorite': t('dialog.world.actions.favorites_tooltip'),
|
|
'Make Home': t('dialog.world.actions.make_home'),
|
|
'Reset Home': t('dialog.world.actions.reset_home'),
|
|
Publish: t('dialog.world.actions.publish_to_labs'),
|
|
Unpublish: t('dialog.world.actions.unpublish'),
|
|
'Delete Persistent Data': t('dialog.world.actions.delete_persistent_data'),
|
|
Delete: t('dialog.world.actions.delete')
|
|
};
|
|
modalStore
|
|
.confirm({
|
|
description: t('confirm.command_question', {
|
|
command: commandLabelMap[command] ?? command
|
|
}),
|
|
title: t('confirm.title')
|
|
})
|
|
.then(({ ok }) => {
|
|
if (!ok) return;
|
|
switch (command) {
|
|
case 'Delete Favorite':
|
|
favoriteRequest.deleteFavorite({
|
|
objectId: D.id
|
|
});
|
|
break;
|
|
case 'Make Home':
|
|
userRequest
|
|
.saveCurrentUser({
|
|
homeLocation: D.id
|
|
})
|
|
.then((args) => {
|
|
toast.success(t('message.world.home_updated'));
|
|
return args;
|
|
});
|
|
break;
|
|
case 'Reset Home':
|
|
userRequest
|
|
.saveCurrentUser({
|
|
homeLocation: ''
|
|
})
|
|
.then((args) => {
|
|
toast.success(t('message.world.home_reset'));
|
|
return args;
|
|
});
|
|
break;
|
|
case 'Publish':
|
|
worldRequest
|
|
.publishWorld({
|
|
worldId: D.id
|
|
})
|
|
.then((args) => {
|
|
toast.success(t('message.world.published'));
|
|
return args;
|
|
});
|
|
break;
|
|
case 'Unpublish':
|
|
worldRequest
|
|
.unpublishWorld({
|
|
worldId: D.id
|
|
})
|
|
.then((args) => {
|
|
toast.success(t('message.world.unpublished'));
|
|
return args;
|
|
});
|
|
break;
|
|
case 'Delete Persistent Data':
|
|
miscRequest
|
|
.deleteWorldPersistData({
|
|
worldId: D.id
|
|
})
|
|
.then((args) => {
|
|
if (args.params.worldId === worldDialog.value.id && worldDialog.value.visible) {
|
|
worldDialog.value.hasPersistData = false;
|
|
}
|
|
toast.success(t('message.world.persistent_data_deleted'));
|
|
return args;
|
|
});
|
|
break;
|
|
case 'Delete':
|
|
worldRequest
|
|
.deleteWorld({
|
|
worldId: D.id
|
|
})
|
|
.then((args) => {
|
|
const { json } = args;
|
|
cachedWorlds.delete(json.id);
|
|
if (worldDialog.value.ref.authorId === json.authorId) {
|
|
const map = new Map();
|
|
for (const ref of cachedWorlds.values()) {
|
|
if (ref.authorId === json.authorId) {
|
|
map.set(ref.id, ref);
|
|
}
|
|
}
|
|
const array = Array.from(map.values());
|
|
userDialog.value.worlds = array;
|
|
}
|
|
toast.success(t('message.world.deleted'));
|
|
D.visible = false;
|
|
return args;
|
|
});
|
|
break;
|
|
}
|
|
})
|
|
.catch(() => {});
|
|
break;
|
|
case 'Previous Instances':
|
|
showPreviousInstancesListDialog(D.ref);
|
|
break;
|
|
case 'Share':
|
|
copyWorldUrl();
|
|
break;
|
|
case 'Change Allowed Domains':
|
|
showWorldAllowedDomainsDialog();
|
|
break;
|
|
case 'Change Tags':
|
|
isSetWorldTagsDialogVisible.value = true;
|
|
break;
|
|
case 'Download Unity Package':
|
|
openExternalLink(replaceVrcPackageUrl(worldDialog.value.ref.unityPackageUrl));
|
|
break;
|
|
case 'Change Image':
|
|
showChangeWorldImageDialog();
|
|
break;
|
|
case 'Refresh':
|
|
const { tag, shortName } = worldDialog.value.$location;
|
|
showWorldDialog(tag, shortName, { forceRefresh: true });
|
|
break;
|
|
case 'New Instance':
|
|
showNewInstanceDialog(D.$location.tag);
|
|
break;
|
|
case 'Add Favorite':
|
|
showFavoriteDialog('world', D.id);
|
|
break;
|
|
case 'New Instance and Self Invite':
|
|
newInstanceSelfInvite(D.id);
|
|
break;
|
|
case 'Rename':
|
|
promptRenameWorld(D);
|
|
break;
|
|
case 'Change Description':
|
|
promptChangeWorldDescription(D);
|
|
break;
|
|
case 'Change Capacity':
|
|
promptChangeWorldCapacity(D);
|
|
break;
|
|
case 'Change Recommended Capacity':
|
|
promptChangeWorldRecommendedCapacity(D);
|
|
break;
|
|
case 'Change YouTube Preview':
|
|
promptChangeWorldYouTubePreview(D);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param world
|
|
*/
|
|
function promptRenameWorld(world) {
|
|
modalStore
|
|
.prompt({
|
|
title: t('prompt.rename_world.header'),
|
|
description: t('prompt.rename_world.description'),
|
|
confirmText: t('prompt.rename_world.ok'),
|
|
cancelText: t('prompt.rename_world.cancel'),
|
|
inputValue: world.ref.name,
|
|
errorMessage: t('prompt.rename_world.input_error')
|
|
})
|
|
.then(({ ok, value }) => {
|
|
if (!ok) return;
|
|
if (value && value !== world.ref.name) {
|
|
worldRequest
|
|
.saveWorld({
|
|
id: world.id,
|
|
name: value
|
|
})
|
|
.then((args) => {
|
|
toast.success(t('prompt.rename_world.message.success'));
|
|
return args;
|
|
});
|
|
}
|
|
})
|
|
.catch(() => {});
|
|
}
|
|
/**
|
|
*
|
|
* @param world
|
|
*/
|
|
function promptChangeWorldDescription(world) {
|
|
modalStore
|
|
.prompt({
|
|
title: t('prompt.change_world_description.header'),
|
|
description: t('prompt.change_world_description.description'),
|
|
confirmText: t('prompt.change_world_description.ok'),
|
|
cancelText: t('prompt.change_world_description.cancel'),
|
|
inputValue: world.ref.description,
|
|
errorMessage: t('prompt.change_world_description.input_error')
|
|
})
|
|
.then(({ ok, value }) => {
|
|
if (!ok) return;
|
|
if (value && value !== world.ref.description) {
|
|
worldRequest
|
|
.saveWorld({
|
|
id: world.id,
|
|
description: value
|
|
})
|
|
.then((args) => {
|
|
toast.success(t('prompt.change_world_description.message.success'));
|
|
return args;
|
|
});
|
|
}
|
|
})
|
|
.catch(() => {});
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param world
|
|
*/
|
|
function promptChangeWorldCapacity(world) {
|
|
modalStore
|
|
.prompt({
|
|
title: t('prompt.change_world_capacity.header'),
|
|
description: t('prompt.change_world_capacity.description'),
|
|
confirmText: t('prompt.change_world_capacity.ok'),
|
|
cancelText: t('prompt.change_world_capacity.cancel'),
|
|
inputValue: world.ref.capacity,
|
|
pattern: /\d+$/,
|
|
errorMessage: t('prompt.change_world_capacity.input_error')
|
|
})
|
|
.then(({ ok, value }) => {
|
|
if (!ok) return;
|
|
if (value && value !== world.ref.capacity) {
|
|
worldRequest
|
|
.saveWorld({
|
|
id: world.id,
|
|
capacity: Number(value)
|
|
})
|
|
.then((args) => {
|
|
toast.success(t('prompt.change_world_capacity.message.success'));
|
|
return args;
|
|
});
|
|
}
|
|
})
|
|
.catch(() => {});
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param world
|
|
*/
|
|
function promptChangeWorldRecommendedCapacity(world) {
|
|
modalStore
|
|
.prompt({
|
|
title: t('prompt.change_world_recommended_capacity.header'),
|
|
description: t('prompt.change_world_recommended_capacity.description'),
|
|
confirmText: t('prompt.change_world_capacity.ok'),
|
|
cancelText: t('prompt.change_world_capacity.cancel'),
|
|
inputValue: world.ref.recommendedCapacity,
|
|
pattern: /\d+$/,
|
|
errorMessage: t('prompt.change_world_recommended_capacity.input_error')
|
|
})
|
|
.then(({ ok, value }) => {
|
|
if (!ok) return;
|
|
if (value && value !== world.ref.recommendedCapacity) {
|
|
worldRequest
|
|
.saveWorld({
|
|
id: world.id,
|
|
recommendedCapacity: Number(value)
|
|
})
|
|
.then((args) => {
|
|
toast.success(t('prompt.change_world_recommended_capacity.message.success'));
|
|
return args;
|
|
});
|
|
}
|
|
})
|
|
.catch(() => {});
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param world
|
|
*/
|
|
function promptChangeWorldYouTubePreview(world) {
|
|
modalStore
|
|
.prompt({
|
|
title: t('prompt.change_world_preview.header'),
|
|
description: t('prompt.change_world_preview.description'),
|
|
confirmText: t('prompt.change_world_preview.ok'),
|
|
cancelText: t('prompt.change_world_preview.cancel'),
|
|
inputValue: world.ref.previewYoutubeId,
|
|
errorMessage: t('prompt.change_world_preview.input_error')
|
|
})
|
|
.then(({ ok, value }) => {
|
|
if (!ok) return;
|
|
if (value && value !== world.ref.previewYoutubeId) {
|
|
let processedValue = value;
|
|
if (value.length > 11) {
|
|
try {
|
|
const url = new URL(value);
|
|
const id1 = url.pathname;
|
|
const id2 = url.searchParams.get('v');
|
|
if (id1 && id1.length === 12) {
|
|
processedValue = id1.substring(1, 12);
|
|
}
|
|
if (id2 && id2.length === 11) {
|
|
processedValue = id2;
|
|
}
|
|
} catch {
|
|
toast.error(t('prompt.change_world_preview.message.error'));
|
|
return;
|
|
}
|
|
}
|
|
if (processedValue !== world.ref.previewYoutubeId) {
|
|
worldRequest
|
|
.saveWorld({
|
|
id: world.id,
|
|
previewYoutubeId: processedValue
|
|
})
|
|
.then((args) => {
|
|
toast.success(t('prompt.change_world_preview.message.success'));
|
|
return args;
|
|
});
|
|
}
|
|
}
|
|
})
|
|
.catch(() => {});
|
|
}
|
|
/**
|
|
*
|
|
*/
|
|
function onWorldMemoChange() {
|
|
const worldId = worldDialog.value.id;
|
|
const memo = worldDialog.value.memo;
|
|
if (memo) {
|
|
database.setWorldMemo({
|
|
worldId,
|
|
editedAt: new Date().toJSON(),
|
|
memo
|
|
});
|
|
} else {
|
|
database.deleteWorldMemo(worldId);
|
|
}
|
|
}
|
|
/**
|
|
*
|
|
* @param worldRef
|
|
*/
|
|
function showPreviousInstancesListDialog(worldRef) {
|
|
openPreviousInstancesListDialog('world', worldRef);
|
|
}
|
|
/**
|
|
*
|
|
*/
|
|
function refreshWorldDialogTreeData() {
|
|
treeData.value = formatJsonVars(worldDialog.value.ref);
|
|
}
|
|
/**
|
|
*
|
|
*/
|
|
function copyWorldId() {
|
|
navigator.clipboard
|
|
.writeText(worldDialog.value.id)
|
|
.then(() => {
|
|
toast.success(t('message.world.id_copied'));
|
|
})
|
|
.catch((err) => {
|
|
console.error('copy failed:', err);
|
|
toast.error(t('message.copy_failed'));
|
|
});
|
|
}
|
|
/**
|
|
*
|
|
*/
|
|
function copyWorldUrl() {
|
|
navigator.clipboard
|
|
.writeText(`https://vrchat.com/home/world/${worldDialog.value.id}`)
|
|
.then(() => {
|
|
toast.success(t('message.world.url_copied'));
|
|
})
|
|
.catch((err) => {
|
|
console.error('copy failed:', err);
|
|
toast.error(t('message.copy_failed'));
|
|
});
|
|
}
|
|
/**
|
|
*
|
|
*/
|
|
function copyWorldName() {
|
|
navigator.clipboard
|
|
.writeText(worldDialog.value.ref.name)
|
|
.then(() => {
|
|
toast.success(t('message.world.name_copied'));
|
|
})
|
|
.catch((err) => {
|
|
console.error('copy failed:', err);
|
|
toast.error(t('message.copy_failed'));
|
|
});
|
|
}
|
|
/**
|
|
*
|
|
*/
|
|
function showWorldAllowedDomainsDialog() {
|
|
const D = worldAllowedDomainsDialog.value;
|
|
D.worldId = worldDialog.value.id;
|
|
D.urlList = worldDialog.value.ref?.urlList ?? [];
|
|
D.visible = true;
|
|
}
|
|
|
|
/**
|
|
*
|
|
*/
|
|
async function translateDescription() {
|
|
if (isTranslating.value) return;
|
|
|
|
const description = worldDialog.value.ref.description;
|
|
if (!description) return;
|
|
|
|
// Toggle: if already translated, clear to show original
|
|
if (translatedDescription.value) {
|
|
translatedDescription.value = '';
|
|
return;
|
|
}
|
|
|
|
isTranslating.value = true;
|
|
try {
|
|
const translated = await translateText(description, bioLanguage.value);
|
|
if (!translated) {
|
|
throw new Error('No translation returned');
|
|
}
|
|
|
|
translatedDescription.value = translated;
|
|
} catch (error) {
|
|
console.error('Translation failed:', error);
|
|
} finally {
|
|
isTranslating.value = false;
|
|
}
|
|
}
|
|
|
|
watch(
|
|
() => [worldDialog.value.id, worldDialog.value.ref?.description],
|
|
() => {
|
|
translatedDescription.value = '';
|
|
}
|
|
);
|
|
</script>
|