presentation_folder.js

import {
    PresentationWaveform,
    PresentationTranscript,
    PresentationController,
    PresentationSectionNav,
    buildProject,
    initPresThemeToggle,
} from './presentation.js';

// ── State ─────────────────────────────────────────────────────────────────────

const HEADER_H = 56;

let projects = [];       // built Project instances
let currentIdx = 0;

let ctrl          = null;
let presWaveform  = null;
let presTranscript = null;
let presSectionNav = null;
let playerGroup   = null;

// ── DOM refs ──────────────────────────────────────────────────────────────────

/**
 * @param {string} id - Element ID to look up.
 * @returns {HTMLElement}
 */
function el(id) { return document.getElementById(id); }

// ── State panels ──────────────────────────────────────────────────────────────

/** Opens the playlist sidebar panel. */
function openPlaylist() {
    el('presPlaylist').classList.add('open');
    document.body.classList.add('pres-playlist-open');
    el('presPlaylistToggle').style.display = '';
    if (window.innerWidth <= 640) {
        el('presPlaylistBackdrop').style.display = '';
    }
}

/** Closes the playlist sidebar panel. */
function closePlaylist() {
    el('presPlaylist').classList.remove('open');
    document.body.classList.remove('pres-playlist-open');
    el('presPlaylistBackdrop').style.display = 'none';
}

/** @param {string} id - element id of the state panel to show */
function showState(id) {
    ['presLoading', 'presAuthRequired', 'presNoAccess', 'presEmpty'].forEach(s => {
        el(s).style.display = (s === id) ? '' : 'none';
    });
    el('presFolderContent').style.display = 'none';
    closePlaylist();
    el('presPlaylistToggle').style.display = 'none';
}

/** Shows the main presentation content area and hides all state panels. */
function showPresentation() {
    ['presLoading', 'presAuthRequired', 'presNoAccess', 'presEmpty'].forEach(s => {
        el(s).style.display = 'none';
    });
    el('presFolderContent').style.display   = '';
    el('presWaveformSection').style.display   = '';
    el('presTranscriptSection').style.display = '';
    if (window.innerWidth > 640) {
        openPlaylist();
    } else {
        el('presPlaylistToggle').style.display = '';
    }
}

// ── Playlist ──────────────────────────────────────────────────────────────────

/** Renders the playlist nav items from the current projects array. */
function buildPlaylist() {
    const nav = el('presPlaylist');
    nav.innerHTML = '<div class="pres-playlist-header">Playlist</div>';
    projects.forEach((project, idx) => {
        const btn = document.createElement('button');
        btn.className = 'pres-playlist-item';
        btn.textContent = project.projectName;
        btn.addEventListener('click', () => loadProject(idx));
        nav.appendChild(btn);
    });
}

/** Syncs the active class on playlist items to match currentIdx. */
function updatePlaylistActive() {
    el('presPlaylist').querySelectorAll('.pres-playlist-item').forEach((btn, i) => {
        btn.classList.toggle('active', i === currentIdx);
    });
}

// ── Load project into panels ──────────────────────────────────────────────────

/** @param {number} idx - index into the projects array to load */
function loadProject(idx) {
    currentIdx = idx;
    const project = projects[idx];

    ctrl.activeProject    = project;
    ctrl.activeSegmentIdx = -1;
    ctrl.selectedSegmentIdx = -1;

    presWaveform.loadFromProject(project);
    presTranscript.loadFromProject(project);

    presSectionNav.setSections(presTranscript.sections);
    updatePlaylistActive();

    el('presBreadcrumbProject').textContent      = project.projectName;
    el('presBreadcrumbSep').style.display        = '';
    el('presCopyProjectBtn').style.display       = '';

    window.scrollTo({ top: 0, behavior: 'smooth' });
    if (window.innerWidth <= 640) closePlaylist();
}

// ── Init ──────────────────────────────────────────────────────────────────────

