mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-05-04 13:56:07 +02:00
1196 lines
50 KiB
Vue
1196 lines
50 KiB
Vue
<template>
|
|
<div class="gallery-page x-container">
|
|
<div class="flex items-center gap-2 ml-2">
|
|
<Button variant="ghost" size="sm" class="mr-3" @click="goBack">
|
|
<ArrowLeft />
|
|
{{ t('nav_tooltip.tools') }}
|
|
</Button>
|
|
<span class="header">{{ t('dialog.gallery_icons.header') }}</span>
|
|
</div>
|
|
<TabsUnderline default-value="gallery" :items="galleryTabs" :unmount-on-hide="false">
|
|
<template #label-gallery>
|
|
<span>
|
|
{{ t('dialog.gallery_icons.gallery') }}
|
|
<span class="gallery-tab-count"> {{ galleryTable.length }}/64 </span>
|
|
</span>
|
|
</template>
|
|
<template #label-icons>
|
|
<span>
|
|
{{ t('dialog.gallery_icons.icons') }}
|
|
<span class="gallery-tab-count"> {{ VRCPlusIconsTable.length }}/64 </span>
|
|
</span>
|
|
</template>
|
|
<template #label-emojis>
|
|
<span>
|
|
{{ t('dialog.gallery_icons.emojis') }}
|
|
<span class="gallery-tab-count">
|
|
{{ emojiTable.length }}/{{ cachedConfigTyped.maxUserEmoji }}
|
|
</span>
|
|
</span>
|
|
</template>
|
|
<template #label-stickers>
|
|
<span>
|
|
{{ t('dialog.gallery_icons.stickers') }}
|
|
<span class="gallery-tab-count">
|
|
{{ stickerTable.length }}/{{ cachedConfigTyped.maxUserStickers }}
|
|
</span>
|
|
</span>
|
|
</template>
|
|
<template #label-prints>
|
|
<span>
|
|
{{ t('dialog.gallery_icons.prints') }}
|
|
<span class="gallery-tab-count"> {{ printTable.length }}/64 </span>
|
|
</span>
|
|
</template>
|
|
<template #label-inventory>
|
|
<span>
|
|
{{ t('dialog.gallery_icons.inventory') }}
|
|
<span class="gallery-tab-count">
|
|
{{ inventoryTable.length }}
|
|
</span>
|
|
</span>
|
|
</template>
|
|
<template #gallery>
|
|
<div>
|
|
<input
|
|
id="GalleryUploadButton"
|
|
type="file"
|
|
accept="image/*"
|
|
@change="onFileChangeGallery"
|
|
style="display: none" />
|
|
<span>{{ t('dialog.gallery_icons.recommended_image_size') }}: 1200x900px (4:3)</span>
|
|
<br />
|
|
<br />
|
|
<ButtonGroup>
|
|
<Button variant="outline" size="sm" @click="refreshGalleryTable">
|
|
<RefreshCw />
|
|
{{ t('dialog.gallery_icons.refresh') }}
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
:disabled="!isLocalUserVrcPlusSupporter || isUploading"
|
|
@click="displayGalleryUpload">
|
|
<Upload />
|
|
{{ t('dialog.gallery_icons.upload') }}
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
:disabled="!currentUser.profilePicOverride"
|
|
@click="setProfilePicOverride('')">
|
|
<X />
|
|
{{ t('dialog.gallery_icons.clear') }}
|
|
</Button>
|
|
</ButtonGroup>
|
|
<br />
|
|
<div
|
|
class="x-friend-item"
|
|
v-for="image in galleryTable"
|
|
:key="image.id"
|
|
style="display: inline-block; margin-top: 10px; width: unset; cursor: default">
|
|
<template v-if="image.versions && image.versions.length > 0">
|
|
<div
|
|
class="h-[200px] w-[200px] rounded-[20px] overflow-hidden"
|
|
v-if="image.versions[image.versions.length - 1].file.url"
|
|
@click="setProfilePicOverride(image.id)"
|
|
:class="compareCurrentProfilePic(image.id) ? 'cursor-default' : 'cursor-pointer'">
|
|
<img
|
|
class="h-full w-full rounded-[15px] object-cover"
|
|
:src="image.versions[image.versions.length - 1].file.url"
|
|
loading="lazy" />
|
|
</div>
|
|
<div class="float-right" style="margin-top: 5px">
|
|
<Button
|
|
class="rounded-full mr-2"
|
|
size="icon-sm"
|
|
variant="outline"
|
|
@click="
|
|
showFullscreenImageDialog(image.versions[image.versions.length - 1].file.url)
|
|
">
|
|
<Maximize2 />
|
|
</Button>
|
|
<Button
|
|
class="rounded-full"
|
|
size="icon-sm"
|
|
variant="outline"
|
|
@click="deleteGalleryImage(image.id)">
|
|
<Trash2 />
|
|
</Button>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<template #icons>
|
|
<div>
|
|
<input
|
|
id="VRCPlusIconUploadButton"
|
|
type="file"
|
|
accept="image/*"
|
|
@change="onFileChangeVRCPlusIcon"
|
|
style="display: none" />
|
|
<span>{{ t('dialog.gallery_icons.recommended_image_size') }}: 2048x2048px (1:1)</span>
|
|
<br />
|
|
<br />
|
|
<ButtonGroup>
|
|
<Button variant="outline" size="sm" @click="refreshVRCPlusIconsTable">
|
|
<RefreshCw />
|
|
{{ t('dialog.gallery_icons.refresh') }}
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
:disabled="!isLocalUserVrcPlusSupporter || isUploading"
|
|
@click="displayVRCPlusIconUpload">
|
|
<Upload />
|
|
{{ t('dialog.gallery_icons.upload') }}
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
:disabled="!currentUser.userIcon"
|
|
@click="setVRCPlusIcon('')">
|
|
<X />
|
|
{{ t('dialog.gallery_icons.clear') }}
|
|
</Button>
|
|
</ButtonGroup>
|
|
<br />
|
|
<div
|
|
class="x-friend-item"
|
|
v-for="image in VRCPlusIconsTable"
|
|
:key="image.id"
|
|
style="display: inline-block; margin-top: 10px; width: unset; cursor: default">
|
|
<template v-if="image.versions && image.versions.length > 0"
|
|
><div
|
|
class="h-[200px] w-[200px] rounded-[20px] overflow-hidden"
|
|
v-if="image.versions[image.versions.length - 1].file.url"
|
|
@click="setVRCPlusIcon(image.id)"
|
|
:class="compareCurrentVRCPlusIcon(image.id) ? 'cursor-default' : 'cursor-pointer'">
|
|
<img
|
|
class="h-full w-full rounded-[15px] object-cover"
|
|
:src="image.versions[image.versions.length - 1].file.url"
|
|
loading="lazy" />
|
|
</div>
|
|
<div class="float-right" style="margin-top: 5px">
|
|
<Button
|
|
class="rounded-full mr-2"
|
|
size="icon-sm"
|
|
variant="outline"
|
|
@click="
|
|
showFullscreenImageDialog(image.versions[image.versions.length - 1].file.url)
|
|
">
|
|
<Maximize2 />
|
|
</Button>
|
|
<Button
|
|
class="rounded-full"
|
|
size="icon-sm"
|
|
variant="outline"
|
|
@click="deleteVRCPlusIcon(image.id)"
|
|
><Trash2
|
|
/></Button></div
|
|
></template>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<template #emojis>
|
|
<div>
|
|
<input
|
|
id="EmojiUploadButton"
|
|
type="file"
|
|
accept="image/*"
|
|
@change="onFileChangeEmoji"
|
|
style="display: none" />
|
|
<span>{{ t('dialog.gallery_icons.recommended_image_size') }}: 1024x1024px (1:1)</span>
|
|
<br />
|
|
<br />
|
|
<div>
|
|
<ButtonGroup style="margin-right: 10px">
|
|
<Button variant="outline" size="sm" @click="refreshEmojiTable">
|
|
<RefreshCw />
|
|
{{ t('dialog.gallery_icons.refresh') }}
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
:disabled="!isLocalUserVrcPlusSupporter || isUploading"
|
|
@click="displayEmojiUpload">
|
|
<Upload />
|
|
{{ t('dialog.gallery_icons.upload') }}
|
|
</Button>
|
|
</ButtonGroup>
|
|
<br />
|
|
<br />
|
|
<VirtualCombobox
|
|
v-model="emojiAnimationStyle"
|
|
:groups="emojiStylePickerGroups"
|
|
:placeholder="t('dialog.gallery_icons.emoji_animation_styles')"
|
|
:search-placeholder="t('dialog.gallery_icons.emoji_animation_styles')"
|
|
:clearable="false"
|
|
:close-on-select="true">
|
|
<template #item="{ item, selected }">
|
|
<div class="flex w-full items-center gap-2">
|
|
<div class="h-10 w-10 shrink-0 overflow-hidden rounded-sm bg-black/5">
|
|
<img
|
|
class="h-full w-full object-cover"
|
|
:src="`${emojiAnimationStyleUrl}${item.fileName}`"
|
|
loading="lazy" />
|
|
</div>
|
|
<span class="truncate text-sm" v-text="item.label"></span>
|
|
<span v-if="selected" class="ml-auto opacity-70">✓</span>
|
|
</div>
|
|
</template>
|
|
</VirtualCombobox>
|
|
<label class="inline-flex items-center gap-2">
|
|
<Checkbox v-model="emojiAnimType" />
|
|
<span>{{ t('dialog.gallery_icons.emoji_animation_type') }}</span>
|
|
</label>
|
|
<template v-if="emojiAnimType">
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
class="mr-3"
|
|
@click="openExternalLink('https://vrcemoji.com')">
|
|
{{ t('dialog.gallery_icons.create_animated_emoji') }}
|
|
</Button>
|
|
<span style="margin-right: 10px">{{ t('dialog.gallery_icons.emoji_animation_fps') }}</span>
|
|
<NumberField
|
|
v-model="emojiAnimFps"
|
|
:min="1"
|
|
:max="64"
|
|
:step="1"
|
|
:format-options="{ maximumFractionDigits: 0 }"
|
|
class="mr-2.5 w-28">
|
|
<NumberFieldContent>
|
|
<NumberFieldDecrement />
|
|
<NumberFieldInput />
|
|
<NumberFieldIncrement />
|
|
</NumberFieldContent>
|
|
</NumberField>
|
|
<span style="margin-right: 10px">{{
|
|
t('dialog.gallery_icons.emoji_animation_frame_count')
|
|
}}</span>
|
|
<NumberField
|
|
v-model="emojiAnimFrameCount"
|
|
:min="2"
|
|
:max="64"
|
|
:step="1"
|
|
:format-options="{ maximumFractionDigits: 0 }"
|
|
class="mr-2.5 w-28">
|
|
<NumberFieldContent>
|
|
<NumberFieldDecrement />
|
|
<NumberFieldInput />
|
|
<NumberFieldIncrement />
|
|
</NumberFieldContent>
|
|
</NumberField>
|
|
<label class="inline-flex items-center gap-2" style="margin-left: 10px; margin-right: 10px">
|
|
<Checkbox v-model="emojiAnimLoopPingPong" />
|
|
<span>{{ t('dialog.gallery_icons.emoji_loop_pingpong') }}</span>
|
|
</label>
|
|
<br />
|
|
<br />
|
|
<span>{{ t('dialog.gallery_icons.flipbook_info') }}</span>
|
|
</template>
|
|
</div>
|
|
<br />
|
|
<div
|
|
class="x-friend-item"
|
|
v-for="image in emojiTable"
|
|
:key="image.id"
|
|
style="display: inline-block; margin-top: 10px; width: unset; cursor: default">
|
|
<template v-if="image.versions && image.versions.length > 0">
|
|
<div
|
|
class="h-[200px] w-[200px] rounded-[20px] overflow-hidden cursor-pointer"
|
|
v-if="image.versions[image.versions.length - 1].file.url"
|
|
@click="
|
|
showFullscreenImageDialog(
|
|
image.versions[image.versions.length - 1].file.url,
|
|
getEmojiFileName(image)
|
|
)
|
|
">
|
|
<Emoji
|
|
:imageUrl="image.versions[image.versions.length - 1].file.url"
|
|
:size="200"></Emoji>
|
|
</div>
|
|
<div style="display: inline-block; margin: 5px">
|
|
<span v-if="image.loopStyle === 'pingpong'">
|
|
<RefreshCw style="margin-right: 5px" />
|
|
</span>
|
|
<span style="margin-right: 5px">{{ image.animationStyle }}</span>
|
|
<span v-if="image.framesOverTime" style="margin-right: 5px"
|
|
>{{ image.framesOverTime }}fps</span
|
|
>
|
|
<span v-if="image.frames" style="margin-right: 5px">{{ image.frames }}frames</span>
|
|
<br />
|
|
</div>
|
|
<div class="float-right" style="margin-top: 5px">
|
|
<Button
|
|
class="rounded-full mr-2"
|
|
size="icon-sm"
|
|
variant="outline"
|
|
@click="
|
|
showFullscreenImageDialog(
|
|
image.versions[image.versions.length - 1].file.url,
|
|
getEmojiFileName(image)
|
|
)
|
|
"
|
|
><Maximize2
|
|
/></Button>
|
|
<Button
|
|
class="rounded-full mr-2"
|
|
size="icon-sm"
|
|
variant="outline"
|
|
@click="deleteEmoji(image.id)">
|
|
<Trash2
|
|
/></Button></div
|
|
></template>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<template #stickers>
|
|
<div>
|
|
<input
|
|
id="StickerUploadButton"
|
|
type="file"
|
|
accept="image/*"
|
|
@change="onFileChangeSticker"
|
|
style="display: none" />
|
|
<span>{{ t('dialog.gallery_icons.recommended_image_size') }}: 1024x1024px (1:1)</span>
|
|
<br />
|
|
<br />
|
|
<ButtonGroup>
|
|
<Button variant="outline" size="sm" @click="refreshStickerTable">
|
|
<RefreshCw />
|
|
{{ t('dialog.gallery_icons.refresh') }}
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
:disabled="!isLocalUserVrcPlusSupporter || isUploading"
|
|
@click="displayStickerUpload">
|
|
<Upload />
|
|
{{ t('dialog.gallery_icons.upload') }}
|
|
</Button>
|
|
</ButtonGroup>
|
|
<br />
|
|
<div
|
|
class="x-friend-item"
|
|
v-for="image in stickerTable"
|
|
:key="image.id"
|
|
style="display: inline-block; margin-top: 10px; width: unset; cursor: default">
|
|
<template v-if="image.versions && image.versions.length > 0">
|
|
<div
|
|
class="h-[200px] w-[200px] rounded-[20px] overflow-hidden cursor-pointer"
|
|
v-if="image.versions[image.versions.length - 1].file.url"
|
|
@click="showFullscreenImageDialog(image.versions[image.versions.length - 1].file.url)">
|
|
<img
|
|
class="h-full w-full rounded-[15px] object-cover"
|
|
:src="image.versions[image.versions.length - 1].file.url"
|
|
loading="lazy" />
|
|
</div>
|
|
<div class="float-right" style="margin-top: 5px">
|
|
<Button
|
|
class="rounded-full mr-2"
|
|
size="icon-sm"
|
|
variant="outline"
|
|
@click="
|
|
showFullscreenImageDialog(image.versions[image.versions.length - 1].file.url)
|
|
"
|
|
><Maximize2
|
|
/></Button>
|
|
<Button
|
|
class="rounded-full"
|
|
size="icon-sm"
|
|
variant="outline"
|
|
@click="deleteSticker(image.id)"
|
|
><Trash2
|
|
/></Button></div
|
|
></template>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<template #prints>
|
|
<div>
|
|
<input
|
|
id="PrintUploadButton"
|
|
type="file"
|
|
accept="image/*"
|
|
@change="onFileChangePrint"
|
|
style="display: none" />
|
|
<span>{{ t('dialog.gallery_icons.recommended_image_size') }}: 1920x1080px (16:9)</span>
|
|
<br />
|
|
<br />
|
|
<div style="display: flex; align-items: center">
|
|
<ButtonGroup>
|
|
<Button variant="outline" size="sm" @click="refreshPrintTable">
|
|
<RefreshCw />
|
|
{{ t('dialog.gallery_icons.refresh') }}
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
:disabled="!isLocalUserVrcPlusSupporter || isUploading"
|
|
@click="displayPrintUpload">
|
|
<Upload />
|
|
{{ t('dialog.gallery_icons.upload') }}
|
|
</Button>
|
|
</ButtonGroup>
|
|
<InputGroupTextareaField
|
|
v-model="printUploadNote"
|
|
:rows="1"
|
|
:maxlength="32"
|
|
style="margin-left: 10px; width: 300px"
|
|
:placeholder="t('dialog.gallery_icons.note')"
|
|
input-class="resize-none min-h-0" />
|
|
<label class="inline-flex items-center gap-2" style="margin-left: 10px; margin-right: 10px">
|
|
<Checkbox v-model="printCropBorder" />
|
|
<span>{{ t('dialog.gallery_icons.crop_print_border') }}</span>
|
|
</label>
|
|
</div>
|
|
<br />
|
|
<div
|
|
class="x-friend-item"
|
|
v-for="image in printTable"
|
|
:key="image.id"
|
|
style="display: inline-block; margin-top: 10px; width: unset; cursor: default">
|
|
<div
|
|
class="h-[200px] w-[200px] rounded-[20px] overflow-hidden cursor-pointer"
|
|
@click="showFullscreenImageDialog(image.files.image, getPrintFileName(image))">
|
|
<img
|
|
class="h-full w-full rounded-[15px] object-cover"
|
|
:src="image.files.image"
|
|
loading="lazy" />
|
|
</div>
|
|
<div style="margin-top: 5px; width: 208px">
|
|
<span class="block truncate" v-if="image.note" v-text="image.note"></span>
|
|
<span v-else class="block"> </span>
|
|
<Location
|
|
class="block truncate"
|
|
v-if="image.worldId"
|
|
:location="image.worldId"
|
|
:hint="image.worldName" />
|
|
<span v-else class="block"> </span>
|
|
<DisplayName
|
|
class="block truncate gallery-meta"
|
|
v-if="image.authorId"
|
|
:userid="image.authorId"
|
|
:hint="image.authorName" />
|
|
<span v-else class="gallery-meta"> </span>
|
|
<span v-if="image.createdAt" class="block truncate gallery-meta gallery-meta--small">
|
|
{{ formatDateFilter(image.createdAt, 'long') }}
|
|
</span>
|
|
<span v-else class="block"> </span>
|
|
</div>
|
|
<div class="float-right">
|
|
<Button
|
|
class="rounded-full mr-2"
|
|
size="icon-sm"
|
|
variant="outline"
|
|
@click="showFullscreenImageDialog(image.files.image, getPrintFileName(image))">
|
|
<Maximize2
|
|
/></Button>
|
|
<Button
|
|
class="rounded-full"
|
|
size="icon-sm"
|
|
variant="outline"
|
|
@click="deletePrint(image.id)">
|
|
<Trash2
|
|
/></Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<template #inventory>
|
|
<div>
|
|
<br />
|
|
<br />
|
|
<div style="display: flex; align-items: center">
|
|
<ButtonGroup>
|
|
<Button variant="outline" size="sm" @click="getInventory">
|
|
<RefreshCw />
|
|
{{ t('dialog.gallery_icons.refresh') }}
|
|
</Button>
|
|
<Button variant="outline" size="sm" @click="redeemReward">
|
|
<Gift />
|
|
{{ t('dialog.gallery_icons.redeem') }}
|
|
</Button>
|
|
</ButtonGroup>
|
|
</div>
|
|
<br />
|
|
<div
|
|
class="x-friend-item"
|
|
v-for="item in inventoryTable"
|
|
:key="item.id"
|
|
style="display: inline-block; margin-top: 10px; width: unset; cursor: default">
|
|
<div class="h-[200px] w-[200px] rounded-[20px] overflow-hidden cursor-default">
|
|
<img
|
|
class="h-full w-full rounded-[15px] object-cover"
|
|
:src="item.imageUrl"
|
|
loading="lazy" />
|
|
</div>
|
|
<div style="margin-top: 5px; width: 208px">
|
|
<span class="block truncate" v-text="item.name"></span>
|
|
<span v-if="item.description" class="block truncate" v-text="item.description"></span>
|
|
<span v-else class="block"> </span>
|
|
<span class="block truncate gallery-meta gallery-meta--small">
|
|
{{ formatDateFilter(item.created_at, 'long') }}
|
|
</span>
|
|
<span v-if="item.itemType === 'prop'">{{ t('dialog.gallery_icons.item') }}</span>
|
|
<span v-else-if="item.itemType === 'sticker'">{{ t('dialog.gallery_icons.sticker') }}</span>
|
|
<span v-else-if="item.itemType === 'droneskin'">{{
|
|
t('dialog.gallery_icons.drone_skin')
|
|
}}</span>
|
|
<span v-else-if="item.itemType === 'emoji'">{{ t('dialog.gallery_icons.emoji') }}</span>
|
|
<span v-else v-text="item.itemTypeLabel"></span>
|
|
</div>
|
|
<Button
|
|
size="sm"
|
|
v-if="item.itemType === 'bundle'"
|
|
@click="consumeInventoryBundle(item.id)"
|
|
class="float-right">
|
|
{{ t('dialog.gallery_icons.consume_bundle') }}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</TabsUnderline>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ArrowLeft, Gift, Maximize2, RefreshCw, Trash2, Upload, X } from 'lucide-vue-next';
|
|
import {
|
|
NumberField,
|
|
NumberFieldContent,
|
|
NumberFieldDecrement,
|
|
NumberFieldIncrement,
|
|
NumberFieldInput
|
|
} from '@/components/ui/number-field';
|
|
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
|
import { Button } from '@/components/ui/button';
|
|
import { ButtonGroup } from '@/components/ui/button-group';
|
|
import { Checkbox } from '@/components/ui/checkbox';
|
|
import { InputGroupTextareaField } from '@/components/ui/input-group';
|
|
import { TabsUnderline } from '@/components/ui/tabs';
|
|
import { VirtualCombobox } from '@/components/ui/virtual-combobox';
|
|
import { storeToRefs } from 'pinia';
|
|
import { toast } from 'vue-sonner';
|
|
import { useI18n } from 'vue-i18n';
|
|
import { useRouter } from 'vue-router';
|
|
|
|
import {
|
|
extractFileId,
|
|
formatDateFilter,
|
|
getEmojiFileName,
|
|
getPrintFileName,
|
|
openExternalLink
|
|
} from '../../shared/utils';
|
|
import { inventoryRequest, miscRequest, userRequest, vrcPlusIconRequest, vrcPlusImageRequest } from '../../api';
|
|
import { useAdvancedSettingsStore, useAuthStore, useGalleryStore, useModalStore, useUserStore } from '../../stores';
|
|
import { emojiAnimationStyleList, emojiAnimationStyleUrl } from '../../shared/constants';
|
|
import { AppDebug } from '../../service/appConfig';
|
|
import { handleImageUploadInput } from '../../shared/utils/imageUpload';
|
|
|
|
import Emoji from '../../components/Emoji.vue';
|
|
|
|
const { t } = useI18n();
|
|
const router = useRouter();
|
|
const modalStore = useModalStore();
|
|
|
|
const {
|
|
galleryTable,
|
|
galleryDialogVisible,
|
|
VRCPlusIconsTable,
|
|
printUploadNote,
|
|
printCropBorder,
|
|
stickerTable,
|
|
printTable,
|
|
emojiTable,
|
|
inventoryTable
|
|
} = storeToRefs(useGalleryStore());
|
|
const {
|
|
loadGalleryData,
|
|
refreshGalleryTable,
|
|
refreshVRCPlusIconsTable,
|
|
refreshStickerTable,
|
|
refreshPrintTable,
|
|
refreshEmojiTable,
|
|
getInventory,
|
|
handleStickerAdd,
|
|
handleGalleryImageAdd
|
|
} = useGalleryStore();
|
|
|
|
const { currentUserInventory } = storeToRefs(useAdvancedSettingsStore());
|
|
const { showFullscreenImageDialog } = useGalleryStore();
|
|
const { currentUser, isLocalUserVrcPlusSupporter } = storeToRefs(useUserStore());
|
|
const { cachedConfig } = storeToRefs(useAuthStore());
|
|
const cachedConfigTyped = computed(
|
|
() => /** @type {{ maxUserEmoji?: number, maxUserStickers?: number }} */ (cachedConfig.value ?? {})
|
|
);
|
|
const galleryTabs = computed(() => [
|
|
{ value: 'gallery', label: t('dialog.gallery_icons.gallery') },
|
|
{ value: 'icons', label: t('dialog.gallery_icons.icons') },
|
|
{ value: 'emojis', label: t('dialog.gallery_icons.emojis') },
|
|
{ value: 'stickers', label: t('dialog.gallery_icons.stickers') },
|
|
{ value: 'prints', label: t('dialog.gallery_icons.prints') },
|
|
{ value: 'inventory', label: t('dialog.gallery_icons.inventory') }
|
|
]);
|
|
|
|
const emojiAnimFps = ref(15);
|
|
const emojiAnimFrameCount = ref(4);
|
|
const emojiAnimType = ref(false);
|
|
const emojiAnimationStyle = ref('Stop');
|
|
const emojiAnimLoopPingPong = ref(false);
|
|
|
|
const emojiStylePickerGroups = computed(() => [
|
|
{
|
|
key: 'emojiAnimationStyles',
|
|
label: t('dialog.gallery_icons.emoji_animation_styles'),
|
|
items: Object.entries(emojiAnimationStyleList).map(([styleName, fileName]) => ({
|
|
value: styleName,
|
|
label: styleName,
|
|
search: styleName,
|
|
fileName
|
|
}))
|
|
}
|
|
]);
|
|
|
|
const pendingUploads = ref(0);
|
|
const isUploading = computed(() => pendingUploads.value > 0);
|
|
|
|
onMounted(() => {
|
|
galleryDialogVisible.value = true;
|
|
loadGalleryData();
|
|
});
|
|
|
|
onBeforeUnmount(() => {
|
|
galleryDialogVisible.value = false;
|
|
});
|
|
|
|
function startUpload() {
|
|
pendingUploads.value += 1;
|
|
}
|
|
|
|
function finishUpload() {
|
|
pendingUploads.value = Math.max(0, pendingUploads.value - 1);
|
|
}
|
|
|
|
function goBack() {
|
|
galleryDialogVisible.value = false;
|
|
router.push({ name: 'tools' });
|
|
}
|
|
|
|
function onFileChangeGallery(e) {
|
|
const { file, clearInput } = handleImageUploadInput(e, {
|
|
inputSelector: '#GalleryUploadButton',
|
|
tooLargeMessage: () => t('message.file.too_large'),
|
|
invalidTypeMessage: () => t('message.file.not_image')
|
|
});
|
|
if (!file) {
|
|
return;
|
|
}
|
|
startUpload();
|
|
const r = new FileReader();
|
|
const handleReaderError = () => finishUpload();
|
|
r.onerror = handleReaderError;
|
|
r.onabort = handleReaderError;
|
|
r.onload = function () {
|
|
try {
|
|
const base64Body = btoa(r.result.toString());
|
|
const uploadPromise = vrcPlusImageRequest.uploadGalleryImage(base64Body).then((args) => {
|
|
handleGalleryImageAdd(args);
|
|
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', error);
|
|
})
|
|
.finally(() => finishUpload());
|
|
} catch (error) {
|
|
finishUpload();
|
|
console.error('Failed to process image', error);
|
|
}
|
|
};
|
|
try {
|
|
r.readAsBinaryString(file);
|
|
} catch (error) {
|
|
clearInput();
|
|
finishUpload();
|
|
console.error('Failed to read file', error);
|
|
}
|
|
clearInput();
|
|
}
|
|
|
|
function displayGalleryUpload() {
|
|
document.getElementById('GalleryUploadButton').click();
|
|
}
|
|
|
|
function setProfilePicOverride(fileId) {
|
|
if (!isLocalUserVrcPlusSupporter.value) {
|
|
toast.error(t('message.vrcplus.required'));
|
|
return;
|
|
}
|
|
let profilePicOverride = '';
|
|
if (fileId) {
|
|
profilePicOverride = `${AppDebug.endpointDomain}/file/${fileId}/1`;
|
|
}
|
|
if (profilePicOverride === currentUser.value.profilePicOverride) {
|
|
return;
|
|
}
|
|
userRequest
|
|
.saveCurrentUser({
|
|
profilePicOverride
|
|
})
|
|
.then((args) => {
|
|
toast.success('Profile picture changed');
|
|
return args;
|
|
});
|
|
}
|
|
|
|
function compareCurrentProfilePic(fileId) {
|
|
const currentProfilePicOverride = extractFileId(currentUser.value.profilePicOverride);
|
|
if (fileId === currentProfilePicOverride) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function deleteGalleryImage(fileId) {
|
|
miscRequest.deleteFile(fileId).then((args) => {
|
|
const array = galleryTable.value;
|
|
const { length } = array;
|
|
for (let i = 0; i < length; ++i) {
|
|
if (args.fileId === array[i].id) {
|
|
array.splice(i, 1);
|
|
break;
|
|
}
|
|
}
|
|
|
|
return args;
|
|
});
|
|
}
|
|
|
|
function onFileChangeVRCPlusIcon(e) {
|
|
const { file, clearInput } = handleImageUploadInput(e, {
|
|
inputSelector: '#VRCPlusIconUploadButton',
|
|
tooLargeMessage: () => t('message.file.too_large'),
|
|
invalidTypeMessage: () => t('message.file.not_image')
|
|
});
|
|
if (!file) {
|
|
return;
|
|
}
|
|
startUpload();
|
|
const r = new FileReader();
|
|
const handleReaderError = () => finishUpload();
|
|
r.onerror = handleReaderError;
|
|
r.onabort = handleReaderError;
|
|
r.onload = function () {
|
|
try {
|
|
const base64Body = btoa(r.result.toString());
|
|
const uploadPromise = vrcPlusIconRequest.uploadVRCPlusIcon(base64Body).then((args) => {
|
|
if (Object.keys(VRCPlusIconsTable.value).length !== 0) {
|
|
VRCPlusIconsTable.value.unshift(args.json);
|
|
}
|
|
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 VRC+ icon', error);
|
|
})
|
|
.finally(() => finishUpload());
|
|
} catch (error) {
|
|
finishUpload();
|
|
console.error('Failed to process upload', error);
|
|
}
|
|
};
|
|
try {
|
|
r.readAsBinaryString(file);
|
|
} catch (error) {
|
|
clearInput();
|
|
finishUpload();
|
|
console.error('Failed to read file', error);
|
|
}
|
|
clearInput();
|
|
}
|
|
|
|
function displayVRCPlusIconUpload() {
|
|
document.getElementById('VRCPlusIconUploadButton').click();
|
|
}
|
|
|
|
function setVRCPlusIcon(fileId) {
|
|
if (!isLocalUserVrcPlusSupporter.value) {
|
|
toast.error(t('message.vrcplus.required'));
|
|
return;
|
|
}
|
|
let userIcon = '';
|
|
if (fileId) {
|
|
userIcon = `${AppDebug.endpointDomain}/file/${fileId}/1`;
|
|
}
|
|
if (userIcon === currentUser.value.userIcon) {
|
|
return;
|
|
}
|
|
userRequest
|
|
.saveCurrentUser({
|
|
userIcon
|
|
})
|
|
.then((args) => {
|
|
toast.success('Icon changed');
|
|
return args;
|
|
});
|
|
}
|
|
|
|
function compareCurrentVRCPlusIcon(userIcon) {
|
|
const currentUserIcon = extractFileId(currentUser.value.userIcon);
|
|
if (userIcon === currentUserIcon) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function deleteVRCPlusIcon(fileId) {
|
|
miscRequest.deleteFile(fileId).then((args) => {
|
|
const array = VRCPlusIconsTable.value;
|
|
const { length } = array;
|
|
for (let i = 0; i < length; ++i) {
|
|
if (args.fileId === array[i].id) {
|
|
array.splice(i, 1);
|
|
break;
|
|
}
|
|
}
|
|
return args;
|
|
});
|
|
}
|
|
|
|
function parseEmojiFileName(fileName) {
|
|
// remove file extension
|
|
fileName = fileName.replace(/\.[^/.]+$/, '');
|
|
const array = fileName.split('_');
|
|
for (let i = 0; i < array.length; ++i) {
|
|
const value = array[i];
|
|
if (value.endsWith('animationStyle')) {
|
|
emojiAnimType.value = false;
|
|
emojiAnimationStyle.value = value.replace('animationStyle', '').toLowerCase();
|
|
}
|
|
if (value.endsWith('frames')) {
|
|
emojiAnimType.value = true;
|
|
emojiAnimFrameCount.value = parseInt(value.replace('frames', ''));
|
|
}
|
|
if (value.endsWith('fps')) {
|
|
emojiAnimFps.value = parseInt(value.replace('fps', ''));
|
|
}
|
|
if (value.endsWith('loopStyle')) {
|
|
emojiAnimLoopPingPong.value = value.replace('loopStyle', '').toLowerCase();
|
|
}
|
|
}
|
|
}
|
|
|
|
function onFileChangeEmoji(e) {
|
|
const { file, clearInput } = handleImageUploadInput(e, {
|
|
inputSelector: '#EmojiUploadButton',
|
|
tooLargeMessage: () => t('message.file.too_large'),
|
|
invalidTypeMessage: () => t('message.file.not_image')
|
|
});
|
|
if (!file) {
|
|
return;
|
|
}
|
|
startUpload();
|
|
// set Emoji settings from fileName
|
|
parseEmojiFileName(file.name);
|
|
const r = new FileReader();
|
|
const handleReaderError = () => finishUpload();
|
|
r.onerror = handleReaderError;
|
|
r.onabort = handleReaderError;
|
|
r.onload = function () {
|
|
try {
|
|
const params = {
|
|
tag: emojiAnimType.value ? 'emojianimated' : 'emoji',
|
|
animationStyle: emojiAnimationStyle.value.toLowerCase(),
|
|
maskTag: 'square'
|
|
};
|
|
if (emojiAnimType.value) {
|
|
params.frames = emojiAnimFrameCount.value;
|
|
params.framesOverTime = emojiAnimFps.value;
|
|
}
|
|
if (emojiAnimLoopPingPong.value) {
|
|
params.loopStyle = 'pingpong';
|
|
}
|
|
const base64Body = btoa(r.result.toString());
|
|
const uploadPromise = vrcPlusImageRequest.uploadEmoji(base64Body, params).then((args) => {
|
|
if (Object.keys(emojiTable.value).length !== 0) {
|
|
emojiTable.value.unshift(args.json);
|
|
}
|
|
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', error);
|
|
})
|
|
.finally(() => finishUpload());
|
|
} catch (error) {
|
|
finishUpload();
|
|
console.error('Failed to process upload', error);
|
|
}
|
|
};
|
|
try {
|
|
r.readAsBinaryString(file);
|
|
} catch (error) {
|
|
clearInput();
|
|
finishUpload();
|
|
console.error('Failed to read file', error);
|
|
}
|
|
clearInput();
|
|
}
|
|
|
|
function displayEmojiUpload() {
|
|
document.getElementById('EmojiUploadButton').click();
|
|
}
|
|
|
|
function deleteEmoji(fileId) {
|
|
miscRequest.deleteFile(fileId).then((args) => {
|
|
const array = emojiTable.value;
|
|
const { length } = array;
|
|
for (let i = 0; i < length; ++i) {
|
|
if (args.fileId === array[i].id) {
|
|
array.splice(i, 1);
|
|
break;
|
|
}
|
|
}
|
|
return args;
|
|
});
|
|
}
|
|
|
|
function onFileChangeSticker(e) {
|
|
const { file, clearInput } = handleImageUploadInput(e, {
|
|
inputSelector: '#StickerUploadButton',
|
|
tooLargeMessage: () => t('message.file.too_large'),
|
|
invalidTypeMessage: () => t('message.file.not_image')
|
|
});
|
|
if (!file) {
|
|
return;
|
|
}
|
|
startUpload();
|
|
const r = new FileReader();
|
|
const handleReaderError = () => finishUpload();
|
|
r.onerror = handleReaderError;
|
|
r.onabort = handleReaderError;
|
|
r.onload = function () {
|
|
try {
|
|
const params = {
|
|
tag: 'sticker',
|
|
maskTag: 'square'
|
|
};
|
|
const base64Body = btoa(r.result.toString());
|
|
const uploadPromise = vrcPlusImageRequest.uploadSticker(base64Body, params).then((args) => {
|
|
handleStickerAdd(args);
|
|
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', error);
|
|
})
|
|
.finally(() => finishUpload());
|
|
} catch (error) {
|
|
finishUpload();
|
|
console.error('Failed to process upload', error);
|
|
}
|
|
};
|
|
try {
|
|
r.readAsBinaryString(file);
|
|
} catch (error) {
|
|
clearInput();
|
|
finishUpload();
|
|
console.error('Failed to read file', error);
|
|
}
|
|
clearInput();
|
|
}
|
|
|
|
function displayStickerUpload() {
|
|
document.getElementById('StickerUploadButton').click();
|
|
}
|
|
|
|
function deleteSticker(fileId) {
|
|
miscRequest.deleteFile(fileId).then((args) => {
|
|
const array = stickerTable.value;
|
|
const { length } = array;
|
|
for (let i = 0; i < length; ++i) {
|
|
if (args.fileId === array[i].id) {
|
|
array.splice(i, 1);
|
|
break;
|
|
}
|
|
}
|
|
|
|
return args;
|
|
});
|
|
}
|
|
|
|
function onFileChangePrint(e) {
|
|
const { file, clearInput } = handleImageUploadInput(e, {
|
|
inputSelector: '#PrintUploadButton',
|
|
tooLargeMessage: () => t('message.file.too_large'),
|
|
invalidTypeMessage: () => t('message.file.not_image')
|
|
});
|
|
if (!file) {
|
|
return;
|
|
}
|
|
startUpload();
|
|
const r = new FileReader();
|
|
const handleReaderError = () => finishUpload();
|
|
r.onerror = handleReaderError;
|
|
r.onabort = handleReaderError;
|
|
r.onload = function () {
|
|
try {
|
|
const date = new Date();
|
|
// why the fuck isn't this UTC
|
|
date.setMinutes(date.getMinutes() - date.getTimezoneOffset());
|
|
const timestamp = date.toISOString().slice(0, 19);
|
|
const params = {
|
|
note: printUploadNote.value,
|
|
// worldId: '',
|
|
timestamp
|
|
};
|
|
const base64Body = btoa(r.result.toString());
|
|
const cropWhiteBorder = printCropBorder.value;
|
|
const uploadPromise = vrcPlusImageRequest
|
|
.uploadPrint(base64Body, cropWhiteBorder, params)
|
|
.then((args) => {
|
|
if (Object.keys(printTable.value).length !== 0) {
|
|
printTable.value.unshift(args.json);
|
|
}
|
|
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', error);
|
|
})
|
|
.finally(() => finishUpload());
|
|
} catch (error) {
|
|
finishUpload();
|
|
console.error('Failed to process upload', error);
|
|
}
|
|
};
|
|
try {
|
|
r.readAsBinaryString(file);
|
|
} catch (error) {
|
|
clearInput();
|
|
finishUpload();
|
|
console.error('Failed to read file', error);
|
|
}
|
|
clearInput();
|
|
}
|
|
|
|
function displayPrintUpload() {
|
|
document.getElementById('PrintUploadButton').click();
|
|
}
|
|
|
|
function deletePrint(printId) {
|
|
vrcPlusImageRequest.deletePrint(printId).then((args) => {
|
|
const array = printTable.value;
|
|
const { length } = array;
|
|
for (let i = 0; i < length; ++i) {
|
|
if (args.printId === array[i].id) {
|
|
array.splice(i, 1);
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
async function consumeInventoryBundle(inventoryId) {
|
|
try {
|
|
await inventoryRequest.consumeInventoryBundle({
|
|
inventoryId
|
|
});
|
|
currentUserInventory.value.delete(inventoryId);
|
|
const array = inventoryTable.value;
|
|
const { length } = array;
|
|
for (let i = 0; i < length; ++i) {
|
|
if (inventoryId === array[i].id) {
|
|
array.splice(i, 1);
|
|
break;
|
|
}
|
|
}
|
|
getInventory();
|
|
} catch (error) {
|
|
console.error('Error consuming inventory bundle:', error);
|
|
}
|
|
// -- response --
|
|
// errors: []
|
|
// inventoryItems : []
|
|
// inventoryItemsCreated: 0
|
|
}
|
|
|
|
async function redeemReward() {
|
|
modalStore
|
|
.prompt({
|
|
title: t('prompt.redeem.header'),
|
|
description: t('prompt.redeem.description'),
|
|
confirmText: t('prompt.redeem.redeem'),
|
|
cancelText: t('prompt.redeem.cancel')
|
|
})
|
|
.then(({ ok, value }) => {
|
|
if (!ok) return;
|
|
if (value) {
|
|
inventoryRequest
|
|
.redeemReward({
|
|
code: value.trim()
|
|
})
|
|
.then((args) => {
|
|
toast.success(t('prompt.redeem.success'));
|
|
getInventory();
|
|
return args;
|
|
})
|
|
.catch((error) => {
|
|
console.error('Error redeeming reward:', error);
|
|
});
|
|
}
|
|
})
|
|
.catch(() => {});
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.gallery-tab-count {
|
|
font-size: 12px;
|
|
margin-left: 5px;
|
|
}
|
|
|
|
.gallery-meta {
|
|
font-family: monospace;
|
|
display: block;
|
|
}
|
|
|
|
.gallery-meta--small {
|
|
font-size: 11px;
|
|
}
|
|
</style>
|