Huge refactor, submission system addition & security improvements. +Implementation of moderation cmds.
This commit is contained in:
240
frontend/package-lock.json
generated
240
frontend/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"dompurify": "^3.0.0",
|
||||
"highlight.js": "^11.11.1",
|
||||
"mammoth": "^1.12.0",
|
||||
"marked": "^12.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
92
frontend/src/components/CodeViewer.svelte
Normal file
92
frontend/src/components/CodeViewer.svelte
Normal 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>
|
||||
98
frontend/src/components/DocxViewer.svelte
Normal file
98
frontend/src/components/DocxViewer.svelte
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
23
frontend/src/components/PdfViewer.svelte
Normal file
23
frontend/src/components/PdfViewer.svelte
Normal 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>
|
||||
56
frontend/src/components/SensitiveWarning.svelte
Normal file
56
frontend/src/components/SensitiveWarning.svelte
Normal 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>
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
16
frontend/src/lib/lang.js
Normal 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;
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user