Skip to content

Commit 44890d1

Browse files
committed
feat[frontend]: unify decrypt and display page
1 parent 61b4d2d commit 44890d1

File tree

10 files changed

+200
-119
lines changed

10 files changed

+200
-119
lines changed

frontend/components/CodeEditor.tsx

Lines changed: 41 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
// based on vanilla https://css-tricks.com/creating-an-editable-textarea-that-supports-syntax-highlighted-code/
1+
// inspired by https://css-tricks.com/creating-an-editable-textarea-that-supports-syntax-highlighted-code/
22

33
import React, { useEffect, useRef, useState } from "react"
44

55
import "../styles/highlight-theme-light.css"
6-
import { tst } from "../utils/overrides.js"
7-
import { escapeHtml } from "../../worker/common.js"
8-
import { usePrism } from "../utils/HighlightLoader.js"
6+
import { autoCompleteOverrides, inputOverrides, selectOverrides, tst } from "../utils/overrides.js"
7+
import { useHLJS, highlightHTML } from "../utils/HighlightLoader.js"
98
import { Autocomplete, AutocompleteItem, Input, Select, SelectItem } from "@heroui/react"
109

1110
import "../styles/highlight-theme-light.css"
@@ -79,13 +78,15 @@ export function CodeEditor({
7978
className,
8079
...rest
8180
}: CodeInputProps) {
82-
const refHighlighting = useRef<HTMLPreElement | null>(null)
81+
const refHighlighting = useRef<HTMLDivElement | null>(null)
8382
const refTextarea = useRef<HTMLTextAreaElement | null>(null)
8483

8584
const [heightPx, setHeightPx] = useState<number>(0)
86-
const hljs = usePrism()
85+
const hljs = useHLJS()
8786
const [tabSetting, setTabSettings] = useState<TabSetting>({ char: "space", width: 2 })
8887

88+
const lineCount = (content?.match(/\n/g)?.length || 0) + 1
89+
8990
function syncScroll() {
9091
refHighlighting.current!.scrollLeft = refTextarea.current!.scrollLeft
9192
refHighlighting.current!.scrollTop = refTextarea.current!.scrollTop
@@ -131,21 +132,22 @@ export function CodeEditor({
131132
}
132133
}, [])
133134

134-
function highlightedHTML() {
135-
if (hljs && lang && hljs.listLanguages().includes(lang) && lang !== "plaintext") {
136-
const highlighted = hljs.highlight(handleNewLines(content), { language: lang })
137-
return highlighted.value
138-
} else {
139-
return escapeHtml(content)
140-
}
141-
}
135+
const lineNumOffset = `${Math.floor(Math.log10(lineCount)) + 2}em`
142136

143137
return (
144138
<div className={className} {...rest}>
145139
<div className={"mb-2 gap-2 flex flex-row" + " "}>
146-
<Input type={"text"} label={"File name"} size={"sm"} value={filename || ""} onValueChange={setFilename} />
140+
<Input
141+
classNames={inputOverrides}
142+
type={"text"}
143+
label={"File name"}
144+
size={"sm"}
145+
value={filename || ""}
146+
onValueChange={setFilename}
147+
/>
147148
<Autocomplete
148149
className={"max-w-[10em]"}
150+
classNames={autoCompleteOverrides}
149151
label={"Language"}
150152
size={"sm"}
151153
defaultItems={hljs ? hljs.listLanguages().map((lang) => ({ key: lang })) : []}
@@ -160,7 +162,8 @@ export function CodeEditor({
160162
<Select
161163
size={"sm"}
162164
label={"Indent With"}
163-
className={"max-w-[10em]"}
165+
className={"max-w-[10em] text-foreground"}
166+
classNames={selectOverrides}
164167
selectedKeys={[formatTabSetting(tabSetting, false)]}
165168
onSelectionChange={(s) => {
166169
setTabSettings(parseTabSetting(s.currentKey as string)!)
@@ -171,19 +174,32 @@ export function CodeEditor({
171174
))}
172175
</Select>
173176
</div>
174-
<div className={`w-full bg-default-100 ${tst} rounded-xl p-2`}>
177+
<div className={`w-full bg-default-100 ${tst} rounded-xl p-2 relative`}>
175178
<div
176-
className={"relative w-full"}
179+
className={`relative w-full`}
177180
style={{ tabSize: tabSetting.char === "tab" ? tabSetting.width : undefined }}
178181
>
179-
<pre
180-
className={"w-full font-mono overflow-auto text-foreground top-0 left-0 absolute"}
181-
ref={refHighlighting}
182-
dangerouslySetInnerHTML={{ __html: highlightedHTML() }}
183-
style={{ height: `${heightPx}px` }}
184-
></pre>
182+
<div className={"w-full font-mono overflow-auto top-0 left-0 absolute"} ref={refHighlighting}>
183+
<pre
184+
className={`text-foreground ${tst}`}
185+
style={{ paddingLeft: lineNumOffset, height: `${heightPx}px` }}
186+
dangerouslySetInnerHTML={{ __html: highlightHTML(hljs, lang, handleNewLines(content)) }}
187+
></pre>
188+
<span
189+
className={
190+
"line-number-rows font-mono absolute pointer-events-none text-default-500 top-0 left-1 " +
191+
`border-solid border-default-300 border-r-1 ${tst}`
192+
}
193+
>
194+
{Array.from({ length: lineCount }, (_, idx) => {
195+
return <span key={idx} />
196+
})}
197+
</span>
198+
</div>
185199
<textarea
186-
className={`w-full font-mono min-h-[20em] text-[transparent] placeholder-default-400 ${tst} caret-foreground bg-transparent outline-none relative`}
200+
className={`w-full font-mono min-h-[20em] text-[transparent] placeholder-default-400
201+
caret-foreground bg-transparent outline-none relative`}
202+
style={{ paddingLeft: lineNumOffset }}
187203
ref={refTextarea}
188204
readOnly={disabled}
189205
placeholder={placeholder}

frontend/components/DarkModeToggle.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,17 @@ export function useDarkModeSelection(): [
5454
}, [])
5555

5656
const isDark = modeSelection === undefined || modeSelection === "system" ? isSystemDark : modeSelection === "dark"
57+
58+
useEffect(() => {
59+
if (isDark) {
60+
document.body.classList.remove("light")
61+
document.body.classList.add("dark")
62+
} else {
63+
document.body.classList.remove("dark")
64+
document.body.classList.add("light")
65+
}
66+
}, [isDark])
67+
5768
return [isDark, modeSelection, setModeSelection]
5869
}
5970

frontend/components/UploadedPanel.tsx

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,14 @@ interface UploadedPanelProps extends CardProps {
1313
encryptionKey?: string
1414
}
1515

16-
const makeDecryptionUrl = (url: string, key: string) => {
16+
const makeDecryptionUrl = (url: string, key?: string) => {
1717
const urlParsed = new URL(url)
1818
urlParsed.pathname = "/d" + urlParsed.pathname
19-
return urlParsed.toString() + "#" + key
19+
if (key) {
20+
return urlParsed.toString() + "#" + key
21+
} else {
22+
return urlParsed.toString()
23+
}
2024
}
2125

2226
export function UploadedPanel({
@@ -50,23 +54,21 @@ export function UploadedPanel({
5054
) : (
5155
pasteResponse && (
5256
<>
53-
{encryptionKey && (
54-
<Input
55-
{...inputProps}
56-
label={"Decryption URL"}
57-
color={"success"}
58-
value={makeDecryptionUrl(pasteResponse.url, encryptionKey)}
59-
endContent={
60-
<CopyWidget
61-
className={copyWidgetClassNames}
62-
getCopyContent={() => makeDecryptionUrl(pasteResponse.url, encryptionKey)}
63-
/>
64-
}
65-
/>
66-
)}
6757
<Input
6858
{...inputProps}
69-
label={"Paste URL"}
59+
label={"Display URL"}
60+
color={encryptionKey ? "success" : "default"}
61+
value={makeDecryptionUrl(pasteResponse.url, encryptionKey)}
62+
endContent={
63+
<CopyWidget
64+
className={copyWidgetClassNames}
65+
getCopyContent={() => makeDecryptionUrl(pasteResponse.url, encryptionKey)}
66+
/>
67+
}
68+
/>
69+
<Input
70+
{...inputProps}
71+
label={"Raw URL"}
7072
value={pasteResponse.url}
7173
endContent={<CopyWidget className={copyWidgetClassNames} getCopyContent={() => pasteResponse.url} />}
7274
/>

frontend/hero-theme.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { heroui } from "@heroui/react"
22

33
export default heroui({
4+
defaultTheme: "light",
45
themes: {
6+
light: {},
57
dark: {
68
colors: {
79
background: "#111111",

frontend/pages/DecryptPaste.tsx

Lines changed: 70 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useEffect, useMemo, useState } from "react"
1+
import React, { useEffect, useState } from "react"
22

33
import { Button, CircularProgress, Link, Tooltip } from "@heroui/react"
44
import binaryExtensions from "binary-extensions"
@@ -14,6 +14,9 @@ import { formatSize } from "../utils/utils.js"
1414
import { tst } from "../utils/overrides.js"
1515

1616
import "../style.css"
17+
import "../styles/highlight-theme-light.css"
18+
import "../styles/highlight-theme-dark.css"
19+
import { highlightHTML, useHLJS } from "../utils/HighlightLoader.js"
1720

1821
function isBinaryPath(path: string) {
1922
return binaryExtensions.includes(path.replace(/.*\./, ""))
@@ -22,31 +25,30 @@ function isBinaryPath(path: string) {
2225
export function DecryptPaste() {
2326
const [pasteFile, setPasteFile] = useState<File | undefined>(undefined)
2427
const [pasteContentBuffer, setPasteContentBuffer] = useState<ArrayBuffer | undefined>(undefined)
28+
const [pasteLang, setPasteLang] = useState<string | undefined>(undefined)
2529

2630
const [isFileBinary, setFileBinary] = useState(false)
31+
const [isDecrypted, setDecrypted] = useState(false)
2732
const [forceShowBinary, setForceShowBinary] = useState(false)
2833
const showFileContent = pasteFile !== undefined && (!isFileBinary || forceShowBinary)
2934

3035
const [isLoading, setIsLoading] = useState<boolean>(false)
3136

3237
const { ErrorModal, showModal, handleFailedResp } = useErrorModal()
33-
const [isDark, modeSelection, setModeSelection] = useDarkModeSelection()
38+
const [_, modeSelection, setModeSelection] = useDarkModeSelection()
39+
const hljs = useHLJS()
3440

35-
const pasteStringContent = useMemo<string | undefined>(() => {
36-
return pasteContentBuffer && new TextDecoder().decode(pasteContentBuffer)
37-
}, [pasteContentBuffer])
41+
const pasteStringContent = pasteContentBuffer && new TextDecoder().decode(pasteContentBuffer)
42+
43+
const pasteLineCount = (pasteStringContent?.match(/\n/g)?.length || 0) + 1
3844

3945
// uncomment the following lines for testing
40-
// const url = new URL("http://localhost:8787/d/dHYQ.jpg.txt#uqeULsBTb2I3iC7rD6AaYh4oJ5lMjJA2nYR+H0U8bEA=")
46+
// const url = new URL("http://localhost:8787/GQbf")
4147
const url = location
4248

4349
const { name, ext, filename } = parsePath(url.pathname)
44-
const keyString = url.hash.slice(1)
4550

4651
useEffect(() => {
47-
if (keyString.length === 0) {
48-
showModal("Error", "No encryption key is given. You should append the key after a “#” character in the URL")
49-
}
5052
const pasteUrl = `${API_URL}/${name}`
5153

5254
const fetchPaste = async () => {
@@ -59,42 +61,54 @@ export function DecryptPaste() {
5961
}
6062

6163
const scheme: EncryptionScheme | null = resp.headers.get("X-PB-Encryption-Scheme") as EncryptionScheme | null
62-
if (scheme === null) {
63-
showModal("Error", "No encryption scheme is given by the server")
64-
return
65-
}
66-
let key: CryptoKey | undefined
67-
try {
68-
key = await decodeKey(scheme, keyString)
69-
} catch {
70-
showModal("Error", `Failed to parse “${keyString}” as ${scheme} key`)
71-
return
72-
}
73-
if (key === undefined) {
74-
showModal("Error", `Failed to parse “${keyString}” as ${scheme} key`)
75-
return
64+
let filenameFromDisp = resp.headers.has("Content-Disposition")
65+
? parseFilenameFromContentDisposition(resp.headers.get("Content-Disposition")!) || undefined
66+
: undefined
67+
if (filenameFromDisp && scheme !== null) {
68+
filenameFromDisp = filenameFromDisp.replace(/.encrypted$/, "")
7669
}
7770

78-
const decrypted = await decrypt(scheme, key, await resp.bytes())
79-
if (decrypted === null) {
80-
showModal("Error", "Failed to decrypt content")
81-
} else {
82-
const filenameFromDispTrimmed = resp.headers.has("Content-Disposition")
83-
? parseFilenameFromContentDisposition(resp.headers.get("Content-Disposition")!)?.replace(
84-
/.encrypted$/g,
85-
"",
86-
) || undefined
87-
: undefined
71+
const lang = resp.headers.get("X-PB-Highlight-Language")
72+
73+
const inferredFilename = filename || (ext && name + ext) || filenameFromDisp
74+
const respBytes = await resp.bytes()
75+
const isBinary = lang === null && inferredFilename !== undefined && isBinaryPath(inferredFilename)
76+
setPasteLang(lang || undefined)
77+
setFileBinary(isBinary)
8878

89-
// TODO: highlight with lang
90-
const lang = resp.headers.get("X-PB-Highlight-Language")
79+
if (scheme === null) {
80+
setPasteFile(new File([respBytes], inferredFilename || name))
81+
setPasteContentBuffer(respBytes)
82+
} else {
83+
const keyString = url.hash.slice(1)
84+
if (keyString.length === 0) {
85+
showModal("Error", "No encryption key is given. You should append the key after a “#” character in the URL")
86+
}
87+
let key: CryptoKey | undefined
88+
try {
89+
key = await decodeKey(scheme, keyString)
90+
} catch {
91+
showModal("Error", `Failed to parse “${keyString}” as ${scheme} key`)
92+
return
93+
}
94+
if (key === undefined) {
95+
showModal("Error", `Failed to parse “${keyString}” as ${scheme} key`)
96+
return
97+
}
98+
99+
const decrypted = await decrypt(scheme, key, respBytes)
100+
if (decrypted === null) {
101+
showModal("Error", "Failed to decrypt content")
102+
return
103+
}
91104

92-
const inferredFilename = filename || (ext && name + ext) || filenameFromDispTrimmed
93105
setPasteFile(new File([decrypted], inferredFilename || name))
94106
setPasteContentBuffer(decrypted)
107+
setPasteLang(lang || undefined)
95108

96109
const isBinary = lang === null && inferredFilename !== undefined && isBinaryPath(inferredFilename)
97110
setFileBinary(isBinary)
111+
setDecrypted(true)
98112
}
99113
} finally {
100114
setIsLoading(false)
@@ -108,7 +122,7 @@ export function DecryptPaste() {
108122

109123
const fileIndicator = pasteFile && (
110124
<div className="text-foreground-600 mb-2 text-small">
111-
{`${pasteFile?.name} (${formatSize(pasteFile.size)})`}
125+
{`${pasteFile?.name} (${formatSize(pasteFile.size)})` + (pasteLang ? ` (${pasteLang})` : "")}
112126
{forceShowBinary && (
113127
<button className="ml-2 text-primary-500" onClick={() => setForceShowBinary(false)}>
114128
(Click to hide)
@@ -132,10 +146,7 @@ export function DecryptPaste() {
132146
const buttonClasses = `rounded-full bg-background hover:bg-default-100 ${tst}`
133147
return (
134148
<main
135-
className={
136-
`flex flex-col items-center min-h-screen transition-transform-background bg-background ${tst} text-foreground w-full p-2` +
137-
(isDark ? " dark" : " light")
138-
}
149+
className={`flex flex-col items-center min-h-screen transition-transform-background bg-background ${tst} text-foreground w-full p-2`}
139150
>
140151
<div className="w-full max-w-[64rem]">
141152
<div className="flex flex-row my-4 items-center justify-between">
@@ -148,7 +159,7 @@ export function DecryptPaste() {
148159
</Link>
149160
<span className="mx-2">{" / "}</span>
150161
<code>{name}</code>
151-
<span className="ml-1">{isLoading ? " (Loading…)" : pasteFile ? " (Decrypted)" : ""}</span>
162+
<span className="ml-1">{isLoading ? " (Loading…)" : isDecrypted ? " (Decrypted)" : ""}</span>
152163
</h1>
153164
{showFileContent && (
154165
<Tooltip content={`Copy to clipboard`}>
@@ -167,7 +178,7 @@ export function DecryptPaste() {
167178
<DarkModeToggle modeSelection={modeSelection} setModeSelection={setModeSelection} />
168179
</div>
169180
<div className="my-4">
170-
<div className={`min-h-[30rem] w-full bg-secondary-50 rounded-lg p-3 relative ${tst}`}>
181+
<div className={`min-h-[30rem] w-full bg-default-50 rounded-lg p-3 relative ${tst}`}>
171182
{isLoading ? (
172183
<CircularProgress className="absolute top-[50%] left-[50%] translate-[-50%]" />
173184
) : (
@@ -176,8 +187,21 @@ export function DecryptPaste() {
176187
{showFileContent ? (
177188
<>
178189
{fileIndicator}
179-
<div className="font-mono whitespace-pre-wrap" role="article">
180-
{pasteStringContent!}
190+
<div className="font-mono whitespace-pre-wrap relative" role="article">
191+
<pre
192+
style={{ paddingLeft: `${Math.floor(Math.log10(pasteLineCount)) + 2}em` }}
193+
dangerouslySetInnerHTML={{ __html: highlightHTML(hljs, pasteLang, pasteStringContent!) }}
194+
/>
195+
<span
196+
className={
197+
"line-number-rows absolute pointer-events-none text-default-500 top-0 left-0 " +
198+
"border-solid border-default-300 border-r-1"
199+
}
200+
>
201+
{Array.from({ length: pasteLineCount }, (_, idx) => {
202+
return <span key={idx} />
203+
})}
204+
</span>
181205
</div>
182206
</>
183207
) : (

0 commit comments

Comments
 (0)