mirror of
https://github.com/UNITRONIX/BetterDesk.git
synced 2026-09-10 09:35:39 +00:00
4d2a4e0488
UI: group chip buttons changed to icon-only (matching folder chips), Create Group tile styling unified with Create Folder, old .group-chip-action CSS replaced with unified .chip-action class. Go server: peerResponse and singlePeerResponse now return status as int (1=active, 0=disabled) instead of string, added status_text for admin panel backward compat. Fixes RustDesk client crash 'type String is not a subtype of type int?'. Go server: new handleUsersWithClientFallback — detects RustDesk client requests to /api/users and returns current user without requiring user.view permission. Fixes disappearing folders/groups caused by _getUsers() 403 short-circuiting _pull(). Node.js: normalisePeer updated to use status_text fallback for status_tier. Branding: RustDesk Server Management -> BetterDesk Server Management across themes, i18n, settings. Console version bumped to 3.0.0. This commit was made possible thanks to Insolve.
782 lines
44 KiB
HTML
782 lines
44 KiB
HTML
<%- include('layouts/main', {
|
|
title: _('nav.settings'),
|
|
pageStyles: ['pages'],
|
|
pageScripts: ['settings'],
|
|
currentPage: 'settings',
|
|
breadcrumb: [{ label: _('nav.settings') }],
|
|
body: `
|
|
<div class="page-header">
|
|
<h1 class="page-title">${_('settings.title')}</h1>
|
|
<p class="page-subtitle">${_('settings.subtitle')}</p>
|
|
</div>
|
|
|
|
<!-- Settings Tab Navigation -->
|
|
<div class="settings-tabs">
|
|
<button class="settings-tab active" data-tab="general">
|
|
<span class="material-icons">tune</span>
|
|
${_('settings.tab_general')}
|
|
</button>
|
|
<button class="settings-tab" data-tab="branding">
|
|
<span class="material-icons">palette</span>
|
|
${_('settings.tab_branding')}
|
|
</button>
|
|
${canServerConfig ? `<button class="settings-tab" data-tab="backup">
|
|
<span class="material-icons">backup</span>
|
|
${_('settings.tab_backup')}
|
|
</button>
|
|
<button class="settings-tab" data-tab="updates">
|
|
<span class="material-icons">system_update</span>
|
|
${_('updates.tab_title')}
|
|
</button>` : ''}
|
|
</div>
|
|
|
|
<!-- General Tab -->
|
|
<div class="settings-tab-content active" id="tab-general">
|
|
<div class="settings-sections">
|
|
<!-- Password Change Section -->
|
|
<div class="settings-section">
|
|
<div class="settings-section-header">
|
|
<h2 class="settings-section-title">
|
|
<span class="material-icons">lock</span>
|
|
${_('settings.change_password')}
|
|
</h2>
|
|
<p class="settings-section-desc">${_('settings.password_desc')}</p>
|
|
</div>
|
|
<div class="settings-section-body">
|
|
<form class="settings-form" id="password-form">
|
|
<div class="form-group">
|
|
<label class="form-label" for="current-password">${_('settings.current_password')}</label>
|
|
<input type="password" id="current-password" class="form-input" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label" for="new-password">${_('settings.new_password')}</label>
|
|
<input type="password" id="new-password" class="form-input" required minlength="8">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label" for="confirm-password">${_('settings.confirm_password')}</label>
|
|
<input type="password" id="confirm-password" class="form-input" required>
|
|
</div>
|
|
|
|
<div class="password-requirements">
|
|
<strong>${_('settings.password_requirements')}:</strong>
|
|
<ul>
|
|
<li id="req-length">${_('settings.req_length')}</li>
|
|
<li id="req-uppercase">${_('settings.req_uppercase')}</li>
|
|
<li id="req-lowercase">${_('settings.req_lowercase')}</li>
|
|
<li id="req-number">${_('settings.req_number')}</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<button type="submit" class="btn btn-primary">
|
|
<span class="material-icons">save</span>
|
|
${_('settings.save_password')}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Server Information Section -->
|
|
<div class="settings-section">
|
|
<div class="settings-section-header">
|
|
<h2 class="settings-section-title">
|
|
<span class="material-icons">info</span>
|
|
${_('settings.server_info')}
|
|
</h2>
|
|
</div>
|
|
<div class="settings-section-body">
|
|
<div class="server-info-grid">
|
|
<div class="server-info-item">
|
|
<span class="server-info-label">
|
|
<span class="material-icons">tag</span>
|
|
${_('settings.version')}
|
|
</span>
|
|
<span class="server-info-value">v${appVersion}</span>
|
|
</div>
|
|
<div class="server-info-item">
|
|
<span class="server-info-label">
|
|
<span class="material-icons">storage</span>
|
|
${_('settings.database')}
|
|
</span>
|
|
<span class="server-info-value" id="db-path">-</span>
|
|
</div>
|
|
<div class="server-info-item">
|
|
<span class="server-info-label">
|
|
<span class="material-icons">schedule</span>
|
|
${_('settings.uptime')}
|
|
</span>
|
|
<span class="server-info-value" id="uptime">-</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Two-Factor Authentication Section -->
|
|
<div class="settings-section">
|
|
<div class="settings-section-header">
|
|
<h2 class="settings-section-title">
|
|
<span class="material-icons">security</span>
|
|
${_('settings.totp_title')}
|
|
</h2>
|
|
<p class="settings-section-desc">${_('settings.totp_desc')}</p>
|
|
</div>
|
|
<div class="settings-section-body">
|
|
<div id="totp-status-container">
|
|
<div class="totp-status-loading">
|
|
<span class="material-icons spin">sync</span>
|
|
${_('common.loading')}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Audit Log Section -->
|
|
<div class="settings-section">
|
|
<div class="settings-section-header">
|
|
<h2 class="settings-section-title">
|
|
<span class="material-icons">history</span>
|
|
${_('settings.audit_log')}
|
|
</h2>
|
|
<p class="settings-section-desc">${_('settings.audit_desc')}</p>
|
|
</div>
|
|
<div class="settings-section-body">
|
|
<div class="audit-log-container">
|
|
<table class="audit-log-table">
|
|
<thead>
|
|
<tr>
|
|
<th>${_('audit.time')}</th>
|
|
<th>${_('audit.user')}</th>
|
|
<th>${_('audit.action')}</th>
|
|
<th>${_('audit.details')}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="audit-log-body">
|
|
<tr>
|
|
<td colspan="4" class="text-center text-muted">
|
|
${_('common.loading')}
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tutorials Section -->
|
|
<div class="settings-section">
|
|
<div class="settings-section-header">
|
|
<h2 class="settings-section-title">
|
|
<span class="material-icons">school</span>
|
|
${_('settings.tutorials_title')}
|
|
</h2>
|
|
<p class="settings-section-desc">${_('settings.tutorials_desc')}</p>
|
|
</div>
|
|
<div class="settings-section-body">
|
|
<div class="form-group" style="max-width: 600px; display: flex; align-items: center; gap: 12px;">
|
|
<input type="checkbox" id="tutorials-enabled" class="toggle-switch">
|
|
<div>
|
|
<label class="form-label" for="tutorials-enabled" style="margin-bottom: 0; cursor: pointer;">${_('settings.tutorials_enabled')}</label>
|
|
<small class="form-hint" style="display: block; margin-top: 2px;">${_('settings.tutorials_enabled_hint')}</small>
|
|
</div>
|
|
</div>
|
|
<div class="form-group" style="max-width: 600px;">
|
|
<button class="btn btn-secondary btn-sm" id="tutorials-reset-btn">
|
|
<span class="material-icons">refresh</span>
|
|
${_('tutorial.reset_all')}
|
|
</button>
|
|
<small class="form-hint" style="display: block; margin-top: 6px;">${_('settings.tutorials_reset_hint')}</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Branding Tab -->
|
|
<div class="settings-tab-content" id="tab-branding">
|
|
<div class="settings-sections">
|
|
<!-- Brand Identity -->
|
|
<div class="settings-section">
|
|
<div class="settings-section-header">
|
|
<h2 class="settings-section-title">
|
|
<span class="material-icons">badge</span>
|
|
${_('branding.identity_title')}
|
|
</h2>
|
|
<p class="settings-section-desc">${_('branding.identity_desc')}</p>
|
|
</div>
|
|
<div class="settings-section-body">
|
|
<form class="settings-form branding-form" id="branding-identity-form" style="max-width: 600px;">
|
|
<div class="form-group">
|
|
<label class="form-label" for="brand-name">${_('branding.app_name')}</label>
|
|
<input type="text" id="brand-name" class="form-input" maxlength="50"
|
|
placeholder="BetterDesk">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label" for="brand-description">${_('branding.app_description')}</label>
|
|
<input type="text" id="brand-description" class="form-input" maxlength="100"
|
|
placeholder="BetterDesk Server Management">
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Logo Configuration -->
|
|
<div class="settings-section">
|
|
<div class="settings-section-header">
|
|
<h2 class="settings-section-title">
|
|
<span class="material-icons">image</span>
|
|
${_('branding.logo_title')}
|
|
</h2>
|
|
<p class="settings-section-desc">${_('branding.logo_desc')}</p>
|
|
</div>
|
|
<div class="settings-section-body">
|
|
<div class="branding-logo-config">
|
|
<div class="logo-type-selector">
|
|
<label class="logo-type-option">
|
|
<input type="radio" name="logo-type" value="text">
|
|
<span class="logo-type-card">
|
|
<span class="material-icons">title</span>
|
|
<span>${_('branding.logo_type_text')}</span>
|
|
</span>
|
|
</label>
|
|
<label class="logo-type-option">
|
|
<input type="radio" name="logo-type" value="icon">
|
|
<span class="logo-type-card">
|
|
<span class="material-icons">text_fields</span>
|
|
<span>${_('branding.logo_type_icon')}</span>
|
|
</span>
|
|
</label>
|
|
<label class="logo-type-option">
|
|
<input type="radio" name="logo-type" value="svg">
|
|
<span class="logo-type-card">
|
|
<span class="material-icons">code</span>
|
|
<span>${_('branding.logo_type_svg')}</span>
|
|
</span>
|
|
</label>
|
|
<label class="logo-type-option">
|
|
<input type="radio" name="logo-type" value="image">
|
|
<span class="logo-type-card">
|
|
<span class="material-icons">photo</span>
|
|
<span>${_('branding.logo_type_image')}</span>
|
|
</span>
|
|
</label>
|
|
</div>
|
|
|
|
<!-- Text Logo options -->
|
|
<div class="logo-config-panel hidden" id="logo-text-panel">
|
|
<div class="form-group">
|
|
<label class="form-label" for="logo-text-input">${_('branding.logo_text')}</label>
|
|
<input type="text" id="logo-text-input" class="form-input"
|
|
placeholder="insolve" maxlength="30">
|
|
<small class="form-hint">${_('branding.logo_text_hint')}</small>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label" for="logo-text-accent">${_('branding.logo_text_accent')}</label>
|
|
<input type="text" id="logo-text-accent" class="form-input"
|
|
placeholder="Core Server" maxlength="30">
|
|
<small class="form-hint">${_('branding.logo_text_accent_hint')}</small>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Icon options -->
|
|
<div class="logo-config-panel" id="logo-icon-panel">
|
|
<div class="form-group">
|
|
<label class="form-label" for="logo-icon-name">${_('branding.logo_icon_name')}</label>
|
|
<input type="text" id="logo-icon-name" class="form-input"
|
|
placeholder="dns" maxlength="50">
|
|
<small class="form-hint">${_('branding.logo_icon_hint')}</small>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- SVG options -->
|
|
<div class="logo-config-panel hidden" id="logo-svg-panel">
|
|
<div class="form-group">
|
|
<label class="form-label" for="logo-svg-input">${_('branding.logo_svg_code')}</label>
|
|
<textarea id="logo-svg-input" class="form-input form-textarea" rows="6"
|
|
placeholder="<svg>...</svg>"></textarea>
|
|
<small class="form-hint">${_('branding.logo_svg_hint')}</small>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Image options -->
|
|
<div class="logo-config-panel hidden" id="logo-image-panel">
|
|
<div class="form-group">
|
|
<label class="form-label">${_('branding.logo_image_upload')}</label>
|
|
<div class="logo-upload-area">
|
|
<input type="file" id="logo-image-file" accept="image/png,image/jpeg,image/gif,image/webp,image/svg+xml" class="logo-file-input">
|
|
<label for="logo-image-file" class="logo-upload-btn">
|
|
<span class="material-icons">upload_file</span>
|
|
<span>${_('branding.logo_image_choose')}</span>
|
|
</label>
|
|
<span id="logo-file-name" class="logo-file-name"></span>
|
|
</div>
|
|
<small class="form-hint">${_('branding.logo_image_hint')}</small>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label" for="logo-image-url">${_('branding.logo_image_url')}</label>
|
|
<input type="text" id="logo-image-url" class="form-input"
|
|
placeholder="https://example.com/logo.png">
|
|
<small class="form-hint">${_('branding.logo_image_url_hint')}</small>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Logo preview -->
|
|
<div class="branding-preview-box">
|
|
<label class="form-label">${_('branding.preview')}</label>
|
|
<div class="logo-preview" id="logo-preview">
|
|
<span class="material-icons">dns</span>
|
|
<span class="logo-preview-text">BetterDesk</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Color Scheme -->
|
|
<div class="settings-section">
|
|
<div class="settings-section-header">
|
|
<h2 class="settings-section-title">
|
|
<span class="material-icons">palette</span>
|
|
${_('branding.colors_title')}
|
|
</h2>
|
|
<p class="settings-section-desc">${_('branding.colors_desc')}</p>
|
|
</div>
|
|
<div class="settings-section-body">
|
|
<div class="color-grid">
|
|
<div class="color-group">
|
|
<h3 class="color-group-title">${_('branding.colors_backgrounds')}</h3>
|
|
<div class="color-row">
|
|
<label class="form-label">Primary</label>
|
|
<div class="color-input-wrapper">
|
|
<input type="color" class="color-picker" data-color="bgPrimary" value="#0d1117">
|
|
<input type="text" class="form-input color-hex" data-color="bgPrimary"
|
|
placeholder="#0d1117" maxlength="7">
|
|
</div>
|
|
</div>
|
|
<div class="color-row">
|
|
<label class="form-label">Secondary</label>
|
|
<div class="color-input-wrapper">
|
|
<input type="color" class="color-picker" data-color="bgSecondary" value="#161b22">
|
|
<input type="text" class="form-input color-hex" data-color="bgSecondary"
|
|
placeholder="#161b22" maxlength="7">
|
|
</div>
|
|
</div>
|
|
<div class="color-row">
|
|
<label class="form-label">Tertiary</label>
|
|
<div class="color-input-wrapper">
|
|
<input type="color" class="color-picker" data-color="bgTertiary" value="#21262d">
|
|
<input type="text" class="form-input color-hex" data-color="bgTertiary"
|
|
placeholder="#21262d" maxlength="7">
|
|
</div>
|
|
</div>
|
|
<div class="color-row">
|
|
<label class="form-label">Elevated</label>
|
|
<div class="color-input-wrapper">
|
|
<input type="color" class="color-picker" data-color="bgElevated" value="#30363d">
|
|
<input type="text" class="form-input color-hex" data-color="bgElevated"
|
|
placeholder="#30363d" maxlength="7">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="color-group">
|
|
<h3 class="color-group-title">${_('branding.colors_text')}</h3>
|
|
<div class="color-row">
|
|
<label class="form-label">Primary</label>
|
|
<div class="color-input-wrapper">
|
|
<input type="color" class="color-picker" data-color="textPrimary" value="#e6edf3">
|
|
<input type="text" class="form-input color-hex" data-color="textPrimary"
|
|
placeholder="#e6edf3" maxlength="7">
|
|
</div>
|
|
</div>
|
|
<div class="color-row">
|
|
<label class="form-label">Secondary</label>
|
|
<div class="color-input-wrapper">
|
|
<input type="color" class="color-picker" data-color="textSecondary" value="#8b949e">
|
|
<input type="text" class="form-input color-hex" data-color="textSecondary"
|
|
placeholder="#8b949e" maxlength="7">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="color-group">
|
|
<h3 class="color-group-title">${_('branding.colors_accents')}</h3>
|
|
<div class="color-row">
|
|
<label class="form-label">Blue (Primary Accent)</label>
|
|
<div class="color-input-wrapper">
|
|
<input type="color" class="color-picker" data-color="accentBlue" value="#58a6ff">
|
|
<input type="text" class="form-input color-hex" data-color="accentBlue"
|
|
placeholder="#58a6ff" maxlength="7">
|
|
</div>
|
|
</div>
|
|
<div class="color-row">
|
|
<label class="form-label">Green (Success)</label>
|
|
<div class="color-input-wrapper">
|
|
<input type="color" class="color-picker" data-color="accentGreen" value="#2ea44f">
|
|
<input type="text" class="form-input color-hex" data-color="accentGreen"
|
|
placeholder="#2ea44f" maxlength="7">
|
|
</div>
|
|
</div>
|
|
<div class="color-row">
|
|
<label class="form-label">Red (Danger)</label>
|
|
<div class="color-input-wrapper">
|
|
<input type="color" class="color-picker" data-color="accentRed" value="#f85149">
|
|
<input type="text" class="form-input color-hex" data-color="accentRed"
|
|
placeholder="#f85149" maxlength="7">
|
|
</div>
|
|
</div>
|
|
<div class="color-row">
|
|
<label class="form-label">Yellow (Warning)</label>
|
|
<div class="color-input-wrapper">
|
|
<input type="color" class="color-picker" data-color="accentYellow" value="#d29922">
|
|
<input type="text" class="form-input color-hex" data-color="accentYellow"
|
|
placeholder="#d29922" maxlength="7">
|
|
</div>
|
|
</div>
|
|
<div class="color-row">
|
|
<label class="form-label">Purple</label>
|
|
<div class="color-input-wrapper">
|
|
<input type="color" class="color-picker" data-color="accentPurple" value="#a371f7">
|
|
<input type="text" class="form-input color-hex" data-color="accentPurple"
|
|
placeholder="#a371f7" maxlength="7">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="color-group">
|
|
<h3 class="color-group-title">${_('branding.colors_borders')}</h3>
|
|
<div class="color-row">
|
|
<label class="form-label">Primary</label>
|
|
<div class="color-input-wrapper">
|
|
<input type="color" class="color-picker" data-color="borderPrimary" value="#30363d">
|
|
<input type="text" class="form-input color-hex" data-color="borderPrimary"
|
|
placeholder="#30363d" maxlength="7">
|
|
</div>
|
|
</div>
|
|
<div class="color-row">
|
|
<label class="form-label">Secondary</label>
|
|
<div class="color-input-wrapper">
|
|
<input type="color" class="color-picker" data-color="borderSecondary" value="#21262d">
|
|
<input type="text" class="form-input color-hex" data-color="borderSecondary"
|
|
placeholder="#21262d" maxlength="7">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Typography -->
|
|
<div class="settings-section">
|
|
<div class="settings-section-header">
|
|
<h2 class="settings-section-title">
|
|
<span class="material-icons">text_format</span>
|
|
${_('branding.typography_title')}
|
|
</h2>
|
|
<p class="settings-section-desc">${_('branding.typography_desc')}</p>
|
|
</div>
|
|
<div class="settings-section-body">
|
|
<div class="font-config">
|
|
<!-- Heading Font -->
|
|
<div class="font-slot" id="font-heading-slot">
|
|
<div class="font-slot-header">
|
|
<h3 class="font-slot-title">${_('branding.font_heading')}</h3>
|
|
<small class="form-hint">${_('branding.font_heading_hint')}</small>
|
|
</div>
|
|
<div class="font-picker-row">
|
|
<div class="font-search-wrapper">
|
|
<input type="text" id="font-heading-search" class="form-input font-search-input"
|
|
placeholder="${_('branding.font_search_placeholder')}" autocomplete="off">
|
|
<div class="font-dropdown hidden" id="font-heading-dropdown"></div>
|
|
</div>
|
|
<input type="hidden" id="font-heading-value">
|
|
<span class="font-current" id="font-heading-current">${_('branding.font_system_default')}</span>
|
|
<button class="btn btn-ghost btn-sm font-clear-btn hidden" id="font-heading-clear" title="${_('branding.font_clear')}">
|
|
<span class="material-icons">close</span>
|
|
</button>
|
|
</div>
|
|
<div class="font-preview" id="font-heading-preview">
|
|
<span class="font-preview-text" style="font-size: 1.5rem; font-weight: 600;">The quick brown fox jumps over the lazy dog</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Body Font -->
|
|
<div class="font-slot" id="font-body-slot">
|
|
<div class="font-slot-header">
|
|
<h3 class="font-slot-title">${_('branding.font_body')}</h3>
|
|
<small class="form-hint">${_('branding.font_body_hint')}</small>
|
|
</div>
|
|
<div class="font-picker-row">
|
|
<div class="font-search-wrapper">
|
|
<input type="text" id="font-body-search" class="form-input font-search-input"
|
|
placeholder="${_('branding.font_search_placeholder')}" autocomplete="off">
|
|
<div class="font-dropdown hidden" id="font-body-dropdown"></div>
|
|
</div>
|
|
<input type="hidden" id="font-body-value">
|
|
<span class="font-current" id="font-body-current">${_('branding.font_system_default')}</span>
|
|
<button class="btn btn-ghost btn-sm font-clear-btn hidden" id="font-body-clear" title="${_('branding.font_clear')}">
|
|
<span class="material-icons">close</span>
|
|
</button>
|
|
</div>
|
|
<div class="font-preview" id="font-body-preview">
|
|
<span class="font-preview-text">The quick brown fox jumps over the lazy dog. 0123456789</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Category filter -->
|
|
<div class="font-category-filter">
|
|
<button class="btn btn-ghost btn-sm font-cat-btn active" data-category="all">${_('branding.font_cat_all')}</button>
|
|
<button class="btn btn-ghost btn-sm font-cat-btn" data-category="sans-serif">Sans-Serif</button>
|
|
<button class="btn btn-ghost btn-sm font-cat-btn" data-category="serif">Serif</button>
|
|
<button class="btn btn-ghost btn-sm font-cat-btn" data-category="monospace">Mono</button>
|
|
<button class="btn btn-ghost btn-sm font-cat-btn" data-category="display">Display</button>
|
|
</div>
|
|
|
|
<!-- Downloaded fonts info -->
|
|
<div class="font-local-info" id="font-local-info">
|
|
<span class="material-icons">folder</span>
|
|
<span id="font-local-count">0</span> ${_('branding.font_downloaded_count')}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Import / Export -->
|
|
<div class="settings-section">
|
|
<div class="settings-section-header">
|
|
<h2 class="settings-section-title">
|
|
<span class="material-icons">swap_horiz</span>
|
|
${_('branding.import_export_title')}
|
|
</h2>
|
|
<p class="settings-section-desc">${_('branding.import_export_desc')}</p>
|
|
</div>
|
|
<div class="settings-section-body">
|
|
<div class="branding-actions-row">
|
|
<button class="btn btn-secondary" id="branding-export-btn">
|
|
<span class="material-icons">download</span>
|
|
${_('branding.export_theme')}
|
|
</button>
|
|
<label class="btn btn-secondary">
|
|
<span class="material-icons">upload</span>
|
|
${_('branding.import_theme')}
|
|
<input type="file" id="branding-import-input" accept=".json" hidden>
|
|
</label>
|
|
<button class="btn btn-danger" id="branding-reset-btn">
|
|
<span class="material-icons">restart_alt</span>
|
|
${_('branding.reset_defaults')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Save Branding Button -->
|
|
<div class="branding-save-bar">
|
|
<button class="btn btn-primary btn-lg" id="branding-save-btn">
|
|
<span class="material-icons">save</span>
|
|
${_('branding.save_branding')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Backup & Restore Tab (server.config permission required) -->
|
|
<div class="settings-tab-content" id="tab-backup" ${canServerConfig ? '' : 'style="display:none"'}>
|
|
<div class="settings-sections">
|
|
<!-- Backup Info -->
|
|
<div class="settings-section">
|
|
<div class="settings-section-header">
|
|
<h2 class="settings-section-title">
|
|
<span class="material-icons">backup</span>
|
|
${_('backup.title')}
|
|
</h2>
|
|
<p class="settings-section-desc">${_('backup.desc')}</p>
|
|
</div>
|
|
<div class="settings-section-body">
|
|
<div class="backup-stats" id="backup-stats">
|
|
<div class="backup-stat-row">
|
|
<span class="backup-stat-label">${_('backup.stat_users')}</span>
|
|
<span class="backup-stat-value" id="backup-stat-users">-</span>
|
|
</div>
|
|
<div class="backup-stat-row">
|
|
<span class="backup-stat-label">${_('backup.stat_settings')}</span>
|
|
<span class="backup-stat-value" id="backup-stat-settings">-</span>
|
|
</div>
|
|
<div class="backup-stat-row">
|
|
<span class="backup-stat-label">${_('backup.stat_folders')}</span>
|
|
<span class="backup-stat-value" id="backup-stat-folders">-</span>
|
|
</div>
|
|
<div class="backup-stat-row">
|
|
<span class="backup-stat-label">${_('backup.stat_groups')}</span>
|
|
<span class="backup-stat-value" id="backup-stat-groups">-</span>
|
|
</div>
|
|
<div class="backup-stat-row">
|
|
<span class="backup-stat-label">${_('backup.stat_strategies')}</span>
|
|
<span class="backup-stat-value" id="backup-stat-strategies">-</span>
|
|
</div>
|
|
<div class="backup-stat-row">
|
|
<span class="backup-stat-label">${_('backup.stat_backend')}</span>
|
|
<span class="backup-stat-value" id="backup-stat-backend">-</span>
|
|
</div>
|
|
</div>
|
|
<div class="backup-actions">
|
|
<button class="btn btn-primary" id="backup-download-btn">
|
|
<span class="material-icons">download</span>
|
|
${_('backup.download')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Restore -->
|
|
<div class="settings-section">
|
|
<div class="settings-section-header">
|
|
<h2 class="settings-section-title">
|
|
<span class="material-icons">restore</span>
|
|
${_('backup.restore_title')}
|
|
</h2>
|
|
<p class="settings-section-desc">${_('backup.restore_desc')}</p>
|
|
</div>
|
|
<div class="settings-section-body">
|
|
<div class="restore-options">
|
|
<label class="restore-option">
|
|
<input type="checkbox" id="restore-settings" checked>
|
|
<span>${_('backup.opt_settings')}</span>
|
|
</label>
|
|
<label class="restore-option">
|
|
<input type="checkbox" id="restore-branding" checked>
|
|
<span>${_('backup.opt_branding')}</span>
|
|
</label>
|
|
<label class="restore-option">
|
|
<input type="checkbox" id="restore-folders" checked>
|
|
<span>${_('backup.opt_folders')}</span>
|
|
</label>
|
|
<label class="restore-option">
|
|
<input type="checkbox" id="restore-groups" checked>
|
|
<span>${_('backup.opt_groups')}</span>
|
|
</label>
|
|
<label class="restore-option">
|
|
<input type="checkbox" id="restore-addressbooks" checked>
|
|
<span>${_('backup.opt_addressbooks')}</span>
|
|
</label>
|
|
<label class="restore-option restore-option-danger">
|
|
<input type="checkbox" id="restore-users">
|
|
<span>${_('backup.opt_users')}</span>
|
|
<small class="restore-warning">${_('backup.opt_users_warning')}</small>
|
|
</label>
|
|
</div>
|
|
<div class="backup-actions">
|
|
<label class="btn btn-warning" id="restore-upload-label">
|
|
<span class="material-icons">upload</span>
|
|
${_('backup.upload')}
|
|
<input type="file" id="restore-file-input" accept=".json" hidden>
|
|
</label>
|
|
</div>
|
|
<div class="restore-result" id="restore-result" style="display:none;"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Updates Tab (server.config permission required) -->
|
|
<div class="settings-tab-content" id="tab-updates" ${canServerConfig ? '' : 'style="display:none"'}>
|
|
<div class="settings-sections">
|
|
<!-- Version Check -->
|
|
<div class="settings-section">
|
|
<div class="settings-section-header">
|
|
<h2 class="settings-section-title">
|
|
<span class="material-icons">system_update</span>
|
|
${_('updates.title')}
|
|
</h2>
|
|
<p class="settings-section-desc">${_('updates.desc')}</p>
|
|
</div>
|
|
<div class="settings-section-body">
|
|
<div class="update-version-info">
|
|
<div class="update-version-row">
|
|
<span class="update-label">${_('updates.current_version')}</span>
|
|
<span class="update-value" id="update-local-version">v${appVersion}</span>
|
|
</div>
|
|
<div class="update-version-row">
|
|
<span class="update-label">${_('updates.latest_version')}</span>
|
|
<span class="update-value" id="update-remote-version">
|
|
<span class="text-muted">—</span>
|
|
</span>
|
|
</div>
|
|
<div class="update-version-row" id="update-status-row" style="display:none;">
|
|
<span class="update-label">${_('updates.status')}</span>
|
|
<span class="update-value" id="update-status-badge"></span>
|
|
</div>
|
|
</div>
|
|
<div class="update-actions" style="margin-top: 16px;">
|
|
<button class="btn btn-primary" id="update-check-btn">
|
|
<span class="material-icons">refresh</span>
|
|
${_('updates.check_now')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Update Details (hidden until check completes) -->
|
|
<div class="settings-section" id="update-details-section" style="display:none;">
|
|
<div class="settings-section-header">
|
|
<h2 class="settings-section-title">
|
|
<span class="material-icons">description</span>
|
|
${_('updates.changelog')}
|
|
</h2>
|
|
</div>
|
|
<div class="settings-section-body">
|
|
<div class="update-changelog" id="update-changelog"></div>
|
|
<div class="update-files-summary" id="update-files-summary" style="margin-top:12px;"></div>
|
|
<div class="update-actions" style="margin-top: 16px;">
|
|
<label class="restore-option" style="margin-bottom: 12px;">
|
|
<input type="checkbox" id="update-backup-toggle" checked>
|
|
<span>${_('updates.create_backup')}</span>
|
|
</label>
|
|
<button class="btn btn-primary btn-lg" id="update-install-btn" disabled>
|
|
<span class="material-icons">download</span>
|
|
${_('updates.install')}
|
|
</button>
|
|
</div>
|
|
<div class="update-progress" id="update-progress" style="display:none;">
|
|
<div class="update-progress-bar">
|
|
<div class="update-progress-fill" id="update-progress-fill"></div>
|
|
</div>
|
|
<p class="update-progress-text" id="update-progress-text">${_('updates.installing')}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Update Backups -->
|
|
<div class="settings-section">
|
|
<div class="settings-section-header">
|
|
<h2 class="settings-section-title">
|
|
<span class="material-icons">history</span>
|
|
${_('updates.backups_title')}
|
|
</h2>
|
|
<p class="settings-section-desc">${_('updates.backups_desc')}</p>
|
|
</div>
|
|
<div class="settings-section-body">
|
|
<div class="backup-retention-row">
|
|
<label for="backup-retention-input" class="backup-retention-label">
|
|
${_('updates.retention_label')}
|
|
<small class="text-muted">${_('updates.retention_hint')}</small>
|
|
</label>
|
|
<div class="backup-retention-controls">
|
|
<input type="number" id="backup-retention-input" min="0" max="1000" step="1" value="0">
|
|
<button class="btn btn-sm btn-primary" id="backup-retention-save">
|
|
${_('common.save')}
|
|
</button>
|
|
<button class="btn btn-sm btn-outline" id="backup-prune-now">
|
|
<span class="material-icons">cleaning_services</span>
|
|
${_('updates.prune_now')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="backup-summary text-muted" id="backup-summary"></div>
|
|
<div id="update-backups-list">
|
|
<p class="text-muted">${_('common.loading')}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`
|
|
}) %>
|