backend rewrite cont, rebrand

This commit is contained in:
2025-12-06 00:41:38 +01:00
parent 8428efdbce
commit e651c8e18a
33 changed files with 498 additions and 428 deletions

View File

@@ -6,66 +6,24 @@ import json
import os
import math
from pathlib import Path, cwd
from dataclasses import dataclass
from gi.repository import Gio, GLib, GObject
# pseudo struct that contains the profile data that gets serialized into the .json profile.
@dataclass
class Profile:
# Dictionary that stores the ownership status of frames
owned_frames: [str, bool]
unique_frames_owned: int
missing_basics: [str]
missing_basics_count: int
missing_primes: [str]
missing_primes_count: int
class Backend():
# warframe counts
# TODO Update via API/wiki
BASIC_WARFRAMES = 63
PRIME_WARFRAMES = 49
CURRENT_UPDATE = 41
# Profile
profile: Profile
# tracks whether the onboarding dialogue should be shown
fresh_install = True
def get_config_dir(app_id: str) -> Path:
base = Path(GLib.get_user_config_dir())
cfg_dir = base / app_id
cfg_dir.mkdir(parents=True, exist_ok=True)
return cfg_dir
# helpers for profile loading
APP_ID = "gay.valhrafnaz.Gnomeframe"
CONFIG_DIR = get_config_dir(APP_ID)
CONFIG_FILE = CONFIG_DIR / "profile.json"
def __init__():
loaded_profile: dict[str, bool] = Backend.load_profile()
super().__init__()
# load profile
def load_profile() -> dict[str, bool]:
try:
with Backend.CONFIG_FILE.open("r", encoding="utf-8") as f:
data = json.load(f)
return {k: bool(v) for k, v in data.items()}
except FileNotFoundError:
return {}
except (json.JSONDecodeError, OSError) as exc:
print(f"Could not load profile: {exc}")
return {}
def save_profile(state: dict[str, bool]) -> None:
try:
Backend.CONFIG_DIR.mkdir(parents=True, exist_ok=True)
with Backend.CONFIG_FILE.open("w", encoding="utf-8") as f:
json.dump(state, f, indent=2, sort_keys=True)
except OSError as exc:
print(f"Failed to write profile: {exc}")

123
src/gnomeframe/constants.py Normal file
View File

@@ -0,0 +1,123 @@
EXISTING_FRAMES_BASIC = [
"ash",
"atlas",
"banshee",
"baruuk",
"caliban",
"chroma",
"citrine",
"cyte09",
"dagath",
"dante",
"ember",
"equinox",
"excalibur",
"frost",
"gara",
"garuda",
"gauss",
"grendel",
"gyre",
"harrow",
"hildryn",
"hydroid",
"inaros",
"ivara",
"jade",
"khora",
"koumei",
"kullervo",
"lavos",
"limbo",
"loki",
"mag",
"mesa",
"mirage",
"nekros",
"nezha",
"nidus",
"nokko",
"nova",
"nyx",
"oberon",
"octavia",
"oraxia",
"protea",
"qorvex",
"revenant",
"rhino",
"saryn",
"sevagoth",
"styanax",
"temple",
"titania",
"trinity",
"uriel",
"valkyr",
"vauban",
"volt",
"voruna",
"wisp",
"wukong",
"xaku",
"yareli",
"zephyr",
]
EXISTING_FRAMES_PRIME = [
"ash_prime",
"atlas_prime",
"banshee_prime",
"baruuk_prime",
"caliban_prime",
"chroma_prime",
"ember_prime",
"equinox_prime",
"excalibur_umbra",
"frost_prime",
"gara_prime",
"garuda_prime",
"gauss_prime",
"grendel_prime",
"gyre_prime",
"harrow_prime",
"hildryn_prime",
"hydroid_prime",
"inaros_prime",
"ivara_prime",
"khora_prime",
"lavos_prime",
"limbo_prime",
"loki_prime",
"mag_prime",
"mesa_prime",
"mirage_prime",
"nekros_prime",
"nezha_prime",
"nidus_prime",
"nova_prime",
"nyx_prime",
"oberon_prime",
"octavia_prime",
"protea_prime",
"revenant_prime",
"rhino_prime",
"saryn_prime",
"sevagoth_prime",
"titania_prime",
"trinity_prime",
"valkyr_prime",
"vauban_prime",
"volt_prime",
"wisp_prime",
"wukong_prime",
"xaku_prime",
"yareli_prime",
"zephyr_prime"
]
# warframe counts
# TODO Update via API/wiki
BASIC_WARFRAMES = 63
PRIME_WARFRAMES = 49
CURRENT_UPDATE = 41

