diff --git a/.gitignore b/.gitignore index 9573965d..3f2da25d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,9 +2,11 @@ public build resources *.lock +.claude .DS_Store node_modules +package-lock.json # local env files .env.local @@ -37,3 +39,4 @@ content/*/*_private.pem content/*/*.json static/*lock* +app/dist diff --git a/app/package.json b/app/package.json new file mode 100644 index 00000000..44e4a092 --- /dev/null +++ b/app/package.json @@ -0,0 +1,28 @@ +{ + "name": "app-pages", + "version": "1.0.0", + "type": "module", + "scripts": { + "build": "tsx src/build.ts", + "watch": "tsx watch src/build.ts", + "serve": "npx serve dist", + "serve:aiat": "npx serve dist/aiat -p 3000", + "serve:aicard": "npx serve dist/aicard -p 3001", + "serve:airse": "npx serve dist/airse -p 3002", + "dev": "npm run build && npx concurrently \"npm run watch\" \"npm run serve:aiat\"", + "dev:aiat": "npm run build && npx concurrently \"npm run watch\" \"npm run serve:aiat\"", + "dev:aicard": "npm run build && npx concurrently \"npm run watch\" \"npm run serve:aicard\"", + "dev:airse": "npm run build && npx concurrently \"npm run watch\" \"npm run serve:airse\"" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "concurrently": "^9.0.0", + "serve": "^14.0.0", + "tsx": "^4.0.0", + "typescript": "^5.0.0" + }, + "dependencies": { + "marked": "^15.0.0", + "gray-matter": "^4.0.3" + } +} diff --git a/app/src/assets/aiat/app.png b/app/src/assets/aiat/app.png new file mode 100644 index 00000000..f8f45fb6 Binary files /dev/null and b/app/src/assets/aiat/app.png differ diff --git a/app/src/assets/aiat/icon.png b/app/src/assets/aiat/icon.png new file mode 100644 index 00000000..f8f45fb6 Binary files /dev/null and b/app/src/assets/aiat/icon.png differ diff --git a/app/src/assets/aicard/app.png b/app/src/assets/aicard/app.png new file mode 100644 index 00000000..69a6840c Binary files /dev/null and b/app/src/assets/aicard/app.png differ diff --git a/app/src/assets/aicard/icon.png b/app/src/assets/aicard/icon.png new file mode 100644 index 00000000..69a6840c Binary files /dev/null and b/app/src/assets/aicard/icon.png differ diff --git a/app/src/assets/airse/app.png b/app/src/assets/airse/app.png new file mode 100644 index 00000000..5a749265 Binary files /dev/null and b/app/src/assets/airse/app.png differ diff --git a/app/src/assets/airse/icon.png b/app/src/assets/airse/icon.png new file mode 100644 index 00000000..5a749265 Binary files /dev/null and b/app/src/assets/airse/icon.png differ diff --git a/app/src/assets/common/favicon.png b/app/src/assets/common/favicon.png new file mode 100644 index 00000000..2227ba3f Binary files /dev/null and b/app/src/assets/common/favicon.png differ diff --git a/app/src/build.ts b/app/src/build.ts new file mode 100644 index 00000000..83e273f7 --- /dev/null +++ b/app/src/build.ts @@ -0,0 +1,124 @@ +import { readFileSync, writeFileSync, mkdirSync, readdirSync, copyFileSync, existsSync } from 'fs' +import { join, dirname } from 'path' +import { fileURLToPath } from 'url' +import { marked } from 'marked' +import matter from 'gray-matter' +import type { SiteConfig, PageMeta } from './types.js' +import { appLayout, legalLayout, oauthCallback, clientMetadata } from './templates/index.js' + +const __dirname = dirname(fileURLToPath(import.meta.url)) +const sitesDir = join(__dirname, 'sites') +const contentDir = join(__dirname, 'content/pages') +const assetsDir = join(__dirname, 'assets') +const distDir = join(__dirname, '..', 'dist') + +// Load all site configs +function loadSites(): SiteConfig[] { + const files = readdirSync(sitesDir).filter(f => f.endsWith('.json')) + return files.map(f => { + const content = readFileSync(join(sitesDir, f), 'utf-8') + return JSON.parse(content) as SiteConfig + }) +} + +// Load all markdown pages +function loadPages(): Array<{ meta: PageMeta & { path: string }, content: string }> { + const files = readdirSync(contentDir).filter(f => f.endsWith('.md')) + return files.map(f => { + const raw = readFileSync(join(contentDir, f), 'utf-8') + const { data, content } = matter(raw) + return { + meta: data as PageMeta & { path: string }, + content: marked(content) as string + } + }) +} + +// Ensure directory exists +function ensureDir(filePath: string): void { + mkdirSync(dirname(filePath), { recursive: true }) +} + +// Copy assets for a site +function copyAssets(site: SiteConfig): void { + const siteAssetsDir = join(assetsDir, site.id) + const commonAssetsDir = join(assetsDir, 'common') + const outDir = join(distDir, site.id, 'static') + + ensureDir(join(outDir, '_')) + + // Copy common assets first + if (existsSync(commonAssetsDir)) { + const files = readdirSync(commonAssetsDir) + for (const file of files) { + copyFileSync(join(commonAssetsDir, file), join(outDir, file)) + console.log(` -> static/${file} (common)`) + } + } + + // Copy site-specific assets (overrides common) + if (existsSync(siteAssetsDir)) { + const files = readdirSync(siteAssetsDir) + for (const file of files) { + copyFileSync(join(siteAssetsDir, file), join(outDir, file)) + console.log(` -> static/${file}`) + } + } +} + +// Build site +function buildSite(site: SiteConfig, pages: ReturnType): void { + const siteDir = join(distDir, site.id) + + console.log(`Building ${site.name} (${site.domain})...`) + + // Build pages from markdown + for (const page of pages) { + const outPath = join(siteDir, page.meta.path) + ensureDir(outPath) + + let html: string + if (page.meta.template === 'app') { + html = appLayout(site, page.content) + } else { + html = legalLayout(site, page.meta.title, page.content) + } + + writeFileSync(outPath, html) + console.log(` -> ${page.meta.path}`) + } + + // Build OAuth callback + const callbackPath = join(siteDir, 'oauth/callback/index.html') + ensureDir(callbackPath) + writeFileSync(callbackPath, oauthCallback(site)) + console.log(` -> oauth/callback/index.html`) + + // Build client metadata + const metadataPath = join(siteDir, '.well-known/client-metadata.json') + ensureDir(metadataPath) + writeFileSync(metadataPath, clientMetadata(site)) + console.log(` -> .well-known/client-metadata.json`) + + // Copy assets + copyAssets(site) +} + +// Main +function main(): void { + console.log('App Pages Builder\n') + + const sites = loadSites() + const pages = loadPages() + + console.log(`Found ${sites.length} sites, ${pages.length} pages\n`) + + for (const site of sites) { + buildSite(site, pages) + console.log('') + } + + console.log('Done!') +} + +main() diff --git a/app/src/content/pages/app.md b/app/src/content/pages/app.md new file mode 100644 index 00000000..c475ff4d --- /dev/null +++ b/app/src/content/pages/app.md @@ -0,0 +1,5 @@ +--- +title: App Info +template: app +path: about/support/app.html +--- diff --git a/app/src/content/pages/help.md b/app/src/content/pages/help.md new file mode 100644 index 00000000..c172328f --- /dev/null +++ b/app/src/content/pages/help.md @@ -0,0 +1,39 @@ +--- +title: Help +template: legal +path: about/support/help.html +--- + +## Getting Started + +1. Download the app from the App Store +2. Sign in with your AT Protocol handle +3. Start connecting with your community + +## Authentication + +This app uses AT Protocol OAuth for secure authentication. Your credentials are never stored on our servers. + +## Features + +- **Feed**: View posts from people you follow +- **Profile**: Customize your profile and view your posts +- **Search**: Find users and content + +## Troubleshooting + +### Login Issues + +- Ensure your handle is correct +- Check your internet connection +- Try logging out and back in + +### App Crashes + +- Update to the latest version +- Restart the app +- If issues persist, contact support + +## Contact Support + +For additional help, please visit our Git repository or contact us through AT Protocol. diff --git a/app/src/content/pages/index.md b/app/src/content/pages/index.md new file mode 100644 index 00000000..75d5f341 --- /dev/null +++ b/app/src/content/pages/index.md @@ -0,0 +1,5 @@ +--- +title: App Info +template: app +path: index.html +--- diff --git a/app/src/content/pages/license.md b/app/src/content/pages/license.md new file mode 100644 index 00000000..ca7954a4 --- /dev/null +++ b/app/src/content/pages/license.md @@ -0,0 +1,29 @@ +--- +title: License +template: legal +path: about/support/license.html +--- + +## Software License + +This application is open source software. + +## Third-Party Libraries + +This app uses the following open source libraries: + +- **AT Protocol** - MIT License +- **React Native** - MIT License +- **Expo** - MIT License + +## Assets + +App icons and graphics are proprietary unless otherwise noted. + +## Attribution + +Built with AT Protocol by syui. + +## Contact + +For licensing questions, please contact us through our Git repository. diff --git a/app/src/content/pages/privacy.md b/app/src/content/pages/privacy.md new file mode 100644 index 00000000..f7bd51e7 --- /dev/null +++ b/app/src/content/pages/privacy.md @@ -0,0 +1,35 @@ +--- +title: Privacy Policy +template: legal +path: about/support/privacy.html +--- + +## Information We Collect + +This application collects minimal information necessary for its operation: + +- **Account Information**: Your AT Protocol handle and DID for authentication +- **Usage Data**: Basic app usage statistics to improve the service + +## How We Use Your Information + +- To provide and maintain the service +- To authenticate your identity +- To improve user experience + +## Data Storage + +Your data is stored securely on AT Protocol compatible servers. We do not sell or share your personal information with third parties. + +## Your Rights + +You have the right to: +- Access your personal data +- Delete your account and associated data +- Export your data + +## Contact + +If you have questions about this privacy policy, please contact us through our Git repository. + +*Last updated: 2025-01-15* diff --git a/app/src/content/pages/tos.md b/app/src/content/pages/tos.md new file mode 100644 index 00000000..eb1a2607 --- /dev/null +++ b/app/src/content/pages/tos.md @@ -0,0 +1,40 @@ +--- +title: Terms of Service +template: legal +path: about/support/tos.html +--- + +## Acceptance of Terms + +By using this application, you agree to these Terms of Service. + +## Use of Service + +You agree to use this service in accordance with: +- Applicable laws and regulations +- AT Protocol community guidelines +- Respectful behavior towards other users + +## User Content + +You retain ownership of content you create. By posting content, you grant us a license to display it within the service. + +## Prohibited Activities + +- Harassment or abuse of other users +- Spam or automated abuse +- Attempting to compromise the service + +## Termination + +We reserve the right to terminate accounts that violate these terms. + +## Disclaimer + +This service is provided "as is" without warranties of any kind. + +## Changes + +We may update these terms from time to time. Continued use constitutes acceptance of changes. + +*Last updated: 2025-01-15* diff --git a/app/src/sites/aiat.json b/app/src/sites/aiat.json new file mode 100644 index 00000000..d43259c8 --- /dev/null +++ b/app/src/sites/aiat.json @@ -0,0 +1,13 @@ +{ + "id": "aiat", + "name": "Aiat", + "domain": "at.syui.ai", + "scheme": "aiat", + "version": "1.111.0", + "category": "Social", + "description": "Aiat is a social networking application based on AT Protocol. Connect with your community on syu.is.", + "backLink": "syu.is", + "icon": "/static/app.png", + "os": "iOS 26.0+", + "price": "Free" +} diff --git a/app/src/sites/aicard.json b/app/src/sites/aicard.json new file mode 100644 index 00000000..beef4620 --- /dev/null +++ b/app/src/sites/aicard.json @@ -0,0 +1,13 @@ +{ + "id": "aicard", + "name": "Aicard", + "domain": "card.syui.ai", + "scheme": "aicard", + "version": "1.0.0", + "category": "Game", + "description": "Aicard is a card collection game integrated with AT Protocol.", + "backLink": "syui.ai", + "icon": "/static/app.png", + "os": "iOS 26.0+", + "price": "Free" +} diff --git a/app/src/sites/airse.json b/app/src/sites/airse.json new file mode 100644 index 00000000..f2b06264 --- /dev/null +++ b/app/src/sites/airse.json @@ -0,0 +1,13 @@ +{ + "id": "airse", + "name": "Airse", + "domain": "rse.syui.ai", + "scheme": "airse", + "version": "1.0.0", + "category": "Game", + "description": "Airse is an adventure game built with Unreal Engine, featuring AT Protocol integration.", + "backLink": "syui.ai", + "icon": "/static/app.png", + "os": "Windows / macOS", + "price": "Free" +} diff --git a/app/src/templates/index.ts b/app/src/templates/index.ts new file mode 100644 index 00000000..ac716f23 --- /dev/null +++ b/app/src/templates/index.ts @@ -0,0 +1,3 @@ +export { layout, appLayout, legalLayout } from './layout.js' +export { oauthCallback, clientMetadata } from './oauth.js' +export { baseStyles } from './styles.js' diff --git a/app/src/templates/layout.ts b/app/src/templates/layout.ts new file mode 100644 index 00000000..fae7bfca --- /dev/null +++ b/app/src/templates/layout.ts @@ -0,0 +1,110 @@ +import type { SiteConfig } from '../types.js' +import { baseStyles } from './styles.js' + +export function layout(site: SiteConfig, title: string, content: string): string { + return ` + + + + + ${title} - ${site.name} + + + + +
+ ← Back to ${site.backLink} +
+ ${content} + + +` +} + +export function appLayout(site: SiteConfig, content: string): string { + return layout(site, 'App Info', ` +
+ ${site.name} +
${site.name}
+
v${site.version}
+
+ +
+

