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.