Dunestash Public Frontend 0.0.1-a.1

This commit is contained in:
Dunemask 2021-07-24 23:06:32 -06:00
commit fe36970476
67 changed files with 2179 additions and 0 deletions

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
# For Deploy
build/
node_modules/
package-lock.json

10
Dockerfile Normal file
View 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
View 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"
}
}

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 970 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 745 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 752 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 594 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 904 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 746 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 725 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
public/icons/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

1
public/icons/logo.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 21 KiB

View 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
View 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
View 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
View file

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

31
src/App.js Normal file
View 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
View 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
View 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
View 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
View 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
View 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;

View 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>
);
}
}

View 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
View 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
View 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
View 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
View 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;
}

View 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;
}

View 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;
}
}

View 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*/

View 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;
}

View 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;

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View file

@ -0,0 +1,6 @@
@import "../global";
.stash-dropzone {
position: absolute;
height: 100%;
width: inherit;
}

View 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%);
}

View 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;
}

View 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;
}

View 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;

View 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;

View 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;

View 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>
);
}
}

View 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>
);
}
}