mirror of
https://github.com/spalencsar/bearhub.git
synced 2026-07-07 15:14:15 +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,702 +0,0 @@
|
||||
import shutil
|
||||
import time
|
||||
import traceback
|
||||
from subprocess import Popen, STDOUT
|
||||
from threading import Thread
|
||||
from typing import List, Set, Type, Tuple, Dict, Optional, Generator, Callable
|
||||
|
||||
from bauh.api.abstract.controller import SoftwareManager, SearchResult, ApplicationContext, UpgradeRequirements, \
|
||||
UpgradeRequirement, TransactionResult, SoftwareAction, SettingsView, SettingsController
|
||||
from bauh.api.abstract.disk import DiskCacheLoader
|
||||
from bauh.api.abstract.handler import ProcessWatcher, TaskManager
|
||||
from bauh.api.abstract.model import SoftwarePackage, PackageUpdate, PackageHistory, PackageSuggestion, \
|
||||
CustomSoftwareAction
|
||||
from bauh.api.abstract.view import TabGroupComponent, MessageType
|
||||
from bauh.api.exception import NoInternetException
|
||||
from bauh.commons.boot import CreateConfigFile
|
||||
from bauh.commons.html import bold
|
||||
from bauh.commons.regex import RE_URL
|
||||
from bauh.commons.util import sanitize_command_input
|
||||
from bauh.view.core.config import CoreConfigManager
|
||||
from bauh.view.core.settings import GenericSettingsManager
|
||||
from bauh.view.core.update import check_for_update
|
||||
from bauh.view.util import resource
|
||||
from bauh.view.util.resource import get_path
|
||||
from bauh.view.util.util import clean_app_files, restart_app
|
||||
|
||||
|
||||
class GenericUpgradeRequirements(UpgradeRequirements):
|
||||
|
||||
def __init__(self, to_install: List[UpgradeRequirement], to_remove: List[UpgradeRequirement],
|
||||
to_upgrade: List[UpgradeRequirement], cannot_upgrade: List[SoftwarePackage],
|
||||
sub_requirements: Dict[SoftwareManager, UpgradeRequirements]):
|
||||
super(GenericUpgradeRequirements, self).__init__(to_install=to_install, to_upgrade=to_upgrade,
|
||||
to_remove=to_remove, cannot_upgrade=cannot_upgrade)
|
||||
self.sub_requirements = sub_requirements
|
||||
|
||||
|
||||
class GenericSoftwareManager(SoftwareManager, SettingsController):
|
||||
|
||||
def __init__(self, managers: List[SoftwareManager], context: ApplicationContext, config: dict,
|
||||
force_suggestions: bool = False):
|
||||
|
||||
super(GenericSoftwareManager, self).__init__(context=context)
|
||||
self.managers = managers
|
||||
self.map = {t: m for m in self.managers for t in m.get_managed_types()}
|
||||
self._available_cache = {} if config['system']['single_dependency_checking'] else None
|
||||
self.thread_prepare = None
|
||||
self.i18n = context.i18n
|
||||
self.disk_loader_factory = context.disk_loader_factory
|
||||
self.logger = context.logger
|
||||
self._already_prepared = []
|
||||
self.working_managers = []
|
||||
self.config = config
|
||||
self.settings_manager: Optional[GenericSettingsManager] = None
|
||||
self.http_client = context.http_client
|
||||
self.configman = CoreConfigManager()
|
||||
self._action_reset: Optional[CustomSoftwareAction] = None
|
||||
self._dynamic_extra_actions: Optional[Dict[CustomSoftwareAction, Callable[[dict], bool]]] = None
|
||||
self.force_suggestions = force_suggestions
|
||||
|
||||
@property
|
||||
def dynamic_extra_actions(self) -> Dict[CustomSoftwareAction, Callable[[dict], bool]]:
|
||||
if self._dynamic_extra_actions is None:
|
||||
self._dynamic_extra_actions = {
|
||||
CustomSoftwareAction(i18n_label_key='action.backups',
|
||||
i18n_status_key='action.backups.status',
|
||||
i18n_description_key='action.backups.desc',
|
||||
manager_method='launch_timeshift',
|
||||
manager=self,
|
||||
icon_path='timeshift',
|
||||
requires_root=False,
|
||||
refresh=False): self.is_backups_action_available
|
||||
}
|
||||
|
||||
return self._dynamic_extra_actions
|
||||
|
||||
@property
|
||||
def action_reset(self) -> CustomSoftwareAction:
|
||||
if self._action_reset is None:
|
||||
self._action_reset = CustomSoftwareAction(i18n_label_key='action.reset',
|
||||
i18n_status_key='action.reset.status',
|
||||
i18n_description_key='action.reset.desc',
|
||||
manager_method='reset',
|
||||
icon_path=resource.get_path('img/logo.svg'),
|
||||
requires_root=False,
|
||||
manager=self,
|
||||
refresh=False)
|
||||
|
||||
return self._action_reset
|
||||
|
||||
def _is_timeshift_launcher_available(self) -> bool:
|
||||
return bool(shutil.which('timeshift-launcher'))
|
||||
|
||||
def is_backups_action_available(self, app_config: dict) -> bool:
|
||||
return bool(app_config['backup']['enabled']) and self._is_timeshift_launcher_available()
|
||||
|
||||
def reset_cache(self):
|
||||
if self._available_cache is not None:
|
||||
self._available_cache = {}
|
||||
self.working_managers.clear()
|
||||
|
||||
def launch_timeshift(self, root_password: Optional[str], watcher: ProcessWatcher):
|
||||
if self._is_timeshift_launcher_available():
|
||||
try:
|
||||
Popen(['timeshift-launcher'], stderr=STDOUT)
|
||||
return True
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
watcher.show_message(title=self.i18n["error"].capitalize(),
|
||||
body=self.i18n['action.backups.tool_error'].format(bold('Timeshift')),
|
||||
type_=MessageType.ERROR)
|
||||
return False
|
||||
else:
|
||||
watcher.show_message(title=self.i18n["error"].capitalize(),
|
||||
body=self.i18n['action.backups.tool_error'].format(bold('Timeshift')),
|
||||
type_=MessageType.ERROR)
|
||||
return False
|
||||
|
||||
def _can_work(self, man: SoftwareManager):
|
||||
if self._available_cache is not None:
|
||||
available = False
|
||||
for t in man.get_managed_types():
|
||||
available = self._available_cache.get(t)
|
||||
|
||||
if available is None:
|
||||
available = man.is_enabled() and man.can_work()[0]
|
||||
self._available_cache[t] = available
|
||||
|
||||
if available:
|
||||
available = True
|
||||
else:
|
||||
available = man.is_enabled() and man.can_work()[0]
|
||||
|
||||
if available:
|
||||
if man not in self.working_managers:
|
||||
self.working_managers.append(man)
|
||||
else:
|
||||
if man in self.working_managers:
|
||||
self.working_managers.remove(man)
|
||||
|
||||
return available
|
||||
|
||||
def _search(self, word: str, is_url: bool, man: SoftwareManager, disk_loader, res: SearchResult):
|
||||
mti = time.time()
|
||||
apps_found = man.search(words=word, disk_loader=disk_loader, is_url=is_url, limit=-1)
|
||||
mtf = time.time()
|
||||
self.logger.info(f'{man.__class__.__name__} took {mtf - mti:.8f} seconds')
|
||||
|
||||
res.installed.extend(apps_found.installed)
|
||||
res.new.extend(apps_found.new)
|
||||
|
||||
def search(self, words: str, disk_loader: DiskCacheLoader = None, limit: int = -1, is_url: bool = False) -> SearchResult:
|
||||
ti = time.time()
|
||||
self._wait_to_be_ready()
|
||||
|
||||
res = SearchResult.empty()
|
||||
|
||||
if self.context.is_internet_available():
|
||||
norm_query = sanitize_command_input(words).lower()
|
||||
self.logger.info(f"Search query: {norm_query}")
|
||||
|
||||
if norm_query:
|
||||
is_url = bool(RE_URL.match(norm_query))
|
||||
disk_loader = self.disk_loader_factory.new()
|
||||
disk_loader.start()
|
||||
|
||||
threads = []
|
||||
|
||||
for man in self.managers:
|
||||
if self._can_work(man):
|
||||
t = Thread(target=self._search, args=(norm_query, is_url, man, disk_loader, res), daemon=True)
|
||||
t.start()
|
||||
threads.append(t)
|
||||
|
||||
for t in threads:
|
||||
t.join()
|
||||
|
||||
if disk_loader:
|
||||
disk_loader.stop_working()
|
||||
disk_loader.join()
|
||||
else:
|
||||
raise NoInternetException()
|
||||
|
||||
res.update_total()
|
||||
tf = time.time()
|
||||
self.logger.info(f'Took {tf - ti:.8f} seconds')
|
||||
return res
|
||||
|
||||
def _wait_to_be_ready(self):
|
||||
if self.thread_prepare:
|
||||
self.thread_prepare.join()
|
||||
self.thread_prepare = None
|
||||
|
||||
def set_enabled(self, enabled: bool):
|
||||
pass
|
||||
|
||||
def can_work(self) -> Tuple[bool, Optional[str]]:
|
||||
return True, None
|
||||
|
||||
def _get_package_lower_name(self, pkg: SoftwarePackage):
|
||||
return pkg.name.lower()
|
||||
|
||||
def _fill_read_installed(self, man: SoftwareManager, disk_loader: DiskCacheLoader, internet_available: bool,
|
||||
output: List[SearchResult]):
|
||||
mti = time.time()
|
||||
man_res = man.read_installed(disk_loader=disk_loader, pkg_types=None, internet_available=internet_available,
|
||||
limit=-1, only_apps=False)
|
||||
mtf = time.time()
|
||||
self.logger.info(f'{man.__class__.__name__} took {mtf - mti:.4f} seconds')
|
||||
output.append(man_res)
|
||||
|
||||
def read_installed(self, disk_loader: DiskCacheLoader = None, limit: int = -1, only_apps: bool = False, pkg_types: Set[Type[SoftwarePackage]] = None, internet_available: bool = None) -> SearchResult:
|
||||
ti = time.time()
|
||||
self._wait_to_be_ready()
|
||||
|
||||
res = SearchResult([], None, 0)
|
||||
|
||||
disk_loader = None
|
||||
|
||||
net_available = self.context.is_internet_available()
|
||||
read_threads = list()
|
||||
results = list()
|
||||
|
||||
if not pkg_types: # any type
|
||||
for man in self.managers:
|
||||
if self._can_work(man):
|
||||
if not disk_loader:
|
||||
disk_loader = self.disk_loader_factory.new()
|
||||
disk_loader.start()
|
||||
|
||||
t = Thread(target=self._fill_read_installed,
|
||||
args=(man, disk_loader, net_available, results),
|
||||
daemon=True)
|
||||
t.start()
|
||||
read_threads.append(t)
|
||||
else:
|
||||
man_already_used = []
|
||||
|
||||
for t in pkg_types:
|
||||
man = self.map.get(t)
|
||||
if man and (man not in man_already_used) and self._can_work(man):
|
||||
|
||||
if not disk_loader:
|
||||
disk_loader = self.disk_loader_factory.new()
|
||||
disk_loader.start()
|
||||
|
||||
t = Thread(target=self._fill_read_installed,
|
||||
args=(man, disk_loader, net_available, results),
|
||||
daemon=True)
|
||||
t.start()
|
||||
read_threads.append(t)
|
||||
|
||||
for t in read_threads:
|
||||
t.join()
|
||||
|
||||
if disk_loader:
|
||||
disk_loader.stop_working()
|
||||
disk_loader.join()
|
||||
|
||||
for result in results:
|
||||
if result.installed:
|
||||
res.installed.extend(result.installed)
|
||||
res.total += result.total
|
||||
|
||||
if res.installed:
|
||||
for p in res.installed:
|
||||
if p.is_update_ignored():
|
||||
if p.categories is None:
|
||||
p.categories = ['updates_ignored']
|
||||
elif 'updates_ignored' not in p.categories:
|
||||
self._add_category(p, 'updates_ignored')
|
||||
|
||||
res.installed.sort(key=self._get_package_lower_name)
|
||||
|
||||
tf = time.time()
|
||||
self.logger.info(f'Took {tf - ti:.2f} seconds')
|
||||
return res
|
||||
|
||||
def _add_category(self, pkg: SoftwarePackage, category: str):
|
||||
if isinstance(pkg.categories, tuple):
|
||||
pkg.categories = tuple((*pkg.categories, category))
|
||||
elif isinstance(pkg.categories, list):
|
||||
pkg.categories.append(category)
|
||||
elif isinstance(pkg.categories, set):
|
||||
pkg.categories.add(category)
|
||||
|
||||
def downgrade(self, app: SoftwarePackage, root_password: Optional[str], handler: ProcessWatcher) -> bool:
|
||||
man = self._get_manager_for(app)
|
||||
|
||||
if man and app.can_be_downgraded():
|
||||
mti = time.time()
|
||||
res = man.downgrade(app, root_password, handler)
|
||||
mtf = time.time()
|
||||
self.logger.info(f'Took {mtf - mti:.2f} seconds')
|
||||
return res
|
||||
else:
|
||||
raise Exception(f"Downgrading is not possible for {app.__class__.__name__}")
|
||||
|
||||
def clean_cache_for(self, app: SoftwarePackage):
|
||||
man = self._get_manager_for(app)
|
||||
|
||||
if man:
|
||||
return man.clean_cache_for(app)
|
||||
|
||||
def upgrade(self, requirements: GenericUpgradeRequirements, root_password: Optional[str], handler: ProcessWatcher) -> bool:
|
||||
for man, man_reqs in requirements.sub_requirements.items():
|
||||
res = man.upgrade(man_reqs, root_password, handler)
|
||||
|
||||
if not res:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _fill_post_transaction_status(self, pkg: SoftwarePackage, installed: bool):
|
||||
pkg.installed = installed
|
||||
pkg.update = False
|
||||
|
||||
if pkg.latest_version:
|
||||
pkg.version = pkg.latest_version
|
||||
|
||||
def _update_post_transaction_status(self, res: TransactionResult):
|
||||
if res.success:
|
||||
if res.installed:
|
||||
for p in res.installed:
|
||||
self._fill_post_transaction_status(p, True)
|
||||
if res.removed:
|
||||
for p in res.removed:
|
||||
self._fill_post_transaction_status(p, False)
|
||||
|
||||
def uninstall(self, pkg: SoftwarePackage, root_password: Optional[str], handler: ProcessWatcher, disk_loader: DiskCacheLoader = None) -> TransactionResult:
|
||||
man = self._get_manager_for(pkg)
|
||||
|
||||
if man:
|
||||
ti = time.time()
|
||||
disk_loader = self.disk_loader_factory.new()
|
||||
disk_loader.start()
|
||||
self.logger.info(f"Uninstalling {pkg.name}")
|
||||
try:
|
||||
res = man.uninstall(pkg, root_password, handler, disk_loader)
|
||||
disk_loader.stop_working()
|
||||
disk_loader.join()
|
||||
self._update_post_transaction_status(res)
|
||||
return res
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
return TransactionResult(success=False, installed=[], removed=[])
|
||||
finally:
|
||||
tf = time.time()
|
||||
self.logger.info(f'Uninstallation of {pkg} took {(tf - ti) / 60:.2f} minutes')
|
||||
|
||||
def install(self, app: SoftwarePackage, root_password: Optional[str], disk_loader: DiskCacheLoader, handler: ProcessWatcher) -> TransactionResult:
|
||||
man = self._get_manager_for(app)
|
||||
|
||||
if man:
|
||||
ti = time.time()
|
||||
disk_loader = self.disk_loader_factory.new()
|
||||
disk_loader.start()
|
||||
try:
|
||||
self.logger.info(f'Installing {app}')
|
||||
res = man.install(app, root_password, disk_loader, handler)
|
||||
disk_loader.stop_working()
|
||||
disk_loader.join()
|
||||
self._update_post_transaction_status(res)
|
||||
return res
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
return TransactionResult(success=False, installed=[], removed=[])
|
||||
finally:
|
||||
tf = time.time()
|
||||
self.logger.info(f'Installation of {app} took {(tf - ti) / 60:.2f} minutes')
|
||||
|
||||
def get_info(self, app: SoftwarePackage):
|
||||
man = self._get_manager_for(app)
|
||||
|
||||
if man:
|
||||
return man.get_info(app)
|
||||
|
||||
def get_history(self, app: SoftwarePackage) -> PackageHistory:
|
||||
man = self._get_manager_for(app)
|
||||
|
||||
if man:
|
||||
mti = time.time()
|
||||
history = man.get_history(app)
|
||||
mtf = time.time()
|
||||
self.logger.info(f'{man.__class__.__name__} took {mtf - mti:.2f} seconds')
|
||||
return history
|
||||
|
||||
def get_managed_types(self) -> Set[Type[SoftwarePackage]]:
|
||||
available_types = set()
|
||||
|
||||
for man in self.get_working_managers():
|
||||
available_types.update(man.get_managed_types())
|
||||
|
||||
return available_types
|
||||
|
||||
def is_enabled(self):
|
||||
return True
|
||||
|
||||
def _get_manager_for(self, app: SoftwarePackage) -> SoftwareManager:
|
||||
man = self.map[app.__class__]
|
||||
return man if man and self._can_work(man) else None
|
||||
|
||||
def cache_to_disk(self, pkg: SoftwarePackage, icon_bytes: bytes, only_icon: bool):
|
||||
if pkg.supports_disk_cache():
|
||||
man = self._get_manager_for(pkg)
|
||||
|
||||
if man:
|
||||
return man.cache_to_disk(pkg, icon_bytes=icon_bytes, only_icon=only_icon)
|
||||
|
||||
def requires_root(self, action: SoftwareAction, app: SoftwarePackage) -> bool:
|
||||
if app is None:
|
||||
if self.managers:
|
||||
for man in self.managers:
|
||||
if self._can_work(man):
|
||||
if man.requires_root(action, app):
|
||||
return True
|
||||
return False
|
||||
else:
|
||||
man = self._get_manager_for(app)
|
||||
|
||||
if man:
|
||||
return man.requires_root(action, app)
|
||||
|
||||
def prepare(self, task_manager: TaskManager, root_password: Optional[str], internet_available: bool):
|
||||
ti = time.time()
|
||||
self.logger.info("Initializing")
|
||||
taskman = task_manager if task_manager else TaskManager() # empty task manager to prevent null pointers
|
||||
|
||||
create_config = CreateConfigFile(taskman=taskman, configman=self.configman, i18n=self.i18n,
|
||||
task_icon_path=get_path('img/logo.svg'), logger=self.logger)
|
||||
create_config.start()
|
||||
|
||||
if self.managers:
|
||||
internet_on = self.context.is_internet_available()
|
||||
prepare_tasks = []
|
||||
for man in self.managers:
|
||||
if man not in self._already_prepared and self._can_work(man):
|
||||
t = Thread(target=man.prepare, args=(taskman, root_password, internet_on), daemon=True)
|
||||
t.start()
|
||||
prepare_tasks.append(t)
|
||||
self._already_prepared.append(man)
|
||||
|
||||
for t in prepare_tasks:
|
||||
t.join()
|
||||
|
||||
create_config.join()
|
||||
tf = time.time()
|
||||
self.logger.info(f'Finished ({tf - ti:.2f} seconds)')
|
||||
|
||||
def cache_available_managers(self):
|
||||
if self.managers:
|
||||
for man in self.managers:
|
||||
self._can_work(man)
|
||||
|
||||
def list_updates(self, internet_available: bool = None) -> List[PackageUpdate]:
|
||||
self._wait_to_be_ready()
|
||||
|
||||
updates = []
|
||||
|
||||
if self.managers:
|
||||
net_available = self.context.is_internet_available()
|
||||
|
||||
for man in self.managers:
|
||||
if self._can_work(man):
|
||||
man_updates = man.list_updates(internet_available=net_available)
|
||||
if man_updates:
|
||||
updates.extend(man_updates)
|
||||
|
||||
return updates
|
||||
|
||||
def list_warnings(self, internet_available: bool = None) -> List[str]:
|
||||
warnings = []
|
||||
|
||||
int_available = self.context.is_internet_available()
|
||||
|
||||
if int_available:
|
||||
updates_msg = check_for_update(self.logger, self.http_client, self.i18n)
|
||||
|
||||
if updates_msg:
|
||||
warnings.append(updates_msg)
|
||||
|
||||
if self.managers:
|
||||
for man in self.managers:
|
||||
if self._can_work(man):
|
||||
man_warnings = man.list_warnings(internet_available=int_available)
|
||||
|
||||
if man_warnings:
|
||||
warnings.extend(man_warnings)
|
||||
|
||||
return warnings
|
||||
|
||||
def _fill_suggestions(self, suggestions: list, man: SoftwareManager, limit: int, filter_installed: bool):
|
||||
if self._can_work(man):
|
||||
mti = time.time()
|
||||
man_sugs = man.list_suggestions(limit=limit, filter_installed=filter_installed)
|
||||
mtf = time.time()
|
||||
self.logger.info(f'{man.__class__.__name__} took {mtf - mti:.5f} seconds')
|
||||
|
||||
if man_sugs:
|
||||
if 0 < limit < len(man_sugs):
|
||||
man_sugs = man_sugs[0:limit]
|
||||
|
||||
suggestions.extend(man_sugs)
|
||||
|
||||
def list_suggestions(self, limit: int, filter_installed: bool) -> List[PackageSuggestion]:
|
||||
if self.force_suggestions or bool(self.config['suggestions']['enabled']):
|
||||
if self.managers and self.context.is_internet_available():
|
||||
suggestions, threads = [], []
|
||||
for man in self.managers:
|
||||
t = Thread(target=self._fill_suggestions,
|
||||
args=(suggestions, man, int(self.config['suggestions']['by_type']), filter_installed),
|
||||
daemon=True)
|
||||
t.start()
|
||||
threads.append(t)
|
||||
|
||||
for t in threads:
|
||||
t.join()
|
||||
|
||||
if suggestions:
|
||||
suggestions.sort(key=lambda s: s.priority.value, reverse=True)
|
||||
|
||||
return suggestions
|
||||
return []
|
||||
|
||||
def execute_custom_action(self, action: CustomSoftwareAction, pkg: SoftwarePackage, root_password: Optional[str], watcher: ProcessWatcher):
|
||||
if action.requires_internet and not self.context.is_internet_available():
|
||||
raise NoInternetException()
|
||||
|
||||
man = action.manager if action.manager else self._get_manager_for(pkg)
|
||||
|
||||
if man:
|
||||
return eval(f"man.{action.manager_method}({'pkg=pkg, ' if pkg else ''}root_password=root_password, watcher=watcher)")
|
||||
|
||||
def is_default_enabled(self) -> bool:
|
||||
return True
|
||||
|
||||
def launch(self, pkg: SoftwarePackage):
|
||||
self._wait_to_be_ready()
|
||||
|
||||
man = self._get_manager_for(pkg)
|
||||
|
||||
if man:
|
||||
self.logger.info(f'Launching {pkg}')
|
||||
man.launch(pkg)
|
||||
|
||||
def get_screenshots(self, pkg: SoftwarePackage) -> Generator[str, None, None]:
|
||||
man = self._get_manager_for(pkg)
|
||||
|
||||
if man:
|
||||
yield from man.get_screenshots(pkg)
|
||||
|
||||
def get_working_managers(self):
|
||||
return [m for m in self.managers if self._can_work(m)]
|
||||
|
||||
def get_settings(self) -> Optional[Generator[SettingsView, None, None]]:
|
||||
if self.settings_manager is None:
|
||||
self.settings_manager = GenericSettingsManager(managers=self.managers,
|
||||
working_managers=self.working_managers,
|
||||
configman=self.configman,
|
||||
context=self.context)
|
||||
else:
|
||||
self.settings_manager.managers = self.managers
|
||||
self.settings_manager.working_managers = self.working_managers
|
||||
|
||||
yield SettingsView(self, self.settings_manager.get_settings())
|
||||
|
||||
def save_settings(self, component: TabGroupComponent) -> Tuple[bool, Optional[List[str]]]:
|
||||
return self.settings_manager.save_settings(component)
|
||||
|
||||
def _map_pkgs_by_manager(self, pkgs: List[SoftwarePackage], pkg_filters: list = None) -> Dict[SoftwareManager, List[SoftwarePackage]]:
|
||||
by_manager = {}
|
||||
for pkg in pkgs:
|
||||
if pkg_filters and not all((1 for f in pkg_filters if f(pkg))):
|
||||
continue
|
||||
|
||||
man = self._get_manager_for(pkg)
|
||||
|
||||
if man:
|
||||
man_pkgs = by_manager.get(man)
|
||||
|
||||
if man_pkgs is None:
|
||||
man_pkgs = []
|
||||
by_manager[man] = man_pkgs
|
||||
|
||||
man_pkgs.append(pkg)
|
||||
|
||||
return by_manager
|
||||
|
||||
def get_upgrade_requirements(self, pkgs: List[SoftwarePackage], root_password: Optional[str], watcher: ProcessWatcher) -> UpgradeRequirements:
|
||||
by_manager = self._map_pkgs_by_manager(pkgs)
|
||||
res = GenericUpgradeRequirements([], [], [], [], {})
|
||||
|
||||
if by_manager:
|
||||
for man, pkgs in by_manager.items():
|
||||
ti = time.time()
|
||||
man_reqs = man.get_upgrade_requirements(pkgs, root_password, watcher)
|
||||
tf = time.time()
|
||||
self.logger.info(f'{man.__class__.__name__} took {tf - ti:.2f} seconds')
|
||||
|
||||
if not man_reqs:
|
||||
return # it means the process should be stopped
|
||||
|
||||
if man_reqs:
|
||||
res.sub_requirements[man] = man_reqs
|
||||
if man_reqs.to_install:
|
||||
res.to_install.extend(man_reqs.to_install)
|
||||
|
||||
if man_reqs.to_remove:
|
||||
res.to_remove.extend(man_reqs.to_remove)
|
||||
|
||||
if man_reqs.to_upgrade:
|
||||
res.to_upgrade.extend(man_reqs.to_upgrade)
|
||||
|
||||
if man_reqs.cannot_upgrade:
|
||||
res.cannot_upgrade.extend(man_reqs.cannot_upgrade)
|
||||
|
||||
return res
|
||||
|
||||
def reset(self, root_password: Optional[str], watcher: ProcessWatcher) -> bool:
|
||||
body = f"<p>{self.i18n['action.reset.body_1'].format(bold(self.context.app_name))}</p>" \
|
||||
f"<p>{self.i18n['action.reset.body_2']}</p>"
|
||||
|
||||
if watcher.request_confirmation(title=self.i18n['action.reset'],
|
||||
body=body,
|
||||
confirmation_label=self.i18n['proceed'].capitalize(),
|
||||
deny_label=self.i18n['cancel'].capitalize()):
|
||||
|
||||
try:
|
||||
clean_app_files(managers=self.managers, logs=False)
|
||||
restart_app()
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def gen_custom_actions(self) -> Generator[CustomSoftwareAction, None, None]:
|
||||
if self.managers:
|
||||
working_managers = []
|
||||
|
||||
for man in self.managers:
|
||||
if self._can_work(man):
|
||||
working_managers.append(man)
|
||||
|
||||
if working_managers:
|
||||
working_managers.sort(key=lambda m: m.__class__.__name__)
|
||||
|
||||
for man in working_managers:
|
||||
for action in man.gen_custom_actions():
|
||||
action.manager = man
|
||||
yield action
|
||||
|
||||
app_config = self.configman.get_config()
|
||||
|
||||
for action, available in self.dynamic_extra_actions.items():
|
||||
if available(app_config):
|
||||
yield action
|
||||
|
||||
yield self.action_reset
|
||||
|
||||
def _fill_sizes(self, man: SoftwareManager, pkgs: List[SoftwarePackage]):
|
||||
ti = time.time()
|
||||
man.fill_sizes(pkgs)
|
||||
tf = time.time()
|
||||
self.logger.info(f'{man.__class__.__name__} took {tf - ti:.2f} seconds')
|
||||
|
||||
def fill_sizes(self, pkgs: List[SoftwarePackage]):
|
||||
by_manager = self._map_pkgs_by_manager(pkgs, pkg_filters=[lambda p: p.size is None])
|
||||
|
||||
if by_manager:
|
||||
threads = []
|
||||
for man, man_pkgs in by_manager.items():
|
||||
if man_pkgs:
|
||||
t = Thread(target=self._fill_sizes, args=(man, man_pkgs), daemon=True)
|
||||
t.start()
|
||||
threads.append(t)
|
||||
|
||||
for t in threads:
|
||||
t.join()
|
||||
|
||||
def ignore_update(self, pkg: SoftwarePackage):
|
||||
manager = self._get_manager_for(pkg)
|
||||
|
||||
if manager:
|
||||
manager.ignore_update(pkg)
|
||||
|
||||
if pkg.is_update_ignored():
|
||||
if pkg.categories is None:
|
||||
pkg.categories = ['updates_ignored']
|
||||
elif 'updates_ignored' not in pkg.categories:
|
||||
self._add_category(pkg, 'updates_ignored')
|
||||
|
||||
def revert_ignored_update(self, pkg: SoftwarePackage):
|
||||
manager = self._get_manager_for(pkg)
|
||||
|
||||
if manager:
|
||||
manager.revert_ignored_update(pkg)
|
||||
|
||||
if not pkg.is_update_ignored() and pkg.categories and 'updates_ignored' in pkg.categories:
|
||||
if isinstance(pkg.categories, tuple):
|
||||
pkg.categories = tuple(c for c in pkg.categories if c != 'updates_ignored')
|
||||
else:
|
||||
pkg.categories.remove('updates_ignored')
|
||||
@@ -1,328 +0,0 @@
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import time
|
||||
import traceback
|
||||
from io import StringIO, BytesIO
|
||||
from logging import Logger
|
||||
from math import floor
|
||||
from pathlib import Path
|
||||
from threading import Thread
|
||||
from typing import Optional, Tuple
|
||||
|
||||
from bauh.api.abstract.download import FileDownloader
|
||||
from bauh.api.abstract.handler import ProcessWatcher
|
||||
from bauh.api.http import HttpClient
|
||||
from bauh.commons.html import bold
|
||||
from bauh.commons.system import ProcessHandler, SimpleProcess
|
||||
from bauh.commons.view_utils import get_human_size_str
|
||||
from bauh.view.util.translation import I18n
|
||||
|
||||
RE_HAS_EXTENSION = re.compile(r'.+\.\w+$')
|
||||
|
||||
|
||||
class SelfFileDownloader(FileDownloader):
|
||||
|
||||
def __init__(self, logger: Logger, i18n: I18n, http_client: HttpClient,
|
||||
check_ssl: bool):
|
||||
self._logger = logger
|
||||
self._i18n = i18n
|
||||
self._client = http_client
|
||||
self._ssl = check_ssl
|
||||
|
||||
def is_multithreaded(self) -> bool:
|
||||
return False
|
||||
|
||||
def can_work(self) -> bool:
|
||||
return True
|
||||
|
||||
def get_supported_multithreaded_clients(self) -> Tuple[str, ...]:
|
||||
return tuple()
|
||||
|
||||
def is_multithreaded_client_available(self, name: str) -> bool:
|
||||
return False
|
||||
|
||||
def list_available_multithreaded_clients(self) -> Tuple[str, ...]:
|
||||
return tuple()
|
||||
|
||||
def get_supported_clients(self) -> Tuple[str, ...]:
|
||||
return tuple()
|
||||
|
||||
def download(self, file_url: str, watcher: Optional[ProcessWatcher], output_path: str, cwd: str,
|
||||
root_password: Optional[str] = None, substatus_prefix: str = None, display_file_size: bool = True,
|
||||
max_threads: int = None, known_size: int = None) -> bool:
|
||||
try:
|
||||
res = self._client.get(url=file_url, ignore_ssl=not self._ssl, stream=True)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
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 '{file_url}'")
|
||||
|
||||
file_name = file_url.split("/")[-1]
|
||||
msg = StringIO()
|
||||
msg.write(f"{substatus_prefix} " if substatus_prefix else "")
|
||||
msg.write(f"{self._i18n['downloading']} {bold(file_name)}")
|
||||
base_msg = msg.getvalue()
|
||||
|
||||
byte_stream = BytesIO()
|
||||
total_downloaded = 0
|
||||
known_size = content_length and content_length > 0
|
||||
total_size_str = get_human_size_str(content_length) if known_size > 0 else "?"
|
||||
|
||||
try:
|
||||
for data in res.iter_content(chunk_size=1024):
|
||||
byte_stream.write(data)
|
||||
total_downloaded += len(data)
|
||||
perc = f"({(total_downloaded / content_length) * 100:.2f}%) " if known_size > 0 else ""
|
||||
watcher.change_substatus(f"{perc}{base_msg} ({get_human_size_str(total_downloaded)} / {total_size_str})")
|
||||
except Exception:
|
||||
self._logger.error(f"Unexpected exception while downloading file from '{file_url}'")
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
self._logger.info(f"Writing downloaded file content to disk: {output_path}")
|
||||
|
||||
try:
|
||||
with open(output_path, "wb+") as f:
|
||||
f.write(byte_stream.getvalue())
|
||||
except Exception:
|
||||
self._logger.error(f"Unexpected exception when saving downloaded content to disk: {output_path}")
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class AdaptableFileDownloader(FileDownloader):
|
||||
|
||||
def __init__(self, logger: Logger, multithread_enabled: bool, i18n: I18n, http_client: HttpClient,
|
||||
multithread_client: str, check_ssl: bool):
|
||||
self.logger = logger
|
||||
self.multithread_enabled = multithread_enabled
|
||||
self.i18n = i18n
|
||||
self.http_client = http_client
|
||||
self.supported_multithread_clients = ("aria2", "axel")
|
||||
self.multithread_client = multithread_client
|
||||
self.check_ssl = check_ssl
|
||||
self._self_downloader = SelfFileDownloader(logger=logger,
|
||||
i18n=i18n,
|
||||
http_client=http_client,
|
||||
check_ssl=check_ssl)
|
||||
|
||||
@staticmethod
|
||||
def is_aria2c_available() -> bool:
|
||||
return bool(shutil.which('aria2c'))
|
||||
|
||||
@staticmethod
|
||||
def is_axel_available() -> bool:
|
||||
return bool(shutil.which('axel'))
|
||||
|
||||
def _get_aria2c_process(self, url: str, output_path: str, cwd: str, root_password: Optional[str], threads: int) -> SimpleProcess:
|
||||
cmd = ['aria2c', url,
|
||||
'--no-conf',
|
||||
'-x', '16',
|
||||
'--enable-color=false',
|
||||
'--stderr=true',
|
||||
'--summary-interval=0',
|
||||
'--disable-ipv6',
|
||||
'-k', '1M',
|
||||
'--allow-overwrite=true',
|
||||
'-c',
|
||||
'-t', '5',
|
||||
'--max-file-not-found=3',
|
||||
'--file-allocation=none',
|
||||
'--console-log-level=error']
|
||||
|
||||
if threads > 1:
|
||||
cmd.append('-s')
|
||||
cmd.append(str(threads))
|
||||
|
||||
if output_path:
|
||||
output_split = output_path.split('/')
|
||||
cmd.append('-d')
|
||||
cmd.append('/'.join(output_split[:-1]))
|
||||
cmd.append('-o')
|
||||
cmd.append(output_split[-1])
|
||||
|
||||
return SimpleProcess(cmd=cmd, root_password=root_password, cwd=cwd)
|
||||
|
||||
def _get_axel_process(self, url: str, output_path: str, cwd: str, root_password: Optional[str], threads: int) -> SimpleProcess:
|
||||
cmd = ['axel', url, '-n', str(threads), '-4', '-c', '-T', '5']
|
||||
|
||||
if not self.check_ssl:
|
||||
cmd.append('-k')
|
||||
|
||||
if output_path:
|
||||
cmd.append(f'--output={output_path}')
|
||||
|
||||
return SimpleProcess(cmd=cmd, cwd=cwd, root_password=root_password)
|
||||
|
||||
def _rm_bad_file(self, file_name: str, output_path: str, cwd, handler: ProcessHandler, root_password: Optional[str]):
|
||||
to_delete = output_path if output_path else f'{cwd}/{file_name}'
|
||||
|
||||
if to_delete and os.path.exists(to_delete):
|
||||
self.logger.info(f'Removing downloaded file {to_delete}')
|
||||
success, _ = handler.handle_simple(SimpleProcess(['rm', '-rf', to_delete], root_password=root_password))
|
||||
return success
|
||||
|
||||
def _concat_file_size(self, file_url: str, base_substatus: StringIO, watcher: ProcessWatcher):
|
||||
watcher.change_substatus(f'{base_substatus.getvalue()} ( ? Mb )')
|
||||
|
||||
try:
|
||||
size = self.http_client.get_content_length(file_url)
|
||||
|
||||
if size:
|
||||
base_substatus.write(f' ( {size} )')
|
||||
watcher.change_substatus(base_substatus.getvalue())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _get_appropriate_threads_number(self, max_threads: int, known_size: int) -> int:
|
||||
if max_threads and max_threads > 0:
|
||||
threads = max_threads
|
||||
elif known_size:
|
||||
threads = 16 if known_size >= 16000000 else floor(known_size / 1000000)
|
||||
|
||||
if threads <= 0:
|
||||
threads = 1
|
||||
else:
|
||||
threads = 16
|
||||
|
||||
return threads
|
||||
|
||||
def _download_with_threads(self, client: str, file_url: str, output_path: str, cwd: str,
|
||||
max_threads: int, known_size: int, display_file_size: bool, handler: ProcessHandler,
|
||||
root_password: Optional[str] = None, substatus_prefix: Optional[str] = None) \
|
||||
-> Tuple[float, bool]:
|
||||
|
||||
threads = self._get_appropriate_threads_number(max_threads, known_size)
|
||||
|
||||
if client == 'aria2':
|
||||
start_time = time.time()
|
||||
process = self._get_aria2c_process(file_url, output_path, cwd, root_password, threads)
|
||||
downloader = 'aria2'
|
||||
else:
|
||||
start_time = time.time()
|
||||
process = self._get_axel_process(file_url, output_path, cwd, root_password, threads)
|
||||
downloader = 'axel'
|
||||
|
||||
name = file_url.split('/')[-1]
|
||||
|
||||
if output_path and not RE_HAS_EXTENSION.match(name) and RE_HAS_EXTENSION.match(output_path):
|
||||
name = output_path.split('/')[-1]
|
||||
|
||||
if handler.watcher:
|
||||
msg = StringIO()
|
||||
msg.write(f'{substatus_prefix} ' if substatus_prefix else '')
|
||||
msg.write(f"{bold('[{}]'.format(downloader))} {self.i18n['downloading']} {bold(name)}")
|
||||
|
||||
if display_file_size:
|
||||
if known_size:
|
||||
msg.write(f' ( {get_human_size_str(known_size)} )')
|
||||
handler.watcher.change_substatus(msg.getvalue())
|
||||
else:
|
||||
Thread(target=self._concat_file_size, args=(file_url, msg, handler.watcher), daemon=True).start()
|
||||
else:
|
||||
msg.write(' ( ? Mb )')
|
||||
handler.watcher.change_substatus(msg.getvalue())
|
||||
|
||||
success, _ = handler.handle_simple(process)
|
||||
return start_time, success
|
||||
|
||||
def download(self, file_url: str, watcher: ProcessWatcher, output_path: str = None, cwd: str = None, root_password: Optional[str] = None, substatus_prefix: str = None, display_file_size: bool = True, max_threads: int = None, known_size: int = None) -> bool:
|
||||
self.logger.info(f'Downloading {file_url}')
|
||||
handler = ProcessHandler(watcher)
|
||||
file_name = file_url.split('/')[-1]
|
||||
|
||||
final_cwd = cwd if cwd else '.'
|
||||
|
||||
success = False
|
||||
start_time = time.time()
|
||||
try:
|
||||
if output_path:
|
||||
if os.path.exists(output_path):
|
||||
self.logger.info(f'Removing old file found before downloading: {output_path}')
|
||||
os.remove(output_path)
|
||||
self.logger.info(f'Old file {output_path} removed')
|
||||
else:
|
||||
output_dir = os.path.dirname(output_path)
|
||||
|
||||
try:
|
||||
Path(output_dir).mkdir(exist_ok=True, parents=True)
|
||||
except OSError:
|
||||
self.logger.error(f"Could not make download directory '{output_dir}'")
|
||||
watcher.print(self.i18n['error.mkdir'].format(dir=output_dir))
|
||||
return False
|
||||
|
||||
threaded_client = self.get_available_multithreaded_tool()
|
||||
if threaded_client:
|
||||
start_time, success = self._download_with_threads(client=threaded_client, file_url=file_url,
|
||||
output_path=output_path,
|
||||
cwd=final_cwd, max_threads=max_threads,
|
||||
known_size=known_size, handler=handler,
|
||||
display_file_size=display_file_size,
|
||||
root_password=root_password)
|
||||
else:
|
||||
start_time = time.time()
|
||||
success = self._self_downloader.download(file_url=file_url, watcher=watcher, output_path=output_path,
|
||||
cwd=cwd, root_password=root_password,
|
||||
substatus_prefix=substatus_prefix,
|
||||
display_file_size=display_file_size, max_threads=max_threads,
|
||||
known_size=known_size)
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
self._rm_bad_file(file_name, output_path, final_cwd, handler, root_password)
|
||||
|
||||
final_time = time.time()
|
||||
self.logger.info(f'{file_name} download took {(final_time - start_time) / 60:.4f} minutes')
|
||||
|
||||
if not success:
|
||||
self.logger.error(f"Could not download '{file_name}'")
|
||||
self._rm_bad_file(file_name, output_path, final_cwd, handler, root_password)
|
||||
|
||||
return success
|
||||
|
||||
def is_multithreaded(self) -> bool:
|
||||
return bool(self.get_available_multithreaded_tool())
|
||||
|
||||
def get_available_multithreaded_tool(self) -> str:
|
||||
if self.multithread_enabled:
|
||||
if self.multithread_client is None or self.multithread_client not in self.supported_multithread_clients:
|
||||
for client in self.supported_multithread_clients:
|
||||
if self.is_multithreaded_client_available(client):
|
||||
return client
|
||||
else:
|
||||
possible_clients = {*self.supported_multithread_clients}
|
||||
|
||||
if self.is_multithreaded_client_available(self.multithread_client):
|
||||
return self.multithread_client
|
||||
else:
|
||||
possible_clients.remove(self.multithread_client)
|
||||
|
||||
for client in possible_clients:
|
||||
if self.is_multithreaded_client_available(client):
|
||||
return client
|
||||
|
||||
def can_work(self) -> bool:
|
||||
return True
|
||||
|
||||
def get_supported_multithreaded_clients(self) -> Tuple[str, ...]:
|
||||
return self.supported_multithread_clients
|
||||
|
||||
def is_multithreaded_client_available(self, name: str) -> bool:
|
||||
if name == 'aria2':
|
||||
return self.is_aria2c_available()
|
||||
elif name == 'axel':
|
||||
return self.is_axel_available()
|
||||
else:
|
||||
return False
|
||||
|
||||
def list_available_multithreaded_clients(self) -> Tuple[str, ...]:
|
||||
return tuple(c for c in self.supported_multithread_clients if self.is_multithreaded_client_available(c))
|
||||
|
||||
def get_supported_clients(self) -> Tuple[str, ...]:
|
||||
return "self", "aria2", "axel"
|
||||
@@ -1,628 +0,0 @@
|
||||
import os
|
||||
import time
|
||||
import traceback
|
||||
from math import floor
|
||||
from threading import Thread
|
||||
from typing import List, Tuple, Optional, Dict, Type, Iterable
|
||||
|
||||
from PyQt5.QtWidgets import QApplication, QStyleFactory
|
||||
|
||||
from bauh import ROOT_DIR, __app_name__
|
||||
from bauh.api.abstract.context import ApplicationContext
|
||||
from bauh.api.abstract.controller import SoftwareManager, SettingsController, SettingsView
|
||||
from bauh.api.abstract.view import TabComponent, InputOption, TextComponent, MultipleSelectComponent, \
|
||||
PanelComponent, FormComponent, TabGroupComponent, SingleSelectComponent, SelectViewType, TextInputComponent, \
|
||||
FileChooserComponent, RangeInputComponent, ViewComponentAlignment
|
||||
from bauh.commons.view_utils import new_select
|
||||
from bauh.view.core import timeshift
|
||||
from bauh.view.core.config import CoreConfigManager, BACKUP_REMOVE_METHODS, BACKUP_DEFAULT_REMOVE_METHOD
|
||||
from bauh.view.core.downloader import AdaptableFileDownloader
|
||||
from bauh.view.util import translation
|
||||
|
||||
|
||||
class GenericSettingsManager(SettingsController):
|
||||
|
||||
def __init__(self, context: ApplicationContext, managers: List[SoftwareManager],
|
||||
working_managers: List[SoftwareManager], configman: CoreConfigManager):
|
||||
self.context = context
|
||||
self.i18n = context.i18n
|
||||
self.managers = managers
|
||||
self.working_managers = working_managers
|
||||
self.logger = context.logger
|
||||
self.file_downloader = self.context.file_downloader
|
||||
self.configman = configman
|
||||
self._settings_views: Optional[Dict[Type, List[SettingsView]]] = None
|
||||
|
||||
def get_settings(self) -> TabGroupComponent:
|
||||
tabs = list()
|
||||
|
||||
gem_opts, def_gem_opts, gem_tabs = [], set(), []
|
||||
|
||||
self._settings_views = dict()
|
||||
|
||||
for man in self.managers:
|
||||
can_work, reason_not_work = man.can_work()
|
||||
modname = man.__module__.split('.')[-2]
|
||||
|
||||
man_settings = man.get_settings() if can_work else None
|
||||
if man_settings:
|
||||
for view in man_settings:
|
||||
icon_path = view.icon_path
|
||||
|
||||
if not icon_path:
|
||||
icon_path = f"{ROOT_DIR}/gems/{modname}/resources/img/{modname}.svg"
|
||||
|
||||
tab_name = view.label if view.label else self.i18n.get(f'gem.{modname}.label', modname.capitalize())
|
||||
gem_tabs.append(TabComponent(label=tab_name, content=view.component, icon_path=icon_path))
|
||||
|
||||
views = self._settings_views.get(man.__class__, list())
|
||||
self._settings_views[man.__class__] = views
|
||||
views.append(view)
|
||||
|
||||
help_tip = reason_not_work if not can_work and reason_not_work else self.i18n.get(f'gem.{modname}.info')
|
||||
opt = InputOption(label=self.i18n.get(f'gem.{modname}.label', modname.capitalize()),
|
||||
tooltip=help_tip,
|
||||
value=modname,
|
||||
icon_path=f'{ROOT_DIR}/gems/{modname}/resources/img/{modname}.svg',
|
||||
read_only=not can_work,
|
||||
extra_properties={'warning': 'true'} if not can_work else None)
|
||||
gem_opts.append(opt)
|
||||
|
||||
if man.is_enabled() and man in self.working_managers:
|
||||
def_gem_opts.add(opt)
|
||||
|
||||
core_config = self.configman.get_config()
|
||||
|
||||
if gem_opts:
|
||||
type_help = TextComponent(html=self.i18n['core.config.types.tip'])
|
||||
gem_opts.sort(key=lambda o: o.value)
|
||||
gem_selector = MultipleSelectComponent(label=None,
|
||||
tooltip=None,
|
||||
options=gem_opts,
|
||||
max_width=floor(self.context.screen_width * 0.22),
|
||||
default_options=def_gem_opts,
|
||||
id_="gems")
|
||||
tabs.append(TabComponent(label=self.i18n['core.config.tab.types'],
|
||||
content=PanelComponent([type_help, FormComponent([gem_selector], spaces=False)]),
|
||||
id_='core.types'))
|
||||
|
||||
tabs.append(self._gen_general_settings(core_config))
|
||||
tabs.append(self._gen_interface_settings(core_config))
|
||||
tabs.append(self._gen_tray_settings(core_config))
|
||||
tabs.append(self._gen_adv_settings(core_config))
|
||||
|
||||
bkp_settings = self._gen_backup_settings(core_config)
|
||||
|
||||
if bkp_settings:
|
||||
tabs.append(bkp_settings)
|
||||
|
||||
for tab in gem_tabs:
|
||||
tabs.append(tab)
|
||||
|
||||
return TabGroupComponent(tabs)
|
||||
|
||||
def _gen_adv_settings(self, core_config: dict) -> TabComponent:
|
||||
|
||||
input_data_exp = TextInputComponent(label=self.i18n['core.config.mem_cache.data_exp'],
|
||||
tooltip=self.i18n['core.config.mem_cache.data_exp.tip'],
|
||||
value=str(core_config['memory_cache']['data_expiration']),
|
||||
only_int=True,
|
||||
id_="data_exp")
|
||||
|
||||
input_icon_exp = TextInputComponent(label=self.i18n['core.config.mem_cache.icon_exp'],
|
||||
tooltip=self.i18n['core.config.mem_cache.icon_exp.tip'],
|
||||
value=str(core_config['memory_cache']['icon_expiration']),
|
||||
only_int=True,
|
||||
id_="icon_exp")
|
||||
|
||||
select_trim = new_select(label=self.i18n['core.config.trim.after_upgrade'],
|
||||
tip=self.i18n['core.config.trim.after_upgrade.tip'],
|
||||
value=core_config['disk']['trim']['after_upgrade'],
|
||||
opts=[(self.i18n['yes'].capitalize(), True, None),
|
||||
(self.i18n['no'].capitalize(), False, None),
|
||||
(self.i18n['ask'].capitalize(), None, None)],
|
||||
id_='trim_after_upgrade')
|
||||
|
||||
select_dep_check = self._gen_bool_component(label=self.i18n['core.config.system.dep_checking'],
|
||||
tooltip=self.i18n['core.config.system.dep_checking.tip'],
|
||||
value=core_config['system']['single_dependency_checking'],
|
||||
id_='dep_check')
|
||||
|
||||
select_check_ssl = self._gen_bool_component(label=self.i18n['core.config.download.check_ssl'],
|
||||
tooltip=self.i18n['core.config.download.check_ssl.tip'],
|
||||
value=core_config['download']['check_ssl'],
|
||||
id_='download.check_ssl')
|
||||
|
||||
select_dmthread = self._gen_bool_component(label=self.i18n['core.config.download.multithreaded'],
|
||||
tooltip=self.i18n['core.config.download.multithreaded.tip'],
|
||||
id_="down_mthread",
|
||||
value=core_config['download']['multithreaded'])
|
||||
|
||||
select_mthread_client = self._gen_multithread_client_select(core_config)
|
||||
|
||||
inputs = [select_dmthread, select_mthread_client, select_check_ssl, select_trim, select_dep_check,
|
||||
input_data_exp, input_icon_exp]
|
||||
panel = PanelComponent([FormComponent(inputs, spaces=False)], id_='advanced')
|
||||
return TabComponent(self.i18n['core.config.tab.advanced'].capitalize(), panel, None, 'core.adv')
|
||||
|
||||
def _gen_multithread_client_select(self, core_config: dict) -> SingleSelectComponent:
|
||||
available_mthread_clients = [*self.file_downloader.list_available_multithreaded_clients()]
|
||||
available_mthread_clients.sort()
|
||||
|
||||
default_i18n_key = 'default' if available_mthread_clients else 'core.config.download.multithreaded_client.none'
|
||||
mthread_client_opts = [(self.i18n[default_i18n_key].capitalize(), None, None)]
|
||||
|
||||
for client in available_mthread_clients:
|
||||
mthread_client_opts.append((client, client, None))
|
||||
|
||||
current_mthread_client = core_config['download']['multithreaded_client']
|
||||
|
||||
if current_mthread_client not in available_mthread_clients:
|
||||
current_mthread_client = None
|
||||
|
||||
return new_select(label=self.i18n['core.config.download.multithreaded_client'],
|
||||
tip=self.i18n['core.config.download.multithreaded_client.tip'],
|
||||
id_="mthread_client",
|
||||
opts=mthread_client_opts,
|
||||
value=current_mthread_client)
|
||||
|
||||
def _gen_tray_settings(self, core_config: dict) -> TabComponent:
|
||||
input_update_interval = TextInputComponent(label=self.i18n['core.config.updates.interval'].capitalize(),
|
||||
tooltip=self.i18n['core.config.updates.interval.tip'],
|
||||
only_int=True,
|
||||
value=str(core_config['updates']['check_interval']),
|
||||
id_="updates_interval")
|
||||
|
||||
allowed_exts = {'png', 'svg', 'jpg', 'jpeg', 'ico', 'xpm'}
|
||||
de_path = str(core_config['ui']['tray']['default_icon']) if core_config['ui']['tray']['default_icon'] else None
|
||||
select_def_icon = FileChooserComponent(id_='def_icon',
|
||||
label=self.i18n["core.config.ui.tray.default_icon"],
|
||||
tooltip=self.i18n["core.config.ui.tray.default_icon.tip"],
|
||||
file_path=de_path,
|
||||
allowed_extensions=allowed_exts)
|
||||
|
||||
up_path = str(core_config['ui']['tray']['updates_icon']) if core_config['ui']['tray']['updates_icon'] else None
|
||||
select_up_icon = FileChooserComponent(id_='up_icon',
|
||||
label=self.i18n["core.config.ui.tray.updates_icon"].capitalize(),
|
||||
tooltip=self.i18n["core.config.ui.tray.updates_icon.tip"].capitalize(),
|
||||
file_path=up_path,
|
||||
allowed_extensions=allowed_exts)
|
||||
|
||||
sub_comps = [FormComponent([select_def_icon, select_up_icon, input_update_interval], spaces=False)]
|
||||
return TabComponent(self.i18n['core.config.tab.tray'].capitalize(),
|
||||
PanelComponent(sub_comps, id_='tray'), None, 'core.tray')
|
||||
|
||||
def _gen_interface_settings(self, core_config: dict) -> TabComponent:
|
||||
select_hdpi = self._gen_bool_component(label=self.i18n['core.config.ui.hdpi'],
|
||||
tooltip=self.i18n['core.config.ui.hdpi.tip'],
|
||||
value=bool(core_config['ui']['hdpi']),
|
||||
id_='hdpi')
|
||||
|
||||
scale_tip = self.i18n['core.config.ui.auto_scale.tip'].format('QT_AUTO_SCREEN_SCALE_FACTOR')
|
||||
select_ascale = self._gen_bool_component(label=self.i18n['core.config.ui.auto_scale'],
|
||||
tooltip=scale_tip,
|
||||
value=bool(core_config['ui']['auto_scale']),
|
||||
id_='auto_scale')
|
||||
|
||||
try:
|
||||
scale = float(core_config['ui']['scale_factor'])
|
||||
|
||||
if scale < 1.0:
|
||||
scale = 1.0
|
||||
except ValueError:
|
||||
scale = 1.0
|
||||
|
||||
select_scale = RangeInputComponent(id_="scalef", label=self.i18n['core.config.ui.scale_factor'] + ' (%)',
|
||||
tooltip=self.i18n['core.config.ui.scale_factor.tip'],
|
||||
min_value=100, max_value=400, step_value=5, value=int(scale * 100))
|
||||
|
||||
if not core_config['ui']['qt_style']:
|
||||
cur_style = QApplication.instance().property('qt_style')
|
||||
else:
|
||||
cur_style = core_config['ui']['qt_style']
|
||||
|
||||
style_opts = [InputOption(label=s.capitalize(), value=s.lower()) for s in QStyleFactory.keys()]
|
||||
|
||||
default_style = [o for o in style_opts if o.value == cur_style]
|
||||
|
||||
if not default_style:
|
||||
if cur_style:
|
||||
default_style = InputOption(label=cur_style, value=cur_style)
|
||||
style_opts.append(default_style)
|
||||
else:
|
||||
default_style = style_opts[0]
|
||||
else:
|
||||
default_style = default_style[0]
|
||||
|
||||
select_style = SingleSelectComponent(label=self.i18n['style'].capitalize(),
|
||||
tooltip=self.i18n['core.config.ui.qt_style.tooltip'],
|
||||
options=style_opts,
|
||||
default_option=default_style,
|
||||
type_=SelectViewType.COMBO,
|
||||
alignment=ViewComponentAlignment.CENTER,
|
||||
id_="style")
|
||||
|
||||
systheme_tip = self.i18n['core.config.ui.system_theme.tip'].format(app=__app_name__)
|
||||
select_system_theme = self._gen_bool_component(label=self.i18n['core.config.ui.system_theme'],
|
||||
tooltip=systheme_tip,
|
||||
value=bool(core_config['ui']['system_theme']),
|
||||
id_='system_theme')
|
||||
|
||||
input_maxd = TextInputComponent(label=self.i18n['core.config.ui.max_displayed'],
|
||||
tooltip=self.i18n['core.config.ui.max_displayed.tip'],
|
||||
only_int=True,
|
||||
id_="table_max",
|
||||
value=str(core_config['ui']['table']['max_displayed']))
|
||||
|
||||
select_dicons = self._gen_bool_component(label=self.i18n['core.config.download.icons'],
|
||||
tooltip=self.i18n['core.config.download.icons.tip'],
|
||||
id_="down_icons",
|
||||
value=core_config['download']['icons'])
|
||||
|
||||
sub_comps = [FormComponent([select_hdpi, select_ascale, select_scale,
|
||||
select_dicons, select_system_theme,
|
||||
select_style, input_maxd], spaces=False)]
|
||||
|
||||
return TabComponent(self.i18n['core.config.tab.ui'].capitalize(),
|
||||
PanelComponent(sub_comps, id_='interface'), None, 'core.ui')
|
||||
|
||||
def _gen_general_settings(self, core_config: dict) -> TabComponent:
|
||||
locale_keys = translation.get_available_keys()
|
||||
locale_opts = [InputOption(label=self.i18n[f'locale.{k}'].capitalize(), value=k) for k in locale_keys]
|
||||
|
||||
current_locale = None
|
||||
|
||||
if core_config['locale']:
|
||||
current_locale = [loc for loc in locale_opts if loc.value == core_config['locale']]
|
||||
|
||||
if not current_locale:
|
||||
if self.i18n.current_key:
|
||||
current_locale = [loc for loc in locale_opts if loc.value == self.i18n.current_key]
|
||||
|
||||
if not current_locale:
|
||||
current_locale = [loc for loc in locale_opts if loc.value == self.i18n.default_key]
|
||||
|
||||
current_locale = current_locale[0] if current_locale else None
|
||||
|
||||
sel_locale = SingleSelectComponent(label=self.i18n['core.config.locale.label'],
|
||||
options=locale_opts,
|
||||
default_option=current_locale,
|
||||
type_=SelectViewType.COMBO,
|
||||
alignment=ViewComponentAlignment.CENTER,
|
||||
id_='locale')
|
||||
|
||||
sel_store_pwd = self._gen_bool_component(label=self.i18n['core.config.store_password'].capitalize(),
|
||||
tooltip=self.i18n['core.config.store_password.tip'].capitalize(),
|
||||
id_="store_pwd",
|
||||
value=bool(core_config['store_root_password']))
|
||||
|
||||
notify_tip = self.i18n['core.config.system.notifications.tip'].capitalize()
|
||||
sel_sys_notify = self._gen_bool_component(label=self.i18n['core.config.system.notifications'].capitalize(),
|
||||
tooltip=notify_tip,
|
||||
value=bool(core_config['system']['notifications']),
|
||||
id_="sys_notify")
|
||||
|
||||
sel_load_apps = self._gen_bool_component(label=self.i18n['core.config.boot.load_apps'],
|
||||
tooltip=self.i18n['core.config.boot.load_apps.tip'],
|
||||
value=bool(core_config['boot']['load_apps']),
|
||||
id_='boot.load_apps')
|
||||
|
||||
sel_sugs = self._gen_bool_component(label=self.i18n['core.config.suggestions.activated'].capitalize(),
|
||||
tooltip=self.i18n['core.config.suggestions.activated.tip'].capitalize(),
|
||||
id_="sugs_enabled",
|
||||
value=bool(core_config['suggestions']['enabled']))
|
||||
|
||||
inp_sugs = TextInputComponent(label=self.i18n['core.config.suggestions.by_type'],
|
||||
tooltip=self.i18n['core.config.suggestions.by_type.tip'],
|
||||
value=str(core_config['suggestions']['by_type']),
|
||||
only_int=True,
|
||||
id_="sugs_by_type")
|
||||
|
||||
inp_reboot = new_select(label=self.i18n['core.config.updates.reboot'],
|
||||
tip=self.i18n['core.config.updates.reboot.tip'],
|
||||
id_='ask_for_reboot',
|
||||
max_width=None,
|
||||
value=bool(core_config['updates']['ask_for_reboot']),
|
||||
opts=[(self.i18n['ask'].capitalize(), True, None),
|
||||
(self.i18n['no'].capitalize(), False, None)])
|
||||
|
||||
inputs = [sel_locale, sel_store_pwd, sel_sys_notify, sel_load_apps, inp_reboot, sel_sugs, inp_sugs]
|
||||
panel = PanelComponent([FormComponent(inputs, spaces=False)], id_='general')
|
||||
return TabComponent(self.i18n['core.config.tab.general'].capitalize(), panel, None, 'core.gen')
|
||||
|
||||
def _gen_bool_component(self, label: str, tooltip: Optional[str], value: bool, id_: str) -> SingleSelectComponent:
|
||||
|
||||
opts = [InputOption(label=self.i18n['yes'].capitalize(), value=True),
|
||||
InputOption(label=self.i18n['no'].capitalize(), value=False)]
|
||||
|
||||
return SingleSelectComponent(label=label,
|
||||
options=opts,
|
||||
default_option=[o for o in opts if o.value == value][0],
|
||||
type_=SelectViewType.RADIO,
|
||||
tooltip=tooltip,
|
||||
max_per_line=len(opts),
|
||||
id_=id_)
|
||||
|
||||
def _save_settings(self, general: PanelComponent,
|
||||
advanced: PanelComponent,
|
||||
backup: PanelComponent,
|
||||
ui: PanelComponent,
|
||||
tray: PanelComponent,
|
||||
gems_panel: PanelComponent) -> Tuple[bool, Optional[List[str]]]:
|
||||
core_config = self.configman.get_config()
|
||||
|
||||
# general
|
||||
gen_form = general.get_component_by_idx(0, FormComponent)
|
||||
|
||||
locale = gen_form.get_component('locale', SingleSelectComponent).get_selected()
|
||||
|
||||
if locale != self.i18n.current_key:
|
||||
core_config['locale'] = locale
|
||||
|
||||
notifications = gen_form.get_component('sys_notify', SingleSelectComponent).get_selected()
|
||||
core_config['system']['notifications'] = notifications
|
||||
|
||||
suggestions = gen_form.get_component('sugs_enabled', SingleSelectComponent).get_selected()
|
||||
core_config['suggestions']['enabled'] = suggestions
|
||||
|
||||
store_root_pwd = gen_form.get_component('store_pwd', SingleSelectComponent).get_selected()
|
||||
core_config['store_root_password'] = store_root_pwd
|
||||
|
||||
sugs_by_type = gen_form.get_component('sugs_by_type', TextInputComponent).get_int_value()
|
||||
core_config['suggestions']['by_type'] = sugs_by_type
|
||||
|
||||
ask_reboot = gen_form.get_component('ask_for_reboot', SingleSelectComponent).get_selected()
|
||||
core_config['updates']['ask_for_reboot'] = ask_reboot
|
||||
|
||||
load_apps = gen_form.get_component('boot.load_apps', SingleSelectComponent).get_selected()
|
||||
core_config['boot']['load_apps'] = load_apps
|
||||
|
||||
# advanced
|
||||
adv_form = advanced.get_component_by_idx(0, FormComponent)
|
||||
|
||||
download_mthreaded = adv_form.get_component('down_mthread', SingleSelectComponent).get_selected()
|
||||
core_config['download']['multithreaded'] = download_mthreaded
|
||||
|
||||
mthread_client = adv_form.get_component('mthread_client', SingleSelectComponent).get_selected()
|
||||
core_config['download']['multithreaded_client'] = mthread_client
|
||||
|
||||
check_ssl = adv_form.get_component('download.check_ssl', SingleSelectComponent).get_selected()
|
||||
core_config['download']['check_ssl'] = check_ssl
|
||||
|
||||
if isinstance(self.file_downloader, AdaptableFileDownloader):
|
||||
self.file_downloader.multithread_client = mthread_client
|
||||
self.file_downloader.multithread_enabled = download_mthreaded
|
||||
self.file_downloader.check_ssl = check_ssl
|
||||
|
||||
single_dep_check = adv_form.get_component('dep_check', SingleSelectComponent).get_selected()
|
||||
core_config['system']['single_dependency_checking'] = single_dep_check
|
||||
|
||||
data_exp = adv_form.get_component('data_exp', TextInputComponent).get_int_value()
|
||||
core_config['memory_cache']['data_expiration'] = data_exp
|
||||
|
||||
icon_exp = adv_form.get_component('icon_exp', TextInputComponent).get_int_value()
|
||||
core_config['memory_cache']['icon_expiration'] = icon_exp
|
||||
|
||||
trim_after_upgrade = adv_form.get_component('trim_after_upgrade', SingleSelectComponent).get_selected()
|
||||
core_config['disk']['trim']['after_upgrade'] = trim_after_upgrade
|
||||
|
||||
# backup
|
||||
if backup:
|
||||
bkp_form = backup.get_component_by_idx(0, FormComponent)
|
||||
|
||||
core_config['backup']['enabled'] = bkp_form.get_component('enabled', SingleSelectComponent).get_selected()
|
||||
core_config['backup']['mode'] = bkp_form.get_component('mode', SingleSelectComponent).get_selected()
|
||||
core_config['backup']['type'] = bkp_form.get_component('type', SingleSelectComponent).get_selected()
|
||||
core_config['backup']['install'] = bkp_form.get_component('install', SingleSelectComponent).get_selected()
|
||||
core_config['backup']['upgrade'] = bkp_form.get_component('upgrade', SingleSelectComponent).get_selected()
|
||||
|
||||
bkp_remove_method = bkp_form.get_component('remove_method', SingleSelectComponent).get_selected()
|
||||
core_config['backup']['remove_method'] = bkp_remove_method
|
||||
|
||||
bkp_uninstall = bkp_form.get_component('uninstall', SingleSelectComponent).get_selected()
|
||||
core_config['backup']['uninstall'] = bkp_uninstall
|
||||
|
||||
bkp_downgrade = bkp_form.get_component('downgrade', SingleSelectComponent).get_selected()
|
||||
core_config['backup']['downgrade'] = bkp_downgrade
|
||||
|
||||
# tray
|
||||
tray_form = tray.get_component_by_idx(0, FormComponent)
|
||||
|
||||
updates_interval = tray_form.get_component('updates_interval', TextInputComponent).get_int_value()
|
||||
core_config['updates']['check_interval'] = updates_interval
|
||||
|
||||
def_icon_path = tray_form.get_component('def_icon', FileChooserComponent).file_path
|
||||
core_config['ui']['tray']['default_icon'] = def_icon_path if def_icon_path else None
|
||||
|
||||
up_icon_path = tray_form.get_component('up_icon', FileChooserComponent).file_path
|
||||
core_config['ui']['tray']['updates_icon'] = up_icon_path if up_icon_path else None
|
||||
|
||||
# ui
|
||||
ui_form = ui.get_component_by_idx(0, FormComponent)
|
||||
|
||||
core_config['download']['icons'] = ui_form.get_component('down_icons', SingleSelectComponent).get_selected()
|
||||
core_config['ui']['hdpi'] = ui_form.get_component('hdpi', SingleSelectComponent).get_selected()
|
||||
|
||||
previous_autoscale = core_config['ui']['auto_scale']
|
||||
|
||||
core_config['ui']['auto_scale'] = ui_form.get_component('auto_scale', SingleSelectComponent).get_selected()
|
||||
|
||||
if previous_autoscale and not core_config['ui']['auto_scale']:
|
||||
self.logger.info("Deleting environment variable QT_AUTO_SCREEN_SCALE_FACTOR")
|
||||
del os.environ['QT_AUTO_SCREEN_SCALE_FACTOR']
|
||||
|
||||
core_config['ui']['scale_factor'] = ui_form.get_component('scalef').value / 100
|
||||
|
||||
table_max = ui_form.get_component('table_max', TextInputComponent).get_int_value()
|
||||
core_config['ui']['table']['max_displayed'] = table_max
|
||||
|
||||
style = ui_form.get_component('style', SingleSelectComponent).get_selected()
|
||||
|
||||
if core_config['ui']['qt_style']:
|
||||
cur_style = core_config['ui']['qt_style']
|
||||
else:
|
||||
cur_style = QApplication.instance().property('qt_style')
|
||||
|
||||
if style != cur_style:
|
||||
core_config['ui']['qt_style'] = style
|
||||
QApplication.instance().setProperty('qt_style', style)
|
||||
|
||||
core_config['ui']['system_theme'] = ui_form.get_component('system_theme', SingleSelectComponent).get_selected()
|
||||
|
||||
# gems
|
||||
checked_gems = gems_panel.components[1].get_component('gems', MultipleSelectComponent).get_selected_values()
|
||||
|
||||
for man in self.managers:
|
||||
modname = man.__module__.split('.')[-2]
|
||||
enabled = modname in checked_gems
|
||||
man.set_enabled(enabled)
|
||||
|
||||
if core_config['gems'] is None and len(checked_gems) == len(self.managers):
|
||||
sel_gems = None
|
||||
else:
|
||||
sel_gems = checked_gems
|
||||
|
||||
core_config['gems'] = sel_gems
|
||||
|
||||
try:
|
||||
self.configman.save_config(core_config)
|
||||
return True, None
|
||||
except Exception:
|
||||
return False, [traceback.format_exc()]
|
||||
|
||||
def _save_views(self, views: Iterable[SettingsView], success_list: List[bool], warnings: List[str]):
|
||||
success = False
|
||||
|
||||
for view in views:
|
||||
try:
|
||||
res = view.save()
|
||||
|
||||
if res:
|
||||
success, errors = res[0], res[1]
|
||||
|
||||
if errors:
|
||||
warnings.extend(errors)
|
||||
except Exception:
|
||||
self.logger.error(f"An exception happened while {view.controller.__class__.__name__}"
|
||||
f" was trying to save settings")
|
||||
traceback.print_exc()
|
||||
finally:
|
||||
success_list.append(success)
|
||||
|
||||
def _save_core_settings(self, tabs: TabGroupComponent, success_list: List[bool], warnings: List[str]):
|
||||
success = False
|
||||
|
||||
try:
|
||||
bkp = tabs.get_tab('core.bkp')
|
||||
success, errors = self._save_settings(general=tabs.get_tab('core.gen').get_content(PanelComponent),
|
||||
advanced=tabs.get_tab('core.adv').get_content(PanelComponent),
|
||||
tray=tabs.get_tab('core.tray').get_content(PanelComponent),
|
||||
backup=bkp.get_content(PanelComponent) if bkp else None,
|
||||
ui=tabs.get_tab('core.ui').get_content(PanelComponent),
|
||||
gems_panel=tabs.get_tab('core.types').get_content(PanelComponent))
|
||||
if errors:
|
||||
warnings.extend(errors)
|
||||
|
||||
except Exception:
|
||||
self.logger.error("An exception happened while saving the core settings")
|
||||
traceback.print_exc()
|
||||
finally:
|
||||
success_list.append(success)
|
||||
|
||||
def save_settings(self, component: TabGroupComponent) -> Tuple[bool, Optional[List[str]]]:
|
||||
ti = time.time()
|
||||
save_threads, warnings, success_list = [], [], []
|
||||
|
||||
save_core = Thread(target=self._save_core_settings, args=(component, success_list, warnings), daemon=True)
|
||||
save_core.start()
|
||||
save_threads.append(save_core)
|
||||
|
||||
if self._settings_views:
|
||||
|
||||
for views in self._settings_views.values():
|
||||
save_view = Thread(target=self._save_views, args=(views, success_list, warnings), daemon=True)
|
||||
save_view.start()
|
||||
save_threads.append(save_view)
|
||||
|
||||
for t in save_threads:
|
||||
t.join()
|
||||
|
||||
success = all(success_list)
|
||||
tf = time.time()
|
||||
self.logger.info(f"Saving all settings took {tf - ti:.8f} seconds")
|
||||
return success, warnings
|
||||
|
||||
def _gen_backup_settings(self, core_config: dict) -> Optional[TabComponent]:
|
||||
if timeshift.is_available():
|
||||
enabled_opt = self._gen_bool_component(label=self.i18n['core.config.backup'],
|
||||
tooltip=None,
|
||||
value=bool(core_config['backup']['enabled']),
|
||||
id_='enabled')
|
||||
|
||||
ops_opts = [(self.i18n['yes'].capitalize(), True, None),
|
||||
(self.i18n['no'].capitalize(), False, None),
|
||||
(self.i18n['ask'].capitalize(), None, None)]
|
||||
|
||||
install_mode = new_select(label=self.i18n['core.config.backup.install'],
|
||||
tip=None,
|
||||
value=core_config['backup']['install'],
|
||||
opts=ops_opts,
|
||||
id_='install')
|
||||
|
||||
uninstall_mode = new_select(label=self.i18n['core.config.backup.uninstall'],
|
||||
tip=None,
|
||||
value=core_config['backup']['uninstall'],
|
||||
opts=ops_opts,
|
||||
id_='uninstall')
|
||||
|
||||
upgrade_mode = new_select(label=self.i18n['core.config.backup.upgrade'],
|
||||
tip=None,
|
||||
value=core_config['backup']['upgrade'],
|
||||
opts=ops_opts,
|
||||
id_='upgrade')
|
||||
|
||||
downgrade_mode = new_select(label=self.i18n['core.config.backup.downgrade'],
|
||||
tip=None,
|
||||
value=core_config['backup']['downgrade'],
|
||||
opts=ops_opts,
|
||||
id_='downgrade')
|
||||
|
||||
mode = new_select(
|
||||
label=self.i18n['core.config.backup.mode'],
|
||||
tip=None,
|
||||
value=core_config['backup']['mode'],
|
||||
opts=[
|
||||
(self.i18n['core.config.backup.mode.incremental'], 'incremental',
|
||||
self.i18n['core.config.backup.mode.incremental.tip']),
|
||||
(self.i18n['core.config.backup.mode.only_one'], 'only_one',
|
||||
self.i18n['core.config.backup.mode.only_one.tip'])
|
||||
],
|
||||
id_='mode')
|
||||
type_ = new_select(label=self.i18n['type'].capitalize(),
|
||||
tip=None,
|
||||
value=core_config['backup']['type'],
|
||||
opts=[('rsync', 'rsync', None), ('btrfs', 'btrfs', None)],
|
||||
id_='type')
|
||||
|
||||
remove_method = core_config['backup']['remove_method']
|
||||
|
||||
if not remove_method or remove_method not in BACKUP_REMOVE_METHODS:
|
||||
remove_method = BACKUP_DEFAULT_REMOVE_METHOD
|
||||
|
||||
remove_i18n = 'core.config.backup.remove_method'
|
||||
remove_opts = ((self.i18n[f'{remove_i18n}.{m}'], m, self.i18n[f'{remove_i18n}.{m}.tip'])
|
||||
for m in sorted(BACKUP_REMOVE_METHODS))
|
||||
|
||||
remove_label = f'{self.i18n[remove_i18n]} ({self.i18n["core.config.backup.mode"]} ' \
|
||||
f'"{self.i18n["core.config.backup.mode.only_one"].capitalize()}")'
|
||||
|
||||
sel_remove = new_select(label=remove_label,
|
||||
tip=None,
|
||||
value=remove_method,
|
||||
opts=remove_opts,
|
||||
capitalize_label=False,
|
||||
id_='remove_method')
|
||||
|
||||
inputs = [enabled_opt, type_, mode, sel_remove, install_mode, uninstall_mode, upgrade_mode, downgrade_mode]
|
||||
panel = PanelComponent([FormComponent(inputs, spaces=False)], id_='backup')
|
||||
return TabComponent(self.i18n['core.config.tab.backup'].capitalize(), panel, None, 'core.bkp')
|
||||
@@ -1,32 +0,0 @@
|
||||
import os
|
||||
from typing import Optional, Dict
|
||||
from bauh import __app_name__
|
||||
|
||||
|
||||
def read_suggestions_mapping() -> Optional[Dict[str, str]]:
|
||||
file_path = f'/etc/{__app_name__}/suggestions.conf'
|
||||
|
||||
if os.path.isfile(file_path):
|
||||
try:
|
||||
with open(file_path) as f:
|
||||
file_content = f.read()
|
||||
except FileNotFoundError:
|
||||
return
|
||||
|
||||
if not file_content:
|
||||
return
|
||||
|
||||
mapping = {}
|
||||
for line in file_content.split('\n'):
|
||||
line_strip = line.strip()
|
||||
|
||||
if not line_strip.startswith('#'):
|
||||
gem_file = line_strip.split('=')
|
||||
|
||||
if len(gem_file) == 2:
|
||||
gem_name, file_url = gem_file[0].strip(), gem_file[1].strip()
|
||||
|
||||
if gem_name and file_url:
|
||||
mapping[gem_name] = file_url
|
||||
|
||||
return mapping if mapping else None
|
||||
@@ -1,38 +0,0 @@
|
||||
import re
|
||||
import shutil
|
||||
from typing import Optional, Generator
|
||||
|
||||
from bauh import __app_name__
|
||||
from bauh.commons.system import SimpleProcess, new_root_subprocess
|
||||
|
||||
RE_SNAPSHOTS = re.compile(r'\d+\s+>\s+([\w\-_]+)\s+.+<{}>'.format(__app_name__))
|
||||
|
||||
|
||||
def is_available() -> bool:
|
||||
return bool(shutil.which('timeshift'))
|
||||
|
||||
|
||||
def delete_all_snapshots(root_password: Optional[str]) -> SimpleProcess:
|
||||
return SimpleProcess(('timeshift', '--delete-all', '--scripted'), root_password=root_password)
|
||||
|
||||
|
||||
def delete(snapshot_name: str, root_password: Optional[str]) -> SimpleProcess:
|
||||
return SimpleProcess(('timeshift', '--delete', '--snapshot', snapshot_name),
|
||||
shell=True, root_password=root_password)
|
||||
|
||||
|
||||
def create_snapshot(root_password: Optional[str], mode: str) -> SimpleProcess:
|
||||
return SimpleProcess(('timeshift', '--create', '--scripted', f'--{mode}', '--comments', f'<{__app_name__}>'),
|
||||
root_password=root_password)
|
||||
|
||||
|
||||
def read_created_snapshots(root_password: Optional[str]) -> Generator[str, None, None]:
|
||||
proc = new_root_subprocess(cmd=('timeshift', '--list'), root_password=root_password, shell=True)
|
||||
proc.wait()
|
||||
|
||||
if proc.returncode == 0:
|
||||
output = '\n'.join((o.decode() for o in proc.stdout))
|
||||
|
||||
if output:
|
||||
for name in RE_SNAPSHOTS.findall(output):
|
||||
yield name
|
||||
@@ -1,12 +0,0 @@
|
||||
from pathlib import Path
|
||||
|
||||
from bauh.api.paths import CACHE_DIR
|
||||
|
||||
TRAY_CHECK_FILE = f'{CACHE_DIR}/notify_tray' # it is a file that signals to the tray icon it should recheck for updates
|
||||
|
||||
|
||||
def notify_tray():
|
||||
Path(CACHE_DIR).mkdir(exist_ok=True, parents=True)
|
||||
|
||||
with open(TRAY_CHECK_FILE, 'w+') as f:
|
||||
f.write('')
|
||||
@@ -1,58 +0,0 @@
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from bauh import __app_name__, __version__
|
||||
from bauh.api.http import HttpClient
|
||||
from bauh.api.paths import CACHE_DIR
|
||||
from bauh.commons.html import bold, link
|
||||
from bauh.commons.version_util import normalize_version
|
||||
from bauh.view.util.translation import I18n
|
||||
|
||||
|
||||
def check_for_update(logger: logging.Logger, http_client: HttpClient, i18n: I18n, tray: bool = False) -> str:
|
||||
"""
|
||||
:param logger:
|
||||
:param http_client:
|
||||
:param i18n:
|
||||
:param file_prefix: notification file prefix
|
||||
:return: bauh update warning string or 'None' if no update is available
|
||||
"""
|
||||
logger.info("Checking for updates")
|
||||
|
||||
try:
|
||||
releases = http_client.get_json('https://api.github.com/repos/spalencsar/bearhub/releases')
|
||||
|
||||
if releases:
|
||||
latest = None
|
||||
|
||||
for r in releases:
|
||||
if not r['draft']:
|
||||
latest = r
|
||||
break
|
||||
|
||||
if latest and latest.get('tag_name'):
|
||||
notifications_dir = f'{CACHE_DIR}/updates'
|
||||
release_file = '{}/{}{}'.format(notifications_dir, '' if not tray else 'tray_', latest['tag_name'])
|
||||
if os.path.exists(release_file):
|
||||
logger.info("Release {} already notified".format(latest['tag_name']))
|
||||
elif normalize_version(latest['tag_name']) > normalize_version(__version__):
|
||||
try:
|
||||
Path(notifications_dir).mkdir(parents=True, exist_ok=True)
|
||||
with open(release_file, 'w+') as f:
|
||||
f.write('')
|
||||
except Exception:
|
||||
logger.error("An error occurred while trying to create the update notification file: {}".format(release_file))
|
||||
|
||||
if tray:
|
||||
return i18n['tray.warning.update_available'].format(__app_name__, latest['tag_name'])
|
||||
else:
|
||||
return i18n['warning.update_available'].format(bold(__app_name__), bold(latest['tag_name']), link(latest.get('html_url', '?')))
|
||||
else:
|
||||
logger.info("No updates available")
|
||||
else:
|
||||
logger.warning("No official release found")
|
||||
else:
|
||||
logger.warning("No releases returned from the GitHub API")
|
||||
except Exception:
|
||||
logger.error("An error occurred while trying to retrieve the current releases")
|
||||
@@ -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
@@ -1,89 +0,0 @@
|
||||
import datetime
|
||||
from threading import Lock
|
||||
from typing import Optional
|
||||
|
||||
from bauh.api.abstract.cache import MemoryCache, MemoryCacheFactory
|
||||
|
||||
|
||||
class DefaultMemoryCache(MemoryCache):
|
||||
"""
|
||||
A synchronized cache implementation
|
||||
"""
|
||||
|
||||
def __init__(self, expiration_time: int):
|
||||
super(DefaultMemoryCache, self).__init__()
|
||||
self.expiration_time = expiration_time
|
||||
self._cache = {}
|
||||
self.lock = Lock()
|
||||
|
||||
def is_enabled(self):
|
||||
return self.expiration_time < 0 or self.expiration_time > 0
|
||||
|
||||
def add(self, key: str, val: object):
|
||||
if key and self.is_enabled():
|
||||
self.lock.acquire()
|
||||
self._add(key, val)
|
||||
self.lock.release()
|
||||
|
||||
def _add(self, key: str, val: object):
|
||||
if key:
|
||||
self._cache[key] = {'val': val, 'expires_at': datetime.datetime.now(UTC) + datetime.timedelta(seconds=self.expiration_time) if self.expiration_time > 0 else None}
|
||||
|
||||
def add_non_existing(self, key: str, val: object):
|
||||
if key and self. is_enabled():
|
||||
self.lock.acquire()
|
||||
cur_val = self.get(key, lock=False)
|
||||
|
||||
if cur_val is None:
|
||||
self._add(key, val)
|
||||
|
||||
self.lock.release()
|
||||
|
||||
def get(self, key: str, lock: bool = True):
|
||||
if key and self.is_enabled():
|
||||
val = self._cache.get(key)
|
||||
|
||||
if val:
|
||||
expiration = val.get('expires_at')
|
||||
|
||||
if expiration and expiration <= datetime.datetime.now(UTC):
|
||||
if lock:
|
||||
self.lock.acquire()
|
||||
|
||||
del self._cache[key]
|
||||
|
||||
if lock:
|
||||
self.lock.release()
|
||||
|
||||
return None
|
||||
|
||||
return val['val']
|
||||
|
||||
def delete(self, key):
|
||||
if key and self.is_enabled():
|
||||
if key in self._cache:
|
||||
self.lock.acquire()
|
||||
del self._cache[key]
|
||||
self.lock.release()
|
||||
|
||||
def keys(self):
|
||||
return set(self._cache.keys()) if self.is_enabled() else set()
|
||||
|
||||
def clean_expired(self):
|
||||
if self.is_enabled():
|
||||
for key in self.keys():
|
||||
self.get(key)
|
||||
|
||||
|
||||
class DefaultMemoryCacheFactory(MemoryCacheFactory):
|
||||
|
||||
def __init__(self, expiration_time: int):
|
||||
"""
|
||||
:param expiration_time: default expiration time for all instantiated caches
|
||||
"""
|
||||
super(DefaultMemoryCacheFactory, self).__init__()
|
||||
self.expiration_time = expiration_time
|
||||
|
||||
def new(self, expiration: Optional[int] = None) -> MemoryCache:
|
||||
return DefaultMemoryCache(expiration if expiration is not None else self.expiration_time)
|
||||
UTC = datetime.timezone.utc
|
||||
@@ -1,115 +0,0 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from threading import Thread, Lock
|
||||
from typing import Type, Dict, Any, Optional
|
||||
|
||||
import yaml
|
||||
|
||||
from bauh.api.abstract.cache import MemoryCache
|
||||
from bauh.api.abstract.disk import DiskCacheLoader, DiskCacheLoaderFactory
|
||||
from bauh.api.abstract.model import SoftwarePackage
|
||||
|
||||
|
||||
class AsyncDiskCacheLoader(Thread, DiskCacheLoader):
|
||||
|
||||
def __init__(self, cache_map: Dict[Type[SoftwarePackage], MemoryCache], logger: logging.Logger):
|
||||
super(AsyncDiskCacheLoader, self).__init__(daemon=True)
|
||||
self.pkgs = []
|
||||
self._work = True
|
||||
self.lock = Lock()
|
||||
self.cache_map = cache_map
|
||||
self.logger = logger
|
||||
self.processed = 0
|
||||
self._working = False
|
||||
|
||||
def fill(self, pkg: SoftwarePackage, sync: bool = False):
|
||||
"""
|
||||
Adds a package which data must be read from the disk to a queue (if not sync)
|
||||
:param pkg:
|
||||
:param sync:
|
||||
:return:
|
||||
"""
|
||||
if pkg and pkg.supports_disk_cache():
|
||||
if sync or not self._working:
|
||||
self._fill_cached_data(pkg)
|
||||
else:
|
||||
self.pkgs.append(pkg)
|
||||
|
||||
def read(self, pkg: SoftwarePackage) -> Optional[Dict[str, Any]]:
|
||||
if pkg and pkg.supports_disk_cache():
|
||||
data_path = pkg.get_disk_data_path()
|
||||
|
||||
if data_path and os.path.isfile(data_path):
|
||||
ext = data_path.split('.')[-1]
|
||||
|
||||
try:
|
||||
with open(data_path) as f:
|
||||
file_content = f.read()
|
||||
except FileNotFoundError:
|
||||
return
|
||||
|
||||
if file_content:
|
||||
if ext == 'json':
|
||||
cached_data = json.loads(file_content)
|
||||
elif ext in {'yml', 'yaml'}:
|
||||
cached_data = yaml.load(file_content)
|
||||
else:
|
||||
raise Exception(f'The cached data file {data_path} has an unsupported format')
|
||||
|
||||
if cached_data:
|
||||
return cached_data
|
||||
|
||||
else:
|
||||
self.logger.warning(f"No cached content in file {data_path}")
|
||||
|
||||
def stop_working(self):
|
||||
self._work = False
|
||||
|
||||
def run(self):
|
||||
self._working = True
|
||||
last = 0
|
||||
|
||||
while True:
|
||||
time.sleep(0.00001)
|
||||
if len(self.pkgs) > self.processed:
|
||||
pkg = self.pkgs[last]
|
||||
|
||||
self._fill_cached_data(pkg)
|
||||
self.processed += 1
|
||||
last += 1
|
||||
elif not self._work:
|
||||
break
|
||||
|
||||
self._working = False
|
||||
|
||||
def _fill_cached_data(self, pkg: SoftwarePackage) -> bool:
|
||||
cached_data = self.read(pkg)
|
||||
|
||||
if cached_data:
|
||||
pkg.fill_cached_data(cached_data)
|
||||
cache = self.cache_map.get(pkg.__class__)
|
||||
|
||||
if cache:
|
||||
cache.add_non_existing(str(pkg.id), cached_data)
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class DefaultDiskCacheLoaderFactory(DiskCacheLoaderFactory):
|
||||
|
||||
def __init__(self, logger: logging.Logger):
|
||||
super(DefaultDiskCacheLoaderFactory, self).__init__()
|
||||
self.logger = logger
|
||||
self.cache_map = {}
|
||||
|
||||
def map(self, pkg_type: Type[SoftwarePackage], cache: MemoryCache):
|
||||
if pkg_type:
|
||||
if pkg_type not in self.cache_map:
|
||||
self.cache_map[pkg_type] = cache
|
||||
|
||||
def new(self) -> AsyncDiskCacheLoader:
|
||||
return AsyncDiskCacheLoader(cache_map=self.cache_map, logger=self.logger)
|
||||
@@ -1,22 +0,0 @@
|
||||
import logging
|
||||
from logging import INFO
|
||||
|
||||
FORMAT = '%(asctime)s %(levelname)s [%(module_path)s:%(lineno)s - %(funcName)s()] - %(message)s'
|
||||
|
||||
|
||||
class FilePathFilter(logging.Filter):
|
||||
|
||||
def filter(self, record):
|
||||
record.module_path = record.pathname.split('site-packages/')[1] if 'site-packages' in record.pathname else str(record.pathname)
|
||||
return True
|
||||
|
||||
|
||||
def new_logger(name: str, enabled: bool) -> logging.Logger:
|
||||
instance = logging.Logger(name, level=INFO)
|
||||
instance.addFilter(FilePathFilter())
|
||||
stream_handler = logging.StreamHandler()
|
||||
stream_handler.setFormatter(logging.Formatter(FORMAT))
|
||||
instance.addHandler(stream_handler)
|
||||
instance.disabled = not enabled
|
||||
|
||||
return instance
|
||||
@@ -1,5 +0,0 @@
|
||||
from bauh import ROOT_DIR
|
||||
|
||||
|
||||
def get_path(resource_path):
|
||||
return ROOT_DIR + '/view/resources/' + resource_path
|
||||
@@ -1,89 +0,0 @@
|
||||
import glob
|
||||
import locale
|
||||
import os
|
||||
from typing import Tuple, Set
|
||||
|
||||
from bauh.view.util import resource
|
||||
|
||||
|
||||
class I18n(dict):
|
||||
|
||||
def __init__(self, current_key: str, current_locale: dict, default_key: str, default_locale: dict):
|
||||
super(I18n, self).__init__()
|
||||
self.current_key = current_key
|
||||
self.current = current_locale
|
||||
self.default_key = default_key
|
||||
self.default = default_locale
|
||||
|
||||
def __getitem__(self, item):
|
||||
try:
|
||||
return self.current.__getitem__(item)
|
||||
except KeyError:
|
||||
if self.default:
|
||||
try:
|
||||
return self.default.__getitem__(item)
|
||||
except KeyError:
|
||||
return item
|
||||
else:
|
||||
return item
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
res = self.current.get(args[0])
|
||||
|
||||
if res is None:
|
||||
if self.default:
|
||||
return self.default.get(*args, **kwargs)
|
||||
else:
|
||||
return self.current.get(*args, **kwargs)
|
||||
|
||||
return res
|
||||
|
||||
|
||||
def get_available_keys() -> Set[str]:
|
||||
locale_dir = resource.get_path('locale')
|
||||
return {file.split('/')[-1] for file in glob.glob(locale_dir + '/*') if os.path.isfile(file)}
|
||||
|
||||
|
||||
def get_locale_keys(key: str = None, locale_dir: str = resource.get_path('locale')) -> Tuple[str, dict]:
|
||||
|
||||
locale_path = None
|
||||
|
||||
if key is None:
|
||||
try:
|
||||
current_locale = locale.getlocale()
|
||||
|
||||
if current_locale is None or current_locale[0] is None:
|
||||
current_locale = ('en', 'UTF-8')
|
||||
except Exception:
|
||||
current_locale = ('en', 'UTF-8')
|
||||
|
||||
else:
|
||||
current_locale = [key.strip().lower()]
|
||||
|
||||
if current_locale:
|
||||
current_locale = current_locale[0]
|
||||
|
||||
for locale_file in glob.glob(locale_dir + '/*'):
|
||||
name = locale_file.split('/')[-1]
|
||||
|
||||
if current_locale == name or current_locale.startswith(name + '_'):
|
||||
locale_path = locale_file
|
||||
break
|
||||
|
||||
if not locale_path:
|
||||
return current_locale if current_locale else key, {}
|
||||
|
||||
with open(locale_path, 'r') as f:
|
||||
locale_keys = f.readlines()
|
||||
|
||||
locale_obj = {}
|
||||
for line in locale_keys:
|
||||
line_strip = line.strip()
|
||||
if line_strip:
|
||||
try:
|
||||
keyval = line_strip.split('=')
|
||||
locale_obj[keyval[0].strip()] = keyval[1].strip()
|
||||
except Exception:
|
||||
print("Error decoding i18n line '{}'".format(line))
|
||||
|
||||
return locale_path.split('/')[-1], locale_obj
|
||||
@@ -1,88 +0,0 @@
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import traceback
|
||||
from typing import List, Tuple
|
||||
|
||||
from PyQt5.QtCore import QCoreApplication
|
||||
from PyQt5.QtGui import QIcon
|
||||
from colorama import Fore
|
||||
|
||||
from bauh import __app_name__
|
||||
from bauh.api.abstract.controller import SoftwareManager
|
||||
from bauh.api.paths import CONFIG_DIR, CACHE_DIR, TEMP_DIR
|
||||
from bauh.commons.system import run_cmd
|
||||
from bauh.view.util import resource
|
||||
|
||||
|
||||
def notify_user(msg: str, icon_path: str = None):
|
||||
icon_id = icon_path
|
||||
|
||||
if not icon_id:
|
||||
icon_id = get_default_icon()[0]
|
||||
|
||||
os.system("notify-send -a {} {} '{}'".format(__app_name__, "-i {}".format(icon_id) if icon_id else '', msg))
|
||||
|
||||
|
||||
def get_default_icon(system: bool = True) -> Tuple[str, QIcon]:
|
||||
if system:
|
||||
system_icon = QIcon.fromTheme(__app_name__)
|
||||
if not system_icon.isNull():
|
||||
return system_icon.name(), system_icon
|
||||
|
||||
path = resource.get_path('img/logo.svg')
|
||||
return path, QIcon(path)
|
||||
|
||||
|
||||
def restart_app():
|
||||
appimage_path = os.getenv('APPIMAGE')
|
||||
|
||||
restart_cmd = [appimage_path] if appimage_path else [sys.executable, *sys.argv]
|
||||
|
||||
subprocess.Popen(restart_cmd)
|
||||
QCoreApplication.exit()
|
||||
|
||||
|
||||
def get_distro():
|
||||
if os.path.exists('/etc/arch-release'):
|
||||
return 'arch'
|
||||
|
||||
if os.path.exists('/etc/os-release'):
|
||||
with open('/etc/os-release', 'r') as os_release_file:
|
||||
for line in os_release_file:
|
||||
if 'ID_LIKE=arch' in line:
|
||||
return 'arch'
|
||||
|
||||
if os.path.exists('/proc/version'):
|
||||
if 'ubuntu' in run_cmd('cat /proc/version').lower():
|
||||
return 'ubuntu'
|
||||
|
||||
return 'unknown'
|
||||
|
||||
|
||||
def clean_app_files(managers: List[SoftwareManager], logs: bool = True):
|
||||
|
||||
if logs:
|
||||
print('[{}] Cleaning configuration and cache files'.format(__app_name__))
|
||||
|
||||
for path in (CACHE_DIR, CONFIG_DIR, TEMP_DIR):
|
||||
if logs:
|
||||
print('[{}] Deleting directory {}'.format(__app_name__, path))
|
||||
|
||||
if os.path.exists(path):
|
||||
try:
|
||||
shutil.rmtree(path)
|
||||
if logs:
|
||||
print('{}[{}] Directory {} deleted{}'.format(Fore.YELLOW, __app_name__, path, Fore.RESET))
|
||||
except Exception:
|
||||
if logs:
|
||||
print('{}[{}] An exception has happened when deleting {}{}'.format(Fore.RED, __app_name__, path, Fore.RESET))
|
||||
traceback.print_exc()
|
||||
|
||||
if managers:
|
||||
for m in managers:
|
||||
m.clear_data()
|
||||
|
||||
if logs:
|
||||
print('[{}] Cleaning finished'.format(__app_name__))
|
||||
Reference in New Issue
Block a user