View File

@@ -1,6 +1,6 @@
# main.py
#
# Copyright 2025 nihil
# Copyright 2025 valhrafnaz
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
@@ -16,24 +16,57 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: AGPL-3.0-or-later
import sys
import gi
import os
import json
import math
from pathlib import Path
# local imports
from .window import VoidManifestWindow
from .profile import Profile
from .constants import EXISTING_FRAMES_BASIC, EXISTING_FRAMES_PRIME, BASIC_WARFRAMES, PRIME_WARFRAMES, CURRENT_UPDATE
gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
from gi.repository import Gtk, Gio, Adw, GLib
from .window import GnomeframeWindow
from gi.repository import Gtk, Gio, Adw, GLib, GObject # noqa: E402
def get_config_dir(app_id: str) -> Path:
base = Path(GLib.get_user_config_dir())
cfg_dir = base / app_id
cfg_dir.mkdir(parents=True, exist_ok=True)
return cfg_dir
class GnomeframeApplication(Adw.Application):
class VoidManifestApplication(Adw.Application):
# Profile
profile: Profile
# tracks whether the onboarding dialogue should be shown
fresh_install = True
def get_config_dir(app_id: str) -> Path:
base = Path(GLib.get_user_config_dir())
cfg_dir = base / app_id
cfg_dir.mkdir(parents=True, exist_ok=True)
return cfg_dir
# helpers for profile loading
APP_ID = "gay.valhrafnaz.VoidManifest"
CONFIG_DIR = get_config_dir(APP_ID)
CONFIG_FILE = CONFIG_DIR / "profile.json"
"""The main application singleton class."""
def __init__(self):
super().__init__(application_id='gay.valhrafnaz.Gnomeframe',
super().__init__(application_id='gay.valhrafnaz.VoidManifest',
flags=Gio.ApplicationFlags.DEFAULT_FLAGS,
resource_base_path='/gay/valhrafnaz/Gnomeframe')
resource_base_path='/gay/valhrafnaz/VoidManifest')
data_dir = GLib.get_system_data_dirs()[0]
resource = Gio.resource_load(os.path.join(data_dir, 'gnomeframe', 'gnomeframe.gresource'))
@@ -43,6 +76,10 @@ class GnomeframeApplication(Adw.Application):
self.create_action('about', self.on_about_action)
self.create_action('preferences', self.on_preferences_action)
self.CONFIG_DIR = get_config_dir(self.APP_ID)
self.profile = self.load_profile()
def do_activate(self):
"""Called when the application is activated.
@@ -51,17 +88,17 @@ class GnomeframeApplication(Adw.Application):
"""
win = self.props.active_window
if not win:
win = GnomeframeWindow(application=self)
win = VoidManifestWindow(application=self)
win.present()
def on_about_action(self, *args):
"""Callback for the app.about action."""
about = Adw.AboutDialog(application_name='gnomeframe',
application_icon='gay.valhrafnaz.Gnomeframe',
application_icon='gay.valhrafnaz.VoidManifest',
developer_name='valhrafnaz',
version='0.1.0',
developers=['valhrafnaz'],
copyright='© 2025 nihil')
copyright='© 2025 valhrafnaz')
# Translators: Replace "translator-credits" with your name/username, and optionally an email or URL.
about.set_translator_credits(_('translator-credits'))
about.present(self.props.active_window)
@@ -85,8 +122,69 @@ class GnomeframeApplication(Adw.Application):
if shortcuts:
self.set_accels_for_action(f"app.{name}", shortcuts)
def load_profile(self) -> Profile:
try:
with open(self.CONFIG_FILE, 'r') as f:
data = json.load(f)
return Profile(**data)
except (FileNotFoundError, json.JSONDecodeError):
return Profile() # Uses defaults
def save_profile(self, profile) -> None:
self.refresh_profile()
profile_path = self.CONFIG_FILE
with open(profile_path, 'w') as f:
json.dump(profile.to_dict(), f, indent=2)
def reset_frames(self, param):
self.profile.owned_frames = {}
button = self.checklist_page.btns_basic.get_first_child()
while button is not None:
if isinstance(button, Gtk.ToggleButton):
button.set_active(False)
button = button.get_next_sibling()
button = self.checklist_page.btns_prime.get_first_child()
while button is not None:
if isinstance(button, Gtk.ToggleButton):
button.set_active(False)
button = button.get_next_sibling()
self.save_profile(self.profile)
def refresh_profile(self):
# count basics
basic_count = 0
missing_basics: list[str] = []
for basic_frame in EXISTING_FRAMES_BASIC:
if self.profile.owned_frames.get(basic_frame, False):
basic_count = basic_count + 1
else:
missing_basics.append(basic_frame)
# count primes
prime_count = 0
missing_primes: list[str] = []
for prime_frame in EXISTING_FRAMES_PRIME:
if self.profile.owned_frames.get(prime_frame, False):
prime_count = prime_count + 1
else:
missing_primes.append(prime_frame)
# count unique
unique_frames: set[str] = set()
for owned_frame, is_owned in self.profile.owned_frames.items():
if is_owned:
_name = owned_frame.removesuffix("_prime")
_name = _name.removesuffix("_umbra")
unique_frames.add(_name)
self.profile.missing_basics_count = len(missing_basics)
self.profile.missing_primes_count = len(missing_primes)
self.profile.unique_frames_owned = len(unique_frames)
self.profile.missing_basics = missing_basics
self.profile.missing_primes = missing_primes
def main(version):
"""The application's entry point."""
app = GnomeframeApplication()
app = VoidManifestApplication()
return app.run(sys.argv)

