// 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", item_queued : "Synchronisation en attente...", }, 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.", item_queued : "Queued for file synchronisation...", } }; // 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, freeze_ul : 0, last_ul_host : 0, ul_queue : [], }; 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 //~ console.log(document.getElementById(`timeline_${host}`).children[i-1].children[0]) 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(200); } // Un-freeze timeline update flag currentUser.freeze_timeline_update = 0; } function get_child_by_id(id, parent_element, depth=0){ Array.from(parent_element.children).forEach(function(child){ if (depth){ if (child.children[0].id == id){ result = child.children[0]; } } else if (child.id == id) { result = child; } }); return result; } async function update_delete_VLC_playlist(host, delete_element_id) { // Delete item from timeline and send corresponding VLC command let current_timeline = document.getElementById("timeline_" + host); let delete_media = get_child_by_id(delete_element_id, current_timeline, 1); let delete_media_cont = delete_media.parentElement; let delete_media_id = delete_media.getAttribute("media_id"); current_timeline.removeChild(delete_media_cont); send_ajax_cmd("/" + host + "/delete/" + delete_media_id); await sleep(200); adjust_timeline(host); // Unfreeze timeline UI after delay setTimeout(function(){ currentUser.freeze_timeline_update = 0; }, 200); } 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 ) { if ( target_element.id == "delete_btn") { update_delete_VLC_playlist(current_host, dropped_id); document.getElementById("delete_btn").style.backgroundColor = "#df7474"; } else { dropped_element = get_child_by_id(dropped_id, source_element, 1); let dropTarget = shift_elements(dropped_element.parentElement, target_element); //~ console.log(target_element); //~ if (dropTarget) { // Append dropped element to drop target. //~ target_element.appendChild(source_element); target_element.appendChild(dropped_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(host) { // Adapt timeline's UI elements to fit the width of their parent container. //~ let timeline_div = document.querySelector('[id^="timeline_"]').children; let timeline_div = document.getElementById("timeline_" + host).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 current_timeline = document.getElementById("timeline_" + infos_array_element.host); let timeline_medias_array = Array.from(current_timeline.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 { current_timeline = document.getElementById("timeline_" + host); 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 = current_timeline.children.length; add_HTML_attr("timeline_" + host, "length", len); if ( len < items_array.length ) { current_timeline.appendChild( add_HTML_element("div", tl_cont_attr, 0, len) ); } //~ document.getElementById(tl_cont_attr.id + j).replaceChildren(child_node); current_timeline_children = Array.from(current_timeline.children); current_timeline_children[j].replaceChildren(child_node); let media_name = current_timeline_children[j].children[0].innerText; let media_url_str = "url(https://" + host + ":" + CMD_PORT + "/thumb/" + media_name + ")"; current_timeline_children[j].children[0].style.backgroundImage = media_url_str; // Adjust elements width adjust_timeline(host); }); } 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){ adjust_timeline(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.hosts_up = host_up; currentUser.hosts_down = host_down; currentUser.status_all = host_up.length + " client(s)."; return host_up.length; } function freeze_ui_host(host, html_attributes, inner_html=0, inner_text=0){ // Add an overlay element to freeze part of the UI // html_attributes, inner_html are objects with structure {"id":, 0 "class": 0, "attr" : "value", } let container_element = document.getElementById(host); let siblings = container_element.children; // Upload dialog container / background let upload_dialog_cont_element = add_HTML_element("div", html_attributes, 0, host); container_element.insertBefore(upload_dialog_cont_element, siblings[0]); if (inner_html){ let child_node = add_HTML_element("div", inner_html, 0, host); let new_node = container_element.children.item(html_attributes.id + host).appendChild(child_node); if (inner_text){ new_node.innerHTML = inner_text; } } } function freeze_queued_container(command){ freeze_attributes = {"id":"ul_queued_freeze_", "class": "ul_queued_freeze"}; inner_html = {"id":"ul_queued_msg_", "class": "ul_queued_msg"}; inner_text = t9n[LOCALE].item_queued; let host = command.split("/")[2]; inner_text += 'X ${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(host) { let container_element = document.getElementById(host); let ul_cont_exists = document.getElementById("ul_dialog_cont_" + host); if ( ul_cont_exists != undefined) { // Remove ul dialog container_element.removeChild(ul_cont_exists); // Clear update callback clearTimeout(currentUser["ul_timeout_" + host]); } } async function update_upload_status(current_upload) { if (current_upload.status == -1){ // Upload finished console.log("Upload finished - Destroying dialog for " + currentUser.last_ul_host); destroy_upload_status(currentUser.last_ul_host); // Remove first item from command queue currentUser.ul_queue.shift(); // Upload done, un-freeze currentUser.freeze_ul = 0; // Call sync_host() again setTimeout(sync_host, 1000); } else if (current_upload.status) { unfreeze_queued_container(currentUser.last_ul_host); // Upload in progress console.log("Updating dialog..."); //document.getElementById("ul_dialog_cont_" + current_upload.host).style.display = "block"; document.getElementById("ul_dialog_cont_" + currentUser.last_ul_host).style.display = "block"; // Fill table document.getElementById("ul_status_progress_cnt_" + currentUser.last_ul_host).innerHTML = current_upload.progress + " / " + current_upload.total_count + t9n[LOCALE].upload_sent_count_msg; document.getElementById("ul_status_progress_size_" + currentUser.last_ul_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_" + currentUser.last_ul_host).innerHTML = t9n[LOCALE].filename + ": " + current_upload.filename; document.getElementById("ul_status_filesize_" + currentUser.last_ul_host).innerHTML = t9n[LOCALE].size + ": " + current_upload.size + t9n[LOCALE].size_unit; // Progress bar CSS // FIXME : cursor stops updating after upload ? if (current_upload.transferred_percent) { document.getElementById("ul_progress_" + currentUser.last_ul_host).innerText = current_upload.transferred_percent + "%"; document.getElementById("ul_progress_" + currentUser.last_ul_host).style.background = "linear-gradient(90deg, " + TIMELINE_COLOR_CURSOR + " " + current_upload.transferred_percent + "%, #fff " + current_upload.transferred_percent + "%)"; } // Update status callback currentUser["ul_timeout_" + currentUser.last_ul_host] = setTimeout(send_ajax_cmd, 1000, `/sync/${currentUser.last_ul_host}/status`); } else { // Upload is starting console.log("Requesting data to update dialog for " + currentUser.last_ul_host); // Freeze ul cmd currentUser.freeze_ul = 1; //~ currentUser.last_ul_host = current_upload.host; // This will end up calling update_upload_status() again, expecting the ul status value to change from 0 to int send_ajax_cmd(`/sync/${currentUser.last_ul_host}/status`); //~ send_ajax_cmd(`/sync/${current_upload.host}/status`); // Wait a bit for response await sleep(200); } } //
// 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 ) { // 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 { setTimeout(send_ajax_cmd, 180, "/all/list"); setTimeout(send_ajax_cmd, 180, "/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); // User can't be interacting with timeline when clicking on file so unfreeze it. currentUser.freeze_timeline_update = 0; 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); } function sync_host() { // If ul is not in progress if (!currentUser.freeze_ul){ // If there's something in the queue if (currentUser.ul_queue.length){ // Send first command in queue. let command_q = currentUser.ul_queue[0]; console.log("Sending command " + command_q); let host = command_q.split("/")[2]; // Send ul command send_ajax_cmd(command_q); // Update status currentUser.status_all = t9n[LOCALE].sync; // Create dialog display_upload_status(command_q); // Set current host currentUser.last_ul_host = host; // Send command to update dialog according to upload status // This ends up calling update_upload_status() setTimeout(send_ajax_cmd, 200, command_q + "/status"); } else { console.log("Nothing else in the queue !"); // Make sure sane values are set back unfreeze_all_containers(); currentUser.last_ul_host = 0; return 0; } } console.log("UL freezed, waiting a bit..."); return 0; } function cancel_sync(host_btn_id){ let host = host_btn_id.id.split("_")[2]; let command = "/sync/" + host; if (currentUser.ul_queue){ let item_index = currentUser.ul_queue.indexOf(command); // Skip [0] if (item_index > 0){ console.log(host + " unqueuing"); currentUser.ul_queue.splice(item_index, 1); unfreeze_queued_container(host); } } } // UI addEventListener("DOMContentLoaded", function() { //~ adjust_timeline(); let commandButtons = document.querySelectorAll(".command"); for (let i=0, l=commandButtons.length; i -1 || command.indexOf("/poweroff") > -1 ) { if (command.indexOf("/all" ) == -1){ 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 ) { if ( command.indexOf("/all") > -1 ) { console.log("Sync all request"); // If command is /sync/all, add live hosts to sync queue if (currentUser.hosts_up){ currentUser.hosts_up.forEach(function(host){ let command = "/sync/" + host; if (currentUser.ul_queue.indexOf(command) == -1){ console.log("Adding " + host + " to sync queue..."); currentUser.ul_queue.push(command); freeze_queued_container(command); } }); } if (currentUser.ul_queue.length > 1){ sync_host(); } // Prevent request return 0; } // If command not already in queue, add it if (!currentUser.ul_queue.includes(command)){ currentUser.ul_queue.push(command); freeze_queued_container(command); } else { console.log("Command already in queue..."); return 0; } sync_host(); // Prevent request return 1; } 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);