Dunestash Public Frontend 0.0.1-a.1

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

56
src/stash/FileBox.jsx Normal file
View file

@ -0,0 +1,56 @@
import React from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faUsers,
faEye,
faShareSquare,
} from "@fortawesome/free-solid-svg-icons";
import PropTypes from "prop-types";
import "./scss/stash/FileBox.scss";
class FileBox extends React.Component {
readableDate() {
let d = new Date(parseInt(this.props.file.date));
if (isNaN(d.getMonth())) return "";
return `${
d.getMonth() + 1
}/${d.getDate()}/${d.getFullYear()} ${d.getHours()}:${d.getMinutes()}`;
}
selectBox(e) {
e.stopPropagation();
e.preventDefault();
this.props.onSelection(e, this.props.boxUuid);
this.props.removeDriveContextMenu();
}
render() {
return (
<div
tabIndex="0"
className={"filebox" + (this.props.selected ? " selected" : "")}
onClick={this.selectBox.bind(this)}
onContextMenu={() => this.props.contextSelect(this.props.boxUuid)}
onKeyDown={this.props.onBoxKeyPress}
>
<div className="file">
<div className="file-details">
<span className="file-name">{this.props.file.name}</span>
</div>
<div className="file-details">
<span className="file-date">{this.readableDate()}</span>
{this.props.file.public && (
<span className="file-indicators file-subinfo-details">
{this.props.file.public && <FontAwesomeIcon icon={faEye} />}
</span>
)}
</div>
</div>
</div>
);
}
}
FileBox.propTypes = {
file: PropTypes.object,
};
export default FileBox;

125
src/stash/FileDisplay.jsx Normal file
View file

@ -0,0 +1,125 @@
//Module Imports
import React from "react";
import PropTypes from "prop-types";
//Local Imports
import FileBox from "./FileBox";
import "./scss/stash/FileDisplay.scss";
class FileDisplay extends React.Component {
constructor(props) {
super(props);
this.state = {
firstSelectionBoxUuid: null,
};
}
displayClick(e) {
e.preventDefault();
e.stopPropagation();
this.deselectAll();
this.props.removeDriveContextMenu();
}
fileBoxKeysByPosition() {
return Object.keys(this.props.fileBoxes).sort((a, b) => {
return a.position - b.position;
});
}
onSelection(e, boxUuid) {
var fileBoxes = this.props.fileBoxes;
var newBoxes;
const firstSelection = this.state.firstSelectionBoxUuid;
if (e.ctrlKey && firstSelection !== null)
newBoxes = this.segmentSelection(fileBoxes, boxUuid);
else if (e.shiftKey && firstSelection !== null)
newBoxes = this.multiSelection(fileBoxes, boxUuid);
else newBoxes = this.singleSelection(fileBoxes, boxUuid);
this.props.fileBoxesChanged(newBoxes);
}
singleSelection(fileBoxes, boxUuid) {
this.deselectAll();
this.setState({ firstSelectionBoxUuid: boxUuid });
fileBoxes[boxUuid].selected = true;
return fileBoxes;
}
segmentSelection(fileBoxes, boxUuid) {
fileBoxes[boxUuid].selected = !fileBoxes[boxUuid].selected;
return fileBoxes;
}
multiSelection(fileBoxes, boxUuid) {
this.deselectAll();
var firstIndex = fileBoxes[this.state.firstSelectionBoxUuid].position;
var endIndex = fileBoxes[boxUuid].position;
var boxKeys = this.fileBoxKeysByPosition();
if (endIndex < firstIndex) {
let tmp = endIndex;
endIndex = firstIndex;
firstIndex = tmp;
}
//Send selection 1 more for the slice
boxKeys.slice(firstIndex, endIndex + 1).forEach((boxId, i) => {
if (!fileBoxes[boxId].filtered) return;
fileBoxes[boxId].selected = true;
});
return fileBoxes;
}
contextSelect(boxUuid) {
if (this.props.getSelectedBoxes().length > 1) return;
this.onSelection({}, boxUuid);
}
deselectAll() {
var fileBoxes = this.props.fileBoxes;
for (var f in fileBoxes) fileBoxes[f].selected = false;
this.props.fileBoxesChanged(fileBoxes);
}
selectAll() {
var fileBoxes = this.props.fileBoxes;
for (var f in fileBoxes)
if (fileBoxes[f].filtered) fileBoxes[f].selected = true;
this.props.fileBoxesChanged(fileBoxes);
}
onBoxKeyPress(e) {
if (e.keyCode !== 65 || !e.ctrlKey) return;
e.preventDefault();
e.stopPropagation();
this.selectAll();
}
render() {
return (
<div
className="file-display"
onClick={this.displayClick.bind(this)}
onContextMenu={this.props.contextMenu}
>
<div className="box-display">
{this.fileBoxKeysByPosition().map((boxUuid, index) => (
<React.Fragment key={boxUuid}>
{this.props.fileBoxes[boxUuid].filtered && (
<FileBox
file={this.props.fileBoxes[boxUuid].file}
boxUuid={boxUuid}
selected={this.props.fileBoxes[boxUuid].selected}
contextMenu={this.props.contextMenu}
contextSelect={this.contextSelect.bind(this)}
removeDriveContextMenu={this.props.removeDriveContextMenu}
onSelection={this.onSelection.bind(this)}
onBoxKeyPress={this.onBoxKeyPress.bind(this)}
/>
)}
</React.Fragment>
))}
</div>
<div className="file-display-spacer" />
</div>
);
}
}
FileDisplay.propTypes = {
files: PropTypes.object,
};
export default FileDisplay;

View file