View File

@@ -1,168 +1,18 @@
import json
import os
import math
import gi
from pathlib import Path
from gi.repository import Gtk, Adw, GObject, GLib
def get_config_dir(app_id: str) -> Path:
base = Path(GLib.get_user_config_dir())
cfg_dir = base / app_id
cfg_dir.mkdir(parents=True, exist_ok=True)
return cfg_dir
from ..constants import BASIC_WARFRAMES, PRIME_WARFRAMES
APP_ID = "gay.valhrafnaz.Gnomeframe"
CONFIG_DIR = get_config_dir(APP_ID)
CONFIG_FILE = CONFIG_DIR / "profile.json"
TOTAL_WARFRAMES=63 # post uriel, valid until whenever really
TOTAL_PRIMES=49 # post gyre prime, valid until ca. may-2026
gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
from gi.repository import Gtk, Adw, GObject, GLib # noqa: E402
def load_profile() -> dict[str, bool]:
try:
with CONFIG_FILE.open("r", encoding="utf-8") as f:
data = json.load(f)
return {k: bool(v) for k, v in data.items()}
except FileNotFoundError:
return {}
except (json.JSONDecodeError, OSError) as exc:
print(f"Could not load profile: {exc}")
return {}
def save_profile(state: dict[str, bool]) -> None:
try:
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
with CONFIG_FILE.open("w", encoding="utf-8") as f:
json.dump(state, f, indent=2, sort_keys=True)
except OSError as exc:
print(f"Failed to write profile: {exc}")
frame_button_ids = [
"ash",
"atlas",
"banshee",
"baruuk",
"caliban",
"chroma",
"citrine",
"cyte09",
"dagath",
"dante",
"ember",
"equinox",
"exalibur",
"frost",
"gara",
"garuda",
"gauss",
"grendel",
"gyre",
"harrow",
"hildryn",
"hydroid",
"inaros",
"ivara",
"jade",
"khora",
"koumei",
"kullervo",
"lavos",
"limbo",
"loki",
"mag",
"mesa",
"mirage" "nekros",
"nezha",
"nidus",
"nokko",
"nova",
"nyx",
"oberon",
"octavia",
"oraxia",
"protea",
"qorvex",
"revenant",
"rhino",
"saryn",
"sevagoth",
"styanax",
"temple",
"titania",
"trinity",
"valkyr",
"vauban",
"volt",
"voruna",
"wisp",
"wukong",
"xaku",
"yareli",
"zephyr",
"ash_prime",
"atlas_prime",
"banshee_prime",
"baruuk_prime",
"caliban_prime",
"chroma_prime",
"citrine_prime",
"cyte09_prime",
"dagath_prime",
"dante_prime",
"ember_prime",
"equinox_prime",
"exalibur_umbra",
"frost_prime",
"gara_prime",
"garuda_prime",
"gauss_prime",
"grendel_prime",
"gyre_prime",
"harrow_prime",
"hildryn_prime",
"hydroid_prime",
"inaros_prime",
"ivara_prime",
"jade_prime",
"khora_prime",
"koumei_prime",
"kullervo_prime",
"lavos_prime",
"limbo_prime",
"loki_prime",
"mag_prime",
"mesa_prime",
"mirage_prime" "nekros_prime",
"nezha_prime",
"nidus_prime",
"nokko_prime",
"nova_prime",
"nyx_prime",
"oberon_prime",
"octavia_prime",
"oraxia_prime",
"protea_prime",
"qorvex_prime",
"revenant_prime",
"rhino_prime",
"saryn_prime",
"sevagoth_prime",
"styanax_prime",
"temple_prime",
"titania_prime",
"trinity_prime",
"valkyr_prime",
"vauban_prime",
"volt_prime",
"voruna_prime",
"wisp_prime",
"wukong_prime",
"xaku_prime",
"yareli_prime",
"zephyr_prime",
]
@Gtk.Template(resource_path="/gay/valhrafnaz/Gnomeframe/ui/checklist.ui")
@Gtk.Template(resource_path="/gay/valhrafnaz/VoidManifest/ui/checklist.ui")
class ChecklistPage(Gtk.Box):
__gtype_name__ = "ChecklistPage"
@@ -171,17 +21,21 @@ class ChecklistPage(Gtk.Box):
btns_basic = Gtk.Template.Child()
btns_prime = Gtk.Template.Child()
basic_counter = Gtk.Template.Child()
basic_max = Gtk.Template.Child()
basic_percent = Gtk.Template.Child()
prime_counter = Gtk.Template.Child()
prime_max = Gtk.Template.Child()
prime_percent = Gtk.Template.Child()
def __init__(self, *, parent: Adw.ViewStack, window):
super().__init__()
self._parent_window = window
self.app = self._parent_window.get_application()
parent.add_titled(self, "checklist", "Checklist")
wrapper = parent.get_page(self)
wrapper.set_icon_name("check-round-outline2-symbolic")
self.basic_max.set_label(str(BASIC_WARFRAMES))
self.prime_max.set_label(str(PRIME_WARFRAMES))
self._connect_frame_btns()
self._calc_frames()
@@ -190,14 +44,14 @@ class ChecklistPage(Gtk.Box):
while button is not None:
if isinstance(button, Gtk.ToggleButton):
btn_id = button.get_name()
button.set_active(self._parent_window._profile.get(btn_id, False))
button.set_active(self.app.profile.owned_frames.get(btn_id, False))
button.connect("toggled", self._on_button_toggled)
button = button.get_next_sibling()
button = self.btns_prime.get_first_child()
while button is not None:
if isinstance(button, Gtk.ToggleButton):
btn_id = button.get_name()
button.set_active(self._parent_window._profile.get(btn_id, False))
button.set_active(self.app.profile.owned_frames.get(btn_id, False))
button.connect("toggled", self._on_button_toggled)
button = button.get_next_sibling()
@@ -223,9 +77,9 @@ class ChecklistPage(Gtk.Box):
def _calc_frames(self):
basic_counter = self._count_basics()
basic_percent = math.trunc(basic_counter / TOTAL_WARFRAMES * 100)
basic_percent = math.trunc(basic_counter / BASIC_WARFRAMES * 100)
prime_counter = self._count_primes()
prime_percent = math.trunc(prime_counter / TOTAL_PRIMES * 100)
prime_percent = math.trunc(prime_counter / PRIME_WARFRAMES * 100)
self.basic_counter.set_label(str(basic_counter))
self.basic_percent.set_label(str(basic_percent))
self.prime_counter.set_label(str(prime_counter))
@@ -236,8 +90,8 @@ class ChecklistPage(Gtk.Box):
if btn_id == "GtkToggleButton":
print("what")
return
self._parent_window._profile[btn_id] = btn.get_active()
save_profile(self._parent_window._profile)
self.app.profile.owned_frames[btn_id] = btn.get_active()
self.app.save_profile(self.app.profile)
self._calc_frames()

