Updated setting pane; added contect menu

This commit is contained in:
itdominator 2021-10-03 03:54:20 -05:00
parent 633cb5b955
commit 927be36d9f
16 changed files with 498 additions and 29 deletions

View File

@ -23,3 +23,5 @@ n/a
![1 Videos List](images/pic1.png)
![2 Video Playing](images/pic2.png)
![3 Images List](images/pic3.png)
![4 Context menu](images/pic4.png)
![5 Settings Pane With Upload And Create Functionality](images/pic5.png)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

BIN
images/pic4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 454 KiB

BIN
images/pic5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 KiB

View File

@ -1,8 +1,9 @@
# Python imports
import os, json, secrets
import os, json, secrets, re, shutil
# Lib imports
from flask import request, session, render_template, send_from_directory, redirect
from flask_uploads import UploadSet, configure_uploads, ALL
from flask_login import current_user
@ -14,6 +15,8 @@ from core.utils.shellfm import WindowController # Get file manager controller
msgHandler = MessageHandler()
window_controllers = {}
# valid_fname_pat = re.compile(r"/^[a-zA-Z0-9-_\[\]\(\)| ]+$/")
valid_fname_pat = re.compile(r"[a-z0-9A-Z-_\[\]\(\)\| ]{4,20}")
def get_window_controller():
@ -88,13 +91,14 @@ def listFiles(_hash = None):
msg = "Can't manage the request type..."
return msgHandler.createMessageJSON("danger", msg)
@app.route('/api/file-manager-action/<_type>/<_hash>')
@app.route('/api/file-manager-action/<_type>/<_hash>', methods=['GET', 'POST'])
def fileManagerAction(_type, _hash = None):
view = get_window_controller().get_window(1).get_view(0)
if _type == "reset-path" and _hash == None:
if _type == "reset-path" and _hash == "None":
view.set_to_home()
return redirect("/")
msg = "Returning to home directory..."
return msgHandler.createMessageJSON("success", msg)
folder = view.get_current_directory()
file = view.get_path_part_from_hash(_hash)
@ -119,6 +123,30 @@ def fileManagerAction(_type, _hash = None):
return msgHandler.createMessageJSON("success", msg)
# NOTE: Positionally protecting actions further down that are privlidged
# Be aware of ordering!
msg = "Log in with an Admin privlidged user to do this action!"
if not oidc.user_loggedin:
return msgHandler.createMessageJSON("danger", msg)
elif oidc.user_loggedin:
isAdmin = oidc.user_getfield("isAdmin")
if isAdmin != "yes" :
return msgHandler.createMessageJSON("danger", msg)
if _type == "delete":
try:
msg = f"[Success] Deleted the file/folder -->: {file} !"
if os.path.isfile(fpath):
os.unlink(fpath)
else:
shutil.rmtree(fpath)
return msgHandler.createMessageJSON("success", msg)
except Exception as e:
msg = "[Error] Unable to delete the file/folder...."
return msgHandler.createMessageJSON("danger", msg)
@app.route('/api/list-favorites', methods=['GET', 'POST'])
def listFavorites():
if request.method == 'POST':
@ -161,13 +189,91 @@ def manageFavorites(_action):
fave = Favorites(link = sub_path)
db.session.add(fave)
msg = "Added to Favorites successfully..."
else:
elif ACTION == "delete":
fave = db.session.query(Favorites).filter_by(link = sub_path).first()
db.session.delete(fave)
msg = "Deleted from Favorites successfully..."
else:
msg = "Couldn't handle action for favorites item..."
return msgHandler.createMessageJSON("danger", msg)
db.session.commit()
return msgHandler.createMessageJSON("success", msg)
else:
msg = "Can't manage the request type..."
return msgHandler.createMessageJSON("danger", msg)
@app.route('/api/create/<_type>', methods=['GET', 'POST'])
def create_item(_type = None):
if request.method == 'POST':
msg = "Log in with an Admin privlidged user to upload files!"
if not oidc.user_loggedin:
return msgHandler.createMessageJSON("danger", msg)
elif oidc.user_loggedin:
isAdmin = oidc.user_getfield("isAdmin")
if isAdmin != "yes" :
return msgHandler.createMessageJSON("danger", msg)
TYPE = _type.strip()
FNAME = str(request.values['fname']).strip()
if not re.fullmatch(valid_fname_pat, FNAME):
msg = "A new item name can only contain alphanumeric, -, _, |, [], (), or spaces and must be minimum of 4 and max of 20 characters..."
return msgHandler.createMessageJSON("danger", msg)
view = get_window_controller().get_window(1).get_view(0)
folder = view.get_current_directory()
new_item = folder + '/' + FNAME
try:
if TYPE == "dir":
os.mkdir(new_item)
elif TYPE == "file":
open(new_item + ".txt", 'a').close()
else:
msg = "Couldn't handle action type for api create..."
return msgHandler.createMessageJSON("danger", msg)
except Exception as e:
print(repr(e))
msg = "Couldn't create file/folder. An unexpected error occured..."
return msgHandler.createMessageJSON("danger", msg)
msg = "[Success] created the file/dir..."
return msgHandler.createMessageJSON("success", msg)
else:
msg = "Can't manage the request type..."
return msgHandler.createMessageJSON("danger", msg)
@app.route('/upload', methods=['GET', 'POST'])
def upload():
if request.method == 'POST' and len(request.files) > 0:
msg = "Log in with an Admin privlidged user to upload files!"
if not oidc.user_loggedin:
return msgHandler.createMessageJSON("danger", msg)
elif oidc.user_loggedin:
isAdmin = oidc.user_getfield("isAdmin")
if isAdmin != "yes" :
return msgHandler.createMessageJSON("danger", msg)
view = get_window_controller().get_window(1).get_view(0)
folder = view.get_current_directory()
UPLOADS_PTH = folder + '/'
files = UploadSet('files', ALL, default_dest=lambda x: UPLOADS_PTH)
configure_uploads(app, files)
for file in request.files:
try:
files.save(request.files[file])
except Exception as e:
print(repr(e))
msg = "[Error] Failed to upload some or all of the file(s)..."
return msgHandler.createMessageJSON("danger", msg)
msg = "[Success] Uploaded file(s)..."
return msgHandler.createMessageJSON("success", msg)
else:
msg = "Can't manage the request type..."
return msgHandler.createMessageJSON("danger", msg)

