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",
"did": "did:plc:vzsvtbtbnwn22xjqhcu3vd6y",
"handle": "syui.syui.ai",
"collection": "ai.syui.log.post",
"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);
fs::write(&profile_path, serde_json::to_string_pretty(&profile)?)?;
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

View File

@@ -1,13 +1,17 @@
import type { Profile } from '../types'
import { getAvatarUrl } from '../lib/api'
import { getAvatarUrl, getAvatarUrlRemote } from '../lib/api'
export async function renderProfile(
did: string,
profile: Profile,
handle: string,
webUrl?: string
webUrl?: string,
localOnly = false
): 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 description = profile.value.description || ''

View File

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

View File

@@ -14,6 +14,7 @@ import { showLoading, hideLoading } from './components/loading'
const app = document.getElementById('app')!
let currentHandle = ''
let isFirstRender = true
// Filter collections by service domain
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> {
// Skip loading indicator on first render for faster perceived performance
if (!isFirstRender) {
showLoading(app)
}
try {
const config = await getConfig()
@@ -73,12 +77,14 @@ async function render(route: Route): Promise<void> {
// Handle OAuth callback if present (check both ? and #)
const searchParams = new URLSearchParams(window.location.search)
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()
}
// Restore session from storage
// Restore session from storage (skip if oauth disabled)
if (oauthEnabled) {
await restoreSession()
}
// Redirect logged-in user from root to their user page
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 localFirst: boolean
let localOnly: boolean
let did: string | null
if (route.type === 'home') {
handle = config.handle
localFirst = true
localOnly = true
did = config.did || null
} else if (route.handle) {
handle = route.handle
localFirst = handle === config.handle
localOnly = handle === config.handle
did = localOnly ? (config.did || null) : null
} else {
handle = config.handle
localFirst = true
localOnly = true
did = config.did || null
}
currentHandle = handle
// Resolve handle to DID
const did = await resolveHandle(handle)
// Resolve handle to DID only for remote users
if (!did) {
did = await resolveHandle(handle)
}
if (!did) {
app.innerHTML = `
@@ -119,12 +131,12 @@ async function render(route: Route): Promise<void> {
return
}
// Load profile
const profile = await getProfile(did, localFirst)
// Load profile (local only for admin, remote for others)
const profile = await getProfile(did, localOnly)
const webUrl = await getWebUrl(handle)
// Load posts to check for translations
const posts = await getPosts(did, config.collection, localFirst)
// Load posts (local only for admin, remote for others)
const posts = await getPosts(did, config.collection, localOnly)
// Collect available languages from posts
const availableLangs = new Set<string>()
@@ -151,7 +163,7 @@ async function render(route: Route): Promise<void> {
// Profile section
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
@@ -197,7 +209,7 @@ async function render(route: Route): Promise<void> {
} else if (route.type === 'post' && route.rkey) {
// 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)
if (post) {
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)
setupEventHandlers()
} finally {
isFirstRender = false
}
}

View File

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