add ai.syui.rse

This commit is contained in:
2026-01-21 12:56:43 +09:00
parent a44dbc635d
commit ae209e28d4
10 changed files with 201 additions and 2 deletions

BIN
public/rse/character/0.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

BIN
public/rse/character/1.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

BIN
public/rse/character/2.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

BIN
public/rse/character/3.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -38,6 +38,16 @@ export function getServiceLinks(handle: string, collections: string[], migration
}) })
} }
// RSE
if (collections.includes('ai.syui.rse.user')) {
services.push({
name: 'RSE',
icon: '/service/ai.syui.rse.png',
url: `/@${handle}/at/rse`,
collection: 'ai.syui.rse.user'
})
}
return services return services
} }

127
src/web/components/rse.ts Normal file
View File

@@ -0,0 +1,127 @@
// RSE display component for ai.syui.rse.user collection
export interface RseItem {
id: number
cp: number
mode: number
shiny: boolean
unique: boolean
}
export interface RseCollection {
item: RseItem[]
character: RseItem[]
createdAt: string
updatedAt: string
}
// Get rarity class from shiny/unique flags
function getRarityClass(item: RseItem): string {
if (item.unique) return 'unique'
if (item.shiny) return 'shiny'
return ''
}
// Render single item/character
function renderRseItem(item: RseItem, type: 'item' | 'character'): string {
const rarityClass = getRarityClass(item)
const effectsHtml = rarityClass ? `
<div class="card-status pattern-${rarityClass}"></div>
<div class="card-status color-${rarityClass}"></div>
` : ''
return `
<div class="card-item">
<div class="card-wrapper">
<div class="card-reflection">
<img src="/rse/${type}/${item.id}.webp" alt="${type} ${item.id}" loading="lazy" />
</div>
${effectsHtml}
</div>
<div class="card-detail">
<span class="card-cp">${item.cp}</span>
</div>
</div>
`
}
// Render RSE page
export function renderRsePage(
collection: RseCollection | null,
handle: string
): string {
const jsonUrl = `/@${handle}/at/collection/ai.syui.rse.user/self`
if (!collection) {
return `
<div class="card-page">
<div class="card-header">
<h2>RSE</h2>
<a href="${jsonUrl}" class="json-btn">json</a>
</div>
<p class="no-cards">No RSE data found for @${handle}</p>
</div>
`
}
const characters = collection.character || []
const items = collection.item || []
// Stats
const totalChars = characters.length
const totalItems = items.length
const uniqueChars = characters.filter(c => c.unique).length
const shinyChars = characters.filter(c => c.shiny).length
// Sort by unique > shiny > id
const sortedChars = [...characters].sort((a, b) => {
if (a.unique !== b.unique) return a.unique ? -1 : 1
if (a.shiny !== b.shiny) return a.shiny ? -1 : 1
return a.id - b.id
})
const sortedItems = [...items].sort((a, b) => {
if (a.unique !== b.unique) return a.unique ? -1 : 1
if (a.shiny !== b.shiny) return a.shiny ? -1 : 1
return a.id - b.id
})
const charsHtml = sortedChars.map(c => renderRseItem(c, 'character')).join('')
const itemsHtml = sortedItems.map(i => renderRseItem(i, 'item')).join('')
return `
<div class="card-page">
<div class="card-header">
<div class="card-stats">
<div class="stat">
<span class="stat-value">${totalChars}</span>
<span class="stat-label">Characters</span>
</div>
<div class="stat">
<span class="stat-value">${totalItems}</span>
<span class="stat-label">Items</span>
</div>
<div class="stat rare-unique">
<span class="stat-value">${uniqueChars}</span>
<span class="stat-label">Unique</span>
</div>
<div class="stat rare-shiny">
<span class="stat-value">${shinyChars}</span>
<span class="stat-label">Shiny</span>
</div>
</div>
</div>
<div class="card-actions">
<a href="${jsonUrl}" class="json-btn">json</a>
</div>
${charsHtml ? `
<h3 class="rse-section-title">Characters</h3>
<div class="card-grid">${charsHtml}</div>
` : ''}
${itemsHtml ? `
<h3 class="rse-section-title">Items</h3>
<div class="card-grid">${itemsHtml}</div>
` : ''}
</div>
`
}

View File

@@ -603,3 +603,52 @@ export async function getCards(
} }
return null return null
} }
// RSE collection type
export interface RseItem {
id: number
cp: number
mode: number
shiny: boolean
unique: boolean
}
export interface RseCollection {
item: RseItem[]
character: RseItem[]
createdAt: string
updatedAt: string
}
// Get user's RSE collection (ai.syui.rse.user)
export async function getRse(did: string): Promise<RseCollection | null> {
const collection = 'ai.syui.rse.user'
// 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 RseCollection
}
} 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 RseCollection
}
} catch {
// Failed
}
return null
}