/** Initialises the search bar toggle, highlighting, and navigation controls. */
function initSearch() {
    const searchBar = el('presSearchBar');
    const toggleBtn = el('presSearchToggle');
    const fields    = el('presSearchFields');
    const input     = el('presSearchInput');
    const countEl   = el('presSearchCount');
    const prevBtn   = el('presSearchPrev');
    const nextBtn   = el('presSearchNext');

    let matches    = [];
    let focusedIdx = -1;

    /** Opens the search bar and focuses the input. */
    function open() {
        searchBar.classList.add('open');
        fields.style.display = 'flex';
        input.focus();
        input.select();
    }

    /** Closes the search bar and clears all highlights. */
    function close() {
        searchBar.classList.remove('open');
        fields.style.display = 'none';
        _clearMatches();
        input.value = '';
    }

    /** @param {HTMLElement} rootEl - element whose search highlights to remove */
    function _clearTextHighlights(rootEl) {
        rootEl.querySelectorAll('mark.pt-search-hl').forEach(mark => {
            mark.replaceWith(document.createTextNode(mark.textContent));
        });
        rootEl.normalize();
    }

    /**
     * @param {HTMLElement} rootEl - Root element to search within.
     * @param {string} query - Lowercase search term to highlight.
     */
    function _highlightTextInEl(rootEl, query) {
        const walker = document.createTreeWalker(rootEl, NodeFilter.SHOW_TEXT, null);
        const textNodes = [];
        let node;
        while ((node = walker.nextNode())) textNodes.push(node);
        textNodes.forEach(textNode => {
            const text  = textNode.textContent;
            const lower = text.toLowerCase();
            if (!lower.includes(query)) return;
            const frag = document.createDocumentFragment();
            let pos = 0, i = lower.indexOf(query, 0);
            while (i !== -1) {
                if (i > pos) frag.appendChild(document.createTextNode(text.slice(pos, i)));
                const mark = document.createElement('mark');
                mark.className = 'pt-search-hl';
                mark.textContent = text.slice(i, i + query.length);
                frag.appendChild(mark);
                pos = i + query.length;
                i = lower.indexOf(query, pos);
            }
            if (pos < text.length) frag.appendChild(document.createTextNode(text.slice(pos)));
            textNode.parentNode.replaceChild(frag, textNode);
        });
    }

    /** Clears all match highlights and resets navigation state. */
    function _clearMatches() {
        matches.forEach(idx => {
            const segEl = presTranscript._segEls[idx];
            if (!segEl) return;
            segEl.classList.remove('pt-seg-search-focused');
            _clearTextHighlights(segEl);
        });
        matches = [];
        focusedIdx = -1;
        countEl.textContent = '';
        prevBtn.disabled = true;
        nextBtn.disabled = true;
    }

    /** @param {number} i - wrapping index into the matches array */
    function _focusMatch(i) {
        if (!matches.length) return;
        presTranscript._segEls[matches[focusedIdx]]?.classList.remove('pt-seg-search-focused');
        focusedIdx = ((i % matches.length) + matches.length) % matches.length;
        const segEl = presTranscript._segEls[matches[focusedIdx]];
        segEl?.classList.add('pt-seg-search-focused');
        segEl?.scrollIntoView({ behavior: 'smooth', block: 'center' });
        countEl.textContent = `${focusedIdx + 1} / ${matches.length}`;
    }

    /** Scans transcript segments for the current query and highlights matches. */
    function _runSearch() {
        _clearMatches();
        const q = input.value.trim().toLowerCase();
        if (!q) return;
        presTranscript._segEls.forEach((segEl, idx) => {
            if (!segEl) return;
            if (segEl.textContent.toLowerCase().includes(q)) {
                _highlightTextInEl(segEl, q);
                matches.push(idx);
            }
        });
        if (matches.length) {
            prevBtn.disabled = false;
            nextBtn.disabled = false;
            _focusMatch(0);
        } else {
            countEl.textContent = 'No matches';
        }
    }

    toggleBtn.addEventListener('click', () => {
        if (searchBar.classList.contains('open')) { close(); } else { open(); }
    });
    prevBtn.addEventListener('click', () => _focusMatch(focusedIdx - 1));
    nextBtn.addEventListener('click', () => _focusMatch(focusedIdx + 1));
    input.addEventListener('input', _runSearch);
    input.addEventListener('keydown', (e) => {
        if (e.key === 'Escape') { close(); return; }
        if (e.key === 'Enter') {
            e.preventDefault();
            _focusMatch(e.shiftKey ? focusedIdx - 1 : focusedIdx + 1);
        }
    });
    document.addEventListener('keydown', (e) => {
        if ((e.ctrlKey || e.metaKey) && e.key === 'f') {
            e.preventDefault();
            if (searchBar.classList.contains('open')) { input.focus(); input.select(); } else { open(); }
        }
    });
}

/** Wires click handlers to the folder and project copy-link buttons. */
function initCopyBtn() {
    const btn = el('presCopyBtn');
    if (!btn) return;
    btn.addEventListener('click', async () => {
        try {
            await navigator.clipboard.writeText(window.location.href);
            const orig = btn.innerHTML;
            btn.innerHTML = '<span class="pres-copy-icon">&#10003;</span> Copied!';
            btn.disabled = true;
            setTimeout(() => { btn.innerHTML = orig; btn.disabled = false; }, 2000);
        } catch {}
    });

    const projBtn = el('presCopyProjectBtn');
    if (!projBtn) return;
    projBtn.addEventListener('click', async () => {
        const url = `${window.location.origin}/presentation/${projects[currentIdx].projectId}`;
        try {
            await navigator.clipboard.writeText(url);
            const orig = projBtn.innerHTML;
            projBtn.innerHTML = '<span class="pres-copy-icon">&#10003;</span> Copied!';
            projBtn.disabled = true;
            setTimeout(() => { projBtn.innerHTML = orig; projBtn.disabled = false; }, 2000);
        } catch {}
    });
}

