mirror of
https://github.com/mivodev/plugin-mivo-theme.git
synced 2026-01-26 05:15:27 +07:00
feat: bundled theme update & bump to v1.1.0
This commit is contained in:
@@ -2,7 +2,8 @@
|
||||
|
||||
**Category:** Hotspot Tools
|
||||
**Author:** DyzulkDev
|
||||
**Version:** 1.0.0
|
||||
**Version:** 1.1.0
|
||||
**Requirements:** Mivo Core >= v1.2.3
|
||||
|
||||
The **Mivo Theme Downloader** allows you to easily download and install the latest Captive Portal themes directly from the Mivo settings panel.
|
||||
|
||||
|
||||
@@ -7,13 +7,13 @@ 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
|
||||
* Version: 1.1.0
|
||||
* Author: DyzulkDev
|
||||
*
|
||||
* Category: Hotspot Tools
|
||||
* Scope: Session
|
||||
* Tags: theme, downloader, hotspot, captive-portal
|
||||
* Core Version: >= 1.0
|
||||
* Core Version: >= 1.2.3
|
||||
*/
|
||||
|
||||
// 1. Register Routes
|
||||
|
||||
@@ -6,13 +6,13 @@
|
||||
window.MivoConfig = {
|
||||
// API Configuration
|
||||
// Example: "http://192.168.1.1/mivo/public"
|
||||
apiBaseUrl: "",
|
||||
apiBaseUrl: "https://mivo.der.my.id",
|
||||
|
||||
// Your Mivo Session Name
|
||||
apiSession: "router-jakarta-1",
|
||||
apiSession: "my-router",
|
||||
|
||||
// Set to true to force Check Voucher tab even if apiBaseUrl is empty (for dev/test)
|
||||
debugMode: false
|
||||
debugMode: true
|
||||
};
|
||||
|
||||
// 2. Global Utilities
|
||||
@@ -78,6 +78,7 @@ window.formatSeconds = function(seconds) {
|
||||
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'}`);
|
||||
@@ -86,3 +87,235 @@ window.formatSeconds = function(seconds) {
|
||||
|
||||
return parts.join(' ');
|
||||
};
|
||||
|
||||
// Helper to format bytes
|
||||
window.bytesToSize = function(bytes) {
|
||||
if (!bytes || bytes === 0) return '0 B';
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = parseInt(Math.floor(Math.log(Math.abs(bytes)) / Math.log(1024)));
|
||||
return Math.round(bytes / Math.pow(1024, i), 2) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
// Status Page Logic
|
||||
window.initStatusPage = function(props) {
|
||||
return {
|
||||
// Props from HTML
|
||||
uptimeStr: props.uptime || '-',
|
||||
username: props.username || '',
|
||||
limitTimeStr: props.limitTime || '',
|
||||
limitBytesStr: props.limitBytes || '',
|
||||
remainBytesStr: props.remainBytes || '',
|
||||
remainTimeStr: props.remainTime || '',
|
||||
|
||||
// State
|
||||
uptimeSecs: 0,
|
||||
formattedUptime: '-',
|
||||
apiData: null,
|
||||
fetchError: null, // New debug state
|
||||
hasApi: window.MivoConfig?.apiBaseUrl !== '',
|
||||
|
||||
// Derived Limits & Usage
|
||||
limitTimeSecs: 0,
|
||||
limitBytes: 0,
|
||||
remainBytes: 0,
|
||||
limitTimeStrDisplay: '',
|
||||
|
||||
init() {
|
||||
// Initial Parse (Native)
|
||||
this.uptimeSecs = window.parseTimeSeconds(this._clean(this.uptimeStr));
|
||||
this.updateUptime();
|
||||
|
||||
// Start Timer
|
||||
setInterval(() => {
|
||||
this.uptimeSecs++;
|
||||
this.updateUptime();
|
||||
}, 1000);
|
||||
|
||||
// Initial Calculation
|
||||
this.calculateLimits();
|
||||
|
||||
// Smart Fetch: If limits are missing/invalid AND we have API, fetch it.
|
||||
if (this.shouldFetchApi() && this.hasApi) {
|
||||
console.log("MivoStatus: Local/Dev mode detected. Fetching API...");
|
||||
this.fetchStatus();
|
||||
}
|
||||
},
|
||||
|
||||
updateUptime() {
|
||||
this.formattedUptime = window.formatSeconds(this.uptimeSecs);
|
||||
},
|
||||
|
||||
_clean(val) {
|
||||
return (!val || val.startsWith('$')) ? '' : val;
|
||||
},
|
||||
|
||||
shouldFetchApi() {
|
||||
// Fetch if username is valid BUT limits are empty (dev mode or no limit set)
|
||||
// 1. Unreplaced Template Check ('$')
|
||||
if (this.limitTimeStr.startsWith('$') || this.limitBytesStr.startsWith('$') || this.username.startsWith('$')) return true;
|
||||
|
||||
// 2. Empty/Zero Check (Visual Fallback)
|
||||
// If native variables are present but "0" or empty, it usually means "Unlimited" in RouterOS.
|
||||
// HOWEVER, in some custom setups or dev environments, they might just be missing.
|
||||
// We'll trust the API check here: If we HAVE an API config, let's double check it
|
||||
// if we see "0" limits, just in case the API has better info (like your case).
|
||||
const isTimeEmpty = !this.limitTimeStr || this.limitTimeStr === '0' || this.limitTimeStr === '0s';
|
||||
const isDataEmpty = !this.limitBytesStr || this.limitBytesStr === '0';
|
||||
|
||||
if (isTimeEmpty && isDataEmpty) return true;
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
calculateLimits() {
|
||||
// 1. Clean Inputs
|
||||
let cTime = this._clean(this.limitTimeStr);
|
||||
let cBytes = this._clean(this.limitBytesStr);
|
||||
let cRemainBytes = this._clean(this.remainBytesStr);
|
||||
let cRemainTime = this._clean(this.remainTimeStr);
|
||||
|
||||
// 2. Parse Native
|
||||
this.limitTimeSecs = window.parseTimeSeconds(cTime);
|
||||
this.limitBytes = parseInt(cBytes) || 0;
|
||||
this.remainBytes = parseInt(cRemainBytes) || 0;
|
||||
this.limitTimeStrDisplay = cTime;
|
||||
|
||||
// 3. API Overrides (Hybrid Fallback)
|
||||
if (this.apiData) {
|
||||
// If native failed (0), try API
|
||||
if (this.limitBytes === 0 && this.apiData.limit_quota) {
|
||||
this.limitBytes = parseInt(this.apiData.limit_quota) || 0;
|
||||
}
|
||||
if (this.limitTimeSecs === 0 && this.apiData.limit_uptime) {
|
||||
this.limitTimeStrDisplay = this.apiData.limit_uptime;
|
||||
this.limitTimeSecs = window.parseTimeSeconds(this.limitTimeStrDisplay);
|
||||
}
|
||||
|
||||
// For Data Remaining, API usually gives "data_left".
|
||||
// If we don't have native remainBytes, try parsing API
|
||||
if (this.remainBytes === 0 && this.apiData.data_left) {
|
||||
// Try parsing "4.4 MiB" or raw number
|
||||
// Simple regex for raw number check
|
||||
if (!isNaN(this.apiData.data_left)) {
|
||||
// It's a number (bytes)
|
||||
this.remainBytes = parseInt(this.apiData.data_left);
|
||||
} else {
|
||||
// It's formatted. We can't easily get exact bytes without a reverse parser.
|
||||
// But for the Progress Bar, we need bytes.
|
||||
// If we can't parse, we might skip the bar update or implementation a parseBytes helper later.
|
||||
// For now, let's assume API sends raw bytes OR we skip.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Fallback: Infer Limit from Uptime + Remaining
|
||||
// If Limit is 0, but we have Uptime and Remaining, we can calculate the Limit.
|
||||
if (this.limitTimeSecs === 0 && this.uptimeSecs > 0) {
|
||||
let remainingSecs = 0;
|
||||
|
||||
// Try from Native String
|
||||
if (this.remainTimeStr && !this.remainTimeStr.startsWith('$')) {
|
||||
remainingSecs = window.parseTimeSeconds(this.remainTimeStr);
|
||||
}
|
||||
|
||||
// Try from API if native failed
|
||||
if (remainingSecs === 0 && this.apiData && this.apiData.time_left) {
|
||||
remainingSecs = window.parseTimeSeconds(this.apiData.time_left);
|
||||
}
|
||||
|
||||
// Apply Inference
|
||||
if (remainingSecs > 0) {
|
||||
this.limitTimeSecs = this.uptimeSecs + remainingSecs;
|
||||
this.limitTimeStrDisplay = window.formatSeconds(this.limitTimeSecs) + ' (Est)';
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
getTimePercent() {
|
||||
if (this.limitTimeSecs <= 0) return 0;
|
||||
// Native Uptime is counting UP.
|
||||
// Bar: Width = Remaining %.
|
||||
// Remaining = Limit - Uptime.
|
||||
let remaining = this.limitTimeSecs - this.uptimeSecs;
|
||||
let p = (remaining / this.limitTimeSecs) * 100;
|
||||
return p < 0 ? 0 : (p > 100 ? 100 : p);
|
||||
},
|
||||
|
||||
getDataPercent() {
|
||||
if (this.limitBytes <= 0) return 0;
|
||||
|
||||
// 1. Use Remain Bytes (Native or Parsed API)
|
||||
if (this.remainBytes > 0) {
|
||||
let p = (this.remainBytes / this.limitBytes) * 100;
|
||||
return p > 100 ? 100 : p;
|
||||
}
|
||||
|
||||
// 2. Fallback: Estimate from API "data_left" string if needed?
|
||||
// (Skipped for safety to avoid NaN)
|
||||
|
||||
return 0;
|
||||
},
|
||||
|
||||
getBarColor(percent) {
|
||||
if (percent > 50) return 'bg-emerald-500';
|
||||
if (percent > 20) return 'bg-yellow-500';
|
||||
return 'bg-red-500';
|
||||
},
|
||||
|
||||
getRemainTimeDisplay() {
|
||||
const native = this._clean(this.remainTimeStr);
|
||||
if (native) return native;
|
||||
|
||||
if (this.apiData && this.apiData.time_left) {
|
||||
return window.formatTime(this.apiData.time_left);
|
||||
}
|
||||
return '';
|
||||
},
|
||||
|
||||
hasRemainTime() {
|
||||
// Check native
|
||||
if (this._clean(this.remainTimeStr)) return true;
|
||||
// Check API
|
||||
if (this.apiData && this.apiData.time_left) return true;
|
||||
return false;
|
||||
},
|
||||
|
||||
refresh() {
|
||||
window.location.reload();
|
||||
},
|
||||
|
||||
async fetchStatus() {
|
||||
try {
|
||||
// Determine Username: Prop or Fallback
|
||||
let user = this._clean(this.username);
|
||||
// Fallback to 'customer' for debugging if username is unreplaced
|
||||
if (!user && (this.username.startsWith('$') || window.MivoConfig?.debugMode)) {
|
||||
user = "customer";
|
||||
}
|
||||
|
||||
if (!user) return; // Can't fetch without user
|
||||
|
||||
const url = `${window.MivoConfig.apiBaseUrl}/api/voucher/check/${user}`;
|
||||
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;
|
||||
this.calculateLimits();
|
||||
this.fetchError = "Success";
|
||||
} else {
|
||||
this.fetchError = "No Data in JSON";
|
||||
}
|
||||
} else {
|
||||
this.fetchError = "HTTP " + res.status;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('MivoStatus: API Error', e);
|
||||
this.fetchError = e.message;
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -129,6 +129,12 @@
|
||||
},
|
||||
|
||||
submit() {
|
||||
// Fix: Intercept 'check' mode to prevent login submission
|
||||
if (this.loginType === 'check') {
|
||||
this.checkVoucher();
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
|
||||
if (typeof hexMD5 === 'function' && document.sendin) {
|
||||
|
||||
@@ -81,57 +81,14 @@
|
||||
</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="relative w-full max-w-md" x-data="window.initStatusPage({
|
||||
uptime: '$(uptime)',
|
||||
username: '$(username)',
|
||||
limitTime: '$(limit-session-time)',
|
||||
limitBytes: '$(limit-bytes-total)',
|
||||
remainBytes: '$(remain-bytes-total)',
|
||||
remainTime: '$(remain-time)'
|
||||
})" @language-changed.window="updateUptime()">
|
||||
<div class="card">
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<div class="flex items-center gap-4">
|
||||
@@ -198,25 +155,53 @@
|
||||
<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>
|
||||
<!-- Time Limit Bar -->
|
||||
<template x-if="limitTimeSecs > 0">
|
||||
<div class="space-y-1.5 pt-2 border-t border-white/5">
|
||||
<div class="flex items-center justify-between text-xs">
|
||||
<span class="text-slate-400" data-i18n="status.time_left">Time Left</span>
|
||||
<span class="text-foreground font-mono font-bold" x-text="window.formatSeconds(limitTimeSecs - uptimeSecs)"></span>
|
||||
</div>
|
||||
<div class="h-2 w-full bg-slate-700/50 rounded-full overflow-hidden">
|
||||
<div class="h-full rounded-full transition-all duration-500" :class="getBarColor(getTimePercent())" :style="`width: ${getTimePercent()}%`"></div>
|
||||
</div>
|
||||
<!-- Show total limit textual -->
|
||||
<div class="flex justify-end">
|
||||
<span class="text-[10px] text-slate-500">Limit: <span x-text="limitTimeStrDisplay"></span></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">
|
||||
<!-- Native Remaining Time (Text Only Fallback if no limit detected but remain-time exists) -->
|
||||
<template x-if="hasRemainTime() && limitTimeSecs <= 0">
|
||||
<div class="flex flex-col border-t border-white/5 pt-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-slate-400 text-sm" data-i18n="status.remaining">Time Remaining</span>
|
||||
<span class="text-cyan-400 font-bold" x-text="getRemainTimeDisplay()"></span>
|
||||
</div>
|
||||
<!-- Debug info hidden -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Data Limit Bar -->
|
||||
<template x-if="limitBytes > 0 && remainBytes > 0">
|
||||
<div class="space-y-1.5 pt-2 border-t border-white/5">
|
||||
<div class="flex items-center justify-between text-xs">
|
||||
<span class="text-slate-400" data-i18n="status.data_left">Data Left</span>
|
||||
<!-- We need a formatBytes helper or simple logic roughly -->
|
||||
<span class="text-foreground font-mono font-bold" x-text="window.bytesToSize(remainBytes)">$(remain-bytes-total-nice)</span>
|
||||
</div>
|
||||
<div class="h-2 w-full bg-slate-700/50 rounded-full overflow-hidden">
|
||||
<div class="h-full rounded-full transition-all duration-500" :class="getBarColor(getDataPercent())" :style="`width: ${getDataPercent()}%`"></div>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<span class="text-[10px] text-slate-500">Limit: <span x-text="window.bytesToSize(limitBytes)"></span></span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- API Quota (Extra Fallback) -->
|
||||
<template x-if="(!limitBytes || limitBytes <= 0) && 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>
|
||||
|
||||
Reference in New Issue
Block a user