| Name | Message | Date |
|---|---|---|
| 📁 ui | 10 hours ago | |
| 📄 api.ts | 10 hours ago | |
| 📄 auth.ts | 10 hours ago | |
| 📄 content.ts | 10 hours ago | |
| 📄 errors.ts | 10 hours ago | |
| 📄 hash.ts | 10 hours ago | |
| 📄 md5.ts | 10 hours ago | |
| 📄 selectors.ts | 10 hours ago | |
| 📄 titles.ts | 10 hours ago |
📄
src/content.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
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;
}
}