Huge refactor, submission system addition & security improvements. +Implementation of moderation cmds.

This commit is contained in:
unknown
2026-05-22 21:46:06 +02:00
parent 12a0035699
commit 2129081599
32 changed files with 3426 additions and 106 deletions

View File

@@ -9,6 +9,8 @@
"version": "0.1.0",
"dependencies": {
"dompurify": "^3.0.0",
"highlight.js": "^11.11.1",
"mammoth": "^1.12.0",
"marked": "^12.0.0"
},
"devDependencies": {
@@ -467,6 +469,15 @@
"devOptional": true,
"license": "MIT"
},
"node_modules/@xmldom/xmldom": {
"version": "0.8.13",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz",
"integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/acorn": {
"version": "8.16.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
@@ -480,6 +491,15 @@
"node": ">=0.4.0"
}
},
"node_modules/argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"license": "MIT",
"dependencies": {
"sprintf-js": "~1.0.2"
}
},
"node_modules/aria-query": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz",
@@ -500,6 +520,32 @@
"node": ">= 0.4"
}
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/bluebird": {
"version": "3.4.7",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz",
"integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==",
"license": "MIT"
},
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
@@ -510,6 +556,12 @@
"node": ">=6"
}
},
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"license": "MIT"
},
"node_modules/deepmerge": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
@@ -537,6 +589,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/dingbat-to-unicode": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dingbat-to-unicode/-/dingbat-to-unicode-1.0.1.tgz",
"integrity": "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==",
"license": "BSD-2-Clause"
},
"node_modules/dompurify": {
"version": "3.4.5",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.5.tgz",
@@ -546,6 +604,15 @@
"@types/trusted-types": "^2.0.7"
}
},
"node_modules/duck": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/duck/-/duck-0.1.12.tgz",
"integrity": "sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==",
"license": "BSD",
"dependencies": {
"underscore": "^1.13.1"
}
},
"node_modules/esm-env": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
@@ -604,6 +671,27 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/highlight.js": {
"version": "11.11.1",
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
"integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
"license": "MIT"
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/is-reference": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
@@ -614,6 +702,33 @@
"@types/estree": "^1.0.6"
}
},
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"license": "MIT"
},
"node_modules/jszip": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
"license": "(MIT OR GPL-3.0-or-later)",
"dependencies": {
"lie": "~3.3.0",
"pako": "~1.0.2",
"readable-stream": "~2.3.6",
"setimmediate": "^1.0.5"
}
},
"node_modules/lie": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
"license": "MIT",
"dependencies": {
"immediate": "~3.0.5"
}
},
"node_modules/lightningcss": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
@@ -894,6 +1009,17 @@
"dev": true,
"license": "MIT"
},
"node_modules/lop": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/lop/-/lop-0.4.2.tgz",
"integrity": "sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw==",
"license": "BSD-2-Clause",
"dependencies": {
"duck": "^0.1.12",
"option": "~0.2.1",
"underscore": "^1.13.1"
}
},
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@@ -904,6 +1030,30 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
"node_modules/mammoth": {
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/mammoth/-/mammoth-1.12.0.tgz",
"integrity": "sha512-cwnK1RIcRdDMi2HRx2EXGYlxqIEh0Oo3bLhorgnsVJi2UkbX1+jKxuBNR9PC5+JaX7EkmJxFPmo6mjLpqShI2w==",
"license": "BSD-2-Clause",
"dependencies": {
"@xmldom/xmldom": "^0.8.6",
"argparse": "~1.0.3",
"base64-js": "^1.5.1",
"bluebird": "~3.4.0",
"dingbat-to-unicode": "^1.0.1",
"jszip": "^3.7.1",
"lop": "^0.4.2",
"path-is-absolute": "^1.0.0",
"underscore": "^1.13.1",
"xmlbuilder": "^10.0.0"
},
"bin": {
"mammoth": "bin/mammoth"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/marked": {
"version": "12.0.2",
"resolved": "https://registry.npmjs.org/marked/-/marked-12.0.2.tgz",
@@ -946,6 +1096,27 @@
],
"license": "MIT"
},
"node_modules/option": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/option/-/option-0.2.4.tgz",
"integrity": "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==",
"license": "BSD-2-Clause"
},
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"license": "(MIT AND Zlib)"
},
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -995,6 +1166,27 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"license": "MIT"
},
"node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"license": "MIT",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/rolldown": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.2.tgz",
@@ -1029,6 +1221,18 @@
"@rolldown/binding-win32-x64-msvc": "1.0.2"
}
},
"node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
"license": "MIT"
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -1039,6 +1243,21 @@
"node": ">=0.10.0"
}
},
"node_modules/sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
"license": "BSD-3-Clause"
},
"node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/svelte": {
"version": "5.55.9",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.9.tgz",
@@ -1092,6 +1311,18 @@
"license": "0BSD",
"optional": true
},
"node_modules/underscore": {
"version": "1.13.8",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz",
"integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==",
"license": "MIT"
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/vite": {
"version": "8.0.14",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.14.tgz",
@@ -1190,6 +1421,15 @@
}
}
},
"node_modules/xmlbuilder": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz",
"integrity": "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==",
"license": "MIT",
"engines": {
"node": ">=4.0"
}
},
"node_modules/zimmerframe": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz",