@ -0,0 +1,150 @@
import React from "react";
import axios from "axios";
import { toast } from "react-toastify";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faInfoCircle,
faFileDownload,
faTrash,
faEye,
faShareSquare,
} from "@fortawesome/free-solid-svg-icons";
//Local Imports
import "./scss/stash/StashContextMenu.scss";
import { serverUrls, serverAddress } from "./api.json";
//Constants
const downloadUrl = `${serverAddress}/${serverUrls.POST.downloadUrl}`;
const deleteUrl = `${serverAddress}/${serverUrls.POST.deleteUrl}`;
const publicUrl = `${serverAddress}/${serverUrls.POST.publicUrl}`;
const rawUrl = `${serverAddress}/${serverUrls.GET.rawUrl}`;
function getConfig() {
var authToken = localStorage.getItem("authToken");
return { headers: { authorization: `Bearer ${authToken}` } };
}
export default class StashContextMenu extends React.Component {
infoView() {
var selectedCount = this.props.getSelectedBoxes().length;
if (selectedCount === 1) return "View";
if (selectedCount > 1) return `${selectedCount} files selected`;
return "No Files Selected";
}
infoClick(e) {
const selectedBoxes = this.props.getSelectedBoxes();
if (selectedBoxes.length !== 1) return;
const file = selectedBoxes[0];
let win = window.open(`${rawUrl}?target=${file}`);
if (!win || win.closed || typeof win.closed == "undefined") {
window.location = `${rawUrl}?target=${file}`;
}
}
downloadClick() {
const selectedBoxes = this.props.getSelectedBoxes();
//ZIPS ARE NOT SUPPORTED YET
if (selectedBoxes.length > 1)
return toast.error("Downloading multiple files is not yet supported!");
else
return this.handleDownload(`${downloadUrl}?target=${selectedBoxes[0]}`);
}
deleteClick() {
const selectedBoxes = this.props.getSelectedBoxes();
axios
.post(deleteUrl, selectedBoxes, getConfig())
.then((res) => this.handleDelete(res, selectedBoxes))
.catch((e) => this.handleDelete(e.response, selectedBoxes));
}
publicClick() {
const selectedBoxes = this.props.getSelectedBoxes();
axios
.post(publicUrl, selectedBoxes, getConfig())
.then((res) => this.handlePublic(res, selectedBoxes))
.catch((e) => this.handlePublic(e.response, selectedBoxes));
}
handlePublic(res, selectedBoxes) {
const failedFiles = res.data || [];
if (res.status !== 200)
toast.error("There was an issue making some files public!");
let fileBoxes = this.props.fileBoxes;
selectedBoxes.forEach((selectedBoxId) => {
if (!failedFiles.includes(selectedBoxId)) {
fileBoxes[selectedBoxId].file.public = !fileBoxes[selectedBoxId].file
.public;
} else {
fileBoxes[selectedBoxId].selected = true;
}
});
this.props.fileBoxesChanged(fileBoxes);
}
handleDownload(url) {
let win = window.open(url);
if (!win || win.closed || typeof win.closed == "undefined")
window.location = url;
}
/**
* Handles the response from the deleteClick()
* @param {String} response server response
* @param {Array} selectedBoxes Selected Boxes object list
*
*/
handleDelete(res, selectedBoxes) {
const failedFiles = res.data || [];
console.log(res);
if (res.status !== 200) toast.error("Error Deleting Some Files");
let fileBoxes = this.props.fileBoxes;
selectedBoxes.forEach((selectedBoxId) => {
if (!failedFiles.includes(selectedBoxId)) {
delete fileBoxes[selectedBoxId];
} else {
fileBoxes[selectedBoxId].selected = true;
}
});
this.props.fileBoxesChanged(fileBoxes);
}
shareClick() {}
styleCalc() {
const estimatedHeight = 180; //px
const esetimatedWidth = 290; //px
const bodyWidth = document.body.offsetWidth;
const bodyHeight = document.documentElement.offsetHeight;
let top = this.props.y;
let left = this.props.x;
const overFlowX = left + esetimatedWidth > bodyWidth;
const overFlowY = top + estimatedHeight > bodyHeight;
if (overFlowX) left = left - esetimatedWidth;
if (overFlowY) top = top - estimatedHeight;
return { top: `${top}px`, left: `${left}px` };
}
render() {
return (
<div className="drive-context-menu" style={this.styleCalc()}>
<ul>
<li onClick={this.infoClick.bind(this)}>
<FontAwesomeIcon icon={faInfoCircle} />
{this.infoView()}
</li>
<li onClick={this.downloadClick.bind(this)}>
<FontAwesomeIcon icon={faFileDownload} />
Download
</li>
<li onClick={this.deleteClick.bind(this)}>
<FontAwesomeIcon icon={faTrash} />
Delete
</li>
<li onClick={this.publicClick.bind(this)}>
<FontAwesomeIcon icon={faEye} />
Toggle Public
</li>
<li onClick={this.shareClick.bind(this)}>
<FontAwesomeIcon icon={faShareSquare} />
Share
</li>
</ul>
</div>
);
}
}

View file

@ -0,0 +1,31 @@
import React from "react";
import Dropzone from "react-dropzone";
import FileDisplay from "./FileDisplay";
import "./scss/stash/StashDropzone.scss";
class StashDropzone extends React.Component {
render() {
return (
<Dropzone onDrop={(acceptedFiles) => this.props.addUpload(acceptedFiles)}>
{({ getRootProps, getInputProps }) => (
<div className="stash-dropzone" {...getRootProps()}>
<input
id="file-dropzone"
{...getInputProps()}
style={{ display: "none", visibility: "hidden" }}
/>
<FileDisplay
fileBoxes={this.props.fileBoxes}
fileBoxesChanged={this.props.fileBoxesChanged}
contextMenu={this.props.contextMenu}
removeDriveContextMenu={this.props.removeDriveContextMenu}
getSelectedBoxes={this.props.getSelectedBoxes}
/>
</div>
)}
</Dropzone>
);
}
}
export default StashDropzone;

