feat(ui): add offline mode UI, activity log, and recovery features
- Added bottom navigation styles and ripple effect enhancements - Implemented placeholders for charts when Chart.js is unavailable - Improved dynamic loading and error handling for Chart.js - Refactored main.js to initialize UI and Dashboard safely - Added extensive new UI functions for system info, offline details, activity log, bootloop test, recovery points, reboot dialog, logs, and about dialog - Exposed new UI functions globally for interaction - Enhanced user feedback with notifications and modals These changes improve user experience during offline mode and provide comprehensive system monitoring and recovery capabilities. Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
This commit is contained in:
Binary file not shown.
@@ -1120,6 +1120,149 @@ body {
|
||||
animation: spin var(--md-sys-motion-duration-extra-long1) linear infinite;
|
||||
}
|
||||
|
||||
/* Bottom Navigation */
|
||||
.bottom-navigation {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: var(--md-sys-color-surface-container);
|
||||
border-top: 1px solid var(--md-sys-color-outline-variant);
|
||||
display: flex;
|
||||
z-index: var(--z-fab);
|
||||
box-shadow: var(--md-sys-elevation-level2);
|
||||
}
|
||||
|
||||
.bottom-nav-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 8px 12px;
|
||||
min-height: 64px;
|
||||
cursor: pointer;
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
transition: color var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bottom-nav-item.active {
|
||||
color: var(--md-sys-color-primary);
|
||||
}
|
||||
|
||||
.bottom-nav-item span {
|
||||
font-size: var(--md-sys-typescale-label-small-font-size);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.bottom-nav-item i {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
/* Chart Placeholder */
|
||||
.chart-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 300px;
|
||||
background-color: var(--md-sys-color-surface-container);
|
||||
border-radius: var(--md-sys-shape-corner-medium);
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
text-align: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
/* Activity Styles */
|
||||
.activity-log {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.activity-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid var(--md-sys-color-outline-variant);
|
||||
}
|
||||
|
||||
.activity-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.activity-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--md-sys-color-primary-container);
|
||||
color: var(--md-sys-color-on-primary-container);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.activity-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.activity-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.activity-title {
|
||||
font-weight: var(--md-sys-typescale-body-medium-font-weight);
|
||||
color: var(--md-sys-color-on-surface);
|
||||
}
|
||||
|
||||
.activity-time {
|
||||
font-size: var(--md-sys-typescale-body-small-font-size);
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
flex-shrink: 0;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.activity-description {
|
||||
margin: 0;
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
font-size: var(--md-sys-typescale-body-small-font-size);
|
||||
}
|
||||
|
||||
/* Ripple Effect */
|
||||
.ripple-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
.ripple {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
pointer-events: none;
|
||||
transform: scale(0);
|
||||
animation: ripple-animation 0.6s ease-out;
|
||||
}
|
||||
|
||||
@keyframes ripple-animation {
|
||||
to {
|
||||
transform: scale(2);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Utilities */
|
||||
.hidden { display: none !important; }
|
||||
.visible { display: block !important; }
|
||||
|
||||
@@ -455,7 +455,12 @@ const DashboardController = {
|
||||
|
||||
// Check if Chart.js is loaded
|
||||
if (typeof Chart === 'undefined') {
|
||||
console.error('Chart.js is not loaded, skipping boot history chart');
|
||||
console.error('Chart.js is not loaded, showing placeholder instead');
|
||||
bootHistoryCanvas.style.display = 'none';
|
||||
const placeholder = document.createElement('div');
|
||||
placeholder.className = 'chart-placeholder';
|
||||
placeholder.innerHTML = '<p>Charts require internet connection</p><button class="btn small" onclick="location.reload()">Reload</button>';
|
||||
bootHistoryCanvas.parentNode.appendChild(placeholder);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -549,7 +554,12 @@ const DashboardController = {
|
||||
|
||||
// Check if Chart.js is loaded
|
||||
if (typeof Chart === 'undefined') {
|
||||
console.error('Chart.js is not loaded, skipping backup size chart');
|
||||
console.error('Chart.js is not loaded, showing placeholder instead');
|
||||
backupSizeCanvas.style.display = 'none';
|
||||
const placeholder = document.createElement('div');
|
||||
placeholder.className = 'chart-placeholder';
|
||||
placeholder.innerHTML = '<p>Charts require internet connection</p><button class="btn small" onclick="location.reload()">Reload</button>';
|
||||
backupSizeCanvas.parentNode.appendChild(placeholder);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1313,14 +1323,28 @@ const DashboardController = {
|
||||
|
||||
// Initialize dashboard when document is loaded
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Add Chart.js script dynamically
|
||||
const chartScript = document.createElement('script');
|
||||
chartScript.src = 'https://cdn.jsdelivr.net/npm/chart.js';
|
||||
chartScript.onload = () => {
|
||||
console.log('Chart.js loaded');
|
||||
// Check if Chart.js is already loaded
|
||||
if (typeof Chart !== 'undefined') {
|
||||
console.log('Chart.js already loaded');
|
||||
DashboardController.init();
|
||||
};
|
||||
document.head.appendChild(chartScript);
|
||||
} else {
|
||||
// Add Chart.js script dynamically
|
||||
const chartScript = document.createElement('script');
|
||||
chartScript.src = 'https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.js';
|
||||
chartScript.onload = () => {
|
||||
console.log('Chart.js loaded successfully');
|
||||
// Wait a bit for Chart.js to initialize
|
||||
setTimeout(() => {
|
||||
DashboardController.init();
|
||||
}, 100);
|
||||
};
|
||||
chartScript.onerror = (error) => {
|
||||
console.error('Failed to load Chart.js:', error);
|
||||
// Initialize without charts
|
||||
DashboardController.init();
|
||||
};
|
||||
document.head.appendChild(chartScript);
|
||||
}
|
||||
|
||||
// Expose dashboard controller globally
|
||||
window.Dashboard = DashboardController;
|
||||
|
||||
@@ -55,17 +55,14 @@ const AppState = {
|
||||
*/
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Initialize UI components
|
||||
UI.initTheme();
|
||||
UI.initRipple();
|
||||
UI.initNavigation();
|
||||
UI.initBottomNav();
|
||||
UI.initModals();
|
||||
UI.initFab();
|
||||
UI.initSwitches();
|
||||
UI.initSliders();
|
||||
if (typeof UI !== 'undefined') {
|
||||
UI.init(); // Use the main init function
|
||||
}
|
||||
|
||||
// Initialize Dashboard
|
||||
Dashboard.init();
|
||||
if (typeof Dashboard !== 'undefined') {
|
||||
Dashboard.init();
|
||||
}
|
||||
|
||||
// Initialize WebUIX API connection
|
||||
initWebUIX();
|
||||
@@ -83,7 +80,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
refreshData();
|
||||
|
||||
// Show toast to indicate app is ready
|
||||
UI.showToast('Application initialized');
|
||||
if (typeof UI !== 'undefined' && UI.showNotification) {
|
||||
UI.showNotification('Application initialized', 'info');
|
||||
}
|
||||
|
||||
console.log('KernelSU Anti-Bootloop Backup WebUI initialized');
|
||||
});
|
||||
@@ -2038,4 +2037,320 @@ function formatSize(bytes) {
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update system info in UI
|
||||
*/
|
||||
function updateSystemInfo() {
|
||||
const { systemInfo } = AppState;
|
||||
|
||||
// Update system overview
|
||||
const deviceModel = document.getElementById('device-model');
|
||||
const androidVersion = document.getElementById('android-version');
|
||||
const kernelsuVersion = document.getElementById('kernelsu-version');
|
||||
const moduleVersion = document.getElementById('module-version');
|
||||
|
||||
if (deviceModel) deviceModel.textContent = systemInfo.deviceModel || 'Unknown';
|
||||
if (androidVersion) androidVersion.textContent = systemInfo.androidVersion || 'Unknown';
|
||||
if (kernelsuVersion) kernelsuVersion.textContent = systemInfo.kernelSUVersion || 'Unknown';
|
||||
if (moduleVersion) moduleVersion.textContent = systemInfo.moduleVersion || 'v1.0.0';
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse size string to bytes for comparison
|
||||
* @param {string} sizeStr - Size string (e.g., "1.2G")
|
||||
* @returns {number} Size in bytes
|
||||
*/
|
||||
function parseSize(sizeStr) {
|
||||
const units = { 'B': 1, 'KB': 1024, 'MB': 1024**2, 'GB': 1024**3, 'TB': 1024**4 };
|
||||
const match = sizeStr.match(/([\d.]+)\s*(\w+)/);
|
||||
if (!match) return 0;
|
||||
const [, value, unit] = match;
|
||||
return parseFloat(value) * (units[unit] || 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show offline details dialog
|
||||
*/
|
||||
function showOfflineDetails() {
|
||||
const content = `
|
||||
<div class="offline-details">
|
||||
<p>The application is currently running in offline mode. Some features may be limited:</p>
|
||||
<ul>
|
||||
<li>Live system monitoring is disabled</li>
|
||||
<li>Backup operations use cached data</li>
|
||||
<li>Settings changes are stored locally</li>
|
||||
<li>Real-time notifications are not available</li>
|
||||
</ul>
|
||||
<p>To restore full functionality, ensure:</p>
|
||||
<ul>
|
||||
<li>WebUIX API is available</li>
|
||||
<li>KernelSU module is properly installed</li>
|
||||
<li>Network connectivity is stable</li>
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
|
||||
UI.showModal('Offline Mode Details', content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show activity log in a modal
|
||||
*/
|
||||
function showActivityLog() {
|
||||
let content = '<div class="activity-log-modal">';
|
||||
|
||||
if (AppState.activityLog.length === 0) {
|
||||
content += '<p class="placeholder-text">No activity to display</p>';
|
||||
} else {
|
||||
content += '<div class="activity-list">';
|
||||
|
||||
AppState.activityLog.forEach(activity => {
|
||||
const typeIcon = {
|
||||
'backup': 'backup',
|
||||
'restore': 'restore',
|
||||
'safety': 'security',
|
||||
'error': 'error',
|
||||
'warning': 'warning'
|
||||
}[activity.type] || 'info';
|
||||
|
||||
content += `
|
||||
<div class="activity-item ${activity.type}">
|
||||
<div class="activity-icon">
|
||||
<i class="material-icons">${typeIcon}</i>
|
||||
</div>
|
||||
<div class="activity-content">
|
||||
<div class="activity-message">${activity.message}</div>
|
||||
<div class="activity-time">${activity.date.toLocaleString()}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
content += '</div>';
|
||||
}
|
||||
|
||||
content += '</div>';
|
||||
|
||||
UI.showModal('Activity Log', content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test bootloop protection
|
||||
*/
|
||||
function testProtection() {
|
||||
UI.showConfirmDialog(
|
||||
'Test Bootloop Protection',
|
||||
'This will test the bootloop protection system by simulating a boot failure. Are you sure you want to continue?',
|
||||
'Test',
|
||||
'Cancel',
|
||||
async () => {
|
||||
UI.showLoader('Testing bootloop protection...');
|
||||
|
||||
try {
|
||||
if (AppState.isWebUIXConnected) {
|
||||
const result = await executeCommand('sh /data/adb/modules/kernelsu_antibootloop_backup/scripts/test-protection.sh');
|
||||
|
||||
if (result && result.includes('Test completed successfully')) {
|
||||
UI.showToast('Bootloop protection test completed successfully');
|
||||
logActivity('safety', 'Bootloop protection test completed');
|
||||
} else {
|
||||
UI.showToast('Bootloop protection test failed', 'error');
|
||||
}
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
UI.showToast('Bootloop protection test completed (mock mode)');
|
||||
logActivity('safety', 'Bootloop protection test completed (mock)');
|
||||
}, 2000);
|
||||
}
|
||||
} catch (error) {
|
||||
UI.showToast('Failed to test bootloop protection', 'error');
|
||||
console.error('Test protection error:', error);
|
||||
} finally {
|
||||
UI.hideLoader();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create recovery point
|
||||
* @param {string} description - Recovery point description
|
||||
*/
|
||||
function createRecoveryPoint(description = 'Manual recovery point') {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
UI.showLoader('Creating recovery point...');
|
||||
|
||||
if (AppState.isWebUIXConnected) {
|
||||
const name = `recovery_point_${Date.now()}.point`;
|
||||
const command = `sh /data/adb/modules/kernelsu_antibootloop_backup/scripts/recovery-point.sh create "${name}" "${description}"`;
|
||||
|
||||
const result = await executeCommand(command);
|
||||
|
||||
if (result && result.includes('Recovery point created')) {
|
||||
UI.showToast('Recovery point created successfully');
|
||||
logActivity('safety', `Created recovery point: ${description}`);
|
||||
|
||||
// Add to recovery points array
|
||||
AppState.recoveryPoints.push({
|
||||
name,
|
||||
path: `/data/adb/modules/kernelsu_antibootloop_backup/config/recovery_points/${name}`,
|
||||
date: new Date(),
|
||||
description
|
||||
});
|
||||
|
||||
updateRecoveryPointList();
|
||||
resolve(true);
|
||||
} else {
|
||||
UI.showToast('Failed to create recovery point', 'error');
|
||||
reject(new Error('Recovery point creation failed'));
|
||||
}
|
||||
} else {
|
||||
// Mock mode
|
||||
setTimeout(() => {
|
||||
UI.showToast('Recovery point created successfully (mock mode)');
|
||||
logActivity('safety', `Created recovery point: ${description} (mock)`);
|
||||
|
||||
const name = `recovery_point_${Date.now()}.point`;
|
||||
AppState.recoveryPoints.push({
|
||||
name,
|
||||
path: `/mock/path/${name}`,
|
||||
date: new Date(),
|
||||
description
|
||||
});
|
||||
|
||||
updateRecoveryPointList();
|
||||
resolve(true);
|
||||
}, 1500);
|
||||
}
|
||||
} catch (error) {
|
||||
UI.showToast('Failed to create recovery point', 'error');
|
||||
console.error('Create recovery point error:', error);
|
||||
reject(error);
|
||||
} finally {
|
||||
UI.hideLoader();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show reboot dialog
|
||||
* @param {string} message - Reboot message
|
||||
*/
|
||||
function showRebootDialog(message) {
|
||||
UI.showConfirmDialog(
|
||||
'Reboot Required',
|
||||
message,
|
||||
'Reboot Now',
|
||||
'Later',
|
||||
async () => {
|
||||
UI.showLoader('Rebooting device...');
|
||||
|
||||
if (AppState.isWebUIXConnected) {
|
||||
try {
|
||||
await executeCommand('reboot');
|
||||
} catch (error) {
|
||||
console.error('Reboot command error:', error);
|
||||
}
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
UI.hideLoader();
|
||||
UI.showToast('Reboot command sent (mock mode)');
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show logs dialog
|
||||
*/
|
||||
function showLogs() {
|
||||
UI.showLoader('Loading logs...');
|
||||
|
||||
const loadLogs = async () => {
|
||||
if (!AppState.isWebUIXConnected) {
|
||||
return 'Mock log content\n[INFO] Module started\n[INFO] Bootloop protection enabled\n[INFO] Backup system ready';
|
||||
}
|
||||
|
||||
try {
|
||||
const logPath = '/data/adb/modules/kernelsu_antibootloop_backup/logs/module.log';
|
||||
return await executeCommand(`tail -100 ${logPath}`);
|
||||
} catch (error) {
|
||||
return 'Error loading logs: ' + error.message;
|
||||
}
|
||||
};
|
||||
|
||||
loadLogs().then(logContent => {
|
||||
UI.hideLoader();
|
||||
|
||||
const content = `
|
||||
<div class="logs-viewer">
|
||||
<div class="logs-toolbar">
|
||||
<button class="btn small" onclick="downloadLogs()">Download</button>
|
||||
<button class="btn small" onclick="clearLogs()">Clear</button>
|
||||
<button class="btn small" onclick="refreshLogs()">Refresh</button>
|
||||
</div>
|
||||
<pre class="logs-content">${logContent}</pre>
|
||||
</div>
|
||||
`;
|
||||
|
||||
UI.showModal('System Logs', content);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show about dialog
|
||||
*/
|
||||
function showAboutDialog() {
|
||||
const content = `
|
||||
<div class="about-dialog">
|
||||
<div class="app-info">
|
||||
<h3>KernelSU Anti-Bootloop & Backup</h3>
|
||||
<p>Version: ${APP_CONFIG.version}</p>
|
||||
<p>Author: Wiktor/overspend1</p>
|
||||
<p>Built: ${new Date().getFullYear()}</p>
|
||||
</div>
|
||||
|
||||
<div class="description">
|
||||
<p>Advanced KernelSU module that combines anti-bootloop protection with comprehensive backup and restoration capabilities.</p>
|
||||
</div>
|
||||
|
||||
<div class="features">
|
||||
<h4>Features:</h4>
|
||||
<ul>
|
||||
<li>Bootloop protection and recovery</li>
|
||||
<li>Comprehensive backup system</li>
|
||||
<li>WebUIX-compliant interface</li>
|
||||
<li>Encrypted backups</li>
|
||||
<li>Multi-stage recovery mechanisms</li>
|
||||
<li>Real-time system monitoring</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="system-info">
|
||||
<h4>System Information:</h4>
|
||||
<p>Device: ${AppState.systemInfo.deviceModel}</p>
|
||||
<p>Android: ${AppState.systemInfo.androidVersion}</p>
|
||||
<p>KernelSU: ${AppState.systemInfo.kernelSUVersion}</p>
|
||||
<p>Kernel: ${AppState.systemInfo.kernelVersion}</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
UI.showModal('About', content);
|
||||
}
|
||||
|
||||
// Expose functions globally
|
||||
window.updateSystemInfo = updateSystemInfo;
|
||||
window.showOfflineDetails = showOfflineDetails;
|
||||
window.showActivityLog = showActivityLog;
|
||||
window.testProtection = testProtection;
|
||||
window.createRecoveryPoint = createRecoveryPoint;
|
||||
window.showRebootDialog = showRebootDialog;
|
||||
window.showLogs = showLogs;
|
||||
window.showAboutDialog = showAboutDialog;
|
||||
window.parseSize = parseSize;
|
||||
window.AppState = AppState;
|
||||
@@ -118,6 +118,10 @@ const UIController = {
|
||||
/**
|
||||
* Initialize Material Design ripple effect
|
||||
*/
|
||||
initRipple: function() {
|
||||
return this.initRippleEffect();
|
||||
},
|
||||
|
||||
initRippleEffect: function() {
|
||||
// Add ripple effect to buttons
|
||||
document.querySelectorAll('.btn, .icon-button, .nav-item, .bottom-nav-item').forEach(button => {
|
||||
|
||||
Reference in New Issue
Block a user