add ai.syui.rse
This commit is contained in:
BIN
public/rse/character/0.webp
Normal file
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
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
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
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
BIN
public/rse/item/0.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
@@ -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
127
src/web/components/rse.ts
Normal 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>
|
||||||
|
`
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user