show dates on entity detail pages

This commit is contained in:
Justin Edmund 2025-12-02 01:24:53 -08:00
parent 6f1f0c60a2
commit 1b8232ed5a
3 changed files with 270 additions and 27 deletions

View file

@ -17,10 +17,10 @@
import CharacterUncapSection from '$lib/features/database/characters/sections/CharacterUncapSection.svelte' import CharacterUncapSection from '$lib/features/database/characters/sections/CharacterUncapSection.svelte'
import CharacterTaxonomySection from '$lib/features/database/characters/sections/CharacterTaxonomySection.svelte' import CharacterTaxonomySection from '$lib/features/database/characters/sections/CharacterTaxonomySection.svelte'
import CharacterStatsSection from '$lib/features/database/characters/sections/CharacterStatsSection.svelte' import CharacterStatsSection from '$lib/features/database/characters/sections/CharacterStatsSection.svelte'
import CharacterImagesSection from '$lib/features/database/characters/sections/CharacterImagesSection.svelte'
import EntityImagesTab from '$lib/features/database/detail/tabs/EntityImagesTab.svelte' import EntityImagesTab from '$lib/features/database/detail/tabs/EntityImagesTab.svelte'
import EntityRawDataTab from '$lib/features/database/detail/tabs/EntityRawDataTab.svelte' import EntityRawDataTab from '$lib/features/database/detail/tabs/EntityRawDataTab.svelte'
import DetailsContainer from '$lib/components/ui/DetailsContainer.svelte' import DetailsContainer from '$lib/components/ui/DetailsContainer.svelte'
import DetailItem from '$lib/components/ui/DetailItem.svelte'
import { getCharacterImage } from '$lib/utils/images' import { getCharacterImage } from '$lib/utils/images'
// Types // Types
@ -81,6 +81,9 @@
return getCharacterImage(character?.granblueId, 'grid', '01') return getCharacterImage(character?.granblueId, 'grid', '01')
} }
// Available image sizes for characters
const characterSizes = ['detail', 'grid', 'main', 'square']
// Generate image items for character (variants and poses based on uncap level) // Generate image items for character (variants and poses based on uncap level)
const characterImages = $derived.by((): ImageItem[] => { const characterImages = $derived.by((): ImageItem[] => {
if (!character?.granblueId) return [] if (!character?.granblueId) return []
@ -88,8 +91,7 @@
const variants = ['detail', 'grid', 'main', 'square'] as const const variants = ['detail', 'grid', 'main', 'square'] as const
const images: ImageItem[] = [] const images: ImageItem[] = []
// Determine available poses based on uncap level // Only include poses that are available - _01 = Base, _02 = MLB (3*), _03 = FLB (5*), _04 = Transcendence
// _01 = Base, _02 = MLB (3*), _03 = FLB (5*), _04 = Transcendence
const poses: { id: string; label: string }[] = [ const poses: { id: string; label: string }[] = [
{ id: '01', label: 'Base' }, { id: '01', label: 'Base' },
{ id: '02', label: 'MLB' } { id: '02', label: 'MLB' }
@ -103,19 +105,51 @@
poses.push({ id: '04', label: 'Transcendence' }) poses.push({ id: '04', label: 'Transcendence' })
} }
for (const variant of variants) { for (const pose of poses) {
for (const pose of poses) { for (const variant of variants) {
images.push({ images.push({
url: getCharacterImage(character.granblueId, variant, pose.id), url: getCharacterImage(character.granblueId, variant, pose.id),
label: `${variant} (${pose.label})`, label: `${variant} (${pose.label})`,
variant, variant,
pose: pose.id pose: pose.id,
poseLabel: pose.label
}) })
} }
} }
return images return images
}) })
// Image download handlers
async function handleDownloadImage(size: string, transformation: string | undefined, force: boolean) {
if (!character?.id) return
await entityAdapter.downloadCharacterImage(character.id, size, transformation, force)
}
async function handleDownloadAllPose(pose: string, force: boolean) {
if (!character?.id) return
// Download all sizes for this pose
for (const size of characterSizes) {
await entityAdapter.downloadCharacterImage(character.id, size, pose, force)
}
}
async function handleDownloadAllImages(force: boolean) {
if (!character?.id) return
await entityAdapter.downloadCharacterImages(character.id, { force })
}
async function handleDownloadSize(size: string) {
if (!character?.id) return
// Download this size for all available poses
const poses = ['01', '02']
if (character.uncap?.flb) poses.push('03')
if (character.uncap?.transcendence) poses.push('04')
for (const pose of poses) {
await entityAdapter.downloadCharacterImage(character.id, size, pose, false)
}
}
</script> </script>
<div class="page"> <div class="page">
@ -128,6 +162,9 @@
editUrl={canEdit ? editUrl : undefined} editUrl={canEdit ? editUrl : undefined}
{currentTab} {currentTab}
onTabChange={handleTabChange} onTabChange={handleTabChange}
onDownloadAllImages={canEdit ? handleDownloadAllImages : undefined}
onDownloadSize={canEdit ? handleDownloadSize : undefined}
availableSizes={characterSizes}
> >
{#if currentTab === 'info'} {#if currentTab === 'info'}
<section class="details"> <section class="details">
@ -136,12 +173,18 @@
<CharacterTaxonomySection {character} /> <CharacterTaxonomySection {character} />
<CharacterStatsSection {character} /> <CharacterStatsSection {character} />
{#if character?.id && character?.granblueId} {#if character.releaseDate || character.flbDate || character.ulbDate}
<CharacterImagesSection <DetailsContainer title="Dates">
characterId={character.id} {#if character.releaseDate}
granblueId={character.granblueId} <DetailItem label="Release Date" value={character.releaseDate} />
{canEdit} {/if}
/> {#if character.flbDate}
<DetailItem label="FLB Date" value={character.flbDate} />
{/if}
{#if character.ulbDate}
<DetailItem label="ULB Date" value={character.ulbDate} />
{/if}
</DetailsContainer>
{/if} {/if}
{#if relatedQuery.data?.length} {#if relatedQuery.data?.length}
@ -162,13 +205,26 @@
{/if} {/if}
</section> </section>
{:else if currentTab === 'images'} {:else if currentTab === 'images'}
<EntityImagesTab images={characterImages} /> <EntityImagesTab
images={characterImages}
{canEdit}
onDownloadImage={canEdit ? handleDownloadImage : undefined}
onDownloadAllPose={canEdit ? handleDownloadAllPose : undefined}
/>
{:else if currentTab === 'raw'} {:else if currentTab === 'raw'}
<EntityRawDataTab <EntityRawDataTab
wikiRaw={rawDataQuery.data?.wikiRaw} wikiRaw={rawDataQuery.data?.wikiRaw}
gameRawEn={rawDataQuery.data?.gameRawEn} gameRawEn={rawDataQuery.data?.gameRawEn}
gameRawJp={rawDataQuery.data?.gameRawJp} gameRawJp={rawDataQuery.data?.gameRawJp}
isLoading={rawDataQuery.isLoading} isLoading={rawDataQuery.isLoading}
{canEdit}
onFetchWiki={canEdit && character?.id
? async () => {
const result = await entityAdapter.fetchCharacterWiki(character.id)
rawDataQuery.refetch()
return result
}
: undefined}
/> />
{/if} {/if}
</DetailScaffold> </DetailScaffold>

View file

@ -19,6 +19,8 @@
import SummonStatsSection from '$lib/features/database/summons/sections/SummonStatsSection.svelte' import SummonStatsSection from '$lib/features/database/summons/sections/SummonStatsSection.svelte'
import EntityImagesTab from '$lib/features/database/detail/tabs/EntityImagesTab.svelte' import EntityImagesTab from '$lib/features/database/detail/tabs/EntityImagesTab.svelte'
import EntityRawDataTab from '$lib/features/database/detail/tabs/EntityRawDataTab.svelte' import EntityRawDataTab from '$lib/features/database/detail/tabs/EntityRawDataTab.svelte'
import DetailsContainer from '$lib/components/ui/DetailsContainer.svelte'
import DetailItem from '$lib/components/ui/DetailItem.svelte'
import { getSummonImage } from '$lib/utils/images' import { getSummonImage } from '$lib/utils/images'
// Types // Types
@ -69,23 +71,85 @@
return getSummonImage(summon?.granblueId, 'grid') return getSummonImage(summon?.granblueId, 'grid')
} }
// Available image sizes for summons
const summonSizes = ['detail', 'grid', 'main', 'square', 'wide']
// Generate image items for summon (detail, grid, main, square, wide variants) // Generate image items for summon (detail, grid, main, square, wide variants)
// Summons have transformations: Base (no suffix), ULB (_02), Transcendence Stage 1 (_03), Transcendence Stage 5 (_04)
const summonImages = $derived.by((): ImageItem[] => { const summonImages = $derived.by((): ImageItem[] => {
if (!summon?.granblueId) return [] if (!summon?.granblueId) return []
const variants = ['detail', 'grid', 'main', 'square', 'wide'] as const const variants = ['detail', 'grid', 'main', 'square', 'wide'] as const
const images: ImageItem[] = [] const images: ImageItem[] = []
for (const variant of variants) { // Only include transformations that are available
images.push({ const transformations: { id: string; label: string; suffix?: string }[] = [
url: getSummonImage(summon.granblueId, variant), { id: '01', label: 'Base', suffix: undefined }
label: variant, ]
variant
}) if (summon.uncap?.ulb) {
transformations.push({ id: '02', label: 'ULB', suffix: '02' })
}
if (summon.uncap?.transcendence) {
transformations.push(
{ id: '03', label: 'Transcendence (1)', suffix: '03' },
{ id: '04', label: 'Transcendence (5)', suffix: '04' }
)
}
for (const transformation of transformations) {
for (const variant of variants) {
images.push({
url: getSummonImage(summon.granblueId, variant, transformation.suffix),
label: `${variant} (${transformation.label})`,
variant,
pose: transformation.id,
poseLabel: transformation.label
})
}
} }
return images return images
}) })
// Image download handlers
async function handleDownloadImage(size: string, transformation: string | undefined, force: boolean) {
if (!summon?.id) return
// For summons, '01' means base (no transformation suffix)
const trans = transformation === '01' ? undefined : transformation
await entityAdapter.downloadSummonImage(summon.id, size, trans, force)
}
async function handleDownloadAllPose(pose: string, force: boolean) {
if (!summon?.id) return
const trans = pose === '01' ? undefined : pose
// Download all sizes for this pose
for (const size of summonSizes) {
await entityAdapter.downloadSummonImage(summon.id, size, trans, force)
}
}
async function handleDownloadAllImages(force: boolean) {
if (!summon?.id) return
await entityAdapter.downloadSummonImages(summon.id, { force })
}
async function handleDownloadSize(size: string) {
if (!summon?.id) return
// Download this size for all available transformations
const transformations: (string | undefined)[] = [undefined]
if (summon.uncap?.ulb) {
transformations.push('02')
}
if (summon.uncap?.transcendence) {
transformations.push('03', '04')
}
for (const trans of transformations) {
await entityAdapter.downloadSummonImage(summon.id, size, trans, false)
}
}
</script> </script>
<div class="page"> <div class="page">
@ -98,6 +162,9 @@
editUrl={canEdit ? editUrl : undefined} editUrl={canEdit ? editUrl : undefined}
{currentTab} {currentTab}
onTabChange={handleTabChange} onTabChange={handleTabChange}
onDownloadAllImages={canEdit ? handleDownloadAllImages : undefined}
onDownloadSize={canEdit ? handleDownloadSize : undefined}
availableSizes={summonSizes}
> >
{#if currentTab === 'info'} {#if currentTab === 'info'}
<section class="details"> <section class="details">
@ -147,15 +214,45 @@
</div> </div>
{/if} {/if}
</div> </div>
{#if summon.releaseDate || summon.flbDate || summon.ulbDate || summon.transcendenceDate}
<DetailsContainer title="Dates">
{#if summon.releaseDate}
<DetailItem label="Release Date" value={summon.releaseDate} />
{/if}
{#if summon.flbDate}
<DetailItem label="FLB Date" value={summon.flbDate} />
{/if}
{#if summon.ulbDate}
<DetailItem label="ULB Date" value={summon.ulbDate} />
{/if}
{#if summon.transcendenceDate}
<DetailItem label="Transcendence Date" value={summon.transcendenceDate} />
{/if}
</DetailsContainer>
{/if}
</section> </section>
{:else if currentTab === 'images'} {:else if currentTab === 'images'}
<EntityImagesTab images={summonImages} /> <EntityImagesTab
images={summonImages}
{canEdit}
onDownloadImage={canEdit ? handleDownloadImage : undefined}
onDownloadAllPose={canEdit ? handleDownloadAllPose : undefined}
/>
{:else if currentTab === 'raw'} {:else if currentTab === 'raw'}
<EntityRawDataTab <EntityRawDataTab
wikiRaw={rawDataQuery.data?.wikiRaw} wikiRaw={rawDataQuery.data?.wikiRaw}
gameRawEn={rawDataQuery.data?.gameRawEn} gameRawEn={rawDataQuery.data?.gameRawEn}
gameRawJp={rawDataQuery.data?.gameRawJp} gameRawJp={rawDataQuery.data?.gameRawJp}
isLoading={rawDataQuery.isLoading} isLoading={rawDataQuery.isLoading}
{canEdit}
onFetchWiki={canEdit && summon?.id
? async () => {
const result = await entityAdapter.fetchSummonWiki(summon.id)
rawDataQuery.refetch()
return result
}
: undefined}
/> />
{/if} {/if}
</DetailScaffold> </DetailScaffold>

View file

@ -19,6 +19,8 @@
import WeaponStatsSection from '$lib/features/database/weapons/sections/WeaponStatsSection.svelte' import WeaponStatsSection from '$lib/features/database/weapons/sections/WeaponStatsSection.svelte'
import EntityImagesTab from '$lib/features/database/detail/tabs/EntityImagesTab.svelte' import EntityImagesTab from '$lib/features/database/detail/tabs/EntityImagesTab.svelte'
import EntityRawDataTab from '$lib/features/database/detail/tabs/EntityRawDataTab.svelte' import EntityRawDataTab from '$lib/features/database/detail/tabs/EntityRawDataTab.svelte'
import DetailsContainer from '$lib/components/ui/DetailsContainer.svelte'
import DetailItem from '$lib/components/ui/DetailItem.svelte'
import { getWeaponGridImage, getWeaponImage as getWeaponImageUrl } from '$lib/utils/images' import { getWeaponGridImage, getWeaponImage as getWeaponImageUrl } from '$lib/utils/images'
// Types // Types
@ -69,23 +71,78 @@
return getWeaponGridImage(weapon?.granblueId, weapon?.element, weapon?.instanceElement) return getWeaponGridImage(weapon?.granblueId, weapon?.element, weapon?.instanceElement)
} }
// Available image sizes for weapons
const weaponSizes = ['base', 'grid', 'main', 'square']
// Generate image items for weapon (base, grid, main, square variants) // Generate image items for weapon (base, grid, main, square variants)
// Weapons have transformations: Base (no suffix), Transcendence Stage 1 (_02), Transcendence Stage 5 (_03)
const weaponImages = $derived.by((): ImageItem[] => { const weaponImages = $derived.by((): ImageItem[] => {
if (!weapon?.granblueId) return [] if (!weapon?.granblueId) return []
const variants = ['base', 'grid', 'main', 'square'] as const const variants = ['base', 'grid', 'main', 'square'] as const
const images: ImageItem[] = [] const images: ImageItem[] = []
for (const variant of variants) { // Only include transformations that are available
images.push({ const transformations: { id: string; label: string; suffix?: string }[] = [
url: getWeaponImageUrl(weapon.granblueId, variant), { id: '01', label: 'Base', suffix: undefined }
label: variant, ]
variant
}) if (weapon.uncap?.transcendence) {
transformations.push(
{ id: '02', label: 'Transcendence (1)', suffix: '02' },
{ id: '03', label: 'Transcendence (5)', suffix: '03' }
)
}
for (const transformation of transformations) {
for (const variant of variants) {
images.push({
url: getWeaponImageUrl(weapon.granblueId, variant, undefined, transformation.suffix),
label: `${variant} (${transformation.label})`,
variant,
pose: transformation.id,
poseLabel: transformation.label
})
}
} }
return images return images
}) })
// Image download handlers
async function handleDownloadImage(size: string, transformation: string | undefined, force: boolean) {
if (!weapon?.id) return
// For weapons, '01' means base (no transformation suffix)
const trans = transformation === '01' ? undefined : transformation
await entityAdapter.downloadWeaponImage(weapon.id, size, trans, force)
}
async function handleDownloadAllPose(pose: string, force: boolean) {
if (!weapon?.id) return
const trans = pose === '01' ? undefined : pose
// Download all sizes for this pose
for (const size of weaponSizes) {
await entityAdapter.downloadWeaponImage(weapon.id, size, trans, force)
}
}
async function handleDownloadAllImages(force: boolean) {
if (!weapon?.id) return
await entityAdapter.downloadWeaponImages(weapon.id, { force })
}
async function handleDownloadSize(size: string) {
if (!weapon?.id) return
// Download this size for all available transformations
const transformations: (string | undefined)[] = [undefined]
if (weapon.uncap?.transcendence) {
transformations.push('02', '03')
}
for (const trans of transformations) {
await entityAdapter.downloadWeaponImage(weapon.id, size, trans, false)
}
}
</script> </script>
<div class="page"> <div class="page">
@ -98,6 +155,9 @@
editUrl={canEdit ? editUrl : undefined} editUrl={canEdit ? editUrl : undefined}
{currentTab} {currentTab}
onTabChange={handleTabChange} onTabChange={handleTabChange}
onDownloadAllImages={canEdit ? handleDownloadAllImages : undefined}
onDownloadSize={canEdit ? handleDownloadSize : undefined}
availableSizes={weaponSizes}
> >
{#if currentTab === 'info'} {#if currentTab === 'info'}
<section class="details"> <section class="details">
@ -123,15 +183,45 @@
{/if} {/if}
</div> </div>
</div> </div>
{#if weapon.releaseDate || weapon.flbDate || weapon.ulbDate || weapon.transcendenceDate}
<DetailsContainer title="Dates">
{#if weapon.releaseDate}
<DetailItem label="Release Date" value={weapon.releaseDate} />
{/if}
{#if weapon.flbDate}
<DetailItem label="FLB Date" value={weapon.flbDate} />
{/if}
{#if weapon.ulbDate}
<DetailItem label="ULB Date" value={weapon.ulbDate} />
{/if}
{#if weapon.transcendenceDate}
<DetailItem label="Transcendence Date" value={weapon.transcendenceDate} />
{/if}
</DetailsContainer>
{/if}
</section> </section>
{:else if currentTab === 'images'} {:else if currentTab === 'images'}
<EntityImagesTab images={weaponImages} /> <EntityImagesTab
images={weaponImages}
{canEdit}
onDownloadImage={canEdit ? handleDownloadImage : undefined}
onDownloadAllPose={canEdit ? handleDownloadAllPose : undefined}
/>
{:else if currentTab === 'raw'} {:else if currentTab === 'raw'}
<EntityRawDataTab <EntityRawDataTab
wikiRaw={rawDataQuery.data?.wikiRaw} wikiRaw={rawDataQuery.data?.wikiRaw}
gameRawEn={rawDataQuery.data?.gameRawEn} gameRawEn={rawDataQuery.data?.gameRawEn}
gameRawJp={rawDataQuery.data?.gameRawJp} gameRawJp={rawDataQuery.data?.gameRawJp}
isLoading={rawDataQuery.isLoading} isLoading={rawDataQuery.isLoading}
{canEdit}
onFetchWiki={canEdit && weapon?.id
? async () => {
const result = await entityAdapter.fetchWeaponWiki(weapon.id)
rawDataQuery.refetch()
return result
}
: undefined}
/> />
{/if} {/if}
</DetailScaffold> </DetailScaffold>