mirror of
https://github.com/mivodev/plugin-mivo-theme.git
synced 2026-01-26 05:15:27 +07:00
docs: add MIT license
This commit is contained in:
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 DyzulkDev
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
21
README.md
Normal file
21
README.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Mivo Theme Downloader
|
||||||
|
|
||||||
|
**Category:** Hotspot Tools
|
||||||
|
**Author:** DyzulkDev
|
||||||
|
**Version:** 1.0.0
|
||||||
|
|
||||||
|
The **Mivo Theme Downloader** allows you to easily download and install the latest Captive Portal themes directly from the Mivo settings panel.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
- **One-Click Download**: Fetch the latest themes from the official repository.
|
||||||
|
- **Auto-Extraction**: Automatically extracts and places theme files in the correct directory.
|
||||||
|
- **Updates**: Keeps your hotspot login page up to date with new designs.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
1. Download the plugin ZIP file.
|
||||||
|
2. Go to **Mivo > Settings > Plugins**.
|
||||||
|
3. Click **Upload Plugin** and select the ZIP file.
|
||||||
|
4. The plugin will appear in your installed list.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
Navigate to the **Theme Manager** in the sidebar (under Hotspot Tools) to browse and download available themes.
|
||||||
234
plugin.php
Normal file
234
plugin.php
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Core\Hooks;
|
||||||
|
use App\Core\Router;
|
||||||
|
use App\Helpers\UrlHelper; // Assuming this exists or we use global functions
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin Name: Mivo Theme Downloader
|
||||||
|
* Description: Allows downloading the Captive Portal theme with auto-configuration.
|
||||||
|
* Version: 1.0.0
|
||||||
|
* Author: DyzulkDev
|
||||||
|
*
|
||||||
|
* Category: Hotspot Tools
|
||||||
|
* Scope: Session
|
||||||
|
* Tags: theme, downloader, hotspot, captive-portal
|
||||||
|
* Core Version: >= 1.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 1. Register Routes
|
||||||
|
Hooks::addAction('router_init', function(Router $router) {
|
||||||
|
|
||||||
|
// Page to show the download button
|
||||||
|
$router->get('/{session}/theme/manager', function($session) {
|
||||||
|
$title = 'Theme Manager'; // Fallback title
|
||||||
|
|
||||||
|
// Include Header
|
||||||
|
require ROOT . '/app/Views/layouts/header_main.php';
|
||||||
|
|
||||||
|
// 1. Inject Plugin Translations using the new extend() method
|
||||||
|
?>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
if (window.i18n) {
|
||||||
|
window.i18n.extend({
|
||||||
|
"theme_manager": {
|
||||||
|
"title": "Theme Manager",
|
||||||
|
"desc": "Manage and download your captive portal theme.",
|
||||||
|
"download_title": "Download Mivo Theme",
|
||||||
|
"download_desc": "Download the fully configured captive portal theme for this router session. The package includes your specific configuration (API Base URL and Session ID) automatically injected into the theme assets.",
|
||||||
|
"btn_download": "Download Theme (.zip)",
|
||||||
|
"install_title": "Installation",
|
||||||
|
"install_steps": {
|
||||||
|
"1": "Download the zip file.",
|
||||||
|
"2": "Extract the contents.",
|
||||||
|
"3": "Upload the folders (css, fonts, js, etc.) and login.html to your Mikrotik Hotspot directory."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add Indonesian Translations if needed (or basic object merge based on current lang?)
|
||||||
|
// ideally extend takes a full object structure.
|
||||||
|
// Wait, our i18n structure is flat language based files?
|
||||||
|
// i18n.js loads `/lang/{lang}.json`.
|
||||||
|
// So `this.translations` is the flat object for the CURRENT language.
|
||||||
|
// So we need to detect current lang and inject the RIGHT on.
|
||||||
|
|
||||||
|
const currentLang = localStorage.getItem('mivo_lang') || 'en';
|
||||||
|
if (currentLang === 'id') {
|
||||||
|
window.i18n.extend({
|
||||||
|
"theme_manager": {
|
||||||
|
"title": "Manajer Tema",
|
||||||
|
"desc": "Kelola dan unduh tema captive portal Anda.",
|
||||||
|
"download_title": "Unduh Tema Mivo",
|
||||||
|
"download_desc": "Unduh tema captive portal yang sudah dikonfigurasi sepenuhnya untuk sesi router ini. Paket ini mencakup konfigurasi spesifik Anda (URL Dasar API dan ID Sesi) yang disuntikkan secara otomatis ke dalam aset tema.",
|
||||||
|
"btn_download": "Unduh Tema (.zip)",
|
||||||
|
"install_title": "Instalasi",
|
||||||
|
"install_steps": {
|
||||||
|
"1": "Unduh file zip.",
|
||||||
|
"2": "Ekstrak isinya.",
|
||||||
|
"3": "Unggah folder (css, fonts, js, dll.) dan login.html ke direktori Hotspot Mikrotik Anda."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="space-y-6">
|
||||||
|
<div class="mb-6">
|
||||||
|
<h1 class="text-2xl font-bold tracking-tight" data-i18n="theme_manager.title">Theme Manager</h1>
|
||||||
|
<p class="text-sm text-accents-5" data-i18n="theme_manager.desc">Manage and download your captive portal theme.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card p-6 max-w-2xl">
|
||||||
|
<h2 class="text-lg font-semibold mb-2" data-i18n="theme_manager.download_title">Download Mivo Theme</h2>
|
||||||
|
<p class="text-accents-5 mb-6 text-sm" data-i18n="theme_manager.download_desc">Download the fully configured captive portal theme for this router session. The package includes your specific configuration (API Base URL and Session ID) automatically injected into the theme assets.</p>
|
||||||
|
|
||||||
|
<form action="/<?= htmlspecialchars($session) ?>/theme/download" method="POST" target="_blank">
|
||||||
|
<button type="submit" class="btn btn-primary px-6 py-2.5 font-semibold text-sm">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-download mr-2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" x2="12" y1="15" y2="3"/></svg>
|
||||||
|
<span data-i18n="theme_manager.btn_download">Download Theme (.zip)</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="mt-6 pt-6 border-t border-accents-2">
|
||||||
|
<h3 class="text-sm font-semibold mb-2" data-i18n="theme_manager.install_title">Installation</h3>
|
||||||
|
<ol class="list-decimal list-inside text-sm text-accents-5 space-y-1">
|
||||||
|
<li data-i18n="theme_manager.install_steps.1">Download the zip file.</li>
|
||||||
|
<li data-i18n="theme_manager.install_steps.2">Extract the contents.</li>
|
||||||
|
<li data-i18n="theme_manager.install_steps.3">Upload the folders (css, fonts, js, etc.) and login.html to your Mikrotik Hotspot directory.</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
|
||||||
|
// Include Footer
|
||||||
|
require ROOT . '/app/Views/layouts/footer_main.php';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle Download Process
|
||||||
|
$router->post('/{session}/theme/download', function($session) {
|
||||||
|
$sourcePath = __DIR__ . '/theme';
|
||||||
|
if (!is_dir($sourcePath)) {
|
||||||
|
die("Theme source not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine Configuration
|
||||||
|
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' || $_SERVER['SERVER_PORT'] == 443) ? "https://" : "http://";
|
||||||
|
$host = $_SERVER['HTTP_HOST'];
|
||||||
|
$baseUrl = $protocol . $host . '/mivo/public'; // Adjust if needed based on real deployment, maybe just $protocol . $host if root
|
||||||
|
// If app is in root:
|
||||||
|
$baseUrl = $protocol . $host;
|
||||||
|
|
||||||
|
// Prepare Zip
|
||||||
|
$zipFile = sys_get_temp_dir() . '/mivo-theme-' . $session . '-' . date('YmdHis') . '.zip';
|
||||||
|
$zip = new ZipArchive();
|
||||||
|
|
||||||
|
if ($zip->open($zipFile, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== TRUE) {
|
||||||
|
die("Cannot create zip file.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$files = new RecursiveIteratorIterator(
|
||||||
|
new RecursiveDirectoryIterator($sourcePath, RecursiveDirectoryIterator::SKIP_DOTS),
|
||||||
|
RecursiveIteratorIterator::LEAVES_ONLY
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($files as $name => $file) {
|
||||||
|
if ($file->isDir()) continue;
|
||||||
|
|
||||||
|
$filePath = $file->getRealPath();
|
||||||
|
$relativePath = substr($filePath, strlen($sourcePath) + 1);
|
||||||
|
|
||||||
|
// Normalize slashes for Zip
|
||||||
|
$zipPath = str_replace('\\', '/', $relativePath);
|
||||||
|
|
||||||
|
if (strpos($zipPath, 'assets/js/main.js') !== false) {
|
||||||
|
// Read and Replace
|
||||||
|
$content = file_get_contents($filePath);
|
||||||
|
|
||||||
|
// Replacements
|
||||||
|
$content = str_replace('apiBaseUrl: ""', 'apiBaseUrl: "' . $baseUrl . '"', $content);
|
||||||
|
$content = str_replace('apiSession: "router-jakarta-1"', 'apiSession: "' . $session . '"', $content);
|
||||||
|
|
||||||
|
$zip->addFromString($zipPath, $content);
|
||||||
|
} else {
|
||||||
|
$zip->addFile($filePath, $zipPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$zip->close();
|
||||||
|
|
||||||
|
// Serve File
|
||||||
|
if (file_exists($zipFile)) {
|
||||||
|
header('Content-Description: File Transfer');
|
||||||
|
header('Content-Type: application/zip');
|
||||||
|
header('Content-Disposition: attachment; filename="mivo-theme-'.$session.'.zip"');
|
||||||
|
header('Expires: 0');
|
||||||
|
header('Cache-Control: must-revalidate');
|
||||||
|
header('Pragma: public');
|
||||||
|
header('Content-Length: ' . filesize($zipFile));
|
||||||
|
readfile($zipFile);
|
||||||
|
unlink($zipFile);
|
||||||
|
exit;
|
||||||
|
} else {
|
||||||
|
die("Failed to create zip.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Inject Menu into Sidebar (Using Footer JS Hack)
|
||||||
|
Hooks::addAction('mivo_footer', function() {
|
||||||
|
// Get current session from URL if possible
|
||||||
|
$uri = $_SERVER['REQUEST_URI'] ?? '/';
|
||||||
|
$parts = explode('/', trim($uri, '/'));
|
||||||
|
if (count($parts) >= 2 && $parts[1] === 'dashboard' || isset($parts[0])) {
|
||||||
|
$session = $parts[0]; // Assuming /{session}/...
|
||||||
|
|
||||||
|
// Check if it's a valid session context looking like a session string
|
||||||
|
// Simple basic check to avoid injecting on non-session pages
|
||||||
|
if (!empty($session) && $session !== 'settings' && $session !== 'login' && $session !== 'install') {
|
||||||
|
?>
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
|
// Find the Hotspot Menu Container
|
||||||
|
const hotspotMenu = document.getElementById('hotspot-menu');
|
||||||
|
if (hotspotMenu) {
|
||||||
|
// Inject Translation for Menu
|
||||||
|
if (window.i18n) {
|
||||||
|
const currentLang = localStorage.getItem('mivo_lang') || 'en';
|
||||||
|
const menuTrans = {
|
||||||
|
"en": { "sidebar": { "theme_manager": "Theme Manager" } },
|
||||||
|
"id": { "sidebar": { "theme_manager": "Manajer Tema" } }
|
||||||
|
};
|
||||||
|
// Merge relevant lang
|
||||||
|
if (menuTrans[currentLang]) {
|
||||||
|
window.i18n.extend(menuTrans[currentLang]);
|
||||||
|
} else {
|
||||||
|
window.i18n.extend(menuTrans['en']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Link
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = '/<?= $session ?>/theme/manager';
|
||||||
|
link.className = 'block px-3 py-2 rounded-md text-sm transition-colors text-accents-6 hover:text-foreground';
|
||||||
|
link.innerHTML = '<span data-i18n="sidebar.theme_manager">Theme Manager</span>';
|
||||||
|
|
||||||
|
// Add styles to match active state if needed (simplified)
|
||||||
|
if (window.location.pathname.includes('/theme/manager')) {
|
||||||
|
link.className = 'block px-3 py-2 rounded-md text-sm transition-colors bg-white/40 dark:bg-white/5 text-foreground ring-1 ring-white/10 font-medium';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append
|
||||||
|
hotspotMenu.appendChild(link);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
57
theme/alogin.html
Normal file
57
theme/alogin.html
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="pragma" content="no-cache">
|
||||||
|
<meta http-equiv="expires" content="-1">
|
||||||
|
<link rel="icon" href="favicon.ico">
|
||||||
|
<link rel="stylesheet" href="assets/css/styles.css">
|
||||||
|
<script defer="" src="assets/js/alpine.min.js"></script>
|
||||||
|
<script src="assets/js/main.js"></script>
|
||||||
|
<script src="assets/js/theme.js"></script>
|
||||||
|
<script src="assets/js/i18n.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
if (localStorage.getItem('mivo_theme') === 'dark' || (!localStorage.getItem('mivo_theme') && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('dark');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<body class="antialiased bg-background text-foreground bg-cover bg-center min-h-screen flex items-center justify-center p-4 selection:bg-accent selection:text-white overflow-hidden" x-data="theme" :class="theme" :style="'background-image: url(\'assets/img/' + (theme === 'light' ? 'bg-light.jpg' : 'bg-dark.jpg') + '\');'">
|
||||||
|
|
||||||
|
<div class="fixed inset-0 bg-background/30 backdrop-blur-sm z-0"></div>
|
||||||
|
|
||||||
|
<div class="relative z-10 w-full max-w-sm">
|
||||||
|
<div class="card p-8 text-center space-y-6">
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<div class="relative w-16 h-16 flex items-center justify-center rounded-2xl bg-primary/10 text-primary animate-pulse">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-8 h-8" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12l14 0"></path><path d="M13 18l6 -6"></path><path d="M13 6l6 6"></path></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h1 class="text-xl font-bold tracking-tight">Redirecting...</h1>
|
||||||
|
<p class="text-slate-500 text-sm">You are being redirected to your destination.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-xs text-slate-400">
|
||||||
|
If you are not redirected automatically, <a href="$(link-redirect)" class="text-primary hover:underline font-medium">click here</a>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-8 pt-8 border-t border-accents-2 dark:border-white/10">
|
||||||
|
<p class="text-accents-5 text-[10px] tracking-widest uppercase">MIVO THEME BY DYZULKDEV • POWERED BY MIVO</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mikrotik Redirect Logic -->
|
||||||
|
<meta http-equiv="refresh" content="2; url=$(link-redirect)">
|
||||||
|
<script>
|
||||||
|
setTimeout(function() {
|
||||||
|
window.location.href = '$(link-redirect)';
|
||||||
|
}, 2000);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
11
theme/api.json
Normal file
11
theme/api.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"captive": $(if logged-in == 'yes')false$(else)true$(endif),
|
||||||
|
"user-portal-url": "$(link-login-only)",
|
||||||
|
$(if session-timeout-secs != 0)
|
||||||
|
"seconds-remaining": $(session-timeout-secs),
|
||||||
|
$(endif)
|
||||||
|
$(if remain-bytes-total)
|
||||||
|
"bytes-remaining": $(remain-bytes-total),
|
||||||
|
$(endif)
|
||||||
|
"can-extend-session": true
|
||||||
|
}
|
||||||
0
theme/assets/aa.txt
Normal file
0
theme/assets/aa.txt
Normal file
2512
theme/assets/css/styles.css
Normal file
2512
theme/assets/css/styles.css
Normal file
File diff suppressed because it is too large
Load Diff
6
theme/assets/img/logo-m-dark.svg
Normal file
6
theme/assets/img/logo-m-dark.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="512" height="512">
|
||||||
|
<path d="M0 0 C40.59 0 81.18 0 123 0 C128.94 9.9 128.94 9.9 135 20 C137.46577512 23.98910833 139.93737241 27.97040257 142.4375 31.9375 C143.71783436 33.97762211 144.99777509 36.01799131 146.27734375 38.05859375 C146.90237793 39.05487793 147.52741211 40.05116211 148.17138672 41.07763672 C150.96446678 45.54134411 153.73307277 50.02005064 156.5 54.5 C161.0792303 61.91158465 165.68818082 69.30397167 170.3125 76.6875 C176.26318836 86.19138864 182.15150549 95.73229223 188.01879883 105.2878418 C193.08709306 113.53496427 198.208198 121.74780885 203.34887695 129.94995117 C208.00076575 137.38038241 212.60177706 144.84108448 217.1875 152.3125 C221.7457377 159.73815683 226.3289587 167.14474569 231 174.5 C236.37297853 182.96055964 241.62451084 191.49338078 246.8671875 200.03515625 C251.37750651 207.37833217 255.92338863 214.69797726 260.5 222 C265.77200627 230.41153127 270.99463848 238.85188721 276.1875 247.3125 C280.74574404 254.73816715 285.32948761 262.14440277 290 269.5 C294.5525814 276.67922327 299.0626269 283.88437921 303.53466797 291.11401367 C304.25463221 292.27682202 304.97596045 293.43878708 305.69873047 294.59985352 C306.71188663 296.22822455 307.71932676 297.86014774 308.7265625 299.4921875 C309.31002441 300.43433105 309.89348633 301.37647461 310.49462891 302.34716797 C313.29448494 307.28119905 313.29448494 307.28119905 314 310 C313.10742187 312.33886719 313.10742187 312.33886719 311.46875 314.796875 C310.85410889 315.73821289 310.23946777 316.67955078 309.60620117 317.64941406 C308.91115479 318.67260742 308.2161084 319.69580078 307.5 320.75 C306.42359253 322.38146973 306.42359253 322.38146973 305.32543945 324.04589844 C303.33059412 327.06821098 301.312855 330.07460252 299.29058838 333.07861328 C297.08153396 336.36738288 294.89143772 339.6687451 292.69921875 342.96875 C289.93705235 347.12519373 287.17076618 351.27866178 284.38696289 355.4206543 C279.69084095 362.41374195 275.05797703 369.43667567 270.546875 376.55078125 C269.67715056 377.91328474 268.80712089 379.27559345 267.93676758 380.63769531 C266.69343815 382.58352582 265.45644896 384.53196515 264.23886108 386.49401855 C263.1314603 388.27830807 262.00378362 390.04810218 260.87109375 391.81640625 C260.24130615 392.82131104 259.61151855 393.82621582 258.96264648 394.86157227 C257 397 257 397 254.55784607 397.3649292 C252 397 252 397 250.43936157 395.46855164 C250.00127167 394.75278458 249.56318176 394.03701752 249.11181641 393.29956055 C248.60661469 392.49629837 248.10141296 391.69303619 247.5809021 390.86543274 C247.05147003 389.98558212 246.52203796 389.10573151 245.9765625 388.19921875 C244.81884975 386.34633241 243.66030081 384.49396831 242.50097656 382.64208984 C241.91658813 381.69622009 241.33219971 380.75035034 240.73010254 379.77581787 C238.15263697 375.6404705 235.46629193 371.58029076 232.7734375 367.51953125 C231.72553934 365.92993275 230.67801875 364.34008528 229.63085938 362.75 C228.54557801 361.1041633 227.46029141 359.45833007 226.375 357.8125 C225.20371001 356.03519977 224.03248774 354.2578549 222.86132812 352.48046875 C220.44133946 348.80793487 218.02106714 345.13558818 215.60058594 341.46337891 C210.45583874 333.65758543 205.31468042 325.84942899 200.17340088 318.04135132 C197.36576183 313.77746136 194.55786745 309.51373955 191.75 305.25 C190.62499879 303.54166746 189.49999879 301.83333413 188.375 300.125 C187.818125 299.279375 187.26125 298.43375 186.6875 297.5625 C181.62499997 289.87499995 181.62499997 289.87499995 179.93762207 287.31268311 C178.81221794 285.6037376 177.68681104 283.89479391 176.56140137 282.18585205 C173.75506191 277.92439992 170.94877005 273.66291647 168.14257812 269.40136719 C163.00738565 261.60304695 157.87180924 253.80498071 152.73449707 246.00805664 C150.38571799 242.44323839 148.03721141 238.87824066 145.6887207 235.31323242 C144.60145193 233.66291655 143.51405939 232.01268221 142.42651367 230.36254883 C132.25421049 214.9279819 122.13043857 199.46224835 112 184 C111.67 292.24 111.34 400.48 111 512 C74.37 512 37.74 512 0 512 C0 343.04 0 174.08 0 0 Z " fill="#ffffff" transform="translate(0,0)"/>
|
||||||
|
<path d="M0 0 C40.59 0 81.18 0 123 0 C123 168.96 123 337.92 123 512 C86.04 512 49.08 512 11 512 C10.67 402.77 10.34 293.54 10 181 C4.72 189.25 -0.56 197.5 -6 206 C-9.39444207 211.15548467 -12.78716673 216.30779963 -16.22192383 221.43579102 C-17.599304 223.49245466 -18.97271714 225.55172895 -20.34570312 227.61132812 C-22.50073142 230.84064053 -24.66547146 234.06319701 -26.83917236 237.27996826 C-27.94230355 238.91450952 -29.03943552 240.55309486 -30.13623047 242.19189453 C-30.76029785 243.1095459 -31.38436523 244.02719727 -32.02734375 244.97265625 C-32.559646 245.760354 -33.09194824 246.54805176 -33.64038086 247.35961914 C-34.08905518 247.90094482 -34.53772949 248.44227051 -35 249 C-35.66 249 -36.32 249 -37 249 C-38.35009766 247.20947266 -38.35009766 247.20947266 -39.9140625 244.6640625 C-40.501875 243.7153125 -41.0896875 242.7665625 -41.6953125 241.7890625 C-42.33210938 240.74492187 -42.96890625 239.70078125 -43.625 238.625 C-44.29015625 237.54734375 -44.9553125 236.4696875 -45.640625 235.359375 C-47.41266874 232.48561118 -49.17847981 229.6082147 -50.94104004 226.7286377 C-52.63090725 223.9701121 -54.32738918 221.21566567 -56.0234375 218.4609375 C-58.01919588 215.21887138 -60.01443826 211.97649876 -62.00732422 208.73266602 C-66.55995548 201.32481658 -71.14231268 193.93622184 -75.75 186.5625 C-76.66652344 185.09151855 -76.66652344 185.09151855 -77.6015625 183.59082031 C-80.57044721 178.83646363 -83.57016988 174.10746656 -86.640625 169.41796875 C-87.20813477 168.54390381 -87.77564453 167.66983887 -88.36035156 166.76928711 C-89.42453538 165.1341899 -90.49885882 163.50563072 -91.58496094 161.88500977 C-92.05192383 161.16498779 -92.51888672 160.44496582 -93 159.703125 C-93.4125 159.08083008 -93.825 158.45853516 -94.25 157.81738281 C-95.23752366 155.42443811 -94.91065312 154.39823636 -94 152 C-93.28247855 150.60752627 -92.51012761 149.24258512 -91.69921875 147.90234375 C-90.98511841 146.71560059 -90.98511841 146.71560059 -90.2565918 145.50488281 C-89.7387915 144.65764648 -89.22099121 143.81041016 -88.6875 142.9375 C-88.14843018 142.04772461 -87.60936035 141.15794922 -87.05395508 140.24121094 C-82.51613674 132.77957799 -77.86646672 125.39067843 -73.18359375 118.01953125 C-63.57216547 102.88747676 -54.1487842 87.63834556 -44.77050781 72.36108398 C-40.88941569 66.04649596 -36.97338141 59.75688711 -33 53.5 C-21.76980025 35.81118073 -10.92512644 17.87747962 0 0 Z " fill="#ffffff" transform="translate(389,0)"/>
|
||||||
|
<path d="M0 0 C4.62276577 4.14454862 7.47322448 8.77374995 10.5625 14.125 C11.62963614 15.93860414 12.69869312 17.75107923 13.76953125 19.5625 C14.30916504 20.47644531 14.84879883 21.39039063 15.40478516 22.33203125 C17.78240918 26.30856558 20.25885329 30.21891917 22.75 34.125 C28.22964988 42.73406002 33.58176135 51.41803925 38.89916992 60.12817383 C42.24750462 65.59578939 45.64232668 71.01967842 49.16796875 76.375 C49.82361816 77.37684326 50.47926758 78.37868652 51.15478516 79.41088867 C52.39829787 81.30225655 53.6539539 83.18571316 54.92333984 85.05981445 C55.47586426 85.9024585 56.02838867 86.74510254 56.59765625 87.61328125 C57.08758057 88.34329346 57.57750488 89.07330566 58.08227539 89.82543945 C59.28133227 92.66662052 58.92600786 94.09059312 58 97 C57.01611328 99.00463867 57.01611328 99.00463867 55.8203125 100.91796875 C55.38589844 101.61470703 54.95148438 102.31144531 54.50390625 103.02929688 C54.04886719 103.74150391 53.59382813 104.45371094 53.125 105.1875 C52.67769531 105.91130859 52.23039062 106.63511719 51.76953125 107.38085938 C49.65391647 110.73526333 48.36691842 112.75538772 45 115 C44.37508667 113.98353394 44.37508667 113.98353394 43.73754883 112.9465332 C27.17658577 86.00544711 27.17658577 86.00544711 10.46337891 59.15869141 C2.53411428 46.50404945 -5.26881404 33.77655539 -13 21 C-4.738041 6.21867882 -4.738041 6.21867882 0 0 Z " fill="#ffffff" transform="translate(281,175)"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 7.9 KiB |
6
theme/assets/img/logo-m.svg
Normal file
6
theme/assets/img/logo-m.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="512" height="512">
|
||||||
|
<path d="M0 0 C40.59 0 81.18 0 123 0 C128.94 9.9 128.94 9.9 135 20 C137.46577512 23.98910833 139.93737241 27.97040257 142.4375 31.9375 C143.71783436 33.97762211 144.99777509 36.01799131 146.27734375 38.05859375 C146.90237793 39.05487793 147.52741211 40.05116211 148.17138672 41.07763672 C150.96446678 45.54134411 153.73307277 50.02005064 156.5 54.5 C161.0792303 61.91158465 165.68818082 69.30397167 170.3125 76.6875 C176.26318836 86.19138864 182.15150549 95.73229223 188.01879883 105.2878418 C193.08709306 113.53496427 198.208198 121.74780885 203.34887695 129.94995117 C208.00076575 137.38038241 212.60177706 144.84108448 217.1875 152.3125 C221.7457377 159.73815683 226.3289587 167.14474569 231 174.5 C236.37297853 182.96055964 241.62451084 191.49338078 246.8671875 200.03515625 C251.37750651 207.37833217 255.92338863 214.69797726 260.5 222 C265.77200627 230.41153127 270.99463848 238.85188721 276.1875 247.3125 C280.74574404 254.73816715 285.32948761 262.14440277 290 269.5 C294.5525814 276.67922327 299.0626269 283.88437921 303.53466797 291.11401367 C304.25463221 292.27682202 304.97596045 293.43878708 305.69873047 294.59985352 C306.71188663 296.22822455 307.71932676 297.86014774 308.7265625 299.4921875 C309.31002441 300.43433105 309.89348633 301.37647461 310.49462891 302.34716797 C313.29448494 307.28119905 313.29448494 307.28119905 314 310 C313.10742187 312.33886719 313.10742187 312.33886719 311.46875 314.796875 C310.85410889 315.73821289 310.23946777 316.67955078 309.60620117 317.64941406 C308.91115479 318.67260742 308.2161084 319.69580078 307.5 320.75 C306.42359253 322.38146973 306.42359253 322.38146973 305.32543945 324.04589844 C303.33059412 327.06821098 301.312855 330.07460252 299.29058838 333.07861328 C297.08153396 336.36738288 294.89143772 339.6687451 292.69921875 342.96875 C289.93705235 347.12519373 287.17076618 351.27866178 284.38696289 355.4206543 C279.69084095 362.41374195 275.05797703 369.43667567 270.546875 376.55078125 C269.67715056 377.91328474 268.80712089 379.27559345 267.93676758 380.63769531 C266.69343815 382.58352582 265.45644896 384.53196515 264.23886108 386.49401855 C263.1314603 388.27830807 262.00378362 390.04810218 260.87109375 391.81640625 C260.24130615 392.82131104 259.61151855 393.82621582 258.96264648 394.86157227 C257 397 257 397 254.55784607 397.3649292 C252 397 252 397 250.43936157 395.46855164 C250.00127167 394.75278458 249.56318176 394.03701752 249.11181641 393.29956055 C248.60661469 392.49629837 248.10141296 391.69303619 247.5809021 390.86543274 C247.05147003 389.98558212 246.52203796 389.10573151 245.9765625 388.19921875 C244.81884975 386.34633241 243.66030081 384.49396831 242.50097656 382.64208984 C241.91658813 381.69622009 241.33219971 380.75035034 240.73010254 379.77581787 C238.15263697 375.6404705 235.46629193 371.58029076 232.7734375 367.51953125 C231.72553934 365.92993275 230.67801875 364.34008528 229.63085938 362.75 C228.54557801 361.1041633 227.46029141 359.45833007 226.375 357.8125 C225.20371001 356.03519977 224.03248774 354.2578549 222.86132812 352.48046875 C220.44133946 348.80793487 218.02106714 345.13558818 215.60058594 341.46337891 C210.45583874 333.65758543 205.31468042 325.84942899 200.17340088 318.04135132 C197.36576183 313.77746136 194.55786745 309.51373955 191.75 305.25 C190.62499879 303.54166746 189.49999879 301.83333413 188.375 300.125 C187.818125 299.279375 187.26125 298.43375 186.6875 297.5625 C181.62499997 289.87499995 181.62499997 289.87499995 179.93762207 287.31268311 C178.81221794 285.6037376 177.68681104 283.89479391 176.56140137 282.18585205 C173.75506191 277.92439992 170.94877005 273.66291647 168.14257812 269.40136719 C163.00738565 261.60304695 157.87180924 253.80498071 152.73449707 246.00805664 C150.38571799 242.44323839 148.03721141 238.87824066 145.6887207 235.31323242 C144.60145193 233.66291655 143.51405939 232.01268221 142.42651367 230.36254883 C132.25421049 214.9279819 122.13043857 199.46224835 112 184 C111.67 292.24 111.34 400.48 111 512 C74.37 512 37.74 512 0 512 C0 343.04 0 174.08 0 0 Z " fill="#000000" transform="translate(0,0)"/>
|
||||||
|
<path d="M0 0 C40.59 0 81.18 0 123 0 C123 168.96 123 337.92 123 512 C86.04 512 49.08 512 11 512 C10.67 402.77 10.34 293.54 10 181 C4.72 189.25 -0.56 197.5 -6 206 C-9.39444207 211.15548467 -12.78716673 216.30779963 -16.22192383 221.43579102 C-17.599304 223.49245466 -18.97271714 225.55172895 -20.34570312 227.61132812 C-22.50073142 230.84064053 -24.66547146 234.06319701 -26.83917236 237.27996826 C-27.94230355 238.91450952 -29.03943552 240.55309486 -30.13623047 242.19189453 C-30.76029785 243.1095459 -31.38436523 244.02719727 -32.02734375 244.97265625 C-32.559646 245.760354 -33.09194824 246.54805176 -33.64038086 247.35961914 C-34.08905518 247.90094482 -34.53772949 248.44227051 -35 249 C-35.66 249 -36.32 249 -37 249 C-38.35009766 247.20947266 -38.35009766 247.20947266 -39.9140625 244.6640625 C-40.501875 243.7153125 -41.0896875 242.7665625 -41.6953125 241.7890625 C-42.33210938 240.74492187 -42.96890625 239.70078125 -43.625 238.625 C-44.29015625 237.54734375 -44.9553125 236.4696875 -45.640625 235.359375 C-47.41266874 232.48561118 -49.17847981 229.6082147 -50.94104004 226.7286377 C-52.63090725 223.9701121 -54.32738918 221.21566567 -56.0234375 218.4609375 C-58.01919588 215.21887138 -60.01443826 211.97649876 -62.00732422 208.73266602 C-66.55995548 201.32481658 -71.14231268 193.93622184 -75.75 186.5625 C-76.66652344 185.09151855 -76.66652344 185.09151855 -77.6015625 183.59082031 C-80.57044721 178.83646363 -83.57016988 174.10746656 -86.640625 169.41796875 C-87.20813477 168.54390381 -87.77564453 167.66983887 -88.36035156 166.76928711 C-89.42453538 165.1341899 -90.49885882 163.50563072 -91.58496094 161.88500977 C-92.05192383 161.16498779 -92.51888672 160.44496582 -93 159.703125 C-93.4125 159.08083008 -93.825 158.45853516 -94.25 157.81738281 C-95.23752366 155.42443811 -94.91065312 154.39823636 -94 152 C-93.28247855 150.60752627 -92.51012761 149.24258512 -91.69921875 147.90234375 C-90.98511841 146.71560059 -90.98511841 146.71560059 -90.2565918 145.50488281 C-89.7387915 144.65764648 -89.22099121 143.81041016 -88.6875 142.9375 C-88.14843018 142.04772461 -87.60936035 141.15794922 -87.05395508 140.24121094 C-82.51613674 132.77957799 -77.86646672 125.39067843 -73.18359375 118.01953125 C-63.57216547 102.88747676 -54.1487842 87.63834556 -44.77050781 72.36108398 C-40.88941569 66.04649596 -36.97338141 59.75688711 -33 53.5 C-21.76980025 35.81118073 -10.92512644 17.87747962 0 0 Z " fill="#000000" transform="translate(389,0)"/>
|
||||||
|
<path d="M0 0 C4.62276577 4.14454862 7.47322448 8.77374995 10.5625 14.125 C11.62963614 15.93860414 12.69869312 17.75107923 13.76953125 19.5625 C14.30916504 20.47644531 14.84879883 21.39039063 15.40478516 22.33203125 C17.78240918 26.30856558 20.25885329 30.21891917 22.75 34.125 C28.22964988 42.73406002 33.58176135 51.41803925 38.89916992 60.12817383 C42.24750462 65.59578939 45.64232668 71.01967842 49.16796875 76.375 C49.82361816 77.37684326 50.47926758 78.37868652 51.15478516 79.41088867 C52.39829787 81.30225655 53.6539539 83.18571316 54.92333984 85.05981445 C55.47586426 85.9024585 56.02838867 86.74510254 56.59765625 87.61328125 C57.08758057 88.34329346 57.57750488 89.07330566 58.08227539 89.82543945 C59.28133227 92.66662052 58.92600786 94.09059312 58 97 C57.01611328 99.00463867 57.01611328 99.00463867 55.8203125 100.91796875 C55.38589844 101.61470703 54.95148438 102.31144531 54.50390625 103.02929688 C54.04886719 103.74150391 53.59382813 104.45371094 53.125 105.1875 C52.67769531 105.91130859 52.23039062 106.63511719 51.76953125 107.38085938 C49.65391647 110.73526333 48.36691842 112.75538772 45 115 C44.37508667 113.98353394 44.37508667 113.98353394 43.73754883 112.9465332 C27.17658577 86.00544711 27.17658577 86.00544711 10.46337891 59.15869141 C2.53411428 46.50404945 -5.26881404 33.77655539 -13 21 C-4.738041 6.21867882 -4.738041 6.21867882 0 0 Z " fill="#000000" transform="translate(281,175)"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 7.9 KiB |
5
theme/assets/js/alpine.min.js
vendored
Normal file
5
theme/assets/js/alpine.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
theme/assets/js/html5-qrcode.min.js
vendored
Normal file
1
theme/assets/js/html5-qrcode.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
55
theme/assets/js/i18n.js
Normal file
55
theme/assets/js/i18n.js
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
class I18n {
|
||||||
|
constructor() {
|
||||||
|
this.currentLang = localStorage.getItem('mivo_lang') || 'en';
|
||||||
|
this.translations = {};
|
||||||
|
this.isLoaded = false;
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
await this.loadLanguage(this.currentLang);
|
||||||
|
this.isLoaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadLanguage(lang) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`assets/lang/${lang}.json`);
|
||||||
|
if (!response.ok) throw new Error(`Failed to load: ${lang}`);
|
||||||
|
|
||||||
|
this.translations = await response.json();
|
||||||
|
this.currentLang = lang;
|
||||||
|
localStorage.setItem('mivo_lang', lang);
|
||||||
|
this.applyTranslations();
|
||||||
|
|
||||||
|
document.documentElement.lang = lang;
|
||||||
|
window.dispatchEvent(new CustomEvent('language-changed', { detail: { lang } }));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('I18n Error:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
applyTranslations() {
|
||||||
|
document.querySelectorAll('[data-i18n]').forEach(element => {
|
||||||
|
const key = element.getAttribute('data-i18n');
|
||||||
|
const translation = this.getNestedValue(this.translations, key);
|
||||||
|
|
||||||
|
if (translation) {
|
||||||
|
if (element.tagName === 'INPUT' && element.getAttribute('placeholder')) {
|
||||||
|
element.placeholder = translation;
|
||||||
|
} else {
|
||||||
|
element.textContent = translation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getNestedValue(obj, path) {
|
||||||
|
return path.split('.').reduce((acc, part) => acc && acc[part], obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.i18n = new I18n();
|
||||||
|
|
||||||
|
function changeLanguage(lang) {
|
||||||
|
window.i18n.loadLanguage(lang);
|
||||||
|
}
|
||||||
88
theme/assets/js/main.js
Normal file
88
theme/assets/js/main.js
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
/**
|
||||||
|
* MIVO Theme Configuration & Main Utilities
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 1. Configuration
|
||||||
|
window.MivoConfig = {
|
||||||
|
// API Configuration
|
||||||
|
// Example: "http://192.168.1.1/mivo/public"
|
||||||
|
apiBaseUrl: "",
|
||||||
|
|
||||||
|
// Your Mivo Session Name
|
||||||
|
apiSession: "router-jakarta-1",
|
||||||
|
|
||||||
|
// Set to true to force Check Voucher tab even if apiBaseUrl is empty (for dev/test)
|
||||||
|
debugMode: false
|
||||||
|
};
|
||||||
|
|
||||||
|
// 2. Global Utilities
|
||||||
|
window.formatTime = function(str) {
|
||||||
|
if (!str || str === '-') return '-';
|
||||||
|
|
||||||
|
// Normalize string: specific fix for "7 h 12 m 48 s" (remove spaces between value and unit)
|
||||||
|
// Converts "7 h 12 m" -> "7h12m" for easier parsing, while keeping standard "1w2d" intact.
|
||||||
|
const normalized = str.toLowerCase().replace(/\s+/g, '');
|
||||||
|
|
||||||
|
// Regex to parse MikroTik time format (e.g. 1w6d20h56m25s)
|
||||||
|
const regex = /(\d+)([wdhms])/g;
|
||||||
|
let match;
|
||||||
|
const parts = [];
|
||||||
|
|
||||||
|
while ((match = regex.exec(normalized)) !== null) {
|
||||||
|
const val = match[1];
|
||||||
|
const unit = match[2];
|
||||||
|
// Use i18n to get localized unit name. Fallback to code if not found.
|
||||||
|
const unitName = (window.i18n && window.i18n.translations?.time?.[unit]) || unit;
|
||||||
|
parts.push(`${val} ${unitName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.length > 0 ? parts.join(' ') : str;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper to parse time string into total seconds for Live Timer
|
||||||
|
window.parseTimeSeconds = function(str) {
|
||||||
|
if (!str || str === '-') return 0;
|
||||||
|
const normalized = str.toLowerCase().replace(/\s+/g, '');
|
||||||
|
const regex = /(\d+)([wdhms])/g;
|
||||||
|
let match;
|
||||||
|
let totalSeconds = 0;
|
||||||
|
|
||||||
|
while ((match = regex.exec(normalized)) !== null) {
|
||||||
|
const val = parseInt(match[1]);
|
||||||
|
const unit = match[2];
|
||||||
|
|
||||||
|
switch(unit) {
|
||||||
|
case 'w': totalSeconds += val * 604800; break;
|
||||||
|
case 'd': totalSeconds += val * 86400; break;
|
||||||
|
case 'h': totalSeconds += val * 3600; break;
|
||||||
|
case 'm': totalSeconds += val * 60; break;
|
||||||
|
case 's': totalSeconds += val; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return totalSeconds;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper to format seconds back to string (e.g. 70s -> 1m 10s)
|
||||||
|
window.formatSeconds = function(seconds) {
|
||||||
|
if (seconds <= 0) return '0s';
|
||||||
|
|
||||||
|
const w = Math.floor(seconds / 604800);
|
||||||
|
seconds %= 604800;
|
||||||
|
const d = Math.floor(seconds / 86400);
|
||||||
|
seconds %= 86400;
|
||||||
|
const h = Math.floor(seconds / 3600);
|
||||||
|
seconds %= 3600;
|
||||||
|
const m = Math.floor(seconds / 60);
|
||||||
|
const s = seconds % 60;
|
||||||
|
|
||||||
|
const parts = [];
|
||||||
|
const t = window.i18n?.translations?.time || {};
|
||||||
|
|
||||||
|
if (w > 0) parts.push(`${w} ${t.w || 'w'}`);
|
||||||
|
if (d > 0) parts.push(`${d} ${t.d || 'd'}`);
|
||||||
|
if (h > 0) parts.push(`${h} ${t.h || 'h'}`);
|
||||||
|
if (m > 0) parts.push(`${m} ${t.m || 'm'}`);
|
||||||
|
if (s > 0) parts.push(`${s} ${t.s || 's'}`);
|
||||||
|
|
||||||
|
return parts.join(' ');
|
||||||
|
};
|
||||||
62
theme/assets/js/md5.js
Normal file
62
theme/assets/js/md5.js
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
/*
|
||||||
|
* A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
|
||||||
|
* Digest Algorithm, as defined in RFC 1321.
|
||||||
|
* Version 2.1 Copyright (C) Paul Johnston 1999 - 2002.
|
||||||
|
* Other contributors: Greg Holt, Andrew Kepert, Yoni Elere, David Ivanees
|
||||||
|
*/
|
||||||
|
var hexcase = 0; var b64pad = ""; var chrsz = 8;
|
||||||
|
function hexMD5(s){ return binl2hex(core_md5(str2binl(s), s.length * chrsz));}
|
||||||
|
function b64_md5(s){ return binl2b64(core_md5(str2binl(s), s.length * chrsz));}
|
||||||
|
function str_md5(s){ return binl2str(core_md5(str2binl(s), s.length * chrsz));}
|
||||||
|
function hex_hmac_md5(key, data) { return binl2hex(core_hmac_md5(key, data)); }
|
||||||
|
function b64_hmac_md5(key, data) { return binl2b64(core_hmac_md5(key, data)); }
|
||||||
|
function str_hmac_md5(key, data) { return binl2str(core_hmac_md5(key, data)); }
|
||||||
|
function md5_vm_test() { return hexMD5("abc") == "900150983cd24fb0d6963f7d28e17f72"; }
|
||||||
|
function core_md5(x, len)
|
||||||
|
{
|
||||||
|
x[len >> 5] |= 0x80 << ((len) % 32);
|
||||||
|
x[(((len + 64) >>> 9) << 4) + 14] = len;
|
||||||
|
var a = 1732584193; var b = -271733879; var c = -1732584194; var d = 271733878;
|
||||||
|
for(var i = 0; i < x.length; i += 16)
|
||||||
|
{
|
||||||
|
var olda = a; var oldb = b; var oldc = c; var oldd = d;
|
||||||
|
a = md5_ff(a, b, c, d, x[i+ 0], 7 , -680876936); d = md5_ff(d, a, b, c, x[i+ 1], 12, -389564586); c = md5_ff(c, d, a, b, x[i+ 2], 17, 606105819); b = md5_ff(b, c, d, a, x[i+ 3], 22, -1044525330);
|
||||||
|
a = md5_ff(a, b, c, d, x[i+ 4], 7 , -176418897); d = md5_ff(d, a, b, c, x[i+ 5], 12, 1200080426); c = md5_ff(c, d, a, b, x[i+ 6], 17, -1473231341); b = md5_ff(b, c, d, a, x[i+ 7], 22, -45705983);
|
||||||
|
a = md5_ff(a, b, c, d, x[i+ 8], 7 , 1770035416); d = md5_ff(d, a, b, c, x[i+ 9], 12, -1958414417); c = md5_ff(c, d, a, b, x[i+10], 17, -42063); b = md5_ff(b, c, d, a, x[i+11], 22, -1990404162);
|
||||||
|
a = md5_ff(a, b, c, d, x[i+12], 7 , 1804603682); d = md5_ff(d, a, b, c, x[i+13], 12, -40341101); c = md5_ff(c, d, a, b, x[i+14], 17, -1502002290); b = md5_ff(b, c, d, a, x[i+15], 22, 1236535329);
|
||||||
|
a = md5_gg(a, b, c, d, x[i+ 1], 5 , -165796510); d = md5_gg(d, a, b, c, x[i+ 6], 9 , -1069501632); c = md5_gg(c, d, a, b, x[i+11], 14, 643717713); b = md5_gg(b, c, d, a, x[i+ 0], 20, -373897302);
|
||||||
|
a = md5_gg(a, b, c, d, x[i+ 5], 5 , -701558691); d = md5_gg(d, a, b, c, x[i+10], 9 , 38016083); c = md5_gg(c, d, a, b, x[i+15], 14, -660478335); b = md5_gg(b, c, d, a, x[i+ 4], 20, -405537848);
|
||||||
|
a = md5_gg(a, b, c, d, x[i+ 9], 5 , 568446438); d = md5_gg(d, a, b, c, x[i+14], 9 , -1019803690); c = md5_gg(c, d, a, b, x[i+ 3], 14, -187363961); b = md5_gg(b, c, d, a, x[i+ 8], 20, 1163531501);
|
||||||
|
a = md5_gg(a, b, c, d, x[i+13], 5 , -1444681467); d = md5_gg(d, a, b, c, x[i+ 2], 9 , -51403784); c = md5_gg(c, d, a, b, x[i+ 7], 14, 1735328473); b = md5_gg(b, c, d, a, x[i+12], 20, -1926607734);
|
||||||
|
a = md5_hh(a, b, c, d, x[i+ 5], 4 , -378558); d = md5_hh(d, a, b, c, x[i+ 8], 11, -2022574463); c = md5_hh(c, d, a, b, x[i+11], 16, 1839030562); b = md5_hh(b, c, d, a, x[i+14], 23, -35309556);
|
||||||
|
a = md5_hh(a, b, c, d, x[i+ 1], 4 , -1530992060); d = md5_hh(d, a, b, c, x[i+ 4], 11, 1272893353); c = md5_hh(c, d, a, b, x[i+ 7], 16, -155497632); b = md5_hh(b, c, d, a, x[i+10], 23, -1094730640);
|
||||||
|
a = md5_hh(a, b, c, d, x[i+13], 4 , 681279174); d = md5_hh(d, a, b, c, x[i+ 0], 11, -358537222); c = md5_hh(c, d, a, b, x[i+ 3], 16, -722521979); b = md5_hh(b, c, d, a, x[i+ 6], 23, 76029189);
|
||||||
|
a = md5_hh(a, b, c, d, x[i+ 9], 4 , -640364487); d = md5_hh(d, a, b, c, x[i+12], 11, -421815835); c = md5_hh(c, d, a, b, x[i+15], 16, 530742520); b = md5_hh(b, c, d, a, x[i+ 2], 23, -995338651);
|
||||||
|
a = md5_ii(a, b, c, d, x[i+ 0], 6 , -198630844); d = md5_ii(d, a, b, c, x[i+ 7], 10, 1126891415); c = md5_ii(c, d, a, b, x[i+14], 15, -1416354905); b = md5_ii(b, c, d, a, x[i+ 5], 21, -57434055);
|
||||||
|
a = md5_ii(a, b, c, d, x[i+12], 6 , 1700485571); d = md5_ii(d, a, b, c, x[i+ 3], 10, -1894986606); c = md5_ii(c, d, a, b, x[i+10], 15, -1051523); b = md5_ii(b, c, d, a, x[i+ 1], 21, -2054922799);
|
||||||
|
a = md5_ii(a, b, c, d, x[i+ 8], 6 , 1873313359); d = md5_ii(d, a, b, c, x[i+15], 10, -30611744); c = md5_ii(c, d, a, b, x[i+ 6], 15, -1560198380); b = md5_ii(b, c, d, a, x[i+13], 21, 1309151649);
|
||||||
|
a = md5_ii(a, b, c, d, x[i+ 4], 6 , -145523070); d = md5_ii(d, a, b, c, x[i+11], 10, -1120210379); c = md5_ii(c, d, a, b, x[i+ 2], 15, 718787259); b = md5_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 Array(a, b, c, d);
|
||||||
|
}
|
||||||
|
function md5_cmn(q, a, b, x, s, t) { return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s),b); }
|
||||||
|
function md5_ff(a, b, c, d, x, s, t) { return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t); }
|
||||||
|
function md5_gg(a, b, c, d, x, s, t) { return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t); }
|
||||||
|
function md5_hh(a, b, c, d, x, s, t) { return md5_cmn(b ^ c ^ d, a, b, x, s, t); }
|
||||||
|
function md5_ii(a, b, c, d, x, s, t) { return md5_cmn(c ^ (b | (~d)), a, b, x, s, t); }
|
||||||
|
function core_hmac_md5(key, data)
|
||||||
|
{
|
||||||
|
var bkey = str2binl(key);
|
||||||
|
if(bkey.length > 16) bkey = core_md5(bkey, key.length * chrsz);
|
||||||
|
var ipad = Array(16), opad = Array(16);
|
||||||
|
for(var i = 0; i < 16; i++) { ipad[i] = bkey[i] ^ 0x36363636; opad[i] = bkey[i] ^ 0x5C5C5C5C; }
|
||||||
|
var hash = core_md5(ipad.concat(str2binl(data)), (16 * 32) + data.length * chrsz);
|
||||||
|
return core_md5(opad.concat(hash), (16 * 32) + 512);
|
||||||
|
}
|
||||||
|
function safe_add(x, y) { var lsw = (x & 0xFFFF) + (y & 0xFFFF); var msw = (x >> 16) + (y >> 16) + (lsw >> 16); return (msw << 16) | (lsw & 0xFFFF); }
|
||||||
|
function bit_rol(num, cnt) { return (num << cnt) | (num >>> (32 - cnt)); }
|
||||||
|
function str2binl(str) { var bin = Array(); var mask = (1 << chrsz) - 1; for(var i = 0; i < str.length * chrsz; i += chrsz) bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (i%32); return bin; }
|
||||||
|
function binl2str(bin) { var str = ""; var mask = (1 << chrsz) - 1; for(var i = 0; i < bin.length * 32; i += chrsz) str += String.fromCharCode((bin[i>>5] >>> (i % 32)) & mask); return str; }
|
||||||
|
function binl2hex(binarray) { var hex_tab = hexcase ? "0123456789ABCDEF" : "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; }
|
||||||
|
function binl2b64(binarray) { var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; var str = ""; for(var i = 0; i < binarray.length * 4; i += 3) { var triplet = (((binarray[i >> 2] >> 8 * ( i % 4)) & 0xFF) << 16) | (((binarray[i+1 >> 2] >> 8 * ((i+1) % 4)) & 0xFF) << 8 ) | ((binarray[i+2 >> 2] >> 8 * ((i+2) % 4)) & 0xFF); for(var j = 0; j < 4; j++) { if(i * 8 + j * 6 > binarray.length * 32) str += b64pad; else str += tab.charAt((triplet >> 6*(3-j)) & 0x3F); } } return str; }
|
||||||
164
theme/assets/js/qr.js
Normal file
164
theme/assets/js/qr.js
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
function qrMixin() {
|
||||||
|
return {
|
||||||
|
showQr: false,
|
||||||
|
qrScanner: null,
|
||||||
|
qrType: 'login', // 'login' or 'check'
|
||||||
|
scanTarget: 'voucher', // 'voucher', 'member', 'check'
|
||||||
|
qrResult: null,
|
||||||
|
qrError: '',
|
||||||
|
facingMode: 'environment', // 'environment' or 'user'
|
||||||
|
|
||||||
|
initQr(target) {
|
||||||
|
this.scanTarget = target;
|
||||||
|
this.qrType = target === 'check' ? 'check' : 'login';
|
||||||
|
this.showQr = true;
|
||||||
|
this.qrResult = null;
|
||||||
|
this.qrError = '';
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.startCamera();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async startCamera() {
|
||||||
|
if (this.qrScanner) {
|
||||||
|
await this.stopCamera();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create instance (using Html5Qrcode directly, NOT Scanner widget)
|
||||||
|
this.qrScanner = new Html5Qrcode("reader");
|
||||||
|
|
||||||
|
const config = { fps: 10, qrbox: { width: 250, height: 250 } };
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.qrScanner.start(
|
||||||
|
{ facingMode: this.facingMode },
|
||||||
|
config,
|
||||||
|
this.onScanSuccess.bind(this),
|
||||||
|
this.onScanFailure.bind(this)
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error starting scanner", err);
|
||||||
|
this.qrError = "Camera error: " + (err.message || err);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async stopCamera() {
|
||||||
|
if (this.qrScanner) {
|
||||||
|
try {
|
||||||
|
if(this.qrScanner.isScanning) {
|
||||||
|
await this.qrScanner.stop();
|
||||||
|
}
|
||||||
|
this.qrScanner.clear();
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Error stopping scanner", e);
|
||||||
|
}
|
||||||
|
this.qrScanner = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async switchCamera() {
|
||||||
|
this.facingMode = this.facingMode === 'environment' ? 'user' : 'environment';
|
||||||
|
await this.startCamera();
|
||||||
|
},
|
||||||
|
|
||||||
|
async scanFile(event) {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
// Stop camera temporarily if running
|
||||||
|
await this.stopCamera();
|
||||||
|
|
||||||
|
// Create temp instance for file scan
|
||||||
|
const fileScanner = new Html5Qrcode("reader");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const decodedText = await fileScanner.scanFile(file, true);
|
||||||
|
this.onScanSuccess(decodedText, null);
|
||||||
|
} catch (err) {
|
||||||
|
this.qrError = "File scan failed: " + (err.message || "No QR found");
|
||||||
|
// Resume camera if failed
|
||||||
|
await this.startCamera();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear file input
|
||||||
|
event.target.value = '';
|
||||||
|
},
|
||||||
|
|
||||||
|
onScanSuccess(decodedText, decodedResult) {
|
||||||
|
this.qrError = '';
|
||||||
|
try {
|
||||||
|
// Strict Validation
|
||||||
|
const url = new URL(decodedText);
|
||||||
|
const currentHost = window.location.hostname;
|
||||||
|
const qrHost = url.hostname;
|
||||||
|
|
||||||
|
// 1. Hostname Check (Strict)
|
||||||
|
// Skip check for file uploads if they might come from anywhere?
|
||||||
|
// No, adhere to strict security even for files.
|
||||||
|
if (qrHost !== currentHost && currentHost !== '127.0.0.1' && currentHost !== 'localhost') {
|
||||||
|
// Check allowed domains if implemented, otherwise strictly block
|
||||||
|
throw new Error('Invalid Hostname. QR is for: ' + qrHost);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Parse Data
|
||||||
|
const params = new URLSearchParams(url.search);
|
||||||
|
|
||||||
|
if (this.qrType === 'login') {
|
||||||
|
const u = params.get('user') || params.get('username');
|
||||||
|
const p = params.get('password');
|
||||||
|
|
||||||
|
if (!u || !p) throw new Error('Invalid Login QR. Missing username/password.');
|
||||||
|
|
||||||
|
this.qrResult = { type: 'login', username: u, password: p, display: `User: ${u}` };
|
||||||
|
this.stopCamera();
|
||||||
|
} else if (this.qrType === 'check') {
|
||||||
|
const c = params.get('code') || params.get('user') || params.get('username');
|
||||||
|
|
||||||
|
if (!c) throw new Error('Invalid Check QR. Missing voucher code.');
|
||||||
|
|
||||||
|
// Directly verify without confirmation step
|
||||||
|
this.stopCamera();
|
||||||
|
this.checkCode = c;
|
||||||
|
this.loginType = 'check';
|
||||||
|
this.closeQr();
|
||||||
|
this.checkVoucher();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(e);
|
||||||
|
this.qrError = 'Security Error: ' + e.message;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onScanFailure(error) {
|
||||||
|
// Ignore frame read errors
|
||||||
|
},
|
||||||
|
|
||||||
|
confirmQr() {
|
||||||
|
if (!this.qrResult) return;
|
||||||
|
|
||||||
|
if (this.scanTarget === 'voucher') {
|
||||||
|
this.auth.voucher = this.qrResult.username;
|
||||||
|
this.loginType = 'voucher';
|
||||||
|
this.submit();
|
||||||
|
} else if (this.scanTarget === 'member') {
|
||||||
|
this.auth.username = this.qrResult.username;
|
||||||
|
this.auth.password = this.qrResult.password;
|
||||||
|
this.loginType = 'member';
|
||||||
|
this.submit();
|
||||||
|
} else if (this.scanTarget === 'check') {
|
||||||
|
this.checkCode = this.qrResult.code;
|
||||||
|
this.loginType = 'check'; // Switch tab
|
||||||
|
this.checkVoucher();
|
||||||
|
}
|
||||||
|
this.closeQr();
|
||||||
|
},
|
||||||
|
|
||||||
|
closeQr() {
|
||||||
|
this.showQr = false;
|
||||||
|
this.stopCamera();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
24
theme/assets/js/theme.js
Normal file
24
theme/assets/js/theme.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
function initTheme() {
|
||||||
|
return {
|
||||||
|
theme: localStorage.getItem('mivo_theme') || 'dark',
|
||||||
|
toggle() {
|
||||||
|
this.theme = this.theme === 'dark' ? 'light' : 'dark';
|
||||||
|
this.apply();
|
||||||
|
},
|
||||||
|
setTheme(val) {
|
||||||
|
this.theme = val;
|
||||||
|
this.apply();
|
||||||
|
},
|
||||||
|
apply() {
|
||||||
|
localStorage.setItem('mivo_theme', this.theme);
|
||||||
|
if (this.theme === 'dark') {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('dark');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
init() {
|
||||||
|
this.apply();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
61
theme/assets/lang/en.json
Normal file
61
theme/assets/lang/en.json
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
{
|
||||||
|
"hotspot": {
|
||||||
|
"title": "Mivo Hotspot",
|
||||||
|
"subtitle": "Please login to access the internet",
|
||||||
|
"voucher": "Voucher",
|
||||||
|
"member": "Member",
|
||||||
|
"voucher_code": "Voucher Code",
|
||||||
|
"username": "Username",
|
||||||
|
"password": "Password",
|
||||||
|
"connect": "Connect",
|
||||||
|
"connecting": "Connecting...",
|
||||||
|
"check": "Check",
|
||||||
|
"checking": "Checking...",
|
||||||
|
"powered_by": "Powered by Mivo Theme"
|
||||||
|
},
|
||||||
|
"check": {
|
||||||
|
"title": "Check Voucher",
|
||||||
|
"subtitle": "Check your remaining quota",
|
||||||
|
"input_placeholder": "Enter voucher code...",
|
||||||
|
"not_found": "Voucher not found or not active.",
|
||||||
|
"quota_left": "Quota Left",
|
||||||
|
"time_left": "Time Left",
|
||||||
|
"uptime": "Uptime",
|
||||||
|
"validity": "Validity",
|
||||||
|
"expiration": "Expires",
|
||||||
|
"status": "Status"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"connected": "Connected",
|
||||||
|
"active_session": "Active Session",
|
||||||
|
"user": "User",
|
||||||
|
"ip_address": "IP Address",
|
||||||
|
"mac_address": "MAC Address",
|
||||||
|
"download": "Download",
|
||||||
|
"upload": "Upload",
|
||||||
|
"uptime": "Session Uptime",
|
||||||
|
"remaining": "Time Remaining",
|
||||||
|
"disconnect": "Disconnect",
|
||||||
|
"refresh": "Refresh Status",
|
||||||
|
"footer": "MIVO THEME • PREMIUM HOTSPOT"
|
||||||
|
},
|
||||||
|
"logout": {
|
||||||
|
"title": "Logged Out",
|
||||||
|
"subtitle": "You have been successfully disconnected from the hotspot.",
|
||||||
|
"uptime": "Total Uptime",
|
||||||
|
"data": "Data Used",
|
||||||
|
"login_again": "Login Again",
|
||||||
|
"footer": "MIVO THEME • SEE YOU AGAIN"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"title": "System Error",
|
||||||
|
"back": "Back to Login"
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"w": "Week",
|
||||||
|
"d": "Day",
|
||||||
|
"h": "Hour",
|
||||||
|
"m": "Minute",
|
||||||
|
"s": "Second"
|
||||||
|
}
|
||||||
|
}
|
||||||
61
theme/assets/lang/id.json
Normal file
61
theme/assets/lang/id.json
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
{
|
||||||
|
"hotspot": {
|
||||||
|
"title": "Mivo Hotspot",
|
||||||
|
"subtitle": "Silakan login untuk mengakses internet",
|
||||||
|
"voucher": "Voucher",
|
||||||
|
"member": "Member",
|
||||||
|
"voucher_code": "Kode Voucher",
|
||||||
|
"username": "Username",
|
||||||
|
"password": "Password",
|
||||||
|
"connect": "Hubungkan",
|
||||||
|
"connecting": "Menghubungkan...",
|
||||||
|
"check": "Cek",
|
||||||
|
"checking": "Mengecek...",
|
||||||
|
"powered_by": "Didukung oleh Tema Mivo"
|
||||||
|
},
|
||||||
|
"check": {
|
||||||
|
"title": "Cek Voucher",
|
||||||
|
"subtitle": "Cek sisa kuota Anda",
|
||||||
|
"input_placeholder": "Masukkan kode voucher...",
|
||||||
|
"not_found": "Voucher tidak ditemukan atau belum aktif.",
|
||||||
|
"quota_left": "Sisa Kuota",
|
||||||
|
"time_left": "Sisa Waktu",
|
||||||
|
"uptime": "Durasi Pakai",
|
||||||
|
"validity": "Masa Aktif",
|
||||||
|
"expiration": "Kedaluwarsa",
|
||||||
|
"status": "Status"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"connected": "Terhubung",
|
||||||
|
"active_session": "Sesi Aktif",
|
||||||
|
"user": "Pengguna",
|
||||||
|
"ip_address": "Alamat IP",
|
||||||
|
"mac_address": "Alamat MAC",
|
||||||
|
"download": "Unduh",
|
||||||
|
"upload": "Unggah",
|
||||||
|
"uptime": "Waktu Aktif",
|
||||||
|
"remaining": "Sisa Waktu",
|
||||||
|
"disconnect": "Putuskan Koneksi",
|
||||||
|
"refresh": "Perbarui Status",
|
||||||
|
"footer": "MIVO THEME • PREMIUM HOTSPOT"
|
||||||
|
},
|
||||||
|
"logout": {
|
||||||
|
"title": "Keluar",
|
||||||
|
"subtitle": "Anda telah berhasil diputuskan dari hotspot.",
|
||||||
|
"uptime": "Total Waktu",
|
||||||
|
"data": "Data Digunakan",
|
||||||
|
"login_again": "Login Lagi",
|
||||||
|
"footer": "MIVO THEME • SAMPAI JUMPA LAGI"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"title": "Error Sistem",
|
||||||
|
"back": "Kembali ke Login"
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"w": "Minggu",
|
||||||
|
"d": "Hari",
|
||||||
|
"h": "Jam",
|
||||||
|
"m": "Menit",
|
||||||
|
"s": "Detik"
|
||||||
|
}
|
||||||
|
}
|
||||||
4
theme/assets/svg/id.svg
Normal file
4
theme/assets/svg/id.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-id" viewBox="0 0 640 480">
|
||||||
|
<path fill="#e70011" d="M0 0h640v240H0Z"/>
|
||||||
|
<path fill="#fff" d="M0 240h640v240H0Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 178 B |
9
theme/assets/svg/us.svg
Normal file
9
theme/assets/svg/us.svg
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-us" viewBox="0 0 640 480">
|
||||||
|
<path fill="#bd3d44" d="M0 0h640v480H0"/>
|
||||||
|
<path stroke="#fff" stroke-width="37" d="M0 55.3h640M0 129h640M0 203h640M0 277h640M0 351h640M0 425h640"/>
|
||||||
|
<path fill="#192f5d" d="M0 0h364.8v258.5H0"/>
|
||||||
|
<marker id="us-a" markerHeight="30" markerWidth="30">
|
||||||
|
<path fill="#fff" d="m14 0 9 27L0 10h28L5 27z"/>
|
||||||
|
</marker>
|
||||||
|
<path fill="none" marker-mid="url(#us-a)" d="m0 0 16 11h61 61 61 61 60L47 37h61 61 60 61L16 63h61 61 61 61 60L47 89h61 61 60 61L16 115h61 61 61 61 60L47 141h61 61 60 61L16 166h61 61 61 61 60L47 192h61 61 60 61L16 218h61 61 61 61 60z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 648 B |
91
theme/error.html
Normal file
91
theme/error.html
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="pragma" content="no-cache">
|
||||||
|
<meta http-equiv="expires" content="-1">
|
||||||
|
<link rel="icon" href="favicon.ico">
|
||||||
|
<link rel="stylesheet" href="assets/css/styles.css">
|
||||||
|
<script defer="" src="assets/js/alpine.min.js"></script>
|
||||||
|
<script src="assets/js/main.js"></script>
|
||||||
|
<script src="assets/js/theme.js"></script>
|
||||||
|
<script src="assets/js/i18n.js"></script>
|
||||||
|
|
||||||
|
<title>Mivo Hotspot - Error</title>
|
||||||
|
</head>
|
||||||
|
<body class="min-h-screen flex flex-col items-center justify-start p-4 pt-36 bg-background transition-colors duration-500" x-data="initTheme()">
|
||||||
|
|
||||||
|
<div class="fixed inset-0 z-0 pointer-events-none">
|
||||||
|
<!-- Subtle Grid Pattern -->
|
||||||
|
<div class="absolute inset-0 bg-[url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGNpcmNsZSBjeD0iMSIgY3k9IjEiIHI9IjEiIGZpbGw9InJnYmEoMCwwLDAsMC4wNSkiLz48L3N2Zz4=')] dark:bg-[url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGNpcmNsZSBjeD0iMSIgY3k9IjEiIHI9IjEiIGZpbGw9InJnYmEoMjU1LDI1NSwyNTUsMC4wNSkiLz48L3N2Zz4=')] [mask-image:linear-gradient(to_bottom,white,transparent)]"></div>
|
||||||
|
<!-- glowing blobs -->
|
||||||
|
<div class="absolute -top-[20%] -left-[10%] w-[70vw] h-[70vw] rounded-full bg-blue-500/10 dark:bg-blue-500/5 blur-[120px] animate-pulse" style="animation-duration: 4s;"></div>
|
||||||
|
<div class="absolute top-[30%] -right-[15%] w-[60vw] h-[60vw] rounded-full bg-purple-500/10 dark:bg-purple-500/5 blur-[100px] animate-pulse" style="animation-duration: 6s; animation-delay: 1s;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="nav-card transition-all duration-500 transform" x-data="{ showNav: true, lastScrollY: 0 }" @scroll.window="
|
||||||
|
showNav = window.scrollY < lastScrollY || window.scrollY < 50;
|
||||||
|
lastScrollY = window.scrollY;
|
||||||
|
" :class="showNav ? 'translate-y-0 opacity-100' : '-translate-y-24 opacity-0 pointer-events-none'">
|
||||||
|
<div class="theme-toggle-container">
|
||||||
|
<div class="theme-slider" :class="theme === 'dark' ? 'translate-x-[36px]' : 'translate-x-0'"></div>
|
||||||
|
<button @click="setTheme('light')" :class="theme === 'light' ? 'text-black' : 'text-slate-500 hover:text-slate-700'" class="theme-btn">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>
|
||||||
|
</button>
|
||||||
|
<button @click="setTheme('dark')" :class="theme === 'dark' ? 'text-white' : 'text-slate-300 hover:text-white'" class="theme-btn">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="h-6 w-px bg-black/5 dark:bg-white/10 mx-1"></div>
|
||||||
|
<div class="relative" x-data="{ open: false, lang: localStorage.getItem('mivo_lang') || 'en' }" @language-changed.window="lang = $event.detail.lang">
|
||||||
|
<button @click="open = !open" class="flex items-center gap-2 px-3 h-10 rounded-full bg-black/5 dark:bg-white/5 border border-black/5 dark:border-white/10 text-xs font-bold text-foreground hover:bg-black/10 dark:hover:bg-white/20 transition-all backdrop-blur-md min-w-[80px] justify-between shadow-sm">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<img :src="'assets/svg/' + (lang === 'id' ? 'id' : 'us') + '.svg'" class="w-4 h-3 rounded-sm object-cover" alt="Flag">
|
||||||
|
<span x-text="lang.toUpperCase()">EN</span>
|
||||||
|
</div>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" :class="open ? 'rotate-180' : ''" class="w-3.5 h-3.5 transition-transform text-accents-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"></path></svg>
|
||||||
|
</button>
|
||||||
|
<div x-show="open" @click.away="open = false" x-transition:enter="transition ease-out duration-100" x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100" style="display: none;" class="absolute right-0 mt-2 w-48 rounded-xl bg-white/90 dark:bg-black/80 border border-black/5 dark:border-white/10 backdrop-blur-xl shadow-xl overflow-hidden py-1 ring-1 ring-black/5">
|
||||||
|
<button @click="changeLanguage('id'); open = false" class="w-full text-left px-4 py-3 text-xs font-medium text-foreground hover:bg-black/5 dark:hover:bg-white/10 transition-colors flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<img src="assets/svg/id.svg" class="w-4 h-3 rounded-sm object-cover" alt="ID">
|
||||||
|
<span>Bahasa Indonesia</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-[10px] text-accents-5 font-bold">ID</span>
|
||||||
|
</button>
|
||||||
|
<button @click="changeLanguage('en'); open = false" class="w-full text-left px-4 py-3 text-xs font-medium text-foreground hover:bg-black/5 dark:hover:bg-white/10 transition-colors flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<img src="assets/svg/us.svg" class="w-4 h-3 rounded-sm object-cover" alt="US">
|
||||||
|
<span>English</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-[10px] text-accents-5 font-bold">EN</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="relative w-full max-w-sm">
|
||||||
|
<div class="card text-center">
|
||||||
|
<div class="flex justify-center mb-6">
|
||||||
|
<img :src="'assets/img/' + (theme === 'light' ? 'logo-m' : 'logo-m-dark') + '.svg'" alt="Mivo Logo" class="h-10 w-auto">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 class="text-xl font-bold tracking-tight text-foreground mb-4" data-i18n="error.title">System Error</h1>
|
||||||
|
|
||||||
|
<div class="p-4 rounded-xl bg-red-500/10 border border-red-500/20 text-red-400 text-sm mb-6 flex items-start gap-3 text-left">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 shrink-0 mt-0.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>
|
||||||
|
<div class="font-mono break-words w-full">$(error)</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="$(link-login)" class="w-full btn-secondary gap-2">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="12" x2="5" y2="12"></line><polyline points="12 19 5 12 12 5"></polyline></svg>
|
||||||
|
<span data-i18n="error.back">Back to Login</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
BIN
theme/favicon.ico
Normal file
BIN
theme/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 550 KiB |
395
theme/login.html
Normal file
395
theme/login.html
Normal file
@@ -0,0 +1,395 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="pragma" content="no-cache">
|
||||||
|
<meta http-equiv="expires" content="-1">
|
||||||
|
<link rel="icon" href="favicon.ico">
|
||||||
|
<link rel="stylesheet" href="assets/css/styles.css">
|
||||||
|
<script defer="" src="assets/js/alpine.min.js"></script>
|
||||||
|
<script src="assets/js/main.js"></script>
|
||||||
|
<script src="assets/js/theme.js"></script>
|
||||||
|
<script src="assets/js/i18n.js"></script>
|
||||||
|
|
||||||
|
<title>Mivo Hotspot - Login</title>
|
||||||
|
<script src="assets/js/md5.js"></script>
|
||||||
|
<script src="assets/js/html5-qrcode.min.js"></script>
|
||||||
|
</head>
|
||||||
|
<body class="min-h-screen flex flex-col items-center justify-start p-4 pt-36 bg-background transition-colors duration-500" x-data="initTheme()">
|
||||||
|
|
||||||
|
<div class="fixed inset-0 z-0 pointer-events-none">
|
||||||
|
<!-- Subtle Grid Pattern -->
|
||||||
|
<div class="absolute inset-0 bg-[url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGNpcmNsZSBjeD0iMSIgY3k9IjEiIHI9IjEiIGZpbGw9InJnYmEoMCwwLDAsMC4wNSkiLz48L3N2Zz4=')] dark:bg-[url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGNpcmNsZSBjeD0iMSIgY3k9IjEiIHI9IjEiIGZpbGw9InJnYmEoMjU1LDI1NSwyNTUsMC4wNSkiLz48L3N2Zz4=')] [mask-image:linear-gradient(to_bottom,white,transparent)]"></div>
|
||||||
|
<!-- glowing blobs -->
|
||||||
|
<div class="absolute -top-[20%] -left-[10%] w-[70vw] h-[70vw] rounded-full bg-blue-500/10 dark:bg-blue-500/5 blur-[120px] animate-pulse" style="animation-duration: 4s;"></div>
|
||||||
|
<div class="absolute top-[30%] -right-[15%] w-[60vw] h-[60vw] rounded-full bg-purple-500/10 dark:bg-purple-500/5 blur-[100px] animate-pulse" style="animation-duration: 6s; animation-delay: 1s;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="nav-card transition-all duration-500 transform" x-data="{ showNav: true, lastScrollY: 0 }" @scroll.window="
|
||||||
|
showNav = window.scrollY < lastScrollY || window.scrollY < 50;
|
||||||
|
lastScrollY = window.scrollY;
|
||||||
|
" :class="showNav ? 'translate-y-0 opacity-100' : '-translate-y-24 opacity-0 pointer-events-none'">
|
||||||
|
<div class="theme-toggle-container">
|
||||||
|
<div class="theme-slider" :class="theme === 'dark' ? 'translate-x-[36px]' : 'translate-x-0'"></div>
|
||||||
|
<button @click="setTheme('light')" :class="theme === 'light' ? 'text-black' : 'text-slate-500 hover:text-slate-700'" class="theme-btn">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>
|
||||||
|
</button>
|
||||||
|
<button @click="setTheme('dark')" :class="theme === 'dark' ? 'text-white' : 'text-slate-300 hover:text-white'" class="theme-btn">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="h-6 w-px bg-black/5 dark:bg-white/10 mx-1"></div>
|
||||||
|
<div class="relative" x-data="{ open: false, lang: localStorage.getItem('mivo_lang') || 'en' }" @language-changed.window="lang = $event.detail.lang">
|
||||||
|
<button @click="open = !open" class="flex items-center gap-2 px-3 h-10 rounded-full bg-black/5 dark:bg-white/5 border border-black/5 dark:border-white/10 text-xs font-bold text-foreground hover:bg-black/10 dark:hover:bg-white/20 transition-all backdrop-blur-md min-w-[80px] justify-between shadow-sm">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<img :src="'assets/svg/' + (lang === 'id' ? 'id' : 'us') + '.svg'" class="w-4 h-3 rounded-sm object-cover" alt="Flag">
|
||||||
|
<span x-text="lang.toUpperCase()">EN</span>
|
||||||
|
</div>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" :class="open ? 'rotate-180' : ''" class="w-3.5 h-3.5 transition-transform text-accents-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"></path></svg>
|
||||||
|
</button>
|
||||||
|
<div x-show="open" @click.away="open = false" x-transition:enter="transition ease-out duration-100" x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100" style="display: none;" class="absolute right-0 mt-2 w-48 rounded-xl bg-white/90 dark:bg-black/80 border border-black/5 dark:border-white/10 backdrop-blur-xl shadow-xl overflow-hidden py-1 ring-1 ring-black/5">
|
||||||
|
<button @click="changeLanguage('id'); open = false" class="w-full text-left px-4 py-3 text-xs font-medium text-foreground hover:bg-black/5 dark:hover:bg-white/10 transition-colors flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<img src="assets/svg/id.svg" class="w-4 h-3 rounded-sm object-cover" alt="ID">
|
||||||
|
<span>Bahasa Indonesia</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-[10px] text-accents-5 font-bold">ID</span>
|
||||||
|
</button>
|
||||||
|
<button @click="changeLanguage('en'); open = false" class="w-full text-left px-4 py-3 text-xs font-medium text-foreground hover:bg-black/5 dark:hover:bg-white/10 transition-colors flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<img src="assets/svg/us.svg" class="w-4 h-3 rounded-sm object-cover" alt="US">
|
||||||
|
<span>English</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-[10px] text-accents-5 font-bold">EN</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<script src="assets/js/qr.js"></script>
|
||||||
|
<script>
|
||||||
|
function loginData() {
|
||||||
|
return {
|
||||||
|
...qrMixin(),
|
||||||
|
loading: false,
|
||||||
|
checkLoading: false,
|
||||||
|
loginType: 'voucher', // 'voucher', 'member', 'check'
|
||||||
|
checkCode: '',
|
||||||
|
checkRes: null,
|
||||||
|
checkError: false,
|
||||||
|
showPass: false,
|
||||||
|
// API config
|
||||||
|
hasApi: window.MivoConfig?.apiBaseUrl !== '' || window.MivoConfig?.debugMode,
|
||||||
|
|
||||||
|
auth: {
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
voucher: ''
|
||||||
|
},
|
||||||
|
|
||||||
|
async checkVoucher() {
|
||||||
|
if (!this.checkCode) return;
|
||||||
|
this.checkLoading = true;
|
||||||
|
this.checkRes = null;
|
||||||
|
this.checkError = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${window.MivoConfig.apiBaseUrl}/api/status/check`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
session: window.MivoConfig.apiSession,
|
||||||
|
code: this.checkCode
|
||||||
|
})
|
||||||
|
});
|
||||||
|
const result = await response.json();
|
||||||
|
if (result.success) {
|
||||||
|
this.checkRes = result.data;
|
||||||
|
} else {
|
||||||
|
this.checkError = result.message || 'not_found';
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
this.checkError = 'Connection failed';
|
||||||
|
} finally {
|
||||||
|
this.checkLoading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
get submitUsername() {
|
||||||
|
return this.loginType === 'voucher' ? this.auth.voucher : this.auth.username;
|
||||||
|
},
|
||||||
|
|
||||||
|
get submitPassword() {
|
||||||
|
return this.loginType === 'voucher' ? this.auth.voucher : this.auth.password;
|
||||||
|
},
|
||||||
|
|
||||||
|
submit() {
|
||||||
|
this.loading = true;
|
||||||
|
|
||||||
|
if (typeof hexMD5 === 'function' && document.sendin) {
|
||||||
|
document.sendin.username.value = this.submitUsername;
|
||||||
|
document.sendin.password.value = hexMD5('$(chap-id)' + this.submitPassword + '$(chap-challenge)');
|
||||||
|
document.sendin.submit();
|
||||||
|
} else {
|
||||||
|
// For PAP: form 'login' already has hidden inputs with x-model binding to submitUsername/Password
|
||||||
|
// We just need to ensure the Values are ready.
|
||||||
|
// Since we use :value on hidden inputs, they update reactively.
|
||||||
|
// However, we must wait for Alpine tick? Actually native submit picks up current DOM value.
|
||||||
|
// Just in case, force update? No need.
|
||||||
|
setTimeout(() => {
|
||||||
|
document.getElementById('send-form').submit();
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<div class="relative w-full max-w-md" x-data="loginData()">
|
||||||
|
<div class="card overflow-hidden">
|
||||||
|
$(if chap-id)
|
||||||
|
<form name="sendin" action="$(link-login-only)" method="post" style="display:none">
|
||||||
|
<input type="hidden" name="username">
|
||||||
|
<input type="hidden" name="password">
|
||||||
|
<input type="hidden" name="dst" value="$(link-orig)">
|
||||||
|
<input type="hidden" name="popup" value="true">
|
||||||
|
</form>
|
||||||
|
$(endif)
|
||||||
|
|
||||||
|
<div class="text-center mb-6">
|
||||||
|
<div class="flex justify-center mb-4">
|
||||||
|
<img :src="'assets/img/' + (theme === 'light' ? 'logo-m' : 'logo-m-dark') + '.svg'" alt="Mivo Logo" class="h-10 w-auto">
|
||||||
|
</div>
|
||||||
|
<h1 class="text-xl font-bold tracking-tight text-foreground" data-i18n="hotspot.title">Mivo Hotspot</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tabs -->
|
||||||
|
<div class="tabs-container">
|
||||||
|
<div class="tab-slider" :class="{
|
||||||
|
'w-[calc(50%-4px)]': !hasApi,
|
||||||
|
'w-[calc(33.33%-4px)]': hasApi,
|
||||||
|
'translate-x-0': loginType === 'voucher',
|
||||||
|
'translate-x-[calc(100%+4px)]': loginType === 'member' && hasApi,
|
||||||
|
'translate-x-full': loginType === 'member' && !hasApi,
|
||||||
|
'translate-x-[calc(200%+8px)]': loginType === 'check' && hasApi
|
||||||
|
}"></div>
|
||||||
|
<button @click="loginType = 'voucher'" :class="loginType === 'voucher' ? 'tab-link-active' : 'tab-link-inactive'" class="tab-link" data-i18n="hotspot.voucher">Voucher</button>
|
||||||
|
<button @click="loginType = 'member'" :class="loginType === 'member' ? 'tab-link-active' : 'tab-link-inactive'" class="tab-link" data-i18n="hotspot.member">Member</button>
|
||||||
|
<template x-if="hasApi">
|
||||||
|
<button @click="loginType = 'check'" :class="loginType === 'check' ? 'tab-link-active' : 'tab-link-inactive'" class="tab-link" data-i18n="hotspot.check">Check</button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="send-form" name="login" action="$(link-login-only)" method="post" @submit.prevent="submit()">
|
||||||
|
<input type="hidden" name="dst" value="$(link-orig)">
|
||||||
|
<input type="hidden" name="popup" value="true">
|
||||||
|
|
||||||
|
<!-- Actual Submission Inputs -->
|
||||||
|
<input type="hidden" name="username" :value="submitUsername">
|
||||||
|
<input type="hidden" name="password" :value="submitPassword">
|
||||||
|
|
||||||
|
<!-- Sliding Tab Content -->
|
||||||
|
<div class="tab-window">
|
||||||
|
<div :class="{
|
||||||
|
'tab-track': !hasApi,
|
||||||
|
'tab-track-3': hasApi,
|
||||||
|
'translate-x-0': loginType === 'voucher',
|
||||||
|
'-translate-x-1/2': loginType === 'member' && !hasApi,
|
||||||
|
'-translate-x-1/3': loginType === 'member' && hasApi,
|
||||||
|
'-translate-x-2/3': loginType === 'check' && hasApi
|
||||||
|
}">
|
||||||
|
<!-- Voucher Pane -->
|
||||||
|
<div :class="hasApi ? 'tab-pane-3' : 'tab-pane'">
|
||||||
|
<div class="space-y-4 mb-4">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="text-xs font-semibold uppercase tracking-wider text-accents-5 ml-1" data-i18n="hotspot.voucher_code">Voucher Code</label>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<div class="relative flex-1">
|
||||||
|
<!-- Removed name="username" to prevent conflict -->
|
||||||
|
<input type="text" x-model="auth.voucher" placeholder="Voucher..." class="form-input text-lg tracking-widest font-mono focus:ring-foreground pr-10" data-i18n="hotspot.voucher_code" x-ref="voucherInput">
|
||||||
|
<button type="button" @click="initQr('voucher')" class="absolute right-2 top-1/2 -translate-y-1/2 text-slate-400 hover:text-foreground">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"></rect><rect x="14" y="3" width="7" height="7"></rect><rect x="14" y="14" width="7" height="7"></rect><path d="M3 14h7v7H3z"></path></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button type="submit" :disabled="loading" class="btn-primary h-10 px-4 rounded-xl shrink-0 aspect-square flex items-center justify-center">
|
||||||
|
<svg x-show="!loading" xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M12 5l7 7-7 7"></path></svg>
|
||||||
|
<svg x-show="loading" class="animate-spin h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Trial Button -->
|
||||||
|
$(if trial == 'yes')
|
||||||
|
<a href="$(link-login-only)?dst=$(link-orig-esc)&username=T-$(mac-esc)" class="w-full btn-secondary h-12 flex items-center justify-center gap-2 group">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-accents-5 group-hover:text-foreground transition-colors" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v20M2 12h20M5 5l14 14M19 5L5 19"></path></svg>
|
||||||
|
<span data-i18n="hotspot.trial">Free Trial Login</span>
|
||||||
|
</a>
|
||||||
|
$(endif)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Member Pane -->
|
||||||
|
<div :class="hasApi ? 'tab-pane-3' : 'tab-pane'">
|
||||||
|
<div class="space-y-4 mb-4">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="text-xs font-semibold uppercase tracking-wider text-accents-5 ml-1" data-i18n="hotspot.username">Username</label>
|
||||||
|
<div class="relative">
|
||||||
|
<!-- Removed name="username" -->
|
||||||
|
<input type="text" x-model="auth.username" placeholder="Username" class="form-input pr-10" data-i18n="hotspot.username">
|
||||||
|
<button type="button" @click="initQr('member')" class="absolute right-2 top-1/2 -translate-y-1/2 text-slate-400 hover:text-foreground">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"></rect><rect x="14" y="3" width="7" height="7"></rect><rect x="14" y="14" width="7" height="7"></rect><path d="M3 14h7v7H3z"></path></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="text-xs font-semibold uppercase tracking-wider text-accents-5 ml-1" data-i18n="hotspot.password">Password</label>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<div class="relative flex-1">
|
||||||
|
<!-- Removed name="password" -->
|
||||||
|
<input :type="showPass ? 'text' : 'password'" x-model="auth.password" placeholder="••••••••" class="form-input text-center pr-10" data-i18n="hotspot.password">
|
||||||
|
<button type="button" @click="showPass = !showPass" class="absolute right-2 top-1/2 -translate-y-1/2 text-slate-400 hover:text-foreground">
|
||||||
|
<svg x-show="!showPass" xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>
|
||||||
|
<svg x-show="showPass" xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path><line x1="1" y1="1" x2="23" y2="23"></line></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button type="submit" :disabled="loading" class="btn-primary h-10 px-4 rounded-xl shrink-0 aspect-square flex items-center justify-center">
|
||||||
|
<svg x-show="!loading" xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M12 5l7 7-7 7"></path></svg>
|
||||||
|
<svg x-show="loading" class="animate-spin h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Check Pane -->
|
||||||
|
<template x-if="hasApi">
|
||||||
|
<div class="tab-pane-3">
|
||||||
|
<div class="space-y-4 mb-4">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="text-xs font-semibold uppercase tracking-wider text-accents-5 ml-1" data-i18n="check.title">Check Voucher</label>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<div class="relative flex-1">
|
||||||
|
<input type="text" x-model="checkCode" placeholder="Voucher..." class="form-input text-lg tracking-widest font-mono focus:ring-foreground pr-10">
|
||||||
|
<button type="button" @click="initQr('check')" class="absolute right-2 top-1/2 -translate-y-1/2 text-slate-400 hover:text-foreground">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"></rect><rect x="14" y="3" width="7" height="7"></rect><rect x="14" y="14" width="7" height="7"></rect><path d="M3 14h7v7H3z"></path></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button type="button" @click="checkVoucher()" :disabled="checkLoading" class="btn-primary h-10 px-4 rounded-xl shrink-0 aspect-square flex items-center justify-center">
|
||||||
|
<svg x-show="!checkLoading" xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>
|
||||||
|
<svg x-show="checkLoading" class="animate-spin h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Result Area -->
|
||||||
|
<div x-show="checkRes || checkError" x-transition="" class="card-inner p-3 min-h-[100px] flex flex-col justify-center">
|
||||||
|
<div x-show="checkRes" class="space-y-2">
|
||||||
|
<div class="flex justify-between text-xs">
|
||||||
|
<span class="text-slate-500" data-i18n="check.status">Status</span>
|
||||||
|
<span class="font-bold text-emerald-500" x-text="checkRes?.status_label"></span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between text-xs">
|
||||||
|
<span class="text-slate-500" data-i18n="check.quota_left">Remaining</span>
|
||||||
|
<span class="font-bold text-foreground" x-text="checkRes?.data_left"></span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between text-xs">
|
||||||
|
<span class="text-slate-500" data-i18n="check.time_left">Time Left</span>
|
||||||
|
<span class="font-bold text-foreground" x-text="window.formatTime(checkRes?.time_left)"></span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between text-xs">
|
||||||
|
<span class="text-slate-500" data-i18n="check.expiration">Expires</span>
|
||||||
|
<span class="font-bold text-foreground" x-text="checkRes?.expiration"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div x-show="checkError" class="text-center text-red-500 text-xs py-4" data-i18n="check.not_found">
|
||||||
|
Voucher not found or not active.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error Message -->
|
||||||
|
$(if error)
|
||||||
|
<div x-data="{ errorShow: false }" x-init="$nextTick(() => errorShow = true)" style="display: none;" x-show="errorShow" x-transition:enter="transition ease-out duration-1000" x-transition:enter-start="opacity-0 translate-y-2 scale-95" x-transition:enter-end="opacity-100 translate-y-0 scale-100">
|
||||||
|
<div class="mt-4 p-3 rounded-xl bg-red-500/10 border border-red-500/20 text-red-500 text-xs flex items-start gap-3 shadow-lg shadow-red-500/5">
|
||||||
|
<div class="p-1 rounded-full bg-red-500/20 shrink-0">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
||||||
|
</div>
|
||||||
|
<span class="py-0.5 font-medium leading-relaxed">$(error)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
$(endif)
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="mt-8 pt-8 border-t border-accents-2 dark:border-white/10">
|
||||||
|
<p class="text-accents-5 text-[10px] tracking-widest uppercase">MIVO THEME BY DYZULKDEV • POWERED BY MIVO</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- QR Modal -->
|
||||||
|
<!-- QR Modal -->
|
||||||
|
<div x-show="showQr" style="display: none;" class="fixed inset-0 z-[100] flex items-center justify-center bg-black/80 backdrop-blur-sm p-4" x-transition.opacity="">
|
||||||
|
<div class="card w-full max-w-sm relative" @click.outside="closeQr()">
|
||||||
|
<button @click="closeQr()" class="absolute top-4 right-4 text-slate-400 hover:text-foreground">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<h3 class="text-lg font-bold mb-4" x-text="qrType === 'login' ? 'Scan Login QR' : 'Scan Check QR'"></h3>
|
||||||
|
|
||||||
|
<div x-show="!qrResult" class="relative rounded-xl overflow-hidden bg-black/5 dark:bg-white/5 mb-4 aspect-square">
|
||||||
|
<div id="reader" class="w-full h-full object-cover"></div>
|
||||||
|
<!-- Overlay Guide -->
|
||||||
|
<div class="absolute inset-0 border-2 border-white/30 m-8 rounded-lg pointer-events-none">
|
||||||
|
<div class="absolute top-0 left-0 w-4 h-4 border-t-4 border-l-4 border-emerald-500 -mt-0.5 -ml-0.5"></div>
|
||||||
|
<div class="absolute top-0 right-0 w-4 h-4 border-t-4 border-r-4 border-emerald-500 -mt-0.5 -mr-0.5"></div>
|
||||||
|
<div class="absolute bottom-0 left-0 w-4 h-4 border-b-4 border-l-4 border-emerald-500 -mb-0.5 -ml-0.5"></div>
|
||||||
|
<div class="absolute bottom-0 right-0 w-4 h-4 border-b-4 border-r-4 border-emerald-500 -mb-0.5 -mr-0.5"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div x-show="!qrResult" class="grid grid-cols-2 gap-2 mb-4">
|
||||||
|
<button @click="switchCamera()" class="btn-secondary h-10 text-xs gap-2">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 7h-9"></path><path d="M14 17H5"></path><circle cx="17" cy="17" r="3"></circle><circle cx="7" cy="7" r="3"></circle></svg>
|
||||||
|
Flip Camera
|
||||||
|
</button>
|
||||||
|
<button @click="$refs.qrFile.click()" class="btn-secondary h-10 text-xs gap-2">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><circle cx="8.5" cy="8.5" r="1.5"></circle><polyline points="21 15 16 10 5 21"></polyline></svg>
|
||||||
|
Check Image
|
||||||
|
</button>
|
||||||
|
<input type="file" x-ref="qrFile" class="hidden" accept="image/*" @change="scanFile($event)">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Validation Error -->
|
||||||
|
<div x-show="qrError" class="mb-4 p-3 rounded-lg bg-red-500/10 border border-red-500/20 text-red-500 text-sm text-center">
|
||||||
|
<span x-text="qrError"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Confirmation Step -->
|
||||||
|
<div x-show="qrResult" class="text-center space-y-4">
|
||||||
|
<div class="w-16 h-16 mx-auto bg-green-500/20 rounded-full flex items-center justify-center">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-8 h-8 text-green-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 class="font-bold text-foreground">Scanned Successfully!</h4>
|
||||||
|
<p class="text-sm text-slate-400 mt-1" x-text="qrResult?.display"></p>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2 mt-4">
|
||||||
|
<button @click="initQr(scanTarget)" class="w-full btn bg-slate-100 dark:bg-white/10 text-foreground hover:bg-slate-200 dark:hover:bg-white/20">Rescan</button>
|
||||||
|
<button @click="confirmQr()" class="w-full btn-primary">Confirm</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
109
theme/logout.html
Normal file
109
theme/logout.html
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="pragma" content="no-cache">
|
||||||
|
<meta http-equiv="expires" content="-1">
|
||||||
|
<link rel="icon" href="favicon.ico">
|
||||||
|
<link rel="stylesheet" href="assets/css/styles.css">
|
||||||
|
<script defer="" src="assets/js/alpine.min.js"></script>
|
||||||
|
<script src="assets/js/main.js"></script>
|
||||||
|
<script src="assets/js/theme.js"></script>
|
||||||
|
<script src="assets/js/i18n.js"></script>
|
||||||
|
|
||||||
|
<title>Mivo Hotspot - Logout</title>
|
||||||
|
<meta http-equiv="refresh" content="3; url=$(link-login)">
|
||||||
|
</head>
|
||||||
|
<body class="min-h-screen flex flex-col items-center justify-start p-4 pt-36 bg-background transition-colors duration-500" x-data="initTheme()">
|
||||||
|
|
||||||
|
<div class="fixed inset-0 z-0 pointer-events-none">
|
||||||
|
<!-- Subtle Grid Pattern -->
|
||||||
|
<div class="absolute inset-0 bg-[url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGNpcmNsZSBjeD0iMSIgY3k9IjEiIHI9IjEiIGZpbGw9InJnYmEoMCwwLDAsMC4wNSkiLz48L3N2Zz4=')] dark:bg-[url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGNpcmNsZSBjeD0iMSIgY3k9IjEiIHI9IjEiIGZpbGw9InJnYmEoMjU1LDI1NSwyNTUsMC4wNSkiLz48L3N2Zz4=')] [mask-image:linear-gradient(to_bottom,white,transparent)]"></div>
|
||||||
|
<!-- glowing blobs -->
|
||||||
|
<div class="absolute -top-[20%] -left-[10%] w-[70vw] h-[70vw] rounded-full bg-blue-500/10 dark:bg-blue-500/5 blur-[120px] animate-pulse" style="animation-duration: 4s;"></div>
|
||||||
|
<div class="absolute top-[30%] -right-[15%] w-[60vw] h-[60vw] rounded-full bg-purple-500/10 dark:bg-purple-500/5 blur-[100px] animate-pulse" style="animation-duration: 6s; animation-delay: 1s;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="nav-card transition-all duration-500 transform" x-data="{ showNav: true, lastScrollY: 0 }" @scroll.window="
|
||||||
|
showNav = window.scrollY < lastScrollY || window.scrollY < 50;
|
||||||
|
lastScrollY = window.scrollY;
|
||||||
|
" :class="showNav ? 'translate-y-0 opacity-100' : '-translate-y-24 opacity-0 pointer-events-none'">
|
||||||
|
<div class="theme-toggle-container">
|
||||||
|
<div class="theme-slider" :class="theme === 'dark' ? 'translate-x-[36px]' : 'translate-x-0'"></div>
|
||||||
|
<button @click="setTheme('light')" :class="theme === 'light' ? 'text-black' : 'text-slate-500 hover:text-slate-700'" class="theme-btn">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>
|
||||||
|
</button>
|
||||||
|
<button @click="setTheme('dark')" :class="theme === 'dark' ? 'text-white' : 'text-slate-300 hover:text-white'" class="theme-btn">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="h-6 w-px bg-black/5 dark:bg-white/10 mx-1"></div>
|
||||||
|
<div class="relative" x-data="{ open: false, lang: localStorage.getItem('mivo_lang') || 'en' }" @language-changed.window="lang = $event.detail.lang">
|
||||||
|
<button @click="open = !open" class="flex items-center gap-2 px-3 h-10 rounded-full bg-black/5 dark:bg-white/5 border border-black/5 dark:border-white/10 text-xs font-bold text-foreground hover:bg-black/10 dark:hover:bg-white/20 transition-all backdrop-blur-md min-w-[80px] justify-between shadow-sm">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<img :src="'assets/svg/' + (lang === 'id' ? 'id' : 'us') + '.svg'" class="w-4 h-3 rounded-sm object-cover" alt="Flag">
|
||||||
|
<span x-text="lang.toUpperCase()">EN</span>
|
||||||
|
</div>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" :class="open ? 'rotate-180' : ''" class="w-3.5 h-3.5 transition-transform text-accents-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"></path></svg>
|
||||||
|
</button>
|
||||||
|
<div x-show="open" @click.away="open = false" x-transition:enter="transition ease-out duration-100" x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100" style="display: none;" class="absolute right-0 mt-2 w-48 rounded-xl bg-white/90 dark:bg-black/80 border border-black/5 dark:border-white/10 backdrop-blur-xl shadow-xl overflow-hidden py-1 ring-1 ring-black/5">
|
||||||
|
<button @click="changeLanguage('id'); open = false" class="w-full text-left px-4 py-3 text-xs font-medium text-foreground hover:bg-black/5 dark:hover:bg-white/10 transition-colors flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<img src="assets/svg/id.svg" class="w-4 h-3 rounded-sm object-cover" alt="ID">
|
||||||
|
<span>Bahasa Indonesia</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-[10px] text-accents-5 font-bold">ID</span>
|
||||||
|
</button>
|
||||||
|
<button @click="changeLanguage('en'); open = false" class="w-full text-left px-4 py-3 text-xs font-medium text-foreground hover:bg-black/5 dark:hover:bg-white/10 transition-colors flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<img src="assets/svg/us.svg" class="w-4 h-3 rounded-sm object-cover" alt="US">
|
||||||
|
<span>English</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-[10px] text-accents-5 font-bold">EN</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="relative w-full max-w-sm">
|
||||||
|
<div class="card text-center">
|
||||||
|
<div class="flex justify-center mb-6">
|
||||||
|
<img :src="'assets/img/' + (theme === 'light' ? 'logo-m' : 'logo-m-dark') + '.svg'" alt="Mivo Logo" class="h-12 w-auto">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 class="text-2xl font-bold tracking-tight text-foreground mb-2" data-i18n="logout.title">Logged Out</h1>
|
||||||
|
<p class="text-slate-400 text-sm mb-8" data-i18n="logout.subtitle">You have been successfully disconnected from the hotspot.</p>
|
||||||
|
|
||||||
|
<div class="space-y-4 mb-8">
|
||||||
|
<div class="card-inner flex items-center justify-between">
|
||||||
|
<span class="text-slate-400 text-sm" data-i18n="logout.user">User</span>
|
||||||
|
<span class="text-foreground font-mono font-medium">$(username)</span>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div class="card-inner">
|
||||||
|
<p class="text-slate-500 text-[10px] uppercase tracking-wider font-bold mb-1" data-i18n="logout.uptime">Total Uptime</p>
|
||||||
|
<p class="text-foreground text-sm font-mono">$(uptime)</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-inner">
|
||||||
|
<p class="text-slate-500 text-[10px] uppercase tracking-wider font-bold mb-1" data-i18n="logout.data">Data Used</p>
|
||||||
|
<p class="text-foreground text-sm font-mono">$(bytes-total-nice)</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="$(link-login)" class="w-full btn-primary" data-i18n="logout.login_again">
|
||||||
|
Login Again
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="mt-8 pt-8 border-t border-accents-2 dark:border-white/10">
|
||||||
|
<p class="text-accents-5 text-[10px] tracking-widest uppercase">MIVO THEME BY DYZULKDEV • POWERED BY MIVO</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
62
theme/radvert.html
Normal file
62
theme/radvert.html
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="pragma" content="no-cache">
|
||||||
|
<meta http-equiv="expires" content="-1">
|
||||||
|
<link rel="icon" href="favicon.ico">
|
||||||
|
<link rel="stylesheet" href="assets/css/styles.css">
|
||||||
|
<script defer="" src="assets/js/alpine.min.js"></script>
|
||||||
|
<script src="assets/js/main.js"></script>
|
||||||
|
<script src="assets/js/theme.js"></script>
|
||||||
|
<script src="assets/js/i18n.js"></script>
|
||||||
|
|
||||||
|
<body onload="openAdvert()" class="antialiased bg-background text-foreground bg-cover bg-center min-h-screen flex items-center justify-start p-4 pt-36 selection:bg-accent selection:text-white overflow-hidden" x-data="theme" :class="theme" :style="'background-image: url(\'assets/img/' + (theme === 'light' ? 'bg-light.jpg' : 'bg-dark.jpg') + '\');'">
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var popup = '';
|
||||||
|
function openOrig() {
|
||||||
|
if (window.focus) popup.focus();
|
||||||
|
location.href = unescape('$(link-orig-esc)');
|
||||||
|
}
|
||||||
|
function openAd() {
|
||||||
|
location.href = unescape('$(link-redirect-esc)');
|
||||||
|
}
|
||||||
|
function openAdvert() {
|
||||||
|
if (window.name != 'hotspot_advert') {
|
||||||
|
popup = open('$(link-redirect)', 'hotspot_advert', '');
|
||||||
|
setTimeout("openOrig", 1000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setTimeout("openAd", 1000);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="fixed inset-0 bg-background/30 backdrop-blur-sm z-0"></div>
|
||||||
|
|
||||||
|
<div class="relative z-10 w-full max-w-sm">
|
||||||
|
<div class="card p-8 text-center space-y-6">
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<div class="relative w-16 h-16 flex items-center justify-center rounded-2xl bg-blue-500/10 text-blue-500">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-8 h-8" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h1 class="text-xl font-bold tracking-tight">Advertisement</h1>
|
||||||
|
<p class="text-slate-500 text-sm">Please view the advertisement to continue.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-xs text-slate-400">
|
||||||
|
If nothing happens, open <a href="$(link-redirect)" target="hotspot_advert" class="text-primary hover:underline font-medium">advertisement</a> manually.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="mt-8 pt-8 border-t border-accents-2 dark:border-white/10">
|
||||||
|
<p class="text-accents-5 text-[10px] tracking-widest uppercase">MIVO THEME BY DYZULKDEV • POWERED BY MIVO</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<meta http-equiv="refresh" content="2; url=$(link-orig)">
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
41
theme/redirect.html
Normal file
41
theme/redirect.html
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
$(if http-status == 302)Hotspot redirect$(endif)
|
||||||
|
$(if http-header == "Location")$(link-redirect)$(endif)
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="pragma" content="no-cache">
|
||||||
|
<meta http-equiv="expires" content="-1">
|
||||||
|
<link rel="icon" href="favicon.ico">
|
||||||
|
<link rel="stylesheet" href="assets/css/styles.css">
|
||||||
|
<script defer="" src="assets/js/alpine.min.js"></script>
|
||||||
|
<script src="assets/js/main.js"></script>
|
||||||
|
<script src="assets/js/theme.js"></script>
|
||||||
|
<script src="assets/js/i18n.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
if (localStorage.getItem('mivo_theme') === 'dark' || (!localStorage.getItem('mivo_theme') && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('dark');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<body class="antialiased bg-background text-foreground bg-cover bg-center min-h-screen flex items-center justify-center p-4 selection:bg-accent selection:text-white overflow-hidden" x-data="theme" :class="theme" :style="'background-image: url(\'assets/img/' + (theme === 'light' ? 'bg-light.jpg' : 'bg-dark.jpg') + '\');'">
|
||||||
|
|
||||||
|
<div class="fixed inset-0 bg-background/30 backdrop-blur-sm z-0"></div>
|
||||||
|
|
||||||
|
<div class="relative z-10 flex justify-center items-center">
|
||||||
|
<!-- Minimal Spinner -->
|
||||||
|
<svg class="animate-spin h-16 w-16 text-white drop-shadow-lg" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||||
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Standard Redirect Logic (Disabled Temporarily) -->
|
||||||
|
<!-- <meta http-equiv="refresh" content="2; url=$(link-redirect)"> -->
|
||||||
|
<!-- <script>
|
||||||
|
window.location.href = '$(link-redirect)';
|
||||||
|
</script> -->
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
51
theme/rlogin.html
Normal file
51
theme/rlogin.html
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="pragma" content="no-cache">
|
||||||
|
<meta http-equiv="expires" content="-1">
|
||||||
|
<link rel="icon" href="favicon.ico">
|
||||||
|
<link rel="stylesheet" href="assets/css/styles.css">
|
||||||
|
<script defer="" src="assets/js/alpine.min.js"></script>
|
||||||
|
<script src="assets/js/main.js"></script>
|
||||||
|
<script src="assets/js/theme.js"></script>
|
||||||
|
<script src="assets/js/i18n.js"></script>
|
||||||
|
|
||||||
|
<body class="antialiased bg-background text-foreground bg-cover bg-center min-h-screen flex items-center justify-center p-4 selection:bg-accent selection:text-white overflow-hidden" x-data="theme" :class="theme" :style="'background-image: url(\'assets/img/' + (theme === 'light' ? 'bg-light.jpg' : 'bg-dark.jpg') + '\');'">
|
||||||
|
|
||||||
|
<div class="fixed inset-0 bg-background/30 backdrop-blur-sm z-0"></div>
|
||||||
|
|
||||||
|
<div class="relative z-10 w-full max-w-sm">
|
||||||
|
<div class="card p-8 text-center space-y-6">
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<div class="relative w-16 h-16 flex items-center justify-center rounded-2xl bg-amber-500/10 text-amber-500 animate-pulse">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-8 h-8" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h1 class="text-xl font-bold tracking-tight">Login Required</h1>
|
||||||
|
<p class="text-slate-500 text-sm">Redirecting to login page...</p>
|
||||||
|
$(if http-status == 302)<p class="text-xs text-amber-500">Hotspot login required</p>$(endif)
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-xs text-slate-400">
|
||||||
|
If not redirected, <a href="$(link-redirect)" class="text-primary hover:underline font-medium">click here</a>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="mt-8 pt-8 border-t border-accents-2 dark:border-white/10">
|
||||||
|
<p class="text-accents-5 text-[10px] tracking-widest uppercase">MIVO THEME BY DYZULKDEV • POWERED BY MIVO</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Redirect Logic -->
|
||||||
|
$(if http-header == "Location")$(link-redirect)$(endif)
|
||||||
|
<meta http-equiv="refresh" content="2; url=$(link-redirect)">
|
||||||
|
<script>
|
||||||
|
setTimeout(function() {
|
||||||
|
window.location.href = '$(link-redirect)';
|
||||||
|
}, 2000);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
245
theme/status.html
Normal file
245
theme/status.html
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="pragma" content="no-cache">
|
||||||
|
<meta http-equiv="expires" content="-1">
|
||||||
|
<link rel="icon" href="favicon.ico">
|
||||||
|
<link rel="stylesheet" href="assets/css/styles.css">
|
||||||
|
<script defer="" src="assets/js/alpine.min.js"></script>
|
||||||
|
<script src="assets/js/main.js"></script>
|
||||||
|
<script src="assets/js/theme.js"></script>
|
||||||
|
<script src="assets/js/i18n.js"></script>
|
||||||
|
|
||||||
|
<title>Mivo Hotspot - Status</title>
|
||||||
|
$(if refresh-timeout)
|
||||||
|
<meta http-equiv="refresh" content="$(refresh-timeout-secs); url=$(link-status)">
|
||||||
|
$(endif)
|
||||||
|
<script>
|
||||||
|
$(if advert-pending == 'yes')
|
||||||
|
var popup = '';
|
||||||
|
function openAdvert() {
|
||||||
|
popup = open('$(link-advert)', 'hotspot_advert', '');
|
||||||
|
setTimeout('popup.focus()', 1000);
|
||||||
|
}
|
||||||
|
window.addEventListener('load', openAdvert);
|
||||||
|
$(endif)
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body class="min-h-screen flex flex-col items-center justify-start p-4 pt-36 bg-background transition-colors duration-500" x-data="initTheme()">
|
||||||
|
|
||||||
|
<div class="fixed inset-0 z-0 pointer-events-none">
|
||||||
|
<!-- Subtle Grid Pattern -->
|
||||||
|
<div class="absolute inset-0 bg-[url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGNpcmNsZSBjeD0iMSIgY3k9IjEiIHI9IjEiIGZpbGw9InJnYmEoMCwwLDAsMC4wNSkiLz48L3N2Zz4=')] dark:bg-[url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGNpcmNsZSBjeD0iMSIgY3k9IjEiIHI9IjEiIGZpbGw9InJnYmEoMjU1LDI1NSwyNTUsMC4wNSkiLz48L3N2Zz4=')] [mask-image:linear-gradient(to_bottom,white,transparent)]"></div>
|
||||||
|
<!-- glowing blobs -->
|
||||||
|
<div class="absolute -top-[20%] -left-[10%] w-[70vw] h-[70vw] rounded-full bg-blue-500/10 dark:bg-blue-500/5 blur-[120px] animate-pulse" style="animation-duration: 4s;"></div>
|
||||||
|
<div class="absolute top-[30%] -right-[15%] w-[60vw] h-[60vw] rounded-full bg-purple-500/10 dark:bg-purple-500/5 blur-[100px] animate-pulse" style="animation-duration: 6s; animation-delay: 1s;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="nav-card transition-all duration-500 transform" x-data="{ showNav: true, lastScrollY: 0 }" @scroll.window="
|
||||||
|
showNav = window.scrollY < lastScrollY || window.scrollY < 50;
|
||||||
|
lastScrollY = window.scrollY;
|
||||||
|
" :class="showNav ? 'translate-y-0 opacity-100' : '-translate-y-24 opacity-0 pointer-events-none'">
|
||||||
|
<div class="theme-toggle-container">
|
||||||
|
<div class="theme-slider" :class="theme === 'dark' ? 'translate-x-[36px]' : 'translate-x-0'"></div>
|
||||||
|
<button @click="setTheme('light')" :class="theme === 'light' ? 'text-black' : 'text-slate-500 hover:text-slate-700'" class="theme-btn">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>
|
||||||
|
</button>
|
||||||
|
<button @click="setTheme('dark')" :class="theme === 'dark' ? 'text-white' : 'text-slate-300 hover:text-white'" class="theme-btn">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="h-6 w-px bg-black/5 dark:bg-white/10 mx-1"></div>
|
||||||
|
<div class="relative" x-data="{ open: false, lang: localStorage.getItem('mivo_lang') || 'en' }" @language-changed.window="lang = $event.detail.lang">
|
||||||
|
<button @click="open = !open" class="flex items-center gap-2 px-3 h-10 rounded-full bg-black/5 dark:bg-white/5 border border-black/5 dark:border-white/10 text-xs font-bold text-foreground hover:bg-black/10 dark:hover:bg-white/20 transition-all backdrop-blur-md min-w-[80px] justify-between shadow-sm">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<img :src="'assets/svg/' + (lang === 'id' ? 'id' : 'us') + '.svg'" class="w-4 h-3 rounded-sm object-cover" alt="Flag">
|
||||||
|
<span x-text="lang.toUpperCase()">EN</span>
|
||||||
|
</div>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" :class="open ? 'rotate-180' : ''" class="w-3.5 h-3.5 transition-transform text-accents-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"></path></svg>
|
||||||
|
</button>
|
||||||
|
<div x-show="open" @click.away="open = false" x-transition:enter="transition ease-out duration-100" x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100" style="display: none;" class="absolute right-0 mt-2 w-48 rounded-xl bg-white/90 dark:bg-black/80 border border-black/5 dark:border-white/10 backdrop-blur-xl shadow-xl overflow-hidden py-1 ring-1 ring-black/5">
|
||||||
|
<button @click="changeLanguage('id'); open = false" class="w-full text-left px-4 py-3 text-xs font-medium text-foreground hover:bg-black/5 dark:hover:bg-white/10 transition-colors flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<img src="assets/svg/id.svg" class="w-4 h-3 rounded-sm object-cover" alt="ID">
|
||||||
|
<span>Bahasa Indonesia</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-[10px] text-accents-5 font-bold">ID</span>
|
||||||
|
</button>
|
||||||
|
<button @click="changeLanguage('en'); open = false" class="w-full text-left px-4 py-3 text-xs font-medium text-foreground hover:bg-black/5 dark:hover:bg-white/10 transition-colors flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<img src="assets/svg/us.svg" class="w-4 h-3 rounded-sm object-cover" alt="US">
|
||||||
|
<span>English</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-[10px] text-accents-5 font-bold">EN</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="relative w-full max-w-md" x-data="{
|
||||||
|
refresh() { window.location.reload(); },
|
||||||
|
// Live Timer
|
||||||
|
uptimeStr: '$(uptime)',
|
||||||
|
uptimeSecs: 0,
|
||||||
|
formattedUptime: '-',
|
||||||
|
|
||||||
|
// API Data
|
||||||
|
apiData: null,
|
||||||
|
hasApi: window.MivoConfig?.apiBaseUrl !== '',
|
||||||
|
|
||||||
|
init() {
|
||||||
|
// Parse initial uptime
|
||||||
|
this.uptimeSecs = window.parseTimeSeconds(this.uptimeStr);
|
||||||
|
this.updateUptime();
|
||||||
|
|
||||||
|
// Start Timer
|
||||||
|
setInterval(() => {
|
||||||
|
this.uptimeSecs++;
|
||||||
|
this.updateUptime();
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
// Fetch API stats if available
|
||||||
|
if (this.hasApi) {
|
||||||
|
this.fetchStatus();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateUptime() {
|
||||||
|
this.formattedUptime = window.formatSeconds(this.uptimeSecs);
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetchStatus() {
|
||||||
|
try {
|
||||||
|
// Use $(username) to query the Check Voucher API or similar endpoint
|
||||||
|
const url = `${window.MivoConfig.apiBaseUrl}/api/voucher/check/$(username)`;
|
||||||
|
const res = await fetch(url, {
|
||||||
|
headers: { 'X-Mivo-Session': window.MivoConfig.apiSession }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
const json = await res.json();
|
||||||
|
if (json && json.data) {
|
||||||
|
this.apiData = json.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('API Error:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}" x-init="init()" @language-changed.window="updateUptime()">
|
||||||
|
<div class="card">
|
||||||
|
<div class="flex items-center justify-between mb-8">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<img :src="'assets/img/' + (theme === 'light' ? 'logo-m' : 'logo-m-dark') + '.svg'" alt="Mivo Logo" class="h-10 w-auto">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-bold tracking-tight text-foreground" data-i18n="status.connected">Connected</h1>
|
||||||
|
<p class="text-emerald-600 dark:text-emerald-400 text-xs font-medium flex items-center gap-1.5" data-i18n="status.active_session">
|
||||||
|
<span class="relative flex h-2 w-2">
|
||||||
|
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
|
||||||
|
<span class="relative inline-flex rounded-full h-2 w-2 bg-emerald-500"></span>
|
||||||
|
</span>
|
||||||
|
Active Session
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="h-12 w-12 rounded-2xl bg-gradient-to-br from-white/10 to-transparent border border-white/10 flex items-center justify-center text-foreground shadow-inner">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.55a11 11 0 0 1 14.08 0"></path><path d="M1.42 9a16 16 0 0 1 21.16 0"></path><path d="M8.53 16.11a6 6 0 0 1 6.95 0"></path><line x1="12" y1="20" x2="12.01" y2="20"></line></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- User Info Section -->
|
||||||
|
<div class="space-y-4 mb-8">
|
||||||
|
<div class="card-inner flex items-center justify-between">
|
||||||
|
<span class="text-slate-400 text-sm" data-i18n="status.user">User</span>
|
||||||
|
<span class="text-foreground font-mono font-medium">$(username)</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div class="card-inner">
|
||||||
|
<p class="text-slate-500 text-[10px] uppercase tracking-wider font-bold mb-1" data-i18n="status.ip_address">IP Address</p>
|
||||||
|
<p class="text-foreground text-sm font-mono">$(ip)</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-inner flex flex-col justify-center">
|
||||||
|
<p class="text-slate-500 text-[10px] uppercase tracking-wider font-bold mb-1" data-i18n="status.mac_address">MAC</p>
|
||||||
|
<p class="text-foreground text-sm font-mono truncate" title="$(mac)">$(mac)</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Stats Grid -->
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div class="card-inner group hover:bg-white/5 transition-colors">
|
||||||
|
<div class="flex items-center gap-2 mb-2">
|
||||||
|
<div class="p-1 rounded-md bg-transparent">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 text-emerald-500 dark:text-emerald-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12l7 7 7-7"></path></svg>
|
||||||
|
</div>
|
||||||
|
<span class="text-slate-500 text-[10px] uppercase tracking-wider font-bold" data-i18n="status.download">Download</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-foreground text-lg font-bold">$(bytes-out-nice)</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-inner group hover:bg-white/5 transition-colors">
|
||||||
|
<div class="flex items-center gap-2 mb-2">
|
||||||
|
<div class="p-1 rounded-md bg-transparent">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 text-blue-500 dark:text-blue-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19V5M5 12l7-7 7 7"></path></svg>
|
||||||
|
</div>
|
||||||
|
<span class="text-slate-500 text-[10px] uppercase tracking-wider font-bold" data-i18n="status.upload">Upload</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-foreground text-lg font-bold">$(bytes-in-nice)</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-inner space-y-3">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-slate-400 text-sm" data-i18n="status.uptime">Session Uptime</span>
|
||||||
|
<span class="text-foreground font-medium transition-all" x-text="formattedUptime">$(uptime)</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Native Remaining Time (If available from MT) -->
|
||||||
|
$(if remain-time)
|
||||||
|
<div class="flex items-center justify-between border-t border-white/5 pt-2">
|
||||||
|
<span class="text-slate-400 text-sm" data-i18n="status.remaining">Time Remaining</span>
|
||||||
|
<span class="text-cyan-400 font-bold" x-text="window.formatTime('$(remain-time)')">$(remain-time)</span>
|
||||||
|
</div>
|
||||||
|
$(endif)
|
||||||
|
|
||||||
|
<!-- API Remaining Time (Fallback) -->
|
||||||
|
<template x-if="apiData && !'$(remain-time)'">
|
||||||
|
<div class="flex items-center justify-between border-t border-white/5 pt-2">
|
||||||
|
<span class="text-slate-400 text-sm" data-i18n="status.remaining">Time Remaining</span>
|
||||||
|
<!-- Assuming API returns time_left -->
|
||||||
|
<span class="text-cyan-400 font-bold" x-text="window.formatTime(apiData.time_left)"></span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- API Quota (Extra) -->
|
||||||
|
<template x-if="apiData && apiData.data_left">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-slate-400 text-sm">Quota Remaining</span>
|
||||||
|
<span class="text-emerald-400 font-bold" x-text="apiData.data_left"></span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-3">
|
||||||
|
<button type="button" class="w-full btn-danger" onclick="location.href='$(link-logout)'" data-i18n="status.disconnect">
|
||||||
|
Disconnect
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button @click="refresh()" class="w-full btn-secondary" data-i18n="status.refresh">
|
||||||
|
Refresh Status
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-8 pt-8 border-t border-accents-2 dark:border-white/10">
|
||||||
|
<p class="text-accents-5 text-[10px] tracking-widest uppercase">MIVO THEME BY DYZULKDEV • POWERED BY MIVO</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
101
theme/xml/WISPAccessGatewayParam.xsd
Normal file
101
theme/xml/WISPAccessGatewayParam.xsd
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" attributeFormDefault="unqualified">
|
||||||
|
<xs:element name="WISPAccessGatewayParam">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:choice>
|
||||||
|
<xs:element name="Redirect" type="RedirectType"/>
|
||||||
|
<xs:element name="Proxy" type="ProxyType"/>
|
||||||
|
<xs:element name="AuthenticationReply" type="AuthenticationReplyType"/>
|
||||||
|
<xs:element name="AuthenticationPollReply" type="AuthenticationPollReplyType"/>
|
||||||
|
<xs:element name="LogoffReply" type="LogoffReplyType"/>
|
||||||
|
<xs:element name="AbortLoginReply" type="AbortLoginReplyType"/>
|
||||||
|
</xs:choice>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<xs:simpleType name="AbortLoginURLType">
|
||||||
|
<xs:restriction base="xs:anyURI"/>
|
||||||
|
</xs:simpleType>
|
||||||
|
<xs:simpleType name="NextURLType">
|
||||||
|
<xs:restriction base="xs:anyURI"/>
|
||||||
|
</xs:simpleType>
|
||||||
|
<xs:simpleType name="AccessProcedureType">
|
||||||
|
<xs:restriction base="xs:string"/>
|
||||||
|
</xs:simpleType>
|
||||||
|
<xs:simpleType name="AccessLocationType">
|
||||||
|
<xs:restriction base="xs:string"/>
|
||||||
|
</xs:simpleType>
|
||||||
|
<xs:simpleType name="LocationNameType">
|
||||||
|
<xs:restriction base="xs:string"/>
|
||||||
|
</xs:simpleType>
|
||||||
|
<xs:simpleType name="LoginURLType">
|
||||||
|
<xs:restriction base="xs:anyURI"/>
|
||||||
|
</xs:simpleType>
|
||||||
|
<xs:simpleType name="MessageTypeType">
|
||||||
|
<xs:restriction base="xs:integer"/>
|
||||||
|
</xs:simpleType>
|
||||||
|
<xs:simpleType name="ResponseCodeType">
|
||||||
|
<xs:restriction base="xs:integer"/>
|
||||||
|
</xs:simpleType>
|
||||||
|
<xs:simpleType name="ReplyMessageType">
|
||||||
|
<xs:restriction base="xs:string"/>
|
||||||
|
</xs:simpleType>
|
||||||
|
<xs:simpleType name="LoginResultsURLType">
|
||||||
|
<xs:restriction base="xs:anyURI"/>
|
||||||
|
</xs:simpleType>
|
||||||
|
<xs:simpleType name="LogoffURLType">
|
||||||
|
<xs:restriction base="xs:anyURI"/>
|
||||||
|
</xs:simpleType>
|
||||||
|
<xs:simpleType name="DelayType">
|
||||||
|
<xs:restriction base="xs:integer"/>
|
||||||
|
</xs:simpleType>
|
||||||
|
<xs:complexType name="RedirectType">
|
||||||
|
<xs:all>
|
||||||
|
<xs:element name="AccessProcedure" type="AccessProcedureType"/>
|
||||||
|
<xs:element name="AccessLocation" type="AccessLocationType"/>
|
||||||
|
<xs:element name="LocationName" type="LocationNameType"/>
|
||||||
|
<xs:element name="LoginURL" type="LoginURLType"/>
|
||||||
|
<xs:element name="AbortLoginURL" type="AbortLoginURLType"/>
|
||||||
|
<xs:element name="MessageType" type="MessageTypeType"/>
|
||||||
|
<xs:element name="ResponseCode" type="ResponseCodeType"/>
|
||||||
|
</xs:all>
|
||||||
|
</xs:complexType>
|
||||||
|
<xs:complexType name="ProxyType">
|
||||||
|
<xs:all>
|
||||||
|
<xs:element name="MessageType" type="MessageTypeType"/>
|
||||||
|
<xs:element name="ResponseCode" type="ResponseCodeType"/>
|
||||||
|
<xs:element name="NextURL" type="NextURLType" minOccurs="0" maxOccurs="1"/>
|
||||||
|
<xs:element name="Delay" type="DelayType" minOccurs="0" maxOccurs="1"/>
|
||||||
|
</xs:all>
|
||||||
|
</xs:complexType>
|
||||||
|
<xs:complexType name="AuthenticationReplyType">
|
||||||
|
<xs:all>
|
||||||
|
<xs:element name="MessageType" type="MessageTypeType"/>
|
||||||
|
<xs:element name="ResponseCode" type="ResponseCodeType"/>
|
||||||
|
<xs:element name="ReplyMessage" type="ReplyMessageType" minOccurs="0" maxOccurs="1"/>
|
||||||
|
<xs:element name="LoginResultsURL" type="LoginResultsURLType" minOccurs="0" maxOccurs="1"/>
|
||||||
|
<xs:element name="LogoffURL" type="LogoffURLType" minOccurs="0" maxOccurs="1"/>
|
||||||
|
</xs:all>
|
||||||
|
</xs:complexType>
|
||||||
|
<xs:complexType name="AuthenticationPollReplyType">
|
||||||
|
<xs:all>
|
||||||
|
<xs:element name="MessageType" type="MessageTypeType"/>
|
||||||
|
<xs:element name="ResponseCode" type="ResponseCodeType"/>
|
||||||
|
<xs:element name="ReplyMessage" type="ReplyMessageType" minOccurs="0" maxOccurs="1"/>
|
||||||
|
<xs:element name="Delay" type="DelayType" minOccurs="0" maxOccurs="1"/>
|
||||||
|
<xs:element name="LogoffURL" type="LogoffURLType" minOccurs="0" maxOccurs="1"/>
|
||||||
|
</xs:all>
|
||||||
|
</xs:complexType>
|
||||||
|
<xs:complexType name="LogoffReplyType">
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element name="MessageType" type="MessageTypeType"/>
|
||||||
|
<xs:element name="ResponseCode" type="ResponseCodeType"/>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
<xs:complexType name="AbortLoginReplyType">
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element name="MessageType" type="MessageTypeType"/>
|
||||||
|
<xs:element name="ResponseCode" type="ResponseCodeType"/>
|
||||||
|
<xs:element name="LogoffURL" type="LogoffURLType" minOccurs="0" maxOccurs="1"/>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:schema>
|
||||||
18
theme/xml/alogin.html
Normal file
18
theme/xml/alogin.html
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<HTML> <!--
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<WISPAccessGatewayParam
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:noNamespaceSchemaLocation="http://$(hostname)/xml/WISPAccessGatewayParam.xsd">
|
||||||
|
<AuthenticationReply>
|
||||||
|
<MessageType>120</MessageType>
|
||||||
|
<ResponseCode>50</ResponseCode>
|
||||||
|
<LogoffURL>$(link-logout)</LogoffURL>
|
||||||
|
<RedirectionURL>$(link-redirect)</RedirectionURL>
|
||||||
|
$(if radius18[0]) <ReplyMessage>$(radius18[0])</ReplyMessage> $(endif)
|
||||||
|
$(if radius18[1]) <ReplyMessage>$(radius18[1])</ReplyMessage> $(endif)
|
||||||
|
$(if radius18[2]) <ReplyMessage>$(radius18[2])</ReplyMessage> $(endif)
|
||||||
|
$(if radius18[3]) <ReplyMessage>$(radius18[3])</ReplyMessage> $(endif)
|
||||||
|
$(if radius18[4]) <ReplyMessage>$(radius18[4])</ReplyMessage> $(endif)
|
||||||
|
</AuthenticationReply>
|
||||||
|
</WISPAccessGatewayParam>
|
||||||
|
--> </HTML>
|
||||||
12
theme/xml/error.html
Normal file
12
theme/xml/error.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<HTML> <!--
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<WISPAccessGatewayParam
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:noNamespaceSchemaLocation="http://$(hostname)/xml/WISPAccessGatewayParam.xsd">
|
||||||
|
<AuthenticationReply>
|
||||||
|
<MessageType>120</MessageType>
|
||||||
|
<ResponseCode>255</ResponseCode>
|
||||||
|
<ReplyMessage>$(error)</ReplyMessage>
|
||||||
|
</AuthenticationReply>
|
||||||
|
</WISPAccessGatewayParam>
|
||||||
|
--> </HTML>
|
||||||
22
theme/xml/login.html
Normal file
22
theme/xml/login.html
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<HTML> <!--
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<WISPAccessGatewayParam
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:noNamespaceSchemaLocation="http://$(hostname)/xml/WISPAccessGatewayParam.xsd">
|
||||||
|
<AuthenticationReply>
|
||||||
|
<MessageType>120</MessageType>
|
||||||
|
<ResponseCode>
|
||||||
|
$(if error-type == 'radius-timeout')
|
||||||
|
102
|
||||||
|
$(else)
|
||||||
|
100
|
||||||
|
$(endif)
|
||||||
|
</ResponseCode>
|
||||||
|
$(if error) <ReplyMessage>$(error)</ReplyMessage> $(endif)
|
||||||
|
$(if radius18[1]) <ReplyMessage>$(radius18[1])</ReplyMessage> $(endif)
|
||||||
|
$(if radius18[2]) <ReplyMessage>$(radius18[2])</ReplyMessage> $(endif)
|
||||||
|
$(if radius18[3]) <ReplyMessage>$(radius18[3])</ReplyMessage> $(endif)
|
||||||
|
$(if radius18[4]) <ReplyMessage>$(radius18[4])</ReplyMessage> $(endif)
|
||||||
|
</AuthenticationReply>
|
||||||
|
</WISPAccessGatewayParam>
|
||||||
|
--> </HTML>
|
||||||
11
theme/xml/logout.html
Normal file
11
theme/xml/logout.html
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<HTML> <!--
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<WISPAccessGatewayParam
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:noNamespaceSchemaLocation="http://$(hostname)/xml/WISPAccessGatewayParam.xsd">
|
||||||
|
<LogoffReply>
|
||||||
|
<MessageType>130</MessageType>
|
||||||
|
<ResponseCode>150</ResponseCode>
|
||||||
|
</LogoffReply>
|
||||||
|
</WISPAccessGatewayParam>
|
||||||
|
--> </HTML>
|
||||||
15
theme/xml/rlogin.html
Normal file
15
theme/xml/rlogin.html
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<HTML> <!--
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<WISPAccessGatewayParam
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:noNamespaceSchemaLocation="http://$(hostname)/xml/WISPAccessGatewayParam.xsd">
|
||||||
|
<Redirect>
|
||||||
|
<AccessProcedure>1.0</AccessProcedure>
|
||||||
|
<AccessLocation>$(location-id)</AccessLocation>
|
||||||
|
<LocationName>$(location-name)</LocationName>
|
||||||
|
<LoginURL>$(link-login-only)</LoginURL>
|
||||||
|
<MessageType>100</MessageType>
|
||||||
|
<ResponseCode>0</ResponseCode>
|
||||||
|
</Redirect>
|
||||||
|
</WISPAccessGatewayParam>
|
||||||
|
--> </HTML>
|
||||||
Reference in New Issue
Block a user