View File

@ -0,0 +1,50 @@
.menu {
width: 165px;
z-index: 999;
box-shadow: 0 4px 5px 3px rgba(0, 0, 0, 0.2);
background-color: rgba(0, 0, 0, 0.64);
position: fixed;
display: none;
transition: 0.2s display ease-in;
}
.menu .menu-options {
list-style: none;
padding: 10px 0;
z-index: 1;
}
.menu .menu-options .menu-option {
font-weight: 500;
z-index: 1;
padding: 10px 40px 10px 20px;
cursor: pointer;
}
.menu .menu-options .menu-option:hover {
background: rgba(255, 255, 255, 0.64);
color: rgba(0, 0, 0, 0.5);
}
button {
background: grey;
border: none;
}
button .next {
color: green;
}
button[disabled="false"]:hover .next {
color: red;
animation: move 0.5s;
animation-iteration-count: 2;
}
@keyframes move {
from {
transform: translate(0%);
}
50% {
transform: translate(-40%);
}
to {
transform: transform(0%);
}
}

View File

@ -1,3 +1,13 @@
const goHomeAjax = async (hash) => {
const data = "empty=NULL";
doAjax("api/file-manager-action/reset-path/None", data, "reset-path");
}
const deleteItemAjax = async (hash) => {
const data = "empty=NULL";
doAjax("api/file-manager-action/delete/" + hash, data, "delete-file");
}
const listFilesAjax = async (hash) => {
const data = "empty=NULL";
doAjax("api/list-files/" + hash, data, "list-files");
@ -19,6 +29,8 @@ const manageFavoritesAjax = async (action) => {
}
const doAjax = (actionPath, data, action) => {
let xhttp = new XMLHttpRequest();
@ -44,6 +56,60 @@ const doAjax = (actionPath, data, action) => {
xhttp.send(data);
}
const doAjaxUpload = (actionPath, data, fname, action) => {
let bs64 = btoa(unescape(encodeURIComponent(fname))).split("==")[0];
const query = '[id="' + bs64 + '"]';
let progressbar = document.querySelector(query);
let xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState === 4 && this.status === 200) {
if (this.responseText != null) { // this.responseXML if getting XML data
postAjaxController(JSON.parse(this.responseText), action);
} else {
msg = "[Fail] Status Code: " + response.status +
"\n[Message] --> " + response.statusText;
handleMessage('alert-warning', msg);
}
}
};
// For upload tracking with GET...
xhttp.onprogress = function (e) {
if (e.lengthComputable) {
percent = (e.loaded / e.total) * 100;
text = parseFloat(percent).toFixed(2) + '% Complete (' + fname + ')';
if (e.loaded !== e.total ) {
updateProgressBar(progressbar, text, percent, "info");
} else {
updateProgressBar(progressbar, text, percent, "success");
}
}
}
// For upload tracking with POST...
xhttp.upload.addEventListener("progress", function(e){
if (e.lengthComputable) {
percent = parseFloat( Math.floor(
(
(e.loaded / e.total) * 100 ).toFixed(2)
).toFixed(2)
);
text = percent + '% Complete (' + fname + ')';
if (percent <= 95) {
updateProgressBar(progressbar, text, percent, "info");
} else {
updateProgressBar(progressbar, text, percent, "success");
}
}
}, false);
xhttp.open("POST", actionPath);
// Force return to be JSON NOTE: Use application/xml to force XML
xhttp.overrideMimeType('application/json');
xhttp.send(data);
}
const fetchData = async (url) => {
let response = await fetch(url);
return await response.json();

View File

@ -0,0 +1,41 @@
const menu = document.querySelector(".menu");
let menuVisible = false;
let active_card = null;
const toggleMenu = command => {
menu.style.display = command === "show" ? "block" : "none";
menu.style.zIndex = "9999";
menuVisible = !menuVisible;
};
const setPosition = ({ top, left }) => {
menu.style.left = `${left}px`;
menu.style.top = `${top}px`;
toggleMenu("show");
};
window.addEventListener("click", e => {
if(menuVisible) toggleMenu("hide");
});
window.addEventListener("contextmenu", e => {
e.preventDefault();
let target = e.target;
let elm = target;
while (elm.nodeName != "BODY") {
if (!elm.classList.contains("card")) {
elm = elm.parentElement;
} else {
active_card = elm;
break
}
}
const origin = {
left: e.pageX,
top: e.pageY
};
setPosition(origin);
return false;
});

View File

@ -0,0 +1,30 @@
const createItem = (type) => {
if (type == null || type == '') {
displayMessage("Create type isn't set...", "danger", 3, "settings-alert-zone-new-items");
return ;
}
let newItem = document.getElementById("newItem");
let fname = newItem.value;
const regex = /^[a-z0-9A-Z-_\[\]\(\)\| ]{4,20}$/;
if (fname.search(regex) == -1) {
displayMessage("A new item name can only contain alphanumeric, -, _, |, [], (), or spaces and must be minimum of 4 and max of 20 characters...", "danger", 3, "settings-alert-zone-new-items");
return ;
}
newItem.value = "";
createItemAjax(type, fname);
}
$( "#toUpload" ).bind( "change", function(eve) {
const files = eve.target.files;
setUploadListTitles(files);
});
$( "#uploadFiles" ).bind( "click", function(eve) {
const files = document.getElementById('toUpload').files;
uploadFiles(files);
});

View File

@ -1,7 +1,35 @@
const postAjaxController = (data, action) => {
if (data.message) {
message = data.message
displayMessage(message.text, message.type);
type = data.message.type
message = data.message.text
if (action === "reset-path") {
reloadDirectory();
return ;
}
if (action === "delete-file") {
reloadDirectory();
displayMessage(message, type);
return ;
}
if (action === "upload-text" || action === "upload-file") {
let field = null;
if (action === "upload-text") field = "settings-alert-zone-text";
if (action === "upload-file") field = "settings-alert-zone-files";
displayMessage(message, type, 3, field);
reloadDirectory();
return ;
}
if (action === "create-item") {
displayMessage(message, type, 3, "settings-alert-zone-new-items");
reloadDirectory();
return ;
}
displayMessage(message, type);
return ;
}

View File

@ -0,0 +1,89 @@
// Uploader Logic
const setUploadListTitles = (files = null) => {
if (files == null) {
return ;
}
let list = document.getElementById('uploadListTitles');
clearChildNodes(list);
for (var i = 0; i < files.length; i++) {
let liTag = document.createElement('LI');
let name = document.createTextNode(files[i].name);
liTag.className = "list-group-item disabled progress-bar";
let bs64 = btoa(unescape(encodeURIComponent(files[i].name))).split("==")[0];
liTag.setAttribute("id", bs64);
liTag.append(name);
list.append(liTag);
}
}
const uploadFiles = (files = null) => {
const size = files.length;
if (files == null || size < 1) {
displayMessage("Nothing to upload...", "warning", "page-alert-zone-2");
return ;
}
// Multi-upload...
if (size > 1) {
for (var i = 0; i < size; i++) {
file = files[i];
name = file.name;
data = createFormDataFiles([file]);
doAjaxUpload('upload', data, name, "upload-file");
}
} else { // Single upload...
data = createFormDataFiles(files);
name = files[0].name;
doAjaxUpload('upload', data, name, "upload-file");
}
}
const createFormDataFiles = (files) => {
let form = new FormData();
for (var i = 0; i < files.length; i++) {
form.append(files[i].name, files[i]);
}
return form;
}
// Progressbar handler
const updateProgressBar = (progressbar = null, text = "Nothing uploading...",
percent = 0, type = "error") => {
if (progressbar == null) {
return ;
}
if (type == "info") {
progressbar.setAttribute("aria-valuenow", percent);
progressbar.style.width = percent + "%";
// progressbar.innerText = text;
progressbar.classList.remove('bg-success');
progressbar.classList.add('progress-bar-animated');
progressbar.classList.add('bg-info');
return ;
}
if (type == "success") {
progressbar.setAttribute("aria-valuenow", 100);
progressbar.style.width = "100%";
// progressbar.innerText = text;
progressbar.classList.remove('progress-bar-animated');
progressbar.classList.remove('bg-info');
progressbar.classList.add('bg-success');
return ;
}
progressbar.style.width = "100%";
progressbar.innerText = "An Error Occured";
progressbar.classList.remove('progress-bar-animated');
progressbar.classList.remove('bg-info');
progressbar.classList.remove('bg-success');
progressbar.classList.add('bg-danger');
}

View File

@ -1,3 +1,37 @@
// Context Menu items
const goHome = () => {
goHomeAjax();
}
const clearUlList = () => {
const titles = document.getElementById('uploadListTitles');
const files = document.getElementById('toUpload');
files.value = null;
clearChildNodes(titles);
}
const downloadItem = (eve) => {
let elm = active_card.querySelector('a');
elm.click();
}
const deleteItem = (eve) => {
let elm = active_card.querySelector('[hash]'); // With attribute named "hash"
let elm2 = active_card.querySelector('[title]'); // With attribute named "title"
const hash = elm.getAttribute("hash");
const title = elm2.getAttribute("title");
let res = confirm("Delete: " + title + " ?");
if (res == true) {
deleteItemAjax(hash);
}
}
// Header menu items
const reloadDirectory = () => {
const target = document.getElementById('refresh-btn');
const hash = target.getAttribute("hash");
@ -5,7 +39,7 @@ const reloadDirectory = () => {
}
const goUpADirectory = () => {
const target = document.getElementById('back-btn')
const target = document.getElementById('back-btn');
const hash = target.getAttribute("hash");
listFilesAjax(hash);
}

View File

@ -34,6 +34,7 @@
<!-- Site CSS -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/main.css')}}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/context-menu.css')}}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/overrides.css')}}">
{% block header_css_additional %}
{% endblock header_css_additional %}
@ -47,11 +48,25 @@
</head>
{% endblock %}
<body>
<video id="bg" src="{{ url_for('static', filename='imgs/backgrounds/particles.mp4')}}"
<!-- <video id="bg" src="{{ url_for('static', filename='imgs/backgrounds/particles.mp4')}}"
poster="{{ url_for('static', filename='imgs/backgrounds/000.png')}}"
autoplay loop>
</video> -->
<video id="bg" src="{{ url_for('static', filename='imgs/backgrounds/tendrels.webm')}}"
poster="{{ url_for('static', filename='imgs/backgrounds/000.png')}}"
autoplay loop>
</video>
<div class="menu">
<ul class="menu-options">
<li class="menu-option" onclick="goHome()">Home Directory</li>
<li class="menu-option" onclick="clearUlList()">Clear Upload List</li>
<li class="menu-option" onclick="downloadItem()">Download</li>
<li class="menu-option" onclick="deleteItem()">Delete</li>
</ul>
</div>
{% block body_header %}
{% include "body-header.html" %}
@ -115,6 +130,8 @@
<!-- Application Imports -->
{% block body_scripts_additional %}
{% endblock body_scripts_additional%}
<script src="{{ url_for('static', filename='js/context-menu.js')}}"></script>
{% endblock %}
</body>
</html>

