<?php
/**
 * v2.1.3 - dodano wersję API do wyszukiwania plików: ?q=teb&recursive=1&api=1
 * v2.2.0 - dodano wyświetlanie rozmiaru pliku w MB
 * v2.2.1 - Iteracja 1: poprawiono formularz logowania, usunięto martwy kod po stream_audio, poprawiono walidację pobierania pliku oraz zabezpieczono niezaimplementowaną akcję convert_to_mp4 przed błędem krytycznym
 * v2.2.2 - Iteracja 2: dodano helper BuildStreamUrl() i scentralizowano generowanie linków stream oraz stream_audio dla audio/wideo
 * v2.2.3 - Iteracja 3: dodano helper EscapeHtml() i ujednolicono escapowanie HTML w listach plików, wynikach wyszukiwania oraz linkach audio/wideo
 * v2.2.4 - Iteracja 4: dodano renderer RenderFileActions() i ograniczono duplikację HTML dla akcji plików
 * v2.2.5 - Iteracja 5: domknięto użycie EscapeHtml() w formularzu wyszukiwania, komunikatach i breadcrumbs oraz poprawiono formatowanie helperów
 * v2.2.6 - Iteracja 6: dodano ResolveStreamPathFromRequest() i usunięto duplikację dekodowania parametrów stream oraz stream_audio
 * v2.2.7 - Iteracja 7: dodano SendPlainHttpError() i ujednolicono odpowiedzi błędów HTTP dla download, stream oraz stream_audio
 * v2.2.8 - Iteracja 8: dodano zmienną $sAction i uproszczono warunki obsługi akcji download, stream oraz stream_audio
 * v2.2.9 - Iteracja 9: dodano GetRequestString() i ujednolicono pobieranie parametrów GET dla action, file, dir oraz streamingu
 * v2.3.0 - Iteracja 10: dodano lokalne zmienne requestu dla file, dir i ograniczono wielokrotne wywołania GetRequestString()
 * v2.3.1 - Iteracja 11: dodano mini-dispatcher switch dla akcji stream oraz stream_audio
 * v2.3.2 - Iteracja 12: uporządkowano pobieranie parametrów API wyszukiwania przez GetRequestString()
 * v2.3.3 - Iteracja 13: dodano SendJsonResponse() i ujednolicono zwracanie odpowiedzi JSON dla API wyszukiwania
 * v2.3.4 - Iteracja 14: ujednolicono błędy API wyszukiwania jako odpowiedzi JSON
 * v2.3.5 - Iteracja 15: dodano automatyczny tryb galerii zdjęć z centralnym podglądem, miniaturami i paginacją grafik
 * v2.3.6 - dodano widok wideo (RenderVideoViewer, RenderVideoPagination, GetVideoFiles) z paginacją i nawigacją klawiaturową
 * v2.4.0 - poprawki bezpieczeństwa: XSS w live search (escHtml JS), password_hash dla config.php, fix path traversal (str_starts_with+separator)
 * v2.4.1 - generowanie miniaturek JPEG po stronie serwera (action=thumb): cache w .thumbcache/, EXIF orientation, HTTP 304/ETag, białe tło dla PNG/GIF
 * v2.4.2 - poprawki bezpieczeństwa: CSRF dla delete_config (POST+token), X-Frame-Options: SAMEORIGIN, Referrer-Policy
 * v2.5.0 - weryfikacja integralności plików: SHA-256 snapshot per katalog (.integrity.json), porównanie zmian/nowych/usuniętych
 * v2.6.0 - widok audio: odtwarzacz w stylu Spotify (view=audio) z playlistą, progress barem, skrubbingiem, klawiaturą i responsywnym layoutem
 * v2.7.0 - nowy dolny player (Spotify-style): obracająca się ikona, progress bar, seek, volume, play/pause btn; view=audio deleguje do globalnego window.AP; "Odtwórz wszystkie" / "Dodaj wszystkie do kolejki"
 * v2.8.0 - view=audio: dolny pasek nie pojawia się (initPlayerIfNeeded pomija inicjalizację gdy #apViewer aktywny); ap-main sticky — player widoczny, lista przewija się pod spodem
 * v2.9.0 - cache rozmiarów katalogów (DirSizeCache, fingerprint mtime + TTL 30d); przebudowa gallery view: fullscreen grid kafelkowy, lightbox z animacją expand, lazy load, hotspoty boczne, klawiatura ←→Esc
 * v2.9.1 - refaktoryzacja: getExt(), parseRangeHeader(), ujednolicenie DisableOutputCompression(); poprawki CSS (z-index, body login); JS getShuffledIndex(), closeModal()
 * v2.9.2 - redukcja kodu: array_filter, getMimeType(), delegacja eventów, usunięto duplikaty CSS/wrapperów
 * v2.9.3 - trwały cache ZIP mediów (zip_build SSE → .media_cache.zip, zip_cached dla userów, zip_cache_delete); lazy loading rozmiarów katalogów (action=dir_size, sequential AJAX)
 * v2.9.4 - snapshot integralności przez SSE z paskiem postępu (streamIntegrityBuild); szybki fingerprint dla plików >50MB (mtime+size); dynamiczny tytuł modala
 * v2.9.5 - bezpieczeństwo: isPathAuthorized() — stream/thumb/download/zip respektują ochronę config.php na całej ścieżce; stream tylko media (blokada config.php/json); isWithinBase() z separatorem; CSRF w create_config; session_regenerate_id po logowaniu; hash_equals dla admina; globalny nosniff
 * v2.9.6 - isHiddenEntry(): centralne ukrywanie wrażliwych plików (dotfiles, php/json/sql/db/env/key/log/bak, backupy ~, macOS ._*) w listingu, wyszukiwarce i pobieraniu; usunięto martwe disallowedExtensions/isMacJunkFile
 * v3.0.0 - wydanie stabilne: konsolidacja hardeningu bezpieczeństwa (autoryzacja ścieżek, ukrywanie wrażliwych plików, CSRF, sesje) oraz funkcji ZIP/integralności/lazy-load z gałęzi 2.9.x
 *
 **/
define('SOFTWARE_VERSION', 'v3.0.0');
define('ADMIN_USER', 'admin');
define('ADMIN_PASS', 'admin123');              // ✱ użyj haszowania w produkcji

$bLocalDev = in_array($_SERVER['REMOTE_ADDR'] ?? '', ['127.0.0.1', '::1'], true);
ini_set('display_errors', $bLocalDev ? '1' : '0');
ini_set('display_startup_errors', $bLocalDev ? '1' : '0');
error_reporting($bLocalDev ? E_ALL : 0);

