Files
dickendock/frontend/src/tools/paywallkiller.jsx

303 lines
11 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useRef } from 'react';
import { S, api } from '../lib.js';
// ── Farben ─────────────────────────────────────────────────────────────────
const C = {
accent: '#f59e0b',
success: '#4ade80',
error: '#f87171',
muted: 'rgba(255,255,255,0.4)',
dim: 'rgba(255,255,255,0.07)',
};
// ── Hilfsfunktion: Domain aus URL extrahieren ──────────────────────────────
function getDomain(url) {
try { return new URL(url).hostname.replace(/^www\./, ''); }
catch { return url; }
}
export default function PaywallKiller() {
const [inputUrl, setInputUrl] = useState('');
const [status, setStatus] = useState('idle'); // idle | checking | found | notfound | pdf | error
const [archiveUrl, setArchiveUrl] = useState('');
const [archiveType, setArchiveType] = useState('');
const [errMsg, setErrMsg] = useState('');
const [pdfLoading, setPdfLoading] = useState(false);
const inputRef = useRef(null);
// ── URL prüfen ────────────────────────────────────────────────────────────
async function handleCheck() {
const url = inputUrl.trim();
if (!url) return;
setStatus('checking');
setErrMsg('');
setArchiveUrl('');
setArchiveType('');
try {
const data = await api('/tools/paywallkiller/check', { body: { url } });
setArchiveUrl(data.archiveUrl);
setArchiveType(data.type);
setStatus('found');
} catch (err) {
if (err.message && err.message.includes('Kein Archiv')) {
setStatus('notfound');
} else {
setErrMsg(err.message || 'Unbekannter Fehler');
setStatus('error');
}
}
}
// ── PDF herunterladen ─────────────────────────────────────────────────────
async function handlePdf() {
setPdfLoading(true);
try {
const token = localStorage.getItem('sk_token');
const res = await fetch('/api/tools/paywallkiller/pdf', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
body: JSON.stringify({ archiveUrl }),
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data.error || 'PDF-Fehler');
}
const blob = await res.blob();
const objUrl = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = objUrl;
a.download = 'archiv_' + Date.now() + '.pdf';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
setTimeout(() => URL.revokeObjectURL(objUrl), 5000);
} catch (err) {
setErrMsg(err.message || 'PDF konnte nicht erstellt werden');
setStatus('error');
} finally {
setPdfLoading(false);
}
}
// ── Reset ─────────────────────────────────────────────────────────────────
function handleReset() {
setInputUrl('');
setStatus('idle');
setArchiveUrl('');
setArchiveType('');
setErrMsg('');
setPdfLoading(false);
setTimeout(() => inputRef.current?.focus(), 50);
}
// ── Paste aus Clipboard ───────────────────────────────────────────────────
async function handlePaste() {
try {
const text = await navigator.clipboard.readText();
if (text) setInputUrl(text.trim());
} catch {
inputRef.current?.focus();
}
}
const isChecking = status === 'checking';
const isFound = status === 'found';
const isNotFound = status === 'notfound';
const isError = status === 'error';
return (
<div style={{ padding: '20px 16px', maxWidth: 660, margin: '0 auto' }}>
{/* Header */}
<div style={{ marginBottom: 24 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 4 }}>
<span style={{ fontSize: 22 }}>🔓</span>
<span style={{ color: C.accent, fontFamily: 'monospace', fontSize: 13, letterSpacing: 1 }}>
PAYWALL-KILLER
</span>
</div>
<div style={{ color: C.muted, fontSize: 11, fontFamily: 'monospace' }}>
Artikel über archive.is abrufen · Als PDF speichern
</div>
</div>
{/* URL-Eingabe */}
<div style={{ ...S.card, marginBottom: 16 }}>
<div style={{ ...S.head, marginBottom: 10 }}>ARTIKEL-URL</div>
<div style={{ display: 'flex', gap: 8, marginBottom: 10 }}>
<input
ref={inputRef}
value={inputUrl}
onChange={e => setInputUrl(e.target.value)}
onKeyDown={e => e.key === 'Enter' && !isChecking && handleCheck()}
placeholder="https://www.zeitung.de/artikel/..."
style={{ ...S.inp, flex: 1 }}
/>
<button onClick={handlePaste} style={{ ...S.btn(C.muted, true), padding: '5px 12px' }} title="Aus Zwischenablage einfügen">
📋
</button>
</div>
<div style={{ display: 'flex', gap: 8 }}>
<button
onClick={handleCheck}
disabled={isChecking || !inputUrl.trim()}
style={{
...S.btn(C.accent),
flex: 1,
opacity: (isChecking || !inputUrl.trim()) ? 0.5 : 1,
cursor: (isChecking || !inputUrl.trim()) ? 'default' : 'pointer',
}}
>
{isChecking ? '⏳ Suche Archiv...' : '🔍 Archiv suchen'}
</button>
{(isFound || isNotFound || isError) && (
<button onClick={handleReset} style={S.btn(C.muted, false)}>
Zurücksetzen
</button>
)}
</div>
</div>
{/* Gefunden */}
{isFound && (
<div style={{ ...S.card, borderColor: `${C.success}44`, marginBottom: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
<span style={{ fontSize: 16 }}></span>
<span style={{ color: C.success, fontFamily: 'monospace', fontSize: 12 }}>
Archiv gefunden
<span style={{
marginLeft: 8,
background: `${C.accent}22`,
border: `1px solid ${C.accent}44`,
borderRadius: 4,
padding: '1px 6px',
fontSize: 10,
color: C.accent,
}}>
{archiveType === 'newest' ? 'neuestes' : 'ältestes'} Archiv
</span>
</span>
</div>
{/* Archive-URL anzeigen */}
<div style={{
background: 'rgba(255,255,255,0.03)',
border: '1px solid rgba(255,255,255,0.08)',
borderRadius: 6,
padding: '8px 10px',
marginBottom: 14,
wordBreak: 'break-all',
}}>
<a href={archiveUrl} target="_blank" rel="noreferrer" style={{ color: C.accent, fontSize: 11, fontFamily: 'monospace' }}>
{archiveUrl}
</a>
</div>
{/* Aktions-Buttons */}
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
<a
href={archiveUrl}
target="_blank"
rel="noreferrer"
style={{ ...S.btn(C.success), textDecoration: 'none', display: 'inline-block' }}
>
🌐 Im Browser öffnen
</a>
<button
onClick={handlePdf}
disabled={pdfLoading}
style={{
...S.btn(C.accent),
opacity: pdfLoading ? 0.5 : 1,
cursor: pdfLoading ? 'default' : 'pointer',
}}
>
{pdfLoading ? '⏳ Erstelle PDF...' : '⬇ Als PDF speichern'}
</button>
</div>
{/* PDF-Hinweis */}
{pdfLoading && (
<div style={{ color: C.muted, fontSize: 11, fontFamily: 'monospace', marginTop: 10 }}>
Seite wird geladen und als PDF gerendert. Das kann 1530 Sekunden dauern...
</div>
)}
</div>
)}
{/* Kein Archiv */}
{isNotFound && (
<div style={{ ...S.card, borderColor: `${C.error}44` }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
<span style={{ fontSize: 16 }}>📭</span>
<span style={{ color: C.error, fontFamily: 'monospace', fontSize: 12 }}>
Kein Archiv gefunden
</span>
</div>
<div style={{ color: C.muted, fontSize: 11, fontFamily: 'monospace', lineHeight: 1.6 }}>
Für <strong style={{ color: 'rgba(255,255,255,0.6)' }}>{getDomain(inputUrl)}</strong> existiert
kein Eintrag auf archive.is. Der Artikel wurde möglicherweise noch nie archiviert.
</div>
<div style={{ marginTop: 12 }}>
<a
href={`https://archive.is/${inputUrl.trim()}`}
target="_blank"
rel="noreferrer"
style={{ ...S.btn(C.muted), textDecoration: 'none', display: 'inline-block', fontSize: 11 }}
>
🔗 Manuell auf archive.is prüfen
</a>
</div>
</div>
)}
{/* Fehler */}
{isError && (
<div style={{ ...S.card, borderColor: `${C.error}44` }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span style={{ fontSize: 16 }}></span>
<span style={{ color: C.error, fontFamily: 'monospace', fontSize: 12 }}>
{errMsg}
</span>
</div>
</div>
)}
{/* Info-Sektion */}
{status === 'idle' && (
<div style={{ ...S.card, marginTop: 8 }}>
<div style={{ ...S.head, marginBottom: 10 }}>WIE ES FUNKTIONIERT</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{[
['1', 'URL des gesperrten Artikels einfügen'],
['2', 'Archiv auf archive.is suchen (newest → oldest)'],
['3', 'Archivierte Version im Browser öffnen oder als PDF speichern'],
].map(([num, text]) => (
<div key={num} style={{ display: 'flex', gap: 10, alignItems: 'flex-start' }}>
<span style={{
background: `${C.accent}22`,
border: `1px solid ${C.accent}44`,
borderRadius: '50%',
width: 20, height: 20,
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 10, color: C.accent, fontFamily: 'monospace', flexShrink: 0,
}}>
{num}
</span>
<span style={{ color: C.muted, fontSize: 11, fontFamily: 'monospace', lineHeight: 1.6 }}>
{text}
</span>
</div>
))}
</div>
</div>
)}
</div>
);
}