summaryrefslogtreecommitdiff
path: root/raveos-app-installer/raveos-app-installer.py
diff options
context:
space:
mode:
Diffstat (limited to 'raveos-app-installer/raveos-app-installer.py')
-rw-r--r--raveos-app-installer/raveos-app-installer.py214
1 files changed, 202 insertions, 12 deletions
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.")