if (session_status() === PHP_SESSION_NONE)
{
    session_set_cookie_params([
        'httponly' => true,
        'samesite' => 'Lax',
        'secure'   => (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'),
    ]);
    session_start();
}

if (empty($_SESSION['csrf_token']))
{
    $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}

header('X-Frame-Options: SAMEORIGIN');
header('Referrer-Policy: strict-origin-when-cross-origin');
header('X-Content-Type-Options: nosniff');


class View
{

    // --- HEAD / FOOTER ---
    public static function headLogin($title = 'File Browser')
    {
        return <<<HTML
        <html lang="pl">
        <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width,initial-scale=1">
        <title>{$title}</title>
        <style>
            body{font-family:Arial;background:#111;color:#ddd;display:flex;flex-direction:column;justify-content:center;align-items:center;min-height:100vh;margin:0}
            .box{margin-top: 40%; background:#1c1c1c;padding:30px;border-radius:8px;box-shadow:0 0 10px #000;width:320px}
            input{width:100%;padding:10px;margin:10px 0;background:#333;border:1px solid #555;color:#fff;border-radius:4px}
            button{width:100%;padding:10px;background:#444;color:#fff;border:none;border-radius:4px;cursor:pointer}
            button:hover{background:#555}
            .error{color:#ff6666;text-align:center;margin-bottom:10px}
            .main-content { flex: 1; }
            .footer { width: 100%; padding: 10px; background-color: #1c1c1c; color: #888; text-align: center; border-top: 1px solid #333; }
        </style>
        </head>
        <body>

        HTML;
    }

    public static function footer()
    {
        return "<footer class=\"footer\">Wersja oprogramowania: ".SOFTWARE_VERSION." </footer></body></html>";
    }


    // --- LOGIN FORM (uniwersalny) ---
    // $action może być '' aby użyć aktualnego URL
    public static function loginForm($action = '', $error = '', $sTitle='', $extraFields = '')
    {

        $actionEsc = htmlspecialchars($action ?: $_SERVER['REQUEST_URI'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
        $errHtml   = $error ? $error : '';

return <<<HTML
            <div class="main-content">
            <div class="box">
                <form method="post" action="{$actionEsc}">
                    <h2 style="text-align:center;margin:0 0 10px">{$sTitle}</h2>
                    {$extraFields}
                    <input type="text" name="username" placeholder="Login" required>
                    <input type="password" name="password" placeholder="Hasło" required>
                    <button type="submit">Zaloguj</button>

                    <h3>{$errHtml}</h3>
                </form>
            </div>
            </div>

        HTML;
    }

    public function setStyleCss()
    {
        return <<<HTML
    <style>
    /* styl CSS bez zmian */
    body {
        font-family: Arial, sans-serif;
        background-color: #1c1c1c;
        color: #ffffff;
        margin: 0;
        padding: 0;
    }

    /* iOS: nie wymuszaj powiększania tekstu */
    html { -webkit-text-size-adjust: 100%; }

    /* Wygląd pola wyszukiwania na mobile/retina */
    #searchInput{
      -webkit-appearance: none;
      appearance: none;
      font-size: 16px;       /* anty-zoom iOS */
      line-height: 1.2;
      border-radius: 10px;
      touch-action: manipulation;
    }
    #searchInput::placeholder{ color:#aaa; }
    #searchInput:focus{
      outline: 2px solid #4aa3ff;
      outline-offset: 2px;
    }

    .top-bar {
        display: flex;
        justify-content: space-between;
        align-items: center;
        padding: 20px;
        background-color: #1c1c1c;
        border-bottom: 1px solid #333;
    }

    .left-container h1 {
        margin: 0;
        font-size: 24px;
        color: #fff;
    }

    .right-container {
        text-align: right;
    }

    .logout-btn {
        background-color: #444;
        color: #fff;
        padding: 8px 16px;
        border: none;
        border-radius: 6px;
        text-decoration: none;
        font-size: 14px;
        transition: background 0.3s ease;
    }

    .logout-btn:hover {
        background-color: #555;
    }

    .file-list-container {
        width: 100%;
        padding: 20px;
        background-color: rgba(28, 28, 28, 0.9);
        box-sizing: border-box;
    }

    table {
        width: 100%;
        border-collapse: collapse;
        margin-top: 20px;
        table-layout: fixed;
    }
    th, td {
        padding: 10px;
        border-bottom: 1px solid #444;
        text-align: left;
        word-wrap: break-word;
    }
    th {
        background-color: #2c2c2c;
    }
    tr:hover {
        background-color: #333;
    }
    a {
        color: #61dafb;
        text-decoration: none;
    }
    a:hover {
        text-decoration: underline;
    }
    .btn {
        background-color: #444;
        color: #fff;
        padding: 5px 10px;
        border: none;
        border-radius: 5px;
        cursor: pointer;
        font-size: 12px;
        margin: 2px;
    }
    .btn:hover {
        background-color: #555;
    }

    .btn.btn-sm{
     padding: 6px 8px;
    font-size: 12px;
    line-height: 1.2;
    }
    .icon {
        font-size: 16px;
        margin-right: 5px;
    }
    .modal {
        display: none;
        position: fixed;
        top: 0; left: 0;
        width: 100%; height: 100%;
        background-color: rgba(0, 0, 0, 0.8);
        justify-content: center;
        align-items: center;
        z-index: 1000;
    }
    .modal-content {
        position: relative;
        max-width: 90vw;
        max-height: 90vh;
        text-align: center;
        overflow: hidden;
    }
    .modal-content img,
    .modal-content video {
        max-width: 90vw;
        max-height: 90vh;
        width: auto;
        height: auto;
        object-fit: contain;
        display: block;
        margin: 0 auto;
    }
    .modal-close {
        position: absolute;
        top: 10px; right: 10px;
        background: none;
        border: none;
        color: #fff;
        font-size: 24px;
        cursor: pointer;
    }

    #videoModal,
    #previewModal,
    #shareModal,
    #playlistPanel,
    #zipModal {
      z-index: 5000 !important;
    }

    #zipModal .modal-content {
      background: #1c1c1c;
      border-radius: 12px;
      padding: 28px 32px;
      min-width: 320px;
      max-width: 460px;
      width: 90%;
      text-align: left;
      position: relative;
    }
    #zipModal h3 { margin: 0 0 16px; color: #cde2ff; font-size: 16px; }
    .zip-track {
      background: #333;
      border-radius: 6px;
      height: 10px;
      overflow: hidden;
      margin-bottom: 10px;
    }
    .zip-bar {
      background: linear-gradient(90deg, #4a90e2, #7bc8ff);
      height: 100%;
      width: 0%;
      transition: width 0.25s ease;
      border-radius: 6px;
    }
    .zip-status { color: #a9c4ff; font-size: 13px; margin-bottom: 4px; }
    .zip-file   { color: #555; font-size: 11px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-bottom: 18px; min-height: 16px; }
    #videoModal .modal-content,
    #previewModal .modal-content,
    #shareModal .modal-content,
    #playlistPanel .modal-content{
      position: relative;
    }

    #audioPlayerBar {
      position: fixed; left:0; right:0; bottom:0; z-index: 2000;
      background: #181818; border-top: 1px solid #282828;
      display: flex; align-items: center; gap: 0;
      height: 88px; padding: 0 16px;
    }
    .ab-left {
      display: flex; align-items: center; gap: 12px;
      width: 240px; min-width: 160px; overflow: hidden; flex-shrink: 0;
    }
    .ab-art {
      width: 50px; height: 50px; border-radius: 50%; flex-shrink: 0;
      background: radial-gradient(circle at 40% 35%, #2a2a4a, #111);
      display: flex; align-items: center; justify-content: center;
      font-size: 22px; box-shadow: 0 2px 10px rgba(0,0,0,.5);
    }
    .ab-art.playing { animation: apSpin 18s linear infinite; }
    .ab-title {
      font-size: 13px; color: #fff; white-space: nowrap;
      overflow: hidden; text-overflow: ellipsis; display: block;
    }
    .ab-center {
      flex: 1; display: flex; flex-direction: column;
      align-items: center; gap: 4px; padding: 0 16px; min-width: 0;
    }
    .ab-controls { display: flex; align-items: center; gap: 10px; }
    .ab-btn {
      background: none; border: none; color: #b3b3b3;
      font-size: 20px; cursor: pointer; padding: 4px 6px;
      border-radius: 50%; line-height: 1; transition: color .15s;
    }
    .ab-btn:hover { color: #fff; }
    .ab-btn.active { color: #1db954; }
    .ab-btn-play {
      width: 38px; height: 38px; background: #fff; color: #000;
      font-size: 16px; display: flex; align-items: center; justify-content: center;
    }
    .ab-btn-play:hover { background: #1db954; color: #000; transform: scale(1.06); }
    .ab-progress-wrap {
      display: flex; align-items: center; gap: 8px; width: 100%;
    }
    .ab-time { font-size: 11px; color: #aaa; min-width: 30px; flex-shrink: 0; }
    .ab-progress-bar {
      flex: 1; height: 4px; background: #3e3e3e; border-radius: 2px;
      cursor: pointer; position: relative;
    }
    .ab-progress-bar:hover { height: 6px; }
    .ab-progress-fill {
      height: 100%; width: 0%; background: #1db954; border-radius: 2px;
      pointer-events: none; transition: width .2s linear;
    }
    .ab-progress-bar:hover .ab-progress-fill { background: #fff; }
    .ab-right {
      display: flex; align-items: center; gap: 10px;
      width: 200px; justify-content: flex-end; flex-shrink: 0;
    }
    .ab-volume { accent-color: #1db954; width: 90px; cursor: pointer; }

    #playlistPanel .modal-content li{
      display:flex; align-items:center; justify-content:space-between;
      padding:6px 4px; border-bottom:1px solid #333; font-size:14px;
    }
    #playlistPanel .track-title{ flex:1; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
    #playlistPanel .track-actions .btn{ margin-left:6px; }

    #playlistPanel .modal-content { max-height: 90vh; overflow: auto } 
    #playlistList { max-height: 70vh; overflow: auto }

    /* Shuffle aktywne = zielone */
    #audioShuffle.active{
      background: #2e7d32;   /* ciemna zieleń */
      color: #fff;
      border-color: #245b28;
    }


    tr.row-playing, #playlistList li.playing {
      background: #1f3d1f !important;
      border-color: #285c28 !important;
      color: #e6ffe6;
      transition: background .2s ease, color .2s ease;
    }

    tr.row-playing:hover { background: #234823 !important; }
    tr.row-playing td:first-child { box-shadow: inset 4px 0 0 0 #32a852; }

    /* Wiersze list klikane jak kafelki – podpowiedzka „tapnij mnie” */
    tbody tr{ cursor: pointer; }
    /* Niech klik w przyciski nadal działa niezależnie */
    td .btn, td a, td button{ cursor: pointer; }



   
        .sticky-search{
          position: sticky;
          top: 0;
          z-index: 900;
          background: #1a1a1a;
          border-bottom: 1px solid #333;
        }

        /* ——— panel live-wyników ——— */
        #liveResults{
          display: none;              /* ukryty gdy nic nie wpisane */
          opacity: 0;                 
          transform: translateY(-4px);
          transition: opacity .25s ease, transform .25s ease;
          background: rgba(30, 60, 100, 0.25);
          border: 1px solid rgba(80, 130, 200, 0.35);
          border-radius: 10px;
          padding: 6px 10px 8px 10px;
          margin-top: 8px;
          backdrop-filter: blur(6px);
          box-shadow: 0 0 10px rgba(0, 0, 30, 0.3);
          color: #cde2ff;
          scroll-margin-top: 80px;    /* gdy przewijamy do wyników */
        }
        #liveResults table{
          width: 100%;
          border-collapse: collapse;
        }
        #liveResults th {
          padding: 6px 8px;
          border-bottom: 1px solid #2a2a2a;
          text-align: left;
          font-weight: normal;
        }
        #liveResults .actions .btn{
          margin-left: 6px;
        }

        /* Gdy pokazujemy panel – płynne pojawienie i lekkie wsunięcie */
        #liveResults.is-visible{
          display: block;
          opacity: 1;
          transform: translateY(0);
          animation: fadeSlideIn .25s ease;
        }

        @keyframes fadeSlideIn{
          from{ opacity:0; transform: translateY(-6px); }
          to  { opacity:1; transform: translateY(0); }
        }

        #liveResults tr{ border-bottom: 1px solid rgba(120,160,220,.25); }
        #liveResults tr:hover{ background: rgba(80,140,220,.15); transition: background .25s ease; }
        #liveResults td{ padding: 6px 8px; color:#e0eaff; font-size:14px; }
        #liveResults a, #liveResults .btn{
          color:#bcd8ff; background: rgba(50,90,150,.4);
          border:1px solid rgba(90,130,200,.3); border-radius:6px; padding:4px 8px; font-size:13px;
          text-decoration:none; transition: background .25s ease, color .25s ease, border-color .25s ease;
        }
        #liveResults .btn:hover{ background: rgba(100,160,250,.4); color:#fff; border-color: rgba(150,200,255,.4); }
        #liveResults .info{ color:#a9c4ff; text-align:center; padding:12px 6px; }



        #adminLoginBtn {
          position: relative;
          margin-left: auto;
          margin-right: 8px;
        }
   

    .footer {
        text-align: center;
        padding: 10px;
        font-size: 13px;
        color: #888;
        background-color: #1c1c1c;
        border-top: 1px solid #333;
        margin-top: 40px;
    }

    .breadcrumbs{
      padding: 8px 0 4px 0;
      font-size: 14px;
      color: #aaa;
      white-space: nowrap;
      overflow-x: auto;
      -webkit-overflow-scrolling: touch;
    }
    .breadcrumbs a{
      color: #61dafb;
      text-decoration: none;
    }
    .breadcrumbs a:hover{
      text-decoration: underline;
    }
    .breadcrumbs .sep{
      color: #666;
      margin: 0 6px;
    }


        /* Pasek narzędziowy galerii/wideo/audio */
    .gallery-toolbar {
        display: flex;
        justify-content: space-between;
        align-items: center;
        flex-wrap: wrap;
        gap: 8px;
        padding: 10px 14px;
        margin: 8px 0 12px;
        background: #202020;
        border: 1px solid #333;
        border-radius: 10px;
    }

    .gallery-toolbar-info {
        color: #aaa;
        font-size: 13px;
        flex-shrink: 0;
    }

    .gallery-toolbar-buttons {
        display: flex;
        gap: 6px;
        flex-wrap: wrap;
        justify-content: flex-end;
    }

        /* Gallery File View – fullscreen grid + lightbox */
    #galleryGrid {
        background: #000;
        overflow: hidden;
        font-size: 0;
        line-height: 0;
        margin: 0 -20px;
    }

    .gallery-tile {
        display: inline-block;
        width: 20%;
        padding-bottom: 20%;
        position: relative;
        cursor: pointer;
        background-size: cover;
        background-position: center center;
        background-color: #111;
        transition: filter .1s ease-in;
        vertical-align: top;
    }

    .gallery-tile:hover {
        filter: brightness(.6);
        transition: filter .1s ease-out;
    }

    /* Lightbox overlay */
    #galleryLightbox {
        display: none;
        position: fixed;
        inset: 0;
        z-index: 5000;
        overflow: hidden;
    }

    #galleryLbBg {
        position: absolute;
        left: 0; top: 0;
        width: 100%; height: 100%;
        background: #000;
        opacity: .96;
    }

    #galleryLbImgWrap {
        position: absolute;
        inset: 0 0 72px 0;
        display: flex;
        align-items: center;
        justify-content: center;
        opacity: 0;
        transition: opacity .3s ease;
        z-index: 1;
    }

    #galleryLbImg {
        max-width: 100%;
        max-height: 100%;
        object-fit: contain;
        display: block;
        user-select: none;
        pointer-events: none;
    }

    /* Side hotspots */
    .gallery-lb-hs {
        position: absolute;
        top: 0;
        bottom: 72px;
        width: 25%;
        z-index: 2;
        cursor: pointer;
    }

    #galleryLbHsPrev { left: 0; }
    #galleryLbHsNext { right: 0; }

    .gallery-lb-hs-btn {
        position: absolute;
        top: 50%;
        transform: translateY(-50%);
        width: 50px; height: 50px;
        background: rgba(0,0,0,.55);
        border: none;
        border-radius: 4px;
        color: #a0a0a0;
        font-size: 32px;
        cursor: pointer;
        opacity: 0;
        transition: opacity .2s ease-out;
        display: flex;
        align-items: center;
        justify-content: center;
        padding: 0;
        line-height: 1;
        pointer-events: none;
    }

    #galleryLbHsPrev .gallery-lb-hs-btn { left: 0; }
    #galleryLbHsNext .gallery-lb-hs-btn { right: 0; }
    .gallery-lb-hs:hover .gallery-lb-hs-btn { opacity: 1; }

    /* Bottom bar */
    #galleryLbBar {
        position: absolute;
        left: 0; right: 0; bottom: 0;
        height: 72px;
        display: flex;
        align-items: center;
        justify-content: center;
        gap: 12px;
        z-index: 3;
        opacity: 0;
        transition: opacity .3s ease;
    }

    #galleryLbBarPrev,
    #galleryLbBarNext {
        background: none;
        border: none;
        color: #a0a0a0;
        font-size: 30px;
        cursor: pointer;
        width: 45px; height: 45px;
        display: flex;
        align-items: center;
        justify-content: center;
        border-radius: 50%;
        transition: color .15s;
        padding: 0;
    }
    #galleryLbBarPrev:hover,
    #galleryLbBarNext:hover { color: #fff; }

    #galleryLbCount {
        font-size: 13px;
        color: #aaa;
        min-width: 60px;
        text-align: center;
    }

    #galleryLbDownload {
        display: inline-flex;
        align-items: center;
        gap: 5px;
        padding: 7px 12px;
        background: rgba(255,255,255,.1);
        border: 1px solid rgba(255,255,255,.2);
        border-radius: 7px;
        color: #ddd;
        text-decoration: none;
        font-size: 13px;
        transition: background .15s;
    }
    #galleryLbDownload:hover { background: rgba(255,255,255,.2); color: #fff; }

    /* Close button */
    #galleryLbClose {
        position: absolute;
        top: 0; right: 0;
        width: 45px; height: 45px;
        background: none;
        border: none;
        color: #a0a0a0;
        font-size: 20px;
        cursor: pointer;
        z-index: 4;
        display: flex;
        align-items: center;
        justify-content: center;
        transition: color .15s;
    }
    #galleryLbClose:hover { color: #fff; }

    .gallery-empty {
        padding: 20px;
        background: #202020;
        border: 1px solid #333;
        border-radius: 10px;
        color: #bbb;
    }

    @media (max-width: 768px) {
        .gallery-tile { width: 25%; padding-bottom: 25%; }
    }
    @media (max-width: 568px) {
        .gallery-tile { width: 50%; padding-bottom: 50%; }
    }

        /* Video File View */
    .video-viewer {
        margin: 12px 0 18px;
        padding: 12px;
        background: #141414;
        border: 1px solid #333;
        border-radius: 12px;
    }

    .video-stage {
        position: relative;
        min-height: 420px;
        background: #090909;
        border: 1px solid #2b2b2b;
        border-radius: 12px;
        display: flex;
        align-items: center;
        justify-content: center;
        overflow: hidden;
    }

    .video-stage video {
        width: 100%;
        max-height: 78vh;
        display: block;
        background: #000;
    }

    .video-actions {
        position: absolute;
        top: 12px;
        right: 12px;
        z-index: 8;
        display: flex;
        gap: 8px;
        flex-wrap: wrap;
        justify-content: flex-end;
    }

    .video-action-btn {
        display: inline-flex;
        align-items: center;
        gap: 6px;
        padding: 8px 11px;
        border-radius: 8px;
        background: rgba(0, 0, 0, 0.65);
        border: 1px solid rgba(255, 255, 255, 0.18);
        color: #fff;
        text-decoration: none;
        font-size: 13px;
        line-height: 1;
        cursor: pointer;
    }

    .video-action-btn:hover {
        background: rgba(255, 255, 255, 0.16);
        color: #fff;
    }

    .video-nav {
        position: absolute;
        top: 50%;
        transform: translateY(-50%);
        width: 48px;
        height: 72px;
        border: 0;
        border-radius: 10px;
        background: rgba(0, 0, 0, 0.55);
        color: #fff;
        font-size: 34px;
        cursor: pointer;
        z-index: 7;
    }

    .video-nav:hover {
        background: rgba(255, 255, 255, 0.14);
    }

    .video-prev {
        left: 12px;
    }

    .video-next {
        right: 12px;
    }

    .video-caption {
        margin-top: 10px;
        color: #aaa;
        text-align: center;
        font-size: 14px;
        word-break: break-word;
    }

    .video-items {
        display: grid;
        grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
        gap: 8px;
        margin-top: 12px;
    }

    .video-item {
        border: 2px solid transparent;
        border-radius: 8px;
        background: #222;
        padding: 10px;
        cursor: pointer;
        min-height: 82px;
        color: #ddd;
        text-align: left;
        overflow: hidden;
    }

    .video-item.active {
        border-color: #ffb347;
    }

    .video-item-icon {
        display: block;
        font-size: 26px;
        margin-bottom: 6px;
    }

    .video-item-name {
        display: block;
        font-size: 13px;
        line-height: 1.25;
        word-break: break-word;
    }

    .video-share-status {
        margin-top: 8px;
        color: #90ee90;
        text-align: center;
        font-size: 13px;
        min-height: 18px;
    }

    .video-empty {
        padding: 20px;
        background: #202020;
        border: 1px solid #333;
        border-radius: 10px;
        color: #bbb;
    }


    /* ------------------------------------- */
    /* AUDIO PLAYER VIEW                     */
    /* ------------------------------------- */

    .ap-viewer {
        display: flex;
        gap: 0;
        background: #121212;
        border: 1px solid #2a2a2a;
        border-radius: 14px;
        overflow: hidden;
        margin: 12px 0 18px;
        height: 85vh;
        min-height: 480px;
        align-items: stretch;
    }

    /* --- sidebar (track list) --- */
    .ap-sidebar {
        width: 280px;
        min-width: 220px;
        flex-shrink: 0;
        background: #181818;
        border-right: 1px solid #2a2a2a;
        overflow-y: auto;
    }

    .ap-sidebar-title {
        padding: 16px 16px 8px;
        font-size: 11px;
        font-weight: 700;
        letter-spacing: .1em;
        color: #aaa;
        text-transform: uppercase;
    }

    .ap-track-row {
        display: flex;
        align-items: center;
    }

    .ap-track-row:hover { background: #282828; }
    .ap-track-row:hover .ap-add-btn { opacity: 1; }

    .ap-track {
        display: flex;
        align-items: center;
        gap: 10px;
        flex: 1;
        padding: 10px 16px;
        background: none;
        border: none;
        color: #b3b3b3;
        font-size: 14px;
        text-align: left;
        cursor: pointer;
        border-radius: 0;
        transition: color .15s;
        min-width: 0;
    }

    .ap-track:hover { color: #fff; }
    .ap-track.active { color: #1db954; }

    .ap-add-btn {
        background: none;
        border: none;
        color: #1db954;
        font-size: 18px;
        font-weight: bold;
        cursor: pointer;
        padding: 6px 12px;
        opacity: 1;
        transition: color .15s;
        flex-shrink: 0;
    }

    .ap-add-btn:hover { color: #fff; }

    .ap-track-num {
        min-width: 22px;
        font-size: 13px;
        color: #777;
        text-align: right;
        flex-shrink: 0;
    }

    .ap-track.active .ap-track-num { display: none; }

    .ap-track-info { flex: 1; overflow: hidden; }

    .ap-track-name {
        display: block;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
        font-size: 14px;
    }

    /* animated bars icon (shown when active) */
    .ap-track-bars {
        display: none;
        align-items: flex-end;
        gap: 2px;
        height: 14px;
        flex-shrink: 0;
    }

    .ap-track.active .ap-track-bars { display: flex; }

    .ap-track-bars span {
        display: block;
        width: 3px;
        background: #1db954;
        border-radius: 2px;
        animation: apBars .9s ease-in-out infinite alternate;
    }

    .ap-track-bars span:nth-child(1) { height: 60%; animation-delay: 0s; }
    .ap-track-bars span:nth-child(2) { height: 100%; animation-delay: .2s; }
    .ap-track-bars span:nth-child(3) { height: 40%; animation-delay: .4s; }

    .ap-track.paused .ap-track-bars span { animation-play-state: paused; }

    @keyframes apBars {
        from { transform: scaleY(.4); }
        to   { transform: scaleY(1); }
    }

    /* --- main player panel --- */
    .ap-main {
        flex: 1;
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        padding: 36px 40px;
        gap: 16px;
        background: linear-gradient(160deg, #1a1a2e 0%, #121212 60%);
        overflow-y: auto;
    }

    .ap-art {
        width: 200px;
        height: 200px;
        border-radius: 50%;
        background: radial-gradient(circle at 40% 35%, #2a2a4a, #0d0d0d);
        display: flex;
        align-items: center;
        justify-content: center;
        font-size: 72px;
        box-shadow: 0 8px 40px rgba(0,0,0,.6);
        transition: box-shadow .4s;
        flex-shrink: 0;
    }

    .ap-art.playing {
        box-shadow: 0 0 0 6px rgba(29,185,84,.25), 0 8px 40px rgba(0,0,0,.7);
        animation: apSpin 18s linear infinite;
    }

    @keyframes apSpin {
        from { transform: rotate(0deg); }
        to   { transform: rotate(360deg); }
    }

    .ap-track-title {
        font-size: 20px;
        font-weight: 700;
        color: #fff;
        text-align: center;
        max-width: 100%;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    .ap-track-sub {
        font-size: 13px;
        color: #aaa;
    }

    .ap-progress-wrap {
        display: flex;
        align-items: center;
        gap: 10px;
        width: 100%;
        max-width: 460px;
    }

    .ap-time { font-size: 12px; color: #aaa; min-width: 34px; }

    .ap-progress-bar {
        flex: 1;
        height: 4px;
        background: #3e3e3e;
        border-radius: 2px;
        cursor: pointer;
        position: relative;
    }

    .ap-progress-bar:hover { height: 6px; }

    .ap-progress-fill {
        height: 100%;
        width: 0%;
        background: #1db954;
        border-radius: 2px;
        pointer-events: none;
        transition: width .2s linear;
    }

    .ap-progress-bar:hover .ap-progress-fill { background: #fff; }

    .ap-controls {
        display: flex;
        align-items: center;
        gap: 20px;
    }

    .ap-btn {
        background: none;
        border: none;
        color: #b3b3b3;
        font-size: 26px;
        cursor: pointer;
        padding: 6px;
        border-radius: 50%;
        line-height: 1;
        transition: color .15s, transform .1s;
    }

    .ap-btn:hover { color: #fff; }

    .ap-btn-play {
        width: 56px;
        height: 56px;
        background: #1db954;
        color: #000;
        font-size: 22px;
        display: flex;
        align-items: center;
        justify-content: center;
    }

    .ap-btn-play:hover { background: #1ed760; color: #000; transform: scale(1.05); }

    .ap-volume-wrap {
        display: flex;
        align-items: center;
        gap: 8px;
        color: #aaa;
        font-size: 14px;
        width: 100%;
        max-width: 240px;
    }

    .ap-volume {
        flex: 1;
        accent-color: #1db954;
        cursor: pointer;
    }

    .ap-empty {
        padding: 20px;
        background: #202020;
        border: 1px solid #333;
        border-radius: 10px;
        color: #bbb;
    }

    /* ------------------------------------- */
    /* RESPONSIVE CSS  */
    /* ------------------------------------- */

    @media (max-width: 768px) {
          /* Ukryj kolumnę typu pliku (rozszerzenie) */
          .table-main tbody tr td:nth-child(3),
          #liveResults table tbody tr td:nth-child(3) {
            display: none;
          }

          /* Ukryj kolumnę ikonki pliku (pierwsza) */
          .table-main tbody tr td:first-child,
          #liveResults table tbody tr td:first-child {
            display: none;
          }
          .breadcrumbs{ font-size: 20px; }

        .top-bar {
            flex-direction: column;
            align-items: flex-start;
            padding: 15px;
        }

        #audioPlayerBar {
            height: auto;
            min-height: 60px;
            padding: 6px 10px;
            flex-wrap: wrap;
            gap: 4px;
        }
        .ab-left {
            width: auto; min-width: 0; flex: 1;
        }
        .ab-art { display: none; }
        .ab-center {
            order: 3; width: 100%; padding: 0; gap: 4px;
        }
        .ab-right {
            width: auto;
        }
        .ab-volume { display: none; }
        .ab-progress-wrap { gap: 5px; }
        .ab-controls { gap: 6px; }

        .left-container {
            margin-bottom: 10px;
        }

        .right-container {
            width: 100%;
            text-align: left;
        }

        .logout-btn {
            width: 100%;
            text-align: center;
            box-sizing: border-box;
        }

        .file-list-container {
            padding: 10px;
        }

        /* Układ tabeli dla urządzeń mobilnych */
        table {
            display: block;
        }
        thead {
            display: none;
        }
        tbody {
            display: block;
            width: 100%;
        }
        tr {
            margin-bottom: 15px;
            border: 1px solid #444;
            border-radius: 8px;
            display: block;
            padding: 10px;
            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
        }
        td {
            display: block;
            justify-content: space-between;
            align-items: center;
            border-bottom: 1px solid #444;
            padding: 8px 0;
            text-align: right;
        }
        td:last-child {
            border-bottom: none;
        }
        td::before {
            content: attr(data-label);
            font-weight: bold;
            text-transform: uppercase;
            color: #888;
            text-align: left;
        }

        /* Dopasowanie modala do urządzeń mobilnych */
        .modal-content {
            max-width: 95vw;
            max-height: 95vh;
        }
        .modal-content img,
        .modal-content video {
            max-width: 95vw;
            max-height: 95vh;
        }

        /* Dopasowanie przycisków w tabeli na małych ekranach */
        td a.btn {
            display: inline-block;
            box-sizing: border-box;
            text-align: center;
            margin: 2px 1%;
        }

        /* w tabeli/flex-kartach – zostaw nazwę pliku i ikonę typu */
          thead { display: none; }    /* już masz, ale zostawiamy */

          /* Przyciskom obcinamy tekst – pokażemy ikony przez ::after */
          /* tylko przyciski w tabelach/listach (nie w top barze, nie globalnie) */
        td .btn, .actions .btn { 
            font-size: 0 !important;
            padding: 8px 10px;
            min-width: 38px;
            text-align: center;
            display: inline-block;
          }

          /* Download → strzałka */
          .btn[data-action="download"]::after{ content: "⬇️"; font-size: 18px; }

          /* Podgląd obrazów */
          .preview-btn::after{ content: "🖼️"; font-size: 18px; }

          /* Video: Odtwórz + Udostępnij */
          .watch-btn::after{ content: "🎬"; font-size: 18px; }
          .share-btn::after{ content: "🔗"; font-size: 18px; }

          /* Audio: odtwórz / dodaj do kolejki (tylko w tabeli/akcjach) */
          td .audio-play-btn::after, .actions .audio-play-btn::after { content: "▶️"; font-size: 18px; }
          td .audio-add-btn::after,  .actions .audio-add-btn::after  { content: "➕"; font-size: 18px; }

            #adminLoginBtn {
            position: absolute;
            top: 8px;
            right: 8px;
            padding: 8px 10px;
            font-size: 0 !important;          /* ukryj tekst */
            min-width: 30px;
            border-radius: 8px;
          }

          #adminLoginBtn::after {
            content: "👤";
            font-size: 18px;
          }

          

          /* Player – same ikonki */
        #audioPrev, #audioNext, #audioShuffle, #audioClear, #togglePlaylist { font-size: 0 !important; min-width: 38px; }

        /* prev/next */
        #audioPrev::after  { content: "⏮️"; font-size: 18px; }
        #audioNext::after  { content: "⏭️"; font-size: 18px; }

        /* pozostałe (już były, ale zostaw komplet) */
        #audioShuffle::after   { content: "🔀"; font-size: 18px; }
        #audioClear::after     { content: "🗑️"; font-size: 18px; }
        #togglePlaylist::after { content: "🎵"; font-size: 18px; }

        /* Download (w wierszach tabel) */
        td .btn[data-action="download"]::after, .actions .btn[data-action="download"]::after { content: "⬇️"; font-size: 18px; }

        /* reszta akcji (w tabelach/listach) */
        td .preview-btn::after, .actions .preview-btn::after { content: "🖼️"; font-size: 18px; }
        td .watch-btn::after,   .actions .watch-btn::after   { content: "🎬"; font-size: 18px; }
        td .share-btn::after,   .actions .share-btn::after   { content: "🔗"; font-size: 18px; }

        /* Przyciski folderów */
        td .btn[data-action="zip-dl"]::after      { content: "📦"; font-size: 18px; }
        td .btn[data-action="zip-build"]::after   { content: "📦"; font-size: 18px; }
        td .btn[data-action="zip-rebuild"]::after { content: "🔄"; font-size: 18px; }
        td .btn[data-action="zip-del"]::after     { content: "🗑️"; font-size: 18px; }
        td .btn[data-action="cfg-edit"]::after    { content: "✏️"; font-size: 18px; }
        td .btn[data-action="cfg-del"]::after     { content: "🗑️"; font-size: 18px; }
        td .btn[data-action="cfg-create"]::after  { content: "🔒"; font-size: 18px; }
        /* sticky nad treścią, ale POD modalami */
        .sticky-search{ position: sticky; top: 0; z-index: 900; background: #1a1a1a; border-bottom: 1px solid #333; }

        /* Modale zawsze nad wszystkim */
        #videoModal, #previewModal, #shareModal, #playlistPanel { z-index: 5000 !important; }

        /* GŁÓWNA tabela (5 kolumn): ukryj kolumnę ROZMIAR = 4. wiersza */
        .table-main tbody tr td:nth-child(4){ display: none; }

        /* LIVE RESULTS (4 kolumny): ukryj ROZMIAR = 3. wiersza, NIE akcje */
        #liveResults table tbody tr td:nth-child(3){ display: none; }

        /* na wszelki wypadek wymuś 16px na mobile */
        #searchInput{
          font-size: 16px !important;
          padding: 12px 14px;
        }

        /* mniejsze odstępy nad wynikami */
        #liveResults{ margin-top: 4px; }

        /* sticky nad treścią, ale pod modalami (modale masz na z-index:5000) */
        .sticky-search{
          position: sticky;
          top: 0;
          z-index: 900;
          padding: 8px 10px;
          background:#1a1a1a;
          border-bottom:1px solid #333;
        }

          #liveResults {
            background: rgba(30, 70, 130, 0.4);
            padding: 8px 10px;
          }
          #liveResults td {
            font-size: 15px;
          }
          #liveResults .btn {
            font-size: 14px;
          }

        /* Audio player mobile */
        .ap-viewer { flex-direction: column; height: auto; min-height: unset; }
        .ap-sidebar { width: 100%; height: 40vh; max-height: 40vh; border-right: none; border-top: 1px solid #2a2a2a; order: 2; }
        .ap-main { padding: 24px 20px; order: 1; flex: 0 0 auto; }
        .ap-art { width: 140px; height: 140px; font-size: 52px; }
        .ap-track-title { font-size: 17px; }
        .ap-progress-wrap, .ap-volume-wrap { max-width: 100%; }

    }
    </style>
    HTML;
    }

}//koniec classy




/**
 * Cache rozmiarów katalogów — zapis w .dircache.json przy bazie.
 * Wykrywanie zmian przez fingerprint mtime bezpośrednich dzieci + TTL 2h.
 */
class DirSizeCache
{
    private string $cacheFile;
    private array  $data  = [];
    private bool   $dirty = false;
    private int    $ttl;

    public function __construct(string $baseDir, int $ttl = 7200)
    {
        $this->cacheFile = $baseDir . DIRECTORY_SEPARATOR . '.dircache.json';
        $this->ttl       = $ttl;
        $this->load();
    }

    private function load(): void
    {
        if (!file_exists($this->cacheFile)) return;
        $raw = @file_get_contents($this->cacheFile);
        if ($raw === false) return;
        $decoded = @json_decode($raw, true);
        if (is_array($decoded)) $this->data = $decoded;
    }

    public function save(): void
    {
        if (!$this->dirty) return;
        @file_put_contents(
            $this->cacheFile,
            json_encode($this->data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT),
            LOCK_EX
        );
        $this->dirty = false;
    }

    /** Zwraca rozmiar z cache lub null gdy brak/nieaktualny. */
    public function get(string $dir): ?int
    {
        $key = md5($dir);
        if (!isset($this->data[$key])) return null;

        $entry = $this->data[$key];

        // sprawdź TTL
        if ((time() - ($entry['at'] ?? 0)) > $this->ttl) return null;

        // sprawdź fingerprint
        if (($entry['fp'] ?? '') !== $this->fingerprint($dir)) return null;

        return (int)$entry['size'];
    }

    public function set(string $dir, int $size): void
    {
        $key = md5($dir);
        $this->data[$key] = [
            'size' => $size,
            'fp'   => $this->fingerprint($dir),
            'at'   => time(),
            'path' => $dir,
        ];
        $this->dirty = true;
    }

    /**
     * Fingerprint oparty na mtime bezpośrednich dzieci katalogu.
     * Gdy plik/folder jest dodany/usunięty lub zmienia się mtime podkatalogu
     * (bo w nim coś dodano) — fingerprint się zmienia.
     */
    private function fingerprint(string $dir): string
    {
        if (!is_dir($dir)) return '';
        $parts = [(string)@filemtime($dir)];
        try {
            $di = new DirectoryIterator($dir);
            foreach ($di as $item) {
                if ($item->isDot()) continue;
                $parts[] = $item->getFilename() . ':' . $item->getMTime() . ':' . ($item->isDir() ? 'd' : $item->getSize());
            }
        } catch (Throwable $e) {}
        sort($parts);
        return md5(implode('|', $parts));
    }
}

class FileBrowser
{
    private string       $baseDir;
    private string       $currentDir;
    private array        $sensitiveExtensions = [
        // skrypty / kod
        'php', 'phtml', 'php3', 'php4', 'php5', 'php7', 'php8', 'phps', 'pht', 'phar',
        'sh', 'bash', 'zsh', 'pl', 'cgi', 'py', 'rb', 'asp', 'aspx', 'jsp',
        // dane / bazy / konfiguracja
        'json', 'sql', 'db', 'sqlite', 'sqlite3', 'mdb', 'accdb',
        'env', 'ini', 'conf', 'config', 'cfg', 'htaccess', 'htpasswd',
        'yml', 'yaml', 'xml', 'toml',
        // sekrety / klucze
        'key', 'pem', 'crt', 'cert', 'cer', 'p12', 'pfx', 'ppk', 'asc', 'gpg',
        // kopie / logi / tymczasowe
        'log', 'bak', 'old', 'orig', 'save', 'swp', 'swo', 'tmp', 'temp', 'lock', 'dist', 'sample',
    ];
    public  $oView;
    private DirSizeCache $dirCache;



    public function __construct(string $baseDir, ?string $relativeDir = null)
    {
        $this->baseDir = realpath($baseDir);
        $requestedDir  = $relativeDir ? realpath($this->baseDir . DIRECTORY_SEPARATOR . $relativeDir) : $this->baseDir;

        if ($requestedDir === false
            || $requestedDir !== $this->baseDir
                && !str_starts_with($requestedDir, $this->baseDir . DIRECTORY_SEPARATOR))
        {
            $requestedDir = $this->baseDir;
        }

        if (class_exists('View'))
        {
            $this->oView = new View();
        }
        else
        {
            return "Brak biblioteki view";
        }

        $this->currentDir = $requestedDir;
        $this->dirCache   = new DirSizeCache($this->baseDir, 86400 * 30);

        // zapis cache przy końcu żądania (nawet przy exit/die)
        $cache = &$this->dirCache;
        register_shutdown_function(static function() use (&$cache) {
            $cache->save();
        });
    }

    /**
     * Czy wpis (plik lub katalog) powinien być ukryty przed listingiem,
     * wyszukiwaniem i pobieraniem. Ukrywa:
     *  - wszystkie ukryte/dotfiles (.htaccess, .env, .git, .DS_Store, ._*, .integrity.json, .media_cache.* itd.)
     *  - kopie zapasowe edytorów (plik.txt~)
     *  - wrażliwe rozszerzenia (php, json, sql, db, env, key, log, bak ...)
     */
    public function isHiddenEntry(string $name): bool
    {
        if ($name === '' || $name[0] === '.') return true;      // dotfiles + macOS ._*
        if (str_ends_with($name, '~')) return true;             // backupy edytorów
        return in_array($this->getExt($name), $this->sensitiveExtensions, true);
    }



    public function getBaseFolder(): string
    {
        return trim(dirname($_SERVER['SCRIPT_NAME']), '/');
    }



    public function getBaseUrl(): string
    {
        $protocol   = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? "https://" : "http://";
        $host       = $_SERVER['HTTP_HOST'];
        $scriptName = dirname($_SERVER['SCRIPT_NAME']);

        return rtrim($protocol . $host . $scriptName, '/') . '/';
    }


    /*
     * @version 1.0
     * @param string $sRelativeDir Ścieżka katalogu względem katalogu bazowego
     * @param string $sFileName Nazwa pliku
     * @param string $sAction Akcja streamingu: stream albo stream_audio
     * @return string
     */
    public function BuildStreamUrl(string $sRelativeDir, string $sFileName, string $sAction): string
    {
        $aAllowedActions = ['stream', 'stream_audio'];

        if (!in_array($sAction, $aAllowedActions, true)) {
            $sAction = 'stream';
        }

        $sProtocol = $this->getProtocol();
        $sHost     = $_SERVER['HTTP_HOST'];
        $sScript   = $_SERVER['PHP_SELF'];

        $sDirB64  = rawurlencode(base64_encode($sRelativeDir));
        $sFileB64 = rawurlencode(base64_encode($sFileName));

        return $sProtocol . '://' . $sHost . $sScript
            . '?action=' . rawurlencode($sAction)
            . '&dir=' . $sDirB64
            . '&file=' . $sFileB64;
    }

    /*
     * @version 1.0
     * @param string $sRelativeDir Ścieżka katalogu względem baseDir
     * @param string $sFileName    Nazwa pliku
     * @return string URL do miniatury (action=thumb)
     */
    public function BuildThumbUrl(string $sRelativeDir, string $sFileName): string
    {
        $sProtocol = $this->getProtocol();
        $sHost     = $_SERVER['HTTP_HOST'];
        $sScript   = $_SERVER['PHP_SELF'];

        $sDirB64  = rawurlencode(base64_encode($sRelativeDir));
        $sFileB64 = rawurlencode(base64_encode($sFileName));

        return "{$sProtocol}://{$sHost}{$sScript}?action=thumb&dir={$sDirB64}&file={$sFileB64}";
    }

    /*
     * @version 1.0
     * @param mixed $mDirParam Parametr dir z requestu
     * @param mixed $mFileParam Parametr file z requestu
     * @return string|null
     */
    public function ResolveStreamPathFromRequest($mDirParam, $mFileParam): ?string
    {
        if (is_array($mDirParam) || is_array($mFileParam)) {
            return null;
        }

        $sDirDecoded = base64_decode(rawurldecode((string)$mDirParam), true);
        $sFileDecoded = base64_decode(rawurldecode((string)$mFileParam), true);

        if ($sDirDecoded === false || $sFileDecoded === false) {
            return null;
        }

        return rtrim($this->getBaseDir(), DIRECTORY_SEPARATOR)
            . DIRECTORY_SEPARATOR
            . ltrim($sDirDecoded, DIRECTORY_SEPARATOR)
            . DIRECTORY_SEPARATOR
            . $sFileDecoded;
    }

    /*
     * @version 1.0
     * @param mixed $mValue Wartość do bezpiecznego wyświetlenia w HTML
     * @return string
     */
    public function EscapeHtml($mValue): string
    {
        return htmlspecialchars((string)$mValue, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
    }

    /*
     * @version 1.0
     * @param int $iStatusCode Kod HTTP
     * @param string $sMessage Komunikat tekstowy
     * @return void
     */
    private function DisableOutputCompression(): void
    {
        while (ob_get_level() > 0) { @ob_end_clean(); }
        if (function_exists('apache_setenv')) { @apache_setenv('no-gzip', '1'); }
        @ini_set('zlib.output_compression', '0');
        @header_remove('Content-Type');
        @header_remove('Content-Encoding');
    }

    public function SendPlainHttpError(int $iStatusCode, string $sMessage): void
    {
        $this->DisableOutputCompression();
        http_response_code($iStatusCode);
        header('Content-Type: text/plain; charset=utf-8');
        echo $sMessage;
        exit;
    }

    /*
     * @version 1.0
     * @param string $sKey Nazwa parametru GET
     * @param string $sDefault Wartość domyślna
     * @return string
     */
        public function GetRequestString(string $sKey, string $sDefault = ''): string
    {
        if (!isset($_GET[$sKey]) || is_array($_GET[$sKey])) {
            return $sDefault;
        }

        return (string)$_GET[$sKey];
    }

    /*
     * @version 1.0
     * @param array $aData Dane odpowiedzi JSON
     * @param int $iStatusCode Kod HTTP
     * @return void
     */
    public function SendJsonResponse(array $aData, int $iStatusCode = 200): void
    {
        $this->DisableOutputCompression();
        http_response_code($iStatusCode);
        header('Content-Type: application/json; charset=utf-8');

        echo json_encode(
            $aData,
            JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
        );

        exit;
    }

    /*
     * @version 1.0
     * @param string $sName Nazwa pliku
     * @param string $sUrl URL do pliku / podglądu
     * @param string $sWatchUrl URL do streamingu wideo
     * @param string $sAudioUrl URL do streamingu audio
     * @param string $sDownloadHref URL pobierania pliku
     * @param bool $bIsIndexHtml Czy plik jest index.html
     * @param bool $bAllowAviWatch Czy pokazywać przycisk odtwarzania dla AVI
     * @return string
     */
    public function RenderFileActions(
        string $sName,
        string $sUrl,
        string $sWatchUrl,
        string $sAudioUrl,
        string $sDownloadHref,
        bool $bIsIndexHtml = false,
        bool $bAllowAviWatch = true
    ): string {
        $sExt = $this->getExt($sName);
        $sHtml = '';

        if ($this->isPreviewable($sName)) {
        $sHtml .= '<button class="btn preview-btn"'
            . ' data-file="' . $this->EscapeHtml($sUrl) . '"'
            . ' data-type="' . $this->EscapeHtml($sExt) . '">'
            . 'Podgląd'
            . '</button>' . PHP_EOL;
    }

        if ($this->isVideo($sName) && ($bAllowAviWatch || $sExt !== 'avi')) {
        $sHtml .= '<button class="btn watch-btn"'
            . ' data-file="' . $this->EscapeHtml($sWatchUrl) . '">'
            . 'Odtwórz'
            . '</button>' . PHP_EOL;

        $sHtml .= '<button class="btn share-btn"'
            . ' data-file-url="' . $this->EscapeHtml($sWatchUrl) . '">'
            . '🔗 Udostępnij'
            . '</button>' . PHP_EOL;
    }

    if ($this->isAudio($sName)) {
        $sHtml .= '<button class="btn audio-play-btn"'
            . ' data-audio-url="' . $this->EscapeHtml($sAudioUrl) . '"'
            . ' data-audio-title="' . $this->EscapeHtml($sName) . '">'
            . '▶️ Odtwórz'
            . '</button>' . PHP_EOL;

        $sHtml .= '<button class="btn audio-add-btn"'
            . ' data-audio-url="' . $this->EscapeHtml($sAudioUrl) . '"'
            . ' data-audio-title="' . $this->EscapeHtml($sName) . '">'
            . '➕ Do kolejki'
            . '</button>' . PHP_EOL;
    }

    if (!$bIsIndexHtml) {
        $sHtml .= '<a class="btn" data-action="download"'
            . ' href="' . $this->EscapeHtml($sDownloadHref) . '">'
            . 'Download'
            . '</a>' . PHP_EOL;
    }

    return $sHtml;
}




    /**
     * Szuka plików (i opcjonalnie folderów) zgodnych z zapytaniem.
     *
     * - Domyślnie zwraca tablicę wyników.
     * - Jeśli w URL jest ?api=1 → zamiast tego zwraca JSON i kończy skrypt.
     *
     * @param string      $query      - zapytanie (szuka części nazwy, case-insensitive).
     * @param string|null $startDir   - katalog startowy (pełna ścieżka). Jeśli null użyje $this->currentDir.
     * @param bool        $recursive  - czy przeszukiwać podkatalogi.
     * @param int         $maxResults - maksymalna liczba wyników (domyślnie 500).
     * @return array                  - lista wyników: każdy element to tablica z keys: name, path, is_dir, size, mtime, url, icon
     */
    public function searchFiles(string $query, ?string $startDir = null, bool $recursive = true, int $maxResults = 500): array
    {
        $query = trim($query);
        if ($query === '') return [];

        // --- ext: parser (wiele rozszerzeń + aliasy 'audio','video') ---
        $extFilters = []; // pusta => brak filtru
        if (preg_match('/\bext:([a-z0-9,|]+)\b/i', $query, $m)) {
            $raw = strtolower($m[1]);
            $parts = preg_split('/[|,]/', $raw, -1, PREG_SPLIT_NO_EMPTY);

            $alias = [
                'audio' => ['mp3','m4a','aac','ogg','oga','opus','wav','flac'],
                'video' => ['mp4','webm','ogv','mov','avi','mkv'],
            ];
            foreach ($parts as $p) {
                if (isset($alias[$p]))  { $extFilters = array_merge($extFilters, $alias[$p]); }
                else                    { $extFilters[] = $p; }
            }
            $extFilters = array_values(array_unique($extFilters));
            $query = trim(str_ireplace($m[0], '', $query)); // usuń "ext:..." z zapytania
        }

        $results = [];
        $dir     = $startDir ? realpath($startDir) : realpath($this->currentDir);

        if ($dir === false) {
            $dir = $this->baseDir;
        }
        // bezpieczeństwo: upewnij się, że przeszukiwanie nie wychodzi poza baseDir
        if ($dir !== $this->baseDir
            && !str_starts_with($dir, $this->baseDir . DIRECTORY_SEPARATOR)) {
            $dir = $this->baseDir;
        }

        try {
            if ($recursive) {
                $it = new RecursiveIteratorIterator(
                    new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS),
                    RecursiveIteratorIterator::SELF_FIRST
                );
                foreach ($it as $fileInfo) {
                    if ($fileInfo->isDir()) continue;

                    $name      = $fileInfo->getFilename();
                    $extension = strtolower($fileInfo->getExtension());
                    if ($this->isHiddenEntry($name)) continue;
                    if (!empty($extFilters) && !in_array($extension, $extFilters, true)) continue;
                    if (stripos($name, $query) === false && $query !== '') continue;

                    $fullPath = $fileInfo->getPathname();
                    $fileDirAbs = dirname($fullPath);
                    $relDir = $this->relativeToBase($fileDirAbs) ?: '/';

                    $watchUrl = $this->BuildStreamUrl($relDir, $name, 'stream');
                    $audioUrl = $this->BuildStreamUrl($relDir, $name, 'stream_audio');

                    $results[] = [
                        'name'      => $name,
                        'path'      => $fullPath,
                        'rel_dir'   => $relDir,              // <-- DODANE
                        'is_dir'    => false,
                        'size'      => $fileInfo->getSize(),
                        'mtime'     => $fileInfo->getMTime(),
                        'url'       => $this->getFileUrl($fullPath), // zostawiamy, ale player go nie używa
                        'icon'      => $this->getFileTypeIcon($name),
                        'audio_url' => $this->isAudio($name) ? $audioUrl : null,   // <-- DODANE
                        'video_url' => $this->isVideo($name) ? $watchUrl : null,   // <-- DODANE
                    ];

                    if (count($results) >= $maxResults) break;
                }
            } else {
    $di = new DirectoryIterator($dir);
    foreach ($di as $fileInfo) {
        if ($fileInfo->isDot() || $fileInfo->isDir()) continue;

        $name      = $fileInfo->getFilename();
        $extension = strtolower($fileInfo->getExtension());
        if ($this->isHiddenEntry($name)) continue;
        if (!empty($extFilters) && !in_array($extension, $extFilters, true)) continue;
        if (stripos($name, $query) === false && $query !== '') continue;

        $fullPath = $fileInfo->getPathname();
        $fileDirAbs = dirname($fullPath);
        $relDir = $this->relativeToBase($fileDirAbs) ?: '/';

        $watchUrl = $this->BuildStreamUrl($relDir, $name, 'stream');
        $audioUrl = $this->BuildStreamUrl($relDir, $name, 'stream_audio');

        $results[] = [
            'name'      => $name,
            'path'      => $fullPath,
            'rel_dir'   => $relDir,
            'is_dir'    => false,
            'size'      => $fileInfo->getSize(),
            'mtime'     => $fileInfo->getMTime(),
            'url'       => $this->getFileUrl($fullPath),
            'icon'      => $this->getFileTypeIcon($name),
            'audio_url' => $this->isAudio($name) ? $audioUrl : null,
            'video_url' => $this->isVideo($name) ? $watchUrl : null,
        ];

        if (count($results) >= $maxResults) break;
    }
}

        } catch (Exception $e) {
            // w razie błędów - zwróć to co znaleziono do tej pory
        }

        return $results;
    }



    public function getPathUrl(string $path): string
    {
        $relativePath = str_replace($this->baseDir, '', $path);
        $relativePath = trim($relativePath, DIRECTORY_SEPARATOR);
        return $_SERVER['PHP_SELF'] . '?dir=' . rawurlencode($relativePath);
    }



    public function getFileUrl(string $path): string
    {
        $relativePath = str_replace($this->baseDir, '', $path);
        $relativePath = trim($relativePath, DIRECTORY_SEPARATOR);
        return rtrim($this->getBaseUrl(), '/') . '/' . $relativePath;

    }



    public function getFileTypeIcon(string $file): string
    {
        $extension = $this->getExt($file);
        $icons = [
            'jpg' => '🖼️', 'jpeg' => '🖼️', 'png' => '🖼️', 'gif' => '🖼️',
            'mp4' => '🎥', 'mov'  => '🎥', 'avi' => '🎥',
            'mp3' => '🎵', 'wav'  => '🎵',
            'zip' => '📦', 'rar'  => '📦', '7z'   => '📦',
            'pdf' => '📄', 'doc'  => '📄', 'docx' => '📄',
        ];
        return $icons[$extension] ?? '📁';
    }



    /*
    * @version 1.1
    * @param string $file Nazwa pliku
    * @return bool
    */
    public function isPreviewable(string $file): bool
    {
        $sExtension   = $this->getExt($file);
        $aPreviewable = ['jpg', 'jpeg', 'png', 'gif', 'webp'];

        return in_array($sExtension, $aPreviewable, true);
    }

    /*
    * @version 1.1
    * @param string $file Nazwa pliku
    * @return bool
    */
    public function isVideo($file)
    {
        $sExtension = $this->getExt($file);
        $aVideo     = ['mp4', 'webm', 'ogv', 'mov', 'm4v', 'avi', 'mkv'];

        return in_array($sExtension, $aVideo, true);
    }

    public function getExt(string $file): string
    {
        return strtolower(pathinfo($file, PATHINFO_EXTENSION));
    }

    private function getWritableTmpDir(): string
    {
        foreach ([sys_get_temp_dir(), '/tmp', '/var/tmp'] as $d) {
            if ($d !== '' && is_dir($d) && is_writable($d)) return $d;
        }
        return sys_get_temp_dir();
    }

    /**
     * Czy ścieżka (już po realpath) leży wewnątrz baseDir.
     * Porównanie z separatorem — odrzuca katalogi-rodzeństwo typu /base2.
     */
    private function isWithinBase(string $real): bool
    {
        return $real === $this->baseDir
            || strncmp($real, $this->baseDir . DIRECTORY_SEPARATOR, strlen($this->baseDir) + 1) === 0;
    }

    /**
     * Sprawdza ochronę config.php dla każdego katalogu na ścieżce
     * od baseDir do katalogu pliku. Admin ma zawsze dostęp.
     * Chroni stream/thumb/download przed obejściem logowania do folderu.
     */
    private function isPathAuthorized(string $absFile): bool
    {
        if ($this->isAdmin()) return true;

        $dir = is_dir($absFile) ? $absFile : dirname($absFile);
        while ($this->isWithinBase($dir)) {
            if (file_exists($dir . DIRECTORY_SEPARATOR . 'config.php')) {
                $sessionKey = md5($dir);
                if (empty($_SESSION['auth'][$sessionKey])) return false;
            }
            if ($dir === $this->baseDir) break;
            $dir = dirname($dir);
        }
        return true;
    }

    private function getProtocol(): string
    {
        return (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
    }

    public function getMimeType(string $ext): string
    {
        return match ($ext) {
            'mp4'       => 'video/mp4',
            'webm'      => 'video/webm',
            'ogv'       => 'video/ogg',
            'mov'       => 'video/quicktime',
            'avi'       => 'video/x-msvideo',
            'mkv'       => 'video/x-matroska',
            'mp3'       => 'audio/mpeg',
            'm4a'       => 'audio/mp4',
            'aac'       => 'audio/aac',
            'ogg','oga','opus' => 'audio/ogg',
            'wav'       => 'audio/wav',
            'flac'      => 'audio/flac',
            default     => 'application/octet-stream',
        };
    }

    /**
     * Parsuje nagłówek HTTP Range. Zwraca ['start'=>int,'end'=>int,'code'=>int]
     * lub null gdy zakres nieprawidłowy (wysyła 416 i kończy żądanie).
     */
    private function parseRangeHeader(int $size): array
    {
        $start = 0;
        $end   = $size - 1;
        $code  = 200;

        if (isset($_SERVER['HTTP_RANGE']) && preg_match('/bytes=(\d*)-(\d*)/', $_SERVER['HTTP_RANGE'], $m)) {
            if ($m[1] !== '') $start = (int)$m[1];
            if ($m[2] !== '') $end   = (int)$m[2];
            if ($start > $end || $end >= $size) {
                header('HTTP/1.1 416 Requested Range Not Satisfiable');
                header('Content-Range: bytes 0-' . ($size - 1) . '/' . $size);
                exit;
            }
            $code = 206;
        }

        return ['start' => $start, 'end' => $end, 'code' => $code];
    }

    public function isAudio(string $file): bool
    {
        $audio = ['mp3','m4a','aac','ogg','oga','wav','flac', 'opus'];
        return in_array($this->getExt($file), $audio, true);
    }

    public function isMediaFile(string $file): bool
    {
        static $media = [
            'jpg','jpeg','png','gif','webp','bmp','tiff',
            'mp4','webm','ogv','mov','m4v','avi','mkv',
            'mp3','m4a','aac','ogg','oga','wav','flac','opus',
        ];
        return in_array($this->getExt($file), $media, true);
    }

    private function buildContentDisposition(string $filename, string $disposition = 'inline'): string
    {
        // Spróbuj ujednolicić formę Unicode (wymaga ext-intl; jeśli nie ma, pomiń)
        if (class_exists('Normalizer')) {
            $filename = Normalizer::normalize($filename, Normalizer::FORM_C) ?? $filename;
        }

        // ASCII fallback – spróbuj transliterować, a jak się nie uda, zrób bezpieczny ASCII
        $ascii = @iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $filename);
        if ($ascii === false || $ascii === '' ) {
            $ascii = preg_replace('/[^A-Za-z0-9_. -]/', '_', $filename);
            if ($ascii === '' ) $ascii = 'file';
        }
        // Usuń problematyczne znaki w ASCII
        $ascii = preg_replace('/[\\r\\n"]/', '_', $ascii);

        // RFC 5987 dla wariantu UTF-8 (percent-encoded)
        $utf8Percent = rawurlencode($filename);

        // Zabezpiecz cudzysłowy i backslash w ASCII wariancie
        $asciiQuoted = addcslashes($ascii, "\"\\\\");

        // Złóż pełny nagłówek z oboma atrybutami
        return sprintf(
            '%s; filename="%s"; filename*=UTF-8\'\'%s',
            $disposition,
            $asciiQuoted,
            $utf8Percent
        );
    }


    public function streamVideo($filePath)
{
    $real = realpath($filePath);
    if ($real === false || !is_file($real) || !$this->isWithinBase($real)) {
        $this->SendPlainHttpError(404, 'Not found');
    }
    if (!$this->isVideo($real)) {
        $this->SendPlainHttpError(403, 'Forbidden');
    }
    if (!$this->isPathAuthorized($real)) {
        $this->SendPlainHttpError(401, 'Unauthorized');
    }

    $this->DisableOutputCompression();

    $contentType = $this->getMimeType($this->getExt($real));

    $size   = filesize($real);
    $start  = 0;
    $end    = $size - 1;
    $code   = 200;

    header('X-Content-Type-Options: nosniff');
    header('Content-Type: ' . $contentType);
    header('Content-Disposition: ' . $this->buildContentDisposition(basename($real), 'inline'));
    header('Accept-Ranges: bytes');
    header('Cache-Control: private, max-age=0, no-cache, no-store, must-revalidate');
    header('Pragma: no-cache');
    header('Expires: 0');

    // Obsługa Range
    ['start' => $start, 'end' => $end, 'code' => $code] = $this->parseRangeHeader($size);

    $length = $end - $start + 1;
    if ($code === 206) {
        header('HTTP/1.1 206 Partial Content');
        header("Content-Range: bytes $start-$end/$size");
    }
    header("Content-Length: $length");

    $fp = fopen($real, 'rb');
    if ($fp === false) { http_response_code(500); echo 'Cannot open file'; exit; }
    if ($start > 0) fseek($fp, $start);

    ignore_user_abort(true);
    set_time_limit(0);

    $chunk = 8192;
    $left = $length;
    while ($left > 0 && !feof($fp)) {
        $read = ($left > $chunk) ? $chunk : $left;
        $buf  = fread($fp, $read);
        if ($buf === false) break;
        echo $buf;
        $left -= strlen($buf);
        if (connection_aborted()) break;
        flush();
    }
    fclose($fp);
    exit;
}



    public function streamAudio(string $filePath): void
    {
        $real = realpath($filePath);
        if ($real === false || !is_file($real) || !$this->isWithinBase($real)) {
            $this->SendPlainHttpError(404, 'Not found');
        }
        if (!$this->isAudio($real)) {
            $this->SendPlainHttpError(403, 'Forbidden');
        }
        if (!$this->isPathAuthorized($real)) {
            $this->SendPlainHttpError(401, 'Unauthorized');
        }

        $mime = $this->getMimeType($this->getExt($real));

        $this->DisableOutputCompression();

        $size  = filesize($real);
        $start = 0;
        $end   = $size - 1;
        $code  = 200;

        // Proste cache headers (opcjonalne)
        $mtime = filemtime($real) ?: time();
        $lastMod = gmdate('D, d M Y H:i:s', $mtime) . ' GMT';
        $etag = '"' . md5($real . '|' . $size . '|' . $mtime) . '"';

        header('X-Content-Type-Options: nosniff');
        header('Content-Type: ' . $mime);
        header('Content-Disposition: ' . $this->buildContentDisposition(basename($real), 'inline'));
        header('Accept-Ranges: bytes');
        header('Cache-Control: private, max-age=0, no-cache, no-store, must-revalidate');
        header('Pragma: no-cache');
        header('Expires: 0');
        header('Last-Modified: ' . $lastMod);
        header('ETag: ' . $etag);

        // Warunkowe 304 (opcjonalne)
        if ((isset($_SERVER['HTTP_IF_NONE_MATCH']) && trim($_SERVER['HTTP_IF_NONE_MATCH']) === $etag) ||
            (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && trim($_SERVER['HTTP_IF_MODIFIED_SINCE']) === $lastMod)) {
            http_response_code(304);
            exit;
        }

        // Obsługa Range
        ['start' => $start, 'end' => $end, 'code' => $code] = $this->parseRangeHeader($size);

        $length = $end - $start + 1;
        if ($code === 206) {
            header('HTTP/1.1 206 Partial Content');
            header("Content-Range: bytes $start-$end/$size");
        }
        header("Content-Length: $length");

        // HEAD → tylko nagłówki
        if (isset($_SERVER['REQUEST_METHOD']) && strtoupper($_SERVER['REQUEST_METHOD']) === 'HEAD') {
            exit;
        }

        $fp = fopen($real, 'rb');
        if ($fp === false) {
            http_response_code(500);
            header('Content-Type: text/plain; charset=utf-8');
            echo 'Cannot open file';
            exit;
        }
        if ($start > 0) fseek($fp, $start);

        ignore_user_abort(true);
        set_time_limit(0);

        $chunk = 8192;
        $bytesLeft = $length;
        while ($bytesLeft > 0 && !feof($fp)) {
            $read = ($bytesLeft > $chunk) ? $chunk : $bytesLeft;
            $buf  = fread($fp, $read);
            if ($buf === false) break;
            echo $buf;
            $bytesLeft -= strlen($buf);
            if (connection_aborted()) break;
            flush();
        }
        fclose($fp);
        exit;
    }

    private function resolveZipDir(string $relDir): string|false
    {
        $relDir = ltrim(str_replace(['..', "\0"], '', $relDir), '/\\');
        $abs    = realpath($this->baseDir . ($relDir !== '' ? DIRECTORY_SEPARATOR . $relDir : ''));
        if ($abs === false || !is_dir($abs)
            || ($abs !== $this->baseDir && strncmp($abs, $this->baseDir . DIRECTORY_SEPARATOR, strlen($this->baseDir) + 1) !== 0)) {
            return false;
        }
        return $abs;
    }

    private function collectMediaFiles(string $abs): array
    {
        $maxBytes  = 2 * 1024 * 1024 * 1024;
        $maxFiles  = 5000;
        $collected = [];
        $totalSize = 0;
        $it = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($abs, FilesystemIterator::SKIP_DOTS),
            RecursiveIteratorIterator::LEAVES_ONLY
        );
        foreach ($it as $f) {
            if (!$f->isFile() || !$this->isMediaFile($f->getFilename())) continue;
            if (count($collected) >= $maxFiles) break;
            $sz = $f->getSize();
            if ($totalSize + $sz > $maxBytes) break;
            $totalSize += $sz;
            $collected[] = $f->getRealPath();
        }
        return $collected;
    }

    // --- ZIP cache helpers ---

    private function mediaCachePaths(string $abs): array
    {
        return [
            'zip'  => $abs . DIRECTORY_SEPARATOR . '.media_cache.zip',
            'meta' => $abs . DIRECTORY_SEPARATOR . '.media_cache.json',
        ];
    }

    public function getMediaCacheInfo(string $abs): ?array
    {
        $p = $this->mediaCachePaths($abs);
        if (!is_file($p['zip'])) return null;
        $meta = is_file($p['meta']) ? (json_decode(file_get_contents($p['meta']), true) ?? []) : [];
        return [
            'size'    => filesize($p['zip']),
            'created' => (int)($meta['created'] ?? filemtime($p['zip'])),
            'count'   => (int)($meta['count'] ?? 0),
        ];
    }

    public function clearMediaCache(string $abs): void
    {
        $p = $this->mediaCachePaths($abs);
        @unlink($p['zip']);
        @unlink($p['meta']);
    }

    // --- ZIP build (SSE, saves to folder permanently) ---

    public function streamZipBuild(string $relDir): void
    {
        @ini_set('display_errors', '0');
        @ini_set('zlib.output_compression', '0');
        @ini_set('output_buffering', 'Off');
        while (ob_get_level() > 0) @ob_end_clean();
        ob_implicit_flush(true);
        session_write_close();

        if (function_exists('apache_setenv')) {
            @apache_setenv('no-gzip', '1');
            @apache_setenv('dont-vary', '1');
        }

        header('Content-Type: text/event-stream; charset=utf-8');
        header('Cache-Control: no-cache, no-store, must-revalidate');
        header('Pragma: no-cache');
        header('X-Accel-Buffering: no');
        header('Connection: keep-alive');

        set_time_limit(0);
        ignore_user_abort(true);

        $sseSend = function(string $event, array $data): void {
            echo ($event !== 'message' ? "event: $event\n" : '') . 'data: ' . json_encode($data) . "\n\n";
            flush();
        };

        $abs = $this->resolveZipDir($relDir);
        if ($abs === false) { $sseSend('ziperr', ['msg' => 'Forbidden']); exit; }
        if (!class_exists('ZipArchive')) { $sseSend('ziperr', ['msg' => 'ZipArchive unavailable']); exit; }
        if (!is_writable($abs)) { $sseSend('ziperr', ['msg' => 'Katalog nie jest zapisywalny przez serwer']); exit; }

        $collected = $this->collectMediaFiles($abs);
        $total     = count($collected);
        if ($total === 0) { $sseSend('ziperr', ['msg' => 'Brak plików media w tym katalogu']); exit; }

        $sseSend('message', ['total' => $total, 'done' => 0, 'file' => '']);

        $tmpDir = $this->getWritableTmpDir();
        $tmp    = @tempnam($tmpDir, 'mzip_');
        if (empty($tmp)) {
            $sseSend('ziperr', ['msg' => "Brak dostępu do katalogu tymczasowego: $tmpDir"]);
            exit;
        }

        $zip = new ZipArchive();
        if ($zip->open($tmp, ZipArchive::OVERWRITE) !== true) {
            @unlink($tmp);
            $sseSend('ziperr', ['msg' => "ZipArchive nie może otworzyć pliku: $tmp"]);
            exit;
        }

        foreach ($collected as $i => $path) {
            $entry = substr($path, strlen($abs) + 1);
            $zip->addFile($path, $entry);
            $zip->setCompressionName($entry, ZipArchive::CM_STORE);
            $sseSend('message', ['total' => $total, 'done' => $i + 1, 'file' => basename($path)]);
        }
        $zip->close();

        $paths = $this->mediaCachePaths($abs);
        rename($tmp, $paths['zip']);
        @chmod($paths['zip'], 0644);
        file_put_contents($paths['meta'], json_encode([
            'created' => time(),
            'count'   => $total,
            'size'    => filesize($paths['zip']),
        ]));

        $sseSend('done', ['count' => $total, 'size' => filesize($paths['zip'])]);
        exit;
    }

    // --- ZIP cached download (all users) ---

    public function streamZipCached(string $relDir): void
    {
        $abs = $this->resolveZipDir($relDir);
        if ($abs === false) $this->SendPlainHttpError(403, 'Forbidden');
        if (!$this->isPathAuthorized($abs)) $this->SendPlainHttpError(401, 'Unauthorized');

        $p = $this->mediaCachePaths($abs);
        if (!is_file($p['zip'])) $this->SendPlainHttpError(404, 'No cached archive');

        $this->DisableOutputCompression();
        $zipName = (basename($abs) ?: 'media') . '_media.zip';
        header('Content-Type: application/zip');
        header('Content-Disposition: ' . $this->buildContentDisposition($zipName, 'attachment'));
        header('Content-Length: ' . filesize($p['zip']));
        header('Cache-Control: no-cache, no-store');
        readfile($p['zip']);
        exit;
    }

     public function handleDownload(string $filename): void
     {
          $file = realpath($this->currentDir . DIRECTORY_SEPARATOR . $filename);

          if ($file === false || !is_file($file) || !$this->isWithinBase($file)) {
              http_response_code(404);
              echo "Error: File does not exist or access is denied.";
              exit;
          }

          if (!$this->isPathAuthorized($file)) {
              $this->SendPlainHttpError(401, 'Unauthorized');
          }

          // Blokada pobierania wrażliwych/ukrytych plików (php, json, sql, db, dotfiles, backupy ...)
          if ($this->isHiddenEntry(basename($file))) {
              http_response_code(403);
              echo "Error: Downloading this file type is not allowed.";
              exit;
          }

        $this->DisableOutputCompression();

        $name = basename($file);
        $size = (string)filesize($file);

        header('Content-Description: File Transfer');
        header('Content-Type: application/octet-stream');               // zawsze binarnie
        header('X-Content-Type-Options: nosniff');                      // nie próbuj zgadywać
        header('Content-Disposition: ' . $this->buildContentDisposition($name, 'attachment'));
        header('Content-Transfer-Encoding: binary');
        header('Content-Length: '.$size);
        header('Cache-Control: no-store, no-cache, must-revalidate');
        header('Pragma: no-cache');
        header('Expires: 0');

        // bez częściowych zakresów – pełny plik do pobrania
        $fp = fopen($file, 'rb');
        if ($fp === false) {
            http_response_code(500);
            echo "Error: Could not open file.";
            exit;
        }
        set_time_limit(0);
        $chunk = 8192;
        while (!feof($fp)) {
            echo fread($fp, $chunk);
            flush();
        }
        fclose($fp);
        exit;
    }

    /*
     * Generuje miniaturę JPEG i serwuje ją z cache'em na dysku.
     * Cache przechowywany w .thumbcache/ w baseDir (klucz = md5 ścieżki + mtime + rozmiar).
     * Obsługuje EXIF orientation dla JPEG i białe tło dla PNG/GIF z przezroczystością.
     *
     * @param string $filePath Pełna ścieżka do pliku źródłowego
     * @param int    $maxW     Maks. szerokość miniatury (px)
     * @param int    $maxH     Maks. wysokość miniatury (px)
     */
    public function serveThumb(string $filePath, int $maxW = 220, int $maxH = 220): void
    {
        $real = realpath($filePath);
        if ($real === false || !is_file($real) || !$this->isWithinBase($real)) {
            $this->SendPlainHttpError(404, 'Not found');
        }
        if (!$this->isPathAuthorized($real)) {
            $this->SendPlainHttpError(401, 'Unauthorized');
        }

        $ext = $this->getExt($real);
        if (!in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'webp'], true)) {
            $this->SendPlainHttpError(415, 'Unsupported image type');
        }

        // Jeśli GD niedostępne — przekieruj do oryginału
        if (!function_exists('imagecreatefromjpeg')) {
            header('Location: ' . $this->getFileUrl($real));
            exit;
        }

        $cacheDir  = $this->baseDir . DIRECTORY_SEPARATOR . '.thumbcache';
        $mtime     = (int)filemtime($real);
        $cacheKey  = md5("{$real}|{$mtime}|{$maxW}x{$maxH}");
        $cachePath = $cacheDir . DIRECTORY_SEPARATOR . $cacheKey . '.jpg';

        if (!is_dir($cacheDir)) {
            @mkdir($cacheDir, 0755, true);
            @file_put_contents($cacheDir . DIRECTORY_SEPARATOR . '.htaccess', "Deny from all\n");
        }

        if (!file_exists($cachePath)) {
            $src = match ($ext) {
                'jpg', 'jpeg' => @imagecreatefromjpeg($real),
                'png'         => @imagecreatefrompng($real),
                'gif'         => @imagecreatefromgif($real),
                'webp'        => function_exists('imagecreatefromwebp') ? @imagecreatefromwebp($real) : false,
                default       => false,
            };

            if ($src === false) {
                $this->SendPlainHttpError(500, 'Cannot decode image');
            }

            // Korekcja orientacji EXIF (zdjęcia z telefonów)
            if (in_array($ext, ['jpg', 'jpeg'], true) && function_exists('exif_read_data')) {
                $exif = @exif_read_data($real);
                $src  = match ((int)($exif['Orientation'] ?? 1)) {
                    3 => imagerotate($src, 180, 0),
                    6 => imagerotate($src, -90,  0),
                    8 => imagerotate($src, 90,   0),
                    default => $src,
                };
            }

            $w     = imagesx($src);
            $h     = imagesy($src);
            $ratio = min($maxW / $w, $maxH / $h, 1.0);
            $nw    = max(1, (int)round($w * $ratio));
            $nh    = max(1, (int)round($h * $ratio));

            $dst   = imagecreatetruecolor($nw, $nh);
            // białe tło (obsługa przezroczystości PNG/GIF przy zapisie do JPEG)
            $white = imagecolorallocate($dst, 255, 255, 255);
            imagefilledrectangle($dst, 0, 0, $nw, $nh, $white);
            imagecopyresampled($dst, $src, 0, 0, 0, 0, $nw, $nh, $w, $h);
            unset($src);

            if (!imagejpeg($dst, $cachePath, 82)) {
                unset($dst);
                $this->SendPlainHttpError(500, 'Cannot write thumbnail cache');
            }
            unset($dst);
        }

        // Serwowanie z cache
        $this->DisableOutputCompression();

        $etag = "\"thumb-{$cacheKey}\"";

        header('Content-Type: image/jpeg');
        header('Content-Length: ' . (int)filesize($cachePath));
        header('Cache-Control: public, max-age=604800, immutable');
        header("ETag: {$etag}");
        header('X-Content-Type-Options: nosniff');

        if (isset($_SERVER['HTTP_IF_NONE_MATCH'])
            && trim($_SERVER['HTTP_IF_NONE_MATCH']) === $etag) {
            http_response_code(304);
            exit;
        }

        http_response_code(200);
        readfile($cachePath);
        exit;
    }

    public function getBaseDir(): string
    {
        return $this->baseDir;
    }


    public function getCurrentDir(): string
    {
        return $this->currentDir;
    }



