Merge pull request #242 from YunoHost/high-quality-apps-and-maintained-states

High quality apps and maintained states
This commit is contained in:
Alexandre Aubin 2019-06-04 00:15:09 +02:00 committed by GitHub
commit 8e8113d743
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 130 additions and 41 deletions

View file

@ -588,6 +588,10 @@ input[type='radio'].nice-radio {
} }
} }
.label-best {
background-color: darkorchid;
}
// only one card for small screens // only one card for small screens
.app-card { .app-card {
width: 100%; width: 100%;

View file

@ -18,7 +18,10 @@
}); });
function levelToColor(level) { function levelToColor(level) {
if (level >= 3) { if (level >= 8) {
return 'best';
}
else if (level > 4) {
return 'success'; return 'success';
} }
else if (level >= 1) { else if (level >= 1) {
@ -32,7 +35,10 @@
} }
function stateToColor(state) { function stateToColor(state) {
if (state === "working" || state === "official") { if (state === "high-quality") {
return 'best';
}
else if (state === "working") {
return 'success'; return 'success';
} }
else { else {
@ -40,6 +46,21 @@
} }
} }
function maintainedStateToColor(state) {
if ( state === "request_help" ) {
return 'info';
}
else if ( state === "request_adoption" ) {
return 'warning';
}
else if ( state === "orphaned" ) {
return 'danger';
}
else {
return 'success';
}
}
function combineColors(stateColor, levelColor, installable) { function combineColors(stateColor, levelColor, installable) {
if (stateColor === "danger" || levelColor === "danger") { if (stateColor === "danger" || levelColor === "danger") {
return 'danger'; return 'danger';
@ -53,36 +74,83 @@
} }
} }
function extractMaintainer(manifest) {
if (manifest.maintainer === undefined)
{
if ((manifest.developer !== undefined) && (manifest.developer.name !== undefined))
{
return manifest.developer.name;
}
else
{
return "?";
}
}
else if (Array.isArray(manifest.maintainer))
{
maintainers = [];
manifest.maintainer.forEach(function(maintainer) {
if (maintainer.name !== undefined)
{
maintainers.push(maintainer.name);
}
});
return maintainers.join(', ');
}
else if (manifest.maintainer.name !== undefined)
{
return manifest.maintainer.name;
}
else
{
return "?";
}
}
// List available apps // List available apps
app.get('#/apps/install', function (c) { app.get('#/apps/install', function (c) {
c.api('/apps', function (data) { // http://api.yunohost.org/#!/app/app_list_get_8 c.api('/apps', function (data) { // http://api.yunohost.org/#!/app/app_list_get_8
c.api('/apps?raw', function (dataraw) { // http://api.yunohost.org/#!/app/app_list_get_8 c.api('/apps?raw', function (dataraw) { // http://api.yunohost.org/#!/app/app_list_get_8
var apps = [] var apps = []
$.each(data['apps'], function(k, v) { $.each(data['apps'], function(k, v) {
if (dataraw[v['id']]['state'] === "validated") app = dataraw[v['id']];
app.level = parseInt(app.level);
if (app.high_quality && app.level > 7)
{ {
dataraw[v['id']]['state'] = "official"; app.state = "high-quality";
}
if ( app.maintained === false )
{
app.maintained = "orphaned";
}
else if ( app.maintained === true )
{
app.maintained = "maintained";
} }
var state = dataraw[v['id']]['state'];
var levelFormatted = parseInt(dataraw[v['id']]['level']);
var isWorking = (state === 'working' || state === 'official') && levelFormatted > 0;
// Keep only the first instance of each app and remove community not working apps
if (!v['id'].match(/__[0-9]{1,5}$/) && (dataraw[v['id']]['repository'] === 'yunohost' || state !== 'notworking')) {
dataraw[v['id']]['installable'] = (!v['installed'] || dataraw[v['id']].manifest.multi_instance) app.manifest.maintainer = extractMaintainer(app.manifest);
dataraw[v['id']]['isCommunity'] = !(dataraw[v['id']]['repository'] === 'yunohost'); var isWorking = (app.state === 'working' || app.state === "high-quality") && app.level > 0;
dataraw[v['id']]['levelFormatted'] = isNaN(levelFormatted) ? '?' : levelFormatted;
dataraw[v['id']]['levelColor'] = levelToColor(levelFormatted);
dataraw[v['id']]['stateColor'] = stateToColor(state);
dataraw[v['id']]['installColor'] = combineColors(dataraw[v['id']]['stateColor'], dataraw[v['id']]['levelColor']);
dataraw[v['id']]['displayLicense'] = (dataraw[v['id']]['manifest']['license'] !== undefined
&& dataraw[v['id']]['manifest']['license'] !== 'free');
dataraw[v['id']]['updateDate'] = dataraw[v['id']]['lastUpdate'] * 1000 || 0;
dataraw[v['id']]['isSafe'] = (dataraw[v['id']]['installColor'] !== 'danger');
dataraw[v['id']]['isWorking'] = isWorking ? "isworking" : "notFullyWorking";
jQuery.extend(dataraw[v['id']], v); // Keep only the first instance of each app and remove not working apps
apps.push(dataraw[v['id']]); if (!v['id'].match(/__[0-9]{1,5}$/) && (app.state !== 'notworking')) {
app.installable = (!v.installed || app.manifest.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";
jQuery.extend(app, v);
apps.push(app);
} }
}); });
@ -107,8 +175,8 @@
return inputMatch && classMatch; return inputMatch && classMatch;
}, },
// Keep only official apps at first render // Default filter is 'decent quality apps'
cardGrid.isotope({ filter: '.isworking' }); cardGrid.isotope({ filter: '.decentQuality' });
jQuery('.dropdownFilter').on('click', function() { jQuery('.dropdownFilter').on('click', function() {
// change dropdown label // change dropdown label
@ -472,7 +540,8 @@
app.helper('appInstallForm', function(appId, manifest, params) { app.helper('appInstallForm', function(appId, manifest, params) {
var data = { var data = {
id: appId, id: appId,
manifest: manifest manifest: manifest,
displayLicense: (manifest['license'] !== undefined && manifest['license'] !== 'free')
}; };
formatYunoHostStyleArguments(data.manifest.arguments.install, params); formatYunoHostStyleArguments(data.manifest.arguments.install, params);

View file

@ -29,16 +29,19 @@
"app_install_cancel": "Installation cancelled.", "app_install_cancel": "Installation cancelled.",
"app_install_custom_no_manifest": "No manifest.json file", "app_install_custom_no_manifest": "No manifest.json file",
"app_list": "App list", "app_list": "App list",
"app_license": "License of the app",
"app_level": "App level", "app_level": "App level",
"app_make_default": "Make default", "app_make_default": "Make default",
"app_no_actions": "This application doesn't have any actions", "app_no_actions": "This application doesn't have any actions",
"app_repository": "Application origin: ", "app_repository": "Application origin: ",
"app_state": "Application state: ", "app_state": "Application state: ",
"app_state_inprogress": "In progress", "app_state_inprogress": "in progress",
"app_state_notworking": "Not 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_official": "Official", "app_state_notworking": "not working",
"app_state_working": "Working", "app_state_notworking_explanation": "This maintainer of this app declared it as 'not working'. IT WILL BREAK YOUR SYSTEM!",
"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",
"app_state_working_explanation": "The maintainer of this app declared it as 'working'. It means that it should be functional (c.f. application level) but is not necessarily peer-reviewed, it may still contain issues or is not fully integrated with YunoHost.",
"application": "Application", "application": "Application",
"applications": "Applications", "applications": "Applications",
"archive_empty": "Empty archive", "archive_empty": "Empty archive",
@ -189,6 +192,7 @@
"label": "Label", "label": "Label",
"label_for_manifestname": "Label for %s", "label_for_manifestname": "Label for %s",
"level": "level", "level": "level",
"license": "License",
"loading": "Loading …", "loading": "Loading …",
"local_archives": "Local archives", "local_archives": "Local archives",
"local_ip": "Local IP", "local_ip": "Local IP",
@ -199,6 +203,8 @@
"logout": "Logout", "logout": "Logout",
"mailbox_quota_description": "For example, 700M is a CD, 4700M is a DVD.", "mailbox_quota_description": "For example, 700M is a CD, 4700M is a DVD.",
"mailbox_quota_placeholder": "Leave empty or set to 0 to disable.", "mailbox_quota_placeholder": "Leave empty or set to 0 to disable.",
"maintained": "maintained",
"maintained_details": "This app was maintained by its maintainer in the last few months.",
"manage_apps": "Manage apps", "manage_apps": "Manage apps",
"manage_domains": "Manage domains", "manage_domains": "Manage domains",
"manage_users": "Manage users", "manage_users": "Manage users",
@ -226,10 +232,13 @@
"no_user_to_add": "No more users to add.", "no_user_to_add": "No more users to add.",
"non_compatible_api": "Non-compatible API", "non_compatible_api": "Non-compatible API",
"ok": "OK", "ok": "OK",
"only_official_apps": "Only official apps", "only_highquality_apps": "Only high-quality apps",
"only_working_apps": "Only working apps", "only_working_apps": "Only working apps",
"only_decent_quality_apps": "Only decent quality apps",
"open": "Open", "open": "Open",
"operations": "Operations", "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!",
"os": "OS", "os": "OS",
"password": "Password", "password": "Password",
"password_confirmation": "Password confirmation", "password_confirmation": "Password confirmation",
@ -279,6 +288,10 @@
"reception": "Reception", "reception": "Reception",
"refresh_app_list": "Refresh list", "refresh_app_list": "Refresh list",
"remove_access": "Remove access", "remove_access": "Remove access",
"request_adoption": "waiting adoption",
"request_adoption_details": "The current maintainer would like to stop maintaining this app. Feel free to propose yourself as the new maintainer!",
"request_help": "need help",
"request_help_details": "The current maintainer would like some help with the maintainance of this app. Feel free to come contribute on it!",
"restore": "Restore", "restore": "Restore",
"run": "Run", "run": "Run",
"running": "Running", "running": "Running",

View file

@ -19,6 +19,10 @@
<dd>{{id}}</dd> <dd>{{id}}</dd>
<dt>{{t 'description'}}</dt> <dt>{{t 'description'}}</dt>
<dd>{{description}}</dd> <dd>{{description}}</dd>
{{#displayLicense}}
<dt>{{t 'license'}}</dt>
<dd>{{manifest.license}}</dd>
{{/displayLicense}}
<dt>{{t 'version'}}</dt> <dt>{{t 'version'}}</dt>
<dd>{{manifest.version}}</dd> <dd>{{manifest.version}}</dd>
<dt>{{t 'multi_instance'}}</dt> <dt>{{t 'multi_instance'}}</dt>

View file

@ -20,11 +20,12 @@
<input type="text" id="filter-app-cards" class="form-control" role="textbox" placeholder="{{t 'search_for_apps'}}" aria-describedby="basic-addon0"/> <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"> <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 type="button" role="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_working_apps'}}</span> <span class="caret"></span> <span id="app-cards-list-filter-text">{{t 'only_decent_quality_apps'}}</span> <span class="caret"></span>
</button> </button>
<ul id="dropdownFilter" class="dropdown-menu" data-filter="isworking" role="menu"> <ul id="dropdownFilter" class="dropdown-menu" data-filter="decentQuality" role="menu">
<li role="presentation" class="button dropdownFilter" data-filter="isHighQuality"><a class="menu-item" role="menu-item" tabindex="-1">{{t 'only_highquality_apps'}}</a></li>
<li role="presentation" class="button dropdownFilter" data-filter="decentQuality"><a class="menu-item" role="menu-item" tabindex="-1">{{t 'only_decent_quality_apps'}}</a></li>
<li role="presentation" class="button dropdownFilter" data-filter="isworking"><a class="menu-item" role="menu-item" tabindex="-1">{{t 'only_working_apps'}}</a></li> <li role="presentation" class="button dropdownFilter" data-filter="isworking"><a class="menu-item" role="menu-item" tabindex="-1">{{t 'only_working_apps'}}</a></li>
<li role="presentation" class="button dropdownFilter" data-filter="official"><a class="menu-item" role="menu-item" tabindex="-1">{{t 'only_official_apps'}}</a></li>
<li role="presentation" class="button dropdownFilter" data-filter="*"><a class="menu-item" role="menu-item" tabindex="-1">{{t 'all_apps'}}</a></li> <li role="presentation" class="button dropdownFilter" data-filter="*"><a class="menu-item" role="menu-item" tabindex="-1">{{t 'all_apps'}}</a></li>
</ul> </ul>
</div> </div>
@ -34,21 +35,19 @@
<div class="list-group grid"> <div class="list-group grid">
{{#apps}} {{#apps}}
<div class="app-card panel panel-default {{status}} {{state}} {{isWorking}} {{level}}-level"> <div class="app-card panel panel-default {{status}} {{state}} {{isWorking}} {{isHighQuality}} {{decentQuality}} {{level}}-level">
<div class="panel-body"> <div class="panel-body">
<h2 class="app-title">{{name}}</h2> <h2 class="app-title">{{name}}</h2>
<div class="category"> <div class="category">
{{#isCommunity}} <span class="label label-info label-as-badge app-status">{{t 'community'}}</span>{{/isCommunity}} <span class="label label-{{stateColor}} label-as-badge app-state" title="{{t (concat 'app_state_' state '_explanation') }}">{{t (concat 'app_state_' state) }}</span>
<span class="label label-{{stateColor}} label-as-badge app-state">{{t 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> <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>
{{#displayLicense}}<span class="label label-default app-license" title="{{t 'app_license'}}">{{license}}</span>{{/displayLicense}} <span class="label label-{{maintainedColor}} label-as-badge maintained-status" title="{{t (concat maintained '_details') }}"> {{t maintained}}</span>
</div> </div>
<div class="app-card-desc">{{description}}</div> <div class="app-card-desc">{{description}}</div>
</div> </div>
<div class="app-card-date-maintainer"> <div class="app-card-date-maintainer">
<i class="fa-refresh"></i> {{formatDate updateDate day="numeric" month="long" year="numeric"}} - <i class="fa-refresh"></i> {{formatDate updateDate day="numeric" month="long" year="numeric"}} -
{{#maintained}}<span title="{{t 'current_maintainer_title'}}" class="maintained"></span><i class="fa-user"></i> {{manifest.maintainer.name}}</span>{{/maintained}} <span title="{{t 'current_maintainer_title'}}" class="maintained"></span><i class="fa-user"></i> {{manifest.maintainer}}</span>
{{^maintained}}<i class="fas fa-warning"></i> {{t 'unmaintained'}}{{/maintained}}
</div> </div>
<div class="btn-group" role="group"> <div class="btn-group" role="group">
<a href="{{git.url}}" target="_BLANK" type="button" role="button" class="btn btn-default col-xs-4"> <a href="{{git.url}}" target="_BLANK" type="button" role="button" class="btn btn-default col-xs-4">