📄 src/content.ts
import { getClientToken, getSessionToken } from './auth';
import { fetchPdfBlob, fetchPdfToken } from './api';
import { CancelledError, DownloadError, toFailureReason } from './errors';
import { PDF_ACCEPT, withMediaHash } from './hash';
import { parseIssueAnchor, type IssueRef } from './selectors';
import { isDownloadableTitle, warmTitlesCache } from './titles';
import { showErrorDialog } from './ui/dialog';
import { openLoadingTab, type LoadingTab } from './ui/loadingTab';

/**
 * VF PDF-opener — content script for https://etidning.vf.se.
 *
 * Clicking an issue in a carousel opens the paper directly as a PDF in a new
 * tab (borrowing the page's own login). If anything fails, a Swedish error
 * dialog appears and the paper opens in the regular reader instead — the
 * site always ends up working exactly as it would without the extension.
 */

let busy = false;

// Capture phase: runs before the reader app's own (React) click handlers.
document.addEventListener('click', onCaptureClick, true);

// Warm the "which titles are downloadable as PDF" cache on load (best effort).
warmTitlesCache(getClientToken(), getSessionToken());

function onCaptureClick(event: MouseEvent): void {
  if (event.button !== 0) return;
  if (event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) return;
  if (!(event.target instanceof Element)) return;
  const anchor = event.target.closest('a');
  if (!(anchor instanceof HTMLAnchorElement)) return;

  const issue = parseIssueAnchor(anchor);
  if (!issue) return;

  if (busy) {
    // A download is already running — swallow further issue clicks quietly.
    event.preventDefault();
    event.stopPropagation();
    return;
  }

  const downloadable = isDownloadableTitle(issue.titleId);
  if (downloadable === false) {
    return; // Insert/bilaga (not downloadable as PDF) — native behaviour.
  }
  if (downloadable === null) {
    // Cache cold — intercept optimistically, and warm for next time.
    warmTitlesCache(getClientToken(), getSessionToken());
  }

  event.preventDefault();
  event.stopPropagation();
  void downloadAndOpenPdf(anchor, issue);
}

async function downloadAndOpenPdf(anchor: HTMLAnchorElement, issue: IssueRef): Promise<void> {
  busy = true;
  const fallbackUrl = anchor.getAttribute('href') ?? window.location.href;
  const fallback = () => window.location.assign(fallbackUrl);

  let tab: LoadingTab | null = null;
  const abortController = new AbortController();

  try {
    tab = openLoadingTab();
    if (!tab) throw new DownloadError('popup-blocked');
    const loadingTab = tab;

    const sessionToken = getSessionToken();
    if (!sessionToken) throw new DownloadError('not-logged-in');
    const clientToken = getClientToken();

    const pdfToken = await fetchPdfToken(issue.issueId, clientToken, sessionToken);
    const pdfUrl = withMediaHash(pdfToken.url, window.location.origin);

    const blob = await fetchPdfBlob(
      pdfUrl,
      PDF_ACCEPT,
      abortController.signal,
      (loadedBytes, totalBytes) => {
        if (loadingTab.isClosed()) {
          abortController.abort(); // The user closed the loading tab — stop quietly.
          return;
        }
        loadingTab.setProgress(loadedBytes, totalBytes);
      },
    );

    if (loadingTab.isClosed()) throw new CancelledError();
    loadingTab.openPdf(URL.createObjectURL(blob));
  } catch (error) {
    tab?.close();
    if (error instanceof CancelledError) return;
    showErrorDialog(toFailureReason(error), fallback);
  } finally {
    busy = false;
  }
}