public function checkAccess(bool $renderLoginOnDeny = true): bool
{
    $configPath = $this->currentDir . DIRECTORY_SEPARATOR . 'config.php';

    // Admin ma pełny dostęp
    if ($this->isAdmin()) {
        return true;
    }

    if (!file_exists($configPath)) {
        return true; // brak ochrony
    }

    $authConfig = include $configPath;
    $sessionKey = md5($this->currentDir);

    if (isset($_GET['logout'])) {
        unset($_SESSION['auth'][$sessionKey]);
        header("Location: " . strtok($_SERVER["REQUEST_URI"], '?'));
        exit;
    }

    if (!isset($_SESSION['auth'][$sessionKey])) {
        if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['username'], $_POST['password'])) {
            if ($_POST['username'] === ($authConfig['username'] ?? null) &&
                password_verify($_POST['password'], $authConfig['password'] ?? '')) {
                session_regenerate_id(true);
                $_SESSION['auth'][$sessionKey] = true;
                header('Location: ' . $_SERVER['REQUEST_URI']);
                exit;
            }
        }

        if ($renderLoginOnDeny) {
            $loginError = ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['username'], $_POST['password']))
                ? "Niepoprawny login lub hasło."
                : "";
            echo $this->oView::headLogin('Zaloguj się do folderu');
            echo $this->oView::loginForm($_SERVER['REQUEST_URI'], $loginError, "🔐 Dostęp chroniony");
            echo $this->oView::footer();
            exit;
        } else {
            // tylko sygnalizuj brak dostępu
            return false;
        }
    }

    return true;
}



