mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-13 11:53:51 +02:00
1368 lines
63 KiB
Vue
1368 lines
63 KiB
Vue
<template>
|
|
<div class="w-223">
|
|
<DialogHeader class="sr-only">
|
|
<DialogTitle>{{ avatarDialog.ref?.name || t('dialog.avatar.info.header') }}</DialogTitle>
|
|
<DialogDescription>
|
|
{{ avatarDialog.ref?.description || avatarDialog.ref?.name || t('dialog.avatar.info.header') }}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<div>
|
|
<div class="flex">
|
|
<div style="flex: none; width: 160px; height: 120px">
|
|
<img
|
|
:src="avatarDialog.ref.thumbnailImageUrl"
|
|
class="cursor-pointer"
|
|
@click="showFullscreenImageDialog(avatarDialog.ref.imageUrl)"
|
|
style="width: 160px; height: 120px; border-radius: 12px; object-fit: cover"
|
|
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"
|
|
v-text="avatarDialog.ref.name"
|
|
@click="copyToClipboard(avatarDialog.ref.name)"></span>
|
|
</div>
|
|
<div style="margin-top: 5px">
|
|
<span
|
|
class="cursor-pointer x-grey"
|
|
style="font-family: monospace"
|
|
@click="showUserDialog(avatarDialog.ref.authorId)"
|
|
v-text="avatarDialog.ref.authorName"></span>
|
|
</div>
|
|
<div>
|
|
<Badge
|
|
v-if="avatarDialog.ref.releaseStatus === 'public'"
|
|
variant="outline"
|
|
style="margin-right: 5px; margin-top: 5px">
|
|
{{ t('dialog.avatar.tags.public') }}
|
|
</Badge>
|
|
<Badge v-else variant="outline" style="margin-right: 5px; margin-top: 5px">
|
|
{{ t('dialog.avatar.tags.private') }}
|
|
</Badge>
|
|
<TooltipWrapper v-if="avatarDialog.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="avatarDialog.platformInfo.pc"
|
|
:class="['x-grey', 'x-tag-platform-pc', 'x-tag-border-left']"
|
|
>{{ avatarDialog.platformInfo.pc.performanceRating }}</span
|
|
>
|
|
<span
|
|
v-if="avatarDialog.fileAnalysis.standalonewindows?._fileSize"
|
|
:class="['x-grey', 'x-tag-platform-pc', 'x-tag-border-left']"
|
|
>{{ avatarDialog.fileAnalysis.standalonewindows._fileSize }}</span
|
|
>
|
|
</Badge>
|
|
</TooltipWrapper>
|
|
<TooltipWrapper v-if="avatarDialog.isQuest" side="top" content="Android">
|
|
<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="avatarDialog.platformInfo.android"
|
|
:class="['x-grey', 'x-tag-platform-quest', 'x-tag-border-left']"
|
|
>{{ avatarDialog.platformInfo.android.performanceRating }}</span
|
|
>
|
|
<span
|
|
v-if="avatarDialog.fileAnalysis.android?._fileSize"
|
|
:class="['x-grey', 'x-tag-platform-quest', 'x-tag-border-left']"
|
|
>{{ avatarDialog.fileAnalysis.android._fileSize }}</span
|
|
>
|
|
</Badge>
|
|
</TooltipWrapper>
|
|
<TooltipWrapper v-if="avatarDialog.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="avatarDialog.platformInfo.ios"
|
|
:class="[
|
|
'x-grey',
|
|
'x-tag-border-left',
|
|
'text-platform-ios',
|
|
'border-platform-ios'
|
|
]"
|
|
>{{ avatarDialog.platformInfo.ios.performanceRating }}</span
|
|
>
|
|
<span
|
|
v-if="avatarDialog.fileAnalysis.ios?._fileSize"
|
|
:class="[
|
|
'x-grey',
|
|
'x-tag-border-left',
|
|
'text-platform-ios',
|
|
'border-platform-ios'
|
|
]"
|
|
>{{ avatarDialog.fileAnalysis.ios._fileSize }}</span
|
|
>
|
|
</Badge>
|
|
</TooltipWrapper>
|
|
<Badge
|
|
v-if="avatarDialog.inCache"
|
|
variant="outline"
|
|
class="cursor-pointer"
|
|
style="margin-right: 5px; margin-top: 5px"
|
|
@click="openFolderGeneric(avatarDialog.cachePath)">
|
|
<span v-text="avatarDialog.cacheSize"></span>
|
|
{{ t('dialog.avatar.tags.cache') }}
|
|
</Badge>
|
|
<Badge
|
|
v-if="avatarDialog.ref.styles?.primary || avatarDialog.ref.styles?.secondary"
|
|
variant="outline"
|
|
style="margin-right: 5px; margin-top: 5px"
|
|
>{{ t('view.favorite.avatars.styles') }}
|
|
<span v-if="avatarDialog.ref.styles.primary" :class="['x-grey', 'x-tag-border-left']">{{
|
|
avatarDialog.ref.styles.primary
|
|
}}</span>
|
|
<span
|
|
v-if="avatarDialog.ref.styles.secondary"
|
|
:class="['x-grey', 'x-tag-border-left']"
|
|
>{{ avatarDialog.ref.styles.secondary }}</span
|
|
>
|
|
</Badge>
|
|
<Badge
|
|
v-if="avatarDialog.isQuestFallback"
|
|
variant="outline"
|
|
style="margin-right: 5px; margin-top: 5px">
|
|
{{ t('dialog.avatar.tags.fallback') }}
|
|
</Badge>
|
|
<Badge
|
|
v-if="avatarDialog.hasImposter"
|
|
variant="outline"
|
|
style="margin-right: 5px; margin-top: 5px"
|
|
>{{ t('dialog.avatar.tags.impostor') }}
|
|
<span v-if="avatarDialog.imposterVersion" :class="['x-grey', 'x-tag-border-left']"
|
|
>v{{ avatarDialog.imposterVersion }}</span
|
|
>
|
|
</Badge>
|
|
<Badge
|
|
v-if="avatarDialog.ref.unityPackageUrl"
|
|
variant="outline"
|
|
style="margin-right: 5px; margin-top: 5px">
|
|
{{ t('dialog.avatar.tags.future_proofing') }}
|
|
</Badge>
|
|
<div>
|
|
<template v-for="tag in avatarDialog.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.avatar.tags.content_horror')
|
|
}}</span>
|
|
<span v-else-if="tag === 'content_gore'">{{
|
|
t('dialog.avatar.tags.content_gore')
|
|
}}</span>
|
|
<span v-else-if="tag === 'content_violence'">{{
|
|
t('dialog.avatar.tags.content_violence')
|
|
}}</span>
|
|
<span v-else-if="tag === 'content_adult'">{{
|
|
t('dialog.avatar.tags.content_adult')
|
|
}}</span>
|
|
<span v-else-if="tag === 'content_sex'">{{
|
|
t('dialog.avatar.tags.content_sex')
|
|
}}</span>
|
|
<span v-else>{{ tag.replace('content_', '') }}</span>
|
|
</Badge>
|
|
<Badge
|
|
v-if="tag.startsWith('author_tag_')"
|
|
variant="outline"
|
|
style="margin-right: 5px; margin-top: 5px">
|
|
<span>
|
|
{{ tag.replace('author_tag_', '') }}
|
|
</span>
|
|
</Badge>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
<div style="margin-top: 5px">
|
|
<span
|
|
v-show="avatarDialog.ref.name !== avatarDialog.ref.description"
|
|
style="font-size: 12px"
|
|
v-text="avatarDialog.ref.description"></span>
|
|
</div>
|
|
</div>
|
|
<div class="ml-2 mt-12">
|
|
<TooltipWrapper
|
|
v-if="avatarDialog.inCache"
|
|
side="top"
|
|
:content="t('dialog.avatar.actions.delete_cache_tooltip')">
|
|
<Button
|
|
class="rounded-full mr-2"
|
|
size="icon-lg"
|
|
variant="outline"
|
|
:disabled="isGameRunning && avatarDialog.cacheLocked"
|
|
@click="deleteVRChatCache(avatarDialog.ref)"
|
|
><Trash2
|
|
/></Button>
|
|
</TooltipWrapper>
|
|
|
|
<TooltipWrapper
|
|
v-if="avatarDialog.isFavorite"
|
|
side="top"
|
|
:content="t('dialog.avatar.actions.favorite_tooltip')">
|
|
<Button class="rounded-full" size="icon-lg" @click="avatarDialogCommand('Add Favorite')"
|
|
><Star
|
|
/></Button>
|
|
</TooltipWrapper>
|
|
<TooltipWrapper v-else side="top" :content="t('dialog.avatar.actions.favorite_tooltip')">
|
|
<Button
|
|
class="rounded-full"
|
|
size="icon-lg"
|
|
variant="outline"
|
|
@click="avatarDialogCommand('Add Favorite')"
|
|
><Star
|
|
/></Button>
|
|
</TooltipWrapper>
|
|
|
|
<TooltipWrapper side="top" :content="t('dialog.avatar.actions.select')">
|
|
<Button
|
|
class="rounded-full ml-2"
|
|
size="icon-lg"
|
|
variant="outline"
|
|
:disabled="currentUser.currentAvatar === avatarDialog.id"
|
|
@click="selectAvatarWithoutConfirmation(avatarDialog.id)">
|
|
<CheckCircle
|
|
/></Button>
|
|
</TooltipWrapper>
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger as-child>
|
|
<Button
|
|
class="rounded-full ml-2"
|
|
:variant="avatarDialog.isBlocked ? 'destructive' : 'outline'"
|
|
size="icon-lg">
|
|
<Ellipsis />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent>
|
|
<DropdownMenuItem @click="avatarDialogCommand('Refresh')">
|
|
<RefreshCw class="size-4" />
|
|
{{ t('dialog.avatar.actions.refresh') }}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem @click="avatarDialogCommand('Share')">
|
|
<Share2 class="size-4" />
|
|
{{ t('dialog.avatar.actions.share') }}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem
|
|
v-if="avatarDialog.isBlocked"
|
|
variant="destructive"
|
|
@click="avatarDialogCommand('Unblock Avatar')">
|
|
<CheckCircle class="size-4" />
|
|
{{ t('dialog.avatar.actions.unblock') }}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem v-else @click="avatarDialogCommand('Block Avatar')">
|
|
<XCircle class="size-4" />
|
|
{{ t('dialog.avatar.actions.block') }}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem
|
|
v-if="/quest/.test(avatarDialog.ref.tags)"
|
|
@click="avatarDialogCommand('Select Fallback Avatar')">
|
|
<Check class="size-4" />
|
|
{{ t('dialog.avatar.actions.select_fallback') }}
|
|
</DropdownMenuItem>
|
|
<template v-if="avatarDialog.ref.authorId === currentUser.id">
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem
|
|
v-if="avatarDialog.ref.releaseStatus === 'public'"
|
|
@click="avatarDialogCommand('Make Private')">
|
|
<User class="size-4" />
|
|
{{ t('dialog.avatar.actions.make_private') }}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem v-else @click="avatarDialogCommand('Make Public')">
|
|
<User class="size-4" />
|
|
{{ t('dialog.avatar.actions.make_public') }}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem @click="avatarDialogCommand('Rename')">
|
|
<Pencil class="size-4" />
|
|
{{ t('dialog.avatar.actions.rename') }}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem @click="avatarDialogCommand('Change Description')">
|
|
<Pencil class="size-4" />
|
|
{{ t('dialog.avatar.actions.change_description') }}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem @click="avatarDialogCommand('Change Content Tags')">
|
|
<Pencil class="size-4" />
|
|
{{ t('dialog.avatar.actions.change_content_tags') }}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem @click="avatarDialogCommand('Change Styles and Author Tags')">
|
|
<Pencil class="size-4" />
|
|
{{ t('dialog.avatar.actions.change_styles_author_tags') }}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem @click="avatarDialogCommand('Change Image')">
|
|
<Image class="size-4" />
|
|
{{ t('dialog.avatar.actions.change_image') }}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem
|
|
v-if="avatarDialog.ref.unityPackageUrl"
|
|
@click="avatarDialogCommand('Download Unity Package')">
|
|
<Download class="size-4" />
|
|
{{ t('dialog.avatar.actions.download_package') }}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem
|
|
v-if="avatarDialog.hasImposter"
|
|
variant="destructive"
|
|
@click="avatarDialogCommand('Regenerate Imposter')">
|
|
<RefreshCw class="size-4" />
|
|
{{ t('dialog.avatar.actions.regenerate_impostor') }}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem
|
|
v-if="avatarDialog.hasImposter"
|
|
variant="destructive"
|
|
@click="avatarDialogCommand('Delete Imposter')">
|
|
<Trash2 class="size-4" />
|
|
{{ t('dialog.avatar.actions.delete_impostor') }}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem v-else @click="avatarDialogCommand('Create Imposter')">
|
|
<User class="size-4" />
|
|
{{ t('dialog.avatar.actions.create_impostor') }}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem variant="destructive" @click="avatarDialogCommand('Delete')">
|
|
<Trash2 class="size-4" />
|
|
{{ t('dialog.avatar.actions.delete') }}
|
|
</DropdownMenuItem>
|
|
</template>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<TabsUnderline
|
|
v-model="avatarDialog.activeTab"
|
|
:items="avatarDialogTabs"
|
|
:unmount-on-hide="false"
|
|
@update:modelValue="avatarDialogTabClick">
|
|
<template #Info>
|
|
<div class="flex flex-wrap items-start px-2.5" style="max-height: unset">
|
|
<div
|
|
v-if="avatarDialog.galleryImages.length || avatarDialog.ref.authorId === currentUser.id"
|
|
style="width: 100%">
|
|
<span class="block truncate font-medium leading-[18px]">{{
|
|
t('dialog.avatar.info.gallery')
|
|
}}</span>
|
|
<input
|
|
id="AvatarGalleryUploadButton"
|
|
type="file"
|
|
accept="image/*"
|
|
style="display: none"
|
|
@change="onFileChangeAvatarGallery" />
|
|
<Button
|
|
v-if="avatarDialog.ref.authorId === currentUser.id"
|
|
variant="outline"
|
|
size="sm"
|
|
:disabled="avatarDialog.galleryLoading"
|
|
class="ml-1"
|
|
@click="displayAvatarGalleryUpload">
|
|
<Upload />
|
|
{{ t('dialog.screenshot_metadata.upload') }}
|
|
</Button>
|
|
<div class="mt-2 w-[80%] ml-20">
|
|
<Carousel v-if="avatarDialog.galleryImages.length" class="w-full">
|
|
<CarouselContent class="h-50">
|
|
<CarouselItem v-for="imageUrl in avatarDialog.galleryImages" :key="imageUrl">
|
|
<div class="relative h-50 w-full">
|
|
<img
|
|
:src="imageUrl"
|
|
style="width: 100%; height: 100%; object-fit: contain"
|
|
@click="showFullscreenImageDialog(imageUrl)"
|
|
loading="lazy" />
|
|
</div>
|
|
</CarouselItem>
|
|
</CarouselContent>
|
|
<CarouselPrevious />
|
|
<CarouselNext />
|
|
</Carousel>
|
|
</div>
|
|
</div>
|
|
<div v-if="avatarDialog.ref.publishedListings?.length">
|
|
<span class="block truncate font-medium leading-[18px]">{{
|
|
t('dialog.avatar.info.listings')
|
|
}}</span>
|
|
<div
|
|
v-for="listing in avatarDialog.ref.publishedListings"
|
|
:key="listing.id"
|
|
class="box-border flex items-center p-1.5 text-[13px] w-full cursor-default">
|
|
<div class="relative inline-block flex-none size-9 mr-2.5">
|
|
<img
|
|
class="size-full rounded-full object-cover"
|
|
:src="getImageUrlFromImageId(listing.imageId)"
|
|
@click="showFullscreenImageDialog(getImageUrlFromImageId(listing.imageId))"
|
|
loading="lazy" />
|
|
</div>
|
|
<div class="flex-1 overflow-hidden">
|
|
<span class="block truncate font-medium leading-[18px]">{{
|
|
listing.displayName
|
|
}}</span>
|
|
<span
|
|
class="block truncate text-xs"
|
|
style="text-decoration: underline; font-style: italic"
|
|
>${{ commaNumber(listing.priceTokens) }}V</span
|
|
>
|
|
<span
|
|
class="block truncate text-xs"
|
|
style="text-overflow: ellipsis; text-wrap: auto"
|
|
v-text="listing.description"></span>
|
|
</div>
|
|
</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]" style="margin-bottom: 5px">{{
|
|
t('dialog.avatar.info.memo')
|
|
}}</span>
|
|
<InputGroupTextareaField
|
|
v-model="memo"
|
|
class="text-xs"
|
|
:rows="2"
|
|
:placeholder="t('dialog.avatar.info.memo_placeholder')"
|
|
input-class="resize-none min-h-0"
|
|
@change="onAvatarMemoChange" />
|
|
</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.avatar.info.id')
|
|
}}</span>
|
|
<span class="block truncate text-xs"
|
|
>{{ avatarDialog.id
|
|
}}<TooltipWrapper side="top" :content="t('dialog.avatar.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="copyAvatarId(avatarDialog.id)">
|
|
{{ t('dialog.avatar.info.copy_id') }}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem @click="copyAvatarUrl(avatarDialog.id)">
|
|
{{ t('dialog.avatar.info.copy_url') }}
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</TooltipWrapper></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.avatar.info.created_at')
|
|
}}</span>
|
|
<span class="block truncate text-xs">{{
|
|
formatDateFilter(avatarDialog.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 font-medium leading-[18px]" style="display: inline">{{
|
|
t('dialog.avatar.info.last_updated')
|
|
}}</span>
|
|
<TooltipWrapper
|
|
v-if="Object.keys(avatarDialog.fileAnalysis).length"
|
|
side="top"
|
|
style="margin-left: 5px">
|
|
<template #content>
|
|
<template
|
|
v-for="(created_at, platform) in avatarDialogPlatformCreatedAt"
|
|
: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(avatarDialog.ref.updated_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]">{{
|
|
t('dialog.avatar.info.version')
|
|
}}</span>
|
|
<span
|
|
v-if="avatarDialog.ref.version !== 0"
|
|
class="block truncate text-xs"
|
|
v-text="avatarDialog.ref.version"></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.avatar.info.time_spent')
|
|
}}</span>
|
|
|
|
<span v-if="avatarDialog.timeSpent === 0" class="block truncate text-xs">-</span>
|
|
<span v-else class="block truncate text-xs">{{
|
|
timeToText(avatarDialog.timeSpent)
|
|
}}</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.avatar.info.platform')
|
|
}}</span>
|
|
<span
|
|
v-if="avatarDialogPlatform"
|
|
class="block truncate text-xs"
|
|
v-text="avatarDialogPlatform"></span>
|
|
<span v-else class="block truncate text-xs">-</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<template #JSON>
|
|
<Button
|
|
class="rounded-full mr-2"
|
|
size="icon-sm"
|
|
variant="ghost"
|
|
@click="refreshAvatarDialogTreeData()">
|
|
<RefreshCw />
|
|
</Button>
|
|
<Button
|
|
class="rounded-full"
|
|
size="icon-sm"
|
|
variant="ghost"
|
|
@click="downloadAndSaveJson(avatarDialog.id, avatarDialog.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(avatarDialog.fileAnalysis).length"
|
|
:data="avatarDialog.fileAnalysis"
|
|
:deep="2"
|
|
:theme="isDarkMode ? 'dark' : 'light'"
|
|
show-icon />
|
|
</template>
|
|
</TabsUnderline>
|
|
<template v-if="avatarDialog.visible">
|
|
<SetAvatarTagsDialog v-model:setAvatarTagsDialog="setAvatarTagsDialog" />
|
|
<SetAvatarStylesDialog v-model:setAvatarStylesDialog="setAvatarStylesDialog" />
|
|
<input
|
|
id="AvatarImageUploadButton"
|
|
type="file"
|
|
accept="image/*"
|
|
style="display: none"
|
|
@change="onFileChangeAvatarImage" />
|
|
<ImageCropDialog
|
|
:open="cropDialogOpen"
|
|
:title="t('dialog.change_content_image.avatar')"
|
|
:aspect-ratio="4 / 3"
|
|
:file="cropDialogFile"
|
|
@update:open="cropDialogOpen = $event"
|
|
@confirm="onCropConfirmAvatar" />
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import {
|
|
Apple,
|
|
Check,
|
|
CheckCircle,
|
|
ChevronDown,
|
|
Copy,
|
|
Download,
|
|
Ellipsis,
|
|
Image,
|
|
Monitor,
|
|
Pencil,
|
|
RefreshCw,
|
|
Share2,
|
|
Smartphone,
|
|
Star,
|
|
Trash2,
|
|
Upload,
|
|
User,
|
|
XCircle
|
|
} from 'lucide-vue-next';
|
|
import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from '@/components/ui/carousel';
|
|
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 { 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,
|
|
copyToClipboard,
|
|
downloadAndSaveJson,
|
|
formatDateFilter,
|
|
openExternalLink,
|
|
openFolderGeneric,
|
|
replaceVrcPackageUrl,
|
|
timeToText
|
|
} from '../../../shared/utils';
|
|
import {
|
|
useAppearanceSettingsStore,
|
|
useAvatarStore,
|
|
useFavoriteStore,
|
|
useGalleryStore,
|
|
useGameStore,
|
|
useModalStore,
|
|
useUiStore,
|
|
useUserStore
|
|
} from '../../../stores';
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger
|
|
} from '../../ui/dropdown-menu';
|
|
import {
|
|
handleImageUploadInput,
|
|
readFileAsBase64,
|
|
resizeImageToFitLimits,
|
|
uploadImageLegacy,
|
|
withUploadTimeout
|
|
} from '../../../shared/utils/imageUpload';
|
|
import { avatarModerationRequest, avatarRequest, favoriteRequest } from '../../../api';
|
|
import { Badge } from '../../ui/badge';
|
|
import { database } from '../../../service/database';
|
|
import { formatJsonVars } from '../../../shared/utils/base/ui';
|
|
|
|
import ImageCropDialog from '../ImageCropDialog.vue';
|
|
|
|
const SetAvatarStylesDialog = defineAsyncComponent(() => import('./SetAvatarStylesDialog.vue'));
|
|
const SetAvatarTagsDialog = defineAsyncComponent(() => import('./SetAvatarTagsDialog.vue'));
|
|
|
|
const { showUserDialog, sortUserDialogAvatars } = useUserStore();
|
|
const { userDialog, currentUser } = storeToRefs(useUserStore());
|
|
const avatarStore = useAvatarStore();
|
|
const { cachedAvatarModerations, cachedAvatars } = avatarStore;
|
|
const { avatarDialog } = storeToRefs(avatarStore);
|
|
const { showAvatarDialog, getAvatarGallery, applyAvatarModeration, applyAvatar, selectAvatarWithoutConfirmation } =
|
|
avatarStore;
|
|
const { showFavoriteDialog } = useFavoriteStore();
|
|
const { isGameRunning } = storeToRefs(useGameStore());
|
|
const { deleteVRChatCache } = useGameStore();
|
|
const { showFullscreenImageDialog } = useGalleryStore();
|
|
const { isDarkMode } = storeToRefs(useAppearanceSettingsStore());
|
|
const modalStore = useModalStore();
|
|
const uiStore = useUiStore();
|
|
|
|
const { t } = useI18n();
|
|
const avatarDialogTabs = computed(() => [
|
|
{ value: 'Info', label: t('dialog.avatar.info.header') },
|
|
{ value: 'JSON', label: t('dialog.avatar.json.header') }
|
|
]);
|
|
|
|
const cropDialogOpen = ref(false);
|
|
const cropDialogFile = ref(null);
|
|
const changeAvatarImageLoading = ref(false);
|
|
|
|
const treeData = ref({});
|
|
const memo = ref('');
|
|
const setAvatarTagsDialog = ref({
|
|
visible: false,
|
|
loading: false,
|
|
ownAvatars: [],
|
|
selectedAvatarIds: [],
|
|
selectedTags: [],
|
|
selectedTagsCsv: '',
|
|
contentHorror: false,
|
|
contentGore: false,
|
|
contentViolence: false,
|
|
contentAdult: false,
|
|
contentSex: false
|
|
});
|
|
const setAvatarStylesDialog = ref({
|
|
visible: false,
|
|
loading: false,
|
|
avatarId: '',
|
|
initialPrimaryStyle: '',
|
|
initialSecondaryStyle: '',
|
|
primaryStyle: '',
|
|
secondaryStyle: '',
|
|
availableAvatarStyles: [],
|
|
availableAvatarStylesMap: new Map(),
|
|
initialTags: [],
|
|
authorTags: ''
|
|
});
|
|
|
|
const avatarDialogPlatform = computed(() => {
|
|
const { ref } = avatarDialog.value;
|
|
const platforms = [];
|
|
if (ref.unityPackages) {
|
|
for (const unityPackage of ref.unityPackages) {
|
|
if (
|
|
unityPackage.variant &&
|
|
unityPackage.variant !== 'standard' &&
|
|
unityPackage.variant !== 'security'
|
|
) {
|
|
// skip imposters
|
|
continue;
|
|
}
|
|
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.push(`${platform}/${unityPackage.unityVersion}`);
|
|
}
|
|
}
|
|
return platforms.join(', ');
|
|
});
|
|
|
|
const avatarDialogPlatformCreatedAt = computed(() => {
|
|
const { ref } = avatarDialog.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(
|
|
() => avatarDialog.value.loading,
|
|
() => {
|
|
if (avatarDialog.value.visible) {
|
|
handleDialogOpen();
|
|
!avatarDialog.value.loading && loadLastActiveTab();
|
|
}
|
|
}
|
|
);
|
|
|
|
/**
|
|
*
|
|
* @param tabName
|
|
*/
|
|
function handleAvatarDialogTab(tabName) {
|
|
avatarDialog.value.lastActiveTab = tabName;
|
|
if (tabName === 'JSON') {
|
|
refreshAvatarDialogTreeData();
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
*/
|
|
function loadLastActiveTab() {
|
|
handleAvatarDialogTab(avatarDialog.value.lastActiveTab);
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param tabName
|
|
*/
|
|
function avatarDialogTabClick(tabName) {
|
|
if (tabName === avatarDialog.value.lastActiveTab) {
|
|
if (tabName === 'JSON') {
|
|
refreshAvatarDialogTreeData();
|
|
}
|
|
return;
|
|
}
|
|
handleAvatarDialogTab(tabName);
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param imageId
|
|
*/
|
|
function getImageUrlFromImageId(imageId) {
|
|
return `${AppDebug.endpointDomain}/file/${imageId}/1/`;
|
|
}
|
|
|
|
/**
|
|
*
|
|
*/
|
|
function handleDialogOpen() {
|
|
setAvatarTagsDialog.value.visible = false;
|
|
avatarDialog.value.timeSpent = 0;
|
|
memo.value = '';
|
|
treeData.value = {};
|
|
getAvatarTimeSpent();
|
|
getAvatarMemo();
|
|
}
|
|
|
|
/**
|
|
*
|
|
*/
|
|
function getAvatarTimeSpent() {
|
|
const D = avatarDialog.value;
|
|
avatarDialog.value.timeSpent = 0;
|
|
database.getAvatarTimeSpent(D.id).then((aviTime) => {
|
|
if (D.id === aviTime.avatarId) {
|
|
avatarDialog.value.timeSpent = aviTime.timeSpent;
|
|
if (D.id === currentUser.value.currentAvatar && currentUser.value.$previousAvatarSwapTime) {
|
|
avatarDialog.value.timeSpent += Date.now() - currentUser.value.$previousAvatarSwapTime;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
*
|
|
*/
|
|
function getAvatarMemo() {
|
|
const D = avatarDialog.value;
|
|
database.getAvatarMemoDB(D.id).then((res) => {
|
|
if (D.id === res.avatarId) {
|
|
memo.value = res.memo;
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param command
|
|
*/
|
|
function avatarDialogCommand(command) {
|
|
const D = avatarDialog.value;
|
|
switch (command) {
|
|
case 'Refresh':
|
|
const avatarId = D.id;
|
|
showAvatarDialog(avatarId, { forceRefresh: true });
|
|
break;
|
|
case 'Share':
|
|
copyAvatarUrl(D.id);
|
|
break;
|
|
case 'Rename':
|
|
promptRenameAvatar(D);
|
|
break;
|
|
case 'Change Image':
|
|
showChangeAvatarImageDialog();
|
|
break;
|
|
case 'Change Description':
|
|
promptChangeAvatarDescription(D);
|
|
break;
|
|
case 'Change Content Tags':
|
|
showSetAvatarTagsDialog(D.id);
|
|
break;
|
|
case 'Change Styles and Author Tags':
|
|
showSetAvatarStylesDialog();
|
|
break;
|
|
case 'Download Unity Package':
|
|
openExternalLink(replaceVrcPackageUrl(avatarDialog.value.ref.unityPackageUrl));
|
|
break;
|
|
case 'Add Favorite':
|
|
showFavoriteDialog('avatar', D.id);
|
|
break;
|
|
default:
|
|
const commandLabelMap = {
|
|
'Delete Favorite': t('dialog.avatar.actions.favorite_tooltip'),
|
|
'Select Fallback Avatar': t('dialog.avatar.actions.select_fallback'),
|
|
'Block Avatar': t('dialog.avatar.actions.block'),
|
|
'Unblock Avatar': t('dialog.avatar.actions.unblock'),
|
|
'Make Public': t('dialog.avatar.actions.make_public'),
|
|
'Make Private': t('dialog.avatar.actions.make_private'),
|
|
Delete: t('dialog.avatar.actions.delete'),
|
|
'Delete Imposter': t('dialog.avatar.actions.delete_impostor'),
|
|
'Create Imposter': t('dialog.avatar.actions.create_impostor'),
|
|
'Regenerate Imposter': t('dialog.avatar.actions.regenerate_impostor')
|
|
};
|
|
modalStore
|
|
.confirm({
|
|
title: t('confirm.title'),
|
|
description: t('confirm.command_question', {
|
|
command: commandLabelMap[command] ?? command
|
|
})
|
|
})
|
|
.then(({ ok }) => {
|
|
if (!ok) return;
|
|
switch (command) {
|
|
case 'Delete Favorite':
|
|
favoriteRequest.deleteFavorite({
|
|
objectId: D.id
|
|
});
|
|
break;
|
|
case 'Select Fallback Avatar':
|
|
avatarRequest
|
|
.selectFallbackAvatar({
|
|
avatarId: D.id
|
|
})
|
|
.then((args) => {
|
|
toast.success(t('message.avatar.fallback_changed'));
|
|
return args;
|
|
});
|
|
break;
|
|
case 'Block Avatar':
|
|
avatarModerationRequest
|
|
.sendAvatarModeration({
|
|
avatarModerationType: 'block',
|
|
targetAvatarId: D.id
|
|
})
|
|
.then((args) => {
|
|
// 'AVATAR-MODERATION';
|
|
applyAvatarModeration(args.json);
|
|
toast.success(t('message.avatar.blocked'));
|
|
return args;
|
|
});
|
|
break;
|
|
case 'Unblock Avatar':
|
|
avatarModerationRequest
|
|
.deleteAvatarModeration({
|
|
avatarModerationType: 'block',
|
|
targetAvatarId: D.id
|
|
})
|
|
.then((args) => {
|
|
cachedAvatarModerations.delete(args.params.targetAvatarId);
|
|
const D = avatarDialog.value;
|
|
if (
|
|
args.params.avatarModerationType === 'block' &&
|
|
D.id === args.params.targetAvatarId
|
|
) {
|
|
D.isBlocked = false;
|
|
}
|
|
});
|
|
break;
|
|
case 'Make Public':
|
|
avatarRequest
|
|
.saveAvatar({
|
|
id: D.id,
|
|
releaseStatus: 'public'
|
|
})
|
|
.then((args) => {
|
|
applyAvatar(args.json);
|
|
toast.success(t('message.avatar.updated_public'));
|
|
return args;
|
|
});
|
|
break;
|
|
case 'Make Private':
|
|
avatarRequest
|
|
.saveAvatar({
|
|
id: D.id,
|
|
releaseStatus: 'private'
|
|
})
|
|
.then((args) => {
|
|
applyAvatar(args.json);
|
|
toast.success(t('message.avatar.updated_private'));
|
|
return args;
|
|
});
|
|
break;
|
|
case 'Delete':
|
|
avatarRequest
|
|
.deleteAvatar({
|
|
avatarId: D.id
|
|
})
|
|
.then((args) => {
|
|
const { json } = args;
|
|
cachedAvatars.delete(json._id);
|
|
if (userDialog.value.id === json.authorId) {
|
|
const map = new Map();
|
|
for (const ref of cachedAvatars.values()) {
|
|
if (ref.authorId === json.authorId) {
|
|
map.set(ref.id, ref);
|
|
}
|
|
}
|
|
const array = Array.from(map.values());
|
|
sortUserDialogAvatars(array);
|
|
}
|
|
|
|
toast.success(t('message.avatar.deleted'));
|
|
uiStore.jumpBackDialogCrumb();
|
|
return args;
|
|
});
|
|
break;
|
|
case 'Delete Imposter':
|
|
avatarRequest
|
|
.deleteImposter({
|
|
avatarId: D.id
|
|
})
|
|
.then((args) => {
|
|
toast.success(t('message.avatar.impostor_deleted'));
|
|
showAvatarDialog(D.id);
|
|
return args;
|
|
});
|
|
break;
|
|
case 'Create Imposter':
|
|
avatarRequest
|
|
.createImposter({
|
|
avatarId: D.id
|
|
})
|
|
.then((args) => {
|
|
toast.success(t('message.avatar.impostor_queued'));
|
|
return args;
|
|
});
|
|
break;
|
|
case 'Regenerate Imposter':
|
|
avatarRequest
|
|
.deleteImposter({
|
|
avatarId: D.id
|
|
})
|
|
.then((args) => {
|
|
showAvatarDialog(D.id);
|
|
return args;
|
|
})
|
|
.finally(() => {
|
|
avatarRequest
|
|
.createImposter({
|
|
avatarId: D.id
|
|
})
|
|
.then((args) => {
|
|
toast.success(t('message.avatar.impostor_regenerated'));
|
|
return args;
|
|
});
|
|
});
|
|
break;
|
|
}
|
|
});
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
*/
|
|
function showChangeAvatarImageDialog() {
|
|
document.getElementById('AvatarImageUploadButton').click();
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param e
|
|
*/
|
|
function onFileChangeAvatarImage(e) {
|
|
const { file, clearInput } = handleImageUploadInput(e, {
|
|
inputSelector: '#AvatarImageUploadButton',
|
|
tooLargeMessage: () => t('message.file.too_large'),
|
|
invalidTypeMessage: () => t('message.file.not_image')
|
|
});
|
|
if (!file) {
|
|
return;
|
|
}
|
|
if (!avatarDialog.value.visible || avatarDialog.value.loading) {
|
|
clearInput();
|
|
return;
|
|
}
|
|
clearInput();
|
|
cropDialogFile.value = file;
|
|
cropDialogOpen.value = true;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param blob
|
|
*/
|
|
async function onCropConfirmAvatar(blob) {
|
|
changeAvatarImageLoading.value = true;
|
|
try {
|
|
await withUploadTimeout(
|
|
(async () => {
|
|
const base64Body = await readFileAsBase64(blob);
|
|
const base64File = await resizeImageToFitLimits(base64Body);
|
|
if (LINUX) {
|
|
const args = await avatarRequest.uploadAvatarImage(base64File);
|
|
const fileUrl = args.json.versions[args.json.versions.length - 1].file.url;
|
|
await avatarRequest.saveAvatar({
|
|
id: avatarDialog.value.id,
|
|
imageUrl: fileUrl
|
|
});
|
|
} else {
|
|
await uploadImageLegacy('avatar', {
|
|
entityId: avatarDialog.value.id,
|
|
imageUrl: avatarDialog.value.ref.imageUrl,
|
|
base64File,
|
|
blob
|
|
});
|
|
}
|
|
})()
|
|
);
|
|
toast.success(t('message.upload.success'));
|
|
// force refresh cover image
|
|
const avatarId = avatarDialog.value.id;
|
|
showAvatarDialog(avatarId, { forceRefresh: true });
|
|
} catch (error) {
|
|
console.error('avatar image upload process failed:', error);
|
|
toast.error(t('message.upload.error'));
|
|
} finally {
|
|
changeAvatarImageLoading.value = false;
|
|
cropDialogOpen.value = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param avatar
|
|
*/
|
|
function promptChangeAvatarDescription(avatar) {
|
|
modalStore
|
|
.prompt({
|
|
title: t('prompt.change_avatar_description.header'),
|
|
description: t('prompt.change_avatar_description.description'),
|
|
confirmText: t('prompt.change_avatar_description.ok'),
|
|
cancelText: t('prompt.change_avatar_description.cancel'),
|
|
inputValue: avatar.ref.description,
|
|
errorMessage: t('prompt.change_avatar_description.input_error')
|
|
})
|
|
.then(({ ok, value }) => {
|
|
if (!ok) return;
|
|
if (value && value !== avatar.ref.description) {
|
|
avatarRequest
|
|
.saveAvatar({
|
|
id: avatar.id,
|
|
description: value
|
|
})
|
|
.then((args) => {
|
|
applyAvatar(args.json);
|
|
toast.success(t('prompt.change_avatar_description.message.success'));
|
|
return args;
|
|
});
|
|
}
|
|
})
|
|
.catch(() => {});
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param avatar
|
|
*/
|
|
function promptRenameAvatar(avatar) {
|
|
modalStore
|
|
.prompt({
|
|
title: t('prompt.rename_avatar.header'),
|
|
description: t('prompt.rename_avatar.description'),
|
|
confirmText: t('prompt.rename_avatar.ok'),
|
|
cancelText: t('prompt.rename_avatar.cancel'),
|
|
inputValue: avatar.ref.name,
|
|
errorMessage: t('prompt.rename_avatar.input_error')
|
|
})
|
|
.then(({ ok, value }) => {
|
|
if (!ok) return;
|
|
if (value && value !== avatar.ref.name) {
|
|
avatarRequest
|
|
.saveAvatar({
|
|
id: avatar.id,
|
|
name: value
|
|
})
|
|
.then((args) => {
|
|
applyAvatar(args.json);
|
|
toast.success(t('prompt.rename_avatar.message.success'));
|
|
return args;
|
|
});
|
|
}
|
|
})
|
|
.catch(() => {});
|
|
}
|
|
|
|
/**
|
|
*
|
|
*/
|
|
function onAvatarMemoChange() {
|
|
if (memo.value) {
|
|
database.setAvatarMemo({
|
|
avatarId: avatarDialog.value.id,
|
|
editedAt: new Date().toJSON(),
|
|
memo: memo.value
|
|
});
|
|
} else {
|
|
database.deleteAvatarMemo(avatarDialog.value.id);
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param id
|
|
*/
|
|
function copyAvatarId(id) {
|
|
copyToClipboard(id);
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param id
|
|
*/
|
|
function copyAvatarUrl(id) {
|
|
copyToClipboard(`https://vrchat.com/home/avatar/${id}`);
|
|
}
|
|
|
|
/**
|
|
*
|
|
*/
|
|
function refreshAvatarDialogTreeData() {
|
|
treeData.value = formatJsonVars(avatarDialog.value.ref);
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param avatarId
|
|
*/
|
|
function showSetAvatarTagsDialog(avatarId) {
|
|
const D = setAvatarTagsDialog.value;
|
|
D.selectedAvatarIds = [avatarId];
|
|
D.visible = true;
|
|
D.loading = true;
|
|
D.ownAvatars = [];
|
|
D.selectedTags = [];
|
|
D.selectedTagsCsv = '';
|
|
D.contentHorror = false;
|
|
D.contentGore = false;
|
|
D.contentViolence = false;
|
|
D.contentAdult = false;
|
|
D.contentSex = false;
|
|
const oldTags = avatarDialog.value.ref.tags;
|
|
oldTags.forEach((tag) => {
|
|
switch (tag) {
|
|
case 'content_horror':
|
|
D.contentHorror = true;
|
|
break;
|
|
case 'content_gore':
|
|
D.contentGore = true;
|
|
break;
|
|
case 'content_violence':
|
|
D.contentViolence = true;
|
|
break;
|
|
case 'content_adult':
|
|
D.contentAdult = true;
|
|
break;
|
|
case 'content_sex':
|
|
D.contentSex = true;
|
|
break;
|
|
default:
|
|
if (tag.startsWith('content_')) {
|
|
D.selectedTags.push(tag.substring(8));
|
|
}
|
|
break;
|
|
}
|
|
});
|
|
for (const ref of cachedAvatars.values()) {
|
|
if (ref.authorId === currentUser.value.id) {
|
|
D.ownAvatars.push(ref);
|
|
}
|
|
}
|
|
nextTick(() => {
|
|
D.loading = false;
|
|
});
|
|
}
|
|
|
|
/**
|
|
*
|
|
*/
|
|
function showSetAvatarStylesDialog() {
|
|
const D = setAvatarStylesDialog.value;
|
|
D.visible = true;
|
|
D.loading = true;
|
|
D.avatarId = avatarDialog.value.id;
|
|
D.primaryStyle = avatarDialog.value.ref.styles?.primary || '';
|
|
D.secondaryStyle = avatarDialog.value.ref.styles?.secondary || '';
|
|
D.initialPrimaryStyle = D.primaryStyle;
|
|
D.initialSecondaryStyle = D.secondaryStyle;
|
|
D.initialTags = avatarDialog.value.ref.tags;
|
|
D.authorTags = '';
|
|
for (const tag of D.initialTags) {
|
|
if (tag.startsWith('author_tag_')) {
|
|
if (D.authorTags) {
|
|
D.authorTags += ',';
|
|
}
|
|
D.authorTags += tag.substring(11);
|
|
}
|
|
}
|
|
nextTick(() => {
|
|
D.loading = false;
|
|
});
|
|
}
|
|
|
|
/**
|
|
*
|
|
*/
|
|
function displayAvatarGalleryUpload() {
|
|
document.getElementById('AvatarGalleryUploadButton').click();
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param e
|
|
*/
|
|
function onFileChangeAvatarGallery(e) {
|
|
const { file, clearInput } = handleImageUploadInput(e, {
|
|
inputSelector: '#AvatarGalleryUploadButton',
|
|
tooLargeMessage: () => t('message.file.too_large'),
|
|
invalidTypeMessage: () => t('message.file.not_image')
|
|
});
|
|
if (!file) {
|
|
return;
|
|
}
|
|
const r = new FileReader();
|
|
const resetLoading = () => {
|
|
avatarDialog.value.galleryLoading = false;
|
|
clearInput();
|
|
};
|
|
r.onerror = resetLoading;
|
|
r.onabort = resetLoading;
|
|
r.onload = function () {
|
|
try {
|
|
avatarDialog.value.galleryLoading = true;
|
|
const base64Body = btoa(r.result.toString());
|
|
const uploadPromise = (async () => {
|
|
const args = await avatarRequest.uploadAvatarGalleryImage(base64Body, avatarDialog.value.id);
|
|
avatarDialog.value.galleryImages = await getAvatarGallery(avatarDialog.value.id);
|
|
return args;
|
|
})();
|
|
toast.promise(uploadPromise, {
|
|
loading: t('message.upload.loading'),
|
|
success: t('message.upload.success'),
|
|
error: t('message.upload.error')
|
|
});
|
|
uploadPromise
|
|
.catch((error) => {
|
|
console.error('Failed to upload image', error);
|
|
})
|
|
.finally(resetLoading);
|
|
} catch (error) {
|
|
console.error('Failed to process image', error);
|
|
resetLoading();
|
|
}
|
|
};
|
|
try {
|
|
r.readAsBinaryString(file);
|
|
} catch (error) {
|
|
console.error('Failed to read file', error);
|
|
resetLoading();
|
|
}
|
|
}
|
|
</script>
|