OPEN-ISP v0.5 (pickle-lime) — rilis open-source perdana
Aplikasi billing & manajemen ISP (fiber optic) — PWA pelanggan, portal teknisi & kolektor, admin panel lengkap, multi-vendor OLT, RADIUS, WhatsApp/Telegram, pembayaran QRIS. Kredit & lisensi (ISC): - Base kode & repo: alijayanet/billing-rtrw oleh Alijaya Net — terima kasih banyak atas base kode dan repo yang bermanfaat. - Add-on: GoWA, LibreSpeed/OpenSpeedTest, Uptime Kuma, notification-forwarder. - Dikembangkan menjadi OPEN-ISP oleh oxpoe — https://github.com/oxpoe/open-isp
This commit is contained in:
commit
eaf05f2811
280 files changed
+123975
No files matched your search
@@ -0,0 +1,104 @@
|
||||
/* admin-theme-sync.js — Menjaga konsistensi tema admin (dark/light).
|
||||
PENTING: observer TIDAK boleh menulis ulang `data-theme` (bisa memicu loop). */
|
||||
(function () {
|
||||
var KEY = 'app-theme';
|
||||
function read() { try { return localStorage.getItem(KEY); } catch (e) { return null; } }
|
||||
|
||||
// Sinkronkan atribut turunan berdasarkan data-theme saat ini (tanpa menulis data-theme).
|
||||
function syncAttrs() {
|
||||
var isLight = document.documentElement.getAttribute('data-theme') === 'light';
|
||||
var bs = isLight ? 'light' : 'dark';
|
||||
if (document.documentElement.getAttribute('data-bs-theme') !== bs) document.documentElement.setAttribute('data-bs-theme', bs);
|
||||
if (document.documentElement.classList.contains('light-theme') !== isLight) document.documentElement.classList.toggle('light-theme', isLight);
|
||||
if (document.body && document.body.classList.contains('light-theme') !== isLight) document.body.classList.toggle('light-theme', isLight);
|
||||
try { if (document.documentElement.style.colorScheme !== bs) document.documentElement.style.colorScheme = bs; } catch (e) {}
|
||||
}
|
||||
|
||||
function applyTheme(t) {
|
||||
t = t || read() || 'dark';
|
||||
if (document.documentElement.getAttribute('data-theme') !== t) document.documentElement.setAttribute('data-theme', t);
|
||||
syncAttrs();
|
||||
return t;
|
||||
}
|
||||
|
||||
window.__applyAdminTheme = applyTheme;
|
||||
applyTheme();
|
||||
|
||||
// Observer hanya menyinkronkan atribut turunan (AMAN, tanpa loop).
|
||||
try { new MutationObserver(syncAttrs).observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme', 'class'] }); } catch (e) {}
|
||||
|
||||
window.addEventListener('DOMContentLoaded', function () { applyTheme(); });
|
||||
window.addEventListener('pageshow', function () { applyTheme(); });
|
||||
|
||||
if (typeof window.toggleAppTheme !== 'function') {
|
||||
window.toggleAppTheme = function () {
|
||||
var cur = document.documentElement.getAttribute('data-theme') === 'light' ? 'dark' : 'light';
|
||||
try { localStorage.setItem(KEY, cur); } catch (e) {}
|
||||
applyTheme(cur);
|
||||
if (typeof window.updateThemeToggleIcons === 'function') window.updateThemeToggleIcons(cur);
|
||||
};
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (e) {
|
||||
var btn = e.target && e.target.closest ? e.target.closest('.theme-toggle-btn') : null;
|
||||
if (!btn) return;
|
||||
if (btn.dataset.themeDelegate === '1') return;
|
||||
btn.dataset.themeDelegate = '1';
|
||||
if (!btn.hasAttribute('onclick')) { e.preventDefault(); window.toggleAppTheme(); }
|
||||
}, true);
|
||||
})();
|
||||
|
||||
// ── Dropdown Aksi (kebab menu) ──
|
||||
// Menu di-"portal" ke <body> saat terbuka, lalu diposisikan fixed relatif tombol.
|
||||
// Ini menghilangkan SEMUA pengaruh ancestor (overflow tabel/kartu, stacking, dsb)
|
||||
// sehingga menu selalu solid & tampil utuh di mana pun (termasuk baris Lunas).
|
||||
var _actOpen = null, _actMenu = null, _actHome = null;
|
||||
function actCloseAll() {
|
||||
if (_actMenu) {
|
||||
try { _actMenu.classList.remove('fixed', 'up'); _actMenu.style.left = ''; _actMenu.style.top = ''; } catch (e) {}
|
||||
try {
|
||||
if (_actHome && _actHome.parent) _actHome.parent.insertBefore(_actMenu, _actHome.next);
|
||||
else document.body.appendChild(_actMenu);
|
||||
} catch (e) {}
|
||||
}
|
||||
document.querySelectorAll('.act.open').forEach(function (x) { x.classList.remove('open'); });
|
||||
_actOpen = null; _actMenu = null; _actHome = null;
|
||||
}
|
||||
function actPlace(btn, menu) {
|
||||
menu.style.left = '0px'; menu.style.top = '0px';
|
||||
var w = menu.offsetWidth, h = menu.offsetHeight;
|
||||
var br = btn.getBoundingClientRect();
|
||||
var vw = window.innerWidth, vh = window.innerHeight, pad = 8, gap = 6;
|
||||
var left = br.right - w; // rata kanan ke tombol
|
||||
if (left + w > vw - pad) left = vw - pad - w;
|
||||
if (left < pad) left = pad;
|
||||
var top = br.bottom + gap; // default: di bawah
|
||||
if (top + h > vh - pad) top = br.top - gap - h; // tak cukup -> ke atas
|
||||
if (top < pad) top = pad;
|
||||
menu.style.left = Math.round(left) + 'px';
|
||||
menu.style.top = Math.round(top) + 'px';
|
||||
}
|
||||
window.actToggle = function (e, btn) {
|
||||
try { if (e) { e.preventDefault(); e.stopPropagation(); } } catch (_) {}
|
||||
var a = btn && btn.closest ? btn.closest('.act') : null; if (!a) return;
|
||||
var wasOpen = a.classList.contains('open');
|
||||
actCloseAll();
|
||||
if (!wasOpen) {
|
||||
a.classList.add('open');
|
||||
var menu = a.querySelector('.act-menu');
|
||||
if (!menu) return;
|
||||
_actOpen = a; _actMenu = menu;
|
||||
_actHome = { parent: menu.parentNode, next: menu.nextSibling };
|
||||
document.body.appendChild(menu); // pindah ke body
|
||||
menu.classList.add('fixed');
|
||||
actPlace(btn, menu);
|
||||
}
|
||||
};
|
||||
// klik di luar menu -> tutup; klik di dalam menu -> biarkan aksi jalan dulu, tutup setelahnya
|
||||
document.addEventListener('click', function (ev) {
|
||||
if (_actMenu && ev.target && _actMenu.contains(ev.target)) { setTimeout(actCloseAll, 0); return; }
|
||||
actCloseAll();
|
||||
});
|
||||
document.addEventListener('keydown', function (ev) { if (ev.key === 'Escape') actCloseAll(); });
|
||||
window.addEventListener('scroll', actCloseAll, true);
|
||||
window.addEventListener('resize', actCloseAll);
|
||||
Reference in new issue
Block a user