| 1 |
|
| 2 |
* @fileoverview App bootstrap, keyboard shortcuts, and native menu integration. |
| 3 |
* |
| 4 |
* `init()` runs on DOMContentLoaded: loads theme, data, wires up event listeners, |
| 5 |
* and shows first-run welcome. Keyboard shortcuts use vim-style navigation (j/k) |
| 6 |
* plus single-key actions (s=star, r=read, /=search, ?=help). |
| 7 |
|
| 8 |
(function() { |
| 9 |
'use strict'; |
| 10 |
|
| 11 |
const { invoke } = window.__TAURI__.core; |
| 12 |
|
| 13 |
|
| 14 |
let initialized = false; |
| 15 |
|
| 16 |
|
| 17 |
let loadRequestId = 0; |
| 18 |
|
| 19 |
|
| 20 |
* Initialize the application: load theme and data, wire up UI, show welcome. |
| 21 |
* @returns {Promise<void>} |
| 22 |
|
| 23 |
async function init() { |
| 24 |
if (initialized) return; |
| 25 |
initialized = true; |
| 26 |
|
| 27 |
|
| 28 |
await BB.themes.init(); |
| 29 |
|
| 30 |
|
| 31 |
try { |
| 32 |
await BB.sources.load(); |
| 33 |
await BB.items.load(); |
| 34 |
} catch (err) { |
| 35 |
BB.ui.showErrorWithRetry( |
| 36 |
'Failed to load data: ' + BB.utils.getErrorMessage(err), |
| 37 |
() => { BB.sources.load(); BB.items.load(); } |
| 38 |
); |
| 39 |
} |
| 40 |
|
| 41 |
|
| 42 |
BB.bookmarks.migrateFromLocalStorage().then(() => BB.bookmarks.updateBadge()); |
| 43 |
|
| 44 |
BB.navigation.init(); |
| 45 |
|
| 46 |
|
| 47 |
try { |
| 48 |
const welcomed = await invoke('get_config', { key: 'bb-welcomed' }); |
| 49 |
if (!welcomed) { |
| 50 |
showWelcome(); |
| 51 |
} |
| 52 |
} catch (_) { |
| 53 |
|
| 54 |
} |
| 55 |
|
| 56 |
|
| 57 |
const searchInput = document.getElementById('search-input'); |
| 58 |
const searchSpinner = document.getElementById('search-spinner'); |
| 59 |
searchInput.addEventListener('input', BB.utils.debounce(async () => { |
| 60 |
BB.state.set('currentSearch', searchInput.value); |
| 61 |
BB.state.resetPagination(); |
| 62 |
const myId = ++loadRequestId; |
| 63 |
searchSpinner.classList.add('active'); |
| 64 |
await BB.items.load(); |
| 65 |
if (loadRequestId === myId) { |
| 66 |
searchSpinner.classList.remove('active'); |
| 67 |
} |
| 68 |
}, 300)); |
| 69 |
|
| 70 |
|
| 71 |
document.getElementById('sort-select').addEventListener('change', async (e) => { |
| 72 |
BB.state.set('currentOrder', e.target.value); |
| 73 |
BB.state.resetPagination(); |
| 74 |
++loadRequestId; |
| 75 |
await BB.items.load(); |
| 76 |
}); |
| 77 |
|
| 78 |
|
| 79 |
document.getElementById('refresh-btn').addEventListener('click', BB.feeds.refresh); |
| 80 |
document.getElementById('add-feed-btn').addEventListener('click', BB.feeds.openAddFeed); |
| 81 |
document.getElementById('save-detail').addEventListener('click', BB.detail.bookmarkItem); |
| 82 |
document.getElementById('close-detail').addEventListener('click', BB.detail.close); |
| 83 |
document.getElementById('saved-articles-btn').addEventListener('click', () => BB.sources.select('__saved__')); |
| 84 |
document.getElementById('load-more-btn').addEventListener('click', BB.items.loadMore); |
| 85 |
document.getElementById('settings-btn').addEventListener('click', showSettings); |
| 86 |
document.getElementById('sync-settings-btn').addEventListener('click', BB.sync.openSettings); |
| 87 |
document.getElementById('help-btn').addEventListener('click', showHelp); |
| 88 |
document.getElementById('unread-toggle').addEventListener('click', toggleUnreadFilter); |
| 89 |
document.getElementById('mark-all-read-btn').addEventListener('click', markAllReadGlobal); |
| 90 |
|
| 91 |
|
| 92 |
document.getElementById('modal-overlay').addEventListener('click', (e) => { |
| 93 |
if (e.target.id === 'modal-overlay') BB.ui.closeModal(); |
| 94 |
}); |
| 95 |
document.getElementById('close-modal').addEventListener('click', BB.ui.closeModal); |
| 96 |
|
| 97 |
|
| 98 |
|
| 99 |
document.addEventListener('contextmenu', (e) => { |
| 100 |
e.preventDefault(); |
| 101 |
showContextMenu(e.clientX, e.clientY); |
| 102 |
}); |
| 103 |
|
| 104 |
|
| 105 |
document.addEventListener('keydown', handleKeyboard); |
| 106 |
|
| 107 |
|
| 108 |
setupMenuListeners(); |
| 109 |
} |
| 110 |
|
| 111 |
|
| 112 |
* Toggle the "unread only" filter on/off. |
| 113 |
|
| 114 |
function toggleUnreadFilter() { |
| 115 |
const btn = document.getElementById('unread-toggle'); |
| 116 |
const active = btn.getAttribute('aria-pressed') === 'true'; |
| 117 |
btn.setAttribute('aria-pressed', !active); |
| 118 |
btn.classList.toggle('active', !active); |
| 119 |
BB.state.set('unreadOnly', !active); |
| 120 |
BB.state.resetPagination(true); |
| 121 |
BB.items.load(); |
| 122 |
} |
| 123 |
|
| 124 |
|
| 125 |
* Mark all items as read (global or per-source). |
| 126 |
|
| 127 |
async function markAllReadGlobal() { |
| 128 |
const source = BB.state.currentSource || null; |
| 129 |
const label = source |
| 130 |
? (BB.state.sources.find(s => s.id === source) || {}).name || 'this source' |
| 131 |
: 'all sources'; |
| 132 |
const ok = await BB.ui.confirmAction('Mark all items in ' + label + ' as read?'); |
| 133 |
if (!ok) return; |
| 134 |
try { |
| 135 |
await BB.api.items.markAllRead(source); |
| 136 |
BB.ui.showToast('Marked all as read'); |
| 137 |
await BB.sources.load(); |
| 138 |
await BB.items.load(); |
| 139 |
} catch (err) { |
| 140 |
BB.ui.showToast('Failed: ' + BB.utils.getErrorMessage(err), 'error'); |
| 141 |
} |
| 142 |
} |
| 143 |
|
| 144 |
|
| 145 |
* Global keyboard shortcut handler. Skipped when focus is in a form input. |
| 146 |
* Key choices follow common reader conventions: j/k from vim, s/r from |
| 147 |
* Google Reader, /=search from vim, ?=help from many CLI tools. |
| 148 |
* @param {KeyboardEvent} e - The keydown event. |
| 149 |
|
| 150 |
function handleKeyboard(e) { |
| 151 |
|
| 152 |
|
| 153 |
|
| 154 |
if (BB.state.isTouchDevice) return; |
| 155 |
|
| 156 |
|
| 157 |
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'SELECT') { |
| 158 |
if (e.key === 'Escape') { |
| 159 |
e.target.blur(); |
| 160 |
} |
| 161 |
return; |
| 162 |
} |
| 163 |
|
| 164 |
const items = BB.state.items; |
| 165 |
const selectedId = BB.state.selectedItemId; |
| 166 |
const currentIdx = selectedId ? items.findIndex(i => i.id === selectedId) : -1; |
| 167 |
|
| 168 |
switch (e.key) { |
| 169 |
case '?': |
| 170 |
e.preventDefault(); |
| 171 |
showHelp(); |
| 172 |
return; |
| 173 |
|
| 174 |
case 'j': |
| 175 |
e.preventDefault(); |
| 176 |
if (currentIdx < items.length - 1) { |
| 177 |
BB.items.selectItem(items[currentIdx + 1].id); |
| 178 |
} else if (items.length > 0 && currentIdx === -1) { |
| 179 |
BB.items.selectItem(items[0].id); |
| 180 |
} |
| 181 |
break; |
| 182 |
|
| 183 |
case 'k': |
| 184 |
e.preventDefault(); |
| 185 |
if (currentIdx > 0) { |
| 186 |
BB.items.selectItem(items[currentIdx - 1].id); |
| 187 |
} |
| 188 |
break; |
| 189 |
|
| 190 |
case 's': |
| 191 |
e.preventDefault(); |
| 192 |
if (selectedId) { |
| 193 |
const item = items.find(i => i.id === selectedId); |
| 194 |
if (item) BB.items.toggleStar(selectedId, item.isStarred); |
| 195 |
} |
| 196 |
break; |
| 197 |
|
| 198 |
case 'r': |
| 199 |
if (!e.metaKey && !e.ctrlKey) { |
| 200 |
e.preventDefault(); |
| 201 |
if (selectedId) { |
| 202 |
const item = items.find(i => i.id === selectedId); |
| 203 |
if (item) BB.items.toggleRead(selectedId, item.isRead); |
| 204 |
} |
| 205 |
} |
| 206 |
break; |
| 207 |
|
| 208 |
case 'u': |
| 209 |
e.preventDefault(); |
| 210 |
toggleUnreadFilter(); |
| 211 |
break; |
| 212 |
|
| 213 |
case 'A': |
| 214 |
if (e.shiftKey) { |
| 215 |
e.preventDefault(); |
| 216 |
markAllReadGlobal(); |
| 217 |
} |
| 218 |
break; |
| 219 |
|
| 220 |
case '/': |
| 221 |
e.preventDefault(); |
| 222 |
document.getElementById('search-input').focus(); |
| 223 |
break; |
| 224 |
|
| 225 |
case 'Escape': |
| 226 |
if (document.querySelector('.main.reader-expanded')) { |
| 227 |
BB.detail.collapseReader(); |
| 228 |
} else if (BB.state.selectedItemId) { |
| 229 |
BB.detail.close(); |
| 230 |
} |
| 231 |
break; |
| 232 |
|
| 233 |
case 'o': |
| 234 |
case 'Enter': |
| 235 |
if (selectedId) { |
| 236 |
BB.detail.openUrl(); |
| 237 |
} |
| 238 |
break; |
| 239 |
} |
| 240 |
} |
| 241 |
|
| 242 |
|
| 243 |
* Show a custom right-click context menu at the given coordinates. |
| 244 |
* @param {number} x - Viewport X position in pixels. |
| 245 |
* @param {number} y - Viewport Y position in pixels. |
| 246 |
|
| 247 |
function showContextMenu(x, y) { |
| 248 |
const old = document.getElementById('context-menu'); |
| 249 |
if (old) old.remove(); |
| 250 |
|
| 251 |
const menu = document.createElement('div'); |
| 252 |
menu.id = 'context-menu'; |
| 253 |
menu.className = 'context-menu'; |
| 254 |
menu.style.left = x + 'px'; |
| 255 |
menu.style.top = y + 'px'; |
| 256 |
|
| 257 |
const refreshItem = document.createElement('button'); |
| 258 |
refreshItem.className = 'context-menu-item'; |
| 259 |
refreshItem.textContent = 'Refresh Feeds'; |
| 260 |
refreshItem.onclick = () => { menu.remove(); BB.feeds.refresh(); }; |
| 261 |
menu.appendChild(refreshItem); |
| 262 |
|
| 263 |
document.body.appendChild(menu); |
| 264 |
|
| 265 |
|
| 266 |
const rect = menu.getBoundingClientRect(); |
| 267 |
if (rect.right > window.innerWidth) menu.style.left = (window.innerWidth - rect.width - 4) + 'px'; |
| 268 |
if (rect.bottom > window.innerHeight) menu.style.top = (window.innerHeight - rect.height - 4) + 'px'; |
| 269 |
|
| 270 |
|
| 271 |
requestAnimationFrame(() => { |
| 272 |
document.addEventListener('click', function dismiss() { |
| 273 |
menu.remove(); |
| 274 |
document.removeEventListener('click', dismiss); |
| 275 |
}, { once: true }); |
| 276 |
}); |
| 277 |
} |
| 278 |
|
| 279 |
|
| 280 |
function setupMenuListeners() { |
| 281 |
const listen = window.__TAURI__.event.listen; |
| 282 |
|
| 283 |
|
| 284 |
listen('auto-fetch-complete', () => { |
| 285 |
BB.sources.load(); |
| 286 |
BB.items.load(); |
| 287 |
}); |
| 288 |
|
| 289 |
listen('auto-fetch-error', (event) => { |
| 290 |
const pluginId = event.payload?.pluginId || 'unknown'; |
| 291 |
const error = event.payload?.error || ''; |
| 292 |
BB.ui.showToast('Failed to fetch ' + pluginId + (error ? ': ' + error : ''), 'error'); |
| 293 |
}); |
| 294 |
|
| 295 |
listen('feed-circuit-broken', (event) => { |
| 296 |
const pluginId = event.payload?.pluginId || 'unknown'; |
| 297 |
BB.ui.showErrorWithRetry( |
| 298 |
'Feed "' + pluginId + '" disabled after repeated failures', |
| 299 |
async () => { |
| 300 |
try { |
| 301 |
await BB.api.feeds.resetCircuitBreaker(pluginId); |
| 302 |
BB.ui.showToast('Feed re-enabled'); |
| 303 |
BB.sources.load(); |
| 304 |
BB.items.load(); |
| 305 |
} catch (err) { |
| 306 |
BB.ui.showToast('Failed to reset: ' + BB.utils.getErrorMessage(err), 'error'); |
| 307 |
} |
| 308 |
} |
| 309 |
); |
| 310 |
BB.sources.load(); |
| 311 |
}); |
| 312 |
|
| 313 |
listen('menu:refresh', () => BB.feeds.refresh()); |
| 314 |
listen('menu:add_feed', () => BB.feeds.openAddFeed()); |
| 315 |
listen('menu:import_opml', () => BB.feeds.importOpml()); |
| 316 |
listen('menu:export_opml', () => BB.feeds.exportOpml()); |
| 317 |
listen('menu:view_all', async () => { |
| 318 |
BB.state.set('currentSource', ''); |
| 319 |
BB.state.resetPagination(); |
| 320 |
await BB.items.load(); |
| 321 |
BB.sources.render(BB.state.sources); |
| 322 |
}); |
| 323 |
listen('menu:view_unread', () => { |
| 324 |
BB.state.set('currentOrder', 'unread'); |
| 325 |
document.getElementById('sort-select').value = 'unread'; |
| 326 |
BB.state.resetPagination(); |
| 327 |
BB.items.load(); |
| 328 |
}); |
| 329 |
listen('menu:view_starred', () => { |
| 330 |
BB.state.set('currentOrder', 'starred'); |
| 331 |
document.getElementById('sort-select').value = 'starred'; |
| 332 |
BB.state.resetPagination(); |
| 333 |
BB.items.load(); |
| 334 |
}); |
| 335 |
|
| 336 |
|
| 337 |
window.addEventListener('beforeunload', () => { |
| 338 |
const read = BB.state.sessionArticlesRead; |
| 339 |
const starred = BB.state.sessionArticlesStarred; |
| 340 |
if (read > 0 || starred > 0) { |
| 341 |
const parts = []; |
| 342 |
if (read > 0) parts.push(read + ' read'); |
| 343 |
if (starred > 0) parts.push(starred + ' starred'); |
| 344 |
BB.ui.showToast('This session: ' + parts.join(', ')); |
| 345 |
} |
| 346 |
}); |
| 347 |
} |
| 348 |
|
| 349 |
|
| 350 |
function showSettings() { |
| 351 |
const body = document.getElementById('modal-body'); |
| 352 |
const title = document.getElementById('modal-title'); |
| 353 |
title.textContent = 'Settings'; |
| 354 |
body.innerHTML = ''; |
| 355 |
const content = document.createElement('div'); |
| 356 |
content.className = 'settings-content'; |
| 357 |
|
| 358 |
|
| 359 |
const themeGroup = document.createElement('div'); |
| 360 |
themeGroup.className = 'form-group'; |
| 361 |
const themeLabel = document.createElement('label'); |
| 362 |
themeLabel.textContent = 'Theme'; |
| 363 |
const themeContainer = document.createElement('div'); |
| 364 |
themeContainer.id = 'settings-theme-container'; |
| 365 |
themeGroup.appendChild(themeLabel); |
| 366 |
themeGroup.appendChild(themeContainer); |
| 367 |
|
| 368 |
|
| 369 |
const themeActions = document.createElement('div'); |
| 370 |
themeActions.className = 'theme-actions'; |
| 371 |
|
| 372 |
const themeImportBtn = document.createElement('button'); |
| 373 |
themeImportBtn.className = 'btn btn-small'; |
| 374 |
themeImportBtn.textContent = 'Import Theme'; |
| 375 |
themeImportBtn.onclick = () => BB.themes.importTheme(); |
| 376 |
themeActions.appendChild(themeImportBtn); |
| 377 |
|
| 378 |
const themeExportBtn = document.createElement('button'); |
| 379 |
themeExportBtn.className = 'btn btn-small'; |
| 380 |
themeExportBtn.textContent = 'Export Current'; |
| 381 |
themeExportBtn.onclick = () => BB.themes.exportTheme(); |
| 382 |
themeActions.appendChild(themeExportBtn); |
| 383 |
|
| 384 |
themeGroup.appendChild(themeActions); |
| 385 |
content.appendChild(themeGroup); |
| 386 |
|
| 387 |
|
| 388 |
const dataGroup = document.createElement('div'); |
| 389 |
dataGroup.className = 'form-group'; |
| 390 |
const dataLabel = document.createElement('label'); |
| 391 |
dataLabel.textContent = 'Data'; |
| 392 |
dataGroup.appendChild(dataLabel); |
| 393 |
|
| 394 |
const dataActions = document.createElement('div'); |
| 395 |
dataActions.className = 'form-row'; |
| 396 |
|
| 397 |
const importBtn = document.createElement('button'); |
| 398 |
importBtn.className = 'btn btn-small'; |
| 399 |
importBtn.textContent = 'Import OPML'; |
| 400 |
importBtn.onclick = () => { BB.ui.closeModal(); BB.feeds.importOpml(); }; |
| 401 |
dataActions.appendChild(importBtn); |
| 402 |
|
| 403 |
const exportBtn = document.createElement('button'); |
| 404 |
exportBtn.className = 'btn btn-small'; |
| 405 |
exportBtn.textContent = 'Export OPML'; |
| 406 |
exportBtn.onclick = () => { BB.ui.closeModal(); BB.feeds.exportOpml(); }; |
| 407 |
dataActions.appendChild(exportBtn); |
| 408 |
|
| 409 |
dataGroup.appendChild(dataActions); |
| 410 |
content.appendChild(dataGroup); |
| 411 |
|
| 412 |
|
| 413 |
if (BB.sync) { |
| 414 |
const syncGroup = document.createElement('div'); |
| 415 |
syncGroup.className = 'form-group'; |
| 416 |
const syncLabel = document.createElement('label'); |
| 417 |
syncLabel.textContent = 'Cloud Sync'; |
| 418 |
syncGroup.appendChild(syncLabel); |
| 419 |
const syncBtn = document.createElement('button'); |
| 420 |
syncBtn.className = 'btn btn-small'; |
| 421 |
syncBtn.textContent = 'Sync Settings'; |
| 422 |
syncBtn.onclick = () => { BB.ui.closeModal(); BB.sync.openSettings(); }; |
| 423 |
syncGroup.appendChild(syncBtn); |
| 424 |
content.appendChild(syncGroup); |
| 425 |
} |
| 426 |
|
| 427 |
const actions = document.createElement('div'); |
| 428 |
actions.className = 'form-actions'; |
| 429 |
const closeBtn = document.createElement('button'); |
| 430 |
closeBtn.className = 'btn'; |
| 431 |
closeBtn.textContent = 'Close'; |
| 432 |
closeBtn.addEventListener('click', BB.ui.closeModal); |
| 433 |
actions.appendChild(closeBtn); |
| 434 |
content.appendChild(actions); |
| 435 |
|
| 436 |
body.appendChild(content); |
| 437 |
BB.themes.buildSelector(themeContainer); |
| 438 |
BB.ui.openModal(); |
| 439 |
} |
| 440 |
|
| 441 |
|
| 442 |
function showHelp() { |
| 443 |
const body = document.getElementById('modal-body'); |
| 444 |
const title = document.getElementById('modal-title'); |
| 445 |
title.textContent = 'Keyboard Shortcuts'; |
| 446 |
body.innerHTML = ` |
| 447 |
<div class="help-shortcuts"> |
| 448 |
<div class="help-section"> |
| 449 |
<h3>Navigation</h3> |
| 450 |
<div class="help-row"><kbd>j</kbd><span>Next item</span></div> |
| 451 |
<div class="help-row"><kbd>k</kbd><span>Previous item</span></div> |
| 452 |
<div class="help-row"><kbd>o</kbd> / <kbd>Enter</kbd><span>Open in browser</span></div> |
| 453 |
<div class="help-row"><kbd>/</kbd><span>Focus search</span></div> |
| 454 |
<div class="help-row"><kbd>Esc</kbd><span>Close detail panel</span></div> |
| 455 |
</div> |
| 456 |
<div class="help-section"> |
| 457 |
<h3>Actions</h3> |
| 458 |
<div class="help-row"><kbd>s</kbd><span>Star / unstar</span></div> |
| 459 |
<div class="help-row"><kbd>r</kbd><span>Toggle read / unread</span></div> |
| 460 |
<div class="help-row"><kbd>u</kbd><span>Toggle unread-only filter</span></div> |
| 461 |
<div class="help-row"><kbd>Shift</kbd> <kbd>A</kbd><span>Mark all as read</span></div> |
| 462 |
<div class="help-row"><kbd>?</kbd><span>Show this help</span></div> |
| 463 |
</div> |
| 464 |
<div class="help-section"> |
| 465 |
<h3>Menu Shortcuts</h3> |
| 466 |
<div class="help-row"><kbd>\u2318R</kbd><span>Refresh all feeds</span></div> |
| 467 |
<div class="help-row"><kbd>\u2318N</kbd><span>Add new feed</span></div> |
| 468 |
<div class="help-row"><kbd>\u2318I</kbd><span>Import OPML</span></div> |
| 469 |
<div class="help-row"><kbd>\u2318E</kbd><span>Export OPML</span></div> |
| 470 |
</div> |
| 471 |
</div> |
| 472 |
`; |
| 473 |
BB.ui.openModal(); |
| 474 |
} |
| 475 |
|
| 476 |
|
| 477 |
function showWelcome() { |
| 478 |
const body = document.getElementById('modal-body'); |
| 479 |
const title = document.getElementById('modal-title'); |
| 480 |
title.textContent = 'Welcome to Balanced Breakfast'; |
| 481 |
body.innerHTML = ''; |
| 482 |
const content = document.createElement('div'); |
| 483 |
content.className = 'welcome-content'; |
| 484 |
content.innerHTML = |
| 485 |
'<p>Your personal feed reader for RSS, Atom, podcasts, and more.</p>' + |
| 486 |
'<h3>Getting Started</h3>' + |
| 487 |
'<p>Subscribe to feeds with the <strong>+ Add Feed</strong> button, or import existing subscriptions via <strong>File > Import OPML</strong>.</p>' + |
| 488 |
'<h3>Keyboard Shortcuts</h3>' + |
| 489 |
'<p>Press <kbd>?</kbd> anytime to see all shortcuts. Navigate with <kbd>j</kbd>/<kbd>k</kbd>, star with <kbd>s</kbd>, toggle read with <kbd>r</kbd>.</p>' + |
| 490 |
'<h3>Themes & Settings</h3>' + |
| 491 |
'<p>Click the gear icon (<strong>⚙</strong>) in the header to change themes and configure preferences.</p>'; |
| 492 |
|
| 493 |
const cta = document.createElement('div'); |
| 494 |
cta.className = 'welcome-cta'; |
| 495 |
const addBtn = document.createElement('button'); |
| 496 |
addBtn.className = 'btn btn-success'; |
| 497 |
addBtn.textContent = 'Add Your First Feed'; |
| 498 |
addBtn.addEventListener('click', BB.feeds.openAddFeed); |
| 499 |
const exploreBtn = document.createElement('button'); |
| 500 |
exploreBtn.className = 'btn'; |
| 501 |
exploreBtn.textContent = 'Explore First'; |
| 502 |
exploreBtn.addEventListener('click', BB.ui.closeModal); |
| 503 |
cta.appendChild(addBtn); |
| 504 |
cta.appendChild(exploreBtn); |
| 505 |
content.appendChild(cta); |
| 506 |
|
| 507 |
body.appendChild(content); |
| 508 |
BB.ui.openModal(); |
| 509 |
invoke('set_config', { key: 'bb-welcomed', value: '1' }); |
| 510 |
} |
| 511 |
|
| 512 |
BB.app = { init, showHelp, showSettings, showWelcome }; |
| 513 |
|
| 514 |
|
| 515 |
if (document.readyState === 'loading') { |
| 516 |
document.addEventListener('DOMContentLoaded', init); |
| 517 |
} else { |
| 518 |
init(); |
| 519 |
} |
| 520 |
})(); |
| 521 |
|