Merge branch 'dev' into bugfix/file-tree-reload

This commit is contained in:
amcmanu3 2023-01-29 15:13:02 -05:00
commit 040536f320
10 changed files with 907 additions and 383 deletions

View File

@ -2,11 +2,13 @@
## --- [4.0.20] - 2022/TBD
### New features
- Add option to run command before backup. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/536))
- Make Config.json editable from panel. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/532))
### Bug fixes
- Fix local java server imports. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/529))
- Fix Schedule Restore | Add Backup Config Preservation. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/533))
- Rework `/public` Route. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/538))
### Tweaks
- Hide stats DB directory from files tree. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/530))
- Added further login screen customisation settings. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/531))
- Set backup filename to use same time as schedule. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/534))
- Move Schedules to from DB to Queue Datatype. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/535))

View File

@ -78,6 +78,7 @@ class Helpers:
self.websocket_helper = WebSocketHelper(self)
self.translation = Translation(self)
self.update_available = False
self.ignored_names = ["crafty_managed.txt", "db_stats"]
@staticmethod
def auto_installer_fix(ex):
@ -947,8 +948,7 @@ class Helpers:
return data
@staticmethod
def generate_tree(folder, output=""):
def generate_tree(self, folder, output=""):
dir_list = []
unsorted_files = []
file_list = os.listdir(folder)
@ -965,9 +965,12 @@ class Helpers:
rel = os.path.join(folder, raw_filename)
dpath = os.path.join(folder, filename)
if os.path.isdir(rel):
if filename not in self.ignored_names:
output += f"""<li id="{dpath}li" class="tree-item" data-path="{dpath}">
\n<div id="{dpath}" data-path="{dpath}" data-name="{filename}" class="tree-caret tree-ctx-item tree-folder">
<span id="{dpath}span" class="files-tree-title" data-path="{dpath}" data-name="{filename}" onclick="getDirView(event)">
\n<div id="{dpath}" data-path="{dpath}" data-name="{filename}"
class="tree-caret tree-ctx-item tree-folder">
<span id="{dpath}span" class="files-tree-title" data-path="{dpath}"
data-name="{filename}" onclick="getDirView(event)">
<i style="color: var(--info);" class="far fa-folder"></i>
<i style="color: var(--info);" class="far fa-folder-open"></i>
{filename}
@ -975,7 +978,7 @@ class Helpers:
</div><li>
\n"""
else:
if filename != "crafty_managed.txt":
if filename not in self.ignored_names:
output += f"""<li id="{dpath}li"
class="d-block tree-ctx-item tree-file tree-item"
data-path="{dpath}"
@ -984,8 +987,7 @@ class Helpers:
<i class="far fa-file"></i></span>{filename}</li>"""
return output
@staticmethod
def generate_dir(folder, output=""):
def generate_dir(self, folder, output=""):
dir_list = []
unsorted_files = []
@ -1004,16 +1006,19 @@ class Helpers:
dpath = os.path.join(folder, filename)
rel = os.path.join(folder, raw_filename)
if os.path.isdir(rel):
if filename not in self.ignored_names:
output += f"""<li id="{dpath}li" class="tree-item" data-path="{dpath}">
\n<div id="{dpath}" data-path="{dpath}" data-name="{filename}" class="tree-caret tree-ctx-item tree-folder">
<span id="{dpath}span" class="files-tree-title" data-path="{dpath}" data-name="{filename}" onclick="getDirView(event)">
\n<div id="{dpath}" data-path="{dpath}" data-name="{filename}"
class="tree-caret tree-ctx-item tree-folder">
<span id="{dpath}span" class="files-tree-title" data-path="{dpath}"
data-name="{filename}" onclick="getDirView(event)">
<i style="color: var(--info);" class="far fa-folder"></i>
<i style="color: var(--info);" class="far fa-folder-open"></i>
{filename}
</span>
</div><li>"""
else:
if filename != "crafty_managed.txt":
if filename not in self.ignored_names:
output += f"""<li id="{dpath}li"
class="d-block tree-ctx-item tree-file tree-item"
data-path="{dpath}"

View File

@ -100,7 +100,7 @@ class FileHandler(BaseHandler):
self.write(
Helpers.get_os_understandable_path(path)
+ "\n"
+ Helpers.generate_tree(path)
+ self.helper.generate_tree(path)
)
self.finish()
@ -121,7 +121,7 @@ class FileHandler(BaseHandler):
self.write(
Helpers.get_os_understandable_path(path)
+ "\n"
+ Helpers.generate_dir(path)
+ self.helper.generate_dir(path)
)
self.finish()

View File

@ -858,13 +858,51 @@ class PanelHandler(BaseHandler):
page_data["roles"] = self.controller.roles.get_all_roles()
page_data["auth-servers"][user.user_id] = super_auth_servers
page_data["managed_users"] = []
else:
page_data["managed_users"] = self.controller.users.get_managed_users(
exec_user["user_id"]
)
page_data["assigned_roles"] = []
for item in page_data["roles"]:
page_data["assigned_roles"].append(item.role_id)
page_data["managed_roles"] = self.controller.users.get_managed_roles(
exec_user["user_id"]
)
page_data["active_link"] = "panel_config"
template = "panel/panel_config.html"
elif page == "config_json":
if exec_user["superuser"]:
with open(self.helper.settings_file, "r", encoding="utf-8") as f:
data = json.load(f)
page_data["config-json"] = data
page_data["availables_languages"] = []
page_data["all_languages"] = []
for file in sorted(
os.listdir(
os.path.join(self.helper.root_dir, "app", "translations")
)
):
if file.endswith(".json"):
if file.split(".")[0] not in self.helper.get_setting(
"disabled_language_files"
):
page_data["availables_languages"].append(file.split(".")[0])
page_data["all_languages"].append(file.split(".")[0])
page_data["active_link"] = "config_json"
template = "panel/config_json.html"
elif page == "custom_login":
if exec_user["superuser"]:
page_data["backgrounds"] = []
cached_split = self.controller.cached_login.split("/")
if len(cached_split) == 1:
page_data["backgrounds"].append(
self.controller.cached_login
)
page_data["backgrounds"].append(self.controller.cached_login)
else:
page_data["backgrounds"].append(cached_split[1])
if "login_1.jpg" not in page_data["backgrounds"]:
@ -887,19 +925,9 @@ class PanelHandler(BaseHandler):
page_data[
"login_opacity"
] = self.controller.management.get_login_opacity()
else:
page_data["managed_users"] = self.controller.users.get_managed_users(
exec_user["user_id"]
)
page_data["assigned_roles"] = []
for item in page_data["roles"]:
page_data["assigned_roles"].append(item.role_id)
page_data["managed_roles"] = self.controller.users.get_managed_roles(
exec_user["user_id"]
)
template = "panel/panel_config.html"
page_data["active_link"] = "custom_login"
template = "panel/custom_login.html"
elif page == "add_user":
page_data["new_user"] = True
@ -957,7 +985,9 @@ class PanelHandler(BaseHandler):
os.listdir(os.path.join(self.helper.root_dir, "app", "translations"))
):
if file.endswith(".json"):
if file not in self.helper.get_setting("disabled_language_files"):
if file.split(".")[0] not in self.helper.get_setting(
"disabled_language_files"
):
if file != str(page_data["languages"][0] + ".json"):
page_data["languages"].append(file.split(".")[0])
@ -1168,7 +1198,9 @@ class PanelHandler(BaseHandler):
os.listdir(os.path.join(self.helper.root_dir, "app", "translations"))
):
if file.endswith(".json"):
if file not in self.helper.get_setting("disabled_language_files"):
if file.split(".")[0] not in self.helper.get_setting(
"disabled_language_files"
):
if file != str(page_data["languages"][0] + ".json"):
page_data["languages"].append(file.split(".")[0])
@ -1720,6 +1752,38 @@ class PanelHandler(BaseHandler):
self.tasks_manager.reload_schedule_from_db()
self.redirect(f"/panel/server_detail?id={server_id}&subpage=backup")
elif page == "config_json":
try:
data = {}
with open(self.helper.settings_file, "r", encoding="utf-8") as f:
keys = json.load(f).keys()
this_uuid = self.get_argument("uuid")
for key in keys:
arg_data = self.get_argument(key)
if arg_data.startswith(this_uuid):
arg_data = arg_data.split(",")
arg_data.pop(0)
data[key] = arg_data
else:
try:
data[key] = int(arg_data)
except:
if arg_data == "True":
data[key] = True
elif arg_data == "False":
data[key] = False
else:
data[key] = arg_data
with open(self.helper.settings_file, "w", encoding="utf-8") as f:
json.dump(data, f, indent=4)
except Exception as e:
logger.critical(
"Config File Error: Unable to read "
f"{self.helper.settings_file} due to {e}"
)
self.redirect("/panel/config_json")
if page == "new_schedule":
server_id = self.check_server_id()
if not server_id:

View File

@ -0,0 +1,318 @@
{% extends ../base.html %}
{% block meta %}
{% end %}
{% block title %}Crafty Controller - {{ translate('panelConfig', 'pageTitle', data['lang']) }}{% end %}
{% block content %}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-select/1.13.10/css/bootstrap-select.min.css">
<div class="content-wrapper">
<!-- Page Title Header Starts-->
<div class="row page-title-header">
<div class="col-12">
<div class="page-header">
<h4 class="page-title">
{{ translate('panelConfig', 'title', data['lang']) }}
<br />
</h4>
</div>
</div>
</div>
<!-- Page Title Header Ends-->
<div class="row">
<div class="col-md-12 grid-margin">
<div class="card">
<div class="card-body">
{% if data['superuser'] %}
{% include "parts/crafty_config_list.html %}
{% end %}
<!-- Page Title Header Starts-->
<div class="row page-title-header">
<div class="col-12">
<div class="page-header">
<!-- TODO: Translate the following -->
<h4 class="page-title">Config.json</h4>
</div>
</div>
</div>
<!-- Page Title Header Ends-->
<form id="config-form" class="forms-sample" method="post" action="/panel/config_json">
{% raw xsrf_form_html() %}
{% for item in data['config-json'].items() %}
<div class="form-group">
<label class="form" for="{{item[0]}}">{{item[0]}}
<small class="text-muted ml-1">
</small> </label><br />
{% if item[0] == 'language' %}
<select name="{{item[0]}}" class="form-control">
{% for lang in data['availables_languages'] %}
{% if lang == item[1] %}
<option selected>{{lang}}</option>
{% else %}
<option>{{lang}}</option>
{% end %}
{% end %}
</select>
{% elif item[0] == 'disabled_language_files' %}
<div class="input-group">
<button type="button" class="btn btn-outline-default custom-picker" onclick="$('option', $('#lang_select')).each(function(element) {
$(this).removeAttr('selected').prop('selected', false); $('.selectpicker').selectpicker('refresh')
});">Enable all Languages</button>
<select id="lang_select" class="form-control selectpicker show-tick" data-icon-base="fas" data-tick-icon="fa-check" multiple data-style="custom-picker">
{% for lang in data['all_languages'] %}
{% if lang in item[1] %}
<option selected>{{lang}}</option>
{% else %}
<option>{{lang}}</option>
{% end %}
{% end %}
</select>
<textarea id="disabled_lang" name="{{item[0]}}" class="form-control list hidden" rows="{{ len(data['all_languages']) }}" value="{{','.join(item[1])}}" hidden>{{','.join(item[1])}}</textarea>
</div>
{% elif isinstance(item[1], list) %}
<textarea value="{{','.join(item[1])}}" type="text" name="{{item[0]}}" class="form-control list">{{','.join(item[1])}}</textarea>
{% elif isinstance(item[1], bool) %}
{% if item[1] == True %}
<div style="margin-left: 30px;">
<input type="radio" class="form-check-input" name="{{item[0]}}" id="True" value="True" checked>
 <label for="True">True</label><br>
<input type="radio" class="form-check-input" name="{{item[0]}}" id="False" value="False">
 <label for="False">False</label>
</div>
{% else %}
<div style="margin-left: 30px;">
<input type="radio" class="form-check-input" name="{{item[0]}}" id="True" value="True">
 <label for="True">True</label><br>
<input type="radio" class="form-check-input" name="{{item[0]}}" id="False" value="False" checked>
 <label for="False">False</label>
</div>
{% end %}
{% elif isinstance(item[1], int) %}
<input type="number" class="form-control" name="{{item[0]}}" id="{{item[0]}}" value="{{ item[1] }}" step="1" min="0" required>
{% else %}
<input type="text" class="form-control" name="{{item[0]}}" id="{{item[0]}}" value="{{ item[1] }}" step="2" min="0" required>
{% end %}
</div>
{% end %}
<button class="btn btn-success" type="submit">Submit</button>&nbsp;<span id="submit-status"></span>
</form>
</div>
</div>
</div>
</div>
</div>
<style>
.custom-picker {
border: 1px solid var(--outline);
}
.dropdown-menu.inner {
display: inline-block !important;
}
.popover-body {
color: white !important;
;
}
input[type="radio"] {
-ms-transform: scale(1.5);
/* IE 9 */
-webkit-transform: scale(1.5);
/* Chrome, Safari, Opera */
transform: scale(1.5);
}
</style>
<!-- content-wrapper ends -->
{% end %}
{% block js %}
<script>
$("#config-form").submit(function (e) {
let uuid = uuidv4();
var token = getCookie("_xsrf")
e.preventDefault();
$("#submit-status").html('<i class="fa fa-spinner fa-spin"></i>');
/* Convert multiple select to text list */
let selected_Lang = $('#lang_select').val();
$('#disabled_lang').val(selected_Lang);
let class_list = document.getElementsByClassName("list");
let form_json = convertFormToJSON($("#config-form"));
for (let i = 0; i < class_list.length; i++) {
let str = String($(class_list.item(i)).val())
form_json[$(class_list.item(i)).attr("name")] = uuid + "," + str.replace(/\s/g, '');
};
form_json['uuid'] = uuid;
$.ajax({
type: "POST",
headers: { 'X-XSRFToken': token },
dataType: "text",
url: '/panel/config_json',
data: form_json,
success: function (data) {
$("#submit-status").html('<i class="fa fa-check"></i>');
},
});
});
function uuidv4() {
return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c =>
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
);
}
function convertFormToJSON(form) {
const array = $(form).serializeArray(); // Encodes the set of form elements as an array of names and values.
const json = {};
$.each(array, function () {
json[this.name] = this.value || "";
});
return json;
}
$(document).ready(function () {
$('[data-toggle="popover"]').popover();
if ($(window).width() < 1000) {
$('.too_small').popover("show");
$('.too_small2').popover("show");
}
});
$(window).ready(function () {
$('body').click(function () {
$('.too_small').popover("hide");
$('.too_small2').popover("hide");
});
});
$(window).resize(function () {
// This will execute whenever the window is resized
if ($(window).width() < 1000) {
$('.too_small').popover("show");
}
else {
$('.too_small').popover("hide");
} // New width
if ($(window).width() < 1000) {
$('.too_small2').popover("show");
}
else {
$('.too_small2').popover("hide");
} // New width
});
$('#file').change(function () {
console.log("File changed");
if ($('#file').val()) {
$('#upload-button').prop("disabled", false);
console.log("File changed good");
}
});
</script>
<script>
$(document).ready(function () {
console.log('ready for JS!');
$('.selectpicker').selectpicker("refresh");
});
$(".show_button").click(function () {
console.log("showing key");
api_key = $(this).attr("data-id");
bootbox.alert({
backdrop: true,
title: '',
message: api_key,
});
});
$('.clear-comm').click(function () {
var token = getCookie("_xsrf")
$.ajax({
type: "POST",
headers: { 'X-XSRFToken': token },
url: '/ajax/clear_comm',
success: function (data) {
},
});
})
$('.delete-photo').click(function () {
var token = getCookie("_xsrf")
let photo = $('#photo').find(":selected").val();
$.ajax({
type: "POST",
headers: { 'X-XSRFToken': token },
url: '/ajax/delete_photo?photo=' + photo,
success: function (data) {
location.reload();
},
});
})
$('.select-photo').click(function () {
var token = getCookie("_xsrf")
let photo = $('#photo').find(":selected").val();
$.ajax({
type: "POST",
headers: { 'X-XSRFToken': token },
url: '/ajax/select_photo?photo=' + photo,
success: function (data) {
window.location.reload();
},
});
})
var file;
function sendFile() {
file = $("#file")[0].files[0]
document.getElementById("upload_input").innerHTML = '<div class="progress"><div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%">&nbsp;<i class="fa-solid fa-spinner"></i></div></div>'
let xmlHttpRequest = new XMLHttpRequest();
let token = getCookie("_xsrf")
let fileName = file.name
let target = '/upload'
let mimeType = file.type
let size = file.size
let type = 'background'
xmlHttpRequest.open('POST', target, true);
xmlHttpRequest.setRequestHeader('X-Content-Type', mimeType);
xmlHttpRequest.setRequestHeader('X-XSRFToken', token);
xmlHttpRequest.setRequestHeader('X-Content-Length', size);
xmlHttpRequest.setRequestHeader('X-Content-Disposition', 'attachment; filename="' + fileName + '"');
xmlHttpRequest.setRequestHeader('X-Content-Upload-Type', type);
xmlHttpRequest.setRequestHeader('X-FileName', fileName);
xmlHttpRequest.addEventListener('load', (event) => {
if (event.target.responseText == 'success') {
console.log('Upload for file', file.name, 'was successful!')
document.getElementById("upload_input").innerHTML = '<div class="card-header header-sm d-flex justify-content-between align-items-center"><span id="file-uploaded" style="color: gray;">' + fileName + '</span> 🔒</div>';
setTimeout(function () {
window.location.reload();
}, 2000);
}
else {
alert('Upload failed with response: ' + event.target.responseText);
doUpload = false;
}
}, false);
xmlHttpRequest.addEventListener('error', (e) => {
console.error('Error while uploading file', file.name + '.', 'Event:', e)
}, false);
xmlHttpRequest.send(file);
}
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-select/1.13.10/js/bootstrap-select.min.js">
</script>
{% end %}

View File

@ -0,0 +1,391 @@
{% extends ../base.html %}
{% block meta %}
{% end %}
{% block title %}Crafty Controller - {{ translate('customLogin', 'pageTitle', data['lang']) }}{% end %}
{% block content %}
<div class="content-wrapper">
<!-- Page Title Header Starts-->
<div class="row page-title-header">
<div class="col-12">
<div class="page-header">
<h4 class="page-title">
{{ translate('panelConfig', 'title', data['lang']) }}
<br />
</h4>
</div>
</div>
</div>
<!-- Page Title Header Ends-->
<div class="row">
<div class="col-md-12 grid-margin">
<div class="card">
<div class="card-body">
{% if data['superuser'] %}
{% include "parts/crafty_config_list.html %}
{% end %}
<!-- Page Title Header Starts-->
<div class="row page-title-header">
<div class="col-12">
<div class="page-header">
<!-- TODO: Translate the following -->
<h4 class="page-title">{{ translate('customLogin', 'customLoginPage', data['lang']) }}</h4>
</div>
</div>
</div>
<!-- Page Title Header Ends-->
<div class="row">
<div class="col-md-12 col-lg-12 grid-margin stretch-card">
<div class="card">
<div class="card-body">
<div class="row">
<div class="col-12">
<h4>{{ translate('customLogin', 'loginImage', data['lang']) }}</h4>
<hr>
<form class="form-row" name="zip" method="post" class="server-wizard" onSubmit="wait_msg(true)">
{% raw xsrf_form_html() %}
<input type="hidden" value="import_zip" name="create_type">
<div class="col form-group">
<span id="upload_input"><input type="file" class="form-control-file" id="file" name="file"
multiple="false" required></span>
</div>
<div class="col form-group">
<button type="button" class="btn btn-info" id="upload-button" onclick="sendFile()"
disabled>UPLOAD</button>
</div>
</form>
<hr>
<hr />
</div>
<div class="col-12">
<div>
<h6>{{ translate('customLogin', 'preview', data['lang']) }}:</h6>
<form id="photo_form">
<div class="form-group row">
<label for="photo" class="col-sm-6 col-form-label">Selected Background Image</label>
<div class="col-sm-6">
<select class="form-select form-control form-control-lg select-css form-control-plaintext"
id="photo" name="photo" form="photo_form" onchange="updateBackgroundPreview()">
{% for image in data["backgrounds"] %}
<option value="{{image}}">{{image}}</option>
{% end %}
</select>
</div>
</div>
<div id="photo_loading" class="form-group" hidden>
<div class="progress">
<div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar"
aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%">&nbsp;<i
class="fa-solid fa-spinner"></i></div>
</div>
</div>
<div class="form-group row">
<label class="col-sm-3" for="formControlRange">{{ translate('customLogin', 'loginOpacity',
data['lang']) }}</label>
<label class="col-sm-1" id="opacityValue">{{ data['login_opacity'] }}%</label>
<div class="range col-sm-8">
<input type="range" class="form-control-range" id="modal_opacity" name="modal_opacity"
onchange="previewOpacity()" min="0" max="100" value="{{ data['login_opacity'] }}">
</div>
</div>
<div id="login_preview" style="position: relative;">
<img id="bg-preview" src="../../static/assets/images/auth/{{ data['background'] }}"
class="img-fluid" alt="Responsive image">
<div id="login-form-preview">
<div id="login-form-background" class="auto-form-wrapper login-modal">
<div class="text-center auto-form-logo">
<img src="/static/assets/images/logo_long.svg">
</div>
<style>
#login-form-preview {
display: flex;
position: absolute;
overflow: hidden;
top: 50%;
left: 50%;
transform: translateX(-50%) translateY(-50%);
max-width: 90%;
max-height: 90%;
}
.auto-form-wrapper {
background: rgb(34, 36, 55, 1);
padding: 2rem 2rem 0.5rem;
border-radius: 4px;
-webkit-box-shadow: 0 -25px 37.7px 11.3px rgb(8 143 220 / 7%);
box-shadow: 0 -25px 37.7px 11.3px rgb(8 143 220 / 7%);
color: #fff;
}
/*.auto-form-logo {
background: #222437;
padding: 0rem;
margin: 0.5rem 0rem;
border-radius: 0.2rem;
color: #fff;
}*/
.login-modal {
border-radius: 0.4rem !important;
box-shadow: 0 8px 12px 0 hsla(0, 0%, 0%, 0.2) !important;
}
.login-text-input {
border: none !important;
background-color: hsl(234, 30%, 45%);
color: var(--white) !important;
}
.login-text-input:hover,
.login-text-input:focus {
background-color: hsl(234, 30%, 39%) !important;
}
.login-input {
border-radius: 0.4rem !important;
box-shadow: 0 8px 12px 0 hsla(0, 0%, 0%, 0.2);
transition: all 0.3s ease-in-out;
}
.login-input:hover,
.login-input:focus {
box-shadow: 0 12px 16px 0 hsla(0, 0%, 0%, 0.4);
}
</style>
<div id="login_form_data">
<input type="hidden" name="_xsrf"
value="2|1d603267|809fb6bd82f677d440e484dde7c3a310|1671726040" disabled>
<div class="form-group">
<label class="label">Username</label>
<div class="input-group">
<input type="text" class="form-control login-text-input login-input"
placeholder="Username" name="username" id="username" required="true" disabled>
</div>
</div>
<div class="form-group">
<label class="label">Password</label>
<div class="input-group">
<input type="password" class="form-control login-text-input login-input"
placeholder="Password" name="password" id="password" required="true" disabled>
</div>
</div>
<div class="form-group">
<button class="login-input btn btn-primary submit-btn btn-block" disabled>Log
In</button>
</div>
<fieldset style="color: red; text-align: center;">
<span></span>
</fieldset>
<div class="form-group d-flex justify-content-between">
<div class="form-check form-check-flat mt-0">
&nbsp;
</div>
<a href="#" class="text-small forgot-password" disabled>Forgot Password</a>
</div>
<div class="text-block text-center my-3">
<span class="text-small font-weight-semibold"><a
href="https://craftycontrol.com/">Crafty Control
4.0.20</a> </span>
</div>
</div>
</div>
</div>
</div>
<div class="form-group">
<button class="btn btn-outline-success select-photo" type="button">{{
translate('customLogin',
'apply', data['lang']) }}</button>
<button class="btn btn-outline-danger delete-photo" type="button">{{
translate('customLogin',
'delete', data['lang']) }}</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
.popover-body {
color: white !important;
;
}
</style>
<!-- content-wrapper ends -->
{% end %}
{% block js %}
<script>
$(document).ready(function () {
$('[data-toggle="popover"]').popover();
if ($(window).width() < 1000) {
$('.too_small').popover("show");
$('.too_small2').popover("show");
}
});
$(window).ready(function () {
$('body').click(function () {
$('.too_small').popover("hide");
$('.too_small2').popover("hide");
});
});
$(window).resize(function () {
// This will execute whenever the window is resized
if ($(window).width() < 1000) {
$('.too_small').popover("show");
}
else {
$('.too_small').popover("hide");
} // New width
if ($(window).width() < 1000) {
$('.too_small2').popover("show");
}
else {
$('.too_small2').popover("hide");
} // New width
});
$('#file').change(function () {
console.log("File changed");
if ($('#file').val()) {
$('#upload-button').prop("disabled", false);
console.log("File changed good");
}
});
</script>
<script>
$(document).ready(function () {
console.log('ready for JS!')
});
$(".show_button").click(function () {
console.log("showing key");
api_key = $(this).attr("data-id");
bootbox.alert({
backdrop: true,
title: '',
message: api_key,
});
});
$('.delete-photo').click(function () {
var token = getCookie("_xsrf")
let photo = $('#photo').find(":selected").val();
$.ajax({
type: "POST",
headers: { 'X-XSRFToken': token },
url: '/ajax/delete_photo?photo=' + encodeURIComponent(photo),
success: function (data) {
location.reload();
},
});
})
$('.select-photo').click(function () {
var token = getCookie("_xsrf")
let photo = $('#photo').find(":selected").val();
let opacity = $('#modal_opacity').val();
let enc_photo = encodeURIComponent(photo);
$.ajax({
type: "POST",
headers: { 'X-XSRFToken': token },
url: '/ajax/select_photo?photo=' + enc_photo + '&opacity=' + opacity,
success: function (data) {
window.location.reload();
},
});
})
$(document).ready(function () {
let opacity = parseInt($("#modal_opacity").val());
document.getElementById('login-form-background').style.background = 'rgb(34, 36, 55, ' + (opacity / 100) + ')';
});
function previewOpacity() {
let opacity = parseInt($("#modal_opacity").val())
console.debug("Selected Opacity = " + opacity + "%");
document.getElementById('opacityValue').innerHTML = (opacity) + "%";
document.getElementById('login-form-background').style.background = 'rgb(34, 36, 55, ' + (opacity / 100) + ')';
}
function updateBackgroundSelect() {
$("#photo").val($("#try_photo").val()).change();
}
function updateBackgroundPreview() {
var img = document.getElementById('bg-preview');
if ($("#photo").val() == "login_1.jpg") {
var src_path = "../../static/assets/images/auth/".concat($("#photo").val());
}
else {
var src_path = "../../static/assets/images/auth/custom/".concat($("#photo").val());
}
img.src = src_path;
}
var file;
function sendFile() {
file = $("#file")[0].files[0]
document.getElementById("upload_input").innerHTML = '<div class="progress"><div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%">&nbsp;<i class="fa-solid fa-spinner"></i></div></div>';
let xmlHttpRequest = new XMLHttpRequest();
let token = getCookie("_xsrf")
let fileName = file.name
let target = '/upload'
let mimeType = file.type
let size = file.size
let type = 'background'
xmlHttpRequest.open('POST', target, true);
xmlHttpRequest.setRequestHeader('X-Content-Type', mimeType);
xmlHttpRequest.setRequestHeader('X-XSRFToken', token);
xmlHttpRequest.setRequestHeader('X-Content-Length', size);
xmlHttpRequest.setRequestHeader('X-Content-Disposition', 'attachment; filename="' + fileName + '"');
xmlHttpRequest.setRequestHeader('X-Content-Upload-Type', type);
xmlHttpRequest.setRequestHeader('X-FileName', fileName);
xmlHttpRequest.addEventListener('load', (event) => {
if (event.target.responseText == 'success') {
console.log('Upload for file', file.name, 'was successful!')
document.getElementById("upload_input").innerHTML = '<div class="card-header header-sm d-flex justify-content-between align-items-center"><span id="file-uploaded" style="color: gray;">' + fileName + '</span> 🔒</div>';
setTimeout(function () {
window.location.reload();
}, 2000);
}
else {
alert('Upload failed with response: ' + event.target.responseText);
doUpload = false;
}
}, false);
xmlHttpRequest.addEventListener('error', (e) => {
console.error('Error while uploading file', file.name + '.', 'Event:', e)
}, false);
xmlHttpRequest.send(file);
}
</script>
{% end %}

View File

@ -8,6 +8,28 @@
{% block content %}
<div class="content-wrapper">
<!-- Page Title Header Starts-->
<div class="row page-title-header">
<div class="col-12">
<div class="page-header">
<h4 class="page-title">
{{ translate('panelConfig', 'title', data['lang']) }}
<br />
</h4>
</div>
</div>
</div>
<!-- Page Title Header Ends-->
<div class="row">
<div class="col-md-12 grid-margin">
<div class="card">
<div class="card-body">
{% if data['superuser'] %}
{% include "parts/crafty_config_list.html %}
{% end %}
<!-- Page Title Header Starts-->
<div class="row page-title-header">
@ -21,11 +43,6 @@
</div>
<!-- Page Title Header Ends-->
<div class="row">
<div class="col-md-12 grid-margin">
<div class="card">
<div class="card-body">
<div class="row">
<div class="col-md-12 col-lg-12 grid-margin stretch-card">
<div class="card">
@ -33,9 +50,7 @@
<h4 class="card-title"><i class="fas fa-users"></i> {{ translate('panelConfig', 'users', data['lang'])
}}</h4>
{% if data['user_data']['hints'] %}
<span class="too_small" title="{{ translate('dashboard', 'cannotSee', data['lang']) }}" ,
data-content="{{ translate('dashboard', 'cannotSeeOnMobile2', data['lang']) }}" ,
data-placement="top"></span>
<span class="too_small" title="{{ translate('dashboard', 'cannotSee', data['lang']) }}" , data-content="{{ translate('dashboard', 'cannotSeeOnMobile2', data['lang']) }}" , data-placement="top"></span>
{% end %}
<!-- TODO: Translate the following -->
<div><a class="nav-link" href="/panel/add_user"><i class="fas fa-plus-circle"></i> &nbsp; {{
@ -133,9 +148,7 @@
<h4 class="card-title"><i class="fas fa-user-tag"></i> {{ translate('panelConfig', 'roles',
data['lang']) }}</h4>
{% if data['user_data']['hints'] %}
<span class="too_small2" title="{{ translate('dashboard', 'cannotSee', data['lang']) }}" ,
data-content="{{ translate('dashboard', 'cannotSeeOnMobile2', data['lang']) }}" ,
data-placement="top"></span>
<span class="too_small2" title="{{ translate('dashboard', 'cannotSee', data['lang']) }}" , data-content="{{ translate('dashboard', 'cannotSeeOnMobile2', data['lang']) }}" , data-placement="top"></span>
{% end %}
<div><a class="nav-link" href="/panel/add_role"><i class="fas fa-plus-circle"></i> &nbsp; {{
translate('panelConfig', 'newRole', data['lang']) }}</a></div>
@ -224,185 +237,6 @@
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 col-lg-12 grid-margin stretch-card">
<div class="card">
<div class="card-header header-sm d-flex justify-content-between align-items-center">
<h4>{{ translate('panelConfig', 'customLoginPage', data['lang']) }}</h4>
</div>
<div class="card-body">
<div class="row">
<div class="col-lg-6">
<h4>{{ translate('panelConfig', 'loginImage', data['lang']) }}</h4>
<hr>
<form class="form-row" name="zip" method="post" class="server-wizard" onSubmit="wait_msg(true)">
{% raw xsrf_form_html() %}
<input type="hidden" value="import_zip" name="create_type">
<div class="col form-group">
<span id="upload_input"><input type="file" class="form-control-file" id="file" name="file"
multiple="false" required></span>
</div>
<div class="col form-group">
<button type="button" class="btn btn-info" id="upload-button" onclick="sendFile()"
disabled>UPLOAD</button>
</div>
</form>
<hr>
<hr />
</div>
<div class="col-lg-6">
<div>
<h6>{{ translate('panelConfig', 'preview', data['lang']) }}:</h6>
<form id="photo_form">
<div class="form-group row">
<label for="photo" class="col-sm-6 col-form-label">Selected Background Image</label>
<div class="col-sm-6">
<select class="form-select form-control form-control-lg select-css form-control-plaintext"
id="photo" name="photo" form="photo_form" onchange="updateBackgroundPreview()">
{% for image in data["backgrounds"] %}
<option value="{{image}}">{{image}}</option>
{% end %}
</select>
</div>
</div>
<div id="photo_loading" class="form-group" hidden>
<div class="progress">
<div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar"
aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%">&nbsp;<i
class="fa-solid fa-spinner"></i></div>
</div>
</div>
<div class="form-group row">
<label class="col-sm-3" for="formControlRange">{{ translate('panelConfig', 'loginOpacity',
data['lang']) }}</label>
<label class="col-sm-1" id="opacityValue">{{ data['login_opacity'] }}%</label>
<div class="range col-sm-8">
<input type="range" class="form-control-range" id="modal_opacity" name="modal_opacity"
onchange="previewOpacity()" min="0" max="100" value="{{ data['login_opacity'] }}">
</div>
</div>
<div id="login_preview" style="position: relative;">
<img id="bg-preview" src="../../static/assets/images/auth/{{ data['background'] }}"
class="img-fluid" alt="Responsive image">
<div id="login-form-preview">
<div id="login-form-background" class="auto-form-wrapper login-modal">
<div class="text-center auto-form-logo">
<img src="/static/assets/images/logo_long.svg">
</div>
<style>
#login-form-preview {
display: flex;
position: absolute;
overflow: hidden;
top: 50%;
left: 50%;
transform: translateX(-50%) translateY(-50%);
max-width: 90%;
max-height: 90%;
}
.auto-form-wrapper {
background: rgb(34, 36, 55, 1);
padding: 2rem 2rem 0.5rem;
border-radius: 4px;
-webkit-box-shadow: 0 -25px 37.7px 11.3px rgb(8 143 220 / 7%);
box-shadow: 0 -25px 37.7px 11.3px rgb(8 143 220 / 7%);
color: #fff;
}
/*.auto-form-logo {
background: #222437;
padding: 0rem;
margin: 0.5rem 0rem;
border-radius: 0.2rem;
color: #fff;
}*/
.login-modal {
border-radius: 0.4rem !important;
box-shadow: 0 8px 12px 0 hsla(0, 0%, 0%, 0.2) !important;
}
.login-text-input {
border: none !important;
background-color: hsl(234, 30%, 45%);
color: var(--white) !important;
}
.login-text-input:hover,
.login-text-input:focus {
background-color: hsl(234, 30%, 39%) !important;
}
.login-input {
border-radius: 0.4rem !important;
box-shadow: 0 8px 12px 0 hsla(0, 0%, 0%, 0.2);
transition: all 0.3s ease-in-out;
}
.login-input:hover,
.login-input:focus {
box-shadow: 0 12px 16px 0 hsla(0, 0%, 0%, 0.4);
}
</style>
<div id="login_form_data">
<input type="hidden" name="_xsrf"
value="2|1d603267|809fb6bd82f677d440e484dde7c3a310|1671726040" disabled>
<div class="form-group">
<label class="label">Username</label>
<div class="input-group">
<input type="text" class="form-control login-text-input login-input"
placeholder="Username" name="username" id="username" required="true" disabled>
</div>
</div>
<div class="form-group">
<label class="label">Password</label>
<div class="input-group">
<input type="password" class="form-control login-text-input login-input"
placeholder="Password" name="password" id="password" required="true" disabled>
</div>
</div>
<div class="form-group">
<button class="login-input btn btn-primary submit-btn btn-block" disabled>Log
In</button>
</div>
<fieldset style="color: red; text-align: center;">
<span></span>
</fieldset>
<div class="form-group d-flex justify-content-between">
<div class="form-check form-check-flat mt-0">
&nbsp;
</div>
<a href="#" class="text-small forgot-password" disabled>Forgot Password</a>
</div>
<div class="text-block text-center my-3">
<span class="text-small font-weight-semibold"><a
href="https://craftycontrol.com/">Crafty Control
4.0.20</a> </span>
</div>
</div>
</div>
</div>
</div>
<div class="form-group">
<button class="btn btn-outline-success select-photo" type="button">{{
translate('panelConfig',
'apply', data['lang']) }}</button>
<button class="btn btn-outline-danger delete-photo" type="button">{{
translate('panelConfig',
'delete', data['lang']) }}</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% end %}
</div>
</div>
@ -461,116 +295,5 @@
}
});
</script>
<script>
$(document).ready(function () {
console.log('ready for JS!')
});
$(".show_button").click(function () {
console.log("showing key");
api_key = $(this).attr("data-id");
bootbox.alert({
backdrop: true,
title: '',
message: api_key,
});
});
$('.delete-photo').click(function () {
var token = getCookie("_xsrf")
let photo = $('#photo').find(":selected").val();
$.ajax({
type: "POST",
headers: { 'X-XSRFToken': token },
url: '/ajax/delete_photo?photo=' + encodeURIComponent(photo),
success: function (data) {
location.reload();
},
});
})
$('.select-photo').click(function () {
var token = getCookie("_xsrf")
let photo = $('#photo').find(":selected").val();
let opacity = $('#modal_opacity').val();
let enc_photo = encodeURIComponent(photo);
$.ajax({
type: "POST",
headers: { 'X-XSRFToken': token },
url: '/ajax/select_photo?photo=' + enc_photo + '&opacity=' + opacity,
success: function (data) {
window.location.reload();
},
});
})
$(document).ready(function () {
let opacity = parseInt($("#modal_opacity").val());
document.getElementById('login-form-background').style.background = 'rgb(34, 36, 55, ' + (opacity / 100) + ')';
});
function previewOpacity() {
let opacity = parseInt($("#modal_opacity").val())
console.debug("Selected Opacity = " + opacity + "%");
document.getElementById('opacityValue').innerHTML = (opacity) + "%";
document.getElementById('login-form-background').style.background = 'rgb(34, 36, 55, ' + (opacity / 100) + ')';
}
function updateBackgroundSelect() {
$("#photo").val($("#try_photo").val()).change();
}
function updateBackgroundPreview() {
var img = document.getElementById('bg-preview');
if ($("#photo").val() == "login_1.jpg") {
var src_path = "../../static/assets/images/auth/".concat($("#photo").val());
}
else {
var src_path = "../../static/assets/images/auth/custom/".concat($("#photo").val());
}
img.src = src_path;
}
var file;
function sendFile() {
file = $("#file")[0].files[0]
document.getElementById("upload_input").innerHTML = '<div class="progress"><div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%">&nbsp;<i class="fa-solid fa-spinner"></i></div></div>';
let xmlHttpRequest = new XMLHttpRequest();
let token = getCookie("_xsrf")
let fileName = file.name
let target = '/upload'
let mimeType = file.type
let size = file.size
let type = 'background'
xmlHttpRequest.open('POST', target, true);
xmlHttpRequest.setRequestHeader('X-Content-Type', mimeType);
xmlHttpRequest.setRequestHeader('X-XSRFToken', token);
xmlHttpRequest.setRequestHeader('X-Content-Length', size);
xmlHttpRequest.setRequestHeader('X-Content-Disposition', 'attachment; filename="' + fileName + '"');
xmlHttpRequest.setRequestHeader('X-Content-Upload-Type', type);
xmlHttpRequest.setRequestHeader('X-FileName', fileName);
xmlHttpRequest.addEventListener('load', (event) => {
if (event.target.responseText == 'success') {
console.log('Upload for file', file.name, 'was successful!')
document.getElementById("upload_input").innerHTML = '<div class="card-header header-sm d-flex justify-content-between align-items-center"><span id="file-uploaded" style="color: gray;">' + fileName + '</span> 🔒</div>';
setTimeout(function () {
window.location.reload();
}, 2000);
}
else {
alert('Upload failed with response: ' + event.target.responseText);
doUpload = false;
}
}, false);
xmlHttpRequest.addEventListener('error', (e) => {
console.error('Error while uploading file', file.name + '.', 'Event:', e)
}, false);
xmlHttpRequest.send(file);
}
</script>
{% end %}

View File

@ -0,0 +1,14 @@
<ul class="nav nav-tabs col-md-12 tab-simple-styled" role="tablist" style="margin-top: 0;">
<li class="nav-item term-nav-item">
<a class="nav-link {% if data['active_link'] == 'panel_config' %}active{% end %}" href="/panel/panel_config" role="tab" aria-selected="false">
<i class="fas fa-file-signature"></i>Panel Config</a>
</li>
<li class="nav-item term-nav-item">
<a class="nav-link {% if data['active_link'] == 'config_json' %}active{% end %}" href="/panel/config_json" role="tab" aria-selected="false">
<i class="fas fa-file-signature"></i>Config.json</a>
</li>
<li class="nav-item term-nav-item">
<a class="nav-link {% if data['active_link'] == 'custom_login' %}active{% end %}" href="/panel/custom_login" role="tab" aria-selected="false">
<i class="fas fa-file-signature"></i>Custom Login</a>
</li>
</ul>

View File

@ -213,6 +213,8 @@
"assignedRoles": "Assigned Roles",
"cancel": "Cancel",
"clearComms": "Clear Un-executed Commands",
"select": "Select",
"apply": "Apply",
"delete": "Delete",
"edit": "Edit",
"enabled": "Enabled",
@ -228,6 +230,9 @@
"superConfirmTitle": "Enable superuser? Are you sure?",
"user": "User",
"users": "Users",
"title": "Crafty Configuration"
},
"customLogin": {
"customLoginPage": "Customise the Login Page",
"loginImage": "Upload a background image for the login screen.",
"backgroundUpload": "Background Upload",
@ -235,8 +240,10 @@
"loginOpacity": "Select the Login Window Opacity",
"select": "Select",
"apply": "Apply",
"delete": "Delete",
"selectImage": "Select an image",
"preview": "Preview"
"preview": "Preview",
"pageTitle": "Custom Login Page"
},
"rolesConfig": {
"config": "Role Config",