fix loading

This commit is contained in:
2026-01-18 21:37:00 +09:00
parent 88d8b6bfa6
commit e6de42d7e4
6 changed files with 108 additions and 43 deletions

View File

@@ -1,5 +1,6 @@
{ {
"title": "syui.ai", "title": "syui.ai",
"did": "did:plc:vzsvtbtbnwn22xjqhcu3vd6y",
"handle": "syui.syui.ai", "handle": "syui.syui.ai",
"collection": "ai.syui.log.post", "collection": "ai.syui.log.post",
"network": "syu.is", "network": "syu.is",

View File

@@ -308,6 +308,27 @@ pub async fn sync_to_local(output: &str) -> Result<()> {
let profile_path = format!("{}/self.json", profile_dir); let profile_path = format!("{}/self.json", profile_dir);
fs::write(&profile_path, serde_json::to_string_pretty(&profile)?)?; fs::write(&profile_path, serde_json::to_string_pretty(&profile)?)?;
println!("Saved: {}", profile_path); println!("Saved: {}", profile_path);
// Download avatar blob if present
if let Some(avatar_cid) = profile["value"]["avatar"]["ref"]["$link"].as_str() {
let blob_dir = format!("{}/blob", did_dir);
fs::create_dir_all(&blob_dir)?;
let blob_path = format!("{}/{}", blob_dir, avatar_cid);
let blob_url = format!(
"{}/xrpc/com.atproto.sync.getBlob?did={}&cid={}",
pds, did, avatar_cid
);
println!("Downloading avatar: {}", avatar_cid);
let blob_res = client.get(&blob_url).send().await?;
if blob_res.status().is_success() {
let blob_bytes = blob_res.bytes().await?;
fs::write(&blob_path, &blob_bytes)?;
println!("Saved: {}", blob_path);
} else {
println!("Failed to download avatar: {}", blob_res.status());
}
}
} }
// 3. Sync collection records // 3. Sync collection records

View File

@@ -1,13 +1,17 @@
import type { Profile } from '../types' import type { Profile } from '../types'
import { getAvatarUrl } from '../lib/api' import { getAvatarUrl, getAvatarUrlRemote } from '../lib/api'
export async function renderProfile( export async function renderProfile(
did: string, did: string,
profile: Profile, profile: Profile,
handle: string, handle: string,
webUrl?: string webUrl?: string,
localOnly = false
): Promise<string> { ): Promise<string> {
const avatarUrl = await getAvatarUrl(did, profile) // Local mode: sync, no API call. Remote mode: async with API call
const avatarUrl = localOnly
? getAvatarUrl(did, profile, true)
: await getAvatarUrlRemote(did, profile)
const displayName = profile.value.displayName || handle || 'Unknown' const displayName = profile.value.displayName || handle || 'Unknown'
const description = profile.value.description || '' const description = profile.value.description || ''

View File

@@ -80,13 +80,16 @@ async function getLocalProfile(did: string): Promise<Profile | null> {
return null return null
} }
// Load profile (local first for admin, remote for others) // Load profile (local only for admin, remote for others)
export async function getProfile(did: string, localFirst = true): Promise<Profile | null> { export async function getProfile(did: string, localOnly = false): Promise<Profile | null> {
if (localFirst) { // Try local first
const local = await getLocalProfile(did) const local = await getLocalProfile(did)
if (local) return local if (local) return local
}
// If local only mode, don't call API
if (localOnly) return null
// Remote fallback
const pds = await getPds(did) const pds = await getPds(did)
if (!pds) return null if (!pds) return null
@@ -101,8 +104,23 @@ export async function getProfile(did: string, localFirst = true): Promise<Profil
return null return null
} }
// Get avatar URL // Get avatar URL (local only for admin, remote for others)
export async function getAvatarUrl(did: string, profile: Profile): Promise<string | null> { export function getAvatarUrl(did: string, profile: Profile, localOnly = false): string | null {
if (!profile.value.avatar) return null
const cid = profile.value.avatar.ref.$link
// Local mode: use local blob path (sync command downloads this)
if (localOnly) {
return `/content/${did}/blob/${cid}`
}
// Remote mode: use PDS blob URL (requires getPds call from caller if needed)
return null
}
// Get avatar URL with PDS lookup (async, for remote users)
export async function getAvatarUrlRemote(did: string, profile: Profile): Promise<string | null> {
if (!profile.value.avatar) return null if (!profile.value.avatar) return null
const pds = await getPds(did) const pds = await getPds(did)
@@ -132,13 +150,16 @@ async function getLocalPosts(did: string, collection: string): Promise<Post[]> {
return [] return []
} }
// Load posts (local first for admin, remote for others) // Load posts (local only for admin, remote for others)
export async function getPosts(did: string, collection: string, localFirst = true): Promise<Post[]> { export async function getPosts(did: string, collection: string, localOnly = false): Promise<Post[]> {
if (localFirst) { // Try local first
const local = await getLocalPosts(did, collection) const local = await getLocalPosts(did, collection)
if (local.length > 0) return local if (local.length > 0) return local
}
// If local only mode, don't call API
if (localOnly) return []
// Remote fallback
const pds = await getPds(did) const pds = await getPds(did)
if (!pds) return [] if (!pds) return []
@@ -158,17 +179,20 @@ export async function getPosts(did: string, collection: string, localFirst = tru
return [] return []
} }
// Get single post // Get single post (local only for admin, remote for others)
export async function getPost(did: string, collection: string, rkey: string, localFirst = true): Promise<Post | null> { export async function getPost(did: string, collection: string, rkey: string, localOnly = false): Promise<Post | null> {
if (localFirst) { // Try local first
try { try {
const res = await fetch(`/content/${did}/${collection}/${rkey}.json`) const res = await fetch(`/content/${did}/${collection}/${rkey}.json`)
if (res.ok && isJsonResponse(res)) return res.json() if (res.ok && isJsonResponse(res)) return res.json()
} catch { } catch {
// Not found // Not found
} }
}
// If local only mode, don't call API
if (localOnly) return null
// Remote fallback
const pds = await getPds(did) const pds = await getPds(did)
if (!pds) return null if (!pds) return null

View File

@@ -14,6 +14,7 @@ import { showLoading, hideLoading } from './components/loading'
const app = document.getElementById('app')! const app = document.getElementById('app')!
let currentHandle = '' let currentHandle = ''
let isFirstRender = true
// Filter collections by service domain // Filter collections by service domain
function filterCollectionsByService(collections: string[], service: string): string[] { function filterCollectionsByService(collections: string[], service: string): string[] {
@@ -52,7 +53,10 @@ async function getWebUrl(handle: string): Promise<string | undefined> {
} }
async function render(route: Route): Promise<void> { async function render(route: Route): Promise<void> {
// Skip loading indicator on first render for faster perceived performance
if (!isFirstRender) {
showLoading(app) showLoading(app)
}
try { try {
const config = await getConfig() const config = await getConfig()
@@ -73,12 +77,14 @@ async function render(route: Route): Promise<void> {
// Handle OAuth callback if present (check both ? and #) // Handle OAuth callback if present (check both ? and #)
const searchParams = new URLSearchParams(window.location.search) const searchParams = new URLSearchParams(window.location.search)
const hashParams = window.location.hash ? new URLSearchParams(window.location.hash.slice(1)) : null const hashParams = window.location.hash ? new URLSearchParams(window.location.hash.slice(1)) : null
if (searchParams.has('code') || searchParams.has('state') || hashParams?.has('code') || hashParams?.has('state')) { if (oauthEnabled && (searchParams.has('code') || searchParams.has('state') || hashParams?.has('code') || hashParams?.has('state'))) {
await handleCallback() await handleCallback()
} }
// Restore session from storage // Restore session from storage (skip if oauth disabled)
if (oauthEnabled) {
await restoreSession() await restoreSession()
}
// Redirect logged-in user from root to their user page // Redirect logged-in user from root to their user page
if (route.type === 'home' && isLoggedIn()) { if (route.type === 'home' && isLoggedIn()) {
@@ -89,25 +95,31 @@ async function render(route: Route): Promise<void> {
} }
} }
// Determine handle and whether to use local data // Determine handle and whether to use local data only (no API calls)
let handle: string let handle: string
let localFirst: boolean let localOnly: boolean
let did: string | null
if (route.type === 'home') { if (route.type === 'home') {
handle = config.handle handle = config.handle
localFirst = true localOnly = true
did = config.did || null
} else if (route.handle) { } else if (route.handle) {
handle = route.handle handle = route.handle
localFirst = handle === config.handle localOnly = handle === config.handle
did = localOnly ? (config.did || null) : null
} else { } else {
handle = config.handle handle = config.handle
localFirst = true localOnly = true
did = config.did || null
} }
currentHandle = handle currentHandle = handle
// Resolve handle to DID // Resolve handle to DID only for remote users
const did = await resolveHandle(handle) if (!did) {
did = await resolveHandle(handle)
}
if (!did) { if (!did) {
app.innerHTML = ` app.innerHTML = `
@@ -119,12 +131,12 @@ async function render(route: Route): Promise<void> {
return return
} }
// Load profile // Load profile (local only for admin, remote for others)
const profile = await getProfile(did, localFirst) const profile = await getProfile(did, localOnly)
const webUrl = await getWebUrl(handle) const webUrl = await getWebUrl(handle)
// Load posts to check for translations // Load posts (local only for admin, remote for others)
const posts = await getPosts(did, config.collection, localFirst) const posts = await getPosts(did, config.collection, localOnly)
// Collect available languages from posts // Collect available languages from posts
const availableLangs = new Set<string>() const availableLangs = new Set<string>()
@@ -151,7 +163,7 @@ async function render(route: Route): Promise<void> {
// Profile section // Profile section
if (profile) { if (profile) {
html += await renderProfile(did, profile, handle, webUrl) html += await renderProfile(did, profile, handle, webUrl, localOnly)
} }
// Check if logged-in user owns this content // Check if logged-in user owns this content
@@ -197,7 +209,7 @@ async function render(route: Route): Promise<void> {
} else if (route.type === 'post' && route.rkey) { } else if (route.type === 'post' && route.rkey) {
// Post detail (config.collection with markdown) // Post detail (config.collection with markdown)
const post = await getPost(did, config.collection, route.rkey, localFirst) const post = await getPost(did, config.collection, route.rkey, localOnly)
html += renderLangSelector(langList) html += renderLangSelector(langList)
if (post) { if (post) {
html += `<div id="content">${renderPostDetail(post, handle, config.collection, isOwner, config.siteUrl, webUrl)}</div>` html += `<div id="content">${renderPostDetail(post, handle, config.collection, isOwner, config.siteUrl, webUrl)}</div>`
@@ -273,6 +285,8 @@ async function render(route: Route): Promise<void> {
` `
hideLoading(app) hideLoading(app)
setupEventHandlers() setupEventHandlers()
} finally {
isFirstRender = false
} }
} }

View File

@@ -1,6 +1,7 @@
// Config types // Config types
export interface AppConfig { export interface AppConfig {
title: string title: string
did?: string
handle: string handle: string
collection: string collection: string
network: string network: string