From 09d254d0715b877f72aac863ea914d80683575ce Mon Sep 17 00:00:00 2001 From: Nippy Date: Sat, 10 Oct 2026 10:49:30 +0200 Subject: artcraft update --- raveos-app-installer/raveos-app-installer.py | 214 +++++++++++++++++++++++++-- 1 file changed, 202 insertions(+), 12 deletions(-) (limited to 'raveos-app-installer/raveos-app-installer.py') diff --git a/raveos-app-installer/raveos-app-installer.py b/raveos-app-installer/raveos-app-installer.py index 15f9f77..ab841f3 100644 --- a/raveos-app-installer/raveos-app-installer.py +++ b/raveos-app-installer/raveos-app-installer.py @@ -68,6 +68,7 @@ TYPE_LABEL = { "flatpak-user": "flatpak", "script": "script", "message": "info", + "choice": "menu", } @@ -114,6 +115,9 @@ T = { "row_installed": {"hu": "telepítve", "en": "installed"}, "row_not_installed": {"hu": "nincs telepítve", "en": "not installed"}, "row_info": {"hu": "info", "en": "info"}, + "choice_installed": {"hu": "{n}/{total} telepítve", "en": "{n}/{total} installed"}, + "choice_pick": {"hu": "Válassz appot…", "en": "Choose apps…"}, + "choice_selected": {"hu": "{n} kiválasztva", "en": "{n} selected"}, "dest_placeholder": {"hu": "Backup fájl útvonala (.tar.gz)…", "en": "Backup file path (.tar.gz)…"}, "btn_browse": {"hu": "Tallóz", "en": "Browse"}, "btn_save": {"hu": "Mentés", "en": "Save"}, @@ -279,6 +283,44 @@ def build_qss() -> str: border: 1px solid {C['accent']}; }} + /* ArtCraft legördülő (több választós) */ + QToolButton#Choice {{ + background: {C['surface']}; + border: 1px solid {C['border']}; + border-radius: 4px; + padding: 4px 10px; + color: {C['text']}; + font-size: 12px; + }} + QToolButton#Choice:hover {{ + background: {C['surface_hi']}; + border-color: {C['border_hi']}; + }} + QToolButton#Choice::menu-indicator {{ image: none; width: 0; }} + QMenu {{ + background: {C['surface']}; + border: 1px solid {C['border']}; + padding: 4px; + }} + QMenu::item {{ + padding: 5px 22px 5px 26px; + border-radius: 3px; + color: {C['text']}; + }} + QMenu::item:selected {{ background: {C['surface_hi']}; }} + QMenu::item:disabled {{ color: {C['text_mut']}; }} + QMenu::indicator {{ width: 14px; height: 14px; left: 8px; }} + QMenu::indicator:unchecked {{ + border: 1px solid {C['border_hi']}; + border-radius: 3px; + background: transparent; + }} + QMenu::indicator:checked {{ + background: {C['accent']}; + border: 1px solid {C['accent']}; + border-radius: 3px; + }} + /* Log */ QPlainTextEdit#Log {{ background: {C['header']}; @@ -378,10 +420,23 @@ def is_installed(app: dict, idx: dict) -> bool: except (subprocess.SubprocessError, OSError): return False + if t == "choice": + return any(_option_installed(o, idx) for o in app.get("options", [])) + return False +def _option_installed(opt: dict, idx: dict) -> bool: + pkgs = opt.get("packages") or [opt["id"]] + return all(p in idx.get("pacman", set()) for p in pkgs) + + +def _all_options_installed(app: dict, idx: dict) -> bool: + opts = app.get("options", []) + return bool(opts) and all(_option_installed(o, idx) for o in opts) + + def _root_prefix(gui: bool) -> list: """GUI -> sudo -S (jelszó stdin-ről), terminál -> sudo, ha már root -> semmi.""" if os.geteuid() == 0: @@ -569,10 +624,10 @@ def run_gui() -> int: QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QLineEdit, QComboBox, QScrollArea, QFrame, QCheckBox, QPlainTextEdit, QProgressBar, QButtonGroup, QSizePolicy, - QMessageBox, QFileDialog, QDialog, + QMessageBox, QFileDialog, QDialog, QToolButton, QMenu, ) from PyQt6.QtCore import Qt, QThread, pyqtSignal - from PyQt6.QtGui import QFont, QFontDatabase + from PyQt6.QtGui import QFont, QFontDatabase, QAction def display_font(size: int, weight=QFont.Weight.Normal) -> QFont: for fam in ("Bebas Neue", "Oswald", "Anton"): @@ -607,12 +662,33 @@ def run_gui() -> int: self.progress.emit(int((i + 1) / total * 100)) self.done.emit(ok, total) + class CheckableMenu(QMenu): + def mouseReleaseEvent(self, e): + act = self.activeAction() + if act is not None and act.isEnabled() and act.isCheckable(): + act.setChecked(not act.isChecked()) + e.accept() + return + super().mouseReleaseEvent(e) + + def keyPressEvent(self, e): + if e.key() in (Qt.Key.Key_Return, Qt.Key.Key_Enter, Qt.Key.Key_Space): + act = self.activeAction() + if act is not None and act.isEnabled() and act.isCheckable(): + act.setChecked(not act.isChecked()) + e.accept() + return + super().keyPressEvent(e) + class Row(QFrame): - def __init__(self, app, installed): + def __init__(self, app, installed, idx=None, only_inst=False): super().__init__() self.app = app self.installed = installed + self.idx = idx or {"pacman": set(), "flatpak": set()} self.is_message = app.get("type") == "message" + self.is_choice = app.get("type") == "choice" + self.option_actions = [] self.setObjectName("Row") lay = QHBoxLayout(self) lay.setContentsMargins(12, 9, 12, 9) @@ -636,9 +712,47 @@ def run_gui() -> int: mid.addWidget(sub) lay.addLayout(mid, 1) + if self.is_choice: + self.menu_btn = QToolButton() + self.menu_btn.setObjectName("Choice") + self.menu_btn.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup) + menu = CheckableMenu(self.menu_btn) + for o in app.get("options", []): + oinst = _option_installed(o, self.idx) + if only_inst and not oinst: + continue + act = QAction(o["name"] + (" ✓" if oinst else ""), menu) + act.setCheckable(True) + odesc = _loc(o.get("desc")) + if odesc: + act.setToolTip(odesc) + act.toggled.connect(self._choice_sync) + menu.addAction(act) + self.option_actions.append((act, o)) + if not self.option_actions: + act = QAction("-", menu) + act.setEnabled(False) + menu.addAction(act) + self.menu_btn.setMenu(menu) + self.menu_btn.setFixedWidth(180) + lay.addWidget(self.menu_btn) + self.cb.toggled.connect(self._choice_toggle_all) + self._choice_sync() + if self.is_message: status = QLabel(tr("row_info")) status.setStyleSheet(f"color:{C['accent_hi']}; font-size:12px;") + elif self.is_choice: + total = len(app.get("options", [])) + names = [o["name"] for o in app.get("options", []) + if _option_installed(o, self.idx)] + n = len(names) + status = QLabel( + tr("choice_installed", n=n, total=total) if n else tr("row_not_installed")) + status.setStyleSheet( + f"color:{C['accent_hi'] if n else C['text_mut']}; font-size:12px;") + if names: + status.setToolTip(", ".join(names)) else: status = QLabel(tr("row_installed") if installed else tr("row_not_installed")) status.setStyleSheet( @@ -647,6 +761,28 @@ def run_gui() -> int: self.setCursor(Qt.CursorShape.PointingHandCursor) + def _choice_sync(self): + names = [o["name"] for act, o in self.option_actions if act.isChecked()] + n = len(names) + if n == 0: + text = tr("choice_pick") + elif n == 1: + text = names[0] + else: + text = tr("choice_selected", n=n) + self.menu_btn.setText(text + " ▾") + self.menu_btn.setToolTip(", ".join(names) if names else tr("choice_pick")) + self.cb.blockSignals(True) + self.cb.setChecked(n > 0) + self.cb.blockSignals(False) + + def _choice_toggle_all(self, checked): + for act, o in self.option_actions: + act.blockSignals(True) + act.setChecked(checked) + act.blockSignals(False) + self._choice_sync() + def mousePressEvent(self, e): if e.button() == Qt.MouseButton.LeftButton: if self.is_message: @@ -1330,9 +1466,10 @@ def run_gui() -> int: for r in self.rows: r.setParent(None) self.rows.clear() + only_inst = self.tab_inst.isChecked() for app in self.catalog: inst = is_installed(app, self.idx) - row = Row(app, inst) + row = Row(app, inst, self.idx, only_inst) self.list_lay.insertWidget(self.list_lay.count() - 1, row) self.rows.append(row) @@ -1345,20 +1482,45 @@ def run_gui() -> int: self.btn_install.setVisible(not only_inst) for r in self.rows: show = True + is_choice = r.app.get("type") == "choice" if only_inst and not r.installed: show = False - if not only_inst and r.installed: - show = False + if not only_inst: + if is_choice: + if _all_options_installed(r.app, self.idx): + show = False + elif r.installed: + show = False if cat_idx != 0 and r.app["category"] != cat: show = False - if q and q not in r.app["name"].lower() \ - and q not in _loc(r.app.get("desc")).lower(): - show = False + if q: + hay = (r.app["name"] + " " + _loc(r.app.get("desc"))).lower() + if is_choice: + hay += " " + " ".join( + o["name"] for o in r.app.get("options", [])).lower() + if q not in hay: + show = False r.setVisible(show) def selected(self): - return [r.app for r in self.rows - if r.cb.isChecked() and r.isVisible() and not r.is_message] + res = [] + for r in self.rows: + if not r.isVisible() or r.is_message: + continue + if r.app.get("type") == "choice": + if not r.cb.isChecked(): + continue + for act, o in r.option_actions: + if act.isChecked(): + res.append({ + **o, + "type": "pacman", + "category": r.app["category"], + "name": f"{r.app['name']}: {o['name']}", + }) + elif r.cb.isChecked(): + res.append(r.app) + return res def _ask_password(self) -> str: """Jelszó dialog - egyszer fut az akció elején. Üres string = megszakítva.""" @@ -1593,7 +1755,17 @@ def run_gui() -> int: def _resolve(names, catalog): - by = {a["name"].lower(): a for a in catalog} + by = {} + for a in catalog: + by[a["name"].lower()] = a + if a.get("type") == "choice": + for o in a.get("options", []): + by.setdefault(o["name"].lower(), { + **o, + "type": "pacman", + "category": a["category"], + "name": f"{a['name']}: {o['name']}", + }) found, missing = [], [] for n in names: a = by.get(n.lower()) @@ -1618,6 +1790,24 @@ def cli_action(names, catalog, install: bool): print(f"Nem található: {m}") if not found: return 1 + idx = installed_index() + expanded = [] + for a in found: + if a.get("type") == "choice": + for o in a.get("options", []): + if install or _option_installed(o, idx): + expanded.append({ + **o, + "type": "pacman", + "category": a["category"], + "name": f"{a['name']}: {o['name']}", + }) + else: + expanded.append(a) + found = expanded + if not found: + print("Nincs végrehajtható lépés.") + return 1 steps = plan_commands(found, install, gui=False) if not steps: print("Nincs végrehajtható lépés.") -- cgit v1.3