add image download buttons to detail scaffold
This commit is contained in:
parent
2771e202cb
commit
38762c8946
3 changed files with 406 additions and 25 deletions
|
|
@ -1,9 +1,11 @@
|
||||||
<svelte:options runes={true} />
|
<svelte:options runes={true} />
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { DropdownMenu } from 'bits-ui'
|
||||||
import DetailsHeader from '$lib/components/ui/DetailsHeader.svelte'
|
import DetailsHeader from '$lib/components/ui/DetailsHeader.svelte'
|
||||||
import SegmentedControl from '$lib/components/ui/segmented-control/SegmentedControl.svelte'
|
import SegmentedControl from '$lib/components/ui/segmented-control/SegmentedControl.svelte'
|
||||||
import Segment from '$lib/components/ui/segmented-control/Segment.svelte'
|
import Segment from '$lib/components/ui/segmented-control/Segment.svelte'
|
||||||
|
import Button from '$lib/components/ui/Button.svelte'
|
||||||
import type { Snippet } from 'svelte'
|
import type { Snippet } from 'svelte'
|
||||||
|
|
||||||
export type DetailTab = 'info' | 'images' | 'raw'
|
export type DetailTab = 'info' | 'images' | 'raw'
|
||||||
|
|
@ -24,6 +26,10 @@
|
||||||
currentTab?: DetailTab
|
currentTab?: DetailTab
|
||||||
onTabChange?: (tab: DetailTab) => void
|
onTabChange?: (tab: DetailTab) => void
|
||||||
showTabs?: boolean
|
showTabs?: boolean
|
||||||
|
// Image download handlers
|
||||||
|
onDownloadAllImages?: (force: boolean) => Promise<void>
|
||||||
|
onDownloadSize?: (size: string) => Promise<void>
|
||||||
|
availableSizes?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
|
|
@ -41,12 +47,45 @@
|
||||||
currentTab = 'info',
|
currentTab = 'info',
|
||||||
onTabChange,
|
onTabChange,
|
||||||
showTabs = true,
|
showTabs = true,
|
||||||
|
onDownloadAllImages,
|
||||||
|
onDownloadSize,
|
||||||
|
availableSizes = [],
|
||||||
children
|
children
|
||||||
}: Props & { children: Snippet } = $props()
|
}: Props & { children: Snippet } = $props()
|
||||||
|
|
||||||
|
let isDownloading = $state(false)
|
||||||
|
let dropdownOpen = $state(false)
|
||||||
|
|
||||||
function handleTabChange(value: string) {
|
function handleTabChange(value: string) {
|
||||||
onTabChange?.(value as DetailTab)
|
onTabChange?.(value as DetailTab)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleDownloadAll(force: boolean) {
|
||||||
|
if (!onDownloadAllImages) return
|
||||||
|
isDownloading = true
|
||||||
|
dropdownOpen = false
|
||||||
|
try {
|
||||||
|
await onDownloadAllImages(force)
|
||||||
|
} finally {
|
||||||
|
isDownloading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDownloadSize(size: string) {
|
||||||
|
if (!onDownloadSize) return
|
||||||
|
isDownloading = true
|
||||||
|
dropdownOpen = false
|
||||||
|
try {
|
||||||
|
await onDownloadSize(size)
|
||||||
|
} finally {
|
||||||
|
isDownloading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show download button when on images tab and can edit
|
||||||
|
const showDownloadDropdown = $derived(
|
||||||
|
showEdit && currentTab === 'images' && onDownloadAllImages && !editMode
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
|
|
@ -74,6 +113,48 @@
|
||||||
<Segment value="images">Images</Segment>
|
<Segment value="images">Images</Segment>
|
||||||
<Segment value="raw">Raw Data</Segment>
|
<Segment value="raw">Raw Data</Segment>
|
||||||
</SegmentedControl>
|
</SegmentedControl>
|
||||||
|
|
||||||
|
{#if showDownloadDropdown}
|
||||||
|
<div class="download-dropdown">
|
||||||
|
<DropdownMenu.Root bind:open={dropdownOpen}>
|
||||||
|
<DropdownMenu.Trigger>
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<Button {...props} variant="secondary" size="small" disabled={isDownloading}>
|
||||||
|
{isDownloading ? 'Downloading...' : 'Download Images'}
|
||||||
|
</Button>
|
||||||
|
{/snippet}
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
|
||||||
|
<DropdownMenu.Portal>
|
||||||
|
<DropdownMenu.Content class="dropdown-menu" sideOffset={4}>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
class="dropdown-menu-item"
|
||||||
|
onclick={() => handleDownloadAll(false)}
|
||||||
|
>
|
||||||
|
Download All Images
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
class="dropdown-menu-item"
|
||||||
|
onclick={() => handleDownloadAll(true)}
|
||||||
|
>
|
||||||
|
Re-download All Images
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
{#if availableSizes.length > 0}
|
||||||
|
<DropdownMenu.Separator class="dropdown-menu-separator" />
|
||||||
|
{#each availableSizes as size}
|
||||||
|
<DropdownMenu.Item
|
||||||
|
class="dropdown-menu-item"
|
||||||
|
onclick={() => handleDownloadSize(size)}
|
||||||
|
>
|
||||||
|
Download All "{size}" Images
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Portal>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|
@ -109,6 +190,15 @@
|
||||||
|
|
||||||
.tab-navigation {
|
.tab-navigation {
|
||||||
padding: spacing.$unit-2x;
|
padding: spacing.$unit-2x;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: spacing.$unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-dropdown {
|
||||||
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-controls {
|
.edit-controls {
|
||||||
|
|
@ -139,4 +229,36 @@
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Import menu styles
|
||||||
|
:global(.dropdown-menu) {
|
||||||
|
background: var(--app-bg, white);
|
||||||
|
border: 1px solid var(--border-color, #ddd);
|
||||||
|
border-radius: layout.$card-corner;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
padding: spacing.$unit-half;
|
||||||
|
min-width: calc(spacing.$unit * 22.5);
|
||||||
|
z-index: 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dropdown-menu-item) {
|
||||||
|
padding: spacing.$unit spacing.$unit-2x;
|
||||||
|
border-radius: layout.$item-corner-small;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: typography.$font-regular;
|
||||||
|
color: var(--text-primary);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: spacing.$unit;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--button-contained-bg-hover, #f5f5f5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dropdown-menu-separator) {
|
||||||
|
height: 1px;
|
||||||
|
background: var(--menu-separator, #e5e5e5);
|
||||||
|
margin: spacing.$unit-half 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,205 @@
|
||||||
<svelte:options runes={true} />
|
<svelte:options runes={true} />
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { ContextMenu } from 'bits-ui'
|
||||||
|
import ContextMenuWrapper from '$lib/components/ui/menu/ContextMenuWrapper.svelte'
|
||||||
|
|
||||||
export interface ImageItem {
|
export interface ImageItem {
|
||||||
url: string
|
url: string
|
||||||
label: string
|
label: string
|
||||||
variant: string
|
variant: string
|
||||||
pose?: string
|
pose?: string
|
||||||
|
poseLabel?: string // Custom label for the pose group (e.g., "ULB" for summons)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
images: ImageItem[]
|
images: ImageItem[]
|
||||||
|
canEdit?: boolean
|
||||||
|
onDownloadImage?: (
|
||||||
|
size: string,
|
||||||
|
transformation: string | undefined,
|
||||||
|
force: boolean
|
||||||
|
) => Promise<void>
|
||||||
|
onDownloadAllPose?: (pose: string, force: boolean) => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
let { images }: Props = $props()
|
let {
|
||||||
|
images,
|
||||||
|
canEdit = false,
|
||||||
|
onDownloadImage,
|
||||||
|
onDownloadAllPose
|
||||||
|
}: Props = $props()
|
||||||
|
|
||||||
|
// Track download status per image
|
||||||
|
let downloadingImages = $state<Set<string>>(new Set())
|
||||||
|
|
||||||
|
// Group images by pose for better layout
|
||||||
|
const imagesByPose = $derived.by(() => {
|
||||||
|
const groups = new Map<string, ImageItem[]>()
|
||||||
|
for (const image of images) {
|
||||||
|
const key = image.pose ?? 'default'
|
||||||
|
if (!groups.has(key)) {
|
||||||
|
groups.set(key, [])
|
||||||
|
}
|
||||||
|
groups.get(key)!.push(image)
|
||||||
|
}
|
||||||
|
return groups
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get pose labels in order
|
||||||
|
// Characters: 01=Base, 02=MLB, 03=FLB, 04=Transcendence
|
||||||
|
// Summons: 01=Base, 02=ULB, 03=Transcendence
|
||||||
|
// Weapons: 01=Base, 02=Transcendence
|
||||||
|
const poseOrder = ['01', '02', '03', '04', 'default']
|
||||||
|
const poseLabels: Record<string, string> = {
|
||||||
|
'01': 'Base',
|
||||||
|
'02': 'MLB', // Will be overridden by label from page for summons/weapons
|
||||||
|
'03': 'FLB',
|
||||||
|
'04': 'Transcendence',
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortedPoses = $derived(
|
||||||
|
Array.from(imagesByPose.keys()).sort(
|
||||||
|
(a, b) => poseOrder.indexOf(a) - poseOrder.indexOf(b)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Get the pose label from the first image in each group, or fall back to default
|
||||||
|
function getPoseLabel(pose: string, poseImages: ImageItem[]): string {
|
||||||
|
const customLabel = poseImages[0]?.poseLabel
|
||||||
|
if (customLabel) return customLabel
|
||||||
|
return poseLabels[pose] || pose
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a unique key for an image (for tracking download state)
|
||||||
|
function getImageKey(image: ImageItem): string {
|
||||||
|
return `${image.pose ?? 'default'}-${image.variant}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle single image download
|
||||||
|
async function handleDownload(image: ImageItem, force: boolean) {
|
||||||
|
if (!onDownloadImage) return
|
||||||
|
|
||||||
|
const key = getImageKey(image)
|
||||||
|
downloadingImages.add(key)
|
||||||
|
downloadingImages = new Set(downloadingImages)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await onDownloadImage(image.variant, image.pose, force)
|
||||||
|
} finally {
|
||||||
|
downloadingImages.delete(key)
|
||||||
|
downloadingImages = new Set(downloadingImages)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle download all images for a pose
|
||||||
|
async function handleDownloadAllPose(pose: string, force: boolean) {
|
||||||
|
if (!onDownloadAllPose) return
|
||||||
|
|
||||||
|
// Mark all images in this pose as downloading
|
||||||
|
const poseImages = imagesByPose.get(pose) ?? []
|
||||||
|
for (const img of poseImages) {
|
||||||
|
downloadingImages.add(getImageKey(img))
|
||||||
|
}
|
||||||
|
downloadingImages = new Set(downloadingImages)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await onDownloadAllPose(pose, force)
|
||||||
|
} finally {
|
||||||
|
for (const img of poseImages) {
|
||||||
|
downloadingImages.delete(getImageKey(img))
|
||||||
|
}
|
||||||
|
downloadingImages = new Set(downloadingImages)
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="images-tab">
|
<div class="images-tab">
|
||||||
<div class="images-grid">
|
{#each sortedPoses as pose}
|
||||||
{#each images as image}
|
{@const poseImages = imagesByPose.get(pose) ?? []}
|
||||||
<div class="image-item">
|
{@const poseLabel = getPoseLabel(pose, poseImages)}
|
||||||
<a href={image.url} target="_blank" rel="noopener noreferrer">
|
{@const showHeader = poseLabel && sortedPoses.length > 1}
|
||||||
<img src={image.url} alt={image.label} loading="lazy" />
|
|
||||||
</a>
|
{#if showHeader}
|
||||||
<span class="image-label">{image.label}</span>
|
<h3 class="pose-header">{poseLabel}</h3>
|
||||||
</div>
|
{/if}
|
||||||
{/each}
|
|
||||||
</div>
|
<div class="images-grid">
|
||||||
|
{#each poseImages as image}
|
||||||
|
{@const imageKey = getImageKey(image)}
|
||||||
|
{@const isDownloading = downloadingImages.has(imageKey)}
|
||||||
|
|
||||||
|
{#if canEdit && onDownloadImage}
|
||||||
|
<ContextMenuWrapper>
|
||||||
|
{#snippet trigger()}
|
||||||
|
<div class="image-item" class:downloading={isDownloading}>
|
||||||
|
<a
|
||||||
|
href={image.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="image-container"
|
||||||
|
>
|
||||||
|
<img src={image.url} alt={image.label} loading="lazy" />
|
||||||
|
{#if isDownloading}
|
||||||
|
<div class="download-overlay">
|
||||||
|
<span class="download-spinner"></span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</a>
|
||||||
|
<span class="image-label">{image.variant}</span>
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
{#snippet menu()}
|
||||||
|
<ContextMenu.Item
|
||||||
|
class="context-menu-item"
|
||||||
|
onclick={() => handleDownload(image, false)}
|
||||||
|
disabled={isDownloading}
|
||||||
|
>
|
||||||
|
Download Image
|
||||||
|
</ContextMenu.Item>
|
||||||
|
<ContextMenu.Item
|
||||||
|
class="context-menu-item"
|
||||||
|
onclick={() => handleDownload(image, true)}
|
||||||
|
disabled={isDownloading}
|
||||||
|
>
|
||||||
|
Re-download Image
|
||||||
|
</ContextMenu.Item>
|
||||||
|
{#if onDownloadAllPose}
|
||||||
|
<ContextMenu.Separator class="context-menu-separator" />
|
||||||
|
<ContextMenu.Item
|
||||||
|
class="context-menu-item"
|
||||||
|
onclick={() => handleDownloadAllPose(pose, false)}
|
||||||
|
>
|
||||||
|
Download All {poseLabel} Images
|
||||||
|
</ContextMenu.Item>
|
||||||
|
{/if}
|
||||||
|
<ContextMenu.Separator class="context-menu-separator" />
|
||||||
|
<ContextMenu.Item
|
||||||
|
class="context-menu-item"
|
||||||
|
onclick={() => window.open(image.url, '_blank')}
|
||||||
|
>
|
||||||
|
Open in New Tab
|
||||||
|
</ContextMenu.Item>
|
||||||
|
{/snippet}
|
||||||
|
</ContextMenuWrapper>
|
||||||
|
{:else}
|
||||||
|
<div class="image-item">
|
||||||
|
<a
|
||||||
|
href={image.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="image-container"
|
||||||
|
>
|
||||||
|
<img src={image.url} alt={image.label} loading="lazy" />
|
||||||
|
</a>
|
||||||
|
<span class="image-label">{image.variant}</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|
@ -38,10 +212,23 @@
|
||||||
padding: spacing.$unit-2x;
|
padding: spacing.$unit-2x;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pose-header {
|
||||||
|
font-size: typography.$font-regular;
|
||||||
|
font-weight: 600;
|
||||||
|
color: colors.$grey-30;
|
||||||
|
margin: 0 0 spacing.$unit 0;
|
||||||
|
padding-top: spacing.$unit-2x;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.images-grid {
|
.images-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||||
gap: spacing.$unit-2x;
|
gap: spacing.$unit-2x;
|
||||||
|
margin-bottom: spacing.$unit-2x;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-item {
|
.image-item {
|
||||||
|
|
@ -50,22 +237,58 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: spacing.$unit;
|
gap: spacing.$unit;
|
||||||
|
|
||||||
a {
|
&.downloading {
|
||||||
display: block;
|
opacity: 0.7;
|
||||||
border-radius: layout.$item-corner;
|
}
|
||||||
overflow: hidden;
|
}
|
||||||
transition: transform 0.2s ease;
|
|
||||||
|
|
||||||
&:hover {
|
.image-container {
|
||||||
transform: scale(1.02);
|
position: relative;
|
||||||
}
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
background: colors.$grey-90;
|
||||||
|
border-radius: layout.$item-corner;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.02);
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
display: block;
|
display: block;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
width: auto;
|
||||||
height: auto;
|
height: auto;
|
||||||
background: colors.$grey-90;
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-overlay {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-spinner {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||||
|
border-top-color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -73,5 +296,6 @@
|
||||||
font-size: typography.$font-small;
|
font-size: typography.$font-small;
|
||||||
color: colors.$grey-40;
|
color: colors.$grey-40;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
text-transform: capitalize;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -119,13 +119,33 @@ export function getCharacterDetailImage(
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get weapon image URL
|
* Get weapon image URL
|
||||||
|
* @param transformation - Optional transformation suffix ('02' for transcendence)
|
||||||
*/
|
*/
|
||||||
export function getWeaponImage(
|
export function getWeaponImage(
|
||||||
id: string | number | null | undefined,
|
id: string | number | null | undefined,
|
||||||
variant: ImageVariant = 'main',
|
variant: ImageVariant = 'main',
|
||||||
element?: number
|
element?: number,
|
||||||
|
transformation?: string
|
||||||
): string {
|
): string {
|
||||||
return getImageUrl('weapon', id, variant, { element })
|
if (!id) {
|
||||||
|
return getPlaceholderImage('weapon', variant)
|
||||||
|
}
|
||||||
|
|
||||||
|
const directory = getImageDirectory('weapon', variant)
|
||||||
|
const extension = getFileExtension('weapon', variant)
|
||||||
|
const basePath = `${getBasePath()}/${directory}`
|
||||||
|
|
||||||
|
// Handle element-specific weapon grids
|
||||||
|
if (variant === 'grid' && element && element > 0) {
|
||||||
|
return `${basePath}/${id}_${element}${extension}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle transformation suffix (transcendence)
|
||||||
|
if (transformation) {
|
||||||
|
return `${basePath}/${id}_${transformation}${extension}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${basePath}/${id}${extension}`
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -139,12 +159,27 @@ export function getWeaponBaseImage(
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get summon image URL
|
* Get summon image URL
|
||||||
|
* @param transformation - Optional transformation suffix ('02' for ULB, '03' for transcendence)
|
||||||
*/
|
*/
|
||||||
export function getSummonImage(
|
export function getSummonImage(
|
||||||
id: string | number | null | undefined,
|
id: string | number | null | undefined,
|
||||||
variant: ImageVariant = 'main'
|
variant: ImageVariant = 'main',
|
||||||
|
transformation?: string
|
||||||
): string {
|
): string {
|
||||||
return getImageUrl('summon', id, variant)
|
if (!id) {
|
||||||
|
return getPlaceholderImage('summon', variant)
|
||||||
|
}
|
||||||
|
|
||||||
|
const directory = getImageDirectory('summon', variant)
|
||||||
|
const extension = getFileExtension('summon', variant)
|
||||||
|
const basePath = `${getBasePath()}/${directory}`
|
||||||
|
|
||||||
|
// Handle transformation suffix (ULB, transcendence)
|
||||||
|
if (transformation) {
|
||||||
|
return `${basePath}/${id}_${transformation}${extension}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${basePath}/${id}${extension}`
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue