dragndrop + bulk upload + bulk delete + shit/cmd/ctrl multi file selection + player default view if no fallback playlist

This commit is contained in:
jr-k 2024-07-17 16:13:43 +02:00
parent fab5f8f49e
commit 5065c9fe54
26 changed files with 2185 additions and 174 deletions

File diff suppressed because one or more lines are too long

90
data/www/js/dragdrop.js vendored Normal file
View File

@ -0,0 +1,90 @@
jQuery(function ($) {
const main = function () {
fileUpload();
};
const fileUpload = function () {
$('.btn-super-upload').each(function () {
const $button = $(this);
const $input = $(this).find('input[type=file]');
$input.fileupload({
url: $(this).attr('data-route'),
dropZone: $('body'),
formData: {},
dataType: 'json',
add: function (e, data) {
const $alert = $('.alert-danger');
const $bar = $button.find('.progress-bar');
$bar.css('width', '0%');
$button.addClass('uploading').removeClass('btn-info btn-super-upload').addClass('btn-naked btn-super-upload-busy');
$alert.addClass('hidden').text('');
data.submit();
},
progressall: function (e, data) {
const progress = parseInt(data.loaded / data.total * 100, 10);
const $bar = $button.find('.progress-bar');
const $percent = $button.find('.percent');
$bar.css('width', progress + '%');
$percent.text(progress + '%');
},
always: function (e, data) {
const response = data._response.jqXHR;
$button.removeClass('uploading').removeClass('btn-naked btn-super-upload-busy').addClass('btn-info btn-super-upload');
if (response.status != 200) {
const $alert = $('.alert-danger').removeClass('hidden');
if (response.status == 413) {
$alert.text(l.js_common_http_error_413);
} else {
$alert.text(l.js_common_http_error_occured.replace('%code%', response.status));
}
} else {
document.location.reload();
}
}
});
});
};
main();
$(document).on('click', '.btn-super-upload', function (e) {
$(this).find('input[type=file]')[0].click();
});
$(document).on('dragenter', 'body', function () {
$(this).addClass('dragenter');
return false;
});
$(document).on('dragover', 'body', function (e) {
e.preventDefault();
e.stopPropagation();
$(this).addClass('dragover');
return false;
});
$(document).on('dragleave', 'body', function (e) {
e.preventDefault();
e.stopPropagation();
$(this).removeClass('dragenter dragover');
return false;
});
$(document).on('drop', 'body', function (e) {
e.preventDefault();
e.stopPropagation();
$(this).removeClass('dragenter dragover');
const $dz = $('.dropzone:visible');
if (isset($dz.attr('data-handle-drop') && $dz.attr('data-handle-drop') === '1')) {
const $inputTarget = $("#" + $dz.attr('data-related-input'));
const droppedFiles = e.originalEvent.dataTransfer.files;
$inputTarget.prop("files", droppedFiles).trigger('change');
}
return false;
});
});

View File

@ -2,6 +2,7 @@ let onPickedElement = function (element) {
}; };
jQuery(function ($) { jQuery(function ($) {
let lastClicked = null;
const explrSidebarOpenFromFolder = function (folderId) { const explrSidebarOpenFromFolder = function (folderId) {
const $leaf = $('.li-explr-folder-' + folderId); const $leaf = $('.li-explr-folder-' + folderId);
@ -70,29 +71,59 @@ jQuery(function ($) {
initExplr(); initExplr();
}; };
const selectEpxlrLink = function ($link) { const updateBodyClasses = function () {
$('a.explr-link').removeClass('highlight-clicked'); const $selectedLinks = $('a.explr-link.highlight-clicked');
$('a.explr-link').parent().removeClass('highlight-clicked'); const isMultiSelect = $selectedLinks.length > 1;
$('body').removeClass('explr-selection explr-selection-actionable explr-selection-entity explr-selection-folder'); const isSingleSelect = $selectedLinks.length === 1;
const $link = $selectedLinks.last();
if ($link.hasClass('explr-item-selectable')) { $('body')
$link.addClass('highlight-clicked'); .toggleClass('explr-selection', isSingleSelect)
$link.parent().addClass('highlight-clicked'); .toggleClass('explr-selection-actionable', isSingleSelect && $link.hasClass('explr-item-actionable'))
$('body').addClass('explr-selection'); .toggleClass('explr-selection-entity', isSingleSelect && $link.hasClass('explr-item-entity'))
if ($link.hasClass('explr-item-actionable')) { .toggleClass('explr-selection-folder', isSingleSelect && $link.hasClass('explr-item-folder'))
$('body').addClass('explr-selection-actionable'); .toggleClass('explr-multiselection', isMultiSelect)
} .toggleClass('explr-multiselection-actionable', isMultiSelect && $selectedLinks.hasClass('explr-item-actionable'))
if ($link.hasClass('explr-item-entity')) { .toggleClass('explr-multiselection-entity', isMultiSelect && $selectedLinks.hasClass('explr-item-entity'))
$('body').addClass('explr-selection-entity'); .toggleClass('explr-multiselection-folder', isMultiSelect && $selectedLinks.hasClass('explr-item-folder'));
}
if ($link.hasClass('explr-item-folder')) {
$('body').addClass('explr-selection-folder');
}
}
}; };
const selectEpxlrLink = function ($link) {
$link.addClass('highlight-clicked');
$link.parent().addClass('highlight-clicked');
updateBodyClasses();
};
const clearSelection = function () {
$('a.explr-link').removeClass('highlight-clicked');
$('a.explr-link').parent().removeClass('highlight-clicked');
$('body').removeClass('explr-selection explr-selection-actionable explr-selection-entity explr-selection-folder explr-multiselection explr-multiselection-actionable explr-multiselection-entity explr-multiselection-folder');
};
const handleShiftClick = function ($link) {
const $links = $('li > a.explr-link');
const start = $links.index(lastClicked);
const end = $links.index($link);
const [from, to] = start < end ? [start, end] : [end, start];
$links.slice(from, to + 1).each(function () {
selectEpxlrLink($(this));
});
updateBodyClasses();
};
const handleCmdCtrlClick = function ($link) {
if ($link.hasClass('highlight-clicked')) {
$link.removeClass('highlight-clicked');
$link.parent().removeClass('highlight-clicked');
} else {
selectEpxlrLink($link);
}
updateBodyClasses();
};
const getExplrSelection = function () { const getExplrSelection = function () {
return $('.explr-dirview .highlight-clicked'); return $('.explr-dirview li.highlight-clicked');
}; };
const renameExplrItem = function ($item) { const renameExplrItem = function ($item) {
@ -116,9 +147,9 @@ jQuery(function ($) {
let route; let route;
if (is_folder) { if (is_folder) {
route = $(this).attr('data-folder-route') + '?id=' + $item.attr('data-id'); route = $(this).attr('data-folder-route') + '&id=' + $item.attr('data-id');
} else { } else {
route = $(this).attr('data-entity-route') + '?id=' + $item.attr('data-id'); route = $(this).attr('data-entity-route') + '&id=' + $item.attr('data-id');
} }
if (confirm(l.js_common_are_you_sure)) { if (confirm(l.js_common_are_you_sure)) {
@ -126,18 +157,44 @@ jQuery(function ($) {
} }
}); });
$(document).on('click', '.explr-items-delete', function () {
const $items = getExplrSelection();
const folder_ids = [], entity_ids = [];
$items.each(function() {
const is_folder = $(this).attr('data-folder') === '1';
const id = $(this).attr('data-id');
if (is_folder) {
folder_ids.push(id);
} else {
entity_ids.push(id);
}
});
if (confirm(l.js_common_are_you_sure)) {
document.location.href = $(this).attr('data-route')
+ '&folder_ids=' + folder_ids.join(',')
+ '&entity_ids=' + entity_ids.join(',')
;
}
});
$(document).keyup(function (e) { $(document).keyup(function (e) {
const $selectedLink = $('.explr-item-selectable.highlight-clicked'); const $selectedLink = $('.explr-item-selectable.highlight-clicked');
const $selectedLi = $selectedLink.parents('li:eq(0)'); const $selectedLi = $selectedLink.parents('li:eq(0)');
if (e.key === "Escape") {
$('.dirview .new-folder').addClass('hidden');
$('.dirview .renaming').removeClass('renaming');
clearSelection();
}
if ($('.renaming input:focus').length > 0) { if ($('.renaming input:focus').length > 0) {
return; return;
} }
if (e.key === "Escape") { if (e.code === "Space") {
$('.dirview .new-folder').addClass('hidden');
$('.dirview .renaming').removeClass('renaming');
} else if (e.code === "Space") {
renameExplrItem($selectedLi); renameExplrItem($selectedLi);
} else if ($selectedLink.length) { } else if ($selectedLink.length) {
const $prevLi = $selectedLi.prev('li:visible'); const $prevLi = $selectedLi.prev('li:visible');
@ -158,6 +215,9 @@ jQuery(function ($) {
if ($('.explr-item-delete:visible').length) { if ($('.explr-item-delete:visible').length) {
$('.explr-item-delete:visible').click(); $('.explr-item-delete:visible').click();
} }
if ($('.explr-items-delete:visible').length) {
$('.explr-items-delete:visible').click();
}
} }
} else if (e.key.indexOf('Arrow') === 0) { } else if (e.key.indexOf('Arrow') === 0) {
selectEpxlrLink($('.explr-dirview li:visible:eq(0)').find('.explr-link')); selectEpxlrLink($('.explr-dirview li:visible:eq(0)').find('.explr-link'));
@ -167,7 +227,17 @@ jQuery(function ($) {
// Explorer item selection // Explorer item selection
$(document).on('click', 'a.explr-link', function (event) { $(document).on('click', 'a.explr-link', function (event) {
event.preventDefault(); event.preventDefault();
selectEpxlrLink($(this)); const $link = $(this);
if (event.shiftKey && lastClicked) {
handleShiftClick($link);
} else if (event.metaKey || event.ctrlKey) {
handleCmdCtrlClick($link);
} else {
clearSelection();
selectEpxlrLink($link);
}
lastClicked = $link;
}); });
$(document).on('click', 'a.explr-pick-element', function (event) { $(document).on('click', 'a.explr-pick-element', function (event) {
event.preventDefault(); event.preventDefault();

View File

@ -1,6 +1,10 @@
const $modalsRoot = $('.modals'); const $modalsRoot = $('.modals');
const $pickersRoot = $('.pickers'); const $pickersRoot = $('.pickers');
const isset = function (obj){
return obj !== undefined && obj !== null;
};
const showModal = function (modalClass) { const showModal = function (modalClass) {
$modalsRoot.removeClass('hidden').find('form').trigger('reset'); $modalsRoot.removeClass('hidden').find('form').trigger('reset');
$modalsRoot.find('.modal').addClass('hidden'); $modalsRoot.find('.modal').addClass('hidden');

1477
data/www/js/lib/jquery-fileupload.js vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -22,7 +22,7 @@ jQuery(document).ready(function ($) {
$('.modal-variable-edit input:visible:eq(0)').focus().select(); $('.modal-variable-edit input:visible:eq(0)').focus().select();
$('#variable-edit-name').val(variable.name); $('#variable-edit-name').val(variable.name);
$('#variable-edit-description').html(variable.description); $('#variable-edit-description').html(variable.description);
$('#variable-edit-description-edition').html(variable.description_edition).toggleClass('hidden', variable.description_edition == ''); $('#variable-edit-description-edition').html(variable.description_edition).toggleClass('hidden', variable.description_edition === '');
$('#variable-edit-value').val(variable.value); $('#variable-edit-value').val(variable.value);
$('#variable-edit-id').val(variable.id); $('#variable-edit-id').val(variable.id);
}); });

View File

@ -77,7 +77,6 @@ main {
margin-right: 10px; margin-right: 10px;
} }
.explr-selection-actions + .btn,
.btn:first-child { .btn:first-child {
margin-left: 0 !important; margin-left: 0 !important;
} }

View File

@ -15,6 +15,7 @@ button,
letter-spacing: -0.5px; letter-spacing: -0.5px;
margin-top: -$shadowOffset; margin-top: -$shadowOffset;
min-width: 38px; min-width: 38px;
min-height: 34px;
text-align: center; text-align: center;
justify-content: center; justify-content: center;

View File

@ -0,0 +1,64 @@
body.dragover {
.shakeondrag {
animation: shakednd .1s linear alternate infinite;
}
}
.btn-super-upload-busy,
.btn-super-upload {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
margin-left: 10px;
position: relative;
&.btn-super-upload-busy {
border: none !important;
}
.unprogress {
display: block;
}
.progress {
display: none;
width: 200px;
height: 10px;
background: #666;
border-radius: $baseRadius;
flex-direction: row;
justify-content: flex-start;
align-items: center;
.progress-bar {
border-radius: $baseRadius;
background-color: $seaBlue;
height: 100%;
}
.percent {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
left: 0;
right: 0;
bottom: 0;
top: 2px;
font-size: 15px;
color: white;
text-shadow: 0 0 2px black;
}
}
&.uploading {
.progress {
display: block;
}
.unprogress {
display: none;
}
}
}

View File

@ -73,6 +73,7 @@ ul.explr-tree {
} }
} }
.explr-multiselection-actions,
.explr-selection-actions { .explr-selection-actions {
display: none; display: none;
flex-direction: row; flex-direction: row;
@ -107,6 +108,28 @@ body.explr-selection-actionable {
} }
} }
body.explr-multiselection-actionable {
.explr-multiselection-actions {
display: flex;
}
&.explr-multiselection-folder {
.explr-multiselection-actions {
button.explr-multiselection-folder {
display: flex;
}
}
}
&.explr-multiselection-entity {
.explr-multiselection-actions {
button.explr-multiselection-entity {
display: flex;
}
}
}
}
ul.explr-dirview { ul.explr-dirview {
display: flex; display: flex;
@ -119,9 +142,9 @@ ul.explr-dirview {
justify-content: flex-start; justify-content: flex-start;
align-items: center; align-items: center;
flex-shrink: 0; flex-shrink: 0;
margin: 18px; margin: 10px 10px;
min-width: 90px; min-width: 100px;
min-height: 104px; min-height: 130px;
padding-top: 5px; padding-top: 5px;
border: 1px solid transparent; border: 1px solid transparent;
border-radius: $baseRadius; border-radius: $baseRadius;
@ -162,6 +185,35 @@ ul.explr-dirview {
min-width: 84px; min-width: 84px;
position: relative; position: relative;
&.with-thumbnail {
.img-holder {
width: 64px;
height: 64px;
background: #070707;
border-radius: 8px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
overflow: hidden;
margin-bottom: 12px;
img {
max-height: 100%;
max-width: 100%;
}
}
i {
font-size: 24px;
position: absolute;
top: -4px;
left: 0;
text-shadow: 0 .5px .5px #777;
}
}
i { i {
font-size: 64px; font-size: 64px;
margin-bottom: 12px; margin-bottom: 12px;

View File

@ -10,3 +10,12 @@
left: 27px; left: 27px;
} }
} }
@keyframes shakednd {
0% {
transform: rotate(-2deg);
}
100% {
transform: rotate(2deg);
}
}

View File

@ -21,6 +21,7 @@
@import 'components/breadcrumb'; @import 'components/breadcrumb';
@import 'components/modals'; @import 'components/modals';
@import 'components/toast'; @import 'components/toast';
@import 'components/dragdrop';
// Legacy // Legacy
@import 'components/panes'; @import 'components/panes';

View File

@ -1,9 +1,29 @@
.view-content-list main .main-container { .view-content-list {
main .main-container {
.page-content {
.inner {
padding-bottom: 10px;
.dropzone {
flex: 1;
align-self: stretch;
border: 1px solid transparent;
}
}
}
.content-object-input { .content-object-input {
margin-bottom: 6px; margin-bottom: 6px;
}
}
&.dragover {
main .main-container .inner .dropzone {
border-radius: $baseRadius;
background: rgba($white, .1);
border: 1px dashed rgba($white, .5);
}
} }
} }

View File

@ -244,6 +244,8 @@
"common_copied": "Element copied in clipboard!", "common_copied": "Element copied in clipboard!",
"common_host_placeholder": "raspberrypi.local or 192.168.1.85", "common_host_placeholder": "raspberrypi.local or 192.168.1.85",
"common_reachable_at": "Host", "common_reachable_at": "Host",
"common_http_error_occured": "Error %code% occured",
"common_http_error_413": "Files are too large",
"logout": "Logout", "logout": "Logout",
"login_error_not_found": "Bad credentials", "login_error_not_found": "Bad credentials",
"login_error_bad_credentials": "Bad credentials", "login_error_bad_credentials": "Bad credentials",
@ -300,5 +302,6 @@
"sysinfo_network_interface": "Network Interface", "sysinfo_network_interface": "Network Interface",
"sysinfo_mac_address": "MAC Address", "sysinfo_mac_address": "MAC Address",
"sysinfo_ip_address": "IP Address", "sysinfo_ip_address": "IP Address",
"player_default_welcome_message": "To manage this player, go to a browser at" "player_default_welcome_message": "To manage this player, go to a browser at",
"player_noplaylist_welcome_message": "No playlist is configured by default, go to a browser at"
} }

View File

@ -245,6 +245,8 @@
"common_copied": "¡Elemento copiado!", "common_copied": "¡Elemento copiado!",
"common_host_placeholder": "raspberrypi.local o 192.168.1.85", "common_host_placeholder": "raspberrypi.local o 192.168.1.85",
"common_reachable_at": "Host", "common_reachable_at": "Host",
"common_http_error_occured": "Se ha producido un error %code%",
"common_http_error_413": "Los archivos son demasiado grandes",
"logout": "Cerrar sesión", "logout": "Cerrar sesión",
"login_error_not_found": "Credenciales incorrectas", "login_error_not_found": "Credenciales incorrectas",
"login_error_bad_credentials": "Credenciales incorrectas", "login_error_bad_credentials": "Credenciales incorrectas",
@ -301,5 +303,6 @@
"sysinfo_network_interface": "Interfaz de red", "sysinfo_network_interface": "Interfaz de red",
"sysinfo_mac_address": "Dirección MAC", "sysinfo_mac_address": "Dirección MAC",
"sysinfo_ip_address": "Dirección IP", "sysinfo_ip_address": "Dirección IP",
"player_default_welcome_message": "Para gestionar este reproductor, ve a un navegador en" "player_default_welcome_message": "Para gestionar este reproductor, ve a un navegador en",
"player_noplaylist_welcome_message": "No hay ninguna playlist configurada de forma predeterminada, vaya a un navegador en"
} }

View File

@ -246,6 +246,8 @@
"common_copied": "Element copié !", "common_copied": "Element copié !",
"common_host_placeholder": "raspberrypi.local ou 192.168.1.85", "common_host_placeholder": "raspberrypi.local ou 192.168.1.85",
"common_reachable_at": "Hôte", "common_reachable_at": "Hôte",
"common_http_error_occured": "Une erreur %code% est apparue",
"common_http_error_413": "Les fichiers sont trop volumineux",
"logout": "Déconnexion", "logout": "Déconnexion",
"login_error_not_found": "Identifiants invalides", "login_error_not_found": "Identifiants invalides",
"login_error_bad_credentials": "Identifiants invalides", "login_error_bad_credentials": "Identifiants invalides",
@ -302,5 +304,6 @@
"sysinfo_network_interface": "Interface Réseau", "sysinfo_network_interface": "Interface Réseau",
"sysinfo_mac_address": "Addresse MAC", "sysinfo_mac_address": "Addresse MAC",
"sysinfo_ip_address": "Addresse IP", "sysinfo_ip_address": "Addresse IP",
"player_default_welcome_message": "Pour gérer ce lecteur, allez sur un navigateur à l'adresse" "player_default_welcome_message": "Pour gérer ce lecteur, allez sur un navigateur à l'adresse",
"player_noplaylist_welcome_message": "Aucune playlist n'est configurée par défaut, allez sur un navigateur à l'adresse"
} }

View File

@ -245,6 +245,8 @@
"common_copied": "Elemento copiato!", "common_copied": "Elemento copiato!",
"common_host_placeholder": "raspberrypi.local o 192.168.1.85", "common_host_placeholder": "raspberrypi.local o 192.168.1.85",
"common_reachable_at": "Host", "common_reachable_at": "Host",
"common_http_error_occured": "Si è verificato un errore %code%",
"common_http_error_413": "I file sono troppo grandi",
"logout": "Logout", "logout": "Logout",
"login_error_not_found": "Credenziali errate", "login_error_not_found": "Credenziali errate",
"login_error_bad_credentials": "Credenziali errate", "login_error_bad_credentials": "Credenziali errate",
@ -301,5 +303,6 @@
"sysinfo_network_interface": "interfaccia di rete", "sysinfo_network_interface": "interfaccia di rete",
"sysinfo_mac_address": "Indirizzo MAC", "sysinfo_mac_address": "Indirizzo MAC",
"sysinfo_ip_address": "indirizzo IP", "sysinfo_ip_address": "indirizzo IP",
"player_default_welcome_message": "Per gestire questo lettore, vai al browser all'indirizzo" "player_default_welcome_message": "Per gestire questo lettore, vai al browser all'indirizzo",
"player_noplaylist_welcome_message": "Nessuna playlist è configurata per impostazione predefinita, vai su un browser all'indirizzo"
} }

View File

@ -28,11 +28,25 @@ class ContentController(ObController):
self._app.add_url_rule('/slideshow/content/rename-folder', 'slideshow_content_folder_rename', self._auth(self.slideshow_content_folder_rename), methods=['POST']) self._app.add_url_rule('/slideshow/content/rename-folder', 'slideshow_content_folder_rename', self._auth(self.slideshow_content_folder_rename), methods=['POST'])
self._app.add_url_rule('/slideshow/content/delete-folder', 'slideshow_content_folder_delete', self._auth(self.slideshow_content_folder_delete), methods=['GET']) self._app.add_url_rule('/slideshow/content/delete-folder', 'slideshow_content_folder_delete', self._auth(self.slideshow_content_folder_delete), methods=['GET'])
self._app.add_url_rule('/slideshow/content/show/<content_id>', 'slideshow_content_show', self._auth(self.slideshow_content_show), methods=['GET']) self._app.add_url_rule('/slideshow/content/show/<content_id>', 'slideshow_content_show', self._auth(self.slideshow_content_show), methods=['GET'])
self._app.add_url_rule('/slideshow/content/upload-bulk', 'slideshow_content_upload_bulk', self._auth(self.slideshow_content_upload_bulk), methods=['POST'])
self._app.add_url_rule('/slideshow/content/delete-bulk-explr', 'slideshow_content_delete_bulk_explr', self._auth(self.slideshow_content_delete_bulk_explr), methods=['GET'])
def get_working_folder(self):
working_folder_path = request.args.get('path', None)
working_folder = None
if working_folder_path:
working_folder = self._model_store.folder().get_one_by_path(path=working_folder_path, entity=FolderEntity.CONTENT)
if not working_folder:
working_folder_path = self._model_store.variable().get_one_by_name('last_folder_content').as_string()
working_folder = self._model_store.folder().get_one_by_path(path=working_folder_path, entity=FolderEntity.CONTENT)
return working_folder_path, working_folder
def slideshow_content_list(self): def slideshow_content_list(self):
self._model_store.variable().update_by_name('last_pillmenu_slideshow', 'slideshow_content_list') self._model_store.variable().update_by_name('last_pillmenu_slideshow', 'slideshow_content_list')
working_folder_path = self._model_store.variable().get_one_by_name('last_folder_content').as_string() working_folder_path, working_folder = self.get_working_folder()
working_folder = self._model_store.folder().get_one_by_path(path=working_folder_path, entity=FolderEntity.CONTENT)
slides_with_content = self._model_store.slide().get_all_indexed(attribute='content_id', multiple=True) slides_with_content = self._model_store.slide().get_all_indexed(attribute='content_id', multiple=True)
return render_template( return render_template(
@ -48,8 +62,7 @@ class ContentController(ObController):
) )
def slideshow_content_add(self): def slideshow_content_add(self):
working_folder_path = self._model_store.variable().get_one_by_name('last_folder_content').as_string() working_folder_path, working_folder = self.get_working_folder()
working_folder = self._model_store.folder().get_one_by_path(path=working_folder_path, entity=FolderEntity.CONTENT)
self._model_store.content().add_form_raw( self._model_store.content().add_form_raw(
name=request.form['name'], name=request.form['name'],
@ -60,7 +73,24 @@ class ContentController(ObController):
folder_id=working_folder.id if working_folder else None folder_id=working_folder.id if working_folder else None
) )
return redirect(url_for('slideshow_content_list')) return redirect(url_for('slideshow_content_list', path=working_folder_path))
def slideshow_content_upload_bulk(self):
working_folder_path, working_folder = self.get_working_folder()
file = request.files['object']
type = ContentType.guess_content_type_file(file)
name = file.filename.rsplit('.', 1)[0]
self._model_store.content().add_form_raw(
name=name,
type=type,
request_files=request.files,
upload_dir=self._app.config['UPLOAD_FOLDER'],
folder_id=working_folder.id if working_folder else None
)
return redirect(url_for('slideshow_content_list', path=working_folder_path))
def slideshow_content_edit(self, content_id: int = 0): def slideshow_content_edit(self, content_id: int = 0):
content = self._model_store.content().get(content_id) content = self._model_store.content().get(content_id)
@ -68,8 +98,7 @@ class ContentController(ObController):
if not content: if not content:
return abort(404) return abort(404)
working_folder_path = self._model_store.variable().get_one_by_name('last_folder_content').as_string() working_folder_path, working_folder = self.get_working_folder()
working_folder = self._model_store.folder().get_one_by_path(path=working_folder_path, entity=FolderEntity.CONTENT)
return render_template( return render_template(
'slideshow/contents/edit.jinja.html', 'slideshow/contents/edit.jinja.html',
@ -80,10 +109,11 @@ class ContentController(ObController):
) )
def slideshow_content_save(self, content_id: int = 0): def slideshow_content_save(self, content_id: int = 0):
working_folder_path, working_folder = self.get_working_folder()
content = self._model_store.content().get(content_id) content = self._model_store.content().get(content_id)
if not content: if not content:
return redirect(url_for('slideshow_content_list')) return redirect(url_for('slideshow_content_list', path=working_folder_path))
self._model_store.content().update_form( self._model_store.content().update_form(
id=content.id, id=content.id,
@ -95,27 +125,25 @@ class ContentController(ObController):
return redirect(url_for('slideshow_content_edit', content_id=content_id, saved=1)) return redirect(url_for('slideshow_content_edit', content_id=content_id, saved=1))
def slideshow_content_delete(self): def slideshow_content_delete(self):
content = self._model_store.content().get(request.args.get('id')) working_folder_path, working_folder = self.get_working_folder()
error_tuple = self.delete_content_by_id(request.args.get('id'))
route_args = {
"path": working_folder_path,
}
if not content: if error_tuple:
return redirect(url_for('slideshow_content_list')) route_args[error_tuple[0]] = error_tuple[1]
slide_counter = self._model_store.slide().count_slides_for_content(content.id) return redirect(url_for('slideshow_content_list', **route_args))
if slide_counter > 0:
return redirect(url_for('slideshow_content_list', referenced_in_slide_error=True))
self._model_store.content().delete(content.id)
self._post_update()
return redirect(url_for('slideshow_content_list'))
def slideshow_content_rename(self): def slideshow_content_rename(self):
working_folder_path, working_folder = self.get_working_folder()
self._model_store.content().update_form( self._model_store.content().update_form(
id=request.form['id'], id=request.form['id'],
name=request.form['name'], name=request.form['name'],
) )
return redirect(url_for('slideshow_content_list')) return redirect(url_for('slideshow_content_list', path=working_folder_path))
def slideshow_content_cd(self): def slideshow_content_cd(self):
path = request.args.get('path') path = request.args.get('path')
@ -140,46 +168,46 @@ class ContentController(ObController):
return redirect(url_for('slideshow_content_list', path=path)) return redirect(url_for('slideshow_content_list', path=path))
def slideshow_content_folder_add(self): def slideshow_content_folder_add(self):
working_folder_path, working_folder = self.get_working_folder()
self._model_store.folder().add_folder( self._model_store.folder().add_folder(
entity=FolderEntity.CONTENT, entity=FolderEntity.CONTENT,
name=request.form['name'], name=request.form['name'],
working_folder_path=request.form['working_folder_path'], working_folder_path=working_folder_path
) )
return redirect(url_for('slideshow_content_list')) return redirect(url_for('slideshow_content_list', path=working_folder_path))
def slideshow_content_folder_rename(self): def slideshow_content_folder_rename(self):
working_folder_path, working_folder = self.get_working_folder()
self._model_store.folder().rename_folder( self._model_store.folder().rename_folder(
folder_id=request.form['id'], folder_id=request.form['id'],
name=request.form['name'], name=request.form['name'],
) )
return redirect(url_for('slideshow_content_list')) return redirect(url_for('slideshow_content_list', path=working_folder_path))
def slideshow_content_folder_move(self): def slideshow_content_folder_move(self):
working_folder_path, working_folder = self.get_working_folder()
self._model_store.folder().move_to_folder( self._model_store.folder().move_to_folder(
entity_id=request.form['entity_id'], entity_id=request.form['entity_id'],
folder_id=request.form['new_folder_id'], folder_id=request.form['new_folder_id'],
entity_is_folder=True if 'is_folder' in request.form and request.form['is_folder'] == '1' else False, entity_is_folder=True if 'is_folder' in request.form and request.form['is_folder'] == '1' else False,
) )
return redirect(url_for('slideshow_content_list')) return redirect(url_for('slideshow_content_list', path=working_folder_path))
def slideshow_content_folder_delete(self): def slideshow_content_folder_delete(self):
folder = self._model_store.folder().get(request.args.get('id')) working_folder_path, working_folder = self.get_working_folder()
error_tuple = self.delete_folder_by_id(request.args.get('id'))
route_args = {
"path": working_folder_path,
}
if not folder: if error_tuple:
return redirect(url_for('slideshow_content_list')) route_args[error_tuple[0]] = error_tuple[1]
content_counter = self._model_store.content().count_contents_for_folder(folder.id) return redirect(url_for('slideshow_content_list', **route_args))
folder_counter = self._model_store.folder().count_subfolders_for_folder(folder.id)
if content_counter > 0 or folder_counter:
return redirect(url_for('slideshow_content_list', folder_not_empty_error=True))
self._model_store.folder().delete(id=folder.id)
return redirect(url_for('slideshow_content_list'))
def slideshow_content_show(self, content_id: int = 0): def slideshow_content_show(self, content_id: int = 0):
content = self._model_store.content().get(content_id) content = self._model_store.content().get(content_id)
@ -201,6 +229,57 @@ class ContentController(ObController):
return redirect(location) return redirect(location)
def slideshow_content_delete_bulk_explr(self):
working_folder_path, working_folder = self.get_working_folder()
entity_ids = request.args.get('entity_ids', '').split(',')
folder_ids = request.args.get('folder_ids', '').split(',')
route_args_dict = {"path": working_folder_path}
for id in entity_ids:
if id:
error_tuple = self.delete_content_by_id(id)
if error_tuple:
route_args_dict[error_tuple[0]] = error_tuple[1]
for id in folder_ids:
if id:
error_tuple = self.delete_folder_by_id(id)
if error_tuple:
route_args_dict[error_tuple[0]] = error_tuple[1]
return redirect(url_for('slideshow_content_list', **route_args_dict))
def delete_content_by_id(self, id):
content = self._model_store.content().get(id)
if not content:
return None
if self._model_store.slide().count_slides_for_content(content.id) > 0:
return 'referenced_in_slide_error', content.name
self._model_store.content().delete(content.id)
self._post_update()
return None
def delete_folder_by_id(self, id):
folder = self._model_store.folder().get(id)
if not folder:
return None
content_counter = self._model_store.content().count_contents_for_folder(folder.id)
folder_counter = self._model_store.folder().count_subfolders_for_folder(folder.id)
if content_counter > 0 or folder_counter:
return 'folder_not_empty_error', folder.name
self._model_store.folder().delete(id=folder.id)
self._post_update()
return None
def _post_update(self): def _post_update(self):
pass pass

View File

@ -31,11 +31,24 @@ class FleetNodePlayerController(ObController):
self._app.add_url_rule('/fleet/node-player/move-folder', 'fleet_node_player_folder_move', self._auth(self.fleet_node_player_folder_move), methods=['POST']) self._app.add_url_rule('/fleet/node-player/move-folder', 'fleet_node_player_folder_move', self._auth(self.fleet_node_player_folder_move), methods=['POST'])
self._app.add_url_rule('/fleet/node-player/rename-folder', 'fleet_node_player_folder_rename', self._auth(self.fleet_node_player_folder_rename), methods=['POST']) self._app.add_url_rule('/fleet/node-player/rename-folder', 'fleet_node_player_folder_rename', self._auth(self.fleet_node_player_folder_rename), methods=['POST'])
self._app.add_url_rule('/fleet/node-player/delete-folder', 'fleet_node_player_folder_delete', self._auth(self.fleet_node_player_folder_delete), methods=['GET']) self._app.add_url_rule('/fleet/node-player/delete-folder', 'fleet_node_player_folder_delete', self._auth(self.fleet_node_player_folder_delete), methods=['GET'])
self._app.add_url_rule('/fleet/node-player/delete-bulk-explr', 'fleet_node_player_delete_bulk_explr', self._auth(self.fleet_node_player_delete_bulk_explr), methods=['GET'])
def get_working_folder(self):
working_folder_path = request.args.get('path', None)
working_folder = None
if working_folder_path:
working_folder = self._model_store.folder().get_one_by_path(path=working_folder_path, entity=FolderEntity.NODE_PLAYER)
if not working_folder:
working_folder_path = self._model_store.variable().get_one_by_name('last_folder_node_player').as_string()
working_folder = self._model_store.folder().get_one_by_path(path=working_folder_path, entity=FolderEntity.NODE_PLAYER)
return working_folder_path, working_folder
def fleet_node_player_list(self): def fleet_node_player_list(self):
self._model_store.variable().update_by_name('last_pillmenu_fleet', 'fleet_node_player_list') self._model_store.variable().update_by_name('last_pillmenu_fleet', 'fleet_node_player_list')
working_folder_path = self._model_store.variable().get_one_by_name('last_folder_node_player').as_string() working_folder_path, working_folder = self.get_working_folder()
working_folder = self._model_store.folder().get_one_by_path(path=working_folder_path, entity=FolderEntity.NODE_PLAYER)
return render_template( return render_template(
'fleet/node-players/list.jinja.html', 'fleet/node-players/list.jinja.html',
@ -50,8 +63,7 @@ class FleetNodePlayerController(ObController):
) )
def fleet_node_player_add(self): def fleet_node_player_add(self):
working_folder_path = self._model_store.variable().get_one_by_name('last_folder_node_player').as_string() working_folder_path, working_folder = self.get_working_folder()
working_folder = self._model_store.folder().get_one_by_path(path=working_folder_path, entity=FolderEntity.NODE_PLAYER)
self._model_store.node_player().add_form( self._model_store.node_player().add_form(
NodePlayer( NodePlayer(
@ -62,7 +74,7 @@ class FleetNodePlayerController(ObController):
) )
) )
return redirect(url_for('fleet_node_player_list')) return redirect(url_for('fleet_node_player_list', path=working_folder_path))
def fleet_node_player_edit(self, node_player_id: int = 0): def fleet_node_player_edit(self, node_player_id: int = 0):
node_player = self._model_store.node_player().get(node_player_id) node_player = self._model_store.node_player().get(node_player_id)
@ -70,8 +82,7 @@ class FleetNodePlayerController(ObController):
if not node_player: if not node_player:
return abort(404) return abort(404)
working_folder_path = self._model_store.variable().get_one_by_name('last_folder_node_player').as_string() working_folder_path, working_folder = self.get_working_folder()
working_folder = self._model_store.folder().get_one_by_path(path=working_folder_path, entity=FolderEntity.NODE_PLAYER)
return render_template( return render_template(
'fleet/node-players/edit.jinja.html', 'fleet/node-players/edit.jinja.html',
@ -84,9 +95,10 @@ class FleetNodePlayerController(ObController):
def fleet_node_player_save(self, node_player_id: int = 0): def fleet_node_player_save(self, node_player_id: int = 0):
node_player_id = request.form['id'] if 'id' in request.form else node_player_id node_player_id = request.form['id'] if 'id' in request.form else node_player_id
node_player = self._model_store.node_player().get(node_player_id) node_player = self._model_store.node_player().get(node_player_id)
working_folder_path, working_folder = self.get_working_folder()
if not node_player: if not node_player:
return redirect(url_for('fleet_node_player_list')) return redirect(url_for('fleet_node_player_list', path=working_folder_path))
self._model_store.node_player().update_form( self._model_store.node_player().update_form(
id=node_player.id, id=node_player.id,
@ -97,25 +109,28 @@ class FleetNodePlayerController(ObController):
self._post_update() self._post_update()
# return redirect(url_for('fleet_node_player_edit', node_player_id=node_player_id, saved=1)) # return redirect(url_for('fleet_node_player_edit', node_player_id=node_player_id, saved=1))
return redirect(url_for('fleet_node_player_list')) return redirect(url_for('fleet_node_player_list', path=working_folder_path))
def fleet_node_player_delete(self): def fleet_node_player_delete(self):
node_player = self._model_store.node_player().get(request.args.get('id')) working_folder_path, working_folder = self.get_working_folder()
error_tuple = self.delete_node_player_by_id(request.args.get('id'))
route_args = {
"path": working_folder_path,
}
if not node_player: if error_tuple:
return redirect(url_for('fleet_node_player_list')) route_args[error_tuple[0]] = error_tuple[1]
self._model_store.node_player().delete(node_player.id) return redirect(url_for('fleet_node_player_list', **route_args))
self._post_update()
return redirect(url_for('fleet_node_player_list'))
def fleet_node_player_rename(self): def fleet_node_player_rename(self):
working_folder_path, working_folder = self.get_working_folder()
self._model_store.node_player().update_form( self._model_store.node_player().update_form(
id=request.form['id'], id=request.form['id'],
name=request.form['name'], name=request.form['name'],
) )
return redirect(url_for('fleet_node_player_list')) return redirect(url_for('fleet_node_player_list', path=working_folder_path))
def fleet_node_player_cd(self): def fleet_node_player_cd(self):
path = request.args.get('path') path = request.args.get('path')
@ -140,46 +155,96 @@ class FleetNodePlayerController(ObController):
return redirect(url_for('fleet_node_player_list', path=path)) return redirect(url_for('fleet_node_player_list', path=path))
def fleet_node_player_folder_add(self): def fleet_node_player_folder_add(self):
working_folder_path, working_folder = self.get_working_folder()
self._model_store.folder().add_folder( self._model_store.folder().add_folder(
entity=FolderEntity.NODE_PLAYER, entity=FolderEntity.NODE_PLAYER,
name=request.form['name'], name=request.form['name'],
working_folder_path=request.form['working_folder_path'], working_folder_path=working_folder_path
) )
return redirect(url_for('fleet_node_player_list')) return redirect(url_for('fleet_node_player_list', path=working_folder_path))
def fleet_node_player_folder_rename(self): def fleet_node_player_folder_rename(self):
working_folder_path, working_folder = self.get_working_folder()
self._model_store.folder().rename_folder( self._model_store.folder().rename_folder(
folder_id=request.form['id'], folder_id=request.form['id'],
name=request.form['name'], name=request.form['name'],
) )
return redirect(url_for('fleet_node_player_list')) return redirect(url_for('fleet_node_player_list', path=working_folder_path))
def fleet_node_player_folder_move(self): def fleet_node_player_folder_move(self):
working_folder_path, working_folder = self.get_working_folder()
self._model_store.folder().move_to_folder( self._model_store.folder().move_to_folder(
entity_id=request.form['entity_id'], entity_id=request.form['entity_id'],
folder_id=request.form['new_folder_id'], folder_id=request.form['new_folder_id'],
entity_is_folder=True if 'is_folder' in request.form and request.form['is_folder'] == '1' else False, entity_is_folder=True if 'is_folder' in request.form and request.form['is_folder'] == '1' else False,
) )
return redirect(url_for('fleet_node_player_list')) return redirect(url_for('fleet_node_player_list', path=working_folder_path))
def fleet_node_player_folder_delete(self): def fleet_node_player_folder_delete(self):
folder = self._model_store.folder().get(request.args.get('id')) working_folder_path, working_folder = self.get_working_folder()
error_tuple = self.delete_folder_by_id(request.args.get('id'))
route_args = {
"path": working_folder_path,
}
if error_tuple:
route_args[error_tuple[0]] = error_tuple[1]
return redirect(url_for('fleet_node_player_list', **route_args))
def fleet_node_player_delete_bulk_explr(self):
working_folder_path, working_folder = self.get_working_folder()
entity_ids = request.args.get('entity_ids', '').split(',')
folder_ids = request.args.get('folder_ids', '').split(',')
route_args_dict = {"path": working_folder_path}
for id in entity_ids:
if id:
error_tuple = self.delete_node_player_by_id(id)
if error_tuple:
route_args_dict[error_tuple[0]] = error_tuple[1]
for id in folder_ids:
if id:
error_tuple = self.delete_folder_by_id(id)
if error_tuple:
route_args_dict[error_tuple[0]] = error_tuple[1]
return redirect(url_for('fleet_node_player_list', **route_args_dict))
def delete_node_player_by_id(self, id):
node_player = self._model_store.node_player().get(id)
if not node_player:
return None
self._model_store.node_player().delete(node_player.id)
self._post_update()
return None
def delete_folder_by_id(self, id):
folder = self._model_store.folder().get(id)
if not folder: if not folder:
return redirect(url_for('fleet_node_player_list')) return None
node_player_counter = self._model_store.node_player().count_node_players_for_folder(folder.id) node_player_counter = self._model_store.node_player().count_node_players_for_folder(folder.id)
folder_counter = self._model_store.folder().count_subfolders_for_folder(folder.id) folder_counter = self._model_store.folder().count_subfolders_for_folder(folder.id)
if node_player_counter > 0 or folder_counter: if node_player_counter > 0 or folder_counter:
return redirect(url_for('fleet_node_player_list', folder_not_empty_error=True)) return 'folder_not_empty_error', folder.name
self._model_store.folder().delete(id=folder.id) self._model_store.folder().delete(id=folder.id)
return redirect(url_for('fleet_node_player_list')) return None
def _post_update(self): def _post_update(self):
pass pass

View File

@ -40,7 +40,7 @@ class PlayerController(ObController):
try: try:
items = self._get_playlist(playlist_id=playlist_id, preview_content_id=preview_content_id) items = self._get_playlist(playlist_id=playlist_id, preview_content_id=preview_content_id)
except NoFallbackPlaylistException: except NoFallbackPlaylistException:
abort(404) return redirect(url_for('player_default', noplaylist=1))
intro_slide_duration = 0 if items['preview_mode'] else int(request.args.get('intro', self._model_store.variable().get_one_by_name('intro_slide_duration').eval())) intro_slide_duration = 0 if items['preview_mode'] else int(request.args.get('intro', self._model_store.variable().get_one_by_name('intro_slide_duration').eval()))
animation_enabled = bool(request.args.get('animation', int(self._model_store.variable().get_one_by_name('slide_animation_enabled').eval()))) animation_enabled = bool(request.args.get('animation', int(self._model_store.variable().get_one_by_name('slide_animation_enabled').eval())))
@ -64,7 +64,8 @@ class PlayerController(ObController):
return render_template( return render_template(
'player/default.jinja.html', 'player/default.jinja.html',
interfaces=[iface['ip_address'] for iface in get_network_interfaces()], interfaces=[iface['ip_address'] for iface in get_network_interfaces()],
time_with_seconds=self._model_store.variable().get_one_by_name('default_slide_time_with_seconds') time_with_seconds=self._model_store.variable().get_one_by_name('default_slide_time_with_seconds'),
noplaylist=request.args.get('noplaylist', '0') == '1'
) )
def player_playlist(self, playlist_slug_or_id: str = ''): def player_playlist(self, playlist_slug_or_id: str = ''):

View File

@ -73,9 +73,10 @@ class DatabaseManager:
sanitized_params.append(param) sanitized_params.append(param)
try: try:
with self._conn: self._conn.execute('BEGIN')
cur = self._conn.cursor() cur = self._conn.cursor()
cur.execute(query, tuple(sanitized_params)) cur.execute(query, tuple(sanitized_params))
self._conn.commit()
except sqlite3.Error as e: except sqlite3.Error as e:
if not silent_errors: if not silent_errors:
logging.error("SQL query execution error while writing '{}': {}".format(query, e)) logging.error("SQL query execution error while writing '{}': {}".format(query, e))

View File

@ -1,8 +1,11 @@
import mimetypes
from enum import Enum from enum import Enum
from typing import Union from typing import Union, List, Optional
from src.util.utils import str_to_enum from src.util.utils import str_to_enum
class ContentInputType(Enum): class ContentInputType(Enum):
UPLOAD = 'upload' UPLOAD = 'upload'
@ -23,6 +26,25 @@ class ContentType(Enum):
YOUTUBE = 'youtube' YOUTUBE = 'youtube'
VIDEO = 'video' VIDEO = 'video'
@staticmethod
def guess_content_type_file(file):
mime_type, _ = mimetypes.guess_type(file.filename)
if mime_type in [
'image/gif',
'image/png',
'image/jpeg',
'image/webp',
'image/jpg'
]:
return ContentType.PICTURE
elif mime_type in [
'video/mp4'
]:
return ContentType.VIDEO
return None
@staticmethod @staticmethod
def get_input(value: Enum) -> ContentInputType: def get_input(value: Enum) -> ContentInputType:
if value == ContentType.PICTURE: if value == ContentType.PICTURE:

View File

@ -44,8 +44,16 @@
</button> </button>
<button type="button" <button type="button"
class="btn explr-item-delete explr-selection-entity explr-selection-folder btn-danger-alt" class="btn explr-item-delete explr-selection-entity explr-selection-folder btn-danger-alt"
data-folder-route="{{ url_for('fleet_node_player_folder_delete') }}" data-folder-route="{{ url_for('fleet_node_player_folder_delete') }}?path={{ working_folder_path }}"
data-entity-route="{{ url_for('fleet_node_player_delete') }}"> data-entity-route="{{ url_for('fleet_node_player_delete') }}?path={{ working_folder_path }}">
<i class="fa fa-trash-alt"></i>
</button>
</div>
<div class="explr-multiselection-actions">
<button type="button"
class="btn explr-items-delete explr-multiselection-entity explr-multiselection-folder btn-danger-alt"
data-route="{{ url_for('fleet_node_player_delete_bulk_explr') }}?path={{ working_folder_path }}">
<i class="fa fa-trash-alt"></i> <i class="fa fa-trash-alt"></i>
</button> </button>
</div> </div>
@ -114,8 +122,7 @@
<li class="new-folder hidden"> <li class="new-folder hidden">
<a href="javascript:void(0);"> <a href="javascript:void(0);">
<i class="fa fa-folder"></i> <i class="fa fa-folder"></i>
<form action="{{ url_for('fleet_node_player_folder_add') }}" method="POST"> <form action="{{ url_for('fleet_node_player_folder_add') }}?path={{ working_folder_path }}" method="POST">
<input type="hidden" name="working_folder_path" value="{{ working_folder_path }}" />
<input type="text" name="name" autocomplete="off"/> <input type="text" name="name" autocomplete="off"/>
</form> </form>
</a> </a>
@ -141,7 +148,7 @@
class="explr-link explr-item-selectable explr-item-actionable explr-item-folder"> class="explr-link explr-item-selectable explr-item-actionable explr-item-folder">
<i class="fa fa-folder"></i> <i class="fa fa-folder"></i>
<span>{{ truncate(folder.name, explr_limit_chars, '...') }}</span> <span>{{ truncate(folder.name, explr_limit_chars, '...') }}</span>
<form action="{{ url_for('fleet_node_player_folder_rename') }}" method="POST"> <form action="{{ url_for('fleet_node_player_folder_rename') }}?path={{ working_folder_path }}" method="POST">
<input type="text" name="name" value="{{ folder.name }}" autocomplete="off"/> <input type="text" name="name" value="{{ folder.name }}" autocomplete="off"/>
<input type="hidden" name="id" value="{{ folder.id }}"/> <input type="hidden" name="id" value="{{ folder.id }}"/>
</form> </form>
@ -166,7 +173,7 @@
</sub> </sub>
{% endif %} {% endif %}
<span>{{ truncate(node_player.name, explr_limit_chars, '...') }}</span> <span>{{ truncate(node_player.name, explr_limit_chars, '...') }}</span>
<form action="{{ url_for('fleet_node_player_rename') }}" method="POST"> <form action="{{ url_for('fleet_node_player_rename') }}?path={{ working_folder_path }}" method="POST">
<input type="text" name="name" value="{{ node_player.name }}" autocomplete="off"/> <input type="text" name="name" value="{{ node_player.name }}" autocomplete="off"/>
<input type="hidden" name="id" value="{{ node_player.id }}"/> <input type="hidden" name="id" value="{{ node_player.id }}"/>
</form> </form>

View File

@ -63,7 +63,7 @@
<script> <script>
const interfaces = {{ json_dumps(interfaces) | safe }}; const interfaces = {{ json_dumps(interfaces) | safe }};
const translation_common_unknown_ipaddr = '{{ l.common_unknown_ipaddr }}'; const translation_common_unknown_ipaddr = '{{ l.common_unknown_ipaddr }}';
const translation_player_default_welcome_message = '{{ l.player_default_welcome_message }}'; const translation_player_default_welcome_message = '{% if noplaylist %}{{ l.player_noplaylist_welcome_message }}{% else %}{{ l.player_default_welcome_message }}{% endif %}';
const manage_url_template = '{{ 'http://%ipaddr%:' ~ PORT ~ url_for('manage') }}'; const manage_url_template = '{{ 'http://%ipaddr%:' ~ PORT ~ url_for('manage') }}';
const setIps = function(ips) { const setIps = function(ips) {

View File

@ -59,7 +59,7 @@
<div class="horizontal"> <div class="horizontal">
<div class="form-holder"> <div class="form-holder">
<form class="form" action="{{ url_for('slideshow_content_save', content_id=content.id) }}" method="POST"> <form class="form" action="{{ url_for('slideshow_content_save', content_id=content.id) }}?path={{ working_folder_path }}" method="POST">
<div class="form-group"> <div class="form-group">
<label for="content-edit-name">{{ l.slideshow_content_form_label_name }}</label> <label for="content-edit-name">{{ l.slideshow_content_form_label_name }}</label>

View File

@ -10,10 +10,18 @@
{% endblock %} {% endblock %}
{% block add_js %} {% block add_js %}
<script>
var l = $.extend(l, {
'js_common_http_error_occured': '{{ l.common_http_error_occured }}',
'js_common_http_error_413': '{{ l.common_http_error_413 }}'
});
</script>
<script src="{{ STATIC_PREFIX }}js/lib/jquery-explr-1.4.js"></script> <script src="{{ STATIC_PREFIX }}js/lib/jquery-explr-1.4.js"></script>
<script src="{{ STATIC_PREFIX }}js/explorer.js"></script> <script src="{{ STATIC_PREFIX }}js/explorer.js"></script>
<script src="{{ STATIC_PREFIX }}js/slideshow/contents.js"></script> <script src="{{ STATIC_PREFIX }}js/slideshow/contents.js"></script>
<script src="{{ STATIC_PREFIX }}js/lib/jquery-ui.min.js"></script> <script src="{{ STATIC_PREFIX }}js/lib/jquery-ui.min.js"></script>
<script src="{{ STATIC_PREFIX }}js/lib/jquery-fileupload.js"></script>
<script src="{{ STATIC_PREFIX }}js/dragdrop.js"></script>
{{ HOOK(H_SLIDESHOW_CONTENT_JAVASCRIPT) }} {{ HOOK(H_SLIDESHOW_CONTENT_JAVASCRIPT) }}
{% endblock %} {% endblock %}
@ -27,27 +35,47 @@
<div class="top-actions"> <div class="top-actions">
{{ HOOK(H_SLIDESHOW_CONTENT_TOOLBAR_ACTIONS_START) }} {{ HOOK(H_SLIDESHOW_CONTENT_TOOLBAR_ACTIONS_START) }}
<button type="button" class="btn btn-info content-add item-add">
<i class="fa fa-file-circle-plus icon-left"></i>
{{ l.slideshow_content_button_add }}
</button>
<button type="button" class="folder-add btn-neutral"> <button type="button" class="folder-add btn-neutral">
<i class="fa fa-folder-plus icon-left"></i> <i class="fa fa-folder-plus icon-left"></i>
{{ l.common_new_folder }} {{ l.common_new_folder }}
</button> </button>
<button type="button" class="btn btn-info content-add item-add">
<i class="fa fa-file-circle-plus icon-left"></i>
{{ l.slideshow_content_button_add }}
</button>
<a href="javascript:void(0);" class="btn btn-info btn-super-upload" data-route="{{ url_for('slideshow_content_upload_bulk') }}?path={{ working_folder_path }}">
<input type="file" id="content_files" name="object" class="hidden" multiple />
<div class="unprogress">
<i class="fa fa-bolt"></i>
</div>
<div class="progress">
<div class="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width:50%"></div>
<div class="percent">0%</div>
</div>
</a>
<div class="explr-selection-actions"> <div class="explr-selection-actions">
<button type="button" class="btn explr-item-edit explr-selection-entity btn-info" <button type="button" class="btn explr-item-edit explr-selection-entity btn-info"
data-entity-route="{{ url_for('slideshow_content_edit', content_id='!c!') }}"> data-entity-route="{{ url_for('slideshow_content_edit', content_id='!c!') }}">
<i class="fa fa-eye"></i> <i class="fa fa-eye"></i>
</button> </button>
<button type="button" class="btn explr-item-rename explr-selection-entity explr-selection-folder btn-info"> <button type="button"
class="btn explr-item-rename explr-selection-entity explr-selection-folder btn-info">
<i class="fa fa-pencil"></i> <i class="fa fa-pencil"></i>
</button> </button>
<button type="button" <button type="button"
class="btn explr-item-delete explr-selection-entity explr-selection-folder btn-danger-alt" class="btn explr-item-delete explr-selection-entity explr-selection-folder btn-danger-alt"
data-folder-route="{{ url_for('slideshow_content_folder_delete') }}" data-folder-route="{{ url_for('slideshow_content_folder_delete') }}?path={{ working_folder_path }}"
data-entity-route="{{ url_for('slideshow_content_delete') }}"> data-entity-route="{{ url_for('slideshow_content_delete') }}?path={{ working_folder_path }}">
<i class="fa fa-trash-alt"></i>
</button>
</div>
<div class="explr-multiselection-actions">
<button type="button"
class="btn explr-items-delete explr-multiselection-entity explr-multiselection-folder btn-danger-alt"
data-route="{{ url_for('slideshow_content_delete_bulk_explr') }}?path={{ working_folder_path }}">
<i class="fa fa-trash-alt"></i> <i class="fa fa-trash-alt"></i>
</button> </button>
</div> </div>
@ -61,13 +89,13 @@
<i class="fa fa-warning icon-left"></i> <i class="fa fa-warning icon-left"></i>
{{ l.common_folder_not_empty_error }} {{ l.common_folder_not_empty_error }}
</div> </div>
{% endif %} {% elif request.args.get('referenced_in_slide_error') %}
{% if request.args.get('referenced_in_slide_error') %}
<div class="alert alert-danger"> <div class="alert alert-danger">
<i class="fa fa-warning icon-left"></i> <i class="fa fa-warning icon-left"></i>
{{ l.slideshow_content_referenced_in_slide_error }} {{ l.slideshow_content_referenced_in_slide_error }}
</div> </div>
{% else %}
<div class="alert alert-danger hidden"></div>
{% endif %} {% endif %}
<div class="bottom-content"> <div class="bottom-content">
@ -112,64 +140,73 @@
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>
<ul class="explr-dirview"> <div class="dropzone" data-related-input="content_files">
<li class="new-folder hidden"> <ul class="explr-dirview">
<a href="javascript:void(0);"> <li class="new-folder hidden">
<i class="fa fa-folder"></i> <a href="javascript:void(0);">
<form action="{{ url_for('slideshow_content_folder_add') }}" method="POST">
<input type="hidden" name="working_folder_path" value="{{ working_folder_path }}" />
<input type="text" name="name" autocomplete="off"/>
</form>
</a>
</li>
{% set parent_path = '/'.join(working_folder_path.rstrip('/').split('/')[:-1]) %}
{% if parent_path %}
<li class="previous-folder droppable" data-path="{{ parent_path }}"
data-id="{{ working_folder.parent_id }}" data-folder="1">
<a href="{{ url_for('slideshow_content_cd', path=parent_path) }}"
class="explr-link explr-item-selectable explr-item-folder">
<i class="fa fa-folder"></i> <i class="fa fa-folder"></i>
.. <form action="{{ url_for('slideshow_content_folder_add') }}?path={{ working_folder_path }}" method="POST">
</a> <input type="text" name="name" autocomplete="off"/>
</li>
{% endif %}
{% for folder in working_folder_children %}
{% set folder_path = working_folder_path ~ '/' ~ folder.name %}
<li class="draggable droppable" data-path="{{ folder_path }}" data-id="{{ folder.id }}"
data-folder="1">
<a href="{{ url_for('slideshow_content_cd', path=folder_path) }}"
class="explr-link explr-item-selectable explr-item-actionable explr-item-folder">
<i class="fa fa-folder"></i>
<span>{{ truncate(folder.name, explr_limit_chars, '...') }}</span>
<form action="{{ url_for('slideshow_content_folder_rename') }}" method="POST">
<input type="text" name="name" value="{{ folder.name }}" autocomplete="off"/>
<input type="hidden" name="id" value="{{ folder.id }}"/>
</form> </form>
</a> </a>
</li> </li>
{% endfor %}
{% set parent_path = '/'.join(working_folder_path.rstrip('/').split('/')[:-1]) %}
{% if parent_path %}
<li class="previous-folder droppable" data-path="{{ parent_path }}"
data-id="{{ working_folder.parent_id }}" data-folder="1">
<a href="{{ url_for('slideshow_content_cd', path=parent_path) }}"
class="explr-link explr-item-selectable explr-item-folder">
<i class="fa fa-folder"></i>
..
</a>
</li>
{% endif %}
{% for folder in working_folder_children %}
{% set folder_path = working_folder_path ~ '/' ~ folder.name %}
<li class="draggable droppable" data-path="{{ folder_path }}" data-id="{{ folder.id }}"
data-folder="1">
<a href="{{ url_for('slideshow_content_cd', path=folder_path) }}"
class="explr-link explr-item-selectable explr-item-actionable explr-item-folder">
<i class="fa fa-folder"></i>
<span>{{ truncate(folder.name, explr_limit_chars, '...') }}</span>
<form action="{{ url_for('slideshow_content_folder_rename') }}?path={{ working_folder_path }}" method="POST">
<input type="text" name="name" value="{{ folder.name }}" autocomplete="off"/>
<input type="hidden" name="id" value="{{ folder.id }}"/>
</form>
</a>
</li>
{% endfor %}
{% for content in foldered_contents[working_folder.id|default(None)]|default([]) %} {% for content in foldered_contents[working_folder.id|default(None)]|default([]) %}
{% set icon = enum_content_type.get_fa_icon(content.type) %} {% set icon = enum_content_type.get_fa_icon(content.type) %}
{% set color = enum_content_type.get_color_icon(content.type) %} {% set color = enum_content_type.get_color_icon(content.type) %}
{% set thumbnail = content.type == enum_content_type.PICTURE %}
<li class="draggable" data-path="{{ working_folder_path }}" data-id="{{ content.id }}" <li class="draggable" data-path="{{ working_folder_path }}" data-id="{{ content.id }}"
data-folder="0"> data-folder="0">
<a href="{{ url_for('slideshow_content_edit', content_id=content.id) }}" <a href="{{ url_for('slideshow_content_edit', content_id=content.id) }}"
class="explr-link explr-item-selectable explr-item-actionable explr-item-entity"> class="explr-link explr-item-selectable explr-item-actionable explr-item-entity {{ 'with-thumbnail' if thumbnail }}">
<i class="fa {{ icon }} {{ color }}"></i>
<span>{{ truncate(content.name, explr_limit_chars, '...') }}</span> {% if content.type == enum_content_type.PICTURE %}
<form action="{{ url_for('slideshow_content_rename') }}" method="POST"> <div class="img-holder">
<input type="text" name="name" value="{{ content.name }}" autocomplete="off"/> <img src="/{{ content.location }}" alt=""/>
<input type="hidden" name="id" value="{{ content.id }}"/> </div>
</form> {% endif %}
</a>
</li> <i class="fa {{ icon }} {{ color }}"></i>
{% endfor %} <span>{{ truncate(content.name, explr_limit_chars, '...') }}</span>
</ul> <form action="{{ url_for('slideshow_content_rename') }}?path={{ working_folder_path }}" method="POST">
<input type="text" name="name" value="{{ content.name }}" autocomplete="off"/>
<input type="hidden" name="id" value="{{ content.id }}"/>
</form>
</a>
</li>
{% endfor %}
</ul>
</div>
</div> </div>
<div class="modals hidden"> <div class="modals hidden">