const githubRepo = 'https://raw.githubusercontent.com/kolos26/GEOFS-LiverySelector/main'; let jsDelivr = 'https://cdn.jsdelivr.net/gh/kolos26/GEOFS-LiverySelector@main'; const noCommit = jsDelivr; const version = '3.4.0'; const liveryobj = {}; const mpLiveryIds = {}; const mLiveries = {}; const origHTMLs = {}; const uploadHistory = JSON.parse(localStorage.lsUploadHistory || '{}'); const LIVERY_ID_OFFSET = 10e3; const ML_ID_OFFSET = 1e3; let links = []; let airlineobjs = []; let whitelist; let mpAirlineobjs = {}; const LOG_STYLE = "white-space:nowrap;display:inline;color:"; const log = (e, t = "log") => console[t]("%c[%cLivery%cSelector%c] %c", LOG_STYLE + "inherit;", LOG_STYLE + "#bcc3cb;", LOG_STYLE + "#3f5f8a;", LOG_STYLE + "inherit;", LOG_STYLE + "inherit;", e); (async function init() { // find latest commit to ensure the latest files are fetched from jsDelivr try { const res = await fetch(`https://api.github.com/repos/kolos26/GEOFS-LiverySelector/commits/main`); if (!res.ok) jsDelivr = githubRepo; const commit = (await res.json()).sha; if (!/^[a-f0-9]{40}$/.test(commit)) jsDelivr = githubRepo; jsDelivr = jsDelivr.replace("@main", `@${commit}`); } catch (err) {jsDelivr = githubRepo}; // styles fetch(`${jsDelivr}/styles.css?` + Date.now()).then(async data => { const styleTag = createTag('style', { type: 'text/css' }); styleTag.textContent = await data.text(); document.head.appendChild(styleTag); }); //Load liveries (@todo: consider optimising livery.json or converting it to a different datatype) fetch(`${jsDelivr}/livery.json?` + Date.now()).then(handleLiveryJson); // Panel for list const listDiv = appendNewChild(document.querySelector('.geofs-ui-left'), 'div', { id: 'listDiv', class: 'geofs-list geofs-toggle-panel livery-list', 'data-noblur': 'true', 'data-onshow': '{geofs.initializePreferencesPanel()}', // are these properties needed? 'data-onhide': '{geofs.savePreferencesPanel()}' }); listDiv.innerHTML = generateListHTML(); // one big event listener for the main livery list instead of multiple event listeners const livList = document.querySelector("#liverylist"); livList.addEventListener('click', function ({ target }) { if (target.nodeName === "I") return void window.LiverySelector.star(target); // if the element clicked is a star, run the star function const idx = parseInt(target.closest('li').getAttribute('data-idx')); // convert to int because attributes are stored as strings if (idx === void 0) return; // avoid livery selection when other stuff is pressed const airplane = LiverySelector.liveryobj.aircrafts[geofs.aircraft.instance.id] , livery = airplane.liveries[idx]; livery.disabled || (loadLivery(livery.texture, airplane.index, airplane.parts, livery.materials), livery.mp != 'disabled' && setInstanceId(idx + (livery.credits?.toLowerCase() == 'geofs' ? 0 : LIVERY_ID_OFFSET))); }); // uses || (logical OR) to run the right side code only if livery.disabled is falsy livList.addEventListener('error', function(e) { const defaultThumb = `${noCommit}/thumbs/${geofs.aircraft.instance.id}.png`; if (e.target.tagName !== 'IMG' || e.target.src === defaultThumb) return; e.target.onerror = null; e.target.src = defaultThumb; }, true); document.querySelector("#listDiv > ul#favorites").addEventListener("click", function ({ target }) { if (target.nodeName != "LI") return; const $match = $(`#liverylist > [id='${$(target).attr("id").replace("_favorite", "_button")}']`) // find the matching livery list item if ($match.length === 0) return void ui.notification.show(`ID: ${$(target).attr("id")} is missing a liveryList counterpart.`) $match.click(); }); const potatoCheckbox = document.querySelector("#livery-potato-mode"); potatoCheckbox.addEventListener("change", function () { geofs.setPreferenceFromInput(this); document.querySelector(".potato-mode-search").classList.toggle("geofs-visible", this.checked); geofs.savePreferences(); window.LiverySelector[this.checked ? "potatoSearch" : "search"](document.querySelector("#searchlivery").value); }); window.executeOnEventDone("geofsInitialized", () => { potatoCheckbox.checked = geofs.preferences.liveryPotato; document.querySelector(".potato-mode-search").classList.toggle("geofs-visible", potatoCheckbox.checked); }); document.querySelector(".potato-mode-search").addEventListener("click", function () { if (!geofs.preferences.liveryPotato) return; window.LiverySelector.potatoSearch(document.querySelector("#searchlivery").value); }) // Button for panel const geofsUiButton = document.querySelector('.geofs-ui-bottom'); const insertPos = geofs.version >= 3.6 ? 4 : 3; geofsUiButton.insertBefore(generatePanelButtonHTML(), geofsUiButton.children[insertPos]); //remove original buttons const origButtons = document.getElementsByClassName('geofs-liveries geofs-list-collapsible-item'); Object.values(origButtons).forEach(btn => btn.parentElement.removeChild(btn)); //Init airline databases if (localStorage.getItem('links') === null) { localStorage.links = ''; } else { links = localStorage.links.split(","); links.forEach(async function (e) { await fetch(e).then(res => res.json()).then(data => airlineobjs.push(data)); airlineobjs[airlineobjs.length - 1].url = e.trim(); }); } fetch(`${jsDelivr}/whitelist.json?` + Date.now()).then(res => res.json()).then(data => whitelist = data); // Start multiplayer setInterval(updateMultiplayer, 5000); window.addEventListener("keyup", function (e) { if (e.target.classList.contains("geofs-stopKeyupPropagation")) { e.stopImmediatePropagation(); } if (e.key === "l") { LiverySelector.togglePanel(); } }); })(); /** * @param {Response} data */ async function handleLiveryJson(data) { const json = await data.json(); Object.keys(json).forEach(key => liveryobj[key] = json[key]); if (liveryobj.commit) jsDelivr = jsDelivr.replace("@main", "@" + liveryobj.commit) if (liveryobj.version != version) { document.querySelector('.livery-list h3').appendChild( createTag('a', { href: 'https://github.com/kolos26/GEOFS-LiverySelector/releases/latest', target: '_blank', style: 'display:block;width:100%;text-decoration:none;text-align:center;' }, 'Update available: ' + liveryobj.version) ); } // mark aircraft with livery icons Object.keys(liveryobj.aircrafts).forEach(aircraftId => { if (!liveryobj.aircrafts[aircraftId].logo || liveryobj.aircrafts[aircraftId].liveries.length < 2) { return; // only show icon if there's more than one livery, also return if only the id is used by extra vehicles } const element = document.querySelector(`[data-aircraft='${aircraftId}']`); // save original HTML for later use (reload, aircraft change, etc..) if (element) { if (!origHTMLs[aircraftId]) { origHTMLs[aircraftId] = element.innerHTML; } // use orig HTML to concatenate so theres only ever one icon element.innerHTML = origHTMLs[aircraftId] + createTag('img', { src: `${noCommit}/liveryselector-logo-small.svg`, style: 'height:30px;width:auto;margin-left:20px;', title: 'Liveries available' }).outerHTML; if (liveryobj.aircrafts[aircraftId].mp != "disabled") element.innerHTML += createTag('small', { title: 'Liveries are multiplayer compatible\n(visible to other players)' }, '🎮').outerHTML; } }); } /** * Triggers GeoFS API to load texture * * @param {string[]} texture * @param {number[]} index * @param {number[]} parts * @param {Object[]} mats */ function loadLivery(texture, index, parts, mats) { //change livery for (let i = 0; i < texture.length; i++) { const model3d = geofs.aircraft.instance.definition.parts[parts[i]]['3dmodel']; // check for material definition (for untextured parts) if (typeof texture[i] === 'object') { if (texture[i].material !== undefined) { const mat = mats[texture[i].material]; model3d._model.getMaterial(mat.name) .setValue(Object.keys(mat)[1], new Cesium.Cartesian4(...mat[Object.keys(mat)[1]], 1.0)); } continue; } try { if (geofs.version == 2.9) { geofs.api.Model.prototype.changeTexture(texture[i], index[i], model3d); } else if (geofs.version >= 3.0 && geofs.version <= 3.7) { geofs.api.changeModelTexture(model3d._model, texture[i], index[i]); } else { geofs.api.changeModelTexture(model3d._model, texture[i], { index: index[i] }); } } catch (error) { geofs.api.notify("Hmmm... we can't find this livery, check the console for more info."); (error, "error"); } } } /** * Load liveries from text input fields */ function inputLivery() { const airplane = getCurrentAircraft(); const textures = airplane.liveries[0].texture; const inputFields = document.getElementsByName('textureInput'); if (textures.filter(x => x === textures[0]).length === textures.length) { // the same texture is used for all indexes and parts const texture = inputFields[0].value; loadLivery(Array(textures.length).fill(texture), airplane.index, airplane.parts); } else { const texture = []; inputFields.forEach(e => texture.push(e.value)); loadLivery(texture, airplane.index, airplane.parts); } } /** * Submit livery for review */ function submitLivery() { const airplane = getCurrentAircraft(); const textures = airplane.liveries[0].texture; const inputFields = document.getElementsByName('textureInput'); const formFields = {}; document.querySelectorAll('.livery-submit input').forEach(f => formFields[f.id.replace('livery-submit-', '')] = f); if (!localStorage.liveryDiscordId || localStorage.liveryDiscordId.length < 6) { return alert('Invalid Discord User id!'); } if (formFields.liveryname.value.trim().length < 3) { return alert('Invalid Livery Name!'); } if (!formFields['confirm-perms'].checked || !formFields['confirm-legal'].checked) { return alert('Confirm all checkboxes!'); } const json = { name: formFields.liveryname.value.trim(), credits: formFields.credits.value.trim(), texture: [], materials: {} }; if (!json.name || json.name.trim() == '') { return; } const hists = []; const embeds = []; inputFields.forEach((f, i) => { (f.type) if (f.type === "text"){ f.value = f.value.trim(); if (f.value.match(/^https:\/\/.+/i)) { const hist = Object.values(uploadHistory).find(o => o.url == f.value); if (!hist) { return alert('Only self-uploaded imgbb links work for submitting!'); } if (hist.expiration > 0) { return alert('Can\' submit expiring links! DISABLE "Expire links after one hour" option and re-upload texture:\n' + airplane.labels[i]); } const embed = { title: airplane.labels[i] + ' (' + (Math.ceil(hist.size / 1024 / 10.24) / 100) + 'MB, ' + hist.width + 'x' + hist.height + ')', description: f.value, image: { url: f.value }, fields: [ { name: 'Timestamp', value: new Date(hist.time * 1e3), inline: true }, { name: 'File ID', value: hist.id, inline: true }, ] }; if (hist.submitted) { if (!confirm('The following texture was already submitted:\n' + f.value + '\nContinue anyway?')) { return; } embed.fields.push({ name: 'First submitted', value: new Date(hist.submitted * 1e3) }); } embeds.push(embed); hists.push(hist); json.texture.push(f.value); } else { json.texture.push(textures[i]); } } else if (f.type === "color"){ json.materials[f.id] = [parseInt(f.value.substring(1, 3), 16) / 255, parseInt(f.value.substring(3, 5), 16) / 255, parseInt(f.value.substring(5, 7), 16) / 255] } }); if (!embeds.length) return alert('Nothing to submit, upload images first!'); let content = [ `Livery upload by <@${localStorage.liveryDiscordId}>`, `__Plane:__ \`${geofs.aircraft.instance.id}\` ${geofs.aircraft.instance.aircraftRecord.name}`, `__Livery Name:__ \`${json.name}\``, '```json\n' + JSON.stringify(json, null, 2) + '```' ]; fetch(atob(liveryobj.dapi), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content: content.join('\n'), embeds }) }).then(res => { hists.forEach(hist => { hist.submitted = hist.submitted || Math.round(new Date() / 1000); }); localStorage.lsUploadHistory = JSON.stringify(uploadHistory); }); } function sortList(id) { // extremely slow (do not use) const list = domById(id); let i, switching, b, shouldSwitch; switching = true; while (switching) { switching = false; b = list.getElementsByTagName('LI'); for (i = 0; i < (b.length - 1); i++) { shouldSwitch = false; if (b[i].innerHTML.toLowerCase() > b[i + 1].innerHTML.toLowerCase()) { shouldSwitch = true; break; } } if (shouldSwitch) { b[i].parentNode.insertBefore(b[i + 1], b[i]); switching = true; } } } /** * main livery list */ function listLiveries() { const livList = $('#liverylist').html(''); const tempFrag = document.createDocumentFragment() , thumbsDir = noCommit + '/thumbs' , acftId = geofs.aircraft.instance.id , airplane = getCurrentAircraft(); // chained variable declarations $('#listDiv').attr('data-ac', acftId); // tells us which aircraft's liveries are loaded airplane.liveries.forEach((e, t) => e.idx ||= t); airplane.liveries.sort((e, t) => e.name.localeCompare(t.name, undefined, { sensitivity: 'base' })); for (let i = 0; i < airplane.liveries.length; i++) { const e = airplane.liveries[i]; if (e.disabled) continue; const listItem = $('
  • ', {id: [acftId, e.name, 'button'].join('_'), class: 'livery-list-item', "data-idx": i}); listItem.append($('').text(e.name)); listItem.toggleClass('offi', acftId < 100).toggleClass("geofs-visible", !geofs.preferences.liveryPotato); // if param2 is true, it'll add 'offi', if not, it will remove 'offi' acftId < 1000 && listItem.append($('', {loading: 'lazy', src: [thumbsDir, acftId, acftId + '-' + e.idx + '.png'].join('/')})); e.credits && e.credits.length && $('').text(`by ${e.credits}`).appendTo(listItem); $('', { id: acftId + "_" + e.name }).appendTo(listItem); listItem.appendTo(tempFrag); } livList.append(tempFrag); loadFavorites(); loadAirlines(); addCustomForm(); } function loadFavorites() { const favorites = localStorage.getItem('favorites') ?? (localStorage.setItem('favorites', ''), ''); // sets favourites to '' if they can't be found and initialises localStorage.favorites $("#favorites").empty(); const list = favorites.split(','); const airplane = geofs.aircraft.instance.id; list.forEach(function (e) { if ((airplane == e.slice(0, airplane.length)) && (e.charAt(airplane.length) == '_')) { star(domById(e)); } }); } function loadAirlines() { domById("airlinelist").innerHTML = ''; const airplane = getCurrentAircraft(); const textures = airplane.liveries[0].texture; airlineobjs.forEach(function(airline) { let airlinename = appendNewChild(domById('airlinelist'), 'li', { style: "color:" + airline.color + ";background-color:" + airline.bgcolor + "; font-weight: bold;" }); airlinename.innerText = airline.name; let removebtn = appendNewChild(airlinename, "button", { class: "mdl-button mdl-js-button mdl-button--raised mdl-button", style: "float: right; margin-top: 6px; background-color: #9e150b;", onclick: `LiverySelector.removeAirline("${airline.url}")` }); removebtn.innerText = "- Remove airline"; if (Object.keys(airline.aircrafts).includes(geofs.aircraft.instance.id)) { airline.aircrafts[geofs.aircraft.instance.id].liveries.forEach(function (e, i) { let listItem = appendNewChild(domById('airlinelist'), 'li', { id: [geofs.aircraft.instance.id, e.name, 'button'].join('_'), class: 'livery-list-item' }); if ((textures.filter(x => x === textures[0]).length === textures.length) && textures.length !== 1) { // the same texture is used for all indexes and parts const texture = e.texture[0]; listItem.onclick = () => { loadLivery(Array(textures.length).fill(texture), airplane.index, airplane.parts); if (airplane.mp != 'disabled' && whitelist.includes(airline.url.trim())) { setInstanceId({url: airline.url, idx: i}); } } } else { listItem.onclick = () => { loadLivery(e.texture, airplane.index, airplane.parts, e.materials); if (airplane.mp != 'disabled' && whitelist.includes(airline.url.trim())) { setInstanceId({url: airline.url, idx: i}); } } } listItem.innerHTML = createTag('span', { class: 'livery-name' }, e.name).outerHTML; if (e.credits && e.credits.length) { listItem.innerHTML += `by ${e.credits}`; } }); } }); } function addCustomForm() { document.querySelector('#livery-custom-tab-upload .upload-fields').innerHTML = ''; document.querySelector('#livery-custom-tab-direct .upload-fields').innerHTML = ''; const airplane = getCurrentAircraft(); const textures = airplane.liveries[0].texture.filter(t => typeof t !== 'object'); const placeholders = airplane.labels; if (textures.length){ if (textures.filter(x => x === textures[0]).length === textures.length) { // the same texture is used for all indexes and parts createUploadButton(placeholders[0]); createDirectButton(placeholders[0]); } else { placeholders.forEach((placeholder, i) => { createUploadButton(placeholder); createDirectButton(placeholder, i); }); } } if (airplane.liveries[0].materials) { airplane.liveries[0].materials.forEach((material, key) => { let partlist = []; airplane.liveries[0].texture.forEach((e, k) => { if (typeof(e) === 'object'){ if (e.material == key){ partlist.push(airplane.parts[k]); } } }); createColorChooser(material.name, Object.keys(material)[1], partlist); createUploadColorChooser(material.name, Object.keys(material)[1], partlist); }) } // click first tab to refresh button status document.querySelector('.livery-custom-tabs li').click(); } function debounceSearch (func) { let timeoutId = null; return (text) => { clearTimeout(timeoutId); if (geofs.preferences.liveryPotato) return; timeoutId = setTimeout(() => { func(text); }, 250); // debounces for 250 ms }; } const search = debounceSearch(text => { if (geofs.preferences.liveryPotato) return; const liveries = document.getElementById('liverylist').children; // .children is better than .childNodes if (text == '') { log("Potato mode: " + geofs.preferences.liveryPotato); for (const a of liveries) a.classList.toggle('geofs-visible', !geofs.preferences.liveryPotato); return; } console.log(text); text = text.toLowerCase(); // query string lowered here to avoid repeated calls for (let i = 0; i < liveries.length; i++) { const e = liveries[i] , v = e.classList.contains('geofs-visible') if (e.textContent.toLowerCase().includes(text)) { // textContent better than innerText if (!v) e.classList.add('geofs-visible'); } else { if (v) e.classList.remove('geofs-visible'); } }; }); function potatoSearch(text) { const liveries = document.getElementById('liverylist').children; if (text == '') { for (const a of liveries) a.classList.toggle('geofs-visible', false); return; } text = text.toLowerCase(); for (let i = 0; i < liveries.length; i++) { const e = liveries[i] , v = e.classList.contains('geofs-visible'); e.textContent.toLowerCase().includes(text) ? (v || e.classList.add('geofs-visible')) : (v && e.classList.remove('geofs-visible')); }; } function changeMaterial(name, color, type, partlist){ let r = parseInt(color.substring(1, 3), 16) / 255 let g = parseInt(color.substring(3, 5), 16) / 255 let b = parseInt(color.substring(5, 7), 16) / 255 partlist.forEach(part => { geofs.aircraft.instance.definition.parts[part]['3dmodel']._model.getMaterial(name).setValue(type, new Cesium.Cartesian4(r, g, b, 1.0)); }); } /** * Mark as favorite * * @param {HTMLElement} element */ function star(element) { const e = element.classList; const elementId = [element.id, 'favorite'].join('_'); let list = localStorage.getItem('favorites').split(','); if (e.contains("checked")) { domById('favorites').removeChild(domById(elementId)); const index = list.indexOf(element.id); if (index !== -1) { list.splice(index, 1); } localStorage.setItem('favorites', list); } else { const btn = domById([element.id, 'button'].join('_')); const fbtn = appendNewChild(domById('favorites'), 'li', { id: elementId, class: 'livery-list-item' }); // fbtn.onclick = btn.onclick; // moved to loadFavorites fbtn.innerText = btn.children[0].innerText; list.push(element.id); localStorage.setItem('favorites', [...new Set(list)]); } //style animation e.toggle('checked'); } /** * @param {string} id */ function createUploadButton(id) { const customDiv = document.querySelector('#livery-custom-tab-upload .upload-fields'); appendNewChild(customDiv, 'input', { type: 'file', onchange: 'LiverySelector.uploadLivery(this)' }); appendNewChild(customDiv, 'input', { type: 'text', name: 'textureInput', class: 'mdl-textfield__input address-input', placeholder: id, id: id }); appendNewChild(customDiv, 'br'); } /** * @param {string} id * @param {number} i */ function createDirectButton(id, i) { const customDiv = document.querySelector('#livery-custom-tab-direct .upload-fields'); appendNewChild(customDiv, 'input', { type: 'file', onchange: 'LiverySelector.loadLiveryDirect(this,' + i + ')' }); appendNewChild(customDiv, 'span').textContent = id; appendNewChild(customDiv, 'br'); } function createColorChooser(name, type, partlist) { const customDiv = document.querySelector('#livery-custom-tab-direct .upload-fields'); appendNewChild(customDiv, 'input', { type: 'color', name: name, class: 'colorChooser', onchange: `changeMaterial("${name}", this.value, "${type}", [${partlist}])` }); appendNewChild(customDiv, 'span', {style:'padding-top: 20px; padding-bottom: 20px;'}).textContent = name; appendNewChild(customDiv, 'br'); } function createUploadColorChooser(name, type, partlist) { const customDiv = document.querySelector('#livery-custom-tab-upload .upload-fields'); appendNewChild(customDiv, 'input', { type: 'color', name: "textureInput", id: name, class: 'colorChooser', onchange: `changeMaterial("${name}", this.value, "${type}", [${partlist}])` }); appendNewChild(customDiv, 'span', {style:'padding-top: 20px; padding-bottom: 20px;'}).textContent = name; appendNewChild(customDiv, 'br'); } /** * @param {HTMLInputElement} fileInput * @param {number} i */ function loadLiveryDirect(fileInput, i) { const reader = new FileReader(); reader.addEventListener('load', (event) => { const airplane = getCurrentAircraft(); const textures = airplane.liveries[0].texture; const newTexture = event.target.result; if (i === undefined) { loadLivery(Array(textures.length).fill(newTexture), airplane.index, airplane.parts); } else { geofs.api.changeModelTexture( geofs.aircraft.instance.definition.parts[airplane.parts[i]]["3dmodel"]._model, newTexture, { index: airplane.index[i] } ); } fileInput.value = null; }); // read file (if there is one) fileInput.files.length && reader.readAsDataURL(fileInput.files[0]); } /** * @param {HTMLInputElement} fileInput */ function uploadLivery(fileInput) { if (!fileInput.files.length) return; if (!localStorage.imgbbAPIKEY) { alert('No imgbb API key saved! Check API tab'); fileInput.value = null; return; } const form = new FormData(); form.append('image', fileInput.files[0]); if (localStorage.liveryAutoremove) form.append('expiration', (new Date() / 1000) * 60 * 60); const settings = { 'url': `https://api.imgbb.com/1/upload?key=${localStorage.imgbbAPIKEY}`, 'method': 'POST', 'timeout': 0, 'processData': false, 'mimeType': 'multipart/form-data', 'contentType': false, 'data': form }; $.ajax(settings).done(function (response) { const jx = JSON.parse(response); log(jx.data.url); fileInput.nextSibling.value = jx.data.url; fileInput.value = null; if (!uploadHistory[jx.data.id] || (uploadHistory[jx.data.id].expiration !== jx.data.expiration)) { uploadHistory[jx.data.id] = jx.data; localStorage.lsUploadHistory = JSON.stringify(uploadHistory); } }); } function handleCustomTabs(e) { e = e || window.event; const src = e.target || e.srcElement; const tabId = src.innerHTML.toLocaleLowerCase(); // iterate all divs and check if it was the one clicked, hide others domById('customDiv').querySelectorAll(':scope > div').forEach(tabDiv => { if (tabDiv.id != ['livery-custom-tab', tabId].join('-')) { tabDiv.style.display = 'none'; return; } tabDiv.style.display = ''; // special handling for each tab, could be extracted switch (tabId) { case 'upload': { const fields = tabDiv.querySelectorAll('input[type="file"]'); fields.forEach(f => localStorage.imgbbAPIKEY ? f.classList.remove('err') : f.classList.add('err')); const apiKeys = !!localStorage.liveryDiscordId && !!localStorage.imgbbAPIKEY; tabDiv.querySelector('.livery-submit .api').style.display = apiKeys ? '' : 'none'; tabDiv.querySelector('.livery-submit .no-api').style.display = apiKeys ? 'none' : ''; } break; case 'download': { reloadDownloadsForm(tabDiv); } break; case 'api': { reloadSettingsForm(); } break; } }); } /** * reloads texture files for current airplane * * @param {HTMLElement} tabDiv */ function reloadDownloadsForm(tabDiv) { const airplane = getCurrentAircraft(); const liveries = airplane.liveries; const defaults = liveries[0]; const fields = tabDiv.querySelector('.download-fields'); fields.innerHTML = ''; liveries.forEach((livery, liveryNo) => { const textures = livery.texture.filter(t => typeof t !== 'object'); if (!textures.length) return; // ignore material defs appendNewChild(fields, 'h7').textContent = livery.name; const wrap = appendNewChild(fields, 'div'); textures.forEach((href, i) => { if (typeof href === 'object') return; if (liveryNo > 0 && href == defaults.texture[i]) return; const link = appendNewChild(wrap, 'a', { href, target: '_blank', class: "mdl-button mdl-button--raised mdl-button--colored" }); link.textContent = airplane.labels[i]; }); }); } /** * reloads settings form after changes */ function reloadSettingsForm() { const apiInput = domById('livery-setting-apikey'); apiInput.placeholder = localStorage.imgbbAPIKEY ? 'API KEY SAVED ✓ (type CLEAR to remove)' : 'API KEY HERE'; const removeCheckbox = domById('livery-setting-remove'); removeCheckbox.checked = (localStorage.liveryAutoremove == 1); const discordInput = domById('livery-setting-discordid'); discordInput.value = localStorage.liveryDiscordId || ''; } /** * saves setting, gets setting key from event element * * @param {HTMLElement} element */ function saveSetting(element) { const id = element.id.replace('livery-setting-', ''); switch (id) { case 'apikey': { if (element.value.length) { if (element.value.trim().toLowerCase() == 'clear') { delete localStorage.imgbbAPIKEY; } else { localStorage.imgbbAPIKEY = element.value.trim(); } element.value = ''; } } break; case 'remove': { localStorage.liveryAutoremove = element.checked ? '1' : '0'; } break; case 'discordid': { localStorage.liveryDiscordId = element.value.trim(); } break; } reloadSettingsForm(); } async function addAirline() { let url = prompt("Enter URL to the json file of the airline:"); if (!links.includes(url)) { links.push(url); localStorage.links += `,${url}` await fetch(url).then(res => res.json()).then(data => airlineobjs.push(data)); airlineobjs[airlineobjs.length - 1].url = url.trim(); loadAirlines(); } else { alert("Airline already added"); } } function removeAirline(url) { removeItem(links, url.trim()); if (links.toString().charAt(0) === ","){ localStorage.links = links.toString().slice(1); } else { localStorage.links = links.toString(); } airlineobjs.forEach(function (e, index) { if (e.url.trim() === url.trim()) { airlineobjs.splice(index, 1); } }); loadAirlines(); } /** * @returns {object} current aircraft from liveryobj */ function getCurrentAircraft() { return liveryobj.aircrafts[geofs.aircraft.instance.id]; } function setInstanceId(id) { geofs.aircraft.instance.liveryId = id; } async function updateMultiplayer() { const users = Object.values(multiplayer.visibleUsers); const texturePromises = users.map(async u => { const liveryEntry = liveryobj.aircrafts[u.aircraft]; let textures = []; let otherId = u.currentLivery; // if (!liveryEntry || !u.model || liveryEntry.mp == 'disabled') { if (!liveryEntry || !u.model) { // TODO change back, testing return; // without livery or disabled } if (mpLiveryIds[u.id] === otherId) { return; // already updated } mpLiveryIds[u.id] = otherId; if (otherId >= ML_ID_OFFSET && otherId < LIVERY_ID_OFFSET) { textures = getMLTexture(u, liveryEntry); // ML range 1k–10k } else if ( (otherId >= LIVERY_ID_OFFSET && otherId < LIVERY_ID_OFFSET * 2) || typeof otherId === "object" ) { textures = await getMPTexture(u, liveryEntry); // LS range 10k+10k } else { return; // game-managed livery } textures.forEach(texture => { if (texture.material !== undefined) { applyMPMaterial( u.model, texture.material, texture.type, texture.color ); } else { applyMPTexture( texture.uri, texture.tex, img => u.model.changeTexture(img, { index: texture.index }) ); } }); }); await Promise.all(texturePromises); // wait for all user updates to complete } /** * Fetch and resize texture to expected format * @param {string} url * @param {sd} tex * @param {function} cb */ function applyMPTexture(url, tex, cb) { try { Cesium.Resource.fetchImage({ url }).then(img => { const canvas = createTag('canvas', { width: tex._width, height: tex._height }); canvas.getContext('2d').drawImage(img, 0, 0, canvas.width, canvas.height); cb(canvas.toDataURL('image/png')); }); } catch (e) { log(['LSMP', !!tex, url, e].join("\n")); } } function applyMPMaterial(model, name, type, color){ model._model.getMaterial(name).setValue(type, new Cesium.Cartesian4(...color, 1.0)); } /** * @param {object} u * @param {object} liveryEntry */ async function getMPTexture(u, liveryEntry) { const otherId = u.currentLivery - LIVERY_ID_OFFSET; const textures = []; log(u.currentLivery + ": " + typeof(u.currentLivery)); // check model for expected textures const uModelTextures = u.model._model._rendererResources.textures; if (!u.currentLivery) return []; // early return in case of missing livery if (typeof(u.currentLivery) === "object") { //currentLivery is object -> virtual airline liveries log("VA detected"); log(u.currentLivery); if ( mpAirlineobjs[u.currentLivery.url] === undefined) { await fetch(u.currentLivery.url).then(res => res.json()).then(data => mpAirlineobjs[u.currentLivery.url] = data); log(mpAirlineobjs[u.currentLivery.url]); } const texturePromises = liveryEntry.mp.map(async e => { if (e.textureIndex !== undefined) { return { uri: mpAirlineobjs[u.currentLivery.url].aircrafts[u.aircraft].liveries[u.currentLivery.idx].texture[e.textureIndex], tex: uModelTextures[e.modelIndex], index: e.modelIndex }; } else if (e.material !== undefined) { const mat = mpAirlineobjs[u.currentLivery.url].aircrafts[u.aircraft].liveries[u.currentLivery.idx].materials[e.material]; const typeKey = Object.keys(mat)[1]; return { material: mat.name, type: typeKey, color: mat[typeKey] }; } else if (e.mosaic !== undefined) { const mosaicTexture = await generateMosaicTexture( e.mosaic.base, e.mosaic.tiles, mpAirlineobjs[u.currentLivery.url].aircrafts[u.aircraft].liveries[u.currentLivery.idx].texture ); return { uri: mosaicTexture, tex: uModelTextures[e.modelIndex], index: e.modelIndex }; } }); const resolvedTextures = await Promise.all(texturePromises); textures.push(...resolvedTextures); } else { const texturePromises = liveryEntry.mp.map(async e => { if (e.textureIndex !== undefined) { return { uri: liveryEntry.liveries[otherId].texture[e.textureIndex], tex: uModelTextures[e.modelIndex], index: e.modelIndex }; } else if (e.material !== undefined) { const mat = liveryEntry.liveries[otherId].materials[e.material]; const typeKey = Object.keys(mat)[1]; return { material: mat.name, type: typeKey, color: mat[typeKey] }; } else if (e.mosaic !== undefined) { const mosaicTexture = await generateMosaicTexture( e.mosaic.base, e.mosaic.tiles, liveryEntry.liveries[otherId].texture ); return { uri: mosaicTexture, tex: uModelTextures[e.modelIndex], index: e.modelIndex }; } }); const resolvedTextures = await Promise.all(texturePromises); textures.push(...resolvedTextures); } log("getMPtexture\n" + textures); return textures; } /** * @param {object} u * @param {object} liveryEntry */ function getMLTexture(u, liveryEntry) { if (!mLiveries.aircraft) { fetch(atob(liveryobj.mapi)).then(data => data.json()).then(json => { Object.keys(json).forEach(key => mLiveries[key] = json[key]); }); return []; } const liveryId = u.currentLivery - ML_ID_OFFSET; const textures = []; const texIdx = liveryEntry.labels.indexOf('Texture'); if (texIdx !== -1) { textures.push({ uri: mLiveries.aircraft[liveryId].mptx, tex: u.model._model._rendererResources.textures[liveryEntry.index[texIdx]], index: liveryEntry.index[texIdx] }); } return textures; } async function generateMosaicTexture(url, tiles, textures) { const baseImage = await Cesium.Resource.fetchImage({ url }); const canvas = new OffscreenCanvas(baseImage.width, baseImage.height); const ctx = canvas.getContext('2d'); // Draw the base image first ctx.drawImage(baseImage, 0, 0, canvas.width, canvas.height); // Create an array of Promises for drawing all tiles const drawTilePromises = tiles.map(async (tile) => { const image = await Cesium.Resource.fetchImage({ url: textures[tile.textureIndex] }); ctx.drawImage( image, tile.sx, tile.sy, tile.sw, tile.sh, tile.dx, tile.dy, tile.dw, tile.dh ); }); // Wait for all tiles to be drawn await Promise.all(drawTilePromises); // Now canvas is fully rendered; return the data URL log(canvas.toDataURL()); return canvas.toDataURL('image/png'); } /******************* Utilities *********************/ /** * @param {string} id Div ID to toggle, in addition to clicked element */ function toggleDiv(id) { var $e = $(`#${id}`); $e.toggle(); $(window.event.target).toggleClass("closed", $e.css("display") === "none"); } /** * Create tag with el.setAttribute(k, attributes[k])); if (('' + content).length) { el.innerHTML = content; } return el; } /** * Creates a new element
    Potato mode:
    Favorited Liveries
      Available Liveries
        Virtual Airlines
          Load External Liveries





          `; } /** * @returns {HTMLElement} HTML template for main menu livery button */ function generatePanelButtonHTML() { const liveryButton = createTag('button', { title: 'Change livery', id: 'liverybutton', onclick: 'LiverySelector.togglePanel()', class: 'mdl-button mdl-js-button geofs-f-standard-ui geofs-mediumScreenOnly', 'data-toggle-panel': '.livery-list', 'data-tooltip-classname': 'mdl-tooltip--top', 'data-upgraded': ',MaterialButton' }); liveryButton.innerHTML = createTag('img', { src: `${noCommit}/liveryselector-logo-small.svg`, height: '30px' }).outerHTML; return liveryButton; } function togglePanel() { const p = document.getElementById('listDiv'); console.time('listLiveries'); try { p.dataset.ac != geofs.aircraft.instance.id && window.LiverySelector.listLiveries(); } catch (e) { log(e, "error"); } console.timeEnd('listLiveries'); } window.LiverySelector = { liveryobj, saveSetting, toggleDiv, loadLivery, loadLiveryDirect, handleCustomTabs, listLiveries, star, search, inputLivery, uploadLivery, submitLivery, uploadHistory, loadAirlines, addAirline, removeAirline, airlineobjs, setInstanceId, togglePanel, log, potatoSearch, };