316 lines
10 KiB
JavaScript
316 lines
10 KiB
JavaScript
import { useState, useRef } from 'react';
|
||
import { S, api } from '../lib.js';
|
||
|
||
const C = {
|
||
accent: '#f59e0b',
|
||
success: '#4ade80',
|
||
error: '#f87171',
|
||
muted: '#6b7280',
|
||
dim: '#1f2937',
|
||
};
|
||
|
||
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');
|
||
const [archiveUrl, setArchiveUrl] = useState('');
|
||
const [archiveType, setArchiveType] = useState('');
|
||
const [errMsg, setErrMsg] = useState('');
|
||
const [pdfLoading, setPdfLoading] = useState(false);
|
||
const inputRef = useRef(null);
|
||
|
||
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');
|
||
}
|
||
}
|
||
}
|
||
|
||
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);
|
||
}
|
||
}
|
||
|
||
function handleReset() {
|
||
setInputUrl('');
|
||
setStatus('idle');
|
||
setArchiveUrl('');
|
||
setArchiveType('');
|
||
setErrMsg('');
|
||
setPdfLoading(false);
|
||
setTimeout(() => inputRef.current?.focus(), 50);
|
||
}
|
||
|
||
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';
|
||
const hasResult = isFound || isNotFound || isError;
|
||
|
||
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' }}>
|
||
Artikel über archive.is abrufen · Als PDF speichern
|
||
</div>
|
||
</div>
|
||
|
||
{/* Eingabe */}
|
||
<div style={{ ...S.card, marginBottom: 16 }}>
|
||
<div style={{ ...S.head, marginBottom: 10 }}>ARTIKEL-URL</div>
|
||
|
||
{/* Input + Paste-Button */}
|
||
<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}
|
||
title="Aus Zwischenablage einfügen"
|
||
style={{
|
||
background: 'rgba(255,255,255,0.06)',
|
||
border: '1px solid rgba(255,255,255,0.12)',
|
||
borderRadius: 6,
|
||
padding: '0 12px',
|
||
color: '#fff',
|
||
cursor: 'pointer',
|
||
fontSize: 14,
|
||
flexShrink: 0,
|
||
}}
|
||
>
|
||
📋
|
||
</button>
|
||
</div>
|
||
|
||
{/* Action-Buttons */}
|
||
<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.45 : 1,
|
||
cursor: (isChecking || !inputUrl.trim()) ? 'default' : 'pointer',
|
||
}}
|
||
>
|
||
{isChecking ? '⏳ Suche Archiv...' : '🔍 Archiv suchen'}
|
||
</button>
|
||
{hasResult && (
|
||
<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,
|
||
whiteSpace: 'nowrap',
|
||
}}
|
||
>
|
||
✕ Reset
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Gefunden */}
|
||
{isFound && (
|
||
<div style={{ ...S.card, borderColor: `${C.success}55`, marginBottom: 16 }}>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
|
||
<span>✅</span>
|
||
<span style={{ color: C.success, fontFamily: 'monospace', fontSize: 12 }}>
|
||
Archiv gefunden
|
||
</span>
|
||
<span style={{
|
||
background: `${C.accent}20`,
|
||
border: `1px solid ${C.accent}55`,
|
||
borderRadius: 4,
|
||
padding: '1px 7px',
|
||
fontSize: 10,
|
||
color: C.accent,
|
||
fontFamily: 'monospace',
|
||
}}>
|
||
{archiveType === 'newest' ? 'neuester Snapshot' : 'ältester Snapshot'}
|
||
</span>
|
||
</div>
|
||
|
||
<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>
|
||
|
||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
||
<a
|
||
href={archiveUrl}
|
||
target="_blank"
|
||
rel="noreferrer"
|
||
style={{ ...S.btn(C.success), textDecoration: 'none' }}
|
||
>
|
||
🌐 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>
|
||
|
||
{pdfLoading && (
|
||
<div style={{ color: C.muted, fontSize: 11, fontFamily: 'monospace', marginTop: 10 }}>
|
||
Seite wird gerendert, das dauert 15–30 Sekunden...
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* Kein Archiv */}
|
||
{isNotFound && (
|
||
<div style={{ ...S.card, borderColor: `${C.error}44` }}>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
|
||
<span>📭</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, marginBottom: 12 }}>
|
||
Für <strong style={{ color: 'rgba(255,255,255,0.55)' }}>{getDomain(inputUrl)}</strong> wurde
|
||
kein Snapshot auf archive.is gefunden.
|
||
</div>
|
||
<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>
|
||
)}
|
||
|
||
{/* Fehler */}
|
||
{isError && (
|
||
<div style={{ ...S.card, borderColor: `${C.error}44` }}>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||
<span>⚠️</span>
|
||
<span style={{ color: C.error, fontFamily: 'monospace', fontSize: 12 }}>
|
||
{errMsg}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Idle-Info */}
|
||
{status === 'idle' && (
|
||
<div style={{ ...S.card }}>
|
||
<div style={{ ...S.head, marginBottom: 10 }}>SO FUNKTIONIERT ES</div>
|
||
{[
|
||
['1', 'URL des gesperrten Artikels einfügen'],
|
||
['2', 'Archiv auf archive.is suchen'],
|
||
['3', 'Im Browser öffnen oder als PDF speichern'],
|
||
].map(([num, text]) => (
|
||
<div key={num} style={{ display: 'flex', gap: 10, alignItems: 'center', 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',
|
||
}}>{num}</span>
|
||
<span style={{ color: C.muted, fontSize: 11, fontFamily: 'monospace' }}>{text}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|