Ctrl K
ComunidadGuías…

public/mobile.html

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover">
<meta name="theme-color" content="#070b14">
<title>HAMCTRL Mobile</title>
<link rel="stylesheet" href="css/mobile.css?v=202610162130">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tabler-icons/2.47.0/iconfont/tabler-icons.min.css">
</head>
<body>

<!-- window.AppState / window.WS must exist before cw.js/wsjtx.js load below —
     both capture `const S = window.AppState` at script-eval time, and both
     call bare `WS.send(...)`. mobile.js installs the REAL WS.send once its
     own socket is up; this is just a safe placeholder so nothing throws
     before that. See "REUSE, don't reinvent" note in mobile.js. -->
<script>
  window.AppState = window.AppState || { freq: 0, freqB: 0, mode: '', bands: {}, callsign: '', stationLocator: '' };
  window.WS = window.WS || { send: function () {}, ping: function () {} };
</script>

<header class="m-topbar">
  <div class="m-conn"><span id="m-conn-dot" class="m-dot"></span><span id="m-conn-label" data-i18n="m_conn_connecting">łączenie...</span></div>
  <!-- Radio (CI-V) disconnected-duration badge - distinct from m-conn above,
       which only reflects the WS link to the SERVER, not the server's own
       CI-V link to the radio (those can be up/down independently). -->
  <span id="m-rig-disconnected-badge" style="display:none;background:rgba(var(--red-rgb),0.15);color:var(--red);font-size:10px;padding:2px 6px;border-radius:3px;font-family:var(--mono);border:1px solid rgba(var(--red-rgb),0.4);"></span>
  <div class="m-user">
    <span id="callsign-display">--</span>
    <span id="user-role" class="m-role"></span>
    <button id="m-audio-btn" class="m-icon-btn" onclick="Mobile.toggleRxAudio()" data-i18n-title="m_audio_btn_title" title="Dźwięk (odsłuch)">🔇</button>
    <button class="m-icon-btn" onclick="I18n.toggle()" data-i18n-title="m_lang_toggle_title" title="Zmień język / Change language">🌐</button>
    <!-- Czat (2026-09-03) — pełnoekranowy modal zamiast stałego panelu jak
         na desktopie (za mało miejsca żeby trzymać go otwartym cały czas
         obok reszty UI). Badge pokazuje liczbę nieprzeczytanych wiadomości
         przyszłych, gdy modal jest zamknięty — patrz chat.js. -->
    <button class="m-icon-btn" onclick="Mobile.openChat()" data-i18n-title="chat_hdr" title="Czat" style="position:relative;">💬<span id="m-chat-badge" class="m-chat-badge" hidden>0</span></button>
    <button class="m-icon-btn" onclick="Mobile.openSettings()" data-i18n-title="m_settings_title" title="Ustawienia telefonu">⋮</button>
    <button class="m-logout" onclick="Auth.logout()" data-i18n="m_logout_btn">Wyloguj</button>
  </div>
</header>

<div class="m-lockbar">
  <div id="m-lock-status" data-i18n="m_lock_checking">sprawdzam blokadę radia...</div>
  <button id="m-lock-btn" class="m-btn m-btn-amber m-btn-sm" onclick="Mobile.toggleLock()">...</button>
</div>

<div id="m-toast" class="m-toast" hidden></div>

