mirror of
https://github.com/overte-org/community-apps.git
synced 2025-04-05 21:22:00 +02:00
277 lines
7.5 KiB
JavaScript
277 lines
7.5 KiB
JavaScript
//
|
|
// more.js
|
|
//
|
|
// Easily install additional functionality from repositories online
|
|
//
|
|
// Created by Armored Dragon, 2024.
|
|
// Copyright 2024 Overte e.V.
|
|
//
|
|
// Distributed under the Apache License, Version 2.0.
|
|
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
|
|
|
(() => {
|
|
("use strict");
|
|
|
|
var installedScripts = Settings.getValue("ArmoredMore-InstalledScripts", []) || []; // All scripts installed though more.js
|
|
var installedRepositories = Settings.getValue("ArmoredMore-InstalledRepositories", []) || []; // All repositories installed though more.js
|
|
var isFirstRun = Settings.getValue("ArmoredMore-FirstRun", true); // Check if this app has ran before
|
|
|
|
// Global vars
|
|
var tablet;
|
|
var appButton;
|
|
var active = false;
|
|
|
|
tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system");
|
|
tablet.screenChanged.connect(onScreenChanged);
|
|
|
|
appButton = tablet.addButton({
|
|
icon: Script.resolvePath("./img/icon_white.png"),
|
|
activeIcon: Script.resolvePath("./img/icon_black.png"),
|
|
text: "MORE",
|
|
isActive: active,
|
|
});
|
|
// When script ends, remove itself from tablet
|
|
Script.scriptEnding.connect(function () {
|
|
console.log("Shutting Down");
|
|
tablet.removeButton(appButton);
|
|
});
|
|
|
|
// Overlay button toggle
|
|
appButton.clicked.connect(toolbarButtonClicked);
|
|
|
|
tablet.fromQml.connect(fromQML);
|
|
|
|
if (isFirstRun) {
|
|
installRepo("https://more.overte.org/applications/metadata.js");
|
|
Settings.setValue("ArmoredMore-FirstRun", false);
|
|
isFirstRun = false;
|
|
}
|
|
|
|
function toolbarButtonClicked() {
|
|
if (active) {
|
|
tablet.gotoHomeScreen();
|
|
active = !active;
|
|
appButton.editProperties({
|
|
isActive: active,
|
|
});
|
|
} else {
|
|
getLists();
|
|
tablet.loadQMLSource(Script.resolvePath("./more.qml"));
|
|
active = !active;
|
|
appButton.editProperties({
|
|
isActive: active,
|
|
});
|
|
}
|
|
}
|
|
|
|
function installApp({ title, repository, url, icon, description }) {
|
|
// Add script to saved list
|
|
installedScripts.push({
|
|
title: title,
|
|
repository: repository,
|
|
url: url,
|
|
icon: icon,
|
|
description: description,
|
|
});
|
|
|
|
// Save new list as setting
|
|
Settings.setValue("ArmoredMore-InstalledScripts", installedScripts);
|
|
|
|
// Install the script
|
|
ScriptDiscoveryService.loadScript(url, true); // Force reload the script, do not use cache.
|
|
|
|
// Send updated app list
|
|
getLists();
|
|
}
|
|
|
|
function uninstallApp(url) {
|
|
// Find app in saved list
|
|
var entry = installedScripts.filter((app) => app.url == url);
|
|
const index = installedScripts.indexOf(entry);
|
|
|
|
// Remove it from list
|
|
installedScripts.splice(index, 1);
|
|
|
|
// Save new list as setting
|
|
Settings.setValue("ArmoredMore-InstalledScripts", installedScripts);
|
|
|
|
// Uninstall the script
|
|
ScriptDiscoveryService.stopScript(url, false);
|
|
|
|
// Send updated app list
|
|
getLists();
|
|
}
|
|
|
|
async function installRepo(url) {
|
|
// Hardcode support for Overte
|
|
if (url === "https://raw.githubusercontent.com/overte-org/community-apps/master/applications/metadata.js")
|
|
url = "https://more.overte.org/applications/metadata.js"
|
|
|
|
var repoIsInstalled = installedRepositories.find((repo) => repo.url === url) ? true : false;
|
|
if (repoIsInstalled) return; // Repository URL already in the list, don't add it again.
|
|
|
|
// Test repository
|
|
const repo = await request(url);
|
|
if (!repo) return; // Failure
|
|
|
|
// Add repo to saved list
|
|
installedRepositories.push({
|
|
title: repo.title || "Unnamed repository",
|
|
url: url,
|
|
});
|
|
|
|
// Save new list as setting
|
|
Settings.setValue("ArmoredMore-InstalledRepositories", installedRepositories);
|
|
|
|
// Send updated repository list
|
|
getLists();
|
|
}
|
|
function uninstallRepo(url) {
|
|
// Find app in saved list
|
|
var entry = installedRepositories.filter((repo) => repo.url == url);
|
|
const index = installedRepositories.indexOf(entry);
|
|
|
|
// Remove it from list
|
|
installedRepositories.splice(index, 1);
|
|
|
|
// Save new list as setting
|
|
Settings.setValue("ArmoredMore-InstalledRepositories", installedRepositories);
|
|
|
|
// Send updated app list
|
|
getLists();
|
|
}
|
|
|
|
// Startup populate lists
|
|
async function getLists() {
|
|
let applicationList = [];
|
|
let installedAppsByUrl = installedScripts.map((app) => app.url);
|
|
|
|
for (let i = 0; installedRepositories.length > i; i++) {
|
|
let repo = installedRepositories[i];
|
|
let repoContent = await request(repo.url);
|
|
if (!repoContent) continue; // Failure
|
|
|
|
let apps = repoContent.application_list || [];
|
|
|
|
// Filter to non-installed ones
|
|
apps = apps.filter((app) => {
|
|
let appRoot = repo.url.replace(/\/metadata.js(?:on)?/g, "") + `/${app.directory}`;
|
|
|
|
let scriptUrl = appRoot + `/${app.script}`;
|
|
|
|
return installedAppsByUrl.indexOf(scriptUrl) == -1;
|
|
});
|
|
|
|
apps = apps.map((app) => {
|
|
let appRoot = repoContent.base_url + `/${app.directory}`;
|
|
|
|
let scriptUrl = appRoot + `/${app.script}`;
|
|
let scriptIcon = appRoot + `/${app.icon}`;
|
|
|
|
return {
|
|
title: app.name,
|
|
description: app.description,
|
|
icon: scriptIcon,
|
|
repository: repo.title,
|
|
url: scriptUrl,
|
|
};
|
|
});
|
|
|
|
// Add all apps from repo to list
|
|
applicationList.push(...apps);
|
|
}
|
|
|
|
_emitEvent({
|
|
type: "installed_apps",
|
|
app_list: [
|
|
...installedScripts.map((app) => {
|
|
return { ...app, installed: true };
|
|
}),
|
|
...applicationList,
|
|
],
|
|
});
|
|
|
|
_emitEvent({
|
|
type: "installed_repositories",
|
|
repository_list: installedRepositories,
|
|
});
|
|
}
|
|
|
|
function onScreenChanged(type, url) {
|
|
if (url != Script.resolvePath("./more.qml")) {
|
|
active = false;
|
|
appButton.editProperties({
|
|
isActive: active,
|
|
});
|
|
}
|
|
}
|
|
|
|
async function request(url) {
|
|
var xmlHttp = new XMLHttpRequest();
|
|
xmlHttp.open("GET", url, false);
|
|
xmlHttp.send(null);
|
|
|
|
// Hardcode support for Overte Community-Apps metadata.js
|
|
// This can be safely removed at some point in the far future. 7/18/2024
|
|
if (url === "https://more.overte.org/applications/metadata.js") {
|
|
// Scary text formatting to get the metadata.js response object into a JSON object.
|
|
var formattedResponse = xmlHttp.responseText.replace("var metadata = ", "").slice(0, -1).trim();
|
|
|
|
// Extract the application list.
|
|
var applicationList = JSON.parse(formattedResponse).applications;
|
|
|
|
// Convert each entry into a value we expect it to be.
|
|
applicationList = applicationList.map((appEntry) => {
|
|
return {
|
|
name: appEntry.name,
|
|
directory: appEntry.directory,
|
|
script: appEntry.jsfile.replace(`${appEntry.directory}/`, ""),
|
|
icon: appEntry.icon.replace(`${appEntry.directory}/`, ""),
|
|
description: appEntry.description,
|
|
};
|
|
});
|
|
|
|
// Return the formatted list along with extra repository information.
|
|
return { title: "Overte", base_url: "https://more.overte.org/applications", application_list: applicationList };
|
|
}
|
|
|
|
// Any request we make is intended to be a JSON response.
|
|
// If it can not be parsed into JSON then fail.
|
|
try {
|
|
return JSON.parse(xmlHttp.responseText);
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function fromQML(event) {
|
|
console.log(`New QML event:\n${JSON.stringify(event)}`);
|
|
|
|
switch (event.type) {
|
|
case "initialized":
|
|
getLists();
|
|
break;
|
|
case "install_application":
|
|
installApp(event);
|
|
break;
|
|
case "remove_application":
|
|
uninstallApp(event.url);
|
|
break;
|
|
case "install_repo":
|
|
installRepo(event.url);
|
|
break;
|
|
case "remove_repo":
|
|
uninstallRepo(event.url);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Emit a packet to the HTML front end. Easy communication!
|
|
* @param {Object} packet - The Object packet to emit to the HTML
|
|
* @param {("show_message"|"clear_messages"|"notification"|"initial_settings")} packet.type - The type of packet it is
|
|
*/
|
|
function _emitEvent(packet = { type: "" }) {
|
|
tablet.sendToQml(packet);
|
|
}
|
|
})();
|