View File

@@ -14,6 +14,8 @@
},
"dependencies": {
"dompurify": "^3.0.0",
"highlight.js": "^11.11.1",
"mammoth": "^1.12.0",
"marked": "^12.0.0"
}
}

View File

@@ -0,0 +1,92 @@
<script>
import { detectLanguage } from '../lib/lang.js'
import hljs from 'highlight.js'
import 'highlight.js/styles/atom-one-dark.css'
let { src, rawUrl = '', fileName = '' } = $props()
let text = $state('')
let loading = $state(true)
let lang = $derived(detectLanguage(fileName))
$effect(() => {
fetch(src)
.then(r => r.text())
.then(t => {
text = t
loading = false
if (lang) {
requestAnimationFrame(() => {
const block = document.querySelector('.code-viewer code')
if (block) hljs.highlightElement(block)
})
}
})
.catch(() => {
text = 'Failed to load code content.'
loading = false
})
})
</script>
<div class="code-viewer">
{#if fileName || rawUrl}
<div class="header">
{#if fileName}<span class="label">{fileName}</span>{/if}
{#if rawUrl}<a class="raw-btn" href={rawUrl} target="_blank">[ Raw ]</a>{/if}
</div>
{/if}
{#if loading}
<p>Loading code...</p>
{:else}
<pre><code class={lang ? `language-${lang}` : ''}>{text}</code></pre>
{/if}
</div>
<style>
.code-viewer {
max-width: 1000px;
margin: 24px auto;
padding: 16px;
background: var(--retro-panel);
border: 3px solid var(--retro-border);
box-shadow: 6px 6px 0px var(--retro-shadow);
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 2px solid var(--retro-border);
}
.label {
font-family: 'Press Start 2P', cursive;
font-size: 0.6rem;
color: var(--retro-green);
}
.raw-btn {
font-family: 'Press Start 2P', cursive;
font-size: 0.5rem;
padding: 6px 10px;
border: 2px solid var(--retro-border);
background: var(--retro-panel);
color: var(--retro-fg);
text-decoration: none;
box-shadow: 2px 2px 0px rgba(0,0,0,0.15);
}
.raw-btn:hover {
background: var(--retro-green);
color: #fff;
}
pre {
margin: 0;
overflow-x: auto;
background: #1e1e1e;
padding: 12px;
border: 2px solid var(--retro-border);
}
code {
font-family: 'Courier New', Courier, monospace;
font-size: 0.9rem;
}
</style>

View File

@@ -0,0 +1,98 @@
<script>
let { src, downloadUrl, file } = $props()
let html = $state('')
let loading = $state(true)
let error = $state('')
$effect(() => {
fetch(src)
.then(r => r.arrayBuffer())
.then(buf => import('mammoth').then(m => m.default.convertToHtml({ arrayBuffer: buf })))
.then(result => {
html = result.value
loading = false
})
.catch(() => {
error = 'Failed to render DOCX.'
loading = false
})
})
</script>
<div class="docx-viewer">
<div class="header">
<span class="label">[ DOCX ]</span>
<a class="raw-btn" href={downloadUrl} download={file.name}>Download</a>
</div>
{#if loading}
<p>Loading DOCX...</p>
{:else if error}
<p class="error">{error}</p>
<a class="btn" href={downloadUrl} download={file.name}>Download</a>
{:else}
<div class="docx-content">{@html html}</div>
{/if}
</div>
<style>
.docx-viewer {
max-width: 900px;
margin: 24px auto;
padding: 24px;
background: var(--retro-panel);
border: 3px solid var(--retro-border);
box-shadow: 6px 6px 0px var(--retro-shadow);
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 2px solid var(--retro-border);
}
.label {
font-family: 'Press Start 2P', cursive;
font-size: 0.6rem;
color: var(--retro-green);
}
.raw-btn {
font-family: 'Press Start 2P', cursive;
font-size: 0.5rem;
padding: 6px 10px;
border: 2px solid var(--retro-border);
background: var(--retro-panel);
color: var(--retro-fg);
text-decoration: none;
box-shadow: 2px 2px 0px rgba(0,0,0,0.15);
}
.raw-btn:hover {
background: var(--retro-green);
color: #fff;
}
.docx-content {
font-size: 1rem;
line-height: 1.6;
}
.docx-content :global(p) { margin: 0.8em 0; }
.docx-content :global(table) { border-collapse: collapse; width: 100%; }
.docx-content :global(td), .docx-content :global(th) { border: 1px solid #ccc; padding: 6px; }
.error { color: var(--retro-danger); }
.btn {
display: inline-block;
font-family: 'Press Start 2P', cursive;
font-size: 0.55rem;
padding: 10px 14px;
border: 3px solid var(--retro-border);
background: var(--retro-panel);
color: var(--retro-fg);
text-decoration: none;
text-align: center;
box-shadow: 3px 3px 0px rgba(0,0,0,0.15);
margin-top: 8px;
}
.btn:hover {
background: var(--retro-green);
color: #fff;
}
</style>

View File

@@ -1,9 +1,14 @@
<script>
let { src, name } = $props()
let failed = $state(false)
</script>
<div class="image-viewer">
<img {src} alt={name} decoding="async" loading="eager" />
{#if failed}
<div class="image-error">[ Failed to load image ]</div>
{:else}
<img {src} alt={name} decoding="async" loading="eager" onerror={() => failed = true} />
{/if}
</div>
<style>
@@ -19,4 +24,10 @@
box-shadow: 6px 6px 0px var(--retro-shadow);
image-rendering: auto;
}
.image-error {
color: var(--retro-danger);
font-family: var(--retro-font, monospace);
padding: 24px;
text-align: center;
}
</style>

View File

@@ -1,12 +1,17 @@
<script>
import { fileUrl } from '../lib/api.js'
import { fileUrl, rawUrl } from '../lib/api.js'
import { detectLanguage } from '../lib/lang.js'
import ImageViewer from './ImageViewer.svelte'
import VideoPlayer from './VideoPlayer.svelte'
import AudioPlayer from './AudioPlayer.svelte'
import MarkdownRenderer from './MarkdownRenderer.svelte'
import TextViewer from './TextViewer.svelte'
import CodeViewer from './CodeViewer.svelte'
import DocumentCard from './DocumentCard.svelte'
import ExecutableWarning from './ExecutableWarning.svelte'
import SensitiveWarning from './SensitiveWarning.svelte'
import PdfViewer from './PdfViewer.svelte'
import DocxViewer from './DocxViewer.svelte'
let { files, cxid, password = '' } = $props()
@@ -17,7 +22,11 @@
if (flags & 4) return 'audio'
if (flags & 8) return 'markdown'
if (flags & 16) return 'text'
if (flags & 32) {
return file.mime === 'application/pdf' ? 'pdf' : 'docx'
}
if (flags & 64 || flags & 128) return 'dangerous'
if (flags & 512) return 'sensitive'
return 'document'
}
</script>
@@ -39,9 +48,19 @@
{:else if viewer === 'markdown'}
<MarkdownRenderer src={fileUrl(cxid, file.idx, false, password)} />
{:else if viewer === 'text'}
<TextViewer src={fileUrl(cxid, file.idx, false, password)} />
{#if detectLanguage(file.name)}
<CodeViewer src={fileUrl(cxid, file.idx, false, password)} rawUrl={rawUrl(cxid, file.idx, password)} fileName={file.name} />
{:else}
<TextViewer src={fileUrl(cxid, file.idx, false, password)} rawUrl={rawUrl(cxid, file.idx, password)} fileName={file.name} />
{/if}
{:else if viewer === 'pdf'}
<PdfViewer src={fileUrl(cxid, file.idx, false, password)} />
{:else if viewer === 'docx'}
<DocxViewer src={fileUrl(cxid, file.idx, false, password)} downloadUrl={fileUrl(cxid, file.idx, true, password)} {file} />
{:else if viewer === 'dangerous'}
<ExecutableWarning {file} downloadUrl={fileUrl(cxid, file.idx, true, password)} />
{:else if viewer === 'sensitive'}
<SensitiveWarning {file} downloadUrl={fileUrl(cxid, file.idx, true, password)} />
{:else}
<DocumentCard {file} downloadUrl={fileUrl(cxid, file.idx, true, password)} />
{/if}

View File

@@ -0,0 +1,23 @@
<script>
let { src } = $props()
</script>
<div class="pdf-viewer">
<embed src={src} type="application/pdf" />
</div>
<style>
.pdf-viewer {
max-width: 1000px;
margin: 24px auto;
padding: 16px;
background: var(--retro-panel);
border: 3px solid var(--retro-border);
box-shadow: 6px 6px 0px var(--retro-shadow);
}
embed {
width: 100%;
height: 80vh;
border: 2px solid var(--retro-border);
}
</style>

View File

@@ -0,0 +1,56 @@
<script>
import { formatSize } from '../lib/api.js'
let { file, downloadUrl } = $props()
</script>
<div class="warning-card">
<div class="badge">[ Sensitive Data ]</div>
<p class="name">{file.name}</p>
<p class="meta">{file.mime}{formatSize(file.size)}</p>
<p class="notice">
This file may contain sensitive data. Be careful when handling it.
</p>
<a class="btn" href={downloadUrl} download={file.name}>Download</a>
</div>
<style>
.warning-card {
max-width: 600px;
margin: 24px auto;
padding: 24px;
background: #fffdf5;
border: 3px solid #c78000;
box-shadow: 6px 6px 0px rgba(199,128,0,0.15);
text-align: center;
}
.badge {
font-family: 'Press Start 2P', cursive;
font-size: 0.6rem;
color: #c78000;
margin-bottom: 12px;
}
.name {
font-family: 'Press Start 2P', cursive;
font-size: 0.7rem;
word-break: break-all;
}
.meta {
font-size: 0.9rem;
color: #666;
margin: 8px 0;
}
.notice {
font-size: 1rem;
color: #555;
margin: 16px 0;
}
.btn {
border-color: #c78000;
color: #c78000;
}
.btn:hover {
background: #c78000;
color: #fff;
}
</style>

View File

@@ -1,5 +1,5 @@
<script>
let { src } = $props()
let { src, rawUrl = '', fileName = '' } = $props()
let text = $state('')
let loading = $state(true)
@@ -18,6 +18,12 @@
</script>
<div class="text-viewer">
{#if fileName || rawUrl}
<div class="header">
{#if fileName}<span class="label">{fileName}</span>{/if}
{#if rawUrl}<a class="raw-btn" href={rawUrl} target="_blank">[ Raw ]</a>{/if}
</div>
{/if}
{#if loading}
<p>Loading text...</p>
{:else}
@@ -34,6 +40,33 @@
border: 3px solid var(--retro-border);
box-shadow: 6px 6px 0px var(--retro-shadow);
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 2px solid var(--retro-border);
}
.label {
font-family: 'Press Start 2P', cursive;
font-size: 0.6rem;
color: var(--retro-green);
}
.raw-btn {
font-family: 'Press Start 2P', cursive;
font-size: 0.5rem;
padding: 6px 10px;
border: 2px solid var(--retro-border);
background: var(--retro-panel);
color: var(--retro-fg);
text-decoration: none;
box-shadow: 2px 2px 0px rgba(0,0,0,0.15);
}
.raw-btn:hover {
background: var(--retro-green);
color: #fff;
}
pre {
white-space: pre-wrap;
word-break: break-word;

View File

@@ -4,7 +4,9 @@
<div class="video-player">
<!-- svelte-ignore a11y_media_has_caption -->
<video controls preload="metadata" {src}></video>
<video controls preload="metadata" {src}>
<source src={src} type={mime} />
</video>
</div>
<style>

View File

@@ -1,10 +1,10 @@
// "window.location.origin"
const API_BASE = "http://127.0.0.1:8090";
export async function fetchMetadata(cxid) {
const res = await fetch(
`${API_BASE}/api/content/${encodeURIComponent(cxid)}`,
);
export async function fetchMetadata(cxid, password = "") {
let url = `${API_BASE}/api/content/${encodeURIComponent(cxid)}`;
if (password) url += `?sc=${encodeURIComponent(password)}`;
const res = await fetch(url);
if (!res.ok) {
const err = new Error(await res.text());
err.status = res.status;
@@ -34,6 +34,12 @@ export function fileUrl(cxid, fileIdx, download = false, password = "") {
return url;
}
export function rawUrl(cxid, fileIdx, password = "") {
let url = `${API_BASE}/api/content/${encodeURIComponent(cxid)}/file/${fileIdx}/raw`;
if (password) url += `?sc=${encodeURIComponent(password)}`;
return url;
}
export function formatSize(bytes) {
if (bytes === 0) return "0 B";
const units = ["B", "KB", "MB", "GB"];

16
frontend/src/lib/lang.js Normal file
View File

@@ -0,0 +1,16 @@
const EXT_TO_LANG = {
py: 'python', rs: 'rust', js: 'javascript', ts: 'typescript',
jsx: 'javascript', tsx: 'typescript', c: 'c', cpp: 'cpp', cc: 'cpp',
h: 'c', hpp: 'cpp', go: 'go', java: 'java', kt: 'kotlin',
swift: 'swift', rb: 'ruby', php: 'php', cs: 'csharp', scala: 'scala',
r: 'r', m: 'objectivec', mm: 'objectivec', pl: 'perl', lua: 'lua',
json: 'json', xml: 'xml', yaml: 'yaml', yml: 'yaml', toml: 'toml',
ini: 'ini', cfg: 'ini', sh: 'bash', bash: 'bash', ps1: 'powershell',
bat: 'batch', cmd: 'batch', sql: 'sql', dockerfile: 'dockerfile',
makefile: 'makefile', cmake: 'cmake',
};
export function detectLanguage(fileName) {
const ext = fileName.split('.').pop()?.toLowerCase();
return ext ? (EXT_TO_LANG[ext] || null) : null;
}

View File

@@ -64,6 +64,13 @@
<button onclick={submit} disabled={loading}>
{loading ? 'Loading...' : '[ Unlock ]'}
</button>
<details class="misc-section">
<summary>[ Misc ]</summary>
<div class="misc-content">
<a href="https://t.me/harmfulmeowbot?start=report" target="_blank" rel="noopener">Report Content</a>
</div>
</details>
</div>
<footer>
@@ -135,4 +142,31 @@
margin-top: 12px;
color: #777;
}
.misc-section {
margin-top: 8px;
border: 2px solid var(--retro-border);
padding: 8px 12px;
background: #f5f5f5;
}
.misc-section summary {
font-family: 'Press Start 2P', cursive;
font-size: 0.55rem;
color: var(--retro-green);
cursor: pointer;
user-select: none;
}
.misc-content {
margin-top: 8px;
display: flex;
flex-direction: column;
gap: 6px;
}
.misc-content a {
font-size: 0.9rem;
color: var(--retro-green);
text-decoration: none;
}
.misc-content a:hover {
text-decoration: underline;
}
</style>

View File

@@ -1,12 +1,17 @@
<script>
import { fetchMetadata, verifyPassword, fileUrl } from '../lib/api.js'
import { fetchMetadata, verifyPassword, fileUrl, rawUrl } from '../lib/api.js'
import { detectLanguage } from '../lib/lang.js'
import ImageViewer from '../components/ImageViewer.svelte'
import VideoPlayer from '../components/VideoPlayer.svelte'
import AudioPlayer from '../components/AudioPlayer.svelte'
import MarkdownRenderer from '../components/MarkdownRenderer.svelte'
import TextViewer from '../components/TextViewer.svelte'
import CodeViewer from '../components/CodeViewer.svelte'
import DocumentCard from '../components/DocumentCard.svelte'
import ExecutableWarning from '../components/ExecutableWarning.svelte'
import SensitiveWarning from '../components/SensitiveWarning.svelte'
import PdfViewer from '../components/PdfViewer.svelte'
import DocxViewer from '../components/DocxViewer.svelte'
import MixedGallery from '../components/MixedGallery.svelte'
let { cxid, sc } = $props()
@@ -28,28 +33,18 @@
phase = 'loading_meta'
error = ''
try {
const meta = await fetchMetadata(cxid)
const meta = await fetchMetadata(cxid, password)
metadata = meta
if (meta.has_password && !password) {
phase = 'rendering'
} catch (e) {
const status = e.status || 0
if (status === 401) {
phase = 'password_required'
return
}
if (meta.has_password) {
const ok = await verifyPassword(cxid, password)
if (!ok) {
phase = 'password_required'
error = 'Incorrect password.'
return
}
}
phase = 'rendering'
} catch (e) {
phase = 'error'
const status = e.status || 0
if (status === 404) {
error = '[ Not Found ] This content does not exist or has been removed.'
} else if (status === 401) {
error = '[ Unauthorized ] This content requires a password.'
} else if (status === 429) {
error = '[ Rate Limited ] Too many requests. Please wait.'
} else if (status >= 500) {
@@ -65,6 +60,7 @@
if (!password) return
const ok = await verifyPassword(cxid, password)
if (ok) {
metadata = await fetchMetadata(cxid, password)
phase = 'rendering'
} else {
error = 'Incorrect password.'
@@ -83,8 +79,12 @@
if (flags & 4) return 'audio'
if (flags & 8) return 'markdown'
if (flags & 16) return 'text'
if (flags & 32) {
return file.mime === 'application/pdf' ? 'pdf' : 'docx'
}
if (flags & 64) return 'executable'
if (flags & 128) return 'dangerous'
if (flags & 512) return 'sensitive'
return 'document'
}
</script>
@@ -133,9 +133,19 @@
{:else if viewer === 'markdown'}
<MarkdownRenderer src={fileUrl(cxid, file.idx, false, password)} />
{:else if viewer === 'text'}
<TextViewer src={fileUrl(cxid, file.idx, false, password)} />
{#if detectLanguage(file.name)}
<CodeViewer src={fileUrl(cxid, file.idx, false, password)} rawUrl={rawUrl(cxid, file.idx, password)} fileName={file.name} />
{:else}
<TextViewer src={fileUrl(cxid, file.idx, false, password)} rawUrl={rawUrl(cxid, file.idx, password)} fileName={file.name} />
{/if}
{:else if viewer === 'pdf'}
<PdfViewer src={fileUrl(cxid, file.idx, false, password)} />
{:else if viewer === 'docx'}
<DocxViewer src={fileUrl(cxid, file.idx, false, password)} downloadUrl={fileUrl(cxid, file.idx, true, password)} {file} />
{:else if viewer === 'executable' || viewer === 'dangerous'}
<ExecutableWarning {file} downloadUrl={fileUrl(cxid, file.idx, true, password)} />
{:else if viewer === 'sensitive'}
<SensitiveWarning {file} downloadUrl={fileUrl(cxid, file.idx, true, password)} />
{:else}
<DocumentCard {file} downloadUrl={fileUrl(cxid, file.idx, true, password)} />
{/if}