commit fe36970476693c5c8b681385e4610c880fc4f80a Author: Dunemask Date: Sat Jul 24 23:06:32 2021 -0600 Dunestash Public Frontend 0.0.1-a.1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2d196ee --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +# For Deploy +build/ +node_modules/ +package-lock.json diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..bf311e9 --- /dev/null +++ b/Dockerfile @@ -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 diff --git a/package.json b/package.json new file mode 100644 index 0000000..08b48a4 --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/public/extras/blank_user.svg b/public/extras/blank_user.svg new file mode 100644 index 0000000..03fcf85 --- /dev/null +++ b/public/extras/blank_user.svg @@ -0,0 +1,9 @@ + + +Created by potrace 1.16, written by Peter Selinger 2001-2019 + + +Layer 1 + + + \ No newline at end of file diff --git a/public/icons/android-chrome-192x192.png b/public/icons/android-chrome-192x192.png new file mode 100644 index 0000000..27923ab Binary files /dev/null and b/public/icons/android-chrome-192x192.png differ diff --git a/public/icons/android-chrome-512x512.png b/public/icons/android-chrome-512x512.png new file mode 100644 index 0000000..6b07b09 Binary files /dev/null and b/public/icons/android-chrome-512x512.png differ diff --git a/public/icons/apple-touch-icon-114x114-precomposed.png b/public/icons/apple-touch-icon-114x114-precomposed.png new file mode 100644 index 0000000..97f1d27 Binary files /dev/null and b/public/icons/apple-touch-icon-114x114-precomposed.png differ diff --git a/public/icons/apple-touch-icon-114x114.png b/public/icons/apple-touch-icon-114x114.png new file mode 100644 index 0000000..6f88cd6 Binary files /dev/null and b/public/icons/apple-touch-icon-114x114.png differ diff --git a/public/icons/apple-touch-icon-120x120-precomposed.png b/public/icons/apple-touch-icon-120x120-precomposed.png new file mode 100644 index 0000000..464c684 Binary files /dev/null and b/public/icons/apple-touch-icon-120x120-precomposed.png differ diff --git a/public/icons/apple-touch-icon-120x120.png b/public/icons/apple-touch-icon-120x120.png new file mode 100644 index 0000000..001d479 Binary files /dev/null and b/public/icons/apple-touch-icon-120x120.png differ diff --git a/public/icons/apple-touch-icon-144x144-precomposed.png b/public/icons/apple-touch-icon-144x144-precomposed.png new file mode 100644 index 0000000..d72bed9 Binary files /dev/null and b/public/icons/apple-touch-icon-144x144-precomposed.png differ diff --git a/public/icons/apple-touch-icon-144x144.png b/public/icons/apple-touch-icon-144x144.png new file mode 100644 index 0000000..ba92f5d Binary files /dev/null and b/public/icons/apple-touch-icon-144x144.png differ diff --git a/public/icons/apple-touch-icon-152x152-precomposed.png b/public/icons/apple-touch-icon-152x152-precomposed.png new file mode 100644 index 0000000..13d3760 Binary files /dev/null and b/public/icons/apple-touch-icon-152x152-precomposed.png differ diff --git a/public/icons/apple-touch-icon-152x152.png b/public/icons/apple-touch-icon-152x152.png new file mode 100644 index 0000000..49e1fb7 Binary files /dev/null and b/public/icons/apple-touch-icon-152x152.png differ diff --git a/public/icons/apple-touch-icon-180x180-precomposed.png b/public/icons/apple-touch-icon-180x180-precomposed.png new file mode 100644 index 0000000..97d306d Binary files /dev/null and b/public/icons/apple-touch-icon-180x180-precomposed.png differ diff --git a/public/icons/apple-touch-icon-180x180.png b/public/icons/apple-touch-icon-180x180.png new file mode 100644 index 0000000..8bfb541 Binary files /dev/null and b/public/icons/apple-touch-icon-180x180.png differ diff --git a/public/icons/apple-touch-icon-57x57-precomposed.png b/public/icons/apple-touch-icon-57x57-precomposed.png new file mode 100644 index 0000000..5cf0c1d Binary files /dev/null and b/public/icons/apple-touch-icon-57x57-precomposed.png differ diff --git a/public/icons/apple-touch-icon-57x57.png b/public/icons/apple-touch-icon-57x57.png new file mode 100644 index 0000000..bc29a8c Binary files /dev/null and b/public/icons/apple-touch-icon-57x57.png differ diff --git a/public/icons/apple-touch-icon-60x60-precomposed.png b/public/icons/apple-touch-icon-60x60-precomposed.png new file mode 100644 index 0000000..d2f24d0 Binary files /dev/null and b/public/icons/apple-touch-icon-60x60-precomposed.png differ diff --git a/public/icons/apple-touch-icon-60x60.png b/public/icons/apple-touch-icon-60x60.png new file mode 100644 index 0000000..360ddd3 Binary files /dev/null and b/public/icons/apple-touch-icon-60x60.png differ diff --git a/public/icons/apple-touch-icon-72x72-precomposed.png b/public/icons/apple-touch-icon-72x72-precomposed.png new file mode 100644 index 0000000..6ba6c2d Binary files /dev/null and b/public/icons/apple-touch-icon-72x72-precomposed.png differ diff --git a/public/icons/apple-touch-icon-72x72.png b/public/icons/apple-touch-icon-72x72.png new file mode 100644 index 0000000..52aea15 Binary files /dev/null and b/public/icons/apple-touch-icon-72x72.png differ diff --git a/public/icons/apple-touch-icon-76x76-precomposed.png b/public/icons/apple-touch-icon-76x76-precomposed.png new file mode 100644 index 0000000..83b6b7f Binary files /dev/null and b/public/icons/apple-touch-icon-76x76-precomposed.png differ diff --git a/public/icons/apple-touch-icon-76x76.png b/public/icons/apple-touch-icon-76x76.png new file mode 100644 index 0000000..971f6a8 Binary files /dev/null and b/public/icons/apple-touch-icon-76x76.png differ diff --git a/public/icons/apple-touch-icon-precomposed.png b/public/icons/apple-touch-icon-precomposed.png new file mode 100644 index 0000000..97d306d Binary files /dev/null and b/public/icons/apple-touch-icon-precomposed.png differ diff --git a/public/icons/apple-touch-icon.png b/public/icons/apple-touch-icon.png new file mode 100644 index 0000000..8bfb541 Binary files /dev/null and b/public/icons/apple-touch-icon.png differ diff --git a/public/icons/browserconfig.xml b/public/icons/browserconfig.xml new file mode 100644 index 0000000..6e1de61 --- /dev/null +++ b/public/icons/browserconfig.xml @@ -0,0 +1,9 @@ + + + + + + #ffc40d + + + diff --git a/public/icons/favicon-16x16.png b/public/icons/favicon-16x16.png new file mode 100644 index 0000000..5a03560 Binary files /dev/null and b/public/icons/favicon-16x16.png differ diff --git a/public/icons/favicon-32x32.png b/public/icons/favicon-32x32.png new file mode 100644 index 0000000..a0b035a Binary files /dev/null and b/public/icons/favicon-32x32.png differ diff --git a/public/icons/favicon.ico b/public/icons/favicon.ico new file mode 100644 index 0000000..0891573 Binary files /dev/null and b/public/icons/favicon.ico differ diff --git a/public/icons/logo.svg b/public/icons/logo.svg new file mode 100644 index 0000000..cd4a8f0 --- /dev/null +++ b/public/icons/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/mstile-150x150.png b/public/icons/mstile-150x150.png new file mode 100644 index 0000000..ee50858 Binary files /dev/null and b/public/icons/mstile-150x150.png differ diff --git a/public/icons/safari-pinned-tab.svg b/public/icons/safari-pinned-tab.svg new file mode 100644 index 0000000..7a94829 --- /dev/null +++ b/public/icons/safari-pinned-tab.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/site.webmanifest b/public/icons/site.webmanifest new file mode 100644 index 0000000..9a41863 --- /dev/null +++ b/public/icons/site.webmanifest @@ -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" +} diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..ae6a035 --- /dev/null +++ b/public/index.html @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + Dune Drive + + + +
+ + diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 0000000..ed35c17 --- /dev/null +++ b/public/manifest.json @@ -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" +} diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..e9e57dc --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/src/App.js b/src/App.js new file mode 100644 index 0000000..764a057 --- /dev/null +++ b/src/App.js @@ -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 ( + <> + + + + ); + } +} + +export default App; diff --git a/src/Stash.jsx b/src/Stash.jsx new file mode 100644 index 0000000..cde6c40 --- /dev/null +++ b/src/Stash.jsx @@ -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 ( +
+ + {this.state.contextMenu && ( + + )} + +
+ +
+
+ ); + } +} + +export default Stash; diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..b1ef1c0 --- /dev/null +++ b/src/index.js @@ -0,0 +1,10 @@ +import React from "react"; +import ReactDOM from "react-dom"; +import App from "./App"; + +ReactDOM.render( + + + , + document.getElementById("root") +); diff --git a/src/setupProxy.js b/src/setupProxy.js new file mode 100644 index 0000000..002a45f --- /dev/null +++ b/src/setupProxy.js @@ -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", + }) + ); +}; diff --git a/src/stash/FileBox.jsx b/src/stash/FileBox.jsx new file mode 100644 index 0000000..7ccfe80 --- /dev/null +++ b/src/stash/FileBox.jsx @@ -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 ( +
this.props.contextSelect(this.props.boxUuid)} + onKeyDown={this.props.onBoxKeyPress} + > +
+
+ {this.props.file.name} +
+
+ {this.readableDate()} + {this.props.file.public && ( + + {this.props.file.public && } + + )} +
+
+
+ ); + } +} +FileBox.propTypes = { + file: PropTypes.object, +}; + +export default FileBox; diff --git a/src/stash/FileDisplay.jsx b/src/stash/FileDisplay.jsx new file mode 100644 index 0000000..485e8b2 --- /dev/null +++ b/src/stash/FileDisplay.jsx @@ -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 ( +
+
+ {this.fileBoxKeysByPosition().map((boxUuid, index) => ( + + {this.props.fileBoxes[boxUuid].filtered && ( + + )} + + ))} +
+
+
+ ); + } +} + +FileDisplay.propTypes = { + files: PropTypes.object, +}; +export default FileDisplay; diff --git a/src/stash/StashContextMenu.jsx b/src/stash/StashContextMenu.jsx new file mode 100644 index 0000000..9f6e017 --- /dev/null +++ b/src/stash/StashContextMenu.jsx @@ -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 ( +
+
    +
  • + + {this.infoView()} +
  • +
  • + + Download +
  • +
  • + + Delete +
  • +
  • + + Toggle Public +
  • +
  • + + Share +
  • +
+
+ ); + } +} diff --git a/src/stash/StashDropzone.jsx b/src/stash/StashDropzone.jsx new file mode 100644 index 0000000..3c98ffe --- /dev/null +++ b/src/stash/StashDropzone.jsx @@ -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 ( + this.props.addUpload(acceptedFiles)}> + {({ getRootProps, getInputProps }) => ( +
+ + +
+ )} +
+ ); + } +} + +export default StashDropzone; diff --git a/src/stash/StashUpload.jsx b/src/stash/StashUpload.jsx new file mode 100644 index 0000000..fcebfd5 --- /dev/null +++ b/src/stash/StashUpload.jsx @@ -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 ( + <> + + + + ); + } +} diff --git a/src/stash/Stashbar.jsx b/src/stash/Stashbar.jsx new file mode 100644 index 0000000..f43dce1 --- /dev/null +++ b/src/stash/Stashbar.jsx @@ -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 ( + + ); + else return ; + } + + render() { + return ( +
+
{this.whichBar()}
+
+ ); + } +} + +export default Stashbar; diff --git a/src/stash/api.json b/src/stash/api.json new file mode 100644 index 0000000..29c3d4e --- /dev/null +++ b/src/stash/api.json @@ -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" } +} diff --git a/src/stash/scss/Stash.scss b/src/stash/scss/Stash.scss new file mode 100644 index 0000000..b7be9f9 --- /dev/null +++ b/src/stash/scss/Stash.scss @@ -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; +} diff --git a/src/stash/scss/_global.scss b/src/stash/scss/_global.scss new file mode 100644 index 0000000..3c3bc04 --- /dev/null +++ b/src/stash/scss/_global.scss @@ -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; +} diff --git a/src/stash/scss/global/_animations.scss b/src/stash/scss/global/_animations.scss new file mode 100644 index 0000000..ee02741 --- /dev/null +++ b/src/stash/scss/global/_animations.scss @@ -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; + } +} diff --git a/src/stash/scss/global/_colors.scss b/src/stash/scss/global/_colors.scss new file mode 100644 index 0000000..6c2d1c5 --- /dev/null +++ b/src/stash/scss/global/_colors.scss @@ -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*/ diff --git a/src/stash/scss/global/_fonts.scss b/src/stash/scss/global/_fonts.scss new file mode 100644 index 0000000..6a4379e --- /dev/null +++ b/src/stash/scss/global/_fonts.scss @@ -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; + } diff --git a/src/stash/scss/global/_measurements.scss b/src/stash/scss/global/_measurements.scss new file mode 100644 index 0000000..de682df --- /dev/null +++ b/src/stash/scss/global/_measurements.scss @@ -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; diff --git a/src/stash/scss/stash/FileBox.scss b/src/stash/scss/stash/FileBox.scss new file mode 100644 index 0000000..7f45b27 --- /dev/null +++ b/src/stash/scss/stash/FileBox.scss @@ -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; +} diff --git a/src/stash/scss/stash/FileDisplay.scss b/src/stash/scss/stash/FileDisplay.scss new file mode 100644 index 0000000..8a4c850 --- /dev/null +++ b/src/stash/scss/stash/FileDisplay.scss @@ -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; +} diff --git a/src/stash/scss/stash/Searchbar.scss b/src/stash/scss/stash/Searchbar.scss new file mode 100644 index 0000000..ff58d6b --- /dev/null +++ b/src/stash/scss/stash/Searchbar.scss @@ -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; +} diff --git a/src/stash/scss/stash/StashContextMenu.scss b/src/stash/scss/stash/StashContextMenu.scss new file mode 100644 index 0000000..a03b66d --- /dev/null +++ b/src/stash/scss/stash/StashContextMenu.scss @@ -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; +} diff --git a/src/stash/scss/stash/StashDropzone.scss b/src/stash/scss/stash/StashDropzone.scss new file mode 100644 index 0000000..3cd8971 --- /dev/null +++ b/src/stash/scss/stash/StashDropzone.scss @@ -0,0 +1,6 @@ +@import "../global"; +.stash-dropzone { + position: absolute; + height: 100%; + width: inherit; +} diff --git a/src/stash/scss/stash/StashUploadDialog.scss b/src/stash/scss/stash/StashUploadDialog.scss new file mode 100644 index 0000000..ba49789 --- /dev/null +++ b/src/stash/scss/stash/StashUploadDialog.scss @@ -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%); +} diff --git a/src/stash/scss/stash/StashUploadWatcher.scss b/src/stash/scss/stash/StashUploadWatcher.scss new file mode 100644 index 0000000..39fe3a4 --- /dev/null +++ b/src/stash/scss/stash/StashUploadWatcher.scss @@ -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; +} diff --git a/src/stash/scss/stash/Stashbar.scss b/src/stash/scss/stash/Stashbar.scss new file mode 100644 index 0000000..afd6c2a --- /dev/null +++ b/src/stash/scss/stash/Stashbar.scss @@ -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; +} diff --git a/src/stash/stashbar/StashbarMenu.jsx b/src/stash/stashbar/StashbarMenu.jsx new file mode 100644 index 0000000..3231840 --- /dev/null +++ b/src/stash/stashbar/StashbarMenu.jsx @@ -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 ( +
+ + + + + + + this.props.setSearchMode(true)} + > + + +
+ ); + } +} + +export default StashbarMenu; diff --git a/src/stash/stashbar/StashbarSearch.jsx b/src/stash/stashbar/StashbarSearch.jsx new file mode 100644 index 0000000..ce8fff6 --- /dev/null +++ b/src/stash/stashbar/StashbarSearch.jsx @@ -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 ( + +
+
+ + + + + + + +
+
+
+ {this.state.query.includes(hashtagChar) && ( + + )} + {this.state.tags.length > 0 && ( +
+ {this.state.tags.map((filter, index) => ( + + {filter}{" "} + this.removeTag(filter)} + /> + + ))} +
+ )} +
+
+ ); + } +} + +export default StashbarSearch; diff --git a/src/stash/stashbar/StashbarSearchTagDisplay.jsx b/src/stash/stashbar/StashbarSearchTagDisplay.jsx new file mode 100644 index 0000000..98f5c33 --- /dev/null +++ b/src/stash/stashbar/StashbarSearchTagDisplay.jsx @@ -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 ( + No Filters Matched Your Search + ); + else + return ( + + {tags.map((tag, index) => ( + this.filterClicked.bind(this)(tag)} + > + {tag} + + ))} + + ); + } + + render() { + return ( +
+
{this.displayTags()}
+
+ ); + } +} + +export default StashbarSearchTagDisplay; diff --git a/src/stash/uploader/StashUploadDialog.jsx b/src/stash/uploader/StashUploadDialog.jsx new file mode 100644 index 0000000..7ca1d83 --- /dev/null +++ b/src/stash/uploader/StashUploadDialog.jsx @@ -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 = ; +const errorIcon = ; +const retryIcon = ; +const cancelIcon = ; +const upIcon = ; +const downIcon = ; +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 ( +
+
+
+ + {this.props.errorCount > 0 ? errorIcon : successIcon} + 0 ? "flex" : "none" }} + > + + {this.props.errorCount} + + +
+
+ + this.setState({ isMinimized: !this.state.isMinimized }) + } + > + Uploads + + {this.state.isMinimized && upIcon} + {!this.state.isMinimized && downIcon} + + +
+
+ + {retryIcon} + + + {cancelIcon} + +
+
+
+ {Object.values(this.props.uploads).map( + (upload, index) => + upload && ( + this.props.retryUpload(upload.uploadUuid)} + clearUpload={() => this.props.clearUpload(upload.uploadUuid)} + uploadProgress={upload.progress} + uploadStatus={upload.status} + /> + ) + )} +
+
+ ); + } +} diff --git a/src/stash/uploader/StashUploadWatcher.jsx b/src/stash/uploader/StashUploadWatcher.jsx new file mode 100644 index 0000000..5f06277 --- /dev/null +++ b/src/stash/uploader/StashUploadWatcher.jsx @@ -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 = ; +const errorIcon = ; +const retryIcon = ; +const cancelIcon = ; +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 ( + + {this.props.uploadProgress}% + + ); + return ; + } + + 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 ( +
+
+
+ + {this.progressIndicator()} + +
+
+ {this.props.file.name} +
+ {this.props.uploadStatus === "Error" && ( + + {retryIcon} + + )} + + {cancelIcon} + +
+
+ ); + } +}