/* ----------  ADMIN HANDLING  ---------- */

public function isAdmin(): bool
{
    return !empty($_SESSION['is_admin']);
}

public function handleAdminLogin(): void
{
    // 1) Wylogowanie admina
    if (isset($_GET['logoutAdmin']))
    {
        unset($_SESSION['is_admin']);
        header('Location: ' . strtok($_SERVER['REQUEST_URI'], '?'));
        exit;
    }

    // 2) Jeśli ?admin – pokaż formularz (o ile jeszcze nie zalogowany)
    if (isset($_GET['admin']) && !$this->isAdmin())
    {
        $error      = null;
        $loginError = "";

        if ($_SERVER['REQUEST_METHOD'] === 'POST')
        {
            $u = (string)($_POST['username'] ?? '');
            $p = (string)($_POST['password'] ?? '');
            if (hash_equals(ADMIN_USER, $u) && hash_equals(ADMIN_PASS, $p))
            {
                session_regenerate_id(true);
                $_SESSION['is_admin'] = true;
                header('Location: ' . strtok($_SERVER['REQUEST_URI'], '?'));   // usuń ?admin
                exit;
            }
            $loginError = 'Błędny login lub hasło.';
        }


    echo $this->oView::headLogin('Zaloguj się');
    echo $this->oView::loginForm($_SERVER['REQUEST_URI'], $loginError, "Panel administratora");
    echo $this->oView::footer();
    exit;
    }
}



public function handleCreateConfig(): void
{
    $loginError = "";

    if (!$this->isAdmin())
    {
        echo "Brak uprawnień.";
        exit;
    }

    $configFile = $this->currentDir . DIRECTORY_SEPARATOR . 'config.php';

     // Jeśli plik istnieje, pokaż obecne dane logowania
    if (file_exists($configFile))
    {
        $config = include $configFile;
        if (is_array($config) && isset($config['username'], $config['password']))
        {
            $loginError .= "<br><br>🔑 Obecne dane logowania:<br><br>";
            $loginError .= "Login: <b>" . $this->EscapeHtml($config['username']) . "</b><br>";
            $loginError .= "Hasło: <b>" . $this->EscapeHtml($config['password']) . "</b>";
        }
        else
        {
            $loginError .= "⚠️ Plik config.php istnieje, ale ma niepoprawny format.<br><br>";
        }
    }

    // Jeśli formularz został przesłany
    if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['username'], $_POST['password']))
    {
        if (!hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'] ?? '')) {
            http_response_code(403);
            exit('Forbidden (CSRF)');
        }

        $username = trim($_POST['username']);
        $password = trim($_POST['password']);

        if ($username === '' || $password === '')
        {
            $loginError = "Login i hasło nie mogą być puste.";
            exit;
        }

        $hash    = password_hash($password, PASSWORD_BCRYPT);
        $content = "<?php\nreturn [\n    'username' => " . var_export($username, true) . ",\n    'password' => " . var_export($hash, true) . ",\n];\n";

      if (!is_writable($this->currentDir))
      {
            $loginError = "❌ Błąd: katalog " . $this->EscapeHtml($this->currentDir) . " nie ma uprawnień do zapisu\n";
            $loginError .= "Sprawdź prawa chmod (np. chmod 755 lub chmod 775)";
      }
      else if (@file_put_contents($configFile, $content) === false)
      {
            $loginError = "❌ Wystąpił błąd podczas zapisu pliku config.php w katalogu:";
            $loginError .= " " . $this->EscapeHtml($this->currentDir) . "";
            $loginError .= "🔍 Upewnij się, że serwer ma prawa do zapisu (chmod, właściciel plików).";
      }
      else
      {
        $loginError  = "✅ Plik config.php został pomyślnie utworzony.<br><br>\n\n";
        $loginError .= "<a style=\"color: white; text-decoration: none;\" href=\"?dir=" . urlencode(trim(str_replace($this->baseDir, '', $this->currentDir), DIRECTORY_SEPARATOR)) . "\">⬅️ Wróć</a>";
      }

    }

    $csrfField = '<input type="hidden" name="csrf_token" value="'
        . $this->EscapeHtml($_SESSION['csrf_token']) . '">';
    echo $this->oView::headLogin('Tworzenie config.php');
    echo $this->oView::loginForm($_SERVER['REQUEST_URI'], $loginError, "Tworzenie zabezpieczenia", $csrfField);
    echo $this->oView::footer();
    exit;
} //koniec funcji create config


/**
 * Generuje formularz wyszukiwania (tylko dla admina).
 * Zwraca gotowy HTML jako string (nie echo) – dzięki temu łatwo wstawić w dowolnym miejscu widoku.
 *
 * Uwaga:
 * - używa $this->baseDir i $this->currentDir do wyliczenia pola 'dir'
 * - zachowuje dotychczasowe zachowanie checkboxa 'recursive'
 * - bezpiecznie obsługuje $_GET['q'] (ignoruje, jeśli tablica)
 */
public function setCreateSerarch(bool $public = false): string
{
    // Jeżeli nie public – jak dotąd tylko dla admina
    if (!$public && !$this->isAdmin()) {
        return '';
    }

    $sQuery    = (isset($_GET['q']) && !is_array($_GET['q'])) ? (string)$_GET['q'] : '';
    $relDirRaw = trim(str_replace($this->baseDir, '', $this->currentDir), DIRECTORY_SEPARATOR);
    $relDirEsc = $this->EscapeHtml($relDirRaw);

    $isRecursiveChecked = (!isset($_GET['recursive']) || $_GET['recursive'] == '1') ? ' checked' : '';
    $showClear = !empty($_GET['q']);
    $clearUrl  = $this->EscapeHtml($_SERVER['PHP_SELF'])
            . '?dir=' . rawurlencode($relDirRaw);

    $sQueryEsc = $this->EscapeHtml($sQuery);

    // dla public: komunikat że wyszukuje tylko MP3
    $publicNote = $public
      ? '<br><small style="color:#aaa; margin-left:8px;">Wyszukuje MP3 i WIDEO, w tym podkatalogi. Użyj filtra: ext:audio, ext:video lub np. ext:mp3,mp4</small>'
      : '';

    $recursiveCtl = $public
      ? '<input type="hidden" name="recursive" value="1">'
      : '<label style="margin-left:8px;"><input type="checkbox" name="recursive" value="1"'
          . $isRecursiveChecked . '> Szukaj w podkatalogach</label>';

    return '
    <form id="searchForm" class="sticky-search" method="get" action="" style="margin:0 0 12px 0;">
        <input type="hidden" name="dir" value="' . $relDirEsc . '">
        <input
          id="searchInput"
          type="search"
          name="q"
          value="' . $sQueryEsc . '"
          placeholder="Szukaj (min. 2 znaki)…"
          inputmode="search"
          enterkeyhint="search"
          autocomplete="off"
          autocorrect="off"
          autocapitalize="none"
          spellcheck="false"
          style="padding:12px 14px;margin-bottom:12px;width:100%;box-sizing:border-box;background:#333;border:1px solid #555;color:#fff;border-radius:10px;font-size:16px;line-height:1.2;"
        >
        ' . $recursiveCtl . '
        ' . ($showClear ? '<a class="btn" href="' . $clearUrl . '">Wyczyść wyniki</a>' : '') . '
    </form>
    <div id="liveResults"></div>';
}

/** Konwersja bajtów na MB (2 miejsca po przecinku) */
public function bytesToMB(int $bytes): float
{
    return round($bytes / (1024 ** 2), 2);
}

public function formatBytes(int $bytes): string
{
    if ($bytes < 1024 ** 3) {
        return number_format($bytes / (1024 ** 2), 2, ',', ' ') . ' MB';
    }
    if ($bytes < 1024 ** 4) {
        return number_format($bytes / (1024 ** 3), 2, ',', ' ') . ' GB';
    }
    return number_format($bytes / (1024 ** 4), 2, ',', ' ') . ' TB';
}

/**
 * Rekurencyjny rozmiar katalogu w bajtach.
 * Wynik jest cache'owany w .dircache.json — przeliczany tylko gdy zmieni się
 * mtime któregoś z bezpośrednich dzieci (dodanie/usunięcie pliku) lub po 2h TTL.
 */
public function getDirectorySize(string $dir): int
{
    $dir = realpath($dir);
    if ($dir === false || !is_dir($dir)) return 0;
    if (strncmp($dir, $this->baseDir, strlen($this->baseDir)) !== 0) return 0;

    $cached = $this->dirCache->get($dir);
    if ($cached !== null) return $cached;

    $bytes = 0;
    try {
        $it = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS),
            RecursiveIteratorIterator::SELF_FIRST
        );
        foreach ($it as $item) {
            if ($item->isLink()) continue;
            if ($item->isFile()) $bytes += (int)$item->getSize();
        }
    } catch (Throwable $e) {}

    $this->dirCache->set($dir, $bytes);
    return $bytes;
}

/**
 * Zwraca foldery i pliki bieżącego katalogu + meta (rozmiar, mtime, url, ikonka).
 * - Rozmiar plików w MB wyliczany zawsze.
 * - Rozmiar folderów w MB wyliczany tylko, gdy ?folder_size=1 (żeby nie mielić dysku).
 */
public function getFilesAndFolders(): array
{
    $folders = [];
    $files   = [];

    $relDirCurrent  = $this->relativeToBase($this->currentDir) ?: '/';

    $di = new DirectoryIterator($this->currentDir);
    foreach ($di as $info) {
        if ($info->isDot()) continue;

        $name = $info->getFilename();
        // ukryj wrażliwe/ukryte wpisy (dotfiles, php/json/sql/db/env, backupy, macOS ._*)
        if ($this->isHiddenEntry($name)) continue;

        $path = $info->getPathname();

        if ($info->isDir()) {
            $sizeBytes = -1; // ładowany leniwie przez AJAX
            $folders[] = [
                'name'       => $name,
                'path'       => $path,
                'is_dir'     => true,
                'size_bytes' => $sizeBytes,
                'size_mb'    => 0,
                'mtime'      => $info->getMTime(),
                'url'        => $_SERVER['PHP_SELF'] . '?dir=' . rawurlencode(trim(str_replace($this->baseDir, '', $path), DIRECTORY_SEPARATOR)),
                'icon'       => '📁', // albo $this->getFileTypeIcon($name) jeśli tak wolisz
            ];
            continue;
        }

        // pliki (wrażliwe rozszerzenia odfiltrowane już przez isHiddenEntry)
        $sizeBytes = (int)$info->getSize();
        $files[] = [
            'name'       => $name,
            'path'       => $path,
            'is_dir'     => false,
            'size_bytes' => $sizeBytes,
            'size_mb'    => $this->bytesToMB($sizeBytes),
            'mtime'      => $info->getMTime(),
            'url'        => $this->getFileUrl($path),
            'icon'       => $this->getFileTypeIcon($name),
            'thumb_url'  => $this->isPreviewable($name)
                                ? $this->BuildThumbUrl($relDirCurrent, $name)
                                : null,
        ];
    }

    // sortowanie jak wcześniej
    usort($folders, fn($a,$b) => strnatcasecmp($a['name'], $b['name']));
    usort($files,   fn($a,$b) => strnatcasecmp($a['name'], $b['name']));

    return ['folders' => $folders, 'files' => $files];
}

public function relativeToBase(string $abs): string
{
    $real = realpath($abs) ?: $abs;
    if (strncmp($real, $this->baseDir, strlen($this->baseDir)) !== 0) {
        return '';
    }
    return ltrim(str_replace($this->baseDir, '', $real), DIRECTORY_SEPARATOR);
}

/*
 * @version 1.0
 * @param array $aFiles Lista plików z getFilesAndFolders()
 * @return array
 */
public function GetImageFiles(array $aFiles): array
{
    return array_values(array_filter($aFiles, fn($f) => $this->isPreviewable((string)($f['name'] ?? ''))));
}

/*
 * @version 1.0
 * @param string $sKey Nazwa parametru GET
 * @param int $iDefault Wartość domyślna
 * @param int $iMin Minimalna wartość
 * @param int $iMax Maksymalna wartość
 * @return int
 */
public function GetRequestInt(string $sKey, int $iDefault = 1, int $iMin = 1, int $iMax = 999999): int
{
    if (!isset($_GET[$sKey]) || is_array($_GET[$sKey])) {
        return $iDefault;
    }

    $iValue = (int)$_GET[$sKey];

    if ($iValue < $iMin) {
        return $iMin;
    }

    if ($iValue > $iMax) {
        return $iMax;
    }

    return $iValue;
}

/*
 * @version 1.0
 * @param array $aItems Lista elementów
 * @param int $iPage Aktualna strona
 * @param int $iPerPage Liczba elementów na stronę
 * @return array
 */
public function PaginateItems(array $aItems, int $iPage, int $iPerPage): array
{
    $iTotalItems = count($aItems);
    $iTotalPages = max(1, (int)ceil($iTotalItems / $iPerPage));
    $iPage       = max(1, min($iPage, $iTotalPages));
    $iOffset     = ($iPage - 1) * $iPerPage;

    return [
        'items'       => array_slice($aItems, $iOffset, $iPerPage),
        'page'        => $iPage,
        'per_page'    => $iPerPage,
        'total_items' => $iTotalItems,
        'total_pages' => $iTotalPages,
    ];
}

/*
 * @version 1.0
 * @param string $sView Tryb widoku
 * @param int $iPage Numer strony
 * @return string
 */
public function BuildCurrentDirUrl(string $sView = '', int $iPage = 1): string
{
    $sRelDir = $this->relativeToBase($this->getCurrentDir());

    $aQuery = [];

    if ($sRelDir !== '') {
        $aQuery['dir'] = $sRelDir;
    }

    if ($sView !== '') {
        $aQuery['view'] = $sView;
    }

    if ($iPage > 1) {
        $aQuery['page'] = $iPage;
    }

    $sQuery = http_build_query($aQuery);

    return $_SERVER['PHP_SELF'] . ($sQuery !== '' ? '?' . $sQuery : '');
}