${site.description}

+
+ +
+
App Information
+
+
+
Version
+
${site.version}
+
+
+
Category
+
${site.category}
+
+
+
Supported OS
+
${site.os}
+
+
+
Price
+
${site.price}
+
+
+
+ +
+
Developer
+
syui
+ + +
+ +
+
Bitcoin
+
+ + 3BqHXxraZyBapyNpJmniJDh9zqzuB8aoRr + copy +
+
+ + ${content} + + +`) +} + +export function legalLayout(site: SiteConfig, title: string, content: string): string { + return layout(site, title, ` +
+

${title}

+ ${content} +
+`) +} diff --git a/app/src/templates/oauth.ts b/app/src/templates/oauth.ts new file mode 100644 index 00000000..37c57d67 --- /dev/null +++ b/app/src/templates/oauth.ts @@ -0,0 +1,46 @@ +import type { SiteConfig } from '../types.js' + +export function oauthCallback(site: SiteConfig): string { + return ` + + + Redirecting... + + + +

Redirecting to app...

+

If nothing happens, click + here to continue in browser.

+ +` +} + +export function clientMetadata(site: SiteConfig): string { + return JSON.stringify({ + client_id: `https://${site.domain}/.well-known/client-metadata.json`, + client_name: site.name, + client_uri: `https://${site.domain}`, + logo_uri: `https://${site.domain}/static/icon.png`, + redirect_uris: [ + `https://${site.domain}/oauth/callback` + ], + scope: 'atproto transition:generic', + grant_types: [ + 'authorization_code', + 'refresh_token' + ], + response_types: [ + 'code' + ], + token_endpoint_auth_method: 'none', + application_type: 'web', + dpop_bound_access_tokens: true + }, null, 2) +} diff --git a/app/src/templates/styles.ts b/app/src/templates/styles.ts new file mode 100644 index 00000000..6fa052a0 --- /dev/null +++ b/app/src/templates/styles.ts @@ -0,0 +1,51 @@ +export const baseStyles = ` +* { box-sizing: border-box; margin: 0; padding: 0; } +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; + line-height: 1.6; + color: #1a1a1a; + background: #fff; + padding: 20px; + max-width: 800px; + margin: 0 auto; +} +@media (prefers-color-scheme: dark) { + body { background: #000; color: #e0e0e0; } + a { color: #6bb3ff; } + h1, h2, h3 { color: #fff; } + .section { background: #1a1a1a; } + .info-item { background: #2a2a2a; } +} +.header { margin-bottom: 32px; } +.back-link { display: inline-block; margin-bottom: 16px; font-size: 14px; color: #0066cc; text-decoration: none; } +.back-link:hover { text-decoration: underline; } +.app-header { text-align: center; margin-bottom: 32px; } +.app-icon { width: 80px; height: 80px; border-radius: 18px; margin-bottom: 12px; } +.app-name { font-size: 24px; font-weight: bold; margin-bottom: 4px; } +.app-version { font-size: 14px; color: #666; } +.section { background: #f5f5f5; border-radius: 16px; padding: 20px; margin-bottom: 16px; } +.section-title { font-size: 13px; font-weight: 600; color: #999; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 12px; } +.description { font-size: 15px; line-height: 22px; } +.info-grid { display: flex; flex-wrap: wrap; gap: 8px; } +.info-item { flex: 1; min-width: 45%; text-align: center; background: #e8e8e8; border-radius: 12px; padding: 12px; } +.info-label { font-size: 11px; color: #999; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px; } +.info-value { font-size: 16px; font-weight: 600; } +.developer-name { font-size: 18px; font-weight: 600; margin-bottom: 12px; } +.link-row { display: flex; align-items: center; padding: 12px 0; border-top: 1px solid rgba(0,0,0,0.1); } +.link-icon { font-size: 14px; font-weight: 600; color: #666; width: 70px; } +.link-value { flex: 1; font-size: 14px; color: #0084ff; text-decoration: none; } +.link-value:hover { text-decoration: underline; } +.link-arrow { font-size: 16px; color: #ccc; } +.bitcoin-row { display: flex; align-items: center; background: rgba(247, 147, 26, 0.08); border-radius: 12px; padding: 14px; gap: 10px; } +.bitcoin-label { font-size: 18px; font-weight: 600; color: #f7931a; } +.bitcoin-address { flex: 1; font-size: 11px; font-family: monospace; color: #666; word-break: break-all; } +.copy-btn { font-size: 12px; color: #999; cursor: pointer; min-width: 50px; text-align: right; } +.copy-btn:hover { color: #0084ff; } +.footer { text-align: center; margin-top: 32px; padding-top: 20px; border-top: 1px solid #ddd; } +.copyright { font-size: 12px; color: #999; } +.content h1 { font-size: 24px; margin-bottom: 16px; } +.content h2 { font-size: 18px; margin: 24px 0 12px; } +.content p { margin-bottom: 12px; } +.content ul { margin: 12px 0; padding-left: 24px; } +.content li { margin-bottom: 8px; } +` diff --git a/app/src/types.ts b/app/src/types.ts new file mode 100644 index 00000000..64194774 --- /dev/null +++ b/app/src/types.ts @@ -0,0 +1,18 @@ +export interface SiteConfig { + id: string + name: string + domain: string + scheme: string + version: string + category: string + description: string + backLink: string + icon: string + os: string + price: string +} + +export interface PageMeta { + title: string + template?: 'default' | 'app' | 'legal' +} diff --git a/app/tsconfig.json b/app/tsconfig.json new file mode 100644 index 00000000..84fadc17 --- /dev/null +++ b/app/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "esModuleInterop": true, + "strict": true, + "resolveJsonModule": true, + "outDir": "./dist", + "rootDir": "./src", + "declaration": true, + "skipLibCheck": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +}