215 lines
8.8 KiB
JavaScript
215 lines
8.8 KiB
JavaScript
import { useState, useRef, useEffect } from 'react';
|
||
import { S, api } from '../lib.js';
|
||
|
||
const C = {
|
||
accent: '#f59e0b',
|
||
success: '#4ade80',
|
||
error: '#f87171',
|
||
muted: '#6b7280',
|
||
};
|
||
|
||
export default function PaywallKiller() {
|
||
const [inputUrl, setInputUrl] = useState('');
|
||
const [status, setStatus] = useState('idle');
|
||
const [archiveUrl, setArchiveUrl] = useState('');
|
||
const [showCaptcha, setShowCaptcha] = useState(false);
|
||
const [errMsg, setErrMsg] = useState('');
|
||
const [pdfLoading, setPdfLoading] = useState(false);
|
||
const inputRef = useRef(null);
|
||
const iframeRef = useRef(null);
|
||
|
||
function getTargetUrl() {
|
||
const url = inputUrl.trim();
|
||
return url.startsWith('http') ? url : 'https://' + url;
|
||
}
|
||
|
||
function handleSearch() {
|
||
if (!inputUrl.trim()) return;
|
||
const aUrl = `https://archive.ph/newest/${getTargetUrl()}`;
|
||
setArchiveUrl(aUrl);
|
||
setStatus('captcha');
|
||
setShowCaptcha(true);
|
||
setErrMsg('');
|
||
}
|
||
|
||
function handleCaptchaDone() {
|
||
setShowCaptcha(false);
|
||
setStatus('ready');
|
||
}
|
||
|
||
async function handlePdf() {
|
||
setPdfLoading(true);
|
||
setErrMsg('');
|
||
try {
|
||
// Cookies für archive.ph aus dem Browser lesen
|
||
// document.cookie liefert nur same-site cookies – archive.ph cookies
|
||
// sind third-party und nicht lesbar. Wir übergeben stattdessen
|
||
// die URL und lassen Puppeteer direkt die Session simulieren:
|
||
// Puppeteer öffnet zuerst archive.ph root um Cookies zu setzen,
|
||
// dann direkt die Snapshot-URL.
|
||
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 d = await res.json().catch(() => ({}));
|
||
throw new Error(d.error || 'PDF-Fehler');
|
||
}
|
||
const blob = await res.blob();
|
||
const objUrl = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = objUrl;
|
||
a.download = 'artikel_' + Date.now() + '.pdf';
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
document.body.removeChild(a);
|
||
setTimeout(() => URL.revokeObjectURL(objUrl), 5000);
|
||
} catch (err) {
|
||
setErrMsg(err.message || 'PDF fehlgeschlagen');
|
||
} finally {
|
||
setPdfLoading(false);
|
||
}
|
||
}
|
||
|
||
function handleReset() {
|
||
setInputUrl('');
|
||
setStatus('idle');
|
||
setArchiveUrl('');
|
||
setShowCaptcha(false);
|
||
setErrMsg('');
|
||
setPdfLoading(false);
|
||
setTimeout(() => inputRef.current?.focus(), 50);
|
||
}
|
||
|
||
return (
|
||
<div style={{ padding: '20px 16px', maxWidth: 640, margin: '0 auto' }}>
|
||
|
||
{/* Header */}
|
||
<div style={{ marginBottom: 24 }}>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 4 }}>
|
||
<span style={{ fontSize: 20 }}>🔓</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' }}>archive.ph · Als PDF speichern</div>
|
||
</div>
|
||
|
||
{/* URL-Eingabe */}
|
||
<div style={{ ...S.card, marginBottom: 16 }}>
|
||
<div style={{ ...S.head, marginBottom: 10 }}>ARTIKEL-URL</div>
|
||
<input
|
||
ref={inputRef}
|
||
value={inputUrl}
|
||
onChange={e => setInputUrl(e.target.value)}
|
||
onKeyDown={e => e.key === 'Enter' && status === 'idle' && handleSearch()}
|
||
placeholder="URL einfügen (Long-Press → Einfügen)"
|
||
style={{ ...S.inp, width: '100%', boxSizing: 'border-box', marginBottom: 10 }}
|
||
/>
|
||
<div style={{ display: 'flex', gap: 8 }}>
|
||
{status === 'idle' && (
|
||
<button onClick={handleSearch} disabled={!inputUrl.trim()}
|
||
style={{ ...S.btn(C.accent), flex: 1, opacity: !inputUrl.trim() ? 0.45 : 1, cursor: !inputUrl.trim() ? 'default' : 'pointer' }}>
|
||
🔍 Archiv öffnen
|
||
</button>
|
||
)}
|
||
{status !== 'idle' && (
|
||
<button onClick={handleReset}
|
||
style={{ background: 'rgba(255,255,255,0.06)', border: '1px solid rgba(255,255,255,0.14)', borderRadius: 6, padding: '8px 14px', color: 'rgba(255,255,255,0.6)', cursor: 'pointer', fontFamily: 'monospace', fontSize: 12 }}>
|
||
✕ Reset
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* CAPTCHA-Schritt */}
|
||
{status === 'captcha' && (
|
||
<div style={{ ...S.card, borderColor: `${C.accent}55`, marginBottom: 16 }}>
|
||
<div style={{ ...S.head, marginBottom: 8 }}>SCHRITT 1 – CAPTCHA BESTÄTIGEN</div>
|
||
<div style={{ color: C.muted, fontSize: 11, fontFamily: 'monospace', marginBottom: 12, lineHeight: 1.6 }}>
|
||
archive.ph wird unten geladen. Falls ein CAPTCHA erscheint: abhaken, warten bis der Artikel sichtbar ist, dann auf "Artikel bereit → PDF erstellen" klicken.
|
||
</div>
|
||
|
||
{/* archive.ph iframe */}
|
||
<div style={{ borderRadius: 8, overflow: 'hidden', border: '1px solid rgba(255,255,255,0.1)', marginBottom: 12, background: '#fff' }}>
|
||
<iframe
|
||
ref={iframeRef}
|
||
src={archiveUrl}
|
||
style={{ width: '100%', height: 400, border: 'none', display: 'block' }}
|
||
title="archive.ph"
|
||
/>
|
||
</div>
|
||
|
||
<div style={{ display: 'flex', gap: 8 }}>
|
||
<a href={archiveUrl} target="_blank" rel="noreferrer"
|
||
style={{ ...S.btn(C.muted), textDecoration: 'none', fontSize: 12 }}>
|
||
🌐 Im Browser öffnen
|
||
</a>
|
||
<button onClick={handleCaptchaDone}
|
||
style={{ ...S.btn(C.success), flex: 1, fontSize: 12 }}>
|
||
✅ Artikel bereit → PDF erstellen
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* PDF-Schritt */}
|
||
{status === 'ready' && (
|
||
<div style={{ ...S.card, borderColor: `${C.success}55`, marginBottom: 16 }}>
|
||
<div style={{ ...S.head, marginBottom: 8 }}>SCHRITT 2 – PDF ERSTELLEN</div>
|
||
<div style={{ color: C.muted, fontSize: 11, fontFamily: 'monospace', marginBottom: 12, lineHeight: 1.6 }}>
|
||
Der Server lädt den Artikel jetzt über archive.ph und erstellt ein PDF. Das dauert 15–30 Sekunden.
|
||
</div>
|
||
<div style={{ background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.08)', borderRadius: 6, padding: '7px 10px', marginBottom: 12, wordBreak: 'break-all' }}>
|
||
<span style={{ color: C.muted, fontSize: 10, fontFamily: 'monospace' }}>{archiveUrl}</span>
|
||
</div>
|
||
<div style={{ display: 'flex', gap: 8 }}>
|
||
<a href={archiveUrl} target="_blank" rel="noreferrer"
|
||
style={{ ...S.btn(C.success), textDecoration: 'none', fontSize: 12 }}>
|
||
🌐 Öffnen
|
||
</a>
|
||
<button onClick={handlePdf} disabled={pdfLoading}
|
||
style={{ ...S.btn(C.accent), flex: 1, opacity: pdfLoading ? 0.5 : 1, cursor: pdfLoading ? 'default' : 'pointer', fontSize: 12 }}>
|
||
{pdfLoading ? '⏳ Erstelle PDF...' : '⬇ Als PDF speichern'}
|
||
</button>
|
||
</div>
|
||
{pdfLoading && (
|
||
<div style={{ color: C.muted, fontSize: 11, fontFamily: 'monospace', marginTop: 8 }}>
|
||
Seite wird serverseitig gerendert (15–30s)...
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{errMsg && (
|
||
<div style={{ ...S.card, borderColor: `${C.error}44`, marginTop: 8 }}>
|
||
<div style={{ display: 'flex', gap: 8 }}>
|
||
<span>⚠️</span>
|
||
<span style={{ color: C.error, fontFamily: 'monospace', fontSize: 12 }}>{errMsg}</span>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{status === 'idle' && (
|
||
<div style={{ ...S.card }}>
|
||
<div style={{ ...S.head, marginBottom: 10 }}>SO FUNKTIONIERT ES</div>
|
||
{[
|
||
['1', 'URL des Artikels einfügen'],
|
||
['2', 'archive.ph wird geladen – CAPTCHA einmalig bestätigen'],
|
||
['3', 'Wenn der Artikel sichtbar ist: "PDF erstellen" klicken'],
|
||
['4', 'PDF wird automatisch heruntergeladen'],
|
||
].map(([num, text]) => (
|
||
<div key={num} style={{ display: 'flex', gap: 10, alignItems: 'flex-start', marginBottom: 8 }}>
|
||
<span style={{ background: `${C.accent}20`, border: `1px solid ${C.accent}44`, borderRadius: '50%', width: 20, height: 20, flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 10, color: C.accent, fontFamily: 'monospace', marginTop: 2 }}>{num}</span>
|
||
<span style={{ color: C.muted, fontSize: 11, fontFamily: 'monospace', lineHeight: 1.6 }}>{text}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|