1// ==================================== 2// PERSONALIZED SETTINGS MENU 3// Bottom-left floating settings panel 4// ==================================== 5 6(function() { 7 'use strict'; 8 9 function emitDevLog() { 10 if (typeof window !== 'undefined' && typeof window.devLog === 'function') { 11 window.devLog.apply(window, arguments); 12 } 13 } 14 function emitDevWarn() { 15 if (typeof window !== 'undefined' && typeof window.devWarn === 'function') { 16 window.devWarn.apply(window, arguments); 17 } 18 } 19 20 function isCitrusOsPage() { 21 return !!(document.body && document.body.classList.contains('page-citrusos')); 22 } 23 24 function normalizeCitrusUiScale(value) { 25 var parsed = parseInt(value, 10); 26 if (CITRUS_UI_SCALE_OPTIONS.indexOf(parsed) === -1) { 27 return DEFAULT_SETTINGS.citrusUiScale; 28 } 29 return parsed; 30 } 31 32 function applyCitrusUiScale(scale) { 33 var normalized = normalizeCitrusUiScale(scale != null ? scale : currentSettings.citrusUiScale); 34 var factor = normalized / 100; 35 document.documentElement.style.setProperty('--citrusos-ui-scale', String(factor)); 36 if (isCitrusOsPage()) { 37 document.body.style.setProperty('--citrusos-ui-scale', String(factor)); 38 } 39 } 40 41 function parseDarkMode(value, fallback) { 42 if (window.SiteThemeBoot && typeof window.SiteThemeBoot.parseDarkMode === 'function') { 43 return window.SiteThemeBoot.parseDarkMode(value, fallback); 44 } 45 if (typeof value === 'boolean') return value; 46 if (value === 'true' || value === 1) return true; 47 if (value === 'false' || value === 0) return false; 48 return fallback; 49 } 50 51 function readSiteDarkMode() { 52 if (window.SiteThemeToggle && typeof window.SiteThemeToggle.readDarkModeSetting === 'function') { 53 return window.SiteThemeToggle.readDarkModeSetting(); 54 } 55 return parseDarkMode(currentSettings.darkMode, DEFAULT_SETTINGS.darkMode); 56 } 57 58 // Default settings 59 const DEFAULT_SETTINGS = { 60 darkMode: true, // Light/Dark theme toggle (default dark for new visitors) 61 palette: 'default', // Sitewide color palette (synced with chatroom) 62 follower: 'none', // 'none', 'marsey', 'skeletile', 'orca', 'dragon', or 'fish2D' 63 performanceMode: false, 64 clickSparks: true, 65 cursorTrail: 'none', // 'none', 'sparkles', 'fairy', 'rainbow', 'bubbles', 'hearts', 'stars', 'fire', 'snow', 'leaves' 66 trailAmount: 2, // 0=Minimum ⦠4=Extreme (Normal default) 67 customCursor: 'none', // 'none', 'asriel', 'axolotl', Rough.js, or Two.js cursors 68 seasonalOverlay: false, // Seasonal effects (Halloween, Christmas, Valentine's, etc.) 69 background: 'caustics', // Ambient stack preset â see siteBackgrounds.js 70 backgroundTuning: null, // Per-background shader tuning â merged on load 71 weatherOverlay: false, // Weather overlay toggle 72 homeLanding: true, // Homepage fluid landing animation (index full load) 73 uiSounds: false, // Micro UI sounds (off by default) 74 citrusUiScale: 100, // CitrusOS desktop UI scale: 100 | 125 | 150 75 musicPreload: 'off', // CitrusOS boot: off | metadata | full 76 desktopIconsVisible: true, 77 desktopIconLabelsVisible: true, 78 desktopIconScale: 100, // 75 | 100 | 125 | 150 79 citrusWallpaperDim: 0, // 0â50 desktop dim overlay 80 citrusWallpaperSaturation: 100, // 50â150 81 citrusWallpaperBrightness: 100, // 50â150 82 citrusWallpaperBlur: 0, // 0â12 px on desktop wallpaper layers 83 taskbarAutoHide: false, 84 taskbarClock24Hour: false, 85 taskbarClockShowSeconds: false, 86 taskbarShowDate: true, 87 showDesktopButton: true, 88 windowTitlebarDblClickMaximize: true, 89 citrusSettingsTab: 'appearance', // Last CitrusOS settings sidebar tab 90 trailColors: null, // { primary, secondary, tertiary } â merged on load 91 trailFavorites: [] // Favorite cursor trail ids 92 }; 93 94 const CITRUS_UI_SCALE_OPTIONS = [100, 125, 150];
95 const DESKTOP_ICON_SCALE_OPTIONS = [75, 100, 125, 150]; 96 const MUSIC_PRELOAD_OPTIONS = ['off', 'metadata', 'full']; 97 98 function normalizeMusicPreload(value) { 99 if (MUSIC_PRELOAD_OPTIONS.indexOf(value) !== -1) return value; 100 return DEFAULT_SETTINGS.musicPreload; 101 } 102 103 function normalizeDesktopIconScale(value) { 104 var parsed = parseInt(value, 10); 105 if (DESKTOP_ICON_SCALE_OPTIONS.indexOf(parsed) === -1) { 106 return DEFAULT_SETTINGS.desktopIconScale; 107 } 108 return parsed; 109 } 110 111 function clampSettingNumber(value, min, max, fallback) { 112 var n = parseInt(value, 10); 113 if (Number.isNaN(n)) return fallback; 114 return Math.max(min, Math.min(max, n)); 115 } 116 117 const DEFAULT_WOIM_STRING = { 118 ropePoints: 32, 119 bodyPoints: 32, 120 size: 100, 121 ropeLength: 100, 122 ropeThickness: 100, 123 anchorShow: true, 124 anchorSize: 100, 125 anchorThickness: 100, 126 woim: { colorMode: 'solid', solidColor: '#ff4da6', rgbSpeed: 1, pridePreset: 'rainbow' }, 127 rope: { colorMode: 'solid', solidColor: '#b8b8c8', rgbSpeed: 1, pridePreset: 'rainbow' } 128 }; 129 130 const DEFAULT_JELLYPUFF = { 131 size: 100, 132 pulseRate: 100, 133 tentacleDensity: 100, 134 glowStrength: 100, 135 bell: { colorMode: 'solid', solidColor: '#0a1828', rgbSpeed: 1, pridePreset: 'rainbow' }, 136 tentacles: { colorMode: 'solid', solidColor: '#2a5878', rgbSpeed: 1, pridePreset: 'rainbow' } 137 }; 138 139 let woimColorPanels = { rope: null, woim: null }; 140 let jellyColorPanels = { bell: null, tentacles: null }; 141 142 const DEFAULT_CAT_PAW_ARM = { 143 anchorPreset: 'bottom-center', 144 originX: 50, 145 originY: 108, 146 armThickness: 100, 147 armTaper: 35, 148 armBend: 0, 149 armMargin: 100, 150 followLag: 42, 151 clickSquish: true, 152 showBeans: true, 153 pawPreset: 'classic-white', 154 pawSpecies: 'cat', 155 pawSize: 100, 156 padSize: 100, 157 printsEnabled: true, 158 printSize: 100, 159 printFade: 100, 160 printMax: 16, 161 outlineShow: true, 162 outlineThickness: 100, 163 arm: { colorMode: 'solid', solidColor: '#ffffff', rgbSpeed: 1, pridePreset: 'rainbow' }, 164 pawFur: { colorMode: 'solid', solidColor: '#ffffff', rgbSpeed: 1, pridePreset: 'rainbow' }, 165 pawPads: { colorMode: 'solid', solidColor: '#f2a7b8', rgbSpeed: 1, pridePreset: 'rainbow' }, 166 outline: { colorMode: 'solid', solidColor: '#8b6f5c', rgbSpeed: 1, pridePreset: 'rainbow' }, 167 prints: { colorMode: 'solid', solidColor: '#e89ab0', rgbSpeed: 1, pridePreset: 'rainbow' } 168 }; 169 170 let catPawColorPanels = { arm: null, pawFur: null, pawPads: null, outline: null, prints: null, spriteFur: null }; 171 let catPawSpeciesSelect = null; 172 let catPawPresetSelect = null; 173 let catPawAnchorSelect = null; 174 175 const CAT_PAW_ANCHOR_OPTIONS = [ 176 { value: 'bottom-center', label: 'bottom center' }, 177 { value: 'bottom-left', label: 'bottom left' }, 178 { value: 'bottom-right', label: 'bottom right' }, 179 { value: 'top-center', label: 'top center' }, 180 { value: 'top-left', label: 'top left' }, 181 { value: 'top-right', label: 'top right' }, 182 { value: 'middle-left', label: 'middle left' }, 183 { value: 'middle-right', label: 'middle right' }, 184 { value: 'custom', label: 'custom' } 185 ]; 186 187 const CAT_PAW_SPECIES_OPTIONS = [ 188 { value: 'cat', label: 'cat paw' }, 189 { value: 'dog', label: 'dog paw' } 190 ]; 191 192 function mergeCatPawArm(raw) { 193 if (window.CatPawArmCursor && typeof window.CatPawArmCursor.mergeConfig === 'function') { 194 return window.CatPawArmCursor.mergeConfig(raw); 195 } 196 var d = DEFAULT_CAT_PAW_ARM; 197 if (!raw || typeof raw !== 'object') { 198 return { 199 anchorPreset: d.anchorPreset, 200 originX: d.originX, 201 originY: d.originY, 202 armThickness: d.armThickness, 203 armTaper: d.armTaper, 204 armBend: d.armBend, 205 armMargin: d.armMargin, 206 followLag: d.followLag, 207 clickSquish: d.clickSquish, 208 showBeans: d.showBeans, 209 pawPreset: d.pawPreset, 210 pawSpecies: d.pawSpecies, 211 pawSize: d.pawSize, 212 padSize: d.padSize, 213 printsEnabled: d.printsEnabled, 214 printSize: d.printSize, 215 printFade: d.printFade, 216 printMax: d.printMax, 217 outlineShow: d.outlineShow, 218 outlineThickness: d.outlineThickness, 219 arm: Object.assign({}, d.arm), 220 pawFur: Object.assign({}, d.pawFur), 221 pawPads: Object.assign({}, d.pawPads), 222 outline: Object.assign({}, d.outline), 223 prints: Object.assign({}, d.prints) 224 }; 225 } 226 var pawSize = raw.pawSize !== undefined ? raw.pawSize : d.pawSize; 227 return {
228 anchorPreset: raw.anchorPreset || d.anchorPreset, 229 originX: raw.originX !== undefined ? raw.originX : d.originX, 230 originY: raw.originY !== undefined ? raw.originY : d.originY, 231 armThickness: pawSize, 232 armTaper: raw.armTaper !== undefined ? raw.armTaper : d.armTaper, 233 armBend: raw.armBend !== undefined ? raw.armBend : d.armBend, 234 armMargin: raw.armMargin !== undefined ? raw.armMargin : d.armMargin, 235 followLag: raw.followLag !== undefined ? raw.followLag : d.followLag, 236 clickSquish: typeof raw.clickSquish === 'boolean' ? raw.clickSquish : d.clickSquish, 237 showBeans: typeof raw.showBeans === 'boolean' ? raw.showBeans : d.showBeans, 238 pawPreset: raw.pawPreset || d.pawPreset, 239 pawSpecies: raw.pawSpecies === 'dog' ? 'dog' : 'cat', 240 pawSize: pawSize, 241 padSize: raw.padSize !== undefined ? raw.padSize : d.padSize, 242 printsEnabled: typeof raw.printsEnabled === 'boolean' ? raw.printsEnabled : d.printsEnabled, 243 printSize: raw.printSize !== undefined ? raw.printSize : d.printSize, 244 printFade: raw.printFade !== undefined ? raw.printFade : d.printFade, 245 printMax: raw.printMax !== undefined ? raw.printMax : d.printMax, 246 outlineShow: typeof raw.outlineShow === 'boolean' ? raw.outlineShow : d.outlineShow, 247 outlineThickness: raw.outlineThickness !== undefined ? raw.outlineThickness : d.outlineThickness, 248 arm: Object.assign({}, d.arm, raw.arm || {}), 249 pawFur: Object.assign({}, d.pawFur, raw.pawFur || {}), 250 pawPads: Object.assign({}, d.pawPads, raw.pawPads || {}), 251 outline: Object.assign({}, d.outline, raw.outline || {}), 252 prints: Object.assign({}, d.prints, raw.prints || {}) 253 }; 254 } 255 256 function setCatPawArm(patch, opts) { 257 opts = opts || {}; 258 var prev = mergeCatPawArm(currentSettings.catPawArm); 259 var next = Object.assign({}, prev, patch || {}); 260 ['arm', 'pawFur', 'pawPads', 'outline', 'prints'].forEach(function (key) { 261 if (patch && patch[key]) next[key] = Object.assign({}, prev[key], patch[key]); 262 }); 263 var merged = mergeCatPawArm(next); 264 currentSettings.catPawArm = merged; 265 saveSettings(); 266 if (window.CatPawArmCursor && typeof window.CatPawArmCursor.applySettings === 'function') { 267 window.CatPawArmCursor.applySettings(merged); 268 } 269 try { 270 document.dispatchEvent(new CustomEvent('pupwife:setting-changed', { 271 detail: { key: 'catPawArm', value: merged } 272 })); 273 } catch (e) { /* ignore */ } 274 if (opts.syncUi !== false) { 275 syncCatPawSettingsUI(document.getElementById('settings-panel')); 276 } 277 } 278 279 function getCatPawAnchorPresetMap() { 280 if (window.CatPawArmCursor && window.CatPawArmCursor.ANCHOR_PRESETS) { 281 return window.CatPawArmCursor.ANCHOR_PRESETS; 282 } 283 return { 284 'bottom-center': { originX: 50, originY: 108 }, 285 'bottom-left': { originX: 8, originY: 108 }, 286 'bottom-right': { originX: 92, originY: 108 }, 287 'top-center': { originX: 50, originY: -8 }, 288 'top-left': { originX: 8, originY: -8 }, 289 'top-right': { originX: 92, originY: -8 }, 290 'middle-left': { originX: 0, originY: 50 }, 291 'middle-right': { originX: 100, originY: 50 } 292 }; 293 } 294 295 function snapCatPawAnchorPreset(preset) { 296 var map = getCatPawAnchorPresetMap(); 297 var coords = map[preset]; 298 if (!coords) return null; 299 return { anchorPreset: preset, originX: coords.originX, originY: coords.originY }; 300 } 301 302 function randomHexColor() { 303 return '#' + Math.floor(Math.random() * 0xffffff).toString(16).padStart(6, '0'); 304 } 305 306 function randomColorPart() { 307 var modes = ['solid', 'rgb', 'pride']; 308 var mode = modes[Math.floor(Math.random() * modes.length)]; 309 var pride = ['rainbow', 'trans', 'bi', 'pan', 'lesbian', 'nb', 'ace', 'progress']; 310 return { 311 colorMode: mode, 312 solidColor: randomHexColor(), 313 rgbSpeed: Math.round((0.2 + Math.random() * 2.8) * 10) / 10, 314 pridePreset: pride[Math.floor(Math.random() * pride.length)] 315 }; 316 } 317 318 function getCatPawPresetOptions() { 319 if (window.CatPawArmCursor && typeof window.CatPawArmCursor.getPresetOptions === 'function') { 320 return window.CatPawArmCursor.getPresetOptions(); 321 } 322 return [{ value: 'classic-white', label: 'Classic white' }]; 323 } 324 325 function catPawUsesSpriteArt(presetId) { 326 var p = presetId || mergeCatPawArm(currentSettings.catPawArm).pawPreset; 327 if (!window.CatPawArmCursor) return false;
328 if (typeof window.CatPawArmCursor.hasPresetArt === 'function') { 329 return window.CatPawArmCursor.hasPresetArt(p); 330 } 331 if (typeof window.CatPawArmCursor.hasSpriteArt === 'function') { 332 return window.CatPawArmCursor.hasSpriteArt(p); 333 } 334 return false; 335 } 336 337 function updateCatPawArtModeUI(panel) { 338 if (!panel) return; 339 var spriteArt = catPawUsesSpriteArt(); 340 var pawPanel = panel.querySelector('#cat-paw-arm-settings'); 341 if (pawPanel) pawPanel.classList.toggle('catpaw-art-mode', spriteArt); 342 panel.querySelectorAll('.catpaw-procedural-only').forEach(function (el) { 343 el.hidden = spriteArt; 344 }); 345 panel.querySelectorAll('.catpaw-sprite-only').forEach(function (el) { 346 el.hidden = !spriteArt; 347 }); 348 var speciesField = panel.querySelector('.catpaw-species-field'); 349 if (speciesField) speciesField.hidden = spriteArt; 350 var note = panel.querySelector('.catpaw-sprite-art-note'); 351 if (note) { 352 note.hidden = !spriteArt; 353 note.textContent = 'Fur and arm tint together. Turn off toe beans for the back of the paw.'; 354 } 355 } 356 357 function randomizeCatPawColors(opts) { 358 opts = opts || {}; 359 if (catPawUsesSpriteArt()) return; 360 setCatPawArm({ 361 arm: randomColorPart(), 362 pawFur: randomColorPart(), 363 pawPads: randomColorPart(), 364 outline: randomColorPart(), 365 prints: randomColorPart() 366 }); 367 if (opts.syncUI !== false) { 368 syncCatPawSettingsUI(document.getElementById('settings-panel')); 369 } 370 } 371 372 function randomizeCatPawShape(opts) { 373 opts = opts || {}; 374 var presets = CAT_PAW_ANCHOR_OPTIONS.filter(function (o) { return o.value !== 'custom'; }); 375 var pick = presets[Math.floor(Math.random() * presets.length)]; 376 var snap = snapCatPawAnchorPreset(pick.value) || {}; 377 setCatPawArm(Object.assign({ 378 pawSpecies: Math.random() < 0.5 ? 'cat' : 'dog', 379 armTaper: Math.floor(Math.random() * 101), 380 armBend: Math.floor(Math.random() * 201), 381 followLag: 5 + Math.floor(Math.random() * 96), 382 clickSquish: Math.random() < 0.9, 383 showBeans: Math.random() < 0.85, 384 pawSize: 25 + Math.floor(Math.random() * 376), 385 padSize: 50 + Math.floor(Math.random() * 151), 386 printsEnabled: Math.random() < 0.9, 387 printSize: 25 + Math.floor(Math.random() * 176), 388 printFade: 25 + Math.floor(Math.random() * 176), 389 printMax: 4 + Math.floor(Math.random() * 29), 390 outlineShow: Math.random() < 0.95, 391 outlineThickness: 50 + Math.floor(Math.random() * 151) 392 }, snap)); 393 if (opts.syncUI !== false) { 394 syncCatPawSettingsUI(document.getElementById('settings-panel')); 395 } 396 } 397 398 function randomizeCatPawAll(opts) { 399 randomizeCatPawColors({ syncUI: false }); 400 randomizeCatPawShape({ syncUI: false }); 401 syncCatPawSettingsUI(document.getElementById('settings-panel')); 402 } 403 404 function mergeWoimString(raw) { 405 if (window.WoimStringFollower && typeof window.WoimStringFollower.mergeConfig === 'function') { 406 return window.WoimStringFollower.mergeConfig(raw); 407 } 408 var d = DEFAULT_WOIM_STRING; 409 if (!raw || typeof raw !== 'object') { 410 return { 411 ropePoints: d.ropePoints, 412 bodyPoints: d.bodyPoints, 413 size: d.size, 414 ropeLength: d.ropeLength, 415 ropeThickness: d.ropeThickness, 416 anchorShow: d.anchorShow, 417 anchorSize: d.anchorSize, 418 anchorThickness: d.anchorThickness, 419 woim: Object.assign({}, d.woim), 420 rope: Object.assign({}, d.rope) 421 }; 422 } 423 return { 424 ropePoints: parseWoimNotch(raw.ropePoints, 16, 64, d.ropePoints), 425 bodyPoints: parseWoimNotch(raw.bodyPoints, 16, 64, d.bodyPoints), 426 size: raw.size !== undefined ? raw.size : d.size, 427 ropeLength: raw.ropeLength !== undefined ? raw.ropeLength : d.ropeLength, 428 ropeThickness: raw.ropeThickness !== undefined ? raw.ropeThickness : d.ropeThickness, 429 anchorShow: typeof raw.anchorShow === 'boolean' ? raw.anchorShow : d.anchorShow, 430 anchorSize: raw.anchorSize !== undefined ? raw.anchorSize : d.anchorSize, 431 anchorThickness: raw.anchorThickness !== undefined ? raw.anchorThickness : d.anchorThickness, 432 woim: Object.assign({}, d.woim, raw.woim || {}), 433 rope: Object.assign({}, d.rope, raw.rope || {}) 434 }; 435 } 436 437 function setWoimString(patch) { 438 var prev = mergeWoimString(currentSettings.woimString); 439 var next = Object.assign({}, prev, patch || {}); 440 if (patch && patch.woim) next.woim = Object.assign({}, prev.woim, patch.woim); 441 if (patch && patch.rope) next.rope = Object.assign({}, prev.rope, patch.rope); 442 setSetting('woimString', mergeWoimString(next)); 443 } 444 445 function mergeJellypuff(raw) { 446 if (window.JellypuffFollower && typeof window.JellypuffFollower.mergeConfig === 'function') { 447 return window.JellypuffFollower.mergeConfig(raw); 448 } 449 var d = DEFAULT_JELLYPUFF; 450 if (!raw || typeof raw !== 'object') { 451 return { 452 size: d.size, 453 pulseRate: d.pulseRate, 454 tentacleDensity: d.tentacleDensity, 455 glowStrength: d.glowStrength, 456 bell: Object.assign({}, d.bell), 457 tentacles: Object.assign({}, d.tentacles) 458 }; 459 } 460 return { 461 size: raw.size !== undefined ? raw.size : d.size, 462 pulseRate: raw.pulseRate !== undefined ? raw.pulseRate : d.pulseRate, 463 tentacleDensity: raw.tentacleDensity !== undefined ? raw.tentacleDensity : d.tentacleDensity, 464 glowStrength: raw.glowStrength !== undefined ? raw.glowStrength : d.glowStrength, 465 bell: Object.assign({}, d.bell, raw.bell || {}), 466 tentacles: Object.assign({}, d.tentacles, raw.tentacles || {}) 467 }; 468 } 469 470 function setJellypuff(patch) { 471 var prev = mergeJellypuff(currentSettings.jellypuff); 472 var next = Object.assign({}, prev, patch || {}); 473 if (patch && patch.bell) next.bell = Object.assign({}, prev.bell, patch.bell); 474 if (patch && patch.tentacles) next.tentacles = Object.assign({}, prev.tentacles, patch.tentacles); 475 setSetting('jellypuff', mergeJellypuff(next)); 476 } 477 478 function parseWoimNotch(value, min, max, fallback) { 479 var n = parseInt(value, 10); 480 if (Number.isNaN(n)) return fallback; 481 return Math.max(min, Math.min(max, n)); 482 } 483 484 // Load settings from localStorage or use defaults
485 let currentSettings = loadSettings(); 486 487 // ==================================== 488 // SETTINGS STORAGE 489 // ==================================== 490 491 function loadSettings() { 492 try { 493 const saved = localStorage.getItem('puppywifey_settings'); 494 if (saved) { 495 const parsed = JSON.parse(saved); 496 // Merge with defaults in case new settings were added 497 const merged = { ...DEFAULT_SETTINGS, ...parsed }; 498 merged.darkMode = parseDarkMode(merged.darkMode, DEFAULT_SETTINGS.darkMode); 499 if (window.SiteThemePalettes && typeof window.SiteThemePalettes.readMergedThemeSettings === 'function') { 500 const theme = window.SiteThemePalettes.readMergedThemeSettings(); 501 merged.palette = theme.palette; 502 merged.darkMode = theme.darkMode; 503 } else if (!merged.palette) { 504 merged.palette = DEFAULT_SETTINGS.palette; 505 } 506 if (typeof merged.uiSounds !== 'boolean') { 507 merged.uiSounds = DEFAULT_SETTINGS.uiSounds; 508 } 509 merged.citrusUiScale = normalizeCitrusUiScale(merged.citrusUiScale); 510 merged.musicPreload = normalizeMusicPreload(merged.musicPreload); 511 merged.desktopIconsVisible = typeof merged.desktopIconsVisible === 'boolean' 512 ? merged.desktopIconsVisible 513 : DEFAULT_SETTINGS.desktopIconsVisible; 514 merged.desktopIconLabelsVisible = typeof merged.desktopIconLabelsVisible === 'boolean' 515 ? merged.desktopIconLabelsVisible 516 : DEFAULT_SETTINGS.desktopIconLabelsVisible; 517 merged.desktopIconScale = normalizeDesktopIconScale(merged.desktopIconScale); 518 merged.citrusWallpaperDim = clampSettingNumber( 519 merged.citrusWallpaperDim, 0, 50, DEFAULT_SETTINGS.citrusWallpaperDim 520 ); 521 merged.citrusWallpaperSaturation = clampSettingNumber( 522 merged.citrusWallpaperSaturation, 50, 150, DEFAULT_SETTINGS.citrusWallpaperSaturation 523 ); 524 merged.citrusWallpaperBrightness = clampSettingNumber( 525 merged.citrusWallpaperBrightness, 50, 150, DEFAULT_SETTINGS.citrusWallpaperBrightness 526 ); 527 merged.citrusWallpaperBlur = clampSettingNumber( 528 merged.citrusWallpaperBlur, 0, 12, DEFAULT_SETTINGS.citrusWallpaperBlur 529 ); 530 merged.taskbarAutoHide = typeof merged.taskbarAutoHide === 'boolean' 531 ? merged.taskbarAutoHide 532 : DEFAULT_SETTINGS.taskbarAutoHide; 533 merged.taskbarClock24Hour = typeof merged.taskbarClock24Hour === 'boolean' 534 ? merged.taskbarClock24Hour 535 : DEFAULT_SETTINGS.taskbarClock24Hour; 536 merged.taskbarClockShowSeconds = typeof merged.taskbarClockShowSeconds === 'boolean' 537 ? merged.taskbarClockShowSeconds 538 : DEFAULT_SETTINGS.taskbarClockShowSeconds; 539 merged.taskbarShowDate = typeof merged.taskbarShowDate === 'boolean' 540 ? merged.taskbarShowDate 541 : DEFAULT_SETTINGS.taskbarShowDate; 542 merged.showDesktopButton = typeof merged.showDesktopButton === 'boolean' 543 ? merged.showDesktopButton 544 : DEFAULT_SETTINGS.showDesktopButton; 545 merged.windowTitlebarDblClickMaximize = typeof merged.windowTitlebarDblClickMaximize === 'boolean' 546 ? merged.windowTitlebarDblClickMaximize 547 : DEFAULT_SETTINGS.windowTitlebarDblClickMaximize; 548 var validSettingsTabs = ['account', 'appearance', 'desktop', 'cursor', 'effects', 'advanced']; 549 if (validSettingsTabs.indexOf(merged.citrusSettingsTab) === -1) { 550 merged.citrusSettingsTab = DEFAULT_SETTINGS.citrusSettingsTab; 551 } 552 if (merged.trailAmount === undefined || merged.trailAmount === null) { 553 merged.trailAmount = DEFAULT_SETTINGS.trailAmount; 554 } else { 555 const parsedAmount = parseInt(merged.trailAmount, 10); 556 merged.trailAmount = Number.isNaN(parsedAmount) 557 ? DEFAULT_SETTINGS.trailAmount 558 : Math.max(0, Math.min(4, parsedAmount)); 559 } 560 if (merged.cursorTrail === 'worms') merged.cursorTrail = 'woims'; 561 if (merged.cursorTrail === 'gummyWorms') merged.cursorTrail = 'gummyWoims'; 562 if (merged.follower === 'koi') merged.follower = 'fish2D'; 563 if (!merged.background || typeof merged.background !== 'string') { 564 merged.background = DEFAULT_SETTINGS.background; 565 }
565 else if (window.SiteBackgrounds && typeof window.SiteBackgrounds.normalizeId === 'function') { 566 merged.background = window.SiteBackgrounds.normalizeId(merged.background); 567 } else if (merged.background !== 'caustics' && merged.background !== 'horizon' && merged.background !== 'clouds' && merged.background !== 'aquarium') { 568 merged.background = DEFAULT_SETTINGS.background; 569 } 570 if (window.SiteBackgroundTuning && typeof window.SiteBackgroundTuning.mergeSettings === 'function') { 571 merged.backgroundTuning = window.SiteBackgroundTuning.mergeSettings(parsed.backgroundTuning); 572 } 573 // Retired Matter.js custom cursors 574 if (merged.customCursor && String(merged.customCursor).startsWith('matter-')) { 575 merged.customCursor = 'none'; 576 } 577 // Retired toggles â always off (UI removed) 578 delete merged.fishGlow; 579 delete merged.shadows; 580 delete merged.navConfetti; 581 // Retired toggles â always on (UI removed) 582 delete merged.caustics; 583 delete merged.aurora; 584 delete merged.bubbles; 585 merged.woimString = mergeWoimString(parsed.woimString); 586 merged.jellypuff = mergeJellypuff(parsed.jellypuff); 587 merged.catPawArm = mergeCatPawArm(parsed.catPawArm); 588 Object.keys(merged).forEach(function (key) { 589 if (key.indexOf('catpaw-') === 0) delete merged[key]; 590 }); 591 if (window.PupwifeTrailColorSupport && typeof window.PupwifeTrailColorSupport.mergeTrailColors === 'function') { 592 merged.trailColors = window.PupwifeTrailColorSupport.mergeTrailColors(parsed.trailColors); 593 } else if (parsed.trailColors && typeof parsed.trailColors === 'object') { 594 merged.trailColors = parsed.trailColors; 595 } else { 596 merged.trailColors = { primary: '#c8a8ff', secondary: '#ffb3ba', tertiary: '#89cff0' }; 597 } 598 merged.trailFavorites = Array.isArray(parsed.trailFavorites) 599 ? parsed.trailFavorites.filter(function (v) { return typeof v === 'string' && v; }) 600 : DEFAULT_SETTINGS.trailFavorites; 601 return merged; 602 } 603 } catch (e) { 604 emitDevWarn('Failed to load settings:', e); 605 } 606 return Object.assign({ ...DEFAULT_SETTINGS }, { 607 woimString: mergeWoimString({}), 608 jellypuff: mergeJellypuff({}), 609 catPawArm: mergeCatPawArm({}), 610 trailColors: window.PupwifeTrailColorSupport 611 ? window.PupwifeTrailColorSupport.mergeTrailColors(null) 612 : { primary: '#c8a8ff', secondary: '#ffb3ba', tertiary: '#89cff0' }, 613 trailFavorites: [] 614 }); 615 } 616 617 function saveSettings() { 618 try { 619 localStorage.setItem('puppywifey_settings', JSON.stringify(currentSettings)); 620 emitDevLog('Settings saved!', currentSettings); 621 } catch (e) { 622 emitDevWarn('Failed to save settings:', e); 623 } 624 } 625 626 function getSetting(key) { 627 return currentSettings[key]; 628 } 629 630 function setSetting(key, value) { 631 currentSettings[key] = value; 632 saveSettings(); 633 applySettings(); 634 try { 635 document.dispatchEvent(new CustomEvent('pupwife:setting-changed', { 636 detail: { key: key, value: value } 637 })); 638 } catch (e) { /* ignore */ } 639 } 640 641 function setSettingQuiet(key, value) { 642 currentSettings[key] = value; 643 saveSettings(); 644 } 645 646 // ==================================== 647 // APPLY SETTINGS TO PAGE 648 // ==================================== 649 650 function applySettings() { 651 emitDevLog('Settings applying...', currentSettings); 652 653 // Theme â palette everywhere; light/dark via chatroom when on chatroom route 654 let themeState = { 655 palette: currentSettings.palette || DEFAULT_SETTINGS.palette, 656 colorMode: currentSettings.darkMode ? 'dark' : 'light', 657 darkMode: !!currentSettings.darkMode 658 }; 659 if (window.SiteThemeToggle && typeof window.SiteThemeToggle.readThemeSettings === 'function') { 660 const synced = window.SiteThemeToggle.readThemeSettings(); 661 themeState.palette = currentSettings.palette || synced.palette; 662 if (isChatroomPage()) { 663 themeState.colorMode = synced.colorMode; 664 themeState.darkMode = synced.darkMode; 665 } 666 } 667 currentSettings.palette = themeState.palette; 668 if (window.SiteThemeToggle && typeof window.SiteThemeToggle.a
668pplyThemeState === 'function') { 669 window.SiteThemeToggle.applyThemeState(themeState); 670 } else if (!/chatroom/.test(window.location.pathname)) { 671 const isDark = themeState.darkMode; 672 document.body.classList.toggle('dark-mode', isDark); 673 document.documentElement.classList.toggle('dark-mode', isDark); 674 document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light'); 675 document.documentElement.setAttribute('data-site-palette', themeState.palette); 676 document.documentElement.setAttribute('data-color-mode', themeState.colorMode); 677 } 678 updateDarkModeUI(isChatroomPage() ? isChatroomDarkTheme() : themeState.darkMode); 679 setPalettePickerValue(themeState.palette); 680 681 // Ambient background stack (caustics preset by default; extensible via siteBackgrounds.js) 682 if (window.SiteBackgroundApply && typeof window.SiteBackgroundApply.applyFromSettings === 'function') { 683 window.SiteBackgroundApply.applyFromSettings(); 684 } 685 686 if (window.SiteBackgroundTuning && typeof window.SiteBackgroundTuning.applyAll === 'function') { 687 window.SiteBackgroundTuning.applyAll(currentSettings.backgroundTuning); 688 } else { 689 toggleEffect('caustics', true); 690 toggleEffect('aurora', true); 691 toggleEffect('bubbles', true); 692 } 693 694 if (currentSettings.performanceMode) { 695 toggleEffect('clickSparks', false); 696 toggleEffect('seasonalOverlay', false); 697 toggleEffect('weatherOverlay', false); 698 toggleRetiredEffects(false); 699 700 // Disable cursor trail in performance mode 701 toggleCursorTrail('none'); 702 703 if (window.CatPawArmCursor && typeof window.CatPawArmCursor.setCursor === 'function') { 704 window.CatPawArmCursor.setCursor('none'); 705 } 706 707 // Set global performance flag (bubbles ignore this â see bubbles.js) 708 window.PERFORMANCE_MODE_ENABLED = true; 709 710 // Notify perfModeToggle.js if it exists 711 if (typeof window.togglePerformanceMode === 'function') { 712 const currentPerfMode = window.PERFORMANCE_MODE_ENABLED || false; 713 if (!currentPerfMode) { 714 window.togglePerformanceMode(); 715 } 716 } 717 } else { 718 window.PERFORMANCE_MODE_ENABLED = false; 719 720 toggleRetiredEffects(false); 721 toggleEffect('clickSparks', currentSettings.clickSparks); 722 toggleEffect('seasonalOverlay', currentSettings.seasonalOverlay); 723 toggleEffect('weatherOverlay', currentSettings.weatherOverlay); 724 } 725 726 // Follower selection 727 if (window.switchFollower && typeof window.switchFollower === 'function') { 728 // Check if we're on chat page 729 const isOnChatPage = window.location.pathname.includes('chat.html') || 730 window.location.pathname.endsWith('chat'); 731 732 // Force "none" on chat page, otherwise apply saved setting 733 if (isOnChatPage) { 734 emitDevLog('â ï¸ Chat page detected - forcing follower to "none"'); 735 window.switchFollower('none'); 736 } else { 737 const savedFollower = getSetting('follower'); 738 if (savedFollower) { 739 window.switchFollower(savedFollower, false); 740 } 741 } 742 } 743 744 // Cursor trail (independent toggle) 745 toggleCursorTrail(currentSettings.cursorTrail); 746 toggleCursorTrailAmount(currentSettings.trailAmount); 747 applyTrailColorsSetting(currentSettings.trailColors); 748 749 // Custom cursor 750 toggleCustomCursor(currentSettings.customCursor); 751 752 // Homepage landing â dismiss immediately if turned off mid-landing 753 if (!currentSettings.homeLanding && 754 window.HomeLanding && 755 typeof window.HomeLanding.isActive === 'function' && 756 window.HomeLanding.isActive()) { 757 window.HomeLanding.destroy(); 758 } 759 760 applyCitrusUiScale(currentSettings.citrusUiScale); 761 applyCitrusDesktopPrefs(); 762 applyCitrusTaskbarPrefs(); 763 applySitewideWallpaperFilters(); 764 } 765 766 function applyCitrusTaskbarPrefs() { 767 if (!isCitrusOsPage()) return; 768 if (window.CitrusDesktop && typeof window.CitrusDesktop.applyTaskbarPrefs === 'function') { 769 window.CitrusDesktop.applyTaskbarPrefs({ 770 taskbarAutoHide: currentSettings.taskbarAutoHide, 771 taskbarClock24Hour: currentSettings.taskbarClock24Hour, 772 taskbarClockShowSeconds: currentSettings.taskbarClockShowSeconds, 773 taskbarShowDate: currentSettings.taskbarShowDate, 774 showDesktopButton: currentSettings.showDesktopButton, 775 windowTitlebarDblClickMaximize: currentSettings.windowTitlebarDblClickMaximize 776 }); 777 } 778 } 779 780 function getWallpaperFilterLayerIds() { 781 var ids = ['page-background', 'aurora-overlay', 'webgl-overlay']; 782 if (document.documentElement.getAttribute('data-site-background') === 'aquarium') { 783 ids.push('site-aquarium-ocean', 'site-aquarium-plants', 'site-aquarium-fish', 'fish-glow-overlay'); 784 } 785 return ids; 786 } 787 788 function applyWallpaperLayerFilters(sat, brightness, blur) { 789 var filterParts = []; 790 if (sat !== 100) filterParts.push('saturate(' + (sat / 100) + ')'); 791 if (brightness !== 100) filterParts.push('brightness(' + (brightness / 100) + ')'); 792 if (blur > 0) filterParts.push('blur(' + blur + 'px)');
793 var filter = filterParts.length ? filterParts.join(' ') : ''; 794 getWallpaperFilterLayerIds().forEach(function (id) { 795 var el = document.getElementById(id); 796 if (el) el.style.filter = filter; 797 }); 798 } 799 800 function applyCitrusDesktopPrefs() { 801 if (!isCitrusOsPage()) return; 802 803 if (window.CitrusDesktop && typeof window.CitrusDesktop.applyDesktopIconPrefs === 'function') { 804 window.CitrusDesktop.applyDesktopIconPrefs({ 805 desktopIconsVisible: currentSettings.desktopIconsVisible, 806 desktopIconLabelsVisible: currentSettings.desktopIconLabelsVisible, 807 desktopIconScale: currentSettings.desktopIconScale 808 }); 809 } 810 811 var dim = currentSettings.citrusWallpaperDim || 0; 812 var sat = currentSettings.citrusWallpaperSaturation != null 813 ? currentSettings.citrusWallpaperSaturation 814 : DEFAULT_SETTINGS.citrusWallpaperSaturation; 815 var brightness = currentSettings.citrusWallpaperBrightness != null 816 ? currentSettings.citrusWallpaperBrightness 817 : DEFAULT_SETTINGS.citrusWallpaperBrightness; 818 var blur = currentSettings.citrusWallpaperBlur != null 819 ? currentSettings.citrusWallpaperBlur 820 : DEFAULT_SETTINGS.citrusWallpaperBlur; 821 var tint = document.getElementById('citrusos-wallpaper-tint'); 822 if (!tint) { 823 tint = document.createElement('div'); 824 tint.id = 'citrusos-wallpaper-tint'; 825 var shell = document.getElementById('citrusos-shell'); 826 if (shell) { 827 shell.insertAdjacentElement('afterbegin', tint); 828 } else { 829 document.body.appendChild(tint); 830 } 831 } 832 tint.style.background = dim > 0 ? 'rgba(0, 0, 0, ' + (dim / 100) + ')' : 'transparent'; 833 834 applyWallpaperLayerFilters(sat, brightness, blur); 835 } 836 837 function applySitewideWallpaperFilters() { 838 if (isCitrusOsPage()) return; 839 var sat = currentSettings.citrusWallpaperSaturation != null 840 ? currentSettings.citrusWallpaperSaturation 841 : DEFAULT_SETTINGS.citrusWallpaperSaturation; 842 var brightness = currentSettings.citrusWallpaperBrightness != null 843 ? currentSettings.citrusWallpaperBrightness 844 : DEFAULT_SETTINGS.citrusWallpaperBrightness; 845 var blur = currentSettings.citrusWallpaperBlur != null 846 ? currentSettings.citrusWallpaperBlur 847 : DEFAULT_SETTINGS.citrusWallpaperBlur; 848 applyWallpaperLayerFilters(sat, brightness, blur); 849 } 850 851 function applyWallpaperFilters() { 852 if (isCitrusOsPage()) { 853 applyCitrusDesktopPrefs(); 854 return; 855 } 856 applySitewideWallpaperFilters(); 857 } 858 859 function toggleRetiredEffects(enabled) { 860 toggleEffect('fishGlow', enabled); 861 toggleEffect('shadows', enabled); 862 toggleEffect('navConfetti', enabled); 863 } 864 865 function toggleEffect(effectName, enabled) { 866 const effectMap = { 867 fishGlow: 'fish-glow-overlay', 868 shadows: null, // handled by global flag
869 caustics: 'webgl-overlay', 870 aurora: 'aurora-overlay', 871 clickSparks: null, // handled by global flag 872 seasonalOverlay: null, // handled by seasonalOverlay instance 873 bubbles: null, // handled by global flag 874 navConfetti: null // handled by global flag 875 }; 876 877 const elementId = effectMap[effectName]; 878 879 if (effectName === 'seasonalOverlay') { 880 // Handle seasonal overlay toggle 881 if (enabled) { 882 if (!window.seasonalOverlay && typeof SeasonalOverlay !== 'undefined') { 883 window.seasonalOverlay = new SeasonalOverlay(); 884 emitDevLog('ð Seasonal overlay enabled'); 885 } 886 } else { 887 if (window.seasonalOverlay && typeof window.seasonalOverlay.destroy === 'function') { 888 window.seasonalOverlay.destroy(); 889 window.seasonalOverlay = null; 890 emitDevLog('ð Seasonal overlay disabled'); 891 } 892 } 893 return; 894 } 895 896 if (effectName === 'weatherOverlay') { 897 // Handle weather overlay toggle 898 if (enabled) { 899 if (!window.weatherOverlay && typeof WeatherOverlay !== 'undefined') { 900 window.weatherOverlay = new WeatherOverlay(); 901 emitDevLog('ð¤ï¸ Weather overlay enabled'); 902 } 903 } else { 904 if (window.weatherOverlay && typeof window.weatherOverlay.destroy === 'function') { 905 window.weatherOverlay.destroy(); 906 window.weatherOverlay = null; 907 emitDevLog('ð¤ï¸ Weather overlay disabled'); 908 } 909 } 910 return; 911 } 912 913 if (effectName === 'bubbles') { 914 // Handle bubbles toggle via global flag 915 window.BUBBLES_DISABLED = !enabled; 916 emitDevLog('Bubbles:', enabled ? 'ON' : 'OFF'); 917 return; 918 } 919 920 if (effectName === 'navConfetti') { 921 // Handle nav confetti toggle via global flag 922 window.NAV_CONFETTI_DISABLED = !enabled; 923 emitDevLog('Nav Confetti:', enabled ? 'ON' : 'OFF'); 924 return; 925 } 926 927 if (effectName === 'shadows') { 928 // Toggle shadows via global flag 929 if (!window.SHADOWS_DISABLED) { 930 window.SHADOWS_DISABLED = !enabled; 931 } else { 932 window.SHADOWS_DISABLED = !enabled; 933 } 934 935 // Also update fishBuddyShadows if it exists 936 if (window.fishBuddyShadows && window.fishBuddyShadows.setPerformanceMode) { 937 window.fishBuddyShadows.setPerformanceMode(!enabled); 938 } 939 940 emitDevLog('Shadows:', enabled ? 'ON' : 'OFF'); 941 } else if (effectName === 'clickSparks') { 942 window.CLICK_SPARKS_DISABLED = !enabled; 943 if (window.ClickSparks && typeof window.ClickSparks.setEnabled === 'function') { 944 window.ClickSparks.setEnabled(enabled); 945 } 946 emitDevLog('Click sparks:', enabled ? 'ON' : 'OFF'); 947 } else if (elementId) { 948 const element = document.getElementById(elementId); 949 if (element) { 950 element.style.display = enabled ? '' : 'none'; 951 } 952 } 953 } 954 955 function applyTrailColorsSetting(colors) { 956 var merged = colors; 957 if (window.PupwifeTrailColorSupport && typeof window.PupwifeTrailColorSupport.mergeTrailColors === 'function') { 958 merged = window.PupwifeTrailColorSupport.mergeTrailColors(colors); 959 } 960 currentSettings.trailColors = merged; 961 if (window.CursorTrails && typeof window.CursorTrails.setTrailColors === 'function') { 962 window.CursorTrails.setTrailColors(merged); 963 } 964 } 965 966 function toggleCursorTrail(trailType) { 967 if (window.CursorTrails && typeof window.CursorTrails.setTrailType === 'function') { 968 window.CursorTrails.setTrailType(trailType); 969 } else { 970 emitDevWarn('CursorTrails not loaded yet'); 971 } 972 } 973 974 const TRAIL_AMOUNT_LABELS = ['Minimum', 'Low', 'Normal', 'High', 'Extreme']; 975 976 function parseTrailAmount(value, fallback) { 977 if (fallback === undefined) fallback = DEFAULT_SETTINGS.trailAmount; 978 const n = parseInt(value, 10); 979 if (Number.isNaN(n)) return fallback; 980 return Math.max(0, Math.min(4, n)); 981 } 982 983 function toggleCursorTrailAmount(level) { 984 const amount = parseTrailAmount(level); 985 if (window.CursorTrails && typeof window.CursorTrails.setTrailAmount === 'function') { 986 window.CursorTrails.setTrailAmount(amount); 987 } 988 } 989 990 function updateTrailAmountLabel(panel) { 991 if (!panel) return; 992 const slider = panel.querySelector('#setting-trailAmount'); 993 const label = panel.querySelector('#setting-trailAmount-label'); 994 if (!slider || !label) return; 995 const v = parseTrailAmount(slider.value); 996 label.textContent = TRAIL_AMOUNT_LABELS[v]; 997 slider.setAttribute('aria-valuetext', TRAIL_AMOUNT_LABELS[v]); 998 } 999 1000 function updateTrailControlsState() { 1001 const panel = document.getElementById('settings-panel'); 1002 if (!panel) return; 1003 const amountDisabled = currentSettings.cursorTrail === 'none' || currentSettings.performanceMode; 1004 setCursorTrailPickerDisabled(!!currentSettings.performanceMode); 1005 const amountRow = panel.querySelector('#setting-trailAmount-row'); 1006 if (amountRow) { 1007 amountRow.classList.toggle('is-disabled', amountDisabled); 1008 } 1009 const slider = panel.querySelector('#setting-trailAmount'); 1010 if (slider) { 1011 slider.disabled = amountDisabled; 1012 } 1013 } 1014 1015 function scriptSrcMatches(el, src) { 1016 if (!el) return false;
1017 var attr = el.getAttribute('src') || ''; 1018 if (attr === src) return true; 1019 try { 1020 return el.src && (el.src.endsWith(src) || el.src.indexOf(src.replace(/^\//, '')) !== -1); 1021 } catch (e) { 1022 return false; 1023 } 1024 } 1025 1026 function findScriptBySrc(src) { 1027 var scripts = document.querySelectorAll('script[src]'); 1028 for (var i = 0; i < scripts.length; i++) { 1029 if (scriptSrcMatches(scripts[i], src)) return scripts[i]; 1030 } 1031 return null; 1032 } 1033 1034 function loadScriptThenRun(src, onReady) { 1035 function run() { 1036 if (typeof onReady === 'function') onReady(); 1037 } 1038 1039 if (src.indexOf('roughjsCursor.js') !== -1 && window.RoughJSCursor) { 1040 run(); 1041 return; 1042 } 1043 1044 if (window.PupwifeScriptLoad && typeof window.PupwifeScriptLoad.once === 'function') { 1045 window.PupwifeScriptLoad.once(src, { markKey: 'pupwifeLoaded' }).then(run).catch(run); 1046 return; 1047 } 1048 1049 var existing = findScriptBySrc(src); 1050 if (existing) { 1051 if (existing.getAttribute('data-loaded') === '1') { 1052 run(); 1053 return; 1054 } 1055 var finished = function () { 1056 existing.setAttribute('data-loaded', '1'); 1057 run(); 1058 }; 1059 existing.addEventListener('load', finished, { once: true }); 1060 existing.addEventListener('error', finished, { once: true }); 1061 window.setTimeout(finished, 0); 1062 return; 1063 } 1064 1065 var script = document.createElement('script'); 1066 script.src = src; 1067 script.onload = function () { 1068 script.setAttribute('data-loaded', '1'); 1069 run(); 1070 }; 1071 script.onerror = function () { 1072 script.setAttribute('data-loaded', '1'); 1073 run(); 1074 }; 1075 document.head.appendChild(script); 1076 } 1077 1078 function clearImageCustomCursor() { 1079 const existingStyle = document.getElementById('custom-cursor-style'); 1080 if (existingStyle) { 1081 existingStyle.remove(); 1082 } 1083 } 1084 1085 function applyCatPawArmCursor() { 1086 clearImageCustomCursor(); 1087 if (window.TwoJSCursor && typeof window.TwoJSCursor.setCursor === 'function') { 1088 window.TwoJSCursor.setCursor('none'); 1089 } 1090 if (window.RoughJSCursor && typeof window.RoughJSCursor.setCursor === 'function') { 1091 window.RoughJSCursor.setCursor('none'); 1092 } 1093 if (currentSettings.performanceMode) { 1094 if (window.CatPawArmCursor && typeof window.CatPawArmCursor.setCursor === 'function') { 1095 window.CatPawArmCursor.setCursor('none'); 1096 } 1097 return; 1098 } 1099 if (window.CatPawArmCursor && typeof window.CatPawArmCursor.setCursor === 'function') { 1100 window.CatPawArmCursor.setCursor('cat-paw-arm'); 1101 return; 1102 } 1103 emitDevWarn('Stretchy paw cursor not loaded yet'); 1104 loadScriptThenRun('/scripts/dependencies/cursors/catPawArmCursor.js', function () { 1105 if (window.CatPawArmCursor) { 1106 window.CatPawArmCursor.setCursor('cat-paw-arm'); 1107 } 1108 }); 1109 } 1110 1111 function applyRoughCursor(cursorType) { 1112 if (window.CatPawArmCursor && typeof window.CatPawArmCursor.setCursor === 'function') { 1113 window.CatPawArmCursor.setCursor('none'); 1114 } 1115 clearImageCustomCursor(); 1116 if (window.TwoJSCursor && typeof window.TwoJSCursor.setCursor === 'function') { 1117 window.TwoJSCursor.setCursor('none'); 1118 } 1119 if (window.RoughJSCursor && typeof window.RoughJSCursor.setCursor === 'function') { 1120 window.RoughJSCursor.setCursor(cursorType); 1121 return; 1122 } 1123 emitDevWarn('Rough.js cursor system not loaded yet'); 1124 loadScriptThenRun('/scripts/dependencies/cursors/roughjsCursor.js', () => { 1125 if (window.RoughJSCursor) { 1126 window.RoughJSCursor.setCursor(cursorType); 1127 } 1128 }); 1129 } 1130 1131 function applyTwoJsCursor(cursorType) { 1132 if (window.CatPawArmCursor && typeof window.CatPawArmCursor.setCursor === 'function') { 1133 window.CatPawArmCursor.setCursor('none'); 1134 } 1135 clearImageCustomCursor(); 1136 if (window.RoughJSCursor && typeof window.RoughJSCursor.setCursor === 'function') { 1137 window.RoughJSCursor.setCursor('none'); 1138 } 1139 if (window.TwoJSCursor && typeof window.TwoJSCursor.setCursor === 'function') { 1140 window.TwoJSCursor.setCursor(cursorType); 1141 return; 1142 } 1143 emitDevWarn('Two.js cursor system not loaded yet'); 1144 loadScriptThenRun('/scripts/dependencies/cursors/twojsCursor.js', () => { 1145 if (window.TwoJSCursor) { 1146 window.TwoJSCursor.setCursor(cursorType); 1147 } 1148 }); 1149 } 1150 1151 function clearEngineCursors() { 1152 if (window.CatPawArmCursor && typeof window.CatPawArmCursor.setCursor === 'function') { 1153 window.CatPawArmCursor.setCursor('none'); 1154 } 1155 if (window.TwoJSCursor && typeof window.TwoJSCursor.setCursor === 'function') { 1156 window.TwoJSCursor.setCursor('none'); 1157 } 1158 if (window.RoughJSCursor && typeof window.RoughJSCursor.setCursor === 'function') { 1159 window.RoughJSCursor.setCursor('none'); 1160 } 1161 } 1162 1163 function toggleCustomCursor(cursorType) { 1164 // Handle stretchy paw cursor 1165 if (cursorType === 'cat-paw-arm') {
1166 applyCatPawArmCursor(); 1167 updateCatPawPanelVisibility(); 1168 return; 1169 } 1170 1171 updateCatPawPanelVisibility(); 1172 1173 // Handle Rough.js cursors 1174 if (cursorType && cursorType.startsWith('rough-')) { 1175 applyRoughCursor(cursorType); 1176 return; 1177 } 1178 1179 // Handle Two.js cursors 1180 if (cursorType && cursorType.startsWith('twojs-')) { 1181 applyTwoJsCursor(cursorType); 1182 return; 1183 } 1184 1185 // Remove engine cursors if switching away 1186 clearEngineCursors(); 1187 1188 // Remove existing custom cursor style if it exists 1189 clearImageCustomCursor(); 1190 1191 if (cursorType === 'none') { 1192 // No custom cursor - use defaults 1193 return; 1194 } 1195 1196 // Create style element for custom cursors 1197 const style = document.createElement('style'); 1198 style.id = 'custom-cursor-style'; 1199 1200 if (cursorType === 'asriel') { 1201 // Asriel cursor set 1202 style.textContent = ` 1203 * { 1204 cursor: url('/cursors/Asriel/cursor.webp'), auto !important; 1205 } 1206 a, button, [role="button"], input[type="button"], input[type="submit"], 1207 input[type="reset"], [onclick], .clickable, [style*="cursor: pointer"] { 1208 cursor: url('/cursors/Asriel/pointer.webp'), pointer !important; 1209 } 1210 input[type="text"], input[type="password"], input[type="email"], 1211 input[type="search"], input[type="url"], input[type="tel"], 1212 textarea, [contenteditable="true"], [contenteditable], 1213 [style*="cursor: text"] { 1214 cursor: url('/cursors/Asriel/text.webp'), text !important; 1215 } 1216 [style*="cursor: wait"], .loading, [aria-busy="true"] { 1217 cursor: url('/cursors/Asriel/wait.webp'), wait !important; 1218 } 1219 [disabled], [style*="cursor: not-allowed"], 1220 [aria-disabled="true"] { 1221 cursor: url('/cursors/Asriel/not-allowed.webp'), not-allowed !important; 1222 } 1223 `; 1224 } else if (cursorType === 'axolotl') { 1225 // Axolotl cursor set - using available files with fallbacks 1226 style.textContent = ` 1227 * { 1228 cursor: url('/cursors/Axolotl/normal-select.webp'), auto !important; 1229 } 1230 a, button, [role="button"], input[type="button"], input[type="submit"], 1231 input[type="reset"], [onclick], .clickable, [style*="cursor: pointer"] { 1232 cursor: url('/cursors/Axolotl/link-select.webp'), pointer !important; 1233 } 1234 input[type="text"], input[type="password"], input[type="email"], 1235 input[type="search"], input[type="url"], input[type="tel"], 1236 textarea, [contenteditable="true"], [contenteditable], 1237 [style*="cursor: text"] { 1238 cursor: url('/cursors/Axolotl/text-select.webp'), text !important; 1239 } 1240 [style*="cursor: wait"], .loading, [aria-busy="true"] { 1241 cursor: url('/cursors/Axolotl/busy.webp'), wait !important; 1242 } 1243 [disabled], [style*="cursor: not-allowed"], 1244 [aria-disabled="true"] { 1245 cursor: url('/cursors/Axolotl/unavailable.webp'), not-allowed !important; 1246 } 1247 [style*="cursor: move"], [draggable="true"] { 1248 cursor: url('/cursors/Axolotl/move.webp'), move !important; 1249 } 1250 [style*="cursor: crosshair"] { 1251 cursor: url('/cursors/Axolotl/precision-select.webp'), crosshair !important; 1252 } 1253 [style*="cursor: help"] { 1254 cursor: url('/cursors/Axolotl/normal-select.webp'), help !important; 1255 } 1256 [style*="cursor: ew-resize"], [style*="cursor: col-resize"] { 1257 cursor: url('/cursors/Axolotl/move.webp'), ew-resize !important; 1258 } 1259 [style*="cursor: ns-resize"], [style*="cursor: row-resize"] { 1260 cursor: url('/cursors/Axolotl/move.webp'), ns-resize !important; 1261 } 1262 [style*="cursor: nwse-resize"], [style*="cursor: nw-resize"], [style*="cursor: se-resize"] { 1263 cursor: url('/cursors/Axolotl/move.webp'), nwse-resize !important; 1264 } 1265 [style*="cursor: nesw-resize"], [style*="cursor: ne-resize"], [style*="cursor: sw-resize"] { 1266 cursor: url('/cursors/Axolotl/move.webp'), nesw-resize !important; 1267 } 1268 [style*="cursor: alias"] { 1269 cursor: url('/cursors/Axolotl/link-select.webp'), alias !important; 1270 } 1271 [style*="cursor: context-menu"] { 1272 cursor: url('/cursors/Axolotl/alternate-select.webp'), context-menu !important; 1273 } 1274 `; 1275 } 1276 1277 document.head.appendChild(style); 1278 emitDevLog(`ð¯ Custom cursor set to: ${cursorType}`); 1279 } 1280 1281 // ==================================== 1282 // UI CREATION 1283 // ==================================== 1284 1285 function ensureSettingsAssets() { 1286 if (isCitrusOsPage()) { 1287 return; 1288 } 1289 if (!document.querySelector('link[href*="settings-menu.css"]')) { 1290 var css = document.createElement('link'); 1291 css.rel = 'stylesheet'; 1292 css.href = '/css/settings-menu.css'; 1293 css.type = 'text/css'; 1294 document.head.appendChild(css); 1295 } 1296 } 1297 1298 function ensureTrailPickerScript() { 1299 return new Promise(function (resolve) { 1300 if (window.PupwifeTrailPicker) { 1301 resolve(); 1302 return; 1303 } 1304 var existing = document.querySelector('script[data-trail-picker]'); 1305 if (existing) { 1306 if (existing.getAttribute('data-trail-picker-ready') === '1') { 1307 resolve(); 1308 return; 1309 } 1310 existing.addEventListener('load', resolve, { once: true }); 1311 existing.addEventListener('error', resolve, { once: true }); 1312 return; 1313 } 1314 var script = document.createElement('script'); 1315 script.src = '/scripts/effects/trailPicker.js'; 1316 script.setAttribute('data-trail-picker', '1'); 1317 script.onload = script.onerror = function () { 1318 script.setAttribute('data-trail-picker-ready', '1'); 1319 resolve(); 1320 }; 1321 document.head.appendChild(script); 1322 }); 1323 } 1324 1325 function mountCursorTrailPicker(panel) { 1326 var mountEl = panel.querySelector('#setting-cursorTrail-picker'); 1327 if (!mountEl || !window.PupwifeTrailPicker) return; 1328 window.PupwifeTrailPicker.mount(mountEl, { 1329 value: currentSettings.cursorTrail, 1330 disabled: !!currentSettings.performanceMode, 1331 getFavorites: function () { 1332 return currentSettings.trailFavorites || []; 1333 }, 1334 onFavoriteChange: function (favs) { 1335 setSetting('trailFavorites', favs.slice()); 1336 if (typeof window.PupwifeTrailPicker.refreshFavorites === 'function') { 1337 window.PupwifeTrailPicker.refreshFavorites(); 1338 } 1339 }, 1340 onChange: function (value) { 1341 setSetting('cursorTrail', value); 1342 updateTrailControlsState(); 1343 updateTrailColorPanelVisibility(); 1344 if (window.PupwifeSettingsDiscoverability) { 1345 window.PupwifeSettingsDiscoverability.markSeen('trail', value); 1346 } 1347 } 1348 }); 1349 } 1350 1351 function setCursorTrailPickerValue(value) { 1352 if (window.PupwifeTrailPicker && typeof window.PupwifeTrailPicker.setValue === 'function') { 1353 window.PupwifeTrailPicker.setValue(value); 1354 } 1355 } 1356 1357 function setCursorTrailPickerDisabled(disabled) { 1358 if (window.PupwifeTrailPicker && typeof window.PupwifeTrailPicker.setDisabled === 'function') { 1359 window.PupwifeTrailPicker.setDisabled(disabled); 1360 } 1361 } 1362 1363 function ensureCursorPickerScript() { 1364 return loadScriptOnce('/scripts/effects/settingsBubbleSelect.js', 'bubble-select') 1365 .then(function () { 1366 return new Promise(function (resolve) { 1367 if (window.PupwifeCursorPicker) { 1368 resolve(); 1369 return; 1370 } 1371 var existing = document.querySelector('script[data-cursor-picker]'); 1372 if (existing) { 1373 if (existing.getAttribute('data-cursor-picker-ready') === '1') { 1374 resolve(); 1375 return; 1376 } 1377 existing.addEventListener('load', resolve, { once: true }); 1378 existing.addEventListener('error', resolve, { once: true }); 1379 return; 1380 } 1381 var script = document.createElement('script'); 1382 script.src = '/scripts/effects/cursorPicker.js'; 1383 script.setAttribute('data-cursor-picker', '1'); 1384 script.onload = script.onerror = function () { 1385 script.setAttribute('data-cursor-picker-ready', '1'); 1386 resolve(); 1387 }; 1388 document.head.appendChild(script); 1389 }); 1390 }); 1391 } 1392 1393 function mountCustomCursorPicker(panel) { 1394 var mountEl = panel.querySelector('#setting-customCursor-picker'); 1395 if (!mountEl || !window.PupwifeCursorPicker) return; 1396 window.PupwifeCursorPicker.mount(mountEl, { 1397 value: currentSettings.customCursor, 1398 disabled: false, 1399 onChange: function (value) { 1400 setSetting('customCursor', value); 1401 updateCatPawPanelVisibility(); 1402 } 1403 }); 1404 } 1405 1406 function setCustomCursorPickerValue(value) { 1407 if (window.PupwifeCursorPicker && typeof window.PupwifeCursorPicker.setValue === 'function') { 1408 window.PupwifeCursorPicker.setValue(value); 1409 } 1410 } 1411 1412 function setCustomCursorPickerDisabled(disabled) { 1413 if (window.PupwifeCursorPicker && typeof window.PupwifeCursorPicker.setDisabled === 'function') { 1414 window.PupwifeCursorPicker.setDisabled(disabled); 1415 } 1416 } 1417 1418 function ensurePalettePickerScript() { 1419 return loadScriptOnce('/scripts/effects/siteThemePalettes.js', 'site-theme-palettes') 1420 .then(function () { 1421 return loadScriptOnce('/scripts/effects/settingsBubbleSelect.js', 'bubble-select'); 1422 }) 1423 .then(function () { 1424 return new Promise(function (resolve) { 1425 if (window.PupwifePalettePicker) { 1426 resolve(); 1427 return; 1428 } 1429 var existing = document.querySelector('script[data-palette-picker]'); 1430 if (existing) { 1431 if (existing.getAttribute('data-palette-picker-ready') === '1') { 1432 resolve(); 1433 return; 1434 } 1435 existing.addEventListener('load', resolve, { once: true }); 1436 existing.addEventListener('error', resolve, { once: true }); 1437 return; 1438 } 1439 var script = document.createElement('script'); 1440 script.src = '/scripts/effects/palettePicker.js'; 1441 script.setAttribute('data-palette-picker', '1'); 1442 script.onload = script.onerror = function () { 1443 script.setAttribute('data-palette-picker-ready', '1'); 1444 resolve(); 1445 }; 1446 document.head.appendChild(script); 1447 }); 1448 }); 1449 } 1450 1451 function mountPalettePicker(panel) { 1452 var mountEl = panel.querySelector('#setting-palette-picker'); 1453 if (!mountEl || !window.PupwifePalettePicker) return; 1454 var palette = currentSettings.palette || DEFAULT_SETTINGS.palette; 1455 if (window.SiteThemeToggle && typeof window.SiteThemeToggle.readPaletteSetting === 'function') { 1456 palette = window.SiteThemeToggle.readPaletteSetting(); 1457 } 1458 window.PupwifePalettePicker.mount(mountEl, { 1459 value: palette, 1460 onChange: handlePaletteChange 1461 }); 1462 } 1463 1464 function setPalettePickerValue(value) { 1465 if (window.PupwifePalettePicker && typeof window.PupwifePalettePicker.setValue === 'function') { 1466 window.PupwifePalettePicker.setValue(value); 1467 } 1468 } 1469
1470 function ensureBackgroundPickerScript() { 1471 return loadScriptOnce('/scripts/effects/siteBackgrounds.js', 'site-backgrounds') 1472 .then(function () { 1473 return loadScriptOnce('/scripts/effects/siteBackgroundApply.js', 'site-background-apply'); 1474 }) 1475 .then(function () { 1476 return loadScriptOnce('/scripts/effects/settingsBubbleSelect.js', 'bubble-select'); 1477 }) 1478 .then(function () { 1479 return new Promise(function (resolve) { 1480 if (window.PupwifeBackgroundPicker) { 1481 resolve(); 1482 return; 1483 } 1484 var existing = document.querySelector('script[data-background-picker]'); 1485 if (existing) { 1486 if (existing.getAttribute('data-background-picker-ready') === '1') { 1487 resolve(); 1488 return; 1489 } 1490 existing.addEventListener('load', resolve, { once: true }); 1491 existing.addEventListener('error', resolve, { once: true }); 1492 return; 1493 } 1494 var script = document.createElement('script'); 1495 script.src = '/scripts/effects/backgroundPicker.js'; 1496 script.setAttribute('data-background-picker', '1'); 1497 script.onload = script.onerror = function () { 1498 script.setAttribute('data-background-picker-ready', '1'); 1499 resolve(); 1500 }; 1501 document.head.appendChild(script); 1502 }); 1503 }) 1504 .then(function () { 1505 return loadScriptOnce('/scripts/effects/backgroundTuning.js', 'background-tuning'); 1506 }); 1507 } 1508 1509 var backgroundTuningUi = null; 1510 1511 function mountBackgroundTuning(panel) { 1512 if (!window.SiteBackgroundTuning) return; 1513 var host = panel.querySelector('#setting-background-tuning-body'); 1514 if (!host) return; 1515 window.SiteBackgroundTuning.bindCollapsible(panel); 1516 backgroundTuningUi = window.SiteBackgroundTuning.mount(host, { 1517 tuning: currentSettings.backgroundTuning, 1518 activeBackground: currentSettings.background || DEFAULT_SETTINGS.background, 1519 onChange: function (tuning, changedPresetId) { 1520 currentSettings.backgroundTuning = window.SiteBackgroundTuning.saveTuning(tuning, changedPresetId); 1521 } 1522 }); 1523 } 1524 1525 function setBackgroundTuningActive(id) { 1526 if (backgroundTuningUi && typeof backgroundTuningUi.setActiveBackground === 'function') { 1527 backgroundTuningUi.setActiveBackground(id); 1528 } 1529 } 1530 1531 function updateCloudsBackgroundWarning() { 1532 var panel = document.getElementById('settings-panel'); 1533 if (!panel) return; 1534 var warning = panel.querySelector('#setting-background-clouds-warning'); 1535 if (!warning) return; 1536 var bg = currentSettings.background || DEFAULT_SETTINGS.background; 1537 var wantsClouds = bg === 'clouds'; 1538 var unsupported = window.SiteBackgroundApply && 1539 typeof window.SiteBackgroundApply.webgl2Supported === 'function' && 1540 !window.SiteBackgroundApply.webgl2Supported(); 1541 var fallback = window.SiteBackgroundApply && 1542 typeof window.SiteBackgroundApply.isCloudsFallbackActive === 'function' && 1543 window.SiteBackgroundApply.isCloudsFallbackActive(); 1544 warning.style.display = (wantsClouds && (unsupported || fallback)) ? '' : 'none'; 1545 } 1546 1547 function mountBackgroundPicker(panel) { 1548 var mountEl = panel.querySelector('#setting-background-picker'); 1549 if (!mountEl || !window.PupwifeBackgroundPicker) return; 1550 window.PupwifeBackgroundPicker.mount(mountEl, { 1551 value: currentSettings.background || DEFAULT_SETTINGS.background, 1552 disabled: false, 1553 onChange: function (value) { 1554 setSetting('background', value);
1555 setBackgroundTuningActive(value); 1556 updateCloudsBackgroundWarning(); 1557 } 1558 }); 1559 updateCloudsBackgroundWarning(); 1560 } 1561 1562 function setBackgroundPickerValue(value) { 1563 if (window.PupwifeBackgroundPicker && typeof window.PupwifeBackgroundPicker.setValue === 'function') { 1564 window.PupwifeBackgroundPicker.setValue(value); 1565 } 1566 } 1567 1568 function handlePaletteChange(value) { 1569 currentSettings.palette = value; 1570 if (window.SiteThemeToggle && typeof window.SiteThemeToggle.setPalette === 'function') { 1571 window.SiteThemeToggle.setPalette(value, 'settings'); 1572 } else if (window.SiteThemePalettes && typeof window.SiteThemePalettes.writeMergedThemeSettings === 'function') { 1573 window.SiteThemePalettes.writeMergedThemeSettings({ palette: value }); 1574 } else { 1575 setSetting('palette', value); 1576 return; 1577 } 1578 saveSettings(); 1579 emitDevLog('Color palette: ' + value); 1580 } 1581 1582 function loadScriptOnce(src, attr) { 1583 return new Promise(function (resolve) { 1584 if (attr === 'site-backgrounds' && window.SiteBackgrounds) { 1585 resolve(); 1586 return; 1587 } 1588 if (attr === 'site-background-apply' && window.SiteBackgroundApply) { 1589 resolve(); 1590 return; 1591 } 1592 if (attr === 'site-theme-palettes' && window.SiteThemePalettes) { 1593 resolve(); 1594 return; 1595 } 1596 if (attr === 'bubble-select' && window.PupwifeSettingsBubbleSelect) { 1597 resolve(); 1598 return; 1599 } 1600 if (attr === 'background-tuning' && window.SiteBackgroundTuning) { 1601 resolve(); 1602 return; 1603 } 1604 if (attr === 'color-picker' && window.PupwifeSettingsColorPicker) { 1605 resolve(); 1606 return; 1607 } 1608 if (attr === 'woim-color-panel' && window.PupwifeWoimColorPanel) { 1609 resolve(); 1610 return; 1611 } 1612 if (attr === 'discoverability' && window.PupwifeSettingsDiscoverability) { 1613 resolve(); 1614 return; 1615 } 1616 if (attr === 'trail-colors' && window.PupwifeTrailColorSupport) { 1617 resolve(); 1618 return; 1619 } 1620 if (attr === 'search' && window.PupwifeSettingsSearch) { 1621 resolve(); 1622 return; 1623 } 1624 if (attr === 'export-import' && window.PupwifeSettingsExportImport) { 1625 resolve(); 1626 return; 1627 } 1628 var existing = document.querySelector('script[data-settings-' + attr + ']'); 1629 if (existing) { 1630 existing.addEventListener('load', resolve, { once: true }); 1631 existing.addEventListener('error', resolve, { once: true }); 1632 return; 1633 } 1634 var script = document.createElement('script'); 1635 script.src = src; 1636 script.setAttribute('data-settings-' + attr, '1'); 1637 script.onload = script.onerror = resolve; 1638 document.head.appendChild(script); 1639 }); 1640 } 1641 1642 function ensureWoimColorPanelScript() { 1643 return loadScriptOnce('/scripts/effects/settingsBubbleSelect.js', 'bubble-select') 1644 .then(function () { 1645 return loadScriptOnce('/scripts/effects/settingsColorPicker.js', 'color-picker'); 1646 }) 1647 .then(function () { 1648 return loadScriptOnce('/scripts/effects/woimColorPanel.js', 'woim-color-panel'); 1649 }); 1650 } 1651 1652 function updateWoimPanelVisibility(followerType) { 1653 var panel = document.getElementById('woim-string-settings'); 1654 if (panel) { 1655 panel.classList.toggle('is-visible', followerType === 'woimString'); 1656 } 1657 } 1658 1659 function bindWoimCollapsible(panel) { 1660 var woimPanel = panel.querySelector('#woim-string-settings'); 1661 var toggle = panel.querySelector('#woim-string-settings-toggle'); 1662 var body = panel.querySelector('#woim-string-settings-body'); 1663 if (!woimPanel || !toggle || !body || woimPanel.dataset.collapseBound === '1') return; 1664 woimPanel.dataset.collapseBound = '1'; 1665 1666 var collapsed = true; 1667 try { 1668 var stored = localStorage.getItem('pupwife_woim_settings_collapsed'); 1669 if (stored === '0') collapsed = false;
1670 else if (stored === '1') collapsed = true; 1671 } catch (e) { /* ignore */ } 1672 1673 function applyCollapsed(state) { 1674 collapsed = !!state; 1675 woimPanel.classList.toggle('is-collapsed', collapsed); 1676 toggle.setAttribute('aria-expanded', collapsed ? 'false' : 'true'); 1677 try { 1678 localStorage.setItem('pupwife_woim_settings_collapsed', collapsed ? '1' : '0'); 1679 } catch (e) { /* ignore */ } 1680 } 1681 1682 applyCollapsed(collapsed); 1683 1684 toggle.addEventListener('click', function () { 1685 applyCollapsed(!collapsed); 1686 }); 1687 } 1688 1689 function bindWoimSlider(panel, inputId, labelId, key, formatLabel) { 1690 var input = panel.querySelector('#' + inputId); 1691 var label = panel.querySelector('#' + labelId); 1692 if (!input) return; 1693 1694 function readValue() { 1695 return parseWoimNotch(input.value, parseInt(input.min, 10), parseInt(input.max, 10), input.value); 1696 } 1697 1698 function applyLive(v) { 1699 if (window.WoimStringFollower && typeof window.WoimStringFollower.applySettings === 'function') { 1700 var live = mergeWoimString(currentSettings.woimString); 1701 live[key] = v; 1702 window.WoimStringFollower.applySettings(live); 1703 } 1704 } 1705 1706 input.addEventListener('input', function () { 1707 var v = readValue(); 1708 applyLive(v); 1709 if (label) label.textContent = formatLabel ? formatLabel(v) : String(v); 1710 }); 1711 input.addEventListener('change', function () { 1712 var v = readValue(); 1713 input.value = String(v); 1714 var patch = {}; 1715 patch[key] = v; 1716 setWoimString(patch); 1717 if (label) label.textContent = formatLabel ? formatLabel(v) : String(v); 1718 }); 1719 } 1720 1721 function syncWoimSettingsUI(panel) { 1722 if (!panel) return; 1723 var w = mergeWoimString(currentSettings.woimString); 1724 var map = [ 1725 ['setting-woim-ropePoints', 'setting-woim-ropePoints-label', w.ropePoints, null], 1726 ['setting-woim-bodyPoints', 'setting-woim-bodyPoints-label', w.bodyPoints, null], 1727 ['setting-woim-size', 'setting-woim-size-label', w.size, function (v) { return v + '%'; }], 1728 ['setting-woim-ropeLength', 'setting-woim-ropeLength-label', w.ropeLength, function (v) { return v + '%'; }], 1729 ['setting-woim-ropeThickness', 'setting-woim-ropeThickness-label', w.ropeThickness, function (v) { return v + '%'; }], 1730 ['setting-woim-anchorSize', 'setting-woim-anchorSize-label', w.anchorSize, function (v) { return v + '%'; }], 1731 ['setting-woim-anchorThickness', 'setting-woim-anchorThickness-label', w.anchorThickness, function (v) { return v + '%'; }] 1732 ]; 1733 map.forEach(function (row) { 1734 var input = panel.querySelector('#' + row[0]); 1735 var label = panel.querySelector('#' + row[1]); 1736 if (input) input.value = String(row[2]); 1737 if (label) label.textContent = row[3] ? row[3](row[2]) : String(row[2]); 1738 }); 1739 var anchorShow = panel.querySelector('#setting-woim-anchorShow'); 1740 if (anchorShow) { 1741 anchorShow.checked = w.anchorShow === true; 1742 } 1743 if (woimColorPanels.rope) woimColorPanels.rope.setValue(w.rope); 1744 if (woimColorPanels.woim) woimColorPanels.woim.setValue(w.woim); 1745 updateWoimPanelVisibility(currentSettings.follower); 1746 } 1747 1748 function mountWoimSettingsPanel(panel) { 1749 if (!panel || !window.PupwifeWoimColorPanel) return; 1750 var woimPanel = panel.querySelector('#woim-string-settings'); 1751 if (!woimPanel || woimPanel.dataset.mounted === '1') { 1752 syncWoimSettingsUI(panel); 1753 return; 1754 } 1755 woimPanel.dataset.mounted = '1'; 1756 1757 bindWoimCollapsible(panel); 1758 1759 bindWoimSlider(panel, 'setting-woim-ropePoints', 'setting-woim-ropePoints-label', 'ropePoints'); 1760 bindWoimSlider(panel, 'setting-woim-bodyPoints', 'setting-woim-bodyPoints-label', 'bodyPoints'); 1761 bindWoimSlider(panel, 'setting-woim-size', 'setting-woim-size-label', 'size', function (v) { return v + '%'; }); 1762 bindWoimSlider(panel, 'setting-woim-ropeLength', 'setting-woim-ropeLength-label', 'ropeLength', function (v) { return v + '%'; }); 1763 bindWoimSlider(panel, 'setting-woim-ropeThickness', 'setting-woim-ropeThickness-label', 'ropeThickness', function (v) { return v + '%'; }); 1764 bindWoimSlider(panel, 'setting-woim-anchorSize', 'setting-woim-anchorSize-label', 'anchorSize', function (v) { return v + '%'; }); 1765 bindWoimSlider(panel, 'setting-woim-anchorThickness', 'setting-woim-anchorThickness-label', 'anchorThickness', function (v) { return v + '%'; }); 1766 1767 var anchorShow = panel.querySelector('#setting-woim-anchorShow'); 1768 if (anchorShow) { 1769 anchorShow.addEventListener('change', function () { 1770 setWoimString({ anchorShow: !!anchorShow.checked }); 1771 }); 1772 } 1773 1774 var w = mergeWoimString(currentSettings.woimString); 1775 var ropeMount = panel.querySelector('#woim-rope-color-mount'); 1776 var bodyMount = panel.querySelector('#woim-body-color-mount'); 1777 if (ropeMount) { 1778 woimColorPanels.rope = window.PupwifeWoimColorPanel.mount(ropeMount, { 1779 idPrefix: 'woim-rope-color', 1780 value: w.rope, 1781 onChange: function (val) { setWoimString({ rope: val }); } 1782 }); 1783 } 1784 if (bodyMount) { 1785 woimColorPanels.woim = window.PupwifeWoimColorPanel.mount(bodyMount, { 1786 idPrefix: 'woim-body-color', 1787 value: w.woim, 1788 onChange: function (val) { setWoimString({ woim: val }); } 1789 }); 1790 } 1791 1792 syncWoimSettingsUI(panel); 1793 } 1794 1795 function updateJellypuffPanelVisibility(followerType) {
1796 var panel = document.getElementById('jellypuff-settings'); 1797 if (panel) { 1798 panel.classList.toggle('is-visible', followerType === 'jellypuff'); 1799 } 1800 } 1801 1802 function bindJellypuffCollapsible(panel) { 1803 var jellyPanel = panel.querySelector('#jellypuff-settings'); 1804 var toggle = panel.querySelector('#jellypuff-settings-toggle'); 1805 var body = panel.querySelector('#jellypuff-settings-body'); 1806 if (!jellyPanel || !toggle || !body || jellyPanel.dataset.collapseBound === '1') return; 1807 jellyPanel.dataset.collapseBound = '1'; 1808 1809 var collapsed = true; 1810 try { 1811 var stored = localStorage.getItem('pupwife_jellypuff_settings_collapsed'); 1812 if (stored === '0') collapsed = false; 1813 else if (stored === '1') collapsed = true; 1814 } catch (e) { /* ignore */ } 1815 1816 function applyCollapsed(state) { 1817 collapsed = !!state; 1818 jellyPanel.classList.toggle('is-collapsed', collapsed); 1819 toggle.setAttribute('aria-expanded', collapsed ? 'false' : 'true'); 1820 try { 1821 localStorage.setItem('pupwife_jellypuff_settings_collapsed', collapsed ? '1' : '0'); 1822 } catch (e) { /* ignore */ } 1823 } 1824 1825 applyCollapsed(collapsed); 1826 1827 toggle.addEventListener('click', function () { 1828 applyCollapsed(!collapsed); 1829 }); 1830 } 1831 1832 function bindJellypuffSlider(panel, inputId, labelId, key, formatLabel) { 1833 var input = panel.querySelector('#' + inputId); 1834 var label = panel.querySelector('#' + labelId); 1835 if (!input) return; 1836 1837 function readValue() { 1838 return parseWoimNotch(input.value, parseInt(input.min, 10), parseInt(input.max, 10), input.value); 1839 } 1840 1841 function applyLive(v) { 1842 if (window.JellypuffFollower && typeof window.JellypuffFollower.applySettings === 'function') { 1843 var live = mergeJellypuff(currentSettings.jellypuff); 1844 live[key] = v; 1845 window.JellypuffFollower.applySettings(live); 1846 } 1847 } 1848 1849 input.addEventListener('input', function () { 1850 var v = readValue(); 1851 applyLive(v); 1852 if (label) label.textContent = formatLabel ? formatLabel(v) : String(v); 1853 }); 1854 input.addEventListener('change', function () { 1855 var v = readValue(); 1856 input.value = String(v); 1857 var patch = {}; 1858 patch[key] = v; 1859 setJellypuff(patch); 1860 if (label) label.textContent = formatLabel ? formatLabel(v) : String(v); 1861 }); 1862 } 1863 1864 function syncJellypuffSettingsUI(panel) { 1865 if (!panel) return; 1866 var j = mergeJellypuff(currentSettings.jellypuff); 1867 var map = [ 1868 ['setting-jellypuff-size', 'setting-jellypuff-size-label', j.size, function (v) { return v + '%'; }], 1869 ['setting-jellypuff-pulseRate', 'setting-jellypuff-pulseRate-label', j.pulseRate, function (v) { return v + '%'; }], 1870 ['setting-jellypuff-tentacleDensity', 'setting-jellypuff-tentacleDensity-label', j.tentacleDensity, function (v) { return v + '%'; }], 1871 ['setting-jellypuff-glowStrength', 'setting-jellypuff-glowStrength-label', j.glowStrength, function (v) { return v + '%'; }] 1872 ]; 1873 map.forEach(function (row) { 1874 var input = panel.querySelector('#' + row[0]); 1875 var label = panel.querySelector('#' + row[1]); 1876 if (input) input.value = String(row[2]); 1877 if (label) label.textContent = row[3] ? row[3](row[2]) : String(row[2]); 1878 }); 1879 if (jellyColorPanels.bell) jellyColorPanels.bell.setValue(j.bell); 1880 if (jellyColorPanels.tentacles) jellyColorPanels.tentacles.setValue(j.tentacles); 1881 updateJellypuffPanelVisibility(currentSettings.follower); 1882 } 1883 1884 function mountJellypuffSettingsPanel(panel) { 1885 if (!panel || !window.PupwifeWoimColorPanel) return; 1886 var jellyPanel = panel.querySelector('#jellypuff-settings'); 1887 if (!jellyPanel || jellyPanel.dataset.mounted === '1') { 1888 syncJellypuffSettingsUI(panel); 1889 return; 1890 } 1891 jellyPanel.dataset.mounted = '1'; 1892 1893 bindJellypuffCollapsible(panel); 1894 1895 bindJellypuffSlider(panel, 'setting-jellypuff-size', 'setting-jellypuff-size-label', 'size', function (v) { return v + '%'; }); 1896 bindJellypuffSlider(panel, 'setting-jellypuff-pulseRate', 'setting-jellypuff-pulseRate-label', 'pulseRate', function (v) { return v + '%'; }); 1897 bindJellypuffSlider(panel, 'setting-jellypuff-tentacleDensity', 'setting-jellypuff-tentacleDensity-label', 'tentacleDensity', function (v) { return v + '%'; }); 1898 bindJellypuffSlider(panel, 'setting-jellypuff-glowStrength', 'setting-jellypuff-glowStrength-label', 'glowStrength', function (v) { return v + '%'; }); 1899 1900 var j = mergeJellypuff(currentSettings.jellypuff); 1901 var bellMount = panel.querySelector('#jellypuff-bell-color-mount'); 1902 var tentMount = panel.querySelector('#jellypuff-tentacles-color-mount'); 1903 if (bellMount) { 1904 jellyColorPanels.bell = window.PupwifeWoimColorPanel.mount(bellMount, { 1905 idPrefix: 'jellypuff-bell-color', 1906 value: j.bell, 1907 onChange: function (val) { setJellypuff({ bell: val }); } 1908 }); 1909 } 1910 if (tentMount) { 1911 jellyColorPanels.tentacles = window.PupwifeWoimColorPanel.mount(tentMount, { 1912 idPrefix: 'jellypuff-tentacles-color', 1913 value: j.tentacles, 1914 onChange: function (val) { setJellypuff({ tentacles: val }); } 1915 }); 1916 } 1917 1918 syncJellypuffSettingsUI(panel); 1919 } 1920 1921 function updateCatPawPanelVisibility() { 1922 var panel = document.getElementById('cat-paw-arm-settings'); 1923 if (panel) { 1924 panel.classList.toggle('is-visible', currentSettings.customCursor === 'cat-paw-arm'); 1925 } 1926 } 1927 1928 function bindCatPawCollapsible(panel) { 1929 var pawPanel = panel.querySelector('#cat-paw-arm-settings'); 1930 var toggle = panel.querySelector('#cat-paw-arm-settings-toggle'); 1931 if (!pawPanel || !toggle || pawPanel.dataset.collapseBound === '1') return; 1932 pawPanel.dataset.collapseBound = '1'; 1933 1934 var collapsed = true; 1935 try { 1936 var stored = localStorage.getItem('pupwife_cat_paw_settings_collapsed'); 1937 if (stored === '0') collapsed = false;
1938 else if (stored === '1') collapsed = true; 1939 } catch (e) { /* ignore */ } 1940 1941 function applyCollapsed(state) { 1942 collapsed = !!state; 1943 pawPanel.classList.toggle('is-collapsed', collapsed); 1944 toggle.setAttribute('aria-expanded', collapsed ? 'false' : 'true'); 1945 try { 1946 localStorage.setItem('pupwife_cat_paw_settings_collapsed', collapsed ? '1' : '0'); 1947 } catch (e) { /* ignore */ } 1948 } 1949 1950 applyCollapsed(collapsed); 1951 toggle.addEventListener('click', function () { 1952 applyCollapsed(!collapsed); 1953 }); 1954 } 1955 1956 function bindCatPawSlider(panel, inputId, labelId, key, formatLabel) { 1957 var input = panel.querySelector('#' + inputId); 1958 var label = panel.querySelector('#' + labelId); 1959 if (!input) return; 1960 1961 function readValue() { 1962 var min = parseFloat(input.min); 1963 var max = parseFloat(input.max); 1964 var n = parseFloat(input.value); 1965 if (Number.isNaN(n)) return input.value; 1966 if (min < 0 || String(input.step).indexOf('.') >= 0) { 1967 return Math.max(min, Math.min(max, n)); 1968 } 1969 return parseWoimNotch(input.value, min, max, input.value); 1970 } 1971 1972 function applyLive(v) { 1973 if (window.CatPawArmCursor && typeof window.CatPawArmCursor.applySettings === 'function') { 1974 var live = mergeCatPawArm(currentSettings.catPawArm); 1975 live[key] = v; 1976 if (key === 'pawSize') live.armThickness = v; 1977 window.CatPawArmCursor.applySettings(live); 1978 } 1979 } 1980 1981 input.addEventListener('input', function () { 1982 var v = readValue(); 1983 applyLive(v); 1984 if (label) label.textContent = formatLabel ? formatLabel(v) : String(v); 1985 }); 1986 input.addEventListener('change', function () { 1987 var v = readValue(); 1988 input.value = String(v); 1989 var patch = {}; 1990 patch[key] = v; 1991 if (key === 'pawSize') { 1992 patch.armThickness = v; 1993 } 1994 if (key === 'originX' || key === 'originY') { 1995 patch.anchorPreset = 'custom'; 1996 } 1997 setCatPawArm(patch); 1998 if (label) label.textContent = formatLabel ? formatLabel(v) : String(v); 1999 if (key === 'originX' || key === 'originY') { 2000 if (catPawAnchorSelect) catPawAnchorSelect.setValue('custom'); 2001 } 2002 }); 2003 } 2004 2005 function syncCatPawSettingsUI(panel) { 2006 if (!panel) return; 2007 var c = mergeCatPawArm(currentSettings.catPawArm); 2008 var map = [ 2009 ['setting-catpaw-originX', 'setting-catpaw-originX-label', c.originX, function (v) { return v + '%'; }], 2010 ['setting-catpaw-originY', 'setting-catpaw-originY-label', c.originY, function (v) { return v + '%'; }], 2011 ['setting-catpaw-armTaper', 'setting-catpaw-armTaper-label', c.armTaper, function (v) { return v + '%'; }], 2012 ['setting-catpaw-armBend', 'setting-catpaw-armBend-label', c.armBend, function (v) { return v + '%'; }], 2013 ['setting-catpaw-followLag', 'setting-catpaw-followLag-label', c.followLag, function (v) { return v + '%'; }], 2014 ['setting-catpaw-pawSize', 'setting-catpaw-pawSize-label', c.pawSize, function (v) { return v + '%'; }], 2015 ['setting-catpaw-padSize', 'setting-catpaw-padSize-label', c.padSize, function (v) { return v + '%'; }], 2016 ['setting-catpaw-printSize', 'setting-catpaw-printSize-label', c.printSize, function (v) { return v + '%'; }], 2017 ['setting-catpaw-printFade', 'setting-catpaw-printFade-label', c.printFade, function (v) { return v + '%'; }], 2018 ['setting-catpaw-printMax', 'setting-catpaw-printMax-label', c.printMax, null], 2019 ['setting-catpaw-outlineThickness', 'setting-catpaw-outlineThickness-label', c.outlineThickness, function (v) { return v + '%'; }] 2020 ]; 2021 map.forEach(function (row) { 2022 var input = panel.querySelector('#' + row[0]); 2023 var label = panel.querySelector('#' + row[1]);
2024 if (input) input.value = String(row[2]); 2025 if (label) label.textContent = row[3] ? row[3](row[2]) : String(row[2]); 2026 }); 2027 2028 var toggles = [ 2029 ['setting-catpaw-clickSquish', 'clickSquish'], 2030 ['setting-catpaw-showBeans', 'showBeans'], 2031 ['setting-catpaw-printsEnabled', 'printsEnabled'], 2032 ['setting-catpaw-outlineShow', 'outlineShow'] 2033 ]; 2034 toggles.forEach(function (row) { 2035 var el = panel.querySelector('#' + row[0]); 2036 if (el) el.checked = c[row[1]] === true; 2037 }); 2038 2039 if (catPawSpeciesSelect) catPawSpeciesSelect.setValue(c.pawSpecies); 2040 if (catPawPresetSelect) catPawPresetSelect.setValue(c.pawPreset || 'classic-white'); 2041 if (catPawAnchorSelect) { 2042 var anchorVal = c.anchorPreset === 'center' ? 'bottom-center' : c.anchorPreset; 2043 catPawAnchorSelect.setValue(anchorVal); 2044 if (c.anchorPreset === 'center') { 2045 setCatPawArm({ anchorPreset: 'bottom-center' }, { syncUi: false }); 2046 } 2047 } 2048 Object.keys(catPawColorPanels).forEach(function (key) { 2049 if (!catPawColorPanels[key]) return; 2050 if (key === 'spriteFur') { 2051 catPawColorPanels.spriteFur.setValue(c.pawFur); 2052 } else { 2053 catPawColorPanels[key].setValue(c[key]); 2054 } 2055 }); 2056 updateCatPawPanelVisibility(); 2057 updateCatPawArtModeUI(panel); 2058 } 2059 2060 function mountCatPawSettingsPanel(panel) { 2061 if (!panel) return; 2062 var pawPanel = panel.querySelector('#cat-paw-arm-settings'); 2063 if (!pawPanel) return; 2064 2065 bindCatPawCollapsible(panel); 2066 2067 if (pawPanel.dataset.mounted !== '1') { 2068 pawPanel.dataset.mounted = '1'; 2069 2070 bindCatPawSlider(panel, 'setting-catpaw-originX', 'setting-catpaw-originX-label', 'originX', function (v) { return v + '%'; }); 2071 bindCatPawSlider(panel, 'setting-catpaw-originY', 'setting-catpaw-originY-label', 'originY', function (v) { return v + '%'; }); 2072 bindCatPawSlider(panel, 'setting-catpaw-armTaper', 'setting-catpaw-armTaper-label', 'armTaper', function (v) { return v + '%'; }); 2073 bindCatPawSlider(panel, 'setting-catpaw-armBend', 'setting-catpaw-armBend-label', 'armBend', function (v) { return v + '%'; }); 2074 bindCatPawSlider(panel, 'setting-catpaw-followLag', 'setting-catpaw-followLag-label', 'followLag', function (v) { return v + '%'; }); 2075 bindCatPawSlider(panel, 'setting-catpaw-pawSize', 'setting-catpaw-pawSize-label', 'pawSize', function (v) { return v + '%'; }); 2076 bindCatPawSlider(panel, 'setting-catpaw-padSize', 'setting-catpaw-padSize-label', 'padSize', function (v) { return v + '%'; }); 2077 bindCatPawSlider(panel, 'setting-catpaw-printSize', 'setting-catpaw-printSize-label', 'printSize', function (v) { return v + '%'; }); 2078 bindCatPawSlider(panel, 'setting-catpaw-printFade', 'setting-catpaw-printFade-label', 'printFade', function (v) { return v + '%'; }); 2079 bindCatPawSlider(panel, 'setting-catpaw-printMax', 'setting-catpaw-printMax-label', 'printMax'); 2080 bindCatPawSlider(panel, 'setting-catpaw-outlineThickness', 'setting-catpaw-outlineThickness-label', 'outlineThickness', function (v) { return v + '%'; }); 2081 2082 [ 2083 ['setting-catpaw-clickSquish', 'clickSquish'], 2084 ['setting-catpaw-showBeans', 'showBeans'], 2085 ['setting-catpaw-printsEnabled', 'printsEnabled'], 2086 ['setting-catpaw-outlineShow', 'outlineShow'] 2087 ].forEach(function (row) { 2088 var el = panel.querySelector('#' + row[0]); 2089 if (el) { 2090 el.addEventListener('change', function () { 2091 var patch = {}; 2092 patch[row[1]] = !!el.checked; 2093 setCatPawArm(patch); 2094 }); 2095 } 2096 }); 2097 2098 var presetMount = panel.querySelector('#catpaw-preset-mount'); 2099 if (presetMount && window.PupwifeSettingsBubbleSelect) { 2100 catPawPresetSelect = window.PupwifeSettingsBubbleSelect.create(presetMount, { 2101 idPrefix: 'catpaw-preset', 2102 ariaLabel: 'Paw style', 2103 options: getCatPawPresetOptions(), 2104 value: mergeCatPawArm(currentSettings.catPawArm).pawPreset || 'classic-white', 2105 onChange: function (val) { 2106 var patch = { pawPreset: val }; 2107 var presetDef = window.CatPawArmCursor && window.CatPawArmCursor.PAW_PRESETS 2108 ? window.CatPawArmCursor.PAW_PRESETS[val] 2109 : null; 2110 if (presetDef && presetDef.species) patch.pawSpecies = presetDef.species; 2111 if (presetDef && presetDef.defaultFur) { 2112 patch.pawFur = Object.assign({}, mergeCatPawArm(currentSettings.catPawArm).pawFur, { 2113 colorMode: 'solid', 2114 solidColor: presetDef.defaultFur 2115 }); 2116 patch.arm = Object.assign({}, patch.pawFur); 2117 } 2118 setCatPawArm(patch); 2119 updateCatPawArtModeUI(panel); 2120 } 2121 }); 2122 } 2123 2124 var speciesMount = panel.querySelector('#catpaw-species-mount'); 2125 if (speciesMount && window.PupwifeSettingsBubbleSelect) { 2126 catPawSpeciesSelect = window.PupwifeSettingsBubbleSelect.create(speciesMount, { 2127 idPrefix: 'catpaw-species', 2128 ariaLabel: 'Paw species', 2129 options: CAT_PAW_SPECIES_OPTIONS, 2130 value: mergeCatPawArm(currentSettings.catPawArm).pawSpecies, 2131 onChange: function (val) { 2132 setCatPawArm({ 2133 pawSpecies: val === 'dog' ? 'dog' : 'cat', 2134 pawPreset: val === 'dog' ? '' : 'classic-white' 2135 }); 2136 updateCatPawArtModeUI(panel); 2137 } 2138 }); 2139 } 2140 2141 var anchorMount = panel.querySelector('#catpaw-anchor-mount'); 2142 if (anchorMount && window.PupwifeSettingsBubbleSelect) { 2143 catPawAnchorSelect = window.PupwifeSettingsBubbleSelect.create(anchorMount, { 2144 idPrefix: 'catpaw-anchor', 2145 ariaLabel: 'Anchor preset', 2146 options: CAT_PAW_ANCHOR_OPTIONS, 2147 value: mergeCatPawArm(currentSettings.catPawArm).anchorPreset, 2148 onChange: function (val) { 2149 if (val === 'custom') { 2150 setCatPawArm({ anchorPreset: 'custom' }); 2151 return; 2152 } 2153 var snap = snapCatPawAnchorPreset(val); 2154 if (snap) setCatPawArm(snap); 2155 } 2156 }); 2157 } 2158 2159 var colorMounts = [ 2160 ['catpaw-arm-color-mount', 'arm'], 2161 ['catpaw-fur-color-mount', 'pawFur'], 2162 ['catpaw-pads-color-mount', 'pawPads'], 2163 ['catpaw-outline-color-mount', 'outline'], 2164 ['catpaw-prints-color-mount', 'prints'] 2165 ]; 2166 var c0 = mergeCatPawArm(currentSettings.catPawArm); 2167 if (window.PupwifeWoimColorPanel) {
2168 colorMounts.forEach(function (row) { 2169 var mount = panel.querySelector('#' + row[0]); 2170 if (!mount) return; 2171 catPawColorPanels[row[1]] = window.PupwifeWoimColorPanel.mount(mount, { 2172 idPrefix: 'catpaw-' + row[1] + '-color', 2173 value: c0[row[1]], 2174 onChange: (function (colorKey) { 2175 return function (val) { 2176 var patch = {}; 2177 patch[colorKey] = val; 2178 setCatPawArm(patch); 2179 }; 2180 })(row[1]) 2181 }); 2182 }); 2183 var spriteFurMount = panel.querySelector('#catpaw-sprite-fur-color-mount'); 2184 if (spriteFurMount) { 2185 catPawColorPanels.spriteFur = window.PupwifeWoimColorPanel.mount(spriteFurMount, { 2186 idPrefix: 'catpaw-sprite-fur-color', 2187 value: c0.pawFur, 2188 onChange: function (val) { 2189 setCatPawArm({ pawFur: val, arm: val }); 2190 } 2191 }); 2192 } 2193 } 2194 2195 var randColors = panel.querySelector('#catpaw-randomize-colors'); 2196 if (randColors) { 2197 randColors.addEventListener('click', function () { randomizeCatPawColors(); }); 2198 } 2199 var randShape = panel.querySelector('#catpaw-randomize-shape'); 2200 if (randShape) { 2201 randShape.addEventListener('click', function () { randomizeCatPawShape(); }); 2202 } 2203 var randAll = panel.querySelector('#catpaw-randomize-all'); 2204 if (randAll) { 2205 randAll.addEventListener('click', function () { randomizeCatPawAll(); }); 2206 } 2207 } 2208 2209 syncCatPawSettingsUI(panel); 2210 } 2211 2212 var trailColorPickers = { primary: null, secondary: null, tertiary: null }; 2213 2214 function getMergedTrailColors() { 2215 if (window.PupwifeTrailColorSupport && typeof window.PupwifeTrailColorSupport.mergeTrailColors === 'function') { 2216 return window.PupwifeTrailColorSupport.mergeTrailColors(currentSettings.trailColors); 2217 } 2218 return currentSettings.trailColors || { primary: '#c8a8ff', secondary: '#ffb3ba', tertiary: '#89cff0' }; 2219 } 2220 2221 function setTrailColorsPartial(patch, opts) { 2222 opts = opts || {}; 2223 var next = Object.assign({}, getMergedTrailColors(), patch || {}); 2224 currentSettings.trailColors = next; 2225 applyTrailColorsSetting(next); 2226 try { 2227 localStorage.setItem('puppywifey_settings', JSON.stringify(currentSettings)); 2228 } catch (e) { 2229 emitDevWarn('Failed to save settings:', e); 2230 } 2231 try { 2232 document.dispatchEvent(new CustomEvent('pupwife:setting-changed', { 2233 detail: { key: 'trailColors', value: next } 2234 })); 2235 } catch (e) { /* ignore */ } 2236 if (opts.syncPickers) { 2237 syncTrailColorPickers(); 2238 } 2239 } 2240 2241 function updateTrailColorPanelVisibility() { 2242 var wrap = document.getElementById('settings-trail-colors'); 2243 if (!wrap) return; 2244 var supports = window.PupwifeTrailColorSupport && 2245 window.PupwifeTrailColorSupport.supportsTrailColors(currentSettings.cursorTrail); 2246 wrap.hidden = !supports;
2247 wrap.classList.toggle('is-visible', !!supports); 2248 } 2249 2250 function syncTrailColorPickers() { 2251 var colors = getMergedTrailColors(); 2252 ['primary', 'secondary', 'tertiary'].forEach(function (key) { 2253 if (trailColorPickers[key] && typeof trailColorPickers[key].setValue === 'function') { 2254 trailColorPickers[key].setValue(colors[key]); 2255 } 2256 }); 2257 } 2258 2259 function mountTrailColorPanel(panel) { 2260 if (!panel || !window.PupwifeSettingsColorPicker) return; 2261 var wrap = panel.querySelector('#settings-trail-colors'); 2262 if (!wrap || wrap.dataset.mounted === '1') { 2263 updateTrailColorPanelVisibility(); 2264 syncTrailColorPickers(); 2265 return; 2266 } 2267 wrap.dataset.mounted = '1'; 2268 var colors = getMergedTrailColors(); 2269 var slots = [ 2270 { key: 'primary', label: 'Primary', id: 'trail-color-primary' }, 2271 { key: 'secondary', label: 'Secondary', id: 'trail-color-secondary' }, 2272 { key: 'tertiary', label: 'Tertiary', id: 'trail-color-tertiary' } 2273 ]; 2274 slots.forEach(function (slot) { 2275 var field = document.createElement('div'); 2276 field.className = 'settings-trail-color__field'; 2277 field.dataset.settingsSearch = slot.label.toLowerCase() + ' trail color'; 2278 field.innerHTML = '<span class="settings-field__label">' + slot.label + '</span><div id="' + slot.id + '-mount"></div>'; 2279 wrap.appendChild(field); 2280 var mountEl = field.querySelector('#' + slot.id + '-mount'); 2281 trailColorPickers[slot.key] = window.PupwifeSettingsColorPicker.mount(mountEl, { 2282 idPrefix: slot.id, 2283 value: colors[slot.key], 2284 onChange: (function (colorKey) { 2285 return function (hex) { 2286 var patch = {}; 2287 patch[colorKey] = hex; 2288 setTrailColorsPartial(patch, { syncPickers: false }); 2289 }; 2290 })(slot.key), 2291 onCommit: (function (colorKey) { 2292 return function (hex) { 2293 var patch = {}; 2294 patch[colorKey] = hex; 2295 setTrailColorsPartial(patch, { syncPickers: false }); 2296 }; 2297 })(slot.key) 2298 }); 2299 }); 2300 2301 var matchBtn = panel.querySelector('#settings-trail-match-palette'); 2302 if (matchBtn) { 2303 matchBtn.addEventListener('click', function () { 2304 if (!window.PupwifeTrailColorSupport) return; 2305 var matched = window.PupwifeTrailColorSupport.getPaletteTrailColors(currentSettings.palette); 2306 setTrailColorsPartial(matched, { syncPickers: true }); 2307 }); 2308 } 2309 2310 updateTrailColorPanelVisibility(); 2311 } 2312 2313 function randomizeCursorSettings(picks) { 2314 if (picks.trail && window.PupwifeTrailPicker && typeof window.PupwifeTrailPicker.allTrailValues === 'function') { 2315 var trails = window.PupwifeTrailPicker.allTrailValues(); 2316 if (trails.length) { 2317 setSetting('cursorTrail', trails[Math.floor(Math.random() * trails.length)]); 2318 } 2319 } 2320 if (picks.trailAmount) { 2321 setSetting('trailAmount', Math.floor(Math.random() * 5)); 2322 } 2323 if (picks.trailColors && window.PupwifeTrailColorSupport) { 2324 setTrailColorsPartial(window.PupwifeTrailColorSupport.randomTrailColors(), { syncPickers: true }); 2325 } 2326 if (picks.customCursor && window.PupwifeCursorPicker && window.PupwifeCursorPicker.GROUPS) { 2327 if (currentSettings.customCursor === 'cat-paw-arm') { 2328 randomizeCatPawColors(); 2329 } else { 2330 var cursorValues = ['none']; 2331 window.PupwifeCursorPicker.GROUPS.forEach(function (group) { 2332 group.options.forEach(function (opt) { 2333 cursorValues.push(opt.value); 2334 }); 2335 }); 2336 setSetting('customCursor', cursorValues[Math.floor(Math.random() * cursorV
2336alues.length)]); 2337 } 2338 } 2339 if (picks.follower) { 2340 var followers = ['none', 'marsey', 'skeletile', 'orca', 'dragon', 'fish2D', 'woimString', 'jellypuff']; 2341 var nextFollower = followers[Math.floor(Math.random() * followers.length)]; 2342 setSetting('follower', nextFollower); 2343 if (nextFollower === 'skeletile' && window.skeletile && typeof window.skeletile.regenerate === 'function') { 2344 window.skeletile.regenerate(); 2345 } 2346 if (nextFollower === 'dragon' && window.DragonFollower && typeof window.DragonFollower.regenerate === 'function') { 2347 window.DragonFollower.regenerate(); 2348 } 2349 } 2350 updateSettingsUI(); 2351 } 2352 2353 function applyImportedSettings(partial) { 2354 Object.keys(partial).forEach(function (key) { 2355 if (key === 'woimString') { 2356 currentSettings.woimString = mergeWoimString(partial.woimString); 2357 } else if (key === 'jellypuff') { 2358 currentSettings.jellypuff = mergeJellypuff(partial.jellypuff); 2359 } else if (key === 'catPawArm') { 2360 currentSettings.catPawArm = mergeCatPawArm(partial.catPawArm); 2361 } else if (key === 'trailColors') { 2362 currentSettings.trailColors = partial.trailColors; 2363 } else if (key === 'trailFavorites') { 2364 currentSettings.trailFavorites = Array.isArray(partial.trailFavorites) ? partial.trailFavorites.slice() : []; 2365 } else { 2366 currentSettings[key] = partial[key]; 2367 } 2368 }); 2369 saveSettings(); 2370 applySettings(); 2371 updateSettingsUI(); 2372 if (window.PupwifeTrailPicker && typeof window.PupwifeTrailPicker.refreshFavorites === 'function') { 2373 window.PupwifeTrailPicker.refreshFavorites(); 2374 } 2375 } 2376 2377 function ensureSettingsHelperScripts() { 2378 return loadScriptOnce('/scripts/effects/settingsDiscoverability.js', 'discoverability') 2379 .then(function () { return loadScriptOnce('/scripts/effects/trailColorSupport.js', 'trail-colors'); }) 2380 .then(function () { return loadScriptOnce('/scripts/effects/settingsSearch.js', 'search'); }) 2381 .then(function () { return loadScriptOnce('/scripts/effects/settingsExportImport.js', 'export-import'); }); 2382 } 2383 2384 function ensureSettingsFontAwesome() { 2385 if (typeof window.ensureSiteFontAwesome === 'function') { 2386 return window.ensureSiteFontAwesome(); 2387 } 2388 return new Promise(function (resolve) { 2389 if (document.querySelector('script[src*="ensureSiteFontAwesome.js"]')) { 2390 var pending = document.querySelector('script[src*="ensureSiteFontAwesome.js"]'); 2391 function done() { 2392 if (typeof window.ensureSiteFontAwesome === 'function') { 2393 window.ensureSiteFontAwesome().then(resolve); 2394 } else { 2395 resolve(); 2396 } 2397 } 2398 if (pending.getAttribute('data-fa-loader-ready') === '1') { 2399 done(); 2400 } else { 2401 pending.addEventListener('load', done, { once: true }); 2402 pending.addEventListener('error', done, { once: true }); 2403 } 2404 return; 2405 } 2406 var script = document.createElement('script'); 2407 script.src = '/scripts/effects/ensureSiteFontAwesome.js'; 2408 script.async = false; 2409 script.setAttribute('data-fa-loader', '1'); 2410 script.onload = script.onerror = function () { 2411 script.setAttribute('data-fa-loader-ready', '1'); 2412 if (typeof window.ensureSiteFontAwesome === 'function') { 2413 window.ensureSiteFontAwesome().then(resolve); 2414 } else { 2415 resolve(); 2416 } 2417 }; 2418 document.head.appendChild(script); 2419 }); 2420 } 2421 2422 function createSettingsUI() { 2423 ensureSettingsAssets(); 2424 2425 const backdrop = document.createElement('div'); 2426 backdrop.id = 'settings-backdrop'; 2427 2428 const panel = document.createElement('div'); 2429 panel.id = 'settings-panel'; 2430 2431 panel.innerHTML = ` 2432 <button type="button" id="settings-close" class="settings-panel__close" aria-label="Close settings"> 2433 <i class="fa-solid fa-xmark fa-width-auto" aria-hidden="true"></i> 2434 </button> 2435 2436 <header class="settings-panel__header"> 2437 <h1 class="settings-panel__title"> 2438 <i class="fa-solid fa-sliders fa-width-auto" aria-hidden="true"></i> Settings 2439 </h1> 2440 <p class="settings-panel__subtitle">Personalize your experience</p> 2441 </header> 2442 2443 <div id="account-access-card" class="settings-card settings-card--account" aria-live="polite" data-settings-tab="account" data-settings-search="account sign in login guest"></div> 2444 2445 <div class="settings-search" data-settings-search-ignore> 2446 <i class="fa-solid fa-magnifying-glass fa-width-auto settings-search__icon" aria-hidden="true"></i> 2447 <input type="search" id="settings-search-input" class="settings-search__input" placeholder="Search settingsâ¦" autocomplete="off" aria-label="Search settings"> 2448 </div> 2449 <p id="settings-search-empty" class="settings-search-empty" data-settings-search-ignore hidden>No matching settings. Try another word?</p> 2450 2451 <section class="settings-section settings-section--compact" data-settings-tab="appearance" data-settings-section="theme" data-settings-search="theme appearance palette dark light color background caustics ambient shader"> 2452 <h2 class="settings-section__title"><i class="fa-solid fa-palette fa-width-auto" aria-hidden="true"></i> Theme</h2> 2453 <div class="settings-card settings-appearance-card" id="settings-appearance-card"> 2454 <div class="settings-appearance-grid"> 2455 <div class="settings-appearance-item settings-appearance-item--mode" data-settings-search="dark mode light theme"> 2456 <p class="settings-card__label"><i class="fa-solid fa-circle-half-stroke fa-width-auto" aria-hidden="true"></i> Light / dark</p> 2457 <div class="settings-theme-card settings-theme-card--inline" id="settings-theme-card"> 2458 <div class="settings-theme-card__row"> 2459 <i class="fa-solid fa-sun fa-width-auto settings-theme-card__icon settings-theme-card__icon--sun" id="theme-icon-sun" aria-hidden="true"></i> 2460 <label class="settings-switch" title="Toggle dark mode"> 2461 <input type="checkbox" id="setting-darkMode" class="settings-switch__input">
2462 <span class="settings-switch__track" aria-hidden="true"></span> 2463 </label> 2464 <i class="fa-solid fa-moon fa-width-auto settings-theme-card__icon settings-theme-card__icon--moon" id="theme-icon-moon" aria-hidden="true"></i> 2465 </div> 2466 </div> 2467 </div> 2468 <div class="settings-appearance-item settings-appearance-item--palette" data-settings-search="color palette theme swatch"> 2469 <label class="settings-card__label" for="setting-palette-trigger"> 2470 <i class="fa-solid fa-swatchbook fa-width-auto" aria-hidden="true"></i> Color palette 2471 </label> 2472 <div id="setting-palette-picker" class="settings-palette-picker-host"></div> 2473 </div> 2474 <div class="settings-appearance-item settings-appearance-item--background settings-appearance-grid__full" data-settings-search="background caustics aurora ambient shader water horizon clouds tuning sliders"> 2475 <label class="settings-card__label" for="setting-background-trigger"> 2476 <i class="fa-solid fa-water fa-width-auto" aria-hidden="true"></i> Background 2477 </label> 2478 <div id="setting-background-picker" class="settings-background-picker-host"></div> 2479 <div id="setting-background-clouds-warning" class="settings-alert" style="display: none;" role="status"> 2480 <i class="fa-solid fa-triangle-exclamation fa-width-auto" aria-hidden="true"></i> 2481 <div> 2482 <strong>Clouds needs WebGL2</strong> 2483 <span class="settings-alert__sub">Your browser cannot run the Clouds shader, so Caustics is showing instead. Try a newer browser or enable hardware acceleration.</span> 2484 </div> 2485 </div> 2486 <div id="setting-background-tuning" class="settings-woim-panel settings-bg-tuning-panel is-visible is-collapsed"> 2487 <button type="button" id="setting-background-tuning-toggle" class="settings-woim-panel__toggle" aria-expanded="false" aria-controls="setting-background-tuning-body"> 2488 <span class="settings-woim-panel__toggle-label"><i class="fa-solid fa-sliders fa-width-auto" aria-hidden="true"></i> Background tuning</span> 2489 <i class="fa-solid fa-chevron-down settings-woim-panel__chevron fa-width-auto" aria-hidden="true"></i> 2490 </button> 2491 <div id="setting-background-tuning-body" class="settings-woim-panel__body"></div> 2492 </div> 2493 </div> 2494 <div class="settings-appearance-item settings-appearance-grid__full settings-citrus-wallpaper-block" id="settings-citrus-wallpaper-section" data-settings-search="citrusos desktop wallpaper dim darken saturation brightness blur overlay filter tweak" hidden> 2495 <div class="settings-card settings-appearance-card settings-citrus-wallpaper-card"> 2496 <div class="settings-field settings-woim-slider" data-settings-search="wallpaper dim darken overlay desktop"> 2497 <span class="settings-field__label"><i class="fa-solid fa-circle-half-stroke fa-width-auto" aria-hidden="true"></i> Dim overlay</span> 2498 <div class="settings-trail-amount"> 2499 <input type="range" id="setting-citrusWallpaperDim" class="settings-trail-amount__slider settings-citrus-slider" min="0" max="50" step="1" value="0"> 2500 <output class="settings-trail-amount__value" id="setting-citrusWallpaperDim-label" for="setting-citrusWallpaperDim">0%</output> 2501 </div> 2502 </div> 2503 <div class="settings-field settings-woim-slider" data-settings-search="wallpaper saturation color vivid desktop">
2504 <span class="settings-field__label"><i class="fa-solid fa-droplet fa-width-auto" aria-hidden="true"></i> Saturation</span> 2505 <div class="settings-trail-amount"> 2506 <input type="range" id="setting-citrusWallpaperSaturation" class="settings-trail-amount__slider settings-citrus-slider" min="50" max="150" step="5" value="100"> 2507 <output class="settings-trail-amount__value" id="setting-citrusWallpaperSaturation-label" for="setting-citrusWallpaperSaturation">100%</output> 2508 </div> 2509 </div> 2510 <div class="settings-field settings-woim-slider" data-settings-search="wallpaper brightness lighten darken desktop"> 2511 <span class="settings-field__label"><i class="fa-solid fa-sun fa-width-auto" aria-hidden="true"></i> Brightness</span> 2512 <div class="settings-trail-amount"> 2513 <input type="range" id="setting-citrusWallpaperBrightness" class="settings-trail-amount__slider settings-citrus-slider" min="50" max="150" step="5" value="100"> 2514 <output class="settings-trail-amount__value" id="setting-citrusWallpaperBrightness-label" for="setting-citrusWallpaperBrightness">100%</output> 2515 </div> 2516 </div> 2517 <div class="settings-field settings-woim-slider" data-settings-search="wallpaper blur soften desktop"> 2518 <span class="settings-field__label"><i class="fa-solid fa-wand-magic-sparkles fa-width-auto" aria-hidden="true"></i> Blur</span> 2519 <div class="settings-trail-amount"> 2520 <input type="range" id="setting-citrusWallpaperBlur" class="settings-trail-amount__slider settings-citrus-slider" min="0" max="12" step="1" value="0"> 2521 <output class="settings-trail-amount__value" id="setting-citrusWallpaperBlur-label" for="setting-citrusWallpaperBlur">0 px</output> 2522 </div> 2523 </div> 2524 <div class="settings-inline-actions"> 2525 <button type="button" id="settings-citrus-wallpaper-reset" class="settings-btn settings-btn--compact settings-desktop-action-btn" data-settings-search="reset wallpaper desktop tweaks defaults"> 2526 <i class="fa-solid fa-rotate-left fa-width-auto" aria-hidden="true"></i> Reset wallpaper tweaks 2527 </button> 2528 </div> 2529 </div> 2530 </div> 2531 </div> 2532 </div> 2533 </section> 2534 2535 <section class="settings-section settings-section--compact" id="settings-citrusos-section" data-settings-tab="desktop" data-settings-section="general" data-settings-search="citrusos desktop ui scale size taskbar icons start menu general" hidden> 2536 <h2 class="settings-section__title"><i class="fa-solid fa-sliders fa-width-auto" aria-hidden="true"></i> General</h2> 2537 <div class="settings-card"> 2538 <div class="settings-field" data-settings-search="ui scale size desktop icons taskbar readability"> 2539 <span class="settings-field__label"><i class="fa-solid fa-up-right-and-down-left-from-center fa-width-auto" aria-hidden="true"></i> Desktop UI scale</span> 2540 <div class="settings-radio-grid settings-radio-grid--scale"> 2541 <label class="settings-radio"> 2542 <input type="radio" name="citrusUiScale" value="100"> 2543 <span class="settings-radio__box"><span>100%</span><small>Default</small></span> 2544 </label> 2545 <label class="settings-radio"> 2546 <input type="radio" name="citrusUiScale" value="125">
2547 <span class="settings-radio__box"><span>125%</span><small>Large</small></span> 2548 </label> 2549 <label class="settings-radio"> 2550 <input type="radio" name="citrusUiScale" value="150"> 2551 <span class="settings-radio__box"><span>150%</span><small>Extra large</small></span> 2552 </label> 2553 </div> 2554 </div> 2555 </div> 2556 </section> 2557 2558 <section class="settings-section settings-section--compact" id="settings-taskbar-section" data-settings-tab="desktop" data-settings-section="taskbar" data-settings-search="taskbar auto hide clock date seconds show desktop tray" hidden> 2559 <h2 class="settings-section__title"><i class="fa-solid fa-bars fa-width-auto" aria-hidden="true"></i> Taskbar</h2> 2560 <div class="settings-card"> 2561 <label class="settings-row settings-row--toggle settings-row--compact" data-settings-search="auto hide taskbar edge hover reveal"> 2562 <span class="settings-row__label"><i class="fa-solid fa-eye-slash fa-width-auto settings-row__icon" aria-hidden="true"></i><span>Auto-hide taskbar</span></span> 2563 <span class="settings-switch"><input type="checkbox" id="setting-taskbarAutoHide" class="settings-switch__input"><span class="settings-switch__track" aria-hidden="true"></span></span> 2564 </label> 2565 <label class="settings-row settings-row--toggle settings-row--compact" data-settings-search="show desktop button peek wallpaper"> 2566 <span class="settings-row__label"><i class="fa-solid fa-border-none fa-width-auto settings-row__icon" aria-hidden="true"></i><span>Show Desktop button</span></span> 2567 <span class="settings-switch"><input type="checkbox" id="setting-showDesktopButton" class="settings-switch__input" checked><span class="settings-switch__track" aria-hidden="true"></span></span> 2568 </label> 2569 <label class="settings-row settings-row--toggle settings-row--compact" data-settings-search="clock 24 hour military time"> 2570 <span class="settings-row__label"><i class="fa-solid fa-clock fa-width-auto settings-row__icon" aria-hidden="true"></i><span>24-hour clock</span></span> 2571 <span class="settings-switch"><input type="checkbox" id="setting-taskbarClock24Hour" class="settings-switch__input"><span class="settings-switch__track" aria-hidden="true"></span></span> 2572 </label> 2573 <label class="settings-row settings-row--toggle settings-row--compact" data-settings-search="clock seconds tray time"> 2574 <span class="settings-row__label"><i class="fa-solid fa-stopwatch fa-width-auto settings-row__icon" aria-hidden="true"></i><span>Show seconds on clock</span></span> 2575 <span class="settings-switch"><input type="checkbox" id="setting-taskbarClockShowSeconds" class="settings-switch__input"><span class="settings-switch__track" aria-hidden="true"></span></span> 2576 </label> 2577 <label class="settings-row settings-row--toggle settings-row--compact" data-settings-search="date tray calendar"> 2578 <span class="settings-row__label"><i class="fa-solid fa-calendar-day fa-width-auto settings-row__icon" aria-hidden="true"></i><span>Show date in tray</span></span> 2579 <span class="settings-switch"><input type="checkbox" id="setting-taskbarShowDate" class="settings-switch__input" checked><span class="settings-switch__track" aria-hidden="true"></span></span> 2580 </label> 2581 </div> 2582 </section> 2583 2584 <section class="settings-section settings-section--compact" id="settings-windows-section" data-settings-tab="desktop" data-settings-section="windows" data-settings-search="windows titlebar maximize double click snap edge" hidden> 2585 <h2 class="settings-section__title"><i class="fa-solid fa-window-maximize fa-width-auto" aria-hidden="true"></i> Windows</h2> 2586 <div class="settings-card"> 2587 <label class="settings-row settings-row--toggle settings-row--compact" data-settings-search="double click titlebar maximize restore">
2588 <span class="settings-row__label"><i class="fa-solid fa-up-down fa-width-auto settings-row__icon" aria-hidden="true"></i><span>Double-click titlebar to maximize</span></span> 2589 <span class="settings-switch"><input type="checkbox" id="setting-windowTitlebarDblClickMaximize" class="settings-switch__input" checked><span class="settings-switch__track" aria-hidden="true"></span></span> 2590 </label> 2591 </div> 2592 </section> 2593 2594 <section class="settings-section settings-section--compact" id="settings-desktop-section" data-settings-tab="desktop" data-settings-section="desktop" data-settings-search="desktop icons wallpaper arrange reset labels hide show saturation dim tint"> 2595 <h2 class="settings-section__title"><i class="fa-solid fa-border-all fa-width-auto" aria-hidden="true"></i> Desktop icons</h2> 2596 <div class="settings-card"> 2597 <label class="settings-row settings-row--toggle settings-row--compact" data-settings-search="show desktop icons hide icons"> 2598 <span class="settings-row__label"><i class="fa-solid fa-icons fa-width-auto settings-row__icon" aria-hidden="true"></i><span>Show desktop icons</span></span> 2599 <span class="settings-switch"><input type="checkbox" id="setting-desktopIconsVisible" class="settings-switch__input" checked><span class="settings-switch__track" aria-hidden="true"></span></span> 2600 </label> 2601 <label class="settings-row settings-row--toggle settings-row--compact" data-settings-search="icon labels text names"> 2602 <span class="settings-row__label"><i class="fa-solid fa-font fa-width-auto settings-row__icon" aria-hidden="true"></i><span>Show icon labels</span></span> 2603 <span class="settings-switch"><input type="checkbox" id="setting-desktopIconLabelsVisible" class="settings-switch__input" checked><span class="settings-switch__track" aria-hidden="true"></span></span> 2604 </label> 2605 <div class="settings-field" data-settings-search="desktop icon size scale bigger smaller"> 2606 <span class="settings-field__label"><i class="fa-solid fa-up-right-and-down-left-from-center fa-width-auto" aria-hidden="true"></i> Icon size</span> 2607 <div class="settings-radio-grid settings-radio-grid--scale"> 2608 <label class="settings-radio"> 2609 <input type="radio" name="desktopIconScale" value="75"> 2610 <span class="settings-radio__box"><span>75%</span><small>Small</small></span> 2611 </label> 2612 <label class="settings-radio"> 2613 <input type="radio" name="desktopIconScale" value="100"> 2614 <span class="settings-radio__box"><span>100%</span><small>Default</small></span> 2615 </label> 2616 <label class="settings-radio"> 2617 <input type="radio" name="desktopIconScale" value="125"> 2618 <span class="settings-radio__box"><span>125%</span><small>Large</small></span> 2619 </label> 2620 <label class="settings-radio"> 2621 <input type="radio" name="desktopIconScale" value="150"> 2622 <span class="settings-radio__box"><span>150%</span><small>Extra large</small></span> 2623 </label> 2624 </div> 2625 </div> 2626 <div class="settings-desktop-actions"> 2627 <button type="button" id="settings-desktop-auto-arrange" class="settings-btn settings-btn--compact settings-desktop-action-btn" data-settings-search="auto arrange tidy desktop icons reset icon layout"> 2628 <i class="fa-solid fa-grip fa-width-auto" aria-hidden="true"></i> Reset icon layout 2629 </button> 2630 <button type="button" id="settings-desktop-reset-folder-layout" class="settings-btn settings-btn--compact settings-desktop-action-btn" data-settings-search="reset folder layout user folders"> 2631 <i class="fa-solid fa-folder-tree fa-width-auto" aria-hidden="true"></i> Reset folder layout 2632 </button> 2633 <button type="button" id="settings-desktop-reset-icons" class="settings-btn settings-btn--compact settings-desktop-action-btn" data-settings-search="reset desktop icons default grid factory"> 2634 <i class="fa-solid fa-rotate-left fa-width-auto" aria-hidden="true"></i> Reset desktop to defaults 2635 </button> 2636 </div> 2637 </div> 2638 </section> 2639 2640 <section class="settings-section settings-section--compact" data-settings-tab="cursor" data-settings-section="follower" data-settings-search="follower buddy marsey skeletile dragon orca fish woim jellypuff jellyfish"> 2641 <h2 class="settings-section__title"><i class="fa-solid fa-arrow-pointer fa-width-auto" aria-hidden="true"></i> Cursor buddy</h2> 2642 <div id="follower-warning" class="settings-alert" style="display: none;" role="status"> 2643 <i class="fa-solid fa-triangle-exclamation fa-width-auto" aria-hidden="true"></i> 2644 <div> 2645 <strong>Followers disabled on chat page</strong>
2646 <span class="settings-alert__sub">Browser security prevents tracking your cursor inside the chatbox iframe. Followers work normally on all other pages!</span> 2647 </div> 2648 </div> 2649 <div class="settings-radio-grid"> 2650 <label class="settings-radio"> 2651 <input type="radio" name="follower" value="none"> 2652 <span class="settings-radio__box"><i class="fa-solid fa-ban fa-width-auto" aria-hidden="true"></i><span>None</span></span> 2653 </label> 2654 <label class="settings-radio"> 2655 <input type="radio" name="follower" value="marsey"> 2656 <span class="settings-radio__box"><i class="fa-solid fa-cat fa-width-auto" aria-hidden="true"></i><span>Marsey cat</span></span> 2657 </label> 2658 <label class="settings-radio settings-radio--full"> 2659 <input type="radio" name="follower" value="skeletile"> 2660 <span class="settings-radio__box"><i class="fa-solid fa-skull fa-width-auto" aria-hidden="true"></i><span>Skeletile <small>(R to regenerate)</small></span></span> 2661 </label> 2662 <button type="button" id="regenerate-skeletile-btn" class="settings-regen-btn settings-radio--full" title="Generate a new random skeletile"> 2663 <i class="fa-solid fa-arrows-rotate fa-width-auto" aria-hidden="true"></i> Regenerate 2664 </button> 2665 <label class="settings-radio settings-radio--full"> 2666 <input type="radio" name="follower" value="dragon"> 2667 <span class="settings-radio__box"><i class="fa-solid fa-dragon fa-width-auto" aria-hidden="true"></i><span>Dragon <small>(R to regenerate)</small></span></span> 2668 </label> 2669 <button type="button" id="regenerate-dragon-btn" class="settings-regen-btn settings-radio--full" title="Generate a new random dragon"> 2670 <i class="fa-solid fa-arrows-rotate fa-width-auto" aria-hidden="true"></i> Regenerate 2671 </button> 2672 <label class="settings-radio"> 2673 <input type="radio" name="follower" value="orca"> 2674 <span class="settings-radio__box"><i class="fa-solid fa-fish fa-width-auto" aria-hidden="true"></i><span>Orca</span></span> 2675 </label> 2676 <label class="settings-radio"> 2677 <input type="radio" name="follower" value="fish2D"> 2678 <span class="settings-radio__box"><i class="fa-solid fa-fish-fins fa-width-auto" aria-hidden="true"></i><span>2D fish</span></span> 2679 </label> 2680 <label class="settings-radio settings-radio--full"> 2681 <input type="radio" name="follower" value="woimString"> 2682 <span class="settings-radio__box"><i class="fa-solid fa-worm fa-width-auto" aria-hidden="true"></i><span>Woim On A String</span></span> 2683 </label> 2684 <label class="settings-radio settings-radio--full"> 2685 <input type="radio" name="follower" value="jellypuff"> 2686 <span class="settings-radio__box"><i class="fa-solid fa-droplet fa-width-auto" aria-hidden="true"></i><span>Jellypuff</span></span> 2687 </label> 2688 </div> 2689 <div id="jellypuff-settings" class="settings-woim-panel is-collapsed"> 2690 <button type="button" id="jellypuff-settings-toggle" class="settings-woim-panel__toggle" aria-expanded="false" aria-controls="jellypuff-settings-body"> 2691 <span class="settings-woim-panel__toggle-label"><i class="fa-solid fa-droplet fa-width-auto" aria-hidden="true"></i> Jellypuff options</span> 2692 <i class="fa-solid fa-chevron-down settings-woim-panel__chevron fa-width-auto" aria-hidden="true"></i> 2693 </button>
2694 <div id="jellypuff-settings-body" class="settings-woim-panel__body"> 2695 <h3 class="settings-woim-panel__title"><i class="fa-solid fa-sliders fa-width-auto" aria-hidden="true"></i> Motion & size</h3> 2696 <div class="settings-field settings-woim-slider"> 2697 <span class="settings-field__label">Bell size</span> 2698 <div class="settings-trail-amount"> 2699 <input type="range" id="setting-jellypuff-size" class="settings-trail-amount__slider settings-citrus-slider" min="25" max="200" step="5" value="100"> 2700 <output class="settings-trail-amount__value" id="setting-jellypuff-size-label" for="setting-jellypuff-size">100%</output> 2701 </div> 2702 </div> 2703 <div class="settings-field settings-woim-slider"> 2704 <span class="settings-field__label">Pulse rate</span> 2705 <div class="settings-trail-amount"> 2706 <input type="range" id="setting-jellypuff-pulseRate" class="settings-trail-amount__slider settings-citrus-slider" min="40" max="200" step="5" value="100"> 2707 <output class="settings-trail-amount__value" id="setting-jellypuff-pulseRate-label" for="setting-jellypuff-pulseRate">100%</output> 2708 </div> 2709 </div> 2710 <div class="settings-field settings-woim-slider"> 2711 <span class="settings-field__label">Tentacle density</span> 2712 <div class="settings-trail-amount"> 2713 <input type="range" id="setting-jellypuff-tentacleDensity" class="settings-trail-amount__slider settings-citrus-slider" min="50" max="200" step="5" value="100"> 2714 <output class="settings-trail-amount__value" id="setting-jellypuff-tentacleDensity-label" for="setting-jellypuff-tentacleDensity">100%</output> 2715 </div> 2716 </div> 2717 <div class="settings-field settings-woim-slider"> 2718 <span class="settings-field__label">Glow strength</span> 2719 <div class="settings-trail-amount"> 2720 <input type="range" id="setting-jellypuff-glowStrength" class="settings-trail-amount__slider settings-citrus-slider" min="0" max="200" step="5" value="100"> 2721 <output class="settings-trail-amount__value" id="setting-jellypuff-glowStrength-label" for="setting-jellypuff-glowStrength">100%</output> 2722 </div> 2723 </div> 2724 <h3 class="settings-woim-panel__title"><i class="fa-solid fa-palette fa-width-auto" aria-hidden="true"></i> Bell colors</h3> 2725 <div id="jellypuff-bell-color-mount"></div> 2726 <h3 class="settings-woim-panel__title"><i class="fa-solid fa-palette fa-width-auto" aria-hidden="true"></i> Tentacle colors</h3> 2727 <div id="jellypuff-tentacles-color-mount"></div> 2728 </div> 2729 </div> 2730 <div id="woim-string-settings" class="settings-woim-panel is-collapsed"> 2731 <button type="button" id="woim-string-settings-toggle" class="settings-woim-panel__toggle" aria-expanded="false" aria-controls="woim-string-settings-body"> 2732 <span class="settings-woim-panel__toggle-label"><i class="fa-solid fa-worm fa-width-auto" aria-hidden="true"></i> Woim On A String options</span> 2733 <i class="fa-solid fa-chevron-down settings-woim-panel__chevron fa-width-auto" aria-hidden="true"></i> 2734 </button> 2735 <div id="woim-string-settings-body" class="settings-woim-panel__body"> 2736 <h3 class="settings-woim-panel__title"><i class="fa-solid fa-link fa-width-auto" aria-hidden="true"></i> String & anchor</h3> 2737 <div class="settings-field settings-woim-slider">
2738 <span class="settings-field__label">Rope points</span> 2739 <div class="settings-trail-amount"> 2740 <input type="range" id="setting-woim-ropePoints" class="settings-trail-amount__slider settings-citrus-slider" min="16" max="64" step="1" value="32" list="setting-woim-ropePoints-ticks"> 2741 <datalist id="setting-woim-ropePoints-ticks"> 2742 <option value="16"></option><option value="24"></option><option value="32"></option><option value="48"></option><option value="64"></option> 2743 </datalist> 2744 <output class="settings-trail-amount__value" id="setting-woim-ropePoints-label" for="setting-woim-ropePoints">32</output> 2745 </div> 2746 </div> 2747 <div class="settings-field settings-woim-slider"> 2748 <span class="settings-field__label">Rope length</span> 2749 <div class="settings-trail-amount"> 2750 <input type="range" id="setting-woim-ropeLength" class="settings-trail-amount__slider settings-citrus-slider" min="50" max="200" step="5" value="100"> 2751 <output class="settings-trail-amount__value" id="setting-woim-ropeLength-label" for="setting-woim-ropeLength">100%</output> 2752 </div> 2753 </div> 2754 <div class="settings-field settings-woim-slider"> 2755 <span class="settings-field__label">Rope thickness</span> 2756 <div class="settings-trail-amount"> 2757 <input type="range" id="setting-woim-ropeThickness" class="settings-trail-amount__slider settings-citrus-slider" min="25" max="300" step="5" value="100"> 2758 <output class="settings-trail-amount__value" id="setting-woim-ropeThickness-label" for="setting-woim-ropeThickness">100%</output> 2759 </div> 2760 </div> 2761 <label class="settings-row settings-row--toggle"> 2762 <span class="settings-row__label"><span>Anchor ring</span></span> 2763 <span class="settings-switch"> 2764 <input type="checkbox" id="setting-woim-anchorShow" class="settings-switch__input" checked> 2765 <span class="settings-switch__track" aria-hidden="true"></span> 2766 </span> 2767 </label> 2768 <div class="settings-field settings-woim-slider"> 2769 <span class="settings-field__label">Ring size</span> 2770 <div class="settings-trail-amount"> 2771 <input type="range" id="setting-woim-anchorSize" class="settings-trail-amount__slider settings-citrus-slider" min="50" max="200" step="5" value="100"> 2772 <output class="settings-trail-amount__value" id="setting-woim-anchorSize-label" for="setting-woim-anchorSize">100%</output> 2773 </div> 2774 </div> 2775 <div class="settings-field settings-woim-slider"> 2776 <span class="settings-field__label">Ring thickness</span> 2777 <div class="settings-trail-amount"> 2778 <input type="range" id="setting-woim-anchorThickness" class="settings-trail-amount__slider settings-citrus-slider" min="50" max="200" step="5" value="100"> 2779 <output class="settings-trail-amount__value" id="setting-woim-anchorThickness-label" for="setting-woim-anchorThickness">100%</output> 2780 </div> 2781 </div> 2782 <h3 class="settings-woim-panel__title"><i class="fa-solid fa-palette fa-width-auto" aria-hidden="true"></i> Rope colors</h3> 2783 <div id="woim-rope-color-mount"></div> 2784 <h3 class="settings-woim-panel__title"><i class="fa-solid fa-worm fa-width-auto" aria-hidden="true"></i> Squirmle</h3> 2785 <div class="settings-field settings-woim-slider">
2786 <span class="settings-field__label">Woim points</span> 2787 <div class="settings-trail-amount"> 2788 <input type="range" id="setting-woim-bodyPoints" class="settings-trail-amount__slider settings-citrus-slider" min="16" max="64" step="1" value="32" list="setting-woim-bodyPoints-ticks"> 2789 <datalist id="setting-woim-bodyPoints-ticks"> 2790 <option value="16"></option><option value="24"></option><option value="32"></option><option value="48"></option><option value="64"></option> 2791 </datalist> 2792 <output class="settings-trail-amount__value" id="setting-woim-bodyPoints-label" for="setting-woim-bodyPoints">32</output> 2793 </div> 2794 </div> 2795 <div class="settings-field settings-woim-slider"> 2796 <span class="settings-field__label">Woim size</span> 2797 <div class="settings-trail-amount"> 2798 <input type="range" id="setting-woim-size" class="settings-trail-amount__slider settings-citrus-slider" min="25" max="400" step="5" value="100"> 2799 <output class="settings-trail-amount__value" id="setting-woim-size-label" for="setting-woim-size">100%</output> 2800 </div> 2801 </div> 2802 <h3 class="settings-woim-panel__title"><i class="fa-solid fa-palette fa-width-auto" aria-hidden="true"></i> Woim colors</h3> 2803 <div id="woim-body-color-mount"></div> 2804 </div> 2805 </div> 2806 </section> 2807 2808 <section class="settings-section settings-section--compact" id="settings-cursor-section" data-settings-tab="cursor" data-settings-section="cursor" data-settings-search="cursor trail pointer mouse sparkles"> 2809 <h2 class="settings-section__title"><i class="fa-solid fa-sparkles fa-width-auto" aria-hidden="true"></i> Cursor & trails</h2> 2810 2811 <div class="settings-field" data-settings-search="cursor trail type sparkles fairy paw"> 2812 <span class="settings-field__label"><i class="fa-solid fa-route fa-width-auto" aria-hidden="true"></i> Trail type <small>(star to favorite)</small></span> 2813 <div class="settings-bubble-select" id="setting-cursorTrail-picker"></div> 2814 </div> 2815 2816 <div class="settings-field settings-trail-amount-row" id="setting-trailAmount-row" data-settings-search="trail amount density particles"> 2817 <span class="settings-field__label" id="setting-trailAmount-label-heading"> 2818 <i class="fa-solid fa-sliders fa-width-auto" aria-hidden="true"></i> Trail amount 2819 </span> 2820 <div class="settings-trail-amount"> 2821 <input type="range" id="setting-trailAmount" class="settings-trail-amount__slider settings-citrus-slider" min="0" max="4" step="1" value="2" aria-labelledby="setting-trailAmount-label-heading" aria-valuemin="0" aria-valuemax="4" aria-valuetext="Normal" list="setting-trailAmount-ticks"> 2822 <datalist id="setting-trailAmount-ticks"> 2823 <option value="0" label="Min"></option><option value="1" label="Low"></option><option value="2" label="Norm"></option><option value="3" label="High"></option><option value="4" label="Max"></option> 2824 </datalist> 2825 <output id="setting-trailAmount-label" class="settings-trail-amount__value" for="setting-trailAmount">Normal</output> 2826 </div> 2827 </div> 2828 2829 <div id="settings-trail-colors" class="settings-trail-colors" hidden data-settings-search="trail colors primary secondary tertiary palette customize"> 2830 <p class="settings-subsection__title"><i class="fa-solid fa-palette fa-width-auto" aria-hidden="true"></i> Trail colors</p> 2831 </div> 2832 2833 <div class="settings-inline-actions" data-settings-search="match palette randomize cursor dice shuffle"> 2834 <button type="button" id="settings-trail-match-palette"
2834class="settings-btn settings-btn--compact"> 2835 <i class="fa-solid fa-swatchbook fa-width-auto" aria-hidden="true"></i> Match theme palette 2836 </button> 2837 <button type="button" id="settings-randomize-cursor" class="settings-btn settings-btn--compact"> 2838 <i class="fa-solid fa-shuffle fa-width-auto" aria-hidden="true"></i> Randomize cursor stuff 2839 </button> 2840 </div> 2841 2842 <div class="settings-field settings-field--compact" data-settings-search="custom cursor mouse pointer asriel axolotl recursive stretchy paw cat dog"> 2843 <span class="settings-field__label"><i class="fa-solid fa-computer-mouse fa-width-auto" aria-hidden="true"></i> Custom cursor</span> 2844 <div class="settings-bubble-select" id="setting-customCursor-picker"></div> 2845 </div> 2846 2847 <div id="cat-paw-arm-settings" class="settings-woim-panel is-collapsed"> 2848 <button type="button" id="cat-paw-arm-settings-toggle" class="settings-woim-panel__toggle" aria-expanded="false" aria-controls="cat-paw-arm-settings-body"> 2849 <span class="settings-woim-panel__toggle-label"><i class="fa-solid fa-paw fa-width-auto" aria-hidden="true"></i> Stretchy paw options</span> 2850 <i class="fa-solid fa-chevron-down settings-woim-panel__chevron fa-width-auto" aria-hidden="true"></i> 2851 </button> 2852 <div id="cat-paw-arm-settings-body" class="settings-woim-panel__body"> 2853 <p class="settings-section__hint catpaw-sprite-art-note" hidden>Fur and arm tint together. Turn off toe beans for the back of the paw.</p> 2854 2855 <h3 class="settings-woim-panel__title"><i class="fa-solid fa-paw fa-width-auto" aria-hidden="true"></i> Paw</h3> 2856 <div class="catpaw-settings-grid catpaw-settings-grid--dropdowns"> 2857 <div class="settings-field settings-woim-slider"> 2858 <span class="settings-field__label">Paw style</span> 2859 <div id="catpaw-preset-mount"></div> 2860 </div> 2861 <div class="settings-field settings-woim-slider"> 2862 <span class="settings-field__label">Anchor preset</span> 2863 <div id="catpaw-anchor-mount"></div> 2864 </div> 2865 <div class="settings-field settings-woim-slider catpaw-settings-grid__full catpaw-species-field"> 2866 <span class="settings-field__label">Species</span> 2867 <div id="catpaw-species-mount"></div> 2868 </div> 2869 <div class="settings-field settings-woim-slider"> 2870 <span class="settings-field__label">Paw & arm size</span> 2871 <div class="settings-trail-amount"> 2872 <input type="range" id="setting-catpaw-pawSize" class="settings-trail-amount__slider settings-citrus-slider" min="25" max="400" step="5" value="100"> 2873 <output class="settings-trail-amount__value" id="setting-catpaw-pawSize-label" for="setting-catpaw-pawSize">100%</output> 2874 </div> 2875 </div> 2876 <div class="settings-field settings-woim-slider catpaw-procedural-only"> 2877 <span class="settings-field__label">Pad size</span> 2878 <div class="settings-trail-amount"> 2879 <input type="range" id="setting-catpaw-padSize" class="settings-trail-amount__slider settings-citrus-slider" min="50" max="200" step="5" value="100"> 2880 <output class="settings-trail-amount__value" id="setting-catpaw-padSize-label" for="setting-catpaw-padSize">100%</output> 2881 </div> 2882 </div> 2883 <div class="catpaw-settings-toggles catpaw-settings-grid__full"> 2884 <label class="settings-row settings-row--toggle settings-row--compact">
2885 <span class="settings-row__label"><span>Click squish</span></span> 2886 <span class="settings-switch"> 2887 <input type="checkbox" id="setting-catpaw-clickSquish" class="settings-switch__input" checked> 2888 <span class="settings-switch__track" aria-hidden="true"></span> 2889 </span> 2890 </label> 2891 <label class="settings-row settings-row--toggle settings-row--compact"> 2892 <span class="settings-row__label"><span>Toe beans</span></span> 2893 <span class="settings-switch"> 2894 <input type="checkbox" id="setting-catpaw-showBeans" class="settings-switch__input" checked> 2895 <span class="settings-switch__track" aria-hidden="true"></span> 2896 </span> 2897 </label> 2898 </div> 2899 </div> 2900 2901 <h3 class="settings-woim-panel__title catpaw-sprite-only" hidden><i class="fa-solid fa-palette fa-width-auto" aria-hidden="true"></i> Fur & arm color</h3> 2902 <div id="catpaw-sprite-fur-color-mount" class="catpaw-sprite-only catpaw-settings-grid__full" hidden></div> 2903 2904 <h3 class="settings-woim-panel__title"><i class="fa-solid fa-link fa-width-auto" aria-hidden="true"></i> Arm</h3> 2905 <div class="catpaw-settings-grid"> 2906 <div class="settings-field settings-woim-slider"> 2907 <span class="settings-field__label">Origin X</span> 2908 <div class="settings-trail-amount"> 2909 <input type="range" id="setting-catpaw-originX" class="settings-trail-amount__slider settings-citrus-slider" min="-20" max="120" step="1" value="50"> 2910 <output class="settings-trail-amount__value" id="setting-catpaw-originX-label" for="setting-catpaw-originX">50%</output> 2911 </div> 2912 </div> 2913 <div class="settings-field settings-woim-slider"> 2914 <span class="settings-field__label">Origin Y</span> 2915 <div class="settings-trail-amount"> 2916 <input type="range" id="setting-catpaw-originY" class="settings-trail-amount__slider settings-citrus-slider" min="-20" max="120" step="1" value="108"> 2917 <output class="settings-trail-amount__value" id="setting-catpaw-originY-label" for="setting-catpaw-originY">108%</output> 2918 </div> 2919 </div> 2920 <div class="settings-field settings-woim-slider"> 2921 <span class="settings-field__label">Arm taper</span> 2922 <div class="settings-trail-amount"> 2923 <input type="range" id="setting-catpaw-armTaper" class="settings-trail-amount__slider settings-citrus-slider" min="0" max="100" step="5" value="35"> 2924 <output class="settings-trail-amount__value" id="setting-catpaw-armTaper-label" for="setting-catpaw-armTaper">35%</output> 2925 </div> 2926 </div> 2927 <div class="settings-field settings-woim-slider"> 2928 <span class="settings-field__label">Arm bend amount</span> 2929 <div class="settings-trail-amount"> 2930 <input type="range" id="setting-catpaw-armBend" class="settings-trail-amount__slider settings-citrus-slider" min="0" max="200" step="5" value="0"> 2931 <output class="settings-trail-amount__value" id="setting-catpaw-armBend-label" for="setting-catpaw-armBend">0%</output> 2932 </div> 2933 </div> 2934 </div> 2935 2936 <h3 class="settings-woim-panel__title"><i class="fa-solid fa-arrows-up-down-left-right fa-width-auto" aria-hidden="true"></i> Motion</h3> 2937 <div class="catpaw-settings-grid"> 2938 <div class="settings-field settings-woim-slider catpaw-settings-grid__full">
2939 <span class="settings-field__label">Follow lag</span> 2940 <div class="settings-trail-amount"> 2941 <input type="range" id="setting-catpaw-followLag" class="settings-trail-amount__slider settings-citrus-slider" min="5" max="100" step="1" value="42"> 2942 <output class="settings-trail-amount__value" id="setting-catpaw-followLag-label" for="setting-catpaw-followLag">42%</output> 2943 </div> 2944 </div> 2945 </div> 2946 2947 <h3 class="settings-woim-panel__title"><i class="fa-solid fa-shoe-prints fa-width-auto" aria-hidden="true"></i> Paw prints</h3> 2948 <div class="catpaw-settings-grid"> 2949 <div class="catpaw-settings-toggles catpaw-settings-grid__full"> 2950 <label class="settings-row settings-row--toggle settings-row--compact"> 2951 <span class="settings-row__label"><span>Paw prints on click</span></span> 2952 <span class="settings-switch"> 2953 <input type="checkbox" id="setting-catpaw-printsEnabled" class="settings-switch__input" checked> 2954 <span class="settings-switch__track" aria-hidden="true"></span> 2955 </span> 2956 </label> 2957 </div> 2958 <div class="settings-field settings-woim-slider"> 2959 <span class="settings-field__label">Print size</span> 2960 <div class="settings-trail-amount"> 2961 <input type="range" id="setting-catpaw-printSize" class="settings-trail-amount__slider settings-citrus-slider" min="25" max="200" step="5" value="100"> 2962 <output class="settings-trail-amount__value" id="setting-catpaw-printSize-label" for="setting-catpaw-printSize">100%</output> 2963 </div> 2964 </div> 2965 <div class="settings-field settings-woim-slider"> 2966 <span class="settings-field__label">Print fade</span> 2967 <div class="settings-trail-amount"> 2968 <input type="range" id="setting-catpaw-printFade" class="settings-trail-amount__slider settings-citrus-slider" min="25" max="200" step="5" value="100"> 2969 <output class="settings-trail-amount__value" id="setting-catpaw-printFade-label" for="setting-catpaw-printFade">100%</output> 2970 </div> 2971 </div> 2972 <div class="settings-field settings-woim-slider catpaw-settings-grid__full"> 2973 <span class="settings-field__label">Max prints</span> 2974 <div class="settings-trail-amount"> 2975 <input type="range" id="setting-catpaw-printMax" class="settings-trail-amount__slider settings-citrus-slider" min="4" max="32" step="1" value="16"> 2976 <output class="settings-trail-amount__value" id="setting-catpaw-printMax-label" for="setting-catpaw-printMax">16</output> 2977 </div> 2978 </div> 2979 </div> 2980 2981 <h3 class="settings-woim-panel__title catpaw-procedural-only"><i class="fa-solid fa-palette fa-width-auto" aria-hidden="true"></i> Colors</h3> 2982 <div class="catpaw-settings-grid catpaw-procedural-only"> 2983 <div class="catpaw-settings-grid__full"> 2984 <span class="settings-field__label">Arm color</span> 2985 <div id="catpaw-arm-color-mount"></div> 2986 </div> 2987 <div class="catpaw-settings-grid__full"> 2988 <span class="settings-field__label">Paw fur</span> 2989 <div id="catpaw-fur-color-mount"></div> 2990 </div> 2991 <div class="catpaw-settings-grid__full">
2992 <span class="settings-field__label">Paw pads</span> 2993 <div id="catpaw-pads-color-mount"></div> 2994 </div> 2995 <div class="catpaw-settings-grid__full"> 2996 <span class="settings-field__label">Outline</span> 2997 <label class="settings-row settings-row--toggle settings-row--compact"> 2998 <span class="settings-row__label"><span>Show outline</span></span> 2999 <span class="settings-switch"> 3000 <input type="checkbox" id="setting-catpaw-outlineShow" class="settings-switch__input" checked> 3001 <span class="settings-switch__track" aria-hidden="true"></span> 3002 </span> 3003 </label> 3004 <div class="settings-field settings-woim-slider"> 3005 <span class="settings-field__label">Outline thickness</span> 3006 <div class="settings-trail-amount"> 3007 <input type="range" id="setting-catpaw-outlineThickness" class="settings-trail-amount__slider settings-citrus-slider" min="50" max="200" step="5" value="100"> 3008 <output class="settings-trail-amount__value" id="setting-catpaw-outlineThickness-label" for="setting-catpaw-outlineThickness">100%</output> 3009 </div> 3010 </div> 3011 <div id="catpaw-outline-color-mount"></div> 3012 </div> 3013 <div class="catpaw-settings-grid__full"> 3014 <span class="settings-field__label">Print color</span> 3015 <div id="catpaw-prints-color-mount"></div> 3016 </div> 3017 </div> 3018 3019 <div class="settings-inline-actions"> 3020 <button type="button" id="catpaw-randomize-colors" class="settings-btn settings-btn--compact catpaw-procedural-only"> 3021 <i class="fa-solid fa-palette fa-width-auto" aria-hidden="true"></i> Randomize colors 3022 </button> 3023 <button type="button" id="catpaw-randomize-shape" class="settings-btn settings-btn--compact"> 3024 <i class="fa-solid fa-sliders fa-width-auto" aria-hidden="true"></i> Randomize shape & motion 3025 </button> 3026 <button type="button" id="catpaw-randomize-all" class="settings-btn settings-btn--compact"> 3027 <i class="fa-solid fa-shuffle fa-width-auto" aria-hidden="true"></i> Randomize all paw options 3028 </button> 3029 </div> 3030 </div> 3031 </div> 3032 </section> 3033 3034 <section class="settings-section settings-section--compact" data-settings-tab="effects" data-settings-section="effects" data-settings-search="performance effects overlay sounds sparks weather seasonal home landing"> 3035 <h2 class="settings-section__title"><i class="fa-solid fa-wand-magic-sparkles fa-width-auto" aria-hidden="true"></i> Effects & performance</h2> 3036 3037 <label class="settings-row settings-row--toggle settings-row--compact" data-settings-search="performance mode fps lag slow"> 3038 <span class="settings-row__label"> 3039 <i class="fa-solid fa-bolt fa-width-auto settings-row__icon" aria-hidden="true"></i> 3040 <span>Performance mode <small>(disables optional effects)</small></span> 3041 </span> 3042 <span class="settings-switch"> 3043 <input type="checkbox" id="setting-performanceMode" class="settings-switch__input"> 3044 <span class="settings-switch__track" aria-hidden="true"></span> 3045 </span> 3046 </label> 3047 3048 <div id="visual-effects-section" class="settings-subsection"> 3049 <p class="settings-section__hint">Optional overlays â off when performance mode is on</p> 3050 3051 <label class="settings-row settings-row--toggle settings-row--compact" data-settings-search="ui sounds clicks whoosh audio">
3052 <span class="settings-row__label"><i class="fa-solid fa-volume-high fa-width-auto settings-row__icon" aria-hidden="true"></i><span>UI sounds</span></span> 3053 <span class="settings-switch"><input type="checkbox" id="setting-uiSounds" class="settings-switch__input"><span class="settings-switch__track" aria-hidden="true"></span></span> 3054 </label> 3055 <label class="settings-row settings-row--toggle settings-row--compact" data-settings-search="click sparks burst particles"> 3056 <span class="settings-row__label"><i class="fa-solid fa-burst fa-width-auto settings-row__icon" aria-hidden="true"></i><span>Click sparks</span></span> 3057 <span class="settings-switch"><input type="checkbox" id="setting-clickSparks" class="settings-switch__input"><span class="settings-switch__track" aria-hidden="true"></span></span> 3058 </label> 3059 <label class="settings-row settings-row--toggle settings-row--compact" data-settings-search="seasonal holiday halloween christmas overlay"> 3060 <span class="settings-row__label"><i class="fa-solid fa-calendar-days fa-width-auto settings-row__icon" aria-hidden="true"></i><span>Seasonal overlay</span></span> 3061 <span class="settings-switch"><input type="checkbox" id="setting-seasonalOverlay" class="settings-switch__input"><span class="settings-switch__track" aria-hidden="true"></span></span> 3062 </label> 3063 <label class="settings-row settings-row--toggle settings-row--compact" data-settings-search="weather rain snow overlay"> 3064 <span class="settings-row__label"><i class="fa-solid fa-cloud-sun-rain fa-width-auto settings-row__icon" aria-hidden="true"></i><span>Weather overlay</span></span> 3065 <span class="settings-switch"><input type="checkbox" id="setting-weatherOverlay" class="settings-switch__input"><span class="settings-switch__track" aria-hidden="true"></span></span> 3066 </label> 3067 <label class="settings-row settings-row--toggle settings-row--compact" data-settings-search="home landing animation index homepage fluid"> 3068 <span class="settings-row__label"><i class="fa-solid fa-water fa-width-auto settings-row__icon" aria-hidden="true"></i><span>Home landing animation <small>(index)</small></span></span> 3069 <span class="settings-switch"><input type="checkbox" id="setting-homeLanding" class="settings-switch__input"><span class="settings-switch__track" aria-hidden="true"></span></span> 3070 </label> 3071 </div> 3072 </section> 3073 3074 <footer class="settings-footer" data-settings-tab="advanced" data-settings-search="reset export import tour logout backup share music preload boot" data-settings-search-ignore> 3075 <div class="settings-card settings-card--compact" data-settings-search="music preload boot cache metadata songs library faster load"> 3076 <span class="settings-field__label"><i class="fa-solid fa-music fa-width-auto" aria-hidden="true"></i> Music library preload</span> 3077 <p class="settings-section__hint">Controls how much music data loads during CitrusOS boot. Turn down for faster startup if you rarely use Music.</p> 3078 <div class="settings-radio-grid settings-radio-grid--scale"> 3079 <label class="settings-radio"> 3080 <input type="radio" name="musicPreload" value="off"> 3081 <span class="settings-radio__box"><span>Off</span><small>Fastest boot</small></span> 3082 </label> 3083 <label class="settings-radio"> 3084 <input type="radio" name="musicPreload" value="metadata"> 3085 <span class="settings-radio__box"><span>Metadata</span><small>Durations only</small></span> 3086 </label> 3087 <label class="settings-radio"> 3088 <input type="radio" name="musicPreload" value="full">
3089 <span class="settings-radio__box"><span>Full</span><small>Cache songs</small></span> 3090 </label> 3091 </div> 3092 </div> 3093 <div class="settings-footer__actions"> 3094 <button type="button" id="settings-export" class="settings-btn"> 3095 <i class="fa-solid fa-file-export fa-width-auto" aria-hidden="true"></i> Export 3096 </button> 3097 <button type="button" id="settings-import" class="settings-btn"> 3098 <i class="fa-solid fa-file-import fa-width-auto" aria-hidden="true"></i> Import 3099 </button> 3100 <button type="button" id="settings-reset" class="settings-btn"> 3101 <i class="fa-solid fa-rotate-left fa-width-auto" aria-hidden="true"></i> Reset defaults 3102 </button> 3103 <button type="button" id="settings-replay-home-tour" class="settings-btn"> 3104 <i class="fa-solid fa-route fa-width-auto" aria-hidden="true"></i> Replay site tour 3105 </button> 3106 <button type="button" id="settings-logout" class="settings-btn settings-btn--danger"> 3107 <i class="fa-solid fa-right-from-bracket fa-width-auto" aria-hidden="true"></i> Log out 3108 </button> 3109 </div> 3110 <p class="settings-footer__note">Settings are saved locally in your browser~ :3c</p> 3111 </footer> 3112 `; 3113 3114 // Append to page 3115 if (isCitrusOsPage()) { 3116 panel.id = 'settings-panel'; 3117 panel.style.display = 'none'; 3118 document.body.appendChild(panel); 3119 } else { 3120 document.body.appendChild(backdrop); 3121 document.body.appendChild(panel); 3122 } 3123 3124 // Check if we're on chat page and show warning 3125 const isOnChatPage = window.location.pathname.includes('chat.html') || 3126 window.location.pathname.endsWith('chat'); 3127 3128 if (isOnChatPage) { 3129 // Show warning 3130 const warning = panel.querySelector('#follower-warning'); 3131 if (warning) { 3132 warning.style.display = 'block'; 3133 } 3134 3135 // Disable follower options (except "none") and force "none" selection 3136 panel.querySelectorAll('input[name="follower"]').forEach(radio => { 3137 var label = radio.closest('.settings-radio'); 3138 if (radio.value !== 'none') { 3139 radio.disabled = true; 3140 if (label) label.classList.add('is-disabled'); 3141 } else { 3142 radio.checked = true; 3143 } 3144 }); 3145 3146 emitDevLog('â ï¸ Settings menu: Chat page detected, followers disabled'); 3147 } else { 3148 const warning = panel.querySelector('#follower-warning'); 3149 if (warning) { 3150 warning.style.display = 'none'; 3151 } 3152 3153 panel.querySelectorAll('input[name="follower"]').forEach(radio => { 3154 radio.disabled = false; 3155 var label = radio.closest('.settings-radio'); 3156 if (label) label.classList.remove('is-disabled'); 3157 }); 3158 } 3159 3160 // Event listeners 3161 if (backdrop.parentNode) { 3162 backdrop.addEventListener('click', closeSettings); 3163 } 3164 var closeBtn = panel.querySelector('#settings-close'); 3165 if (closeBtn) { 3166 closeBtn.addEventListener('click', closeSettings); 3167 } 3168 panel.querySelector('#settings-reset').addEventListener('click', resetSettings); 3169 3170 var replayTourBtn = panel.querySelector('#settings-replay-home-tour'); 3171 if (replayTourBtn) { 3172 replayTourBtn.addEventListener('click', function () { 3173 if (window.HomeTour && typeof window.HomeTour.replay === 'function') { 3174 window.HomeTour.replay(); 3175 } else if (typeof window.replayHomeTour === 'function') { 3176 window.replayHomeTour(); 3177 } else { 3178 try { 3179 sessionStorage.setItem('pupwife_replayHomeTour', '1'); 3180 } catch (e) {} 3181 window.location.href = 'index.html'; 3182 } 3183 }); 3184 } 3185 3186 // Setting change listeners (skip nested woim / stretchy-paw panel toggles â they have their own handlers) 3187 panel.querySelectorAll('input[type="checkbox"]').forEach(checkbox => { 3188 var id = checkbox.id || ''; 3189 if (id.indexOf('setting-catpaw-') === 0 || id.indexOf('setting-woim-') === 0 || id.indexOf('setting-jellypuff-') === 0) return; 3190 if (checkbox.id === 'setting-darkMode') { 3191 checkbox.addEventListener('change', handleDarkModeToggle); 3192 } else { 3193 checkbox.addEventListener('change', handleSettingChange); 3194 } 3195 }); 3196 3197 panel.querySelectorAll('input[name="follower"]').forEach(radio => { 3198 radio.addEventListener('change', handleFollowerChange); 3199 }); 3200 3201 panel.querySelectorAll('input[name="citrusUiScale"]').forEach(function (radio) { 3202 radio.addEventListener('change', handleCitrusUiScaleChange); 3203 }); 3204 3205 panel.querySelectorAll('input[name="desktopIconScale"]').forEach(function (radio) { 3206 radio.addEventListener('change', handleDesktopIconScaleChange); 3207 }); 3208 3209 panel.querySelectorAll('input[name="musicPreload"]').forEach(function (radio) { 3210 radio.addEventListener('change', handleMusicPreloadChange); 3211 }); 3212 3213 bindDesktopWallpaperSliders(panel); 3214 bindDesktopIconActionButtons(panel); 3215
3216 syncCitrusSettingsSectionVisibility(); 3217 syncSettingsPanelCitrusTheme(); 3218 3219 mountCursorTrailPicker(panel); 3220 mountCustomCursorPicker(panel); 3221 mountPalettePicker(panel); 3222 mountBackgroundPicker(panel); 3223 mountBackgroundTuning(panel); 3224 updateCloudsBackgroundWarning(); 3225 document.addEventListener('pupwife:clouds-fallback-changed', updateCloudsBackgroundWarning); 3226 mountWoimSettingsPanel(panel); 3227 ensureWoimColorPanelScript().then(function () { 3228 mountJellypuffSettingsPanel(panel); 3229 mountCatPawSettingsPanel(panel); 3230 }); 3231 mountTrailColorPanel(panel); 3232 3233 if (window.PupwifeSettingsSearch && typeof window.PupwifeSettingsSearch.bind === 'function') { 3234 window.PupwifeSettingsSearch.bind(panel); 3235 } 3236 3237 var randomizeBtn = panel.querySelector('#settings-randomize-cursor'); 3238 if (randomizeBtn) { 3239 randomizeBtn.addEventListener('click', function () { 3240 if (window.PupwifeSettingsDiscoverability) { 3241 window.PupwifeSettingsDiscoverability.markSeen('settings', 'randomize-cursor'); 3242 } 3243 if (window.PupwifeSettingsExportImport && typeof window.PupwifeSettingsExportImport.openRandomizeDialog === 'function') { 3244 window.PupwifeSettingsExportImport.openRandomizeDialog(randomizeCursorSettings); 3245 } 3246 }); 3247 } 3248 3249 var exportBtn = panel.querySelector('#settings-export'); 3250 if (exportBtn) { 3251 exportBtn.addEventListener('click', function () { 3252 if (window.PupwifeSettingsDiscoverability) { 3253 window.PupwifeSettingsDiscoverability.markSeen('settings', 'export-import'); 3254 } 3255 if (window.PupwifeSettingsExportImport && typeof window.PupwifeSettingsExportImport.openExportDialog === 'function') { 3256 window.PupwifeSettingsExportImport.openExportDialog(currentSettings); 3257 } 3258 }); 3259 } 3260 3261 var importBtn = panel.querySelector('#settings-import'); 3262 if (importBtn) { 3263 importBtn.addEventListener('click', function () { 3264 if (window.PupwifeSettingsDiscoverability) { 3265 window.PupwifeSettingsDiscoverability.markSeen('settings', 'export-import'); 3266 } 3267 if (window.PupwifeSettingsExportImport && typeof window.PupwifeSettingsExportImport.openImportDialog === 'function') { 3268 window.PupwifeSettingsExportImport.openImportDialog(applyImportedSettings); 3269 } 3270 }); 3271 } 3272 3273 applyNewFeatureBadges(panel); 3274 3275 const trailAmountSlider = panel.querySelector('#setting-trailAmount'); 3276 if (trailAmountSlider) { 3277 trailAmountSlider.addEventListener('input', (e) => { 3278 updateTrailAmountLabel(panel); 3279 }); 3280 trailAmountSlider.addEventListener('change', (e) => { 3281 const amount = parseTrailAmount(e.target.value); 3282 e.target.value = String(amount); 3283 setSetting('trailAmount', amount); 3284 updateTrailAmountLabel(panel); 3285 }); 3286 } 3287 3288 // Weather overlay checkbox 3289 const weatherOverlayCheckbox = panel.querySelector('#setting-weatherOverlay'); 3290 if (weatherOverlayCheckbox) { 3291 weatherOverlayCheckbox.addEventListener('change', (e) => { 3292 setSetting('weatherOverlay', e.target.checked); 3293 // Toggle is handled by applySettings() 3294 }); 3295 } 3296 3297 // Skeletile regeneration button 3298 const regenerateBtn = panel.querySelector('#regenerate-skeletile-btn'); 3299 if (regenerateBtn) { 3300 regenerateBtn.addEventListener('click', () => { 3301 if (window.skeletile && typeof window.skeletile.regenerate === 'function') { 3302 window.skeletile.regenerate(); 3303 emitDevLog('𦴠Skeletile regenerated!'); 3304 } else { 3305 emitDevWarn('Skeletile regenerate function not available'); 3306 } 3307 }); 3308 3309 } 3310 3311 // Dragon regeneration button 3312 const regenerateDragonBtn = panel.querySelector('#regenerate-dragon-btn'); 3313 if (regenerateDragonBtn) { 3314 regenerateDragonBtn.addEventListener('click', () => { 3315 if (window.DragonFollower && typeof window.DragonFollower.regenerate === 'function') { 3316 window.DragonFollower.regenerate(); 3317 emitDevLog('ð Dragon regenerated!'); 3318 // Ensure dragon is shown after regeneration 3319 // Check if follower switcher is available and dragon is selected 3320 if (currentSettings.follower === 'dragon') { 3321 setTimeout(() => { 3322 // Trigger showDragon through followerSwitcher if available 3323 if (window.switchFollower) { 3324 // Just ensure it's visible - the regenerate already handles initialization 3325 const dragonContainer = document.getElementById('dragon-follower'); 3326 if (dragonContainer) { 3327 dragonContainer.style.display = ''; 3328 dragonContainer.style.opacity = '1'; 3329 } 3330 } 3331 }, 200); 3332 } 3333 } else { 3334 emitDevWarn('Dragon regenerate function not available'); 3335 } 3336 }); 3337 3338 } 3339 3340 // Close with Escape key 3341 document.addEventListener('keydown', (e) => { 3342 if (e.key !== 'Escape') return;
3343 if (isCitrusOsPage() && window.CitrusSettingsWindow && window.CitrusSettingsWindow.isOpen()) { 3344 if (window.PupwifeSettingsColorPicker && 3345 typeof window.PupwifeSettingsColorPicker.isOpen === 'function' && 3346 window.PupwifeSettingsColorPicker.isOpen()) { 3347 window.PupwifeSettingsColorPicker.closeOpen(); 3348 return; 3349 } 3350 closeSettings(); 3351 return; 3352 } 3353 const panel = document.getElementById('settings-panel'); 3354 if (panel && panel.classList.contains('settings-panel--open')) { 3355 if (window.PupwifeSettingsColorPicker && 3356 typeof window.PupwifeSettingsColorPicker.isOpen === 'function' && 3357 window.PupwifeSettingsColorPicker.isOpen()) { 3358 window.PupwifeSettingsColorPicker.closeOpen(); 3359 return; 3360 } 3361 closeSettings(); 3362 } 3363 }); 3364 3365 // Regenerate Skeletile with R key when Skeletile is selected 3366 document.addEventListener('keydown', (e) => { 3367 // Only trigger if R is pressed and not typing in an input/textarea 3368 if (e.key === 'r' || e.key === 'R') { 3369 const activeElement = document.activeElement; 3370 const isInputFocused = activeElement && ( 3371 activeElement.tagName === 'INPUT' || 3372 activeElement.tagName === 'TEXTAREA' || 3373 activeElement.isContentEditable 3374 ); 3375 3376 // Only regenerate if not typing in an input field 3377 if (!isInputFocused) { 3378 if (currentSettings.follower === 'skeletile') { 3379 if (window.skeletile && typeof window.skeletile.regenerate === 'function') { 3380 window.skeletile.regenerate(); 3381 emitDevLog('𦴠Skeletile regenerated via R key!'); 3382 } 3383 } else if (currentSettings.follower === 'dragon') { 3384 if (window.DragonFollower && typeof window.DragonFollower.regenerate === 'function') { 3385 window.DragonFollower.regenerate(); 3386 emitDevLog('ð Dragon regenerated via R key!'); 3387 // Ensure dragon is shown after regeneration 3388 setTimeout(() => { 3389 const dragonContainer = document.getElementById('dragon-follower'); 3390 if (dragonContainer) { 3391 dragonContainer.style.display = ''; 3392 dragonContainer.style.opacity = '1'; 3393 } 3394 }, 200); 3395 } 3396 } 3397 } 3398 } 3399 }); 3400 3401 // Logout button 3402 const logoutBtn = panel.querySelector('#settings-logout'); 3403 if (logoutBtn) { 3404 logoutBtn.addEventListener('click', () => { 3405 if (confirm('Are you sure you want to log out? You will need to enter your password to log back in.')) { 3406 if (window.__sharedUsername && typeof window.__sharedUsername.logout === 'function') { 3407 window.__sharedUsername.logout(); 3408 } else { 3409 // Fallback if API not available 3410 localStorage.removeItem('puppy_shared_username'); 3411 localStorage.removeItem('puppy_username_password'); 3412 location.reload(); 3413 } 3414 } 3415 }); 3416 } 3417 3418 syncSettingsPanelChatroomTheme(); 3419 updateAccountIndicator(); 3420 applyChatroomSiteSettingsNotice(); 3421 emitDevLog('Settings menu initialized!'); 3422 } 3423 3424 function isChatroomPage() { 3425 return /chatroom/.test(window.location.pathname || ''); 3426 } 3427 3428 function isChatroomDarkTheme() { 3429 if (window.ChatroomSettings && typeof window.ChatroomSettings.getColorMode === 'function') { 3430 return window.ChatroomSettings.getColorMode() === 'dark'; 3431 } 3432 return document.documentElement.getAttribute('data-color-mode') !== 'light'; 3433 } 3434 3435 function applyChatroomSiteSettingsNotice() { 3436 const panel = document.getElementById('settings-panel'); 3437 if (!panel) return; 3438 3439 const followerSection = panel.querySelector('[data-settings-section="follower"]'); 3440 const visualSection = panel.querySelector('#visual-effects-section'); 3441 const trailSection = panel.querySelector('#settings-cursor-section'); 3442 const perfSection = panel.querySelector('[data-settings-section="effects"]'); 3443 3444 let notice = panel.querySelector('#settings-chatroom-effects-notice'); 3445 if (isChatroomPage()) { 3446 if (!notice) { 3447 notice = document.createElement('div'); 3448 notice.id = 'settings-chatroom-effects-notice'; 3449 notice.className = 'settings-alert'; 3450 notice.setAttribute('role', 'status'); 3451 notice.innerHTML = 3452 '<i class="fa-solid fa-circle-info fa-width-auto" aria-hidden="true"></i>' + 3453 '<span>Homepage effects aren\'t available on this page.</span>'; 3454 const appearanceCard = panel.querySelector('#settings-appearance-card'); 3455 if (appearanceCard) { 3456 appearanceCard.insertAdjacentElement('afterend', notice); 3457 } else { 3458 panel.querySelector('.settings-panel__header')?.after(notice); 3459 } 3460 } 3461 notice.style.display = ''; 3462 [followerSection, visualSection, trailSection, perfSection].forEac
3462h(function (section) { 3463 if (!section) return; 3464 section.classList.add('settings-section--chatroom-unavailable'); 3465 section.querySelectorAll('input, select, button').forEach(function (el) { 3466 el.disabled = true; 3467 }); 3468 }); 3469 const subtitle = panel.querySelector('.settings-panel__subtitle'); 3470 if (subtitle) { 3471 subtitle.textContent = 'Light/dark for chatroom · effects apply on other pages'; 3472 } 3473 } else { 3474 if (notice) notice.style.display = 'none'; 3475 [followerSection, visualSection, trailSection, perfSection].forEach(function (section) { 3476 if (!section) return; 3477 section.classList.remove('settings-section--chatroom-unavailable'); 3478 section.querySelectorAll('input, select, button').forEach(function (el) { 3479 el.disabled = false; 3480 }); 3481 }); 3482 } 3483 } 3484 3485 function syncSettingsPanelChatroomTheme() { 3486 const panel = document.getElementById('settings-panel'); 3487 if (!panel) return; 3488 if (isChatroomPage()) { 3489 panel.classList.add('settings-panel--chatroom'); 3490 updateDarkModeUI(isChatroomDarkTheme()); 3491 } else { 3492 panel.classList.remove('settings-panel--chatroom'); 3493 } 3494 } 3495 3496 function syncSettingsPanelCitrusTheme() { 3497 const panel = document.getElementById('settings-panel'); 3498 if (!panel) return; 3499 panel.classList.toggle('settings-panel--citrusos', isCitrusOsPage()); 3500 syncCitrusSettingsSectionVisibility(); 3501 const subtitle = panel.querySelector('.settings-panel__subtitle'); 3502 if (subtitle && !isChatroomPage()) { 3503 subtitle.textContent = isCitrusOsPage() 3504 ? 'CitrusOS system preferences' 3505 : 'Personalize your experience'; 3506 } 3507 } 3508 3509 function syncCitrusSettingsSectionVisibility() { 3510 const citrusSection = document.getElementById('settings-citrusos-section'); 3511 const taskbarSection = document.getElementById('settings-taskbar-section'); 3512 const windowsSection = document.getElementById('settings-windows-section'); 3513 const desktopSection = document.getElementById('settings-desktop-section'); 3514 const wallpaperSection = document.getElementById('settings-citrus-wallpaper-section'); 3515 const onCitrus = isCitrusOsPage(); 3516 if (citrusSection) citrusSection.hidden = !onCitrus; 3517 if (taskbarSection) taskbarSection.hidden = !onCitrus; 3518 if (windowsSection) windowsSection.hidden = !onCitrus; 3519 if (desktopSection) desktopSection.hidden = !onCitrus; 3520 if (wallpaperSection) wallpaperSection.hidden = !onCitrus; 3521 } 3522 3523 function bindCitrusWallpaperSlider(panel, sliderId, labelId, options) { 3524 options = options || {}; 3525 var slider = panel.querySelector('#' + sliderId); 3526 var label = panel.querySelector('#' + labelId); 3527 if (!slider) return; 3528 3529 function formatValue(raw) { 3530 if (typeof options.format === 'function') return options.format(raw); 3531 if (options.suffix) return raw + options.suffix; 3532 return String(raw); 3533 } 3534 3535 slider.addEventListener('input', function () { 3536 var next = clampSettingNumber(slider.value, options.min, options.max, options.fallback); 3537 currentSettings[options.key] = next; 3538 applyCitrusDesktopPrefs(); 3539 if (label) label.textContent = formatValue(String(next)); 3540 }); 3541 slider.addEventListener('change', function () { 3542 var next = clampSettingNumber(slider.value, options.min, options.max, options.fallback); 3543 setSetting(options.key, next); 3544 slider.value = String(next); 3545 if (label) label.textContent = formatValue(String(next)); 3546 }); 3547 } 3548 3549 function bindDesktopWallpaperSliders(panel) { 3550 bindCitrusWallpaperSlider(panel, 'setting-citrusWallpaperDim', 'setting-citrusWallpaperDim-label', { 3551 key: 'citrusWallpaperDim', 3552 min: 0, 3553 max: 50, 3554 fallback: 0, 3555 suffix: '%' 3556 }); 3557 bindCitrusWallpaperSlider(panel, 'setting-citrusWallpaperSaturation', 'setting-citrusWallpaperSaturation-label', { 3558 key: 'citrusWallpaperSaturation', 3559 min: 50, 3560 max: 150, 3561 fallback: 100, 3562 suffix: '%' 3563 }); 3564 bindCitrusWallpaperSlider(panel, 'setting-citrusWallpaperBrightness', 'setting-citrusWallpaperBrightness-label', { 3565 key: 'citrusWallpaperBrightness', 3566 min: 50, 3567 max: 150, 3568 fallback: 100, 3569 suffix: '%' 3570 }); 3571 bindCitrusWallpaperSlider(panel, 'setting-citrusWallpaperBlur', 'setting-citrusWallpaperBlur-label', { 3572 key: 'citrusWallpaperBlur', 3573 min: 0, 3574 max: 12, 3575 fallback: 0, 3576 format: function (raw) { return raw + ' px'; } 3577 }); 3578 3579 var resetBtn = panel.querySelector('#settings-citrus-wallpaper-reset'); 3580 if (resetBtn) { 3581 resetBtn.addEventListener('click', function () { 3582 setSetting('citrusWallpaperDim', DEFAULT_SETTINGS.citrusWallpaperDim); 3583 setSetting('citrusWallpaperSaturation', DEFAULT_SETTINGS.citrusWallpaperSaturation); 3584 setSetting('citrusWallpaperBrightness', DEFAULT_SETTINGS.citrusWallpaperBrightness); 3585 setSetting('citrusWallpaperBlur', DEFAULT_SETTINGS.citrusWallpaperBlur); 3586 updateSettingsUI(); 3587 }); 3588 } 3589 } 3590 3591 function bindDesktopIconActionButtons(panel) { 3592 var resetBtn = panel.querySelector('#settings-desktop-reset-icons'); 3593 if (resetBtn) { 3594 resetBtn.addEventListener('click', function () { 3595 if (window.CitrusDesktop && typeof window.CitrusDesktop.resetDesktopToDefaults === 'function') { 3596 function runReset() { 3597 window.CitrusDesktop.resetDesktopToDefaults(); 3598 } 3599 if (window.CitrusShellDialogs && typeof window.CitrusShellDialogs.confirm === 'function') { 3600 window.CitrusShellDialogs.confirm({
3601 title: 'Reset desktop?', 3602 message: 'Restore factory desktop shortcuts and folder layout?', 3603 confirmLabel: 'Reset', 3604 danger: true 3605 }).then(function (ok) { 3606 if (ok) runReset(); 3607 }); 3608 return; 3609 } 3610 if (window.confirm('Restore factory desktop shortcuts and folder layout?')) { 3611 runReset(); 3612 } 3613 } 3614 }); 3615 } 3616 3617 var arrangeBtn = panel.querySelector('#settings-desktop-auto-arrange'); 3618 if (arrangeBtn) { 3619 arrangeBtn.addEventListener('click', function () { 3620 if (window.CitrusDesktop && typeof window.CitrusDesktop.resetDesktopIconLayout === 'function') { 3621 window.CitrusDesktop.resetDesktopIconLayout(); 3622 } else if (window.CitrusDesktop && typeof window.CitrusDesktop.autoArrangeDesktopIcons === 'function') { 3623 window.CitrusDesktop.autoArrangeDesktopIcons(); 3624 } 3625 }); 3626 } 3627 3628 var folderResetBtn = panel.querySelector('#settings-desktop-reset-folder-layout'); 3629 if (folderResetBtn) { 3630 folderResetBtn.addEventListener('click', function () { 3631 if (window.CitrusDesktop && typeof window.CitrusDesktop.resetFolderLayout === 'function') { 3632 window.CitrusDesktop.resetFolderLayout(); 3633 } 3634 }); 3635 } 3636 } 3637 3638 function onSettingsWindowOpen() { 3639 const panel = document.getElementById('settings-panel'); 3640 if (!panel) return; 3641 3642 syncSettingsPanelChatroomTheme(); 3643 syncSettingsPanelCitrusTheme(); 3644 applyChatroomSiteSettingsNotice(); 3645 3646 if (isChatroomPage() && window.ChatroomSettings && typeof window.ChatroomSettings.getColorMode === 'function') { 3647 const dark = window.ChatroomSettings.getColorMode() === 'dark'; 3648 updateDarkModeUI(dark); 3649 const siteToggle = panel.querySelector('#setting-darkMode'); 3650 if (siteToggle) siteToggle.checked = dark; 3651 } 3652 3653 updateSettingsUI(); 3654 updateCloudsBackgroundWarning(); 3655 if (!backgroundTuningUi && window.SiteBackgroundTuning) { 3656 mountBackgroundTuning(panel); 3657 } else if (backgroundTuningUi && typeof backgroundTuningUi.setActiveBackground === 'function') { 3658 setBackgroundTuningActive(currentSettings.background || DEFAULT_SETTINGS.background); 3659 } 3660 3661 updateAccountIndicator(); 3662 3663 try { 3664 document.dispatchEvent(new CustomEvent('pupwife:settings-opened')); 3665 } catch (e) { /* ignore */ } 3666 3667 emitDevLog('Settings window opened'); 3668 } 3669 3670 function openSettings() { 3671 if (isCitrusOsPage() && window.CitrusSettingsWindow && typeof window.CitrusSettingsWindow.open === 'function') { 3672 window.CitrusSettingsWindow.open(); 3673 return; 3674 } 3675 3676 const backdrop = document.getElementById('settings-backdrop'); 3677 const panel = document.getElementById('settings-panel'); 3678 if (!backdrop || !panel) return; 3679 3680 backdrop.classList.add('settings-backdrop--open'); 3681 panel.classList.add('settings-panel--open'); 3682 onSettingsWindowOpen(); 3683 emitDevLog('Settings menu opened'); 3684 } 3685 3686 function isLoggedInAccount() { 3687 const username = window.__sharedUsername ? window.__sharedUsername.get() : null; 3688 const hasPassword = localStorage.getItem('puppy_username_password') !== null; 3689 const isGuestMode = localStorage.getItem('puppy_guest_mode') === 'true'; 3690 return !!(username && hasPassword && !isGuestMode); 3691 } 3692 3693 function openAccountSignIn() { 3694 if (window.__sharedUsername && typeof window.__sharedUsername.promptAgain === 'function') { 3695 window.__sharedUsername.promptAgain(); 3696 } else { 3697 window.location.href = 'account.html'; 3698 } 3699 } 3700 3701 function updateAccountIndicator() { 3702 const card = document.getElementById('account-access-card'); 3703 const logoutBtn = document.getElementById('settings-logout'); 3704 if (!card) return; 3705 3706 if (isLoggedInAccount()) { 3707 const username = window.__sharedUsername.get(); 3708 card.innerHTML = 3709 '<p class="settings-card__label"><i class="fa-solid fa-circle-user fa-width-auto" aria-hidden="true"></i> Logged in as</p>' + 3710 '<p class="settings-card__value">' + escapeHtml(username) + '</p>' + 3711 '<a href="account.html" class="settings-card__link">' + 3712 'View account <i class="fa-solid fa-arrow-right fa-width-auto" aria-hidden="true"></i></a>'; 3713 if (logoutBtn) logoutBtn.style.display = ''; 3714 } else { 3715 card.innerHTML = 3716 '<p class="settings-card__label"><i class="fa-solid fa-circle-user fa-width-auto" aria-hidden="true"></i> Account</p>' + 3717 '<p class="settings-section__hint" style="margin: 0.35rem 0 0.75rem;">Browsing as a guest is fine! Create an account anytime to save scores and use the chatroom.</p>' + 3718 '<button type="button" id="settings-sign-in" class="settings-card__link settings-card__link--button">' + 3719 '<i class="fa-solid fa-right-to-bracket fa-width-auto" aria-hidden="true"></i> Sign in / Create account</button>' + 3720 '<a href="account.html" class="settings-card__link" style="margin-top: 0.45rem;">Account page <i class="fa-solid fa-arrow-right fa-width-auto" aria-hidden="true"></i></a>';
3721 const signInBtn = card.querySelector('#settings-sign-in'); 3722 if (signInBtn) { 3723 signInBtn.addEventListener('click', function () { 3724 openAccountSignIn(); 3725 }); 3726 } 3727 if (logoutBtn) logoutBtn.style.display = 'none'; 3728 } 3729 3730 if (window.AccountIndicator && typeof window.AccountIndicator.update === 'function') { 3731 window.AccountIndicator.update(); 3732 } 3733 if (typeof window.syncSidebarAccountRow === 'function') { 3734 window.syncSidebarAccountRow(); 3735 } 3736 } 3737 3738 function escapeHtml(text) { 3739 if (text == null) return ''; 3740 const div = document.createElement('div'); 3741 div.textContent = String(text); 3742 return div.innerHTML; 3743 } 3744 3745 function closeSettings() { 3746 if (isCitrusOsPage() && window.CitrusSettingsWindow && typeof window.CitrusSettingsWindow.isOpen === 'function' && window.CitrusSettingsWindow.isOpen()) { 3747 window.CitrusSettingsWindow.close(); 3748 emitDevLog('Settings window closed'); 3749 return; 3750 } 3751 3752 if (window.PupwifeTrailPicker && typeof window.PupwifeTrailPicker.close === 'function') { 3753 window.PupwifeTrailPicker.close(); 3754 } 3755 if (window.PupwifeCursorPicker && typeof window.PupwifeCursorPicker.close === 'function') { 3756 window.PupwifeCursorPicker.close(); 3757 } 3758 const backdrop = document.getElementById('settings-backdrop'); 3759 const panel = document.getElementById('settings-panel'); 3760 if (!panel) return; 3761 3762 if (backdrop) backdrop.classList.remove('settings-backdrop--open'); 3763 panel.classList.remove('settings-panel--open'); 3764 3765 try { 3766 document.dispatchEvent(new CustomEvent('pupwife:settings-closed')); 3767 } catch (e) { 3768 document.dispatchEvent(new Event('pupwife:settings-closed')); 3769 } 3770 3771 emitDevLog('Settings menu closed'); 3772 } 3773 3774 function updateDarkModeUI(isDark) { 3775 const panel = document.getElementById('settings-panel'); 3776 if (!panel) return; 3777 3778 const toggle = panel.querySelector('#setting-darkMode'); 3779 const themeCard = panel.querySelector('#settings-theme-card'); 3780 3781 if (toggle) toggle.checked = isDark; 3782 if (themeCard) themeCard.classList.toggle('is-dark', isDark); 3783 } 3784 3785 function updateSettingsUI() { 3786 const panel = document.getElementById('settings-panel'); 3787 if (!panel) return; 3788 3789 // Check if we're on chat page 3790 const isOnChatPage = window.location.pathname.includes('chat.html') || 3791 window.location.pathname.endsWith('chat'); 3792 3793 // Update checkboxes 3794 for (const key in currentSettings) { 3795 if (key === 'trailAmount') continue; 3796 const checkbox = panel.querySelector(`#setting-${key}`); 3797 if (checkbox && checkbox.type === 'checkbox') { 3798 checkbox.checked = currentSettings[key]; 3799 } 3800 } 3801 3802 // Update dark mode UI (on chatroom, reflect chatroom theme â site darkMode toggle does not apply there) 3803 updateDarkModeUI(isChatroomPage() ? isChatroomDarkTheme() : currentSettings.darkMode); 3804 3805 const citrusScaleRadio = panel.querySelector('input[name="citrusUiScale"][value="' + String(normalizeCitrusUiScale(currentSettings.citrusUiScale)) + '"]'); 3806 if (citrusScaleRadio) { 3807 citrusScaleRadio.checked = true; 3808 } 3809 3810 const desktopScaleRadio = panel.querySelector('input[name="desktopIconScale"][value="' + String(normalizeDesktopIconScale(currentSettings.desktopIconScale)) + '"]'); 3811 if (desktopScaleRadio) { 3812 desktopScaleRadio.checked = true; 3813 } 3814 3815 const musicPreloadRadio = panel.querySelector('input[name="musicPreload"][value="' + normalizeMusicPreload(currentSettings.musicPreload) + '"]'); 3816 if (musicPreloadRadio) { 3817 musicPreloadRadio.checked = true; 3818 } 3819 3820 const dimSlider = panel.querySelector('#setting-citrusWallpaperDim'); 3821 const dimLabel = panel.querySelector('#setting-citrusWallpaperDim-label'); 3822 if (dimSlider) {
3823 dimSlider.value = String(currentSettings.citrusWallpaperDim || 0); 3824 if (dimLabel) dimLabel.textContent = dimSlider.value + '%'; 3825 } 3826 3827 const satSlider = panel.querySelector('#setting-citrusWallpaperSaturation'); 3828 const satLabel = panel.querySelector('#setting-citrusWallpaperSaturation-label'); 3829 if (satSlider) { 3830 satSlider.value = String(currentSettings.citrusWallpaperSaturation != null 3831 ? currentSettings.citrusWallpaperSaturation 3832 : DEFAULT_SETTINGS.citrusWallpaperSaturation); 3833 if (satLabel) satLabel.textContent = satSlider.value + '%'; 3834 } 3835 3836 const brightnessSlider = panel.querySelector('#setting-citrusWallpaperBrightness'); 3837 const brightnessLabel = panel.querySelector('#setting-citrusWallpaperBrightness-label'); 3838 if (brightnessSlider) { 3839 brightnessSlider.value = String(currentSettings.citrusWallpaperBrightness != null 3840 ? currentSettings.citrusWallpaperBrightness 3841 : DEFAULT_SETTINGS.citrusWallpaperBrightness); 3842 if (brightnessLabel) brightnessLabel.textContent = brightnessSlider.value + '%'; 3843 } 3844 3845 const blurSlider = panel.querySelector('#setting-citrusWallpaperBlur'); 3846 const blurLabel = panel.querySelector('#setting-citrusWallpaperBlur-label'); 3847 if (blurSlider) { 3848 blurSlider.value = String(currentSettings.citrusWallpaperBlur != null 3849 ? currentSettings.citrusWallpaperBlur 3850 : DEFAULT_SETTINGS.citrusWallpaperBlur); 3851 if (blurLabel) blurLabel.textContent = blurSlider.value + ' px'; 3852 } 3853 3854 // Update follower radio buttons 3855 // On chat page, show the saved preference but keep it disabled 3856 const followerRadio = panel.querySelector(`input[name="follower"][value="${currentSettings.follower}"]`); 3857 if (followerRadio && !isOnChatPage) { 3858 followerRadio.checked = true; 3859 } else if (isOnChatPage) { 3860 // On chat page, check "none" but show the user's actual preference in the UI 3861 const noneRadio = panel.querySelector(`input[name="follower"][value="none"]`); 3862 if (noneRadio) { 3863 noneRadio.checked = true; 3864 } 3865 // Also visually indicate the user's saved preference 3866 if (followerRadio) { 3867 var savedLabel = followerRadio.closest('.settings-radio'); 3868 if (savedLabel) { 3869 savedLabel.classList.add('is-saved-preference'); 3870 savedLabel.title = 'This is your saved preference (will resume on other pages)'; 3871 } 3872 } 3873 } 3874 3875 // Update cursor trail picker 3876 setCursorTrailPickerValue(currentSettings.cursorTrail); 3877 3878 const trailAmountSlider = panel.querySelector('#setting-trailAmount'); 3879 if (trailAmountSlider) { 3880 trailAmountSlider.value = String(currentSettings.trailAmount); 3881 updateTrailAmountLabel(panel); 3882 } 3883 updateTrailControlsState(); 3884 3885 setCustomCursorPickerValue(currentSettings.customCursor); 3886 3887 setBackgroundPickerValue(currentSettings.background || DEFAULT_SETTINGS.background); 3888 3889 // Update weather overlay checkbox 3890 const weatherOverlayCheckbox = panel.querySelector('#setting-weatherOverlay'); 3891 if (weatherOverlayCheckbox) { 3892 weatherOverlayCheckbox.checked = currentSettings.weatherOverlay || false; 3893 } 3894 3895 // Disable/enable visual effects based on performance mode 3896 const visualSection = panel.querySelector('#visual-effects-section'); 3897 const isPerformanceMode = currentSettings.performanceMode; 3898 3899 if (visualSection) { 3900 visualSection.querySelectorAll('input[type="checkbox"]').forEach(checkbox => { 3901 checkbox.disabled = isPerformanceMode; 3902 var row = checkbox.closest('.settings-row'); 3903 if (row) row.classList.toggle('is-disabled', isPerformanceMode); 3904 }); 3905 } 3906 3907 const regenerateBtn = document.getElementById('regenerate-skeletile-btn'); 3908 if (regenerateBtn) { 3909 regenerateBtn.classList.toggle('is-visible', currentSettings.follower === 'skeletile'); 3910 } 3911 3912 const regenerateDragonBtn = document.getElementById('regenerate-dragon-btn'); 3913 if (regenerateDragonBtn) { 3914 regenerateDragonBtn.classList.toggle('is-visible', currentSettings.follower === 'dragon'); 3915 } 3916 3917 syncWoimSettingsUI(panel); 3918 syncJellypuffSettingsUI(panel); 3919 syncCatPawSettingsUI(panel); 3920 updateTrailColorPanelVisibility(); 3921 syncTrailColorPickers(); 3922 applyNewFeatureBadges(panel); 3923 } 3924 3925 function handleDarkModeToggle(e) { 3926 const value = e.target.checked; 3927 3928 if (isChatroomPage()) { 3929 if (window.ChatroomSettings && typeof window.ChatroomSettings.setColorMode === 'function') { 3930 window.ChatroomSettings.setColorMode(value ? 'dark' : 'light'); 3931 } else { 3932 setSetting('darkMode', value); 3933 document.dispatchEvent(new CustomEvent('pupwife-color-mode', { 3934 detail: { colorMode: value ? 'dark' : 'light', darkMode: value } 3935 })); 3936 } 3937 updateDarkModeUI(value); 3938 emitDevLog(`Chatroom color mode: ${value ? 'dark' : 'light'}`); 3939 return; 3940 } 3941 3942 const isOnThemeExcludedPage = /cookbook/.test(window.location.pathname); 3943 if (isOnThemeExcludedPage) { 3944 setSetting('darkMode', value); 3945 updateDarkModeUI(value); 3946 emitDevLog(`Dark mode: ${value ? 'ON' : 'OFF'} (saved for other pages)`); 3947 return; 3948 } 3949 3950 const isDark = !!value; 3951 currentSettings.darkMode = isDark; 3952 if (window.SiteThemeToggle && typeof window.SiteThemeToggle.setDarkMode === 'function') { 3953 window.SiteThemeToggle.setDarkMode(isDark, 'settings'); 3954 } else { 3955 setSetting('darkMode', isDark); 3956 updateDarkModeUI(isDark); 3957 } 3958 emitDevLog(`Dark mode: ${isDark ? 'ON' : 'OFF'}`); 3959 } 3960 3961 function handleSettingChange(e) { 3962 const settingKey = e.target.id.replace('setting-', ''); 3963 const value = e.target.checked; 3964 3965 setSetting(settingKey, value); 3966 updateSettingsUI(); 3967 3968 emitDevLog(`Setting changed: ${settingKey} = ${value}`); 3969 } 3970 3971 function handleCitrusUiScaleChange(e) { 3972 var scale = normalizeCitrusUiScale(e.target.value); 3973 setSetting('citrusUiScale', scale); 3974 applyCitrusUiScale(scale); 3975 updateSettingsUI(); 3976 emitDevLog('CitrusOS UI scale: ' + scale + '%'); 3977 } 3978 3979 function handleDesktopIconScaleChange(e) { 3980 var scale = normalizeDesktopIconScale(e.target.value); 3981 setSetting('desktopIconScale', scale); 3982 applyCitrusDesktopPrefs(); 3983 updateSettingsUI(); 3984 emitDevLog('Desktop icon scale: ' + scale + '%'); 3985 } 3986 3987 function handleMusicPreloadChange(e) { 3988 var mode = normalizeMusicPreload(e.target.value); 3989 setSetting('musicPreload', mode); 3990 updateSettingsUI(); 3991 emitDevLog('Music preload: ' + mode); 3992 } 3993 3994 function handleFollowerChange(e) { 3995 // Check if we're on chat page 3996 const isOnChatPage = window.location.pathname.includes('chat.html') || 3997 window.location.pathname.endsWith('chat'); 3998 3999 // Don't allow changes on chat page 4000 if (isOnChatPage) { 4001 emitDevLog('â ï¸ Cannot change follower on chat page'); 4002 e.preventDefault(); 4003 return; 4004 } 4005 4006 const followerType = e.target.value; 4007 setSetting('follower', followerType); 4008 4009 // Show/hide Skeletile regenerate button 4010 const regenerateBtn = document.getElementById('regenerate-skeletile-btn'); 4011 if (regenerateBtn) { 4012 regenerateBtn.classList.toggle('is-visible', followerType === 'skeletile'); 4013 } 4014 4015 const regenerateDragonBtn = document.getElementById('regenerate-dragon-btn'); 4016 if (regenerateDragonBtn) { 4017 regenerateDragonBtn.classList.toggle('is-visible', followerType === 'dragon'); 4018 } 4019 4020 updateWoimPanelVisibility(followerType); 4021 updateJellypuffPanelVisibility(followerType); 4022 4023 emitDevLog(`Follower changed to: ${followerType}`); 4024 } 4025 4026 function applyNewFeatureBadges(panel) { 4027 if (!panel || !window.PupwifeSettingsDiscoverability) return; 4028 var items = [ 4029 { sel: '#settings-export', cat: 'settings', id: 'export-import' }, 4030 { sel: '#settings-import', cat: 'settings', id: 'export-import' }, 4031 { sel: '#settings-randomize-cursor', cat: 'settings', id: 'randomize-cursor' } 4032 ];
4033 items.forEach(function (item) { 4034 var el = panel.querySelector(item.sel); 4035 if (!el || el.querySelector('.settings-feature-new')) return; 4036 if (!window.PupwifeSettingsDiscoverability.isNew(item.cat, item.id)) return; 4037 var badge = document.createElement('span'); 4038 badge.className = 'settings-feature-new'; 4039 badge.textContent = 'NEW!'; 4040 el.appendChild(badge); 4041 el.addEventListener('click', function () { 4042 window.PupwifeSettingsDiscoverability.markSeen(item.cat, item.id); 4043 if (badge.parentNode) badge.remove(); 4044 }, { once: true }); 4045 }); 4046 } 4047 4048 function resetSettings() { 4049 if (confirm('reset all settings to defaults? this cannot be undone!')) { 4050 currentSettings = Object.assign({ ...DEFAULT_SETTINGS }, { 4051 woimString: mergeWoimString({}), 4052 jellypuff: mergeJellypuff({}), 4053 catPawArm: mergeCatPawArm({}), 4054 trailColors: window.PupwifeTrailColorSupport 4055 ? window.PupwifeTrailColorSupport.mergeTrailColors(null) 4056 : { primary: '#c8a8ff', secondary: '#ffb3ba', tertiary: '#89cff0' }, 4057 trailFavorites: [] 4058 }); 4059 saveSettings(); 4060 applySettings(); 4061 updateSettingsUI(); 4062 emitDevLog('Settings reset to defaults!'); 4063 } 4064 } 4065 4066 // ==================================== 4067 // INITIALIZATION 4068 // ==================================== 4069 4070 function syncCurrentSettingsFromStorage() { 4071 currentSettings = loadSettings(); 4072 try { 4073 const raw = localStorage.getItem('puppywifey_settings'); 4074 if (raw) { 4075 const parsed = JSON.parse(raw); 4076 if (parsed && typeof parsed === 'object' && 4077 Object.prototype.hasOwnProperty.call(parsed, 'darkMode') && 4078 typeof parsed.darkMode !== 'boolean') { 4079 parsed.darkMode = parseDarkMode(parsed.darkMode, DEFAULT_SETTINGS.darkMode); 4080 localStorage.setItem('puppywifey_settings', JSON.stringify(parsed)); 4081 } 4082 } 4083 } catch (e) {} 4084 } 4085 4086 function init() { 4087 function boot() { 4088 syncCurrentSettingsFromStorage(); 4089 ensureSettingsFontAwesome() 4090 .then(ensureSettingsHelperScripts) 4091 .then(ensurePalettePickerScript) 4092 .then(ensureBackgroundPickerScript) 4093 .then(ensureTrailPickerScript) 4094 .then(ensureCursorPickerScript) 4095 .then(ensureWoimColorPanelScript) 4096 .then(function () { 4097 createSettingsUI(); 4098 applySettings(); 4099 }); 4100 } 4101 4102 if (document.readyState === 'loading') { 4103 document.addEventListener('DOMContentLoaded', boot); 4104 } else { 4105 boot(); 4106 } 4107 } 4108 4109 // ==================================== 4110 // GLOBAL API 4111 // ==================================== 4112 4113 window.SettingsMenu = { 4114 getSetting, 4115 setSetting, 4116 setSettingQuiet, 4117 loadSettings, 4118 mergeWoimString, 4119 setWoimString, 4120 saveSettings, 4121 openSettings: openSettings, 4122 closeSettings: closeSettings, 4123 onWindowOpen: onSettingsWindowOpen, 4124 applyWallpaperFilters: applyWallpaperFilters 4125 }; 4126 4127 document.addEventListener('pupwife-color-mode', function (e) { 4128 if (!e.detail) return; 4129 updateDarkModeUI(e.detail.colorMode === 'dark'); 4130 const toggle = document.getElementById('setting-darkMode'); 4131 if (toggle) { 4132 toggle.checked = e.detail.colorMode === 'dark'; 4133 } 4134 if (e.detail.palette) { 4135 currentSettings.palette = e.detail.palette; 4136 setPalettePickerValue(e.detail.palette); 4137 } 4138 }); 4139 4140 document.addEventListener('pupwife-site-theme', function (e) { 4141 if (!e.detail) return; 4142 if (typeof e.detail.darkMode === 'boolean') { 4143 if (e.detail.source !== 'settings') { 4144 currentSettings.darkMode = e.detail.darkMode; 4145 updateDarkModeUI(e.detail.darkMode); 4146 } 4147 if (e.detail.source === 'home-landing') { 4148 return; 4149 } 4150 } 4151 if (e.detail.palette) { 4152 currentSettings.palette = e.detail.palette; 4153 setPalettePickerValue(e.detail.palette); 4154 } 4155 }); 4156 4157 // Start! 4158 init(); 4159
vendor: 5 bytes, line 4160
4160})();
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.