- Updated various views to replace 'dashboard-card' with 'content-card' for consistency in styling. - Modified filter and table card classes across multiple pages including applications, campaigns, commands, datasets, dayparts, displays, display groups, display profiles, fonts, layouts, libraries, menu boards, modules, player software, playlists, resolutions, schedules, settings, sync groups, tags, tasks, templates, transitions, users, and user groups.
552 lines
27 KiB
Twig
552 lines
27 KiB
Twig
{#
|
|
* Copyright (C) 2021 Xibo Signage Ltd
|
|
*
|
|
* Xibo - Digital Signage - http://www.xibo.org.uk
|
|
*
|
|
* This file is part of Xibo.
|
|
*
|
|
* Xibo is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* any later version.
|
|
*
|
|
* Xibo is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
|
#}
|
|
{% extends "authed.twig" %}
|
|
{% import "inline.twig" as inline %}
|
|
|
|
{% block title %}{{ "Playlists"|trans }} | {% endblock %}
|
|
|
|
{% block actionMenu %}{% endblock %}
|
|
|
|
{% block pageContent %}
|
|
<div class="ots-displays-page">
|
|
<div class="page-header ots-page-header">
|
|
<h1>{% trans "Playlists" %}</h1>
|
|
<p class="text-muted">{% trans "Create and manage content playlists." %}</p>
|
|
</div>
|
|
|
|
<div class="widget content-card ots-displays-card">
|
|
<div class="widget-body ots-displays-body">
|
|
<div class="XiboGrid" id="{{ random() }}" data-grid-name="playlistView">
|
|
<div class="XiboFilter card mb-3 bg-light content-card ots-filter-card">
|
|
<div class="ots-filter-header">
|
|
<h3 class="ots-filter-title">{% trans "Filter Playlists" %}</h3>
|
|
<button type="button" class="ots-filter-toggle" id="ots-filter-collapse-btn" title="{% trans 'Toggle filter panel' %}">
|
|
<i class="fa fa-chevron-up"></i>
|
|
</button>
|
|
</div>
|
|
<div class="ots-filter-content" id="ots-filter-content">
|
|
<div class="FilterDiv card-body" id="Filter">
|
|
<ul class="nav nav-tabs" role="tablist">
|
|
<li class="nav-item"><a class="nav-link active" href="#general-filter" role="tab" data-toggle="tab"><span>{% trans "General" %}</span></a></li>
|
|
<li class="nav-item"><a class="nav-link" href="#advanced-filter" role="tab" data-toggle="tab"><span>{% trans "Advanced" %}</span></a></li>
|
|
</ul>
|
|
<form class="form-inline">
|
|
<div class="tab-content">
|
|
<div class="tab-pane active" id="general-filter">
|
|
|
|
{% set title %}{% trans "Name" %}{% endset %}
|
|
{{ inline.inputNameGrid('name', title) }}
|
|
|
|
{% if currentUser.featureEnabled("tag.tagging") %}
|
|
{% set title %}{% trans "Tags" %}{% endset %}
|
|
{% set exactTagTitle %}{% trans "Exact match?" %}{% endset %}
|
|
{% set logicalOperatorTitle %}{% trans "When filtering by multiple Tags, which logical operator should be used?" %}{% endset %}
|
|
{% set helpText %}{% trans "A comma separated list of tags to filter by. Enter a tag|tag value to filter tags with values. Enter --no-tag to filter all items without tags. Enter - before a tag or tag value to exclude from results." %}{% endset %}
|
|
{{ inline.inputWithTags("tags", title, null, helpText, null, null, null, "exactTags", exactTagTitle, logicalOperatorTitle) }}
|
|
{% endif %}
|
|
|
|
{% set attributes = [
|
|
{ name: "data-live-search", value: "true" },
|
|
{ name: "data-selected-text-format", value: "count > 4" }
|
|
] %}
|
|
|
|
{% set title %}{% trans "Owner" %}{% endset %}
|
|
{% set helpText %}{% trans "Show items owned by the selected User." %}{% endset %}
|
|
{% set attributes = [
|
|
{ name: "data-width", value: "100%" },
|
|
{ name: "data-allow-clear", value: "true" },
|
|
{ name: "data-placeholder--id", value: null },
|
|
{ name: "data-placeholder--value", value: "" },
|
|
{ name: "data-search-url", value: url_for("user.search") },
|
|
{ name: "data-search-term", value: "userName" },
|
|
{ name: "data-search-term-tags", value: "tags" },
|
|
{ name: "data-id-property", value: "userId" },
|
|
{ name: "data-text-property", value: "userName" },
|
|
{ name: "data-initial-key", value: "userId" },
|
|
] %}
|
|
{{ inline.dropdown("userId", "single", title, "", null, "userId", "userName", helpText, "pagedSelect", "", "", "", attributes) }}
|
|
|
|
{% set title %}{% trans "Owner User Group" %}{% endset %}
|
|
{% set helpText %}{% trans "Show items owned by users in the selected User Group." %}{% endset %}
|
|
{% set attributes = [
|
|
{ name: "data-width", value: "100%" },
|
|
{ name: "data-allow-clear", value: "true" },
|
|
{ name: "data-placeholder--id", value: null },
|
|
{ name: "data-placeholder--value", value: "" },
|
|
{ name: "data-search-url", value: url_for("group.search") },
|
|
{ name: "data-search-term", value: "group" },
|
|
{ name: "data-id-property", value: "groupId" },
|
|
{ name: "data-text-property", value: "group" },
|
|
{ name: "data-initial-key", value: "userGroupId" },
|
|
] %}
|
|
{{ inline.dropdown("ownerUserGroupId", "single", title, "", null, "groupId", "group", helpText, "pagedSelect", "", "", "", attributes) }}
|
|
{{ inline.hidden("folderId") }}
|
|
|
|
{% set title %}{% trans "Layout ID" %}{% endset %}
|
|
{{ inline.number("layoutId", title, layoutId) }}
|
|
</div>
|
|
<div class="tab-pane" id="advanced-filter">
|
|
|
|
{% set title %}{% trans "Show" %}{% endset %}
|
|
{% set values = [{id: 1, value: "All"}, {id: 2, value: "Only Used"}, {id: 3, value: "Only Unused"}] %}
|
|
{{ inline.dropdown("playlistStatusId", "single", title, 1, values, "id", "value") }}
|
|
|
|
{% if currentUser.featureEnabled("library.view") %}
|
|
{% set title %}{% trans "Media" %}{% endset %}
|
|
{{ inline.input("mediaLike", title) }}
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid-with-folders-container ots-grid-with-folders">
|
|
<div class="grid-folder-tree-container p-3 content-card ots-folder-tree" id="grid-folder-filter">
|
|
<input id="jstree-search" class="form-control" type="text" placeholder="{% trans "Search" %}">
|
|
<div class="form-check">
|
|
<input type="checkbox" class="form-check-input" id="folder-tree-clear-selection-button">
|
|
<label class="form-check-label" for="folder-tree-clear-selection-button" title="{% trans "Search in all folders" %}">{% trans "All Folders" %}</label>
|
|
</div>
|
|
<div class="folder-search-no-results d-none">
|
|
<p>{% trans 'No Folders matching the search term' %}</p>
|
|
</div>
|
|
<div id="container-folder-tree"></div>
|
|
</div>
|
|
<div id="datatable-container">
|
|
<div class="XiboData card py-3 content-card ots-table-card">
|
|
<div class="ots-table-toolbar">
|
|
<button type="button" id="folder-tree-select-folder-button" class="btn btn-sm btn-outline-secondary ots-toolbar-btn" title="{% trans "Open / Close Folder Search options" %}"><i class="fas fa-folder"></i></button>
|
|
<div id="breadcrumbs"></div>
|
|
{% if currentUser.featureEnabled("playlist.add") %}
|
|
<button class="btn btn-sm btn-success ots-toolbar-btn XiboFormButton" title="{% trans "Add Playlist" %}" href="{{ url_for("playlist.add.form") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i></button>
|
|
{% endif %}
|
|
<button class="btn btn-sm btn-primary ots-toolbar-btn" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
|
|
</div>
|
|
<table id="playlists" class="table table-striped" data-content-type="playlist"
|
|
data-content-id-name="playlistId" data-state-preference-name="playlistGrid" style="width: 100%;">
|
|
<thead>
|
|
<tr>
|
|
<th>{% trans "ID" %}</th>
|
|
<th>{% trans "Name" %}</th>
|
|
<th>{% trans "Duration" %}</th>
|
|
{% if currentUser.featureEnabled("tag.tagging") %}<th>{% trans "Tags" %}</th>{% endif %}
|
|
<th>{% trans "Dynamic?" %}</th>
|
|
<th>{% trans "Owner" %}</th>
|
|
<th>{% trans "Sharing" %}</th>
|
|
<th>{% trans "Created" %}</th>
|
|
<th>{% trans "Modified" %}</th>
|
|
<th>{% trans "Stats?" %}</th>
|
|
<th class="rowMenu"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="dummyLayout" style="display:none"></div>
|
|
|
|
<div id="editor-container"></div>
|
|
|
|
<div class="loading-overlay">
|
|
<i class="fa fa-spinner fa-spin loading-icon"></i>
|
|
</div>
|
|
|
|
{% endblock %}
|
|
|
|
{% block javaScript %}
|
|
{# Add common files #}
|
|
{% include "editorTranslations.twig" %}
|
|
{% include "editorVars.twig" %}
|
|
|
|
<script src="{{ theme.rootUri() }}dist/playlistEditor.bundle.min.js?v={{ version }}&rev={{ revision }}" nonce="{{ cspNonce }}"></script>
|
|
<script src="{{ theme.rootUri() }}dist/codeEditor.bundle.min.js?v={{ version }}&rev={{revision}}" nonce="{{ cspNonce }}"></script>
|
|
<script src="{{ theme.rootUri() }}dist/wysiwygEditor.bundle.min.js?v={{ version }}&rev={{revision}}" nonce="{{ cspNonce }}"></script>
|
|
<script src="{{ theme.rootUri() }}dist/editorCommon.bundle.min.js?v={{ version }}&rev={{revision}}" nonce="{{ cspNonce }}"></script>
|
|
<script type="text/javascript" nonce="{{ cspNonce }}">
|
|
|
|
{# Custom translations #}
|
|
{% autoescape "js" %}
|
|
{# Insert custom translations here #}
|
|
{% endautoescape %}
|
|
|
|
var table;
|
|
$(document).ready(function () {
|
|
|
|
{% if not currentUser.featureEnabled("folder.view") %}
|
|
disableFolders();
|
|
{% endif %}
|
|
|
|
// Create ourselves a little hidden layout for preview sizing, etc
|
|
$("#dummyLayout").html('<div id="layout" data-background-color="#000000" style="background-color: #000000" designer_scale="1"><div id="region_-1" zindex="1" tip_scale="1" designer_scale="1" width="800" height="450"></div></div>');
|
|
|
|
// Configure the DataTable
|
|
table = $("#playlists").DataTable({
|
|
"language": dataTablesLanguage,
|
|
dom: dataTablesTemplate,
|
|
"lengthMenu": [10, 25, 50, 100, 250, 500],
|
|
serverSide: true,
|
|
stateSave: true,
|
|
responsive: true,
|
|
stateLoadCallback: dataTableStateLoadCallback,
|
|
stateSaveCallback: dataTableStateSaveCallback,
|
|
filter: false,
|
|
searchDelay: 3000,
|
|
"order": [[1, "asc"]],
|
|
ajax: {
|
|
url: "{{ url_for("playlist.search") }}",
|
|
"data": function (d) {
|
|
$.extend(d, $("#playlists").closest(".XiboGrid").find(".FilterDiv form").serializeObject());
|
|
}
|
|
},
|
|
"columns": [
|
|
{"data": "playlistId", responsivePriority: 2},
|
|
{
|
|
"data": "name",
|
|
responsivePriority: 3,
|
|
"render": dataTableSpacingPreformatted
|
|
},
|
|
{
|
|
"data": "duration",
|
|
responsivePriority: 3,
|
|
"render": function (data, type, row) {
|
|
if (type !== "display" && type !== "export")
|
|
return data;
|
|
|
|
if (row.requiresDurationUpdate === 1) {
|
|
return '<span class="fa fa-clock-o" title="{{ "Changes have been made and we are recalculating this Playlists duration" }}"></span>';
|
|
} else if (row.requiresDurationUpdate !== 0) {
|
|
return moment().startOf("day").seconds(data).format("H:mm:ss") + ' <span class="fa fa-clock-o" title="{{ "This duration will be updated at " }}' + moment(row.requiresDurationUpdate, "X").format(jsDateFormat) + '"></span>';
|
|
}
|
|
|
|
return dataTableTimeFromSeconds(data, type, row);
|
|
}
|
|
},
|
|
{% if currentUser.featureEnabled("tag.tagging") %}{
|
|
"sortable": false,
|
|
"visible": false,
|
|
responsivePriority: 4,
|
|
"data": dataTableCreateTags
|
|
},{% endif %}
|
|
{"data": "isDynamic", "render": dataTableTickCrossColumn, responsivePriority: 4},
|
|
{"data": "owner", responsivePriority: 4},
|
|
{
|
|
"data": "groupsWithPermissions",
|
|
responsivePriority: 5,
|
|
"render": dataTableCreatePermissions
|
|
},
|
|
{
|
|
"data": "createdDt",
|
|
responsivePriority: 6,
|
|
"render": dataTableDateFromIso,
|
|
"visible": false
|
|
},
|
|
{
|
|
"data": "modifiedDt",
|
|
responsivePriority: 6,
|
|
"render": dataTableDateFromIso,
|
|
"visible": false
|
|
},
|
|
{
|
|
"name": "enableStat",
|
|
responsivePriority: 6,
|
|
"data": function (data) {
|
|
|
|
var icon = "";
|
|
if (data.enableStat == 'On')
|
|
icon = "fa-check";
|
|
else if (data.enableStat == 'Off')
|
|
icon = "fa-times";
|
|
else
|
|
icon = "fa-level-down";
|
|
|
|
return '<span class="fa ' + icon + '" title="' + (data.enableStatDescription) + '"></span>';
|
|
}
|
|
},
|
|
{
|
|
"orderable": false,
|
|
responsivePriority: 1,
|
|
"data": dataTableButtonsColumn
|
|
}
|
|
]
|
|
});
|
|
|
|
table.on('draw', dataTableDraw);
|
|
table.on('draw', {form: $("#playlists").closest(".XiboGrid").find(".FilterDiv form")}, dataTableCreateTagEvents);
|
|
table.on('processing.dt', dataTableProcessing);
|
|
dataTableAddButtons(table, $('#playlists_wrapper').find('.dataTables_buttons'));
|
|
|
|
$("#refreshGrid").click(function () {
|
|
table.ajax.reload();
|
|
});
|
|
});
|
|
|
|
// Playlist Add Form
|
|
// contains a grid on the populate tab
|
|
// hook up the grid
|
|
var mediaTable;
|
|
var nameFilter;
|
|
var tagFilter;
|
|
var exactTags;
|
|
var logicalOperator;
|
|
var logicalOperatorName;
|
|
var filterFolderId;
|
|
|
|
function playlistEditorFormOpen(formData) {
|
|
|
|
// Clear container
|
|
$('#editor-container').empty();
|
|
|
|
// Append form
|
|
$('#editor-container').append(formData.message);
|
|
}
|
|
|
|
function playlistFormOpen(dialog) {
|
|
mediaTable = null;
|
|
|
|
$(dialog).find("input[name=filterMediaName]").on("keyup", _.debounce(function () {
|
|
playlistFormPopulateMediaTable(dialog);
|
|
}, 500));
|
|
|
|
$(dialog).find("input[name=filterMediaTag], input[name=exactTags], select[name=logicalOperator], select[name=logicalOperatorName], select[name=filterFolderId]").on("change", function () {
|
|
playlistFormPopulateMediaTable(dialog);
|
|
});
|
|
|
|
// First time in there
|
|
playlistFormPopulateMediaTable(dialog);
|
|
|
|
// Run function to set the form submit behaviour
|
|
playlistAddFormOpen();
|
|
}
|
|
|
|
///
|
|
/// Playlist Usage Form
|
|
///
|
|
function usageFormOpen(dialog) {
|
|
// Displays tab
|
|
var usageTable = $("#usageReportTable").DataTable({
|
|
"language": dataTablesLanguage,
|
|
serverSide: true,
|
|
stateSave: true, stateDuration: 0,
|
|
filter: false,
|
|
searchDelay: 3000,
|
|
responsive: true,
|
|
"order": [[1, "asc"]],
|
|
ajax: {
|
|
"url": "{{ url_for("playlist.usage", {id:':id'}) }}".replace(":id", $("#usageReportTable").data().playlistId),
|
|
"data": function (dataDisplay) {
|
|
$.extend(dataDisplay, $(dialog).find("#usageReportForm").serializeObject());
|
|
return dataDisplay;
|
|
}
|
|
},
|
|
"columns": [
|
|
{"data": "displayId"},
|
|
{"data": "display"},
|
|
{"data": "description"}
|
|
]
|
|
});
|
|
|
|
usageTable.on('draw', dataTableDraw);
|
|
usageTable.on('processing.dt', dataTableProcessing);
|
|
|
|
// Layouts tab
|
|
var usageTableLayouts = $("#usageReportLayoutsTable").DataTable({
|
|
"language": dataTablesLanguage,
|
|
serverSide: true,
|
|
stateSave: true, stateDuration: 0,
|
|
filter: false,
|
|
searchDelay: 3000,
|
|
responsive: true,
|
|
"order": [[1, "asc"]],
|
|
ajax: {
|
|
"url": "{{ url_for("playlist.usage.layouts", {id:':id'}) }}".replace(":id", $("#usageReportLayoutsTable").data().playlistId)
|
|
},
|
|
"columns": [
|
|
{"data": "layoutId"},
|
|
{"data": "layout"},
|
|
{"data": "description"},
|
|
{
|
|
"orderable": false,
|
|
"data": dataTableButtonsColumn
|
|
}
|
|
]
|
|
});
|
|
|
|
usageTableLayouts.on('draw', dataTableDraw);
|
|
usageTableLayouts.on('processing.dt', dataTableProcessing);
|
|
}
|
|
|
|
function playlistFormPopulateMediaTable(dialog) {
|
|
nameFilter = $(dialog).find("input[name=filterMediaName]").val();
|
|
tagFilter = $(dialog).find("input[name=filterMediaTag]").val();
|
|
exactTags = $(dialog).find("input[name=exactTags]").is(':checked')
|
|
logicalOperator = $(dialog).find("select[name=logicalOperator]").val();
|
|
logicalOperatorName = $(dialog).find("select[name=logicalOperatorName]").val();
|
|
filterFolderId = $(dialog).find("select[name=filterFolderId]").val() ?? "";
|
|
|
|
if (nameFilter === "" && tagFilter === "" && filterFolderId === "") {
|
|
if (mediaTable != null) {
|
|
mediaTable.destroy();
|
|
mediaTable = null;
|
|
$("#playlistLibraryMedia tbody").empty();
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (mediaTable != null) {
|
|
mediaTable.ajax.reload();
|
|
} else {
|
|
mediaTable = $("#playlistLibraryMedia").DataTable({
|
|
"language": dataTablesLanguage,
|
|
serverSide: true,
|
|
stateSave: true,
|
|
stateDuration: 0,
|
|
filter: false,
|
|
responsive: true,
|
|
searchDelay: 3000,
|
|
"order": [[1, "asc"]],
|
|
ajax: {
|
|
"url": "{{ url_for("library.search") }}",
|
|
"data": function (d) {
|
|
$.extend(
|
|
d,
|
|
{
|
|
media: nameFilter,
|
|
tags: tagFilter,
|
|
folderId: filterFolderId,
|
|
assignable: 1,
|
|
exactTags: exactTags,
|
|
logicalOperator: logicalOperator,
|
|
logicalOperatorName: logicalOperatorName
|
|
}
|
|
);
|
|
}
|
|
},
|
|
"columns": [
|
|
{"data": "mediaId"},
|
|
{"data": "name"},
|
|
{"data": "mediaType"},
|
|
{% if currentUser.featureEnabled("tag.tagging") %}{"data": dataTableCreateTags},{% endif %}
|
|
{
|
|
"name": "duration",
|
|
"data": function (data, type) {
|
|
if (type !== "display")
|
|
return data.duration;
|
|
|
|
return moment().startOf("day").seconds(data.duration).format("H:mm:ss");
|
|
}
|
|
}
|
|
]
|
|
});
|
|
|
|
mediaTable.on('processing.dt', dataTableProcessing);
|
|
mediaTable.on('draw', {form: $(".playlistForm")}, dataTableCreateTagEvents);
|
|
}
|
|
}
|
|
|
|
function setEnableStatMultiSelectFormOpen(dialog) {
|
|
|
|
var $select = $('<select id="enableStat" name="enableStat" class="form-control">' +
|
|
'<option value="Off">{% trans %} Off {% endtrans %}</option>' +
|
|
'<option value="On">{% trans %} On {% endtrans %}</option>' +
|
|
'<option value="Inherit">{% trans %} Inherit {% endtrans %}</option>' +
|
|
'</select>');
|
|
|
|
$select.on('change', function () {
|
|
dialog.data().commitData = {enableStat: $(this).val()};
|
|
}).trigger('change');
|
|
|
|
$(dialog).find('.modal-body').append($select);
|
|
}
|
|
|
|
function playlistAddFormOpen() {
|
|
$("#playlistAddForm").off("submit").submit(function (e) {
|
|
e.preventDefault();
|
|
var form = $(this);
|
|
|
|
$.ajax({
|
|
type: $(this).attr("method"),
|
|
url: $(this).attr("action"),
|
|
data: $(this).serialize(),
|
|
cache: false,
|
|
dataType: "json",
|
|
success: function (xhr, textStatus, error) {
|
|
|
|
XiboSubmitResponse(xhr, form);
|
|
|
|
if (xhr.success && xhr.data.isDynamic == 0) {
|
|
|
|
// Open the editor
|
|
openPlaylistEditorForm(xhr.id);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
function openPlaylistEditorForm(playlistId) {
|
|
var requestPath = playlistEditorUrl;
|
|
|
|
// replace id if necessary/exists
|
|
requestPath = requestPath.replace(':id', playlistId);
|
|
|
|
$.ajax({
|
|
url: requestPath,
|
|
type: 'GET'
|
|
}).done(function (res) {
|
|
|
|
if (!res.success) {
|
|
// Login Form needed?
|
|
if (res.login) {
|
|
window.location.reload();
|
|
} else {
|
|
// Just an error we dont know about
|
|
if (res.message == undefined) {
|
|
console.error(res);
|
|
} else {
|
|
console.error(res.message);
|
|
}
|
|
}
|
|
} else {
|
|
// Clear container
|
|
$('#editor-container').empty();
|
|
|
|
// Append form
|
|
$('#editor-container').append(res.html);
|
|
}
|
|
}).fail(function (jqXHR, textStatus, errorThrown) {
|
|
// Output error to console
|
|
console.error(jqXHR, textStatus, errorThrown);
|
|
});
|
|
}
|
|
</script>
|
|
{% endblock %}
|