mirror of
https://github.com/StuffAnThings/qbit_manage.git
synced 2025-10-11 06:16:35 +08:00
# Requirements Updated - fastapi==0.116.0 - retrying==1.4.0 - uvicorn==0.35.0 # New Features - **Web UI**: Introduced a new Web UI for configuring and managing qBit Manage. - Visual Configuration Editor for YAML files. - Command Execution directly from the UI. - Undo/Redo History for changes. - Theme Support (light/dark mode). - Responsive Design for desktop and mobile. - Real-time YAML Preview. - Pass skip qbitorrent check as optional parameter to the API (Adds #860)\ **Full Changelog**: https://github.com/StuffAnThings/qbit_manage/compare/v4.4.0...v4.5.0 --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: ineednewpajamas <73252768+ineednewpajamas@users.noreply.github.com>
217 lines
6.9 KiB
JavaScript
217 lines
6.9 KiB
JavaScript
/**
|
|
* Header Component
|
|
* Handles header-specific functionality including mobile menu toggle,
|
|
* action buttons, and responsive behavior
|
|
*/
|
|
|
|
class HeaderComponent {
|
|
constructor() {
|
|
this.mobileMenuToggle = document.getElementById('mobile-menu-toggle');
|
|
this.headerActions = document.querySelector('.header-actions');
|
|
this.sidebar = document.querySelector('.sidebar');
|
|
this.isMobileMenuOpen = false;
|
|
this.originalButtonTexts = new Map(); // Store original button texts
|
|
|
|
this.init();
|
|
}
|
|
|
|
init() {
|
|
this.bindEvents();
|
|
this.handleResize();
|
|
|
|
// Listen for window resize to handle responsive behavior
|
|
window.addEventListener('resize', () => this.handleResize());
|
|
}
|
|
|
|
bindEvents() {
|
|
// Mobile menu toggle
|
|
if (this.mobileMenuToggle) {
|
|
this.mobileMenuToggle.addEventListener('click', () => this.toggleMobileMenu());
|
|
}
|
|
|
|
// Header action buttons
|
|
this.bindActionButtons();
|
|
}
|
|
|
|
bindActionButtons() {
|
|
// Save button
|
|
const saveBtn = document.getElementById('save-config-btn');
|
|
if (saveBtn) {
|
|
saveBtn.addEventListener('click', () => this.handleSave());
|
|
}
|
|
|
|
// Validate button
|
|
const validateBtn = document.getElementById('validate-config-btn');
|
|
if (validateBtn) {
|
|
validateBtn.addEventListener('click', () => this.handleValidate());
|
|
}
|
|
|
|
// Backup button
|
|
const backupBtn = document.getElementById('backup-config-btn');
|
|
if (backupBtn) {
|
|
backupBtn.addEventListener('click', () => this.handleBackup());
|
|
}
|
|
|
|
// Help button
|
|
const helpBtn = document.getElementById('help-btn');
|
|
if (helpBtn) {
|
|
helpBtn.addEventListener('click', () => this.handleHelp());
|
|
}
|
|
|
|
// Undo/Redo buttons are handled by history manager
|
|
}
|
|
|
|
toggleMobileMenu() {
|
|
this.isMobileMenuOpen = !this.isMobileMenuOpen;
|
|
|
|
if (this.sidebar) {
|
|
this.sidebar.classList.toggle('mobile-open', this.isMobileMenuOpen);
|
|
}
|
|
|
|
// Update mobile menu toggle icon
|
|
const icon = this.mobileMenuToggle.querySelector('.material-icons');
|
|
if (icon) {
|
|
icon.textContent = this.isMobileMenuOpen ? 'close' : 'menu';
|
|
}
|
|
|
|
// Update aria-expanded attribute
|
|
this.mobileMenuToggle.setAttribute('aria-expanded', this.isMobileMenuOpen.toString());
|
|
}
|
|
|
|
handleResize() {
|
|
const isMobile = window.innerWidth < 768;
|
|
|
|
if (this.mobileMenuToggle) {
|
|
this.mobileMenuToggle.style.display = isMobile ? 'flex' : 'none';
|
|
}
|
|
|
|
// Close mobile menu on desktop
|
|
if (!isMobile && this.isMobileMenuOpen) {
|
|
this.toggleMobileMenu();
|
|
}
|
|
|
|
// Adjust header actions visibility on small screens
|
|
this.adjustHeaderActions(window.innerWidth);
|
|
}
|
|
|
|
adjustHeaderActions(width) {
|
|
if (!this.headerActions) return;
|
|
|
|
const buttons = this.headerActions.querySelectorAll('.btn:not(.btn-icon)');
|
|
|
|
if (width < 640) {
|
|
// On very small screens, hide button text and show only icons
|
|
buttons.forEach(btn => {
|
|
// Store original text if not already stored
|
|
if (!this.originalButtonTexts.has(btn)) {
|
|
const textNodes = Array.from(btn.childNodes).filter(node =>
|
|
node.nodeType === Node.TEXT_NODE && node.textContent.trim()
|
|
);
|
|
if (textNodes.length > 0) {
|
|
this.originalButtonTexts.set(btn, textNodes[0].textContent.trim());
|
|
}
|
|
}
|
|
|
|
// Hide button text
|
|
const textNodes = Array.from(btn.childNodes).filter(node =>
|
|
node.nodeType === Node.TEXT_NODE
|
|
);
|
|
textNodes.forEach(textNode => {
|
|
textNode.textContent = '';
|
|
});
|
|
btn.classList.add('btn-icon-only');
|
|
});
|
|
} else {
|
|
// Restore button text on larger screens
|
|
buttons.forEach(btn => {
|
|
btn.classList.remove('btn-icon-only');
|
|
|
|
// Restore original text
|
|
if (this.originalButtonTexts.has(btn)) {
|
|
const originalText = this.originalButtonTexts.get(btn);
|
|
// Find the last text node or create one if needed
|
|
const textNodes = Array.from(btn.childNodes).filter(node =>
|
|
node.nodeType === Node.TEXT_NODE
|
|
);
|
|
|
|
if (textNodes.length > 0) {
|
|
// Use the last text node (typically after the icon)
|
|
textNodes[textNodes.length - 1].textContent = originalText;
|
|
} else {
|
|
// Create a new text node if none exists
|
|
btn.appendChild(document.createTextNode(originalText));
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
handleSave() {
|
|
// This will be handled by the main app's save functionality
|
|
// Just emit a custom event that the main app can listen to
|
|
window.dispatchEvent(new CustomEvent('header:save'));
|
|
}
|
|
|
|
handleValidate() {
|
|
// Emit validation event
|
|
window.dispatchEvent(new CustomEvent('header:validate'));
|
|
}
|
|
|
|
handleBackup() {
|
|
// Emit backup event
|
|
window.dispatchEvent(new CustomEvent('header:backup'));
|
|
}
|
|
|
|
handleHelp() {
|
|
// Emit help event
|
|
window.dispatchEvent(new CustomEvent('header:help'));
|
|
}
|
|
|
|
// Public methods for external control
|
|
updateSaveButtonState(enabled) {
|
|
const saveBtn = document.getElementById('save-config-btn');
|
|
if (saveBtn) {
|
|
saveBtn.disabled = !enabled;
|
|
}
|
|
}
|
|
|
|
updateHistoryButtonStates(canUndo, canRedo) {
|
|
const undoBtn = document.getElementById('undo-btn');
|
|
const redoBtn = document.getElementById('redo-btn');
|
|
|
|
if (undoBtn) undoBtn.disabled = !canUndo;
|
|
if (redoBtn) redoBtn.disabled = !canRedo;
|
|
}
|
|
|
|
showLoadingState(button) {
|
|
if (!button) return;
|
|
|
|
const icon = button.querySelector('.material-icons');
|
|
if (icon) {
|
|
icon.textContent = 'hourglass_empty';
|
|
icon.classList.add('rotating');
|
|
}
|
|
button.disabled = true;
|
|
}
|
|
|
|
hideLoadingState(button, originalIcon) {
|
|
if (!button) return;
|
|
|
|
const icon = button.querySelector('.material-icons');
|
|
if (icon) {
|
|
icon.textContent = originalIcon;
|
|
icon.classList.remove('rotating');
|
|
}
|
|
button.disabled = false;
|
|
}
|
|
}
|
|
|
|
// Initialize header component when DOM is loaded
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
window.headerComponent = new HeaderComponent();
|
|
});
|
|
|
|
// Export for module usage
|
|
if (typeof module !== 'undefined' && module.exports) {
|
|
module.exports = HeaderComponent;
|
|
}
|