/*
 * @version 1.0
 * @param array $aImages Lista grafik dla aktualnej strony
 * @param int $iPage Aktualna strona
 * @param int $iTotalPages Liczba stron
 * @param int $iTotalImages Liczba wszystkich grafik
 * @return string
 */
public function RenderImageGallery(array $aImages): string
{
    if (count($aImages) === 0) {
        return '<div class="gallery-empty">Brak grafik w tym katalogu.</div>';
    }

    $aJs = [];
    foreach ($aImages as $aImg) {
        $sUrl   = (string)($aImg['url']       ?? '');
        $sThumb = (string)($aImg['thumb_url'] ?? $sUrl);
        $sName  = (string)($aImg['name']      ?? '');
        if ($sUrl === '') continue;
        $aJs[] = ['url' => $sUrl, 'thumb' => $sThumb, 'name' => $sName];
    }
    $sJsData = json_encode($aJs, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_HEX_TAG);

    return <<<HTML
<div id="galleryGrid"></div>

<div id="galleryLightbox">
    <div id="galleryLbBg"></div>

    <div class="gallery-lb-hs" id="galleryLbHsPrev">
        <button class="gallery-lb-hs-btn" aria-label="Poprzednie">&#8249;</button>
    </div>
    <div class="gallery-lb-hs" id="galleryLbHsNext">
        <button class="gallery-lb-hs-btn" aria-label="Następne">&#8250;</button>
    </div>

    <div id="galleryLbImgWrap">
        <img id="galleryLbImg" src="" alt="">
    </div>

    <div id="galleryLbBar">
        <button id="galleryLbBarPrev" aria-label="Poprzednie">&#8249;</button>
        <span id="galleryLbCount"></span>
        <button id="galleryLbBarNext" aria-label="Następne">&#8250;</button>
        <a id="galleryLbDownload" href="#" download="">&#8659; Pobierz</a>
    </div>

    <button id="galleryLbClose" aria-label="Zamknij">&#10005;</button>
</div>

<script>window._galleryImages = {$sJsData};</script>
HTML;
}

    /*
    * @version 1.0
    * @param int $iPage Aktualna strona
    * @param int $iTotalPages Liczba stron
    * @return string
    */
    public function RenderPaginationHtml(string $sView, int $iPage, int $iTotalPages): string
    {
        if ($iTotalPages <= 1) {
            return '';
        }

        $iStart = max(1, $iPage - 3);
        $iEnd   = min($iTotalPages, $iPage + 3);
        $url    = fn(int $p) => $this->EscapeHtml($this->BuildCurrentDirUrl($sView, $p));

        $sHtml  = '<div class="gallery-pagination">' . PHP_EOL;

        if ($iPage > 1) {
            $sHtml .= '<a href="' . $url($iPage - 1) . '">‹ Poprzednia</a>' . PHP_EOL;
        }

        if ($iStart > 1) {
            $sHtml .= '<a href="' . $url(1) . '">1</a>' . PHP_EOL;
            if ($iStart > 2) { $sHtml .= '<span>...</span>' . PHP_EOL; }
        }

        for ($i = $iStart; $i <= $iEnd; $i++) {
            $sHtml .= ($i === $iPage)
                ? "<span class=\"active\">{$i}</span>" . PHP_EOL
                : "<a href=\"{$url($i)}\">{$i}</a>" . PHP_EOL;
        }

        if ($iEnd < $iTotalPages) {
            if ($iEnd < $iTotalPages - 1) { $sHtml .= '<span>...</span>' . PHP_EOL; }
            $sHtml .= '<a href="' . $url($iTotalPages) . '">' . $iTotalPages . '</a>' . PHP_EOL;
        }

        if ($iPage < $iTotalPages) {
            $sHtml .= '<a href="' . $url($iPage + 1) . '">Następna ›</a>' . PHP_EOL;
        }

        return "{$sHtml}</div>\n";
    }

    /*
 * @version 1.0
 * @param array $aFiles Lista plików z getFilesAndFolders()
 * @return array
 */
public function GetVideoFiles(array $aFiles): array
{
    return array_values(array_filter($aFiles, fn($f) => $this->isVideo((string)($f['name'] ?? ''))));
}

public function GetAudioFiles(array $aFiles): array
{
    return array_values(array_filter($aFiles, fn($f) => $this->isAudio((string)($f['name'] ?? ''))));
}

public function RenderAudioPlayer(array $aTracks): string
{
    if (empty($aTracks)) {
        return '<div class="ap-empty">Brak plików audio w tym katalogu.</div>';
    }

    $sRelDir = $this->relativeToBase($this->getCurrentDir());
    $total   = count($aTracks);

    $sidebarItems = '';
    $allUrls      = [];
    $allTitles    = [];
    foreach ($aTracks as $i => $t) {
        $name   = (string)($t['name'] ?? '');
        $url    = $this->BuildStreamUrl($sRelDir, $name, 'stream_audio');
        $title  = pathinfo($name, PATHINFO_FILENAME);
        $urlEsc   = $this->EscapeHtml($url);
        $titleEsc = $this->EscapeHtml($title);
        $allUrls[]    = $url;
        $allTitles[]  = $title;
        $sidebarItems .= "<div class=\"ap-track-row\">"
            . "<button class=\"ap-track audio-play-btn\" data-index=\"{$i}\""
            .  " data-audio-url=\"{$urlEsc}\" data-audio-title=\"{$titleEsc}\">"
            . "<span class=\"ap-track-num\">" . $i + 1 . "</span>"
            . "<span class=\"ap-track-info\"><span class=\"ap-track-name\">{$titleEsc}</span></span>"
            . "<span class=\"ap-track-bars\" aria-hidden=\"true\"><span></span><span></span><span></span></span>"
            . "</button>"
            . "<button class=\"ap-add-btn audio-add-btn\" data-audio-url=\"{$urlEsc}\" data-audio-title=\"{$titleEsc}\" title=\"Dodaj do kolejki\">+</button>"
            . "</div>\n";
    }

    $allUrlsJson   = htmlspecialchars(json_encode($allUrls),   ENT_QUOTES, 'UTF-8');
    $allTitlesJson = htmlspecialchars(json_encode($allTitles), ENT_QUOTES, 'UTF-8');

    return <<<HTML
<div class="ap-viewer" id="apViewer"
     data-all-urls="{$allUrlsJson}"
     data-all-titles="{$allTitlesJson}">
  <div class="ap-sidebar" id="apSidebar">
    <div class="ap-sidebar-title">Playlista &mdash; {$total} utworów</div>
    {$sidebarItems}
  </div>
  <div class="ap-main">
    <div class="ap-art" id="apArt">🎵</div>
    <div class="ap-track-title" id="apTitle">Wybierz utwór</div>
    <div class="ap-track-sub" id="apSub">{$total} utworów w katalogu</div>
    <div class="ap-progress-wrap">
      <span class="ap-time" id="apCurrent">0:00</span>
      <div class="ap-progress-bar" id="apBar"><div class="ap-progress-fill" id="apFill"></div></div>
      <span class="ap-time" id="apDuration">–:––</span>
    </div>
    <div class="ap-controls">
      <button class="ap-btn" id="apPrev" title="Poprzedni">⏮</button>
      <button class="ap-btn ap-btn-play" id="apPlay" title="Odtwórz">▶</button>
      <button class="ap-btn" id="apNext" title="Następny">⏭</button>
    </div>
    <div class="ap-volume-wrap">
      <span>🔈</span>
      <input type="range" class="ap-volume" id="apVolume" min="0" max="1" step="0.02" value="1">
      <span>🔊</span>
    </div>
    <div style="margin-top:14px;display:flex;gap:8px;flex-wrap:wrap;justify-content:center;">
      <button class="btn" id="apPlayAll">▶ Odtwórz wszystkie</button>
      <button class="btn" id="apAddAll">➕ Dodaj wszystkie</button>
      <button class="btn" id="apShowQueue">📋 Kolejka</button>
    </div>
  </div>
</div>
HTML;
}

/*
 * @version 1.0
 * @param array $aVideos Lista filmów dla aktualnej strony
 * @param int $iPage Aktualna strona
 * @param int $iTotalPages Liczba stron
 * @param int $iTotalVideos Liczba wszystkich filmów
 * @return string
 */
public function RenderVideoViewer(array $aVideos, int $iPage, int $iTotalPages, int $iTotalVideos): string
{
    if (count($aVideos) === 0) {
        return '<div class="video-empty">Brak plików wideo w tym katalogu.</div>';
    }

    $sHtml = '<div class="video-viewer" id="videoViewer" data-page="' . (int)$iPage . '">' . PHP_EOL;

    $sFirstName = (string)($aVideos[0]['name'] ?? '');
    $sRelDir    = $this->relativeToBase($this->getCurrentDir());
    $sFirstUrl  = $this->BuildStreamUrl($sRelDir, $sFirstName, 'stream');

    $sHtml .= '<div class="video-stage">' . PHP_EOL;

    $sHtml .= '<div class="video-actions">' . PHP_EOL;
    $sHtml .= '<a class="video-action-btn" id="videoDownload" href="' . $this->EscapeHtml($sFirstUrl) . '" download="' . $this->EscapeHtml($sFirstName) . '">⬇ Pobierz</a>' . PHP_EOL;
    $sHtml .= '<button type="button" class="video-action-btn share-btn" id="videoShare" data-file-url="' . $this->EscapeHtml($sFirstUrl) . '" data-name="' . $this->EscapeHtml($sFirstName) . '">🔗 Udostępnij</button>' . PHP_EOL;
    $sHtml .= '</div>' . PHP_EOL;

    $sHtml .= '<button type="button" class="video-nav video-prev" id="videoPrev" aria-label="Poprzedni film">‹</button>' . PHP_EOL;

    $sHtml .= '<video id="videoMainPlayer" controls preload="metadata">' . PHP_EOL;
    $sHtml .= '<source id="videoMainSource" src="' . $this->EscapeHtml($sFirstUrl) . '">' . PHP_EOL;
    $sHtml .= 'Twoja przeglądarka nie obsługuje odtwarzacza wideo.' . PHP_EOL;
    $sHtml .= '</video>' . PHP_EOL;

    $sHtml .= '<button type="button" class="video-nav video-next" id="videoNext" aria-label="Następny film">›</button>' . PHP_EOL;
    $sHtml .= '</div>' . PHP_EOL;

    $sHtml .= '<div class="video-caption" id="videoCaption">'
        . $this->EscapeHtml($sFirstName)
        . ' — strona ' . (int)$iPage . ' / ' . (int)$iTotalPages
        . ', filmów: ' . (int)$iTotalVideos
        . '</div>' . PHP_EOL;

    

    $sHtml .= '<div class="video-items" id="videoItems">' . PHP_EOL;

    foreach ($aVideos as $iIndex => $aVideo) {
        $sName = (string)($aVideo['name'] ?? '');
        $sUrl  = $this->BuildStreamUrl($sRelDir, $sName, 'stream');

        if ($sName === '' || $sUrl === '') {
            continue;
        }

        $sHtml .= '<button type="button" class="video-item' . ($iIndex === 0 ? ' active' : '') . '"'
            . ' data-index="' . (int)$iIndex . '"'
            . ' data-file="' . $this->EscapeHtml($sUrl) . '"'
            . ' data-name="' . $this->EscapeHtml($sName) . '">'
            . '<span class="video-item-icon">🎬</span>'
            . '<span class="video-item-name">' . $this->EscapeHtml($sName) . '</span>'
            . '</button>' . PHP_EOL;
    }

    $sHtml .= '</div>' . PHP_EOL;
    $sHtml .= '</div>' . PHP_EOL;

    return $sHtml;
}

/*
 * @version 1.0
 * @param int $iPage Aktualna strona
 * @param int $iTotalPages Liczba stron
 * @return string
 */
/* ----------  INTEGRITY / CHECKSUM  ---------- */

public function integritySnapshotFile(string $dir): string
{
    return $dir . DIRECTORY_SEPARATOR . '.integrity.json';
}

private function integrityFileFingerprint(string $path, int $size): string
{
    // Dla dużych plików (>50 MB) pomijamy SHA-256 — używamy mtime+size
    // Wystarczy do wykrycia zmian, nie obciąża I/O przy dużych plikach wideo
    if ($size > 52428800) {
        return 'fast:' . filemtime($path) . ':' . $size;
    }
    return hash_file('sha256', $path);
}

private function integrityIterator(string $dir, bool $recursive): iterable
{
    if ($recursive) {
        return new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS),
            RecursiveIteratorIterator::SELF_FIRST
        );
    }
    return new DirectoryIterator($dir);
}

private function integritySkip(string $rel, string $basename): bool
{
    return in_array($basename, ['.integrity.json', '.DS_Store', '.media_cache.zip', '.media_cache.json'], true)
        || str_starts_with($rel, '.thumbcache' . DIRECTORY_SEPARATOR)
        || str_starts_with($rel, '.media_cache');
}

public function buildIntegritySnapshot(string $dir, bool $recursive): array
{
    $files = [];
    try {
        foreach ($this->integrityIterator($dir, $recursive) as $item) {
            if ($item->isLink() || $item->isDir()) continue;
            $path = $item->getPathname();
            $rel  = ltrim(str_replace($dir, '', $path), DIRECTORY_SEPARATOR);
            if ($this->integritySkip($rel, basename($path))) continue;
            $sz = $item->getSize();
            $files[$rel] = [
                'hash'  => $this->integrityFileFingerprint($path, $sz),
                'size'  => $sz,
                'mtime' => $item->getMTime(),
            ];
        }
    } catch (Throwable) {}
    ksort($files);
    return $files;
}

public function streamIntegrityBuild(string $dir, bool $recursive): void
{
    @ini_set('display_errors', '0');
    @ini_set('zlib.output_compression', '0');
    @ini_set('output_buffering', 'Off');
    while (ob_get_level() > 0) @ob_end_clean();
    ob_implicit_flush(true);
    session_write_close();

    if (function_exists('apache_setenv')) {
        @apache_setenv('no-gzip', '1');
        @apache_setenv('dont-vary', '1');
    }
    header('Content-Type: text/event-stream; charset=utf-8');
    header('Cache-Control: no-cache, no-store, must-revalidate');
    header('Pragma: no-cache');
    header('X-Accel-Buffering: no');
    header('Connection: keep-alive');
    set_time_limit(0);
    ignore_user_abort(true);

    $sseSend = function(string $event, array $data): void {
        echo ($event !== 'message' ? "event: $event\n" : '') . 'data: ' . json_encode($data) . "\n\n";
        flush();
    };

    $abs = realpath($dir);
    if ($abs === false || !is_dir($abs)
        || $abs !== $this->baseDir && strncmp($abs, $this->baseDir . DIRECTORY_SEPARATOR, strlen($this->baseDir) + 1) !== 0) {
        $sseSend('ziperr', ['msg' => 'Forbidden']); exit;
    }

    // Zbierz listę plików najpierw, żeby znać total
    $items = [];
    try {
        foreach ($this->integrityIterator($abs, $recursive) as $item) {
            if ($item->isLink() || $item->isDir()) continue;
            $path = $item->getPathname();
            $rel  = ltrim(str_replace($abs, '', $path), DIRECTORY_SEPARATOR);
            if ($this->integritySkip($rel, basename($path))) continue;
            $items[] = [$path, $rel, $item->getSize(), $item->getMTime()];
        }
    } catch (Throwable) {}

    $total = count($items);
    $sseSend('message', ['total' => $total, 'done' => 0, 'file' => '']);

    $files = [];
    foreach ($items as $i => [$path, $rel, $sz, $mtime]) {
        $files[$rel] = [
            'hash'  => $this->integrityFileFingerprint($path, $sz),
            'size'  => $sz,
            'mtime' => $mtime,
        ];
        if ($i % 5 === 0 || $i === $total - 1) {
            $sseSend('message', ['total' => $total, 'done' => $i + 1, 'file' => basename($path)]);
        }
    }
    ksort($files);
    $this->saveIntegritySnapshot($abs, $files, $recursive);

    session_start();
    $_SESSION['integrity_flash'] = [
        'action'    => 'snapshot',
        'count'     => $total,
        'recursive' => $recursive,
        'date'      => date('Y-m-d H:i:s'),
    ];
    session_write_close();

    $sseSend('done', ['count' => $total]);
    exit;
}

public function saveIntegritySnapshot(string $dir, array $files, bool $recursive): bool
{
    $data = [
        'captured_at' => date('Y-m-d H:i:s'),
        'recursive'   => $recursive,
        'files'       => $files,
    ];
    return (bool) file_put_contents(
        $this->integritySnapshotFile($dir),
        json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
    );
}

public function loadIntegritySnapshot(string $dir): ?array
{
    $path = $this->integritySnapshotFile($dir);
    if (!file_exists($path)) return null;
    $data = json_decode(file_get_contents($path), true);
    return is_array($data) ? $data : null;
}

public function verifyIntegritySnapshot(string $dir, bool $recursive): array
{
    $snapshot = $this->loadIntegritySnapshot($dir);
    if ($snapshot === null) return ['error' => 'Brak snapshotu'];

    $current  = $this->buildIntegritySnapshot($dir, $recursive);
    $saved    = $snapshot['files'] ?? [];

    $changed = $added = $removed = $ok = [];

    foreach ($current as $rel => $info) {
        if (!isset($saved[$rel])) {
            $added[] = $rel;
        } elseif ($saved[$rel]['hash'] !== $info['hash']) {
            $changed[] = ['file' => $rel, 'old_size' => $saved[$rel]['size'], 'new_size' => $info['size']];
        } else {
            $ok[] = $rel;
        }
    }
    foreach ($saved as $rel => $info) {
        if (!isset($current[$rel])) $removed[] = $rel;
    }

    return [
        'snapshot_date' => $snapshot['captured_at'],
        'recursive'     => $snapshot['recursive'],
        'changed'       => $changed,
        'added'         => $added,
        'removed'       => $removed,
        'ok_count'      => count($ok),
    ];
}

}//koniec klasy FilesBroweser


// Utworzenie instancji klasy
$browser    = new FileBrowser(__DIR__, isset($_GET['dir']) && !is_array($_GET['dir']) ? $_GET['dir'] : null);
$oView      = new View();
$baseDir    = $browser->getBaseDir();
$currentDir = $browser->getCurrentDir();

$sAction         = $browser->GetRequestString('action');
$sDirParam       = $browser->GetRequestString('dir');
$sFileParam      = $browser->GetRequestString('file');
$sQueryParam     = $browser->GetRequestString('q');
$sRecursiveParam = $browser->GetRequestString('recursive');
$sApiParam       = $browser->GetRequestString('api');


// Obsługa pobierania
if ($sAction === 'download' && $sFileParam !== '') {
    if (!$browser->checkAccess(false)) {
        $browser->SendPlainHttpError(401, 'Unauthorized');
    }

    $browser->handleDownload($sFileParam);
    exit;
}

// API wyszukiwania – tylko admin, PRZED HTML
if ($sApiParam === '1' && $sQueryParam === '') {
    $browser->SendJsonResponse([
        'ok'    => false,
        'error' => 'Missing query parameter: q',
    ], 400);
}

if ($sApiParam === '1' && $sQueryParam !== '') {
    $sQuery = trim($sQueryParam);
    $bRecursive = ($sRecursiveParam === '' || $sRecursiveParam === '1');

    if (!$browser->isAdmin()) {
        // Publiczne API: tylko audio/wideo i wymuszona rekurencja.
        if (stripos($sQuery, 'ext:') === false) {
            $sQuery .= ' ext:audio,video';
        }

        $bRecursive = true;
    }

    $aItems = $browser->searchFiles($sQuery, $currentDir, $bRecursive, 500);

        $browser->SendJsonResponse([
        'ok'        => true,
        'query'     => $sQuery,
        'recursive' => $bRecursive,
        'count'     => count($aItems),
        'items'     => $aItems,
    ]);
}


// Obsługa streamingu i miniaturek przed generowaniem HTML.
switch ($sAction) {
    case 'thumb':
        if ($sDirParam === '' || $sFileParam === '') {
            break;
        }
        if (!$browser->checkAccess(false)) {
            $browser->SendPlainHttpError(401, 'Unauthorized');
        }
        $path = $browser->ResolveStreamPathFromRequest($sDirParam, $sFileParam);
        if ($path === null) {
            $browser->SendPlainHttpError(400, 'Bad request');
        }
        $browser->serveThumb($path);
        exit;

    case 'stream':
        if ($sDirParam === '' || $sFileParam === '') {
            break;
        }

        if (!$browser->checkAccess(false)) {
            $browser->SendPlainHttpError(401, 'Unauthorized');
        }

        $path = $browser->ResolveStreamPathFromRequest($sDirParam, $sFileParam);

        if ($path === null) {
            $browser->SendPlainHttpError(400, 'Bad request');
        }

        $browser->streamVideo($path);
        exit;

    case 'stream_audio':
        if ($sDirParam === '' || $sFileParam === '') {
            break;
        }

        if (!$browser->checkAccess(false)) {
            $browser->SendPlainHttpError(401, 'Unauthorized');
        }

        $path = $browser->ResolveStreamPathFromRequest($sDirParam, $sFileParam);

        if ($path === null) {
            $browser->SendPlainHttpError(400, 'Bad request');
        }

        $browser->streamAudio($path);
        exit;

    case 'dir_size':
        if (!$browser->checkAccess(false)) {
            $browser->SendPlainHttpError(401, 'Unauthorized');
        }
        header('Content-Type: application/json; charset=utf-8');
        header('Cache-Control: no-cache');
        $relD  = ltrim(str_replace(['..', "\0"], '', $sDirParam), '/\\');
        $absD  = realpath($browser->getBaseDir() . ($relD !== '' ? DIRECTORY_SEPARATOR . $relD : ''));
        $baseD = $browser->getBaseDir();
        if ($absD === false || !is_dir($absD)
            || $absD !== $baseD && strncmp($absD, $baseD . DIRECTORY_SEPARATOR, strlen($baseD) + 1) !== 0) {
            echo json_encode(['f' => '—']); exit;
        }
        $sz = $browser->getDirectorySize($absD);
        echo json_encode(['f' => $sz > 0 ? $browser->formatBytes($sz) : '—']);
        exit;

    case 'integrity_build':
        if (!$browser->isAdmin()) {
            $browser->SendPlainHttpError(403, 'Forbidden');
        }
        $relD    = ltrim(str_replace(['..', "\0"], '', $sDirParam), '/\\');
        $absD    = realpath($browser->getBaseDir() . ($relD !== '' ? DIRECTORY_SEPARATOR . $relD : ''));
        $browser->streamIntegrityBuild((string)$absD, isset($_GET['recursive']) && $_GET['recursive'] === '1');
        exit;

    case 'zip_build':
        if (!$browser->isAdmin()) {
            $browser->SendPlainHttpError(403, 'Forbidden');
        }
        $browser->streamZipBuild($sDirParam);
        exit;

    case 'zip_cached':
        if (!$browser->checkAccess(false)) {
            $browser->SendPlainHttpError(401, 'Unauthorized');
        }
        $browser->streamZipCached($sDirParam);
        exit;
}

$browser->checkAccess();
$browser->handleAdminLogin();




// Pobranie listy plików i folderów
$directoryContent = $browser->getFilesAndFolders();

// Dynamiczne tryby widoku dla katalogów z dużą liczbą grafik lub filmów.
$aGalleryImages         = $browser->GetImageFiles($directoryContent['files']);
$iGalleryImagesCount    = count($aGalleryImages);
$iGalleryAutoThreshold  = 12;
$iGalleryPerPage        = 24;

$aVideoFiles            = $browser->GetVideoFiles($directoryContent['files']);
$iVideoFilesCount       = count($aVideoFiles);
$iVideoAutoThreshold    = 4;
$iVideoPerPage          = 12;

$sRequestedView         = $browser->GetRequestString('view', '');
$iCurrentPage           = $browser->GetRequestInt('page', 1, 1);
$iGalleryPage           = $iCurrentPage;
$iVideoPage             = $iCurrentPage;

$bAutoGalleryAvailable  = ($iGalleryImagesCount >= $iGalleryAutoThreshold);
$bAutoVideoAvailable    = ($iVideoFilesCount >= $iVideoAutoThreshold);

$aAudioFiles            = $browser->GetAudioFiles($directoryContent['files']);
$iAudioFilesCount       = count($aAudioFiles);

$bGalleryView           = ($sRequestedView === 'gallery');
$bVideoView             = ($sRequestedView === 'video');
$bAudioView             = ($sRequestedView === 'audio');

$aGalleryPaginationData = $browser->PaginateItems($aGalleryImages, $iGalleryPage, $iGalleryPerPage);
$aVideoPaginationData   = $browser->PaginateItems($aVideoFiles, $iVideoPage, $iVideoPerPage);