View File

@ -11,27 +11,33 @@
</div>
<div class="modal-body">
<div class="row">
<div class="col scroller" style="max-height: 30em; overflow: auto;">
<!-- <center>
<form>
<input class="ulFile" type="file" title="files To Upload" name="filesToUpload[]" data-bs-multiple-caption="{count} files selected" multiple />
<input type="button" onclick="uploadFiles()" name="UploadFiles" title="Upload File(s)" value="Upload File(s)" />
<input type="reset" title="Clear" id="CLEARBTTN" value="Clear" style="display:none;">
<input type="text" id="DIRPATHUL" name="DIRPATHUL" value="">
</form>
<br/>
<input type="text" id="NewItem" value=""/>
<input type="button" value="New Dir" onclick="createItem('dir')"/>
<input type="button" value="New File" onclick="createItem('file')"/>
<input type="button" value="Show Server Messages" onclick="tgglElmView('serverMsgView')"/>
<br/>
<input id="MergeType" type="checkbox" onchange="getDir('./')" />
<label for="MergeType">Show seassons in same list.</label>
</center> -->
<div class="row scroller" style="max-height: 30em; overflow: auto;">
<!-- File Upload -->
<div class="col-md-6 controls-secondary">
{% if oidc_loggedin() and oidc_isAdmin() %}
<!-- Server Messages -->
<div id="settings-alert-zone-files"></div>
<!-- To Upload List -->
<ul id="uploadListTitles" class="list-group scroller" style="min-height: 286px; max-height: 286px; overflow: hidden auto;">
</ul>
<input id="toUpload" class="btn btn-dark" type="file" multiple>
<button id="uploadFiles" class="btn btn-success">Submit Files...</button>
{% else %}
<h3>Upload not available to this user...</h3>
{% endif %}
</div>
<div class="col-md-6 controls-secondary">
{% if oidc_loggedin() and oidc_isAdmin() %}
<div id="settings-alert-zone-new-items"></div>
<input type="text" id="newItem" placeholder="File name..." value=""/>
<input type="button" class="btn btn-dark" value="New Dir" onclick="createItem('dir')"/>
<input type="button" class="btn btn-dark" value="New File" onclick="createItem('file')"/>
{% endif %}
<br/>
<!-- <input id="MergeType" type="checkbox" onchange="getDir('./')" />
<label for="MergeType">Show seassons in same list.</label> -->
</div>
<div id="page-alert-zone-2"></div>
</div>
</div>