PageSourceSearch

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

js pupwife.com collected 2026-09-30 15:04:24 UTC 9,031 bytes, 261 lines download raw bytes

1/**
2 * CitrusOS user-created folders — virtual filesystem lite.
3 */
4(function (global) {
5    'use strict';
6
7    var STORAGE_KEY = 'citrusos_user_folders_v1';
8
9    function readStore() {
10        try {
11            var raw = global.localStorage.getItem(STORAGE_KEY);
12            if (!raw) return { version: 1, folders: {}, rootOrder: [] };
13            var parsed = JSON.parse(raw);
14            if (!parsed || typeof parsed !== 'object') return { version: 1, folders: {}, rootOrder: [] };
15            if (!parsed.folders || typeof parsed.folders !== 'object') parsed.folders = {};
16            if (!Array.isArray(parsed.rootOrder)) parsed.rootOrder = [];
17            parsed.version = 1;
18            return parsed;
19        } catch (e) {
20            return { version: 1, folders: {}, rootOrder: [] };
21        }
22    }
23
24    function writeStore(data) {
25        try {
26            global.localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
27        } catch (e) { /* ignore */ }
28        emitChanged();
29    }
30
31    function emitChanged() {
32        try {
33            global.dispatchEvent(new CustomEvent('pupwife:citrus-user-folders-changed'));
34        } catch (e) { /* ignore */ }
35    }
36
37    function uid() {
38        return 'uf-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 6);
39    }
40
41    function containerIdFor(folderId) {
42        return 'user:' + folderId;
43    }
44
45    function parseContainerId(containerId) {
46        if (!containerId || containerId.indexOf('user:') !== 0) return null;
47        return containerId.slice(5);
48    }
49
50    function getFolder(folderId) {
51        var store = readStore();
52        return store.folders[folderId] || null;
53    }
54
55    function listRootFolders() {
56        var store = readStore();
57        var list = [];
58        for (var i = 0; i < store.rootOrder.length; i++) {
59            var f = store.folders[store.rootOrder[i]];
60            if (f) list.push(f);
61        }
62        return list;
63    }
64
65    function listChildFolders(parentId) {
66        var store = readStore();
67        var parent = parentId ? store.folders[parentId] : null;
68        var childIds = parent ? (parent.children || []) : store.rootOrder;
69        var list = [];
70        for (var i = 0; i < childIds.length; i++) {
71            var f = store.folders[childIds[i]];
72            if (f) list.push(f);
73        }
74        return list;
75    }
76
77    function listAnchoredInHub(hubId, pathSegments) {
78        var store = readStore();
79        var pathKey = (pathSegments && pathSegments.length) ? pathSegments.join('/') : '';
80        var list = [];
81        var keys = Object.keys(store.folders);
82        for (var i = 0; i < keys.length; i++) {
83            var f = store.folders[keys[i]];
84            if (!f || f.parent) continue;
85            if (f.anchorContainer === hubId && (f.anchorPathKey || '') === pathKey) {
86                list.push(f);
87            }
88        }
89        list.sort(function (a, b) { return (a.sortIndex || 0) - (b.sortIndex || 0); });
90        return list;
91    }
92
93    function createFolder(opts) {
94        opts = opts || {};
95        var store = readStore();
96        var id = uid();
97        var label = (opts.label || 'New folder').trim() || 'New folder';
98        var folder = {
99            id: id,
100            label: label,
101            icon: opts.icon || 'fa-solid fa-folder',
102            parent: opts.parent || null,
103            anchorContainer: opts.anchorContainer || null,
104            anchorPath: (opts.anchorPath || []).slice(),
105            anchorPathKey: pathKeyFrom(opts.anchorPath),
106            children: [],
107            sortIndex: typeof opts.sortIndex === 'number' ? opts.sortIndex : nextSortIndex(opts)
108        };
109        store.folders[id] = folder;
110
111        if (folder.parent) {
112            var parent = store.folders[folder.parent];
113            if (parent) {
114                if (!Array.isArray(parent.children)) parent.children = [];
115                parent.children.push(id);
116            }
117        } else if (folder.anchorContainer) {
118            /* anchored inside static hub */
119        } else {
120            store.rootOrder.push(id);
121        }
122
123        writeStore(store);
124        return folder;
125    }
126
127    function pathKeyFrom(path) {
128        return (path && path.length) ? path.join('/') : '';
129    }
130
131    function nextSortIndex(opts) {
132        if (opts.parent) {
133            var siblings = listChildFolders(opts.parent);
134            return siblings.length;
135        }
136        if (opts.anchorContainer) {
137            return listAnchoredInHub(opts.anchorContainer, opts.anchorPath).length;
138        }
139        return readStore().rootOrder.length;
140    }
141
142    function renameFolder(folderId, label) {
143        var store = readStore();
144        var folder = store.folders[folderId];
145        if (!folder) return null;
146        folder.label = (label || '').trim() || folder.label;
147        writeStore(store);
148        return folder;
149    }
150
151    function countContents(folderId) {
152        var layout = global.CitrusFolderLayout;
153        var childFolders = listChildFolders(folderId).length;
154        var items = layout && layout.listEntriesForLocation
155            ? layout.listEntriesForLocation(containerIdFor(folderId), []).length
156            : 0;
157        return childFolders + items;
158    }
159
160    function deleteFolder(folderId, force) {
161        var store = readStore();
162        var folder = store.folders[folderId];
163        if (!folder) return false;
164        if (!force && countContents(folderId) > 0) return false;
165
166        if (folder.parent) {
167            var parent = store.folders[folder.parent];
168            if (parent && parent.children) {
169                parent.children = parent.children.filter(function (id) { return id !== folderId; });
170            }
171        } else if (!folder.anchorContainer) {
172            store.rootOrder = store.rootOrder.filter(function (id) { return id !== folderId; });
173        }
174
175        delete store.folders[folderId];
176        writeStore(store);
177        return true;
178    }
179
180    function moveFolderNode(folderId, targetParentId, targetAnchorContainer, targetAnchorPath) {
181        var store = readStore();
182        var folder = store.folders[folderId];
183        if (!folder) return false;
184
185        if (targetParentId && isDescendant(targetParentId, folderId)) return false;
186
187        if (folder.parent) {
188            var oldParent = store.folders[folder.parent];
189            if (oldParent && oldParent.children) {
190                oldParent.children = oldParent.children.filter(function (id) { return id !== folderId; });
191            }
192        } else if (!folder.anchorContainer) {
193            store.rootOrder = store.rootOrder.filter(function (id) { return id !== folderId; });
194        }
195
196        folder.parent = targetParentId || null;
197        folder.anchorContainer = targetParentId ? null : (targetAnchorContainer || null);
198        folder.anchorPath = targetParentId ? [] : (targetAnchorPath || []).slice();
199        folder.anchorPathKey = targetParentId ? '' : pathKeyFrom(targetAnchorPath);
200
201        if (folder.parent) {
202            var newParent = store.folders[folder.parent];
203            if (newParent) {
204                if (!Array.isArray(newParent.children)) newParent.children = [];
205                if (newParent.children.indexOf(folderId) === -1) newParent.children.push(folderId);
206            }
207        } else if (!folder.anchorContainer) {
208            if (store.rootOrder.indexOf(folderId) === -1) store.rootOrder.push(folderId);
209        }
210
211        writeStore(store);
212        return true;
213    }
214
215    function isDescendant(candidateId, ancestorId) {
216        if (!candidateId || !ancestorId) return false;
217        if (candidateId === ancestorId) return true;
218        var current = getFolder(candidateId);
219        while (current && current.parent) {
220            if (current.parent === ancestorId) return true;
221            current = getFolder(current.parent);
222        }
223        return false;
224    }
225
226    function clearAll() {
227        writeStore({ version: 1, folders: {}, rootOrder: [] });
228    }
229
230    function folderToListingNode(folder) {
231        return {
232            id: folder.id,
233            label: folder.label,
234            icon: folder.icon || 'fa-solid fa-folder',
235            iconType: 'fa',
236            type: 'folder',
237            userFolder: true,
238            userFolderId: folder.id,
239            containerId: containerIdFor(folder.id)
240        };
241    }
242
243    global.CitrusUserFolders = {
244        STORAGE_KEY: STORAGE_KEY,
245        readStore: readStore,
246        containerIdFor: containerIdFor,
247        parseContainerId: parseContainerId,
248        getFolder: getFolder,
249        listRootFolders: listRootFolders,
250        listChildFolders: listChildFolders,
251        listAnchoredInHub: listAnchoredInHub,
252        createFolder: createFolder,
253        renameFolder: renameFolder,
254        deleteFolder: deleteFolder,
255        moveFolderNode: moveFolderNode,
256        countContents: countContents,
257        isDescendant: isDescendant,
258        clearAll: clearAll,
259        folderToListingNode: folderToListingNode
260    };
261})(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.