draw things doodl.waow.tech
draw atproto
0

Configure Feed

Select the types of activity you want to include in your feed.

doodl / src / app.ts
3.2 kB 78 lines
1import { setupCanvas } from "./ui/draw"; 2import type { OAuthSession } from "@atproto/oauth-client-browser"; 3import type { PrefMap } from "./data/prefs"; 4 5// the app shell: the genuinely shared state (session, canvas, the chrome refs), 6// the router, and a tiny event bus so feature modules can react to view changes 7// and ui refreshes without importing each other. each feature `init`s against 8// this; main.ts is just the composition root that wires them together. 9 10const $ = <T extends HTMLElement>(id: string) => document.getElementById(id) as T; 11 12// localStorage keys that cross features (the draft + save intent survive the 13// full-page OAuth redirect; restored during boot in main.ts). 14export const DRAFT_KEY = "doodl:draft"; 15export const PENDING_SAVE = "doodl:pending-save"; 16 17// ---- shared DOM refs + the canvas ---- 18export const pad = setupCanvas($<HTMLCanvasElement>("pad")); 19export const statusEl = $("status"); 20export const sessionEl = $("session"); 21export const saveBtn = $<HTMLButtonElement>("save"); 22export const views = { 23 draw: $("view-draw"), 24 explore: $("view-explore"), 25 settings: $("view-settings"), 26}; 27export const navBtns = { draw: $("nav-draw"), explore: $("nav-explore") }; 28 29// ---- shared mutable state (accessor-guarded so reads are always current) ---- 30let session: OAuthSession | null = null; 31let currentHandle: string | null = null; 32let myDefaults: PrefMap = {}; 33 34export const getSession = () => session; 35export const setSession = (s: OAuthSession | null) => (session = s); 36export const getHandle = () => currentHandle; 37export const setHandle = (h: string | null) => (currentHandle = h); 38export const getDefaults = () => myDefaults; 39export const setDefaults = (d: PrefMap) => (myDefaults = d); 40 41// ---- tiny event bus: "view" (a view was shown) and "refresh" (re-render chrome) ---- 42type Ev = "view" | "refresh"; 43const listeners: Record<Ev, Array<(view?: View) => void>> = { view: [], refresh: [] }; 44export function on(ev: Ev, cb: (view?: View) => void) { 45 listeners[ev].push(cb); 46} 47function emit(ev: Ev, view?: View) { 48 for (const f of listeners[ev]) f(view); 49} 50export function refreshUI() { 51 emit("refresh"); 52} 53 54// ---- router: views are real URLs (wisp --spa serves index.html for any path) ---- 55export type View = "draw" | "explore" | "settings"; 56const VIEW_PATH: Record<View, string> = { 57 draw: "/", 58 explore: "/explore", 59 settings: "/settings", 60}; 61export function routeView(): View { 62 const p = location.pathname.replace(/\/+$/, ""); 63 if (p.endsWith("/explore")) return "explore"; 64 if (p.endsWith("/settings")) return "settings"; 65 return "draw"; 66} 67export function show(view: View, opts: { push?: boolean } = {}) { 68 (Object.keys(views) as View[]).forEach((k) => (views[k].hidden = k !== view)); 69 navBtns.draw.classList.toggle("active", view === "draw"); 70 navBtns.explore.classList.toggle("active", view === "explore"); 71 // the settings trigger is redundant while already on the settings page 72 sessionEl.hidden = view === "settings"; 73 emit("view", view); 74 if (opts.push !== false && location.pathname !== VIEW_PATH[view]) { 75 history.pushState({ view }, "", VIEW_PATH[view]); 76 } 77} 78window.addEventListener("popstate", () => show(routeView(), { push: false }));