PageSourceSearch

https://kagn.app/hub-features.js

js kagn.app collected 2026-09-25 13:38:23 UTC 8,049 bytes, 158 lines download raw bytes

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.