This commit is contained in:
2026-01-28 20:25:49 +09:00
parent 1ea96be29f
commit 724fb31e0a
11 changed files with 104 additions and 18 deletions

View File

@@ -3,11 +3,12 @@
"cid": "bafyreihgv33l3kdbpghum3ul5uugscfoaooyyx7nu3eaiuge4l6pru6je4",
"value": {
"item": [
{ "id": 0, "name": "i", "text": { "en": "world", "ja": "世界" } },
{ "id": 1, "name": "gordbox", "text": { "en": "The first color in this world", "ja": "この世界の最初の色" } },
{ "id": 2, "name": "silverbox", "text": { "en": "The second color in this world", "ja": "この世界の2番目の色" } },
{ "id": 3, "name": "bluebox", "text": { "en": "The third color in this world. It was once red, but was rewritten by someone", "ja": "この世界の3番目の色。かつては赤だったが、何者かに書き換えられた" } },
{ "id": 4, "name": "copperbox", "text": { "en": "The Third Lost Color of the World", "ja": "この世界の失われた3番目の色" } }
{ "id": 0, "name": "a", "text": { "en": "world", "ja": "世界" } },
{ "id": 1, "name": "i", "text": { "en": "The Pillar of Creation at the center of the galaxy stands tall in a place called the Eternal Stone, which is said to tolerate no change. The three attributes of gold, silver, and copper are engraved on it.", "ja": "銀河の中心にある創造の柱。そこにそびえ立つ永遠の石には金、銀、銅というこの世界の3つの属性が刻み込まれている" } },
{ "id": 2, "name": "gordbox", "text": { "en": "The first color in this world", "ja": "この世界の最初の色" } },
{ "id": 3, "name": "silverbox", "text": { "en": "The second color in this world", "ja": "この世界の2番目の色" } },
{ "id": 4, "name": "bluebox", "text": { "en": "The third color in this world. It was once red, but was rewritten by someone", "ja": "この世界の3番目の色。かつては赤だったが、何者かに書き換えられた" } },
{ "id": 5, "name": "copperbox", "text": { "en": "The Third Lost Color of the World", "ja": "この世界から失われた3番目の色" } }
],
"$type": "ai.syui.rse.admin",
"system": [

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

BIN
public/rse/item/5.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -156,13 +156,9 @@ export function renderCardPage(
}
}
// Sort by unique first, then rarity (desc), then by id
// Sort by id
const sortedGroups = Array.from(cardGroups.values())
.sort((a, b) => {
if (a.card.unique !== b.card.unique) return a.card.unique ? -1 : 1
if (b.card.rare !== a.card.rare) return b.card.rare - a.card.rare
return a.card.id - b.card.id
})
.sort((a, b) => a.card.id - b.card.id)
const cardsHtml = sortedGroups.map(({ card, count }) => {
return renderCard(card, '/card', count)

View File

@@ -1,6 +1,16 @@
// RSE display component for ai.syui.rse.user collection
import { renderCard, type UserCard, type CardAdminEntry, type CardAdminData } from './card'
export interface RseAdminItem {
id: number
name: string
text: { ja: string; en: string }
}
export interface RseAdminData {
item: RseAdminItem[]
}
export interface RseItem {
id: number
cp: number
@@ -15,6 +25,18 @@ export interface RseCollection {
updatedAt: string
}
// Get current language
function getLang(): string {
return localStorage.getItem('preferredLang') || 'ja'
}
// Get localized text
function getLocalizedText(obj: { ja: string; en: string } | undefined): string {
if (!obj) return ''
const lang = getLang()
return obj[lang as 'ja' | 'en'] || obj.ja || obj.en || ''
}
// Get rarity class from unique flag
function getRarityClass(item: RseItem): string {
if (item.unique) return 'unique'
@@ -99,13 +121,27 @@ function renderCharacterSection(
}
// Render single item
function renderRseItem(item: RseItem): string {
function renderRseItem(item: RseItem, rseAdminData: RseAdminData | null): string {
const rarityClass = getRarityClass(item)
const effectsHtml = rarityClass ? `
<div class="card-status pattern-${rarityClass}"></div>
<div class="card-status color-${rarityClass}"></div>
` : ''
// Get admin entry for this item
const adminEntry = rseAdminData?.item?.find(i => i.id === item.id)
const name = adminEntry?.name || ''
const text = adminEntry ? getLocalizedText(adminEntry.text) : ''
const infoHtml = (name || text) ? `
<div class="card-info">
<div class="card-info-header">
<span class="card-info-name">${name}</span>
</div>
${text ? `<div class="card-info-text">${text}</div>` : ''}
</div>
` : ''
return `
<div class="card-item">
<div class="card-wrapper">
@@ -117,6 +153,7 @@ function renderRseItem(item: RseItem): string {
<div class="card-detail">
<span class="card-cp">${item.cp}</span>
</div>
${infoHtml}
</div>
`
}
@@ -126,7 +163,8 @@ export function renderRsePage(
collection: RseCollection | null,
handle: string,
userCards: UserCard[] = [],
adminData: CardAdminData | null = null
adminData: CardAdminData | null = null,
rseAdminData: RseAdminData | null = null
): string {
const jsonUrl = `/@${handle}/at/collection/ai.syui.rse.user/self`
@@ -157,7 +195,7 @@ export function renderRsePage(
const charsHtml = sortedChars.map(c =>
renderCharacterSection(c, userCards, adminData)
).join('')
const itemsHtml = sortedItems.map(i => renderRseItem(i)).join('')
const itemsHtml = sortedItems.map(i => renderRseItem(i, rseAdminData)).join('')
return `
<div class="card-page">

View File

@@ -728,6 +728,56 @@ export async function getCardAdmin(did: string): Promise<CardAdminData | null> {
return null
}
// RSE admin data types
export interface RseAdminItem {
id: number
name: string
text: { ja: string; en: string }
}
export interface RseAdminData {
item: RseAdminItem[]
ability: unknown[]
character: unknown[]
system: unknown[]
collection: unknown[]
createdAt: string
updatedAt: string
}
// Get RSE admin data (ai.syui.rse.admin)
export async function getRseAdmin(did: string): Promise<RseAdminData | null> {
const collection = 'ai.syui.rse.admin'
// Try local first
try {
const res = await fetch(`/content/${did}/${collection}/self.json`)
if (res.ok && isJsonResponse(res)) {
const record = await res.json()
return record.value as RseAdminData
}
} catch {
// Try remote
}
// Remote fallback
const pds = await getPds(did)
if (!pds) return null
try {
const host = pds.replace('https://', '')
const url = `${xrpcUrl(host, comAtprotoRepo.getRecord)}?repo=${did}&collection=${collection}&rkey=self`
const res = await fetch(url)
if (res.ok) {
const record = await res.json()
return record.value as RseAdminData
}
} catch {
// Failed
}
return null
}
// Get user's links (ai.syui.at.link)
export async function getLinks(did: string): Promise<LinkCollection | null> {
const collection = 'ai.syui.at.link'

View File

@@ -1,7 +1,7 @@
import './styles/main.css'
import './styles/card.css'
import './styles/card-migrate.css'
import { getConfig, resolveHandle, getProfile, getPosts, getPost, describeRepo, listRecords, getRecord, getPds, getNetworks, getChatMessages, getCards, getCardAdmin, getRse, getLinks } from './lib/api'
import { getConfig, resolveHandle, getProfile, getPosts, getPost, describeRepo, listRecords, getRecord, getPds, getNetworks, getChatMessages, getCards, getCardAdmin, getRse, getRseAdmin, getLinks } from './lib/api'
import { parseRoute, onRouteChange, navigate, type Route } from './lib/router'
import { login, logout, handleCallback, restoreSession, isLoggedIn, getLoggedInHandle, getLoggedInDid, deleteRecord, updatePost, updateChat, updateLinks } from './lib/auth'
import { validateRecord } from './lib/lexicon'
@@ -256,13 +256,14 @@ async function render(route: Route): Promise<void> {
// RSE page with character cards
const cardCollection = config.cardCollection || 'ai.syui.card.user'
const adminDid = config.bot?.did || config.did || did
const [rseData, cards, adminData] = await Promise.all([
const [rseData, cards, adminData, rseAdminData] = await Promise.all([
getRse(did),
getCards(did, cardCollection),
getCardAdmin(adminDid)
getCardAdmin(adminDid),
getRseAdmin(adminDid)
])
const userCards = cards?.card || []
html += `<div id="content">${renderRsePage(rseData, handle, userCards, adminData)}</div>`
html += `<div id="content">${renderRsePage(rseData, handle, userCards, adminData, rseAdminData)}</div>`
html += `<nav class="back-nav"><a href="/@${handle}">${handle}</a></nav>`
} else if (route.type === 'link') {