PageSourceSearch

https://pupwife.com/scripts/effects/settingsExportImport.js

js pupwife.com collected 2026-09-30 15:06:29 UTC 16,598 bytes, 372 lines download raw bytes

1/**
2 * Selective export / import for pupwife site settings.
3 */
4(function (global) {
5    'use strict';
6
7    var EXPORT_VERSION = 1;
8
9    var CATEGORIES = [
10        {
11            id: 'appearance',
12            label: 'Theme & appearance',
13            icon: 'fa-palette',
14            keys: ['darkMode', 'palette']
15        },
16        {
17            id: 'cursorTrails',
18            label: 'Cursor trails',
19            icon: 'fa-sparkles',
20            keys: ['cursorTrail', 'trailAmount', 'trailColors', 'trailFavorites']
21        },
22        {
23            id: 'customCursor',
24            label: 'Custom cursor',
25            icon: 'fa-computer-mouse',
26            keys: ['customCursor', 'catPawArm']
27        },
28        {
29            id: 'follower',
30            label: 'Cursor buddy / follower',
31            icon: 'fa-arrow-pointer',
32            keys: ['follower', 'woimString', 'jellypuff']
33        },
34        {
35            id: 'effects',
36            label: 'Performance & effects',
37            icon: 'fa-wand-magic-sparkles',
38            keys: ['performanceMode', 'clickSparks', 'seasonalOverlay', 'weatherOverlay', 'uiSounds', 'homeLanding']
39        },
40        {
41            id: 'citrusDesktop',
42            label: 'CitrusOS desktop',
43            icon: 'fa-desktop',
44            keys: [
45                'citrusUiScale',
46                'desktopIconsVisible',
47                'desktopIconLabelsVisible',
48                'desktopIconScale',
49                'citrusWallpaperDim',
50                'citrusWallpaperSaturation',
51                'citrusWallpaperBrightness',
52                'citrusWallpaperBlur',
53                'taskbarAutoHide',
54                'taskbarClock24Hour',
55                'taskbarClockShowSeconds',
56                'taskbarShowDate',
57                'showDesktopButton',
58                'windowTitlebarDblClickMaximize'
59            ]
60        }
61    ];
62
63    function escapeHtml(text) {
64        if (text == null) return '';
65        var div = global.document.createElement('div');
66        div.textContent = String(text);
67        return div.innerHTML;
68    }
69
70    function getCategoryById(id) {
71        for (var i = 0; i < CATEGORIES.length; i++) {
72            if (CATEGORIES[i].id === id) return CATEGORIES[i];
73        }
74        return null;
75    }
76
77    function pickSettings(all, categoryIds) {
78        var out = {};
79        var ids = categoryIds || [];
80        ids.forEach(function (catId) {
81            var cat = getCategoryById(catId);
82            if (!cat) return;
83            cat.keys.forEach(function (key) {
84                if (all[key] !== undefined) out[key] = all[key];
85            });
86        });
87        return out;
88    }
89
90    function buildExportPayload(allSettings, categoryIds) {
91        return {
92            version: EXPORT_VERSION,
93            app: 'pupwife-settings',
94            exportedAt: new Date().toISOString(),
95            categories: categoryIds.slice(),
96            settings: pickSettings(allSettings, categoryIds)
97        };
98    }
99
100    function parseImportPayload(raw) {
101        var data = typeof raw === 'string' ? JSON.parse(raw) : raw;
102        if (!data || typeof data !== 'object') throw new Error('Invalid settings file.');
103        if (data.app && data.app !== 'pupwife-settings') throw new Error('This file is not from pupwife settings.');
104        var settings = data.settings;
105        if (!settings || typeof settings !== 'object') throw new Error('No settings found in file.');
106        var cats = Array.isArray(data.categories) ? data.categories : Object.keys(settings);
107        return { settings: settings, categories: cats, exportedAt: data.exportedAt || null };
108    }
109
110    function detectCategoriesInSettings(settings) {
111        var found = [];
112        CATEGORIES.forEach(function (cat) {
113            var has = cat.keys.some(function (key) {
114                return settings[key] !== undefined;
115            });
116            if (has) found.push(cat.id);
117        });
118        return found;
119    }
120
121    function createOverlay(className) {
122        var el = global.document.createElement('div');
123        el.className = className;
124        el.setAttribute('role', 'dialog');
125        el.setAttribute('aria-modal', 'true');
126        return el;
127    }
128
129    function buildDialogCheckRow(opts) {
130        var checked = !!opts.checked;
131        var dataKey = opts.dataKey;
132        var dataValue = opts.value;
133        return '<label class="settings-dialog__check">' +
134            '<input type="checkbox" class="settings-dialog__check-input" data-' + dataKey + '="' + dataValue + '"' + (checked ? ' checked' : '') + '>
134' +
135            '<span class="settings-dialog__tick" aria-hidden="true"><i class="fa-solid fa-check fa-width-auto"></i></span>' +
136            '<span class="settings-dialog__check-icon"><i class="fa-solid ' + opts.icon + ' fa-width-auto" aria-hidden="true"></i></span>' +
137            '<span class="settings-dialog__check-label">' + escapeHtml(opts.label) + '</span>' +
138            '</label>';
139    }
140
141    function buildCategoryChecklist(selectedIds) {
142        var html = '<div class="settings-dialog__checks">';
143        CATEGORIES.forEach(function (cat) {
144            var checked = !selectedIds.length || selectedIds.indexOf(cat.id) !== -1;
145            html += buildDialogCheckRow({
146                dataKey: 'cat',
147                value: cat.id,
148                checked: checked,
149                icon: cat.icon,
150                label: cat.label
151            });
152        });
153        html += '</div>';
154        return html;
155    }
156
157    function readCheckedCategories(root) {
158        var ids = [];
159        root.querySelectorAll('.settings-dialog__check-input:checked').forEach(function (input) {
160            if (input.dataset.cat) ids.push(input.dataset.cat);
161        });
162        return ids;
163    }
164
165    function closeDialog(overlay) {
166        if (overlay && overlay.parentNode) overlay.parentNode.removeChild(overlay);
167    }
168
169    function openExportDialog(allSettings, onExport) {
170        var overlay = createOverlay('settings-dialog-backdrop settings-dialog-backdrop--open');
171        overlay.innerHTML =
172            '<div class="settings-dialog">' +
173            '<button type="button" class="settings-dialog__close" aria-label="Close"><i class="fa-solid fa-xmark fa-width-auto" aria-hidden="true"></i></button>' +
174            '<h2 class="settings-dialog__title"><i class="fa-solid fa-file-export fa-width-auto" aria-hidden="true"></i> Export settings</h2>' +
175            '<p class="settings-dialog__hint">Choose what to include in your shareable settings bundle.</p>' +
176            buildCategoryChecklist(CATEGORIES.map(function (c) { return c.id; })) +
177            '<div class="settings-dialog__actions">' +
178            '<button type="button" class="settings-btn settings-dialog__btn" data-action="copy"><i class="fa-solid fa-copy fa-width-auto" aria-hidden="true"></i> Copy JSON</button>' +
179            '<button type="button" class="settings-btn settings-dialog__btn" data-action="download"><i class="fa-solid fa-download fa-width-auto" aria-hidden="true"></i> Download</button>' +
180            '</div>' +
181            '<textarea class="settings-dialog__preview" id="settings-export-preview" readonly rows="6" aria-label="Export preview"></textarea>' +
182            '</div>';
183
184        global.document.body.appendChild(overlay);
185
186        function refreshPreview() {
187            var cats = readCheckedCategories(overlay);
188            var payload = buildExportPayload(allSettings, cats);
189            var preview = overlay.querySelector('#settings-export-preview');
190            if (preview) preview.value = JSON.stringify(payload, null, 2);
191        }
192
193        refreshPreview();
194        overlay.querySelectorAll('.settings-dialog__check-input').forEach(function (input) {
195            input.addEventListener('change', refreshPreview);
196        });
197
198        overlay.querySelector('[data-action="copy"]').addEventListener('click', function () {
199            var text = overlay.querySelector('#settings-export-preview').value;
200            if (navigator.clipboard && navigator.clipboard.writeText) {
201                navigator.clipboard.writeText(text).then(function () {
202                    if (typeof onExport === 'function') onExport('copied');
203                });
204            } else {
205                overlay.querySelector('#settings-export-preview').select();
206                global.document.execCommand('copy');
207            }
208        });
209
210        overlay.querySelector('[data-action="download"]').addEventListener('click', function () {
211            var text = overlay.querySelector('#settings-export-preview').value;
212            var blob = new Blob([text], { type: 'application/json' });
213            var url = URL.createObjectURL(blob);
214            var a = global.document.createElement('a');
215            a.href = url;
216            a.download = 'pupwife-settings.json';
217            a.click();
218            URL.revokeObjectURL(url);
219        });
220
221        overlay.querySelector('.settings-dialog__close').addEventListener('click', function () {
222            closeDialog(overlay);
223        });
224        overlay.addEventListener('click', function (e) {
225            if (e.target === overlay) closeDialog(overlay);
226        });
227    }
228
229    function openImportDialog(onImport) {
230        var overlay = createOverlay('settings-dialog-backdrop settings-dialog-backdrop--open');
231        overlay.innerHTML =
232            '<div class="settings-dialog">' +
233            '<button type="button" class="settings-dialog__close" aria-label="Close"><i class="fa-solid fa-xmark fa-width-auto" aria-hidden="true"></i></button>' +
234            '<h2 class="settings-dialog__title"><i class="fa-solid fa-file-import fa-width-auto" aria-hidden="true"></i> Import settings</h2>' +
235            '<p class="settings-dialog__hint">Paste a settings JSON bundle or pick a file. Then choose what to apply.</p>' +
236            '<textarea class="settings-dialog__preview" id="settings-import-input" rows="5" placeholder="Paste pupwife settings JSON here…" aria-label="Import JSON"></textarea>' +
237            '<label class="settings-dialog__file"><i class="fa-solid fa-folder-open fa-width-auto" aria-hidden="true"></i> Choose file' +
238            '<input type="file" accept="application/json,.json" id="settings-import-file" hidden></label>' +
239            '<div id="settings-import-categories" hidden>' +
240            '<p class="settings-dialog__sub">Found in file — select categories to apply:</p>' +
241            buildCategoryChecklist([]) +
242            '</div>' +
243            '<div class="settings-dialog__actions">' +
244            '<button type="button" class="settings-btn settings-dialog__btn settings-btn--danger" data-action="apply" disabled><i class="fa-solid fa-check fa-width-auto" aria-hidden="true"></i> Apply selected</button>' +
245            '</div>' +
246            '<p class="settings-dialog__warn" id="settings-import-error" hidden></p>' +
247            '</div>';
248
249        global.document.body.appendChild(overlay);
250
251        var parsedPayload = null;
252        var errEl = overlay.querySelector('#settings-import-error');
253        var catWrap = overlay.querySelector('#settings-import-categories');
254        var applyBtn = overlay.querySelector('[data-action="apply"]');
255
256        function showError(msg) {
257            if (!errEl) return;
258            errEl.textContent = msg || '';
259            errEl.hidden = !msg;
260        }
261
262        function loadPayload(raw) {
263            try {
264                parsedPayload = parseImportPayload(raw);
265                showError('');
266                var detected = detectCategoriesInSettings(parsedPayload.settings);
267                catWrap.hidden = false;
268                catWrap.querySelectorAll('.settings-dialog__check-input').forEach(function (input) {
269                    input.checked = detected.indexOf(input.dataset.cat) !== -1;
270                });
271                applyBtn.disabled = !detected.length;
272            } catch (e) {
273                parsedPayload = null;
274                catWrap.hidden = true;
275                applyBtn.disabled = true;
276                showError(e.message || 'Could not read settings file.');
277            }
278        }
279
280        overlay.querySelector('#settings-import-input').addEventListener('input', function (e) {
281            if (!e.target.value.trim()) {
282                parsedPayload = null;
283                catWrap.hidden = true;
284                applyBtn.disabled = true;
285                showError('');
286                return;
287            }
288            loadPayload(e.target.value);
289        });
290
291        overlay.querySelector('#settings-import-file').addEventListener('change', function (e) {
292            var file = e.target.files && e.target.files[0];
293            if (!file) return;
294            var reader = new FileReader();
295            reader.onload = function () {
296                overlay.querySelector('#settings-import-input').value = reader.result;
297                loadPayload(reader.result);
298            };
299            reader.readAsText(file);
300        });
301
302        applyBtn.addEventListener('click', function () {
303            if (!parsedPayload) return;
304            var cats = readCheckedCategories(overlay);
305            if (!cats.length) {
306                showError('Pick at least one category to import.');
307                return;
308            }
309            var merged = pickSettings(parsedPayload.settings, cats);
310            if (typeof onImport === 'function') {
311                onImport(merged, cats);
312            }
313            closeDialog(overlay);
314        });
315
316        overlay.querySelector('.settings-dialog__close').addEventListener('click', function () {
317            closeDialog(overlay);
318        });
319        overlay.addEventListener('click', function (e) {
320            if (e.target === overlay) closeDialog(overlay);
321        });
322    }
323
324    function openRandomizeDialog(onConfirm) {
325        var overlay = createOverlay('settings-dialog-backdrop settings-dialog-backdrop--open');
326        overlay.innerHTML =
327            '<div class="settings-dialog">' +
328            '<button type="button" class="settings-dialog__close" aria-label="Close"><i class="fa-solid fa-xmark fa-width-auto" aria-hidden="true"></i></button>' +
329            '<h2 class="settings-dialog__title"><i class="fa-solid fa-shuffle fa-width-auto" aria-hidden="true"></i> Randomize cursor stuff</h2>' +
330            '<p class="settings-dialog__hint">Pick what gets rerolled. Your theme and account stay untouched.</p>' +
331            '<div class="settings-dialog__checks">' +
332            buildDialogCheckRow({ dataKey: 'rand', value: 'trail', checked: true, icon: 'fa-sparkles', label: 'Cursor trail type' }) +
333            buildDialogCheckRow({ dataKey: 'rand', value: 'trailAmount', checked: true, icon: 'fa-sliders', label: 'Trail amount' }) +
334            buildDialogCheckRow({ dataKey: 'rand', value: 'trailColors', checked: true, icon: 'fa-palette', label: 'Trail colors' }) +
335            buildDialogCheckRow({ dataKey: 'rand', value: 'customCursor', checked: false, icon: 'fa-computer-mouse', label: 'Custom cursor' }) +
336            buildDialogCheckRow({ dataKey: 'rand', value: 'follower', checked: true, icon: 'fa-cat', label: 'Cursor buddy' }) +
337            '</div>' +
338            '<div class="settings-dialog__actions">' +
339            '<button type="button" class="settings-btn settings-dialog__btn" data-action="go"><i class="fa-solid fa-dice fa-width-auto" aria-hidden="true"></i> Randomize!</button>' +
340            '</div>' +
341            '</div>';
342
343        global.document.body.appendChild(overlay);
344
345        overlay.querySelector('[data-action="go"]').addEventListener('click', function () {
346            var picks = {};
347            overlay.querySelectorAll('.settings-dialog__check-input[data-rand]').forEach(function (input) {
348                picks[input.dataset.rand] = input.checked;
349            });
350            if (!Object.keys(picks).some(function (k) { return picks[k]; })) return;
351            closeDialog(overlay);
352            if (typeof onConfirm === 'function') onConfirm(picks);
353        });
354
355        overlay.querySelector('.settings-dialog__close').addEventListener('click', function () {
356            closeDialog(overlay);
357        });
358        overlay.addEventListener('click', function (e) {
359            if (e.target === overlay) closeDialog(overlay);
360        });
361    }
362
363    global.PupwifeSettingsExportImport = {
364        CATEGORIES: CATEGORIES,
365        buildExportPayload: buildExportPayload,
366        parseImportPayload: parseImportPayload,
367        pickSettings: pickSettings,
368        openExportDialog: openExportDialog,
369        openImportDialog: openImportDialog,
370        openRandomizeDialog: openRandomizeDialog
371    };
372})(window);

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.