Dunestash Public Frontend 0.0.1-a.1
This commit is contained in:
commit
fe36970476
67 changed files with 2179 additions and 0 deletions
56
src/stash/FileBox.jsx
Normal file
56
src/stash/FileBox.jsx
Normal file
|
@ -0,0 +1,56 @@
|
|||
import React from "react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
faUsers,
|
||||
faEye,
|
||||
faShareSquare,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import PropTypes from "prop-types";
|
||||
import "./scss/stash/FileBox.scss";
|
||||
class FileBox extends React.Component {
|
||||
readableDate() {
|
||||
let d = new Date(parseInt(this.props.file.date));
|
||||
if (isNaN(d.getMonth())) return "";
|
||||
return `${
|
||||
d.getMonth() + 1
|
||||
}/${d.getDate()}/${d.getFullYear()} ${d.getHours()}:${d.getMinutes()}`;
|
||||
}
|
||||
selectBox(e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.props.onSelection(e, this.props.boxUuid);
|
||||
this.props.removeDriveContextMenu();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
tabIndex="0"
|
||||
className={"filebox" + (this.props.selected ? " selected" : "")}
|
||||
onClick={this.selectBox.bind(this)}
|
||||
onContextMenu={() => this.props.contextSelect(this.props.boxUuid)}
|
||||
onKeyDown={this.props.onBoxKeyPress}
|
||||
>
|
||||
<div className="file">
|
||||
<div className="file-details">
|
||||
<span className="file-name">{this.props.file.name}</span>
|
||||
</div>
|
||||
<div className="file-details">
|
||||
<span className="file-date">{this.readableDate()}</span>
|
||||
{this.props.file.public && (
|
||||
<span className="file-indicators file-subinfo-details">
|
||||
{this.props.file.public && <FontAwesomeIcon icon={faEye} />}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
FileBox.propTypes = {
|
||||
file: PropTypes.object,
|
||||
};
|
||||
|
||||
export default FileBox;
|
125
src/stash/FileDisplay.jsx
Normal file
125
src/stash/FileDisplay.jsx
Normal file
|
@ -0,0 +1,125 @@
|
|||
//Module Imports
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
//Local Imports
|
||||
import FileBox from "./FileBox";
|
||||
import "./scss/stash/FileDisplay.scss";
|
||||
class FileDisplay extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
firstSelectionBoxUuid: null,
|
||||
};
|
||||
}
|
||||
|
||||
displayClick(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.deselectAll();
|
||||
this.props.removeDriveContextMenu();
|
||||
}
|
||||
|
||||
fileBoxKeysByPosition() {
|
||||
return Object.keys(this.props.fileBoxes).sort((a, b) => {
|
||||
return a.position - b.position;
|
||||
});
|
||||
}
|
||||
|
||||
onSelection(e, boxUuid) {
|
||||
var fileBoxes = this.props.fileBoxes;
|
||||
var newBoxes;
|
||||
const firstSelection = this.state.firstSelectionBoxUuid;
|
||||
if (e.ctrlKey && firstSelection !== null)
|
||||
newBoxes = this.segmentSelection(fileBoxes, boxUuid);
|
||||
else if (e.shiftKey && firstSelection !== null)
|
||||
newBoxes = this.multiSelection(fileBoxes, boxUuid);
|
||||
else newBoxes = this.singleSelection(fileBoxes, boxUuid);
|
||||
this.props.fileBoxesChanged(newBoxes);
|
||||
}
|
||||
|
||||
singleSelection(fileBoxes, boxUuid) {
|
||||
this.deselectAll();
|
||||
this.setState({ firstSelectionBoxUuid: boxUuid });
|
||||
fileBoxes[boxUuid].selected = true;
|
||||
return fileBoxes;
|
||||
}
|
||||
|
||||
segmentSelection(fileBoxes, boxUuid) {
|
||||
fileBoxes[boxUuid].selected = !fileBoxes[boxUuid].selected;
|
||||
return fileBoxes;
|
||||
}
|
||||
|
||||
multiSelection(fileBoxes, boxUuid) {
|
||||
this.deselectAll();
|
||||
var firstIndex = fileBoxes[this.state.firstSelectionBoxUuid].position;
|
||||
var endIndex = fileBoxes[boxUuid].position;
|
||||
var boxKeys = this.fileBoxKeysByPosition();
|
||||
if (endIndex < firstIndex) {
|
||||
let tmp = endIndex;
|
||||
endIndex = firstIndex;
|
||||
firstIndex = tmp;
|
||||
}
|
||||
//Send selection 1 more for the slice
|
||||
boxKeys.slice(firstIndex, endIndex + 1).forEach((boxId, i) => {
|
||||
if (!fileBoxes[boxId].filtered) return;
|
||||
fileBoxes[boxId].selected = true;
|
||||
});
|
||||
return fileBoxes;
|
||||
}
|
||||
|
||||
contextSelect(boxUuid) {
|
||||
if (this.props.getSelectedBoxes().length > 1) return;
|
||||
this.onSelection({}, boxUuid);
|
||||
}
|
||||
deselectAll() {
|
||||
var fileBoxes = this.props.fileBoxes;
|
||||
for (var f in fileBoxes) fileBoxes[f].selected = false;
|
||||
this.props.fileBoxesChanged(fileBoxes);
|
||||
}
|
||||
selectAll() {
|
||||
var fileBoxes = this.props.fileBoxes;
|
||||
for (var f in fileBoxes)
|
||||
if (fileBoxes[f].filtered) fileBoxes[f].selected = true;
|
||||
this.props.fileBoxesChanged(fileBoxes);
|
||||
}
|
||||
onBoxKeyPress(e) {
|
||||
if (e.keyCode !== 65 || !e.ctrlKey) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.selectAll();
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
className="file-display"
|
||||
onClick={this.displayClick.bind(this)}
|
||||
onContextMenu={this.props.contextMenu}
|
||||
>
|
||||
<div className="box-display">
|
||||
{this.fileBoxKeysByPosition().map((boxUuid, index) => (
|
||||
<React.Fragment key={boxUuid}>
|
||||
{this.props.fileBoxes[boxUuid].filtered && (
|
||||
<FileBox
|
||||
file={this.props.fileBoxes[boxUuid].file}
|
||||
boxUuid={boxUuid}
|
||||
selected={this.props.fileBoxes[boxUuid].selected}
|
||||
contextMenu={this.props.contextMenu}
|
||||
contextSelect={this.contextSelect.bind(this)}
|
||||
removeDriveContextMenu={this.props.removeDriveContextMenu}
|
||||
onSelection={this.onSelection.bind(this)}
|
||||
onBoxKeyPress={this.onBoxKeyPress.bind(this)}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
<div className="file-display-spacer" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FileDisplay.propTypes = {
|
||||
files: PropTypes.object,
|
||||
};
|
||||
export default FileDisplay;
|
150
src/stash/StashContextMenu.jsx
Normal file
150
src/stash/StashContextMenu.jsx
Normal file
|
@ -0,0 +1,150 @@
|
|||
import React from "react";
|
||||
import axios from "axios";
|
||||
import { toast } from "react-toastify";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
faInfoCircle,
|
||||
faFileDownload,
|
||||
faTrash,
|
||||
faEye,
|
||||
faShareSquare,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
//Local Imports
|
||||
import "./scss/stash/StashContextMenu.scss";
|
||||
import { serverUrls, serverAddress } from "./api.json";
|
||||
//Constants
|
||||
const downloadUrl = `${serverAddress}/${serverUrls.POST.downloadUrl}`;
|
||||
const deleteUrl = `${serverAddress}/${serverUrls.POST.deleteUrl}`;
|
||||
const publicUrl = `${serverAddress}/${serverUrls.POST.publicUrl}`;
|
||||
const rawUrl = `${serverAddress}/${serverUrls.GET.rawUrl}`;
|
||||
|
||||
function getConfig() {
|
||||
var authToken = localStorage.getItem("authToken");
|
||||
return { headers: { authorization: `Bearer ${authToken}` } };
|
||||
}
|
||||
|
||||
export default class StashContextMenu extends React.Component {
|
||||
infoView() {
|
||||
var selectedCount = this.props.getSelectedBoxes().length;
|
||||
if (selectedCount === 1) return "View";
|
||||
if (selectedCount > 1) return `${selectedCount} files selected`;
|
||||
return "No Files Selected";
|
||||
}
|
||||
|
||||
infoClick(e) {
|
||||
const selectedBoxes = this.props.getSelectedBoxes();
|
||||
if (selectedBoxes.length !== 1) return;
|
||||
const file = selectedBoxes[0];
|
||||
let win = window.open(`${rawUrl}?target=${file}`);
|
||||
if (!win || win.closed || typeof win.closed == "undefined") {
|
||||
window.location = `${rawUrl}?target=${file}`;
|
||||
}
|
||||
}
|
||||
downloadClick() {
|
||||
const selectedBoxes = this.props.getSelectedBoxes();
|
||||
//ZIPS ARE NOT SUPPORTED YET
|
||||
if (selectedBoxes.length > 1)
|
||||
return toast.error("Downloading multiple files is not yet supported!");
|
||||
else
|
||||
return this.handleDownload(`${downloadUrl}?target=${selectedBoxes[0]}`);
|
||||
}
|
||||
deleteClick() {
|
||||
const selectedBoxes = this.props.getSelectedBoxes();
|
||||
axios
|
||||
.post(deleteUrl, selectedBoxes, getConfig())
|
||||
.then((res) => this.handleDelete(res, selectedBoxes))
|
||||
.catch((e) => this.handleDelete(e.response, selectedBoxes));
|
||||
}
|
||||
publicClick() {
|
||||
const selectedBoxes = this.props.getSelectedBoxes();
|
||||
axios
|
||||
.post(publicUrl, selectedBoxes, getConfig())
|
||||
.then((res) => this.handlePublic(res, selectedBoxes))
|
||||
.catch((e) => this.handlePublic(e.response, selectedBoxes));
|
||||
}
|
||||
|
||||
handlePublic(res, selectedBoxes) {
|
||||
const failedFiles = res.data || [];
|
||||
if (res.status !== 200)
|
||||
toast.error("There was an issue making some files public!");
|
||||
let fileBoxes = this.props.fileBoxes;
|
||||
selectedBoxes.forEach((selectedBoxId) => {
|
||||
if (!failedFiles.includes(selectedBoxId)) {
|
||||
fileBoxes[selectedBoxId].file.public = !fileBoxes[selectedBoxId].file
|
||||
.public;
|
||||
} else {
|
||||
fileBoxes[selectedBoxId].selected = true;
|
||||
}
|
||||
});
|
||||
this.props.fileBoxesChanged(fileBoxes);
|
||||
}
|
||||
handleDownload(url) {
|
||||
let win = window.open(url);
|
||||
if (!win || win.closed || typeof win.closed == "undefined")
|
||||
window.location = url;
|
||||
}
|
||||
/**
|
||||
* Handles the response from the deleteClick()
|
||||
* @param {String} response server response
|
||||
* @param {Array} selectedBoxes Selected Boxes object list
|
||||
*
|
||||
*/
|
||||
handleDelete(res, selectedBoxes) {
|
||||
const failedFiles = res.data || [];
|
||||
console.log(res);
|
||||
if (res.status !== 200) toast.error("Error Deleting Some Files");
|
||||
let fileBoxes = this.props.fileBoxes;
|
||||
selectedBoxes.forEach((selectedBoxId) => {
|
||||
if (!failedFiles.includes(selectedBoxId)) {
|
||||
delete fileBoxes[selectedBoxId];
|
||||
} else {
|
||||
fileBoxes[selectedBoxId].selected = true;
|
||||
}
|
||||
});
|
||||
this.props.fileBoxesChanged(fileBoxes);
|
||||
}
|
||||
shareClick() {}
|
||||
|
||||
styleCalc() {
|
||||
const estimatedHeight = 180; //px
|
||||
const esetimatedWidth = 290; //px
|
||||
const bodyWidth = document.body.offsetWidth;
|
||||
const bodyHeight = document.documentElement.offsetHeight;
|
||||
let top = this.props.y;
|
||||
let left = this.props.x;
|
||||
const overFlowX = left + esetimatedWidth > bodyWidth;
|
||||
const overFlowY = top + estimatedHeight > bodyHeight;
|
||||
if (overFlowX) left = left - esetimatedWidth;
|
||||
if (overFlowY) top = top - estimatedHeight;
|
||||
return { top: `${top}px`, left: `${left}px` };
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="drive-context-menu" style={this.styleCalc()}>
|
||||
<ul>
|
||||
<li onClick={this.infoClick.bind(this)}>
|
||||
<FontAwesomeIcon icon={faInfoCircle} />
|
||||
{this.infoView()}
|
||||
</li>
|
||||
<li onClick={this.downloadClick.bind(this)}>
|
||||
<FontAwesomeIcon icon={faFileDownload} />
|
||||
Download
|
||||
</li>
|
||||
<li onClick={this.deleteClick.bind(this)}>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
Delete
|
||||
</li>
|
||||
<li onClick={this.publicClick.bind(this)}>
|
||||
<FontAwesomeIcon icon={faEye} />
|
||||
Toggle Public
|
||||
</li>
|
||||
<li onClick={this.shareClick.bind(this)}>
|
||||
<FontAwesomeIcon icon={faShareSquare} />
|
||||
Share
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
31
src/stash/StashDropzone.jsx
Normal file
31
src/stash/StashDropzone.jsx
Normal file
|
@ -0,0 +1,31 @@
|
|||
import React from "react";
|
||||
import Dropzone from "react-dropzone";
|
||||
import FileDisplay from "./FileDisplay";
|
||||
|
||||
import "./scss/stash/StashDropzone.scss";
|
||||
class StashDropzone extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<Dropzone onDrop={(acceptedFiles) => this.props.addUpload(acceptedFiles)}>
|
||||
{({ getRootProps, getInputProps }) => (
|
||||
<div className="stash-dropzone" {...getRootProps()}>
|
||||
<input
|
||||
id="file-dropzone"
|
||||
{...getInputProps()}
|
||||
style={{ display: "none", visibility: "hidden" }}
|
||||
/>
|
||||
<FileDisplay
|
||||
fileBoxes={this.props.fileBoxes}
|
||||
fileBoxesChanged={this.props.fileBoxesChanged}
|
||||
contextMenu={this.props.contextMenu}
|
||||
removeDriveContextMenu={this.props.removeDriveContextMenu}
|
||||
getSelectedBoxes={this.props.getSelectedBoxes}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Dropzone>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default StashDropzone;
|
198
src/stash/StashUpload.jsx
Normal file
198
src/stash/StashUpload.jsx
Normal file
|
@ -0,0 +1,198 @@
|
|||
//Module Imports
|
||||
import React from "react";
|
||||
import axios, { CancelToken } from "axios";
|
||||
import { toast } from "react-toastify";
|
||||
//Local Imports
|
||||
import StashDropzone from "./StashDropzone";
|
||||
import StashUploadDialog from "./uploader/StashUploadDialog";
|
||||
//Constants
|
||||
import { serverUrls, serverFields, serverAddress } from "./api.json";
|
||||
const uploadUrl = `${serverAddress}/${serverUrls.POST.uploadUrl}`;
|
||||
const uploadField = serverFields.uploadField;
|
||||
const cancelMessage = "User Canceled";
|
||||
const successClearTime = 200;
|
||||
|
||||
function buildUpload(file, uploadUuid, onProgress) {
|
||||
var authToken = localStorage.getItem("authToken");
|
||||
var upload = {
|
||||
file,
|
||||
uploadUuid,
|
||||
progress: 0,
|
||||
status: null,
|
||||
started: false,
|
||||
};
|
||||
const cancelToken = new CancelToken((cancel) => {
|
||||
upload.cancelUpload = () => cancel(cancelMessage);
|
||||
});
|
||||
upload.config = {
|
||||
headers: { authorization: `Bearer ${authToken}`, filesize: file.size },
|
||||
onUploadProgress: (e) => onProgress(e, uploadUuid),
|
||||
cancelToken,
|
||||
};
|
||||
return upload;
|
||||
}
|
||||
|
||||
export default class StashUpload extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
uploads: {},
|
||||
errorCount: 0,
|
||||
fadeOnClear: false,
|
||||
};
|
||||
}
|
||||
|
||||
uploadProgress(e, uploadUuid) {
|
||||
const totalLength = e.lengthComputable
|
||||
? e.total
|
||||
: e.target.getResponseHeader("content-length") ||
|
||||
e.target.getResponseHeader("x-decompressed-content-length");
|
||||
if (totalLength !== null) {
|
||||
const loaded = Math.round((e.loaded * 100) / totalLength);
|
||||
var uploads = this.state.uploads;
|
||||
if (loaded === 100 || uploads[uploadUuid] == null) return;
|
||||
uploads[uploadUuid].progress = loaded;
|
||||
this.setState({ uploads });
|
||||
}
|
||||
}
|
||||
|
||||
startAllUploads() {
|
||||
var uploads = this.state.uploads;
|
||||
for (var u in uploads) {
|
||||
if (uploads[u].started) continue;
|
||||
uploads[u].started = true;
|
||||
this.startUpload(uploads[u]);
|
||||
}
|
||||
this.setState({ uploads });
|
||||
}
|
||||
|
||||
startUpload(upload) {
|
||||
const data = new FormData();
|
||||
data.append(uploadField, upload.file);
|
||||
axios
|
||||
.post(uploadUrl, data, upload.config)
|
||||
.then((res) => this.uploadDone(res, upload))
|
||||
.catch((e) => this.uploadError(e, upload));
|
||||
}
|
||||
|
||||
uploadDone(res, upload) {
|
||||
if (res.status !== 200) return this.showError(upload.uploadUuid);
|
||||
var uploads = this.state.uploads;
|
||||
delete uploads[upload.uploadUuid].cancelToken;
|
||||
this.setState({ uploads });
|
||||
this.showSuccess(upload.uploadUuid);
|
||||
this.props.addFilebox(res.data);
|
||||
}
|
||||
|
||||
uploadError(e, upload) {
|
||||
if (e.message === cancelMessage) console.log("Upload Canceled");
|
||||
else if (e.response == null) toast.error("Unknown Error Occured!");
|
||||
else if (e.response.status === 401) toast.error("Not Logged In!");
|
||||
else if (e.response.status === 500) toast.error("Drive Full!");
|
||||
this.showError(upload.uploadUuid);
|
||||
}
|
||||
|
||||
showError(uploadUuid) {
|
||||
var uploads = this.state.uploads;
|
||||
uploads[uploadUuid].status = "Error";
|
||||
this.setState({ uploads, errorCount: this.state.errorCount + 1 });
|
||||
}
|
||||
|
||||
showSuccess(uploadUuid) {
|
||||
var uploads = this.state.uploads;
|
||||
uploads[uploadUuid].status = "Success";
|
||||
this.setState({ uploads });
|
||||
setTimeout(() => this.removeUpload(uploadUuid), successClearTime);
|
||||
}
|
||||
|
||||
addUpload(files) {
|
||||
var uploads = this.state.uploads;
|
||||
const uploadTime = Date.now();
|
||||
var uploadUuid;
|
||||
files.forEach((file, i) => {
|
||||
uploadUuid = `${uploadTime}-${i}`;
|
||||
uploads[uploadUuid] = buildUpload(
|
||||
file,
|
||||
uploadUuid,
|
||||
this.uploadProgress.bind(this)
|
||||
);
|
||||
});
|
||||
this.setState({ uploads }, this.startAllUploads);
|
||||
}
|
||||
|
||||
retryUpload(uploadUuid) {
|
||||
if (!uploadUuid) return;
|
||||
var uploads = this.state.uploads;
|
||||
var errorCount = this.state.errorCount;
|
||||
const file = uploads[uploadUuid].file;
|
||||
//Remove error count if the upload errored because we're now removing it
|
||||
if (uploads[uploadUuid].status === "Error") errorCount--;
|
||||
//Update and remove the upload
|
||||
this.removeUpload(uploadUuid);
|
||||
this.addUpload([file]);
|
||||
this.setState({ errorCount });
|
||||
}
|
||||
clearUpload(uploadUuid) {
|
||||
var uploads = this.state.uploads;
|
||||
if (uploads[uploadUuid].status !== null) this.removeUpload(uploadUuid);
|
||||
else uploads[uploadUuid].cancelUpload();
|
||||
}
|
||||
clearAll() {
|
||||
var uploads = this.state.uploads;
|
||||
var onlyPending = true;
|
||||
var u;
|
||||
for (u in uploads) {
|
||||
if (uploads[u].status !== null) {
|
||||
delete uploads[u];
|
||||
onlyPending = false;
|
||||
}
|
||||
}
|
||||
//If onlypending cancel all uploads currently remaining
|
||||
if (onlyPending) for (u in uploads) uploads[u].cancelUpload();
|
||||
this.setState({ uploads, errorCount: 0 });
|
||||
}
|
||||
retryAll() {
|
||||
var uploads = this.state.uploads;
|
||||
//Splicing so itterate backwards
|
||||
//(retryUpload is what calls the splice via removeUpload)
|
||||
for (var u in uploads) {
|
||||
if (uploads[u].status === "Error") this.retryUpload(u);
|
||||
}
|
||||
}
|
||||
removeUpload(uploadUuid) {
|
||||
if (!uploadUuid) return;
|
||||
//Remove error count if the upload errored because we're now removing it
|
||||
var errorCount = this.state.errorCount;
|
||||
var fadeOnClear = this.state.fadeOnClear;
|
||||
var uploads = this.state.uploads;
|
||||
if (uploads[uploadUuid].status === "Error") errorCount--;
|
||||
//Update and remove the upload
|
||||
delete uploads[uploadUuid];
|
||||
if (Object.keys(uploads).length === 0) fadeOnClear = true;
|
||||
this.setState({ uploads, errorCount, fadeOnClear });
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<StashDropzone
|
||||
fileBoxes={this.props.fileBoxes}
|
||||
fileBoxesChanged={this.props.fileBoxesChanged}
|
||||
addUpload={this.addUpload.bind(this)}
|
||||
contextMenu={this.props.contextMenu}
|
||||
removeDriveContextMenu={this.props.removeDriveContextMenu}
|
||||
getSelectedBoxes={this.props.getSelectedBoxes}
|
||||
/>
|
||||
<StashUploadDialog
|
||||
fadeOnClear={this.state.fadeOnClear}
|
||||
uploads={this.state.uploads}
|
||||
errorCount={this.state.errorCount}
|
||||
clearAll={this.clearAll.bind(this)}
|
||||
retryAll={this.retryAll.bind(this)}
|
||||
clearUpload={this.clearUpload.bind(this)}
|
||||
retryUpload={this.retryUpload.bind(this)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
39
src/stash/Stashbar.jsx
Normal file
39
src/stash/Stashbar.jsx
Normal file
|
@ -0,0 +1,39 @@
|
|||
//Module Imports
|
||||
import React from "react";
|
||||
//Local Imports
|
||||
import StashbarMenu from "./stashbar/StashbarMenu.jsx";
|
||||
import StashbarSearch from "./stashbar/StashbarSearch.jsx";
|
||||
class Stashbar extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
searchMode: false,
|
||||
};
|
||||
}
|
||||
|
||||
setSearchMode(value) {
|
||||
this.setState({ searchMode: value });
|
||||
}
|
||||
|
||||
whichBar() {
|
||||
if (this.state.searchMode)
|
||||
return (
|
||||
<StashbarSearch
|
||||
fileBoxes={this.props.fileBoxes}
|
||||
fileBoxesChanged={this.props.fileBoxesChanged}
|
||||
setSearchMode={this.setSearchMode.bind(this)}
|
||||
/>
|
||||
);
|
||||
else return <StashbarMenu setSearchMode={this.setSearchMode.bind(this)} />;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="stashbar">
|
||||
<div className="stashbar-content">{this.whichBar()}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Stashbar;
|
20
src/stash/api.json
Normal file
20
src/stash/api.json
Normal file
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"serverAddress": "http://nubian.dunestorm.net:52001",
|
||||
"serverUrls": {
|
||||
"POST": {
|
||||
"uploadUrl": "api/stash/upload",
|
||||
"downloadUrl": "api/stash/download",
|
||||
"deleteUrl": "api/stash/delete",
|
||||
"publicUrl": "api/stash/public"
|
||||
},
|
||||
"GET": {
|
||||
"filesUrl": "api/stash/files",
|
||||
"rawUrl": "api/stash/raw",
|
||||
"avatar": "api/user/avatar"
|
||||
}
|
||||
},
|
||||
"serverFields": {
|
||||
"uploadField": "user-selected-file"
|
||||
},
|
||||
"constants": { "jwtHeader": "authorization" }
|
||||
}
|
17
src/stash/scss/Stash.scss
Normal file
17
src/stash/scss/Stash.scss
Normal file
|
@ -0,0 +1,17 @@
|
|||
@import "./global";
|
||||
|
||||
.dunestash {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.stash {
|
||||
width: 100%;
|
||||
height: calc(100% - #{$stashbarHeight});
|
||||
position: relative;
|
||||
margin: auto;
|
||||
display: flex;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
}
|
35
src/stash/scss/_global.scss
Normal file
35
src/stash/scss/_global.scss
Normal file
|
@ -0,0 +1,35 @@
|
|||
@import "global/colors", "global/fonts", "global/measurements",
|
||||
"global/animations";
|
||||
html,
|
||||
body {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
text-align: center;
|
||||
margin-top: 0;
|
||||
color: $foreground;
|
||||
background: #eee;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
*::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
}
|
||||
*::-webkit-scrollbar-track {
|
||||
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
||||
border-radius: $defaultBorderRadius;
|
||||
}
|
||||
*::-webkit-scrollbar-thumb {
|
||||
border-radius: $defaultBorderRadius;
|
||||
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.5);
|
||||
background-color: lighten($sectionMenuOptions, 40%);
|
||||
}
|
||||
*::-webkit-scrollbar-thumb:hover {
|
||||
background-color: lighten($sectionMenuOptions, 50%);
|
||||
}
|
||||
input:focus,
|
||||
select:focus,
|
||||
textarea:focus,
|
||||
button:focus {
|
||||
outline: none;
|
||||
}
|
121
src/stash/scss/global/_animations.scss
Normal file
121
src/stash/scss/global/_animations.scss
Normal file
|
@ -0,0 +1,121 @@
|
|||
/*Shimmer for links*/
|
||||
@-webkit-keyframes glowingShimmer {
|
||||
0% {
|
||||
background-position: -4rem top;
|
||||
/*50px*/
|
||||
}
|
||||
70% {
|
||||
background-position: 12.5rem top;
|
||||
/*200px*/
|
||||
}
|
||||
100% {
|
||||
background-position: 12.5rem top;
|
||||
/*200px*/
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes toastify-notification-fade-out {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes toast-notification-fade-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 0.9;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin animation-breathe($b1, $b2, $f1, $f2) {
|
||||
@keyframes breathe {
|
||||
0% {
|
||||
background-color: $b1;
|
||||
color: $f1;
|
||||
}
|
||||
50% {
|
||||
background-color: $b2;
|
||||
color: $f2;
|
||||
}
|
||||
100% {
|
||||
background-color: $b1;
|
||||
color: $f1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@mixin animation-bounce($f1, $f2) {
|
||||
@keyframes bounce {
|
||||
0% {
|
||||
color: $f1;
|
||||
}
|
||||
|
||||
50% {
|
||||
color: $f2;
|
||||
}
|
||||
100% {
|
||||
color: $f1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes file-watcher-fade {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
40% {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
80% {
|
||||
background: $successColor;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes upload-dialog-expand {
|
||||
0% {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
100% {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes upload-dialog-minimize {
|
||||
0% {
|
||||
height: 50%;
|
||||
}
|
||||
|
||||
100% {
|
||||
height: 50px;
|
||||
}
|
||||
}
|
16
src/stash/scss/global/_colors.scss
Normal file
16
src/stash/scss/global/_colors.scss
Normal file
|
@ -0,0 +1,16 @@
|
|||
/*Status Colors*/
|
||||
$successColor: limegreen;
|
||||
$errorColor: #e20006;
|
||||
$informationColor: #0095ff;
|
||||
/*Background & Foreground colors*/
|
||||
$background: rgba(20, 20, 20, 1);
|
||||
$backgroundGradientStart: $background;
|
||||
$backgroundGradientEnd: #848486;
|
||||
$foreground: white;
|
||||
$sectionForeground: $foreground;
|
||||
/*Visual "components" (divs, etc)*/
|
||||
$sectionBackground: rgba($background, 0.95);
|
||||
$sectionMenu: darken(rgba($background, 0.8), 10%);
|
||||
$sectionMenuOptions: rgba(lighten($sectionMenu, 16%), 98%);
|
||||
$borderColor: #13120e;
|
||||
/*Unique Components*/
|
48
src/stash/scss/global/_fonts.scss
Normal file
48
src/stash/scss/global/_fonts.scss
Normal file
|
@ -0,0 +1,48 @@
|
|||
|
||||
h1 {
|
||||
font-family: "Dejavu Sans";
|
||||
font-size: 33px;
|
||||
font-style: normal;
|
||||
font-variant: normal;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
h2, label {
|
||||
font-family: "Dejavu Sans";
|
||||
font-size: 28px;
|
||||
font-style: normal;
|
||||
font-variant: normal;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-family: "Dejavu Sans";
|
||||
font-size: 24px;
|
||||
font-style: normal;
|
||||
font-variant: normal;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
p, a {
|
||||
font-family: "Dejavu Sans";
|
||||
font-size: 18px;
|
||||
font-style: normal;
|
||||
font-variant: normal;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
font-family: "Dejavu Sans";
|
||||
font-size: 21px;
|
||||
font-style: normal;
|
||||
font-variant: normal;
|
||||
font-weight: 400;
|
||||
line-height: 30px; }
|
||||
|
||||
pre {
|
||||
font-family: "Dejavu Sans";
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-variant: normal;
|
||||
font-weight: 400;
|
||||
}
|
13
src/stash/scss/global/_measurements.scss
Normal file
13
src/stash/scss/global/_measurements.scss
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*Spacing*/
|
||||
$contentTopGap: 0px;
|
||||
/*Size*/
|
||||
$pageMinWidth: 300px;
|
||||
$pageMaxWidth: 780px;
|
||||
$defaultBoxPadding: 15px;
|
||||
/*Components*/
|
||||
$stashbarHeight: 4rem;
|
||||
$fileFiltersHeight: 1.5rem;
|
||||
$toastMinWidth: calc(#{$pageMinWidth} - 100px);
|
||||
$toastMaxWidth: calc(50% - 20px);
|
||||
/*Misc*/
|
||||
$defaultBorderRadius: 2px;
|
61
src/stash/scss/stash/FileBox.scss
Normal file
61
src/stash/scss/stash/FileBox.scss
Normal file
|
@ -0,0 +1,61 @@
|
|||
@import "../global";
|
||||
.filebox {
|
||||
margin: 0.4rem 0.4rem;
|
||||
display: flex;
|
||||
max-height: 4rem;
|
||||
height: 100%;
|
||||
max-width: 25em;
|
||||
padding: 0.4rem 0;
|
||||
width: 100%;
|
||||
align-self: flex-start;
|
||||
position: relative;
|
||||
background: $sectionMenuOptions;
|
||||
box-shadow: 1px 3px 2px $sectionMenuOptions;
|
||||
}
|
||||
|
||||
.filebox:hover {
|
||||
cursor: pointer;
|
||||
background: lighten($sectionMenuOptions, 3%);
|
||||
}
|
||||
|
||||
.filebox.selected {
|
||||
background: $informationColor;
|
||||
box-shadow: 1px 3px 2px darken($informationColor, 20%);
|
||||
}
|
||||
|
||||
.filebox.selected:hover {
|
||||
background: lighten($informationColor, 3%);
|
||||
}
|
||||
|
||||
.file {
|
||||
color: white;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
max-height: 4rem;
|
||||
height: 100%;
|
||||
max-width: 25em;
|
||||
padding: 0.4rem 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.file-details {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
height: 50%;
|
||||
margin: auto;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.file-info-details {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
padding: 0.125rem 0;
|
||||
max-width: 39ch;
|
||||
}
|
||||
|
||||
.file-subinfo-details {
|
||||
padding: 0 5px;
|
||||
}
|
23
src/stash/scss/stash/FileDisplay.scss
Normal file
23
src/stash/scss/stash/FileDisplay.scss
Normal file
|
@ -0,0 +1,23 @@
|
|||
@import "../global";
|
||||
.file-display {
|
||||
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.box-display{
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
flex-wrap: wrap;
|
||||
flex-grow: 1;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-o-user-select: none;
|
||||
}
|
||||
|
||||
.file-display-spacer {
|
||||
width: 100%;
|
||||
height: 2rem;
|
||||
}
|
120
src/stash/scss/stash/Searchbar.scss
Normal file
120
src/stash/scss/stash/Searchbar.scss
Normal file
|
@ -0,0 +1,120 @@
|
|||
@import "../global";
|
||||
$searchIndent: 0.125rem;
|
||||
|
||||
.file-searchbar {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
background: darken(#eee, 5%);
|
||||
box-shadow: 0.25px 1px 0.5px darken(#eee, 8%);
|
||||
}
|
||||
|
||||
.file-searchbox {
|
||||
margin: 0 auto;
|
||||
display: inline-flex;
|
||||
background: inherit;
|
||||
color: $sectionMenuOptions;
|
||||
width: 100%;
|
||||
border-bottom: 1px darken(#eee, 22%) solid;
|
||||
}
|
||||
.file-searchbar svg {
|
||||
margin: auto 0;
|
||||
height: 100%;
|
||||
padding: 0 0.15rem 0 0.5rem;
|
||||
}
|
||||
|
||||
#file-search {
|
||||
width: 100%;
|
||||
font-size: 1.125rem;
|
||||
background: inherit;
|
||||
text-indent: $searchIndent;
|
||||
border: none;
|
||||
}
|
||||
#file-search::placeholder {
|
||||
color: $sectionMenuOptions;
|
||||
}
|
||||
.file-searchbox-action {
|
||||
padding: 0 0.325rem;
|
||||
height: inherit;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.file-searchbox-action svg {
|
||||
margin: auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#file-searchbox-hashtag {
|
||||
padding-right: calc(0.325rem + #{$searchIndent} * 4);
|
||||
}
|
||||
|
||||
.file-searchbox-action:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.file-searchbar-extensions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.file-filters {
|
||||
background: darken(#eee, 10%);
|
||||
width: 100%;
|
||||
display: inline-flex;
|
||||
flex-grow: nowrap;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
overflow-x: auto;
|
||||
border-bottom: 1px darken(#eee, 12%) solid;
|
||||
}
|
||||
.file-filter {
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
display: inline-flex;
|
||||
margin: auto 0.5rem;
|
||||
padding: 2px;
|
||||
width: 5rem;
|
||||
background: $informationColor;
|
||||
box-shadow: 0.375px 1.125px 0.75px darken($informationColor, 20%);
|
||||
}
|
||||
|
||||
.file-filter svg {
|
||||
margin: auto 0;
|
||||
width: 0.8rem;
|
||||
height: 0.8rem;
|
||||
}
|
||||
.file-filter svg:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.query-filters {
|
||||
width: 100%;
|
||||
background: darken(#eee, 10%);
|
||||
border-bottom: 1px darken(#eee, 20%) solid;
|
||||
}
|
||||
|
||||
.query-filter-list {
|
||||
width: 100%;
|
||||
height: 1.5rem;
|
||||
display: inline-flex;
|
||||
flex-grow: nowrap;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.query-filter {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: $sectionMenuOptions;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
display: inline-flex;
|
||||
border: 1px darken(#eee, 22%) solid;
|
||||
}
|
||||
|
||||
.query-filter-list span:first-child {
|
||||
border-left: 0px;
|
||||
}
|
48
src/stash/scss/stash/StashContextMenu.scss
Normal file
48
src/stash/scss/stash/StashContextMenu.scss
Normal file
|
@ -0,0 +1,48 @@
|
|||
@import "../global";
|
||||
//Context Menu
|
||||
.drive-context-menu {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
background: lighten($sectionMenuOptions, 10%);
|
||||
box-shadow: 1px 3px 2px lighten ($sectionMenuOptions, 5%);
|
||||
color: $foreground;
|
||||
z-index: 300;
|
||||
width: 100%;
|
||||
max-width: inherit;
|
||||
font-size: 1rem;
|
||||
max-width: 18em;
|
||||
min-height: 11em;
|
||||
}
|
||||
|
||||
.drive-context-menu ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.drive-context-menu li:hover {
|
||||
opacity: 0.85;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.drive-context-menu li {
|
||||
width: inherit;
|
||||
text-align: left;
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
|
||||
.drive-context-menu svg {
|
||||
min-width: 1rem;
|
||||
padding: 0 0.5rem;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.drive-context-menu li:not(:last-child) {
|
||||
margin-bottom: 0.85rem;
|
||||
}
|
||||
|
||||
.drive-context-menu a {
|
||||
color: $foreground;
|
||||
text-decoration: none;
|
||||
font-size: inherit;
|
||||
}
|
6
src/stash/scss/stash/StashDropzone.scss
Normal file
6
src/stash/scss/stash/StashDropzone.scss
Normal file
|
@ -0,0 +1,6 @@
|
|||
@import "../global";
|
||||
.stash-dropzone {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: inherit;
|
||||
}
|
161
src/stash/scss/stash/StashUploadDialog.scss
Normal file
161
src/stash/scss/stash/StashUploadDialog.scss
Normal file
|
@ -0,0 +1,161 @@
|
|||
@import "../global";
|
||||
//Variables
|
||||
$watcherIndicatorSize: 18px;
|
||||
$watcherActionSize: 14px;
|
||||
$uploadBoxesSize: 50px;
|
||||
$actionButtonSize: 25px;
|
||||
/*File Upload Dialog Styling*/
|
||||
.fud {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background-color: $sectionMenuOptions;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: 540px;
|
||||
max-height: 250px;
|
||||
overflow: hidden;
|
||||
-webkit-transform: translate3d(0, 0, 0);
|
||||
z-index: 100;
|
||||
display: block;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.fud-fade {
|
||||
visibility: visible;
|
||||
animation: file-watcher-fade 2s forwards;
|
||||
}
|
||||
|
||||
.fud-maximized {
|
||||
visibility: visible;
|
||||
animation: upload-dialog-expand 1.2s forwards;
|
||||
animation-timing-function: ease-out;
|
||||
}
|
||||
|
||||
.fud-minimized {
|
||||
visibility: visible;
|
||||
animation: upload-dialog-minimize 0.4s forwards;
|
||||
animation-timing-function: ease-out;
|
||||
}
|
||||
|
||||
#fud-header {
|
||||
display: inline-flex;
|
||||
height: $uploadBoxesSize;
|
||||
font-size: $watcherIndicatorSize;
|
||||
background-color: darken($sectionMenuOptions, 10%);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.fud-header-title-wrapper {
|
||||
margin: auto;
|
||||
padding: 10px;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
#fud-header-title {
|
||||
margin-left: 40px;
|
||||
}
|
||||
#fud-header-title:hover {
|
||||
opacity: 0.8;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#fud-minimize {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
#header-actions {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.fud-actions {
|
||||
//width: calc(#{$actionButtonSize} * 3);
|
||||
display: inline-flex;
|
||||
margin: auto;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
height: $uploadBoxesSize;
|
||||
}
|
||||
|
||||
.fud-action {
|
||||
min-width: $actionButtonSize;
|
||||
padding: 0 5px;
|
||||
|
||||
font-size: $watcherActionSize;
|
||||
}
|
||||
|
||||
.fud-action svg,
|
||||
i {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.fud-actions span:hover {
|
||||
opacity: 0.8;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#fud-header-status {
|
||||
font-size: calc(#{$watcherIndicatorSize - 2px});
|
||||
min-width: $uploadBoxesSize;
|
||||
min-height: $uploadBoxesSize;
|
||||
max-width: $uploadBoxesSize;
|
||||
max-height: $uploadBoxesSize;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/*Error and status icon Start with errors displaying as none*/
|
||||
#fud-status-icon {
|
||||
margin: auto;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.fud-error-wrapper {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 9px;
|
||||
left: 32px;
|
||||
font-size: 18px;
|
||||
}
|
||||
.fud-error-wrapper {
|
||||
color: $errorColor;
|
||||
max-width: 18px;
|
||||
max-height: 18px;
|
||||
}
|
||||
|
||||
#fud-error-count {
|
||||
font-size: 14px;
|
||||
color: $foreground;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.fud-status-error {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
#fud-queued-files {
|
||||
max-height: 200px;
|
||||
height: 100%;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
#fud-queued-files::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
}
|
||||
#fud-queued-files::-webkit-scrollbar-track {
|
||||
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
||||
border-radius: $defaultBorderRadius;
|
||||
}
|
||||
#fud-queued-files::-webkit-scrollbar-thumb {
|
||||
border-radius: $defaultBorderRadius;
|
||||
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.5);
|
||||
background-color: lighten($sectionMenuOptions, 40%);
|
||||
}
|
||||
#fud-queued-files::-webkit-scrollbar-thumb:hover {
|
||||
background-color: lighten($sectionMenuOptions, 50%);
|
||||
}
|
79
src/stash/scss/stash/StashUploadWatcher.scss
Normal file
79
src/stash/scss/stash/StashUploadWatcher.scss
Normal file
|
@ -0,0 +1,79 @@
|
|||
@import "./StashUploadDialog.scss";
|
||||
/*File Watcher Styling*/
|
||||
.file-watcher {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: inline-flex;
|
||||
max-height: $uploadBoxesSize;
|
||||
overflow: hidden;
|
||||
word-wrap: break-word;
|
||||
padding: none;
|
||||
}
|
||||
.file-watcher-progressbar {
|
||||
min-width: $uploadBoxesSize;
|
||||
min-height: $uploadBoxesSize;
|
||||
max-width: $uploadBoxesSize;
|
||||
max-height: $uploadBoxesSize;
|
||||
display: flex;
|
||||
max-height: $uploadBoxesSize;
|
||||
background-color: lighten($sectionMenuOptions, 20%);
|
||||
}
|
||||
.file-watcher.active {
|
||||
background-color: lighten($sectionMenuOptions, 10%);
|
||||
}
|
||||
.file-watcher.success {
|
||||
animation: file-watcher-fade 2s forwards;
|
||||
}
|
||||
.file-watcher-progressbar-fill {
|
||||
height: 100%;
|
||||
width: 0%;
|
||||
background: $successColor;
|
||||
display: flex;
|
||||
font-weight: bold;
|
||||
//align-items: center;
|
||||
transition: width 0.22s;
|
||||
}
|
||||
.file-watcher-progressbar-fill.error {
|
||||
background: $errorColor;
|
||||
}
|
||||
|
||||
.file-watcher-progressbar-indicator {
|
||||
min-width: $uploadBoxesSize;
|
||||
min-height: $uploadBoxesSize;
|
||||
max-width: $uploadBoxesSize;
|
||||
max-height: $uploadBoxesSize;
|
||||
color: $foreground;
|
||||
display: flex;
|
||||
font-size: $watcherIndicatorSize;
|
||||
}
|
||||
|
||||
.file-watcher-progressbar-indicator svg,
|
||||
span {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.file-watcher-name {
|
||||
margin: auto;
|
||||
overflow: hidden;
|
||||
word-wrap: break-word;
|
||||
padding-left: 10px;
|
||||
align-self: center;
|
||||
width: 100%;
|
||||
}
|
||||
.file-watcher-action {
|
||||
width: calc(#{$uploadBoxesSize} / 2);
|
||||
height: $uploadBoxesSize;
|
||||
display: flex;
|
||||
font-weight: bold;
|
||||
align-items: center;
|
||||
padding: 0px 5px;
|
||||
float: right;
|
||||
}
|
||||
.file-watcher-action svg {
|
||||
margin: auto;
|
||||
font-size: $watcherActionSize;
|
||||
}
|
||||
.file-watcher-action:hover {
|
||||
opacity: 0.8;
|
||||
cursor: pointer;
|
||||
}
|
34
src/stash/scss/stash/Stashbar.scss
Normal file
34
src/stash/scss/stash/Stashbar.scss
Normal file
|
@ -0,0 +1,34 @@
|
|||
@import "../global";
|
||||
|
||||
.stashbar {
|
||||
width: 100%;
|
||||
z-index: 200;
|
||||
}
|
||||
.stashbar-menu {
|
||||
width: 100%;
|
||||
height: $stashbarHeight;
|
||||
display: inline-flex;
|
||||
background-color: rgba($sectionMenu, 1);
|
||||
}
|
||||
|
||||
.stashbar-action {
|
||||
font-size: 1.25rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.stashbar-action svg {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.stashbar-action label {
|
||||
font-size: inherit;
|
||||
}
|
||||
.stashbar-action label:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.stashbar-action:hover {
|
||||
opacity: 0.85;
|
||||
cursor: pointer;
|
||||
}
|
33
src/stash/stashbar/StashbarMenu.jsx
Normal file
33
src/stash/stashbar/StashbarMenu.jsx
Normal file
|
@ -0,0 +1,33 @@
|
|||
import React from "react";
|
||||
import {
|
||||
faBars,
|
||||
faCloudUploadAlt,
|
||||
faSearch,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import "../scss/stash/Stashbar.scss";
|
||||
|
||||
class StashbarMenu extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div className="stashbar-menu">
|
||||
<span className="stashbar-action">
|
||||
<FontAwesomeIcon icon={faBars} />
|
||||
</span>
|
||||
<span className="stashbar-action">
|
||||
<label htmlFor="file-dropzone">
|
||||
<FontAwesomeIcon icon={faCloudUploadAlt} />
|
||||
</label>
|
||||
</span>
|
||||
<span
|
||||
className="stashbar-action"
|
||||
onClick={() => this.props.setSearchMode(true)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faSearch} />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default StashbarMenu;
|
174
src/stash/stashbar/StashbarSearch.jsx
Normal file
174
src/stash/stashbar/StashbarSearch.jsx
Normal file
|
@ -0,0 +1,174 @@
|
|||
import React from "react";
|
||||
import {
|
||||
faTimes,
|
||||
faArrowLeft,
|
||||
faHashtag,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
//Local imports
|
||||
import StashbarSearchTagDisplay from "./StashbarSearchTagDisplay";
|
||||
import "../scss/stash/Searchbar.scss";
|
||||
//Constants
|
||||
const hashtagChar = "#";
|
||||
const searchFilters = ["Selected", "Public"];
|
||||
class StashbarSearch extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
tags: [],
|
||||
query: "",
|
||||
};
|
||||
}
|
||||
|
||||
//Filtering Functions
|
||||
|
||||
updateQuery(query) {
|
||||
this.setState({ query }, this.updateFiltered);
|
||||
}
|
||||
|
||||
updateFiltered() {
|
||||
var fileBoxes = this.markAllFiltered();
|
||||
const activeTags = this.state.tags;
|
||||
const query = this.state.query.toLowerCase();
|
||||
for (var f in fileBoxes) {
|
||||
//If file isn't selected
|
||||
if (activeTags.includes(searchFilters[0]) && !fileBoxes[f].selected)
|
||||
fileBoxes[f].filtered = false;
|
||||
//If file isn't public
|
||||
if (activeTags.includes(searchFilters[1]) && !fileBoxes[f].file.public) {
|
||||
fileBoxes[f].filtered = false;
|
||||
}
|
||||
if (!fileBoxes[f].file.name.toLowerCase().includes(query))
|
||||
fileBoxes[f].filtered = false;
|
||||
}
|
||||
this.setState({ fileBoxes });
|
||||
}
|
||||
|
||||
markAllFiltered() {
|
||||
var fileBoxes = this.props.fileBoxes;
|
||||
for (var f in fileBoxes) {
|
||||
fileBoxes[f].filtered = true;
|
||||
}
|
||||
this.props.fileBoxesChanged(fileBoxes);
|
||||
return fileBoxes;
|
||||
}
|
||||
|
||||
//Searchbar Functions
|
||||
searchChanged(e) {
|
||||
this.updateQuery(e.target.value);
|
||||
}
|
||||
|
||||
searchbarClose() {
|
||||
this.markAllFiltered();
|
||||
this.props.setSearchMode(false);
|
||||
}
|
||||
//Tag Functions
|
||||
hashtagClick() {
|
||||
this.updateQuery(hashtagChar);
|
||||
}
|
||||
|
||||
queryTag(e) {
|
||||
var query = this.state.query;
|
||||
if (e.key !== "Enter" || query[0] !== hashtagChar) return;
|
||||
const emptySpace = "";
|
||||
const space = " ";
|
||||
query = query.substring(1).toLowerCase();
|
||||
const filters = this.getAvailableTags();
|
||||
var filter = null;
|
||||
for (filter of filters) {
|
||||
if (filter.toLowerCase().includes(query)) break;
|
||||
}
|
||||
if (filter !== null) this.addTag(filter);
|
||||
var firstSpace = query.indexOf(space);
|
||||
if (firstSpace === -1) query = emptySpace;
|
||||
else query = query.substring(query.indexOf(space));
|
||||
this.updateQuery(query);
|
||||
}
|
||||
|
||||
removeTag(tag) {
|
||||
var tags = this.state.tags;
|
||||
tags.splice(tags.indexOf(tag), 1);
|
||||
this.setState({ tags }, this.updateFiltered);
|
||||
}
|
||||
|
||||
addTag(tag) {
|
||||
var tags = this.state.tags;
|
||||
tags.push(tag);
|
||||
this.setState({ tags }, this.updateFiltered);
|
||||
}
|
||||
|
||||
getAvailableTags() {
|
||||
var availableFilters = [];
|
||||
searchFilters.forEach((filter, i) => {
|
||||
if (i === 0) return; //Ignore first filter 'Selected' triggered by selecting
|
||||
if (this.state.tags.includes(filter)) return;
|
||||
if (
|
||||
!filter
|
||||
.toLowerCase()
|
||||
.includes(this.state.query.substring(1).toLowerCase())
|
||||
)
|
||||
return;
|
||||
availableFilters.push(filter);
|
||||
});
|
||||
return availableFilters;
|
||||
}
|
||||
//Render
|
||||
render() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="file-searchbar stashbar-menu">
|
||||
<div className="file-searchbox">
|
||||
<span
|
||||
className="file-searchbox-action"
|
||||
onClick={this.searchbarClose.bind(this)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faArrowLeft} />
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
id="file-search"
|
||||
name="file-search"
|
||||
placeholder="Search"
|
||||
value={this.state.query}
|
||||
onChange={this.searchChanged.bind(this)}
|
||||
autoComplete="off"
|
||||
onKeyUp={this.queryTag.bind(this)}
|
||||
></input>
|
||||
<span
|
||||
className="file-searchbox-action"
|
||||
id="file-searchbox-hashtag"
|
||||
onClick={this.hashtagClick.bind(this)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faHashtag} />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="file-searchbar-extensions">
|
||||
{this.state.query.includes(hashtagChar) && (
|
||||
<StashbarSearchTagDisplay
|
||||
query={this.state.query}
|
||||
getAvailableTags={this.getAvailableTags.bind(this)}
|
||||
addTag={this.addTag.bind(this)}
|
||||
updateQuery={this.updateQuery.bind(this)}
|
||||
/>
|
||||
)}
|
||||
{this.state.tags.length > 0 && (
|
||||
<div className="file-filters">
|
||||
{this.state.tags.map((filter, index) => (
|
||||
<span className="file-filter" key={index}>
|
||||
<span> {filter}</span>{" "}
|
||||
<FontAwesomeIcon
|
||||
icon={faTimes}
|
||||
onClick={() => this.removeTag(filter)}
|
||||
/>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default StashbarSearch;
|
38
src/stash/stashbar/StashbarSearchTagDisplay.jsx
Normal file
38
src/stash/stashbar/StashbarSearchTagDisplay.jsx
Normal file
|
@ -0,0 +1,38 @@
|
|||
import React from "react";
|
||||
class StashbarSearchTagDisplay extends React.Component {
|
||||
filterClicked(tag) {
|
||||
this.props.updateQuery("");
|
||||
this.props.addTag(tag);
|
||||
}
|
||||
displayTags() {
|
||||
const tags = this.props.getAvailableTags();
|
||||
if (tags.length === 0)
|
||||
return (
|
||||
<span className="query-filter">No Filters Matched Your Search</span>
|
||||
);
|
||||
else
|
||||
return (
|
||||
<React.Fragment>
|
||||
{tags.map((tag, index) => (
|
||||
<span
|
||||
className="query-filter"
|
||||
key={index}
|
||||
onClick={() => this.filterClicked.bind(this)(tag)}
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="query-filters">
|
||||
<div className="query-filter-list">{this.displayTags()}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default StashbarSearchTagDisplay;
|
108
src/stash/uploader/StashUploadDialog.jsx
Normal file
108
src/stash/uploader/StashUploadDialog.jsx
Normal file
|
@ -0,0 +1,108 @@
|
|||
//Module Imports
|
||||
import React from "react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
faExclamationTriangle,
|
||||
faCloudUploadAlt,
|
||||
faRedoAlt,
|
||||
faTimes,
|
||||
faAngleUp,
|
||||
faAngleDown,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
//Local Imports
|
||||
import StashUploadWatcher from "./StashUploadWatcher";
|
||||
import "../scss/stash/StashUploadDialog.scss";
|
||||
//Icons List
|
||||
const successIcon = <FontAwesomeIcon icon={faCloudUploadAlt} />;
|
||||
const errorIcon = <FontAwesomeIcon icon={faExclamationTriangle} />;
|
||||
const retryIcon = <FontAwesomeIcon icon={faRedoAlt} />;
|
||||
const cancelIcon = <FontAwesomeIcon icon={faTimes} />;
|
||||
const upIcon = <FontAwesomeIcon icon={faAngleUp} />;
|
||||
const downIcon = <FontAwesomeIcon icon={faAngleDown} />;
|
||||
export default class StashUploadDialog extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isMinimized: false,
|
||||
};
|
||||
}
|
||||
|
||||
fudDisplay() {
|
||||
const uploadLength = Object.values(this.props.uploads).length;
|
||||
var className = "fud";
|
||||
if (this.props.fadeOnClear && uploadLength === 0) {
|
||||
return (className += " fud-fade");
|
||||
}
|
||||
if (this.state.isMinimized) return (className += " fud-minimized");
|
||||
if (uploadLength > 0) {
|
||||
return (className += " fud-maximized");
|
||||
}
|
||||
return className;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={this.fudDisplay()}>
|
||||
<div id="fud-header">
|
||||
<div id="fud-header-status">
|
||||
<span id="fud-status-icon">
|
||||
{this.props.errorCount > 0 ? errorIcon : successIcon}
|
||||
<span
|
||||
className="fud-error-wrapper"
|
||||
style={{ display: this.props.errorCount > 0 ? "flex" : "none" }}
|
||||
>
|
||||
<i className="fas fa-circle"></i>
|
||||
<span id="fud-error-count">{this.props.errorCount}</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="fud-header-title-wrapper">
|
||||
<span
|
||||
id="fud-header-title"
|
||||
onClick={() =>
|
||||
this.setState({ isMinimized: !this.state.isMinimized })
|
||||
}
|
||||
>
|
||||
Uploads
|
||||
<span id="fud-minimize">
|
||||
{this.state.isMinimized && upIcon}
|
||||
{!this.state.isMinimized && downIcon}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="fud-actions" id="header-actions">
|
||||
<span
|
||||
id="fud-retry"
|
||||
className="fud-action"
|
||||
onClick={this.props.retryAll}
|
||||
>
|
||||
{retryIcon}
|
||||
</span>
|
||||
<span
|
||||
id="fud-clear"
|
||||
className="fud-action"
|
||||
onClick={this.props.clearAll}
|
||||
>
|
||||
{cancelIcon}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="fud-queued-files">
|
||||
{Object.values(this.props.uploads).map(
|
||||
(upload, index) =>
|
||||
upload && (
|
||||
<StashUploadWatcher
|
||||
file={upload.file}
|
||||
key={upload.uploadUuid}
|
||||
retryUpload={() => this.props.retryUpload(upload.uploadUuid)}
|
||||
clearUpload={() => this.props.clearUpload(upload.uploadUuid)}
|
||||
uploadProgress={upload.progress}
|
||||
uploadStatus={upload.status}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
84
src/stash/uploader/StashUploadWatcher.jsx
Normal file
84
src/stash/uploader/StashUploadWatcher.jsx
Normal file
|
@ -0,0 +1,84 @@
|
|||
import React from "react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
faExclamationTriangle,
|
||||
faCheck,
|
||||
faRedoAlt,
|
||||
faTimes,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
//Local Imports
|
||||
import "../scss/stash/StashUploadWatcher.scss";
|
||||
|
||||
//Constants
|
||||
const successIcon = <FontAwesomeIcon icon={faCheck} />;
|
||||
const errorIcon = <FontAwesomeIcon icon={faExclamationTriangle} />;
|
||||
const retryIcon = <FontAwesomeIcon icon={faRedoAlt} />;
|
||||
const cancelIcon = <FontAwesomeIcon icon={faTimes} />;
|
||||
export default class StashUploadWatcher extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.retryUpload = props.retryUpload;
|
||||
this.clearUpload = props.clearUpload;
|
||||
}
|
||||
|
||||
isUploading() {
|
||||
return this.props.uploadProgress > 0 && this.props.uploadProgress !== 100;
|
||||
}
|
||||
|
||||
progressIndicator() {
|
||||
if (this.props.uploadStatus === "Success") return successIcon;
|
||||
if (this.props.uploadStatus === "Error") return errorIcon;
|
||||
if (this.isUploading())
|
||||
return (
|
||||
<span className="file-watcher-progressbar-text">
|
||||
{this.props.uploadProgress}%
|
||||
</span>
|
||||
);
|
||||
return <span />;
|
||||
}
|
||||
|
||||
watcherStatus() {
|
||||
var className = "file-watcher";
|
||||
if (this.isUploading() && this.props.uploadStatus !== "Error")
|
||||
className += " active";
|
||||
if (this.props.uploadStatus === "Success") className += " success";
|
||||
return className;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={this.watcherStatus()}>
|
||||
<div className="file-watcher-progressbar">
|
||||
<div
|
||||
className={
|
||||
this.props.uploadStatus === "Error"
|
||||
? "file-watcher-progressbar-fill error"
|
||||
: "file-watcher-progressbar-fill"
|
||||
}
|
||||
style={{
|
||||
width:
|
||||
this.props.uploadStatus === "Error"
|
||||
? "100%"
|
||||
: `${this.props.uploadProgress}%`,
|
||||
}}
|
||||
>
|
||||
<span className="file-watcher-progressbar-indicator">
|
||||
{this.progressIndicator()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="file-watcher-name">{this.props.file.name}</span>
|
||||
<div className="fud-actions">
|
||||
{this.props.uploadStatus === "Error" && (
|
||||
<span className="file-watcher-action" onClick={this.retryUpload}>
|
||||
{retryIcon}
|
||||
</span>
|
||||
)}
|
||||
<span className="file-watcher-action" onClick={this.clearUpload}>
|
||||
{cancelIcon}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
Reference in a new issue