From 13a587bf39bc7ad4ec2948348aa3a5bf7187a04f Mon Sep 17 00:00:00 2001 From: mchaptel Date: Mon, 6 Jul 2020 00:03:27 +0200 Subject: [PATCH 001/112] beggining of adding icons --- ExtensionStore/lib/store.js | 102 +++++++++++++++++++++++++----------- README.md | 2 +- 2 files changed, 72 insertions(+), 32 deletions(-) diff --git a/ExtensionStore/lib/store.js b/ExtensionStore/lib/store.js index a00fb7a..e2c7c68 100644 --- a/ExtensionStore/lib/store.js +++ b/ExtensionStore/lib/store.js @@ -485,8 +485,8 @@ Object.defineProperty(Repository.prototype, "contents", { // this._contents = tree; var files = tree.map(function (file) { - if (file.type == "tree") return {path:"/" + file.path + "/", size: file.size}; - if (file.type == "blob") return {path:"/" + file.path, size: file.size}; + if (file.type == "tree") return { path: "/" + file.path + "/", size: file.size }; + if (file.type == "blob") return { path: "/" + file.path, size: file.size }; }) this._contents = files; } @@ -572,7 +572,7 @@ Repository.prototype.getFiles = function (filter) { if (typeof filter === 'undefined') var filter = /.*/; var contents = this.contents; - var paths = this.contents.map(function(x){return x.path}) + var paths = this.contents.map(function (x) { return x.path }) this.log.debug(paths.join("\n")) var search = this.searchToRe(filter) @@ -582,7 +582,7 @@ Repository.prototype.getFiles = function (filter) { var results = [] for (var i in paths) { // add files that match the filter but not folders - if (paths[i].match(search) && paths[i].slice(-1)!="/") results.push(contents[i]) + if (paths[i].match(search) && paths[i].slice(-1) != "/") results.push(contents[i]) } return results; @@ -598,9 +598,9 @@ Repository.prototype.searchToRe = function (search) { // sanitize input to prevent broken regex search = search.replace(/\./g, "\\.") - // .replace(/\*/g, "[^/]*") // this is to avoid selecting subfolders contents but do we want that? - .replace(/\*/g, ".*") - .replace(/\?/g, "."); + // .replace(/\*/g, "[^/]*") // this is to avoid selecting subfolders contents but do we want that? + .replace(/\*/g, ".*") + .replace(/\?/g, "."); var searchRe = new RegExp("^" + search + "$", "i"); @@ -676,14 +676,14 @@ Object.defineProperty(Extension.prototype, "rootFolder", { if (typeof this._rootFolder === 'undefined') { var files = this.package.files; if (files.length == 1) { - this._rootFolder = files[0].slice(0, files[0].lastIndexOf("/")+1); + this._rootFolder = files[0].slice(0, files[0].lastIndexOf("/") + 1); } else { var folders = files[0].split("/"); var rootFolder = ""; mainLoop: for (var i = 0; i < folders.length; i++) { - var folder = folders.slice(0, i).join("/")+"/"; + var folder = folders.slice(0, i).join("/") + "/"; for (var j in files) { if (files[j].indexOf(folder) == -1) break mainLoop; } @@ -692,7 +692,7 @@ Object.defineProperty(Extension.prototype, "rootFolder", { this._rootFolder = rootFolder; } } - this.log.debug("rootfolder: "+this._rootFolder) + this.log.debug("rootfolder: " + this._rootFolder) return this._rootFolder; } }); @@ -728,8 +728,8 @@ Object.defineProperty(Extension.prototype, "files", { var files = []; for (var i in packageFiles) { - this.log.debug("getting extension files matching : "+packageFiles[i]) - var results = this.repository.getFiles("/"+ packageFiles[i]); + this.log.debug("getting extension files matching : " + packageFiles[i]) + var results = this.repository.getFiles("/" + packageFiles[i]); if (results.length > 0) files = files.concat(results); } @@ -747,7 +747,7 @@ Object.defineProperty(Extension.prototype, "files", { */ Object.defineProperty(Extension.prototype, "id", { get: function () { - if (typeof this._id === 'undefined'){ + if (typeof this._id === 'undefined') { var repoName = this.package.repository.replace("https://github.com/", "") var id = (repoName + this.name).replace(/ /g, "_") this._id = id; @@ -775,6 +775,45 @@ Object.defineProperty(Extension.prototype, "localPaths", { }) + +/** + * The ExtensionDownloader instance to handle the downloads for this extension + */ +Object.defineProperty(Extension.prototype, "downloader", { + get: function () { + if (typeof this._downloader === 'undefined') { + this._downloader = new ExtensionDownloader(this); + } + return this._downloader + } +}) + + +/** + * gets the extension icon file. Can provide a callback to execute once the icon has been obtained. + */ + +Extension.prototype.getIcon = function (callback){ + get: function () { + var icon = listFiles(this.downloader.cacheFolder, this.safeName + "_icon.png") + if (icon.length == 0){ + // look for an icon in the repo + } + } +}) + + +/** + * Cleans the problematic characters from the name of the extension. + */ +Object.defineProperty(Extension.prototype, safeName, { + get: function () { + return this.name.replace(/ /g, "_") + .replace(/[:\?\*\\\/"\|\<\>]/g, "") + } +}) + + /** * Output a json of the package of the extension */ @@ -803,8 +842,8 @@ Extension.prototype.matchesSearch = function (search) { * @param {string} version a semantic version string separated by dots. */ Extension.prototype.currentVersionIsOlder = function (version) { - version = version.split(".").map(function(x){return parseInt(x, 10)}); - var ownVersion = this.version.split(".").map(function(x){return parseInt(x, 10)}); + version = version.split(".").map(function (x) { return parseInt(x, 10) }); + var ownVersion = this.version.split(".").map(function (x) { return parseInt(x, 10) }); var length = Math.max(version.length > ownVersion.length); @@ -940,7 +979,7 @@ LocalExtensionList.prototype.checkFiles = function (extension) { */ LocalExtensionList.prototype.install = function (extension) { // if (this.isInstalled(extension)) return true; // extension is already installed - var downloader = new ExtensionDownloader(extension); // dedicated object to implement threaded download later + var downloader = extension.downloader; var installLocation = this.installLocation(extension) var files = downloader.downloadFiles(); @@ -1087,19 +1126,19 @@ LocalExtensionList.prototype.createListFile = function (store) { * Access the custom settings */ Object.defineProperty(LocalExtensionList.prototype, "settings", { - get: function(){ - if (typeof this._settings === 'undefined'){ + get: function () { + if (typeof this._settings === 'undefined') { var ini = readFile(this._ini) - if (!ini){ + if (!ini) { var prefs = {}; - }else{ + } else { var prefs = JSON.parse(ini); } this._settings = prefs; } return this._settings; }, - set: function(settingsObject){ + set: function (settingsObject) { writeFile(this._ini, JSON.stringify(settingsObject, null, " ")) } }) @@ -1109,7 +1148,7 @@ Object.defineProperty(LocalExtensionList.prototype, "settings", { * @param {string} name * @param {string} value */ -LocalExtensionList.prototype.saveData = function(name, value){ +LocalExtensionList.prototype.saveData = function (name, value) { this.log.debug("saving data ", JSON.stringify(value, null, " "), "under name", name) var prefs = this.settings; prefs[name] = value; @@ -1122,7 +1161,7 @@ LocalExtensionList.prototype.saveData = function(name, value){ * @param {string} name The key to retrieve the local data * @param {string} defaultValue The default value in case the local data doesn't exist */ -LocalExtensionList.prototype.getData = function(name, defaultValue){ +LocalExtensionList.prototype.getData = function (name, defaultValue) { if (typeof defaultValue === 'undefined') defaultValue = ""; this.log.debug("getting data", name, "defaultvalue (type:", (typeof defaultValue), ")") var prefs = this.settings; @@ -1131,9 +1170,10 @@ LocalExtensionList.prototype.getData = function(name, defaultValue){ } -// ScriptDownloader Class -------------------------------------------- +// ExtensionDownloader Class -------------------------------------------- /** * @classdesc + * A class that handles downloads * @constructor */ function ExtensionDownloader(extension) { @@ -1141,7 +1181,8 @@ function ExtensionDownloader(extension) { this.log.level = this.log.LEVEL.LOG; this.repository = extension.repository; this.extension = extension; - this.destFolder = specialFolders.temp + "/" + extension.name.replace(/[ :\?]/g, "") + "_" + extension.version; + this.destFolder = specialFolders.temp + "/" + extension.safeName+"_"+extension.version; + this.cacheFolder = specialFolders.temp + "/hues_icons_cache"; } @@ -1159,14 +1200,13 @@ ExtensionDownloader.prototype.downloadFiles = function () { // log ("destPaths: "+destPaths) var files = this.extension.files; - this.log.debug("downloading files : "+files.map(function(x){return x.path}).join("\n")) + this.log.debug("downloading files : " + files.map(function (x) { return x.path }).join("\n")) // cbb : how to connect this to any progress window? var progress = new QProgressDialog(); - progress.title = "Installing extension "+this.extension.name; - progress.setLabelText( "Downloading files..." ); - progress.setRange( 0, files.length ); - progress.modal = true; + progress.title = "Installing extension " + this.extension.name; + progress.setLabelText("Downloading files..."); + progress.setRange(0, files.length); progress.show(); @@ -1184,7 +1224,7 @@ ExtensionDownloader.prototype.downloadFiles = function () { dlFiles.push(destPaths[i]) progress.value = i; } else { - throw new Error("Downloaded file " + destPaths[i] + " size does not match expected size : \n" + dlFile.size + " bytes (expected : " + files[i].size+" bytes)") + throw new Error("Downloaded file " + destPaths[i] + " size does not match expected size : \n" + dlFile.size + " bytes (expected : " + files[i].size + " bytes)") } } diff --git a/README.md b/README.md index daad30d..b7aa2d2 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ For the moment, version numbers are purely indicative and do not allow script ma ## License -This extension is relseased under the Mozilla Public License 2.0. +This extension is released under the Mozilla Public License 2.0. --- From ef0f73bdc8da5238960d72568c2ea2fb024f7a0f Mon Sep 17 00:00:00 2001 From: bob-ross27 <55351437+bob-ross27@users.noreply.github.com> Date: Tue, 18 May 2021 00:28:02 -0300 Subject: [PATCH 002/112] Made extension install modal. --- ExtensionStore/lib/store.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ExtensionStore/lib/store.js b/ExtensionStore/lib/store.js index e2c7c68..f0e0cd6 100644 --- a/ExtensionStore/lib/store.js +++ b/ExtensionStore/lib/store.js @@ -1204,9 +1204,10 @@ ExtensionDownloader.prototype.downloadFiles = function () { // cbb : how to connect this to any progress window? var progress = new QProgressDialog(); - progress.title = "Installing extension " + this.extension.name; - progress.setLabelText("Downloading files..."); - progress.setRange(0, files.length); + progress.title = "Installing extension "+this.extension.name; + progress.setLabelText( "Downloading files..." ); + progress.setRange( 0, files.length ); + progress.modal = true; progress.show(); From ec6c632d4658534a5f58e586d47626b9f0ec24d6 Mon Sep 17 00:00:00 2001 From: MathieuC Date: Thu, 20 May 2021 21:18:52 +0200 Subject: [PATCH 003/112] files structure refactor --- packages/ExtensionStore/tbpackage.json | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 packages/ExtensionStore/tbpackage.json diff --git a/packages/ExtensionStore/tbpackage.json b/packages/ExtensionStore/tbpackage.json deleted file mode 100644 index a1459ab..0000000 --- a/packages/ExtensionStore/tbpackage.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "ExtensionStore", - "version": "0.2.4", - "compatibility": "Harmony Premium 16", - "description": "Changelog: Additional support for branches not named master.", - "isPackage": "true", - "repository": "https://github.com/mchaptel/ExtensionStore/", - "files": [ - "/ExtensionStore/" - ], - "keywords": [ - "store", - "package" - ], - "author": "Mathieu Chaptel", - "license": "MPL-2.0", - "website": "https://github.com/mchaptel/" -} From 8cdec0040a511e6cffedc89d73bd76e448b20ba2 Mon Sep 17 00:00:00 2001 From: mchaptel Date: Fri, 21 May 2021 00:18:05 +0200 Subject: [PATCH 004/112] beggining of adding icons --- ExtensionStore/lib/store.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ExtensionStore/lib/store.js b/ExtensionStore/lib/store.js index f0e0cd6..eec038c 100644 --- a/ExtensionStore/lib/store.js +++ b/ExtensionStore/lib/store.js @@ -1181,7 +1181,7 @@ function ExtensionDownloader(extension) { this.log.level = this.log.LEVEL.LOG; this.repository = extension.repository; this.extension = extension; - this.destFolder = specialFolders.temp + "/" + extension.safeName+"_"+extension.version; + this.destFolder = specialFolders.temp + "/" + extension.name.replace(/[ :\?]/g, "") + "_" + extension.version; this.cacheFolder = specialFolders.temp + "/hues_icons_cache"; } From c4691417ecd8a9d8115bc38b9a4d8911c87c4185 Mon Sep 17 00:00:00 2001 From: mchaptel Date: Fri, 21 May 2021 00:22:41 +0200 Subject: [PATCH 005/112] added extension.safeName --- ExtensionStore/lib/store.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ExtensionStore/lib/store.js b/ExtensionStore/lib/store.js index eec038c..5227a4f 100644 --- a/ExtensionStore/lib/store.js +++ b/ExtensionStore/lib/store.js @@ -944,7 +944,7 @@ Object.defineProperty(LocalExtensionList.prototype, "list", { * gets the install location for a given extension */ LocalExtensionList.prototype.installLocation = function (extension) { - return this.installFolder + (extension.package.isPackage ? "/packages/" + extension.name.replace(" ", "") : "") + return this.installFolder + (extension.package.isPackage ? "/packages/" + extension.safeName : "") } @@ -1005,7 +1005,7 @@ LocalExtensionList.prototype.uninstall = function (extension) { // Remove packages recursively as they have a parent directory. if (extension.package.isPackage) { - var folder = new Dir(this.installFolder + "packages/" + extension.name.replace(" ", "")); + var folder = new Dir(this.installFolder + "packages/" + extension.safeName); this.log.debug("removing folder " + folder.path); if (folder.exists) folder.rmdirs(); } else { @@ -1181,7 +1181,7 @@ function ExtensionDownloader(extension) { this.log.level = this.log.LEVEL.LOG; this.repository = extension.repository; this.extension = extension; - this.destFolder = specialFolders.temp + "/" + extension.name.replace(/[ :\?]/g, "") + "_" + extension.version; + this.destFolder = specialFolders.temp + "/" + extension.safeName + "_" + extension.version; this.cacheFolder = specialFolders.temp + "/hues_icons_cache"; } From 970eed0fff2aafb91bb965075eece704e6fd2aed Mon Sep 17 00:00:00 2001 From: MathieuC Date: Sun, 23 May 2021 01:29:57 +0200 Subject: [PATCH 006/112] added detachable CURLProcess class and WebIcon class --- ExtensionStore/lib/network.js | 136 +++++++++++++++++++++++++++------- 1 file changed, 109 insertions(+), 27 deletions(-) diff --git a/ExtensionStore/lib/network.js b/ExtensionStore/lib/network.js index 74b7cf5..3a5f510 100644 --- a/ExtensionStore/lib/network.js +++ b/ExtensionStore/lib/network.js @@ -9,7 +9,7 @@ var log = new Logger("CURL") * This class extends QObject so it can broadcast signals and be threaded. * @extends QObject */ - function NetworkConnexionHandler() { +function NetworkConnexionHandler() { this.curl = new CURL(); } @@ -62,6 +62,89 @@ NetworkConnexionHandler.prototype.download = function (url, destinationPath) { } + +// WebIcon Class ----------------------------------------------------- +function WebIcon(url, widget) { + log.debug("new icon : "+url) + this.url = url; + this.widget = widget; + + // start the download + this.download(this.setIcon); +} + +WebIcon.prototype.download = function (callback) { + log.debug("starting download of icon "+this.url) + var curl = new CURLProcess(this.url) + curl.launchAndRead(callback, null, false); +} + + +WebIcon.prototype.setIcon = function (byteArray) { + log.debug("download finished, setting icon") + // log.debug(new QTextStream(byteArray).readAll()) + var image = QImage.load(byteArray) + var pixmap = QPixmap.convertFromImage(image) + var icon = new QIcon(pixmap); + this.widget.setIcon(icon); +} + + +// CURLProcess Class ------------------------------------------------- + +function CURLProcess (command) { + this.curl = new CURL() + this.log = new Logger("CURL") + + if (typeof command == "string") var command = [command]; + if (this.curl.bin.indexOf("bin_3rdParty") != -1) command = ["-k"].concat(command); + this.command = ["-s", "-S"].concat(command); + + var bin = this.curl.bin.split("/"); + this.app = bin.pop(); + var directory = bin.join("\\"); + + this.process = new QProcess(); + this.process.setWorkingDirectory(directory); +} + + +CURLProcess.prototype.launchAndRead = function (readCallback, finishedCallback, asText){ + this.log.debug("Executing Process with arguments : "+this.app+" "+this.command.join(" ")); + if (typeof asText=== 'undefined') var asText = true; + + this.process.start(this.app, this.command); + if (typeof readCallback !== 'undefined' && readCallback){ + var onRead = function(){ + var stdout = this.read(asText); + readCallback(stdout); + } + this.process.readyRead.connect(this, onRead); + } + if (typeof finishedCallback !== 'undefined' && finishedCallback) this.process["finished(int)"].connect(this, finishedCallback); +} + + +CURLProcess.prototype.read = function (asText){ + this.log.debug("readyread") + var readOut = this.process.readAllStandardOutput(); + if (asText){ + var output = new QTextStream(readOut).readAll(); + }else{ + var output = readOut; + } + this.log.debug("output:" + output) + + var readErr = this.process.readAllStandardError(); + var errors = new QTextStream(readErr).readAll(); + if (errors) { + this.log.error("curl errors: " + errors.replace("\r", "")); + throw new Error(errors) + } + return output; +} + + // CURL Class -------------------------------------------------------- /** * Curl class to launch curl queries @@ -70,7 +153,6 @@ NetworkConnexionHandler.prototype.download = function (url, destinationPath) { * @param {string[]} command */ function CURL() { - this.log = new Logger("CURL"); } @@ -93,7 +175,7 @@ CURL.prototype.query = function (query, wait) { try { var p = new QProcess(); - this.log.debug("starting process :" + bin + " " + command); + log.debug("starting process :" + bin + " " + command); var command = ["-H", "Authorization: Bearer YOUR_JWT", "-H", "Content-Type: application/json", "-X", "POST", "-d"]; query = query.replace(/\n/gm, "\\\\n").replace(/"/gm, '\\"'); command.push('" \\\\n' + query + '"'); @@ -109,7 +191,7 @@ CURL.prototype.query = function (query, wait) { return output; } catch (err) { - this.log.error("Error with curl command: \n"+command.join(" ")+"\n"+err); + log.error("Error with curl command: \n" + command.join(" ") + "\n" + err); return null; } } @@ -125,17 +207,21 @@ CURL.prototype.get = function (command, wait) { var bin = this.bin; return this.runCommand(bin, command, wait); } catch (err) { - message = "Error with curl command: \n"+command.join(" ")+"\n"+err - this.log.error(message); - throw new Error (message); + message = "Error with curl command: \n" + command.join(" ") + "\n" + err + log.error(message); + throw new Error(message); } } -CURL.prototype.runCommand = function (bin, command, wait, test){ +CURL.prototype.runCommand = function (bin, command, wait, test) { if (typeof test === 'undefined') var test = false; // test will not print the output, just the errors + // The toonboom bundled curl doesn't seem to be equiped for ssh so we have to use unsafe mode + if (bin.indexOf("bin_3rdParty") != -1) command = ["-k"].concat(command); + command = ["-s", "-S"].concat(command); + var loop = new QEventLoop(); var p = new QProcess(); @@ -146,7 +232,7 @@ CURL.prototype.runCommand = function (bin, command, wait, test){ // Use a timer to kill the QProcess after the wait period. var timer = new QTimer(); timer.singleShot = true; - timer["timeout"].connect(this, function() { + timer["timeout"].connect(this, function () { if (loop.isRunning()) { p.kill(); loop.exit(); @@ -154,24 +240,21 @@ CURL.prototype.runCommand = function (bin, command, wait, test){ } }); - // The toonboom bundled curl doesn't seem to be equiped for ssh so we have to use unsafe mode - if (bin.indexOf("bin_3rdParty") != -1) command = ["-k"].concat(command); - command = ["-s", "-S"].concat(command); // Start the process and enter an event loop until the QProcessx exits. - this.log.debug("starting process :" + bin + " " + command.join(" ")); + log.debug("starting process :" + bin + " " + command.join(" ")); p.start(bin, command); timer.start(wait); loop.exec(); var readOut = p.readAllStandardOutput(); var output = new QTextStream(readOut).readAll(); - if (!test) this.log.debug("curl output: " + output); + if (!test) log.debug("curl output: " + output); var readErr = p.readAllStandardError(); var errors = new QTextStream(readErr).readAll(); - if (errors){ - this.log.error("curl errors: " + errors.replace("\r", "")); + if (errors) { + log.error("curl errors: " + errors.replace("\r", "")); throw new Error(errors) } @@ -183,7 +266,7 @@ CURL.prototype.runCommand = function (bin, command, wait, test){ */ Object.defineProperty(CURL.prototype, "bin", { get: function () { - this.log.debug("getting curl bin") + log.debug("getting curl bin") if (typeof CURL.__proto__.bin === 'undefined') { if (about.isWindowsArch()) { @@ -201,21 +284,21 @@ Object.defineProperty(CURL.prototype, "bin", { if ((new File(curl[i])).exists) { // testing connection var bin = curl[i]; - try{ - this.log.info("testing connexion by connecting to github.com") + try { + log.info("testing connexion by connecting to github.com") this.runCommand(bin, ["https://www.github.com/"], 500, true); - this.log.info("CURL bin found, using: "+curl[i]) + log.info("CURL bin found, using: " + curl[i]) CURL.__proto__.bin = bin; return bin; - }catch(err){ - this.log.error(err); - var message = "ExtensionStore: Couldn't establish a connexion.\nCheck that "+bin+" has internet access."; - this.log.error(message); + } catch (err) { + log.error(err); + var message = "ExtensionStore: Couldn't establish a connexion.\nCheck that " + bin + " has internet access."; + log.error(message); } } } var error = "ExtensionStore: a valid CURL install wasn't found. Install CURL first."; - this.log.error(error) + log.error(error) throw new Error(error) } else { return CURL.__proto__.bin; @@ -223,6 +306,5 @@ Object.defineProperty(CURL.prototype, "bin", { } }) - -exports.CURL = CURL +exports.WebIcon = WebIcon exports.NetworkConnexionHandler = NetworkConnexionHandler \ No newline at end of file From be5583157b00d63049b118a4529ea84791211079 Mon Sep 17 00:00:00 2001 From: MathieuC Date: Sun, 23 May 2021 01:30:41 +0200 Subject: [PATCH 007/112] formatting and removed some buggy functions --- ExtensionStore/lib/store.js | 130 +++++++++++++++++------------------- 1 file changed, 63 insertions(+), 67 deletions(-) diff --git a/ExtensionStore/lib/store.js b/ExtensionStore/lib/store.js index 5227a4f..91942ff 100644 --- a/ExtensionStore/lib/store.js +++ b/ExtensionStore/lib/store.js @@ -34,7 +34,6 @@ function test() { function Store() { this.log = new Logger("Store") this.log.info("init store") - } @@ -50,11 +49,11 @@ Object.defineProperty(Store.prototype, "sellers", { if (!sellersFile) sellersFile = "https://raw.githubusercontent.com/mchaptel/ExtensionStore/master/SELLERSLIST"; this.log.debug(sellersFile) try { - if (sellersFile.indexOf("http") == -1){ + if (sellersFile.indexOf("http") == -1) { // using a local file var fileContents = readFile(sellersFile) var sellersList = JSON.parse(fileContents) - }else{ + } else { var sellersList = webQuery.get(sellersFile); } this.log.debug(sellersList) @@ -65,12 +64,12 @@ Object.defineProperty(Store.prototype, "sellers", { // handle wrong packages found in sellers list var validSellers = []; for (var i in sellersList) { - try{ + try { var seller = new Seller(sellersList[i]); var package = seller.package; validSellers.push(seller) - }catch(error){ - this.log.error("problem getting package for seller "+sellersList[i], error); + } catch (error) { + this.log.error("problem getting package for seller " + sellersList[i], error); } } this._sellers = validSellers; @@ -207,7 +206,7 @@ Object.defineProperty(Seller.prototype, "package", { this.log.debug("getting package for " + this.masterRepositoryName); if (!this._tbpackage) { var response = webQuery.get(this.dlUrl + "/tbpackage.json"); - if (!response || response.message){ + if (!response || response.message) { var message = "No valid package found in repository " + this._url + ": " + response.message; throw new Error(message) } @@ -456,7 +455,7 @@ Object.defineProperty(Repository.prototype, "package", { this.log.debug("getting repos package for repo " + this.apiUrl); if (typeof this._package === 'undefined') { var response = webQuery.get(this.dlUrl + "/tbpackage.json"); - if (!response || response.message){ + if (!response || response.message) { this.log.error("No valid package found in repository " + this._url + ": " + response.message) return null } @@ -485,8 +484,8 @@ Object.defineProperty(Repository.prototype, "contents", { // this._contents = tree; var files = tree.map(function (file) { - if (file.type == "tree") return { path: "/" + file.path + "/", size: file.size }; - if (file.type == "blob") return { path: "/" + file.path, size: file.size }; + if (file.type == "tree") return {path:"/" + file.path + "/", size: file.size}; + if (file.type == "blob") return {path:"/" + file.path, size: file.size}; }) this._contents = files; } @@ -572,7 +571,7 @@ Repository.prototype.getFiles = function (filter) { if (typeof filter === 'undefined') var filter = /.*/; var contents = this.contents; - var paths = this.contents.map(function (x) { return x.path }) + var paths = this.contents.map(function(x){return x.path}) this.log.debug(paths.join("\n")) var search = this.searchToRe(filter) @@ -582,7 +581,7 @@ Repository.prototype.getFiles = function (filter) { var results = [] for (var i in paths) { // add files that match the filter but not folders - if (paths[i].match(search) && paths[i].slice(-1) != "/") results.push(contents[i]) + if (paths[i].match(search) && paths[i].slice(-1)!="/") results.push(contents[i]) } return results; @@ -598,9 +597,9 @@ Repository.prototype.searchToRe = function (search) { // sanitize input to prevent broken regex search = search.replace(/\./g, "\\.") - // .replace(/\*/g, "[^/]*") // this is to avoid selecting subfolders contents but do we want that? - .replace(/\*/g, ".*") - .replace(/\?/g, "."); + // .replace(/\*/g, "[^/]*") // this is to avoid selecting subfolders contents but do we want that? + .replace(/\*/g, ".*") + .replace(/\?/g, "."); var searchRe = new RegExp("^" + search + "$", "i"); @@ -676,14 +675,14 @@ Object.defineProperty(Extension.prototype, "rootFolder", { if (typeof this._rootFolder === 'undefined') { var files = this.package.files; if (files.length == 1) { - this._rootFolder = files[0].slice(0, files[0].lastIndexOf("/") + 1); + this._rootFolder = files[0].slice(0, files[0].lastIndexOf("/")+1); } else { var folders = files[0].split("/"); var rootFolder = ""; mainLoop: for (var i = 0; i < folders.length; i++) { - var folder = folders.slice(0, i).join("/") + "/"; + var folder = folders.slice(0, i).join("/")+"/"; for (var j in files) { if (files[j].indexOf(folder) == -1) break mainLoop; } @@ -692,7 +691,7 @@ Object.defineProperty(Extension.prototype, "rootFolder", { this._rootFolder = rootFolder; } } - this.log.debug("rootfolder: " + this._rootFolder) + this.log.debug("rootfolder: "+this._rootFolder) return this._rootFolder; } }); @@ -728,8 +727,8 @@ Object.defineProperty(Extension.prototype, "files", { var files = []; for (var i in packageFiles) { - this.log.debug("getting extension files matching : " + packageFiles[i]) - var results = this.repository.getFiles("/" + packageFiles[i]); + this.log.debug("getting extension files matching : "+packageFiles[i]) + var results = this.repository.getFiles("/"+ packageFiles[i]); if (results.length > 0) files = files.concat(results); } @@ -747,7 +746,7 @@ Object.defineProperty(Extension.prototype, "files", { */ Object.defineProperty(Extension.prototype, "id", { get: function () { - if (typeof this._id === 'undefined') { + if (typeof this._id === 'undefined'){ var repoName = this.package.repository.replace("https://github.com/", "") var id = (repoName + this.name).replace(/ /g, "_") this._id = id; @@ -776,40 +775,39 @@ Object.defineProperty(Extension.prototype, "localPaths", { -/** - * The ExtensionDownloader instance to handle the downloads for this extension - */ -Object.defineProperty(Extension.prototype, "downloader", { - get: function () { - if (typeof this._downloader === 'undefined') { - this._downloader = new ExtensionDownloader(this); - } - return this._downloader - } -}) +// /** +// * The ExtensionDownloader instance to handle the downloads for this extension +// */ +// Object.defineProperty(Extension.prototype, "downloader", { +// get: function () { +// if (typeof this._downloader === 'undefined') { +// this._downloader = new ExtensionDownloader(this); +// } +// return this._downloader +// } +// }) -/** - * gets the extension icon file. Can provide a callback to execute once the icon has been obtained. - */ +// /** +// * gets the extension icon file. Can provide a callback to execute once the icon has been obtained. +// */ -Extension.prototype.getIcon = function (callback){ - get: function () { - var icon = listFiles(this.downloader.cacheFolder, this.safeName + "_icon.png") - if (icon.length == 0){ - // look for an icon in the repo - } - } -}) +// Extension.prototype.getIcon = function (callback) { +// get: function () { +// var icon = listFiles(this.downloader.cacheFolder, this.safeName + "_icon.png") +// if (icon.length == 0) { +// // look for an icon in the repo +// } +// } +// }) /** * Cleans the problematic characters from the name of the extension. */ -Object.defineProperty(Extension.prototype, safeName, { +Object.defineProperty(Extension.prototype, "safeName", { get: function () { - return this.name.replace(/ /g, "_") - .replace(/[:\?\*\\\/"\|\<\>]/g, "") + return this.name.replace(/ /g, "_").replace(/[:\?\*\\\/"\|\<\>]/g, "") } }) @@ -842,8 +840,8 @@ Extension.prototype.matchesSearch = function (search) { * @param {string} version a semantic version string separated by dots. */ Extension.prototype.currentVersionIsOlder = function (version) { - version = version.split(".").map(function (x) { return parseInt(x, 10) }); - var ownVersion = this.version.split(".").map(function (x) { return parseInt(x, 10) }); + version = version.split(".").map(function(x){return parseInt(x, 10)}); + var ownVersion = this.version.split(".").map(function(x){return parseInt(x, 10)}); var length = Math.max(version.length > ownVersion.length); @@ -944,7 +942,7 @@ Object.defineProperty(LocalExtensionList.prototype, "list", { * gets the install location for a given extension */ LocalExtensionList.prototype.installLocation = function (extension) { - return this.installFolder + (extension.package.isPackage ? "/packages/" + extension.safeName : "") + return this.installFolder + (extension.package.isPackage ? "/packages/" + extension.name.replace(" ", "") : "") } @@ -979,7 +977,7 @@ LocalExtensionList.prototype.checkFiles = function (extension) { */ LocalExtensionList.prototype.install = function (extension) { // if (this.isInstalled(extension)) return true; // extension is already installed - var downloader = extension.downloader; + var downloader = new ExtensionDownloader(extension); // dedicated object to implement threaded download later var installLocation = this.installLocation(extension) var files = downloader.downloadFiles(); @@ -1005,7 +1003,7 @@ LocalExtensionList.prototype.uninstall = function (extension) { // Remove packages recursively as they have a parent directory. if (extension.package.isPackage) { - var folder = new Dir(this.installFolder + "packages/" + extension.safeName); + var folder = new Dir(this.installFolder + "packages/" + extension.name.replace(" ", "")); this.log.debug("removing folder " + folder.path); if (folder.exists) folder.rmdirs(); } else { @@ -1126,19 +1124,19 @@ LocalExtensionList.prototype.createListFile = function (store) { * Access the custom settings */ Object.defineProperty(LocalExtensionList.prototype, "settings", { - get: function () { - if (typeof this._settings === 'undefined') { + get: function(){ + if (typeof this._settings === 'undefined'){ var ini = readFile(this._ini) - if (!ini) { + if (!ini){ var prefs = {}; - } else { + }else{ var prefs = JSON.parse(ini); } this._settings = prefs; } return this._settings; }, - set: function (settingsObject) { + set: function(settingsObject){ writeFile(this._ini, JSON.stringify(settingsObject, null, " ")) } }) @@ -1148,7 +1146,7 @@ Object.defineProperty(LocalExtensionList.prototype, "settings", { * @param {string} name * @param {string} value */ -LocalExtensionList.prototype.saveData = function (name, value) { +LocalExtensionList.prototype.saveData = function(name, value){ this.log.debug("saving data ", JSON.stringify(value, null, " "), "under name", name) var prefs = this.settings; prefs[name] = value; @@ -1161,7 +1159,7 @@ LocalExtensionList.prototype.saveData = function (name, value) { * @param {string} name The key to retrieve the local data * @param {string} defaultValue The default value in case the local data doesn't exist */ -LocalExtensionList.prototype.getData = function (name, defaultValue) { +LocalExtensionList.prototype.getData = function(name, defaultValue){ if (typeof defaultValue === 'undefined') defaultValue = ""; this.log.debug("getting data", name, "defaultvalue (type:", (typeof defaultValue), ")") var prefs = this.settings; @@ -1170,10 +1168,9 @@ LocalExtensionList.prototype.getData = function (name, defaultValue) { } -// ExtensionDownloader Class -------------------------------------------- +// ScriptDownloader Class -------------------------------------------- /** * @classdesc - * A class that handles downloads * @constructor */ function ExtensionDownloader(extension) { @@ -1181,8 +1178,7 @@ function ExtensionDownloader(extension) { this.log.level = this.log.LEVEL.LOG; this.repository = extension.repository; this.extension = extension; - this.destFolder = specialFolders.temp + "/" + extension.safeName + "_" + extension.version; - this.cacheFolder = specialFolders.temp + "/hues_icons_cache"; + this.destFolder = specialFolders.temp + "/" + extension.name.replace(/[ :\?]/g, "") + "_" + extension.version; } @@ -1200,13 +1196,13 @@ ExtensionDownloader.prototype.downloadFiles = function () { // log ("destPaths: "+destPaths) var files = this.extension.files; - this.log.debug("downloading files : " + files.map(function (x) { return x.path }).join("\n")) + this.log.debug("downloading files : "+files.map(function(x){return x.path}).join("\n")) // cbb : how to connect this to any progress window? var progress = new QProgressDialog(); - progress.title = "Installing extension "+this.extension.name; - progress.setLabelText( "Downloading files..." ); - progress.setRange( 0, files.length ); + progress.title = "Installing extension " + this.extension.name; + progress.setLabelText("Downloading files..."); + progress.setRange(0, files.length); progress.modal = true; progress.show(); @@ -1225,7 +1221,7 @@ ExtensionDownloader.prototype.downloadFiles = function () { dlFiles.push(destPaths[i]) progress.value = i; } else { - throw new Error("Downloaded file " + destPaths[i] + " size does not match expected size : \n" + dlFile.size + " bytes (expected : " + files[i].size + " bytes)") + throw new Error("Downloaded file " + destPaths[i] + " size does not match expected size : \n" + dlFile.size + " bytes (expected : " + files[i].size+" bytes)") } } From 3c020d4d821785582bd8df6769f046aa2012e6e6 Mon Sep 17 00:00:00 2001 From: MathieuC Date: Sun, 23 May 2021 01:30:58 +0200 Subject: [PATCH 008/112] test of icon loading --- ExtensionStore/app.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ExtensionStore/app.js b/ExtensionStore/app.js index ef92a75..eca96a0 100644 --- a/ExtensionStore/app.js +++ b/ExtensionStore/app.js @@ -1,6 +1,7 @@ var storelib = require("./lib/store.js"); var Logger = require("./lib/logger.js").Logger; var log = new Logger("UI") +var WebIcon = require("./lib/network.js").WebIcon /** @@ -79,6 +80,8 @@ function StoreUI(){ this.uninstallAction = new QAction("Uninstall", this); this.uninstallAction.triggered.connect(this, this.performUninstall); + + new WebIcon("https://raw.githubusercontent.com/mchaptel/ExtensionStore/master/ExtensionStore/resources/logo.png", this.loadStoreButton) } /** From aaa637b3eb407b0c8b34208f8edff3cd808fe278 Mon Sep 17 00:00:00 2001 From: MathieuC Date: Sun, 23 May 2021 14:53:41 +0200 Subject: [PATCH 009/112] fix when trying to trace types that don't concatenate with string --- ExtensionStore/lib/logger.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ExtensionStore/lib/logger.js b/ExtensionStore/lib/logger.js index 14a72ac..42131f8 100644 --- a/ExtensionStore/lib/logger.js +++ b/ExtensionStore/lib/logger.js @@ -52,10 +52,10 @@ Logger.prototype.trace = function (message) { message[m] = "Error: "+error.message+" (line " + error.lineNumber + " in file '" + error.fileName + "')"; } } - if (this.name) var message = this.name + ": " + message.join(" "); try { - MessageLog.trace(message); - System.println(message); + if (this.name) var trace = this.name + ": " + message.join(" "); + MessageLog.trace(trace); + System.println(trace); } catch (err) { for (var i in message) { try{ From 2841141dd4ac82dcc0638602807e4a291201ca34 Mon Sep 17 00:00:00 2001 From: MathieuC Date: Sun, 23 May 2021 14:54:28 +0200 Subject: [PATCH 010/112] refactored CURL class with CURLProcess --- ExtensionStore/lib/network.js | 190 +++++++++++++++++++++++----------- 1 file changed, 132 insertions(+), 58 deletions(-) diff --git a/ExtensionStore/lib/network.js b/ExtensionStore/lib/network.js index 3a5f510..ba8cd4c 100644 --- a/ExtensionStore/lib/network.js +++ b/ExtensionStore/lib/network.js @@ -92,10 +92,16 @@ WebIcon.prototype.setIcon = function (byteArray) { // CURLProcess Class ------------------------------------------------- +/** + * This class wraps a CURL Qprocess and handles the outputs. + * Can perform asynchronous or inline operations without blocking the UI. + * @param {*} command + */ function CURLProcess (command) { this.curl = new CURL() this.log = new Logger("CURL") + // The toonboom bundled curl doesn't seem to be equiped for ssh so we have to use unsafe mode if (typeof command == "string") var command = [command]; if (this.curl.bin.indexOf("bin_3rdParty") != -1) command = ["-k"].concat(command); this.command = ["-s", "-S"].concat(command); @@ -108,25 +114,49 @@ function CURLProcess (command) { this.process.setWorkingDirectory(directory); } - -CURLProcess.prototype.launchAndRead = function (readCallback, finishedCallback, asText){ +/** + * Launches a curl process, and optionally connects callbacks to the readyRead and finished signals. + * The callbacks will be passed the output from the process, as well as the returncode for finishedCallback. + * @param {function} readCallback the callback attached to the 'readyRead' signal, if valid. Signature must be readCallback(QString/QBytesArray) + * @param {function} finishedCallback the callback attached to the 'finished' signal, if valid. Signature must be finishedCallback(QProcess.ExitCode, QString/QBytesArray) + * @param {bool} asText wether to parse the output as text or QByteArray in the callbacks. + * @returns {QProcess} the launched process. + */ +CURLProcess.prototype.asyncRead = function (readCallback, finishedCallback, asText){ this.log.debug("Executing Process with arguments : "+this.app+" "+this.command.join(" ")); - if (typeof asText=== 'undefined') var asText = true; this.process.start(this.app, this.command); if (typeof readCallback !== 'undefined' && readCallback){ var onRead = function(){ + this.log.debug("readyread") var stdout = this.read(asText); readCallback(stdout); } this.process.readyRead.connect(this, onRead); } - if (typeof finishedCallback !== 'undefined' && finishedCallback) this.process["finished(int)"].connect(this, finishedCallback); + + if (typeof finishedCallback !== 'undefined' && finishedCallback){ + var onFinished = function(returnCode){ + this.log.debug("finished") + var stdout = this.read(asText); + finishedCallback(returnCode, stdout); + if (returnCode) this.log.error("CURL returned with error code "+returnCode) + } + this.process["finished(int)"].connect(this, onFinished); + } + + return this.process } +/** + * Reads and returns the stdout of a curl process. If there is any stderr, it will be thrown as an error. + * Each read call "empties" the stream from the process, so subsequent reads will be empty unless new output was returned. + * @param {bool} [asText=true] wether to return the output as text or QByteArray + * @returns the output from the process, as a string or QByteArray + */ CURLProcess.prototype.read = function (asText){ - this.log.debug("readyread") + if (typeof asText === 'undefined' || asText === 'undefined' || asText === null) var asText = true; var readOut = this.process.readAllStandardOutput(); if (asText){ var output = new QTextStream(readOut).readAll(); @@ -145,6 +175,89 @@ CURLProcess.prototype.read = function (asText){ } +/** + * Launches a download without waiting for the end. + * @param {str} destinationPath the path to which the download will be saved + * @param {function} callback a function to execute once download has finished. Signature: callback(QProcess.returnCode, QString) + * @returns {QProcess} the process launched. + */ +CURLProcess.prototype.asyncDownload = function (destinationPath, callback) { + var url = this.command.pop() + url = url.replace(/ /g, "%20"); + destinationPath = destinationPath.replace(/[ :\?\*"\<\>\|][^/\\]/g, ""); + + this.command = ["-L", "-o", destinationPath].concat(this.command) + this.command.push(url) + + var dest = destinationPath.split("/").slice(0, -1).join("/") + var dir = new QDir(dest); + if (!dir.exists()) dir.mkpath(dest); + + return this.asyncRead(null, callback) +} + + +/** + * Run the process and wait for result while running the UI event loop to update progress + * @param {int} wait The amount of milliseconds before the process will be considered failed. + * @param {function} runMethod the CURLProcess method launched (ex: asyncRead, asyncDownload). + * @param {Array} [args] optionally, pass some arguments to the runMethod. + * @returns the output as text. + */ +CURLProcess.prototype.runAndWait = function (wait, runMethod, args) { + if (typeof args === 'undefined') var args = []; + + var loop = new QEventLoop(); + + // Use a timer to kill the QProcess after the wait period. + var timer = new QTimer(); + timer.singleShot = true; + timer["timeout"].connect(this, function () { + if (loop.isRunning()) { + this.process.kill(); + loop.exit(); + throw new Error("Timeout running command "+this.command.join(" ")); + } + }); + + // Start the process and enter an event loop until the QProcess exits. + this.process["finished(int)"].connect(this, function(){loop.exit()}) + runMethod.apply(this, args); + timer.start(wait); + loop.exec(); + + var output = this.read(); + return output; +} + + +/** + * Performs a CURL get query, and returns the result when ready. Blocks execution of the code but not the event loop. + * @param {int} [wait=5000] optional, the timeout for the query. + * @returns {string} + */ +CURLProcess.prototype.get = function(wait){ + if (typeof wait === 'undefined') var wait = 5000; + + var output = this.runAndWait(wait, this.asyncRead) + return output; +} + + +/** + * Performs a download through curl. The result of the operation will be returned as well. + * @param {str} destinationPath The location to which the download will be saved. + * @param {int} [wait=30000] optional, the timeout for the query (for downloads, 30s by default) + * @returns + */ +CURLProcess.prototype.download = function(destinationPath, wait){ + if (typeof wait === 'undefined') var wait = 30000; + + var output = this.runAndWait(wait, this.asyncDownload, [destinationPath]) + return output; +} + + // CURL Class -------------------------------------------------------- /** * Curl class to launch curl queries @@ -153,6 +266,7 @@ CURLProcess.prototype.read = function (asText){ * @param {string[]} command */ function CURL() { + this.log = new Logger("CURL") } @@ -202,63 +316,24 @@ CURL.prototype.query = function (query, wait) { */ CURL.prototype.get = function (command, wait) { if (typeof command == "string") command = [command] - if (typeof wait === 'undefined') var wait = 5000; - try { - var bin = this.bin; - return this.runCommand(bin, command, wait); - } catch (err) { - message = "Error with curl command: \n" + command.join(" ") + "\n" + err - log.error(message); - throw new Error(message); - } + var curl = new CURLProcess(command); + return curl.get(wait); } - -CURL.prototype.runCommand = function (bin, command, wait, test) { - if (typeof test === 'undefined') var test = false; // test will not print the output, just the errors - - // The toonboom bundled curl doesn't seem to be equiped for ssh so we have to use unsafe mode - if (bin.indexOf("bin_3rdParty") != -1) command = ["-k"].concat(command); - command = ["-s", "-S"].concat(command); - - var loop = new QEventLoop(); - - var p = new QProcess(); - p["finished(int,QProcess::ExitStatus)"].connect(this, function () { - loop.exit(); - }); - - // Use a timer to kill the QProcess after the wait period. - var timer = new QTimer(); - timer.singleShot = true; - timer["timeout"].connect(this, function () { - if (loop.isRunning()) { - p.kill(); - loop.exit(); - throw new Error("Timeout updating extension."); - } - }); +CURL.prototype.download = function (url, wait) { + var curl = new CURLProcess(url); + return curl.download(wait); +} - // Start the process and enter an event loop until the QProcessx exits. - log.debug("starting process :" + bin + " " + command.join(" ")); - p.start(bin, command); - timer.start(wait); - loop.exec(); +CURL.prototype.runCommand = function (command, wait, test) { + if (typeof test === 'undefined') var test = false; // test will not print the output, just the errors - var readOut = p.readAllStandardOutput(); - var output = new QTextStream(readOut).readAll(); - if (!test) log.debug("curl output: " + output); + var curl = new CURLProcess(command); + var output = curl.runAndWait(wait) - var readErr = p.readAllStandardError(); - var errors = new QTextStream(readErr).readAll(); - if (errors) { - log.error("curl errors: " + errors.replace("\r", "")); - throw new Error(errors) - } - - return output; + if (!test) return output; } /** @@ -266,9 +341,8 @@ CURL.prototype.runCommand = function (bin, command, wait, test) { */ Object.defineProperty(CURL.prototype, "bin", { get: function () { - log.debug("getting curl bin") - if (typeof CURL.__proto__.bin === 'undefined') { + log.debug("getting curl bin") if (about.isWindowsArch()) { var curl = [System.getenv("windir") + "/system32/curl.exe", System.getenv("ProgramFiles") + "/Git/mingw64/bin/curl.exe", @@ -286,7 +360,7 @@ Object.defineProperty(CURL.prototype, "bin", { var bin = curl[i]; try { log.info("testing connexion by connecting to github.com") - this.runCommand(bin, ["https://www.github.com/"], 500, true); + this.get("https://www.github.com/", 500); log.info("CURL bin found, using: " + curl[i]) CURL.__proto__.bin = bin; return bin; From 11d8dfd9aaf2a763b286f19df043333eca33773e Mon Sep 17 00:00:00 2001 From: MathieuC Date: Sun, 23 May 2021 14:55:30 +0200 Subject: [PATCH 011/112] now progress during install shows before first github fetch, and folder creation for download is handled by the CURL class --- ExtensionStore/lib/store.js | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/ExtensionStore/lib/store.js b/ExtensionStore/lib/store.js index 91942ff..bd0cd40 100644 --- a/ExtensionStore/lib/store.js +++ b/ExtensionStore/lib/store.js @@ -1193,25 +1193,20 @@ ExtensionDownloader.prototype.downloadFiles = function () { var destPaths = this.extension.localPaths.map(function (x) { return destFolder + x }); var dlFiles = [this.destFolder]; - // log ("destPaths: "+destPaths) - var files = this.extension.files; - - this.log.debug("downloading files : "+files.map(function(x){return x.path}).join("\n")) - - // cbb : how to connect this to any progress window? var progress = new QProgressDialog(); progress.title = "Installing extension " + this.extension.name; progress.setLabelText("Downloading files..."); progress.setRange(0, files.length); progress.modal = true; + // log ("destPaths: "+destPaths) + var files = this.extension.files; + + this.log.debug("downloading files : "+files.map(function(x){return x.path}).join("\n")) + progress.show(); for (var i = 0; i < files.length; i++) { - // make the directory - var dest = destPaths[i].split("/").slice(0, -1).join("/") - var dir = new QDir(dest); - if (!dir.exists()) dir.mkpath(dest); webQuery.download(this.getDownloadUrl(files[i].path), destPaths[i]); var dlFile = new File(destPaths[i]); From e7668b50b843b357c9345a00d6b418f6a91ed19c Mon Sep 17 00:00:00 2001 From: MathieuC Date: Sun, 23 May 2021 14:55:49 +0200 Subject: [PATCH 012/112] flesh out WebIcon class using new CURLProcess --- ExtensionStore/lib/network.js | 47 ++++++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/ExtensionStore/lib/network.js b/ExtensionStore/lib/network.js index ba8cd4c..acf3136 100644 --- a/ExtensionStore/lib/network.js +++ b/ExtensionStore/lib/network.js @@ -64,29 +64,46 @@ NetworkConnexionHandler.prototype.download = function (url, destinationPath) { // WebIcon Class ----------------------------------------------------- -function WebIcon(url, widget) { - log.debug("new icon : "+url) +function WebIcon(url) { + this.log = new Logger("Icon") + if (url.indexOf(".png") == -1){ + // dealing with a website, we'll get the favicon + url = "https://www.google.com/s2/favicons?sz=32&domain_url=" + url; + } + + this.log.debug("new icon : "+url) this.url = url; - this.widget = widget; - // start the download - this.download(this.setIcon); + var fileName = url.split("/").pop() + this.dlUrl = specialFolders.temp + "/HUES_iconscache/" + fileName + ".png" } WebIcon.prototype.download = function (callback) { - log.debug("starting download of icon "+this.url) - var curl = new CURLProcess(this.url) - curl.launchAndRead(callback, null, false); + this.log.debug(this.dlUrl); + var icon = new QFile(this.dlUrl); + if (icon.exists()){ + this.log.debug("file exists") + callback.apply(this, []); + } else { + this.log.debug("starting download of icon "+this.url); + var curl = new CURLProcess(this.url); + var p = curl.asyncDownload(this.dlUrl); + p["finished(int)"].connect(this, callback) + } +} + +WebIcon.prototype.setToWidget = function(widget){ + this.widget = widget; + this.log.debug(widget) + this.download(this.setIcon) } -WebIcon.prototype.setIcon = function (byteArray) { - log.debug("download finished, setting icon") - // log.debug(new QTextStream(byteArray).readAll()) - var image = QImage.load(byteArray) - var pixmap = QPixmap.convertFromImage(image) - var icon = new QIcon(pixmap); - this.widget.setIcon(icon); +WebIcon.prototype.setIcon = function () { + this.log.debug("download finished, setting icon") + this.log.debug("icon url : "+this.dlUrl) + var icon = new QIcon(this.dlUrl); + this.widget.icon = icon; } From b763cc65791476e571917f58b4e5cd88512ab35b Mon Sep 17 00:00:00 2001 From: MathieuC Date: Sun, 23 May 2021 14:56:06 +0200 Subject: [PATCH 013/112] set discord icon to loadbutton as example --- ExtensionStore/app.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ExtensionStore/app.js b/ExtensionStore/app.js index eca96a0..80d00c3 100644 --- a/ExtensionStore/app.js +++ b/ExtensionStore/app.js @@ -81,7 +81,8 @@ function StoreUI(){ this.uninstallAction = new QAction("Uninstall", this); this.uninstallAction.triggered.connect(this, this.performUninstall); - new WebIcon("https://raw.githubusercontent.com/mchaptel/ExtensionStore/master/ExtensionStore/resources/logo.png", this.loadStoreButton) + var icon = new WebIcon("https://discord.com") + icon.setToWidget(this.aboutFrame.loadStoreButton) } /** From 3e8f4e4b03657842ec0817764d649f9958eec6aa Mon Sep 17 00:00:00 2001 From: MathieuC Date: Sun, 23 May 2021 16:04:44 +0200 Subject: [PATCH 014/112] added docstrings + remove some original CURL class uses --- ExtensionStore/lib/network.js | 39 ++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/ExtensionStore/lib/network.js b/ExtensionStore/lib/network.js index acf3136..78aa14b 100644 --- a/ExtensionStore/lib/network.js +++ b/ExtensionStore/lib/network.js @@ -19,7 +19,8 @@ function NetworkConnexionHandler() { */ NetworkConnexionHandler.prototype.get = function (command) { // handle errors - var result = this.curl.get(command); + var curl = new CURLProcess(command) + var result = curl.get(); try { json = JSON.parse(result); if (json.hasOwnProperty("message")) { @@ -53,11 +54,8 @@ NetworkConnexionHandler.prototype.get = function (command) { * Makes a download request for the given file url, and downloads it to the chosen location */ NetworkConnexionHandler.prototype.download = function (url, destinationPath) { - url = url.replace(/ /g, "%20"); - destinationPath = destinationPath.replace(/[ :\?\*"\<\>\|][^/\\]/g, ""); - - var command = ["-L", "-o", destinationPath, url]; - var result = this.curl.get(command, 30000); // 30s timeout + var curl = new CURLProcess(url) + var result = curl.download(destinationPath, 30000); // 30s timeout return result; } @@ -66,42 +64,49 @@ NetworkConnexionHandler.prototype.download = function (url, destinationPath) { // WebIcon Class ----------------------------------------------------- function WebIcon(url) { this.log = new Logger("Icon") + var fileName = url.split("/").pop() + if (url.indexOf(".png") == -1){ // dealing with a website, we'll get the favicon url = "https://www.google.com/s2/favicons?sz=32&domain_url=" + url; + fileName = fileName + ".png" } - this.log.debug("new icon : "+url) this.url = url; - - var fileName = url.split("/").pop() - this.dlUrl = specialFolders.temp + "/HUES_iconscache/" + fileName + ".png" + this.dlUrl = specialFolders.temp + "/HUES_iconscache/" + fileName } +/** + * Downloads the icon file or returns it from cache then runs the callback + * @private + * @param {function} callback an action to execute once download is finished + */ WebIcon.prototype.download = function (callback) { - this.log.debug(this.dlUrl); + //only download if file doesn't exist, otherwise run callback directly var icon = new QFile(this.dlUrl); if (icon.exists()){ - this.log.debug("file exists") callback.apply(this, []); } else { - this.log.debug("starting download of icon "+this.url); var curl = new CURLProcess(this.url); var p = curl.asyncDownload(this.dlUrl); p["finished(int)"].connect(this, callback) } } +/** + * Call to set the icon to a specific widget. + * @param {QWidget} widget a widget that supports icons + */ WebIcon.prototype.setToWidget = function(widget){ this.widget = widget; - this.log.debug(widget) this.download(this.setIcon) } - +/** + * Sets the icon on the widget once the file is available. + * @private + */ WebIcon.prototype.setIcon = function () { - this.log.debug("download finished, setting icon") - this.log.debug("icon url : "+this.dlUrl) var icon = new QIcon(this.dlUrl); this.widget.icon = icon; } From 90732996b1702e046c6e29ed1bf34987c5327bba Mon Sep 17 00:00:00 2001 From: MathieuC Date: Sun, 23 May 2021 16:05:00 +0200 Subject: [PATCH 015/112] improve reactivity of popup display during install --- ExtensionStore/lib/store.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/ExtensionStore/lib/store.js b/ExtensionStore/lib/store.js index bd0cd40..883a1e7 100644 --- a/ExtensionStore/lib/store.js +++ b/ExtensionStore/lib/store.js @@ -1190,22 +1190,21 @@ ExtensionDownloader.prototype.downloadFiles = function () { this.log.info("starting download of files from extension " + this.extension.name); var destFolder = this.destFolder; this.log.debug(this.extension instanceof Extension) - var destPaths = this.extension.localPaths.map(function (x) { return destFolder + x }); - var dlFiles = [this.destFolder]; var progress = new QProgressDialog(); progress.title = "Installing extension " + this.extension.name; progress.setLabelText("Downloading files..."); - progress.setRange(0, files.length); progress.modal = true; + progress.show(); // log ("destPaths: "+destPaths) + var destPaths = this.extension.localPaths.map(function (x) { return destFolder + x }); + var dlFiles = [this.destFolder]; var files = this.extension.files; + progress.setRange(0, files.length); this.log.debug("downloading files : "+files.map(function(x){return x.path}).join("\n")) - progress.show(); - for (var i = 0; i < files.length; i++) { webQuery.download(this.getDownloadUrl(files[i].path), destPaths[i]); From 3d6fa36eccb2865220db7d744c97afcc2c2e3f75 Mon Sep 17 00:00:00 2001 From: MathieuC Date: Sun, 23 May 2021 18:48:29 +0200 Subject: [PATCH 016/112] handle multiple file formats for github avatars --- ExtensionStore/lib/network.js | 127 +++++++++++++++++++++++++--------- 1 file changed, 95 insertions(+), 32 deletions(-) diff --git a/ExtensionStore/lib/network.js b/ExtensionStore/lib/network.js index 78aa14b..530714e 100644 --- a/ExtensionStore/lib/network.js +++ b/ExtensionStore/lib/network.js @@ -63,18 +63,43 @@ NetworkConnexionHandler.prototype.download = function (url, destinationPath) { // WebIcon Class ----------------------------------------------------- function WebIcon(url) { - this.log = new Logger("Icon") - var fileName = url.split("/").pop() + this.log = new Logger("Icon"); + this.url = url; +} + +/** + * class member: the location of the cache + */ +WebIcon.cacheFolder = specialFolders.temp + "/HUES_iconscache/"; + + +/** + * the url for the download + */ +Object.defineProperty(WebIcon.prototype, "dlUrl", { + get: function () { + if (typeof this._dlUrl === 'undefined') { + var fileName = this.url.split("/").pop(); + + var idRe = /avatars.githubusercontent.com\/u\/(\d+)\?/ + var matches = idRe.exec(this.url); + if (matches) { + this.log.debug(matches); + // avatar urls on github are a bit strange + var extension = this.getImageFormat(); + fileName = matches[1] + "." + extension; + } else if (this.url.indexOf(".png") == -1) { + // dealing with a website, we'll get the favicon + this.url = "https://www.google.com/s2/favicons?sz=32&domain_url=" + this.url; + fileName = fileName + ".png"; + } - if (url.indexOf(".png") == -1){ - // dealing with a website, we'll get the favicon - url = "https://www.google.com/s2/favicons?sz=32&domain_url=" + url; - fileName = fileName + ".png" + this._dlUrl = WebIcon.cacheFolder + fileName; + } + return this._dlUrl; } +}) - this.url = url; - this.dlUrl = specialFolders.temp + "/HUES_iconscache/" + fileName -} /** * Downloads the icon file or returns it from cache then runs the callback @@ -84,22 +109,45 @@ function WebIcon(url) { WebIcon.prototype.download = function (callback) { //only download if file doesn't exist, otherwise run callback directly var icon = new QFile(this.dlUrl); - if (icon.exists()){ + if (icon.exists()) { callback.apply(this, []); } else { var curl = new CURLProcess(this.url); var p = curl.asyncDownload(this.dlUrl); - p["finished(int)"].connect(this, callback) + p["finished(int)"].connect(this, callback); } } +/** + * gets the content type from the file header + * @returns {string} the extension for the file + */ +WebIcon.prototype.getImageFormat = function () { + var curl = new CURLProcess(["-L", "-w", "%{content_type}", this.url]); + var response = curl.get(1000); + log.debug(response); + + var re = /image\/(\w+)/ + + if (response) { + var match = re.exec(response); + if (match) { + var extension = match[1].toLocaleLowerCase(); + this.log.debug(extension); + return extension; + } + } + + throw new Error("Couldn't get file format for url " + this.url); +} + /** * Call to set the icon to a specific widget. * @param {QWidget} widget a widget that supports icons */ -WebIcon.prototype.setToWidget = function(widget){ +WebIcon.prototype.setToWidget = function (widget) { this.widget = widget; - this.download(this.setIcon) + this.download(this.setIcon); } /** @@ -108,10 +156,25 @@ WebIcon.prototype.setToWidget = function(widget){ */ WebIcon.prototype.setIcon = function () { var icon = new QIcon(this.dlUrl); - this.widget.icon = icon; + var size = UiLoader.dpiScale(32) + icon.size = new QSize(size, size); + + // handle difference between QWidgets and QTreeWidgetItems + if (this.widget instanceof QWidget) { + this.widget.icon = icon; + } else if (this.widget instanceof QTreeWidgetItem) { + this.widget.setIcon(0, icon); + } } +WebIcon.deleteCache = function () { + var cache = new QDir(WebIcon.cacheFolder) + if (cache.exists()) { + cache.rmdirs() + } +} + // CURLProcess Class ------------------------------------------------- /** @@ -119,7 +182,7 @@ WebIcon.prototype.setIcon = function () { * Can perform asynchronous or inline operations without blocking the UI. * @param {*} command */ -function CURLProcess (command) { +function CURLProcess(command) { this.curl = new CURL() this.log = new Logger("CURL") @@ -129,11 +192,11 @@ function CURLProcess (command) { this.command = ["-s", "-S"].concat(command); var bin = this.curl.bin.split("/"); - this.app = bin.pop(); - var directory = bin.join("\\"); + this.app = bin.pop(); + var directory = bin.join("\\"); this.process = new QProcess(); - this.process.setWorkingDirectory(directory); + this.process.setWorkingDirectory(directory); } /** @@ -144,12 +207,12 @@ function CURLProcess (command) { * @param {bool} asText wether to parse the output as text or QByteArray in the callbacks. * @returns {QProcess} the launched process. */ -CURLProcess.prototype.asyncRead = function (readCallback, finishedCallback, asText){ - this.log.debug("Executing Process with arguments : "+this.app+" "+this.command.join(" ")); +CURLProcess.prototype.asyncRead = function (readCallback, finishedCallback, asText) { + this.log.debug("Executing Process with arguments : " + this.app + " " + this.command.join(" ")); this.process.start(this.app, this.command); - if (typeof readCallback !== 'undefined' && readCallback){ - var onRead = function(){ + if (typeof readCallback !== 'undefined' && readCallback) { + var onRead = function () { this.log.debug("readyread") var stdout = this.read(asText); readCallback(stdout); @@ -157,12 +220,12 @@ CURLProcess.prototype.asyncRead = function (readCallback, finishedCallback, asTe this.process.readyRead.connect(this, onRead); } - if (typeof finishedCallback !== 'undefined' && finishedCallback){ - var onFinished = function(returnCode){ + if (typeof finishedCallback !== 'undefined' && finishedCallback) { + var onFinished = function (returnCode) { this.log.debug("finished") var stdout = this.read(asText); finishedCallback(returnCode, stdout); - if (returnCode) this.log.error("CURL returned with error code "+returnCode) + if (returnCode) this.log.error("CURL returned with error code " + returnCode) } this.process["finished(int)"].connect(this, onFinished); } @@ -177,12 +240,12 @@ CURLProcess.prototype.asyncRead = function (readCallback, finishedCallback, asTe * @param {bool} [asText=true] wether to return the output as text or QByteArray * @returns the output from the process, as a string or QByteArray */ -CURLProcess.prototype.read = function (asText){ +CURLProcess.prototype.read = function (asText) { if (typeof asText === 'undefined' || asText === 'undefined' || asText === null) var asText = true; var readOut = this.process.readAllStandardOutput(); - if (asText){ + if (asText) { var output = new QTextStream(readOut).readAll(); - }else{ + } else { var output = readOut; } this.log.debug("output:" + output) @@ -238,12 +301,12 @@ CURLProcess.prototype.runAndWait = function (wait, runMethod, args) { if (loop.isRunning()) { this.process.kill(); loop.exit(); - throw new Error("Timeout running command "+this.command.join(" ")); + throw new Error("Timeout running command " + this.command.join(" ")); } }); // Start the process and enter an event loop until the QProcess exits. - this.process["finished(int)"].connect(this, function(){loop.exit()}) + this.process["finished(int)"].connect(this, function () { loop.exit() }) runMethod.apply(this, args); timer.start(wait); loop.exec(); @@ -258,7 +321,7 @@ CURLProcess.prototype.runAndWait = function (wait, runMethod, args) { * @param {int} [wait=5000] optional, the timeout for the query. * @returns {string} */ -CURLProcess.prototype.get = function(wait){ +CURLProcess.prototype.get = function (wait) { if (typeof wait === 'undefined') var wait = 5000; var output = this.runAndWait(wait, this.asyncRead) @@ -272,7 +335,7 @@ CURLProcess.prototype.get = function(wait){ * @param {int} [wait=30000] optional, the timeout for the query (for downloads, 30s by default) * @returns */ -CURLProcess.prototype.download = function(destinationPath, wait){ +CURLProcess.prototype.download = function (destinationPath, wait) { if (typeof wait === 'undefined') var wait = 30000; var output = this.runAndWait(wait, this.asyncDownload, [destinationPath]) From 1ff9e1c2793cffecd1741fd3963d56816763b6c1 Mon Sep 17 00:00:00 2001 From: MathieuC Date: Sun, 23 May 2021 18:49:14 +0200 Subject: [PATCH 017/112] add Icons to web links buttons and script makers --- ExtensionStore/app.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/ExtensionStore/app.js b/ExtensionStore/app.js index 80d00c3..3dede5c 100644 --- a/ExtensionStore/app.js +++ b/ExtensionStore/app.js @@ -312,6 +312,10 @@ StoreUI.prototype.updateExtensionsList = function(){ var sellerItem = new QTreeWidgetItem([sellers[i].name], 0); this.extensionsList.addTopLevelItem(sellerItem); + if (sellers[i].iconUrl){ + var sellerIcon = new WebIcon(sellers[i].iconUrl); + sellerIcon.setToWidget(sellerItem); + } sellerItem.setExpanded(true); for (var j in extensions) { @@ -335,6 +339,13 @@ StoreUI.prototype.updateDescriptionPanel = function(extension) { this.storeDescriptionPanel.sourceButton.toolTip = extension.package.repository; this.storeDescriptionPanel.websiteButton.toolTip = extension.package.website; + var websiteIcon = new WebIcon(extension.package.website) + websiteIcon.setToWidget(this.storeDescriptionPanel.websiteButton) + + // for some reason, this url is the only one that actually returns an icon + var githubIcon = new WebIcon("https://avatars.githubusercontent.com/u") + githubIcon.setToWidget(this.storeDescriptionPanel.sourceButton) + // update install button to reflect whether or not the extension is already installed if (this.localList.isInstalled(extension)) { var localExtension = this.localList.extensions[extension.id]; From 1b95c3376eb715ccb5486b02c6d6cbaf9b99ab93 Mon Sep 17 00:00:00 2001 From: MathieuC Date: Sun, 23 May 2021 18:49:40 +0200 Subject: [PATCH 018/112] cleanup some debug in store.js --- ExtensionStore/lib/store.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ExtensionStore/lib/store.js b/ExtensionStore/lib/store.js index 883a1e7..c018311 100644 --- a/ExtensionStore/lib/store.js +++ b/ExtensionStore/lib/store.js @@ -1189,7 +1189,6 @@ function ExtensionDownloader(extension) { ExtensionDownloader.prototype.downloadFiles = function () { this.log.info("starting download of files from extension " + this.extension.name); var destFolder = this.destFolder; - this.log.debug(this.extension instanceof Extension) var progress = new QProgressDialog(); progress.title = "Installing extension " + this.extension.name; @@ -1197,7 +1196,7 @@ ExtensionDownloader.prototype.downloadFiles = function () { progress.modal = true; progress.show(); - // log ("destPaths: "+destPaths) + // get the files list (heavy operations) var destPaths = this.extension.localPaths.map(function (x) { return destFolder + x }); var dlFiles = [this.destFolder]; var files = this.extension.files; From 2c27a9c32e23ddb107a63d48b3653ee401095a2e Mon Sep 17 00:00:00 2001 From: MathieuC Date: Mon, 24 May 2021 20:21:48 +0200 Subject: [PATCH 019/112] improve icon fetching speed by reducing curl calls --- ExtensionStore/lib/network.js | 197 +++++++++++++++++----------------- ExtensionStore/lib/store.js | 20 +++- 2 files changed, 116 insertions(+), 101 deletions(-) diff --git a/ExtensionStore/lib/network.js b/ExtensionStore/lib/network.js index 530714e..7e133bd 100644 --- a/ExtensionStore/lib/network.js +++ b/ExtensionStore/lib/network.js @@ -1,5 +1,6 @@ var Logger = require("logger.js").Logger var log = new Logger("CURL") +var readFile = require("io.js").readFile // NetworkConnexionHandler Class -------------------------------------- /** @@ -10,7 +11,6 @@ var log = new Logger("CURL") * @extends QObject */ function NetworkConnexionHandler() { - this.curl = new CURL(); } @@ -25,19 +25,15 @@ NetworkConnexionHandler.prototype.get = function (command) { json = JSON.parse(result); if (json.hasOwnProperty("message")) { if (json.message == "Not Found") { - log.error("File not present in repository : " + this._url); - return null; - } - if (json.message == "Not Found") { - log.error("File not present in repository : " + this._url); + log.error("File not present in repository : " + command); return null; } if (json.message == "Moved Permanently") { - log.error("Repository " + this._url + " has moved to : " + json.url); + log.error("Repository " + command + " has moved to : " + json.url); return json; } if (json.message == "400: Invalid request") { - log.error("Couldn't reach repository : " + this._url + ". Make sure it is a valid github address.") + log.error("Couldn't reach repository : " + command + ". Make sure it is a valid github address.") return null; } } @@ -65,6 +61,7 @@ NetworkConnexionHandler.prototype.download = function (url, destinationPath) { function WebIcon(url) { this.log = new Logger("Icon"); this.url = url; + this.readFile = readFile; } /** @@ -74,29 +71,31 @@ WebIcon.cacheFolder = specialFolders.temp + "/HUES_iconscache/"; /** - * the url for the download + * the path for the download */ -Object.defineProperty(WebIcon.prototype, "dlUrl", { +Object.defineProperty(WebIcon.prototype, "dlPath", { get: function () { - if (typeof this._dlUrl === 'undefined') { + if (typeof this._dlPath === 'undefined') { var fileName = this.url.split("/").pop(); - var idRe = /avatars.githubusercontent.com\/u\/(\d+)\?/ - var matches = idRe.exec(this.url); - if (matches) { - this.log.debug(matches); - // avatar urls on github are a bit strange - var extension = this.getImageFormat(); - fileName = matches[1] + "." + extension; + var userNameRe = /https:\/\/github.com\/([\w\d]+\.png)/ + var matches = userNameRe.exec(this.url); + + if (matches){ + // we have a github avatar url + fileName = matches[1]; } else if (this.url.indexOf(".png") == -1) { // dealing with a website, we'll get the favicon this.url = "https://www.google.com/s2/favicons?sz=32&domain_url=" + this.url; fileName = fileName + ".png"; } - this._dlUrl = WebIcon.cacheFolder + fileName; + this._dlPath = WebIcon.cacheFolder + fileName; } - return this._dlUrl; + return this._dlPath; + }, + set: function(newPath){ + this._dlPath = newPath; } }) @@ -108,39 +107,49 @@ Object.defineProperty(WebIcon.prototype, "dlUrl", { */ WebIcon.prototype.download = function (callback) { //only download if file doesn't exist, otherwise run callback directly - var icon = new QFile(this.dlUrl); - if (icon.exists()) { + var iconFile = new QFile(this.dlPath); + var alternatePath = this.dlPath.replace(".png", ".jpeg") + var alternateIcon = new QFile(alternatePath) + var fileName = this.dlPath.split("/").pop() + + if (iconFile.exists()) { + callback.apply(this, []); + } else if (alternateIcon.exists()){ + // github avatar can be pngs or jpegs + this.dlPath = alternatePath; callback.apply(this, []); } else { - var curl = new CURLProcess(this.url); - var p = curl.asyncDownload(this.dlUrl); - p["finished(int)"].connect(this, callback); - } -} + // no cached version, we fetch it + // we need to save the header from the download to retrieve file type + var headerPath = WebIcon.cacheFolder + fileName + "header.txt" + var curl = new CURLProcess(["-D", headerPath, this.url]); + var p = curl.asyncDownload(this.dlPath); + + // When donwload process completes, find the file type from the downloaded header + // and rename the file before calling the callback. + var renameFileExtension = function(){ + var header = this.readFile(headerPath); + + // detect the file format by looking for image/* value in header + var extensionRe = /image\/(\w+)/ + var match = extensionRe.exec(header); + if (match[1] != "png"){ + // by default, files will be named png, we rename otherwise. + iconFile.rename(alternateIcon.fileName()); + this.dlPath = alternatePath; + } -/** - * gets the content type from the file header - * @returns {string} the extension for the file - */ -WebIcon.prototype.getImageFormat = function () { - var curl = new CURLProcess(["-L", "-w", "%{content_type}", this.url]); - var response = curl.get(1000); - log.debug(response); - - var re = /image\/(\w+)/ - - if (response) { - var match = re.exec(response); - if (match) { - var extension = match[1].toLocaleLowerCase(); - this.log.debug(extension); - return extension; + // delete the saved header file. + var headerFile = new QFile(headerPath); + headerFile.remove() + + callback.apply(this, []); } + p["finished(int)"].connect(this, renameFileExtension); } - - throw new Error("Couldn't get file format for url " + this.url); } + /** * Call to set the icon to a specific widget. * @param {QWidget} widget a widget that supports icons @@ -155,7 +164,7 @@ WebIcon.prototype.setToWidget = function (widget) { * @private */ WebIcon.prototype.setIcon = function () { - var icon = new QIcon(this.dlUrl); + var icon = new QIcon(this.dlPath); var size = UiLoader.dpiScale(32) icon.size = new QSize(size, size); @@ -180,20 +189,25 @@ WebIcon.deleteCache = function () { /** * This class wraps a CURL Qprocess and handles the outputs. * Can perform asynchronous or inline operations without blocking the UI. - * @param {*} command + * @param {Array} command + * @param {string} [bin] optional, set the bin for this process */ -function CURLProcess(command) { - this.curl = new CURL() +function CURLProcess(command, bin) { this.log = new Logger("CURL") + if (typeof bin === 'undefined') { + var curl = new CURL() + var bin = curl.bin; + } + // The toonboom bundled curl doesn't seem to be equiped for ssh so we have to use unsafe mode if (typeof command == "string") var command = [command]; - if (this.curl.bin.indexOf("bin_3rdParty") != -1) command = ["-k"].concat(command); + if (bin.indexOf("bin_3rdParty") != -1) command = ["-k"].concat(command); this.command = ["-s", "-S"].concat(command); - var bin = this.curl.bin.split("/"); - this.app = bin.pop(); - var directory = bin.join("\\"); + var binPath = bin.split("/"); + this.app = binPath.pop(); + var directory = binPath.join("\\"); this.process = new QProcess(); this.process.setWorkingDirectory(directory); @@ -225,12 +239,12 @@ CURLProcess.prototype.asyncRead = function (readCallback, finishedCallback, asTe this.log.debug("finished") var stdout = this.read(asText); finishedCallback(returnCode, stdout); - if (returnCode) this.log.error("CURL returned with error code " + returnCode) + if (returnCode) this.log.error("CURL returned with error code " + returnCode); } this.process["finished(int)"].connect(this, onFinished); } - return this.process + return this.process; } @@ -248,7 +262,7 @@ CURLProcess.prototype.read = function (asText) { } else { var output = readOut; } - this.log.debug("output:" + output) + this.log.debug("output:" + output); var readErr = this.process.readAllStandardError(); var errors = new QTextStream(readErr).readAll(); @@ -267,18 +281,18 @@ CURLProcess.prototype.read = function (asText) { * @returns {QProcess} the process launched. */ CURLProcess.prototype.asyncDownload = function (destinationPath, callback) { - var url = this.command.pop() + var url = this.command.pop(); url = url.replace(/ /g, "%20"); destinationPath = destinationPath.replace(/[ :\?\*"\<\>\|][^/\\]/g, ""); - this.command = ["-L", "-o", destinationPath].concat(this.command) - this.command.push(url) + this.command = ["-L", "-o", destinationPath].concat(this.command); + this.command.push(url); - var dest = destinationPath.split("/").slice(0, -1).join("/") + var dest = destinationPath.split("/").slice(0, -1).join("/"); var dir = new QDir(dest); if (!dir.exists()) dir.mkpath(dest); - return this.asyncRead(null, callback) + return this.asyncRead(null, callback); } @@ -329,19 +343,6 @@ CURLProcess.prototype.get = function (wait) { } -/** - * Performs a download through curl. The result of the operation will be returned as well. - * @param {str} destinationPath The location to which the download will be saved. - * @param {int} [wait=30000] optional, the timeout for the query (for downloads, 30s by default) - * @returns - */ -CURLProcess.prototype.download = function (destinationPath, wait) { - if (typeof wait === 'undefined') var wait = 30000; - - var output = this.runAndWait(wait, this.asyncDownload, [destinationPath]) - return output; -} - // CURL Class -------------------------------------------------------- /** @@ -368,32 +369,32 @@ function CURL() { * // more : https://docs.sourcegraph.com/api/graphql/examples * // more info about authentication : https://developer.github.com/apps/building-github-apps/authenticating-with-github-apps/ */ -CURL.prototype.query = function (query, wait) { - if (typeof wait === 'undefined') var wait = 5000; - var bin = this.bin; - try { - var p = new QProcess(); +// CURL.prototype.query = function (query, wait) { +// if (typeof wait === 'undefined') var wait = 5000; +// var bin = this.bin; +// try { +// var p = new QProcess(); - log.debug("starting process :" + bin + " " + command); - var command = ["-H", "Authorization: Bearer YOUR_JWT", "-H", "Content-Type: application/json", "-X", "POST", "-d"]; - query = query.replace(/\n/gm, "\\\\n").replace(/"/gm, '\\"'); - command.push('" \\\\n' + query + '"'); - command.push("https://api.github.com/graphql"); +// log.debug("starting process :" + bin + " " + command); +// var command = ["-H", "Authorization: Bearer YOUR_JWT", "-H", "Content-Type: application/json", "-X", "POST", "-d"]; +// query = query.replace(/\n/gm, "\\\\n").replace(/"/gm, '\\"'); +// command.push('" \\\\n' + query + '"'); +// command.push("https://api.github.com/graphql"); - p.start(bin, command); +// p.start(bin, command); - p.waitForFinished(wait); +// p.waitForFinished(wait); - var readOut = p.readAllStandardOutput(); - var output = new QTextStream(readOut).readAll(); - //log ("json: "+output); +// var readOut = p.readAllStandardOutput(); +// var output = new QTextStream(readOut).readAll(); +// //log ("json: "+output); - return output; - } catch (err) { - log.error("Error with curl command: \n" + command.join(" ") + "\n" + err); - return null; - } -} +// return output; +// } catch (err) { +// log.error("Error with curl command: \n" + command.join(" ") + "\n" + err); +// return null; +// } +// } /** @@ -445,7 +446,9 @@ Object.defineProperty(CURL.prototype, "bin", { var bin = curl[i]; try { log.info("testing connexion by connecting to github.com") - this.get("https://www.github.com/", 500); + var p = new CURLProcess("https://github.com/", bin) + var response = p.get(500); + if (!response) throw new Error ("https://github.com/ unreachable.") log.info("CURL bin found, using: " + curl[i]) CURL.__proto__.bin = bin; return bin; diff --git a/ExtensionStore/lib/store.js b/ExtensionStore/lib/store.js index c018311..10bd8e2 100644 --- a/ExtensionStore/lib/store.js +++ b/ExtensionStore/lib/store.js @@ -257,7 +257,18 @@ Object.defineProperty(Seller.prototype, "name", { */ Object.defineProperty(Seller.prototype, "apiUrl", { get: function () { - return "https://api.github.com/users/" + this.masterRepositoryName.split("/")[0]; + return "https://api.github.com/users/" + this.githubUserName; + } +}); + + +/** + * get the github url for the user associated with the master Repository. + * @type {string} + */ + Object.defineProperty(Seller.prototype, "githubUserName", { + get: function () { + return this.masterRepositoryName.split("/")[0]; } }); @@ -269,9 +280,10 @@ Object.defineProperty(Seller.prototype, "apiUrl", { Object.defineProperty(Seller.prototype, "iconUrl", { get: function () { if (typeof this._icon === 'undefined') { - var userInfo = webQuery.get(this.apiUrl); - if (!userInfo.hasOwnProperty("avatar_url")) this._icon = ""; - this._icon = userInfo.avatar_url; + // var userInfo = webQuery.get(this.apiUrl); + // if (!userInfo.hasOwnProperty("avatar_url")) this._icon = ""; + // this._icon = userInfo.avatar_url; + return "https://github.com/" + this.githubUserName + ".png" } return this._icon } From a89702d1c1869bd1f52bb3af2e9f8d9b2e0b1e52 Mon Sep 17 00:00:00 2001 From: MathieuC Date: Mon, 24 May 2021 20:22:39 +0200 Subject: [PATCH 020/112] remove testing discord icon on load page --- ExtensionStore/app.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/ExtensionStore/app.js b/ExtensionStore/app.js index 3dede5c..78b0be0 100644 --- a/ExtensionStore/app.js +++ b/ExtensionStore/app.js @@ -80,9 +80,6 @@ function StoreUI(){ this.uninstallAction = new QAction("Uninstall", this); this.uninstallAction.triggered.connect(this, this.performUninstall); - - var icon = new WebIcon("https://discord.com") - icon.setToWidget(this.aboutFrame.loadStoreButton) } /** From a9dbdd64b2bea2a8f2385e37894dcea03edbb5fc Mon Sep 17 00:00:00 2001 From: MathieuC Date: Tue, 25 May 2021 12:43:15 +0200 Subject: [PATCH 021/112] added function recursiveRemoveDir to io.js --- ExtensionStore/lib/io.js | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/ExtensionStore/lib/io.js b/ExtensionStore/lib/io.js index 9367d78..0c310d8 100644 --- a/ExtensionStore/lib/io.js +++ b/ExtensionStore/lib/io.js @@ -65,7 +65,7 @@ function recursiveFileCopy(folder, destination) { var command = ["-Rv", folder + "/.", destination]; } - log.debug("starting process :"+bin+" "+command); + log.debug("starting process :" + bin + " " + command); p.start(bin, command); p.waitForFinished(-1); @@ -76,12 +76,38 @@ function recursiveFileCopy(folder, destination) { return output; } catch (err) { - log.error("error on line "+err.lineNumber+" of file "+err.fileName+": \n"+err); + log.error("error on line " + err.lineNumber + " of file " + err.fileName + ": \n" + err); return null; } } + +function recursiveRemoveDir(folder) { + var dir = new QDir(); + dir.setPath(folder); + dir.setFilter(QDir.Dirs); + var subfolders = dir.entryList(); + + var files = listFiles(folder) + + for (var i in files) { + var file = new QFile(folder + "/" + files[i]) + file.remove() + } + + for (var i in subfolders) { + if (subfolders[i] != "." && subfolders[i] != "..") { + recursiveRemoveDir(folder + "/" + subfolders[i]); + } + } + + log.debug("removing folder : " + folder) + dir = new Dir(folder); + dir.rmdirs(); +} + exports.listFiles = listFiles exports.writeFile = writeFile exports.readFile = readFile -exports.recursiveFileCopy = recursiveFileCopy \ No newline at end of file +exports.recursiveFileCopy = recursiveFileCopy +exports.recursiveRemoveDir = recursiveRemoveDir \ No newline at end of file From 483a81196dc36abc0a85de22cab9bb42fc453afe Mon Sep 17 00:00:00 2001 From: MathieuC Date: Tue, 25 May 2021 12:43:31 +0200 Subject: [PATCH 022/112] add back missing download function --- ExtensionStore/lib/network.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/ExtensionStore/lib/network.js b/ExtensionStore/lib/network.js index 7e133bd..8c3fde0 100644 --- a/ExtensionStore/lib/network.js +++ b/ExtensionStore/lib/network.js @@ -343,6 +343,13 @@ CURLProcess.prototype.get = function (wait) { } +CURLProcess.prototype.download = function (destinationPath, wait) { + if (typeof wait === 'undefined') var wait = 30000; + + var output = this.runAndWait(wait, this.asyncDownload, [destinationPath]) + return output; +} + // CURL Class -------------------------------------------------------- /** @@ -433,7 +440,7 @@ Object.defineProperty(CURL.prototype, "bin", { var curl = [System.getenv("windir") + "/system32/curl.exe", System.getenv("ProgramFiles") + "/Git/mingw64/bin/curl.exe", specialFolders.bin + "/bin_3rdParty/curl.exe"]; - // var curl = [specialFolders.bin + "/bin_3rdParty/curl.exe"]; // testing Harmony curl bin + var curl = [specialFolders.bin + "/bin_3rdParty/curl.exe"]; // testing Harmony curl bin } else { var curl = ["/usr/bin/curl", "/usr/local/bin/curl", From 347376c8ecfb9c3ae7b040bb976e7fb4754aa69a Mon Sep 17 00:00:00 2001 From: MathieuC Date: Tue, 25 May 2021 12:43:54 +0200 Subject: [PATCH 023/112] fix removing of folders during uninstall --- ExtensionStore/lib/store.js | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/ExtensionStore/lib/store.js b/ExtensionStore/lib/store.js index 10bd8e2..2146ad1 100644 --- a/ExtensionStore/lib/store.js +++ b/ExtensionStore/lib/store.js @@ -5,6 +5,7 @@ var io = require("./io.js"); var readFile = io.readFile; var writeFile = io.writeFile; var recursiveFileCopy = io.recursiveFileCopy; +var recursiveRemoveDir = io.recursiveRemoveDir; Logger.level = 2; @@ -1013,19 +1014,19 @@ LocalExtensionList.prototype.uninstall = function (extension) { if (!this.isInstalled(extension)) return true // extension isn't installed var localExtension = this.extensions[extension.id]; + // Otherwise remove all script files (.js, .ui, .png etc.) + var files = localExtension.package.localFiles; + for (var i in files) { + this.log.debug("removing file " + files[i]); + var file = new File(files[i]); + if (file.exists) file.remove(); + } + // Remove packages recursively as they have a parent directory. if (extension.package.isPackage) { - var folder = new Dir(this.installFolder + "packages/" + extension.name.replace(" ", "")); + var folder = this.installFolder + "/packages/" + extension.name.replace(" ", ""); this.log.debug("removing folder " + folder.path); - if (folder.exists) folder.rmdirs(); - } else { - // Otherwise remove all script files (.js, .ui, .png etc.) - var files = localExtension.package.localFiles; - for (var i in files) { - this.log.debug("removing file " + files[i]); - var file = new File(files[i]); - if (file.exists) file.remove(); - } + recursiveRemoveDir(folder); } // Update the extension list accordingly. From 84f8cc2aa00b5563af5f70b81bf99de274e4ef78 Mon Sep 17 00:00:00 2001 From: MathieuC Date: Tue, 25 May 2021 19:01:13 +0200 Subject: [PATCH 024/112] Revert "fix removing of folders during uninstall" This reverts commit 347376c8ecfb9c3ae7b040bb976e7fb4754aa69a. --- ExtensionStore/lib/store.js | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/ExtensionStore/lib/store.js b/ExtensionStore/lib/store.js index 2146ad1..10bd8e2 100644 --- a/ExtensionStore/lib/store.js +++ b/ExtensionStore/lib/store.js @@ -5,7 +5,6 @@ var io = require("./io.js"); var readFile = io.readFile; var writeFile = io.writeFile; var recursiveFileCopy = io.recursiveFileCopy; -var recursiveRemoveDir = io.recursiveRemoveDir; Logger.level = 2; @@ -1014,19 +1013,19 @@ LocalExtensionList.prototype.uninstall = function (extension) { if (!this.isInstalled(extension)) return true // extension isn't installed var localExtension = this.extensions[extension.id]; - // Otherwise remove all script files (.js, .ui, .png etc.) - var files = localExtension.package.localFiles; - for (var i in files) { - this.log.debug("removing file " + files[i]); - var file = new File(files[i]); - if (file.exists) file.remove(); - } - // Remove packages recursively as they have a parent directory. if (extension.package.isPackage) { - var folder = this.installFolder + "/packages/" + extension.name.replace(" ", ""); + var folder = new Dir(this.installFolder + "packages/" + extension.name.replace(" ", "")); this.log.debug("removing folder " + folder.path); - recursiveRemoveDir(folder); + if (folder.exists) folder.rmdirs(); + } else { + // Otherwise remove all script files (.js, .ui, .png etc.) + var files = localExtension.package.localFiles; + for (var i in files) { + this.log.debug("removing file " + files[i]); + var file = new File(files[i]); + if (file.exists) file.remove(); + } } // Update the extension list accordingly. From cf3d13152143950097885098cac7ddc6e13a3310 Mon Sep 17 00:00:00 2001 From: MathieuC Date: Tue, 25 May 2021 19:01:20 +0200 Subject: [PATCH 025/112] Revert "added function recursiveRemoveDir to io.js" This reverts commit a9dbdd64b2bea2a8f2385e37894dcea03edbb5fc. --- ExtensionStore/lib/io.js | 32 +++----------------------------- 1 file changed, 3 insertions(+), 29 deletions(-) diff --git a/ExtensionStore/lib/io.js b/ExtensionStore/lib/io.js index 0c310d8..9367d78 100644 --- a/ExtensionStore/lib/io.js +++ b/ExtensionStore/lib/io.js @@ -65,7 +65,7 @@ function recursiveFileCopy(folder, destination) { var command = ["-Rv", folder + "/.", destination]; } - log.debug("starting process :" + bin + " " + command); + log.debug("starting process :"+bin+" "+command); p.start(bin, command); p.waitForFinished(-1); @@ -76,38 +76,12 @@ function recursiveFileCopy(folder, destination) { return output; } catch (err) { - log.error("error on line " + err.lineNumber + " of file " + err.fileName + ": \n" + err); + log.error("error on line "+err.lineNumber+" of file "+err.fileName+": \n"+err); return null; } } - -function recursiveRemoveDir(folder) { - var dir = new QDir(); - dir.setPath(folder); - dir.setFilter(QDir.Dirs); - var subfolders = dir.entryList(); - - var files = listFiles(folder) - - for (var i in files) { - var file = new QFile(folder + "/" + files[i]) - file.remove() - } - - for (var i in subfolders) { - if (subfolders[i] != "." && subfolders[i] != "..") { - recursiveRemoveDir(folder + "/" + subfolders[i]); - } - } - - log.debug("removing folder : " + folder) - dir = new Dir(folder); - dir.rmdirs(); -} - exports.listFiles = listFiles exports.writeFile = writeFile exports.readFile = readFile -exports.recursiveFileCopy = recursiveFileCopy -exports.recursiveRemoveDir = recursiveRemoveDir \ No newline at end of file +exports.recursiveFileCopy = recursiveFileCopy \ No newline at end of file From e511538348deb398d01e1fc9e315c895109a0bb6 Mon Sep 17 00:00:00 2001 From: MathieuC Date: Tue, 25 May 2021 23:08:37 +0200 Subject: [PATCH 026/112] added icons support, with an experimental automated search (disabled for now) --- ExtensionStore/app.js | 9 ++++- ExtensionStore/lib/network.js | 6 ++- ExtensionStore/lib/store.js | 71 ++++++++++++++++++++++++++++++----- 3 files changed, 73 insertions(+), 13 deletions(-) diff --git a/ExtensionStore/app.js b/ExtensionStore/app.js index 78b0be0..6023378 100644 --- a/ExtensionStore/app.js +++ b/ExtensionStore/app.js @@ -479,7 +479,7 @@ DescriptionView.prototype = Object.create(QWebView.prototype) var icon = "✓"; this.setToolTip(1, "Extension is installed correctly."); var localExtension = localList.extensions[extension.id]; - log.debug(extension.id, localList.checkFiles(localExtension)); + // log.debug("checking files from "+extension.id, localList.checkFiles(localExtension)); if (localExtension.currentVersionIsOlder(extension.version)) { icon = "↺"; this.setToolTip(1, "Update available:\ncurrently installed version : v" + extension.version); @@ -492,6 +492,13 @@ DescriptionView.prototype = Object.create(QWebView.prototype) } this.setText(1, icon); + if (extension.iconUrl){ + // set up an icon if one is available + log.debug("adding icon to extension "+ extension.name + " from url : "+extension.iconUrl) + this.extensionIcon = new WebIcon(extension.iconUrl); + this.extensionIcon.setToWidget(this); + } + // store the extension id in the item this.setData(0, Qt.UserRole, extension.id); } diff --git a/ExtensionStore/lib/network.js b/ExtensionStore/lib/network.js index 8c3fde0..a3bf1a7 100644 --- a/ExtensionStore/lib/network.js +++ b/ExtensionStore/lib/network.js @@ -133,7 +133,7 @@ WebIcon.prototype.download = function (callback) { // detect the file format by looking for image/* value in header var extensionRe = /image\/(\w+)/ var match = extensionRe.exec(header); - if (match[1] != "png"){ + if (match && match[1] != "png"){ // by default, files will be named png, we rename otherwise. iconFile.rename(alternateIcon.fileName()); this.dlPath = alternatePath; @@ -285,6 +285,8 @@ CURLProcess.prototype.asyncDownload = function (destinationPath, callback) { url = url.replace(/ /g, "%20"); destinationPath = destinationPath.replace(/[ :\?\*"\<\>\|][^/\\]/g, ""); + this.log.debug("starting async download of url "+url) + this.command = ["-L", "-o", destinationPath].concat(this.command); this.command.push(url); @@ -440,7 +442,7 @@ Object.defineProperty(CURL.prototype, "bin", { var curl = [System.getenv("windir") + "/system32/curl.exe", System.getenv("ProgramFiles") + "/Git/mingw64/bin/curl.exe", specialFolders.bin + "/bin_3rdParty/curl.exe"]; - var curl = [specialFolders.bin + "/bin_3rdParty/curl.exe"]; // testing Harmony curl bin + // var curl = [specialFolders.bin + "/bin_3rdParty/curl.exe"]; // testing Harmony curl bin } else { var curl = ["/usr/bin/curl", "/usr/local/bin/curl", diff --git a/ExtensionStore/lib/store.js b/ExtensionStore/lib/store.js index 10bd8e2..e64f87b 100644 --- a/ExtensionStore/lib/store.js +++ b/ExtensionStore/lib/store.js @@ -280,9 +280,6 @@ Object.defineProperty(Seller.prototype, "apiUrl", { Object.defineProperty(Seller.prototype, "iconUrl", { get: function () { if (typeof this._icon === 'undefined') { - // var userInfo = webQuery.get(this.apiUrl); - // if (!userInfo.hasOwnProperty("avatar_url")) this._icon = ""; - // this._icon = userInfo.avatar_url; return "https://github.com/" + this.githubUserName + ".png" } return this._icon @@ -464,8 +461,8 @@ Object.defineProperty(Repository.prototype, "dlUrl", { */ Object.defineProperty(Repository.prototype, "package", { get: function () { - this.log.debug("getting repos package for repo " + this.apiUrl); if (typeof this._package === 'undefined') { + this.log.debug("getting repos package for repo " + this.apiUrl); var response = webQuery.get(this.dlUrl + "/tbpackage.json"); if (!response || response.message) { this.log.error("No valid package found in repository " + this._url + ": " + response.message) @@ -485,15 +482,20 @@ Object.defineProperty(Repository.prototype, "package", { */ Object.defineProperty(Repository.prototype, "contents", { get: function () { - this.log.debug("getting repos contents for repo " + this.apiUrl); if (typeof this._contents === 'undefined') { - var contents = webQuery.get(this.masterBranchTree + "?recursive=true"); - if (!contents) return null; + this.log.debug("getting repos contents for repo " + this.apiUrl); + try{ + var contents = webQuery.get(this.masterBranchTree + "?recursive=true"); + }catch(error){ + // in case of bad query, we avoid pulling it over and over, and consider it empty + this.log.error(error); + this._contents = []; + return this._contents; + } var tree = contents.tree; - this.log.debug(JSON.stringify(tree, null, " ")); - // this._contents = tree; + // this.log.debug(JSON.stringify(tree, null, " ")); var files = tree.map(function (file) { if (file.type == "tree") return {path:"/" + file.path + "/", size: file.size}; @@ -585,7 +587,7 @@ Repository.prototype.getFiles = function (filter) { var contents = this.contents; var paths = this.contents.map(function(x){return x.path}) - this.log.debug(paths.join("\n")) + // this.log.debug(paths.join("\n")) var search = this.searchToRe(filter) this.log.debug("getting files in repository that match search " + search) @@ -662,6 +664,7 @@ Object.defineProperty(Extension.prototype, "package", { "repository": this.repository._url, "isPackage": false, "files": [], + "icon": "", "keywords": [], "author": "", "license": "", @@ -677,8 +680,56 @@ Object.defineProperty(Extension.prototype, "package", { }) +/** + * Get the icon url for the extension + * @type {string} + */ + Object.defineProperty(Extension.prototype, "iconUrl", { + get: function () { + if (typeof this._icon === 'undefined') { + + var automaticSearchEnabled = false + + this.log.debug("getting icon url for "+this.name) + this._icon = ""; + if (this.package.icon){ + this._icon = this.package.icon; + }else if (automaticSearchEnabled){ + // try to guess the icon by looking for it in the files + // heavier since it requires polling github for the files + // CBB: cache the fact that a given extension has one/no icon? + // since this lets us search the HUES icons cache + var files = this.files.map(function(x){return x.path}); + this.log.debug(files); + + var pngs = []; + for (var i in files){ + var file = files[i]; + // this.log.debug("file:" + file) + if (file.indexOf(".png") != -1) pngs.push(file); + + // if it's named like the extension with a png suffix + if (file == this.name + ".png"){ + this._icon = file; + break + } + } + // if only 1 png is available in the list, we return it + if (pngs.length == 1) this._icon = pngs[0]; + } + if (this._icon) { + this.log.debug("found icon "+this._icon) + this._icon = this.repository.dlUrl + "/" + this._icon; + } + } + return this._icon; + } +}) + + /** * The highest level folder on the repository that includes files included in this extension + * Doesn't poll github, since it only looks at the files listed in the package * @name Extension#rootFolder * @type {object} */ From 26cbc62aacc9b518679967b3a57dca951ff69e55 Mon Sep 17 00:00:00 2001 From: MathieuC Date: Tue, 25 May 2021 23:45:45 +0200 Subject: [PATCH 027/112] add icons cache to avoid polling the google api more than once per website; also a fallback placeholder icon for extensions --- ExtensionStore/app.js | 4 ++++ ExtensionStore/lib/network.js | 21 +++++++++++++++++---- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/ExtensionStore/app.js b/ExtensionStore/app.js index 6023378..097eb77 100644 --- a/ExtensionStore/app.js +++ b/ExtensionStore/app.js @@ -497,6 +497,10 @@ DescriptionView.prototype = Object.create(QWebView.prototype) log.debug("adding icon to extension "+ extension.name + " from url : "+extension.iconUrl) this.extensionIcon = new WebIcon(extension.iconUrl); this.extensionIcon.setToWidget(this); + + }else{ + // fallback to local icon + this.setIcon(0, new QIcon(specialFolders.resource + "/icons/old/folder.png")) } // store the extension id in the item diff --git a/ExtensionStore/lib/network.js b/ExtensionStore/lib/network.js index a3bf1a7..6a0b8a0 100644 --- a/ExtensionStore/lib/network.js +++ b/ExtensionStore/lib/network.js @@ -65,10 +65,15 @@ function WebIcon(url) { } /** - * class member: the location of the cache + * class member: the location of the cache folder */ WebIcon.cacheFolder = specialFolders.temp + "/HUES_iconscache/"; +/** + * class memeber : a caching mechanism to avoid using curl + * when looking for the same website across several icons + */ +WebIcon.iconsCache = {} /** * the path for the download @@ -76,6 +81,12 @@ WebIcon.cacheFolder = specialFolders.temp + "/HUES_iconscache/"; Object.defineProperty(WebIcon.prototype, "dlPath", { get: function () { if (typeof this._dlPath === 'undefined') { + // first look in the cache for this url + if (WebIcon.iconsCache[this.url]){ + this._dlPath = WebIcon.iconsCache[this.url]; + return this._dlPath; + } + var fileName = this.url.split("/").pop(); var userNameRe = /https:\/\/github.com\/([\w\d]+\.png)/ @@ -91,6 +102,7 @@ Object.defineProperty(WebIcon.prototype, "dlPath", { } this._dlPath = WebIcon.cacheFolder + fileName; + WebIcon.iconsCache[this.url] = this._dlPath } return this._dlPath; }, @@ -178,9 +190,10 @@ WebIcon.prototype.setIcon = function () { WebIcon.deleteCache = function () { - var cache = new QDir(WebIcon.cacheFolder) - if (cache.exists()) { - cache.rmdirs() + WebIcon.iconsCache = {}; + var cache = new Dir(WebIcon.cacheFolder); + if (cache.exists) { + cache.rmdirs(); } } From c27fc44c818ed388a44a55eb0b170f0886b68d3b Mon Sep 17 00:00:00 2001 From: bob-ross27 <55351437+bob-ross27@users.noreply.github.com> Date: Sat, 22 May 2021 00:14:20 -0300 Subject: [PATCH 028/112] UI Overhaul - Added style lib and associated resources (qss, icons). --- ExtensionStore/app.js | 108 ++- ExtensionStore/lib/style.js | 107 +++ .../resources/GitHub-Mark-Light-32px.png | Bin 0 -> 1571 bytes ExtensionStore/resources/cancel_icon.png | Bin 0 -> 2091 bytes .../resources/default_extension_icon.png | Bin 0 -> 1602 bytes .../resources/default_github_avatar_icon.png | Bin 0 -> 1895 bytes ExtensionStore/resources/error_icon.png | Bin 0 -> 1886 bytes ExtensionStore/resources/globe_icon.png | Bin 0 -> 604 bytes ExtensionStore/resources/installed_icon.png | Bin 0 -> 1965 bytes ExtensionStore/resources/logo_header.png | Bin 0 -> 2708 bytes .../resources/magnifying_glass_icon.png | Bin 0 -> 1603 bytes .../resources/not_installed_icon.png | Bin 0 -> 2044 bytes ExtensionStore/resources/store.ui | 620 ++++++++++++------ ExtensionStore/resources/stylesheet_dark.qss | 355 ++++++++++ ExtensionStore/resources/stylesheet_light.qss | 0 ExtensionStore/resources/update_icon.png | Bin 0 -> 1936 bytes 16 files changed, 962 insertions(+), 228 deletions(-) create mode 100644 ExtensionStore/lib/style.js create mode 100644 ExtensionStore/resources/GitHub-Mark-Light-32px.png create mode 100644 ExtensionStore/resources/cancel_icon.png create mode 100644 ExtensionStore/resources/default_extension_icon.png create mode 100644 ExtensionStore/resources/default_github_avatar_icon.png create mode 100644 ExtensionStore/resources/error_icon.png create mode 100644 ExtensionStore/resources/globe_icon.png create mode 100644 ExtensionStore/resources/installed_icon.png create mode 100644 ExtensionStore/resources/logo_header.png create mode 100644 ExtensionStore/resources/magnifying_glass_icon.png create mode 100644 ExtensionStore/resources/not_installed_icon.png create mode 100644 ExtensionStore/resources/stylesheet_dark.qss create mode 100644 ExtensionStore/resources/stylesheet_light.qss create mode 100644 ExtensionStore/resources/update_icon.png diff --git a/ExtensionStore/app.js b/ExtensionStore/app.js index 097eb77..424944f 100644 --- a/ExtensionStore/app.js +++ b/ExtensionStore/app.js @@ -2,7 +2,7 @@ var storelib = require("./lib/store.js"); var Logger = require("./lib/logger.js").Logger; var log = new Logger("UI") var WebIcon = require("./lib/network.js").WebIcon - +var style = require("./lib/style.js"); /** * The main extension store widget class @@ -23,37 +23,72 @@ function StoreUI(){ this.ui.minimumWidth = UiLoader.dpiScale(350); this.ui.minimumHeight = UiLoader.dpiScale(200); + // Set the global application stylesheet + this.ui.setStyleSheet(style.getSyleSheet()); + // create shorthand references to some of the main widgets of the ui + this.eulaFrame = this.ui.eulaFrame; this.storeFrame = this.ui.storeFrame; this.aboutFrame = this.ui.aboutFrame; - this.storeListPanel = this.storeFrame.storeSplitter.widget(0); - this.storeDescriptionPanel = this.storeFrame.storeSplitter.widget(1); + this.storeListPanel = this.storeFrame.storeSplitter.extensionFrame; + this.storeDescriptionPanel = this.storeFrame.storeSplitter.sidepanelFrame; this.extensionsList = this.storeListPanel.extensionsList; this.updateRibbon = this.aboutFrame.updateRibbon + this.storeHeader = this.storeFrame.storeHeader; + this.storeFooter = this.storeFrame.storeFooter; // Hide the store and the loading UI elements. this.storeFrame.hide(); this.setUpdateProgressUIState(false); - var logo = storelib.appFolder+"/resources/logo.png" - var logoPixmap = new QPixmap(logo); - this.aboutFrame.storeLabel.setPixmap(logoPixmap) + if (!preferences.getBool("HUES_EULA_ACCEPTED", "")) { + this.aboutFrame.hide(); + + // EULA logo + var eulaLogo = storelib.appFolder + "/resources/logo.png" + eulaLogo = style.getImage(eulaLogo); + var eulaLogoPixmap = new QPixmap(eulaLogo); + this.eulaFrame.innerFrame.eulaLogo.setPixmap(eulaLogoPixmap); + this.eulaFrame.innerFrame.eulaCB.stateChanged.connect(this, function() { + preferences.setBool("HUES_EULA_ACCEPTED", true); + this.eulaFrame.hide(); + this.aboutFrame.show(); + }); + } + else { + this.eulaFrame.hide(); + } + + // About logo + var logo = storelib.appFolder+"/resources/logo.png"; + logo = style.getImage(logo); + var logoPixmap = new QPixmap(logo); + this.aboutFrame.storeLabel.setPixmap(logoPixmap); + + // Header logo + var headerLogo = storelib.appFolder+"/resources/logo_header.png"; + headerLogo = style.getImage(headerLogo); + var headerLogoPixmap = new QPixmap(headerLogo); + this.storeHeader.headerLogo.setPixmap(headerLogoPixmap); + this.checkForUpdates() // connect UI signals this.aboutFrame.loadStoreButton.clicked.connect(this, this.loadStore) // filter the store list -------------------------------------------- - this.storeFrame.searchStore.textChanged.connect(this, this.updateExtensionsList) + this.storeHeader.searchStore.textChanged.connect(this, this.updateExtensionsList) // filter by installed only ----------------------------------------- - this.storeFrame.showInstalledCheckbox.toggled.connect(this, this.updateExtensionsList) + this.storeHeader.showInstalledCheckbox.toggled.connect(this, this.updateExtensionsList) // Clear search button ---------------------------------------------- - UiLoader.setSvgIcon(this.storeFrame.storeClearSearch, specialFolders.resource + "/icons/old/edit_delete.png"); - this.storeFrame.storeClearSearch.clicked.connect(this, function () { - this.storeFrame.searchStore.text = ""; + var clearSearchIcon = storelib.appFolder+"/resources/cancel_icon.png"; + clearSearchIcon = style.getImage(clearSearchIcon); + UiLoader.setSvgIcon(this.storeHeader.storeClearSearch, clearSearchIcon); + this.storeHeader.storeClearSearch.clicked.connect(this, function () { + this.storeHeader.searchStore.text = ""; }) // update and display the description panel when selection changes -- @@ -69,7 +104,7 @@ function StoreUI(){ QDesktopServices.openUrl(new QUrl(this.storeDescriptionPanel.websiteButton.toolTip)); }); - this.storeFrame.registerButton.clicked.connect(this, this.registerExtension); + this.storeFooter.registerButton.clicked.connect(this, this.registerExtension); // Install Button Actions ------------------------------------------- this.installAction = new QAction("Install", this); @@ -102,6 +137,7 @@ StoreUI.prototype.show = function(){ * @param {boolean} visible - Determine whether the progress state should be enabled or disabled. */ StoreUI.prototype.setUpdateProgressUIState = function(visible){ + this.aboutFrame.updateButton.visible = !visible; this.aboutFrame.loadStoreButton.visible = !visible; this.aboutFrame.updateLabel.visible = visible; this.aboutFrame.updateProgress.visible = visible; @@ -203,7 +239,8 @@ StoreUI.prototype.getInstalledVersion = function(){ StoreUI.prototype.checkForUpdates = function(){ var updateRibbon = this.updateRibbon - var updateRibbonStyleSheet = "QWidget { background-color: blue; }"; + var defaultRibbonStyleSheet = "QWidget { background-color: " + style.COLORS["03DP"] + "; color: white; bottom-right-radius: 10px; bottom-left-radius: 10px }"; + var updateRibbonStyleSheet = "QWidget { background-color: " + style.COLORS.YELLOW + "; color: black }"; var storeUi = this; try{ @@ -215,12 +252,13 @@ StoreUI.prototype.checkForUpdates = function(){ if (!storeExtension.currentVersionIsOlder(currentVersion) && (currentVersion != storeVersion)) { updateRibbon.storeVersion.setText("v" + currentVersion + " ⓘ New version available: v" + storeVersion); updateRibbon.setStyleSheet(updateRibbonStyleSheet); - updateRibbon.updateButton.toolTip = storeExtension.package.description; - updateRibbon.updateButton.clicked.connect(this, function(){storeUi.updateStore(currentVersion, storeVersion)}); + this.aboutFrame.updateButton.toolTip = storeExtension.package.description; + this.aboutFrame.updateButton.clicked.connect(this, function(){storeUi.updateStore(currentVersion, storeVersion)}); } else { - updateRibbon.updateButton.hide(); + this.aboutFrame.updateButton.hide(); updateRibbon.storeVersion.setText("v" + currentVersion + " ✓ - Store is up to date."); - this.storeFrame.storeVersionLabel.setText("v" + currentVersion ); + updateRibbon.setStyleSheet(defaultRibbonStyleSheet); + this.storeFooter.storeVersionLabel.setText("v" + currentVersion ); } }catch(err){ @@ -237,10 +275,10 @@ StoreUI.prototype.checkForUpdates = function(){ * @param {*} message */ StoreUI.prototype.lockStore = function(message){ - var noConnexionRibbonStyleSheet = "QWidget { background-color: darkRed; color: white; }"; + var noConnexionRibbonStyleSheet = "QWidget { background-color: " + style.COLORS.RED + "; color: white; }"; - this.aboutFrame.loadStoreButton.enabled = false; - this.updateRibbon.updateButton.hide(); + this.ui.aboutFrame.loadStoreButton.enabled = false; + this.ui.aboutFrame.updateButton.hide(); this.updateRibbon.setStyleSheet(noConnexionRibbonStyleSheet); this.updateRibbon.storeVersion.setText(message); } @@ -272,7 +310,7 @@ StoreUI.prototype.updateExtensionsList = function(){ return a.name.toLowerCase() < b.name.toLowerCase()?-1:1 } - var filter = this.storeFrame.searchStore.text; + var filter = this.storeHeader.searchStore.text; var sellers = this.store.sellers; // sort sellers alphabetically sellers.sort(nameSort) @@ -301,7 +339,7 @@ StoreUI.prototype.updateExtensionsList = function(){ for (var j in extensionList) { var extension = extensionList[j]; - if (this.storeFrame.showInstalledCheckbox.checked && !this.localList.isInstalled(extension)) continue; + if (this.storeHeader.showInstalledCheckbox.checked && !this.localList.isInstalled(extension)) continue; if (extension.matchesSearch(filter)) extensions.push(extension); } @@ -347,17 +385,21 @@ StoreUI.prototype.updateDescriptionPanel = function(extension) { if (this.localList.isInstalled(extension)) { var localExtension = this.localList.extensions[extension.id]; if (!localExtension.currentVersionIsOlder(extension.version) && this.localList.checkFiles(extension)) { + // Extension installed and up-to-date. + this.storeDescriptionPanel.installButton.setStyleSheet("QToolButton { border-color: transparent transparent " + style.COLORS.ORANGE + " transparent; }"); this.storeDescriptionPanel.installButton.removeAction(this.installAction); this.storeDescriptionPanel.installButton.removeAction(this.updateAction); this.storeDescriptionPanel.installButton.setDefaultAction(this.uninstallAction); } else { - //change to update + // Extension installed and update available. + this.storeDescriptionPanel.installButton.setStyleSheet("QToolButton { border-color: transparent transparent " + style.COLORS.YELLOW + " transparent; }"); this.storeDescriptionPanel.installButton.removeAction(this.installAction); this.storeDescriptionPanel.installButton.removeAction(this.uninstallAction); this.storeDescriptionPanel.installButton.setDefaultAction(this.updateAction); } } else { - // installAction.setText("Install") + // Extension not installed. + this.storeDescriptionPanel.installButton.setStyleSheet("QToolButton { border-color: transparent transparent " + style.COLORS.GREEN + " transparent; }"); this.storeDescriptionPanel.installButton.removeAction(this.uninstallAction); this.storeDescriptionPanel.installButton.removeAction(this.updateAction); this.storeDescriptionPanel.installButton.setDefaultAction(this.installAction); @@ -443,14 +485,12 @@ StoreUI.prototype.performUninstall = function(){ * A QWebView to display the description * @param {QWidget} parent */ - function DescriptionView(parent){ +function DescriptionView(parent){ var webPreviewsFontFamily = "Arial"; var webPreviewsFontSize = UiLoader.dpiScale(12); - var webPreviewsStyleSheet = "QWebView { background-color: lightGrey; }"; QWebView.call(this, parent) - this.setStyleSheet(webPreviewsStyleSheet); this.setMinimumSize(0, 0); this.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum); var settings = this.settings(); @@ -476,21 +516,21 @@ DescriptionView.prototype = Object.create(QWebView.prototype) QTreeWidgetItem.call(this, [extensionLabel, icon], 1024); // add an icon in the middle column showing if installed and if update present if (localList.isInstalled(extension)) { - var icon = "✓"; + var icon = style.ICONS["installed"]; this.setToolTip(1, "Extension is installed correctly."); var localExtension = localList.extensions[extension.id]; // log.debug("checking files from "+extension.id, localList.checkFiles(localExtension)); if (localExtension.currentVersionIsOlder(extension.version)) { - icon = "↺"; + icon = style.ICONS["update"]; this.setToolTip(1, "Update available:\ncurrently installed version : v" + extension.version); } else if (!localList.checkFiles(localExtension)) { - icon = "!"; + icon = style.ICONS["error"]; this.setToolTip(1, "Some files from this extension are missing."); } } else { - var icon = "✗"; + icon = style.ICONS["not installed"]; } - this.setText(1, icon); + this.setIcon(1, new QIcon(icon)); if (extension.iconUrl){ // set up an icon if one is available @@ -500,7 +540,9 @@ DescriptionView.prototype = Object.create(QWebView.prototype) }else{ // fallback to local icon - this.setIcon(0, new QIcon(specialFolders.resource + "/icons/old/folder.png")) + var extensionIcon = storelib.appFolder + "/resources/default_extension_icon.png"; + extensionIcon = style.getImage(extensionIcon); + this.setIcon(0, new QIcon(extensionIcon)); } // store the extension id in the item diff --git a/ExtensionStore/lib/style.js b/ExtensionStore/lib/style.js new file mode 100644 index 0000000..002202d --- /dev/null +++ b/ExtensionStore/lib/style.js @@ -0,0 +1,107 @@ +var Logger = require("./logger.js").Logger; +var io = require("./io.js"); + +var log = new Logger("Style"); +var appFolder = require("./store.js").appFolder; + +// Enum to hold dark style palette. +// 4% opacity over Material UI palette. +const ColorsDark = { + "00DP": "#121115", + "01DP": "#1E1D21", + "02DP": "#232226", + "03DP": "#252428", + "04DP": "#29282C", + "06DP": "#2C2B2F", + "08DP": "#2E2D31", + "12DP": "#333236", + "16DP": "#363539", + "24DP": "#38373B", + "ACCENT_LIGHT": "#B6B1D8", // Lighter - 50% white screen overlaid. + "ACCENT_PRIMARY": "#4B3C9E", // Full intensity + "ACCENT_DARK": "#373061", // Subdued - 50% against D1 + "ACCENT_BG": "#2B283B", // Very subdued - 20% against D1 + "GREEN": "#30D158", // Valid. + "RED": "#FF453A", // Error + "YELLOW": "#FFD60A", // New or updated + "ORANGE": "#FF9F0A", // Notice. + "BLUE": "#A1CBEC", // Store update. +} + +// Enum to hold light style palette. +// TODO: Make dedicated light theme and associated palette. +const ColorsLight = ColorsDark; + +const COLORS = isDarkStyle() ? ColorsDark : ColorsLight; + +var iconFolder = appFolder + "/resources"; +const ICONS = { + "installed": getImage(iconFolder + "/installed_icon.png"), + "update": getImage(iconFolder + "/update_icon.png"), + "error": getImage(iconFolder + "/error_icon.png"), + "not installed": getImage(iconFolder + "/not_installed_icon.png"), +} + +/** + * Detect the current Harmony application stylesheet. + * @returns {Boolean} true if dark style active, false if light theme active. + */ +function isDarkStyle() { + return preferences.getBool("DARK_STYLE_SHEET", ""); +} + + +/** + * Build and return a final qss string. Incorporate all necessary + * style-specific overrides. + */ +function getSyleSheet() { + var styleFile = storelib.appFolder + "/resources/stylesheet_dark.qss"; + var styleSheet = io.readFile(styleFile); + + // Get light-specific style overriddes + if (!isDarkStyle()) { + styleFileLight = storelib.appFolder + "/resources/stylesheet_light.qss"; + styleSheet += io.readFile(styleFileLight); + } + + // Replace template colors with final palettes. + for (color in COLORS) { + var colorRe = new RegExp("@" + color, "g"); + styleSheet = styleSheet.replace(colorRe, COLORS[color]); + } + + log.debug("Final qss stylesheet:\n" + styleSheet); + return styleSheet; +} + + +/** + * Return the appropriate image path based on Harmony style. + * @param {String} imagePath + * @returns {String} Path to the correct image for the Harmony style. + */ +function getImage(imagePath) { + + // Images are default themed dark - just return the original image if dark style is active. + if (isDarkStyle()) { + return imagePath; + } + + // Harmony in light theme. Attempt to use @light variant. + var image = new QFileInfo(imagePath); + var imageRemapped = new QFileInfo(image.absolutePath() + "/" + image.baseName() + "@light." + image.suffix()); + if (imageRemapped.exists()) { + log.debug("Using light themed variant of of " + imagePath); + return imageRemapped.filePath(); + } + + // @light variant not found, fallback to using original image path. + log.debug("No light styled variant of image, using default."); + return imagePath; +} + +exports.ICONS = ICONS; +exports.COLORS = COLORS; +exports.getImage = getImage; +exports.getSyleSheet = getSyleSheet; \ No newline at end of file diff --git a/ExtensionStore/resources/GitHub-Mark-Light-32px.png b/ExtensionStore/resources/GitHub-Mark-Light-32px.png new file mode 100644 index 0000000000000000000000000000000000000000..628da97c70890c73e59204f5b140c4e67671e92d GIT binary patch literal 1571 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 literal 0 HcmV?d00001 diff --git a/ExtensionStore/resources/default_extension_icon.png b/ExtensionStore/resources/default_extension_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..2f808f8b63900235b976552e4a31b6c49c6f2c14 GIT binary patch literal 1602 zcmbVMTWB0r7@l2X*+drPrCn5$qGiPRJH_mCcO|rWt3r-r6H5(B_&d!|O9Wpa# zoSE%z7O_dJ6fNSD5ZZddJ_#a%U{e$m1;sb@MHD3HgU}b1M(_eYcxG>;5-oP%%(;E% z`|tk?(^C`s`|j>z7-oNNGBZPG5RTYB`Y%NPJVU2mck&6JVQ#)X91-T+g+mN;(}0gi=N*5r!;rzg?wM|25{C^LQIS-G6h~EU*sClW{0jLO>hAS50;q2_tt(?s zU2MB6!Vj`1sTpmD?1}dC%PtmYuurOH$A(RIok+jS#M6N`9MFx4);`#hIUPkW?fTJ*x7nHXnt*>qA;lSy6{ zq^7!>26K)dAVXx%7HzG9Dd~UYMGNMWQcy!49xGFCT{rJUs=OE$Y|9SupC&f%b`^IAPCG`p?Di_U z>2^Ep&=%*>UiQi6^Domr_*E`5malC8^sUyv1`d3Bb^D?>cVhOb_pTmUV7|L@*Jp1% z@X)hsKR&+m`ICQL0-M@B>x-q;%j^pCO61IM>t`2s8ef#&zwb45sE57v)V^C5PLDFT z)&F??Ky0+o3wl?Xkxk`}xzmfxGud~UW86AJ4q7V%J%iszKJMEXT#pR*9Np?+qA$J? zjZw~vE0+%^qOq<1FNYpn`8o3Hc=XEsbh&jdk{GyGug12VvC(lRxs6wU__Zj#`Ssf` tUHX0M)S(X#-+A`nj$b(6h|KkGM2|lA(fP_Z6B}Wd&t<1F?~E_5{SAe>{ICE3 literal 0 HcmV?d00001 diff --git a/ExtensionStore/resources/default_github_avatar_icon.png b/ExtensionStore/resources/default_github_avatar_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c119f34e19335a32393bee64855ab05419da63a4 GIT binary patch literal 1895 zcmbVNTWs4@7*0bwdLdO95rUOUo`=wi>iCjdZ8hs9>DEk37j+RWVuBpo$BEm-Hn!U& z9ZczEqg^MaK_?1S8iE&i*^0zv(k6`#9+5UAgm{<=NPQTUru7Mxr*fPoQH$DbS|rEE zKHvZS-+wv3XJUOv9QFI^Z8n=D(i`qacgnhI_n`j)`@ip@Tb z)RpaR4p45R(Ta~9wPZ3C7lzq0ioz%*WJk0S&uftEhlV<$3$SMt$vj|1qj4b}0#nuF zs+ulWDpr<^F^t=b9UPDqQOz1HTMS?rm@tT2RU;@L!T90~&3OWxmmw$*M^T&5s4B`* zekZ7p;uzmHDC!vznBb9MQQ#z1&j1v#oB>HlYDyBvDrw|GYFgEiW2DZr^?f81is`B( zr_qMdf213W^o0C0>-Q6kn=ZE488Ekzqgzuzo1A zW&7j0jLslPKQWJWGOga>dSwGuHD5Y9C*a$qRa(Z1?ZN@UItD>pu<8#*y!2Y$Ap?&W z!6t1s1rg~(`eGAisFIllI_yfKY(28?$lb&nVD#bAAFp`3OW)R~Pa?gPRv`tHBt%1% z#I0aRYx-;!h5UcLihEloRT+-PpS7$EUi45I-B3Z)IvUJNcG+dKRkcLIUGdcN{g1zH zoS>X%uU~UEFO2-@ICe6zl3$o#V^^BZ?42Zdd4JpA%ZtAb>(OWEp{6ef^dGesm0*+S#SdT8xXmiP?1G+jw<hm9N zbtjhWch9`iHa&Uy@FXdIICykzXs%|D>(1o2-TPMUW7?aAmwvbZ^685QZ=GVlyX2}K zN<7_4oSO$Tw}0Dk-YWztqjouY_QC2qb@$%7>~P*V{?Au$I9g)ooy)Ox=kyWR^t-jw z7eUpPpW@XMf82GPJ{4Sl>ixwF^XqdT)C?ACZmd-oW}VkQg7^K_r!dmf7rxS+nD`IY CplJpG literal 0 HcmV?d00001 diff --git a/ExtensionStore/resources/error_icon.png b/ExtensionStore/resources/error_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..64c28a8633fd36d4b30fc27297142583068dea13 GIT binary patch literal 1886 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 Ntc)Dffu$vN{{_ny#k~Lk literal 0 HcmV?d00001 diff --git a/ExtensionStore/resources/magnifying_glass_icon.png b/ExtensionStore/resources/magnifying_glass_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..62f7a4d6ea0005eab0f22ce18cd77354b53ddf81 GIT binary patch literal 1603 zcmbVMTWB0r7@lg3ZET9PV1qZtDG3{!nK?6i>CTX`+3akyTe|6zU2GI8o}D?nJM7M! z>C9~Qf?yLtC|X(rirT7usFxymK`>wuACxGfmZ}dvC`RZ*5v4`Y=B4VHy%9^a*nuu+GHmmaw+PzZT{Fmy?Bx!ZrN_+ zX1&?;K3T&i2X&lBT-~$@nxe*c*KMekkw@oI(XbTe(%Fv~+Rzo|KsXK3b{v(AWYa;} z=5$VLmNiLdc0WLm*JVOrA`jAavtqe&U13_hGFkh_JVUo2URhyczCe05ouT8{L3D%z ztOh`Y-X(E5%tI}oAEQMe3Ooq$LWl)G4oY$;M0Y-hh;#IUJfrUE2qQa%DS4hP^L(vV z<7z<;J4IfQB#8$iFN!Qdux`WhV4bzx9bE<$xte3xo`EgeXM}lN^%RDv+P1;8d$g9@ z@e}bFUxzj?aKN{z1=O`3&aOI@mT+C;Q3aXE@?3%ydRV)JJ?xh7zfkwC{~>_*Hl6Ot zSXUR*?1^x_#2hiBS!o2N7UTmTVu;{2SE=zIgBgaNM*Vz)1t_Q z=)eKP(s9ilUGo5`&_fF2M~wwxRt)DvK@NsxaTg1MG5~F88tX=(u`V=nEtKSp4n25X zu&&7k?3j=kY?!c!c-ty6bkB`)99OVI1QYFoYtK{hc*em6qe3p+nW;%Sm54_KDH35t zPH3r{PRl9F^`NDpl&UbqIhw)pOQPE7lui~M>Z~dT-AWf3YJM3gjONE5=}hO? zxM2^pX=^1!NdF@*+AtRvyc%>+tVq0d-Mr(8^1NTLZ9DjDO>Et*Def+ubPRvl>s4ga z>vqT@EzTjmOuaR`ne@T0Q)(=i{PfpnV*^is+Y`m}=);BIH&^ev+-y?EPJQ|QiI-nJ z@nGPOE$qS5+12^O%V2S0^x$~^lP@%)BYT&J_HMfR+=@4THt?EeJstby`0K-y3xnU} z-#B(h_07Jy)u)W-mG=^r<4arLosd4>dG7G%&jik#n$qt1&OQ3UBli!TpY87({&~-t zmHFi{`m@ZHeOLRge0yo)@gGJPzrXpV-$u%x%#WVF>BpsuF16*Whx;$Q{e^XuR_A}( zmKxgtFREL!VCNCm9^O8(|Gsw?A3K!z>yBzX3#x B30D9B literal 0 HcmV?d00001 diff --git a/ExtensionStore/resources/not_installed_icon.png b/ExtensionStore/resources/not_installed_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..ced82ca4ca750dae467d4f1a4a5799cf5968e176 GIT binary patch literal 2044 zcmbVN2}~4M7#?v&QS1SDfr;ZFR*u=39hPH9mcw0SS9X#6u%>Z#W?(1o?uym@cl``-Wk z_f(~(Bu$^{KNW(Y>3W?u9n83EOz{No9FJc&gUL(K<%tk9&DS+1KzkczL(rsITxPD6 zYc!BFZ& zpz5d%R>3XSCXV?Dcnot;0;Z7TE}KSx4DH4Vg?7sbIYVQtg|)IaNd#Dh8!MQ3i5JcM zKZtf8zd`_fYc#q!UaO1M>ZTB-#3EqEm_uHR7Bfo(7E5PEzR*syiA6xoK$kTFsj;(^ z#M?7@-ZCDk)Nz$zf{-iWMLC>};fuxKmoBhcN@CTBD{3enff5m!1VKh9No6>Shm$xy z3N`W!mtXRCXgH~eCYAUrP>?eWB~fn#Gc=jc+pQEZn6pv^EGF0r5ZLWTQo~z#J0J$y zg}=P7*Jx7hd_HFZ2V#2C5?G(8iBzZ}BT+)G7*W?~B=t5?qHHv)*QycV9684^WPVfx zm9JvM(dfv0V60L}p(>mJX-b)*qZAR51Z4t5BM~FDJYDEYf${syf4slk&Vd@FEdN`M zt1?}=L+UsYM73nBbTZgAV=D^>kE9Dp(XKL3BeX02EQ5?~bN|}|qhgEAED-uHc`*tT z`FyFEva<;Vz*}$3I}9j~xe9jF4(zWcj-0(z+~aW2FqwS zfgq0$_1c6?^H0CjpH3mt-YM^yohhH0Yf7LT8_!kmiT@xWSU0c;-{*WHbNVR*wsf73 z4_Lfy!4JCTNele$TZoPI-N)_`ll_9*>juiMG(3tssrvE|?wwIqe=U1eC>$m{>Zog- z=!g%P4K0s{Ds|-@0sS41TelU3aTSO#*|5$#dzaSpF} z9N42&EgejJ1`G4+mpxxNtNuK*K52E$1=@fAr^|$kH#erPKM>eM7=Jz`k)3|USw6#c z-vl)Gt$BNIb^nGS{p}mi63(cD3r@wYOct(w*LZDzZ0F{a$yKRCY4dWj=iJY(MT3Uq zQ{S&}+K1$ac9Nm!mDVt`NH%{F(6EJ&T^ zx9}q|XKr?#Pk*uO(*1zP6*L{Iu%1n7zcpN6INZE0)Ol6bK0&i@)8buy<`=I2vpz9J K+p=WU`ac1m@7lir literal 0 HcmV?d00001 diff --git a/ExtensionStore/resources/store.ui b/ExtensionStore/resources/store.ui index 5fe85e8..687e6c5 100644 --- a/ExtensionStore/resources/store.ui +++ b/ExtensionStore/resources/store.ui @@ -6,8 +6,8 @@ 0 0 - 565 - 1043 + 862 + 1008 @@ -29,8 +29,154 @@ 0 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 25 + + + 25 + + + 25 + + + 25 + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + <html><head/><body><p><span style=" font-size:11pt; font-weight:600;">Harmony Unofficial Extension Store</span></p></body></html> + + + Qt::AlignCenter + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + + + + + QFrame::NoFrame + + + QFrame::Plain + + + true + + + + + 0 + 0 + 773 + 120 + + + + + + + <html><head/><body><p>Sed voluptas magnam quasi ipsa eaque occaecati voluptatum fugiat. Repudiandae odio commodi sed et cum nam quisquam. Nulla velit hic assumenda omnis omnis. Sint deleniti tempore atque a eveniet voluptas ab qui. </p><p>Tenetur autem earum iure blanditiis eum sit perferendis. Et qui tempore eum quidem reiciendis sint laudantium qui. Delectus nisi quia voluptas. Rerum adipisci voluptas facilis. Voluptate at quia nisi sequi labore. Ea optio necessitatibus ut sit. </p><p>Quod animi aut sapiente. Sed aut perspiciatis nesciunt minima voluptatem. Et dolorum itaque consectetur cumque perspiciatis necessitatibus. Aut quos ipsa amet. Et id in ut et omnis. Nostrum iste vitae sed cupiditate quia. </p></body></html> + + + Qt::AlignJustify|Qt::AlignTop + + + true + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + I accept + + + false + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + 0 + 0 + + QFrame::StyledPanel @@ -48,108 +194,117 @@ 0 - 9 + 0 0 - - - 0 - - - 9 - - - 9 - - - 9 - - - 6 - - - - - - 1 - 1 - - - - - 0 - 0 - - - - - 200 - 16777215 - - - - - - - Search by name or keyword... - - - true - - - - - - - - 0 - 1 - - - - - 23 - 23 - - - - X - - - true - - - - - - - Qt::Horizontal - - - QSizePolicy::Maximum - - - - 6 - 0 - - - - - - - - Qt::LeftToRight - - - Only show installed extensions - - - - + + + + 0 + + + 9 + + + 9 + + + 9 + + + 6 + + + + + + 0 + 0 + + + + Logo + + + + + + + + 1 + 1 + + + + + 0 + 0 + + + + + 200 + 16777215 + + + + + + + Search by name or keyword... + + + true + + + + + + + + 0 + 1 + + + + X + + + true + + + + + + + Qt::Horizontal + + + QSizePolicy::Maximum + + + + 12 + 0 + + + + + + + + Qt::LeftToRight + + + Only show installed extensions + + + + + @@ -175,19 +330,25 @@ 0 + + + Qt::Horizontal 4 - + 9 + + + true @@ -197,12 +358,21 @@ QAbstractItemView::ContiguousSelection + + + 18 + 18 + + 10 true + + true + 250 @@ -247,13 +417,13 @@ - + 9 - 0 + 9 @@ -510,8 +680,14 @@ 16777215 + + + - test + Install + + + Qt::ToolButtonIconOnly false @@ -525,75 +701,95 @@ - - - 0 - - - 9 - - - 3 - - - 9 - - - 3 - - - - - v0.0.1 - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 6 - 0 - - - - - - - - Register new extension - - - - - - - Qt::Horizontal - - - QSizePolicy::Expanding - - - - 0 - 0 - - - - - + + + + 0 + + + 9 + + + 3 + + + 9 + + + 3 + + + + + v0.0.1 + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 0 + + + + + + + + + 0 + 0 + + + + + 130 + 25 + + + + Register new extension + + + + + + + Qt::Horizontal + + + QSizePolicy::Expanding + + + + 0 + 0 + + + + + + + + + 0 + 0 + + QFrame::StyledPanel @@ -670,6 +866,9 @@ + + + QFrame::NoFrame @@ -684,8 +883,8 @@ 0 0 - 513 - 266 + 793 + 240 @@ -712,9 +911,18 @@ 0 + + + 500 + 0 + + false + + + <html><head/><body><p>This extension store is made available for free by entusiast script makers and users and is not endorsed or curated by Toonboom. Use the extensions available for download on this store at your own risk.</p><p>The extensions available on this store are open source and sourced from checked github accounts. Make sure you read the source code and understand it before you install any extension.</p><p>If you have a question about a script, you can use the provided github or website address to contact the script author. We cannot offer support for extensions not working correctly.</p><p>If you want to register as a script creator and want to share your scripts through this extension, get in touch and we will add your github account to the list of content makers.</p><p>The source for this extension can be viewed here: </p><p><a href="https://github.com/mchaptel/ExtensionStore/tree/master/packages/ExtensionStore"><span style=" text-decoration: underline; color:#55aaff;">https://github.com/mchaptel/ExtensionStore</span></a></p><p><a href="https://github.com/mchaptel/ExtensionStore/blob/master/SELLERSLIST"><span style=" text-decoration: underline; color:#55aaff;">The list of extension makers is available here.</span></a></p><p><a href="https://twitter.com/mathieuchaptel"><span style=" text-decoration: underline; color:#55aaff;">For news about the development and to give feedback, follow us on twitter!</span></a></p></body></html> @@ -771,8 +979,8 @@ - - + + @@ -791,6 +999,47 @@ + + + + + 0 + 0 + + + + + 80 + 30 + + + + + 200 + 16777215 + + + + Install Update + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 0 + 5 + + + + @@ -808,6 +1057,12 @@ + + + 16777215 + 5 + + 0 @@ -864,31 +1119,6 @@ - - - - - 0 - 0 - - - - - 80 - 0 - - - - - 200 - 16777215 - - - - Install Update - - - diff --git a/ExtensionStore/resources/stylesheet_dark.qss b/ExtensionStore/resources/stylesheet_dark.qss new file mode 100644 index 0000000..ab41d06 --- /dev/null +++ b/ExtensionStore/resources/stylesheet_dark.qss @@ -0,0 +1,355 @@ +/* +====================== +Global +Overall widget styling. +====================== +*/ + +/* Only useful if rounding corners and update label fixed. */ +QWidget#Form { + background-color: @00DP; +} + +/* Overall BG color */ +QFrame{ + background-color: @01DP; +} + +/* Set labels to transparent for text and logos. */ +QLabel{ + background-color: transparent; +} + +/* Loading/Installing progress bar */ +QProgressBar { + border: none; + padding: 0px; + background: @ACCENT_BG; +} +QProgressBar::chunk { + background-color: @ACCENT_PRIMARY; +} + +/* Push Buttons */ +QPushButton { + background: @08DP; + border-radius: 7px; + selection-color: @ACCENT_PRIMARY; +} +QPushButton:hover { + border-color: @ACCENT_LIGHT; + border-width: 2px; + background: @12DP; +} +QPushButton:pressed { + background: @02DP; + border-color: @ACCENT_PRIMARY; + border-width: 2px; +} + + +/* Scrollbars */ +QScrollBar { + image: none; + border: none; + background: @08DP; +} + +QScrollBar::handle { + border: none; + background: @08DP; + image: none; + border-color: none; +} +QScrollBar::handle:hover{ + background: @12DP; +} + +/* Scroll bar background (above handle) */ +QScrollBar::add-page { + border: none; + background: @02DP; +} + +/* Scroll bar background (below handle) */ +QScrollBar::sub-page { + border: none; + background: @02DP; +} + +/* Up arrow */ +QScrollBar::add-line { + border: none; + background: none; + image: none; +} + +/* Down arrow */ +QScrollBar::sub-line { + border: none; + background: none; + image: none; +} + +/* Line Edits */ +QLineEdit { + color: lightGrey; + margin: 1px; + border-color: @12DP; + border-width: 2px; + border-radius: 5px; + background: @03DP; + selection-background-color: @ACCENT_LIGHT; +} + +/* Tooltips */ +QToolTip { + color: lightGrey; + background-color: @06DP; +} + +/* +====================== +EULA Frame +====================== +*/ + +QFrame#eulaFrame { + background-color: @01DP; +} + +QFrame#innerFrame { + background-color: @06DP; + border-radius:10px; +} + +/* Scrollable region base widget */ +QScrollArea#scrollArea_2 { + margin-left: 5px; + margin-right: 5px; +} + +/* About screen scrollable region (text background). */ +QWidget#scrollAreaWidgetContents_2 { + background-color: @03DP; +} + +/* About Screen text */ +QLabel#eulaText { + background-color: transparent; + margin: 5px; + font-family: Arial; + font-size: 10pt; +} + +/* EULA checkbox */ +QCheckBox#eulaCB { + font-family: Arial; + font-size: 12pt; + border-color: @ACCENT_PRIMARY; + background-color: transparent; +} + +/* +====================== +About Frame +====================== +*/ + + +/* About Screen text */ +QLabel#label_3 { + background-color: transparent; + margin: 5px; + font-family: Arial; + font-size: 10pt; +} + +/* About screen scrollable region (text background). */ +QWidget#scrollAreaWidgetContents { + background-color: @02DP; +} + +/* Update Ribbon */ +QLabel#storeVersion { + font-family: Arial; + font-size: 10pt; +} + +/* Update Store Button */ +QPushButton#updateButton { + padding: 5px; + color: black; + background-color: @YELLOW; +} + +QPushButton:hover#updateButton { + border-color: @ORANGE; +} + +/* +====================== +Store Frame +====================== +*/ +/* Store Frame */ +QFrame#storeFrame { + background-color: @12DP; +} + +/* +====================== + Store Header +====================== +*/ +/* Store header */ +QFrame#storeHeader { + background-color: @08DP; + border-width: 3px; + border-style: solid; + border-color: transparent transparent @01DP transparent; +} + +/* Top-left Store logo */ +QLabel#headerLogo { + margin-right: 5px; +} + +/* Search store */ +QLineEdit#searchStore { + background-color: transparent; + border-radius: 0px; + border-width: 2px; + border-style: solid; + border-color: transparent transparent @ACCENT_LIGHT transparent; +} +QLineEdit:focus#searchStore { + background-color: @03DP; + border-radius: 10px; + border: none; +} + +/* Store search box clear */ +QToolButton#storeClearSearch { + border: none; + padding: 0px; + margin: 0px; + background-color: transparent; +} + +/* "show installed only" checkbox */ +QCheckBox#showInstalledCheckbox { + background-color: transparent; +} + +/* Sidebar Web View */ +QWebView { + background-color: lightGrey; +} + +/* +====================== + Store Footer +====================== +*/ + +QFrame#storeFooter { + background-color: @02DP; + padding: 1px; + border-width: 3px; + border-style: solid; + border-color: @01DP transparent transparent transparent; +} + +/* Register button */ +QPushButton#registerButton { + border-radius: 2px; + border-width: 2px; + border-style: solid; + border-color: transparent transparent @ACCENT_LIGHT transparent; + background-color: @02DP; +} +QPushButton:hover#registerButton { + background-color: @08DP; +} +/* +====================== + Store Sellers +====================== +*/ + +QTreeView { + background-color: @01DP; + border: 0px; + outline: none; +} +QTreeView::item { + color: white; + background-color: @01DP; + margin: 3px; + border: none +} + +QTreeView::item:hover { + margin:3px; + border-radius: 3px; + background-color: @02DP; +} + +QTreeView::item:selected { + margin:3px; + border-radius: 3px; + border-color: none; + background-color: @04DP; +} + +/* +====================== + Store Sidepanel +====================== +*/ + +QFrame#sidepanelFrame { + background-color: @04DP; +} + +/* Splitter selection handle */ +QSplitter::handle#storeSplitter { + background-color: @04DP; + border: none; +} + +QSplitter::handle:hover#storeSplitter { + background-color: @16DP; +} + +/* Keywords */ +QGroupBox#storeKeywordsGroup { + background-color: @06DP; +} + +QLineEdit#authorStoreLabel { + background-color: @06DP; +} + +QLineEdit#versionStoreLabel { + background-color: @06DP; +} + +/* Sidebar buttons */ +QToolButton { + background-color: @12DP; + border-radius: 3px; + padding: 5px; +} +QToolButton:hover { + background-color: @16DP; +} + +/* Install button */ +QToolButton#installButton { + border-radius: 4px; + border-width: 2px; + border-style: solid; + border-color: transparent transparent @ACCENT_LIGHT transparent; +} \ No newline at end of file diff --git a/ExtensionStore/resources/stylesheet_light.qss b/ExtensionStore/resources/stylesheet_light.qss new file mode 100644 index 0000000..e69de29 diff --git a/ExtensionStore/resources/update_icon.png b/ExtensionStore/resources/update_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..a1c975bdeac5db1fa223602688d41e40d04a358f GIT binary patch literal 1936 zcmbVNe^3-v9DklC2*`lo4WWyLTMOt)3p-A$Q8z1(zV&412qK+>Up*%kCa` zm=%*hkgU;aC_O>@6HORF+dayKmpV z_j%tRpYM-tcC0E*PRvXM03_S2mU8$elxJcb94l47H^EneU|k~tkd&@G<3QW)3;-rf z=bTk?mA#Cld7p-2co(Y)`2>gtV9BzOK+)@28F8_-oZpBJo;r;p9AiW)^X-IPFtZ-c zx zK+L?zA_W=(qY0t_S!B>Kl#8NWu3SV%=(ISY$F+KlAjmuesn;XX3x(=L#!Z%6mPeJr zFC*%aWr4);U@)i&=4p7b7S|dK2At60IvoZPnAG5xsSxIuvPLCXScw)nLFRZrqDZ7% zd_Xp$(9{thd_qjDUyAYs8N)-AfNM2`;?ppYp<_59AbN+DGc?Y6Ss&|{C5Y9=u!4t| zdC9~7gXq}ypA_C2ioHE#Z*XgNj-EUO2`AzlCwcz@p4w;10v0q)I&3~6mJC5 zEV7i$i%y>Rjz-EcYBHkJY4phaO3u&lL21F51FVITStF`M4I}a~UA|MNBlGj&jS+by zL5x7{Jj1yg{tnF}wS}Y-=`m24GYlnD4+S$c>E=Zr1s&#mR4t1O{#q1?(MXzkFE2vH z(7L>_>o&95A@Xj{3pb?l(qhC`VlL1c3JNftMmub--A>y4l1%w&)@CuHkQ@!iF(gCK z6vb*Oj47bB7|ZA>tdL!#$6SPtFlcr9yut$a=y?lI2b2^TJE zspJl6g zkP_LVTFBNz^A3l`<4VDf_<{e`#o@g%#XSm#9YdMMdKLVLbvxD%Tbu}cS)_06A^=p6 z+AKv*PydZJqyJ-5>K4qD(QzV}y~1KEEuFRbD^Xq8l(T|Ny@sr7)a^!!JDSt>m#RO! zJn4L4=hn+-Lcyz-=lA?v<6b)W{iP$uIh%j%>^hxtOWwQJdi(x<)$Z-n53Ba|EKb;o z^&3ESR|V+2x@VkY>T?$sds6xuT@m9!e|mSY=N;M78hNtOIWID0;L?&Uzy9=uA#+;p z)wPaC?lgXVtz_s_rK5L??`UG?rv53Z;f3*QK=r}{ubvwhp+88Q8Yxz3^OGa0oXV4* zcexLvGn#MTlxx#!S`ub!FJ3ow&Ru-~w6%{<=w(Ujp6R)X&C!lL6j7 zk;F>LW#9{2WQKX@&}-S1URBN0L(aQkYj*D^YB1}a#sfliLGVUYZ2F*2tW!?#6+>Hn7bW$;s+|aiDD%$gt+}E#bSFvtRjk zT~;~>G`F-8;7-o!j{nx;bob1t%j8MZZw6{$DYvZieI9y+GwugRs ze8;xr!Mg80+t_t8;jC3{0G4q5sVj@?<_|5u-j=bjeqMOON5Y}O#}M_gGM6`e^vHhn zDAIZ|E$kwn+I#Sq{)0P`?wv^-N_r-o`qr1{A;kgjo=-jCw~N)s>l1%yY1v*C-+ssE ygu|w?`1Xq{`IX>>(?^a2)1b8J{tjbqZ+zdtf;Vra?7OG@Gi@cSEQgD0p8o^*K%j{L literal 0 HcmV?d00001 From 69b73d6a782c84135fa022a5f20e0da1150cda94 Mon Sep 17 00:00:00 2001 From: bob-ross27 <55351437+bob-ross27@users.noreply.github.com> Date: Wed, 26 May 2021 12:33:52 -0300 Subject: [PATCH 029/112] Reduced uninstalled extension icon opacity for visual clarity. --- .../resources/not_installed_icon.png | Bin 2044 -> 2412 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/ExtensionStore/resources/not_installed_icon.png b/ExtensionStore/resources/not_installed_icon.png index ced82ca4ca750dae467d4f1a4a5799cf5968e176..ea4f081c92aa40e142499b3067e619861240f7d0 100644 GIT binary patch delta 942 zcmeyv|3+wn1ryuGjaJ=ETxKDLMpj1VR>nq?&oe0_bN?_^s+gOZ8Csa68t9r@q?za@ z8=0i(S{Rxe>n5fpn;Tdp8JHNGrEZ?doXj}co<)?oII(PUB#WG@g{4`FnVCt7u7P=C znyzuOiKT9mMWT_eSyHNHN@7}~aiU48l0tEbYgvg^c}8lUt&)*}k)f`Esjd;kIi^;o z7L)g|*a$-vC1)h&rKhIYD(NffPqtx^fZIfj`|#QX_BhZcK@6KFe`krS{}c0e1p@<9 zqNj^vNXEUlGtXunau7JaSS?iGtJuNhy{xQ(U#6^fsXn)~RM6|l_t#6$U)sJgct$hN z=3UmiSFb($uwNmqye_S5pPAKMzl-rLjVFV$#t;q+AV>5t>$7?UozZGI@7(bYCAYOdhI6@Md~L;w5t(;RawgOq4ChyCKWC&e{fu3*ro(ZCTP+O% z^*$x9JQ#|O-C(V2Yw?(AJn=SD$xPR2>6Hp5N9K5z#Y#7bMm`Wtt}0RPm{GWNs-^>T z^6#IAgMHojI42tZT$ac*!AR0Q$^Ap3sc4!6Wh6(Z)u1enYPB-m@#5+hKtsTV3%b-44jUl3KADw8DTW_(V^#mcUsjw#f-i5?M4sWz&?E zEc4$nOiYkWj`b=y8uM(W!{){rUfq`K=7crXJYc$bNGLAE@3qvdoyXgrN*%ehb>YLP zQ>RijvKbH7Z(X}{{j#{5e95c=?(x2R^VS62lSqd9<{!?Bk?R<$nv%)@=aPU_a7RYe_JeGjJGc<}gZ`;G?{ tGqjhr?|AZi@AWlbeQS9?oM+g_xK~rFBk+op8v_t{y85}Sb7D$p0sxTFdM^L~ delta 741 zcmVS)Ix#ppG%%C=0(p~n z1SYea17QJ^mIhgW(~ib$0007uNkl6wh0=blUApN#g1O}vkQBxt1D~Sf_bD$#dOd3@bgSn8X zlK!4m1nA{e(g-LmQKpxgiU8d>8O@-Q5_x(bmDjQC}gha_?3DHel zwb+EV5*3q8OpnoqYmmre8l#;`aLL?-P=cl- z(K1bc=D2h^z!Y~?68o6K8>Q2651NkPgC$Ag+V(-iJt=3nKU_<2&(@yQqdvfgw@Zmo z>QNt8RvGJ7lr1IdE(%+L@N()dsgpV@EzYnkXmdKHu{^nOSz1gg?gm(>&1t5|Ei4Q> zFYcZ0pO3Ujk8}Wf-V?Hr$#7Dm0` Date: Wed, 26 May 2021 13:18:54 -0300 Subject: [PATCH 030/112] Using installation button as an inline progress dialog. --- ExtensionStore/app.js | 4 +-- ExtensionStore/lib/store.js | 62 +++++++++++++++++++++++++++---------- 2 files changed, 47 insertions(+), 19 deletions(-) diff --git a/ExtensionStore/app.js b/ExtensionStore/app.js index 424944f..9b87e25 100644 --- a/ExtensionStore/app.js +++ b/ExtensionStore/app.js @@ -288,7 +288,7 @@ StoreUI.prototype.lockStore = function(message){ * installs the version of the store found on the repo. */ StoreUI.prototype.updateStore = function(currentVersion, storeVersion){ - var success = this.localList.install(this.storeExtension); + var success = this.localList.install(this.storeExtension, this.ui.aboutFrame.updateButton); if (success) { MessageBox.information("Store succesfully updated to version v" + storeVersion + ".\n\nPlease restart Harmony for changes to take effect."); this.updateRibbon.storeVersion.setText("v" + currentVersion); @@ -447,7 +447,7 @@ StoreUI.prototype.performInstall = function() { log.info("installing extension : " + extension.repository.name + extension.name); // log(JSON.stringify(extension.package, null, " ")) try { - this.localList.install(extension); + this.localList.install(extension, this.storeDescriptionPanel.installButton); MessageBox.information("Extension " + extension.name + " v" + extension.version + "\nwas installed correctly."); } catch (err) { log.error(err); diff --git a/ExtensionStore/lib/store.js b/ExtensionStore/lib/store.js index e64f87b..97abde3 100644 --- a/ExtensionStore/lib/store.js +++ b/ExtensionStore/lib/store.js @@ -1036,14 +1036,15 @@ LocalExtensionList.prototype.checkFiles = function (extension) { /** * Installs the extension + * @param {QToolButton} widget - Optional widget to use as a progressbar. * @returns {bool} the success of the installation */ -LocalExtensionList.prototype.install = function (extension) { +LocalExtensionList.prototype.install = function (extension, widget) { // if (this.isInstalled(extension)) return true; // extension is already installed var downloader = new ExtensionDownloader(extension); // dedicated object to implement threaded download later var installLocation = this.installLocation(extension) - var files = downloader.downloadFiles(); + var files = downloader.downloadFiles(widget); this.log.debug("downloaded files :\n" + files.join("\n")); var tempFolder = files[0]; // move the files into the script folder or package folder @@ -1247,41 +1248,68 @@ function ExtensionDownloader(extension) { /** * Downloads the files of the extension from the repository set in the object instance. + * @param {QToolButton} widget - UI icon to treat as a progressbar. * @returns [string[]] an array of paths of the downloaded files location, as well as the destination folder at index 0 of the array. */ -ExtensionDownloader.prototype.downloadFiles = function () { +ExtensionDownloader.prototype.downloadFiles = function (widget) { this.log.info("starting download of files from extension " + this.extension.name); var destFolder = this.destFolder; - - var progress = new QProgressDialog(); - progress.title = "Installing extension " + this.extension.name; - progress.setLabelText("Downloading files..."); - progress.modal = true; - progress.show(); - + // get the files list (heavy operations) var destPaths = this.extension.localPaths.map(function (x) { return destFolder + x }); var dlFiles = [this.destFolder]; var files = this.extension.files; - - progress.setRange(0, files.length); + this.log.debug("downloading files : "+files.map(function(x){return x.path}).join("\n")) - for (var i = 0; i < files.length; i++) { + // Set parameters based on whether provided widget is the update button, or install extension button. + var widgetClass = widget.objectName === "installButton" ? "QToolButton" : "QPushButton"; + var completedText = widget.objectName === "installButton" ? "Install Complete" : "Update Complete"; + var initialText = widget.objectName === "installButton" ? "Installing..." : "Updating..."; + var completedState = widget.objectName === "installButton" ? true : false; + // Configure widget style and text for installation. + widget.setStyleSheet( + widgetClass + "{\ + border-color: transparent transparent " + style.COLORS.GREEN + " transparent;\ + color: " + style.COLORS.GREEN + ";\ + }"); + widget.text = initialText; + + for (var i = 0; i < files.length; i++) { webQuery.download(this.getDownloadUrl(files[i].path), destPaths[i]); var dlFile = new File(destPaths[i]); if (dlFile.size == files[i].size) { // download complete! - this.log.debug("successfully downloaded " + files[i].path + " to location : " + destPaths[i]) - dlFiles.push(destPaths[i]) - progress.value = i; + this.log.debug("successfully downloaded " + files[i].path + " to location : " + destPaths[i]); + dlFiles.push(destPaths[i]); + + // Set stylesheet to act as a progressbar. + var progressStopL = i / files.length; + var progressStopR = progressStopL + 0.001; + var progressStyleSheet = widgetClass + " {\ + background-color:\ + qlineargradient(\ + spread:pad,\ + x1:0, y1:0, x2:1, y2:0,\ + stop: " + progressStopL + " " + style.COLORS.GREEN + ",\ + stop:" + progressStopR + " " + style.COLORS["12DP"] + + ");\ + border-color: transparent transparent " + style.COLORS.GREEN + " transparent;\ + color: " + style.COLORS.GREEN + ";\ + }"; + // Update widget with the new linear gradient progression. + widget.setStyleSheet(progressStyleSheet); + } else { throw new Error("Downloaded file " + destPaths[i] + " size does not match expected size : \n" + dlFile.size + " bytes (expected : " + files[i].size+" bytes)") } } - progress.close(); + // Configure widget to indicate the download is completed. + widget.setStyleSheet(widgetClass + " { border: none; background-color: " + style.COLORS.GREEN + "; color: black}"); + widget.text = completedText; + widget.enabled = completedState; return dlFiles; } From 48649995cfd614620af79fb940fbfcdf7142dc0b Mon Sep 17 00:00:00 2001 From: mchaptel Date: Fri, 28 May 2021 00:39:46 +0200 Subject: [PATCH 031/112] new fixed graphics --- ExtensionStore/resources/icon.png | Bin 0 -> 3076 bytes ExtensionStore/resources/icon.svg | 84 ++++++++++++++++++++++++++++++ ExtensionStore/resources/logo.png | Bin 10031 -> 33408 bytes logo_readme.png | Bin 20094 -> 46522 bytes 4 files changed, 84 insertions(+) create mode 100644 ExtensionStore/resources/icon.png create mode 100644 ExtensionStore/resources/icon.svg diff --git a/ExtensionStore/resources/icon.png b/ExtensionStore/resources/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..632901732c6c60014119b89469d54ec025e7d13f GIT binary patch literal 3076 zcmeHJ`&W{A8n>A)-jW#`?~0>&>y%2`YK#hv+A8WOXPqvtw}`|HQbQ*%c*zD~O-&Lk zE0w5iok`OgZ{YxCPs;X;5AX%_hrtWvp?)P`v>fD&I?~Y-{451j;6pDpI4Vs?$Dj?ob_D0N?gc=L z^y8<~BU5nc85m-MPDVzCTT(JU4U0)la7!U3-j(}W0?{{6fnR@{StXl_{^6k{mZzNV zSgkp-XQCPUSuOqWHwonhSCaNUI!)W%Vvg0{@ziqveci726>(y2op5LU8+((AljG&6 zuavZF_9qTFK!2bfiNq!}zkG+@7me5dO?Sf1lhhSAZ1^*bv`rd|Q)h`X6l0RhvM5Cw z-5vjUI{TC{pc8ym*0I)hT#;Fo*>oO=^1X4`*+U%IKUYMEQGS@IWopJmiIc@-W8C?i z_3E5;S+AglDuBSWfkUVy62dfxUjLARlIt->?Rco7M2r zw!VunT0^W=h^T)xVUce~_0T2;Yl>>o+>(X2!1B=S0S0XUEYC<(Rod6RjNo1S22ta$ z=jl;kS6hJs6aLxhD{=D+S1tGe0G*Uygj5#!!Nkp~zO+SVfe23OS#uA$Sg+6+ zcVB4(C-Iw5i1L}iX=}NfJ9KMlXR(Q`?km^cx2>X}${(%hn`iqc#z5f>)5FqB1xA0;jZ*H6`54!t=P)T+Au5Y4qdWR@b$CHu{fT9YP7bx7lo%1;AE z-7KNj`4WnE%)C2rAU(7uJS7KfMCeq!$oII^eM`|T4>Q_awC}aSs;W^OKfV>cJ_GUQ z(B=T7N}fy3ow*i);NUf`)!q9gcvQpGWBdXM8#mD8E`s^e=K7#D1Ojv>;w|mJ>5PZUrfg=%w32~Y zetFNdU-;~G0R40q@@7X0)bpA{Cy?z(&9`2EETdc$>9T ziGET@e`0y+B}&!he$2%5qXo;spftfR4!1V%J1^j8Q%Q>b3;V|itH%WB>lTVJ@@~gU zfbKK9K6cmdS62o^GAbzJld@xN|1nCNc=aUnLKC&UkQgivC!T1O_KK(0FM9XUCPp{< zi$*+nwBtN{Z?gsQP5aL@df4xy*1y{u z!d<)4f3$P{JfzYoAUyPLUcTxR7WsgOxH-}gUWDd~&54!=DOG_km~2D%MU#MVdj`yp zQgw4sOMwEEIT~HpF>LI~vK8Yo0W*Li8#~PjX`bsg zFe{Z%Q+#L#yK_Q+YskaB6zmH*6JahQy$vhR ze^n}3?Y^qxqU(6P? zSHyES)ivXL|AT&wdomi+JH24YsXM=TGTbx3rDj6Po$`kpWpM;81yR|pg2IkMqWMyK zBmPs*BVt=pY0Nl%uYc?N1;kO)Ph8*R^kMEaC6^=GdBVoi!%)7nA=-QcmKoX~FU{J0 zMwRzn=0O|up^ucUR^a{DC&54V1xgetN#Qc5mR&k literal 0 HcmV?d00001 diff --git a/ExtensionStore/resources/icon.svg b/ExtensionStore/resources/icon.svg new file mode 100644 index 0000000..afa0395 --- /dev/null +++ b/ExtensionStore/resources/icon.svg @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + diff --git a/ExtensionStore/resources/logo.png b/ExtensionStore/resources/logo.png index c2ae66b76cee492e046eaf42e9214e108ba3a5ba..1705eaaaef0c79a3f95d1b1a839b02b4811ace11 100644 GIT binary patch literal 33408 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 Y3LehBhVu6`=>J9rtLcHtpWD6pAA;bHDgXcg literal 10031 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 FV1o6O#iIZK diff --git a/logo_readme.png b/logo_readme.png index f5c83ed4013057f4a254ca4428be164fda1c5edd..8f8baef64a8efc950a241027e0ea36ae6a767e62 100644 GIT binary patch literal 46522 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�iMN@&N9li>BTLW-lX+h&k>GY;5#4^(+;-r0=) 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 Date: Fri, 28 May 2021 16:09:04 +0200 Subject: [PATCH 032/112] new class StyledImage to handle images from different stylesheets --- ExtensionStore/app.js | 71 ++++++++++++++++++------------------- ExtensionStore/lib/style.js | 65 +++++++++++++++++++++++++++++++-- 2 files changed, 97 insertions(+), 39 deletions(-) diff --git a/ExtensionStore/app.js b/ExtensionStore/app.js index 424944f..9cc722e 100644 --- a/ExtensionStore/app.js +++ b/ExtensionStore/app.js @@ -3,6 +3,7 @@ var Logger = require("./lib/logger.js").Logger; var log = new Logger("UI") var WebIcon = require("./lib/network.js").WebIcon var style = require("./lib/style.js"); +var StyledImage = style.StyledImage /** * The main extension store widget class @@ -41,17 +42,15 @@ function StoreUI(){ this.storeFrame.hide(); this.setUpdateProgressUIState(false); - if (!preferences.getBool("HUES_EULA_ACCEPTED", "")) { + if (!this.localList.getData("HUES_EULA_ACCEPTED", false)) { this.aboutFrame.hide(); // EULA logo - var eulaLogo = storelib.appFolder + "/resources/logo.png" - eulaLogo = style.getImage(eulaLogo); - var eulaLogoPixmap = new QPixmap(eulaLogo); - this.eulaFrame.innerFrame.eulaLogo.setPixmap(eulaLogoPixmap); + var eulaLogo = new StyledImage(storelib.appFolder + "/resources/logo.png", 600, 150) + this.eulaFrame.innerFrame.eulaLogo.setPixmap(eulaLogo.pixmap); this.eulaFrame.innerFrame.eulaCB.stateChanged.connect(this, function() { - preferences.setBool("HUES_EULA_ACCEPTED", true); + this.localList.saveData("HUES_EULA_ACCEPTED", true); this.eulaFrame.hide(); this.aboutFrame.show(); }); @@ -61,17 +60,13 @@ function StoreUI(){ } // About logo - var logo = storelib.appFolder+"/resources/logo.png"; - logo = style.getImage(logo); - var logoPixmap = new QPixmap(logo); - this.aboutFrame.storeLabel.setPixmap(logoPixmap); + var logo = new StyledImage(storelib.appFolder+"/resources/logo.png", 600, 150); + this.aboutFrame.storeLabel.setPixmap(logo.pixmap); // Header logo - var headerLogo = storelib.appFolder+"/resources/logo_header.png"; - headerLogo = style.getImage(headerLogo); - var headerLogoPixmap = new QPixmap(headerLogo); - this.storeHeader.headerLogo.setPixmap(headerLogoPixmap); - + var headerLogo = new StyledImage(storelib.appFolder+"/resources/icon.png", 24, 24); + this.storeHeader.headerLogo.setPixmap(headerLogo.pixmap); + this.checkForUpdates() // connect UI signals @@ -84,9 +79,13 @@ function StoreUI(){ this.storeHeader.showInstalledCheckbox.toggled.connect(this, this.updateExtensionsList) // Clear search button ---------------------------------------------- - var clearSearchIcon = storelib.appFolder+"/resources/cancel_icon.png"; - clearSearchIcon = style.getImage(clearSearchIcon); - UiLoader.setSvgIcon(this.storeHeader.storeClearSearch, clearSearchIcon); + var clearSearchIcon = new StyledImage(storelib.appFolder+"/resources/cancel_icon.png"); + clearSearchIcon.setAsIcon(this.storeHeader.storeClearSearch) + + this.storeHeader.storeClearSearch.clicked.connect(this, function () { + this.storeHeader.searchStore.text = ""; + }) + this.storeHeader.storeClearSearch.clicked.connect(this, function () { this.storeHeader.searchStore.text = ""; }) @@ -239,14 +238,15 @@ StoreUI.prototype.getInstalledVersion = function(){ StoreUI.prototype.checkForUpdates = function(){ var updateRibbon = this.updateRibbon - var defaultRibbonStyleSheet = "QWidget { background-color: " + style.COLORS["03DP"] + "; color: white; bottom-right-radius: 10px; bottom-left-radius: 10px }"; - var updateRibbonStyleSheet = "QWidget { background-color: " + style.COLORS.YELLOW + "; color: black }"; + var defaultRibbonStyleSheet = style.STYLESHEETS.defaultRibbonStyleSheet; + var updateRibbonStyleSheet = style.STYLESHEETS.updateRibbonStyleSheet; var storeUi = this; try{ var storeExtension = this.storeExtension; var storeVersion = storeExtension.version; var currentVersion = this.getInstalledVersion(); + this.storeFooter.storeVersionLabel.setText("v" + currentVersion ); // if a more recent version of the store exists on the repo, activate the update ribbon if (!storeExtension.currentVersionIsOlder(currentVersion) && (currentVersion != storeVersion)) { @@ -258,9 +258,7 @@ StoreUI.prototype.checkForUpdates = function(){ this.aboutFrame.updateButton.hide(); updateRibbon.storeVersion.setText("v" + currentVersion + " ✓ - Store is up to date."); updateRibbon.setStyleSheet(defaultRibbonStyleSheet); - this.storeFooter.storeVersionLabel.setText("v" + currentVersion ); } - }catch(err){ // couldn't check updates, probably we don't have an internet access. // We set up an error message and disable load button. @@ -275,7 +273,7 @@ StoreUI.prototype.checkForUpdates = function(){ * @param {*} message */ StoreUI.prototype.lockStore = function(message){ - var noConnexionRibbonStyleSheet = "QWidget { background-color: " + style.COLORS.RED + "; color: white; }"; + var noConnexionRibbonStyleSheet = style.STYLESHEETS.noConnexionRibbonStyleSheet; this.ui.aboutFrame.loadStoreButton.enabled = false; this.ui.aboutFrame.updateButton.hide(); @@ -378,34 +376,33 @@ StoreUI.prototype.updateDescriptionPanel = function(extension) { websiteIcon.setToWidget(this.storeDescriptionPanel.websiteButton) // for some reason, this url is the only one that actually returns an icon - var githubIcon = new WebIcon("https://avatars.githubusercontent.com/u") - githubIcon.setToWidget(this.storeDescriptionPanel.sourceButton) + var githubIcon = new StyledImage(style.ICONS.github) + githubIcon.setAsIcon(this.storeDescriptionPanel.sourceButton) // update install button to reflect whether or not the extension is already installed if (this.localList.isInstalled(extension)) { var localExtension = this.localList.extensions[extension.id]; if (!localExtension.currentVersionIsOlder(extension.version) && this.localList.checkFiles(extension)) { // Extension installed and up-to-date. - this.storeDescriptionPanel.installButton.setStyleSheet("QToolButton { border-color: transparent transparent " + style.COLORS.ORANGE + " transparent; }"); + this.storeDescriptionPanel.installButton.setStyleSheet(style.STYLESHEETS.uninstallButton); this.storeDescriptionPanel.installButton.removeAction(this.installAction); this.storeDescriptionPanel.installButton.removeAction(this.updateAction); this.storeDescriptionPanel.installButton.setDefaultAction(this.uninstallAction); } else { // Extension installed and update available. - this.storeDescriptionPanel.installButton.setStyleSheet("QToolButton { border-color: transparent transparent " + style.COLORS.YELLOW + " transparent; }"); + this.storeDescriptionPanel.installButton.setStyleSheet(style.STYLESHEETS.updateButton); this.storeDescriptionPanel.installButton.removeAction(this.installAction); this.storeDescriptionPanel.installButton.removeAction(this.uninstallAction); this.storeDescriptionPanel.installButton.setDefaultAction(this.updateAction); } } else { // Extension not installed. - this.storeDescriptionPanel.installButton.setStyleSheet("QToolButton { border-color: transparent transparent " + style.COLORS.GREEN + " transparent; }"); + this.storeDescriptionPanel.installButton.setStyleSheet(style.STYLESHEETS.installButton); this.storeDescriptionPanel.installButton.removeAction(this.uninstallAction); this.storeDescriptionPanel.installButton.removeAction(this.updateAction); this.storeDescriptionPanel.installButton.setDefaultAction(this.installAction); } this.storeDescriptionPanel.installButton.enabled = (extension.package.files.length > 0) - } @@ -516,21 +513,22 @@ DescriptionView.prototype = Object.create(QWebView.prototype) QTreeWidgetItem.call(this, [extensionLabel, icon], 1024); // add an icon in the middle column showing if installed and if update present if (localList.isInstalled(extension)) { - var icon = style.ICONS["installed"]; + var iconPath = style.ICONS.installed; this.setToolTip(1, "Extension is installed correctly."); var localExtension = localList.extensions[extension.id]; // log.debug("checking files from "+extension.id, localList.checkFiles(localExtension)); if (localExtension.currentVersionIsOlder(extension.version)) { - icon = style.ICONS["update"]; + iconPath = style.ICONS.update; this.setToolTip(1, "Update available:\ncurrently installed version : v" + extension.version); } else if (!localList.checkFiles(localExtension)) { - icon = style.ICONS["error"]; + iconPath = style.ICONS.error; this.setToolTip(1, "Some files from this extension are missing."); } } else { - icon = style.ICONS["not installed"]; + iconPath = style.ICONS.notInstalled; } - this.setIcon(1, new QIcon(icon)); + var icon = new StyledImage(iconPath); + icon.setAsIcon(this, 1); if (extension.iconUrl){ // set up an icon if one is available @@ -540,9 +538,8 @@ DescriptionView.prototype = Object.create(QWebView.prototype) }else{ // fallback to local icon - var extensionIcon = storelib.appFolder + "/resources/default_extension_icon.png"; - extensionIcon = style.getImage(extensionIcon); - this.setIcon(0, new QIcon(extensionIcon)); + var extensionIcon = new StyledImage(storelib.appFolder + "/resources/default_extension_icon.png"); + extensionIcon.setAsIcon(this, 0); } // store the extension id in the item diff --git a/ExtensionStore/lib/style.js b/ExtensionStore/lib/style.js index 002202d..03b7527 100644 --- a/ExtensionStore/lib/style.js +++ b/ExtensionStore/lib/style.js @@ -34,12 +34,22 @@ const ColorsLight = ColorsDark; const COLORS = isDarkStyle() ? ColorsDark : ColorsLight; +const STYLESHEETS = { + defaultRibbonStyleSheet : "QWidget { background-color: " + COLORS["03DP"] + "; color: white; bottom-right-radius: 10px; bottom-left-radius: 10px }", + updateRibbonStyleSheet : "QWidget { background-color: " + COLORS.YELLOW + "; color: black }", + noConnexionRibbonStyleSheet : "QWidget { background-color: " + COLORS.RED + "; color: white; }", + installButton : "QToolButton { border-color: transparent transparent " + COLORS.GREEN + " transparent; }", + uninstallButton : "QToolButton { border-color: transparent transparent " + COLORS.ORANGE + " transparent; }", + updateButton : "QToolButton { border-color: transparent transparent " + COLORS.YELLOW + " transparent; }" +} + var iconFolder = appFolder + "/resources"; const ICONS = { "installed": getImage(iconFolder + "/installed_icon.png"), "update": getImage(iconFolder + "/update_icon.png"), "error": getImage(iconFolder + "/error_icon.png"), - "not installed": getImage(iconFolder + "/not_installed_icon.png"), + "notInstalled": getImage(iconFolder + "/not_installed_icon.png"), + "github": getImage(iconFolder + "/GitHub-Mark-Light-32px.png"), } /** @@ -101,7 +111,58 @@ function getImage(imagePath) { return imagePath; } + +function StyledImage(imagePath, width, height, uniformScaling) { + if (typeof uniformScaling === 'undefined') var uniformScaling = true; + if (typeof width === 'undefined') var width = 0; + if (typeof height === 'undefined') var height = 0; + + this.width = UiLoader.dpiScale(width); + this.height = UiLoader.dpiScale(height); + + this.uniformScaling = uniformScaling; + this.basePath = imagePath; + this.getImage = getImage; +} + + +Object.defineProperty(StyledImage.prototype, "path", { + get: function(){ + return this.getImage(this.basePath); + } +}) + + +Object.defineProperty(StyledImage.prototype, "pixmap", { + get: function(){ + if (typeof this._pixmap === 'undefined') { + log.debug("new pixmap for image : " + this.path) + var pixmap = new QPixmap(this.path); + var aspectRatioFlag = this.uniformScaling?Qt.KeepAspectRatio:Qt.IgnoreAspectRatio; + var pixmap = pixmap.scaled(this.width, this.height, aspectRatioFlag, Qt.SmoothTransformation); + + this._pixmap = pixmap + } + return this._pixmap + } +}) + + +StyledImage.prototype.setAsIcon = function(widget, itemColumn){ + if (widget instanceof QTreeWidgetItem){ + if (typeof itemColumn === 'undefined') var itemColumn = 0; + var icon = new QIcon(this.path); + widget.setIcon(itemColumn, icon); + }else{ + log.debug("setting icon "+this.path) + UiLoader.setSvgIcon(widget, this.path); + } +} + + exports.ICONS = ICONS; exports.COLORS = COLORS; exports.getImage = getImage; -exports.getSyleSheet = getSyleSheet; \ No newline at end of file +exports.getSyleSheet = getSyleSheet; +exports.StyledImage = StyledImage; +exports.STYLESHEETS = STYLESHEETS; \ No newline at end of file From a320ae9ee1d6368c7704e7ed9a6b4c053631a604 Mon Sep 17 00:00:00 2001 From: MathieuC Date: Fri, 28 May 2021 17:17:27 +0200 Subject: [PATCH 033/112] auto toggle of clear search button --- ExtensionStore/app.js | 18 ++++++++++++------ ExtensionStore/lib/style.js | 21 ++++++++++++--------- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/ExtensionStore/app.js b/ExtensionStore/app.js index 9cc722e..bd75441 100644 --- a/ExtensionStore/app.js +++ b/ExtensionStore/app.js @@ -82,9 +82,16 @@ function StoreUI(){ var clearSearchIcon = new StyledImage(storelib.appFolder+"/resources/cancel_icon.png"); clearSearchIcon.setAsIcon(this.storeHeader.storeClearSearch) - this.storeHeader.storeClearSearch.clicked.connect(this, function () { - this.storeHeader.searchStore.text = ""; + var searchField = this.storeHeader.searchStore + var searchClear = this.storeHeader.storeClearSearch + var searchFieldSize = searchField.maximumWidth + + searchField.textChanged.connect(this, function () { + var visible = !!searchField.text; + searchClear.visible = visible; + searchField.maximumWidth = searchFieldSize - searchClear.width * visible; }) + searchClear.hide() this.storeHeader.storeClearSearch.clicked.connect(this, function () { this.storeHeader.searchStore.text = ""; @@ -238,8 +245,8 @@ StoreUI.prototype.getInstalledVersion = function(){ StoreUI.prototype.checkForUpdates = function(){ var updateRibbon = this.updateRibbon - var defaultRibbonStyleSheet = style.STYLESHEETS.defaultRibbonStyleSheet; - var updateRibbonStyleSheet = style.STYLESHEETS.updateRibbonStyleSheet; + var defaultRibbonStyleSheet = style.STYLESHEETS.defaultRibbon; + var updateRibbonStyleSheet = style.STYLESHEETS.updateRibbon; var storeUi = this; try{ @@ -273,7 +280,7 @@ StoreUI.prototype.checkForUpdates = function(){ * @param {*} message */ StoreUI.prototype.lockStore = function(message){ - var noConnexionRibbonStyleSheet = style.STYLESHEETS.noConnexionRibbonStyleSheet; + var noConnexionRibbonStyleSheet = style.STYLESHEETS.noConnexionRibbon; this.ui.aboutFrame.loadStoreButton.enabled = false; this.ui.aboutFrame.updateButton.hide(); @@ -375,7 +382,6 @@ StoreUI.prototype.updateDescriptionPanel = function(extension) { var websiteIcon = new WebIcon(extension.package.website) websiteIcon.setToWidget(this.storeDescriptionPanel.websiteButton) - // for some reason, this url is the only one that actually returns an icon var githubIcon = new StyledImage(style.ICONS.github) githubIcon.setAsIcon(this.storeDescriptionPanel.sourceButton) diff --git a/ExtensionStore/lib/style.js b/ExtensionStore/lib/style.js index 03b7527..15c0698 100644 --- a/ExtensionStore/lib/style.js +++ b/ExtensionStore/lib/style.js @@ -34,15 +34,20 @@ const ColorsLight = ColorsDark; const COLORS = isDarkStyle() ? ColorsDark : ColorsLight; -const STYLESHEETS = { - defaultRibbonStyleSheet : "QWidget { background-color: " + COLORS["03DP"] + "; color: white; bottom-right-radius: 10px; bottom-left-radius: 10px }", - updateRibbonStyleSheet : "QWidget { background-color: " + COLORS.YELLOW + "; color: black }", - noConnexionRibbonStyleSheet : "QWidget { background-color: " + COLORS.RED + "; color: white; }", +const styleSheetsDark = { + defaultRibbon : "QWidget { background-color: " + COLORS["03DP"] + "; color: white; bottom-right-radius: 10px; bottom-left-radius: 10px }", + updateRibbon : "QWidget { background-color: " + COLORS.YELLOW + "; color: black }", + noConnexionRibbon : "QWidget { background-color: " + COLORS.RED + "; color: white; }", installButton : "QToolButton { border-color: transparent transparent " + COLORS.GREEN + " transparent; }", uninstallButton : "QToolButton { border-color: transparent transparent " + COLORS.ORANGE + " transparent; }", updateButton : "QToolButton { border-color: transparent transparent " + COLORS.YELLOW + " transparent; }" } +const styleSheetsLight = styleSheetsDark; + +const STYLESHEETS = isDarkStyle() ? styleSheetsDark : styleSheetsLight; + + var iconFolder = appFolder + "/resources"; const ICONS = { "installed": getImage(iconFolder + "/installed_icon.png"), @@ -159,10 +164,8 @@ StyledImage.prototype.setAsIcon = function(widget, itemColumn){ } } - -exports.ICONS = ICONS; -exports.COLORS = COLORS; -exports.getImage = getImage; exports.getSyleSheet = getSyleSheet; exports.StyledImage = StyledImage; -exports.STYLESHEETS = STYLESHEETS; \ No newline at end of file +exports.STYLESHEETS = STYLESHEETS; +exports.ICONS = ICONS; +exports.COLORS = COLORS; \ No newline at end of file From 3abd81bc772a9a8a05a730c9d2fec29493200e6a Mon Sep 17 00:00:00 2001 From: bob-ross27 <55351437+bob-ross27@users.noreply.github.com> Date: Fri, 28 May 2021 12:56:31 -0300 Subject: [PATCH 034/112] Reworked EULA and About pages. --- ExtensionStore/app.js | 34 +- ExtensionStore/resources/discord_logo.png | Bin 0 -> 1559 bytes ExtensionStore/resources/github_logo.png | Bin 0 -> 1571 bytes ExtensionStore/resources/store.ui | 430 +++++++++++++------ ExtensionStore/resources/stylesheet_dark.qss | 83 ++-- ExtensionStore/resources/twitter_logo.png | Bin 0 -> 18385 bytes 6 files changed, 382 insertions(+), 165 deletions(-) create mode 100644 ExtensionStore/resources/discord_logo.png create mode 100644 ExtensionStore/resources/github_logo.png create mode 100644 ExtensionStore/resources/twitter_logo.png diff --git a/ExtensionStore/app.js b/ExtensionStore/app.js index 9b87e25..493fdb5 100644 --- a/ExtensionStore/app.js +++ b/ExtensionStore/app.js @@ -66,6 +66,38 @@ function StoreUI(){ var logoPixmap = new QPixmap(logo); this.aboutFrame.storeLabel.setPixmap(logoPixmap); + // Social media buttons + socialIconSize = UiLoader.dpiScale(24); + + // Twitter + var twitterLogo = storelib.appFolder + "/resources/twitter_logo.png"; + twitterIcon = new QIcon(style.getImage(twitterLogo)); + twitterIcon.size = new QSize(socialIconSize, socialIconSize); + this.aboutFrame.twitterButton.icon = twitterIcon; + this.aboutFrame.twitterButton.clicked.connect(this, function () { + QDesktopServices.openUrl(new QUrl(this.aboutFrame.twitterButton.toolTip)); + }); + + + // // Github + var githubLogo = storelib.appFolder + "/resources/github_logo.png"; + githubIcon = new QIcon(style.getImage(githubLogo)); + githubIcon.size = new QSize(socialIconSize, socialIconSize); + this.aboutFrame.githubButton.icon = githubIcon; + this.aboutFrame.githubButton.clicked.connect(this, function () { + QDesktopServices.openUrl(new QUrl(this.aboutFrame.githubButton.toolTip)); + }); + + + // // Discord + var discordLogo = storelib.appFolder + "/resources/discord_logo.png"; + discordIcon = new QIcon(style.getImage(discordLogo)); + discordIcon.size = new QSize(socialIconSize, socialIconSize); + this.aboutFrame.discordButton.icon = discordIcon; + this.aboutFrame.discordButton.clicked.connect(this, function () { + QDesktopServices.openUrl(new QUrl(this.aboutFrame.discordButton.toolTip)); + }); + // Header logo var headerLogo = storelib.appFolder+"/resources/logo_header.png"; headerLogo = style.getImage(headerLogo); @@ -239,7 +271,7 @@ StoreUI.prototype.getInstalledVersion = function(){ StoreUI.prototype.checkForUpdates = function(){ var updateRibbon = this.updateRibbon - var defaultRibbonStyleSheet = "QWidget { background-color: " + style.COLORS["03DP"] + "; color: white; bottom-right-radius: 10px; bottom-left-radius: 10px }"; + var defaultRibbonStyleSheet = "QWidget { background-color: transparent; color: gray;}"; var updateRibbonStyleSheet = "QWidget { background-color: " + style.COLORS.YELLOW + "; color: black }"; var storeUi = this; diff --git a/ExtensionStore/resources/discord_logo.png b/ExtensionStore/resources/discord_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..d5346b794c9a35d70a1299fc1362b245b526d605 GIT binary patch literal 1559 zcmb7^c`zFY6vm?mEp@D;Y)ESy>s}(Fh?^u-1rbRbY1mfRP)8j}Bc+rEt-6bC5Up$D zu6s+H*3qEOkaS(GA}w1~XdNY6YjeIGo_@XosPQti1X?x5`L5adtQt=DHQz7~2PIaKWlH_QWZDVz@;u@@&UUT>tAP z>r61#sRV3RtRtW6MKC6(vO8r{j_yJga`7hPTJ&?_5}Vq{+`0h%!s5#sI+qMH&v-B9CH)3pM(=stUjHDuF zNsnKU{%j;tWl6-I$A%LvPrY?ur_mqq_|Pjmko zFr`naT{yaH^hR&Kwv_z2Uj=)saV(D7<%jb#BDFe_z_IiNa0#@r+ooJOL#WBk1pwq< zN`7P~hJA?vi5RT&!1J8NC4Qg@;^)wSKc#P9*{~nZb0nw!yu`qxU_<=WD(^5B_Cu)mY#2W+RG_auhi||T|pL(j@ zNga&Lyo#3bW>rtu@n(HA=wSq^c+z(v%a+vfAu8(qckPP&3bAG4@n0EXws5+|Y#bsW zuv0{`lM9j3d4bka8P#@iH7m8dOJ&=^o!{R~z-v^9sbG$mrkUv*B7?)u+NDjtd6=ct z+>?~(R|CBn$j0Z3MkjYO{V|Z?snJzeznFR7Xph@eM@@0zz*F__XDltI)Zm3vJx5$5 z^)6R8Xz9E!4-TOfwk{uWPM!?D2Vn@%6HQwB#$58)Qzal7!4R-i9jt=!M`QX2l-F1u ziF85Bh*9P`JBOX@zMBfeO@%knPOEXHRxg{E*L%Co6(4GT#UvX9CTtDLas#AE?A_`^ z%@Y*U$4#p4eNOJ7AW=i8@JsyZb8&6=N5>l;ta~Hr^7qNg()9EFD60~zxev&cp-4|m znU>;Be-2E+V}L(oy)wfK!dVu$$9(mjBlB+jN6y4Cg3l9|NEavy*X~{A6)g7YvN5H?3A12a?3s|iM$cv< z=8PXW_?eGh;yMisMb9kbnkXADLdduQJ$y#n_52I#0|eQadD`>Vq?H64@4<+TYeZ|M zpfq$fqZ?s;ey~4DuCu`}C;&t${$)opCxTZXC|{<~luh7z_nv4@T-`E*7TE4Y2{lf= zT*E5~$}?$9y`VE`v2SxZGDLMqV(+lfm@wO?Cq-FV>DA&4ZpLb6FYk%}xPEf5f=aW` zpo3;(FcvW|L*((YTH=a$A*%~QjS8(FbSMh7cFnPh*Vv$!olsBTKK9u3Q2pxUHCW`f ztbgh4bGg)D25V0fm1I-Q=clL6JE&Po%m^v3Gv4*ziSmkvKB7jH&py;rhUH15UBapC zU94SH%K3_3QtLPP{*CugD<;8hs0YHOxeay8aUn(1PHPm>Ty})58NoH0+(b()YDjhi zbaDc_v=jlpmS-h<7Oe{{gM{Q4@W|^UwcT> zSDB)n20W$Ee{2)`Ho^y{=yO=59b`VuVC1VL4c+viioKNYQS;E>k7|H39OYPR?;HOY DGqDaI literal 0 HcmV?d00001 diff --git a/ExtensionStore/resources/github_logo.png b/ExtensionStore/resources/github_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..628da97c70890c73e59204f5b140c4e67671e92d GIT binary patch literal 1571 zcmaJ>c~BE~6izDPQq)#Nu*KOf(n^(VHY9;fiINM65``pc+9*v(mL$bwfCjbc%v9V{8r9iX|O%>Nr%pLD2qT{mty}c=LVleeamv znz3SOSm@kP8jThvOOq(56Yzh*fz(booe!uZij=BJC6+_lbvQ~B8nA2>kXdv_RDtRY z`5QXWWEySCe6vbTs^#f?J!WC*{1~RgVx!nJTJjQyO{dRANgx|FnymtGbD9%JmCh9^y)##j7{Dcqfn*1ta$rG89pJF6w-S7Z037$rr|y0;1Onp_ zGFJdT6Q!1C0AdVB0WOmpuV=AgAQ550Tn+-mivTtYPJmz*#75#_n9oV%!#rSOfmAfy zki%C~=fTp1{O#BLpJ|0jj#m6#|LRWit-vq3PE1z9ZqyvET4sX$-Icqy7t z<=aq5ff86AuBZBu6EjJsYWM0uejufWFTwPA7Su}0Bm$7KFb!q{Um_8~A{LUG#1l(l zSehUda@kU8LIRg9fkk2tZ;~ss5~R+mM<==F7hLHpxqLB>>PQS%Vc7b~?q!%T5+h8Q z4G=4Nzyi5WZ?^gkasJ{?Xhm`JC#WG6$1K2jb@=9&D3EgD#3UhGh#*21rJjulVXjCF zvp76q62jt0zzMG5C7DlfMgPl%C^3+~wf|}Lq=}jz|MmIcQjh1Ok6NjD$Em^Iv26D> z8tt_TnM9~^Tt8mflRGPOrrX|HtT3gG4LEuuk{g2Rn}QgJIa?gZo))!!=o_l9bvD%A zZ`aHajl8#~u?!4f7F#*b*->A=R2L)6!>saz?h>#wTXT-I(XmQ zx{84skS>k=i~i`(6k4C7;Zpfx%dCPVjPayMf8pugtGM=~s=Id1l#8MZJ1-73wV#Q3 zR3>v3%}jbQs1f_Z0xo;%=LILlA+nTpKI4ha%xWW}uqHrNao~&T4AY6m`P$_n-6h*g zhoX+e4n%~gl_lhe#s+AMb7d{5WzvYTa%6Q~si@@4{;s(0zU|H&P3fE+t{7X`S#Cj@ zC#vd}^4pcBD*77Ny5=j$h8EL2_t$O38$SQiJ6fPjJMimypr~MB2(&P0aI|h}$64<0 z>_~duqNjaT=DM^6+N{&B_lED;F2wrl?!4Lk*2((x!fmrcsw+=cI^qttuZ9C}-m~5E z-ryYVpL%^xR#&(0YI5hz<(}F7-p)?FPcyJO-zVO>%9ZDXJH8pnY;GJYFDQ>vd#j_* zRrd}L(r=!g+1#nQwsO?kpS`Qq8`NxE+Zy{gf7*_7J*U2V_|NpLo{iasj7VCg_V9&| ShohtYzipXxh2)4xTk 0 0 - 862 - 1008 + 546 + 1060 + + + 70 + 50 + + Form @@ -101,22 +107,25 @@ 0 0 - 773 - 120 + 474 + 201 - <html><head/><body><p>Sed voluptas magnam quasi ipsa eaque occaecati voluptatum fugiat. Repudiandae odio commodi sed et cum nam quisquam. Nulla velit hic assumenda omnis omnis. Sint deleniti tempore atque a eveniet voluptas ab qui. </p><p>Tenetur autem earum iure blanditiis eum sit perferendis. Et qui tempore eum quidem reiciendis sint laudantium qui. Delectus nisi quia voluptas. Rerum adipisci voluptas facilis. Voluptate at quia nisi sequi labore. Ea optio necessitatibus ut sit. </p><p>Quod animi aut sapiente. Sed aut perspiciatis nesciunt minima voluptatem. Et dolorum itaque consectetur cumque perspiciatis necessitatibus. Aut quos ipsa amet. Et id in ut et omnis. Nostrum iste vitae sed cupiditate quia. </p></body></html> + <html><head/><body><p>This extension store is made available for free by enthusiast script makers and users, and is not endorsed or curated by Toon Boom. Extensions available for download on this store are to be used at your own risk.</p><p>The extensions available on this store are open source and are automatically downloaded from verified github accounts. While efforts are made to vet sellers, it is your responsibility to read and understand the source code before you install any extension.</p><p>Questions, concerns or issues can be directed towards the creator using the provided github or website address. We cannot offer support for extensions not working correctly.</p><p>The source for this extension, including a list of all registered sellers, can be viewed here: <br/><a href="https://github.com/mchaptel/ExtensionStore"><span style=" text-decoration: underline; color:#c8c8c8;">https://github.com/mchaptel/ExtensionStore</span></a></p></body></html> - Qt::AlignJustify|Qt::AlignTop + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop true + + true + @@ -801,13 +810,13 @@ 0 - 0 + 5 0 - 0 + 5 0 @@ -817,106 +826,80 @@ 0 + + QLayout::SetDefaultConstraint + - 25 + 0 - 25 + 0 - 25 + 0 9 - - - - - 0 - 1 - - - - <html><head/><body><p><span style=" font-size:11pt; font-weight:600;">Harmony Unofficial Extension Store</span></p></body></html> - - - Qt::RichText - - - Qt::AlignCenter - - - Qt::Vertical - QSizePolicy::Fixed + QSizePolicy::Expanding 20 - 20 + 40 - - - - - - QFrame::NoFrame - - - QFrame::Plain - - - true - - - - - 0 - 0 - 793 - 240 - - - - - 0 - - - 0 + + + + + + 0 + 1 + - - 0 + + <html><head/><body><p><span style=" font-size:11pt; font-weight:600;">Harmony Unofficial Extension Store</span></p></body></html> - - 3 + + Qt::RichText - - 0 + + Qt::AlignCenter + + + + - + 0 0 - 500 + 70 0 + + + 425 + 70 + + false @@ -924,7 +907,7 @@ - <html><head/><body><p>This extension store is made available for free by entusiast script makers and users and is not endorsed or curated by Toonboom. Use the extensions available for download on this store at your own risk.</p><p>The extensions available on this store are open source and sourced from checked github accounts. Make sure you read the source code and understand it before you install any extension.</p><p>If you have a question about a script, you can use the provided github or website address to contact the script author. We cannot offer support for extensions not working correctly.</p><p>If you want to register as a script creator and want to share your scripts through this extension, get in touch and we will add your github account to the list of content makers.</p><p>The source for this extension can be viewed here: </p><p><a href="https://github.com/mchaptel/ExtensionStore/tree/master/packages/ExtensionStore"><span style=" text-decoration: underline; color:#55aaff;">https://github.com/mchaptel/ExtensionStore</span></a></p><p><a href="https://github.com/mchaptel/ExtensionStore/blob/master/SELLERSLIST"><span style=" text-decoration: underline; color:#55aaff;">The list of extension makers is available here.</span></a></p><p><a href="https://twitter.com/mathieuchaptel"><span style=" text-decoration: underline; color:#55aaff;">For news about the development and to give feedback, follow us on twitter!</span></a></p></body></html> + <html><head/><body><p align="center"><span style=" font-size:12pt;">Bringing together enthusiast script makers and users using the power of open source.</span></p></body></html> Qt::AutoText @@ -936,13 +919,160 @@ true + false + + + + + + + + + 0 + + + 0 + + + + + Qt::Horizontal + + + QSizePolicy::Expanding + + + + 40 + 20 + + + + + + + + https://twitter.com/mathieuchaptel + + + twitter + + + + 24 + 24 + + + + true + + + + + + + Qt::Horizontal + + + QSizePolicy::Maximum + + + + 15 + 20 + + + + + + + + https://github.com/mchaptel/ExtensionStore + + + github + + + + 24 + 24 + + + + false + + + + + + + Qt::Horizontal + + + QSizePolicy::Maximum + + + + 15 + 20 + + + + + + + + https://discord.gg/ETNmCWYN + + + discord + + + + 24 + 24 + + + true + + + + Qt::Horizontal + + + QSizePolicy::Expanding + + + + 40 + 20 + + + + - - + + + + + Qt::Vertical + + + QSizePolicy::Maximum + + + + 20 + 15 + + + + + @@ -955,91 +1085,79 @@ 0 - 37 - - - - - - - - Qt::Vertical - - - QSizePolicy::Minimum - - - - 20 - 6 + 0 - + - - - + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 30 + + + + + + + + + + + 0 + 0 + + - 80 + 120 30 - 150 + 120 16777215 - Load Store + Install Update - - - - - 0 - 0 - - + + + + + + - 80 + 120 30 - 200 + 120 16777215 - Install Update + Load Store - - - - Qt::Vertical - - - QSizePolicy::Fixed - - - - 0 - 5 - - - - @@ -1056,20 +1174,56 @@ - - - - 16777215 - 5 - - - - 0 - - - -1 - - + + + + + Qt::Horizontal + + + + 20 + 20 + + + + + + + + + 0 + 0 + + + + + 16777215 + 5 + + + + 0 + + + -1 + + + + + + + Qt::Horizontal + + + + 20 + 20 + + + + + @@ -1079,12 +1233,12 @@ Qt::Vertical - QSizePolicy::Maximum + QSizePolicy::Expanding 20 - 40 + 0 diff --git a/ExtensionStore/resources/stylesheet_dark.qss b/ExtensionStore/resources/stylesheet_dark.qss index ab41d06..c97d6cb 100644 --- a/ExtensionStore/resources/stylesheet_dark.qss +++ b/ExtensionStore/resources/stylesheet_dark.qss @@ -30,24 +30,6 @@ QProgressBar::chunk { background-color: @ACCENT_PRIMARY; } -/* Push Buttons */ -QPushButton { - background: @08DP; - border-radius: 7px; - selection-color: @ACCENT_PRIMARY; -} -QPushButton:hover { - border-color: @ACCENT_LIGHT; - border-width: 2px; - background: @12DP; -} -QPushButton:pressed { - background: @02DP; - border-color: @ACCENT_PRIMARY; - border-width: 2px; -} - - /* Scrollbars */ QScrollBar { image: none; @@ -139,13 +121,13 @@ QLabel#eulaText { background-color: transparent; margin: 5px; font-family: Arial; - font-size: 10pt; + font-size: 11pt; } /* EULA checkbox */ QCheckBox#eulaCB { font-family: Arial; - font-size: 12pt; + font-size: 13pt; border-color: @ACCENT_PRIMARY; background-color: transparent; } @@ -156,20 +138,69 @@ About Frame ====================== */ - /* About Screen text */ QLabel#label_3 { background-color: transparent; - margin: 5px; font-family: Arial; - font-size: 10pt; + font-size: 12pt; + margin-top: 15px; } -/* About screen scrollable region (text background). */ -QWidget#scrollAreaWidgetContents { - background-color: @02DP; +/* Social Media */ + +/* Twitter */ +QToolButton#twitterButton { + background-color: transparent; +} +QToolButton:hover#twitterButton { + border-width: 2px; + border-radius: 4px; + border-style: solid; + border-color: @ACCENT_LIGHT; +} + +/* Github */ +QToolButton#githubButton { + background-color: transparent; +} +QToolButton:hover#githubButton { + border-width: 2px; + border-radius: 4px; + border-style: solid; + border-color: @ACCENT_LIGHT; +} + +/* Discord */ +QToolButton#discordButton { + background-color: transparent; +} +QToolButton:hover#discordButton { + border-width: 2px; + border-radius: 4px; + border-style: solid; + border-color: @ACCENT_LIGHT; } + +/* Push Buttons */ +QPushButton#loadStoreButton { + background: @08DP; + border-width: 2px; + border-style: solid; + border-color: transparent transparent @ACCENT_LIGHT transparent; + border-radius: 7px; +} + +QPushButton:hover#loadStoreButton { + border-color: @ACCENT_LIGHT; + background: @12DP; +} +QPushButton:pressed#loadStoreButton { + background: @02DP; + border-color: @ACCENT_PRIMARY; +} + + /* Update Ribbon */ QLabel#storeVersion { font-family: Arial; diff --git a/ExtensionStore/resources/twitter_logo.png b/ExtensionStore/resources/twitter_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..66962e7d3c80a1a4cd8efec3c4653bb41be32a1b GIT binary patch literal 18385 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 S90Mi?q3zc8RuvX>=Klk?^^WrZ literal 0 HcmV?d00001 From 431bf4cb467b23692094a4341b3eb08262c7a939 Mon Sep 17 00:00:00 2001 From: bob-ross27 <55351437+bob-ross27@users.noreply.github.com> Date: Fri, 28 May 2021 13:38:44 -0300 Subject: [PATCH 035/112] Reworked to use new StyledImage class, moved icons into dedicated directory. --- ExtensionStore/app.js | 51 +++----- ExtensionStore/lib/style.js | 120 ++++++++++-------- .../resources/GitHub-Mark-Light-32px.png | Bin 1571 -> 0 bytes .../resources/{ => icons}/cancel_icon.png | Bin .../{ => icons}/default_extension_icon.png | Bin .../default_github_avatar_icon.png | Bin .../resources/{ => icons}/discord_logo.png | Bin .../resources/{ => icons}/error_icon.png | Bin .../resources/{ => icons}/github_logo.png | Bin .../resources/{ => icons}/globe_icon.png | Bin ExtensionStore/resources/{ => icons}/icon.png | Bin ExtensionStore/resources/{ => icons}/icon.svg | 0 .../resources/{ => icons}/installed_icon.png | Bin .../{ => icons}/magnifying_glass_icon.png | Bin .../{ => icons}/not_installed_icon.png | Bin .../resources/{ => icons}/twitter_logo.png | Bin .../resources/{ => icons}/update_icon.png | Bin ExtensionStore/resources/logo_header.png | Bin 2708 -> 0 bytes 18 files changed, 86 insertions(+), 85 deletions(-) delete mode 100644 ExtensionStore/resources/GitHub-Mark-Light-32px.png rename ExtensionStore/resources/{ => icons}/cancel_icon.png (100%) rename ExtensionStore/resources/{ => icons}/default_extension_icon.png (100%) rename ExtensionStore/resources/{ => icons}/default_github_avatar_icon.png (100%) rename ExtensionStore/resources/{ => icons}/discord_logo.png (100%) rename ExtensionStore/resources/{ => icons}/error_icon.png (100%) rename ExtensionStore/resources/{ => icons}/github_logo.png (100%) rename ExtensionStore/resources/{ => icons}/globe_icon.png (100%) rename ExtensionStore/resources/{ => icons}/icon.png (100%) rename ExtensionStore/resources/{ => icons}/icon.svg (100%) rename ExtensionStore/resources/{ => icons}/installed_icon.png (100%) rename ExtensionStore/resources/{ => icons}/magnifying_glass_icon.png (100%) rename ExtensionStore/resources/{ => icons}/not_installed_icon.png (100%) rename ExtensionStore/resources/{ => icons}/twitter_logo.png (100%) rename ExtensionStore/resources/{ => icons}/update_icon.png (100%) delete mode 100644 ExtensionStore/resources/logo_header.png diff --git a/ExtensionStore/app.js b/ExtensionStore/app.js index 918590b..cc32975 100644 --- a/ExtensionStore/app.js +++ b/ExtensionStore/app.js @@ -64,45 +64,36 @@ function StoreUI(){ this.aboutFrame.storeLabel.setPixmap(logo.pixmap); // Social media buttons - socialIconSize = UiLoader.dpiScale(24); - // Twitter - var twitterLogo = storelib.appFolder + "/resources/twitter_logo.png"; - twitterIcon = new QIcon(style.getImage(twitterLogo)); - twitterIcon.size = new QSize(socialIconSize, socialIconSize); - this.aboutFrame.twitterButton.icon = twitterIcon; - this.aboutFrame.twitterButton.clicked.connect(this, function () { - QDesktopServices.openUrl(new QUrl(this.aboutFrame.twitterButton.toolTip)); - }); + var twitterIcon = new StyledImage(style.ICONS.twitter); + twitterIcon.setAsIcon(this.aboutFrame.twitterButton); + // Github + var githubIcon = new StyledImage(style.ICONS.github); + githubIcon.setAsIcon(this.aboutFrame.githubButton); - // // Github - var githubLogo = storelib.appFolder + "/resources/github_logo.png"; - githubIcon = new QIcon(style.getImage(githubLogo)); - githubIcon.size = new QSize(socialIconSize, socialIconSize); - this.aboutFrame.githubButton.icon = githubIcon; - this.aboutFrame.githubButton.clicked.connect(this, function () { - QDesktopServices.openUrl(new QUrl(this.aboutFrame.githubButton.toolTip)); - }); - - - // // Discord - var discordLogo = storelib.appFolder + "/resources/discord_logo.png"; - discordIcon = new QIcon(style.getImage(discordLogo)); - discordIcon.size = new QSize(socialIconSize, socialIconSize); - this.aboutFrame.discordButton.icon = discordIcon; - this.aboutFrame.discordButton.clicked.connect(this, function () { - QDesktopServices.openUrl(new QUrl(this.aboutFrame.discordButton.toolTip)); - }); + // Discord + var discordIcon = new StyledImage(style.ICONS.discord); + discordIcon.setAsIcon(this.aboutFrame.discordButton); // Header logo - var headerLogo = new StyledImage(storelib.appFolder+"/resources/icon.png", 24, 24); + var headerLogo = new StyledImage(style.ICONS.headerLogo, 24, 24); this.storeHeader.headerLogo.setPixmap(headerLogo.pixmap); this.checkForUpdates() // connect UI signals this.aboutFrame.loadStoreButton.clicked.connect(this, this.loadStore) + // Social media UI signals + this.aboutFrame.twitterButton.clicked.connect(this, function () { + QDesktopServices.openUrl(new QUrl(this.aboutFrame.twitterButton.toolTip)); + }); + this.aboutFrame.discordButton.clicked.connect(this, function () { + QDesktopServices.openUrl(new QUrl(this.aboutFrame.discordButton.toolTip)); + }); + this.aboutFrame.githubButton.clicked.connect(this, function () { + QDesktopServices.openUrl(new QUrl(this.aboutFrame.githubButton.toolTip)); + }); // filter the store list -------------------------------------------- this.storeHeader.searchStore.textChanged.connect(this, this.updateExtensionsList) @@ -111,7 +102,7 @@ function StoreUI(){ this.storeHeader.showInstalledCheckbox.toggled.connect(this, this.updateExtensionsList) // Clear search button ---------------------------------------------- - var clearSearchIcon = new StyledImage(storelib.appFolder+"/resources/cancel_icon.png"); + var clearSearchIcon = new StyledImage(style.ICONS.cancelSearch); clearSearchIcon.setAsIcon(this.storeHeader.storeClearSearch) var searchField = this.storeHeader.searchStore @@ -576,7 +567,7 @@ DescriptionView.prototype = Object.create(QWebView.prototype) }else{ // fallback to local icon - var extensionIcon = new StyledImage(storelib.appFolder + "/resources/default_extension_icon.png"); + var extensionIcon = new StyledImage(style.ICONS.defaultExtension); extensionIcon.setAsIcon(this, 0); } diff --git a/ExtensionStore/lib/style.js b/ExtensionStore/lib/style.js index 15c0698..9ac9e34 100644 --- a/ExtensionStore/lib/style.js +++ b/ExtensionStore/lib/style.js @@ -7,25 +7,25 @@ var appFolder = require("./store.js").appFolder; // Enum to hold dark style palette. // 4% opacity over Material UI palette. const ColorsDark = { - "00DP": "#121115", - "01DP": "#1E1D21", - "02DP": "#232226", - "03DP": "#252428", - "04DP": "#29282C", - "06DP": "#2C2B2F", - "08DP": "#2E2D31", - "12DP": "#333236", - "16DP": "#363539", - "24DP": "#38373B", - "ACCENT_LIGHT": "#B6B1D8", // Lighter - 50% white screen overlaid. - "ACCENT_PRIMARY": "#4B3C9E", // Full intensity - "ACCENT_DARK": "#373061", // Subdued - 50% against D1 - "ACCENT_BG": "#2B283B", // Very subdued - 20% against D1 - "GREEN": "#30D158", // Valid. - "RED": "#FF453A", // Error - "YELLOW": "#FFD60A", // New or updated - "ORANGE": "#FF9F0A", // Notice. - "BLUE": "#A1CBEC", // Store update. + "00DP": "#121115", + "01DP": "#1E1D21", + "02DP": "#232226", + "03DP": "#252428", + "04DP": "#29282C", + "06DP": "#2C2B2F", + "08DP": "#2E2D31", + "12DP": "#333236", + "16DP": "#363539", + "24DP": "#38373B", + "ACCENT_LIGHT": "#B6B1D8", // Lighter - 50% white screen overlaid. + "ACCENT_PRIMARY": "#4B3C9E", // Full intensity + "ACCENT_DARK": "#373061", // Subdued - 50% against D1 + "ACCENT_BG": "#2B283B", // Very subdued - 20% against D1 + "GREEN": "#30D158", // Valid. + "RED": "#FF453A", // Error + "YELLOW": "#FFD60A", // New or updated + "ORANGE": "#FF9F0A", // Notice. + "BLUE": "#A1CBEC", // Store update. } // Enum to hold light style palette. @@ -35,7 +35,7 @@ const ColorsLight = ColorsDark; const COLORS = isDarkStyle() ? ColorsDark : ColorsLight; const styleSheetsDark = { - defaultRibbon : "QWidget { background-color: " + COLORS["03DP"] + "; color: white; bottom-right-radius: 10px; bottom-left-radius: 10px }", + defaultRibbon : "QWidget { background-color: transparent; color: gray;}", updateRibbon : "QWidget { background-color: " + COLORS.YELLOW + "; color: black }", noConnexionRibbon : "QWidget { background-color: " + COLORS.RED + "; color: white; }", installButton : "QToolButton { border-color: transparent transparent " + COLORS.GREEN + " transparent; }", @@ -48,13 +48,23 @@ const styleSheetsLight = styleSheetsDark; const STYLESHEETS = isDarkStyle() ? styleSheetsDark : styleSheetsLight; -var iconFolder = appFolder + "/resources"; +var iconFolder = appFolder + "/resources/icons"; const ICONS = { - "installed": getImage(iconFolder + "/installed_icon.png"), - "update": getImage(iconFolder + "/update_icon.png"), - "error": getImage(iconFolder + "/error_icon.png"), - "notInstalled": getImage(iconFolder + "/not_installed_icon.png"), - "github": getImage(iconFolder + "/GitHub-Mark-Light-32px.png"), + // Store + "headerLogo": getImage(iconFolder + "/icon.png"), + "cancelSearch": getImage(iconFolder + "/cancel_icon.png"), + // Extension install states + "installed": getImage(iconFolder + "/installed_icon.png"), + "update": getImage(iconFolder + "/update_icon.png"), + "error": getImage(iconFolder + "/error_icon.png"), + "notInstalled": getImage(iconFolder + "/not_installed_icon.png"), + // Store tree widget icons + "defaultExtension": getImage(iconFolder + "/default_extension_icon.png"), + "defaultGithubAvatar": getImage(iconFolder + "/default_github_avatar_icon.png"), + // Social media + "twitter": getImage(iconFolder + "/twitter_logo.png"), + "github": getImage(iconFolder + "/github_logo.png"), + "discord": getImage(iconFolder + "/discord_logo.png"), } /** @@ -62,7 +72,7 @@ const ICONS = { * @returns {Boolean} true if dark style active, false if light theme active. */ function isDarkStyle() { - return preferences.getBool("DARK_STYLE_SHEET", ""); + return preferences.getBool("DARK_STYLE_SHEET", ""); } @@ -71,23 +81,23 @@ function isDarkStyle() { * style-specific overrides. */ function getSyleSheet() { - var styleFile = storelib.appFolder + "/resources/stylesheet_dark.qss"; - var styleSheet = io.readFile(styleFile); + var styleFile = storelib.appFolder + "/resources/stylesheet_dark.qss"; + var styleSheet = io.readFile(styleFile); - // Get light-specific style overriddes - if (!isDarkStyle()) { - styleFileLight = storelib.appFolder + "/resources/stylesheet_light.qss"; - styleSheet += io.readFile(styleFileLight); - } + // Get light-specific style overriddes + if (!isDarkStyle()) { + styleFileLight = storelib.appFolder + "/resources/stylesheet_light.qss"; + styleSheet += io.readFile(styleFileLight); + } - // Replace template colors with final palettes. - for (color in COLORS) { - var colorRe = new RegExp("@" + color, "g"); - styleSheet = styleSheet.replace(colorRe, COLORS[color]); - } + // Replace template colors with final palettes. + for (color in COLORS) { + var colorRe = new RegExp("@" + color, "g"); + styleSheet = styleSheet.replace(colorRe, COLORS[color]); + } - log.debug("Final qss stylesheet:\n" + styleSheet); - return styleSheet; + log.debug("Final qss stylesheet:\n" + styleSheet); + return styleSheet; } @@ -98,22 +108,22 @@ function getSyleSheet() { */ function getImage(imagePath) { - // Images are default themed dark - just return the original image if dark style is active. - if (isDarkStyle()) { - return imagePath; - } + // Images are default themed dark - just return the original image if dark style is active. + if (isDarkStyle()) { + return imagePath; + } - // Harmony in light theme. Attempt to use @light variant. - var image = new QFileInfo(imagePath); - var imageRemapped = new QFileInfo(image.absolutePath() + "/" + image.baseName() + "@light." + image.suffix()); - if (imageRemapped.exists()) { - log.debug("Using light themed variant of of " + imagePath); - return imageRemapped.filePath(); - } + // Harmony in light theme. Attempt to use @light variant. + var image = new QFileInfo(imagePath); + var imageRemapped = new QFileInfo(image.absolutePath() + "/" + image.baseName() + "@light." + image.suffix()); + if (imageRemapped.exists()) { + log.debug("Using light themed variant of of " + imagePath); + return imageRemapped.filePath(); + } - // @light variant not found, fallback to using original image path. - log.debug("No light styled variant of image, using default."); - return imagePath; + // @light variant not found, fallback to using original image path. + log.debug("No light styled variant of image, using default."); + return imagePath; } diff --git a/ExtensionStore/resources/GitHub-Mark-Light-32px.png b/ExtensionStore/resources/GitHub-Mark-Light-32px.png deleted file mode 100644 index 628da97c70890c73e59204f5b140c4e67671e92d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1571 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 Ntc)Dffu$vN{{_ny#k~Lk From 72664a4da83298d13d7d5543734559f7df4c6103 Mon Sep 17 00:00:00 2001 From: MathieuC Date: Sat, 29 May 2021 21:38:56 +0200 Subject: [PATCH 036/112] remove install button to add it through code --- ExtensionStore/resources/store.ui | 78 ++++++++++---------- ExtensionStore/resources/stylesheet_dark.qss | 6 +- 2 files changed, 46 insertions(+), 38 deletions(-) diff --git a/ExtensionStore/resources/store.ui b/ExtensionStore/resources/store.ui index 11c5eb6..8c20219 100644 --- a/ExtensionStore/resources/store.ui +++ b/ExtensionStore/resources/store.ui @@ -73,6 +73,9 @@ Qt::AlignCenter + + installButtonPlaceHolder + @@ -108,7 +111,7 @@ 0 0 474 - 201 + 223 @@ -550,7 +553,7 @@ - + @@ -567,6 +570,30 @@ + + + + 0 + + + + 3 + + + 0 + + + 0 + + + 0 + + + 0 + + + + @@ -668,42 +695,19 @@ - - - 15 - - - 15 + + + 0 - - - - - 80 - 30 - - - - - 130 - 16777215 - - - - - - - Install - - - Qt::ToolButtonIconOnly - - - false - - - - + + + 15 + + + 15 + + + diff --git a/ExtensionStore/resources/stylesheet_dark.qss b/ExtensionStore/resources/stylesheet_dark.qss index c97d6cb..6421014 100644 --- a/ExtensionStore/resources/stylesheet_dark.qss +++ b/ExtensionStore/resources/stylesheet_dark.qss @@ -85,7 +85,7 @@ QLineEdit { } /* Tooltips */ -QToolTip { +QToolTip { color: lightGrey; background-color: @06DP; } @@ -344,6 +344,10 @@ QFrame#sidepanelFrame { background-color: @04DP; } +QFrame#installButtonPlaceHolder { + background-color: @04DP; +} + /* Splitter selection handle */ QSplitter::handle#storeSplitter { background-color: @04DP; From 5a696733e274880cfd0f04c324151c5bb266c3ef Mon Sep 17 00:00:00 2001 From: MathieuC Date: Sat, 29 May 2021 21:39:13 +0200 Subject: [PATCH 037/112] simplify error print in io.js --- ExtensionStore/lib/io.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ExtensionStore/lib/io.js b/ExtensionStore/lib/io.js index 9367d78..170b6e1 100644 --- a/ExtensionStore/lib/io.js +++ b/ExtensionStore/lib/io.js @@ -76,7 +76,7 @@ function recursiveFileCopy(folder, destination) { return output; } catch (err) { - log.error("error on line "+err.lineNumber+" of file "+err.fileName+": \n"+err); + log.error(err); return null; } } From b1df25e8791eba291ef863c5c0a35568465e5297 Mon Sep 17 00:00:00 2001 From: MathieuC Date: Sat, 29 May 2021 21:40:24 +0200 Subject: [PATCH 038/112] create Signal class, move widgets into widgets.js, set up install progress through Signals --- ExtensionStore/app.js | 199 ++++++++++++--------------------- ExtensionStore/lib/register.js | 1 + ExtensionStore/lib/store.js | 150 ++++++++++--------------- ExtensionStore/lib/widgets.js | 192 +++++++++++++++++++++++++++++++ 4 files changed, 323 insertions(+), 219 deletions(-) create mode 100644 ExtensionStore/lib/widgets.js diff --git a/ExtensionStore/app.js b/ExtensionStore/app.js index cc32975..f982e1d 100644 --- a/ExtensionStore/app.js +++ b/ExtensionStore/app.js @@ -1,16 +1,21 @@ var storelib = require("./lib/store.js"); var Logger = require("./lib/logger.js").Logger; -var log = new Logger("UI") -var WebIcon = require("./lib/network.js").WebIcon +var WebIcon = require("./lib/network.js").WebIcon; var style = require("./lib/style.js"); -var StyledImage = style.StyledImage +var widgets = require("./lib/widgets.js"); +var DescriptionView = widgets.DescriptionView; +var ExtensionItem = widgets.ExtensionItem; +var ProgressButton = widgets.ProgressButton; +var StyledImage = style.StyledImage; + +var log = new Logger("UI"); /** * The main extension store widget class */ -function StoreUI(){ +function StoreUI() { this.store = new storelib.Store(); - log.debug("loading UI") + log.debug("loading UI"); // the list of installed extensions this.localList = new storelib.LocalExtensionList(this.store); @@ -46,10 +51,10 @@ function StoreUI(){ this.aboutFrame.hide(); // EULA logo - var eulaLogo = new StyledImage(storelib.appFolder + "/resources/logo.png", 600, 150) + var eulaLogo = new StyledImage(storelib.appFolder + "/resources/logo.png", 800, 140) this.eulaFrame.innerFrame.eulaLogo.setPixmap(eulaLogo.pixmap); - this.eulaFrame.innerFrame.eulaCB.stateChanged.connect(this, function() { + this.eulaFrame.innerFrame.eulaCB.stateChanged.connect(this, function () { this.localList.saveData("HUES_EULA_ACCEPTED", true); this.eulaFrame.hide(); this.aboutFrame.show(); @@ -60,7 +65,7 @@ function StoreUI(){ } // About logo - var logo = new StyledImage(storelib.appFolder+"/resources/logo.png", 600, 150); + var logo = new StyledImage(storelib.appFolder + "/resources/logo.png", 800, 140); this.aboutFrame.storeLabel.setPixmap(logo.pixmap); // Social media buttons @@ -77,7 +82,7 @@ function StoreUI(){ discordIcon.setAsIcon(this.aboutFrame.discordButton); // Header logo - var headerLogo = new StyledImage(style.ICONS.headerLogo, 24, 24); + var headerLogo = new StyledImage(style.ICONS.headerLogo, 22, 22); this.storeHeader.headerLogo.setPixmap(headerLogo.pixmap); this.checkForUpdates() @@ -136,6 +141,9 @@ function StoreUI(){ this.storeFooter.registerButton.clicked.connect(this, this.registerExtension); // Install Button Actions ------------------------------------------- + this.installButton = new ProgressButton(); + this.storeDescriptionPanel.installButtonPlaceHolder.layout().addWidget(this.installButton, 1, Qt.AlignCenter); + this.installAction = new QAction("Install", this); this.installAction.triggered.connect(this, this.performInstall); @@ -149,14 +157,14 @@ function StoreUI(){ /** * Brings up the register extension dialog for script makers */ -StoreUI.prototype.registerExtension = function(){ +StoreUI.prototype.registerExtension = function () { var RegisterExtensionDialog = require("./lib/register.js").RegisterExtensionDialog; var registerDialog = new RegisterExtensionDialog(this.store, this.localList); registerDialog.show(); } -StoreUI.prototype.show = function(){ +StoreUI.prototype.show = function () { this.ui.show() } @@ -165,7 +173,7 @@ StoreUI.prototype.show = function(){ * store and retrieving extensions. * @param {boolean} visible - Determine whether the progress state should be enabled or disabled. */ -StoreUI.prototype.setUpdateProgressUIState = function(visible){ +StoreUI.prototype.setUpdateProgressUIState = function (visible) { this.aboutFrame.updateButton.visible = !visible; this.aboutFrame.loadStoreButton.visible = !visible; this.aboutFrame.updateLabel.visible = visible; @@ -176,7 +184,7 @@ StoreUI.prototype.setUpdateProgressUIState = function(visible){ /** * Loads the store */ -StoreUI.prototype.loadStore = function(){ +StoreUI.prototype.loadStore = function () { // setup the store widget sizes this.extensionsList.setColumnWidth(0, UiLoader.dpiScale(220)); this.extensionsList.setColumnWidth(1, UiLoader.dpiScale(30)); @@ -199,9 +207,9 @@ StoreUI.prototype.loadStore = function(){ this.setUpdateProgressUIState(true); // Fetch the list of available extensions. - try{ + try { this.storeExtensions = this.store.extensions; - }catch(err){ + } catch (err) { log.error(err) this.setUpdateProgressUIState(false); this.lockStore("Could not load Extensions list.") @@ -235,14 +243,14 @@ StoreUI.prototype.loadStore = function(){ * Looks for the version in the local list, first by id, then by name in case ID changed. * @returns {string} the current version of the installed store */ -StoreUI.prototype.getInstalledVersion = function(){ +StoreUI.prototype.getInstalledVersion = function () { if (this.localList.list.length > 0) { - if (this.localList.hasOwnProperty(this.storeExtension.id)){ + if (this.localList.hasOwnProperty(this.storeExtension.id)) { var installedStore = this.localList.extensions[this.storeExtension.id]; - }else{ + } else { // in case id changed (repo changed), we search by name - for (var i in this.localList.extensions){ - if (this.localList.extensions[i].name == this.storeExtension.name){ + for (var i in this.localList.extensions) { + if (this.localList.extensions[i].name == this.storeExtension.name) { var installedStore = this.localList.extensions[i]; break; } @@ -250,7 +258,7 @@ StoreUI.prototype.getInstalledVersion = function(){ } var currentVersion = installedStore.version; - }else{ + } else { // in case of missing list file, we find out the current version by parsing the json ? var json = this.store.localPackage; if (!json) throw new Error("Invalid store tbpackage.json") @@ -265,31 +273,31 @@ StoreUI.prototype.getInstalledVersion = function(){ /** * Checks for a new version and updates the ribbon accordingly */ -StoreUI.prototype.checkForUpdates = function(){ +StoreUI.prototype.checkForUpdates = function () { var updateRibbon = this.updateRibbon var defaultRibbonStyleSheet = style.STYLESHEETS.defaultRibbon; var updateRibbonStyleSheet = style.STYLESHEETS.updateRibbon; var storeUi = this; - try{ + try { var storeExtension = this.storeExtension; var storeVersion = storeExtension.version; var currentVersion = this.getInstalledVersion(); - this.storeFooter.storeVersionLabel.setText("v" + currentVersion ); + this.storeFooter.storeVersionLabel.setText("v" + currentVersion); // if a more recent version of the store exists on the repo, activate the update ribbon if (!storeExtension.currentVersionIsOlder(currentVersion) && (currentVersion != storeVersion)) { updateRibbon.storeVersion.setText("v" + currentVersion + " ⓘ New version available: v" + storeVersion); updateRibbon.setStyleSheet(updateRibbonStyleSheet); this.aboutFrame.updateButton.toolTip = storeExtension.package.description; - this.aboutFrame.updateButton.clicked.connect(this, function(){storeUi.updateStore(currentVersion, storeVersion)}); + this.aboutFrame.updateButton.clicked.connect(this, function () { storeUi.updateStore(currentVersion, storeVersion) }); } else { this.aboutFrame.updateButton.hide(); updateRibbon.storeVersion.setText("v" + currentVersion + " ✓ - Store is up to date."); updateRibbon.setStyleSheet(defaultRibbonStyleSheet); } - }catch(err){ + } catch (err) { // couldn't check updates, probably we don't have an internet access. // We set up an error message and disable load button. log.error(err) @@ -302,7 +310,7 @@ StoreUI.prototype.checkForUpdates = function(){ * Disable store load button and display an error message * @param {*} message */ -StoreUI.prototype.lockStore = function(message){ +StoreUI.prototype.lockStore = function (message) { var noConnexionRibbonStyleSheet = style.STYLESHEETS.noConnexionRibbon; this.ui.aboutFrame.loadStoreButton.enabled = false; @@ -315,7 +323,7 @@ StoreUI.prototype.lockStore = function(message){ /** * installs the version of the store found on the repo. */ -StoreUI.prototype.updateStore = function(currentVersion, storeVersion){ +StoreUI.prototype.updateStore = function (currentVersion, storeVersion) { var success = this.localList.install(this.storeExtension, this.ui.aboutFrame.updateButton); if (success) { MessageBox.information("Store succesfully updated to version v" + storeVersion + ".\n\nPlease restart Harmony for changes to take effect."); @@ -331,11 +339,11 @@ StoreUI.prototype.updateStore = function(currentVersion, storeVersion){ /** * Updates the list widget displaying the extensions */ -StoreUI.prototype.updateExtensionsList = function(){ +StoreUI.prototype.updateExtensionsList = function () { if (this.localList.list.length == 0) this.localList.createListFile(this.store); - function nameSort(a, b){ - return a.name.toLowerCase() < b.name.toLowerCase()?-1:1 + function nameSort(a, b) { + return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1 } var filter = this.storeHeader.searchStore.text; @@ -360,7 +368,7 @@ StoreUI.prototype.updateExtensionsList = function(){ // get extensions as a list to sort it alphabetically var extensionList = [] - for (var j in sellerExtensions){ + for (var j in sellerExtensions) { extensionList.push(sellerExtensions[j]) } extensionList.sort(nameSort); @@ -375,7 +383,7 @@ StoreUI.prototype.updateExtensionsList = function(){ var sellerItem = new QTreeWidgetItem([sellers[i].name], 0); this.extensionsList.addTopLevelItem(sellerItem); - if (sellers[i].iconUrl){ + if (sellers[i].iconUrl) { var sellerIcon = new WebIcon(sellers[i].iconUrl); sellerIcon.setToWidget(sellerItem); } @@ -394,7 +402,7 @@ StoreUI.prototype.updateExtensionsList = function(){ * Updates the info in the slideout description panel for the given extension * @param {storeLib.Extension} extension */ -StoreUI.prototype.updateDescriptionPanel = function(extension) { +StoreUI.prototype.updateDescriptionPanel = function (extension) { this.storeDescriptionPanel.versionStoreLabel.text = extension.version; this.descriptionText.setHtml(extension.package.description); this.storeDescriptionPanel.storeKeywordsGroup.storeKeywordsLabel.text = extension.package.keywords.join(", "); @@ -413,32 +421,32 @@ StoreUI.prototype.updateDescriptionPanel = function(extension) { var localExtension = this.localList.extensions[extension.id]; if (!localExtension.currentVersionIsOlder(extension.version) && this.localList.checkFiles(extension)) { // Extension installed and up-to-date. - this.storeDescriptionPanel.installButton.setStyleSheet(style.STYLESHEETS.uninstallButton); - this.storeDescriptionPanel.installButton.removeAction(this.installAction); - this.storeDescriptionPanel.installButton.removeAction(this.updateAction); - this.storeDescriptionPanel.installButton.setDefaultAction(this.uninstallAction); + this.installButton.setStyleSheet(style.STYLESHEETS.uninstallButton); + this.installButton.removeAction(this.installAction); + this.installButton.removeAction(this.updateAction); + this.installButton.setDefaultAction(this.uninstallAction); } else { // Extension installed and update available. - this.storeDescriptionPanel.installButton.setStyleSheet(style.STYLESHEETS.updateButton); - this.storeDescriptionPanel.installButton.removeAction(this.installAction); - this.storeDescriptionPanel.installButton.removeAction(this.uninstallAction); - this.storeDescriptionPanel.installButton.setDefaultAction(this.updateAction); + this.installButton.setStyleSheet(style.STYLESHEETS.updateButton); + this.installButton.removeAction(this.installAction); + this.installButton.removeAction(this.uninstallAction); + this.installButton.setDefaultAction(this.updateAction); } } else { // Extension not installed. - this.storeDescriptionPanel.installButton.setStyleSheet(style.STYLESHEETS.installButton); - this.storeDescriptionPanel.installButton.removeAction(this.uninstallAction); - this.storeDescriptionPanel.installButton.removeAction(this.updateAction); - this.storeDescriptionPanel.installButton.setDefaultAction(this.installAction); + this.installButton.setStyleSheet(style.STYLESHEETS.installButton); + this.installButton.removeAction(this.uninstallAction); + this.installButton.removeAction(this.updateAction); + this.installButton.setDefaultAction(this.installAction); } - this.storeDescriptionPanel.installButton.enabled = (extension.package.files.length > 0) + this.installButton.enabled = (extension.package.files.length > 0) } /** * Slide the extension description panel in and out */ -StoreUI.prototype.toggleDescriptionPanel = function(){ +StoreUI.prototype.toggleDescriptionPanel = function () { var selection = this.extensionsList.selectedItems(); @@ -464,7 +472,7 @@ StoreUI.prototype.toggleDescriptionPanel = function(){ /** * Installs the currently selected extension */ -StoreUI.prototype.performInstall = function() { +StoreUI.prototype.performInstall = function () { var selection = this.extensionsList.selectedItems(); if (selection.length == 0) return var id = selection[0].data(0, Qt.UserRole); @@ -472,22 +480,33 @@ StoreUI.prototype.performInstall = function() { log.info("installing extension : " + extension.repository.name + extension.name); // log(JSON.stringify(extension.package, null, " ")) - try { - this.localList.install(extension, this.storeDescriptionPanel.installButton); + var installer = extension.installer; + // log.debug(installer.onInstallProgressChanged) + // log.debug(this.installButton.setProgress) + + this.success = function (){ MessageBox.information("Extension " + extension.name + " v" + extension.version + "\nwas installed correctly."); - } catch (err) { + this.localList.refreshExtensions(); + this.updateExtensionsList(); + } + + this.failure = function (){ log.error(err); MessageBox.information("There was an error while installing extension\n" + extension.name + " v" + extension.version + ":\n\n" + err); } - this.localList.refreshExtensions(); - this.updateExtensionsList(); + + installer.onInstallProgressChanged.connect(this.installButton, this.installButton.setProgress); + installer.onInstallFinished.connect(this, this.success); + installer.onInstallFailed.connect(this, this.failure); + + this.localList.install(extension) } /** * Uninstalls the currently selected extension */ -StoreUI.prototype.performUninstall = function(){ +StoreUI.prototype.performUninstall = function () { var selection = this.extensionsList.selectedItems(); if (selection.length == 0) return; var id = selection[0].data(0, Qt.UserRole); @@ -506,74 +525,4 @@ StoreUI.prototype.performUninstall = function(){ } - -/** - * A QWebView to display the description - * @param {QWidget} parent - */ -function DescriptionView(parent){ - var webPreviewsFontFamily = "Arial"; - var webPreviewsFontSize = UiLoader.dpiScale(12); - - QWebView.call(this, parent) - - this.setMinimumSize(0, 0); - this.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum); - var settings = this.settings(); - settings.setFontFamily(QWebSettings.StandardFont, webPreviewsFontFamily); - settings.setFontSize(QWebSettings.DefaultFontSize, webPreviewsFontSize); -} -DescriptionView.prototype = Object.create(QWebView.prototype) - -/** - * The QTreeWidgetTtem that represents a single extension in the store list. - * @classdesc - * @param {storeLib.Extension} extension the extension which will be represented by this item - * @param {storelib.LocalExtensionList} localList the list of extensions installed on this machine - * @param {QTreeWidget} parent the parent widget for this item - */ - function ExtensionItem(extension, localList, parent) { - this._parent = parent // this is the QTreeWidget - var newExtensions = localList.getData("newExtensions", []); - var extensionLabel = extension.name; - - if (newExtensions.indexOf(extension.id) != -1) extensionLabel += " ★new!" - - QTreeWidgetItem.call(this, [extensionLabel, icon], 1024); - // add an icon in the middle column showing if installed and if update present - if (localList.isInstalled(extension)) { - var iconPath = style.ICONS.installed; - this.setToolTip(1, "Extension is installed correctly."); - var localExtension = localList.extensions[extension.id]; - // log.debug("checking files from "+extension.id, localList.checkFiles(localExtension)); - if (localExtension.currentVersionIsOlder(extension.version)) { - iconPath = style.ICONS.update; - this.setToolTip(1, "Update available:\ncurrently installed version : v" + extension.version); - } else if (!localList.checkFiles(localExtension)) { - iconPath = style.ICONS.error; - this.setToolTip(1, "Some files from this extension are missing."); - } - } else { - iconPath = style.ICONS.notInstalled; - } - var icon = new StyledImage(iconPath); - icon.setAsIcon(this, 1); - - if (extension.iconUrl){ - // set up an icon if one is available - log.debug("adding icon to extension "+ extension.name + " from url : "+extension.iconUrl) - this.extensionIcon = new WebIcon(extension.iconUrl); - this.extensionIcon.setToWidget(this); - - }else{ - // fallback to local icon - var extensionIcon = new StyledImage(style.ICONS.defaultExtension); - extensionIcon.setAsIcon(this, 0); - } - - // store the extension id in the item - this.setData(0, Qt.UserRole, extension.id); -} -ExtensionItem.prototype = Object.create(QTreeWidgetItem.prototype); - exports.StoreUI = StoreUI; \ No newline at end of file diff --git a/ExtensionStore/lib/register.js b/ExtensionStore/lib/register.js index c1c4bc6..6ce8c5a 100644 --- a/ExtensionStore/lib/register.js +++ b/ExtensionStore/lib/register.js @@ -1,4 +1,5 @@ var Logger = require("./logger.js").Logger; +var DescriptionView = require("./lib/widgets.js").DescriptionView; /** * The custom dialog to register a new extension diff --git a/ExtensionStore/lib/store.js b/ExtensionStore/lib/store.js index 97abde3..84924df 100644 --- a/ExtensionStore/lib/store.js +++ b/ExtensionStore/lib/store.js @@ -1,6 +1,7 @@ var NetworkConnexionHandler = require("./network.js").NetworkConnexionHandler; var webQuery = new NetworkConnexionHandler(); var Logger = require("./logger.js").Logger; +var Signal = require("./widgets.js").Signal; var io = require("./io.js"); var readFile = io.readFile; var writeFile = io.writeFile; @@ -837,32 +838,17 @@ Object.defineProperty(Extension.prototype, "localPaths", { }) - -// /** -// * The ExtensionDownloader instance to handle the downloads for this extension -// */ -// Object.defineProperty(Extension.prototype, "downloader", { -// get: function () { -// if (typeof this._downloader === 'undefined') { -// this._downloader = new ExtensionDownloader(this); -// } -// return this._downloader -// } -// }) - - -// /** -// * gets the extension icon file. Can provide a callback to execute once the icon has been obtained. -// */ - -// Extension.prototype.getIcon = function (callback) { -// get: function () { -// var icon = listFiles(this.downloader.cacheFolder, this.safeName + "_icon.png") -// if (icon.length == 0) { -// // look for an icon in the repo -// } -// } -// }) +/** + * The ExtensionInstaller instance to handle the downloads for this extension + */ +Object.defineProperty(Extension.prototype, "installer", { + get: function () { + if (typeof this._installer === 'undefined') { + this._installer = new ExtensionInstaller(this); + } + return this._installer + } +}) /** @@ -1036,23 +1022,26 @@ LocalExtensionList.prototype.checkFiles = function (extension) { /** * Installs the extension - * @param {QToolButton} widget - Optional widget to use as a progressbar. - * @returns {bool} the success of the installation + * @returns {ExtensionInstaller} the installer instance */ -LocalExtensionList.prototype.install = function (extension, widget) { +LocalExtensionList.prototype.install = function (extension) { // if (this.isInstalled(extension)) return true; // extension is already installed - var downloader = new ExtensionDownloader(extension); // dedicated object to implement threaded download later + var installer = extension.installer; // dedicated object to implement threaded download later var installLocation = this.installLocation(extension) - var files = downloader.downloadFiles(widget); - this.log.debug("downloaded files :\n" + files.join("\n")); - var tempFolder = files[0]; - // move the files into the script folder or package folder - recursiveFileCopy(tempFolder, installLocation); - this.addToList(extension); // create a record of this installation + function copyFiles (files){ + this.log.debug("downloaded files :\n" + files.join("\n")); + var tempFolder = files[0]; + // move the files into the script folder or package folder + recursiveFileCopy(tempFolder, installLocation); + this.addToList(extension); // create a record of this installation + this.log.debug("adding to list "+extension); + } - return true; + installer.onInstallFinished.connect(this, copyFiles) + installer.downloadFiles(); + return installer; } @@ -1211,7 +1200,7 @@ Object.defineProperty(LocalExtensionList.prototype, "settings", { * @param {string} value */ LocalExtensionList.prototype.saveData = function(name, value){ - this.log.debug("saving data ", JSON.stringify(value, null, " "), "under name", name) + // this.log.debug("saving data ", JSON.stringify(value, null, " "), "under name", name) var prefs = this.settings; prefs[name] = value; this.settings = prefs; @@ -1225,20 +1214,24 @@ LocalExtensionList.prototype.saveData = function(name, value){ */ LocalExtensionList.prototype.getData = function(name, defaultValue){ if (typeof defaultValue === 'undefined') defaultValue = ""; - this.log.debug("getting data", name, "defaultvalue (type:", (typeof defaultValue), ")") + // this.log.debug("getting data", name, "defaultvalue (type:", (typeof defaultValue), ")") var prefs = this.settings; if (typeof prefs[name] === 'undefined') return defaultValue; return prefs[name]; } -// ScriptDownloader Class -------------------------------------------- +// ExtensionInstaller Class -------------------------------------------- /** * @classdesc * @constructor */ -function ExtensionDownloader(extension) { - this.log = new Logger("ExtensionDownloader") +function ExtensionInstaller(extension) { + this.onInstallProgressChanged = new Signal(); + this.onInstallFinished = new Signal(); + this.onInstallFailed = new Signal(); + + this.log = new Logger("ExtensionInstaller") this.log.level = this.log.LEVEL.LOG; this.repository = extension.repository; this.extension = extension; @@ -1248,74 +1241,43 @@ function ExtensionDownloader(extension) { /** * Downloads the files of the extension from the repository set in the object instance. - * @param {QToolButton} widget - UI icon to treat as a progressbar. - * @returns [string[]] an array of paths of the downloaded files location, as well as the destination folder at index 0 of the array. */ -ExtensionDownloader.prototype.downloadFiles = function (widget) { +ExtensionInstaller.prototype.downloadFiles = function () { this.log.info("starting download of files from extension " + this.extension.name); var destFolder = this.destFolder; - + // get the files list (heavy operations) + this.onInstallProgressChanged.emit(0); // show the progress bar at 0 var destPaths = this.extension.localPaths.map(function (x) { return destFolder + x }); var dlFiles = [this.destFolder]; var files = this.extension.files; - - this.log.debug("downloading files : "+files.map(function(x){return x.path}).join("\n")) - // Set parameters based on whether provided widget is the update button, or install extension button. - var widgetClass = widget.objectName === "installButton" ? "QToolButton" : "QPushButton"; - var completedText = widget.objectName === "installButton" ? "Install Complete" : "Update Complete"; - var initialText = widget.objectName === "installButton" ? "Installing..." : "Updating..."; - var completedState = widget.objectName === "installButton" ? true : false; - - // Configure widget style and text for installation. - widget.setStyleSheet( - widgetClass + "{\ - border-color: transparent transparent " + style.COLORS.GREEN + " transparent;\ - color: " + style.COLORS.GREEN + ";\ - }"); - widget.text = initialText; + this.log.debug("downloading files : "+files.map(function(x){return x.path}).join("\n")) for (var i = 0; i < files.length; i++) { - webQuery.download(this.getDownloadUrl(files[i].path), destPaths[i]); - var dlFile = new File(destPaths[i]); - if (dlFile.size == files[i].size) { - // download complete! - this.log.debug("successfully downloaded " + files[i].path + " to location : " + destPaths[i]); - dlFiles.push(destPaths[i]); - - // Set stylesheet to act as a progressbar. - var progressStopL = i / files.length; - var progressStopR = progressStopL + 0.001; - var progressStyleSheet = widgetClass + " {\ - background-color:\ - qlineargradient(\ - spread:pad,\ - x1:0, y1:0, x2:1, y2:0,\ - stop: " + progressStopL + " " + style.COLORS.GREEN + ",\ - stop:" + progressStopR + " " + style.COLORS["12DP"] + - ");\ - border-color: transparent transparent " + style.COLORS.GREEN + " transparent;\ - color: " + style.COLORS.GREEN + ";\ - }"; - // Update widget with the new linear gradient progression. - widget.setStyleSheet(progressStyleSheet); - - } else { - throw new Error("Downloaded file " + destPaths[i] + " size does not match expected size : \n" + dlFile.size + " bytes (expected : " + files[i].size+" bytes)") + this.onInstallProgressChanged.emit(i/files.length); + try{ + webQuery.download(this.getDownloadUrl(files[i].path), destPaths[i]); + var dlFile = new File(destPaths[i]); + if (dlFile.size == files[i].size) { + // download complete! + this.log.debug("successfully downloaded " + files[i].path + " to location : " + destPaths[i]); + dlFiles.push(destPaths[i]); + } else { + var error = new Error("Downloaded file " + destPaths[i] + " size does not match expected size : \n" + dlFile.size + " bytes (expected : " + files[i].size+" bytes)"); + throw error; + } + }catch(error){ + this.onInstallFailed.emit(error); } } - // Configure widget to indicate the download is completed. - widget.setStyleSheet(widgetClass + " { border: none; background-color: " + style.COLORS.GREEN + "; color: black}"); - widget.text = completedText; - widget.enabled = completedState; - - return dlFiles; + this.onInstallProgressChanged.emit(1); + this.onInstallFinished.emit(dlFiles); } -ExtensionDownloader.prototype.getDownloadUrl = function (filePath) { +ExtensionInstaller.prototype.getDownloadUrl = function (filePath) { return this.extension.repository.dlUrl + filePath; } diff --git a/ExtensionStore/lib/widgets.js b/ExtensionStore/lib/widgets.js new file mode 100644 index 0000000..8e4ad44 --- /dev/null +++ b/ExtensionStore/lib/widgets.js @@ -0,0 +1,192 @@ +var Logger = require("logger.js").Logger; +var log = new Logger("Widgets"); + +/** + * A QWebView to display the description + * @param {QWidget} parent + */ + function DescriptionView(parent){ + var webPreviewsFontFamily = "Arial"; + var webPreviewsFontSize = UiLoader.dpiScale(12); + + QWebView.call(this, parent) + + this.setMinimumSize(0, 0); + this.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum); + var settings = this.settings(); + settings.setFontFamily(QWebSettings.StandardFont, webPreviewsFontFamily); + settings.setFontSize(QWebSettings.DefaultFontSize, webPreviewsFontSize); +} +DescriptionView.prototype = Object.create(QWebView.prototype) + +/** + * The QTreeWidgetTtem that represents a single extension in the store list. + * @classdesc + * @param {storeLib.Extension} extension the extension which will be represented by this item + * @param {storelib.LocalExtensionList} localList the list of extensions installed on this machine + * @param {QTreeWidget} parent the parent widget for this item + */ + function ExtensionItem(extension, localList, parent) { + this._parent = parent // this is the QTreeWidget + var newExtensions = localList.getData("newExtensions", []); + var extensionLabel = extension.name; + + if (newExtensions.indexOf(extension.id) != -1) extensionLabel += " ★new!" + + QTreeWidgetItem.call(this, [extensionLabel, icon], 1024); + // add an icon in the middle column showing if installed and if update present + if (localList.isInstalled(extension)) { + var iconPath = style.ICONS.installed; + this.setToolTip(1, "Extension is installed correctly."); + var localExtension = localList.extensions[extension.id]; + // log.debug("checking files from "+extension.id, localList.checkFiles(localExtension)); + if (localExtension.currentVersionIsOlder(extension.version)) { + iconPath = style.ICONS.update; + this.setToolTip(1, "Update available:\ncurrently installed version : v" + extension.version); + } else if (!localList.checkFiles(localExtension)) { + iconPath = style.ICONS.error; + this.setToolTip(1, "Some files from this extension are missing."); + } + } else { + iconPath = style.ICONS.notInstalled; + } + var icon = new StyledImage(iconPath); + icon.setAsIcon(this, 1); + + if (extension.iconUrl){ + // set up an icon if one is available + log.debug("adding icon to extension "+ extension.name + " from url : "+extension.iconUrl) + this.extensionIcon = new WebIcon(extension.iconUrl); + this.extensionIcon.setToWidget(this); + + }else{ + // fallback to local icon + var extensionIcon = new StyledImage(style.ICONS.defaultExtension); + extensionIcon.setAsIcon(this, 0); + } + + // store the extension id in the item + this.setData(0, Qt.UserRole, extension.id); +} +ExtensionItem.prototype = Object.create(QTreeWidgetItem.prototype); + + +/** + * A button that can also shows progress + */ +function ProgressButton(){ + QToolButton.apply(this, arguments) + this.maximumWidth = this.minimumWidth = UiLoader.dpiScale(130); + this.maximumHeight = this.minimumHeight = UiLoader.dpiScale(30); + + // Set parameters based on whether provided widget is the update button, or install extension button. + // var widgetClass = widget.objectName === "installButton" ? "QToolButton" : "QPushButton"; + // var completedText = widget.objectName === "installButton" ? "Install Complete" : "Update Complete"; + // var initialText = widget.objectName === "installButton" ? "Installing..." : "Updating..."; + // var completedState = widget.objectName === "installButton" ? true : false; + + // Configure widget style and text for installation. + this.setStyleSheet( + "QToolButton {"+ + " border-color: transparent transparent " + style.COLORS.GREEN + " transparent;"+ + " color: " + style.COLORS.GREEN + ";" + + "}"); + // this.text = initialText; +} +ProgressButton.prototype = Object.create(QToolButton.prototype) + +ProgressButton.prototype.setProgress = function (progress){ + log.debug(this) + log.debug("progress button update for progress: "+progress) + if (progress < 0){ + // hide progress bar + } else if (progress < 1) { + // Set stylesheet to act as a progressbar. + var progressStopL = progress; + var progressStopR = progressStopL + 0.001; + var progressStyleSheet = "QToolButton {" + + "background-color:" + + " qlineargradient(" + + " spread:pad," + + " x1:0, y1:0, x2:1, y2:0," + + " stop: " + progressStopL + " " + style.COLORS.GREEN + "," + + " stop:" + progressStopR + " " + style.COLORS["12DP"] + + " );"+ + " border-color: transparent transparent " + style.COLORS.GREEN + " transparent;" + + " color: " + style.COLORS.GREEN + ";" + + "}"; + // Update widget with the new linear gradient progression. + this.setStyleSheet(progressStyleSheet); + + }else{ + // Configure widget to indicate the download is completed. + this.setStyleSheet("QToolButton { border: none; background-color: " + style.COLORS.GREEN + "; color: black}"); + // this.text = completedText; + // this.enabled = true; + } +} + + +/** + * A Qt like custom signal that can be defined, connected and emitted. + * @param {type} type the type of value accepted as argument when calling emit() + */ + function Signal(type){ + // this.emitType = type; + this.connexions = []; + this.blocked = false; +} + +Signal.prototype.connect = function (context, slot){ + // support slot.connect(callback) synthax + if (typeof slot === 'undefined'){ + var slot = context; + var context = null; + } + this.connexions.push ({context: context, slot:slot}); +} + +Signal.prototype.disconnect = function(slot){ + if (typeof slot === "undefined"){ + this.connexions = []; + return + } + + for (var i in this.connexions){ + if (this.connexions[i].slot == slot){ + this.connexions.splice(i, 1); + } + } +} + +Signal.prototype.emit = function () { + if (this.blocked) return; + + // if (!(value instanceof this.type)){ // can't make it work for primitives, might try to fix later? + // throw new error ("Signal can't emit type "+ (typeof value) + ". Must be : " + this.type) + // } + + var args = [] + for (var i=0; i Date: Sat, 29 May 2021 22:52:58 +0200 Subject: [PATCH 039/112] fix for button not updating after install complete --- ExtensionStore/app.js | 63 ++++++++++++++++++++++------------- ExtensionStore/lib/store.js | 1 - ExtensionStore/lib/widgets.js | 57 +++++++++++++------------------ 3 files changed, 63 insertions(+), 58 deletions(-) diff --git a/ExtensionStore/app.js b/ExtensionStore/app.js index f982e1d..5aee674 100644 --- a/ExtensionStore/app.js +++ b/ExtensionStore/app.js @@ -142,6 +142,7 @@ function StoreUI() { // Install Button Actions ------------------------------------------- this.installButton = new ProgressButton(); + this.installButton.objectName = "installButton"; this.storeDescriptionPanel.installButtonPlaceHolder.layout().addWidget(this.installButton, 1, Qt.AlignCenter); this.installAction = new QAction("Install", this); @@ -154,6 +155,24 @@ function StoreUI() { this.uninstallAction.triggered.connect(this, this.performUninstall); } + +/** + * The currently selected Extension in the list. + */ + Object.defineProperty(StoreUI.prototype, "selectedExtension", { + get: function(){ + var selection = this.extensionsList.selectedItems(); + if (selection.length > 0 && selection[0].type() != 0){ + var id = selection[0].data(0, Qt.UserRole); + var extension = this.store.extensions[id]; + + return extension; + } + return null; + } +}) + + /** * Brings up the register extension dialog for script makers */ @@ -342,6 +361,8 @@ StoreUI.prototype.updateStore = function (currentVersion, storeVersion) { StoreUI.prototype.updateExtensionsList = function () { if (this.localList.list.length == 0) this.localList.createListFile(this.store); + log.debug("updating extensions list") + function nameSort(a, b) { return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1 } @@ -399,10 +420,12 @@ StoreUI.prototype.updateExtensionsList = function () { /** - * Updates the info in the slideout description panel for the given extension - * @param {storeLib.Extension} extension + * Updates the info in the slideout description panel for the currently selected extension */ -StoreUI.prototype.updateDescriptionPanel = function (extension) { +StoreUI.prototype.updateDescriptionPanel = function () { + var extension = this.selectedExtension; + if (!extension) return + this.storeDescriptionPanel.versionStoreLabel.text = extension.version; this.descriptionText.setHtml(extension.package.description); this.storeDescriptionPanel.storeKeywordsGroup.storeKeywordsLabel.text = extension.package.keywords.join(", "); @@ -421,20 +444,26 @@ StoreUI.prototype.updateDescriptionPanel = function (extension) { var localExtension = this.localList.extensions[extension.id]; if (!localExtension.currentVersionIsOlder(extension.version) && this.localList.checkFiles(extension)) { // Extension installed and up-to-date. + log.debug("set button to uninstall") this.installButton.setStyleSheet(style.STYLESHEETS.uninstallButton); + this.installButton.accentColor = style.COLORS.ORANGE; this.installButton.removeAction(this.installAction); this.installButton.removeAction(this.updateAction); this.installButton.setDefaultAction(this.uninstallAction); } else { + log.debug("set button to update") // Extension installed and update available. this.installButton.setStyleSheet(style.STYLESHEETS.updateButton); + this.installButton.accentColor = style.COLORS.YELLOW; this.installButton.removeAction(this.installAction); this.installButton.removeAction(this.uninstallAction); this.installButton.setDefaultAction(this.updateAction); } } else { // Extension not installed. + log.debug("set button to install") this.installButton.setStyleSheet(style.STYLESHEETS.installButton); + this.installButton.accentColor = style.COLORS.GREEN; this.installButton.removeAction(this.uninstallAction); this.installButton.removeAction(this.updateAction); this.installButton.setDefaultAction(this.installAction); @@ -447,21 +476,16 @@ StoreUI.prototype.updateDescriptionPanel = function (extension) { * Slide the extension description panel in and out */ StoreUI.prototype.toggleDescriptionPanel = function () { - var selection = this.extensionsList.selectedItems(); - - + // only save the splitter size if it's not collapsed if (this.storeFrame.storeSplitter.sizes()[1] != 0) { this.storeFrameState = this.storeFrame.storeSplitter.saveState(); } + var extension = this.selectedExtension; - - if (selection.length > 0 && selection[0].type() != 0) { + if (extension) { this.storeFrame.storeSplitter.restoreState(this.storeFrameState); - var id = selection[0].data(0, Qt.UserRole); - var extension = this.store.extensions[id]; - // populate the description panel - this.updateDescriptionPanel(extension); + this.updateDescriptionPanel(); } else { // collapse description this.storeFrame.storeSplitter.setSizes([this.storeFrame.storeSplitter.width, 0]); @@ -479,16 +503,7 @@ StoreUI.prototype.performInstall = function () { var extension = this.store.extensions[id]; log.info("installing extension : " + extension.repository.name + extension.name); - // log(JSON.stringify(extension.package, null, " ")) var installer = extension.installer; - // log.debug(installer.onInstallProgressChanged) - // log.debug(this.installButton.setProgress) - - this.success = function (){ - MessageBox.information("Extension " + extension.name + " v" + extension.version + "\nwas installed correctly."); - this.localList.refreshExtensions(); - this.updateExtensionsList(); - } this.failure = function (){ log.error(err); @@ -496,10 +511,12 @@ StoreUI.prototype.performInstall = function () { } installer.onInstallProgressChanged.connect(this.installButton, this.installButton.setProgress); - installer.onInstallFinished.connect(this, this.success); installer.onInstallFailed.connect(this, this.failure); - this.localList.install(extension) + this.localList.install(extension); + this.localList.refreshExtensions(); + this.updateExtensionsList(); + this.updateDescriptionPanel(); } diff --git a/ExtensionStore/lib/store.js b/ExtensionStore/lib/store.js index 84924df..3524e70 100644 --- a/ExtensionStore/lib/store.js +++ b/ExtensionStore/lib/store.js @@ -1025,7 +1025,6 @@ LocalExtensionList.prototype.checkFiles = function (extension) { * @returns {ExtensionInstaller} the installer instance */ LocalExtensionList.prototype.install = function (extension) { - // if (this.isInstalled(extension)) return true; // extension is already installed var installer = extension.installer; // dedicated object to implement threaded download later var installLocation = this.installLocation(extension) diff --git a/ExtensionStore/lib/widgets.js b/ExtensionStore/lib/widgets.js index 8e4ad44..77c9c8b 100644 --- a/ExtensionStore/lib/widgets.js +++ b/ExtensionStore/lib/widgets.js @@ -74,33 +74,22 @@ ExtensionItem.prototype = Object.create(QTreeWidgetItem.prototype); /** * A button that can also shows progress */ -function ProgressButton(){ - QToolButton.apply(this, arguments) +function ProgressButton(color){ + QToolButton.call(this); this.maximumWidth = this.minimumWidth = UiLoader.dpiScale(130); this.maximumHeight = this.minimumHeight = UiLoader.dpiScale(30); - // Set parameters based on whether provided widget is the update button, or install extension button. - // var widgetClass = widget.objectName === "installButton" ? "QToolButton" : "QPushButton"; - // var completedText = widget.objectName === "installButton" ? "Install Complete" : "Update Complete"; - // var initialText = widget.objectName === "installButton" ? "Installing..." : "Updating..."; - // var completedState = widget.objectName === "installButton" ? true : false; - - // Configure widget style and text for installation. - this.setStyleSheet( - "QToolButton {"+ - " border-color: transparent transparent " + style.COLORS.GREEN + " transparent;"+ - " color: " + style.COLORS.GREEN + ";" + - "}"); - // this.text = initialText; + this.accentColor = color; } ProgressButton.prototype = Object.create(QToolButton.prototype) ProgressButton.prototype.setProgress = function (progress){ - log.debug(this) - log.debug("progress button update for progress: "+progress) if (progress < 0){ - // hide progress bar + // hide progress bar ? } else if (progress < 1) { + // this.text = "Installing..."; + this.enabled = false; + // Set stylesheet to act as a progressbar. var progressStopL = progress; var progressStopR = progressStopL + 0.001; @@ -109,26 +98,27 @@ ProgressButton.prototype.setProgress = function (progress){ " qlineargradient(" + " spread:pad," + " x1:0, y1:0, x2:1, y2:0," + - " stop: " + progressStopL + " " + style.COLORS.GREEN + "," + + " stop: " + progressStopL + " " + this.accentColor + "," + " stop:" + progressStopR + " " + style.COLORS["12DP"] + " );"+ - " border-color: transparent transparent " + style.COLORS.GREEN + " transparent;" + - " color: " + style.COLORS.GREEN + ";" + + " border-color: transparent transparent " + this.accentColor + " transparent;" + + " color: white;" + "}"; // Update widget with the new linear gradient progression. this.setStyleSheet(progressStyleSheet); }else{ // Configure widget to indicate the download is completed. - this.setStyleSheet("QToolButton { border: none; background-color: " + style.COLORS.GREEN + "; color: black}"); - // this.text = completedText; - // this.enabled = true; + this.setStyleSheet("QToolButton { border: none; background-color: " + this.accentColor + "; color: white}"); + this.enabled = true; } } /** * A Qt like custom signal that can be defined, connected and emitted. + * As this signal is not actually threaded, the connected callbacks will be exectuted + * directly when the signal is emited, and the rest of the code will execute after. * @param {type} type the type of value accepted as argument when calling emit() */ function Signal(type){ @@ -166,27 +156,26 @@ Signal.prototype.emit = function () { // throw new error ("Signal can't emit type "+ (typeof value) + ". Must be : " + this.type) // } - var args = [] + var args = []; for (var i=0; i Date: Sun, 30 May 2021 12:42:43 -0300 Subject: [PATCH 040/112] Add update text on ProgressButton. --- ExtensionStore/lib/widgets.js | 45 +++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/ExtensionStore/lib/widgets.js b/ExtensionStore/lib/widgets.js index 77c9c8b..eac6167 100644 --- a/ExtensionStore/lib/widgets.js +++ b/ExtensionStore/lib/widgets.js @@ -83,7 +83,48 @@ function ProgressButton(color){ } ProgressButton.prototype = Object.create(QToolButton.prototype) +/** + * Basic implementation of determing Action state (install, uninstall, update) + * by using the action tooltip. + * @returns {Object} Object containing the state, default text and updating base. + */ +ProgressButton.prototype.getState = function() { + var state; + switch (this.defaultAction.toolTip) { + case "install": + state = { + "state": "install", + "defaultText": "Install", + "progressText": "Installing" + } + break; + case "uninstall": + state = { + "state": "uninstall", + "defaultText": "Uninstall", + "progressText": "Uninstalling" + } + break; + case "update": + state = { + "state": "update", + "defaultText": "Update", + "progressText": "Updating" + } + default: + break; + } + return state; +} + +Object.defineProperty(ProgressButton.prototype, "text", { + set: function (text) { + this.defaultAction().text = text; + }}); + ProgressButton.prototype.setProgress = function (progress){ + var state = this.getState(); + if (progress < 0){ // hide progress bar ? } else if (progress < 1) { @@ -107,10 +148,14 @@ ProgressButton.prototype.setProgress = function (progress){ // Update widget with the new linear gradient progression. this.setStyleSheet(progressStyleSheet); + // Update text with progress + this.text = state.progressText + " " + Math.round((progressStopL * 100)) + "%"; + }else{ // Configure widget to indicate the download is completed. this.setStyleSheet("QToolButton { border: none; background-color: " + this.accentColor + "; color: white}"); this.enabled = true; + this.text = state.defaultText; } } From f5618f06052476c7c099e7c0df2581703f1e92d6 Mon Sep 17 00:00:00 2001 From: MathieuC Date: Mon, 31 May 2021 20:11:54 +0200 Subject: [PATCH 041/112] set stylesheet on ProgressButton when changing accentColor --- ExtensionStore/lib/style.js | 19 +++++++++++++++++-- ExtensionStore/lib/widgets.js | 24 +++++++++++++++++++++--- 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/ExtensionStore/lib/style.js b/ExtensionStore/lib/style.js index 9ac9e34..4bb5bb9 100644 --- a/ExtensionStore/lib/style.js +++ b/ExtensionStore/lib/style.js @@ -38,6 +38,7 @@ const styleSheetsDark = { defaultRibbon : "QWidget { background-color: transparent; color: gray;}", updateRibbon : "QWidget { background-color: " + COLORS.YELLOW + "; color: black }", noConnexionRibbon : "QWidget { background-color: " + COLORS.RED + "; color: white; }", + progressButton : "QToolButton { border-color: transparent transparent @ACCENT transparent; }", installButton : "QToolButton { border-color: transparent transparent " + COLORS.GREEN + " transparent; }", uninstallButton : "QToolButton { border-color: transparent transparent " + COLORS.ORANGE + " transparent; }", updateButton : "QToolButton { border-color: transparent transparent " + COLORS.YELLOW + " transparent; }" @@ -151,9 +152,23 @@ Object.defineProperty(StyledImage.prototype, "path", { Object.defineProperty(StyledImage.prototype, "pixmap", { get: function(){ if (typeof this._pixmap === 'undefined') { - log.debug("new pixmap for image : " + this.path) + log.debug("new pixmap for image : " + this.path); var pixmap = new QPixmap(this.path); - var aspectRatioFlag = this.uniformScaling?Qt.KeepAspectRatio:Qt.IgnoreAspectRatio; + + // work out scaling based on params + if (this.uniformScaling){ + if (this.width && this.height){ + // keep inside the given rectangle + var aspectRatioFlag = Qt.KeepAspectRatio; + }else{ + // if one of the width or height is missing, only the other value will be used + var aspectRatioFlag = Qt.KeepAspectRatioByExpanding; + } + }else{ + // resize to match the box exactly + var aspectRatioFlag = Qt.IgnoreAspectRatio; + } + var pixmap = pixmap.scaled(this.width, this.height, aspectRatioFlag, Qt.SmoothTransformation); this._pixmap = pixmap diff --git a/ExtensionStore/lib/widgets.js b/ExtensionStore/lib/widgets.js index 77c9c8b..59117f4 100644 --- a/ExtensionStore/lib/widgets.js +++ b/ExtensionStore/lib/widgets.js @@ -79,13 +79,14 @@ function ProgressButton(color){ this.maximumWidth = this.minimumWidth = UiLoader.dpiScale(130); this.maximumHeight = this.minimumHeight = UiLoader.dpiScale(30); - this.accentColor = color; + this._accentColor = color; } ProgressButton.prototype = Object.create(QToolButton.prototype) ProgressButton.prototype.setProgress = function (progress){ if (progress < 0){ - // hide progress bar ? + // reset stylesheet by changing the accentColor + this.accentColor = this.accentColor; } else if (progress < 1) { // this.text = "Installing..."; this.enabled = false; @@ -114,6 +115,17 @@ ProgressButton.prototype.setProgress = function (progress){ } } +Object.defineProperty(ProgressButton.prototype, "accentColor", { + get: function(){ + return this._accentColor; + }, + set: function(newColor){ + this._accentColor = newColor; + this.setStyleSheet(style.STYLESHEETS.progressButton.replace("@ACCENT", this._accentColor)) + } +}) + + /** * A Qt like custom signal that can be defined, connected and emitted. @@ -167,7 +179,13 @@ Signal.prototype.emit = function () { var context = this.connexions[i].context; var slot = this.connexions[i].slot; log.debug("calling slot "+ slot); - slot.apply(context, args); + + // support connecting signals to each other + if (slot instanceof Signal){ + slot.emit.apply(context, args) + }else{ + slot.apply(context, args); + } } } From 7e6887ca63472c05e5706002bff8b5475a6dfd85 Mon Sep 17 00:00:00 2001 From: MathieuC Date: Mon, 31 May 2021 20:12:20 +0200 Subject: [PATCH 042/112] simplify getting selected extension/setting stylesheet on install button --- ExtensionStore/app.js | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/ExtensionStore/app.js b/ExtensionStore/app.js index 5aee674..5a2740a 100644 --- a/ExtensionStore/app.js +++ b/ExtensionStore/app.js @@ -445,7 +445,6 @@ StoreUI.prototype.updateDescriptionPanel = function () { if (!localExtension.currentVersionIsOlder(extension.version) && this.localList.checkFiles(extension)) { // Extension installed and up-to-date. log.debug("set button to uninstall") - this.installButton.setStyleSheet(style.STYLESHEETS.uninstallButton); this.installButton.accentColor = style.COLORS.ORANGE; this.installButton.removeAction(this.installAction); this.installButton.removeAction(this.updateAction); @@ -453,7 +452,6 @@ StoreUI.prototype.updateDescriptionPanel = function () { } else { log.debug("set button to update") // Extension installed and update available. - this.installButton.setStyleSheet(style.STYLESHEETS.updateButton); this.installButton.accentColor = style.COLORS.YELLOW; this.installButton.removeAction(this.installAction); this.installButton.removeAction(this.uninstallAction); @@ -462,7 +460,6 @@ StoreUI.prototype.updateDescriptionPanel = function () { } else { // Extension not installed. log.debug("set button to install") - this.installButton.setStyleSheet(style.STYLESHEETS.installButton); this.installButton.accentColor = style.COLORS.GREEN; this.installButton.removeAction(this.uninstallAction); this.installButton.removeAction(this.updateAction); @@ -497,10 +494,8 @@ StoreUI.prototype.toggleDescriptionPanel = function () { * Installs the currently selected extension */ StoreUI.prototype.performInstall = function () { - var selection = this.extensionsList.selectedItems(); - if (selection.length == 0) return - var id = selection[0].data(0, Qt.UserRole); - var extension = this.store.extensions[id]; + var extension = this.selectedExtension + if (!extension) return log.info("installing extension : " + extension.repository.name + extension.name); var installer = extension.installer; @@ -524,10 +519,8 @@ StoreUI.prototype.performInstall = function () { * Uninstalls the currently selected extension */ StoreUI.prototype.performUninstall = function () { - var selection = this.extensionsList.selectedItems(); - if (selection.length == 0) return; - var id = selection[0].data(0, Qt.UserRole); - var extension = this.store.extensions[id]; + var extension = this.selectedExtension + if (!extension) return log.info("uninstalling extension : " + extension.repository.name + extension.name); try { From 2ab4827cf0ec952c8b8ac791ae994a415d701b93 Mon Sep 17 00:00:00 2001 From: MathieuC Date: Mon, 31 May 2021 20:12:29 +0200 Subject: [PATCH 043/112] fix import in register --- ExtensionStore/lib/register.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ExtensionStore/lib/register.js b/ExtensionStore/lib/register.js index 6ce8c5a..298634a 100644 --- a/ExtensionStore/lib/register.js +++ b/ExtensionStore/lib/register.js @@ -1,5 +1,5 @@ var Logger = require("./logger.js").Logger; -var DescriptionView = require("./lib/widgets.js").DescriptionView; +var DescriptionView = require("./widgets.js").DescriptionView; /** * The custom dialog to register a new extension From a6cc2393fd49a1d2e7240d4e485f08e9d1924a53 Mon Sep 17 00:00:00 2001 From: MathieuC Date: Mon, 31 May 2021 20:12:58 +0200 Subject: [PATCH 044/112] fix resize on search field --- ExtensionStore/resources/stylesheet_dark.qss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ExtensionStore/resources/stylesheet_dark.qss b/ExtensionStore/resources/stylesheet_dark.qss index 6421014..b5ed03f 100644 --- a/ExtensionStore/resources/stylesheet_dark.qss +++ b/ExtensionStore/resources/stylesheet_dark.qss @@ -257,7 +257,8 @@ QLineEdit#searchStore { QLineEdit:focus#searchStore { background-color: @03DP; border-radius: 10px; - border: none; + border-width: 2px; + border-color: transparent; } /* Store search box clear */ From 55e325082c356648641d11b685232427313dada6 Mon Sep 17 00:00:00 2001 From: bob-ross27 <55351437+bob-ross27@users.noreply.github.com> Date: Mon, 31 May 2021 21:21:47 -0300 Subject: [PATCH 045/112] Refactored ProgressButton and added 2 child classes for loading and installation. --- ExtensionStore/app.js | 31 ++----- ExtensionStore/lib/style.js | 3 +- ExtensionStore/lib/widgets.js | 170 ++++++++++++++++++++++------------ 3 files changed, 120 insertions(+), 84 deletions(-) diff --git a/ExtensionStore/app.js b/ExtensionStore/app.js index 5a2740a..fdb3a97 100644 --- a/ExtensionStore/app.js +++ b/ExtensionStore/app.js @@ -6,6 +6,7 @@ var widgets = require("./lib/widgets.js"); var DescriptionView = widgets.DescriptionView; var ExtensionItem = widgets.ExtensionItem; var ProgressButton = widgets.ProgressButton; +var InstallButton = widgets.InstallButton; var StyledImage = style.StyledImage; var log = new Logger("UI"); @@ -141,18 +142,13 @@ function StoreUI() { this.storeFooter.registerButton.clicked.connect(this, this.registerExtension); // Install Button Actions ------------------------------------------- - this.installButton = new ProgressButton(); + this.installButton = new InstallButton("Install"); this.installButton.objectName = "installButton"; this.storeDescriptionPanel.installButtonPlaceHolder.layout().addWidget(this.installButton, 1, Qt.AlignCenter); - this.installAction = new QAction("Install", this); - this.installAction.triggered.connect(this, this.performInstall); - - this.updateAction = new QAction("Update", this); - this.updateAction.triggered.connect(this, this.performInstall); - - this.uninstallAction = new QAction("Uninstall", this); - this.uninstallAction.triggered.connect(this, this.performUninstall); + this.installButton.modes.INSTALL.action.triggered.connect(this, this.performInstall); + this.installButton.modes.UPDATE.action.triggered.connect(this, this.performInstall); + this.installButton.modes.UNINSTALL.action.triggered.connect(this, this.performUninstall); } @@ -444,26 +440,17 @@ StoreUI.prototype.updateDescriptionPanel = function () { var localExtension = this.localList.extensions[extension.id]; if (!localExtension.currentVersionIsOlder(extension.version) && this.localList.checkFiles(extension)) { // Extension installed and up-to-date. - log.debug("set button to uninstall") - this.installButton.accentColor = style.COLORS.ORANGE; - this.installButton.removeAction(this.installAction); - this.installButton.removeAction(this.updateAction); - this.installButton.setDefaultAction(this.uninstallAction); + log.debug("set button to uninstall"); + this.installButton.mode = "Uninstall"; } else { log.debug("set button to update") // Extension installed and update available. - this.installButton.accentColor = style.COLORS.YELLOW; - this.installButton.removeAction(this.installAction); - this.installButton.removeAction(this.uninstallAction); - this.installButton.setDefaultAction(this.updateAction); + this.installButton.mode = "Update"; } } else { // Extension not installed. log.debug("set button to install") - this.installButton.accentColor = style.COLORS.GREEN; - this.installButton.removeAction(this.uninstallAction); - this.installButton.removeAction(this.updateAction); - this.installButton.setDefaultAction(this.installAction); + this.installButton.mode = "Install"; } this.installButton.enabled = (extension.package.files.length > 0) } diff --git a/ExtensionStore/lib/style.js b/ExtensionStore/lib/style.js index 4bb5bb9..5ef9dd1 100644 --- a/ExtensionStore/lib/style.js +++ b/ExtensionStore/lib/style.js @@ -41,7 +41,8 @@ const styleSheetsDark = { progressButton : "QToolButton { border-color: transparent transparent @ACCENT transparent; }", installButton : "QToolButton { border-color: transparent transparent " + COLORS.GREEN + " transparent; }", uninstallButton : "QToolButton { border-color: transparent transparent " + COLORS.ORANGE + " transparent; }", - updateButton : "QToolButton { border-color: transparent transparent " + COLORS.YELLOW + " transparent; }" + updateButton : "QToolButton { border-color: transparent transparent " + COLORS.YELLOW + " transparent; }", + loadButton : "QToolButton { border-color: transparent transparent " + COLORS.ACCENT_LIGHT + " transparent; }", } const styleSheetsLight = styleSheetsDark; diff --git a/ExtensionStore/lib/widgets.js b/ExtensionStore/lib/widgets.js index b8d5405..c175dad 100644 --- a/ExtensionStore/lib/widgets.js +++ b/ExtensionStore/lib/widgets.js @@ -72,104 +72,150 @@ ExtensionItem.prototype = Object.create(QTreeWidgetItem.prototype); /** - * A button that can also shows progress + * A button that can also show progress + * @classdesc + * @constructor */ -function ProgressButton(color){ +function ProgressButton(){ QToolButton.call(this); this.maximumWidth = this.minimumWidth = UiLoader.dpiScale(130); this.maximumHeight = this.minimumHeight = UiLoader.dpiScale(30); - this._accentColor = color; } -ProgressButton.prototype = Object.create(QToolButton.prototype) +ProgressButton.prototype = Object.create(QToolButton.prototype); /** - * Basic implementation of determing Action state (install, uninstall, update) - * by using the action tooltip. - * @returns {Object} Object containing the state, default text and updating base. + * Set the text of the underlying Action rather than the widget directly. */ -ProgressButton.prototype.getState = function() { - var state; - switch (this.defaultAction.toolTip) { - case "install": - state = { - "state": "install", - "defaultText": "Install", - "progressText": "Installing" - } - break; - case "uninstall": - state = { - "state": "uninstall", - "defaultText": "Uninstall", - "progressText": "Uninstalling" - } - break; - case "update": - state = { - "state": "update", - "defaultText": "Update", - "progressText": "Updating" - } - default: - break; - } - return state; -} - Object.defineProperty(ProgressButton.prototype, "text", { + get: function () { + return this.defaultAction().text; + }, set: function (text) { this.defaultAction().text = text; - }}); + } +}); + +/** + * Get or Set the button mode. + * Changing the mode alters the visual appearance as well + * as exposes a different Action. + */ +Object.defineProperty(ProgressButton.prototype, "mode", { + get: function () { + return this._mode; + }, + set: function (mode) { + var mode = mode.toUpperCase(); + this._mode = this.modes[mode]; + this.accentColor = this.modes[mode].accentColor; + this.setStyleSheet(this.modes[mode].styleSheet); + this.removeAction(this.defaultAction()); + this.setDefaultAction(this.modes[mode].action); + } +}); -ProgressButton.prototype.setProgress = function (progress){ - var state = this.getState(); - if (progress < 0){ - // reset stylesheet by changing the accentColor - this.accentColor = this.accentColor; - } else if (progress < 1) { - // this.text = "Installing..."; +/** + * Use the background stylesheet of the widget to act as a progress bar. + * @param {Int} progress - Value from 0 to 1 that the operation is currently at. + */ +ProgressButton.prototype.setProgress = function (progress) { + var accentColor = this.accentColor; + var backgroundColor = this.backgroundColor; + + if (progress < 1) { this.enabled = false; // Set stylesheet to act as a progressbar. - var progressStopL = progress; - var progressStopR = progressStopL + 0.001; + var progressStopR = progress; + var progressStopL = progressStopR - 0.001; var progressStyleSheet = "QToolButton {" + "background-color:" + " qlineargradient(" + " spread:pad," + " x1:0, y1:0, x2:1, y2:0," + - " stop: " + progressStopL + " " + this.accentColor + "," + - " stop:" + progressStopR + " " + style.COLORS["12DP"] + + " stop: " + progressStopL + " " + accentColor + "," + + " stop:" + progressStopR + " " + backgroundColor + " );"+ - " border-color: transparent transparent " + this.accentColor + " transparent;" + + " border-color: transparent transparent " + accentColor + " transparent;" + " color: white;" + "}"; // Update widget with the new linear gradient progression. this.setStyleSheet(progressStyleSheet); // Update text with progress - this.text = state.progressText + " " + Math.round((progressStopL * 100)) + "%"; + this.text = this.mode.progressText + " " + Math.round((progressStopR * 100)) + "%"; - }else{ + } else { // Configure widget to indicate the download is completed. - this.setStyleSheet("QToolButton { border: none; background-color: " + this.accentColor + "; color: white}"); + this.setStyleSheet("QToolButton { border: none; background-color: " + accentColor + "; color: white}"); this.enabled = true; - this.text = state.defaultText; + this.text = this.mode.defaultText; } } -Object.defineProperty(ProgressButton.prototype, "accentColor", { - get: function(){ - return this._accentColor; - }, - set: function(newColor){ - this._accentColor = newColor; - this.setStyleSheet(style.STYLESHEETS.progressButton.replace("@ACCENT", this._accentColor)) +/** + * ProgressButton child class for Loading operations. + * @classdesc + * @constructor + */ +function LoadButton() { + ProgressButton.call(this); + + this.backgroundColor = style.COLORS["08DP"]; + + this.modes = { + "LOAD": { + "action": new QAction("Load", this), + "defaultText": "Load", + "progressText": "Loading", + "accentColor": style.COLORS.ACCENT_LIGHT, + "styleSheet": style.STYLESHEETS.LoadButton, + } } -}) + this.mode = "LOAD"; +} +InstallButton.prototype = Object.create(ProgressButton.prototype); +/** + * ProgressButton child class for Extension installation, uninstallation and updates. + * @classdesc + * @constructor + * @param {String} mode - Default mode to set the button to. + */ +function InstallButton(mode) { + ProgressButton.call(this); + + this.backgroundColor = style.COLORS["12DP"]; + + this.modes = { + "INSTALL": { + "action": new QAction("Install", this), + "defaultText": "Install", + "progressText": "Installing", + "accentColor": style.COLORS.GREEN, + "styleSheet": style.STYLESHEETS.installButton, + }, + "UNINSTALL": { + "action": new QAction("Uninstall", this), + "defaultText": "Uninstall", + "progressText": "Uninstalling", + "accentColor": style.COLORS.ORANGE, + "styleSheet": style.STYLESHEETS.uninstallButton, + }, + "UPDATE": { + "action": new QAction("Update", this), + "defaultText": "Update", + "progressText": "Updating", + "accentColor": style.COLORS.YELLOW, + "styleSheet": style.STYLESHEETS.updateButton, + }, + } + + this.mode = mode; +} +InstallButton.prototype = Object.create(ProgressButton.prototype); /** @@ -240,5 +286,7 @@ Signal.prototype.toString = function(){ exports.Signal = Signal; exports.ProgressButton = ProgressButton; +exports.LoadButton = LoadButton; +exports.InstallButton = InstallButton; exports.DescriptionView = DescriptionView; exports.ExtensionItem = ExtensionItem; \ No newline at end of file From 012bdfa909c9e39a43fcdee30a57b9f59eb45540 Mon Sep 17 00:00:00 2001 From: bob-ross27 <55351437+bob-ross27@users.noreply.github.com> Date: Tue, 1 Jun 2021 00:13:34 -0300 Subject: [PATCH 046/112] Added verification that files were removed from disk. --- ExtensionStore/lib/store.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/ExtensionStore/lib/store.js b/ExtensionStore/lib/store.js index 7aa453b..2fbe1cf 100644 --- a/ExtensionStore/lib/store.js +++ b/ExtensionStore/lib/store.js @@ -1071,7 +1071,16 @@ LocalExtensionList.prototype.uninstall = function (extension) { // Update the extension list accordingly. this.removeFromList(extension); - return true; + // Verify delete operations. + var filesDeleted = localExtension.package.localFiles.every(function(x) { + return !new QFileInfo(x).exists(); + }); + + // Return operation success. + if (filesDeleted) { + return true; + } + throw new Error("Unable to delete one or more local extension files during uninstall."); } From 26b93c9dc0b925f7740073419c361fd44d71d1cb Mon Sep 17 00:00:00 2001 From: MathieuC Date: Tue, 1 Jun 2021 12:40:08 +0200 Subject: [PATCH 047/112] move appFolder into io.js --- ExtensionStore/app.js | 5 +++-- ExtensionStore/lib/io.js | 9 ++++++++- ExtensionStore/lib/register.js | 5 +++-- ExtensionStore/lib/store.js | 5 +---- ExtensionStore/lib/style.js | 2 +- 5 files changed, 16 insertions(+), 10 deletions(-) diff --git a/ExtensionStore/app.js b/ExtensionStore/app.js index fdb3a97..ecabab4 100644 --- a/ExtensionStore/app.js +++ b/ExtensionStore/app.js @@ -3,6 +3,7 @@ var Logger = require("./lib/logger.js").Logger; var WebIcon = require("./lib/network.js").WebIcon; var style = require("./lib/style.js"); var widgets = require("./lib/widgets.js"); +var appFolder = require("./lib/io.js").appFolder; var DescriptionView = widgets.DescriptionView; var ExtensionItem = widgets.ExtensionItem; var ProgressButton = widgets.ProgressButton; @@ -52,7 +53,7 @@ function StoreUI() { this.aboutFrame.hide(); // EULA logo - var eulaLogo = new StyledImage(storelib.appFolder + "/resources/logo.png", 800, 140) + var eulaLogo = new StyledImage(appFolder + "/resources/logo.png", 800, 140) this.eulaFrame.innerFrame.eulaLogo.setPixmap(eulaLogo.pixmap); this.eulaFrame.innerFrame.eulaCB.stateChanged.connect(this, function () { @@ -66,7 +67,7 @@ function StoreUI() { } // About logo - var logo = new StyledImage(storelib.appFolder + "/resources/logo.png", 800, 140); + var logo = new StyledImage(appFolder + "/resources/logo.png", 800, 140); this.aboutFrame.storeLabel.setPixmap(logo.pixmap); // Social media buttons diff --git a/ExtensionStore/lib/io.js b/ExtensionStore/lib/io.js index 170b6e1..9bb6661 100644 --- a/ExtensionStore/lib/io.js +++ b/ExtensionStore/lib/io.js @@ -81,7 +81,14 @@ function recursiveFileCopy(folder, destination) { } } + +// returns the folder of this file +var appFolder = __file__.split("/").slice(0, -2).join("/"); +if (appFolder.indexOf("repo") == -1) Logger.level = 1; // disable logging if extension isn't in a repository + + exports.listFiles = listFiles exports.writeFile = writeFile exports.readFile = readFile -exports.recursiveFileCopy = recursiveFileCopy \ No newline at end of file +exports.recursiveFileCopy = recursiveFileCopy +exports.appFolder = appFolder \ No newline at end of file diff --git a/ExtensionStore/lib/register.js b/ExtensionStore/lib/register.js index 298634a..0388b3a 100644 --- a/ExtensionStore/lib/register.js +++ b/ExtensionStore/lib/register.js @@ -1,5 +1,6 @@ var Logger = require("./logger.js").Logger; var DescriptionView = require("./widgets.js").DescriptionView; +var appFolder = require("./lib/io.js").appFolder; /** * The custom dialog to register a new extension @@ -8,7 +9,7 @@ var DescriptionView = require("./widgets.js").DescriptionView; */ function RegisterExtensionDialog(store, localList){ - var appFolder = storelib.appFolder; + var appFolder = appFolder; this.ui = UiLoader.load(appFolder + "/resources/register.ui"); this.store = store; @@ -393,7 +394,7 @@ function FilesPicker(url, includedFiles){ this.includedFileBackground = new QBrush(new QColor(Qt.darkRed), Qt.SolidPattern); // load and setup the dialog - this.ui = UiLoader.load(storelib.appFolder + "/resources/pickFiles.ui"); + this.ui = UiLoader.load(appFolder + "/resources/pickFiles.ui"); this.filesPanel = this.ui.filesSplitter.widget(0); this.fileList = this.filesPanel.repoContents; this.filterField = this.filesPanel.filterField; diff --git a/ExtensionStore/lib/store.js b/ExtensionStore/lib/store.js index 2fbe1cf..1bb33fb 100644 --- a/ExtensionStore/lib/store.js +++ b/ExtensionStore/lib/store.js @@ -6,6 +6,7 @@ var io = require("./io.js"); var readFile = io.readFile; var writeFile = io.writeFile; var recursiveFileCopy = io.recursiveFileCopy; +var appFolder = io.appFolder; Logger.level = 2; @@ -1292,10 +1293,6 @@ ExtensionInstaller.prototype.getDownloadUrl = function (filePath) { // Helper functions --------------------------------------------------- -// returns the folder of this file -var appFolder = __file__.split("/").slice(0, -2).join("/"); -if (appFolder.indexOf("repo") == -1) Logger.level = 1; // disable logging if extension isn't in a repository - // make a deep copy of an object function deepCopy(object) { var copy = JSON.parse(JSON.stringify(object)) diff --git a/ExtensionStore/lib/style.js b/ExtensionStore/lib/style.js index 5ef9dd1..c9852bd 100644 --- a/ExtensionStore/lib/style.js +++ b/ExtensionStore/lib/style.js @@ -2,7 +2,7 @@ var Logger = require("./logger.js").Logger; var io = require("./io.js"); var log = new Logger("Style"); -var appFolder = require("./store.js").appFolder; +var appFolder = io.appFolder; // Enum to hold dark style palette. // 4% opacity over Material UI palette. From f7ee10bb86e67d66eb428880a32a4efd0f096969 Mon Sep 17 00:00:00 2001 From: MathieuC Date: Tue, 1 Jun 2021 12:40:28 +0200 Subject: [PATCH 048/112] refactor progressButton/installButton --- ExtensionStore/lib/widgets.js | 132 +++++++++++++++++----------------- 1 file changed, 67 insertions(+), 65 deletions(-) diff --git a/ExtensionStore/lib/widgets.js b/ExtensionStore/lib/widgets.js index c175dad..4bb3496 100644 --- a/ExtensionStore/lib/widgets.js +++ b/ExtensionStore/lib/widgets.js @@ -1,4 +1,5 @@ var Logger = require("logger.js").Logger; +var style = require("style.js"); var log = new Logger("Widgets"); /** @@ -76,56 +77,53 @@ ExtensionItem.prototype = Object.create(QTreeWidgetItem.prototype); * @classdesc * @constructor */ -function ProgressButton(){ +function ProgressButton(color, text, progressText, finishedText){ + if (typeof finishedText === 'undefined') var finishedText = "Done"; + if (typeof progressText === 'undefined') var progressText = "In progress..."; + QToolButton.call(this); this.maximumWidth = this.minimumWidth = UiLoader.dpiScale(130); this.maximumHeight = this.minimumHeight = UiLoader.dpiScale(30); - + this.backgroundColor = style.COLORS["12DP"]; // get this from stylesheet? + this.accentColor = color; + this.defaultText = text; + this.progressText = progressText; + this.finishedText = finishedText; } ProgressButton.prototype = Object.create(QToolButton.prototype); -/** - * Set the text of the underlying Action rather than the widget directly. - */ -Object.defineProperty(ProgressButton.prototype, "text", { - get: function () { - return this.defaultAction().text; - }, - set: function (text) { - this.defaultAction().text = text; - } -}); /** - * Get or Set the button mode. - * Changing the mode alters the visual appearance as well - * as exposes a different Action. + * The accent color used by the button to show the loading and the border. + * Setting this will apply the corresponding stylesheet. */ -Object.defineProperty(ProgressButton.prototype, "mode", { - get: function () { - return this._mode; +Object.defineProperty(ProgressButton.prototype, "accentColor", { + get: function(){ + return this._accentColor; }, - set: function (mode) { - var mode = mode.toUpperCase(); - this._mode = this.modes[mode]; - this.accentColor = this.modes[mode].accentColor; - this.setStyleSheet(this.modes[mode].styleSheet); - this.removeAction(this.defaultAction()); - this.setDefaultAction(this.modes[mode].action); + set: function(newColor){ + this._accentColor = newColor; + this.setStyleSheet(style.STYLESHEETS.progressButton.replace("@ACCENT", this._accentColor)) } -}); +}) /** * Use the background stylesheet of the widget to act as a progress bar. - * @param {Int} progress - Value from 0 to 1 that the operation is currently at. + * @param {Int} progress - Value from 0 to 1 that the operation is currently at. */ ProgressButton.prototype.setProgress = function (progress) { var accentColor = this.accentColor; var backgroundColor = this.backgroundColor; - if (progress < 1) { + if (progress < 0) { + // resetting stylesheet by setting accentColor + this.accentColor = accentColor; + this.text = this.defaultText; + + } else if (progress < 1) { this.enabled = false; + this.text = this.progressText; // Set stylesheet to act as a progressbar. var progressStopR = progress; @@ -151,32 +149,11 @@ ProgressButton.prototype.setProgress = function (progress) { // Configure widget to indicate the download is completed. this.setStyleSheet("QToolButton { border: none; background-color: " + accentColor + "; color: white}"); this.enabled = true; - this.text = this.mode.defaultText; + this.text = this.finishedText; } } -/** - * ProgressButton child class for Loading operations. - * @classdesc - * @constructor - */ -function LoadButton() { - ProgressButton.call(this); - - this.backgroundColor = style.COLORS["08DP"]; - this.modes = { - "LOAD": { - "action": new QAction("Load", this), - "defaultText": "Load", - "progressText": "Loading", - "accentColor": style.COLORS.ACCENT_LIGHT, - "styleSheet": style.STYLESHEETS.LoadButton, - } - } - this.mode = "LOAD"; -} -InstallButton.prototype = Object.create(ProgressButton.prototype); /** * ProgressButton child class for Extension installation, uninstallation and updates. @@ -184,36 +161,61 @@ InstallButton.prototype = Object.create(ProgressButton.prototype); * @constructor * @param {String} mode - Default mode to set the button to. */ -function InstallButton(mode) { +function InstallButton() { ProgressButton.call(this); - - this.backgroundColor = style.COLORS["12DP"]; - this.modes = { "INSTALL": { "action": new QAction("Install", this), - "defaultText": "Install", - "progressText": "Installing", + "progressText": "Installing...", "accentColor": style.COLORS.GREEN, - "styleSheet": style.STYLESHEETS.installButton, }, "UNINSTALL": { "action": new QAction("Uninstall", this), - "defaultText": "Uninstall", - "progressText": "Uninstalling", + "progressText": "Uninstalling...", "accentColor": style.COLORS.ORANGE, - "styleSheet": style.STYLESHEETS.uninstallButton, }, "UPDATE": { "action": new QAction("Update", this), - "defaultText": "Update", - "progressText": "Updating", + "progressText": "Updating...", "accentColor": style.COLORS.YELLOW, - "styleSheet": style.STYLESHEETS.updateButton, }, } - this.mode = mode; + this.mode = "INSTALL"; +} +InstallButton.prototype = Object.create(ProgressButton.prototype); + + +/** + * Get or Set the button mode. + * Changing the mode alters the visual appearance as well + * as exposes a different Action. + */ +Object.defineProperty(InstallButton.prototype, "mode", { + get: function () { + return this.modes[this._mode]; + }, + set: function (mode) { + var mode = mode.toUpperCase(); + + if (mode != this._mode){ + this._mode = mode; + this.accentColor = this.modes[mode].accentColor; + this.progressText = this.modes[mode].progressText; + this.removeAction(this.defaultAction()); + this.setDefaultAction(this.modes[mode].action); + } + } +}); + + +/** + * ProgressButton child class for Loading operations. + * @classdesc + * @constructor + */ +function LoadButton() { + ProgressButton.call(this, style.COLORS.ACCENT_LIGHT, "Load Store", "Loading..."); } InstallButton.prototype = Object.create(ProgressButton.prototype); From 3e8a5cdf0263eb68820b4d939d4f87f677a319ad Mon Sep 17 00:00:00 2001 From: MathieuC Date: Tue, 1 Jun 2021 12:48:50 +0200 Subject: [PATCH 049/112] sanity check for mode setting --- ExtensionStore/lib/widgets.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ExtensionStore/lib/widgets.js b/ExtensionStore/lib/widgets.js index 4bb3496..2f137ef 100644 --- a/ExtensionStore/lib/widgets.js +++ b/ExtensionStore/lib/widgets.js @@ -197,13 +197,15 @@ Object.defineProperty(InstallButton.prototype, "mode", { }, set: function (mode) { var mode = mode.toUpperCase(); + var modeDetails = this.modes[mode] + if (!modeDetails) throw new Error ("Can't set InstallButton mode to "+ mode+ ", mode can only be 'INSTALL', 'UNINSTALL' or 'UPDATE'." ) if (mode != this._mode){ this._mode = mode; - this.accentColor = this.modes[mode].accentColor; - this.progressText = this.modes[mode].progressText; + this.accentColor = modeDetails.accentColor; + this.progressText = modeDetails.progressText; this.removeAction(this.defaultAction()); - this.setDefaultAction(this.modes[mode].action); + this.setDefaultAction(modeDetails.action); } } }); From b4a3aef1135a9d61475e0369bce4f5a29b204ff6 Mon Sep 17 00:00:00 2001 From: MathieuC Date: Sat, 5 Jun 2021 22:16:02 +0200 Subject: [PATCH 050/112] Update widgets.js --- ExtensionStore/lib/widgets.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ExtensionStore/lib/widgets.js b/ExtensionStore/lib/widgets.js index 2f137ef..1c0f301 100644 --- a/ExtensionStore/lib/widgets.js +++ b/ExtensionStore/lib/widgets.js @@ -219,7 +219,7 @@ Object.defineProperty(InstallButton.prototype, "mode", { function LoadButton() { ProgressButton.call(this, style.COLORS.ACCENT_LIGHT, "Load Store", "Loading..."); } -InstallButton.prototype = Object.create(ProgressButton.prototype); +LoadButton.prototype = Object.create(ProgressButton.prototype); /** From cc2b0aedee3044b76ec81504713239ab2edc462e Mon Sep 17 00:00:00 2001 From: MathieuC Date: Sun, 6 Jun 2021 02:23:00 +0200 Subject: [PATCH 051/112] get isPackage from presence of configure.js file --- ExtensionStore/lib/store.js | 29 ++++++++++++++++++++++++----- ExtensionStore/lib/widgets.js | 1 - 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/ExtensionStore/lib/store.js b/ExtensionStore/lib/store.js index 1bb33fb..4ef3a83 100644 --- a/ExtensionStore/lib/store.js +++ b/ExtensionStore/lib/store.js @@ -664,7 +664,6 @@ Object.defineProperty(Extension.prototype, "package", { "compatibility": "Harmony Premium 16", "description": "", "repository": this.repository._url, - "isPackage": false, "files": [], "icon": "", "keywords": [], @@ -832,7 +831,6 @@ Object.defineProperty(Extension.prototype, "localPaths", { if (typeof this._localPaths === 'undefined') { var rootFolder = this.rootFolder; this._localPaths = this.files.map(function (x) { return x.path.replace(rootFolder, "") }) - // log ("file paths : "+this._localPaths) } return this._localPaths; } @@ -852,6 +850,27 @@ Object.defineProperty(Extension.prototype, "installer", { }) +/** + * Whether this extension is a package (queries the repo for files list) + */ +Object.defineProperty(Extension.prototype, "isPackage", { + get: function () { + if (typeof this._isPackage === 'undefined') { + var _files = this.package.localFiles?this.package.localFiles:this.files.map(function(x){return x.path}); + + this._isPackage = false; + for (var i in _files){ + if (_files[i].indexOf("configure.js") != -1) { + this._isPackage = true; + break + } + } + } + return this._isPackage; + } +}) + + /** * Cleans the problematic characters from the name of the extension. */ @@ -992,7 +1011,7 @@ Object.defineProperty(LocalExtensionList.prototype, "list", { * gets the install location for a given extension */ LocalExtensionList.prototype.installLocation = function (extension) { - return this.installFolder + (extension.package.isPackage ? "/packages/" + extension.name.replace(" ", "") : "") + return this.installFolder + (extension.isPackage ? "/packages/" + extension.safeName : ""); } @@ -1055,8 +1074,8 @@ LocalExtensionList.prototype.uninstall = function (extension) { var localExtension = this.extensions[extension.id]; // Remove packages recursively as they have a parent directory. - if (extension.package.isPackage) { - var folder = new Dir(this.installFolder + "/packages/" + extension.name.replace(" ", "")); + if (localExtension.isPackage) { + var folder = new Dir(this.installFolder + "/packages/" + localExtension.safeName); this.log.debug("removing folder " + folder.path); if (folder.exists) folder.rmdirs(); } else { diff --git a/ExtensionStore/lib/widgets.js b/ExtensionStore/lib/widgets.js index 1c0f301..a9d0490 100644 --- a/ExtensionStore/lib/widgets.js +++ b/ExtensionStore/lib/widgets.js @@ -40,7 +40,6 @@ DescriptionView.prototype = Object.create(QWebView.prototype) var iconPath = style.ICONS.installed; this.setToolTip(1, "Extension is installed correctly."); var localExtension = localList.extensions[extension.id]; - // log.debug("checking files from "+extension.id, localList.checkFiles(localExtension)); if (localExtension.currentVersionIsOlder(extension.version)) { iconPath = style.ICONS.update; this.setToolTip(1, "Update available:\ncurrently installed version : v" + extension.version); From 085665e385c64717c989febbf001b9b933d3e214 Mon Sep 17 00:00:00 2001 From: bob-ross27 <55351437+bob-ross27@users.noreply.github.com> Date: Thu, 3 Jun 2021 21:16:42 -0300 Subject: [PATCH 052/112] Moved store load button to the LoadButton class. --- ExtensionStore/app.js | 22 +- ExtensionStore/lib/store.js | 25 +- ExtensionStore/lib/style.js | 5 +- ExtensionStore/lib/widgets.js | 25 +- ExtensionStore/resources/store.ui | 667 ++++++++----------- ExtensionStore/resources/stylesheet_dark.qss | 13 +- 6 files changed, 340 insertions(+), 417 deletions(-) diff --git a/ExtensionStore/app.js b/ExtensionStore/app.js index ecabab4..f18043d 100644 --- a/ExtensionStore/app.js +++ b/ExtensionStore/app.js @@ -6,7 +6,7 @@ var widgets = require("./lib/widgets.js"); var appFolder = require("./lib/io.js").appFolder; var DescriptionView = widgets.DescriptionView; var ExtensionItem = widgets.ExtensionItem; -var ProgressButton = widgets.ProgressButton; +var LoadButton = widgets.LoadButton; var InstallButton = widgets.InstallButton; var StyledImage = style.StyledImage; @@ -34,6 +34,10 @@ function StoreUI() { // Set the global application stylesheet this.ui.setStyleSheet(style.getSyleSheet()); + // Create Load Store Button + this.loadStoreButton = new LoadButton(); + this.loadStoreButton.objectName = "loadStoreButton"; + // create shorthand references to some of the main widgets of the ui this.eulaFrame = this.ui.eulaFrame; this.storeFrame = this.ui.storeFrame; @@ -45,6 +49,9 @@ function StoreUI() { this.storeHeader = this.storeFrame.storeHeader; this.storeFooter = this.storeFrame.storeFooter; + // Insert the Loading button + this.aboutFrame.layout().insertWidget(6, this.loadStoreButton, 0, Qt.AlignCenter); + // Hide the store and the loading UI elements. this.storeFrame.hide(); this.setUpdateProgressUIState(false); @@ -90,7 +97,8 @@ function StoreUI() { this.checkForUpdates() // connect UI signals - this.aboutFrame.loadStoreButton.clicked.connect(this, this.loadStore) + this.loadStoreButton.released.connect(this, this.loadStore); + // Social media UI signals this.aboutFrame.twitterButton.clicked.connect(this, function () { QDesktopServices.openUrl(new QUrl(this.aboutFrame.twitterButton.toolTip)); @@ -102,6 +110,9 @@ function StoreUI() { QDesktopServices.openUrl(new QUrl(this.aboutFrame.githubButton.toolTip)); }); + this.store.onLoadProgressChanged.connect(this.aboutFrame.updateProgress, this.aboutFrame.updateProgress.setValue); + this.store.onLoadProgressChanged.connect(this.loadStoreButton, this.loadStoreButton.setProgress); + // filter the store list -------------------------------------------- this.storeHeader.searchStore.textChanged.connect(this, this.updateExtensionsList) @@ -143,7 +154,7 @@ function StoreUI() { this.storeFooter.registerButton.clicked.connect(this, this.registerExtension); // Install Button Actions ------------------------------------------- - this.installButton = new InstallButton("Install"); + this.installButton = new InstallButton(); this.installButton.objectName = "installButton"; this.storeDescriptionPanel.installButtonPlaceHolder.layout().addWidget(this.installButton, 1, Qt.AlignCenter); @@ -190,10 +201,9 @@ StoreUI.prototype.show = function () { * @param {boolean} visible - Determine whether the progress state should be enabled or disabled. */ StoreUI.prototype.setUpdateProgressUIState = function (visible) { - this.aboutFrame.updateButton.visible = !visible; - this.aboutFrame.loadStoreButton.visible = !visible; - this.aboutFrame.updateLabel.visible = visible; this.aboutFrame.updateProgress.visible = visible; + this.aboutFrame.updateButton.visible = !visible; + this.aboutFrame.updateRibbon.storeVersion.visible = !visible; } diff --git a/ExtensionStore/lib/store.js b/ExtensionStore/lib/store.js index 1bb33fb..5e97d96 100644 --- a/ExtensionStore/lib/store.js +++ b/ExtensionStore/lib/store.js @@ -34,8 +34,9 @@ function test() { * The Store class is used to search the github repos for available extensions */ function Store() { - this.log = new Logger("Store") - this.log.info("init store") + this.log = new Logger("Store"); + this.log.info("init store"); + this.onLoadProgressChanged = new Signal(); } @@ -65,11 +66,12 @@ Object.defineProperty(Store.prototype, "sellers", { // handle wrong packages found in sellers list var validSellers = []; - for (var i in sellersList) { + for (var i = 0; i < sellersList.length; i += 1) { try { var seller = new Seller(sellersList[i]); var package = seller.package; - validSellers.push(seller) + validSellers.push(seller); + this.onLoadProgressChanged.emit((i / sellersList.length) * 100); } catch (error) { this.log.error("problem getting package for seller " + sellersList[i], error); } @@ -129,6 +131,7 @@ Object.defineProperty(Store.prototype, "extensions", { } } + this.onLoadProgressChanged.emit(100); return this._extensions; } }) @@ -149,7 +152,6 @@ Object.defineProperty(Store.prototype, "storeExtension", { }) - /** * The contents of the local tbpackage.json file */ @@ -168,7 +170,6 @@ Object.defineProperty(Store.prototype, "localPackage", { }) - // Seller Class ------------------------------------------------ /** * @constructor @@ -623,7 +624,6 @@ Repository.prototype.searchToRe = function (search) { } - // Extension Class --------------------------------------------------- /** * @classdesc @@ -650,6 +650,7 @@ Object.defineProperty(Extension.prototype, "name", { } }) + /** * Get the json package describing this extension. Thanks to this getter setter, we can ensure the package file is complete even with an obsolete json */ @@ -1144,7 +1145,6 @@ LocalExtensionList.prototype.refreshExtensions = function () { } - /** * goes through the store extensions and checks whether it is already installed even if not in the list */ @@ -1182,6 +1182,7 @@ LocalExtensionList.prototype.createListFile = function (store) { return this.list; } + /** * Access the custom settings */ @@ -1203,6 +1204,7 @@ Object.defineProperty(LocalExtensionList.prototype, "settings", { } }) + /** * Saves the specified data to a local file. * @param {string} name @@ -1264,7 +1266,7 @@ ExtensionInstaller.prototype.downloadFiles = function () { this.log.debug("downloading files : "+files.map(function(x){return x.path}).join("\n")) for (var i = 0; i < files.length; i++) { - this.onInstallProgressChanged.emit(i/files.length); + this.onInstallProgressChanged.emit((i/files.length) * 100); try{ webQuery.download(this.getDownloadUrl(files[i].path), destPaths[i]); var dlFile = new File(destPaths[i]); @@ -1281,7 +1283,7 @@ ExtensionInstaller.prototype.downloadFiles = function () { } } - this.onInstallProgressChanged.emit(1); + this.onInstallProgressChanged.emit(100); this.onInstallFinished.emit(dlFiles); } @@ -1303,5 +1305,4 @@ function deepCopy(object) { exports.Store = Store; exports.LocalExtensionList = LocalExtensionList; exports.Seller = Seller; -exports.Repository = Repository; -exports.appFolder = appFolder; \ No newline at end of file +exports.Repository = Repository; \ No newline at end of file diff --git a/ExtensionStore/lib/style.js b/ExtensionStore/lib/style.js index c9852bd..6edc36e 100644 --- a/ExtensionStore/lib/style.js +++ b/ExtensionStore/lib/style.js @@ -1,6 +1,5 @@ var Logger = require("./logger.js").Logger; var io = require("./io.js"); - var log = new Logger("Style"); var appFolder = io.appFolder; @@ -83,12 +82,12 @@ function isDarkStyle() { * style-specific overrides. */ function getSyleSheet() { - var styleFile = storelib.appFolder + "/resources/stylesheet_dark.qss"; + var styleFile = appFolder + "/resources/stylesheet_dark.qss"; var styleSheet = io.readFile(styleFile); // Get light-specific style overriddes if (!isDarkStyle()) { - styleFileLight = storelib.appFolder + "/resources/stylesheet_light.qss"; + styleFileLight = appFolder + "/resources/stylesheet_light.qss"; styleSheet += io.readFile(styleFileLight); } diff --git a/ExtensionStore/lib/widgets.js b/ExtensionStore/lib/widgets.js index 2f137ef..f002a0e 100644 --- a/ExtensionStore/lib/widgets.js +++ b/ExtensionStore/lib/widgets.js @@ -73,7 +73,7 @@ ExtensionItem.prototype = Object.create(QTreeWidgetItem.prototype); /** - * A button that can also show progress + * A button that can show progress by animating the background stylesheet. * @classdesc * @constructor */ @@ -113,15 +113,17 @@ Object.defineProperty(ProgressButton.prototype, "accentColor", { * @param {Int} progress - Value from 0 to 1 that the operation is currently at. */ ProgressButton.prototype.setProgress = function (progress) { + // ProgressBar requires integers, so remap from 0 => 1 for the QLinearGradient. + var progress = progress / 100; + var accentColor = this.accentColor; var backgroundColor = this.backgroundColor; - if (progress < 0) { - // resetting stylesheet by setting accentColor - this.accentColor = accentColor; - this.text = this.defaultText; + // Nothing to do. + if (progress === 0) return; - } else if (progress < 1) { + // Operation in progress + if (progress < 1) { this.enabled = false; this.text = this.progressText; @@ -146,7 +148,7 @@ ProgressButton.prototype.setProgress = function (progress) { this.text = this.mode.progressText + " " + Math.round((progressStopR * 100)) + "%"; } else { - // Configure widget to indicate the download is completed. + // Configure widget to indicate the operation is complete. this.setStyleSheet("QToolButton { border: none; background-color: " + accentColor + "; color: white}"); this.enabled = true; this.text = this.finishedText; @@ -200,7 +202,7 @@ Object.defineProperty(InstallButton.prototype, "mode", { var modeDetails = this.modes[mode] if (!modeDetails) throw new Error ("Can't set InstallButton mode to "+ mode+ ", mode can only be 'INSTALL', 'UNINSTALL' or 'UPDATE'." ) - if (mode != this._mode){ + if (mode !== this._mode){ this._mode = mode; this.accentColor = modeDetails.accentColor; this.progressText = modeDetails.progressText; @@ -218,13 +220,16 @@ Object.defineProperty(InstallButton.prototype, "mode", { */ function LoadButton() { ProgressButton.call(this, style.COLORS.ACCENT_LIGHT, "Load Store", "Loading..."); + this.action = new QAction(this.defaultText, this); + this.setDefaultAction(this.action); + } -InstallButton.prototype = Object.create(ProgressButton.prototype); +LoadButton.prototype = Object.create(ProgressButton.prototype); /** * A Qt like custom signal that can be defined, connected and emitted. - * As this signal is not actually threaded, the connected callbacks will be exectuted + * As this signal is not actually threaded, the connected callbacks will be executed * directly when the signal is emited, and the rest of the code will execute after. * @param {type} type the type of value accepted as argument when calling emit() */ diff --git a/ExtensionStore/resources/store.ui b/ExtensionStore/resources/store.ui index 8c20219..758c448 100644 --- a/ExtensionStore/resources/store.ui +++ b/ExtensionStore/resources/store.ui @@ -6,8 +6,8 @@ 0 0 - 546 - 1060 + 794 + 1053 @@ -110,7 +110,7 @@ 0 0 - 474 + 722 223 @@ -798,11 +798,14 @@ - + 0 0 + + + QFrame::StyledPanel @@ -811,444 +814,303 @@ - 0 + 8 - 5 + 0 0 - 5 + 0 0 - - - 0 + + + Qt::Vertical - - QLayout::SetDefaultConstraint + + + 20 + 40 + - - 0 + + + + + + + 0 + 1 + + + + <html><head/><body><p><span style=" font-size:11pt; font-weight:600;">Harmony Unofficial Extension Store</span></p></body></html> + + + Qt::RichText + + + Qt::AlignCenter + + + + + + + + + + 0 + 0 + + + + + 70 + 0 + + + + + 425 + 70 + + + + false + + + + + + <html><head/><body><p align="center"><span style=" font-size:12pt;">Bringing together enthusiast script makers and users using the power of open source.</span></p></body></html> + + + Qt::AutoText + + + Qt::AlignJustify|Qt::AlignTop + + + true + + + false + + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed - + + + 20 + 30 + + + + + + + 0 0 - - 9 - - + - Qt::Vertical + Qt::Horizontal QSizePolicy::Expanding - 20 - 40 + 40 + 20 - - - - - - 0 - 1 - - - - <html><head/><body><p><span style=" font-size:11pt; font-weight:600;">Harmony Unofficial Extension Store</span></p></body></html> - - - Qt::RichText - - - Qt::AlignCenter - - - - - - - - - - 0 - 0 - - - - - 70 - 0 - - - - - 425 - 70 - - - - false - - - - - - <html><head/><body><p align="center"><span style=" font-size:12pt;">Bringing together enthusiast script makers and users using the power of open source.</span></p></body></html> - - - Qt::AutoText - - - Qt::AlignJustify|Qt::AlignTop - - - true - - - false - - - - - - - - - 0 - - - 0 - - - - - Qt::Horizontal - - - QSizePolicy::Expanding - - - - 40 - 20 - - - - - - - - https://twitter.com/mathieuchaptel - - - twitter - - - - 24 - 24 - - - - true - - - - - - - Qt::Horizontal - - - QSizePolicy::Maximum - - - - 15 - 20 - - - - - - - - https://github.com/mchaptel/ExtensionStore - - - github - - - - 24 - 24 - - - - false - - - - - - - Qt::Horizontal - - - QSizePolicy::Maximum - - - - 15 - 20 - - - - - - - - https://discord.gg/ETNmCWYN - - - discord - - - - 24 - 24 - - - - true - - - - - - - Qt::Horizontal - - - QSizePolicy::Expanding - - - - 40 - 20 - - - - - - - - - - Qt::Vertical - - - QSizePolicy::Maximum - - - - 20 - 15 - - - - - + + + https://twitter.com/mathieuchaptel + + + twitter + + + + 24 + 24 + + + + true + + - + - Qt::Vertical + Qt::Horizontal QSizePolicy::Maximum - 0 - 0 + 15 + 20 - - - - - Qt::Vertical - - - QSizePolicy::Fixed - - - - 20 - 30 - - - - - - - - - - - 0 - 0 - - - - - 120 - 30 - - - - - 120 - 16777215 - - - - Install Update - - - - - - - - - - - - 120 - 30 - - - - - 120 - 16777215 - - - - Load Store - - - - - - - - - <html><head/><body><p><span style=" font-weight:600;">Please Wait - Updating Extension List...</span></p></body></html> - - - Qt::AlignCenter - - - 5 - - - - - - - - - Qt::Horizontal - - - - 20 - 20 - - - - - - - - - 0 - 0 - - - - - 16777215 - 5 - - - - 0 - - - -1 - - - - - - - Qt::Horizontal - - - - 20 - 20 - - - - - - - + + + https://github.com/mchaptel/ExtensionStore + + + github + + + + 24 + 24 + + + + false + + + + + + + Qt::Horizontal + + + QSizePolicy::Maximum + + + + 15 + 20 + + + + + + + + https://discord.gg/ETNmCWYN + + + discord + + + + 24 + 24 + + + + true + + - + - Qt::Vertical + Qt::Horizontal QSizePolicy::Expanding - 20 - 0 + 40 + 20 + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + + + + + + 0 + 0 + + + + + 130 + 30 + + + + + 120 + 16777215 + + + + Install Update + + + + + + + Qt::Vertical + + + QSizePolicy::Expanding + + + + 20 + 40 + + + + @@ -1259,17 +1121,33 @@ - 9 + 0 3 - 9 + 0 3 + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 8 + 20 + + + + @@ -1293,6 +1171,31 @@ + + + + + 0 + 0 + + + + + 16777215 + 5 + + + + 100 + + + -1 + + + false + + + diff --git a/ExtensionStore/resources/stylesheet_dark.qss b/ExtensionStore/resources/stylesheet_dark.qss index b5ed03f..4381ed5 100644 --- a/ExtensionStore/resources/stylesheet_dark.qss +++ b/ExtensionStore/resources/stylesheet_dark.qss @@ -7,7 +7,7 @@ Overall widget styling. /* Only useful if rounding corners and update label fixed. */ QWidget#Form { - background-color: @00DP; + background-color: @01DP; } /* Overall BG color */ @@ -183,7 +183,7 @@ QToolButton:hover#discordButton { /* Push Buttons */ -QPushButton#loadStoreButton { +QToolButton#loadStoreButton { background: @08DP; border-width: 2px; border-style: solid; @@ -191,11 +191,11 @@ QPushButton#loadStoreButton { border-radius: 7px; } -QPushButton:hover#loadStoreButton { +QToolButton:hover#loadStoreButton { border-color: @ACCENT_LIGHT; background: @12DP; } -QPushButton:pressed#loadStoreButton { +QToolButton:pressed#loadStoreButton { background: @02DP; border-color: @ACCENT_PRIMARY; } @@ -212,6 +212,11 @@ QPushButton#updateButton { padding: 5px; color: black; background-color: @YELLOW; + border-width: 2px; + border-style: solid; + border-color: transparent; + border-radius: 7px; + } QPushButton:hover#updateButton { From 5213ccfb5d52f8d8c65ae19d0e4ed35246cb8db8 Mon Sep 17 00:00:00 2001 From: bob-ross27 <55351437+bob-ross27@users.noreply.github.com> Date: Sun, 6 Jun 2021 14:19:48 -0300 Subject: [PATCH 053/112] Added support for dropshadows on widgets. --- ExtensionStore/app.js | 6 ++++++ ExtensionStore/lib/style.js | 32 +++++++++++++++++++++++++++++++ ExtensionStore/lib/widgets.js | 2 +- ExtensionStore/resources/store.ui | 2 +- 4 files changed, 40 insertions(+), 2 deletions(-) diff --git a/ExtensionStore/app.js b/ExtensionStore/app.js index f18043d..5c2e6c7 100644 --- a/ExtensionStore/app.js +++ b/ExtensionStore/app.js @@ -37,6 +37,7 @@ function StoreUI() { // Create Load Store Button this.loadStoreButton = new LoadButton(); this.loadStoreButton.objectName = "loadStoreButton"; + style.addDropShadow(this.loadStoreButton, 10, 0, 8); // create shorthand references to some of the main widgets of the ui this.eulaFrame = this.ui.eulaFrame; @@ -161,6 +162,11 @@ function StoreUI() { this.installButton.modes.INSTALL.action.triggered.connect(this, this.performInstall); this.installButton.modes.UPDATE.action.triggered.connect(this, this.performInstall); this.installButton.modes.UNINSTALL.action.triggered.connect(this, this.performUninstall); + + // Add Dropshadow to buttons. + style.addDropShadow(this.installButton, 10, 0, 8); + style.addDropShadow(this.storeDescriptionPanel.websiteButton); + style.addDropShadow(this.storeDescriptionPanel.sourceButton); } diff --git a/ExtensionStore/lib/style.js b/ExtensionStore/lib/style.js index 6edc36e..ce9c01d 100644 --- a/ExtensionStore/lib/style.js +++ b/ExtensionStore/lib/style.js @@ -128,6 +128,37 @@ function getImage(imagePath) { } +/** + * Add a Dropshadow graphic effect to the provided widget. + * @param {QWidget} widget - Widget to apply the dropshadow to. + * @param {Int} radius - Radius of the blur applied to the dropshadow. + * @param {Int} offsetX - How many pixels to offset the blur in the X coordinate. + * @param {Int} offsetY - How many pixels to offset the blur in the Y coordinate. + * @param {Int} opacity - Opacity from 0 => 255, where 0 is fully transparent. + */ + function addDropShadow(widget, radius, offsetX, offsetY, opacity) { + var radius = radius || 10; + var offsetX = offsetX || 0; + var offsetY = offsetY || 3; + var opacity = opacity || 70; + + var dropShadow = new QGraphicsDropShadowEffect(); + dropShadow.setBlurRadius(radius); + dropShadow.setOffset(offsetX, offsetY); + var shadowColor = new QColor(style.COLORS["00DP"]); + shadowColor.setAlpha(opacity); + dropShadow.setColor(shadowColor); + + // Apply the effect. Catch errors if a widget that doesn't support the setGraphicEffect call is provided. + try { + widget.setGraphicsEffect(dropShadow); + } + catch (err) { + log.debug("Widget doesn't support setting a graphics effect."); + } +} + + function StyledImage(imagePath, width, height, uniformScaling) { if (typeof uniformScaling === 'undefined') var uniformScaling = true; if (typeof width === 'undefined') var width = 0; @@ -189,6 +220,7 @@ StyledImage.prototype.setAsIcon = function(widget, itemColumn){ } } +exports.addDropShadow = addDropShadow; exports.getSyleSheet = getSyleSheet; exports.StyledImage = StyledImage; exports.STYLESHEETS = STYLESHEETS; diff --git a/ExtensionStore/lib/widgets.js b/ExtensionStore/lib/widgets.js index f002a0e..e928008 100644 --- a/ExtensionStore/lib/widgets.js +++ b/ExtensionStore/lib/widgets.js @@ -219,7 +219,7 @@ Object.defineProperty(InstallButton.prototype, "mode", { * @constructor */ function LoadButton() { - ProgressButton.call(this, style.COLORS.ACCENT_LIGHT, "Load Store", "Loading..."); + ProgressButton.call(this, style.COLORS.ACCENT_PRIMARY, "Load Store", "Loading..."); this.action = new QAction(this.defaultText, this); this.setDefaultAction(this.action); diff --git a/ExtensionStore/resources/store.ui b/ExtensionStore/resources/store.ui index 758c448..0df008e 100644 --- a/ExtensionStore/resources/store.ui +++ b/ExtensionStore/resources/store.ui @@ -997,7 +997,7 @@ - false + true From 0df20c7f2a97427c85e8998edc516809ce494ea4 Mon Sep 17 00:00:00 2001 From: bob-ross27 <55351437+bob-ross27@users.noreply.github.com> Date: Mon, 7 Jun 2021 21:04:25 -0300 Subject: [PATCH 054/112] Adjusted EULA layout and added dropshadows. --- ExtensionStore/app.js | 4 + ExtensionStore/lib/style.js | 2 +- ExtensionStore/resources/store.ui | 179 ++++++++++--------- ExtensionStore/resources/stylesheet_dark.qss | 13 +- 4 files changed, 113 insertions(+), 85 deletions(-) diff --git a/ExtensionStore/app.js b/ExtensionStore/app.js index 5c2e6c7..4c6ec89 100644 --- a/ExtensionStore/app.js +++ b/ExtensionStore/app.js @@ -50,6 +50,10 @@ function StoreUI() { this.storeHeader = this.storeFrame.storeHeader; this.storeFooter = this.storeFrame.storeFooter; + // Add a dropshadow to the EULA inner frame. + style.addDropShadow(this.eulaFrame.innerFrame, 10, 10, 10); + style.addDropShadow(this.eulaFrame.innerFrame.textFrame, 5, 5, 5, 50); + // Insert the Loading button this.aboutFrame.layout().insertWidget(6, this.loadStoreButton, 0, Qt.AlignCenter); diff --git a/ExtensionStore/lib/style.js b/ExtensionStore/lib/style.js index ce9c01d..7a28611 100644 --- a/ExtensionStore/lib/style.js +++ b/ExtensionStore/lib/style.js @@ -17,7 +17,7 @@ const ColorsDark = { "16DP": "#363539", "24DP": "#38373B", "ACCENT_LIGHT": "#B6B1D8", // Lighter - 50% white screen overlaid. - "ACCENT_PRIMARY": "#4B3C9E", // Full intensity + "ACCENT_PRIMARY": "#5241B2", // Full intensity "ACCENT_DARK": "#373061", // Subdued - 50% against D1 "ACCENT_BG": "#2B283B", // Very subdued - 20% against D1 "GREEN": "#30D158", // Valid. diff --git a/ExtensionStore/resources/store.ui b/ExtensionStore/resources/store.ui index 0df008e..aeafb5b 100644 --- a/ExtensionStore/resources/store.ui +++ b/ExtensionStore/resources/store.ui @@ -45,16 +45,16 @@ - 25 + 16 - 25 + 16 - 25 + 16 - 25 + 16 @@ -65,6 +65,18 @@ QFrame::Raised + + 9 + + + 9 + + + 9 + + + 9 + @@ -89,91 +101,95 @@ 20 - 20 + 5 - - - QFrame::NoFrame - - - QFrame::Plain - - - true - - - - - 0 - 0 - 722 - 223 - + + + + 3 - - - - - <html><head/><body><p>This extension store is made available for free by enthusiast script makers and users, and is not endorsed or curated by Toon Boom. Extensions available for download on this store are to be used at your own risk.</p><p>The extensions available on this store are open source and are automatically downloaded from verified github accounts. While efforts are made to vet sellers, it is your responsibility to read and understand the source code before you install any extension.</p><p>Questions, concerns or issues can be directed towards the creator using the provided github or website address. We cannot offer support for extensions not working correctly.</p><p>The source for this extension, including a list of all registered sellers, can be viewed here: <br/><a href="https://github.com/mchaptel/ExtensionStore"><span style=" text-decoration: underline; color:#c8c8c8;">https://github.com/mchaptel/ExtensionStore</span></a></p></body></html> - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - - - true - - - true + + 0 + + + 3 + + + 0 + + + + + QFrame::NoFrame + + + QFrame::Plain + + + true + + + + + 0 + 0 + 734 + 228 + + + + + + <html><head/><body><p>This extension store is made available for free by enthusiast script makers and users, and is not endorsed or curated by Toon Boom. Extensions available for download on this store are to be used at your own risk.</p><p>The extensions available on this store are open source and are automatically downloaded from verified github accounts. While efforts are made to vet sellers, it is your responsibility to read and understand the source code before you install any extension.</p><p>Questions, concerns or issues can be directed towards the creator using the provided github or website address. We cannot offer support for extensions not working correctly.</p><p>The source for this extension, including a list of all registered sellers, can be viewed here: <br/><a href="https://github.com/mchaptel/ExtensionStore"><span style=" text-decoration: underline; color:#c8c8c8;">https://github.com/mchaptel/ExtensionStore</span></a></p></body></html> + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + true + + + + - - - + + + - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - I accept - - - false - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 5 + + + + + + + + I accept + + + false + + @@ -814,7 +830,7 @@ - 8 + 4 0 @@ -862,6 +878,9 @@ + + 4 + @@ -918,7 +937,7 @@ 20 - 30 + 15 diff --git a/ExtensionStore/resources/stylesheet_dark.qss b/ExtensionStore/resources/stylesheet_dark.qss index 4381ed5..d50e143 100644 --- a/ExtensionStore/resources/stylesheet_dark.qss +++ b/ExtensionStore/resources/stylesheet_dark.qss @@ -101,19 +101,24 @@ QFrame#eulaFrame { } QFrame#innerFrame { + background-color: @03DP; + border-radius:8px; +} + +QFrame#textFrame { background-color: @06DP; - border-radius:10px; + border-radius:8px; } /* Scrollable region base widget */ QScrollArea#scrollArea_2 { - margin-left: 5px; - margin-right: 5px; + background-color: transparent; + } /* About screen scrollable region (text background). */ QWidget#scrollAreaWidgetContents_2 { - background-color: @03DP; + background-color: transparent; } /* About Screen text */ From 11706ce95b209bd2cdeb8b88ce7ae779455ecaf7 Mon Sep 17 00:00:00 2001 From: bob-ross27 <55351437+bob-ross27@users.noreply.github.com> Date: Mon, 7 Jun 2021 21:06:42 -0300 Subject: [PATCH 055/112] Cropped logo, updated resize calls to match proportionally. --- ExtensionStore/app.js | 4 ++-- ExtensionStore/resources/logo.png | Bin 33408 -> 32085 bytes 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ExtensionStore/app.js b/ExtensionStore/app.js index 4c6ec89..cf242a6 100644 --- a/ExtensionStore/app.js +++ b/ExtensionStore/app.js @@ -65,7 +65,7 @@ function StoreUI() { this.aboutFrame.hide(); // EULA logo - var eulaLogo = new StyledImage(appFolder + "/resources/logo.png", 800, 140) + var eulaLogo = new StyledImage(appFolder + "/resources/logo.png", 380, 120); this.eulaFrame.innerFrame.eulaLogo.setPixmap(eulaLogo.pixmap); this.eulaFrame.innerFrame.eulaCB.stateChanged.connect(this, function () { @@ -79,7 +79,7 @@ function StoreUI() { } // About logo - var logo = new StyledImage(appFolder + "/resources/logo.png", 800, 140); + var logo = new StyledImage(appFolder + "/resources/logo.png", 380, 120); this.aboutFrame.storeLabel.setPixmap(logo.pixmap); // Social media buttons diff --git a/ExtensionStore/resources/logo.png b/ExtensionStore/resources/logo.png index 1705eaaaef0c79a3f95d1b1a839b02b4811ace11..c94dfc2eb6ad61550e4d857120f5cd8f7bbb3ed7 100644 GIT binary patch literal 32085 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 LiSl3m`13yi)d_+~ literal 33408 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 Y3LehBhVu6`=>J9rtLcHtpWD6pAA;bHDgXcg From 65dc98393826952e4bf1cd3585e9d62fe689810d Mon Sep 17 00:00:00 2001 From: bob-ross27 <55351437+bob-ross27@users.noreply.github.com> Date: Mon, 7 Jun 2021 20:52:33 -0300 Subject: [PATCH 056/112] Created and updated comments and formatting. Changed optional parameters to match project style. --- ExtensionStore/lib/style.js | 55 +++++++++++++++----- ExtensionStore/resources/stylesheet_dark.qss | 29 +++++++---- 2 files changed, 61 insertions(+), 23 deletions(-) diff --git a/ExtensionStore/lib/style.js b/ExtensionStore/lib/style.js index 7a28611..97ebebb 100644 --- a/ExtensionStore/lib/style.js +++ b/ExtensionStore/lib/style.js @@ -17,22 +17,24 @@ const ColorsDark = { "16DP": "#363539", "24DP": "#38373B", "ACCENT_LIGHT": "#B6B1D8", // Lighter - 50% white screen overlaid. - "ACCENT_PRIMARY": "#5241B2", // Full intensity - "ACCENT_DARK": "#373061", // Subdued - 50% against D1 - "ACCENT_BG": "#2B283B", // Very subdued - 20% against D1 + "ACCENT_PRIMARY": "#5241B2", // Full intensity. + "ACCENT_DARK": "#373061", // Subdued - 50% against 01DP. + "ACCENT_BG": "#2B283B", // Very subdued - 20% against 01DP. "GREEN": "#30D158", // Valid. - "RED": "#FF453A", // Error - "YELLOW": "#FFD60A", // New or updated - "ORANGE": "#FF9F0A", // Notice. - "BLUE": "#A1CBEC", // Store update. + "RED": "#FF453A", // Error. + "YELLOW": "#FFD60A", // New or updated. + "ORANGE": "#FF9F0A", // Warning. + "BLUE": "#A1CBEC", } // Enum to hold light style palette. // TODO: Make dedicated light theme and associated palette. const ColorsLight = ColorsDark; +// Use the appropriate colors const for further lookups. const COLORS = isDarkStyle() ? ColorsDark : ColorsLight; +// Enum to hold light style palette. const styleSheetsDark = { defaultRibbon : "QWidget { background-color: transparent; color: gray;}", updateRibbon : "QWidget { background-color: " + COLORS.YELLOW + "; color: black }", @@ -44,11 +46,15 @@ const styleSheetsDark = { loadButton : "QToolButton { border-color: transparent transparent " + COLORS.ACCENT_LIGHT + " transparent; }", } +// Enum to hold light style stylesheets. +// TODO: Make dedicated light theme and associated stylesheets. const styleSheetsLight = styleSheetsDark; +// Use the appropriate stylesheet const for further lookups. const STYLESHEETS = isDarkStyle() ? styleSheetsDark : styleSheetsLight; - +// Enum to hold application icons. Automatically return the appropriately themed +// image by calling getImage. var iconFolder = appFolder + "/resources/icons"; const ICONS = { // Store @@ -80,6 +86,7 @@ function isDarkStyle() { /** * Build and return a final qss string. Incorporate all necessary * style-specific overrides. + * @returns {String} Resulting stylesheet based on the Application theme. */ function getSyleSheet() { var styleFile = appFolder + "/resources/stylesheet_dark.qss"; @@ -104,7 +111,7 @@ function getSyleSheet() { /** * Return the appropriate image path based on Harmony style. - * @param {String} imagePath + * @param {String} imagePath - Path to the image to be evaluated. * @returns {String} Path to the correct image for the Harmony style. */ function getImage(imagePath) { @@ -137,10 +144,10 @@ function getImage(imagePath) { * @param {Int} opacity - Opacity from 0 => 255, where 0 is fully transparent. */ function addDropShadow(widget, radius, offsetX, offsetY, opacity) { - var radius = radius || 10; - var offsetX = offsetX || 0; - var offsetY = offsetY || 3; - var opacity = opacity || 70; + if (typeof radius === 'undefined') var radius = 10; + if (typeof offsetX === 'undefined') var offsetX = 0; + if (typeof offsetY === 'undefined') var offsetY = 3; + if (typeof opacity === 'undefined') var opacity = 70; var dropShadow = new QGraphicsDropShadowEffect(); dropShadow.setBlurRadius(radius); @@ -159,6 +166,16 @@ function getImage(imagePath) { } +/** + * Class to handle the creation of Pixmaps, including suppoort for automatically + * returning the correctly themed image, scaling Pixmaps if necessary, and applying onto + * widgets as QIcons.\ + * @class + * @param {String} imagePath - Path to the image. + * @param {Int} width - Width in display pixels. + * @param {Int} height - Height in display pixels. + * @param {Boolean} uniformScaling - Whether to maintain the original aspect ratio when scaling. + */ function StyledImage(imagePath, width, height, uniformScaling) { if (typeof uniformScaling === 'undefined') var uniformScaling = true; if (typeof width === 'undefined') var width = 0; @@ -173,6 +190,9 @@ function StyledImage(imagePath, width, height, uniformScaling) { } +/** + * Filesystem path to the image - remapped to the appropriate theme. + */ Object.defineProperty(StyledImage.prototype, "path", { get: function(){ return this.getImage(this.basePath); @@ -180,6 +200,9 @@ Object.defineProperty(StyledImage.prototype, "path", { }) +/** + * Create a new Pixmap for the image, scaling if necessary. + */ Object.defineProperty(StyledImage.prototype, "pixmap", { get: function(){ if (typeof this._pixmap === 'undefined') { @@ -209,6 +232,11 @@ Object.defineProperty(StyledImage.prototype, "pixmap", { }) +/** + * Apply the image to a widget. + * @param {QWidget} widget - The widget the image should be applied to as an icon. + * @param {Int} itemColumn - Index of the column the icon should be applied to, if the widget is a QTreeWidgetItem. + */ StyledImage.prototype.setAsIcon = function(widget, itemColumn){ if (widget instanceof QTreeWidgetItem){ if (typeof itemColumn === 'undefined') var itemColumn = 0; @@ -220,6 +248,7 @@ StyledImage.prototype.setAsIcon = function(widget, itemColumn){ } } + exports.addDropShadow = addDropShadow; exports.getSyleSheet = getSyleSheet; exports.StyledImage = StyledImage; diff --git a/ExtensionStore/resources/stylesheet_dark.qss b/ExtensionStore/resources/stylesheet_dark.qss index d50e143..406306a 100644 --- a/ExtensionStore/resources/stylesheet_dark.qss +++ b/ExtensionStore/resources/stylesheet_dark.qss @@ -5,7 +5,7 @@ Overall widget styling. ====================== */ -/* Only useful if rounding corners and update label fixed. */ +/* Base widget of the entire UI */ QWidget#Form { background-color: @01DP; } @@ -15,7 +15,7 @@ QFrame{ background-color: @01DP; } -/* Set labels to transparent for text and logos. */ +/* Set labels to transparent for text and logos */ QLabel{ background-color: transparent; } @@ -96,32 +96,35 @@ EULA Frame ====================== */ +/* Exterior frame */ QFrame#eulaFrame { background-color: @01DP; } +/* Elevated inner frame */ QFrame#innerFrame { background-color: @03DP; border-radius:8px; } +/* Elevated EULA Text container */ QFrame#textFrame { background-color: @06DP; border-radius:8px; } -/* Scrollable region base widget */ +/* Scrollable EULA text region base widget */ QScrollArea#scrollArea_2 { background-color: transparent; } -/* About screen scrollable region (text background). */ +/* EULA screen scrollable region (text background). */ QWidget#scrollAreaWidgetContents_2 { background-color: transparent; } -/* About Screen text */ +/* EULA Screen text */ QLabel#eulaText { background-color: transparent; margin: 5px; @@ -129,7 +132,7 @@ QLabel#eulaText { font-size: 11pt; } -/* EULA checkbox */ +/* EULA agreement checkbox */ QCheckBox#eulaCB { font-family: Arial; font-size: 13pt; @@ -186,8 +189,7 @@ QToolButton:hover#discordButton { border-color: @ACCENT_LIGHT; } - -/* Push Buttons */ +/* Load Store Button */ QToolButton#loadStoreButton { background: @08DP; border-width: 2px; @@ -206,7 +208,7 @@ QToolButton:pressed#loadStoreButton { } -/* Update Ribbon */ +/* Update Ribbon store version text */ QLabel#storeVersion { font-family: Arial; font-size: 10pt; @@ -233,6 +235,7 @@ QPushButton:hover#updateButton { Store Frame ====================== */ + /* Store Frame */ QFrame#storeFrame { background-color: @12DP; @@ -243,6 +246,7 @@ QFrame#storeFrame { Store Header ====================== */ + /* Store header */ QFrame#storeHeader { background-color: @08DP; @@ -303,7 +307,7 @@ QFrame#storeFooter { border-color: @01DP transparent transparent transparent; } -/* Register button */ +/* Register new extension button */ QPushButton#registerButton { border-radius: 2px; border-width: 2px; @@ -314,17 +318,21 @@ QPushButton#registerButton { QPushButton:hover#registerButton { background-color: @08DP; } + /* ====================== Store Sellers ====================== */ +/* Sellers base tree widgert */ QTreeView { background-color: @01DP; border: 0px; outline: none; } + +/* Item within the base sellers tree widget */ QTreeView::item { color: white; background-color: @01DP; @@ -351,6 +359,7 @@ QTreeView::item:selected { ====================== */ +/* Sliding description panel background */ QFrame#sidepanelFrame { background-color: @04DP; } From 01f0b56457ae8bdea2b5f16e18419ea4b0f0f38e Mon Sep 17 00:00:00 2001 From: bob-ross27 <55351437+bob-ross27@users.noreply.github.com> Date: Mon, 7 Jun 2021 22:06:05 -0300 Subject: [PATCH 057/112] Updated the About screen layout. --- ExtensionStore/app.js | 3 +++ ExtensionStore/resources/store.ui | 14 +++++++------- ExtensionStore/resources/stylesheet_dark.qss | 8 +++++--- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/ExtensionStore/app.js b/ExtensionStore/app.js index cf242a6..2336408 100644 --- a/ExtensionStore/app.js +++ b/ExtensionStore/app.js @@ -54,6 +54,9 @@ function StoreUI() { style.addDropShadow(this.eulaFrame.innerFrame, 10, 10, 10); style.addDropShadow(this.eulaFrame.innerFrame.textFrame, 5, 5, 5, 50); + // Add a light dropshadow to the about screen text - to shadow the bottom border. + style.addDropShadow(this.aboutFrame.label_3, 5, 5, 5, 25); + // Insert the Loading button this.aboutFrame.layout().insertWidget(6, this.loadStoreButton, 0, Qt.AlignCenter); diff --git a/ExtensionStore/resources/store.ui b/ExtensionStore/resources/store.ui index aeafb5b..ab3a44b 100644 --- a/ExtensionStore/resources/store.ui +++ b/ExtensionStore/resources/store.ui @@ -7,7 +7,7 @@ 0 0 794 - 1053 + 1056 @@ -138,7 +138,7 @@ 0 0 734 - 228 + 229 @@ -852,7 +852,7 @@ 20 - 40 + 5 @@ -897,7 +897,7 @@ - 425 + 350 70 @@ -908,7 +908,7 @@ - <html><head/><body><p align="center"><span style=" font-size:12pt;">Bringing together enthusiast script makers and users using the power of open source.</span></p></body></html> + <html><head/><body><p align="center"><span style=" font-size:11pt;">Bringing together enthusiast script makers and users through the power of open source.</span></p></body></html> Qt::AutoText @@ -937,7 +937,7 @@ 20 - 15 + 25 @@ -1125,7 +1125,7 @@ 20 - 40 + 5 diff --git a/ExtensionStore/resources/stylesheet_dark.qss b/ExtensionStore/resources/stylesheet_dark.qss index 406306a..698dc2c 100644 --- a/ExtensionStore/resources/stylesheet_dark.qss +++ b/ExtensionStore/resources/stylesheet_dark.qss @@ -149,13 +149,15 @@ About Frame /* About Screen text */ QLabel#label_3 { background-color: transparent; - font-family: Arial; + font-family: Palatino; font-size: 12pt; - margin-top: 15px; + padding-bottom: 7px; + border-width: 2px; + border-style: solid; + border-color: transparent transparent @04DP transparent; } /* Social Media */ - /* Twitter */ QToolButton#twitterButton { background-color: transparent; From e8bd910434fe4b4b03d51b9501b619d8e5e42ca7 Mon Sep 17 00:00:00 2001 From: bob-ross27 <55351437+bob-ross27@users.noreply.github.com> Date: Mon, 7 Jun 2021 22:37:16 -0300 Subject: [PATCH 058/112] Slightly adjusted EULA wording and reduced text size 1pt. --- ExtensionStore/resources/store.ui | 2 +- ExtensionStore/resources/stylesheet_dark.qss | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ExtensionStore/resources/store.ui b/ExtensionStore/resources/store.ui index ab3a44b..3814784 100644 --- a/ExtensionStore/resources/store.ui +++ b/ExtensionStore/resources/store.ui @@ -145,7 +145,7 @@ - <html><head/><body><p>This extension store is made available for free by enthusiast script makers and users, and is not endorsed or curated by Toon Boom. Extensions available for download on this store are to be used at your own risk.</p><p>The extensions available on this store are open source and are automatically downloaded from verified github accounts. While efforts are made to vet sellers, it is your responsibility to read and understand the source code before you install any extension.</p><p>Questions, concerns or issues can be directed towards the creator using the provided github or website address. We cannot offer support for extensions not working correctly.</p><p>The source for this extension, including a list of all registered sellers, can be viewed here: <br/><a href="https://github.com/mchaptel/ExtensionStore"><span style=" text-decoration: underline; color:#c8c8c8;">https://github.com/mchaptel/ExtensionStore</span></a></p></body></html> + <html><head/><body><p>This extension store is made available for free by enthusiast script makers, and is not endorsed or curated by Toon Boom. Extensions available for download on this store are to be used at your own risk.</p><p>The extensions available on this store are open source and will be automatically downloaded from verified GitHub accounts. While efforts are made to vet sellers, it is your responsibility to read and understand the source code before you install any extension.</p><p>Questions, concerns or issues may be directed towards the creator using the provided GitHub or website address. We cannot offer support for extensions not working correctly.</p><p>The source for this extension, including a list of all registered sellers, can be viewed here: <br/><a href="https://github.com/mchaptel/ExtensionStore"><span style=" text-decoration: underline; color:#c8c8c8;">https://github.com/mchaptel/ExtensionStore</span></a></p></body></html> Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop diff --git a/ExtensionStore/resources/stylesheet_dark.qss b/ExtensionStore/resources/stylesheet_dark.qss index 698dc2c..7d00e10 100644 --- a/ExtensionStore/resources/stylesheet_dark.qss +++ b/ExtensionStore/resources/stylesheet_dark.qss @@ -129,7 +129,7 @@ QLabel#eulaText { background-color: transparent; margin: 5px; font-family: Arial; - font-size: 11pt; + font-size: 10pt; } /* EULA agreement checkbox */ From 6dfcd9a36003c6c93edfae352e30a1a0242e01e1 Mon Sep 17 00:00:00 2001 From: bob-ross27 <55351437+bob-ross27@users.noreply.github.com> Date: Wed, 9 Jun 2021 00:12:46 -0300 Subject: [PATCH 059/112] Subclassed progressbar to move progress rescaling, and replaced UI bar. --- ExtensionStore/app.js | 17 ++++++++-- ExtensionStore/lib/store.js | 8 ++--- ExtensionStore/lib/widgets.js | 55 ++++++++++++++++++++++++++++--- ExtensionStore/resources/store.ui | 25 -------------- 4 files changed, 68 insertions(+), 37 deletions(-) diff --git a/ExtensionStore/app.js b/ExtensionStore/app.js index 2336408..fc4ae26 100644 --- a/ExtensionStore/app.js +++ b/ExtensionStore/app.js @@ -8,6 +8,7 @@ var DescriptionView = widgets.DescriptionView; var ExtensionItem = widgets.ExtensionItem; var LoadButton = widgets.LoadButton; var InstallButton = widgets.InstallButton; +var ProgressBar = widgets.ProgressBar; var StyledImage = style.StyledImage; var log = new Logger("UI"); @@ -39,6 +40,10 @@ function StoreUI() { this.loadStoreButton.objectName = "loadStoreButton"; style.addDropShadow(this.loadStoreButton, 10, 0, 8); + // Create progressbar + this.updateProgress = new ProgressBar(); + this.updateProgress.objectName = "updateProgress"; + // create shorthand references to some of the main widgets of the ui this.eulaFrame = this.ui.eulaFrame; this.storeFrame = this.ui.storeFrame; @@ -57,9 +62,15 @@ function StoreUI() { // Add a light dropshadow to the about screen text - to shadow the bottom border. style.addDropShadow(this.aboutFrame.label_3, 5, 5, 5, 25); - // Insert the Loading button + // Add a dropshadow to the Update store button. + style.addDropShadow(this.aboutFrame.updateButton, 5, 5, 5, 50); + + // Insert the Loading button. this.aboutFrame.layout().insertWidget(6, this.loadStoreButton, 0, Qt.AlignCenter); + // Insert the progress bar. + this.aboutFrame.layout().insertWidget(10, this.updateProgress, 0, 0); + // Hide the store and the loading UI elements. this.storeFrame.hide(); this.setUpdateProgressUIState(false); @@ -118,7 +129,7 @@ function StoreUI() { QDesktopServices.openUrl(new QUrl(this.aboutFrame.githubButton.toolTip)); }); - this.store.onLoadProgressChanged.connect(this.aboutFrame.updateProgress, this.aboutFrame.updateProgress.setValue); + this.store.onLoadProgressChanged.connect(this.updateProgress, this.updateProgress.setProgress); this.store.onLoadProgressChanged.connect(this.loadStoreButton, this.loadStoreButton.setProgress); // filter the store list -------------------------------------------- @@ -214,7 +225,7 @@ StoreUI.prototype.show = function () { * @param {boolean} visible - Determine whether the progress state should be enabled or disabled. */ StoreUI.prototype.setUpdateProgressUIState = function (visible) { - this.aboutFrame.updateProgress.visible = visible; + this.updateProgress.visible = visible; this.aboutFrame.updateButton.visible = !visible; this.aboutFrame.updateRibbon.storeVersion.visible = !visible; } diff --git a/ExtensionStore/lib/store.js b/ExtensionStore/lib/store.js index 5e97d96..f3e4d3f 100644 --- a/ExtensionStore/lib/store.js +++ b/ExtensionStore/lib/store.js @@ -71,7 +71,7 @@ Object.defineProperty(Store.prototype, "sellers", { var seller = new Seller(sellersList[i]); var package = seller.package; validSellers.push(seller); - this.onLoadProgressChanged.emit((i / sellersList.length) * 100); + this.onLoadProgressChanged.emit(i / sellersList.length); } catch (error) { this.log.error("problem getting package for seller " + sellersList[i], error); } @@ -131,7 +131,7 @@ Object.defineProperty(Store.prototype, "extensions", { } } - this.onLoadProgressChanged.emit(100); + this.onLoadProgressChanged.emit(1); return this._extensions; } }) @@ -1266,7 +1266,7 @@ ExtensionInstaller.prototype.downloadFiles = function () { this.log.debug("downloading files : "+files.map(function(x){return x.path}).join("\n")) for (var i = 0; i < files.length; i++) { - this.onInstallProgressChanged.emit((i/files.length) * 100); + this.onInstallProgressChanged.emit(i/files.length); try{ webQuery.download(this.getDownloadUrl(files[i].path), destPaths[i]); var dlFile = new File(destPaths[i]); @@ -1283,7 +1283,7 @@ ExtensionInstaller.prototype.downloadFiles = function () { } } - this.onInstallProgressChanged.emit(100); + this.onInstallProgressChanged.emit(1); this.onInstallFinished.emit(dlFiles); } diff --git a/ExtensionStore/lib/widgets.js b/ExtensionStore/lib/widgets.js index e928008..dee1743 100644 --- a/ExtensionStore/lib/widgets.js +++ b/ExtensionStore/lib/widgets.js @@ -20,6 +20,7 @@ var log = new Logger("Widgets"); } DescriptionView.prototype = Object.create(QWebView.prototype) + /** * The QTreeWidgetTtem that represents a single extension in the store list. * @classdesc @@ -113,9 +114,6 @@ Object.defineProperty(ProgressButton.prototype, "accentColor", { * @param {Int} progress - Value from 0 to 1 that the operation is currently at. */ ProgressButton.prototype.setProgress = function (progress) { - // ProgressBar requires integers, so remap from 0 => 1 for the QLinearGradient. - var progress = progress / 100; - var accentColor = this.accentColor; var backgroundColor = this.backgroundColor; @@ -156,7 +154,6 @@ ProgressButton.prototype.setProgress = function (progress) { } - /** * ProgressButton child class for Extension installation, uninstallation and updates. * @classdesc @@ -239,6 +236,12 @@ LoadButton.prototype = Object.create(ProgressButton.prototype); this.blocked = false; } + +/** + * Register the calling object and the slot. + * @param {object} context + * @param {function} slot + */ Signal.prototype.connect = function (context, slot){ // support slot.connect(callback) synthax if (typeof slot === 'undefined'){ @@ -248,6 +251,11 @@ Signal.prototype.connect = function (context, slot){ this.connexions.push ({context: context, slot:slot}); } + +/** + * Remove a connection registered with this Signal. + * @param {function} slot + */ Signal.prototype.disconnect = function(slot){ if (typeof slot === "undefined"){ this.connexions = []; @@ -261,6 +269,10 @@ Signal.prototype.disconnect = function(slot){ } } + +/** + * Call the slot function using the provided context and and any arguments. + */ Signal.prototype.emit = function () { if (this.blocked) return; @@ -289,13 +301,46 @@ Signal.prototype.emit = function () { } } + Signal.prototype.toString = function(){ return "Signal"; } + +/** + * Child class QProgressBar to remap the number range used by ProgressButton's QLinearGradient + * into the range used by the QProgressBar. + */ +function ProgressBar() { + QProgressBar.call(this); + + // Exclusively using an input percentage from 0 => 100. + this.value = 0; + this.maximum = 100; + + // Set the default geometry. + this.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding); + this.maximumHeight = 5; + + // Hide progress text. + this.textVisible = false; +} +ProgressBar.prototype = Object.create(QProgressBar.prototype); + + +/** + * Transform the input value and update the progress bar. + * @param {number} value - Progress as a percentage with a range of 0 => 1 . + */ +ProgressBar.prototype.setProgress = function(value) { + this.setValue(value * 100); +} + + exports.Signal = Signal; exports.ProgressButton = ProgressButton; exports.LoadButton = LoadButton; exports.InstallButton = InstallButton; exports.DescriptionView = DescriptionView; -exports.ExtensionItem = ExtensionItem; \ No newline at end of file +exports.ExtensionItem = ExtensionItem; +exports.ProgressBar = ProgressBar; \ No newline at end of file diff --git a/ExtensionStore/resources/store.ui b/ExtensionStore/resources/store.ui index 3814784..979ed8b 100644 --- a/ExtensionStore/resources/store.ui +++ b/ExtensionStore/resources/store.ui @@ -1190,31 +1190,6 @@ - - - - - 0 - 0 - - - - - 16777215 - 5 - - - - 100 - - - -1 - - - false - - - From eca4e268e716f256a95e57c1ef657533bb5b8c96 Mon Sep 17 00:00:00 2001 From: MathieuC Date: Fri, 11 Jun 2021 21:49:30 +0200 Subject: [PATCH 060/112] fix logger displaying debugs when it shouldn't --- .gitignore | 2 ++ ExtensionStore/lib/io.js | 1 - ExtensionStore/lib/store.js | 1 - 3 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ec6e2f2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# vscode +.qt_for_python diff --git a/ExtensionStore/lib/io.js b/ExtensionStore/lib/io.js index 9bb6661..46fbc2b 100644 --- a/ExtensionStore/lib/io.js +++ b/ExtensionStore/lib/io.js @@ -84,7 +84,6 @@ function recursiveFileCopy(folder, destination) { // returns the folder of this file var appFolder = __file__.split("/").slice(0, -2).join("/"); -if (appFolder.indexOf("repo") == -1) Logger.level = 1; // disable logging if extension isn't in a repository exports.listFiles = listFiles diff --git a/ExtensionStore/lib/store.js b/ExtensionStore/lib/store.js index ba777a6..0cf62a5 100644 --- a/ExtensionStore/lib/store.js +++ b/ExtensionStore/lib/store.js @@ -8,7 +8,6 @@ var writeFile = io.writeFile; var recursiveFileCopy = io.recursiveFileCopy; var appFolder = io.appFolder; -Logger.level = 2; function test() { var store = new Store() From 6dd1e49a3890af4e7853d5b315ff03202619c77a Mon Sep 17 00:00:00 2001 From: MathieuC Date: Fri, 11 Jun 2021 21:50:34 +0200 Subject: [PATCH 061/112] prevent installer signals from firing more than once --- ExtensionStore/lib/store.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/ExtensionStore/lib/store.js b/ExtensionStore/lib/store.js index 0cf62a5..73561a4 100644 --- a/ExtensionStore/lib/store.js +++ b/ExtensionStore/lib/store.js @@ -940,7 +940,6 @@ function LocalExtensionList(store) { this._installFolder = specialFolders.userScripts; // default install folder, can be modified with installFolder property this._listFile = specialFolders.userConfig + "/.extensionsList"; this._ini = specialFolders.userConfig + "/.extensionStorePrefs" - // if (this.list.length == 0) this.createListFile(store); // initialize the list file that contains the extensions (!heavy! CBB: do it at a different time?) } @@ -1042,7 +1041,6 @@ LocalExtensionList.prototype.checkFiles = function (extension) { /** * Installs the extension - * @returns {ExtensionInstaller} the installer instance */ LocalExtensionList.prototype.install = function (extension) { var installer = extension.installer; // dedicated object to implement threaded download later @@ -1055,12 +1053,11 @@ LocalExtensionList.prototype.install = function (extension) { recursiveFileCopy(tempFolder, installLocation); this.addToList(extension); // create a record of this installation this.log.debug("adding to list "+extension); + delete extension._installer; } installer.onInstallFinished.connect(this, copyFiles) installer.downloadFiles(); - - return installer; } From 4551d58dec1528e594460d23d4fe4e20d6dc4d9f Mon Sep 17 00:00:00 2001 From: MathieuC Date: Fri, 11 Jun 2021 21:50:55 +0200 Subject: [PATCH 062/112] add a small delay before switching the button aspect --- ExtensionStore/app.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/ExtensionStore/app.js b/ExtensionStore/app.js index fc4ae26..2e5ac31 100644 --- a/ExtensionStore/app.js +++ b/ExtensionStore/app.js @@ -532,8 +532,15 @@ StoreUI.prototype.performInstall = function () { this.localList.install(extension); this.localList.refreshExtensions(); - this.updateExtensionsList(); - this.updateDescriptionPanel(); + + // delay refresh after install completes + var timer = new QTimer(); + timer.singleShot = true; + timer["timeout"].connect(this, function() { + this.updateExtensionsList(); + this.updateDescriptionPanel(); + }); + timer.start(700); } From 6aea48f8b83f9df3faf5aac277f48a9c5ec16286 Mon Sep 17 00:00:00 2001 From: MathieuC Date: Sat, 12 Jun 2021 02:21:07 +0200 Subject: [PATCH 063/112] add SocialButton widget --- ExtensionStore/lib/widgets.js | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/ExtensionStore/lib/widgets.js b/ExtensionStore/lib/widgets.js index 777c836..a92ff64 100644 --- a/ExtensionStore/lib/widgets.js +++ b/ExtensionStore/lib/widgets.js @@ -142,7 +142,7 @@ ProgressButton.prototype.setProgress = function (progress) { this.setStyleSheet(progressStyleSheet); // Update text with progress - this.text = this.mode.progressText + " " + Math.round((progressStopR * 100)) + "%"; + this.text = this.progressText + " " + Math.round((progressStopR * 100)) + "%"; } else { // Configure widget to indicate the operation is complete. @@ -223,6 +223,29 @@ function LoadButton() { LoadButton.prototype = Object.create(ProgressButton.prototype); +/** + * A simple button to display a social media link + * @param {string} url + */ +function SocialButton(url){ + QToolButton.call(this); + this.toolTip = url; + + this.maximumHeight = this.maximumWidth = UiLoader.dpiScale(24); + + // shadows seem to accumulate? leaving this in the hope to fix it later + // style.addDropShadow(this); + + var icon = new WebIcon(url); + icon.setToWidget(this); + + this.clicked.connect(this, function(){ + QDesktopServices.openUrl(new QUrl(url)); + }) +} +SocialButton.prototype = Object.create(QToolButton.prototype) + + /** * A Qt like custom signal that can be defined, connected and emitted. * As this signal is not actually threaded, the connected callbacks will be executed @@ -342,4 +365,5 @@ exports.LoadButton = LoadButton; exports.InstallButton = InstallButton; exports.DescriptionView = DescriptionView; exports.ExtensionItem = ExtensionItem; -exports.ProgressBar = ProgressBar; \ No newline at end of file +exports.ProgressBar = ProgressBar; +exports.SocialButton = SocialButton; \ No newline at end of file From c79c8731634ad9b833ee6e6b0f79eee1d41a0049 Mon Sep 17 00:00:00 2001 From: MathieuC Date: Sat, 12 Jun 2021 02:22:03 +0200 Subject: [PATCH 064/112] size shadow with dpi, set widget as shadow parent --- ExtensionStore/lib/style.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ExtensionStore/lib/style.js b/ExtensionStore/lib/style.js index 97ebebb..bf23dc7 100644 --- a/ExtensionStore/lib/style.js +++ b/ExtensionStore/lib/style.js @@ -140,7 +140,7 @@ function getImage(imagePath) { * @param {QWidget} widget - Widget to apply the dropshadow to. * @param {Int} radius - Radius of the blur applied to the dropshadow. * @param {Int} offsetX - How many pixels to offset the blur in the X coordinate. - * @param {Int} offsetY - How many pixels to offset the blur in the Y coordinate. + * @param {Int} offsetY - How many pixels to offset the blur in the Y coordinate. * @param {Int} opacity - Opacity from 0 => 255, where 0 is fully transparent. */ function addDropShadow(widget, radius, offsetX, offsetY, opacity) { @@ -149,9 +149,10 @@ function getImage(imagePath) { if (typeof offsetY === 'undefined') var offsetY = 3; if (typeof opacity === 'undefined') var opacity = 70; - var dropShadow = new QGraphicsDropShadowEffect(); - dropShadow.setBlurRadius(radius); - dropShadow.setOffset(offsetX, offsetY); + var dropShadow = new QGraphicsDropShadowEffect(widget); + dropShadow.setBlurRadius(UiLoader.dpiScale(radius)); + dropShadow.setOffset(UiLoader.dpiScale(offsetX), UiLoader.dpiScale(offsetY)); + var shadowColor = new QColor(style.COLORS["00DP"]); shadowColor.setAlpha(opacity); dropShadow.setColor(shadowColor); @@ -171,7 +172,7 @@ function getImage(imagePath) { * returning the correctly themed image, scaling Pixmaps if necessary, and applying onto * widgets as QIcons.\ * @class - * @param {String} imagePath - Path to the image. + * @param {String} imagePath - Path to the image. * @param {Int} width - Width in display pixels. * @param {Int} height - Height in display pixels. * @param {Boolean} uniformScaling - Whether to maintain the original aspect ratio when scaling. @@ -189,7 +190,6 @@ function StyledImage(imagePath, width, height, uniformScaling) { this.getImage = getImage; } - /** * Filesystem path to the image - remapped to the appropriate theme. */ From 7dea44bed31c5c9e4543b56ac8a737230f933772 Mon Sep 17 00:00:00 2001 From: MathieuC Date: Sat, 12 Jun 2021 02:24:19 +0200 Subject: [PATCH 065/112] add reference to seller in repository --- ExtensionStore/lib/store.js | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/ExtensionStore/lib/store.js b/ExtensionStore/lib/store.js index 73561a4..405ecd3 100644 --- a/ExtensionStore/lib/store.js +++ b/ExtensionStore/lib/store.js @@ -112,7 +112,7 @@ Object.defineProperty(Store.prototype, "repositories", { Object.defineProperty(Store.prototype, "extensions", { get: function () { if (typeof this._extensions === 'undefined') { - this.log.debug("getting the list of available extensions.") + this.log.debug("getting the list of available extensions.") var repos = this.repositories; var extensions = []; @@ -143,7 +143,8 @@ Object.defineProperty(Store.prototype, "storeExtension", { get: function () { if (typeof this._storeExtension === 'undefined') { var storePackage = webQuery.get("https://raw.githubusercontent.com/mchaptel/ExtensionStore/master/ExtensionStore/tbpackage.json") - this._storeRepository = new Repository(storePackage.repository) + this._storeSeller = new Seller ("https://raw.githubusercontent.com/mchaptel/") + this._storeRepository = new Repository(this._storeSeller, storePackage.repository); this._storeExtension = new Extension(this._storeRepository, storePackage); } return this._storeExtension @@ -205,8 +206,8 @@ Object.defineProperty(Seller.prototype, "dlUrl", { */ Object.defineProperty(Seller.prototype, "package", { get: function () { - this.log.debug("getting package for " + this.masterRepositoryName); if (!this._tbpackage) { + this.log.debug("getting package for " + this.masterRepositoryName); var response = webQuery.get(this.dlUrl + "/tbpackage.json"); if (!response || response.message) { var message = "No valid package found in repository " + this._url + ": " + response.message; @@ -299,7 +300,7 @@ Object.defineProperty(Seller.prototype, "repositories", { this.log.debug("getting seller repositories for seller " + this._url); this._repositories = []; var tbpackage = this.package; - if (tbpackage == null) return this._repositories; + if (!tbpackage) return this._repositories; // use a repositories object to avoid duplicates var extensionsPackages = tbpackage.extensions; @@ -307,7 +308,7 @@ Object.defineProperty(Seller.prototype, "repositories", { for (var i in extensionsPackages) { var repoName = extensionsPackages[i].repository; if (!repositories.hasOwnProperty(repoName)) { - repositories[repoName] = new Repository(repoName); + repositories[repoName] = new Repository(this, repoName); repositories[repoName]._extensions = []; this._repositories.push(repositories[repoName]); } @@ -357,7 +358,7 @@ Seller.prototype.addExtension = function (name) { } } - var extension = new Extension(new Repository(this._url), { name: name, version: "1.0.0" }); + var extension = new Extension(new Repository(this, this._url), { name: name, version: "1.0.0" }); extension.package = { name: name, version: "1.0.0" } this._extensions[extension.id] = extension; @@ -423,11 +424,17 @@ Seller.prototype.loadFromFile = function (packageFile) { * @property {Package} package instance of the package class that holds the package informations * @property {Object} contents parsed json from the api query */ -function Repository(url) { +function Repository(seller, url) { this.log = new Logger("Repository") if (url.slice(-1) != "/") url += "/"; + this.seller = seller; + + if (url.indexOf(this.seller.githubUserName)==-1){ + throw new Error("Seller Repository must be under same github account as extension list."); + } this._url = url; - this.name = this._url.replace("https://github.com/", "") + + this.name = this._url.replace("https://github.com/", ""); } From 7f5c6c04e66a8449f5a15ec1ae9b60c90e4491de Mon Sep 17 00:00:00 2001 From: MathieuC Date: Sat, 12 Jun 2021 02:24:46 +0200 Subject: [PATCH 066/112] add shortcut to social links from Seller --- ExtensionStore/lib/store.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/ExtensionStore/lib/store.js b/ExtensionStore/lib/store.js index 405ecd3..82ba4be 100644 --- a/ExtensionStore/lib/store.js +++ b/ExtensionStore/lib/store.js @@ -290,6 +290,22 @@ Object.defineProperty(Seller.prototype, "iconUrl", { }) + +/** + * The social media links provided by the seller in the package. + * @type {string[]} + */ +Object.defineProperty(Seller.prototype, "socials", { + get: function () { + var social = this.package.social; + if (social && typeof social == "string") social = [social]; + return social; + } +}) + + + + /** * Get the repositories for this seller * @type {Repository[]} From 0dd381498fed89e6fbe7495277694189e6ff3192 Mon Sep 17 00:00:00 2001 From: MathieuC Date: Sat, 12 Jun 2021 02:25:13 +0200 Subject: [PATCH 067/112] tweak signal values during install/load to increase responsiveness feel --- ExtensionStore/lib/store.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/ExtensionStore/lib/store.js b/ExtensionStore/lib/store.js index 82ba4be..cc51ad9 100644 --- a/ExtensionStore/lib/store.js +++ b/ExtensionStore/lib/store.js @@ -46,6 +46,9 @@ Object.defineProperty(Store.prototype, "sellers", { get: function () { if (typeof this._sellers === 'undefined') { this.log.debug("getting sellers"); + // set progress directly once to make the button feel more reponsive while thhe store fetches info + this.onLoadProgressChanged.emit(0.001); + // the sellers list can be overriden with an environment variable for local studio installs var sellersFile = System.getenv("HUES_SELLERS_PATH"); if (!sellersFile) sellersFile = "https://raw.githubusercontent.com/mchaptel/ExtensionStore/master/SELLERSLIST"; @@ -70,7 +73,7 @@ Object.defineProperty(Store.prototype, "sellers", { var seller = new Seller(sellersList[i]); var package = seller.package; validSellers.push(seller); - this.onLoadProgressChanged.emit(i / sellersList.length); + this.onLoadProgressChanged.emit((i+1) / (sellersList.length+1)); } catch (error) { this.log.error("problem getting package for seller " + sellersList[i], error); } @@ -128,9 +131,9 @@ Object.defineProperty(Store.prototype, "extensions", { for (var i in extensions) { this._extensions[extensions[i].id] = extensions[i]; } + this.onLoadProgressChanged.emit(1); } - this.onLoadProgressChanged.emit(1); return this._extensions; } }) @@ -490,7 +493,7 @@ Object.defineProperty(Repository.prototype, "package", { this.log.debug("getting repos package for repo " + this.apiUrl); var response = webQuery.get(this.dlUrl + "/tbpackage.json"); if (!response || response.message) { - this.log.error("No valid package found in repository " + this._url + ": " + response.message) + this.log.error("No valid package found in repository " + this._url + ": " + response.message); return null } this._package = response; @@ -1296,7 +1299,7 @@ ExtensionInstaller.prototype.downloadFiles = function () { var destFolder = this.destFolder; // get the files list (heavy operations) - this.onInstallProgressChanged.emit(0); // show the progress bar at 0 + this.onInstallProgressChanged.emit(0.1); // show the progress bar starting var destPaths = this.extension.localPaths.map(function (x) { return destFolder + x }); var dlFiles = [this.destFolder]; var files = this.extension.files; @@ -1304,7 +1307,7 @@ ExtensionInstaller.prototype.downloadFiles = function () { this.log.debug("downloading files : "+files.map(function(x){return x.path}).join("\n")) for (var i = 0; i < files.length; i++) { - this.onInstallProgressChanged.emit(i/files.length); + this.onInstallProgressChanged.emit((i+1)/(files.length+1)); try{ webQuery.download(this.getDownloadUrl(files[i].path), destPaths[i]); var dlFile = new File(destPaths[i]); From 5487a5bb730d29734ab0262d27ac27c7937f1ccc Mon Sep 17 00:00:00 2001 From: MathieuC Date: Sat, 12 Jun 2021 02:26:15 +0200 Subject: [PATCH 068/112] add socialButtons in description panel, and fallback for missing values in extension package --- ExtensionStore/app.js | 33 ++++++++++++++++---- ExtensionStore/resources/stylesheet_dark.qss | 5 +++ 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/ExtensionStore/app.js b/ExtensionStore/app.js index 2e5ac31..6ca2b38 100644 --- a/ExtensionStore/app.js +++ b/ExtensionStore/app.js @@ -9,6 +9,7 @@ var ExtensionItem = widgets.ExtensionItem; var LoadButton = widgets.LoadButton; var InstallButton = widgets.InstallButton; var ProgressBar = widgets.ProgressBar; +var SocialButton = widgets.SocialButton; var StyledImage = style.StyledImage; var log = new Logger("UI"); @@ -457,18 +458,35 @@ StoreUI.prototype.updateDescriptionPanel = function () { var extension = this.selectedExtension; if (!extension) return + var website = extension.package.website; + var author = extension.package.author; + var socials = extension.repository.seller.socials; + this.storeDescriptionPanel.versionStoreLabel.text = extension.version; this.descriptionText.setHtml(extension.package.description); this.storeDescriptionPanel.storeKeywordsGroup.storeKeywordsLabel.text = extension.package.keywords.join(", "); - this.storeDescriptionPanel.authorStoreLabel.text = extension.package.author; + this.storeDescriptionPanel.authorStoreLabel.text = author?author:extension.repository.seller.name; this.storeDescriptionPanel.sourceButton.toolTip = extension.package.repository; - this.storeDescriptionPanel.websiteButton.toolTip = extension.package.website; + this.storeDescriptionPanel.websiteButton.toolTip = website?website:extension.repository.seller.website; + + var websiteIcon = new WebIcon(extension.package.website); + websiteIcon.setToWidget(this.storeDescriptionPanel.websiteButton); - var websiteIcon = new WebIcon(extension.package.website) - websiteIcon.setToWidget(this.storeDescriptionPanel.websiteButton) + var githubIcon = new StyledImage(style.ICONS.github); + githubIcon.setAsIcon(this.storeDescriptionPanel.sourceButton); - var githubIcon = new StyledImage(style.ICONS.github) - githubIcon.setAsIcon(this.storeDescriptionPanel.sourceButton) + // create buttons next to the name for social links + var socialsLayout = this.storeDescriptionPanel.authorSocialFrame.layout() + // clear existing social buttons + while(socialsLayout.count()){ + delete socialsLayout.takeAt(0); + } + + socials = socials.slice(0,4) // limit the display at 4 links + for (var i in socials){ + var socialButton = new SocialButton(socials[i]); + socialsLayout.addWidget(socialButton, 0, Qt.AlignCenter); + } // update install button to reflect whether or not the extension is already installed if (this.localList.isInstalled(extension)) { @@ -519,6 +537,9 @@ StoreUI.prototype.performInstall = function () { var extension = this.selectedExtension if (!extension) return + // set progress directly once to make the button feel more reponsive while thhe store fetches info + this.installButton.setProgress(0.001) + log.info("installing extension : " + extension.repository.name + extension.name); var installer = extension.installer; diff --git a/ExtensionStore/resources/stylesheet_dark.qss b/ExtensionStore/resources/stylesheet_dark.qss index 7d00e10..578d0b8 100644 --- a/ExtensionStore/resources/stylesheet_dark.qss +++ b/ExtensionStore/resources/stylesheet_dark.qss @@ -366,6 +366,11 @@ QFrame#sidepanelFrame { background-color: @04DP; } +/* Sliding description panel background */ +QFrame#authorSocialFrame { + background-color: @04DP; +} + QFrame#installButtonPlaceHolder { background-color: @04DP; } From e261f74bc9d36d1441187afd149b3b80292a3557 Mon Sep 17 00:00:00 2001 From: MathieuC Date: Sat, 12 Jun 2021 02:42:14 +0200 Subject: [PATCH 069/112] fix register import --- ExtensionStore/lib/register.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ExtensionStore/lib/register.js b/ExtensionStore/lib/register.js index 0388b3a..0a58d04 100644 --- a/ExtensionStore/lib/register.js +++ b/ExtensionStore/lib/register.js @@ -1,6 +1,6 @@ var Logger = require("./logger.js").Logger; var DescriptionView = require("./widgets.js").DescriptionView; -var appFolder = require("./lib/io.js").appFolder; +var appFolder = require("./io.js").appFolder; /** * The custom dialog to register a new extension @@ -9,7 +9,6 @@ var appFolder = require("./lib/io.js").appFolder; */ function RegisterExtensionDialog(store, localList){ - var appFolder = appFolder; this.ui = UiLoader.load(appFolder + "/resources/register.ui"); this.store = store; From 1d5cb580f17811452375f8112c4ed6594ae84ab3 Mon Sep 17 00:00:00 2001 From: MathieuC Date: Sat, 12 Jun 2021 15:09:37 +0200 Subject: [PATCH 070/112] fix accumulating drop shadow --- ExtensionStore/app.js | 3 ++- ExtensionStore/lib/widgets.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/ExtensionStore/app.js b/ExtensionStore/app.js index 6ca2b38..6d16feb 100644 --- a/ExtensionStore/app.js +++ b/ExtensionStore/app.js @@ -479,7 +479,8 @@ StoreUI.prototype.updateDescriptionPanel = function () { var socialsLayout = this.storeDescriptionPanel.authorSocialFrame.layout() // clear existing social buttons while(socialsLayout.count()){ - delete socialsLayout.takeAt(0); + var button = socialsLayout.takeAt(0).widget(); + button.deleteLater(); } socials = socials.slice(0,4) // limit the display at 4 links diff --git a/ExtensionStore/lib/widgets.js b/ExtensionStore/lib/widgets.js index a92ff64..8cc9dbe 100644 --- a/ExtensionStore/lib/widgets.js +++ b/ExtensionStore/lib/widgets.js @@ -234,7 +234,7 @@ function SocialButton(url){ this.maximumHeight = this.maximumWidth = UiLoader.dpiScale(24); // shadows seem to accumulate? leaving this in the hope to fix it later - // style.addDropShadow(this); + style.addDropShadow(this); var icon = new WebIcon(url); icon.setToWidget(this); From 0904a53c3654f091d13da694c049900187d3ee3a Mon Sep 17 00:00:00 2001 From: MathieuC Date: Sat, 12 Jun 2021 15:21:50 +0200 Subject: [PATCH 071/112] resize column when moving side panel --- ExtensionStore/app.js | 5 +++++ SELLERSLIST | 8 +------- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/ExtensionStore/app.js b/ExtensionStore/app.js index 6d16feb..c7223bd 100644 --- a/ExtensionStore/app.js +++ b/ExtensionStore/app.js @@ -173,6 +173,11 @@ function StoreUI() { this.storeFooter.registerButton.clicked.connect(this, this.registerExtension); + this.storeFrame.storeSplitter.splitterMoved.connect(this, function(){ + var list = this.extensionsList; + list.setColumnWidth(0, list.width - list.columnWidth(1)); + }) + // Install Button Actions ------------------------------------------- this.installButton = new InstallButton(); this.installButton.objectName = "installButton"; diff --git a/SELLERSLIST b/SELLERSLIST index 629b71b..27efa49 100644 --- a/SELLERSLIST +++ b/SELLERSLIST @@ -1,9 +1,3 @@ [ - "https://github.com/jonathan-fontaine/TBScripts/", - "https://github.com/mchaptel/TBScripts/", - "https://github.com/cfourney/OpenHarmony/", - "https://github.com/yueda1984/MC-Load-Poses-On-Selected-Frames/", - "https://github.com/alarigger/AL_Expose_All_Substitutions/", - "https://github.com/bob-ross27/toonboom/", - "https://github.com/35743/HarmonyScripts/" + "https://github.com/mchaptel/TBScripts/" ] From 61df47609025ec3897d561565713369e67a79a5c Mon Sep 17 00:00:00 2001 From: MathieuC Date: Sat, 12 Jun 2021 18:27:11 +0200 Subject: [PATCH 072/112] properly set width of extension list columns at load --- ExtensionStore/app.js | 11 ++++---- ExtensionStore/lib/widgets.js | 2 +- ExtensionStore/resources/store.ui | 42 +++++++++++++++++-------------- 3 files changed, 30 insertions(+), 25 deletions(-) diff --git a/ExtensionStore/app.js b/ExtensionStore/app.js index c7223bd..585bf30 100644 --- a/ExtensionStore/app.js +++ b/ExtensionStore/app.js @@ -175,7 +175,7 @@ function StoreUI() { this.storeFrame.storeSplitter.splitterMoved.connect(this, function(){ var list = this.extensionsList; - list.setColumnWidth(0, list.width - list.columnWidth(1)); + list.setColumnWidth(0, list.width - UiLoader.dpiScale(30)); }) // Install Button Actions ------------------------------------------- @@ -241,10 +241,6 @@ StoreUI.prototype.setUpdateProgressUIState = function (visible) { * Loads the store */ StoreUI.prototype.loadStore = function () { - // setup the store widget sizes - this.extensionsList.setColumnWidth(0, UiLoader.dpiScale(220)); - this.extensionsList.setColumnWidth(1, UiLoader.dpiScale(30)); - // setup the scrollArea containing the webview this.descriptionText = new DescriptionView() var webWidget = this.storeDescriptionPanel.webContent; @@ -256,6 +252,7 @@ StoreUI.prototype.loadStore = function () { var storeFrame = this.storeFrame; storeFrame.storeSplitter.setSizes([storeFrame.width / 2, storeFrame.width / 2]); this.storeFrameState = storeFrame.storeSplitter.saveState(); + storeFrame.storeSplitter.setSizes([storeFrame.storeSplitter.width, 0]); // Show progress dialog to give user indication that the list of extensions is being @@ -291,6 +288,10 @@ StoreUI.prototype.loadStore = function () { // Show the fully loaded store. this.storeFrame.show(); this.aboutFrame.hide(); + + // setup the store widget sizes + this.extensionsList.setColumnWidth(1, UiLoader.dpiScale(30)); + this.extensionsList.setColumnWidth(0, (this.extensionsList.width / 2) - this.extensionsList.indentation - this.extensionsList.columnWidth(1)); } diff --git a/ExtensionStore/lib/widgets.js b/ExtensionStore/lib/widgets.js index 8cc9dbe..c3f67d6 100644 --- a/ExtensionStore/lib/widgets.js +++ b/ExtensionStore/lib/widgets.js @@ -51,7 +51,7 @@ DescriptionView.prototype = Object.create(QWebView.prototype) } else { iconPath = style.ICONS.notInstalled; } - var icon = new StyledImage(iconPath); + var icon = new StyledImage(iconPath, 18, 18); icon.setAsIcon(this, 1); if (extension.iconUrl){ diff --git a/ExtensionStore/resources/store.ui b/ExtensionStore/resources/store.ui index 979ed8b..c30f182 100644 --- a/ExtensionStore/resources/store.ui +++ b/ExtensionStore/resources/store.ui @@ -368,42 +368,57 @@ 4 + + 1 + - - 9 - + + Qt::ScrollBarAlwaysOff + + + QAbstractItemView::NoEditTriggers + true - - QAbstractItemView::NoDragDrop - QAbstractItemView::ContiguousSelection - 18 - 18 + 20 + 20 + + QAbstractItemView::ScrollPerPixel + 10 + false + + true true + + 2 + 250 + + false + Name @@ -414,11 +429,6 @@ - - - - - github.com/ascriptmaker @@ -426,9 +436,6 @@ - - - script1 @@ -436,9 +443,6 @@ O - - - From 5c6f0fb208af12aac7e27e2c747e09a9a83fd110 Mon Sep 17 00:00:00 2001 From: MathieuC Date: Sat, 12 Jun 2021 18:42:37 +0200 Subject: [PATCH 073/112] freeze panels update during install --- ExtensionStore/app.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/ExtensionStore/app.js b/ExtensionStore/app.js index 585bf30..3d2e422 100644 --- a/ExtensionStore/app.js +++ b/ExtensionStore/app.js @@ -463,6 +463,7 @@ StoreUI.prototype.updateExtensionsList = function () { StoreUI.prototype.updateDescriptionPanel = function () { var extension = this.selectedExtension; if (!extension) return + if (this.installing) return var website = extension.package.website; var author = extension.package.author; @@ -541,6 +542,7 @@ StoreUI.prototype.toggleDescriptionPanel = function () { * Installs the currently selected extension */ StoreUI.prototype.performInstall = function () { + this.installing = true; var extension = this.selectedExtension if (!extension) return @@ -565,6 +567,8 @@ StoreUI.prototype.performInstall = function () { var timer = new QTimer(); timer.singleShot = true; timer["timeout"].connect(this, function() { + this.installing = false; + this.updateExtensionsList(); this.updateDescriptionPanel(); }); @@ -576,6 +580,8 @@ StoreUI.prototype.performInstall = function () { * Uninstalls the currently selected extension */ StoreUI.prototype.performUninstall = function () { + this.installing = true; + var extension = this.selectedExtension if (!extension) return @@ -587,6 +593,9 @@ StoreUI.prototype.performUninstall = function () { log.error(err); MessageBox.information("There was an error while uninstalling extension\n" + extension.name + " v" + extension.version + ":\n\n" + err); } + + this.installing = false; + this.localList.refreshExtensions(); this.updateExtensionsList(); } From ace85b47628960d8b3e5a77a7b0a4e6d65edbb03 Mon Sep 17 00:00:00 2001 From: bob-ross27 <55351437+bob-ross27@users.noreply.github.com> Date: Sat, 12 Jun 2021 13:58:31 -0300 Subject: [PATCH 074/112] Unified lineedit and groupbox themes. Slightly lightened outline. --- ExtensionStore/resources/stylesheet_dark.qss | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ExtensionStore/resources/stylesheet_dark.qss b/ExtensionStore/resources/stylesheet_dark.qss index 578d0b8..d50ea5d 100644 --- a/ExtensionStore/resources/stylesheet_dark.qss +++ b/ExtensionStore/resources/stylesheet_dark.qss @@ -77,7 +77,7 @@ QScrollBar::sub-line { QLineEdit { color: lightGrey; margin: 1px; - border-color: @12DP; + border-color: @16DP; border-width: 2px; border-radius: 5px; background: @03DP; @@ -387,6 +387,9 @@ QSplitter::handle:hover#storeSplitter { /* Keywords */ QGroupBox#storeKeywordsGroup { + border-color: @16DP; + border-style: solid; + border-width: 2px; background-color: @06DP; } From 4a4a3a6d73485ac1b31424c792c465aad4f485de Mon Sep 17 00:00:00 2001 From: MathieuC Date: Sat, 12 Jun 2021 18:58:52 +0200 Subject: [PATCH 075/112] extract resize columns into a function --- ExtensionStore/app.js | 29 ++++++++++++++++------------- SELLERSLIST | 8 +++++++- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/ExtensionStore/app.js b/ExtensionStore/app.js index 3d2e422..e7c1eb2 100644 --- a/ExtensionStore/app.js +++ b/ExtensionStore/app.js @@ -173,10 +173,7 @@ function StoreUI() { this.storeFooter.registerButton.clicked.connect(this, this.registerExtension); - this.storeFrame.storeSplitter.splitterMoved.connect(this, function(){ - var list = this.extensionsList; - list.setColumnWidth(0, list.width - UiLoader.dpiScale(30)); - }) + this.storeFrame.storeSplitter.splitterMoved.connect(this, this.resizeColumns); // Install Button Actions ------------------------------------------- this.installButton = new InstallButton(); @@ -248,13 +245,6 @@ StoreUI.prototype.loadStore = function () { webWidget.layout().setContentsMargins(0, 0, 0, 0); webWidget.layout().addWidget(this.descriptionText, 0, Qt.AlignTop); - // set default expanded size to half the splitter size - var storeFrame = this.storeFrame; - storeFrame.storeSplitter.setSizes([storeFrame.width / 2, storeFrame.width / 2]); - this.storeFrameState = storeFrame.storeSplitter.saveState(); - - storeFrame.storeSplitter.setSizes([storeFrame.storeSplitter.width, 0]); - // Show progress dialog to give user indication that the list of extensions is being // updated. this.setUpdateProgressUIState(true); @@ -289,9 +279,15 @@ StoreUI.prototype.loadStore = function () { this.storeFrame.show(); this.aboutFrame.hide(); + // set default expanded size to half the splitter size + var storeFrame = this.storeFrame; + storeFrame.storeSplitter.setSizes([storeFrame.width / 2, storeFrame.width / 2]); + this.storeFrameState = storeFrame.storeSplitter.saveState(); + // setup the store widget sizes - this.extensionsList.setColumnWidth(1, UiLoader.dpiScale(30)); - this.extensionsList.setColumnWidth(0, (this.extensionsList.width / 2) - this.extensionsList.indentation - this.extensionsList.columnWidth(1)); + this.resizeColumns() + + storeFrame.storeSplitter.setSizes([storeFrame.storeSplitter.width, 0]); } @@ -538,6 +534,13 @@ StoreUI.prototype.toggleDescriptionPanel = function () { } +StoreUI.prototype.resizeColumns = function(){ + var list = this.extensionsList + var scroll = list.verticalScrollBar() + list.setColumnWidth(1, UiLoader.dpiScale(30)) + list.setColumnWidth(0, list.width - scroll.visible*scroll.width - list.columnWidth(1)) +} + /** * Installs the currently selected extension */ diff --git a/SELLERSLIST b/SELLERSLIST index 27efa49..629b71b 100644 --- a/SELLERSLIST +++ b/SELLERSLIST @@ -1,3 +1,9 @@ [ - "https://github.com/mchaptel/TBScripts/" + "https://github.com/jonathan-fontaine/TBScripts/", + "https://github.com/mchaptel/TBScripts/", + "https://github.com/cfourney/OpenHarmony/", + "https://github.com/yueda1984/MC-Load-Poses-On-Selected-Frames/", + "https://github.com/alarigger/AL_Expose_All_Substitutions/", + "https://github.com/bob-ross27/toonboom/", + "https://github.com/35743/HarmonyScripts/" ] From a7ea5635f2fdc6b1d20eed603cea839ab377d060 Mon Sep 17 00:00:00 2001 From: MathieuC Date: Sat, 12 Jun 2021 19:06:21 +0200 Subject: [PATCH 076/112] added search by seller name --- ExtensionStore/lib/store.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ExtensionStore/lib/store.js b/ExtensionStore/lib/store.js index cc51ad9..2092b5b 100644 --- a/ExtensionStore/lib/store.js +++ b/ExtensionStore/lib/store.js @@ -925,7 +925,8 @@ Extension.prototype.matchesSearch = function (search) { // match all of the terms in the search, in any order, amongst the name and keywords search = RegExp("(?=.*" + search.split(" ").join(")(?=.*") + (").*"), "ig") - var searchableString = this.name + "," + this.package.keywords.join(",") + // search using seller name, extension name and keywords + var searchableString = this.repository.seller.name + "," + this.name + "," + this.package.keywords.join(",") return (search.exec(searchableString)); } From b49379bd82a2ccc15bc4e3196f8e9024a7d017e1 Mon Sep 17 00:00:00 2001 From: MathieuC Date: Sat, 12 Jun 2021 19:41:57 +0200 Subject: [PATCH 077/112] small margin tweaks around extension list --- ExtensionStore/app.js | 14 +++++++------- ExtensionStore/lib/register.js | 2 ++ ExtensionStore/resources/store.ui | 17 ++++++++++++++++- ExtensionStore/resources/stylesheet_dark.qss | 18 ++++++++++++------ 4 files changed, 37 insertions(+), 14 deletions(-) diff --git a/ExtensionStore/app.js b/ExtensionStore/app.js index e7c1eb2..af5970a 100644 --- a/ExtensionStore/app.js +++ b/ExtensionStore/app.js @@ -535,10 +535,10 @@ StoreUI.prototype.toggleDescriptionPanel = function () { StoreUI.prototype.resizeColumns = function(){ - var list = this.extensionsList - var scroll = list.verticalScrollBar() - list.setColumnWidth(1, UiLoader.dpiScale(30)) - list.setColumnWidth(0, list.width - scroll.visible*scroll.width - list.columnWidth(1)) + var list = this.extensionsList; + var scroll = list.verticalScrollBar(); + list.setColumnWidth(1, UiLoader.dpiScale(35)); + list.setColumnWidth(0, list.width - scroll.visible*scroll.width - list.columnWidth(1)); } /** @@ -546,11 +546,11 @@ StoreUI.prototype.resizeColumns = function(){ */ StoreUI.prototype.performInstall = function () { this.installing = true; - var extension = this.selectedExtension - if (!extension) return + var extension = this.selectedExtension; + if (!extension) return; // set progress directly once to make the button feel more reponsive while thhe store fetches info - this.installButton.setProgress(0.001) + this.installButton.setProgress(0.001); log.info("installing extension : " + extension.repository.name + extension.name); var installer = extension.installer; diff --git a/ExtensionStore/lib/register.js b/ExtensionStore/lib/register.js index 0a58d04..1f9899b 100644 --- a/ExtensionStore/lib/register.js +++ b/ExtensionStore/lib/register.js @@ -2,6 +2,8 @@ var Logger = require("./logger.js").Logger; var DescriptionView = require("./widgets.js").DescriptionView; var appFolder = require("./io.js").appFolder; +log = new Logger("Register") + /** * The custom dialog to register a new extension * @param {Store} store diff --git a/ExtensionStore/resources/store.ui b/ExtensionStore/resources/store.ui index c30f182..cdcd5d5 100644 --- a/ExtensionStore/resources/store.ui +++ b/ExtensionStore/resources/store.ui @@ -365,13 +365,28 @@ Qt::Horizontal - 4 + 6 1 + + 0 + + + 4 + + + 0 + + + 0 + + + 0 + diff --git a/ExtensionStore/resources/stylesheet_dark.qss b/ExtensionStore/resources/stylesheet_dark.qss index d50ea5d..a116022 100644 --- a/ExtensionStore/resources/stylesheet_dark.qss +++ b/ExtensionStore/resources/stylesheet_dark.qss @@ -38,10 +38,10 @@ QScrollBar { } QScrollBar::handle { - border: none; - background: @08DP; - image: none; - border-color: none; + border: none; + background: @08DP; + image: none; + border-color: none; } QScrollBar::handle:hover{ background: @12DP; @@ -79,8 +79,8 @@ QLineEdit { margin: 1px; border-color: @16DP; border-width: 2px; - border-radius: 5px; - background: @03DP; + border-radius: 5px; + background: @03DP; selection-background-color: @ACCENT_LIGHT; } @@ -401,6 +401,12 @@ QLineEdit#versionStoreLabel { background-color: @06DP; } +/* Splitter selection handle */ +QSplitter::handle#storeSplitter { + background-color: @04DP; + border-left: 2px solid @01DP; +} + /* Sidebar buttons */ QToolButton { background-color: @12DP; From 9b9ec63bd05f7b353d6b3f4bd2398c971fd88a74 Mon Sep 17 00:00:00 2001 From: bob-ross27 <55351437+bob-ross27@users.noreply.github.com> Date: Sat, 12 Jun 2021 22:40:31 -0300 Subject: [PATCH 078/112] Added failure state to InstallButton. --- ExtensionStore/app.js | 21 +++++++++++++++------ ExtensionStore/lib/store.js | 15 +++++++++++++-- ExtensionStore/lib/style.js | 1 + ExtensionStore/lib/widgets.js | 30 ++++++++++++++++++++++++++++++ 4 files changed, 59 insertions(+), 8 deletions(-) diff --git a/ExtensionStore/app.js b/ExtensionStore/app.js index af5970a..8a9ef69 100644 --- a/ExtensionStore/app.js +++ b/ExtensionStore/app.js @@ -555,24 +555,33 @@ StoreUI.prototype.performInstall = function () { log.info("installing extension : " + extension.repository.name + extension.name); var installer = extension.installer; - this.failure = function (){ + // Log extension installation error. + this.failure = function (err){ log.error(err); - MessageBox.information("There was an error while installing extension\n" + extension.name + " v" + extension.version + ":\n\n" + err); } installer.onInstallProgressChanged.connect(this.installButton, this.installButton.setProgress); installer.onInstallFailed.connect(this, this.failure); + installer.onInstallFailed.connect(this.installButton, this.installButton.setFailState); - this.localList.install(extension); - this.localList.refreshExtensions(); + // Attempt to install the extension. + var extensionInstalled = this.localList.install(extension); + + // If extension install failed - alert user. + // Not in failure function to avoid being called for each failed proc, and to only appear after InstallButton has changed to a failed state. + if (!extensionInstalled) { + MessageBox.information("There was an error while installing extension\n" + extension.name + " v" + extension.version + ":\n\n"); + } // delay refresh after install completes var timer = new QTimer(); timer.singleShot = true; timer["timeout"].connect(this, function() { this.installing = false; - - this.updateExtensionsList(); + this.localList.refreshExtensions(); + if (extensionInstalled) { + this.updateExtensionsList(); + } this.updateDescriptionPanel(); }); timer.start(700); diff --git a/ExtensionStore/lib/store.js b/ExtensionStore/lib/store.js index 2092b5b..0874f8f 100644 --- a/ExtensionStore/lib/store.js +++ b/ExtensionStore/lib/store.js @@ -1083,8 +1083,18 @@ LocalExtensionList.prototype.install = function (extension) { delete extension._installer; } - installer.onInstallFinished.connect(this, copyFiles) - installer.downloadFiles(); + installer.onInstallFinished.connect(this, copyFiles); + + // Try to download the extension files. + try { + installer.downloadFiles(); + return true; + } + catch (error) { + this.log.debug("Unable to install extension: " + error); + delete extension._installer; + return false; + } } @@ -1322,6 +1332,7 @@ ExtensionInstaller.prototype.downloadFiles = function () { } }catch(error){ this.onInstallFailed.emit(error); + throw error; } } diff --git a/ExtensionStore/lib/style.js b/ExtensionStore/lib/style.js index bf23dc7..9e6b51e 100644 --- a/ExtensionStore/lib/style.js +++ b/ExtensionStore/lib/style.js @@ -41,6 +41,7 @@ const styleSheetsDark = { noConnexionRibbon : "QWidget { background-color: " + COLORS.RED + "; color: white; }", progressButton : "QToolButton { border-color: transparent transparent @ACCENT transparent; }", installButton : "QToolButton { border-color: transparent transparent " + COLORS.GREEN + " transparent; }", + installFailedButton : "QToolButton { border-color: transparent transparent " + COLORS.RED + " transparent; }", uninstallButton : "QToolButton { border-color: transparent transparent " + COLORS.ORANGE + " transparent; }", updateButton : "QToolButton { border-color: transparent transparent " + COLORS.YELLOW + " transparent; }", loadButton : "QToolButton { border-color: transparent transparent " + COLORS.ACCENT_LIGHT + " transparent; }", diff --git a/ExtensionStore/lib/widgets.js b/ExtensionStore/lib/widgets.js index c3f67d6..10e0a2e 100644 --- a/ExtensionStore/lib/widgets.js +++ b/ExtensionStore/lib/widgets.js @@ -89,6 +89,7 @@ function ProgressButton(color, text, progressText, finishedText){ this.defaultText = text; this.progressText = progressText; this.finishedText = finishedText; + this.hasFailed = false; } ProgressButton.prototype = Object.create(QToolButton.prototype); @@ -113,6 +114,12 @@ Object.defineProperty(ProgressButton.prototype, "accentColor", { * @param {Int} progress - Value from 0 to 1 that the operation is currently at. */ ProgressButton.prototype.setProgress = function (progress) { + + // Disable progress updates if the button has failed. hasFailed is Implemented in child classes. + if (this.hasFailed) { + return; + } + var accentColor = this.accentColor; var backgroundColor = this.backgroundColor; @@ -177,6 +184,11 @@ function InstallButton() { "progressText": "Updating...", "accentColor": style.COLORS.YELLOW, }, + "FAIL": { + "action": new QAction("Failed", this), + "progressText": "Failed", + "accentColor": style.COLORS.RED, + } } this.mode = "INSTALL"; @@ -204,11 +216,29 @@ Object.defineProperty(InstallButton.prototype, "mode", { this.progressText = modeDetails.progressText; this.removeAction(this.defaultAction()); this.setDefaultAction(modeDetails.action); + this.hasFailed = false; } } }); +/** + * Disable progress updates on the button, and set the button mode/stylesheet to indicate failure of + * an extension install. + */ + InstallButton.prototype.setFailState = function() { + // Only needs to run once. + if (this.hasFailed) { + return; + } + + this.hasFailed = true; + this.mode = "FAIL"; + this.text = "Failed"; + this.setStyleSheet("QToolButton { background-color: " + style.COLORS.RED + "; border-color: transparent; color: white}"); +} + + /** * ProgressButton child class for Loading operations. * @classdesc From 370832db37196cb0e876d3b7e5fb32848bc58cf1 Mon Sep 17 00:00:00 2001 From: bob-ross27 <55351437+bob-ross27@users.noreply.github.com> Date: Sat, 12 Jun 2021 23:28:50 -0300 Subject: [PATCH 079/112] Fixed ui shift when store loading progressbar appears. --- ExtensionStore/app.js | 14 +++++++-- ExtensionStore/resources/store.ui | 31 +------------------- ExtensionStore/resources/stylesheet_dark.qss | 1 + 3 files changed, 14 insertions(+), 32 deletions(-) diff --git a/ExtensionStore/app.js b/ExtensionStore/app.js index 8a9ef69..20fdb8e 100644 --- a/ExtensionStore/app.js +++ b/ExtensionStore/app.js @@ -70,7 +70,7 @@ function StoreUI() { this.aboutFrame.layout().insertWidget(6, this.loadStoreButton, 0, Qt.AlignCenter); // Insert the progress bar. - this.aboutFrame.layout().insertWidget(10, this.updateProgress, 0, 0); + this.aboutFrame.updateRibbon.layout().insertWidget(0, this.updateProgress, 0, Qt.AlignBottom); // Hide the store and the loading UI elements. this.storeFrame.hide(); @@ -228,9 +228,19 @@ StoreUI.prototype.show = function () { * @param {boolean} visible - Determine whether the progress state should be enabled or disabled. */ StoreUI.prototype.setUpdateProgressUIState = function (visible) { + // Save the existing store text and change text to maintain geometry while being effectively hidden. + if (visible) { + this.aboutFrame.updateRibbon.storeVersion.toolTip = this.aboutFrame.updateRibbon.storeVersion.text; + this.aboutFrame.updateRibbon.storeVersion.text = ""; + } + else { + // Restore store text. + this.aboutFrame.updateRibbon.storeVersion.text = this.aboutFrame.updateRibbon.storeVersion.toolTip; + this.aboutFrame.updateRibbon.storeVersion.toolTip = ""; + } + this.updateProgress.visible = visible; this.aboutFrame.updateButton.visible = !visible; - this.aboutFrame.updateRibbon.storeVersion.visible = !visible; } diff --git a/ExtensionStore/resources/store.ui b/ExtensionStore/resources/store.ui index cdcd5d5..9d0452b 100644 --- a/ExtensionStore/resources/store.ui +++ b/ExtensionStore/resources/store.ui @@ -1170,42 +1170,13 @@ 3 - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 8 - 20 - - - - - + v0.0.1 - - - - Qt::Horizontal - - - - 40 - 20 - - - - diff --git a/ExtensionStore/resources/stylesheet_dark.qss b/ExtensionStore/resources/stylesheet_dark.qss index a116022..95e5009 100644 --- a/ExtensionStore/resources/stylesheet_dark.qss +++ b/ExtensionStore/resources/stylesheet_dark.qss @@ -214,6 +214,7 @@ QToolButton:pressed#loadStoreButton { QLabel#storeVersion { font-family: Arial; font-size: 10pt; + padding-left: 10px; } /* Update Store Button */ From fb7859e2ff6b47b42c21280c31e7dc2c53e8f2f1 Mon Sep 17 00:00:00 2001 From: bob-ross27 <55351437+bob-ross27@users.noreply.github.com> Date: Sat, 12 Jun 2021 23:31:00 -0300 Subject: [PATCH 080/112] Moved store init log to debug. --- ExtensionStore/lib/store.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ExtensionStore/lib/store.js b/ExtensionStore/lib/store.js index 0874f8f..5f79ba4 100644 --- a/ExtensionStore/lib/store.js +++ b/ExtensionStore/lib/store.js @@ -34,7 +34,7 @@ function test() { */ function Store() { this.log = new Logger("Store"); - this.log.info("init store"); + this.log.debug("init store"); this.onLoadProgressChanged = new Signal(); } From 275773dfd3d3fe0f1fb82bbed4f94f0a27d86792 Mon Sep 17 00:00:00 2001 From: bob-ross27 <55351437+bob-ross27@users.noreply.github.com> Date: Sat, 12 Jun 2021 23:47:02 -0300 Subject: [PATCH 081/112] Fixed indentation. --- ExtensionStore/resources/stylesheet_dark.qss | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/ExtensionStore/resources/stylesheet_dark.qss b/ExtensionStore/resources/stylesheet_dark.qss index 95e5009..8bda9f3 100644 --- a/ExtensionStore/resources/stylesheet_dark.qss +++ b/ExtensionStore/resources/stylesheet_dark.qss @@ -22,19 +22,19 @@ QLabel{ /* Loading/Installing progress bar */ QProgressBar { - border: none; - padding: 0px; - background: @ACCENT_BG; + border: none; + padding: 0px; + background: @ACCENT_BG; } QProgressBar::chunk { - background-color: @ACCENT_PRIMARY; + background-color: @ACCENT_PRIMARY; } /* Scrollbars */ QScrollBar { image: none; - border: none; - background: @08DP; + border: none; + background: @08DP; } QScrollBar::handle { @@ -44,7 +44,7 @@ QScrollBar::handle { border-color: none; } QScrollBar::handle:hover{ - background: @12DP; + background: @12DP; } /* Scroll bar background (above handle) */ @@ -271,6 +271,7 @@ QLineEdit#searchStore { border-style: solid; border-color: transparent transparent @ACCENT_LIGHT transparent; } + QLineEdit:focus#searchStore { background-color: @03DP; border-radius: 10px; @@ -404,8 +405,8 @@ QLineEdit#versionStoreLabel { /* Splitter selection handle */ QSplitter::handle#storeSplitter { - background-color: @04DP; - border-left: 2px solid @01DP; + background-color: @04DP; + border-left: 2px solid @01DP; } /* Sidebar buttons */ From 7835c86fdb39c7d9eecb2ce91be4369462384e9c Mon Sep 17 00:00:00 2001 From: bob-ross27 <55351437+bob-ross27@users.noreply.github.com> Date: Sat, 12 Jun 2021 21:41:30 -0300 Subject: [PATCH 082/112] Fixed error in function name. --- ExtensionStore/app.js | 2 +- ExtensionStore/lib/style.js | 4 ++-- ExtensionStore/lib/widgets.js | 7 ++++--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/ExtensionStore/app.js b/ExtensionStore/app.js index 20fdb8e..4e87302 100644 --- a/ExtensionStore/app.js +++ b/ExtensionStore/app.js @@ -34,7 +34,7 @@ function StoreUI() { this.ui.minimumHeight = UiLoader.dpiScale(200); // Set the global application stylesheet - this.ui.setStyleSheet(style.getSyleSheet()); + this.ui.setStyleSheet(style.getStyleSheet()); // Create Load Store Button this.loadStoreButton = new LoadButton(); diff --git a/ExtensionStore/lib/style.js b/ExtensionStore/lib/style.js index 9e6b51e..1857786 100644 --- a/ExtensionStore/lib/style.js +++ b/ExtensionStore/lib/style.js @@ -89,7 +89,7 @@ function isDarkStyle() { * style-specific overrides. * @returns {String} Resulting stylesheet based on the Application theme. */ -function getSyleSheet() { +function getStyleSheet() { var styleFile = appFolder + "/resources/stylesheet_dark.qss"; var styleSheet = io.readFile(styleFile); @@ -251,7 +251,7 @@ StyledImage.prototype.setAsIcon = function(widget, itemColumn){ exports.addDropShadow = addDropShadow; -exports.getSyleSheet = getSyleSheet; +exports.getStyleSheet = getStyleSheet; exports.StyledImage = StyledImage; exports.STYLESHEETS = STYLESHEETS; exports.ICONS = ICONS; diff --git a/ExtensionStore/lib/widgets.js b/ExtensionStore/lib/widgets.js index 10e0a2e..899271b 100644 --- a/ExtensionStore/lib/widgets.js +++ b/ExtensionStore/lib/widgets.js @@ -280,7 +280,7 @@ SocialButton.prototype = Object.create(QToolButton.prototype) * A Qt like custom signal that can be defined, connected and emitted. * As this signal is not actually threaded, the connected callbacks will be executed * directly when the signal is emited, and the rest of the code will execute after. - * @param {type} type the type of value accepted as argument when calling emit() + * @param {type} type - The type of value accepted as argument when calling emit() */ function Signal(type){ // this.emitType = type; @@ -381,8 +381,9 @@ ProgressBar.prototype = Object.create(QProgressBar.prototype); /** - * Transform the input value and update the progress bar. - * @param {number} value - Progress as a percentage with a range of 0 => 1 . + * Transform the input value from the input range (0=>1) to the range expected + * by the QProgressBar (0=>100). Set the progressbar value with the remapped value.. + * @param {number} value - Progress as a percentage with a range of 0 => 1. */ ProgressBar.prototype.setProgress = function(value) { this.setValue(value * 100); From 6df3294617133e476367d4abc20d49bbf3cd5fde Mon Sep 17 00:00:00 2001 From: bob-ross27 <55351437+bob-ross27@users.noreply.github.com> Date: Sat, 12 Jun 2021 23:36:55 -0300 Subject: [PATCH 083/112] Further fixes to match project style. --- ExtensionStore/lib/style.js | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/ExtensionStore/lib/style.js b/ExtensionStore/lib/style.js index 1857786..2c54361 100644 --- a/ExtensionStore/lib/style.js +++ b/ExtensionStore/lib/style.js @@ -95,14 +95,14 @@ function getStyleSheet() { // Get light-specific style overriddes if (!isDarkStyle()) { - styleFileLight = appFolder + "/resources/stylesheet_light.qss"; - styleSheet += io.readFile(styleFileLight); + styleFileLight = appFolder + "/resources/stylesheet_light.qss"; + styleSheet += io.readFile(styleFileLight); } // Replace template colors with final palettes. for (color in COLORS) { - var colorRe = new RegExp("@" + color, "g"); - styleSheet = styleSheet.replace(colorRe, COLORS[color]); + var colorRe = new RegExp("@" + color, "g"); + styleSheet = styleSheet.replace(colorRe, COLORS[color]); } log.debug("Final qss stylesheet:\n" + styleSheet); @@ -119,15 +119,15 @@ function getImage(imagePath) { // Images are default themed dark - just return the original image if dark style is active. if (isDarkStyle()) { - return imagePath; + return imagePath; } // Harmony in light theme. Attempt to use @light variant. var image = new QFileInfo(imagePath); var imageRemapped = new QFileInfo(image.absolutePath() + "/" + image.baseName() + "@light." + image.suffix()); if (imageRemapped.exists()) { - log.debug("Using light themed variant of of " + imagePath); - return imageRemapped.filePath(); + log.debug("Using light themed variant of of " + imagePath); + return imageRemapped.filePath(); } // @light variant not found, fallback to using original image path. @@ -195,7 +195,7 @@ function StyledImage(imagePath, width, height, uniformScaling) { * Filesystem path to the image - remapped to the appropriate theme. */ Object.defineProperty(StyledImage.prototype, "path", { - get: function(){ + get: function() { return this.getImage(this.basePath); } }) @@ -211,15 +211,15 @@ Object.defineProperty(StyledImage.prototype, "pixmap", { var pixmap = new QPixmap(this.path); // work out scaling based on params - if (this.uniformScaling){ + if (this.uniformScaling) { if (this.width && this.height){ // keep inside the given rectangle var aspectRatioFlag = Qt.KeepAspectRatio; - }else{ + } else { // if one of the width or height is missing, only the other value will be used var aspectRatioFlag = Qt.KeepAspectRatioByExpanding; } - }else{ + } else { // resize to match the box exactly var aspectRatioFlag = Qt.IgnoreAspectRatio; } @@ -239,12 +239,12 @@ Object.defineProperty(StyledImage.prototype, "pixmap", { * @param {Int} itemColumn - Index of the column the icon should be applied to, if the widget is a QTreeWidgetItem. */ StyledImage.prototype.setAsIcon = function(widget, itemColumn){ - if (widget instanceof QTreeWidgetItem){ + if (widget instanceof QTreeWidgetItem) { if (typeof itemColumn === 'undefined') var itemColumn = 0; var icon = new QIcon(this.path); widget.setIcon(itemColumn, icon); - }else{ - log.debug("setting icon "+this.path) + } else { + log.debug("setting icon " + this.path) UiLoader.setSvgIcon(widget, this.path); } } From fc3821c0da15afea8f4ba5424d2af9260f32d1d6 Mon Sep 17 00:00:00 2001 From: bob-ross27 <55351437+bob-ross27@users.noreply.github.com> Date: Sat, 12 Jun 2021 23:52:52 -0300 Subject: [PATCH 084/112] Adjusted store version alignment. --- ExtensionStore/resources/store.ui | 2 +- ExtensionStore/resources/stylesheet_dark.qss | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/ExtensionStore/resources/store.ui b/ExtensionStore/resources/store.ui index 9d0452b..b0e7dfe 100644 --- a/ExtensionStore/resources/store.ui +++ b/ExtensionStore/resources/store.ui @@ -1170,7 +1170,7 @@ 3 - + v0.0.1 diff --git a/ExtensionStore/resources/stylesheet_dark.qss b/ExtensionStore/resources/stylesheet_dark.qss index 8bda9f3..e8b7165 100644 --- a/ExtensionStore/resources/stylesheet_dark.qss +++ b/ExtensionStore/resources/stylesheet_dark.qss @@ -215,6 +215,7 @@ QLabel#storeVersion { font-family: Arial; font-size: 10pt; padding-left: 10px; + padding-bottom: 5px; } /* Update Store Button */ From f922fc4d10e6b25b5aaa88b2224de607517638a7 Mon Sep 17 00:00:00 2001 From: bob-ross27 <55351437+bob-ross27@users.noreply.github.com> Date: Sat, 12 Jun 2021 23:55:22 -0300 Subject: [PATCH 085/112] Fixed indententation with tabs to 2 width spaces. --- ExtensionStore/resources/stylesheet_dark.qss | 290 +++++++++---------- 1 file changed, 145 insertions(+), 145 deletions(-) diff --git a/ExtensionStore/resources/stylesheet_dark.qss b/ExtensionStore/resources/stylesheet_dark.qss index e8b7165..0f23f50 100644 --- a/ExtensionStore/resources/stylesheet_dark.qss +++ b/ExtensionStore/resources/stylesheet_dark.qss @@ -7,17 +7,17 @@ Overall widget styling. /* Base widget of the entire UI */ QWidget#Form { - background-color: @01DP; + background-color: @01DP; } /* Overall BG color */ QFrame{ - background-color: @01DP; + background-color: @01DP; } /* Set labels to transparent for text and logos */ QLabel{ - background-color: transparent; + background-color: transparent; } /* Loading/Installing progress bar */ @@ -32,7 +32,7 @@ QProgressBar::chunk { /* Scrollbars */ QScrollBar { - image: none; + image: none; border: none; background: @08DP; } @@ -49,45 +49,45 @@ QScrollBar::handle:hover{ /* Scroll bar background (above handle) */ QScrollBar::add-page { - border: none; - background: @02DP; + border: none; + background: @02DP; } /* Scroll bar background (below handle) */ QScrollBar::sub-page { - border: none; - background: @02DP; + border: none; + background: @02DP; } /* Up arrow */ QScrollBar::add-line { - border: none; - background: none; - image: none; + border: none; + background: none; + image: none; } /* Down arrow */ QScrollBar::sub-line { - border: none; - background: none; - image: none; + border: none; + background: none; + image: none; } /* Line Edits */ QLineEdit { - color: lightGrey; - margin: 1px; - border-color: @16DP; - border-width: 2px; + color: lightGrey; + margin: 1px; + border-color: @16DP; + border-width: 2px; border-radius: 5px; background: @03DP; - selection-background-color: @ACCENT_LIGHT; + selection-background-color: @ACCENT_LIGHT; } /* Tooltips */ QToolTip { - color: lightGrey; - background-color: @06DP; + color: lightGrey; + background-color: @06DP; } /* @@ -98,46 +98,46 @@ EULA Frame /* Exterior frame */ QFrame#eulaFrame { - background-color: @01DP; + background-color: @01DP; } /* Elevated inner frame */ QFrame#innerFrame { - background-color: @03DP; - border-radius:8px; + background-color: @03DP; + border-radius:8px; } /* Elevated EULA Text container */ QFrame#textFrame { - background-color: @06DP; - border-radius:8px; + background-color: @06DP; + border-radius:8px; } /* Scrollable EULA text region base widget */ QScrollArea#scrollArea_2 { - background-color: transparent; + background-color: transparent; } /* EULA screen scrollable region (text background). */ QWidget#scrollAreaWidgetContents_2 { - background-color: transparent; + background-color: transparent; } /* EULA Screen text */ QLabel#eulaText { - background-color: transparent; - margin: 5px; - font-family: Arial; - font-size: 10pt; + background-color: transparent; + margin: 5px; + font-family: Arial; + font-size: 10pt; } /* EULA agreement checkbox */ QCheckBox#eulaCB { - font-family: Arial; - font-size: 13pt; - border-color: @ACCENT_PRIMARY; - background-color: transparent; + font-family: Arial; + font-size: 13pt; + border-color: @ACCENT_PRIMARY; + background-color: transparent; } /* @@ -148,90 +148,90 @@ About Frame /* About Screen text */ QLabel#label_3 { - background-color: transparent; - font-family: Palatino; - font-size: 12pt; - padding-bottom: 7px; - border-width: 2px; - border-style: solid; - border-color: transparent transparent @04DP transparent; + background-color: transparent; + font-family: Palatino; + font-size: 12pt; + padding-bottom: 7px; + border-width: 2px; + border-style: solid; + border-color: transparent transparent @04DP transparent; } /* Social Media */ /* Twitter */ QToolButton#twitterButton { - background-color: transparent; + background-color: transparent; } QToolButton:hover#twitterButton { - border-width: 2px; - border-radius: 4px; - border-style: solid; - border-color: @ACCENT_LIGHT; + border-width: 2px; + border-radius: 4px; + border-style: solid; + border-color: @ACCENT_LIGHT; } /* Github */ QToolButton#githubButton { - background-color: transparent; + background-color: transparent; } QToolButton:hover#githubButton { - border-width: 2px; - border-radius: 4px; - border-style: solid; - border-color: @ACCENT_LIGHT; + border-width: 2px; + border-radius: 4px; + border-style: solid; + border-color: @ACCENT_LIGHT; } /* Discord */ QToolButton#discordButton { - background-color: transparent; + background-color: transparent; } QToolButton:hover#discordButton { - border-width: 2px; - border-radius: 4px; - border-style: solid; - border-color: @ACCENT_LIGHT; + border-width: 2px; + border-radius: 4px; + border-style: solid; + border-color: @ACCENT_LIGHT; } /* Load Store Button */ QToolButton#loadStoreButton { - background: @08DP; - border-width: 2px; - border-style: solid; - border-color: transparent transparent @ACCENT_LIGHT transparent; - border-radius: 7px; + background: @08DP; + border-width: 2px; + border-style: solid; + border-color: transparent transparent @ACCENT_LIGHT transparent; + border-radius: 7px; } QToolButton:hover#loadStoreButton { - border-color: @ACCENT_LIGHT; - background: @12DP; + border-color: @ACCENT_LIGHT; + background: @12DP; } QToolButton:pressed#loadStoreButton { - background: @02DP; - border-color: @ACCENT_PRIMARY; + background: @02DP; + border-color: @ACCENT_PRIMARY; } /* Update Ribbon store version text */ QLabel#storeVersion { - font-family: Arial; - font-size: 10pt; + font-family: Arial; + font-size: 10pt; padding-left: 10px; padding-bottom: 5px; } /* Update Store Button */ QPushButton#updateButton { - padding: 5px; - color: black; - background-color: @YELLOW; - border-width: 2px; - border-style: solid; - border-color: transparent; - border-radius: 7px; + padding: 5px; + color: black; + background-color: @YELLOW; + border-width: 2px; + border-style: solid; + border-color: transparent; + border-radius: 7px; } QPushButton:hover#updateButton { - border-color: @ORANGE; + border-color: @ORANGE; } /* @@ -242,146 +242,146 @@ Store Frame /* Store Frame */ QFrame#storeFrame { - background-color: @12DP; + background-color: @12DP; } /* ====================== - Store Header + Store Header ====================== */ /* Store header */ QFrame#storeHeader { - background-color: @08DP; - border-width: 3px; - border-style: solid; - border-color: transparent transparent @01DP transparent; + background-color: @08DP; + border-width: 3px; + border-style: solid; + border-color: transparent transparent @01DP transparent; } /* Top-left Store logo */ QLabel#headerLogo { - margin-right: 5px; + margin-right: 5px; } /* Search store */ QLineEdit#searchStore { - background-color: transparent; - border-radius: 0px; - border-width: 2px; - border-style: solid; - border-color: transparent transparent @ACCENT_LIGHT transparent; + background-color: transparent; + border-radius: 0px; + border-width: 2px; + border-style: solid; + border-color: transparent transparent @ACCENT_LIGHT transparent; } QLineEdit:focus#searchStore { - background-color: @03DP; - border-radius: 10px; - border-width: 2px; - border-color: transparent; + background-color: @03DP; + border-radius: 10px; + border-width: 2px; + border-color: transparent; } /* Store search box clear */ QToolButton#storeClearSearch { - border: none; - padding: 0px; - margin: 0px; - background-color: transparent; + border: none; + padding: 0px; + margin: 0px; + background-color: transparent; } /* "show installed only" checkbox */ QCheckBox#showInstalledCheckbox { - background-color: transparent; + background-color: transparent; } /* Sidebar Web View */ QWebView { - background-color: lightGrey; + background-color: lightGrey; } /* ====================== - Store Footer + Store Footer ====================== */ QFrame#storeFooter { - background-color: @02DP; - padding: 1px; - border-width: 3px; - border-style: solid; - border-color: @01DP transparent transparent transparent; + background-color: @02DP; + padding: 1px; + border-width: 3px; + border-style: solid; + border-color: @01DP transparent transparent transparent; } /* Register new extension button */ QPushButton#registerButton { - border-radius: 2px; - border-width: 2px; - border-style: solid; - border-color: transparent transparent @ACCENT_LIGHT transparent; - background-color: @02DP; + border-radius: 2px; + border-width: 2px; + border-style: solid; + border-color: transparent transparent @ACCENT_LIGHT transparent; + background-color: @02DP; } QPushButton:hover#registerButton { - background-color: @08DP; + background-color: @08DP; } /* ====================== - Store Sellers + Store Sellers ====================== */ /* Sellers base tree widgert */ QTreeView { - background-color: @01DP; - border: 0px; - outline: none; + background-color: @01DP; + border: 0px; + outline: none; } /* Item within the base sellers tree widget */ QTreeView::item { - color: white; - background-color: @01DP; - margin: 3px; - border: none + color: white; + background-color: @01DP; + margin: 3px; + border: none } QTreeView::item:hover { - margin:3px; - border-radius: 3px; - background-color: @02DP; + margin:3px; + border-radius: 3px; + background-color: @02DP; } QTreeView::item:selected { - margin:3px; - border-radius: 3px; - border-color: none; - background-color: @04DP; + margin:3px; + border-radius: 3px; + border-color: none; + background-color: @04DP; } /* ====================== - Store Sidepanel + Store Sidepanel ====================== */ /* Sliding description panel background */ QFrame#sidepanelFrame { - background-color: @04DP; + background-color: @04DP; } /* Sliding description panel background */ QFrame#authorSocialFrame { - background-color: @04DP; + background-color: @04DP; } QFrame#installButtonPlaceHolder { - background-color: @04DP; + background-color: @04DP; } /* Splitter selection handle */ QSplitter::handle#storeSplitter { background-color: @04DP; - border: none; + border: none; } QSplitter::handle:hover#storeSplitter { @@ -390,18 +390,18 @@ QSplitter::handle:hover#storeSplitter { /* Keywords */ QGroupBox#storeKeywordsGroup { - border-color: @16DP; - border-style: solid; - border-width: 2px; - background-color: @06DP; + border-color: @16DP; + border-style: solid; + border-width: 2px; + background-color: @06DP; } QLineEdit#authorStoreLabel { - background-color: @06DP; + background-color: @06DP; } QLineEdit#versionStoreLabel { - background-color: @06DP; + background-color: @06DP; } /* Splitter selection handle */ @@ -412,18 +412,18 @@ QSplitter::handle#storeSplitter { /* Sidebar buttons */ QToolButton { - background-color: @12DP; - border-radius: 3px; - padding: 5px; + background-color: @12DP; + border-radius: 3px; + padding: 5px; } QToolButton:hover { - background-color: @16DP; + background-color: @16DP; } /* Install button */ QToolButton#installButton { - border-radius: 4px; - border-width: 2px; - border-style: solid; - border-color: transparent transparent @ACCENT_LIGHT transparent; + border-radius: 4px; + border-width: 2px; + border-style: solid; + border-color: transparent transparent @ACCENT_LIGHT transparent; } \ No newline at end of file From 4caab32994ed18714ae94f3fb90862dd1a5f98ef Mon Sep 17 00:00:00 2001 From: bob-ross27 <55351437+bob-ross27@users.noreply.github.com> Date: Sun, 13 Jun 2021 12:56:45 -0300 Subject: [PATCH 086/112] Adjusted hiding UI elements during store load to work when the store has a pending update. --- ExtensionStore/app.js | 20 +++++++++++--------- ExtensionStore/lib/style.js | 1 + 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/ExtensionStore/app.js b/ExtensionStore/app.js index 4e87302..05ce7f9 100644 --- a/ExtensionStore/app.js +++ b/ExtensionStore/app.js @@ -74,7 +74,7 @@ function StoreUI() { // Hide the store and the loading UI elements. this.storeFrame.hide(); - this.setUpdateProgressUIState(false); + this.setStoreLoadUIState(false); if (!this.localList.getData("HUES_EULA_ACCEPTED", false)) { this.aboutFrame.hide(); @@ -225,11 +225,14 @@ StoreUI.prototype.show = function () { /** * Show widgets responsible for showing progress to the user when loading the * store and retrieving extensions. - * @param {boolean} visible - Determine whether the progress state should be enabled or disabled. + * @param {boolean} enabled - Determine whether the progress state should be enabled or disabled. */ -StoreUI.prototype.setUpdateProgressUIState = function (visible) { - // Save the existing store text and change text to maintain geometry while being effectively hidden. - if (visible) { +StoreUI.prototype.setStoreLoadUIState = function (enabled) { + if (enabled) { + // Hide elements during store load without removing them from the UI to avoid elements shifting. + this.aboutFrame.updateButton.setStyleSheet(style.STYLESHEETS.updateButtonInvisible); + this.aboutFrame.updateButton.setGraphicsEffect(null); + this.aboutFrame.updateRibbon.setStyleSheet(style.STYLESHEETS.defaultRibbon); this.aboutFrame.updateRibbon.storeVersion.toolTip = this.aboutFrame.updateRibbon.storeVersion.text; this.aboutFrame.updateRibbon.storeVersion.text = ""; } @@ -239,8 +242,7 @@ StoreUI.prototype.setUpdateProgressUIState = function (visible) { this.aboutFrame.updateRibbon.storeVersion.toolTip = ""; } - this.updateProgress.visible = visible; - this.aboutFrame.updateButton.visible = !visible; + this.updateProgress.visible = enabled; } @@ -257,14 +259,14 @@ StoreUI.prototype.loadStore = function () { // Show progress dialog to give user indication that the list of extensions is being // updated. - this.setUpdateProgressUIState(true); + this.setStoreLoadUIState(true); // Fetch the list of available extensions. try { this.storeExtensions = this.store.extensions; } catch (err) { log.error(err) - this.setUpdateProgressUIState(false); + this.setStoreLoadUIState(false); this.lockStore("Could not load Extensions list.") return } diff --git a/ExtensionStore/lib/style.js b/ExtensionStore/lib/style.js index 2c54361..14b0601 100644 --- a/ExtensionStore/lib/style.js +++ b/ExtensionStore/lib/style.js @@ -45,6 +45,7 @@ const styleSheetsDark = { uninstallButton : "QToolButton { border-color: transparent transparent " + COLORS.ORANGE + " transparent; }", updateButton : "QToolButton { border-color: transparent transparent " + COLORS.YELLOW + " transparent; }", loadButton : "QToolButton { border-color: transparent transparent " + COLORS.ACCENT_LIGHT + " transparent; }", + updateButtonInvisible : "QPushButton { border-color: transparent; background-color: transparent; color: transparent; }", } // Enum to hold light style stylesheets. From bb237c26b8ffc20367eca28fed1cd5bf1bd7a9e3 Mon Sep 17 00:00:00 2001 From: bob-ross27 <55351437+bob-ross27@users.noreply.github.com> Date: Sun, 13 Jun 2021 16:31:15 -0300 Subject: [PATCH 087/112] Fix for update button hiding at end of store update. --- ExtensionStore/app.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ExtensionStore/app.js b/ExtensionStore/app.js index 05ce7f9..6a16173 100644 --- a/ExtensionStore/app.js +++ b/ExtensionStore/app.js @@ -393,8 +393,9 @@ StoreUI.prototype.updateStore = function (currentVersion, storeVersion) { if (success) { MessageBox.information("Store succesfully updated to version v" + storeVersion + ".\n\nPlease restart Harmony for changes to take effect."); this.updateRibbon.storeVersion.setText("v" + currentVersion); - this.updateRibbon.setStyleSheet(""); - this.updateRibbon.updateButton.hide(); + this.updateRibbon.setStyleSheet(style.STYLESHEETS.defaultRibbon); + this.ui.aboutFrame.updateButton.setStyleSheet(style.STYLESHEETS.updateButtonInvisible); + this.ui.aboutFrame.updateButton.setGraphicsEffect(null); } else { MessageBox.information("There was a problem updating to v" + storeVersion + ".\n\n The update was not successful."); } From f328bea14e83520cae66ed8d83c646a13b15245f Mon Sep 17 00:00:00 2001 From: bob-ross27 <55351437+bob-ross27@users.noreply.github.com> Date: Mon, 14 Jun 2021 08:43:21 -0300 Subject: [PATCH 088/112] Fixed blue highlight color on QTreeWidgetItem icons. --- ExtensionStore/lib/style.js | 4 +++- ExtensionStore/lib/widgets.js | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/ExtensionStore/lib/style.js b/ExtensionStore/lib/style.js index 14b0601..7c0a787 100644 --- a/ExtensionStore/lib/style.js +++ b/ExtensionStore/lib/style.js @@ -242,7 +242,9 @@ Object.defineProperty(StyledImage.prototype, "pixmap", { StyledImage.prototype.setAsIcon = function(widget, itemColumn){ if (widget instanceof QTreeWidgetItem) { if (typeof itemColumn === 'undefined') var itemColumn = 0; - var icon = new QIcon(this.path); + var icon = new QIcon(); + icon.addPixmap(this.pixmap, QIcon.Normal); + icon.addPixmap(this.pixmap, QIcon.Selected); widget.setIcon(itemColumn, icon); } else { log.debug("setting icon " + this.path) diff --git a/ExtensionStore/lib/widgets.js b/ExtensionStore/lib/widgets.js index 899271b..6431c7d 100644 --- a/ExtensionStore/lib/widgets.js +++ b/ExtensionStore/lib/widgets.js @@ -62,7 +62,7 @@ DescriptionView.prototype = Object.create(QWebView.prototype) }else{ // fallback to local icon - var extensionIcon = new StyledImage(style.ICONS.defaultExtension); + var extensionIcon = new StyledImage(style.ICONS.defaultExtension, 18, 18); extensionIcon.setAsIcon(this, 0); } From c84dfee899c535f471e33277609e3802974f1845 Mon Sep 17 00:00:00 2001 From: bob-ross27 <55351437+bob-ross27@users.noreply.github.com> Date: Mon, 14 Jun 2021 09:20:23 -0300 Subject: [PATCH 089/112] Applied icon selection fix to WebIcon class, and restored default extension icon size. --- ExtensionStore/lib/network.js | 5 ++++- ExtensionStore/lib/widgets.js | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/ExtensionStore/lib/network.js b/ExtensionStore/lib/network.js index 6a0b8a0..8273899 100644 --- a/ExtensionStore/lib/network.js +++ b/ExtensionStore/lib/network.js @@ -176,7 +176,10 @@ WebIcon.prototype.setToWidget = function (widget) { * @private */ WebIcon.prototype.setIcon = function () { - var icon = new QIcon(this.dlPath); + var icon = new QIcon(); + var iconPixmap = new QPixmap(this.dlPath); + icon.addPixmap(iconPixmap, QIcon.Normal); + icon.addPixmap(iconPixmap, QIcon.Selected); var size = UiLoader.dpiScale(32) icon.size = new QSize(size, size); diff --git a/ExtensionStore/lib/widgets.js b/ExtensionStore/lib/widgets.js index 6431c7d..689fa24 100644 --- a/ExtensionStore/lib/widgets.js +++ b/ExtensionStore/lib/widgets.js @@ -62,7 +62,7 @@ DescriptionView.prototype = Object.create(QWebView.prototype) }else{ // fallback to local icon - var extensionIcon = new StyledImage(style.ICONS.defaultExtension, 18, 18); + var extensionIcon = new StyledImage(style.ICONS.defaultExtension, 20, 20); extensionIcon.setAsIcon(this, 0); } From 723f9c0bd8935d586b66797241a8fa07ad96e2c3 Mon Sep 17 00:00:00 2001 From: bob-ross27 <55351437+bob-ross27@users.noreply.github.com> Date: Mon, 14 Jun 2021 15:24:37 -0300 Subject: [PATCH 090/112] Fixes for store updating, and handle paths with leading slashes better. --- ExtensionStore/lib/store.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ExtensionStore/lib/store.js b/ExtensionStore/lib/store.js index 5f79ba4..b98bfa4 100644 --- a/ExtensionStore/lib/store.js +++ b/ExtensionStore/lib/store.js @@ -755,8 +755,8 @@ Object.defineProperty(Extension.prototype, "package", { /** - * The highest level folder on the repository that includes files included in this extension - * Doesn't poll github, since it only looks at the files listed in the package + * The longest common path in the repository, which contains all files in the extension. + * Doesn't poll github, since it only looks at the files listed in the package. * @name Extension#rootFolder * @type {object} */ @@ -764,6 +764,7 @@ Object.defineProperty(Extension.prototype, "rootFolder", { get: function () { if (typeof this._rootFolder === 'undefined') { var files = this.package.files; + files = files.map(function(x) { return x.match(/^\/?(.*)/)[1] }); // Removing leading /'s. if (files.length == 1) { this._rootFolder = files[0].slice(0, files[0].lastIndexOf("/")+1); } else { @@ -818,7 +819,8 @@ Object.defineProperty(Extension.prototype, "files", { for (var i in packageFiles) { this.log.debug("getting extension files matching : "+packageFiles[i]) - var results = this.repository.getFiles("/"+ packageFiles[i]); + var filter = packageFiles[i].substring(0,1) === "/" ? packageFiles[i] : "/" + packageFiles[i]; + var results = this.repository.getFiles(filter); if (results.length > 0) files = files.concat(results); } From 2ec54eb81e5300745007be6b9852d4249c6aac26 Mon Sep 17 00:00:00 2001 From: bob-ross27 <55351437+bob-ross27@users.noreply.github.com> Date: Mon, 14 Jun 2021 23:41:49 -0300 Subject: [PATCH 091/112] Adjusted message after store update to indicate reopening the extension is sufficient. --- ExtensionStore/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ExtensionStore/app.js b/ExtensionStore/app.js index 6a16173..636c962 100644 --- a/ExtensionStore/app.js +++ b/ExtensionStore/app.js @@ -391,7 +391,7 @@ StoreUI.prototype.lockStore = function (message) { StoreUI.prototype.updateStore = function (currentVersion, storeVersion) { var success = this.localList.install(this.storeExtension, this.ui.aboutFrame.updateButton); if (success) { - MessageBox.information("Store succesfully updated to version v" + storeVersion + ".\n\nPlease restart Harmony for changes to take effect."); + MessageBox.information("Store succesfully updated to version v" + storeVersion + ".\n\nPlease close and reopen HUES for changes to take effect."); this.updateRibbon.storeVersion.setText("v" + currentVersion); this.updateRibbon.setStyleSheet(style.STYLESHEETS.defaultRibbon); this.ui.aboutFrame.updateButton.setStyleSheet(style.STYLESHEETS.updateButtonInvisible); From 53fcce11840ee9bf6e2660cfe7f369800cd8a0ff Mon Sep 17 00:00:00 2001 From: bob-ross27 <55351437+bob-ross27@users.noreply.github.com> Date: Mon, 14 Jun 2021 23:45:06 -0300 Subject: [PATCH 092/112] Change log from error to debug, as permanent redirect isn't an error state. --- ExtensionStore/lib/network.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ExtensionStore/lib/network.js b/ExtensionStore/lib/network.js index 8273899..19b597f 100644 --- a/ExtensionStore/lib/network.js +++ b/ExtensionStore/lib/network.js @@ -29,7 +29,7 @@ NetworkConnexionHandler.prototype.get = function (command) { return null; } if (json.message == "Moved Permanently") { - log.error("Repository " + command + " has moved to : " + json.url); + log.debug("Repository " + command + " has moved to : " + json.url); return json; } if (json.message == "400: Invalid request") { From e9978e1b53e46ef6e97255c5ffed67e005aa4948 Mon Sep 17 00:00:00 2001 From: bob-ross27 <55351437+bob-ross27@users.noreply.github.com> Date: Mon, 14 Jun 2021 23:56:22 -0300 Subject: [PATCH 093/112] Added check to avoid downloading empty file lists. --- ExtensionStore/lib/store.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/ExtensionStore/lib/store.js b/ExtensionStore/lib/store.js index b98bfa4..63ff29e 100644 --- a/ExtensionStore/lib/store.js +++ b/ExtensionStore/lib/store.js @@ -1319,6 +1319,14 @@ ExtensionInstaller.prototype.downloadFiles = function () { this.log.debug("downloading files : "+files.map(function(x){return x.path}).join("\n")) + // If the file list is empty, there was likely an issue parsing the tbpackage and the operaiton + // should be aborted. + if (!files.length) { + this.onInstallFailed.emit("No files found to download."); + throw new Error("No files found to download"); + }; + + // Download each file and update the progress for (var i = 0; i < files.length; i++) { this.onInstallProgressChanged.emit((i+1)/(files.length+1)); try{ From 9061482b62aebfd8d3be906a4ad958db70404b62 Mon Sep 17 00:00:00 2001 From: bob-ross27 <55351437+bob-ross27@users.noreply.github.com> Date: Tue, 15 Jun 2021 09:25:26 -0300 Subject: [PATCH 094/112] Changed store update button to an InstallButton and hooked into progress updates. --- ExtensionStore/app.js | 76 +++++++++++++++----- ExtensionStore/lib/style.js | 5 +- ExtensionStore/resources/store.ui | 30 ++------ ExtensionStore/resources/stylesheet_dark.qss | 10 +-- 4 files changed, 69 insertions(+), 52 deletions(-) diff --git a/ExtensionStore/app.js b/ExtensionStore/app.js index 636c962..60265c4 100644 --- a/ExtensionStore/app.js +++ b/ExtensionStore/app.js @@ -41,6 +41,13 @@ function StoreUI() { this.loadStoreButton.objectName = "loadStoreButton"; style.addDropShadow(this.loadStoreButton, 10, 0, 8); + // Create Store Update Button + this.updateButton = new InstallButton(); + this.updateButton.mode = "Update"; + this.updateButton.objectName = "updateButton"; + this.updateButton.text = "Update Store"; + style.addDropShadow(this.updateButton, 10, 0, 8); + // Create progressbar this.updateProgress = new ProgressBar(); this.updateProgress.objectName = "updateProgress"; @@ -63,12 +70,12 @@ function StoreUI() { // Add a light dropshadow to the about screen text - to shadow the bottom border. style.addDropShadow(this.aboutFrame.label_3, 5, 5, 5, 25); - // Add a dropshadow to the Update store button. - style.addDropShadow(this.aboutFrame.updateButton, 5, 5, 5, 50); - // Insert the Loading button. this.aboutFrame.layout().insertWidget(6, this.loadStoreButton, 0, Qt.AlignCenter); + // Insert the Store Update button. + this.aboutFrame.layout().insertWidget(6, this.updateButton, 0, Qt.AlignCenter); + // Insert the progress bar. this.aboutFrame.updateRibbon.layout().insertWidget(0, this.updateProgress, 0, Qt.AlignBottom); @@ -230,8 +237,8 @@ StoreUI.prototype.show = function () { StoreUI.prototype.setStoreLoadUIState = function (enabled) { if (enabled) { // Hide elements during store load without removing them from the UI to avoid elements shifting. - this.aboutFrame.updateButton.setStyleSheet(style.STYLESHEETS.updateButtonInvisible); - this.aboutFrame.updateButton.setGraphicsEffect(null); + this.updateButton.setStyleSheet(style.STYLESHEETS.InstallButtonInvisible); + this.updateButton.setGraphicsEffect(null); this.aboutFrame.updateRibbon.setStyleSheet(style.STYLESHEETS.defaultRibbon); this.aboutFrame.updateRibbon.storeVersion.toolTip = this.aboutFrame.updateRibbon.storeVersion.text; this.aboutFrame.updateRibbon.storeVersion.text = ""; @@ -340,9 +347,6 @@ StoreUI.prototype.getInstalledVersion = function () { */ StoreUI.prototype.checkForUpdates = function () { var updateRibbon = this.updateRibbon - - var defaultRibbonStyleSheet = style.STYLESHEETS.defaultRibbon; - var updateRibbonStyleSheet = style.STYLESHEETS.updateRibbon; var storeUi = this; try { @@ -354,13 +358,13 @@ StoreUI.prototype.checkForUpdates = function () { // if a more recent version of the store exists on the repo, activate the update ribbon if (!storeExtension.currentVersionIsOlder(currentVersion) && (currentVersion != storeVersion)) { updateRibbon.storeVersion.setText("v" + currentVersion + " ⓘ New version available: v" + storeVersion); - updateRibbon.setStyleSheet(updateRibbonStyleSheet); - this.aboutFrame.updateButton.toolTip = storeExtension.package.description; - this.aboutFrame.updateButton.clicked.connect(this, function () { storeUi.updateStore(currentVersion, storeVersion) }); + updateRibbon.setStyleSheet(style.STYLESHEETS.updateRibbon); + this.updateButton.toolTip = storeExtension.package.description; + this.updateButton.clicked.connect(this, function () { storeUi.updateStore(currentVersion, storeVersion) }); } else { - this.aboutFrame.updateButton.hide(); + this.updateButton.hide(); updateRibbon.storeVersion.setText("v" + currentVersion + " ✓ - Store is up to date."); - updateRibbon.setStyleSheet(defaultRibbonStyleSheet); + updateRibbon.setStyleSheet(style.STYLESHEETS.defaultRibbon); } } catch (err) { // couldn't check updates, probably we don't have an internet access. @@ -376,10 +380,10 @@ StoreUI.prototype.checkForUpdates = function () { * @param {*} message */ StoreUI.prototype.lockStore = function (message) { - var noConnexionRibbonStyleSheet = style.STYLESHEETS.noConnexionRibbon; + var noConnexionRibbonStyleSheet = style.STYLESHEETS.failureRibbon; this.ui.aboutFrame.loadStoreButton.enabled = false; - this.ui.aboutFrame.updateButton.hide(); + this.updateButton.hide(); this.updateRibbon.setStyleSheet(noConnexionRibbonStyleSheet); this.updateRibbon.storeVersion.setText(message); } @@ -389,14 +393,48 @@ StoreUI.prototype.lockStore = function (message) { * installs the version of the store found on the repo. */ StoreUI.prototype.updateStore = function (currentVersion, storeVersion) { - var success = this.localList.install(this.storeExtension, this.ui.aboutFrame.updateButton); + // Store shouldn't load after update until it's been reloaded. + this.loadStoreButton.setStyleSheet(style.STYLESHEETS.InstallButtonInvisible); + this.loadStoreButton.setGraphicsEffect(null); + this.loadStoreButton.enabled = false; + this.loadStoreButton.toolTip = ""; + + // set progress directly once to make the button feel more reponsive while thhe store fetches info + this.updateButton.setProgress(0.001); + + log.info("installing extension : " + this.storeExtension.repository.name + this.storeExtension.name); + var installer = this.storeExtension.installer; + + // Log store update error. + this.failure = function (err){ + log.error(err); + } + + // Connect the installer signals to the update button. + installer.onInstallProgressChanged.connect(this.updateButton, this.updateButton.setProgress); + installer.onInstallFailed.connect(this, this.failure); + installer.onInstallFailed.connect(this.updateButton, this.updateButton.setFailState); + + // Attempt the actual install. + var success = this.localList.install(this.storeExtension); if (success) { - MessageBox.information("Store succesfully updated to version v" + storeVersion + ".\n\nPlease close and reopen HUES for changes to take effect."); + // Updated successfully - Adjust UI to indicate update success without shifting the UI. this.updateRibbon.storeVersion.setText("v" + currentVersion); this.updateRibbon.setStyleSheet(style.STYLESHEETS.defaultRibbon); - this.ui.aboutFrame.updateButton.setStyleSheet(style.STYLESHEETS.updateButtonInvisible); - this.ui.aboutFrame.updateButton.setGraphicsEffect(null); + this.updateButton.setStyleSheet(style.STYLESHEETS.updateButtonSuccess); + this.updateButton.maximumWidth = 500; + this.updateButton.text = "Please reload HUES to apply the update."; + this.updateButton.setGraphicsEffect(null); + this.updateButton.toolTip = ""; + this.updateButton.enabled = false; + + MessageBox.information("Store succesfully updated to version v" + storeVersion + ".\n\nPlease close and reopen HUES for changes to take effect."); + } else { + // Update failed - set to the RED failure style. + this.updateRibbon.setStyleSheet(style.STYLESHEETS.failureRibbon); + this.updateButton.setFailState(); + this.updateButton.enabled = false; MessageBox.information("There was a problem updating to v" + storeVersion + ".\n\n The update was not successful."); } } diff --git a/ExtensionStore/lib/style.js b/ExtensionStore/lib/style.js index 7c0a787..e4b8bf2 100644 --- a/ExtensionStore/lib/style.js +++ b/ExtensionStore/lib/style.js @@ -38,14 +38,15 @@ const COLORS = isDarkStyle() ? ColorsDark : ColorsLight; const styleSheetsDark = { defaultRibbon : "QWidget { background-color: transparent; color: gray;}", updateRibbon : "QWidget { background-color: " + COLORS.YELLOW + "; color: black }", - noConnexionRibbon : "QWidget { background-color: " + COLORS.RED + "; color: white; }", + failureRibbon : "QWidget { background-color: " + COLORS.RED + "; color: white; }", progressButton : "QToolButton { border-color: transparent transparent @ACCENT transparent; }", installButton : "QToolButton { border-color: transparent transparent " + COLORS.GREEN + " transparent; }", installFailedButton : "QToolButton { border-color: transparent transparent " + COLORS.RED + " transparent; }", uninstallButton : "QToolButton { border-color: transparent transparent " + COLORS.ORANGE + " transparent; }", updateButton : "QToolButton { border-color: transparent transparent " + COLORS.YELLOW + " transparent; }", + updateButtonSuccess: "QToolButton { background-color: transparent; border-color: transparent; color: " + COLORS.YELLOW + "; font-family: Arial Black; font-size: " + UiLoader.dpiScale(11) + "pt;}", + InstallButtonInvisible : "QToolButton { border-color: transparent; background-color: transparent; color: transparent; }", loadButton : "QToolButton { border-color: transparent transparent " + COLORS.ACCENT_LIGHT + " transparent; }", - updateButtonInvisible : "QPushButton { border-color: transparent; background-color: transparent; color: transparent; }", } // Enum to hold light style stylesheets. diff --git a/ExtensionStore/resources/store.ui b/ExtensionStore/resources/store.ui index b0e7dfe..df85444 100644 --- a/ExtensionStore/resources/store.ui +++ b/ExtensionStore/resources/store.ui @@ -1108,31 +1108,6 @@ - - - - - 0 - 0 - - - - - 130 - 30 - - - - - 120 - 16777215 - - - - Install Update - - - @@ -1173,7 +1148,10 @@ - v0.0.1 + <html><head/><body><p><span style=" font-size:9pt;">v0.0.1</span></p></body></html> + + + Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft diff --git a/ExtensionStore/resources/stylesheet_dark.qss b/ExtensionStore/resources/stylesheet_dark.qss index 0f23f50..7718987 100644 --- a/ExtensionStore/resources/stylesheet_dark.qss +++ b/ExtensionStore/resources/stylesheet_dark.qss @@ -212,14 +212,14 @@ QToolButton:pressed#loadStoreButton { /* Update Ribbon store version text */ QLabel#storeVersion { - font-family: Arial; - font-size: 10pt; - padding-left: 10px; - padding-bottom: 5px; + font-family: Palatino; + font-size: 9pt; + padding-left: 8px; + margin-bottom: -2px; } /* Update Store Button */ -QPushButton#updateButton { +QToolButton#updateButton { padding: 5px; color: black; background-color: @YELLOW; From dc715cd26b25ea46198fab8b7b3fc6bf1c00bae4 Mon Sep 17 00:00:00 2001 From: bob-ross27 <55351437+bob-ross27@users.noreply.github.com> Date: Tue, 15 Jun 2021 20:17:19 -0300 Subject: [PATCH 095/112] Uninstall extension prior to update. --- ExtensionStore/app.js | 21 +++++++++++++++++++++ ExtensionStore/lib/store.js | 29 +++++++++++++++++------------ 2 files changed, 38 insertions(+), 12 deletions(-) diff --git a/ExtensionStore/app.js b/ExtensionStore/app.js index 60265c4..78519ea 100644 --- a/ExtensionStore/app.js +++ b/ExtensionStore/app.js @@ -415,6 +415,15 @@ StoreUI.prototype.updateStore = function (currentVersion, storeVersion) { installer.onInstallFailed.connect(this, this.failure); installer.onInstallFailed.connect(this.updateButton, this.updateButton.setFailState); + // Remove existing files before updating the store. + try { + this.localList.uninstall(this.storeExtension); + } + catch (err) { + // Only log errors as it's not a crucial step in updating the extension. + log.debug("Unable to remove local files before updating store. " + err); + } + // Attempt the actual install. var success = this.localList.install(this.storeExtension); if (success) { @@ -615,6 +624,18 @@ StoreUI.prototype.performInstall = function () { installer.onInstallFailed.connect(this, this.failure); installer.onInstallFailed.connect(this.installButton, this.installButton.setFailState); + // If the extension is already installed, this is an update operation. Remove local files before continuing + // to clean up the filesystem. + if (this.localList.isInstalled(extension)) { + try { + this.localList.uninstall(extension); + } + catch (err) { + // Not a critical failure, log and continue. + log.debug("Unable to remove local files before updating extension. " + err); + } + } + // Attempt to install the extension. var extensionInstalled = this.localList.install(extension); diff --git a/ExtensionStore/lib/store.js b/ExtensionStore/lib/store.js index 63ff29e..39aac41 100644 --- a/ExtensionStore/lib/store.js +++ b/ExtensionStore/lib/store.js @@ -1109,20 +1109,25 @@ LocalExtensionList.prototype.uninstall = function (extension) { if (!this.isInstalled(extension)) return true // extension isn't installed var localExtension = this.extensions[extension.id]; - // Remove packages recursively as they have a parent directory. - if (localExtension.isPackage) { - var folder = new Dir(this.installFolder + "/packages/" + localExtension.safeName); - this.log.debug("removing folder " + folder.path); - if (folder.exists) folder.rmdirs(); - } else { - // Otherwise remove all script files (.js, .ui, .png etc.) - var files = localExtension.package.localFiles; - for (var i in files) { - this.log.debug("removing file " + files[i]); - var file = new File(files[i]); - if (file.exists) file.remove(); + try { + // Remove packages recursively as they have a parent directory. + if (localExtension.isPackage) { + var folder = new Dir(this.installFolder + "/packages/" + localExtension.safeName); + this.log.debug("removing folder " + folder.path); + if (folder.exists) folder.rmdirs(); + } else { + // Otherwise remove all script files (.js, .ui, .png etc.) + var files = localExtension.package.localFiles; + for (var i in files) { + this.log.debug("removing file " + files[i]); + var file = new File(files[i]); + if (file.exists) file.remove(); + } } } + catch (err) { + this.log.debug(err); + } // Update the extension list accordingly. this.removeFromList(extension); From 05a5abd2374fb074a2dc83f248a1768b46e1cf1a Mon Sep 17 00:00:00 2001 From: MathieuC Date: Wed, 16 Jun 2021 21:08:46 +0200 Subject: [PATCH 096/112] add LocalList.update() to handle removal of existing extension --- ExtensionStore/app.js | 7 +------ ExtensionStore/lib/store.js | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/ExtensionStore/app.js b/ExtensionStore/app.js index 78519ea..035dd51 100644 --- a/ExtensionStore/app.js +++ b/ExtensionStore/app.js @@ -417,12 +417,7 @@ StoreUI.prototype.updateStore = function (currentVersion, storeVersion) { // Remove existing files before updating the store. try { - this.localList.uninstall(this.storeExtension); - } - catch (err) { - // Only log errors as it's not a crucial step in updating the extension. - log.debug("Unable to remove local files before updating store. " + err); - } + this.localList.update(this.storeExtension); // Attempt the actual install. var success = this.localList.install(this.storeExtension); diff --git a/ExtensionStore/lib/store.js b/ExtensionStore/lib/store.js index 39aac41..d0cc25d 100644 --- a/ExtensionStore/lib/store.js +++ b/ExtensionStore/lib/store.js @@ -1144,6 +1144,21 @@ LocalExtensionList.prototype.uninstall = function (extension) { throw new Error("Unable to delete one or more local extension files during uninstall."); } +/** + * Updates the installation by removing then reinstalling the extension + * @param {Extension} extension - The extension to be removed locally. + * @returns {boolean} the success of the uninstallation. + */ + LocalExtensionList.prototype.update = function (extension) { + this.log.debug("updating extension "+ extension.name); + if (this.isInstalled(extension)){ + // uninstall first to clear any parasitic files + this.uninstall(extension); + } + this.install(extension); +} + + /** * Adds an extension to the installed list From 5efb8370e9cabada7938beb47cb29d76f62809b3 Mon Sep 17 00:00:00 2001 From: MathieuC Date: Wed, 16 Jun 2021 21:09:34 +0200 Subject: [PATCH 097/112] simplify errors handling --- ExtensionStore/app.js | 26 ++++++-------------- ExtensionStore/lib/store.js | 47 +++++++++++++++++-------------------- 2 files changed, 28 insertions(+), 45 deletions(-) diff --git a/ExtensionStore/app.js b/ExtensionStore/app.js index 035dd51..94f05eb 100644 --- a/ExtensionStore/app.js +++ b/ExtensionStore/app.js @@ -238,7 +238,7 @@ StoreUI.prototype.setStoreLoadUIState = function (enabled) { if (enabled) { // Hide elements during store load without removing them from the UI to avoid elements shifting. this.updateButton.setStyleSheet(style.STYLESHEETS.InstallButtonInvisible); - this.updateButton.setGraphicsEffect(null); + this.updateButton.setGraphicsEffect(null); this.aboutFrame.updateRibbon.setStyleSheet(style.STYLESHEETS.defaultRibbon); this.aboutFrame.updateRibbon.storeVersion.toolTip = this.aboutFrame.updateRibbon.storeVersion.text; this.aboutFrame.updateRibbon.storeVersion.text = ""; @@ -419,9 +419,6 @@ StoreUI.prototype.updateStore = function (currentVersion, storeVersion) { try { this.localList.update(this.storeExtension); - // Attempt the actual install. - var success = this.localList.install(this.storeExtension); - if (success) { // Updated successfully - Adjust UI to indicate update success without shifting the UI. this.updateRibbon.storeVersion.setText("v" + currentVersion); this.updateRibbon.setStyleSheet(style.STYLESHEETS.defaultRibbon); @@ -433,8 +430,10 @@ StoreUI.prototype.updateStore = function (currentVersion, storeVersion) { this.updateButton.enabled = false; MessageBox.information("Store succesfully updated to version v" + storeVersion + ".\n\nPlease close and reopen HUES for changes to take effect."); - - } else { + } + catch (err) { + // Only log errors as it's not a crucial step in updating the extension. + log.debug("Unable to remove local files before updating store. " + err); // Update failed - set to the RED failure style. this.updateRibbon.setStyleSheet(style.STYLESHEETS.failureRibbon); this.updateButton.setFailState(); @@ -613,23 +612,12 @@ StoreUI.prototype.performInstall = function () { // Log extension installation error. this.failure = function (err){ log.error(err); + this.installButton.setFailState() + this.installing = false; } installer.onInstallProgressChanged.connect(this.installButton, this.installButton.setProgress); installer.onInstallFailed.connect(this, this.failure); - installer.onInstallFailed.connect(this.installButton, this.installButton.setFailState); - - // If the extension is already installed, this is an update operation. Remove local files before continuing - // to clean up the filesystem. - if (this.localList.isInstalled(extension)) { - try { - this.localList.uninstall(extension); - } - catch (err) { - // Not a critical failure, log and continue. - log.debug("Unable to remove local files before updating extension. " + err); - } - } // Attempt to install the extension. var extensionInstalled = this.localList.install(extension); diff --git a/ExtensionStore/lib/store.js b/ExtensionStore/lib/store.js index d0cc25d..4ec862c 100644 --- a/ExtensionStore/lib/store.js +++ b/ExtensionStore/lib/store.js @@ -1085,18 +1085,16 @@ LocalExtensionList.prototype.install = function (extension) { delete extension._installer; } - installer.onInstallFinished.connect(this, copyFiles); - - // Try to download the extension files. - try { - installer.downloadFiles(); - return true; - } - catch (error) { + function handleFailed(error){ this.log.debug("Unable to install extension: " + error); delete extension._installer; - return false; } + + // Try to download the extension files. + installer.onInstallFinished.connect(this, copyFiles); + installer.onInstallFailed.connect(this, handleFailed); + + installer.downloadFiles(); } @@ -1108,26 +1106,22 @@ LocalExtensionList.prototype.install = function (extension) { LocalExtensionList.prototype.uninstall = function (extension) { if (!this.isInstalled(extension)) return true // extension isn't installed var localExtension = this.extensions[extension.id]; + this.log.debug("uninstalling extension "+extension.name) - try { - // Remove packages recursively as they have a parent directory. - if (localExtension.isPackage) { - var folder = new Dir(this.installFolder + "/packages/" + localExtension.safeName); - this.log.debug("removing folder " + folder.path); - if (folder.exists) folder.rmdirs(); - } else { - // Otherwise remove all script files (.js, .ui, .png etc.) - var files = localExtension.package.localFiles; - for (var i in files) { - this.log.debug("removing file " + files[i]); - var file = new File(files[i]); - if (file.exists) file.remove(); - } + // Remove packages recursively as they have a parent directory. + if (localExtension.isPackage) { + var folder = new Dir(this.installFolder + "/packages/" + localExtension.safeName); + this.log.debug("removing folder " + folder.path); + if (folder.exists) folder.rmdirs(); + } else { + // Otherwise remove all script files (.js, .ui, .png etc.) + var files = localExtension.package.localFiles; + for (var i in files) { + this.log.debug("removing file " + files[i]); + var file = new File(files[i]); + if (file.exists) file.remove(); } } - catch (err) { - this.log.debug(err); - } // Update the extension list accordingly. this.removeFromList(extension); @@ -1352,6 +1346,7 @@ ExtensionInstaller.prototype.downloadFiles = function () { try{ webQuery.download(this.getDownloadUrl(files[i].path), destPaths[i]); var dlFile = new File(destPaths[i]); + if (dlFile.size == files[i].size) { // download complete! this.log.debug("successfully downloaded " + files[i].path + " to location : " + destPaths[i]); From 409156f45542f6613213db5def9123419ff11f51 Mon Sep 17 00:00:00 2001 From: MathieuC Date: Wed, 16 Jun 2021 21:20:38 +0200 Subject: [PATCH 098/112] remove extensionInstalled variable reliance --- ExtensionStore/app.js | 36 ++++++++++++++--------------- ExtensionStore/lib/store.js | 46 +++++++++++++++++++------------------ 2 files changed, 41 insertions(+), 41 deletions(-) diff --git a/ExtensionStore/app.js b/ExtensionStore/app.js index 94f05eb..970f3c9 100644 --- a/ExtensionStore/app.js +++ b/ExtensionStore/app.js @@ -620,26 +620,24 @@ StoreUI.prototype.performInstall = function () { installer.onInstallFailed.connect(this, this.failure); // Attempt to install the extension. - var extensionInstalled = this.localList.install(extension); - - // If extension install failed - alert user. - // Not in failure function to avoid being called for each failed proc, and to only appear after InstallButton has changed to a failed state. - if (!extensionInstalled) { - MessageBox.information("There was an error while installing extension\n" + extension.name + " v" + extension.version + ":\n\n"); - } - - // delay refresh after install completes - var timer = new QTimer(); - timer.singleShot = true; - timer["timeout"].connect(this, function() { - this.installing = false; - this.localList.refreshExtensions(); - if (extensionInstalled) { + try{ + this.localList.install(extension); + + // delay refresh after install completes + var timer = new QTimer(); + timer.singleShot = true; + timer["timeout"].connect(this, function() { + this.installing = false; + this.localList.refreshExtensions(); this.updateExtensionsList(); - } - this.updateDescriptionPanel(); - }); - timer.start(700); + this.updateDescriptionPanel(); + }); + timer.start(1000); + }catch(error) { + // If extension install failed - alert user. + // Not in failure function to avoid being called for each failed proc, and to only appear after InstallButton has changed to a failed state. + MessageBox.information("There was an error while installing extension\n" + extension.name + " v" + extension.version + ":\n\n" + error); + } } diff --git a/ExtensionStore/lib/store.js b/ExtensionStore/lib/store.js index 4ec862c..c3df193 100644 --- a/ExtensionStore/lib/store.js +++ b/ExtensionStore/lib/store.js @@ -1325,25 +1325,27 @@ ExtensionInstaller.prototype.downloadFiles = function () { this.log.info("starting download of files from extension " + this.extension.name); var destFolder = this.destFolder; - // get the files list (heavy operations) - this.onInstallProgressChanged.emit(0.1); // show the progress bar starting - var destPaths = this.extension.localPaths.map(function (x) { return destFolder + x }); - var dlFiles = [this.destFolder]; - var files = this.extension.files; - - this.log.debug("downloading files : "+files.map(function(x){return x.path}).join("\n")) - - // If the file list is empty, there was likely an issue parsing the tbpackage and the operaiton - // should be aborted. - if (!files.length) { - this.onInstallFailed.emit("No files found to download."); - throw new Error("No files found to download"); - }; - - // Download each file and update the progress - for (var i = 0; i < files.length; i++) { - this.onInstallProgressChanged.emit((i+1)/(files.length+1)); - try{ + // wrap in a try catch to forward the error as a signal to be handled by the ui + try{ + // get the files list (heavy operations) + this.onInstallProgressChanged.emit(0.1); // show the progress bar starting + var destPaths = this.extension.localPaths.map(function (x) { return destFolder + x }); + var dlFiles = [this.destFolder]; + var files = this.extension.files; + + + this.log.debug("downloading files : "+files.map(function(x){return x.path}).join("\n")) + + // If the file list is empty, there was likely an issue parsing the tbpackage and the operaiton + // should be aborted. + if (!files.length) { + this.onInstallFailed.emit("No files found to download."); + throw new Error("No files found to download"); + }; + + // Download each file and update the progress + for (var i = 0; i < files.length; i++) { + this.onInstallProgressChanged.emit((i+1)/(files.length+1)); webQuery.download(this.getDownloadUrl(files[i].path), destPaths[i]); var dlFile = new File(destPaths[i]); @@ -1355,10 +1357,10 @@ ExtensionInstaller.prototype.downloadFiles = function () { var error = new Error("Downloaded file " + destPaths[i] + " size does not match expected size : \n" + dlFile.size + " bytes (expected : " + files[i].size+" bytes)"); throw error; } - }catch(error){ - this.onInstallFailed.emit(error); - throw error; } + }catch(error){ + this.onInstallFailed.emit(error); + throw error; } this.onInstallProgressChanged.emit(1); From 5f28ce5d7dd6bc680b89a87f5baedd7ca02d1d26 Mon Sep 17 00:00:00 2001 From: bob-ross27 <55351437+bob-ross27@users.noreply.github.com> Date: Thu, 1 Jul 2021 14:20:54 -0300 Subject: [PATCH 099/112] Give user an indication that the extension list is updating, as it can be length on first load. --- ExtensionStore/app.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ExtensionStore/app.js b/ExtensionStore/app.js index 970f3c9..1af53ba 100644 --- a/ExtensionStore/app.js +++ b/ExtensionStore/app.js @@ -278,6 +278,12 @@ StoreUI.prototype.loadStore = function () { return } + // Update UI as updating the extension list on first load can be time intensive. + this.loadStoreButton.maximumWidth = 500; + this.loadStoreButton.text = "Updating extension list..."; + this.loadStoreButton.toolTip = ""; + this.loadStoreButton.enabled = false; + // saving the list of extensions so we can pinpoint the new ones at next startup and highlight them var oldExtensions = this.localList.getData("extensions", []) var newExtensions = this.localList.getData("newExtensions", []) From ec5f993dc85ffb0a756aaa032244cdf4e503b105 Mon Sep 17 00:00:00 2001 From: bob-ross27 <55351437+bob-ross27@users.noreply.github.com> Date: Thu, 1 Jul 2021 14:23:11 -0300 Subject: [PATCH 100/112] Reverted extension.safeName replacement character changing to '_' --- ExtensionStore/lib/store.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ExtensionStore/lib/store.js b/ExtensionStore/lib/store.js index c3df193..bc1d583 100644 --- a/ExtensionStore/lib/store.js +++ b/ExtensionStore/lib/store.js @@ -904,7 +904,7 @@ Object.defineProperty(Extension.prototype, "isPackage", { */ Object.defineProperty(Extension.prototype, "safeName", { get: function () { - return this.name.replace(/ /g, "_").replace(/[:\?\*\\\/"\|\<\>]/g, "") + return this.name.replace(/ /g, "").replace(/[:\?\*\\\/"\|\<\>]/g, "") } }) From 95247a53ec44a5feb075992ef1506274af037ee9 Mon Sep 17 00:00:00 2001 From: bob-ross27 <55351437+bob-ross27@users.noreply.github.com> Date: Thu, 1 Jul 2021 14:29:05 -0300 Subject: [PATCH 101/112] Updated tbpackage to reflect the 0.3.0 release. --- ExtensionStore/tbpackage.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ExtensionStore/tbpackage.json b/ExtensionStore/tbpackage.json index a1a9cba..8b38e2a 100644 --- a/ExtensionStore/tbpackage.json +++ b/ExtensionStore/tbpackage.json @@ -1,12 +1,12 @@ { "name": "ExtensionStore", - "version": "0.2.6", + "version": "0.3.0", "compatibility": "Harmony Premium 16", - "description": "Changelog: Improved error handling during loading.", + "description": "Changelog: UI Overhaul.", "isPackage": "true", "repository": "https://github.com/mchaptel/ExtensionStore/", "files": [ - "/ExtensionStore/" + "ExtensionStore/" ], "keywords": [ "store", From cc33baa586723b8ef1d5f2b08c01d70284904bcf Mon Sep 17 00:00:00 2001 From: MathieuC Date: Sun, 11 Jul 2021 00:10:32 +0200 Subject: [PATCH 102/112] simplify setFailed function using setProgress for the stylesheet --- ExtensionStore/lib/widgets.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ExtensionStore/lib/widgets.js b/ExtensionStore/lib/widgets.js index 689fa24..eff2e6d 100644 --- a/ExtensionStore/lib/widgets.js +++ b/ExtensionStore/lib/widgets.js @@ -6,7 +6,7 @@ var log = new Logger("Widgets"); * A QWebView to display the description * @param {QWidget} parent */ - function DescriptionView(parent){ +function DescriptionView(parent){ var webPreviewsFontFamily = "Arial"; var webPreviewsFontSize = UiLoader.dpiScale(12); @@ -28,7 +28,7 @@ DescriptionView.prototype = Object.create(QWebView.prototype) * @param {storelib.LocalExtensionList} localList the list of extensions installed on this machine * @param {QTreeWidget} parent the parent widget for this item */ - function ExtensionItem(extension, localList, parent) { +function ExtensionItem(extension, localList, parent) { this._parent = parent // this is the QTreeWidget var newExtensions = localList.getData("newExtensions", []); var extensionLabel = extension.name; @@ -226,7 +226,7 @@ Object.defineProperty(InstallButton.prototype, "mode", { * Disable progress updates on the button, and set the button mode/stylesheet to indicate failure of * an extension install. */ - InstallButton.prototype.setFailState = function() { +InstallButton.prototype.setFailState = function() { // Only needs to run once. if (this.hasFailed) { return; @@ -234,8 +234,8 @@ Object.defineProperty(InstallButton.prototype, "mode", { this.hasFailed = true; this.mode = "FAIL"; + this.setProgress(100); this.text = "Failed"; - this.setStyleSheet("QToolButton { background-color: " + style.COLORS.RED + "; border-color: transparent; color: white}"); } @@ -381,7 +381,7 @@ ProgressBar.prototype = Object.create(QProgressBar.prototype); /** - * Transform the input value from the input range (0=>1) to the range expected + * Transform the input value from the input range (0=>1) to the range expected * by the QProgressBar (0=>100). Set the progressbar value with the remapped value.. * @param {number} value - Progress as a percentage with a range of 0 => 1. */ From b53d97d81d5a50f284cbb0b60b98ebf178d798a9 Mon Sep 17 00:00:00 2001 From: MathieuC Date: Sun, 11 Jul 2021 01:16:51 +0200 Subject: [PATCH 103/112] add possibility to change button message during progress setting --- ExtensionStore/lib/widgets.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ExtensionStore/lib/widgets.js b/ExtensionStore/lib/widgets.js index eff2e6d..ad70a57 100644 --- a/ExtensionStore/lib/widgets.js +++ b/ExtensionStore/lib/widgets.js @@ -113,7 +113,8 @@ Object.defineProperty(ProgressButton.prototype, "accentColor", { * Use the background stylesheet of the widget to act as a progress bar. * @param {Int} progress - Value from 0 to 1 that the operation is currently at. */ -ProgressButton.prototype.setProgress = function (progress) { +ProgressButton.prototype.setProgress = function (progress, message) { + if (typeof message === 'undefined') var message = this.progressText; // Disable progress updates if the button has failed. hasFailed is Implemented in child classes. if (this.hasFailed) { @@ -149,7 +150,7 @@ ProgressButton.prototype.setProgress = function (progress) { this.setStyleSheet(progressStyleSheet); // Update text with progress - this.text = this.progressText + " " + Math.round((progressStopR * 100)) + "%"; + this.text = message + " " + Math.round((progressStopR * 100)) + "%"; } else { // Configure widget to indicate the operation is complete. From b80575a9f8f9fa2038975340d6962d8c6f36e9ab Mon Sep 17 00:00:00 2001 From: MathieuC Date: Sun, 11 Jul 2021 01:18:31 +0200 Subject: [PATCH 104/112] get icon filename for webIcons from start of url except for github images --- ExtensionStore/lib/network.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/ExtensionStore/lib/network.js b/ExtensionStore/lib/network.js index 19b597f..b7d8641 100644 --- a/ExtensionStore/lib/network.js +++ b/ExtensionStore/lib/network.js @@ -87,13 +87,17 @@ Object.defineProperty(WebIcon.prototype, "dlPath", { return this._dlPath; } - var fileName = this.url.split("/").pop(); + // get the name from the domain of the url... + var url = this.url.split("/"); + var fileName = url.shift(); + while (!fileName || fileName.indexOf("http")!=-1) fileName = url.shift(); - var userNameRe = /https:\/\/github.com\/([\w\d]+\.png)/ - var matches = userNameRe.exec(this.url); + //... except for images coming from github (avatars/icons) + var githubIconsRe = /https:\/\/.*github.*\.com\/.*?([^\/]+\.png)$/ + var matches = githubIconsRe.exec(this.url); if (matches){ - // we have a github avatar url + // we have a github avatar/icon url fileName = matches[1]; } else if (this.url.indexOf(".png") == -1) { // dealing with a website, we'll get the favicon From 13f0bb9c628fa2c269a7f199bb2e5e30729a854a Mon Sep 17 00:00:00 2001 From: MathieuC Date: Sun, 11 Jul 2021 01:19:03 +0200 Subject: [PATCH 105/112] add counter for api.github.com calls for debug --- ExtensionStore/lib/network.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/ExtensionStore/lib/network.js b/ExtensionStore/lib/network.js index b7d8641..227311f 100644 --- a/ExtensionStore/lib/network.js +++ b/ExtensionStore/lib/network.js @@ -244,6 +244,22 @@ function CURLProcess(command, bin) { CURLProcess.prototype.asyncRead = function (readCallback, finishedCallback, asText) { this.log.debug("Executing Process with arguments : " + this.app + " " + this.command.join(" ")); + // keep track of the number of times we access the github api + // only useful for testing as this gets reset if harmony is closed. + if (!CURLProcess.__proto__.githubApiCounter) CURLProcess.__proto__.githubApiCounter = []; + var apiCounter = CURLProcess.__proto__.githubApiCounter; + var now = (new Date()).getTime(); + + // remove counters older than an hour + for (var i=apiCounter.length; i>=0; i--){ + if (apiCounter[i] + 3600000 < now ) apiCounter.splice(i,1); + } + + if (this.command.join("").indexOf("api.github.com") != -1){ + apiCounter.push(now); + log.debug(apiCounter.length+" attempts to connect to github api within the last hour."); + } + this.process.start(this.app, this.command); if (typeof readCallback !== 'undefined' && readCallback) { var onRead = function () { From e002367d839374b6c21a812b31457c7823fddc9a Mon Sep 17 00:00:00 2001 From: MathieuC Date: Sun, 11 Jul 2021 01:19:43 +0200 Subject: [PATCH 106/112] centralise curl errors in CURLProcess class --- ExtensionStore/lib/network.js | 46 ++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/ExtensionStore/lib/network.js b/ExtensionStore/lib/network.js index 227311f..0b28c11 100644 --- a/ExtensionStore/lib/network.js +++ b/ExtensionStore/lib/network.js @@ -7,8 +7,6 @@ var readFile = require("io.js").readFile * @constructor * @classdesc * The NetworkConnexionHandler class handles web queries and downloads. It uses curl for communicating with the remote apis.
- * This class extends QObject so it can broadcast signals and be threaded. - * @extends QObject */ function NetworkConnexionHandler() { } @@ -21,28 +19,32 @@ NetworkConnexionHandler.prototype.get = function (command) { // handle errors var curl = new CURLProcess(command) var result = curl.get(); - try { - json = JSON.parse(result); - if (json.hasOwnProperty("message")) { - if (json.message == "Not Found") { - log.error("File not present in repository : " + command); - return null; - } - if (json.message == "Moved Permanently") { - log.debug("Repository " + command + " has moved to : " + json.url); - return json; - } - if (json.message == "400: Invalid request") { - log.error("Couldn't reach repository : " + command + ". Make sure it is a valid github address.") - return null; - } + try{ + var json = JSON.parse(result); + }catch(err){ + throw new Error ("command " + command + " did not return a valid JSON :\n" + result); + } + if (json.hasOwnProperty("message")) { + if (json.message == "Not Found") { + throw new Error ("File not present in repository : " + command); + } + // if api limit reached, throw an error + if (json.message.indexOf("API rate limit exceeded") != -1) { + var limitInfo = this.get("https://api.github.com/rate_limit"); + var wait = Math.round((parseInt(limitInfo.rate.reset+"000", 10) - (new Date()).getTime())/60000); + throw new Error ("Github api is limited to 60 calls within one hour.\nPlease try again after "+wait+" minutes.") + } + // handle falty redirection in github response + if (json.message == "Moved Permanently") { + log.debug("Repository " + command + " has moved to : " + json.url); + var result = this.get(json.url.replace("repositories", "repos")); + return result; + } + if (json.message == "400: Invalid request") { + throw new Error ("Couldn't reach repository : " + command + ". Make sure it is a valid github address.") } - return json; - } catch (error) { - var message = ("command " + command + " did not return a valid JSON : " + result); - log.error(message, error); - throw new Error(message); } + return json; } From d961fad78817729c014b8345e01c6bee6cefbc74 Mon Sep 17 00:00:00 2001 From: MathieuC Date: Sun, 11 Jul 2021 01:22:29 +0200 Subject: [PATCH 107/112] remove errors handling from masterBranchTree --- ExtensionStore/lib/store.js | 37 ++++++++++++++++--------------------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/ExtensionStore/lib/store.js b/ExtensionStore/lib/store.js index bc1d583..c544c63 100644 --- a/ExtensionStore/lib/store.js +++ b/ExtensionStore/lib/store.js @@ -47,7 +47,7 @@ Object.defineProperty(Store.prototype, "sellers", { if (typeof this._sellers === 'undefined') { this.log.debug("getting sellers"); // set progress directly once to make the button feel more reponsive while thhe store fetches info - this.onLoadProgressChanged.emit(0.001); + this.onLoadProgressChanged.emit(0.001, "Loading..."); // the sellers list can be overriden with an environment variable for local studio installs var sellersFile = System.getenv("HUES_SELLERS_PATH"); @@ -73,7 +73,7 @@ Object.defineProperty(Store.prototype, "sellers", { var seller = new Seller(sellersList[i]); var package = seller.package; validSellers.push(seller); - this.onLoadProgressChanged.emit((i+1) / (sellersList.length+1)); + this.onLoadProgressChanged.emit((i+1) / (sellersList.length+1), "Loading..."); } catch (error) { this.log.error("problem getting package for seller " + sellersList[i], error); } @@ -131,7 +131,7 @@ Object.defineProperty(Store.prototype, "extensions", { for (var i in extensions) { this._extensions[extensions[i].id] = extensions[i]; } - this.onLoadProgressChanged.emit(1); + this.onLoadProgressChanged.emit(1, "Done."); } return this._extensions; @@ -517,6 +517,7 @@ Object.defineProperty(Repository.prototype, "contents", { }catch(error){ // in case of bad query, we avoid pulling it over and over, and consider it empty this.log.error(error); + MessageBox.information(error); this._contents = []; return this._contents; } @@ -573,20 +574,13 @@ Object.defineProperty(Repository.prototype, "masterBranchTree", { get: function () { if (typeof this._tree === 'undefined') { // Try to get the master branch. - var response = webQuery.get(this.apiUrl + "branches/master"); - - // Return doesn't contain a commit - indicating it's likely an error - // or a redirect. - if (response && response.message === "Moved Permanently") { - // Redirect provided, so get from the provided url instead. - response = webQuery.get(response.url.replace("repositories", "repos")); - } + try{ + var response = webQuery.get(this.apiUrl + "branches/master"); - // Assign url or throw error if no valid branch could be detected. - if (response && response.commit) { + // Assign url or throw error if no valid branch could be detected. this._tree = response.commit.commit.tree.url; // the query returns a big object in which this is the address of the contents tree - } else { - throw new Error("Unable to find a valid branch."); + } catch (err) { + throw new Error("Unable to get files on repository "+this.name+":\n\n"+err); } } return this._tree @@ -613,17 +607,17 @@ Repository.prototype.getFiles = function (filter) { if (typeof filter === 'undefined') var filter = /.*/; var contents = this.contents; - var paths = this.contents.map(function(x){return x.path}) + var paths = this.contents.map(function(x){return x.path}); // this.log.debug(paths.join("\n")) - var search = this.searchToRe(filter) + var search = this.searchToRe(filter); - this.log.debug("getting files in repository that match search " + search) + this.log.debug("getting files in repository that match search " + search); - var results = [] + var results = []; for (var i in paths) { // add files that match the filter but not folders - if (paths[i].match(search) && paths[i].slice(-1)!="/") results.push(contents[i]) + if (paths[i].match(search) && paths[i].slice(-1)!="/") results.push(contents[i]); } return results; @@ -658,7 +652,7 @@ Repository.prototype.searchToRe = function (search) { */ function Extension(repository, tbpackage) { this.log = new Logger("Extension") - this.repository = repository + this.repository = repository; this._name = tbpackage.name; this.version = tbpackage.version; this.package = tbpackage; @@ -790,6 +784,7 @@ Object.defineProperty(Extension.prototype, "rootFolder", { /** * The complete list of files corresponding to this extension + * This costs one api.github.com call. * @name Extension#files * @type {object} * @example From 0ed242de522ca6f3a5a81fc39cec48d858d57afa Mon Sep 17 00:00:00 2001 From: MathieuC Date: Sun, 11 Jul 2021 01:24:28 +0200 Subject: [PATCH 108/112] use a signal to report detection of extensions during loading --- ExtensionStore/app.js | 12 +++++++++--- ExtensionStore/lib/store.js | 33 +++++++++++++++++++++++++-------- 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/ExtensionStore/app.js b/ExtensionStore/app.js index 1af53ba..6d72013 100644 --- a/ExtensionStore/app.js +++ b/ExtensionStore/app.js @@ -139,6 +139,7 @@ function StoreUI() { this.store.onLoadProgressChanged.connect(this.updateProgress, this.updateProgress.setProgress); this.store.onLoadProgressChanged.connect(this.loadStoreButton, this.loadStoreButton.setProgress); + this.localList.extensionsDetectionProgressChanged.connect(this.loadStoreButton, this.loadStoreButton.setProgress); // filter the store list -------------------------------------------- this.storeHeader.searchStore.textChanged.connect(this, this.updateExtensionsList) @@ -280,7 +281,7 @@ StoreUI.prototype.loadStore = function () { // Update UI as updating the extension list on first load can be time intensive. this.loadStoreButton.maximumWidth = 500; - this.loadStoreButton.text = "Updating extension list..."; + this.loadStoreButton.text = "Detecting installed extensions..."; this.loadStoreButton.toolTip = ""; this.loadStoreButton.enabled = false; @@ -453,8 +454,13 @@ StoreUI.prototype.updateStore = function (currentVersion, storeVersion) { * Updates the list widget displaying the extensions */ StoreUI.prototype.updateExtensionsList = function () { - if (this.localList.list.length == 0) this.localList.createListFile(this.store); - + if (this.localList.list.length == 0){ + try{ + this.localList.createListFile(this.store); + }catch(err){ + MessageBox.trace("Error during detection of existing extensions : "+err.message) + } + } log.debug("updating extensions list") function nameSort(a, b) { diff --git a/ExtensionStore/lib/store.js b/ExtensionStore/lib/store.js index c544c63..f7486f6 100644 --- a/ExtensionStore/lib/store.js +++ b/ExtensionStore/lib/store.js @@ -964,6 +964,7 @@ function LocalExtensionList(store) { this._installFolder = specialFolders.userScripts; // default install folder, can be modified with installFolder property this._listFile = specialFolders.userConfig + "/.extensionsList"; this._ini = specialFolders.userConfig + "/.extensionStorePrefs" + this.extensionsDetectionProgressChanged = new Signal(); } @@ -1213,18 +1214,34 @@ LocalExtensionList.prototype.refreshExtensions = function () { */ LocalExtensionList.prototype.findInstalledExtensions = function (store) { var installedExtensions = []; + + var progressMessage = "Detecting installed extensions..." + this.extensionsDetectionProgressChanged.emit(0.001, progressMessage); + + var count = 0; + var length = Object.keys(store.extensions).length; + for (var i in store.extensions) { - var extension = store.extensions[i]; - var destPath = this.installLocation(extension); - var extensionFiles = extension.localPaths.map(function (x) { return destPath + x }); - for (var i in extensionFiles) { - if (new File(extensionFiles[i]).exists) { - // found an extension, we add it to the list - installedExtensions.push(extension); - continue; + try{ + // this may fail in case of reaching api limit. + this.extensionsDetectionProgressChanged.emit(count++/(length+1), progressMessage); + + var extension = store.extensions[i]; + var destPath = this.installLocation(extension); + var extensionFiles = extension.localPaths.map(function (x) { return destPath + x }); + for (var i in extensionFiles) { + if (new File(extensionFiles[i]).exists) { + // found an extension, we add it to the list + installedExtensions.push(extension); + continue; + } } + }catch(err){ + throw new Error ("error during detection of installed extensions: " + err); } } + this.extensionsDetectionProgressChanged.emit(1, progressMessage); + return installedExtensions; } From 73f05f410483fce5afdfe781c5854d395943ee96 Mon Sep 17 00:00:00 2001 From: MathieuC Date: Sun, 11 Jul 2021 02:25:49 +0200 Subject: [PATCH 109/112] override progressMessage/finishedText with message param if given in ProgressDialog.setProgress() --- ExtensionStore/lib/widgets.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/ExtensionStore/lib/widgets.js b/ExtensionStore/lib/widgets.js index ad70a57..af4de2e 100644 --- a/ExtensionStore/lib/widgets.js +++ b/ExtensionStore/lib/widgets.js @@ -114,7 +114,6 @@ Object.defineProperty(ProgressButton.prototype, "accentColor", { * @param {Int} progress - Value from 0 to 1 that the operation is currently at. */ ProgressButton.prototype.setProgress = function (progress, message) { - if (typeof message === 'undefined') var message = this.progressText; // Disable progress updates if the button has failed. hasFailed is Implemented in child classes. if (this.hasFailed) { @@ -130,7 +129,6 @@ ProgressButton.prototype.setProgress = function (progress, message) { // Operation in progress if (progress < 1) { this.enabled = false; - this.text = this.progressText; // Set stylesheet to act as a progressbar. var progressStopR = progress; @@ -150,13 +148,13 @@ ProgressButton.prototype.setProgress = function (progress, message) { this.setStyleSheet(progressStyleSheet); // Update text with progress - this.text = message + " " + Math.round((progressStopR * 100)) + "%"; + this.text = message?message:this.progressText + " " + Math.round((progressStopR * 100)) + "%"; } else { // Configure widget to indicate the operation is complete. this.setStyleSheet("QToolButton { border: none; background-color: " + accentColor + "; color: white}"); this.enabled = true; - this.text = this.finishedText; + this.text = message?message:this.finishedText; } } From f83b0e4e5a0a3e4fb3f7ce6f16fb818d87049ee5 Mon Sep 17 00:00:00 2001 From: MathieuC Date: Sun, 11 Jul 2021 02:26:44 +0200 Subject: [PATCH 110/112] throw an error in NetworkConnexionHandler if url returns nothing. --- ExtensionStore/lib/network.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/ExtensionStore/lib/network.js b/ExtensionStore/lib/network.js index 0b28c11..64db1e5 100644 --- a/ExtensionStore/lib/network.js +++ b/ExtensionStore/lib/network.js @@ -17,8 +17,13 @@ function NetworkConnexionHandler() { */ NetworkConnexionHandler.prototype.get = function (command) { // handle errors + // throw new Error("no connection"); var curl = new CURLProcess(command) - var result = curl.get(); + try{ + var result = curl.get(); + }catch(err){ + throw new Error ("Couldn't reach "+ command + ". Check connection."); + } try{ var json = JSON.parse(result); }catch(err){ @@ -501,12 +506,12 @@ Object.defineProperty(CURL.prototype, "bin", { return bin; } catch (err) { log.error(err); - var message = "ExtensionStore: Couldn't establish a connexion.\nCheck that " + bin + " has internet access."; + var message = "Couldn't establish a connexion.\nCheck that " + bin + " has internet access."; log.error(message); } } } - var error = "ExtensionStore: a valid CURL install wasn't found. Install CURL first."; + var error = "A valid CURL install wasn't found. Install CURL first."; log.error(error) throw new Error(error) } else { From 0a50ed1de83b63cd5c461cc5786fc845ec535684 Mon Sep 17 00:00:00 2001 From: MathieuC Date: Sun, 11 Jul 2021 02:28:16 +0200 Subject: [PATCH 111/112] emit onInstallFailed from LocalExtensionList.install if extension files can't be gathered --- ExtensionStore/lib/store.js | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/ExtensionStore/lib/store.js b/ExtensionStore/lib/store.js index f7486f6..9a3c626 100644 --- a/ExtensionStore/lib/store.js +++ b/ExtensionStore/lib/store.js @@ -515,11 +515,9 @@ Object.defineProperty(Repository.prototype, "contents", { try{ var contents = webQuery.get(this.masterBranchTree + "?recursive=true"); }catch(error){ - // in case of bad query, we avoid pulling it over and over, and consider it empty - this.log.error(error); - MessageBox.information(error); + // in case of bad query, we avoid pulling it over and over, and save a cache before throwing the error this._contents = []; - return this._contents; + throw new Error (error) } var tree = contents.tree; @@ -1069,7 +1067,12 @@ LocalExtensionList.prototype.checkFiles = function (extension) { */ LocalExtensionList.prototype.install = function (extension) { var installer = extension.installer; // dedicated object to implement threaded download later - var installLocation = this.installLocation(extension) + try{ + var installLocation = this.installLocation(extension) + }catch(err){ + installer.onInstallFailed.emit(err) + throw err; + } function copyFiles (files){ this.log.debug("downloaded files :\n" + files.join("\n")); @@ -1221,8 +1224,8 @@ LocalExtensionList.prototype.findInstalledExtensions = function (store) { var count = 0; var length = Object.keys(store.extensions).length; - for (var i in store.extensions) { - try{ + try{ + for (var i in store.extensions) { // this may fail in case of reaching api limit. this.extensionsDetectionProgressChanged.emit(count++/(length+1), progressMessage); @@ -1236,10 +1239,11 @@ LocalExtensionList.prototype.findInstalledExtensions = function (store) { continue; } } - }catch(err){ - throw new Error ("error during detection of installed extensions: " + err); } + }catch(err){ + throw new Error ("error during detection of installed extensions: " + err); } + this.extensionsDetectionProgressChanged.emit(1, progressMessage); return installedExtensions; From d4ce984059355a8677d09f39d481a97bc6200092 Mon Sep 17 00:00:00 2001 From: MathieuC Date: Sun, 11 Jul 2021 02:28:51 +0200 Subject: [PATCH 112/112] reorganise store init to ensure loading + error displaying in ribbon --- ExtensionStore/app.js | 102 ++++++++++++++++++++++-------------------- 1 file changed, 54 insertions(+), 48 deletions(-) diff --git a/ExtensionStore/app.js b/ExtensionStore/app.js index 6d72013..6f93ab9 100644 --- a/ExtensionStore/app.js +++ b/ExtensionStore/app.js @@ -18,15 +18,8 @@ var log = new Logger("UI"); * The main extension store widget class */ function StoreUI() { - this.store = new storelib.Store(); - log.debug("loading UI"); - - // the list of installed extensions - this.localList = new storelib.LocalExtensionList(this.store); - - // the extension representing the store on the remote repository - this.storeExtension = this.store.storeExtension; + log.debug("loading UI"); // setting up UI --------------------------------------------------- var packageView = ScriptManager.getView("Extension Store"); this.ui = ScriptManager.loadViewUI(packageView, "./resources/store.ui"); @@ -83,22 +76,6 @@ function StoreUI() { this.storeFrame.hide(); this.setStoreLoadUIState(false); - if (!this.localList.getData("HUES_EULA_ACCEPTED", false)) { - this.aboutFrame.hide(); - - // EULA logo - var eulaLogo = new StyledImage(appFolder + "/resources/logo.png", 380, 120); - this.eulaFrame.innerFrame.eulaLogo.setPixmap(eulaLogo.pixmap); - - this.eulaFrame.innerFrame.eulaCB.stateChanged.connect(this, function () { - this.localList.saveData("HUES_EULA_ACCEPTED", true); - this.eulaFrame.hide(); - this.aboutFrame.show(); - }); - } - else { - this.eulaFrame.hide(); - } // About logo var logo = new StyledImage(appFolder + "/resources/logo.png", 380, 120); @@ -121,7 +98,38 @@ function StoreUI() { var headerLogo = new StyledImage(style.ICONS.headerLogo, 22, 22); this.storeHeader.headerLogo.setPixmap(headerLogo.pixmap); - this.checkForUpdates() + try{ + log.debug("loading Store"); + this.store = new storelib.Store(); + + // the list of installed extensions + this.localList = new storelib.LocalExtensionList(this.store); + + // the extension representing the store on the remote repository + this.storeExtension = this.store.storeExtension; + this.checkForUpdates(); + + }catch(err){ + log.error(err) + this.lockStore(err) + } + + if (!this.localList.getData("HUES_EULA_ACCEPTED", false)) { + this.aboutFrame.hide(); + + // EULA logo + var eulaLogo = new StyledImage(appFolder + "/resources/logo.png", 380, 120); + this.eulaFrame.innerFrame.eulaLogo.setPixmap(eulaLogo.pixmap); + + this.eulaFrame.innerFrame.eulaCB.stateChanged.connect(this, function () { + this.localList.saveData("HUES_EULA_ACCEPTED", true); + this.eulaFrame.hide(); + this.aboutFrame.show(); + }); + } + else { + this.eulaFrame.hide(); + } // connect UI signals this.loadStoreButton.released.connect(this, this.loadStore); @@ -356,29 +364,26 @@ StoreUI.prototype.checkForUpdates = function () { var updateRibbon = this.updateRibbon var storeUi = this; - try { - var storeExtension = this.storeExtension; - var storeVersion = storeExtension.version; - var currentVersion = this.getInstalledVersion(); - this.storeFooter.storeVersionLabel.setText("v" + currentVersion); - - // if a more recent version of the store exists on the repo, activate the update ribbon - if (!storeExtension.currentVersionIsOlder(currentVersion) && (currentVersion != storeVersion)) { - updateRibbon.storeVersion.setText("v" + currentVersion + " ⓘ New version available: v" + storeVersion); - updateRibbon.setStyleSheet(style.STYLESHEETS.updateRibbon); - this.updateButton.toolTip = storeExtension.package.description; - this.updateButton.clicked.connect(this, function () { storeUi.updateStore(currentVersion, storeVersion) }); - } else { - this.updateButton.hide(); - updateRibbon.storeVersion.setText("v" + currentVersion + " ✓ - Store is up to date."); - updateRibbon.setStyleSheet(style.STYLESHEETS.defaultRibbon); - } - } catch (err) { - // couldn't check updates, probably we don't have an internet access. - // We set up an error message and disable load button. - log.error(err) - this.lockStore("Could not connect to GitHub. Store disabled, check internet access."); + var storeExtension = this.storeExtension; + var storeVersion = storeExtension.version; + var currentVersion = this.getInstalledVersion(); + this.storeFooter.storeVersionLabel.setText("v" + currentVersion); + + // if a more recent version of the store exists on the repo, activate the update ribbon + if (!storeExtension.currentVersionIsOlder(currentVersion) && (currentVersion != storeVersion)) { + updateRibbon.storeVersion.setText("v" + currentVersion + " ⓘ New version available: v" + storeVersion); + updateRibbon.setStyleSheet(style.STYLESHEETS.updateRibbon); + this.updateButton.toolTip = storeExtension.package.description; + this.updateButton.clicked.connect(this, function () { storeUi.updateStore(currentVersion, storeVersion) }); + } else { + this.updateButton.hide(); + updateRibbon.storeVersion.setText("v" + currentVersion + " ✓ - Store is up to date."); + updateRibbon.setStyleSheet(style.STYLESHEETS.defaultRibbon); } + // // couldn't check updates, probably we don't have an internet access. + // // We set up an error message and disable load button. + // log.error(err) + // this.lockStore("Could not connect to GitHub. Store disabled, check internet access."); } @@ -458,7 +463,8 @@ StoreUI.prototype.updateExtensionsList = function () { try{ this.localList.createListFile(this.store); }catch(err){ - MessageBox.trace("Error during detection of existing extensions : "+err.message) + this.loadStoreButton.setProgress(1, "Detection Failed.") + MessageBox.information("Error during detection of existing extensions: "+err.message + "\n\nThe list of installed extensions might not be accurate.") } } log.debug("updating extensions list")