$hasMedia = false;
$hasMp3   = false;
foreach ($directoryContent['files'] as $f) {
    $isAudio = $browser->isAudio($f['name']);
    if ($isAudio)                            { $hasMp3   = true; }
    if ($isAudio || $browser->isVideo($f['name'])) { $hasMedia = true; }
    if ($hasMedia && $hasMp3) break;
}
// Klucz katalogu do namespacowania ciastek (per folder)
$currentRel = $browser->relativeToBase($browser->getCurrentDir()) ?: '/';
$dirKeyB64  = base64_encode($currentRel);


if ($browser->isAdmin() && isset($_GET['admin_action']))
{
    $action    = $_GET['admin_action'];
    $targetDir = realpath($baseDir . DIRECTORY_SEPARATOR . ($_GET['dir'] ?? ''));

    if ($targetDir === false
        || $targetDir !== $baseDir
            && !str_starts_with($targetDir, $baseDir . DIRECTORY_SEPARATOR))
    {
        exit('Błąd: niedozwolona ścieżka.');
    }

    $configFile = $targetDir . DIRECTORY_SEPARATOR . 'config.php';



    switch ($action)
    {
        case 'zip_cache_delete':
            if ($_SERVER['REQUEST_METHOD'] !== 'POST'
                || !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'] ?? ''))
            {
                http_response_code(403); exit('Forbidden');
            }
            $browser->clearMediaCache($targetDir);
            $relBack = trim(str_replace($baseDir, '', $targetDir), DIRECTORY_SEPARATOR);
            header('Location: ?dir=' . rawurlencode($relBack) . '&admin=1');
            exit;

        case 'edit_config':
            $browser->handleCreateConfig();
            break;

        case 'delete_config':
            if ($_SERVER['REQUEST_METHOD'] !== 'POST'
                || !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'] ?? ''))
            {
                http_response_code(403);
                exit('Forbidden');
            }
            if (file_exists($configFile))
            {
                unlink($configFile);
                header("Location: ?dir=" . urlencode(trim(str_replace($baseDir, '', $targetDir), DIRECTORY_SEPARATOR)) . "&admin=1");
                exit;
            }
            break;

        case 'create_config':
            $browser->handleCreateConfig();
            break;

        case 'integrity_snapshot':
            if ($_SERVER['REQUEST_METHOD'] !== 'POST'
                || !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'] ?? ''))
            {
                http_response_code(403); exit('Forbidden');
            }
            $recursive = !empty($_POST['recursive']);
            $snapFiles = $browser->buildIntegritySnapshot($targetDir, $recursive);
            $browser->saveIntegritySnapshot($targetDir, $snapFiles, $recursive);
            $_SESSION['integrity_flash'] = [
                'action'    => 'snapshot',
                'count'     => count($snapFiles),
                'recursive' => $recursive,
                'date'      => date('Y-m-d H:i:s'),
            ];
            $relBack = urlencode(trim(str_replace($baseDir, '', $targetDir), DIRECTORY_SEPARATOR));
            header("Location: ?dir={$relBack}&admin=1");
            exit;

        case 'integrity_verify':
            $verifyResult = $browser->verifyIntegritySnapshot($targetDir, false);
            $_SESSION['integrity_flash'] = [
                'action'  => 'verify',
                'result'  => $verifyResult,
            ];
            $relBack = urlencode(trim(str_replace($baseDir, '', $targetDir), DIRECTORY_SEPARATOR));
            header("Location: ?dir={$relBack}&admin=1");
            exit;

        case 'convert_to_mp4':
          if (!method_exists($browser, 'convertToMp4')) {
              http_response_code(501);
              echo "Funkcja konwersji MP4 nie jest jeszcze zaimplementowana.";
              break;
          }

          $inputFile  = $_GET['dir'] . DIRECTORY_SEPARATOR . $_GET['file'];
          $outputFile = str_ireplace('.avi', '.mp4', $inputFile);

          if ($browser->convertToMp4($inputFile, $outputFile)) {
              echo "Konwersja rozpoczęta pomyślnie.";
          } else {
              echo "Błąd konwersji.";
          }
          break;
        // Dodaj kolejne przypadki, jeśli potrzebujesz
    }
}
?>

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>File Browser</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">

        <?php echo $oView->setStyleCss(); ?>

</head>

<body>

<div class="file-list-container">
    <div class="top-bar">
        <div class="left-container"><h1>File Browser</h1></div>

        <div class="right-container">
            <?php
                $sessionKey = md5($currentDir);

                if ($browser->isAdmin())
                {
                    // ▼ administrator już zalogowany
                    echo '<a class="btn logout-btn" href="?logoutAdmin=1"> Wyloguj admina </a>';
                }
                else
                {
                    // ▼ link do logowania admina
                    echo '<a href="?admin=1" id="adminLoginBtn" class="btn">Zaloguj admina</a>';
                    
                }

                /* przycisk katalogowego wylogowania (jeśli istnieje sesja) */
                if (isset($_SESSION['auth'][$sessionKey]))
                {
                    echo '<a class="btn logout-btn" style="margin-left:10px" href="?dir=' .
                         urlencode(trim(str_replace($baseDir, '', $currentDir), DIRECTORY_SEPARATOR)) .
                         '&logout=1">🚪 Wyloguj katalog</a>';
                }
            ?>

        </div>
</div>



<?php
// admin – pełny formularz
if ($browser->isAdmin()) {
    echo $browser->setCreateSerarch(false);
// zwykły user – pokaż tylko, gdy jest audio/video w bieżącym katalogu
} elseif ($hasMedia) {
    echo $browser->setCreateSerarch(true);
}
?>

<?php if ($browser->isAdmin()): ?>
<?php
    $snapshot     = $browser->loadIntegritySnapshot($currentDir);
    $snapDate     = $snapshot ? $snapshot['captured_at'] : null;
    $relDirEnc    = urlencode(trim(str_replace($baseDir, '', $currentDir), DIRECTORY_SEPARATOR));
    $csrfToken    = htmlspecialchars($_SESSION['csrf_token'], ENT_QUOTES, 'UTF-8');
    $integrityFlash = $_SESSION['integrity_flash'] ?? null;
    unset($_SESSION['integrity_flash']);
?>
<div style="margin:0 0 12px;padding:8px 12px;background:#1a1a1a;border:1px solid #333;border-radius:8px;display:flex;align-items:center;flex-wrap:wrap;gap:8px;">
    <span style="color:#aaa;font-size:13px;">🔐 Integralność:</span>
    <?php if ($snapDate): ?>
        <span style="color:#777;font-size:12px;">snapshot: <?php echo htmlspecialchars($snapDate, ENT_QUOTES, 'UTF-8'); ?></span>
        <a class="btn" href="?dir=<?php echo $relDirEnc; ?>&admin=1&admin_action=integrity_verify">🔍 Sprawdź</a>
    <?php else: ?>
        <span style="color:#555;font-size:12px;">brak snapshotu</span>
    <?php endif; ?>
    <label style="font-size:12px;color:#aaa;margin-right:4px;">
        <input type="checkbox" id="snapshotRecursive"> rekurencyjnie
    </label>
    <button class="btn" onclick="startIntegrityBuild(<?php echo htmlspecialchars(json_encode($currentRel === '/' ? '' : $currentRel), ENT_QUOTES); ?>)">
        📸 <?php echo $snapDate ? 'Odśwież snapshot' : 'Utwórz snapshot'; ?>
    </button>
</div>
<?php endif; ?>

<?php

$searchResults = null;
$q = '';
if (!empty($_GET['q']) && !is_array($_GET['q'])) {
    if ($browser->isAdmin() || $hasMedia) {
        $q = trim($_GET['q']);
        $recursive = isset($_GET['recursive']) && $_GET['recursive'] == '1';
        $max = 800;

        if (!$browser->isAdmin()) {
            // Domyślnie wymuś AV i rekurencję dla zwykłego usera,
            // jeśli nie podał żadnego ext:
            if (!preg_match('/\bext:/i', $q)) {
                $q .= ' ext:audio,video';
            }
            $recursive = true;
        }

        $searchResults = $browser->searchFiles($q, $currentDir, $recursive, $max);
    }
}

?>
    <?php if ($searchResults !== null): ?>
    <h3>
      Wyniki wyszukiwania dla: "<?php echo $browser->EscapeHtml($q); ?>"
      (<?php echo is_array($searchResults) ? count($searchResults) : 0; ?>)
      <?php if (!$browser->isAdmin()): ?>
        <small style="color:#aaa;"> – podkatalogi włączone</small>
      <?php endif; ?>
    </h3>
    <?php if (count($searchResults) === 0): ?>
        <p>Brak wyników.</p>
    <?php else: ?>
            <table class="table-main">
      <thead>
        <tr>
          <th>Icon</th>
          <th>Nazwa</th>
          <th>Rozszerzenie</th>
          <th>Rozmiar (MB)</th>
          <th>Akcje</th>
        </tr>
      </thead>
      <tbody>
      <?php foreach ($searchResults as $res): ?>
        <?php
            $name = $res['name'] ?? '';
            $url  = $res['url']  ?? '';
            $icon = $res['icon'] ?? '📄';
            $ext  = strtoupper(pathinfo($name, PATHINFO_EXTENSION));
            $sizeMB = isset($res['size']) ? number_format($res['size'] / (1024*1024), 2, ',', ' ') : '—';

            // Użyj katalogu, w którym faktycznie leży znaleziony plik.
            $fileDirAbs = isset($res['path']) ? dirname($res['path']) : $browser->getCurrentDir();
            $relDir = $browser->relativeToBase($fileDirAbs) ?: '/';

            $watchUrl = $browser->BuildStreamUrl($relDir, $name, 'stream');
            $audioUrl = $browser->BuildStreamUrl($relDir, $name, 'stream_audio');

            // Download zawsze z katalogu, w którym faktycznie leży znaleziony plik.
            $dlRel = $relDir;
            $dlHref = $_SERVER['PHP_SELF'] . '?dir=' . rawurlencode($dlRel) . '&action=download&file=' . rawurlencode($name);
            $isIndexHtml = (strtolower($name) === 'index.html');
        ?>
        <tr>
          <td><?php echo $icon; ?></td>
          <td><?php echo $browser->EscapeHtml($name); ?></td>
          <td><?php echo $browser->EscapeHtml($ext); ?></td>
          <td><?php echo $sizeMB; ?></td>
          <td>
                <?php echo $browser->RenderFileActions(
                    $name,
                    $url,
                    $watchUrl,
                    $audioUrl,
                    $dlHref,
                    $isIndexHtml,
                    true
                ); ?>
            </td>
        </tr>
      <?php endforeach; ?>
      </tbody>
    </table>
    <?php endif; ?>
<?php else: ?>

    <!-- tutaj istniejący kod wyświetlający normalną listę katalogu -->
    <?php
      // zbuduj relatywną ścieżkę względem baseDir
      $relPath = trim(str_replace($baseDir, '', $currentDir), DIRECTORY_SEPARATOR);
      $segments = ($relPath === '') ? [] : explode(DIRECTORY_SEPARATOR, $relPath);

      // bazowy link do root
      $crumbs = [];
      $rootHref = $browser->EscapeHtml($_SERVER['PHP_SELF']);
      $crumbs[] = '<a href="'.$rootHref.'">../</a>';

      // kolejne segmenty /a/b/c → budujemy narastająco ?dir=a, ?dir=a/b, ...
      $accum = '';
      foreach ($segments as $seg) {
          if ($seg === '') continue;
          $accum .= ($accum === '' ? '' : DIRECTORY_SEPARATOR) . $seg;
          $href = $rootHref . '?dir=' . rawurlencode($accum);
          $crumbs[] = '<a href="' . $browser->EscapeHtml($href) . '">' . $browser->EscapeHtml($seg) . '</a>';
      }
    ?>
    <nav class="breadcrumbs">
      <?php echo implode(' <span class="sep">›</span> ', $crumbs); ?>
    </nav>

    <?php if ($iGalleryImagesCount > 0 || $iVideoFilesCount > 0 || $iAudioFilesCount > 0): ?>
        <div class="gallery-toolbar">
            <div class="gallery-toolbar-info">
                <?php if ($iGalleryImagesCount > 0): ?>Grafiki: <?php echo (int)$iGalleryImagesCount; ?> | <?php endif; ?>
                <?php if ($iVideoFilesCount > 0): ?>Wideo: <?php echo (int)$iVideoFilesCount; ?> | <?php endif; ?>
                <?php if ($iAudioFilesCount > 0): ?>Audio: <?php echo (int)$iAudioFilesCount; ?><?php endif; ?>
            </div>

            <div class="gallery-toolbar-buttons">
                <?php if ($iGalleryImagesCount > 0): ?>
                    <a class="btn" href="<?php echo $browser->EscapeHtml($browser->BuildCurrentDirUrl('gallery', 1)); ?>">🖼️ Galeria</a>
                <?php endif; ?>

                <?php if ($iVideoFilesCount > 0): ?>
                    <a class="btn" href="<?php echo $browser->EscapeHtml($browser->BuildCurrentDirUrl('video', 1)); ?>">🎬 Wideo</a>
                <?php endif; ?>

                <?php if ($iAudioFilesCount > 0): ?>
                    <a class="btn" href="<?php echo $browser->EscapeHtml($browser->BuildCurrentDirUrl('audio', 1)); ?>">🎵 Muzyka</a>
                <?php endif; ?>

                <a class="btn" href="<?php echo $browser->EscapeHtml($browser->BuildCurrentDirUrl('list', 1)); ?>">📋 Lista</a>
                <?php
                $currentCacheInfo = $browser->getMediaCacheInfo($currentDir);
                if ($currentCacheInfo):
                    $cacheAge = time() - $currentCacheInfo['created'];
                    $cacheAgeStr = $cacheAge < 86400 ? 'dziś'
                        : (($d = round($cacheAge/86400)) < 7 ? "{$d} dni temu" : round($cacheAge/604800).' tyg. temu');
                ?>
                    <a class="btn" href="?action=zip_cached&dir=<?php echo rawurlencode($relPath); ?>"
                       title="Archiwum z <?php echo $cacheAgeStr; ?>, <?php echo $browser->formatBytes($currentCacheInfo['size']); ?>">
                        📦 ZIP (<?php echo $browser->formatBytes($currentCacheInfo['size']); ?>)
                    </a>
                <?php endif; ?>
                <?php if ($browser->isAdmin()): ?>
                    <button class="btn" onclick="startZipBuild(<?php echo htmlspecialchars(json_encode($relPath), ENT_QUOTES); ?>)"
                            title="<?php echo $currentCacheInfo ? 'Odbuduj archiwum ZIP' : 'Zbuduj archiwum ZIP mediów'; ?>">
                        <?php echo $currentCacheInfo ? '🔄 Odbuduj ZIP' : '📦 Buduj ZIP'; ?>
                    </button>
                    <?php if ($currentCacheInfo): ?>
                    <form method="post" action="?dir=<?php echo rawurlencode($relPath); ?>&admin=1&admin_action=zip_cache_delete" style="display:inline">
                        <input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($_SESSION['csrf_token'], ENT_QUOTES, 'UTF-8'); ?>">
                        <button type="submit" class="btn" title="Usuń zapisane archiwum">🗑️ Usuń ZIP</button>
                    </form>
                    <?php endif; ?>
                <?php endif; ?>
            </div>
        </div>
    <?php endif; ?>

    <?php if ($bGalleryView): ?>

        <?php echo $browser->RenderImageGallery($aGalleryImages); ?>

    <?php elseif ($bVideoView): ?>

        <?php
            echo $browser->RenderVideoViewer(
                $aVideoPaginationData['items'],
                (int)$aVideoPaginationData['page'],
                (int)$aVideoPaginationData['total_pages'],
                (int)$aVideoPaginationData['total_items']
            );

            echo $browser->RenderPaginationHtml('video',
                (int)$aVideoPaginationData['page'],
                (int)$aVideoPaginationData['total_pages']
            );
        ?>

    <?php elseif ($bAudioView): ?>

        <?php echo $browser->RenderAudioPlayer($aAudioFiles); ?>

    <?php else: ?>

        <table class="table-main">
            <thead>
              <tr>
                <th>Icon</th>
                <th>Nazwa</th>
                <th>Rozszerzenie</th>
                <th>Rozmiar (MB)</th>
                <th>Akcje</th>
              </tr>
            </thead>
        <tbody>



<?php foreach ($directoryContent['folders'] as $folder): ?>
    <?php
        // Rozpakowanie metadanych folderu
        $folderName = (string)($folder['name'] ?? '');
        $folderPath = (string)($folder['path'] ?? '');
        $folderUrl  = (string)($folder['url']  ?? ''); // link do wejścia w katalog z getFilesAndFolders()

        // Relatywna ścieżka do budowania własnych URL-i admina
        $relFolder  = $browser->relativeToBase($folderPath);
        $encodedDir = rawurlencode($relFolder);

        // Zabezpieczenie: jeżeli coś poszło nie tak – pomijamy ten wiersz
        if ($folderName === '' || $folderPath === '' || $folderUrl === '') {
            continue;
        }

        $configExists   = file_exists($folderPath . DIRECTORY_SEPARATOR . 'config.php');
        $folderCache    = $browser->getMediaCacheInfo($folderPath);
        $folderCacheEnc = htmlspecialchars(json_encode($relFolder), ENT_QUOTES);
    ?>
    <tr style="cursor:pointer;" onclick="window.location='<?php echo $browser->EscapeHtml($folderUrl); ?>'">
        <td>📁</td>

        <td>
            <a href="<?php echo $browser->EscapeHtml($folderUrl); ?>">
              <?php echo $browser->EscapeHtml($folderName); ?>
          </a>
        </td>

        <td>Folder</td>
        <td class="dir-size-cell" data-dir="<?php echo htmlspecialchars($relFolder, ENT_QUOTES); ?>">
          <span style="color:#555">⏳</span>
        </td>

        <td>
            <?php if ($folderCache): ?>
                <a class="btn" data-action="zip-dl" href="?action=zip_cached&dir=<?php echo $encodedDir; ?>"
                   onclick="event.stopPropagation(); return true;"
                   title="Archiwum ZIP, <?php echo $browser->formatBytes($folderCache['size']); ?>">📦 ZIP</a>
            <?php endif; ?>
            <?php if ($browser->isAdmin()): ?>
                <button class="btn" data-action="<?php echo $folderCache ? 'zip-rebuild' : 'zip-build'; ?>"
                        onclick="event.stopPropagation(); startZipBuild(<?php echo $folderCacheEnc; ?>);"
                        title="<?php echo $folderCache ? 'Odbuduj archiwum ZIP' : 'Zbuduj archiwum ZIP mediów'; ?>">
                    <?php echo $folderCache ? '🔄' : '📦'; ?>
                </button>
                <?php if ($folderCache): ?>
                <form method="post" action="?dir=<?php echo $encodedDir; ?>&admin=1&admin_action=zip_cache_delete"
                      onsubmit="event.stopPropagation(); return confirm('Usunąć zapisane archiwum ZIP?');" style="display:inline">
                    <input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($_SESSION['csrf_token'], ENT_QUOTES, 'UTF-8'); ?>">
                    <button type="submit" class="btn" data-action="zip-del" onclick="event.stopPropagation();" title="Usuń archiwum ZIP">🗑️</button>
                </form>
                <?php endif; ?>
                <?php if ($configExists): ?>
                    <a class="btn" data-action="cfg-edit" href="?dir=<?php echo $encodedDir; ?>&admin=1&admin_action=edit_config" onclick="event.stopPropagation(); return true;">✏️ Edytuj</a>
                    <form method="post" action="?dir=<?php echo $encodedDir; ?>&admin=1&admin_action=delete_config"
                          onsubmit="event.stopPropagation(); return confirm('Na pewno usunąć zabezpieczenie?');" style="display:inline">
                        <input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($_SESSION['csrf_token'], ENT_QUOTES, 'UTF-8'); ?>">
                        <button type="submit" class="btn" data-action="cfg-del" onclick="event.stopPropagation();">🗑️ Usuń</button>
                    </form>
                <?php else: ?>
                    <a class="btn" data-action="cfg-create" href="?dir=<?php echo $encodedDir; ?>&admin=1&admin_action=create_config" onclick="event.stopPropagation(); return true;">➕ Zabezpiecz</a>
                <?php endif; ?>
            <?php endif; ?>
        </td>
    </tr>
<?php endforeach; ?>


        <?php
            // Katalog bieżący — stały dla całej pętli
            $relDirFiles = $currentRel;
        ?>
        <?php foreach ($directoryContent['files'] as $f): ?>
        <?php
            $name    = $f['name'];
            $url     = $f['url'];
            $icon    = $f['icon'];
            $sizeMB  = $f['size_mb'];
            $ext     = $browser->getExt($name);

            $sLink       = $browser->BuildStreamUrl($relDirFiles, $name, 'stream');
            $audioLink   = $browser->BuildStreamUrl($relDirFiles, $name, 'stream_audio');
            $isVideo     = $browser->isVideo($name);
            $isIndexHtml = (strtolower($name) === 'index.html');
        ?>
    <tr>
        <td><?php echo $icon; ?></td>

        <td>
            <?php if ($isIndexHtml): ?>
                <a href="<?php echo $browser->EscapeHtml($url); ?>" target="_blank">
                    <?php echo $browser->EscapeHtml($name); ?>
                </a>
            <?php else: ?>
                <?php if ($isVideo): ?>
                    <a href="<?php echo $browser->EscapeHtml($sLink); ?>">
                        <?php echo $browser->EscapeHtml($name); ?>
                    </a>
                <?php else: ?>
                    <?php echo $browser->EscapeHtml($name); ?>
                <?php endif; ?>
            <?php endif; ?>
        </td>

        <td><?php echo strtoupper($ext); ?></td>

        <td><?php echo $browser->formatBytes($f['size_bytes']); ?></td>

        <td>
            <?php
                $dlHref = $_SERVER['PHP_SELF']
                    . '?dir=' . rawurlencode($relDirFiles)
                    . '&action=download'
                    . '&file=' . rawurlencode($f['name']);

                echo $browser->RenderFileActions(
                    $name,
                    $url,
                    $sLink,
                    $audioLink,
                    $dlHref,
                    $isIndexHtml,
                    false
                );
            ?>
        </td>
    </tr>
<?php endforeach; ?>

        </tbody>
    </table>

    <?php endif; ?>

<!-- koniec po wyszukiwaniu  !-->
<?php endif; ?>





</div>

<div id="previewModal" class="modal">
    <div class="modal-content">
        <button class="modal-close" onclick="closeAllModals()">✖</button>
        <div id="previewContent"></div>
    </div>
</div>

<div id="videoModal" class="modal" style="display:none;">
    <div class="modal-content" style="background:#000; padding:20px;">
        <video id="videoPlayer" controls style="max-width:90vw;max-height:85vh;width:auto;height:auto;object-fit:contain;"></video>
        <br>
        <button onclick="closeAllModals()">Close</button>
    </div>
</div>

<!-- MODAL POP-UP DO UDOSTĘPNIANIA -->
<div id="shareModal" class="modal">
    <div class="modal-content">
        <button class="modal-close" onclick="closeShareModal()">✖</button>
        <p>Skopiuj link do pliku:</p>
        <input type="text" id="shareLinkInput" readonly style="width: 80%; padding: 8px; margin-bottom: 15px; width: 100%; background: #333; border: 1px solid #555; color: #fff; border-radius: 4px;" />
        <button class="btn" onclick="copyShareLink()">📋 Kopiuj</button>

        <div style="margin-top: 20px;">
            <p>Lub zeskanuj kod QR:</p>
            <img id="qrCodeImage" alt="Kod QR" style="max-width: 150px; height: auto;" />
        </div>
    </div>
</div>

<!-- MODAL ZIP PROGRESS -->
<div id="zipModal" class="modal">
    <div class="modal-content">
        <button class="modal-close" onclick="closeZipModal()">✖</button>
        <h3 id="zipModalTitle">📦 Tworzenie archiwum ZIP</h3>
        <div class="zip-track"><div id="zipBar" class="zip-bar"></div></div>
        <div id="zipStatus" class="zip-status">Przygotowywanie...</div>
        <div id="zipFile" class="zip-file"></div>
        <button class="btn" onclick="closeZipModal()">Anuluj</button>
    </div>
</div>

<!-- MODAL INTEGRALNOŚCI -->
<div id="integrityModal" class="modal" style="display:none;">
    <div class="modal-content" style="background:#1c1c1c;padding:28px 32px;border-radius:10px;max-width:600px;width:90%;max-height:80vh;overflow-y:auto;text-align:left;">
        <button class="modal-close" onclick="document.getElementById('integrityModal').style.display='none'">✖</button>
        <div id="integrityModalBody"></div>
    </div>
</div>

<?php if ($browser->isAdmin() && $integrityFlash): ?>
<script>
(function(){
    <?php if ($integrityFlash['action'] === 'snapshot'): ?>
    const body = document.getElementById('integrityModalBody');
    body.innerHTML = `
        <h3 style="margin-top:0;color:#4caf50">📸 Snapshot zapisany</h3>
        <p style="color:#ccc">Data: <b><?php echo htmlspecialchars($integrityFlash['date'], ENT_QUOTES, 'UTF-8'); ?></b></p>
        <p style="color:#ccc">Przeskanowano plików: <b><?php echo (int)$integrityFlash['count']; ?></b></p>
        <p style="color:#ccc">Tryb: <b><?php echo $integrityFlash['recursive'] ? 'rekurencyjny' : 'bieżący katalog'; ?></b></p>
    `;
    <?php elseif ($integrityFlash['action'] === 'verify'): ?>
    <?php
        $r        = $integrityFlash['result'];
        $changed  = $r['changed']  ?? [];
        $added    = $r['added']    ?? [];
        $removed  = $r['removed']  ?? [];
        $okCount  = (int)($r['ok_count'] ?? 0);
        $snapDate = htmlspecialchars($r['snapshot_date'] ?? '—', ENT_QUOTES, 'UTF-8');
        $hasIssues = $changed || $added || $removed;

        $changedHtml = '';
        foreach ($changed as $c) {
            $f = htmlspecialchars($c['file'], ENT_QUOTES, 'UTF-8');
            $changedHtml .= "<li>{$f} <small style='color:#aaa'>({$browser->formatBytes($c['old_size'])} → {$browser->formatBytes($c['new_size'])})</small></li>";
        }
        $addedHtml = '';
        foreach ($added as $f) {
            $addedHtml .= '<li>' . htmlspecialchars($f, ENT_QUOTES, 'UTF-8') . '</li>';
        }
        $removedHtml = '';
        foreach ($removed as $f) {
            $removedHtml .= '<li>' . htmlspecialchars($f, ENT_QUOTES, 'UTF-8') . '</li>';
        }
    ?>
    const body = document.getElementById('integrityModalBody');
    body.innerHTML = `
        <h3 style="margin-top:0;color:<?php echo $hasIssues ? '#ff9800' : '#4caf50'; ?>">
            <?php echo $hasIssues ? '⚠️ Wykryto zmiany' : '✅ Brak zmian'; ?>
        </h3>
        <p style="color:#aaa;font-size:13px;">Snapshot z: <b><?php echo $snapDate; ?></b> &nbsp;|&nbsp; Zgodnych: <b><?php echo $okCount; ?></b></p>
        <?php if ($changedHtml): ?>
        <p style="color:#ff9800;margin-bottom:4px"><b>⚠️ Zmienione (<?php echo count($changed); ?>)</b></p>
        <ul style="color:#ddd;margin:0 0 12px;padding-left:20px"><?php echo $changedHtml; ?></ul>
        <?php endif; ?>
        <?php if ($addedHtml): ?>
        <p style="color:#2196f3;margin-bottom:4px"><b>➕ Nowe (<?php echo count($added); ?>)</b></p>
        <ul style="color:#ddd;margin:0 0 12px;padding-left:20px"><?php echo $addedHtml; ?></ul>
        <?php endif; ?>
        <?php if ($removedHtml): ?>
        <p style="color:#f44336;margin-bottom:4px"><b>🗑️ Usunięte (<?php echo count($removed); ?>)</b></p>
        <ul style="color:#ddd;margin:0 0 12px;padding-left:20px"><?php echo $removedHtml; ?></ul>
        <?php endif; ?>
    `;
    <?php endif; ?>
    document.getElementById('integrityModal').style.display = 'flex';
})();
</script>
<?php endif; ?>

<script>
    // Nowa funkcja do otwierania modala udostępniania
    function showShareModal(fileUrl) {
        const modal = document.getElementById('shareModal');
        const linkInput = document.getElementById('shareLinkInput');
        const qrImage = document.getElementById('qrCodeImage');

        linkInput.value = fileUrl;

        // Generowanie kodu QR za pomocą zewnętrznego API
        const qrApiUrl = `https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=${encodeURIComponent(fileUrl)}`;
        qrImage.src = qrApiUrl;

        modal.style.display = 'flex';
    }

    function closeModal(id) {
        const el = document.getElementById(id);
        if (el) el.style.display = 'none';
    }
    function closeShareModal() { closeModal('shareModal'); }

    // --- ZIP build (persistent cache) ---
    let _zipES = null;
    function startZipBuild(dir) {
        const modal  = document.getElementById('zipModal');
        const bar    = document.getElementById('zipBar');
        const status = document.getElementById('zipStatus');
        const fileEl = document.getElementById('zipFile');
        document.getElementById('zipModalTitle').textContent = '📦 Tworzenie archiwum ZIP';
        bar.style.width      = '0%';
        bar.style.background = 'linear-gradient(90deg,#4a90e2,#7bc8ff)';
        status.textContent   = 'Przygotowywanie...';
        fileEl.textContent   = '';
        modal.style.display  = 'flex';
        if (_zipES) { _zipES.close(); _zipES = null; }
        _zipES = new EventSource('?action=zip_build&dir=' + encodeURIComponent(dir));
        _zipES.onmessage = function(e) {
            const d   = JSON.parse(e.data);
            const pct = d.total > 0 ? Math.round(d.done / d.total * 100) : 0;
            bar.style.width    = pct + '%';
            status.textContent = d.done + ' / ' + d.total + ' plików — ' + pct + '%';
            fileEl.textContent = d.file || '';
        };
        _zipES.addEventListener('done', function(e) {
            _zipES.close(); _zipES = null;
            const d = JSON.parse(e.data);
            bar.style.width    = '100%';
            status.textContent = '✅ Zapisano! ' + d.count + ' plików. Odświeżanie...';
            fileEl.textContent = '';
            setTimeout(() => { location.reload(); }, 1200);
        });
        _zipES.addEventListener('ziperr', function(e) {
            _zipES.close(); _zipES = null;
            bar.style.background = '#c0392b';
            try { status.textContent = '❌ ' + JSON.parse(e.data).msg; }
            catch(ex) { status.textContent = '❌ Błąd serwera'; }
            fileEl.textContent = '';
        });
        _zipES.onerror = function() {
            if (!_zipES || _zipES.readyState === EventSource.CLOSED) return;
            _zipES.close(); _zipES = null;
            bar.style.background = '#c0392b';
            status.textContent   = '❌ Błąd połączenia (sprawdź logi PHP)';
            fileEl.textContent   = '';
        };
    }
    function closeZipModal() {
        if (_zipES) { _zipES.close(); _zipES = null; }
        document.getElementById('zipModal').style.display = 'none';
    }

    function startIntegrityBuild(dir) {
        const recursive = document.getElementById('snapshotRecursive')?.checked ? '1' : '0';
        const modal  = document.getElementById('zipModal');
        const bar    = document.getElementById('zipBar');
        const status = document.getElementById('zipStatus');
        const fileEl = document.getElementById('zipFile');
        document.getElementById('zipModalTitle').textContent = '📸 Tworzenie snapshotu integralności';
        bar.style.width      = '0%';
        bar.style.background = 'linear-gradient(90deg,#e2844a,#ffc87b)';
        status.textContent   = 'Skanowanie plików...';
        fileEl.textContent   = '';
        modal.style.display  = 'flex';
        if (_zipES) { _zipES.close(); _zipES = null; }
        _zipES = new EventSource('?action=integrity_build&dir=' + encodeURIComponent(dir) + '&recursive=' + recursive);
        _zipES.onmessage = function(e) {
            const d   = JSON.parse(e.data);
            const pct = d.total > 0 ? Math.round(d.done / d.total * 100) : 0;
            bar.style.width    = pct + '%';
            status.textContent = d.done + ' / ' + d.total + ' plików — ' + pct + '%';
            fileEl.textContent = d.file || '';
        };
        _zipES.addEventListener('done', function(e) {
            _zipES.close(); _zipES = null;
            const d = JSON.parse(e.data);
            bar.style.width    = '100%';
            status.textContent = '✅ Snapshot zapisany (' + d.count + ' plików). Odświeżanie...';
            fileEl.textContent = '';
            setTimeout(() => location.reload(), 1200);
        });
        _zipES.addEventListener('ziperr', function(e) {
            _zipES.close(); _zipES = null;
            bar.style.background = '#c0392b';
            try { status.textContent = '❌ ' + JSON.parse(e.data).msg; }
            catch(ex) { status.textContent = '❌ Błąd serwera'; }
            fileEl.textContent = '';
        });
        _zipES.onerror = function() {
            if (!_zipES || _zipES.readyState === EventSource.CLOSED) return;
            _zipES.close(); _zipES = null;
            bar.style.background = '#c0392b';
            status.textContent   = '❌ Błąd połączenia (sprawdź logi PHP)';
            fileEl.textContent   = '';
        };
    }

    // --- Lazy loading rozmiarów katalogów ---
    (function() {
        const cells = Array.from(document.querySelectorAll('.dir-size-cell'));
        if (!cells.length) return;
        let i = 0;
        function loadNext() {
            if (i >= cells.length) return;
            const cell = cells[i++];
            const dir  = cell.dataset.dir || '';
            fetch('?action=dir_size&dir=' + encodeURIComponent(dir))
                .then(r => r.json())
                .then(d => { cell.textContent = d.f || '—'; })
                .catch(() => { cell.textContent = '—'; })
                .finally(() => setTimeout(loadNext, 80));
        }
        // Poczekaj aż strona będzie gotowa, potem zacznij ładować
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', loadNext);
        } else {
            loadNext();
        }
    })();

    // Nowa funkcja do kopiowania linku
    function copyShareLink() {
        const linkInput = document.getElementById('shareLinkInput');
        linkInput.select();
        document.execCommand('copy');
        alert('Link skopiowany do schowka!');
    }

    // Obsługa kliknięcia przycisku Udostępnij
    document.addEventListener('click', (event) => {
        if (event.target.classList.contains('share-btn')) {
            const fileUrl = event.target.getAttribute('data-file-url');
            if (fileUrl) {
                showShareModal(fileUrl);
            }
        }
    });

    // Zamykanie modala po kliknięciu poza nim
    window.addEventListener('click', (event) => {
        const shareModal = document.getElementById('shareModal');
        if (event.target === shareModal) {
            closeShareModal();
        }
    });

    // Upewnij się, że inne modale też mają funkcje zamykania
    // ... tutaj ewentualnie inne funkcje zamykania modali, które już masz
</script>

<script>
    const previewModal     = document.getElementById('previewModal');
    const previewContent   = document.getElementById('previewContent');
    const videoModal       = document.getElementById('videoModal');
    const videoPlayer      = document.getElementById('videoPlayer');

    // ---- Gallery fullscreen grid + lightbox ----
    (function () {
        const imgs = window._galleryImages;
        if (!imgs || !imgs.length) return;

        const grid      = document.getElementById('galleryGrid');
        const lb        = document.getElementById('galleryLightbox');
        const lbBg      = document.getElementById('galleryLbBg');
        const lbImgWrap = document.getElementById('galleryLbImgWrap');
        const lbImg     = document.getElementById('galleryLbImg');
        const lbBar     = document.getElementById('galleryLbBar');
        const lbCount   = document.getElementById('galleryLbCount');
        const lbDl      = document.getElementById('galleryLbDownload');
        const lbClose   = document.getElementById('galleryLbClose');
        const hsPrev    = document.getElementById('galleryLbHsPrev');
        const hsNext    = document.getElementById('galleryLbHsNext');
        const lbBarPrev = document.getElementById('galleryLbBarPrev');
        const lbBarNext = document.getElementById('galleryLbBarNext');

        if (!grid || !lb) return;

        let currentIdx      = 0;
        let isOpen          = false;
        let isTransitioning = false;
        const tiles         = [];

        // Buduj kafelki
        imgs.forEach(function (img, i) {
            const tile = document.createElement('div');
            tile.className    = 'gallery-tile';
            tile._thumbSrc    = img.thumb;
            tile._loaded      = false;
            tile.addEventListener('click', function () { openLightbox(i, tile); });
            grid.appendChild(tile);
            tiles.push(tile);
        });

        // Lazy loading przy scrollu
        function loadVisible() {
            for (let i = 0; i < tiles.length; i++) {
                const t = tiles[i];
                if (t._loaded) continue;
                const rect = t.getBoundingClientRect();
                if (rect.top < window.innerHeight + 400) {
                    t.style.backgroundImage = 'url(' + JSON.stringify(t._thumbSrc) + ')';
                    t._loaded = true;
                } else {
                    break;
                }
            }
        }

        window.addEventListener('scroll', loadVisible, { passive: true });
        window.addEventListener('resize', loadVisible, { passive: true });
        loadVisible();

        // Lightbox
        function openLightbox(idx, tileEl) {
            if (isTransitioning) return;
            isTransitioning = true;
            currentIdx = idx;
            isOpen     = true;

            lb.style.display        = 'block';
            lbImgWrap.style.opacity = '0';
            lbBar.style.opacity     = '0';
            lbClose.style.opacity   = '0';
            document.body.style.overflow = 'hidden';

            // Animacja tła z pozycji kafelka
            const rect = tileEl.getBoundingClientRect();
            lbBg.style.transition = 'none';
            Object.assign(lbBg.style, {
                left: rect.left + 'px', top: rect.top + 'px',
                width: rect.width + 'px', height: rect.height + 'px'
            });

            // Wymuszamy reflow przed animacją
            lbBg.offsetHeight;

            lbBg.style.transition = 'left .25s ease-out, top .25s ease-out, width .25s ease-out, height .25s ease-out';
            Object.assign(lbBg.style, { left: '0', top: '0', width: '100%', height: '100%' });

            setTimeout(function () {
                lbBg.style.transition = '';
                showImage(idx, true);
                lbBar.style.opacity   = '1';
                lbClose.style.opacity = '1';
                isTransitioning = false;
            }, 270);

            document.addEventListener('keydown', onKey);
        }

        function closeLightbox() {
            if (!isOpen) return;
            isOpen = false;
            lb.style.display = 'none';
            lbImg.src        = '';
            document.body.style.overflow = '';
            document.removeEventListener('keydown', onKey);
            // Resetuj tło na następne otwarcie
            Object.assign(lbBg.style, { transition: '', left: '', top: '', width: '', height: '' });
        }

        function showImage(idx, snap) {
            if (idx < 0) idx = imgs.length - 1;
            if (idx >= imgs.length) idx = 0;
            currentIdx = idx;

            const img = imgs[idx];
            lbCount.textContent = (idx + 1) + ' / ' + imgs.length;
            lbDl.href           = img.url;
            lbDl.download       = img.name;

            if (snap) {
                lbImg.src               = img.url;
                lbImgWrap.style.opacity = '1';
            } else {
                lbImgWrap.style.opacity = '0';
                lbImg.src = img.url;
                lbImg.onload = function () {
                    lbImgWrap.style.transition = 'opacity .25s';
                    lbImgWrap.style.opacity    = '1';
                };
            }
        }

        function onKey(e) {
            if (e.key === 'ArrowRight' || e.key === 'ArrowLeft' || e.key === 'Escape') {
                e.preventDefault();
            }
            if (e.key === 'ArrowRight') showImage(currentIdx + 1, false);
            if (e.key === 'ArrowLeft')  showImage(currentIdx - 1, false);
            if (e.key === 'Escape')     closeLightbox();
        }

        lbClose.addEventListener('click', closeLightbox);
        hsPrev.addEventListener('click', function () { showImage(currentIdx - 1, false); });
        hsNext.addEventListener('click', function () { showImage(currentIdx + 1, false); });
        lbBarPrev.addEventListener('click', function () { showImage(currentIdx - 1, false); });
        lbBarNext.addEventListener('click', function () { showImage(currentIdx + 1, false); });
    })();

    document.addEventListener('keydown', (event) => {
        if (videoMainPlayer && videoItems.length > 0) {
            if (event.key === 'ArrowLeft')  { setVideoFile(videoIndex - 1); }
            if (event.key === 'ArrowRight') { setVideoFile(videoIndex + 1); }
        }
    });

        const videoMainPlayer = document.getElementById('videoMainPlayer');
    const videoMainSource = document.getElementById('videoMainSource');
    const videoCaption    = document.getElementById('videoCaption');
    const videoPrev       = document.getElementById('videoPrev');
    const videoNext       = document.getElementById('videoNext');
    const videoDownload   = document.getElementById('videoDownload');
    const videoShare      = document.getElementById('videoShare');
   
    const videoItems      = Array.from(document.querySelectorAll('.video-item'));
    let videoIndex        = 0;

    function setVideoFile(index) {
        if (!videoMainPlayer || !videoMainSource || videoItems.length === 0) {
            return;
        }

        if (index < 0) {
            index = videoItems.length - 1;
        }

        if (index >= videoItems.length) {
            index = 0;
        }

        videoIndex = index;

        const item = videoItems[videoIndex];
        const fileUrl = item.getAttribute('data-file');
        const fileName = item.getAttribute('data-name');

        videoMainSource.setAttribute('src', fileUrl);
        videoMainPlayer.load();

        if (videoCaption) {
            videoCaption.textContent = fileName + ' — ' + (videoIndex + 1) + ' / ' + videoItems.length;
        }

        if (videoDownload) {
            videoDownload.setAttribute('href', fileUrl);
            videoDownload.setAttribute('download', fileName);
        }

        if (videoShare) {
            videoShare.setAttribute('data-file-url', fileUrl);
            videoShare.setAttribute('data-name', fileName);
        }


        videoItems.forEach(itemNode => itemNode.classList.remove('active'));
        item.classList.add('active');
    }

    document.getElementById('videoItems')?.addEventListener('click', e => {
        const item = e.target.closest('.video-item');
        if (item) setVideoFile(videoItems.indexOf(item));
    });

    if (videoPrev) {
        videoPrev.addEventListener('click', () => {
            setVideoFile(videoIndex - 1);
        });
    }

    if (videoNext) {
        videoNext.addEventListener('click', () => {
            setVideoFile(videoIndex + 1);
        });
    }



    document.querySelectorAll('.preview-btn').forEach(button => {
        button.addEventListener('click', () => {
            const fileUrl  = button.getAttribute('data-file');
            const fileType = button.getAttribute('data-type').toLowerCase();

            if (['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(fileType))
            {
                previewContent.innerHTML = `<img src="${fileUrl}" alt="Image Preview" style="max-width:90vw;max-height:90vh;width:auto;height:auto;object-fit:contain;">`;
                previewModal.style.display = 'flex';
            }
            else
            {
                previewContent.innerHTML = `<p>Preview not available for this file type.</p>`;
                previewModal.style.display = 'flex';
            }
        });
    });

  document.addEventListener('click', (e) => {
  const btn = e.target.closest('.watch-btn');
  if (!btn) return;

  const videoUrl = btn.getAttribute('data-file');
  if (!videoUrl) return;

  videoModal.style.display = 'flex';
  videoPlayer.src = videoUrl;
  videoPlayer.load();
  videoPlayer.play().catch(()=>{});
});

    function closeAllModals() {
        closeModal('previewModal');
        previewContent.innerHTML = '';

        closeModal('videoModal');
        videoPlayer.pause();
        videoPlayer.removeAttribute('src');
        videoPlayer.load();
        videoPlayer.currentTime = 0;
    }

    // Dodaj to do zamykania obu modali, kiedy klikniesz poza ich obszarem
    window.addEventListener('click', (event) => {
        if (event.target === previewModal || event.target === videoModal) {
            closeAllModals();
        }
    });
</script>

<footer class="footer">
    Wersja oprogramowania: <?php echo SOFTWARE_VERSION; ?>
</footer>


<script>
  const HAS_MEDIA      = <?php echo $hasMedia ? 'true' : 'false'; ?>;
  const CURRENT_DIR_REL= <?php echo json_encode($currentRel); ?>;   // np. "muzyka/rock"
  const CURRENT_DIR_KEY= <?php echo json_encode($dirKeyB64); ?>;    // base64(CURRENT_DIR_REL)
</script>

<script>
  // te 3 zmienne już masz z PHP:
  // const CURRENT_DIR_REL = "...";
  // const CURRENT_DIR_KEY = "...";  // base64(rel)
  // (HAS_MEDIA nie jest już potrzebne do uruchomienia playera)

  (function() {
    let initialized = false;
    let queue = [];     // [{title, url}]
    let index = -1;
    let shuffle = false;

    const MAX_QUEUE_ITEMS = 200;
    const COOKIE_QUEUE   = 'mp3_queue_v1_'   + CURRENT_DIR_KEY;
    const COOKIE_SHUFFLE = 'mp3_shuffle_v1_' + CURRENT_DIR_KEY;

    // --- DOM (pobieramy leniwie w init) ---
    let bar, audio, titleEl, btnPrev, btnNext, btnShuffle, btnClear, btnTogglePl, panelPl, panelClose, listEl;
    let abArt, abPlayPause, abBar, abFill, abCurrent, abDuration, abVolume;
    let abSeeking = false;

    // --- helpers cookie ---
    function setCookie(name, value, days) {
      const d = new Date(); d.setTime(d.getTime() + (days*24*60*60*1000));
      document.cookie = name + "=" + value + ";expires="+ d.toUTCString() + ";path=/;SameSite=Lax";
    }

// ——— zaznacz „row-playing” w tabelach i listach po URL ———
function highlightCurrentByUrl(url){
  // 1) zdejmij poprzednie zaznaczenia
  document.querySelectorAll('tr.row-playing').forEach(el => el.classList.remove('row-playing'));
  document.querySelectorAll('#playlistList li.playing').forEach(el => el.classList.remove('playing'));

  if(!url) return;

  // 2) znajdź wszystkie przyciski odpowiadające temu URL (główna tabela + wyniki wyszukiwania + live)
  const btns = document.querySelectorAll(
    `.audio-play-btn[data-audio-url="${CSS.escape(url)}"], 
     .audio-add-btn[data-audio-url="${CSS.escape(url)}"]`
  );

  // 3) dodaj klasę tr.row-playing
  btns.forEach(b => {
    const row = b.closest('tr');
    if (row) row.classList.add('row-playing');
  });

  // 4) w kolejce (panel) – zaznacz odpowiadający element
  if (listEl && queue.length){
    // znajdź index bieżącego w kolejce po URL
    const curIdx = queue.findIndex(t => t.url === url);
    const li = listEl.children[curIdx];
    if (li) li.classList.add('playing');
  }
}

// ——— przewinięcie do pierwszego widocznego „row-playing” ———
function scrollCurrentIntoView(){
  // priorytet: jeśli otwarty panel kolejki – przewiń w nim
  if (panelPl && panelPl.style.display === 'flex' && listEl){
    const li = listEl.querySelector('li.playing');
    if (li) { li.scrollIntoView({block:'center', behavior:'smooth'}); return; }
  }

  // potem: główna tabela/wyniki statyczne
  const mainRow = document.querySelector('table tbody tr.row-playing');
  if (mainRow) { mainRow.scrollIntoView({block:'center', behavior:'smooth'}); return; }

  // na końcu: „live results”
  const live = document.getElementById('liveResults');
  if (live){
    const liveRow = live.querySelector('tbody tr.row-playing');
    if (liveRow) liveRow.scrollIntoView({block:'center', behavior:'smooth'});
  }
}

    function getCookie(name) {
      const cname = name + "=";
      const ca = document.cookie.split(';');
      for (let c of ca) {
        while (c.charAt(0)===' ') c=c.substring(1);
        if (c.indexOf(cname)===0) return c.substring(cname.length);
      }
      return "";
    }

    // --- sprawdzanie, czy url jest z tego katalogu (lub podkatalogu) ---
    function sameDirectory(url) {
      try {
        const u = new URL(url, window.location.href);
        const b64 = u.searchParams.get('dir') || '';
        const b64dec = decodeURIComponent(b64);

        if (b64dec === CURRENT_DIR_KEY) return true; // dokładnie ten sam katalog

        let rel = '';
        try { rel = atob(b64dec); } catch(e) {}
        if (!rel) return false;

        const norm = s => (('/' + s).replace(/\/+/g,'/').replace(/\/$/, '')) + '/';
        const cur  = norm(CURRENT_DIR_REL);
        const tgt  = norm(rel);
        return tgt === cur || tgt.startsWith(cur); // ten sam lub podkatalog
      } catch(e) {
        return false;
      }
    }

    // --- helpers format ---
    function abFmt(t) {
      if (!isFinite(t)) return '–:––';
      return Math.floor(t/60) + ':' + String(Math.floor(t%60)).padStart(2,'0');
    }

    // --- render/UI ---
    function updateTitle() {
      const title = (index>=0 && queue[index]) ? queue[index].title : '– brak utworu –';
      if (titleEl) titleEl.textContent = title;
      // sync view=audio title if open
      const apTitle = document.getElementById('apTitle');
      if (apTitle) apTitle.textContent = title;
    }
    function renderPlaylist() {
      if (!listEl) return;
      listEl.innerHTML = '';
      queue.forEach((t, i) => {
        const li = document.createElement('li');
        const left = document.createElement('div');
        left.className = 'track-title';
        left.textContent = (i===index ? '▶ ' : '') + t.title;

        const actions = document.createElement('div');
        actions.className = 'track-actions';

        const btnPlay = document.createElement('button');
        btnPlay.className = 'btn';
        btnPlay.textContent = '►';
        btnPlay.addEventListener('click', () => playIndex(i));

        const btnDel = document.createElement('button');
        btnDel.className = 'btn';
        btnDel.textContent = '🗑️';
        btnDel.addEventListener('click', () => removeAt(i));

        actions.appendChild(btnPlay);
        actions.appendChild(btnDel);

        li.appendChild(left);
        li.appendChild(actions);

        if (i === index) li.classList.add('playing');

        listEl.appendChild(li);
      });
    }
    function openPlaylist(){ if (panelPl) { panelPl.style.display='flex'; renderPlaylist(); } }
    function closePlaylist(){ closeModal('playlistPanel'); }

    // --- player controls ---
    function playIndex(i) {
      if (!queue.length) return;
      if (i < 0) i = 0;
      if (i >= queue.length) i = 0;
      index = i;
      audio.src = queue[index].url;
      updateTitle();
      renderPlaylist();

      highlightCurrentByUrl(queue[index].url);
      scrollCurrentIntoView();

      audio.play().catch(()=>{});
    }
    function getShuffledIndex() {
      let ni = Math.floor(Math.random()*queue.length);
      if (queue.length>1) while (ni===index) ni = Math.floor(Math.random()*queue.length);
      return ni;
    }
    function nextTrack() {
      if (!queue.length) return;
      playIndex(shuffle ? getShuffledIndex() : index+1);
    }
    function prevTrack() {
      if (!queue.length) return;
      playIndex(shuffle ? getShuffledIndex() : (index-1<0 ? queue.length-1 : index-1));
    }

    function toggleShuffle(){
      shuffle = !shuffle;
      if(btnShuffle){
        btnShuffle.textContent = '🔀 Losowo: ' + (shuffle ? 'ON' : 'OFF');
        btnShuffle.classList.toggle('active', shuffle); // <-- ZIELONY
      }
      setCookie(COOKIE_SHUFFLE, shuffle ? '1' : '0', 30);
    }

    function clearQueue(){
      queue=[]; index=-1; setCookie(COOKIE_QUEUE, '', -1); updateTitle(); renderPlaylist();
      audio.pause(); audio.removeAttribute('src'); audio.load();
    }
    function removeAt(i){
      if (i<0 || i>=queue.length) return;
      const wasCurrent = (i === index);
      queue.splice(i,1);
      if (wasCurrent) {
        if (i >= queue.length) index = queue.length - 1;
        if (index >= 0) playIndex(index);
        else { index=-1; updateTitle(); audio.pause(); audio.removeAttribute('src'); audio.load(); }
      } else {
        if (i < index) index--;
        updateTitle();
      }
      try {
        const json = JSON.stringify(queue);
        const b64  = btoa(unescape(encodeURIComponent(json)));
        setCookie(COOKIE_QUEUE, b64, 30);
      } catch(e){}
      renderPlaylist();
    }

    function addToQueue(track) {
      if (!sameDirectory(track.url)) {
        alert('Ten utwór pochodzi z innego katalogu. Kolejka dotyczy tylko bieżącej lokalizacji.');
        return;
      }
      if (!queue.some(t => t.url === track.url)) {
        queue.push(track);
        if (queue.length > MAX_QUEUE_ITEMS) queue.shift();
        try {
          const json = JSON.stringify(queue);
          const b64  = btoa(unescape(encodeURIComponent(json)));
          setCookie(COOKIE_QUEUE, b64, 30);
        } catch(e){}
        renderPlaylist();
      }
    }
    function playNow(track) {
      if (!sameDirectory(track.url)) {
        alert('Ten utwór pochodzi z innego katalogu. Kolejka dotyczy tylko bieżącej lokalizacji.');
        return;
      }
      const i = queue.findIndex(t => t.url === track.url);
      if (i === -1) {
        addToQueue(track);
        playIndex(queue.length - 1);
      } else {
        playIndex(i);
      }
    }

    // --- leniwa inicjalizacja ---
    function initPlayerIfNeeded() {
      if (initialized) return;
      const inAudioView = !!document.getElementById('apViewer');

      // 1) pobierz DOM
      bar        = document.getElementById('audioPlayerBar');
      audio      = document.getElementById('audioPlayer');
      titleEl    = document.getElementById('audioTitle');
      btnPrev    = document.getElementById('audioPrev');
      btnNext    = document.getElementById('audioNext');
      btnShuffle = document.getElementById('audioShuffle');
      btnClear   = document.getElementById('audioClear');
      btnTogglePl= document.getElementById('togglePlaylist');
      panelPl    = document.getElementById('playlistPanel');
      panelClose = document.getElementById('closePlaylist');
      listEl     = document.getElementById('playlistList');
      abArt      = document.getElementById('abArt');
      abPlayPause= document.getElementById('audioPlayPause');
      abBar      = document.getElementById('abBar');
      abFill     = document.getElementById('abFill');
      abCurrent  = document.getElementById('abCurrent');
      abDuration = document.getElementById('abDuration');
      abVolume   = document.getElementById('abVolume');

      if (!audio) return; // audio element jest wymagany; reszta opcjonalna

      // 2) pokaż pasek — tylko gdy nie jesteśmy w view=audio
      if (!inAudioView && bar) bar.style.display = 'flex';

      // 3) kontrolki podstawowe
      if (btnNext)    btnNext.addEventListener('click', nextTrack);
      if (btnPrev)    btnPrev.addEventListener('click', prevTrack);
      if (btnShuffle) btnShuffle.addEventListener('click', toggleShuffle);
      if (btnClear)   btnClear.addEventListener('click', clearQueue);
      audio.addEventListener('ended', nextTrack);
      if (btnTogglePl) btnTogglePl.addEventListener('click', openPlaylist);
      if (panelClose)  panelClose.addEventListener('click', closePlaylist);
      window.addEventListener('click', (e) => { if (e.target === panelPl) closePlaylist(); });

      // 4) nowy play/pause button
      if (abPlayPause) abPlayPause.addEventListener('click', () => {
        audio.paused ? audio.play() : audio.pause();
      });

      // 5) progress bar
      audio.addEventListener('play',  () => {
        if (abPlayPause) abPlayPause.textContent = '⏸';
        if (abArt) abArt.classList.add('playing');
        syncApView('play');
      });
      audio.addEventListener('pause', () => {
        if (abPlayPause) abPlayPause.textContent = '▶';
        if (abArt) abArt.classList.remove('playing');
        syncApView('pause');
      });
      audio.addEventListener('loadedmetadata', () => {
        if (abDuration) abDuration.textContent = abFmt(audio.duration);
        syncApView('meta');
      });
      audio.addEventListener('timeupdate', () => {
        if (abSeeking || !audio.duration) return;
        const pct = audio.currentTime / audio.duration * 100;
        if (abFill)    abFill.style.width = pct + '%';
        if (abCurrent) abCurrent.textContent = abFmt(audio.currentTime);
        syncApView('time');
      });

      if (abBar) {
        const abSeek = (e) => {
          const r = abBar.getBoundingClientRect();
          const p = Math.max(0, Math.min(1, (e.clientX - r.left) / r.width));
          if (audio.duration) audio.currentTime = p * audio.duration;
          if (abFill) abFill.style.width = (p*100) + '%';
        };
        abBar.addEventListener('mousedown',  (e) => { abSeeking = true; abSeek(e); });
        document.addEventListener('mousemove',(e) => { if (abSeeking) abSeek(e); });
        document.addEventListener('mouseup',  ()  => { abSeeking = false; });
        abBar.addEventListener('touchstart', (e) => { abSeeking = true; abSeek(e.touches[0]); }, {passive:true});
        document.addEventListener('touchmove',(e) => { if (abSeeking) abSeek(e.touches[0]); }, {passive:true});
        document.addEventListener('touchend', ()  => { abSeeking = false; });
      }

      // 6) volume
      if (abVolume) abVolume.addEventListener('input', () => { audio.volume = abVolume.value; });

      // 7) ciasteczka
      try {
        const b64 = getCookie(COOKIE_QUEUE);
        if (b64) {
          const json = decodeURIComponent(escape(atob(b64)));
          const arr  = JSON.parse(json);
          if (Array.isArray(arr)) queue = arr.filter(x => x && x.url && x.title);
        }
      } catch(e){}
      shuffle = (getCookie(COOKIE_SHUFFLE) === '1');
      if (btnShuffle) btnShuffle.classList.toggle('active', shuffle);

      updateTitle();
      renderPlaylist();
      initialized = true;
    }

    // --- sync stanu z view=audio (ap-*) gdy otwarte ---
    function syncApView(event) {
      const apViewer = document.getElementById('apViewer');
      if (!apViewer) return;
      const apFillEl = document.getElementById('apFill');
      const apCurEl  = document.getElementById('apCurrent');
      const apDurEl  = document.getElementById('apDuration');
      const apArtEl  = document.getElementById('apArt');
      const apPlayEl = document.getElementById('apPlay');
      if (event === 'time' && !apViewer._seeking) {
        const pct = audio.duration ? audio.currentTime / audio.duration * 100 : 0;
        if (apFillEl) apFillEl.style.width = pct + '%';
        if (apCurEl)  apCurEl.textContent  = abFmt(audio.currentTime);
      }
      if (event === 'meta' && apDurEl) apDurEl.textContent = abFmt(audio.duration);
      if (event === 'play')  { if (apArtEl) apArtEl.classList.add('playing');    if (apPlayEl) apPlayEl.textContent = '⏸'; }
      if (event === 'pause') { if (apArtEl) apArtEl.classList.remove('playing'); if (apPlayEl) apPlayEl.textContent = '▶'; }
      // highlight active track in sidebar
      const url = audio.src;
      document.querySelectorAll('.ap-track').forEach(btn => {
        btn.classList.toggle('active', btn.dataset.audioUrl === url || btn.dataset.audioUrl === decodeURIComponent(url));
        btn.classList.remove('paused');
      });
      if (event === 'pause') document.querySelectorAll('.ap-track.active').forEach(b => b.classList.add('paused'));
    }

    // --- globalna delegacja klików tylko dla przycisków audio ---
    document.addEventListener('click', (e) => {
      const btn = e.target.closest('.audio-play-btn,.audio-add-btn');
      if (!btn) return;
      initPlayerIfNeeded();
      const url   = btn.getAttribute('data-audio-url');
      const title = btn.getAttribute('data-audio-title') || url;
      if (!url) return;
      const track = {title, url};
      if (btn.classList.contains('audio-add-btn')) addToQueue(track);
      else playNow(track);
    });

    // --- eksportuj API dla view=audio ---
    window.AP = {
      playNow,
      addToQueue,
      nextTrack,
      prevTrack,
      playAll(tracks) {
        initPlayerIfNeeded();
        if (!tracks.length) return;
        tracks.forEach(t => addToQueue(t));
        playNow(tracks[0]);
      },
      getAudio() { return audio; },
      init: initPlayerIfNeeded,
      openPlaylist() { initPlayerIfNeeded(); openPlaylist(); },
    };
  })();
</script>

<script>
(function(){
  // ——— pomocnicze: debounce ———
  function debounce(fn, ms){
    let t;
    return function(...args){
      clearTimeout(t);
      t = setTimeout(() => fn.apply(this, args), ms);
    };
  }

  // ——— uchwyty do DOM ———
  const form  = document.getElementById('searchForm');
  const input = document.getElementById('searchInput');
  const live  = document.getElementById('liveResults');
  if(!form || !input || !live) return;

  // aktualny katalog (base64) z hidden inputa
  const dirInput = form.querySelector('input[name="dir"]');
  const dirB64   = dirInput ? dirInput.value : '';

    function scrollToLiveResults() {
    const liveEl = document.getElementById('liveResults');
    const sticky = document.querySelector('.sticky-search');
    const player = document.getElementById('audioPlayerBar');

    if (!liveEl) return;

    // Oblicz bezpieczny offset (wysokość sticky search + player, jeśli widoczny)
    const stickyH = sticky ? sticky.offsetHeight : 0;
    const playerH = (player && player.style.display !== 'none') ? player.offsetHeight : 0;
    const offset  = stickyH + playerH + 10; // +10px luzu

    // docelowa pozycja przewinięcia:
    const top = liveEl.getBoundingClientRect().top + window.pageYOffset - offset;

    // Jeżeli #liveResults już jest w kadrze, nie przewijaj agresywnie
    const rect = liveEl.getBoundingClientRect();
    const inView = rect.top >= 0 && rect.top < (window.innerHeight - offset);
    if (inView) return;

    window.scrollTo({ top, behavior: 'smooth' });
  }

    // Pomocnicze sterowanie panelem wyników
  function showLive() {
    const liveEl = document.getElementById('liveResults');
    if (!liveEl) return;
    // restart animacji przy każdym odświeżeniu
    liveEl.classList.remove('is-visible');
    // wymuś reflow aby animation się zresetowała
    void liveEl.offsetWidth;
    liveEl.classList.add('is-visible');
  }
  function hideLive() {
    const liveEl = document.getElementById('liveResults');
    if (!liveEl) return;
    liveEl.classList.remove('is-visible'); // display:none wraca z CSS bazowego
    liveEl.innerHTML = '';                 // czyść zawartość
  }

  // ——— helper: escape HTML (zabezpiecza przed XSS) ———
  function escHtml(s) {
    return String(s)
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&#39;');
  }

  // ——— rysowanie wyników ———
  function renderResults(data){
  if(!data || !Array.isArray(data.items) || data.items.length === 0){
    live.innerHTML = '<div style="color:#888;">Brak wyników…</div>';
    showLive();
    scrollToLiveResults();
    return;
  }

  const rows = data.items.map(it=>{
    const sizeMB = (it.size ? Math.round((it.size/1048576)*100)/100 : 0).toFixed(2);
    const name   = it.name || '';
    const relDir = it.rel_dir || '';
    const dlHref = `${location.pathname}?dir=${encodeURIComponent(relDir)}&action=download&file=${encodeURIComponent(name)}`;

    let actions = '';

    // AUDIO: play + add
    if (it.audio_url) {
      actions += '<button class="btn btn-sm audio-play-btn"'
              +  ' data-audio-url="'+escHtml(it.audio_url)+'"'
              +  ' data-audio-title="'+escHtml(name)+'">▶︎ Odtwórz</button> ';

      actions += '<button class="btn btn-sm audio-add-btn"'
              +  ' data-audio-url="'+escHtml(it.audio_url)+'"'
              +  ' data-audio-title="'+escHtml(name)+'">➕ Do kolejki</button> ';
    }

    // VIDEO: watch + share
    if (it.video_url) {
      actions += '<button class="btn btn-sm watch-btn" data-file="'+escHtml(it.video_url)+'">🎬 Odtwórz</button> ';
      actions += '<button class="btn btn-sm share-btn" data-file-url="'+escHtml(it.video_url)+'">🔗 Udostępnij</button> ';
    }

    // DOWNLOAD: zawsze gdy mamy nazwę/rel_dir
    if (name.toLowerCase() !== 'index.html') {
      actions += `<a class="btn btn-sm" data-action="download" href="${escHtml(dlHref)}">⬇️ Download</a>`;
    }

    return '<tr>'
      + '<td style="width:26px">'+(it.icon||'')+'</td>'
      + '<td>'+escHtml(name)+'</td>'
      + '<td style="width:90px">'+sizeMB+' MB</td>'
      + '<td class="actions" style="width:260px;text-align:right;">'+actions+'</td>'
      + '</tr>';
  }).join('');

  live.innerHTML =
    '<div style="margin:4px 0 8px 0; color:#aaa;">Wyniki (live): '+(data.count||data.items.length)+'</div>'
    + '<table>'
    + '<thead><tr><th></th><th>Nazwa</th><th>Rozmiar</th><th style="text-align:right;">Akcje</th></tr></thead>'
    + '<tbody>'+rows+'</tbody>'
    + '</table>';

    showLive();              // pokaż z animacją
    scrollToLiveResults(); 

  // Uwaga: nie podpinać tu listenerów dla audio — działa delegacja z kodu playera
}

  // ——— wywołanie API ———
  const doSearch = debounce(function(q){
    // minimalna długość
    if(q.trim().length < 2){
      hideLive();
      return;
    }
    // budujemy URL do API (zachowujemy katalog przez ?dir=…)
    const url = `${location.pathname}?api=1&dir=${encodeURIComponent(dirB64)}&q=${encodeURIComponent(q)}&recursive=1`;
    live.innerHTML = '<div style="color:#888;">Szukam…</div>';
    showLive();                  // pokaż od razu "Szukam…"
    scrollToLiveResults();       // przesuń, by było widoczne

    fetch(url)
      .then(r => r.json())
      .then(renderResults)
      .catch(()=> live.innerHTML = '<div style="color:#c66;">Błąd podczas wyszukiwania.</div>');
  }, 300);

  // ——— nasłuchiwanie wpisywania ———
  input.addEventListener('input', (e)=> doSearch(e.target.value));

  // ——— przechwytujemy submit (żeby Enter nie przeładowywał strony) ———
  form.addEventListener('submit', (e)=>{
    e.preventDefault();
    doSearch(input.value || '');
  });
})();
</script>



<?php if ($hasMedia): ?>
<div id="audioPlayerBar" style="display:none;">
  <div class="ab-left">
    <div class="ab-art" id="abArt">🎵</div>
    <span class="ab-title" id="audioTitle">– brak utworu –</span>
  </div>
  <div class="ab-center">
    <div class="ab-controls">
      <button class="ab-btn" id="audioShuffle" title="Losowo">🔀</button>
      <button class="ab-btn" id="audioPrev" title="Poprzedni">⏮</button>
      <button class="ab-btn ab-btn-play" id="audioPlayPause" title="Odtwórz">▶</button>
      <button class="ab-btn" id="audioNext" title="Następny">⏭</button>
      <button class="ab-btn" id="audioClear" title="Wyczyść kolejkę">🗑️</button>
      <button class="ab-btn" id="togglePlaylist" title="Kolejka">🎵</button>
    </div>
    <div class="ab-progress-wrap">
      <span class="ab-time" id="abCurrent">0:00</span>
      <div class="ab-progress-bar" id="abBar"><div class="ab-progress-fill" id="abFill"></div></div>
      <span class="ab-time" id="abDuration">–:––</span>
    </div>
  </div>
  <div class="ab-right">
    <input type="range" class="ab-volume" id="abVolume" min="0" max="1" step="0.02" value="1">
  </div>
  <audio id="audioPlayer" preload="metadata"></audio>
</div>

<div id="playlistPanel" class="modal" style="display:none;">
  <div class="modal-content" style="max-width:600px;text-align:left;">
    <button class="modal-close" id="closePlaylist">✖</button>
    <h3>Twoja kolejka</h3>
    <ul id="playlistList" style="list-style:none;padding:0;margin:0;"></ul>
  </div>
</div>
<?php endif; ?>

<script>
/* Double-tap/double-click na wiersz: uruchom główną akcję (audio/video/preview/download) */
(function(){
  // Znajdź „pierwszą sensowną” akcję w wierszu
  function findPrimaryAction(row){
    return row.querySelector('.audio-play-btn') ||
           row.querySelector('.watch-btn')      ||
           row.querySelector('.preview-btn')    ||
           row.querySelector('a.btn[data-action="download"], a.btn[href]');
  }

  // Uruchom akcję (klik przycisku albo przejście do linku)
  function triggerRowAction(row){
    const el = findPrimaryAction(row);
    if(!el) return;
    if (el.tagName === 'A'){
      // Download lub inny link
      const href = el.getAttribute('href');
      if (href) window.location.href = href;
    } else {
      // Button – kliknij programowo
      el.click();
    }
  }

  // Zignoruj, jeśli faktycznie klikamy w przycisk/link w wierszu
  function clickedInteractive(target){
    return !!target.closest('.btn, button, a');
  }

  // Desktop: podwójne kliknięcie
  document.addEventListener('dblclick', function(e){
    const row = e.target.closest('tbody tr, #liveResults tr');
    if (!row) return;
    if (clickedInteractive(e.target)) return;
    triggerRowAction(row);
  });

  // Mobile: double-tap (dwa „touchend” w krótkim czasie na tym samym wierszu)
  let lastTapTime = 0;
  let lastRow = null;
  document.addEventListener('touchend', function(e){
    const row = e.target.closest('tbody tr, #liveResults tr');
    if (!row) return;
    if (clickedInteractive(e.target)) return;

    const now = Date.now();
    if (lastRow === row && (now - lastTapTime) < 350){
      // Drugi tap – potraktuj jako double-tap
      e.preventDefault();  // ogranicza double-tap-zoom na iOS
      triggerRowAction(row);
      lastRow = null;
      lastTapTime = 0;
    } else {
      lastRow = row;
      lastTapTime = now;
      // wyczyść, jeśli drugi tap nie nadejdzie
      setTimeout(function(){
        if (Date.now() - lastTapTime >= 360){
          lastRow = null;
          lastTapTime = 0;
        }
      }, 360);
    }
  }, {passive:false});
})();

/* ---- view=audio — deleguje do window.AP (globalny player) ---- */
(function () {
    const viewer = document.getElementById('apViewer');
    if (!viewer) return;

    const playBtn  = document.getElementById('apPlay');
    const prevBtn  = document.getElementById('apPrev');
    const nextBtn  = document.getElementById('apNext');
    const bar      = document.getElementById('apBar');
    const volInput = document.getElementById('apVolume');

    // Przyciski "Odtwórz wszystkie" / "Dodaj wszystkie"
    const playAllBtn   = document.getElementById('apPlayAll');
    const addAllBtn    = document.getElementById('apAddAll');
    const showQueueBtn = document.getElementById('apShowQueue');

    function getAP() { return window.AP || null; }

    function allTracks() {
        try {
            const urls   = JSON.parse(viewer.dataset.allUrls   || '[]');
            const titles = JSON.parse(viewer.dataset.allTitles || '[]');
            return urls.map((url, i) => ({ url, title: titles[i] || url }));
        } catch(e) { return []; }
    }

    if (playAllBtn) playAllBtn.addEventListener('click', () => {
        const ap = getAP();
        if (ap) ap.playAll(allTracks());
    });

    if (addAllBtn) addAllBtn.addEventListener('click', () => {
        const ap = getAP();
        if (!ap) return;
        allTracks().forEach(t => ap.addToQueue(t));
    });

    if (showQueueBtn) showQueueBtn.addEventListener('click', () => {
        const ap = getAP();
        if (ap) { ap.init(); ap.openPlaylist(); }
    });

    // Kontrolki środkowego panelu sterują globalnym playerem
    if (playBtn) playBtn.addEventListener('click', () => {
        const a = getAP()?.getAudio();
        if (a) a.paused ? a.play() : a.pause();
    });
    if (prevBtn) prevBtn.addEventListener('click', () => getAP()?.prevTrack());
    if (nextBtn) nextBtn.addEventListener('click', () => getAP()?.nextTrack());

    // Seek w ap-bar → globalny audio
    if (bar) {
        const seekTo = (e) => {
            const a = getAP()?.getAudio();
            if (!a || !a.duration) return;
            const r = bar.getBoundingClientRect();
            a.currentTime = Math.max(0, Math.min(1, (e.clientX - r.left) / r.width)) * a.duration;
        };
        let s = false;
        bar.addEventListener('mousedown',  (e) => { s = true; seekTo(e); viewer._seeking = true; });
        document.addEventListener('mousemove', (e) => { if (s) seekTo(e); });
        document.addEventListener('mouseup',   ()  => { s = false; viewer._seeking = false; });
        bar.addEventListener('touchstart', (e) => { s = true; seekTo(e.touches[0]); viewer._seeking = true; }, {passive:true});
        document.addEventListener('touchmove', (e) => { if (s) seekTo(e.touches[0]); }, {passive:true});
        document.addEventListener('touchend',  ()  => { s = false; viewer._seeking = false; });
    }

    // Volume ap → globalny audio
    if (volInput) volInput.addEventListener('input', () => {
        const a = getAP()?.getAudio();
        if (a) a.volume = volInput.value;
    });

    // Klawiatura (Space / strzałki) gdy widok audio aktywny
    document.addEventListener('keydown', (e) => {
        if (['INPUT', 'TEXTAREA', 'SELECT'].includes(e.target.tagName)) return;
        const ap = getAP();
        if (!ap) return;
        if (e.code === 'Space')      { e.preventDefault(); const a = ap.getAudio(); if (a) a.paused ? a.play() : a.pause(); }
        if (e.code === 'ArrowRight') { ap.nextTrack(); }
        if (e.code === 'ArrowLeft')  { ap.prevTrack(); }
    });
})();
</script>
</body>
</html>
