(function() { // Get application context var app = Sammy.apps['#main']; var store = app.store; /** * Apps * */ // List installed apps app.get('#/apps', function (c) { c.api('GET', '/apps?full', {}, function(data) { var apps = data['apps']; c.arraySortById(apps); c.view('app/app_list', {apps: apps}); }); }); function levelToColor(level) { if (level >= 8) { return 'best'; } else if (level > 4) { return 'success'; } else if (level >= 1) { return 'warning'; } else if (isNaN(level)) { return 'default'; } else { return 'danger' } } function stateToColor(state) { if (state === "high-quality") { return 'best'; } else if (state === "working") { return 'success'; } else { return 'danger'; } } 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) { if (stateColor === "danger" || levelColor === "danger") { return 'danger'; } else if (stateColor === "warning" || levelColor === "warning" || levelColor === "default") { return 'warning'; } else { return 'info'; } } 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 "?"; } } // 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) { app.state = "high-quality"; } if ( app.maintained === false ) { app.maintained = "orphaned"; } else if ( app.maintained === true ) { app.maintained = "maintained"; } app.manifest.maintainer = extractMaintainer(app.manifest); var isWorking = (app.state === 'working' || app.state === "high-quality") && app.level > 0; app.installable = (!app.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"; 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 }); // 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').data("filter", jQuery(this).data("filter")); // filter ! cardGrid.isotope({ filter: filterApps }); }); jQuery("#filter-app-cards").on("keyup", function() { cardGrid.isotope({ filter: filterApps }); }); $("#install-custom-app a[role='button']").on('click', function() { var url = $("#install-custom-app input[name='url']")[0].value; if (url.indexOf("github.com") < 0) { return; } c.confirm( y18n.t('applications'), y18n.t('confirm_install_custom_app'), function(){ c.redirect_to('#/apps/install/custom/' + encodeURIComponent(url)); } ); }); }; // 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']+'?full', {}, function(data) { c.api('GET', '/users/permissions', {}, function(data_permissions) { // Permissions data.permissions = data_permissions.permissions[c.params['app']+".main"]["allowed"]; // Multilingual description data.description = (typeof data.manifest.description[y18n.locale] !== 'undefined') ? data.manifest.description[y18n.locale] : data.manifest.description['en'] ; // Multi Instance settings data.manifest.multi_instance = data.manifest.multi_instance ? y18n.t('yes') : y18n.t('no'); data.install_time = new Date(data.settings.install_time * 1000); c.view('app/app_info', data, function() { // Button to set the app as default $('button[data-action="set-as-default"]').on("click", function() { var app = $(this).data("app"); c.confirm( y18n.t('applications'), y18n.t('confirm_app_default'), function() { c.api('PUT', '/apps/'+app+'/default', {}, function() { c.refresh() }); } ); }); // Button to uninstall the app $('button[data-action="uninstall"]').on("click", function() { var app = $(this).data("app"); c.confirm( y18n.t('applications'), y18n.t('confirm_uninstall', [app]), function() { c.api('DELETE', '/apps/'+ app, {}, function() { c.redirect_to('#/apps'); }); } ); }); }); }); }); }); // // App actions // // Get app actions list app.get('#/apps/:app/actions', function (c) { c.api('GET', '/apps/'+c.params['app']+'/actions', {}, function(data) { $.each(data.actions, function(_, action) { formatYunoHostStyleArguments(action.arguments, c.params); // Multilingual description if (action.description && Array.isArray(action.description)) action.description = (typeof action.description[y18n.locale] !== 'undefined') ? action.description[y18n.locale] : action.description['en'] ; }); c.view('app/app_actions', data); return; }); }); // Perform app action app.put('#/apps/:app/actions/:action', function(c) { // taken from app install $.each(c.params, function(k, v) { if (typeof(v) === 'object' && Array.isArray(v)) { // And return only first value c.params[k] = v[0]; } }); var app_id = c.params['app']; delete c.params['app']; var action_id = c.params['action']; delete c.params['action']; var params = { 'args': c.serialize(c.params.toHash()) } c.api('PUT', '/apps/'+app_id+'/actions/'+action_id, params, function() { c.redirect_to('#/apps/'+app_id+'/actions', {slide:false}); }); }); // // App config panel // // Get app config panel app.get('#/apps/:app/config-panel', function (c) { c.api('GET', '/apps/'+c.params['app']+'/config-panel', {}, function(data) { $.each(data.config_panel.panel, function(_, panel) { $.each(panel.sections, function(_, section) { formatYunoHostStyleArguments(section.options, c.params); }); }); c.view('app/app_config-panel', data); }); }); app.post('#/apps/:app/config', function(c) { // taken from app install $.each(c.params, function(k, v) { if (typeof(v) === 'object' && Array.isArray(v)) { // And return only first value c.params[k] = v[0]; } }); var app_id = c.params['app']; delete c.params['app']; var params = { 'args': c.serialize(c.params.toHash()) } c.api('POST', '/apps/'+app_id+'/config', params, function() { c.redirect_to('#/apps/'+app_id+'/config-panel', {slide:false}); }); }) // Helper function that formats YunoHost style arguments for generating a form function formatYunoHostStyleArguments(args, params) { if (!args) { return; } // this is in place modification, I don't like it but it was done this way $.each(args, function(k, v) { // Default values args[k].type = (typeof v.type !== 'undefined') ? v.type : 'string'; args[k].inputType = 'text'; args[k].isPassword = false; args[k].isDisplayText = false; args[k].required = (typeof v.optional !== 'undefined' && (v.optional == "true" || v.optional == true)) ? '' : 'required'; args[k].attributes = ""; args[k].helpText = ""; args[k].helpLink = ""; // Multilingual label if (typeof args[k].ask === "string") { args[k].label = args[k].ask; } else if (typeof args[k].ask[y18n.locale] !== 'undefined') { args[k].label = args[k].ask[y18n.locale]; } else { args[k].label = args[k].ask['en']; } // Multilingual help text if (typeof args[k].help !== 'undefined') { args[k].helpText = (typeof args[k].help[y18n.locale] !== 'undefined') ? args[k].help[y18n.locale] : args[k].help['en'] ; } // Input with choices becomes select list if (typeof args[k].choices !== 'undefined') { // Update choices values with key and checked data var choices = [] $.each(args[k].choices, function(ck, cv){ // Non key/value choices have numeric key, that we don't want. if (typeof ck == "number") { // Key is Value in this case. ck = cv; } choices.push({ value: ck, label: cv, selected: (ck == args[k].default) ? true : false, }); }); args[k].choices = choices; } // Special case for domain input. // Display a list of available domains if (v.name == 'domain' || args[k].type == 'domain') { args[k].choices = []; $.each(params.domains, function(key, domain){ args[k].choices.push({ value: domain, label: domain, selected: false }); }); // Custom help link args[k].helpLink += "<a href='#/domains'>"+y18n.t('manage_domains')+"</a>"; } // Special case for admin / user input. // Display a list of available users if (v.name == 'admin' || args[k].type == 'user') { args[k].choices = []; $.each(params.users, function(username, user){ args[k].choices.push({ value: username, label: user.fullname+' ('+user.mail+')', selected: false }); }); // Custom help link args[k].helpLink += "<a href='#/users'>"+y18n.t('manage_users')+"</a>"; } // 'app' type input display a list of available apps if (args[k].type == 'app') { args[k].choices = []; $.each(params.apps, function(key, app){ args[k].choices.push({ value: app.id, label: app.name, selected: false }); }); // Custom help link args[k].helpLink += "<a href='#/apps'>"+y18n.t('manage_apps')+"</a>"; } // Boolean fields if (args[k].type == 'boolean') { args[k].inputType = 'checkbox'; // Checked or not ? if (typeof args[k].default !== 'undefined') { if (args[k].default == true) { args[k].attributes = 'checked="checked"'; } } // 'default' is used as value, so we need to force it for checkboxes. args[k].default = 1; // Checkbox should not be required to be unchecked args[k].required = ''; // Clone a hidden input with empty value // https://stackoverflow.com/questions/476426/submit-an-html-form-with-empty-checkboxes var inputClone = { name : args[k].name, inputType : 'hidden', default : 0 }; args.push(inputClone); } // 'password' type input. if (v.name == 'password' || args[k].type == 'password') { // Change html input type args[k].inputType = 'password'; args[k].isPassword = true; } if (args[k].type == "display_text") { args[k].isDisplayText = true; args[k].label = args[k].label.split("\n"); } }); } // Helper function that build app installation form app.helper('appInstallForm', function(appId, manifest, params) { var data = { id: appId, manifest: manifest, displayLicense: (manifest['license'] !== undefined && manifest['license'] !== 'free') }; formatYunoHostStyleArguments(manifest.arguments.install, params); // Multilingual description if (typeof manifest.description === 'string') { data.description = manifest.description; } else if (typeof manifest.description[y18n.locale] !== 'undefined') { data.description = manifest.description[y18n.locale]; } else { data.description = manifest.description['en']; } // Multi Instance settings boolean to text data.manifest.multi_instance = manifest.multi_instance ? y18n.t('yes') : y18n.t('no'); // View app install form c.view('app/app_install', data); return; }); // App installation form app.get('#/apps/install/:app', function (c) { c.api('GET', '/appscatalog?full', {}, function(data) { var app_name = c.params["app"]; var app_infos = data["apps"][app_name]; if (app_infos['state'] === "validated") { app_infos['state'] = "official"; } var state_color = stateToColor(app_infos['state']); var level_color = levelToColor(parseInt(app_infos['level'])); var is_safe_for_install_color = combineColors(state_color, level_color); if ((is_safe_for_install_color === "warning") || (is_safe_for_install_color === "danger")) { c.confirm( y18n.t("applications"), y18n.t("confirm_install_app_"+is_safe_for_install_color), function(){ c.appInstallForm( app_name, app_infos.manifest, c.params ); }, function () { c.redirect_to('#/apps/catalog'); } ); } else { c.appInstallForm( app_name, app_infos.manifest, c.params ); } }); }); // Install app (POST) app.post('#/apps', function(c) { // Warn admin if app is going to be installed on domain root. if (c.params['path'] !== '/' || confirm(y18n.t('confirm_install_domain_root', [c.params['domain']]))) { var params = { label: c.params['label'], app: c.params['app'] }; // Check for duplicate arg produced by empty checkbox. (See inputClone) delete c.params['label']; delete c.params['app']; $.each(c.params, function(k, v) { if (typeof(v) === 'object' && Array.isArray(v)) { // And return only first value c.params[k] = v[0]; } }); params['args'] = c.serialize(c.params.toHash()); // Do not pass empty args. if (params['args'] === "") { delete params['args']; } c.api('POST', '/apps', params, function() { c.redirect_to('#/apps'); }); } else { c.flash('warning', y18n.t('app_install_cancel')); c.refresh(); } }); // Install custom app from github app.get('#/apps/install/custom/:url', function(c) { // Force trailing slash url = c.params['url']; url = url.replace(/\/?$/, '/'); raw_manifest_url = url.replace('github.com', 'raw.githubusercontent.com') + 'master/manifest.json' // Fetch manifest.json jQuery.ajax({ url: raw_manifest_url, type: 'GET' }) .done(function(manifest) { // raw.githubusercontent.com serve content as plain text manifest = jQuery.parseJSON(manifest) || {}; c.appInstallForm( url, manifest, c.params ); }) .fail(function(xhr) { c.flash('fail', y18n.t('app_install_custom_no_manifest')); c.redirect("#/apps/catalog/"); }); }); // Get app change label page app.get('#/apps/:app/changelabel', function (c) { c.api('GET', '/apps/'+c.params['app']+'?full', {}, function(app_data) { data = { id: c.params['app'], label: app_data.settings.label, }; c.view('app/app_changelabel', data); }); }); // Change app label app.post('#/apps/:app/changelabel', function (c) { params = {'new_label': c.params['label']}; c.api('PUT', '/apps/' + c.params['app'] + '/label', params, function(data) { c.redirect_to('#/apps/'+ c.params['app']); }); }); // Get app change URL page app.get('#/apps/:app/changeurl', function (c) { c.api('GET', '/apps/'+c.params['app']+'?full', {}, function(app_data) { c.api('GET', '/domains', {}, function(domain_data) { // Display a list of available domains var domains = []; $.each(domain_data.domains, function(k, domain) { domains.push({ value: domain, label: domain, // Select current domain selected: (domain == app_data.settings.domain ? true : false) }); }); data = { id: c.params['app'], label: app_data.manifest.name, domains: domains, // Pre-fill with current path path: app_data.settings.path }; c.view('app/app_changeurl', data); }); }); }); // Change app URL app.post('#/apps/:app/changeurl', function (c) { c.confirm( y18n.t('applications'), y18n.t('confirm_app_change_url', [c.params['app']]), function() { params = {'domain': c.params['domain'], 'path': c.params['path']}; c.api('PUT', '/apps/' + c.params['app'] + '/changeurl', params, function(data) { c.redirect_to('#/apps/'+ c.params['app']); }); } ); }); })();