fix loading
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -8,10 +8,9 @@
|
|||||||
"title": "ailogを作り直した",
|
"title": "ailogを作り直した",
|
||||||
"translations": {
|
"translations": {
|
||||||
"en": {
|
"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"
|
"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);
|
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
|
||||||
|
|||||||
@@ -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 || ''
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user