mirror of
https://github.com/MarekWo/mc-webui.git
synced 2026-11-06 02:32:21 +01:00
Android's WebView scales text by the system font-size setting; Chrome
ignores it and uses its own accessibility preference. With the system
font one step above default, the wrapper rendered every string ~20%
larger than the website at an identical layout width -- the brand
truncated to "mc-web...", message bodies wrapped early, and the footer
broke apart. Pin textZoom to 100 so the wrapper matches the browser.
The footer was fragile independently of that. Status, region badge and
the "Updated:" time sat in a justify-content-between row with no wrap,
so once they exceeded the width each item broke internally instead of
the row reflowing -- a dot with "Polaczono" stranded below it, a time
split from its own label. Longer translated labels ("Aktualizacja:" vs
"Updated:") hit this well before English did. Adding flex-wrap plus
text-nowrap on the items makes them reflow as whole units; .badge
already carries white-space: nowrap, so it needs nothing.
Verified in Chrome against the local container, pl locale, region badge
forced visible, at 412/360/320 CSS px and with a 1.2x text-only override
that reproduces textZoom: before/after screenshots show the reported
breakage and a clean two-line reflow respectively, with no horizontal
overflow. At full width the change is a no-op.
Rides along in the already-pending, never-built wrapper 1.2.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
383 lines
18 KiB
HTML
383 lines
18 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}{{ t('chat.title') }} - mc-webui{% endblock %}
|
|
|
|
{% block extra_head %}
|
|
<!-- Emoji Picker (local) -->
|
|
<script type="module" src="{{ url_for('static', filename='vendor/emoji-picker-element/index.js') }}"></script>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid d-flex flex-column" style="height: 100%;">
|
|
<!-- Main content: sidebar + chat -->
|
|
<div class="d-flex flex-grow-1 overflow-hidden" style="min-height: 0;">
|
|
<!-- Channel Sidebar (visible on lg+ screens) -->
|
|
<div id="channelSidebar" class="channel-sidebar">
|
|
<div class="channel-sidebar-header">
|
|
<i class="bi bi-broadcast-pin"></i> {{ t('chat.channels') }}
|
|
</div>
|
|
<div class="channel-sidebar-list" id="channelSidebarList">
|
|
<!-- Populated by JavaScript -->
|
|
</div>
|
|
</div>
|
|
<!-- Chat Area -->
|
|
<div class="flex-grow-1 d-flex flex-column" style="min-width: 0;">
|
|
<!-- Messages Container -->
|
|
<div class="flex-grow-1 position-relative overflow-hidden" style="min-height: 0;">
|
|
<!-- Filter bar overlay -->
|
|
<div id="filterBar" class="filter-bar">
|
|
<div class="filter-bar-inner">
|
|
<div class="filter-input-wrapper">
|
|
<input type="text" id="filterInput" class="filter-bar-input" placeholder="{{ t('chat.filter.ph') }}" autocomplete="off">
|
|
<!-- Filter mentions autocomplete popup -->
|
|
<div id="filterMentionsPopup" class="mentions-popup filter-mentions-popup hidden">
|
|
<div class="mentions-list" id="filterMentionsList"></div>
|
|
</div>
|
|
</div>
|
|
<button type="button" id="filterMeBtn" class="filter-bar-btn filter-bar-btn-me" title="{{ t('chat.filter.me_title') }}">
|
|
<i class="bi bi-person-fill"></i>
|
|
</button>
|
|
<span id="filterMatchCount" class="filter-match-count"></span>
|
|
<button type="button" id="filterClearBtn" class="filter-bar-btn filter-bar-btn-clear" title="{{ t('chat.filter.clear_title') }}">
|
|
<i class="bi bi-x"></i>
|
|
</button>
|
|
<button type="button" id="filterCloseBtn" class="filter-bar-btn filter-bar-btn-close" title="{{ t('common.close') }}">
|
|
<i class="bi bi-x-lg"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div id="messagesContainer" class="messages-container h-100 overflow-auto p-3">
|
|
<div id="messagesList">
|
|
<!-- Messages will be loaded here via JavaScript -->
|
|
<div class="text-center text-muted py-5">
|
|
<div class="spinner-border" role="status">
|
|
<span class="visually-hidden">{{ t('common.loading') }}</span>
|
|
</div>
|
|
<p class="mt-3">{{ t('chat.loading_messages') }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- Scroll to bottom button -->
|
|
<button id="scrollToBottomBtn" class="scroll-to-bottom-btn" title="{{ t('chat.scroll_bottom_title') }}">
|
|
<i class="bi bi-chevron-double-down"></i>
|
|
</button>
|
|
</div>
|
|
<!-- Send Message Form -->
|
|
<div class="border-top bg-light">
|
|
<form id="sendMessageForm" class="p-3">
|
|
<div class="emoji-picker-container">
|
|
<div class="input-group">
|
|
<textarea
|
|
id="messageInput"
|
|
class="form-control"
|
|
placeholder="{{ t('chat.input_ph') }}"
|
|
rows="2"
|
|
maxlength="500"
|
|
required
|
|
></textarea>
|
|
<button type="button" class="btn btn-outline-secondary" id="emojiBtn" title="{{ t('chat.emoji_title') }}">
|
|
<i class="bi bi-emoji-smile"></i>
|
|
</button>
|
|
<button type="submit" class="btn btn-primary px-4" id="sendBtn">
|
|
<i class="bi bi-send"></i>
|
|
</button>
|
|
</div>
|
|
<!-- Emoji picker popup (hidden by default) -->
|
|
<div id="emojiPickerPopup" class="emoji-picker-popup hidden"></div>
|
|
<!-- Mentions autocomplete popup (hidden by default) -->
|
|
<div id="mentionsPopup" class="mentions-popup hidden">
|
|
<div class="mentions-list" id="mentionsList"></div>
|
|
</div>
|
|
</div>
|
|
<div class="d-flex justify-content-end">
|
|
<small id="charCounter" class="text-muted">0 / 135</small>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<!-- Status Bar -->
|
|
<div class="border-top">
|
|
<div class="p-2 small text-muted d-flex flex-wrap gap-2 justify-content-between align-items-center">
|
|
<div class="d-flex flex-wrap gap-2 align-items-center">
|
|
<span id="statusText" class="text-nowrap">
|
|
<i class="bi bi-circle-fill text-secondary"></i> {{ t('common.connecting') }}
|
|
</span>
|
|
<span id="regionIndicator" class="badge bg-info text-dark d-none" role="button"
|
|
title="{{ t('chat.region_title') }}">
|
|
<i class="bi bi-pin-map"></i> <span id="regionIndicatorName"></span>
|
|
</span>
|
|
</div>
|
|
<span id="lastRefresh" class="text-nowrap">{{ t('chat.updated', time=t('common.never')) }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Floating Action Buttons -->
|
|
<div class="fab-container" id="fabContainer">
|
|
<button class="fab fab-toggle" id="fabToggle" title="{{ t('chat.fab.hide') }}">
|
|
<i class="bi bi-chevron-right"></i>
|
|
</button>
|
|
<button class="fab fab-filter" id="filterFab" title="{{ t('chat.filter.fab_title') }}">
|
|
<i class="bi bi-funnel-fill"></i>
|
|
</button>
|
|
<button class="fab fab-search" id="globalSearchBtn" data-bs-toggle="modal" data-bs-target="#searchModal" title="{{ t('chat.fab.search_title') }}">
|
|
<i class="bi bi-search"></i>
|
|
</button>
|
|
<button class="fab fab-dm" data-bs-toggle="modal" data-bs-target="#dmModal" title="{{ t('dm.title') }}">
|
|
<i class="bi bi-envelope-fill"></i>
|
|
</button>
|
|
<button class="fab fab-contacts" data-bs-toggle="modal" data-bs-target="#contactsModal" title="{{ t('contacts.title') }}">
|
|
<i class="bi bi-person-fill"></i>
|
|
</button>
|
|
<button class="fab fab-settings" data-bs-toggle="modal" data-bs-target="#settingsModal" title="{{ t('common.settings') }}">
|
|
<i class="bi bi-gear-fill"></i>
|
|
</button>
|
|
<button class="fab fab-advert d-none" id="fab-advert" title="{{ t('menu.advert') }}">
|
|
<i class="bi bi-megaphone"></i>
|
|
</button>
|
|
<button class="fab fab-floodadvert d-none" id="fab-floodadvert" title="{{ t('menu.flood_advert') }}">
|
|
<i class="bi bi-broadcast"></i>
|
|
</button>
|
|
<button class="fab fab-map d-none" id="fab-map" title="{{ t('menu.map') }}">
|
|
<i class="bi bi-map"></i>
|
|
</button>
|
|
<button class="fab fab-console d-none" id="fab-console" data-bs-toggle="modal" data-bs-target="#consoleModal" title="{{ t('console.title') }}">
|
|
<i class="bi bi-terminal"></i>
|
|
</button>
|
|
<button class="fab fab-repeaters d-none" id="fab-repeaters" data-bs-toggle="modal" data-bs-target="#repeatersModal" title="{{ t('repeaters.title') }}">
|
|
<i class="bi bi-diagram-3"></i>
|
|
</button>
|
|
<button class="fab fab-pathanalyzer d-none" id="fab-pathanalyzer" data-bs-toggle="modal" data-bs-target="#pathAnalyzerModal" title="{{ t('pa.title') }}">
|
|
<i class="bi bi-signpost-split"></i>
|
|
</button>
|
|
<button class="fab fab-deviceinfo d-none" id="fab-deviceinfo" data-bs-toggle="modal" data-bs-target="#deviceInfoModal" title="{{ t('menu.device_info') }}">
|
|
<i class="bi bi-cpu"></i>
|
|
</button>
|
|
<button class="fab fab-syslog d-none" id="fab-syslog" data-bs-toggle="modal" data-bs-target="#logsModal" title="{{ t('logs.title') }}">
|
|
<i class="bi bi-journal-text"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- DM Modal (Full Screen) -->
|
|
<div class="modal fade" id="dmModal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-fullscreen">
|
|
<div class="modal-content">
|
|
<div class="modal-header bg-success text-white">
|
|
<h5 class="modal-title"><i class="bi bi-envelope"></i> {{ t('dm.title') }}</h5>
|
|
<button type="button" class="btn btn-outline-light" data-bs-dismiss="modal">
|
|
<i class="bi bi-x-lg"></i> {{ t('common.close') }}
|
|
</button>
|
|
</div>
|
|
<div class="modal-body p-0">
|
|
<iframe id="dmFrame" src="/dm" style="width: 100%; height: 100%; border: none;"></iframe>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Contact Management Modal (Full Screen) -->
|
|
<div class="modal fade" id="contactsModal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-fullscreen">
|
|
<div class="modal-content">
|
|
<div class="modal-header bg-primary text-white">
|
|
<h5 class="modal-title"><i class="bi bi-person-check"></i> {{ t('contacts.title') }}</h5>
|
|
<button type="button" class="btn btn-outline-light" data-bs-dismiss="modal">
|
|
<i class="bi bi-x-lg"></i> {{ t('common.close') }}
|
|
</button>
|
|
</div>
|
|
<div class="modal-body p-0">
|
|
<iframe id="contactsFrame" src="/contacts/manage" style="width: 100%; height: 100%; border: none;"></iframe>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Console Modal (Full Screen) -->
|
|
<div class="modal fade" id="consoleModal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-fullscreen">
|
|
<div class="modal-content" style="background-color: #1a1a2e;">
|
|
<div class="modal-header" style="background-color: #16213e; border-bottom: 1px solid #0f3460;">
|
|
<h5 class="modal-title text-white"><i class="bi bi-terminal"></i> mc-webui {{ t('console.title') }}</h5>
|
|
<button type="button" class="btn btn-outline-light" data-bs-dismiss="modal">
|
|
<i class="bi bi-x-lg"></i> {{ t('common.close') }}
|
|
</button>
|
|
</div>
|
|
<div class="modal-body p-0">
|
|
<iframe id="consoleFrame" src="/console" style="width: 100%; height: 100%; border: none;"></iframe>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- My Repeaters Modal (Full Screen) -->
|
|
<div class="modal fade" id="repeatersModal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-fullscreen">
|
|
<div class="modal-content">
|
|
<div class="modal-header bg-success text-white">
|
|
<h5 class="modal-title"><i class="bi bi-diagram-3"></i> {{ t('repeaters.title') }}</h5>
|
|
<button type="button" class="btn btn-outline-light" data-bs-dismiss="modal">
|
|
<i class="bi bi-x-lg"></i> {{ t('common.close') }}
|
|
</button>
|
|
</div>
|
|
<div class="modal-body p-0">
|
|
<iframe id="repeatersFrame" style="width: 100%; height: 100%; border: none;"></iframe>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Path Analyzer Modal (Full Screen) -->
|
|
<div class="modal fade" id="pathAnalyzerModal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-fullscreen">
|
|
<div class="modal-content">
|
|
<div class="modal-header pathanalyzer-header text-white">
|
|
<h5 class="modal-title"><i class="bi bi-signpost-split"></i> {{ t('pa.title') }}</h5>
|
|
<button type="button" class="btn btn-outline-light" data-bs-dismiss="modal">
|
|
<i class="bi bi-x-lg"></i> {{ t('common.close') }}
|
|
</button>
|
|
</div>
|
|
<div class="modal-body p-0">
|
|
<iframe id="pathAnalyzerFrame" style="width: 100%; height: 100%; border: none;"></iframe>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- System Log Modal (Full Screen) -->
|
|
<div class="modal fade" id="logsModal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-fullscreen">
|
|
<div class="modal-content" style="background-color: #1a1a2e;">
|
|
<div class="modal-header" style="background-color: #16213e; border-bottom: 1px solid #0f3460;">
|
|
<h5 class="modal-title text-white"><i class="bi bi-journal-text"></i> {{ t('logs.title') }}</h5>
|
|
<button type="button" class="btn btn-outline-light" data-bs-dismiss="modal">
|
|
<i class="bi bi-x-lg"></i> {{ t('common.close') }}
|
|
</button>
|
|
</div>
|
|
<div class="modal-body p-0">
|
|
<iframe id="logsFrame" style="width: 100%; height: 100%; border: none;"></iframe>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_scripts %}
|
|
<script>
|
|
// Pass configuration from Flask to JavaScript
|
|
window.MC_CONFIG = {
|
|
deviceName: "{{ device_name }}"
|
|
};
|
|
|
|
// Reload iframe content when modals are opened to ensure fresh data
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const dmModal = document.getElementById('dmModal');
|
|
const contactsModal = document.getElementById('contactsModal');
|
|
|
|
if (dmModal) {
|
|
dmModal.addEventListener('show.bs.modal', function () {
|
|
const dmFrame = document.getElementById('dmFrame');
|
|
if (dmFrame) {
|
|
dmFrame.src = dmFrame.src;
|
|
}
|
|
});
|
|
|
|
// Update DM badges when modal is closed (without full page reload)
|
|
dmModal.addEventListener('hidden.bs.modal', async function () {
|
|
try {
|
|
// Reload DM read status from server to sync with app.js automatic updates
|
|
// This ensures dmLastSeenTimestamps in app.js is current
|
|
if (typeof loadDmLastSeenTimestampsFromServer === 'function') {
|
|
await loadDmLastSeenTimestampsFromServer();
|
|
}
|
|
|
|
// Trigger DM badge update (now handled by app.js on FAB button)
|
|
if (typeof checkDmUpdates === 'function') {
|
|
await checkDmUpdates();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error updating DM badges:', error);
|
|
}
|
|
});
|
|
}
|
|
|
|
if (contactsModal) {
|
|
contactsModal.addEventListener('show.bs.modal', function () {
|
|
const contactsFrame = document.getElementById('contactsFrame');
|
|
if (contactsFrame) {
|
|
contactsFrame.src = contactsFrame.src;
|
|
}
|
|
});
|
|
|
|
// Update pending contacts badge when modal is closed
|
|
contactsModal.addEventListener('hidden.bs.modal', async function () {
|
|
try {
|
|
// Trigger pending contacts badge update (handled by app.js on FAB button)
|
|
if (typeof updatePendingContactsBadge === 'function') {
|
|
await updatePendingContactsBadge();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error updating pending contacts badge:', error);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Console modal - reload iframe when opened to reset WebSocket connection
|
|
const consoleModal = document.getElementById('consoleModal');
|
|
if (consoleModal) {
|
|
consoleModal.addEventListener('show.bs.modal', function () {
|
|
const consoleFrame = document.getElementById('consoleFrame');
|
|
if (consoleFrame) {
|
|
consoleFrame.src = '/console';
|
|
}
|
|
});
|
|
}
|
|
|
|
// My Repeaters modal - (re)load iframe on open for fresh data
|
|
const repeatersModal = document.getElementById('repeatersModal');
|
|
if (repeatersModal) {
|
|
repeatersModal.addEventListener('show.bs.modal', function () {
|
|
const repeatersFrame = document.getElementById('repeatersFrame');
|
|
if (repeatersFrame) {
|
|
repeatersFrame.src = '/repeaters';
|
|
}
|
|
});
|
|
}
|
|
|
|
// Path Analyzer modal - (re)load iframe on open for fresh data
|
|
const pathAnalyzerModal = document.getElementById('pathAnalyzerModal');
|
|
if (pathAnalyzerModal) {
|
|
pathAnalyzerModal.addEventListener('show.bs.modal', function () {
|
|
const pathAnalyzerFrame = document.getElementById('pathAnalyzerFrame');
|
|
if (pathAnalyzerFrame) {
|
|
// Deep link from a chat path popup (openPathInAnalyzer):
|
|
// preselect the message + echo on the map view
|
|
const dl = window.paDeepLink;
|
|
window.paDeepLink = null;
|
|
pathAnalyzerFrame.src = dl
|
|
? `/path-analyzer?hash=${encodeURIComponent(dl.hash)}&path=${encodeURIComponent(dl.path)}`
|
|
: '/path-analyzer';
|
|
}
|
|
});
|
|
}
|
|
|
|
// System Log modal - load iframe on open, clear on close
|
|
const logsModal = document.getElementById('logsModal');
|
|
if (logsModal) {
|
|
logsModal.addEventListener('show.bs.modal', function () {
|
|
const logsFrame = document.getElementById('logsFrame');
|
|
if (logsFrame) {
|
|
logsFrame.src = '/logs';
|
|
}
|
|
});
|
|
logsModal.addEventListener('hidden.bs.modal', function () {
|
|
const logsFrame = document.getElementById('logsFrame');
|
|
if (logsFrame) {
|
|
logsFrame.src = ''; // disconnect WebSocket when closed
|
|
}
|
|
});
|
|
}
|
|
});
|
|
</script>
|
|
{% endblock %}
|