mirror of
https://github.com/aclist/dztui.git
synced 2026-11-12 04:50:37 +01:00
Compare commits
82
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2208787b4 | ||
|
|
77682cacd4 | ||
|
|
e8d7d9eb15 | ||
|
|
047b9dbbf6 | ||
|
|
d5c9f24ad5 | ||
|
|
f486cc506f | ||
|
|
307792ba5b | ||
|
|
f7d95d5407 | ||
|
|
8b09622fe5 | ||
|
|
7f6d61083c | ||
|
|
3e7b49203a | ||
|
|
269ef18a48 | ||
|
|
8aaa806cd3 | ||
|
|
e4dc545038 | ||
|
|
35623b6456 | ||
|
|
b9cb7ec973 | ||
|
|
e29433b388 | ||
|
|
170990ee1d | ||
|
|
1dbbd5977d | ||
|
|
c267b1bbc7 | ||
|
|
439109d11d | ||
|
|
d0b3a723c6 | ||
|
|
535bc505f7 | ||
|
|
f2375feeec | ||
|
|
1b9448735d | ||
|
|
d5693b07fb | ||
|
|
7a6f4a2bb9 | ||
|
|
ba92d15085 | ||
|
|
65e04c2fc4 | ||
|
|
0907f5a474 | ||
|
|
bfa83c3f94 | ||
|
|
aef48e515b | ||
|
|
e6ebeaeb5e | ||
|
|
88c740105a | ||
|
|
b98a6f95a6 | ||
|
|
87d76e83ba | ||
|
|
a5374d1f1a | ||
|
|
dcc4cb494b | ||
|
|
ff1e150eb1 | ||
|
|
c7ef91f02e | ||
|
|
95bc05eab5 | ||
|
|
89dc6aa8a8 | ||
|
|
9e8e3730c7 | ||
|
|
52c126ff8c | ||
|
|
e5c8f2a69c | ||
|
|
63316a01a9 | ||
|
|
0729273503 | ||
|
|
91d164d666 | ||
|
|
0d7f022c43 | ||
|
|
f76c400697 | ||
|
|
7ae5880890 | ||
|
|
afbced228d | ||
|
|
f5d7bf713d | ||
|
|
c1630adee1 | ||
|
|
88f6b84bea | ||
|
|
134dcb6f0b | ||
|
|
cc0354c881 | ||
|
|
f2d21be347 | ||
|
|
f65fa153cc | ||
|
|
8aa7328d8f | ||
|
|
bb04854cd7 | ||
|
|
5014b2a467 | ||
|
|
d020295aa3 | ||
|
|
cb03db6032 | ||
|
|
9b948271e1 | ||
|
|
7322970c54 | ||
|
|
504a2a724c | ||
|
|
c66511b005 | ||
|
|
0c4026daf8 | ||
|
|
e03c6905c6 | ||
|
|
8fb3462f35 | ||
|
|
bfe9b9d147 | ||
|
|
9f36569fd4 | ||
|
|
da18b41136 | ||
|
|
f092207c61 | ||
|
|
c94e4e73db | ||
|
|
8da264686a | ||
|
|
598973104c | ||
|
|
3677b01fdc | ||
|
|
4f6223f90b | ||
|
|
441066a1a0 | ||
|
|
fa1ac8fa31 |
No files matched your search
+3
-7
@@ -57,6 +57,7 @@
|
||||
- List timestamp of last DayZ client update
|
||||
- Validate mods via Mod page to pull in updates
|
||||
- Support for Nasdara (Badlands) DLC
|
||||
- Load offline mods
|
||||
|
||||
### Changed
|
||||
- Conform to PEP 440 versioning for beta versions
|
||||
@@ -99,16 +100,11 @@
|
||||
- Current keyword filter not being applied when refreshing table
|
||||
- Steam Flatpak cannot run DZGUI as a non-Steam game
|
||||
- Collisions between native Steam and Steam Flatpak clients
|
||||
|
||||
### Unreleased
|
||||
- Load offline mods
|
||||
- Local documentation
|
||||
- Raw debug command in context menu
|
||||
- Sticky toggle signal on radio buttons when reloading Options
|
||||
|
||||
### Developer-facing
|
||||
- Add pyproject.toml file
|
||||
- Show deprecation warnings
|
||||
- Options -> Dev page
|
||||
- Raw debug command in preconnect menu
|
||||
|
||||
## [6.0.5] 2026-05-17
|
||||
### Changed
|
||||
|
||||
+2
-3
@@ -8,7 +8,7 @@ If you are a developer, you can skip to the end.
|
||||
# Requesting help
|
||||
|
||||
If you encounter a problem with DZGUI, you can submit tickets on the GitHub
|
||||
[issue tracker](https://github.com/aclist/dztui/issues) under the
|
||||
[issue tracker](https://github.com/aclist/dzgui/issues) under the
|
||||
`troubleshooting` tag.
|
||||
|
||||
# How can I help the project?
|
||||
@@ -24,7 +24,7 @@ accordingly.
|
||||
|
||||
## Submitting a ticket
|
||||
|
||||
Navigate to the GitHub [issue tracker](https://github.com/aclist/dztui/issues).
|
||||
Navigate to the GitHub [issue tracker](https://github.com/aclist/dzgui/issues).
|
||||
From there, follow the onscreen prompts. You will be asked questions such as:
|
||||
|
||||
- What version are you using?
|
||||
@@ -55,7 +55,6 @@ The following naming conventions apply for PRs:
|
||||
- fix/<your-fix> - patch/hotfix branches
|
||||
- feat/<your-feature> - feature branches
|
||||
- doc/<your-doc-branch> - documentation branches
|
||||
- infra/<your-infra-branch> - infrastructure branches
|
||||
|
||||
Implement your changes and test them locally. If they work, you may
|
||||
open a merge request, and then we review your changes. If everything is OK, it
|
||||
|
||||
+6
-5
@@ -5,7 +5,7 @@
|
||||
### Fetch submodules
|
||||
git submodule update --recursive --init
|
||||
|
||||
### Install with dev dependencies
|
||||
### Install into editable venv with dev dependencies
|
||||
uv pip install -e '.[dev]'
|
||||
|
||||
This includes all dependencies for building, linting, testing, and checking in code.
|
||||
@@ -16,7 +16,7 @@ docs: documentation tools
|
||||
lint: code linting, fixing, and type checking
|
||||
test: unit and integration tests
|
||||
|
||||
## Miscellaneous
|
||||
## Build process
|
||||
|
||||
### Setting up precommit hooks
|
||||
pre-commit install
|
||||
@@ -25,6 +25,7 @@ pre-commit install
|
||||
sphinx-build -M html source build -a
|
||||
|
||||
### Building standalone release binaries
|
||||
1. Set up the build target: `rustup target add x86_64-unknown-linux-musl`
|
||||
2. Run `python3 scripts/release.py`
|
||||
3. Generate a release and tag against the resulting tarfile generated in `dist`
|
||||
1. Install the applicable system package containing the 'musl-gcc' wrapper
|
||||
2. Set up the build target: `rustup target add x86_64-unknown-linux-musl`
|
||||
3. Run `python3 scripts/release.py`
|
||||
4. Generate a release and tag against the resulting tarfile generated in `dist`
|
||||
@@ -1,11 +1,11 @@
|
||||
Submitting a ticket
|
||||
==========================
|
||||
|
||||
Tickets are tracked at the `DZGUI issue tracker <https://github.com/aclist/dztui/issues>`_.
|
||||
Tickets are tracked at the `DZGUI issue tracker <https://github.com/aclist/dzgui/issues>`_.
|
||||
|
||||
You will need to `register for a GitHub account <https://github.com/signup>`_ in order to submit one.
|
||||
|
||||
If have checked the :doc:`kb` and `closed issues <https://github.com/aclist/dztui/issues?q=is%3Aissue+is%3Aclosed>`_ area and did not find a solution, three ticket types are provided for your
|
||||
If have checked the :doc:`kb` and `closed issues <https://github.com/aclist/dzgui/issues?q=is%3Aissue+is%3Aclosed>`_ area and did not find a solution, three ticket types are provided for your
|
||||
convenience:
|
||||
|
||||
- **Bug report**: for unexpected, breaking behavior in the application
|
||||
@@ -20,18 +20,17 @@ Below are some examples of how to format a constructive report that will ensure
|
||||
A non-actionable report
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Subject: add button
|
||||
Subject: BUG: add button
|
||||
|
||||
hi, you must add a button to show how long I have to wait to join a full server, the app is useless without this...do your job, guys!!! ;-)
|
||||
|
||||
|
||||
This report exhibits a number of problems:
|
||||
|
||||
- Vague title and summary: in addition to being unclear, this will slow down filing the incoming report.
|
||||
- Problem space is poorly defined: it is not clear where the issue lies, where the feature is supposed to be implemented, nor why.
|
||||
- Lack of context: the feature may already exist, but there is not enough information to go on. Alternatively, the feature may be technically impossible to implement, but there is not enough
|
||||
information to go on.
|
||||
- Incorrect report type: this is a feature request filed as a bug report.
|
||||
- Vague title and summary: this will slow down filing the incoming report.
|
||||
- Problem space is poorly defined: it is not clear where the issue lies, where the feature is supposed to be implemented, nor why.
|
||||
- Lack of context: the feature may already exist, but there is not enough information to go on. Alternatively, the feature may be technically impossible to implement.
|
||||
- Unprofessional tone: the message is disrespectful towards project members and inserts the user's subjective opinion.
|
||||
|
||||
An actionable report
|
||||
@@ -42,7 +41,7 @@ An actionable report
|
||||
This occurs specifically on version 5.5.0-beta.2 if the setting ALLOW_MULTI is set to ON. I was able to reproduce it using the following steps:
|
||||
|
||||
Distribution: Fedora
|
||||
|
||||
|
||||
- Ensure that ALLOW_MULTI is set to ON
|
||||
- Open the application and immediately navigate to the Server Browser
|
||||
- Uses the Ctrl-click binding to select multiple rows in the table
|
||||
@@ -56,5 +55,5 @@ This report is actionable because it is objective, specific, and detailed:
|
||||
- The subject is clearly defined: specific details ensure it will be filed in a timely manner.
|
||||
- Scope is explicit: the specific version, settings, and conditions under which the problem occur were given.
|
||||
- Ample context: the user gave steps to reproduce the problem and clarified each one.
|
||||
- Courteous tone: the user interacted with project members in a professional manner.
|
||||
- Supporting information: the user provided a crash dump to expedite resolution of the issue.
|
||||
- Courteous tone: the user interacted with project members in a professional manner.
|
||||
+2
-2
@@ -36,5 +36,5 @@ html_show_copyright = False
|
||||
html_show_sourcelink = False
|
||||
|
||||
today_fmt = "%Y-%m-%d"
|
||||
release = "6.x.x - 7.x.x"
|
||||
version = "6.x.x - 7.x.x"
|
||||
release = "7.x.x"
|
||||
version = "7.x.x"
|
||||
@@ -1,19 +1,14 @@
|
||||
Developers
|
||||
===========
|
||||
|
||||
This project is open source under the GPLv3 license. You can find the source code on
|
||||
`GitHub <https://github.com/aclist/dztui>`_. If you wish to contribute to
|
||||
`GitHub <https://github.com/aclist/dzgui>`_. If you wish to contribute to
|
||||
development, you will find further help in the
|
||||
`CONTRIBUTING.md <https://github.com/aclist/dztui/blob/main/CONTRIBUTING.md>`_
|
||||
`CONTRIBUTING.md <https://github.com/aclist/dzgui/blob/dzgui7/CONTRIBUTING.md>`_
|
||||
file.
|
||||
|
||||
All contributors are expected to act in accordance with the project's
|
||||
`code of conduct <https://github.com/aclist/dztui/blob/main/CODE_OF_CONDUCT.md>`_,
|
||||
`code of conduct <https://github.com/aclist/dzgui/blob/dzgui7/CODE_OF_CONDUCT.md>`_,
|
||||
and follow the project's standards and guidelines.
|
||||
|
||||
To install DZGUI and related dependencies for linting, testing, and version control,
|
||||
invoke the command ``uv pip install .[dev]``.
|
||||
|
||||
After this process, install the applicable pre-commit hooks by invoking the command ``pre-commit install``.
|
||||
|
||||
These pre-commit hooks enforce code checks that prevent making commits unless the relevant tests pass.
|
||||
Detailed instructions on setting up a build environment and installing DZGUI from source code
|
||||
can be found in the `DEVELOPERS.md <https://github.com/aclist/dzgui/blob/dzgui7/DEVELOPERS.md>`_ file.
|
||||
@@ -1,110 +0,0 @@
|
||||
Installation (DZGUI 6 legacy)
|
||||
==================
|
||||
|
||||
Basic runtime dependencies
|
||||
---------------------------------
|
||||
|
||||
If not already installed, the below can be found in your system’s package manager.
|
||||
|
||||
If any dependencies are missing when the application starts, it will warn you, so you need not take any preemptive measures here.
|
||||
|
||||
- curl
|
||||
- jq
|
||||
- steam
|
||||
- wmctrl or xdotool
|
||||
- zenity
|
||||
|
||||
.. note::
|
||||
All dependencies are installed out of the box on Steam Deck.
|
||||
|
||||
UI dependencies (Python and PyGObject)
|
||||
---------------------------------------
|
||||
.. important::
|
||||
As of version 6.0.0, you must have a valid Python 3.13 and matching PyGObject environment.
|
||||
Depending on your distribution, this may require additional manual intervention.
|
||||
|
||||
Arch Linux (vanilla variants)
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
You will need the ``python3.13`` package, if it is not already installed.
|
||||
|
||||
Install the following build dependencies:
|
||||
|
||||
.. code:: console
|
||||
|
||||
sudo pacman -S cmake gcc pkgconf python-cairo
|
||||
|
||||
.. include:: venv.rst
|
||||
|
||||
CachyOS (Arch)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
Refer to the build instructions for Arch Linux first.
|
||||
The ``python3.13`` package is not available on CachyOS, so it is recommended to
|
||||
use ``uv`` to manage Python versions.
|
||||
|
||||
.. code:: console
|
||||
|
||||
sudo pacman -S uv
|
||||
|
||||
.. include:: venv.rst
|
||||
|
||||
Manjaro 26 (Arch)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
If Python 3.13 is not already available out of the box, refer to the build instructions for :ref:`Arch Linux (vanilla variants)`.
|
||||
|
||||
Debian 13 variants
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
Python 3.13 and PyGObject are available out of the box. No additional steps are necessary.
|
||||
You can proceed to the :ref:`Installscript`.
|
||||
|
||||
Fedora 43 variants
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
Fedora 43 ships with a newer version of Python (3.14), so you must set up Python 3.13 in its own
|
||||
virtual environment (venv) to prevent it from conflicting with the system package.
|
||||
|
||||
Install the following build dependencies:
|
||||
|
||||
.. code:: console
|
||||
|
||||
sudo dnf install cairo cairo-devel cairo-gobject-devel gcc python3.13 python3.13-devel
|
||||
|
||||
.. include:: venv.rst
|
||||
|
||||
Ubuntu 24 variants
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
Ubuntu ships with an older version of Python (3.12). You must install additional dependencies.
|
||||
Add the PPA below to add Python 3.13 to your sources:
|
||||
|
||||
.. code:: console
|
||||
|
||||
sudo add-apt-repository ppa:deadsnakes/ppa
|
||||
|
||||
Install the following build dependencies:
|
||||
|
||||
.. code:: console
|
||||
|
||||
sudo apt install python3.13 python3.13-venv python3.13-dev build-essential python3-gi-cairo pkg-config libgirepository-2.0-dev libcairo2-dev
|
||||
|
||||
.. include:: venv.rst
|
||||
|
||||
Installscript
|
||||
-----------------------
|
||||
|
||||
Invoke the command below from a terminal. This script fetches the main DZGUI script from the repository,
|
||||
makes it executable, and launches DZGUI's first-time setup. After running this installscript, you will
|
||||
have the file ``dzgui.sh`` wherever you invoked the command.
|
||||
|
||||
.. code:: console
|
||||
|
||||
curl -s "https://codeberg.org/aclist/dztui/raw/branch/dzgui/install.sh" | bash
|
||||
|
||||
nixOS installation
|
||||
----------------------
|
||||
Two packages are independently maintained by other contributors. Each takes a slightly different approach.
|
||||
nixOS packages are standalone and do not require cloning the git repository.
|
||||
|
||||
- https://github.com/lelgenio/dzgui-nix
|
||||
- https://github.com/jiriks74/dzgui.flake
|
||||
|
||||
Next steps
|
||||
---------------
|
||||
Proceed to the :doc:`setup` section for setup instructions.
|
||||
@@ -1,33 +0,0 @@
|
||||
Installation (DZGUI 7 beta)
|
||||
==================
|
||||
|
||||
In order to make the first-time setup experience easier for end-users, DZGUI 7 no longer requires
|
||||
the tool to be installed from source code into a working Python environment. All necessary dependencies and the
|
||||
runtime environment are built-in.
|
||||
|
||||
Turnkey installer
|
||||
------------------------
|
||||
|
||||
- Visit the project's `Releases page <https://github.com/aclist/dztui/releases/latest>`_.
|
||||
- Extract the DZGUI tarball from the top of the **Assets** list.
|
||||
- From a terminal, run the command ``./dzgui`` or double click on the extracted file in
|
||||
a file explorer.
|
||||
- Once the setup wizard opens, follow the steps at the bottom of this page.
|
||||
|
||||
UI dependencies (Python and PyGObject)
|
||||
---------------------------------------
|
||||
On most major distributions, GObject introspection libraries should be available by default.
|
||||
|
||||
If you receive an error about a missing ``libgirepository-2.0`` dependency, install it via your system's package manager.
|
||||
|
||||
On Debian-based distributions (Ubuntu, etc.), this package is not explicitly installed on the system.
|
||||
You can install it via the following set of commands:
|
||||
|
||||
.. code:: console
|
||||
|
||||
sudo apt update
|
||||
sudo apt install libgirepository-2.0-0
|
||||
|
||||
Next steps
|
||||
---------------
|
||||
Proceed to the :doc:`setup` section for setup instructions.
|
||||
@@ -0,0 +1,12 @@
|
||||
Command line launch flags
|
||||
==========================
|
||||
When run from the terminal, DZGUI provides several flags:
|
||||
|
||||
``-d, --debug``: enable developer debugging features. This does not add hidden or extra functionality; it merely enables developers to track deprecation warnings and see values stored in memory.
|
||||
End-users will gain no benefit from using this flag.
|
||||
|
||||
``-m, --map``: check and update system map count value. Unnecessary on most modern systems. If your system does not meet the threshold, the DZGUI setup wizard will prompt you to use this flag.
|
||||
|
||||
``-u, --uninstall``: uninstall: granular selection of which state/configuration files to remove.
|
||||
|
||||
``-v, --version``: displays the DZGUI version.
|
||||
@@ -15,8 +15,8 @@ A DayZ server browser/mod manager for Linux. Used to list official and community
|
||||
|
||||
Source code:
|
||||
|
||||
- `GitHub <https://www.github.com/aclist/dztui>`_
|
||||
- `Codeberg (mirror) <https://codeberg.org/aclist/dztui>`_
|
||||
- `GitHub <https://www.github.com/aclist/dzgui>`_
|
||||
- `Codeberg (mirror) <https://codeberg.org/aclist/dzgui>`_
|
||||
|
||||
Installation
|
||||
-------------
|
||||
@@ -56,8 +56,8 @@ See: :doc:`kb` or :ref:`Submitting a ticket`.
|
||||
:maxdepth: 3
|
||||
:hidden:
|
||||
|
||||
steam
|
||||
locale
|
||||
flags
|
||||
|
||||
.. toctree::
|
||||
:caption: Help
|
||||
|
||||
@@ -1,28 +1,37 @@
|
||||
Installation
|
||||
==================
|
||||
|
||||
As of this writing, you have a choice of two DZGUI versions:
|
||||
.. note::
|
||||
Version 6 of DZGUI 6 has been fully replaced with DZGUI 7.
|
||||
Users on existing DZGUI 6 installations should migrate to the DZGUI 7 build.
|
||||
|
||||
- DZGUI 6: Legacy stable version, more complicated to install
|
||||
- DZGUI 7: Unstable beta version, much easier to install
|
||||
In order to make the first-time setup experience easier for end-users, DZGUI 7 no longer requires
|
||||
the tool to be installed from source code into a working Python environment. All necessary dependencies and the
|
||||
runtime environment are built-in.
|
||||
|
||||
DZGUI 6
|
||||
--------------------
|
||||
This version is no longer receiving feature updates, but will receive security updates until it is replaced by DZGUI 7.
|
||||
Turnkey installer
|
||||
------------------------
|
||||
|
||||
DZGUI 6 must be installed from the source code and has many more package dependencies and prerequisites for it to function correctly.
|
||||
If you are looking for a one-click, self-contained installer, use :ref:`DZGUI 7` instead.
|
||||
- Visit the project's `Releases page <https://github.com/aclist/dzgui/releases/latest>`_.
|
||||
- Download the DZGUI archive from the top of the **Assets** list. This file will be labeled ``dzgui-<VERSION>-x86_64-unknown-linux-musl.tar.gz``.
|
||||
- Extract the archive's contents.
|
||||
- From a terminal, run the command ``./dzgui`` or double click on the extracted file in a file explorer.
|
||||
- Once the setup wizard opens, follow the steps at the bottom of this page.
|
||||
|
||||
To install **DZGUI 6**, visit :doc:`dzgui6`.
|
||||
UI dependencies (Python and PyGObject)
|
||||
---------------------------------------
|
||||
On most major distributions, GObject introspection libraries should be available by default.
|
||||
|
||||
If you receive an error about a missing ``libgirepository-2.0`` dependency, install it via your system's package manager.
|
||||
|
||||
DZGUI 7
|
||||
--------------------
|
||||
This version is currently in beta testing and has all of the features found in DZGUI 6, but may be unstable.
|
||||
On Debian-based distributions (Ubuntu, etc.), this package is not explicitly installed on the system.
|
||||
You can install it via the following set of commands:
|
||||
|
||||
DZGUI 7 has an all-in-one installer and setup wizard, and many new features not found in DZGUI 6.
|
||||
However, this codebase is newer and is provided as a pre-release beta candidate.
|
||||
.. code:: console
|
||||
|
||||
If you want the simplest installer and don't mind experiencing the occasional bug, use this version.
|
||||
sudo apt update
|
||||
sudo apt install libgirepository-2.0-0
|
||||
|
||||
To install **DZGUI 7**, visit :doc:`dzgui7`.
|
||||
Next steps
|
||||
---------------
|
||||
Proceed to the :doc:`setup` section for setup instructions.
|
||||
+15
-2
@@ -98,15 +98,28 @@ tend to have less headroom for simultaneous connections than their wired counter
|
||||
|
||||
DZG-009: Floating dialogs appear maximized on tiling window managers
|
||||
-------------------------------------------------------------------
|
||||
|
||||
The main DZGUI window and its child dialogs are expected to be rendered as floating by your window manager.
|
||||
DZGUI sends window manager hints to this effect, but tiling window managers designed to bisect the screen into quadrants (e.g., i3 window manager)
|
||||
may try to always launch applications in fullscreen.
|
||||
|
||||
To resolve this, set specific exclusions or window hints in your WM's configuration file.
|
||||
For example, for i3, add the following to your `XDG_CONFIG_HOME/i3/config` file (defaults to `$HOME/.config/i3/config`):
|
||||
For example, for i3, add the following to your ``XDG_CONFIG_HOME/i3/config`` file (defaults to ``$HOME/.config/i3/config``):
|
||||
|
||||
.. code:: console
|
||||
|
||||
for_window [instance="DZGUI"] floating enable, move position center
|
||||
for_window [instance="DZGUI - Dialog"] floating enable, move position center
|
||||
|
||||
DZG-010: Maps combobox extends to full height of window when there are many maps to choose from
|
||||
---------------------------------------------------------------------------------------------------
|
||||
This is a limitation of GTK 3. If and when DZGUI is refactored to support the GTK 4 API, this visual glitch is expected to be resolved.
|
||||
|
||||
DZG-011: GTK reports "Failed to load module 'xapp-gtk3-module' when starting DZGUI
|
||||
---------------------------------------------------------------------------------------------------
|
||||
This is a purely cosmetic message that appears when Cinnamon (Xapp) desktop integations are not present.
|
||||
While you can suppress this message by installing the package ``xapp``, if you are not actually using Cinnamon as
|
||||
part of your desktop environment, it will pull in a large number of unnecessary dependencies.
|
||||
|
||||
DZG-012: After adding the DZGUI Steam shortcut via the installer, shortcut icons do not appear in the small library list
|
||||
-------------------------------------------------------------------------------------------------------------------------
|
||||
It can take 1-2 restarts of the Steam client for these icons to synchronize.
|
||||
+14
-5
@@ -19,14 +19,14 @@ You will need at a minimum a :ref:`Steam Web API key<steam_api>` in order to bro
|
||||
|
||||
Steam Web API key
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
|
||||
**This step is required**.
|
||||
|
||||
Register for a `Steam Web API key <https://steamcommunity.com/dev/apikey>`_ (free) using your Steam account. You will be asked for a unique URL for your app when registering.
|
||||
Register for a `Steam Web API key <https://steamcommunity.com/dev/apikey>`_ (free) using your Steam account.
|
||||
|
||||
Since this key is for a personal use application and does not actually call back anywhere, set a generic local identifier here like "127.0.0.1" or some other name that is meaningful to you.
|
||||
You will be asked for a unique URL for your app when registering. Typically, an API would send logs back to your URL in the form of callbacks.
|
||||
However, since these API queries will be used for local consumption by a single user only, you can set a generic local identifier here like ``127.0.0.1`` to have the server discard them.
|
||||
|
||||
Once configured, you can insert this key in the app when launching it for the first time.
|
||||
Once configured, you can insert this API key in the app when launching it for the first time.
|
||||
|
||||
.. note::
|
||||
If you are confused about this requirement, please refer to DZGUI Knowledge Base article :ref:`DZG-007<DZG-007>` for additional information.
|
||||
@@ -36,7 +36,16 @@ Battlemetrics API key
|
||||
.. warning::
|
||||
|
||||
This feature is no longer supported in DZGUI. Battlemetrics has switched to a paid service.
|
||||
Battlemetrics queries are expected to fail in DZGUI 6. In DZGUI 7, this feature has been removed entirely.
|
||||
|
||||
Choosing a Steam client
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
If multiple Steam clients are available (native Steam client/Flatpak Steam client), you will be
|
||||
asked to choose which you want to use. Due to the way Steam's inter-process communication and internal URIs work,
|
||||
if you choose one or the other, you should not run both clients simultaneously. This can lead DZGUI to send requests to the wrong one.
|
||||
|
||||
.. note::
|
||||
If you decide to switch to using a different Steam client with DZGUI, please uninstall fully (``dzgui -u``) and re-run the first time setup.
|
||||
|
||||
|
||||
Next steps
|
||||
--------------
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
Steam integration
|
||||
====================
|
||||
|
||||
.. note::
|
||||
|
||||
In :doc:`DZGUI 7 <dzgui7>`, Steam shortcut creation and cover art setup are handled automatically during setup. You can skip this step unless you are using DZGUI 6.
|
||||
|
||||
DZGUI can be added to Steam as a "non-Steam game" in order to facilitate integration with Steam Deck or desktop environments.
|
||||
|
||||
First, launch Steam in the **Large** (default) view.
|
||||
|
||||
.. attention::
|
||||
Steam Deck users: Gaming Mode has limited support for configuration of custom games. You will need to switch to **Desktop Mode** in order to add a non-Steam game.
|
||||
|
||||
.. figure:: images/01.png
|
||||
:scale: 70%
|
||||
:alt: Adding a non-Steam game
|
||||
|
||||
Select **Add a Game** > **Add a Non-Steam Game** from the bottom left corner of the Steam client.
|
||||
|
||||
Navigate to ``$HOME/.local/share/applications`` and select ``dzgui.desktop``.
|
||||
|
||||
Select **Add Selected Programs** to add a shortcut to DZGUI.
|
||||
|
||||
Artwork
|
||||
^^^^^^^^^^
|
||||
|
||||
DZGUI ships with custom artwork that provides various cover/splash images to allow it to integrate with the rest of your games library.
|
||||
|
||||
.. figure:: images/02.png
|
||||
:scale: 70%
|
||||
:alt: Adding a non-Steam game
|
||||
|
||||
From the main library view, navigate to the app’s details page and right-click the blank image header at the top.
|
||||
|
||||
.. figure:: images/03.png
|
||||
:scale: 70%
|
||||
:alt: Adding a non-Steam game
|
||||
|
||||
|
||||
Select **Set Custom Background**
|
||||
|
||||
Select to display **All Files** from the **File type** dropdown
|
||||
|
||||
Navigate to ``$HOME/.local/share/dzgui`` and select ``hero.png``.
|
||||
|
||||
.. figure:: images/04.png
|
||||
:scale: 70%
|
||||
:alt: Adding a non-Steam game
|
||||
|
||||
Next, right-click the image background and select **Set Custom Logo**.
|
||||
|
||||
Navigate to ``$HOME/.local/share/dzgui`` and select ``logo.png``.
|
||||
|
||||
.. figure:: images/05.png
|
||||
:scale: 70%
|
||||
:alt: Adding a non-Steam game
|
||||
|
||||
Notice that this removes the redundant app name that occluded the image, as seen above.
|
||||
|
||||
.. figure:: images/06.png
|
||||
:scale: 70%
|
||||
:alt: Adding a non-Steam game
|
||||
|
||||
Next, navigate to your Library index (looks like a bookshelf of cover art) and find the DZGUI app.
|
||||
|
||||
Right-click its cover and select **Manage** > **Set custom artwork**.
|
||||
|
||||
Navigate to ``$HOME/.local/share/dzgui`` and select ``grid.png``.
|
||||
|
||||
.. figure:: images/07.png
|
||||
:scale: 70%
|
||||
:alt: Adding a non-Steam game
|
||||
|
||||
The final result will create box art looking like the above.
|
||||
|
||||
.. figure:: images/08.png
|
||||
:scale: 100%
|
||||
:alt: Adding a non-Steam game
|
||||
|
||||
Next, right-click the DZGUI entry in your Library and select **Properties** to open the properties dialog. Next to the **Shortcut** field, you will see a small square box which represents the game’s
|
||||
icon. Click this to open a file explorer, navigate to ``$HOME/.local/share/dzgui``, and select ``icon.png``. This will add a small icon to the list view.
|
||||
|
||||
Finally, after you launch DZGUI for the first time, you should quit the application and return to the Library view. Select the **Recent Games** dropdown on the right-hand side.
|
||||
|
||||
Steam shows a collection of box art based on categories like "Play Next", "Recent Games", etc. Look for a **downward-pointing caret icon** and click it, then select the **Recent Games** category.
|
||||
|
||||
.. figure:: images/09.png
|
||||
:scale: 100%
|
||||
:alt: Adding a non-Steam game
|
||||
|
||||
If DZGUI was the last item played, it will be shown with a landscape, rather than portrait, orientation cover, which is initially blank.
|
||||
|
||||
To customize this cover, right click it and select **Manage** > **Set custom artwork**, navigate to ``$HOME/.local/share/dzgui``, then select ``hero.png``.
|
||||
@@ -1,12 +1,8 @@
|
||||
Uninstallation
|
||||
==================
|
||||
To uninstall DZGUI and its associated files, invoke ``./dzgui`` from the terminal with the ``-u`` or ``--uninstall`` flag.
|
||||
|
||||
To uninstall DZGUI and its associated files, invoke ``./dzgui.sh`` with the ``-u`` or ``--uninstall`` flag.
|
||||
|
||||
Follow the prompts to choose from:
|
||||
|
||||
- **Full uninstall** -- removes the application and all user-defined configuration files
|
||||
- **Partial uninstall** -- removes the application, but keeps user-defined configuration files
|
||||
You will be prompted to select atomic files you wish to remove.
|
||||
|
||||
.. tip::
|
||||
If you intend to reinstall DZGUI again, keeping your configuration files is recommended.
|
||||
If you intend to reinstall DZGUI again, keeping your configuration files at a minimum is recommended.
|
||||
+12
-8
@@ -1,25 +1,29 @@
|
||||
Usage
|
||||
=========
|
||||
|
||||
It is always advised to have Steam running in the background. DZGUI is meant to run "on top of" Steam.
|
||||
|
||||
DZGUI can be launched in one of three ways.
|
||||
|
||||
From a terminal
|
||||
------------------
|
||||
|
||||
Invoke the command ``./dzgui.sh`` from wherever you downloaded the DZGUI script to.
|
||||
Invoke the command ``./dzgui`` from wherever you downloaded DZGUI to.
|
||||
|
||||
Launching from a terminal may give more verbose information in the event of a crash, and can be a good way of troubleshooting problems.
|
||||
|
||||
From the shortcut shipped with the application
|
||||
--------------------------------------------------
|
||||
When completing the first-time setup, you will be asked if you want to create additional system shortcuts.
|
||||
If applicable for yourDesktop Environment, these shortcuts can be created.
|
||||
|
||||
On most distributions, if you are using a desktop environment based on the Freedesktop specification, shortcuts will be installed for you.
|
||||
|
||||
- One shortcut is located under the ``Games`` category of your system’s applications list/start menu.
|
||||
- The other is accessed via the ``DZGUI`` desktop icon (Steam Deck only)
|
||||
- One shortcut will be located under the ``Games`` category of your system’s applications list/start menu.
|
||||
- The other is accessed via the ``DZGUI`` shortcut on the desktop
|
||||
|
||||
From Steam
|
||||
--------------
|
||||
See :doc:`steam`.
|
||||
DZGUI will also ask you if you want to add a "non-Steam game" shortcut to Steam.
|
||||
This shortcut will launch DZGUI directly and comes with integrated grid and cover artwork.
|
||||
|
||||
You must restart Steam for this shortcut to take effect in your library.
|
||||
|
||||
.. note::
|
||||
You can also pass DZGUI launch parameter's to the Steam shortcut via Steam's right-click menu. See :doc:`flags`.
|
||||
+2
-1
@@ -111,7 +111,8 @@ class WorkshopACF(ACF):
|
||||
def __init__(self, file: Path) -> None:
|
||||
super().__init__(file)
|
||||
|
||||
def unpack(self, d: dict, lines: list[Any] = []) -> str:
|
||||
def unpack(self, d: dict[str, Any], lines: list[Any] = []) -> str:
|
||||
self.indent: int
|
||||
t1 = "AppWorkshop"
|
||||
t2 = ("WorkshopItemsInstalled", "WorkshopItemDetails")
|
||||
for k, v in d.items():
|
||||
|
||||
+7
-11
@@ -2,10 +2,9 @@ import hashlib
|
||||
import logging
|
||||
import re
|
||||
|
||||
from concurrent.futures import wait
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from dataclasses import dataclass
|
||||
from concurrent.futures import ThreadPoolExecutor, wait
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from dzgui.api.steam import get_app_path
|
||||
from dzgui.api.servers import get_rules, fqip_to_record
|
||||
@@ -18,8 +17,7 @@ from dzgui.const.constants import (
|
||||
|
||||
from dzgui.config.query import lookup
|
||||
from dzgui.const.enum import Preferences
|
||||
|
||||
from typing import Any
|
||||
from dzgui.init.migrate import clean_legacy_versions_file
|
||||
|
||||
logger = logging.getLogger(APP_NAME)
|
||||
|
||||
@@ -106,10 +104,6 @@ def get_delimited_mods(steam_path: Path) -> list[Any]:
|
||||
return parse_mods(mods)
|
||||
|
||||
|
||||
def get_missing_mods(local: list, remote: list) -> list:
|
||||
return [mod for mod in remote if mod not in local]
|
||||
|
||||
|
||||
def _hash(uid: str, use_custom: bool = False) -> str:
|
||||
md5 = hashlib.md5()
|
||||
md5.update(uid.encode("ascii"))
|
||||
@@ -121,10 +115,12 @@ def remove_stale_signatures(
|
||||
config: Path, versions: Path, ids: list[int] | None = None
|
||||
) -> None:
|
||||
if versions.is_file() is False:
|
||||
logger.warning("Creating new version signatures file")
|
||||
logger.debug("Creating new version signatures file")
|
||||
versions.touch()
|
||||
return
|
||||
|
||||
clean_legacy_versions_file(versions)
|
||||
|
||||
path = lookup(config, Preferences.DEFAULT)
|
||||
steam_path = Path(path)
|
||||
if ids is None:
|
||||
@@ -144,7 +140,7 @@ def remove_stale_signatures(
|
||||
|
||||
|
||||
def find_stale_mods(config: Path) -> list[int]:
|
||||
def push_record(rec: str) -> list:
|
||||
def push_record(rec: str) -> list[int]:
|
||||
record = fqip_to_record(rec)
|
||||
if record is None:
|
||||
return []
|
||||
|
||||
+11
-8
@@ -9,10 +9,11 @@ import subprocess
|
||||
import threading
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Optional, TYPE_CHECKING, Union
|
||||
from typing import Any, TYPE_CHECKING, Union
|
||||
|
||||
from dzgui.const.constants import APP_NAME, REQUEST_TIMEOUT
|
||||
from dzgui.const.endpoints import STEAM_SERVERS
|
||||
from dzgui.model.proxy_model import ServerRow
|
||||
from dzgui.util.format import format_exception
|
||||
from dzgui.util import strings
|
||||
|
||||
@@ -111,7 +112,7 @@ def get_netmask() -> str:
|
||||
|
||||
|
||||
# TODO: rename
|
||||
def test_ip(suffix: int, port: int, event: threading.Event) -> dict | None:
|
||||
def test_ip(suffix: int, port: int, event: threading.Event) -> dict[str, Any] | None:
|
||||
if event.is_set():
|
||||
return None
|
||||
netmask = get_netmask()
|
||||
@@ -139,7 +140,9 @@ def sanitize(name: str) -> str:
|
||||
return name
|
||||
|
||||
|
||||
def parse_json(json: list) -> list:
|
||||
def parse_json(
|
||||
json: list[dict[str, Any]],
|
||||
) -> list[ServerRow]:
|
||||
"""
|
||||
Server metadata is underspecified and server operators
|
||||
tend to insert random garbage in the headers. In case
|
||||
@@ -224,7 +227,7 @@ def parse_json(json: list) -> list:
|
||||
mapname = row["map"].lower()
|
||||
players = row["players"]
|
||||
max_players = row["max_players"]
|
||||
raw = (
|
||||
raw = ServerRow(
|
||||
name,
|
||||
mapname,
|
||||
view,
|
||||
@@ -488,7 +491,7 @@ def query_by_record(record: Record, update_gameport: bool = False) -> A2SInfo:
|
||||
return A2SInfo(record, None)
|
||||
|
||||
|
||||
def short_ip_to_record(addr: str) -> Optional[Record]:
|
||||
def short_ip_to_record(addr: str) -> Record | None:
|
||||
r = addr.split(":")
|
||||
# TODO: raise exceptions instead of Nonetype
|
||||
if len(r) != 2:
|
||||
@@ -496,7 +499,7 @@ def short_ip_to_record(addr: str) -> Optional[Record]:
|
||||
return Record(r[0], 0, int(r[1]))
|
||||
|
||||
|
||||
def fqip_to_record(addr: str) -> Optional[Record]:
|
||||
def fqip_to_record(addr: str) -> Record | None:
|
||||
r = addr.split(":")
|
||||
if len(r) != 3:
|
||||
return None
|
||||
@@ -507,14 +510,14 @@ def record_to_fqip(record: Record) -> str:
|
||||
return f"{record.ip}:{record.gameport}:{record.qport}"
|
||||
|
||||
|
||||
def response_to_fqip(res: dict) -> str:
|
||||
def response_to_fqip(res: dict[str, Any]) -> str:
|
||||
ip = res["addr"].split(":")[0]
|
||||
gameport = res["gameport"]
|
||||
qport = res["addr"].split(":")[1]
|
||||
return f"{ip}:{gameport}:{qport}"
|
||||
|
||||
|
||||
def response_to_record(res: dict) -> Record:
|
||||
def response_to_record(res: dict[str, Any]) -> Record:
|
||||
ip = res["addr"].split(":")[0]
|
||||
gameport = res["gameport"]
|
||||
qport = res["addr"].split(":")[1]
|
||||
|
||||
+2
-3
@@ -6,7 +6,6 @@ from importlib import resources
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
|
||||
from dzgui.const.constants import (
|
||||
APP_NAME,
|
||||
APP_NAME_LOWER,
|
||||
@@ -18,8 +17,8 @@ from dzgui.config.query import lookup
|
||||
from dzgui.config.userprefs import UserPrefs
|
||||
from dzgui.config.xdg import get_xdg_paths, parse_filepaths
|
||||
from dzgui.init.migrate import (
|
||||
has_new_config,
|
||||
copy_state_files,
|
||||
has_new_config,
|
||||
migrate_cols_file,
|
||||
)
|
||||
from dzgui.init.prereqs import has_flatpak_client, has_steam_client
|
||||
@@ -107,7 +106,7 @@ def load_steam_icon() -> None:
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk # noqa
|
||||
from gi.repository import Gtk
|
||||
|
||||
try:
|
||||
traversable = resources.files(APP_NAME_LOWER).joinpath(ICONS_PATH)
|
||||
|
||||
@@ -5,6 +5,7 @@ from dzgui.util._json import read_json
|
||||
|
||||
from dzgui.const.enum import Preferences
|
||||
|
||||
|
||||
class ConfigFileError(Exception):
|
||||
pass
|
||||
|
||||
@@ -39,4 +40,4 @@ def get_favorites(path: Path) -> list[str]:
|
||||
|
||||
|
||||
def enum_to_key(enum: Preferences) -> str:
|
||||
return str(enum.dict["key"])
|
||||
return str(enum.key)
|
||||
@@ -6,7 +6,8 @@ if TYPE_CHECKING:
|
||||
from dzgui.util.ip import Coords
|
||||
|
||||
|
||||
# NOTE: mutable dataclass, 'use_miles' key changes on demand
|
||||
# NOTE: mutable dataclass: 'use_miles' key changes on demand
|
||||
# 'frozen' cannot be True
|
||||
@dataclass(slots=True)
|
||||
class UserPrefs:
|
||||
is_steam_deck: bool
|
||||
|
||||
+3
-3
@@ -12,7 +12,7 @@ from dzgui.const.constants import (
|
||||
from dzgui.init.proc import is_sandboxed
|
||||
|
||||
|
||||
@dataclass
|
||||
@dataclass(slots=True, frozen=True)
|
||||
class Xdg:
|
||||
config: Path
|
||||
columns: Path
|
||||
@@ -51,7 +51,7 @@ def is_writeable(path_str: str) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def get_xdg_paths() -> dict:
|
||||
def get_xdg_paths() -> dict[str, Path]:
|
||||
home = Path.home()
|
||||
xdg_paths = {
|
||||
"XDG_CONFIG_HOME": home / ".config",
|
||||
@@ -73,7 +73,7 @@ def get_xdg_paths() -> dict:
|
||||
return resolved_paths
|
||||
|
||||
|
||||
def parse_filepaths(xdg: dict) -> Xdg:
|
||||
def parse_filepaths(xdg: dict[str, Path]) -> Xdg:
|
||||
config = xdg["XDG_CONFIG_HOME"]
|
||||
state = xdg["XDG_STATE_HOME"]
|
||||
share = xdg["XDG_DATA_HOME"]
|
||||
|
||||
@@ -8,9 +8,9 @@ STEAM_UNSUB = "https://api.steampowered.com/IPublishedFileService/Unsubscribe/v1
|
||||
STEAM_APP_DETAILS = "https://store.steampowered.com/api/appdetails?"
|
||||
|
||||
GITHUB = "https://github.com/aclist"
|
||||
GITHUB_RELEASES = "https://api.github.com/repos/aclist/dztui/releases/latest"
|
||||
GITHUB_RELEASES = "https://api.github.com/repos/aclist/dzgui/releases/latest"
|
||||
CODEBERG_RELEASES = "https://codeberg.org/api/v1/repos/aclist/dzgui/releases/latest"
|
||||
GITHUB_USER_RELEASES = "https://github.com/aclist/dztui/releases"
|
||||
GITHUB_USER_RELEASES = "https://github.com/aclist/dzgui/releases"
|
||||
DB_IP = "https://db-ip.com/db/download/ip-to-city-lite"
|
||||
IP_ECHO = "https://ipecho.net/plain"
|
||||
COORDS_API = "http://ip-api.com/json"
|
||||
|
||||
+224
-206
@@ -1,7 +1,9 @@
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import Any, Self
|
||||
|
||||
from dzgui.util import strings
|
||||
from dzgui.strings import rows
|
||||
|
||||
|
||||
class Port(Enum):
|
||||
@@ -46,125 +48,61 @@ class FilterMode(Enum):
|
||||
TOGGLE_ON = 3
|
||||
|
||||
|
||||
class EnumWithAttrs(Enum):
|
||||
def __new__(cls, *args: Any, **kwargs: Any) -> Self:
|
||||
value = len(cls.__members__) + 1
|
||||
obj = object.__new__(cls)
|
||||
obj._value_ = value
|
||||
return obj
|
||||
|
||||
def __init__(self, d: dict):
|
||||
self.dict = d
|
||||
@dataclass(slots=True, frozen=True)
|
||||
class ModButtonMeta:
|
||||
label: str
|
||||
tooltip: str
|
||||
|
||||
|
||||
class Preferences(EnumWithAttrs):
|
||||
STEAM = {
|
||||
"key": "steam_api",
|
||||
}
|
||||
CLIENT = {
|
||||
"key": "client",
|
||||
}
|
||||
WINDOW = {
|
||||
"key": "fullscreen",
|
||||
}
|
||||
DIST = {
|
||||
"key": "use_miles",
|
||||
}
|
||||
NAME = {
|
||||
"key": "name",
|
||||
}
|
||||
DEFAULT = {
|
||||
"key": "default_steam_path",
|
||||
}
|
||||
FAV_LBL = {
|
||||
"key": "fav_label",
|
||||
}
|
||||
FAV_SRV = {
|
||||
"key": "fav_server",
|
||||
}
|
||||
IP_LIST = {
|
||||
"key": "ip_list",
|
||||
}
|
||||
START_TAB = {
|
||||
"key": "start_tab",
|
||||
}
|
||||
@dataclass(slots=True, frozen=True)
|
||||
class ButtonTypeMeta:
|
||||
label: str
|
||||
tooltip: str
|
||||
opens: NotebookPage | None
|
||||
|
||||
|
||||
class NotebookPage(EnumWithAttrs):
|
||||
OFFLINE = {"crumbs": strings.crumbs.offline, "statusbar": True}
|
||||
CHANGELOG = {"crumbs": strings.crumbs.changelog, "statusbar": True}
|
||||
DEVELOPERS = {"crumbs": strings.crumbs.developers, "statusbar": False}
|
||||
HELP = {"crumbs": strings.crumbs._help, "statusbar": True}
|
||||
KEYS = {"crumbs": strings.crumbs.keys, "statusbar": True}
|
||||
LOG = {"crumbs": strings.crumbs.log, "statusbar": True}
|
||||
MODS = {"crumbs": strings.crumbs.mods, "statusbar": True}
|
||||
OPTIONS = {"crumbs": strings.crumbs.options, "statusbar": False}
|
||||
SERVERS = {"crumbs": strings.crumbs.servers, "statusbar": True}
|
||||
THANKS = {"crumbs": strings.crumbs.thanks, "statusbar": True}
|
||||
CONNECTION = {"crumbs": "Connect", "statusbar": True}
|
||||
@dataclass(slots=True, frozen=True)
|
||||
class ConfigKey:
|
||||
key: str
|
||||
|
||||
|
||||
class RowType(EnumWithAttrs):
|
||||
# TODO: strings
|
||||
CHANGELOG = {
|
||||
"label": "View changelog",
|
||||
"tooltip": "Opens the DZGUI changelog",
|
||||
}
|
||||
SHOW_LOG = {
|
||||
"label": "Show debug log",
|
||||
"tooltip": "Read the DZGUI log generated since startup",
|
||||
}
|
||||
SEPARATOR = {
|
||||
"label": "SEPARATOR",
|
||||
"tooltip": "",
|
||||
}
|
||||
DUMP_LOG = {
|
||||
"label": "Output system info to log file",
|
||||
"tooltip": "Dump diagnostic data for troubleshooting",
|
||||
}
|
||||
DOCS = {
|
||||
"label": "Documentation/help files (GitHub) ⧉",
|
||||
"tooltip": "Open the DZGUI documentation in a browser",
|
||||
}
|
||||
DOCS_FALLBACK = {
|
||||
"label": "Documentation/help files (Codeberg mirror) ⧉",
|
||||
"tooltip": "Open the DZGUI documentation in a browser",
|
||||
}
|
||||
BUGS = {
|
||||
"label": "Report a bug (GitHub) ⧉",
|
||||
"tooltip": "Open the DZGUI issue tracker in a browser",
|
||||
}
|
||||
FORUM = {
|
||||
"label": "DZGUI Subreddit ⧉",
|
||||
"tooltip": "Open the DZGUI discussion forum in a browser",
|
||||
}
|
||||
SPONSOR = {
|
||||
"label": "Sponsor (GitHub) ⧉",
|
||||
"tooltip": "Sponsor development of the DZGUI project",
|
||||
}
|
||||
THANKS = {
|
||||
"label": "Special thanks",
|
||||
"tooltip": "A list of contributors, testers, and sponsors",
|
||||
}
|
||||
@dataclass(slots=True, frozen=True)
|
||||
class NotebookMeta:
|
||||
crumbs: str
|
||||
has_statusbar: bool
|
||||
|
||||
|
||||
# TODO: rename to ContextMenuItem
|
||||
class ContextMenu(EnumWithAttrs):
|
||||
ADD_NOTE = {"label": strings.add_note}
|
||||
ADD_SERVER = {"label": strings.add}
|
||||
CONNECT = {"label": strings.connect}
|
||||
COPY_LOG_CLIPBOARD = {"label": strings.copy_log}
|
||||
COPY_SERVER_IP = {"label": strings.copy_ip}
|
||||
COPY_SERVER_NAME = {"label": strings.copy_name}
|
||||
UNSUB_MOD = {"label": strings.unsub_mod}
|
||||
VALIDATE_MOD = {"label": strings.validate_mod}
|
||||
OPEN_WORKSHOP = {"label": strings.open_workshop}
|
||||
REFRESH_PLAYERS = {"label": strings.refresh_players}
|
||||
REMOVE_HISTORY = {"label": strings.remove_history}
|
||||
REMOVE_SERVER = {"label": strings.remove}
|
||||
SET_FAV = {"label": strings.add_fav}
|
||||
SHOW_DETAILS = {"label": strings.show_details}
|
||||
SHOW_MODS = {"label": strings.show_mods}
|
||||
@dataclass(slots=True, frozen=True)
|
||||
class RowMeta:
|
||||
label: str
|
||||
tooltip: str
|
||||
|
||||
|
||||
@dataclass(slots=True, frozen=True)
|
||||
class ContextMenuMeta:
|
||||
label: str
|
||||
|
||||
|
||||
class ContextMenuItem(Enum):
|
||||
ADD_NOTE = ContextMenuMeta(strings.add_note)
|
||||
ADD_SERVER = ContextMenuMeta(strings.add)
|
||||
CONNECT = ContextMenuMeta(strings.connect)
|
||||
COPY_LOG_CLIPBOARD = ContextMenuMeta(strings.copy_log)
|
||||
COPY_SERVER_IP = ContextMenuMeta(strings.copy_ip)
|
||||
COPY_SERVER_NAME = ContextMenuMeta(strings.copy_name)
|
||||
UNSUB_MOD = ContextMenuMeta(strings.unsub_mod)
|
||||
VALIDATE_MOD = ContextMenuMeta(strings.validate_mod)
|
||||
OPEN_WORKSHOP = ContextMenuMeta(strings.open_workshop)
|
||||
REFRESH_PLAYERS = ContextMenuMeta(strings.refresh_players)
|
||||
REMOVE_HISTORY = ContextMenuMeta(strings.remove_history)
|
||||
REMOVE_SERVER = ContextMenuMeta(strings.remove)
|
||||
SET_FAV = ContextMenuMeta(strings.add_fav)
|
||||
SHOW_DETAILS = ContextMenuMeta(strings.show_details)
|
||||
SHOW_MODS = ContextMenuMeta(strings.show_mods)
|
||||
|
||||
@property
|
||||
def label(self) -> str:
|
||||
return self.value.label
|
||||
|
||||
|
||||
class ContextMenuGroup(Enum):
|
||||
@@ -172,113 +110,193 @@ class ContextMenuGroup(Enum):
|
||||
Groupings of context menu items
|
||||
"""
|
||||
|
||||
SERVER_MOD = (ContextMenu.OPEN_WORKSHOP,)
|
||||
MOD = (ContextMenu.OPEN_WORKSHOP, ContextMenu.UNSUB_MOD, ContextMenu.VALIDATE_MOD)
|
||||
SERVER_MOD = (ContextMenuItem.OPEN_WORKSHOP,)
|
||||
MOD = (
|
||||
ContextMenuItem.OPEN_WORKSHOP,
|
||||
ContextMenuItem.UNSUB_MOD,
|
||||
ContextMenuItem.VALIDATE_MOD,
|
||||
)
|
||||
MOD_OFFLINE = (None,)
|
||||
LOG = (ContextMenu.COPY_LOG_CLIPBOARD,)
|
||||
LOG = (ContextMenuItem.COPY_LOG_CLIPBOARD,)
|
||||
SERVER_BROWSER = (
|
||||
ContextMenu.CONNECT,
|
||||
ContextMenu.ADD_SERVER,
|
||||
ContextMenu.SET_FAV,
|
||||
ContextMenu.COPY_SERVER_NAME,
|
||||
ContextMenu.COPY_SERVER_IP,
|
||||
ContextMenu.ADD_NOTE,
|
||||
ContextMenu.SHOW_MODS,
|
||||
ContextMenu.SHOW_DETAILS,
|
||||
ContextMenu.REFRESH_PLAYERS,
|
||||
ContextMenuItem.CONNECT,
|
||||
ContextMenuItem.ADD_SERVER,
|
||||
ContextMenuItem.SET_FAV,
|
||||
ContextMenuItem.COPY_SERVER_NAME,
|
||||
ContextMenuItem.COPY_SERVER_IP,
|
||||
ContextMenuItem.ADD_NOTE,
|
||||
ContextMenuItem.SHOW_MODS,
|
||||
ContextMenuItem.SHOW_DETAILS,
|
||||
ContextMenuItem.REFRESH_PLAYERS,
|
||||
)
|
||||
SCAN_LAN = (
|
||||
ContextMenu.CONNECT,
|
||||
ContextMenu.SET_FAV,
|
||||
ContextMenu.COPY_SERVER_NAME,
|
||||
ContextMenu.COPY_SERVER_IP,
|
||||
ContextMenu.ADD_NOTE,
|
||||
ContextMenu.SHOW_MODS,
|
||||
ContextMenu.SHOW_DETAILS,
|
||||
ContextMenu.REFRESH_PLAYERS,
|
||||
ContextMenuItem.CONNECT,
|
||||
ContextMenuItem.SET_FAV,
|
||||
ContextMenuItem.COPY_SERVER_NAME,
|
||||
ContextMenuItem.COPY_SERVER_IP,
|
||||
ContextMenuItem.ADD_NOTE,
|
||||
ContextMenuItem.SHOW_MODS,
|
||||
ContextMenuItem.SHOW_DETAILS,
|
||||
ContextMenuItem.REFRESH_PLAYERS,
|
||||
)
|
||||
SAVED = (
|
||||
ContextMenu.CONNECT,
|
||||
ContextMenu.SET_FAV,
|
||||
ContextMenu.REMOVE_SERVER,
|
||||
ContextMenu.COPY_SERVER_NAME,
|
||||
ContextMenu.COPY_SERVER_IP,
|
||||
ContextMenu.ADD_NOTE,
|
||||
ContextMenu.SHOW_MODS,
|
||||
ContextMenu.SHOW_DETAILS,
|
||||
ContextMenu.REFRESH_PLAYERS,
|
||||
ContextMenuItem.CONNECT,
|
||||
ContextMenuItem.SET_FAV,
|
||||
ContextMenuItem.REMOVE_SERVER,
|
||||
ContextMenuItem.COPY_SERVER_NAME,
|
||||
ContextMenuItem.COPY_SERVER_IP,
|
||||
ContextMenuItem.ADD_NOTE,
|
||||
ContextMenuItem.SHOW_MODS,
|
||||
ContextMenuItem.SHOW_DETAILS,
|
||||
ContextMenuItem.REFRESH_PLAYERS,
|
||||
)
|
||||
RECENT = (
|
||||
ContextMenu.CONNECT,
|
||||
ContextMenu.ADD_SERVER,
|
||||
ContextMenu.SET_FAV,
|
||||
ContextMenu.REMOVE_HISTORY,
|
||||
ContextMenu.COPY_SERVER_NAME,
|
||||
ContextMenu.COPY_SERVER_IP,
|
||||
ContextMenu.ADD_NOTE,
|
||||
ContextMenu.SHOW_MODS,
|
||||
ContextMenu.SHOW_DETAILS,
|
||||
ContextMenu.REFRESH_PLAYERS,
|
||||
ContextMenuItem.CONNECT,
|
||||
ContextMenuItem.ADD_SERVER,
|
||||
ContextMenuItem.SET_FAV,
|
||||
ContextMenuItem.REMOVE_HISTORY,
|
||||
ContextMenuItem.COPY_SERVER_NAME,
|
||||
ContextMenuItem.COPY_SERVER_IP,
|
||||
ContextMenuItem.ADD_NOTE,
|
||||
ContextMenuItem.SHOW_MODS,
|
||||
ContextMenuItem.SHOW_DETAILS,
|
||||
ContextMenuItem.REFRESH_PLAYERS,
|
||||
)
|
||||
|
||||
|
||||
class ModButton(EnumWithAttrs):
|
||||
SELECT_ALL = {
|
||||
"label": strings.mod_panel.select_all,
|
||||
"tooltip": strings.mod_panel.select_all_tooltip,
|
||||
}
|
||||
UNSELECT_ALL = {
|
||||
"label": strings.mod_panel.unselect_all,
|
||||
"tooltip": strings.mod_panel.unselect_all_tooltip,
|
||||
}
|
||||
HIGHLIGHT_STALE = {
|
||||
"label": strings.mod_panel.highlight_stale,
|
||||
"tooltip": strings.mod_panel.highlight_stale_tooltip,
|
||||
}
|
||||
UNHIGHLIGHT_STALE = {
|
||||
"label": strings.mod_panel.unhighlight_stale,
|
||||
"tooltip": strings.mod_panel.unhighlight_stale_tooltip,
|
||||
}
|
||||
UNSUB_SELECTED = {
|
||||
"label": strings.mod_panel.unsub_selected,
|
||||
"tooltip": strings.mod_panel.unsub_selected_tooltip,
|
||||
}
|
||||
SELECT_STALE = {
|
||||
"label": strings.mod_panel.select_stale,
|
||||
"tooltip": strings.mod_panel.select_stale_tooltip,
|
||||
}
|
||||
VALIDATE = {
|
||||
"label": strings.mod_panel.validate,
|
||||
"tooltip": strings.mod_panel.validate_tooltip,
|
||||
}
|
||||
class Preferences(Enum):
|
||||
STEAM = ConfigKey("steam_api")
|
||||
CLIENT = ConfigKey("client")
|
||||
WINDOW = ConfigKey("fullscreen")
|
||||
DIST = ConfigKey("use_miles")
|
||||
NAME = ConfigKey("name")
|
||||
DEFAULT = ConfigKey("default_steam_path")
|
||||
FAV_LBL = ConfigKey("fav_label")
|
||||
FAV_SRV = ConfigKey("fav_server")
|
||||
IP_LIST = ConfigKey("ip_list")
|
||||
START_TAB = ConfigKey("start_tab")
|
||||
|
||||
@property
|
||||
def key(self) -> str:
|
||||
return self.value.key
|
||||
|
||||
|
||||
class ButtonType(EnumWithAttrs):
|
||||
SERVERS = {
|
||||
"label": strings.buttons.servers_label,
|
||||
"tooltip": strings.buttons.servers_tooltip,
|
||||
"opens": NotebookPage.SERVERS,
|
||||
}
|
||||
MODS = {
|
||||
"label": strings.buttons.mods_label,
|
||||
"tooltip": strings.buttons.mods_tooltip,
|
||||
"opens": NotebookPage.MODS,
|
||||
}
|
||||
OPTIONS = {
|
||||
"label": strings.buttons.options_label,
|
||||
"tooltip": strings.buttons.options_tooltip,
|
||||
"opens": NotebookPage.OPTIONS,
|
||||
}
|
||||
HELP = {
|
||||
"label": strings.buttons.help_label,
|
||||
"tooltip": strings.buttons.help_tooltip,
|
||||
"opens": NotebookPage.HELP,
|
||||
}
|
||||
EXIT = {
|
||||
"label": strings.buttons.exit_label,
|
||||
"tooltip": strings.buttons.exit_tooltip,
|
||||
"opens": None,
|
||||
}
|
||||
class NotebookPage(Enum):
|
||||
OFFLINE = NotebookMeta(strings.crumbs.offline, True)
|
||||
CHANGELOG = NotebookMeta(strings.crumbs.changelog, True)
|
||||
DEVELOPERS = NotebookMeta(strings.crumbs.developers, False)
|
||||
HELP = NotebookMeta(strings.crumbs._help, True)
|
||||
KEYS = NotebookMeta(strings.crumbs.keys, True)
|
||||
LOG = NotebookMeta(strings.crumbs.log, True)
|
||||
MODS = NotebookMeta(strings.crumbs.mods, True)
|
||||
OPTIONS = NotebookMeta(strings.crumbs.options, False)
|
||||
SERVERS = NotebookMeta(strings.crumbs.servers, True)
|
||||
THANKS = NotebookMeta(strings.crumbs.thanks, True)
|
||||
CONNECTION = NotebookMeta(strings.crumbs.connect, True)
|
||||
|
||||
@property
|
||||
def crumbs(self) -> str:
|
||||
return self.value.crumbs
|
||||
|
||||
@property
|
||||
def has_statusbar(self) -> bool:
|
||||
return self.value.has_statusbar
|
||||
|
||||
|
||||
class RowType(Enum):
|
||||
CHANGELOG = RowMeta(rows.changelog_label, rows.changelog_status)
|
||||
SHOW_LOG = RowMeta(rows.log_label, rows.log_status)
|
||||
SEPARATOR = RowMeta(rows.separator_label, rows.separator_status)
|
||||
DUMP_LOG = RowMeta(rows.system_log_label, rows.system_log_status)
|
||||
DOCS = RowMeta(rows.docs_label, rows.docs_status)
|
||||
DOCS_FALLBACK = RowMeta(rows.docs_fallback_label, rows.docs_fallback_status)
|
||||
BUGS = RowMeta(rows.bugs_label, rows.bugs_status)
|
||||
FORUM = RowMeta(rows.forum_label, rows.forum_status)
|
||||
SPONSOR = RowMeta(rows.sponsor_label, rows.sponsor_status)
|
||||
THANKS = RowMeta(rows.thanks_label, rows.thanks_status)
|
||||
|
||||
@property
|
||||
def label(self) -> str:
|
||||
return self.value.label
|
||||
|
||||
@property
|
||||
def tooltip(self) -> str:
|
||||
return self.value.tooltip
|
||||
|
||||
|
||||
class ModButton(Enum):
|
||||
SELECT_ALL = ModButtonMeta(
|
||||
strings.mod_panel.select_all, strings.mod_panel.select_all_tooltip
|
||||
)
|
||||
UNSELECT_ALL = ModButtonMeta(
|
||||
strings.mod_panel.unselect_all, strings.mod_panel.unselect_all_tooltip
|
||||
)
|
||||
HIGHLIGHT_STALE = ModButtonMeta(
|
||||
strings.mod_panel.highlight_stale, strings.mod_panel.highlight_stale_tooltip
|
||||
)
|
||||
UNHIGHLIGHT_STALE = ModButtonMeta(
|
||||
strings.mod_panel.unhighlight_stale, strings.mod_panel.unhighlight_stale_tooltip
|
||||
)
|
||||
UNSUB_SELECTED = ModButtonMeta(
|
||||
strings.mod_panel.unsub_selected,
|
||||
strings.mod_panel.unsub_selected_tooltip,
|
||||
)
|
||||
SELECT_STALE = ModButtonMeta(
|
||||
strings.mod_panel.select_stale,
|
||||
strings.mod_panel.select_stale_tooltip,
|
||||
)
|
||||
VALIDATE = ModButtonMeta(
|
||||
strings.mod_panel.validate,
|
||||
strings.mod_panel.validate_tooltip,
|
||||
)
|
||||
|
||||
@property
|
||||
def label(self) -> str:
|
||||
return self.value.label
|
||||
|
||||
@property
|
||||
def tooltip(self) -> str:
|
||||
return self.value.tooltip
|
||||
|
||||
|
||||
class ButtonType(Enum):
|
||||
SERVERS = ButtonTypeMeta(
|
||||
strings.buttons.servers_label,
|
||||
strings.buttons.servers_tooltip,
|
||||
NotebookPage.SERVERS,
|
||||
)
|
||||
MODS = ButtonTypeMeta(
|
||||
strings.buttons.mods_label,
|
||||
strings.buttons.mods_tooltip,
|
||||
NotebookPage.MODS,
|
||||
)
|
||||
OPTIONS = ButtonTypeMeta(
|
||||
strings.buttons.options_label,
|
||||
strings.buttons.options_tooltip,
|
||||
NotebookPage.OPTIONS,
|
||||
)
|
||||
HELP = ButtonTypeMeta(
|
||||
strings.buttons.help_label,
|
||||
strings.buttons.help_tooltip,
|
||||
NotebookPage.HELP,
|
||||
)
|
||||
EXIT = ButtonTypeMeta(
|
||||
strings.buttons.exit_label,
|
||||
strings.buttons.exit_tooltip,
|
||||
None,
|
||||
)
|
||||
|
||||
@property
|
||||
def label(self) -> str:
|
||||
return self.value.label
|
||||
|
||||
@property
|
||||
def tooltip(self) -> str:
|
||||
return self.value.tooltip
|
||||
|
||||
@property
|
||||
def opens(self) -> NotebookPage | None:
|
||||
return self.value.opens
|
||||
|
||||
|
||||
HELP_MENU_ROWS = (
|
||||
|
||||
+14
-19
@@ -1,22 +1,19 @@
|
||||
import gi
|
||||
import logging
|
||||
import threading
|
||||
|
||||
from typing import Any, TYPE_CHECKING, Union
|
||||
|
||||
import dzgui.util._json as JSON # noqa
|
||||
|
||||
|
||||
from dzgui.config.userprefs import UserPrefs
|
||||
from dzgui.const.constants import APP_NAME
|
||||
from dzgui.const.enum import (
|
||||
ButtonType,
|
||||
ContextMenu,
|
||||
ContextMenuItem,
|
||||
FilterMode,
|
||||
NotebookPage,
|
||||
Preferences,
|
||||
ServerTab,
|
||||
)
|
||||
|
||||
from dzgui.config.userprefs import UserPrefs
|
||||
from dzgui.const.constants import APP_NAME
|
||||
from dzgui.controllers.emitter import Emitter
|
||||
from dzgui.managers.config import ConfigManager
|
||||
from dzgui.managers.connection import ConnectionManager
|
||||
@@ -29,8 +26,6 @@ from dzgui.util.open_links import open_user_workshop, open_workshop_page
|
||||
from dzgui.views.dialogs.filepicker import FilePicker
|
||||
from dzgui.views.dialogs.generic import ExceptionDialog
|
||||
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, Gdk, GLib, GObject
|
||||
|
||||
@@ -77,7 +72,7 @@ class AppNavigation:
|
||||
|
||||
class Controller(GObject.GObject):
|
||||
def __init__(self) -> None:
|
||||
self.dist_cache: dict[str, "Haversine"] = {}
|
||||
self.dist_cache: dict[str, "Haversine" | None] = {}
|
||||
self.mediator = AppNavigation()
|
||||
|
||||
self.prefs: UserPrefs
|
||||
@@ -196,7 +191,9 @@ class Controller(GObject.GObject):
|
||||
def show_developers_page(self) -> None:
|
||||
self.open_page(NotebookPage.DEVELOPERS)
|
||||
|
||||
def open_page(self, page: NotebookPage) -> None:
|
||||
def open_page(self, page: NotebookPage | None) -> None:
|
||||
if page is None:
|
||||
return
|
||||
self.mediator.grid.notebook.set_page_by_enum(page)
|
||||
|
||||
def open_page_by_button(self, button: "ContextualButton") -> None:
|
||||
@@ -262,7 +259,7 @@ class Controller(GObject.GObject):
|
||||
logger.critical(e)
|
||||
return
|
||||
|
||||
def menu_action(self, action: ContextMenu, tree: "ServerTreeView") -> None:
|
||||
def menu_action(self, action: ContextMenuItem, tree: "ServerTreeView") -> None:
|
||||
context_man = ContextMenuManager(tree, self)
|
||||
context_man.process(action)
|
||||
|
||||
@@ -274,7 +271,7 @@ class Controller(GObject.GObject):
|
||||
except Exception as e:
|
||||
dialog = ExceptionDialog(self, str(e))
|
||||
dialog.show_all()
|
||||
dialog.run()
|
||||
dialog.run() # type: ignore[no-untyped-call]
|
||||
|
||||
def select_colorized(self) -> None:
|
||||
mod_man = self.mediator.modtreeview.get_mod_man()
|
||||
@@ -308,7 +305,7 @@ class Controller(GObject.GObject):
|
||||
except Exception as e:
|
||||
dialog = ExceptionDialog(self, str(e))
|
||||
dialog.show_all()
|
||||
dialog.run()
|
||||
dialog.run() # type: ignore[no-untyped-call]
|
||||
|
||||
def update_steam_api_key(self, text: str) -> None:
|
||||
self.config_man.update_steam_api_key(text)
|
||||
@@ -355,22 +352,20 @@ class Controller(GObject.GObject):
|
||||
filter_man.set_filter(filt, state)
|
||||
self.mediator.filters.set_filters(filters)
|
||||
|
||||
def get_dist_cache(self) -> dict[str, "Haversine"]:
|
||||
def get_dist_cache(self) -> dict[str, Union["Haversine", None]]:
|
||||
return self.dist_cache
|
||||
|
||||
def set_dist_cache(self, addr: str, haversine: "Haversine") -> None:
|
||||
def set_dist_cache(self, addr: str, haversine: Union["Haversine", None]) -> None:
|
||||
self.dist_cache[addr] = haversine
|
||||
|
||||
def get_map_store(self) -> Gtk.ListStore:
|
||||
filter_man = self.get_filter_man()
|
||||
return filter_man.get_map_store()
|
||||
|
||||
def get_enabled_filters(self) -> dict:
|
||||
def get_enabled_filters(self) -> dict[str, bool]:
|
||||
filter_man = self.get_filter_man()
|
||||
return filter_man.get_filters()
|
||||
|
||||
# FIXME: wipe maps store when changing tabs if model is none
|
||||
|
||||
def has_server_model(self) -> bool:
|
||||
treeview = self.get_active_treeview()
|
||||
proxy_man = treeview.get_proxy_man()
|
||||
|
||||
+10
-7
@@ -53,6 +53,11 @@
|
||||
- Expand/collapse changelog versions
|
||||
- Support local light/dark mode changes on system theme
|
||||
- Server time calculator
|
||||
- Add "Unmodded" filter to servers
|
||||
- List timestamp of last DayZ client update
|
||||
- Validate mods via Mod page to pull in updates
|
||||
- Support for Nasdara (Badlands) DLC
|
||||
- Load offline mods
|
||||
|
||||
### Changed
|
||||
- Conform to PEP 440 versioning for beta versions
|
||||
@@ -74,6 +79,7 @@
|
||||
- Enable LAN page Empty/Full filters on first run of app
|
||||
- Propagate subscribed mods to Steam client
|
||||
- Make j/k keys in context menus follow same wraparound logic as down arrow/up arrow
|
||||
- Use SQLite DB for geolocation records
|
||||
|
||||
### Dropped
|
||||
- Battlemetrics API support
|
||||
@@ -92,16 +98,13 @@
|
||||
- Moved dialogs out of threads
|
||||
- History table not being in lock-step with state file
|
||||
- Current keyword filter not being applied when refreshing table
|
||||
|
||||
### Unreleased
|
||||
- Load offline mods
|
||||
- Local documentation
|
||||
- Raw debug command in context menu
|
||||
- Steam Flatpak cannot run DZGUI as a non-Steam game
|
||||
- Collisions between native Steam and Steam Flatpak clients
|
||||
- Sticky toggle signal on radio buttons when reloading Options
|
||||
|
||||
### Developer-facing
|
||||
- Add pyproject.toml file
|
||||
- Show deprecation warnings
|
||||
- Options -> Dev page
|
||||
- Raw debug command in preconnect menu
|
||||
|
||||
## [6.0.5] 2026-05-17
|
||||
### Changed
|
||||
|
||||
+22
-1
@@ -1,12 +1,21 @@
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import shutil
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from dzgui.const.constants import LEGACY_CONFIG_PATH, LEGACY_COLS_PATH, LEGACY_IPS_PATH
|
||||
from dzgui.const.constants import (
|
||||
APP_NAME,
|
||||
LEGACY_CONFIG_PATH,
|
||||
LEGACY_COLS_PATH,
|
||||
LEGACY_IPS_PATH,
|
||||
)
|
||||
from dzgui.config.convert import rc2json
|
||||
from dzgui.util._json import read_json, write_json
|
||||
|
||||
logger = logging.getLogger(APP_NAME)
|
||||
|
||||
|
||||
def migrate_legacy_conf(config: Path) -> Any:
|
||||
old_conf = Path.home() / LEGACY_CONFIG_PATH
|
||||
@@ -70,3 +79,15 @@ def copy_ipdb(ips_path: Path) -> None:
|
||||
return
|
||||
# TODO: ensure that month file is copied
|
||||
shutil.copy(legacy, ips_path)
|
||||
|
||||
|
||||
def clean_legacy_versions_file(versions: Path) -> None:
|
||||
# NOTE: handling for #523
|
||||
try:
|
||||
pat = r"^(\d+),(\d+)$"
|
||||
lines = versions.read_text().splitlines()
|
||||
clean = [line for line in lines if re.match(pat, line)]
|
||||
concat = "\n".join(clean)
|
||||
versions.write_text(concat)
|
||||
except Exception as e:
|
||||
logger.warning(e)
|
||||
+7
-4
@@ -30,16 +30,19 @@ def is_steam_running(cmd: str) -> bool:
|
||||
def is_sandboxed() -> bool:
|
||||
return bool(os.environ.get("FLATPAK_ID"))
|
||||
|
||||
def get_flatpak_output() -> list[str]:
|
||||
proc = subprocess.check_output(
|
||||
[FLATPAK_CMD, "ps", "--columns=application"], text=True
|
||||
)
|
||||
lines = proc.splitlines()
|
||||
return lines
|
||||
|
||||
def is_flatpak_steam_running() -> bool:
|
||||
if is_sandboxed():
|
||||
return True
|
||||
if has_cmd(FLATPAK_CMD) is False:
|
||||
return False
|
||||
proc = subprocess.check_output(
|
||||
[FLATPAK_CMD, "ps", "--columns=application"], text=True
|
||||
)
|
||||
lines = proc.splitlines()
|
||||
lines = get_flatpak_output()
|
||||
if FLATPAK_APPID in lines:
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -46,6 +46,15 @@ class ConfigManager:
|
||||
ips.append(record)
|
||||
self.update_config(Preferences.IP_LIST, ips)
|
||||
|
||||
def remove_from_history_file(self, fqip: str) -> None:
|
||||
try:
|
||||
history = self.prefs.paths.history
|
||||
ips = [line.rstrip() for line in history.read_text().splitlines()]
|
||||
ips.remove(fqip)
|
||||
history.write_text("\n".join(ips))
|
||||
except Exception as e:
|
||||
logger.critical(e)
|
||||
|
||||
def update_history_file(self, fqip: str) -> None:
|
||||
try:
|
||||
with open(self.prefs.paths.history, "r") as f:
|
||||
@@ -124,7 +133,7 @@ class ConfigManager:
|
||||
return False
|
||||
|
||||
def enum_to_key(self, enum: Preferences) -> str:
|
||||
return str(enum.dict["key"])
|
||||
return str(enum.key)
|
||||
|
||||
def toggle_config(self, key: Preferences) -> None:
|
||||
try:
|
||||
@@ -162,7 +171,7 @@ class ConfigManager:
|
||||
trace = traceback.format_exc()
|
||||
dialog = ExceptionDialog(self.controller, trace)
|
||||
dialog.show_all()
|
||||
dialog.run()
|
||||
dialog.run() # type: ignore[no-untyped-call]
|
||||
|
||||
def save_res_and_quit(self, tv: "ServerTreeView", window: "OuterWindow") -> None:
|
||||
columns = tv.get_columns()
|
||||
|
||||
@@ -291,22 +291,22 @@ class ConnectionManager:
|
||||
|
||||
def _present_modlist_dialog(self, mods: list[list[str]]) -> None:
|
||||
dialog = ServerModDialog(self.controller, mods)
|
||||
dialog.run()
|
||||
dialog.run() # type: ignore[no-untyped-call]
|
||||
|
||||
def _present_details_dialog(self, details: Servers.Details) -> None:
|
||||
dialog = ServerDetailsDialog(self.controller, details)
|
||||
dialog.run()
|
||||
dialog.run() # type: ignore[no-untyped-call]
|
||||
|
||||
def _workshop_timeout(self, msg: str) -> None:
|
||||
dialog = ExceptionDialog(self.controller, msg)
|
||||
dialog.show_all()
|
||||
dialog.run()
|
||||
dialog.run() # type: ignore[no-untyped-call]
|
||||
|
||||
def _server_timeout(self) -> None:
|
||||
dialog = ExceptionDialog(self.controller, server_timeout)
|
||||
dialog.set_details_buffer(kb.DZG_006)
|
||||
dialog.show_all()
|
||||
dialog.run()
|
||||
dialog.run() # type: ignore[no-untyped-call]
|
||||
|
||||
def get_debug_args(self) -> str:
|
||||
addr = f"{self.record.ip}:{self.record.gameport}"
|
||||
@@ -330,7 +330,6 @@ class ConnectionManager:
|
||||
|
||||
self.thread_man.update_dialog(waiting_for_launch)
|
||||
while True:
|
||||
# FIXME: cancel should not be visible here per setting above
|
||||
if self.controller.is_cancel_pending():
|
||||
# TODO: some facility to also close spawned steam process
|
||||
return
|
||||
|
||||
@@ -2,7 +2,7 @@ import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from dzgui.const.constants import APP_NAME
|
||||
from dzgui.const.enum import ContextMenu, Preferences
|
||||
from dzgui.const.enum import ContextMenuItem, Preferences
|
||||
from dzgui.util.clip import copy_clipboard
|
||||
from dzgui.util.open_links import open_workshop_page
|
||||
|
||||
@@ -34,7 +34,7 @@ class ContextMenuManager:
|
||||
self.controller = controller
|
||||
self.treeview = treeview
|
||||
|
||||
def process(self, action: ContextMenu) -> None:
|
||||
def process(self, action: ContextMenuItem) -> None:
|
||||
if isinstance(self.treeview, ServerTreeView):
|
||||
record = self.treeview.get_record()
|
||||
if record is None:
|
||||
@@ -42,14 +42,14 @@ class ContextMenuManager:
|
||||
|
||||
match action:
|
||||
# UNTHREADED
|
||||
case ContextMenu.ADD_NOTE:
|
||||
case ContextMenuItem.ADD_NOTE:
|
||||
dialog = NoteDialog(self.controller)
|
||||
dialog.run()
|
||||
case ContextMenu.COPY_SERVER_IP:
|
||||
dialog.run() # type: ignore[no-untyped-call]
|
||||
case ContextMenuItem.COPY_SERVER_IP:
|
||||
self.copy_server_ip()
|
||||
case ContextMenu.COPY_SERVER_NAME:
|
||||
case ContextMenuItem.COPY_SERVER_NAME:
|
||||
self.copy_server_name()
|
||||
case ContextMenu.SET_FAV:
|
||||
case ContextMenuItem.SET_FAV:
|
||||
name = self.treeview.get_value_at_index(0)
|
||||
# NOTE: fully qualified ip
|
||||
fqip = self.treeview.get_record_string()
|
||||
@@ -58,40 +58,40 @@ class ContextMenuManager:
|
||||
self.controller.set_fav(name, fqip, simple)
|
||||
|
||||
# THREADED
|
||||
case ContextMenu.ADD_SERVER:
|
||||
case ContextMenuItem.ADD_SERVER:
|
||||
self.controller.add_by_record(record)
|
||||
case ContextMenu.CONNECT:
|
||||
case ContextMenuItem.CONNECT:
|
||||
self.controller.connect_by_record(record)
|
||||
case ContextMenu.REFRESH_PLAYERS:
|
||||
case ContextMenuItem.REFRESH_PLAYERS:
|
||||
self.controller.refresh_players(record)
|
||||
case ContextMenu.REMOVE_HISTORY:
|
||||
case ContextMenuItem.REMOVE_HISTORY:
|
||||
self.controller.remove_from_history(record)
|
||||
case ContextMenu.REMOVE_SERVER:
|
||||
case ContextMenuItem.REMOVE_SERVER:
|
||||
self.controller.remove_by_record(record)
|
||||
case ContextMenu.SHOW_DETAILS:
|
||||
case ContextMenuItem.SHOW_DETAILS:
|
||||
self.controller.get_details(record)
|
||||
case ContextMenu.SHOW_MODS:
|
||||
case ContextMenuItem.SHOW_MODS:
|
||||
self.controller.get_modlist(record)
|
||||
|
||||
if isinstance(self.treeview, (ModTreeView, OfflineModTreeView)):
|
||||
match action:
|
||||
case ContextMenu.OPEN_WORKSHOP:
|
||||
case ContextMenuItem.OPEN_WORKSHOP:
|
||||
self.open_mod_page()
|
||||
case ContextMenu.UNSUB_MOD:
|
||||
case ContextMenuItem.UNSUB_MOD:
|
||||
self.controller.unsub_mods(self.treeview)
|
||||
case ContextMenu.VALIDATE_MOD:
|
||||
case ContextMenuItem.VALIDATE_MOD:
|
||||
self.controller.validate_mods(self.treeview)
|
||||
case _:
|
||||
pass
|
||||
|
||||
if isinstance(self.treeview, LogTreeView):
|
||||
match action:
|
||||
case ContextMenu.COPY_LOG_CLIPBOARD:
|
||||
case ContextMenuItem.COPY_LOG_CLIPBOARD:
|
||||
self.copy_log()
|
||||
|
||||
if isinstance(self.treeview, ServerModTreeView):
|
||||
match action:
|
||||
case ContextMenu.OPEN_WORKSHOP:
|
||||
case ContextMenuItem.OPEN_WORKSHOP:
|
||||
self.open_mod_page()
|
||||
|
||||
def copy_server_ip(self) -> None:
|
||||
|
||||
@@ -51,7 +51,6 @@ class FilterManager:
|
||||
self.active_map = (0, all_maps)
|
||||
self.prior_map = all_maps
|
||||
|
||||
self.filters: list
|
||||
self.enabled_filters = dict(self.default_filters)
|
||||
|
||||
def get_tooltips(self) -> dict[str, str]:
|
||||
@@ -75,11 +74,11 @@ class FilterManager:
|
||||
def set_active_keyword(self, word: str) -> None:
|
||||
self.active_keyword = word
|
||||
|
||||
def get_default_filters(self) -> dict:
|
||||
def get_default_filters(self) -> dict[str, bool]:
|
||||
"""Deep copy of defaults"""
|
||||
return dict(self.default_filters)
|
||||
|
||||
def get_filters(self) -> dict:
|
||||
def get_filters(self) -> dict[str, bool]:
|
||||
return self.enabled_filters
|
||||
|
||||
def set_filter(self, label: str, state: bool) -> None:
|
||||
@@ -110,7 +109,7 @@ class FilterManager:
|
||||
for m in clean:
|
||||
self.append_map([m])
|
||||
|
||||
def get_all_filters(self) -> tuple:
|
||||
def get_all_filters(self) -> tuple[str, ...]:
|
||||
map_name = self.get_active_map_name()
|
||||
enabled = self.get_filters()
|
||||
kw = self.get_active_keyword()
|
||||
|
||||
@@ -221,7 +221,7 @@ class ModManager:
|
||||
total_size += mod[3]
|
||||
return total_mods, total_size
|
||||
|
||||
def _on_stale_mods_found(self, stale_mods: list) -> None:
|
||||
def _on_stale_mods_found(self, stale_mods: list[int]) -> None:
|
||||
"""Manipulates attached ListStore in the main event loop"""
|
||||
model = self.treeview.get_model()
|
||||
if model is None:
|
||||
|
||||
@@ -52,7 +52,7 @@ class OfflineManager:
|
||||
is_valid = is_mission(folder)
|
||||
self.emitter.emit("custom_mission_loaded", str(folder), is_valid)
|
||||
|
||||
def find_custom_mods(self, callback: Callable) -> None:
|
||||
def find_custom_mods(self, callback: Callable[[], None]) -> None:
|
||||
folder = self.open_folderpicker(dialogs.custom_mod_dialog)
|
||||
window = self.controller.get_window()
|
||||
window.set_sensitive(False)
|
||||
|
||||
@@ -22,8 +22,8 @@ logger = logging.getLogger(APP_NAME)
|
||||
|
||||
def call_on_thread(
|
||||
dialog_str: str, show_dialog: bool = True, show_cancel: bool = False
|
||||
) -> Callable:
|
||||
def decorator(func: Callable) -> Callable:
|
||||
) -> Callable[..., Any]:
|
||||
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
||||
@wraps(func)
|
||||
def wrapper(*args: Any, **kwargs: Any) -> None:
|
||||
self = args[0]
|
||||
@@ -42,7 +42,7 @@ def call_on_thread(
|
||||
|
||||
|
||||
class StoredFunc:
|
||||
def __init__(self, func: Callable, *args: Any, **kwargs: Any) -> None:
|
||||
def __init__(self, func: Callable[..., Any], *args: Any, **kwargs: Any) -> None:
|
||||
sig = inspect.signature(func)
|
||||
self.func = func
|
||||
self.bindings = sig.bind(*args, **kwargs)
|
||||
|
||||
@@ -55,9 +55,9 @@ class UpdateManager:
|
||||
def _on_update_success(self) -> None:
|
||||
msg = dialogs.update_success
|
||||
dialog = QuitDialog(self.controller, msg)
|
||||
dialog.run()
|
||||
dialog.run() # type: ignore[no-untyped-call]
|
||||
|
||||
def _on_update_failure(self, msg: str) -> None:
|
||||
dialog = ExceptionDialog(self.controller, msg)
|
||||
dialog.show_all()
|
||||
dialog.run()
|
||||
dialog.run() # type: ignore[no-untyped-call]
|
||||
@@ -11,7 +11,7 @@ from gi.repository.Gtk import ListStore, TreeIter
|
||||
from gi.repository import GObject
|
||||
|
||||
GTYPE_TO_PYTHON = {
|
||||
GObject.type_from_name(GObject.type_name(ptype)): ptype
|
||||
GObject.type_from_name(GObject.type_name(ptype)): ptype # type: ignore[no-untyped-call]
|
||||
for ptype in (int, float, str, bool, object)
|
||||
}
|
||||
|
||||
@@ -147,7 +147,7 @@ class ModelFactory:
|
||||
|
||||
def make_help_store(self) -> FastInsertListStore:
|
||||
store = self.new_model_from_class(MenuCols)
|
||||
rows = [[row.dict["label"], row] for row in HELP_MENU_ROWS]
|
||||
rows = [[row.label, row] for row in HELP_MENU_ROWS]
|
||||
store.extend(rows)
|
||||
return store
|
||||
|
||||
|
||||
+170
-79
@@ -1,6 +1,8 @@
|
||||
import re
|
||||
|
||||
from typing import Any, TYPE_CHECKING, Union
|
||||
from dataclasses import dataclass, fields
|
||||
from operator import attrgetter
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from dzgui.const.enum import FilterMode
|
||||
from dzgui.model.model_factory import ModelFactory
|
||||
@@ -17,6 +19,121 @@ DAY_REG = r"([0][6-9]|[1][0-7])"
|
||||
NIGHT_REG = r"([0][0-5]|[1][8-9]|[2][0-3])"
|
||||
|
||||
|
||||
@dataclass(slots=True, frozen=True)
|
||||
class ServerRow:
|
||||
name: str
|
||||
mapname: str
|
||||
view: str
|
||||
time: str
|
||||
players: int
|
||||
max_players: int
|
||||
queue: int
|
||||
ip: str
|
||||
qport: int
|
||||
ping: str
|
||||
provider: str
|
||||
modded: bool
|
||||
|
||||
# NOTE: construct getter once for fast lookup in lieu of field introspection
|
||||
_to_tuple = attrgetter(
|
||||
"name",
|
||||
"mapname",
|
||||
"view",
|
||||
"time",
|
||||
"players",
|
||||
"max_players",
|
||||
"queue",
|
||||
"ip",
|
||||
"qport",
|
||||
"ping",
|
||||
"provider",
|
||||
"modded",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def marshal_rows(
|
||||
cls, rows: list["ServerRow"]
|
||||
) -> list[tuple[str, str, str, str, int, int, int, str, int, str, str, bool]]:
|
||||
"""Marshals data before passing into FastInsertListStore's extend() method"""
|
||||
getter = cls._to_tuple
|
||||
return [getter(row) for row in rows]
|
||||
|
||||
@classmethod
|
||||
def get_index(cls, field: str) -> int:
|
||||
_fields = [f.name for f in fields(cls)]
|
||||
return _fields.index(field)
|
||||
|
||||
|
||||
class FiltrationManager:
|
||||
def __init__(self) -> None:
|
||||
|
||||
self.filter_map = {
|
||||
strings.filter_3pp: self.filter_3pp,
|
||||
strings.filter_1pp: self.filter_1pp,
|
||||
strings.filter_official: self.filter_official,
|
||||
strings.filter_unofficial: self.filter_unofficial,
|
||||
strings.filter_empty: self.filter_empty,
|
||||
strings.filter_full: self.filter_full,
|
||||
strings.filter_duplicate: self.filter_duplicate,
|
||||
strings.filter_day: self.filter_day,
|
||||
strings.filter_night: self.filter_night,
|
||||
strings.filter_nonascii: self.filter_nonascii,
|
||||
strings.filter_lowpop: self.filter_lowpop,
|
||||
strings.filter_modded: self.filter_modded,
|
||||
strings.filter_unmodded: self.filter_unmodded,
|
||||
}
|
||||
|
||||
def filter_3pp(self, rows: list[ServerRow]) -> list[ServerRow]:
|
||||
return [row for row in rows if row.view != strings.filter_3pp]
|
||||
|
||||
def filter_1pp(self, rows: list[ServerRow]) -> list[ServerRow]:
|
||||
return [row for row in rows if row.view != strings.filter_1pp]
|
||||
|
||||
def filter_official(self, rows: list[ServerRow]) -> list[ServerRow]:
|
||||
return [row for row in rows if row.provider != strings.filter_official]
|
||||
|
||||
def filter_unofficial(self, rows: list[ServerRow]) -> list[ServerRow]:
|
||||
return [row for row in rows if row.provider != strings.filter_unofficial]
|
||||
|
||||
def filter_empty(self, rows: list[ServerRow]) -> list[ServerRow]:
|
||||
return [row for row in rows if row.players != 0]
|
||||
|
||||
def filter_full(self, rows: list[ServerRow]) -> list[ServerRow]:
|
||||
return [row for row in rows if row.players != row.max_players]
|
||||
|
||||
def filter_duplicate(self, rows: list[ServerRow]) -> list[ServerRow]:
|
||||
seen = set()
|
||||
final = []
|
||||
for row in rows:
|
||||
if row.name not in seen:
|
||||
seen.add(row.name)
|
||||
final.append(row)
|
||||
rows = final
|
||||
return rows
|
||||
|
||||
def filter_day(self, rows: list[ServerRow]) -> list[ServerRow]:
|
||||
return [row for row in rows if not re.match(DAY_REG, row.time)]
|
||||
|
||||
def filter_night(self, rows: list[ServerRow]) -> list[ServerRow]:
|
||||
return [row for row in rows if not re.match(NIGHT_REG, row.time)]
|
||||
|
||||
def filter_nonascii(self, rows: list[ServerRow]) -> list[ServerRow]:
|
||||
return [row for row in rows if row.name.isascii()]
|
||||
|
||||
def filter_lowpop(self, rows: list[ServerRow]) -> list[ServerRow]:
|
||||
return [row for row in rows if (row.players / row.max_players * 100) > 30]
|
||||
|
||||
def filter_modded(self, rows: list[ServerRow]) -> list[ServerRow]:
|
||||
return [row for row in rows if not row.modded]
|
||||
|
||||
def filter_unmodded(self, rows: list[ServerRow]) -> list[ServerRow]:
|
||||
return [row for row in rows if row.modded]
|
||||
|
||||
def filter(self, filter_type: str, rows: list[ServerRow]) -> list[ServerRow]:
|
||||
func = self.filter_map[filter_type]
|
||||
return func(rows)
|
||||
|
||||
|
||||
class ProxyModelManager:
|
||||
"""
|
||||
Manages access to cached FastInsertListStore resources and
|
||||
@@ -31,20 +148,16 @@ class ProxyModelManager:
|
||||
|
||||
def __init__(self, filter_man: "FilterManager") -> None:
|
||||
self.filter_cache: dict[
|
||||
tuple[str],
|
||||
list[tuple[str, str, str, str, int, int, int, str, int, int, str, bool]],
|
||||
tuple[str, ...],
|
||||
list[ServerRow],
|
||||
] = {}
|
||||
|
||||
self.proxy_model: "FastInsertListStore"
|
||||
self.filter_man = filter_man
|
||||
self.filtration_manager = FiltrationManager()
|
||||
|
||||
self.control_model: (
|
||||
list[tuple[str, str, str, str, int, int, int, str, int, int, str, bool]]
|
||||
| None
|
||||
) = None
|
||||
self.filtered: list[
|
||||
tuple[str, str, str, str, int, int, int, str, int, int, str, bool]
|
||||
] = []
|
||||
self.control_model: list[ServerRow] | None = None
|
||||
self.filtered: list[ServerRow] = []
|
||||
self.success = True
|
||||
|
||||
def has_control_model(self) -> bool:
|
||||
@@ -52,12 +165,7 @@ class ProxyModelManager:
|
||||
return False
|
||||
return True
|
||||
|
||||
def append_row(self, row: list) -> None:
|
||||
self.proxy_model.append(row)
|
||||
|
||||
def append_row_to_control(
|
||||
self, row: tuple[str, str, str, str, int, int, int, str, int, int, str, bool]
|
||||
) -> None:
|
||||
def append_row_to_control(self, row: ServerRow) -> None:
|
||||
if self.control_model is None:
|
||||
raise AttributeError("Trying to add rows to a non-existent model")
|
||||
self.control_model.append(row)
|
||||
@@ -65,22 +173,24 @@ class ProxyModelManager:
|
||||
|
||||
def update_playercount(self, playercount: "NewPlayerCount") -> None:
|
||||
treeiter = playercount.treeiter
|
||||
self.proxy_model[treeiter][4] = playercount.players
|
||||
self.proxy_model[treeiter][6] = playercount.queue
|
||||
field = [f.name for f in fields(ServerRow)]
|
||||
index_players = field.index("players")
|
||||
index_queue = field.index("queue")
|
||||
self.proxy_model[treeiter][index_players] = playercount.players
|
||||
self.proxy_model[treeiter][index_queue] = playercount.queue
|
||||
|
||||
# TODO: use dataclass for record rows
|
||||
def append_row_to_history(
|
||||
self,
|
||||
history: tuple[str, str, str, str, int, int, int, str, int, int, str, bool],
|
||||
history: ServerRow,
|
||||
) -> None:
|
||||
addr = history[7]
|
||||
qport = history[8]
|
||||
addr = history.ip
|
||||
qport = history.qport
|
||||
|
||||
if self.control_model is None:
|
||||
raise AttributeError("Trying to append row to empty model")
|
||||
found = False
|
||||
for i, row in enumerate(self.control_model):
|
||||
if addr == row[7] and qport == row[8]:
|
||||
if addr == row.ip and qport == row.qport:
|
||||
item = self.control_model.pop(i)
|
||||
self.control_model.append(item)
|
||||
found = True
|
||||
@@ -89,6 +199,7 @@ class ProxyModelManager:
|
||||
if found is False:
|
||||
self.control_model.append(history)
|
||||
|
||||
# NOTE: truncate history to 10 records max
|
||||
if len(self.control_model) == 11:
|
||||
del self.control_model[0]
|
||||
|
||||
@@ -100,7 +211,7 @@ class ProxyModelManager:
|
||||
addr = f"{record.ip}:{record.gameport}"
|
||||
qport = record.qport
|
||||
for row in self.control_model:
|
||||
if addr == row[7] and qport == row[8]:
|
||||
if addr == row.ip and qport == row.qport:
|
||||
self.control_model.remove(row)
|
||||
break
|
||||
self.wipe_cache()
|
||||
@@ -109,19 +220,22 @@ class ProxyModelManager:
|
||||
def clear_proxy_model(self) -> None:
|
||||
self.proxy_model.clear()
|
||||
|
||||
def get_proxy_model(self) -> Union["FastInsertListStore", None]:
|
||||
def get_proxy_model(self) -> "FastInsertListStore | None":
|
||||
if hasattr(self, "proxy_model"):
|
||||
return self.proxy_model
|
||||
return None
|
||||
|
||||
def filter(
|
||||
self, mode: FilterMode, skip_cache: bool = False
|
||||
) -> Union["FastInsertListStore", None]:
|
||||
) -> "FastInsertListStore | None":
|
||||
"""
|
||||
Native Gtk.TreeView.refilter() method was not performant enough
|
||||
when running in the main loop with 40k+ records
|
||||
|
||||
skip_cache: used when updating a record on Saved Servers/History and forcing a refilter
|
||||
|
||||
NOTE: data must be marshalled from ServerRow format before constructing list stores
|
||||
The "clone" is the proxy model (list of tuples) actually fed into Gtk.TreeView
|
||||
"""
|
||||
# TODO: filter cache: return a dataclass object with clearly enumerated map, keyword, and filter values
|
||||
# instead of just a serial list of strings
|
||||
@@ -130,8 +244,9 @@ class ProxyModelManager:
|
||||
if skip_cache is False:
|
||||
if filters in self.filter_cache:
|
||||
cached_rows = self.filter_cache[filters]
|
||||
marshalled_rows = ServerRow.marshal_rows(cached_rows)
|
||||
clone = ModelFactory().make_server_store()
|
||||
clone.extend(cached_rows)
|
||||
clone.extend(marshalled_rows)
|
||||
self.set_proxy_model(clone)
|
||||
self.set_filtered(cached_rows)
|
||||
return None
|
||||
@@ -150,17 +265,18 @@ class ProxyModelManager:
|
||||
clone = ModelFactory().make_server_store()
|
||||
if len(rows) > 0:
|
||||
rows = self.sort_rows(rows)
|
||||
clone.extend(rows)
|
||||
marshalled_rows = ServerRow.marshal_rows(rows)
|
||||
clone.extend(marshalled_rows)
|
||||
|
||||
self.set_cache(filters, rows)
|
||||
self.set_proxy_model(clone)
|
||||
return clone
|
||||
|
||||
def sort_rows(self, rows: list) -> list:
|
||||
rows.sort(key=lambda x: re.sub(r"[^A-Za-z0-9]+", "", x[0].lower()))
|
||||
def sort_rows(self, rows: list[ServerRow]) -> list[ServerRow]:
|
||||
rows.sort(key=lambda x: re.sub(r"[^A-Za-z0-9]+", "", x.name.lower()))
|
||||
return rows
|
||||
|
||||
def filter_initial(self, filters: tuple) -> list:
|
||||
def filter_initial(self, filters: tuple[str, ...]) -> list[ServerRow]:
|
||||
"""
|
||||
Simply culls the control model of any disabled filters
|
||||
"""
|
||||
@@ -169,7 +285,7 @@ class ProxyModelManager:
|
||||
self.set_filtered(self.filter_toggle_off(filters, f))
|
||||
return self.filtered
|
||||
|
||||
def filter_map(self, filters: tuple) -> list:
|
||||
def filter_map(self, filters: tuple[str, ...]) -> list[ServerRow]:
|
||||
"""
|
||||
Multi-filtration for any context starts by narrowing by map
|
||||
"""
|
||||
@@ -179,10 +295,10 @@ class ProxyModelManager:
|
||||
if sel_map == str_filters.all_maps:
|
||||
return rows
|
||||
|
||||
rows = [row for row in rows if row[1] == sel_map]
|
||||
rows = [row for row in rows if row.mapname == sel_map]
|
||||
return rows
|
||||
|
||||
def filter_keyword(self, filters: tuple) -> list:
|
||||
def filter_keyword(self, filters: tuple[str, ...]) -> list[ServerRow]:
|
||||
keyword = self.filter_man.get_active_keyword()
|
||||
rows = self.filtered
|
||||
|
||||
@@ -192,13 +308,15 @@ class ProxyModelManager:
|
||||
filtered = [
|
||||
row
|
||||
for row in rows
|
||||
if keyword in row[0].lower()
|
||||
or keyword in row[1].lower()
|
||||
or keyword in row[7].lower()
|
||||
if keyword in row.name.lower()
|
||||
or keyword in row.mapname.lower()
|
||||
or keyword in row.ip.lower()
|
||||
]
|
||||
return filtered
|
||||
|
||||
def filter_toggle_off(self, filters: tuple, filter_type: str) -> list:
|
||||
def filter_toggle_off(
|
||||
self, filters: tuple[str, ...], filter_type: str
|
||||
) -> list[ServerRow]:
|
||||
"""
|
||||
Sub-filtration of the current model
|
||||
"""
|
||||
@@ -212,43 +330,11 @@ class ProxyModelManager:
|
||||
self.set_filtered(None)
|
||||
return []
|
||||
|
||||
rows = self.filtered
|
||||
match filter_type:
|
||||
case strings.filter_3pp:
|
||||
rows = [row for row in rows if row[2] != strings.filter_3pp]
|
||||
case strings.filter_1pp:
|
||||
rows = [row for row in rows if row[2] != strings.filter_1pp]
|
||||
case strings.filter_official:
|
||||
rows = [row for row in rows if row[10] != strings.filter_official]
|
||||
case strings.filter_unofficial:
|
||||
rows = [row for row in rows if row[10] != strings.filter_unofficial]
|
||||
case strings.filter_empty:
|
||||
rows = [row for row in rows if row[4] != 0]
|
||||
case strings.filter_full:
|
||||
rows = [row for row in rows if row[4] != row[5]]
|
||||
case strings.filter_duplicate:
|
||||
seen = set()
|
||||
final = []
|
||||
for row in rows:
|
||||
if row[0] not in seen:
|
||||
seen.add(row[0])
|
||||
final.append(row)
|
||||
rows = final
|
||||
case strings.filter_day:
|
||||
rows = [row for row in rows if not re.match(DAY_REG, row[3])]
|
||||
case strings.filter_night:
|
||||
rows = [row for row in rows if not re.match(NIGHT_REG, row[3])]
|
||||
case strings.filter_nonascii:
|
||||
rows = [row for row in rows if row[0].isascii()]
|
||||
case strings.filter_lowpop:
|
||||
rows = [row for row in rows if (row[4] / row[5] * 100) > 30]
|
||||
case strings.filter_modded:
|
||||
rows = [row for row in rows if not row[11]]
|
||||
case strings.filter_unmodded:
|
||||
rows = [row for row in rows if row[11]]
|
||||
# TODO: pack filter type enum in filter checkbox, rather than stringwise comparison
|
||||
rows = self.filtration_manager.filter(filter_type, self.filtered)
|
||||
return rows
|
||||
|
||||
def filter_toggle_on(self, filters: tuple[str]) -> list:
|
||||
def filter_toggle_on(self, filters: tuple[str, ...]) -> list[ServerRow]:
|
||||
"""Effectively applies all filters"""
|
||||
self.set_filtered(self.control_model)
|
||||
self.set_filtered(self.filter_map(filters))
|
||||
@@ -259,31 +345,36 @@ class ProxyModelManager:
|
||||
self.set_filtered(self.filter_toggle_off(filters, f))
|
||||
return self.filtered
|
||||
|
||||
def set_cache(self, filters: tuple[str], filtered_rows: list[tuple]) -> None:
|
||||
def set_cache(
|
||||
self, filters: tuple[str, ...], filtered_rows: list[ServerRow]
|
||||
) -> None:
|
||||
self.filter_cache[filters] = filtered_rows
|
||||
|
||||
def set_filtered(self, rows: list | None) -> None:
|
||||
def set_filtered(self, rows: list[ServerRow] | None) -> None:
|
||||
if rows is None:
|
||||
self.filtered = []
|
||||
else:
|
||||
self.filtered = rows
|
||||
|
||||
def get_filtered(self) -> list:
|
||||
def get_filtered(self) -> list[ServerRow]:
|
||||
return self.filtered
|
||||
|
||||
def set_proxy_model(self, model: "FastInsertListStore") -> None:
|
||||
"""
|
||||
FastInsertListStore representation of the raw model after filtration
|
||||
|
||||
This attribute is saved from within a thread so that it later be retrieved
|
||||
and safely packed into the TreeView in the main loop, cf. get_proxy_model()
|
||||
"""
|
||||
self.proxy_model = model
|
||||
|
||||
def set_control(self, rows: list) -> None:
|
||||
def set_control(self, rows: list[ServerRow]) -> None:
|
||||
"""
|
||||
Raw data model prior to filtration
|
||||
"""
|
||||
self.control_model = rows
|
||||
|
||||
def get_control(self) -> list[Any]:
|
||||
def get_control(self) -> list[ServerRow]:
|
||||
if self.control_model is None:
|
||||
raise AttributeError(
|
||||
"Expected a populated control model, but it is Nonetype"
|
||||
@@ -294,7 +385,7 @@ class ProxyModelManager:
|
||||
self.filtered = []
|
||||
self.filter_cache = {}
|
||||
|
||||
def push(self, data: list[Any]) -> None:
|
||||
def push(self, data: list[ServerRow]) -> None:
|
||||
self.wipe_cache()
|
||||
self.set_control(data)
|
||||
cur_keyword = self.filter_man.get_active_keyword()
|
||||
|
||||
+14
-15
@@ -26,7 +26,7 @@ from gi.repository import Gtk, GLib
|
||||
if TYPE_CHECKING:
|
||||
from dzgui.api.servers import A2SInfo, Record
|
||||
from dzgui.controllers.mc import Controller
|
||||
from dzgui.model.proxy_model import ProxyModelManager
|
||||
from dzgui.model.proxy_model import ProxyModelManager, ServerRow
|
||||
from dzgui.views.trees.tree_servers import ServerTreeView
|
||||
|
||||
logger = logging.getLogger(APP_NAME)
|
||||
@@ -34,7 +34,7 @@ LAN_TIMEOUT = 0.5
|
||||
API_TIMEOUT = 3
|
||||
|
||||
|
||||
@dataclass
|
||||
@dataclass(slots=True, frozen=True)
|
||||
class NewPlayerCount:
|
||||
treeiter: Gtk.TreeIter
|
||||
players: int
|
||||
@@ -241,10 +241,7 @@ class ServerModelManager:
|
||||
|
||||
def update_history(
|
||||
self,
|
||||
rows: (
|
||||
list[tuple[str, str, str, str, int, int, int, str, int, int, str, bool]]
|
||||
| None
|
||||
) = None,
|
||||
rows: list["ServerRow"] | None = None,
|
||||
) -> None:
|
||||
proxy_man = self._get_proxy_man()
|
||||
if rows is not None:
|
||||
@@ -267,7 +264,6 @@ class ServerModelManager:
|
||||
self.first_iteration = False
|
||||
self.emitter.emit("load_maps", store)
|
||||
|
||||
# TODO: dataclass for record rows; check for other dict annotations
|
||||
def add_to_history(self, data: tuple[dict[str, Any], "Record"]) -> None:
|
||||
row, record = data
|
||||
proxy_man = self._get_proxy_man()
|
||||
@@ -285,6 +281,10 @@ class ServerModelManager:
|
||||
def remove_from_history(self, record: Servers.Record) -> None:
|
||||
proxy_man = self._get_proxy_man()
|
||||
proxy_man.remove_row_from_control(record)
|
||||
|
||||
fqip = Servers.record_to_fqip(record)
|
||||
config_man = self.controller.get_config_man()
|
||||
config_man.remove_from_history_file(fqip)
|
||||
self.update_history()
|
||||
|
||||
@call_on_thread(dialog.querying)
|
||||
@@ -341,8 +341,8 @@ class ServerModelManager:
|
||||
return
|
||||
config_man.add_saved_server(fqip)
|
||||
|
||||
# FIXME: this is valid if saved servers tab is already open,
|
||||
# but not if app was just booted; causes single row to appear prematurely
|
||||
# TODO: this is valid if saved servers tab is already open,
|
||||
# but not if tab is unloaded; causes single row to appear prematurely
|
||||
if proxy_man.has_control_model() is False:
|
||||
proxy_man.push(records)
|
||||
else:
|
||||
@@ -432,7 +432,7 @@ class ServerModelManager:
|
||||
if show_dialog:
|
||||
dialog = ExceptionDialog(self.controller, dialogs.load_error_lan)
|
||||
dialog.show_all()
|
||||
dialog.run()
|
||||
dialog.run() # type: ignore[no-untyped-call]
|
||||
|
||||
def _cleanup_on_failure(self, show_dialog: bool = True) -> None:
|
||||
# TODO: disable map, keyword, and filter widgets if model is None
|
||||
@@ -451,15 +451,15 @@ class ServerModelManager:
|
||||
dialog = ExceptionDialog(self.controller, server_timeout)
|
||||
dialog.set_details_buffer(kb.DZG_006)
|
||||
dialog.show_all()
|
||||
dialog.run()
|
||||
dialog.run() # type: ignore[no-untyped-call]
|
||||
|
||||
def _push_data(self, data: list[Any]) -> None:
|
||||
def _push_data(self, data: list["ServerRow"]) -> None:
|
||||
self._get_proxy_man().push(data)
|
||||
self._sort_unique_maps(data)
|
||||
self.thread_man.set_cleanup_func(StoredFunc(self._cleanup_on_success))
|
||||
|
||||
def _sort_unique_maps(self, data: list) -> None:
|
||||
u_maps = set([row[1] for row in data])
|
||||
def _sort_unique_maps(self, data: list[Any]) -> None:
|
||||
u_maps = set([row.mapname for row in data])
|
||||
self._set_new_maps(sorted(u_maps))
|
||||
|
||||
def _set_new_maps(self, maps: list[str]) -> None:
|
||||
@@ -473,7 +473,6 @@ class ServerModelManager:
|
||||
|
||||
@call_on_thread(dialog.filtering)
|
||||
def refilter(self, mode: FilterMode) -> None:
|
||||
# FIXME: causes two wait dialogs when map selection change signal emits after loading servers
|
||||
self.first_iteration = False
|
||||
proxy_man = self._get_proxy_man()
|
||||
proxy_man.filter(mode)
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
changelog_label = "View changelog"
|
||||
changelog_status = "Opens the DZGUI changelog"
|
||||
|
||||
log_label = "Show debug log"
|
||||
log_status = "Read the DZGUI log generated since startup"
|
||||
|
||||
separator_label = "SEPARATOR"
|
||||
separator_status = ""
|
||||
|
||||
system_log_label = "Output system info to log file"
|
||||
system_log_status = "Dump diagnostic data for troubleshooting"
|
||||
|
||||
docs_label = "Documentation/help files (GitHub) ⧉"
|
||||
docs_status = "Open the DZGUI documentation in a browser"
|
||||
|
||||
docs_fallback_label = "Documentation/help files (Codeberg mirror) ⧉"
|
||||
docs_fallback_status = "Open the DZGUI documentation in a browser"
|
||||
|
||||
bugs_label = "Report a bug (GitHub) ⧉"
|
||||
bugs_status = "Open the DZGUI issue tracker in a browser"
|
||||
|
||||
forum_label = "DZGUI Subreddit ⧉"
|
||||
forum_status = "Open the DZGUI discussion forum in a browser"
|
||||
|
||||
sponsor_label = "Sponsor (GitHub) ⧉"
|
||||
sponsor_status = "Sponsor development of the DZGUI project"
|
||||
|
||||
thanks_label = "Special thanks"
|
||||
thanks_status = "A list of contributors, testers, and sponsors"
|
||||
+6
-11
@@ -11,6 +11,7 @@ from dzgui.const.enum import Preferences
|
||||
from dzgui.config.query import lookup
|
||||
from dzgui.init.prefix import get_version
|
||||
from dzgui.util.redact import redact_home
|
||||
from dzgui.util.strings import unknown
|
||||
|
||||
logger = logging.getLogger(APP_NAME)
|
||||
|
||||
@@ -27,28 +28,22 @@ def get_cpu_model() -> str:
|
||||
|
||||
|
||||
def print_servers(servers: list[str]) -> str:
|
||||
s = ""
|
||||
for server in servers:
|
||||
s += server + "\n"
|
||||
return s
|
||||
return "\n".join(servers)
|
||||
|
||||
|
||||
def print_mods(mods: list[int]) -> str:
|
||||
s = ""
|
||||
for mod in mods:
|
||||
s += str(mod) + "\n"
|
||||
return s
|
||||
return "\n".join(map(str, mods))
|
||||
|
||||
|
||||
def write_diagnostic(config: Path, outfile: Path) -> None:
|
||||
date = datetime.now().isoformat()
|
||||
|
||||
try:
|
||||
# FIXME: ID_LIKE not available on CachyOS
|
||||
# NOTE: ID_LIKE not available on CachyOS
|
||||
distro = platform.freedesktop_os_release()["ID_LIKE"]
|
||||
except Exception as e:
|
||||
logger.warn(e)
|
||||
distro = "Unknown"
|
||||
distro = unknown
|
||||
|
||||
kernel = os.uname().release
|
||||
cpu = get_cpu_model()
|
||||
@@ -68,7 +63,6 @@ def write_diagnostic(config: Path, outfile: Path) -> None:
|
||||
servers = lookup(config, Preferences.IP_LIST)
|
||||
servers_pretty = print_servers(servers)
|
||||
|
||||
# FIXME: extraneous newlines in lists of mods
|
||||
template = f"""\
|
||||
{APP_NAME} version {version}
|
||||
Date: {date}
|
||||
@@ -82,6 +76,7 @@ def write_diagnostic(config: Path, outfile: Path) -> None:
|
||||
|
||||
Mods:
|
||||
{mods_pretty}
|
||||
|
||||
Servers:
|
||||
{servers_pretty}
|
||||
"""
|
||||
|
||||
+2
-2
@@ -54,7 +54,7 @@ class CalcDist:
|
||||
self,
|
||||
addr: str,
|
||||
enum: "ServerTab",
|
||||
result_queue: "Queue",
|
||||
result_queue: "Queue[tuple[str, Haversine | None, ServerTab]]",
|
||||
controller: "Controller",
|
||||
cache: dict[str, Haversine],
|
||||
) -> None:
|
||||
@@ -67,7 +67,7 @@ class CalcDist:
|
||||
self.ip = self.addr
|
||||
|
||||
dist = self.compare(self.ip)
|
||||
self.result_queue.put([self.addr, dist, self.enum])
|
||||
self.result_queue.put((self.addr, dist, self.enum))
|
||||
|
||||
def compare(self, remote_ip: str) -> Haversine | None:
|
||||
prefs = self.controller.get_prefs()
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import logging
|
||||
import subprocess
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from dzgui.const.constants import APP_NAME, FLATPAK_APPID, GTK_APP_ID, TMP_PATH
|
||||
|
||||
logger = logging.getLogger(APP_NAME)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
def apply_overrides(xdg: dict[str, str]) -> None:
|
||||
|
||||
def apply_overrides(xdg: dict[str, "Path"]) -> None:
|
||||
try:
|
||||
# NOTE: D-Bus registration required for sandbox
|
||||
args = [
|
||||
@@ -19,7 +23,7 @@ def apply_overrides(xdg: dict[str, str]) -> None:
|
||||
subprocess.run([*args], capture_output=True)
|
||||
|
||||
paths: list[str] = []
|
||||
paths.extend([*xdg.values()])
|
||||
paths.extend([str(path) for path in xdg.values()])
|
||||
paths.append(TMP_PATH)
|
||||
|
||||
for path in paths:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from dzgui.util.localize import number
|
||||
from dzgui.strings.generic import selectable_tree
|
||||
@@ -60,7 +61,7 @@ def format_server_mods(mods: int) -> str:
|
||||
return f"Found {mods:n} {plural}. {workshop}"
|
||||
|
||||
|
||||
def format_player_count(model: Gtk.TreeModel | None, control: list) -> str:
|
||||
def format_player_count(model: Gtk.TreeModel | None, control: list[Any]) -> str:
|
||||
players = 0
|
||||
hits: int
|
||||
status: str
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Callable
|
||||
|
||||
def immutable_dataclass(cls: type | None = None, **kwargs: bool) -> type | Callable[[type], type]:
|
||||
kwargs["slots"] = True
|
||||
kwargs["frozen"] = True
|
||||
if cls is None:
|
||||
return lambda c: dataclass(c, **kwargs)
|
||||
return dataclass(cls, **kwargs)
|
||||
+3
-4
@@ -1,10 +1,10 @@
|
||||
import a2s
|
||||
from ipaddress import ip_address
|
||||
import requests
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
from dataclasses import dataclass
|
||||
from ipaddress import ip_address
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from dzgui.api.servers import Record
|
||||
@@ -12,12 +12,11 @@ from dzgui.config.ipdb import read_from_table
|
||||
from dzgui.const.constants import REQUEST_TIMEOUT
|
||||
from dzgui.const.endpoints import IP_ECHO
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@dataclass
|
||||
@dataclass(slots=True, frozen=True)
|
||||
class Coords:
|
||||
lat: float
|
||||
lon: float
|
||||
@@ -31,7 +30,7 @@ class ReservedIPError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def ip2list(ip: str) -> list:
|
||||
def ip2list(ip: str) -> list[int]:
|
||||
return [int(digit) for digit in ip.split(".")]
|
||||
|
||||
|
||||
|
||||
@@ -118,6 +118,7 @@ servers = {
|
||||
"0": "toggle filter 10",
|
||||
"Minus": "toggle filter 11",
|
||||
"Backslash": "toggle filter 12",
|
||||
"Apostrophe": "toggle filter 13",
|
||||
}
|
||||
vim = {
|
||||
"j": "Move down a row/scroll up",
|
||||
@@ -249,6 +250,7 @@ class Crumbs:
|
||||
developers: str
|
||||
default: str
|
||||
offline: str
|
||||
connect: str
|
||||
|
||||
|
||||
@dataclass(slots=True, frozen=True)
|
||||
@@ -436,6 +438,7 @@ crumbs = Crumbs(
|
||||
developers="Options > Developers",
|
||||
default="Servers > ",
|
||||
offline="Mods > Play offline",
|
||||
connect="Connect",
|
||||
)
|
||||
|
||||
no_mods = "No local mods found."
|
||||
|
||||
+5
-3
@@ -1,3 +1,4 @@
|
||||
import gi
|
||||
import logging
|
||||
import signal
|
||||
|
||||
@@ -34,8 +35,6 @@ from dzgui.views.pages.preconnect import PreConnectionAssistant
|
||||
from dzgui.views.pages.servers import ServerNotebook
|
||||
from dzgui.views.pages.thanks import Thanks
|
||||
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
gi.require_version("GLibUnix", "2.0")
|
||||
from gi.repository import Gtk, GLib, GLibUnix, Gdk # type: ignore
|
||||
@@ -49,7 +48,10 @@ logger = logging.getLogger(APP_NAME)
|
||||
class OuterWindow(Gtk.ApplicationWindow):
|
||||
def __init__(self, app: "App") -> None:
|
||||
super().__init__(
|
||||
application=app, title=APP_NAME, border_width=10, icon_name=APP_NAME_LOWER
|
||||
application=app,
|
||||
title=APP_NAME,
|
||||
border_width=10,
|
||||
icon_name=APP_NAME_LOWER,
|
||||
)
|
||||
|
||||
self.hb = AppHeaderBar()
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
from typing import Sequence
|
||||
|
||||
import gi
|
||||
|
||||
from typing import Sequence
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk # noqa
|
||||
from gi.repository import Gtk
|
||||
|
||||
|
||||
class GenericBox(Gtk.Box):
|
||||
|
||||
@@ -19,7 +19,11 @@ if TYPE_CHECKING:
|
||||
|
||||
class ContextualButton(Gtk.Button):
|
||||
def __init__(
|
||||
self, label: str, opens: "NotebookPage", tooltip: str, context: ButtonType
|
||||
self,
|
||||
label: str,
|
||||
opens: "NotebookPage | None",
|
||||
tooltip: str,
|
||||
context: ButtonType,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
label=label,
|
||||
@@ -51,9 +55,9 @@ class ButtonBox(Gtk.Box):
|
||||
|
||||
for side_button in ButtonType:
|
||||
button = ContextualButton(
|
||||
label=side_button.dict["label"],
|
||||
opens=side_button.dict["opens"],
|
||||
tooltip=side_button.dict["tooltip"],
|
||||
label=side_button.label,
|
||||
opens=side_button.opens,
|
||||
tooltip=side_button.tooltip,
|
||||
context=side_button,
|
||||
)
|
||||
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
from typing import Callable, Literal, Self, TYPE_CHECKING, Union
|
||||
from typing import Callable, Literal, Self, TYPE_CHECKING
|
||||
|
||||
from dzgui.util.clip import copy_clipboard
|
||||
from dzgui.util.format import pluralize
|
||||
from dzgui.strings import buttons
|
||||
from dzgui.util.strings import (
|
||||
alert_button_tooltip,
|
||||
atomic_buttons,
|
||||
)
|
||||
from dzgui.const.icons import (
|
||||
@@ -14,7 +12,6 @@ from dzgui.const.icons import (
|
||||
ICON_LIST_ADD,
|
||||
ICON_REFRESH,
|
||||
ICON_STEAM,
|
||||
ICON_WARNING,
|
||||
ICON_WEB_BROWSER,
|
||||
)
|
||||
from dzgui.strings import connect_panel
|
||||
@@ -64,7 +61,7 @@ class IconButton(Gtk.Button):
|
||||
self.icon = Icon(icon, margin_start=margin_start, margin_end=margin_end)
|
||||
self.set_image(self.icon)
|
||||
self.set_image_position(position)
|
||||
self.set_focus_on_click(False)
|
||||
self.set_focus_on_click(False) # type: ignore[no-untyped-call]
|
||||
self.set_always_show_image(True)
|
||||
|
||||
def swap_icon(self, icon: str) -> None:
|
||||
@@ -101,14 +98,13 @@ class LargeIconTextButton(IconButton):
|
||||
|
||||
|
||||
class ClipboardButton(IconTextButton):
|
||||
def __init__(self, controller: Union["Controller", None], func: Callable) -> None:
|
||||
def __init__(self, func: Callable[[], str]) -> None:
|
||||
super().__init__(ICON_CLIPBOARD, atomic_buttons.copy)
|
||||
self.controller = controller
|
||||
self.connect("clicked", self._on_button_clicked, func)
|
||||
|
||||
self.set_tooltip_text(buttons.clipboard_button)
|
||||
|
||||
def _on_button_clicked(self, button: Self, func: Callable) -> None:
|
||||
def _on_button_clicked(self, button: Self, func: Callable[[], str]) -> None:
|
||||
def revert() -> Literal[False]:
|
||||
self.revert_icon()
|
||||
return False
|
||||
@@ -119,10 +115,9 @@ class ClipboardButton(IconTextButton):
|
||||
GLib.timeout_add(600, revert)
|
||||
|
||||
|
||||
# TODO: determine when controller would be passed to this button or drop
|
||||
class CopyIpButton(ClipboardButton):
|
||||
def __init__(self, controller: Union["Controller", None], func: Callable) -> None:
|
||||
super().__init__(controller, func)
|
||||
def __init__(self, func: Callable[[], str]) -> None:
|
||||
super().__init__(func)
|
||||
|
||||
self.set_tooltip_text(buttons.clipboard_ip_button)
|
||||
|
||||
@@ -249,30 +244,6 @@ class AddButton(IconTextButton):
|
||||
self.set_tooltip_text(connect_panel.add_tooltip)
|
||||
|
||||
|
||||
# TODO: drop
|
||||
class LoggerAlertsButton(IconTextButton):
|
||||
def __init__(self, warnings: int, errors: int) -> None:
|
||||
warnings_text = ""
|
||||
errors_text = ""
|
||||
separator = ""
|
||||
if warnings > 0:
|
||||
warnings_plural = pluralize("warnings", warnings)
|
||||
warnings_text = f"{warnings} {warnings_plural}"
|
||||
if errors > 0:
|
||||
errors_plural = pluralize("errors", errors)
|
||||
errors_text = f" {errors} {errors_plural}"
|
||||
if warnings + errors > 1:
|
||||
separator = ","
|
||||
concat = warnings_text + separator + errors_text
|
||||
super().__init__(
|
||||
icon=ICON_WARNING,
|
||||
label=f"Loaded with{concat}",
|
||||
)
|
||||
self.set_halign(Gtk.Align.END)
|
||||
self.set_hexpand(True)
|
||||
self.set_tooltip_text(alert_button_tooltip)
|
||||
|
||||
|
||||
class SpinnerButton(Gtk.Button):
|
||||
def __init__(self, label: str) -> None:
|
||||
super().__init__()
|
||||
|
||||
@@ -189,7 +189,7 @@ class FavPanel(Gtk.Frame):
|
||||
|
||||
self.fav_button = SteamConnectButton()
|
||||
self.fav_button.connect("clicked", self._on_connect_clicked)
|
||||
self.copy_button = CopyIpButton(self.controller, self.get_fav_ip)
|
||||
self.copy_button = CopyIpButton(self.get_fav_ip)
|
||||
|
||||
self.unset_button = CloseButton(unset_button_label)
|
||||
self.unset_button.connect("clicked", self._on_unset_clicked)
|
||||
|
||||
@@ -43,7 +43,7 @@ class Breadcrumbs(Gtk.Label):
|
||||
label = page.get_current_tab_text()
|
||||
self.set_server_crumbs(label)
|
||||
else:
|
||||
crumbs = enum.dict["crumbs"]
|
||||
crumbs = enum.crumbs
|
||||
self.set_crumbs(crumbs)
|
||||
|
||||
def set_server_crumbs(self, suffix: str) -> None:
|
||||
|
||||
@@ -39,7 +39,7 @@ class ValidatedEntry(Gtk.Entry):
|
||||
def __init__(
|
||||
self,
|
||||
controller: "Controller",
|
||||
func: Callable,
|
||||
func: Callable[[str], bool],
|
||||
placeholder_text: str = "",
|
||||
tooltip_text: str = "",
|
||||
) -> None:
|
||||
@@ -47,7 +47,6 @@ class ValidatedEntry(Gtk.Entry):
|
||||
hexpand=True, placeholder_text=placeholder_text, tooltip_text=tooltip_text
|
||||
)
|
||||
|
||||
self.func: Callable
|
||||
self.set_validation_func(func)
|
||||
self.controller = controller
|
||||
|
||||
@@ -73,7 +72,7 @@ class ValidatedEntry(Gtk.Entry):
|
||||
self.emit("string_validated", False)
|
||||
remove_class(self, self.classname)
|
||||
|
||||
def set_validation_func(self, func: Callable) -> None:
|
||||
def set_validation_func(self, func: Callable[[str], bool]) -> None:
|
||||
self.func = func
|
||||
|
||||
def insert_icon(self) -> None:
|
||||
@@ -142,7 +141,7 @@ class APIEntry(Gtk.Box):
|
||||
def __init__(self) -> None:
|
||||
super().__init__(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
|
||||
|
||||
self.func: Callable | None = None
|
||||
self.func: Callable[[str], None] | None = None
|
||||
|
||||
self.entry = Gtk.Entry(
|
||||
width_chars=60, hexpand=True, placeholder_text=api_placeholder
|
||||
@@ -221,7 +220,7 @@ class APIEntry(Gtk.Box):
|
||||
res = self.func(text)
|
||||
return res
|
||||
|
||||
def set_validation_func(self, func: Callable | None) -> None:
|
||||
def set_validation_func(self, func: Callable[[str], None] | None) -> None:
|
||||
self.func = func
|
||||
|
||||
def disable_button(self) -> None:
|
||||
|
||||
@@ -31,7 +31,7 @@ class ButtonGrid(Gtk.Grid):
|
||||
def __init__(
|
||||
self,
|
||||
controller: "Controller",
|
||||
defaults: dict[str, str],
|
||||
defaults: dict[str, bool],
|
||||
tooltips: dict[str, str],
|
||||
) -> None:
|
||||
super().__init__(
|
||||
@@ -97,7 +97,7 @@ class ButtonGrid(Gtk.Grid):
|
||||
logger.info(f"User toggled button '{label}' to {state}")
|
||||
self.emitter.emit("check_toggled", label, state)
|
||||
|
||||
def get_checkboxes(self) -> list:
|
||||
def get_checkboxes(self) -> list[Gtk.CheckButton]:
|
||||
return self.checks
|
||||
|
||||
|
||||
@@ -306,6 +306,7 @@ class FilterPanel(Gtk.Box):
|
||||
Gdk.KEY_0: 9,
|
||||
Gdk.KEY_minus: 10,
|
||||
Gdk.KEY_backslash: 11,
|
||||
Gdk.KEY_apostrophe: 12,
|
||||
}
|
||||
if keyval not in mappings:
|
||||
return False
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
from typing import Self
|
||||
from dzgui.util import css
|
||||
|
||||
import gi
|
||||
|
||||
from typing import Self
|
||||
|
||||
from dzgui.util import css
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk # noqa
|
||||
from gi.repository import Gtk
|
||||
|
||||
|
||||
# TODO: subclass Gtk.Frame directly
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import gi
|
||||
from typing import Self
|
||||
from warnings import deprecated
|
||||
|
||||
@@ -8,10 +9,8 @@ from dzgui.strings import options
|
||||
from dzgui.views.components.box import HBox
|
||||
from dzgui.views.components.buttons import Icon
|
||||
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk # noqa
|
||||
from gi.repository import Gtk
|
||||
|
||||
|
||||
@deprecated("Client choice is determined at setup time")
|
||||
|
||||
@@ -19,8 +19,8 @@ class EnumeratedModButton(Gtk.Button):
|
||||
super().__init__(margin_start=10, margin_end=10, focus_on_click=False)
|
||||
|
||||
self.enum = enum
|
||||
self.set_label(enum.dict["label"])
|
||||
self.set_tooltip_text(enum.dict["tooltip"])
|
||||
self.set_label(enum.label)
|
||||
self.set_tooltip_text(enum.tooltip)
|
||||
|
||||
|
||||
class ModPanelButton(EnumeratedModButton):
|
||||
|
||||
@@ -106,14 +106,15 @@ class RightPanel(Gtk.Box):
|
||||
if page.loaded is True:
|
||||
return
|
||||
self.filters_vbox.set_sensitive(False)
|
||||
# TODO: unless it is lan page
|
||||
# self.refresh_button.set_sensitive(False)
|
||||
self.refresh_button.set_sensitive(False)
|
||||
|
||||
def _on_lan_page_init(self, emitter: "Emitter") -> None:
|
||||
self.filters_vbox.set_sensitive(False)
|
||||
|
||||
def _on_servers_loaded(self, emitter: "Emitter", context: "ServerTab") -> None:
|
||||
# TODO: similar logic on notebook page change
|
||||
# TODO: wipe maps store when changing tabs if model is None
|
||||
|
||||
state = self.controller.has_server_model()
|
||||
self.filters_vbox.set_sensitive(state)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk # noqa
|
||||
from gi.repository import Gtk
|
||||
|
||||
|
||||
class NoOverlayScrolledWindow(Gtk.ScrolledWindow):
|
||||
|
||||
@@ -2,7 +2,6 @@ from typing import Self, Union, TYPE_CHECKING
|
||||
|
||||
from dzgui.const.enum import NotebookPage, ServerTab
|
||||
from dzgui.util.strings import esc_to_return, question_to_return
|
||||
from dzgui.views.components.buttons import LoggerAlertsButton
|
||||
|
||||
import gi
|
||||
|
||||
@@ -47,9 +46,6 @@ class Statusbar(Gtk.Grid):
|
||||
self.emitter.connect("servers_loaded", self._on_servers_loaded)
|
||||
self.emitter.connect("mods_updated", self._on_mods_updated)
|
||||
|
||||
def _on_alerts_clicked(self, button: LoggerAlertsButton) -> None:
|
||||
self.controller.populate_log()
|
||||
|
||||
def _on_mods_updated(self, emitter: "Emitter", msg: str, mods: int) -> None:
|
||||
self.set_by_context(NotebookPage.MODS, msg)
|
||||
|
||||
@@ -60,7 +56,7 @@ class Statusbar(Gtk.Grid):
|
||||
return
|
||||
|
||||
enum = notebook.get_page_by_enum()
|
||||
show_statusbar = enum.dict["statusbar"]
|
||||
show_statusbar = enum.has_statusbar
|
||||
|
||||
if show_statusbar is False:
|
||||
self.set_by_context(enum, "")
|
||||
@@ -105,7 +101,7 @@ class Statusbar(Gtk.Grid):
|
||||
if type(context) is NotebookPage:
|
||||
self.prior_enum = context
|
||||
self.prior_status = ""
|
||||
# FIXME: CalcDist is being called when table is not loaded
|
||||
|
||||
if dist is None:
|
||||
self.set_by_context(context, "")
|
||||
return
|
||||
@@ -152,5 +148,5 @@ class Statusbar(Gtk.Grid):
|
||||
row = tree.get_value_at_index(1)
|
||||
if self.controller.loaded is False:
|
||||
return
|
||||
tooltip = row.dict["tooltip"]
|
||||
tooltip = row.tooltip
|
||||
self.set_by_context(NotebookPage.HELP, tooltip)
|
||||
@@ -1,13 +1,9 @@
|
||||
import gi
|
||||
import sys
|
||||
|
||||
from enum import Enum
|
||||
from typing import Any, Literal, Self, TYPE_CHECKING
|
||||
|
||||
# import time
|
||||
from enum import Enum
|
||||
|
||||
|
||||
from dzgui.views.mixins.colorscheme import ColorAwareApp
|
||||
|
||||
# TODO: import dialog titles
|
||||
from dzgui.api.mods import remove_stale_signatures as remove_stale
|
||||
from dzgui.config.ipdb import get_ipdb
|
||||
from dzgui.const.constants import HEX_GREEN, HEX_RED
|
||||
@@ -20,12 +16,10 @@ from dzgui.util.format import format_exception
|
||||
from dzgui.util.strings import dialog_header
|
||||
from dzgui.util.symlink import rebuild_symlinks
|
||||
from dzgui.views.components.buttons import ClipboardButton
|
||||
|
||||
|
||||
import gi
|
||||
from dzgui.views.mixins.colorscheme import ColorAwareApp
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, GLib, Gdk # noqa
|
||||
from gi.repository import Gtk, GLib, Gdk
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dzgui.config.xdg import Xdg
|
||||
@@ -104,7 +98,7 @@ class BootDialog(ColorAwareApp, Gtk.Dialog): # type: ignore
|
||||
self.error_label = Gtk.Label(margin_start=15, margin_end=15)
|
||||
|
||||
# TODO: abstract class
|
||||
self.copy_button = ClipboardButton(None, lambda: self.error_label.get_text())
|
||||
self.copy_button = ClipboardButton(self.error_label.get_text)
|
||||
self.button_hbox = Gtk.Box(
|
||||
orientation=Gtk.Orientation.HORIZONTAL, halign=Gtk.Align.CENTER, spacing=10
|
||||
)
|
||||
|
||||
@@ -46,7 +46,7 @@ class AbortDialog(ColorAwareApp, Gtk.MessageDialog): # type: ignore
|
||||
|
||||
self.connect("response", self._on_response)
|
||||
|
||||
self.run()
|
||||
self.run() # type: ignore[no-untyped-call]
|
||||
self.destroy()
|
||||
|
||||
def _on_response(self, dialog: Self, response: Gtk.ResponseType) -> None:
|
||||
|
||||
@@ -16,14 +16,14 @@ class FilePicker(Gtk.FileChooserDialog):
|
||||
transient_for=parent,
|
||||
resizable=True,
|
||||
)
|
||||
self.add_buttons("_Cancel", Gtk.ResponseType.CANCEL)
|
||||
self.add_buttons("_Save", Gtk.ResponseType.OK)
|
||||
self.add_buttons("_Cancel", Gtk.ResponseType.CANCEL) # type: ignore[no-untyped-call]
|
||||
self.add_buttons("_Save", Gtk.ResponseType.OK) # type: ignore[no-untyped-call]
|
||||
self.set_default_response(Gtk.ResponseType.OK)
|
||||
|
||||
self.set_current_name(picker.placeholder)
|
||||
|
||||
def pick_file(self) -> Path | None:
|
||||
res = self.run()
|
||||
res = self.run() # type: ignore[no-untyped-call]
|
||||
if res in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
|
||||
self.destroy()
|
||||
return None
|
||||
@@ -43,17 +43,18 @@ class FolderPicker(Gtk.FileChooserDialog):
|
||||
parent=parent,
|
||||
resizable=True,
|
||||
)
|
||||
self.add_buttons("_Cancel", Gtk.ResponseType.CANCEL)
|
||||
self.add_buttons("_OK", Gtk.ResponseType.OK)
|
||||
self.add_buttons("_Cancel", Gtk.ResponseType.CANCEL) # type: ignore[no-untyped-call]
|
||||
self.add_buttons("_OK", Gtk.ResponseType.OK) # type: ignore[no-untyped-call]
|
||||
self.set_default_response(Gtk.ResponseType.OK)
|
||||
|
||||
def pick_folder(self) -> Path | None:
|
||||
res = self.run()
|
||||
res = self.run() # type: ignore[no-untyped-call]
|
||||
if res in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
|
||||
self.destroy()
|
||||
return None
|
||||
if res == Gtk.ResponseType.OK:
|
||||
folder = self.get_current_folder()
|
||||
# TODO: this should never be possible
|
||||
if folder is None:
|
||||
try:
|
||||
uri = self.get_uri()
|
||||
@@ -61,7 +62,7 @@ class FolderPicker(Gtk.FileChooserDialog):
|
||||
return None
|
||||
return Path.from_uri(uri)
|
||||
except Exception:
|
||||
# FIXME: edge cases
|
||||
# TODO: edge cases
|
||||
return Path("/")
|
||||
self.destroy()
|
||||
return Path(folder)
|
||||
|
||||
@@ -185,7 +185,7 @@ class TextBufferDialog(GenericDialog):
|
||||
secondary=secondary,
|
||||
)
|
||||
self.text = text
|
||||
copy_button = ClipboardButton(controller, self.get_text)
|
||||
copy_button = ClipboardButton(self.get_text)
|
||||
self.add_action_widget(copy_button, Gtk.ResponseType.NONE)
|
||||
self.add_button("OK", Gtk.ResponseType.OK)
|
||||
self.connect("response", self._on_response)
|
||||
|
||||
@@ -15,7 +15,7 @@ from dzgui.views.dialogs.generic import GenericDialog
|
||||
from dzgui.views.trees.tree_base import TreeView
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, Gdk, Pango # noqa
|
||||
from gi.repository import Gtk, Gdk, Pango
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dzgui.api.servers import Details
|
||||
|
||||
@@ -22,7 +22,7 @@ from gi.repository import Gtk, GLib, Pango
|
||||
|
||||
|
||||
class Assistant(Gtk.Assistant):
|
||||
def __init__(self, is_deck: bool, paths: dict[str, str]):
|
||||
def __init__(self, is_deck: bool, paths: dict[str, Path]):
|
||||
super().__init__()
|
||||
if is_deck:
|
||||
# NOTE: deemed to be "safe" dimensions that exclude taskbar size
|
||||
@@ -63,7 +63,7 @@ class Assistant(Gtk.Assistant):
|
||||
|
||||
|
||||
class Window(Gtk.Window):
|
||||
def __init__(self, is_deck: bool, paths: dict[str, str]) -> None:
|
||||
def __init__(self, is_deck: bool, paths: dict[str, Path]) -> None:
|
||||
super().__init__(title=APP_NAME, icon_name=APP_NAME)
|
||||
self.assistant = Assistant(is_deck, paths)
|
||||
|
||||
@@ -121,7 +121,7 @@ class CheckboxWithPath(CheckboxWithLabel):
|
||||
|
||||
|
||||
class UninstallPage(ScrolledWizardPage):
|
||||
def __init__(self, paths: dict[str, str], pyapp: str | None):
|
||||
def __init__(self, paths: dict[str, Path], pyapp: str | None):
|
||||
super().__init__(heading=uninstall.pg1_heading, description=uninstall.pg1_blurb)
|
||||
|
||||
self.pyapp = pyapp
|
||||
@@ -219,7 +219,7 @@ class UninstallPage(ScrolledWizardPage):
|
||||
|
||||
|
||||
class UninstallWizard(ColorAwareApp, Gtk.Application): # type: ignore
|
||||
def __init__(self, is_deck: bool, paths: dict[str, str]) -> None:
|
||||
def __init__(self, is_deck: bool, paths: dict[str, Path]) -> None:
|
||||
super().__init__()
|
||||
config = Path(paths["XDG_CONFIG_HOME"])
|
||||
if not config.is_dir() or not config.joinpath("config.json").is_file():
|
||||
|
||||
@@ -168,7 +168,12 @@ class NotificationFrame(Gtk.Frame):
|
||||
|
||||
class APIValidationPage(EnumeratedWizardPage):
|
||||
def __init__(
|
||||
self, enum: PageNum, heading: str, description: str, link: str, func: Callable
|
||||
self,
|
||||
enum: PageNum,
|
||||
heading: str,
|
||||
description: str,
|
||||
link: str,
|
||||
func: Callable[[str], None],
|
||||
) -> None:
|
||||
super().__init__(
|
||||
enum=enum,
|
||||
@@ -421,7 +426,7 @@ class PreferencesPage(EnumeratedWizardPage):
|
||||
|
||||
|
||||
class FlatpakOverridePage(EnumeratedWizardPage):
|
||||
def __init__(self, xdg_paths: dict[str, str]) -> None:
|
||||
def __init__(self, xdg_paths: dict[str, Path]) -> None:
|
||||
super().__init__(
|
||||
enum=PageNum.FLATPAK,
|
||||
heading=wizard.heading_flatpak,
|
||||
@@ -471,7 +476,7 @@ class CompletionPage(EnumeratedWizardPage):
|
||||
|
||||
|
||||
class Assistant(Gtk.Assistant):
|
||||
def __init__(self, is_deck: bool, XDG: "Xdg", xdg_paths: dict[str, str]):
|
||||
def __init__(self, is_deck: bool, XDG: "Xdg", xdg_paths: dict[str, Path]):
|
||||
super().__init__()
|
||||
if is_deck:
|
||||
# NOTE: deemed to be "safe" dimensions that exclude taskbar size
|
||||
@@ -776,7 +781,7 @@ class SteamPathPage(EnumeratedWizardPage):
|
||||
|
||||
|
||||
class SetupWizard(ColorAwareApp, Gtk.Application): # type: ignore
|
||||
def __init__(self, is_deck: bool, XDG: "Xdg", xdg_paths: dict[str, str]) -> None:
|
||||
def __init__(self, is_deck: bool, XDG: "Xdg", xdg_paths: dict[str, Path]) -> None:
|
||||
super().__init__()
|
||||
GLib.set_prgname(APP_NAME)
|
||||
self.win = Window(is_deck, XDG, xdg_paths)
|
||||
@@ -787,7 +792,7 @@ class SetupWizard(ColorAwareApp, Gtk.Application): # type: ignore
|
||||
|
||||
|
||||
class Window(Gtk.Window):
|
||||
def __init__(self, is_deck: bool, XDG: "Xdg", xdg_paths: dict[str, str]) -> None:
|
||||
def __init__(self, is_deck: bool, XDG: "Xdg", xdg_paths: dict[str, Path]) -> None:
|
||||
super().__init__(title=APP_NAME, icon_name=APP_NAME)
|
||||
self.assistant = Assistant(is_deck, XDG, xdg_paths)
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import gi
|
||||
|
||||
from typing import Any
|
||||
|
||||
from dzgui.const.constants import (
|
||||
@@ -7,10 +9,8 @@ from dzgui.const.constants import (
|
||||
GIO_SETTINGS_VAL,
|
||||
)
|
||||
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, Gio # noqa
|
||||
from gi.repository import Gtk, Gio
|
||||
|
||||
|
||||
class ColorAwareApp:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import logging
|
||||
|
||||
from dzgui.const.constants import APP_NAME
|
||||
from dzgui.const.enum import ContextMenu
|
||||
from dzgui.const.enum import ContextMenuItem
|
||||
from dzgui.util.keys import is_ctrl_mask, is_navkey
|
||||
from dzgui.util.strings import edit_note
|
||||
from dzgui.views.trees.tree_base import TreeView
|
||||
@@ -15,9 +15,7 @@ logger = logging.getLogger(APP_NAME)
|
||||
|
||||
|
||||
class ContextMixin(TreeView):
|
||||
def present_menu(
|
||||
self, widget: Gtk.Widget, event: Gdk.Event
|
||||
) -> bool:
|
||||
def present_menu(self, widget: Gtk.Widget, event: Gdk.Event) -> bool:
|
||||
|
||||
if self.menu is None:
|
||||
return False
|
||||
@@ -26,7 +24,7 @@ class ContextMixin(TreeView):
|
||||
|
||||
match event.type:
|
||||
case Gdk.EventType.BUTTON_PRESS:
|
||||
if event.button != 3:
|
||||
if event.button != Gdk.BUTTON_SECONDARY: # type: ignore[comparison-overlap]
|
||||
return False
|
||||
self._process_button_event(event) # type: ignore
|
||||
case Gdk.EventType.KEY_PRESS:
|
||||
@@ -60,17 +58,17 @@ class ContextMixin(TreeView):
|
||||
self.context_menu.select_first(False)
|
||||
return True
|
||||
|
||||
def _process_dynamic_row(self, row: ContextMenu) -> Gtk.MenuItem:
|
||||
if row == ContextMenu.ADD_SERVER and self.is_in_favs(): # type: ignore
|
||||
row = ContextMenu.REMOVE_SERVER
|
||||
def _process_dynamic_row(self, row: ContextMenuItem) -> Gtk.MenuItem:
|
||||
if row == ContextMenuItem.ADD_SERVER and self.is_in_favs(): # type: ignore
|
||||
row = ContextMenuItem.REMOVE_SERVER
|
||||
|
||||
item = Gtk.MenuItem(label=row.dict["label"])
|
||||
item = Gtk.MenuItem(label=row.label)
|
||||
item.connect("activate", self._on_menu_click, row)
|
||||
|
||||
if row == ContextMenu.SHOW_MODS:
|
||||
if row == ContextMenuItem.SHOW_MODS:
|
||||
item.set_sensitive(self.is_modded()) # type: ignore
|
||||
|
||||
if row == ContextMenu.ADD_NOTE:
|
||||
if row == ContextMenuItem.ADD_NOTE:
|
||||
if self.controller.has_note():
|
||||
item.set_label(edit_note)
|
||||
|
||||
@@ -94,7 +92,7 @@ class ContextMixin(TreeView):
|
||||
except AttributeError:
|
||||
return False
|
||||
|
||||
def _on_menu_click(self, widget: Gtk.MenuItem, enum: ContextMenu) -> None:
|
||||
def _on_menu_click(self, widget: Gtk.MenuItem, enum: ContextMenuItem) -> None:
|
||||
self.controller.menu_action(enum, self) # type: ignore
|
||||
|
||||
def _on_key(self, menu: Gtk.Menu, event: Gdk.EventKey) -> bool | None:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, Gdk # noqa
|
||||
from gi.repository import Gtk, Gdk
|
||||
|
||||
|
||||
class HelpMenuMixin:
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import gi
|
||||
|
||||
from dzgui.const.enum import VAdjustment
|
||||
from dzgui.const.constants import SCROLL_INCREMENT
|
||||
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, Gdk # noqa
|
||||
from gi.repository import Gtk, Gdk
|
||||
|
||||
|
||||
class ScrollableMixin:
|
||||
|
||||
@@ -13,6 +13,7 @@ if TYPE_CHECKING:
|
||||
from dzgui.controllers.mc import Controller
|
||||
|
||||
|
||||
# TODO: refactor into autopopulated ListStores
|
||||
class Keybindings(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore
|
||||
"""
|
||||
Notebook page holding a prearranged grid
|
||||
@@ -35,7 +36,7 @@ class Keybindings(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore
|
||||
|
||||
self.connect("key-press-event", self._on_keypress)
|
||||
|
||||
def build_keys(self, items: list) -> Gtk.Grid:
|
||||
def build_keys(self, items: list[dict[str, str]]) -> Gtk.Grid:
|
||||
grid = Gtk.Grid(row_spacing=10, column_homogeneous=True)
|
||||
grid.set_halign(Gtk.Align.START)
|
||||
row, col = 1, 0
|
||||
@@ -72,7 +73,7 @@ class Keybindings(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore
|
||||
row += 1
|
||||
return grid
|
||||
|
||||
def build_sidebar(self, categories: list) -> Gtk.Grid:
|
||||
def build_sidebar(self, categories: list[str]) -> Gtk.Grid:
|
||||
row, col = 0, 0
|
||||
w, h = 1, 1
|
||||
sidebar = Gtk.Grid(row_homogeneous=True, orientation=Gtk.Orientation.VERTICAL)
|
||||
@@ -83,7 +84,7 @@ class Keybindings(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore
|
||||
sidebar.attach(label, col, row, w, h)
|
||||
return sidebar
|
||||
|
||||
def build_grid(self, items: list) -> Gtk.Grid:
|
||||
def build_grid(self, items: list[dict[str, str]]) -> Gtk.Grid:
|
||||
grid = Gtk.Grid(
|
||||
row_spacing=20,
|
||||
halign=Gtk.Align.CENTER,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import gi
|
||||
from pathlib import Path
|
||||
|
||||
from typing import Self, TYPE_CHECKING
|
||||
@@ -8,10 +9,8 @@ from dzgui.views.components.buttons import SteamWorkshopButton
|
||||
from dzgui.views.components.scrollable import NoOverlayScrolledWindow
|
||||
from dzgui.views.trees.tree_mods import ModTreeView
|
||||
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, Gdk # noqa
|
||||
from gi.repository import Gtk
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dzgui.controllers.mc import Controller
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import gi
|
||||
|
||||
from enum import Enum
|
||||
from typing import Self, TYPE_CHECKING, Union
|
||||
|
||||
from dzgui.util import css
|
||||
from dzgui.const.icons import ICON_EDIT_DELETE, ICON_FOLDER, ICON_WARNING
|
||||
from dzgui.const.constants import (
|
||||
APPID_DAYZ,
|
||||
APPID_DAYZ_EXP,
|
||||
@@ -11,8 +12,10 @@ from dzgui.const.constants import (
|
||||
APPNAME_DAYZ_EXP_HUMAN,
|
||||
)
|
||||
from dzgui.const.enum import ContextMenuGroup, NotebookPage
|
||||
from dzgui.const.icons import ICON_EDIT_DELETE, ICON_FOLDER, ICON_WARNING
|
||||
from dzgui.managers.offline import OfflineManager
|
||||
from dzgui.strings import generic, offline
|
||||
from dzgui.util import css
|
||||
from dzgui.views.components.box import HBox, VBox
|
||||
from dzgui.views.components.buttons import Icon, IconTextButton
|
||||
from dzgui.views.components.eventbox import InfoEventBox
|
||||
@@ -21,11 +24,8 @@ from dzgui.views.components.scrollable import NoOverlayScrolledWindow
|
||||
from dzgui.views.components.misc import ErrorPopover
|
||||
from dzgui.views.trees.tree_mods import OfflineModTreeView
|
||||
|
||||
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, Gdk, GObject # noqa
|
||||
from gi.repository import Gtk, Gdk
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dzgui.controllers.mc import Controller
|
||||
@@ -136,7 +136,6 @@ class FolderHBox(HBox):
|
||||
self.folder = ""
|
||||
self.label.set_label("")
|
||||
self.unset_button.hide()
|
||||
# FIXME: rename signal
|
||||
self.emitter.emit("custom_mods_unloaded", self)
|
||||
|
||||
def get_button(self) -> Gtk.Button:
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import gi
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Callable, TYPE_CHECKING
|
||||
from typing import Any, Callable, TYPE_CHECKING
|
||||
|
||||
from dzgui.api import pefile as PeFile
|
||||
from dzgui.api.steam import get_last_updated
|
||||
|
||||
from dzgui.config import query
|
||||
from dzgui.const.constants import (
|
||||
APPID_DAYZ,
|
||||
@@ -14,12 +15,11 @@ from dzgui.const.constants import (
|
||||
FLATPAK_SANDBOX,
|
||||
FLATPAK_STEAM_PATH,
|
||||
)
|
||||
from dzgui.const.icons import ICON_VIEW_CONCEAL, ICON_VIEW_REVEAL
|
||||
from dzgui.const.endpoints import STEAM_API_SETUP
|
||||
from dzgui.const.enum import Preferences, ServerTab
|
||||
from dzgui.const.icons import ICON_VIEW_CONCEAL, ICON_VIEW_REVEAL
|
||||
from dzgui.strings import options
|
||||
from dzgui.util import strings, css, open_links
|
||||
|
||||
from dzgui.views.components.box import ShortHBox, VBox
|
||||
from dzgui.views.components.eventbox import InfoEventBox
|
||||
from dzgui.views.components.labels import LeftLabel
|
||||
@@ -29,11 +29,8 @@ from dzgui.views.components.misc import ErrorPopover
|
||||
from dzgui.views.dialogs.generic import ExceptionDialog
|
||||
from dzgui.views.mixins.scrollable_mixin import ScrollableMixin
|
||||
|
||||
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, Gdk # noqa
|
||||
from gi.repository import Gtk, Gdk
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dzgui.controllers.mc import Controller
|
||||
@@ -51,7 +48,7 @@ class SubmitField(Gtk.Box):
|
||||
) -> None:
|
||||
super().__init__(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
|
||||
|
||||
self.callback: Callable
|
||||
self.callback: Callable[[], None]
|
||||
self.controller = controller
|
||||
self.context = context
|
||||
|
||||
@@ -139,7 +136,7 @@ class SubmitField(Gtk.Box):
|
||||
return
|
||||
self.save_option()
|
||||
|
||||
def set_callback(self, callback: Callable) -> None:
|
||||
def set_callback(self, callback: Callable[[], None]) -> None:
|
||||
self.callback = callback
|
||||
|
||||
def save_option(self) -> None:
|
||||
@@ -206,7 +203,7 @@ class NameSubmitField(SubmitField):
|
||||
Preferences.NAME,
|
||||
private=False,
|
||||
)
|
||||
self.entry.set_width_chars(30) # type: ignore
|
||||
self.entry.set_width_chars(30)
|
||||
self.set_callback(self.save_player_name)
|
||||
|
||||
def save_player_name(self) -> None:
|
||||
@@ -233,10 +230,19 @@ class ToggleField(Gtk.Box):
|
||||
self.pack_start(self.radio1, expand=False, fill=False, padding=0)
|
||||
self.pack_start(self.radio2, expand=False, fill=False, padding=0)
|
||||
|
||||
self.signal_id: int | None = None
|
||||
|
||||
def disconnect_signal(self) -> None:
|
||||
if self.signal_id is not None:
|
||||
self.radio1.disconnect(self.signal_id)
|
||||
|
||||
def set_suboption_active(self, state: bool) -> None:
|
||||
# NOTE: defer connection of signal until after state is set
|
||||
self.radio2.set_active(state)
|
||||
self.radio1.connect("toggled", self._on_radio_toggled, self.context)
|
||||
self.disconnect_signal()
|
||||
self.signal_id = self.radio1.connect(
|
||||
"toggled", self._on_radio_toggled, self.context
|
||||
)
|
||||
|
||||
def _on_radio_toggled(self, button: Gtk.RadioButton, context: Preferences) -> None:
|
||||
self.controller.toggle_config(context)
|
||||
@@ -402,7 +408,8 @@ class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore
|
||||
def _on_link_button_clicked(self, button: Gtk.Button, url: str) -> None:
|
||||
open_links.open_link_by_url(url)
|
||||
|
||||
def _make_grid(self, rows: list) -> Gtk.Grid:
|
||||
# TODO: use FlowBox
|
||||
def _make_grid(self, rows: list[list[Any]]) -> Gtk.Grid:
|
||||
grid = Gtk.Grid(
|
||||
orientation=Gtk.Orientation.VERTICAL,
|
||||
column_spacing=10,
|
||||
@@ -435,7 +442,7 @@ class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore
|
||||
if prefs.paths.config.is_file() is False:
|
||||
dialog = ExceptionDialog(self.controller, strings.config_not_found)
|
||||
dialog.show_all()
|
||||
dialog.run()
|
||||
dialog.run() # type: ignore[no-untyped-call]
|
||||
raise OSError(f"Config file '{prefs.paths.config}' not found")
|
||||
|
||||
config = query.get_config(prefs.paths.config)
|
||||
@@ -485,14 +492,5 @@ class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore
|
||||
start_tab = self.controller.query_config(Preferences.START_TAB)
|
||||
self.start_tab_combo.set_active(start_tab)
|
||||
|
||||
def _suppress_toggles(self, state: bool) -> None:
|
||||
for toggle in [
|
||||
self.fullscreen_toggle,
|
||||
self.distance_toggle,
|
||||
]:
|
||||
self.controller.suppress_signal(
|
||||
self, toggle.get_children()[0], "_on_radio_toggled", state
|
||||
)
|
||||
|
||||
def grab_content_area(self) -> None:
|
||||
self.grab_focus()
|
||||
@@ -1,11 +1,12 @@
|
||||
from dataclasses import dataclass
|
||||
import gi
|
||||
|
||||
from typing import Self, Sequence, TYPE_CHECKING
|
||||
|
||||
from dzgui.const.enum import NotebookPage
|
||||
from dzgui.const.icons import (
|
||||
ICON_ERROR,
|
||||
ICON_WARNING,
|
||||
)
|
||||
from dzgui.const.enum import NotebookPage
|
||||
from dzgui.util.css import add_class
|
||||
from dzgui.util.keys import is_ctrl_mask
|
||||
from dzgui.util.localize import number
|
||||
@@ -15,33 +16,15 @@ from dzgui.views.dialogs.generic import DebugDialog
|
||||
from dzgui.views.components.frame import HeadingFrame
|
||||
from dzgui.views.trees.tree_server_mods import ServerModTreeView
|
||||
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gdk, Gtk
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dzgui.managers.connection import Prerequisites
|
||||
from dzgui.controllers.mc import Controller
|
||||
from dzgui.controllers.emitter import Emitter
|
||||
|
||||
|
||||
@dataclass
|
||||
class Warnings:
|
||||
passworded: bool
|
||||
dayz_running: bool
|
||||
no_steam: bool
|
||||
wrong_version: bool
|
||||
|
||||
|
||||
@dataclass
|
||||
class Errors:
|
||||
no_space: bool
|
||||
no_dayz_exp: bool
|
||||
no_dayz: bool
|
||||
|
||||
|
||||
class Placeholder(Gtk.Label):
|
||||
def __init__(self, text: str) -> None:
|
||||
super().__init__(
|
||||
@@ -231,7 +214,7 @@ class PreConnectionAssistant(Gtk.Box):
|
||||
args = self.controller.get_debug_args()
|
||||
d = DebugDialog(self.controller, args)
|
||||
d.show_all()
|
||||
d.run()
|
||||
d.run() # type: ignore[no-untyped-call]
|
||||
|
||||
def _on_ok_clicked(self, button: Gtk.Button) -> None:
|
||||
self.controller.update_and_connect()
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import gi
|
||||
import logging
|
||||
|
||||
from typing import Any, Optional, TYPE_CHECKING
|
||||
from typing import Any, TYPE_CHECKING
|
||||
|
||||
from dzgui.const.constants import APP_NAME, SEPARATOR
|
||||
from dzgui.util.keys import is_ctrl_mask, is_navkey
|
||||
from dzgui.views.mixins.cursor_mixin import CursorMixin
|
||||
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa
|
||||
from gi.repository import Gtk, Gdk, GObject
|
||||
|
||||
logger = logging.getLogger(APP_NAME)
|
||||
|
||||
@@ -20,7 +19,7 @@ if TYPE_CHECKING:
|
||||
|
||||
class TreeView(CursorMixin, Gtk.TreeView): # type: ignore
|
||||
def __init__(
|
||||
self, controller: "Controller", menu: Optional["ContextMenuGroup"]
|
||||
self, controller: "Controller", menu: "ContextMenuGroup | None"
|
||||
) -> None:
|
||||
super().__init__(
|
||||
enable_search=False,
|
||||
@@ -59,7 +58,7 @@ class TreeView(CursorMixin, Gtk.TreeView): # type: ignore
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_current_iter(self) -> Optional[Gtk.TreeIter]:
|
||||
def get_current_iter(self) -> Gtk.TreeIter | None:
|
||||
it = self.get_selection().get_selected()[1]
|
||||
return it
|
||||
|
||||
@@ -79,11 +78,6 @@ class TreeView(CursorMixin, Gtk.TreeView): # type: ignore
|
||||
index = path.get_indices()[0]
|
||||
return index
|
||||
|
||||
def get_selected_records(self) -> list:
|
||||
sel = self.get_selection()
|
||||
model, rows = sel.get_selected_rows()
|
||||
return [model[row] for row in rows]
|
||||
|
||||
def _on_keypress(self, treeview: Gtk.TreeView, event: Gdk.EventKey) -> bool:
|
||||
from dzgui.views.trees.tree_servers import ServerTreeView
|
||||
|
||||
@@ -145,9 +139,11 @@ class TreeView(CursorMixin, Gtk.TreeView): # type: ignore
|
||||
self.set_cursor(path)
|
||||
|
||||
def get_value_at_index(self, index: int) -> Any:
|
||||
(model, pathlist) = self.get_model_and_pathlist()
|
||||
model, pathlist = self.get_model_and_pathlist()
|
||||
if len(pathlist) < 1:
|
||||
return ""
|
||||
if model is None:
|
||||
return ""
|
||||
path = pathlist[0]
|
||||
tree_iter = model.get_iter(path)
|
||||
value = model.get_value(tree_iter, index)
|
||||
@@ -161,20 +157,16 @@ class TreeView(CursorMixin, Gtk.TreeView): # type: ignore
|
||||
sel = self.get_selection()
|
||||
self._on_tree_selection_changed(sel)
|
||||
|
||||
def get_model_and_pathlist(self) -> tuple:
|
||||
def get_model_and_pathlist(
|
||||
self,
|
||||
) -> tuple[Gtk.TreeModel | None, list[Gtk.TreePath]]:
|
||||
select = self.get_selection()
|
||||
sels = select.get_selected_rows()
|
||||
(model, pathlist) = sels
|
||||
model: Gtk.TreeModel | None
|
||||
pathlist: list[Gtk.TreePath]
|
||||
model, pathlist = sels
|
||||
return (model, pathlist)
|
||||
|
||||
# @deprecated("Currently unused")
|
||||
# def get_mpath(self) -> Optional[Gtk.TreePath]:
|
||||
# (model, pathlist) = self.get_model_and_pathlist()
|
||||
# if len(pathlist) < 1:
|
||||
# return None
|
||||
# path = pathlist[0]
|
||||
# return path
|
||||
|
||||
def _on_row_activated(
|
||||
self,
|
||||
treeview: Gtk.TreeView,
|
||||
@@ -189,15 +181,6 @@ class TreeView(CursorMixin, Gtk.TreeView): # type: ignore
|
||||
return True
|
||||
return False
|
||||
|
||||
# @deprecated("unused")
|
||||
# def get_selected_row(self) -> Optional[Gtk.TreeModelRow]:
|
||||
# ind = self.get_selected_row_index()
|
||||
# model = self.get_model()
|
||||
# if model is None:
|
||||
# return None
|
||||
# row = model[ind]
|
||||
# return row
|
||||
|
||||
def get_col_value_by_path_index(self, path: Gtk.TreePath, index: int) -> Any:
|
||||
model = self.get_model()
|
||||
if model is None:
|
||||
|
||||
@@ -10,7 +10,7 @@ from dzgui.views.trees.tree_base import TreeView
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa
|
||||
from gi.repository import Gtk
|
||||
|
||||
logger = logging.getLogger(APP_NAME)
|
||||
|
||||
@@ -77,4 +77,4 @@ class MenuTreeView(TreeView):
|
||||
raise ValueError("Trying to call a method on non-existent model")
|
||||
_iter = self.get_focused_row_iter()
|
||||
rowtype = model.get_value(_iter, 1)
|
||||
return str(rowtype.dict["tooltip"])
|
||||
return str(rowtype.tooltip)
|
||||
@@ -1,4 +1,6 @@
|
||||
import gi
|
||||
import logging
|
||||
|
||||
from typing import Any, TYPE_CHECKING
|
||||
|
||||
from dzgui.const.constants import APP_NAME, HEX_RED
|
||||
@@ -9,10 +11,8 @@ from dzgui.views.mixins.context_mixin import ContextMixin
|
||||
from dzgui.views.mixins.mods_mixin import ModsMixin
|
||||
from dzgui.views.trees.tree_base import TreeView
|
||||
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa
|
||||
from gi.repository import Gtk, Gdk
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import gi
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from dzgui.const.enum import ContextMenuGroup
|
||||
from dzgui.model.model_factory import ModelFactory
|
||||
from dzgui.strings import server_mods
|
||||
from dzgui.views.mixins.context_mixin import ContextMixin
|
||||
from dzgui.views.trees.tree_base import TreeView
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, GLib, Gdk, Pango # noqa
|
||||
from gi.repository import Gtk, Pango
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dzgui.controllers.mc import Controller
|
||||
|
||||
@@ -1,30 +1,25 @@
|
||||
import gi
|
||||
import logging
|
||||
import threading
|
||||
|
||||
from queue import Queue
|
||||
from typing import Any, Self, Union
|
||||
from typing import Any, Literal, Self, TYPE_CHECKING, Union
|
||||
from warnings import deprecated
|
||||
|
||||
from dzgui.api.servers import ping, Record
|
||||
from dzgui.const.constants import APP_NAME
|
||||
from dzgui.const.enum import ContextMenu, ContextMenuGroup, ServerTab
|
||||
from dzgui.const.enum import ContextMenuItem, ContextMenuGroup, ServerTab
|
||||
from dzgui.managers.filter import FilterManager
|
||||
from dzgui.model.proxy_model import ProxyModelManager
|
||||
from dzgui.model.proxy_model import ProxyModelManager, ServerRow
|
||||
from dzgui.util import strings
|
||||
from dzgui.util.dist import CalcDist
|
||||
from dzgui.util.keys import is_ctrl_mask
|
||||
from dzgui.views.mixins.context_mixin import ContextMixin
|
||||
from typing import Literal, TYPE_CHECKING
|
||||
|
||||
|
||||
from dzgui.views.trees.tree_base import TreeView
|
||||
import dzgui.util._json as JSON # noqa
|
||||
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa
|
||||
|
||||
from gi.repository import Gtk, GLib, Gdk, GObject
|
||||
|
||||
logger = logging.getLogger(APP_NAME)
|
||||
|
||||
@@ -32,9 +27,17 @@ if TYPE_CHECKING:
|
||||
from dzgui.controllers.mc import Controller
|
||||
from dzgui.controllers.emitter import Emitter
|
||||
from dzgui.model.model_factory import FastInsertListStore
|
||||
from dzgui.util.dist import Haversine
|
||||
|
||||
# TODO: consts
|
||||
QUEUE_CHECK_DELAY = 200
|
||||
|
||||
IND_NAME = ServerRow.get_index("name")
|
||||
IND_ADDR = ServerRow.get_index("ip")
|
||||
IND_QPORT = ServerRow.get_index("qport")
|
||||
IND_PING = ServerRow.get_index("ping")
|
||||
IND_MODDED = ServerRow.get_index("modded")
|
||||
|
||||
|
||||
class ServerTreeView(ContextMixin, TreeView): # type: ignore
|
||||
def __init__(
|
||||
@@ -64,7 +67,7 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
|
||||
|
||||
self.queue_id: int
|
||||
self.handler_id: int
|
||||
self.queue: Queue = Queue()
|
||||
self.queue: Queue[tuple[str, "Haversine" | None, ServerTab]] = Queue()
|
||||
|
||||
self.seen_cache: list[Record] = []
|
||||
|
||||
@@ -105,7 +108,7 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
|
||||
if column_title in width_map:
|
||||
w = width_map[column_title]
|
||||
column.set_fixed_width(w)
|
||||
if column_title == "Ping":
|
||||
if i == IND_PING:
|
||||
column.set_cell_data_func(renderer, self._get_ping)
|
||||
|
||||
column.connect("notify::fixed-width", self._on_col_width_changed)
|
||||
@@ -144,8 +147,8 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
|
||||
if model is None:
|
||||
return False
|
||||
tree_iter = model.get_iter(path[0])
|
||||
ip = model.get_value(tree_iter, 7)
|
||||
qport = model.get_value(tree_iter, 8)
|
||||
ip = model.get_value(tree_iter, IND_ADDR)
|
||||
qport = model.get_value(tree_iter, IND_QPORT)
|
||||
addr = ip + ":" + str(qport)
|
||||
note = self.controller.get_note_by_record(addr)
|
||||
|
||||
@@ -168,14 +171,12 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
|
||||
return self.enum
|
||||
|
||||
def _on_map(self, widget: Self) -> None:
|
||||
# TODO: disable filter panel if current model is None
|
||||
state = self.get_enum() is ServerTab.LAN
|
||||
self.emitter.emit("lan_tab_toggled", state)
|
||||
|
||||
store = self.filter_man.get_map_store()
|
||||
|
||||
# FIXME: if model is none, wipe maps
|
||||
# distinguish this signal from changing map combo in-situ
|
||||
# TODO: distinguish this signal from changing map combo in-situ
|
||||
self.emitter.emit("load_maps", store)
|
||||
self.handler_id = self.emitter.connect("statusbar_loaded", self.start_distcalc)
|
||||
self.start_queue_checker()
|
||||
@@ -220,11 +221,11 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
|
||||
|
||||
def _check_result_queue(self) -> Literal[True]:
|
||||
latest_result = None
|
||||
# FIXME: empty results cause statusbar to be emptied
|
||||
# TODO: Nonetype results cause statusbar to be blank
|
||||
while not self.queue.empty():
|
||||
latest_result = self.queue.get()
|
||||
|
||||
# FIXME: cache is being checked in two passes
|
||||
# TODO: cache is being checked in two passes
|
||||
cache = self.controller.get_dist_cache()
|
||||
|
||||
if latest_result:
|
||||
@@ -250,9 +251,9 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
|
||||
if self.enum is ServerTab.LAN:
|
||||
self.emitter.emit("request_custom_port_focus")
|
||||
case Gdk.KEY_c:
|
||||
self.controller.menu_action(ContextMenu.COPY_SERVER_IP, self)
|
||||
self.controller.menu_action(ContextMenuItem.COPY_SERVER_IP, self)
|
||||
case Gdk.KEY_r:
|
||||
self.controller.menu_action(ContextMenu.REFRESH_PLAYERS, self)
|
||||
self.controller.menu_action(ContextMenuItem.REFRESH_PLAYERS, self)
|
||||
else:
|
||||
match event.keyval:
|
||||
case Gdk.KEY_l | Gdk.KEY_Right:
|
||||
@@ -266,15 +267,9 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
|
||||
(model, pathlist) = sels
|
||||
path = pathlist[0]
|
||||
tree_iter = model.get_iter(path)
|
||||
has_mods = model.get_value(tree_iter, 11)
|
||||
has_mods = model.get_value(tree_iter, IND_MODDED)
|
||||
return bool(has_mods)
|
||||
|
||||
# def get_selected_row(self) -> Gtk.TreeModelRow:
|
||||
# sel = self.get_selection()
|
||||
# sels = sel.get_selected_rows()
|
||||
# print(type(sels[0]))
|
||||
# return sels[0]
|
||||
|
||||
def is_in_favs(self) -> bool:
|
||||
record = self.get_record_string()
|
||||
if self.controller.get_config_man().is_in_favs(record):
|
||||
@@ -303,17 +298,17 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
|
||||
self.start_distcalc()
|
||||
|
||||
def get_name(self) -> str:
|
||||
return str(self.get_value_at_index(0))
|
||||
return str(self.get_value_at_index(IND_NAME))
|
||||
|
||||
def get_simplified_ip(self) -> str:
|
||||
addr = self.get_value_at_index(7)
|
||||
qport = self.get_value_at_index(8)
|
||||
addr = self.get_value_at_index(IND_ADDR)
|
||||
qport = self.get_value_at_index(IND_QPORT)
|
||||
ip = addr.split(":")[0]
|
||||
return f"{ip}:{qport}"
|
||||
|
||||
def get_record_string(self) -> str:
|
||||
addr = self.get_value_at_index(7)
|
||||
qport = self.get_value_at_index(8)
|
||||
addr = self.get_value_at_index(IND_ADDR)
|
||||
qport = self.get_value_at_index(IND_QPORT)
|
||||
return f"{addr}:{qport}"
|
||||
|
||||
def get_record(self) -> Record | None:
|
||||
@@ -352,8 +347,7 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
|
||||
def _redraw(
|
||||
self, model: "FastInsertListStore", _iter: Gtk.TreeIter, _ping: int
|
||||
) -> None:
|
||||
ping_column = 9
|
||||
model.set(_iter, ping_column, _ping)
|
||||
model.set(_iter, IND_PING, _ping)
|
||||
self.queue_draw()
|
||||
|
||||
def wipe_cache(self) -> None:
|
||||
@@ -368,13 +362,10 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
|
||||
data: Any,
|
||||
) -> None:
|
||||
|
||||
addr_column = 7
|
||||
qport_column = 8
|
||||
|
||||
addr = model.get_value(_iter, addr_column).split(":")
|
||||
addr = model.get_value(_iter, IND_ADDR).split(":")
|
||||
ip = addr[0]
|
||||
gameport = int(addr[1])
|
||||
qport = model.get_value(_iter, qport_column)
|
||||
qport = model.get_value(_iter, IND_QPORT)
|
||||
record = Record(ip, gameport, qport)
|
||||
|
||||
if record in self.seen_cache:
|
||||
@@ -406,11 +397,16 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
|
||||
path = model.get_path(it)
|
||||
row_index = path.get_indices()[0]
|
||||
try:
|
||||
start, end = self.get_visible_range()
|
||||
start, end = self.get_visible_range() # type: ignore[no-untyped-call]
|
||||
except Exception:
|
||||
return
|
||||
if row_index >= start[0] <= end[0]:
|
||||
# NOTE: fetch raw data rows
|
||||
real_model = self.proxy_man.get_control()
|
||||
value = real_model[row_index][col_index]
|
||||
from dataclasses import fields
|
||||
from dzgui.model.proxy_model import ServerRow
|
||||
|
||||
key = fields(ServerRow)[col_index].name
|
||||
|
||||
value = getattr(real_model[row_index], key)
|
||||
cell.set_property("text", str(value))
|
||||
@@ -1,13 +1,13 @@
|
||||
import gi
|
||||
|
||||
from typing import TYPE_CHECKING, Union
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dzgui.model.model_factory import FastInsertListStore
|
||||
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk # noqa
|
||||
|
||||
|
||||
class SimpleTree(Gtk.ScrolledWindow):
|
||||
"""Simple scrollable 1-column tree"""
|
||||
|
||||
+1
-1
@@ -105,7 +105,7 @@ python-a2s = { path = "./dzgui/lib/a2s", editable = true }
|
||||
dayzquery = { path = "./dzgui/lib/dayzquery", editable = true }
|
||||
|
||||
[tool.uv]
|
||||
config-settings-package = { pygobject-stubs = { config = "Gtk3,Gdk3,Soup2" } }
|
||||
config-settings-package = { pygobject-stubs = { config = "Gtk3,Gdk3" } }
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
markers = [
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import tomllib
|
||||
from shared import get_git_root
|
||||
from typing import Any
|
||||
|
||||
|
||||
def get_entrypoint() -> None:
|
||||
def get_entrypoint() -> Any:
|
||||
root = get_git_root()
|
||||
p = root.joinpath("pyproject.toml")
|
||||
with open(p, "rb") as f:
|
||||
|
||||
+3
-3
@@ -52,7 +52,7 @@ def untar(file: str, glob: str, renamed: str) -> None:
|
||||
os.rename(_dir, build_dir.joinpath(renamed))
|
||||
|
||||
|
||||
def install_deps() -> None:
|
||||
def install_deps() -> int:
|
||||
pip = build_dir.joinpath(f"{CPYTHON_BUILD}/bin/pip")
|
||||
proc = subprocess.run([pip, "install", root])
|
||||
return proc.returncode
|
||||
@@ -65,12 +65,12 @@ def repackage() -> None:
|
||||
tar.add(build_dir.joinpath(CPYTHON_BUILD), arcname="python")
|
||||
|
||||
|
||||
def rebuild_cpython() -> None:
|
||||
def rebuild_cpython() -> str:
|
||||
if cpython_dir.is_dir() is False:
|
||||
get_cpython()
|
||||
code = install_deps()
|
||||
if code != 0:
|
||||
return
|
||||
return ""
|
||||
repackage()
|
||||
return get_packaged_version()
|
||||
|
||||
|
||||
+2
-2
@@ -92,8 +92,8 @@ with tarfile.open(tarpath, "w:gz") as tar:
|
||||
tar.add(subfolder, arcname=appname, filter=update_uname)
|
||||
print(f"Wrote tarfile to '{tarpath}'")
|
||||
|
||||
proc = subprocess.run([release_exe, "-v"], capture_output=True, text=True)
|
||||
assert proc.stdout.rstrip() == version
|
||||
built_proc = subprocess.run([release_exe, "-v"], capture_output=True, text=True)
|
||||
assert str(built_proc.stdout).rstrip() == version
|
||||
|
||||
# TODO: clean up staging directory
|
||||
release_exe.unlink()
|
||||
|
||||
+10
-4
@@ -8,7 +8,13 @@ def get_image_path() -> Path:
|
||||
return get_git_root().joinpath("dzgui/data/images/example.png")
|
||||
|
||||
|
||||
p = get_image_path()
|
||||
app = build_gui("", False)
|
||||
app.set_automation_param("screenshot_path", p)
|
||||
app.run(None)
|
||||
def take_screenshot() -> None:
|
||||
p = get_image_path()
|
||||
app = build_gui("", False)
|
||||
if app is None:
|
||||
return
|
||||
app.set_automation_param("screenshot_path", p)
|
||||
app.run(None)
|
||||
|
||||
|
||||
take_screenshot()
|
||||
+7
-10
@@ -3,14 +3,11 @@ import subprocess
|
||||
import sys
|
||||
|
||||
cmd = ["git", "ls-files", "--others", "--exclude-standard"]
|
||||
result = subprocess.Popen(
|
||||
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
|
||||
result = subprocess.run(
|
||||
cmd, capture_output=True, text=True
|
||||
)
|
||||
with open(".gitignore", "r") as f:
|
||||
ignored = [line.rstrip() for line in f]
|
||||
|
||||
untracked = [os.path.basename(line.rstrip()) for line in result.stdout]
|
||||
for file in untracked:
|
||||
if not any(file in item for item in ignored):
|
||||
print(f"File '{file}' is untracked. Add to .gitignore file if this is intended.")
|
||||
sys.exit(1)
|
||||
untracked = [os.path.basename(line.rstrip()) for line in result.stdout.splitlines()]
|
||||
if bool(untracked):
|
||||
print(f"The following files are untracked. Add to .gitignore if this is intended.")
|
||||
print("\n".join(untracked))
|
||||
sys.exit(1)
|
||||
Vendored
+4
-2
@@ -1,5 +1,7 @@
|
||||
from pathlib import Path
|
||||
|
||||
CWD = Path(__file__).parent
|
||||
def fixture_path(fixture: str) -> str:
|
||||
return str(CWD / fixture)
|
||||
|
||||
|
||||
def fixture_path(fixture: str) -> Path:
|
||||
return Path(CWD).joinpath(fixture)
|
||||
@@ -1,6 +1,7 @@
|
||||
import pytest
|
||||
|
||||
from _pytest.monkeypatch import MonkeyPatch
|
||||
from collections.abc import Iterator
|
||||
from pathlib import Path
|
||||
|
||||
from dzgui.api.steam import get_app_allows_downloads, get_last_updated
|
||||
@@ -36,30 +37,32 @@ def steam_path() -> Path:
|
||||
|
||||
|
||||
@pytest.fixture(scope="module", autouse=True)
|
||||
def patch_api() -> None:
|
||||
def patch_api() -> Iterator[None]:
|
||||
mp = MonkeyPatch()
|
||||
mp.setattr("dzgui.api.steam.get_app_path", mock_path)
|
||||
yield
|
||||
mp.undo()
|
||||
|
||||
|
||||
def test_no_last_update(steam_path) -> None:
|
||||
def test_no_last_update(steam_path: Path) -> None:
|
||||
assert get_last_updated(steam_path, 555) is None
|
||||
|
||||
|
||||
def test_last_update(steam_path) -> None:
|
||||
def test_last_update(steam_path: Path) -> None:
|
||||
assert get_last_updated(steam_path, 444) == "2026-10-22 00:09:33"
|
||||
|
||||
|
||||
def test_app_allows_downloads(steam_path) -> None:
|
||||
def test_app_allows_downloads(steam_path: Path) -> None:
|
||||
assert get_app_allows_downloads(steam_path, 111) is True
|
||||
|
||||
|
||||
def test_app_disallows_downloads(steam_path):
|
||||
def test_app_disallows_downloads(steam_path: Path) -> None:
|
||||
assert get_app_allows_downloads(steam_path, 222) is False
|
||||
|
||||
|
||||
def test_app_delegates_downloads(monkeypatch, steam_path, client_allows):
|
||||
def test_app_delegates_downloads(
|
||||
monkeypatch: MonkeyPatch, steam_path: Path, client_allows: Path
|
||||
) -> None:
|
||||
monkeypatch.setattr("dzgui.api.steam.get_config", mock_config_disallows)
|
||||
assert get_app_allows_downloads(steam_path, 333) is False
|
||||
monkeypatch.setattr("dzgui.api.steam.get_config", mock_config_allows)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import pytest
|
||||
import tempfile
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from dzgui.app_init import copy_bare_configs
|
||||
from dzgui.config.xdg import get_xdg_paths, parse_filepaths
|
||||
|
||||
@@ -8,9 +10,13 @@ from dzgui.config.xdg import get_xdg_paths, parse_filepaths
|
||||
CONF_STRING = "DZGUI_CONF\n"
|
||||
STATE_STRING = "DZGUI_STATE\n"
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from _pytest.monkeypatch import MonkeyPatch
|
||||
from dzgui.config.xdg import Xdg
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def state_files():
|
||||
def state_files() -> list[str]:
|
||||
state = [
|
||||
"dzg.history",
|
||||
"dzg.versions",
|
||||
@@ -25,7 +31,7 @@ def state_files():
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def xdg_paths(monkeypatch):
|
||||
def xdg_paths(monkeypatch: "MonkeyPatch") -> "Xdg":
|
||||
routes = {
|
||||
"XDG_CONFIG_HOME": "",
|
||||
"XDG_STATE_HOME": "",
|
||||
@@ -42,7 +48,7 @@ def xdg_paths(monkeypatch):
|
||||
|
||||
|
||||
@pytest.mark.mods
|
||||
def test_config_file_import(xdg_paths, state_files):
|
||||
def test_config_file_import(xdg_paths: "Xdg", state_files: list[str]) -> None:
|
||||
|
||||
# NOTE: write bare files in root
|
||||
tmp_conf = xdg_paths.config.parent.parent
|
||||
@@ -67,7 +73,7 @@ def test_config_file_import(xdg_paths, state_files):
|
||||
|
||||
|
||||
@pytest.mark.mods
|
||||
def test_config_file_no_import(xdg_paths, state_files):
|
||||
def test_config_file_no_import(xdg_paths: "Xdg", state_files: list[str]) -> None:
|
||||
for subdir in xdg_paths.config.parent, xdg_paths.resolution.parent:
|
||||
subdir.mkdir()
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from pathlib import Path
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def changelog(request) -> None:
|
||||
def changelog(request: pytest.FixtureRequest) -> str:
|
||||
root = request.config.rootpath
|
||||
changelog = Path(root).joinpath("CHANGELOG.md").read_text()
|
||||
return changelog
|
||||
@@ -19,7 +19,7 @@ def count_hash(line: str) -> int:
|
||||
return cnt
|
||||
|
||||
|
||||
def test_changelog_prefix(changelog) -> None:
|
||||
def test_changelog_prefix(changelog: str) -> None:
|
||||
r = r".*(\[.*\]).*"
|
||||
lines = changelog.splitlines()
|
||||
sort = sorted(lines)
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import pytest
|
||||
|
||||
from typing import Any, TYPE_CHECKING
|
||||
|
||||
from dzgui.const.constants import FLATPAK_RUN_CMD, STEAM_CMD
|
||||
from dzgui.controllers.mc import Controller
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from _pytest.monkeypatch import MonkeyPatch
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"value, expected", [(FLATPAK_RUN_CMD, "Flatpak"), (STEAM_CMD, "Steam")]
|
||||
)
|
||||
def test_human_name(monkeypatch, value, expected) -> None:
|
||||
def mock_query_config(*args) -> str:
|
||||
def test_human_name(monkeypatch: "MonkeyPatch", value: str, expected: str) -> None:
|
||||
def mock_query_config(*args: Any) -> str:
|
||||
return value
|
||||
|
||||
monkeypatch.setattr(
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import gi
|
||||
import pytest
|
||||
import subprocess
|
||||
|
||||
from dzgui.const.constants import GIO_SETTINGS_KEY
|
||||
from dzgui.views.mixins.colorscheme import ColorAwareApp
|
||||
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, Gio, GLib # noqa
|
||||
from gi.repository import Gtk, GLib
|
||||
|
||||
|
||||
class DummyWidget(ColorAwareApp, Gtk.Button):
|
||||
class DummyWidget(ColorAwareApp, Gtk.Button): # type: ignore
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
@@ -27,7 +26,7 @@ def widget() -> DummyWidget:
|
||||
|
||||
|
||||
@pytest.mark.parametrize("key, expect", [("default", False), ("prefer-dark", True)])
|
||||
def test_light_mode(widget, key: str, expect: bool) -> None:
|
||||
def test_light_mode(widget: DummyWidget, key: str, expect: bool) -> None:
|
||||
local_pref = subprocess.run(
|
||||
["gsettings", "get", "org.gnome.desktop.interface", "color-scheme"],
|
||||
capture_output=True,
|
||||
|
||||
+21
-6
@@ -1,33 +1,48 @@
|
||||
import pytest
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from dzgui.init.migrate import convert_cols_file
|
||||
from tests.fixtures import fixture_path
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def columns_with_perspective():
|
||||
def columns_with_perspective() -> "Path":
|
||||
return fixture_path("columns_1")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def columns_without_perspective():
|
||||
def columns_without_perspective() -> "Path":
|
||||
return fixture_path("columns_2")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def columns_with_view():
|
||||
def columns_with_view() -> "Path":
|
||||
return fixture_path("columns_3")
|
||||
|
||||
|
||||
@pytest.mark.config
|
||||
def test_columns_with_perspective(columns_with_perspective):
|
||||
def test_columns_with_perspective(columns_with_perspective: "Path") -> None:
|
||||
j = convert_cols_file(columns_with_perspective)
|
||||
if j is None:
|
||||
return
|
||||
assert "View" in j["cols"]
|
||||
assert "Max" in j["cols"]
|
||||
|
||||
|
||||
@pytest.mark.config
|
||||
def test_columns_without_perspective(columns_without_perspective):
|
||||
def test_columns_without_perspective(columns_without_perspective: "Path") -> None:
|
||||
j = convert_cols_file(columns_without_perspective)
|
||||
if j is None:
|
||||
return
|
||||
assert "View" not in j["cols"]
|
||||
assert "Max" not in j["cols"]
|
||||
|
||||
|
||||
@pytest.mark.config
|
||||
def test_columns_with_view(columns_with_view):
|
||||
def test_columns_with_view(columns_with_view: "Path") -> None:
|
||||
j = convert_cols_file(columns_with_view)
|
||||
assert j is None
|
||||
+17
-12
@@ -1,25 +1,30 @@
|
||||
import json
|
||||
import pytest
|
||||
|
||||
from typing import Any, TYPE_CHECKING
|
||||
|
||||
from dzgui.const.boilerplate import config_boilerplate
|
||||
from dzgui.config.query import get_config
|
||||
from dzgui.config.xdg import get_xdg_paths, parse_filepaths
|
||||
from dzgui.config import convert
|
||||
from tests.fixtures import fixture_path
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def legacy_config():
|
||||
def legacy_config() -> "Path":
|
||||
return fixture_path("dztuirc_1")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def unset_values():
|
||||
def unset_values() -> "Path":
|
||||
return fixture_path("dztuirc_3")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def keys():
|
||||
def keys() -> list[str]:
|
||||
return [
|
||||
"fav_server",
|
||||
"fav_label",
|
||||
@@ -36,7 +41,7 @@ def keys():
|
||||
|
||||
# TODO: use a static fixture instead of system config
|
||||
@pytest.fixture
|
||||
def config():
|
||||
def config() -> Any:
|
||||
paths = get_xdg_paths()
|
||||
xdg = parse_filepaths(paths)
|
||||
return get_config(xdg.config)
|
||||
@@ -44,21 +49,21 @@ def config():
|
||||
|
||||
@pytest.mark.post_install
|
||||
@pytest.mark.realconfig
|
||||
def test_invalid_config_value(config):
|
||||
def test_invalid_config_value(config: Any) -> None:
|
||||
with pytest.raises(Exception):
|
||||
assert config["foo"] is None
|
||||
|
||||
|
||||
@pytest.mark.post_install
|
||||
@pytest.mark.realconfig
|
||||
def test_default_config_values(keys, config):
|
||||
def test_default_config_values(keys: list[str], config: Any) -> None:
|
||||
for key in keys:
|
||||
assert config[key] is not None
|
||||
|
||||
|
||||
@pytest.mark.post_install
|
||||
@pytest.mark.realconfig
|
||||
def test_contains_invalid_values(keys, config):
|
||||
def test_contains_invalid_values(keys: list[str], config: Any) -> None:
|
||||
for key in config:
|
||||
assert key in keys
|
||||
|
||||
@@ -70,15 +75,15 @@ def test_contains_invalid_values(keys, config):
|
||||
("dztuirc_2", (False, False, False)),
|
||||
],
|
||||
)
|
||||
def test_bool_conversion(fixture, expect):
|
||||
fixture = fixture_path(fixture)
|
||||
j = convert.rc2json(fixture)
|
||||
def test_bool_conversion(fixture: str, expect: tuple[bool, bool, bool]) -> None:
|
||||
fixture_p = fixture_path(fixture)
|
||||
j = convert.rc2json(fixture_p)
|
||||
j = json.loads(j)
|
||||
assert j["fullscreen"] == expect[0]
|
||||
assert not j["use_miles"]
|
||||
|
||||
|
||||
def test_key_conversion(legacy_config):
|
||||
def test_key_conversion(legacy_config: "Path") -> None:
|
||||
j = convert.rc2json(legacy_config)
|
||||
j = json.loads(j)
|
||||
keys = [
|
||||
@@ -92,7 +97,7 @@ def test_key_conversion(legacy_config):
|
||||
assert key not in j
|
||||
|
||||
|
||||
def test_missing_values(unset_values):
|
||||
def test_missing_values(unset_values: "Path") -> None:
|
||||
j = convert.rc2json(unset_values)
|
||||
j = json.loads(j)
|
||||
assert j.keys() == config_boilerplate.keys()
|
||||
@@ -1,6 +1,8 @@
|
||||
import configparser
|
||||
import pytest
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from dzgui.const.constants import FLATPAK_APPID, GTK_APP_ID, TMP_PATH
|
||||
from dzgui.config.xdg import get_xdg_paths
|
||||
from dzgui.util.flatpak import apply_overrides
|
||||
@@ -13,17 +15,17 @@ pytestmark = pytest.mark.flatpak
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def xdg_paths() -> dict[str, str]:
|
||||
def xdg_paths() -> dict[str, Path]:
|
||||
return get_xdg_paths()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def override(xdg_paths) -> None:
|
||||
def override(xdg_paths: dict[str, Path]) -> None:
|
||||
apply_overrides(xdg_paths)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def config(xdg_paths) -> configparser.ConfigParser:
|
||||
def config(xdg_paths: dict[str, Path]) -> configparser.ConfigParser:
|
||||
conf_path = xdg_paths["XDG_DATA_HOME"].parent.joinpath(
|
||||
f"flatpak/overrides/{FLATPAK_APPID}"
|
||||
)
|
||||
@@ -32,14 +34,16 @@ def config(xdg_paths) -> configparser.ConfigParser:
|
||||
return config
|
||||
|
||||
|
||||
def test_session_bus(override, config) -> None:
|
||||
def test_session_bus(override: None, config: configparser.ConfigParser) -> None:
|
||||
bus = config[BUS_KEY][GTK_APP_ID]
|
||||
assert bus == "own"
|
||||
|
||||
|
||||
def test_paths(override, xdg_paths, config) -> None:
|
||||
expect: list[str] = []
|
||||
expect.append(TMP_PATH)
|
||||
def test_paths(
|
||||
override: None, xdg_paths: dict[str, Path], config: configparser.ConfigParser
|
||||
) -> None:
|
||||
expect: list[Path] = []
|
||||
expect.append(Path(TMP_PATH))
|
||||
expect.extend([*xdg_paths.values()])
|
||||
paths = config[PATHS_KEY][PATHS_SUBKEY].split(";")
|
||||
assert all(str(path) in paths for path in expect)
|
||||
@@ -2,10 +2,11 @@ import pytest
|
||||
|
||||
from dzgui.const.constants import FLATPAK_RUN_CMD, FLATPAK_SANDBOX
|
||||
from dzgui.const.enum import Preferences
|
||||
from dzgui.controllers.mc import Controller
|
||||
from dzgui.views.pages.options import FlatpakToggleField
|
||||
|
||||
|
||||
class MockController:
|
||||
class MockController(Controller):
|
||||
def __init__(self) -> None:
|
||||
self.command: str
|
||||
|
||||
|
||||
Loaded 100 of 120 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user