198
src/stash/StashUpload.jsx Normal file
View file

@ -0,0 +1,198 @@
//Module Imports
import React from "react";
import axios, { CancelToken } from "axios";
import { toast } from "react-toastify";
//Local Imports
import StashDropzone from "./StashDropzone";
import StashUploadDialog from "./uploader/StashUploadDialog";
//Constants
import { serverUrls, serverFields, serverAddress } from "./api.json";
const uploadUrl = `${serverAddress}/${serverUrls.POST.uploadUrl}`;
const uploadField = serverFields.uploadField;
const cancelMessage = "User Canceled";
const successClearTime = 200;
function buildUpload(file, uploadUuid, onProgress) {
var authToken = localStorage.getItem("authToken");
var upload = {
file,
uploadUuid,
progress: 0,
status: null,
started: false,
};
const cancelToken = new CancelToken((cancel) => {
upload.cancelUpload = () => cancel(cancelMessage);
});
upload.config = {
headers: { authorization: `Bearer ${authToken}`, filesize: file.size },
onUploadProgress: (e) => onProgress(e, uploadUuid),
cancelToken,
};
return upload;
}
export default class StashUpload extends React.Component {
constructor(props) {
super(props);
this.state = {
uploads: {},
errorCount: 0,
fadeOnClear: false,
};
}
uploadProgress(e, uploadUuid) {
const totalLength = e.lengthComputable
? e.total
: e.target.getResponseHeader("content-length") ||
e.target.getResponseHeader("x-decompressed-content-length");
if (totalLength !== null) {
const loaded = Math.round((e.loaded * 100) / totalLength);
var uploads = this.state.uploads;
if (loaded === 100 || uploads[uploadUuid] == null) return;
uploads[uploadUuid].progress = loaded;
this.setState({ uploads });
}
}
startAllUploads() {
var uploads = this.state.uploads;
for (var u in uploads) {
if (uploads[u].started) continue;
uploads[u].started = true;
this.startUpload(uploads[u]);
}
this.setState({ uploads });
}
startUpload(upload) {
const data = new FormData();
data.append(uploadField, upload.file);
axios
.post(uploadUrl, data, upload.config)
.then((res) => this.uploadDone(res, upload))
.catch((e) => this.uploadError(e, upload));
}
uploadDone(res, upload) {
if (res.status !== 200) return this.showError(upload.uploadUuid);
var uploads = this.state.uploads;
delete uploads[upload.uploadUuid].cancelToken;
this.setState({ uploads });
this.showSuccess(upload.uploadUuid);
this.props.addFilebox(res.data);
}
uploadError(e, upload) {
if (e.message === cancelMessage) console.log("Upload Canceled");
else if (e.response == null) toast.error("Unknown Error Occured!");
else if (e.response.status === 401) toast.error("Not Logged In!");
else if (e.response.status === 500) toast.error("Drive Full!");
this.showError(upload.uploadUuid);
}
showError(uploadUuid) {
var uploads = this.state.uploads;
uploads[uploadUuid].status = "Error";
this.setState({ uploads, errorCount: this.state.errorCount + 1 });
}
showSuccess(uploadUuid) {
var uploads = this.state.uploads;
uploads[uploadUuid].status = "Success";
this.setState({ uploads });
setTimeout(() => this.removeUpload(uploadUuid), successClearTime);
}
addUpload(files) {
var uploads = this.state.uploads;
const uploadTime = Date.now();
var uploadUuid;
files.forEach((file, i) => {
uploadUuid = `${uploadTime}-${i}`;
uploads[uploadUuid] = buildUpload(
file,
uploadUuid,
this.uploadProgress.bind(this)
);
});
this.setState({ uploads }, this.startAllUploads);
}
retryUpload(uploadUuid) {
if (!uploadUuid) return;
var uploads = this.state.uploads;
var errorCount = this.state.errorCount;
const file = uploads[uploadUuid].file;
//Remove error count if the upload errored because we're now removing it
if (uploads[uploadUuid].status === "Error") errorCount--;
//Update and remove the upload
this.removeUpload(uploadUuid);
this.addUpload([file]);
this.setState({ errorCount });
}
clearUpload(uploadUuid) {
var uploads = this.state.uploads;
if (uploads[uploadUuid].status !== null) this.removeUpload(uploadUuid);
else uploads[uploadUuid].cancelUpload();
}
clearAll() {
var uploads = this.state.uploads;
var onlyPending = true;
var u;
for (u in uploads) {
if (uploads[u].status !== null) {
delete uploads[u];
onlyPending = false;
}
}
//If onlypending cancel all uploads currently remaining
if (onlyPending) for (u in uploads) uploads[u].cancelUpload();
this.setState({ uploads, errorCount: 0 });
}
retryAll() {
var uploads = this.state.uploads;
//Splicing so itterate backwards
//(retryUpload is what calls the splice via removeUpload)
for (var u in uploads) {
if (uploads[u].status === "Error") this.retryUpload(u);
}
}
removeUpload(uploadUuid) {
if (!uploadUuid) return;
//Remove error count if the upload errored because we're now removing it
var errorCount = this.state.errorCount;
var fadeOnClear = this.state.fadeOnClear;
var uploads = this.state.uploads;
if (uploads[uploadUuid].status === "Error") errorCount--;
//Update and remove the upload
delete uploads[uploadUuid];
if (Object.keys(uploads).length === 0) fadeOnClear = true;
this.setState({ uploads, errorCount, fadeOnClear });
}
render() {
return (
<>
<StashDropzone
fileBoxes={this.props.fileBoxes}
fileBoxesChanged={this.props.fileBoxesChanged}
addUpload={this.addUpload.bind(this)}
contextMenu={this.props.contextMenu}
removeDriveContextMenu={this.props.removeDriveContextMenu}
getSelectedBoxes={this.props.getSelectedBoxes}
/>
<StashUploadDialog
fadeOnClear={this.state.fadeOnClear}
uploads={this.state.uploads}
errorCount={this.state.errorCount}
clearAll={this.clearAll.bind(this)}
retryAll={this.retryAll.bind(this)}
clearUpload={this.clearUpload.bind(this)}
retryUpload={this.retryUpload.bind(this)}
/>
</>
);
}
}

