feat: upload progress bar and error message display for each file, #146
This commit is contained in:
parent
0c47850fbc
commit
257fff3e92
70
interface.py
70
interface.py
@ -2,6 +2,8 @@
|
|||||||
import sqlite3
|
import sqlite3
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from flask import Flask, render_template, request, redirect, send_file, Response, jsonify, abort
|
from flask import Flask, render_template, request, redirect, send_file, Response, jsonify, abort
|
||||||
|
from werkzeug.utils import secure_filename
|
||||||
|
|
||||||
import variables as var
|
import variables as var
|
||||||
import util
|
import util
|
||||||
import math
|
import math
|
||||||
@ -61,7 +63,7 @@ class ReverseProxied(object):
|
|||||||
|
|
||||||
|
|
||||||
web = Flask(__name__)
|
web = Flask(__name__)
|
||||||
#web.config['TEMPLATES_AUTO_RELOAD'] = True
|
web.config['TEMPLATES_AUTO_RELOAD'] = True
|
||||||
log = logging.getLogger("bot")
|
log = logging.getLogger("bot")
|
||||||
user = 'Remote Control'
|
user = 'Remote Control'
|
||||||
|
|
||||||
@ -71,6 +73,7 @@ def init_proxy():
|
|||||||
if var.is_proxified:
|
if var.is_proxified:
|
||||||
web.wsgi_app = ReverseProxied(web.wsgi_app)
|
web.wsgi_app = ReverseProxied(web.wsgi_app)
|
||||||
|
|
||||||
|
|
||||||
# https://stackoverflow.com/questions/29725217/password-protect-one-webpage-in-flask-app
|
# https://stackoverflow.com/questions/29725217/password-protect-one-webpage-in-flask-app
|
||||||
|
|
||||||
|
|
||||||
@ -94,11 +97,13 @@ def requires_auth(f):
|
|||||||
def decorated(*args, **kwargs):
|
def decorated(*args, **kwargs):
|
||||||
global log
|
global log
|
||||||
auth = request.authorization
|
auth = request.authorization
|
||||||
if var.config.getboolean("webinterface", "require_auth") and (not auth or not check_auth(auth.username, auth.password)):
|
if var.config.getboolean("webinterface", "require_auth") and (
|
||||||
|
not auth or not check_auth(auth.username, auth.password)):
|
||||||
if auth:
|
if auth:
|
||||||
log.info("web: Failed login attempt, user: %s" % auth.username)
|
log.info("web: Failed login attempt, user: %s" % auth.username)
|
||||||
return authenticate()
|
return authenticate()
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
return decorated
|
return decorated
|
||||||
|
|
||||||
|
|
||||||
@ -129,13 +134,14 @@ def build_tags_color_lookup():
|
|||||||
|
|
||||||
return color_lookup
|
return color_lookup
|
||||||
|
|
||||||
|
|
||||||
def get_all_dirs():
|
def get_all_dirs():
|
||||||
dirs = []
|
dirs = []
|
||||||
paths = var.music_db.query_all_paths()
|
paths = var.music_db.query_all_paths()
|
||||||
for path in paths:
|
for path in paths:
|
||||||
pos = 0
|
pos = 0
|
||||||
while True:
|
while True:
|
||||||
pos = path.find("/", pos+1)
|
pos = path.find("/", pos + 1)
|
||||||
if pos == -1:
|
if pos == -1:
|
||||||
break
|
break
|
||||||
folder = path[:pos]
|
folder = path[:pos]
|
||||||
@ -173,11 +179,10 @@ def playlist():
|
|||||||
_from = int(request.args['range_from'])
|
_from = int(request.args['range_from'])
|
||||||
_to = int(request.args['range_to'])
|
_to = int(request.args['range_to'])
|
||||||
else:
|
else:
|
||||||
if var.playlist.current_index - int(DEFAULT_DISPLAY_COUNT/2) > 0:
|
if var.playlist.current_index - int(DEFAULT_DISPLAY_COUNT / 2) > 0:
|
||||||
_from = var.playlist.current_index - int(DEFAULT_DISPLAY_COUNT/2)
|
_from = var.playlist.current_index - int(DEFAULT_DISPLAY_COUNT / 2)
|
||||||
_to = _from - 1 + DEFAULT_DISPLAY_COUNT
|
_to = _from - 1 + DEFAULT_DISPLAY_COUNT
|
||||||
|
|
||||||
|
|
||||||
tags_color_lookup = build_tags_color_lookup() # TODO: cached this?
|
tags_color_lookup = build_tags_color_lookup() # TODO: cached this?
|
||||||
items = []
|
items = []
|
||||||
|
|
||||||
@ -196,12 +201,12 @@ def playlist():
|
|||||||
if item.artist:
|
if item.artist:
|
||||||
artist = item.artist
|
artist = item.artist
|
||||||
elif isinstance(item, URLItem):
|
elif isinstance(item, URLItem):
|
||||||
path = f" <a href=\"{ item.url }\"><i>{ item.url }</i></a>"
|
path = f" <a href=\"{item.url}\"><i>{item.url}</i></a>"
|
||||||
elif isinstance(item, PlaylistURLItem):
|
elif isinstance(item, PlaylistURLItem):
|
||||||
path = f" <a href=\"{ item.url }\"><i>{ item.url }</i></a>"
|
path = f" <a href=\"{item.url}\"><i>{item.url}</i></a>"
|
||||||
artist = f" <a href=\"{ item.playlist_url }\"><i>{ item.playlist_title }</i></a>"
|
artist = f" <a href=\"{item.playlist_url}\"><i>{item.playlist_title}</i></a>"
|
||||||
elif isinstance(item, RadioItem):
|
elif isinstance(item, RadioItem):
|
||||||
path = f" <a href=\"{ item.url }\"><i>{ item.url }</i></a>"
|
path = f" <a href=\"{item.url}\"><i>{item.url}</i></a>"
|
||||||
|
|
||||||
thumb = ""
|
thumb = ""
|
||||||
if item.type != 'radio' and item.thumbnail:
|
if item.type != 'radio' and item.thumbnail:
|
||||||
@ -373,6 +378,7 @@ def post():
|
|||||||
log.info("web: playback mode changed to autoplay.")
|
log.info("web: playback mode changed to autoplay.")
|
||||||
if action == "rescan":
|
if action == "rescan":
|
||||||
var.cache.build_dir_cache()
|
var.cache.build_dir_cache()
|
||||||
|
var.music_db.manage_special_tags()
|
||||||
log.info("web: Local file cache refreshed.")
|
log.info("web: Local file cache refreshed.")
|
||||||
elif action == "stop":
|
elif action == "stop":
|
||||||
if var.config.getboolean("bot", "clear_when_stop_in_oneshot", fallback=False) \
|
if var.config.getboolean("bot", "clear_when_stop_in_oneshot", fallback=False) \
|
||||||
@ -421,6 +427,7 @@ def post():
|
|||||||
|
|
||||||
return status()
|
return status()
|
||||||
|
|
||||||
|
|
||||||
def build_library_query_condition(form):
|
def build_library_query_condition(form):
|
||||||
try:
|
try:
|
||||||
condition = Condition()
|
condition = Condition()
|
||||||
@ -457,6 +464,7 @@ def build_library_query_condition(form):
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
abort(400)
|
abort(400)
|
||||||
|
|
||||||
|
|
||||||
@web.route("/library", methods=['POST'])
|
@web.route("/library", methods=['POST'])
|
||||||
@requires_auth
|
@requires_auth
|
||||||
def library():
|
def library():
|
||||||
@ -476,7 +484,7 @@ def library():
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
if not total_count:
|
if not total_count:
|
||||||
return ('', 204)
|
return '', 204
|
||||||
|
|
||||||
if request.form['action'] == 'add':
|
if request.form['action'] == 'add':
|
||||||
items = dicts_to_items(var.music_db.query_music(condition))
|
items = dicts_to_items(var.music_db.query_music(condition))
|
||||||
@ -521,11 +529,8 @@ def library():
|
|||||||
|
|
||||||
results = []
|
results = []
|
||||||
for item in items:
|
for item in items:
|
||||||
result = {}
|
result = {'id': item.id, 'title': item.title, 'type': item.display_type(),
|
||||||
result['id'] = item.id
|
'tags': [(tag, tag_color(tag)) for tag in item.tags]}
|
||||||
result['title'] = item.title
|
|
||||||
result['type'] = item.display_type()
|
|
||||||
result['tags'] = [(tag, tag_color(tag)) for tag in item.tags]
|
|
||||||
if item.type != 'radio' and item.thumbnail:
|
if item.type != 'radio' and item.thumbnail:
|
||||||
result['thumb'] = f"data:image/PNG;base64,{item.thumbnail}"
|
result['thumb'] = f"data:image/PNG;base64,{item.thumbnail}"
|
||||||
else:
|
else:
|
||||||
@ -566,21 +571,21 @@ def library():
|
|||||||
def upload():
|
def upload():
|
||||||
global log
|
global log
|
||||||
|
|
||||||
files = request.files.getlist("file[]")
|
file = request.files['file']
|
||||||
if not files:
|
if not file:
|
||||||
return redirect("./", code=400)
|
abort(400)
|
||||||
|
|
||||||
# filename = secure_filename(file.filename).strip()
|
|
||||||
for file in files:
|
|
||||||
filename = file.filename
|
filename = file.filename
|
||||||
if filename == '':
|
if filename == '':
|
||||||
return redirect("./", code=400)
|
abort(400)
|
||||||
|
|
||||||
targetdir = request.form['targetdir'].strip()
|
targetdir = request.form['targetdir'].strip()
|
||||||
if targetdir == '':
|
if targetdir == '':
|
||||||
targetdir = 'uploads/'
|
targetdir = 'uploads/'
|
||||||
elif '../' in targetdir:
|
elif '../' in targetdir:
|
||||||
return redirect("./", code=400)
|
abort(403)
|
||||||
|
|
||||||
|
filename = secure_filename(file.filename).strip()
|
||||||
|
|
||||||
log.info('web: Uploading file from %s:' % request.remote_addr)
|
log.info('web: Uploading file from %s:' % request.remote_addr)
|
||||||
log.info('web: - filename: ' + filename)
|
log.info('web: - filename: ' + filename)
|
||||||
@ -589,30 +594,27 @@ def upload():
|
|||||||
|
|
||||||
if "audio" in file.mimetype:
|
if "audio" in file.mimetype:
|
||||||
storagepath = os.path.abspath(os.path.join(var.music_folder, targetdir))
|
storagepath = os.path.abspath(os.path.join(var.music_folder, targetdir))
|
||||||
print('storagepath:', storagepath)
|
|
||||||
if not storagepath.startswith(os.path.abspath(var.music_folder)):
|
if not storagepath.startswith(os.path.abspath(var.music_folder)):
|
||||||
return redirect("./", code=400)
|
abort(403)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
os.makedirs(storagepath)
|
os.makedirs(storagepath)
|
||||||
except OSError as ee:
|
except OSError as ee:
|
||||||
if ee.errno != errno.EEXIST:
|
if ee.errno != errno.EEXIST:
|
||||||
return redirect("./", code=500)
|
log.error(f'web: failed to create directory {storagepath}')
|
||||||
|
abort(500)
|
||||||
|
|
||||||
filepath = os.path.join(storagepath, filename)
|
filepath = os.path.join(storagepath, filename)
|
||||||
log.info(' - filepath: ' + filepath)
|
log.info('web: - file saved at: ' + filepath)
|
||||||
if os.path.exists(filepath):
|
if os.path.exists(filepath):
|
||||||
continue
|
return 'File existed!', 409
|
||||||
|
|
||||||
file.save(filepath)
|
file.save(filepath)
|
||||||
else:
|
else:
|
||||||
continue
|
log.error(f'web: unsupported file type {file.mimetype}! File was not saved.')
|
||||||
|
return 'Unsupported media type!', 415
|
||||||
|
|
||||||
var.cache.build_dir_cache()
|
return '', 200
|
||||||
var.music_db.manage_special_tags()
|
|
||||||
log.info("web: Local file cache refreshed.")
|
|
||||||
|
|
||||||
return redirect("./", code=302)
|
|
||||||
|
|
||||||
|
|
||||||
@web.route('/download', methods=["GET"])
|
@web.route('/download', methods=["GET"])
|
||||||
|
@ -265,7 +265,6 @@ class OneshotPlaylist(BasePlaylist):
|
|||||||
return self
|
return self
|
||||||
|
|
||||||
def next(self):
|
def next(self):
|
||||||
print(f"*** next asked")
|
|
||||||
if len(self) > 0:
|
if len(self) > 0:
|
||||||
self.version += 1
|
self.version += 1
|
||||||
|
|
||||||
|
@ -4,9 +4,6 @@ $('#uploadSelectFile').on('change', function () {
|
|||||||
//replace the "Choose a file" label
|
//replace the "Choose a file" label
|
||||||
$(this).next('.custom-file-label').html(fileName);
|
$(this).next('.custom-file-label').html(fileName);
|
||||||
});
|
});
|
||||||
$('a.a-submit, button.btn-submit').on('click', function (event) {
|
|
||||||
$(event.target).closest('form').submit();
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// ----------------------
|
// ----------------------
|
||||||
@ -677,6 +674,10 @@ function processResults(data) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------
|
||||||
|
// ------ Tagging ------
|
||||||
|
// ---------------------
|
||||||
|
|
||||||
const add_tag_modal_title = $("#addTagModalTitle");
|
const add_tag_modal_title = $("#addTagModalTitle");
|
||||||
const add_tag_modal_item_id = $("#addTagModalItemId");
|
const add_tag_modal_item_id = $("#addTagModalItemId");
|
||||||
const add_tag_modal_tags = $("#addTagModalTags");
|
const add_tag_modal_tags = $("#addTagModalTags");
|
||||||
@ -780,6 +781,154 @@ function setVolumeDelayed(new_volume_value) {
|
|||||||
}, 500); // delay in milliseconds
|
}, 500); // delay in milliseconds
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------
|
||||||
|
// ------- Upload ------
|
||||||
|
// ---------------------
|
||||||
|
|
||||||
|
const uploadModal = $("#uploadModal");
|
||||||
|
|
||||||
|
const uploadFileInput = document.getElementById("uploadSelectFile");
|
||||||
|
const uploadModalItem = document.getElementsByClassName("uploadItem")[0];
|
||||||
|
const uploadModalList = document.getElementById("uploadModalList");
|
||||||
|
const uploadTargetDir = document.getElementById("uploadTargetDir");
|
||||||
|
const uploadSuccessAlert = document.getElementById("uploadSuccessAlert");
|
||||||
|
const uploadSubmitBtn = document.getElementById("uploadSubmit");
|
||||||
|
const uploadCancelBtn = document.getElementById("uploadCancel");
|
||||||
|
const uploadCloseBtn = document.getElementById("uploadClose");
|
||||||
|
|
||||||
|
const maxFileSize = parseInt(document.getElementById("maxUploadFileSize").value);
|
||||||
|
|
||||||
|
let filesToProceed = [];
|
||||||
|
let filesProgressItem = {};
|
||||||
|
let runningXHR = null;
|
||||||
|
|
||||||
|
uploadSubmitBtn.addEventListener("click", uploadStart)
|
||||||
|
|
||||||
|
function uploadStart(){
|
||||||
|
uploadModalList.textContent = '';
|
||||||
|
uploadSuccessAlert.style.display = 'none';
|
||||||
|
uploadCancelBtn.style.display = 'none';
|
||||||
|
uploadCloseBtn.style.display = 'block';
|
||||||
|
const file_list = uploadFileInput.files;
|
||||||
|
|
||||||
|
if (file_list.length) {
|
||||||
|
for (const file of file_list) {
|
||||||
|
generateUploadProgressItem(file);
|
||||||
|
if (file.size > maxFileSize) {
|
||||||
|
setUploadError(file.name, 'File too large!')
|
||||||
|
continue;
|
||||||
|
} else if (!file.type.includes("audio")) {
|
||||||
|
setUploadError(file.name, 'Unsupported media format!')
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
filesToProceed.push(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadFileInput.value = '';
|
||||||
|
uploadModal.modal("show");
|
||||||
|
uploadNextFile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setProgressBar(bar, progress) {
|
||||||
|
let prog_str = Math.floor(progress*100).toString();
|
||||||
|
bar.setAttribute("aria-valuenow", prog_str);
|
||||||
|
bar.style.width = prog_str + "%";
|
||||||
|
}
|
||||||
|
|
||||||
|
function setUploadError(filename, error){
|
||||||
|
let file_progress_item = filesProgressItem[filename];
|
||||||
|
|
||||||
|
file_progress_item.title.classList.add("text-muted");
|
||||||
|
file_progress_item.error.innerHTML += 'Error: ' + error;
|
||||||
|
setProgressBar(file_progress_item.progress, 1);
|
||||||
|
file_progress_item.progress.classList.add("bg-danger");
|
||||||
|
file_progress_item.progress.classList.remove("progress-bar-animated");
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateUploadProgressItem(file){
|
||||||
|
let item_clone = uploadModalItem.cloneNode(true);
|
||||||
|
let title = item_clone.querySelector(".uploadItemTitle");
|
||||||
|
title.innerHTML = file.name;
|
||||||
|
let error = item_clone.querySelector(".uploadItemError");
|
||||||
|
let progress = item_clone.querySelector(".uploadProgress");
|
||||||
|
item_clone.style.display = "block";
|
||||||
|
|
||||||
|
let item = { title: title, error: error, progress: progress };
|
||||||
|
filesProgressItem[file.name] = item;
|
||||||
|
uploadModalList.appendChild(item_clone);
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
function uploadNextFile(){
|
||||||
|
uploadCancelBtn.style.display = 'block';
|
||||||
|
uploadCloseBtn.style.display = 'none';
|
||||||
|
|
||||||
|
let req = new XMLHttpRequest();
|
||||||
|
let file = filesToProceed.shift();
|
||||||
|
let file_progress_item = filesProgressItem[file.name];
|
||||||
|
|
||||||
|
req.addEventListener("load", function(){
|
||||||
|
if (this.status === 200) {
|
||||||
|
setProgressBar(file_progress_item.progress, 1);
|
||||||
|
file_progress_item.progress.classList.add("bg-success");
|
||||||
|
file_progress_item.progress.classList.remove("progress-bar-animated");
|
||||||
|
} else if (this.status === 400 || this.status === 403) {
|
||||||
|
setUploadError(file.name, 'Illegal request!')
|
||||||
|
} else if (this.status === 500) {
|
||||||
|
setUploadError(file.name, 'Server internal error!')
|
||||||
|
} else {
|
||||||
|
if (this.responseText) {
|
||||||
|
setUploadError(file.name, this.responseText)
|
||||||
|
} else {
|
||||||
|
setUploadError(file.name, 'Unknown error!')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filesToProceed.length) {
|
||||||
|
uploadNextFile();
|
||||||
|
} else {
|
||||||
|
uploadSuccessAlert.style.display = "block";
|
||||||
|
runningXHR = null;
|
||||||
|
|
||||||
|
uploadCancelBtn.style.display = 'none';
|
||||||
|
uploadCloseBtn.style.display = 'block';
|
||||||
|
|
||||||
|
request('post', {action : 'rescan'});
|
||||||
|
updateResults();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
req.addEventListener("progress", function(e){
|
||||||
|
if (e.lengthComputable) {
|
||||||
|
setProgressBar(file_progress_item.progress, e.loaded / e.total);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let form = new FormData();
|
||||||
|
form.append('file', file);
|
||||||
|
form.append('targetdir', uploadTargetDir.value);
|
||||||
|
|
||||||
|
req.open('POST', 'upload');
|
||||||
|
req.send(form);
|
||||||
|
|
||||||
|
file_progress_item.progress.classList.add("progress-bar-striped");
|
||||||
|
file_progress_item.progress.classList.add("progress-bar-animated");
|
||||||
|
|
||||||
|
runningXHR = req;
|
||||||
|
}
|
||||||
|
|
||||||
|
function uploadCancel(){
|
||||||
|
runningXHR.abort()
|
||||||
|
filesToProceed = [];
|
||||||
|
uploadModal.modal('hide');
|
||||||
|
uploadFileInput.value = '';
|
||||||
|
request('post', {action : 'rescan'});
|
||||||
|
updateResults();
|
||||||
|
}
|
||||||
|
|
||||||
themeInit();
|
themeInit();
|
||||||
updateResults();
|
updateResults();
|
||||||
$(document).ready(updatePlaylist);
|
$(document).ready(updatePlaylist);
|
@ -250,17 +250,15 @@
|
|||||||
<button class="library-item-add-next btn btn-info btn-sm btn-space" type="button"
|
<button class="library-item-add-next btn btn-info btn-sm btn-space" type="button"
|
||||||
title="Next to play">
|
title="Next to play">
|
||||||
<svg class="library-btn-svg" style="width: 1rem; fill: currentColor;" viewBox="5 5 17 17">
|
<svg class="library-btn-svg" style="width: 1rem; fill: currentColor;" viewBox="5 5 17 17">
|
||||||
<path d="m5.700245,3.92964l0,14.150376l11.451127,-7.075188l-11.451127,-7.075188z" />
|
<path d="m5.700245,3.92964l0,14.150376l11.451127,-7.075188l-11.451127,-7.075188z"></path>
|
||||||
<path
|
<path d="m20.942859,18.221072l-3.323292,0l0,3.323292l-1.107764,0l0,-3.323292l-3.323292,0l0,-1.107764l3.323292,0l0,-3.323292l1.107764,0l0,3.323292l3.323292,0l0,1.107764z"></path>
|
||||||
d="m20.942859,18.221072l-3.323292,0l0,3.323292l-1.107764,0l0,-3.323292l-3.323292,0l0,-1.107764l3.323292,0l0,-3.323292l1.107764,0l0,3.323292l3.323292,0l0,1.107764z" />
|
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
</button>
|
</button>
|
||||||
<button class="library-item-add-bottom library-btn btn btn-info btn-sm btn-space" type="button"
|
<button class="library-item-add-bottom library-btn btn btn-info btn-sm btn-space" type="button"
|
||||||
title="Add to bottom">
|
title="Add to bottom">
|
||||||
<svg class="library-btn-svg" style="width: 1rem; fill: currentColor;" viewBox="2 2 20 20">
|
<svg class="library-btn-svg" style="width: 1rem; fill: currentColor;" viewBox="2 2 20 20">
|
||||||
<path
|
<path d="M2,16H10V14H2M18,14V10H16V14H12V16H16V20H18V16H22V14M14,6H2V8H14M14,10H2V12H14V10Z"></path>
|
||||||
d="M2,16H10V14H2M18,14V10H16V14H12V16H16V20H18V16H22V14M14,6H2V8H14M14,10H2V12H14V10Z" />
|
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
</button>
|
</button>
|
||||||
@ -357,7 +355,7 @@
|
|||||||
<div class="input-group-prepend">
|
<div class="input-group-prepend">
|
||||||
<span class="input-group-text">Upload To</span>
|
<span class="input-group-text">Upload To</span>
|
||||||
</div>
|
</div>
|
||||||
<input class="form-control" list="targetdirs" id="targetdir" name="targetdir"
|
<input class="form-control" list="targetdirs" id="uploadTargetDir" name="targetdir"
|
||||||
placeholder="uploads" />
|
placeholder="uploads" />
|
||||||
<datalist id="targetdirs">
|
<datalist id="targetdirs">
|
||||||
{% for dir in dirs %}
|
{% for dir in dirs %}
|
||||||
@ -367,7 +365,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<button class="btn btn-primary" type="submit" id="uploadSubmit">Upload!</button>
|
<button class="btn btn-primary" type="button" id="uploadSubmit"><i class="fas fa-upload mr-1"></i>Upload!</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -424,7 +422,7 @@
|
|||||||
<input hidden type="text" name="keywords" value="">
|
<input hidden type="text" name="keywords" value="">
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- Add tags input -->
|
<!-- Add tags modal -->
|
||||||
<div class="modal fade" id="addTagModal" tabindex="-1" role="dialog" aria-hidden="true">
|
<div class="modal fade" id="addTagModal" tabindex="-1" role="dialog" aria-hidden="true">
|
||||||
<div class="modal-dialog" role="document">
|
<div class="modal-dialog" role="document">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
@ -463,6 +461,42 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Upload files modal -->
|
||||||
|
<div class="modal fade" id="uploadModal" tabindex="-1" role="dialog" aria-hidden="true">
|
||||||
|
<div class="modal-dialog" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="uploadTitle"><i class="fas fa-upload mr-1"></i>Uploading files...</h5>
|
||||||
|
</div>
|
||||||
|
<div id="uploadModalBody" class="modal-body">
|
||||||
|
<div id="uploadSuccessAlert" class="alert alert-success" role="alert" style="display: none">
|
||||||
|
<i class="fas fa-check mr-1"></i>
|
||||||
|
Uploaded finished!
|
||||||
|
</div>
|
||||||
|
<div id="uploadModalList" style="margin-left: 5px; margin-bottom: 10px;">
|
||||||
|
<div class="uploadItem" style="display: none; width: 100%; padding-bottom: 8px;">
|
||||||
|
<i class="far fa-file-alt mr-1"></i>
|
||||||
|
<span class="uploadItemTitle mr-3"></span>
|
||||||
|
<span class="uploadItemError text-danger"></span>
|
||||||
|
<div class="progress" style="margin-top: 5px; height: 10px;">
|
||||||
|
<div class="uploadProgress progress-bar" role="progressbar" aria-valuenow="0"
|
||||||
|
aria-valuemin="0" aria-valuemax="100"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" id="uploadClose" class="btn btn-success" data-dismiss="modal">
|
||||||
|
<i class="fas fa-times mr-1"></i> Close</button>
|
||||||
|
<button type="button" id="uploadCancel" class="btn btn-danger">
|
||||||
|
<i class="fas fa-trash-alt mr-1" aria-hidden="true"></i> Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="hidden" id="maxUploadFileSize" value="31457280" />
|
||||||
|
|
||||||
<script src="static/js/jquery-3.5.1.min.js"></script>
|
<script src="static/js/jquery-3.5.1.min.js"></script>
|
||||||
<script src="static/js/popper.min.js"></script>
|
<script src="static/js/popper.min.js"></script>
|
||||||
<script src="static/js/bootstrap.min.js"></script>
|
<script src="static/js/bootstrap.min.js"></script>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user