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

192 lines
9.5 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 } from '../lib.js';
const C = {
accent: '#f59e0b',
success: '#4ade80',
error: '#f87171',
muted: '#6b7280',
};
export default function PaywallKiller() {
const [inputUrl, setInputUrl] = useState('');
const [archiveUrl, setArchiveUrl] = useState('');
const [status, setStatus] = useState('idle');
const inputRef = useRef(null);
function getTargetUrl() {
let url = inputUrl.trim();
if (!url.startsWith('http')) url = 'https://' + url;
// Mobile Subdomains auf Desktop normalisieren (m.bild.de → www.bild.de)
// damit archive.ph den richtigen Snapshot findet
try {
const u = new URL(url);
if (u.hostname.startsWith('m.')) {
u.hostname = 'www.' + u.hostname.slice(2);
url = u.toString();
} else if (u.hostname.startsWith('mobile.')) {
u.hostname = 'www.' + u.hostname.slice(7);
url = u.toString();
}
} catch {}
return url;
}
function handleSearch() {
if (!inputUrl.trim()) return;
const targetUrl = getTargetUrl();
// archive.ph Submit (no-cors, fire-and-forget) triggert Archivierung
try {
const formData = new FormData();
formData.append('url', targetUrl);
formData.append('anyway', '1');
fetch('https://archive.ph/submit/', {
method: 'POST',
mode: 'no-cors',
body: formData,
}).catch(() => {});
} catch {}
setArchiveUrl(`https://archive.ph/newest/${targetUrl}`);
setStatus('ready');
}
function handleReset() {
setInputUrl('');
setArchiveUrl('');
setStatus('idle');
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', marginBottom: 10 }}>archive.ph · Als PDF speichern</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' }}>
<span style={{ color: 'rgba(255,255,255,0.25)', fontFamily: 'monospace', fontSize: 9, letterSpacing: 0.5 }}>GETESTET MIT</span>
{[
{ name: 'waz.de', domain: 'waz.de' },
{ name: 'bild.de', domain: 'bild.de' },
{ name: 'heise.de', domain: 'heise.de' },
].map(({ name, domain }) => (
<div key={name} style={{ display: 'flex', alignItems: 'center', gap: 4, padding: '2px 7px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.08)', borderRadius: 4 }}>
<img src={`https://www.google.com/s2/favicons?domain=${domain}&sz=16`} width={12} height={12} style={{ borderRadius: 2, opacity: 0.7 }} alt="" />
<span style={{ color: 'rgba(255,255,255,0.35)', fontFamily: 'monospace', fontSize: 9 }}>{name}</span>
</div>
))}
</div>
</div>
{/* 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 suchen
</button>
) : (
<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>
{/* Ergebnis */}
{status === 'ready' && (
<div style={{ ...S.card, borderColor: `${C.accent}55`, marginBottom: 12 }}>
<div style={{ ...S.head, marginBottom: 12 }}>ARTIKEL ÖFFNEN & ALS PDF SPEICHERN</div>
{/* Schritt 1 */}
<div style={{ display: 'flex', gap: 10, marginBottom: 4 }}>
<span style={{ background: `${C.accent}20`, border: `1px solid ${C.accent}44`, borderRadius: '50%', width: 22, height: 22, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 11, color: C.accent, fontFamily: 'monospace', flexShrink: 0, marginTop: 1 }}>1</span>
<div style={{ flex: 1 }}>
<div style={{ color: 'rgba(255,255,255,0.7)', fontFamily: 'monospace', fontSize: 11, marginBottom: 8 }}>
Öffne den Artikel auf archive.ph. Falls CAPTCHA erscheint: einmalig lösen. Falls "No results": auf <strong style={{ color: 'rgba(255,255,255,0.5)' }}>"archive this url"</strong> klicken, kurz warten, dann nochmal öffnen.
</div>
<a href={archiveUrl} target="_blank" rel="noreferrer"
style={{ ...S.btn(C.accent), display: 'block', textAlign: 'center', textDecoration: 'none', boxSizing: 'border-box', fontSize: 13 }}>
🌐 Artikel auf archive.ph öffnen
</a>
</div>
</div>
{/* Nochmal öffnen nach Archivierung */}
<div style={{ margin: '10px 0 16px 32px', padding: '8px 12px', background: 'rgba(255,255,255,0.03)', borderRadius: 6, border: '1px solid rgba(255,255,255,0.07)' }}>
<div style={{ color: C.muted, fontSize: 10, fontFamily: 'monospace', marginBottom: 6 }}>
Nach "archive this url" ~1530s warten, dann:
</div>
<a href={archiveUrl} target="_blank" rel="noreferrer"
style={{ ...S.btn(C.muted), display: 'inline-block', textDecoration: 'none', fontSize: 11 }}>
🔄 Nochmal öffnen
</a>
</div>
{/* Schritt 2 */}
<div style={{ display: 'flex', gap: 10 }}>
<span style={{ background: `${C.success}20`, border: `1px solid ${C.success}44`, borderRadius: '50%', width: 22, height: 22, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 11, color: C.success, fontFamily: 'monospace', flexShrink: 0, marginTop: 1 }}>2</span>
<div style={{ flex: 1 }}>
<div style={{ color: 'rgba(255,255,255,0.7)', fontFamily: 'monospace', fontSize: 11, marginBottom: 8 }}>
Wenn der Artikel sichtbar ist, als PDF speichern:
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
{[
['📱', 'Android Chrome', 'Menü (⋮) → Teilen → Drucken → Als PDF · Kopf-/Fußzeilen: aus'],
['🍎', 'iOS Safari', 'Teilen-Button → Als PDF sichern'],
['💻', 'Desktop', 'Strg+P / Cmd+P → Als PDF · Kopf-/Fußzeilen: aus'],
].map(([icon, label, desc]) => (
<div key={label} style={{ padding: '8px 10px', background: 'rgba(255,255,255,0.03)', borderRadius: 6, border: '1px solid rgba(255,255,255,0.07)' }}>
<div style={{ color: 'rgba(255,255,255,0.6)', fontFamily: 'monospace', fontSize: 11, marginBottom: 2 }}>{icon} {label}</div>
<div style={{ color: C.muted, fontFamily: 'monospace', fontSize: 10, lineHeight: 1.5 }}>{desc}</div>
</div>
))}
</div>
<div style={{ marginTop: 10, padding: '8px 10px', background: 'rgba(255,255,255,0.02)', borderRadius: 6, border: '1px solid rgba(255,255,255,0.05)' }}>
<div style={{ color: 'rgba(255,255,255,0.3)', fontFamily: 'monospace', fontSize: 10, lineHeight: 1.5 }}>
Der archive.ph-Balken oben ist Teil der archivierten Seite und kann nicht automatisch entfernt werden.
</div>
</div>
</div>
</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', 'Artikel auf archive.ph öffnen'],
['3', 'Falls nicht archiviert: "archive this url" klicken, kurz warten, nochmal öffnen'],
['4', 'Über Drucken-Funktion als PDF speichern'],
].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>
);
}