801 lines
30 KiB
Twig
801 lines
30 KiB
Twig
{#
|
||
/**
|
||
* OTS Signage — Modern Upload Media Modal
|
||
* Replaces the core Xibo include-file-upload.twig with a redesigned,
|
||
* drag-and-drop, multi-file upload experience.
|
||
*
|
||
* Reuses the existing openUploadForm(options) API so every page
|
||
* (library, layout, fonts, player software, dataset, etc.) keeps working
|
||
* without any caller changes.
|
||
*
|
||
* Dependencies already present in Xibo: jQuery, jQuery UI, jQuery File Upload,
|
||
* Bootstrap 4 modal, moment.js.
|
||
*/
|
||
#}
|
||
|
||
{# ── Upload Modal Markup ────────────────────────────────────────────────── #}
|
||
<div class="modal fade ots-upload-modal" id="ots-upload-modal" tabindex="-1"
|
||
role="dialog" aria-labelledby="ots-upload-modal-title" aria-modal="true">
|
||
<div class="modal-dialog modal-lg modal-dialog-centered" role="document">
|
||
<div class="modal-content ots-upload-content">
|
||
|
||
{# Header #}
|
||
<div class="modal-header ots-upload-header">
|
||
<h5 class="modal-title ots-upload-title" id="ots-upload-modal-title"></h5>
|
||
<button type="button" class="ots-upload-close" data-dismiss="modal" aria-label="Close">
|
||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M15 5L5 15M5 5l10 10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
|
||
</button>
|
||
</div>
|
||
|
||
{# Body #}
|
||
<div class="modal-body ots-upload-body">
|
||
|
||
{# Tab switcher: File / URL #}
|
||
<div class="ots-upload-tabs" id="ots-upload-tabs">
|
||
<button type="button" class="ots-upload-tab active" data-tab="file" id="ots-tab-file">
|
||
<i class="fas fa-file-upload"></i> File
|
||
</button>
|
||
<button type="button" class="ots-upload-tab" data-tab="url" id="ots-tab-url">
|
||
<i class="fas fa-link"></i> URL
|
||
</button>
|
||
</div>
|
||
|
||
{# ── FILE TAB ──────────────────────────────────────────────── #}
|
||
<div class="ots-upload-tab-content" id="ots-upload-tab-file">
|
||
|
||
{# Folder selector row – shown only when options.folderSelector is true #}
|
||
<div class="ots-upload-folder-row d-none" id="ots-upload-folder-row">
|
||
<span class="ots-upload-folder-label" id="ots-upload-folder-label"></span>
|
||
<button type="button" class="btn btn-sm ots-upload-folder-btn" id="ots-upload-folder-btn" title="">
|
||
<i class="fas fa-folder-open"></i> <span id="ots-upload-folder-text"></span>
|
||
</button>
|
||
</div>
|
||
|
||
{# Max file size notice #}
|
||
<div class="ots-upload-notice d-none" id="ots-upload-size-notice"></div>
|
||
|
||
{# Drop-zone #}
|
||
<form id="ots-upload-form" enctype="multipart/form-data" method="POST">
|
||
<div class="ots-upload-dropzone" id="ots-upload-dropzone" role="button" tabindex="0"
|
||
aria-label="Drag and drop files here or click to browse">
|
||
<div class="ots-upload-dropzone-inner">
|
||
<div class="ots-upload-dropzone-icon">
|
||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none">
|
||
<rect x="4" y="8" width="40" height="32" rx="6" stroke="currentColor" stroke-width="2" fill="none"/>
|
||
<path d="M24 30V18m0 0l-6 6m6-6l6 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||
</svg>
|
||
</div>
|
||
<p class="ots-upload-dropzone-text">
|
||
<strong id="ots-upload-drop-label">Drop files here</strong><br>
|
||
<span class="ots-upload-dropzone-sub">or <span class="ots-upload-browse-link">browse your computer</span></span>
|
||
</p>
|
||
</div>
|
||
</div>
|
||
{# File input lives outside the dropzone to avoid click-event loops #}
|
||
<input type="file" id="ots-upload-input" name="files[]" multiple class="ots-upload-input-hidden" />
|
||
</form>
|
||
|
||
{# Valid extensions badge #}
|
||
<div class="ots-upload-ext-info d-none" id="ots-upload-ext-info"></div>
|
||
|
||
{# Options row (update in layouts / delete old revisions) #}
|
||
<div class="ots-upload-options d-none" id="ots-upload-options"></div>
|
||
|
||
{# File list / queue #}
|
||
<div class="ots-upload-queue d-none" id="ots-upload-queue">
|
||
<div class="ots-upload-queue-header">
|
||
<span class="ots-upload-queue-title">Files</span>
|
||
<span class="ots-upload-queue-count" id="ots-upload-queue-count"></span>
|
||
</div>
|
||
<ul class="ots-upload-file-list" id="ots-upload-file-list"></ul>
|
||
</div>
|
||
|
||
</div>{# /ots-upload-tab-file #}
|
||
|
||
{# ── URL TAB ───────────────────────────────────────────────── #}
|
||
<div class="ots-upload-tab-content d-none" id="ots-upload-tab-url">
|
||
<div class="ots-upload-url-section">
|
||
<div class="ots-upload-url-icon">
|
||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none">
|
||
<path d="M17 23l6-6m-3.5.5a5 5 0 017.07 0l1.42 1.42a5 5 0 010 7.07l-2.83 2.83a5 5 0 01-7.07 0" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||
<path d="M23 17l-6 6m3.5-.5a5 5 0 00-7.07 0l-1.42-1.42a5 5 0 010-7.07l2.83-2.83a5 5 0 017.07 0" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||
</svg>
|
||
</div>
|
||
<p class="ots-upload-url-desc">Add media from an external URL</p>
|
||
<div class="ots-upload-url-fields">
|
||
<div class="ots-upload-url-field">
|
||
<label for="ots-upload-url-input">URL</label>
|
||
<input type="url" id="ots-upload-url-input" class="form-control ots-upload-url-input"
|
||
placeholder="https://example.com/image.jpg" autocomplete="off" />
|
||
</div>
|
||
<button type="button" class="btn ots-upload-btn-start ots-upload-url-add" id="ots-upload-url-add">
|
||
<i class="fas fa-plus"></i> Add to queue
|
||
</button>
|
||
</div>
|
||
{# URL queue list #}
|
||
<div class="ots-upload-queue d-none" id="ots-upload-url-queue">
|
||
<div class="ots-upload-queue-header">
|
||
<span class="ots-upload-queue-title">URLs</span>
|
||
<span class="ots-upload-queue-count" id="ots-upload-url-queue-count"></span>
|
||
</div>
|
||
<ul class="ots-upload-file-list" id="ots-upload-url-list"></ul>
|
||
</div>
|
||
</div>
|
||
</div>{# /ots-upload-tab-url #}
|
||
|
||
</div>
|
||
|
||
{# Footer #}
|
||
<div class="modal-footer ots-upload-footer">
|
||
<button type="button" class="btn ots-upload-btn-cancel" data-dismiss="modal" id="ots-upload-btn-cancel">Cancel</button>
|
||
<button type="button" class="btn ots-upload-btn-start d-none" id="ots-upload-btn-start">
|
||
<i class="fas fa-cloud-upload-alt"></i> <span id="ots-upload-btn-start-label">Start upload</span>
|
||
</button>
|
||
<button type="button" class="btn ots-upload-btn-done d-none" id="ots-upload-btn-done">Done</button>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{# ── Upload JavaScript ──────────────────────────────────────────────────── #}
|
||
<script type="text/javascript" nonce="{{ cspNonce }}">
|
||
/**
|
||
* openUploadForm(options)
|
||
* Drop-in replacement for the core Xibo openUploadForm.
|
||
* Keeps the same options API so existing page callers (library, layout, fonts,
|
||
* player-software, dataset) work without modification.
|
||
*
|
||
* Options shape (all optional except url):
|
||
* {
|
||
* url: String – POST endpoint
|
||
* title: String – modal title
|
||
* initialisedBy: String – an identifier for the caller
|
||
* buttons: {
|
||
* main: { label, className, callback }
|
||
* },
|
||
* templateOptions: {
|
||
* multi: Boolean – allow multiple files (default true)
|
||
* trans: { addFiles, startUpload, cancelUpload, selectFolder, ... },
|
||
* upload: { maxSize, maxSizeMessage, validExt, validExtensionsMessage },
|
||
* folderSelector: Boolean,
|
||
* currentWorkingFolderId: Number,
|
||
* oldMediaId: Number – when replacing a media item
|
||
* oldFolderId: Number,
|
||
* updateInAllChecked: Boolean,
|
||
* deleteOldRevisionsChecked: Boolean,
|
||
* },
|
||
* uploadDoneEvent: Function – called when all uploads finish
|
||
* }
|
||
*/
|
||
window.openUploadForm = function openUploadForm(options) {
|
||
'use strict';
|
||
|
||
options = options || {};
|
||
var tOpts = options.templateOptions || {};
|
||
var trans = tOpts.trans || {};
|
||
var upload = tOpts.upload || {};
|
||
var multi = tOpts.multi !== false;
|
||
|
||
// ── References ──
|
||
var $modal = $('#ots-upload-modal');
|
||
var $title = $('#ots-upload-modal-title');
|
||
var $dropzone = $('#ots-upload-dropzone');
|
||
var $form = $('#ots-upload-form');
|
||
var $input = $('#ots-upload-input');
|
||
var $queue = $('#ots-upload-queue');
|
||
var $fileList = $('#ots-upload-file-list');
|
||
var $queueCount= $('#ots-upload-queue-count');
|
||
var $btnStart = $('#ots-upload-btn-start');
|
||
var $btnDone = $('#ots-upload-btn-done');
|
||
var $btnCancel = $('#ots-upload-btn-cancel');
|
||
var $startLabel= $('#ots-upload-btn-start-label');
|
||
var $folderRow = $('#ots-upload-folder-row');
|
||
var $sizeNotice= $('#ots-upload-size-notice');
|
||
var $extInfo = $('#ots-upload-ext-info');
|
||
var $optionsRow= $('#ots-upload-options');
|
||
var $dropLabel = $('#ots-upload-drop-label');
|
||
|
||
// ── Extra references for URL tab ──
|
||
var $tabFile = $('#ots-tab-file');
|
||
var $tabUrl = $('#ots-tab-url');
|
||
var $panelFile = $('#ots-upload-tab-file');
|
||
var $panelUrl = $('#ots-upload-tab-url');
|
||
var $urlInput = $('#ots-upload-url-input');
|
||
var $urlAddBtn = $('#ots-upload-url-add');
|
||
var $urlQueue = $('#ots-upload-url-queue');
|
||
var $urlList = $('#ots-upload-url-list');
|
||
var $urlCount = $('#ots-upload-url-queue-count');
|
||
var urlQueue = []; // { url, id, status, $el, xhr }
|
||
|
||
// ── Reset state ──
|
||
$fileList.empty();
|
||
$urlList.empty();
|
||
$queue.addClass('d-none');
|
||
$urlQueue.addClass('d-none');
|
||
$btnStart.addClass('d-none');
|
||
$btnDone.addClass('d-none');
|
||
$folderRow.addClass('d-none');
|
||
$sizeNotice.addClass('d-none');
|
||
$extInfo.addClass('d-none');
|
||
$optionsRow.addClass('d-none').empty();
|
||
$input.val('');
|
||
$urlInput.val('');
|
||
$dropzone.removeClass('ots-upload-dropzone--over ots-upload-dropzone--has-files');
|
||
|
||
// Reset to file tab
|
||
$tabFile.addClass('active');
|
||
$tabUrl.removeClass('active');
|
||
$panelFile.removeClass('d-none');
|
||
$panelUrl.addClass('d-none');
|
||
|
||
// ── Populate UI from options ──
|
||
$title.text(options.title || 'Upload');
|
||
$startLabel.text(trans.startUpload || 'Start upload');
|
||
$btnCancel.text(trans.cancelUpload || 'Cancel');
|
||
$dropLabel.text(trans.addFiles || 'Drop files here');
|
||
|
||
if (!multi) {
|
||
$input.removeAttr('multiple');
|
||
} else {
|
||
$input.attr('multiple', 'multiple');
|
||
}
|
||
|
||
// Max file size notice
|
||
if (upload.maxSizeMessage) {
|
||
$sizeNotice.text(upload.maxSizeMessage).removeClass('d-none');
|
||
}
|
||
|
||
// Valid extensions
|
||
if (upload.validExt) {
|
||
var extList = upload.validExt.replace(/\|/g, ', ');
|
||
var extMsg = upload.validExtensionsMessage || ('Allowed: ' + extList);
|
||
$extInfo.text(extMsg).removeClass('d-none');
|
||
}
|
||
|
||
// Folder selector
|
||
if (tOpts.folderSelector) {
|
||
$folderRow.removeClass('d-none');
|
||
$('#ots-upload-folder-label').text((trans.selectedFolder || 'Current Folder:'));
|
||
$('#ots-upload-folder-text').text(trans.selectFolder || 'Select Folder');
|
||
$('#ots-upload-folder-btn').attr('title', trans.selectFolderTitle || 'Change folder');
|
||
|
||
// Wire folder-selector button using the CMS's built-in folder-tree modal
|
||
// (templates['folder-tree'], initJsTreeAjax — provided by the Xibo core)
|
||
$('#ots-upload-folder-btn').off('click').on('click', function() {
|
||
var modalId = 'ots-upload-folder-tree-modal';
|
||
var containerId = 'ots-upload-folder-form-tree';
|
||
var $ftModal = $('#' + modalId);
|
||
|
||
// ── First open: build the modal from the Handlebars template ──
|
||
if ($ftModal.length === 0 && typeof templates !== 'undefined' && templates['folder-tree']) {
|
||
var folderTreeTpl = templates['folder-tree'];
|
||
var treeConfig = {
|
||
container: containerId,
|
||
modal: modalId
|
||
};
|
||
if (typeof translations !== 'undefined' && translations.folderTree) {
|
||
treeConfig.trans = translations.folderTree;
|
||
}
|
||
$('body').append(folderTreeTpl(treeConfig));
|
||
$ftModal = $('#' + modalId);
|
||
|
||
// Inject OK / Cancel footer
|
||
var $footer = $ftModal.find('.modal-footer');
|
||
if ($footer.length === 0) {
|
||
$footer = $('<div class="modal-footer"></div>');
|
||
$ftModal.find('.modal-content').append($footer);
|
||
}
|
||
$footer.empty().append(
|
||
'<button type="button" class="btn btn-sm ots-upload-btn-cancel" data-dismiss="modal">Cancel</button>' +
|
||
'<button type="button" class="btn btn-sm ots-upload-btn-start" id="ots-folder-confirm-btn">' +
|
||
'<i class="fas fa-check"></i> OK' +
|
||
'</button>'
|
||
);
|
||
|
||
// Configure as static backdrop once
|
||
$ftModal.modal({ backdrop: 'static', keyboard: true, show: false });
|
||
|
||
// Fix stacked-modal body class when this modal closes
|
||
$ftModal.on('hidden.bs.modal', function() {
|
||
if ($('.modal:visible').length) {
|
||
$(document.body).addClass('modal-open');
|
||
}
|
||
});
|
||
}
|
||
|
||
if ($ftModal.length === 0) {
|
||
console.warn('Folder tree template not available');
|
||
return;
|
||
}
|
||
|
||
// ── Every open: reset pending selection and re-init jstree ──
|
||
var pendingFolderId = tOpts.currentWorkingFolderId || null;
|
||
var pendingFolderName = null;
|
||
|
||
// Destroy previous jstree instance so it re-initialises cleanly
|
||
var $treeContainer = $ftModal.find('#' + containerId);
|
||
if ($treeContainer.jstree && $treeContainer.jstree(true)) {
|
||
try { $treeContainer.jstree('destroy'); } catch(e) {}
|
||
}
|
||
|
||
// Initialise jstree
|
||
if (typeof initJsTreeAjax === 'function') {
|
||
initJsTreeAjax($treeContainer, 'ots-upload-form', true, 600);
|
||
}
|
||
|
||
// Show the modal (works on first and subsequent opens)
|
||
$ftModal.modal('show');
|
||
|
||
// Bind selection handler after the modal is visible + jstree auto-select settles
|
||
$ftModal.off('shown.bs.modal.otsUpload').on('shown.bs.modal.otsUpload', function() {
|
||
setTimeout(function() {
|
||
$treeContainer.off('select_node.jstree.otsUpload')
|
||
.on('select_node.jstree.otsUpload', function(e, data) {
|
||
if (data && data.node) {
|
||
pendingFolderId = data.node.id;
|
||
pendingFolderName = data.node.text || data.node.id;
|
||
}
|
||
});
|
||
}, 500);
|
||
});
|
||
|
||
// OK button — apply selection and close
|
||
$ftModal.find('#ots-folder-confirm-btn').off('click').on('click', function() {
|
||
if (pendingFolderId) {
|
||
tOpts.currentWorkingFolderId = pendingFolderId;
|
||
$('#ots-upload-folder-text').text(pendingFolderName || pendingFolderId);
|
||
}
|
||
$ftModal.modal('hide');
|
||
});
|
||
});
|
||
}
|
||
|
||
// Done button
|
||
var mainBtn = (options.buttons && options.buttons.main) || {};
|
||
$btnDone.text(mainBtn.label || 'Done');
|
||
if (mainBtn.className) {
|
||
$btnDone.attr('class', 'btn ots-upload-btn-done d-none ' + mainBtn.className);
|
||
}
|
||
|
||
// ── Internal state ──
|
||
var fileQueue = []; // { file, id, status, $el, xhr }
|
||
var nextId = 0;
|
||
var uploading = false;
|
||
var uploadCount = 0;
|
||
var successCount= 0;
|
||
|
||
// ── Helper: human-readable size ──
|
||
function humanSize(bytes) {
|
||
if (bytes < 1024) return bytes + ' B';
|
||
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
|
||
return (bytes / 1048576).toFixed(1) + ' MB';
|
||
}
|
||
|
||
// ── Helper: valid extension check ──
|
||
function isExtAllowed(filename) {
|
||
if (!upload.validExt) return true;
|
||
var ext = filename.split('.').pop().toLowerCase();
|
||
var allowed = upload.validExt.toLowerCase().split('|');
|
||
return allowed.indexOf(ext) !== -1;
|
||
}
|
||
|
||
// ── Helper: generate preview (images only) ──
|
||
function generatePreview(file, $thumb) {
|
||
if (file.type && file.type.indexOf('image/') === 0 && file.size < 10 * 1048576) {
|
||
var reader = new FileReader();
|
||
reader.onload = function(e) {
|
||
$thumb.css('background-image', 'url(' + e.target.result + ')').addClass('has-preview');
|
||
};
|
||
reader.readAsDataURL(file);
|
||
} else {
|
||
// Icon based on type
|
||
var icon = 'fa-file';
|
||
if (file.type && file.type.indexOf('video/') === 0) icon = 'fa-file-video';
|
||
else if (file.type && file.type.indexOf('audio/') === 0) icon = 'fa-file-audio';
|
||
else if (file.type && file.type.indexOf('application/pdf') === 0) icon = 'fa-file-pdf';
|
||
else if (file.name && /\.(xlsx?|csv)$/i.test(file.name)) icon = 'fa-file-excel';
|
||
$thumb.html('<i class="fas ' + icon + '"></i>');
|
||
}
|
||
}
|
||
|
||
// ── Add files to queue ──
|
||
function addFiles(files) {
|
||
for (var i = 0; i < files.length; i++) {
|
||
var file = files[i];
|
||
|
||
// Multi check
|
||
if (!multi && fileQueue.length >= 1) {
|
||
// Replace existing file
|
||
fileQueue = [];
|
||
$fileList.empty();
|
||
}
|
||
|
||
var id = nextId++;
|
||
var extOk = isExtAllowed(file.name);
|
||
var sizeOk = !upload.maxSize || file.size <= upload.maxSize;
|
||
|
||
var statusClass = '';
|
||
var statusText = humanSize(file.size);
|
||
if (!extOk) { statusClass = 'ots-upload-file--error'; statusText = 'Invalid file type'; }
|
||
else if (!sizeOk) { statusClass = 'ots-upload-file--error'; statusText = 'File too large'; }
|
||
|
||
var $el = $(
|
||
'<li class="ots-upload-file-item ' + statusClass + '" data-id="' + id + '">' +
|
||
'<div class="ots-upload-file-thumb"></div>' +
|
||
'<div class="ots-upload-file-info">' +
|
||
'<span class="ots-upload-file-name">' + $('<span>').text(file.name).html() + '</span>' +
|
||
'<span class="ots-upload-file-meta">' + statusText + '</span>' +
|
||
'<div class="ots-upload-file-progress"><div class="ots-upload-file-progress-bar"></div></div>' +
|
||
'</div>' +
|
||
'<button type="button" class="ots-upload-file-remove" aria-label="Remove" title="Remove">' +
|
||
'<i class="fas fa-times"></i>' +
|
||
'</button>' +
|
||
'</li>'
|
||
);
|
||
|
||
generatePreview(file, $el.find('.ots-upload-file-thumb'));
|
||
|
||
$el.find('.ots-upload-file-remove').on('click', (function(fileId) {
|
||
return function() { removeFile(fileId); };
|
||
})(id));
|
||
|
||
$fileList.append($el);
|
||
|
||
fileQueue.push({
|
||
file: file,
|
||
id: id,
|
||
status: (extOk && sizeOk) ? 'pending' : 'error',
|
||
$el: $el,
|
||
xhr: null
|
||
});
|
||
}
|
||
|
||
updateQueueUI();
|
||
}
|
||
|
||
// ── Remove file ──
|
||
function removeFile(id) {
|
||
fileQueue = fileQueue.filter(function(f) {
|
||
if (f.id === id) {
|
||
f.$el.slideUp(200, function() { $(this).remove(); });
|
||
if (f.xhr) { try { f.xhr.abort(); } catch(e){} }
|
||
return false;
|
||
}
|
||
return true;
|
||
});
|
||
updateQueueUI();
|
||
}
|
||
|
||
// ── Queue UI update ──
|
||
function updateQueueUI() {
|
||
var validFiles = fileQueue.filter(function(f) { return f.status === 'pending'; });
|
||
var total = fileQueue.length;
|
||
$queueCount.text(total + ' file' + (total !== 1 ? 's' : ''));
|
||
if (total > 0) {
|
||
$queue.removeClass('d-none');
|
||
$dropzone.addClass('ots-upload-dropzone--has-files');
|
||
} else {
|
||
$queue.addClass('d-none');
|
||
$dropzone.removeClass('ots-upload-dropzone--has-files');
|
||
}
|
||
// Show start button only when there are valid pending files and not already uploading
|
||
if (validFiles.length > 0 && !uploading) {
|
||
$btnStart.removeClass('d-none');
|
||
} else if (!uploading) {
|
||
$btnStart.addClass('d-none');
|
||
}
|
||
}
|
||
|
||
// ── Upload all pending items (files + URLs) ──
|
||
function startUpload() {
|
||
var filePending = fileQueue.filter(function(f) { return f.status === 'pending'; });
|
||
var urlPending = urlQueue.filter(function(f) { return f.status === 'pending'; });
|
||
var allPending = filePending.concat(urlPending);
|
||
if (allPending.length === 0) return;
|
||
uploading = true;
|
||
uploadCount = allPending.length;
|
||
successCount = 0;
|
||
|
||
$btnStart.addClass('d-none');
|
||
$btnCancel.text(trans.cancelUpload || 'Cancel');
|
||
|
||
// Upload sequentially
|
||
var idx = 0;
|
||
function uploadNext() {
|
||
if (idx >= allPending.length) {
|
||
uploading = false;
|
||
onAllDone();
|
||
return;
|
||
}
|
||
var item = allPending[idx++];
|
||
if (item.file) {
|
||
uploadSingle(item, uploadNext);
|
||
} else if (item.url) {
|
||
uploadUrlItem(item, uploadNext);
|
||
} else {
|
||
uploadNext();
|
||
}
|
||
}
|
||
uploadNext();
|
||
}
|
||
|
||
// ── Upload a single file ──
|
||
function uploadSingle(item, callback) {
|
||
item.status = 'uploading';
|
||
item.$el.addClass('ots-upload-file--uploading');
|
||
|
||
var formData = new FormData();
|
||
formData.append('files[]', item.file, item.file.name);
|
||
|
||
// Standard Xibo hidden fields
|
||
if (tOpts.currentWorkingFolderId) formData.append('folderId', tOpts.currentWorkingFolderId);
|
||
if (tOpts.oldMediaId) formData.append('oldMediaId', tOpts.oldMediaId);
|
||
if (tOpts.oldFolderId) formData.append('oldFolderId', tOpts.oldFolderId);
|
||
|
||
// Checkboxes
|
||
$optionsRow.find('input[type="checkbox"]').each(function() {
|
||
formData.append($(this).attr('name'), $(this).is(':checked') ? '1' : '0');
|
||
});
|
||
|
||
var $bar = item.$el.find('.ots-upload-file-progress-bar');
|
||
var $meta = item.$el.find('.ots-upload-file-meta');
|
||
|
||
item.xhr = $.ajax({
|
||
url: options.url,
|
||
type: 'POST',
|
||
data: formData,
|
||
processData: false,
|
||
contentType: false,
|
||
xhr: function() {
|
||
var xhr = new XMLHttpRequest();
|
||
xhr.upload.addEventListener('progress', function(e) {
|
||
if (e.lengthComputable) {
|
||
var pct = Math.round((e.loaded / e.total) * 100);
|
||
$bar.css('width', pct + '%');
|
||
$meta.text(pct + '%');
|
||
}
|
||
});
|
||
return xhr;
|
||
},
|
||
success: function(response) {
|
||
item.status = 'done';
|
||
item.$el.removeClass('ots-upload-file--uploading').addClass('ots-upload-file--done');
|
||
$bar.css('width', '100%');
|
||
$meta.text('Complete');
|
||
successCount++;
|
||
if (typeof options.uploadDoneEvent === 'function') {
|
||
options.uploadDoneEvent(item.file, response);
|
||
}
|
||
callback();
|
||
},
|
||
error: function(xhr) {
|
||
item.status = 'error';
|
||
item.$el.removeClass('ots-upload-file--uploading').addClass('ots-upload-file--error');
|
||
var msg = 'Upload failed';
|
||
try { msg = JSON.parse(xhr.responseText).message || msg; } catch(e){}
|
||
$meta.text(msg);
|
||
$bar.css('width', '0%');
|
||
callback();
|
||
}
|
||
});
|
||
}
|
||
|
||
// ── All uploads finished ──
|
||
function onAllDone() {
|
||
$btnDone.removeClass('d-none');
|
||
$btnStart.addClass('d-none');
|
||
$queueCount.text(successCount + '/' + uploadCount + ' uploaded');
|
||
}
|
||
|
||
// ── Drag & drop ──
|
||
$dropzone.off('.otsUpload').on({
|
||
'dragenter.otsUpload dragover.otsUpload': function(e) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
$dropzone.addClass('ots-upload-dropzone--over');
|
||
},
|
||
'dragleave.otsUpload': function(e) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
$dropzone.removeClass('ots-upload-dropzone--over');
|
||
},
|
||
'drop.otsUpload': function(e) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
$dropzone.removeClass('ots-upload-dropzone--over');
|
||
var dt = e.originalEvent.dataTransfer;
|
||
if (dt && dt.files && dt.files.length) {
|
||
addFiles(dt.files);
|
||
}
|
||
}
|
||
});
|
||
|
||
// Click to browse — use native .click() on the raw DOM element;
|
||
// jQuery's .trigger('click') does NOT open the file picker in most browsers.
|
||
$dropzone.off('click.otsUpload').on('click.otsUpload', function(e) {
|
||
// Don't trigger if clicking on the remove button inside the queue
|
||
if ($(e.target).closest('.ots-upload-file-remove').length) return;
|
||
$input[0].click();
|
||
});
|
||
|
||
// Keyboard accessibility on dropzone
|
||
$dropzone.off('keydown.otsUpload').on('keydown.otsUpload', function(e) {
|
||
if (e.key === 'Enter' || e.key === ' ') {
|
||
e.preventDefault();
|
||
$input[0].click();
|
||
}
|
||
});
|
||
|
||
// File input change
|
||
$input.off('change.otsUpload').on('change.otsUpload', function() {
|
||
if (this.files && this.files.length) {
|
||
addFiles(this.files);
|
||
// Reset so the same file can be re-selected
|
||
this.value = '';
|
||
}
|
||
});
|
||
|
||
// Start upload button
|
||
$btnStart.off('click.otsUpload').on('click.otsUpload', function() {
|
||
startUpload();
|
||
});
|
||
|
||
// Done button
|
||
$btnDone.off('click.otsUpload').on('click.otsUpload', function() {
|
||
if (mainBtn.callback) {
|
||
mainBtn.callback();
|
||
}
|
||
$modal.modal('hide');
|
||
});
|
||
|
||
// Clean up on modal close
|
||
$modal.off('hidden.bs.modal.otsUpload').on('hidden.bs.modal.otsUpload', function() {
|
||
// Abort any in-progress uploads
|
||
fileQueue.forEach(function(f) {
|
||
if (f.xhr) { try { f.xhr.abort(); } catch(e){} }
|
||
});
|
||
fileQueue = [];
|
||
$fileList.empty();
|
||
uploading = false;
|
||
});
|
||
|
||
// ── Tab switching ──
|
||
$tabFile.off('click.otsUpload').on('click.otsUpload', function() {
|
||
$tabFile.addClass('active');
|
||
$tabUrl.removeClass('active');
|
||
$panelFile.removeClass('d-none');
|
||
$panelUrl.addClass('d-none');
|
||
});
|
||
$tabUrl.off('click.otsUpload').on('click.otsUpload', function() {
|
||
$tabUrl.addClass('active');
|
||
$tabFile.removeClass('active');
|
||
$panelUrl.removeClass('d-none');
|
||
$panelFile.addClass('d-none');
|
||
});
|
||
|
||
// ── URL: add to queue ──
|
||
function addUrlToQueue(url) {
|
||
if (!url || !url.trim()) return;
|
||
url = url.trim();
|
||
var id = nextId++;
|
||
var displayName = url.length > 60 ? url.substring(0, 57) + '...' : url;
|
||
|
||
var $el = $(
|
||
'<li class="ots-upload-file-item" data-id="' + id + '">' +
|
||
'<div class="ots-upload-file-thumb"><i class="fas fa-link"></i></div>' +
|
||
'<div class="ots-upload-file-info">' +
|
||
'<span class="ots-upload-file-name">' + $('<span>').text(displayName).html() + '</span>' +
|
||
'<span class="ots-upload-file-meta">Ready</span>' +
|
||
'<div class="ots-upload-file-progress"><div class="ots-upload-file-progress-bar"></div></div>' +
|
||
'</div>' +
|
||
'<button type="button" class="ots-upload-file-remove" aria-label="Remove" title="Remove">' +
|
||
'<i class="fas fa-times"></i>' +
|
||
'</button>' +
|
||
'</li>'
|
||
);
|
||
|
||
$el.find('.ots-upload-file-remove').on('click', (function(fileId) {
|
||
return function() { removeUrlItem(fileId); };
|
||
})(id));
|
||
|
||
$urlList.append($el);
|
||
urlQueue.push({ url: url, id: id, status: 'pending', $el: $el, xhr: null });
|
||
updateUrlQueueUI();
|
||
}
|
||
|
||
function removeUrlItem(id) {
|
||
urlQueue = urlQueue.filter(function(f) {
|
||
if (f.id === id) {
|
||
f.$el.slideUp(200, function() { $(this).remove(); });
|
||
if (f.xhr) { try { f.xhr.abort(); } catch(e){} }
|
||
return false;
|
||
}
|
||
return true;
|
||
});
|
||
updateUrlQueueUI();
|
||
}
|
||
|
||
function updateUrlQueueUI() {
|
||
var pending = urlQueue.filter(function(f) { return f.status === 'pending'; });
|
||
var total = urlQueue.length;
|
||
$urlCount.text(total + ' URL' + (total !== 1 ? 's' : ''));
|
||
if (total > 0) {
|
||
$urlQueue.removeClass('d-none');
|
||
} else {
|
||
$urlQueue.addClass('d-none');
|
||
}
|
||
if (pending.length > 0 && !uploading) {
|
||
$btnStart.removeClass('d-none');
|
||
} else if (!uploading && fileQueue.filter(function(f) { return f.status === 'pending'; }).length === 0) {
|
||
$btnStart.addClass('d-none');
|
||
}
|
||
}
|
||
|
||
$urlAddBtn.off('click.otsUpload').on('click.otsUpload', function() {
|
||
addUrlToQueue($urlInput.val());
|
||
$urlInput.val('').focus();
|
||
});
|
||
|
||
// Allow Enter key in URL input to add
|
||
$urlInput.off('keydown.otsUpload').on('keydown.otsUpload', function(e) {
|
||
if (e.key === 'Enter') {
|
||
e.preventDefault();
|
||
addUrlToQueue($urlInput.val());
|
||
$urlInput.val('').focus();
|
||
}
|
||
});
|
||
|
||
// ── Upload a single URL item ──
|
||
function uploadUrlItem(item, callback) {
|
||
item.status = 'uploading';
|
||
item.$el.addClass('ots-upload-file--uploading');
|
||
var $bar = item.$el.find('.ots-upload-file-progress-bar');
|
||
var $meta = item.$el.find('.ots-upload-file-meta');
|
||
$bar.css('width', '50%');
|
||
$meta.text('Downloading...');
|
||
|
||
var postData = { url: item.url };
|
||
if (tOpts.currentWorkingFolderId) postData.folderId = tOpts.currentWorkingFolderId;
|
||
|
||
item.xhr = $.ajax({
|
||
url: options.url,
|
||
type: 'POST',
|
||
data: postData,
|
||
success: function(response) {
|
||
item.status = 'done';
|
||
item.$el.removeClass('ots-upload-file--uploading').addClass('ots-upload-file--done');
|
||
$bar.css('width', '100%');
|
||
$meta.text('Complete');
|
||
successCount++;
|
||
if (typeof options.uploadDoneEvent === 'function') {
|
||
options.uploadDoneEvent(null, response);
|
||
}
|
||
callback();
|
||
},
|
||
error: function(xhr) {
|
||
item.status = 'error';
|
||
item.$el.removeClass('ots-upload-file--uploading').addClass('ots-upload-file--error');
|
||
var msg = 'Upload failed';
|
||
try { msg = JSON.parse(xhr.responseText).message || msg; } catch(e){}
|
||
$meta.text(msg);
|
||
$bar.css('width', '0%');
|
||
callback();
|
||
}
|
||
});
|
||
}
|
||
|
||
// ── Clean up URL queue on modal close ──
|
||
$modal.off('hidden.bs.modal.otsUploadUrl').on('hidden.bs.modal.otsUploadUrl', function() {
|
||
urlQueue.forEach(function(f) {
|
||
if (f.xhr) { try { f.xhr.abort(); } catch(e){} }
|
||
});
|
||
urlQueue = [];
|
||
$urlList.empty();
|
||
});
|
||
|
||
// ── Show modal ──
|
||
$modal.modal({ backdrop: 'static', keyboard: true });
|
||
};
|
||
</script>
|