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