39
src/stash/Stashbar.jsx Normal file
View file

@ -0,0 +1,39 @@
//Module Imports
import React from "react";
//Local Imports
import StashbarMenu from "./stashbar/StashbarMenu.jsx";
import StashbarSearch from "./stashbar/StashbarSearch.jsx";
class Stashbar extends React.Component {
constructor(props) {
super(props);
this.state = {
searchMode: false,
};
}
setSearchMode(value) {
this.setState({ searchMode: value });
}
whichBar() {
if (this.state.searchMode)
return (
<StashbarSearch
fileBoxes={this.props.fileBoxes}
fileBoxesChanged={this.props.fileBoxesChanged}
setSearchMode={this.setSearchMode.bind(this)}
/>
);
else return <StashbarMenu setSearchMode={this.setSearchMode.bind(this)} />;
}
render() {
return (
<div className="stashbar">
<div className="stashbar-content">{this.whichBar()}</div>
</div>
);
}
}
export default Stashbar;

20
src/stash/api.json Normal file
View file

@ -0,0 +1,20 @@
{
"serverAddress": "http://nubian.dunestorm.net:52001",
"serverUrls": {
"POST": {
"uploadUrl": "api/stash/upload",
"downloadUrl": "api/stash/download",
"deleteUrl": "api/stash/delete",
"publicUrl": "api/stash/public"
},
"GET": {
"filesUrl": "api/stash/files",
"rawUrl": "api/stash/raw",
"avatar": "api/user/avatar"
}
},
"serverFields": {
"uploadField": "user-selected-file"
},
"constants": { "jwtHeader": "authorization" }
}

17
src/stash/scss/Stash.scss Normal file
View file

@ -0,0 +1,17 @@
@import "./global";
.dunestash {
position: fixed;
width: 100%;
height: 100%;
}
.stash {
width: 100%;
height: calc(100% - #{$stashbarHeight});
position: relative;
margin: auto;
display: flex;
overflow-x: hidden;
overflow-y: scroll;
}

View file

@ -0,0 +1,35 @@
@import "global/colors", "global/fonts", "global/measurements",
"global/animations";
html,
body {
margin-left: auto;
margin-right: auto;
text-align: center;
margin-top: 0;
color: $foreground;
background: #eee;
height: 100%;
width: 100%;
}
*::-webkit-scrollbar {
width: 5px;
height: 5px;
}
*::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
border-radius: $defaultBorderRadius;
}
*::-webkit-scrollbar-thumb {
border-radius: $defaultBorderRadius;
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.5);
background-color: lighten($sectionMenuOptions, 40%);
}
*::-webkit-scrollbar-thumb:hover {
background-color: lighten($sectionMenuOptions, 50%);
}
input:focus,
select:focus,
textarea:focus,
button:focus {
outline: none;
}

View file

