diff --git a/public/rse/character/0.webp b/public/rse/character/0.webp new file mode 100644 index 0000000..d6780d1 Binary files /dev/null and b/public/rse/character/0.webp differ diff --git a/public/rse/character/1.webp b/public/rse/character/1.webp new file mode 100644 index 0000000..5e3779d Binary files /dev/null and b/public/rse/character/1.webp differ diff --git a/public/rse/character/2.webp b/public/rse/character/2.webp new file mode 100644 index 0000000..ae74088 Binary files /dev/null and b/public/rse/character/2.webp differ diff --git a/public/rse/character/3.webp b/public/rse/character/3.webp new file mode 100644 index 0000000..6ba4d5f Binary files /dev/null and b/public/rse/character/3.webp differ diff --git a/public/rse/item/0.webp b/public/rse/item/0.webp new file mode 100644 index 0000000..749a39d Binary files /dev/null and b/public/rse/item/0.webp differ diff --git a/src/web/components/profile.ts b/src/web/components/profile.ts index a868d83..4bc52b0 100644 --- a/src/web/components/profile.ts +++ b/src/web/components/profile.ts @@ -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 } diff --git a/src/web/components/rse.ts b/src/web/components/rse.ts new file mode 100644 index 0000000..c9f87e2 --- /dev/null +++ b/src/web/components/rse.ts @@ -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 ? ` +
+
+ ` : '' + + return ` +
+
+
+ ${type} ${item.id} +
+ ${effectsHtml} +
+
+ ${item.cp} +
+
+ ` +} + +// 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 ` +
+
+

RSE

+ json +
+

No RSE data found for @${handle}

+
+ ` + } + + 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 ` +
+
+
+
+ ${totalChars} + Characters +
+
+ ${totalItems} + Items +
+
+ ${uniqueChars} + Unique +
+
+ ${shinyChars} + Shiny +
+
+
+
+ json +
+ ${charsHtml ? ` +

Characters

+
${charsHtml}
+ ` : ''} + ${itemsHtml ? ` +

Items

+
${itemsHtml}
+ ` : ''} +
+ ` +} diff --git a/src/web/lib/api.ts b/src/web/lib/api.ts index e22f59e..d851031 100644 --- a/src/web/lib/api.ts +++ b/src/web/lib/api.ts @@ -603,3 +603,52 @@ export async function getCards( } 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 { + 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 +} diff --git a/src/web/lib/router.ts b/src/web/lib/router.ts index 6891bfe..c2d8fbe 100644 --- a/src/web/lib/router.ts +++ b/src/web/lib/router.ts @@ -1,5 +1,5 @@ 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 rkey?: string service?: string @@ -63,6 +63,12 @@ export function parseRoute(): Route { 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} const chatThreadMatch = path.match(/^\/@([^/]+)\/at\/chat\/([^/]+)$/) if (chatThreadMatch) { diff --git a/src/web/main.ts b/src/web/main.ts index a3916cf..5c22e66 100644 --- a/src/web/main.ts +++ b/src/web/main.ts @@ -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, 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 { login, logout, handleCallback, restoreSession, isLoggedIn, getLoggedInHandle, getLoggedInDid, deleteRecord, updatePost } from './lib/auth' import { validateRecord } from './lib/lexicon' @@ -14,6 +14,7 @@ import { renderModeTabs, renderLangSelector, setupModeTabs } from './components/ import { renderFooter } from './components/footer' import { renderChatListPage, renderChatThreadPage } from './components/chat' import { renderCardPage } from './components/card' +import { renderRsePage } from './components/rse' import { checkMigrationStatus, renderMigrationPage, setupMigrationButton } from './components/card-migrate' import { showLoading, hideLoading } from './components/loading' @@ -254,6 +255,12 @@ async function render(route: Route): Promise { html += `
${renderMigrationPage(cardMigrationState, handle, isOwner)}
` html += `` + } else if (route.type === 'rse') { + // RSE page + const rseData = await getRse(did) + html += `
${renderRsePage(rseData, handle)}
` + html += `` + } else if (route.type === 'chat') { // Chat list page - show threads started by this user if (!config.bot) {