// Config const DEBUG = 0; const CMD_PORT = "8888"; const TIMELINE_COLOR_CURSOR = "#FF8839"; const TIMELINE_COLOR_BG = "#2EB8E600"; const DEFAULT_LOCALE = "en"; // t9n let t9n = { fr : { statusDefault : "Recherche des clients connectés...", confirmMessage : "Êtes vous certain de vouloir effectuer cette action ?", filename : "Nom", duration : "Durée", size : "Taille", sync : "Transfert des fichiers..." , size_unit : " Mio", of : " de ", upload_sent_count_msg : " éléments envoyés", }, en : { statusDefault : "Searching network for live hosts...", confirmMessage : "Are you sure?", filename : "Filename", duration : "Duration", size : "Size", sync : "Syncing files...", size_unit : " MB", of : " of ", upload_sent_count_msg : " elements transferred.", } }; // Timeline drag and drop elements default attributes const tl_cont_attr = {"id":"tl_cont", "ondrop": "drop(event, this)", "ondragover":"allow_drop(event)"}; const tl_drag_attr = {"id":"tl_drag", "draggable":"true", "ondragstart":"drag(event, this)"}; // Global object window.currentUser = { scan_interval : 3000, status_all : t9n[LOCALE].statusDefault, medias_status : {}, freeze_timeline_update : 0, last_ul_host : 0, }; function sleep(ms) { let delay = new Promise(function(resolve) { setTimeout(resolve, ms); }); return delay ; // arrow notation equivalent // return new Promise(resolve => setTimeout(resolve, ms)); } async function update_sort_VLC_playlist(host) { // Update host's VLC playlist according to web UI timeline let media_count = document.getElementById(`timeline_${host}`).children.length; // Reversed loop for (let i=media_count, l=0; i>l; i--) { // Find current's timeline element children's 'media_id' value let to_shift = document.getElementById(`timeline_${host}`).children[i-1].children[0].getAttribute("media_id"); //~ console.log(to_shift + " : " + document.getElementById(`timeline_${host}`).children[i-1].children[0].innerText); // Move 'to_shift' after element with id '1' : // In VLC's playlist XML representation, the playlist node always gets id '1', so moving to that id // really means moving to the the very start of the playlist. send_ajax_cmd("/" + host + "/move/" + to_shift + "/1"); await sleep(90); } // Un-freeze timeline update flag currentUser.freeze_timeline_update = 0; } async function update_delete_VLC_playlist(host, delete_element_id) { let delete_media = document.getElementById(delete_element_id); let delete_media_cont = delete_media.parentElement; let delete_media_id = delete_media.getAttribute("media_id"); document.getElementById("timeline_" + host).removeChild(delete_media_cont); send_ajax_cmd("/" + host + "/delete/" + delete_media_id); await sleep(90); adjust_timeline(); currentUser.freeze_timeline_update = 0; } function find_target_index(element, index) { if (element == this) { return index + 1; } return 0; } function shift_elements(source_element, target_element) { // Shift elements in the timeline UI // // Get a list of current element siblings let siblings_list = Array.from(source_element.parentElement.children); let siblings_list_slice; // Find indexes of source and target elements in the timeline let source_index = siblings_list.findIndex(find_target_index, source_element); let target_index = siblings_list.findIndex(find_target_index, target_element); let insert_at = 0; // Target element is on the left of the source element if (source_index < target_index){ siblings_list_slice = siblings_list.slice(source_index + 1, target_index + 1); insert_at = source_index; } else { // Target element is on the right of the source element siblings_list_slice = siblings_list.slice(target_index, source_index ); insert_at = target_index + 1; } // Shift elements according to insert_at for (let i=0, l=siblings_list_slice.length; i -1 ) { update_delete_VLC_playlist(current_host, dropped_id); } else { let dropTarget = shift_elements(source_element.parentElement, target_element); //~ if (dropTarget) { // Append dropped element to drop target. target_element.appendChild(source_element); update_sort_VLC_playlist(current_host); //~ } } // Un-freeze timeline update flag //~ currentUser.freeze_timeline_update = 0; } send_ajax_cmd("/" + current_host + "/list"); } function adjust_timeline() { // Adapt timeline's UI elements to fit the width of their parent container. let timeline_div = document.querySelector('[id^="timeline_"]').children; let div_width = 100 / timeline_div.length; for (let i=0, l=timeline_div.length; i -1){ HTML_element = HTML_element.children[child]; } let HTML_attr = document.createAttribute(attribute); HTML_attr.value = val; HTML_element.setAttributeNode(HTML_attr); } function add_HTML_element(type, attribute, meta=0, uid=-1){ // Add HTML element with type 'type' and attributes 'attribute'. // 'attribute' should be a javascript object containing HTML attributes keys/values to be applied to the new element // 'meta' should be an array // 'uid' is used to make the HTML id attribute unique. If not set, this will use the loop's iteration count i // let HTML_element = document.createElement(type); let HTML_attributes = Object.keys(attribute); for (let i=0, l=HTML_attributes.length; i " + infos_array_element.time + " / " + infos_array_element.leng; currentUser.medias_status[infos_array_element.id] = infos_array_element.pos; // Highlight currently playing element let timeline_medias_array = Array.from(document.querySelectorAll('[media_id]')); timeline_medias_array.forEach(function(media_element){ if ( media_element.getAttribute("media_id") == infos_array_element.id ) { let first_CSS_gradient_stop = infos_array_element.pos * 100; let second_CSS_gradient_stop = first_CSS_gradient_stop - 1 + "%"; first_CSS_gradient_stop = first_CSS_gradient_stop + "%"; let media_url_str = "url(https://" + infos_array_element.host + ":" + CMD_PORT + "/thumb/" + infos_array_element.file + ")"; let media_cssgrad_rule = "linear-gradient(90deg," + TIMELINE_COLOR_BG + " " + second_CSS_gradient_stop + ", " + TIMELINE_COLOR_CURSOR + " " + first_CSS_gradient_stop + ", " + TIMELINE_COLOR_BG + " " + first_CSS_gradient_stop + ")," + media_url_str; media_element.style.backgroundImage = media_cssgrad_rule; media_element.style.borderBottom = "4px solid " + TIMELINE_COLOR_CURSOR; } else { let media_url_str = "url(https://" + infos_array_element.host + ":" + CMD_PORT + "/thumb/" + media_element.innerText + ")"; media_element.style.backgroundImage = media_url_str; media_element.style.borderBottom = "None"; } }); } else { document.getElementById("status_" + infos_array_element.host).innerHTML = "