@ -0,0 +1,121 @@
/*Shimmer for links*/
@-webkit-keyframes glowingShimmer {
0% {
background-position: -4rem top;
/*50px*/
}
70% {
background-position: 12.5rem top;
/*200px*/
}
100% {
background-position: 12.5rem top;
/*200px*/
}
}
@keyframes fade-in {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes toastify-notification-fade-out {
0% {
opacity: 1;
}
100% {
opacity: 0;
visibility: hidden;
}
}
@keyframes toast-notification-fade-in {
0% {
opacity: 0;
}
100% {
opacity: 0.9;
visibility: visible;
}
}
@mixin animation-breathe($b1, $b2, $f1, $f2) {
@keyframes breathe {
0% {
background-color: $b1;
color: $f1;
}
50% {
background-color: $b2;
color: $f2;
}
100% {
background-color: $b1;
color: $f1;
}
}
}
@mixin animation-bounce($f1, $f2) {
@keyframes bounce {
0% {
color: $f1;
}
50% {
color: $f2;
}
100% {
color: $f1;
}
}
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@keyframes file-watcher-fade {
0% {
opacity: 1;
}
40% {
opacity: 0;
visibility: hidden;
}
80% {
background: $successColor;
}
100% {
opacity: 0;
visibility: hidden;
}
}
@keyframes upload-dialog-expand {
0% {
height: 50px;
}
100% {
height: 100%;
}
}
@keyframes upload-dialog-minimize {
0% {
height: 50%;
}
100% {
height: 50px;
}
}

View file

@ -0,0 +1,16 @@
/*Status Colors*/
$successColor: limegreen;
$errorColor: #e20006;
$informationColor: #0095ff;
/*Background & Foreground colors*/
$background: rgba(20, 20, 20, 1);
$backgroundGradientStart: $background;
$backgroundGradientEnd: #848486;
$foreground: white;
$sectionForeground: $foreground;
/*Visual "components" (divs, etc)*/
$sectionBackground: rgba($background, 0.95);
$sectionMenu: darken(rgba($background, 0.8), 10%);
$sectionMenuOptions: rgba(lighten($sectionMenu, 16%), 98%);
$borderColor: #13120e;
/*Unique Components*/

View file

@ -0,0 +1,48 @@
h1 {
font-family: "Dejavu Sans";
font-size: 33px;
font-style: normal;
font-variant: normal;
font-weight: 700;
}
h2, label {
font-family: "Dejavu Sans";
font-size: 28px;
font-style: normal;
font-variant: normal;
font-weight: 700;
}
h3 {
font-family: "Dejavu Sans";
font-size: 24px;
font-style: normal;
font-variant: normal;
font-weight: 700;
}
p, a {
font-family: "Dejavu Sans";
font-size: 18px;
font-style: normal;
font-variant: normal;
font-weight: 400;
}
blockquote {
font-family: "Dejavu Sans";
font-size: 21px;
font-style: normal;
font-variant: normal;
font-weight: 400;
line-height: 30px; }
pre {
font-family: "Dejavu Sans";
font-size: 13px;
font-style: normal;
font-variant: normal;
font-weight: 400;
}

View file

@ -0,0 +1,13 @@
/*Spacing*/
$contentTopGap: 0px;
/*Size*/
$pageMinWidth: 300px;
$pageMaxWidth: 780px;
$defaultBoxPadding: 15px;
/*Components*/
$stashbarHeight: 4rem;
$fileFiltersHeight: 1.5rem;
$toastMinWidth: calc(#{$pageMinWidth} - 100px);
$toastMaxWidth: calc(50% - 20px);
/*Misc*/
$defaultBorderRadius: 2px;

View file

@ -0,0 +1,61 @@
@import "../global";
.filebox {
margin: 0.4rem 0.4rem;
display: flex;
max-height: 4rem;
height: 100%;
max-width: 25em;
padding: 0.4rem 0;
width: 100%;
align-self: flex-start;
position: relative;
background: $sectionMenuOptions;
box-shadow: 1px 3px 2px $sectionMenuOptions;
}
.filebox:hover {
cursor: pointer;
background: lighten($sectionMenuOptions, 3%);
}
.filebox.selected {
background: $informationColor;
box-shadow: 1px 3px 2px darken($informationColor, 20%);
}
.filebox.selected:hover {
background: lighten($informationColor, 3%);
}
.file {
color: white;
justify-content: center;
text-align: center;
max-height: 4rem;
height: 100%;
max-width: 25em;
padding: 0.4rem 0;
width: 100%;
}
.file-details {
display: flex;
overflow: hidden;
white-space: nowrap;
width: 100%;
height: 50%;
margin: auto;
justify-content: center;
}
.file-info-details {
overflow: hidden;
white-space: nowrap;
width: 100%;
padding: 0.125rem 0;
max-width: 39ch;
}
.file-subinfo-details {
padding: 0 5px;
}

View file

@ -0,0 +1,23 @@
@import "../global";
.file-display {
height: 100%;
}
.box-display{
display: flex;
flex-flow: row;
flex-wrap: wrap;
flex-grow: 1;
align-items: flex-start;
justify-content: center;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-o-user-select: none;
}
.file-display-spacer {
width: 100%;
height: 2rem;
}

View file

@ -0,0 +1,120 @@
@import "../global";
$searchIndent: 0.125rem;
.file-searchbar {
width: 100%;
display: flex;
flex-wrap: wrap;
background: darken(#eee, 5%);
box-shadow: 0.25px 1px 0.5px darken(#eee, 8%);
}
.file-searchbox {
margin: 0 auto;
display: inline-flex;
background: inherit;
color: $sectionMenuOptions;
width: 100%;
border-bottom: 1px darken(#eee, 22%) solid;
}
.file-searchbar svg {
margin: auto 0;
height: 100%;
padding: 0 0.15rem 0 0.5rem;
}
#file-search {
width: 100%;
font-size: 1.125rem;
background: inherit;
text-indent: $searchIndent;
border: none;
}
#file-search::placeholder {
color: $sectionMenuOptions;
}
.file-searchbox-action {
padding: 0 0.325rem;
height: inherit;
display: block;
}
.file-searchbox-action svg {
margin: auto;
height: 100%;
}
#file-searchbox-hashtag {
padding-right: calc(0.325rem + #{$searchIndent} * 4);
}
.file-searchbox-action:hover {
cursor: pointer;
}
.file-searchbar-extensions {
width: 100%;
}
.file-filters {
background: darken(#eee, 10%);
width: 100%;
display: inline-flex;
flex-grow: nowrap;
justify-content: flex-start;
align-items: flex-start;
overflow-x: auto;
border-bottom: 1px darken(#eee, 12%) solid;
}
.file-filter {
justify-content: center;
text-align: center;
display: inline-flex;
margin: auto 0.5rem;
padding: 2px;
width: 5rem;
background: $informationColor;
box-shadow: 0.375px 1.125px 0.75px darken($informationColor, 20%);
}
.file-filter svg {
margin: auto 0;
width: 0.8rem;
height: 0.8rem;
}
.file-filter svg:hover {
cursor: pointer;
}
.query-filters {
width: 100%;
background: darken(#eee, 10%);
border-bottom: 1px darken(#eee, 20%) solid;
}
.query-filter-list {
width: 100%;
height: 1.5rem;
display: inline-flex;
flex-grow: nowrap;
justify-content: flex-start;
align-items: flex-start;
overflow-x: auto;
overflow-y: hidden;
}
.query-filter {
display: inline-flex;
align-items: center;
color: $sectionMenuOptions;
width: 100%;
height: 100%;
justify-content: center;
text-align: center;
display: inline-flex;
border: 1px darken(#eee, 22%) solid;
}
.query-filter-list span:first-child {
border-left: 0px;
}

View file

@ -0,0 +1,48 @@
@import "../global";
//Context Menu
.drive-context-menu {
position: fixed;
display: flex;
background: lighten($sectionMenuOptions, 10%);
box-shadow: 1px 3px 2px lighten ($sectionMenuOptions, 5%);
color: $foreground;
z-index: 300;
width: 100%;
max-width: inherit;
font-size: 1rem;
max-width: 18em;
min-height: 11em;
}
.drive-context-menu ul {
list-style-type: none;
padding: 0;
width: 100%;
}
.drive-context-menu li:hover {
opacity: 0.85;
cursor: pointer;
}
.drive-context-menu li {
width: inherit;
text-align: left;
padding: 0 0.5rem;
}
.drive-context-menu svg {
min-width: 1rem;
padding: 0 0.5rem;
height: 100%;
}
.drive-context-menu li:not(:last-child) {
margin-bottom: 0.85rem;
}
.drive-context-menu a {
color: $foreground;
text-decoration: none;
font-size: inherit;
}

View file

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

View file

@ -0,0 +1,161 @@
@import "../global";
//Variables
$watcherIndicatorSize: 18px;
$watcherActionSize: 14px;
$uploadBoxesSize: 50px;
$actionButtonSize: 25px;
/*File Upload Dialog Styling*/
.fud {
position: fixed;
bottom: 0;
left: 0;
background-color: $sectionMenuOptions;
width: 100%;
height: 100%;
max-width: 540px;
max-height: 250px;
overflow: hidden;
-webkit-transform: translate3d(0, 0, 0);
z-index: 100;
display: block;
visibility: hidden;
}
.fud-fade {
visibility: visible;
animation: file-watcher-fade 2s forwards;
}
.fud-maximized {
visibility: visible;
animation: upload-dialog-expand 1.2s forwards;
animation-timing-function: ease-out;
}
.fud-minimized {
visibility: visible;
animation: upload-dialog-minimize 0.4s forwards;
animation-timing-function: ease-out;
}
#fud-header {
display: inline-flex;
height: $uploadBoxesSize;
font-size: $watcherIndicatorSize;
background-color: darken($sectionMenuOptions, 10%);
width: 100%;
}
.fud-header-title-wrapper {
margin: auto;
padding: 10px;
width: 100%;
overflow: hidden;
text-align: center;
padding-left: 10px;
}
#fud-header-title {
margin-left: 40px;
}
#fud-header-title:hover {
opacity: 0.8;
cursor: pointer;
}
#fud-minimize {
padding: 10px;
}
#header-actions {
margin-right: 5px;
}
.fud-actions {
//width: calc(#{$actionButtonSize} * 3);
display: inline-flex;
margin: auto;
align-items: center;
text-align: center;
height: $uploadBoxesSize;
}
.fud-action {
min-width: $actionButtonSize;
padding: 0 5px;
font-size: $watcherActionSize;
}
.fud-action svg,
i {
margin: auto;
}
.fud-actions span:hover {
opacity: 0.8;
cursor: pointer;
}
#fud-header-status {
font-size: calc(#{$watcherIndicatorSize - 2px});
min-width: $uploadBoxesSize;
min-height: $uploadBoxesSize;
max-width: $uploadBoxesSize;
max-height: $uploadBoxesSize;
display: flex;
}
/*Error and status icon Start with errors displaying as none*/
#fud-status-icon {
margin: auto;
font-size: 18px;
}
.fud-error-wrapper {
display: none;
position: absolute;
top: 9px;
left: 32px;
font-size: 18px;
}
.fud-error-wrapper {
color: $errorColor;
max-width: 18px;
max-height: 18px;
}
#fud-error-count {
font-size: 14px;
color: $foreground;
position: absolute;
top: 0;
width: 100%;
}
.fud-status-error {
margin-top: 10px;
}
#fud-queued-files {
max-height: 200px;
height: 100%;
height: 100%;
width: 100%;
overflow-y: scroll;
}
#fud-queued-files::-webkit-scrollbar {
width: 5px;
}
#fud-queued-files::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
border-radius: $defaultBorderRadius;
}
#fud-queued-files::-webkit-scrollbar-thumb {
border-radius: $defaultBorderRadius;
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.5);
background-color: lighten($sectionMenuOptions, 40%);
}
#fud-queued-files::-webkit-scrollbar-thumb:hover {
background-color: lighten($sectionMenuOptions, 50%);
}

