mirror of
https://github.com/MarekWo/mc-webui.git
synced 2026-10-30 23:51:57 +01:00
The Interface tab carried data-demo-lock as a whole, which greyed out the sidebar layout breakpoint along with the toast settings. The breakpoint is localStorage — per browser, invisible to every other visitor — so it belongs with theme, notification permission and the quick-access bar, all of which were already allowed. The toast settings are server-side and shared, so the lock moves onto the uiSettingsForm that owns them. Audited every other locked region against the localStorage key list: this was the only per-browser control caught by a lock. Notifications, theme, FAB placement/size/position and the sidebar breakpoint are now the complete set of preferences a demo visitor may change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1700 lines
118 KiB
HTML
1700 lines
118 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="{{ lang }}" data-theme="light" data-bs-theme="light">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
|
<title>{% block title %}mc-webui{% endblock %}</title>
|
|
|
|
<!-- Favicon -->
|
|
<link rel="apple-touch-icon" sizes="180x180" href="{{ url_for('static', filename='images/apple-touch-icon.png') }}">
|
|
<link rel="icon" type="image/png" sizes="32x32" href="{{ url_for('static', filename='images/favicon-32x32.png') }}">
|
|
<link rel="icon" type="image/png" sizes="16x16" href="{{ url_for('static', filename='images/favicon-16x16.png') }}">
|
|
<link rel="manifest" href="{{ url_for('static', filename='manifest.json') }}">
|
|
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
|
|
|
<!-- Theme: apply saved preference before CSS loads to prevent flash -->
|
|
<script>
|
|
(function() {
|
|
var theme = localStorage.getItem('mc-webui-theme') || 'light';
|
|
document.documentElement.setAttribute('data-theme', theme);
|
|
document.documentElement.setAttribute('data-bs-theme', theme);
|
|
})();
|
|
</script>
|
|
|
|
<!-- Sidebar breakpoint: apply saved preference before CSS loads to prevent layout flash -->
|
|
<script>
|
|
(function() {
|
|
try {
|
|
var stored = parseInt(localStorage.getItem('mc-webui-sidebar-breakpoint'), 10);
|
|
var bp = (isNaN(stored) || stored < 600 || stored > 2000) ? 992 : stored;
|
|
if (window.innerWidth >= bp) {
|
|
document.documentElement.classList.add('layout-wide');
|
|
}
|
|
} catch (e) {}
|
|
})();
|
|
</script>
|
|
|
|
{% include "_head_i18n.html" %}
|
|
|
|
<!-- Bootstrap 5 CSS (local) -->
|
|
<link href="{{ url_for('static', filename='vendor/bootstrap/css/bootstrap.min.css') }}" rel="stylesheet">
|
|
<!-- Bootstrap Icons (local) -->
|
|
<link rel="stylesheet" href="{{ url_for('static', filename='vendor/bootstrap-icons/bootstrap-icons.css') }}">
|
|
|
|
<!-- Leaflet CSS -->
|
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
|
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
|
|
crossorigin="" />
|
|
|
|
<!-- Custom CSS -->
|
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
|
<!-- Theme CSS (light/dark mode) -->
|
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/theme.css') }}">
|
|
|
|
{% block extra_head %}{% endblock %}
|
|
</head>
|
|
<body>
|
|
<!-- Navbar -->
|
|
<nav class="navbar navbar-dark bg-primary">
|
|
<div class="container-fluid">
|
|
<span class="navbar-brand mb-0 h1">
|
|
<i class="bi bi-broadcast"></i> mc-webui
|
|
{% if device_name %}
|
|
<small class="text-white-50 d-none d-sm-inline">- {{ device_name }}</small>
|
|
{% endif %}
|
|
{% if transport_type == 'ble' %}
|
|
<span class="badge bg-info ms-1 d-none d-sm-inline" title="{{ t('nav.ble_title') }}">BLE</span>
|
|
{% elif transport_type == 'tcp' %}
|
|
<span class="badge bg-warning text-dark ms-1 d-none d-sm-inline" title="{{ t('nav.tcp_title') }}">TCP</span>
|
|
{% endif %}
|
|
</span>
|
|
<div class="d-flex align-items-center gap-2">
|
|
<div id="notificationBell" class="btn btn-outline-light position-relative navbar-touch-btn" style="cursor: pointer;" onclick="markAllChannelsRead()" title="{{ t('nav.mark_all_read_title') }}">
|
|
<i class="bi bi-bell"></i>
|
|
</div>
|
|
<div class="position-relative channel-mobile-selector" id="channelSelectorWrapper" style="min-width: 80px; max-width: 140px;">
|
|
{# A button, not a text input: a text field pops the on-screen
|
|
keyboard open on every channel switch, and the firmware's
|
|
channel limit makes searching them pointless anyway. #}
|
|
<button type="button"
|
|
id="channelSelectorButton"
|
|
class="form-select navbar-touch-select text-start text-truncate"
|
|
title="{{ t('nav.select_channel_title') }}"
|
|
aria-haspopup="listbox"
|
|
aria-expanded="false">Public</button>
|
|
<div id="channelSelectorDropdown" class="channel-selector-dropdown" style="display: none;"></div>
|
|
</div>
|
|
<button class="btn btn-outline-light navbar-touch-btn" data-bs-toggle="offcanvas" data-bs-target="#mainMenu" title="{{ t('menu.title') }}">
|
|
<i class="bi bi-list"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<!-- Offcanvas Menu -->
|
|
<div class="offcanvas offcanvas-end" tabindex="-1" id="mainMenu">
|
|
<div class="offcanvas-header">
|
|
<h5 class="offcanvas-title"><i class="bi bi-menu-button-wide"></i> {{ t('menu.title') }}</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="offcanvas"></button>
|
|
</div>
|
|
<div class="px-3 pb-2 text-muted small border-bottom">
|
|
<div class="d-flex align-items-center justify-content-between">
|
|
<span id="versionDisplay">
|
|
<i class="bi bi-tag"></i> <span id="releaseText" class="fw-semibold">{{ release }}</span>
|
|
<span class="badge bg-secondary ms-1" id="branchBadge">{{ git_branch }}</span>{% if demo_mode %}<span class="badge bg-warning text-dark ms-1 demo-badge" id="demoBadge">{{ t('demo.badge') }}</span>{% endif %}
|
|
<!-- Exact build: what pins a deployment down, and what the
|
|
update poller compares - keep it as its own element -->
|
|
<span id="versionText" class="d-block opacity-75" title="{{ t('menu.exact_build_title') }}">{{ version }}</span>
|
|
</span>
|
|
<button id="checkUpdateBtn" data-demo-lock class="btn btn-sm btn-outline-secondary py-0 px-1" title="{{ t('menu.check_updates_title') }}">
|
|
<i class="bi bi-arrow-repeat" id="checkUpdateIcon"></i>
|
|
</button>
|
|
</div>
|
|
<div id="updateLinkContainer" class="d-none mt-1"></div>
|
|
</div>
|
|
<div class="offcanvas-body">
|
|
<div class="list-group list-group-flush">
|
|
<!-- Messages -->
|
|
<!-- The Android wrapper has no pull-to-refresh of its own, so
|
|
this is the manual way out of a list that fell behind -->
|
|
<button id="refreshBtn" class="list-group-item list-group-item-action d-flex align-items-center gap-3" type="button"
|
|
data-bs-dismiss="offcanvas" title="{{ t('menu.refresh_title') }}">
|
|
<i class="bi bi-arrow-clockwise" style="font-size: 1.5rem;"></i>
|
|
<div class="flex-grow-1">
|
|
<div>{{ t('common.refresh') }}</div>
|
|
<small class="d-block text-muted">{{ t('menu.refresh_desc') }}</small>
|
|
</div>
|
|
</button>
|
|
<button id="menu-filter" class="list-group-item list-group-item-action d-flex align-items-center gap-3 d-none" type="button">
|
|
<i class="bi bi-funnel" style="font-size: 1.5rem;"></i>
|
|
<div class="flex-grow-1">
|
|
<div>{{ t('menu.filter') }}</div>
|
|
<small class="d-block text-muted">{{ t('menu.filter_desc') }}</small>
|
|
</div>
|
|
</button>
|
|
<button id="menu-search" class="list-group-item list-group-item-action d-flex align-items-center gap-3 d-none" type="button"
|
|
data-bs-toggle="modal" data-bs-target="#searchModal" data-bs-dismiss="offcanvas">
|
|
<i class="bi bi-search" style="font-size: 1.5rem;"></i>
|
|
<div class="flex-grow-1">
|
|
<div>{{ t('menu.search') }}</div>
|
|
<small class="d-block text-muted">{{ t('menu.search_desc') }}</small>
|
|
</div>
|
|
</button>
|
|
<button id="menu-dm" class="list-group-item list-group-item-action d-flex align-items-center gap-3 d-none" type="button"
|
|
data-bs-toggle="modal" data-bs-target="#dmModal" data-bs-dismiss="offcanvas">
|
|
<i class="bi bi-envelope-fill" style="font-size: 1.5rem;"></i>
|
|
<div class="flex-grow-1">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<span>{{ t('dm.title') }}</span>
|
|
<span class="badge rounded-pill bg-danger d-none menu-badge-dm">0</span>
|
|
</div>
|
|
<small class="d-block text-muted">{{ t('menu.dm_desc') }}</small>
|
|
</div>
|
|
</button>
|
|
<button id="menu-contacts" class="list-group-item list-group-item-action d-flex align-items-center gap-3 d-none" type="button"
|
|
data-bs-toggle="modal" data-bs-target="#contactsModal" data-bs-dismiss="offcanvas">
|
|
<i class="bi bi-person-lines-fill" style="font-size: 1.5rem;"></i>
|
|
<div class="flex-grow-1">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<span>{{ t('contacts.title') }}</span>
|
|
<span class="badge rounded-pill bg-warning d-none menu-badge-contacts">0</span>
|
|
</div>
|
|
<small class="d-block text-muted">{{ t('menu.contacts_desc') }}</small>
|
|
</div>
|
|
</button>
|
|
<button class="list-group-item list-group-item-action d-flex align-items-center gap-3" data-bs-toggle="modal" data-bs-target="#channelsModal" data-bs-dismiss="offcanvas">
|
|
<i class="bi bi-broadcast-pin" style="font-size: 1.5rem;"></i>
|
|
<span>{{ t('menu.channels') }}</span>
|
|
</button>
|
|
<div class="list-group-item">
|
|
<div class="d-flex align-items-center gap-3 mb-2">
|
|
<i class="bi bi-calendar3" style="font-size: 1.5rem;"></i>
|
|
<label for="dateSelector" class="form-label mb-0">{{ t('menu.history') }}</label>
|
|
</div>
|
|
<select id="dateSelector" class="form-select" title="{{ t('menu.select_date_title') }}">
|
|
<option value="">{{ t('menu.today_live') }}</option>
|
|
<!-- Archive dates loaded dynamically via JavaScript -->
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Network -->
|
|
<div class="list-group-item py-2 mt-2">
|
|
<small class="text-muted fw-bold text-uppercase">{{ t('menu.section.network') }}</small>
|
|
</div>
|
|
<button class="list-group-item list-group-item-action d-flex align-items-center gap-3" id="advertBtn" data-demo-lock title="{{ t('menu.advert_title') }}">
|
|
<i class="bi bi-megaphone" style="font-size: 1.5rem;"></i>
|
|
<div>
|
|
<span>{{ t('menu.advert') }}</span>
|
|
<small class="d-block text-muted">{{ t('menu.advert_desc') }}</small>
|
|
</div>
|
|
</button>
|
|
<button class="list-group-item list-group-item-action d-flex align-items-center gap-3 text-warning" id="floodadvBtn" data-demo-lock title="{{ t('menu.flood_advert_title') }}">
|
|
<i class="bi bi-broadcast" style="font-size: 1.5rem;"></i>
|
|
<div>
|
|
<span>{{ t('menu.flood_advert') }}</span>
|
|
<small class="d-block text-muted">{{ t('menu.flood_advert_desc') }}</small>
|
|
</div>
|
|
</button>
|
|
|
|
<!-- Tools -->
|
|
<div class="list-group-item py-2 mt-2">
|
|
<small class="text-muted fw-bold text-uppercase">{{ t('menu.section.tools') }}</small>
|
|
</div>
|
|
<button class="list-group-item list-group-item-action d-flex align-items-center gap-3"
|
|
id="mapBtn" title="{{ t('menu.map_title') }}">
|
|
<i class="bi bi-map" style="font-size: 1.5rem;"></i>
|
|
<div>
|
|
<span>{{ t('menu.map') }}</span>
|
|
<small class="d-block text-muted">{{ t('menu.map_desc') }}</small>
|
|
</div>
|
|
</button>
|
|
<button id="consoleBtn" class="list-group-item list-group-item-action d-flex align-items-center gap-3" data-bs-toggle="modal" data-bs-target="#consoleModal" data-bs-dismiss="offcanvas">
|
|
<i class="bi bi-terminal" style="font-size: 1.5rem;"></i>
|
|
<div>
|
|
<span>{{ t('console.title') }}</span>
|
|
<small class="d-block text-muted">{{ t('menu.console_desc') }}</small>
|
|
</div>
|
|
</button>
|
|
<button id="repeatersBtn" class="list-group-item list-group-item-action d-flex align-items-center gap-3" data-bs-toggle="modal" data-bs-target="#repeatersModal" data-bs-dismiss="offcanvas">
|
|
<i class="bi bi-diagram-3" style="font-size: 1.5rem;"></i>
|
|
<div>
|
|
<span>{{ t('repeaters.title') }}</span>
|
|
<small class="d-block text-muted">{{ t('menu.repeaters_desc') }}</small>
|
|
</div>
|
|
</button>
|
|
<button id="pathAnalyzerBtn" class="list-group-item list-group-item-action d-flex align-items-center gap-3" data-bs-toggle="modal" data-bs-target="#pathAnalyzerModal" data-bs-dismiss="offcanvas">
|
|
<i class="bi bi-signpost-split" style="font-size: 1.5rem;"></i>
|
|
<div>
|
|
<span>{{ t('pa.title') }}</span>
|
|
<small class="d-block text-muted">{{ t('menu.pa_desc') }}</small>
|
|
</div>
|
|
</button>
|
|
|
|
<!-- System -->
|
|
<div class="list-group-item py-2 mt-2">
|
|
<small class="text-muted fw-bold text-uppercase">{{ t('menu.section.system') }}</small>
|
|
</div>
|
|
<button id="deviceInfoBtn" class="list-group-item list-group-item-action d-flex align-items-center gap-3" data-bs-toggle="modal" data-bs-target="#deviceInfoModal" data-bs-dismiss="offcanvas">
|
|
<i class="bi bi-cpu" style="font-size: 1.5rem;"></i>
|
|
<span>{{ t('menu.device_info') }}</span>
|
|
</button>
|
|
<button id="logsBtn" data-demo-lock="hide" class="list-group-item list-group-item-action d-flex align-items-center gap-3" data-bs-toggle="modal" data-bs-target="#logsModal" data-bs-dismiss="offcanvas">
|
|
<i class="bi bi-journal-text" style="font-size: 1.5rem;"></i>
|
|
<div>
|
|
<span>{{ t('logs.title') }}</span>
|
|
<small class="d-block text-muted">{{ t('menu.logs_desc') }}</small>
|
|
</div>
|
|
</button>
|
|
<button id="menu-settings" class="list-group-item list-group-item-action d-flex align-items-center gap-3 d-none" data-bs-toggle="modal" data-bs-target="#settingsModal" data-bs-dismiss="offcanvas">
|
|
<i class="bi bi-gear" style="font-size: 1.5rem;"></i>
|
|
<div>
|
|
<span>{{ t('common.settings') }}</span>
|
|
<small class="d-block text-muted">{{ t('menu.settings_desc') }}</small>
|
|
</div>
|
|
</button>
|
|
<button class="list-group-item list-group-item-action d-flex align-items-center gap-3" data-bs-toggle="modal" data-bs-target="#backupModal" data-bs-dismiss="offcanvas">
|
|
<i class="bi bi-database-down" style="font-size: 1.5rem;"></i>
|
|
<div>
|
|
<span>{{ t('menu.backup') }}</span>
|
|
<small class="d-block text-muted">{{ t('menu.backup_desc') }}</small>
|
|
</div>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Main Content -->
|
|
<main>
|
|
{% block content %}{% endblock %}
|
|
</main>
|
|
|
|
<!-- Channels Management Modal -->
|
|
<div class="modal fade" id="channelsModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title"><i class="bi bi-broadcast-pin"></i> {{ t('menu.channels') }}</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<!-- Channel List -->
|
|
<h6>{{ t('channels.your') }}</h6>
|
|
<div id="channelsList" class="list-group mb-3">
|
|
<div class="text-center text-muted py-3">
|
|
<div class="spinner-border spinner-border-sm"></div> {{ t('common.loading') }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Actions -->
|
|
<div class="btn-group w-100 mb-3" role="group">
|
|
<button type="button" class="btn btn-primary" data-bs-toggle="collapse" data-bs-target="#addChannelForm">
|
|
<i class="bi bi-plus-circle"></i> {{ t('channels.add_new') }}
|
|
</button>
|
|
<button type="button" class="btn btn-success" data-bs-toggle="collapse" data-bs-target="#joinChannelForm">
|
|
<i class="bi bi-box-arrow-in-right"></i> {{ t('channels.join_existing') }}
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Add Channel Form (collapsed) -->
|
|
<div class="collapse" id="addChannelForm">
|
|
<div class="card card-body mb-3">
|
|
<h6>{{ t('channels.create_title') }}</h6>
|
|
<form id="createChannelForm">
|
|
<div class="mb-2">
|
|
<label for="newChannelName" class="form-label">{{ t('channels.name') }}</label>
|
|
<input type="text" class="form-control" id="newChannelName"
|
|
placeholder="{{ t('channels.name_ph') }}"
|
|
pattern="[a-zA-Z0-9_\-]+"
|
|
required>
|
|
<small class="text-muted">{{ t('channels.name_hint') }}</small>
|
|
</div>
|
|
<button type="submit" class="btn btn-primary">
|
|
<i class="bi bi-plus-circle"></i> {{ t('channels.create_btn') }}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Join Channel Form (collapsed) -->
|
|
<div class="collapse" id="joinChannelForm">
|
|
<div class="card card-body mb-3">
|
|
<h6>{{ t('channels.join_title') }}</h6>
|
|
<form id="joinChannelFormSubmit">
|
|
<div class="mb-2">
|
|
<label for="joinChannelName" class="form-label">{{ t('channels.name') }}</label>
|
|
<input type="text" class="form-control" id="joinChannelName" required>
|
|
</div>
|
|
<div class="mb-2">
|
|
<label for="joinChannelKey" class="form-label">{{ t('channels.key_label') }} <small class="text-muted">{{ t('channels.key_optional') }}</small></label>
|
|
<input type="text" class="form-control" id="joinChannelKey"
|
|
placeholder="{{ t('channels.key_ph') }}"
|
|
pattern="[a-fA-F0-9]{32}"
|
|
maxlength="32">
|
|
</div>
|
|
<div class="d-flex gap-2">
|
|
<button type="submit" class="btn btn-success">
|
|
<i class="bi bi-box-arrow-in-right"></i> {{ t('channels.join_btn') }}
|
|
</button>
|
|
<button type="button" class="btn btn-secondary" id="scanQRBtn">
|
|
<i class="bi bi-qr-code-scan"></i> {{ t('channels.scan_qr') }}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Set Channel Region Scope Modal -->
|
|
<div class="modal fade" id="regionPickerModal" tabindex="-1">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">
|
|
<i class="bi bi-pin-map"></i>
|
|
{{ t('channels.region_scope_for') }} <span id="regionPickerChannelName" class="fw-bold"></span>
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="alert alert-info py-2 small mb-3">
|
|
<i class="bi bi-info-circle"></i> {{ t('channels.region_hint') }}
|
|
</div>
|
|
<div id="regionPickerList" class="list-group mb-0"></div>
|
|
</div>
|
|
<div class="modal-footer py-2">
|
|
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">{{ t('common.cancel') }}</button>
|
|
<button type="button" class="btn btn-primary btn-sm" id="regionPickerSaveBtn">{{ t('common.save') }}</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Share Channel Modal -->
|
|
<div class="modal fade" id="shareChannelModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title"><i class="bi bi-share"></i> {{ t('channels.share_title') }}</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<h6 id="shareChannelName"></h6>
|
|
|
|
<!-- QR Code -->
|
|
<div class="text-center mb-3">
|
|
<img id="shareChannelQR" src="" alt="{{ t('channels.qr_alt') }}" class="img-fluid" style="max-width: 300px;">
|
|
</div>
|
|
|
|
<!-- Channel Details -->
|
|
<div class="mb-3">
|
|
<label class="form-label fw-bold">{{ t('channels.key') }}</label>
|
|
<div class="input-group">
|
|
<input type="text" class="form-control font-monospace" id="shareChannelKey" readonly>
|
|
<button class="btn btn-outline-secondary" type="button" onclick="copyChannelKey()">
|
|
<i class="bi bi-clipboard"></i> {{ t('common.copy') }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="alert alert-info small">
|
|
<i class="bi bi-info-circle"></i>
|
|
{{ t('channels.share_hint') }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Device Info Modal -->
|
|
<div class="modal fade" id="deviceInfoModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title"><i class="bi bi-cpu"></i> {{ t('device.title') }}</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<ul class="nav nav-tabs mb-3" role="tablist">
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#tabDeviceInfo" type="button">{{ t('device.tab.info') }}</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#tabDeviceStats" type="button" id="statsTabBtn">{{ t('device.tab.stats') }}</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#tabDeviceShare" type="button" id="shareTabBtn">{{ t('device.tab.share') }}</button>
|
|
</li>
|
|
</ul>
|
|
<div class="tab-content">
|
|
<div class="tab-pane fade show active" id="tabDeviceInfo">
|
|
<div id="deviceInfoContent">
|
|
<div class="text-center py-3">
|
|
<div class="spinner-border spinner-border-sm"></div> {{ t('common.loading') }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="tab-pane fade" id="tabDeviceStats">
|
|
<div id="deviceStatsContent">
|
|
<div class="text-center py-3 text-muted">
|
|
{{ t('device.stats_hint') }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="tab-pane fade" id="tabDeviceShare">
|
|
<div id="deviceShareContent">
|
|
<div class="text-center py-3 text-muted">
|
|
{{ t('device.share_hint') }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Settings Modal -->
|
|
<div class="modal fade" id="settingsModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title"><i class="bi bi-gear"></i> {{ t('common.settings') }}</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
{% if demo_mode and not demo_unlocked %}
|
|
<div class="demo-notice" id="demoSettingsNotice">
|
|
<i class="bi bi-shield-lock"></i>
|
|
<span>{{ t_html('demo.settings_notice') }}</span>
|
|
</div>
|
|
{% endif %}
|
|
<ul class="nav nav-tabs mb-3" role="tablist">
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#tabSettingsDevice" type="button">{{ t('device.title') }}</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#tabSettingsMessages" type="button">{{ t('settings.tab.messages') }}</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#tabSettingsChat" type="button">{{ t('settings.tab.chat') }}</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#tabSettingsInterface" type="button">{{ t('settings.tab.interface') }}</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#tabSettingsAppearance" type="button">{{ t('settings.tab.appearance') }}</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#tabSettingsContacts" type="button">{{ t('settings.tab.contacts') }}</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#tabSettingsRegions" type="button">{{ t('settings.tab.regions') }}</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#tabSettingsAnalyzer" type="button">{{ t('settings.tab.analyzer') }}</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#tabSettingsObserver" type="button">{{ t('settings.tab.observer') }}</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#tabSettingsNotifications" type="button">{{ t('settings.tab.notifications') }}</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#tabSettingsDiagnostics" type="button">{{ t('settings.tab.diagnostics') }}</button>
|
|
</li>
|
|
</ul>
|
|
<div class="tab-content">
|
|
<!-- Device Settings Tab -->
|
|
<div class="tab-pane fade show active" id="tabSettingsDevice">
|
|
<ul class="nav nav-pills nav-fill mb-3" role="tablist">
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link active py-1 px-2" data-bs-toggle="pill"
|
|
data-bs-target="#tabDevicePublicInfo" type="button">{{ t('settings.device.public_info') }}</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link py-1 px-2" data-bs-toggle="pill"
|
|
data-bs-target="#tabDeviceRadio" type="button">{{ t('settings.device.radio') }}</button>
|
|
</li>
|
|
</ul>
|
|
<div class="tab-content">
|
|
<!-- Public Info sub-tab -->
|
|
<div class="tab-pane fade show active" id="tabDevicePublicInfo">
|
|
<form id="devicePublicInfoForm" data-demo-lock>
|
|
<table class="table table-sm table-borderless mb-3 align-middle">
|
|
<tbody>
|
|
<tr>
|
|
<td class="ps-0">{{ t('common.name') }}</td>
|
|
<td class="pe-0"><input type="text" class="form-control form-control-sm"
|
|
id="settDeviceName" maxlength="32" placeholder="{{ t('settings.device.name_ph') }}"></td>
|
|
</tr>
|
|
<tr>
|
|
<td class="ps-0">{{ t('rptmgmt.set.lat') }}</td>
|
|
<td class="pe-0">
|
|
<div class="input-group input-group-sm">
|
|
<input type="number" class="form-control form-control-sm"
|
|
id="settDeviceLat" step="0.000001" min="-90" max="90" placeholder="0.000000">
|
|
<button type="button" class="btn btn-outline-secondary" id="settDevicePickMapBtn"
|
|
title="{{ t('rptmgmt.set.map_pick') }}"><i class="bi bi-geo-alt"></i></button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="ps-0">{{ t('rptmgmt.set.lon') }}</td>
|
|
<td class="pe-0"><input type="number" class="form-control form-control-sm"
|
|
id="settDeviceLon" step="0.000001" min="-180" max="180" placeholder="0.000000"></td>
|
|
</tr>
|
|
<tr>
|
|
<td class="ps-0">{{ t('settings.device.share_pos') }}
|
|
<span class="badge rounded-pill text-muted" data-bs-toggle="tooltip"
|
|
title="{{ t('settings.device.share_pos_help') }}"><i class="bi bi-info-circle"></i></span>
|
|
</td>
|
|
<td class="pe-0">
|
|
<div class="form-check form-switch mb-0">
|
|
<input class="form-check-input" type="checkbox" id="settDeviceAdvertLoc">
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="ps-0">{{ t('settings.device.path_hash_mode') }}
|
|
<span class="badge rounded-pill text-muted" data-bs-toggle="tooltip"
|
|
title="{{ t('settings.device.path_hash_mode_help') }}"><i class="bi bi-info-circle"></i></span>
|
|
</td>
|
|
<td class="pe-0">
|
|
<select class="form-select form-select-sm" id="settDevicePathHashMode">
|
|
<option value="0">{{ t('settings.device.hash_1b') }}</option>
|
|
<option value="1">{{ t('settings.device.hash_2b') }}</option>
|
|
<option value="2">{{ t('settings.device.hash_3b') }}</option>
|
|
</select>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<div class="d-flex gap-2">
|
|
<button type="submit" class="btn btn-primary btn-sm">{{ t('common.save') }}</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<!-- Radio Settings sub-tab -->
|
|
<div class="tab-pane fade" id="tabDeviceRadio">
|
|
<form id="deviceRadioForm" data-demo-lock>
|
|
<table class="table table-sm table-borderless mb-3 align-middle">
|
|
<tbody>
|
|
<tr>
|
|
<td class="ps-0" colspan="2">
|
|
<select class="form-select form-select-sm" id="settRadioPreset">
|
|
<option value="">{{ t('settings.device.load_preset') }}</option>
|
|
</select>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="ps-0">{{ t('settings.device.freq') }}</td>
|
|
<td class="pe-0" style="width:7rem"><input type="number" class="form-control form-control-sm"
|
|
id="settRadioFreq" step="0.001" min="100" max="1000"></td>
|
|
</tr>
|
|
<tr>
|
|
<td class="ps-0">{{ t('settings.device.bw') }}</td>
|
|
<td class="pe-0">
|
|
<select class="form-select form-select-sm" id="settRadioBw" style="width:7rem">
|
|
<option value="7.8">7.8</option>
|
|
<option value="10.4">10.4</option>
|
|
<option value="15.6">15.6</option>
|
|
<option value="20.8">20.8</option>
|
|
<option value="31.25">31.25</option>
|
|
<option value="41.7">41.7</option>
|
|
<option value="62.5">62.5</option>
|
|
<option value="125">125</option>
|
|
<option value="250">250</option>
|
|
<option value="500">500</option>
|
|
</select>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="ps-0">{{ t('settings.device.sf') }}</td>
|
|
<td class="pe-0" style="width:7rem"><input type="number" class="form-control form-control-sm"
|
|
id="settRadioSf" min="5" max="12"></td>
|
|
</tr>
|
|
<tr>
|
|
<td class="ps-0">{{ t('settings.device.cr') }}</td>
|
|
<td class="pe-0" style="width:7rem"><input type="number" class="form-control form-control-sm"
|
|
id="settRadioCr" min="5" max="8"></td>
|
|
</tr>
|
|
<tr>
|
|
<td class="ps-0">{{ t('settings.device.tx_power') }}</td>
|
|
<td class="pe-0" style="width:7rem"><input type="number" class="form-control form-control-sm"
|
|
id="settRadioTxPower" min="0" max="30"></td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<div class="d-flex gap-2">
|
|
<button type="submit" class="btn btn-primary btn-sm">{{ t('common.save') }}</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="tab-pane fade" id="tabSettingsMessages" data-demo-lock>
|
|
<div id="settingsMessagesContent">
|
|
<form id="dmRetrySettingsForm">
|
|
<p class="text-muted small mb-3">{{ t('settings.msg.intro') }}</p>
|
|
<h6 class="text-muted mb-2">{{ t('settings.msg.direct_header') }}</h6>
|
|
<table class="table table-sm table-borderless mb-3 align-middle">
|
|
<tbody>
|
|
<tr>
|
|
<td class="ps-0">{{ t('settings.msg.direct_retries') }} <span class="badge rounded-pill text-muted" data-bs-toggle="tooltip" title="{{ t('settings.msg.direct_retries_help') }}"><i class="bi bi-info-circle"></i></span></td>
|
|
<td class="pe-0" style="width:5rem"><input type="number" class="form-control form-control-sm" id="settDirectMaxRetries" min="0" max="20" value="3"></td>
|
|
</tr>
|
|
<tr>
|
|
<td class="ps-0">{{ t('settings.msg.flood_retries') }} <span class="badge rounded-pill text-muted" data-bs-toggle="tooltip" title="{{ t('settings.msg.flood_retries_help') }}"><i class="bi bi-info-circle"></i></span></td>
|
|
<td class="pe-0"><input type="number" class="form-control form-control-sm" id="settDirectFloodRetries" min="0" max="5" value="1"></td>
|
|
</tr>
|
|
<tr>
|
|
<td class="ps-0">{{ t('settings.msg.interval') }} <span class="badge rounded-pill text-muted" data-bs-toggle="tooltip" title="{{ t('settings.msg.direct_interval_help') }}"><i class="bi bi-info-circle"></i></span></td>
|
|
<td class="pe-0"><input type="number" class="form-control form-control-sm" id="settDirectInterval" min="5" max="300" value="30"></td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<h6 class="text-muted mb-2">{{ t('settings.msg.flood_header') }}</h6>
|
|
<table class="table table-sm table-borderless mb-3 align-middle">
|
|
<tbody>
|
|
<tr>
|
|
<td class="ps-0">{{ t('settings.msg.max_retries') }} <span class="badge rounded-pill text-muted" data-bs-toggle="tooltip" title="{{ t('settings.msg.max_retries_help') }}"><i class="bi bi-info-circle"></i></span></td>
|
|
<td class="pe-0" style="width:5rem"><input type="number" class="form-control form-control-sm" id="settFloodMaxRetries" min="0" max="10" value="3"></td>
|
|
</tr>
|
|
<tr>
|
|
<td class="ps-0">{{ t('settings.msg.interval') }} <span class="badge rounded-pill text-muted" data-bs-toggle="tooltip" title="{{ t('settings.msg.flood_interval_help') }}"><i class="bi bi-info-circle"></i></span></td>
|
|
<td class="pe-0"><input type="number" class="form-control form-control-sm" id="settFloodInterval" min="5" max="300" value="60"></td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<h6 class="text-muted mb-2">{{ t('settings.msg.other') }}</h6>
|
|
<table class="table table-sm table-borderless mb-3 align-middle">
|
|
<tbody>
|
|
<tr>
|
|
<td class="ps-0">{{ t('settings.msg.grace') }} <span class="badge rounded-pill text-muted" data-bs-toggle="tooltip" title="{{ t('settings.msg.grace_help') }}"><i class="bi bi-info-circle"></i></span></td>
|
|
<td class="pe-0" style="width:5rem"><input type="number" class="form-control form-control-sm" id="settGracePeriod" min="10" max="300" value="60"></td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<div class="d-flex gap-2">
|
|
<button type="submit" class="btn btn-primary btn-sm">{{ t('common.save') }}</button>
|
|
<button type="button" class="btn btn-outline-secondary btn-sm" id="settingsResetBtn">{{ t('settings.reset_defaults') }}</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
<div class="tab-pane fade" id="tabSettingsChat" data-demo-lock>
|
|
<form id="chatSettingsForm">
|
|
<h6 class="text-muted mb-2">{{ t('settings.chat.quote') }}</h6>
|
|
<table class="table table-sm table-borderless mb-3 align-middle">
|
|
<tbody>
|
|
<tr>
|
|
<td class="ps-0">{{ t('settings.chat.quote_len') }} <span class="badge rounded-pill text-muted" data-bs-toggle="tooltip" title="{{ t('settings.chat.quote_len_help') }}"><i class="bi bi-info-circle"></i></span></td>
|
|
<td class="pe-0" style="width:5rem"><input type="number" class="form-control form-control-sm" id="settQuoteMaxBytes" min="5" max="120" value="20"></td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<h6 class="text-muted mb-2">{{ t('settings.chat.route_popup') }}</h6>
|
|
<p class="text-muted small mb-2">{{ t('settings.chat.route_popup_desc') }}</p>
|
|
<table class="table table-sm table-borderless mb-3 align-middle">
|
|
<tbody>
|
|
<tr>
|
|
<td class="ps-0">{{ t('settings.chat.autoclose') }} <span class="badge rounded-pill text-muted" data-bs-toggle="tooltip" title="{{ t('settings.chat.autoclose_help') }}"><i class="bi bi-info-circle"></i></span></td>
|
|
<td class="pe-0" style="width:5rem"><input type="number" class="form-control form-control-sm" id="settPathPopupTimeout" min="1" max="60" value="8"></td>
|
|
</tr>
|
|
<tr>
|
|
<td class="ps-0">{{ t('settings.chat.no_autoclose') }} <span class="badge rounded-pill text-muted" data-bs-toggle="tooltip" title="{{ t('settings.chat.no_autoclose_help') }}"><i class="bi bi-info-circle"></i></span></td>
|
|
<td class="pe-0">
|
|
<div class="form-check form-switch mb-0">
|
|
<input class="form-check-input" type="checkbox" id="settPathPopupNoAutoclose">
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<div class="d-flex gap-2">
|
|
<button type="submit" class="btn btn-primary btn-sm">{{ t('common.save') }}</button>
|
|
<button type="button" class="btn btn-outline-secondary btn-sm" id="chatSettingsResetBtn">{{ t('settings.reset_defaults') }}</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<!-- Not locked as a whole in demo mode: the sidebar breakpoint above the
|
|
<hr> is localStorage, i.e. per-browser. Only the toast form below
|
|
is server-side and shared, so the lock sits on that form. -->
|
|
<div class="tab-pane fade" id="tabSettingsInterface">
|
|
<h6 class="text-muted mb-2">{{ t('settings.iface.layout') }}</h6>
|
|
<p class="text-muted small mb-2">{{ t('settings.iface.layout_desc') }}</p>
|
|
<table class="table table-sm table-borderless mb-3 align-middle">
|
|
<tbody>
|
|
<tr>
|
|
<td class="ps-0">{{ t('settings.iface.breakpoint') }} <span class="badge rounded-pill text-muted" data-bs-toggle="tooltip" title="{{ t('settings.iface.breakpoint_help') }}"><i class="bi bi-info-circle"></i></span></td>
|
|
<td class="pe-0" style="width:10rem">
|
|
<div class="d-flex gap-1">
|
|
<input type="number" class="form-control form-control-sm" id="settSidebarBreakpoint" min="600" max="2000" step="1" value="992">
|
|
<button type="button" class="btn btn-outline-secondary btn-sm" id="settSidebarBreakpointReset" title="{{ t('settings.iface.breakpoint_reset_title') }}"><i class="bi bi-arrow-counterclockwise"></i></button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<hr>
|
|
<form id="uiSettingsForm" data-demo-lock>
|
|
<h6 class="text-muted mb-2">{{ t('settings.tab.notifications') }}</h6>
|
|
<p class="text-muted small mb-2">{{ t('settings.iface.toast_desc') }}</p>
|
|
<table class="table table-sm table-borderless mb-3 align-middle">
|
|
<tbody>
|
|
<tr>
|
|
<td class="ps-0">{{ t('settings.chat.autoclose') }} <span class="badge rounded-pill text-muted" data-bs-toggle="tooltip" title="{{ t('settings.iface.toast_autoclose_help') }}"><i class="bi bi-info-circle"></i></span></td>
|
|
<td class="pe-0" style="width:5rem"><input type="number" class="form-control form-control-sm" id="settToastTimeout" min="1" max="60" step="0.5" value="2"></td>
|
|
</tr>
|
|
<tr>
|
|
<td class="ps-0">{{ t('settings.chat.no_autoclose') }} <span class="badge rounded-pill text-muted" data-bs-toggle="tooltip" title="{{ t('settings.iface.toast_no_autoclose_help') }}"><i class="bi bi-info-circle"></i></span></td>
|
|
<td class="pe-0">
|
|
<div class="form-check form-switch mb-0">
|
|
<input class="form-check-input" type="checkbox" id="settToastNoAutoclose">
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="ps-0">{{ t('settings.iface.position') }}</td>
|
|
<td class="pe-0">
|
|
<select class="form-select form-select-sm" id="settToastPosition">
|
|
<option value="top-left">{{ t('settings.iface.pos_top_left') }}</option>
|
|
<option value="top-right">{{ t('settings.iface.pos_top_right') }}</option>
|
|
<option value="bottom-left">{{ t('settings.iface.pos_bottom_left') }}</option>
|
|
<option value="bottom-right">{{ t('settings.iface.pos_bottom_right') }}</option>
|
|
<option value="center">{{ t('settings.iface.pos_center') }}</option>
|
|
</select>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<div class="d-flex gap-2">
|
|
<button type="submit" class="btn btn-primary btn-sm">{{ t('common.save') }}</button>
|
|
<button type="button" class="btn btn-outline-secondary btn-sm" id="uiSettingsResetBtn">{{ t('settings.reset_defaults') }}</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="tab-pane fade" id="tabSettingsAppearance">
|
|
<h6 class="text-muted mb-3">{{ t('settings.appear.language') }}</h6>
|
|
<div class="input-group input-group-sm mb-2" data-demo-lock>
|
|
<select class="form-select form-select-sm" id="settLanguage">
|
|
{% for code, name in available_languages.items() %}
|
|
<option value="{{ code }}"{% if code == lang %} selected{% endif %}>{{ name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
<button class="btn btn-outline-secondary" type="button" id="reloadTranslationsBtn"
|
|
title="{{ t('settings.appear.reload_title') }}">
|
|
<i class="bi bi-arrow-clockwise"></i>
|
|
</button>
|
|
</div>
|
|
<small class="d-block text-muted mb-3">
|
|
{{ t_html('settings.appear.language_hint') }}
|
|
</small>
|
|
|
|
<hr>
|
|
<h6 class="text-muted mb-3">{{ t('settings.appear.theme') }}</h6>
|
|
<div class="d-flex flex-column gap-2">
|
|
<div class="theme-option active" data-theme-value="light" onclick="setTheme('light')">
|
|
<div class="theme-option-preview light">
|
|
<i class="bi bi-sun"></i>
|
|
</div>
|
|
<div>
|
|
<div class="theme-option-label">{{ t('settings.appear.theme_light') }}</div>
|
|
<div class="theme-option-desc">{{ t('settings.appear.theme_light_desc') }}</div>
|
|
</div>
|
|
</div>
|
|
<div class="theme-option" data-theme-value="dark" onclick="setTheme('dark')">
|
|
<div class="theme-option-preview dark">
|
|
<i class="bi bi-moon-stars" style="color: #60a5fa;"></i>
|
|
</div>
|
|
<div>
|
|
<div class="theme-option-label">{{ t('settings.appear.theme_dark') }}</div>
|
|
<div class="theme-option-desc">{{ t('settings.appear.theme_dark_desc') }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<hr>
|
|
<h6 class="text-muted mb-3">{{ t('settings.appear.quick_access') }}</h6>
|
|
<div class="form-check mb-3">
|
|
<input class="form-check-input" type="checkbox" id="settShowFab">
|
|
<label class="form-check-label" for="settShowFab">{{ t('settings.appear.show_fab') }}</label>
|
|
<small class="d-block text-muted">{{ t('settings.appear.show_fab_hint') }}</small>
|
|
</div>
|
|
<div id="fabAppearanceControls">
|
|
<table class="table table-sm align-middle mb-3" id="settItemPlacementTable">
|
|
<thead>
|
|
<tr>
|
|
<th class="ps-0 fw-normal text-muted small">{{ t('settings.appear.item') }}</th>
|
|
<th class="pe-0 text-end fw-normal text-muted small">{{ t('settings.appear.placement') }}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr data-placement-key="filter">
|
|
<td class="ps-0">{{ t('menu.filter') }}</td>
|
|
<td class="pe-0 text-end">
|
|
<div class="btn-group btn-group-sm" role="group" aria-label="{{ t('settings.appear.placement_aria', item=t('menu.filter')) }}">
|
|
<input type="radio" class="btn-check" name="place-filter" id="place-filter-fab" value="fab" autocomplete="off">
|
|
<label class="btn btn-outline-primary" for="place-filter-fab">{{ t('settings.appear.place_fab') }}</label>
|
|
<input type="radio" class="btn-check" name="place-filter" id="place-filter-menu" value="menu" autocomplete="off">
|
|
<label class="btn btn-outline-primary" for="place-filter-menu">{{ t('settings.appear.place_menu') }}</label>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<tr data-placement-key="search">
|
|
<td class="ps-0">{{ t('menu.search') }}</td>
|
|
<td class="pe-0 text-end">
|
|
<div class="btn-group btn-group-sm" role="group" aria-label="{{ t('settings.appear.placement_aria', item=t('menu.search')) }}">
|
|
<input type="radio" class="btn-check" name="place-search" id="place-search-fab" value="fab" autocomplete="off">
|
|
<label class="btn btn-outline-primary" for="place-search-fab">{{ t('settings.appear.place_fab') }}</label>
|
|
<input type="radio" class="btn-check" name="place-search" id="place-search-menu" value="menu" autocomplete="off">
|
|
<label class="btn btn-outline-primary" for="place-search-menu">{{ t('settings.appear.place_menu') }}</label>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<tr data-placement-key="dm">
|
|
<td class="ps-0">{{ t('dm.title') }}</td>
|
|
<td class="pe-0 text-end">
|
|
<div class="btn-group btn-group-sm" role="group" aria-label="{{ t('settings.appear.placement_aria', item=t('dm.title')) }}">
|
|
<input type="radio" class="btn-check" name="place-dm" id="place-dm-fab" value="fab" autocomplete="off">
|
|
<label class="btn btn-outline-primary" for="place-dm-fab">{{ t('settings.appear.place_fab') }}</label>
|
|
<input type="radio" class="btn-check" name="place-dm" id="place-dm-menu" value="menu" autocomplete="off">
|
|
<label class="btn btn-outline-primary" for="place-dm-menu">{{ t('settings.appear.place_menu') }}</label>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<tr data-placement-key="contacts">
|
|
<td class="ps-0">{{ t('contacts.title') }}</td>
|
|
<td class="pe-0 text-end">
|
|
<div class="btn-group btn-group-sm" role="group" aria-label="{{ t('settings.appear.placement_aria', item=t('contacts.title')) }}">
|
|
<input type="radio" class="btn-check" name="place-contacts" id="place-contacts-fab" value="fab" autocomplete="off">
|
|
<label class="btn btn-outline-primary" for="place-contacts-fab">{{ t('settings.appear.place_fab') }}</label>
|
|
<input type="radio" class="btn-check" name="place-contacts" id="place-contacts-menu" value="menu" autocomplete="off">
|
|
<label class="btn btn-outline-primary" for="place-contacts-menu">{{ t('settings.appear.place_menu') }}</label>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<tr data-placement-key="settings">
|
|
<td class="ps-0">{{ t('common.settings') }}</td>
|
|
<td class="pe-0 text-end">
|
|
<div class="btn-group btn-group-sm" role="group" aria-label="{{ t('settings.appear.placement_aria', item=t('common.settings')) }}">
|
|
<input type="radio" class="btn-check" name="place-settings" id="place-settings-fab" value="fab" autocomplete="off">
|
|
<label class="btn btn-outline-primary" for="place-settings-fab">{{ t('settings.appear.place_fab') }}</label>
|
|
<input type="radio" class="btn-check" name="place-settings" id="place-settings-menu" value="menu" autocomplete="off">
|
|
<label class="btn btn-outline-primary" for="place-settings-menu">{{ t('settings.appear.place_menu') }}</label>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<tr data-placement-key="advert">
|
|
<td class="ps-0">{{ t('menu.advert') }}</td>
|
|
<td class="pe-0 text-end">
|
|
<div class="btn-group btn-group-sm" role="group" aria-label="{{ t('settings.appear.placement_aria', item=t('menu.advert')) }}">
|
|
<input type="radio" class="btn-check" name="place-advert" id="place-advert-fab" value="fab" autocomplete="off">
|
|
<label class="btn btn-outline-primary" for="place-advert-fab">{{ t('settings.appear.place_fab') }}</label>
|
|
<input type="radio" class="btn-check" name="place-advert" id="place-advert-menu" value="menu" autocomplete="off">
|
|
<label class="btn btn-outline-primary" for="place-advert-menu">{{ t('settings.appear.place_menu') }}</label>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<tr data-placement-key="floodadvert">
|
|
<td class="ps-0">{{ t('menu.flood_advert') }}</td>
|
|
<td class="pe-0 text-end">
|
|
<div class="btn-group btn-group-sm" role="group" aria-label="{{ t('settings.appear.placement_aria', item=t('menu.flood_advert')) }}">
|
|
<input type="radio" class="btn-check" name="place-floodadvert" id="place-floodadvert-fab" value="fab" autocomplete="off">
|
|
<label class="btn btn-outline-primary" for="place-floodadvert-fab">{{ t('settings.appear.place_fab') }}</label>
|
|
<input type="radio" class="btn-check" name="place-floodadvert" id="place-floodadvert-menu" value="menu" autocomplete="off">
|
|
<label class="btn btn-outline-primary" for="place-floodadvert-menu">{{ t('settings.appear.place_menu') }}</label>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<tr data-placement-key="map">
|
|
<td class="ps-0">{{ t('menu.map') }}</td>
|
|
<td class="pe-0 text-end">
|
|
<div class="btn-group btn-group-sm" role="group" aria-label="{{ t('settings.appear.placement_aria', item=t('menu.map')) }}">
|
|
<input type="radio" class="btn-check" name="place-map" id="place-map-fab" value="fab" autocomplete="off">
|
|
<label class="btn btn-outline-primary" for="place-map-fab">{{ t('settings.appear.place_fab') }}</label>
|
|
<input type="radio" class="btn-check" name="place-map" id="place-map-menu" value="menu" autocomplete="off">
|
|
<label class="btn btn-outline-primary" for="place-map-menu">{{ t('settings.appear.place_menu') }}</label>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<tr data-placement-key="console">
|
|
<td class="ps-0">{{ t('console.title') }}</td>
|
|
<td class="pe-0 text-end">
|
|
<div class="btn-group btn-group-sm" role="group" aria-label="{{ t('settings.appear.placement_aria', item=t('console.title')) }}">
|
|
<input type="radio" class="btn-check" name="place-console" id="place-console-fab" value="fab" autocomplete="off">
|
|
<label class="btn btn-outline-primary" for="place-console-fab">{{ t('settings.appear.place_fab') }}</label>
|
|
<input type="radio" class="btn-check" name="place-console" id="place-console-menu" value="menu" autocomplete="off">
|
|
<label class="btn btn-outline-primary" for="place-console-menu">{{ t('settings.appear.place_menu') }}</label>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<tr data-placement-key="repeaters">
|
|
<td class="ps-0">{{ t('repeaters.title') }}</td>
|
|
<td class="pe-0 text-end">
|
|
<div class="btn-group btn-group-sm" role="group" aria-label="{{ t('settings.appear.placement_aria', item=t('repeaters.title')) }}">
|
|
<input type="radio" class="btn-check" name="place-repeaters" id="place-repeaters-fab" value="fab" autocomplete="off">
|
|
<label class="btn btn-outline-primary" for="place-repeaters-fab">{{ t('settings.appear.place_fab') }}</label>
|
|
<input type="radio" class="btn-check" name="place-repeaters" id="place-repeaters-menu" value="menu" autocomplete="off">
|
|
<label class="btn btn-outline-primary" for="place-repeaters-menu">{{ t('settings.appear.place_menu') }}</label>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<tr data-placement-key="pathanalyzer">
|
|
<td class="ps-0">{{ t('pa.title') }}</td>
|
|
<td class="pe-0 text-end">
|
|
<div class="btn-group btn-group-sm" role="group" aria-label="{{ t('settings.appear.placement_aria', item=t('pa.title')) }}">
|
|
<input type="radio" class="btn-check" name="place-pathanalyzer" id="place-pathanalyzer-fab" value="fab" autocomplete="off">
|
|
<label class="btn btn-outline-primary" for="place-pathanalyzer-fab">{{ t('settings.appear.place_fab') }}</label>
|
|
<input type="radio" class="btn-check" name="place-pathanalyzer" id="place-pathanalyzer-menu" value="menu" autocomplete="off">
|
|
<label class="btn btn-outline-primary" for="place-pathanalyzer-menu">{{ t('settings.appear.place_menu') }}</label>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<tr data-placement-key="deviceinfo">
|
|
<td class="ps-0">{{ t('menu.device_info') }}</td>
|
|
<td class="pe-0 text-end">
|
|
<div class="btn-group btn-group-sm" role="group" aria-label="{{ t('settings.appear.placement_aria', item=t('menu.device_info')) }}">
|
|
<input type="radio" class="btn-check" name="place-deviceinfo" id="place-deviceinfo-fab" value="fab" autocomplete="off">
|
|
<label class="btn btn-outline-primary" for="place-deviceinfo-fab">{{ t('settings.appear.place_fab') }}</label>
|
|
<input type="radio" class="btn-check" name="place-deviceinfo" id="place-deviceinfo-menu" value="menu" autocomplete="off">
|
|
<label class="btn btn-outline-primary" for="place-deviceinfo-menu">{{ t('settings.appear.place_menu') }}</label>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<tr data-placement-key="syslog">
|
|
<td class="ps-0">{{ t('logs.title') }}</td>
|
|
<td class="pe-0 text-end">
|
|
<div class="btn-group btn-group-sm" role="group" aria-label="{{ t('settings.appear.placement_aria', item=t('logs.title')) }}">
|
|
<input type="radio" class="btn-check" name="place-syslog" id="place-syslog-fab" value="fab" autocomplete="off">
|
|
<label class="btn btn-outline-primary" for="place-syslog-fab">{{ t('settings.appear.place_fab') }}</label>
|
|
<input type="radio" class="btn-check" name="place-syslog" id="place-syslog-menu" value="menu" autocomplete="off">
|
|
<label class="btn btn-outline-primary" for="place-syslog-menu">{{ t('settings.appear.place_menu') }}</label>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<table class="table table-sm table-borderless mb-3 align-middle">
|
|
<tbody>
|
|
<tr>
|
|
<td class="ps-0">{{ t('settings.appear.btn_size') }}</td>
|
|
<td class="pe-0 d-flex align-items-center gap-2" style="width:12rem">
|
|
<input type="range" class="form-range flex-grow-1" id="settFabSize" min="28" max="72" step="2" value="56">
|
|
<span class="text-muted small" id="settFabSizeVal" style="min-width:2.5rem;text-align:right">56</span>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="ps-0">{{ t('settings.appear.spacing') }}</td>
|
|
<td class="pe-0 d-flex align-items-center gap-2">
|
|
<input type="range" class="form-range flex-grow-1" id="settFabGap" min="2" max="24" step="1" value="12">
|
|
<span class="text-muted small" id="settFabGapVal" style="min-width:2.5rem;text-align:right">12</span>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="ps-0">{{ t('settings.appear.position') }}</td>
|
|
<td class="pe-0">
|
|
<button type="button" class="btn btn-outline-secondary btn-sm" id="settFabResetPos">{{ t('settings.appear.reset_default') }}</button>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<div class="tab-pane fade" id="tabSettingsContacts" data-demo-lock>
|
|
<table class="table table-sm table-borderless mb-0 align-middle">
|
|
<tbody>
|
|
<tr>
|
|
<td class="ps-0">{{ t('settings.contacts.manual_approval') }}
|
|
<span class="badge rounded-pill text-muted" data-bs-toggle="tooltip"
|
|
title="{{ t('settings.contacts.manual_approval_help') }}"><i class="bi bi-info-circle"></i></span>
|
|
</td>
|
|
<td class="pe-0">
|
|
<div class="form-check form-switch mb-0">
|
|
<input class="form-check-input" type="checkbox" id="settManualApproval">
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="ps-0" id="settSuppressAdvertNotifsLabel">{{ t('settings.contacts.suppress_notifs') }}
|
|
<span class="badge rounded-pill text-muted" data-bs-toggle="tooltip"
|
|
title="{{ t('settings.contacts.suppress_notifs_help') }}"><i class="bi bi-info-circle"></i></span>
|
|
</td>
|
|
<td class="pe-0">
|
|
<div class="form-check form-switch mb-0">
|
|
<input class="form-check-input" type="checkbox" id="settSuppressAdvertNotifs">
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="ps-0" id="settAutoIgnoreAdvertsLabel">{{ t('settings.contacts.auto_ignore') }}
|
|
<span class="badge rounded-pill text-muted" data-bs-toggle="tooltip"
|
|
title="{{ t('settings.contacts.auto_ignore_help') }}"><i class="bi bi-info-circle"></i></span>
|
|
</td>
|
|
<td class="pe-0">
|
|
<div class="form-check form-switch mb-0">
|
|
<input class="form-check-input" type="checkbox" id="settAutoIgnoreAdverts">
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="tab-pane fade" id="tabSettingsRegions" data-demo-lock>
|
|
<div class="alert alert-info py-2 small mb-3 mt-2">
|
|
<i class="bi bi-info-circle"></i> {{ t_html('settings.regions.intro') }}
|
|
</div>
|
|
<h6 class="mb-2">{{ t('settings.regions.registry') }}</h6>
|
|
<div id="regionsList" class="list-group mb-3">
|
|
<div class="text-center text-muted py-3 small">
|
|
<div class="spinner-border spinner-border-sm"></div> {{ t('common.loading') }}
|
|
</div>
|
|
</div>
|
|
<form id="addRegionForm" class="row g-2 mb-0">
|
|
<div class="col-8">
|
|
<input type="text" class="form-control form-control-sm" id="newRegionName"
|
|
placeholder="{{ t('settings.regions.name_ph') }}" required maxlength="30" autocomplete="off">
|
|
</div>
|
|
<div class="col-4">
|
|
<button type="submit" class="btn btn-primary btn-sm w-100">
|
|
<i class="bi bi-plus-circle"></i> {{ t('common.add') }}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
<div class="form-text small">
|
|
{{ t_html('settings.regions.tip') }}
|
|
</div>
|
|
</div>
|
|
<div class="tab-pane fade" id="tabSettingsAnalyzer" data-demo-lock>
|
|
<div class="alert alert-info py-2 small mb-3 mt-2">
|
|
<i class="bi bi-info-circle"></i> {{ t_html('settings.analyzer.intro') }}
|
|
</div>
|
|
<h6 class="mb-2">{{ t('settings.analyzer.services') }}</h6>
|
|
<div id="analyzersList" class="list-group mb-3">
|
|
<div class="text-center text-muted py-3 small">
|
|
<div class="spinner-border spinner-border-sm"></div> {{ t('common.loading') }}
|
|
</div>
|
|
</div>
|
|
<button type="button" class="btn btn-primary btn-sm" id="addAnalyzerBtn">
|
|
<i class="bi bi-plus-circle"></i> {{ t('analyzer.add') }}
|
|
</button>
|
|
<div class="form-text small mt-2">
|
|
{{ t('settings.analyzer.tip') }}
|
|
</div>
|
|
</div>
|
|
<div class="tab-pane fade" id="tabSettingsObserver" data-demo-lock>
|
|
<div class="alert alert-info py-2 small mb-3 mt-2">
|
|
<i class="bi bi-info-circle"></i> {{ t_html('settings.observer.intro') }}
|
|
</div>
|
|
<div class="form-check form-switch mb-2">
|
|
<input class="form-check-input" type="checkbox" id="observerEnabledToggle">
|
|
<label class="form-check-label" for="observerEnabledToggle">{{ t('settings.observer.enable') }}</label>
|
|
</div>
|
|
<div class="row g-2 mb-1">
|
|
<div class="col-6">
|
|
<label class="form-label small mb-1" for="observerIataInput">{{ t('settings.observer.iata') }}</label>
|
|
<input type="text" class="form-control form-control-sm" id="observerIataInput"
|
|
maxlength="3" placeholder="{{ t('settings.observer.iata_ph') }}" autocomplete="off"
|
|
style="text-transform: uppercase;">
|
|
</div>
|
|
<div class="col-6">
|
|
<label class="form-label small mb-1" for="observerAdvertIntervalInput">{{ t('settings.observer.advert_interval') }}</label>
|
|
<input type="number" class="form-control form-control-sm" id="observerAdvertIntervalInput"
|
|
min="0" max="8760" step="1" placeholder="{{ t('settings.observer.advert_interval_ph') }}">
|
|
</div>
|
|
</div>
|
|
<div class="form-text small mb-2">
|
|
{{ t_html('settings.observer.hint') }}
|
|
</div>
|
|
<div id="observerStatusLine" class="small text-muted mb-3"></div>
|
|
<h6 class="mb-2">{{ t('settings.observer.brokers') }}</h6>
|
|
<div id="observerBrokersList" class="list-group mb-3">
|
|
<div class="text-center text-muted py-3 small">
|
|
<div class="spinner-border spinner-border-sm"></div> {{ t('common.loading') }}
|
|
</div>
|
|
</div>
|
|
<button type="button" class="btn btn-primary btn-sm" id="addObserverBrokerBtn">
|
|
<i class="bi bi-plus-circle"></i> {{ t('observer.add_broker') }}
|
|
</button>
|
|
</div>
|
|
<div class="tab-pane fade" id="tabSettingsNotifications">
|
|
<p class="text-muted small mb-3">{{ t('settings.notif.desc') }}</p>
|
|
<button id="notificationsToggle" class="list-group-item list-group-item-action d-flex align-items-center gap-3 border rounded" type="button" style="width: 100%;">
|
|
<i class="bi bi-bell" style="font-size: 1.5rem;"></i>
|
|
<div class="flex-grow-1 text-start">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<span>{{ t('settings.tab.notifications') }}</span>
|
|
<span id="notificationStatus" class="badge bg-secondary">{{ t('common.disabled') }}</span>
|
|
</div>
|
|
<small class="text-muted d-block mt-1" style="font-size: 0.75rem;">
|
|
{{ t('settings.notif.works_hidden') }}
|
|
</small>
|
|
</div>
|
|
</button>
|
|
</div>
|
|
<div class="tab-pane fade" id="tabSettingsDiagnostics" data-demo-lock>
|
|
<div class="alert alert-info py-2 small mb-3 mt-2">
|
|
<i class="bi bi-info-circle"></i> {{ t_html('settings.diag.intro') }}
|
|
</div>
|
|
<div class="alert alert-warning py-2 small mb-3">
|
|
<i class="bi bi-shield-exclamation"></i> {{ t_html('settings.diag.privacy') }}
|
|
</div>
|
|
|
|
<div id="diagIdleControls">
|
|
<div class="row g-2 mb-2">
|
|
<div class="col-6">
|
|
<label class="form-label small mb-1" for="diagDurationSelect">{{ t('settings.diag.duration') }}</label>
|
|
<select class="form-select form-select-sm" id="diagDurationSelect"></select>
|
|
</div>
|
|
<div class="col-6">
|
|
<label class="form-label small mb-1" for="diagNoteInput">{{ t('settings.diag.note') }}</label>
|
|
<input type="text" class="form-control form-control-sm" id="diagNoteInput"
|
|
maxlength="200" placeholder="{{ t('settings.diag.note_ph') }}" autocomplete="off">
|
|
</div>
|
|
</div>
|
|
<div class="form-check form-switch mb-3">
|
|
<input class="form-check-input" type="checkbox" id="diagDebugLogsToggle" checked>
|
|
<label class="form-check-label small" for="diagDebugLogsToggle">{{ t('settings.diag.debug_logs') }}</label>
|
|
<div class="form-text small">{{ t('settings.diag.debug_logs_hint') }}</div>
|
|
</div>
|
|
<button type="button" class="btn btn-danger btn-sm" id="diagStartBtn">
|
|
<i class="bi bi-record-circle"></i> {{ t('settings.diag.start') }}
|
|
</button>
|
|
</div>
|
|
|
|
<div id="diagRecordingControls" class="d-none">
|
|
<div class="alert alert-danger py-2 mb-2">
|
|
<div class="d-flex align-items-center gap-2">
|
|
<span class="spinner-grow spinner-grow-sm"></span>
|
|
<strong>{{ t('settings.diag.recording') }}</strong>
|
|
</div>
|
|
<div class="small mt-1" id="diagLiveCounters"></div>
|
|
<div class="progress mt-2" style="height: 4px;">
|
|
<div class="progress-bar bg-danger" id="diagProgressBar" style="width: 0%"></div>
|
|
</div>
|
|
</div>
|
|
<p class="small text-muted mb-2">{{ t('settings.diag.reproduce_hint') }}</p>
|
|
<button type="button" class="btn btn-primary btn-sm" id="diagStopBtn">
|
|
<i class="bi bi-stop-circle"></i> {{ t('settings.diag.stop') }}
|
|
</button>
|
|
</div>
|
|
|
|
<hr class="my-3">
|
|
<h6 class="mb-2">{{ t('settings.diag.captures') }}</h6>
|
|
<div id="diagCapturesList" class="list-group mb-3">
|
|
<div class="text-center text-muted py-3 small">
|
|
<div class="spinner-border spinner-border-sm"></div> {{ t('common.loading') }}
|
|
</div>
|
|
</div>
|
|
|
|
<h6 class="mb-2">{{ t('settings.diag.sharing') }}</h6>
|
|
<div class="form-text small mb-2">{{ t_html('settings.diag.sharing_hint') }}</div>
|
|
<div class="row g-2">
|
|
<div class="col-12">
|
|
<label class="form-label small mb-1" for="diagUploadUrlInput">{{ t('settings.diag.upload_url') }}</label>
|
|
<input type="text" class="form-control form-control-sm" id="diagUploadUrlInput" autocomplete="off">
|
|
</div>
|
|
<div class="col-12">
|
|
<label class="form-label small mb-1" for="diagUploadTokenInput">{{ t('settings.diag.upload_token') }}</label>
|
|
<input type="password" class="form-control form-control-sm" id="diagUploadTokenInput"
|
|
placeholder="{{ t('settings.diag.upload_token_ph') }}" autocomplete="off">
|
|
<div class="form-text small" id="diagTokenHint"></div>
|
|
</div>
|
|
</div>
|
|
<button type="button" class="btn btn-outline-secondary btn-sm mt-2" id="diagSaveUploadBtn">
|
|
{{ t('common.save') }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{% if demo_mode and demo_has_unlock_code %}
|
|
<div class="demo-unlock-box" data-demo-lock-exempt>
|
|
{% if demo_unlocked %}
|
|
<div class="d-flex align-items-center justify-content-between gap-2">
|
|
<div class="small text-success">
|
|
<i class="bi bi-shield-check"></i> {{ t('demo.unlocked_state') }}
|
|
</div>
|
|
<button type="button" class="btn btn-outline-secondary btn-sm" id="demoLockBtn">
|
|
{{ t('demo.lock_btn') }}
|
|
</button>
|
|
</div>
|
|
{% else %}
|
|
<label class="form-label small mb-1" for="demoUnlockInput">
|
|
<i class="bi bi-key"></i> {{ t('demo.unlock_label') }}
|
|
</label>
|
|
<div class="input-group input-group-sm">
|
|
<input type="password" class="form-control form-control-sm" id="demoUnlockInput"
|
|
autocomplete="off" placeholder="{{ t('demo.unlock_ph') }}">
|
|
<button class="btn btn-primary" type="button" id="demoUnlockBtn">
|
|
{{ t('demo.unlock_btn') }}
|
|
</button>
|
|
</div>
|
|
<div class="form-text small" id="demoUnlockHint">{{ t('demo.unlock_hint') }}</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
<div class="modal-footer py-2">
|
|
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">{{ t('common.close') }}</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Analyzer Edit Modal (create + edit) -->
|
|
<div class="modal fade" id="analyzerEditModal" tabindex="-1" style="z-index: 1080;">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header py-2">
|
|
<h6 class="modal-title"><i class="bi bi-clipboard-data"></i> <span id="analyzerEditModalTitle">{{ t('analyzer.add') }}</span></h6>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form id="analyzerEditForm">
|
|
<div class="modal-body">
|
|
<input type="hidden" id="analyzerEditId" value="">
|
|
<div class="mb-3">
|
|
<label class="form-label small mb-1" for="analyzerEditName">{{ t('common.name') }}</label>
|
|
<input type="text" class="form-control form-control-sm" id="analyzerEditName"
|
|
placeholder="{{ t('analyzer.name_ph') }}" required maxlength="60" autocomplete="off">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label small mb-1" for="analyzerEditUrl">{{ t('analyzer.url') }}</label>
|
|
<input type="text" class="form-control form-control-sm" id="analyzerEditUrl"
|
|
placeholder="https://analyzer.example.com/#/packets/{packetHash}" required autocomplete="off">
|
|
<div class="form-text small">{{ t_html('analyzer.url_hint') }}</div>
|
|
</div>
|
|
<div class="form-check form-switch">
|
|
<input class="form-check-input" type="checkbox" id="analyzerEditEnabled" checked>
|
|
<label class="form-check-label small" for="analyzerEditEnabled">{{ t('common.enabled') }}</label>
|
|
</div>
|
|
<div id="analyzerEditError" class="alert alert-danger py-1 small mt-3 d-none"></div>
|
|
</div>
|
|
<div class="modal-footer py-2">
|
|
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">{{ t('common.cancel') }}</button>
|
|
<button type="submit" class="btn btn-primary btn-sm">{{ t('common.save') }}</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Analyzer Chooser Modal -->
|
|
<div class="modal fade" id="analyzerChooserModal" tabindex="-1" style="z-index: 1080;">
|
|
<div class="modal-dialog modal-dialog-centered modal-sm">
|
|
<div class="modal-content">
|
|
<div class="modal-header py-2">
|
|
<h6 class="modal-title"><i class="bi bi-clipboard-data"></i> {{ t('analyzer.choose') }}</h6>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body p-2">
|
|
<div id="analyzerChooserList" class="list-group"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Observer Broker Edit Modal (create + edit) -->
|
|
<div class="modal fade" id="observerBrokerEditModal" tabindex="-1" style="z-index: 1080;">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header py-2">
|
|
<h6 class="modal-title"><i class="bi bi-broadcast"></i> <span id="observerBrokerEditModalTitle">{{ t('observer.add_broker') }}</span></h6>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form id="observerBrokerEditForm">
|
|
<div class="modal-body">
|
|
<input type="hidden" id="observerBrokerEditId" value="">
|
|
<div class="mb-2">
|
|
<label class="form-label small mb-1" for="observerBrokerEditName">{{ t('common.name') }}</label>
|
|
<input type="text" class="form-control form-control-sm" id="observerBrokerEditName"
|
|
placeholder="{{ t('observer.name_ph') }}" required maxlength="60" autocomplete="off">
|
|
</div>
|
|
<div class="row g-2 mb-1">
|
|
<div class="col-8">
|
|
<label class="form-label small mb-1" for="observerBrokerEditHost">{{ t('observer.host') }}</label>
|
|
<input type="text" class="form-control form-control-sm" id="observerBrokerEditHost"
|
|
placeholder="mqtt.example.com" required autocomplete="off">
|
|
</div>
|
|
<div class="col-4">
|
|
<label class="form-label small mb-1" for="observerBrokerEditPort">{{ t('observer.port') }}</label>
|
|
<input type="number" class="form-control form-control-sm" id="observerBrokerEditPort"
|
|
min="1" max="65535" value="1883" required>
|
|
</div>
|
|
</div>
|
|
<div class="form-text small mb-2">{{ t('observer.ports_hint') }}</div>
|
|
<div class="row g-2 mb-2">
|
|
<div class="col-6">
|
|
<label class="form-label small mb-1" for="observerBrokerEditUsername">{{ t('observer.username') }}</label>
|
|
<input type="text" class="form-control form-control-sm" id="observerBrokerEditUsername"
|
|
autocomplete="off">
|
|
</div>
|
|
<div class="col-6">
|
|
<label class="form-label small mb-1" for="observerBrokerEditPassword">{{ t('observer.password') }}</label>
|
|
<input type="password" class="form-control form-control-sm" id="observerBrokerEditPassword"
|
|
autocomplete="new-password">
|
|
</div>
|
|
</div>
|
|
<div class="form-text small mb-2 d-none" id="observerBrokerPasswordHint">
|
|
{{ t('observer.password_hint') }}
|
|
</div>
|
|
<div class="form-check form-switch">
|
|
<input class="form-check-input" type="checkbox" id="observerBrokerEditTls">
|
|
<label class="form-check-label small" for="observerBrokerEditTls">{{ t('observer.use_tls') }}</label>
|
|
</div>
|
|
<div class="form-check form-switch">
|
|
<input class="form-check-input" type="checkbox" id="observerBrokerEditTlsVerify" checked>
|
|
<label class="form-check-label small" for="observerBrokerEditTlsVerify">{{ t('observer.verify_tls') }}</label>
|
|
</div>
|
|
<div class="form-check form-switch">
|
|
<input class="form-check-input" type="checkbox" id="observerBrokerEditEnabled" checked>
|
|
<label class="form-check-label small" for="observerBrokerEditEnabled">{{ t('common.enabled') }}</label>
|
|
</div>
|
|
<div id="observerBrokerEditError" class="alert alert-danger py-1 small mt-3 d-none"></div>
|
|
</div>
|
|
<div class="modal-footer py-2">
|
|
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">{{ t('common.cancel') }}</button>
|
|
<button type="submit" class="btn btn-primary btn-sm">{{ t('common.save') }}</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Coordinate Picker Map Modal -->
|
|
<div class="modal fade" id="coordPickerModal" tabindex="-1" style="z-index: 1080;">
|
|
<div class="modal-dialog modal-lg modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header py-2">
|
|
<h6 class="modal-title"><i class="bi bi-geo-alt"></i> {{ t('coord.title') }}</h6>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body p-0">
|
|
<div id="coordPickerMap" style="height: 400px; width: 100%;"></div>
|
|
</div>
|
|
<div class="modal-footer py-2">
|
|
<span class="me-auto small text-muted" id="coordPickerLabel">{{ t('coord.hint') }}</span>
|
|
<button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">{{ t('common.cancel') }}</button>
|
|
<button type="button" class="btn btn-sm btn-primary" id="coordPickerConfirmBtn" disabled>{{ t('coord.confirm') }}</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Quote Dialog Modal -->
|
|
<div class="modal fade" id="quoteModal" tabindex="-1">
|
|
<div class="modal-dialog modal-sm modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header py-2">
|
|
<h6 class="modal-title"><i class="bi bi-quote"></i> {{ t('quote.title') }}</h6>
|
|
<button type="button" class="btn-close btn-close-sm" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body py-2">
|
|
<p class="text-muted small mb-2" id="quotePreview"></p>
|
|
<div class="d-flex gap-2 align-items-center mb-2">
|
|
<label class="form-label mb-0 small text-nowrap" for="quoteBytesInput">{{ t('quote.max_bytes') }}</label>
|
|
<input type="number" class="form-control form-control-sm" id="quoteBytesInput" min="5" max="120" style="width:5rem">
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer py-1">
|
|
<button type="button" class="btn btn-outline-secondary btn-sm" id="quoteTruncatedBtn">{{ t('quote.truncated') }}</button>
|
|
<button type="button" class="btn btn-primary btn-sm" id="quoteFullBtn">{{ t('quote.full') }}</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Map Modal (Leaflet) -->
|
|
<div class="modal fade" id="mapModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title"><i class="bi bi-geo-alt"></i> <span id="mapModalTitle">{{ t('menu.map') }}</span></h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body p-0">
|
|
<!-- Type filter (hidden for single contact view) -->
|
|
<div id="mapTypeFilter" class="d-none px-3 py-2 border-bottom bg-light">
|
|
<div class="d-flex flex-wrap gap-2 align-items-center">
|
|
<span class="map-filter-badge active" data-type="1" id="mapFilterCOM">COM</span>
|
|
<span class="map-filter-badge active" data-type="2" id="mapFilterREP">REP</span>
|
|
<span class="map-filter-badge active" data-type="3" id="mapFilterROOM">ROOM</span>
|
|
<span class="map-filter-badge active" data-type="4" id="mapFilterSENS">SENS</span>
|
|
<div class="form-check form-switch ms-auto mb-0">
|
|
<input class="form-check-input" type="checkbox" id="mapCachedSwitch">
|
|
<label class="form-check-label small" for="mapCachedSwitch">{{ t('repeaters.map.cached') }}</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div id="leafletMap" style="height: 400px; width: 100%;"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Update Modal -->
|
|
<div class="modal fade" id="updateModal" tabindex="-1" data-bs-backdrop="static" data-bs-keyboard="false">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title"><i class="bi bi-cloud-download"></i> {{ t('update.title') }}</h5>
|
|
</div>
|
|
<div class="modal-body text-center py-4">
|
|
<div id="updateStatus">
|
|
<div class="spinner-border text-primary mb-3" role="status">
|
|
<span class="visually-hidden">{{ t('common.loading') }}</span>
|
|
</div>
|
|
<p id="updateMessage" class="mb-0">{{ t('update.checking') }}</p>
|
|
<p id="updateWhatsNew" class="d-none mt-2 mb-0"><a href="#" target="_blank" class="text-muted small"><i class="bi bi-github"></i> {{ t('update.whats_new') }}</a></p>
|
|
</div>
|
|
<div id="updateProgress" class="d-none">
|
|
<div class="progress mb-3" style="height: 8px;">
|
|
<div class="progress-bar progress-bar-striped progress-bar-animated"
|
|
role="progressbar" style="width: 100%"></div>
|
|
</div>
|
|
<p id="updateProgressMessage" class="text-muted mb-0">{{ t('update.updating') }}</p>
|
|
</div>
|
|
<div id="updateResult" class="d-none">
|
|
<i id="updateResultIcon" class="bi fs-1 mb-3 d-block"></i>
|
|
<p id="updateResultMessage" class="mb-0"></p>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer justify-content-center" id="updateFooter">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" id="updateCancelBtn">{{ t('common.cancel') }}</button>
|
|
<button type="button" class="btn btn-primary d-none" id="updateConfirmBtn">{{ t('update.now') }}</button>
|
|
<button type="button" class="btn btn-primary d-none" id="updateReloadBtn" onclick="location.reload()">{{ t('update.reload') }}</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Backup Modal -->
|
|
<div class="modal fade" id="backupModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title"><i class="bi bi-database-down"></i> {{ t('backup.title') }}</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<button class="btn btn-primary" id="createBackupBtn" data-demo-lock onclick="createBackup()">
|
|
<i class="bi bi-plus-circle"></i> {{ t('backup.create') }}
|
|
</button>
|
|
<span id="backupAutoStatus" class="text-muted small"></span>
|
|
</div>
|
|
<div id="backupList">
|
|
<div class="text-center text-muted py-3">
|
|
<div class="spinner-border spinner-border-sm"></div> {{ t('common.loading') }}
|
|
</div>
|
|
</div>
|
|
<hr class="my-3">
|
|
<h6 class="mb-2"><i class="bi bi-arrows-collapse"></i> {{ t('backup.optimize_title') }}</h6>
|
|
<p class="text-muted small mb-2">
|
|
{{ t_html('backup.optimize_hint') }}
|
|
</p>
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<button class="btn btn-outline-primary btn-sm" id="vacuumDbBtn" data-demo-lock onclick="optimizeDatabase()">
|
|
<i class="bi bi-arrows-collapse"></i> {{ t('backup.optimize_now') }}
|
|
</button>
|
|
<span id="vacuumDbStatus" class="text-muted small">{{ t('backup.current_size') }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Search Modal -->
|
|
<div class="modal fade" id="searchModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title"><i class="bi bi-search"></i> {{ t('chat.fab.search_title') }}</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="input-group mb-3">
|
|
<input type="text" class="form-control" id="searchInput" placeholder="{{ t('search.ph') }}" autofocus>
|
|
<button class="btn btn-primary" type="button" id="searchBtn">
|
|
<i class="bi bi-search"></i>
|
|
</button>
|
|
<button class="btn btn-outline-secondary" type="button" id="searchHelpBtn" title="{{ t('search.help_title') }}">
|
|
<i class="bi bi-question-circle"></i>
|
|
</button>
|
|
</div>
|
|
<div id="searchHelp" class="alert alert-light small mb-3" style="display:none;">
|
|
<strong>{{ t('search.tips') }}</strong>
|
|
<ul class="mb-1 ps-3">
|
|
<li><code>hello world</code> — {{ t('search.tip_and') }}</li>
|
|
<li><code>"hello world"</code> — {{ t('search.tip_phrase') }}</li>
|
|
<li><code>hello OR world</code> — {{ t('search.tip_or') }}</li>
|
|
<li><code>hello NOT world</code> — {{ t('search.tip_not') }}</li>
|
|
<li><code>hell*</code> — {{ t('search.tip_prefix') }}</li>
|
|
</ul>
|
|
<div class="text-muted">{{ t_html('search.special_chars') }}<br>
|
|
<a href="https://www.sqlite.org/fts5.html#full_text_query_syntax" target="_blank" rel="noopener">{{ t('search.fts5_ref') }} <i class="bi bi-box-arrow-up-right"></i></a></div>
|
|
</div>
|
|
<div id="searchResults">
|
|
<div class="text-center text-muted py-4">
|
|
<i class="bi bi-search" style="font-size: 2rem;"></i>
|
|
<p class="mt-2 mb-0">{{ t('search.empty') }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Toast container for notifications (position classes applied by JS from ui_settings) -->
|
|
<div class="toast-container position-fixed top-0 start-0 p-3" data-toast-container>
|
|
<div id="notificationToast" class="toast" role="alert">
|
|
<div class="toast-header">
|
|
<strong class="me-auto">mc-webui</strong>
|
|
<button type="button" class="btn-close" data-bs-dismiss="toast"></button>
|
|
</div>
|
|
<div class="toast-body"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Bootstrap 5 JS Bundle (local) -->
|
|
<script src="{{ url_for('static', filename='vendor/bootstrap/js/bootstrap.bundle.min.js') }}"></script>
|
|
|
|
<!-- Leaflet JS -->
|
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
|
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
|
|
crossorigin=""></script>
|
|
|
|
<!-- Coordinate validation shared by every map (must load after Leaflet) -->
|
|
<script src="{{ url_for('static', filename='js/geo-utils.js') }}"></script>
|
|
|
|
<!-- Share token formats (must load before message-utils.js, which renders them) -->
|
|
<script src="{{ url_for('static', filename='js/share-tokens.js') }}"></script>
|
|
|
|
<!-- Message Content Processing Utilities (must load before app.js and dm.js) -->
|
|
<script src="{{ url_for('static', filename='js/message-utils.js') }}"></script>
|
|
|
|
<!-- Share composer: the "+" button and its pickers (needs message-utils.js) -->
|
|
<script src="{{ url_for('static', filename='js/share-composer.js') }}"></script>
|
|
|
|
<!-- Filter Utilities (must load before app.js) -->
|
|
<script src="{{ url_for('static', filename='js/filter-utils.js') }}"></script>
|
|
|
|
<!-- SocketIO for real-time updates -->
|
|
<script src="{{ url_for('static', filename='vendor/socket.io/socket.io.min.js') }}"></script>
|
|
|
|
<!-- FAB Utilities (drag, sizing — must load before app.js) -->
|
|
<script src="{{ url_for('static', filename='js/fab-utils.js') }}"></script>
|
|
|
|
<!-- Custom JS -->
|
|
<!-- QR Code generator (for Device Share) -->
|
|
<script src="https://cdn.jsdelivr.net/npm/qrcodejs@1.0.0/qrcode.min.js"></script>
|
|
|
|
<script src="{{ url_for('static', filename='js/app.js') }}"></script>
|
|
|
|
<!-- PWA Viewport Fix for Android -->
|
|
<script>
|
|
// Fix viewport corruption on Android PWA after page reload
|
|
window.addEventListener('load', function() {
|
|
// Force viewport recalculation without modifying body height
|
|
setTimeout(() => {
|
|
window.scrollTo(0, 0);
|
|
// Force reflow using a less invasive method
|
|
void(document.documentElement.offsetHeight);
|
|
// Trigger resize event to recalculate layout
|
|
window.dispatchEvent(new Event('resize'));
|
|
}, 150);
|
|
});
|
|
</script>
|
|
|
|
<!-- Service Worker Registration for PWA -->
|
|
<script>
|
|
// Register Service Worker for PWA functionality
|
|
if ('serviceWorker' in navigator) {
|
|
window.addEventListener('load', () => {
|
|
navigator.serviceWorker.register('/static/js/sw.js')
|
|
.then((registration) => {
|
|
console.log('Service Worker registered:', registration.scope);
|
|
})
|
|
.catch((error) => {
|
|
console.error('Service Worker registration failed:', error);
|
|
});
|
|
});
|
|
}
|
|
</script>
|
|
|
|
<!-- Theme Switching -->
|
|
<script>
|
|
function setTheme(theme) {
|
|
document.documentElement.setAttribute('data-theme', theme);
|
|
document.documentElement.setAttribute('data-bs-theme', theme);
|
|
localStorage.setItem('mc-webui-theme', theme);
|
|
// Update theme selector UI
|
|
document.querySelectorAll('.theme-option').forEach(function(el) {
|
|
el.classList.toggle('active', el.getAttribute('data-theme-value') === theme);
|
|
});
|
|
}
|
|
|
|
// Initialize theme selector UI on settings modal open
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
var current = localStorage.getItem('mc-webui-theme') || 'light';
|
|
document.querySelectorAll('.theme-option').forEach(function(el) {
|
|
el.classList.toggle('active', el.getAttribute('data-theme-value') === current);
|
|
});
|
|
|
|
// --- FAB appearance controls ---
|
|
var fabSizeSlider = document.getElementById('settFabSize');
|
|
var fabSizeVal = document.getElementById('settFabSizeVal');
|
|
var fabGapSlider = document.getElementById('settFabGap');
|
|
var fabGapVal = document.getElementById('settFabGapVal');
|
|
var fabResetPos = document.getElementById('settFabResetPos');
|
|
|
|
// Load saved values
|
|
var savedSize = localStorage.getItem('mc-webui-fab-size');
|
|
var savedGap = localStorage.getItem('mc-webui-fab-gap');
|
|
if (savedSize && fabSizeSlider) { fabSizeSlider.value = savedSize; fabSizeVal.textContent = savedSize; }
|
|
if (savedGap && fabGapSlider) { fabGapSlider.value = savedGap; fabGapVal.textContent = savedGap; }
|
|
|
|
// Live preview on slider change
|
|
if (fabSizeSlider) {
|
|
fabSizeSlider.addEventListener('input', function() {
|
|
var v = this.value;
|
|
fabSizeVal.textContent = v;
|
|
document.documentElement.style.setProperty('--fab-custom-size', v + 'px');
|
|
localStorage.setItem('mc-webui-fab-size', v);
|
|
});
|
|
}
|
|
if (fabGapSlider) {
|
|
fabGapSlider.addEventListener('input', function() {
|
|
var v = this.value;
|
|
fabGapVal.textContent = v;
|
|
document.documentElement.style.setProperty('--fab-custom-gap', v + 'px');
|
|
localStorage.setItem('mc-webui-fab-gap', v);
|
|
});
|
|
}
|
|
|
|
// Reset position button
|
|
if (fabResetPos) {
|
|
fabResetPos.addEventListener('click', function() {
|
|
localStorage.removeItem('mc-webui-fab-pos');
|
|
localStorage.removeItem('mc-webui-fab-pos-dm');
|
|
var container = document.getElementById('fabContainer');
|
|
if (container) {
|
|
container.style.left = '';
|
|
container.style.right = '16px';
|
|
container.style.top = '80px';
|
|
}
|
|
});
|
|
}
|
|
});
|
|
</script>
|
|
|
|
{% block extra_scripts %}{% endblock %}
|
|
</body>
|
|
</html>
|