1// Small entry points for Home's optional features. Their implementation stays 2// precached for offline use, but is only executed when the user opens it. 3(function () { 4 'use strict'; 5 var assets = Object.create(null); 6 var features = Object.create(null); 7 var editorCallbacks = {}; 8 var openingEditor = null; 9 var pending = 0; 10 var domReady = new Promise(function (resolve) { 11 if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', resolve, { once: true }); 12 else resolve(); 13 }); 14 var definitions = { 15 editor: { label: 'sighting editor', scripts: ['/media.js', '/sighting-edit.js'] }, 16 profile: { label: 'profile', styles: ['/profile-edit.css?v=20260802-guide-safety'], scripts: ['/guide-areas.js', '/profile-edit.js?v=20260802-guide-safety'] }, 17 share: { label: 'sharing', scripts: ['/share.js'] }, 18 guide: { label: 'guide', scripts: ['/guide-chat.js'] } 19 }; 20 21 function loadAsset(url, style) { 22 if (assets[url]) return assets[url]; 23 assets[url] = new Promise(function (resolve, reject) { 24 var el = document.createElement(style ? 'link' : 'script'); 25 if (style) { el.rel = 'stylesheet'; el.href = url; } 26 else { el.src = url; el.async = false; } 27 el.onload = function () { resolve(); }; 28 el.onerror = function () { el.remove(); reject(new Error('Could not load ' + url)); }; 29 document.head.appendChild(el); 30 }).catch(function (error) { 31 delete assets[url]; // a transient failure must not poison retries 32 throw error; 33 }); 34 return assets[url]; 35 } 36 37 function status(message) { 38 var el = document.getElementById('kagnFeatureStatus'); 39 if (!message) { if (el) el.remove(); return; } 40 if (!el) { 41 el = document.createElement('div'); 42 el.id = 'kagnFeatureStatus'; 43 el.setAttribute('role', 'status'); 44 el.style.cssText = 'position:fixed;bottom:90px;left:16px;right:16px;z-index:20000;padding:12px 16px;border-radius:12px;background:#1a3a2a;color:white;text-align:center;font:16px system-ui'; 45 document.body.appendChild(el); 46 } 47 el.textContent = message; 48 } 49 50 function ensure(name) { 51 if (features[name]) return features[name]; 52 var def = definitions[name]; 53 features[name] = Promise.all((def.styles || []).map(function (url) { return loadAsset(url, true); })) 54 .then(async function () { 55 for (var url of def.scripts) await loadAsset(url, false); 56 await domReady; // shared auth, storage and sync scripts are ready 57 // A script can fire 'load' after a runtime error. Never recurse 58 // into the entry point if its implementation did not publish. 59 var ready = name === 'editor' ? window.SightingEdit !== editor : 60 name === 'profile' ? window.ProfileEdit !== profile : 61 name === 'share' ? window.KagnShare !== share : window.KagnGuideChat !== guide; 62 if (!ready) throw new Error('Feature did not initialize'); 63 if (name === 'editor') window.SightingEdit.setCallbacks(editorCallbacks); 64 }).catch(function (error) { 65 delete features[name]; 66 throw error; 67 }); 68 return features[name]; 69 } 70 71 function call(name, run) { 72 pending++; 73 status('Loading ' + definitions[name].label + 'â¦'); 74 // End the loading notice BEFORE invoking a modal that returns a promise 75 // for user input (e.g. profile completion). Keep errors actionable. 76 return ensure(name).then(function () { 77 if (!--pending) status(''); 78 return run(); 79 }, function (error) { 80 pending--; 81 console.warn('[Home] Feature unavailable:', error.message); 82 status('Could not open ' + definitions[name].label + '. Please try again.'); 83 return false; 84 }); 85 } 86 87 var editor = window.SightingEdit = {
88 init: function (options) { Object.assign(editorCallbacks, options || {}); }, 89 setCallbacks: function (options) { Object.assign(editorCallbacks, options || {}); }, 90 open: function (options) { 91 if (!openingEditor) openingEditor = call('editor', function () { return window.SightingEdit.open(options); }) 92 .finally(function () { openingEditor = null; }); 93 return openingEditor; 94 }, 95 close: function () {} // there is no editor to close before its first open 96 }; 97 98 var profile = window.ProfileEdit = { 99 open: function () { return call('profile', function () { return window.ProfileEdit.open(); }); }, 100 isComplete: function () { return !!(window.KagnSync && KagnSync.isProfileComplete()); }, 101 promptIfIncomplete: function () { 102 if (window.ProfileEdit.isComplete()) return Promise.resolve(true); 103 return call('profile', function () { return window.ProfileEdit.promptIfIncomplete(); }); 104 } 105 }; 106 window.openProfileEditModal = function (options) { 107 return call('profile', function () { return window.openProfileEditModal(options); }); 108 }; 109 window.openDeleteAccountFlow = function () { 110 return call('profile', function () { return window.openDeleteAccountFlow(); }); 111 }; 112 113 var share = window.KagnShare = {}; 114 ['shareImageWithText', 'shareSighting', 'shareSpeciesSighting', 'createShareLink', 115 'generateSightingCard', 'showShareLoading', 'hideShareLoading', 'showSharePreview', 116 'showSharedSighting', 'closeSharedSighting', 'checkForSharedSighting', 'shareTrip', 117 'generateTripCard', 'showSharedTrip', 'closeSharedTrip', 'checkForSharedTrip', 118 'shareIdentification', 'generateIdentificationCard'].forEach(function (method) { 119 share[method] = function () { 120 var args = arguments; 121 return call('share', function () { return window.KagnShare[method].apply(window.KagnShare, args); }); 122 }; 123 }); 124 125 var guide = window.KagnGuideChat = { 126 open: function (seed) { return call('guide', function () { return window.KagnGuideChat.open(seed); }); }, 127 close: function () {}, 128 isEnabled: function () { return !!(window.KAGN_FLAGS && KAGN_FLAGS.guide_chat); } 129 }; 130 function revealGuide() { 131 var bar = document.getElementById('askGuideBar'); 132 if (bar && guide.isEnabled()) bar.hidden = false; 133 } 134 document.addEventListener('click', function (event) { 135 if (window.KagnGuideChat !== guide) return; // real module wires later clicks 136 var target = event.target.closest('#askGuideBar, .guide-chip'); 137 if (target) guide.open(target.classList.contains('guide-chip') ? target.getAttribute('data-q') || target.textContent : undefined); 138 }); 139 window.addEventListener('kagn-flags-loaded', revealGuide); 140 domReady.then(revealGuide); 141 142 // Command URLs still open their feature automatically. Defer until the 143 // regular scripts are ready so the auth and storage contracts stay intact. 144 function deepLinks() { 145 var params = new URLSearchParams(location.search); 146 if (window.KagnShare === share && (params.has('s') || params.has('t') || /^\/v\//.test(location.pathname))) { 147 call('share', function () {}); // share.js consumes the command on load 148 } 149 var pendingGuide = false; 150 try { pendingGuide = localStorage.getItem('kagn-pending-guide-setup') === '1'; } catch (_) {} 151 if (!features.profile && (params.get('edit') === 'guide' || params.get('edit') === 'profile' || pendingGuide)) { 152 call('profile', function () {}); // profile-edit.js owns intent/auth handling 153 } 154 } 155 domReady.then(deepLinks); 156 window.addEventListener('pageshow', function () { domReady.then(deepLinks); }); 157 window.addEventListener('focus', function () { domReady.then(deepLinks); }); 158})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.