View file

@ -0,0 +1,79 @@
@import "./StashUploadDialog.scss";
/*File Watcher Styling*/
.file-watcher {
height: 100%;
width: 100%;
display: inline-flex;
max-height: $uploadBoxesSize;
overflow: hidden;
word-wrap: break-word;
padding: none;
}
.file-watcher-progressbar {
min-width: $uploadBoxesSize;
min-height: $uploadBoxesSize;
max-width: $uploadBoxesSize;
max-height: $uploadBoxesSize;
display: flex;
max-height: $uploadBoxesSize;
background-color: lighten($sectionMenuOptions, 20%);
}
.file-watcher.active {
background-color: lighten($sectionMenuOptions, 10%);
}
.file-watcher.success {
animation: file-watcher-fade 2s forwards;
}
.file-watcher-progressbar-fill {
height: 100%;
width: 0%;
background: $successColor;
display: flex;
font-weight: bold;
//align-items: center;
transition: width 0.22s;
}
.file-watcher-progressbar-fill.error {
background: $errorColor;
}
.file-watcher-progressbar-indicator {
min-width: $uploadBoxesSize;
min-height: $uploadBoxesSize;
max-width: $uploadBoxesSize;
max-height: $uploadBoxesSize;
color: $foreground;
display: flex;
font-size: $watcherIndicatorSize;
}
.file-watcher-progressbar-indicator svg,
span {
margin: auto;
}
.file-watcher-name {
margin: auto;
overflow: hidden;
word-wrap: break-word;
padding-left: 10px;
align-self: center;
width: 100%;
}
.file-watcher-action {
width: calc(#{$uploadBoxesSize} / 2);
height: $uploadBoxesSize;
display: flex;
font-weight: bold;
align-items: center;
padding: 0px 5px;
float: right;
}
.file-watcher-action svg {
margin: auto;
font-size: $watcherActionSize;
}
.file-watcher-action:hover {
opacity: 0.8;
cursor: pointer;
}

View file

@ -0,0 +1,34 @@
@import "../global";
.stashbar {
width: 100%;
z-index: 200;
}
.stashbar-menu {
width: 100%;
height: $stashbarHeight;
display: inline-flex;
background-color: rgba($sectionMenu, 1);
}
.stashbar-action {
font-size: 1.25rem;
width: 100%;
}
.stashbar-action svg {
height: 100%;
width: 100%;
}
.stashbar-action label {
font-size: inherit;
}
.stashbar-action label:hover {
cursor: pointer;
}
.stashbar-action:hover {
opacity: 0.85;
cursor: pointer;
}

View file

@ -0,0 +1,33 @@
import React from "react";
import {
faBars,
faCloudUploadAlt,
faSearch,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import "../scss/stash/Stashbar.scss";
class StashbarMenu extends React.Component {
render() {
return (
<div className="stashbar-menu">
<span className="stashbar-action">
<FontAwesomeIcon icon={faBars} />
</span>
<span className="stashbar-action">
<label htmlFor="file-dropzone">
<FontAwesomeIcon icon={faCloudUploadAlt} />
</label>
</span>
<span
className="stashbar-action"
onClick={() => this.props.setSearchMode(true)}
>
<FontAwesomeIcon icon={faSearch} />
</span>
</div>
);
}
}
export default StashbarMenu;

View file

@ -0,0 +1,174 @@
import React from "react";
import {
faTimes,
faArrowLeft,
faHashtag,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
//Local imports
import StashbarSearchTagDisplay from "./StashbarSearchTagDisplay";
import "../scss/stash/Searchbar.scss";
//Constants
const hashtagChar = "#";
const searchFilters = ["Selected", "Public"];
class StashbarSearch extends React.Component {
constructor(props) {
super(props);
this.state = {
tags: [],
query: "",
};
}
//Filtering Functions
updateQuery(query) {
this.setState({ query }, this.updateFiltered);
}
updateFiltered() {
var fileBoxes = this.markAllFiltered();
const activeTags = this.state.tags;
const query = this.state.query.toLowerCase();
for (var f in fileBoxes) {
//If file isn't selected
if (activeTags.includes(searchFilters[0]) && !fileBoxes[f].selected)
fileBoxes[f].filtered = false;
//If file isn't public
if (activeTags.includes(searchFilters[1]) && !fileBoxes[f].file.public) {
fileBoxes[f].filtered = false;
}
if (!fileBoxes[f].file.name.toLowerCase().includes(query))
fileBoxes[f].filtered = false;
}
this.setState({ fileBoxes });
}
markAllFiltered() {
var fileBoxes = this.props.fileBoxes;
for (var f in fileBoxes) {
fileBoxes[f].filtered = true;
}
this.props.fileBoxesChanged(fileBoxes);
return fileBoxes;
}
//Searchbar Functions
searchChanged(e) {
this.updateQuery(e.target.value);
}
searchbarClose() {
this.markAllFiltered();
this.props.setSearchMode(false);
}
//Tag Functions
hashtagClick() {
this.updateQuery(hashtagChar);
}
queryTag(e) {
var query = this.state.query;
if (e.key !== "Enter" || query[0] !== hashtagChar) return;
const emptySpace = "";
const space = " ";
query = query.substring(1).toLowerCase();
const filters = this.getAvailableTags();
var filter = null;
for (filter of filters) {
if (filter.toLowerCase().includes(query)) break;
}
if (filter !== null) this.addTag(filter);
var firstSpace = query.indexOf(space);
if (firstSpace === -1) query = emptySpace;
else query = query.substring(query.indexOf(space));
this.updateQuery(query);
}
removeTag(tag) {
var tags = this.state.tags;
tags.splice(tags.indexOf(tag), 1);
this.setState({ tags }, this.updateFiltered);
}
addTag(tag) {
var tags = this.state.tags;
tags.push(tag);
this.setState({ tags }, this.updateFiltered);
}
getAvailableTags() {
var availableFilters = [];
searchFilters.forEach((filter, i) => {
if (i === 0) return; //Ignore first filter 'Selected' triggered by selecting
if (this.state.tags.includes(filter)) return;
if (
!filter
.toLowerCase()
.includes(this.state.query.substring(1).toLowerCase())
)
return;
availableFilters.push(filter);
});
return availableFilters;
}
//Render
render() {
return (
<React.Fragment>
<div className="file-searchbar stashbar-menu">
<div className="file-searchbox">
<span
className="file-searchbox-action"
onClick={this.searchbarClose.bind(this)}
>
<FontAwesomeIcon icon={faArrowLeft} />
</span>
<input
type="text"
id="file-search"
name="file-search"
placeholder="Search"
value={this.state.query}
onChange={this.searchChanged.bind(this)}
autoComplete="off"
onKeyUp={this.queryTag.bind(this)}
></input>
<span
className="file-searchbox-action"
id="file-searchbox-hashtag"
onClick={this.hashtagClick.bind(this)}
>
<FontAwesomeIcon icon={faHashtag} />
</span>
</div>
</div>
<div className="file-searchbar-extensions">
{this.state.query.includes(hashtagChar) && (
<StashbarSearchTagDisplay
query={this.state.query}
getAvailableTags={this.getAvailableTags.bind(this)}
addTag={this.addTag.bind(this)}
updateQuery={this.updateQuery.bind(this)}
/>
)}
{this.state.tags.length > 0 && (
<div className="file-filters">
{this.state.tags.map((filter, index) => (
<span className="file-filter" key={index}>
<span> {filter}</span>{" "}
<FontAwesomeIcon
icon={faTimes}
onClick={() => this.removeTag(filter)}
/>
</span>
))}
</div>
)}
</div>
</React.Fragment>
);
}
}
export default StashbarSearch;

View file

@ -0,0 +1,38 @@
import React from "react";
class StashbarSearchTagDisplay extends React.Component {
filterClicked(tag) {
this.props.updateQuery("");
this.props.addTag(tag);
}
displayTags() {
const tags = this.props.getAvailableTags();
if (tags.length === 0)
return (
<span className="query-filter">No Filters Matched Your Search</span>
);
else
return (
<React.Fragment>
{tags.map((tag, index) => (
<span
className="query-filter"
key={index}
onClick={() => this.filterClicked.bind(this)(tag)}
>
{tag}
</span>
))}
</React.Fragment>
);
}
render() {
return (
<div className="query-filters">
<div className="query-filter-list">{this.displayTags()}</div>
</div>
);
}
}
export default StashbarSearchTagDisplay;

View file

@ -0,0 +1,108 @@
//Module Imports
import React from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faExclamationTriangle,
faCloudUploadAlt,
faRedoAlt,
faTimes,
faAngleUp,
faAngleDown,
} from "@fortawesome/free-solid-svg-icons";
//Local Imports
import StashUploadWatcher from "./StashUploadWatcher";
import "../scss/stash/StashUploadDialog.scss";
//Icons List
const successIcon = <FontAwesomeIcon icon={faCloudUploadAlt} />;
const errorIcon = <FontAwesomeIcon icon={faExclamationTriangle} />;
const retryIcon = <FontAwesomeIcon icon={faRedoAlt} />;
const cancelIcon = <FontAwesomeIcon icon={faTimes} />;
const upIcon = <FontAwesomeIcon icon={faAngleUp} />;
const downIcon = <FontAwesomeIcon icon={faAngleDown} />;
export default class StashUploadDialog extends React.Component {
constructor(props) {
super(props);
this.state = {
isMinimized: false,
};
}
fudDisplay() {
const uploadLength = Object.values(this.props.uploads).length;
var className = "fud";
if (this.props.fadeOnClear && uploadLength === 0) {
return (className += " fud-fade");
}
if (this.state.isMinimized) return (className += " fud-minimized");
if (uploadLength > 0) {
return (className += " fud-maximized");
}
return className;
}
render() {
return (
<div className={this.fudDisplay()}>
<div id="fud-header">
<div id="fud-header-status">
<span id="fud-status-icon">
{this.props.errorCount > 0 ? errorIcon : successIcon}
<span
className="fud-error-wrapper"
style={{ display: this.props.errorCount > 0 ? "flex" : "none" }}
>
<i className="fas fa-circle"></i>
<span id="fud-error-count">{this.props.errorCount}</span>
</span>
</span>
</div>
<div className="fud-header-title-wrapper">
<span
id="fud-header-title"
onClick={() =>
this.setState({ isMinimized: !this.state.isMinimized })
}
>
Uploads
<span id="fud-minimize">
{this.state.isMinimized && upIcon}
{!this.state.isMinimized && downIcon}
</span>
</span>
</div>
<div className="fud-actions" id="header-actions">
<span
id="fud-retry"
className="fud-action"
onClick={this.props.retryAll}
>
{retryIcon}
</span>
<span
id="fud-clear"
className="fud-action"
onClick={this.props.clearAll}
>
{cancelIcon}
</span>
</div>
</div>
<div id="fud-queued-files">
{Object.values(this.props.uploads).map(
(upload, index) =>
upload && (
<StashUploadWatcher
file={upload.file}
key={upload.uploadUuid}
retryUpload={() => this.props.retryUpload(upload.uploadUuid)}
clearUpload={() => this.props.clearUpload(upload.uploadUuid)}
uploadProgress={upload.progress}
uploadStatus={upload.status}
/>
)
)}
</div>
</div>
);
}
}

View file

@ -0,0 +1,84 @@
import React from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faExclamationTriangle,
faCheck,
faRedoAlt,
faTimes,
} from "@fortawesome/free-solid-svg-icons";
//Local Imports
import "../scss/stash/StashUploadWatcher.scss";
//Constants
const successIcon = <FontAwesomeIcon icon={faCheck} />;
const errorIcon = <FontAwesomeIcon icon={faExclamationTriangle} />;
const retryIcon = <FontAwesomeIcon icon={faRedoAlt} />;
const cancelIcon = <FontAwesomeIcon icon={faTimes} />;
export default class StashUploadWatcher extends React.Component {
constructor(props) {
super(props);
this.retryUpload = props.retryUpload;
this.clearUpload = props.clearUpload;
}
isUploading() {
return this.props.uploadProgress > 0 && this.props.uploadProgress !== 100;
}
progressIndicator() {
if (this.props.uploadStatus === "Success") return successIcon;
if (this.props.uploadStatus === "Error") return errorIcon;
if (this.isUploading())
return (
<span className="file-watcher-progressbar-text">
{this.props.uploadProgress}%
</span>
);
return <span />;
}
watcherStatus() {
var className = "file-watcher";
if (this.isUploading() && this.props.uploadStatus !== "Error")
className += " active";
if (this.props.uploadStatus === "Success") className += " success";
return className;
}
render() {
return (
<div className={this.watcherStatus()}>
<div className="file-watcher-progressbar">
<div
className={
this.props.uploadStatus === "Error"
? "file-watcher-progressbar-fill error"
: "file-watcher-progressbar-fill"
}
style={{
width:
this.props.uploadStatus === "Error"
? "100%"
: `${this.props.uploadProgress}%`,
}}
>
<span className="file-watcher-progressbar-indicator">
{this.progressIndicator()}
</span>
</div>
</div>
<span className="file-watcher-name">{this.props.file.name}</span>
<div className="fud-actions">
{this.props.uploadStatus === "Error" && (
<span className="file-watcher-action" onClick={this.retryUpload}>
{retryIcon}
</span>
)}
<span className="file-watcher-action" onClick={this.clearUpload}>
{cancelIcon}
</span>
</div>
</div>
);
}
}