"; } toggle_indicator(infos_array_element, "loop"); toggle_indicator(infos_array_element, "repeat"); return infos_array_element.status; } function update_list(infos_array_element){ // Playlist infos are displayed in a div ; number of items in list and total duration. //~ document.getElementById("playlist_"+infos_array[i].host).innerHTML = infos_array[i].leng + " item(s) in playlist - " + infos_array[i].duration; // Populate timeline according to the content of the playlist // Get returned items as an array let items_array = Array.from(infos_array_element.items); let host = infos_array_element.host; // If playlist is empty, remove all media divs in the timeline UI. if (items_array.length == 0){ let child_list = Array.from(document.getElementById("timeline_" + host).children); child_list.forEach(function(child){ document.getElementById("timeline_" + host).removeChild(child); }); } else { items_array.forEach(function(item, j){ item_meta = item.split(';'); let child_node = add_HTML_element("div", tl_drag_attr, item_meta, j); let len = document.getElementById("timeline_" + host).children.length; add_HTML_attr("timeline_" + host, "length", len); if ( len < items_array.length ) { document.getElementById("timeline_" + host).appendChild( add_HTML_element("div", tl_cont_attr, 0, len) ); } document.getElementById(tl_cont_attr.id + j).replaceChildren(child_node); let media_name = document.getElementById(tl_cont_attr.id + j).children[0].innerText; let media_url_str = "url(https://" + host + ":" + CMD_PORT + "/thumb/" + media_name + ")"; document.getElementById(tl_cont_attr.id + j).children[0].style.backgroundImage = media_url_str; // Adjust elements width adjust_timeline(); }); } return items_array.length; } function populate_HTML_table(inner_text, host="all", CSS_class="file_selection") { tr = document.createElement("tr"); td = document.createElement("td"); tr.appendChild(td); tr.setAttribute("class", CSS_class); tr.host = host; td.innerText = inner_text; // Add an event to the created element to send enqueue command when a file is clicked in the list tr.addEventListener("click", enqueue_file, false); document.getElementById("file_sel_" + host).appendChild(tr); } function empty_HTML_table(host="all", keep=1) { let HTML_table_element = document.getElementById("file_sel_" + host); let HTML_table_element_length = HTML_table_element.childElementCount; for (let i=keep; i 4 ? 4 : rssi_norm; i" + "" + t9n[LOCALE].filename + "" + "" + t9n[LOCALE].duration + "" + ""; infos_array.forEach(function(element){ html_table += "" + "" + element + "" + "" + "00:00" + "" + "" ; }); html_table += ""; return html_table; } function update_host_list(infos_array){ let host_up = infos_array[0]; let host_down = infos_array[1]; if (host_up.length) { host_up.forEach(function(host){ document.getElementById(host).style.display = 'block'; send_ajax_cmd("/" + host + "/list"); send_ajax_cmd("/" + host + "/rssi"); }); currentUser.scan_interval = 10000; } host_down.forEach(function(element){ document.getElementById(element).style.display = 'none'; }); currentUser.status_all = host_up.length + " client(s)."; return host_up.length; } function display_upload_status(command) { // TODO : First this should set the HTMl with empty values, then call update_upload... let host = command.split("/")[2]; let container_element = document.getElementById(host); let siblings = container_element.children; // Upload dialog container / background let upload_dialog_cont_attributes = {"id":"ul_dialog_cont_", "class": "upload_dialog_cont"}; let upload_dialog_cont_element = add_HTML_element("div", upload_dialog_cont_attributes, 0, host); let ul_cont_exists = document.getElementById(upload_dialog_cont_attributes["id"] + host); // Upload dialog //~ let upload_dialog_attributes = {"id":"ul_dialog_", "class": "upload_dialog"}; //~ let upload_dialog_element = add_HTML_element("div", upload_dialog_attributes, 0, host); //~ let ul_dialog_exists = document.getElementById(upload_dialog_cont_attributes["id"] + host); let upload_status_table = `
${t9n[LOCALE].sync}
` let upload_dialog_HTML = `
` if ( ul_cont_exists != undefined) { container_element.removeChild(ul_cont_exists); } container_element.insertBefore(upload_dialog_cont_element, siblings[0]); document.getElementById(upload_dialog_cont_attributes["id"] + host).innerHTML = upload_dialog_HTML; document.getElementById("ul_stop_btn_" + host).addEventListener("click", send_btn_cmd, false); document.getElementById("ul_status_" + host).innerHTML = upload_status_table; } function destroy_upload_status() { let container_element = document.getElementById(currentUser.last_ul_host); let ul_cont_exists = document.getElementById("ul_dialog_cont_" + currentUser.last_ul_host); if ( ul_cont_exists != undefined) { container_element.removeChild(ul_cont_exists); clearTimeout(currentUser["ul_timeout_" + currentUser.last_ul_host]); } } async function update_upload_status(current_upload) { console.log("Updating upload status..."); //~ console.log(current_upload); if (current_upload.status == -1){ console.log("Destroying dialog..." + current_upload.host) destroy_upload_status(); } else if (current_upload.status) { console.log("Filling dialog...") document.getElementById("ul_dialog_cont_" + current_upload.host).style.display = "block"; // Fill table document.getElementById("ul_status_progress_cnt_" + current_upload.host).innerHTML = current_upload.progress + " / " + current_upload.total_count + t9n[LOCALE].upload_sent_count_msg; document.getElementById("ul_status_progress_size_" + current_upload.host).innerHTML = current_upload.transferred_size + t9n[LOCALE].size_unit + t9n[LOCALE].of + current_upload.total_size + t9n[LOCALE].size_unit; document.getElementById("ul_status_filename_" + current_upload.host).innerHTML = t9n[LOCALE].filename + ": " + current_upload.filename; document.getElementById("ul_status_filesize_" + current_upload.host).innerHTML = t9n[LOCALE].size + ": " + current_upload.size + t9n[LOCALE].size_unit; // Progress bar CSS if (current_upload.transferred_percent) { document.getElementById("ul_progress_" + current_upload.host).innerText = current_upload.transferred_percent + "%"; document.getElementById("ul_progress_" + current_upload.host).style.background = "linear-gradient(90deg, " + TIMELINE_COLOR_CURSOR + " " + current_upload.transferred_percent + "%, #fff " + current_upload.transferred_percent + "%)"; } currentUser["ul_timeout_" + current_upload.host] = setTimeout(send_ajax_cmd, 3000, `/sync/${current_upload.host}/status`); } else { console.log("requesting status data"); currentUser.last_ul_host = current_upload.host; send_ajax_cmd(`/sync/${current_upload.host}/status`); await sleep(500); } } //
// Metadata display function parse_result(command, infos_array) { if (command == "/all/status") { // Requests current status of every instances, especially current media file's name, position and length. // Also retrieves loop and repeat status. infos_array.forEach(update_status); } else if (command.indexOf("/list") > -1 ) { console.log(currentUser.freeze_timeline_update); // Requests playlist of every instances if (!currentUser.freeze_timeline_update){ infos_array.forEach(update_list); } } else if (command == "/scan") { // Scan for live hosts update_host_list(infos_array); } else if (command == "/browse_local") { // Display local media files in a table document.getElementById("filelist").innerHTML = update_local_filelist(infos_array); } else if (command == "/all/rssi") { // RSSI strength indicator infos_array.forEach(update_rssi_indicator); //~ } else if ( command == "/all/browse") { } else if (command.indexOf("/browse") > -1) { // Display remote media files in a table infos_array.forEach(update_remote_filelist); } else if (command.indexOf("/sync/") > -1) { if (command.indexOf("/status") > -1 ) { console.log("updating infos..."); console.log(infos_array); update_upload_status(infos_array); } else if (command.indexOf("/stop") > -1 ) { console.log("stopping upload..."); console.log(infos_array); destroy_upload_status(); } else { console.log("displaying status"); //~ console.log(infos_array); //~ if (infos_array.total_count) { //~ display_upload_status(infos_array) } } else { setTimeout(send_ajax_cmd, 80, "/all/list"); setTimeout(send_ajax_cmd, 120, "/all/status"); } } function send_ajax_cmd(command) { let request = new XMLHttpRequest(); request.onload = function() { if (request.readyState === request.DONE) { if (request.status === 200) { // responseText is a string, use parse to get an array. if (!currentUser.freeze_timeline_update) { let infos_array = JSON.parse(request.responseText); parse_result(command, infos_array); } } } }; request.open("GET", command, true); request.send(); } function enqueue_file(evt) { send_ajax_cmd("/" + evt.currentTarget.host + "/enqueue/" + evt.currentTarget.innerText); setTimeout(send_ajax_cmd, 40, "/" + evt.currentTarget.host + "/list"); } function send_btn_cmd(evt) { let clickedButton = event.currentTarget; let command = clickedButton.value; send_ajax_cmd(command); //~ setTimeout(send_ajax_cmd, 40, "/" + evt.currentTarget.host + "/list"); } function update_statusall_content() { document.getElementById("status_all").innerHTML = currentUser.status_all; } function scan_hosts() { send_ajax_cmd("/scan"); update_statusall_content(); setTimeout(scan_hosts, currentUser.scan_interval); } // UI addEventListener("DOMContentLoaded", function() { adjust_timeline(); let commandButtons = document.querySelectorAll(".command"); for (let i=0, l=commandButtons.length; i -1 || command.indexOf("/poweroff") > -1 ) { // TODO : l10n if ( !confirm(t9n[LOCALE].confirmMessage) ) { return 0; } } else if ( command == "/scan" ) { document.getElementById("status_all").innerHTML = currentUser.status_all; } else if ( command.indexOf("/sort") > -1 ){ if (command.indexOf('/1/') > -1 ) { clickedButton.value = clickedButton.value.replace('/1/','/0/'); } else { clickedButton.value = clickedButton.value.replace('/0/','/1/'); } } // AJAX request let request = new XMLHttpRequest(); if ( command == "/scan") { request.onload = send_ajax_cmd(command); } else if ( command.indexOf("/clear") > -1 || command.indexOf("/sort") > -1) { request.onload = send_ajax_cmd(command); } else if ( command.indexOf("/sync/") > -1 ) { console.log("Sync command detected") currentUser.status_all = t9n[LOCALE].sync; // Display dialog display_upload_status(command); // Request values to fill dialog //~ request.onload = send_ajax_cmd(command + "/status"); setTimeout(send_ajax_cmd, 1000, command + "/status"); } else if ( command.indexOf("/browse") > -1 ) { request.onload = send_ajax_cmd("/all/browse"); } request.open("GET", command, true); request.send(); }); } }, true); send_ajax_cmd("/scan"); send_ajax_cmd("/browse_local"); setInterval(send_ajax_cmd, 500, "/all/status"); setTimeout(send_ajax_cmd, 1000, "/all/list"); setTimeout(send_ajax_cmd, 3000, "/all/browse"); setInterval(send_ajax_cmd, 20000, "/all/rssi"); setTimeout(scan_hosts, currentUser.scan_interval);