refactor: Paywall-Killer nur Browser-Flow, kein Server mehr
This commit is contained in:
@@ -1,168 +1,8 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const { authenticate } = require('../../middleware/auth');
|
|
||||||
const https = require('https');
|
|
||||||
const http = require('http');
|
|
||||||
const crypto = require('crypto');
|
|
||||||
const path = require('path');
|
|
||||||
const fs = require('fs');
|
|
||||||
|
|
||||||
// ── HTTPS GET Helper ───────────────────────────────────────────────────────
|
// Paywall-Killer hat keinen aktiven Backend-Endpoint mehr.
|
||||||
function fetchHtml(url, redirects = 0) {
|
// archive.ph blockt Server-seitige Requests (429).
|
||||||
return new Promise((resolve, reject) => {
|
// Der komplette Flow läuft im Browser des Users.
|
||||||
if (redirects > 5) return reject(new Error('Zu viele Redirects'));
|
|
||||||
const mod = url.startsWith('https') ? https : http;
|
|
||||||
const req = mod.get(url, {
|
|
||||||
headers: {
|
|
||||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
|
|
||||||
'Accept': 'text/html,application/xhtml+xml',
|
|
||||||
'Accept-Language': 'de,en;q=0.9',
|
|
||||||
'Accept-Encoding': 'identity',
|
|
||||||
},
|
|
||||||
timeout: 20000,
|
|
||||||
}, (res) => {
|
|
||||||
if ([301,302,303,307,308].includes(res.statusCode) && res.headers.location) {
|
|
||||||
let loc = res.headers.location;
|
|
||||||
if (!loc.startsWith('http')) {
|
|
||||||
const b = new URL(url);
|
|
||||||
loc = loc.startsWith('/') ? `${b.protocol}//${b.host}${loc}` : `${b.protocol}//${b.host}/${loc}`;
|
|
||||||
}
|
|
||||||
res.resume();
|
|
||||||
return fetchHtml(loc, redirects + 1).then(resolve).catch(reject);
|
|
||||||
}
|
|
||||||
let body = '';
|
|
||||||
res.setEncoding('utf8');
|
|
||||||
res.on('data', d => { if (body.length < 5000000) body += d; });
|
|
||||||
res.on('end', () => resolve({ status: res.statusCode, body, finalUrl: url }));
|
|
||||||
res.on('error', reject);
|
|
||||||
});
|
|
||||||
req.on('error', reject);
|
|
||||||
req.on('timeout', () => { req.destroy(); reject(new Error('Timeout')); });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── GET /api/tools/paywallkiller/render?url=...&token=... ────────────────
|
|
||||||
// Holt archive.ph Seite, entfernt Banner + schreibt Links um → sauberes HTML
|
|
||||||
// Token als Query-Parameter da der Endpoint direkt im Browser geöffnet wird
|
|
||||||
router.get('/render', (req, res, next) => {
|
|
||||||
const jwt = require('jsonwebtoken');
|
|
||||||
const SECRET = process.env.JWT_SECRET || 'dev-secret';
|
|
||||||
const token = req.query.token || (req.headers.authorization || '').replace('Bearer ', '');
|
|
||||||
if (!token) return res.status(401).send('Nicht eingeloggt');
|
|
||||||
try { req.user = jwt.verify(token, SECRET); next(); }
|
|
||||||
catch { return res.status(401).send('Token ungültig'); }
|
|
||||||
}, async (req, res) => {
|
|
||||||
const { url } = req.query;
|
|
||||||
if (!url) return res.status(400).send('url fehlt');
|
|
||||||
|
|
||||||
// Nur archive.ph URLs erlauben
|
|
||||||
if (!/^https?:\/\/archive\.(ph|is|today|fo|li|vn|md|gg)\//.test(url)) {
|
|
||||||
return res.status(400).send('Nur archive.ph URLs erlaubt');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await fetchHtml(url);
|
|
||||||
if (result.status !== 200) {
|
|
||||||
return res.status(502).send(`archive.ph antwortete mit ${result.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
let html = result.body;
|
|
||||||
|
|
||||||
// ── Banner entfernen ──────────────────────────────────────────────────
|
|
||||||
// Der archive.ph Banner hat die ID "HEADER" oder ist ein <div> ganz oben
|
|
||||||
html = html
|
|
||||||
// Kompletten Wayback/archive Header-Block entfernen
|
|
||||||
.replace(/<div[^>]*id=["']HEADER["'][^>]*>[\s\S]*?<\/div>/gi, '')
|
|
||||||
.replace(/<div[^>]*id=["']wm-ipp[^"']*["'][^>]*>[\s\S]*?<\/div>/gi, '')
|
|
||||||
// archive.ph spezifische Toolbar
|
|
||||||
.replace(/<div[^>]*class=["'][^"']*THUMBS-BLOCK[^"']*["'][^>]*>[\s\S]*?<\/div>/gi, '')
|
|
||||||
.replace(/<div[^>]*id=["']tools["'][^>]*>[\s\S]*?<\/div>/gi, '');
|
|
||||||
|
|
||||||
// ── Links zurück auf Originale umschreiben ────────────────────────────
|
|
||||||
// archive.ph schreibt Links um zu: https://archive.ph/TIMESTAMP/https://original.url
|
|
||||||
// Wir extrahieren die Original-URL
|
|
||||||
html = html.replace(
|
|
||||||
/https?:\/\/archive\.(?:ph|is|today|fo|li|vn|md|gg)\/[a-zA-Z0-9]+\/(https?:\/\/[^"'\s>]+)/g,
|
|
||||||
'$1'
|
|
||||||
);
|
|
||||||
// Auch relative Archive-Links
|
|
||||||
html = html.replace(
|
|
||||||
/href="\/[a-zA-Z0-9]{4,10}\/(https?:\/\/[^"]+)"/g,
|
|
||||||
'href="$1"'
|
|
||||||
);
|
|
||||||
|
|
||||||
// ── CSS für sauberes Drucken injizieren ───────────────────────────────
|
|
||||||
const printCss = `
|
|
||||||
<style id="pwk-print-style">
|
|
||||||
/* Archive.ph Banner + Toolbar ausblenden */
|
|
||||||
#HEADER, #tools, #THUMBS-BLOCK,
|
|
||||||
div[id^="HEADER"], div[id^="tools"],
|
|
||||||
.TEXT-BLOCK > div:first-child,
|
|
||||||
[class*="THUMBS"], [class*="tools-block"] {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
/* Druck-Optimierung */
|
|
||||||
@media print {
|
|
||||||
#HEADER, #tools, [id^="wm-"], [class*="archive-toolbar"],
|
|
||||||
div:first-child > div:first-child { display: none !important; }
|
|
||||||
body { margin: 0 !important; }
|
|
||||||
a[href] { color: inherit !important; text-decoration: none !important; }
|
|
||||||
a[href]::after { content: "" !important; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<script>
|
|
||||||
// Banner sofort nach Load entfernen
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
['HEADER','tools','THUMBS-BLOCK'].forEach(function(id) {
|
|
||||||
var el = document.getElementById(id);
|
|
||||||
if (el) el.remove();
|
|
||||||
});
|
|
||||||
// Ersten Child-Div entfernen wenn er archive-Toolbar ist
|
|
||||||
var first = document.body && document.body.firstElementChild;
|
|
||||||
if (first && (first.id === 'HEADER' || first.className.includes('HEADER'))) {
|
|
||||||
first.remove();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>`;
|
|
||||||
|
|
||||||
// CSS vor </head> einfügen
|
|
||||||
if (html.includes('</head>')) {
|
|
||||||
html = html.replace('</head>', printCss + '</head>');
|
|
||||||
} else {
|
|
||||||
html = printCss + html;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Base-Tag setzen damit relative URLs funktionieren
|
|
||||||
const baseTag = `<base href="${url}">`;
|
|
||||||
if (html.includes('<head>')) {
|
|
||||||
html = html.replace('<head>', '<head>' + baseTag);
|
|
||||||
}
|
|
||||||
|
|
||||||
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
||||||
// X-Frame-Options entfernen damit wir es anzeigen können
|
|
||||||
res.removeHeader('X-Frame-Options');
|
|
||||||
res.send(html);
|
|
||||||
} catch (err) {
|
|
||||||
res.status(500).send(`Fehler: ${err.message}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── GET /api/tools/paywallkiller/cdx?url=... ──────────────────────────────
|
|
||||||
router.get('/cdx', authenticate, async (req, res) => {
|
|
||||||
const { url } = req.query;
|
|
||||||
if (!url) return res.status(400).json({ error: 'url fehlt' });
|
|
||||||
let targetUrl = url.trim();
|
|
||||||
if (!targetUrl.startsWith('http')) targetUrl = 'https://' + targetUrl;
|
|
||||||
const cdxUrl = `https://web.archive.org/cdx/search/cdx?url=${encodeURIComponent(targetUrl)}&output=json&limit=1&fl=timestamp&filter=statuscode:200&fastLatest=true`;
|
|
||||||
try {
|
|
||||||
const r = await fetchHtml(cdxUrl);
|
|
||||||
if (r.status !== 200) return res.status(502).json({ error: `CDX ${r.status}` });
|
|
||||||
const data = JSON.parse(r.body);
|
|
||||||
if (!Array.isArray(data) || data.length < 2 || !data[1]?.[0]) return res.json({ found: false });
|
|
||||||
return res.json({ found: true, waybackUrl: `https://web.archive.org/web/${data[1][0]}/${targetUrl}` });
|
|
||||||
} catch (err) {
|
|
||||||
return res.status(500).json({ error: err.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@@ -9,13 +9,10 @@ const C = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function PaywallKiller() {
|
export default function PaywallKiller() {
|
||||||
const [inputUrl, setInputUrl] = useState('');
|
const [inputUrl, setInputUrl] = useState('');
|
||||||
const [snapshotUrl, setSnapshotUrl] = useState('');
|
const [archiveUrl, setArchiveUrl] = useState('');
|
||||||
const [status, setStatus] = useState('idle'); // idle | step1 | step2
|
const [done, setDone] = useState(false);
|
||||||
const [archiveUrl, setArchiveUrl] = useState('');
|
const inputRef = useRef(null);
|
||||||
const [captchaDone, setCaptchaDone] = useState(false);
|
|
||||||
const inputRef = useRef(null);
|
|
||||||
const snapshotRef = useRef(null);
|
|
||||||
|
|
||||||
function getTargetUrl() {
|
function getTargetUrl() {
|
||||||
const url = inputUrl.trim();
|
const url = inputUrl.trim();
|
||||||
@@ -25,37 +22,16 @@ export default function PaywallKiller() {
|
|||||||
function handleSearch() {
|
function handleSearch() {
|
||||||
if (!inputUrl.trim()) return;
|
if (!inputUrl.trim()) return;
|
||||||
setArchiveUrl(`https://archive.ph/newest/${getTargetUrl()}`);
|
setArchiveUrl(`https://archive.ph/newest/${getTargetUrl()}`);
|
||||||
setStatus('step1');
|
setDone(false);
|
||||||
setCaptchaDone(false);
|
|
||||||
setSnapshotUrl('');
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleCaptchaDone() {
|
|
||||||
setCaptchaDone(true);
|
|
||||||
setStatus('step2');
|
|
||||||
setTimeout(() => snapshotRef.current?.focus(), 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRenderUrl() {
|
|
||||||
const snap = snapshotUrl.trim();
|
|
||||||
if (snap && snap.startsWith('https://archive.')) {
|
|
||||||
const token = localStorage.getItem('sk_token') || '';
|
|
||||||
return `/api/tools/paywallkiller/render?url=${encodeURIComponent(snap)}&token=${encodeURIComponent(token)}`;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleReset() {
|
function handleReset() {
|
||||||
setInputUrl('');
|
setInputUrl('');
|
||||||
setSnapshotUrl('');
|
|
||||||
setStatus('idle');
|
|
||||||
setArchiveUrl('');
|
setArchiveUrl('');
|
||||||
setCaptchaDone(false);
|
setDone(false);
|
||||||
setTimeout(() => inputRef.current?.focus(), 50);
|
setTimeout(() => inputRef.current?.focus(), 50);
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderUrl = getRenderUrl();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: '20px 16px', maxWidth: 640, margin: '0 auto' }}>
|
<div style={{ padding: '20px 16px', maxWidth: 640, margin: '0 auto' }}>
|
||||||
|
|
||||||
@@ -64,114 +40,91 @@ export default function PaywallKiller() {
|
|||||||
<span style={{ fontSize: 20 }}>🔓</span>
|
<span style={{ fontSize: 20 }}>🔓</span>
|
||||||
<span style={{ color: C.accent, fontFamily: 'monospace', fontSize: 13, letterSpacing: 1 }}>PAYWALL-KILLER</span>
|
<span style={{ color: C.accent, fontFamily: 'monospace', fontSize: 13, letterSpacing: 1 }}>PAYWALL-KILLER</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ color: C.muted, fontSize: 11, fontFamily: 'monospace' }}>archive.ph · Bereinigt · Als PDF drucken</div>
|
<div style={{ color: C.muted, fontSize: 11, fontFamily: 'monospace' }}>archive.ph · Als PDF speichern</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* URL-Eingabe */}
|
{/* Eingabe */}
|
||||||
<div style={{ ...S.card, marginBottom: 16 }}>
|
<div style={{ ...S.card, marginBottom: 16 }}>
|
||||||
<div style={{ ...S.head, marginBottom: 10 }}>ARTIKEL-URL</div>
|
<div style={{ ...S.head, marginBottom: 10 }}>ARTIKEL-URL</div>
|
||||||
<input
|
<input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
value={inputUrl}
|
value={inputUrl}
|
||||||
onChange={e => setInputUrl(e.target.value)}
|
onChange={e => setInputUrl(e.target.value)}
|
||||||
onKeyDown={e => e.key === 'Enter' && status === 'idle' && handleSearch()}
|
onKeyDown={e => e.key === 'Enter' && handleSearch()}
|
||||||
placeholder="URL einfügen (Long-Press → Einfügen)"
|
placeholder="URL einfügen (Long-Press → Einfügen)"
|
||||||
style={{ ...S.inp, width: '100%', boxSizing: 'border-box', marginBottom: 10 }}
|
style={{ ...S.inp, width: '100%', boxSizing: 'border-box', marginBottom: 10 }}
|
||||||
/>
|
/>
|
||||||
<div style={{ display: 'flex', gap: 8 }}>
|
<div style={{ display: 'flex', gap: 8 }}>
|
||||||
{status === 'idle' ? (
|
<button onClick={handleSearch} disabled={!inputUrl.trim()}
|
||||||
<button onClick={handleSearch} disabled={!inputUrl.trim()}
|
style={{ ...S.btn(C.accent), flex: 1, opacity: !inputUrl.trim() ? 0.45 : 1, cursor: !inputUrl.trim() ? 'default' : 'pointer' }}>
|
||||||
style={{ ...S.btn(C.accent), flex: 1, opacity: !inputUrl.trim() ? 0.45 : 1, cursor: !inputUrl.trim() ? 'default' : 'pointer' }}>
|
🔍 Archiv suchen
|
||||||
🔍 Archiv suchen
|
</button>
|
||||||
</button>
|
{archiveUrl && (
|
||||||
) : (
|
|
||||||
<button onClick={handleReset}
|
<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 }}>
|
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>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Schritt 1: archive.ph öffnen */}
|
{/* Ergebnis */}
|
||||||
{(status === 'step1' || status === 'step2') && (
|
{archiveUrl && (
|
||||||
<div style={{ ...S.card, borderColor: status === 'step2' ? `${C.success}44` : `${C.accent}55`, marginBottom: 12 }}>
|
<div style={{ ...S.card, borderColor: `${C.accent}55`, marginBottom: 12 }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10 }}>
|
<div style={{ ...S.head, marginBottom: 12 }}>ARTIKEL ÖFFNEN & ALS PDF SPEICHERN</div>
|
||||||
<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 }}>1</span>
|
|
||||||
<span style={{ color: C.accent, fontFamily: 'monospace', fontSize: 12 }}>ARCHIVE.PH ÖFFNEN</span>
|
|
||||||
{status === 'step2' && <span style={{ color: C.success, fontFamily: 'monospace', fontSize: 10 }}>✓ erledigt</span>}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{status === 'step1' && (
|
{/* Schritt 1 */}
|
||||||
<>
|
<div style={{ display: 'flex', gap: 10, marginBottom: 16 }}>
|
||||||
<div style={{ color: C.muted, fontSize: 11, fontFamily: 'monospace', lineHeight: 1.7, marginBottom: 12 }}>
|
<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>
|
||||||
Öffne archive.ph. Falls ein CAPTCHA erscheint: lösen und warten bis der Artikel vollständig sichtbar ist. Dann die <strong style={{ color: 'rgba(255,255,255,0.6)' }}>URL aus der Adressleiste</strong> kopieren – sie sieht dann so aus:<br/>
|
<div style={{ flex: 1 }}>
|
||||||
<span style={{ color: C.accent }}>archive.ph/AbCdE</span> oder <span style={{ color: C.accent }}>archive.ph/20260608.../https://...</span>
|
<div style={{ color: 'rgba(255,255,255,0.7)', fontFamily: 'monospace', fontSize: 11, marginBottom: 8 }}>
|
||||||
|
Öffne archive.ph – falls CAPTCHA erscheint einmalig lösen:
|
||||||
</div>
|
</div>
|
||||||
<a href={archiveUrl} target="_blank" rel="noreferrer"
|
<a href={archiveUrl} target="_blank" rel="noreferrer"
|
||||||
style={{ ...S.btn(C.accent), display: 'block', textAlign: 'center', textDecoration: 'none', boxSizing: 'border-box', fontSize: 13, marginBottom: 12 }}>
|
style={{ ...S.btn(C.accent), display: 'block', textAlign: 'center', textDecoration: 'none', boxSizing: 'border-box', fontSize: 13 }}>
|
||||||
🌐 archive.ph öffnen
|
🌐 Artikel auf archive.ph öffnen
|
||||||
</a>
|
</a>
|
||||||
<button onClick={handleCaptchaDone}
|
</div>
|
||||||
style={{ ...S.btn(C.success), width: '100%', fontSize: 12 }}>
|
|
||||||
✅ Artikel ist sichtbar → weiter
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Schritt 2: Snapshot-URL einfügen + bereinigt öffnen */}
|
|
||||||
{status === 'step2' && (
|
|
||||||
<div style={{ ...S.card, borderColor: `${C.success}55`, marginBottom: 12 }}>
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 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 }}>2</span>
|
|
||||||
<span style={{ color: C.success, fontFamily: 'monospace', fontSize: 12 }}>SNAPSHOT-URL EINFÜGEN</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ color: C.muted, fontSize: 11, fontFamily: 'monospace', lineHeight: 1.6, marginBottom: 10 }}>
|
{/* Schritt 2 */}
|
||||||
Kopiere die URL aus der Adressleiste von archive.ph und füge sie hier ein:
|
<div style={{ display: 'flex', gap: 10 }}>
|
||||||
</div>
|
<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 }}>
|
||||||
<input
|
<div style={{ color: 'rgba(255,255,255,0.7)', fontFamily: 'monospace', fontSize: 11, marginBottom: 8 }}>
|
||||||
ref={snapshotRef}
|
Wenn der Artikel sichtbar ist, als PDF speichern:
|
||||||
value={snapshotUrl}
|
</div>
|
||||||
onChange={e => setSnapshotUrl(e.target.value)}
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||||
placeholder="https://archive.ph/AbCdE"
|
{[
|
||||||
style={{ ...S.inp, width: '100%', boxSizing: 'border-box', marginBottom: 12, fontSize: 12 }}
|
['📱', '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'],
|
||||||
{renderUrl ? (
|
].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)' }}>
|
||||||
<a href={renderUrl} target="_blank" rel="noreferrer"
|
<div style={{ color: 'rgba(255,255,255,0.6)', fontFamily: 'monospace', fontSize: 11, marginBottom: 2 }}>{icon} {label}</div>
|
||||||
style={{ ...S.btn(C.success), display: 'block', textAlign: 'center', textDecoration: 'none', boxSizing: 'border-box', fontSize: 13, marginBottom: 10 }}>
|
<div style={{ color: C.muted, fontFamily: 'monospace', fontSize: 10, lineHeight: 1.5 }}>{desc}</div>
|
||||||
🧹 Bereinigt öffnen (ohne Banner & Archiv-Links)
|
</div>
|
||||||
</a>
|
))}
|
||||||
<div style={{ padding: '8px 12px', background: 'rgba(255,255,255,0.02)', borderRadius: 6, border: '1px solid rgba(255,255,255,0.06)' }}>
|
</div>
|
||||||
<div style={{ color: 'rgba(255,255,255,0.4)', fontFamily: 'monospace', fontSize: 10, marginBottom: 4 }}>DANN ALS PDF DRUCKEN</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: C.muted, fontFamily: 'monospace', fontSize: 10, lineHeight: 1.6 }}>
|
<div style={{ color: 'rgba(255,255,255,0.3)', fontFamily: 'monospace', fontSize: 10, lineHeight: 1.5 }}>
|
||||||
📱 Android: Menü → Teilen → Drucken → Als PDF · Kopf-/Fußzeilen aus{'\n'}
|
ℹ️ Der archive.ph-Balken oben ist Teil der archivierten Seite und kann nicht automatisch entfernt werden. Im Druckdialog "Kopf-/Fußzeilen" deaktivieren hält das PDF sauber.
|
||||||
🍎 iOS: Teilen → Als PDF sichern{'\n'}
|
|
||||||
💻 Desktop: Strg+P → Als PDF · Kopf-/Fußzeilen aus
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div style={{ color: C.muted, fontSize: 11, fontFamily: 'monospace', opacity: 0.5 }}>
|
|
||||||
Snapshot-URL oben einfügen um fortzufahren...
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{status === 'idle' && (
|
{/* Idle-Info */}
|
||||||
|
{!archiveUrl && (
|
||||||
<div style={{ ...S.card }}>
|
<div style={{ ...S.card }}>
|
||||||
<div style={{ ...S.head, marginBottom: 10 }}>SO FUNKTIONIERT ES</div>
|
<div style={{ ...S.head, marginBottom: 10 }}>SO FUNKTIONIERT ES</div>
|
||||||
{[
|
{[
|
||||||
['1', 'URL des gesperrten Artikels einfügen'],
|
['1', 'URL des gesperrten Artikels einfügen'],
|
||||||
['2', 'archive.ph öffnen, CAPTCHA einmalig lösen'],
|
['2', 'Artikel auf archive.ph öffnen'],
|
||||||
['3', 'URL aus Adressleiste kopieren und hier einfügen'],
|
['3', 'CAPTCHA einmalig lösen (danach dauerhaft gespeichert)'],
|
||||||
['4', 'Bereinigt öffnen (kein Banner, normale Links) → als PDF drucken'],
|
['4', 'Über Drucken-Funktion als PDF speichern'],
|
||||||
].map(([num, text]) => (
|
].map(([num, text]) => (
|
||||||
<div key={num} style={{ display: 'flex', gap: 10, alignItems: 'flex-start', marginBottom: 8 }}>
|
<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={{ 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>
|
||||||
|
|||||||
Reference in New Issue
Block a user