From e5b7ed4fafffb56a7d43d0663977e5cd5d30ce0b Mon Sep 17 00:00:00 2001 From: Joseph Nuthalapati Date: Fri, 20 Sep 2024 15:49:45 +0530 Subject: [PATCH] *: Implements tags for apps - Add tags to Info component of apps. Use only English tags for all operations. Localized tags are used for presentation to the user only. Add tags to all the apps. Conventions (English): 1. Tags describing use cases should be in kebab case. 2. Protocols in tag names should be in their canonical format. 3. Tags needn't be 100% technically correct. This can get in the way of comparing apps using a tag. Words that describe use cases that users can easily understand should be preferred over being pedantic. 4. Tags should be short, ideally not more than 2 words. Avoid conjunctions like "and", "or" in tags. 5. Avoid redundant words like "server", or "web-clients". Most apps on FreedomBox are either servers or web clients. 6. Keep your nouns singular in tags. - Use query params to filter the Apps page by tags. When all tags are removed, redirect to /apps. - Add UI elements to add and remove tag filters in the Apps page. Make the UI similar to GitLab issue tags. Since there are 40 apps, there will be at least 40 tags. Selecting a tag from a dropdown will be difficult on mobile devices. A fuzzy search is useful to find tags to add to the filter. Allow user to find the best match for the search term and highlight it visually. The user can then press Enter to select the highlighted tag. Make tag search case-insensitive. Make the dropdown menu scrollable with a fixed size. User input is debounced by 300 ms during search. - tests: Add missing mock in test_module_loader.py - Add functional tests [sunil] - 'list' can be used instead of 'List' for typing in recent Python versions. - Reserve tripe-quoted strings for docstrings. - Undo some changes in module initialization, use module_name for logging errors. - isort and yapf changes. - Encode parameters before adding them to the URL. Tests: - Tested the functionality of filtering by tag with one tag and two tags. Signed-off-by: Joseph Nuthalapati Signed-off-by: Sunil Mohan Adapa Reviewed-by: Sunil Mohan Adapa --- plinth/app.py | 21 ++- plinth/module_loader.py | 7 +- plinth/modules/bepasty/__init__.py | 2 +- plinth/modules/bepasty/manifest.py | 2 + plinth/modules/calibre/__init__.py | 2 +- plinth/modules/calibre/manifest.py | 2 + plinth/modules/coturn/__init__.py | 3 +- plinth/modules/coturn/manifest.py | 4 + plinth/modules/deluge/__init__.py | 3 +- plinth/modules/deluge/manifest.py | 2 + plinth/modules/ejabberd/__init__.py | 12 +- plinth/modules/ejabberd/manifest.py | 9 + plinth/modules/email/__init__.py | 2 +- plinth/modules/email/manifest.py | 2 + plinth/modules/featherwiki/__init__.py | 2 +- plinth/modules/featherwiki/manifest.py | 2 + plinth/modules/gitweb/__init__.py | 2 +- plinth/modules/gitweb/manifest.py | 2 + plinth/modules/i2p/__init__.py | 2 +- plinth/modules/i2p/manifest.py | 2 + plinth/modules/ikiwiki/__init__.py | 2 +- plinth/modules/ikiwiki/manifest.py | 2 + plinth/modules/infinoted/__init__.py | 2 +- plinth/modules/infinoted/manifest.py | 2 + plinth/modules/janus/__init__.py | 2 +- plinth/modules/janus/manifest.py | 2 + plinth/modules/jsxc/__init__.py | 2 +- plinth/modules/jsxc/manifest.py | 2 + plinth/modules/kiwix/__init__.py | 2 +- plinth/modules/kiwix/manifest.py | 2 + plinth/modules/matrixsynapse/__init__.py | 3 +- plinth/modules/matrixsynapse/manifest.py | 10 ++ plinth/modules/mediawiki/__init__.py | 3 +- plinth/modules/mediawiki/manifest.py | 2 + plinth/modules/minetest/__init__.py | 2 +- plinth/modules/minetest/manifest.py | 2 + plinth/modules/minidlna/__init__.py | 3 +- plinth/modules/minidlna/manifest.py | 2 + plinth/modules/miniflux/__init__.py | 2 +- plinth/modules/miniflux/manifest.py | 2 + plinth/modules/mumble/__init__.py | 2 +- plinth/modules/mumble/manifest.py | 2 + plinth/modules/nextcloud/__init__.py | 2 +- plinth/modules/nextcloud/manifest.py | 2 + plinth/modules/openvpn/__init__.py | 2 +- plinth/modules/openvpn/manifest.py | 2 + plinth/modules/privoxy/__init__.py | 3 +- plinth/modules/privoxy/manifest.py | 4 + plinth/modules/quassel/__init__.py | 2 +- plinth/modules/quassel/manifest.py | 2 + plinth/modules/radicale/__init__.py | 3 +- plinth/modules/radicale/manifest.py | 8 + plinth/modules/roundcube/__init__.py | 3 +- plinth/modules/roundcube/manifest.py | 12 +- plinth/modules/rssbridge/__init__.py | 2 +- plinth/modules/rssbridge/manifest.py | 9 +- plinth/modules/samba/__init__.py | 2 +- plinth/modules/samba/manifest.py | 2 + plinth/modules/searx/__init__.py | 2 +- plinth/modules/searx/manifest.py | 2 + plinth/modules/shaarli/__init__.py | 2 +- plinth/modules/shaarli/manifest.py | 4 +- plinth/modules/shadowsocks/__init__.py | 3 +- plinth/modules/shadowsocks/manifest.py | 10 ++ plinth/modules/shadowsocksserver/__init__.py | 3 +- plinth/modules/shadowsocksserver/manifest.py | 4 + plinth/modules/sharing/__init__.py | 3 +- plinth/modules/sharing/manifest.py | 4 + plinth/modules/syncthing/__init__.py | 3 +- plinth/modules/syncthing/manifest.py | 2 + plinth/modules/tiddlywiki/__init__.py | 3 +- plinth/modules/tiddlywiki/manifest.py | 11 ++ plinth/modules/tor/__init__.py | 2 +- plinth/modules/tor/manifest.py | 7 + plinth/modules/torproxy/__init__.py | 2 +- plinth/modules/torproxy/manifest.py | 8 + plinth/modules/transmission/__init__.py | 3 +- plinth/modules/transmission/manifest.py | 2 + plinth/modules/ttrss/__init__.py | 2 +- plinth/modules/ttrss/manifest.py | 2 + plinth/modules/wireguard/__init__.py | 2 +- plinth/modules/wireguard/manifest.py | 2 + plinth/modules/wordpress/__init__.py | 2 +- plinth/modules/wordpress/manifest.py | 8 +- plinth/modules/zoph/manifest.py | 2 + plinth/templates/app-header.html | 11 ++ plinth/templates/apps.html | 37 ++++ plinth/templates/cards.html | 6 +- plinth/tests/tags/__init__.py | 0 plinth/tests/tags/test_functional.py | 60 +++++++ plinth/views.py | 56 +++++- static/tags.js | 170 +++++++++++++++++++ static/themes/default/css/main.css | 54 +++++- 93 files changed, 622 insertions(+), 72 deletions(-) create mode 100644 plinth/tests/tags/__init__.py create mode 100644 plinth/tests/tags/test_functional.py create mode 100644 static/tags.js diff --git a/plinth/app.py b/plinth/app.py index be936b674..b9aa568ef 100644 --- a/plinth/app.py +++ b/plinth/app.py @@ -7,7 +7,7 @@ import collections import enum import inspect import logging -from typing import ClassVar, TypeAlias +from typing import ClassVar, Dict, List, TypeAlias from plinth import cfg from plinth.diagnostic_check import DiagnosticCheck @@ -434,7 +434,7 @@ class Info(FollowerComponent): def __init__(self, app_id, version, is_essential=False, depends=None, name=None, icon=None, icon_filename=None, short_description=None, description=None, manual_page=None, - clients=None, donation_url=None): + clients=None, donation_url=None, tags=None): """Store the basic properties of an app as a component. Each app must contain at least one component of this type to provide @@ -504,6 +504,9 @@ class Info(FollowerComponent): 'donation_url' is a link to a webpage that describes how to donate to the upstream project. + 'tags' is a list of tags that describe the app. Tags help users to find + similar apps or alternatives and discover use cases. + """ self.component_id = app_id + '-info' self.app_id = app_id @@ -518,9 +521,19 @@ class Info(FollowerComponent): self.manual_page = manual_page self.clients = clients self.donation_url = donation_url + self.tags = tags or [] if clients: clients_module.validate(clients) + @classmethod + def list_tags(self) -> list: + """Return a list of untranslated tags.""" + tags = set() + for app in App.list(): + tags.update(app.info.tags) + + return list(tags) + class EnableState(LeaderComponent): """A component to hold the enable state of an app using a simple flag. @@ -626,8 +639,8 @@ def _initialize_module(module_name, module): for app_class in app_classes: app_class() except Exception as exception: - logger.exception('Exception while running init for %s: %s', module, - exception) + logger.exception('Exception while running init for module %s: %s', + module_name, exception) if cfg.develop: raise diff --git a/plinth/module_loader.py b/plinth/module_loader.py index 007fb396f..c45888be8 100644 --- a/plinth/module_loader.py +++ b/plinth/module_loader.py @@ -3,7 +3,6 @@ Discover, load and manage FreedomBox applications. """ -import collections import importlib import logging import pathlib @@ -16,7 +15,7 @@ from plinth.signals import pre_module_loading logger = logging.getLogger(__name__) -loaded_modules = collections.OrderedDict() +loaded_modules = dict() _modules_to_load = None @@ -36,8 +35,8 @@ def load_modules(): for module_import_path in get_modules_to_load(): module_name = module_import_path.split('.')[-1] try: - loaded_modules[module_name] = importlib.import_module( - module_import_path) + module = importlib.import_module(module_import_path) + loaded_modules[module_name] = module except Exception as exception: logger.exception('Could not import %s: %s', module_import_path, exception) diff --git a/plinth/modules/bepasty/__init__.py b/plinth/modules/bepasty/__init__.py index 09c199a3e..ff8208e2c 100644 --- a/plinth/modules/bepasty/__init__.py +++ b/plinth/modules/bepasty/__init__.py @@ -58,7 +58,7 @@ class BepastyApp(app_module.App): icon_filename='bepasty', short_description=_('File & Snippet Sharing'), description=_description, manual_page='bepasty', - clients=manifest.clients) + clients=manifest.clients, tags=manifest.tags) self.add(info) menu_item = menu.Menu('menu-bepasty', info.name, diff --git a/plinth/modules/bepasty/manifest.py b/plinth/modules/bepasty/manifest.py index b29d271fa..6eb552dbb 100644 --- a/plinth/modules/bepasty/manifest.py +++ b/plinth/modules/bepasty/manifest.py @@ -19,3 +19,5 @@ backup = { }, 'services': ['uwsgi'], } + +tags = [_('File Sharing'), _('Pastebin')] diff --git a/plinth/modules/calibre/__init__.py b/plinth/modules/calibre/__init__.py index a04792a88..646e88525 100644 --- a/plinth/modules/calibre/__init__.py +++ b/plinth/modules/calibre/__init__.py @@ -56,7 +56,7 @@ class CalibreApp(app_module.App): name=_('calibre'), icon_filename='calibre', short_description=_('E-book Library'), description=_description, manual_page='Calibre', - clients=manifest.clients, + clients=manifest.clients, tags=manifest.tags, donation_url='https://calibre-ebook.com/donate') self.add(info) diff --git a/plinth/modules/calibre/manifest.py b/plinth/modules/calibre/manifest.py index a862e90fd..f92b136ed 100644 --- a/plinth/modules/calibre/manifest.py +++ b/plinth/modules/calibre/manifest.py @@ -16,3 +16,5 @@ backup = { }, 'services': ['calibre-server-freedombox'] } + +tags = [_('Ebook'), _('Library'), _('Ebook Reader')] diff --git a/plinth/modules/coturn/__init__.py b/plinth/modules/coturn/__init__.py index 133511ac7..805de147d 100644 --- a/plinth/modules/coturn/__init__.py +++ b/plinth/modules/coturn/__init__.py @@ -51,7 +51,8 @@ class CoturnApp(app_module.App): info = app_module.Info(app_id=self.app_id, version=self._version, name=_('Coturn'), icon_filename='coturn', short_description=_('VoIP Helper'), - description=_description, manual_page='Coturn') + description=_description, manual_page='Coturn', + tags=manifest.tags) self.add(info) menu_item = menu.Menu('menu-coturn', info.name, info.short_description, diff --git a/plinth/modules/coturn/manifest.py b/plinth/modules/coturn/manifest.py index 0e7047930..777f09a21 100644 --- a/plinth/modules/coturn/manifest.py +++ b/plinth/modules/coturn/manifest.py @@ -1,3 +1,7 @@ # SPDX-License-Identifier: AGPL-3.0-or-later +from django.utils.translation import gettext_lazy as _ + backup = {'secrets': {'directories': ['/etc/coturn']}, 'services': ['coturn']} + +tags = [_('VoIP'), _('STUN'), _('TURN')] diff --git a/plinth/modules/deluge/__init__.py b/plinth/modules/deluge/__init__.py index deec8588e..9772fcf19 100644 --- a/plinth/modules/deluge/__init__.py +++ b/plinth/modules/deluge/__init__.py @@ -47,7 +47,8 @@ class DelugeApp(app_module.App): short_description=_('BitTorrent Web Client'), description=_description, manual_page='Deluge', clients=manifest.clients, - donation_url='https://www.patreon.com/deluge_cas') + donation_url='https://www.patreon.com/deluge_cas', + tags=manifest.tags) self.add(info) menu_item = menu.Menu('menu-deluge', info.name, info.short_description, diff --git a/plinth/modules/deluge/manifest.py b/plinth/modules/deluge/manifest.py index 81f8fb8f9..7335ea1b5 100644 --- a/plinth/modules/deluge/manifest.py +++ b/plinth/modules/deluge/manifest.py @@ -17,3 +17,5 @@ backup = { }, 'services': ['deluged', 'deluge-web'] } + +tags = [_('File Sharing'), _('BitTorrent'), _('Client'), _('P2P')] diff --git a/plinth/modules/ejabberd/__init__.py b/plinth/modules/ejabberd/__init__.py index e8240e14f..20e2ee88d 100644 --- a/plinth/modules/ejabberd/__init__.py +++ b/plinth/modules/ejabberd/__init__.py @@ -56,11 +56,13 @@ class EjabberdApp(app_module.App): """Create components for the app.""" super().__init__() - info = app_module.Info( - app_id=self.app_id, version=self._version, depends=['coturn'], - name=_('ejabberd'), icon_filename='ejabberd', - short_description=_('Chat Server'), description=_description, - manual_page='ejabberd', clients=manifest.clients) + info = app_module.Info(app_id=self.app_id, version=self._version, + depends=['coturn'], name=_('ejabberd'), + icon_filename='ejabberd', + short_description=_('Chat Server'), + description=_description, + manual_page='ejabberd', + clients=manifest.clients, tags=manifest.tags) self.add(info) menu_item = menu.Menu('menu-ejabberd', info.name, diff --git a/plinth/modules/ejabberd/manifest.py b/plinth/modules/ejabberd/manifest.py index 8c3c17b3a..62108c4ce 100644 --- a/plinth/modules/ejabberd/manifest.py +++ b/plinth/modules/ejabberd/manifest.py @@ -119,3 +119,12 @@ backup = { }, 'services': ['ejabberd'] } + +tags = [ + _('XMPP'), + _('VoIP'), + _('IM'), + _('Encrypted Messaging'), + _('Audio Chat'), + _('Video Chat') +] diff --git a/plinth/modules/email/__init__.py b/plinth/modules/email/__init__.py index 09ba154d4..08aea32d8 100644 --- a/plinth/modules/email/__init__.py +++ b/plinth/modules/email/__init__.py @@ -63,7 +63,7 @@ class EmailApp(plinth.app.App): icon_filename='email', short_description=_('Email Server'), description=_description, manual_page='Email', - clients=manifest.clients, + clients=manifest.clients, tags=manifest.tags, donation_url='https://rspamd.com/support.html') self.add(info) diff --git a/plinth/modules/email/manifest.py b/plinth/modules/email/manifest.py index d46f8fbca..6097000e2 100644 --- a/plinth/modules/email/manifest.py +++ b/plinth/modules/email/manifest.py @@ -78,3 +78,5 @@ backup = { }, 'services': ['postfix', 'dovecot', 'rspamd'] } + +tags = [_('Email')] diff --git a/plinth/modules/featherwiki/__init__.py b/plinth/modules/featherwiki/__init__.py index eb6eeb9e9..3ac5dae01 100644 --- a/plinth/modules/featherwiki/__init__.py +++ b/plinth/modules/featherwiki/__init__.py @@ -61,7 +61,7 @@ class FeatherWikiApp(app_module.App): short_description=_('Personal Notebooks'), description=_description, manual_page='FeatherWiki', - clients=manifest.clients) + clients=manifest.clients, tags=manifest.tags) self.add(info) menu_item = menu.Menu('menu-featherwiki', info.name, diff --git a/plinth/modules/featherwiki/manifest.py b/plinth/modules/featherwiki/manifest.py index 0469011c0..2855ee098 100644 --- a/plinth/modules/featherwiki/manifest.py +++ b/plinth/modules/featherwiki/manifest.py @@ -14,3 +14,5 @@ clients = [{ }] backup = {'data': {'directories': [str(wiki_dir)]}} + +tags = [_('Wiki'), _('Note Taking'), _('Website'), _('Quine'), _('non-Debian')] diff --git a/plinth/modules/gitweb/__init__.py b/plinth/modules/gitweb/__init__.py index 78c34ecf5..7e59fb0c5 100644 --- a/plinth/modules/gitweb/__init__.py +++ b/plinth/modules/gitweb/__init__.py @@ -48,7 +48,7 @@ class GitwebApp(app_module.App): name=_('Gitweb'), icon_filename='gitweb', short_description=_('Simple Git Hosting'), description=_description, manual_page='GitWeb', - clients=manifest.clients) + clients=manifest.clients, tags=manifest.tags) self.add(info) menu_item = menu.Menu('menu-gitweb', info.name, info.short_description, diff --git a/plinth/modules/gitweb/manifest.py b/plinth/modules/gitweb/manifest.py index ad2f8e782..f48d69410 100644 --- a/plinth/modules/gitweb/manifest.py +++ b/plinth/modules/gitweb/manifest.py @@ -33,3 +33,5 @@ clients = [ ] backup = {'data': {'directories': [GIT_REPO_PATH]}} + +tags = [_('Git'), _('Version Control'), _('Dev Tool')] diff --git a/plinth/modules/i2p/__init__.py b/plinth/modules/i2p/__init__.py index acd89bcdf..4116357f3 100644 --- a/plinth/modules/i2p/__init__.py +++ b/plinth/modules/i2p/__init__.py @@ -52,7 +52,7 @@ class I2PApp(app_module.App): name=_('I2P'), icon_filename='i2p', short_description=_('Anonymity Network'), description=_description, manual_page='I2P', - clients=manifest.clients) + clients=manifest.clients, tags=manifest.tags) self.add(info) menu_item = menu.Menu('menu-i2p', info.name, info.short_description, diff --git a/plinth/modules/i2p/manifest.py b/plinth/modules/i2p/manifest.py index 6c41320d3..2f1f64beb 100644 --- a/plinth/modules/i2p/manifest.py +++ b/plinth/modules/i2p/manifest.py @@ -39,3 +39,5 @@ backup = { }, 'services': ['i2p'] } + +tags = [_('Anonymity Network'), _('Censorship Resistance')] diff --git a/plinth/modules/ikiwiki/__init__.py b/plinth/modules/ikiwiki/__init__.py index 3d498fdea..7e07ef6ff 100644 --- a/plinth/modules/ikiwiki/__init__.py +++ b/plinth/modules/ikiwiki/__init__.py @@ -45,7 +45,7 @@ class IkiwikiApp(app_module.App): name=_('ikiwiki'), icon_filename='ikiwiki', short_description=_('Wiki and Blog'), description=_description, manual_page='Ikiwiki', - clients=manifest.clients, + clients=manifest.clients, tags=manifest.tags, donation_url='https://ikiwiki.info/tipjar/') self.add(info) diff --git a/plinth/modules/ikiwiki/manifest.py b/plinth/modules/ikiwiki/manifest.py index 6d8f98bc2..c531de3c4 100644 --- a/plinth/modules/ikiwiki/manifest.py +++ b/plinth/modules/ikiwiki/manifest.py @@ -11,3 +11,5 @@ clients = [{ }] backup = {'data': {'directories': ['/var/lib/ikiwiki/', '/var/www/ikiwiki/']}} + +tags = [_('Wiki'), _('Blog'), _('Website')] diff --git a/plinth/modules/infinoted/__init__.py b/plinth/modules/infinoted/__init__.py index 00f98e69c..6a297ae6c 100644 --- a/plinth/modules/infinoted/__init__.py +++ b/plinth/modules/infinoted/__init__.py @@ -42,7 +42,7 @@ class InfinotedApp(app_module.App): short_description=_('Gobby Server'), description=_description, manual_page='Infinoted', - clients=manifest.clients) + clients=manifest.clients, tags=manifest.tags) self.add(info) menu_item = menu.Menu('menu-infinoted', info.name, diff --git a/plinth/modules/infinoted/manifest.py b/plinth/modules/infinoted/manifest.py index d844d5c27..fad1f5786 100644 --- a/plinth/modules/infinoted/manifest.py +++ b/plinth/modules/infinoted/manifest.py @@ -42,3 +42,5 @@ backup = { }, 'services': ['infinoted'] } + +tags = [_('Note Taking'), _('Collaborative Editing'), _('Gobby')] diff --git a/plinth/modules/janus/__init__.py b/plinth/modules/janus/__init__.py index 3c4dafac2..f1e6ae8e1 100644 --- a/plinth/modules/janus/__init__.py +++ b/plinth/modules/janus/__init__.py @@ -43,7 +43,7 @@ class JanusApp(app_module.App): icon_filename='janus', short_description=_('Video Room'), description=_description, manual_page='Janus', - clients=manifest.clients) + clients=manifest.clients, tags=manifest.tags) self.add(info) menu_item = menu.Menu('menu-janus', info.name, info.short_description, diff --git a/plinth/modules/janus/manifest.py b/plinth/modules/janus/manifest.py index 467c65002..780f75efb 100644 --- a/plinth/modules/janus/manifest.py +++ b/plinth/modules/janus/manifest.py @@ -12,3 +12,5 @@ clients = [{ }] backup: dict = {} + +tags = [_('Video Conferencing'), _('WebRTC')] diff --git a/plinth/modules/jsxc/__init__.py b/plinth/modules/jsxc/__init__.py index 2f66bdcd8..314f55d83 100644 --- a/plinth/modules/jsxc/__init__.py +++ b/plinth/modules/jsxc/__init__.py @@ -38,7 +38,7 @@ class JSXCApp(app_module.App): name=_('JSXC'), icon_filename='jsxc', short_description=_('Chat Client'), description=_description, manual_page='JSXC', - clients=manifest.clients) + clients=manifest.clients, tags=manifest.tags) self.add(info) menu_item = menu.Menu('menu-jsxc', info.name, info.short_description, diff --git a/plinth/modules/jsxc/manifest.py b/plinth/modules/jsxc/manifest.py index 0eb350b7c..565924b3f 100644 --- a/plinth/modules/jsxc/manifest.py +++ b/plinth/modules/jsxc/manifest.py @@ -12,3 +12,5 @@ clients = [{ }] backup: dict = {} + +tags = [_('XMPP'), _('Client')] diff --git a/plinth/modules/kiwix/__init__.py b/plinth/modules/kiwix/__init__.py index eaa439a12..eded154d8 100644 --- a/plinth/modules/kiwix/__init__.py +++ b/plinth/modules/kiwix/__init__.py @@ -56,7 +56,7 @@ class KiwixApp(app_module.App): app_id=self.app_id, version=self._version, name=_('Kiwix'), icon_filename='kiwix', short_description=_('Offline Wikipedia'), description=_description, manual_page='Kiwix', - clients=manifest.clients, + clients=manifest.clients, tags=manifest.tags, donation_url='https://www.kiwix.org/en/support-us/') self.add(info) diff --git a/plinth/modules/kiwix/manifest.py b/plinth/modules/kiwix/manifest.py index bf45ebe8d..f989f9cd8 100644 --- a/plinth/modules/kiwix/manifest.py +++ b/plinth/modules/kiwix/manifest.py @@ -18,3 +18,5 @@ backup = { }, 'services': ['kiwix-server-freedombox'] } + +tags = [_('Offline Reader'), _('Archival'), _('Censorship Resistance')] diff --git a/plinth/modules/matrixsynapse/__init__.py b/plinth/modules/matrixsynapse/__init__.py index dc1292081..403771907 100644 --- a/plinth/modules/matrixsynapse/__init__.py +++ b/plinth/modules/matrixsynapse/__init__.py @@ -54,7 +54,8 @@ class MatrixSynapseApp(app_module.App): app_id=self.app_id, version=self._version, depends=['coturn'], name=_('Matrix Synapse'), icon_filename='matrixsynapse', short_description=_('Chat Server'), description=_description, - manual_page='MatrixSynapse', clients=manifest.clients) + manual_page='MatrixSynapse', clients=manifest.clients, + tags=manifest.tags) self.add(info) menu_item = menu.Menu('menu-matrixsynapse', info.name, diff --git a/plinth/modules/matrixsynapse/manifest.py b/plinth/modules/matrixsynapse/manifest.py index efeddb638..fc7ad7da0 100644 --- a/plinth/modules/matrixsynapse/manifest.py +++ b/plinth/modules/matrixsynapse/manifest.py @@ -96,3 +96,13 @@ backup = { }, 'services': ['matrix-synapse'] } + +tags = [ + _('Chat Room'), + _('Encrypted Messaging'), + _('IM'), + _('Audio Chat'), + _('Video Chat'), + _('Matrix'), + _('VoIP') +] diff --git a/plinth/modules/mediawiki/__init__.py b/plinth/modules/mediawiki/__init__.py index 413e8da85..31eb4edc9 100644 --- a/plinth/modules/mediawiki/__init__.py +++ b/plinth/modules/mediawiki/__init__.py @@ -52,7 +52,8 @@ class MediaWikiApp(app_module.App): short_description=_('Wiki'), description=_description, manual_page='MediaWiki', - clients=manifest.clients) + clients=manifest.clients, + tags=manifest.tags) self.add(info) menu_item = menu.Menu('menu-mediawiki', info.name, diff --git a/plinth/modules/mediawiki/manifest.py b/plinth/modules/mediawiki/manifest.py index 3e4c72b75..e7f8713bd 100644 --- a/plinth/modules/mediawiki/manifest.py +++ b/plinth/modules/mediawiki/manifest.py @@ -21,3 +21,5 @@ backup = { }, 'services': ['mediawiki-jobrunner'] } + +tags = [_('Wiki'), _('Website')] diff --git a/plinth/modules/minetest/__init__.py b/plinth/modules/minetest/__init__.py index fe08ad228..cea175750 100644 --- a/plinth/modules/minetest/__init__.py +++ b/plinth/modules/minetest/__init__.py @@ -56,7 +56,7 @@ class MinetestApp(app_module.App): app_id=self.app_id, version=self._version, name=_('Minetest'), icon_filename='minetest', short_description=_('Block Sandbox'), description=_description, manual_page='Minetest', - clients=manifest.clients, + clients=manifest.clients, tags=manifest.tags, donation_url='https://www.minetest.net/get-involved/#donate') self.add(info) diff --git a/plinth/modules/minetest/manifest.py b/plinth/modules/minetest/manifest.py index c23ff3f15..539a8c148 100644 --- a/plinth/modules/minetest/manifest.py +++ b/plinth/modules/minetest/manifest.py @@ -45,3 +45,5 @@ backup = { }, 'services': ['minetest-server'] } + +tags = [_('Game'), _('Block Sandbox')] diff --git a/plinth/modules/minidlna/__init__.py b/plinth/modules/minidlna/__init__.py index a301b9764..1dc6d7cd9 100644 --- a/plinth/modules/minidlna/__init__.py +++ b/plinth/modules/minidlna/__init__.py @@ -46,7 +46,8 @@ class MiniDLNAApp(app_module.App): short_description=_('Simple Media Server'), description=_description, manual_page='MiniDLNA', - clients=manifest.clients) + clients=manifest.clients, + tags=manifest.tags) self.add(info) menu_item = menu.Menu( diff --git a/plinth/modules/minidlna/manifest.py b/plinth/modules/minidlna/manifest.py index 5f62d84a1..007e8ce76 100644 --- a/plinth/modules/minidlna/manifest.py +++ b/plinth/modules/minidlna/manifest.py @@ -112,3 +112,5 @@ backup = { }, 'services': ['minidlna'] } + +tags = [_('Media Server'), _('Television'), _('UPnP'), _('DLNA')] diff --git a/plinth/modules/miniflux/__init__.py b/plinth/modules/miniflux/__init__.py index cb846b541..8ae1cccaa 100644 --- a/plinth/modules/miniflux/__init__.py +++ b/plinth/modules/miniflux/__init__.py @@ -44,7 +44,7 @@ class MinifluxApp(app_module.App): short_description=_('News Feed Reader'), description=_description, manual_page='miniflux', - clients=manifest.clients, + clients=manifest.clients, tags=manifest.tags, donation_url='https://miniflux.app/#donations') self.add(info) diff --git a/plinth/modules/miniflux/manifest.py b/plinth/modules/miniflux/manifest.py index af244b701..f112c564b 100644 --- a/plinth/modules/miniflux/manifest.py +++ b/plinth/modules/miniflux/manifest.py @@ -134,3 +134,5 @@ backup = { }, 'services': ['miniflux'] } + +tags = [_('Feed Reader'), _('News'), _('RSS'), _('ATOM')] diff --git a/plinth/modules/mumble/__init__.py b/plinth/modules/mumble/__init__.py index e43f26be7..f03e81966 100644 --- a/plinth/modules/mumble/__init__.py +++ b/plinth/modules/mumble/__init__.py @@ -45,7 +45,7 @@ class MumbleApp(app_module.App): app_id=self.app_id, version=self._version, name=_('Mumble'), icon_filename='mumble', short_description=_('Voice Chat'), description=_description, manual_page='Mumble', - clients=manifest.clients, + clients=manifest.clients, tags=manifest.tags, donation_url='https://wiki.mumble.info/wiki/Donate') self.add(info) diff --git a/plinth/modules/mumble/manifest.py b/plinth/modules/mumble/manifest.py index 95a4c8fb4..fa7843aed 100644 --- a/plinth/modules/mumble/manifest.py +++ b/plinth/modules/mumble/manifest.py @@ -63,3 +63,5 @@ backup = { }, 'services': ['mumble-server'] } + +tags = [_('Audio Chat'), _('VoIP')] diff --git a/plinth/modules/nextcloud/__init__.py b/plinth/modules/nextcloud/__init__.py index 3a3f8920f..cb2812790 100644 --- a/plinth/modules/nextcloud/__init__.py +++ b/plinth/modules/nextcloud/__init__.py @@ -59,7 +59,7 @@ class NextcloudApp(app_module.App): icon_filename='nextcloud', short_description=_('File Storage & Collaboration'), description=_description, manual_page='Nextcloud', - clients=manifest.clients) + clients=manifest.clients, tags=manifest.tags) self.add(info) menu_item = menu.Menu('menu-nextcloud', info.name, diff --git a/plinth/modules/nextcloud/manifest.py b/plinth/modules/nextcloud/manifest.py index fa875a67a..cf30d69d2 100644 --- a/plinth/modules/nextcloud/manifest.py +++ b/plinth/modules/nextcloud/manifest.py @@ -52,3 +52,5 @@ backup = { 'files': ['/var/lib/plinth/backups-data/nextcloud-database.sql'] } } + +tags = [_('Cloud Storage'), _('File Sharing'), _('non-Debian')] diff --git a/plinth/modules/openvpn/__init__.py b/plinth/modules/openvpn/__init__.py index 5a41ea820..5ceb93601 100644 --- a/plinth/modules/openvpn/__init__.py +++ b/plinth/modules/openvpn/__init__.py @@ -46,7 +46,7 @@ class OpenVPNApp(app_module.App): name=_('OpenVPN'), icon_filename='openvpn', short_description=_('Virtual Private Network'), description=_description, manual_page='OpenVPN', - clients=manifest.clients) + clients=manifest.clients, tags=manifest.tags) self.add(info) menu_item = menu.Menu('menu-openvpn', info.name, diff --git a/plinth/modules/openvpn/manifest.py b/plinth/modules/openvpn/manifest.py index 22f745926..cf7f2b613 100644 --- a/plinth/modules/openvpn/manifest.py +++ b/plinth/modules/openvpn/manifest.py @@ -56,3 +56,5 @@ clients = [{ 'url': 'https://tunnelblick.net/downloads.html' }] }] + +tags = [_('VPN'), _('Anonymity'), _('Remote Access')] diff --git a/plinth/modules/privoxy/__init__.py b/plinth/modules/privoxy/__init__.py index 0e4a01e28..f65a2b494 100644 --- a/plinth/modules/privoxy/__init__.py +++ b/plinth/modules/privoxy/__init__.py @@ -53,7 +53,8 @@ class PrivoxyApp(app_module.App): app_id=self.app_id, version=self._version, name=_('Privoxy'), icon_filename='privoxy', short_description=_('Web Proxy'), description=_description, manual_page='Privoxy', - donation_url='https://www.privoxy.org/faq/general.html#DONATE') + donation_url='https://www.privoxy.org/faq/general.html#DONATE', + tags=manifest.tags) self.add(info) menu_item = menu.Menu('menu-privoxy', info.name, diff --git a/plinth/modules/privoxy/manifest.py b/plinth/modules/privoxy/manifest.py index 099231aa8..f346fe0a0 100644 --- a/plinth/modules/privoxy/manifest.py +++ b/plinth/modules/privoxy/manifest.py @@ -3,4 +3,8 @@ Application manifest for privoxy. """ +from django.utils.translation import gettext_lazy as _ + backup: dict = {} + +tags = [_('Ad Blocker'), _('Proxy'), _('Local Network')] diff --git a/plinth/modules/quassel/__init__.py b/plinth/modules/quassel/__init__.py index ed59be865..a0d90b66b 100644 --- a/plinth/modules/quassel/__init__.py +++ b/plinth/modules/quassel/__init__.py @@ -51,7 +51,7 @@ class QuasselApp(app_module.App): name=_('Quassel'), icon_filename='quassel', short_description=_('IRC Client'), description=_description, manual_page='Quassel', - clients=manifest.clients) + clients=manifest.clients, tags=manifest.tags) self.add(info) menu_item = menu.Menu('menu-quassel', info.name, diff --git a/plinth/modules/quassel/manifest.py b/plinth/modules/quassel/manifest.py index 2deb7f047..63aa96256 100644 --- a/plinth/modules/quassel/manifest.py +++ b/plinth/modules/quassel/manifest.py @@ -50,3 +50,5 @@ backup = { }, 'services': ['quasselcore'], } + +tags = [_('Chat Room'), _('IRC'), _('Client')] diff --git a/plinth/modules/radicale/__init__.py b/plinth/modules/radicale/__init__.py index 70c7cdf2c..acc046597 100644 --- a/plinth/modules/radicale/__init__.py +++ b/plinth/modules/radicale/__init__.py @@ -54,7 +54,8 @@ class RadicaleApp(app_module.App): short_description=_('Calendar and Addressbook'), description=_description, manual_page='Radicale', - clients=manifest.clients) + clients=manifest.clients, + tags=manifest.tags) self.add(info) menu_item = menu.Menu('menu-radicale', info.name, diff --git a/plinth/modules/radicale/manifest.py b/plinth/modules/radicale/manifest.py index 632ab1058..b27bc7523 100644 --- a/plinth/modules/radicale/manifest.py +++ b/plinth/modules/radicale/manifest.py @@ -87,3 +87,11 @@ backup = { }, 'services': ['uwsgi'] } + +tags = [ + _('Calendar'), + _('Contacts'), + _('Synchronization'), + _('CalDAV'), + _('CardDAV') +] diff --git a/plinth/modules/roundcube/__init__.py b/plinth/modules/roundcube/__init__.py index 0268208ff..25fe745e3 100644 --- a/plinth/modules/roundcube/__init__.py +++ b/plinth/modules/roundcube/__init__.py @@ -51,7 +51,8 @@ class RoundcubeApp(app_module.App): short_description=_('Email Client'), description=_description, manual_page='Roundcube', - clients=manifest.clients) + clients=manifest.clients, + tags=manifest.tags) self.add(info) menu_item = menu.Menu('menu-roundcube', info.name, diff --git a/plinth/modules/roundcube/manifest.py b/plinth/modules/roundcube/manifest.py index 314d6740c..919f53bde 100644 --- a/plinth/modules/roundcube/manifest.py +++ b/plinth/modules/roundcube/manifest.py @@ -11,8 +11,12 @@ clients = [{ }] backup = { - 'data': { - 'files': ['/etc/roundcube/freedombox-config.php', - '/var/lib/dbconfig-common/sqlite3/roundcube/roundcube'] - } + 'data': { + 'files': [ + '/etc/roundcube/freedombox-config.php', + '/var/lib/dbconfig-common/sqlite3/roundcube/roundcube' + ] + } } + +tags = [_('Email'), _('Contacts'), _('Client')] diff --git a/plinth/modules/rssbridge/__init__.py b/plinth/modules/rssbridge/__init__.py index 7126b5ac3..13bf1cca7 100644 --- a/plinth/modules/rssbridge/__init__.py +++ b/plinth/modules/rssbridge/__init__.py @@ -50,7 +50,7 @@ class RSSBridgeApp(app_module.App): short_description=_('RSS Feed Generator'), description=_description, manual_page='RSSBridge', donation_url=None, - clients=manifest.clients) + clients=manifest.clients, tags=manifest.tags) self.add(info) menu_item = menu.Menu('menu-rssbridge', info.name, diff --git a/plinth/modules/rssbridge/manifest.py b/plinth/modules/rssbridge/manifest.py index 0048bcd43..cfe398c9f 100644 --- a/plinth/modules/rssbridge/manifest.py +++ b/plinth/modules/rssbridge/manifest.py @@ -1,7 +1,6 @@ # SPDX-License-Identifier: AGPL-3.0-or-later from django.utils.translation import gettext_lazy as _ - """ Application manifest for RSS-Bridge. """ @@ -14,8 +13,6 @@ clients = [{ }] }] -backup = { - 'data': { - 'files': ['/etc/rss-bridge/is_public'] - } -} +backup = {'data': {'files': ['/etc/rss-bridge/is_public']}} + +tags = [_('Feed Generator'), _('News'), _('RSS'), _('ATOM')] diff --git a/plinth/modules/samba/__init__.py b/plinth/modules/samba/__init__.py index a433db765..80747f700 100644 --- a/plinth/modules/samba/__init__.py +++ b/plinth/modules/samba/__init__.py @@ -53,7 +53,7 @@ class SambaApp(app_module.App): app_id=self.app_id, version=self._version, name=_('Samba'), icon_filename='samba', short_description=_('Network File Storage'), manual_page='Samba', description=_description, - clients=manifest.clients, + clients=manifest.clients, tags=manifest.tags, donation_url='https://www.samba.org/samba/donations.html') self.add(info) diff --git a/plinth/modules/samba/manifest.py b/plinth/modules/samba/manifest.py index 821a48ece..4195c3753 100644 --- a/plinth/modules/samba/manifest.py +++ b/plinth/modules/samba/manifest.py @@ -84,3 +84,5 @@ clients = [{ }] backup: dict = {} + +tags = [_('File Sharing'), _('Local Network')] diff --git a/plinth/modules/searx/__init__.py b/plinth/modules/searx/__init__.py index f81e3375f..8178c51b3 100644 --- a/plinth/modules/searx/__init__.py +++ b/plinth/modules/searx/__init__.py @@ -41,7 +41,7 @@ class SearxApp(app_module.App): app_id=self.app_id, version=self._version, name=_('Searx'), icon_filename='searx', short_description=_('Web Search'), description=_description, manual_page='Searx', - clients=manifest.clients, + clients=manifest.clients, tags=manifest.tags, donation_url='https://searx.me/static/donate.html') self.add(info) diff --git a/plinth/modules/searx/manifest.py b/plinth/modules/searx/manifest.py index d8db2f2df..5643f9354 100644 --- a/plinth/modules/searx/manifest.py +++ b/plinth/modules/searx/manifest.py @@ -13,3 +13,5 @@ clients = [{ PUBLIC_ACCESS_SETTING_FILE = '/etc/searx/allow_public_access' backup = {'config': {'files': [PUBLIC_ACCESS_SETTING_FILE]}} + +tags = [_('Web Search'), _('Metasearch Engine')] diff --git a/plinth/modules/shaarli/__init__.py b/plinth/modules/shaarli/__init__.py index 4559f2613..14f322e94 100644 --- a/plinth/modules/shaarli/__init__.py +++ b/plinth/modules/shaarli/__init__.py @@ -36,7 +36,7 @@ class ShaarliApp(app_module.App): name=_('Shaarli'), icon_filename='shaarli', short_description=_('Bookmarks'), description=_description, manual_page='Shaarli', - clients=manifest.clients) + clients=manifest.clients, tags=manifest.tags) self.add(info) menu_item = menu.Menu('menu-shaarli', info.name, diff --git a/plinth/modules/shaarli/manifest.py b/plinth/modules/shaarli/manifest.py index 152413363..ae4a70823 100644 --- a/plinth/modules/shaarli/manifest.py +++ b/plinth/modules/shaarli/manifest.py @@ -1,6 +1,6 @@ # SPDX-License-Identifier: AGPL-3.0-or-later """ -Application manifest for bind. +Application manifest for Shaarli. """ from django.utils.translation import gettext_lazy as _ @@ -30,3 +30,5 @@ clients = [{ }] backup = {'data': {'directories': ['/var/lib/shaarli/data']}} + +tags = [_('Bookmarks'), _('Link Blog'), _('Single User')] diff --git a/plinth/modules/shadowsocks/__init__.py b/plinth/modules/shadowsocks/__init__.py index 57f26d4cb..7b1fd4179 100644 --- a/plinth/modules/shadowsocks/__init__.py +++ b/plinth/modules/shadowsocks/__init__.py @@ -51,7 +51,8 @@ class ShadowsocksApp(app_module.App): icon_filename='shadowsocks', short_description=_('Bypass Censorship'), description=_description, - manual_page='Shadowsocks') + manual_page='Shadowsocks', + tags=manifest.tags) self.add(info) menu_item = menu.Menu('menu-shadowsocks', info.name, diff --git a/plinth/modules/shadowsocks/manifest.py b/plinth/modules/shadowsocks/manifest.py index 52d8f00e6..5715b73c1 100644 --- a/plinth/modules/shadowsocks/manifest.py +++ b/plinth/modules/shadowsocks/manifest.py @@ -3,6 +3,8 @@ Application manifest for Shadowsocks Client. """ +from django.utils.translation import gettext_lazy as _ + backup = { 'secrets': { 'files': [ @@ -11,3 +13,11 @@ backup = { }, 'services': ['shadowsocks-libev-local@freedombox'] } + +tags = [ + _('Proxy'), + _('Client'), + _('SOCKS5'), + _('Censorship Resistance'), + _('Shadowsocks') +] diff --git a/plinth/modules/shadowsocksserver/__init__.py b/plinth/modules/shadowsocksserver/__init__.py index f574ca4e1..b78302d7d 100644 --- a/plinth/modules/shadowsocksserver/__init__.py +++ b/plinth/modules/shadowsocksserver/__init__.py @@ -47,7 +47,8 @@ class ShadowsocksServerApp(app_module.App): app_id=self.app_id, version=self._version, name=_('Shadowsocks Server'), icon_filename='shadowsocks', short_description=_('Help Others Bypass Censorship'), - description=_description, manual_page='Shadowsocks') + description=_description, manual_page='Shadowsocks', + tags=manifest.tags) self.add(info) menu_item = menu.Menu('menu-shadowsocks-server', info.name, diff --git a/plinth/modules/shadowsocksserver/manifest.py b/plinth/modules/shadowsocksserver/manifest.py index c18b96fc4..0f34ea395 100644 --- a/plinth/modules/shadowsocksserver/manifest.py +++ b/plinth/modules/shadowsocksserver/manifest.py @@ -3,6 +3,8 @@ Application manifest for Shadowsocks Server. """ +from django.utils.translation import gettext_lazy as _ + backup = { 'secrets': { 'files': [ @@ -11,3 +13,5 @@ backup = { }, 'services': ['shadowsocks-libev-server@fbxserver'] } + +tags = [_('Proxy'), _('SOCKS5'), _('Censorship Resistance'), _('Shadowsocks')] diff --git a/plinth/modules/sharing/__init__.py b/plinth/modules/sharing/__init__.py index e6c1e4c5d..8650a899e 100644 --- a/plinth/modules/sharing/__init__.py +++ b/plinth/modules/sharing/__init__.py @@ -32,7 +32,8 @@ class SharingApp(app_module.App): super().__init__() info = app_module.Info(app_id=self.app_id, version=self._version, name=_('Sharing'), icon_filename='sharing', - manual_page='Sharing', description=_description) + manual_page='Sharing', description=_description, + tags=manifest.tags) self.add(info) menu_item = menu.Menu('menu-sharing', info.name, None, diff --git a/plinth/modules/sharing/manifest.py b/plinth/modules/sharing/manifest.py index 2da78201d..f7b839a21 100644 --- a/plinth/modules/sharing/manifest.py +++ b/plinth/modules/sharing/manifest.py @@ -3,6 +3,8 @@ Application manifest for sharing. """ +from django.utils.translation import gettext_lazy as _ + backup = { 'config': { 'files': ['/etc/apache2/conf-available/sharing-freedombox.conf'] @@ -13,3 +15,5 @@ backup = { 'name': 'sharing-freedombox' }] } + +tags = [_('File Sharing')] diff --git a/plinth/modules/syncthing/__init__.py b/plinth/modules/syncthing/__init__.py index 4c11317a6..91db0559a 100644 --- a/plinth/modules/syncthing/__init__.py +++ b/plinth/modules/syncthing/__init__.py @@ -61,7 +61,8 @@ class SyncthingApp(app_module.App): description=_description, manual_page='Syncthing', clients=manifest.clients, - donation_url='https://syncthing.net/donations/') + donation_url='https://syncthing.net/donations/', + tags=manifest.tags) self.add(info) menu_item = menu.Menu('menu-syncthing', info.name, diff --git a/plinth/modules/syncthing/manifest.py b/plinth/modules/syncthing/manifest.py index 82fab9d6c..2225a1a69 100644 --- a/plinth/modules/syncthing/manifest.py +++ b/plinth/modules/syncthing/manifest.py @@ -54,3 +54,5 @@ backup = { }, 'services': ['syncthing@syncthing'] } + +tags = [_('Synchronization'), _('File Sharing'), _('Cloud Storage')] diff --git a/plinth/modules/tiddlywiki/__init__.py b/plinth/modules/tiddlywiki/__init__.py index dd7d673ed..012064beb 100644 --- a/plinth/modules/tiddlywiki/__init__.py +++ b/plinth/modules/tiddlywiki/__init__.py @@ -66,7 +66,8 @@ class TiddlyWikiApp(app_module.App): short_description=_('Non-linear Notebooks'), description=_description, manual_page='TiddlyWiki', - clients=manifest.clients) + clients=manifest.clients, + tags=manifest.tags) self.add(info) menu_item = menu.Menu('menu-tiddlywiki', info.name, diff --git a/plinth/modules/tiddlywiki/manifest.py b/plinth/modules/tiddlywiki/manifest.py index 9b9f4d29f..d7da627f0 100644 --- a/plinth/modules/tiddlywiki/manifest.py +++ b/plinth/modules/tiddlywiki/manifest.py @@ -14,3 +14,14 @@ clients = [{ }] backup = {'data': {'directories': [str(wiki_dir)]}} + +tags = [ + _('Wiki'), + _('Note Taking'), + _('Website'), + _('Journal'), + _('Digital Garden'), + _('Zettelkasten'), + _('Quine'), + _('non-Debian') +] diff --git a/plinth/modules/tor/__init__.py b/plinth/modules/tor/__init__.py index d603aab2b..1aaafa8c7 100644 --- a/plinth/modules/tor/__init__.py +++ b/plinth/modules/tor/__init__.py @@ -62,7 +62,7 @@ class TorApp(app_module.App): ], name=_('Tor'), icon_filename='tor', short_description=_('Anonymity Network'), description=_description, manual_page='Tor', - clients=manifest.clients, + clients=manifest.clients, tags=manifest.tags, donation_url='https://donate.torproject.org/') self.add(info) diff --git a/plinth/modules/tor/manifest.py b/plinth/modules/tor/manifest.py index b2e0aa1bd..3d3eafe96 100644 --- a/plinth/modules/tor/manifest.py +++ b/plinth/modules/tor/manifest.py @@ -52,3 +52,10 @@ backup = { }, 'services': ['tor@plinth'] } + +tags = [ + _('Relay'), + _('Anonymity Network'), + _('Censorship Resistance'), + _('Tor') +] diff --git a/plinth/modules/torproxy/__init__.py b/plinth/modules/torproxy/__init__.py index cd38b8c17..191dd30de 100644 --- a/plinth/modules/torproxy/__init__.py +++ b/plinth/modules/torproxy/__init__.py @@ -57,7 +57,7 @@ class TorProxyApp(app_module.App): short_description=_('Anonymity Network'), description=_description, manual_page='TorProxy', - clients=manifest.clients, + clients=manifest.clients, tags=manifest.tags, donation_url='https://donate.torproject.org/') self.add(info) diff --git a/plinth/modules/torproxy/manifest.py b/plinth/modules/torproxy/manifest.py index a5a2fb777..6378e8876 100644 --- a/plinth/modules/torproxy/manifest.py +++ b/plinth/modules/torproxy/manifest.py @@ -50,3 +50,11 @@ backup = { }, 'services': ['tor@fbxproxy'] } + +tags = [ + _('Proxy'), + _('SOCKS5'), + _('Anonymity Network'), + _('Censorship Resistance'), + _('Tor') +] diff --git a/plinth/modules/transmission/__init__.py b/plinth/modules/transmission/__init__.py index 4ad655571..830e4e758 100644 --- a/plinth/modules/transmission/__init__.py +++ b/plinth/modules/transmission/__init__.py @@ -76,7 +76,8 @@ class TransmissionApp(app_module.App): short_description=_('BitTorrent Web Client'), description=_description, manual_page='Transmission', clients=manifest.clients, - donation_url='https://transmissionbt.com/donate/') + donation_url='https://transmissionbt.com/donate/', + tags=manifest.tags) self.add(info) menu_item = menu.Menu('menu-transmission', info.name, diff --git a/plinth/modules/transmission/manifest.py b/plinth/modules/transmission/manifest.py index ed7023d47..b99e3c49c 100644 --- a/plinth/modules/transmission/manifest.py +++ b/plinth/modules/transmission/manifest.py @@ -35,3 +35,5 @@ backup = { }, 'services': ['transmission-daemon'] } + +tags = [_('File Sharing'), _('BitTorrent'), _('Client'), _('P2P')] diff --git a/plinth/modules/ttrss/__init__.py b/plinth/modules/ttrss/__init__.py index 945f2bb6b..acaddf54d 100644 --- a/plinth/modules/ttrss/__init__.py +++ b/plinth/modules/ttrss/__init__.py @@ -51,7 +51,7 @@ class TTRSSApp(app_module.App): short_description=_('News Feed Reader'), description=_description, manual_page='TinyTinyRSS', - clients=manifest.clients, + clients=manifest.clients, tags=manifest.tags, donation_url='https://www.patreon.com/cthulhoo') self.add(info) diff --git a/plinth/modules/ttrss/manifest.py b/plinth/modules/ttrss/manifest.py index 46aa863a6..2510c0d3d 100644 --- a/plinth/modules/ttrss/manifest.py +++ b/plinth/modules/ttrss/manifest.py @@ -51,3 +51,5 @@ backup = { }, 'services': ['tt-rss'] } + +tags = [_('Feed Reader'), _('News'), _('RSS'), _('ATOM')] diff --git a/plinth/modules/wireguard/__init__.py b/plinth/modules/wireguard/__init__.py index 043db2aba..8b072a817 100644 --- a/plinth/modules/wireguard/__init__.py +++ b/plinth/modules/wireguard/__init__.py @@ -48,7 +48,7 @@ class WireguardApp(app_module.App): icon_filename='wireguard', short_description=_('Virtual Private Network'), description=_description, manual_page='WireGuard', - clients=manifest.clients, + clients=manifest.clients, tags=manifest.tags, donation_url='https://www.wireguard.com/donations/') self.add(info) diff --git a/plinth/modules/wireguard/manifest.py b/plinth/modules/wireguard/manifest.py index 6c1be52b8..274783aa3 100644 --- a/plinth/modules/wireguard/manifest.py +++ b/plinth/modules/wireguard/manifest.py @@ -41,3 +41,5 @@ clients = [{ 'url': 'https://apps.apple.com/us/app/wireguard/id1441195209' }] }] + +tags = [_('VPN'), _('Anonymity'), _('Remote Access'), _('P2P')] diff --git a/plinth/modules/wordpress/__init__.py b/plinth/modules/wordpress/__init__.py index 91d24de24..35ed78b97 100644 --- a/plinth/modules/wordpress/__init__.py +++ b/plinth/modules/wordpress/__init__.py @@ -53,7 +53,7 @@ class WordPressApp(app_module.App): app_id=self.app_id, version=self._version, name=_('WordPress'), icon_filename='wordpress', short_description=_('Website and Blog'), description=_description, manual_page='WordPress', - clients=manifest.clients, + clients=manifest.clients, tags=manifest.tags, donation_url='https://wordpressfoundation.org/donate/') self.add(info) diff --git a/plinth/modules/wordpress/manifest.py b/plinth/modules/wordpress/manifest.py index 51199ce53..1928f26ad 100644 --- a/plinth/modules/wordpress/manifest.py +++ b/plinth/modules/wordpress/manifest.py @@ -12,11 +12,15 @@ clients = [{ backup = { 'data': { - 'files': ['/var/lib/plinth/backups-data/wordpress-database.sql', - '/etc/wordpress/is_public'], + 'files': [ + '/var/lib/plinth/backups-data/wordpress-database.sql', + '/etc/wordpress/is_public' + ], 'directories': ['/var/lib/wordpress/'] }, 'secrets': { 'directories': ['/etc/wordpress/'] }, } + +tags = [_('Website'), _('Blog')] diff --git a/plinth/modules/zoph/manifest.py b/plinth/modules/zoph/manifest.py index a9a0564f3..81680b453 100644 --- a/plinth/modules/zoph/manifest.py +++ b/plinth/modules/zoph/manifest.py @@ -22,3 +22,5 @@ backup = { ], } } + +tags = [_('Image Viewer'), _('Photo'), _('Library')] diff --git a/plinth/templates/app-header.html b/plinth/templates/app-header.html index 154a0681f..73acd4e6b 100644 --- a/plinth/templates/app-header.html +++ b/plinth/templates/app-header.html @@ -51,6 +51,17 @@ {% endfor %} {% endblock %} + {% if app_info.tags %} +
+ {% for tag in app_info.tags %} + + {% trans tag %} + + {% endfor %} +
+ {% endif %} + {% if app_info.manual_page %}

