webui: Add hashing implementation

This commit is contained in:
Kevin Kandlbinder 2022-09-08 19:27:18 +02:00
parent 04a4584f74
commit c0b44b5a89
6 changed files with 294 additions and 15 deletions

View file

@ -17,12 +17,14 @@
"@types/react-relay": "^13.0.1",
"@types/react-table": "^7.7.12",
"@types/relay-runtime": "^13.0.2",
"animejs": "^3.2.1",
"axios": "^0.27.2",
"hamburger-react": "^2.5.0",
"i18next": "^21.9.1",
"i18next-browser-languagedetector": "^6.1.5",
"i18next-http-backend": "^1.4.1",
"i18next-parser": "^6.5.0",
"js-sha512": "^0.8.0",
"lucide-react": "^0.88.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
@ -60,6 +62,7 @@
]
},
"devDependencies": {
"@types/animejs": "^3.1.5",
"babel-plugin-relay": "^14.1.0",
"graphql": "^16.3.0",
"relay-compiler": "^13.2.0"

View file

@ -2,6 +2,98 @@
$slideOverBreakpoint: 1000px;
.modalOuter {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: var(--veles-color-background-transparent);
opacity: 0;
pointer-events: none;
transition: opacity .25s;
&.active {
opacity: 1;
pointer-events: auto;
}
.modalTitle {
font-size: 1.5em;
font-weight: 700;
}
.uploader {
position: fixed;
top: 50%;
left: 50%;
width: 100%;
height: 100%;
max-width: 800px;
max-height: 700px;
transform: translate(-50%, -50%);
background-color: var(--veles-color-background);
padding: var(--veles-layout-padding);
border: thin solid var(--veles-color-border);
border-radius: var(--veles-layout-border-radius);
overflow: hidden;
.dropArea {
height: 200px;
border: 5px dashed var(--veles-color-border);
position: relative;
.dropAreaContent {
position: absolute;
top: 50%;
left: 50%;
text-align: center;
justify-content: center;
align-items: center;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
}
}
.fileList {
display: flex;
flex-direction: column;
margin-top: var(--veles-layout-padding);
> span {
text-align: center;
opacity: .75;
padding: var(--veles-layout-padding-slim) var(--veles-layout-padding);
}
.file {
display: flex;
padding: var(--veles-layout-padding-slim) var(--veles-layout-padding);
border-bottom: thin solid var(--veles-color-border);
.fileName {
flex-grow: 1;
}
.fileHash {
width: 200px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
}
}
}
}
.roomsContainer {
display: flex;
height: calc(100% + 2*var(--veles-layout-padding));

View file

@ -1,4 +1,4 @@
import React, {useState} from "react";
import React, {useCallback, useEffect, useRef, useState} from "react";
import {useNavigate, useOutlet} from "react-router-dom";
import styles from "./Entries.module.scss";
@ -9,11 +9,19 @@ import {graphql} from "babel-plugin-relay/macro";
import {EntriesQuery} from "./__generated__/EntriesQuery.graphql";
import {X} from "lucide-react";
import {LoaderSuspense} from "../../../common/Loader";
import {sha512} from "js-sha512";
import {ReactComponent as Logo} from "../../../../logo.svg";
import anime, {AnimeInstance} from "animejs";
type Props = {
initialQueryRef: PreloadedQuery<EntriesQuery>,
}
type UploadQueueEntry = {
fileName: string
fileHash: string
}
const Entries = ({initialQueryRef}: Props) => {
const outlet = useOutlet()
const navigate = useNavigate()
@ -32,14 +40,173 @@ const Entries = ({initialQueryRef}: Props) => {
const [title, setTitle] = useState(defaultTitle)
const [showUploader, setShowUploader] = useState(false)
const [uploadCounter, setUploadCounter] = useState(0)
const uploadQueue = useRef<UploadQueueEntry[]>([])
const incompleteCounter = useRef(0)
const loadingAnimation = useRef<AnimeInstance>()
const loadingLogo = useRef<SVGSVGElement>(null)
const setupAnim = useCallback(() => {
if(!loadingLogo.current) return
if(incompleteCounter.current <= 0) {
anime({
targets: "#"+loadingLogo.current.id+" .number",
duration: 500,
easing: "easeInOutCubic",
opacity: 1,
})
loadingAnimation.current = undefined
return
}
loadingAnimation.current = anime({
targets: "#"+loadingLogo.current.id+" .number",
duration: 500,
easing: "easeInOutCubic",
opacity: () => anime.random(.25, 1),
complete: () => setupAnim()
})
}, [])
useEffect(() => {
if(!loadingLogo.current) return
if(!loadingAnimation.current && incompleteCounter.current > 0) setupAnim()
}, [uploadCounter, setupAnim])
const processFile = (file: File) => {
const reader = new FileReader()
reader.addEventListener('load', (ev) => {
const res = ev.target!.result as ArrayBuffer;
const hash = sha512(res)
uploadQueue.current.push({
fileName: file.name,
fileHash: hash,
})
incompleteCounter.current--
setUploadCounter(prev => prev + 1)
})
reader.addEventListener('error', (err) => {
console.error(err)
incompleteCounter.current--
})
reader.readAsArrayBuffer(file)
}
const processDirectory = (directory: FileSystemDirectoryEntry) => {
const reader = directory.createReader()
reader.readEntries((entries) => {
entries.forEach((entry) => {
if(entry.isFile) {
(entry as FileSystemFileEntry).file((file) => {
processFile(file)
incompleteCounter.current++
})
return
}
if(entry.isDirectory) {
processDirectory(entry as FileSystemDirectoryEntry)
incompleteCounter.current++
return
}
})
incompleteCounter.current--
}, (err) => {
console.error("Could not read directory", directory.fullPath, err)
incompleteCounter.current--
})
}
const processDataTransfer = (items: DataTransferItemList) => {
for(let i = 0; i < items.length; i++) {
const item = items[i];
const entry = item.webkitGetAsEntry()
if(!entry) {
continue
}
if(entry.isDirectory) {
const dir = entry as FileSystemDirectoryEntry;
processDirectory(dir)
continue
}
if(entry.isFile) {
const file = entry as FileSystemFileEntry;
file.file((file) => {
processFile(file)
})
continue
}
}
}
return <div className={styles.roomsContainer}>
<div className={styles.roomsOverview + (outlet ? " "+styles.leaveSpace : "")}>
<h1><Trans i18nKey={"entries.title"}>Entry Management</Trans></h1>
<button onClick={() => setShowUploader(true)}>Upload New</button>
<EntriesTable initialQueryRef={data}/>
</div>
<div className={styles.modalOuter + (showUploader ? " "+styles.active :"")}>
<div className={styles.uploader} role={"dialog"} aria-modal={true}>
<span className={styles.modalTitle}>Upload entries</span>
<p>In this form you'll be able to upload new entries. Only the hashes of the files will be sent to the server. The actual files never leave your device.</p>
{/*<div>
<input type={"text"} placeholder={"Lists to upload to"} />
</div>*/}
<div className={styles.dropArea} onDragOver={(ev) => {
ev.stopPropagation();
ev.preventDefault();
ev.dataTransfer.dropEffect = 'copy';
}} onDrop={(ev) => {
ev.stopPropagation();
ev.preventDefault();
const fileList = ev.dataTransfer.items;
processDataTransfer(fileList)
}}>
<div className={styles.dropAreaContent}>
<Logo ref={loadingLogo} id={"loadingLogo"}/>
<span>{incompleteCounter.current > 0 ? "Hashing..." : "Drop files to hash"}</span>
</div>
</div>
<div className={styles.fileList}>
{
uploadQueue.current.reverse().slice(0, 5).map((entry) => {
return <div className={styles.file}>
<span className={styles.fileName}>{entry.fileName}</span>
<span className={styles.fileHash}>{entry.fileHash}</span>
</div>
})
}
{uploadQueue.current.length > 5 && <span>And {uploadQueue.current.length - 5} more...</span>}
</div>
<button onClick={() => setShowUploader(false)}>Cancel</button>
</div>
</div>
<div className={styles.slideOver + (outlet ? " "+styles.active : "")}>
<EntriesSlideOverTitleContext.Provider value={setTitle}>
<div className={styles.slideOverHeader}>

View file

@ -20,6 +20,7 @@ html, body, #root {
:root {
--veles-color-background: #0d0d0d;
--veles-color-background-transparent: #0d0d0df0;
--veles-color-foreground: #fff;
--veles-color-border: #1c1c1c;
--veles-color-border-highlight: #464646;
@ -41,6 +42,7 @@ html, body, #root {
@media(prefers-color-scheme: light) {
:root {
--veles-color-background: #f2f2f2;
--veles-color-background-transparent: #f2f2f2f0;
--veles-color-foreground: #0d0d0d;
--veles-color-border: #cccccc;
--veles-color-border-highlight: #9b9b9b;

View file

@ -10,20 +10,20 @@
<linearGradient id="linearGradient1303" x1="49.777" x2="70.07" y1="81.546" y2="81.546" gradientTransform="translate(0,6)" gradientUnits="userSpaceOnUse" xlink:href="#linearGradient43644"/>
<linearGradient id="linearGradient31669" x1="91.453" x2="72.905" y1="48.722" y2="48.722" gradientUnits="userSpaceOnUse" xlink:href="#linearGradient43644"/>
</defs>
<g transform="translate(-3.6704 12.24)" fill="url(#linearGradient31669)" style="shape-inside:url(#rect8261);white-space:pre" aria-label="101 011 110 100">
<path d="m74.758 17.636h4.2969v-14.831l-4.6745 0.9375v-2.3958l4.6484-0.9375h2.6302v17.227h4.2969v2.2135h-11.198z"/>
<path d="m96.894 2.1413q-2.0313 0-3.0599 2.0052-1.0156 1.9922-1.0156 6.0026 0 3.9974 1.0156 6.0026 1.0286 1.9922 3.0599 1.9922 2.0443 0 3.0599-1.9922 1.0286-2.0052 1.0286-6.0026 0-4.0104-1.0286-6.0026-1.0156-2.0052-3.0599-2.0052zm0-2.0833q3.2682 0 4.987 2.5911 1.7318 2.5781 1.7318 7.5 0 4.9089-1.7318 7.5-1.7188 2.5781-4.987 2.5781-3.2682 0-5-2.5781-1.7188-2.5911-1.7188-7.5 0-4.9219 1.7188-7.5 1.7318-2.5911 5-2.5911z"/>
<path d="m108.69 17.636h4.2969v-14.831l-4.6745 0.9375v-2.3958l4.6484-0.9375h2.6302v17.227h4.2969v2.2135h-11.198z"/>
<path d="m79.928 27.475q-2.0313 0-3.0599 2.0052-1.0156 1.9922-1.0156 6.0026 0 3.9974 1.0156 6.0026 1.0286 1.9922 3.0599 1.9922 2.0443 0 3.0599-1.9922 1.0286-2.0052 1.0286-6.0026 0-4.0104-1.0286-6.0026-1.0156-2.0052-3.0599-2.0052zm0-2.0833q3.2682 0 4.987 2.5911 1.7318 2.5781 1.7318 7.5 0 4.9089-1.7318 7.5-1.7188 2.5781-4.987 2.5781t-5-2.5781q-1.7188-2.5911-1.7188-7.5 0-4.9219 1.7188-7.5 1.7318-2.5911 5-2.5911z"/>
<path d="m91.725 42.969h4.2969v-14.831l-4.6745 0.9375v-2.3958l4.6484-0.9375h2.6302v17.227h4.2969v2.2135h-11.198z"/>
<path d="m108.69 42.969h4.2969v-14.831l-4.6745 0.9375v-2.3958l4.6484-0.9375h2.6302v17.227h4.2969v2.2135h-11.198z"/>
<path d="m74.758 68.303h4.2969v-14.831l-4.6745 0.9375v-2.3958l4.6484-0.9375h2.6302v17.227h4.2969v2.2135h-11.198z"/>
<path d="m91.725 68.303h4.2969v-14.831l-4.6745 0.9375v-2.3958l4.6484-0.9375h2.6302v17.227h4.2969v2.2135h-11.198z"/>
<path d="m113.86 52.808q-2.0312 0-3.0599 2.0052-1.0156 1.9922-1.0156 6.0026 0 3.9974 1.0156 6.0026 1.0286 1.9922 3.0599 1.9922 2.0443 0 3.0599-1.9922 1.0286-2.0052 1.0286-6.0026 0-4.0104-1.0286-6.0026-1.0156-2.0052-3.0599-2.0052zm0-2.0833q3.2682 0 4.987 2.5911 1.7318 2.5781 1.7318 7.5 0 4.9089-1.7318 7.5-1.7188 2.5781-4.987 2.5781t-5-2.5781q-1.7188-2.5911-1.7188-7.5 0-4.9219 1.7188-7.5 1.7318-2.5911 5-2.5911z"/>
<path d="m74.758 93.636h4.2969v-14.831l-4.6745 0.9375v-2.3958l4.6484-0.9375h2.6302v17.227h4.2969v2.2135h-11.198z"/>
<path d="m96.894 78.141q-2.0313 0-3.0599 2.0052-1.0156 1.9922-1.0156 6.0026 0 3.9974 1.0156 6.0026 1.0286 1.9922 3.0599 1.9922 2.0443 0 3.0599-1.9922 1.0286-2.0052 1.0286-6.0026 0-4.0104-1.0286-6.0026-1.0156-2.0052-3.0599-2.0052zm0-2.0833q3.2682 0 4.987 2.5911 1.7318 2.5781 1.7318 7.5 0 4.9089-1.7318 7.5-1.7188 2.5781-4.987 2.5781-3.2682 0-5-2.5781-1.7188-2.5911-1.7188-7.5 0-4.9219 1.7188-7.5 1.7318-2.5911 5-2.5911z"/>
<path d="m113.86 78.141q-2.0312 0-3.0599 2.0052-1.0156 1.9922-1.0156 6.0026 0 3.9974 1.0156 6.0026 1.0286 1.9922 3.0599 1.9922 2.0443 0 3.0599-1.9922 1.0286-2.0052 1.0286-6.0026 0-4.0104-1.0286-6.0026-1.0156-2.0052-3.0599-2.0052zm0-2.0833q3.2682 0 4.987 2.5911 1.7318 2.5781 1.7318 7.5 0 4.9089-1.7318 7.5-1.7188 2.5781-4.987 2.5781t-5-2.5781q-1.7188-2.5911-1.7188-7.5 0-4.9219 1.7188-7.5 1.7318-2.5911 5-2.5911z"/>
</g>
<g transform="translate(-3.6704 12.24)" fill="url(#linearGradient31669)" style="shape-inside:url(#rect8261);white-space:pre" aria-label="101 011 110 100">
<path class="number" d="m74.758 17.636h4.2969v-14.831l-4.6745 0.9375v-2.3958l4.6484-0.9375h2.6302v17.227h4.2969v2.2135h-11.198z"/>
<path class="number" d="m96.894 2.1413q-2.0313 0-3.0599 2.0052-1.0156 1.9922-1.0156 6.0026 0 3.9974 1.0156 6.0026 1.0286 1.9922 3.0599 1.9922 2.0443 0 3.0599-1.9922 1.0286-2.0052 1.0286-6.0026 0-4.0104-1.0286-6.0026-1.0156-2.0052-3.0599-2.0052zm0-2.0833q3.2682 0 4.987 2.5911 1.7318 2.5781 1.7318 7.5 0 4.9089-1.7318 7.5-1.7188 2.5781-4.987 2.5781-3.2682 0-5-2.5781-1.7188-2.5911-1.7188-7.5 0-4.9219 1.7188-7.5 1.7318-2.5911 5-2.5911z"/>
<path class="number" d="m108.69 17.636h4.2969v-14.831l-4.6745 0.9375v-2.3958l4.6484-0.9375h2.6302v17.227h4.2969v2.2135h-11.198z"/>
<path class="number" d="m79.928 27.475q-2.0313 0-3.0599 2.0052-1.0156 1.9922-1.0156 6.0026 0 3.9974 1.0156 6.0026 1.0286 1.9922 3.0599 1.9922 2.0443 0 3.0599-1.9922 1.0286-2.0052 1.0286-6.0026 0-4.0104-1.0286-6.0026-1.0156-2.0052-3.0599-2.0052zm0-2.0833q3.2682 0 4.987 2.5911 1.7318 2.5781 1.7318 7.5 0 4.9089-1.7318 7.5-1.7188 2.5781-4.987 2.5781t-5-2.5781q-1.7188-2.5911-1.7188-7.5 0-4.9219 1.7188-7.5 1.7318-2.5911 5-2.5911z"/>
<path class="number" d="m91.725 42.969h4.2969v-14.831l-4.6745 0.9375v-2.3958l4.6484-0.9375h2.6302v17.227h4.2969v2.2135h-11.198z"/>
<path class="number" d="m108.69 42.969h4.2969v-14.831l-4.6745 0.9375v-2.3958l4.6484-0.9375h2.6302v17.227h4.2969v2.2135h-11.198z"/>
<path class="number" d="m74.758 68.303h4.2969v-14.831l-4.6745 0.9375v-2.3958l4.6484-0.9375h2.6302v17.227h4.2969v2.2135h-11.198z"/>
<path class="number" d="m91.725 68.303h4.2969v-14.831l-4.6745 0.9375v-2.3958l4.6484-0.9375h2.6302v17.227h4.2969v2.2135h-11.198z"/>
<path class="number" d="m113.86 52.808q-2.0312 0-3.0599 2.0052-1.0156 1.9922-1.0156 6.0026 0 3.9974 1.0156 6.0026 1.0286 1.9922 3.0599 1.9922 2.0443 0 3.0599-1.9922 1.0286-2.0052 1.0286-6.0026 0-4.0104-1.0286-6.0026-1.0156-2.0052-3.0599-2.0052zm0-2.0833q3.2682 0 4.987 2.5911 1.7318 2.5781 1.7318 7.5 0 4.9089-1.7318 7.5-1.7188 2.5781-4.987 2.5781t-5-2.5781q-1.7188-2.5911-1.7188-7.5 0-4.9219 1.7188-7.5 1.7318-2.5911 5-2.5911z"/>
<path class="number" d="m74.758 93.636h4.2969v-14.831l-4.6745 0.9375v-2.3958l4.6484-0.9375h2.6302v17.227h4.2969v2.2135h-11.198z"/>
<path class="number" d="m96.894 78.141q-2.0313 0-3.0599 2.0052-1.0156 1.9922-1.0156 6.0026 0 3.9974 1.0156 6.0026 1.0286 1.9922 3.0599 1.9922 2.0443 0 3.0599-1.9922 1.0286-2.0052 1.0286-6.0026 0-4.0104-1.0286-6.0026-1.0156-2.0052-3.0599-2.0052zm0-2.0833q3.2682 0 4.987 2.5911 1.7318 2.5781 1.7318 7.5 0 4.9089-1.7318 7.5-1.7188 2.5781-4.987 2.5781-3.2682 0-5-2.5781-1.7188-2.5911-1.7188-7.5 0-4.9219 1.7188-7.5 1.7318-2.5911 5-2.5911z"/>
<path class="number" d="m113.86 78.141q-2.0312 0-3.0599 2.0052-1.0156 1.9922-1.0156 6.0026 0 3.9974 1.0156 6.0026 1.0286 1.9922 3.0599 1.9922 2.0443 0 3.0599-1.9922 1.0286-2.0052 1.0286-6.0026 0-4.0104-1.0286-6.0026-1.0156-2.0052-3.0599-2.0052zm0-2.0833q3.2682 0 4.987 2.5911 1.7318 2.5781 1.7318 7.5 0 4.9089-1.7318 7.5-1.7188 2.5781-4.987 2.5781t-5-2.5781q-1.7188-2.5911-1.7188-7.5 0-4.9219 1.7188-7.5 1.7318-2.5911 5-2.5911z"/>
</g>
<path d="m4.0226 8.0649 30.026 50.894h59.933l30.009-50.864" fill="none" stroke="url(#linearGradient1301)" stroke-linecap="round" stroke-linejoin="round" stroke-width="4.048"/>
<path d="m36.324 67.382h47.484l-23.793 40.329z" fill="none" stroke="url(#linearGradient1303)" stroke-linejoin="round" stroke-width="4.048"/>
</svg>

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

View file

@ -1718,6 +1718,11 @@
resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad"
integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==
"@types/animejs@^3.1.5":
version "3.1.5"
resolved "https://registry.yarnpkg.com/@types/animejs/-/animejs-3.1.5.tgz#3cb6c7b90069c6e2969a392eb65ec6311e92c1ab"
integrity sha512-4i3i1YuNaNEPoHBJY78uzYu8qKIwyx96G04tnVtNhRMQC9I1Xhg6fY9GeWmZAzudaesKKrPkQgTCthT1zSGYyg==
"@types/aria-query@^4.2.0":
version "4.2.2"
resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-4.2.2.tgz#ed4e0ad92306a704f9fb132a0cfcf77486dbe2bc"
@ -2443,6 +2448,11 @@ ajv@^8.0.0, ajv@^8.6.0, ajv@^8.8.0:
require-from-string "^2.0.2"
uri-js "^4.2.2"
animejs@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/animejs/-/animejs-3.2.1.tgz#de9fe2e792f44a777a7fdf6ae160e26604b0cdda"
integrity sha512-sWno3ugFryK5nhiDm/2BKeFCpZv7vzerWUcUPyAZLDhMek3+S/p418ldZJbJXo5ZUOpfm2kP2XRO4NJcULMy9A==
ansi-escapes@^4.2.1, ansi-escapes@^4.3.1:
version "4.3.2"
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e"
@ -6168,6 +6178,11 @@ jest@^27.4.3:
import-local "^3.0.2"
jest-cli "^27.5.1"
js-sha512@^0.8.0:
version "0.8.0"
resolved "https://registry.yarnpkg.com/js-sha512/-/js-sha512-0.8.0.tgz#dd22db8d02756faccf19f218e3ed61ec8249f7d4"
integrity sha512-PWsmefG6Jkodqt+ePTvBZCSMFgN7Clckjd0O7su3I0+BW2QWUTJNzjktHsztGLhncP2h8mcF9V9Y2Ha59pAViQ==
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"