View File

@@ -1,9 +1,23 @@
from gi.repository import Gtk, Adw, GObject, GLib
import gi
@Gtk.Template(resource_path="/gay/valhrafnaz/Gnomeframe/ui/home.ui")
from ..constants import BASIC_WARFRAMES, PRIME_WARFRAMES
gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
from gi.repository import Gtk, Adw, GObject, GLib # noqa: E402
@Gtk.Template(resource_path="/gay/valhrafnaz/VoidManifest/ui/home.ui")
class HomePage(Gtk.Box):
__gtype_name__ = "HomePage"
owned_frames = Gtk.Template.Child()
owned_basics = Gtk.Template.Child()
missing_basics = Gtk.Template.Child()
owned_primes = Gtk.Template.Child()
missing_primes = Gtk.Template.Child()
def __init__(self, *, parent: Adw.ViewStack):
"""
`parent` is the ViewStack that will host this page.
@@ -14,3 +28,13 @@ class HomePage(Gtk.Box):
parent.add_titled(self, "home", "Home")
wrapper = parent.get_page(self)
wrapper.set_icon_name("compass2-symbolic")
self.app = self.get_root().get_application()
self.app.profile._on_change = self.refresh
self.refresh()
def refresh(self):
self.owned_frames.set_label(str (self.app.profile.unique_frames_owned))
self.owned_basics.set_label(str (BASIC_WARFRAMES - self.app.profile.missing_basics_count))
self.owned_primes.set_label(str (PRIME_WARFRAMES - self.app.profile.missing_primes_count))
self.missing_basics.set_label(str(self.app.profile.missing_basics_count))
self.missing_primes.set_label(str(self.app.profile.missing_primes_count))

View File

@@ -1,6 +1,6 @@
from gi.repository import Gtk, Adw, GObject, GLib
@Gtk.Template(resource_path="/gay/valhrafnaz/Gnomeframe/ui/settings.ui")
@Gtk.Template(resource_path="/gay/valhrafnaz/VoidManifest/ui/settings.ui")
class SettingsPage(Gtk.Box):
__gtype_name__ = "SettingsPage"

29
src/gnomeframe/profile.py Normal file
View File

@@ -0,0 +1,29 @@
from dataclasses import fields, field, dataclass
from typing import Callable
from .constants import BASIC_WARFRAMES, PRIME_WARFRAMES
# pseudo struct that contains the profile data that gets serialized into the .json profile.
@dataclass
class Profile:
owned_frames: dict[str, bool] = field(default_factory=dict)
unique_frames_owned: int = 0
missing_basics: list[str] = field(default_factory=list)
missing_basics_count: int = BASIC_WARFRAMES
missing_primes: list[str] = field(default_factory=list)
missing_primes_count: int = PRIME_WARFRAMES
_on_change: Callable | None = field(default=None, repr=False, compare=False)
def __setattr__(self, name, value):
super().__setattr__(name, value)
# Notify on change (skip private attributes)
if not name.startswith('_') and hasattr(self, '_on_change') and self._on_change:
self._on_change()
def to_dict(self) -> dict:
"""Convert to dict, excluding private fields"""
return {
f.name: getattr(self, f.name)
for f in fields(self)
if not f.name.startswith('_')
}

View File

@@ -29,85 +29,32 @@ from gi.repository import Adw
from gi.repository import Gtk, Gio
from gi.repository import GLib
def get_config_dir(app_id: str) -> Path:
base = Path(GLib.get_user_config_dir())
cfg_dir = base / app_id
cfg_dir.mkdir(parents=True, exist_ok=True)
return cfg_dir
APP_ID = "gay.valhrafnaz.Gnomeframe"
CONFIG_DIR = get_config_dir(APP_ID)
CONFIG_FILE = CONFIG_DIR / "profile.json"
def load_profile() -> dict[str, bool]:
try:
with CONFIG_FILE.open("r", encoding="utf-8") as f:
data = json.load(f)
return {k: bool(v) for k, v in data.items()}
except FileNotFoundError:
return {}
except (json.JSONDecodeError, OSError) as exc:
print(f"Could not load profile: {exc}")
return {}
def save_profile(state: dict[str, bool]) -> None:
try:
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
with CONFIG_FILE.open("w", encoding="utf-8") as f:
json.dump(state, f, indent=2, sort_keys=True)
except OSError as exc:
print(f"Failed to write profile: {exc}")
@Gtk.Template(resource_path='/gay/valhrafnaz/Gnomeframe/ui/window.ui')
class GnomeframeWindow(Adw.ApplicationWindow):
__gtype_name__ = 'GnomeframeWindow'
@Gtk.Template(resource_path='/gay/valhrafnaz/VoidManifest/ui/window.ui')
class VoidManifestWindow(Adw.ApplicationWindow):
__gtype_name__ = 'VoidManifestWindow'
viewstack = Gtk.Template.Child()
btn_reset_profile = Gtk.Template.Child()
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._profile: dict[str, bool] = load_profile()
self.app = self.get_application()
self._load_page_templates()
reset_action = Gio.SimpleAction.new("reset-selections", None)
reset_action.connect("activate", self._reset_all)
reset_action.connect("activate", self.app.reset_frames)
self.get_application().add_action(reset_action)
self.btn_reset_profile.connect("clicked", self._reset_all)
self.btn_reset_profile.connect("clicked", self.app.reset_frames)
self.connect("close-request", self._on_close_request)
# self.settings = Gio.Settings(schema_id="gay.valhrafnaz.Gnomeframe")
# self.settings.bind("window-width", self, "default-width", Gio.SettingsBindFlags.DEFAULT)
# self.settings.bind("window-height", self, "default-height", Gio.SettingsBindFlags.DEFAULT)
# self.settings.bind("window-maximized", self, "maximized", Gio.SettingsBindFlags.DEFAULT)
def _load_page_templates(self):
self.home_page = HomePage(parent=self.viewstack)
self.checklist_page = ChecklistPage(parent=self.viewstack, window=self)
self.settings_page = SettingsPage(parent=self.viewstack)
def _reset_all(self, param):
self._profile.clear()
button = self.checklist_page.btns_basic.get_first_child()
while button is not None:
if isinstance(button, Gtk.ToggleButton):
button.set_active(False)
button = button.get_next_sibling()
button = self.checklist_page.btns_prime.get_first_child()
while button is not None:
if isinstance(button, Gtk.ToggleButton):
button.set_active(False)
button = button.get_next_sibling()
save_profile(self._profile)
def _on_close_request(self, *args):
save_profile(self._profile)
self.app.save_profile(self.app.profile)
return False