<main class="m-main">

  <!-- ── RADIO (własna, mobilna implementacja — freq/tryb/pasmo/PTT/metry) ── -->
  <section class="m-tabpanel active" id="tab-radio">

    <section class="m-card m-freq-card">
      <div class="m-vfo-row">
        <button id="m-vfoa-btn" class="m-vfo-btn active" data-vfo="VFOA" data-perm-disable="freq">VFO A</button>
        <button id="m-vfob-btn" class="m-vfo-btn" data-vfo="VFOB" data-perm-disable="freq">VFO B</button>
      </div>

      <div id="m-freq" class="m-freq">-.---.-- MHz</div>
      <div class="m-freq-other-row">
        <span>VFO B</span>
        <span id="m-freq-other" class="m-freq-other">-.---.-- MHz</span>
      </div>
      <div class="m-freq-hint" data-i18n="m_freq_hint">przytrzymaj częstotliwość, żeby wpisać ręcznie</div>

      <div id="m-mode-row" class="m-chip-row"></div>

      <div id="m-freq-strip" class="m-freq-strip" data-perm-disable="freq">
        <div class="m-freq-strip-track">
          <span class="m-freq-strip-hint" data-i18n="m_freq_strip_hint">◄ przeciągnij, żeby stroić ►</span>
        </div>
      </div>

      <div id="m-band-row" class="m-chip-row m-band-row"></div>

      <div class="m-vfo-actions">
        <button id="m-split-btn" class="m-btn m-btn-sm" data-perm-disable="split" onclick="Mobile.toggleSplit()">SPLIT</button>
        <button class="m-btn m-btn-sm" data-perm-disable="freq" onclick="Mobile.vfoSwap()">A⇄B</button>
        <button class="m-btn m-btn-sm" data-perm-disable="freq" onclick="Mobile.vfoEqualize()">A→B</button>
      </div>
    </section>

    <!-- Operatorzy online + prośby o TRX — reużywa RadioLock/OpPanel
         (public/js/radiolock.js, wydzielone z index.html) tymi samymi ID
         co desktop (op-list/op-btn-*), tylko przestylizowane. Istniejący
         prosty przełącznik zajmij/zwolnij w górnym pasku (#m-lock-status/
         #m-lock-btn) zostaje bez zmian — to dodaje to czego brakowało:
         listę kto jest online i obsługę próśb o radio. Powiadomienie o
         przychodzącej prośbie (_showRequestToast) działa automatycznie
         na całej stronie, niezależnie od tej karty. -->
    <section class="m-card">
      <details class="m-collapsible" open>
        <summary class="m-card-title">
          <span data-i18n="operators_hdr">Operatorzy</span> <span id="op-online-count" class="m-op-count">0</span>
        </summary>
        <div id="op-list" class="m-op-list"></div>
        <div id="op-lock-status" class="m-op-lock-status" data-i18n="radio_free">🔓 RADIO WOLNE</div>
        <div class="m-vfo-actions">
          <button id="op-btn-take" class="m-btn m-btn-amber m-btn-sm" onclick="RadioLock.lockRadio()" style="display:none;" data-i18n="take_trx_btn">▶ PRZEJMIJ TRX</button>
          <button id="op-btn-release" class="m-btn m-btn-red m-btn-sm" onclick="RadioLock.releaseRadio()" style="display:none;" data-i18n="release_trx_btn">✕ ODDAJ TRX</button>
        </div>
        <button id="op-btn-request" class="m-btn m-btn-amber m-btn-sm" onclick="RadioLock.requestRadio()" style="display:none;width:100%;margin-top:6px;" data-i18n="request_trx_btn">🙋 POPROŚ O TRX</button>
        <button id="op-btn-force" class="m-btn m-btn-sm" onclick="RadioLock.forceRelease()" style="display:none;width:100%;margin-top:6px;" data-i18n="force_release_btn">⚡ WYMUŚ (admin)</button>
      </details>
    </section>

    <section class="m-card m-meters">
      <div class="m-meter"><span class="m-meter-lbl">S</span><span id="m-smeter" class="m-meter-val">--</span></div>
      <div class="m-meter"><span class="m-meter-lbl">PWR</span><span id="m-pwr" class="m-meter-val">--</span></div>
      <div class="m-meter"><span class="m-meter-lbl">SWR</span><span id="m-swr" class="m-meter-val">--</span></div>
      <div class="m-meter"><span class="m-meter-lbl">ALC</span><span id="m-alc" class="m-meter-val">--</span></div>
    </section>

    <section class="m-card m-ptt-card">
      <button id="m-ptt-btn" class="m-ptt-btn" data-perm-disable="ptt">PTT</button>
    </section>

    <!-- Przyciski funkcji radia (NB/NR/COMP/MON/...) są generowane przez
         public/js/radiofunctions.js na podstawie tego, co admin udostępnił
         temu userowi (ten sam mechanizm co desktop — GET /api/rig/features)
         — mobile.html tylko daje im te same ID. FILTR/P.AMP/ATT to te same
         <select> co desktop (nigdy nie synchronizowane z powrotem z radia
         na desktopie też — czysto "wyślij i zapomnij", stan pokazuje się
         realnie na samym radiu). -->
    <section class="m-card">
      <details class="m-collapsible" open>
        <summary class="m-card-title" data-i18n="m_functions_hdr">Funkcje</summary>
        <div id="rig-features-buttons" class="rf-buttons-row"></div>
        <!-- Przekazniki (relay_ui.js, reuzyty 1:1 z desktopu - ten sam
             #relay-buttons id, ten sam /api/relay/state + /api/relay/action;
             chowa sie samo (display:none) gdy admin nie dodal zadnego). -->
        <div id="relay-buttons" class="rf-buttons-row" style="display:none;margin-top:8px;"></div>
        <div class="m-select-row">
          <span class="m-sub-lbl" data-i18n="radio_filter_lbl">FILTR (FIL1/2/3 — z menu radia)</span>
          <select id="bw-select" class="m-select" onchange="WS.send({type:'mode', mode:AppState.mode, filterNum: parseInt(this.value)})">
            <option value="1">FIL1</option>
            <option value="2">FIL2</option>
            <option value="3">FIL3</option>
          </select>
        </div>
        <div class="m-select-row">
          <span class="m-sub-lbl">P.AMP</span>
          <select id="preamp-select" class="m-select" onchange="WS.send({type:'preamp', value: parseInt(this.value)})">
            <option value="0">P.AMP OFF</option>
            <option value="1">P.AMP 1 ON</option>
            <option value="2">P.AMP 2 ON</option>
          </select>
        </div>
        <div class="m-select-row">
          <span class="m-sub-lbl">ATT</span>
          <select id="att-select" class="m-select" onchange="WS.send({type:'attenuator', value: this.value === '1'})">
            <option value="0">ATT OFF</option>
            <option value="1">ATT ON</option>
          </select>
        </div>
      </details>
    </section>

    <!-- id musi być na <details>, nie na sekcji-owijce: renderSliders()
         (radiofunctions.js) robi document.getElementById('rig-dynamic-sliders')
         i appendChild(tile) BEZPOŚREDNIO do tego elementu — gdyby id było
         na zewnętrznym <section>, suwaki wylądowałyby POZA <details> i nie
         dałoby się ich zwinąć. -->
    <section class="m-card">
      <details class="m-collapsible" id="rig-dynamic-sliders" style="display:none;">
        <summary class="m-card-title" data-i18n="m_sliders_hdr">Suwaki radia</summary>
      </details>
    </section>

  </section>

  <!-- ── FT8/FT4 — celowo UŻYWA tych samych ID co desktop (index.html /
       wsjtx.js), żeby cała logika (silnik auto-QSO, timer bezpieczeństwa,
       makra TX, lista dekodów) była TĄ SAMĄ, już przetestowaną
       logiką z desktopu — mobile.css tylko ją inaczej stylizuje. Zero
       duplikatu logiki FT8 w mobile.js. ── -->
  <section class="m-tabpanel" id="tab-ft8">

    <section class="m-card">
      <!-- Master switch for the whole FT8 pipeline (Rust decoder + waterfall
           broadcast loop, see webapp.py::ft8_rx_enable / _ft8_waterfall_loop) -
           desktop has always had this button (id="wj-own-rx-btn", same
           WSJTX.toggleOwnRx()), but the mobile FT8 tab build-out never added
           an equivalent control. Without it there was no way to ever turn
           decoding/waterfall on from the phone - explains "black waterfall,
           empty decode list despite strong S-meter" exactly (both the Rust
           decode loop and the waterfall broadcast loop are gated behind
           self._ft8_rx_enabled, which starts False on every server start). -->
      <button id="wj-own-rx-btn" onclick="WSJTX.toggleOwnRx()" class="m-btn m-ft8-own-rx-btn" data-i18n="wj_own_rx_start">▶ START (własny RX)</button>
      <div class="m-ft8-top-row">
        <div class="wj-mode-switch" id="wj-mode-switch">
          <button class="wj-mode-btn active" id="wj-mode-ft8-btn" onclick="WSJTX.setDecodeMode('FT8')">FT8</button>
          <button class="wj-mode-btn" id="wj-mode-ft4-btn" onclick="WSJTX.setDecodeMode('FT4')">FT4</button>
        </div>
        <div class="wsjtx-clock-wrap">
          <canvas id="wj-clock" width="28" height="28"></canvas>
          <div id="wj-clock-s" class="m-ft8-clock-s">0</div>
        </div>
        <div class="wsjtx-status-pill" id="wj-status-pill">○ OFFLINE</div>
      </div>
      <select id="wj-band-select" class="wj-band-select" onchange="WSJTX.tuneToBand(this.value)">
        <option value="" data-i18n="wj_band_placeholder">-- pasmo --</option>
      </select>
      <div class="m-ft8-check-row">
        <label class="m-switch-row-inline">
          <input type="checkbox" id="wj-cq-only-toggle" onchange="WSJTX.toggleCqOnly()"> CQ
        </label>
        <label class="m-switch-row-inline m-hound-check">
          <input type="checkbox" id="wj-hound-toggle" onchange="WSJTX.toggleHound(this.checked)"> HOUND
        </label>
        <!-- Ten sam przycisk co desktop (index.html) - WSJTX.houndConfirm()
             istnial ale nic go nie wolalo, patrz ft8_timer_hound_ui_fixes. -->
        <button id="wj-hound-confirm-btn" onclick="WSJTX.houndConfirm()" style="display:none;"
          class="m-btn m-btn-amber m-btn-sm" data-i18n="wj_hound_confirm_btn">✓ POTWIERDŹ</button>
      </div>
      <!-- Elementy Hound ukryte tak samo jak na desktopie (index.html:
           "Ukryte elementy Hound dla JS" - wsjtx.js sam wypelnia/czyta
           te ID, nigdy nie mialy widocznego panelu nawet na desktopie -
           przelacznik wyzej + dotkniecie wodospadu wystarcza do obslugi. -->
      <input type="checkbox" id="wj-auto-seq-toggle" checked style="display:none;">
      <div id="wj-hound-status" style="display:none;"></div>
      <div id="wj-hound-seq" style="display:none;"></div>
      <div id="wj-hound-step-1" style="display:none;"><span id="wj-hound-step-1-status"></span></div>
      <div id="wj-hound-step-2" style="display:none;"><span id="wj-hound-step-2-status"></span></div>
      <div id="wj-hound-step-3" style="display:none;"><span id="wj-hound-step-3-status"></span></div>
      <div id="wj-hound-step-4" style="display:none;"><span id="wj-hound-step-4-status"></span></div>
      <span id="wj-hound-fox-call" style="display:none;"></span>
      <span id="wj-hound-tx-freq" style="display:none;"></span>
      <span id="wj-hound-attempts" style="display:none;"></span>
      <div id="wj-hound-active-badge" style="display:none;"></div>
      <div id="wj-hound-desc" style="display:none;"></div>
      <div id="wj-hound-status-inline" style="display:none;"></div>
      <button id="wj-hound-stop-btn" style="display:none;" onclick="WSJTX.houndStop()"></button>
    </section>

    <!-- Mini-wodospad — ten sam wj-scope-canvas + wsjtx_scope.js co desktop
         (WebGL, dane/palety identyczne), tylko nizszy przez CSS. Dotyk
         dziala dzieki dopisanym w wsjtx_scope.js touchstart/move/end
         (desktop mial tylko obsluge myszy). -->
    <section class="m-card m-ft8-scope-card">
      <div class="m-ft8-scope-wrap">
        <canvas id="wj-scope-canvas" class="m-ft8-scope-canvas"></canvas>
      </div>
      <div class="m-ft8-df-row">
        <span>RX <span id="wj-rx-df">--</span> Hz</span>
        <span>TX <span id="wj-tx-df">--</span> Hz</span>
      </div>
    </section>

    <section class="m-card">
      <div id="wj-autoqso-status" class="m-ft8-status" data-i18n="m_autoqso_ready">Automatyka gotowa — dotknij wywołanie CQ na liście, żeby rozpocząć QSO</div>
      <div class="m-ft8-timer-row">
        <span id="ft8-timer-display" class="m-ft8-timer">--:--</span>
        <button id="ft8-timer-confirm" class="m-btn m-btn-amber m-btn-sm" onclick="FT8Timer.confirm()" style="display:none" data-i18n="m_confirm_presence">Potwierdzam obecność</button>
      </div>
      <div class="m-ft8-actions">
        <button id="wj-autoqso-skip" class="m-btn m-btn-red m-btn-sm" data-perm-disable="ptt" onclick="WSJTX.skipAutoQso()" style="display:none" data-i18n="m_skip_qso_btn">Pomiń QSO</button>
        <!-- No data-perm-disable: HALT TX is a stop action (same rule as
             ptt:false/ft8_tune_stop/webrtc_stop - never gate a stop), and
             matches desktop's wj-halt-tx-btn which has no gate either. -->
        <button id="wj-halt-tx-btn" class="m-btn m-btn-red m-btn-sm" onclick="WSJTX.haltTx()">STOP TX</button>
      </div>
    </section>

    <section class="m-card">
      <div class="m-card-title" data-i18n="m_dx_card_title">DX (znak korespondenta)</div>
      <input id="wj-dx-call" type="text" class="m-text-input" placeholder="-- dotknij wywołanie na liście lub wpisz --"
        data-i18n-placeholder="m_dx_call_ph"
        onkeydown="if(event.key==='Enter'){WSJTX.searchDxCall(this.value)}"
        onblur="WSJTX.searchDxCall(this.value)">
      <div class="m-card-title" style="margin-top:10px;" data-i18n="m_tx_macros_hdr">Makra TX</div>
      <div class="wsjtx-tx-grid m-tx-grid">
        <button class="wj-tx-btn m-tx-btn" id="wj-tx1" data-perm-disable="ptt" onclick="WSJTX.sendTx(1)">
          <span class="wj-tx-num m-tx-btn-lbl">F1</span>
          <span class="wj-tx-text m-tx-btn-txt" id="wj-tx1-text">CQ {MYCALL} {GRID}</span>
        </button>
        <button class="wj-tx-btn m-tx-btn" id="wj-tx2" data-perm-disable="ptt" onclick="WSJTX.sendTx(2)">
          <span class="wj-tx-num m-tx-btn-lbl">F2</span>
          <span class="wj-tx-text m-tx-btn-txt" id="wj-tx2-text">{DXCALL} {MYCALL} {GRID}</span>
        </button>
        <button class="wj-tx-btn m-tx-btn" id="wj-tx3" data-perm-disable="ptt" onclick="WSJTX.sendTx(3)">
          <span class="wj-tx-num m-tx-btn-lbl">F3</span>
          <span class="wj-tx-text m-tx-btn-txt" id="wj-tx3-text">{DXCALL} {MYCALL} R{REPORT}</span>
        </button>
        <button class="wj-tx-btn m-tx-btn" id="wj-tx4" data-perm-disable="ptt" onclick="WSJTX.sendTx(4)">
          <span class="wj-tx-num m-tx-btn-lbl">F4</span>
          <span class="wj-tx-text m-tx-btn-txt" id="wj-tx4-text">{DXCALL} {MYCALL} RRR</span>
        </button>
        <button class="wj-tx-btn m-tx-btn" id="wj-tx5" data-perm-disable="ptt" onclick="WSJTX.sendTx(5)">
          <span class="wj-tx-num m-tx-btn-lbl">F5</span>
          <span class="wj-tx-text m-tx-btn-txt" id="wj-tx5-text">{DXCALL} {MYCALL} 73</span>
        </button>
        <button class="wj-tx-btn m-tx-btn" id="wj-tx6" data-perm-disable="ptt" onclick="WSJTX.sendTx(6)">
          <span class="wj-tx-num m-tx-btn-lbl">F6</span>
          <span class="wj-tx-text m-tx-btn-txt" id="wj-tx6-text">{DXCALL} RR73</span>
        </button>
      </div>
    </section>

    <section class="m-card m-ft8-decodes-card">
      <div class="m-card-title-row">
        <span class="m-card-title" data-i18n="m_received_calls_hdr">Odebrane wywołania</span>
        <span class="m-mini-btn" onclick="WSJTX.clearDecodes()" data-i18n="m_clear_btn">wyczyść</span>
      </div>
      <div id="wj-decodes" class="wsjtx-decode-list m-ft8-decodes"></div>
    </section>

    <!-- Rzadziej dotykane ustawienia TX — zwinięte domyślnie, te same ID/
         funkcje wsjtx.js co desktop (period switch, TX/RX freq, freeze,
         Fake Split, TUNE). HALT TX zostaje wyżej, zawsze widoczny (bezpieczeństwo). -->
    <section class="m-card">
      <details class="m-collapsible">
        <summary class="m-card-title" data-i18n="m_tx_settings_hdr">Ustawienia TX</summary>
        <div class="wj-period-switch m-ft8-period-switch" id="wj-period-switch">
          <button class="wj-mode-btn active" id="wj-period-1-btn" onclick="WSJTX.setTxPeriod(1)">1st</button>
          <button class="wj-mode-btn" id="wj-period-2-btn" onclick="WSJTX.setTxPeriod(2)">2nd</button>
        </div>
        <div class="m-ft8-freq-row">
          <span class="m-sub-lbl">TX Hz</span>
          <input type="number" id="wj-tx-freq-input" class="m-text-input m-ft8-freq-input" step="1"
            onchange="WSJTX.setTxFreqManual(this.value)">
          <button id="wj-rx-eq-tx-btn" class="m-btn m-btn-sm" onclick="WSJTX.rxEqTx()">RX=TX</button>
        </div>
        <div class="m-ft8-freq-row">
          <span class="m-sub-lbl">RX Hz</span>
          <input type="number" id="wj-rx-freq-input" class="m-text-input m-ft8-freq-input" step="1"
            onchange="WSJTX.setRxFreqManual(this.value)">
          <button id="wj-tx-eq-rx-btn" class="m-btn m-btn-sm" onclick="WSJTX.txEqRx()">TX=RX</button>
        </div>
        <div class="m-ft8-actions">
          <button id="wj-tx-freeze-btn" class="m-btn m-btn-sm" onclick="WSJTX.toggleTxFreeze()" data-i18n="wj_tx_free_btn">🔓 TX WOLNY</button>
          <button id="wj-fake-split-toggle" class="m-btn m-btn-sm" onclick="WSJTX.toggleFakeSplit()" data-i18n="wj_fake_split_off">⭕ Fake Split: OFF</button>
        </div>
        <div id="wj-fake-split-target" class="m-ft8-fake-split-target"></div>
        <button id="wj-tune-btn" class="m-btn m-btn-sm" onclick="WSJTX.startTune()" style="margin-top:6px;width:100%;">🎵 TUNE</button>
      </details>
    </section>

    <!-- Filtry widoku i palety wodospadu — zwinięte, dotykane rzadko. -->
    <section class="m-card">
      <details class="m-collapsible">
        <summary class="m-card-title" data-i18n="m_filters_hdr">Filtry / widok</summary>
        <label class="m-switch-row">
          <span data-i18n="m_hide_worked_lbl">Ukryj pracowane (HIDE WORKED)</span>
          <input type="checkbox" id="wj-hide-worked" onchange="WSJTX.toggleHideWorked(this)">
        </label>
        <div class="m-wpm-row">
          <span class="m-sub-lbl">REF</span>
          <input type="range" id="wj-palette-ref" class="m-wpm-slider" min="0" max="100" value="15" step="1"
            oninput="WSJTXScope.setPaletteReference(this.value/100);document.getElementById('wj-palette-ref-val').textContent=this.value">
          <span id="wj-palette-ref-val" class="m-wpm-val">15</span>
        </div>
        <div class="m-wpm-row">
          <span class="m-sub-lbl">ZERO</span>
          <input type="range" id="wj-palette-zero" class="m-wpm-slider" min="0" max="60" value="0" step="1"
            oninput="WSJTXScope.setPaletteZero(this.value/100);document.getElementById('wj-palette-zero-val').textContent=this.value">
          <span id="wj-palette-zero-val" class="m-wpm-val">0</span>
        </div>
        <div class="m-wpm-row">
          <span class="m-sub-lbl">GAIN</span>
          <input type="range" id="wj-palette-gain" class="m-wpm-slider" min="30" max="300" value="100" step="5"
            oninput="WSJTXScope.setPaletteGain(this.value/100);document.getElementById('wj-palette-gain-val').textContent=(this.value/100).toFixed(1)">
          <span id="wj-palette-gain-val" class="m-wpm-val">1.0</span>
        </div>
        <button onclick="WSJTX.resetPaletteAdjust()" class="m-btn m-btn-sm" style="width:100%;margin-top:4px;" data-i18n="m_reset_palette_btn">↺ Reset palety</button>
      </details>
    </section>

  </section>

  <!-- ── CW — też na ID desktopu (cw.js), tylko przestylizowane na dotyk;
       edycja makra wprost na telefonie (dotknij i przytrzymaj karte). ── -->
  <section class="m-tabpanel" id="tab-cw">

    <section class="m-card">
      <div id="cw-status" class="m-cw-status" data-i18n="cw_status_ready">● GOTOWY</div>
      <div class="m-wpm-row">
        <span class="m-sub-lbl">WPM</span>
        <input type="range" min="6" max="48" step="1" value="18" id="cw-wpm-slider"
          class="m-wpm-slider" data-perm-disable="cw" oninput="CW.setWPM(this.value)">
        <span id="cw-wpm-val" class="m-wpm-val">18</span>
      </div>
      <div id="cw-macros-grid" class="m-macro-grid">
        <div class="m-macro-card" id="cwm-1" data-id="1"><span class="m-macro-lbl" id="cwm-lbl-1">F1</span><span class="m-macro-txt" id="cwm-txt-1">—</span></div>
        <div class="m-macro-card" id="cwm-2" data-id="2"><span class="m-macro-lbl" id="cwm-lbl-2">F2</span><span class="m-macro-txt" id="cwm-txt-2">—</span></div>
        <div class="m-macro-card" id="cwm-3" data-id="3"><span class="m-macro-lbl" id="cwm-lbl-3">F3</span><span class="m-macro-txt" id="cwm-txt-3">—</span></div>
        <div class="m-macro-card" id="cwm-4" data-id="4"><span class="m-macro-lbl" id="cwm-lbl-4">F4</span><span class="m-macro-txt" id="cwm-txt-4">—</span></div>
        <div class="m-macro-card" id="cwm-5" data-id="5"><span class="m-macro-lbl" id="cwm-lbl-5">F5</span><span class="m-macro-txt" id="cwm-txt-5">—</span></div>
        <div class="m-macro-card" id="cwm-6" data-id="6"><span class="m-macro-lbl" id="cwm-lbl-6">F6</span><span class="m-macro-txt" id="cwm-txt-6">—</span></div>
      </div>
      <div class="m-cw-hint" data-i18n="m_cw_hint">dotknij = wyślij • przytrzymaj = edytuj</div>
      <div class="m-cw-custom-row">
        <input id="cw-custom-input" class="m-text-input" type="text" placeholder="własny tekst CW..." data-i18n-placeholder="m_cw_custom_ph" data-perm-disable="cw">
        <button class="m-btn m-btn-sm" data-perm-disable="cw" onclick="CW.sendCustom()" data-i18n="m_send_btn">Wyślij</button>
      </div>
      <button class="m-btn m-btn-red m-btn-sm" data-perm-disable="cw" onclick="CW.stopCW()">STOP</button>
    </section>

  </section>

  <!-- ── LOG — szybkie dodanie QSO reużywa qsolog.js::quickLog() (te same
       ID co desktopowy pasek "SZYBKIE LOGOWANIE QSO"): pasmo/tryb/freq
       biora sie automatycznie z window.AppState (freq/mode), które
       mobile.js trzyma zsynchronizowane z własnym stanem. ── -->
  <section class="m-tabpanel" id="tab-log">
    <section class="m-card">
      <div class="m-card-title" data-i18n="m_quicklog_hdr">Szybki log QSO</div>
      <div class="m-qlog-row">
        <input type="text" id="qlog-call" class="m-text-input" placeholder="CALL"
          onkeydown="if(event.key==='Enter')QSOLog.quickLog()"
          oninput="QSOLog.checkQuickLogWorkedBefore(this.value);QSOLog.updateQuickLogHint()">
      </div>
      <div class="m-qlog-row">
        <div class="m-qlog-field">
          <span class="m-sub-lbl">SENT</span>
          <input type="text" id="qlog-rst-s" class="m-text-input" placeholder="599" maxlength="3" value="599">
        </div>
        <div class="m-qlog-field">
          <span class="m-sub-lbl">RCVD</span>
          <input type="text" id="qlog-rst-r" class="m-text-input" placeholder="599" maxlength="3" value="599">
        </div>
        <div class="m-qlog-field">
          <span class="m-sub-lbl">GRID</span>
          <input type="text" id="qlog-grid" class="m-text-input" placeholder="KO02" maxlength="8"
            oninput="QSOLog.updateQuickLogHint()">
        </div>
      </div>
      <button class="m-btn m-btn-amber m-btn-sm" onclick="QSOLog.quickLog()">+ LOG QSO</button>
      <div id="qlog-status" class="m-qlog-status"></div>
    </section>
    <section class="m-card m-log-card">
      <div class="m-card-title" data-i18n="m_log_hdr">Log QSO</div>
      <div id="m-log-list" class="m-log-list"><div class="m-log-empty" data-i18n="settings_loading">ładowanie...</div></div>
    </section>
  </section>

  <!-- ── ROTOR — reużywa rotormini.js (RotW) tak jak FT8/CW/RadioFunctions/
       QSOLog: te same ID (rot-*), własny <details>/<summary> nie jest tu
       potrzebny bo cała karta i tak chowa się sama (RotW.init/updateUI
       robi panel.style.display='none' gdy nie ma skonfigurowanego rotora). ── -->
  <section class="m-tabpanel" id="tab-rotor">
    <section class="m-card" id="rot-side-panel">
      <div class="m-rot-header">
        <span class="m-card-title" data-i18n="m_rotor_hdr">Rotor</span>
        <span id="rot-status-badge" class="m-rot-badge" data-i18n="m_rotor_none_badge">● brak</span>
      </div>
      <div class="m-rot-compass-wrap">
        <canvas id="rot-compass" width="205" height="205"></canvas>
        <div class="m-rot-az-overlay"><span id="rot-az-display">---</span>°</div>
      </div>
      <div id="rot-name-label" class="m-rot-name"></div>
      <input id="rot-target-inp" type="text" class="m-text-input" placeholder="272 lub KO02" data-perm-disable="rotator"
        onkeydown="if(event.key==='Enter')RotW.go()" oninput="RotW.onInput()">
      <div id="rot-target-preview" class="m-rot-preview"></div>
      <div class="m-vfo-actions">
        <button class="m-btn m-btn-amber m-btn-sm" data-perm-disable="rotator" onclick="RotW.go()">▶ START</button>
        <button class="m-btn m-btn-red m-btn-sm" data-perm-disable="rotator" onclick="RotW.stop()">■ STOP</button>
      </div>
    </section>
  </section>

  <a class="m-desktop-link" href="/?ui=desktop" data-i18n="m_desktop_link">Pełna wersja (desktop)</a>

</main>

<nav class="m-tabbar">
  <button class="m-tab active" data-tab="radio">📻<span data-i18n="m_tab_radio">Radio</span></button>
  <button class="m-tab" data-tab="ft8">📡<span data-i18n="m_tab_ft8">FT8</span></button>
  <button class="m-tab" data-tab="cw">📟<span data-i18n="m_tab_cw">CW</span></button>
  <button class="m-tab" data-tab="log">📋<span data-i18n="m_tab_log">Log</span></button>
  <button class="m-tab" data-tab="rotor">🧭<span data-i18n="m_tab_rotor">Rotor</span></button>
</nav>

<!-- Ustawienia telefonu ("⋮") — NIE zakładka na dolnym pasku, to celowo
     osobne miejsce na rzeczy dotyczące tego konkretnego telefonu/
     przeglądarki (nie funkcji radia): dźwięk RX + wybór mikrofonu TX.
     Mikrofon zapisuje się pod TYM SAMYM kluczem localStorage co
     desktopowy PROFIL (ham_audio_micId, patrz profile_audio.js), więc
     wybór zrobiony kiedyś na tej samej przeglądarce na desktopie
     obowiązuje też tutaj. -->
<div id="m-settings-modal" class="m-modal" style="display:none">
  <div class="m-modal-box">
    <div class="m-card-title" data-i18n="m_settings_title">Ustawienia telefonu</div>
    <label class="m-switch-row">
      <span data-i18n="m_audio_toggle_lbl">🔊 Dźwięk (odsłuch)</span>
      <input type="checkbox" id="m-settings-audio-toggle" onchange="Mobile.toggleRxAudio(this.checked)">
    </label>
    <div class="m-select-row">
      <span class="m-sub-lbl" data-i18n="m_mic_lbl">MIKROFON (nadawanie)</span>
      <select id="m-mic-select" class="m-select" onchange="Mobile.setMicDevice(this.value)">
        <option value="" data-i18n="m_mic_default_opt">-- domyślny --</option>
      </select>
    </div>
    <div class="m-modal-actions">
      <button class="m-btn m-btn-sm" onclick="Mobile.closeSettings()" data-i18n="m_close_btn">Zamknij</button>
    </div>
  </div>
</div>

<!-- CZAT — pełnoekranowy modal. #chat-messages/#chat-input mają CELOWO
     te same id co na desktopie (index.html), żeby chat.js dzialal
     niezmieniony na obu platformach (ten sam wzorzec co wsjtx.js/cw.js/
     rotormini.js/radiolock.js gdzie indziej w tym projekcie) - tylko
     "shell" wokol jest inny (modal zamiast stalego panelu). -->
<div id="m-chat-modal" class="m-modal" style="display:none" onclick="if(event.target===this) Mobile.closeChat()">
  <div class="m-modal-box m-chat-modal-box">
    <div class="m-card-title" data-i18n="chat_hdr">CZAT</div>
    <div id="chat-messages" class="m-chat-messages"></div>
    <div class="m-chat-input-row">
      <input id="chat-input" type="text" maxlength="500" class="m-text-input" data-i18n-placeholder="chat_input_ph" placeholder="Wiadomość…">
      <button class="m-btn m-btn-sm" onclick="Chat.send()">➤</button>
    </div>
    <div class="m-modal-actions">
      <button class="m-btn m-btn-sm" onclick="Mobile.closeChat()" data-i18n="m_close_btn">Zamknij</button>
    </div>
  </div>
</div>

<!-- Ręczne wpisanie częstotliwości — przytrzymanie #m-freq/#m-freq-other
     (initFreqEditGestures w mobile.js) otwiera ten modal dla VFO A/B. -->
<div id="m-freq-edit-modal" class="m-modal" style="display:none">
  <div class="m-modal-box">
    <div class="m-card-title" id="m-freq-edit-title">Częstotliwość</div>
    <input id="m-freq-edit-input" class="m-text-input" type="text" inputmode="decimal"
      placeholder="np. 14.074 (MHz)" data-i18n-placeholder="m_freq_edit_ph">
    <div class="m-modal-actions">
      <button class="m-btn m-btn-sm" onclick="Mobile.closeFreqEdit()" data-i18n="common_cancel_btn">Anuluj</button>
      <button class="m-btn m-btn-amber m-btn-sm" onclick="Mobile.saveFreqEdit()" data-i18n="m_set_btn">Ustaw</button>
    </div>
  </div>
</div>

<!-- Generyczny modal potwierdzenia — window.UI.confirmModal() w mobile.js,
     ten sam kontrakt (message, {title,okLabel,danger}) co desktopowy
     #confirm-modal (ui.js), potrzebny żeby RadioLock.forceRelease()
     (reużyte z radiolock.js) miało na czym zapytać "na pewno?" zamiast
     cicho nic nie robić (window.UI?.confirmModal bez tego zwracał
     undefined -> "wymuś" nigdy by faktycznie nie zadzialalo). -->
<div id="confirm-modal" class="m-modal" style="display:none" onclick="if(event.target===this) UI._confirmModalCancel()">
  <div class="m-modal-box">
    <div class="m-card-title" id="confirm-modal-title" data-i18n="common_confirm_title">POTWIERDŹ</div>
    <div id="confirm-modal-msg" class="m-confirm-msg"></div>
    <div class="m-modal-actions">
      <button class="m-btn m-btn-sm" onclick="UI._confirmModalCancel()" data-i18n="common_cancel_btn">Anuluj</button>
      <button id="confirm-modal-ok" class="m-btn m-btn-amber m-btn-sm" onclick="UI._confirmModalSubmit()">OK</button>
    </div>
  </div>
</div>

<!-- Edycja makra CW — te same ID co desktopowy #cw-edit-modal (cw.js:
     startEdit/saveEditMacro czytają/pisza dokladnie te pola), tylko
     wyglad z mobile.css. -->