View File

@@ -1,5 +1,5 @@
export interface Route { export interface Route {
type: 'home' | 'user' | 'post' | 'postpage' | 'atbrowser' | 'service' | 'collection' | 'record' | 'chat' | 'chat-thread' | 'card' | 'card-old' type: 'home' | 'user' | 'post' | 'postpage' | 'atbrowser' | 'service' | 'collection' | 'record' | 'chat' | 'chat-thread' | 'card' | 'card-old' | 'rse'
handle?: string handle?: string
rkey?: string rkey?: string
service?: string service?: string
@@ -63,6 +63,12 @@ export function parseRoute(): Route {
return { type: 'card-old', handle: cardOldMatch[1] } return { type: 'card-old', handle: cardOldMatch[1] }
} }
// RSE page: /@handle/at/rse
const rseMatch = path.match(/^\/@([^/]+)\/at\/rse\/?$/)
if (rseMatch) {
return { type: 'rse', handle: rseMatch[1] }
}
// Chat thread: /@handle/at/chat/{rkey} // Chat thread: /@handle/at/chat/{rkey}
const chatThreadMatch = path.match(/^\/@([^/]+)\/at\/chat\/([^/]+)$/) const chatThreadMatch = path.match(/^\/@([^/]+)\/at\/chat\/([^/]+)$/)
if (chatThreadMatch) { if (chatThreadMatch) {

View File

@@ -1,7 +1,7 @@
import './styles/main.css' import './styles/main.css'
import './styles/card.css' import './styles/card.css'
import './styles/card-migrate.css' import './styles/card-migrate.css'
import { getConfig, resolveHandle, getProfile, getPosts, getPost, describeRepo, listRecords, getRecord, getPds, getNetworks, getChatMessages, getCards, getOldApiUserByDid, hasCardOldRecord } from './lib/api' import { getConfig, resolveHandle, getProfile, getPosts, getPost, describeRepo, listRecords, getRecord, getPds, getNetworks, getChatMessages, getCards, getRse, getOldApiUserByDid, hasCardOldRecord } from './lib/api'
import { parseRoute, onRouteChange, navigate, type Route } from './lib/router' import { parseRoute, onRouteChange, navigate, type Route } from './lib/router'
import { login, logout, handleCallback, restoreSession, isLoggedIn, getLoggedInHandle, getLoggedInDid, deleteRecord, updatePost } from './lib/auth' import { login, logout, handleCallback, restoreSession, isLoggedIn, getLoggedInHandle, getLoggedInDid, deleteRecord, updatePost } from './lib/auth'
import { validateRecord } from './lib/lexicon' import { validateRecord } from './lib/lexicon'
@@ -14,6 +14,7 @@ import { renderModeTabs, renderLangSelector, setupModeTabs } from './components/
import { renderFooter } from './components/footer' import { renderFooter } from './components/footer'
import { renderChatListPage, renderChatThreadPage } from './components/chat' import { renderChatListPage, renderChatThreadPage } from './components/chat'
import { renderCardPage } from './components/card' import { renderCardPage } from './components/card'
import { renderRsePage } from './components/rse'
import { checkMigrationStatus, renderMigrationPage, setupMigrationButton } from './components/card-migrate' import { checkMigrationStatus, renderMigrationPage, setupMigrationButton } from './components/card-migrate'
import { showLoading, hideLoading } from './components/loading' import { showLoading, hideLoading } from './components/loading'
@@ -254,6 +255,12 @@ async function render(route: Route): Promise<void> {
html += `<div id="content">${renderMigrationPage(cardMigrationState, handle, isOwner)}</div>` html += `<div id="content">${renderMigrationPage(cardMigrationState, handle, isOwner)}</div>`
html += `<nav class="back-nav"><a href="/@${handle}">${handle}</a></nav>` html += `<nav class="back-nav"><a href="/@${handle}">${handle}</a></nav>`
} else if (route.type === 'rse') {
// RSE page
const rseData = await getRse(did)
html += `<div id="content">${renderRsePage(rseData, handle)}</div>`
html += `<nav class="back-nav"><a href="/@${handle}">${handle}</a></nav>`
} else if (route.type === 'chat') { } else if (route.type === 'chat') {
// Chat list page - show threads started by this user // Chat list page - show threads started by this user
if (!config.bot) { if (!config.bot) {