Merge pull request #279 from YunoHost/app-categories

WIP : App categories
This commit is contained in:
Alexandre Aubin 2019-12-12 19:11:13 +01:00 committed by GitHub
commit 1f47ce7927
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 246 additions and 115 deletions

View file

@ -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%;

View file

@ -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,13 +107,35 @@
}
}
// 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']];
// 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
});
});
});
});
// 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) {
// Ignore not working apps
if (app.state === 'notworking') { return; }
// Ignore apps not in this category
if ((category_id !== "all") && (app.category !== category_id)) { return; }
app.id = app.manifest.id;
app.level = parseInt(app.level);
if (app.high_quality && app.level > 7)
@ -132,10 +154,7 @@
app.manifest.maintainer = extractMaintainer(app.manifest);
var isWorking = (app.state === 'working' || app.state === "high-quality") && app.level > 0;
// Keep only the first instance of each app and remove not working apps
if (!v['id'].match(/__[0-9]{1,5}$/) && (app.state !== 'notworking')) {
app.installable = (!v.installed || app.manifest.multi_instance)
app.installable = (!app.installed || app.manifest.supports_multi_instance)
app.levelFormatted = isNaN(app.level) ? '?' : app.level;
app.levelColor = levelToColor(app.level);
@ -149,59 +168,84 @@
app.isHighQuality = (app.state === "high-quality") ? "isHighQuality" : "";
app.decentQuality = (app.level > 4)?"decentQuality":"badQuality";
jQuery.extend(app, v);
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('.grid').isotope({
var cardGrid = jQuery('#apps').isotope({
itemSelector: '.app-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);
var filterClass = jQuery("#dropdownFilter").attr("data-filter");
var classMatch = (filterClass === '*') ? true : jQuery(this).hasClass(filterClass);
return inputMatch && classMatch;
},
// Default filter is 'decent quality apps'
cardGrid.isotope({ filter: '.decentQuality' });
$(".subtag-selector button").on("click", function() {
var selector = $(this).parent();
$("button", selector).removeClass("active");
$(this).addClass("active");
cardGrid.isotope({ filter: filterApps });
});
filterApps = function () {
// Check text search
var input = jQuery("#filter-app-cards").val().toLowerCase();
if (jQuery(this).find('.app-title').text().toLowerCase().indexOf(input) <= -1) return false;
// 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;
}
// Check quality criteria
var class_ = jQuery("#dropdownFilter").data("filter");
if ((class_ !== '*') && (! jQuery(this).hasClass(class_))) return false;
return true;
},
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 });
});
};
// render
c.view('app/app_list_install', {apps: apps}, setupFilterEvents);
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

View file

@ -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.",

View file

