From 749aba93fe3b266796466fdb2e7888067f8e30f2 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 25 Nov 2019 23:28:00 +0100 Subject: [PATCH 1/6] Propagate API changes + improve semantics before actually working on app categories implementation --- src/js/yunohost/controllers/apps.js | 159 +++++++++--------- .../{app_list_install.ms => app_catalog.ms} | 6 +- src/views/app/app_info.ms | 4 +- src/views/app/app_install.ms | 2 +- src/views/app/app_list.ms | 4 +- 5 files changed, 86 insertions(+), 89 deletions(-) rename src/views/app/{app_list_install.ms => app_catalog.ms} (95%) diff --git a/src/js/yunohost/controllers/apps.js b/src/js/yunohost/controllers/apps.js index 17c91e10..dacd8ef1 100644 --- a/src/js/yunohost/controllers/apps.js +++ b/src/js/yunohost/controllers/apps.js @@ -10,7 +10,7 @@ // List installed apps app.get('#/apps', function (c) { - c.api('GET', '/apps?installed', {}, function(data) { + c.api('GET', '/apps?full', {}, function(data) { var apps = data['apps']; c.arraySortById(apps); c.view('app/app_list', {apps: apps}); @@ -107,101 +107,98 @@ } } - // List available apps - app.get('#/apps/install', function (c) { - c.api('GET', '/apps', {}, function (data) { - c.api('GET', '/apps?raw', {}, function (dataraw) { - var apps = [] - $.each(data['apps'], function(k, v) { - app = dataraw[v['id']]; - app.level = parseInt(app.level); + // Display app catalog + app.get('#/apps/catalog', function (c) { + c.api('GET', '/appscatalog?full&with_categories', {}, function (data) { + var apps = [] + $.each(data['apps'], function(name, app) { - if (app.high_quality && app.level > 7) - { - app.state = "high-quality"; - } - if ( app.maintained === false ) - { - app.maintained = "orphaned"; - } - else if ( app.maintained === true ) - { - app.maintained = "maintained"; - } + // Ignore not working apps + if (app.state === 'notworking') { return; } - app.manifest.maintainer = extractMaintainer(app.manifest); - var isWorking = (app.state === 'working' || app.state === "high-quality") && app.level > 0; + app.id = app.manifest.id; + app.level = parseInt(app.level); - // Keep only the first instance of each app and remove not working apps - if (!v['id'].match(/__[0-9]{1,5}$/) && (app.state !== 'notworking')) { + if (app.high_quality && app.level > 7) + { + app.state = "high-quality"; + } + if ( app.maintained === false ) + { + app.maintained = "orphaned"; + } + else if ( app.maintained === true ) + { + app.maintained = "maintained"; + } - app.installable = (!v.installed || app.manifest.multi_instance) - app.levelFormatted = isNaN(app.level) ? '?' : app.level; + app.manifest.maintainer = extractMaintainer(app.manifest); + var isWorking = (app.state === 'working' || app.state === "high-quality") && app.level > 0; - app.levelColor = levelToColor(app.level); - app.stateColor = stateToColor(app.state); - app.maintainedColor = maintainedStateToColor(app.maintained); - app.installColor = combineColors(app.stateColor, app.levelColor); + app.installable = (!app.installed || app.manifest.supports_multi_instance) + app.levelFormatted = isNaN(app.level) ? '?' : app.level; - app.updateDate = app.lastUpdate * 1000 || 0; - app.isSafe = (app.installColor !== 'danger'); - app.isWorking = isWorking ? "isworking" : "notFullyWorking"; - app.isHighQuality = (app.state === "high-quality") ? "isHighQuality" : ""; - app.decentQuality = (app.level > 4)?"decentQuality":"badQuality"; + app.levelColor = levelToColor(app.level); + app.stateColor = stateToColor(app.state); + app.maintainedColor = maintainedStateToColor(app.maintained); + app.installColor = combineColors(app.stateColor, app.levelColor); - jQuery.extend(app, v); - apps.push(app); - } + app.updateDate = app.lastUpdate * 1000 || 0; + app.isSafe = (app.installColor !== 'danger'); + app.isWorking = isWorking ? "isworking" : "notFullyWorking"; + app.isHighQuality = (app.state === "high-quality") ? "isHighQuality" : ""; + app.decentQuality = (app.level > 4)?"decentQuality":"badQuality"; + + apps.push(app); + }); + + // Sort app list + c.arraySortById(apps); + + // setup filtering of apps once the view is loaded + function setupFilterEvents () { + // Uses plugin isotope to filter apps (we could had ordering to) + var cardGrid = jQuery('.grid').isotope({ + itemSelector: '.app-card', + layoutMode: 'fitRows', + transitionDuration: 200 }); - // Sort app list - c.arraySortById(apps); + filterByClassAndName = function () { + var input = jQuery("#filter-app-cards").val().toLowerCase(); + var inputMatch = (jQuery(this).find('.app-title').text().toLowerCase().indexOf(input) > -1); - // setup filtering of apps once the view is loaded - function setupFilterEvents () { - // Uses plugin isotope to filter apps (we could had ordering to) - var cardGrid = jQuery('.grid').isotope({ - itemSelector: '.app-card', - layoutMode: 'fitRows', - transitionDuration: 200 - }); + var filterClass = jQuery("#dropdownFilter").attr("data-filter"); + var classMatch = (filterClass === '*') ? true : jQuery(this).hasClass(filterClass); + return inputMatch && classMatch; + }, - filterByClassAndName = function () { - var input = jQuery("#filter-app-cards").val().toLowerCase(); - var inputMatch = (jQuery(this).find('.app-title').text().toLowerCase().indexOf(input) > -1); + // Default filter is 'decent quality apps' + cardGrid.isotope({ filter: '.decentQuality' }); - var filterClass = jQuery("#dropdownFilter").attr("data-filter"); - var classMatch = (filterClass === '*') ? true : jQuery(this).hasClass(filterClass); - return inputMatch && classMatch; - }, + jQuery('.dropdownFilter').on('click', function() { + // change dropdown label + jQuery('#app-cards-list-filter-text').text(jQuery(this).find('.menu-item').text()); + // change filter attribute + jQuery('#dropdownFilter').attr("data-filter", jQuery(this).attr("data-filter")); + // filter ! + cardGrid.isotope({ filter: filterByClassAndName }); + }); - // Default filter is 'decent quality apps' - cardGrid.isotope({ filter: '.decentQuality' }); + jQuery("#filter-app-cards").on("keyup", function() { + cardGrid.isotope({ filter: filterByClassAndName }); + }); + }; - jQuery('.dropdownFilter').on('click', function() { - // change dropdown label - jQuery('#app-cards-list-filter-text').text(jQuery(this).find('.menu-item').text()); - // change filter attribute - jQuery('#dropdownFilter').attr("data-filter", jQuery(this).attr("data-filter")); - // filter ! - cardGrid.isotope({ filter: filterByClassAndName }); - }); + // render + c.view('app/app_catalog', {apps: apps}, setupFilterEvents); - jQuery("#filter-app-cards").on("keyup", function() { - cardGrid.isotope({ filter: filterByClassAndName }); - }); - }; - - // render - c.view('app/app_list_install', {apps: apps}, setupFilterEvents); - - }); }); }); // Get app information app.get('#/apps/:app', function (c) { - c.api('GET', '/apps/'+c.params['app']+'?raw', {}, function(data) { + c.api('GET', '/apps/'+c.params['app']+'?full', {}, function(data) { c.api('GET', '/users/permissions', {}, function(data_permissions) { // Permissions @@ -506,9 +503,9 @@ // App installation form app.get('#/apps/install/:app', function (c) { - c.api('GET', '/apps?raw', {}, function(data) { + c.api('GET', '/appscatalog?full', {}, function(data) { var app_name = c.params["app"]; - var app_infos = data[app_name]; + var app_infos = data["apps"][app_name]; if (app_infos['state'] === "validated") { app_infos['state'] = "official"; @@ -535,7 +532,7 @@ { c.appInstallForm( c.params['app'], - data[c.params['app']].manifest, + app_infos.manifest, c.params ); } @@ -622,7 +619,7 @@ // Get app change label page app.get('#/apps/:app/changelabel', function (c) { - c.api('GET', '/apps/'+c.params['app']+'?raw', {}, function(app_data) { + c.api('GET', '/apps/'+c.params['app']+'?full', {}, function(app_data) { data = { id: c.params['app'], label: app_data.settings.label, @@ -641,7 +638,7 @@ // Get app change URL page app.get('#/apps/:app/changeurl', function (c) { - c.api('GET', '/apps/'+c.params['app']+'?raw', {}, function(app_data) { + c.api('GET', '/apps/'+c.params['app']+'?full', {}, function(app_data) { c.api('GET', '/domains', {}, function(domain_data) { // Display a list of available domains diff --git a/src/views/app/app_list_install.ms b/src/views/app/app_catalog.ms similarity index 95% rename from src/views/app/app_list_install.ms rename to src/views/app/app_catalog.ms index e0ce26b0..e1754446 100644 --- a/src/views/app/app_list_install.ms +++ b/src/views/app/app_catalog.ms @@ -28,13 +28,13 @@ {{#apps}}
-

