mirror of
https://github.com/thegeneralist01/archivr
synced 2026-10-09 12:55:00 +02:00
Merge branch 'feat/archive-text-with-title' into integration-all-three
# Conflicts: # crates/archivr-server/static/assets/index-C4rpqY7g.js # crates/archivr-server/static/index.html
This commit is contained in:
commit
73b0010d60
6 changed files with 112 additions and 51 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -6,8 +6,8 @@
|
||||||
<title>Archivr</title>
|
<title>Archivr</title>
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||||||
<link rel="icon" type="image/x-icon" href="/favicon.ico">
|
<link rel="icon" type="image/x-icon" href="/favicon.ico">
|
||||||
<script type="module" crossorigin src="/assets/index-CO8p3FL2.js"></script>
|
<script type="module" crossorigin src="/assets/index-C4rpqY7g.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-BLxoi9rt.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-BV7QPxEc.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,12 @@ import VideoPreview from './VideoPreview';
|
||||||
import IframePreview from './IframePreview';
|
import IframePreview from './IframePreview';
|
||||||
import ImagePreview from './ImagePreview';
|
import ImagePreview from './ImagePreview';
|
||||||
import TweetPreview from './TweetPreview';
|
import TweetPreview from './TweetPreview';
|
||||||
|
import TextPreview from './TextPreview';
|
||||||
|
|
||||||
const VIDEO_EXTS = new Set(['mp4', 'webm', 'mov', 'mkv', 'avi', 'm4v', 'ogv']);
|
const VIDEO_EXTS = new Set(['mp4', 'webm', 'mov', 'mkv', 'avi', 'm4v', 'ogv']);
|
||||||
const AUDIO_EXTS = new Set(['mp3', 'ogg', 'm4a', 'opus', 'wav', 'flac', 'aac']);
|
const AUDIO_EXTS = new Set(['mp3', 'ogg', 'm4a', 'opus', 'wav', 'flac', 'aac']);
|
||||||
const IMAGE_EXTS = new Set(['jpg', 'jpeg', 'png', 'gif', 'webp', 'avif', 'svg', 'bmp']);
|
const IMAGE_EXTS = new Set(['jpg', 'jpeg', 'png', 'gif', 'webp', 'avif', 'svg', 'bmp']);
|
||||||
|
const TEXT_EXTS = new Set(['md', 'markdown', 'txt']);
|
||||||
const CONTENT_TYPES = {
|
const CONTENT_TYPES = {
|
||||||
mp4: 'video/mp4', webm: 'video/webm', mov: 'video/quicktime',
|
mp4: 'video/mp4', webm: 'video/webm', mov: 'video/quicktime',
|
||||||
mkv: 'video/x-matroska', avi: 'video/x-msvideo', m4v: 'video/mp4', ogv: 'video/ogg',
|
mkv: 'video/x-matroska', avi: 'video/x-msvideo', m4v: 'video/mp4', ogv: 'video/ogg',
|
||||||
|
|
@ -131,7 +133,20 @@ export default function PreviewPanel({ archiveId, entry, detail, fullPage, onXAr
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7. Image
|
// 6b. Plain text / Markdown
|
||||||
|
if (TEXT_EXTS.has(ext)) {
|
||||||
|
return (
|
||||||
|
<div className="preview-panel" style={{ flex: 1, minHeight: 0, overflow: 'auto' }}>
|
||||||
|
<TextPreview
|
||||||
|
src={primaryMediaUrl}
|
||||||
|
mime={primaryArtifact.mime_type}
|
||||||
|
title={summary.title}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Image
|
||||||
if (IMAGE_EXTS.has(ext)) {
|
if (IMAGE_EXTS.has(ext)) {
|
||||||
return (
|
return (
|
||||||
<div className="preview-panel" style={{ height: '100%' }}>
|
<div className="preview-panel" style={{ height: '100%' }}>
|
||||||
|
|
|
||||||
46
frontend/src/components/TextPreview.jsx
Normal file
46
frontend/src/components/TextPreview.jsx
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the primary_media artifact of a text/document entry as plain text.
|
||||||
|
*
|
||||||
|
* v1 intentionally does NOT render Markdown — we don't want a Markdown parser
|
||||||
|
* dep just to unblock the "no preview available" fallback, and monospace text
|
||||||
|
* with visible fences reads fine for the note-length content this feature
|
||||||
|
* captures. Bump to a real renderer if/when Markdown-authored entries grow.
|
||||||
|
*/
|
||||||
|
export default function TextPreview({ src, mime, title }) {
|
||||||
|
const [text, setText] = useState(null)
|
||||||
|
const [error, setError] = useState(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false
|
||||||
|
setText(null)
|
||||||
|
setError(null)
|
||||||
|
fetch(src, { credentials: 'same-origin' })
|
||||||
|
.then(r => {
|
||||||
|
if (!r.ok) throw new Error(`HTTP ${r.status}`)
|
||||||
|
return r.text()
|
||||||
|
})
|
||||||
|
.then(t => { if (!cancelled) setText(t) })
|
||||||
|
.catch(e => { if (!cancelled) setError(e.message || String(e)) })
|
||||||
|
return () => { cancelled = true }
|
||||||
|
}, [src])
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="text-preview text-preview--error">
|
||||||
|
Failed to load text: {error}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (text === null) {
|
||||||
|
return <div className="text-preview text-preview--loading">Loading…</div>
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="text-preview">
|
||||||
|
{title && <h1 className="text-preview__title">{title}</h1>}
|
||||||
|
<pre className="text-preview__body">{text}</pre>
|
||||||
|
{mime && <div className="text-preview__mime">{mime}</div>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -3285,3 +3285,51 @@ body.has-audio-bar { padding-bottom: 56px; }
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Text / Markdown preview ────────────────────────────────────────────── */
|
||||||
|
.text-preview {
|
||||||
|
padding: 32px 40px 48px;
|
||||||
|
max-width: 780px;
|
||||||
|
margin: 0 auto;
|
||||||
|
font-family: var(--sans);
|
||||||
|
color: var(--ink);
|
||||||
|
overflow: auto;
|
||||||
|
height: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.text-preview--loading,
|
||||||
|
.text-preview--error {
|
||||||
|
padding: 24px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.text-preview--error { color: var(--accent); }
|
||||||
|
.text-preview__title {
|
||||||
|
font-family: var(--serif, var(--sans));
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0 0 20px;
|
||||||
|
line-height: 1.25;
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
.text-preview__body {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
word-break: break-word;
|
||||||
|
font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
|
||||||
|
font-size: 13.5px;
|
||||||
|
line-height: 1.65;
|
||||||
|
color: var(--ink);
|
||||||
|
background: var(--paper);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--r2);
|
||||||
|
padding: 16px 18px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.text-preview__mime {
|
||||||
|
margin-top: 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--muted);
|
||||||
|
letter-spacing: .04em;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue