Zero-copy FTP/HTTP Daemon compatible with all POSIX systems
| 1 | /* ══ SETTINGS VIEW ════════════════════════════════════════════════════════ |
| 2 | * User preferences panel. All settings persist in localStorage. |
| 3 | * ES5 compatible for PS5 browser. |
| 4 | * |
| 5 | * ┌─────────────────────────────────────────────────┐ |
| 6 | * │ ⚙ Settings │ |
| 7 | * │ │ |
| 8 | * │ ┌─ Display ──────────────────────────────────┐ │ |
| 9 | * │ │ Theme [PS5 ▾] │ │ |
| 10 | * │ │ Default View [Grid ▾] │ │ |
| 11 | * │ │ Animations [●━━━] │ │ |
| 12 | * │ └────────────────────────────────────────────┘ │ |
| 13 | * │ │ |
| 14 | * │ ┌─ File Explorer ────────────────────────────┐ │ |
| 15 | * │ │ Show System Folders [━━━○] │ │ |
| 16 | * │ │ Show Hidden Files [━━━○] │ │ |
| 17 | * │ └────────────────────────────────────────────┘ │ |
| 18 | * │ │ |
| 19 | * │ ┌─ About ────────────────────────────────────┐ │ |
| 20 | * │ │ zftpd v1.5.0 │ │ |
| 21 | * │ │ Console File Manager │ │ |
| 22 | * │ │ github.com/seregonwar/zftpd │ │ |
| 23 | * │ └────────────────────────────────────────────┘ │ |
| 24 | * └─────────────────────────────────────────────────┘ |
| 25 | * ═════════════════════════════════════════════════════════════════════════ */ |
| 26 | |
| 27 | var ZFTPD = ZFTPD || {}; |
| 28 | |
| 29 | (function (Z) { |
| 30 | 'use strict'; |
| 31 | |
| 32 | var D = document; |
| 33 | var ICO = Z.ICO; |
| 34 | |
| 35 | /* ══════════════════════════════════════════════════════════════════════ |
| 36 | * DEFAULT SETTINGS |
| 37 | * |
| 38 | * Dangerous PS4/PS5 system folders that can brick the console |
| 39 | * if modified by an inexperienced user: |
| 40 | * |
| 41 | * /system — Core OS binaries |
| 42 | * /system_ex — Extended OS (updates, recovery) |
| 43 | * /preinst — Pre-installed system data |
| 44 | * /preinst2 — Secondary pre-install partition |
| 45 | * /mini-syscore — Minimal system core (safe mode) |
| 46 | * /sandcastle — Sandbox environment |
| 47 | * /update — System update staging area |
| 48 | * /dev — Device nodes |
| 49 | * /proc — Process filesystem |
| 50 | * ══════════════════════════════════════════════════════════════════════ */ |
| 51 | |
| 52 | var DEFAULTS = { |
| 53 | showSystemFolders: false, |
| 54 | showHiddenFiles: false, |
| 55 | defaultView: 'grid', |
| 56 | animationsEnabled: true |
| 57 | }; |
| 58 | |
| 59 | var DANGEROUS_FOLDERS = [ |
| 60 | 'system', 'system_ex', 'preinst', 'preinst2', |
| 61 | 'mini-syscore', 'sandcastle', 'update', |
| 62 | 'dev', 'proc' |
| 63 | ]; |
| 64 | |
| 65 | /* ── Load / Save ── */ |
| 66 | |
| 67 | Z.settings = {}; |
| 68 | |
| 69 | Z.loadSettings = function () { |
| 70 | var saved = {}; |
| 71 | try { |
| 72 | var raw = localStorage.getItem('zftpd_settings'); |
| 73 | if (raw) saved = JSON.parse(raw); |
| 74 | } catch (e) { /* ignore */ } |
| 75 | |
| 76 | var key; |
| 77 | for (key in DEFAULTS) { |
| 78 | if (DEFAULTS.hasOwnProperty(key)) { |
| 79 | Z.settings[key] = saved.hasOwnProperty(key) ? saved[key] : DEFAULTS[key]; |
| 80 | } |
| 81 | } |
| 82 | }; |
| 83 | |
| 84 | Z.saveSettings = function () { |
| 85 | try { |
| 86 | localStorage.setItem('zftpd_settings', JSON.stringify(Z.settings)); |
| 87 | } catch (e) { /* ignore */ } |
| 88 | }; |
| 89 | |
| 90 | /* ── Dangerous folder check ── */ |
| 91 | |
| 92 | Z.isDangerousFolder = function (name) { |
| 93 | var lower = (name || '').toLowerCase(); |
| 94 | for (var i = 0; i < DANGEROUS_FOLDERS.length; i++) { |
| 95 | if (lower === DANGEROUS_FOLDERS[i]) return true; |
| 96 | } |
| 97 | return false; |
| 98 | }; |
| 99 | |
| 100 | Z.isHiddenFile = function (name) { |
| 101 | return (name || '').charAt(0) === '.'; |
| 102 | }; |
| 103 | |
| 104 | /* ── Init on load ── */ |
| 105 | Z.loadSettings(); |
| 106 | |
| 107 | /* ══════════════════════════════════════════════════════════════════════ |
| 108 | * SETTINGS VIEW RENDERER |
| 109 | * ══════════════════════════════════════════════════════════════════════ */ |
| 110 | |
| 111 | var settings = {}; |
| 112 | |
| 113 | settings.refresh = function () { |
| 114 | var container = D.getElementById('settings-content'); |
| 115 | if (!container) return; |
| 116 | |
| 117 | Z.loadSettings(); |
| 118 | |
| 119 | container.innerHTML = |
| 120 | /* ── Header ── */ |
| 121 | '<div class="sett-header">' + |
| 122 | '<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>' + |
| 123 | '<span>Settings</span>' + |
| 124 | '</div>' + |
| 125 | |
| 126 | /* ══ Display Section ══ */ |
| 127 | '<div class="sett-section">' + |
| 128 | '<div class="sett-section-title">' + |
| 129 | '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect width="20" height="14" x="2" y="3" rx="2"/><line x1="8" x2="16" y1="21" y2="21"/><line x1="12" x2="12" y1="17" y2="21"/></svg>' + |
| 130 | ' Display' + |
| 131 | '</div>' + |
| 132 | |
| 133 | /* Theme selector */ |
| 134 | '<div class="sett-row">' + |
| 135 | '<div class="sett-label">' + |
| 136 | '<div class="sett-name">Theme</div>' + |
| 137 | '<div class="sett-desc">Choose your color scheme</div>' + |
| 138 | '</div>' + |
| 139 | '<select id="sett-theme" class="sett-select">' + |
| 140 | _buildThemeOptions() + |
| 141 | '</select>' + |
| 142 | '</div>' + |
| 143 | |
| 144 | /* Default View */ |
| 145 | '<div class="sett-row">' + |
| 146 | '<div class="sett-label">' + |
| 147 | '<div class="sett-name">Default View</div>' + |
| 148 | '<div class="sett-desc">File explorer layout mode</div>' + |
| 149 | '</div>' + |
| 150 | '<select id="sett-default-view" class="sett-select">' + |
| 151 | '<option value="grid"' + (Z.settings.defaultView === 'grid' ? ' selected' : '') + '>Grid</option>' + |
| 152 | '<option value="list"' + (Z.settings.defaultView === 'list' ? ' selected' : '') + '>List</option>' + |
| 153 | '<option value="details"' + (Z.settings.defaultView === 'details' ? ' selected' : '') + '>Details</option>' + |
| 154 | '</select>' + |
| 155 | '</div>' + |
| 156 | |
| 157 | /* Animations */ |
| 158 | _buildToggleRow('sett-animations', 'Animations', 'Smooth transitions and micro-animations', Z.settings.animationsEnabled) + |
| 159 | |
| 160 | '</div>' + |
| 161 | |
| 162 | /* ══ File Explorer Section ══ */ |
| 163 | '<div class="sett-section">' + |
| 164 | '<div class="sett-section-title">' + |
| 165 | '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>' + |
| 166 | ' File Explorer' + |
| 167 | '</div>' + |
| 168 | |
| 169 | _buildToggleRow('sett-sys-folders', 'Show System Folders', |
| 170 | 'Reveal system, system_ex, preinst and other protected directories. Modifying these can brick your console.', |
| 171 | Z.settings.showSystemFolders, true) + |
| 172 | |
| 173 | _buildToggleRow('sett-hidden', 'Show Hidden Files', |
| 174 | 'Show files and folders starting with a dot (.)', |
| 175 | Z.settings.showHiddenFiles) + |
| 176 | |
| 177 | '</div>' + |
| 178 | |
| 179 | /* ══ Hardware & System ══ */ |
| 180 | '<div class="sett-section">' + |
| 181 | '<div class="sett-section-title">' + |
| 182 | '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="4" y="4" width="16" height="16" rx="2" ry="2"/><rect x="9" y="9" width="6" height="6"/><line x1="9" y1="1" x2="9" y2="4"/><line x1="15" y1="1" x2="15" y2="4"/><line x1="9" y1="20" x2="9" y2="23"/><line x1="15" y1="20" x2="15" y2="23"/><line x1="20" y1="9" x2="23" y2="9"/><line x1="20" y1="14" x2="23" y2="14"/><line x1="1" y1="9" x2="4" y2="9"/><line x1="1" y1="14" x2="4" y2="14"/></svg>' + |
| 183 | ' Console Hardware' + |
| 184 | '</div>' + |
| 185 | _buildSliderRow('sett-fan-speed', 'Target Temperature', |
| 186 | 'Threshold for SMC fan control algorithm. A lower temp generally means higher RPM. (PS4/PS5 payload exclusive)', |
| 187 | Z.settings.fanThreshold || 60, 40, 90, '°C') + |
| 188 | '</div>' + |
| 189 | |
| 190 | /* ══ Danger Zone ══ */ |
| 191 | '<div class="sett-section sett-danger-info">' + |
| 192 | '<div class="sett-section-title">' + |
| 193 | '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>' + |
| 194 | ' Protected Directories' + |
| 195 | '</div>' + |
| 196 | '<div class="sett-danger-list">' + |
| 197 | _buildDangerList() + |
| 198 | '</div>' + |
| 199 | '</div>' + |
| 200 | |
| 201 | /* ══ About Section ══ */ |
| 202 | '<div class="sett-section">' + |
| 203 | '<div class="sett-section-title">' + |
| 204 | '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>' + |
| 205 | ' About' + |
| 206 | '</div>' + |
| 207 | '<div class="sett-about">' + |
| 208 | '<div class="sett-about-logo">' + |
| 209 | '<img src="assets/zftpd-logo.png" alt="zftpd" style="width:48px;height:48px;object-fit:contain;">' + |
| 210 | '</div>' + |
| 211 | '<div class="sett-about-info">' + |
| 212 | '<div class="sett-about-name">zftpd</div>' + |
| 213 | '<div class="sett-about-ver">v1.5.0</div>' + |
| 214 | '<div class="sett-about-desc">The ultimate PS4/PS5 file manager.<br>FTP + HTTP + PKG installer.</div>' + |
| 215 | '<div class="sett-about-link">' + |
| 216 | '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.4 5.4 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65S8.93 17.38 9 18v4"/><path d="M9 18c-4.51 2-5-2-7-2"/></svg>' + |
| 217 | ' github.com/seregonwar/zftpd' + |
| 218 | '</div>' + |
| 219 | '</div>' + |
| 220 | '</div>' + |
| 221 | '</div>'; |
| 222 | |
| 223 | /* ── Wire up event handlers ── */ |
| 224 | _bindEvents(); |
| 225 | }; |
| 226 | |
| 227 | /* ── HTML Helpers ── */ |
| 228 | |
| 229 | function _buildThemeOptions() { |
| 230 | var cur = D.documentElement.getAttribute('data-theme') || 'ps5'; |
| 231 | var html = ''; |
| 232 | for (var i = 0; i < Z.THEMES.length; i++) { |
| 233 | var t = Z.THEMES[i]; |
| 234 | html += '<option value="' + t.id + '"' + (t.id === cur ? ' selected' : '') + '>' + t.name + ' — ' + t.desc + '</option>'; |
| 235 | } |
| 236 | return html; |
| 237 | } |
| 238 | |
| 239 | function _buildToggleRow(id, name, desc, checked, isDanger) { |
| 240 | return '<div class="sett-row">' + |
| 241 | '<div class="sett-label">' + |
| 242 | '<div class="sett-name">' + (isDanger ? '<span class="sett-warn-dot"></span>' : '') + name + '</div>' + |
| 243 | '<div class="sett-desc">' + desc + '</div>' + |
| 244 | '</div>' + |
| 245 | '<label class="sett-toggle">' + |
| 246 | '<input type="checkbox" id="' + id + '"' + (checked ? ' checked' : '') + '>' + |
| 247 | '<span class="sett-toggle-track' + (isDanger ? ' danger' : '') + '">' + |
| 248 | '<span class="sett-toggle-thumb"></span>' + |
| 249 | '</span>' + |
| 250 | '</label>' + |
| 251 | '</div>'; |
| 252 | } |
| 253 | |
| 254 | function _buildSliderRow(id, name, desc, value, min, max, unit, isDanger) { |
| 255 | return '<div class="sett-row" style="flex-wrap:wrap;">' + |
| 256 | '<div class="sett-label" style="flex:1 1 200px;">' + |
| 257 | '<div class="sett-name">' + (isDanger ? '<span class="sett-warn-dot"></span>' : '') + name + '</div>' + |
| 258 | '<div class="sett-desc">' + desc + '</div>' + |
| 259 | '</div>' + |
| 260 | '<div class="sett-slider-wrap" style="display:flex;align-items:center;gap:12px;flex:1 1 200px;justify-content:flex-end;">' + |
| 261 | '<input type="range" id="' + id + '" min="' + min + '" max="' + max + '" value="' + value + '" style="flex:auto;max-width:200px;">' + |
| 262 | '<span id="' + id + '-val" style="min-width:40px;text-align:right;font-weight:600;font-variant-numeric:tabular-nums;">' + value + unit + '</span>' + |
| 263 | '</div>' + |
| 264 | '</div>'; |
| 265 | } |
| 266 | |
| 267 | function _buildDangerList() { |
| 268 | var html = ''; |
| 269 | for (var i = 0; i < DANGEROUS_FOLDERS.length; i++) { |
| 270 | html += '<span class="sett-danger-tag">/' + DANGEROUS_FOLDERS[i] + '</span>'; |
| 271 | } |
| 272 | return html; |
| 273 | } |
| 274 | |
| 275 | /* ── Event Binding ── */ |
| 276 | |
| 277 | function _bindEvents() { |
| 278 | /* Theme */ |
| 279 | var themeSel = D.getElementById('sett-theme'); |
| 280 | if (themeSel) { |
| 281 | themeSel.onchange = function () { |
| 282 | Z.setTheme(this.value); |
| 283 | /* Re-render to update the theme selector */ |
| 284 | }; |
| 285 | } |
| 286 | |
| 287 | /* Default view */ |
| 288 | var viewSel = D.getElementById('sett-default-view'); |
| 289 | if (viewSel) { |
| 290 | viewSel.onchange = function () { |
| 291 | Z.settings.defaultView = this.value; |
| 292 | Z.saveSettings(); |
| 293 | }; |
| 294 | } |
| 295 | |
| 296 | /* Animations */ |
| 297 | var animCb = D.getElementById('sett-animations'); |
| 298 | if (animCb) { |
| 299 | animCb.onchange = function () { |
| 300 | Z.settings.animationsEnabled = this.checked; |
| 301 | Z.saveSettings(); |
| 302 | /* Toggle CSS animations globally */ |
| 303 | if (!this.checked) { |
| 304 | D.documentElement.style.setProperty('--transition-fast', '0s'); |
| 305 | D.documentElement.style.setProperty('--transition-med', '0s'); |
| 306 | D.documentElement.style.setProperty('--transition-slow', '0s'); |
| 307 | } else { |
| 308 | D.documentElement.style.removeProperty('--transition-fast'); |
| 309 | D.documentElement.style.removeProperty('--transition-med'); |
| 310 | D.documentElement.style.removeProperty('--transition-slow'); |
| 311 | } |
| 312 | }; |
| 313 | } |
| 314 | |
| 315 | /* Show system folders */ |
| 316 | var sysCb = D.getElementById('sett-sys-folders'); |
| 317 | if (sysCb) { |
| 318 | sysCb.onchange = function () { |
| 319 | Z.settings.showSystemFolders = this.checked; |
| 320 | Z.saveSettings(); |
| 321 | Z.toast(this.checked ? 'System folders visible' : 'System folders hidden', this.checked ? 'wn' : 'ok'); |
| 322 | }; |
| 323 | } |
| 324 | |
| 325 | /* Show hidden files */ |
| 326 | var hidCb = D.getElementById('sett-hidden'); |
| 327 | if (hidCb) { |
| 328 | hidCb.onchange = function () { |
| 329 | Z.settings.showHiddenFiles = this.checked; |
| 330 | Z.saveSettings(); |
| 331 | Z.toast(this.checked ? 'Hidden files visible' : 'Hidden files hidden', 'ok'); |
| 332 | }; |
| 333 | } |
| 334 | |
| 335 | /* Fan Speed */ |
| 336 | var fanSlider = D.getElementById('sett-fan-speed'); |
| 337 | if (fanSlider) { |
| 338 | fanSlider.onchange = function () { |
| 339 | var val = parseInt(this.value, 10); |
| 340 | Z.settings.fanThreshold = val; |
| 341 | Z.saveSettings(); |
| 342 | |
| 343 | fetch('/api/admin/fan?threshold=' + val) |
| 344 | .then(function(res) { return res.json(); }) |
| 345 | .then(function(data) { |
| 346 | if (data.status === 'ok') { |
| 347 | Z.toast('Fan threshold updated to ' + val + '°C', 'ok'); |
| 348 | } else { |
| 349 | Z.toast('Fan control failed: ' + (data.message || 'Error'), 'er'); |
| 350 | } |
| 351 | }) |
| 352 | .catch(function(e) { |
| 353 | Z.toast('Fan API is unreachable or blocked', 'er'); |
| 354 | }); |
| 355 | }; |
| 356 | |
| 357 | fanSlider.oninput = function () { |
| 358 | var valEl = D.getElementById('sett-fan-speed-val'); |
| 359 | if (valEl) valEl.textContent = this.value + '°C'; |
| 360 | }; |
| 361 | } |
| 362 | } |
| 363 | |
| 364 | Z.settingsView = settings; |
| 365 | |
| 366 | })(ZFTPD); |
| 367 |