{{name}}

+

{{manifest.name}}

{{t (concat 'app_state_' state) }} {{t 'level'}} {{levelFormatted}} {{t maintained}}
-
{{description}}
+
{{manifest.description}}
{{formatDate updateDate day="numeric" month="long" year="numeric"}} - @@ -48,7 +48,7 @@ Readme {{#installable}} - + {{t 'install'}}{{^isSafe}} {{/isSafe}} {{/installable}} diff --git a/src/views/app/app_info.ms b/src/views/app/app_info.ms index 7c972a0e..b8da64da 100644 --- a/src/views/app/app_info.ms +++ b/src/views/app/app_info.ms @@ -22,7 +22,7 @@
{{t 'version'}}
{{version}}
{{t 'multi_instance'}}
-
{{manifest.multi_instance}}
+
{{supports_multi_instance}}
{{t 'install_time'}}
{{formatTime install_time day="numeric" month="long" year="numeric" hour="numeric" minute="numeric"}}
{{#if settings.domain}} @@ -64,7 +64,7 @@

{{t 'app_info_changeurl_desc' settings.domain}}

- {{#if change_url}} + {{#if supports_change_url}} {{t 'app_change_url'}} diff --git a/src/views/app/app_install.ms b/src/views/app/app_install.ms index dcf8e017..05dcfb46 100644 --- a/src/views/app/app_install.ms +++ b/src/views/app/app_install.ms @@ -18,7 +18,7 @@
{{t 'id'}}
{{id}}
{{t 'description'}}
-
{{description}}
+
{{manifest.description}}
{{#displayLicense}}
{{t 'license'}}
{{manifest.license}}
diff --git a/src/views/app/app_list.ms b/src/views/app/app_list.ms index 2840f2a9..bf658106 100644 --- a/src/views/app/app_list.ms +++ b/src/views/app/app_list.ms @@ -4,7 +4,7 @@
@@ -16,7 +16,7 @@

- {{label}} {{name}} + {{settings.label}} {{name}}

{{description}}

From 8ec78160decb231344e9793cddb9dba90e620b66 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 26 Nov 2019 00:38:02 +0100 Subject: [PATCH 2/6] First rendering of the categories, still drafty --- src/css/style.less | 22 +++++++++++++++++----- src/js/yunohost/controllers/apps.js | 10 ++++++++-- src/views/app/app_catalog.ms | 15 ++++++++++++++- 3 files changed, 39 insertions(+), 8 deletions(-) diff --git a/src/css/style.less b/src/css/style.less index 3c8322c2..0eaa0f21 100644 --- a/src/css/style.less +++ b/src/css/style.less @@ -631,8 +631,12 @@ input[type='radio'].nice-radio { background-color: darkorchid; } +.app-category-card { + text-align: center; +} + // only one card for small screens -.app-card { +.app-card, .app-category-card { width: 100%; .btn-group { width: 100%; @@ -654,12 +658,12 @@ input[type='radio'].nice-radio { } -.app-title { +.app-title, .app-category-title { margin-top: 5px; font-weight: 600; } -.app-card-desc { +.app-card-desc, .app-category-card-desc { height: 6rem; overflow: hidden; } @@ -697,6 +701,14 @@ input[type='radio'].nice-radio { } } +.app-category-card .panel-body { + padding: 2em; + height: 10em; +} + +.app-category-title { + line-height: 0.5em; +} /** Groups View **/ #view-groups { @@ -806,7 +818,7 @@ input[type='radio'].nice-radio { } // display 2 cards between 640 and 992px - .app-card { + .app-card, .app-category-card { width: 47.9%; margin: 1%; } @@ -908,7 +920,7 @@ input[type='radio'].nice-radio { // bootstrap breakpoint for large screen is 992px @media screen and (min-width: 992px) { - .app-card { + .app-card, .app-category-card { // display 3 cards by row width: 31.3%; margin: 1%; diff --git a/src/js/yunohost/controllers/apps.js b/src/js/yunohost/controllers/apps.js index dacd8ef1..694ca9f6 100644 --- a/src/js/yunohost/controllers/apps.js +++ b/src/js/yunohost/controllers/apps.js @@ -158,12 +158,18 @@ // setup filtering of apps once the view is loaded function setupFilterEvents () { // Uses plugin isotope to filter apps (we could had ordering to) - var cardGrid = jQuery('.grid').isotope({ + var cardGrid = jQuery('#apps').isotope({ itemSelector: '.app-card', layoutMode: 'fitRows', transitionDuration: 200 }); + var categoryGrid = jQuery('#app-categories').isotope({ + itemSelector: '.app-category-card', + layoutMode: 'fitRows', + transitionDuration: 200 + }); + filterByClassAndName = function () { var input = jQuery("#filter-app-cards").val().toLowerCase(); var inputMatch = (jQuery(this).find('.app-title').text().toLowerCase().indexOf(input) > -1); @@ -191,7 +197,7 @@ }; // render - c.view('app/app_catalog', {apps: apps}, setupFilterEvents); + c.view('app/app_catalog', {apps: apps, categories: data["categories"]}, setupFilterEvents); }); }); diff --git a/src/views/app/app_catalog.ms b/src/views/app/app_catalog.ms index e1754446..4d444f0d 100644 --- a/src/views/app/app_catalog.ms +++ b/src/views/app/app_catalog.ms @@ -24,7 +24,20 @@
-
+
+ {{#categories}} +
+
+

{{title}}

+

{{description}}

+
+
+ {{/categories}} +
+ +
+ +
{{#apps}}
From 2e8f2990a297e221ca08b7bf5ca1ac73db8378aa Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 27 Nov 2019 03:37:01 +0100 Subject: [PATCH 3/6] Implement category and subtags filtering --- src/css/style.less | 2 +- src/js/yunohost/controllers/apps.js | 91 +++++++++++++++++++++++++---- src/views/app/app_catalog.ms | 36 +++++++++--- src/views/app/app_install.ms | 2 +- 4 files changed, 108 insertions(+), 23 deletions(-) diff --git a/src/css/style.less b/src/css/style.less index 0eaa0f21..f7b0e207 100644 --- a/src/css/style.less +++ b/src/css/style.less @@ -707,7 +707,7 @@ input[type='radio'].nice-radio { } .app-category-title { - line-height: 0.5em; + line-height: 1em; } /** Groups View **/ diff --git a/src/js/yunohost/controllers/apps.js b/src/js/yunohost/controllers/apps.js index 694ca9f6..c62477d5 100644 --- a/src/js/yunohost/controllers/apps.js +++ b/src/js/yunohost/controllers/apps.js @@ -164,35 +164,102 @@ transitionDuration: 200 }); - var categoryGrid = jQuery('#app-categories').isotope({ + // Default filter is 'decent quality apps' + cardGrid.isotope({ filter: '.decentQuality' }); + + var categoryGrid = jQuery('#category-selector').isotope({ itemSelector: '.app-category-card', layoutMode: 'fitRows', transitionDuration: 200 }); - filterByClassAndName = function () { + $("#category-selector").show(); + $("#current-category-filter").hide(); + $("#current-category-separator").hide(); + $("#back-to-category-selection").hide(); + $(".subtag-selector").hide(); + + $("#category-selector button").on("click", function() { + var category = $(this).data("category"); + var title = $("h2", this).html(); + + // Feed info and display the selecter category next to the search bar + $("#current-category-filter").html(title); + $("#current-category-filter").data("category", category); + $("#current-category-filter").show(); + $("#back-to-category-selection").show(); + $("#current-category-separator").show(); + + // Hide the category selector + $("#category-selector").hide(); + + // Display the subtags selector + $(".subtag-selector").hide(); + $(".subtag-selector[data-app-category='"+category+"']").show(); + cardGrid.isotope({ filter: filterApps }); + }); + + $("#back-to-category-selection").on("click", function() { + + // Hide / reset selected cateory next to the search bar + $("#current-category-filter").hide(); + $("#current-category-filter").data("category", ""); + $("#back-to-category-selection").hide(); + $("#current-category-separator").hide(); + + // Display the category selector + $("#category-selector").show(); + + // Hide subtag selector + $(".subtag-selector").hide(); + cardGrid.isotope({ filter: filterApps }); + }); + + $(".subtag-selector button").on("click", function() { + var selector = $(this).parent(); + var category = selector.data("app-category"); + $("button", selector).removeClass("active"); + $(this).addClass("active"); + cardGrid.isotope({ filter: filterApps }); + }); + + + filterApps = function () { + + // Check text search var input = jQuery("#filter-app-cards").val().toLowerCase(); - var inputMatch = (jQuery(this).find('.app-title').text().toLowerCase().indexOf(input) > -1); + if (jQuery(this).find('.app-title').text().toLowerCase().indexOf(input) <= -1) return false; - var filterClass = jQuery("#dropdownFilter").attr("data-filter"); - var classMatch = (filterClass === '*') ? true : jQuery(this).hasClass(filterClass); - return inputMatch && classMatch; + // Check category + var category = $("#current-category-filter").data("category"); + if ((category !== '') && (jQuery(this).data("category") !== category)) return false; + + // Check subtags + var subtag = $(".subtag-selector[data-app-category='"+category+"'] button.active").data("subtag"); + var this_subtags = jQuery(this).data("subtags"); + if ((subtag !== undefined) && (subtag !== "all")) { + if ((subtag === "others") && (this_subtags !== "")) return false; + if ((subtag !== "others") && (this_subtags.split(",").indexOf(subtag) <= -1)) return false; + } + + // Check quality criteria + var class_ = jQuery("#dropdownFilter").data("filter"); + if ((class_ !== '*') && (! jQuery(this).hasClass(class_))) return false; + + return true; }, - // Default filter is 'decent quality apps' - cardGrid.isotope({ filter: '.decentQuality' }); - jQuery('.dropdownFilter').on('click', function() { // change dropdown label jQuery('#app-cards-list-filter-text').text(jQuery(this).find('.menu-item').text()); // change filter attribute - jQuery('#dropdownFilter').attr("data-filter", jQuery(this).attr("data-filter")); + jQuery('#dropdownFilter').data("filter", jQuery(this).data("filter")); // filter ! - cardGrid.isotope({ filter: filterByClassAndName }); + cardGrid.isotope({ filter: filterApps }); }); jQuery("#filter-app-cards").on("keyup", function() { - cardGrid.isotope({ filter: filterByClassAndName }); + cardGrid.isotope({ filter: filterApps }); }); }; diff --git a/src/views/app/app_catalog.ms b/src/views/app/app_catalog.ms index 4d444f0d..aac34205 100644 --- a/src/views/app/app_catalog.ms +++ b/src/views/app/app_catalog.ms @@ -1,16 +1,19 @@
- +
+ +   +
-