diff --git a/src/css/style.less b/src/css/style.less index 3c8322c2..c52302a7 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%; @@ -653,13 +657,16 @@ input[type='radio'].nice-radio { } } +.app-state { + font-size: 10px; +} -.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; } @@ -670,6 +677,7 @@ input[type='radio'].nice-radio { margin-bottom: 3px; margin-right: 7px; margin-top: -5px; + height: 18px; } .auto-width { @@ -686,6 +694,7 @@ input[type='radio'].nice-radio { .app-card .panel-body { padding: 1.5rem; + padding-bottom: 0.5rem; h3 { margin-top: 0; @@ -697,6 +706,33 @@ input[type='radio'].nice-radio { } } +.app-category-card .panel-body { + padding: 2em; + height: 10em; + color: #333 !important; +} + +.subtag-selector { + text-align: center; +} + +.app-category-card { + text-decoration: none !important; +} + +.app-category-card:hover { + border-color: #777; +} + +.app-category-title { + line-height: 1em; + white-space: normal; +} + +.app-category-card-desc { + white-space: normal; +} + /** Groups View **/ #view-groups { @@ -806,7 +842,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 +944,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 17c91e10..aa8d2c3d 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}); @@ -70,7 +70,7 @@ } else { - return 'success'; + return 'info'; } } @@ -107,101 +107,145 @@ } } - // 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); - 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"; - } + // Display catalog home page where users chooses to browse a specific category + app.get('#/apps/catalog', function (c) { + c.api('GET', '/appscatalog?full&with_categories', {}, function (data) { + c.view('app/app_catalog_home', {categories: data["categories"]}, function() { + // Configure layout / rendering for app-category-cards + $('#category-selector').isotope({ + itemSelector: '.app-category-card', + layoutMode: 'fitRows', + transitionDuration: 200 + }); + }); + }); + }); - app.manifest.maintainer = extractMaintainer(app.manifest); - var isWorking = (app.state === 'working' || app.state === "high-quality") && app.level > 0; + // Display app catalog for a specific category + app.get('#/apps/catalog/:category', function (c) { + var category_id = c.params['category']; + c.api('GET', '/appscatalog?full&with_categories', {}, function (data) { + var apps = []; + $.each(data['apps'], function(name, app) { - // Keep only the first instance of each app and remove not working apps - if (!v['id'].match(/__[0-9]{1,5}$/) && (app.state !== 'notworking')) { + // Ignore not working apps + if (app.state === 'notworking') { return; } - app.installable = (!v.installed || app.manifest.multi_instance) - app.levelFormatted = isNaN(app.level) ? '?' : app.level; + // Ignore apps not in this category + if ((category_id !== "all") && (app.category !== category_id)) { return; } - app.levelColor = levelToColor(app.level); - app.stateColor = stateToColor(app.state); - app.maintainedColor = maintainedStateToColor(app.maintained); - app.installColor = combineColors(app.stateColor, app.levelColor); + app.id = app.manifest.id; + app.level = parseInt(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"; + 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"; + } - jQuery.extend(app, v); - apps.push(app); - } + app.manifest.maintainer = extractMaintainer(app.manifest); + var isWorking = (app.state === 'working' || app.state === "high-quality") && app.level > 0; + + app.installable = (!app.installed || app.manifest.supports_multi_instance) + app.levelFormatted = isNaN(app.level) ? '?' : app.level; + + app.levelColor = levelToColor(app.level); + app.stateColor = stateToColor(app.state); + app.maintainedColor = maintainedStateToColor(app.maintained); + app.installColor = combineColors(app.stateColor, app.levelColor); + + 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); + }); + + var category = undefined; + $.each(data['categories'], function(i, this_category) { + if (this_category.id === category_id) { category = this_category; } + }); + + if (category_id === "all") { + category = {title: y18n.t("all_apps"), icon: "search"}; + } + + // 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('#apps').isotope({ + itemSelector: '.app-card', + layoutMode: 'fitRows', + transitionDuration: 200 }); - // Sort app list - c.arraySortById(apps); + // Default filter is 'decent quality apps' + cardGrid.isotope({ filter: '.decentQuality' }); - // 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 - }); + $(".subtag-selector button").on("click", function() { + var selector = $(this).parent(); + $("button", selector).removeClass("active"); + $(this).addClass("active"); + cardGrid.isotope({ filter: filterApps }); + }); - filterByClassAndName = function () { - var input = jQuery("#filter-app-cards").val().toLowerCase(); - var inputMatch = (jQuery(this).find('.app-title').text().toLowerCase().indexOf(input) > -1); + filterApps = function () { - var filterClass = jQuery("#dropdownFilter").attr("data-filter"); - var classMatch = (filterClass === '*') ? true : jQuery(this).hasClass(filterClass); - return inputMatch && classMatch; - }, + // Check text search + var input = jQuery("#filter-app-cards").val().toLowerCase(); + if (jQuery(this).find('.app-title').text().toLowerCase().indexOf(input) <= -1) return false; - // Default filter is 'decent quality apps' - cardGrid.isotope({ filter: '.decentQuality' }); + // Check subtags + var subtag = $(".subtag-selector 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; + } - 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 }); - }); + // Check quality criteria + var class_ = jQuery("#dropdownFilter").data("filter"); + if ((class_ !== '*') && (! jQuery(this).hasClass(class_))) return false; - jQuery("#filter-app-cards").on("keyup", function() { - cardGrid.isotope({ filter: filterByClassAndName }); - }); - }; + return true; + }, - // render - c.view('app/app_list_install', {apps: apps}, setupFilterEvents); + 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').data("filter", jQuery(this).data("filter")); + // filter ! + cardGrid.isotope({ filter: filterApps }); + }); + + jQuery("#filter-app-cards").on("keyup", function() { + cardGrid.isotope({ filter: filterApps }); + }); + }; + + // render + c.view('app/app_catalog_category', {apps: apps, category: category}, 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 +550,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 +579,7 @@ { c.appInstallForm( c.params['app'], - data[c.params['app']].manifest, + app_infos.manifest, c.params ); } @@ -622,7 +666,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 +685,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/locales/en.json b/src/locales/en.json index 35145778..9e5f6fd8 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -5,6 +5,7 @@ "advanced": "Advanced", "remove": "Remove", "administration_password": "Administration password", + "all": "All", "all_apps": "All apps", "api_not_responding": "The YunoHost API is not responding. Maybe 'yunohost-api' is down or got restarted?", "app_change_label": "Change Label", @@ -23,10 +24,12 @@ "app_no_actions": "This application doesn't have any actions", "app_repository": "Application origin: ", "app_state": "Application state: ", - "app_state_inprogress": "in progress", + "app_state_inprogress": "not yet working", "app_state_inprogress_explanation": "This maintainer of this app declared that this application is not ready yet for production use. BE CAREFUL!", "app_state_notworking": "not working", "app_state_notworking_explanation": "This maintainer of this app declared it as 'not working'. IT WILL BREAK YOUR SYSTEM!", + "app_state_low_quality": "low quality", + "app_state_low_quality_explanation": "This app may be functional, but may still contain issues, or is not fully integrated with YunoHost, or it does not respect the good practices.", "app_state_high-quality": "high quality", "app_state_high-quality_explanation": "This app is well-integrated with YunoHost. It has been (and is!) peer-reviewed by the YunoHost app team. It can be expected to be safe and maintained on the long-term.", "app_state_working": "working", @@ -51,6 +54,7 @@ "begin": "Begin", "both": "Both", "cancel": "Cancel", + "catalog": "Catalog", "check": "Check", "close": "Close", "configuration": "Configuration", @@ -218,8 +222,9 @@ "only_decent_quality_apps": "Only decent quality apps", "open": "Open", "operations": "Operations", - "orphaned": "not maintained", - "orphaned_details": "This app is not maintained anymore. It may still be working but won't receive any upgrade. Feel free to come and revive it!", + "orphaned": "Not maintained", + "orphaned_details": "This app has not been maintained for quite some time. It may still be working, but won't receive any upgrade until somebody volunteers to take care of it. Feel free to contribute to revive it!", + "others": "Others", "password": "Password", "password_confirmation": "Password confirmation", "password_description": "Password must be at least %s characters long.", diff --git a/src/views/app/app_list_install.ms b/src/views/app/app_catalog_category.ms similarity index 55% rename from src/views/app/app_list_install.ms rename to src/views/app/app_catalog_category.ms index e0ce26b0..fd3ac12e 100644 --- a/src/views/app/app_list_install.ms +++ b/src/views/app/app_catalog_category.ms @@ -1,16 +1,20 @@
{{t 'home'}} {{t 'applications'}} - {{t 'install'}} + {{t 'catalog'}} + {{category.title}}
- +
+ {{category.title}} +   +
-