diff --git a/.gitignore b/.gitignore index 3dfb4de..4bb5ad3 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ subprojects/blueprint-compiler +_build \ No newline at end of file diff --git a/data/resources/ui/checklist.blp b/data/resources/ui/checklist.blp index 7c3f341..8ff5562 100644 --- a/data/resources/ui/checklist.blp +++ b/data/resources/ui/checklist.blp @@ -24,11 +24,11 @@ template $ChecklistPage: Box { } Label { - label: _("List of all Warframes available as of Dec 10 '25"); + label: _("List of all Warframes available as of today"); margin-bottom: 12; styles [ - "dim-label", + "dimmed", ] } @@ -155,7 +155,7 @@ template $ChecklistPage: Box { } ToggleButton cyte09 { - name: "cyte09"; + name: "cyte-09"; label: _("Cyte-09"); margin-bottom: 4; } @@ -622,7 +622,7 @@ template $ChecklistPage: Box { } ToggleButton cyte09_prime { - name: "cyte09_prime"; + name: "cyte-09_prime"; label: _("Cyte-09 Prime"); margin-bottom: 4; can-target: false; diff --git a/data/resources/ui/checklist.ui b/data/resources/ui/checklist.ui index 277f00f..13419c8 100644 --- a/data/resources/ui/checklist.ui +++ b/data/resources/ui/checklist.ui @@ -32,10 +32,10 @@ corresponding .blp file and regenerate this file with blueprint-compiler. - List of all Warframes available as of Dec 10 '25 + List of all Warframes available as of today 12 @@ -189,7 +189,7 @@ corresponding .blp file and regenerate this file with blueprint-compiler. - cyte09 + cyte-09 Cyte-09 4 @@ -740,7 +740,7 @@ corresponding .blp file and regenerate this file with blueprint-compiler. - cyte09_prime + cyte-09_prime Cyte-09 Prime 4 false diff --git a/data/resources/ui/home.blp b/data/resources/ui/home.blp index 260f449..19dc376 100644 --- a/data/resources/ui/home.blp +++ b/data/resources/ui/home.blp @@ -5,59 +5,65 @@ template $HomePage: Box { orientation: vertical; baseline-position: center; Adw.Clamp { - Adw.PreferencesGroup { - title: _("Welcome back, Tenno"); - styles [ - 'list-title' - ] - Adw.ActionRow { - title: _("Owned Unique Frames:"); - title-selectable: false; - activatable: false; + ScrolledWindow { + hexpand: true; + vexpand: true; + child: Adw.PreferencesGroup { + margin-start:20; + margin-end: 20; + title: _("Welcome back, Tenno"); + styles [ + 'list-title' + ] + Adw.ExpanderRow owned_frames_row { + title: _("Owned Unique Frames:"); + title-selectable: false; + activatable: false; - [suffix] - Label owned_frames { + [suffix] + Label owned_frames { + } } - } - Adw.ActionRow { - title: _("Owned Basic Frames:"); - title-selectable: false; - activatable: false; + Adw.ExpanderRow owned_basics_row { + title: _("Owned Basic Frames:"); + title-selectable: false; + activatable: false; - [suffix] - Label owned_basics { + [suffix] + Label owned_basics { + } } - } - Adw.ActionRow { - title: _("Missing Basic Frames:"); - title-selectable: false; - activatable: false; + Adw.ExpanderRow missing_basics_row { + title: _("Missing Basic Frames:"); + title-selectable: false; + activatable: false; - [suffix] - Label missing_basics { + [suffix] + Label missing_basics { + } } - } - Adw.ActionRow { - title: _("Owned Prime Frames:"); - title-selectable: false; - activatable: false; + Adw.ExpanderRow owned_primes_row { + title: _("Owned Prime Frames:"); + title-selectable: false; + activatable: false; - [suffix] - Label owned_primes { + [suffix] + Label owned_primes { + } } - } - Adw.ActionRow { - title: _("Missing Prime Frames:"); - title-selectable: false; - activatable: false; + Adw.ExpanderRow missing_primes_row { + title: _("Missing Prime Frames:"); + title-selectable: false; + activatable: false; - [suffix] - Label missing_primes { + [suffix] + Label missing_primes { + } } - } - } - } + }; + } + } } \ No newline at end of file diff --git a/data/resources/ui/home.ui b/data/resources/ui/home.ui index 3617e9e..d92c5f1 100644 --- a/data/resources/ui/home.ui +++ b/data/resources/ui/home.ui @@ -12,61 +12,69 @@ corresponding .blp file and regenerate this file with blueprint-compiler. - - Welcome back, Tenno - - - - Owned Unique Frames: - false - false - - + + true + true + + + 20 + 20 + Welcome back, Tenno + + + + Owned Unique Frames: + false + false + + + + + + + + Owned Basic Frames: + false + false + + + + + + + + Missing Basic Frames: + false + false + + + + + + + + Owned Prime Frames: + false + false + + + + + + + + Missing Prime Frames: + false + false + + + + - - - - Owned Basic Frames: - false - false - - - - - - - - Missing Basic Frames: - false - false - - - - - - - - Owned Prime Frames: - false - false - - - - - - - - Missing Prime Frames: - false - false - - - - - + diff --git a/data/resources/ui/window.blp b/data/resources/ui/window.blp index 399099d..5ee4641 100644 --- a/data/resources/ui/window.blp +++ b/data/resources/ui/window.blp @@ -35,7 +35,7 @@ template $VoidmanifestWindow: Adw.ApplicationWindow { // Main View content: Adw.ViewStack viewstack { - + notify::visible-child => $on_page_changed(); }; [bottom] ActionBar { diff --git a/data/resources/ui/window.ui b/data/resources/ui/window.ui index 1815b9b..94b6090 100644 --- a/data/resources/ui/window.ui +++ b/data/resources/ui/window.ui @@ -39,7 +39,9 @@ corresponding .blp file and regenerate this file with blueprint-compiler. - + + + diff --git a/data/resources/ui/wishlist.blp b/data/resources/ui/wishlist.blp index 04c3ba7..8c5959a 100644 --- a/data/resources/ui/wishlist.blp +++ b/data/resources/ui/wishlist.blp @@ -5,20 +5,25 @@ template $WishlistPage: Gtk.Box { orientation: vertical; baseline-position: center; Adw.Clamp { - Adw.PreferencesGroup wishlist { - header-suffix: Button btn_wishlist_add { - icon-name: 'plus-circle-outline-symbolic'; + ScrolledWindow { + hexpand: true; + vexpand: true; + Adw.PreferencesGroup wishlist { + margin-end:20; + header-suffix: Button btn_wishlist_add { + icon-name: 'plus-circle-outline-symbolic'; + styles [ + 'flat', + 'circular', + 'suggested-action' + ] + }; + separate-rows: true; + title: _("Wishlist"); styles [ - 'flat', - 'circular', - 'suggested-action' + "list-title" ] - }; - separate-rows: true; - title: _("Wishlist"); - styles [ - "list-title" - ] + } } } } \ No newline at end of file diff --git a/data/resources/ui/wishlist.ui b/data/resources/ui/wishlist.ui index 21c4297..b3aeb77 100644 --- a/data/resources/ui/wishlist.ui +++ b/data/resources/ui/wishlist.ui @@ -12,22 +12,29 @@ corresponding .blp file and regenerate this file with blueprint-compiler. - - - - plus-circle-outline-symbolic + + true + true + + + 20 + + + plus-circle-outline-symbolic + + + + true + Wishlist - - true - Wishlist - + diff --git a/src/voidmanifest/constants.py b/src/voidmanifest/constants.py index e66185d..3aa1028 100644 --- a/src/voidmanifest/constants.py +++ b/src/voidmanifest/constants.py @@ -6,7 +6,7 @@ EXISTING_FRAMES_BASIC = [ "caliban", "chroma", "citrine", - "cyte09", + "cyte-09", "dagath", "dante", "ember", @@ -52,7 +52,6 @@ EXISTING_FRAMES_BASIC = [ "temple", "titania", "trinity", - "uriel", "valkyr", "vauban", "volt", @@ -79,7 +78,6 @@ EXISTING_FRAMES_PRIME = [ "garuda_prime", "gauss_prime", "grendel_prime", - "gyre_prime", "harrow_prime", "hildryn_prime", "hydroid_prime", @@ -119,7 +117,6 @@ EXISTING_FRAMES_PRIME = [ ID_TO_NAME: dict[str, str] = dict() NAME_TO_ID: dict[str, str] = dict() # warframe counts -# TODO Update via API/wiki BASIC_WARFRAMES = 63 PRIME_WARFRAMES = 49 CURRENT_UPDATE = 41 diff --git a/src/voidmanifest/main.py b/src/voidmanifest/main.py index cccfd1d..7adff3c 100644 --- a/src/voidmanifest/main.py +++ b/src/voidmanifest/main.py @@ -125,7 +125,7 @@ class VoidmanifestApplication(Adw.Application): def _refresh_metadata(self): url = 'https://api.warframestat.us/warframes' - params = 'only=name,isPrime' + params = 'only=name,isPrime,category' resp = requests.get(url, params) base_frames_list: list[str] = list() prime_frames_list: list[str] = list() @@ -135,6 +135,9 @@ class VoidmanifestApplication(Adw.Application): else: print('CRITICAL ERROR: Could not refresh metadata from https://api.warframestat.us/') for warframe in resp_data: + # skip every non-warframe + if warframe['category'] != 'Warframes': + continue name = warframe['name'] # skip these two frames since they cannot be acquired anymore and break symmetry in the list if name == 'Excalibur Umbra Prime' or name == 'Excalibur Prime': @@ -145,12 +148,23 @@ class VoidmanifestApplication(Adw.Application): prime_frames_list.append(frame_id) else: base_frames_list.append(frame_id) + # TODO REMOVE + # only needed until API is finally updated + base_frames_list.append('uriel') + prime_frames_list.append('gyre_prime') + base_frames_list = sorted(list(set(base_frames_list))) + prime_frames_list = sorted(list(set(prime_frames_list))) EXISTING_FRAMES_BASIC = base_frames_list.copy() EXISTING_FRAMES_PRIME = prime_frames_list.copy() BASIC_WARFRAMES = len(EXISTING_FRAMES_BASIC) PRIME_WARFRAMES = len(EXISTING_FRAMES_PRIME) # dictionary that translates ids to names for better responsiveness on UI ID_TO_NAME[frame_id] = name + # TODO REMOVE + # only needed until API is finally updated + ID_TO_NAME['gyre_prime'] = 'Gyre Prime' + ID_TO_NAME['uriel'] = 'Uriel' + NAME_TO_ID = dict((v,k) for k,v in ID_TO_NAME.items()) @@ -170,7 +184,7 @@ class VoidmanifestApplication(Adw.Application): json.dump(profile.to_dict(), f, indent=2) def reset_frames(self, param): - self.profile.owned_frames = {} + self.profile.owned_frames = [] button = self.checklist_page.btns_basic.get_first_child() while button is not None: if isinstance(button, Gtk.ToggleButton): @@ -188,7 +202,7 @@ class VoidmanifestApplication(Adw.Application): basic_count = 0 missing_basics: list[str] = [] for basic_frame in EXISTING_FRAMES_BASIC: - if self.profile.owned_frames.get(basic_frame, False): + if basic_frame in self.profile.owned_frames: basic_count = basic_count + 1 else: missing_basics.append(basic_frame) @@ -196,18 +210,18 @@ class VoidmanifestApplication(Adw.Application): prime_count = 0 missing_primes: list[str] = [] for prime_frame in EXISTING_FRAMES_PRIME: - if self.profile.owned_frames.get(prime_frame, False): + if prime_frame in self.profile.owned_frames: 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) + for owned_frame in self.profile.owned_frames: + _name = owned_frame.removesuffix("_prime") + _name = _name.removesuffix("_umbra") + unique_frames.add(_name) + self.profile.unique_frames_list = list(unique_frames) self.profile.missing_basics_count = len(missing_basics) self.profile.missing_primes_count = len(missing_primes) self.profile.unique_frames_owned = len(unique_frames) diff --git a/src/voidmanifest/pages/checklist.py b/src/voidmanifest/pages/checklist.py index 64c811d..7e72419 100644 --- a/src/voidmanifest/pages/checklist.py +++ b/src/voidmanifest/pages/checklist.py @@ -44,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.app.profile.owned_frames.get(btn_id, False)) + button.set_active((btn_id in self.app.profile.owned_frames)) 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.app.profile.owned_frames.get(btn_id, False)) + button.set_active((btn_id in self.app.profile.owned_frames)) button.connect("toggled", self._on_button_toggled) button = button.get_next_sibling() @@ -90,7 +90,12 @@ class ChecklistPage(Gtk.Box): if btn_id == "GtkToggleButton": print("what") return - self.app.profile.owned_frames[btn_id] = btn.get_active() + if btn.get_active(): + self.app.profile.owned_frames.append(btn_id) + if btn_id in self.app.profile.wishlist(btn_id): + self.app.profile.wishlist.remove(btn_id) + else: + self.app.profile.owned_frames.remove(btn_id) self.app.save_profile(self.app.profile) self._calc_frames() diff --git a/src/voidmanifest/pages/home.py b/src/voidmanifest/pages/home.py index af1e5f6..a5e8d15 100644 --- a/src/voidmanifest/pages/home.py +++ b/src/voidmanifest/pages/home.py @@ -1,6 +1,6 @@ import gi -from ..constants import BASIC_WARFRAMES, PRIME_WARFRAMES +from ..constants import BASIC_WARFRAMES, PRIME_WARFRAMES, ID_TO_NAME gi.require_version('Gtk', '4.0') gi.require_version('Adw', '1') @@ -12,29 +12,80 @@ from gi.repository import Gtk, Adw, GObject, GLib # noqa: E402 class HomePage(Gtk.Box): __gtype_name__ = "HomePage" + app = None + owned_frames = Gtk.Template.Child() + owned_frames_row = Gtk.Template.Child() owned_basics = Gtk.Template.Child() + owned_basics_row = Gtk.Template.Child() missing_basics = Gtk.Template.Child() + missing_basics_row = Gtk.Template.Child() owned_primes = Gtk.Template.Child() + owned_primes_row = Gtk.Template.Child() missing_primes = Gtk.Template.Child() + missing_primes_row = Gtk.Template.Child() def __init__(self, *, parent: Adw.ViewStack): - """ - `parent` is the ViewStack that will host this page. - By passing it to the superclass constructor we tell GTK to - insert the newly created page into that stack automatically. - """ super().__init__() 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._unique_rows: dict[str, Adw.ActionRow] = {} + self._missing_basics_rows: dict[str, Adw.ActionRow] = {} + self._missing_primes_rows: dict[str, Adw.ActionRow] = {} + self._basic_rows: dict[str, Adw.ActionRow] = {} + self._prime_rows: dict[str, Adw.ActionRow] = {} self.refresh() def refresh(self): + # Prevents race condition where refresh is called before __init__ + if self.app is None: + return + + self.app.profile.owned_frames = sorted(list(set(self.app.profile.owned_frames))) + + # set labels 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)) + # populate dropdowns + for frame in self.app.profile.owned_frames: + if "_prime" in frame or "_umbra" in frame: + row = Adw.ActionRow( + title=ID_TO_NAME[frame] + ) + if not self._prime_rows.get(frame, False): + self._prime_rows[frame] = row + self.owned_primes_row.add_row(row) + else: + row = Adw.ActionRow( + title=ID_TO_NAME[frame] + ) + if not self._basic_rows.get(frame, False): + self._basic_rows[frame] = row + self.owned_basics_row.add_row(row) + + frame = frame.replace('_prime', '') + row = Adw.ActionRow( + title=ID_TO_NAME[frame] + ) + if not self._unique_rows.get(frame, False): + self._unique_rows[frame] = row + self.owned_frames_row.add_row(row) + for prime in self.app.profile.missing_primes: + row = Adw.ActionRow( + title=ID_TO_NAME[prime] + ) + if not self._missing_primes_rows.get(prime, False): + self._missing_primes_rows[prime] = row + self.missing_primes_row.add_row(row) + for basic in self.app.profile.missing_basics: + row = Adw.ActionRow( + title=ID_TO_NAME[basic] + ) + if not self._missing_basics_rows.get(basic, False): + self._missing_basics_rows[basic] = row + self.missing_basics_row.add_row(row) diff --git a/src/voidmanifest/pages/wishlist.py b/src/voidmanifest/pages/wishlist.py index fb82a36..938ea45 100644 --- a/src/voidmanifest/pages/wishlist.py +++ b/src/voidmanifest/pages/wishlist.py @@ -25,9 +25,10 @@ class WishlistPage(Gtk.Box): wrapper.set_icon_name("logs-symbolic") self.btn_wishlist_add.connect('clicked', self._open_wishlist_dialogue) self._wishlist_rows: dict[str, Adw.ActionRow] = {} + self.refresh_wishlist() def _open_wishlist_dialogue(self, button): - dialog = FramePickerDialog(missing_frames=self.app.profile.missing_basics + self.app.profile.missing_primes) + dialog = FramePickerDialog(missing_frames=self.app.profile.missing_basics + self.app.profile.missing_primes, app=self.app) dialog.connect('frame-selected', self.add_frame_to_wishlist) dialog.present(self.get_root()) @@ -52,7 +53,7 @@ class WishlistPage(Gtk.Box): self.wishlist.add(row) self.app.profile.wishlist.append(frame_id) self.app.profile.wishlist = sorted(self.app.profile.wishlist) - self._refresh_wishlist + self.refresh_wishlist def btn_remove(self, button): self.remove_frame_from_wishlist(button.frame_id) @@ -61,14 +62,24 @@ class WishlistPage(Gtk.Box): if frame_id in self._wishlist_rows: row = self._wishlist_rows.pop(frame_id) self.wishlist.remove(row) + if frame_id in self.app.profile.wishlist: + self.app.profile.wishlist.remove(frame_id) def has_frame(self, frame_id) -> bool: return frame_id in self._wishlist_rows - def _refresh_wishlist(self): + def refresh_wishlist(self): + # deduplicate + self.app.profile.wishlist = list(set(self.app.profile.wishlist)) + for frame_id in self.app.profile.wishlist: if frame_id not in self._wishlist_rows: self.add_frame_to_wishlist(Adw.Dialog.new(),frame_id) - for frame_id in list(self._wishlist_rows): + to_delete: list[str] = [] + for frame_id in self._wishlist_rows: if frame_id not in self.app.profile.wishlist: - self.remove_frame_from_wishlist(frame_id) + to_delete.append(frame_id) + if frame_id in self.app.profile.owned_frames: + to_delete.append(frame_id) + for frame_id in to_delete: + self.remove_frame_from_wishlist(frame_id) diff --git a/src/voidmanifest/profile.py b/src/voidmanifest/profile.py index 78b423b..c22380e 100644 --- a/src/voidmanifest/profile.py +++ b/src/voidmanifest/profile.py @@ -5,22 +5,15 @@ 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) + owned_frames: list[str] = field(default_factory=list) unique_frames_owned: int = 0 + unique_frames_list: list[str] = field(default_factory=list) 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 wishlist: list[str] = field(default_factory=list) - _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 { diff --git a/src/voidmanifest/utils/framepicker.py b/src/voidmanifest/utils/framepicker.py index aa27c3c..218ef1c 100644 --- a/src/voidmanifest/utils/framepicker.py +++ b/src/voidmanifest/utils/framepicker.py @@ -16,9 +16,9 @@ class FramePickerDialog(Adw.Dialog): search_entry = Gtk.Template.Child() frame_list = Gtk.Template.Child() - def __init__(self, missing_frames: list[str], **kwargs): + def __init__(self, app: Adw.ApplicationWindow, missing_frames: list[str], **kwargs): super().__init__(**kwargs) - + self.app = app self.missing_frames = missing_frames self._populate_list() @@ -26,11 +26,12 @@ class FramePickerDialog(Adw.Dialog): def _populate_list(self): for frame_name in sorted(self.missing_frames): - row = Adw.ActionRow(title=frame_name.replace("_", " ").title(), - activatable=True - ) - row.frame_id = frame_name - self.frame_list.append(row) + if frame_name not in self.app.profile.wishlist: + row = Adw.ActionRow(title=frame_name.replace("_", " ").title(), + activatable=True + ) + row.frame_id = frame_name + self.frame_list.append(row) def _filter_func(self, row): search_text = self.search_entry.get_text().lower() diff --git a/src/voidmanifest/window.py b/src/voidmanifest/window.py index fa2deb9..715e0b5 100644 --- a/src/voidmanifest/window.py +++ b/src/voidmanifest/window.py @@ -56,4 +56,16 @@ class VoidmanifestWindow(Adw.ApplicationWindow): self.app.save_profile(self.app.profile) return False + @Gtk.Template.Callback() + def on_page_changed(self, stack, param): + self.app.refresh_profile() + visible_child = stack.get_visible_child() + visible_name = stack.get_visible_child_name() + match visible_name: + case 'home': + visible_child.refresh() + case 'wishlist': + visible_child.refresh_wishlist() + case _: + return