initial commit
This commit is contained in:
0
src/__init__.py
Normal file
0
src/__init__.py
Normal file
10
src/gnomeframe.gresource.xml
Normal file
10
src/gnomeframe.gresource.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<gresources>
|
||||
<gresource prefix="/gay/valhrafnaz/Gnomeframe/">
|
||||
<file>style.css</file>
|
||||
<file preprocess="xml-stripblanks">ui/window.ui</file>
|
||||
<file preprocess="xml-stripblanks">ui/shortcuts-dialog.ui</file>
|
||||
<file preprocess="xml-stripblanks">ui/checklist.ui</file>
|
||||
<file preprocess="xml-stripblanks">icons/bw/scalable/floppy-symbolic.svg</file>
|
||||
</gresource>
|
||||
</gresources>
|
||||
46
src/gnomeframe.in
Executable file
46
src/gnomeframe.in
Executable file
@@ -0,0 +1,46 @@
|
||||
#!@PYTHON@
|
||||
|
||||
# gnomeframe.in
|
||||
#
|
||||
# Copyright 2025 nihil
|
||||
#
|
||||
# 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
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import os
|
||||
import sys
|
||||
import signal
|
||||
import locale
|
||||
import gettext
|
||||
|
||||
VERSION = '@VERSION@'
|
||||
pkgdatadir = '@pkgdatadir@'
|
||||
localedir = '@localedir@'
|
||||
|
||||
sys.path.insert(1, pkgdatadir)
|
||||
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
||||
locale.bindtextdomain('gnomeframe', localedir)
|
||||
locale.textdomain('gnomeframe')
|
||||
gettext.install('gnomeframe', localedir)
|
||||
|
||||
if __name__ == '__main__':
|
||||
import gi
|
||||
|
||||
from gi.repository import Gio
|
||||
resource = Gio.Resource.load(os.path.join(pkgdatadir, 'gnomeframe.gresource'))
|
||||
resource._register()
|
||||
|
||||
from gnomeframe import main
|
||||
sys.exit(main.main(VERSION))
|
||||
2
src/icons/bw/scalable/floppy-symbolic.svg
Normal file
2
src/icons/bw/scalable/floppy-symbolic.svg
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><path d="m 3.96875 1 s -2.96875 0 -2.96875 2.96875 v 8.03125 s 0 0.5 0.3125 0.71875 l 1.6875 1.6875 v -10.40625 c 0 -1 1 -1 1 -1 h 8 s 1 0 1 1 v 8 c 0 1 -1 1 -1 1 h -1 v 2 h 1 s 3 0 3 -2.96875 v -8.0625 s 0 -2.96875 -2.96875 -2.96875 z m 1.03125 8 c -0.554688 0 -1 0.445312 -1 1 v 4 c 0 0.554688 0.445312 1 1 1 h 4 c 0.554688 0 1 -0.445312 1 -1 v -4 c 0 -0.554688 -0.445312 -1 -1 -1 z m 0 1 h 2 v 4 h -2 z m 0 0" fill="#222222"/></svg>
|
||||
|
After Width: | Height: | Size: 562 B |
86
src/main.py
Normal file
86
src/main.py
Normal file
@@ -0,0 +1,86 @@
|
||||
# main.py
|
||||
#
|
||||
# Copyright 2025 nihil
|
||||
#
|
||||
# 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
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import sys
|
||||
import gi
|
||||
|
||||
gi.require_version('Gtk', '4.0')
|
||||
gi.require_version('Adw', '1')
|
||||
|
||||
from gi.repository import Gtk, Gio, Adw
|
||||
from .window import GnomeframeWindow
|
||||
|
||||
|
||||
class GnomeframeApplication(Adw.Application):
|
||||
"""The main application singleton class."""
|
||||
def __init__(self):
|
||||
super().__init__(application_id='gay.valhrafnaz.Gnomeframe',
|
||||
flags=Gio.ApplicationFlags.DEFAULT_FLAGS,
|
||||
resource_base_path='/gay/valhrafnaz/Gnomeframe')
|
||||
self.create_action('quit', lambda *_: self.quit(), ['<control>q'])
|
||||
self.create_action('about', self.on_about_action)
|
||||
self.create_action('preferences', self.on_preferences_action)
|
||||
|
||||
def do_activate(self):
|
||||
"""Called when the application is activated.
|
||||
|
||||
We raise the application's main window, creating it if
|
||||
necessary.
|
||||
"""
|
||||
win = self.props.active_window
|
||||
if not win:
|
||||
win = GnomeframeWindow(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',
|
||||
developer_name='nihil',
|
||||
version='0.1.0',
|
||||
developers=['nihil'],
|
||||
copyright='© 2025 nihil')
|
||||
# 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)
|
||||
|
||||
def on_preferences_action(self, widget, _):
|
||||
"""Callback for the app.preferences action."""
|
||||
print('app.preferences action activated')
|
||||
|
||||
def create_action(self, name, callback, shortcuts=None):
|
||||
"""Add an application action.
|
||||
|
||||
Args:
|
||||
name: the name of the action
|
||||
callback: the function to be called when the action is
|
||||
activated
|
||||
shortcuts: an optional list of accelerators
|
||||
"""
|
||||
action = Gio.SimpleAction.new(name, None)
|
||||
action.connect("activate", callback)
|
||||
self.add_action(action)
|
||||
if shortcuts:
|
||||
self.set_accels_for_action(f"app.{name}", shortcuts)
|
||||
|
||||
|
||||
def main(version):
|
||||
"""The application's entry point."""
|
||||
app = GnomeframeApplication()
|
||||
return app.run(sys.argv)
|
||||
49
src/meson.build
Normal file
49
src/meson.build
Normal file
@@ -0,0 +1,49 @@
|
||||
pkgdatadir = get_option('prefix') / get_option('datadir') / meson.project_name()
|
||||
moduledir = pkgdatadir / 'gnomeframe'
|
||||
|
||||
gnome = import('gnome')
|
||||
|
||||
|
||||
blueprints = custom_target('blueprints',
|
||||
input: files(
|
||||
'ui/window.blp',
|
||||
'ui/shortcuts-dialog.blp',
|
||||
'ui/checklist.blp',
|
||||
),
|
||||
output: '.',
|
||||
command: [find_program('blueprint-compiler'), 'batch-compile', '@OUTPUT', '@CURRENT_SOURCE_DIRECTORY@' , '@INPUT@'],
|
||||
)
|
||||
|
||||
|
||||
gnome.compile_resources('gnomeframe',
|
||||
'gnomeframe.gresource.xml',
|
||||
gresource_bundle: true,
|
||||
install: true,
|
||||
install_dir: pkgdatadir,
|
||||
dependencies: blueprints,
|
||||
)
|
||||
|
||||
python = import('python')
|
||||
|
||||
conf = configuration_data()
|
||||
conf.set('PYTHON', python.find_installation('python3').full_path())
|
||||
conf.set('VERSION', meson.project_version())
|
||||
conf.set('localedir', get_option('prefix') / get_option('localedir'))
|
||||
conf.set('pkgdatadir', pkgdatadir)
|
||||
|
||||
configure_file(
|
||||
input: 'gnomeframe.in',
|
||||
output: 'gnomeframe',
|
||||
configuration: conf,
|
||||
install: true,
|
||||
install_dir: get_option('bindir'),
|
||||
install_mode: 'rwxr-xr-x'
|
||||
)
|
||||
|
||||
gnomeframe_sources = [
|
||||
'__init__.py',
|
||||
'main.py',
|
||||
'window.py',
|
||||
]
|
||||
|
||||
install_data(gnomeframe_sources, install_dir: moduledir)
|
||||
31
src/style.css
Normal file
31
src/style.css
Normal file
@@ -0,0 +1,31 @@
|
||||
.disabled {
|
||||
background: rgb(29,29,29);
|
||||
}
|
||||
|
||||
.toggle:checked {
|
||||
background: var(--accent-bg-color);
|
||||
}
|
||||
|
||||
.toggle:hover {
|
||||
background: color-mix(in srgb, #222226 ,var(--accent-color) 20%);
|
||||
}
|
||||
|
||||
.toggle:active {
|
||||
background: var(--accent-color);
|
||||
}
|
||||
|
||||
flowboxchild:hover {
|
||||
background: #222222;
|
||||
}
|
||||
|
||||
.flowbox:active {
|
||||
background: #222222;
|
||||
}
|
||||
|
||||
.box:active {
|
||||
background: #222222;
|
||||
}
|
||||
|
||||
.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
1010
src/ui/checklist.blp
Normal file
1010
src/ui/checklist.blp
Normal file
File diff suppressed because it is too large
Load Diff
1171
src/ui/checklist.ui
Normal file
1171
src/ui/checklist.ui
Normal file
File diff suppressed because it is too large
Load Diff
16
src/ui/shortcuts-dialog.blp
Normal file
16
src/ui/shortcuts-dialog.blp
Normal file
@@ -0,0 +1,16 @@
|
||||
using Gtk 4.0;
|
||||
using Adw 1;
|
||||
|
||||
Adw.ShortcutsDialog {
|
||||
Adw.ShortcutsSection {
|
||||
title: _("Shortcuts");
|
||||
Adw.ShortcutsItem {
|
||||
title: _("Show Shortcuts");
|
||||
action-name: "app.shortcuts";
|
||||
}
|
||||
Adw.ShortcutsItem {
|
||||
title: _("Quit");
|
||||
action-name: "app.quit";
|
||||
}
|
||||
}
|
||||
}
|
||||
28
src/ui/shortcuts-dialog.ui
Normal file
28
src/ui/shortcuts-dialog.ui
Normal file
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
DO NOT EDIT!
|
||||
This file was @generated by blueprint-compiler. Instead, edit the
|
||||
corresponding .blp file and regenerate this file with blueprint-compiler.
|
||||
-->
|
||||
<interface>
|
||||
<requires lib="gtk" version="4.0"/>
|
||||
<object class="AdwShortcutsDialog">
|
||||
<child>
|
||||
<object class="AdwShortcutsSection">
|
||||
<property name="title" translatable="yes">Shortcuts</property>
|
||||
<child>
|
||||
<object class="AdwShortcutsItem">
|
||||
<property name="title" translatable="yes">Show Shortcuts</property>
|
||||
<property name="action-name">app.shortcuts</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwShortcutsItem">
|
||||
<property name="title" translatable="yes">Quit</property>
|
||||
<property name="action-name">app.quit</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
1061
src/ui/window.blp
Normal file
1061
src/ui/window.blp
Normal file
File diff suppressed because it is too large
Load Diff
1220
src/ui/window.ui
Normal file
1220
src/ui/window.ui
Normal file
File diff suppressed because it is too large
Load Diff
295
src/window.py
Normal file
295
src/window.py
Normal file
@@ -0,0 +1,295 @@
|
||||
# window.py
|
||||
#
|
||||
# Copyright 2025 nihil
|
||||
#
|
||||
# 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
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
import json
|
||||
import os
|
||||
import math
|
||||
from pathlib import Path
|
||||
|
||||
from gi.repository import Adw
|
||||
from gi.repository import Gtk, Gio
|
||||
from gi.repository import GLib
|
||||
|
||||
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",
|
||||
]
|
||||
|
||||
|
||||
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)
|
||||
print(f"Found config dir at: {cfg_dir}")
|
||||
return cfg_dir
|
||||
|
||||
APP_ID = "gay.valhrafnaz.Gnomeframe"
|
||||
CONFIG_DIR = get_config_dir(APP_ID)
|
||||
CONFIG_FILE = CONFIG_DIR / "profile.json"
|
||||
|
||||
TOTAL_WARFRAMES=62 # pre uriel, valid until 10-12-2025
|
||||
TOTAL_PRIMES=49 # post gyre prime, valid until ca. may-2026
|
||||
|
||||
|
||||
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/compiled/window.ui')
|
||||
class GnomeframeWindow(Adw.ApplicationWindow):
|
||||
__gtype_name__ = 'GnomeframeWindow'
|
||||
|
||||
frame_box = Gtk.Template.Child()
|
||||
btns_basic = Gtk.Template.Child()
|
||||
btns_prime = Gtk.Template.Child()
|
||||
basic_counter = Gtk.Template.Child()
|
||||
basic_percent = Gtk.Template.Child()
|
||||
prime_counter = Gtk.Template.Child()
|
||||
prime_percent = Gtk.Template.Child()
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self._profile: dict[str, bool] = load_profile()
|
||||
|
||||
|
||||
self._connect_frame_btns()
|
||||
|
||||
reset_action = Gio.SimpleAction.new("reset-selections", None)
|
||||
reset_action.connect("activate", self._reset_all)
|
||||
self.get_application().add_action(reset_action)
|
||||
|
||||
self.connect("close-request", self._on_close_request)
|
||||
|
||||
self._calc_frames()
|
||||
|
||||
# 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 _connect_frame_btns(self):
|
||||
button = self.btns_basic.get_first_child()
|
||||
while button is not None:
|
||||
if isinstance(button, Gtk.ToggleButton):
|
||||
btn_id = button.get_name()
|
||||
button.set_active(self._profile.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._profile.get(btn_id, False))
|
||||
button.connect("toggled", self._on_button_toggled)
|
||||
button = button.get_next_sibling()
|
||||
|
||||
def _count_basics(self) -> int:
|
||||
button = self.btns_basic.get_first_child()
|
||||
counter = 0
|
||||
while button is not None:
|
||||
if isinstance(button, Gtk.ToggleButton):
|
||||
if button.get_active():
|
||||
counter = counter+1
|
||||
button = button.get_next_sibling()
|
||||
return counter
|
||||
|
||||
def _count_primes(self) -> int:
|
||||
button = self.btns_prime.get_first_child()
|
||||
counter = 0
|
||||
while button is not None:
|
||||
if isinstance(button, Gtk.ToggleButton):
|
||||
if button.get_active():
|
||||
counter = counter+1
|
||||
button = button.get_next_sibling()
|
||||
return counter
|
||||
|
||||
def _calc_frames(self):
|
||||
basic_counter = self._count_basics()
|
||||
basic_percent = math.trunc(basic_counter / TOTAL_WARFRAMES * 100)
|
||||
prime_counter = self._count_primes()
|
||||
prime_percent = math.trunc(prime_counter / TOTAL_PRIMES * 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))
|
||||
self.prime_percent.set_label(str(prime_percent))
|
||||
|
||||
def _on_button_toggled(self, btn: Gtk.ToggleButton):
|
||||
btn_id = btn.get_name()
|
||||
if btn_id == "GtkToggleButton":
|
||||
print("what")
|
||||
return
|
||||
self._profile[btn_id] = btn.get_active()
|
||||
save_profile(self._profile)
|
||||
self._calc_frames()
|
||||
|
||||
def _reset_all(self, action, param):
|
||||
self._profile.clear()
|
||||
button = self.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.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)
|
||||
return False
|
||||
|
||||
|
||||
Reference in New Issue
Block a user