PageSourceSearch

https://pupwife.com/scripts/desktop/citrusDesktopShortcuts.js

js pupwife.com collected 2026-09-30 15:04:52 UTC 26,675 bytes, 718 lines download raw bytes

1/**
2 * CitrusOS desktop shortcuts — persisted shortcut list, factory defaults, resolve to apps.
3 */
4(function (global) {
5    'use strict';
6
7    var SESSION_KEY = 'citrusos_session_v3';
8    var GRID_COL = 96;
9    var GRID_ROW = 104;
10    var GRID_PAD_X = 16;
11    var GRID_PAD_Y = 16;
12
13    var PINNED_CORNER_KEYS = {
14        siteinfo: 'bottom-right'
15    };
16
17    function readSession() {
18        try {
19            var raw = global.localStorage.getItem(SESSION_KEY);
20            return raw ? JSON.parse(raw) : {};
21        } catch (e) {
22            return {};
23        }
24    }
25
26    function writeSession(session) {
27        try {
28            global.localStorage.setItem(SESSION_KEY, JSON.stringify(session));
29        } catch (e) { /* ignore */ }
30    }
31
32    function uid() {
33        return 'sc-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 7);
34    }
35
36    function snapToGrid(x, y) {
37        return {
38            x: GRID_PAD_X + Math.round((x - GRID_PAD_X) / GRID_COL) * GRID_COL,
39            y: GRID_PAD_Y + Math.round((y - GRID_PAD_Y) / GRID_ROW) * GRID_ROW
40        };
41    }
42
43    function getDesktopEl() {
44        return document.getElementById('citrusos-desktop');
45    }
46
47    function desktopBottomRight() {
48        var desktop = getDesktopEl();
49        var w = desktop ? (desktop.clientWidth || desktop.offsetWidth) : 0;
50        var h = desktop ? (desktop.clientHeight || desktop.offsetHeight) : 0;
51        if (!w || !h) {
52            return snapToGrid(GRID_PAD_X + GRID_COL * 8, GRID_PAD_Y + GRID_ROW * 4);
53        }
54        return snapToGrid(
55            Math.max(GRID_PAD_X, w - GRID_COL - GRID_PAD_X),
56            Math.max(GRID_PAD_Y, h - GRID_ROW - GRID_PAD_Y)
57        );
58    }
59
60    function targetKeyFromApp(app) {
61        if (!app) return '';
62        if (app.launchTier === 'note' && app.noteId) return 'note:' + app.noteId;
63        if (app.launchTier === 'userFolder' || (app.userFolderId && app.folderId && String(app.folderId).indexOf('user:') === 0)) {
64            var ufId = app.userFolderId || String(app.folderId).replace(/^user:/, '');
65            return 'user:' + ufId;
66        }
67        if (app.launchTier === 'folder' && app.folderPath && app.folderPath.length) {
68            return 'folder:' + (app.folderId || app.id) + ':' + app.folderPath.join('/');
69        }
70        if (app.launchTier === 'folder' && app.folderId) {
71            return app.folderId;
72        }
73        if (app.href && global.CitrusAppRegistry) {
74            var registry = global.CitrusAppRegistry;
75            var byHref = registry.getAppByHref ? registry.getAppByHref(app.href) : null;
76            if (byHref && byHref.id && byHref.id === app.id) return app.id;
77            if (app.id && registry.getApp && registry.getApp(app.id)) return app.id;
78            return 'href:' + (registry.getAppByHref ? normalizeHref(app.href) : app.href);
79        }
80        return app.id || '';
81    }
82
83    function normalizeHref(href) {
84        if (global.CitrusFolderRegistry && global.CitrusFolderRegistry.normalizeHref) {
85            return global.CitrusFolderRegistry.normalizeHref(href);
86        }
87        if (!href) return '';
88        var key = String(href).replace(/^\//, '').split('?')[0].split('#')[0];
89        if (!/\.html$/i.test(key) && key.indexOf('.') === -1) key += '.html';
90        return '/' + key;
91    }
92
93    function targetTypeFromKey(targetKey) {
94        if (!targetKey) return 'app';
95        if (targetKey.indexOf('note:') === 0) return 'note';
96        if (targetKey.indexOf('user:') === 0) return 'userFolder';
97        if (targetKey.indexOf('folder:') === 0) return 'folder';
98        if (targetKey.indexOf('href:') === 0) return 'app';
99        if (global.CitrusAppRegistry && global.CitrusAppRegistry.getApp) {
100            var app = global.CitrusAppRegistry.getApp(targetKey);
101            if (app) {
102                if (app.launchTier === 'folder') return 'folder';
103                if (app.launchTier === 'note') return 'note';
104                return 'app';
105            }
106        }
107        return 'app';
108    }
109
110    function factoryDefaults() {
111        var registry = global.CitrusAppRegistry;
112        if (!registry || !registry.getApps) return [];
113        var apps = registry.getApps();
114        var shortcuts = [];
115        var col = 0;
116        var row = 0;
117        for (var i = 0; i < apps.length; i++) {
118            var app = apps[i];
119            if (app.ownerOnly && registry.isSiteOwner && !registry.isSiteOwner()) continue;
120            var targetKey = targetKeyFromApp(app);
121            var pos;
122            if (PINNED_CORNER_KEYS[app.id] === 'bottom-right') {
123                pos = desktopBottomRight();
124            } else {
125                pos = snapToGrid(GRID_PAD_X + col * GRID_COL, GRID_PAD_Y + row * GRID_ROW);
126                row += 1;
127                if (row > 5) {
128                    row = 0;
129                    col += 1;
130                }
131            }
132            shortcuts.push({
133                id: uid(),
134                targetType: targetTypeFromKey(targetKey),
135                targetKey: targetKey,
136                label: null,
137                x: pos.x,
138                y: pos.y
139            });
140        }
141        return shortcuts;
142    }
143
144    function migrateFromLegacySession(session) {
145        session = session || readSession();
146        var registry = global.CitrusAppRegistry;
147        if (!registry) return factoryDefaults();
148
149        var apps = registry.getApps();
150        var iconMap = {};
151        var icons = Array.isArray(session.icons) ? session.icons : [];
152        for (var i = 0; i < icons.length; i++) {
153            if (icons[i] && icons[i].appId) {
154                iconMap[icons[i].appId] = { x: icons[i].x, y: icons[i].y };
155            }
156        }
157
158        var shortcuts = [];
159        for (var j = 0; j < apps.length; j++) {
160            var app = apps[j];
161            if (app.ownerOnly && registry.isSiteOwner && !registry.isSiteOwner()) continue;
162            var targetKey = targetKeyFromApp(app);
163            var pos = iconMap[app.id];
164            if (!pos) {
165                if (PINNED_CORNER_KEYS[app.id] === 'bottom-right') {
166                    pos = desktopBottomRight();
167                } else {
168                    var slot = nextFreeSlot(shortcuts);
169                    pos = slot;
170                }
171            }
172            shortcuts.push({
173                id: uid(),
174                targetType: targetTypeFromKey(targetKey),
175                targetKey: targetKey,
176                label: null,
177                x: pos.x,
178                y: pos.y
179            });
180        }
181        return shortcuts;
182    }
183
184    function getShortcuts(session) {
185        session = session || readSession();
186        if (Array.isArray(session.desktopShortcuts) && session.desktopShortcuts.length) {
187            return session.desktopShortcuts.slice();
188        }
189        return null;
190    }
191
192    function ensureShortcuts(session) {
193        session = session || readSession();
194        var list = getShortcuts(session);
195        if (!list) {
196            list = migrateFromLegacySession(session);
197            session.desktopShortcuts = list;
198            writeSession(session);
199        }
200        return session.desktopShortcuts;
201    }
202
203    function saveShortcuts(list) {
204        var session = readSession();
205        session.desktopShortcuts = list;
206        writeSession(session);
207    }
208
209    function nextFreeSlot(existing) {
210        existing = existing || ensureShortcuts();
211        var used = {};
212        for (var i = 0; i < existing.length; i++) {
213            var sc = existing[i];
214            var col = Math.round((sc.x - GRID_PAD_X) / GRID_COL);
215            var row = Math.round((sc.y - GRID_PAD_Y) / GRID_ROW);
216            if (col >= 0 && row >= 0) used[col + ':' + row] = true;
217        }
218        var col = 0;
219        var row = 0;
220        while (used[col + ':' + row]) {
221            row += 1;
222            if (row > 5) {
223                row = 0;
224                col += 1;
225            }
226        }
227        return snapToGrid(GRID_PAD_X + col * GRID_COL, GRID_PAD_Y + row * GRID_ROW);
228    }
229
230    function isGridSlotUsed(x, y, list, ignoreShortcutId) {
231        for (var i = 0; i < list.length; i++) {
232            if (ignoreShortcutId && list[i].id === ignoreShortcutId) continue;
233            if (list[i].x === x && list[i].y === y) return true;
234        }
235        return false;
236    }
237
238    function clampToDesktopBounds(x, y, desktopEl) {
239        desktopEl = desktopEl || getDesktopEl();
240        var maxX = GRID_PAD_X;
241        var maxY = GRID_PAD_Y;
242        if (desktopEl) {
243            maxX = Math.max(GRID_PAD_X, (desktopEl.clientWidth || 0) - GRID_COL - GRID_PAD_X);
244            maxY = Math.max(GRID_PAD_Y, (desktopEl.clientHeight || 0
244) - GRID_ROW - GRID_PAD_Y);
245        }
246        var snapped = snapToGrid(x, y);
247        return {
248            x: Math.max(GRID_PAD_X, Math.min(snapped.x, maxX)),
249            y: Math.max(GRID_PAD_Y, Math.min(snapped.y, maxY))
250        };
251    }
252
253    /** Snap to grid near a drop point; nudge to a free cell if occupied. */
254    function resolveDropPosition(preferredX, preferredY, opts) {
255        opts = opts || {};
256        var list = opts.list || ensureShortcuts();
257        var desktopEl = opts.desktopEl || getDesktopEl();
258        var centered = clampToDesktopBounds(
259            preferredX - GRID_COL / 2,
260            preferredY - GRID_ROW / 2,
261            desktopEl
262        );
263        if (!isGridSlotUsed(centered.x, centered.y, list, opts.ignoreShortcutId)) {
264            return centered;
265        }
266
267        var col = Math.round((centered.x - GRID_PAD_X) / GRID_COL);
268        var row = Math.round((centered.y - GRID_PAD_Y) / GRID_ROW);
269        var radius;
270        for (radius = 1; radius <= 24; radius++) {
271            for (var dc = -radius; dc <= radius; dc++) {
272                for (var dr = -radius; dr <= radius; dr++) {
273                    if (Math.abs(dc) !== radius && Math.abs(dr) !== radius) continue;
274                    var candidate = clampToDesktopBounds(
275                        GRID_PAD_X + (col + dc) * GRID_COL,
276                        GRID_PAD_Y + (row + dr) * GRID_ROW,
277                        desktopEl
278                    );
279                    if (!isGridSlotUsed(candidate.x, candidate.y, list, opts.ignoreShortcutId)) {
280                        return candidate;
281                    }
282                }
283            }
284        }
285        return nextFreeSlot(list);
286    }
287
288    function clientPointToDesktop(clientX, clientY, desktopEl) {
289        desktopEl = desktopEl || getDesktopEl();
290        if (!desktopEl) return null;
291        var rect = desktopEl.getBoundingClientRect();
292        var x = clientX - rect.left;
293        var y = clientY - rect.top;
294        if (global.CitrusBrowserZoom && typeof global.CitrusBrowserZoom.isCompensating === 'function' &&
295            global.CitrusBrowserZoom.isCompensating()) {
296            var scale = global.CitrusBrowserZoom.getScale();
297            x *= scale;
298            y *= scale;
299        }
300        return { x: x, y: y };
301    }
302
303    function resolveShortcut(sc) {
304        if (!sc) return null;
305        var registry = global.CitrusAppRegistry;
306        if (!registry) return null;
307        var key = sc.targetKey || '';
308
309        if (key.indexOf('note:') === 0) {
310            var noteId = key.slice(5);
311            return registry.getAppById
312                ? registry.getAppById('note:' + noteId)
313                : {
314                    id: 'note:' + noteId,
315                    label: 'Note',
316                    href: '/stickynotes.html',
317                    icon: 'fa-solid fa-file-lines',
318                    iconType: 'fa',
319                    launchTier: 'note',
320                    noteId: noteId,
321                    singleton: false
322                };
323        }
324
325        if (key.indexOf('user:') === 0) {
326            var ufId = key.slice(5);
327            var uf = global.CitrusUserFolders && global.CitrusUserFolders.getFolder
328                ? global.CitrusUserFolders.getFolder(ufId)
329                : null;
330            var ufLabel = uf ? uf.label : 'Folder';
331            return {
332                id: 'userfolder:' + ufId,
333                label: sc.label || ufLabel,
334                icon: (uf && uf.icon) || 'fa-solid fa-folder',
335                iconType: 'fa',
336                launchTier: 'folder',
337                folderId: 'user:' + ufId,
338                userFolderId: ufId,
339                singleton: true,
340                defaultSize: { w: 680, h: 500 },
341                resizable: false
342            };
343        }
344
345        if (key.indexOf('folder:') === 0) {
346            var parts = key.split(':');
347            var hubId = parts[1];
348            var path = parts.length > 2 ? parts.slice(2).join(':').split('/').filter(Boolean) : [];
349            var hubMeta = global.CitrusFolderRegistry && global.CitrusFolderRegistry.FOLDER_META
350                ? global.CitrusFolderRegistry.FOLDER_META[hubId]
351                : null;
352            var subLabel = path.length ? path[path.length - 1] : (hubMeta ? hubMeta.label : hubId);
353            return {
354                id: 'folder:' + hubId + (path.length ? ':' + path.join('/') : ''),
355                label: sc.label || subLabel,
356                href: '/' + hubId + '.html',
357                icon: (hubMeta && hubMeta.icon) || 'fa-solid fa-folder',
358                iconType: 'citrus-folder',
359                launchTier: 'folder',
360                folderId: hubId,
361                folderPath: path,
362                singleton: true,
363                defaultSize: { w: 680, h: 500 },
364                resizable: false
365            };
366        }
367
368        if (key.indexOf('href:') === 0) {
369            var href = normalizeHref(key.slice(5));
370            var leaf = registry.getAppByHref ? registry.getAppByHref(href) : null;
371            if (leaf) return applyLabelOverride(leaf, sc.label);
372        }
373
374        var app = registry.getAppById ? registry.getAppById(key) : registry.getApp(key);
375        if (app) return applyLabelOverride(app, sc.label);
376
377        app = registry.getAppByHref ? registry.getAppByHref('/' + key + '.html') : null;
378        if (app) return applyLabelOverride(app, sc.label);
379
380        return null;
381    }
382
383    function applyLabelOverride(app, label) {
384        if (!app || !label) return app;
385        return Object.assign({}, app, { label: label, shortcutLabel: label });
386    }
387
388    function getShortcutById(shortcutId) {
389        var list = ensureShortcuts();
390        for (var i = 0; i < list.length; i++) {
391            if (list[i].id === shortcutId) return list[i];
392        }
393        return null;
394    }
395
396    function addShortcut(targetKey, opts) {
397        opts = opts || {};
398        var list = ensureShortcuts().slice();
399        var pos = opts.x != null && opts.y != null
400            ? resolveDropPosition(opts.x, opts.y, { list: list, ignoreShortcutId: opts.ignoreShortcutId })
401            : nextFreeSlot(list);
402        var sc = {
403            id: uid(),
404            targetType: opts.targetType || targetTypeFromKey(targetKey),
405            targetKey: targetKey,
406            label: opts.label || null,
407            x: pos.x,
408            y: pos.y
409        };
410        list.push(sc);
411        saveShortcuts(list);
412        return sc;
413    }
414
415    function removeShortcut(shortcutId) {
416        var list = ensureShortcuts().slice();
417        var next = list.filter(function (sc) { return sc.id !== shortcutId; });
418        if (next.length === list.length) return false;
419        saveShortcuts(next);
420        return true;
421    }
422
423    function removeShortcutsByTargetKey(targetKey) {
424        var list = ensureShortcuts().slice();
425        var next = list.filter(function (sc) { return sc.targetKey !== targetKey; });
426        if (next.length === list.length) return false;
427        saveShortcuts(next);
428        return true;
429    }
430
431    function findFreeSlotNear(preferredX, preferredY, occupied, placed, desktopEl) {
432        var key = preferredX + ':' + preferredY;
433        if (!occupied[key] && !placed[key]) {
434            return { x: preferredX, y: preferredY };
435        }
436
437        var col = Math.round((preferredX - GRID_PAD_X) / GRID_COL);
438        var row = Math.round((preferredY - GRID_PAD_Y) / GRID_ROW);
439        var radius;
440        for (radius = 1; radius <= 24; radius++) {
441            for (var dc = -radius; dc <= radius; dc++) {
442                for (var dr = -radius; dr <= radius; dr++) {
443                    if (Math.abs(dc) !== radius && Math.abs(dr) !== radius) continue;
444                    var candidate = clampToDesktopBounds(
445                        GRID_PAD_X + (col + dc) * GRID_COL,
446                        GRID_PAD_Y + (row + dr) * GRID_ROW,
447                        desktopEl
448                    );
449                    var ck = candidate.x + ':' + candidate.y;
450                    if (!occupied[ck] && !placed[ck]) return candidate;
451                }
452            }
453        }
454
455        var scratch = [];
456        var keys = Object.keys(occupied);
457        for (var i = 0; i < keys.length; i++) {
458            var parts = keys[i].split(':');
459            scratch.push({ x: parseInt(parts[0], 10), y: parseInt(parts[1], 10) });
460        }
461        var placedKeys = Object.keys(placed);
462        for (var j = 0; j < placedKeys.length; j++) {
463            var pp = placedKeys[j].split(':');
464            scratch.push({ x: parseInt(pp[0], 10), y: parseInt(pp[1], 10) });
465        }
466        return nextFreeSlot(scratch);
467    }
468
469    /**
470     * Snap a dragged icon group to the grid without stacking on the same cell.
471     * @param {{ id: string, x: number, y: number }[]} members current pixel positions
472     */
473    function resolveGroupDropPositions(members, desktopEl) {
474        if (!members || !members.length) return [];
475        desktopEl = desktopEl || getDesktopEl();
476        var list = ensureShortcuts();
477        var movingIds = {};
478        for (var i = 0; i < members.length; i++) movingIds[members[i].id] = true;
479
480        var occupied = {};
481        for (var j = 0; j < list.length; j++) {
482            if (movingIds[list[j].id]) continue;
483            occupied[list[j].x + ':' + list[j].y] = true;
484        }
485
486        var sorted = members.map(function (member) {
487            var snapped = snapToGrid(member.x, member.y);
488            var clamped = clampToDesktopBounds(snapped.x, snapped.y, desktopEl);
489            return { id: member.id, x: clamped.x, y: clamped.y };
490        });
491        sorted.sort(function (a, b) {
492            if (a.y !== b.y) return a.y - b.y;
493            return a.x - b.x;
494        });
495
496        var placed = {};
497        var results = [];
498        for (var k = 0; k < sorted.length; k++) {
499            var item = sorted[k];
500            var slot = findFreeSlotNear(item.x, item.y, occupied, placed, desktopEl);
501            var slotKey = slot.x + ':' + slot.y;
502            placed[slotKey] = item.id;
503            results.push({ id: item.id, x: slot.x, y: slot.y });
504        }
505        return results;
506    }
507
508    function updateGroupPositions(updates) {
509        if (!updates || !updates.length) return;
510        var list = ensureShortcuts().slice();
511        var map = {};
512        for (var i = 0; i < updates.length; i++) map[updates[i].id] = updates[i];
513        for (var j = 0; j < list.length; j++) {
514            if (map[list[j].id]) {
515                list[j].x = map[list[j].id].x;
516                list[j].y = map[list[j].id].y;
517            }
518        }
519        saveShortcuts(list);
520    }
521
522    function clampGroupDelta(group, dx, dy, desktopEl) {
523        desktopEl = desktopEl || getDesktopEl();
524        var maxX = GRID_PAD_X;
525        var maxY = GRID_PAD_Y;
526        if (desktopEl) {
527            maxX = Math.max(GRID_PAD_X, (desktopEl.clientWidth || 0) - GRID_COL - GRID_PAD_X);
528            maxY = Math.max(GRID_PAD_Y, (desktopEl.clientHeight || 0
528) - GRID_ROW - GRID_PAD_Y);
529        }
530
531        var minDx = -Infinity;
532        var maxDx = Infinity;
533        var minDy = -Infinity;
534        var maxDy = Infinity;
535        for (var i = 0; i < group.length; i++) {
536            var member = group[i];
537            var origLeft = member.origLeft;
538            var origTop = member.origTop;
539            if (typeof origLeft !== 'number') origLeft = parseInt(member.el && member.el.style.left, 10) || 0;
540            if (typeof origTop !== 'number') origTop = parseInt(member.el && member.el.style.top, 10) || 0;
541            minDx = Math.max(minDx, GRID_PAD_X - origLeft);
542            maxDx = Math.min(maxDx, maxX - origLeft);
543            minDy = Math.max(minDy, GRID_PAD_Y - origTop);
544            maxDy = Math.min(maxDy, maxY - origTop);
545        }
546
547        return {
548            dx: Math.max(minDx, Math.min(maxDx, dx)),
549            dy: Math.max(minDy, Math.min(maxDy, dy))
550        };
551    }
552
553    function renameShortcut(shortcutId, label) {
554        var list = ensureShortcuts().slice();
555        for (var i = 0; i < list.length; i++) {
556            if (list[i].id === shortcutId) {
557                list[i].label = (label || '').trim() || null;
558                saveShortcuts(list);
559                return list[i];
560            }
561        }
562        return null;
563    }
564
565    function updateShortcutPosition(shortcutId, x, y) {
566        var list = ensureShortcuts().slice();
567        for (var i = 0; i < list.length; i++) {
568            if (list[i].id === shortcutId) {
569                var snapped = snapToGrid(x, y);
570                list[i].x = snapped.x;
571                list[i].y = snapped.y;
572                saveShortcuts(list);
573                return list[i];
574            }
575        }
576        return null;
577    }
578
579    function syncPositionsFromDom() {
580        var desktop = getDesktopEl();
581        if (!desktop) return;
582        var icons = desktop.querySelectorAll('.citrus-desktop-icon');
583        var list = ensureShortcuts().slice();
584        var map = {};
585        for (var i = 0; i < list.length; i++) map[list[i].id] = list[i];
586        for (var j = 0; j < icons.length; j++) {
587            var id = icons[j].getAttribute('data-shortcut-id');
588            if (!id || !map[id]) continue;
589            map[id].x = parseInt(icons[j].style.left, 10) || map[id].x;
590            map[id].y = parseInt(icons[j].style.top, 10) || map[id].y;
591        }
592        saveShortcuts(list);
593    }
594
595    function resetToFactory() {
596        var list = factoryDefaults();
597        saveShortcuts(list);
598        return list;
599    }
600
601    function resetLayoutOnly() {
602        var list = ensureShortcuts().slice();
603        var sorted = list.slice().sort(function (a, b) {
604            if (a.y !== b.y) return a.y - b.y;
605            return a.x - b.x;
606        });
607        var col = 0;
608        var row = 0;
609        for (var i = 0; i < sorted.length; i++) {
610            var snapped = snapToGrid(GRID_PAD_X + col * GRID_COL, GRID_PAD_Y + row * GRID_ROW);
611            sorted[i].x = snapped.x;
612            sorted[i].y = snapped.y;
613            row += 1;
614            if (row > 5) {
615                row = 0;
616                col += 1;
617            }
618        }
619        saveShortcuts(sorted);
620        return sorted;
621    }
622
623    function removeUserFolderShortcuts() {
624        var list = ensureShortcuts().slice();
625        var next = list.filter(function (sc) {
626            return !(sc.targetKey && sc.targetKey.indexOf('user:') === 0);
627        });
628        saveShortcuts(next);
629        return next;
630    }
631
632    function isShortcutAllowed(sc) {
633        var app = resolveShortcut(sc);
634        if (!app) return false;
635        if (app.ownerOnly && global.CitrusAppRegistry && global.CitrusAppRegistry.isSiteOwner &&
636            !global.CitrusAppRegistry.isSiteOwner()) {
637            return false;
638        }
639        return true;
640    }
641
642    function listVisibleShortcuts() {
643        return ensureShortcuts().filter(isShortcutAllowed);
644    }
645
646    function layoutNormalizeKey(key) {
647        if (global.CitrusFolderLayout && global.CitrusFolderLayout.normalizeItemKey) {
648            return global.CitrusFolderLayout.normalizeItemKey(key);
649        }
650        return normalizeHref(key).replace(/^\//, '');
651    }
652
653    function resolveLayoutItemKey(targetKey) {
654        if (!targetKey) return '';
655        if (targetKey.indexOf('note:') === 0) return targetKey;
656        if (targetKey.indexOf('href:') === 0) {
657            return layoutNormalizeKey(targetKey.slice(5));
658        }
659        if (global.CitrusAppRegistry) {
660            var app = global.CitrusAppRegistry.getAppById
661                ? global.CitrusAppRegistry.getAppById(targetKey)
662                : (global.CitrusAppRegistry.getApp ? global.CitrusAppRegistry.getApp(targetKey) : null);
663            if (app && app.href) return layoutNormalizeKey(app.href);
664        }
665        return layoutNormalizeKey(targetKey);
666    }
667
668    function targetKeyFromFolderEntry(entry) {
669        if (!entry) return '';
670        if (entry.type === 'note') return 'note:' + (entry.noteId || entry.id);
671        if (entry.userFolder || entry.userFolderId) return 'user:' + entry.userFolderId;
672        if (entry.type === 'folder' && !entry.userFolder) {
673            return entry.id;
674        }
675        if (entry.href) return 'href:' + normalizeHref(entry.href).replace(/^\//, '');
676        return entry.id || '';
677    }
678
679    global.CitrusDesktopShortcuts = {
680        SESSION_KEY: SESSION_KEY,
681        GRID_COL: GRID_COL,
682        GRID_ROW: GRID_ROW,
683        GRID_PAD_X: GRID_PAD_X,
684        GRID_PAD_Y: GRID_PAD_Y,
685        readSession: readSession,
686        writeSession: writeSession,
687        snapToGrid: snapToGrid,
688        targetKeyFromApp: targetKeyFromApp,
689        resolveLayoutItemKey: resolveLayoutItemKey,
690        targetKeyFromFolderEntry: targetKeyFromFolderEntry,
691        targetTypeFromKey: targetTypeFromKey,
692        factoryDefaults: factoryDefaults,
693        migrateFromLegacySession: migrateFromLegacySession,
694        ensureShortcuts: ensureShortcuts,
695        getShortcuts: getShortcuts,
696        saveShortcuts: saveShortcuts,
697        resolveShortcut: resolveShortcut,
698        getShortcutById: getShortcutById,
699        addShortcut: addShortcut,
700        removeShortcut: removeShortcut,
701        removeShortcutsByTargetKey: removeShortcutsByTargetKey,
702        renameShortcut: renameShortcut,
703        updateShortcutPosition: updateShortcutPosition,
704        syncPositionsFromDom: syncPositionsFromDom,
705        nextFreeSlot: nextFreeSlot,
706        resolveDropPosition: resolveDropPosition,
707        resolveGroupDropPositions: resolveGroupDropPositions,
708        updateGroupPositions: updateGroupPositions,
709        clampGroupDelta: clampGroupDelta,
710        clampToDesktopBounds: clampToDesktopBounds,
711        clientPointToDesktop: clientPointToDesktop,
712        resetToFactory: resetToFactory,
713        resetLayoutOnly: resetLayoutOnly,
714        removeUserFolderShortcuts: removeUserFolderShortcuts,
715        listVisibleShortcuts: listVisibleShortcuts,
716        isShortcutAllowed: isShortcutAllowed
717    };
718})(typeof window !== 'undefined' ? window : this);

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.