/** Entry point: builds all panels and loads the first project in the playlist. */
async function init() {
    const pdata = window.PDATA;
    initCopyBtn();
    initPresThemeToggle(null, null);

    if (pdata.ownerName) {
        el('presBreadcrumbOwner').textContent  = pdata.ownerName;
        el('presBreadcrumbOwner').style.display    = '';
        el('presBreadcrumbOwnerSep').style.display = '';
    }

    if (!pdata?.projects?.length) {
        showState('presEmpty');
        return;
    }

    // Build all Project instances up front
    projects = pdata.projects.map(p => buildProject(p));

    if (!projects.length) {
        showState('presEmpty');
        return;
    }

    // ── Build panels ──────────────────────────────────────────────────────────
    ctrl = new PresentationController();

    playerGroup = el('presPlayerGroup');
    const wfMount = el('presWaveformMount');
    presWaveform = new PresentationWaveform(wfMount, ctrl, {
        onRegionHover:    idx => ctrl.onRegionHover(idx),
        onRegionSelect:   idx => ctrl.onRegionSelect(idx),
        onRegionActivate: idx => ctrl.onRegionActivate(idx),
        onUserSeek:       idx => ctrl.onUserSeek(idx),
    });

    const transcriptEl = el('presTranscript');
    presTranscript = new PresentationTranscript(transcriptEl, ctrl);
    presTranscript.scrollToSegment = (idx) => {
        if (idx < 0) return;
        const segEl = transcriptEl.querySelector(`.pt-seg[data-idx="${idx}"]`);
        if (!segEl) return;
        segEl.style.scrollMarginTop = (HEADER_H + playerGroup.offsetHeight + 16) + 'px';
        segEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
    };

    presSectionNav = new PresentationSectionNav(
        el('presSectionNav'),
        el('presSectionNavList'),
        el('presSectionNavToggle'),
    );

    ctrl.setPanels(presWaveform, presTranscript, presSectionNav);
    initSearch();
    buildPlaylist();

    el('presPlaylistToggle').addEventListener('click', () => {
        el('presPlaylist').classList.contains('open') ? closePlaylist() : openPlaylist();
    });
    el('presPlaylistBackdrop').addEventListener('click', closePlaylist);

    const actionsToggle = el('presActionsToggle');
    const actionsMenu   = el('presActionsMenu');
    actionsToggle.addEventListener('click', (e) => {
        e.stopPropagation();
        actionsMenu.classList.toggle('open');
    });
    document.addEventListener('click', (e) => {
        if (!actionsMenu.contains(e.target) && e.target !== actionsToggle) {
            actionsMenu.classList.remove('open');
        }
    });

    document.addEventListener('keydown', (e) => {
        if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
        if (e.key === 'ArrowLeft'  && currentIdx > 0)                  loadProject(currentIdx - 1);
        if (e.key === 'ArrowRight' && currentIdx < projects.length - 1) loadProject(currentIdx + 1);
    });

    // ── Sticky observer ───────────────────────────────────────────────────────
    const sentinel = el('presWaveformSentinel');
    new IntersectionObserver(([entry]) => {
        const stuck = !entry.isIntersecting && entry.boundingClientRect.top < 0;
        playerGroup.classList.toggle('pres-waveform-stuck', stuck);
    }).observe(sentinel);

    const searchAnchor = el('presSearchAnchor');
    const sectionNavEl = el('presSectionNav');
    const headerEl     = document.querySelector('.pres-header');
    const playlistEl   = el('presPlaylist');

    /** Keeps playlist panel top/height aligned with the header height. */
    function _updatePlaylistTop() {
        const h = headerEl ? headerEl.offsetHeight : HEADER_H;
        playlistEl.style.top    = h + 'px';
        playlistEl.style.height = `calc(100vh - ${h}px)`;
    }
    _updatePlaylistTop();
    if (headerEl) new ResizeObserver(_updatePlaylistTop).observe(headerEl);

    /** Recalculates sticky top offsets for the search bar and section nav. */
    function _updateStickyTops() {
        const headerH_actual = headerEl ? headerEl.offsetHeight : HEADER_H;
        const playerBottom = headerH_actual + playerGroup.offsetHeight;
        searchAnchor.style.top = (playerBottom + 8) + 'px';
        sectionNavEl.style.top = (playerBottom + 20) + 'px';
    }
    _updateStickyTops();
    new ResizeObserver(_updateStickyTops).observe(playerGroup);

    presSectionNav.setScrollOffset(() => (headerEl ? headerEl.offsetHeight : HEADER_H) + playerGroup.offsetHeight + 16);
    presSectionNav.setOnNavigate(t => ctrl.seekTo(t));

    // ── Load first project ────────────────────────────────────────────────────
    showPresentation();
    loadProject(0);
}

(async () => {
    try {
        await init();
    } catch (err) {
        console.error('Folder presentation init failed:', err);
        showState('presNoAccess');
    }
})();