diff --git a/CHANGELOG.md b/CHANGELOG.md index 54df4785..3f50825e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,10 +11,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Improvements - AUR: - Optional dependencies are not checked by default in their installation popup. +- History panel can now me maximized, minimized and allows to copy column content. - Minor UI improvements ### AppImage support -- Search, install, uninstall applications available in [AppImageHub](https://appimage.github.io) +- Search, install, uninstall and retrieve history of applications available in [AppImageHub](https://appimage.github.io) - Adds desktop entries (menu shortcuts) for the installed applications diff --git a/bauh/gems/appimage/controller.py b/bauh/gems/appimage/controller.py index 33c95a75..a0b4a455 100644 --- a/bauh/gems/appimage/controller.py +++ b/bauh/gems/appimage/controller.py @@ -19,7 +19,9 @@ from bauh.commons.system import SystemProcess, new_subprocess, ProcessHandler, r from bauh.gems.appimage import query, INSTALLATION_PATH from bauh.gems.appimage.model import AppImage -DB_PATH = '{}/{}'.format(HOME_PATH, '.local/share/bauh/appimage/appimage.db') +DB_APPS_PATH = '{}/{}'.format(HOME_PATH, '.local/share/bauh/appimage/apps.db') +DB_RELEASES_PATH = '{}/{}'.format(HOME_PATH, '.local/share/bauh/appimage/releases.db') + DESKTOP_ENTRIES_PATH = '{}/.local/share/applications'.format(HOME_PATH) RE_DESKTOP_EXEC = re.compile(r'Exec\s*=\s*.+\n') @@ -39,18 +41,18 @@ class AppImageManager(SoftwareManager): self.logger = context.logger self.file_downloader = context.file_downloader - def _get_db_connection(self) -> sqlite3.Connection: - if os.path.exists(DB_PATH): - return sqlite3.connect(DB_PATH) + def _get_db_connection(self, db_path: str) -> sqlite3.Connection: + if os.path.exists(db_path): + return sqlite3.connect(db_path) def search(self, words: str, disk_loader: DiskCacheLoader, limit: int = -1) -> SearchResult: res = SearchResult([], [], 0) - connection = self._get_db_connection() + connection = self._get_db_connection(DB_APPS_PATH) if connection: try: cursor = connection.cursor() - cursor.execute(query.SEARCH_BY_NAME_OR_DESCRIPTION.format(words, words)) + cursor.execute(query.SEARCH_APPS_BY_NAME_OR_DESCRIPTION.format(words, words)) for l in cursor.fetchall(): app = AppImage(*l) @@ -58,7 +60,7 @@ class AppImageManager(SoftwareManager): finally: connection.close() else: - self.logger.warning('Could not get a connection from the local database at {}'.format(DB_PATH)) + self.logger.warning('Could not get a connection from the local database at {}'.format(DB_APPS_PATH)) res.total = len(res.installed) + len(res.new) return res @@ -111,8 +113,35 @@ class AppImageManager(SoftwareManager): return pkg.get_data_to_cache() def get_history(self, pkg: AppImage) -> PackageHistory: - # TODO - pass + history = [] + res = PackageHistory(pkg, history, -1) + + connection = self._get_db_connection(DB_APPS_PATH) + + if connection: + cursor = connection.cursor() + + cursor.execute(query.FIND_APP_ID_BY_NAME_AND_GITHUB.format(pkg.name.lower(), pkg.github.lower())) + app_tuple = cursor.fetchone() + + if not app_tuple: + raise Exception("Could not retrieve {} from the database {}".format(pkg, DB_APPS_PATH)) + + connection.close() + + connection = self._get_db_connection(DB_RELEASES_PATH) + cursor = connection.cursor() + + releases = cursor.execute(query.FIND_RELEASES_BY_APP_ID.format(app_tuple[0])) + + if releases: + for idx, tup in enumerate(releases): + history.append({'0_version': tup[0], '1_published_at': tup[2], '2_url_download': tup[1]}) + + if res.pkg_status_idx == -1 and pkg.version == tup[0]: + res.pkg_status_idx = idx + + return res def _find_desktop_file(self, folder: str) -> str: for r, d, files in os.walk(folder): diff --git a/bauh/gems/appimage/model.py b/bauh/gems/appimage/model.py index e70389b7..41107ee2 100644 --- a/bauh/gems/appimage/model.py +++ b/bauh/gems/appimage/model.py @@ -4,7 +4,7 @@ from bauh.api.abstract.model import SoftwarePackage from bauh.commons import resource from bauh.gems.appimage import ROOT_DIR, INSTALLATION_PATH -CACHED_ATTRS = {'name', 'description', 'version', 'url_download', 'author', 'license', 'source', 'icon_path'} +CACHED_ATTRS = {'name', 'description', 'version', 'url_download', 'author', 'license', 'source', 'icon_path', 'github'} class AppImage(SoftwarePackage): @@ -22,12 +22,14 @@ class AppImage(SoftwarePackage): self.icon_path = icon_path self.author = author + def __repr__(self): + return "{} (name={}, github={})".format(self.__class__.__name__, self.name, self.github) + def can_be_installed(self): return not self.installed and self.url_download def has_history(self): - # TODO - return False + return self.installed def has_info(self): return True diff --git a/bauh/gems/appimage/query.py b/bauh/gems/appimage/query.py index 1149aaf5..b4741694 100644 --- a/bauh/gems/appimage/query.py +++ b/bauh/gems/appimage/query.py @@ -1,5 +1,7 @@ -ATTRS = ('name', 'description', 'github', 'source', 'version', 'url_download', 'url_icon', 'license', 'author') +APP_ATTRS = ('name', 'description', 'github', 'source', 'version', 'url_download', 'url_icon', 'license', 'author') +RELEASE_ATTRS = ('version', 'url_download', 'published_at') -_SELECT_BASE = "SELECT {} FROM apps".format(','.join(ATTRS)) -SEARCH_BY_NAME_OR_DESCRIPTION = _SELECT_BASE + " WHERE lower(name) LIKE '%{}%' or lower(description) LIKE '%{}%'" +SEARCH_APPS_BY_NAME_OR_DESCRIPTION = "SELECT {} FROM apps".format(','.join(APP_ATTRS)) + " WHERE lower(name) LIKE '%{}%' or lower(description) LIKE '%{}%'" +FIND_APP_ID_BY_NAME_AND_GITHUB = "SELECT id FROM apps WHERE lower(name) = '{}' and lower(github) = '{}'" +FIND_RELEASES_BY_APP_ID = "SELECT {} FROM releases".format(','.join(RELEASE_ATTRS)) + " WHERE app_id = {} ORDER BY version desc" diff --git a/bauh/gems/appimage/resources/locale/en b/bauh/gems/appimage/resources/locale/en index adfa558e..09640abb 100644 --- a/bauh/gems/appimage/resources/locale/en +++ b/bauh/gems/appimage/resources/locale/en @@ -4,4 +4,7 @@ appimage.install.extract=Extracting the content from {} appimage.install.desktop_entry=Generating a menu shortcut appimage.uninstall.error.remove_folder=Could not remove the application installation directory {} appimage.info.url_download=File URL -appimage.info.icon_path=icon \ No newline at end of file +appimage.info.icon_path=icon +appimage.history.0_version=version +appimage.history.1_published_at=date +appimage.history.2_url_download=File URL \ No newline at end of file diff --git a/bauh/gems/appimage/resources/locale/es b/bauh/gems/appimage/resources/locale/es index 81134f43..a3f64406 100644 --- a/bauh/gems/appimage/resources/locale/es +++ b/bauh/gems/appimage/resources/locale/es @@ -4,4 +4,7 @@ appimage.install.extract=Extrayendo el contenido de {} appimage.install.desktop_entry=Creando un atajo en el menú appimage.uninstall.error.remove_folder=No se pudo eliminar el directorio de instalación de la aplicación {} appimage.info.url_download=URL del archivo -appimage.info.icon_path=icono \ No newline at end of file +appimage.info.icon_path=icono +appimage.history.0_version=versión +appimage.history.1_published_at=fecha +appimage.history.2_url_download=URL del archivo \ No newline at end of file diff --git a/bauh/gems/appimage/resources/locale/pt b/bauh/gems/appimage/resources/locale/pt index 087603e4..f411bca0 100644 --- a/bauh/gems/appimage/resources/locale/pt +++ b/bauh/gems/appimage/resources/locale/pt @@ -4,4 +4,7 @@ appimage.install.extract=Extraindo o conteúdo de {} appimage.install.desktop_entry=Criando um atalho no menu appimage.uninstall.error.remove_folder=Não foi possível remover o diretóri ode instalação do aplicativo {} appimage.info.url_download=URL do arquivo -appimage.info.icon_path=ícone \ No newline at end of file +appimage.info.icon_path=ícone +appimage.history.0_version=versão +appimage.history.1_published_at=data +appimage.history.2_url_download=URL do arquivo \ No newline at end of file diff --git a/bauh/view/qt/history.py b/bauh/view/qt/history.py index 89d51869..7e441498 100644 --- a/bauh/view/qt/history.py +++ b/bauh/view/qt/history.py @@ -4,16 +4,18 @@ from functools import reduce from PyQt5.QtCore import Qt from PyQt5.QtGui import QColor from PyQt5.QtWidgets import QDialog, QVBoxLayout, QTableWidget, QTableWidgetItem, QHeaderView + from bauh.api.abstract.cache import MemoryCache from bauh.api.abstract.model import PackageHistory class HistoryDialog(QDialog): - def __init__(self, history: PackageHistory, icon_cache: MemoryCache, locale_keys: dict): + def __init__(self, history: PackageHistory, icon_cache: MemoryCache, i18n: dict): super(HistoryDialog, self).__init__() + self.setWindowFlags(self.windowFlags() | Qt.WindowSystemMenuHint | Qt.WindowMinMaxButtonsHint) - self.setWindowTitle('{} - {} ({})'.format(locale_keys['popup.history.title'], history.pkg.name, history.pkg.get_type())) + self.setWindowTitle('{} - {} ({})'.format(i18n['popup.history.title'], history.pkg.name, history.pkg.get_type())) layout = QVBoxLayout() self.setLayout(layout) @@ -26,20 +28,19 @@ class HistoryDialog(QDialog): table_history.setColumnCount(len(history.history[0])) table_history.setRowCount(len(history.history)) - table_history.setHorizontalHeaderLabels([locale_keys.get(history.pkg.get_type() + '.history.' + key, key).capitalize() for key in sorted(history.history[0].keys())]) + table_history.setHorizontalHeaderLabels([i18n.get(history.pkg.get_type().lower() + '.history.' + key, i18n.get(key, key)).capitalize() for key in sorted(history.history[0].keys())]) - for row, commit in enumerate(history.history): + for row, data in enumerate(history.history): current_status = history.pkg_status_idx == row - for col, key in enumerate(sorted(commit.keys())): + for col, key in enumerate(sorted(data.keys())): item = QTableWidgetItem() - item.setText(str(commit[key])) - item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) + item.setText(str(data[key])) if current_status: item.setBackground(QColor('#ffbf00' if row != 0 else '#32CD32')) - tip = '{}. {}.'.format(locale_keys['popup.history.selected.tooltip'], locale_keys['version.{}'.format('updated'if row == 0 else 'outdated')].capitalize()) + tip = '{}. {}.'.format(i18n['popup.history.selected.tooltip'], i18n['version.{}'.format('updated'if row == 0 else 'outdated')].capitalize()) item.setToolTip(tip)