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

316 lines
10 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';
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 1530 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>
);
}