@ -1,16 +1,20 @@
<div class="btn-breadcrumb">
<a href="#/" ><i class="fa-home"></i><span class="sr-only">{{t 'home'}}</span></a>
<a href="#/apps">{{t 'applications'}}</a>
<a href="#/apps/install">{{t 'install'}}</a>
<a href="#/apps/catalog">{{t 'catalog'}}</a>
<a href="#/apps/catalog/{{category.id}}">{{category.title}}</a>
</div>
<div class="separator"></div>
<div class="input-group" id="app-filter-input">
<span class="input-group-addon"><i class="fas fa-search"></i></span>
<div class="input-group-btn"><a class="btn btn-primary" href="#/apps/catalog"><i class="fa-arrow-left"></i></a></div>
<span class="input-group-addon"><i class="fa-fw fa-{{category.icon}}"></i> {{category.title}}</span>
<span class="input-group-addon" style="background: white;border: none;">&nbsp;</span>
<span class="input-group-addon"><i class="fa-search"></i></span>
<input type="text" id="filter-app-cards" class="form-control" role="textbox" placeholder="{{t 'search_for_apps'}}" aria-describedby="basic-addon0"/>
<div class="input-group-btn">
<button type="button" role="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<button class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span id="app-cards-list-filter-text">{{t 'only_decent_quality_apps'}}</span> <span class="caret"></span>
</button>
<ul id="dropdownFilter" class="dropdown-menu" data-filter="decentQuality" role="menu">
@ -24,32 +28,51 @@
<div class="separator"></div>
<div class="list-group grid">
{{#apps}}
<div class="app-card panel panel-default {{status}} {{state}} {{isWorking}} {{isHighQuality}} {{decentQuality}} {{level}}-level">
<div class="panel-body">
<h2 class="app-title">{{name}}</h2>
<div class="category">
<span class="label label-{{stateColor}} label-as-badge app-state" title="{{t (concat 'app_state_' state '_explanation') }}">{{t (concat 'app_state_' state) }}</span>
<a target="_BLANK" href="https://yunohost.org/#/packaging_apps_levels"><span class="label label-{{levelColor}} label-as-badge app-level" title="{{t 'app_level'}}">{{t 'level'}} {{levelFormatted}}</span></a>
<span class="label label-{{maintainedColor}} label-as-badge maintained-status" title="{{t (concat maintained '_details') }}"> {{t maintained}}</span>
<div class="subtag-selector">
{{#if category.subtags}}
<button class="btn btn-default active" data-subtag="all">{{t 'all'}}</button>
{{/if}}
{{#category.subtags}}
<button class="btn btn-default" data-subtag="{{id}}">{{title}}</button>
{{/category.subtags}}
{{#if category.subtags}}
<button class="btn btn-default" data-subtag="others">{{t 'others'}}</button>
{{/if}}
</div>
<div class="app-card-desc">{{description}}</div>
<div class="separator"></div>
<div id="apps" class="list-group grid">
{{#apps}}
<div class="app-card panel panel-default {{state}} {{isWorking}} {{isHighQuality}} {{decentQuality}} {{level}}-level" data-subtags="{{#subtags}}{{.}}{{#unless @last}},{{/unless}}{{/subtags}}">
<div class="panel-body">
<h2 class="app-title">
{{manifest.name}}
{{#if (eq state 'working') }}
{{#if (eq decentQuality 'badQuality')}}
<span class="label label-warning label-as-badge app-state" title="{{t 'app_state_low_quality_explanation' }}">{{t 'app_state_low_quality' }}</span>
{{/if}}
{{else}}
<span class="label label-{{stateColor}} label-as-badge app-state" title="{{t (concat 'app_state_' state '_explanation') }}">{{t (concat 'app_state_' state) }}</span>
{{/if}}
</h2>
<div class="app-card-desc">{{manifest.description}}</div>
</div>
<div class="app-card-date-maintainer">
<i class="fa-refresh"></i> {{formatDate updateDate day="numeric" month="long" year="numeric"}} -
<span title="{{t 'current_maintainer_title'}}" class="maintained"></span><i class="fa-user"></i> {{manifest.maintainer}}</span>
{{#if (eq maintainedColor 'danger') }}
<span class="text text-warning maintained-status" title="{{t (concat maintained '_details') }}"><i class="fa-fw fa-warning"></i> {{t maintained}}</span>
{{/if}}
</div>
<div class="btn-group" role="group">
<a href="{{git.url}}" target="_BLANK" type="button" role="button" class="btn btn-default col-xs-4">
<i class="fa-globe"></i> Code
<i class="fa-fw fa-code"></i> Code
</a>
<a href="{{git.url}}/blob/master/README.md" target="_BLANK" type="button" role="button" class="btn btn-default col-xs-4">
<i class="fa-book"></i> Readme
<i class="fa-fw fa-book"></i> Readme
</a>
{{#installable}}
<a href="#/apps/install/{{id}}" type="button" role="button" class="btn btn-{{installColor}} col-xs-4 active">
<i class="fa-plus"></i> {{t 'install'}}{{^isSafe}} <i class="fa-warning"></i>{{/isSafe}}
<a href="#/apps/install/{{manifest.id}}" type="button" role="button" class="btn btn-{{installColor}} col-xs-4 active">
<i class="fa-fw fa-plus"></i> {{t 'install'}}{{^isSafe}} <i class="fa-fw fa-warning"></i>{{/isSafe}}
</a>
{{/installable}}
{{^installable}}
@ -66,7 +89,7 @@
</div>
<div class="panel-body">
<p class="alert alert-warning">
<span class="fa-warning"></span>
<span class="fa-fw fa-warning"></span>
{{t 'confirm_install_custom_app'}}
</p>
<form action="#/apps/install/custom" method="POST" class="form-horizontal">
@ -75,7 +98,7 @@
<div class="col-sm-12">
<input type="url" id="url" name="url" class="form-control" value="" placeholder="https://github.com/USER/REPOSITORY" required pattern="^https://github.com/[a-zA-Z0-9-_.]+/[a-zA-Z0-9-_.]+[/]?$">
<p class="text-warning">
<span class="fa-github"></span> {{t 'custom_app_url_only_github'}}
<span class="fa-fw fa-github"></span> {{t 'custom_app_url_only_github'}}
</p>
</div>
</div>

View file

@ -0,0 +1,23 @@
<div class="btn-breadcrumb">
<a href="#/" ><i class="fa-home"></i><span class="sr-only">{{t 'home'}}</span></a>
<a href="#/apps">{{t 'applications'}}</a>
<a href="#/apps/catalog">{{t 'catalog'}}</a>
</div>
<div class="separator"></div>
<div id="category-selector" class="list-group grid">
{{#categories}}
<a class="app-category-card panel panel-default" href="#/apps/catalog/{{id}}">
<div class="panel-body">
<h2 class="app-category-title"><span class="fa-fw fa-{{icon}}"></span> {{title}}</h2>
<h3 class="app-category-card-desc">{{description}}</h3>
</div>
</a>
{{/categories}}
<a class="app-category-card" href="#/apps/catalog/all">
<div class="panel-body">
<h2 class="app-category-title" style="padding-top: 3em'"><span class="fa-fw fa-search"></span> {{t 'all_apps'}}</h2>
</div>
</a>
</div>

View file

@ -22,7 +22,7 @@
<dt>{{t 'version'}}</dt>
<dd>{{version}}</dd>
<dt>{{t 'multi_instance'}}</dt>
<dd>{{manifest.multi_instance}}</dd>
<dd>{{supports_multi_instance}}</dd>
<dt>{{t 'install_time'}}</dt>
<dd>{{formatTime install_time day="numeric" month="long" year="numeric" hour="numeric" minute="numeric"}}</dd>
{{#if settings.domain}}
@ -64,7 +64,7 @@
<hr>
<div class="container">
<p>{{t 'app_info_changeurl_desc' settings.domain}}</p>
{{#if change_url}}
{{#if supports_change_url}}
<a href="#/apps/{{settings.id}}/changeurl" role="button" class="btn btn-info slide">
<span class="fa-exchange"></span> {{t 'app_change_url'}}
</a>

View file

@ -1,8 +1,8 @@
<div class="btn-breadcrumb">
<a href="#/" ><i class="fa-home"></i><span class="sr-only">{{t 'home'}}</span></a>
<a href="#/apps">{{t 'applications'}}</a>
<a href="#/apps/install">{{t 'install'}}</a>
<a href="#/apps/install/{{id}}">{{manifest.name}}</a>
<a href="#/apps/catalog">{{t 'catalog'}}</a>
<a href="#/apps/install/{{id}}">{{t 'install_name' manifest.name}}</a>
</div>
<div class="separator"></div>
@ -18,7 +18,7 @@
<dt>{{t 'id'}}</dt>
<dd>{{id}}</dd>
<dt>{{t 'description'}}</dt>
<dd>{{description}}</dd>
<dd>{{manifest.description}}</dd>
{{#displayLicense}}
<dt>{{t 'license'}}</dt>
<dd>{{manifest.license}}</dd>

View file

@ -4,7 +4,7 @@
</div>
<div class="actions-group">
<a role="button" href="#/apps/install" class="btn btn-success slide">
<a role="button" href="#/apps/catalog" class="btn btn-success slide">
<span class="fa-plus"></span> {{t 'install'}}
</a>
</div>
@ -16,7 +16,7 @@
<a href="#/apps/{{id}}" class="list-group-item slide" title="{{t 'infos'}}">
<span class="fa-chevron-right pull-right"></span>
<h2 class="list-group-item-heading">
{{label}} <small>{{name}}</small>
{{settings.label}} <small>{{name}}</small>
</h2>
<p class="list-group-item-text">{{description}}</p>
</a>