Full document, spreadsheet, slideshow, and diagram tooling
0
fork

Configure Feed

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

at main 378 lines 15 kB view raw
1/** 2 * Landing page entry point — wires together state, DOM refs, and 3 * the extracted modules (render, events, create, import, toast). 4 */ 5 6import type { DocumentMeta, Folder, FolderAssignments, StarMap } from './landing-types.js'; 7import { syncKeys, getLocalKeys } from './lib/key-sync.js'; 8import { ensureWrappingKey } from './lib/key-passphrase.js'; 9import { createCommandPalette, type PaletteAction } from './command-palette.js'; 10import { BUILT_IN_TEMPLATES } from './templates.js'; 11import { DEFAULT_SORT } from './landing-utils.js'; 12import { showToast } from './landing-toast.js'; 13import { renderDocuments as doRender, docPath } from './landing-render.js'; 14import type { RenderDeps } from './landing-render.js'; 15import { setupDelegatedListeners, setupOneOffListeners, initUsername } from './landing-events.js'; 16import type { EventDeps } from './landing-events.js'; 17import { createDocument, createFromTemplate, openDailyNote, openCalendar } from './landing-create.js'; 18import type { CreateDeps } from './landing-create.js'; 19import { setupDragAndDrop } from './landing-import.js'; 20import { listDocuments, listTrashedDocuments } from './lib/local-store.js'; 21import { mountOfflineIndicator } from './lib/offline-indicator.js'; 22import { initAuth } from './lib/auth.js'; 23 24// --- DOM refs --- 25const docListEl = document.getElementById('doc-list') as HTMLElement; 26const folderListEl = document.getElementById('folder-list') as HTMLElement; 27const noResultsEl = document.getElementById('no-results') as HTMLElement; 28const newBtn = document.getElementById('new-btn') as HTMLElement; 29const newDropdown = document.getElementById('new-dropdown') as HTMLElement; 30const newMenu = document.getElementById('new-menu') as HTMLElement; 31const dailyNoteBtn = document.getElementById('daily-note') as HTMLElement; 32const moreBtn = document.getElementById('more-btn') as HTMLElement; 33const moreMenu = document.getElementById('more-menu') as HTMLElement; 34const moreMenuPanel = document.getElementById('more-menu-panel') as HTMLElement; 35const searchInput = document.getElementById('search-input') as HTMLInputElement; 36const searchClear = document.getElementById('search-clear') as HTMLElement; 37const sortBtn = document.getElementById('sort-btn') as HTMLElement; 38const sortLabel = document.getElementById('sort-label') as HTMLElement; 39const sortMenu = document.getElementById('sort-menu') as HTMLElement; 40const newFolderBtn = document.getElementById('new-folder-btn') as HTMLElement; 41const backupExportBtn = document.getElementById('backup-export-btn') as HTMLElement; 42const backupImportBtn = document.getElementById('backup-import-btn') as HTMLElement; 43const backupImportInput = document.getElementById('backup-import-input') as HTMLInputElement; 44const viewToggleBtn = document.getElementById('view-toggle') as HTMLElement; 45const breadcrumbsEl = document.getElementById('breadcrumbs') as HTMLElement; 46const trashSection = document.getElementById('trash-section') as HTMLElement; 47const trashToggle = document.getElementById('trash-toggle') as HTMLElement; 48const trashCount = document.getElementById('trash-count') as HTMLElement; 49const trashListEl = document.getElementById('trash-list') as HTMLElement; 50 51// Modals 52const usernameModal = document.getElementById('username-modal') as HTMLElement; 53const usernameInput = document.getElementById('username-input') as HTMLInputElement; 54const usernameConfirm = document.getElementById('username-confirm') as HTMLElement; 55const folderModal = document.getElementById('folder-modal') as HTMLElement; 56const folderModalTitle = document.getElementById('folder-modal-title') as HTMLElement; 57const folderNameInput = document.getElementById('folder-name-input') as HTMLInputElement; 58const folderCancel = document.getElementById('folder-cancel') as HTMLElement; 59const folderConfirmBtn = document.getElementById('folder-confirm') as HTMLElement; 60const moveModal = document.getElementById('move-modal') as HTMLElement; 61const moveFolderList = document.getElementById('move-folder-list') as HTMLElement; 62const moveCancel = document.getElementById('move-cancel') as HTMLElement; 63const userBadge = document.getElementById('user-badge') as HTMLElement; 64 65// --- State --- 66let allDocs: DocumentMeta[] = []; 67let trashedDocs: DocumentMeta[] = []; 68let currentSort: string = localStorage.getItem('atmos-sort') || DEFAULT_SORT; 69let stars: StarMap = JSON.parse(localStorage.getItem('atmos-stars') || '{}'); 70let folders: Folder[] = JSON.parse(localStorage.getItem('atmos-folders') || '[]'); 71let folderAssignments: FolderAssignments = JSON.parse(localStorage.getItem('atmos-folder-assignments') || '{}'); 72let currentFolderId: string | null = null; 73let recentIds: string[] = JSON.parse(localStorage.getItem('atmos-recent') || '[]'); 74let searchQuery = ''; 75let activeTagFilter: string | null = null; 76let activeTypeFilter: string | null = null; 77let trashExpanded = false; 78let viewMode: 'list' | 'grid' = (localStorage.getItem('atmos-view-mode') as 'list' | 'grid') || 'list'; 79let moveDocId: string | null = null; 80 81// --- Build shared deps objects --- 82 83const renderDeps: RenderDeps = { 84 docListEl, folderListEl, noResultsEl, breadcrumbsEl, 85 trashSection, trashCount, trashListEl, 86 viewToggleBtn, moveFolderList, moveModal, 87 88 getAllDocs: () => allDocs, 89 getTrashedDocs: () => trashedDocs, 90 getStars: () => stars, 91 getFolders: () => folders, 92 getFolderAssignments: () => folderAssignments, 93 getCurrentFolderId: () => currentFolderId, 94 getSearchQuery: () => searchQuery, 95 getActiveTagFilter: () => activeTagFilter, 96 getActiveTypeFilter: () => activeTypeFilter, 97 getCurrentSort: () => currentSort, 98 getViewMode: () => viewMode, 99 getTrashExpanded: () => trashExpanded, 100 getRecentIds: () => recentIds, 101 setMoveDocId: (id) => { moveDocId = id; }, 102}; 103 104function renderDocuments() { doRender(renderDeps); } 105 106const eventDeps: EventDeps = { 107 // DOM elements 108 docListEl, folderListEl, trashListEl, breadcrumbsEl, 109 moveFolderList, moveModal, 110 searchInput, searchClear, 111 sortBtn, sortLabel, sortMenu, 112 newFolderBtn, viewToggleBtn, trashToggle, 113 usernameModal, usernameInput, usernameConfirm, 114 folderModal, folderModalTitle, folderNameInput, 115 folderCancel, folderConfirm: folderConfirmBtn, moveCancel, userBadge, 116 backupExportBtn, backupImportBtn, backupImportInput, 117 118 // State accessors 119 getAllDocs: () => allDocs, 120 setAllDocs: (d) => { allDocs = d; }, 121 getTrashedDocs: () => trashedDocs, 122 setTrashedDocs: (d) => { trashedDocs = d; }, 123 getStars: () => stars, 124 setStars: (s) => { stars = s; }, 125 getFolders: () => folders, 126 setFolders: (f) => { folders = f; }, 127 getFolderAssignments: () => folderAssignments, 128 setFolderAssignments: (a) => { folderAssignments = a; }, 129 getCurrentFolderId: () => currentFolderId, 130 setCurrentFolderId: (id) => { currentFolderId = id; }, 131 getRecentIds: () => recentIds, 132 setRecentIds: (ids) => { recentIds = ids; }, 133 getSearchQuery: () => searchQuery, 134 setSearchQuery: (q) => { searchQuery = q; }, 135 getActiveTagFilter: () => activeTagFilter, 136 setActiveTagFilter: (t) => { activeTagFilter = t; }, 137 getActiveTypeFilter: () => activeTypeFilter, 138 setActiveTypeFilter: (t: string | null) => { activeTypeFilter = t; }, 139 getTrashExpanded: () => trashExpanded, 140 setTrashExpanded: (v) => { trashExpanded = v; }, 141 getViewMode: () => viewMode, 142 setViewMode: (m) => { viewMode = m; }, 143 getCurrentSort: () => currentSort, 144 setCurrentSort: (s) => { currentSort = s; }, 145 getMoveDocId: () => moveDocId, 146 setMoveDocId: (id) => { moveDocId = id; }, 147 148 // Callbacks 149 renderDocuments, 150 loadDocuments, 151 getRenderDeps: () => renderDeps, 152}; 153 154const createDeps: CreateDeps = { 155 getCurrentFolderId: () => currentFolderId, 156 getFolderAssignments: () => folderAssignments, 157 setFolderAssignments: (a) => { folderAssignments = a; }, 158 getRecentIds: () => recentIds, 159 setRecentIds: (ids) => { recentIds = ids; }, 160 getAllDocs: () => allDocs, 161}; 162 163// --- Wire up event listeners --- 164setupDelegatedListeners(eventDeps); 165setupOneOffListeners(eventDeps); 166 167// --- New document menu --- 168function handleNewAction(kind: string): void { 169 switch (kind) { 170 case 'doc': 171 case 'sheet': 172 case 'form': 173 case 'diagram': 174 createDocument(createDeps, kind); 175 break; 176 case 'calendar': 177 openCalendar(createDeps); 178 break; 179 } 180} 181 182function closeNewMenu(): void { 183 newDropdown.classList.remove('open'); 184 newBtn.setAttribute('aria-expanded', 'false'); 185} 186function toggleNewMenu(): void { 187 const open = newDropdown.classList.toggle('open'); 188 newBtn.setAttribute('aria-expanded', open ? 'true' : 'false'); 189 if (open) closeMoreMenu(); 190} 191newBtn.addEventListener('click', (e) => { 192 e.preventDefault(); 193 e.stopPropagation(); 194 toggleNewMenu(); 195}); 196newMenu.addEventListener('click', (e) => { 197 const target = (e.target as HTMLElement).closest('.new-menu-item') as HTMLElement | null; 198 if (!target) return; 199 if ((target as HTMLButtonElement).disabled) return; 200 const kind = target.dataset.new; 201 if (!kind) return; 202 closeNewMenu(); 203 handleNewAction(kind); 204}); 205 206dailyNoteBtn.addEventListener('click', (e) => { e.preventDefault(); openDailyNote(createDeps); }); 207 208// --- More menu (folder / import / backup) --- 209function closeMoreMenu(): void { 210 moreMenu.classList.remove('open'); 211 moreBtn.setAttribute('aria-expanded', 'false'); 212} 213function toggleMoreMenu(): void { 214 const open = moreMenu.classList.toggle('open'); 215 moreBtn.setAttribute('aria-expanded', open ? 'true' : 'false'); 216 if (open) closeNewMenu(); 217} 218moreBtn.addEventListener('click', (e) => { 219 e.preventDefault(); 220 e.stopPropagation(); 221 toggleMoreMenu(); 222}); 223// Clicking any item inside the more menu closes it afterward. 224// The item's own handler (wired in landing-events.ts) still runs. 225moreMenuPanel.addEventListener('click', (e) => { 226 const target = (e.target as HTMLElement).closest('.more-menu-item'); 227 if (target) closeMoreMenu(); 228}); 229 230// Dismiss menus on outside click / Escape 231document.addEventListener('click', (e) => { 232 const t = e.target as HTMLElement; 233 if (!newDropdown.contains(t)) closeNewMenu(); 234 if (!moreMenu.contains(t)) closeMoreMenu(); 235}); 236document.addEventListener('keydown', (e) => { 237 if (e.key === 'Escape') { 238 closeNewMenu(); 239 closeMoreMenu(); 240 } 241}); 242 243// --- Drag-and-drop file import --- 244setupDragAndDrop({ 245 getCurrentFolderId: () => currentFolderId, 246 getFolderAssignments: () => folderAssignments, 247 setFolderAssignments: (a) => { folderAssignments = a; }, 248}); 249 250// --- Attach key strings to documents for URL navigation --- 251async function attachKeyStrings(docs: DocumentMeta[]): Promise<void> { 252 const keys = await getLocalKeys(); 253 for (const doc of docs) { 254 doc._keyStr = keys[doc.id]; 255 } 256} 257 258// --- Load & render --- 259async function loadDocuments() { 260 try { 261 const [active, trashed] = await Promise.all([ 262 listDocuments(), 263 listTrashedDocuments(), 264 ]); 265 266 const docs: DocumentMeta[] = active.map(d => ({ 267 id: d.id, 268 type: d.type, 269 name: d.name, 270 deleted_at: d.deleted_at, 271 tags: d.tags, 272 created_at: d.created_at, 273 updated_at: d.updated_at, 274 })); 275 276 const trash: DocumentMeta[] = trashed.map(d => ({ 277 id: d.id, 278 type: d.type, 279 name: d.name, 280 deleted_at: d.deleted_at, 281 tags: d.tags, 282 created_at: d.created_at, 283 updated_at: d.updated_at, 284 })); 285 286 await Promise.all([attachKeyStrings(docs), attachKeyStrings(trash)]); 287 288 allDocs = docs; 289 trashedDocs = trash; 290 renderDocuments(); 291 } catch (err) { 292 console.error('Failed to load documents:', err); 293 showToast('Failed to load documents — try refreshing', 5000, true); 294 } 295} 296 297// Mount offline indicator badge (fixed position, hidden until offline) 298mountOfflineIndicator(); 299 300// --- Command Palette --- 301createCommandPalette({ 302 actions: [ 303 { id: 'new-doc', label: 'New Document', category: 'action', icon: '\u270e', action: () => createDocument(createDeps, 'doc') }, 304 { id: 'new-sheet', label: 'New Spreadsheet', category: 'action', icon: '\u25a6', action: () => createDocument(createDeps, 'sheet') }, 305 { id: 'new-form', label: 'New Form', category: 'action', icon: '\u2637', action: () => createDocument(createDeps, 'form') }, 306 { id: 'new-diagram', label: 'New Diagram', category: 'action', icon: '\u25d3', action: () => createDocument(createDeps, 'diagram') }, 307 { id: 'new-calendar', label: 'Calendar', category: 'action', icon: '\u2630', action: () => openCalendar(createDeps) }, 308 { id: 'daily-note', label: "Today's Note", category: 'action', icon: '\u2666', action: () => openDailyNote(createDeps) }, 309 { id: 'backup-export', label: 'Export Backup', category: 'action', icon: '\u2913', action: () => backupExportBtn.click() }, 310 { id: 'backup-import', label: 'Restore Backup', category: 'action', icon: '\u2912', action: () => backupImportBtn.click() }, 311 ...BUILT_IN_TEMPLATES.map(t => ({ 312 id: `template-${t.id}`, 313 label: `Template: ${t.name}`, 314 category: 'action' as const, 315 icon: t.icon, 316 action: () => createFromTemplate(createDeps, t.id), 317 })), 318 ], 319 fetchDocuments: async (): Promise<PaletteAction[]> => { 320 return allDocs 321 .filter(doc => doc._keyStr) 322 .map(doc => { 323 const path = docPath(doc.type); 324 return { 325 id: `doc-${doc.id}`, 326 label: doc.name || 'Untitled', 327 category: 'document' as const, 328 icon: { doc: '\u270e', sheet: '\u25a6', form: '\u2637', slide: '\u25eb', diagram: '\u25d3', calendar: '\u2630' }[doc.type] || '\u25a6', 329 action: () => { window.location.href = `${path}/${doc.id}#${doc._keyStr}`; }, 330 }; 331 }); 332 }, 333}); 334 335// --- Instance info panel --- 336document.getElementById('instance-info-btn')?.addEventListener('click', async () => { 337 const { getInstanceInfo, describeInstance } = await import('./lib/instance-info.js'); 338 const info = await getInstanceInfo(); 339 const desc = describeInstance(info); 340 const { showInstanceInfoModal } = await import('./lib/instance-info-modal.js'); 341 showInstanceInfoModal(desc, info); 342}); 343 344// --- Init --- 345initUsername(eventDeps); 346ensureWrappingKey() 347 .then(() => syncKeys()) 348 .then(async () => { 349 await initAuth(); 350 const { ensurePdsIdentity } = await import('./lib/pds-setup.js'); 351 const ready = await ensurePdsIdentity(); 352 if (ready) { 353 const storageText = document.getElementById('storage-status-text'); 354 if (storageText) storageText.textContent = 'Documents are encrypted and synced to your PDS.'; 355 const { pushLocalDocuments } = await import('./lib/pds-push-sync.js'); 356 await pushLocalDocuments(); 357 const { pullRemoteDocuments } = await import('./lib/pds-pull-sync.js'); 358 await pullRemoteDocuments(); 359 } 360 }) 361 .then(() => loadDocuments()); 362 363// --- Handle PWA shortcut actions (?action=new-doc, etc.) --- 364const urlAction = new URLSearchParams(window.location.search).get('action'); 365if (urlAction) { 366 const actionMap: Record<string, () => void> = { 367 'new-doc': () => createDocument(createDeps, 'doc'), 368 'new-sheet': () => createDocument(createDeps, 'sheet'), 369 'new-form': () => createDocument(createDeps, 'form'), 370 'new-diagram': () => createDocument(createDeps, 'diagram'), 371 'new-calendar': () => openCalendar(createDeps), 372 }; 373 const handler = actionMap[urlAction]; 374 if (handler) { 375 history.replaceState(null, '', '/'); 376 handler(); 377 } 378}