diff --git a/plinth/templates/apps.html b/plinth/templates/apps.html index 99a69b797..1d2cabf73 100644 --- a/plinth/templates/apps.html +++ b/plinth/templates/apps.html @@ -7,3 +7,40 @@ {% load i18n %} {% block body_class %}apps-page{% endblock %} + +{% block tags %} + {% if tags %} +

+ {% endif %} +{% endblock %} + +{% block page_js %} + +{% endblock %} diff --git a/plinth/templates/cards.html b/plinth/templates/cards.html index f5ba47d82..64ac96354 100644 --- a/plinth/templates/cards.html +++ b/plinth/templates/cards.html @@ -15,10 +15,12 @@ + {% block tags %}{% endblock %} +
- {% for item in submenu.sorted_items %} + {% for item in menu_items %} {% if not show_disabled or item.is_enabled %} {% if advanced_mode or not item.advanced %} {% include "card.html" %} @@ -34,7 +36,7 @@
{% trans "Disabled" %}
- {% for item in submenu.sorted_items %} + {% for item in menu_items %} {% if not item.is_enabled %} {% if advanced_mode or not item.advanced %} {% include "card.html" %} diff --git a/plinth/tests/tags/__init__.py b/plinth/tests/tags/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/plinth/tests/tags/test_functional.py b/plinth/tests/tags/test_functional.py new file mode 100644 index 000000000..85c8bd6f1 --- /dev/null +++ b/plinth/tests/tags/test_functional.py @@ -0,0 +1,60 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later +""" +Functional, browser based tests for transmission app. +""" + +import pytest + +from selenium.webdriver.common.keys import Keys +from plinth.tests import functional + +pytestmark = [pytest.mark.tags] + + +def _is_app_listed(session_browser, app): + """Assert that the specified app is listed on the page.""" + app_links = session_browser.links.find_by_href(f'/plinth/apps/{app}/') + assert len(app_links) == 1 + + +@pytest.fixture(autouse=True) +def fixture_bittorrent_tag(session_browser): + """Click on the BitTorrent tag.""" + bittorrent_tag = '/plinth/apps/?tag=BitTorrent' + functional.login(session_browser) + functional.nav_to_module(session_browser, 'transmission') + with functional.wait_for_page_update(session_browser, timeout=10, + expected_url=bittorrent_tag): + session_browser.links.find_by_href(bittorrent_tag).click() + + +def test_bittorrent_tag(session_browser): + """Test that the BitTorrent tag lists Deluge and Transmission.""" + _is_app_listed(session_browser, 'deluge') + _is_app_listed(session_browser, 'transmission') + + +def test_search_for_tag(session_browser): + """Test that searching for a tag returns the expected apps.""" + search_input = session_browser.driver.find_element_by_id('add-tag-input') + with functional.wait_for_page_update( + session_browser, timeout=10, + expected_url='/plinth/apps/?tag=BitTorrent&tag=File%20Sharing'): + search_input.click() + search_input.send_keys('file') + search_input.send_keys(Keys.ENTER) + for app in ['deluge', 'nextcloud', 'sharing', 'syncthing', 'transmission']: + _is_app_listed(session_browser, app) + + +def test_click_on_tag(session_browser): + """Test that clicking on a tag lists the expected apps.""" + search_input = session_browser.driver.find_element_by_id('add-tag-input') + with functional.wait_for_page_update( + session_browser, timeout=10, + expected_url='/plinth/apps/?tag=BitTorrent&tag=Cloud%20Storage'): + search_input.click() + session_browser.find_by_css( + ".dropdown-item[data-value='Cloud Storage']").click() + for app in ['deluge', 'nextcloud', 'syncthing', 'transmission']: + _is_app_listed(session_browser, app) diff --git a/plinth/views.py b/plinth/views.py index be07617a4..ddd4eb312 100644 --- a/plinth/views.py +++ b/plinth/views.py @@ -23,6 +23,7 @@ from django.views.generic.edit import FormView from stronghold.decorators import public from plinth import app as app_module +from plinth import menu from plinth.daemon import app_is_running from plinth.modules.config import get_advanced_mode from plinth.modules.firewall.components import get_port_forwarding_info @@ -120,13 +121,61 @@ def index(request): class AppsIndexView(TemplateView): - """View for apps index""" + """View for apps index. + + This view supports filtering apps by one or more tags. If no tags are + provided, it will show all the apps. If one or more tags are provided, + it will select apps matching any of the provided tags. + """ template_name = 'apps.html' + @staticmethod + def _pick_menu_items(menu_items, selected_tags): + """Return a sorted list of menu items filtered by tags.""" + + def _mismatch_map(menu_item) -> list[bool]: + """Return a list of mismatches for selected tags. + + A mismatch is when a selected tag is *not* present in the list of + tags for menu item. + """ + menu_tags = set(menu_item.app.info.tags) + return [tag not in menu_tags for tag in selected_tags] + + def _sort_key(menu_item): + """Returns a comparable tuple to sort menu items. + + Sort items by tag match count first, then by the order of matched + tags in user specified order, then by the order set by menu item, + and then by the name of the menu item in current locale (by + configured collation order). + """ + return (_mismatch_map(menu_item).count(True), + _mismatch_map(menu_item), menu_item.order, + menu_item.name.lower()) + + # Filter out menu items that don't match any of the selected tags. If + # no tags are selected, return all menu items. Otherwise, return all + # menu items that have at least one matching tag. + filtered_menu_items = [ + menu_item for menu_item in menu_items + if (not selected_tags) or (not all(_mismatch_map(menu_item))) + ] + + return sorted(filtered_menu_items, key=_sort_key) + def get_context_data(self, *args, **kwargs): context = super().get_context_data(*args, **kwargs) context['show_disabled'] = True context['advanced_mode'] = get_advanced_mode() + + tags = self.request.GET.getlist('tag', []) + menu_items = menu.main_menu.active_item(self.request).items + + context['tags'] = tags + context['all_tags'] = app_module.Info.list_tags() + context['menu_items'] = self._pick_menu_items(menu_items, tags) + return context @@ -364,8 +413,9 @@ class SetupView(TemplateView): context['setup_state'] = setup_state context['operations'] = operation.manager.filter(app.app_id) context['show_rerun_setup'] = False - context['show_uninstall'] = (not app.info.is_essential and setup_state - != app_module.App.SetupState.NEEDS_SETUP) + context['show_uninstall'] = ( + not app.info.is_essential + and setup_state != app_module.App.SetupState.NEEDS_SETUP) # Perform expensive operation only if needed. if not context['operations']: diff --git a/static/tags.js b/static/tags.js new file mode 100644 index 000000000..822ec9b06 --- /dev/null +++ b/static/tags.js @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +/** + * @licstart The following is the entire license notice for the JavaScript + * code in this page. + * + * This file is part of FreedomBox. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * @licend The above is the entire license notice for the JavaScript code + * in this page. + */ + +/** + * Update the URL path and history based on the selected tags. + * + * If no tags are selected, redirects to the base apps path. Otherwise, + * constructs a new URL with query parameters for each tag and updates + * the browser history. + * + * @param {string[]} tags - An array of selected tag names. + */ +function updatePath(tags) { + const appsPath = window.location.pathname; + if (tags.length === 0) { + this.location.assign(appsPath); + } else { + let queryParams = tags.map(tag => `tag=${tag}`).join('&'); + let newPath = `${appsPath}?${queryParams}`; + this.history.pushState({ tags: tags }, '', newPath); + this.location.assign(newPath); + } +} + +/** + * Get a list of tags currently displayed, excluding a specific tag if provided. + * + * Iterates through the tag badges in the UI, extracts their text content, + * and returns an array of tag names. + * + * @param {string} [tagToRemove] - The name of the tag to exclude. + * @returns {string[]} An array of tag names currently displayed. + */ +function getTags(tagToRemove) { + const tagBadges = document.querySelectorAll('#selected-tags .tag-badge'); + return Array.from(tagBadges) + .map(tag => tag.dataset.tag) + .filter(tagName => tagName !== tagToRemove); +} + +/** + * Filter and highlight the best matching tag based on the search term. + * + * This function updates the visibility and highlighting of dropdown items + * to match the user's input in the search box. It determines the best + * matching item and marks it as active. + * + * @param {KeyboardEvent} event - The keyboard event that triggered the search. + */ +function findMatchingTag(addTagInput, dropdownItems) { + const searchTerm = addTagInput.value.toLowerCase().trim(); + + // Remove highlighting from all items + dropdownItems.forEach(item => item.classList.remove('active')); + + let bestMatch = null; + dropdownItems.forEach(item => { + const text = item.textContent.toLowerCase(); + if (text.includes(searchTerm)) { + item.style.display = 'block'; + function matchesEarly () { + return text.indexOf(searchTerm) < bestMatch.textContent.toLowerCase().indexOf(searchTerm); + }; + if (bestMatch === null || matchesEarly()) { + bestMatch = item; + } + } else { + item.style.display = 'none'; + } + }); + + // Highlight the best match + if (bestMatch) { + bestMatch.classList.add('active'); + } +} + +/** + * Manage tag-related UI interactions for filtering and displaying apps. + * + * This script manages the user interface for filtering apps based on + * selected tags. It provides functionality for adding and removing tags, + * updating the URL based on selected tags, and displaying a set of + * available tags in a searchable dropdown. + */ +document.addEventListener('DOMContentLoaded', function () { + + // Remove Tag handler. + document.querySelectorAll('.remove-tag').forEach(button => { + button.addEventListener('click', () => { + let tag = button.dataset.tag; + let tags = getTags(tag); + updatePath(tags); + }); + }); + + /** + * Searchable dropdown for selecting tags. + * + * As the user types in the input field, the dropdown list is filtered + * to show only matching items. The best matching item (first match if + * multiple match) is highlighted. Pressing Enter selects the + * highlighted item and adds it as a tag. + */ + const addTagInput = document.getElementById('add-tag-input'); + const dropdownItems = document.querySelectorAll('li.dropdown-item'); + + var timeoutId; + addTagInput.addEventListener('keyup', (event) => { + clearTimeout(timeoutId); + // Select the active tag if the user pressed Enter + if (event.key === 'Enter') { + dropdownItems.forEach(item => { + if (item.classList.contains('active')) { + item.click(); + } + }); + } + // Debounce the user input for search with 300ms delay. + timeoutId = setTimeout(findMatchingTag(addTagInput, dropdownItems), 300); + }); + + dropdownItems.forEach(item => { + item.addEventListener('click', () => { + const selectedTag = item.dataset.value; + + // Add the selected tag and update the path. + let tags = getTags(''); + tags.push(selectedTag); + updatePath(tags); + + // Reset the input field and dropdown. + addTagInput.value = ''; + dropdownItems.forEach(item => { + item.style.display = ''; + item.classList.remove('active'); + }); + }); + }); + + // Handle browser back/forward navigation. + window.addEventListener('popstate', function (event) { + if (event.state && event.state.tags) { + updatePath(event.state.tags); + } + }); +}); + diff --git a/static/themes/default/css/main.css b/static/themes/default/css/main.css index f2ff58061..bc636d527 100644 --- a/static/themes/default/css/main.css +++ b/static/themes/default/css/main.css @@ -309,7 +309,6 @@ html { } #wrapper { - min-height: 100%; position: relative; } @@ -321,6 +320,59 @@ html { margin-bottom: 1.25rem; } +/* Tag Input Container */ +.tag-input { + display: flex; + align-items: center; + border: 1px solid #ced4da; + border-radius: .25rem; + padding: .375rem .75rem; + width: 100%; + background-color: #fff; + margin-bottom: 3rem; +} + +/* Selected Tags */ +.tag-input #selected-tags { + display: flex; + flex-wrap: wrap; + margin-right: .5rem; +} + +.tag-input .tag { + background-color: #e9ecef; /* Light gray background */ + border-radius: .25rem; + padding: .25rem .5rem; + margin-right: .25rem; + margin-bottom: .25rem; + display: flex; + align-items: center; +} + +/* Remove tag button */ +.tag-badge .remove-tag { + background-color: #f8f9fa; /* Match the tag's background color */ + border: none; + padding: 0.25rem 0.5rem; + cursor: pointer; +} + +/* Adjust input field width */ +.tag-input input[type="search"] { + flex-grow: 1; + border: none; + outline: none; + box-shadow: none; + width: auto; + min-width: 3rem; +} + +/* dropdown-menu for tags is a scrollable list */ +.tag-input .dropdown-menu { + bottom: calc(-100% - 10px); + overflow-y: auto; +} + @media (min-width: 768px) { .content-container { padding: 1.5rem 3rem 3rem;