diff --git a/applications/metadata.js b/applications/metadata.js index 2398a11..d05fa3c 100644 --- a/applications/metadata.js +++ b/applications/metadata.js @@ -333,6 +333,15 @@ var metadata = { "applications": "icon": "domainMapper/icon_inactive_white.png", "caption": "DOMAP" }, + { + "isActive": true, + "directory": "more", + "name": "MoreNG", + "description": "More app rewrite in QML. Please see documentation https://github.com/overte-org/community-apps/tree/master/applications/more for more information", + "jsfile": "more/more.js", + "icon": "more/img/icon_white.png", + "caption": "MORE-NG" + }, { "isActive": true, "directory": "hmd3rdPerson", diff --git a/applications/more/README.md b/applications/more/README.md new file mode 100644 index 0000000..0e70c89 --- /dev/null +++ b/applications/more/README.md @@ -0,0 +1,69 @@ +# More +1. What is More +2. User manual + - Installation + - Adding Repositories + - Usability tips +3. Development + - How to create your own repository + +## What is More +More is an application that allows users to quickly install additional applications hosted from online repositories. + +## User Manual +### Installation +**Recommended method**: + +To install this application, visit the pre-installed More application provided inside of Overte. + +Inside of the pre installed More application, search for the app that is also called "More". +Click the "Install" button and the More application will be installed. +You will also need to add the url Add `https://raw.githubusercontent.com/overte-org/community-apps/master/applications/more/more.qml` to the QML whitelist manually. + +**Alternative**: + +You can install More manually by following these instructions: +1. In Interface, go to Edit > Running Scripts. +2. Load the script url: `https://raw.githubusercontent.com/overte-org/community-apps/master/applications/more/more.js` +3. Add `https://raw.githubusercontent.com/overte-org/community-apps/master/applications/more/more.qml` to the QML whitelist. + +### Adding Repositories +To add a repository, navigate to the application menu by pressing the top right most button with the hamburger icon. +From there, click the section labeled "Repository Manager". In the text field labeled "Add a manifest.json url", paste in a url that provides a manifest json object. +As an example, if you wanted to provide a url to a manifest file hosted on GitHub, you would paste in something along the lines of `https://raw.githubusercontent.com/user/respository/manifest.json`. +After the url is in the text field, press the green plus button to add the repository to the list. Do note that this app does attempt to verify repositories when they are added. If you find that your repository can not be added successfully, ensure the repository is of the correct format. Also make sure that the url you are providing is correct. +If you are a repository host, ensure that your repository is set up correctly. + + +### Usability Tips +TODO + +## Development +### How to create your own repository +#### Github +To turn a GitHub repository into a Overte repository provider, you need to make your GitHub repository a "[GitHub Pages](https://pages.github.com/)" repository. +The defaults provided by GitHub will be sufficient for this use case. Select the root of the repository as the GitHub Pages root. +After you have ensured that your repository is set up correctly, you can add the `manifest.json` file to the root of your repository. + +This is the format to use: +```json +{ + "title": "My GitHub Repository", // This is the name that will show up in the repository manager + "base_url": "https://raw.githubusercontent.com/myuser/myrepository", // This is what the More app uses as a base to search for applications provided by the 'applications' key just below this entry. + + // This is the list of all applications this repository will provide to the More app. + "applications": [ + { + "name": "My App", // The name of the application to display. + "directory": "myapp", // The directory of the application relative to the 'base_url'. This will be interpreted as 'https://raw.githubusercontent.com/myuser/myrepository/myapp' internally. + "script": "myapp.js", // The entry script of the application. + "icon": "icon.png", // The icon of the application to show in the list. + "description": "This is my first application! Download this please." // The description of the application to display in the "details" page. + }, + /// ...and other applications + ] +} +``` + +#### HTTPS Servers +To provide the applications from a standard HTTPS server, the procedure is largely the same as with GitHub. You simply need to provide a JSON response that is in the same format as the above "GitHub" format. \ No newline at end of file diff --git a/applications/more/img/icon_black.png b/applications/more/img/icon_black.png new file mode 100644 index 0000000..47c4f63 Binary files /dev/null and b/applications/more/img/icon_black.png differ diff --git a/applications/more/img/icon_white.png b/applications/more/img/icon_white.png new file mode 100644 index 0000000..8764e6a Binary files /dev/null and b/applications/more/img/icon_white.png differ diff --git a/applications/more/menu.svg b/applications/more/menu.svg new file mode 100644 index 0000000..8d3273d --- /dev/null +++ b/applications/more/menu.svg @@ -0,0 +1,38 @@ + + + + + + diff --git a/applications/more/more.js b/applications/more/more.js new file mode 100644 index 0000000..65be984 --- /dev/null +++ b/applications/more/more.js @@ -0,0 +1,277 @@ +// +// 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); + } +})(); diff --git a/applications/more/more.qml b/applications/more/more.qml new file mode 100644 index 0000000..c6e074b --- /dev/null +++ b/applications/more/more.qml @@ -0,0 +1,950 @@ +import QtQuick 2.7 +import QtQuick.Controls 2.0 +import QtQuick.Layouts 1.3 + +Rectangle { + color: Qt.rgba(0.1,0.1,0.1,1) + signal sendToScript(var message); + width: 200 + height: 700 + id: root + + property string current_page: "app_list" + + Timer { + interval: 10 + running: true + repeat: false + onTriggered: { + toScript({type: "initialized"}); + } + } + + // User view + Item { + anchors.fill: parent + + // Navigation Bar + Rectangle { + id: navigation_bar + width: parent.width + height: 60 + color: Qt.rgba(0,0,0,1) + visible: current_page == "app_list" + + Item { + anchors.centerIn: parent + width: parent.width - 20 + height: parent.height - 25 + + Rectangle { + color: "white" + width: parent.width - 100 + anchors.verticalCenter: parent.verticalCenter + height: parent.height + radius: 5 + + TextInput { + width: parent.width - 10 + color: "black" + font.pointSize: 12 + anchors.centerIn: parent + id: search_query + onAccepted: { + if (current_page == "app_list"){ + searchList(search_query.text, app_listings); + return; + } + // if (current_page == "repos"){ + // searchList(search_query.text, repo_list); + // return; + // } + } + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: parent.parent.children[1].x = 15 + onExited: parent.parent.children[1].x = 5 + + onClicked: (mouse) => { + parent.forceActiveFocus() // Hack? Maybe see if this can be better done another way + } + } + } + + Text { + color: "Gray" + font.pointSize: 10 + anchors.verticalCenter: parent.verticalCenter + x: 5 + text: "Search..." + font.italic: true + visible: parent.children[0].text == "" + + Behavior on x { + NumberAnimation { + duration: 100 + } + } + + } + + } + + Rectangle { + color: "#296992" + width: parent.width - parent.children[0].width - 10 + anchors.verticalCenter: parent.verticalCenter + height: parent.height + radius: 5 + anchors.right: parent.right + + Image { + source: "menu.svg" + anchors.centerIn: parent + sourceSize.width: 20 + sourceSize.height: 20 + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: parent.color = "#122F41" + onExited: parent.color = "#296992" + + onClicked: { + root.current_page = "page_selection" + } + } + } + } + } + + // Go back button from app details + Rectangle { + id: go_back_button + width: parent.width + height: 60 + color: Qt.rgba(0,0,0,1) + visible: current_page != "app_list" + + Rectangle { + width: parent.width - 20 + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + height: 35 + radius: 5 + color: "#771d1d" + + Text { + color: "white" + font.pointSize: 12 + anchors.centerIn: parent + text: "Back" + } + + MouseArea { + anchors.fill: parent + + hoverEnabled: true + onEntered: parent.color = "#471111" + onExited: parent.color = "#771d1d" + + onClicked: { + if (current_page == "page_selection") return current_page = "app_list"; + if (current_page == "details") return current_page = "app_list"; + current_page = "page_selection" + } + } + } + } + + // Pages ---- + + // Apps Listing + Item { + width: parent.width + height: parent.height + anchors.top: navigation_bar.bottom + visible: current_page == "app_list" + + // Installed Apps + ListView { + property int index_selected: -1 + width: parent.width + height: parent.height - 60 + clip: true + interactive: true + id: app_listing_list + model: app_listings + + delegate: Loader { + property int delegateIndex: index + property string delegateTitle: model.title + property string delegateRepository: model.repository + property string delegateDescription: model.description + property string delegateIcon: model.icon + property string delegateURL: model.url + property bool delegateInstalled: model.installed + property bool delegateIsVisible: model.is_visible + width: app_listing_list.width + + sourceComponent: app_listing + } + } + + ListModel { + id: app_listings + } + } + + // Installed Apps + Item { + width: parent.width + height: parent.height - 40 + anchors.top: navigation_bar.bottom + visible: current_page == "installed_app_list" + + // Installed Apps + ListView { + property int index_selected: -1 + width: parent.width + height: parent.height - 60 + clip: true + interactive: true + id: installed_apps_list + model: installed_apps + + delegate: Loader { + property int delegateIndex: index + property string delegateTitle: model.title + property string delegateRepository: model.repository + property string delegateDescription: model.description + property string delegateIcon: model.icon + property string delegateURL: model.url + property bool delegateInstalled: model.installed + property bool delegateIsVisible: model.is_visible + width: installed_apps_list.width + + sourceComponent: app_listing + } + } + + ListModel { + id: installed_apps + } + } + + // Page selection + Item { + width: parent.width + height: parent.height - 40 + anchors.top: navigation_bar.bottom + visible: current_page == "page_selection" + + // Installed Apps + ListView { + property int index_selected: -1 + width: parent.width + height: parent.height - 60 + clip: true + interactive: true + model: ListModel { + + // TODO: + ListElement { + name: "Installed Apps" + description: "View a list of applications installed" + page_name: "installed_app_list" + } + ListElement { + name: "Repository Manager" + description: "Manage your list of repositories" + page_name: "repos" + } + + + } + + delegate: Component { + Rectangle { + width: parent.width + height: 60 + color: index % 2 === 0 ? "transparent" : Qt.rgba(0.15,0.15,0.15,1) + + Item { + height: parent.height + width: parent.width - 40 + + Behavior on x { + NumberAnimation { + duration: 150 + } + } + + Text { + width: parent.width - 50 + y: 5 + height: 30 + text: name + font.pixelSize: 16 + color: "white" + anchors.horizontalCenter: parent.horizontalCenter + font.italic: true + } + Text { + width: parent.width - 50 + height: 15 + anchors.top: parent.children[0].bottom + text: description + font.pixelSize: 12 + color: "white" + anchors.horizontalCenter: parent.horizontalCenter + font.italic: true + } + } + + Text { + width: 50 + height: parent.height + text: ">" + color: "transparent" + x: parent.width - 150 + font.pixelSize: 40 + anchors.verticalCenter: parent.verticalCenter + + Behavior on x { + NumberAnimation { + duration: 150 + } + } + Behavior on color { + ColorAnimation { + duration: 150 + } + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + + onEntered: { + parent.color = "#111111" + + parent.children[0].x = parent.children[0].x + 20 + + // Arrow + parent.children[1].x = parent.width - 50 + parent.children[1].color = "white" + } + onExited: { + parent.color = index % 2 === 0 ? "transparent" : Qt.rgba(0.15,0.15,0.15,1) + + parent.children[0].x = 0 + + // Arrow + parent.children[1].x = parent.width - 150 + parent.children[1].color = "transparent" + } + + onClicked: (mouse) => { + current_page = page_name + } + } + } + } + } + } + + // Repository Manager + Item { + width: parent.width + height: parent.height - 40 + anchors.top: navigation_bar.bottom + visible: current_page == "repos" + + Rectangle { + height: 70 + width: parent.width + color: "#111111" + + Item { + width: parent.width - 10 + height: parent.height + anchors.horizontalCenter: parent.horizontalCenter + + + Text{ + text: "Add a new repository" + color: "White" + font.pointSize: 12 + wrapMode: Text.WordWrap + height: 30 + } + + Rectangle{ + width: parent.width - 70 + height: 30 + radius: 5 + anchors.top: parent.children[0].bottom + + TextInput { + width: parent.width - 10 + color: "black" + font.pointSize: 12 + anchors.centerIn: parent + id: repo_url + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: parent.parent.children[1].x = 15 + onExited: parent.parent.children[1].x = 5 + + onClicked: (mouse) => { + parent.forceActiveFocus() // Hack? Maybe see if this can be better done another way + } + } + } + + Text { + color: "Gray" + font.pointSize: 10 + anchors.verticalCenter: parent.verticalCenter + x: 5 + text: "Add a manifest.json url" + font.italic: true + visible: parent.children[0].text == "" + Behavior on x { + NumberAnimation { + duration: 100 + } + } + } + } + + Rectangle { + anchors.top: parent.children[0].bottom + width: parent.width - parent.children[1].width - 10 + anchors.right: parent.right + height: 30 + color: "green" + radius: 5 + + Text { + text: "+" + color: "White" + font.pointSize: 14 + anchors.centerIn: parent + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: parent.color = "#004D00" + onExited: parent.color = "green" + + onClicked: { + installNewRepository(repo_url.text); + repo_url.text = ""; + } + } + } + } + + } + + ListView { + property int index_selected: -1 + width: parent.width + height: parent.height - 60 + clip: true + interactive: true + spacing: 5 + id: registered_repo_list + model: repo_list + anchors.top: parent.children[0].bottom + delegate: Loader { + property int delegateIndex: index + property string delegateTitle: model.title + property string delegateURL: model.url + property bool selected: false + property bool delegateIsVisible: model.is_visible + + width: registered_repo_list.width + + sourceComponent: repo_listing + } + } + ListModel { + id: repo_list + } + } + + // App Details + Item { + width: parent.width - 20 + height: parent.height - 40 + anchors.top: navigation_bar.bottom + visible: current_page == "details" + anchors.horizontalCenter: parent.horizontalCenter + + Item { + width: parent.width + height: 100 + y: 10 + + + Rectangle{ + width: 100 + height: 100 + radius: 5 + + Rectangle { + color: "black" + width: 96 + height: 96 + radius: 5 + anchors.centerIn: parent + + Image { + id: details_icon + width: 90 + height: 90 + anchors.centerIn: parent + } + } + } + + Text { + x: parent.children[0].width + 10 + text: "" + color:"white"; + font.pointSize: 14 + id: details_title + } + + Text { + x: parent.children[0].width + 10 + y: parent.children[1].height + 5 + text: "" + color: "gray"; + font.pointSize: 10 + id: details_repo_url + } + } + + Item { + width: parent.width + anchors.top: parent.children[0].bottom + + Text{ + text: "" + color: "white"; + wrapMode: Text.WordWrap + width: parent.width + font.pointSize: 12 + y: 20 + id: details_description + } + } + + } + } + + // Templates + Component { + id: app_listing + + Rectangle { + property int index: delegateIndex + property string title: delegateTitle + property string repo: delegateRepository + property string description: delegateDescription + property string icon: delegateIcon + property string url: delegateURL + property bool installed: delegateInstalled + property bool is_visible: delegateIsVisible + + property bool selected: (app_listing_list.index_selected == index) + + visible: is_visible + height: is_visible ? selected ? 100 : 60 : 0 + width: parent.width + + color: index % 2 === 0 ? "transparent" : Qt.rgba(0.15,0.15,0.15,1) + + Behavior on height { + NumberAnimation { + duration: 100 + } + } + + Item { + width: parent.width - 10 + anchors.horizontalCenter: parent.horizontalCenter + height: parent.height + clip: true + + // Icon + Rectangle { + width: 50 + height: 50 + radius: 5 + color: installed ? "#505186" : "white" + y: 5 + + Rectangle{ + anchors.centerIn: parent + width: 46 + height: 46 + radius: 5 + color: "black" + + Image { + source: icon + anchors.centerIn: parent + sourceSize.width: 40 + sourceSize.height: 40 + } + } + } + + // App info + Item { + width: parent.width - parent.children[0].width - 50 + x: parent.children[0].width + 10 + height: 20 + + Text { + width: parent.width + height: 20 + text: title + color: "white" + font.pointSize: 12 + wrapMode: Text.NoWrap + elide: Text.ElideRight + } + Text { + width: parent.width + height: 20 + text: repo + color: "gray" + font.pointSize: 10 + anchors.top: parent.children[0].bottom + } + } + + // Action Buttons + Item { + width: parent.width + height: 30 + + y: 65 + visible: selected ? true : false + + + Rectangle { + width: 120 + height: parent.height + radius: 5 + color: "#771d1d" + visible: installed + + Text{ + text: "Uninstall" + anchors.centerIn: parent + color:"white" + } + + MouseArea { + anchors.fill: parent + + hoverEnabled: true + onEntered: parent.color = "#471111" + onExited: parent.color = "#771d1d" + + onClicked: { + removeApp(url); + } + } + + } + + Rectangle { + width: 120 + height: parent.height + radius: 5 + color: "#00930f" + visible: !installed + + Text{ + text: "Install" + anchors.centerIn: parent + color:"white" + } + + MouseArea { + anchors.fill: parent + + hoverEnabled: true + onEntered: parent.color = "#005809" + onExited: parent.color = "#00930f" + + onClicked: { + installNewApp(title, url, repo, description, icon); + } + } + } + + Rectangle { + width: 120 + height: parent.height + radius: 5 + color: "#505186" + x: parent.children[0].width + 5 + + Text { + text: "Details" + anchors.centerIn: parent + color:"white" + } + + MouseArea { + anchors.fill: parent + + hoverEnabled: true + onEntered: parent.color = "#303150" + onExited: parent.color = "#505186" + + onClicked: { + openAppDetails(title, url, repo, description, icon); + } + } + } + } + + MouseArea { + width: parent.width + height: 60 + + hoverEnabled: true + onEntered: { + parent.parent.color = "#111111" + } + onExited: { + parent.parent.color = parent.parent.index % 2 === 0 ? "transparent" : Qt.rgba(0.15,0.15,0.15,1) + } + + onClicked: { + if (app_listing_list.index_selected == index){ + app_listing_list.index_selected = -1; + return; + } + + app_listing_list.index_selected = index + } + } + + } + } + } + + Component { + id: repo_listing + + Rectangle { + property int index: delegateIndex + property string title: delegateTitle + property string url: delegateURL + property bool is_visible: delegateIsVisible + + property bool selected: (registered_repo_list.index_selected == index) + + height: selected ? 70 : 40 + width: parent.width + visible: is_visible + color: index % 2 === 0 ? "transparent" : Qt.rgba(0.15,0.15,0.15,1) + clip: true + + Behavior on height { + NumberAnimation { + duration: 100 + easing.type: Easing.OutQuad + } + } + + Item { + width: parent.width - 10 + anchors.horizontalCenter: parent.horizontalCenter + height: parent.height + + // Repo Info + Text { + width: parent.width + height: 20 + text: title + color: "white" + font.pointSize: 12 + wrapMode: Text.NoWrap + elide: Text.ElideRight + } + Text { + width: parent.width + height: 20 + anchors.top: parent.children[0].bottom + text: url + color: "gray" + font.pointSize: 10 + wrapMode: Text.NoWrap + elide: Text.ElideRight + } + + // Action Buttons + Item { + height: selected ? 30 : 0 + width: parent.width + anchors.top: parent.children[1].bottom + visible: selected ? true : false + + Rectangle { + width: 120 + height: parent.height + radius: 5 + color: "#771d1d" + + Text { + text: "Remove" + anchors.centerIn: parent + color:"white" + } + + MouseArea { + anchors.fill: parent + + hoverEnabled: true + onEntered: parent.color = "#471111" + onExited: parent.color = "#771d1d" + + onClicked: { + removeRepository(url); + } + } + } + } + + } + + MouseArea { + width: parent.width + height: 40 + + hoverEnabled: true + onEntered: { + parent.color = "#111111" + } + onExited: { + parent.color = parent.index % 2 === 0 ? "transparent" : Qt.rgba(0.15,0.15,0.15,1) + } + + onClicked: { + if (registered_repo_list.index_selected == index){ + registered_repo_list.index_selected = -1; + return; + } + + registered_repo_list.index_selected = index; + } + } + + } + } + + // List population and management + function addApplicationsToList(message){ + message.app_list.forEach((app) => { + app_listings.append({title: app.title, repository: app.repository, description: app.description, icon: app.icon, url: app.url, installed: app.installed || false, is_visible: true }); + + if (app.installed){ + installed_apps.append({title: app.title, repository: app.repository, description: app.description, icon: app.icon, url: app.url, installed: true, is_visible: true }); + } + }) + } + function clearApplicationList(){ + app_listings.clear() + installed_apps.clear() + app_listing_list.index_selected = -1; + } + function addRepositoriesToList(message){ + message.repository_list.forEach((repo) => repo_list.append({ title: repo.title, url: repo.url, is_visible: true })) + } + function clearRepositoryList(){ + repo_list.clear() + registered_repo_list.index_selected = -1; + } + + // Funcionality + function installNewRepository(url){ + toScript({type: "install_repo", url: url}); + } + function removeRepository(url){ + toScript({type: "remove_repo", url: url}); + } + function installNewApp(title, url, repository, description, icon){ + toScript({type: "install_application", title: title, url: url, repository: repository, description: description, icon: icon}); + } + function removeApp(url){ + toScript({type: "remove_application", url: url}); + } + + // Searching + function searchList(text, element){ + + for (var i = 0; i < element.count; i++) { + var app = element.get(i); + + var is_found = app.title.toLowerCase().includes(text.toLowerCase()) || app.description.toLowerCase().includes(text.toLowerCase()) || app.url.toLowerCase().includes(text.toLowerCase()) + + if (!app.title.toLowerCase().includes(text.toLowerCase())){ + app.is_visible = false; + } + else { + app.is_visible = true + } + + } + } + + // App Details page + function openAppDetails(title, url, repo, description, icon){ + current_page = "details"; + details_title.text = title; + details_repo_url.text = repo; + details_description.text = description; + details_icon.source = icon; + } + + // Messages from script + function fromScript(message) { + switch (message.type){ + case "installed_apps": + clearApplicationList(); + addApplicationsToList(message); + break; + case "installed_repositories": + clearRepositoryList(); + addRepositoriesToList(message) + break; + case "clear_messages": + break; + case "initial_settings": + break; + } + } + + // Send message to script + function toScript(packet){ + sendToScript(packet) + } +}