mirror of
https://github.com/spalencsar/bearhub.git
synced 2026-07-08 12:04:16 +02:00
AUR bearhub: fix stale source cache collision (pkgrel=10)
Use tag-specific tarball name bearhub-0.10.7-bearhub.6.tar.gz so makepkg and AUR helpers do not reuse an outdated bearhub-0.10.7.tar.gz from cache. Ignore makepkg artifacts under packaging/aur/.
This commit is contained in:
@@ -1,113 +0,0 @@
|
||||
from glob import glob
|
||||
|
||||
from PyQt5.QtGui import QIcon
|
||||
from PyQt5.QtWidgets import QVBoxLayout, QDialog, QLabel, QWidget, QHBoxLayout, QSizePolicy, QApplication
|
||||
|
||||
from bauh import __version__, ROOT_DIR
|
||||
from bauh.context import generate_i18n
|
||||
from bauh.view.util import resource
|
||||
|
||||
DISPLAY_NAME = 'Bearhub'
|
||||
PROJECT_URL = 'https://github.com/spalencsar/bearhub'
|
||||
LICENSE_URL = 'https://raw.githubusercontent.com/spalencsar/bearhub/main/LICENSE'
|
||||
|
||||
|
||||
class AboutDialog(QDialog):
|
||||
|
||||
def __init__(self, app_config: dict):
|
||||
super(AboutDialog, self).__init__()
|
||||
i18n = generate_i18n(app_config, resource.get_path('locale/about'))
|
||||
self.setWindowTitle('{} ({})'.format(i18n['about.title'].capitalize(), DISPLAY_NAME))
|
||||
layout = QVBoxLayout()
|
||||
self.setLayout(layout)
|
||||
|
||||
logo_container = QWidget()
|
||||
logo_container.setObjectName('logo_container')
|
||||
logo_container.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
|
||||
logo_container.setLayout(QHBoxLayout())
|
||||
|
||||
label_logo = QLabel()
|
||||
label_logo.setObjectName('logo')
|
||||
|
||||
logo_container.layout().addWidget(label_logo)
|
||||
layout.addWidget(logo_container)
|
||||
|
||||
label_name = QLabel(DISPLAY_NAME)
|
||||
label_name.setObjectName('app_name')
|
||||
layout.addWidget(label_name)
|
||||
|
||||
label_version = QLabel(i18n['about.version'].lower() + ' ' + __version__)
|
||||
label_version.setObjectName('app_version')
|
||||
layout.addWidget(label_version)
|
||||
|
||||
layout.addWidget(QLabel(''))
|
||||
|
||||
line_desc = QLabel(i18n['about.info.desc'])
|
||||
line_desc.setObjectName('app_description')
|
||||
layout.addWidget(line_desc)
|
||||
|
||||
layout.addWidget(QLabel(''))
|
||||
|
||||
available_gems = [f for f in glob('{}/gems/*'.format(ROOT_DIR)) if not f.endswith('.py') and not f.endswith('__pycache__')]
|
||||
available_gems.sort()
|
||||
|
||||
gems_widget = QWidget()
|
||||
gems_widget.setLayout(QHBoxLayout())
|
||||
|
||||
gems_widget.layout().addWidget(QLabel())
|
||||
gem_logo_size = int(0.032552083 * QApplication.primaryScreen().size().height())
|
||||
|
||||
for gem_path in available_gems:
|
||||
icon = QLabel()
|
||||
icon.setObjectName('gem_logo')
|
||||
icon_path = gem_path + '/resources/img/{}.svg'.format(gem_path.split('/')[-1])
|
||||
icon.setPixmap(QIcon(icon_path).pixmap(gem_logo_size, gem_logo_size))
|
||||
gems_widget.layout().addWidget(icon)
|
||||
|
||||
gems_widget.layout().addWidget(QLabel())
|
||||
|
||||
layout.addWidget(gems_widget)
|
||||
layout.addWidget(QLabel(''))
|
||||
|
||||
label_more_info = QLabel()
|
||||
label_more_info.setObjectName('app_more_information')
|
||||
label_more_info.setText(i18n['about.info.link'] + " <a href='{url}'>{url}</a>".format(url=PROJECT_URL))
|
||||
label_more_info.setOpenExternalLinks(True)
|
||||
layout.addWidget(label_more_info)
|
||||
|
||||
label_license = QLabel()
|
||||
label_license.setObjectName('app_license')
|
||||
label_license.setText("<a href='{}'>{}</a>".format(LICENSE_URL, i18n['about.info.license']))
|
||||
label_license.setOpenExternalLinks(True)
|
||||
layout.addWidget(label_license)
|
||||
|
||||
layout.addWidget(QLabel(''))
|
||||
|
||||
label_trouble_question = QLabel(i18n['about.info.trouble.question'])
|
||||
label_trouble_question.setObjectName('app_trouble_question')
|
||||
|
||||
layout.addWidget(label_trouble_question)
|
||||
|
||||
label_trouble_answer = QLabel(i18n['about.info.trouble.answer'])
|
||||
label_trouble_answer.setObjectName('app_trouble_answer')
|
||||
|
||||
layout.addWidget(label_trouble_answer)
|
||||
|
||||
layout.addWidget(QLabel(''))
|
||||
|
||||
label_rate_question = QLabel(i18n['about.info.rate.question'])
|
||||
label_rate_question.setObjectName('app_rate_question')
|
||||
layout.addWidget(label_rate_question)
|
||||
|
||||
label_rate_answer = QLabel(i18n['about.info.rate.answer'])
|
||||
label_rate_answer.setObjectName('app_rate_answer')
|
||||
layout.addWidget(label_rate_answer)
|
||||
|
||||
layout.addWidget(QLabel(''))
|
||||
|
||||
self.adjustSize()
|
||||
self.setFixedSize(self.size())
|
||||
|
||||
def closeEvent(self, event):
|
||||
event.ignore()
|
||||
self.hide()
|
||||
@@ -1,631 +0,0 @@
|
||||
import operator
|
||||
import os
|
||||
from functools import reduce
|
||||
from logging import Logger
|
||||
from threading import Lock
|
||||
from typing import List, Optional, Dict
|
||||
|
||||
from PyQt5.QtCore import Qt, QSize
|
||||
from PyQt5.QtGui import QPixmap, QIcon, QCursor
|
||||
from PyQt5.QtWidgets import QTableWidget, QTableView, QMenu, QToolButton, QWidget, \
|
||||
QHeaderView, QLabel, QHBoxLayout, QToolBar, QSizePolicy
|
||||
|
||||
from bauh.api.abstract.cache import MemoryCache
|
||||
from bauh.api.abstract.model import PackageStatus, CustomSoftwareAction
|
||||
from bauh.api.abstract.view import MessageType
|
||||
from bauh.commons.html import strip_html, bold
|
||||
from bauh.commons.regex import RE_URL
|
||||
from bauh.view.qt.components import IconButton, QCustomMenuAction, QCustomToolbar
|
||||
from bauh.view.qt.dialog import ConfirmationDialog
|
||||
from bauh.view.qt.qt_utils import get_current_screen_geometry
|
||||
from bauh.view.qt.thread import URLFileDownloader
|
||||
from bauh.view.qt.view_model import PackageView
|
||||
from bauh.view.util.translation import I18n
|
||||
|
||||
|
||||
class UpgradeToggleButton(QToolButton):
|
||||
|
||||
def __init__(self, pkg: Optional[PackageView], root: QWidget, i18n: I18n, checked: bool = True,
|
||||
clickable: bool = True):
|
||||
super(UpgradeToggleButton, self).__init__()
|
||||
self.app_view = pkg
|
||||
self.root = root
|
||||
|
||||
self.setCursor(QCursor(Qt.PointingHandCursor))
|
||||
self.setCheckable(True)
|
||||
|
||||
if clickable:
|
||||
self.clicked.connect(self.change_state)
|
||||
|
||||
if not clickable and not checked:
|
||||
self.setProperty('enabled', 'false')
|
||||
|
||||
if not checked:
|
||||
self.click()
|
||||
|
||||
if clickable:
|
||||
self.setToolTip('{} {}'.format(i18n['manage_window.apps_table.upgrade_toggle.tooltip'],
|
||||
i18n['manage_window.apps_table.upgrade_toggle.enabled.tooltip']))
|
||||
else:
|
||||
if not checked:
|
||||
self.setEnabled(False)
|
||||
|
||||
tooltip = i18n['{}.update.disabled.tooltip'.format(pkg.model.gem_name)]
|
||||
|
||||
if tooltip:
|
||||
self.setToolTip(tooltip)
|
||||
else:
|
||||
self.setToolTip('{} {}'.format(i18n['manage_window.apps_table.upgrade_toggle.tooltip'],
|
||||
i18n['manage_window.apps_table.upgrade_toggle.disabled.tooltip']))
|
||||
else:
|
||||
self.setCheckable(False)
|
||||
|
||||
def change_state(self, not_checked: bool):
|
||||
self.app_view.update_checked = not not_checked
|
||||
self.setProperty('toggled', str(self.app_view.update_checked).lower())
|
||||
self.root.update_bt_upgrade()
|
||||
self.style().unpolish(self)
|
||||
self.style().polish(self)
|
||||
|
||||
|
||||
class PackagesTable(QTableWidget):
|
||||
COL_NUMBER = 9
|
||||
DEFAULT_ICON_SIZE = QSize(16, 16)
|
||||
|
||||
def __init__(self, parent: QWidget, icon_cache: MemoryCache, download_icons: bool, logger: Logger):
|
||||
super(PackagesTable, self).__init__()
|
||||
self.setObjectName('table_packages')
|
||||
self.setParent(parent)
|
||||
self.window = parent
|
||||
self.download_icons = download_icons
|
||||
self.logger = logger
|
||||
self.setColumnCount(self.COL_NUMBER)
|
||||
self.setFocusPolicy(Qt.NoFocus)
|
||||
self.setShowGrid(False)
|
||||
self.verticalHeader().setVisible(False)
|
||||
self.horizontalHeader().setVisible(False)
|
||||
self.horizontalHeader().setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
|
||||
self.setSelectionBehavior(QTableView.SelectRows)
|
||||
self.setHorizontalHeaderLabels(('' for _ in range(self.columnCount())))
|
||||
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||
self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
|
||||
self.horizontalScrollBar().setCursor(QCursor(Qt.PointingHandCursor))
|
||||
self.verticalScrollBar().setCursor(QCursor(Qt.PointingHandCursor))
|
||||
|
||||
self.file_downloader: Optional[URLFileDownloader] = None
|
||||
|
||||
self.icon_cache = icon_cache
|
||||
self.lock_async_data = Lock()
|
||||
self.setRowHeight(80, 80)
|
||||
self.cache_type_icon = {}
|
||||
self.cache_default_icon: Dict[str, QIcon] = dict()
|
||||
self.i18n = self.window.i18n
|
||||
|
||||
def has_any_settings(self, pkg: PackageView):
|
||||
return pkg.model.has_history() or \
|
||||
pkg.model.can_be_downgraded() or \
|
||||
pkg.model.supports_ignored_updates() or \
|
||||
bool(pkg.model.get_custom_actions())
|
||||
|
||||
def show_pkg_actions(self, pkg: PackageView):
|
||||
menu_row = QMenu()
|
||||
menu_row.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred)
|
||||
menu_row.setObjectName('app_actions')
|
||||
menu_row.setCursor(QCursor(Qt.PointingHandCursor))
|
||||
|
||||
if pkg.model.installed:
|
||||
|
||||
if pkg.model.has_history():
|
||||
def show_history():
|
||||
self.window.begin_show_history(pkg)
|
||||
|
||||
menu_row.addAction(QCustomMenuAction(parent=menu_row,
|
||||
label=self.i18n["manage_window.apps_table.row.actions.history"],
|
||||
action=show_history,
|
||||
button_name='app_history'))
|
||||
|
||||
if pkg.model.can_be_downgraded():
|
||||
|
||||
def downgrade():
|
||||
if ConfirmationDialog(
|
||||
title=self.i18n['manage_window.apps_table.row.actions.downgrade'],
|
||||
body=self._parag(self.i18n['manage_window.apps_table.row.actions.downgrade.popup.body'].format(self._bold(str(pkg)))),
|
||||
i18n=self.i18n).ask():
|
||||
self.window.begin_downgrade(pkg)
|
||||
|
||||
menu_row.addAction(QCustomMenuAction(parent=menu_row,
|
||||
label=self.i18n["manage_window.apps_table.row.actions.downgrade"],
|
||||
action=downgrade,
|
||||
button_name='app_downgrade'))
|
||||
|
||||
if pkg.model.supports_ignored_updates():
|
||||
if pkg.model.is_update_ignored():
|
||||
action_label = self.i18n["manage_window.apps_table.row.actions.ignore_updates_reverse"]
|
||||
button_name = 'revert_ignore_updates'
|
||||
else:
|
||||
action_label = self.i18n["manage_window.apps_table.row.actions.ignore_updates"]
|
||||
button_name = 'ignore_updates'
|
||||
|
||||
def ignore_updates():
|
||||
self.window.begin_ignore_updates(pkg)
|
||||
|
||||
menu_row.addAction(QCustomMenuAction(parent=menu_row,
|
||||
label=action_label,
|
||||
button_name=button_name,
|
||||
action=ignore_updates))
|
||||
|
||||
custom_actions = pkg.model.get_custom_actions()
|
||||
if custom_actions:
|
||||
menu_row.addActions((self._map_custom_action(pkg, a, menu_row) for a in custom_actions))
|
||||
|
||||
menu_row.adjustSize()
|
||||
menu_row.popup(QCursor.pos())
|
||||
menu_row.exec_()
|
||||
|
||||
def _map_custom_action(self, pkg: PackageView, action: CustomSoftwareAction, parent: QWidget) -> QCustomMenuAction:
|
||||
def custom_action():
|
||||
if action.i18n_confirm_key:
|
||||
body = self.i18n[action.i18n_confirm_key].format(bold(pkg.model.name))
|
||||
else:
|
||||
body = '{} ?'.format(self.i18n[action.i18n_label_key])
|
||||
|
||||
if not action.requires_confirmation or ConfirmationDialog(icon=QIcon(pkg.model.get_type_icon_path()),
|
||||
title=self.i18n[action.i18n_label_key],
|
||||
body=self._parag(body),
|
||||
i18n=self.i18n).ask():
|
||||
self.window.begin_execute_custom_action(pkg, action)
|
||||
|
||||
tip = self.i18n[action.i18n_description_key] if action.i18n_description_key else None
|
||||
return QCustomMenuAction(parent=parent,
|
||||
label=self.i18n[action.i18n_label_key],
|
||||
icon=QIcon(action.icon_path) if action.icon_path else None,
|
||||
tooltip=tip,
|
||||
action=custom_action)
|
||||
|
||||
def refresh(self, pkg: PackageView):
|
||||
screen_width = get_current_screen_geometry(self.parent()).width()
|
||||
self._update_row(pkg, screen_width, update_check_enabled=False, change_update_col=False)
|
||||
|
||||
def update_package(self, pkg: PackageView, screen_width: int, change_update_col: bool = False):
|
||||
if self.download_icons and pkg.model.icon_url and pkg.model.icon_url.startswith("http"):
|
||||
self._setup_file_downloader(max_workers=1, max_downloads=1)
|
||||
self.file_downloader.get(pkg.model.icon_url, pkg.table_index)
|
||||
|
||||
self._update_row(pkg, screen_width, change_update_col=change_update_col)
|
||||
|
||||
def _uninstall(self, pkg: PackageView):
|
||||
if ConfirmationDialog(title=self.i18n['manage_window.apps_table.row.actions.uninstall.popup.title'],
|
||||
body=self._parag(
|
||||
self.i18n['manage_window.apps_table.row.actions.uninstall.popup.body'].format(
|
||||
self._bold(str(pkg)))),
|
||||
i18n=self.i18n).ask():
|
||||
self.window.begin_uninstall(pkg)
|
||||
|
||||
def _bold(self, text: str) -> str:
|
||||
return '<span style="font-weight: bold">{}</span>'.format(text)
|
||||
|
||||
def _parag(self, text: str) -> str:
|
||||
return '<p>{}</p>'.format(text)
|
||||
|
||||
def _install_app(self, pkgv: PackageView):
|
||||
|
||||
body = self.i18n['manage_window.apps_table.row.actions.install.popup.body'].format(self._bold(str(pkgv)))
|
||||
|
||||
confirm_icon = MessageType.INFO
|
||||
if not pkgv.model.is_trustable():
|
||||
warning = self.i18n["action.install.unverified.warning"]
|
||||
confirm_icon = MessageType.WARNING
|
||||
body += '<br/><br/> {}'.format(
|
||||
'<br/>'.join(('{}.'.format(phrase) for phrase in warning.split('.') if phrase)))
|
||||
|
||||
if ConfirmationDialog(title=self.i18n['manage_window.apps_table.row.actions.install.popup.title'],
|
||||
body=self._parag(body),
|
||||
i18n=self.i18n,
|
||||
confirmation_icon_type=confirm_icon).ask():
|
||||
self.window.install(pkgv)
|
||||
|
||||
def _update_pkg_icon(self, url_: str, content: Optional[bytes], table_idx: int):
|
||||
if not content:
|
||||
return content
|
||||
|
||||
icon_data = self.icon_cache.get(url_)
|
||||
icon_was_cached = True
|
||||
|
||||
if not icon_data:
|
||||
icon_bytes = content
|
||||
|
||||
if not icon_bytes:
|
||||
return
|
||||
|
||||
icon_was_cached = False
|
||||
pixmap = QPixmap()
|
||||
pixmap.loadFromData(icon_bytes)
|
||||
|
||||
if not pixmap.isNull():
|
||||
icon = QIcon(pixmap)
|
||||
icon_data = {'icon': icon, 'bytes': icon_bytes}
|
||||
self.icon_cache.add(url_, icon_data)
|
||||
|
||||
if icon_data:
|
||||
for pkg in self.window.pkgs:
|
||||
if pkg.table_index == table_idx:
|
||||
self._update_icon(self.cellWidget(table_idx, 0), icon_data['icon'])
|
||||
|
||||
if pkg.model.supports_disk_cache() and pkg.model.get_disk_icon_path() and icon_data['bytes']:
|
||||
if not icon_was_cached or not os.path.exists(pkg.model.get_disk_icon_path()):
|
||||
self.window.manager.cache_to_disk(pkg=pkg.model, icon_bytes=icon_data['bytes'],
|
||||
only_icon=True)
|
||||
|
||||
def update_packages(self, pkgs: List[PackageView], update_check_enabled: bool = True):
|
||||
self.setRowCount(0) # removes the overwrite effect when updates the table
|
||||
self.setEnabled(True)
|
||||
|
||||
if pkgs:
|
||||
screen_width = get_current_screen_geometry(self.parent()).width()
|
||||
self.setColumnCount(self.COL_NUMBER if update_check_enabled else self.COL_NUMBER - 1)
|
||||
self.setRowCount(len(pkgs))
|
||||
|
||||
file_downloader_defined = False
|
||||
|
||||
for idx, pkg in enumerate(pkgs):
|
||||
pkg.table_index = idx
|
||||
|
||||
if self.download_icons and pkg.model.status == PackageStatus.READY and pkg.model.icon_url \
|
||||
and RE_URL.match(pkg.model.icon_url):
|
||||
if not file_downloader_defined:
|
||||
self._setup_file_downloader()
|
||||
file_downloader_defined = True
|
||||
|
||||
self.file_downloader.get(pkg.model.icon_url, idx)
|
||||
|
||||
self._update_row(pkg, screen_width, update_check_enabled)
|
||||
|
||||
self.scrollToTop()
|
||||
|
||||
def _update_row(self, pkg: PackageView, screen_width: int,
|
||||
update_check_enabled: bool = True, change_update_col: bool = True):
|
||||
self._set_col_icon(0, pkg)
|
||||
self._set_col_name(1, pkg, screen_width)
|
||||
self._set_col_version(2, pkg, screen_width)
|
||||
self._set_col_description(3, pkg, screen_width)
|
||||
self._set_col_publisher(4, pkg, screen_width)
|
||||
self._set_col_type(5, pkg)
|
||||
self._set_col_installed(6, pkg)
|
||||
self._set_col_actions(7, pkg)
|
||||
|
||||
if change_update_col and update_check_enabled:
|
||||
if pkg.model.installed and not pkg.model.is_update_ignored() and pkg.model.update:
|
||||
col_update = QCustomToolbar()
|
||||
col_update.add_space()
|
||||
col_update.add_widget(UpgradeToggleButton(pkg=pkg,
|
||||
root=self.window,
|
||||
i18n=self.i18n,
|
||||
checked=pkg.update_checked if pkg.model.can_be_updated() else False,
|
||||
clickable=pkg.model.can_be_updated()))
|
||||
col_update.add_space()
|
||||
else:
|
||||
col_update = QLabel()
|
||||
|
||||
self.setCellWidget(pkg.table_index, 8, col_update)
|
||||
|
||||
def _gen_row_button(self, text: str, name: str, callback, tip: Optional[str] = None) -> QToolButton:
|
||||
col_bt = QToolButton()
|
||||
col_bt.setProperty('text_only', 'true')
|
||||
col_bt.setObjectName(name)
|
||||
col_bt.setCursor(QCursor(Qt.PointingHandCursor))
|
||||
col_bt.setText(text)
|
||||
col_bt.clicked.connect(callback)
|
||||
|
||||
if tip:
|
||||
col_bt.setToolTip(tip)
|
||||
|
||||
return col_bt
|
||||
|
||||
def _set_col_installed(self, col: int, pkg: PackageView):
|
||||
toolbar = QCustomToolbar()
|
||||
toolbar.add_space()
|
||||
|
||||
if pkg.model.installed:
|
||||
if pkg.model.can_be_uninstalled():
|
||||
def uninstall():
|
||||
self._uninstall(pkg)
|
||||
|
||||
item = self._gen_row_button(text=self.i18n['uninstall'].capitalize(),
|
||||
name='bt_uninstall',
|
||||
callback=uninstall,
|
||||
tip=self.i18n['manage_window.bt_uninstall.tip'])
|
||||
else:
|
||||
item = None
|
||||
|
||||
elif pkg.model.can_be_installed():
|
||||
def install():
|
||||
self._install_app(pkg)
|
||||
|
||||
item = self._gen_row_button(text=self.i18n['install'].capitalize(),
|
||||
name='bt_install',
|
||||
callback=install,
|
||||
tip=self.i18n['manage_window.bt_install.tip'])
|
||||
else:
|
||||
item = None
|
||||
|
||||
toolbar.add_widget(item)
|
||||
toolbar.add_space()
|
||||
self.setCellWidget(pkg.table_index, col, toolbar)
|
||||
|
||||
def _set_col_type(self, col: int, pkg: PackageView):
|
||||
icon_data = self.cache_type_icon.get(pkg.model.get_type())
|
||||
|
||||
if icon_data is None:
|
||||
icon = QIcon(pkg.model.get_type_icon_path())
|
||||
pixmap = icon.pixmap(self._get_icon_size(icon))
|
||||
icon_data = {'px': pixmap, 'tip': '{}: {}'.format(self.i18n['type'], pkg.get_type_label())}
|
||||
self.cache_type_icon[pkg.model.get_type()] = icon_data
|
||||
|
||||
col_type_icon = QLabel()
|
||||
col_type_icon.setProperty('icon', 'true')
|
||||
col_type_icon.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred)
|
||||
col_type_icon.setPixmap(icon_data['px'])
|
||||
col_type_icon.setToolTip(icon_data['tip'])
|
||||
self.setCellWidget(pkg.table_index, col, col_type_icon)
|
||||
|
||||
def _set_col_version(self, col: int, pkg: PackageView, screen_width: int):
|
||||
label_version = QLabel(str(pkg.model.version if pkg.model.version else '?'))
|
||||
label_version.setObjectName('app_version')
|
||||
label_version.setAlignment(Qt.AlignCenter)
|
||||
|
||||
item = QWidget()
|
||||
item.setProperty('container', 'true')
|
||||
item.setLayout(QHBoxLayout())
|
||||
item.layout().addWidget(label_version)
|
||||
|
||||
if pkg.model.version:
|
||||
tooltip = self.i18n['version.installed'] if pkg.model.installed else self.i18n['version']
|
||||
else:
|
||||
tooltip = self.i18n['version.unknown']
|
||||
|
||||
if pkg.model.installed and pkg.model.update and not pkg.model.is_update_ignored():
|
||||
label_version.setProperty('update', 'true')
|
||||
tooltip = pkg.model.get_update_tip() or self.i18n['version.installed_outdated']
|
||||
|
||||
if pkg.model.installed and pkg.model.is_update_ignored():
|
||||
label_version.setProperty('ignored', 'true')
|
||||
tooltip = self.i18n['version.updates_ignored']
|
||||
|
||||
if pkg.model.installed and pkg.model.update and not pkg.model.is_update_ignored() and pkg.model.version and pkg.model.latest_version and pkg.model.version != pkg.model.latest_version:
|
||||
tooltip = f"{tooltip} ({self.i18n['version.installed']}: {pkg.model.version} | " \
|
||||
f"{self.i18n['version.latest']}: {pkg.model.latest_version})"
|
||||
label_version.setText(f"{label_version.text()} > {pkg.model.latest_version}")
|
||||
|
||||
if label_version.sizeHint().width() / screen_width > 0.22:
|
||||
label_version.setText(pkg.model.latest_version)
|
||||
|
||||
item.setToolTip(tooltip)
|
||||
self.setCellWidget(pkg.table_index, col, item)
|
||||
|
||||
def _read_default_icon(self, pkgv: PackageView):
|
||||
icon_path = pkgv.model.get_default_icon_path()
|
||||
icon = self.cache_default_icon.get(icon_path)
|
||||
|
||||
if not icon:
|
||||
icon = QIcon(icon_path)
|
||||
self.cache_default_icon[icon_path] = icon
|
||||
|
||||
return icon
|
||||
|
||||
def _set_col_icon(self, col: int, pkg: PackageView):
|
||||
icon_path = pkg.model.get_disk_icon_path()
|
||||
if pkg.model.installed and pkg.model.supports_disk_cache() and icon_path:
|
||||
if icon_path.startswith('/'):
|
||||
if os.path.isfile(icon_path):
|
||||
with open(icon_path, 'rb') as f:
|
||||
icon_bytes = f.read()
|
||||
pixmap = QPixmap()
|
||||
pixmap.loadFromData(icon_bytes)
|
||||
icon = QIcon(pixmap)
|
||||
self.icon_cache.add_non_existing(pkg.model.icon_url, {'icon': icon, 'bytes': icon_bytes})
|
||||
else:
|
||||
icon = self._read_default_icon(pkg)
|
||||
else:
|
||||
try:
|
||||
icon = QIcon.fromTheme(icon_path)
|
||||
|
||||
if icon.isNull():
|
||||
icon = self._read_default_icon(pkg)
|
||||
elif pkg.model.icon_url:
|
||||
self.icon_cache.add_non_existing(pkg.model.icon_url, {'icon': icon, 'bytes': None})
|
||||
|
||||
except Exception:
|
||||
icon = self._read_default_icon(pkg)
|
||||
|
||||
elif not pkg.model.icon_url:
|
||||
icon = self._read_default_icon(pkg)
|
||||
else:
|
||||
icon_data = self.icon_cache.get(pkg.model.icon_url)
|
||||
icon = icon_data['icon'] if icon_data else self._read_default_icon(pkg)
|
||||
|
||||
col_icon = QLabel()
|
||||
col_icon.setProperty('icon', 'true')
|
||||
col_icon.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred)
|
||||
self._update_icon(col_icon, icon)
|
||||
self.setCellWidget(pkg.table_index, col, col_icon)
|
||||
|
||||
def _set_col_name(self, col: int, pkg: PackageView, screen_width: int):
|
||||
col_name = QLabel()
|
||||
col_name.setObjectName('app_name')
|
||||
col_name.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred)
|
||||
|
||||
name = pkg.model.get_display_name().strip()
|
||||
if name:
|
||||
col_name.setToolTip('{}: {}'.format(self.i18n['app.name'].lower(), pkg.model.get_name_tooltip()))
|
||||
else:
|
||||
name = '...'
|
||||
col_name.setToolTip(self.i18n['app.name'].lower())
|
||||
|
||||
col_name.setText(name)
|
||||
screen_perc = col_name.sizeHint().width() / screen_width
|
||||
|
||||
if screen_perc > 0.15:
|
||||
max_chars = int(len(name) * 0.15 / screen_perc) - 3
|
||||
col_name.setText(name[0:max_chars] + '...')
|
||||
|
||||
self.setCellWidget(pkg.table_index, col, col_name)
|
||||
|
||||
def _update_icon(self, label: QLabel, icon: QIcon):
|
||||
label.setPixmap(icon.pixmap(self._get_icon_size(icon)))
|
||||
|
||||
def _get_icon_size(self, icon: QIcon) -> QSize:
|
||||
sizes = icon.availableSizes()
|
||||
return sizes[-1] if sizes else self.DEFAULT_ICON_SIZE
|
||||
|
||||
def _set_col_description(self, col: int, pkg: PackageView, screen_width: int):
|
||||
item = QLabel()
|
||||
item.setObjectName('app_description')
|
||||
|
||||
if pkg.model.description is not None or not pkg.model.is_application() or pkg.model.status == PackageStatus.READY:
|
||||
desc = pkg.model.description.split('\n')[0] if pkg.model.description else pkg.model.description
|
||||
else:
|
||||
desc = '...'
|
||||
|
||||
if desc and desc != '...':
|
||||
desc = strip_html(desc)
|
||||
|
||||
item.setText(desc)
|
||||
|
||||
current_width_perc = item.sizeHint().width() / screen_width
|
||||
if current_width_perc > 0.18:
|
||||
max_width = int(len(desc) * 0.18 / current_width_perc) - 3
|
||||
desc = desc[0:max_width] + '...'
|
||||
item.setText(desc)
|
||||
|
||||
if pkg.model.description:
|
||||
item.setToolTip(pkg.model.description)
|
||||
|
||||
self.setCellWidget(pkg.table_index, col, item)
|
||||
|
||||
def _set_col_publisher(self, col: int, pkg: PackageView, screen_width: int):
|
||||
item = QToolBar()
|
||||
|
||||
publisher = pkg.model.get_publisher()
|
||||
full_publisher = None
|
||||
|
||||
lb_name = QLabel()
|
||||
lb_name.setObjectName('app_publisher')
|
||||
|
||||
if publisher:
|
||||
publisher = publisher.strip()
|
||||
full_publisher = publisher
|
||||
|
||||
if publisher:
|
||||
lb_name.setText(publisher)
|
||||
screen_perc = lb_name.sizeHint().width() / screen_width
|
||||
|
||||
if screen_perc > 0.12:
|
||||
max_chars = int(len(publisher) * 0.12 / screen_perc) - 3
|
||||
publisher = publisher[0: max_chars] + '...'
|
||||
lb_name.setText(publisher)
|
||||
|
||||
if not publisher:
|
||||
if not pkg.model.installed:
|
||||
lb_name.setProperty('publisher_known', 'false')
|
||||
|
||||
publisher = self.i18n['unknown']
|
||||
|
||||
lb_name.setText(f' {publisher}')
|
||||
item.addWidget(lb_name)
|
||||
|
||||
if publisher and full_publisher:
|
||||
lb_name.setToolTip(
|
||||
self.i18n['publisher'].capitalize() + ((': ' + full_publisher) if full_publisher else ''))
|
||||
|
||||
if pkg.model.is_trustable():
|
||||
lb_verified = QLabel()
|
||||
lb_verified.setObjectName('icon_publisher_verified')
|
||||
lb_verified.setCursor(QCursor(Qt.WhatsThisCursor))
|
||||
lb_verified.setToolTip(self.i18n['publisher.verified'].capitalize())
|
||||
item.addWidget(lb_verified)
|
||||
else:
|
||||
lb_name.setText(lb_name.text() + " ")
|
||||
|
||||
self.setCellWidget(pkg.table_index, col, item)
|
||||
|
||||
def _set_col_actions(self, col: int, pkg: PackageView):
|
||||
toolbar = QCustomToolbar()
|
||||
toolbar.setObjectName('app_actions')
|
||||
toolbar.add_space()
|
||||
|
||||
if pkg.model.installed:
|
||||
def run():
|
||||
self.window.begin_launch_package(pkg)
|
||||
|
||||
bt = IconButton(i18n=self.i18n, action=run, tooltip=self.i18n['action.run.tooltip'])
|
||||
bt.setObjectName('app_run')
|
||||
|
||||
if not pkg.model.can_be_run():
|
||||
bt.setEnabled(False)
|
||||
bt.setProperty('_enabled', 'false')
|
||||
|
||||
toolbar.layout().addWidget(bt)
|
||||
|
||||
settings = self.has_any_settings(pkg)
|
||||
|
||||
if pkg.model.installed:
|
||||
def handle_custom_actions():
|
||||
self.show_pkg_actions(pkg)
|
||||
|
||||
bt = IconButton(i18n=self.i18n, action=handle_custom_actions, tooltip=self.i18n['action.settings.tooltip'])
|
||||
bt.setObjectName('app_actions')
|
||||
bt.setEnabled(bool(settings))
|
||||
toolbar.layout().addWidget(bt)
|
||||
|
||||
if not pkg.model.installed:
|
||||
def show_screenshots():
|
||||
self.window.begin_show_screenshots(pkg)
|
||||
|
||||
bt = IconButton(i18n=self.i18n, action=show_screenshots,
|
||||
tooltip=self.i18n['action.screenshots.tooltip'])
|
||||
bt.setObjectName('app_screenshots')
|
||||
|
||||
if not pkg.model.has_screenshots():
|
||||
bt.setEnabled(False)
|
||||
bt.setProperty('_enabled', 'false')
|
||||
|
||||
toolbar.layout().addWidget(bt)
|
||||
|
||||
def show_info():
|
||||
self.window.begin_show_info(pkg)
|
||||
|
||||
bt = IconButton(i18n=self.i18n, action=show_info, tooltip=self.i18n['action.info.tooltip'])
|
||||
bt.setObjectName('app_info')
|
||||
bt.setEnabled(bool(pkg.model.has_info()))
|
||||
toolbar.layout().addWidget(bt)
|
||||
|
||||
self.setCellWidget(pkg.table_index, col, toolbar)
|
||||
|
||||
def change_headers_policy(self, policy: QHeaderView = QHeaderView.ResizeToContents, maximized: bool = False):
|
||||
header_horizontal = self.horizontalHeader()
|
||||
for i in range(self.columnCount()):
|
||||
if maximized:
|
||||
if i in (2, 3):
|
||||
header_horizontal.setSectionResizeMode(i, QHeaderView.Stretch)
|
||||
else:
|
||||
header_horizontal.setSectionResizeMode(i, QHeaderView.ResizeToContents)
|
||||
else:
|
||||
header_horizontal.setSectionResizeMode(i, policy)
|
||||
|
||||
def get_width(self):
|
||||
return reduce(operator.add, [self.columnWidth(i) for i in range(self.columnCount())])
|
||||
|
||||
def _setup_file_downloader(self, max_workers: int = 50, max_downloads: int = -1) -> None:
|
||||
self.file_downloader = URLFileDownloader(logger=self.logger,
|
||||
max_workers=max_workers,
|
||||
max_downloads=max_downloads,
|
||||
parent=self)
|
||||
self.file_downloader.signal_downloaded.connect(self._update_pkg_icon)
|
||||
self.file_downloader.start()
|
||||
|
||||
def stop_file_downloader(self, wait: bool = False) -> None:
|
||||
if self.file_downloader:
|
||||
self.file_downloader.stop()
|
||||
|
||||
if wait:
|
||||
self.file_downloader.wait()
|
||||
@@ -1,151 +0,0 @@
|
||||
from typing import List, Dict, Any, NamedTuple, Optional, Union, Collection, Iterable
|
||||
|
||||
from bauh.api.abstract.model import SoftwarePackage
|
||||
from bauh.view.qt.view_model import PackageView
|
||||
|
||||
|
||||
class PackageFilters(NamedTuple):
|
||||
"""
|
||||
It represents the manage window selected filters
|
||||
"""
|
||||
|
||||
display_limit: int
|
||||
category: str
|
||||
name: Optional[str]
|
||||
only_apps: bool
|
||||
only_installed: bool
|
||||
only_updates: bool
|
||||
only_verified: bool
|
||||
search: Optional[str] # initial search term
|
||||
type: str
|
||||
|
||||
@property
|
||||
def anything(self) -> bool:
|
||||
return not self.only_installed and not self.only_updates and not self.only_apps \
|
||||
and not self.only_verified and not self.name and self.type == "any" and self.category == "any"
|
||||
|
||||
|
||||
def new_pkgs_info() -> Dict[str, Any]:
|
||||
return {'apps_count': 0, # number of application packages
|
||||
'napps_count': 0, # number of not application packages (libraries, runtimes or something else)
|
||||
'available_types': {}, # available package types in 'new_pkgs'
|
||||
'updates': 0,
|
||||
'app_updates': 0,
|
||||
'napp_updates': 0,
|
||||
'pkgs_displayed': [],
|
||||
'not_installed': 0,
|
||||
'installed': 0,
|
||||
'categories': set(),
|
||||
'verified': 0,
|
||||
'pkgs': []} # total packages
|
||||
|
||||
|
||||
def update_info(pkgv: PackageView, pkgs_info: Dict[str, Any]):
|
||||
pkgs_info['available_types'][pkgv.model.get_type()] = {'icon': pkgv.model.get_type_icon_path(), 'label': pkgv.get_type_label()}
|
||||
|
||||
if pkgv.model.is_application():
|
||||
pkgs_info['apps_count'] += 1
|
||||
else:
|
||||
pkgs_info['napps_count'] += 1
|
||||
|
||||
if pkgv.model.is_trustable():
|
||||
pkgs_info['verified'] += 1
|
||||
|
||||
if pkgv.model.update and not pkgv.model.is_update_ignored():
|
||||
if pkgv.model.is_application():
|
||||
pkgs_info['app_updates'] += 1
|
||||
else:
|
||||
pkgs_info['napp_updates'] += 1
|
||||
|
||||
pkgs_info['updates'] += 1
|
||||
|
||||
if pkgv.model.categories:
|
||||
for c in pkgv.model.categories:
|
||||
if c:
|
||||
cat = c.lower().strip()
|
||||
if cat:
|
||||
pkgs_info['categories'].add(cat)
|
||||
|
||||
pkgs_info['pkgs'].append(pkgv)
|
||||
|
||||
if pkgv.model.installed:
|
||||
pkgs_info['installed'] += 1
|
||||
else:
|
||||
pkgs_info['not_installed'] += 1
|
||||
|
||||
|
||||
def apply_filters(pkg: PackageView, filters: PackageFilters, info: dict, limit: bool = True):
|
||||
if not limit or not filters.display_limit or len(info['pkgs_displayed']) < filters.display_limit:
|
||||
if not is_package_hidden(pkg, filters):
|
||||
info['pkgs_displayed'].append(pkg)
|
||||
|
||||
|
||||
def sum_updates_displayed(info: dict) -> int:
|
||||
updates = 0
|
||||
if info['pkgs_displayed']:
|
||||
for p in info['pkgs_displayed']:
|
||||
if p.model.update and not p.model.is_update_ignored():
|
||||
updates += 1
|
||||
|
||||
return updates
|
||||
|
||||
|
||||
def is_package_hidden(pkg: PackageView, filters: PackageFilters) -> bool:
|
||||
hidden = filters.only_installed and not pkg.model.installed
|
||||
|
||||
if not hidden and filters.only_apps:
|
||||
hidden = pkg.model.installed and not pkg.model.is_application()
|
||||
|
||||
if not hidden and filters.only_updates:
|
||||
hidden = not pkg.model.update or pkg.model.is_update_ignored()
|
||||
|
||||
if not hidden and filters.only_verified:
|
||||
hidden = not pkg.model.is_trustable()
|
||||
|
||||
if not hidden and filters.type is not None and filters.type != "any":
|
||||
hidden = pkg.model.get_type() != filters.type
|
||||
|
||||
if not hidden and filters.category is not None and filters.category != "any":
|
||||
hidden = not pkg.model.categories or not next((c for c in pkg.model.categories if c.lower() == filters.category), None)
|
||||
|
||||
if not hidden and filters.name:
|
||||
hidden = not filters.name.startswith(pkg.model.name.lower())
|
||||
|
||||
return hidden
|
||||
|
||||
|
||||
def _by_name(pkg: Union[SoftwarePackage, PackageView]):
|
||||
return pkg.name.lower()
|
||||
|
||||
|
||||
def sort_packages(pkgs: Iterable[Union[SoftwarePackage, PackageView]], word: Optional[str],
|
||||
limit: int = 0) -> List[SoftwarePackage]:
|
||||
exact, starts_with, contains, others = [], [], [], []
|
||||
|
||||
if not word:
|
||||
others.extend(pkgs)
|
||||
else:
|
||||
for p in pkgs:
|
||||
lower_name = p.name.lower()
|
||||
if word == lower_name:
|
||||
exact.append(p)
|
||||
elif lower_name.startswith(word):
|
||||
starts_with.append(p)
|
||||
elif word in lower_name:
|
||||
contains.append(p)
|
||||
else:
|
||||
others.append(p)
|
||||
|
||||
res = []
|
||||
for app_list in (exact, starts_with, contains, others):
|
||||
if app_list:
|
||||
last = limit - len(res) if limit is not None and limit > 0 else None
|
||||
|
||||
if last is not None and last <= 0:
|
||||
break
|
||||
|
||||
to_add = app_list[0:last]
|
||||
to_add.sort(key=_by_name)
|
||||
res.extend(to_add)
|
||||
|
||||
return res
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,138 +0,0 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtGui import QIcon, QCursor
|
||||
from PyQt5.QtWidgets import QMessageBox, QLabel, QWidget, QHBoxLayout, QDialog, QVBoxLayout, QSizePolicy, QPushButton, \
|
||||
QScrollArea, QFrame
|
||||
|
||||
from bauh.api.abstract.view import MessageType
|
||||
from bauh.view.qt.components import new_spacer
|
||||
from bauh.view.util import resource
|
||||
from bauh.view.util.translation import I18n
|
||||
|
||||
MSG_TYPE_MAP = {
|
||||
MessageType.ERROR: QMessageBox.Critical,
|
||||
MessageType.INFO: QMessageBox.Information,
|
||||
MessageType.WARNING: QMessageBox.Warning
|
||||
}
|
||||
|
||||
|
||||
def show_message(title: str, body: str, type_: MessageType, icon: QIcon = QIcon(resource.get_path('img/logo.svg'))):
|
||||
popup = QMessageBox()
|
||||
popup.setWindowTitle(title)
|
||||
popup.setText(body)
|
||||
popup.setIcon(MSG_TYPE_MAP[type_])
|
||||
|
||||
if icon:
|
||||
popup.setWindowIcon(icon)
|
||||
|
||||
popup.exec_()
|
||||
|
||||
|
||||
class ConfirmationDialog(QDialog):
|
||||
|
||||
def __init__(self, title: str, body: Optional[str], i18n: I18n, icon: QIcon = QIcon(resource.get_path('img/logo.svg')),
|
||||
widgets: Optional[List[QWidget]] = None, confirmation_button: bool = True, deny_button: bool = True,
|
||||
window_cancel: bool = False, confirmation_label: Optional[str] = None, deny_label: Optional[str] = None,
|
||||
confirmation_icon: bool = True, min_width: Optional[int] = None,
|
||||
min_height: Optional[int] = None, max_width: Optional[int] = None,
|
||||
confirmation_icon_type: MessageType = MessageType.INFO):
|
||||
super(ConfirmationDialog, self).__init__()
|
||||
|
||||
if not window_cancel:
|
||||
self.setWindowFlags(Qt.CustomizeWindowHint | Qt.WindowTitleHint)
|
||||
|
||||
self.setLayout(QVBoxLayout())
|
||||
self.setWindowTitle(title)
|
||||
self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred)
|
||||
self.setMinimumWidth(min_width if min_width and min_width > 0 else 250)
|
||||
|
||||
if max_width is not None and max_width > 0:
|
||||
self.setMaximumWidth(max_width)
|
||||
|
||||
if isinstance(min_height, int) and min_height > 0:
|
||||
self.setMinimumHeight(min_height)
|
||||
|
||||
self.confirmed = False
|
||||
|
||||
if icon:
|
||||
self.setWindowIcon(icon)
|
||||
|
||||
container_body = QWidget()
|
||||
container_body.setObjectName('confirm_container_body')
|
||||
container_body.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred)
|
||||
|
||||
if isinstance(min_height, int) and min_height > 0:
|
||||
container_body.setMinimumWidth(min_height)
|
||||
|
||||
if widgets:
|
||||
container_body.setLayout(QVBoxLayout())
|
||||
scroll = QScrollArea(self)
|
||||
scroll.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred)
|
||||
scroll.setFrameShape(QFrame.NoFrame)
|
||||
scroll.setWidgetResizable(True)
|
||||
scroll.setWidget(container_body)
|
||||
self.layout().addWidget(scroll)
|
||||
else:
|
||||
container_body.setLayout(QHBoxLayout())
|
||||
self.layout().addWidget(container_body)
|
||||
|
||||
if confirmation_icon:
|
||||
lb_icon = QLabel()
|
||||
lb_icon.setObjectName("confirm_dialog_icon")
|
||||
lb_icon.setProperty("type", confirmation_icon_type.name.lower())
|
||||
container_body.layout().addWidget(lb_icon)
|
||||
|
||||
if body:
|
||||
lb_msg = QLabel(body)
|
||||
lb_msg.setObjectName('confirm_msg')
|
||||
container_body.layout().addWidget(lb_msg)
|
||||
|
||||
if widgets:
|
||||
for w in widgets:
|
||||
container_body.layout().addWidget(w)
|
||||
else:
|
||||
container_body.layout().addWidget(new_spacer())
|
||||
|
||||
container_bottom = QWidget()
|
||||
container_bottom.setObjectName('confirm_container_bottom')
|
||||
container_bottom.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred)
|
||||
container_bottom.setLayout(QHBoxLayout())
|
||||
self.layout().addWidget(container_bottom)
|
||||
|
||||
container_bottom.layout().addWidget(new_spacer())
|
||||
|
||||
if confirmation_button:
|
||||
bt_confirm = QPushButton(confirmation_label.capitalize() if confirmation_label else i18n['popup.button.yes'])
|
||||
bt_confirm.setObjectName('ok')
|
||||
bt_confirm.setCursor(QCursor(Qt.PointingHandCursor))
|
||||
bt_confirm.setDefault(True)
|
||||
bt_confirm.setAutoDefault(True)
|
||||
bt_confirm.clicked.connect(self.confirm)
|
||||
container_bottom.layout().addWidget(bt_confirm)
|
||||
|
||||
if deny_button:
|
||||
bt_cancel = QPushButton(deny_label.capitalize() if deny_label else i18n['popup.button.no'])
|
||||
bt_cancel.setObjectName('bt_cancel')
|
||||
bt_cancel.setCursor(QCursor(Qt.PointingHandCursor))
|
||||
bt_cancel.clicked.connect(self.close)
|
||||
container_bottom.layout().addWidget(bt_cancel)
|
||||
|
||||
if not confirmation_button:
|
||||
bt_cancel.setDefault(True)
|
||||
bt_cancel.setAutoDefault(True)
|
||||
|
||||
def confirm(self):
|
||||
self.confirmed = True
|
||||
self.close()
|
||||
|
||||
def ask(self) -> bool:
|
||||
self.exec_()
|
||||
return self.confirmed
|
||||
|
||||
|
||||
def ask_confirmation(title: str, body: str, i18n: I18n, icon: QIcon = QIcon(resource.get_path('img/logo.svg')),
|
||||
widgets: List[QWidget] = None) -> bool:
|
||||
popup = ConfirmationDialog(title=title, body=body, i18n=i18n, icon=icon, widgets=widgets)
|
||||
popup.exec_()
|
||||
return popup.confirmed
|
||||
@@ -1,70 +0,0 @@
|
||||
import operator
|
||||
from functools import reduce
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtGui import QIcon, QCursor
|
||||
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QTableWidget, QHeaderView, QLabel
|
||||
|
||||
from bauh.api.abstract.cache import MemoryCache
|
||||
from bauh.api.abstract.model import PackageHistory
|
||||
from bauh.view.qt.view_model import PackageView
|
||||
from bauh.view.util.translation import I18n
|
||||
|
||||
|
||||
class HistoryDialog(QDialog):
|
||||
|
||||
def __init__(self, history: PackageHistory, icon_cache: MemoryCache, i18n: I18n):
|
||||
super(HistoryDialog, self).__init__()
|
||||
self.setWindowFlags(Qt.CustomizeWindowHint | Qt.WindowMinMaxButtonsHint | Qt.WindowCloseButtonHint)
|
||||
|
||||
view = PackageView(model=history.pkg, i18n=i18n)
|
||||
|
||||
self.setWindowTitle('{} - {}'.format(i18n['popup.history.title'], view))
|
||||
|
||||
layout = QVBoxLayout()
|
||||
self.setLayout(layout)
|
||||
|
||||
table_history = QTableWidget()
|
||||
table_history.setFocusPolicy(Qt.NoFocus)
|
||||
table_history.setShowGrid(False)
|
||||
table_history.verticalHeader().setVisible(False)
|
||||
table_history.setAlternatingRowColors(True)
|
||||
|
||||
table_history.setColumnCount(len(history.history[0]))
|
||||
table_history.setRowCount(len(history.history))
|
||||
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, data in enumerate(history.history):
|
||||
|
||||
current_status = history.pkg_status_idx == row
|
||||
|
||||
for col, key in enumerate(sorted(data.keys())):
|
||||
item = QLabel()
|
||||
item.setProperty('even', row % 2 == 0)
|
||||
item.setText(' {}'.format(data[key]))
|
||||
|
||||
if current_status:
|
||||
item.setCursor(QCursor(Qt.WhatsThisCursor))
|
||||
item.setProperty('outdated', str(row != 0).lower())
|
||||
|
||||
tip = '{}. {}.'.format(i18n['popup.history.selected.tooltip'], i18n['version.{}'.format('updated'if row == 0 else 'outdated')].capitalize())
|
||||
|
||||
item.setToolTip(tip)
|
||||
|
||||
table_history.setCellWidget(row, col, item)
|
||||
|
||||
layout.addWidget(table_history)
|
||||
|
||||
header_horizontal = table_history.horizontalHeader()
|
||||
for i in range(0, table_history.columnCount()):
|
||||
header_horizontal.setSectionResizeMode(i, QHeaderView.Stretch)
|
||||
|
||||
new_width = reduce(operator.add, [table_history.columnWidth(i) for i in range(table_history.columnCount())])
|
||||
self.resize(new_width, table_history.height())
|
||||
|
||||
# THERE ARE CRASHES WITH SOME RARE ICONS ( like insomnia ). IT CAN BE A QT BUG. IN THE MEANTIME, ONLY THE TYPE ICON WILL BE RENDERED
|
||||
#
|
||||
# icon_data = icon_cache.get(history.pkg.icon_url)
|
||||
# if icon_data and icon_data.get('icon'):
|
||||
# self.setWindowIcon(icon_data.get('icon'))
|
||||
self.setWindowIcon(QIcon(history.pkg.get_type_icon_path()))
|
||||
@@ -1,158 +0,0 @@
|
||||
import shutil
|
||||
import subprocess
|
||||
from collections.abc import Iterable
|
||||
from subprocess import Popen
|
||||
from typing import Optional
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtGui import QIcon, QCursor
|
||||
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QGroupBox, \
|
||||
QLineEdit, QLabel, QGridLayout, QPushButton, QPlainTextEdit, QScrollArea, QFrame, QWidget, QSizePolicy, \
|
||||
QHBoxLayout
|
||||
|
||||
from bauh.api.abstract.cache import MemoryCache
|
||||
from bauh.commons.regex import RE_URL
|
||||
from bauh.view.qt.components import new_spacer
|
||||
from bauh.view.qt.qt_utils import get_current_screen_geometry
|
||||
from bauh.view.util.translation import I18n
|
||||
|
||||
IGNORED_ATTRS = {'name', '__app__'}
|
||||
|
||||
|
||||
class InfoDialog(QDialog):
|
||||
|
||||
def __init__(self, pkg_info: dict, icon_cache: MemoryCache, i18n: I18n, can_open_url: bool):
|
||||
super(InfoDialog, self).__init__()
|
||||
self.setWindowTitle(str(pkg_info['__app__']))
|
||||
self.i18n = i18n
|
||||
self._can_open_url = can_open_url
|
||||
layout = QVBoxLayout()
|
||||
self.setLayout(layout)
|
||||
|
||||
scroll = QScrollArea(self)
|
||||
scroll.setFrameShape(QFrame.NoFrame)
|
||||
scroll.setWidgetResizable(True)
|
||||
comps_container = QWidget()
|
||||
comps_container.setObjectName('root_container')
|
||||
comps_container.setLayout(QVBoxLayout())
|
||||
scroll.setWidget(comps_container)
|
||||
|
||||
# shows complete field string
|
||||
self.text_field = QPlainTextEdit()
|
||||
self.text_field.setObjectName('full_field')
|
||||
self.text_field.setReadOnly(True)
|
||||
comps_container.layout().addWidget(self.text_field)
|
||||
self.text_field.hide()
|
||||
|
||||
self.gbox_info = QGroupBox()
|
||||
self.gbox_info.setObjectName('fields')
|
||||
self.gbox_info.setLayout(QGridLayout())
|
||||
|
||||
comps_container.layout().addWidget(self.gbox_info)
|
||||
|
||||
# THERE ARE CRASHES WITH SOME RARE ICONS ( like insomnia ). IT CAN BE A QT BUG. IN THE MEANTIME, ONLY THE TYPE ICON WILL BE RENDERED
|
||||
#
|
||||
# icon_data = icon_cache.get(app['__app__'].model.icon_url)
|
||||
#
|
||||
# if icon_data and icon_data.get('icon'):
|
||||
# self.setWindowIcon(icon_data.get('icon'))
|
||||
self.setWindowIcon(QIcon(pkg_info['__app__'].model.get_type_icon_path()))
|
||||
|
||||
for idx, attr in enumerate(sorted(pkg_info.keys())):
|
||||
if attr not in IGNORED_ATTRS and pkg_info[attr] is not None:
|
||||
i18n_key = pkg_info['__app__'].model.gem_name + '.info.' + attr.lower()
|
||||
val = pkg_info[attr]
|
||||
|
||||
if not isinstance(val, str) and isinstance(pkg_info[attr], Iterable):
|
||||
val = ' '.join([str(e).strip() for e in pkg_info[attr] if e])
|
||||
show_val = '\n'.join(['* ' + str(e).strip() for e in pkg_info[attr] if e])
|
||||
else:
|
||||
val = str(pkg_info[attr]).strip()
|
||||
show_val = val
|
||||
|
||||
i18n_val = i18n.get(f"{i18n_key}.{val.lower()}")
|
||||
|
||||
if i18n_val:
|
||||
val = i18n_val
|
||||
show_val = val
|
||||
|
||||
text = QLineEdit()
|
||||
text.setObjectName('field_value')
|
||||
text.setToolTip(show_val)
|
||||
text.setText(val)
|
||||
text.setCursorPosition(0)
|
||||
text.setReadOnly(True)
|
||||
|
||||
label = QLabel(i18n.get(i18n_key, i18n.get(attr.lower(), attr)).capitalize())
|
||||
label.setObjectName('field_name')
|
||||
|
||||
self.gbox_info.layout().addWidget(label, idx, 0)
|
||||
self.gbox_info.layout().addWidget(text, idx, 1)
|
||||
|
||||
self._gen_show_button(idx=idx, val=show_val)
|
||||
|
||||
layout.addWidget(scroll)
|
||||
|
||||
lower_container = QWidget()
|
||||
lower_container.setObjectName('lower_container')
|
||||
lower_container.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)
|
||||
lower_container.setLayout(QHBoxLayout())
|
||||
|
||||
self.bt_back = QPushButton('< {}'.format(self.i18n['back'].capitalize()))
|
||||
self.bt_back.setObjectName('back')
|
||||
self.bt_back.setVisible(False)
|
||||
self.bt_back.setCursor(QCursor(Qt.PointingHandCursor))
|
||||
self.bt_back.clicked.connect(self.back_to_info)
|
||||
|
||||
lower_container.layout().addWidget(self.bt_back)
|
||||
lower_container.layout().addWidget(new_spacer())
|
||||
|
||||
self.bt_close = QPushButton(self.i18n['close'].capitalize())
|
||||
self.bt_close.setObjectName('close')
|
||||
self.bt_close.setCursor(QCursor(Qt.PointingHandCursor))
|
||||
self.bt_close.clicked.connect(self.close)
|
||||
|
||||
lower_container.layout().addWidget(self.bt_close)
|
||||
layout.addWidget(lower_container)
|
||||
self.setMinimumWidth(int(self.gbox_info.sizeHint().width() * 1.2))
|
||||
|
||||
screen_height = get_current_screen_geometry().height()
|
||||
self.setMaximumHeight(int(screen_height * 0.8))
|
||||
self.adjustSize()
|
||||
|
||||
@staticmethod
|
||||
def open_url(url: str):
|
||||
Popen(["xdg-open", url], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL)
|
||||
|
||||
def _show_full_field_val(self, val: str):
|
||||
self.gbox_info.hide()
|
||||
self.bt_back.setVisible(True)
|
||||
self.text_field.show()
|
||||
self.text_field.setPlainText(val)
|
||||
|
||||
def _gen_show_button(self, idx: int, val: str):
|
||||
|
||||
is_url = self._can_open_url and bool(RE_URL.match(val)) if val else False
|
||||
|
||||
if is_url:
|
||||
bt_label = self.i18n["manage_window.info.open_url"]
|
||||
|
||||
def _show_field():
|
||||
self.open_url(val)
|
||||
else:
|
||||
bt_label = self.i18n["show"].capitalize()
|
||||
|
||||
def _show_field():
|
||||
self._show_full_field_val(val)
|
||||
|
||||
bt_full_field = QPushButton(bt_label)
|
||||
bt_full_field.setObjectName("show")
|
||||
bt_full_field.setCursor(QCursor(Qt.PointingHandCursor))
|
||||
bt_full_field.clicked.connect(_show_field)
|
||||
self.gbox_info.layout().addWidget(bt_full_field, idx, 2)
|
||||
|
||||
def back_to_info(self):
|
||||
self.text_field.setPlainText("")
|
||||
self.text_field.hide()
|
||||
self.gbox_info.show()
|
||||
self.bt_back.setVisible(False)
|
||||
@@ -1,453 +0,0 @@
|
||||
import datetime
|
||||
import operator
|
||||
import time
|
||||
from functools import reduce
|
||||
from typing import Tuple, Optional
|
||||
|
||||
from PyQt5.QtCore import QSize, Qt, QThread, pyqtSignal, QCoreApplication, QMutex
|
||||
from PyQt5.QtGui import QIcon, QCursor, QCloseEvent, QShowEvent
|
||||
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QSizePolicy, QTableWidget, QHeaderView, QPushButton, \
|
||||
QProgressBar, QPlainTextEdit, QToolButton, QHBoxLayout
|
||||
|
||||
from bauh import __app_name__
|
||||
from bauh.api.abstract.context import ApplicationContext
|
||||
from bauh.api.abstract.controller import SoftwareManager, SoftwareAction
|
||||
from bauh.api.abstract.handler import TaskManager
|
||||
from bauh.api import user
|
||||
from bauh.view.qt.components import new_spacer, QCustomToolbar
|
||||
from bauh.view.qt.qt_utils import centralize, get_current_screen_geometry
|
||||
from bauh.view.qt.root import RootDialog
|
||||
from bauh.view.qt.thread import AnimateProgress
|
||||
from bauh.view.util.translation import I18n
|
||||
|
||||
|
||||
class Prepare(QThread, TaskManager):
|
||||
signal_register = pyqtSignal(str, str, object)
|
||||
signal_update = pyqtSignal(str, float, str)
|
||||
signal_finished = pyqtSignal(str)
|
||||
signal_started = pyqtSignal(int)
|
||||
signal_ask_password = pyqtSignal()
|
||||
signal_output = pyqtSignal(str, str)
|
||||
|
||||
def __init__(self, context: ApplicationContext, manager: SoftwareManager, i18n: I18n):
|
||||
super(Prepare, self).__init__()
|
||||
self.manager = manager
|
||||
self.i18n = i18n
|
||||
self.context = context
|
||||
self.waiting_password = False
|
||||
self.password_response = None
|
||||
self._tasks_added = set()
|
||||
self._tasks_finished = set()
|
||||
self._add_lock = QMutex()
|
||||
self._finish_lock = QMutex()
|
||||
|
||||
def ask_password(self) -> Tuple[bool, Optional[str]]:
|
||||
self.waiting_password = True
|
||||
self.signal_ask_password.emit()
|
||||
|
||||
while self.waiting_password:
|
||||
self.msleep(100) # waiting for user input
|
||||
|
||||
return self.password_response
|
||||
|
||||
def set_password_reply(self, valid: bool, password: str):
|
||||
self.password_response = valid, password
|
||||
self.waiting_password = False
|
||||
|
||||
def run(self):
|
||||
root_pwd = None
|
||||
if not user.is_root() and self.manager.requires_root(SoftwareAction.PREPARE, None):
|
||||
ok, root_pwd = self.ask_password()
|
||||
|
||||
if not ok:
|
||||
QCoreApplication.exit(1)
|
||||
|
||||
self.manager.prepare(self, root_pwd, None)
|
||||
self.signal_started.emit(len(self._tasks_added))
|
||||
|
||||
def update_progress(self, task_id: str, progress: float, substatus: str):
|
||||
self._add_lock.lock()
|
||||
if task_id in self._tasks_added:
|
||||
self.signal_update.emit(task_id, progress, substatus)
|
||||
self._add_lock.unlock()
|
||||
|
||||
def update_output(self, task_id: str, output: str):
|
||||
self._add_lock.lock()
|
||||
if task_id in self._tasks_added:
|
||||
self.signal_output.emit(task_id, output)
|
||||
self._add_lock.unlock()
|
||||
|
||||
def register_task(self, id_: str, label: str, icon_path: str):
|
||||
self._add_lock.lock()
|
||||
|
||||
if id_ not in self._tasks_added:
|
||||
self._tasks_added.add(id_)
|
||||
self.signal_register.emit(id_, label, icon_path)
|
||||
|
||||
self._add_lock.unlock()
|
||||
|
||||
def finish_task(self, task_id: str):
|
||||
self._add_lock.lock()
|
||||
task_registered = task_id in self._tasks_added
|
||||
self._add_lock.unlock()
|
||||
|
||||
if not task_registered:
|
||||
return
|
||||
|
||||
self._finish_lock.lock()
|
||||
|
||||
if task_id not in self._tasks_finished:
|
||||
self._tasks_finished.add(task_id)
|
||||
self.signal_finished.emit(task_id)
|
||||
|
||||
self._finish_lock.unlock()
|
||||
|
||||
|
||||
class CheckFinished(QThread):
|
||||
signal_finished = pyqtSignal()
|
||||
|
||||
def __init__(self):
|
||||
super(CheckFinished, self).__init__()
|
||||
self.total = 0
|
||||
self.finished = 0
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
if self.finished == self.total:
|
||||
break
|
||||
|
||||
self.msleep(5)
|
||||
|
||||
self.signal_finished.emit()
|
||||
|
||||
def update(self, finished: int):
|
||||
if finished is not None:
|
||||
self.finished = finished
|
||||
|
||||
|
||||
class EnableSkip(QThread):
|
||||
|
||||
signal_timeout = pyqtSignal()
|
||||
|
||||
def run(self):
|
||||
ti = datetime.datetime.now()
|
||||
|
||||
while True:
|
||||
if datetime.datetime.now() >= ti + datetime.timedelta(seconds=10):
|
||||
self.signal_timeout.emit()
|
||||
break
|
||||
|
||||
self.msleep(100)
|
||||
|
||||
|
||||
class PreparePanel(QWidget, TaskManager):
|
||||
|
||||
signal_status = pyqtSignal(int)
|
||||
signal_password_response = pyqtSignal(bool, str)
|
||||
|
||||
def __init__(self, context: ApplicationContext, manager: SoftwareManager,
|
||||
i18n: I18n, manage_window: QWidget, app_config: dict, force_suggestions: bool = False):
|
||||
super(PreparePanel, self).__init__(flags=Qt.CustomizeWindowHint | Qt.WindowTitleHint)
|
||||
self.i18n = i18n
|
||||
self.context = context
|
||||
self.app_config = app_config
|
||||
self.manage_window = manage_window
|
||||
self.setWindowTitle('{} ({})'.format(__app_name__, self.i18n['prepare_panel.title.start'].lower()))
|
||||
|
||||
self.setLayout(QVBoxLayout())
|
||||
self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
|
||||
self.manager = manager
|
||||
self.tasks = {}
|
||||
self.output = {}
|
||||
self.added_tasks = 0
|
||||
self.ftasks = 0
|
||||
self.started_at = None
|
||||
self.self_close = False
|
||||
self.force_suggestions = force_suggestions
|
||||
|
||||
self.prepare_thread = Prepare(self.context, manager, self.i18n)
|
||||
self.prepare_thread.signal_register.connect(self.register_task)
|
||||
self.prepare_thread.signal_update.connect(self.update_progress)
|
||||
self.prepare_thread.signal_finished.connect(self.finish_task)
|
||||
self.prepare_thread.signal_started.connect(self.start)
|
||||
self.prepare_thread.signal_ask_password.connect(self.ask_root_password)
|
||||
self.prepare_thread.signal_output.connect(self.update_output)
|
||||
self.signal_password_response.connect(self.prepare_thread.set_password_reply)
|
||||
|
||||
self.check_thread = CheckFinished()
|
||||
self.signal_status.connect(self.check_thread.update)
|
||||
self.check_thread.signal_finished.connect(self.finish)
|
||||
|
||||
self.skip_thread = EnableSkip()
|
||||
self.skip_thread.signal_timeout.connect(self._enable_skip_button)
|
||||
|
||||
self.progress_thread = AnimateProgress()
|
||||
self.progress_thread.signal_change.connect(self._change_progress)
|
||||
|
||||
self.label_top = QLabel()
|
||||
self.label_top.setCursor(QCursor(Qt.WaitCursor))
|
||||
self.label_top.setText("{}...".format(self.i18n['prepare_panel.title.start'].capitalize()))
|
||||
self.label_top.setObjectName('prepare_status')
|
||||
self.label_top.setAlignment(Qt.AlignHCenter)
|
||||
self.layout().addWidget(self.label_top)
|
||||
self.layout().addWidget(QLabel())
|
||||
|
||||
self.table = QTableWidget()
|
||||
self.table.setObjectName('tasks')
|
||||
self.table.setCursor(QCursor(Qt.WaitCursor))
|
||||
self.table.setFocusPolicy(Qt.NoFocus)
|
||||
self.table.setShowGrid(False)
|
||||
self.table.verticalHeader().setVisible(False)
|
||||
self.table.horizontalHeader().setVisible(False)
|
||||
self.table.horizontalHeader().setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred)
|
||||
self.table.setColumnCount(4)
|
||||
self.table.setHorizontalHeaderLabels(['' for _ in range(4)])
|
||||
self.table.horizontalScrollBar().setCursor(QCursor(Qt.PointingHandCursor))
|
||||
self.table.verticalScrollBar().setCursor(QCursor(Qt.PointingHandCursor))
|
||||
self.layout().addWidget(self.table)
|
||||
|
||||
self.textarea_details = QPlainTextEdit(self)
|
||||
self.textarea_details.setObjectName('task_details')
|
||||
self.textarea_details.setProperty('console', 'true')
|
||||
self.textarea_details.resize(self.table.size())
|
||||
self.layout().addWidget(self.textarea_details)
|
||||
self.textarea_details.setVisible(False)
|
||||
self.textarea_details.setReadOnly(True)
|
||||
self.textarea_details.setMaximumHeight(100)
|
||||
self.current_output_task = None
|
||||
|
||||
self.bottom_widget = QWidget()
|
||||
self.bottom_widget.setLayout(QHBoxLayout())
|
||||
self.bottom_widget.layout().addStretch()
|
||||
|
||||
bt_hide_details = QPushButton(self.i18n['prepare.bt_hide_details'])
|
||||
bt_hide_details.setObjectName('bt_hide_details')
|
||||
bt_hide_details.clicked.connect(self.hide_output)
|
||||
bt_hide_details.setCursor(QCursor(Qt.PointingHandCursor))
|
||||
self.bottom_widget.layout().addWidget(bt_hide_details)
|
||||
self.bottom_widget.layout().addStretch()
|
||||
self.layout().addWidget(self.bottom_widget)
|
||||
self.bottom_widget.setVisible(False)
|
||||
|
||||
self.bt_bar = QCustomToolbar(policy_height=QSizePolicy.Fixed)
|
||||
self.bt_close = QPushButton(self.i18n['close'].capitalize())
|
||||
self.bt_close.setObjectName('bt_cancel')
|
||||
self.bt_close.setCursor(QCursor(Qt.PointingHandCursor))
|
||||
self.bt_close.clicked.connect(self.close)
|
||||
self.bt_close.setVisible(False)
|
||||
self.bt_bar.add_widget(self.bt_close)
|
||||
self.bt_bar.add_widget(new_spacer())
|
||||
|
||||
self.progress_bar = QProgressBar()
|
||||
self.progress_bar.setObjectName('prepare_progress')
|
||||
self.progress_bar.setTextVisible(False)
|
||||
self.progress_bar.setVisible(False)
|
||||
self.progress_bar.setCursor(QCursor(Qt.WaitCursor))
|
||||
self.bt_bar.add_widget(self.progress_bar)
|
||||
self.bt_bar.add_widget(new_spacer())
|
||||
|
||||
self.bt_skip = QPushButton(self.i18n['prepare_panel.bt_skip.label'].capitalize())
|
||||
self.bt_skip.clicked.connect(self.finish)
|
||||
self.bt_skip.setEnabled(False)
|
||||
self.bt_skip.setCursor(QCursor(Qt.WaitCursor))
|
||||
self.bt_bar.add_widget(self.bt_skip)
|
||||
|
||||
self.layout().addWidget(self.bt_bar)
|
||||
centralize(self)
|
||||
|
||||
def hide_output(self):
|
||||
self.current_output_task = None
|
||||
self.textarea_details.setVisible(False)
|
||||
self.textarea_details.clear()
|
||||
self.bottom_widget.setVisible(False)
|
||||
self._resize_columns()
|
||||
self.setFocus(Qt.NoFocusReason)
|
||||
|
||||
if not self.bt_bar.isVisible():
|
||||
self.bt_bar.setVisible(True)
|
||||
|
||||
def ask_root_password(self):
|
||||
valid, root_pwd = RootDialog.ask_password(self.context, self.i18n)
|
||||
self.signal_password_response.emit(valid, root_pwd)
|
||||
|
||||
def _enable_skip_button(self):
|
||||
self.bt_skip.setEnabled(True)
|
||||
self.bt_skip.setCursor(QCursor(Qt.PointingHandCursor))
|
||||
|
||||
def _change_progress(self, value: int):
|
||||
self.progress_bar.setValue(value)
|
||||
|
||||
def get_table_width(self) -> int:
|
||||
return reduce(operator.add, [self.table.columnWidth(i) for i in range(self.table.columnCount())])
|
||||
|
||||
def _resize_columns(self):
|
||||
header_horizontal = self.table.horizontalHeader()
|
||||
for i in range(self.table.columnCount()):
|
||||
header_horizontal.setSectionResizeMode(i, QHeaderView.ResizeToContents)
|
||||
|
||||
self.resize(int(self.get_table_width() * 1.05), self.sizeHint().height())
|
||||
|
||||
def showEvent(self, event: Optional[QShowEvent]) -> None:
|
||||
super().showEvent(event)
|
||||
self.prepare_thread.start()
|
||||
screen_size = get_current_screen_geometry()
|
||||
self.setMinimumWidth(int(screen_size.width() * 0.25))
|
||||
self.setMinimumHeight(int(screen_size.height() * 0.35))
|
||||
self.setMaximumHeight(int(screen_size.height() * 0.95))
|
||||
centralize(self)
|
||||
|
||||
def start(self, tasks: int):
|
||||
self.started_at = time.time()
|
||||
self.check_thread.total = tasks
|
||||
self.check_thread.start()
|
||||
self.skip_thread.start()
|
||||
|
||||
self.progress_thread.start()
|
||||
|
||||
self.bt_close.setVisible(True)
|
||||
self.progress_bar.setVisible(True)
|
||||
|
||||
def closeEvent(self, ev: QCloseEvent):
|
||||
if not self.self_close:
|
||||
QCoreApplication.exit()
|
||||
|
||||
def register_task(self, id_: str, label: str, icon_path: str):
|
||||
self.added_tasks += 1
|
||||
self.table.setRowCount(self.added_tasks)
|
||||
task_row = self.added_tasks - 1
|
||||
|
||||
icon_widget = QWidget()
|
||||
icon_widget.setProperty('container', 'true')
|
||||
icon_widget.setLayout(QHBoxLayout())
|
||||
icon_widget.layout().setContentsMargins(10, 0, 10, 0)
|
||||
|
||||
bt_icon = QToolButton()
|
||||
bt_icon.setObjectName('bt_task')
|
||||
bt_icon.setCursor(QCursor(Qt.WaitCursor))
|
||||
bt_icon.setEnabled(False)
|
||||
bt_icon.setToolTip(self.i18n['prepare.bt_icon.no_output'])
|
||||
|
||||
if icon_path:
|
||||
bt_icon.setIcon(QIcon(icon_path))
|
||||
|
||||
def _show_output():
|
||||
lines = self.output[id_]
|
||||
|
||||
if lines:
|
||||
self.current_output_task = id_
|
||||
self.textarea_details.clear()
|
||||
self.textarea_details.setVisible(True)
|
||||
|
||||
for l in lines:
|
||||
self.textarea_details.appendPlainText(l)
|
||||
|
||||
self.bottom_widget.setVisible(True)
|
||||
|
||||
self.setFocus(Qt.NoFocusReason)
|
||||
|
||||
if self.bt_bar.isVisible():
|
||||
self.bt_bar.setVisible(False)
|
||||
|
||||
bt_icon.clicked.connect(_show_output)
|
||||
icon_widget.layout().addWidget(bt_icon)
|
||||
|
||||
self.table.setCellWidget(task_row, 0, icon_widget)
|
||||
|
||||
lb_status = QLabel(label)
|
||||
lb_status.setObjectName('task_status')
|
||||
lb_status.setProperty('status', 'running')
|
||||
lb_status.setCursor(Qt.WaitCursor)
|
||||
lb_status.setMinimumWidth(50)
|
||||
lb_status.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
|
||||
lb_status_col = 1
|
||||
self.table.setCellWidget(task_row, lb_status_col, lb_status)
|
||||
|
||||
lb_progress = QLabel('{0:.2f}'.format(0) + '%')
|
||||
lb_progress.setObjectName('task_progress')
|
||||
lb_progress.setProperty('status', 'running')
|
||||
lb_progress.setCursor(Qt.WaitCursor)
|
||||
lb_progress.setContentsMargins(10, 0, 10, 0)
|
||||
lb_progress.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
|
||||
lb_progress_col = 2
|
||||
|
||||
self.table.setCellWidget(task_row, lb_progress_col, lb_progress)
|
||||
|
||||
lb_sub = QLabel()
|
||||
lb_sub.setObjectName('task_substatus')
|
||||
lb_sub.setCursor(Qt.WaitCursor)
|
||||
lb_sub.setContentsMargins(10, 0, 10, 0)
|
||||
lb_sub.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
|
||||
lb_sub.setMinimumWidth(50)
|
||||
self.table.setCellWidget(task_row, 3, lb_sub)
|
||||
|
||||
self.tasks[id_] = {'bt_icon': bt_icon,
|
||||
'lb_status': lb_status,
|
||||
'lb_status_col': lb_status_col,
|
||||
'lb_prog': lb_progress,
|
||||
'lb_prog_col': lb_progress_col,
|
||||
'progress': 0,
|
||||
'lb_sub': lb_sub,
|
||||
'finished': False,
|
||||
'row': task_row}
|
||||
|
||||
def update_progress(self, task_id: str, progress: float, substatus: str):
|
||||
task = self.tasks[task_id]
|
||||
|
||||
if progress != task['progress']:
|
||||
task['progress'] = progress
|
||||
task['lb_prog'].setText('{0:.2f}'.format(progress) + '%')
|
||||
|
||||
if substatus:
|
||||
task['lb_sub'].setText('({})'.format(substatus))
|
||||
else:
|
||||
task['lb_sub'].setText('')
|
||||
|
||||
self._resize_columns()
|
||||
|
||||
def update_output(self, task_id: str, output: str):
|
||||
full_output = self.output.get(task_id)
|
||||
|
||||
if full_output is None:
|
||||
full_output = []
|
||||
self.output[task_id] = full_output
|
||||
task = self.tasks[task_id]
|
||||
task['bt_icon'].setEnabled(True)
|
||||
task['bt_icon'].setCursor(QCursor(Qt.PointingHandCursor))
|
||||
task['bt_icon'].setToolTip(self.i18n['prepare.bt_icon.output'])
|
||||
|
||||
full_output.append(output)
|
||||
|
||||
if self.current_output_task == task_id:
|
||||
self.textarea_details.appendPlainText(output)
|
||||
|
||||
def finish_task(self, task_id: str):
|
||||
task = self.tasks[task_id]
|
||||
|
||||
for key in ('lb_prog', 'lb_status', 'lb_sub'):
|
||||
label = task[key]
|
||||
label.setProperty('status', 'done')
|
||||
label.style().unpolish(label)
|
||||
label.style().polish(label)
|
||||
label.update()
|
||||
|
||||
task['finished'] = True
|
||||
self._resize_columns()
|
||||
|
||||
self.ftasks += 1
|
||||
self.signal_status.emit(self.ftasks)
|
||||
|
||||
def finish(self):
|
||||
now = time.time()
|
||||
self.context.logger.info("{0} tasks finished in {1:.9f} seconds".format(self.ftasks, (now - self.started_at)))
|
||||
if self.isVisible():
|
||||
self.manage_window.show()
|
||||
|
||||
if self.force_suggestions:
|
||||
self.manage_window.begin_load_suggestions(filter_installed=True)
|
||||
elif self.app_config['boot']['load_apps']:
|
||||
self.manage_window.begin_refresh_packages()
|
||||
else:
|
||||
self.manage_window.load_without_packages()
|
||||
|
||||
self.self_close = True
|
||||
self.close()
|
||||
@@ -1,44 +0,0 @@
|
||||
from typing import Optional, Union
|
||||
|
||||
from PyQt5.QtCore import Qt, QRect, QPoint
|
||||
from PyQt5.QtGui import QIcon, QPixmap
|
||||
from PyQt5.QtWidgets import QWidget, QApplication, QDesktopWidget
|
||||
|
||||
from bauh.view.util import resource
|
||||
|
||||
desktop: Optional[QDesktopWidget] = None
|
||||
|
||||
|
||||
def centralize(widget: QWidget, align_top_left: bool = True):
|
||||
widget_frame = widget.frameGeometry()
|
||||
screen_geometry = get_current_screen_geometry()
|
||||
widget_frame.moveCenter(screen_geometry.center())
|
||||
|
||||
if align_top_left:
|
||||
widget.move(widget_frame.topLeft())
|
||||
|
||||
|
||||
def load_icon(path: str, width: int, height: int = None) -> QIcon:
|
||||
return QIcon(QPixmap(path).scaled(width, height if height else width, Qt.KeepAspectRatio, Qt.SmoothTransformation))
|
||||
|
||||
|
||||
def load_resource_icon(path: str, width: int, height: int = None) -> QIcon:
|
||||
return load_icon(resource.get_path(path), width, height)
|
||||
|
||||
|
||||
def measure_based_on_width(percent: float) -> int:
|
||||
return round(percent * QApplication.primaryScreen().size().width())
|
||||
|
||||
|
||||
def measure_based_on_height(percent: float) -> int:
|
||||
return round(percent * QApplication.primaryScreen().size().height())
|
||||
|
||||
|
||||
def get_current_screen_geometry(source_widget: Optional[Union[QWidget, QPoint]] = None) -> QRect:
|
||||
global desktop
|
||||
|
||||
if not desktop:
|
||||
desktop = QDesktopWidget()
|
||||
|
||||
current_screen_idx = desktop.screenNumber(source_widget if source_widget else desktop.cursor().pos())
|
||||
return desktop.screen(current_screen_idx).geometry()
|
||||
@@ -1,178 +0,0 @@
|
||||
import os
|
||||
import traceback
|
||||
from typing import Tuple, Optional
|
||||
|
||||
from PyQt5.QtCore import Qt, QThread, pyqtSignal
|
||||
from PyQt5.QtGui import QCursor
|
||||
from PyQt5.QtWidgets import QLineEdit, QApplication, QDialog, QPushButton, QVBoxLayout, \
|
||||
QSizePolicy, QToolBar, QLabel
|
||||
|
||||
from bauh.api.abstract.context import ApplicationContext
|
||||
from bauh.commons.system import new_subprocess
|
||||
from bauh.view.core.config import CoreConfigManager
|
||||
from bauh.view.qt.components import QtComponentsManager, new_spacer
|
||||
from bauh.view.util import util
|
||||
from bauh.view.util.translation import I18n
|
||||
|
||||
ACTION_ASK_ROOT = 99
|
||||
|
||||
|
||||
class ValidatePassword(QThread):
|
||||
|
||||
signal_valid = pyqtSignal(bool)
|
||||
|
||||
def __init__(self, password: Optional[str] = None):
|
||||
super(ValidatePassword, self).__init__()
|
||||
self.password = password
|
||||
|
||||
def run(self):
|
||||
if isinstance(self.password, str):
|
||||
try:
|
||||
valid = validate_password(self.password)
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
valid = False
|
||||
|
||||
self.signal_valid.emit(valid)
|
||||
|
||||
|
||||
class RootDialog(QDialog):
|
||||
|
||||
def __init__(self, i18n: I18n, max_tries: int = 3):
|
||||
super(RootDialog, self).__init__(flags=Qt.CustomizeWindowHint | Qt.WindowTitleHint)
|
||||
self.i18n = i18n
|
||||
self.max_tries = max_tries
|
||||
self.tries = 0
|
||||
self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
|
||||
self.setWindowIcon(util.get_default_icon()[1])
|
||||
self.setWindowTitle(i18n['popup.root.title'])
|
||||
self.setLayout(QVBoxLayout())
|
||||
self.setMinimumWidth(300)
|
||||
|
||||
self.label_msg = QLabel(i18n['popup.root.msg'])
|
||||
self.label_msg.setObjectName('message')
|
||||
self.layout().addWidget(self.label_msg)
|
||||
|
||||
self.input_password = QLineEdit()
|
||||
self.input_password.setObjectName('password')
|
||||
self.layout().addWidget(self.input_password)
|
||||
|
||||
self.label_error = QLabel()
|
||||
self.label_error.setProperty('error', 'true')
|
||||
self.layout().addWidget(self.label_error)
|
||||
self.label_error.hide()
|
||||
|
||||
self.lower_bar = QToolBar()
|
||||
self.layout().addWidget(self.lower_bar)
|
||||
|
||||
self.lower_bar.addWidget(new_spacer())
|
||||
self.bt_ok = QPushButton(i18n['popup.root.continue'])
|
||||
self.bt_ok.setDefault(True)
|
||||
self.bt_ok.setAutoDefault(True)
|
||||
self.bt_ok.setCursor(QCursor(Qt.PointingHandCursor))
|
||||
self.bt_ok.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)
|
||||
self.bt_ok.setObjectName('ok')
|
||||
self.bt_ok.clicked.connect(self._validate_password)
|
||||
self.lower_bar.addWidget(self.bt_ok)
|
||||
|
||||
self.bt_cancel = QPushButton()
|
||||
self.bt_cancel.setText(i18n['popup.button.cancel'])
|
||||
|
||||
self.bt_cancel.setCursor(QCursor(Qt.PointingHandCursor))
|
||||
self.bt_cancel.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)
|
||||
self.bt_cancel.setObjectName('bt_cancel')
|
||||
self.bt_cancel.clicked.connect(self.close)
|
||||
self.lower_bar.addWidget(self.bt_cancel)
|
||||
self.lower_bar.addWidget(new_spacer())
|
||||
|
||||
self.valid = False
|
||||
self.password = None
|
||||
self.validate_password = ValidatePassword()
|
||||
self.validate_password.signal_valid.connect(self._handle_password_validated)
|
||||
|
||||
def _validate_password(self):
|
||||
password = self.input_password.text()
|
||||
|
||||
if isinstance(password, str):
|
||||
self.password = password
|
||||
self.tries += 1
|
||||
self.bt_ok.setEnabled(False)
|
||||
self.bt_cancel.setEnabled(False)
|
||||
self.input_password.setEnabled(False)
|
||||
self.label_error.setText('')
|
||||
|
||||
QApplication.setOverrideCursor(QCursor(Qt.WaitCursor))
|
||||
self.validate_password.password = password
|
||||
self.validate_password.start()
|
||||
|
||||
def _handle_password_validated(self, valid: bool):
|
||||
self.valid = valid
|
||||
|
||||
QApplication.restoreOverrideCursor()
|
||||
tries_ended = self.tries == self.max_tries
|
||||
|
||||
if not self.valid:
|
||||
self.label_error.show()
|
||||
self.bt_cancel.setEnabled(True)
|
||||
|
||||
if tries_ended:
|
||||
self.bt_cancel.setText(self.i18n['close'].capitalize())
|
||||
self.label_error.setText(self.i18n['popup.root.bad_password.last_try'])
|
||||
self.bt_cancel.setFocus()
|
||||
else:
|
||||
self.label_error.setText(self.i18n['popup.root.bad_password.body'])
|
||||
self.bt_ok.setEnabled(True)
|
||||
self.input_password.setEnabled(True)
|
||||
self.input_password.setFocus()
|
||||
else:
|
||||
self.close()
|
||||
|
||||
@staticmethod
|
||||
def ask_password(context: ApplicationContext, i18n: I18n, app_config: Optional[dict] = None,
|
||||
comp_manager: Optional[QtComponentsManager] = None, tries: int = 3) -> Tuple[bool, Optional[str]]:
|
||||
|
||||
current_config = CoreConfigManager().get_config() if not app_config else app_config
|
||||
|
||||
store_password = bool(current_config['store_root_password'])
|
||||
|
||||
if store_password and isinstance(context.root_password, str) and validate_password(context.root_password):
|
||||
return True, context.root_password
|
||||
|
||||
if comp_manager:
|
||||
comp_manager.save_states(state_id=ACTION_ASK_ROOT, only_visible=True)
|
||||
comp_manager.disable_visible()
|
||||
|
||||
diag = RootDialog(i18n=i18n, max_tries=tries)
|
||||
diag.exec()
|
||||
password = diag.password
|
||||
|
||||
if comp_manager:
|
||||
comp_manager.restore_state(ACTION_ASK_ROOT)
|
||||
|
||||
if isinstance(password, str) and store_password:
|
||||
context.root_password = password
|
||||
|
||||
return (True, password) if diag.valid else (False, None)
|
||||
|
||||
|
||||
def is_root():
|
||||
return os.getuid() == 0
|
||||
|
||||
|
||||
def validate_password(password: str) -> bool:
|
||||
clean = new_subprocess(['sudo', '-k']).stdout
|
||||
echo = new_subprocess(['echo', password], stdin=clean).stdout
|
||||
|
||||
validate = new_subprocess(['sudo', '-S', '-v'], stdin=echo)
|
||||
|
||||
for o in validate.stdout:
|
||||
pass
|
||||
|
||||
for o in validate.stderr:
|
||||
if o:
|
||||
line = o.decode()
|
||||
|
||||
if 'incorrect password attempt' in line:
|
||||
return False
|
||||
|
||||
return True
|
||||
@@ -1,216 +0,0 @@
|
||||
import logging
|
||||
import traceback
|
||||
from io import BytesIO
|
||||
from threading import Thread
|
||||
from typing import List, Dict
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtGui import QIcon, QPixmap, QCursor
|
||||
from PyQt5.QtWidgets import QDialog, QLabel, QPushButton, QVBoxLayout, QProgressBar, QApplication, QWidget, \
|
||||
QSizePolicy, QHBoxLayout
|
||||
|
||||
from bauh.api.abstract.cache import MemoryCache
|
||||
from bauh.api.http import HttpClient
|
||||
from bauh.view.qt import qt_utils
|
||||
from bauh.view.qt.components import new_spacer
|
||||
from bauh.view.qt.thread import AnimateProgress
|
||||
from bauh.view.qt.view_model import PackageView
|
||||
from bauh.view.util.translation import I18n
|
||||
|
||||
|
||||
class ScreenshotsDialog(QDialog):
|
||||
|
||||
def __init__(self, pkg: PackageView, http_client: HttpClient, icon_cache: MemoryCache, i18n: I18n, screenshots: List[QPixmap], logger: logging.Logger):
|
||||
super(ScreenshotsDialog, self).__init__()
|
||||
self.setWindowTitle(str(pkg))
|
||||
self.screenshots = screenshots
|
||||
self.logger = logger
|
||||
self.loaded_imgs = []
|
||||
self.download_threads = []
|
||||
self.i18n = i18n
|
||||
self.http_client = http_client
|
||||
self.progress_bar = QProgressBar()
|
||||
self.progress_bar.setObjectName('progress_screenshots')
|
||||
self.progress_bar.setCursor(QCursor(Qt.WaitCursor))
|
||||
self.progress_bar.setMaximumHeight(10 if QApplication.instance().style().objectName().lower() == 'windows' else 6)
|
||||
self.progress_bar.setTextVisible(False)
|
||||
self.thread_progress = AnimateProgress()
|
||||
self.thread_progress.signal_change.connect(self._update_progress)
|
||||
self.thread_progress.start()
|
||||
|
||||
# THERE ARE CRASHES WITH SOME RARE ICONS ( like insomnia ). IT CAN BE A QT BUG. IN THE MEANTIME, ONLY THE TYPE ICON WILL BE RENDERED
|
||||
#
|
||||
# icon_data = icon_cache.get(pkg.model.icon_url)
|
||||
#
|
||||
# if icon_data and icon_data.get('icon'):
|
||||
# self.setWindowIcon(icon_data.get('icon'))
|
||||
# else:
|
||||
# self.setWindowIcon(QIcon(pkg.model.get_type_icon_path()))
|
||||
self.setWindowIcon(QIcon(pkg.model.get_type_icon_path()))
|
||||
self.setLayout(QVBoxLayout())
|
||||
|
||||
self.bt_close = QPushButton(self.i18n['screenshots.bt_close'])
|
||||
self.bt_close.setObjectName('close')
|
||||
self.bt_close.clicked.connect(self.close)
|
||||
self.bt_close.setCursor(QCursor(Qt.PointingHandCursor))
|
||||
|
||||
self.upper_buttons = QWidget()
|
||||
self.upper_buttons.setObjectName('upper_buttons')
|
||||
self.upper_buttons.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)
|
||||
self.upper_buttons.setContentsMargins(0, 0, 0, 0)
|
||||
self.upper_buttons.setLayout(QHBoxLayout())
|
||||
self.upper_buttons.layout().setAlignment(Qt.AlignRight)
|
||||
self.upper_buttons.layout().addWidget(self.bt_close)
|
||||
self.layout().addWidget(self.upper_buttons)
|
||||
|
||||
self.layout().addWidget(new_spacer())
|
||||
|
||||
self.img = QLabel()
|
||||
self.img.setObjectName('image')
|
||||
self.layout().addWidget(self.img)
|
||||
self.layout().addWidget(new_spacer())
|
||||
|
||||
self.container_buttons = QWidget()
|
||||
self.container_buttons.setObjectName('buttons_container')
|
||||
self.container_buttons.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)
|
||||
self.container_buttons.setContentsMargins(0, 0, 0, 0)
|
||||
self.container_buttons.setLayout(QHBoxLayout())
|
||||
|
||||
self.bt_back = QPushButton(' < ' + self.i18n['screenshots.bt_back.label'].capitalize())
|
||||
self.bt_back.setObjectName('back')
|
||||
self.bt_back.setProperty('control', 'true')
|
||||
self.bt_back.setCursor(QCursor(Qt.PointingHandCursor))
|
||||
self.bt_back.clicked.connect(self.back)
|
||||
self.container_buttons.layout().addWidget(self.bt_back)
|
||||
self.container_buttons.layout().addWidget(new_spacer())
|
||||
|
||||
self.img_label = QLabel()
|
||||
self.img_label.setObjectName("image_label")
|
||||
self.container_buttons.layout().addWidget(self.img_label)
|
||||
|
||||
self.container_buttons.layout().addWidget(self.progress_bar)
|
||||
self.container_buttons.layout().addWidget(new_spacer())
|
||||
|
||||
self.bt_next = QPushButton(self.i18n['screenshots.bt_next.label'].capitalize() + ' > ')
|
||||
self.bt_next.setObjectName('next')
|
||||
self.bt_next.setProperty('control', 'true')
|
||||
self.bt_next.setCursor(QCursor(Qt.PointingHandCursor))
|
||||
self.bt_next.clicked.connect(self.next)
|
||||
self.container_buttons.layout().addWidget(self.bt_next)
|
||||
self.download_progress: Dict[int, float] = dict()
|
||||
|
||||
self.layout().addWidget(self.container_buttons)
|
||||
|
||||
self.img_idx = 0
|
||||
self.max_img_width = 800
|
||||
self.max_img_height = 600
|
||||
|
||||
for idx, s in enumerate(self.screenshots):
|
||||
t = Thread(target=self._download_img, args=(idx, s), daemon=True)
|
||||
t.start()
|
||||
|
||||
self.resize(self.max_img_width + 5, self.max_img_height + 5)
|
||||
self._load_img(self.img_idx)
|
||||
qt_utils.centralize(self)
|
||||
|
||||
def _update_progress(self, val: int):
|
||||
self.progress_bar.setValue(val)
|
||||
|
||||
def _load_img(self, img_idx: int):
|
||||
if img_idx != self.img_idx:
|
||||
return
|
||||
|
||||
if len(self.loaded_imgs) > self.img_idx:
|
||||
img = self.loaded_imgs[self.img_idx]
|
||||
|
||||
if isinstance(img, QPixmap):
|
||||
self.img_label.setText(f'{self.img_idx + 1}/{len(self.screenshots)}')
|
||||
self.img.setText('')
|
||||
self.img.setPixmap(img)
|
||||
else:
|
||||
self.img.setText(img)
|
||||
self.img.setPixmap(QPixmap())
|
||||
|
||||
self.img.unsetCursor()
|
||||
self.thread_progress.stop = True
|
||||
self.progress_bar.setVisible(False)
|
||||
else:
|
||||
self.img.setPixmap(QPixmap())
|
||||
self.img.setCursor(QCursor(Qt.WaitCursor))
|
||||
|
||||
progress = self.download_progress.get(self.img_idx, 0)
|
||||
self.img.setText(f"{self.i18n['screenshots.image.loading']} "
|
||||
f"{self.img_idx + 1}/{len(self.screenshots)} ({progress:.2f}%)")
|
||||
self.progress_bar.setVisible(True)
|
||||
self.thread_progress.start()
|
||||
|
||||
if len(self.screenshots) == 1:
|
||||
self.bt_back.setVisible(False)
|
||||
self.bt_next.setVisible(False)
|
||||
else:
|
||||
self.bt_back.setEnabled(self.img_idx != 0)
|
||||
self.bt_next.setEnabled(self.img_idx != len(self.screenshots) - 1)
|
||||
|
||||
def _handle_download_exception(self, idx: int, url: str):
|
||||
self.logger.error(f"Unexpected exception while downloading screenshot from '{url}'")
|
||||
traceback.print_exc()
|
||||
self.loaded_imgs.append(self.i18n["screenshots.download.no_response"])
|
||||
self._load_img(idx)
|
||||
|
||||
def _download_img(self, idx: int, url: str):
|
||||
self.logger.info(f"Downloading image [{idx}] from {url}")
|
||||
|
||||
try:
|
||||
res = self.http_client.get(url=url, stream=True)
|
||||
except Exception:
|
||||
self._handle_download_exception(idx, url)
|
||||
return
|
||||
|
||||
if not res:
|
||||
self.logger.info(f"Could not retrieve image [{idx}] from '{url}'")
|
||||
self.loaded_imgs.append(self.i18n["screenshots.download.no_response"])
|
||||
self._load_img(idx)
|
||||
return
|
||||
|
||||
try:
|
||||
content_length = int(res.headers.get("content-length", 0))
|
||||
except Exception:
|
||||
content_length = 0
|
||||
self.logger.warning(f"Could not retrieve the content-length for file '{url}'")
|
||||
|
||||
if content_length <= 0:
|
||||
self.logger.warning(f"Image [{idx}] has no content ({url})")
|
||||
self.loaded_imgs.append(self.i18n['screenshots.download.no_content'])
|
||||
self._load_img(idx)
|
||||
else:
|
||||
byte_stream = BytesIO()
|
||||
|
||||
total_downloaded = 0
|
||||
try:
|
||||
for data in res.iter_content(chunk_size=1024):
|
||||
byte_stream.write(data)
|
||||
total_downloaded += len(data)
|
||||
self.download_progress[idx] = (total_downloaded / content_length) * 100
|
||||
self._load_img(idx)
|
||||
except Exception:
|
||||
self._handle_download_exception(idx, url)
|
||||
return
|
||||
|
||||
self.logger.info(f"Image [{idx}] successfully downloaded ({url})")
|
||||
pixmap = QPixmap()
|
||||
pixmap.loadFromData(byte_stream.getvalue())
|
||||
|
||||
if pixmap.size().height() > self.max_img_height or pixmap.size().width() > self.max_img_width:
|
||||
pixmap = pixmap.scaled(self.max_img_width, self.max_img_height, Qt.KeepAspectRatio,
|
||||
Qt.SmoothTransformation)
|
||||
|
||||
self.loaded_imgs.append(pixmap)
|
||||
self._load_img(idx)
|
||||
|
||||
def back(self):
|
||||
self.img_idx -= 1
|
||||
self._load_img(self.img_idx)
|
||||
|
||||
def next(self):
|
||||
self.img_idx += 1
|
||||
self._load_img(self.img_idx)
|
||||
@@ -1,149 +0,0 @@
|
||||
import gc
|
||||
from io import StringIO
|
||||
from typing import Optional
|
||||
|
||||
from PyQt5.QtCore import Qt, QCoreApplication, QThread, pyqtSignal
|
||||
from PyQt5.QtGui import QCursor, QShowEvent
|
||||
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QSizePolicy, QPushButton, QHBoxLayout, QApplication
|
||||
|
||||
from bauh import __app_name__
|
||||
from bauh.api.abstract.controller import SoftwareManager
|
||||
from bauh.api.abstract.view import MessageType
|
||||
from bauh.view.core.controller import GenericSoftwareManager
|
||||
from bauh.view.qt import dialog
|
||||
from bauh.view.qt.components import to_widget, new_spacer
|
||||
from bauh.view.qt.dialog import ConfirmationDialog
|
||||
from bauh.view.qt.qt_utils import centralize
|
||||
from bauh.view.util import util
|
||||
from bauh.view.util.translation import I18n
|
||||
|
||||
|
||||
class ReloadManagePanel(QThread):
|
||||
|
||||
signal_finished = pyqtSignal()
|
||||
|
||||
def __init__(self, manager: SoftwareManager):
|
||||
super(ReloadManagePanel, self).__init__()
|
||||
self.manager = manager
|
||||
|
||||
def run(self):
|
||||
if isinstance(self.manager, GenericSoftwareManager):
|
||||
self.manager.reset_cache()
|
||||
|
||||
self.manager.prepare(task_manager=None, root_password=None, internet_available=None)
|
||||
self.signal_finished.emit()
|
||||
|
||||
|
||||
class SettingsWindow(QWidget):
|
||||
|
||||
def __init__(self, manager: SoftwareManager, i18n: I18n, window: QWidget, parent: Optional[QWidget] = None):
|
||||
super(SettingsWindow, self).__init__(parent=parent, flags=Qt.CustomizeWindowHint | Qt.WindowTitleHint)
|
||||
self.setWindowTitle(f"{i18n['settings'].capitalize()} ({__app_name__})")
|
||||
self.setLayout(QVBoxLayout())
|
||||
self.manager = manager
|
||||
self.i18n = i18n
|
||||
self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
|
||||
self.window = window
|
||||
|
||||
self.settings_model = tuple(v for v in self.manager.get_settings())[0].component
|
||||
|
||||
self.tab_group = to_widget(self.settings_model, i18n)
|
||||
self.tab_group.setObjectName('settings')
|
||||
self.layout().addWidget(self.tab_group)
|
||||
|
||||
lower_container = QWidget()
|
||||
lower_container.setObjectName('lower_container')
|
||||
lower_container.setProperty('container', 'true')
|
||||
lower_container.setLayout(QHBoxLayout())
|
||||
lower_container.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)
|
||||
|
||||
self.bt_close = QPushButton()
|
||||
self.bt_close.setObjectName('cancel')
|
||||
self.bt_close.setAutoDefault(True)
|
||||
self.bt_close.setCursor(QCursor(Qt.PointingHandCursor))
|
||||
self.bt_close.setText(self.i18n['close'].capitalize())
|
||||
self.bt_close.clicked.connect(lambda: self.close())
|
||||
lower_container.layout().addWidget(self.bt_close)
|
||||
|
||||
lower_container.layout().addWidget(new_spacer())
|
||||
|
||||
self.bt_change = QPushButton()
|
||||
self.bt_change.setAutoDefault(True)
|
||||
self.bt_change.setObjectName('ok')
|
||||
self.bt_change.setCursor(QCursor(Qt.PointingHandCursor))
|
||||
self.bt_change.setText(self.i18n['change'].capitalize())
|
||||
self.bt_change.clicked.connect(self._save_settings)
|
||||
lower_container.layout().addWidget(self.bt_change)
|
||||
|
||||
self.layout().addWidget(lower_container)
|
||||
|
||||
self.thread_reload_panel = ReloadManagePanel(manager=manager)
|
||||
self.thread_reload_panel.signal_finished.connect(self._reload_manage_panel)
|
||||
centralize(self)
|
||||
|
||||
def showEvent(self, event: Optional[QShowEvent]):
|
||||
super(SettingsWindow, self).showEvent(event)
|
||||
self.setMinimumWidth(int(self.sizeHint().width()))
|
||||
centralize(self)
|
||||
|
||||
def closeEvent(self, event):
|
||||
if self.window and self.window.settings_window == self:
|
||||
self.deleteLater()
|
||||
self.window.settings_window = None
|
||||
elif not self.window:
|
||||
QCoreApplication.exit()
|
||||
|
||||
gc.collect()
|
||||
|
||||
def handle_display(self):
|
||||
if self.isMinimized():
|
||||
self.setWindowState(Qt.WindowNoState)
|
||||
elif self.isHidden():
|
||||
self.show()
|
||||
else:
|
||||
self.setWindowState(self.windowState() and Qt.WindowMinimized or Qt.WindowActive)
|
||||
|
||||
def _save_settings(self):
|
||||
self.tab_group.setEnabled(False)
|
||||
self.bt_change.setEnabled(False)
|
||||
self.bt_close.setEnabled(False)
|
||||
|
||||
success, warnings = self.manager.save_settings(self.settings_model)
|
||||
|
||||
if success:
|
||||
if not self.window:
|
||||
ConfirmationDialog(title=self.i18n['success'].capitalize(),
|
||||
body=f"<p>{self.i18n['settings.changed.success.warning']}</p>",
|
||||
i18n=self.i18n,
|
||||
confirmation_label=self.i18n['ok'],
|
||||
confirmation_icon=False,
|
||||
deny_button=False).ask()
|
||||
QCoreApplication.exit()
|
||||
elif ConfirmationDialog(title=self.i18n['warning'].capitalize(),
|
||||
body=f"<p>{self.i18n['settings.changed.success.warning']}</p>"
|
||||
f"<p>{self.i18n['settings.changed.success.reboot']}</p>",
|
||||
i18n=self.i18n).ask():
|
||||
self.close()
|
||||
util.restart_app()
|
||||
else:
|
||||
self.thread_reload_panel.start()
|
||||
QApplication.setOverrideCursor(Qt.WaitCursor)
|
||||
else:
|
||||
msg = StringIO()
|
||||
msg.write(f"<p>{self.i18n['settings.error']}</p>")
|
||||
|
||||
for w in warnings:
|
||||
msg.write(f'<p style="font-weight: bold">* {w}</p><br/>')
|
||||
|
||||
dialog.show_message(title=self.i18n['warning'].capitalize(), body=msg.getvalue(), type_=MessageType.WARNING)
|
||||
|
||||
self.tab_group.setEnabled(True)
|
||||
self.bt_change.setEnabled(True)
|
||||
self.bt_close.setEnabled(True)
|
||||
|
||||
def _reload_manage_panel(self):
|
||||
if self.window and self.window.isVisible():
|
||||
self.window.reload()
|
||||
|
||||
QApplication.restoreOverrideCursor()
|
||||
self.close()
|
||||
@@ -1,275 +0,0 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import traceback
|
||||
from io import StringIO
|
||||
from subprocess import Popen
|
||||
from threading import Lock, Thread
|
||||
from typing import List
|
||||
|
||||
from PyQt5.QtCore import QThread, pyqtSignal, QCoreApplication, QSize
|
||||
from PyQt5.QtGui import QIcon
|
||||
from PyQt5.QtWidgets import QSystemTrayIcon, QMenu
|
||||
|
||||
from bauh import __app_name__, ROOT_DIR
|
||||
from bauh.api.abstract.model import PackageUpdate
|
||||
from bauh.api.http import HttpClient
|
||||
from bauh.commons import system
|
||||
from bauh.context import generate_i18n
|
||||
from bauh.view.core.tray_client import TRAY_CHECK_FILE
|
||||
from bauh.view.core.update import check_for_update
|
||||
from bauh.view.qt.about import AboutDialog
|
||||
from bauh.view.qt.qt_utils import load_resource_icon
|
||||
from bauh.view.util import util, resource
|
||||
from bauh.view.util.translation import I18n
|
||||
|
||||
CLI_NAME = f'{__app_name__}-cli'
|
||||
|
||||
|
||||
def get_cli_path() -> str:
|
||||
if os.getenv('APPIMAGE'):
|
||||
return f"{os.environ['APPRUN_STARTUP_EXEC_PATH']} {os.environ['APPDIR']}usr/bin/{CLI_NAME}"
|
||||
|
||||
venv = os.getenv('VIRTUAL_ENV')
|
||||
|
||||
if venv:
|
||||
cli_path = f'{venv}/bin/{CLI_NAME}'
|
||||
|
||||
if os.path.exists(cli_path):
|
||||
return cli_path
|
||||
elif not sys.executable.startswith('/usr'):
|
||||
cli_path = f'{sys.prefix}/bin/{CLI_NAME}'
|
||||
|
||||
if os.path.exists(cli_path):
|
||||
return cli_path
|
||||
else:
|
||||
return shutil.which(CLI_NAME)
|
||||
|
||||
|
||||
def list_updates(logger: logging.Logger) -> List[PackageUpdate]:
|
||||
cli_path = get_cli_path()
|
||||
if cli_path:
|
||||
exitcode, output = system.execute(f'{cli_path} updates -f json')
|
||||
|
||||
if exitcode != 0:
|
||||
output_log = output.replace('\n', ' ') if output else ' '
|
||||
logger.warning(f'Command "{CLI_NAME} updates" returned an unexpected exitcode ({exitcode}). Output: {output_log}')
|
||||
elif output:
|
||||
return [PackageUpdate(pkg_id=o['id'], name=o['name'], version=o['version'], pkg_type=o['type']) for o in json.loads(output)]
|
||||
else:
|
||||
logger.info("No updates found")
|
||||
|
||||
else:
|
||||
logger.warning(f'"{CLI_NAME}" seems not to be installed')
|
||||
|
||||
return []
|
||||
|
||||
|
||||
class UpdateCheck(QThread):
|
||||
|
||||
signal = pyqtSignal(list)
|
||||
|
||||
def __init__(self, check_interval: int, lock: Lock, check_file: bool, logger: logging.Logger, parent=None):
|
||||
super(UpdateCheck, self).__init__(parent)
|
||||
self.check_interval = check_interval
|
||||
self.lock = lock
|
||||
self.check_file = check_file
|
||||
self.logger = logger
|
||||
|
||||
def _notify_updates(self):
|
||||
with self.lock:
|
||||
updates = list_updates(self.logger)
|
||||
|
||||
if updates is not None:
|
||||
self.signal.emit(updates)
|
||||
|
||||
self.sleep(int(self.check_interval * 60))
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
if self.check_file:
|
||||
if os.path.exists(TRAY_CHECK_FILE):
|
||||
self._notify_updates()
|
||||
try:
|
||||
os.remove(TRAY_CHECK_FILE)
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
else:
|
||||
self.sleep(self.check_interval)
|
||||
else:
|
||||
self._notify_updates()
|
||||
|
||||
|
||||
class AppUpdateCheck(QThread):
|
||||
|
||||
def __init__(self, http_client: HttpClient, logger: logging.Logger, i18n: I18n, interval: int = 300):
|
||||
super(AppUpdateCheck, self).__init__()
|
||||
self.interval = interval
|
||||
self.http_client = http_client
|
||||
self.logger = logger
|
||||
self.i18n = i18n
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
update_msg = check_for_update(http_client=self.http_client, logger=self.logger, i18n=self.i18n, tray=True)
|
||||
|
||||
if update_msg:
|
||||
util.notify_user(msg=update_msg)
|
||||
|
||||
self.sleep(self.interval)
|
||||
|
||||
|
||||
class TrayIcon(QSystemTrayIcon):
|
||||
|
||||
def __init__(self, config: dict, screen_size: QSize, logger: logging.Logger, manage_process: Popen = None, settings_process: Popen = None):
|
||||
super(TrayIcon, self).__init__()
|
||||
self.app_config = config
|
||||
self.i18n = generate_i18n(config, resource.get_path('locale/tray'))
|
||||
self.screen_size = screen_size
|
||||
self.manage_process = manage_process
|
||||
self.settings_process = settings_process
|
||||
self.logger = logger
|
||||
self.http_client = HttpClient(logger=logger)
|
||||
|
||||
if config['ui']['tray']['default_icon']:
|
||||
self.icon_default = QIcon(config['ui']['tray']['default_icon'])
|
||||
else:
|
||||
self.icon_default = QIcon.fromTheme(f'{__app_name__}_tray_default')
|
||||
if self.icon_default.isNull():
|
||||
self.icon_default = QIcon.fromTheme('bauh_tray_default')
|
||||
|
||||
if self.icon_default.isNull():
|
||||
self.icon_default = load_resource_icon('img/logo.svg', 24)
|
||||
|
||||
if config['ui']['tray']['updates_icon']:
|
||||
self.icon_updates = QIcon(config['ui']['tray']['updates_icon'])
|
||||
else:
|
||||
self.icon_updates = QIcon.fromTheme(f'{__app_name__}_tray_updates')
|
||||
if self.icon_updates.isNull():
|
||||
self.icon_updates = QIcon.fromTheme('bauh_tray_updates')
|
||||
|
||||
if self.icon_updates.isNull():
|
||||
self.icon_updates = load_resource_icon('img/logo_update.svg', 24)
|
||||
|
||||
self.setIcon(self.icon_default)
|
||||
|
||||
self.menu = QMenu()
|
||||
|
||||
self.action_manage = self.menu.addAction(self.i18n['tray.action.manage'])
|
||||
self.action_manage.triggered.connect(self.show_manage_window)
|
||||
|
||||
self.action_settings = self.menu.addAction(self.i18n['tray.settings'].capitalize())
|
||||
self.action_settings.triggered.connect(self.show_settings_window)
|
||||
|
||||
self.action_about = self.menu.addAction(self.i18n['tray.action.about'])
|
||||
self.action_about.triggered.connect(self.show_about)
|
||||
|
||||
self.action_exit = self.menu.addAction(self.i18n['tray.action.exit'])
|
||||
self.action_exit.triggered.connect(lambda: QCoreApplication.exit())
|
||||
|
||||
self.setContextMenu(self.menu)
|
||||
|
||||
self.manage_window = None
|
||||
self.dialog_about = None
|
||||
self.settings_window = None
|
||||
|
||||
self.check_lock = Lock()
|
||||
self.check_thread = UpdateCheck(check_interval=int(config['updates']['check_interval']), check_file=False, lock=self.check_lock, logger=logger)
|
||||
self.check_thread.signal.connect(self.notify_updates)
|
||||
self.check_thread.start()
|
||||
|
||||
self.recheck_thread = UpdateCheck(check_interval=5, check_file=True, lock=self.check_lock, logger=logger)
|
||||
self.recheck_thread.signal.connect(self.notify_updates)
|
||||
self.recheck_thread.start()
|
||||
|
||||
self.update_thread = AppUpdateCheck(http_client=self.http_client, logger=self.logger, i18n=self.i18n)
|
||||
self.update_thread.start()
|
||||
|
||||
self.last_updates = set()
|
||||
self.update_notification = bool(config['system']['notifications'])
|
||||
self.lock_notify = Lock()
|
||||
|
||||
self.activated.connect(self.handle_click)
|
||||
self.set_default_tooltip()
|
||||
|
||||
def set_default_tooltip(self):
|
||||
self.setToolTip(f"{self.i18n['tray.action.manage']} ({__app_name__})".lower())
|
||||
|
||||
def handle_click(self, reason):
|
||||
if reason == self.Trigger:
|
||||
self.show_manage_window()
|
||||
|
||||
def verify_updates(self, notify_user: bool = True):
|
||||
Thread(target=self._verify_updates, args=(notify_user,), daemon=True).start()
|
||||
|
||||
def _verify_updates(self, notify_user: bool):
|
||||
self.notify_updates(self.manager.list_updates(), notify_user=notify_user)
|
||||
|
||||
def notify_updates(self, updates: List[PackageUpdate], notify_user: bool = True):
|
||||
self.lock_notify.acquire()
|
||||
|
||||
try:
|
||||
if len(updates) > 0:
|
||||
self.logger.info(f"{len(updates)} updates available")
|
||||
update_keys = {f'{up.type}:{up.id}:{up.version}' for up in updates}
|
||||
|
||||
new_icon = self.icon_updates
|
||||
|
||||
if update_keys.difference(self.last_updates):
|
||||
self.last_updates = update_keys
|
||||
n_updates = len(updates)
|
||||
ups_by_type = {}
|
||||
|
||||
for key in update_keys:
|
||||
ptype = key.split(':')[0]
|
||||
count = ups_by_type.get(ptype)
|
||||
count = 1 if count is None else count + 1
|
||||
ups_by_type[ptype] = count
|
||||
|
||||
msg = StringIO()
|
||||
msg.write(self.i18n[f"notification.update{'' if n_updates == 1 else 's'}"].format(n_updates))
|
||||
|
||||
if len(ups_by_type) > 1:
|
||||
for ptype in sorted(ups_by_type):
|
||||
msg.write(f'\n * {ptype} ({ups_by_type[ptype]})')
|
||||
|
||||
msg.seek(0)
|
||||
msg = msg.read()
|
||||
self.setToolTip(msg)
|
||||
|
||||
if self.update_notification and notify_user:
|
||||
util.notify_user(msg=msg)
|
||||
|
||||
else:
|
||||
self.last_updates.clear()
|
||||
new_icon = self.icon_default
|
||||
self.set_default_tooltip()
|
||||
|
||||
if self.icon().cacheKey() != new_icon.cacheKey(): # changes the icon if needed
|
||||
self.setIcon(new_icon)
|
||||
|
||||
finally:
|
||||
self.lock_notify.release()
|
||||
|
||||
def show_manage_window(self):
|
||||
if self.manage_process is None:
|
||||
self.manage_process = Popen([sys.executable, f'{ROOT_DIR}/app.py'])
|
||||
elif self.manage_process.poll() is not None: # it means it has finished
|
||||
self.manage_process = None
|
||||
self.show_manage_window()
|
||||
|
||||
def show_settings_window(self):
|
||||
if self.settings_process is None:
|
||||
self.settings_process = Popen([sys.executable, f'{ROOT_DIR}/app.py', '--settings'])
|
||||
elif self.settings_process.poll() is not None: # it means it has finished
|
||||
self.settings_process = None
|
||||
self.show_settings_window()
|
||||
|
||||
def show_about(self):
|
||||
if self.dialog_about is None:
|
||||
self.dialog_about = AboutDialog(self.app_config)
|
||||
|
||||
if self.dialog_about.isHidden():
|
||||
self.dialog_about.show()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,139 +0,0 @@
|
||||
from collections import defaultdict
|
||||
from typing import Dict, List, Generator, Tuple, Optional, Union
|
||||
|
||||
from bauh.view.qt.commons import PackageFilters
|
||||
from bauh.view.qt.view_model import PackageView
|
||||
|
||||
|
||||
def new_character_idx() -> Dict[str, List[PackageView]]:
|
||||
return defaultdict(list)
|
||||
|
||||
|
||||
def new_category_idx() -> Dict[str, Dict[str, List[PackageView]]]:
|
||||
return defaultdict(new_character_idx)
|
||||
|
||||
|
||||
def new_type_index() -> Dict[str, Dict[str, Dict[str, List[PackageView]]]]:
|
||||
return defaultdict(new_category_idx)
|
||||
|
||||
|
||||
def new_verified_index() -> Dict[int, Dict[str, Dict[str, Dict[str, List[PackageView]]]]]:
|
||||
return defaultdict(new_type_index)
|
||||
|
||||
|
||||
def new_update_index() -> Dict[int, Dict[int, Dict[str, Dict[str, Dict[str, List[PackageView]]]]]]:
|
||||
return defaultdict(new_verified_index)
|
||||
|
||||
|
||||
def new_app_index() -> Dict[int, Dict[int, Dict[int, Dict[str, Dict[str, Dict[str, List[PackageView]]]]]]]:
|
||||
return defaultdict(new_update_index)
|
||||
|
||||
|
||||
def new_package_index() -> Dict[int, Dict[int, Dict[int, Dict[int, Dict[str, Dict[str, Dict[str, List[PackageView]]]]]]]]:
|
||||
return defaultdict(new_app_index)
|
||||
|
||||
|
||||
def add_to_index(pkgv: PackageView, index: dict) -> None:
|
||||
# root keys: (1) installed | (0) not installed
|
||||
root_idx = index[1 if pkgv.model.installed else 0]
|
||||
|
||||
# app keys: (1) app | (0) not app
|
||||
app_lvl = root_idx[1 if pkgv.model.is_application() else 0]
|
||||
|
||||
# update keys: (1) update | (0) no update
|
||||
if pkgv.model.installed and pkgv.model.update and not pkgv.model.is_update_ignored():
|
||||
update_lvl = app_lvl[1]
|
||||
else:
|
||||
update_lvl = app_lvl[0]
|
||||
|
||||
# verified keys: (1) verified | (0) unverified
|
||||
verified_lvl = update_lvl[1 if pkgv.model.is_trustable() else 0]
|
||||
|
||||
norm_name = pkgv.name.strip().lower()
|
||||
starts_with_chars = tuple(norm_name[0:i] for i in range(1, len(norm_name) + 1))
|
||||
|
||||
for cat in ("any", *(pkgv.model.categories if pkgv.model.categories else tuple())):
|
||||
category = cat.lower().strip()
|
||||
|
||||
# any type > specific category > any character (None)
|
||||
verified_lvl["any"][category][None].append(pkgv)
|
||||
|
||||
# any type > specific category > characters (start
|
||||
for chars in starts_with_chars:
|
||||
verified_lvl["any"][category][chars].append(pkgv)
|
||||
|
||||
type_lvl = verified_lvl[pkgv.model.get_type()]
|
||||
|
||||
# specific type > specific category > any character (None)
|
||||
type_lvl[category][None].append(pkgv)
|
||||
|
||||
# specific type > any category > first character
|
||||
for chars in starts_with_chars:
|
||||
type_lvl[category][chars].append(pkgv)
|
||||
|
||||
|
||||
def generate_queries(filters: PackageFilters) -> Generator[Tuple[Optional[Union[int, str]], ...], None, None]:
|
||||
chars_query = None
|
||||
|
||||
if filters.name:
|
||||
chars_query = filters.name.strip().lower()
|
||||
|
||||
installed_queries = (1,) if filters.only_installed else (1, 0)
|
||||
apps_queries = (1,) if filters.only_apps else (1, 0)
|
||||
updates_queries = (1,) if filters.only_updates else (1, 0)
|
||||
verified_queries = (1,) if filters.only_verified else (1, 0)
|
||||
|
||||
for installed in installed_queries:
|
||||
for app in apps_queries:
|
||||
for update in updates_queries:
|
||||
for verified in verified_queries:
|
||||
yield installed, app, update, verified, filters.type, filters.category, chars_query
|
||||
|
||||
|
||||
def query_packages(index: dict, filters: PackageFilters) -> Generator[PackageView, None, None]:
|
||||
yield_count = 0
|
||||
yield_limit = filters.display_limit if filters.display_limit and filters.display_limit > 0 else -1
|
||||
|
||||
queries = tuple(generate_queries(filters))
|
||||
|
||||
yielded_pkgs = defaultdict(set)
|
||||
|
||||
for query in queries:
|
||||
packages = index[query[0]][query[1]][query[2]][query[3]][query[4]][query[5]][query[6]]
|
||||
|
||||
for pkgv in packages:
|
||||
yield pkgv
|
||||
yield_count += 1
|
||||
yielded_pkgs[pkgv.model.get_type()].add(pkgv.model.id)
|
||||
|
||||
# checking if the package display limit has been reached
|
||||
if 0 < yield_limit <= yield_count:
|
||||
break
|
||||
|
||||
# if there is a limit and the number of yielded packages is not reached, performs also a "contains" query
|
||||
# checking if the queries target "any character" (none), if so, there is no need to perform the "contains" query
|
||||
any_char_query = next((True for q in queries if q[-1] is None), False)
|
||||
|
||||
if not any_char_query and 0 < yield_limit > yield_count:
|
||||
for query in queries:
|
||||
# checking if the package display limit has been reached
|
||||
if 0 < yield_limit <= yield_count:
|
||||
break
|
||||
|
||||
packages = index[query[0]][query[1]][query[2]][query[3]][query[4]][query[5]][None]
|
||||
|
||||
for pkgv in packages:
|
||||
# checking if the package has already been yielded
|
||||
yield_type_idx = yielded_pkgs.get(pkgv.model.get_type())
|
||||
if yield_type_idx and pkgv.model.id in yield_type_idx:
|
||||
continue
|
||||
|
||||
# checking if the package name contains the chars query
|
||||
if query[6] in pkgv.model.name.lower():
|
||||
yield pkgv
|
||||
yield_count += 1
|
||||
yielded_pkgs[pkgv.model.get_type()].add(pkgv.model.id)
|
||||
|
||||
# checking if the package display limit has been reached
|
||||
if 0 < yield_limit <= yield_count:
|
||||
break
|
||||
@@ -1,44 +0,0 @@
|
||||
from enum import Enum
|
||||
|
||||
from bauh.api.abstract.model import SoftwarePackage, PackageStatus
|
||||
from bauh.view.util.translation import I18n
|
||||
|
||||
|
||||
class PackageViewStatus(Enum):
|
||||
LOADING = 0
|
||||
READY = 1
|
||||
|
||||
|
||||
def get_type_label(type_: str, gem: str, i18n: I18n) -> str:
|
||||
type_label = 'gem.{}.type.{}.label'.format(gem, type_.lower())
|
||||
return i18n.get(type_label, type_.capitalize()).strip()
|
||||
|
||||
|
||||
class PackageView:
|
||||
|
||||
def __init__(self, model: SoftwarePackage, i18n: I18n):
|
||||
self.model = None
|
||||
self.update_checked = None
|
||||
self.status = None
|
||||
self.update_model(model)
|
||||
self.table_index = -1
|
||||
self.i18n = i18n
|
||||
|
||||
def get_type_label(self) -> str:
|
||||
return get_type_label(self.model.get_type(), self.model.gem_name, self.i18n)
|
||||
|
||||
def update_model(self, model: SoftwarePackage):
|
||||
self.model = model
|
||||
self.update_checked = model.update
|
||||
self.status = PackageViewStatus.LOADING if model.status == PackageStatus.LOADING_DATA else PackageViewStatus.READY
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self.model.name
|
||||
|
||||
def __repr__(self):
|
||||
return '{} ({})'.format(self.model.name, self.get_type_label())
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, PackageView):
|
||||
return self.model == other.model
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user