|
|
|
|
@ -1,4 +1,5 @@
|
|
|
|
|
#!/usr/bin/env python3 |
|
|
|
|
"""Cellar GTK – client grafico per Bottle Archive Server (libadwaita).""" |
|
|
|
|
from __future__ import annotations |
|
|
|
|
|
|
|
|
|
import json |
|
|
|
|
@ -12,8 +13,7 @@ try:
|
|
|
|
|
import gi # type: ignore[import-not-found] |
|
|
|
|
except ImportError as exc: |
|
|
|
|
raise SystemExit( |
|
|
|
|
"PyGObject / GTK4 non trovato. Installa i pacchetti di sistema 'python3-gobject' e 'gtk4' " |
|
|
|
|
"oppure l'equivalente della tua distribuzione." |
|
|
|
|
"PyGObject non trovato. Installa 'python3-gobject', 'gtk4' e 'libadwaita'." |
|
|
|
|
) from exc |
|
|
|
|
|
|
|
|
|
from client import ( |
|
|
|
|
@ -25,11 +25,48 @@ from client import (
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
gi.require_version("Gtk", "4.0") |
|
|
|
|
from gi.repository import GLib, Gtk # type: ignore[import-not-found] |
|
|
|
|
gi.require_version("Adw", "1") |
|
|
|
|
from gi.repository import Adw, Gdk, GLib, Gtk # type: ignore[import-not-found] |
|
|
|
|
|
|
|
|
|
# ── Constants ─────────────────────────────────────────────────── |
|
|
|
|
CONFIG_PATH = Path.home() / ".config" / "cellar" / "gtk_client.json" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
APP_CSS = """\ |
|
|
|
|
.archive-row { |
|
|
|
|
padding: 14px 16px; |
|
|
|
|
} |
|
|
|
|
.archive-title { |
|
|
|
|
font-weight: bold; |
|
|
|
|
font-size: 1.05em; |
|
|
|
|
} |
|
|
|
|
.archive-meta { |
|
|
|
|
font-size: 0.88em; |
|
|
|
|
} |
|
|
|
|
.status-chip { |
|
|
|
|
font-size: 0.82em; |
|
|
|
|
font-weight: 600; |
|
|
|
|
padding: 2px 10px; |
|
|
|
|
border-radius: 99px; |
|
|
|
|
} |
|
|
|
|
.status-chip.installed { |
|
|
|
|
color: @success_color; |
|
|
|
|
background: alpha(@success_color, 0.12); |
|
|
|
|
} |
|
|
|
|
.status-chip.not-installed { |
|
|
|
|
color: alpha(@window_fg_color, 0.45); |
|
|
|
|
background: alpha(@window_fg_color, 0.06); |
|
|
|
|
} |
|
|
|
|
progressbar trough { |
|
|
|
|
min-height: 4px; |
|
|
|
|
} |
|
|
|
|
progressbar progress { |
|
|
|
|
min-height: 4px; |
|
|
|
|
border-radius: 2px; |
|
|
|
|
} |
|
|
|
|
""" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── Config ────────────────────────────────────────────────────── |
|
|
|
|
@dataclass |
|
|
|
|
class AppConfig: |
|
|
|
|
server_url: str = DEFAULT_SERVER |
|
|
|
|
@ -40,12 +77,10 @@ class AppConfig:
|
|
|
|
|
def load_config() -> AppConfig: |
|
|
|
|
if not CONFIG_PATH.exists(): |
|
|
|
|
return AppConfig() |
|
|
|
|
|
|
|
|
|
try: |
|
|
|
|
raw = json.loads(CONFIG_PATH.read_text(encoding="utf-8")) |
|
|
|
|
except (OSError, json.JSONDecodeError): |
|
|
|
|
return AppConfig() |
|
|
|
|
|
|
|
|
|
return AppConfig( |
|
|
|
|
server_url=str(raw.get("server_url") or DEFAULT_SERVER).rstrip("/"), |
|
|
|
|
bottles_dir=str(raw.get("bottles_dir") or DEFAULT_BOTTLES_DIR), |
|
|
|
|
@ -58,13 +93,13 @@ def save_config(config: AppConfig) -> None:
|
|
|
|
|
CONFIG_PATH.write_text(json.dumps(asdict(config), indent=2), encoding="utf-8") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── Utility ───────────────────────────────────────────────────── |
|
|
|
|
def format_size(size_bytes: int | None) -> str: |
|
|
|
|
if not size_bytes: |
|
|
|
|
return "-" |
|
|
|
|
return "—" |
|
|
|
|
size = float(size_bytes) |
|
|
|
|
units = ["B", "KB", "MB", "GB"] |
|
|
|
|
for unit in units: |
|
|
|
|
if size < 1024 or unit == units[-1]: |
|
|
|
|
for unit in ("B", "KB", "MB", "GB"): |
|
|
|
|
if size < 1024 or unit == "GB": |
|
|
|
|
return f"{size:.1f} {unit}" |
|
|
|
|
size /= 1024 |
|
|
|
|
return f"{size_bytes} B" |
|
|
|
|
@ -74,8 +109,7 @@ def archive_matches_query(archive: dict[str, Any], query: str) -> bool:
|
|
|
|
|
normalized = query.strip().lower() |
|
|
|
|
if not normalized: |
|
|
|
|
return True |
|
|
|
|
|
|
|
|
|
searchable_fields = ( |
|
|
|
|
searchable = ( |
|
|
|
|
archive.get("name"), |
|
|
|
|
archive.get("bottle_name"), |
|
|
|
|
archive.get("description"), |
|
|
|
|
@ -84,7 +118,7 @@ def archive_matches_query(archive: dict[str, Any], query: str) -> bool:
|
|
|
|
|
archive.get("arch"), |
|
|
|
|
archive.get("windows_version"), |
|
|
|
|
) |
|
|
|
|
haystack = " ".join(str(value or "") for value in searchable_fields).lower() |
|
|
|
|
haystack = " ".join(str(v or "") for v in searchable).lower() |
|
|
|
|
return normalized in haystack |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -92,79 +126,87 @@ def normalize_name(value: str | None) -> str:
|
|
|
|
|
return (value or "").strip().lower() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SettingsWindow(Gtk.Window): |
|
|
|
|
def __init__(self, parent: "CellarWindow", config: AppConfig, on_save: Callable[[AppConfig], None]): |
|
|
|
|
def _load_css() -> None: |
|
|
|
|
provider = Gtk.CssProvider() |
|
|
|
|
provider.load_from_string(APP_CSS) |
|
|
|
|
Gtk.StyleContext.add_provider_for_display( |
|
|
|
|
Gdk.Display.get_default(), |
|
|
|
|
provider, |
|
|
|
|
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION, |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── Settings Dialog ───────────────────────────────────────────── |
|
|
|
|
class SettingsWindow(Adw.Window): |
|
|
|
|
"""Preferences dialog using libadwaita widgets.""" |
|
|
|
|
|
|
|
|
|
def __init__( |
|
|
|
|
self, |
|
|
|
|
parent: "CellarWindow", |
|
|
|
|
config: AppConfig, |
|
|
|
|
on_save: Callable[[AppConfig], None], |
|
|
|
|
): |
|
|
|
|
super().__init__(title="Configurazione") |
|
|
|
|
self.parent = parent |
|
|
|
|
self.on_save = on_save |
|
|
|
|
self._parent_win = parent |
|
|
|
|
self._on_save = on_save |
|
|
|
|
self.set_transient_for(parent) |
|
|
|
|
self.set_modal(True) |
|
|
|
|
self.set_default_size(520, 180) |
|
|
|
|
|
|
|
|
|
root = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) |
|
|
|
|
root.set_margin_top(16) |
|
|
|
|
root.set_margin_bottom(16) |
|
|
|
|
root.set_margin_start(16) |
|
|
|
|
root.set_margin_end(16) |
|
|
|
|
|
|
|
|
|
description = Gtk.Label( |
|
|
|
|
label="Imposta il server Cellar e la cartella locale di Bottles usata per installazione e upload.", |
|
|
|
|
wrap=True, |
|
|
|
|
xalign=0, |
|
|
|
|
self.set_default_size(480, -1) |
|
|
|
|
|
|
|
|
|
toolbar_view = Adw.ToolbarView() |
|
|
|
|
|
|
|
|
|
header = Adw.HeaderBar() |
|
|
|
|
cancel_btn = Gtk.Button(label="Annulla") |
|
|
|
|
cancel_btn.connect("clicked", lambda *_: self.close()) |
|
|
|
|
save_btn = Gtk.Button(label="Salva") |
|
|
|
|
save_btn.add_css_class("suggested-action") |
|
|
|
|
save_btn.connect("clicked", self._save) |
|
|
|
|
header.pack_start(cancel_btn) |
|
|
|
|
header.pack_end(save_btn) |
|
|
|
|
toolbar_view.add_top_bar(header) |
|
|
|
|
|
|
|
|
|
page = Adw.PreferencesPage() |
|
|
|
|
|
|
|
|
|
group = Adw.PreferencesGroup() |
|
|
|
|
group.set_title("Connessione") |
|
|
|
|
group.set_description( |
|
|
|
|
"Configura l'indirizzo del server Cellar e la cartella " |
|
|
|
|
"locale di Bottles usata per l'installazione." |
|
|
|
|
) |
|
|
|
|
root.append(description) |
|
|
|
|
|
|
|
|
|
grid = Gtk.Grid(column_spacing=12, row_spacing=12) |
|
|
|
|
|
|
|
|
|
server_label = Gtk.Label(label="Server", xalign=0) |
|
|
|
|
self.server_entry = Gtk.Entry(text=config.server_url) |
|
|
|
|
self.server_entry.set_hexpand(True) |
|
|
|
|
grid.attach(server_label, 0, 0, 1, 1) |
|
|
|
|
grid.attach(self.server_entry, 1, 0, 1, 1) |
|
|
|
|
|
|
|
|
|
bottles_label = Gtk.Label(label="Cartella Bottles", xalign=0) |
|
|
|
|
self.bottles_entry = Gtk.Entry(text=config.bottles_dir) |
|
|
|
|
self.bottles_entry.set_hexpand(True) |
|
|
|
|
grid.attach(bottles_label, 0, 1, 1, 1) |
|
|
|
|
grid.attach(self.bottles_entry, 1, 1, 1, 1) |
|
|
|
|
|
|
|
|
|
root.append(grid) |
|
|
|
|
self.server_row = Adw.EntryRow(title="Indirizzo server") |
|
|
|
|
self.server_row.set_text(config.server_url) |
|
|
|
|
group.add(self.server_row) |
|
|
|
|
|
|
|
|
|
button_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12) |
|
|
|
|
button_box.set_halign(Gtk.Align.END) |
|
|
|
|
self.bottles_row = Adw.EntryRow(title="Cartella Bottles") |
|
|
|
|
self.bottles_row.set_text(config.bottles_dir) |
|
|
|
|
group.add(self.bottles_row) |
|
|
|
|
|
|
|
|
|
cancel_button = Gtk.Button(label="Annulla") |
|
|
|
|
cancel_button.connect("clicked", lambda *_: self.close()) |
|
|
|
|
|
|
|
|
|
save_button = Gtk.Button(label="Salva") |
|
|
|
|
save_button.add_css_class("suggested-action") |
|
|
|
|
save_button.connect("clicked", self._save) |
|
|
|
|
|
|
|
|
|
button_box.append(cancel_button) |
|
|
|
|
button_box.append(save_button) |
|
|
|
|
root.append(button_box) |
|
|
|
|
|
|
|
|
|
self.set_child(root) |
|
|
|
|
page.add(group) |
|
|
|
|
toolbar_view.set_content(page) |
|
|
|
|
self.set_content(toolbar_view) |
|
|
|
|
|
|
|
|
|
def _save(self, *_args: object) -> None: |
|
|
|
|
server_url = self.server_entry.get_text().strip().rstrip("/") |
|
|
|
|
bottles_dir = self.bottles_entry.get_text().strip() |
|
|
|
|
|
|
|
|
|
server_url = self.server_row.get_text().strip().rstrip("/") |
|
|
|
|
bottles_dir = self.bottles_row.get_text().strip() |
|
|
|
|
if not server_url: |
|
|
|
|
self.parent.show_message("L'indirizzo del server non può essere vuoto.") |
|
|
|
|
return |
|
|
|
|
|
|
|
|
|
config = AppConfig( |
|
|
|
|
server_url=server_url, |
|
|
|
|
bottles_dir=str(Path(bottles_dir).expanduser()) if bottles_dir else str(DEFAULT_BOTTLES_DIR), |
|
|
|
|
replace_existing=self.parent.replace_switch.get_active(), |
|
|
|
|
bottles_dir=( |
|
|
|
|
str(Path(bottles_dir).expanduser()) if bottles_dir else str(DEFAULT_BOTTLES_DIR) |
|
|
|
|
), |
|
|
|
|
replace_existing=self._parent_win.replace_row.get_active(), |
|
|
|
|
) |
|
|
|
|
save_config(config) |
|
|
|
|
self.on_save(config) |
|
|
|
|
self._on_save(config) |
|
|
|
|
self.close() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── Archive Row ───────────────────────────────────────────────── |
|
|
|
|
class ArchiveRow(Gtk.ListBoxRow): |
|
|
|
|
"""A single archive entry in the list, with status icon, info and action button.""" |
|
|
|
|
|
|
|
|
|
def __init__( |
|
|
|
|
self, |
|
|
|
|
archive: dict[str, Any], |
|
|
|
|
@ -174,67 +216,96 @@ class ArchiveRow(Gtk.ListBoxRow):
|
|
|
|
|
): |
|
|
|
|
super().__init__() |
|
|
|
|
self.archive = archive |
|
|
|
|
self.set_selectable(False) |
|
|
|
|
self.set_activatable(False) |
|
|
|
|
|
|
|
|
|
root = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=14) |
|
|
|
|
root.add_css_class("archive-row") |
|
|
|
|
|
|
|
|
|
root = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12) |
|
|
|
|
root.set_margin_top(10) |
|
|
|
|
root.set_margin_bottom(10) |
|
|
|
|
root.set_margin_start(12) |
|
|
|
|
root.set_margin_end(12) |
|
|
|
|
# ── Status icon ── |
|
|
|
|
icon = Gtk.Image() |
|
|
|
|
icon.set_pixel_size(32) |
|
|
|
|
icon.set_valign(Gtk.Align.CENTER) |
|
|
|
|
if is_installed: |
|
|
|
|
icon.set_from_icon_name("object-select-symbolic") |
|
|
|
|
icon.add_css_class("success") |
|
|
|
|
else: |
|
|
|
|
icon.set_from_icon_name("folder-download-symbolic") |
|
|
|
|
icon.add_css_class("dim-label") |
|
|
|
|
|
|
|
|
|
# ── Text column ── |
|
|
|
|
text_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4) |
|
|
|
|
text_box.set_hexpand(True) |
|
|
|
|
text_box.set_valign(Gtk.Align.CENTER) |
|
|
|
|
|
|
|
|
|
title = archive.get("name") or archive.get("bottle_name") or "Archivio senza nome" |
|
|
|
|
bottle_name = archive.get("bottle_name") or "-" |
|
|
|
|
runner = archive.get("runner") or "-" |
|
|
|
|
arch = archive.get("arch") or "-" |
|
|
|
|
windows_version = archive.get("windows_version") or "-" |
|
|
|
|
created_at = archive.get("created_at") or "-" |
|
|
|
|
tags = archive.get("tags") or "-" |
|
|
|
|
description = archive.get("description") or "" |
|
|
|
|
size = format_size(archive.get("size_bytes")) |
|
|
|
|
|
|
|
|
|
title_label = Gtk.Label(xalign=0) |
|
|
|
|
title_label.set_markup(f"<b>{GLib.markup_escape_text(title)}</b>") |
|
|
|
|
|
|
|
|
|
details_text = ( |
|
|
|
|
f"Bottle: {bottle_name} • Runner: {runner} • Arch: {arch} • " |
|
|
|
|
f"Windows: {windows_version} • Size: {size} • Tag: {tags} • Creato: {created_at}" |
|
|
|
|
) |
|
|
|
|
if description: |
|
|
|
|
details_text = f"{details_text}\n{description}" |
|
|
|
|
|
|
|
|
|
details_label = Gtk.Label( |
|
|
|
|
title_label.set_text(title) |
|
|
|
|
title_label.add_css_class("archive-title") |
|
|
|
|
|
|
|
|
|
# Info line: bottle · runner · arch · windows · size |
|
|
|
|
bottle_name = archive.get("bottle_name") or "—" |
|
|
|
|
runner = archive.get("runner") or "—" |
|
|
|
|
arch = archive.get("arch") or "—" |
|
|
|
|
win_ver = archive.get("windows_version") or "—" |
|
|
|
|
size = format_size(archive.get("size_bytes")) |
|
|
|
|
info_label = Gtk.Label( |
|
|
|
|
xalign=0, |
|
|
|
|
wrap=True, |
|
|
|
|
selectable=True, |
|
|
|
|
label=details_text, |
|
|
|
|
label=f"{bottle_name} · {runner} · {arch} · {win_ver} · {size}", |
|
|
|
|
) |
|
|
|
|
details_label.add_css_class("dim-label") |
|
|
|
|
info_label.add_css_class("dim-label") |
|
|
|
|
info_label.add_css_class("archive-meta") |
|
|
|
|
|
|
|
|
|
status_label = Gtk.Label(xalign=0) |
|
|
|
|
if is_installed: |
|
|
|
|
status_label.set_markup("<span foreground='#2e7d32'><b>Già installata sul sistema</b></span>") |
|
|
|
|
else: |
|
|
|
|
status_label.set_markup("<span foreground='#666666'>Non installata localmente</span>") |
|
|
|
|
# Meta line: tags · date · description |
|
|
|
|
tags = archive.get("tags") or "" |
|
|
|
|
created = (archive.get("created_at") or "")[:10] |
|
|
|
|
description = archive.get("description") or "" |
|
|
|
|
meta_parts = [p for p in (tags, created, description) if p] |
|
|
|
|
|
|
|
|
|
text_box.append(title_label) |
|
|
|
|
text_box.append(details_label) |
|
|
|
|
text_box.append(status_label) |
|
|
|
|
text_box.append(info_label) |
|
|
|
|
if meta_parts: |
|
|
|
|
meta_label = Gtk.Label( |
|
|
|
|
xalign=0, wrap=True, label=" · ".join(meta_parts) |
|
|
|
|
) |
|
|
|
|
meta_label.add_css_class("dim-label") |
|
|
|
|
meta_label.add_css_class("archive-meta") |
|
|
|
|
text_box.append(meta_label) |
|
|
|
|
|
|
|
|
|
button_label = "Scarica e installa" |
|
|
|
|
# Status chip |
|
|
|
|
status_label = Gtk.Label(xalign=0) |
|
|
|
|
status_label.add_css_class("status-chip") |
|
|
|
|
if is_installed: |
|
|
|
|
status_label.set_label("✓ Installata") |
|
|
|
|
status_label.add_css_class("installed") |
|
|
|
|
else: |
|
|
|
|
status_label.set_label("Non installata") |
|
|
|
|
status_label.add_css_class("not-installed") |
|
|
|
|
chip_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) |
|
|
|
|
chip_box.set_margin_top(2) |
|
|
|
|
chip_box.append(status_label) |
|
|
|
|
text_box.append(chip_box) |
|
|
|
|
|
|
|
|
|
# ── Install button ── |
|
|
|
|
if is_installed and replace_enabled: |
|
|
|
|
button_label = "Reinstalla" |
|
|
|
|
btn_label = "Reinstalla" |
|
|
|
|
elif is_installed: |
|
|
|
|
button_label = "Già installata" |
|
|
|
|
btn_label = "Già installata" |
|
|
|
|
else: |
|
|
|
|
btn_label = "Installa" |
|
|
|
|
|
|
|
|
|
self.install_button = Gtk.Button(label=button_label) |
|
|
|
|
self.install_button = Gtk.Button(label=btn_label) |
|
|
|
|
self.install_button.set_valign(Gtk.Align.CENTER) |
|
|
|
|
self.install_button.add_css_class("pill") |
|
|
|
|
if not is_installed or replace_enabled: |
|
|
|
|
self.install_button.add_css_class("suggested-action") |
|
|
|
|
self.install_button.set_valign(Gtk.Align.CENTER) |
|
|
|
|
else: |
|
|
|
|
self.install_button.add_css_class("flat") |
|
|
|
|
self.install_button.connect("clicked", lambda *_: on_install(self.archive)) |
|
|
|
|
self.install_button.set_sensitive((not is_installed) or replace_enabled) |
|
|
|
|
|
|
|
|
|
root.append(icon) |
|
|
|
|
root.append(text_box) |
|
|
|
|
root.append(self.install_button) |
|
|
|
|
self.set_child(root) |
|
|
|
|
@ -243,9 +314,12 @@ class ArchiveRow(Gtk.ListBoxRow):
|
|
|
|
|
self.install_button.set_sensitive(enabled) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CellarWindow(Gtk.ApplicationWindow): |
|
|
|
|
def __init__(self, app: Gtk.Application): |
|
|
|
|
super().__init__(application=app, title="Cellar GTK") |
|
|
|
|
# ── Main Window ───────────────────────────────────────────────── |
|
|
|
|
class CellarWindow(Adw.ApplicationWindow): |
|
|
|
|
"""Primary application window using Adw layout widgets.""" |
|
|
|
|
|
|
|
|
|
def __init__(self, app: Adw.Application): |
|
|
|
|
super().__init__(application=app, title="Cellar") |
|
|
|
|
self.config = load_config() |
|
|
|
|
self.settings_window: SettingsWindow | None = None |
|
|
|
|
self.rows: list[ArchiveRow] = [] |
|
|
|
|
@ -253,130 +327,157 @@ class CellarWindow(Gtk.ApplicationWindow):
|
|
|
|
|
self.local_bottle_names: set[str] = set() |
|
|
|
|
self.busy = False |
|
|
|
|
|
|
|
|
|
self.set_default_size(980, 700) |
|
|
|
|
self.set_default_size(800, 680) |
|
|
|
|
|
|
|
|
|
root = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) |
|
|
|
|
root.set_margin_top(12) |
|
|
|
|
root.set_margin_bottom(12) |
|
|
|
|
root.set_margin_start(12) |
|
|
|
|
root.set_margin_end(12) |
|
|
|
|
# ── Header bar ── |
|
|
|
|
header = Adw.HeaderBar() |
|
|
|
|
self._window_title = Adw.WindowTitle(title="Cellar", subtitle="Bottle Archive") |
|
|
|
|
header.set_title_widget(self._window_title) |
|
|
|
|
|
|
|
|
|
header = Gtk.HeaderBar() |
|
|
|
|
header.set_title_widget(Gtk.Label(label="Cellar")) |
|
|
|
|
|
|
|
|
|
self.refresh_button = Gtk.Button(label="Aggiorna") |
|
|
|
|
self.refresh_button = Gtk.Button( |
|
|
|
|
icon_name="view-refresh-symbolic", |
|
|
|
|
tooltip_text="Aggiorna catalogo", |
|
|
|
|
) |
|
|
|
|
self.refresh_button.connect("clicked", lambda *_: self.refresh_archives()) |
|
|
|
|
header.pack_start(self.refresh_button) |
|
|
|
|
|
|
|
|
|
self.settings_button = Gtk.Button(label="Configurazione") |
|
|
|
|
self.spinner = Gtk.Spinner() |
|
|
|
|
header.pack_start(self.spinner) |
|
|
|
|
|
|
|
|
|
self.settings_button = Gtk.Button( |
|
|
|
|
icon_name="emblem-system-symbolic", |
|
|
|
|
tooltip_text="Configurazione", |
|
|
|
|
) |
|
|
|
|
self.settings_button.connect("clicked", lambda *_: self.open_settings()) |
|
|
|
|
header.pack_end(self.settings_button) |
|
|
|
|
self.set_titlebar(header) |
|
|
|
|
|
|
|
|
|
info_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) |
|
|
|
|
info_box.add_css_class("card") |
|
|
|
|
info_box.set_margin_bottom(6) |
|
|
|
|
info_box.set_margin_top(6) |
|
|
|
|
info_box.set_margin_start(6) |
|
|
|
|
info_box.set_margin_end(6) |
|
|
|
|
# ── Toast overlay ── |
|
|
|
|
self.toast_overlay = Adw.ToastOverlay() |
|
|
|
|
|
|
|
|
|
self.server_label = Gtk.Label(xalign=0, selectable=True) |
|
|
|
|
self.bottles_label = Gtk.Label(xalign=0, selectable=True) |
|
|
|
|
self.replace_switch = Gtk.Switch(active=self.config.replace_existing) |
|
|
|
|
self.replace_switch.connect("notify::active", self._store_replace_preference) |
|
|
|
|
# ── Outer box (banner + progress + scrolled content) ── |
|
|
|
|
outer_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) |
|
|
|
|
|
|
|
|
|
replace_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12) |
|
|
|
|
replace_box.append(Gtk.Label(label="Sostituisci installazioni esistenti", xalign=0)) |
|
|
|
|
replace_box.append(self.replace_switch) |
|
|
|
|
self.banner = Adw.Banner() |
|
|
|
|
self.banner.set_revealed(False) |
|
|
|
|
outer_box.append(self.banner) |
|
|
|
|
|
|
|
|
|
info_box.append(self.server_label) |
|
|
|
|
info_box.append(self.bottles_label) |
|
|
|
|
info_box.append(replace_box) |
|
|
|
|
self.progress_bar = Gtk.ProgressBar() |
|
|
|
|
self.progress_bar.set_visible(False) |
|
|
|
|
outer_box.append(self.progress_bar) |
|
|
|
|
|
|
|
|
|
search_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12) |
|
|
|
|
search_box.append(Gtk.Label(label="Cerca", xalign=0)) |
|
|
|
|
# ── Scrolled area ── |
|
|
|
|
scroller = Gtk.ScrolledWindow() |
|
|
|
|
scroller.set_vexpand(True) |
|
|
|
|
|
|
|
|
|
self.search_entry = Gtk.SearchEntry() |
|
|
|
|
self.search_entry.set_hexpand(True) |
|
|
|
|
self.search_entry.set_placeholder_text("Nome, bottle, tag, runner, descrizione…") |
|
|
|
|
self.search_entry.connect("search-changed", self._on_search_changed) |
|
|
|
|
search_box.append(self.search_entry) |
|
|
|
|
clamp = Adw.Clamp() |
|
|
|
|
clamp.set_maximum_size(720) |
|
|
|
|
clamp.set_tightening_threshold(500) |
|
|
|
|
|
|
|
|
|
clear_search_button = Gtk.Button(label="Pulisci") |
|
|
|
|
clear_search_button.connect("clicked", lambda *_: self.search_entry.set_text("")) |
|
|
|
|
search_box.append(clear_search_button) |
|
|
|
|
content_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=18) |
|
|
|
|
content_box.set_margin_top(18) |
|
|
|
|
content_box.set_margin_bottom(24) |
|
|
|
|
content_box.set_margin_start(12) |
|
|
|
|
content_box.set_margin_end(12) |
|
|
|
|
|
|
|
|
|
self.summary_label = Gtk.Label(xalign=0) |
|
|
|
|
# ── Info group (PreferencesGroup) ── |
|
|
|
|
info_group = Adw.PreferencesGroup() |
|
|
|
|
info_group.set_title("Connessione") |
|
|
|
|
|
|
|
|
|
self.status_label = Gtk.Label(xalign=0, wrap=True) |
|
|
|
|
self.progress_bar = Gtk.ProgressBar() |
|
|
|
|
self.progress_bar.set_hexpand(True) |
|
|
|
|
self.progress_bar.set_show_text(False) |
|
|
|
|
self.spinner = Gtk.Spinner() |
|
|
|
|
self.server_info_row = Adw.ActionRow(title="Server") |
|
|
|
|
self.server_info_row.set_subtitle(self.config.server_url) |
|
|
|
|
self.server_info_row.add_css_class("property") |
|
|
|
|
info_group.add(self.server_info_row) |
|
|
|
|
|
|
|
|
|
status_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12) |
|
|
|
|
status_box.append(self.spinner) |
|
|
|
|
status_box.append(self.progress_bar) |
|
|
|
|
self.bottles_info_row = Adw.ActionRow(title="Cartella locale") |
|
|
|
|
self.bottles_info_row.set_subtitle(self.config.bottles_dir) |
|
|
|
|
self.bottles_info_row.add_css_class("property") |
|
|
|
|
info_group.add(self.bottles_info_row) |
|
|
|
|
|
|
|
|
|
root.append(info_box) |
|
|
|
|
root.append(search_box) |
|
|
|
|
root.append(self.summary_label) |
|
|
|
|
root.append(self.status_label) |
|
|
|
|
root.append(status_box) |
|
|
|
|
self.replace_row = Adw.SwitchRow(title="Sostituisci installazioni esistenti") |
|
|
|
|
self.replace_row.set_active(self.config.replace_existing) |
|
|
|
|
self.replace_row.connect("notify::active", self._store_replace_preference) |
|
|
|
|
info_group.add(self.replace_row) |
|
|
|
|
|
|
|
|
|
self.placeholder_label = Gtk.Label( |
|
|
|
|
label="Nessun archivio disponibile. Premi 'Aggiorna' per leggere il catalogo dal server.", |
|
|
|
|
wrap=True, |
|
|
|
|
justify=Gtk.Justification.CENTER, |
|
|
|
|
content_box.append(info_group) |
|
|
|
|
|
|
|
|
|
# ── Search entry ── |
|
|
|
|
self.search_entry = Gtk.SearchEntry() |
|
|
|
|
self.search_entry.set_placeholder_text( |
|
|
|
|
"Cerca per nome, bottle, tag, runner, descrizione…" |
|
|
|
|
) |
|
|
|
|
self.search_entry.connect("search-changed", self._on_search_changed) |
|
|
|
|
content_box.append(self.search_entry) |
|
|
|
|
|
|
|
|
|
# ── Summary ── |
|
|
|
|
self.summary_label = Gtk.Label(xalign=0) |
|
|
|
|
self.summary_label.add_css_class("dim-label") |
|
|
|
|
self.summary_label.add_css_class("caption") |
|
|
|
|
content_box.append(self.summary_label) |
|
|
|
|
|
|
|
|
|
# ── Archive list / empty state ── |
|
|
|
|
self.status_page = Adw.StatusPage() |
|
|
|
|
self.status_page.set_icon_name("system-software-install-symbolic") |
|
|
|
|
self.status_page.set_title("Nessun archivio") |
|
|
|
|
self.status_page.set_description( |
|
|
|
|
"Premi il pulsante ↻ per caricare il catalogo dal server." |
|
|
|
|
) |
|
|
|
|
self.placeholder_label.set_vexpand(True) |
|
|
|
|
|
|
|
|
|
self.listbox = Gtk.ListBox() |
|
|
|
|
self.listbox.set_selection_mode(Gtk.SelectionMode.NONE) |
|
|
|
|
self.listbox.add_css_class("boxed-list") |
|
|
|
|
|
|
|
|
|
scroller = Gtk.ScrolledWindow() |
|
|
|
|
scroller.set_vexpand(True) |
|
|
|
|
scroller.set_hexpand(True) |
|
|
|
|
scroller.set_child(self.listbox) |
|
|
|
|
|
|
|
|
|
self.content_stack = Gtk.Stack() |
|
|
|
|
self.content_stack.add_named(self.placeholder_label, "placeholder") |
|
|
|
|
self.content_stack.add_named(scroller, "list") |
|
|
|
|
self.content_stack.set_transition_type(Gtk.StackTransitionType.CROSSFADE) |
|
|
|
|
self.content_stack.set_transition_duration(200) |
|
|
|
|
self.content_stack.add_named(self.status_page, "placeholder") |
|
|
|
|
self.content_stack.add_named(self.listbox, "list") |
|
|
|
|
|
|
|
|
|
root.append(self.content_stack) |
|
|
|
|
self.set_child(root) |
|
|
|
|
content_box.append(self.content_stack) |
|
|
|
|
|
|
|
|
|
self.update_info_labels() |
|
|
|
|
clamp.set_child(content_box) |
|
|
|
|
scroller.set_child(clamp) |
|
|
|
|
outer_box.append(scroller) |
|
|
|
|
|
|
|
|
|
self.toast_overlay.set_child(outer_box) |
|
|
|
|
|
|
|
|
|
# ── ToolbarView ── |
|
|
|
|
toolbar_view = Adw.ToolbarView() |
|
|
|
|
toolbar_view.add_top_bar(header) |
|
|
|
|
toolbar_view.set_content(self.toast_overlay) |
|
|
|
|
|
|
|
|
|
self.set_content(toolbar_view) |
|
|
|
|
|
|
|
|
|
# ── Init state ── |
|
|
|
|
self.refresh_local_bottles() |
|
|
|
|
self.set_status("Pronto.", 0.0) |
|
|
|
|
self.update_summary(0, 0) |
|
|
|
|
self.refresh_archives() |
|
|
|
|
|
|
|
|
|
# ── Preferences ───────────────────────────────────────────── |
|
|
|
|
def _store_replace_preference(self, *_args: object) -> None: |
|
|
|
|
self.config.replace_existing = self.replace_switch.get_active() |
|
|
|
|
self.config.replace_existing = self.replace_row.get_active() |
|
|
|
|
save_config(self.config) |
|
|
|
|
self.apply_archive_filter() |
|
|
|
|
|
|
|
|
|
def update_info_labels(self) -> None: |
|
|
|
|
self.server_label.set_text(f"Server: {self.config.server_url}") |
|
|
|
|
self.bottles_label.set_text(f"Cartella locale: {self.config.bottles_dir}") |
|
|
|
|
self.replace_switch.set_active(self.config.replace_existing) |
|
|
|
|
self.server_info_row.set_subtitle(self.config.server_url) |
|
|
|
|
self.bottles_info_row.set_subtitle(self.config.bottles_dir) |
|
|
|
|
self.replace_row.set_active(self.config.replace_existing) |
|
|
|
|
|
|
|
|
|
def update_summary(self, visible_count: int, total_count: int) -> None: |
|
|
|
|
if total_count == 0: |
|
|
|
|
self.summary_label.set_text("Nessun archivio caricato.") |
|
|
|
|
return |
|
|
|
|
if visible_count == total_count: |
|
|
|
|
elif visible_count == total_count: |
|
|
|
|
self.summary_label.set_text(f"Archivi visibili: {visible_count}") |
|
|
|
|
return |
|
|
|
|
self.summary_label.set_text(f"Archivi visibili: {visible_count} su {total_count}") |
|
|
|
|
else: |
|
|
|
|
self.summary_label.set_text( |
|
|
|
|
f"Archivi visibili: {visible_count} su {total_count}" |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
# ── Settings window ───────────────────────────────────────── |
|
|
|
|
def open_settings(self) -> None: |
|
|
|
|
if self.settings_window is not None: |
|
|
|
|
self.settings_window.present() |
|
|
|
|
return |
|
|
|
|
|
|
|
|
|
self.settings_window = SettingsWindow(self, self.config, self.apply_config) |
|
|
|
|
self.settings_window.connect("close-request", self._on_settings_closed) |
|
|
|
|
self.settings_window.present() |
|
|
|
|
@ -392,14 +493,15 @@ class CellarWindow(Gtk.ApplicationWindow):
|
|
|
|
|
self.refresh_local_bottles() |
|
|
|
|
self.refresh_archives() |
|
|
|
|
|
|
|
|
|
# ── Local bottles ─────────────────────────────────────────── |
|
|
|
|
def refresh_local_bottles(self) -> None: |
|
|
|
|
bottles_dir = Path(self.config.bottles_dir).expanduser() |
|
|
|
|
local_bottles = collect_local_bottles(bottles_dir) |
|
|
|
|
known_names: set[str] = set() |
|
|
|
|
for bottle in local_bottles: |
|
|
|
|
known_names.add(normalize_name(bottle.get("directory"))) |
|
|
|
|
known_names.add(normalize_name(bottle.get("name"))) |
|
|
|
|
self.local_bottle_names = {name for name in known_names if name} |
|
|
|
|
known: set[str] = set() |
|
|
|
|
for b in local_bottles: |
|
|
|
|
known.add(normalize_name(b.get("directory"))) |
|
|
|
|
known.add(normalize_name(b.get("name"))) |
|
|
|
|
self.local_bottle_names = {n for n in known if n} |
|
|
|
|
|
|
|
|
|
def is_archive_installed(self, archive: dict[str, Any]) -> bool: |
|
|
|
|
candidates = { |
|
|
|
|
@ -407,8 +509,9 @@ class CellarWindow(Gtk.ApplicationWindow):
|
|
|
|
|
normalize_name(archive.get("name")), |
|
|
|
|
} |
|
|
|
|
candidates.discard("") |
|
|
|
|
return any(candidate in self.local_bottle_names for candidate in candidates) |
|
|
|
|
return any(c in self.local_bottle_names for c in candidates) |
|
|
|
|
|
|
|
|
|
# ── Busy / status ─────────────────────────────────────────── |
|
|
|
|
def set_busy(self, busy: bool) -> None: |
|
|
|
|
self.busy = busy |
|
|
|
|
self.refresh_button.set_sensitive(not busy) |
|
|
|
|
@ -422,34 +525,35 @@ class CellarWindow(Gtk.ApplicationWindow):
|
|
|
|
|
self.spinner.stop() |
|
|
|
|
|
|
|
|
|
def set_status(self, message: str, fraction: float | None = None) -> None: |
|
|
|
|
self.status_label.set_text(message) |
|
|
|
|
self.banner.set_title(message) |
|
|
|
|
self.banner.set_revealed(True) |
|
|
|
|
self.progress_bar.set_visible(True) |
|
|
|
|
if fraction is None: |
|
|
|
|
self.progress_bar.pulse() |
|
|
|
|
else: |
|
|
|
|
self.progress_bar.set_fraction(max(0.0, min(1.0, fraction))) |
|
|
|
|
|
|
|
|
|
def show_message(self, message: str, title: str = "Cellar") -> None: |
|
|
|
|
dialog = Gtk.MessageDialog( |
|
|
|
|
transient_for=self, |
|
|
|
|
modal=True, |
|
|
|
|
buttons=Gtk.ButtonsType.OK, |
|
|
|
|
text=title, |
|
|
|
|
secondary_text=message, |
|
|
|
|
) |
|
|
|
|
dialog.connect("response", lambda d, *_: d.close()) |
|
|
|
|
dialog.present() |
|
|
|
|
def _hide_status(self) -> None: |
|
|
|
|
self.banner.set_revealed(False) |
|
|
|
|
self.progress_bar.set_visible(False) |
|
|
|
|
|
|
|
|
|
def _toast(self, message: str, timeout: int = 3) -> None: |
|
|
|
|
toast = Adw.Toast(title=message) |
|
|
|
|
toast.set_timeout(timeout) |
|
|
|
|
self.toast_overlay.add_toast(toast) |
|
|
|
|
|
|
|
|
|
# ── Archive list ──────────────────────────────────────────── |
|
|
|
|
def clear_rows(self) -> None: |
|
|
|
|
self.rows.clear() |
|
|
|
|
child = self.listbox.get_first_child() |
|
|
|
|
while child is not None: |
|
|
|
|
next_child = child.get_next_sibling() |
|
|
|
|
nxt = child.get_next_sibling() |
|
|
|
|
self.listbox.remove(child) |
|
|
|
|
child = next_child |
|
|
|
|
child = nxt |
|
|
|
|
|
|
|
|
|
def render_archives(self, archives: list[dict[str, Any]]) -> None: |
|
|
|
|
self.clear_rows() |
|
|
|
|
replace_enabled = self.replace_switch.get_active() |
|
|
|
|
replace_enabled = self.replace_row.get_active() |
|
|
|
|
for archive in archives: |
|
|
|
|
row = ArchiveRow( |
|
|
|
|
archive, |
|
|
|
|
@ -462,31 +566,42 @@ class CellarWindow(Gtk.ApplicationWindow):
|
|
|
|
|
self.listbox.append(row) |
|
|
|
|
|
|
|
|
|
if self.all_archives and not archives: |
|
|
|
|
self.placeholder_label.set_text("Nessun archivio corrisponde al filtro corrente.") |
|
|
|
|
self.status_page.set_icon_name("edit-find-symbolic") |
|
|
|
|
self.status_page.set_title("Nessun risultato") |
|
|
|
|
self.status_page.set_description( |
|
|
|
|
"Nessun archivio corrisponde al filtro corrente." |
|
|
|
|
) |
|
|
|
|
elif not self.all_archives: |
|
|
|
|
self.placeholder_label.set_text("Nessun archivio disponibile. Premi 'Aggiorna' per leggere il catalogo dal server.") |
|
|
|
|
self.status_page.set_icon_name("system-software-install-symbolic") |
|
|
|
|
self.status_page.set_title("Nessun archivio") |
|
|
|
|
self.status_page.set_description( |
|
|
|
|
"Premi il pulsante ↻ per caricare il catalogo dal server." |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
self.content_stack.set_visible_child_name("list" if archives else "placeholder") |
|
|
|
|
self.content_stack.set_visible_child_name( |
|
|
|
|
"list" if archives else "placeholder" |
|
|
|
|
) |
|
|
|
|
self.update_summary(len(archives), len(self.all_archives)) |
|
|
|
|
|
|
|
|
|
def apply_archive_filter(self) -> None: |
|
|
|
|
query = self.search_entry.get_text() |
|
|
|
|
filtered = [archive for archive in self.all_archives if archive_matches_query(archive, query)] |
|
|
|
|
filtered = [ |
|
|
|
|
a for a in self.all_archives if archive_matches_query(a, query) |
|
|
|
|
] |
|
|
|
|
self.render_archives(filtered) |
|
|
|
|
|
|
|
|
|
def _on_search_changed(self, *_args: object) -> None: |
|
|
|
|
self.apply_archive_filter() |
|
|
|
|
|
|
|
|
|
# ── Background work ───────────────────────────────────────── |
|
|
|
|
def run_background(self, worker: Callable[[], None]) -> None: |
|
|
|
|
thread = threading.Thread(target=worker, daemon=True) |
|
|
|
|
thread.start() |
|
|
|
|
threading.Thread(target=worker, daemon=True).start() |
|
|
|
|
|
|
|
|
|
def refresh_archives(self) -> None: |
|
|
|
|
if self.busy: |
|
|
|
|
return |
|
|
|
|
|
|
|
|
|
self.set_busy(True) |
|
|
|
|
self.set_status("Caricamento catalogo dal server…", None) |
|
|
|
|
self.set_status("Caricamento catalogo dal server…") |
|
|
|
|
|
|
|
|
|
def worker() -> None: |
|
|
|
|
try: |
|
|
|
|
@ -494,7 +609,6 @@ class CellarWindow(Gtk.ApplicationWindow):
|
|
|
|
|
except Exception as exc: |
|
|
|
|
GLib.idle_add(self._handle_error, exc) |
|
|
|
|
return |
|
|
|
|
|
|
|
|
|
GLib.idle_add(self._finish_refresh, archives) |
|
|
|
|
|
|
|
|
|
self.run_background(worker) |
|
|
|
|
@ -503,20 +617,31 @@ class CellarWindow(Gtk.ApplicationWindow):
|
|
|
|
|
self.all_archives = archives |
|
|
|
|
self.refresh_local_bottles() |
|
|
|
|
self.apply_archive_filter() |
|
|
|
|
message = f"Trovati {len(archives)} archivi sul server." if archives else "Nessun archivio trovato sul server." |
|
|
|
|
self.set_status(message, 1.0) |
|
|
|
|
self._hide_status() |
|
|
|
|
n = len(archives) |
|
|
|
|
self._window_title.set_subtitle( |
|
|
|
|
f"{n} archivi{'o' if n == 1 else ''}" if n else "Bottle Archive" |
|
|
|
|
) |
|
|
|
|
if archives: |
|
|
|
|
self._toast(f"Catalogo aggiornato: {n} archivi{'o' if n == 1 else ''}") |
|
|
|
|
else: |
|
|
|
|
self._toast("Nessun archivio trovato sul server.") |
|
|
|
|
self.set_busy(False) |
|
|
|
|
return False |
|
|
|
|
|
|
|
|
|
# ── Install ───────────────────────────────────────────────── |
|
|
|
|
def install_archive(self, archive: dict[str, Any]) -> None: |
|
|
|
|
if self.busy: |
|
|
|
|
return |
|
|
|
|
|
|
|
|
|
archive_name = archive.get("name") or archive.get("bottle_name") or f"#{archive.get('id', '?')}" |
|
|
|
|
archive_name = ( |
|
|
|
|
archive.get("name") |
|
|
|
|
or archive.get("bottle_name") |
|
|
|
|
or f"#{archive.get('id', '?')}" |
|
|
|
|
) |
|
|
|
|
bottles_dir = Path(self.config.bottles_dir).expanduser() |
|
|
|
|
replace_existing = self.replace_switch.get_active() |
|
|
|
|
replace_existing = self.replace_row.get_active() |
|
|
|
|
self.set_busy(True) |
|
|
|
|
self.set_status(f"Avvio installazione di {archive_name}…", 0.0) |
|
|
|
|
self.set_status(f"Installazione di {archive_name}…", 0.0) |
|
|
|
|
|
|
|
|
|
def progress(message: str, fraction: float | None) -> None: |
|
|
|
|
GLib.idle_add(self.set_status, message, fraction) |
|
|
|
|
@ -538,40 +663,40 @@ class CellarWindow(Gtk.ApplicationWindow):
|
|
|
|
|
|
|
|
|
|
def _finish_install(self, archive_name: str, result: int) -> bool: |
|
|
|
|
self.set_busy(False) |
|
|
|
|
self._hide_status() |
|
|
|
|
if result == 0: |
|
|
|
|
self.refresh_local_bottles() |
|
|
|
|
self.apply_archive_filter() |
|
|
|
|
self.set_status(f"Installazione completata: {archive_name}", 1.0) |
|
|
|
|
self.show_message(f"Installazione completata con successo: {archive_name}") |
|
|
|
|
self._toast(f"✓ {archive_name} installata con successo") |
|
|
|
|
else: |
|
|
|
|
self.set_status(f"Installazione non completata: {archive_name}", 0.0) |
|
|
|
|
self.show_message( |
|
|
|
|
"L'installazione non è stata completata. Controlla se la bottle esiste già oppure abilita la sostituzione.", |
|
|
|
|
title="Installazione interrotta", |
|
|
|
|
self._toast( |
|
|
|
|
"Installazione non completata – abilita la sostituzione se la bottle esiste già.", |
|
|
|
|
timeout=5, |
|
|
|
|
) |
|
|
|
|
return False |
|
|
|
|
|
|
|
|
|
# ── Error handling ────────────────────────────────────────── |
|
|
|
|
def _handle_error(self, exc: Exception) -> bool: |
|
|
|
|
self.set_busy(False) |
|
|
|
|
self.set_status("Operazione non riuscita.", 0.0) |
|
|
|
|
|
|
|
|
|
self._hide_status() |
|
|
|
|
if isinstance(exc, urllib.error.HTTPError): |
|
|
|
|
detail = exc.read().decode("utf-8", errors="ignore") |
|
|
|
|
message = f"HTTP {exc.code}: {detail or exc.reason}" |
|
|
|
|
msg = f"HTTP {exc.code}: {detail or exc.reason}" |
|
|
|
|
elif isinstance(exc, urllib.error.URLError): |
|
|
|
|
message = f"Errore di connessione: {exc.reason}" |
|
|
|
|
msg = f"Errore di connessione: {exc.reason}" |
|
|
|
|
else: |
|
|
|
|
message = str(exc) |
|
|
|
|
|
|
|
|
|
self.show_message(message, title="Errore") |
|
|
|
|
msg = str(exc) |
|
|
|
|
self._toast(msg, timeout=5) |
|
|
|
|
return False |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CellarApplication(Gtk.Application): |
|
|
|
|
# ── Application ───────────────────────────────────────────────── |
|
|
|
|
class CellarApplication(Adw.Application): |
|
|
|
|
def __init__(self) -> None: |
|
|
|
|
super().__init__(application_id="com.cellar.gtkclient") |
|
|
|
|
|
|
|
|
|
def do_activate(self) -> None: |
|
|
|
|
_load_css() |
|
|
|
|
window = self.props.active_window |
|
|
|
|
if window is None: |
|
|
|
|
window = CellarWindow(self) |
|
|
|
|
|