Zero-copy FTP/HTTP Daemon compatible with all POSIX systems
| 1 | /* ══ APP — Router & State Management ══════════════════════════════════════ |
| 2 | * Central application bootstrap. Manages view switching, global state, |
| 3 | * and wires up navigation tabs. |
| 4 | * ES5 compatible for PS5 browser. |
| 5 | * ═════════════════════════════════════════════════════════════════════════ */ |
| 6 | |
| 7 | var ZFTPD = ZFTPD || {}; |
| 8 | |
| 9 | (function (Z) { |
| 10 | 'use strict'; |
| 11 | |
| 12 | var D = document; |
| 13 | var $ = Z.$; |
| 14 | |
| 15 | /* ── Global state ── */ |
| 16 | Z.state = { |
| 17 | view: 'dashboard', /* active view: dashboard | explorer | filemanager | downloads */ |
| 18 | path: '/', /* current directory path (explorer) */ |
| 19 | entries: [], /* current directory entries */ |
| 20 | transferActive: false, /* true while upload/copy is running */ |
| 21 | statsTimer: null, /* interval ID for stats polling */ |
| 22 | fmLeftPath: '/', /* file manager left panel path */ |
| 23 | fmRightPath: '/' /* file manager right panel path */ |
| 24 | }; |
| 25 | |
| 26 | /* ── Themes ── */ |
| 27 | Z.THEMES = [ |
| 28 | { id: 'ps5', name: 'PS5', desc: 'Default blue', sw: '#2b8cff' }, |
| 29 | { id: 'cloud', name: 'Cloud', desc: 'Clean white', sw: '#3b82f6' }, |
| 30 | { id: 'matrix', name: 'Matrix', desc: 'Green terminal', sw: '#00e639' }, |
| 31 | { id: 'sunset', name: 'Sunset', desc: 'Warm orange', sw: '#ff6535' }, |
| 32 | { id: 'arctic', name: 'Arctic', desc: 'Cool cyan', sw: '#00d4ff' }, |
| 33 | { id: 'neon', name: 'Neon', desc: 'Purple glow', sw: '#c800ff' }, |
| 34 | { id: 'amber', name: 'Amber', desc: 'Retro gold', sw: '#ffb700' } |
| 35 | ]; |
| 36 | |
| 37 | /* ── View switching ── */ |
| 38 | Z.switchView = function (viewId) { |
| 39 | var valid = { dashboard: 1, explorer: 1, filemanager: 1, downloads: 1, games: 1, settings: 1 }; |
| 40 | if (!valid[viewId]) return; |
| 41 | |
| 42 | Z.state.view = viewId; |
| 43 | |
| 44 | /* Update nav tabs */ |
| 45 | var tabs = D.querySelectorAll('.nav-tab'); |
| 46 | for (var i = 0; i < tabs.length; i++) { |
| 47 | var t = tabs[i]; |
| 48 | if (t.getAttribute('data-view') === viewId) { |
| 49 | t.classList.add('active'); |
| 50 | } else { |
| 51 | t.classList.remove('active'); |
| 52 | } |
| 53 | } |
| 54 | |
| 55 | /* Update view panels */ |
| 56 | var views = D.querySelectorAll('.view'); |
| 57 | for (var j = 0; j < views.length; j++) { |
| 58 | var v = views[j]; |
| 59 | if (v.id === 'view-' + viewId) { |
| 60 | v.classList.add('view-active'); |
| 61 | } else { |
| 62 | v.classList.remove('view-active'); |
| 63 | } |
| 64 | } |
| 65 | |
| 66 | /* Trigger view-specific init */ |
| 67 | if (viewId === 'explorer' && Z.explorer && Z.explorer.init) { |
| 68 | Z.explorer.init(); |
| 69 | Z.explorer.nav(Z.state.path); |
| 70 | } |
| 71 | if (viewId === 'dashboard' && Z.dashboard && Z.dashboard.refresh) { |
| 72 | Z.dashboard.refresh(); |
| 73 | } |
| 74 | if (viewId === 'filemanager' && Z.fileManager && Z.fileManager.init) { |
| 75 | Z.fileManager.init(); |
| 76 | } |
| 77 | if (viewId === 'downloads' && Z.downloadMgr && Z.downloadMgr.refresh) { |
| 78 | Z.downloadMgr.refresh(); |
| 79 | } |
| 80 | if (viewId === 'games' && Z.gamesView && Z.gamesView.refresh) { |
| 81 | Z.gamesView.refresh(); |
| 82 | } |
| 83 | if (viewId === 'settings' && Z.settingsView && Z.settingsView.refresh) { |
| 84 | Z.settingsView.refresh(); |
| 85 | } |
| 86 | |
| 87 | try { localStorage.setItem('zftpd_view', viewId); } catch (e) { } |
| 88 | }; |
| 89 | |
| 90 | /* ═══════════════════════════════════════════════════════════════════════ |
| 91 | * TRANSFER LOCK MODAL |
| 92 | * Shows a blocking modal during uploads/copies preventing navigation. |
| 93 | * |
| 94 | * ┌──────────────────────────────────┐ |
| 95 | * │ ⬆ UPLOADING… 18 MB/s 17s │ |
| 96 | * │ filename.pkg 44% │ |
| 97 | * │ → /data │ |
| 98 | * │ ████████░░░░░░░░░░░░░░░ │ |
| 99 | * │ [‖ Pause] [✕ Cancel] │ |
| 100 | * └──────────────────────────────────┘ |
| 101 | * ═══════════════════════════════════════════════════════════════════════ */ |
| 102 | |
| 103 | var _xferOverlay = null; |
| 104 | var _xferEls = {}; |
| 105 | |
| 106 | function _ensureXferOverlay() { |
| 107 | if (_xferOverlay) return; |
| 108 | var ov = D.createElement('div'); |
| 109 | ov.className = 'xfer-lock-overlay'; |
| 110 | ov.innerHTML = |
| 111 | '<div class="xfer-lock-card">' + |
| 112 | '<div class="xfer-lock-header">' + |
| 113 | '<span id="xfer-label">UPLOADING\u2026</span>' + |
| 114 | '<span><span id="xfer-speed" class="xfer-lock-speed"></span> ' + |
| 115 | '<span id="xfer-time" class="xfer-lock-time"></span> ' + |
| 116 | '<span id="xfer-pct" class="xfer-lock-pct"></span></span>' + |
| 117 | '</div>' + |
| 118 | '<div class="xfer-lock-body">' + |
| 119 | '<div id="xfer-filename" class="xfer-lock-filename"></div>' + |
| 120 | '<div id="xfer-dest" class="xfer-lock-dest"></div>' + |
| 121 | '<div class="xfer-lock-bar"><div id="xfer-bar" class="xfer-lock-bar-fill"></div></div>' + |
| 122 | '</div>' + |
| 123 | '<div class="xfer-lock-footer">' + |
| 124 | '<button id="xfer-pause" class="btn">‖ Pause</button>' + |
| 125 | '<button id="xfer-cancel" class="btn danger">× Cancel</button>' + |
| 126 | '</div>' + |
| 127 | '</div>'; |
| 128 | D.body.appendChild(ov); |
| 129 | _xferOverlay = ov; |
| 130 | _xferEls = { |
| 131 | label: ov.querySelector('#xfer-label'), |
| 132 | speed: ov.querySelector('#xfer-speed'), |
| 133 | time: ov.querySelector('#xfer-time'), |
| 134 | pct: ov.querySelector('#xfer-pct'), |
| 135 | filename: ov.querySelector('#xfer-filename'), |
| 136 | dest: ov.querySelector('#xfer-dest'), |
| 137 | bar: ov.querySelector('#xfer-bar'), |
| 138 | pauseBtn: ov.querySelector('#xfer-pause'), |
| 139 | cancelBtn: ov.querySelector('#xfer-cancel') |
| 140 | }; |
| 141 | } |
| 142 | |
| 143 | /** |
| 144 | * Show the transfer lock modal |
| 145 | * @param {object} opts - { label, filename, dest, onPause, onCancel } |
| 146 | */ |
| 147 | Z.showTransferLock = function (opts) { |
| 148 | _ensureXferOverlay(); |
| 149 | opts = opts || {}; |
| 150 | Z.state.transferActive = true; |
| 151 | _xferEls.label.textContent = (opts.label || 'UPLOADING') + '\u2026'; |
| 152 | _xferEls.filename.textContent = opts.filename || ''; |
| 153 | _xferEls.dest.textContent = opts.dest || '/'; |
| 154 | _xferEls.speed.textContent = ''; |
| 155 | _xferEls.time.textContent = ''; |
| 156 | _xferEls.pct.textContent = ''; |
| 157 | _xferEls.bar.style.width = '0%'; |
| 158 | _xferEls.pauseBtn.onclick = opts.onPause || function () {}; |
| 159 | _xferEls.cancelBtn.onclick = opts.onCancel || function () {}; |
| 160 | /* Show/hide pause button */ |
| 161 | _xferEls.pauseBtn.style.display = opts.onPause ? '' : 'none'; |
| 162 | _xferOverlay.classList.add('on'); |
| 163 | }; |
| 164 | |
| 165 | /** |
| 166 | * Update the transfer lock modal progress |
| 167 | * @param {object} info - { pct, speed, elapsed } |
| 168 | */ |
| 169 | Z.updateTransferLock = function (info) { |
| 170 | if (!_xferOverlay) return; |
| 171 | info = info || {}; |
| 172 | if (typeof info.pct === 'number') { |
| 173 | _xferEls.pct.textContent = info.pct + '%'; |
| 174 | _xferEls.bar.style.width = info.pct + '%'; |
| 175 | } |
| 176 | if (info.speed) _xferEls.speed.textContent = info.speed; |
| 177 | if (info.elapsed) _xferEls.time.textContent = info.elapsed; |
| 178 | }; |
| 179 | |
| 180 | /** Hide the transfer lock modal */ |
| 181 | Z.hideTransferLock = function () { |
| 182 | Z.state.transferActive = false; |
| 183 | if (_xferOverlay) _xferOverlay.classList.remove('on'); |
| 184 | }; |
| 185 | |
| 186 | /* Legacy compat */ |
| 187 | Z.setTransferActive = function (active) { |
| 188 | Z.state.transferActive = !!active; |
| 189 | if (!active) Z.hideTransferLock(); |
| 190 | }; |
| 191 | |
| 192 | Z.ensureTransferIdle = function () { |
| 193 | if (Z.state.transferActive) { |
| 194 | Z.toast('Transfer in progress\u2026', 'wn'); |
| 195 | return false; |
| 196 | } |
| 197 | return true; |
| 198 | }; |
| 199 | |
| 200 | /* ═══════════════════════════════════════════════════════════════════════ |
| 201 | * NOTIFICATION CENTER |
| 202 | * |
| 203 | * Z.notify(title, desc, type) — type: 'ok' | 'er' | 'wn' | '' |
| 204 | * Notifications appear in the bell dropdown in the topbar. |
| 205 | * ═══════════════════════════════════════════════════════════════════════ */ |
| 206 | |
| 207 | var _notifications = []; |
| 208 | var _notifMaxItems = 50; |
| 209 | |
| 210 | Z.notify = function (title, desc, type) { |
| 211 | _notifications.unshift({ |
| 212 | title: title || '', |
| 213 | desc: desc || '', |
| 214 | type: type || '', |
| 215 | time: new Date() |
| 216 | }); |
| 217 | if (_notifications.length > _notifMaxItems) { |
| 218 | _notifications.length = _notifMaxItems; |
| 219 | } |
| 220 | _renderNotifications(); |
| 221 | /* Also show toast for immediate feedback */ |
| 222 | Z.toast(title, type); |
| 223 | }; |
| 224 | |
| 225 | function _renderNotifications() { |
| 226 | var badge = $('tb-notif-badge'); |
| 227 | var list = $('tb-notif-list'); |
| 228 | if (!list) return; |
| 229 | |
| 230 | /* Update badge count */ |
| 231 | if (badge) { |
| 232 | if (_notifications.length > 0) { |
| 233 | badge.textContent = _notifications.length > 99 ? '99+' : _notifications.length; |
| 234 | badge.classList.remove('hidden'); |
| 235 | } else { |
| 236 | badge.classList.add('hidden'); |
| 237 | } |
| 238 | } |
| 239 | |
| 240 | list.innerHTML = ''; |
| 241 | |
| 242 | if (!_notifications.length) { |
| 243 | list.innerHTML = '<div class="tb-notif-empty">No notifications</div>'; |
| 244 | return; |
| 245 | } |
| 246 | |
| 247 | var clr = D.createElement('div'); |
| 248 | clr.style.cssText = 'padding:8px 16px;text-align:right;font-size:12px;color:var(--ac);cursor:pointer;border-bottom:1px solid var(--bd);line-height:1;margin-bottom:4px;font-weight:600;'; |
| 249 | clr.innerHTML = 'Clear All'; |
| 250 | clr.onclick = function(e) { |
| 251 | e.stopPropagation(); |
| 252 | _notifications = []; |
| 253 | _renderNotifications(); |
| 254 | }; |
| 255 | list.appendChild(clr); |
| 256 | |
| 257 | var icoMap = { |
| 258 | ok: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>', |
| 259 | er: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>', |
| 260 | wn: '<svg width="14" height="14" 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>' |
| 261 | }; |
| 262 | var defaultIco = '<svg width="14" height="14" 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>'; |
| 263 | |
| 264 | for (var i = 0; i < _notifications.length && i < 30; i++) { |
| 265 | var n = _notifications[i]; |
| 266 | var ago = _timeAgo(n.time); |
| 267 | var item = D.createElement('div'); |
| 268 | item.className = 'tb-notif-item'; |
| 269 | item.innerHTML = |
| 270 | '<div class="ni-ico ' + (n.type || '') + '">' + (icoMap[n.type] || defaultIco) + '</div>' + |
| 271 | '<div class="ni-body"><div class="ni-title">' + n.title + '</div>' + |
| 272 | (n.desc ? '<div class="ni-desc">' + n.desc + '</div>' : '') + '</div>' + |
| 273 | '<div class="ni-time">' + ago + '</div>'; |
| 274 | |
| 275 | var cb = D.createElement('span'); |
| 276 | cb.innerHTML = '×'; |
| 277 | cb.style.cssText = 'position:absolute;top:10px;right:10px;cursor:pointer;color:var(--tx3);font-size:16px;line-height:1;width:24px;height:24px;display:flex;align-items:center;justify-content:center;border-radius:12px;'; |
| 278 | cb.onmouseover = function(){this.style.background='var(--tg)';this.style.color='var(--tx)';}; |
| 279 | cb.onmouseout = function(){this.style.background='none';this.style.color='var(--tx3)';}; |
| 280 | (function(idx) { |
| 281 | cb.onclick = function(e){ |
| 282 | e.stopPropagation(); |
| 283 | _notifications.splice(idx, 1); |
| 284 | _renderNotifications(); |
| 285 | }; |
| 286 | })(i); |
| 287 | item.appendChild(cb); |
| 288 | list.appendChild(item); |
| 289 | } |
| 290 | } |
| 291 | |
| 292 | function _timeAgo(date) { |
| 293 | var sec = Math.floor((Date.now() - date.getTime()) / 1000); |
| 294 | if (sec < 60) return 'now'; |
| 295 | if (sec < 3600) return Math.floor(sec / 60) + 'm'; |
| 296 | if (sec < 86400) return Math.floor(sec / 3600) + 'h'; |
| 297 | return Math.floor(sec / 86400) + 'd'; |
| 298 | } |
| 299 | |
| 300 | /* ═══════════════════════════════════════════════════════════════════════ |
| 301 | * DOWNLOAD PROGRESS PILL (topbar) |
| 302 | * |
| 303 | * Polls /api/download/status every 2s when downloads are active. |
| 304 | * Updates the pill count + aggregate progress bar. |
| 305 | * ═══════════════════════════════════════════════════════════════════════ */ |
| 306 | |
| 307 | var _dlPollTimer = null; |
| 308 | |
| 309 | function _startDlPolling() { |
| 310 | if (_dlPollTimer) return; |
| 311 | _dlPollTimer = setInterval(_pollDownloads, 2000); |
| 312 | _pollDownloads(); |
| 313 | } |
| 314 | |
| 315 | function _stopDlPolling() { |
| 316 | if (_dlPollTimer) { clearInterval(_dlPollTimer); _dlPollTimer = null; } |
| 317 | } |
| 318 | |
| 319 | function _pollDownloads() { |
| 320 | Z.api.downloadStatus().then(function (data) { |
| 321 | var items = (data && Array.isArray(data.downloads)) ? data.downloads : []; |
| 322 | var active = items.filter(function (d) { return !d.done && !d.error; }); |
| 323 | var pill = $('tb-dl-pill'); |
| 324 | var countEl = $('tb-dl-count'); |
| 325 | var barFill = $('tb-dl-bar-fill'); |
| 326 | |
| 327 | if (active.length > 0) { |
| 328 | if (pill) pill.classList.remove('hidden'); |
| 329 | if (countEl) countEl.textContent = active.length; |
| 330 | |
| 331 | /* Calculate aggregate progress */ |
| 332 | var totalDown = 0, totalSize = 0; |
| 333 | for (var i = 0; i < active.length; i++) { |
| 334 | totalDown += (active[i].downloaded || 0); |
| 335 | totalSize += (active[i].total_size || 0); |
| 336 | } |
| 337 | var pct = totalSize > 0 ? Math.floor(totalDown / totalSize * 100) : 0; |
| 338 | if (barFill) barFill.style.width = pct + '%'; |
| 339 | } else { |
| 340 | if (pill) pill.classList.add('hidden'); |
| 341 | _stopDlPolling(); |
| 342 | } |
| 343 | |
| 344 | /* Notify on completed/errored downloads */ |
| 345 | for (var j = 0; j < items.length; j++) { |
| 346 | var d = items[j]; |
| 347 | var nKey = 'dl_notified_' + (d.id || j); |
| 348 | if (d.done && !d.error && !Z.state[nKey]) { |
| 349 | Z.state[nKey] = true; |
| 350 | Z.notify('Download complete', d.filename || d.url, 'ok'); |
| 351 | } |
| 352 | if (d.error && !Z.state[nKey]) { |
| 353 | Z.state[nKey] = true; |
| 354 | Z.notify('Download failed', (d.filename || d.url) + ': ' + d.error_msg, 'er'); |
| 355 | } |
| 356 | } |
| 357 | }).catch(function () { }); |
| 358 | } |
| 359 | |
| 360 | /* Hook: call this from download-mgr.js when a download starts */ |
| 361 | Z.onDownloadStarted = function () { _startDlPolling(); }; |
| 362 | /* Click pill → go to downloads view */ |
| 363 | D.addEventListener('DOMContentLoaded', function () { |
| 364 | var pill = $('tb-dl-pill'); |
| 365 | if (pill) pill.onclick = function () { Z.switchView('downloads'); }; |
| 366 | }); |
| 367 | |
| 368 | /* ── Theme management ── */ |
| 369 | Z.setTheme = function (id) { |
| 370 | var ids = Z.THEMES.map(function (t) { return t.id; }); |
| 371 | var theme = ids.indexOf(id) >= 0 ? id : 'ps5'; |
| 372 | D.documentElement.setAttribute('data-theme', theme); |
| 373 | |
| 374 | /* Update mobile select */ |
| 375 | var sel = $('theme-select'); |
| 376 | if (sel) sel.value = theme; |
| 377 | |
| 378 | /* Update desktop button swatch */ |
| 379 | var t = Z.THEMES.filter(function (x) { return x.id === theme; })[0] || Z.THEMES[0]; |
| 380 | var sw = $('t-sw'); |
| 381 | if (sw) sw.style.background = t.sw; |
| 382 | var tn = $('t-nm'); |
| 383 | if (tn) tn.textContent = t.name; |
| 384 | |
| 385 | try { localStorage.setItem('zftpd_theme', theme); } catch (e) { } |
| 386 | }; |
| 387 | |
| 388 | /* ── Stats polling ── */ |
| 389 | function refreshStats() { |
| 390 | Z.api.stats(Z.state.path).then(function (d) { |
| 391 | if (Z.dashboard && Z.dashboard.updateStats) Z.dashboard.updateStats(d); |
| 392 | }).catch(function () { }); |
| 393 | } |
| 394 | |
| 395 | /* ── Bootstrap ── */ |
| 396 | D.addEventListener('DOMContentLoaded', function () { |
| 397 | |
| 398 | /* Restore saved preferences */ |
| 399 | try { |
| 400 | var sv = localStorage.getItem('zftpd_view'); |
| 401 | if (sv) Z.state.view = sv; |
| 402 | var st = localStorage.getItem('zftpd_theme'); |
| 403 | if (st) Z.setTheme(st); |
| 404 | } catch (e) { } |
| 405 | |
| 406 | /* Brand logo */ |
| 407 | var bl = D.querySelector('.brand-logo'); |
| 408 | if (bl) bl.src = 'assets/zftpd-logo.png'; |
| 409 | |
| 410 | /* Nav tab clicks */ |
| 411 | var tabs = D.querySelectorAll('.nav-tab'); |
| 412 | for (var i = 0; i < tabs.length; i++) { |
| 413 | (function (tab) { |
| 414 | tab.onclick = function () { |
| 415 | var view = tab.getAttribute('data-view'); |
| 416 | if (view) Z.switchView(view); |
| 417 | }; |
| 418 | })(tabs[i]); |
| 419 | } |
| 420 | |
| 421 | /* Theme selector (mobile) */ |
| 422 | var themeSel = $('theme-select'); |
| 423 | if (themeSel) { |
| 424 | themeSel.innerHTML = ''; |
| 425 | Z.THEMES.forEach(function (t) { |
| 426 | var opt = D.createElement('option'); |
| 427 | opt.value = t.id; |
| 428 | opt.textContent = t.name; |
| 429 | themeSel.appendChild(opt); |
| 430 | }); |
| 431 | var cur = D.documentElement.getAttribute('data-theme') || 'ps5'; |
| 432 | themeSel.value = cur; |
| 433 | themeSel.onchange = function () { Z.setTheme(this.value); }; |
| 434 | } |
| 435 | |
| 436 | /* Theme button (desktop) */ |
| 437 | var themeBtn = $('theme-btn'); |
| 438 | var themeDd = $('theme-dd'); |
| 439 | if (themeBtn && themeDd) { |
| 440 | themeBtn.onclick = function (e) { |
| 441 | e.stopPropagation(); |
| 442 | themeDd.classList.toggle('show'); |
| 443 | themeBtn.classList.toggle('open', themeDd.classList.contains('show')); |
| 444 | /* Build dropdown items */ |
| 445 | themeDd.innerHTML = '<div class="td-hd">Select Theme</div>'; |
| 446 | var curTheme = D.documentElement.getAttribute('data-theme') || 'ps5'; |
| 447 | Z.THEMES.forEach(function (t) { |
| 448 | var el = D.createElement('div'); |
| 449 | el.className = 'td-item' + (t.id === curTheme ? ' active' : ''); |
| 450 | el.innerHTML = '<div class="td-sw" style="background:' + t.sw + '"></div>' + |
| 451 | '<div class="td-info"><div class="td-nm">' + t.name + '</div><div class="td-ds">' + t.desc + '</div></div>' + |
| 452 | '<span class="td-ck">' + Z.ICO.check + '</span>'; |
| 453 | el.onclick = function () { |
| 454 | Z.setTheme(t.id); |
| 455 | themeDd.classList.remove('show'); |
| 456 | themeBtn.classList.remove('open'); |
| 457 | }; |
| 458 | themeDd.appendChild(el); |
| 459 | }); |
| 460 | }; |
| 461 | } |
| 462 | |
| 463 | /* Notification bell toggle */ |
| 464 | var notifBtn = $('tb-notif-btn'); |
| 465 | var notifDd = $('tb-notif-dd'); |
| 466 | if (notifBtn && notifDd) { |
| 467 | notifBtn.onclick = function (e) { |
| 468 | e.stopPropagation(); |
| 469 | notifDd.classList.toggle('show'); |
| 470 | /* Close theme dropdown */ |
| 471 | if (themeDd) themeDd.classList.remove('show'); |
| 472 | if (themeBtn) themeBtn.classList.remove('open'); |
| 473 | /* Re-render to update relative times */ |
| 474 | if (notifDd.classList.contains('show')) _renderNotifications(); |
| 475 | }; |
| 476 | } |
| 477 | |
| 478 | /* Close all dropdowns on outside click */ |
| 479 | D.addEventListener('click', function () { |
| 480 | if (themeDd) themeDd.classList.remove('show'); |
| 481 | if (themeBtn) themeBtn.classList.remove('open'); |
| 482 | if (notifDd) notifDd.classList.remove('show'); |
| 483 | }); |
| 484 | |
| 485 | /* Escape key */ |
| 486 | D.addEventListener('keydown', function (e) { |
| 487 | if (e.key === 'Escape') { |
| 488 | if (themeDd) themeDd.classList.remove('show'); |
| 489 | if (themeBtn) themeBtn.classList.remove('open'); |
| 490 | if (notifDd) notifDd.classList.remove('show'); |
| 491 | } |
| 492 | }); |
| 493 | |
| 494 | /* Drag-and-drop upload */ |
| 495 | var _dd = 0; |
| 496 | D.addEventListener('dragenter', function (e) { |
| 497 | e.preventDefault(); |
| 498 | _dd++; |
| 499 | var drop = $('drop-overlay'); |
| 500 | if (drop) drop.classList.add('on'); |
| 501 | }); |
| 502 | D.addEventListener('dragover', function (e) { e.preventDefault(); }); |
| 503 | D.addEventListener('dragleave', function (e) { |
| 504 | e.preventDefault(); |
| 505 | _dd = Math.max(0, _dd - 1); |
| 506 | if (!_dd) { |
| 507 | var drop = $('drop-overlay'); |
| 508 | if (drop) drop.classList.remove('on'); |
| 509 | } |
| 510 | }); |
| 511 | D.addEventListener('drop', function (e) { |
| 512 | e.preventDefault(); |
| 513 | _dd = 0; |
| 514 | var drop = $('drop-overlay'); |
| 515 | if (drop) drop.classList.remove('on'); |
| 516 | if (Z.explorer && Z.explorer.upload) { |
| 517 | Z.explorer.upload(e.dataTransfer.files); |
| 518 | } |
| 519 | }); |
| 520 | |
| 521 | var dc = $('drop-close'); |
| 522 | if (dc) dc.onclick = function () { |
| 523 | var drop = $('drop-overlay'); |
| 524 | if (drop) drop.classList.remove('on'); |
| 525 | }; |
| 526 | |
| 527 | /* Stats polling */ |
| 528 | Z.state.statsTimer = setInterval(refreshStats, 15000); |
| 529 | |
| 530 | /* Initialize the active view */ |
| 531 | Z.switchView(Z.state.view); |
| 532 | |
| 533 | /* Preload root directory */ |
| 534 | Z.api.list('/').catch(function () { }); |
| 535 | }); |
| 536 | |
| 537 | })(ZFTPD); |
| 538 |