fix loading
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"title": "syui.ai",
|
||||
"did": "did:plc:vzsvtbtbnwn22xjqhcu3vd6y",
|
||||
"handle": "syui.syui.ai",
|
||||
"collection": "ai.syui.log.post",
|
||||
"network": "syu.is",
|
||||
|
||||
@@ -8,10 +8,9 @@
|
||||
"title": "ailogを作り直した",
|
||||
"translations": {
|
||||
"en": {
|
||||
"content": "## About ailog\n\nA site generator that integrates with atproto.\n\n## How to Use ailog\n\n```sh\n$ git clone https://git.syui.ai/ai/log\n$ cd log\n$ cat public/config.json\n{\n \"title\": \"syui.ai\",\n \"handle\": \"syui.syui.ai\",\n \"collection\": \"ai.syui.log.post\",\n \"network\": \"syu.is\",\n \"color\": \"#0066cc\",\n \"siteUrl\": \"https://syui.ai\"\n}\n---\n$ npm run dev\n```\n\n## ailog's Concept\n\n1. Based on at-browser architecture\n2. Uses atproto oAuth for login\n3. Allows posting articles through the logged-in account",
|
||||
"content": "## What is ailog?\n\nA site generator that integrates with atproto.\n\n## How to use ailog\n\n```sh\n$ git clone https://git.syui.ai/ai/log\n$ cd log\n$ cat public/config.json\n{\n \"title\": \"syui.ai\",\n \"handle\": \"syui.syui.ai\",\n \"collection\": \"ai.syui.log.post\",\n \"network\": \"syu.is\",\n \"color\": \"#0066cc\",\n \"siteUrl\": \"https://syui.ai\"\n}\n---\n$ npm run dev\n```\n\n## ailog's concept\n\n1. Based on at-browser as its foundation\n2. Logs in via atproto oauth\n3. Allows users to post articles using their logged-in account",
|
||||
"title": "recreated ailog"
|
||||
}
|
||||
},
|
||||
"lang": "ja"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +1,3 @@
|
||||
["3mchqlshygs2s"]
|
||||
[
|
||||
"3mchqlshygs2s"
|
||||
]
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 162 KiB |
@@ -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
|
||||
|
||||
@@ -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 || ''
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Config types
|
||||
export interface AppConfig {
|
||||
title: string
|
||||
did?: string
|
||||
handle: string
|
||||
collection: string
|
||||
network: string
|
||||
|
||||
Reference in New Issue
Block a user