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:
Sebastian Palencsar
2026-06-27 10:02:45 +02:00
parent c56b8a2473
commit da377aecdc
198 changed files with 4543 additions and 4644 deletions

View File

@@ -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()

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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()))

View File

@@ -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)

View File

@@ -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()

View File

@@ -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()

View File

@@ -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

View File

@@ -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)

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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