<div id="cw-edit-modal" class="m-modal" style="display:none">
  <div class="m-modal-box">
    <div class="m-card-title" id="cw-edit-title" data-i18n="cw_edit_macro_title">Makro</div>
    <input type="hidden" id="cw-edit-id">
    <input id="cw-edit-label" class="m-text-input" type="text" placeholder="etykieta (np. CQ)" data-i18n-placeholder="m_cw_label_ph" maxlength="12">
    <textarea id="cw-edit-text" class="m-textarea" placeholder="treść makra (np. CQ CQ DE {MYCALL} K)" data-i18n-placeholder="m_cw_macro_text_ph"></textarea>
    <div class="m-modal-actions">
      <button class="m-btn m-btn-sm" onclick="CW.closeEdit()" data-i18n="common_cancel_btn">Anuluj</button>
      <button class="m-btn m-btn-amber m-btn-sm" onclick="CW.saveEditMacro()" data-i18n="m_save_btn">Zapisz</button>
    </div>
  </div>
</div>

<script src="js/auth.js?v=202609030005"></script>
<script src="js/i18n.js?v=202609060001"></script>
<script src="js/cw.js?v=202608260003"></script>
<script src="js/beamheading.js?v=202607230001"></script>
<script src="js/wsjtx.js?v=202609050001"></script>
<script src="js/wsjtx_scope.js?v=202608242200"></script>
<script src="js/radiolock.js?v=202610162130"></script>
<script src="js/chat.js?v=202609030004"></script>
<script src="js/radiofunctions.js?v=202609040002"></script>
<script src="js/relay_ui.js?v=202610162130"></script>
<script src="js/qsolog.js?v=202610162130"></script>
<script src="js/rotormini.js?v=202610150002"></script>
<script src="js/mobile_audio.js?v=202609020001"></script>
<script src="js/mobile.js?v=202609030004"></script>
</body>
</html>