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;