Dunestash Public Frontend 0.0.1-a.1
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
# For Deploy
|
||||||
|
build/
|
||||||
|
node_modules/
|
||||||
|
package-lock.json
|
10
Dockerfile
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
FROM node:latest
|
||||||
|
RUN apt-get update && apt-get upgrade -y
|
||||||
|
RUN apt-get install libsass-dev build-essential -y
|
||||||
|
WORKDIR /dunestorm/khufu
|
||||||
|
COPY build /dunestorm/khufu/build/
|
||||||
|
RUN npm i -g serve
|
||||||
|
ENV KHUFU_DEV_TOKEN $KHUFU_DEV_TOKEN
|
||||||
|
CMD ["serve", "-s", "build", "-l", "52026"]
|
||||||
|
# EXPOSE PORTS
|
||||||
|
EXPOSE 52026
|
54
package.json
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
{
|
||||||
|
"name": "dunestash-frontend",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@fortawesome/fontawesome-svg-core": "^1.2.35",
|
||||||
|
"@fortawesome/free-brands-svg-icons": "^5.15.3",
|
||||||
|
"@fortawesome/free-regular-svg-icons": "^5.15.3",
|
||||||
|
"@fortawesome/free-solid-svg-icons": "^5.15.3",
|
||||||
|
"@fortawesome/react-fontawesome": "^0.1.14",
|
||||||
|
"@testing-library/jest-dom": "^5.14.1",
|
||||||
|
"@testing-library/react": "^12.0.0",
|
||||||
|
"@testing-library/user-event": "^13.2.0",
|
||||||
|
"http-proxy-middleware": "^2.0.1",
|
||||||
|
"react": "^17.0.2",
|
||||||
|
"react-axios": "^2.0.5",
|
||||||
|
"react-dom": "^17.0.2",
|
||||||
|
"react-dropzone": "^11.3.4",
|
||||||
|
"react-fontawesome": "^1.7.1",
|
||||||
|
"react-router-dom": "^5.2.0",
|
||||||
|
"react-scripts": "^4.0.3",
|
||||||
|
"react-toastify": "^7.0.4",
|
||||||
|
"remove": "^0.1.5",
|
||||||
|
"sass": "^1.36.0",
|
||||||
|
"web-vitals": "^2.1.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "PORT=52026 react-scripts start",
|
||||||
|
"build": "react-scripts build",
|
||||||
|
"test": "react-scripts test",
|
||||||
|
"eject": "react-scripts eject"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": [
|
||||||
|
"react-app",
|
||||||
|
"react-app/jest"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not op_mini all"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"serve": "^12.0.0"
|
||||||
|
}
|
||||||
|
}
|
9
public/extras/blank_user.svg
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" version="1.0" width="512.000000" height="512.000000" preserveAspectRatio="xMidYMid meet" style=""><rect id="backgroundrect" width="100%" height="100%" x="0" y="0" fill="none" stroke="none"/>
|
||||||
|
<metadata>
|
||||||
|
Created by potrace 1.16, written by Peter Selinger 2001-2019
|
||||||
|
</metadata>
|
||||||
|
|
||||||
|
<g class="currentLayer" style=""><title>Layer 1</title><g transform="translate(0,512) scale(0.10000000149011612,-0.10000000149011612) " fill="#ffffff" stroke="#ffffff" id="svg_1" class="selected" stroke-opacity="1" fill-opacity="1">
|
||||||
|
<path d="M2305 5114 c-352 -48 -591 -115 -850 -238 -119 -56 -326 -183 -450 -275 -129 -96 -390 -357 -486 -486 -256 -344 -414 -710 -491 -1136 -20 -109 -22 -159 -23 -414 0 -310 6 -371 60 -601 27 -112 92 -315 126 -392 118 -266 219 -432 403 -661 45 -57 296 -301 364 -355 73 -57 257 -186 266 -186 3 0 21 -11 41 -24 50 -34 267 -140 350 -172 340 -127 594 -174 945 -174 224 0 385 17 575 61 461 106 869 328 1209 658 422 411 689 945 761 1521 17 142 20 471 5 600 -27 228 -78 434 -164 665 -32 83 -138 300 -172 350 -13 20 -24 38 -24 40 0 7 -118 181 -133 197 -7 7 -32 40 -56 73 -42 56 -294 316 -352 361 -234 187 -395 285 -661 403 -77 34 -280 99 -391 125 -213 51 -279 58 -557 61 -146 2 -278 1 -295 -1z m545 -189 c381 -46 730 -179 1065 -407 116 -78 298 -236 394 -342 68 -75 76 -85 128 -153 93 -122 123 -163 123 -169 0 -3 13 -25 29 -48 44 -64 155 -286 195 -390 57 -151 99 -304 132 -491 22 -122 30 -461 15 -606 -22 -207 -87 -473 -158 -644 -45 -108 -164 -337 -213 -409 -157 -232 -371 -459 -585 -619 l-130 -98 -5 338 c-5 306 -8 346 -28 425 -62 244 -175 449 -335 609 -85 86 -198 174 -264 208 -52 26 -69 23 -171 -35 -42 -25 -112 -56 -155 -69 -290 -92 -555 -67 -832 79 -101 54 -108 54 -194 -1 -144 -92 -278 -226 -377 -378 -68 -106 -94 -159 -137 -285 -59 -171 -67 -243 -67 -581 0 -191 -4 -299 -10 -299 -10 0 -40 21 -173 123 -68 52 -78 60 -153 128 -305 277 -558 684 -673 1084 -82 286 -112 635 -80 923 50 454 248 923 527 1252 85 100 208 224 307 309 340 294 783 488 1240 546 143 18 437 18 585 0z m-820 -3014 c70 -36 183 -75 285 -97 98 -22 378 -25 470 -5 92 19 242 69 307 102 31 16 64 29 71 29 28 0 166 -122 241 -212 121 -146 195 -298 237 -488 19 -86 21 -128 21 -439 1 -402 21 -349 -160 -428 -213 -93 -463 -159 -702 -184 -158 -16 -492 -6 -630 20 -249 46 -411 96 -611 189 -48 22 -90 46 -94 52 -13 20 -8 677 5 748 41 226 148 437 301 593 68 69 169 149 189 149 8 0 40 -13 70 -29z" id="svg_2" stroke="#ffffff" stroke-opacity="1" fill="#ffffff" fill-opacity="1"/>
|
||||||
|
<path d="M2465 4010 c-89 -10 -141 -23 -235 -62 -291 -119 -485 -379 -522 -699 -49 -436 244 -839 677 -930 443 -93 872 167 999 606 126 435 -119 901 -550 1044 -77 25 -238 54 -284 50 -8 -1 -46 -5 -85 -9z m182 -184 c177 -15 371 -135 473 -291 179 -275 150 -606 -75 -843 -351 -372 -966 -229 -1130 263 -34 102 -40 264 -12 365 56 208 203 381 391 459 78 32 210 60 261 55 17 -2 58 -6 92 -8z" id="svg_3" stroke="#ffffff" stroke-opacity="1" fill="#ffffff" fill-opacity="1"/>
|
||||||
|
</g></g></svg>
|
After Width: | Height: | Size: 2.9 KiB |
BIN
public/icons/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
public/icons/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 6 KiB |
BIN
public/icons/apple-touch-icon-114x114-precomposed.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
public/icons/apple-touch-icon-114x114.png
Normal file
After Width: | Height: | Size: 970 B |
BIN
public/icons/apple-touch-icon-120x120-precomposed.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
public/icons/apple-touch-icon-120x120.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
public/icons/apple-touch-icon-144x144-precomposed.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
public/icons/apple-touch-icon-144x144.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
public/icons/apple-touch-icon-152x152-precomposed.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
public/icons/apple-touch-icon-152x152.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
public/icons/apple-touch-icon-180x180-precomposed.png
Normal file
After Width: | Height: | Size: 2 KiB |
BIN
public/icons/apple-touch-icon-180x180.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
public/icons/apple-touch-icon-57x57-precomposed.png
Normal file
After Width: | Height: | Size: 745 B |
BIN
public/icons/apple-touch-icon-57x57.png
Normal file
After Width: | Height: | Size: 541 B |
BIN
public/icons/apple-touch-icon-60x60-precomposed.png
Normal file
After Width: | Height: | Size: 752 B |
BIN
public/icons/apple-touch-icon-60x60.png
Normal file
After Width: | Height: | Size: 594 B |
BIN
public/icons/apple-touch-icon-72x72-precomposed.png
Normal file
After Width: | Height: | Size: 904 B |
BIN
public/icons/apple-touch-icon-72x72.png
Normal file
After Width: | Height: | Size: 746 B |
BIN
public/icons/apple-touch-icon-76x76-precomposed.png
Normal file
After Width: | Height: | Size: 1 KiB |
BIN
public/icons/apple-touch-icon-76x76.png
Normal file
After Width: | Height: | Size: 725 B |
BIN
public/icons/apple-touch-icon-precomposed.png
Normal file
After Width: | Height: | Size: 2 KiB |
BIN
public/icons/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
9
public/icons/browserconfig.xml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<browserconfig>
|
||||||
|
<msapplication>
|
||||||
|
<tile>
|
||||||
|
<square150x150logo src="/icons/mstile-150x150.png"/>
|
||||||
|
<TileColor>#ffc40d</TileColor>
|
||||||
|
</tile>
|
||||||
|
</msapplication>
|
||||||
|
</browserconfig>
|
BIN
public/icons/favicon-16x16.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
public/icons/favicon-32x32.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
public/icons/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
1
public/icons/logo.svg
Normal file
After Width: | Height: | Size: 38 KiB |
BIN
public/icons/mstile-150x150.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
1
public/icons/safari-pinned-tab.svg
Normal file
After Width: | Height: | Size: 21 KiB |
19
public/icons/site.webmanifest
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"name": "Dunestash",
|
||||||
|
"short_name": "Dunestash",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/icons/android-chrome-192x192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/icons/android-chrome-512x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"theme_color": "#ffffff",
|
||||||
|
"background_color": "#ffffff",
|
||||||
|
"display": "standalone"
|
||||||
|
}
|
26
public/index.html
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="%PUBLIC_URL%/icons/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
<meta name="description" content="My Stash" />
|
||||||
|
<!-- Favicon icons -->
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="%PUBLIC_URL%/icons/apple-touch-icon.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="%PUBLIC_URL%/icons/favicon-32x32.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="%PUBLIC_URL%/icons/favicon-16x16.png">
|
||||||
|
<link rel="manifest" href="%PUBLIC_URL%/icons/site.webmanifest">
|
||||||
|
<link rel="mask-icon" href="%PUBLIC_URL%/icons/safari-pinned-tab.svg" color="#fec628">
|
||||||
|
<link rel="shortcut icon" href="%PUBLIC_URL%/icons/favicon.ico">
|
||||||
|
<meta name="msapplication-TileColor" content="#ffc40d">
|
||||||
|
<meta name="msapplication-config" content="%PUBLIC_URL%/icons/browserconfig.xml">
|
||||||
|
<meta name="theme-color" content="#fec628">
|
||||||
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||||
|
<title>Dune Drive</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
13
public/manifest.json
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"short_name": "Dunestash",
|
||||||
|
"name": "Dunestash",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "favicon.ico",
|
||||||
|
"sizes": "64x64 32x32 24x24 16x16",
|
||||||
|
"type": "image/x-icon"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start_url": ".",
|
||||||
|
"display": "standalone"
|
||||||
|
}
|
3
public/robots.txt
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# https://www.robotstxt.org/robotstxt.html
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
31
src/App.js
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
//Module Imports
|
||||||
|
import React from "react";
|
||||||
|
import { ToastContainer } from "react-toastify";
|
||||||
|
import "react-toastify/dist/ReactToastify.css";
|
||||||
|
//Local Imports
|
||||||
|
import Stash from "./Stash";
|
||||||
|
//Constants
|
||||||
|
const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1dWlkIjoiYmVjYWNjNjdhNmRjLTBlN2QtMDBlNi1jYmVhLWVhZGNlYmUxIiwiaWF0IjoxNjI3MTcxMDU1LCJleHAiOjE2Mjk3NjMwNTV9.zqiHrYnJlB7ozwjMnpgVUsBAt9vfLHLICFgWB0MguLA"
|
||||||
|
localStorage.setItem("authToken", token);
|
||||||
|
class App extends React.Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ToastContainer
|
||||||
|
position="top-right"
|
||||||
|
autoClose={5000}
|
||||||
|
hideProgressBar={false}
|
||||||
|
newestOnTop={false}
|
||||||
|
closeOnClick={true}
|
||||||
|
rtl={false}
|
||||||
|
pauseOnFocusLoss
|
||||||
|
draggable
|
||||||
|
pauseOnHover={false}
|
||||||
|
/>
|
||||||
|
<Stash />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
139
src/Stash.jsx
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
//Module Imports
|
||||||
|
import React from "react";
|
||||||
|
import axios from "axios";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
//Local Imports
|
||||||
|
import Stashbar from "./stash/Stashbar";
|
||||||
|
import StashUpload from "./stash/StashUpload";
|
||||||
|
import StashContextMenu from "./stash/StashContextMenu";
|
||||||
|
import { serverUrls, serverAddress } from "./stash/api.json";
|
||||||
|
import "./stash/scss/Stash.scss";
|
||||||
|
//Constants
|
||||||
|
const filesUrl = `${serverAddress}/${serverUrls.GET.filesUrl}`;
|
||||||
|
//Class
|
||||||
|
function getConfig() {
|
||||||
|
var authToken = localStorage.getItem("authToken");
|
||||||
|
console.log({ headers: { authorization: `Bearer ${authToken}` } });
|
||||||
|
return {
|
||||||
|
headers: { authorization: `Bearer ${authToken}`, withCredentials: true },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function buildFilebox(file, index) {
|
||||||
|
return {
|
||||||
|
file,
|
||||||
|
selected: false,
|
||||||
|
filtered: true,
|
||||||
|
position: index,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
class Stash extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
fileBoxes: {},
|
||||||
|
contextMenu: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
axios
|
||||||
|
.get(filesUrl, getConfig())
|
||||||
|
.then((res) => {
|
||||||
|
if (res.status === 401) {
|
||||||
|
console.log("Would redirect to login");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (res.data === undefined || res.data.length === undefined) {
|
||||||
|
toast.error("Error Loading Files");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var fileBoxes = {};
|
||||||
|
res.data.forEach((file, index) => {
|
||||||
|
fileBoxes[file.fileUuid] = buildFilebox(file, index);
|
||||||
|
});
|
||||||
|
this.setState({ fileBoxes });
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
if (error.response.status === 401)
|
||||||
|
console.log("Would redirect to login");
|
||||||
|
else console.error(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fileBoxesChanged(fileBoxes) {
|
||||||
|
this.setState({ fileBoxes });
|
||||||
|
}
|
||||||
|
|
||||||
|
getSelectedBoxes() {
|
||||||
|
var selectedBoxes = [];
|
||||||
|
for (var f in this.state.fileBoxes) {
|
||||||
|
if (!this.state.fileBoxes[f].filtered) continue;
|
||||||
|
if (!this.state.fileBoxes[f].selected) continue;
|
||||||
|
selectedBoxes.push(f);
|
||||||
|
}
|
||||||
|
return selectedBoxes;
|
||||||
|
}
|
||||||
|
|
||||||
|
addFilebox(file) {
|
||||||
|
var fileBoxes = this.state.fileBoxes;
|
||||||
|
fileBoxes[file.fileUuid] = buildFilebox(
|
||||||
|
file,
|
||||||
|
Object.keys(fileBoxes).length
|
||||||
|
);
|
||||||
|
this.setState({ fileBoxes });
|
||||||
|
}
|
||||||
|
|
||||||
|
removeDriveContextMenu() {
|
||||||
|
if (this.state.contextMenu !== null) this.setState({ contextMenu: null });
|
||||||
|
}
|
||||||
|
|
||||||
|
/*Options Menu Functions*/
|
||||||
|
contextMenu(e) {
|
||||||
|
this.removeDriveContextMenu();
|
||||||
|
if (e.ctrlKey || e.shiftKey) return;
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this.setState({
|
||||||
|
contextMenu: {
|
||||||
|
x: e.clientX,
|
||||||
|
y: e.clientY,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="dunestash"
|
||||||
|
onClick={this.removeDriveContextMenu.bind(this)}
|
||||||
|
>
|
||||||
|
<Stashbar
|
||||||
|
fileBoxes={this.state.fileBoxes}
|
||||||
|
fileBoxesChanged={this.fileBoxesChanged.bind(this)}
|
||||||
|
contextMenu={this.contextMenu.bind(this)}
|
||||||
|
/>
|
||||||
|
{this.state.contextMenu && (
|
||||||
|
<StashContextMenu
|
||||||
|
x={this.state.contextMenu.x}
|
||||||
|
y={this.state.contextMenu.y}
|
||||||
|
fileBoxes={this.state.fileBoxes}
|
||||||
|
fileBoxesChanged={this.fileBoxesChanged.bind(this)}
|
||||||
|
getSelectedBoxes={this.getSelectedBoxes.bind(this)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="stash">
|
||||||
|
<StashUpload
|
||||||
|
addFilebox={this.addFilebox.bind(this)}
|
||||||
|
fileBoxes={this.state.fileBoxes}
|
||||||
|
fileBoxesChanged={this.fileBoxesChanged.bind(this)}
|
||||||
|
contextMenu={this.contextMenu.bind(this)}
|
||||||
|
removeDriveContextMenu={this.removeDriveContextMenu.bind(this)}
|
||||||
|
getSelectedBoxes={this.getSelectedBoxes.bind(this)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Stash;
|
10
src/index.js
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom";
|
||||||
|
import App from "./App";
|
||||||
|
|
||||||
|
ReactDOM.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
document.getElementById("root")
|
||||||
|
);
|
12
src/setupProxy.js
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
const { createProxyMiddleware } = require("http-proxy-middleware");
|
||||||
|
const { serverAddress } = require("./stash/api.json");
|
||||||
|
module.exports = (app) => {
|
||||||
|
app.use(
|
||||||
|
"/api",
|
||||||
|
createProxyMiddleware({
|
||||||
|
target: serverAddress,
|
||||||
|
changeOrigin: true,
|
||||||
|
logLevel: "silent",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
56
src/stash/FileBox.jsx
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import React from "react";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import {
|
||||||
|
faUsers,
|
||||||
|
faEye,
|
||||||
|
faShareSquare,
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import "./scss/stash/FileBox.scss";
|
||||||
|
class FileBox extends React.Component {
|
||||||
|
readableDate() {
|
||||||
|
let d = new Date(parseInt(this.props.file.date));
|
||||||
|
if (isNaN(d.getMonth())) return "";
|
||||||
|
return `${
|
||||||
|
d.getMonth() + 1
|
||||||
|
}/${d.getDate()}/${d.getFullYear()} ${d.getHours()}:${d.getMinutes()}`;
|
||||||
|
}
|
||||||
|
selectBox(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
this.props.onSelection(e, this.props.boxUuid);
|
||||||
|
this.props.removeDriveContextMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
tabIndex="0"
|
||||||
|
className={"filebox" + (this.props.selected ? " selected" : "")}
|
||||||
|
onClick={this.selectBox.bind(this)}
|
||||||
|
onContextMenu={() => this.props.contextSelect(this.props.boxUuid)}
|
||||||
|
onKeyDown={this.props.onBoxKeyPress}
|
||||||
|
>
|
||||||
|
<div className="file">
|
||||||
|
<div className="file-details">
|
||||||
|
<span className="file-name">{this.props.file.name}</span>
|
||||||
|
</div>
|
||||||
|
<div className="file-details">
|
||||||
|
<span className="file-date">{this.readableDate()}</span>
|
||||||
|
{this.props.file.public && (
|
||||||
|
<span className="file-indicators file-subinfo-details">
|
||||||
|
{this.props.file.public && <FontAwesomeIcon icon={faEye} />}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FileBox.propTypes = {
|
||||||
|
file: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FileBox;
|
125
src/stash/FileDisplay.jsx
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
//Module Imports
|
||||||
|
import React from "react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
//Local Imports
|
||||||
|
import FileBox from "./FileBox";
|
||||||
|
import "./scss/stash/FileDisplay.scss";
|
||||||
|
class FileDisplay extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
firstSelectionBoxUuid: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
displayClick(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this.deselectAll();
|
||||||
|
this.props.removeDriveContextMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
fileBoxKeysByPosition() {
|
||||||
|
return Object.keys(this.props.fileBoxes).sort((a, b) => {
|
||||||
|
return a.position - b.position;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onSelection(e, boxUuid) {
|
||||||
|
var fileBoxes = this.props.fileBoxes;
|
||||||
|
var newBoxes;
|
||||||
|
const firstSelection = this.state.firstSelectionBoxUuid;
|
||||||
|
if (e.ctrlKey && firstSelection !== null)
|
||||||
|
newBoxes = this.segmentSelection(fileBoxes, boxUuid);
|
||||||
|
else if (e.shiftKey && firstSelection !== null)
|
||||||
|
newBoxes = this.multiSelection(fileBoxes, boxUuid);
|
||||||
|
else newBoxes = this.singleSelection(fileBoxes, boxUuid);
|
||||||
|
this.props.fileBoxesChanged(newBoxes);
|
||||||
|
}
|
||||||
|
|
||||||
|
singleSelection(fileBoxes, boxUuid) {
|
||||||
|
this.deselectAll();
|
||||||
|
this.setState({ firstSelectionBoxUuid: boxUuid });
|
||||||
|
fileBoxes[boxUuid].selected = true;
|
||||||
|
return fileBoxes;
|
||||||
|
}
|
||||||
|
|
||||||
|
segmentSelection(fileBoxes, boxUuid) {
|
||||||
|
fileBoxes[boxUuid].selected = !fileBoxes[boxUuid].selected;
|
||||||
|
return fileBoxes;
|
||||||
|
}
|
||||||
|
|
||||||
|
multiSelection(fileBoxes, boxUuid) {
|
||||||
|
this.deselectAll();
|
||||||
|
var firstIndex = fileBoxes[this.state.firstSelectionBoxUuid].position;
|
||||||
|
var endIndex = fileBoxes[boxUuid].position;
|
||||||
|
var boxKeys = this.fileBoxKeysByPosition();
|
||||||
|
if (endIndex < firstIndex) {
|
||||||
|
let tmp = endIndex;
|
||||||
|
endIndex = firstIndex;
|
||||||
|
firstIndex = tmp;
|
||||||
|
}
|
||||||
|
//Send selection 1 more for the slice
|
||||||
|
boxKeys.slice(firstIndex, endIndex + 1).forEach((boxId, i) => {
|
||||||
|
if (!fileBoxes[boxId].filtered) return;
|
||||||
|
fileBoxes[boxId].selected = true;
|
||||||
|
});
|
||||||
|
return fileBoxes;
|
||||||
|
}
|
||||||
|
|
||||||
|
contextSelect(boxUuid) {
|
||||||
|
if (this.props.getSelectedBoxes().length > 1) return;
|
||||||
|
this.onSelection({}, boxUuid);
|
||||||
|
}
|
||||||
|
deselectAll() {
|
||||||
|
var fileBoxes = this.props.fileBoxes;
|
||||||
|
for (var f in fileBoxes) fileBoxes[f].selected = false;
|
||||||
|
this.props.fileBoxesChanged(fileBoxes);
|
||||||
|
}
|
||||||
|
selectAll() {
|
||||||
|
var fileBoxes = this.props.fileBoxes;
|
||||||
|
for (var f in fileBoxes)
|
||||||
|
if (fileBoxes[f].filtered) fileBoxes[f].selected = true;
|
||||||
|
this.props.fileBoxesChanged(fileBoxes);
|
||||||
|
}
|
||||||
|
onBoxKeyPress(e) {
|
||||||
|
if (e.keyCode !== 65 || !e.ctrlKey) return;
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this.selectAll();
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="file-display"
|
||||||
|
onClick={this.displayClick.bind(this)}
|
||||||
|
onContextMenu={this.props.contextMenu}
|
||||||
|
>
|
||||||
|
<div className="box-display">
|
||||||
|
{this.fileBoxKeysByPosition().map((boxUuid, index) => (
|
||||||
|
<React.Fragment key={boxUuid}>
|
||||||
|
{this.props.fileBoxes[boxUuid].filtered && (
|
||||||
|
<FileBox
|
||||||
|
file={this.props.fileBoxes[boxUuid].file}
|
||||||
|
boxUuid={boxUuid}
|
||||||
|
selected={this.props.fileBoxes[boxUuid].selected}
|
||||||
|
contextMenu={this.props.contextMenu}
|
||||||
|
contextSelect={this.contextSelect.bind(this)}
|
||||||
|
removeDriveContextMenu={this.props.removeDriveContextMenu}
|
||||||
|
onSelection={this.onSelection.bind(this)}
|
||||||
|
onBoxKeyPress={this.onBoxKeyPress.bind(this)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="file-display-spacer" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileDisplay.propTypes = {
|
||||||
|
files: PropTypes.object,
|
||||||
|
};
|
||||||
|
export default FileDisplay;
|
150
src/stash/StashContextMenu.jsx
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
import React from "react";
|
||||||
|
import axios from "axios";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import {
|
||||||
|
faInfoCircle,
|
||||||
|
faFileDownload,
|
||||||
|
faTrash,
|
||||||
|
faEye,
|
||||||
|
faShareSquare,
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
//Local Imports
|
||||||
|
import "./scss/stash/StashContextMenu.scss";
|
||||||
|
import { serverUrls, serverAddress } from "./api.json";
|
||||||
|
//Constants
|
||||||
|
const downloadUrl = `${serverAddress}/${serverUrls.POST.downloadUrl}`;
|
||||||
|
const deleteUrl = `${serverAddress}/${serverUrls.POST.deleteUrl}`;
|
||||||
|
const publicUrl = `${serverAddress}/${serverUrls.POST.publicUrl}`;
|
||||||
|
const rawUrl = `${serverAddress}/${serverUrls.GET.rawUrl}`;
|
||||||
|
|
||||||
|
function getConfig() {
|
||||||
|
var authToken = localStorage.getItem("authToken");
|
||||||
|
return { headers: { authorization: `Bearer ${authToken}` } };
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class StashContextMenu extends React.Component {
|
||||||
|
infoView() {
|
||||||
|
var selectedCount = this.props.getSelectedBoxes().length;
|
||||||
|
if (selectedCount === 1) return "View";
|
||||||
|
if (selectedCount > 1) return `${selectedCount} files selected`;
|
||||||
|
return "No Files Selected";
|
||||||
|
}
|
||||||
|
|
||||||
|
infoClick(e) {
|
||||||
|
const selectedBoxes = this.props.getSelectedBoxes();
|
||||||
|
if (selectedBoxes.length !== 1) return;
|
||||||
|
const file = selectedBoxes[0];
|
||||||
|
let win = window.open(`${rawUrl}?target=${file}`);
|
||||||
|
if (!win || win.closed || typeof win.closed == "undefined") {
|
||||||
|
window.location = `${rawUrl}?target=${file}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
downloadClick() {
|
||||||
|
const selectedBoxes = this.props.getSelectedBoxes();
|
||||||
|
//ZIPS ARE NOT SUPPORTED YET
|
||||||
|
if (selectedBoxes.length > 1)
|
||||||
|
return toast.error("Downloading multiple files is not yet supported!");
|
||||||
|
else
|
||||||
|
return this.handleDownload(`${downloadUrl}?target=${selectedBoxes[0]}`);
|
||||||
|
}
|
||||||
|
deleteClick() {
|
||||||
|
const selectedBoxes = this.props.getSelectedBoxes();
|
||||||
|
axios
|
||||||
|
.post(deleteUrl, selectedBoxes, getConfig())
|
||||||
|
.then((res) => this.handleDelete(res, selectedBoxes))
|
||||||
|
.catch((e) => this.handleDelete(e.response, selectedBoxes));
|
||||||
|
}
|
||||||
|
publicClick() {
|
||||||
|
const selectedBoxes = this.props.getSelectedBoxes();
|
||||||
|
axios
|
||||||
|
.post(publicUrl, selectedBoxes, getConfig())
|
||||||
|
.then((res) => this.handlePublic(res, selectedBoxes))
|
||||||
|
.catch((e) => this.handlePublic(e.response, selectedBoxes));
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePublic(res, selectedBoxes) {
|
||||||
|
const failedFiles = res.data || [];
|
||||||
|
if (res.status !== 200)
|
||||||
|
toast.error("There was an issue making some files public!");
|
||||||
|
let fileBoxes = this.props.fileBoxes;
|
||||||
|
selectedBoxes.forEach((selectedBoxId) => {
|
||||||
|
if (!failedFiles.includes(selectedBoxId)) {
|
||||||
|
fileBoxes[selectedBoxId].file.public = !fileBoxes[selectedBoxId].file
|
||||||
|
.public;
|
||||||
|
} else {
|
||||||
|
fileBoxes[selectedBoxId].selected = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.props.fileBoxesChanged(fileBoxes);
|
||||||
|
}
|
||||||
|
handleDownload(url) {
|
||||||
|
let win = window.open(url);
|
||||||
|
if (!win || win.closed || typeof win.closed == "undefined")
|
||||||
|
window.location = url;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Handles the response from the deleteClick()
|
||||||
|
* @param {String} response server response
|
||||||
|
* @param {Array} selectedBoxes Selected Boxes object list
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
handleDelete(res, selectedBoxes) {
|
||||||
|
const failedFiles = res.data || [];
|
||||||
|
console.log(res);
|
||||||
|
if (res.status !== 200) toast.error("Error Deleting Some Files");
|
||||||
|
let fileBoxes = this.props.fileBoxes;
|
||||||
|
selectedBoxes.forEach((selectedBoxId) => {
|
||||||
|
if (!failedFiles.includes(selectedBoxId)) {
|
||||||
|
delete fileBoxes[selectedBoxId];
|
||||||
|
} else {
|
||||||
|
fileBoxes[selectedBoxId].selected = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.props.fileBoxesChanged(fileBoxes);
|
||||||
|
}
|
||||||
|
shareClick() {}
|
||||||
|
|
||||||
|
styleCalc() {
|
||||||
|
const estimatedHeight = 180; //px
|
||||||
|
const esetimatedWidth = 290; //px
|
||||||
|
const bodyWidth = document.body.offsetWidth;
|
||||||
|
const bodyHeight = document.documentElement.offsetHeight;
|
||||||
|
let top = this.props.y;
|
||||||
|
let left = this.props.x;
|
||||||
|
const overFlowX = left + esetimatedWidth > bodyWidth;
|
||||||
|
const overFlowY = top + estimatedHeight > bodyHeight;
|
||||||
|
if (overFlowX) left = left - esetimatedWidth;
|
||||||
|
if (overFlowY) top = top - estimatedHeight;
|
||||||
|
return { top: `${top}px`, left: `${left}px` };
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className="drive-context-menu" style={this.styleCalc()}>
|
||||||
|
<ul>
|
||||||
|
<li onClick={this.infoClick.bind(this)}>
|
||||||
|
<FontAwesomeIcon icon={faInfoCircle} />
|
||||||
|
{this.infoView()}
|
||||||
|
</li>
|
||||||
|
<li onClick={this.downloadClick.bind(this)}>
|
||||||
|
<FontAwesomeIcon icon={faFileDownload} />
|
||||||
|
Download
|
||||||
|
</li>
|
||||||
|
<li onClick={this.deleteClick.bind(this)}>
|
||||||
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
|
Delete
|
||||||
|
</li>
|
||||||
|
<li onClick={this.publicClick.bind(this)}>
|
||||||
|
<FontAwesomeIcon icon={faEye} />
|
||||||
|
Toggle Public
|
||||||
|
</li>
|
||||||
|
<li onClick={this.shareClick.bind(this)}>
|
||||||
|
<FontAwesomeIcon icon={faShareSquare} />
|
||||||
|
Share
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
31
src/stash/StashDropzone.jsx
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import React from "react";
|
||||||
|
import Dropzone from "react-dropzone";
|
||||||
|
import FileDisplay from "./FileDisplay";
|
||||||
|
|
||||||
|
import "./scss/stash/StashDropzone.scss";
|
||||||
|
class StashDropzone extends React.Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Dropzone onDrop={(acceptedFiles) => this.props.addUpload(acceptedFiles)}>
|
||||||
|
{({ getRootProps, getInputProps }) => (
|
||||||
|
<div className="stash-dropzone" {...getRootProps()}>
|
||||||
|
<input
|
||||||
|
id="file-dropzone"
|
||||||
|
{...getInputProps()}
|
||||||
|
style={{ display: "none", visibility: "hidden" }}
|
||||||
|
/>
|
||||||
|
<FileDisplay
|
||||||
|
fileBoxes={this.props.fileBoxes}
|
||||||
|
fileBoxesChanged={this.props.fileBoxesChanged}
|
||||||
|
contextMenu={this.props.contextMenu}
|
||||||
|
removeDriveContextMenu={this.props.removeDriveContextMenu}
|
||||||
|
getSelectedBoxes={this.props.getSelectedBoxes}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Dropzone>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default StashDropzone;
|
198
src/stash/StashUpload.jsx
Normal file
|
@ -0,0 +1,198 @@
|
||||||
|
//Module Imports
|
||||||
|
import React from "react";
|
||||||
|
import axios, { CancelToken } from "axios";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
//Local Imports
|
||||||
|
import StashDropzone from "./StashDropzone";
|
||||||
|
import StashUploadDialog from "./uploader/StashUploadDialog";
|
||||||
|
//Constants
|
||||||
|
import { serverUrls, serverFields, serverAddress } from "./api.json";
|
||||||
|
const uploadUrl = `${serverAddress}/${serverUrls.POST.uploadUrl}`;
|
||||||
|
const uploadField = serverFields.uploadField;
|
||||||
|
const cancelMessage = "User Canceled";
|
||||||
|
const successClearTime = 200;
|
||||||
|
|
||||||
|
function buildUpload(file, uploadUuid, onProgress) {
|
||||||
|
var authToken = localStorage.getItem("authToken");
|
||||||
|
var upload = {
|
||||||
|
file,
|
||||||
|
uploadUuid,
|
||||||
|
progress: 0,
|
||||||
|
status: null,
|
||||||
|
started: false,
|
||||||
|
};
|
||||||
|
const cancelToken = new CancelToken((cancel) => {
|
||||||
|
upload.cancelUpload = () => cancel(cancelMessage);
|
||||||
|
});
|
||||||
|
upload.config = {
|
||||||
|
headers: { authorization: `Bearer ${authToken}`, filesize: file.size },
|
||||||
|
onUploadProgress: (e) => onProgress(e, uploadUuid),
|
||||||
|
cancelToken,
|
||||||
|
};
|
||||||
|
return upload;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class StashUpload extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
uploads: {},
|
||||||
|
errorCount: 0,
|
||||||
|
fadeOnClear: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadProgress(e, uploadUuid) {
|
||||||
|
const totalLength = e.lengthComputable
|
||||||
|
? e.total
|
||||||
|
: e.target.getResponseHeader("content-length") ||
|
||||||
|
e.target.getResponseHeader("x-decompressed-content-length");
|
||||||
|
if (totalLength !== null) {
|
||||||
|
const loaded = Math.round((e.loaded * 100) / totalLength);
|
||||||
|
var uploads = this.state.uploads;
|
||||||
|
if (loaded === 100 || uploads[uploadUuid] == null) return;
|
||||||
|
uploads[uploadUuid].progress = loaded;
|
||||||
|
this.setState({ uploads });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startAllUploads() {
|
||||||
|
var uploads = this.state.uploads;
|
||||||
|
for (var u in uploads) {
|
||||||
|
if (uploads[u].started) continue;
|
||||||
|
uploads[u].started = true;
|
||||||
|
this.startUpload(uploads[u]);
|
||||||
|
}
|
||||||
|
this.setState({ uploads });
|
||||||
|
}
|
||||||
|
|
||||||
|
startUpload(upload) {
|
||||||
|
const data = new FormData();
|
||||||
|
data.append(uploadField, upload.file);
|
||||||
|
axios
|
||||||
|
.post(uploadUrl, data, upload.config)
|
||||||
|
.then((res) => this.uploadDone(res, upload))
|
||||||
|
.catch((e) => this.uploadError(e, upload));
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadDone(res, upload) {
|
||||||
|
if (res.status !== 200) return this.showError(upload.uploadUuid);
|
||||||
|
var uploads = this.state.uploads;
|
||||||
|
delete uploads[upload.uploadUuid].cancelToken;
|
||||||
|
this.setState({ uploads });
|
||||||
|
this.showSuccess(upload.uploadUuid);
|
||||||
|
this.props.addFilebox(res.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadError(e, upload) {
|
||||||
|
if (e.message === cancelMessage) console.log("Upload Canceled");
|
||||||
|
else if (e.response == null) toast.error("Unknown Error Occured!");
|
||||||
|
else if (e.response.status === 401) toast.error("Not Logged In!");
|
||||||
|
else if (e.response.status === 500) toast.error("Drive Full!");
|
||||||
|
this.showError(upload.uploadUuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
showError(uploadUuid) {
|
||||||
|
var uploads = this.state.uploads;
|
||||||
|
uploads[uploadUuid].status = "Error";
|
||||||
|
this.setState({ uploads, errorCount: this.state.errorCount + 1 });
|
||||||
|
}
|
||||||
|
|
||||||
|
showSuccess(uploadUuid) {
|
||||||
|
var uploads = this.state.uploads;
|
||||||
|
uploads[uploadUuid].status = "Success";
|
||||||
|
this.setState({ uploads });
|
||||||
|
setTimeout(() => this.removeUpload(uploadUuid), successClearTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
addUpload(files) {
|
||||||
|
var uploads = this.state.uploads;
|
||||||
|
const uploadTime = Date.now();
|
||||||
|
var uploadUuid;
|
||||||
|
files.forEach((file, i) => {
|
||||||
|
uploadUuid = `${uploadTime}-${i}`;
|
||||||
|
uploads[uploadUuid] = buildUpload(
|
||||||
|
file,
|
||||||
|
uploadUuid,
|
||||||
|
this.uploadProgress.bind(this)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
this.setState({ uploads }, this.startAllUploads);
|
||||||
|
}
|
||||||
|
|
||||||
|
retryUpload(uploadUuid) {
|
||||||
|
if (!uploadUuid) return;
|
||||||
|
var uploads = this.state.uploads;
|
||||||
|
var errorCount = this.state.errorCount;
|
||||||
|
const file = uploads[uploadUuid].file;
|
||||||
|
//Remove error count if the upload errored because we're now removing it
|
||||||
|
if (uploads[uploadUuid].status === "Error") errorCount--;
|
||||||
|
//Update and remove the upload
|
||||||
|
this.removeUpload(uploadUuid);
|
||||||
|
this.addUpload([file]);
|
||||||
|
this.setState({ errorCount });
|
||||||
|
}
|
||||||
|
clearUpload(uploadUuid) {
|
||||||
|
var uploads = this.state.uploads;
|
||||||
|
if (uploads[uploadUuid].status !== null) this.removeUpload(uploadUuid);
|
||||||
|
else uploads[uploadUuid].cancelUpload();
|
||||||
|
}
|
||||||
|
clearAll() {
|
||||||
|
var uploads = this.state.uploads;
|
||||||
|
var onlyPending = true;
|
||||||
|
var u;
|
||||||
|
for (u in uploads) {
|
||||||
|
if (uploads[u].status !== null) {
|
||||||
|
delete uploads[u];
|
||||||
|
onlyPending = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//If onlypending cancel all uploads currently remaining
|
||||||
|
if (onlyPending) for (u in uploads) uploads[u].cancelUpload();
|
||||||
|
this.setState({ uploads, errorCount: 0 });
|
||||||
|
}
|
||||||
|
retryAll() {
|
||||||
|
var uploads = this.state.uploads;
|
||||||
|
//Splicing so itterate backwards
|
||||||
|
//(retryUpload is what calls the splice via removeUpload)
|
||||||
|
for (var u in uploads) {
|
||||||
|
if (uploads[u].status === "Error") this.retryUpload(u);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
removeUpload(uploadUuid) {
|
||||||
|
if (!uploadUuid) return;
|
||||||
|
//Remove error count if the upload errored because we're now removing it
|
||||||
|
var errorCount = this.state.errorCount;
|
||||||
|
var fadeOnClear = this.state.fadeOnClear;
|
||||||
|
var uploads = this.state.uploads;
|
||||||
|
if (uploads[uploadUuid].status === "Error") errorCount--;
|
||||||
|
//Update and remove the upload
|
||||||
|
delete uploads[uploadUuid];
|
||||||
|
if (Object.keys(uploads).length === 0) fadeOnClear = true;
|
||||||
|
this.setState({ uploads, errorCount, fadeOnClear });
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<StashDropzone
|
||||||
|
fileBoxes={this.props.fileBoxes}
|
||||||
|
fileBoxesChanged={this.props.fileBoxesChanged}
|
||||||
|
addUpload={this.addUpload.bind(this)}
|
||||||
|
contextMenu={this.props.contextMenu}
|
||||||
|
removeDriveContextMenu={this.props.removeDriveContextMenu}
|
||||||
|
getSelectedBoxes={this.props.getSelectedBoxes}
|
||||||
|
/>
|
||||||
|
<StashUploadDialog
|
||||||
|
fadeOnClear={this.state.fadeOnClear}
|
||||||
|
uploads={this.state.uploads}
|
||||||
|
errorCount={this.state.errorCount}
|
||||||
|
clearAll={this.clearAll.bind(this)}
|
||||||
|
retryAll={this.retryAll.bind(this)}
|
||||||
|
clearUpload={this.clearUpload.bind(this)}
|
||||||
|
retryUpload={this.retryUpload.bind(this)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
39
src/stash/Stashbar.jsx
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
//Module Imports
|
||||||
|
import React from "react";
|
||||||
|
//Local Imports
|
||||||
|
import StashbarMenu from "./stashbar/StashbarMenu.jsx";
|
||||||
|
import StashbarSearch from "./stashbar/StashbarSearch.jsx";
|
||||||
|
class Stashbar extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
searchMode: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
setSearchMode(value) {
|
||||||
|
this.setState({ searchMode: value });
|
||||||
|
}
|
||||||
|
|
||||||
|
whichBar() {
|
||||||
|
if (this.state.searchMode)
|
||||||
|
return (
|
||||||
|
<StashbarSearch
|
||||||
|
fileBoxes={this.props.fileBoxes}
|
||||||
|
fileBoxesChanged={this.props.fileBoxesChanged}
|
||||||
|
setSearchMode={this.setSearchMode.bind(this)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
else return <StashbarMenu setSearchMode={this.setSearchMode.bind(this)} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className="stashbar">
|
||||||
|
<div className="stashbar-content">{this.whichBar()}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Stashbar;
|
20
src/stash/api.json
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"serverAddress": "http://nubian.dunestorm.net:52001",
|
||||||
|
"serverUrls": {
|
||||||
|
"POST": {
|
||||||
|
"uploadUrl": "api/stash/upload",
|
||||||
|
"downloadUrl": "api/stash/download",
|
||||||
|
"deleteUrl": "api/stash/delete",
|
||||||
|
"publicUrl": "api/stash/public"
|
||||||
|
},
|
||||||
|
"GET": {
|
||||||
|
"filesUrl": "api/stash/files",
|
||||||
|
"rawUrl": "api/stash/raw",
|
||||||
|
"avatar": "api/user/avatar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"serverFields": {
|
||||||
|
"uploadField": "user-selected-file"
|
||||||
|
},
|
||||||
|
"constants": { "jwtHeader": "authorization" }
|
||||||
|
}
|
17
src/stash/scss/Stash.scss
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
@import "./global";
|
||||||
|
|
||||||
|
.dunestash {
|
||||||
|
position: fixed;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stash {
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100% - #{$stashbarHeight});
|
||||||
|
position: relative;
|
||||||
|
margin: auto;
|
||||||
|
display: flex;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
35
src/stash/scss/_global.scss
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
@import "global/colors", "global/fonts", "global/measurements",
|
||||||
|
"global/animations";
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 0;
|
||||||
|
color: $foreground;
|
||||||
|
background: #eee;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
*::-webkit-scrollbar {
|
||||||
|
width: 5px;
|
||||||
|
height: 5px;
|
||||||
|
}
|
||||||
|
*::-webkit-scrollbar-track {
|
||||||
|
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
||||||
|
border-radius: $defaultBorderRadius;
|
||||||
|
}
|
||||||
|
*::-webkit-scrollbar-thumb {
|
||||||
|
border-radius: $defaultBorderRadius;
|
||||||
|
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.5);
|
||||||
|
background-color: lighten($sectionMenuOptions, 40%);
|
||||||
|
}
|
||||||
|
*::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: lighten($sectionMenuOptions, 50%);
|
||||||
|
}
|
||||||
|
input:focus,
|
||||||
|
select:focus,
|
||||||
|
textarea:focus,
|
||||||
|
button:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
121
src/stash/scss/global/_animations.scss
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
/*Shimmer for links*/
|
||||||
|
@-webkit-keyframes glowingShimmer {
|
||||||
|
0% {
|
||||||
|
background-position: -4rem top;
|
||||||
|
/*50px*/
|
||||||
|
}
|
||||||
|
70% {
|
||||||
|
background-position: 12.5rem top;
|
||||||
|
/*200px*/
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 12.5rem top;
|
||||||
|
/*200px*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fade-in {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes toastify-notification-fade-out {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes toast-notification-fade-in {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0.9;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin animation-breathe($b1, $b2, $f1, $f2) {
|
||||||
|
@keyframes breathe {
|
||||||
|
0% {
|
||||||
|
background-color: $b1;
|
||||||
|
color: $f1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-color: $b2;
|
||||||
|
color: $f2;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-color: $b1;
|
||||||
|
color: $f1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@mixin animation-bounce($f1, $f2) {
|
||||||
|
@keyframes bounce {
|
||||||
|
0% {
|
||||||
|
color: $f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
color: $f2;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
color: $f1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes file-watcher-fade {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
80% {
|
||||||
|
background: $successColor;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes upload-dialog-expand {
|
||||||
|
0% {
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes upload-dialog-minimize {
|
||||||
|
0% {
|
||||||
|
height: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
|
}
|
16
src/stash/scss/global/_colors.scss
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
/*Status Colors*/
|
||||||
|
$successColor: limegreen;
|
||||||
|
$errorColor: #e20006;
|
||||||
|
$informationColor: #0095ff;
|
||||||
|
/*Background & Foreground colors*/
|
||||||
|
$background: rgba(20, 20, 20, 1);
|
||||||
|
$backgroundGradientStart: $background;
|
||||||
|
$backgroundGradientEnd: #848486;
|
||||||
|
$foreground: white;
|
||||||
|
$sectionForeground: $foreground;
|
||||||
|
/*Visual "components" (divs, etc)*/
|
||||||
|
$sectionBackground: rgba($background, 0.95);
|
||||||
|
$sectionMenu: darken(rgba($background, 0.8), 10%);
|
||||||
|
$sectionMenuOptions: rgba(lighten($sectionMenu, 16%), 98%);
|
||||||
|
$borderColor: #13120e;
|
||||||
|
/*Unique Components*/
|
48
src/stash/scss/global/_fonts.scss
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-family: "Dejavu Sans";
|
||||||
|
font-size: 33px;
|
||||||
|
font-style: normal;
|
||||||
|
font-variant: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2, label {
|
||||||
|
font-family: "Dejavu Sans";
|
||||||
|
font-size: 28px;
|
||||||
|
font-style: normal;
|
||||||
|
font-variant: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-family: "Dejavu Sans";
|
||||||
|
font-size: 24px;
|
||||||
|
font-style: normal;
|
||||||
|
font-variant: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
p, a {
|
||||||
|
font-family: "Dejavu Sans";
|
||||||
|
font-size: 18px;
|
||||||
|
font-style: normal;
|
||||||
|
font-variant: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
font-family: "Dejavu Sans";
|
||||||
|
font-size: 21px;
|
||||||
|
font-style: normal;
|
||||||
|
font-variant: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 30px; }
|
||||||
|
|
||||||
|
pre {
|
||||||
|
font-family: "Dejavu Sans";
|
||||||
|
font-size: 13px;
|
||||||
|
font-style: normal;
|
||||||
|
font-variant: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
13
src/stash/scss/global/_measurements.scss
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
/*Spacing*/
|
||||||
|
$contentTopGap: 0px;
|
||||||
|
/*Size*/
|
||||||
|
$pageMinWidth: 300px;
|
||||||
|
$pageMaxWidth: 780px;
|
||||||
|
$defaultBoxPadding: 15px;
|
||||||
|
/*Components*/
|
||||||
|
$stashbarHeight: 4rem;
|
||||||
|
$fileFiltersHeight: 1.5rem;
|
||||||
|
$toastMinWidth: calc(#{$pageMinWidth} - 100px);
|
||||||
|
$toastMaxWidth: calc(50% - 20px);
|
||||||
|
/*Misc*/
|
||||||
|
$defaultBorderRadius: 2px;
|
61
src/stash/scss/stash/FileBox.scss
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
@import "../global";
|
||||||
|
.filebox {
|
||||||
|
margin: 0.4rem 0.4rem;
|
||||||
|
display: flex;
|
||||||
|
max-height: 4rem;
|
||||||
|
height: 100%;
|
||||||
|
max-width: 25em;
|
||||||
|
padding: 0.4rem 0;
|
||||||
|
width: 100%;
|
||||||
|
align-self: flex-start;
|
||||||
|
position: relative;
|
||||||
|
background: $sectionMenuOptions;
|
||||||
|
box-shadow: 1px 3px 2px $sectionMenuOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filebox:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
background: lighten($sectionMenuOptions, 3%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filebox.selected {
|
||||||
|
background: $informationColor;
|
||||||
|
box-shadow: 1px 3px 2px darken($informationColor, 20%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filebox.selected:hover {
|
||||||
|
background: lighten($informationColor, 3%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file {
|
||||||
|
color: white;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
max-height: 4rem;
|
||||||
|
height: 100%;
|
||||||
|
max-width: 25em;
|
||||||
|
padding: 0.4rem 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-details {
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 100%;
|
||||||
|
height: 50%;
|
||||||
|
margin: auto;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-info-details {
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.125rem 0;
|
||||||
|
max-width: 39ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-subinfo-details {
|
||||||
|
padding: 0 5px;
|
||||||
|
}
|
23
src/stash/scss/stash/FileDisplay.scss
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
@import "../global";
|
||||||
|
.file-display {
|
||||||
|
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-display{
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
flex-grow: 1;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-khtml-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-o-user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-display-spacer {
|
||||||
|
width: 100%;
|
||||||
|
height: 2rem;
|
||||||
|
}
|
120
src/stash/scss/stash/Searchbar.scss
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
@import "../global";
|
||||||
|
$searchIndent: 0.125rem;
|
||||||
|
|
||||||
|
.file-searchbar {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
background: darken(#eee, 5%);
|
||||||
|
box-shadow: 0.25px 1px 0.5px darken(#eee, 8%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-searchbox {
|
||||||
|
margin: 0 auto;
|
||||||
|
display: inline-flex;
|
||||||
|
background: inherit;
|
||||||
|
color: $sectionMenuOptions;
|
||||||
|
width: 100%;
|
||||||
|
border-bottom: 1px darken(#eee, 22%) solid;
|
||||||
|
}
|
||||||
|
.file-searchbar svg {
|
||||||
|
margin: auto 0;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0 0.15rem 0 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#file-search {
|
||||||
|
width: 100%;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
background: inherit;
|
||||||
|
text-indent: $searchIndent;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
#file-search::placeholder {
|
||||||
|
color: $sectionMenuOptions;
|
||||||
|
}
|
||||||
|
.file-searchbox-action {
|
||||||
|
padding: 0 0.325rem;
|
||||||
|
height: inherit;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-searchbox-action svg {
|
||||||
|
margin: auto;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#file-searchbox-hashtag {
|
||||||
|
padding-right: calc(0.325rem + #{$searchIndent} * 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-searchbox-action:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-searchbar-extensions {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-filters {
|
||||||
|
background: darken(#eee, 10%);
|
||||||
|
width: 100%;
|
||||||
|
display: inline-flex;
|
||||||
|
flex-grow: nowrap;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: flex-start;
|
||||||
|
overflow-x: auto;
|
||||||
|
border-bottom: 1px darken(#eee, 12%) solid;
|
||||||
|
}
|
||||||
|
.file-filter {
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
display: inline-flex;
|
||||||
|
margin: auto 0.5rem;
|
||||||
|
padding: 2px;
|
||||||
|
width: 5rem;
|
||||||
|
background: $informationColor;
|
||||||
|
box-shadow: 0.375px 1.125px 0.75px darken($informationColor, 20%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-filter svg {
|
||||||
|
margin: auto 0;
|
||||||
|
width: 0.8rem;
|
||||||
|
height: 0.8rem;
|
||||||
|
}
|
||||||
|
.file-filter svg:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-filters {
|
||||||
|
width: 100%;
|
||||||
|
background: darken(#eee, 10%);
|
||||||
|
border-bottom: 1px darken(#eee, 20%) solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-filter-list {
|
||||||
|
width: 100%;
|
||||||
|
height: 1.5rem;
|
||||||
|
display: inline-flex;
|
||||||
|
flex-grow: nowrap;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: flex-start;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-filter {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
color: $sectionMenuOptions;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
display: inline-flex;
|
||||||
|
border: 1px darken(#eee, 22%) solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-filter-list span:first-child {
|
||||||
|
border-left: 0px;
|
||||||
|
}
|
48
src/stash/scss/stash/StashContextMenu.scss
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
@import "../global";
|
||||||
|
//Context Menu
|
||||||
|
.drive-context-menu {
|
||||||
|
position: fixed;
|
||||||
|
display: flex;
|
||||||
|
background: lighten($sectionMenuOptions, 10%);
|
||||||
|
box-shadow: 1px 3px 2px lighten ($sectionMenuOptions, 5%);
|
||||||
|
color: $foreground;
|
||||||
|
z-index: 300;
|
||||||
|
width: 100%;
|
||||||
|
max-width: inherit;
|
||||||
|
font-size: 1rem;
|
||||||
|
max-width: 18em;
|
||||||
|
min-height: 11em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drive-context-menu ul {
|
||||||
|
list-style-type: none;
|
||||||
|
padding: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drive-context-menu li:hover {
|
||||||
|
opacity: 0.85;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drive-context-menu li {
|
||||||
|
width: inherit;
|
||||||
|
text-align: left;
|
||||||
|
padding: 0 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drive-context-menu svg {
|
||||||
|
min-width: 1rem;
|
||||||
|
padding: 0 0.5rem;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drive-context-menu li:not(:last-child) {
|
||||||
|
margin-bottom: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drive-context-menu a {
|
||||||
|
color: $foreground;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
6
src/stash/scss/stash/StashDropzone.scss
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
@import "../global";
|
||||||
|
.stash-dropzone {
|
||||||
|
position: absolute;
|
||||||
|
height: 100%;
|
||||||
|
width: inherit;
|
||||||
|
}
|
161
src/stash/scss/stash/StashUploadDialog.scss
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
@import "../global";
|
||||||
|
//Variables
|
||||||
|
$watcherIndicatorSize: 18px;
|
||||||
|
$watcherActionSize: 14px;
|
||||||
|
$uploadBoxesSize: 50px;
|
||||||
|
$actionButtonSize: 25px;
|
||||||
|
/*File Upload Dialog Styling*/
|
||||||
|
.fud {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
background-color: $sectionMenuOptions;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
max-width: 540px;
|
||||||
|
max-height: 250px;
|
||||||
|
overflow: hidden;
|
||||||
|
-webkit-transform: translate3d(0, 0, 0);
|
||||||
|
z-index: 100;
|
||||||
|
display: block;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fud-fade {
|
||||||
|
visibility: visible;
|
||||||
|
animation: file-watcher-fade 2s forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fud-maximized {
|
||||||
|
visibility: visible;
|
||||||
|
animation: upload-dialog-expand 1.2s forwards;
|
||||||
|
animation-timing-function: ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fud-minimized {
|
||||||
|
visibility: visible;
|
||||||
|
animation: upload-dialog-minimize 0.4s forwards;
|
||||||
|
animation-timing-function: ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
#fud-header {
|
||||||
|
display: inline-flex;
|
||||||
|
height: $uploadBoxesSize;
|
||||||
|
font-size: $watcherIndicatorSize;
|
||||||
|
background-color: darken($sectionMenuOptions, 10%);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fud-header-title-wrapper {
|
||||||
|
margin: auto;
|
||||||
|
padding: 10px;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-align: center;
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#fud-header-title {
|
||||||
|
margin-left: 40px;
|
||||||
|
}
|
||||||
|
#fud-header-title:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#fud-minimize {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#header-actions {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fud-actions {
|
||||||
|
//width: calc(#{$actionButtonSize} * 3);
|
||||||
|
display: inline-flex;
|
||||||
|
margin: auto;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
height: $uploadBoxesSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fud-action {
|
||||||
|
min-width: $actionButtonSize;
|
||||||
|
padding: 0 5px;
|
||||||
|
|
||||||
|
font-size: $watcherActionSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fud-action svg,
|
||||||
|
i {
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fud-actions span:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#fud-header-status {
|
||||||
|
font-size: calc(#{$watcherIndicatorSize - 2px});
|
||||||
|
min-width: $uploadBoxesSize;
|
||||||
|
min-height: $uploadBoxesSize;
|
||||||
|
max-width: $uploadBoxesSize;
|
||||||
|
max-height: $uploadBoxesSize;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*Error and status icon Start with errors displaying as none*/
|
||||||
|
#fud-status-icon {
|
||||||
|
margin: auto;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fud-error-wrapper {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 9px;
|
||||||
|
left: 32px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
.fud-error-wrapper {
|
||||||
|
color: $errorColor;
|
||||||
|
max-width: 18px;
|
||||||
|
max-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#fud-error-count {
|
||||||
|
font-size: 14px;
|
||||||
|
color: $foreground;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fud-status-error {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#fud-queued-files {
|
||||||
|
max-height: 200px;
|
||||||
|
height: 100%;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
#fud-queued-files::-webkit-scrollbar {
|
||||||
|
width: 5px;
|
||||||
|
}
|
||||||
|
#fud-queued-files::-webkit-scrollbar-track {
|
||||||
|
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
||||||
|
border-radius: $defaultBorderRadius;
|
||||||
|
}
|
||||||
|
#fud-queued-files::-webkit-scrollbar-thumb {
|
||||||
|
border-radius: $defaultBorderRadius;
|
||||||
|
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.5);
|
||||||
|
background-color: lighten($sectionMenuOptions, 40%);
|
||||||
|
}
|
||||||
|
#fud-queued-files::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: lighten($sectionMenuOptions, 50%);
|
||||||
|
}
|
79
src/stash/scss/stash/StashUploadWatcher.scss
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
@import "./StashUploadDialog.scss";
|
||||||
|
/*File Watcher Styling*/
|
||||||
|
.file-watcher {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
display: inline-flex;
|
||||||
|
max-height: $uploadBoxesSize;
|
||||||
|
overflow: hidden;
|
||||||
|
word-wrap: break-word;
|
||||||
|
padding: none;
|
||||||
|
}
|
||||||
|
.file-watcher-progressbar {
|
||||||
|
min-width: $uploadBoxesSize;
|
||||||
|
min-height: $uploadBoxesSize;
|
||||||
|
max-width: $uploadBoxesSize;
|
||||||
|
max-height: $uploadBoxesSize;
|
||||||
|
display: flex;
|
||||||
|
max-height: $uploadBoxesSize;
|
||||||
|
background-color: lighten($sectionMenuOptions, 20%);
|
||||||
|
}
|
||||||
|
.file-watcher.active {
|
||||||
|
background-color: lighten($sectionMenuOptions, 10%);
|
||||||
|
}
|
||||||
|
.file-watcher.success {
|
||||||
|
animation: file-watcher-fade 2s forwards;
|
||||||
|
}
|
||||||
|
.file-watcher-progressbar-fill {
|
||||||
|
height: 100%;
|
||||||
|
width: 0%;
|
||||||
|
background: $successColor;
|
||||||
|
display: flex;
|
||||||
|
font-weight: bold;
|
||||||
|
//align-items: center;
|
||||||
|
transition: width 0.22s;
|
||||||
|
}
|
||||||
|
.file-watcher-progressbar-fill.error {
|
||||||
|
background: $errorColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-watcher-progressbar-indicator {
|
||||||
|
min-width: $uploadBoxesSize;
|
||||||
|
min-height: $uploadBoxesSize;
|
||||||
|
max-width: $uploadBoxesSize;
|
||||||
|
max-height: $uploadBoxesSize;
|
||||||
|
color: $foreground;
|
||||||
|
display: flex;
|
||||||
|
font-size: $watcherIndicatorSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-watcher-progressbar-indicator svg,
|
||||||
|
span {
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-watcher-name {
|
||||||
|
margin: auto;
|
||||||
|
overflow: hidden;
|
||||||
|
word-wrap: break-word;
|
||||||
|
padding-left: 10px;
|
||||||
|
align-self: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.file-watcher-action {
|
||||||
|
width: calc(#{$uploadBoxesSize} / 2);
|
||||||
|
height: $uploadBoxesSize;
|
||||||
|
display: flex;
|
||||||
|
font-weight: bold;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0px 5px;
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
.file-watcher-action svg {
|
||||||
|
margin: auto;
|
||||||
|
font-size: $watcherActionSize;
|
||||||
|
}
|
||||||
|
.file-watcher-action:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
34
src/stash/scss/stash/Stashbar.scss
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
@import "../global";
|
||||||
|
|
||||||
|
.stashbar {
|
||||||
|
width: 100%;
|
||||||
|
z-index: 200;
|
||||||
|
}
|
||||||
|
.stashbar-menu {
|
||||||
|
width: 100%;
|
||||||
|
height: $stashbarHeight;
|
||||||
|
display: inline-flex;
|
||||||
|
background-color: rgba($sectionMenu, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stashbar-action {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stashbar-action svg {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stashbar-action label {
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
.stashbar-action label:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stashbar-action:hover {
|
||||||
|
opacity: 0.85;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
33
src/stash/stashbar/StashbarMenu.jsx
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
faBars,
|
||||||
|
faCloudUploadAlt,
|
||||||
|
faSearch,
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import "../scss/stash/Stashbar.scss";
|
||||||
|
|
||||||
|
class StashbarMenu extends React.Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className="stashbar-menu">
|
||||||
|
<span className="stashbar-action">
|
||||||
|
<FontAwesomeIcon icon={faBars} />
|
||||||
|
</span>
|
||||||
|
<span className="stashbar-action">
|
||||||
|
<label htmlFor="file-dropzone">
|
||||||
|
<FontAwesomeIcon icon={faCloudUploadAlt} />
|
||||||
|
</label>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="stashbar-action"
|
||||||
|
onClick={() => this.props.setSearchMode(true)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faSearch} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default StashbarMenu;
|
174
src/stash/stashbar/StashbarSearch.jsx
Normal file
|
@ -0,0 +1,174 @@
|
||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
faTimes,
|
||||||
|
faArrowLeft,
|
||||||
|
faHashtag,
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
//Local imports
|
||||||
|
import StashbarSearchTagDisplay from "./StashbarSearchTagDisplay";
|
||||||
|
import "../scss/stash/Searchbar.scss";
|
||||||
|
//Constants
|
||||||
|
const hashtagChar = "#";
|
||||||
|
const searchFilters = ["Selected", "Public"];
|
||||||
|
class StashbarSearch extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
tags: [],
|
||||||
|
query: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//Filtering Functions
|
||||||
|
|
||||||
|
updateQuery(query) {
|
||||||
|
this.setState({ query }, this.updateFiltered);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFiltered() {
|
||||||
|
var fileBoxes = this.markAllFiltered();
|
||||||
|
const activeTags = this.state.tags;
|
||||||
|
const query = this.state.query.toLowerCase();
|
||||||
|
for (var f in fileBoxes) {
|
||||||
|
//If file isn't selected
|
||||||
|
if (activeTags.includes(searchFilters[0]) && !fileBoxes[f].selected)
|
||||||
|
fileBoxes[f].filtered = false;
|
||||||
|
//If file isn't public
|
||||||
|
if (activeTags.includes(searchFilters[1]) && !fileBoxes[f].file.public) {
|
||||||
|
fileBoxes[f].filtered = false;
|
||||||
|
}
|
||||||
|
if (!fileBoxes[f].file.name.toLowerCase().includes(query))
|
||||||
|
fileBoxes[f].filtered = false;
|
||||||
|
}
|
||||||
|
this.setState({ fileBoxes });
|
||||||
|
}
|
||||||
|
|
||||||
|
markAllFiltered() {
|
||||||
|
var fileBoxes = this.props.fileBoxes;
|
||||||
|
for (var f in fileBoxes) {
|
||||||
|
fileBoxes[f].filtered = true;
|
||||||
|
}
|
||||||
|
this.props.fileBoxesChanged(fileBoxes);
|
||||||
|
return fileBoxes;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Searchbar Functions
|
||||||
|
searchChanged(e) {
|
||||||
|
this.updateQuery(e.target.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
searchbarClose() {
|
||||||
|
this.markAllFiltered();
|
||||||
|
this.props.setSearchMode(false);
|
||||||
|
}
|
||||||
|
//Tag Functions
|
||||||
|
hashtagClick() {
|
||||||
|
this.updateQuery(hashtagChar);
|
||||||
|
}
|
||||||
|
|
||||||
|
queryTag(e) {
|
||||||
|
var query = this.state.query;
|
||||||
|
if (e.key !== "Enter" || query[0] !== hashtagChar) return;
|
||||||
|
const emptySpace = "";
|
||||||
|
const space = " ";
|
||||||
|
query = query.substring(1).toLowerCase();
|
||||||
|
const filters = this.getAvailableTags();
|
||||||
|
var filter = null;
|
||||||
|
for (filter of filters) {
|
||||||
|
if (filter.toLowerCase().includes(query)) break;
|
||||||
|
}
|
||||||
|
if (filter !== null) this.addTag(filter);
|
||||||
|
var firstSpace = query.indexOf(space);
|
||||||
|
if (firstSpace === -1) query = emptySpace;
|
||||||
|
else query = query.substring(query.indexOf(space));
|
||||||
|
this.updateQuery(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeTag(tag) {
|
||||||
|
var tags = this.state.tags;
|
||||||
|
tags.splice(tags.indexOf(tag), 1);
|
||||||
|
this.setState({ tags }, this.updateFiltered);
|
||||||
|
}
|
||||||
|
|
||||||
|
addTag(tag) {
|
||||||
|
var tags = this.state.tags;
|
||||||
|
tags.push(tag);
|
||||||
|
this.setState({ tags }, this.updateFiltered);
|
||||||
|
}
|
||||||
|
|
||||||
|
getAvailableTags() {
|
||||||
|
var availableFilters = [];
|
||||||
|
searchFilters.forEach((filter, i) => {
|
||||||
|
if (i === 0) return; //Ignore first filter 'Selected' triggered by selecting
|
||||||
|
if (this.state.tags.includes(filter)) return;
|
||||||
|
if (
|
||||||
|
!filter
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(this.state.query.substring(1).toLowerCase())
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
availableFilters.push(filter);
|
||||||
|
});
|
||||||
|
return availableFilters;
|
||||||
|
}
|
||||||
|
//Render
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<div className="file-searchbar stashbar-menu">
|
||||||
|
<div className="file-searchbox">
|
||||||
|
<span
|
||||||
|
className="file-searchbox-action"
|
||||||
|
onClick={this.searchbarClose.bind(this)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faArrowLeft} />
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="file-search"
|
||||||
|
name="file-search"
|
||||||
|
placeholder="Search"
|
||||||
|
value={this.state.query}
|
||||||
|
onChange={this.searchChanged.bind(this)}
|
||||||
|
autoComplete="off"
|
||||||
|
onKeyUp={this.queryTag.bind(this)}
|
||||||
|
></input>
|
||||||
|
<span
|
||||||
|
className="file-searchbox-action"
|
||||||
|
id="file-searchbox-hashtag"
|
||||||
|
onClick={this.hashtagClick.bind(this)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faHashtag} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="file-searchbar-extensions">
|
||||||
|
{this.state.query.includes(hashtagChar) && (
|
||||||
|
<StashbarSearchTagDisplay
|
||||||
|
query={this.state.query}
|
||||||
|
getAvailableTags={this.getAvailableTags.bind(this)}
|
||||||
|
addTag={this.addTag.bind(this)}
|
||||||
|
updateQuery={this.updateQuery.bind(this)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{this.state.tags.length > 0 && (
|
||||||
|
<div className="file-filters">
|
||||||
|
{this.state.tags.map((filter, index) => (
|
||||||
|
<span className="file-filter" key={index}>
|
||||||
|
<span> {filter}</span>{" "}
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faTimes}
|
||||||
|
onClick={() => this.removeTag(filter)}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default StashbarSearch;
|
38
src/stash/stashbar/StashbarSearchTagDisplay.jsx
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import React from "react";
|
||||||
|
class StashbarSearchTagDisplay extends React.Component {
|
||||||
|
filterClicked(tag) {
|
||||||
|
this.props.updateQuery("");
|
||||||
|
this.props.addTag(tag);
|
||||||
|
}
|
||||||
|
displayTags() {
|
||||||
|
const tags = this.props.getAvailableTags();
|
||||||
|
if (tags.length === 0)
|
||||||
|
return (
|
||||||
|
<span className="query-filter">No Filters Matched Your Search</span>
|
||||||
|
);
|
||||||
|
else
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
{tags.map((tag, index) => (
|
||||||
|
<span
|
||||||
|
className="query-filter"
|
||||||
|
key={index}
|
||||||
|
onClick={() => this.filterClicked.bind(this)(tag)}
|
||||||
|
>
|
||||||
|
{tag}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className="query-filters">
|
||||||
|
<div className="query-filter-list">{this.displayTags()}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default StashbarSearchTagDisplay;
|
108
src/stash/uploader/StashUploadDialog.jsx
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
//Module Imports
|
||||||
|
import React from "react";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import {
|
||||||
|
faExclamationTriangle,
|
||||||
|
faCloudUploadAlt,
|
||||||
|
faRedoAlt,
|
||||||
|
faTimes,
|
||||||
|
faAngleUp,
|
||||||
|
faAngleDown,
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
//Local Imports
|
||||||
|
import StashUploadWatcher from "./StashUploadWatcher";
|
||||||
|
import "../scss/stash/StashUploadDialog.scss";
|
||||||
|
//Icons List
|
||||||
|
const successIcon = <FontAwesomeIcon icon={faCloudUploadAlt} />;
|
||||||
|
const errorIcon = <FontAwesomeIcon icon={faExclamationTriangle} />;
|
||||||
|
const retryIcon = <FontAwesomeIcon icon={faRedoAlt} />;
|
||||||
|
const cancelIcon = <FontAwesomeIcon icon={faTimes} />;
|
||||||
|
const upIcon = <FontAwesomeIcon icon={faAngleUp} />;
|
||||||
|
const downIcon = <FontAwesomeIcon icon={faAngleDown} />;
|
||||||
|
export default class StashUploadDialog extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
isMinimized: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fudDisplay() {
|
||||||
|
const uploadLength = Object.values(this.props.uploads).length;
|
||||||
|
var className = "fud";
|
||||||
|
if (this.props.fadeOnClear && uploadLength === 0) {
|
||||||
|
return (className += " fud-fade");
|
||||||
|
}
|
||||||
|
if (this.state.isMinimized) return (className += " fud-minimized");
|
||||||
|
if (uploadLength > 0) {
|
||||||
|
return (className += " fud-maximized");
|
||||||
|
}
|
||||||
|
return className;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className={this.fudDisplay()}>
|
||||||
|
<div id="fud-header">
|
||||||
|
<div id="fud-header-status">
|
||||||
|
<span id="fud-status-icon">
|
||||||
|
{this.props.errorCount > 0 ? errorIcon : successIcon}
|
||||||
|
<span
|
||||||
|
className="fud-error-wrapper"
|
||||||
|
style={{ display: this.props.errorCount > 0 ? "flex" : "none" }}
|
||||||
|
>
|
||||||
|
<i className="fas fa-circle"></i>
|
||||||
|
<span id="fud-error-count">{this.props.errorCount}</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="fud-header-title-wrapper">
|
||||||
|
<span
|
||||||
|
id="fud-header-title"
|
||||||
|
onClick={() =>
|
||||||
|
this.setState({ isMinimized: !this.state.isMinimized })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Uploads
|
||||||
|
<span id="fud-minimize">
|
||||||
|
{this.state.isMinimized && upIcon}
|
||||||
|
{!this.state.isMinimized && downIcon}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="fud-actions" id="header-actions">
|
||||||
|
<span
|
||||||
|
id="fud-retry"
|
||||||
|
className="fud-action"
|
||||||
|
onClick={this.props.retryAll}
|
||||||
|
>
|
||||||
|
{retryIcon}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
id="fud-clear"
|
||||||
|
className="fud-action"
|
||||||
|
onClick={this.props.clearAll}
|
||||||
|
>
|
||||||
|
{cancelIcon}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="fud-queued-files">
|
||||||
|
{Object.values(this.props.uploads).map(
|
||||||
|
(upload, index) =>
|
||||||
|
upload && (
|
||||||
|
<StashUploadWatcher
|
||||||
|
file={upload.file}
|
||||||
|
key={upload.uploadUuid}
|
||||||
|
retryUpload={() => this.props.retryUpload(upload.uploadUuid)}
|
||||||
|
clearUpload={() => this.props.clearUpload(upload.uploadUuid)}
|
||||||
|
uploadProgress={upload.progress}
|
||||||
|
uploadStatus={upload.status}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
84
src/stash/uploader/StashUploadWatcher.jsx
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
import React from "react";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import {
|
||||||
|
faExclamationTriangle,
|
||||||
|
faCheck,
|
||||||
|
faRedoAlt,
|
||||||
|
faTimes,
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
//Local Imports
|
||||||
|
import "../scss/stash/StashUploadWatcher.scss";
|
||||||
|
|
||||||
|
//Constants
|
||||||
|
const successIcon = <FontAwesomeIcon icon={faCheck} />;
|
||||||
|
const errorIcon = <FontAwesomeIcon icon={faExclamationTriangle} />;
|
||||||
|
const retryIcon = <FontAwesomeIcon icon={faRedoAlt} />;
|
||||||
|
const cancelIcon = <FontAwesomeIcon icon={faTimes} />;
|
||||||
|
export default class StashUploadWatcher extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.retryUpload = props.retryUpload;
|
||||||
|
this.clearUpload = props.clearUpload;
|
||||||
|
}
|
||||||
|
|
||||||
|
isUploading() {
|
||||||
|
return this.props.uploadProgress > 0 && this.props.uploadProgress !== 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
progressIndicator() {
|
||||||
|
if (this.props.uploadStatus === "Success") return successIcon;
|
||||||
|
if (this.props.uploadStatus === "Error") return errorIcon;
|
||||||
|
if (this.isUploading())
|
||||||
|
return (
|
||||||
|
<span className="file-watcher-progressbar-text">
|
||||||
|
{this.props.uploadProgress}%
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
return <span />;
|
||||||
|
}
|
||||||
|
|
||||||
|
watcherStatus() {
|
||||||
|
var className = "file-watcher";
|
||||||
|
if (this.isUploading() && this.props.uploadStatus !== "Error")
|
||||||
|
className += " active";
|
||||||
|
if (this.props.uploadStatus === "Success") className += " success";
|
||||||
|
return className;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className={this.watcherStatus()}>
|
||||||
|
<div className="file-watcher-progressbar">
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
this.props.uploadStatus === "Error"
|
||||||
|
? "file-watcher-progressbar-fill error"
|
||||||
|
: "file-watcher-progressbar-fill"
|
||||||
|
}
|
||||||
|
style={{
|
||||||
|
width:
|
||||||
|
this.props.uploadStatus === "Error"
|
||||||
|
? "100%"
|
||||||
|
: `${this.props.uploadProgress}%`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="file-watcher-progressbar-indicator">
|
||||||
|
{this.progressIndicator()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span className="file-watcher-name">{this.props.file.name}</span>
|
||||||
|
<div className="fud-actions">
|
||||||
|
{this.props.uploadStatus === "Error" && (
|
||||||
|
<span className="file-watcher-action" onClick={this.retryUpload}>
|
||||||
|
{retryIcon}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className="file-watcher-action" onClick={this.clearUpload}>
|
||||||
|
{cancelIcon}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|