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
Upload
Direct
Download
API
Paste URL or upload image to generate ImgBB URL
Contribute to the LiverySelector Database
-> Enter your ImgBB API Key and Discord User ID in the API tab.
Join our Discord to follow up on your contributions.
By submitting the livery, you agree to the Discord Terms of Service and our Discord server rules. Failing to comply may result in your submissions being ignored.