mirror of
https://github.com/dyzulk/twinpath-hotspot-themes.git
synced 2026-01-26 05:25:40 +07:00
Initial commit: Premium Gold Theme with Dynamic QR and Multi-language support
This commit is contained in:
64
js/config.js
Normal file
64
js/config.js
Normal file
@@ -0,0 +1,64 @@
|
||||
const brandConfig = {
|
||||
brandName: "TwinpathNet",
|
||||
portalUrl: "http://welcome.dyzulk.com/login",
|
||||
creditName: "dyzulk.com",
|
||||
creditUrl: "https://dyzulk.com",
|
||||
assets: {
|
||||
logo: "img/logo-twinpath.svg",
|
||||
icon_ticket: "svg/ticket.svg",
|
||||
icon_user: "svg/user.svg",
|
||||
icon_lock: "svg/lock.svg",
|
||||
icon_scan: "svg/scan-line.svg",
|
||||
icon_image: "svg/image.svg",
|
||||
icon_external: "svg/external-link.svg",
|
||||
icon_copy: "svg/copy.svg",
|
||||
icon_logout: "svg/log-out.svg",
|
||||
icon_success: "svg/check-circle.svg",
|
||||
icon_error: "svg/alert-circle.svg",
|
||||
icon_clock: "svg/clock.svg",
|
||||
icon_upload: "svg/upload-cloud.svg",
|
||||
icon_download: "svg/download-cloud.svg",
|
||||
icon_wifi: "svg/wifi.svg"
|
||||
}
|
||||
};
|
||||
|
||||
function applyBranding() {
|
||||
// Update Document Title
|
||||
const currentTitle = document.title;
|
||||
if (currentTitle.includes('TwinpathNet')) {
|
||||
document.title = currentTitle.replace('TwinpathNet', brandConfig.brandName);
|
||||
} else if (!currentTitle.includes(brandConfig.brandName)) {
|
||||
document.title = `${brandConfig.brandName} > ${currentTitle}`;
|
||||
}
|
||||
|
||||
// Update Elements with data-brand-name
|
||||
document.querySelectorAll('[data-brand-name]').forEach(el => {
|
||||
el.innerText = brandConfig.brandName;
|
||||
if (el.tagName === 'IMG') el.alt = brandConfig.brandName;
|
||||
});
|
||||
|
||||
// Update Elements with data-brand-credit
|
||||
document.querySelectorAll('[data-brand-credit]').forEach(el => {
|
||||
el.innerText = brandConfig.creditName;
|
||||
});
|
||||
|
||||
// Update Links with data-brand-link
|
||||
document.querySelectorAll('[data-brand-link="credit"]').forEach(el => {
|
||||
el.href = brandConfig.creditUrl;
|
||||
});
|
||||
|
||||
// Update Assets (Images/Icons)
|
||||
document.querySelectorAll('[data-asset]').forEach(el => {
|
||||
const assetKey = el.getAttribute('data-asset');
|
||||
if (brandConfig.assets[assetKey]) {
|
||||
el.src = brandConfig.assets[assetKey];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Apply branding as soon as possible
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', applyBranding);
|
||||
} else {
|
||||
applyBranding();
|
||||
}
|
||||
1
js/html5-qrcode.min.js
vendored
Normal file
1
js/html5-qrcode.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
158
js/languages.js
Normal file
158
js/languages.js
Normal file
@@ -0,0 +1,158 @@
|
||||
const translations = {
|
||||
id: {
|
||||
lang_name: "Bahasa Indonesia",
|
||||
operational: "Beroperasi",
|
||||
online: "Terhubung",
|
||||
tab_voucher: "Voucher",
|
||||
tab_member: "Member",
|
||||
voucher_label: "Kode Voucher",
|
||||
voucher_placeholder: "Masukkan kode voucher...",
|
||||
user_label: "Nama Pengguna",
|
||||
pass_label: "Kata Sandi",
|
||||
login_voucher: "Gunakan Voucher",
|
||||
login_member: "Masuk Member",
|
||||
connect_btn: "Hubungkan",
|
||||
scan_btn: "Scan QR Code",
|
||||
trial_btn: "Coba Gratis",
|
||||
status_dashboard_msg: "Kelola sesi aktif dan pantau penggunaan anda.",
|
||||
connection_stats: "Statistik Koneksi",
|
||||
quota_left: "Sisa Kuota",
|
||||
unlimited: "Tanpa Batas",
|
||||
or_text: "Atau",
|
||||
scan_file_btn: "Scan dari Galeri",
|
||||
point_camera: "Arahkan kamera ke QR Code",
|
||||
confirm_title: "Konfirmasi Voucher",
|
||||
confirm_msg: "Apakah Anda ingin masuk dengan kode ini?",
|
||||
open_browser: "Buka di Browser",
|
||||
copy_url: "Salin Link Portal",
|
||||
manual_tip: "Kamera/Galeri diblokir sistem? Silakan buka di Chrome/Safari atau salin link portal di bawah.",
|
||||
hour: "JAM",
|
||||
hours: "JAM",
|
||||
day: "HARI",
|
||||
week: "MINGGU",
|
||||
pricing_title: "Paket Tersedia",
|
||||
session_info: "Informasi Sesi",
|
||||
ip_address: "Alamat IP",
|
||||
upload: "Unggah",
|
||||
download: "Unduh",
|
||||
time_left: "Sisa Waktu",
|
||||
logout_btn: "Keluar (Log Out)",
|
||||
logged_out: "Sudah Keluar",
|
||||
logged_out_msg: "Anda telah berhasil memutuskan koneksi dari jaringan.",
|
||||
login_again: "Masuk Kembali",
|
||||
failed_title: "Koneksi Gagal",
|
||||
try_again: "Coba Lagi",
|
||||
success_title: "Berhasil",
|
||||
success_msg: "Anda sekarang terhubung ke jaringan.",
|
||||
redirect_msg: "Mengalihkan...",
|
||||
redirect_tip: "Klik di sini jika tidak beralih otomatis",
|
||||
adv_title: "Iklan",
|
||||
adv_msg: "Jika tidak terjadi apa-apa, buka iklan secara manual.",
|
||||
adv_link: "iklan",
|
||||
adv_manually: "secara manual",
|
||||
powered_by: "Didukung oleh"
|
||||
},
|
||||
en: {
|
||||
lang_name: "English",
|
||||
operational: "Operational",
|
||||
online: "Online",
|
||||
tab_voucher: "Voucher",
|
||||
tab_member: "Member",
|
||||
voucher_label: "Voucher Code",
|
||||
voucher_placeholder: "Enter code received...",
|
||||
user_label: "Username",
|
||||
pass_label: "Password",
|
||||
login_voucher: "Use Voucher",
|
||||
login_member: "Member Login",
|
||||
connect_btn: "Connect",
|
||||
scan_btn: "Scan QR Code",
|
||||
trial_btn: "Free Trial Access",
|
||||
status_dashboard_msg: "Manage your active session and monitor your usage.",
|
||||
connection_stats: "Connection Stats",
|
||||
quota_left: "Quota Remaining",
|
||||
unlimited: "Unlimited",
|
||||
or_text: "Or",
|
||||
scan_file_btn: "Scan from Gallery",
|
||||
point_camera: "Point camera at QR Code",
|
||||
confirm_title: "Confirm Voucher",
|
||||
confirm_msg: "Do you want to log in with this code?",
|
||||
open_browser: "Open in Browser",
|
||||
copy_url: "Copy Portal Link",
|
||||
manual_tip: "Camera/Gallery restricted? Please open in Chrome/Safari or copy the portal link below.",
|
||||
hour: "HOUR",
|
||||
hours: "HOURS",
|
||||
day: "DAY",
|
||||
week: "WEEK",
|
||||
pricing_title: "Available Packages",
|
||||
session_info: "Session Information",
|
||||
ip_address: "IP Address",
|
||||
upload: "Upload",
|
||||
download: "Download",
|
||||
time_left: "Time Remaining",
|
||||
logout_btn: "Log Out",
|
||||
logged_out: "Logged Out",
|
||||
logged_out_msg: "You have successfully disconnected from the network.",
|
||||
login_again: "Log In Again",
|
||||
failed_title: "Connection Failed",
|
||||
try_again: "Try Again",
|
||||
success_title: "Success",
|
||||
success_msg: "You are now connected to the network.",
|
||||
redirect_msg: "Redirecting...",
|
||||
redirect_tip: "Click here if not redirected automatically",
|
||||
adv_title: "Advertisement",
|
||||
adv_msg: "If nothing happens, open advertisement manually.",
|
||||
adv_link: "advertisement",
|
||||
adv_manually: "manually",
|
||||
powered_by: "Powered by"
|
||||
}
|
||||
};
|
||||
|
||||
function applyLanguage(lang) {
|
||||
document.querySelectorAll('[data-i18n]').forEach(el => {
|
||||
const key = el.getAttribute('data-i18n');
|
||||
if (translations[lang] && translations[lang][key]) {
|
||||
if (el.tagName === 'INPUT' && el.placeholder) {
|
||||
el.placeholder = translations[lang][key];
|
||||
} else {
|
||||
el.innerText = translations[lang][key];
|
||||
}
|
||||
}
|
||||
});
|
||||
localStorage.setItem('twinpath_lang', lang);
|
||||
document.documentElement.lang = lang;
|
||||
|
||||
// Update active state in custom menu
|
||||
document.querySelectorAll('.lang-option').forEach(opt => {
|
||||
opt.classList.toggle('active', opt.getAttribute('data-lang') === lang);
|
||||
});
|
||||
|
||||
// Update button text
|
||||
const langLabel = document.getElementById('lang-label');
|
||||
if (langLabel) langLabel.innerText = lang.toUpperCase();
|
||||
|
||||
// Refresh dynamic content if on dashboard
|
||||
if (typeof initDashboard === 'function') {
|
||||
initDashboard();
|
||||
}
|
||||
}
|
||||
|
||||
function toggleLangMenu() {
|
||||
const menu = document.getElementById('lang-menu');
|
||||
if (menu) menu.classList.toggle('show');
|
||||
}
|
||||
|
||||
// Close menu when clicking outside
|
||||
window.addEventListener('click', (e) => {
|
||||
const dropdown = document.querySelector('.lang-dropdown');
|
||||
const menu = document.getElementById('lang-menu');
|
||||
if (menu && dropdown && !dropdown.contains(e.target)) {
|
||||
menu.classList.remove('show');
|
||||
}
|
||||
});
|
||||
|
||||
function initLanguage() {
|
||||
const savedLang = localStorage.getItem('twinpath_lang') || 'en';
|
||||
applyLanguage(savedLang);
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', initLanguage);
|
||||
217
js/md5.js
Normal file
217
js/md5.js
Normal file
@@ -0,0 +1,217 @@
|
||||
/*
|
||||
* A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
|
||||
* Digest Algorithm, as defined in RFC 1321.
|
||||
* Version 1.1 Copyright (C) Paul Johnston 1999 - 2002.
|
||||
* Code also contributed by Greg Holt
|
||||
* See http://pajhome.org.uk/site/legal.html for details.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Add integers, wrapping at 2^32. This uses 16-bit operations internally
|
||||
* to work around bugs in some JS interpreters.
|
||||
*/
|
||||
function safe_add(x, y)
|
||||
{
|
||||
var lsw = (x & 0xFFFF) + (y & 0xFFFF)
|
||||
var msw = (x >> 16) + (y >> 16) + (lsw >> 16)
|
||||
return (msw << 16) | (lsw & 0xFFFF)
|
||||
}
|
||||
|
||||
/*
|
||||
* Bitwise rotate a 32-bit number to the left.
|
||||
*/
|
||||
function rol(num, cnt)
|
||||
{
|
||||
return (num << cnt) | (num >>> (32 - cnt))
|
||||
}
|
||||
|
||||
/*
|
||||
* These functions implement the four basic operations the algorithm uses.
|
||||
*/
|
||||
function cmn(q, a, b, x, s, t)
|
||||
{
|
||||
return safe_add(rol(safe_add(safe_add(a, q), safe_add(x, t)), s), b)
|
||||
}
|
||||
function ff(a, b, c, d, x, s, t)
|
||||
{
|
||||
return cmn((b & c) | ((~b) & d), a, b, x, s, t)
|
||||
}
|
||||
function gg(a, b, c, d, x, s, t)
|
||||
{
|
||||
return cmn((b & d) | (c & (~d)), a, b, x, s, t)
|
||||
}
|
||||
function hh(a, b, c, d, x, s, t)
|
||||
{
|
||||
return cmn(b ^ c ^ d, a, b, x, s, t)
|
||||
}
|
||||
function ii(a, b, c, d, x, s, t)
|
||||
{
|
||||
return cmn(c ^ (b | (~d)), a, b, x, s, t)
|
||||
}
|
||||
|
||||
/*
|
||||
* Calculate the MD5 of an array of little-endian words, producing an array
|
||||
* of little-endian words.
|
||||
*/
|
||||
function coreMD5(x)
|
||||
{
|
||||
var a = 1732584193
|
||||
var b = -271733879
|
||||
var c = -1732584194
|
||||
var d = 271733878
|
||||
|
||||
for(i = 0; i < x.length; i += 16)
|
||||
{
|
||||
var olda = a
|
||||
var oldb = b
|
||||
var oldc = c
|
||||
var oldd = d
|
||||
|
||||
a = ff(a, b, c, d, x[i+ 0], 7 , -680876936)
|
||||
d = ff(d, a, b, c, x[i+ 1], 12, -389564586)
|
||||
c = ff(c, d, a, b, x[i+ 2], 17, 606105819)
|
||||
b = ff(b, c, d, a, x[i+ 3], 22, -1044525330)
|
||||
a = ff(a, b, c, d, x[i+ 4], 7 , -176418897)
|
||||
d = ff(d, a, b, c, x[i+ 5], 12, 1200080426)
|
||||
c = ff(c, d, a, b, x[i+ 6], 17, -1473231341)
|
||||
b = ff(b, c, d, a, x[i+ 7], 22, -45705983)
|
||||
a = ff(a, b, c, d, x[i+ 8], 7 , 1770035416)
|
||||
d = ff(d, a, b, c, x[i+ 9], 12, -1958414417)
|
||||
c = ff(c, d, a, b, x[i+10], 17, -42063)
|
||||
b = ff(b, c, d, a, x[i+11], 22, -1990404162)
|
||||
a = ff(a, b, c, d, x[i+12], 7 , 1804603682)
|
||||
d = ff(d, a, b, c, x[i+13], 12, -40341101)
|
||||
c = ff(c, d, a, b, x[i+14], 17, -1502002290)
|
||||
b = ff(b, c, d, a, x[i+15], 22, 1236535329)
|
||||
|
||||
a = gg(a, b, c, d, x[i+ 1], 5 , -165796510)
|
||||
d = gg(d, a, b, c, x[i+ 6], 9 , -1069501632)
|
||||
c = gg(c, d, a, b, x[i+11], 14, 643717713)
|
||||
b = gg(b, c, d, a, x[i+ 0], 20, -373897302)
|
||||
a = gg(a, b, c, d, x[i+ 5], 5 , -701558691)
|
||||
d = gg(d, a, b, c, x[i+10], 9 , 38016083)
|
||||
c = gg(c, d, a, b, x[i+15], 14, -660478335)
|
||||
b = gg(b, c, d, a, x[i+ 4], 20, -405537848)
|
||||
a = gg(a, b, c, d, x[i+ 9], 5 , 568446438)
|
||||
d = gg(d, a, b, c, x[i+14], 9 , -1019803690)
|
||||
c = gg(c, d, a, b, x[i+ 3], 14, -187363961)
|
||||
b = gg(b, c, d, a, x[i+ 8], 20, 1163531501)
|
||||
a = gg(a, b, c, d, x[i+13], 5 , -1444681467)
|
||||
d = gg(d, a, b, c, x[i+ 2], 9 , -51403784)
|
||||
c = gg(c, d, a, b, x[i+ 7], 14, 1735328473)
|
||||
b = gg(b, c, d, a, x[i+12], 20, -1926607734)
|
||||
|
||||
a = hh(a, b, c, d, x[i+ 5], 4 , -378558)
|
||||
d = hh(d, a, b, c, x[i+ 8], 11, -2022574463)
|
||||
c = hh(c, d, a, b, x[i+11], 16, 1839030562)
|
||||
b = hh(b, c, d, a, x[i+14], 23, -35309556)
|
||||
a = hh(a, b, c, d, x[i+ 1], 4 , -1530992060)
|
||||
d = hh(d, a, b, c, x[i+ 4], 11, 1272893353)
|
||||
c = hh(c, d, a, b, x[i+ 7], 16, -155497632)
|
||||
b = hh(b, c, d, a, x[i+10], 23, -1094730640)
|
||||
a = hh(a, b, c, d, x[i+13], 4 , 681279174)
|
||||
d = hh(d, a, b, c, x[i+ 0], 11, -358537222)
|
||||
c = hh(c, d, a, b, x[i+ 3], 16, -722521979)
|
||||
b = hh(b, c, d, a, x[i+ 6], 23, 76029189)
|
||||
a = hh(a, b, c, d, x[i+ 9], 4 , -640364487)
|
||||
d = hh(d, a, b, c, x[i+12], 11, -421815835)
|
||||
c = hh(c, d, a, b, x[i+15], 16, 530742520)
|
||||
b = hh(b, c, d, a, x[i+ 2], 23, -995338651)
|
||||
|
||||
a = ii(a, b, c, d, x[i+ 0], 6 , -198630844)
|
||||
d = ii(d, a, b, c, x[i+ 7], 10, 1126891415)
|
||||
c = ii(c, d, a, b, x[i+14], 15, -1416354905)
|
||||
b = ii(b, c, d, a, x[i+ 5], 21, -57434055)
|
||||
a = ii(a, b, c, d, x[i+12], 6 , 1700485571)
|
||||
d = ii(d, a, b, c, x[i+ 3], 10, -1894986606)
|
||||
c = ii(c, d, a, b, x[i+10], 15, -1051523)
|
||||
b = ii(b, c, d, a, x[i+ 1], 21, -2054922799)
|
||||
a = ii(a, b, c, d, x[i+ 8], 6 , 1873313359)
|
||||
d = ii(d, a, b, c, x[i+15], 10, -30611744)
|
||||
c = ii(c, d, a, b, x[i+ 6], 15, -1560198380)
|
||||
b = ii(b, c, d, a, x[i+13], 21, 1309151649)
|
||||
a = ii(a, b, c, d, x[i+ 4], 6 , -145523070)
|
||||
d = ii(d, a, b, c, x[i+11], 10, -1120210379)
|
||||
c = ii(c, d, a, b, x[i+ 2], 15, 718787259)
|
||||
b = ii(b, c, d, a, x[i+ 9], 21, -343485551)
|
||||
|
||||
a = safe_add(a, olda)
|
||||
b = safe_add(b, oldb)
|
||||
c = safe_add(c, oldc)
|
||||
d = safe_add(d, oldd)
|
||||
}
|
||||
return [a, b, c, d]
|
||||
}
|
||||
|
||||
/*
|
||||
* Convert an array of little-endian words to a hex string.
|
||||
*/
|
||||
function binl2hex(binarray)
|
||||
{
|
||||
var hex_tab = "0123456789abcdef"
|
||||
var str = ""
|
||||
for(var i = 0; i < binarray.length * 4; i++)
|
||||
{
|
||||
str += hex_tab.charAt((binarray[i>>2] >> ((i%4)*8+4)) & 0xF) +
|
||||
hex_tab.charAt((binarray[i>>2] >> ((i%4)*8)) & 0xF)
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
/*
|
||||
* Convert an array of little-endian words to a base64 encoded string.
|
||||
*/
|
||||
function binl2b64(binarray)
|
||||
{
|
||||
var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||
var str = ""
|
||||
for(var i = 0; i < binarray.length * 32; i += 6)
|
||||
{
|
||||
str += tab.charAt(((binarray[i>>5] << (i%32)) & 0x3F) |
|
||||
((binarray[i>>5+1] >> (32-i%32)) & 0x3F))
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
/*
|
||||
* Convert an 8-bit character string to a sequence of 16-word blocks, stored
|
||||
* as an array, and append appropriate padding for MD4/5 calculation.
|
||||
* If any of the characters are >255, the high byte is silently ignored.
|
||||
*/
|
||||
function str2binl(str)
|
||||
{
|
||||
var nblk = ((str.length + 8) >> 6) + 1 // number of 16-word blocks
|
||||
var blks = new Array(nblk * 16)
|
||||
for(var i = 0; i < nblk * 16; i++) blks[i] = 0
|
||||
for(var i = 0; i < str.length; i++)
|
||||
blks[i>>2] |= (str.charCodeAt(i) & 0xFF) << ((i%4) * 8)
|
||||
blks[i>>2] |= 0x80 << ((i%4) * 8)
|
||||
blks[nblk*16-2] = str.length * 8
|
||||
return blks
|
||||
}
|
||||
|
||||
/*
|
||||
* Convert a wide-character string to a sequence of 16-word blocks, stored as
|
||||
* an array, and append appropriate padding for MD4/5 calculation.
|
||||
*/
|
||||
function strw2binl(str)
|
||||
{
|
||||
var nblk = ((str.length + 4) >> 5) + 1 // number of 16-word blocks
|
||||
var blks = new Array(nblk * 16)
|
||||
for(var i = 0; i < nblk * 16; i++) blks[i] = 0
|
||||
for(var i = 0; i < str.length; i++)
|
||||
blks[i>>1] |= str.charCodeAt(i) << ((i%2) * 16)
|
||||
blks[i>>1] |= 0x80 << ((i%2) * 16)
|
||||
blks[nblk*16-2] = str.length * 16
|
||||
return blks
|
||||
}
|
||||
|
||||
/*
|
||||
* External interface
|
||||
*/
|
||||
function hexMD5 (str) { return binl2hex(coreMD5( str2binl(str))) }
|
||||
function hexMD5w(str) { return binl2hex(coreMD5(strw2binl(str))) }
|
||||
function b64MD5 (str) { return binl2b64(coreMD5( str2binl(str))) }
|
||||
function b64MD5w(str) { return binl2b64(coreMD5(strw2binl(str))) }
|
||||
/* Backward compatibility */
|
||||
function calcMD5(str) { return binl2hex(coreMD5( str2binl(str))) }
|
||||
212
js/qr-scanner.js
Normal file
212
js/qr-scanner.js
Normal file
@@ -0,0 +1,212 @@
|
||||
let html5QrCode;
|
||||
let scannedUrl = "";
|
||||
|
||||
function safePause() {
|
||||
try {
|
||||
if (html5QrCode && html5QrCode.isScanning) {
|
||||
html5QrCode.pause();
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("SafePause: Scanner already paused or not scanning", e);
|
||||
}
|
||||
}
|
||||
|
||||
function safeResume() {
|
||||
try {
|
||||
if (html5QrCode && html5QrCode.isScanning) {
|
||||
html5QrCode.resume();
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("SafeResume: Scanner not paused or not scanning", e);
|
||||
}
|
||||
}
|
||||
|
||||
function handleDecodedText(decodedText) {
|
||||
console.log(`Scan result: ${decodedText}`);
|
||||
|
||||
let username = decodedText;
|
||||
let password = "";
|
||||
scannedUrl = "";
|
||||
|
||||
// Check if result is a URL (common for Mikhmon vouchers)
|
||||
try {
|
||||
if (decodedText.startsWith('http://') || decodedText.startsWith('https://')) {
|
||||
scannedUrl = decodedText; // Store for redirection
|
||||
const url = new URL(decodedText);
|
||||
const searchParams = url.search || (decodedText.includes('?') ? '?' + decodedText.split('?')[1] : '');
|
||||
const params = new URLSearchParams(searchParams);
|
||||
|
||||
if (params.has('username')) {
|
||||
username = params.get('username');
|
||||
}
|
||||
if (params.has('password')) {
|
||||
password = params.get('password');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error parsing QR URL:", e);
|
||||
}
|
||||
|
||||
// Fill inputs
|
||||
const voucherInput = document.getElementById('voucher-input');
|
||||
const passField = document.getElementById('voucher-pass');
|
||||
if (voucherInput) voucherInput.value = username;
|
||||
if (passField) passField.value = password || username;
|
||||
|
||||
// Show confirmation overlay
|
||||
const overlay = document.getElementById('qr-confirm-overlay');
|
||||
const confirmUser = document.getElementById('confirm-user');
|
||||
if (overlay && confirmUser) {
|
||||
confirmUser.innerText = username;
|
||||
overlay.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Pause camera scanning while confirming
|
||||
safePause();
|
||||
}
|
||||
|
||||
function cancelConfirm() {
|
||||
const overlay = document.getElementById('qr-confirm-overlay');
|
||||
if (overlay) overlay.classList.add('hidden');
|
||||
|
||||
safeResume();
|
||||
}
|
||||
|
||||
function proceedSubmit() {
|
||||
// If it's a URL, redirect directly
|
||||
if (scannedUrl) {
|
||||
console.log("Redirecting to scanned URL:", scannedUrl);
|
||||
window.location.href = scannedUrl;
|
||||
return;
|
||||
}
|
||||
|
||||
// Switch to voucher mode for manual codes
|
||||
setMode('voucher');
|
||||
|
||||
// Sync values to the actual form fields
|
||||
const voucherInput = document.getElementById('voucher-input');
|
||||
const voucherPass = document.getElementById('voucher-pass');
|
||||
const form = document.login;
|
||||
|
||||
if (form && voucherInput) {
|
||||
form.username.value = voucherInput.value;
|
||||
form.password.value = voucherPass ? voucherPass.value : voucherInput.value;
|
||||
}
|
||||
|
||||
// Close scanner
|
||||
closeQR();
|
||||
|
||||
// Submit
|
||||
const submitBtn = document.querySelector('button[type="submit"]');
|
||||
if (submitBtn) {
|
||||
submitBtn.click();
|
||||
}
|
||||
}
|
||||
|
||||
function scanFromFile(event) {
|
||||
try {
|
||||
const file = event.target.files[0];
|
||||
if (!file) {
|
||||
console.log("No file selected.");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("File selected:", file.name, file.type, file.size);
|
||||
|
||||
// Ensure library is available
|
||||
if (typeof Html5Qrcode === 'undefined') {
|
||||
alert("QR Scanner library not loaded. Please wait or refresh.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Pause camera gracefully
|
||||
safePause();
|
||||
|
||||
// Show a loading state
|
||||
const confirmUser = document.getElementById('confirm-user');
|
||||
if (confirmUser) confirmUser.innerText = "Scanning file...";
|
||||
|
||||
// Hide overlay if it was open
|
||||
const overlay = document.getElementById('qr-confirm-overlay');
|
||||
if (overlay) overlay.classList.add('hidden');
|
||||
|
||||
// Reuse instance if possible, or create temporary one for file
|
||||
const fileScanner = new Html5Qrcode("qr-file-reader");
|
||||
|
||||
fileScanner.scanFile(file, true)
|
||||
.then(decodedText => {
|
||||
console.log("Success scanning file:", decodedText);
|
||||
handleDecodedText(decodedText);
|
||||
fileScanner.clear(); // Cleanup
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(`Error scanning file: ${err}`);
|
||||
let msg = "No QR Code found.";
|
||||
if (typeof err === "string" && err.includes("not found")) {
|
||||
msg = "QR Code not detected. Try a clearer or closer photo.";
|
||||
}
|
||||
alert(msg);
|
||||
|
||||
if (confirmUser) confirmUser.innerText = "";
|
||||
|
||||
// Resume camera
|
||||
if (html5QrCode && html5QrCode.isScanning) {
|
||||
html5QrCode.resume();
|
||||
}
|
||||
fileScanner.clear(); // Cleanup
|
||||
});
|
||||
|
||||
// Reset input so searching for the same file again triggers change
|
||||
event.target.value = "";
|
||||
|
||||
} catch (e) {
|
||||
console.error("Fatal error in scanFromFile:", e);
|
||||
const errorMsg = e.message || JSON.stringify(e) || e;
|
||||
alert("An error occurred while opening the file: " + errorMsg);
|
||||
|
||||
// Try to resume camera if it crashed here
|
||||
if (html5QrCode && html5QrCode.isScanning) {
|
||||
html5QrCode.resume();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
function openQR() {
|
||||
const modal = document.getElementById('qr-scanner-modal');
|
||||
modal.style.display = 'flex';
|
||||
|
||||
if (!html5QrCode) {
|
||||
html5QrCode = new Html5Qrcode("reader");
|
||||
}
|
||||
|
||||
const config = { fps: 10, qrbox: { width: 250, height: 250 } };
|
||||
|
||||
html5QrCode.start(
|
||||
{ facingMode: "environment" },
|
||||
config,
|
||||
handleDecodedText
|
||||
).catch(err => {
|
||||
console.error("Scanner start error:", err);
|
||||
});
|
||||
}
|
||||
|
||||
function closeQR() {
|
||||
const modal = document.getElementById('qr-scanner-modal');
|
||||
const overlay = document.getElementById('qr-confirm-overlay');
|
||||
|
||||
if (modal) modal.style.display = 'none';
|
||||
if (overlay) overlay.classList.add('hidden');
|
||||
|
||||
if (html5QrCode) {
|
||||
html5QrCode.stop().then(() => {
|
||||
console.log("Scanner stopped");
|
||||
}).catch((err) => {
|
||||
// Ignore error if already stopped
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
255
js/script.js
Normal file
255
js/script.js
Normal file
@@ -0,0 +1,255 @@
|
||||
function setMode(mode) {
|
||||
const voucherMode = document.getElementById('voucher-mode');
|
||||
const memberMode = document.getElementById('member-mode');
|
||||
const voucherTab = document.querySelector('.tab-btn:nth-child(1)');
|
||||
const memberTab = document.querySelector('.tab-btn:nth-child(2)');
|
||||
const form = document.login;
|
||||
|
||||
if (mode === 'voucher') {
|
||||
voucherMode.classList.remove('hidden');
|
||||
memberMode.classList.add('hidden');
|
||||
voucherTab.classList.add('active');
|
||||
memberTab.classList.remove('active');
|
||||
|
||||
// Sync to form immediately
|
||||
if (form) {
|
||||
const code = document.getElementById('voucher-input').value;
|
||||
form.username.value = code;
|
||||
form.password.value = document.getElementById('voucher-pass').value || code;
|
||||
}
|
||||
} else {
|
||||
voucherMode.classList.add('hidden');
|
||||
memberMode.classList.remove('hidden');
|
||||
voucherTab.classList.remove('active');
|
||||
memberTab.classList.add('active');
|
||||
|
||||
// Sync to form immediately
|
||||
if (form) {
|
||||
form.username.value = document.getElementById('member-user').value;
|
||||
form.password.value = document.getElementById('member-pass').value;
|
||||
}
|
||||
}
|
||||
|
||||
// Update login button text based on mode
|
||||
const loginBtn = document.getElementById('login-btn');
|
||||
if (loginBtn) {
|
||||
const lang = localStorage.getItem('twinpath_lang') || 'en';
|
||||
const key = mode === 'voucher' ? 'login_voucher' : 'login_member';
|
||||
loginBtn.setAttribute('data-i18n', key);
|
||||
if (typeof translations !== 'undefined' && translations[lang] && translations[lang][key]) {
|
||||
loginBtn.innerText = translations[lang][key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function doLogin() {
|
||||
const form = document.login;
|
||||
const mode = document.querySelector('.tab-btn.active').innerText.toLowerCase();
|
||||
|
||||
// Sync inputs based on mode
|
||||
if (mode === 'voucher') {
|
||||
const code = document.getElementById('voucher-input').value;
|
||||
form.username.value = code;
|
||||
form.password.value = code; // Voucher usually uses same code for user/pass or just user with empty pass (depends on config)
|
||||
// Note: Check your hotspot config. Often Vouchers are "Username = Password"
|
||||
} else {
|
||||
form.username.value = document.getElementById('member-user').value;
|
||||
form.password.value = document.getElementById('member-pass').value;
|
||||
}
|
||||
|
||||
// Handle CHAP security if available
|
||||
// Note: This relies on variables injected by TwinpathNet (Mikrotik) into the HTML/JS context
|
||||
// We assume 'hexMD5' is available from md5.js
|
||||
/*
|
||||
TwinpathNet usually puts this logic in the <script> block directly in login.html
|
||||
to access $(chap-id) and $(chap-challenge).
|
||||
Since we extracted this to an external file, we need to ensure those vars are accessible.
|
||||
However, usually $(variables) are NOT replaced in .js files by TwinpathNet.
|
||||
They are ONLY replaced in .html files.
|
||||
|
||||
So, strict MD5 hashing MUST be done inside login.html <script> block, OR
|
||||
we submit plain text and let RouterOS handle PAP (if enabled).
|
||||
|
||||
For this "Cool" template, we will assume PAP is enabled for simplicity,
|
||||
OR we can trust the form to submit properly.
|
||||
*/
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function copyToClipboard(text) {
|
||||
const el = document.createElement('textarea');
|
||||
el.value = text;
|
||||
document.body.appendChild(el);
|
||||
el.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(el);
|
||||
alert("URL Berhasil Disalin! Silakan buka Chrome dan tempel (paste) di sana.");
|
||||
}
|
||||
|
||||
function openInExternalBrowser() {
|
||||
const url = brandConfig.portalUrl;
|
||||
const userAgent = navigator.userAgent || navigator.vendor || window.opera;
|
||||
|
||||
if (/android/i.test(userAgent)) {
|
||||
// Android Intent for Chrome
|
||||
const domain = new URL(brandConfig.portalUrl).hostname;
|
||||
window.location.href = `intent://${domain}#Intent;scheme=http;package=com.android.chrome;end`;
|
||||
} else if (/iPad|iPhone|iPod/.test(userAgent) && !window.MSStream) {
|
||||
// iOS: No magic intent from CNA usually works, so we guide them to use the copy button
|
||||
alert("Untuk pengguna iPhone/iPad: Silakan klik tombol 'Salin Link Portal' lalu buka Safari dan tempel alamatnya.");
|
||||
} else {
|
||||
// Other devices
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
}
|
||||
|
||||
function formatMikrotikTime(timeStr) {
|
||||
if (!timeStr || timeStr === 'unlimited' || timeStr.includes('$(')) return timeStr;
|
||||
|
||||
const regex = /(\d+)([wdhms])/g;
|
||||
let parts = [];
|
||||
let match;
|
||||
|
||||
const unitMap = {
|
||||
'w': { 'en': 'w', 'id': ' Minggu' },
|
||||
'd': { 'en': 'd', 'id': ' Hari' },
|
||||
'h': { 'en': 'h', 'id': ' Jam' },
|
||||
'm': { 'en': 'm', 'id': ' Menit' },
|
||||
's': { 'en': 's', 'id': ' Detik' }
|
||||
};
|
||||
|
||||
const lang = localStorage.getItem('twinpath_lang') || 'en';
|
||||
|
||||
while ((match = regex.exec(timeStr)) !== null) {
|
||||
const val = match[1];
|
||||
const unit = match[2];
|
||||
parts.push(val + (unitMap[unit][lang] || unit));
|
||||
}
|
||||
|
||||
if (parts.length === 0) return timeStr;
|
||||
return parts.slice(0, 3).join(' ');
|
||||
}
|
||||
|
||||
function parseMikrotikTimeToSeconds(timeStr) {
|
||||
if (!timeStr || timeStr.includes('$(')) return 0;
|
||||
const regex = /(\d+)([wdhms])/g;
|
||||
let totalSeconds = 0;
|
||||
let match;
|
||||
const multipliers = { 'w': 604800, 'd': 86400, 'h': 3600, 'm': 60, 's': 1 };
|
||||
|
||||
while ((match = regex.exec(timeStr)) !== null) {
|
||||
totalSeconds += parseInt(match[1]) * (multipliers[match[2]] || 0);
|
||||
}
|
||||
return totalSeconds;
|
||||
}
|
||||
|
||||
function updateProgressBars() {
|
||||
const data = document.getElementById('dashboard-data');
|
||||
if (!data) return;
|
||||
|
||||
const limitUptime = parseMikrotikTimeToSeconds(data.getAttribute('data-limit-uptime'));
|
||||
const uptime = parseMikrotikTimeToSeconds(data.getAttribute('data-uptime'));
|
||||
const limitBytes = parseInt(data.getAttribute('data-limit-bytes')) || 0;
|
||||
const bytesOut = parseInt(data.getAttribute('data-bytes-out')) || 0;
|
||||
|
||||
// Time Progress: (Total - Used) / Total -> Remaining %
|
||||
if (limitUptime > 0) {
|
||||
const remainingTime = Math.max(0, limitUptime - uptime);
|
||||
const timePercent = (remainingTime / limitUptime) * 100;
|
||||
const timeBar = document.querySelector('.progress-time');
|
||||
if (timeBar) timeBar.style.width = timePercent + '%';
|
||||
} else {
|
||||
const timeBar = document.querySelector('.progress-time');
|
||||
if (timeBar) timeBar.style.width = '100%'; // Unlimited
|
||||
}
|
||||
|
||||
// Quota Progress: (Total - Used) / Total -> Remaining %
|
||||
if (limitBytes > 0) {
|
||||
const remainingBytes = Math.max(0, limitBytes - bytesOut);
|
||||
const quotaPercent = (remainingBytes / limitBytes) * 100;
|
||||
const quotaBar = document.querySelector('.progress-quota');
|
||||
if (quotaBar) quotaBar.style.width = quotaPercent + '%';
|
||||
} else {
|
||||
const quotaBar = document.querySelector('.progress-quota');
|
||||
if (quotaBar) quotaBar.style.width = '100%'; // Unlimited
|
||||
}
|
||||
}
|
||||
|
||||
function initDashboard() {
|
||||
// 1. Set Greetings
|
||||
const greetingEl = document.getElementById('greeting-text');
|
||||
if (greetingEl) {
|
||||
const hour = new Date().getHours();
|
||||
const lang = localStorage.getItem('twinpath_lang') || 'en';
|
||||
let greeting = "";
|
||||
|
||||
if (lang === 'id') {
|
||||
if (hour < 11) greeting = "Selamat Pagi";
|
||||
else if (hour < 15) greeting = "Selamat Siang";
|
||||
else if (hour < 19) greeting = "Selamat Sore";
|
||||
else greeting = "Selamat Malam";
|
||||
} else {
|
||||
if (hour < 12) greeting = "Good Morning";
|
||||
else if (hour < 17) greeting = "Good Afternoon";
|
||||
else greeting = "Good Evening";
|
||||
}
|
||||
greetingEl.innerText = greeting + "!";
|
||||
}
|
||||
|
||||
// 2. Format Time Components
|
||||
document.querySelectorAll('[data-type="mikrotik-time"]').forEach(el => {
|
||||
let val = el.innerText.trim();
|
||||
// If empty or Mikrotik failed to replace (still contains $), it's Unlimited
|
||||
if (!val || val.includes('$(')) {
|
||||
const lang = localStorage.getItem('twinpath_lang') || 'en';
|
||||
el.innerText = (translations[lang] && translations[lang]['unlimited']) || 'Unlimited';
|
||||
} else {
|
||||
el.innerText = formatMikrotikTime(val);
|
||||
}
|
||||
});
|
||||
|
||||
// 3. Dynamic Progress Bars
|
||||
updateProgressBars();
|
||||
}
|
||||
|
||||
// Update DOMContentLoaded to include initDashboard
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Determine page type
|
||||
const isLogin = !!document.getElementById('voucher-input');
|
||||
const isStatus = !!document.getElementById('greeting-text');
|
||||
|
||||
if (isLogin) {
|
||||
setMode('voucher');
|
||||
}
|
||||
|
||||
initDashboard();
|
||||
|
||||
// Escape modal
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
const modal = document.getElementById('qr-scanner-modal');
|
||||
if (modal && modal.style.display === 'flex') {
|
||||
if (typeof closeQR === 'function') {
|
||||
closeQR();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function togglePassword(inputId, button) {
|
||||
const passwordInput = document.getElementById(inputId);
|
||||
const toggleIcon = button.querySelector('img');
|
||||
|
||||
if (passwordInput.type === 'password') {
|
||||
passwordInput.type = 'text';
|
||||
toggleIcon.src = 'svg/eye-off.svg';
|
||||
} else {
|
||||
passwordInput.type = 'password';
|
||||
toggleIcon.src = 'svg/eye.svg';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user