mirror of
https://github.com/EDCD/EDMarketConnector.git
synced 2025-04-17 01:22:19 +03:00
Merge branch 'beta'
This commit is contained in:
commit
173300c45c
21
.editorconfig
Normal file
21
.editorconfig
Normal file
@ -0,0 +1,21 @@
|
||||
# This is the project top-level .editorconfig
|
||||
root = true
|
||||
|
||||
# Defaults for all file types
|
||||
[*]
|
||||
# 4-space indents, no TABs
|
||||
indent_style = space
|
||||
tab_width = 4
|
||||
indent_size = tab
|
||||
|
||||
# Hard-wrap at 120 columns
|
||||
max_line_length = 119
|
||||
|
||||
# Windows EOL, for historical reasons
|
||||
end_of_line = crlf
|
||||
|
||||
# UTF-8 is the only sensible option, no BOM
|
||||
charset = utf-8
|
||||
|
||||
# All files should have a final newline
|
||||
insert_final_newline = true
|
6
.github/workflows/pr-checks.yml
vendored
6
.github/workflows/pr-checks.yml
vendored
@ -52,10 +52,10 @@ jobs:
|
||||
####################################################################
|
||||
# Get Python set up
|
||||
####################################################################
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@v2.3.0
|
||||
- name: Set up Python 3.10
|
||||
uses: actions/setup-python@v2.3.2
|
||||
with:
|
||||
python-version: 3.9
|
||||
python-version: "3.10"
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
|
6
.github/workflows/push-checks.yml
vendored
6
.github/workflows/push-checks.yml
vendored
@ -20,10 +20,10 @@ jobs:
|
||||
- uses: actions/checkout@v2.4.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@v2.3.0
|
||||
- name: Set up Python 3.10
|
||||
uses: actions/setup-python@v2.3.2
|
||||
with:
|
||||
python-version: 3.9
|
||||
python-version: "3.10"
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
|
82
.github/workflows/submodule-update.yml
vendored
Normal file
82
.github/workflows/submodule-update.yml
vendored
Normal file
@ -0,0 +1,82 @@
|
||||
---
|
||||
name: Submodule Updates
|
||||
|
||||
on:
|
||||
# We might want this on a schedule once this is in `main`
|
||||
push:
|
||||
branches: [ develop ]
|
||||
|
||||
jobs:
|
||||
check_submodules:
|
||||
name: Pull Request for updated submodules
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
PARENT_REPOSITORY: 'EDCD/EDMarketConnector'
|
||||
CHECKOUT_BRANCH: 'develop'
|
||||
PR_AGAINST_BRANCH: 'develop'
|
||||
OWNER: 'EDCD'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2.4.0
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Update submodules
|
||||
shell: bash
|
||||
run: |
|
||||
git config user.name github-actions
|
||||
git config user.email github-actions@github.com
|
||||
git submodule update --remote
|
||||
|
||||
- name: Check for changes
|
||||
id: check_for_changes
|
||||
continue-on-error: true
|
||||
run: |
|
||||
changes=$(git status --porcelain)
|
||||
if [ -n "${changes}" ];
|
||||
then
|
||||
echo '::set-output changes=true'
|
||||
fi
|
||||
echo '::set-output changes=false'
|
||||
exit 0
|
||||
|
||||
- name: Create submodules changes branch
|
||||
if: steps.check_for_changes.outputs.changes == 'true'
|
||||
run: |
|
||||
git checkout -b $GITHUB_RUN_ID
|
||||
git commit -am "updating submodules"
|
||||
git push --set-upstream origin $GITHUB_RUN_ID
|
||||
|
||||
- name: Create pull request against target branch
|
||||
if: steps.check_for_changes.outputs.changes == 'true'
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
github-token: ${{ inputs.github_token }}
|
||||
script: |
|
||||
await github.rest.pulls.create({
|
||||
owner: '${{ inputs.owner }}',
|
||||
repo: '${{ inputs.parent_repository }}'.split('/')[1].trim(),
|
||||
head: process.env.GITHUB_RUN_ID,
|
||||
base: '${{ inputs.pr_against_branch }}',
|
||||
title: `[Auto-generated] Submodule Updates ${process.env.GITHUB_RUN_ID}`,
|
||||
body: `[Auto-generated] Submodule Updates ${process.env.GITHUB_RUN_ID}`,
|
||||
});
|
||||
|
||||
- name: Add labels
|
||||
if: steps.check_for_changes.outputs.changes == 'true'
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
github-token: ${{ inputs.github_token }}
|
||||
script: |
|
||||
const res = await github.rest.issues.listForRepo({
|
||||
owner: '${{ inputs.owner }}',
|
||||
repo: '${{ inputs.parent_repository }}'.split('/')[1].trim(),
|
||||
});
|
||||
const pr = res.data.filter(i => i.title.includes(process.env.GITHUB_RUN_ID));
|
||||
const prNumber = pr[0].number;
|
||||
await github.rest.issues.addLabels({
|
||||
issue_number: prNumber,
|
||||
owner: '${{ inputs.owner }}',
|
||||
repo: '${{ inputs.parent_repository }}'.split('/')[1].trim(),
|
||||
labels: ['${{ inputs.label }}']
|
||||
});
|
7
.github/workflows/windows-build.yml
vendored
7
.github/workflows/windows-build.yml
vendored
@ -17,9 +17,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2.4.0
|
||||
- uses: actions/setup-python@v2.3.0
|
||||
with:
|
||||
python-version: "3.9.9"
|
||||
submodules: true
|
||||
|
||||
- uses: actions/setup-python@v2.3.2
|
||||
with:
|
||||
python-version: "3.10.2"
|
||||
architecture: "x86"
|
||||
|
||||
- name: Install python tools
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -23,3 +23,5 @@ venv
|
||||
htmlcov/
|
||||
.ignored
|
||||
.coverage
|
||||
EDMarketConnector.wxs
|
||||
wix/components.wxs
|
||||
|
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -1,3 +1,6 @@
|
||||
[submodule "coriolis-data"]
|
||||
path = coriolis-data
|
||||
url = git@github.com:EDCD/coriolis-data.git
|
||||
[submodule "FDevIDs"]
|
||||
path = FDevIDs
|
||||
url = https://github.com/EDCD/FDevIDs.git
|
||||
|
@ -2,3 +2,4 @@
|
||||
follow_imports = skip
|
||||
ignore_missing_imports = True
|
||||
scripts_are_modules = True
|
||||
; platform = darwin
|
||||
|
@ -1,13 +1,19 @@
|
||||
# See https://pre-commit.com for more information
|
||||
# See https://pre-commit.com/hooks.html for more hooks
|
||||
repos:
|
||||
# This can be used to check how pre-commit is being run
|
||||
# - repo: meta
|
||||
# hooks:
|
||||
# - id: identity
|
||||
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: 'v3.4.0'
|
||||
rev: 'v4.1.0'
|
||||
hooks:
|
||||
- id: check-merge-conflict
|
||||
- id: debug-statements
|
||||
- id: end-of-file-fixer
|
||||
- id: mixed-line-ending
|
||||
- id: check-yaml
|
||||
|
||||
#- repo: https://github.com/pre-commit/mirrors-autopep8
|
||||
# rev: ''
|
||||
@ -30,7 +36,7 @@ repos:
|
||||
types: [ python ]
|
||||
|
||||
- repo: https://github.com/pre-commit/pygrep-hooks
|
||||
rev: 'v1.8.0'
|
||||
rev: 'v1.9.0'
|
||||
hooks:
|
||||
- id: python-no-eval
|
||||
- id: python-no-log-warn
|
||||
@ -40,10 +46,13 @@ repos:
|
||||
# mypy - static type checking
|
||||
# mypy --follow-imports skip <file>
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: 'v0.812'
|
||||
rev: 'v0.931'
|
||||
hooks:
|
||||
- id: mypy
|
||||
args: [ "--follow-imports", "skip", "--ignore-missing-imports", "--scripts-are-modules" ]
|
||||
# verbose: true
|
||||
# log_file: 'pre-commit_mypy.log'
|
||||
additional_dependencies: [ types-requests ]
|
||||
# args: [ "--follow-imports", "skip", "--ignore-missing-imports", "--scripts-are-modules" ]
|
||||
|
||||
### # pydocstyle.exe <file>
|
||||
### - repo: https://github.com/FalconSocial/pre-commit-mirrors-pep257
|
||||
@ -59,12 +68,13 @@ repos:
|
||||
|
||||
# safety.exe check -r requirements.txt
|
||||
- repo: https://github.com/Lucas-C/pre-commit-hooks-safety
|
||||
rev: 'v1.2.1'
|
||||
rev: 'v1.2.3'
|
||||
hooks:
|
||||
- id: python-safety-dependencies-check
|
||||
entry: safety
|
||||
args: [check, --bare, -r]
|
||||
args: [check, --bare, --file]
|
||||
language: system
|
||||
files: requirements-dev.txt, requirements.txt
|
||||
|
||||
# Check translation comments are up to date
|
||||
- repo: local
|
||||
@ -77,7 +87,7 @@ repos:
|
||||
always_run: true
|
||||
|
||||
default_language_version:
|
||||
python: python3.9
|
||||
python: python3.10
|
||||
|
||||
default_stages: [ commit, push ]
|
||||
|
||||
|
@ -1 +1 @@
|
||||
3.9.9
|
||||
3.10.2
|
||||
|
237
ChangeLog.md
237
ChangeLog.md
@ -9,11 +9,11 @@ produce the Windows executables and installer.
|
||||
|
||||
---
|
||||
|
||||
* We now test against, and package with, Python 3.9.9.
|
||||
* We now test against, and package with, Python 3.10.2.
|
||||
|
||||
**As a consequence of this we no longer support Windows 7.
|
||||
This is due to
|
||||
[Python 3.9.x itself not supporting Windows 7](https://www.python.org/downloads/windows/).
|
||||
[Python 3.10.x itself not supporting Windows 7](https://www.python.org/downloads/windows/).
|
||||
The application (both EDMarketConnector.exe and EDMC.exe) will crash on
|
||||
startup due to a missing DLL.**
|
||||
|
||||
@ -27,18 +27,243 @@ produce the Windows executables and installer.
|
||||
|
||||
---
|
||||
|
||||
Release 5.3.0
|
||||
===
|
||||
|
||||
As has sadly become routine now, please read
|
||||
[our statement about malware false positives](https://github.com/EDCD/EDMarketConnector/wiki/Troubleshooting#installer-and-or-executables-flagged-as-malicious-viruses)
|
||||
affecting our installers and/or the files they contain. We are as confident
|
||||
as we can be, without detailed auditing of python.org's releases and all
|
||||
the py2exe source and releases, that there is no malware in the files we make
|
||||
available.
|
||||
|
||||
This release is primarily aimed at fixing some more egregious bugs,
|
||||
shortcomings and annoyances with the application. It also adds support for
|
||||
two additional
|
||||
[EDDN](https://github.com/EDCD/EDDN/blob/live/README.md)
|
||||
schemas.
|
||||
|
||||
* We now test and build using Python 3.10.2. We do not *yet* make use of any
|
||||
features specific to Python 3.10 (or 3.9). Let us restate that we
|
||||
absolutely reserve the right to commence doing so.
|
||||
|
||||
* We now set a custom User-Agent header in all web requests, i.e. to EDDN,
|
||||
EDSM and the like. This is of the form:
|
||||
|
||||
`EDCD-EDMarketConnector-<version>`
|
||||
|
||||
* "File" -> "Status" will now show the new Odyssey ranks, both the new
|
||||
categories and the new 'prestige' ranks, e.g. 'Elite I'.
|
||||
|
||||
**NB: Due to an oversight there are currently no translations for these.**
|
||||
|
||||
Closes [#1369](https://github.com/EDCD/EDMarketConnector/issues/1369).
|
||||
|
||||
* Running `EDMarketConnector.exe --reset-ui` will now also reset any changes to
|
||||
the application "UI Scale" or geometry (position and size).
|
||||
|
||||
Closes [#1155](https://github.com/EDCD/EDMarketConnector/issues/1155).
|
||||
|
||||
* We now use UTC-based timestamps in the application's log files. Prior to
|
||||
this change it was the "local time", but without any indication of the
|
||||
applied timezone. Each line's timestamp has ` UTC` as a suffix now. We
|
||||
are assuming that your local clock is correct *and* the timezone is set
|
||||
correctly, such that Python's `time.gmtime()` yields UTC times.
|
||||
|
||||
This should make it easier to correlate application logfiles with in-game
|
||||
time and/or third-party service timestamps.
|
||||
|
||||
* The process used to build the Windows installers should now always pick up
|
||||
all the necessary files automatically. Prior to this we used a manual
|
||||
process to update the installer configuration which was prone to both user
|
||||
error and neglecting to update it as necessary.
|
||||
|
||||
* If the application fails to load valid data from the `NavRoute.json` file
|
||||
when processing a Journal `NavRoute` event, it will attempt to retry this
|
||||
operation a number of times as it processes subsequent Journal events.
|
||||
|
||||
This should hopefully work around a race condition where the game might
|
||||
not have yet updated `NavRoute.json` at all, or has truncated it to empty,
|
||||
when we first attempt this.
|
||||
|
||||
We will also now *NOT* attempt to load `NavRoute.json` during the startup
|
||||
'Journal catch-up' mode, which only sets internal state.
|
||||
|
||||
Closes [#1348](https://github.com/EDCD/EDMarketConnector/issues/1155).
|
||||
|
||||
* Inara: Use the `<journal log>->Statistics->Bank_Account->Current_Wealth`
|
||||
value when sending a `setCommanderCredits` message to Inara to set
|
||||
`commanderAssets`.
|
||||
|
||||
In addition, a `setCommanderCredits` message at game login **will now only
|
||||
ever be sent at game login**. Yes, you will **NEED** to relog to send an
|
||||
updated balance. This is the only way in which to sanely keep the
|
||||
'Total Assets' value on Inara from bouncing around.
|
||||
|
||||
Refer to [Inara:API:docs:setCommanderCredits](https://inara.cz/inara-api-docs/#event-1).
|
||||
|
||||
Closes [#1401](https://github.com/EDCD/EDMarketConnector/issues/1401).
|
||||
|
||||
* Inara: Send a `setCommanderRankPilot` message when the player logs in to the
|
||||
game on-foot. Previously you would *HAVE* to be in a ship at login time
|
||||
for this to be sent.
|
||||
|
||||
Thus, you can now relog on-foot in order to update Inara with any Rank up
|
||||
or progress since the session started.
|
||||
|
||||
Closes [#1378](https://github.com/EDCD/EDMarketConnector/issues/1378).
|
||||
|
||||
* Inara: Fix for always sending a Rank Progress of 0%.
|
||||
|
||||
Closes [#1378](https://github.com/EDCD/EDMarketConnector/issues/1378).
|
||||
|
||||
* Inara: You should once more see updates for any materials used in
|
||||
Engineering. The bug was in our more general Journal event processing
|
||||
code pertaining to `EngineerCraft` events, such that the state passed to
|
||||
the Inara plugin hadn't been updated.
|
||||
|
||||
Such updates should happen 'immediately', but take into account that there
|
||||
can be a delay of up to 35 seconds for any data sent to Inara, due to how
|
||||
we avoid breaking the "2 messages a minute" limit on the Inara API.
|
||||
|
||||
Closes [#1395](https://github.com/EDCD/EDMarketConnector/issues/1395).
|
||||
|
||||
* EDDN: Implement new [approachsettlement/1](https://github.com/EDCD/EDDN/blob/live/schemas/approachsettlement-README.md)
|
||||
schema.
|
||||
|
||||
* EDDN: Implement new [fssallbodiesfound/1](https://github.com/EDCD/EDDN/blob/live/schemas/fssallbodiesfound-README.md)
|
||||
schema.
|
||||
|
||||
* EDDN: We now compress all outgoing messages. This might help get some
|
||||
particularly large `navroute` messages go through.
|
||||
|
||||
If any message is now rejected as 'too large' we will drop it, and thus
|
||||
not retry it later. The application logs will reflect this.
|
||||
|
||||
NB: The EDDN Gateway was updated to allow messages up to 1 MiB in size
|
||||
anyway. The old limit was 100 KiB.
|
||||
|
||||
Closes [#1390](https://github.com/EDCD/EDMarketConnector/issues/1390).
|
||||
|
||||
* EDDN: In an attempt to diagnose some errors observed on the EDDN Gateway
|
||||
with respect to messages sent from this application some additional checks
|
||||
and logging have been added.
|
||||
|
||||
**NB: After some thorough investigation it was concluded that these EDDN
|
||||
errors were likely the result of long-delayed messages due to use of
|
||||
the "Delay sending until docked" option.**
|
||||
|
||||
There should be no functional changes for users. But if you see any of
|
||||
the following in this application's log files **PLEASE OPEN
|
||||
[AN ISSUE ON GITHUB](https://github.com/EDCD/EDMarketConnector/issues/new?assignees=&labels=bug%2C+unconfirmed&template=bug_report.md&title=)
|
||||
with all the requested information**, so that we can correct the relevant
|
||||
code:
|
||||
|
||||
- `No system name in entry, and system_name was not set either! entry: ...`
|
||||
- `BodyName was present but not a string! ...`
|
||||
- `post-processing entry contains entry ...`
|
||||
- `this.body_id was not set properly: ...`
|
||||
- `system is falsey, can't add StarSystem`
|
||||
- `this.coordinates is falsey, can't add StarPos`
|
||||
- `this.systemaddress is falsey, can't add SystemAddress`
|
||||
- `this.status_body_name was not set properly: ...`
|
||||
|
||||
You might also see any of the following in the application status text
|
||||
(bottom of the window):
|
||||
|
||||
- `passed-in system_name is empty, can't add System`
|
||||
- `CodexEntry had empty string, PLEASE ALERT THE EDMC DEVELOPERS`
|
||||
- `system is falsey, can't add StarSystem`
|
||||
- `this.coordinates is falsey, can't add StarPos`
|
||||
- `this.systemaddress is falsey, can't add SystemAddress`
|
||||
|
||||
Ref: [#1403](https://github.com/EDCD/EDMarketConnector/issues/1403)
|
||||
[#1393](https://github.com/EDCD/EDMarketConnector/issues/1393).
|
||||
|
||||
Translations
|
||||
---
|
||||
|
||||
* Use a different workaround for OneSky (translations website) using "zh-Hans"
|
||||
for Chinese (Simplified), whereas Windows will call this "zh-CN". This is
|
||||
in-code and documented with a comment, as opposed to some 'magic' in the
|
||||
Windows Installer configuration that had no such documentation. It's less
|
||||
fragile than relying on that, or developers using a script/documented
|
||||
process to rename the file.
|
||||
|
||||
* As noted above we forgot to upload to
|
||||
[OneSky](https://marginal.oneskyapp.com/collaboration/project/52710)
|
||||
after adding the Odyssey new ranks/categories. This has now been done,
|
||||
and some new phrases await translation.
|
||||
|
||||
Plugin Developers
|
||||
---
|
||||
|
||||
We now test against, and package with Python 3.10.2.
|
||||
|
||||
* We've made no explicit changes to the Python stdlib, or other modules, we
|
||||
currently offer, but we did have to start explicitly including
|
||||
`asyncio` and `multiprocessing` due to using a newer version of `py2exe`
|
||||
for the windows build.
|
||||
|
||||
* We will now include in the Windows installer *all* of the files that `py2exe`
|
||||
places in the build directory. This is vulnerable to a later version of
|
||||
our code, python and/or py2exe no longer causing inclusion of a module.
|
||||
|
||||
We have endeavoured to ensure this release contains *at least* all of the
|
||||
same modules that 5.2.4 did.
|
||||
|
||||
We are looking into
|
||||
[including all of Python stdlib](https://github.com/EDCD/EDMarketConnector/issues/1327),
|
||||
but if there's a particular part of this we don't package then please ask
|
||||
us to by opening an issue on GitHub.
|
||||
|
||||
* We now have an `.editorconfig` file which will instruct your editor/IDE to
|
||||
change some settings pertaining to things like indentation and line wrap,
|
||||
assuming your editor/IDE supports the file.
|
||||
|
||||
See [Contributing.md->Text formatting](Contributing.md#text-formatting).
|
||||
|
||||
* As noted above, prior to this version we weren't properly monitoring
|
||||
`EngineerCraft` events. This caused the `state` passed to plugins to not
|
||||
contain the correct 'materials' (Raw, Manufactured, Encoded) counts.
|
||||
|
||||
* `config.py` has been refactored into a sub-directory, with the per-OS code
|
||||
split into separate files. There *shouldn't* be any changes necessary to
|
||||
how you utilise this, e.g. to determine the application version.
|
||||
|
||||
All forms of any `import` statement that worked before should have
|
||||
unchanged functionality.
|
||||
|
||||
* We now include [FDevIDS](https://github.com/EDCD/FDevIDs) as a
|
||||
sub-repository, and use its files directly for keeping some game data up to
|
||||
date. This should hopefully mean we include, e.g. new ships and modules
|
||||
for loadout exports in a more timely manner.
|
||||
|
||||
Developers of third-party plugins should never have been using these files
|
||||
anyway, so this shouldn't break anything for them.
|
||||
|
||||
* It's unlikely to affect you, but our `requirements-dev.txt` now explicitly
|
||||
cites a specific version of `setuptools`. This was necessary to ensure we
|
||||
have a version that works with `py2exe` for the windows build process.
|
||||
|
||||
If anything this will ensure you have a *more up to date* version of
|
||||
`setuptools` installed.
|
||||
|
||||
---
|
||||
---
|
||||
|
||||
Release 5.2.4
|
||||
===
|
||||
This is a *very* minor update that simply imports the latest versions of
|
||||
This is a *very* minor update that simply imports the latest versions of
|
||||
data files so that some niche functionality works properly.
|
||||
|
||||
* Update `commodity.csv` and `rare_commodity.csv` from the latest
|
||||
[EDCD/FDevIDs](https://github.com/EDCD/FDevIDs) versions. This addresses
|
||||
[EDCD/FDevIDs](https://github.com/EDCD/FDevIDs) versions. This addresses
|
||||
an issue with export of market data in Trade Dangerous format containing
|
||||
`OnionHeadC` rather than the correct name, `Onionhead Gamma Strain`, that
|
||||
`OnionHeadC` rather than the correct name, `Onionhead Gamma Strain`, that
|
||||
Trade Dangerous is expecting.
|
||||
|
||||
This will only have affected Trade Dangerous users who use EDMarketConnector
|
||||
This will only have affected Trade Dangerous users who use EDMarketConnector
|
||||
as a source of market data.
|
||||
|
||||
---
|
||||
|
@ -23,6 +23,16 @@ consistent with our vision for EDMC. Fundamental changes in particular need to b
|
||||
|
||||
---
|
||||
|
||||
## Text formatting
|
||||
|
||||
The project contains an `.editorconfig` file at its root. Please either ensure
|
||||
your editor is taking note of those settings, or cross-check its contents
|
||||
with the
|
||||
[editorconfig documentation](https://github.com/editorconfig/editorconfig/wiki/EditorConfig-Properties)
|
||||
, and ensure your editor/IDE's settings match.
|
||||
|
||||
---
|
||||
|
||||
## General workflow
|
||||
|
||||
1. You will need a GitHub account.
|
||||
@ -181,9 +191,10 @@ only the 'C' (Patch) component.
|
||||
Going forwards we will always use the full [Semantic Version](https://semver.org/#semantic-versioning-specification-semver)
|
||||
and 'folder style' tag names, e.g. `Release/Major.Minor.Patch`.
|
||||
|
||||
Currently the only file that defines the version code-wise is `config.py`.
|
||||
`Changelog.md` and `edmarketconnector.xml` are another matter handled as part
|
||||
of [the release process](docs/Releasing.md#distribution).
|
||||
Currently, the only file that defines the version code-wise is
|
||||
`config/__init__.py`. `Changelog.md` and `edmarketconnector.xml` are another
|
||||
matter handled as part of
|
||||
[the release process](docs/Releasing.md#distribution).
|
||||
|
||||
---
|
||||
|
||||
@ -206,12 +217,18 @@ re-introduce a bug down the line.
|
||||
We use the [`pytest`](https://docs.pytest.org/en/stable/) for unit testing.
|
||||
|
||||
The files for a test should go in a sub-directory of `tests/` named after the
|
||||
(principal) file that contains the code they are testing. e.g. for
|
||||
journal_lock.py the tests are in `tests/journal_lock.py/test_journal_lock.py`.
|
||||
The `test_` prefix on `test_journal_lock.py` is necessary in order for `pytest`
|
||||
to recognise the file as containing tests to be run.
|
||||
(principal) file or directory that contains the code they are testing.
|
||||
For example:
|
||||
|
||||
- Tests for `journal_lock.py` are in
|
||||
`tests/journal_lock.py/test_journal_lock.py`. The `test_` prefix on
|
||||
`test_journal_lock.py` is necessary in order for `pytest` to recognise the
|
||||
file as containing tests to be run.
|
||||
- Tests for `config/` code are located in `tests/config/test_config.py`, not
|
||||
`tests/config.py/test_config.py`
|
||||
|
||||
The sub-directory avoids having a mess of files in `tests`, particularly when
|
||||
there might be supporting files, e.g. `tests/config.py/_old_config.py` or files
|
||||
there might be supporting files, e.g. `tests/config/_old_config.py` or files
|
||||
containing test data.
|
||||
|
||||
Invoking just a bare `pytest` command will run all tests.
|
||||
@ -228,6 +245,42 @@ handy if you want to step through the testing code to be sure of anything.
|
||||
Otherwise, see the [pytest documentation](https://docs.pytest.org/en/stable/contents.html).
|
||||
|
||||
---
|
||||
|
||||
## Imports used only in core plugins
|
||||
|
||||
Because the 'core' plugins, as with any EDMarketConnector plugin, are only ever
|
||||
loaded dynamically, not through an explicit `import` statement, there is no
|
||||
way for `py2exe` to know about them when building the contents of the
|
||||
`dist.win32` directory. See [docs/Releasing.md](docs/Releasing.md) for more
|
||||
information about this build process.
|
||||
|
||||
Thus, you **MUST** check if any imports you add in `plugins/*.py` files are only
|
||||
referenced in that file (or also only in any other core plugin), and if so
|
||||
**YOU MUST ENSURE THAT PERTINENT ADJUSTMENTS ARE MADE IN `setup.py`
|
||||
IN ORDER TO ENSURE THE FILES ARE ACTUALLY PRESENT IN AN END-USER
|
||||
INSTALLATION ON WINDOWS.**
|
||||
|
||||
An exmaple is that as of 2022-02-01 it was noticed that `plugins/eddn.py` now
|
||||
uses `util/text.py`, and is the only code to do so. `py2exe` does not detect
|
||||
this and thus the resulting `dist.win32/library.zip` does not contain the
|
||||
`util/` directory, let alone the `util/text.py` file. The fix was to update
|
||||
the appropriate `packages` definition to:
|
||||
|
||||
```python
|
||||
'packages': [
|
||||
'sqlite3', # Included for plugins
|
||||
'util', # 2022-02-01 only imported in plugins/eddn.py
|
||||
],
|
||||
```
|
||||
|
||||
Note that in this case it's in `packages` because we want the whole directory
|
||||
adding. For a single file an extra item in `includes` would suffice.
|
||||
|
||||
Such additions to `setup.py` should not cause any issues if subsequent project
|
||||
changes cause `py2exe` to automatically pick up the same file(s).
|
||||
|
||||
---
|
||||
|
||||
## Debugging network sends
|
||||
|
||||
Rather than risk sending bad data to a remote service, even if only through
|
||||
@ -484,6 +537,20 @@ Please be verbose here, more info about weird choices is always prefered over ma
|
||||
|
||||
Additionally, if your hack is over around 5 lines, please include a `# HACK END` or similar comment to indicate the end of the hack.
|
||||
|
||||
# Use `sys.platform` for platform guards
|
||||
|
||||
`mypy` (and `pylance`) understand platform guards and will show unreachable code / resolve imports correctly
|
||||
for platform specific things. However, this only works if you directly reference `sys.platform`, importantly
|
||||
the following does not work:
|
||||
|
||||
```py
|
||||
from sys import platform
|
||||
if platform == 'darwin':
|
||||
...
|
||||
```
|
||||
|
||||
It **MUST** be `if sys.platform`.
|
||||
|
||||
---
|
||||
|
||||
## Build process
|
||||
|
@ -46,6 +46,7 @@ from fnmatch import fnmatch
|
||||
# So that any warning about accessing a protected member is only in one place.
|
||||
from sys import _getframe as getframe
|
||||
from threading import get_native_id as thread_native_id
|
||||
from time import gmtime
|
||||
from traceback import print_exc
|
||||
from typing import TYPE_CHECKING, Tuple, cast
|
||||
|
||||
@ -91,6 +92,12 @@ logging.Logger.trace = lambda self, message, *args, **kwargs: self._log( # type
|
||||
**kwargs
|
||||
)
|
||||
|
||||
# MAGIC n/a | 2022-01-20: We want logging timestamps to be in UTC, not least because the game journals log in UTC.
|
||||
# MAGIC-CONT: Note that the game client uses the ED server's idea of UTC, which can easily be different from machine
|
||||
# MAGIC-CONT: local idea of it. So don't expect our log timestamps to perfectly match Journal ones.
|
||||
# MAGIC-CONT: See MAGIC tagged comment in Logger.__init__()
|
||||
logging.Formatter.converter = gmtime
|
||||
|
||||
|
||||
def _trace_if(self: logging.Logger, condition: str, message: str, *args, **kwargs) -> None:
|
||||
if any(fnmatch(condition, p) for p in config_mod.trace_on):
|
||||
@ -162,7 +169,11 @@ class Logger:
|
||||
|
||||
self.logger_formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(process)d:%(thread)d:%(osthreadid)d %(module)s.%(qualname)s:%(lineno)d: %(message)s') # noqa: E501
|
||||
self.logger_formatter.default_time_format = '%Y-%m-%d %H:%M:%S'
|
||||
self.logger_formatter.default_msec_format = '%s.%03d'
|
||||
# MAGIC n/a | 2022-01-20: As of Python 3.10.2 you can *not* use either `%s.%03.d` in default_time_format
|
||||
# MAGIC-CONT: (throws exceptions), *or* use `%Z` in default_time_msec (more exceptions).
|
||||
# MAGIC-CONT: ' UTC' is hard-coded here - we know we're using the local machine's idea of UTC/GMT because we
|
||||
# MAGIC-CONT: cause logging.Formatter() to use `gmtime()` - see MAGIC comment in this file's top-level code.
|
||||
self.logger_formatter.default_msec_format = '%s.%03d UTC'
|
||||
|
||||
self.logger_channel.setFormatter(self.logger_formatter)
|
||||
self.logger.addHandler(self.logger_channel)
|
||||
|
@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Entry point for the main GUI application."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import html
|
||||
@ -14,7 +15,6 @@ import webbrowser
|
||||
from builtins import object, str
|
||||
from os import chdir, environ
|
||||
from os.path import dirname, join
|
||||
from sys import platform
|
||||
from time import localtime, strftime, time
|
||||
from typing import TYPE_CHECKING, Optional, Tuple, Union
|
||||
|
||||
@ -23,7 +23,7 @@ from typing import TYPE_CHECKING, Optional, Tuple, Union
|
||||
# place for things like config.py reading .gitversion
|
||||
if getattr(sys, 'frozen', False):
|
||||
# Under py2exe sys.path[0] is the executable name
|
||||
if platform == 'win32':
|
||||
if sys.platform == 'win32':
|
||||
chdir(dirname(sys.path[0]))
|
||||
# Allow executable to be invoked from any cwd
|
||||
environ['TCL_LIBRARY'] = join(dirname(sys.path[0]), 'lib', 'tcl')
|
||||
@ -74,7 +74,7 @@ if __name__ == '__main__': # noqa: C901
|
||||
###########################################################################
|
||||
parser.add_argument(
|
||||
'--reset-ui',
|
||||
help='reset UI theme and transparency to defaults',
|
||||
help='Reset UI theme, transparency, font, font size, ui scale, and ui geometry to default',
|
||||
action='store_true'
|
||||
)
|
||||
###########################################################################
|
||||
@ -234,7 +234,7 @@ if __name__ == '__main__': # noqa: C901
|
||||
"""Handle any edmc:// auth callback, else foreground existing window."""
|
||||
logger.trace_if('frontier-auth.windows', 'Begin...')
|
||||
|
||||
if platform == 'win32':
|
||||
if sys.platform == 'win32':
|
||||
|
||||
# If *this* instance hasn't locked, then another already has and we
|
||||
# now need to do the edmc:// checks for auth callback
|
||||
@ -370,8 +370,9 @@ if __name__ == '__main__': # noqa: C901
|
||||
# isort: off
|
||||
if TYPE_CHECKING:
|
||||
from logging import TRACE # type: ignore # noqa: F401 # Needed to update mypy
|
||||
import update
|
||||
from infi.systray import SysTrayIcon
|
||||
|
||||
if sys.platform == 'win32':
|
||||
from infi.systray import SysTrayIcon
|
||||
# isort: on
|
||||
|
||||
def _(x: str) -> str:
|
||||
@ -443,7 +444,7 @@ class AppWindow(object):
|
||||
|
||||
self.prefsdialog = None
|
||||
|
||||
if platform == 'win32':
|
||||
if sys.platform == 'win32':
|
||||
from infi.systray import SysTrayIcon
|
||||
|
||||
def open_window(systray: 'SysTrayIcon') -> None:
|
||||
@ -456,8 +457,8 @@ class AppWindow(object):
|
||||
|
||||
plug.load_plugins(master)
|
||||
|
||||
if platform != 'darwin':
|
||||
if platform == 'win32':
|
||||
if sys.platform != 'darwin':
|
||||
if sys.platform == 'win32':
|
||||
self.w.wm_iconbitmap(default='EDMarketConnector.ico')
|
||||
|
||||
else:
|
||||
@ -527,7 +528,7 @@ class AppWindow(object):
|
||||
|
||||
# LANG: Update button in main window
|
||||
self.button = ttk.Button(frame, text=_('Update'), width=28, default=tk.ACTIVE, state=tk.DISABLED)
|
||||
self.theme_button = tk.Label(frame, width=32 if platform == 'darwin' else 28, state=tk.DISABLED)
|
||||
self.theme_button = tk.Label(frame, width=32 if sys.platform == 'darwin' else 28, state=tk.DISABLED)
|
||||
self.status = tk.Label(frame, name='status', anchor=tk.W)
|
||||
|
||||
ui_row = frame.grid_size()[1]
|
||||
@ -540,14 +541,15 @@ class AppWindow(object):
|
||||
theme.button_bind(self.theme_button, self.capi_request_data)
|
||||
|
||||
for child in frame.winfo_children():
|
||||
child.grid_configure(padx=self.PADX, pady=(platform != 'win32' or isinstance(child, tk.Frame)) and 2 or 0)
|
||||
child.grid_configure(padx=self.PADX, pady=(
|
||||
sys.platform != 'win32' or isinstance(child, tk.Frame)) and 2 or 0)
|
||||
|
||||
# The type needs defining for adding the menu entry, but won't be
|
||||
# properly set until later
|
||||
self.updater: update.Updater = None
|
||||
|
||||
self.menubar = tk.Menu()
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
# Can't handle (de)iconify if topmost is set, so suppress iconify button
|
||||
# http://wiki.tcl.tk/13428 and p15 of
|
||||
# https://developer.apple.com/legacy/library/documentation/Carbon/Conceptual/HandlingWindowsControls/windowscontrols.pdf
|
||||
@ -603,7 +605,7 @@ class AppWindow(object):
|
||||
self.help_menu.add_command(command=lambda: not self.HelpAbout.showing and self.HelpAbout(self.w))
|
||||
|
||||
self.menubar.add_cascade(menu=self.help_menu)
|
||||
if platform == 'win32':
|
||||
if sys.platform == 'win32':
|
||||
# Must be added after at least one "real" menu entry
|
||||
self.always_ontop = tk.BooleanVar(value=bool(config.get_int('always_ontop')))
|
||||
self.system_menu = tk.Menu(self.menubar, name='system', tearoff=tk.FALSE)
|
||||
@ -674,11 +676,11 @@ class AppWindow(object):
|
||||
if config.get_str('geometry'):
|
||||
match = re.match(r'\+([\-\d]+)\+([\-\d]+)', config.get_str('geometry'))
|
||||
if match:
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
# http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7
|
||||
if int(match.group(2)) >= 0:
|
||||
self.w.geometry(config.get_str('geometry'))
|
||||
elif platform == 'win32':
|
||||
elif sys.platform == 'win32':
|
||||
# Check that the titlebar will be at least partly on screen
|
||||
import ctypes
|
||||
from ctypes.wintypes import POINT
|
||||
@ -776,7 +778,7 @@ class AppWindow(object):
|
||||
self.suit_shown = True
|
||||
|
||||
if not self.suit_shown:
|
||||
if platform != 'win32':
|
||||
if sys.platform != 'win32':
|
||||
pady = 2
|
||||
|
||||
else:
|
||||
@ -826,7 +828,7 @@ class AppWindow(object):
|
||||
self.system_label['text'] = _('System') + ':' # LANG: Label for 'System' line in main UI
|
||||
self.station_label['text'] = _('Station') + ':' # LANG: Label for 'Station' line in main UI
|
||||
self.button['text'] = self.theme_button['text'] = _('Update') # LANG: Update button in main window
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
self.menubar.entryconfigure(1, label=_('File')) # LANG: 'File' menu title on OSX
|
||||
self.menubar.entryconfigure(2, label=_('Edit')) # LANG: 'Edit' menu title on OSX
|
||||
self.menubar.entryconfigure(3, label=_('View')) # LANG: 'View' menu title on OSX
|
||||
@ -873,7 +875,7 @@ class AppWindow(object):
|
||||
|
||||
self.button['state'] = self.theme_button['state'] = tk.DISABLED
|
||||
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
self.view_menu.entryconfigure(0, state=tk.DISABLED) # Status
|
||||
self.file_menu.entryconfigure(0, state=tk.DISABLED) # Save Raw Data
|
||||
|
||||
@ -887,7 +889,7 @@ class AppWindow(object):
|
||||
# LANG: Successfully authenticated with the Frontier website
|
||||
self.status['text'] = _('Authentication successful')
|
||||
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
self.view_menu.entryconfigure(0, state=tk.NORMAL) # Status
|
||||
self.file_menu.entryconfigure(0, state=tk.NORMAL) # Save Raw Data
|
||||
|
||||
@ -1211,7 +1213,7 @@ class AppWindow(object):
|
||||
companion.session.invalidate()
|
||||
self.login()
|
||||
|
||||
except companion.ServerConnectionError as e:
|
||||
except companion.ServerConnectionError as e: # TODO: unreachable (subclass of ServerLagging -- move to above)
|
||||
logger.warning(f'Exception while contacting server: {e}')
|
||||
err = self.status['text'] = str(e)
|
||||
play_bad = True
|
||||
@ -1429,7 +1431,7 @@ class AppWindow(object):
|
||||
companion.session.auth_callback()
|
||||
# LANG: Successfully authenticated with the Frontier website
|
||||
self.status['text'] = _('Authentication successful')
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
self.view_menu.entryconfigure(0, state=tk.NORMAL) # Status
|
||||
self.file_menu.entryconfigure(0, state=tk.NORMAL) # Save Raw Data
|
||||
|
||||
@ -1570,11 +1572,11 @@ class AppWindow(object):
|
||||
|
||||
# position over parent
|
||||
# http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7
|
||||
if platform != 'darwin' or parent.winfo_rooty() > 0:
|
||||
if sys.platform != 'darwin' or parent.winfo_rooty() > 0:
|
||||
self.geometry(f'+{parent.winfo_rootx():d}+{parent.winfo_rooty():d}')
|
||||
|
||||
# remove decoration
|
||||
if platform == 'win32':
|
||||
if sys.platform == 'win32':
|
||||
self.attributes('-toolwindow', tk.TRUE)
|
||||
|
||||
self.resizable(tk.FALSE, tk.FALSE)
|
||||
@ -1651,7 +1653,7 @@ class AppWindow(object):
|
||||
"""
|
||||
default_extension: str = ''
|
||||
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
default_extension = '.json'
|
||||
|
||||
timestamp: str = strftime('%Y-%m-%dT%H.%M.%S', localtime())
|
||||
@ -1676,7 +1678,7 @@ class AppWindow(object):
|
||||
|
||||
def onexit(self, event=None) -> None:
|
||||
"""Application shutdown procedure."""
|
||||
if platform == 'win32':
|
||||
if sys.platform == 'win32':
|
||||
shutdown_thread = threading.Thread(target=self.systray.shutdown)
|
||||
shutdown_thread.setDaemon(True)
|
||||
shutdown_thread.start()
|
||||
@ -1684,7 +1686,7 @@ class AppWindow(object):
|
||||
config.set_shutdown() # Signal we're in shutdown now.
|
||||
|
||||
# http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7
|
||||
if platform != 'darwin' or self.w.winfo_rooty() > 0:
|
||||
if sys.platform != 'darwin' or self.w.winfo_rooty() > 0:
|
||||
x, y = self.w.geometry().split('+')[1:3] # e.g. '212x170+2881+1267'
|
||||
config.set('geometry', f'+{x}+{y}')
|
||||
|
||||
@ -1865,9 +1867,13 @@ sys.path: {sys.path}'''
|
||||
if args.reset_ui:
|
||||
config.set('theme', 0) # 'Default' theme uses ID 0
|
||||
config.set('ui_transparency', 100) # 100 is completely opaque
|
||||
config.delete('font')
|
||||
config.delete('font_size')
|
||||
logger.info('reset theme, font, font size, and transparency to default.')
|
||||
config.delete('font', suppress=True)
|
||||
config.delete('font_size', suppress=True)
|
||||
|
||||
config.set('ui_scale', 100) # 100% is the default here
|
||||
config.delete('geometry', suppress=True) # unset is recreated by other code
|
||||
|
||||
logger.info('reset theme, transparency, font, font size, ui scale, and ui geometry to default.')
|
||||
|
||||
# We prefer a UTF-8 encoding gets set, but older Windows versions have
|
||||
# issues with this. From Windows 10 1903 onwards we can rely on the
|
||||
|
1
FDevIDs
Submodule
1
FDevIDs
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 9ca62008e93a98dd146bf37cb1e94f2f6c5d2fbf
|
580
L10n/en.template
580
L10n/en.template
@ -1,38 +1,125 @@
|
||||
/* Language name */
|
||||
"!Language" = "English";
|
||||
|
||||
/* companion.py: Frontier CAPI didn't respond; In files: companion.py:220; */
|
||||
"Error: Frontier CAPI didn't respond" = "Error: Frontier CAPI didn't respond";
|
||||
/* eddn.py: Status text shown while attempting to send data; In files: eddn.py:212; eddn.py:584; eddn.py:925; load.py:215; load.py:593; load.py:940; eddn.py:252; eddn.py:699; eddn.py:1439; */
|
||||
"Sending data to EDDN..." = "Sending data to EDDN...";
|
||||
|
||||
/* companion.py: Frontier CAPI data doesn't agree with latest Journal game location; In files: companion.py:239; */
|
||||
"Error: Frontier server is lagging" = "Error: Frontier server is lagging";
|
||||
/* eddn.py: Error while trying to send data to EDDN; In files: eddn.py:257; eddn.py:864; eddn.py:898; eddn.py:937; load.py:260; load.py:878; load.py:913; load.py:952; eddn.py:316; eddn.py:1375; eddn.py:1410; eddn.py:1451; */
|
||||
"Error: Can't connect to EDDN" = "Error: Can't connect to EDDN";
|
||||
|
||||
/* companion.py: Commander is docked at an EDO settlement, got out and back in, we forgot the station; In files: companion.py:255; */
|
||||
"Docked but unknown station: EDO Settlement?" = "Docked but unknown station: EDO Settlement?";
|
||||
/* eddn.py: Enable EDDN support for station data checkbox label; In files: eddn.py:663; load.py:672; eddn.py:1122; */
|
||||
"Send station data to the Elite Dangerous Data Network" = "Send station data to the Elite Dangerous Data Network";
|
||||
|
||||
/* companion.py: Generic "something went wrong with Frontier Auth" error; In files: companion.py:265; */
|
||||
"Error: Invalid Credentials" = "Error: Invalid Credentials";
|
||||
/* eddn.py: Enable EDDN support for system and other scan data checkbox label; In files: eddn.py:673; load.py:682; eddn.py:1133; */
|
||||
"Send system and scan data to the Elite Dangerous Data Network" = "Send system and scan data to the Elite Dangerous Data Network";
|
||||
|
||||
/* companion.py: Frontier CAPI authorisation not for currently game-active commander; In files: companion.py:290; */
|
||||
"Error: Wrong Cmdr" = "Error: Wrong Cmdr";
|
||||
/* eddn.py: EDDN delay sending until docked option is on, this message notes that a send was skipped due to this; In files: eddn.py:683; load.py:692; eddn.py:1144; */
|
||||
"Delay sending until docked" = "Delay sending until docked";
|
||||
|
||||
/* companion.py: Generic error prefix - following text is from Frontier auth service; In files: companion.py:416; companion.py:501; */
|
||||
"Error" = "Error";
|
||||
/* edsm.py: Settings>EDSM - Label on checkbox for 'send data'; In files: edsm.py:197; load.py:197; edsm.py:237; */
|
||||
"Send flight log and Cmdr status to EDSM" = "Send flight log and Cmdr status to EDSM";
|
||||
|
||||
/* companion.py: Frontier auth, no 'usr' section in returned data; companion.py: Frontier auth, no 'customer_id' in 'usr' section in returned data; In files: companion.py:459; companion.py:464; */
|
||||
"Error: Couldn't check token customer_id" = "Error: Couldn't check token customer_id";
|
||||
/* edsm.py: Settings>EDSM - Label on header/URL to EDSM API key page; In files: edsm.py:206; load.py:206; edsm.py:247; */
|
||||
"Elite Dangerous Star Map credentials" = "Elite Dangerous Star Map credentials";
|
||||
|
||||
/* companion.py: Frontier auth customer_id doesn't match game session FID; In files: companion.py:470; */
|
||||
"Error: customer_id doesn't match!" = "Error: customer_id doesn't match!";
|
||||
/* edsm.py: Game Commander name label in EDSM settings; theme.py: Label for commander name in main window; EDMarketConnector.py: Label for commander name in main window; stats.py: Cmdr stats; In files: edsm.py:216; load.py:216; edsm.py:258; theme.py:227; EDMarketConnector.py:822; stats.py:59; */
|
||||
"Cmdr" = "Cmdr";
|
||||
|
||||
/* companion.py: Failed to get Access Token from Frontier Auth service; In files: companion.py:492; */
|
||||
"Error: unable to get token" = "Error: unable to get token";
|
||||
/* edsm.py: EDSM Commander name label in EDSM settings; In files: edsm.py:223; load.py:223; edsm.py:266; */
|
||||
"Commander Name" = "Commander Name";
|
||||
|
||||
/* companion.py: Frontier CAPI returned 418, meaning down for maintenance; In files: companion.py:799; */
|
||||
"Frontier CAPI down for maintenance" = "Frontier CAPI down for maintenance";
|
||||
/* edsm.py: EDSM API key label; inara.py: Inara API key label; In files: edsm.py:230; inara.py:233; load.py:230; load.py:233; edsm.py:274; inara.py:243; */
|
||||
"API Key" = "API Key";
|
||||
|
||||
/* companion.py: Frontier CAPI data retrieval failed; In files: companion.py:811; */
|
||||
"Frontier CAPI query failure" = "Frontier CAPI query failure";
|
||||
/* edsm.py: We have no data on the current commander; prefs.py: No hotkey/shortcut set; stats.py: No rank; In files: edsm.py:256; load.py:256; edsm.py:301; prefs.py:520; prefs.py:1174; prefs.py:1207; stats.py:156; stats.py:175; stats.py:194; stats.py:211; */
|
||||
"None" = "None";
|
||||
|
||||
/* edsm.py: EDSM Plugin - Error message from EDSM API; In files: edsm.py:622; edsm.py:724; load.py:627; load.py:729; edsm.py:747; edsm.py:875; */
|
||||
"Error: EDSM {MSG}" = "Error: EDSM {MSG}";
|
||||
|
||||
/* edsm.py: EDSM Plugin - Error connecting to EDSM API; In files: edsm.py:658; edsm.py:720; load.py:663; load.py:725; edsm.py:784; edsm.py:870; */
|
||||
"Error: Can't connect to EDSM" = "Error: Can't connect to EDSM";
|
||||
|
||||
/* inara.py: Checkbox to enable INARA API Usage; In files: inara.py:215; load.py:215; inara.py:222; */
|
||||
"Send flight log and Cmdr status to Inara" = "Send flight log and Cmdr status to Inara";
|
||||
|
||||
/* inara.py: Text for INARA API keys link ( goes to https://inara.cz/settings-api ); In files: inara.py:225; load.py:225; inara.py:234; */
|
||||
"Inara credentials" = "Inara credentials";
|
||||
|
||||
/* inara.py: INARA API returned some kind of error (error message will be contained in {MSG}); In files: inara.py:1316; inara.py:1328; load.py:1319; load.py:1331; inara.py:1587; inara.py:1600; */
|
||||
"Error: Inara {MSG}" = "Error: Inara {MSG}";
|
||||
|
||||
/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: load.py:51; load.py:51; load.py:88; load.py:104; load.py:110; coriolis.py:52; coriolis.py:55; coriolis.py:101; coriolis.py:117; coriolis.py:123; */
|
||||
"Auto" = "Auto";
|
||||
|
||||
/* coriolis.py: 'Normal' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - normal; In files: load.py:51; load.py:88; load.py:102; coriolis.py:53; coriolis.py:99; coriolis.py:115; */
|
||||
"Normal" = "Normal";
|
||||
|
||||
/* coriolis.py: 'Beta' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - beta; In files: load.py:51; load.py:88; load.py:103; coriolis.py:54; coriolis.py:100; coriolis.py:116; */
|
||||
"Beta" = "Beta";
|
||||
|
||||
/* coriolis.py: Settings>Coriolis: Help/hint for changing coriolis URLs; In files: load.py:64:66; coriolis.py:69:71; */
|
||||
"Set the URL to use with coriolis.io ship loadouts. Note that this MUST end with '/import?data='" = "Set the URL to use with coriolis.io ship loadouts. Note that this MUST end with '/import?data='";
|
||||
|
||||
/* coriolis.py: Settings>Coriolis: Label for 'NOT alpha/beta game version' URL; In files: load.py:69; coriolis.py:75; */
|
||||
"Normal URL" = "Normal URL";
|
||||
|
||||
/* coriolis.py: Generic 'Reset' button label; In files: load.py:71; load.py:78; coriolis.py:78; coriolis.py:87; */
|
||||
"Reset" = "Reset";
|
||||
|
||||
/* coriolis.py: Settings>Coriolis: Label for 'alpha/beta game version' URL; In files: load.py:76; coriolis.py:84; */
|
||||
"Beta URL" = "Beta URL";
|
||||
|
||||
/* In files: load.py:83; */
|
||||
"Override Beta/Normal selection" = "Override Beta/Normal selection";
|
||||
|
||||
/* coriolis.py: Settings>Coriolis - invalid override mode found; In files: load.py:120; coriolis.py:134; */
|
||||
"Invalid Coriolis override mode!" = "Invalid Coriolis override mode!";
|
||||
|
||||
/* eddb.py: Journal Processing disabled due to an active killswitch; In files: load.py:99; eddb.py:100; */
|
||||
"EDDB Journal processing disabled. See Log." = "EDDB Journal processing disabled. See Log.";
|
||||
|
||||
/* eddn.py: Killswitch disabled EDDN; In files: load.py:756; eddn.py:1233; */
|
||||
"EDDN journal handler disabled. See Log." = "EDDN journal handler disabled. See Log.";
|
||||
|
||||
/* edsm.py: EDSM plugin - Journal handling disabled by killswitch; In files: load.py:346; edsm.py:402; */
|
||||
"EDSM Handler disabled. See Log." = "EDSM Handler disabled. See Log.";
|
||||
|
||||
/* inara.py: INARA support disabled via killswitch; In files: load.py:331; inara.py:341; */
|
||||
"Inara disabled. See Log." = "Inara disabled. See Log.";
|
||||
|
||||
/* EDMarketConnector.py: No data was returned for the commander from the Frontier CAPI; In files: test_ast_stuff.py:2; EDMarketConnector.py:1047; */
|
||||
"CAPI: No commander data returned" = "CAPI: No commander data returned";
|
||||
|
||||
/* coriolis.py: Settings>Coriolis: Label for selection of using Normal, Beta or 'auto' Coriolis URL; In files: coriolis.py:94; */
|
||||
"Override Beta/Normal Selection" = "Override Beta/Normal Selection";
|
||||
|
||||
/* eddn.py: EDDN has banned this version of our client; In files: eddn.py:334; */
|
||||
"EDDN Error: EDMC is too old for EDDN. Please update." = "EDDN Error: EDMC is too old for EDDN. Please update.";
|
||||
|
||||
/* eddn.py: EDDN returned an error that indicates something about what we sent it was wrong; In files: eddn.py:340; */
|
||||
"EDDN Error: Validation Failed (EDMC Too Old?). See Log" = "EDDN Error: Validation Failed (EDMC Too Old?). See Log";
|
||||
|
||||
/* eddn.py: EDDN returned some sort of HTTP error, one we didn't expect. {STATUS} contains a number; In files: eddn.py:345; */
|
||||
"EDDN Error: Returned {STATUS} status code" = "EDDN Error: Returned {STATUS} status code";
|
||||
|
||||
/* eddn.py: No 'Route' found in NavRoute.json file; In files: eddn.py:971; */
|
||||
"No 'Route' array in NavRoute.json contents" = "No 'Route' array in NavRoute.json contents";
|
||||
|
||||
/* journal_lock.py: Title text on popup when Journal directory already locked; In files: journal_lock.py:206; */
|
||||
"Journal directory already locked" = "Journal directory already locked";
|
||||
|
||||
/* journal_lock.py: Text for when newly selected Journal directory is already locked; In files: journal_lock.py:223:224; */
|
||||
"The new Journal Directory location is already locked.{CR}You can either attempt to resolve this and then Retry, or choose to Ignore this." = "The new Journal Directory location is already locked.{CR}You can either attempt to resolve this and then Retry, or choose to Ignore this.";
|
||||
|
||||
/* journal_lock.py: Generic 'Retry' button label; In files: journal_lock.py:228; */
|
||||
"Retry" = "Retry";
|
||||
|
||||
/* journal_lock.py: Generic 'Ignore' button label; In files: journal_lock.py:232; */
|
||||
"Ignore" = "Ignore";
|
||||
|
||||
/* ttkHyperlinkLabel.py: Label for 'Copy' as in 'Copy and Paste'; EDMarketConnector.py: Label for 'Copy' as in 'Copy and Paste'; In files: ttkHyperlinkLabel.py:42; EDMarketConnector.py:866; */
|
||||
"Copy" = "Copy";
|
||||
|
||||
/* EDMarketConnector.py: Update button in main window; In files: EDMarketConnector.py:529; EDMarketConnector.py:828; EDMarketConnector.py:1520; */
|
||||
"Update" = "Update";
|
||||
@ -46,25 +133,22 @@
|
||||
/* EDMarketConnector.py: ED Journal file location appears to be in error; In files: EDMarketConnector.py:815; */
|
||||
"Error: Check E:D journal file location" = "Error: Check E:D journal file location";
|
||||
|
||||
/* EDMarketConnector.py: Label for commander name in main window; edsm.py: Game Commander name label in EDSM settings; stats.py: Cmdr stats; theme.py: Label for commander name in main window; In files: EDMarketConnector.py:822; edsm.py:257; stats.py:52; theme.py:227; */
|
||||
"Cmdr" = "Cmdr";
|
||||
|
||||
/* EDMarketConnector.py: 'Ship' or multi-crew role label in main window, as applicable; EDMarketConnector.py: Multicrew role label in main window; In files: EDMarketConnector.py:824; EDMarketConnector.py:1279; */
|
||||
"Role" = "Role";
|
||||
|
||||
/* EDMarketConnector.py: 'Ship' or multi-crew role label in main window, as applicable; EDMarketConnector.py: 'Ship' label in main UI; stats.py: Status dialog subtitle; In files: EDMarketConnector.py:824; EDMarketConnector.py:1289; EDMarketConnector.py:1312; stats.py:367; */
|
||||
/* EDMarketConnector.py: 'Ship' or multi-crew role label in main window, as applicable; EDMarketConnector.py: 'Ship' label in main UI; stats.py: Status dialog subtitle; In files: EDMarketConnector.py:824; EDMarketConnector.py:1289; EDMarketConnector.py:1312; stats.py:409; */
|
||||
"Ship" = "Ship";
|
||||
|
||||
/* EDMarketConnector.py: Label for 'Suit' line in main UI; In files: EDMarketConnector.py:825; */
|
||||
"Suit" = "Suit";
|
||||
|
||||
/* EDMarketConnector.py: Label for 'System' line in main UI; prefs.py: Configuration - Label for selection of 'System' provider website; stats.py: Main window; In files: EDMarketConnector.py:826; prefs.py:607; stats.py:369; */
|
||||
/* EDMarketConnector.py: Label for 'System' line in main UI; prefs.py: Configuration - Label for selection of 'System' provider website; stats.py: Main window; In files: EDMarketConnector.py:826; prefs.py:607; stats.py:411; */
|
||||
"System" = "System";
|
||||
|
||||
/* EDMarketConnector.py: Label for 'Station' line in main UI; prefs.py: Configuration - Label for selection of 'Station' provider website; prefs.py: Appearance - Example 'Normal' text; stats.py: Status dialog subtitle; In files: EDMarketConnector.py:827; prefs.py:625; prefs.py:762; stats.py:370; */
|
||||
/* EDMarketConnector.py: Label for 'Station' line in main UI; prefs.py: Configuration - Label for selection of 'Station' provider website; prefs.py: Appearance - Example 'Normal' text; stats.py: Status dialog subtitle; In files: EDMarketConnector.py:827; prefs.py:625; prefs.py:762; stats.py:412; */
|
||||
"Station" = "Station";
|
||||
|
||||
/* EDMarketConnector.py: 'File' menu title on OSX; EDMarketConnector.py: 'File' menu title; EDMarketConnector.py: 'File' menu; In files: EDMarketConnector.py:830; EDMarketConnector.py:845; EDMarketConnector.py:848; EDMarketConnector.py:2015; */
|
||||
/* EDMarketConnector.py: 'File' menu title on OSX; EDMarketConnector.py: 'File' menu title; EDMarketConnector.py: 'File' menu; In files: EDMarketConnector.py:830; EDMarketConnector.py:845; EDMarketConnector.py:848; EDMarketConnector.py:2019; */
|
||||
"File" = "File";
|
||||
|
||||
/* EDMarketConnector.py: 'Edit' menu title on OSX; EDMarketConnector.py: 'Edit' menu title; In files: EDMarketConnector.py:831; EDMarketConnector.py:846; EDMarketConnector.py:849; */
|
||||
@ -88,7 +172,7 @@
|
||||
/* EDMarketConnector.py: File > Save Raw Data...; In files: EDMarketConnector.py:840; EDMarketConnector.py:854; */
|
||||
"Save Raw Data..." = "Save Raw Data...";
|
||||
|
||||
/* EDMarketConnector.py: File > Status; stats.py: Status dialog title; In files: EDMarketConnector.py:841; EDMarketConnector.py:853; stats.py:364; */
|
||||
/* EDMarketConnector.py: File > Status; stats.py: Status dialog title; In files: EDMarketConnector.py:841; EDMarketConnector.py:853; stats.py:406; */
|
||||
"Status" = "Status";
|
||||
|
||||
/* EDMarketConnector.py: Help > Privacy Policy; In files: EDMarketConnector.py:842; EDMarketConnector.py:860; */
|
||||
@ -97,7 +181,7 @@
|
||||
/* EDMarketConnector.py: Help > Release Notes; In files: EDMarketConnector.py:843; EDMarketConnector.py:861; EDMarketConnector.py:1600; */
|
||||
"Release Notes" = "Release Notes";
|
||||
|
||||
/* EDMarketConnector.py: File > Settings; prefs.py: File > Settings (macOS); In files: EDMarketConnector.py:855; EDMarketConnector.py:2016; prefs.py:255; */
|
||||
/* EDMarketConnector.py: File > Settings; prefs.py: File > Settings (macOS); In files: EDMarketConnector.py:855; EDMarketConnector.py:2020; prefs.py:255; */
|
||||
"Settings" = "Settings";
|
||||
|
||||
/* EDMarketConnector.py: File > Exit; In files: EDMarketConnector.py:856; */
|
||||
@ -106,9 +190,6 @@
|
||||
/* EDMarketConnector.py: Help > Documentation; In files: EDMarketConnector.py:859; */
|
||||
"Documentation" = "Documentation";
|
||||
|
||||
/* EDMarketConnector.py: Label for 'Copy' as in 'Copy and Paste'; ttkHyperlinkLabel.py: Label for 'Copy' as in 'Copy and Paste'; In files: EDMarketConnector.py:866; ttkHyperlinkLabel.py:42; */
|
||||
"Copy" = "Copy";
|
||||
|
||||
/* EDMarketConnector.py: Status - Attempting to get a Frontier Auth Access Token; In files: EDMarketConnector.py:872; */
|
||||
"Logging in..." = "Logging in...";
|
||||
|
||||
@ -142,16 +223,13 @@
|
||||
/* EDMarketConnector.py: Status - Attempting to retrieve data from Frontier CAPI; In files: EDMarketConnector.py:1006; */
|
||||
"Fetching data..." = "Fetching data...";
|
||||
|
||||
/* EDMarketConnector.py: No data was returned for the commander from the Frontier CAPI; In files: EDMarketConnector.py:1047; */
|
||||
"CAPI: No commander data returned" = "CAPI: No commander data returned";
|
||||
|
||||
/* EDMarketConnector.py: We didn't have the commander name when we should have; stats.py: Unknown commander; In files: EDMarketConnector.py:1051; stats.py:298; */
|
||||
/* EDMarketConnector.py: We didn't have the commander name when we should have; stats.py: Unknown commander; In files: EDMarketConnector.py:1051; stats.py:335; */
|
||||
"Who are you?!" = "Who are you?!";
|
||||
|
||||
/* EDMarketConnector.py: We don't know where the commander is, when we should; stats.py: Unknown location; In files: EDMarketConnector.py:1057; stats.py:308; */
|
||||
/* EDMarketConnector.py: We don't know where the commander is, when we should; stats.py: Unknown location; In files: EDMarketConnector.py:1057; stats.py:345; */
|
||||
"Where are you?!" = "Where are you?!";
|
||||
|
||||
/* EDMarketConnector.py: We don't know what ship the commander is in, when we should; stats.py: Unknown ship; In files: EDMarketConnector.py:1064; stats.py:316; */
|
||||
/* EDMarketConnector.py: We don't know what ship the commander is in, when we should; stats.py: Unknown ship; In files: EDMarketConnector.py:1064; stats.py:353; */
|
||||
"What are you flying?!" = "What are you flying?!";
|
||||
|
||||
/* EDMarketConnector.py: Frontier CAPI server error when fetching data; In files: EDMarketConnector.py:1176; */
|
||||
@ -181,126 +259,51 @@
|
||||
/* EDMarketConnector.py: The application is shutting down; In files: EDMarketConnector.py:1693; */
|
||||
"Shutting down..." = "Shutting down...";
|
||||
|
||||
/* EDMarketConnector.py: Popup-text about 'active' plugins without Python 3.x support; In files: EDMarketConnector.py:2004:2010; */
|
||||
/* EDMarketConnector.py: Popup-text about 'active' plugins without Python 3.x support; In files: EDMarketConnector.py:2008:2014; */
|
||||
"One or more of your enabled plugins do not yet have support for Python 3.x. Please see the list on the '{PLUGINS}' tab of '{FILE}' > '{SETTINGS}'. You should check if there is an updated version available, else alert the developer that they need to update the code for Python 3.x.\r\n\r\nYou can disable a plugin by renaming its folder to have '{DISABLED}' on the end of the name." = "One or more of your enabled plugins do not yet have support for Python 3.x. Please see the list on the '{PLUGINS}' tab of '{FILE}' > '{SETTINGS}'. You should check if there is an updated version available, else alert the developer that they need to update the code for Python 3.x.\r\n\r\nYou can disable a plugin by renaming its folder to have '{DISABLED}' on the end of the name.";
|
||||
|
||||
/* EDMarketConnector.py: Settings > Plugins tab; prefs.py: Label on Settings > Plugins tab; In files: EDMarketConnector.py:2014; prefs.py:978; */
|
||||
/* EDMarketConnector.py: Settings > Plugins tab; prefs.py: Label on Settings > Plugins tab; In files: EDMarketConnector.py:2018; prefs.py:978; */
|
||||
"Plugins" = "Plugins";
|
||||
|
||||
/* EDMarketConnector.py: Popup window title for list of 'enabled' plugins that don't work with Python 3.x; In files: EDMarketConnector.py:2025; */
|
||||
/* EDMarketConnector.py: Popup window title for list of 'enabled' plugins that don't work with Python 3.x; In files: EDMarketConnector.py:2029; */
|
||||
"EDMC: Plugins Without Python 3.x Support" = "EDMC: Plugins Without Python 3.x Support";
|
||||
|
||||
/* journal_lock.py: Title text on popup when Journal directory already locked; In files: journal_lock.py:206; */
|
||||
"Journal directory already locked" = "Journal directory already locked";
|
||||
/* companion.py: Frontier CAPI didn't respond; In files: companion.py:218; */
|
||||
"Error: Frontier CAPI didn't respond" = "Error: Frontier CAPI didn't respond";
|
||||
|
||||
/* journal_lock.py: Text for when newly selected Journal directory is already locked; In files: journal_lock.py:223:224; */
|
||||
"The new Journal Directory location is already locked.{CR}You can either attempt to resolve this and then Retry, or choose to Ignore this." = "The new Journal Directory location is already locked.{CR}You can either attempt to resolve this and then Retry, or choose to Ignore this.";
|
||||
/* companion.py: Frontier CAPI data doesn't agree with latest Journal game location; In files: companion.py:237; */
|
||||
"Error: Frontier server is lagging" = "Error: Frontier server is lagging";
|
||||
|
||||
/* journal_lock.py: Generic 'Retry' button label; In files: journal_lock.py:228; */
|
||||
"Retry" = "Retry";
|
||||
/* companion.py: Commander is docked at an EDO settlement, got out and back in, we forgot the station; In files: companion.py:253; */
|
||||
"Docked but unknown station: EDO Settlement?" = "Docked but unknown station: EDO Settlement?";
|
||||
|
||||
/* journal_lock.py: Generic 'Ignore' button label; In files: journal_lock.py:232; */
|
||||
"Ignore" = "Ignore";
|
||||
/* companion.py: Generic "something went wrong with Frontier Auth" error; In files: companion.py:263; */
|
||||
"Error: Invalid Credentials" = "Error: Invalid Credentials";
|
||||
|
||||
/* companion.py: Frontier CAPI authorisation not for currently game-active commander; In files: companion.py:288; */
|
||||
"Error: Wrong Cmdr" = "Error: Wrong Cmdr";
|
||||
|
||||
/* companion.py: Generic error prefix - following text is from Frontier auth service; In files: companion.py:414; companion.py:499; */
|
||||
"Error" = "Error";
|
||||
|
||||
/* companion.py: Frontier auth, no 'usr' section in returned data; companion.py: Frontier auth, no 'customer_id' in 'usr' section in returned data; In files: companion.py:457; companion.py:462; */
|
||||
"Error: Couldn't check token customer_id" = "Error: Couldn't check token customer_id";
|
||||
|
||||
/* companion.py: Frontier auth customer_id doesn't match game session FID; In files: companion.py:468; */
|
||||
"Error: customer_id doesn't match!" = "Error: customer_id doesn't match!";
|
||||
|
||||
/* companion.py: Failed to get Access Token from Frontier Auth service; In files: companion.py:490; */
|
||||
"Error: unable to get token" = "Error: unable to get token";
|
||||
|
||||
/* companion.py: Frontier CAPI returned 418, meaning down for maintenance; In files: companion.py:797; */
|
||||
"Frontier CAPI down for maintenance" = "Frontier CAPI down for maintenance";
|
||||
|
||||
/* companion.py: Frontier CAPI data retrieval failed; In files: companion.py:809; */
|
||||
"Frontier CAPI query failure" = "Frontier CAPI query failure";
|
||||
|
||||
/* l10n.py: The system default language choice in Settings > Appearance; prefs.py: Settings > Configuration - Label on 'reset journal files location to default' button; prefs.py: The system default language choice in Settings > Appearance; prefs.py: Label for 'Default' theme radio button; In files: l10n.py:194; prefs.py:468; prefs.py:702; prefs.py:735; */
|
||||
"Default" = "Default";
|
||||
|
||||
/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:52; coriolis.py:55; coriolis.py:101; coriolis.py:117; coriolis.py:123; */
|
||||
"Auto" = "Auto";
|
||||
|
||||
/* coriolis.py: 'Normal' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - normal; In files: coriolis.py:53; coriolis.py:99; coriolis.py:115; */
|
||||
"Normal" = "Normal";
|
||||
|
||||
/* coriolis.py: 'Beta' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - beta; In files: coriolis.py:54; coriolis.py:100; coriolis.py:116; */
|
||||
"Beta" = "Beta";
|
||||
|
||||
/* coriolis.py: Settings>Coriolis: Help/hint for changing coriolis URLs; In files: coriolis.py:69:71; */
|
||||
"Set the URL to use with coriolis.io ship loadouts. Note that this MUST end with '/import?data='" = "Set the URL to use with coriolis.io ship loadouts. Note that this MUST end with '/import?data='";
|
||||
|
||||
/* coriolis.py: Settings>Coriolis: Label for 'NOT alpha/beta game version' URL; In files: coriolis.py:75; */
|
||||
"Normal URL" = "Normal URL";
|
||||
|
||||
/* coriolis.py: Generic 'Reset' button label; In files: coriolis.py:78; coriolis.py:87; */
|
||||
"Reset" = "Reset";
|
||||
|
||||
/* coriolis.py: Settings>Coriolis: Label for 'alpha/beta game version' URL; In files: coriolis.py:84; */
|
||||
"Beta URL" = "Beta URL";
|
||||
|
||||
/* coriolis.py: Settings>Coriolis: Label for selection of using Normal, Beta or 'auto' Coriolis URL; In files: coriolis.py:94; */
|
||||
"Override Beta/Normal Selection" = "Override Beta/Normal Selection";
|
||||
|
||||
/* coriolis.py: Settings>Coriolis - invalid override mode found; In files: coriolis.py:134; */
|
||||
"Invalid Coriolis override mode!" = "Invalid Coriolis override mode!";
|
||||
|
||||
/* eddb.py: Journal Processing disabled due to an active killswitch; In files: eddb.py:100; */
|
||||
"EDDB Journal processing disabled. See Log." = "EDDB Journal processing disabled. See Log.";
|
||||
|
||||
/* eddn.py: Status text shown while attempting to send data; In files: eddn.py:251; eddn.py:698; eddn.py:1438; */
|
||||
"Sending data to EDDN..." = "Sending data to EDDN...";
|
||||
|
||||
/* eddn.py: Error while trying to send data to EDDN; In files: eddn.py:315; eddn.py:1374; eddn.py:1409; eddn.py:1450; */
|
||||
"Error: Can't connect to EDDN" = "Error: Can't connect to EDDN";
|
||||
|
||||
/* eddn.py: EDDN has banned this version of our client; In files: eddn.py:333; */
|
||||
"EDDN Error: EDMC is too old for EDDN. Please update." = "EDDN Error: EDMC is too old for EDDN. Please update.";
|
||||
|
||||
/* eddn.py: EDDN returned an error that indicates something about what we sent it was wrong; In files: eddn.py:339; */
|
||||
"EDDN Error: Validation Failed (EDMC Too Old?). See Log" = "EDDN Error: Validation Failed (EDMC Too Old?). See Log";
|
||||
|
||||
/* eddn.py: EDDN returned some sort of HTTP error, one we didn't expect. {STATUS} contains a number; In files: eddn.py:344; */
|
||||
"EDDN Error: Returned {STATUS} status code" = "EDDN Error: Returned {STATUS} status code";
|
||||
|
||||
/* eddn.py: No 'Route' found in NavRoute.json file; In files: eddn.py:970; */
|
||||
"No 'Route' array in NavRoute.json contents" = "No 'Route' array in NavRoute.json contents";
|
||||
|
||||
/* eddn.py: Enable EDDN support for station data checkbox label; In files: eddn.py:1121; */
|
||||
"Send station data to the Elite Dangerous Data Network" = "Send station data to the Elite Dangerous Data Network";
|
||||
|
||||
/* eddn.py: Enable EDDN support for system and other scan data checkbox label; In files: eddn.py:1132; */
|
||||
"Send system and scan data to the Elite Dangerous Data Network" = "Send system and scan data to the Elite Dangerous Data Network";
|
||||
|
||||
/* eddn.py: EDDN delay sending until docked option is on, this message notes that a send was skipped due to this; In files: eddn.py:1143; */
|
||||
"Delay sending until docked" = "Delay sending until docked";
|
||||
|
||||
/* eddn.py: Killswitch disabled EDDN; In files: eddn.py:1232; */
|
||||
"EDDN journal handler disabled. See Log." = "EDDN journal handler disabled. See Log.";
|
||||
|
||||
/* edsm.py: Settings>EDSM - Label on checkbox for 'send data'; In files: edsm.py:236; */
|
||||
"Send flight log and Cmdr status to EDSM" = "Send flight log and Cmdr status to EDSM";
|
||||
|
||||
/* edsm.py: Settings>EDSM - Label on header/URL to EDSM API key page; In files: edsm.py:246; */
|
||||
"Elite Dangerous Star Map credentials" = "Elite Dangerous Star Map credentials";
|
||||
|
||||
/* edsm.py: EDSM Commander name label in EDSM settings; In files: edsm.py:265; */
|
||||
"Commander Name" = "Commander Name";
|
||||
|
||||
/* edsm.py: EDSM API key label; inara.py: Inara API key label; In files: edsm.py:273; inara.py:243; */
|
||||
"API Key" = "API Key";
|
||||
|
||||
/* edsm.py: We have no data on the current commander; prefs.py: No hotkey/shortcut set; stats.py: No rank; In files: edsm.py:300; prefs.py:520; prefs.py:1174; prefs.py:1207; stats.py:119; stats.py:138; stats.py:157; stats.py:174; */
|
||||
"None" = "None";
|
||||
|
||||
/* edsm.py: EDSM plugin - Journal handling disabled by killswitch; In files: edsm.py:401; */
|
||||
"EDSM Handler disabled. See Log." = "EDSM Handler disabled. See Log.";
|
||||
|
||||
/* edsm.py: EDSM Plugin - Error message from EDSM API; In files: edsm.py:746; edsm.py:874; */
|
||||
"Error: EDSM {MSG}" = "Error: EDSM {MSG}";
|
||||
|
||||
/* edsm.py: EDSM Plugin - Error connecting to EDSM API; In files: edsm.py:783; edsm.py:869; */
|
||||
"Error: Can't connect to EDSM" = "Error: Can't connect to EDSM";
|
||||
|
||||
/* inara.py: Checkbox to enable INARA API Usage; In files: inara.py:222; */
|
||||
"Send flight log and Cmdr status to Inara" = "Send flight log and Cmdr status to Inara";
|
||||
|
||||
/* inara.py: Text for INARA API keys link ( goes to https://inara.cz/settings-api ); In files: inara.py:234; */
|
||||
"Inara credentials" = "Inara credentials";
|
||||
|
||||
/* inara.py: INARA support disabled via killswitch; In files: inara.py:341; */
|
||||
"Inara disabled. See Log." = "Inara disabled. See Log.";
|
||||
|
||||
/* inara.py: INARA API returned some kind of error (error message will be contained in {MSG}); In files: inara.py:1580; inara.py:1593; */
|
||||
"Error: Inara {MSG}" = "Error: Inara {MSG}";
|
||||
|
||||
/* prefs.py: File > Preferences menu entry for macOS; In files: prefs.py:251; */
|
||||
"Preferences" = "Preferences";
|
||||
|
||||
@ -442,236 +445,303 @@
|
||||
/* prefs.py: Lable on list of user-disabled plugins; In files: prefs.py:969; */
|
||||
"Disabled Plugins" = "Disabled Plugins";
|
||||
|
||||
/* stats.py: Cmdr stats; In files: stats.py:53; */
|
||||
/* stats.py: Cmdr stats; In files: stats.py:60; */
|
||||
"Balance" = "Balance";
|
||||
|
||||
/* stats.py: Cmdr stats; In files: stats.py:54; */
|
||||
/* stats.py: Cmdr stats; In files: stats.py:61; */
|
||||
"Loan" = "Loan";
|
||||
|
||||
/* stats.py: Ranking; In files: stats.py:59; */
|
||||
"Combat" = "Combat";
|
||||
|
||||
/* stats.py: Ranking; In files: stats.py:60; */
|
||||
"Trade" = "Trade";
|
||||
|
||||
/* stats.py: Ranking; In files: stats.py:61; */
|
||||
"Explorer" = "Explorer";
|
||||
|
||||
/* stats.py: Ranking; In files: stats.py:62; */
|
||||
"CQC" = "CQC";
|
||||
|
||||
/* stats.py: Ranking; In files: stats.py:63; */
|
||||
"Federation" = "Federation";
|
||||
|
||||
/* stats.py: Ranking; In files: stats.py:64; */
|
||||
"Empire" = "Empire";
|
||||
|
||||
/* stats.py: Ranking; In files: stats.py:65; */
|
||||
"Powerplay" = "Powerplay";
|
||||
|
||||
/* stats.py: Combat rank; In files: stats.py:73; */
|
||||
"Harmless" = "Harmless";
|
||||
|
||||
/* stats.py: Combat rank; In files: stats.py:74; */
|
||||
"Mostly Harmless" = "Mostly Harmless";
|
||||
|
||||
/* stats.py: Combat rank; In files: stats.py:75; */
|
||||
"Novice" = "Novice";
|
||||
|
||||
/* stats.py: Combat rank; In files: stats.py:76; */
|
||||
"Competent" = "Competent";
|
||||
|
||||
/* stats.py: Combat rank; In files: stats.py:77; */
|
||||
"Expert" = "Expert";
|
||||
|
||||
/* stats.py: Combat rank; stats.py: Empire rank; In files: stats.py:78; stats.py:141; */
|
||||
"Master" = "Master";
|
||||
|
||||
/* stats.py: Combat rank; In files: stats.py:79; */
|
||||
"Dangerous" = "Dangerous";
|
||||
|
||||
/* stats.py: Combat rank; In files: stats.py:80; */
|
||||
"Deadly" = "Deadly";
|
||||
|
||||
/* stats.py: Top rank; In files: stats.py:81; stats.py:92; stats.py:103; stats.py:114; */
|
||||
/* stats.py: Top rank; In files: stats.py:65; */
|
||||
"Elite" = "Elite";
|
||||
|
||||
/* stats.py: Trade rank; In files: stats.py:84; */
|
||||
/* stats.py: Top rank +1; In files: stats.py:66; */
|
||||
"Elite I" = "Elite I";
|
||||
|
||||
/* stats.py: Top rank +2; In files: stats.py:67; */
|
||||
"Elite II" = "Elite II";
|
||||
|
||||
/* stats.py: Top rank +3; In files: stats.py:68; */
|
||||
"Elite III" = "Elite III";
|
||||
|
||||
/* stats.py: Top rank +4; In files: stats.py:69; */
|
||||
"Elite IV" = "Elite IV";
|
||||
|
||||
/* stats.py: Top rank +5; In files: stats.py:70; */
|
||||
"Elite V" = "Elite V";
|
||||
|
||||
/* stats.py: Ranking; In files: stats.py:76; */
|
||||
"Combat" = "Combat";
|
||||
|
||||
/* stats.py: Ranking; In files: stats.py:77; */
|
||||
"Trade" = "Trade";
|
||||
|
||||
/* stats.py: Ranking; In files: stats.py:78; */
|
||||
"Explorer" = "Explorer";
|
||||
|
||||
/* stats.py: Ranking; In files: stats.py:79; */
|
||||
"Mercenary" = "Mercenary";
|
||||
|
||||
/* stats.py: Ranking; In files: stats.py:80; */
|
||||
"Exobiologist" = "Exobiologist";
|
||||
|
||||
/* stats.py: Ranking; In files: stats.py:81; */
|
||||
"CQC" = "CQC";
|
||||
|
||||
/* stats.py: Ranking; In files: stats.py:82; */
|
||||
"Federation" = "Federation";
|
||||
|
||||
/* stats.py: Ranking; In files: stats.py:83; */
|
||||
"Empire" = "Empire";
|
||||
|
||||
/* stats.py: Ranking; In files: stats.py:84; */
|
||||
"Powerplay" = "Powerplay";
|
||||
|
||||
/* stats.py: Combat rank; In files: stats.py:93; */
|
||||
"Harmless" = "Harmless";
|
||||
|
||||
/* stats.py: Combat rank; In files: stats.py:94; */
|
||||
"Mostly Harmless" = "Mostly Harmless";
|
||||
|
||||
/* stats.py: Combat rank; In files: stats.py:95; */
|
||||
"Novice" = "Novice";
|
||||
|
||||
/* stats.py: Combat rank; In files: stats.py:96; */
|
||||
"Competent" = "Competent";
|
||||
|
||||
/* stats.py: Combat rank; In files: stats.py:97; */
|
||||
"Expert" = "Expert";
|
||||
|
||||
/* stats.py: Combat rank; stats.py: Empire rank; In files: stats.py:98; stats.py:178; */
|
||||
"Master" = "Master";
|
||||
|
||||
/* stats.py: Combat rank; In files: stats.py:99; */
|
||||
"Dangerous" = "Dangerous";
|
||||
|
||||
/* stats.py: Combat rank; In files: stats.py:100; */
|
||||
"Deadly" = "Deadly";
|
||||
|
||||
/* stats.py: Trade rank; In files: stats.py:103; */
|
||||
"Penniless" = "Penniless";
|
||||
|
||||
/* stats.py: Trade rank; In files: stats.py:85; */
|
||||
/* stats.py: Trade rank; In files: stats.py:104; */
|
||||
"Mostly Penniless" = "Mostly Penniless";
|
||||
|
||||
/* stats.py: Trade rank; In files: stats.py:86; */
|
||||
/* stats.py: Trade rank; In files: stats.py:105; */
|
||||
"Peddler" = "Peddler";
|
||||
|
||||
/* stats.py: Trade rank; In files: stats.py:87; */
|
||||
/* stats.py: Trade rank; In files: stats.py:106; */
|
||||
"Dealer" = "Dealer";
|
||||
|
||||
/* stats.py: Trade rank; In files: stats.py:88; */
|
||||
/* stats.py: Trade rank; In files: stats.py:107; */
|
||||
"Merchant" = "Merchant";
|
||||
|
||||
/* stats.py: Trade rank; In files: stats.py:89; */
|
||||
/* stats.py: Trade rank; In files: stats.py:108; */
|
||||
"Broker" = "Broker";
|
||||
|
||||
/* stats.py: Trade rank; In files: stats.py:90; */
|
||||
/* stats.py: Trade rank; In files: stats.py:109; */
|
||||
"Entrepreneur" = "Entrepreneur";
|
||||
|
||||
/* stats.py: Trade rank; In files: stats.py:91; */
|
||||
/* stats.py: Trade rank; In files: stats.py:110; */
|
||||
"Tycoon" = "Tycoon";
|
||||
|
||||
/* stats.py: Explorer rank; In files: stats.py:95; */
|
||||
/* stats.py: Explorer rank; In files: stats.py:113; */
|
||||
"Aimless" = "Aimless";
|
||||
|
||||
/* stats.py: Explorer rank; In files: stats.py:96; */
|
||||
/* stats.py: Explorer rank; In files: stats.py:114; */
|
||||
"Mostly Aimless" = "Mostly Aimless";
|
||||
|
||||
/* stats.py: Explorer rank; In files: stats.py:97; */
|
||||
/* stats.py: Explorer rank; In files: stats.py:115; */
|
||||
"Scout" = "Scout";
|
||||
|
||||
/* stats.py: Explorer rank; In files: stats.py:98; */
|
||||
/* stats.py: Explorer rank; In files: stats.py:116; */
|
||||
"Surveyor" = "Surveyor";
|
||||
|
||||
/* stats.py: Explorer rank; In files: stats.py:99; */
|
||||
/* stats.py: Explorer rank; In files: stats.py:117; */
|
||||
"Trailblazer" = "Trailblazer";
|
||||
|
||||
/* stats.py: Explorer rank; In files: stats.py:100; */
|
||||
/* stats.py: Explorer rank; In files: stats.py:118; */
|
||||
"Pathfinder" = "Pathfinder";
|
||||
|
||||
/* stats.py: Explorer rank; In files: stats.py:101; */
|
||||
/* stats.py: Explorer rank; In files: stats.py:119; */
|
||||
"Ranger" = "Ranger";
|
||||
|
||||
/* stats.py: Explorer rank; In files: stats.py:102; */
|
||||
/* stats.py: Explorer rank; In files: stats.py:120; */
|
||||
"Pioneer" = "Pioneer";
|
||||
|
||||
/* stats.py: CQC rank; In files: stats.py:106; */
|
||||
/* stats.py: Mercenary rank; In files: stats.py:124; */
|
||||
"Defenceless" = "Defenceless";
|
||||
|
||||
/* stats.py: Mercenary rank; In files: stats.py:125; */
|
||||
"Mostly Defenceless" = "Mostly Defenceless";
|
||||
|
||||
/* stats.py: Mercenary rank; In files: stats.py:126; */
|
||||
"Rookie" = "Rookie";
|
||||
|
||||
/* stats.py: Mercenary rank; In files: stats.py:127; */
|
||||
"Soldier" = "Soldier";
|
||||
|
||||
/* stats.py: Mercenary rank; In files: stats.py:128; stats.py:130; */
|
||||
"Gunslinger" = "Gunslinger";
|
||||
|
||||
/* stats.py: Mercenary rank; In files: stats.py:129; */
|
||||
"Warrior" = "Warrior";
|
||||
|
||||
/* stats.py: Mercenary rank; In files: stats.py:131; */
|
||||
"Deadeye" = "Deadeye";
|
||||
|
||||
/* stats.py: Exobiologist rank; In files: stats.py:134; */
|
||||
"Directionless" = "Directionless";
|
||||
|
||||
/* stats.py: Exobiologist rank; In files: stats.py:135; */
|
||||
"Mostly Directionless" = "Mostly Directionless";
|
||||
|
||||
/* stats.py: Exobiologist rank; In files: stats.py:136; */
|
||||
"Compiler" = "Compiler";
|
||||
|
||||
/* stats.py: Exobiologist rank; In files: stats.py:137; */
|
||||
"Collector" = "Collector";
|
||||
|
||||
/* stats.py: Exobiologist rank; In files: stats.py:138; */
|
||||
"Cataloguer" = "Cataloguer";
|
||||
|
||||
/* stats.py: Exobiologist rank; In files: stats.py:139; */
|
||||
"Taxonomist" = "Taxonomist";
|
||||
|
||||
/* stats.py: Exobiologist rank; In files: stats.py:140; */
|
||||
"Ecologist" = "Ecologist";
|
||||
|
||||
/* stats.py: Exobiologist rank; In files: stats.py:141; */
|
||||
"Geneticist" = "Geneticist";
|
||||
|
||||
/* stats.py: CQC rank; In files: stats.py:144; */
|
||||
"Helpless" = "Helpless";
|
||||
|
||||
/* stats.py: CQC rank; In files: stats.py:107; */
|
||||
/* stats.py: CQC rank; In files: stats.py:145; */
|
||||
"Mostly Helpless" = "Mostly Helpless";
|
||||
|
||||
/* stats.py: CQC rank; In files: stats.py:108; */
|
||||
/* stats.py: CQC rank; In files: stats.py:146; */
|
||||
"Amateur" = "Amateur";
|
||||
|
||||
/* stats.py: CQC rank; In files: stats.py:109; */
|
||||
/* stats.py: CQC rank; In files: stats.py:147; */
|
||||
"Semi Professional" = "Semi Professional";
|
||||
|
||||
/* stats.py: CQC rank; In files: stats.py:110; */
|
||||
/* stats.py: CQC rank; In files: stats.py:148; */
|
||||
"Professional" = "Professional";
|
||||
|
||||
/* stats.py: CQC rank; In files: stats.py:111; */
|
||||
/* stats.py: CQC rank; In files: stats.py:149; */
|
||||
"Champion" = "Champion";
|
||||
|
||||
/* stats.py: CQC rank; In files: stats.py:112; */
|
||||
/* stats.py: CQC rank; In files: stats.py:150; */
|
||||
"Hero" = "Hero";
|
||||
|
||||
/* stats.py: CQC rank; In files: stats.py:113; */
|
||||
/* stats.py: CQC rank; In files: stats.py:151; */
|
||||
"Gladiator" = "Gladiator";
|
||||
|
||||
/* stats.py: Federation rank; In files: stats.py:120; */
|
||||
/* stats.py: Federation rank; In files: stats.py:157; */
|
||||
"Recruit" = "Recruit";
|
||||
|
||||
/* stats.py: Federation rank; In files: stats.py:121; */
|
||||
/* stats.py: Federation rank; In files: stats.py:158; */
|
||||
"Cadet" = "Cadet";
|
||||
|
||||
/* stats.py: Federation rank; In files: stats.py:122; */
|
||||
/* stats.py: Federation rank; In files: stats.py:159; */
|
||||
"Midshipman" = "Midshipman";
|
||||
|
||||
/* stats.py: Federation rank; In files: stats.py:123; */
|
||||
/* stats.py: Federation rank; In files: stats.py:160; */
|
||||
"Petty Officer" = "Petty Officer";
|
||||
|
||||
/* stats.py: Federation rank; In files: stats.py:124; */
|
||||
/* stats.py: Federation rank; In files: stats.py:161; */
|
||||
"Chief Petty Officer" = "Chief Petty Officer";
|
||||
|
||||
/* stats.py: Federation rank; In files: stats.py:125; */
|
||||
/* stats.py: Federation rank; In files: stats.py:162; */
|
||||
"Warrant Officer" = "Warrant Officer";
|
||||
|
||||
/* stats.py: Federation rank; In files: stats.py:126; */
|
||||
/* stats.py: Federation rank; In files: stats.py:163; */
|
||||
"Ensign" = "Ensign";
|
||||
|
||||
/* stats.py: Federation rank; In files: stats.py:127; */
|
||||
/* stats.py: Federation rank; In files: stats.py:164; */
|
||||
"Lieutenant" = "Lieutenant";
|
||||
|
||||
/* stats.py: Federation rank; In files: stats.py:128; */
|
||||
/* stats.py: Federation rank; In files: stats.py:165; */
|
||||
"Lieutenant Commander" = "Lieutenant Commander";
|
||||
|
||||
/* stats.py: Federation rank; In files: stats.py:129; */
|
||||
/* stats.py: Federation rank; In files: stats.py:166; */
|
||||
"Post Commander" = "Post Commander";
|
||||
|
||||
/* stats.py: Federation rank; In files: stats.py:130; */
|
||||
/* stats.py: Federation rank; In files: stats.py:167; */
|
||||
"Post Captain" = "Post Captain";
|
||||
|
||||
/* stats.py: Federation rank; In files: stats.py:131; */
|
||||
/* stats.py: Federation rank; In files: stats.py:168; */
|
||||
"Rear Admiral" = "Rear Admiral";
|
||||
|
||||
/* stats.py: Federation rank; In files: stats.py:132; */
|
||||
/* stats.py: Federation rank; In files: stats.py:169; */
|
||||
"Vice Admiral" = "Vice Admiral";
|
||||
|
||||
/* stats.py: Federation rank; In files: stats.py:133; */
|
||||
/* stats.py: Federation rank; In files: stats.py:170; */
|
||||
"Admiral" = "Admiral";
|
||||
|
||||
/* stats.py: Empire rank; In files: stats.py:139; */
|
||||
/* stats.py: Empire rank; In files: stats.py:176; */
|
||||
"Outsider" = "Outsider";
|
||||
|
||||
/* stats.py: Empire rank; In files: stats.py:140; */
|
||||
/* stats.py: Empire rank; In files: stats.py:177; */
|
||||
"Serf" = "Serf";
|
||||
|
||||
/* stats.py: Empire rank; In files: stats.py:142; */
|
||||
/* stats.py: Empire rank; In files: stats.py:179; */
|
||||
"Squire" = "Squire";
|
||||
|
||||
/* stats.py: Empire rank; In files: stats.py:143; */
|
||||
/* stats.py: Empire rank; In files: stats.py:180; */
|
||||
"Knight" = "Knight";
|
||||
|
||||
/* stats.py: Empire rank; In files: stats.py:144; */
|
||||
/* stats.py: Empire rank; In files: stats.py:181; */
|
||||
"Lord" = "Lord";
|
||||
|
||||
/* stats.py: Empire rank; In files: stats.py:145; */
|
||||
/* stats.py: Empire rank; In files: stats.py:182; */
|
||||
"Baron" = "Baron";
|
||||
|
||||
/* stats.py: Empire rank; In files: stats.py:146; */
|
||||
/* stats.py: Empire rank; In files: stats.py:183; */
|
||||
"Viscount" = "Viscount";
|
||||
|
||||
/* stats.py: Empire rank; In files: stats.py:147; */
|
||||
/* stats.py: Empire rank; In files: stats.py:184; */
|
||||
"Count" = "Count";
|
||||
|
||||
/* stats.py: Empire rank; In files: stats.py:148; */
|
||||
/* stats.py: Empire rank; In files: stats.py:185; */
|
||||
"Earl" = "Earl";
|
||||
|
||||
/* stats.py: Empire rank; In files: stats.py:149; */
|
||||
/* stats.py: Empire rank; In files: stats.py:186; */
|
||||
"Marquis" = "Marquis";
|
||||
|
||||
/* stats.py: Empire rank; In files: stats.py:150; */
|
||||
/* stats.py: Empire rank; In files: stats.py:187; */
|
||||
"Duke" = "Duke";
|
||||
|
||||
/* stats.py: Empire rank; In files: stats.py:151; */
|
||||
/* stats.py: Empire rank; In files: stats.py:188; */
|
||||
"Prince" = "Prince";
|
||||
|
||||
/* stats.py: Empire rank; In files: stats.py:152; */
|
||||
/* stats.py: Empire rank; In files: stats.py:189; */
|
||||
"King" = "King";
|
||||
|
||||
/* stats.py: Power rank; In files: stats.py:158; */
|
||||
/* stats.py: Power rank; In files: stats.py:195; */
|
||||
"Rating 1" = "Rating 1";
|
||||
|
||||
/* stats.py: Power rank; In files: stats.py:159; */
|
||||
/* stats.py: Power rank; In files: stats.py:196; */
|
||||
"Rating 2" = "Rating 2";
|
||||
|
||||
/* stats.py: Power rank; In files: stats.py:160; */
|
||||
/* stats.py: Power rank; In files: stats.py:197; */
|
||||
"Rating 3" = "Rating 3";
|
||||
|
||||
/* stats.py: Power rank; In files: stats.py:161; */
|
||||
/* stats.py: Power rank; In files: stats.py:198; */
|
||||
"Rating 4" = "Rating 4";
|
||||
|
||||
/* stats.py: Power rank; In files: stats.py:162; */
|
||||
/* stats.py: Power rank; In files: stats.py:199; */
|
||||
"Rating 5" = "Rating 5";
|
||||
|
||||
/* stats.py: Current commander unknown when trying to use 'File' > 'Status'; In files: stats.py:280; */
|
||||
/* stats.py: Current commander unknown when trying to use 'File' > 'Status'; In files: stats.py:317; */
|
||||
"Status: Don't yet know your Commander name" = "Status: Don't yet know your Commander name";
|
||||
|
||||
/* stats.py: No Frontier CAPI data yet when trying to use 'File' > 'Status'; In files: stats.py:288; */
|
||||
/* stats.py: No Frontier CAPI data yet when trying to use 'File' > 'Status'; In files: stats.py:325; */
|
||||
"Status: No CAPI data yet" = "Status: No CAPI data yet";
|
||||
|
||||
/* stats.py: Status dialog subtitle - CR value of ship; In files: stats.py:371; */
|
||||
/* stats.py: Status dialog subtitle - CR value of ship; In files: stats.py:413; */
|
||||
"Value" = "Value";
|
||||
|
||||
/* stats.py: Status dialog title; In files: stats.py:380; */
|
||||
/* stats.py: Status dialog title; In files: stats.py:422; */
|
||||
"Ships" = "Ships";
|
||||
|
||||
|
@ -250,6 +250,9 @@
|
||||
/* eddn.py: EDDN returned some sort of HTTP error, one we didn't expect. {STATUS} contains a number; In files: eddn.py:344; */
|
||||
"EDDN Error: Returned {STATUS} status code" = "Błąd EDDN: Zwrócono kod {STATUS}";
|
||||
|
||||
/* eddn.py: No 'Route' found in NavRoute.json file; In files: eddn.py:970; */
|
||||
"No 'Route' array in NavRoute.json contents" = "Brak tablicy \"Route\" wewnątrz NavRoute.json";
|
||||
|
||||
/* eddn.py: Enable EDDN support for station data checkbox label; In files: eddn.py:1121; */
|
||||
"Send station data to the Elite Dangerous Data Network" = "Wyślij dane do Elite Dangerous Data Network";
|
||||
|
||||
|
23
collate.py
23
collate.py
@ -1,9 +1,21 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Collate lists of seen commodities, modules and ships from dumps of the Companion API output."""
|
||||
"""
|
||||
Collate lists of seen commodities, modules and ships from dumps of the Companion API output.
|
||||
|
||||
Note that currently this will only work with the output files created if you
|
||||
run the main program from a working directory that has a `dump/` directory,
|
||||
which causes a file to be written per CAPI query.
|
||||
|
||||
This script also utilise the file outfitting.csv. As it both reads it in *and*
|
||||
writes out a new copy a local copy, in the root of the project structure, is
|
||||
used for this purpose. If you want to utilise the FDevIDs/ version of the
|
||||
file, copy it over the local one.
|
||||
"""
|
||||
|
||||
import csv
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import sys
|
||||
from os.path import isfile
|
||||
from traceback import print_exc
|
||||
@ -13,14 +25,14 @@ import outfitting
|
||||
from edmc_data import companion_category_map, ship_name_map
|
||||
|
||||
|
||||
def __make_backup(file_name: str, suffix: str = '.bak') -> None:
|
||||
def __make_backup(file_name: pathlib.Path, suffix: str = '.bak') -> None:
|
||||
"""
|
||||
Rename the given file to $file.bak, removing any existing $file.bak. Assumes $file exists on disk.
|
||||
|
||||
:param file_name: The name of the file to make a backup of
|
||||
:param suffix: The suffix to use for backup files (default '.bak')
|
||||
"""
|
||||
backup_name = file_name + suffix
|
||||
backup_name = file_name.parent / (file_name.name + suffix)
|
||||
|
||||
if isfile(backup_name):
|
||||
os.unlink(backup_name)
|
||||
@ -38,7 +50,7 @@ def addcommodities(data) -> None: # noqa: CCR001
|
||||
if not data['lastStarport'].get('commodities'):
|
||||
return
|
||||
|
||||
commodityfile = 'commodity.csv'
|
||||
commodityfile = pathlib.Path('FDevIDs/commodity.csv')
|
||||
commodities = {}
|
||||
|
||||
# slurp existing
|
||||
@ -149,7 +161,7 @@ def addships(data) -> None: # noqa: CCR001
|
||||
if not data['lastStarport'].get('ships'):
|
||||
return
|
||||
|
||||
shipfile = 'shipyard.csv'
|
||||
shipfile = pathlib.Path('shipyard.csv')
|
||||
ships = {}
|
||||
fields = ('id', 'symbol', 'name')
|
||||
|
||||
@ -209,6 +221,7 @@ if __name__ == "__main__":
|
||||
with open(file_name) as f:
|
||||
print(file_name)
|
||||
data = json.load(f)
|
||||
data = data['data']
|
||||
|
||||
if not data['commander'].get('docked'):
|
||||
print('Not docked!')
|
||||
|
10
companion.py
10
companion.py
@ -22,7 +22,6 @@ import urllib.parse
|
||||
import webbrowser
|
||||
from builtins import object, range, str
|
||||
from email.utils import parsedate
|
||||
from os.path import join
|
||||
from queue import Queue
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Mapping, Optional, OrderedDict, TypeVar, Union
|
||||
|
||||
@ -30,7 +29,7 @@ import requests
|
||||
|
||||
import config as conf_module
|
||||
import protocol
|
||||
from config import appname, appversion, config
|
||||
from config import config, user_agent
|
||||
from edmc_data import companion_category_map as category_map
|
||||
from EDMCLogging import get_main_logger
|
||||
from monitor import monitor
|
||||
@ -54,7 +53,6 @@ auth_timeout = 30 # timeout for initial auth
|
||||
|
||||
# Used by both class Auth and Session
|
||||
FRONTIER_AUTH_SERVER = 'https://auth.frontierstore.net'
|
||||
USER_AGENT = f'EDCD-{appname}-{appversion()}'
|
||||
|
||||
SERVER_LIVE = 'https://companion.orerve.net'
|
||||
SERVER_BETA = 'https://pts-companion.orerve.net'
|
||||
@ -305,7 +303,7 @@ class Auth(object):
|
||||
def __init__(self, cmdr: str) -> None:
|
||||
self.cmdr: str = cmdr
|
||||
self.requests_session = requests.Session()
|
||||
self.requests_session.headers['User-Agent'] = USER_AGENT
|
||||
self.requests_session.headers['User-Agent'] = user_agent
|
||||
self.verifier: Union[bytes, None] = None
|
||||
self.state: Union[str, None] = None
|
||||
|
||||
@ -644,7 +642,7 @@ class Session(object):
|
||||
logger.debug('Starting session')
|
||||
self.requests_session = requests.Session()
|
||||
self.requests_session.headers['Authorization'] = f'Bearer {access_token}'
|
||||
self.requests_session.headers['User-Agent'] = USER_AGENT
|
||||
self.requests_session.headers['User-Agent'] = user_agent
|
||||
self.state = Session.STATE_OK
|
||||
|
||||
def login(self, cmdr: str = None, is_beta: Optional[bool] = None) -> bool:
|
||||
@ -1066,7 +1064,7 @@ def fixup(data: CAPIData) -> CAPIData: # noqa: C901, CCR001 # Can't be usefully
|
||||
if not commodity_map:
|
||||
# Lazily populate
|
||||
for f in ('commodity.csv', 'rare_commodity.csv'):
|
||||
with open(join(config.respath_path, f), 'r') as csvfile:
|
||||
with open(config.respath_path / 'FDevIDs' / f, 'r') as csvfile:
|
||||
reader = csv.DictReader(csvfile)
|
||||
|
||||
for row in reader:
|
||||
|
473
config/__init__.py
Normal file
473
config/__init__.py
Normal file
@ -0,0 +1,473 @@
|
||||
"""
|
||||
Code dealing with the configuration of the program.
|
||||
|
||||
Windows uses the Registry to store values in a flat manner.
|
||||
Linux uses a file, but for commonality it's still a flat data structure.
|
||||
macOS uses a 'defaults' object.
|
||||
"""
|
||||
|
||||
|
||||
__all__ = [
|
||||
# defined in the order they appear in the file
|
||||
'GITVERSION_FILE',
|
||||
'appname',
|
||||
'applongname',
|
||||
'appcmdname',
|
||||
'copyright',
|
||||
'update_feed',
|
||||
'update_interval',
|
||||
'debug_senders',
|
||||
'trace_on',
|
||||
'capi_pretend_down',
|
||||
'capi_debug_access_token',
|
||||
'logger',
|
||||
'git_shorthash_from_head',
|
||||
'appversion',
|
||||
'user_agent',
|
||||
'appversion_nobuild',
|
||||
'AbstractConfig',
|
||||
'config'
|
||||
]
|
||||
|
||||
import abc
|
||||
import contextlib
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import traceback
|
||||
import warnings
|
||||
from abc import abstractmethod
|
||||
from typing import Any, Callable, List, Optional, Type, TypeVar, Union
|
||||
|
||||
import semantic_version
|
||||
|
||||
from constants import GITVERSION_FILE, applongname, appname
|
||||
|
||||
# Any of these may be imported by plugins
|
||||
appcmdname = 'EDMC'
|
||||
# appversion **MUST** follow Semantic Versioning rules:
|
||||
# <https://semver.org/#semantic-versioning-specification-semver>
|
||||
# Major.Minor.Patch(-prerelease)(+buildmetadata)
|
||||
# NB: Do *not* import this, use the functions appversion() and appversion_nobuild()
|
||||
_static_appversion = '5.3.0'
|
||||
_cached_version: Optional[semantic_version.Version] = None
|
||||
copyright = '© 2015-2019 Jonathan Harris, 2020-2022 EDCD'
|
||||
|
||||
update_feed = 'https://raw.githubusercontent.com/EDCD/EDMarketConnector/releases/edmarketconnector.xml'
|
||||
update_interval = 8*60*60
|
||||
# Providers marked to be in debug mode. Generally this is expected to switch to sending data to a log file
|
||||
debug_senders: List[str] = []
|
||||
# TRACE logging code that should actually be used. Means not spamming it
|
||||
# *all* if only interested in some things.
|
||||
trace_on: List[str] = []
|
||||
|
||||
capi_pretend_down: bool = False
|
||||
capi_debug_access_token: Optional[str] = None
|
||||
# This must be done here in order to avoid an import cycle with EDMCLogging.
|
||||
# Other code should use EDMCLogging.get_main_logger
|
||||
if os.getenv("EDMC_NO_UI"):
|
||||
logger = logging.getLogger(appcmdname)
|
||||
|
||||
else:
|
||||
logger = logging.getLogger(appname)
|
||||
|
||||
|
||||
_T = TypeVar('_T')
|
||||
|
||||
|
||||
###########################################################################
|
||||
def git_shorthash_from_head() -> str:
|
||||
"""
|
||||
Determine short hash for current git HEAD.
|
||||
|
||||
Includes `.DIRTY` if any changes have been made from HEAD
|
||||
|
||||
:return: str - None if we couldn't determine the short hash.
|
||||
"""
|
||||
shorthash: str = None # type: ignore
|
||||
|
||||
try:
|
||||
git_cmd = subprocess.Popen('git rev-parse --short HEAD'.split(),
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT
|
||||
)
|
||||
out, err = git_cmd.communicate()
|
||||
|
||||
except Exception as e:
|
||||
logger.info(f"Couldn't run git command for short hash: {e!r}")
|
||||
|
||||
else:
|
||||
shorthash = out.decode().rstrip('\n')
|
||||
if re.match(r'^[0-9a-f]{7,}$', shorthash) is None:
|
||||
logger.error(f"'{shorthash}' doesn't look like a valid git short hash, forcing to None")
|
||||
shorthash = None # type: ignore
|
||||
|
||||
if shorthash is not None:
|
||||
with contextlib.suppress(Exception):
|
||||
result = subprocess.run('git diff --stat HEAD'.split(), capture_output=True)
|
||||
if len(result.stdout) > 0:
|
||||
shorthash += '.DIRTY'
|
||||
|
||||
if len(result.stderr) > 0:
|
||||
logger.warning(f'Data from git on stderr:\n{str(result.stderr)}')
|
||||
|
||||
return shorthash
|
||||
|
||||
|
||||
def appversion() -> semantic_version.Version:
|
||||
"""
|
||||
Determine app version including git short hash if possible.
|
||||
|
||||
:return: The augmented app version.
|
||||
"""
|
||||
global _cached_version
|
||||
if _cached_version is not None:
|
||||
return _cached_version
|
||||
|
||||
if getattr(sys, 'frozen', False):
|
||||
# Running frozen, so we should have a .gitversion file
|
||||
# Yes, .parent because if frozen we're inside library.zip
|
||||
with open(pathlib.Path(sys.path[0]).parent / GITVERSION_FILE, 'r', encoding='utf-8') as gitv:
|
||||
shorthash = gitv.read()
|
||||
|
||||
else:
|
||||
# Running from source
|
||||
shorthash = git_shorthash_from_head()
|
||||
if shorthash is None:
|
||||
shorthash = 'UNKNOWN'
|
||||
|
||||
_cached_version = semantic_version.Version(f'{_static_appversion}+{shorthash}')
|
||||
return _cached_version
|
||||
|
||||
|
||||
user_agent = f'EDCD-{appname}-{appversion()}'
|
||||
|
||||
|
||||
def appversion_nobuild() -> semantic_version.Version:
|
||||
"""
|
||||
Determine app version without *any* build meta data.
|
||||
|
||||
This will not only strip any added git short hash, but also any trailing
|
||||
'+<string>' in _static_appversion.
|
||||
|
||||
:return: App version without any build meta data.
|
||||
"""
|
||||
return appversion().truncate('prerelease')
|
||||
###########################################################################
|
||||
|
||||
|
||||
class AbstractConfig(abc.ABC):
|
||||
"""Abstract root class of all platform specific Config implementations."""
|
||||
|
||||
OUT_MKT_EDDN = 1
|
||||
# OUT_MKT_BPC = 2 # No longer supported
|
||||
OUT_MKT_TD = 4
|
||||
OUT_MKT_CSV = 8
|
||||
OUT_SHIP = 16
|
||||
# OUT_SHIP_EDS = 16 # Replaced by OUT_SHIP
|
||||
# OUT_SYS_FILE = 32 # No longer supported
|
||||
# OUT_STAT = 64 # No longer available
|
||||
# OUT_SHIP_CORIOLIS = 128 # Replaced by OUT_SHIP
|
||||
OUT_STATION_ANY = OUT_MKT_EDDN | OUT_MKT_TD | OUT_MKT_CSV
|
||||
# OUT_SYS_EDSM = 256 # Now a plugin
|
||||
# OUT_SYS_AUTO = 512 # Now always automatic
|
||||
OUT_MKT_MANUAL = 1024
|
||||
OUT_SYS_EDDN = 2048
|
||||
OUT_SYS_DELAY = 4096
|
||||
|
||||
app_dir_path: pathlib.Path
|
||||
plugin_dir_path: pathlib.Path
|
||||
internal_plugin_dir_path: pathlib.Path
|
||||
respath_path: pathlib.Path
|
||||
home_path: pathlib.Path
|
||||
default_journal_dir_path: pathlib.Path
|
||||
|
||||
identifier: str
|
||||
|
||||
__in_shutdown = False # Is the application currently shutting down ?
|
||||
__auth_force_localserver = False # Should we use localhost for auth callback ?
|
||||
__auth_force_edmc_protocol = False # Should we force edmc:// protocol ?
|
||||
__eddn_url = None # Non-default EDDN URL
|
||||
__eddn_tracking_ui = False # Show EDDN tracking UI ?
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.home_path = pathlib.Path.home()
|
||||
|
||||
def set_shutdown(self):
|
||||
"""Set flag denoting we're in the shutdown sequence."""
|
||||
self.__in_shutdown = True
|
||||
|
||||
@property
|
||||
def shutting_down(self) -> bool:
|
||||
"""
|
||||
Determine if we're in the shutdown sequence.
|
||||
|
||||
:return: bool - True if in shutdown sequence.
|
||||
"""
|
||||
return self.__in_shutdown
|
||||
|
||||
def set_auth_force_localserver(self):
|
||||
"""Set flag to force use of localhost web server for Frontier Auth callback."""
|
||||
self.__auth_force_localserver = True
|
||||
|
||||
@property
|
||||
def auth_force_localserver(self) -> bool:
|
||||
"""
|
||||
Determine if use of localhost is forced for Frontier Auth callback.
|
||||
|
||||
:return: bool - True if we should use localhost web server.
|
||||
"""
|
||||
return self.__auth_force_localserver
|
||||
|
||||
def set_auth_force_edmc_protocol(self):
|
||||
"""Set flag to force use of localhost web server for Frontier Auth callback."""
|
||||
self.__auth_force_edmc_protocol = True
|
||||
|
||||
@property
|
||||
def auth_force_edmc_protocol(self) -> bool:
|
||||
"""
|
||||
Determine if use of localhost is forced for Frontier Auth callback.
|
||||
|
||||
:return: bool - True if we should use localhost web server.
|
||||
"""
|
||||
return self.__auth_force_edmc_protocol
|
||||
|
||||
def set_eddn_url(self, eddn_url: str):
|
||||
"""Set the specified eddn URL."""
|
||||
self.__eddn_url = eddn_url
|
||||
|
||||
@property
|
||||
def eddn_url(self) -> Optional[str]:
|
||||
"""
|
||||
Provide the custom EDDN URL.
|
||||
|
||||
:return: str - Custom EDDN URL to use.
|
||||
"""
|
||||
return self.__eddn_url
|
||||
|
||||
def set_eddn_tracking_ui(self):
|
||||
"""Activate EDDN tracking UI."""
|
||||
self.__eddn_tracking_ui = True
|
||||
|
||||
@property
|
||||
def eddn_tracking_ui(self) -> bool:
|
||||
"""
|
||||
Determine if the EDDN tracking UI be shown.
|
||||
|
||||
:return: bool - Should tracking UI be active?
|
||||
"""
|
||||
return self.__eddn_tracking_ui
|
||||
|
||||
@property
|
||||
def app_dir(self) -> str:
|
||||
"""Return a string version of app_dir."""
|
||||
return str(self.app_dir_path)
|
||||
|
||||
@property
|
||||
def plugin_dir(self) -> str:
|
||||
"""Return a string version of plugin_dir."""
|
||||
return str(self.plugin_dir_path)
|
||||
|
||||
@property
|
||||
def internal_plugin_dir(self) -> str:
|
||||
"""Return a string version of internal_plugin_dir."""
|
||||
return str(self.internal_plugin_dir_path)
|
||||
|
||||
@property
|
||||
def respath(self) -> str:
|
||||
"""Return a string version of respath."""
|
||||
return str(self.respath_path)
|
||||
|
||||
@property
|
||||
def home(self) -> str:
|
||||
"""Return a string version of home."""
|
||||
return str(self.home_path)
|
||||
|
||||
@property
|
||||
def default_journal_dir(self) -> str:
|
||||
"""Return a string version of default_journal_dir."""
|
||||
return str(self.default_journal_dir_path)
|
||||
|
||||
@staticmethod
|
||||
def _suppress_call(
|
||||
func: Callable[..., _T], exceptions: Union[Type[BaseException], List[Type[BaseException]]] = Exception,
|
||||
*args: Any, **kwargs: Any
|
||||
) -> Optional[_T]:
|
||||
if exceptions is None:
|
||||
exceptions = [Exception]
|
||||
|
||||
if not isinstance(exceptions, list):
|
||||
exceptions = [exceptions]
|
||||
|
||||
with contextlib.suppress(*exceptions): # type: ignore # it works fine, mypy
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return None
|
||||
|
||||
def get(self, key: str, default: Union[list, str, bool, int] = None) -> Union[list, str, bool, int]:
|
||||
"""
|
||||
Return the data for the requested key, or a default.
|
||||
|
||||
:param key: The key data is being requested for.
|
||||
:param default: The default to return if the key does not exist, defaults to None.
|
||||
:raises OSError: On Windows, if a Registry error occurs.
|
||||
:return: The data or the default.
|
||||
"""
|
||||
warnings.warn(DeprecationWarning('get is Deprecated. use the specific getter for your type'))
|
||||
logger.debug('Attempt to use Deprecated get() method\n' + ''.join(traceback.format_stack()))
|
||||
|
||||
if (l := self._suppress_call(self.get_list, ValueError, key, default=None)) is not None:
|
||||
return l
|
||||
|
||||
elif (s := self._suppress_call(self.get_str, ValueError, key, default=None)) is not None:
|
||||
return s
|
||||
|
||||
elif (b := self._suppress_call(self.get_bool, ValueError, key, default=None)) is not None:
|
||||
return b
|
||||
|
||||
elif (i := self._suppress_call(self.get_int, ValueError, key, default=None)) is not None:
|
||||
return i
|
||||
|
||||
return default # type: ignore
|
||||
|
||||
@abstractmethod
|
||||
def get_list(self, key: str, *, default: list = None) -> list:
|
||||
"""
|
||||
Return the list referred to by the given key if it exists, or the default.
|
||||
|
||||
Implements :meth:`AbstractConfig.get_list`.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def get_str(self, key: str, *, default: str = None) -> str:
|
||||
"""
|
||||
Return the string referred to by the given key if it exists, or the default.
|
||||
|
||||
:param key: The key data is being requested for.
|
||||
:param default: Default to return if the key does not exist, defaults to None.
|
||||
:raises ValueError: If an internal error occurs getting or converting a value.
|
||||
:raises OSError: On Windows, if a Registry error occurs.
|
||||
:return: The requested data or the default.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def get_bool(self, key: str, *, default: bool = None) -> bool:
|
||||
"""
|
||||
Return the bool referred to by the given key if it exists, or the default.
|
||||
|
||||
:param key: The key data is being requested for.
|
||||
:param default: Default to return if the key does not exist, defaults to None
|
||||
:raises ValueError: If an internal error occurs getting or converting a value
|
||||
:raises OSError: On Windows, if a Registry error occurs.
|
||||
:return: The requested data or the default
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def getint(self, key: str, *, default: int = 0) -> int:
|
||||
"""
|
||||
Getint is a Deprecated getter method.
|
||||
|
||||
See get_int for its replacement.
|
||||
:raises OSError: On Windows, if a Registry error occurs.
|
||||
"""
|
||||
warnings.warn(DeprecationWarning('getint is Deprecated. Use get_int instead'))
|
||||
logger.debug('Attempt to use Deprecated getint() method\n' + ''.join(traceback.format_stack()))
|
||||
|
||||
return self.get_int(key, default=default)
|
||||
|
||||
@abstractmethod
|
||||
def get_int(self, key: str, *, default: int = 0) -> int:
|
||||
"""
|
||||
Return the int referred to by key if it exists in the config.
|
||||
|
||||
For legacy reasons, the default is 0 and not None.
|
||||
|
||||
:param key: The key data is being requested for.
|
||||
:param default: Default to return if the key does not exist, defaults to 0.
|
||||
:raises ValueError: If the internal representation of this key cannot be converted to an int.
|
||||
:raises OSError: On Windows, if a Registry error occurs.
|
||||
:return: The requested data or the default.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def set(self, key: str, val: Union[int, str, List[str], bool]) -> None:
|
||||
"""
|
||||
Set the given key's data to the given value.
|
||||
|
||||
:param key: The key to set the value on.
|
||||
:param val: The value to set the key's data to.
|
||||
:raises ValueError: On an invalid type.
|
||||
:raises OSError: On Windows, if a Registry error occurs.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def delete(self, key: str, *, suppress=False) -> None:
|
||||
"""
|
||||
Delete the given key from the config.
|
||||
|
||||
:param key: The key to delete.
|
||||
:param suppress: bool - Whether to suppress any errors. Useful in case
|
||||
code to migrate settings is blindly removing an old key.
|
||||
:raises OSError: On Windows, if a registry error occurs.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def save(self) -> None:
|
||||
"""
|
||||
Save the current configuration.
|
||||
|
||||
:raises OSError: On Windows, if a Registry error occurs.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def close(self) -> None:
|
||||
"""Close this config and release any associated resources."""
|
||||
raise NotImplementedError
|
||||
|
||||
def get_password(self, account: str) -> None:
|
||||
"""Legacy password retrieval."""
|
||||
warnings.warn("password subsystem is no longer supported", DeprecationWarning)
|
||||
|
||||
def set_password(self, account: str, password: str) -> None:
|
||||
"""Legacy password setting."""
|
||||
warnings.warn("password subsystem is no longer supported", DeprecationWarning)
|
||||
|
||||
def delete_password(self, account: str) -> None:
|
||||
"""Legacy password deletion."""
|
||||
warnings.warn("password subsystem is no longer supported", DeprecationWarning)
|
||||
|
||||
|
||||
def get_config(*args, **kwargs) -> AbstractConfig:
|
||||
"""
|
||||
Get the appropriate config class for the current platform.
|
||||
|
||||
:param args: Args to be passed through to implementation.
|
||||
:param kwargs: Args to be passed through to implementation.
|
||||
:return: Instance of the implementation.
|
||||
"""
|
||||
if sys.platform == "darwin":
|
||||
from .darwin import MacConfig
|
||||
return MacConfig(*args, **kwargs)
|
||||
|
||||
elif sys.platform == "win32":
|
||||
from .windows import WinConfig
|
||||
return WinConfig(*args, **kwargs)
|
||||
|
||||
elif sys.platform == "linux":
|
||||
from .linux import LinuxConfig
|
||||
return LinuxConfig(*args, **kwargs)
|
||||
|
||||
else:
|
||||
raise ValueError(f'Unknown platform: {sys.platform=}')
|
||||
|
||||
|
||||
config = get_config()
|
184
config/darwin.py
Normal file
184
config/darwin.py
Normal file
@ -0,0 +1,184 @@
|
||||
import pathlib
|
||||
import sys
|
||||
from typing import Any, Dict, List, Union
|
||||
|
||||
from Foundation import ( # type: ignore
|
||||
NSApplicationSupportDirectory, NSBundle, NSDocumentDirectory, NSSearchPathForDirectoriesInDomains, NSUserDefaults,
|
||||
NSUserDomainMask
|
||||
)
|
||||
|
||||
from config import AbstractConfig, appname, logger
|
||||
|
||||
assert sys.platform == 'darwin'
|
||||
|
||||
|
||||
class MacConfig(AbstractConfig):
|
||||
"""MacConfig is the implementation of AbstractConfig for Darwin based OSes."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
support_path = pathlib.Path(
|
||||
NSSearchPathForDirectoriesInDomains(
|
||||
NSApplicationSupportDirectory, NSUserDomainMask, True
|
||||
)[0]
|
||||
)
|
||||
|
||||
self.app_dir_path = support_path / appname
|
||||
self.app_dir_path.mkdir(exist_ok=True)
|
||||
|
||||
self.plugin_dir_path = self.app_dir_path / 'plugins'
|
||||
self.plugin_dir_path.mkdir(exist_ok=True)
|
||||
|
||||
# Bundle IDs identify a singled app though out a system
|
||||
|
||||
if getattr(sys, 'frozen', False):
|
||||
exe_dir = pathlib.Path(sys.executable).parent
|
||||
self.internal_plugin_dir_path = exe_dir.parent / 'Library' / 'plugins'
|
||||
self.respath_path = exe_dir.parent / 'Resources'
|
||||
self.identifier = NSBundle.mainBundle().bundleIdentifier()
|
||||
|
||||
else:
|
||||
file_dir = pathlib.Path(__file__).parent.parent
|
||||
self.internal_plugin_dir_path = file_dir / 'plugins'
|
||||
self.respath_path = file_dir
|
||||
|
||||
self.identifier = f'uk.org.marginal.{appname.lower()}'
|
||||
NSBundle.mainBundle().infoDictionary()['CFBundleIdentifier'] = self.identifier
|
||||
|
||||
self.default_journal_dir_path = support_path / 'Frontier Developments' / 'Elite Dangerous'
|
||||
self._defaults: Any = NSUserDefaults.standardUserDefaults()
|
||||
self._settings: Dict[str, Union[int, str, list]] = dict(
|
||||
self._defaults.persistentDomainForName_(self.identifier) or {}
|
||||
) # make writeable
|
||||
|
||||
if (out_dir := self.get_str('out_dir')) is None or not pathlib.Path(out_dir).exists():
|
||||
self.set('outdir', NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, True)[0])
|
||||
|
||||
def __raw_get(self, key: str) -> Union[None, list, str, int]:
|
||||
"""
|
||||
Retrieve the raw data for the given key.
|
||||
|
||||
:param str: str - The key data is being requested for.
|
||||
:return: The requested data.
|
||||
"""
|
||||
res = self._settings.get(key)
|
||||
# On MacOS Catalina, with python.org python 3.9.2 any 'list'
|
||||
# has type __NSCFArray so a simple `isinstance(res, list)` is
|
||||
# False. So, check it's not-None, and not the other types.
|
||||
#
|
||||
# If we can find where to import the definition of NSCFArray
|
||||
# then we could possibly test against that.
|
||||
if res is not None and not isinstance(res, str) and not isinstance(res, int):
|
||||
return list(res)
|
||||
|
||||
return res
|
||||
|
||||
def get_str(self, key: str, *, default: str = None) -> str:
|
||||
"""
|
||||
Return the string referred to by the given key if it exists, or the default.
|
||||
|
||||
Implements :meth:`AbstractConfig.get_str`.
|
||||
"""
|
||||
res = self.__raw_get(key)
|
||||
if res is None:
|
||||
return default # type: ignore # Yes it could be None, but we're _assuming_ that people gave us a default
|
||||
|
||||
if not isinstance(res, str):
|
||||
raise ValueError(f'unexpected data returned from __raw_get: {type(res)=} {res}')
|
||||
|
||||
return res
|
||||
|
||||
def get_list(self, key: str, *, default: list = None) -> list:
|
||||
"""
|
||||
Return the list referred to by the given key if it exists, or the default.
|
||||
|
||||
Implements :meth:`AbstractConfig.get_list`.
|
||||
"""
|
||||
res = self.__raw_get(key)
|
||||
if res is None:
|
||||
return default # type: ignore # Yes it could be None, but we're _assuming_ that people gave us a default
|
||||
|
||||
elif not isinstance(res, list):
|
||||
raise ValueError(f'__raw_get returned unexpected type {type(res)=} {res!r}')
|
||||
|
||||
return res
|
||||
|
||||
def get_int(self, key: str, *, default: int = 0) -> int:
|
||||
"""
|
||||
Return the int referred to by key if it exists in the config.
|
||||
|
||||
Implements :meth:`AbstractConfig.get_int`.
|
||||
"""
|
||||
res = self.__raw_get(key)
|
||||
if res is None:
|
||||
return default
|
||||
|
||||
elif not isinstance(res, (str, int)):
|
||||
raise ValueError(f'__raw_get returned unexpected type {type(res)=} {res!r}')
|
||||
|
||||
try:
|
||||
return int(res)
|
||||
|
||||
except ValueError as e:
|
||||
logger.error(f'__raw_get returned {res!r} which cannot be parsed to an int: {e}')
|
||||
return default # type: ignore # Yes it could be None, but we're _assuming_ that people gave us a default
|
||||
|
||||
def get_bool(self, key: str, *, default: bool = None) -> bool:
|
||||
"""
|
||||
Return the bool referred to by the given key if it exists, or the default.
|
||||
|
||||
Implements :meth:`AbstractConfig.get_bool`.
|
||||
"""
|
||||
res = self.__raw_get(key)
|
||||
if res is None:
|
||||
return default # type: ignore # Yes it could be None, but we're _assuming_ that people gave us a default
|
||||
|
||||
elif not isinstance(res, bool):
|
||||
raise ValueError(f'__raw_get returned unexpected type {type(res)=} {res!r}')
|
||||
|
||||
return res
|
||||
|
||||
def set(self, key: str, val: Union[int, str, List[str], bool]) -> None:
|
||||
"""
|
||||
Set the given key's data to the given value.
|
||||
|
||||
Implements :meth:`AbstractConfig.set`.
|
||||
"""
|
||||
if self._settings is None:
|
||||
raise ValueError('attempt to use a closed _settings')
|
||||
|
||||
if not isinstance(val, (bool, str, int, list)):
|
||||
raise ValueError(f'Unexpected type for value {type(val)=}')
|
||||
|
||||
self._settings[key] = val
|
||||
|
||||
def delete(self, key: str, *, suppress=False) -> None:
|
||||
"""
|
||||
Delete the given key from the config.
|
||||
|
||||
Implements :meth:`AbstractConfig.delete`.
|
||||
"""
|
||||
try:
|
||||
del self._settings[key]
|
||||
|
||||
except Exception:
|
||||
if suppress:
|
||||
pass
|
||||
|
||||
def save(self) -> None:
|
||||
"""
|
||||
Save the current configuration.
|
||||
|
||||
Implements :meth:`AbstractConfig.save`.
|
||||
"""
|
||||
self._defaults.setPersistentDomain_forName_(self._settings, self.identifier)
|
||||
self._defaults.synchronize()
|
||||
|
||||
def close(self) -> None:
|
||||
"""
|
||||
Close this config and release any associated resources.
|
||||
|
||||
Implements :meth:`AbstractConfig.close`.
|
||||
"""
|
||||
self.save()
|
||||
self._defaults = None
|
245
config/linux.py
Normal file
245
config/linux.py
Normal file
@ -0,0 +1,245 @@
|
||||
"""Linux config implementation."""
|
||||
import os
|
||||
import pathlib
|
||||
import sys
|
||||
from configparser import ConfigParser
|
||||
from typing import TYPE_CHECKING, List, Optional, Union
|
||||
|
||||
from config import AbstractConfig, appname, logger
|
||||
|
||||
assert sys.platform == 'linux'
|
||||
|
||||
|
||||
class LinuxConfig(AbstractConfig):
|
||||
"""Linux implementation of AbstractConfig."""
|
||||
|
||||
SECTION = 'config'
|
||||
# TODO: I dislike this, would rather use a sane config file format. But here we are.
|
||||
__unescape_lut = {'\\': '\\', 'n': '\n', ';': ';', 'r': '\r', '#': '#'}
|
||||
__escape_lut = {'\\': '\\', '\n': 'n', ';': ';', '\r': 'r'}
|
||||
|
||||
def __init__(self, filename: Optional[str] = None) -> None:
|
||||
super().__init__()
|
||||
# http://standards.freedesktop.org/basedir-spec/latest/ar01s03.html
|
||||
xdg_data_home = pathlib.Path(os.getenv('XDG_DATA_HOME', default='~/.local/share')).expanduser()
|
||||
self.app_dir_path = xdg_data_home / appname
|
||||
self.app_dir_path.mkdir(exist_ok=True, parents=True)
|
||||
|
||||
self.plugin_dir_path = self.app_dir_path / 'plugins'
|
||||
self.plugin_dir_path.mkdir(exist_ok=True)
|
||||
|
||||
self.respath_path = pathlib.Path(__file__).parent.parent
|
||||
|
||||
self.internal_plugin_dir_path = self.respath_path / 'plugins'
|
||||
self.default_journal_dir_path = None # type: ignore
|
||||
self.identifier = f'uk.org.marginal.{appname.lower()}' # TODO: Unused?
|
||||
|
||||
config_home = pathlib.Path(os.getenv('XDG_CONFIG_HOME', default='~/.config')).expanduser()
|
||||
|
||||
self.filename = config_home / appname / f'{appname}.ini'
|
||||
if filename is not None:
|
||||
self.filename = pathlib.Path(filename)
|
||||
|
||||
self.filename.parent.mkdir(exist_ok=True, parents=True)
|
||||
|
||||
self.config: Optional[ConfigParser] = ConfigParser(comment_prefixes=('#',), interpolation=None)
|
||||
self.config.read(self.filename) # read() ignores files that dont exist
|
||||
|
||||
# Ensure that our section exists. This is here because configparser will happily create files for us, but it
|
||||
# does not magically create sections
|
||||
try:
|
||||
self.config[self.SECTION].get("this_does_not_exist", fallback=None)
|
||||
except KeyError:
|
||||
logger.info("Config section not found. Backing up existing file (if any) and readding a section header")
|
||||
if self.filename.exists():
|
||||
(self.filename.parent / f'{appname}.ini.backup').write_bytes(self.filename.read_bytes())
|
||||
|
||||
self.config.add_section(self.SECTION)
|
||||
|
||||
if (outdir := self.get_str('outdir')) is None or not pathlib.Path(outdir).is_dir():
|
||||
self.set('outdir', self.home)
|
||||
|
||||
def __escape(self, s: str) -> str:
|
||||
"""
|
||||
Escape a string using self.__escape_lut.
|
||||
|
||||
This does NOT support multi-character escapes.
|
||||
|
||||
:param s: str - String to be escaped.
|
||||
:return: str - The escaped string.
|
||||
"""
|
||||
out = ""
|
||||
for c in s:
|
||||
if c not in self.__escape_lut:
|
||||
out += c
|
||||
continue
|
||||
|
||||
out += '\\' + self.__escape_lut[c]
|
||||
|
||||
return out
|
||||
|
||||
def __unescape(self, s: str) -> str:
|
||||
"""
|
||||
Unescape a string.
|
||||
|
||||
:param s: str - The string to unescape.
|
||||
:return: str - The unescaped string.
|
||||
"""
|
||||
out: List[str] = []
|
||||
i = 0
|
||||
while i < len(s):
|
||||
c = s[i]
|
||||
if c != '\\':
|
||||
out.append(c)
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# We have a backslash, check what its escaping
|
||||
if i == len(s)-1:
|
||||
raise ValueError('Escaped string has unescaped trailer')
|
||||
|
||||
unescaped = self.__unescape_lut.get(s[i+1])
|
||||
if unescaped is None:
|
||||
raise ValueError(f'Unknown escape: \\ {s[i+1]}')
|
||||
|
||||
out.append(unescaped)
|
||||
i += 2
|
||||
|
||||
return "".join(out)
|
||||
|
||||
def __raw_get(self, key: str) -> Optional[str]:
|
||||
"""
|
||||
Get a raw data value from the config file.
|
||||
|
||||
:param key: str - The key data is being requested for.
|
||||
:return: str - The raw data, if found.
|
||||
"""
|
||||
if self.config is None:
|
||||
raise ValueError('Attempt to use a closed config')
|
||||
|
||||
return self.config[self.SECTION].get(key)
|
||||
|
||||
def get_str(self, key: str, *, default: str = None) -> str:
|
||||
"""
|
||||
Return the string referred to by the given key if it exists, or the default.
|
||||
|
||||
Implements :meth:`AbstractConfig.get_str`.
|
||||
"""
|
||||
data = self.__raw_get(key)
|
||||
if data is None:
|
||||
return default # type: ignore # Yes it could be None, but we're _assuming_ that people gave us a default
|
||||
|
||||
if '\n' in data:
|
||||
raise ValueError('asked for string, got list')
|
||||
|
||||
return self.__unescape(data)
|
||||
|
||||
def get_list(self, key: str, *, default: list = None) -> list:
|
||||
"""
|
||||
Return the list referred to by the given key if it exists, or the default.
|
||||
|
||||
Implements :meth:`AbstractConfig.get_list`.
|
||||
"""
|
||||
data = self.__raw_get(key)
|
||||
|
||||
if data is None:
|
||||
return default # type: ignore # Yes it could be None, but we're _assuming_ that people gave us a default
|
||||
|
||||
split = data.split('\n')
|
||||
if split[-1] != ';':
|
||||
raise ValueError('Encoded list does not have trailer sentinel')
|
||||
|
||||
return list(map(self.__unescape, split[:-1]))
|
||||
|
||||
def get_int(self, key: str, *, default: int = 0) -> int:
|
||||
"""
|
||||
Return the int referred to by key if it exists in the config.
|
||||
|
||||
Implements :meth:`AbstractConfig.get_int`.
|
||||
"""
|
||||
data = self.__raw_get(key)
|
||||
|
||||
if data is None:
|
||||
return default
|
||||
|
||||
try:
|
||||
return int(data)
|
||||
|
||||
except ValueError as e:
|
||||
raise ValueError(f'requested {key=} as int cannot be converted to int') from e
|
||||
|
||||
def get_bool(self, key: str, *, default: bool = None) -> bool:
|
||||
"""
|
||||
Return the bool referred to by the given key if it exists, or the default.
|
||||
|
||||
Implements :meth:`AbstractConfig.get_bool`.
|
||||
"""
|
||||
if self.config is None:
|
||||
raise ValueError('attempt to use a closed config')
|
||||
|
||||
data = self.__raw_get(key)
|
||||
if data is None:
|
||||
return default # type: ignore # Yes it could be None, but we're _assuming_ that people gave us a default
|
||||
|
||||
return bool(int(data))
|
||||
|
||||
def set(self, key: str, val: Union[int, str, List[str]]) -> None:
|
||||
"""
|
||||
Set the given key's data to the given value.
|
||||
|
||||
Implements :meth:`AbstractConfig.set`.
|
||||
"""
|
||||
if self.config is None:
|
||||
raise ValueError('attempt to use a closed config')
|
||||
|
||||
to_set: Optional[str] = None
|
||||
if isinstance(val, bool):
|
||||
to_set = str(int(val))
|
||||
|
||||
elif isinstance(val, str):
|
||||
to_set = self.__escape(val)
|
||||
|
||||
elif isinstance(val, int):
|
||||
to_set = str(val)
|
||||
|
||||
elif isinstance(val, list):
|
||||
to_set = '\n'.join([self.__escape(s) for s in val] + [';'])
|
||||
|
||||
else:
|
||||
raise ValueError(f'Unexpected type for value {type(val)=}')
|
||||
|
||||
self.config.set(self.SECTION, key, to_set)
|
||||
self.save()
|
||||
|
||||
def delete(self, key: str, *, suppress=False) -> None:
|
||||
"""
|
||||
Delete the given key from the config.
|
||||
|
||||
Implements :meth:`AbstractConfig.delete`.
|
||||
"""
|
||||
if self.config is None:
|
||||
raise ValueError('attempt to use a closed config')
|
||||
|
||||
self.config.remove_option(self.SECTION, key)
|
||||
self.save()
|
||||
|
||||
def save(self) -> None:
|
||||
"""
|
||||
Save the current configuration.
|
||||
|
||||
Implements :meth:`AbstractConfig.save`.
|
||||
"""
|
||||
if self.config is None:
|
||||
raise ValueError('attempt to use a closed config')
|
||||
|
||||
with open(self.filename, 'w', encoding='utf-8') as f:
|
||||
self.config.write(f)
|
||||
|
||||
def close(self) -> None:
|
||||
"""
|
||||
Close this config and release any associated resources.
|
||||
|
||||
Implements :meth:`AbstractConfig.close`.
|
||||
"""
|
||||
self.save()
|
||||
self.config = None
|
259
config/windows.py
Normal file
259
config/windows.py
Normal file
@ -0,0 +1,259 @@
|
||||
"""Windows config implementation."""
|
||||
|
||||
# spell-checker: words folderid deps hkey edcd
|
||||
import ctypes
|
||||
import functools
|
||||
import pathlib
|
||||
import sys
|
||||
import uuid
|
||||
import winreg
|
||||
from ctypes.wintypes import DWORD, HANDLE
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from config import AbstractConfig, applongname, appname, logger, update_interval
|
||||
|
||||
assert sys.platform == 'win32'
|
||||
|
||||
REG_RESERVED_ALWAYS_ZERO = 0
|
||||
|
||||
# This is the only way to do this from python without external deps (which do this anyway).
|
||||
FOLDERID_Documents = uuid.UUID('{FDD39AD0-238F-46AF-ADB4-6C85480369C7}')
|
||||
FOLDERID_LocalAppData = uuid.UUID('{F1B32785-6FBA-4FCF-9D55-7B8E7F157091}')
|
||||
FOLDERID_Profile = uuid.UUID('{5E6C858F-0E22-4760-9AFE-EA3317B67173}')
|
||||
FOLDERID_SavedGames = uuid.UUID('{4C5C32FF-BB9D-43b0-B5B4-2D72E54EAAA4}')
|
||||
|
||||
SHGetKnownFolderPath = ctypes.windll.shell32.SHGetKnownFolderPath
|
||||
SHGetKnownFolderPath.argtypes = [ctypes.c_char_p, DWORD, HANDLE, ctypes.POINTER(ctypes.c_wchar_p)]
|
||||
|
||||
CoTaskMemFree = ctypes.windll.ole32.CoTaskMemFree
|
||||
CoTaskMemFree.argtypes = [ctypes.c_void_p]
|
||||
|
||||
|
||||
def known_folder_path(guid: uuid.UUID) -> Optional[str]:
|
||||
"""Look up a Windows GUID to actual folder path name."""
|
||||
buf = ctypes.c_wchar_p()
|
||||
if SHGetKnownFolderPath(ctypes.create_string_buffer(guid.bytes_le), 0, 0, ctypes.byref(buf)):
|
||||
return None
|
||||
retval = buf.value # copy data
|
||||
CoTaskMemFree(buf) # and free original
|
||||
return retval
|
||||
|
||||
|
||||
class WinConfig(AbstractConfig):
|
||||
"""Implementation of AbstractConfig for Windows."""
|
||||
|
||||
def __init__(self, do_winsparkle=True) -> None:
|
||||
self.app_dir_path = pathlib.Path(str(known_folder_path(FOLDERID_LocalAppData))) / appname
|
||||
self.app_dir_path.mkdir(exist_ok=True)
|
||||
|
||||
self.plugin_dir_path = self.app_dir_path / 'plugins'
|
||||
self.plugin_dir_path.mkdir(exist_ok=True)
|
||||
|
||||
if getattr(sys, 'frozen', False):
|
||||
self.respath_path = pathlib.Path(sys.executable).parent
|
||||
self.internal_plugin_dir_path = self.respath_path / 'plugins'
|
||||
|
||||
else:
|
||||
self.respath_path = pathlib.Path(__file__).parent.parent
|
||||
self.internal_plugin_dir_path = self.respath_path / 'plugins'
|
||||
|
||||
self.home_path = pathlib.Path.home()
|
||||
|
||||
journal_dir_str = known_folder_path(FOLDERID_SavedGames)
|
||||
journaldir = pathlib.Path(journal_dir_str) if journal_dir_str is not None else None
|
||||
self.default_journal_dir_path = None # type: ignore
|
||||
if journaldir is not None:
|
||||
self.default_journal_dir_path = journaldir / 'Frontier Developments' / 'Elite Dangerous'
|
||||
|
||||
create_key_defaults = functools.partial(
|
||||
winreg.CreateKeyEx,
|
||||
key=winreg.HKEY_CURRENT_USER,
|
||||
access=winreg.KEY_ALL_ACCESS | winreg.KEY_WOW64_64KEY,
|
||||
)
|
||||
|
||||
try:
|
||||
self.__reg_handle: winreg.HKEYType = create_key_defaults(
|
||||
sub_key=r'Software\Marginal\EDMarketConnector'
|
||||
)
|
||||
if do_winsparkle:
|
||||
self.__setup_winsparkle()
|
||||
|
||||
except OSError:
|
||||
logger.exception('could not create required registry keys')
|
||||
raise
|
||||
|
||||
self.identifier = applongname
|
||||
if (outdir_str := self.get_str('outdir')) is None or not pathlib.Path(outdir_str).is_dir():
|
||||
docs = known_folder_path(FOLDERID_Documents)
|
||||
self.set('outdir', docs if docs is not None else self.home)
|
||||
|
||||
def __setup_winsparkle(self):
|
||||
"""Ensure the necessary Registry keys for WinSparkle are present."""
|
||||
create_key_defaults = functools.partial(
|
||||
winreg.CreateKeyEx,
|
||||
key=winreg.HKEY_CURRENT_USER,
|
||||
access=winreg.KEY_ALL_ACCESS | winreg.KEY_WOW64_64KEY,
|
||||
)
|
||||
try:
|
||||
edcd_handle: winreg.HKEYType = create_key_defaults(sub_key=r'Software\EDCD\EDMarketConnector')
|
||||
winsparkle_reg: winreg.HKEYType = winreg.CreateKeyEx(
|
||||
edcd_handle, sub_key='WinSparkle', access=winreg.KEY_ALL_ACCESS | winreg.KEY_WOW64_64KEY
|
||||
)
|
||||
|
||||
except OSError:
|
||||
logger.exception('could not open WinSparkle handle')
|
||||
raise
|
||||
|
||||
# set WinSparkle defaults - https://github.com/vslavik/winsparkle/wiki/Registry-Settings
|
||||
winreg.SetValueEx(
|
||||
winsparkle_reg, 'UpdateInterval', REG_RESERVED_ALWAYS_ZERO, winreg.REG_SZ, str(update_interval)
|
||||
)
|
||||
|
||||
try:
|
||||
winreg.QueryValueEx(winsparkle_reg, 'CheckForUpdates')
|
||||
|
||||
except FileNotFoundError:
|
||||
# Key doesn't exist, set it to a default
|
||||
winreg.SetValueEx(winsparkle_reg, 'CheckForUpdates', REG_RESERVED_ALWAYS_ZERO, winreg.REG_SZ, '1')
|
||||
|
||||
winsparkle_reg.Close()
|
||||
edcd_handle.Close()
|
||||
|
||||
def __get_regentry(self, key: str) -> Union[None, list, str, int]:
|
||||
"""Access the Registry for the raw entry."""
|
||||
try:
|
||||
value, _type = winreg.QueryValueEx(self.__reg_handle, key)
|
||||
except FileNotFoundError:
|
||||
# Key doesn't exist
|
||||
return None
|
||||
|
||||
# The type returned is actually as we'd expect for each of these. The casts are here for type checkers and
|
||||
# For programmers who want to actually know what is going on
|
||||
if _type == winreg.REG_SZ:
|
||||
return str(value)
|
||||
|
||||
elif _type == winreg.REG_DWORD:
|
||||
return int(value)
|
||||
|
||||
elif _type == winreg.REG_MULTI_SZ:
|
||||
return list(value)
|
||||
|
||||
else:
|
||||
logger.warning(f'registry key {key=} returned unknown type {_type=} {value=}')
|
||||
return None
|
||||
|
||||
def get_str(self, key: str, *, default: str = None) -> str:
|
||||
"""
|
||||
Return the string referred to by the given key if it exists, or the default.
|
||||
|
||||
Implements :meth:`AbstractConfig.get_str`.
|
||||
"""
|
||||
res = self.__get_regentry(key)
|
||||
if res is None:
|
||||
return default # type: ignore # Yes it could be None, but we're _assuming_ that people gave us a default
|
||||
|
||||
elif not isinstance(res, str):
|
||||
raise ValueError(f'Data from registry is not a string: {type(res)=} {res=}')
|
||||
|
||||
return res
|
||||
|
||||
def get_list(self, key: str, *, default: list = None) -> list:
|
||||
"""
|
||||
Return the list referred to by the given key if it exists, or the default.
|
||||
|
||||
Implements :meth:`AbstractConfig.get_list`.
|
||||
"""
|
||||
res = self.__get_regentry(key)
|
||||
if res is None:
|
||||
return default # type: ignore # Yes it could be None, but we're _assuming_ that people gave us a default
|
||||
|
||||
elif not isinstance(res, list):
|
||||
raise ValueError(f'Data from registry is not a list: {type(res)=} {res}')
|
||||
|
||||
return res
|
||||
|
||||
def get_int(self, key: str, *, default: int = 0) -> int:
|
||||
"""
|
||||
Return the int referred to by key if it exists in the config.
|
||||
|
||||
Implements :meth:`AbstractConfig.get_int`.
|
||||
"""
|
||||
res = self.__get_regentry(key)
|
||||
if res is None:
|
||||
return default
|
||||
|
||||
if not isinstance(res, int):
|
||||
raise ValueError(f'Data from registry is not an int: {type(res)=} {res}')
|
||||
|
||||
return res
|
||||
|
||||
def get_bool(self, key: str, *, default: bool = None) -> bool:
|
||||
"""
|
||||
Return the bool referred to by the given key if it exists, or the default.
|
||||
|
||||
Implements :meth:`AbstractConfig.get_bool`.
|
||||
"""
|
||||
res = self.get_int(key, default=default) # type: ignore
|
||||
if res is None:
|
||||
return default # type: ignore # Yes it could be None, but we're _assuming_ that people gave us a default
|
||||
|
||||
return bool(res)
|
||||
|
||||
def set(self, key: str, val: Union[int, str, List[str], bool]) -> None:
|
||||
"""
|
||||
Set the given key's data to the given value.
|
||||
|
||||
Implements :meth:`AbstractConfig.set`.
|
||||
"""
|
||||
reg_type = None
|
||||
if isinstance(val, str):
|
||||
reg_type = winreg.REG_SZ
|
||||
winreg.SetValueEx(self.__reg_handle, key, REG_RESERVED_ALWAYS_ZERO, winreg.REG_SZ, val)
|
||||
|
||||
elif isinstance(val, int): # The original code checked for numbers.Integral, I dont think that is needed.
|
||||
reg_type = winreg.REG_DWORD
|
||||
|
||||
elif isinstance(val, list):
|
||||
reg_type = winreg.REG_MULTI_SZ
|
||||
|
||||
elif isinstance(val, bool):
|
||||
reg_type = winreg.REG_DWORD
|
||||
val = int(val)
|
||||
|
||||
else:
|
||||
raise ValueError(f'Unexpected type for value {type(val)=}')
|
||||
|
||||
# Its complaining about the list, it works, tested on windows, ignored.
|
||||
winreg.SetValueEx(self.__reg_handle, key, REG_RESERVED_ALWAYS_ZERO, reg_type, val) # type: ignore
|
||||
|
||||
def delete(self, key: str, *, suppress=False) -> None:
|
||||
"""
|
||||
Delete the given key from the config.
|
||||
|
||||
'key' is relative to the base Registry path we use.
|
||||
|
||||
Implements :meth:`AbstractConfig.delete`.
|
||||
"""
|
||||
try:
|
||||
winreg.DeleteValue(self.__reg_handle, key)
|
||||
except OSError:
|
||||
if suppress:
|
||||
return
|
||||
|
||||
raise
|
||||
|
||||
def save(self) -> None:
|
||||
"""
|
||||
Save the configuration.
|
||||
|
||||
Not required for WinConfig as Registry keys are flushed on write.
|
||||
"""
|
||||
pass
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
Close this config and release any associated resources.
|
||||
|
||||
Implements :meth:`AbstractConfig.close`.
|
||||
"""
|
||||
self.__reg_handle.Close()
|
@ -1 +1 @@
|
||||
Subproject commit e499f68469bb4c3d7cecbc91ab67bd4a0bc40993
|
||||
Subproject commit 5ac8a2474e931bbf7cae351f7c987bc40af81012
|
@ -1,5 +1,12 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build ship and module databases from https://github.com/EDCD/coriolis-data/ ."""
|
||||
"""
|
||||
Build ship and module databases from https://github.com/EDCD/coriolis-data/ .
|
||||
|
||||
This script also utilise the file outfitting.csv. Due to how collate.py
|
||||
both reads and writes to this file a local copy is used, in the root of the
|
||||
project structure, is used for this purpose. If you want to utilise the
|
||||
FDevIDs/ version of the file, copy it over the local one.
|
||||
"""
|
||||
|
||||
|
||||
import csv
|
||||
|
15
dashboard.py
15
dashboard.py
@ -2,11 +2,11 @@
|
||||
|
||||
import json
|
||||
import pathlib
|
||||
import sys
|
||||
import time
|
||||
import tkinter as tk
|
||||
from calendar import timegm
|
||||
from os.path import getsize, isdir, isfile
|
||||
from sys import platform
|
||||
from typing import Any, Dict
|
||||
|
||||
from config import config
|
||||
@ -14,11 +14,11 @@ from EDMCLogging import get_main_logger
|
||||
|
||||
logger = get_main_logger()
|
||||
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
from watchdog.events import FileSystemEventHandler
|
||||
from watchdog.observers import Observer
|
||||
|
||||
elif platform == 'win32':
|
||||
elif sys.platform == 'win32':
|
||||
from watchdog.events import FileSystemEventHandler
|
||||
from watchdog.observers import Observer
|
||||
|
||||
@ -71,26 +71,25 @@ class Dashboard(FileSystemEventHandler):
|
||||
# File system events are unreliable/non-existent over network drives on Linux.
|
||||
# We can't easily tell whether a path points to a network drive, so assume
|
||||
# any non-standard logdir might be on a network drive and poll instead.
|
||||
polling = platform != 'win32'
|
||||
if not polling and not self.observer:
|
||||
if not (sys.platform != 'win32') and not self.observer:
|
||||
logger.debug('Setting up observer...')
|
||||
self.observer = Observer()
|
||||
self.observer.daemon = True
|
||||
self.observer.start()
|
||||
logger.debug('Done')
|
||||
|
||||
elif polling and self.observer:
|
||||
elif (sys.platform != 'win32') and self.observer:
|
||||
logger.debug('Using polling, stopping observer...')
|
||||
self.observer.stop()
|
||||
self.observer = None # type: ignore
|
||||
logger.debug('Done')
|
||||
|
||||
if not self.observed and not polling:
|
||||
if not self.observed and not (sys.platform != 'win32'):
|
||||
logger.debug('Starting observer...')
|
||||
self.observed = self.observer.schedule(self, self.currentdir)
|
||||
logger.debug('Done')
|
||||
|
||||
logger.info(f'{polling and "Polling" or "Monitoring"} Dashboard "{self.currentdir}"')
|
||||
logger.info(f'{(sys.platform != "win32") and "Polling" or "Monitoring"} Dashboard "{self.currentdir}"')
|
||||
|
||||
# Even if we're not intending to poll, poll at least once to process pre-existing
|
||||
# data and to check whether the watchdog thread has crashed due to events not
|
||||
|
@ -1,10 +1,12 @@
|
||||
"""Simple HTTP listener to be used with debugging various EDMC sends."""
|
||||
import gzip
|
||||
import json
|
||||
import pathlib
|
||||
import tempfile
|
||||
import threading
|
||||
import zlib
|
||||
from http import server
|
||||
from typing import Any, Callable, Tuple, Union
|
||||
from typing import Any, Callable, Literal, Tuple, Union
|
||||
from urllib.parse import parse_qs
|
||||
|
||||
from config import appname
|
||||
@ -30,8 +32,11 @@ class LoggingHandler(server.BaseHTTPRequestHandler):
|
||||
def do_POST(self) -> None: # noqa: N802 # I cant change it
|
||||
"""Handle POST."""
|
||||
logger.info(f"Received a POST for {self.path!r}!")
|
||||
data = self.rfile.read(int(self.headers['Content-Length'])).decode('utf-8', errors='replace')
|
||||
to_save = data
|
||||
data_raw: bytes = self.rfile.read(int(self.headers['Content-Length']))
|
||||
|
||||
encoding = self.headers.get('Content-Encoding')
|
||||
|
||||
to_save = data = self.get_printable(data_raw, encoding)
|
||||
|
||||
target_path = self.path
|
||||
if len(target_path) > 1 and target_path[0] == '/':
|
||||
@ -42,7 +47,7 @@ class LoggingHandler(server.BaseHTTPRequestHandler):
|
||||
|
||||
response: Union[Callable[[str], str], str, None] = DEFAULT_RESPONSES.get(target_path)
|
||||
if callable(response):
|
||||
response = response(data)
|
||||
response = response(to_save)
|
||||
|
||||
self.send_response_only(200, "OK")
|
||||
if response is not None:
|
||||
@ -64,12 +69,37 @@ class LoggingHandler(server.BaseHTTPRequestHandler):
|
||||
target_file = output_data_path / (safe_file_name(target_path) + '.log')
|
||||
if target_file.parent != output_data_path:
|
||||
logger.warning(f"REFUSING TO WRITE FILE THAT ISN'T IN THE RIGHT PLACE! {target_file=}")
|
||||
logger.warning(f'DATA FOLLOWS\n{data}')
|
||||
logger.warning(f'DATA FOLLOWS\n{data}') # type: ignore # mypy thinks data is a byte string here
|
||||
return
|
||||
|
||||
with output_lock, target_file.open('a') as f:
|
||||
f.write(to_save + "\n\n")
|
||||
|
||||
@staticmethod
|
||||
def get_printable(data: bytes, compression: Literal['deflate'] | Literal['gzip'] | str | None = None) -> str:
|
||||
"""
|
||||
Convert an incoming data stream into a string.
|
||||
|
||||
:param data: The data to convert
|
||||
:param compression: The compression to remove, defaults to None
|
||||
:raises ValueError: If compression is unknown
|
||||
:return: printable strings
|
||||
"""
|
||||
ret: bytes = b''
|
||||
if compression is None:
|
||||
ret = data
|
||||
|
||||
elif compression == 'deflate':
|
||||
ret = zlib.decompress(data)
|
||||
|
||||
elif compression == 'gzip':
|
||||
ret = gzip.decompress(data)
|
||||
|
||||
else:
|
||||
raise ValueError(f'Unknown encoding for data {compression!r}')
|
||||
|
||||
return ret.decode('utf-8', errors='replace')
|
||||
|
||||
|
||||
def safe_file_name(name: str):
|
||||
"""
|
||||
@ -103,6 +133,7 @@ def generate_inara_response(raw_data: str) -> str:
|
||||
|
||||
|
||||
def extract_edsm_data(data: str) -> dict[str, Any]:
|
||||
"""Extract relevant data from edsm data."""
|
||||
res = parse_qs(data)
|
||||
return {name: data[0] for name, data in res.items()}
|
||||
|
||||
|
@ -17,7 +17,8 @@ this document aims to enable anyone to quickly get up to speed on how to:
|
||||
available versions and asks the user to upgrade.
|
||||
|
||||
Note that for Windows only a 32-bit application is supported at this time.
|
||||
This is principally due to the Windows Registry handling in config.py.
|
||||
This is principally due to the Windows Registry handling in
|
||||
`config/windows.py`.
|
||||
|
||||
|
||||
# Environment
|
||||
@ -103,7 +104,7 @@ that.
|
||||
registry entries on Windows.
|
||||
|
||||
1. Application names, version and URL of the file with latest release
|
||||
information. These are all in the `config.py` file. See the
|
||||
information. These are all in the `config/__init__.py` file. See the
|
||||
`from config import ...` lines in setup.py.
|
||||
1. `appname`: The short appname, e.g. 'EDMarketConnector'
|
||||
2. `applongname`: The long appname, e.g. 'E:D Market Connector'
|
||||
@ -206,24 +207,24 @@ following.
|
||||
|
||||
1. You should by this time know what changes are going into the release, and
|
||||
which branch (stable or beta) you'll be ultimately updating.
|
||||
1. So as to make backing out any mistakes easier create a new branch for this
|
||||
2. So as to make backing out any mistakes easier create a new branch for this
|
||||
release, using a name like `release-4.0.2`. Do not use the tag
|
||||
`Release/4.0.2` form, that could cause confusion.
|
||||
1. `git checkout stable` # Or whichever other branch is appropriate.
|
||||
1. `git pull origin` # Ensures local branch is up to date.
|
||||
1. `git checkout -b release-4.0.2`
|
||||
|
||||
1. Get all the relevant code changes into this branch. This might mean
|
||||
3. Get all the relevant code changes into this branch. This might mean
|
||||
merging from another branch, such as an issue-specific one, or possibly
|
||||
cherry-picking commits. See [Contributing Guidelines](../Contributing.md)
|
||||
for how such branches should be named.
|
||||
|
||||
1. You should have already decided on the new
|
||||
[Version String](#Version-Strings), as it's specified in `config.py`. You'll
|
||||
need to redo the `.msi` build if you forgot. **Remember to do a fresh git
|
||||
commit for this change.**
|
||||
4. You should have already decided on the new
|
||||
[Version String](#Version-Strings), as it's specified in `config/__init__.py`.
|
||||
You'll need to redo the `.msi` build if you forgot. **Remember to do a fresh
|
||||
git commit for this change.**
|
||||
|
||||
1. Prepare a changelog text for the release. You'll need this both for the
|
||||
5. Prepare a changelog text for the release. You'll need this both for the
|
||||
GitHub release and the contents of the `edmarketconnector.xml` file if making
|
||||
a `stable` release, as well as any social media posts you make.
|
||||
1. The primary location of the changelog is [Changelog.md](../Changelog.md) -
|
||||
@ -408,7 +409,7 @@ changelog text to the correct section(s):
|
||||
|
||||
`https://raw.githubusercontent.com/EDCD/EDMarketConnector/releases/edmarketconnector.xml`
|
||||
|
||||
as per `config.py` `update_feed`.
|
||||
as per `config/__init__.py` `update_feed`.
|
||||
|
||||
NB: It can take some time for GitHub to show the changed
|
||||
edmarketconnector.xml contents to all users.
|
||||
@ -438,6 +439,8 @@ When changing the Python version (Major.Minor.Patch) used:
|
||||
|
||||
1. `.github/workflows/windows-build.yml` needs updating to have the GitHub
|
||||
based build use the correct version.
|
||||
2. `ChangeLog.md` - The `We now test against, and package with, Python
|
||||
M.m.P.` line.
|
||||
|
||||
1. Major or Minor level changes:
|
||||
|
||||
@ -446,3 +449,5 @@ When changing the Python version (Major.Minor.Patch) used:
|
||||
pythonXX.dll file.
|
||||
3. `.pre-commit-config.yaml` will need the `default_language_version`
|
||||
section updated to the appropriate version.
|
||||
4. All `.github/workflows/` files will need to be citing the correct
|
||||
version in any `uses: actions/setup-python` section.
|
||||
|
@ -1,10 +1,10 @@
|
||||
"""Implements locking of Journal directory."""
|
||||
|
||||
import pathlib
|
||||
import sys
|
||||
import tkinter as tk
|
||||
from enum import Enum
|
||||
from os import getpid as os_getpid
|
||||
from sys import platform
|
||||
from tkinter import ttk
|
||||
from typing import TYPE_CHECKING, Callable, Optional
|
||||
|
||||
@ -94,7 +94,7 @@ class JournalLock:
|
||||
|
||||
:return: LockResult - See the class Enum definition
|
||||
"""
|
||||
if platform == 'win32':
|
||||
if sys.platform == 'win32':
|
||||
logger.trace_if('journal-lock', 'win32, using msvcrt')
|
||||
# win32 doesn't have fcntl, so we have to use msvcrt
|
||||
import msvcrt
|
||||
@ -143,7 +143,7 @@ class JournalLock:
|
||||
return True # We weren't locked, and still aren't
|
||||
|
||||
unlocked = False
|
||||
if platform == 'win32':
|
||||
if sys.platform == 'win32':
|
||||
logger.trace_if('journal-lock', 'win32, using msvcrt')
|
||||
# win32 doesn't have fcntl, so we have to use msvcrt
|
||||
import msvcrt
|
||||
@ -206,10 +206,10 @@ class JournalLock:
|
||||
self.title(_('Journal directory already locked'))
|
||||
|
||||
# remove decoration
|
||||
if platform == 'win32':
|
||||
if sys.platform == 'win32':
|
||||
self.attributes('-toolwindow', tk.TRUE)
|
||||
|
||||
elif platform == 'darwin':
|
||||
elif sys.platform == 'darwin':
|
||||
# http://wiki.tcl.tk/13428
|
||||
parent.call('tk::unsupported::MacWindowStyle', 'style', self, 'utility')
|
||||
|
||||
|
71
l10n.py
71
l10n.py
@ -12,7 +12,6 @@ import warnings
|
||||
from collections import OrderedDict
|
||||
from contextlib import suppress
|
||||
from os.path import basename, dirname, isdir, isfile, join
|
||||
from sys import platform
|
||||
from typing import TYPE_CHECKING, Dict, Iterable, Optional, Set, TextIO, Union, cast
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@ -37,12 +36,12 @@ LANGUAGE_ID = '!Language'
|
||||
LOCALISATION_DIR = 'L10n'
|
||||
|
||||
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
from Foundation import ( # type: ignore # exists on Darwin
|
||||
NSLocale, NSNumberFormatter, NSNumberFormatterDecimalStyle
|
||||
)
|
||||
|
||||
elif platform == 'win32':
|
||||
elif sys.platform == 'win32':
|
||||
import ctypes
|
||||
from ctypes.wintypes import BOOL, DWORD, LPCVOID, LPCWSTR, LPWSTR
|
||||
if TYPE_CHECKING:
|
||||
@ -176,7 +175,7 @@ class _Translations:
|
||||
def available(self) -> Set[str]:
|
||||
"""Return a list of available language codes."""
|
||||
path = self.respath()
|
||||
if getattr(sys, 'frozen', False) and platform == 'darwin':
|
||||
if getattr(sys, 'frozen', False) and sys.platform == 'darwin':
|
||||
available = {
|
||||
x[:-len('.lproj')] for x in os.listdir(path)
|
||||
if x.endswith('.lproj') and isfile(join(x, 'Localizable.strings'))
|
||||
@ -204,7 +203,7 @@ class _Translations:
|
||||
def respath(self) -> pathlib.Path:
|
||||
"""Path to localisation files."""
|
||||
if getattr(sys, 'frozen', False):
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
return (pathlib.Path(sys.executable).parents[0] / os.pardir / 'Resources').resolve()
|
||||
|
||||
return pathlib.Path(dirname(sys.executable)) / LOCALISATION_DIR
|
||||
@ -233,7 +232,7 @@ class _Translations:
|
||||
except OSError:
|
||||
logger.exception(f'could not open {f}')
|
||||
|
||||
elif getattr(sys, 'frozen', False) and platform == 'darwin':
|
||||
elif getattr(sys, 'frozen', False) and sys.platform == 'darwin':
|
||||
return (self.respath() / f'{lang}.lproj' / 'Localizable.strings').open('r', encoding='utf-16')
|
||||
|
||||
return (self.respath() / f'{lang}.strings').open('r', encoding='utf-8')
|
||||
@ -243,7 +242,7 @@ class _Locale:
|
||||
"""Locale holds a few utility methods to convert data to and from localized versions."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
self.int_formatter = NSNumberFormatter.alloc().init()
|
||||
self.int_formatter.setNumberStyle_(NSNumberFormatterDecimalStyle)
|
||||
self.float_formatter = NSNumberFormatter.alloc().init()
|
||||
@ -276,7 +275,7 @@ class _Locale:
|
||||
if decimals == 0 and not isinstance(number, numbers.Integral):
|
||||
number = int(round(number))
|
||||
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
if not decimals and isinstance(number, numbers.Integral):
|
||||
return self.int_formatter.stringFromNumber_(number)
|
||||
|
||||
@ -298,7 +297,7 @@ class _Locale:
|
||||
:param string: The string to convert
|
||||
:return: None if the string cannot be parsed, otherwise an int or float dependant on input data.
|
||||
"""
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
return self.float_formatter.numberFromString_(string)
|
||||
|
||||
with suppress(ValueError):
|
||||
@ -309,7 +308,7 @@ class _Locale:
|
||||
|
||||
return None
|
||||
|
||||
def preferred_languages(self) -> Iterable[str]:
|
||||
def preferred_languages(self) -> Iterable[str]: # noqa: CCR001
|
||||
"""
|
||||
Return a list of preferred language codes.
|
||||
|
||||
@ -320,34 +319,46 @@ class _Locale:
|
||||
|
||||
:return: The preferred language list
|
||||
"""
|
||||
if platform == 'darwin':
|
||||
return NSLocale.preferredLanguages()
|
||||
languages: Iterable[str]
|
||||
if sys.platform == 'darwin':
|
||||
languages = NSLocale.preferredLanguages()
|
||||
|
||||
elif platform != 'win32':
|
||||
elif sys.platform != 'win32':
|
||||
# POSIX
|
||||
lang = locale.getlocale()[0]
|
||||
return lang and [lang.replace('_', '-')] or []
|
||||
languages = lang and [lang.replace('_', '-')] or []
|
||||
|
||||
def wszarray_to_list(array):
|
||||
offset = 0
|
||||
while offset < len(array):
|
||||
sz = ctypes.wstring_at(ctypes.addressof(array) + offset*2)
|
||||
if sz:
|
||||
yield sz
|
||||
offset += len(sz)+1
|
||||
else:
|
||||
def wszarray_to_list(array):
|
||||
offset = 0
|
||||
while offset < len(array):
|
||||
sz = ctypes.wstring_at(ctypes.addressof(array) + offset*2)
|
||||
if sz:
|
||||
yield sz
|
||||
offset += len(sz)+1
|
||||
|
||||
else:
|
||||
break
|
||||
else:
|
||||
break
|
||||
|
||||
num = ctypes.c_ulong()
|
||||
size = ctypes.c_ulong(0)
|
||||
if GetUserPreferredUILanguages(MUI_LANGUAGE_NAME, ctypes.byref(num), None, ctypes.byref(size)) and size.value:
|
||||
buf = ctypes.create_unicode_buffer(size.value)
|
||||
num = ctypes.c_ulong()
|
||||
size = ctypes.c_ulong(0)
|
||||
languages = []
|
||||
if GetUserPreferredUILanguages(
|
||||
MUI_LANGUAGE_NAME, ctypes.byref(num), None, ctypes.byref(size)
|
||||
) and size.value:
|
||||
buf = ctypes.create_unicode_buffer(size.value)
|
||||
|
||||
if GetUserPreferredUILanguages(MUI_LANGUAGE_NAME, ctypes.byref(num), ctypes.byref(buf), ctypes.byref(size)):
|
||||
return wszarray_to_list(buf)
|
||||
if GetUserPreferredUILanguages(
|
||||
MUI_LANGUAGE_NAME, ctypes.byref(num), ctypes.byref(buf), ctypes.byref(size)
|
||||
):
|
||||
languages = wszarray_to_list(buf)
|
||||
|
||||
return []
|
||||
# HACK: <n/a> | 2021-12-11: OneSky calls "Chinese Simplified" "zh-Hans"
|
||||
# in the name of the file, but that will be zh-CN in terms of
|
||||
# locale. So map zh-CN -> zh-Hans
|
||||
languages = ['zh-Hans' if lang == 'zh-CN' else lang for lang in languages]
|
||||
|
||||
return languages
|
||||
|
||||
|
||||
# singletons
|
||||
|
146
monitor.py
146
monitor.py
@ -1,16 +1,19 @@
|
||||
"""Monitor for new Journal files and contents of latest."""
|
||||
# v [sic]
|
||||
# spell-checker: words onfoot unforseen relog fsdjump suitloadoutid slotid suitid loadoutid fauto Intimidator
|
||||
# spell-checker: words joinacrew quitacrew sellshiponrebuy newbal navroute npccrewpaidwage sauto
|
||||
|
||||
import json
|
||||
import pathlib
|
||||
import queue
|
||||
import re
|
||||
import sys
|
||||
import threading
|
||||
from calendar import timegm
|
||||
from collections import OrderedDict, defaultdict
|
||||
from os import SEEK_END, SEEK_SET, listdir
|
||||
from os.path import basename, expanduser, isdir, join
|
||||
from sys import platform
|
||||
from time import gmtime, localtime, sleep, strftime, strptime, time
|
||||
from time import gmtime, localtime, mktime, sleep, strftime, strptime, time
|
||||
from typing import TYPE_CHECKING, Any, BinaryIO, Dict, List, MutableMapping, Optional
|
||||
from typing import OrderedDict as OrderedDictT
|
||||
from typing import Tuple
|
||||
@ -23,14 +26,17 @@ from config import config
|
||||
from edmc_data import edmc_suit_shortnames, edmc_suit_symbol_localised
|
||||
from EDMCLogging import get_main_logger
|
||||
|
||||
# spell-checker: words navroute
|
||||
|
||||
logger = get_main_logger()
|
||||
STARTUP = 'journal.startup'
|
||||
MAX_NAVROUTE_DISCREPANCY = 5 # Timestamp difference in seconds
|
||||
|
||||
if TYPE_CHECKING:
|
||||
def _(x: str) -> str:
|
||||
return x
|
||||
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
from fcntl import fcntl
|
||||
|
||||
from AppKit import NSWorkspace
|
||||
@ -38,7 +44,7 @@ if platform == 'darwin':
|
||||
from watchdog.observers import Observer
|
||||
F_GLOBAL_NOCACHE = 55
|
||||
|
||||
elif platform == 'win32':
|
||||
elif sys.platform == 'win32':
|
||||
import ctypes
|
||||
from ctypes.wintypes import BOOL, HWND, LPARAM, LPWSTR
|
||||
|
||||
@ -59,6 +65,10 @@ elif platform == 'win32':
|
||||
else:
|
||||
# Linux's inotify doesn't work over CIFS or NFS, so poll
|
||||
FileSystemEventHandler = object # dummy
|
||||
if TYPE_CHECKING:
|
||||
# this isn't ever used, but this will make type checking happy
|
||||
from watchdog.events import FileCreatedEvent
|
||||
from watchdog.observers import Observer
|
||||
|
||||
|
||||
# Journal handler
|
||||
@ -92,6 +102,8 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below
|
||||
# If 1 or 2 a LoadGame event will happen when the game goes live.
|
||||
# If 3 we need to inject a special 'StartUp' event since consumers won't see the LoadGame event.
|
||||
self.live = False
|
||||
# And whilst we're parsing *only to catch up on state*, we might not want to fully process some things
|
||||
self.catching_up = False
|
||||
|
||||
self.game_was_running = False # For generation of the "ShutDown" event
|
||||
|
||||
@ -111,6 +123,9 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below
|
||||
self.systempopulation: Optional[int] = None
|
||||
self.started: Optional[int] = None # Timestamp of the LoadGame event
|
||||
|
||||
self._navroute_retries_remaining = 0
|
||||
self._last_navroute_journal_timestamp: Optional[float] = None
|
||||
|
||||
self.__init_state()
|
||||
|
||||
def __init_state(self) -> None:
|
||||
@ -165,8 +180,11 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below
|
||||
'SuitLoadouts': {},
|
||||
'Taxi': None, # True whenever we are _in_ a taxi. ie, this is reset on Disembark etc.
|
||||
'Dropship': None, # Best effort as to whether or not the above taxi is a dropship.
|
||||
'StarPos': None, # Best effort current system's galaxy position.
|
||||
'Body': None,
|
||||
'BodyType': None,
|
||||
|
||||
'NavRoute': None,
|
||||
}
|
||||
|
||||
def start(self, root: 'tkinter.Tk') -> bool: # noqa: CCR001
|
||||
@ -215,7 +233,7 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below
|
||||
# File system events are unreliable/non-existent over network drives on Linux.
|
||||
# We can't easily tell whether a path points to a network drive, so assume
|
||||
# any non-standard logdir might be on a network drive and poll instead.
|
||||
polling = bool(config.get_str('journaldir')) and platform != 'win32'
|
||||
polling = bool(config.get_str('journaldir')) and sys.platform != 'win32'
|
||||
if not polling and not self.observer:
|
||||
logger.debug('Not polling, no observer, starting an observer...')
|
||||
self.observer = Observer()
|
||||
@ -272,6 +290,7 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below
|
||||
if self.observed:
|
||||
logger.debug('self.observed: Calling unschedule_all()')
|
||||
self.observed = None
|
||||
assert self.observer is not None, 'Observer was none but it is in use?'
|
||||
self.observer.unschedule_all()
|
||||
logger.debug('Done')
|
||||
|
||||
@ -331,9 +350,10 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below
|
||||
logfile = self.logfile
|
||||
if logfile:
|
||||
loghandle: BinaryIO = open(logfile, 'rb', 0) # unbuffered
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
fcntl(loghandle, F_GLOBAL_NOCACHE, -1) # required to avoid corruption on macOS over SMB
|
||||
|
||||
self.catching_up = True
|
||||
for line in loghandle:
|
||||
try:
|
||||
if b'"event":"Location"' in line:
|
||||
@ -344,6 +364,7 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below
|
||||
except Exception as ex:
|
||||
logger.debug(f'Invalid journal entry:\n{line!r}\n', exc_info=ex)
|
||||
|
||||
self.catching_up = False
|
||||
log_pos = loghandle.tell()
|
||||
|
||||
else:
|
||||
@ -382,9 +403,13 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below
|
||||
self.event_queue.put(None)
|
||||
self.live = False
|
||||
|
||||
emitter = None
|
||||
# Watchdog thread -- there is a way to get this by using self.observer.emitters and checking for an attribute:
|
||||
# watch, but that may have unforseen differences in behaviour.
|
||||
emitter = self.observed and self.observer._emitter_for_watch[self.observed] # Note: Uses undocumented attribute
|
||||
if self.observed:
|
||||
assert self.observer is not None, 'self.observer is None but also in use?'
|
||||
# Note: Uses undocumented attribute
|
||||
emitter = self.observed and self.observer._emitter_for_watch[self.observed]
|
||||
|
||||
logger.debug('Entering loop...')
|
||||
while True:
|
||||
@ -440,7 +465,7 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below
|
||||
|
||||
if logfile:
|
||||
loghandle = open(logfile, 'rb', 0) # unbuffered
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
fcntl(loghandle, F_GLOBAL_NOCACHE, -1) # required to avoid corruption on macOS over SMB
|
||||
|
||||
log_pos = 0
|
||||
@ -494,6 +519,8 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below
|
||||
entry: MutableMapping[str, Any] = json.loads(line, object_pairs_hook=OrderedDict)
|
||||
entry['timestamp'] # we expect this to exist # TODO: replace with assert? or an if key in check
|
||||
|
||||
self.__navroute_retry()
|
||||
|
||||
event_type = entry['event'].lower()
|
||||
if event_type == 'fileheader':
|
||||
self.live = False
|
||||
@ -685,7 +712,7 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below
|
||||
# This event is logged when a player (on foot) gets into a ship or SRV
|
||||
# Parameters:
|
||||
# • SRV: true if getting into SRV, false if getting into a ship
|
||||
# • Taxi: true when boarding a taxi transposrt ship
|
||||
# • Taxi: true when boarding a taxi transport ship
|
||||
# • Multicrew: true when boarding another player’s vessel
|
||||
# • ID: player’s ship ID (if players own vessel)
|
||||
# • StarSystem
|
||||
@ -713,7 +740,7 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below
|
||||
#
|
||||
# Parameters:
|
||||
# • SRV: true if getting out of SRV, false if getting out of a ship
|
||||
# • Taxi: true when getting out of a taxi transposrt ship
|
||||
# • Taxi: true when getting out of a taxi transport ship
|
||||
# • Multicrew: true when getting out of another player’s vessel
|
||||
# • ID: player’s ship ID (if players own vessel)
|
||||
# • StarSystem
|
||||
@ -775,13 +802,18 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below
|
||||
self.state['BodyType'] = None
|
||||
|
||||
if 'StarPos' in entry:
|
||||
self.coordinates = tuple(entry['StarPos']) # type: ignore
|
||||
# Plugins need this as well, so copy in state
|
||||
self.state['StarPos'] = self.coordinates = tuple(entry['StarPos']) # type: ignore
|
||||
|
||||
self.systemaddress = entry.get('SystemAddress')
|
||||
|
||||
self.systempopulation = entry.get('Population')
|
||||
|
||||
self.system = 'CQC' if entry['StarSystem'] == 'ProvingGround' else entry['StarSystem']
|
||||
if entry['StarSystem'] == 'ProvingGround':
|
||||
self.system = 'CQC'
|
||||
|
||||
else:
|
||||
self.system = entry['StarSystem']
|
||||
|
||||
self.station = entry.get('StationName') # May be None
|
||||
# If on foot in-station 'Docked' is false, but we have a
|
||||
@ -1304,17 +1336,15 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below
|
||||
self.state['Credits'] += entry.get('Refund', 0)
|
||||
self.state['Taxi'] = False
|
||||
|
||||
elif event_type == 'navroute':
|
||||
elif event_type == 'navroute' and not self.catching_up:
|
||||
# assume we've failed out the gate, then pull it back if things are fine
|
||||
self._last_navroute_journal_timestamp = mktime(strptime(entry['timestamp'], '%Y-%m-%dT%H:%M:%SZ'))
|
||||
self._navroute_retries_remaining = 11
|
||||
|
||||
# Added in ED 3.7 - multi-hop route details in NavRoute.json
|
||||
with open(join(self.currentdir, 'NavRoute.json'), 'rb') as rf: # type: ignore
|
||||
try:
|
||||
entry = json.load(rf)
|
||||
|
||||
except json.JSONDecodeError:
|
||||
logger.exception('Failed decoding NavRoute.json', exc_info=True)
|
||||
|
||||
else:
|
||||
self.state['NavRoute'] = entry
|
||||
# rather than duplicating this, lets just call the function
|
||||
if self.__navroute_retry():
|
||||
entry = self.state['NavRoute']
|
||||
|
||||
elif event_type == 'moduleinfo':
|
||||
with open(join(self.currentdir, 'ModulesInfo.json'), 'rb') as mf: # type: ignore
|
||||
@ -1398,7 +1428,7 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below
|
||||
category = self.category(received['Category'])
|
||||
state_category[received['Material']] += received['Quantity']
|
||||
|
||||
elif event_type == 'EngineerCraft' or (
|
||||
elif event_type == 'engineercraft' or (
|
||||
event_type == 'engineerlegacyconvert' and not entry.get('IsPreview')
|
||||
):
|
||||
|
||||
@ -1930,12 +1960,12 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below
|
||||
|
||||
:return: bool - True if the game is running.
|
||||
"""
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
for app in NSWorkspace.sharedWorkspace().runningApplications():
|
||||
if app.bundleIdentifier() == 'uk.co.frontier.EliteDangerous':
|
||||
return True
|
||||
|
||||
elif platform == 'win32':
|
||||
elif sys.platform == 'win32':
|
||||
def WindowTitle(h): # noqa: N802 # type: ignore
|
||||
if h:
|
||||
length = GetWindowTextLength(h) + 1
|
||||
@ -2157,6 +2187,72 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below
|
||||
|
||||
return slots
|
||||
|
||||
def _parse_navroute_file(self) -> Optional[dict[str, Any]]:
|
||||
"""Read and parse NavRoute.json."""
|
||||
if self.currentdir is None:
|
||||
raise ValueError('currentdir unset')
|
||||
|
||||
try:
|
||||
|
||||
with open(join(self.currentdir, 'NavRoute.json'), 'r') as f:
|
||||
raw = f.read()
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f'Could not open navroute file. Bailing: {e}')
|
||||
return None
|
||||
|
||||
try:
|
||||
data = json.loads(raw)
|
||||
|
||||
except json.JSONDecodeError:
|
||||
logger.exception('Failed to decode NavRoute.json', exc_info=True)
|
||||
return None
|
||||
|
||||
if 'timestamp' not in data: # quick sanity check
|
||||
return None
|
||||
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
def _parse_journal_timestamp(source: str) -> float:
|
||||
return mktime(strptime(source, '%Y-%m-%dT%H:%M:%SZ'))
|
||||
|
||||
def __navroute_retry(self) -> bool:
|
||||
"""Retry reading navroute files."""
|
||||
if self._navroute_retries_remaining == 0:
|
||||
return False
|
||||
|
||||
logger.info(f'Navroute read retry [{self._navroute_retries_remaining}]')
|
||||
self._navroute_retries_remaining -= 1
|
||||
|
||||
if self._last_navroute_journal_timestamp is None:
|
||||
logger.critical('Asked to retry for navroute but also no set time to compare? This is a bug.')
|
||||
return False
|
||||
|
||||
if (file := self._parse_navroute_file()) is None:
|
||||
logger.debug(
|
||||
'Failed to parse NavRoute.json. '
|
||||
+ ('Trying again' if self._navroute_retries_remaining > 0 else 'Giving up')
|
||||
)
|
||||
return False
|
||||
|
||||
# _parse_navroute_file verifies that this exists for us
|
||||
file_time = self._parse_journal_timestamp(file['timestamp'])
|
||||
if abs(file_time - self._last_navroute_journal_timestamp) > MAX_NAVROUTE_DISCREPANCY:
|
||||
logger.debug(
|
||||
f'Time discrepancy of more than {MAX_NAVROUTE_DISCREPANCY}s --'
|
||||
f' ({abs(file_time - self._last_navroute_journal_timestamp)}).'
|
||||
f' {"Trying again" if self._navroute_retries_remaining > 0 else "Giving up"}.'
|
||||
)
|
||||
return False
|
||||
|
||||
# everything is good, lets set what we need to and make sure we dont try again
|
||||
logger.info('Successfully read NavRoute file for last NavRoute event.')
|
||||
self.state['NavRoute'] = file
|
||||
self._navroute_retries_remaining = 0
|
||||
self._last_navroute_journal_timestamp = None
|
||||
return True
|
||||
|
||||
|
||||
# singleton
|
||||
monitor = EDLogs()
|
||||
|
@ -4,22 +4,21 @@
|
||||
# - OSX: page background should be a darker gray than systemWindowBody
|
||||
# selected tab foreground should be White when the window is active
|
||||
#
|
||||
|
||||
from sys import platform
|
||||
|
||||
import sys
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
|
||||
# Entire file may be imported by plugins
|
||||
|
||||
# Can't do this with styles on OSX - http://www.tkdocs.com/tutorial/styles.html#whydifficult
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
from platform import mac_ver
|
||||
PAGEFG = 'systemButtonText'
|
||||
PAGEBG = 'systemButtonActiveDarkShadow'
|
||||
elif platform == 'win32':
|
||||
|
||||
elif sys.platform == 'win32':
|
||||
PAGEFG = 'SystemWindowText'
|
||||
PAGEBG = 'SystemWindow' # typically white
|
||||
PAGEBG = 'SystemWindow' # typically white
|
||||
|
||||
|
||||
class Notebook(ttk.Notebook):
|
||||
@ -29,14 +28,14 @@ class Notebook(ttk.Notebook):
|
||||
ttk.Notebook.__init__(self, master, **kw)
|
||||
style = ttk.Style()
|
||||
|
||||
if platform=='darwin':
|
||||
if list(map(int, mac_ver()[0].split('.'))) >= [10,10]:
|
||||
if sys.platform == 'darwin':
|
||||
if list(map(int, mac_ver()[0].split('.'))) >= [10, 10]:
|
||||
# Hack for tab appearance with 8.5 on Yosemite & El Capitan. For proper fix see
|
||||
# https://github.com/tcltk/tk/commit/55c4dfca9353bbd69bbcec5d63bf1c8dfb461e25
|
||||
style.configure('TNotebook.Tab', padding=(12,10,12,2))
|
||||
style.configure('TNotebook.Tab', padding=(12, 10, 12, 2))
|
||||
style.map('TNotebook.Tab', foreground=[('selected', '!background', 'systemWhite')])
|
||||
self.grid(sticky=tk.NSEW) # Already padded apropriately
|
||||
elif platform == 'win32':
|
||||
self.grid(sticky=tk.NSEW) # Already padded apropriately
|
||||
elif sys.platform == 'win32':
|
||||
style.configure('nb.TFrame', background=PAGEBG)
|
||||
style.configure('nb.TButton', background=PAGEBG)
|
||||
style.configure('nb.TCheckbutton', foreground=PAGEFG, background=PAGEBG)
|
||||
@ -47,56 +46,60 @@ class Notebook(ttk.Notebook):
|
||||
self.grid(padx=10, pady=10, sticky=tk.NSEW)
|
||||
|
||||
|
||||
class Frame(platform == 'darwin' and tk.Frame or ttk.Frame):
|
||||
class Frame(sys.platform == 'darwin' and tk.Frame or ttk.Frame):
|
||||
|
||||
def __init__(self, master=None, **kw):
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
kw['background'] = kw.pop('background', PAGEBG)
|
||||
tk.Frame.__init__(self, master, **kw)
|
||||
tk.Frame(self).grid(pady=5)
|
||||
elif platform == 'win32':
|
||||
elif sys.platform == 'win32':
|
||||
ttk.Frame.__init__(self, master, style='nb.TFrame', **kw)
|
||||
ttk.Frame(self).grid(pady=5) # top spacer
|
||||
ttk.Frame(self).grid(pady=5) # top spacer
|
||||
else:
|
||||
ttk.Frame.__init__(self, master, **kw)
|
||||
ttk.Frame(self).grid(pady=5) # top spacer
|
||||
self.configure(takefocus = 1) # let the frame take focus so that no particular child is focused
|
||||
ttk.Frame(self).grid(pady=5) # top spacer
|
||||
self.configure(takefocus=1) # let the frame take focus so that no particular child is focused
|
||||
|
||||
|
||||
class Label(tk.Label):
|
||||
|
||||
def __init__(self, master=None, **kw):
|
||||
if platform in ['darwin', 'win32']:
|
||||
if sys.platform in ['darwin', 'win32']:
|
||||
kw['foreground'] = kw.pop('foreground', PAGEFG)
|
||||
kw['background'] = kw.pop('background', PAGEBG)
|
||||
else:
|
||||
kw['foreground'] = kw.pop('foreground', ttk.Style().lookup('TLabel', 'foreground'))
|
||||
kw['background'] = kw.pop('background', ttk.Style().lookup('TLabel', 'background'))
|
||||
tk.Label.__init__(self, master, **kw) # Just use tk.Label on all platforms
|
||||
tk.Label.__init__(self, master, **kw) # Just use tk.Label on all platforms
|
||||
|
||||
class Entry(platform == 'darwin' and tk.Entry or ttk.Entry):
|
||||
|
||||
class Entry(sys.platform == 'darwin' and tk.Entry or ttk.Entry):
|
||||
|
||||
def __init__(self, master=None, **kw):
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
kw['highlightbackground'] = kw.pop('highlightbackground', PAGEBG)
|
||||
tk.Entry.__init__(self, master, **kw)
|
||||
else:
|
||||
ttk.Entry.__init__(self, master, **kw)
|
||||
|
||||
class Button(platform == 'darwin' and tk.Button or ttk.Button):
|
||||
|
||||
class Button(sys.platform == 'darwin' and tk.Button or ttk.Button):
|
||||
|
||||
def __init__(self, master=None, **kw):
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
kw['highlightbackground'] = kw.pop('highlightbackground', PAGEBG)
|
||||
tk.Button.__init__(self, master, **kw)
|
||||
elif platform == 'win32':
|
||||
elif sys.platform == 'win32':
|
||||
ttk.Button.__init__(self, master, style='nb.TButton', **kw)
|
||||
else:
|
||||
ttk.Button.__init__(self, master, **kw)
|
||||
|
||||
class ColoredButton(platform == 'darwin' and tk.Label or tk.Button):
|
||||
|
||||
class ColoredButton(sys.platform == 'darwin' and tk.Label or tk.Button):
|
||||
|
||||
def __init__(self, master=None, **kw):
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
# Can't set Button background on OSX, so use a Label instead
|
||||
kw['relief'] = kw.pop('relief', tk.RAISED)
|
||||
self._command = kw.pop('command', None)
|
||||
@ -105,52 +108,55 @@ class ColoredButton(platform == 'darwin' and tk.Label or tk.Button):
|
||||
else:
|
||||
tk.Button.__init__(self, master, **kw)
|
||||
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
def _press(self, event):
|
||||
self._command()
|
||||
|
||||
class Checkbutton(platform == 'darwin' and tk.Checkbutton or ttk.Checkbutton):
|
||||
|
||||
class Checkbutton(sys.platform == 'darwin' and tk.Checkbutton or ttk.Checkbutton):
|
||||
|
||||
def __init__(self, master=None, **kw):
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
kw['foreground'] = kw.pop('foreground', PAGEFG)
|
||||
kw['background'] = kw.pop('background', PAGEBG)
|
||||
tk.Checkbutton.__init__(self, master, **kw)
|
||||
elif platform == 'win32':
|
||||
elif sys.platform == 'win32':
|
||||
ttk.Checkbutton.__init__(self, master, style='nb.TCheckbutton', **kw)
|
||||
else:
|
||||
ttk.Checkbutton.__init__(self, master, **kw)
|
||||
|
||||
class Radiobutton(platform == 'darwin' and tk.Radiobutton or ttk.Radiobutton):
|
||||
|
||||
class Radiobutton(sys.platform == 'darwin' and tk.Radiobutton or ttk.Radiobutton):
|
||||
|
||||
def __init__(self, master=None, **kw):
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
kw['foreground'] = kw.pop('foreground', PAGEFG)
|
||||
kw['background'] = kw.pop('background', PAGEBG)
|
||||
tk.Radiobutton.__init__(self, master, **kw)
|
||||
elif platform == 'win32':
|
||||
elif sys.platform == 'win32':
|
||||
ttk.Radiobutton.__init__(self, master, style='nb.TRadiobutton', **kw)
|
||||
else:
|
||||
ttk.Radiobutton.__init__(self, master, **kw)
|
||||
|
||||
class OptionMenu(platform == 'darwin' and tk.OptionMenu or ttk.OptionMenu):
|
||||
|
||||
class OptionMenu(sys.platform == 'darwin' and tk.OptionMenu or ttk.OptionMenu):
|
||||
|
||||
def __init__(self, master, variable, default=None, *values, **kw):
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
variable.set(default)
|
||||
bg = kw.pop('background', PAGEBG)
|
||||
tk.OptionMenu.__init__(self, master, variable, *values, **kw)
|
||||
self['background'] = bg
|
||||
elif platform == 'win32':
|
||||
elif sys.platform == 'win32':
|
||||
# OptionMenu derives from Menubutton at the Python level, so uses Menubutton's style
|
||||
ttk.OptionMenu.__init__(self, master, variable, default, *values, style='nb.TMenubutton', **kw)
|
||||
self['menu'].configure(background = PAGEBG)
|
||||
self['menu'].configure(background=PAGEBG)
|
||||
# Workaround for https://bugs.python.org/issue25684
|
||||
for i in range(0, self['menu'].index('end')+1):
|
||||
self['menu'].entryconfig(i, variable=variable)
|
||||
else:
|
||||
ttk.OptionMenu.__init__(self, master, variable, default, *values, **kw)
|
||||
self['menu'].configure(background = ttk.Style().lookup('TMenu', 'background'))
|
||||
self['menu'].configure(background=ttk.Style().lookup('TMenu', 'background'))
|
||||
# Workaround for https://bugs.python.org/issue25684
|
||||
for i in range(0, self['menu'].index('end')+1):
|
||||
self['menu'].entryconfig(i, variable=variable)
|
||||
|
@ -1,5 +1,27 @@
|
||||
"""Coriolis ship export."""
|
||||
|
||||
# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $#
|
||||
# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $#
|
||||
#
|
||||
# This is an EDMC 'core' plugin.
|
||||
#
|
||||
# All EDMC plugins are *dynamically* loaded at run-time.
|
||||
#
|
||||
# We build for Windows using `py2exe`.
|
||||
#
|
||||
# `py2exe` can't possibly know about anything in the dynamically loaded
|
||||
# core plugins.
|
||||
#
|
||||
# Thus you **MUST** check if any imports you add in this file are only
|
||||
# referenced in this file (or only in any other core plugin), and if so...
|
||||
#
|
||||
# YOU MUST ENSURE THAT PERTINENT ADJUSTMENTS ARE MADE IN `setup.py`
|
||||
# SO AS TO ENSURE THE FILES ARE ACTUALLY PRESENT IN AN END-USER
|
||||
# INSTALLATION ON WINDOWS.
|
||||
#
|
||||
#
|
||||
# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $#
|
||||
# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $#
|
||||
import base64
|
||||
import gzip
|
||||
import io
|
||||
|
@ -23,6 +23,28 @@
|
||||
#
|
||||
|
||||
|
||||
# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $#
|
||||
# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $#
|
||||
#
|
||||
# This is an EDMC 'core' plugin.
|
||||
#
|
||||
# All EDMC plugins are *dynamically* loaded at run-time.
|
||||
#
|
||||
# We build for Windows using `py2exe`.
|
||||
#
|
||||
# `py2exe` can't possibly know about anything in the dynamically loaded
|
||||
# core plugins.
|
||||
#
|
||||
# Thus you **MUST** check if any imports you add in this file are only
|
||||
# referenced in this file (or only in any other core plugin), and if so...
|
||||
#
|
||||
# YOU MUST ENSURE THAT PERTINENT ADJUSTMENTS ARE MADE IN `setup.py`
|
||||
# SO AS TO ENSURE THE FILES ARE ACTUALLY PRESENT IN AN END-USER
|
||||
# INSTALLATION ON WINDOWS.
|
||||
#
|
||||
#
|
||||
# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $#
|
||||
# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $#
|
||||
import sys
|
||||
from typing import TYPE_CHECKING, Any, Optional
|
||||
|
||||
|
347
plugins/eddn.py
347
plugins/eddn.py
@ -1,5 +1,27 @@
|
||||
"""Handle exporting data to EDDN."""
|
||||
|
||||
# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $#
|
||||
# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $#
|
||||
#
|
||||
# This is an EDMC 'core' plugin.
|
||||
#
|
||||
# All EDMC plugins are *dynamically* loaded at run-time.
|
||||
#
|
||||
# We build for Windows using `py2exe`.
|
||||
#
|
||||
# `py2exe` can't possibly know about anything in the dynamically loaded
|
||||
# core plugins.
|
||||
#
|
||||
# Thus you **MUST** check if any imports you add in this file are only
|
||||
# referenced in this file (or only in any other core plugin), and if so...
|
||||
#
|
||||
# YOU MUST ENSURE THAT PERTINENT ADJUSTMENTS ARE MADE IN `setup.py`
|
||||
# SO AS TO ENSURE THE FILES ARE ACTUALLY PRESENT IN AN END-USER
|
||||
# INSTALLATION ON WINDOWS.
|
||||
#
|
||||
#
|
||||
# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $#
|
||||
# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $#
|
||||
import itertools
|
||||
import json
|
||||
import pathlib
|
||||
@ -10,6 +32,7 @@ from collections import OrderedDict
|
||||
from os import SEEK_SET
|
||||
from os.path import join
|
||||
from platform import system
|
||||
from textwrap import dedent
|
||||
from typing import TYPE_CHECKING, Any, Dict, Iterator, List, Mapping, MutableMapping, Optional
|
||||
from typing import OrderedDict as OrderedDictT
|
||||
from typing import TextIO, Tuple, Union
|
||||
@ -21,12 +44,13 @@ import killswitch
|
||||
import myNotebook as nb # noqa: N813
|
||||
import plug
|
||||
from companion import CAPIData, category_map
|
||||
from config import applongname, appversion_nobuild, config, debug_senders
|
||||
from config import applongname, appversion_nobuild, config, debug_senders, user_agent
|
||||
from EDMCLogging import get_main_logger
|
||||
from monitor import monitor
|
||||
from myNotebook import Frame
|
||||
from prefs import prefsVersion
|
||||
from ttkHyperlinkLabel import HyperlinkLabel
|
||||
from util import text
|
||||
|
||||
if sys.platform != 'win32':
|
||||
from fcntl import LOCK_EX, LOCK_NB, lockf
|
||||
@ -125,6 +149,7 @@ class EDDN:
|
||||
def __init__(self, parent: tk.Tk):
|
||||
self.parent: tk.Tk = parent
|
||||
self.session = requests.Session()
|
||||
self.session.headers['User-Agent'] = user_agent
|
||||
self.replayfile: Optional[TextIO] = None # For delayed messages
|
||||
self.replaylog: List[str] = []
|
||||
|
||||
@ -167,6 +192,10 @@ class EDDN:
|
||||
|
||||
def flush(self):
|
||||
"""Flush the replay file, clearing any data currently there that is not in the replaylog list."""
|
||||
if self.replayfile is None:
|
||||
logger.error('replayfile is None!')
|
||||
return
|
||||
|
||||
self.replayfile.seek(0, SEEK_SET)
|
||||
self.replayfile.truncate()
|
||||
for line in self.replaylog:
|
||||
@ -212,7 +241,20 @@ class EDDN:
|
||||
('message', msg['message']),
|
||||
])
|
||||
|
||||
r = self.session.post(self.eddn_url, data=json.dumps(to_send), timeout=self.TIMEOUT)
|
||||
# About the smallest request is going to be (newlines added for brevity):
|
||||
# {"$schemaRef":"https://eddn.edcd.io/schemas/commodity/3","header":{"softwareName":"E:D Market
|
||||
# Connector Windows","softwareVersion":"5.3.0-beta4extra","uploaderID":"abcdefghijklm"},"messag
|
||||
# e":{"systemName":"delphi","stationName":"The Oracle","marketId":128782803,"timestamp":"2022-0
|
||||
# 1-26T12:00:00Z","commodities":[]}}
|
||||
#
|
||||
# Which comes to 315 bytes (including \n) and compresses to 244 bytes. So lets just compress everything
|
||||
|
||||
encoded, compressed = text.gzip(json.dumps(to_send, separators=(',', ':')), max_size=0)
|
||||
headers: None | dict[str, str] = None
|
||||
if compressed:
|
||||
headers = {'Content-Encoding': 'gzip'}
|
||||
|
||||
r = self.session.post(self.eddn_url, data=encoded, timeout=self.TIMEOUT, headers=headers)
|
||||
if r.status_code != requests.codes.ok:
|
||||
|
||||
# Check if EDDN is still objecting to an empty commodities list
|
||||
@ -225,18 +267,48 @@ class EDDN:
|
||||
logger.trace_if('plugin.eddn', "EDDN is still objecting to empty commodities data")
|
||||
return # We want to silence warnings otherwise
|
||||
|
||||
if r.status_code == 413:
|
||||
extra_data = {
|
||||
'schema_ref': msg.get('$schemaRef', 'Unset $schemaRef!'),
|
||||
'sent_data_len': str(len(encoded)),
|
||||
}
|
||||
|
||||
if '/journal/' in extra_data['schema_ref']:
|
||||
extra_data['event'] = msg.get('message', {}).get('event', 'No Event Set')
|
||||
|
||||
self._log_response(r, header_msg='Got a 413 while POSTing data', **extra_data)
|
||||
return # drop the error
|
||||
|
||||
if not self.UNKNOWN_SCHEMA_RE.match(r.text):
|
||||
logger.debug(
|
||||
f'''Status from POST wasn't OK:
|
||||
Status\t{r.status_code}
|
||||
URL\t{r.url}
|
||||
Headers\t{r.headers}
|
||||
Content:\n{r.text}
|
||||
Msg:\n{msg}'''
|
||||
)
|
||||
self._log_response(r, header_msg='Status from POST wasn\'t 200 (OK)')
|
||||
|
||||
r.raise_for_status()
|
||||
|
||||
def _log_response(
|
||||
self,
|
||||
response: requests.Response,
|
||||
header_msg='Failed to POST to EDDN',
|
||||
**kwargs
|
||||
) -> None:
|
||||
"""
|
||||
Log a response object with optional additional data.
|
||||
|
||||
:param response: The response to log
|
||||
:param header_msg: A header message to add to the log, defaults to 'Failed to POST to EDDN'
|
||||
:param kwargs: Any other notes to add, will be added below the main data in the same format.
|
||||
"""
|
||||
additional_data = "\n".join(
|
||||
f'''{name.replace('_', ' ').title():<8}:\t{value}''' for name, value in kwargs.items()
|
||||
)
|
||||
|
||||
logger.debug(dedent(f'''\
|
||||
{header_msg}:
|
||||
Status :\t{response.status_code}
|
||||
URL :\t{response.url}
|
||||
Headers :\t{response.headers}
|
||||
Content :\t{response.text}
|
||||
''')+additional_data)
|
||||
|
||||
def sendreplay(self) -> None: # noqa: CCR001
|
||||
"""Send cached Journal lines to EDDN."""
|
||||
if not self.replayfile:
|
||||
@ -717,20 +789,27 @@ Msg:\n{msg}'''
|
||||
def entry_augment_system_data(
|
||||
self,
|
||||
entry: MutableMapping[str, Any],
|
||||
system_name: str
|
||||
system_name: str,
|
||||
system_coordinates: list
|
||||
) -> Union[str, MutableMapping[str, Any]]:
|
||||
"""
|
||||
Augment a journal entry with necessary system data.
|
||||
|
||||
:param entry: The journal entry to be augmented.
|
||||
:param system_name: Name of current star system.
|
||||
:param systemname_field_name: Name of journal key for system name.
|
||||
:param system_coordinates: Coordinates of current star system.
|
||||
:return: The augmented version of entry.
|
||||
"""
|
||||
# If 'SystemName' or 'System' is there, it's directly from a journal event.
|
||||
# If they're not there *and* 'StarSystem' isn't either, then we add the latter.
|
||||
if 'SystemName' not in entry and 'System' not in entry and 'StarSystem' not in entry:
|
||||
entry['StarSystem'] = system_name
|
||||
if system_name is None or not isinstance(system_name, str) or system_name == '':
|
||||
# Bad assumptions if this is the case
|
||||
logger.warning(f'No system name in entry, and system_name was not set either! entry:\n{entry!r}\n')
|
||||
return "passed-in system_name is empty, can't add System"
|
||||
|
||||
else:
|
||||
entry['StarSystem'] = system_name
|
||||
|
||||
if 'SystemAddress' not in entry:
|
||||
if this.systemaddress is None:
|
||||
@ -740,21 +819,29 @@ Msg:\n{msg}'''
|
||||
entry['SystemAddress'] = this.systemaddress
|
||||
|
||||
if 'StarPos' not in entry:
|
||||
if not this.coordinates:
|
||||
logger.warning("this.coordinates is None, can't add StarPos")
|
||||
return "this.coordinates is None, can't add StarPos"
|
||||
# Prefer the passed-in, probably monitor.state version
|
||||
if system_coordinates is not None:
|
||||
entry['StarPos'] = system_coordinates
|
||||
|
||||
entry['StarPos'] = list(this.coordinates)
|
||||
# TODO: Deprecate in-plugin tracking
|
||||
elif this.coordinates is not None:
|
||||
entry['StarPos'] = list(this.coordinates)
|
||||
|
||||
else:
|
||||
logger.warning("Neither this_coordinates or this.coordinates set, can't add StarPos")
|
||||
return 'No source for adding StarPos to approachsettlement/1 !'
|
||||
|
||||
return entry
|
||||
|
||||
def export_journal_fssdiscoveryscan(
|
||||
self, cmdr: str, system: str, is_beta: bool, entry: Mapping[str, Any]
|
||||
self, cmdr: str, system_name: str, system_starpos: list, is_beta: bool, entry: Mapping[str, Any]
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Send an FSSDiscoveryScan to EDDN on the correct schema.
|
||||
|
||||
:param cmdr: the commander under which this upload is made
|
||||
:param system_name: Name of current star system
|
||||
:param system_starpos: Coordinates of current star system
|
||||
:param is_beta: whether or not we are in beta mode
|
||||
:param entry: the journal entry to send
|
||||
"""
|
||||
@ -767,7 +854,7 @@ Msg:\n{msg}'''
|
||||
#######################################################################
|
||||
# Augmentations
|
||||
#######################################################################
|
||||
ret = this.eddn.entry_augment_system_data(entry, system)
|
||||
ret = this.eddn.entry_augment_system_data(entry, system_name, system_starpos)
|
||||
if isinstance(ret, str):
|
||||
return ret
|
||||
|
||||
@ -783,13 +870,14 @@ Msg:\n{msg}'''
|
||||
return None
|
||||
|
||||
def export_journal_navbeaconscan(
|
||||
self, cmdr: str, system_name: str, is_beta: bool, entry: Mapping[str, Any]
|
||||
self, cmdr: str, system_name: str, system_starpos: list, is_beta: bool, entry: Mapping[str, Any]
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Send an NavBeaconScan to EDDN on the correct schema.
|
||||
|
||||
:param cmdr: the commander under which this upload is made
|
||||
:param system_name: Name of the current system.
|
||||
:param system_starpos: Coordinates of current star system
|
||||
:param is_beta: whether or not we are in beta mode
|
||||
:param entry: the journal entry to send
|
||||
"""
|
||||
@ -802,7 +890,7 @@ Msg:\n{msg}'''
|
||||
#######################################################################
|
||||
# Augmentations
|
||||
#######################################################################
|
||||
ret = this.eddn.entry_augment_system_data(entry, system_name)
|
||||
ret = this.eddn.entry_augment_system_data(entry, system_name, system_starpos)
|
||||
if isinstance(ret, str):
|
||||
return ret
|
||||
|
||||
@ -817,13 +905,14 @@ Msg:\n{msg}'''
|
||||
this.eddn.export_journal_entry(cmdr, entry, msg)
|
||||
return None
|
||||
|
||||
def export_journal_codexentry(
|
||||
self, cmdr: str, is_beta: bool, entry: MutableMapping[str, Any]
|
||||
def export_journal_codexentry( # noqa: CCR001
|
||||
self, cmdr: str, system_starpos: list, is_beta: bool, entry: MutableMapping[str, Any]
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Send a CodexEntry to EDDN on the correct schema.
|
||||
|
||||
:param cmdr: the commander under which this upload is made
|
||||
:param system_starpos: Coordinates of current star system
|
||||
:param is_beta: whether or not we are in beta mode
|
||||
:param entry: the journal entry to send
|
||||
"""
|
||||
@ -858,21 +947,37 @@ Msg:\n{msg}'''
|
||||
# Augmentations
|
||||
#######################################################################
|
||||
# General 'system' augmentations
|
||||
ret = this.eddn.entry_augment_system_data(entry, entry['System'])
|
||||
ret = this.eddn.entry_augment_system_data(entry, entry['System'], system_starpos)
|
||||
if isinstance(ret, str):
|
||||
return ret
|
||||
|
||||
entry = ret
|
||||
|
||||
# Set BodyName if it's available from Status.json
|
||||
if this.status_body_name is not None:
|
||||
if this.status_body_name is None or not isinstance(this.status_body_name, str):
|
||||
logger.warning(f'this.status_body_name was not set properly:'
|
||||
f' "{this.status_body_name}" ({type(this.status_body_name)})')
|
||||
|
||||
else:
|
||||
entry['BodyName'] = this.status_body_name
|
||||
# Only set BodyID if journal BodyName matches the Status.json one.
|
||||
# This avoids binary body issues.
|
||||
if this.status_body_name == this.body_name:
|
||||
entry['BodyID'] = this.body_id
|
||||
if this.body_id is not None and isinstance(this.body_id, int):
|
||||
entry['BodyID'] = this.body_id
|
||||
|
||||
else:
|
||||
logger.warning(f'this.body_id was not set properly: "{this.body_id}" ({type(this.body_id)})')
|
||||
#######################################################################
|
||||
|
||||
for k, v in entry.items():
|
||||
if v is None or isinstance(v, str) and v == '':
|
||||
logger.warning(f'post-processing entry contains entry["{k}"] = {v} {(type(v))}')
|
||||
# We should drop this message and VERY LOUDLY inform the
|
||||
# user, in the hopes they'll open a bug report with the
|
||||
# raw Journal event that caused this.
|
||||
return 'CodexEntry had empty string, PLEASE ALERT THE EDMC DEVELOPERS'
|
||||
|
||||
msg = {
|
||||
'$schemaRef': f'https://eddn.edcd.io/schemas/codexentry/1{"/test" if is_beta else ""}',
|
||||
'message': entry
|
||||
@ -882,12 +987,13 @@ Msg:\n{msg}'''
|
||||
return None
|
||||
|
||||
def export_journal_scanbarycentre(
|
||||
self, cmdr: str, is_beta: bool, entry: Mapping[str, Any]
|
||||
self, cmdr: str, system_starpos: list, is_beta: bool, entry: Mapping[str, Any]
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Send a ScanBaryCentre to EDDN on the correct schema.
|
||||
|
||||
:param cmdr: the commander under which this upload is made
|
||||
:param system_starpos: Coordinates of current star system
|
||||
:param is_beta: whether or not we are in beta mode
|
||||
:param entry: the journal entry to send
|
||||
"""
|
||||
@ -913,7 +1019,7 @@ Msg:\n{msg}'''
|
||||
#######################################################################
|
||||
# Augmentations
|
||||
#######################################################################
|
||||
ret = this.eddn.entry_augment_system_data(entry, entry['StarSystem'])
|
||||
ret = this.eddn.entry_augment_system_data(entry, entry['StarSystem'], system_starpos)
|
||||
if isinstance(ret, str):
|
||||
return ret
|
||||
|
||||
@ -935,6 +1041,7 @@ Msg:\n{msg}'''
|
||||
Send a NavRoute to EDDN on the correct schema.
|
||||
|
||||
:param cmdr: the commander under which this upload is made
|
||||
:param system_starpos: Coordinates of current star system
|
||||
:param is_beta: whether or not we are in beta mode
|
||||
:param entry: the journal entry to send
|
||||
"""
|
||||
@ -996,6 +1103,86 @@ Msg:\n{msg}'''
|
||||
this.eddn.export_journal_entry(cmdr, entry, msg)
|
||||
return None
|
||||
|
||||
def export_journal_approachsettlement(
|
||||
self, cmdr: str, system_name: str, system_starpos: list, is_beta: bool, entry: MutableMapping[str, Any]
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Send an ApproachSettlement to EDDN on the correct schema.
|
||||
|
||||
:param cmdr: the commander under which this upload is made
|
||||
:param system_name: Name of current star system
|
||||
:param system_starpos: Coordinates of current star system
|
||||
:param is_beta: whether or not we are in beta mode
|
||||
:param entry: the journal entry to send
|
||||
"""
|
||||
# {
|
||||
# "BodyID": 32,
|
||||
# "BodyName": "Ix 5 a a",
|
||||
# "Latitude": 17.090912,
|
||||
# "Longitude": 160.236679,
|
||||
# "MarketID": 3915738368,
|
||||
# "Name": "Arnold Defence Base",
|
||||
# "SystemAddress": 2381282543963,
|
||||
# "event": "ApproachSettlement",
|
||||
# "timestamp": "2021-10-14T12:37:54Z"
|
||||
# }
|
||||
#######################################################################
|
||||
# Augmentations
|
||||
#######################################################################
|
||||
# In this case should add StarSystem and StarPos
|
||||
ret = this.eddn.entry_augment_system_data(entry, system_name, system_starpos)
|
||||
if isinstance(ret, str):
|
||||
return ret
|
||||
|
||||
entry = ret
|
||||
#######################################################################
|
||||
|
||||
msg = {
|
||||
'$schemaRef': f'https://eddn.edcd.io/schemas/approachsettlement/1{"/test" if is_beta else ""}',
|
||||
'message': entry
|
||||
}
|
||||
|
||||
this.eddn.export_journal_entry(cmdr, entry, msg)
|
||||
return None
|
||||
|
||||
def export_journal_fssallbodiesfound(
|
||||
self, cmdr: str, system_name: str, system_starpos: list, is_beta: bool, entry: MutableMapping[str, Any]
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Send an FSSAllBodiesFound message to EDDN on the correct schema.
|
||||
|
||||
:param cmdr: the commander under which this upload is made
|
||||
:param system_name: Name of current star system
|
||||
:param system_starpos: Coordinates of current star system
|
||||
:param is_beta: whether or not we are in beta mode
|
||||
:param entry: the journal entry to send
|
||||
"""
|
||||
# {
|
||||
# "Count": 3,
|
||||
# "SystemAddress": 9466778822057,
|
||||
# "SystemName": "LP 704-74",
|
||||
# "event": "FSSAllBodiesFound",
|
||||
# "timestamp": "2022-02-09T18:15:14Z"
|
||||
# }
|
||||
#######################################################################
|
||||
# Augmentations
|
||||
#######################################################################
|
||||
# In this case should add StarSystem and StarPos
|
||||
ret = this.eddn.entry_augment_system_data(entry, system_name, system_starpos)
|
||||
if isinstance(ret, str):
|
||||
return ret
|
||||
|
||||
entry = ret
|
||||
#######################################################################
|
||||
|
||||
msg = {
|
||||
'$schemaRef': f'https://eddn.edcd.io/schemas/fssallbodiesfound/1{"/test" if is_beta else ""}',
|
||||
'message': entry
|
||||
}
|
||||
|
||||
this.eddn.export_journal_entry(cmdr, entry, msg)
|
||||
return None
|
||||
|
||||
def canonicalise(self, item: str) -> str:
|
||||
"""
|
||||
Canonicalise the given commodity name.
|
||||
@ -1237,6 +1424,7 @@ def journal_entry( # noqa: C901, CCR001
|
||||
return None
|
||||
|
||||
entry = new_data
|
||||
event_name = entry['event'].lower()
|
||||
|
||||
this.on_foot = state['OnFoot']
|
||||
|
||||
@ -1246,8 +1434,8 @@ def journal_entry( # noqa: C901, CCR001
|
||||
this.odyssey = entry['odyssey'] = state['Odyssey']
|
||||
|
||||
# Track location
|
||||
if entry['event'] in ('Location', 'FSDJump', 'Docked', 'CarrierJump'):
|
||||
if entry['event'] in ('Location', 'CarrierJump'):
|
||||
if event_name in ('location', 'fsdjump', 'docked', 'carrierjump'):
|
||||
if event_name in ('location', 'carrierjump'):
|
||||
if entry.get('BodyType') == 'Planet':
|
||||
this.body_name = entry.get('Body')
|
||||
this.body_id = entry.get('BodyID')
|
||||
@ -1255,7 +1443,7 @@ def journal_entry( # noqa: C901, CCR001
|
||||
else:
|
||||
this.body_name = None
|
||||
|
||||
elif entry['event'] == 'FSDJump':
|
||||
elif event_name == 'fsdjump':
|
||||
this.body_name = None
|
||||
this.body_id = None
|
||||
|
||||
@ -1265,13 +1453,20 @@ def journal_entry( # noqa: C901, CCR001
|
||||
elif this.systemaddress != entry.get('SystemAddress'):
|
||||
this.coordinates = None # Docked event doesn't include coordinates
|
||||
|
||||
this.systemaddress = entry.get('SystemAddress') # type: ignore
|
||||
if 'SystemAddress' not in entry:
|
||||
logger.warning(f'"location" event without SystemAddress !!!:\n{entry}\n')
|
||||
|
||||
elif entry['event'] == 'ApproachBody':
|
||||
# But we'll still *use* the value, because if a 'location' event doesn't
|
||||
# have this we've still moved and now don't know where and MUST NOT
|
||||
# continue to use any old value.
|
||||
# Yes, explicitly state `None` here, so it's crystal clear.
|
||||
this.systemaddress = entry.get('SystemAddress', None) # type: ignore
|
||||
|
||||
elif event_name == 'approachbody':
|
||||
this.body_name = entry['Body']
|
||||
this.body_id = entry.get('BodyID')
|
||||
|
||||
elif entry['event'] == 'LeaveBody':
|
||||
elif event_name == 'leavebody':
|
||||
# NB: **NOT** SupercruiseEntry, because we won't get a fresh
|
||||
# ApproachBody if we don't leave Orbital Cruise and land again.
|
||||
# *This* is triggered when you go above Orbital Cruise altitude.
|
||||
@ -1279,7 +1474,7 @@ def journal_entry( # noqa: C901, CCR001
|
||||
this.body_name = None
|
||||
this.body_id = None
|
||||
|
||||
elif entry['event'] == 'Music':
|
||||
elif event_name == 'music':
|
||||
if entry['MusicTrack'] == 'MainMenu':
|
||||
this.body_name = None
|
||||
this.body_id = None
|
||||
@ -1287,24 +1482,43 @@ def journal_entry( # noqa: C901, CCR001
|
||||
|
||||
# Events with their own EDDN schema
|
||||
if config.get_int('output') & config.OUT_SYS_EDDN and not state['Captain']:
|
||||
if entry['event'].lower() == 'fssdiscoveryscan':
|
||||
return this.eddn.export_journal_fssdiscoveryscan(cmdr, system, is_beta, entry)
|
||||
|
||||
if entry['event'].lower() == 'navbeaconscan':
|
||||
return this.eddn.export_journal_navbeaconscan(cmdr, system, is_beta, entry)
|
||||
if event_name == 'fssdiscoveryscan':
|
||||
return this.eddn.export_journal_fssdiscoveryscan(cmdr, system, state['StarPos'], is_beta, entry)
|
||||
|
||||
if entry['event'].lower() == 'codexentry':
|
||||
return this.eddn.export_journal_codexentry(cmdr, is_beta, entry)
|
||||
elif event_name == 'navbeaconscan':
|
||||
return this.eddn.export_journal_navbeaconscan(cmdr, system, state['StarPos'], is_beta, entry)
|
||||
|
||||
if entry['event'].lower() == 'scanbarycentre':
|
||||
return this.eddn.export_journal_scanbarycentre(cmdr, is_beta, entry)
|
||||
elif event_name == 'codexentry':
|
||||
return this.eddn.export_journal_codexentry(cmdr, state['StarPos'], is_beta, entry)
|
||||
|
||||
if entry['event'].lower() == 'navroute':
|
||||
elif event_name == 'scanbarycentre':
|
||||
return this.eddn.export_journal_scanbarycentre(cmdr, state['StarPos'], is_beta, entry)
|
||||
|
||||
elif event_name == 'navroute':
|
||||
return this.eddn.export_journal_navroute(cmdr, is_beta, entry)
|
||||
|
||||
elif event_name == 'approachsettlement':
|
||||
return this.eddn.export_journal_approachsettlement(
|
||||
cmdr,
|
||||
system,
|
||||
state['StarPos'],
|
||||
is_beta,
|
||||
entry
|
||||
)
|
||||
|
||||
elif event_name == 'fssallbodiesfound':
|
||||
return this.eddn.export_journal_fssallbodiesfound(
|
||||
cmdr,
|
||||
system,
|
||||
state['StarPos'],
|
||||
is_beta,
|
||||
entry
|
||||
)
|
||||
|
||||
# Send journal schema events to EDDN, but not when on a crew
|
||||
if (config.get_int('output') & config.OUT_SYS_EDDN and not state['Captain'] and
|
||||
(entry['event'] in ('Location', 'FSDJump', 'Docked', 'Scan', 'SAASignalsFound', 'CarrierJump')) and
|
||||
(event_name in ('location', 'fsdjump', 'docked', 'scan', 'saasignalsfound', 'carrierjump')) and
|
||||
('StarPos' in entry or this.coordinates)):
|
||||
|
||||
# strip out properties disallowed by the schema
|
||||
@ -1334,22 +1548,22 @@ def journal_entry( # noqa: C901, CCR001
|
||||
]
|
||||
|
||||
# add planet to Docked event for planetary stations if known
|
||||
if entry['event'] == 'Docked' and this.body_name:
|
||||
if event_name == 'docked' and this.body_name:
|
||||
entry['Body'] = this.body_name
|
||||
entry['BodyType'] = 'Planet'
|
||||
|
||||
# add mandatory StarSystem, StarPos and SystemAddress properties to Scan events
|
||||
if 'StarSystem' not in entry:
|
||||
if not system:
|
||||
logger.warning("system is None, can't add StarSystem")
|
||||
return "system is None, can't add StarSystem"
|
||||
logger.warning("system is falsey, can't add StarSystem")
|
||||
return "system is falsey, can't add StarSystem"
|
||||
|
||||
entry['StarSystem'] = system
|
||||
|
||||
if 'StarPos' not in entry:
|
||||
if not this.coordinates:
|
||||
logger.warning("this.coordinates is None, can't add StarPos")
|
||||
return "this.coordinates is None, can't add StarPos"
|
||||
logger.warning("this.coordinates is falsey, can't add StarPos")
|
||||
return "this.coordinates is falsey, can't add StarPos"
|
||||
|
||||
# Gazelle[TD] reported seeing a lagged Scan event with incorrect
|
||||
# augmented StarPos: <https://github.com/EDCD/EDMarketConnector/issues/961>
|
||||
@ -1361,8 +1575,8 @@ def journal_entry( # noqa: C901, CCR001
|
||||
|
||||
if 'SystemAddress' not in entry:
|
||||
if not this.systemaddress:
|
||||
logger.warning("this.systemaddress is None, can't add SystemAddress")
|
||||
return "this.systemaddress is None, can't add SystemAddress"
|
||||
logger.warning("this.systemaddress is falsey, can't add SystemAddress")
|
||||
return "this.systemaddress is falsey, can't add SystemAddress"
|
||||
|
||||
entry['SystemAddress'] = this.systemaddress
|
||||
|
||||
@ -1378,7 +1592,7 @@ def journal_entry( # noqa: C901, CCR001
|
||||
return str(e)
|
||||
|
||||
elif (config.get_int('output') & config.OUT_MKT_EDDN and not state['Captain'] and
|
||||
entry['event'] in ('Market', 'Outfitting', 'Shipyard')):
|
||||
event_name in ('market', 'outfitting', 'shipyard')):
|
||||
# Market.json, Outfitting.json or Shipyard.json to process
|
||||
|
||||
try:
|
||||
@ -1393,16 +1607,19 @@ def journal_entry( # noqa: C901, CCR001
|
||||
path = pathlib.Path(journaldir) / f'{entry["event"]}.json'
|
||||
|
||||
with path.open('rb') as f:
|
||||
entry = json.load(f)
|
||||
entry['odyssey'] = this.odyssey
|
||||
if entry['event'] == 'Market':
|
||||
this.eddn.export_journal_commodities(cmdr, is_beta, entry)
|
||||
# Don't assume we can definitely stomp entry & event_name here
|
||||
entry_augment = json.load(f)
|
||||
event_name_augment = entry_augment['event'].lower()
|
||||
entry_augment['odyssey'] = this.odyssey
|
||||
|
||||
elif entry['event'] == 'Outfitting':
|
||||
this.eddn.export_journal_outfitting(cmdr, is_beta, entry)
|
||||
if event_name_augment == 'market':
|
||||
this.eddn.export_journal_commodities(cmdr, is_beta, entry_augment)
|
||||
|
||||
elif entry['event'] == 'Shipyard':
|
||||
this.eddn.export_journal_shipyard(cmdr, is_beta, entry)
|
||||
elif event_name_augment == 'outfitting':
|
||||
this.eddn.export_journal_outfitting(cmdr, is_beta, entry_augment)
|
||||
|
||||
elif event_name_augment == 'shipyard':
|
||||
this.eddn.export_journal_shipyard(cmdr, is_beta, entry_augment)
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.debug(f'Failed exporting {entry["event"]}', exc_info=e)
|
||||
@ -1518,10 +1735,12 @@ def dashboard_entry(cmdr: str, is_beta: bool, entry: Dict[str, Any]) -> None:
|
||||
:param is_beta: Whether non-live game version was detected.
|
||||
:param entry: The latest Status.json data.
|
||||
"""
|
||||
this.status_body_name = None
|
||||
if 'BodyName' in entry:
|
||||
this.status_body_name = entry['BodyName']
|
||||
if not isinstance(entry['BodyName'], str):
|
||||
logger.warning(f'BodyName was present but not a string! "{entry["BodyName"]}" ({type(entry["BodyName"])})')
|
||||
|
||||
else:
|
||||
this.status_body_name = None
|
||||
else:
|
||||
this.status_body_name = entry['BodyName']
|
||||
|
||||
tracking_ui_update()
|
||||
|
@ -9,6 +9,28 @@
|
||||
# 4) Ensure the EDSM API call(back) for setting the image at end of system
|
||||
# text is always fired. i.e. CAPI cmdr_data() processing.
|
||||
|
||||
# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $#
|
||||
# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $#
|
||||
#
|
||||
# This is an EDMC 'core' plugin.
|
||||
#
|
||||
# All EDMC plugins are *dynamically* loaded at run-time.
|
||||
#
|
||||
# We build for Windows using `py2exe`.
|
||||
#
|
||||
# `py2exe` can't possibly know about anything in the dynamically loaded
|
||||
# core plugins.
|
||||
#
|
||||
# Thus you **MUST** check if any imports you add in this file are only
|
||||
# referenced in this file (or only in any other core plugin), and if so...
|
||||
#
|
||||
# YOU MUST ENSURE THAT PERTINENT ADJUSTMENTS ARE MADE IN `setup.py`
|
||||
# SO AS TO ENSURE THE FILES ARE ACTUALLY PRESENT IN AN END-USER
|
||||
# INSTALLATION ON WINDOWS.
|
||||
#
|
||||
#
|
||||
# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $#
|
||||
# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $#
|
||||
import json
|
||||
import threading
|
||||
import tkinter as tk
|
||||
@ -23,7 +45,7 @@ import killswitch
|
||||
import myNotebook as nb # noqa: N813
|
||||
import plug
|
||||
from companion import CAPIData
|
||||
from config import applongname, appversion, config, debug_senders
|
||||
from config import applongname, appversion, config, debug_senders, user_agent
|
||||
from edmc_data import DEBUG_WEBSERVER_HOST, DEBUG_WEBSERVER_PORT
|
||||
from EDMCLogging import get_main_logger
|
||||
from ttkHyperlinkLabel import HyperlinkLabel
|
||||
@ -49,6 +71,7 @@ class This:
|
||||
self.shutting_down = False # Plugin is shutting down.
|
||||
|
||||
self.session: requests.Session = requests.Session()
|
||||
self.session.headers['User-Agent'] = user_agent
|
||||
self.queue: Queue = Queue() # Items to be sent to EDSM by worker thread
|
||||
self.discarded_events: Set[str] = [] # List discarded events from EDSM
|
||||
self.lastlookup: requests.Response # Result of last system lookup
|
||||
|
@ -1,5 +1,27 @@
|
||||
# EDShipyard ship export
|
||||
|
||||
# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $#
|
||||
# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $#
|
||||
#
|
||||
# This is an EDMC 'core' plugin.
|
||||
#
|
||||
# All EDMC plugins are *dynamically* loaded at run-time.
|
||||
#
|
||||
# We build for Windows using `py2exe`.
|
||||
#
|
||||
# `py2exe` can't possibly know about anything in the dynamically loaded
|
||||
# core plugins.
|
||||
#
|
||||
# Thus you **MUST** check if any imports you add in this file are only
|
||||
# referenced in this file (or only in any other core plugin), and if so...
|
||||
#
|
||||
# YOU MUST ENSURE THAT PERTINENT ADJUSTMENTS ARE MADE IN `setup.py`
|
||||
# SO AS TO ENSURE THE FILES ARE ACTUALLY PRESENT IN AN END-USER
|
||||
# INSTALLATION ON WINDOWS.
|
||||
#
|
||||
#
|
||||
# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $#
|
||||
# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $#
|
||||
import base64
|
||||
import gzip
|
||||
import json
|
||||
|
107
plugins/inara.py
107
plugins/inara.py
@ -1,5 +1,27 @@
|
||||
"""Inara Sync."""
|
||||
|
||||
# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $#
|
||||
# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $#
|
||||
#
|
||||
# This is an EDMC 'core' plugin.
|
||||
#
|
||||
# All EDMC plugins are *dynamically* loaded at run-time.
|
||||
#
|
||||
# We build for Windows using `py2exe`.
|
||||
#
|
||||
# `py2exe` can't possibly know about anything in the dynamically loaded
|
||||
# core plugins.
|
||||
#
|
||||
# Thus you **MUST** check if any imports you add in this file are only
|
||||
# referenced in this file (or only in any other core plugin), and if so...
|
||||
#
|
||||
# YOU MUST ENSURE THAT PERTINENT ADJUSTMENTS ARE MADE IN `setup.py`
|
||||
# SO AS TO ENSURE THE FILES ARE ACTUALLY PRESENT IN AN END-USER
|
||||
# INSTALLATION ON WINDOWS.
|
||||
#
|
||||
#
|
||||
# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $#
|
||||
# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $#
|
||||
import json
|
||||
import threading
|
||||
import time
|
||||
@ -33,7 +55,10 @@ if TYPE_CHECKING:
|
||||
|
||||
_TIMEOUT = 20
|
||||
FAKE = ('CQC', 'Training', 'Destination') # Fake systems that shouldn't be sent to Inara
|
||||
CREDIT_RATIO = 1.05 # Update credits if they change by 5% over the course of a session
|
||||
# We only update Credits to Inara if the delta from the last sent value is
|
||||
# greater than certain thresholds
|
||||
CREDITS_DELTA_MIN_FRACTION = 0.05 # Fractional difference threshold
|
||||
CREDITS_DELTA_MIN_ABSOLUTE = 10_000_000 # Absolute difference threshold
|
||||
|
||||
|
||||
# These need to be defined above This
|
||||
@ -76,7 +101,7 @@ class This:
|
||||
self.suppress_docked = False # Skip initial Docked event if started docked
|
||||
self.cargo: Optional[List[OrderedDictT[str, Any]]] = None
|
||||
self.materials: Optional[List[OrderedDictT[str, Any]]] = None
|
||||
self.lastcredits: int = 0 # Send credit update soon after Startup / new game
|
||||
self.last_credits: int = 0 # Send credit update soon after Startup / new game
|
||||
self.storedmodules: Optional[List[OrderedDictT[str, Any]]] = None
|
||||
self.loadout: Optional[OrderedDictT[str, Any]] = None
|
||||
self.fleet: Optional[List[OrderedDictT[str, Any]]] = None
|
||||
@ -372,7 +397,7 @@ def journal_entry( # noqa: C901, CCR001
|
||||
this.suppress_docked = False
|
||||
this.cargo = None
|
||||
this.materials = None
|
||||
this.lastcredits = 0
|
||||
this.last_credits = 0
|
||||
this.storedmodules = None
|
||||
this.loadout = None
|
||||
this.fleet = None
|
||||
@ -383,8 +408,8 @@ def journal_entry( # noqa: C901, CCR001
|
||||
this.station_marketid = None
|
||||
|
||||
elif event_name in ('Resurrect', 'ShipyardBuy', 'ShipyardSell', 'SellShipOnRebuy'):
|
||||
# Events that mean a significant change in credits so we should send credits after next "Update"
|
||||
this.lastcredits = 0
|
||||
# Events that mean a significant change in credits, so we should send credits after next "Update"
|
||||
this.last_credits = 0
|
||||
|
||||
elif event_name in ('ShipyardNew', 'ShipyardSwap') or (event_name == 'Location' and entry['Docked']):
|
||||
this.suppress_docked = True
|
||||
@ -419,23 +444,13 @@ def journal_entry( # noqa: C901, CCR001
|
||||
this.station_marketid = None
|
||||
|
||||
if config.get_int('inara_out') and not is_beta and not this.multicrew and credentials(cmdr):
|
||||
current_creds = Credentials(this.cmdr, this.FID, str(credentials(this.cmdr)))
|
||||
current_credentials = Credentials(this.cmdr, this.FID, str(credentials(this.cmdr)))
|
||||
try:
|
||||
# Dump starting state to Inara
|
||||
if (this.newuser or event_name == 'StartUp' or (this.newsession and event_name == 'Cargo')):
|
||||
this.newuser = False
|
||||
this.newsession = False
|
||||
|
||||
# Send rank info to Inara on startup
|
||||
new_add_event(
|
||||
'setCommanderRankPilot',
|
||||
entry['timestamp'],
|
||||
[
|
||||
{'rankName': k.lower(), 'rankValue': v[0], 'rankProgress': v[1] / 100.0}
|
||||
for k, v in state['Rank'].items() if v is not None
|
||||
]
|
||||
)
|
||||
|
||||
# Don't send the API call with no values.
|
||||
if state['Reputation']:
|
||||
new_add_event(
|
||||
@ -503,6 +518,19 @@ def journal_entry( # noqa: C901, CCR001
|
||||
this.loadout = make_loadout(state)
|
||||
new_add_event('setCommanderShipLoadout', entry['timestamp'], this.loadout)
|
||||
|
||||
# Trigger off the "only observed as being after Ranks" event so that
|
||||
# we have both current Ranks *and* current Progress within them.
|
||||
elif event_name == 'Progress':
|
||||
# Send rank info to Inara on startup
|
||||
new_add_event(
|
||||
'setCommanderRankPilot',
|
||||
entry['timestamp'],
|
||||
[
|
||||
{'rankName': k.lower(), 'rankValue': v[0], 'rankProgress': v[1] / 100.0}
|
||||
for k, v in state['Rank'].items() if v is not None
|
||||
]
|
||||
)
|
||||
|
||||
# Promotions
|
||||
elif event_name == 'Promotion':
|
||||
for k, v in state['Rank'].items():
|
||||
@ -737,17 +765,24 @@ def journal_entry( # noqa: C901, CCR001
|
||||
logger.debug('Adding events', exc_info=e)
|
||||
return str(e)
|
||||
|
||||
# Send credits and stats to Inara on startup only - otherwise may be out of date
|
||||
# We want to utilise some Statistics data, so don't setCommanderCredits here
|
||||
if event_name == 'LoadGame':
|
||||
this.last_credits = state['Credits']
|
||||
|
||||
elif event_name == 'Statistics':
|
||||
inara_data = {
|
||||
'commanderCredits': state['Credits'],
|
||||
'commanderLoan': state['Loan'],
|
||||
}
|
||||
if entry.get('Bank_Account') is not None:
|
||||
if entry['Bank_Account'].get('Current_Wealth') is not None:
|
||||
inara_data['commanderAssets'] = entry['Bank_Account']['Current_Wealth']
|
||||
|
||||
new_add_event(
|
||||
'setCommanderCredits',
|
||||
entry['timestamp'],
|
||||
{'commanderCredits': state['Credits'], 'commanderLoan': state['Loan']}
|
||||
inara_data
|
||||
)
|
||||
|
||||
this.lastcredits = state['Credits']
|
||||
|
||||
elif event_name == 'Statistics':
|
||||
new_add_event('setCommanderGameStatistics', entry['timestamp'], state['Statistics']) # may be out of date
|
||||
|
||||
# Selling / swapping ships
|
||||
@ -837,7 +872,7 @@ def journal_entry( # noqa: C901, CCR001
|
||||
|
||||
if this.fleet != fleet:
|
||||
this.fleet = fleet
|
||||
this.filter_events(current_creds, lambda e: e.name != 'setCommanderShip')
|
||||
this.filter_events(current_credentials, lambda e: e.name != 'setCommanderShip')
|
||||
|
||||
# this.events = [x for x in this.events if x['eventName'] != 'setCommanderShip'] # Remove any unsent
|
||||
for ship in this.fleet:
|
||||
@ -850,7 +885,7 @@ def journal_entry( # noqa: C901, CCR001
|
||||
this.loadout = loadout
|
||||
|
||||
this.filter_events(
|
||||
current_creds,
|
||||
current_credentials,
|
||||
lambda e: (
|
||||
e.name != 'setCommanderShipLoadout'
|
||||
or cast(dict, e.data)['shipGameID'] != cast(dict, this.loadout)['shipGameID'])
|
||||
@ -891,7 +926,7 @@ def journal_entry( # noqa: C901, CCR001
|
||||
# Only send on change
|
||||
this.storedmodules = modules
|
||||
# Remove any unsent
|
||||
this.filter_events(current_creds, lambda e: e.name != 'setCommanderStorageModules')
|
||||
this.filter_events(current_credentials, lambda e: e.name != 'setCommanderStorageModules')
|
||||
|
||||
# this.events = list(filter(lambda e: e['eventName'] != 'setCommanderStorageModules', this.events))
|
||||
new_add_event('setCommanderStorageModules', entry['timestamp'], this.storedmodules)
|
||||
@ -1220,7 +1255,7 @@ def journal_entry( # noqa: C901, CCR001
|
||||
if event_name == 'CommunityGoal':
|
||||
# Remove any unsent
|
||||
this.filter_events(
|
||||
current_creds, lambda e: e.name not in ('setCommunityGoal', 'setCommanderCommunityGoalProgress')
|
||||
current_credentials, lambda e: e.name not in ('setCommunityGoal', 'setCommanderCommunityGoalProgress')
|
||||
)
|
||||
|
||||
# this.events = list(filter(
|
||||
@ -1349,23 +1384,8 @@ def cmdr_data(data: CAPIData, is_beta): # noqa: CCR001
|
||||
this.station_link.update_idletasks()
|
||||
|
||||
if config.get_int('inara_out') and not is_beta and not this.multicrew and credentials(this.cmdr):
|
||||
if not (CREDIT_RATIO > this.lastcredits / data['commander']['credits'] > 1/CREDIT_RATIO):
|
||||
this.filter_events(
|
||||
Credentials(this.cmdr, this.FID, str(credentials(this.cmdr))),
|
||||
lambda e: e.name != 'setCommanderCredits'
|
||||
)
|
||||
|
||||
# this.events = [x for x in this.events if x['eventName'] != 'setCommanderCredits'] # Remove any unsent
|
||||
new_add_event(
|
||||
'setCommanderCredits',
|
||||
data['timestamp'],
|
||||
{
|
||||
'commanderCredits': data['commander']['credits'],
|
||||
'commanderLoan': data['commander'].get('debt', 0),
|
||||
}
|
||||
)
|
||||
|
||||
this.lastcredits = int(data['commander']['credits'])
|
||||
# Only here to ensure the conditional is correct for future additions
|
||||
pass
|
||||
|
||||
|
||||
def make_loadout(state: Dict[str, Any]) -> OrderedDictT[str, Any]: # noqa: CCR001
|
||||
@ -1567,6 +1587,7 @@ def send_data(url: str, data: Mapping[str, Any]) -> bool: # noqa: CCR001
|
||||
:param data: the data to POST
|
||||
:return: success state
|
||||
"""
|
||||
# NB: As of 2022-01-25 Artie has stated the Inara API does *not* support compression
|
||||
r = this.session.post(url, data=json.dumps(data, separators=(',', ':')), timeout=_TIMEOUT)
|
||||
r.raise_for_status()
|
||||
reply = r.json()
|
||||
|
48
prefs.py
48
prefs.py
@ -3,10 +3,10 @@
|
||||
|
||||
import contextlib
|
||||
import logging
|
||||
import sys
|
||||
import tkinter as tk
|
||||
import webbrowser
|
||||
from os.path import expanduser, expandvars, join, normpath
|
||||
from sys import platform
|
||||
from tkinter import colorchooser as tkColorChooser # type: ignore # noqa: N812
|
||||
from tkinter import ttk
|
||||
from types import TracebackType
|
||||
@ -154,7 +154,7 @@ class AutoInc(contextlib.AbstractContextManager):
|
||||
return None
|
||||
|
||||
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
import objc # type: ignore
|
||||
from Foundation import NSFileManager # type: ignore
|
||||
try:
|
||||
@ -179,7 +179,7 @@ if platform == 'darwin':
|
||||
|
||||
was_accessible_at_launch = AXIsProcessTrusted() # type: ignore
|
||||
|
||||
elif platform == 'win32':
|
||||
elif sys.platform == 'win32':
|
||||
import ctypes
|
||||
import winreg
|
||||
from ctypes.wintypes import HINSTANCE, HWND, LPARAM, LPCWSTR, LPVOID, LPWSTR, MAX_PATH, POINT, RECT, SIZE, UINT
|
||||
@ -246,7 +246,7 @@ class PreferencesDialog(tk.Toplevel):
|
||||
|
||||
self.parent = parent
|
||||
self.callback = callback
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
# LANG: File > Preferences menu entry for macOS
|
||||
self.title(_('Preferences'))
|
||||
|
||||
@ -258,15 +258,15 @@ class PreferencesDialog(tk.Toplevel):
|
||||
self.transient(parent)
|
||||
|
||||
# position over parent
|
||||
if platform != 'darwin' or parent.winfo_rooty() > 0: # http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7
|
||||
if sys.platform != 'darwin' or parent.winfo_rooty() > 0: # http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7
|
||||
# TODO this is fixed supposedly.
|
||||
self.geometry(f'+{parent.winfo_rootx()}+{parent.winfo_rooty()}')
|
||||
|
||||
# remove decoration
|
||||
if platform == 'win32':
|
||||
if sys.platform == 'win32':
|
||||
self.attributes('-toolwindow', tk.TRUE)
|
||||
|
||||
elif platform == 'darwin':
|
||||
elif sys.platform == 'darwin':
|
||||
# http://wiki.tcl.tk/13428
|
||||
parent.call('tk::unsupported::MacWindowStyle', 'style', self, 'utility')
|
||||
|
||||
@ -294,7 +294,7 @@ class PreferencesDialog(tk.Toplevel):
|
||||
self.__setup_appearance_tab(notebook)
|
||||
self.__setup_plugin_tab(notebook)
|
||||
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
self.protocol("WM_DELETE_WINDOW", self.apply) # close button applies changes
|
||||
|
||||
else:
|
||||
@ -322,7 +322,7 @@ class PreferencesDialog(tk.Toplevel):
|
||||
self.grab_set()
|
||||
|
||||
# Ensure fully on-screen
|
||||
if platform == 'win32' and CalculatePopupWindowPosition:
|
||||
if sys.platform == 'win32' and CalculatePopupWindowPosition:
|
||||
position = RECT()
|
||||
GetWindowRect(GetParent(self.winfo_id()), position)
|
||||
if CalculatePopupWindowPosition(
|
||||
@ -396,7 +396,7 @@ class PreferencesDialog(tk.Toplevel):
|
||||
self.outdir_entry = nb.Entry(output_frame, takefocus=False)
|
||||
self.outdir_entry.grid(columnspan=2, padx=self.PADX, pady=(0, self.PADY), sticky=tk.EW, row=row.get())
|
||||
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
text = (_('Change...')) # LANG: macOS Preferences - files location selection button
|
||||
|
||||
else:
|
||||
@ -446,7 +446,7 @@ class PreferencesDialog(tk.Toplevel):
|
||||
|
||||
self.logdir_entry.grid(columnspan=4, padx=self.PADX, pady=(0, self.PADY), sticky=tk.EW, row=row.get())
|
||||
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
text = (_('Change...')) # LANG: macOS Preferences - files location selection button
|
||||
|
||||
else:
|
||||
@ -470,7 +470,7 @@ class PreferencesDialog(tk.Toplevel):
|
||||
state=tk.NORMAL if config.get_str('journaldir') else tk.DISABLED
|
||||
).grid(column=2, pady=self.PADY, sticky=tk.EW, row=row.get())
|
||||
|
||||
if platform in ('darwin', 'win32'):
|
||||
if sys.platform in ('darwin', 'win32'):
|
||||
ttk.Separator(config_frame, orient=tk.HORIZONTAL).grid(
|
||||
columnspan=4, padx=self.PADX, pady=self.PADY*4, sticky=tk.EW, row=row.get()
|
||||
)
|
||||
@ -482,11 +482,11 @@ class PreferencesDialog(tk.Toplevel):
|
||||
nb.Label(
|
||||
config_frame,
|
||||
text=_('Keyboard shortcut') if # LANG: Hotkey/Shortcut settings prompt on OSX
|
||||
platform == 'darwin' else
|
||||
sys.platform == 'darwin' else
|
||||
_('Hotkey') # LANG: Hotkey/Shortcut settings prompt on Windows
|
||||
).grid(padx=self.PADX, sticky=tk.W, row=row.get())
|
||||
|
||||
if platform == 'darwin' and not was_accessible_at_launch:
|
||||
if sys.platform == 'darwin' and not was_accessible_at_launch:
|
||||
if AXIsProcessTrusted():
|
||||
# Shortcut settings prompt on OSX
|
||||
nb.Label(
|
||||
@ -511,7 +511,8 @@ class PreferencesDialog(tk.Toplevel):
|
||||
)
|
||||
|
||||
else:
|
||||
self.hotkey_text = nb.Entry(config_frame, width=(20 if platform == 'darwin' else 30), justify=tk.CENTER)
|
||||
self.hotkey_text = nb.Entry(config_frame, width=(
|
||||
20 if sys.platform == 'darwin' else 30), justify=tk.CENTER)
|
||||
self.hotkey_text.insert(
|
||||
0,
|
||||
# No hotkey/shortcut currently defined
|
||||
@ -741,7 +742,7 @@ class PreferencesDialog(tk.Toplevel):
|
||||
appearance_frame, text=_('Dark'), variable=self.theme, value=1, command=self.themevarchanged
|
||||
).grid(columnspan=3, padx=self.BUTTONX, sticky=tk.W, row=row.get())
|
||||
|
||||
if platform == 'win32':
|
||||
if sys.platform == 'win32':
|
||||
nb.Radiobutton(
|
||||
appearance_frame,
|
||||
# LANG: Label for 'Transparent' theme radio button
|
||||
@ -870,7 +871,7 @@ class PreferencesDialog(tk.Toplevel):
|
||||
)
|
||||
self.ontop_button.grid(columnspan=3, padx=self.BUTTONX, sticky=tk.W, row=row.get()) # Appearance setting
|
||||
|
||||
if platform == 'win32':
|
||||
if sys.platform == 'win32':
|
||||
nb.Checkbutton(
|
||||
appearance_frame,
|
||||
# LANG: Appearance option for Windows "minimize to system tray"
|
||||
@ -997,7 +998,7 @@ class PreferencesDialog(tk.Toplevel):
|
||||
def tabchanged(self, event: tk.Event) -> None:
|
||||
"""Handle preferences active tab changing."""
|
||||
self.outvarchanged()
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
# Hack to recompute size so that buttons show up under Mojave
|
||||
notebook = event.widget
|
||||
frame = self.nametowidget(notebook.winfo_parent())
|
||||
@ -1027,9 +1028,8 @@ class PreferencesDialog(tk.Toplevel):
|
||||
|
||||
# If encoding isn't UTF-8 we can't use the tkinter dialog
|
||||
current_locale = locale.getlocale(locale.LC_CTYPE)
|
||||
from sys import platform as sys_platform
|
||||
directory = None
|
||||
if sys_platform == 'win32' and current_locale[1] not in ('utf8', 'UTF8', 'utf-8', 'UTF-8'):
|
||||
if sys.platform == 'win32' and current_locale[1] not in ('utf8', 'UTF8', 'utf-8', 'UTF-8'):
|
||||
def browsecallback(hwnd, uMsg, lParam, lpData): # noqa: N803 # Windows API convention
|
||||
# set initial folder
|
||||
if uMsg == BFFM_INITIALIZED and lpData:
|
||||
@ -1075,7 +1075,7 @@ class PreferencesDialog(tk.Toplevel):
|
||||
# TODO: This is awful.
|
||||
entryfield['state'] = tk.NORMAL # must be writable to update
|
||||
entryfield.delete(0, tk.END)
|
||||
if platform == 'win32':
|
||||
if sys.platform == 'win32':
|
||||
start = len(config.home.split('\\')) if pathvar.get().lower().startswith(config.home.lower()) else 0
|
||||
display = []
|
||||
components = normpath(pathvar.get()).split('\\')
|
||||
@ -1096,7 +1096,7 @@ class PreferencesDialog(tk.Toplevel):
|
||||
entryfield.insert(0, '\\'.join(display))
|
||||
|
||||
# None if path doesn't exist
|
||||
elif platform == 'darwin' and NSFileManager.defaultManager().componentsToDisplayForPath_(pathvar.get()):
|
||||
elif sys.platform == 'darwin' and NSFileManager.defaultManager().componentsToDisplayForPath_(pathvar.get()):
|
||||
if pathvar.get().startswith(config.home):
|
||||
display = ['~'] + NSFileManager.defaultManager().componentsToDisplayForPath_(pathvar.get())[
|
||||
len(NSFileManager.defaultManager().componentsToDisplayForPath_(config.home)):
|
||||
@ -1236,7 +1236,7 @@ class PreferencesDialog(tk.Toplevel):
|
||||
else:
|
||||
config.set('journaldir', logdir)
|
||||
|
||||
if platform in ('darwin', 'win32'):
|
||||
if sys.platform in ('darwin', 'win32'):
|
||||
config.set('hotkey_code', self.hotkey_code)
|
||||
config.set('hotkey_mods', self.hotkey_mods)
|
||||
config.set('hotkey_always', int(not self.hotkey_only.get()))
|
||||
@ -1282,7 +1282,7 @@ class PreferencesDialog(tk.Toplevel):
|
||||
self.parent.wm_attributes('-topmost', 1 if config.get_int('always_ontop') else 0)
|
||||
self.destroy()
|
||||
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
def enableshortcuts(self) -> None:
|
||||
"""Set up macOS preferences shortcut."""
|
||||
self.apply()
|
||||
|
@ -3,10 +3,13 @@ max_line_length = 120
|
||||
|
||||
[tool.isort]
|
||||
multi_line_output = 5
|
||||
line_length = 119
|
||||
line_length = 119
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"] # Search for tests in tests/
|
||||
|
||||
[tool.coverage.run]
|
||||
omit = ["venv/*"] # when running pytest --cov, dont report coverage in venv directories
|
||||
omit = ["venv/*"] # when running pytest --cov, dont report coverage in venv directories
|
||||
|
||||
[tool.pyright]
|
||||
# pythonPlatform = 'Darwin'
|
||||
|
@ -2,11 +2,16 @@
|
||||
# Using legacy 'setup.py install' for flake8-annotations-coverage, since package 'wheel' is not installed.
|
||||
wheel
|
||||
|
||||
# We can't rely on just picking this up from either the base (not venv),
|
||||
# or venv-init-time version. Specify here so that dependabot will prod us
|
||||
# about new versions.
|
||||
setuptools==60.8.2
|
||||
|
||||
# Static analysis tools
|
||||
flake8==4.0.1
|
||||
flake8-annotations-coverage==0.0.5
|
||||
flake8-cognitive-complexity==0.1.0
|
||||
flake8-comprehensions==3.7.0
|
||||
flake8-comprehensions==3.8.0
|
||||
flake8-docstrings==1.6.0
|
||||
isort==5.10.1
|
||||
flake8-isort==4.1.1
|
||||
@ -15,27 +20,31 @@ flake8-noqa==1.2.1
|
||||
flake8-polyfill==1.0.2
|
||||
flake8-use-fstring==1.3
|
||||
|
||||
mypy==0.910
|
||||
mypy==0.931
|
||||
pep8-naming==0.12.1
|
||||
safety==1.10.3
|
||||
types-requests==2.26.0
|
||||
types-requests==2.27.9
|
||||
|
||||
# Code formatting tools
|
||||
autopep8==1.6.0
|
||||
|
||||
# HTML changelogs
|
||||
grip==4.5.2
|
||||
grip==4.6.0
|
||||
|
||||
# Packaging
|
||||
# Used to put together a WiX configuration from template/auto-gen
|
||||
lxml==4.7.1
|
||||
# We only need py2exe on windows.
|
||||
py2exe==0.10.4.1; sys_platform == 'win32'
|
||||
# Pre-release version addressing semantic_version 2.9.0+ issues:
|
||||
# <https://github.com/py2exe/py2exe/issues/126>
|
||||
py2exe==0.11.1.0; sys_platform == 'win32'
|
||||
|
||||
# Testing
|
||||
pytest==6.2.5
|
||||
pytest==7.0.0
|
||||
pytest-cov==3.0.0 # Pytest code coverage support
|
||||
coverage[toml]==6.1.2 # pytest-cov dep. This is here to ensure that it includes TOML support for pyproject.toml configs
|
||||
coverage[toml]==6.3.1 # pytest-cov dep. This is here to ensure that it includes TOML support for pyproject.toml configs
|
||||
# For manipulating folder permissions and the like.
|
||||
pywin32==302; sys_platform == 'win32'
|
||||
pywin32==303; sys_platform == 'win32'
|
||||
|
||||
|
||||
# All of the normal requirements
|
||||
|
@ -1,11 +1,11 @@
|
||||
certifi==2021.10.8
|
||||
requests==2.26.0
|
||||
requests==2.27.1
|
||||
watchdog==2.1.6
|
||||
# Commented out because this doesn't package well with py2exe
|
||||
infi.systray==0.1.12; sys_platform == 'win32'
|
||||
# argh==0.26.2 watchdog dep
|
||||
# pyyaml==5.3.1 watchdog dep
|
||||
semantic-version==2.8.5
|
||||
semantic-version==2.9.0
|
||||
|
||||
# Base requirement for MacOS
|
||||
pyobjc; sys_platform == 'darwin'
|
||||
|
129
setup.py
129
setup.py
@ -9,6 +9,7 @@ Script to build to .exe and .msi package.
|
||||
|
||||
import codecs
|
||||
import os
|
||||
import pathlib
|
||||
import platform
|
||||
import re
|
||||
import shutil
|
||||
@ -18,13 +19,15 @@ from os.path import exists, isdir, join
|
||||
from tempfile import gettempdir
|
||||
from typing import Any, Generator, Set
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from config import (
|
||||
appcmdname, applongname, appname, appversion, appversion_nobuild, copyright, git_shorthash_from_head, update_feed,
|
||||
update_interval
|
||||
)
|
||||
from constants import GITVERSION_FILE
|
||||
|
||||
if sys.version_info[0:2] != (3, 9):
|
||||
if sys.version_info[0:2] != (3, 10):
|
||||
raise AssertionError(f'Unexpected python version {sys.version}')
|
||||
|
||||
###########################################################################
|
||||
@ -141,12 +144,16 @@ if sys.platform == 'darwin':
|
||||
('plugins', x) for x in PLUGINS
|
||||
],
|
||||
'resources': [
|
||||
'commodity.csv',
|
||||
'rare_commodity.csv',
|
||||
'.gitversion', # Contains git short hash
|
||||
'ChangeLog.md',
|
||||
'snd_good.wav',
|
||||
'snd_bad.wav',
|
||||
'modules.p',
|
||||
'ships.p',
|
||||
('FDevIDs', [
|
||||
join('FDevIDs', 'commodity.csv'),
|
||||
join('FDevIDs', 'rare_commodity.csv'),
|
||||
]),
|
||||
],
|
||||
'site_packages': False,
|
||||
'plist': {
|
||||
@ -184,7 +191,10 @@ elif sys.platform == 'win32':
|
||||
'dist_dir': dist_dir,
|
||||
'optimize': 2,
|
||||
'packages': [
|
||||
'asyncio', # No longer auto as of py3.10+py2exe 0.11
|
||||
'multiprocessing', # No longer auto as of py3.10+py2exe 0.11
|
||||
'sqlite3', # Included for plugins
|
||||
'util', # 2022-02-01 only imported in plugins/eddn.py
|
||||
],
|
||||
'includes': [
|
||||
'dataclasses',
|
||||
@ -209,9 +219,7 @@ elif sys.platform == 'win32':
|
||||
'WinSparkle.dll',
|
||||
'WinSparkle.pdb', # For debugging - don't include in package
|
||||
'EUROCAPS.TTF',
|
||||
'Changelog.md',
|
||||
'commodity.csv',
|
||||
'rare_commodity.csv',
|
||||
'ChangeLog.md',
|
||||
'snd_good.wav',
|
||||
'snd_bad.wav',
|
||||
'modules.p',
|
||||
@ -223,6 +231,10 @@ elif sys.platform == 'win32':
|
||||
'EDMarketConnector - reset-ui.bat',
|
||||
]),
|
||||
('L10n', [join('L10n', x) for x in os.listdir('L10n') if x.endswith('.strings')]),
|
||||
('FDevIDs', [
|
||||
join('FDevIDs', 'commodity.csv'),
|
||||
join('FDevIDs', 'rare_commodity.csv'),
|
||||
]),
|
||||
('plugins', PLUGINS),
|
||||
]
|
||||
|
||||
@ -283,13 +295,110 @@ if sys.platform == 'darwin':
|
||||
os.system(f'cd {dist_dir}; ditto -ck --keepParent --sequesterRsrc {appname}.app ../{package_filename}; cd ..')
|
||||
|
||||
elif sys.platform == 'win32':
|
||||
os.system(rf'"{WIXPATH}\candle.exe" -out {dist_dir}\ {appname}.wxs')
|
||||
template_file = pathlib.Path('wix/template.wxs')
|
||||
components_file = pathlib.Path('wix/components.wxs')
|
||||
final_wxs_file = pathlib.Path('EDMarketConnector.wxs')
|
||||
|
||||
if not exists(f'{dist_dir}/{appname}.wixobj'):
|
||||
raise AssertionError(f'No {dist_dir}/{appname}.wixobj: candle.exe failed?')
|
||||
# Use heat.exe to generate the Component for all files inside dist.win32
|
||||
os.system(rf'"{WIXPATH}\heat.exe" dir {dist_dir}\ -ag -sfrag -srid -suid -out {components_file}')
|
||||
|
||||
component_tree = etree.parse(str(components_file))
|
||||
# 1. Change the element:
|
||||
#
|
||||
# <Directory Id="dist.win32" Name="dist.win32">
|
||||
#
|
||||
# to:
|
||||
#
|
||||
# <Directory Id="INSTALLDIR" Name="$(var.PRODUCTNAME)">
|
||||
directory_win32 = component_tree.find('.//{*}Directory[@Id="dist.win32"][@Name="dist.win32"]')
|
||||
if directory_win32 is None:
|
||||
raise ValueError(f'{components_file}: Expected Directory with Id="dist.win32"')
|
||||
|
||||
directory_win32.set('Id', 'INSTALLDIR')
|
||||
directory_win32.set('Name', '$(var.PRODUCTNAME)')
|
||||
# 2. Change:
|
||||
#
|
||||
# <Component Id="EDMarketConnector.exe" Guid="*">
|
||||
# <File Id="EDMarketConnector.exe" KeyPath="yes" Source="SourceDir\EDMarketConnector.exe" />
|
||||
# </Component>
|
||||
#
|
||||
# to:
|
||||
#
|
||||
# <Component Id="MainExecutable" Guid="{D33BB66E-9664-4AB6-A044-3004B50A09B0}">
|
||||
# <File Id="EDMarketConnector.exe" KeyPath="yes" Source="SourceDir\EDMarketConnector.exe" />
|
||||
# <Shortcut Id="MainExeShortcut" Directory="ProgramMenuFolder" Name="$(var.PRODUCTLONGNAME)"
|
||||
# Description="Downloads station data from Elite: Dangerous" WorkingDirectory="INSTALLDIR"
|
||||
# Icon="EDMarketConnector.exe" IconIndex="0" Advertise="yes" />
|
||||
# </Component>
|
||||
main_executable = directory_win32.find('.//{*}Component[@Id="EDMarketConnector.exe"]')
|
||||
if main_executable is None:
|
||||
raise ValueError(f'{components_file}: Expected Component with Id="EDMarketConnector.exe"')
|
||||
|
||||
main_executable.set('Id', 'MainExecutable')
|
||||
main_executable.set('Guid', '{D33BB66E-9664-4AB6-A044-3004B50A09B0}')
|
||||
shortcut = etree.SubElement(
|
||||
main_executable,
|
||||
'Shortcut',
|
||||
nsmap=main_executable.nsmap,
|
||||
attrib={
|
||||
'Id': 'MainExeShortcut',
|
||||
'Directory': 'ProgramMenuFolder',
|
||||
'Name': '$(var.PRODUCTLONGNAME)',
|
||||
'Description': 'Downloads station data from Elite: Dangerous',
|
||||
'WorkingDirectory': 'INSTALLDIR',
|
||||
'Icon': 'EDMarketConnector.exe',
|
||||
'IconIndex': '0',
|
||||
'Advertise': 'yes'
|
||||
}
|
||||
)
|
||||
# Now insert the appropriate parts as a child of the ProgramFilesFolder part
|
||||
# of the template.
|
||||
template_tree = etree.parse(str(template_file))
|
||||
program_files_folder = template_tree.find('.//{*}Directory[@Id="ProgramFilesFolder"]')
|
||||
if program_files_folder is None:
|
||||
raise ValueError(f'{template_file}: Expected Directory with Id="ProgramFilesFolder"')
|
||||
|
||||
program_files_folder.insert(0, directory_win32)
|
||||
# Append the Feature/ComponentRef listing to match
|
||||
feature = template_tree.find('.//{*}Feature[@Id="Complete"][@Level="1"]')
|
||||
if feature is None:
|
||||
raise ValueError(f'{template_file}: Expected Feature element with Id="Complete" Level="1"')
|
||||
|
||||
# This isn't part of the components
|
||||
feature.append(
|
||||
etree.Element(
|
||||
'ComponentRef',
|
||||
attrib={
|
||||
'Id': 'RegistryEntries'
|
||||
},
|
||||
nsmap=directory_win32.nsmap
|
||||
)
|
||||
)
|
||||
for c in directory_win32.findall('.//{*}Component'):
|
||||
feature.append(
|
||||
etree.Element(
|
||||
'ComponentRef',
|
||||
attrib={
|
||||
'Id': c.get('Id')
|
||||
},
|
||||
nsmap=directory_win32.nsmap
|
||||
)
|
||||
)
|
||||
|
||||
# Insert what we now have into the template and write it out
|
||||
template_tree.write(
|
||||
str(final_wxs_file), encoding='utf-8',
|
||||
pretty_print=True,
|
||||
xml_declaration=True
|
||||
)
|
||||
|
||||
os.system(rf'"{WIXPATH}\candle.exe" {appname}.wxs')
|
||||
|
||||
if not exists(f'{appname}.wixobj'):
|
||||
raise AssertionError(f'No {appname}.wixobj: candle.exe failed?')
|
||||
|
||||
package_filename = f'{appname}_win_{appversion_nobuild()}.msi'
|
||||
os.system(rf'"{WIXPATH}\light.exe" -sacl -spdb -sw1076 {dist_dir}\{appname}.wixobj -out {package_filename}')
|
||||
os.system(rf'"{WIXPATH}\light.exe" -b {dist_dir}\ -sacl -spdb -sw1076 {appname}.wixobj -out {package_filename}')
|
||||
|
||||
if not exists(package_filename):
|
||||
raise AssertionError(f'light.exe failed, no {package_filename}')
|
||||
|
96
stats.py
96
stats.py
@ -1,9 +1,9 @@
|
||||
"""CMDR Status information."""
|
||||
import csv
|
||||
import json
|
||||
import sys
|
||||
import tkinter
|
||||
import tkinter as tk
|
||||
from sys import platform
|
||||
from tkinter import ttk
|
||||
from typing import TYPE_CHECKING, Any, AnyStr, Callable, Dict, List, NamedTuple, Optional, Sequence, cast
|
||||
|
||||
@ -20,11 +20,9 @@ logger = EDMCLogging.get_main_logger()
|
||||
if TYPE_CHECKING:
|
||||
def _(x: str) -> str: ...
|
||||
|
||||
if platform == 'win32':
|
||||
if sys.platform == 'win32':
|
||||
import ctypes
|
||||
from ctypes.wintypes import HWND, POINT, RECT, SIZE, UINT
|
||||
if TYPE_CHECKING:
|
||||
import ctypes.windll # type: ignore # Fake this into existing, its really a magic dll thing
|
||||
|
||||
try:
|
||||
CalculatePopupWindowPosition = ctypes.windll.user32.CalculatePopupWindowPosition
|
||||
@ -40,6 +38,13 @@ if platform == 'win32':
|
||||
CalculatePopupWindowPosition = None # type: ignore
|
||||
|
||||
|
||||
CR_LINES_START = 1
|
||||
CR_LINES_END = 3
|
||||
RANK_LINES_START = 3
|
||||
RANK_LINES_END = 9
|
||||
POWERPLAY_LINES_START = 9
|
||||
|
||||
|
||||
def status(data: Dict[str, Any]) -> List[List[str]]:
|
||||
"""
|
||||
Get the current status of the cmdr referred to by data.
|
||||
@ -54,20 +59,33 @@ def status(data: Dict[str, Any]) -> List[List[str]]:
|
||||
[_('Loan'), str(data['commander'].get('debt', 0))], # LANG: Cmdr stats
|
||||
]
|
||||
|
||||
_ELITE_RANKS = [ # noqa: N806 # Its a constant, just needs to be updated at runtime
|
||||
_('Elite'), # LANG: Top rank
|
||||
_('Elite I'), # LANG: Top rank +1
|
||||
_('Elite II'), # LANG: Top rank +2
|
||||
_('Elite III'), # LANG: Top rank +3
|
||||
_('Elite IV'), # LANG: Top rank +4
|
||||
_('Elite V'), # LANG: Top rank +5
|
||||
]
|
||||
|
||||
RANKS = [ # noqa: N806 # Its a constant, just needs to be updated at runtime
|
||||
# in output order
|
||||
(_('Combat'), 'combat'), # LANG: Ranking
|
||||
(_('Trade'), 'trade'), # LANG: Ranking
|
||||
(_('Explorer'), 'explore'), # LANG: Ranking
|
||||
(_('CQC'), 'cqc'), # LANG: Ranking
|
||||
(_('Federation'), 'federation'), # LANG: Ranking
|
||||
(_('Empire'), 'empire'), # LANG: Ranking
|
||||
(_('Powerplay'), 'power'), # LANG: Ranking
|
||||
# ??? , 'crime'), # LANG: Ranking
|
||||
# ??? , 'service'), # LANG: Ranking
|
||||
# Names we show people, vs internal names
|
||||
(_('Combat'), 'combat'), # LANG: Ranking
|
||||
(_('Trade'), 'trade'), # LANG: Ranking
|
||||
(_('Explorer'), 'explore'), # LANG: Ranking
|
||||
(_('Mercenary'), 'soldier'), # LANG: Ranking
|
||||
(_('Exobiologist'), 'exobiologist'), # LANG: Ranking
|
||||
(_('CQC'), 'cqc'), # LANG: Ranking
|
||||
(_('Federation'), 'federation'), # LANG: Ranking
|
||||
(_('Empire'), 'empire'), # LANG: Ranking
|
||||
(_('Powerplay'), 'power'), # LANG: Ranking
|
||||
# ??? , 'crime'), # LANG: Ranking
|
||||
# ??? , 'service'), # LANG: Ranking
|
||||
]
|
||||
|
||||
RANK_NAMES = { # noqa: N806 # Its a constant, just needs to be updated at runtime
|
||||
# These names are the fdev side name (but lower()ed)
|
||||
# http://elite-dangerous.wikia.com/wiki/Pilots_Federation#Ranks
|
||||
'combat': [
|
||||
_('Harmless'), # LANG: Combat rank
|
||||
@ -78,8 +96,7 @@ def status(data: Dict[str, Any]) -> List[List[str]]:
|
||||
_('Master'), # LANG: Combat rank
|
||||
_('Dangerous'), # LANG: Combat rank
|
||||
_('Deadly'), # LANG: Combat rank
|
||||
_('Elite'), # LANG: Top rank
|
||||
],
|
||||
] + _ELITE_RANKS,
|
||||
'trade': [
|
||||
_('Penniless'), # LANG: Trade rank
|
||||
_('Mostly Penniless'), # LANG: Trade rank
|
||||
@ -89,8 +106,7 @@ def status(data: Dict[str, Any]) -> List[List[str]]:
|
||||
_('Broker'), # LANG: Trade rank
|
||||
_('Entrepreneur'), # LANG: Trade rank
|
||||
_('Tycoon'), # LANG: Trade rank
|
||||
_('Elite') # LANG: Top rank
|
||||
],
|
||||
] + _ELITE_RANKS,
|
||||
'explore': [
|
||||
_('Aimless'), # LANG: Explorer rank
|
||||
_('Mostly Aimless'), # LANG: Explorer rank
|
||||
@ -100,8 +116,28 @@ def status(data: Dict[str, Any]) -> List[List[str]]:
|
||||
_('Pathfinder'), # LANG: Explorer rank
|
||||
_('Ranger'), # LANG: Explorer rank
|
||||
_('Pioneer'), # LANG: Explorer rank
|
||||
_('Elite') # LANG: Top rank
|
||||
],
|
||||
|
||||
] + _ELITE_RANKS,
|
||||
'soldier': [
|
||||
_('Defenceless'), # LANG: Mercenary rank
|
||||
_('Mostly Defenceless'), # LANG: Mercenary rank
|
||||
_('Rookie'), # LANG: Mercenary rank
|
||||
_('Soldier'), # LANG: Mercenary rank
|
||||
_('Gunslinger'), # LANG: Mercenary rank
|
||||
_('Warrior'), # LANG: Mercenary rank
|
||||
_('Gunslinger'), # LANG: Mercenary rank
|
||||
_('Deadeye'), # LANG: Mercenary rank
|
||||
] + _ELITE_RANKS,
|
||||
'exobiologist': [
|
||||
_('Directionless'), # LANG: Exobiologist rank
|
||||
_('Mostly Directionless'), # LANG: Exobiologist rank
|
||||
_('Compiler'), # LANG: Exobiologist rank
|
||||
_('Collector'), # LANG: Exobiologist rank
|
||||
_('Cataloguer'), # LANG: Exobiologist rank
|
||||
_('Taxonomist'), # LANG: Exobiologist rank
|
||||
_('Ecologist'), # LANG: Exobiologist rank
|
||||
_('Geneticist'), # LANG: Exobiologist rank
|
||||
] + _ELITE_RANKS,
|
||||
'cqc': [
|
||||
_('Helpless'), # LANG: CQC rank
|
||||
_('Mostly Helpless'), # LANG: CQC rank
|
||||
@ -111,8 +147,7 @@ def status(data: Dict[str, Any]) -> List[List[str]]:
|
||||
_('Champion'), # LANG: CQC rank
|
||||
_('Hero'), # LANG: CQC rank
|
||||
_('Gladiator'), # LANG: CQC rank
|
||||
_('Elite') # LANG: Top rank
|
||||
],
|
||||
] + _ELITE_RANKS,
|
||||
|
||||
# http://elite-dangerous.wikia.com/wiki/Federation#Ranks
|
||||
'federation': [
|
||||
@ -335,15 +370,15 @@ class StatsResults(tk.Toplevel):
|
||||
self.transient(parent)
|
||||
|
||||
# position over parent
|
||||
if platform != 'darwin' or parent.winfo_rooty() > 0: # http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7
|
||||
if sys.platform != 'darwin' or parent.winfo_rooty() > 0: # http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7
|
||||
self.geometry(f"+{parent.winfo_rootx()}+{parent.winfo_rooty()}")
|
||||
|
||||
# remove decoration
|
||||
self.resizable(tk.FALSE, tk.FALSE)
|
||||
if platform == 'win32':
|
||||
if sys.platform == 'win32':
|
||||
self.attributes('-toolwindow', tk.TRUE)
|
||||
|
||||
elif platform == 'darwin':
|
||||
elif sys.platform == 'darwin':
|
||||
# http://wiki.tcl.tk/13428
|
||||
parent.call('tk::unsupported::MacWindowStyle', 'style', self, 'utility')
|
||||
|
||||
@ -353,11 +388,16 @@ class StatsResults(tk.Toplevel):
|
||||
notebook = nb.Notebook(frame)
|
||||
|
||||
page = self.addpage(notebook)
|
||||
for thing in stats[1:3]:
|
||||
for thing in stats[CR_LINES_START:CR_LINES_END]:
|
||||
# assumes things two and three are money
|
||||
self.addpagerow(page, [thing[0], self.credits(int(thing[1]))], with_copy=True)
|
||||
|
||||
for thing in stats[3:]:
|
||||
self.addpagespacer(page)
|
||||
for thing in stats[RANK_LINES_START:RANK_LINES_END]:
|
||||
self.addpagerow(page, thing, with_copy=True)
|
||||
|
||||
self.addpagespacer(page)
|
||||
for thing in stats[POWERPLAY_LINES_START:]:
|
||||
self.addpagerow(page, thing, with_copy=True)
|
||||
|
||||
ttk.Frame(page).grid(pady=5) # bottom spacer
|
||||
@ -379,7 +419,7 @@ class StatsResults(tk.Toplevel):
|
||||
ttk.Frame(page).grid(pady=5) # bottom spacer
|
||||
notebook.add(page, text=_('Ships')) # LANG: Status dialog title
|
||||
|
||||
if platform != 'darwin':
|
||||
if sys.platform != 'darwin':
|
||||
buttonframe = ttk.Frame(frame)
|
||||
buttonframe.grid(padx=10, pady=(0, 10), sticky=tk.NSEW) # type: ignore # the tuple is supported
|
||||
buttonframe.columnconfigure(0, weight=1)
|
||||
@ -391,7 +431,7 @@ class StatsResults(tk.Toplevel):
|
||||
self.grab_set()
|
||||
|
||||
# Ensure fully on-screen
|
||||
if platform == 'win32' and CalculatePopupWindowPosition:
|
||||
if sys.platform == 'win32' and CalculatePopupWindowPosition:
|
||||
position = RECT()
|
||||
GetWindowRect(GetParent(self.winfo_id()), position)
|
||||
if CalculatePopupWindowPosition(
|
||||
|
@ -4,7 +4,6 @@ import warnings
|
||||
from configparser import NoOptionError
|
||||
from os import getenv, makedirs, mkdir, pardir
|
||||
from os.path import dirname, expanduser, isdir, join, normpath
|
||||
from sys import platform
|
||||
from typing import TYPE_CHECKING, Optional, Union
|
||||
|
||||
from config import applongname, appname, update_interval
|
||||
@ -12,13 +11,13 @@ from EDMCLogging import get_main_logger
|
||||
|
||||
logger = get_main_logger()
|
||||
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
from Foundation import ( # type: ignore
|
||||
NSApplicationSupportDirectory, NSBundle, NSDocumentDirectory, NSSearchPathForDirectoriesInDomains,
|
||||
NSUserDefaults, NSUserDomainMask
|
||||
)
|
||||
|
||||
elif platform == 'win32':
|
||||
elif sys.platform == 'win32':
|
||||
import ctypes
|
||||
import uuid
|
||||
from ctypes.wintypes import DWORD, HANDLE, HKEY, LONG, LPCVOID, LPCWSTR
|
||||
@ -90,7 +89,7 @@ elif platform == 'win32':
|
||||
CoTaskMemFree(buf) # and free original
|
||||
return retval
|
||||
|
||||
elif platform == 'linux':
|
||||
elif sys.platform == 'linux':
|
||||
import codecs
|
||||
from configparser import RawConfigParser
|
||||
|
||||
@ -114,7 +113,7 @@ class OldConfig():
|
||||
OUT_SYS_EDDN = 2048
|
||||
OUT_SYS_DELAY = 4096
|
||||
|
||||
if platform == 'darwin': # noqa: C901 # It's gating *all* the functions
|
||||
if sys.platform == 'darwin': # noqa: C901 # It's gating *all* the functions
|
||||
|
||||
def __init__(self):
|
||||
self.app_dir = join(
|
||||
@ -199,7 +198,7 @@ class OldConfig():
|
||||
self.save()
|
||||
self.defaults = None
|
||||
|
||||
elif platform == 'win32':
|
||||
elif sys.platform == 'win32':
|
||||
|
||||
def __init__(self):
|
||||
self.app_dir = join(known_folder_path(FOLDERID_LocalAppData), appname) # type: ignore # Not going to change
|
||||
@ -362,7 +361,7 @@ class OldConfig():
|
||||
RegCloseKey(self.hkey)
|
||||
self.hkey = None
|
||||
|
||||
elif platform == 'linux':
|
||||
elif sys.platform == 'linux':
|
||||
SECTION = 'config'
|
||||
|
||||
def __init__(self):
|
@ -1,4 +1,12 @@
|
||||
"""Test the config system."""
|
||||
"""
|
||||
Test the config system.
|
||||
|
||||
Note: These tests to arbitrary reads and writes to an existing config, including
|
||||
key deletions. Said modifications are to keys that are generated internally.
|
||||
|
||||
Most of these tests are parity tests with the "old" config, and likely one day can be
|
||||
entirely removed.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
@ -19,7 +27,7 @@ print(sys.path)
|
||||
|
||||
from _old_config import old_config # noqa: E402
|
||||
|
||||
from config import LinuxConfig, config # noqa: E402
|
||||
from config import config # noqa: E402
|
||||
|
||||
|
||||
def _fuzz_list(length: int) -> List[str]:
|
||||
@ -77,6 +85,11 @@ class TestNewConfig:
|
||||
|
||||
def __update_linuxconfig(self) -> None:
|
||||
"""On linux config uses ConfigParser, which doesn't update from disk changes. Force the update here."""
|
||||
if sys.platform != 'linux':
|
||||
return
|
||||
|
||||
from config.linux import LinuxConfig
|
||||
|
||||
if isinstance(config, LinuxConfig) and config.config is not None:
|
||||
config.config.read(config.filename)
|
||||
|
||||
@ -163,6 +176,10 @@ class TestOldNewConfig:
|
||||
|
||||
def __update_linuxconfig(self) -> None:
|
||||
"""On linux config uses ConfigParser, which doesn't update from disk changes. Force the update here."""
|
||||
if sys.platform != 'linux':
|
||||
return
|
||||
|
||||
from config.linux import LinuxConfig
|
||||
if isinstance(config, LinuxConfig) and config.config is not None:
|
||||
config.config.read(config.filename)
|
||||
|
206
theme.py
206
theme.py
@ -6,9 +6,9 @@
|
||||
#
|
||||
|
||||
import os
|
||||
import sys
|
||||
import tkinter as tk
|
||||
from os.path import join
|
||||
from sys import platform
|
||||
from tkinter import font as tkFont
|
||||
from tkinter import ttk
|
||||
|
||||
@ -18,20 +18,20 @@ from ttkHyperlinkLabel import HyperlinkLabel
|
||||
if __debug__:
|
||||
from traceback import print_exc
|
||||
|
||||
if platform == "linux":
|
||||
if sys.platform == "linux":
|
||||
from ctypes import POINTER, Structure, byref, c_char_p, c_int, c_long, c_uint, c_ulong, c_void_p, cdll
|
||||
|
||||
|
||||
if platform == 'win32':
|
||||
if sys.platform == 'win32':
|
||||
import ctypes
|
||||
from ctypes.wintypes import DWORD, LPCVOID, LPCWSTR
|
||||
AddFontResourceEx = ctypes.windll.gdi32.AddFontResourceExW
|
||||
AddFontResourceEx.restypes = [LPCWSTR, DWORD, LPCVOID]
|
||||
FR_PRIVATE = 0x10
|
||||
FR_PRIVATE = 0x10
|
||||
FR_NOT_ENUM = 0x20
|
||||
AddFontResourceEx(join(config.respath, u'EUROCAPS.TTF'), FR_PRIVATE, 0)
|
||||
|
||||
elif platform == 'linux':
|
||||
elif sys.platform == 'linux':
|
||||
# pyright: reportUnboundVariable=false
|
||||
XID = c_ulong # from X.h: typedef unsigned long XID
|
||||
Window = XID
|
||||
@ -40,7 +40,7 @@ elif platform == 'linux':
|
||||
|
||||
PropModeReplace = 0
|
||||
PropModePrepend = 1
|
||||
PropModeAppend = 2
|
||||
PropModeAppend = 2
|
||||
|
||||
# From xprops.h
|
||||
MWM_HINTS_FUNCTIONS = 1 << 0
|
||||
@ -69,16 +69,17 @@ elif platform == 'linux':
|
||||
('input_mode', c_long),
|
||||
('status', c_ulong),
|
||||
]
|
||||
|
||||
|
||||
# workaround for https://github.com/EDCD/EDMarketConnector/issues/568
|
||||
if not os.getenv("EDMC_NO_UI") :
|
||||
if not os.getenv("EDMC_NO_UI"):
|
||||
try:
|
||||
xlib = cdll.LoadLibrary('libX11.so.6')
|
||||
XInternAtom = xlib.XInternAtom
|
||||
XInternAtom.argtypes = [POINTER(Display), c_char_p, c_int]
|
||||
XInternAtom.restype = Atom
|
||||
XChangeProperty = xlib.XChangeProperty
|
||||
XChangeProperty.argtypes = [POINTER(Display), Window, Atom, Atom, c_int, c_int, POINTER(MotifWmHints), c_int]
|
||||
XChangeProperty.argtypes = [POINTER(Display), Window, Atom, Atom, c_int,
|
||||
c_int, POINTER(MotifWmHints), c_int]
|
||||
XChangeProperty.restype = c_int
|
||||
XFlush = xlib.XFlush
|
||||
XFlush.argtypes = [POINTER(Display)]
|
||||
@ -87,29 +88,31 @@ elif platform == 'linux':
|
||||
XOpenDisplay.argtypes = [c_char_p]
|
||||
XOpenDisplay.restype = POINTER(Display)
|
||||
XQueryTree = xlib.XQueryTree
|
||||
XQueryTree.argtypes = [POINTER(Display), Window, POINTER(Window), POINTER(Window), POINTER(Window), POINTER(c_uint)]
|
||||
XQueryTree.argtypes = [POINTER(Display), Window, POINTER(
|
||||
Window), POINTER(Window), POINTER(Window), POINTER(c_uint)]
|
||||
XQueryTree.restype = c_int
|
||||
dpy = xlib.XOpenDisplay(None)
|
||||
if not dpy:
|
||||
raise Exception("Can't find your display, can't continue")
|
||||
|
||||
|
||||
motif_wm_hints_property = XInternAtom(dpy, b'_MOTIF_WM_HINTS', False)
|
||||
motif_wm_hints_normal = MotifWmHints(MWM_HINTS_FUNCTIONS | MWM_HINTS_DECORATIONS,
|
||||
MWM_FUNC_RESIZE | MWM_FUNC_MOVE | MWM_FUNC_MINIMIZE | MWM_FUNC_CLOSE,
|
||||
MWM_DECOR_BORDER | MWM_DECOR_RESIZEH | MWM_DECOR_TITLE | MWM_DECOR_MENU | MWM_DECOR_MINIMIZE,
|
||||
0, 0)
|
||||
motif_wm_hints_dark = MotifWmHints(MWM_HINTS_FUNCTIONS | MWM_HINTS_DECORATIONS,
|
||||
MWM_FUNC_RESIZE | MWM_FUNC_MOVE | MWM_FUNC_MINIMIZE | MWM_FUNC_CLOSE,
|
||||
0, 0, 0)
|
||||
MWM_FUNC_RESIZE | MWM_FUNC_MOVE | MWM_FUNC_MINIMIZE | MWM_FUNC_CLOSE,
|
||||
MWM_DECOR_BORDER | MWM_DECOR_RESIZEH | MWM_DECOR_TITLE | MWM_DECOR_MENU | MWM_DECOR_MINIMIZE,
|
||||
0, 0)
|
||||
motif_wm_hints_dark = MotifWmHints(MWM_HINTS_FUNCTIONS | MWM_HINTS_DECORATIONS,
|
||||
MWM_FUNC_RESIZE | MWM_FUNC_MOVE | MWM_FUNC_MINIMIZE | MWM_FUNC_CLOSE,
|
||||
0, 0, 0)
|
||||
except:
|
||||
if __debug__: print_exc()
|
||||
if __debug__:
|
||||
print_exc()
|
||||
dpy = None
|
||||
|
||||
|
||||
class _Theme(object):
|
||||
|
||||
def __init__(self):
|
||||
self.active = None # Starts out with no theme
|
||||
self.active = None # Starts out with no theme
|
||||
self.minwidth = None
|
||||
self.widgets = {}
|
||||
self.widgets_pair = []
|
||||
@ -124,18 +127,18 @@ class _Theme(object):
|
||||
if not self.defaults:
|
||||
# Can't initialise this til window is created # Windows, MacOS
|
||||
self.defaults = {
|
||||
'fg' : tk.Label()['foreground'], # SystemButtonText, systemButtonText
|
||||
'bg' : tk.Label()['background'], # SystemButtonFace, White
|
||||
'font' : tk.Label()['font'], # TkDefaultFont
|
||||
'bitmapfg' : tk.BitmapImage()['foreground'], # '-foreground {} {} #000000 #000000'
|
||||
'bitmapbg' : tk.BitmapImage()['background'], # '-background {} {} {} {}'
|
||||
'entryfg' : tk.Entry()['foreground'], # SystemWindowText, Black
|
||||
'entrybg' : tk.Entry()['background'], # SystemWindow, systemWindowBody
|
||||
'entryfont' : tk.Entry()['font'], # TkTextFont
|
||||
'frame' : tk.Frame()['background'], # SystemButtonFace, systemWindowBody
|
||||
'menufg' : tk.Menu()['foreground'], # SystemMenuText,
|
||||
'menubg' : tk.Menu()['background'], # SystemMenu,
|
||||
'menufont' : tk.Menu()['font'], # TkTextFont
|
||||
'fg': tk.Label()['foreground'], # SystemButtonText, systemButtonText
|
||||
'bg': tk.Label()['background'], # SystemButtonFace, White
|
||||
'font': tk.Label()['font'], # TkDefaultFont
|
||||
'bitmapfg': tk.BitmapImage()['foreground'], # '-foreground {} {} #000000 #000000'
|
||||
'bitmapbg': tk.BitmapImage()['background'], # '-background {} {} {} {}'
|
||||
'entryfg': tk.Entry()['foreground'], # SystemWindowText, Black
|
||||
'entrybg': tk.Entry()['background'], # SystemWindow, systemWindowBody
|
||||
'entryfont': tk.Entry()['font'], # TkTextFont
|
||||
'frame': tk.Frame()['background'], # SystemButtonFace, systemWindowBody
|
||||
'menufg': tk.Menu()['foreground'], # SystemMenuText,
|
||||
'menubg': tk.Menu()['background'], # SystemMenu,
|
||||
'menufont': tk.Menu()['font'], # TkTextFont
|
||||
}
|
||||
|
||||
if widget not in self.widgets:
|
||||
@ -189,26 +192,27 @@ class _Theme(object):
|
||||
def _enter(self, event, image):
|
||||
widget = event.widget
|
||||
if widget and widget['state'] != tk.DISABLED:
|
||||
widget.configure(state = tk.ACTIVE)
|
||||
widget.configure(state=tk.ACTIVE)
|
||||
if image:
|
||||
image.configure(foreground = self.current['activeforeground'], background = self.current['activebackground'])
|
||||
image.configure(foreground=self.current['activeforeground'],
|
||||
background=self.current['activebackground'])
|
||||
|
||||
def _leave(self, event, image):
|
||||
widget = event.widget
|
||||
if widget and widget['state'] != tk.DISABLED:
|
||||
widget.configure(state = tk.NORMAL)
|
||||
widget.configure(state=tk.NORMAL)
|
||||
if image:
|
||||
image.configure(foreground = self.current['foreground'], background = self.current['background'])
|
||||
image.configure(foreground=self.current['foreground'], background=self.current['background'])
|
||||
|
||||
# Set up colors
|
||||
def _colors(self, root, theme):
|
||||
style = ttk.Style()
|
||||
if platform == 'linux':
|
||||
if sys.platform == 'linux':
|
||||
style.theme_use('clam')
|
||||
|
||||
# Default dark theme colors
|
||||
if not config.get_str('dark_text'):
|
||||
config.set('dark_text', '#ff8000') # "Tangerine" in OSX color picker
|
||||
config.set('dark_text', '#ff8000') # "Tangerine" in OSX color picker
|
||||
if not config.get_str('dark_highlight'):
|
||||
config.set('dark_highlight', 'white')
|
||||
|
||||
@ -216,40 +220,40 @@ class _Theme(object):
|
||||
# Dark
|
||||
(r, g, b) = root.winfo_rgb(config.get_str('dark_text'))
|
||||
self.current = {
|
||||
'background' : 'grey4', # OSX inactive dark titlebar color
|
||||
'foreground' : config.get_str('dark_text'),
|
||||
'activebackground' : config.get_str('dark_text'),
|
||||
'activeforeground' : 'grey4',
|
||||
'disabledforeground' : '#%02x%02x%02x' % (int(r/384), int(g/384), int(b/384)),
|
||||
'highlight' : config.get_str('dark_highlight'),
|
||||
'background': 'grey4', # OSX inactive dark titlebar color
|
||||
'foreground': config.get_str('dark_text'),
|
||||
'activebackground': config.get_str('dark_text'),
|
||||
'activeforeground': 'grey4',
|
||||
'disabledforeground': '#%02x%02x%02x' % (int(r/384), int(g/384), int(b/384)),
|
||||
'highlight': config.get_str('dark_highlight'),
|
||||
# Font only supports Latin 1 / Supplement / Extended, and a few General Punctuation and Mathematical Operators
|
||||
# LANG: Label for commander name in main window
|
||||
'font' : (theme > 1 and not 0x250 < ord(_('Cmdr')[0]) < 0x3000 and
|
||||
tkFont.Font(family='Euro Caps', size=10, weight=tkFont.NORMAL) or
|
||||
'TkDefaultFont'),
|
||||
'font': (theme > 1 and not 0x250 < ord(_('Cmdr')[0]) < 0x3000 and
|
||||
tkFont.Font(family='Euro Caps', size=10, weight=tkFont.NORMAL) or
|
||||
'TkDefaultFont'),
|
||||
}
|
||||
else:
|
||||
# (Mostly) system colors
|
||||
style = ttk.Style()
|
||||
self.current = {
|
||||
'background' : (platform == 'darwin' and 'systemMovableModalBackground' or
|
||||
style.lookup('TLabel', 'background')),
|
||||
'foreground' : style.lookup('TLabel', 'foreground'),
|
||||
'activebackground' : (platform == 'win32' and 'SystemHighlight' or
|
||||
style.lookup('TLabel', 'background', ['active'])),
|
||||
'activeforeground' : (platform == 'win32' and 'SystemHighlightText' or
|
||||
style.lookup('TLabel', 'foreground', ['active'])),
|
||||
'disabledforeground' : style.lookup('TLabel', 'foreground', ['disabled']),
|
||||
'highlight' : 'blue',
|
||||
'font' : 'TkDefaultFont',
|
||||
'background': (sys.platform == 'darwin' and 'systemMovableModalBackground' or
|
||||
style.lookup('TLabel', 'background')),
|
||||
'foreground': style.lookup('TLabel', 'foreground'),
|
||||
'activebackground': (sys.platform == 'win32' and 'SystemHighlight' or
|
||||
style.lookup('TLabel', 'background', ['active'])),
|
||||
'activeforeground': (sys.platform == 'win32' and 'SystemHighlightText' or
|
||||
style.lookup('TLabel', 'foreground', ['active'])),
|
||||
'disabledforeground': style.lookup('TLabel', 'foreground', ['disabled']),
|
||||
'highlight': 'blue',
|
||||
'font': 'TkDefaultFont',
|
||||
}
|
||||
|
||||
|
||||
# Apply current theme to a widget and its children, and register it for future updates
|
||||
|
||||
def update(self, widget):
|
||||
assert isinstance(widget, tk.Widget) or isinstance(widget, tk.BitmapImage), widget
|
||||
if not self.current:
|
||||
return # No need to call this for widgets created in plugin_app()
|
||||
return # No need to call this for widgets created in plugin_app()
|
||||
self.register(widget)
|
||||
self._update_widget(widget)
|
||||
if isinstance(widget, tk.Frame) or isinstance(widget, ttk.Frame):
|
||||
@ -258,56 +262,57 @@ class _Theme(object):
|
||||
|
||||
# Apply current theme to a single widget
|
||||
def _update_widget(self, widget):
|
||||
assert widget in self.widgets, '%s %s "%s"' %(widget.winfo_class(), widget, 'text' in widget.keys() and widget['text'])
|
||||
assert widget in self.widgets, '%s %s "%s"' % (
|
||||
widget.winfo_class(), widget, 'text' in widget.keys() and widget['text'])
|
||||
attribs = self.widgets.get(widget, [])
|
||||
|
||||
if isinstance(widget, tk.BitmapImage):
|
||||
# not a widget
|
||||
if 'fg' not in attribs:
|
||||
widget.configure(foreground = self.current['foreground']),
|
||||
widget.configure(foreground=self.current['foreground']),
|
||||
if 'bg' not in attribs:
|
||||
widget.configure(background = self.current['background'])
|
||||
widget.configure(background=self.current['background'])
|
||||
elif 'cursor' in widget.keys() and str(widget['cursor']) not in ['', 'arrow']:
|
||||
# Hack - highlight widgets like HyperlinkLabel with a non-default cursor
|
||||
if 'fg' not in attribs:
|
||||
widget.configure(foreground = self.current['highlight']),
|
||||
if 'insertbackground' in widget.keys(): # tk.Entry
|
||||
widget.configure(insertbackground = self.current['foreground']),
|
||||
widget.configure(foreground=self.current['highlight']),
|
||||
if 'insertbackground' in widget.keys(): # tk.Entry
|
||||
widget.configure(insertbackground=self.current['foreground']),
|
||||
if 'bg' not in attribs:
|
||||
widget.configure(background = self.current['background'])
|
||||
if 'highlightbackground' in widget.keys(): # tk.Entry
|
||||
widget.configure(highlightbackground = self.current['background'])
|
||||
widget.configure(background=self.current['background'])
|
||||
if 'highlightbackground' in widget.keys(): # tk.Entry
|
||||
widget.configure(highlightbackground=self.current['background'])
|
||||
if 'font' not in attribs:
|
||||
widget.configure(font = self.current['font'])
|
||||
widget.configure(font=self.current['font'])
|
||||
elif 'activeforeground' in widget.keys():
|
||||
# e.g. tk.Button, tk.Label, tk.Menu
|
||||
if 'fg' not in attribs:
|
||||
widget.configure(foreground = self.current['foreground'],
|
||||
activeforeground = self.current['activeforeground'],
|
||||
disabledforeground = self.current['disabledforeground'])
|
||||
widget.configure(foreground=self.current['foreground'],
|
||||
activeforeground=self.current['activeforeground'],
|
||||
disabledforeground=self.current['disabledforeground'])
|
||||
if 'bg' not in attribs:
|
||||
widget.configure(background = self.current['background'],
|
||||
activebackground = self.current['activebackground'])
|
||||
if platform == 'darwin' and isinstance(widget, tk.Button):
|
||||
widget.configure(highlightbackground = self.current['background'])
|
||||
widget.configure(background=self.current['background'],
|
||||
activebackground=self.current['activebackground'])
|
||||
if sys.platform == 'darwin' and isinstance(widget, tk.Button):
|
||||
widget.configure(highlightbackground=self.current['background'])
|
||||
if 'font' not in attribs:
|
||||
widget.configure(font = self.current['font'])
|
||||
widget.configure(font=self.current['font'])
|
||||
elif 'foreground' in widget.keys():
|
||||
# e.g. ttk.Label
|
||||
if 'fg' not in attribs:
|
||||
widget.configure(foreground = self.current['foreground']),
|
||||
widget.configure(foreground=self.current['foreground']),
|
||||
if 'bg' not in attribs:
|
||||
widget.configure(background = self.current['background'])
|
||||
widget.configure(background=self.current['background'])
|
||||
if 'font' not in attribs:
|
||||
widget.configure(font = self.current['font'])
|
||||
widget.configure(font=self.current['font'])
|
||||
elif 'background' in widget.keys() or isinstance(widget, tk.Canvas):
|
||||
# e.g. Frame, Canvas
|
||||
if 'bg' not in attribs:
|
||||
widget.configure(background = self.current['background'],
|
||||
highlightbackground = self.current['disabledforeground'])
|
||||
|
||||
widget.configure(background=self.current['background'],
|
||||
highlightbackground=self.current['disabledforeground'])
|
||||
|
||||
# Apply configured theme
|
||||
|
||||
def apply(self, root):
|
||||
|
||||
theme = config.get_int('theme')
|
||||
@ -316,7 +321,7 @@ class _Theme(object):
|
||||
# Apply colors
|
||||
for widget in set(self.widgets):
|
||||
if isinstance(widget, tk.Widget) and not widget.winfo_exists():
|
||||
self.widgets.pop(widget) # has been destroyed
|
||||
self.widgets.pop(widget) # has been destroyed
|
||||
else:
|
||||
self._update_widget(widget)
|
||||
|
||||
@ -334,58 +339,61 @@ class _Theme(object):
|
||||
pair[theme].grid(**gridopts)
|
||||
|
||||
if self.active == theme:
|
||||
return # Don't need to mess with the window manager
|
||||
return # Don't need to mess with the window manager
|
||||
else:
|
||||
self.active = theme
|
||||
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
from AppKit import NSAppearance, NSApplication, NSMiniaturizableWindowMask, NSResizableWindowMask
|
||||
root.update_idletasks() # need main window to be created
|
||||
root.update_idletasks() # need main window to be created
|
||||
appearance = NSAppearance.appearanceNamed_(theme and
|
||||
'NSAppearanceNameDarkAqua' or
|
||||
'NSAppearanceNameAqua')
|
||||
for window in NSApplication.sharedApplication().windows():
|
||||
window.setStyleMask_(window.styleMask() & ~(NSMiniaturizableWindowMask | NSResizableWindowMask)) # disable zoom
|
||||
window.setStyleMask_(window.styleMask() & ~(
|
||||
NSMiniaturizableWindowMask | NSResizableWindowMask)) # disable zoom
|
||||
window.setAppearance_(appearance)
|
||||
|
||||
elif platform == 'win32':
|
||||
elif sys.platform == 'win32':
|
||||
GWL_STYLE = -16
|
||||
WS_MAXIMIZEBOX = 0x00010000
|
||||
WS_MAXIMIZEBOX = 0x00010000
|
||||
# tk8.5.9/win/tkWinWm.c:342
|
||||
GWL_EXSTYLE = -20
|
||||
WS_EX_APPWINDOW = 0x00040000
|
||||
WS_EX_LAYERED = 0x00080000
|
||||
WS_EX_APPWINDOW = 0x00040000
|
||||
WS_EX_LAYERED = 0x00080000
|
||||
GetWindowLongW = ctypes.windll.user32.GetWindowLongW
|
||||
SetWindowLongW = ctypes.windll.user32.SetWindowLongW
|
||||
|
||||
root.overrideredirect(theme and 1 or 0)
|
||||
root.attributes("-transparentcolor", theme > 1 and 'grey4' or '')
|
||||
root.withdraw()
|
||||
root.update_idletasks() # Size and windows styles get recalculated here
|
||||
root.update_idletasks() # Size and windows styles get recalculated here
|
||||
hwnd = ctypes.windll.user32.GetParent(root.winfo_id())
|
||||
SetWindowLongW(hwnd, GWL_STYLE, GetWindowLongW(hwnd, GWL_STYLE) & ~WS_MAXIMIZEBOX) # disable maximize
|
||||
SetWindowLongW(hwnd, GWL_EXSTYLE, theme > 1 and WS_EX_APPWINDOW|WS_EX_LAYERED or WS_EX_APPWINDOW) # Add to taskbar
|
||||
SetWindowLongW(hwnd, GWL_STYLE, GetWindowLongW(hwnd, GWL_STYLE) & ~WS_MAXIMIZEBOX) # disable maximize
|
||||
SetWindowLongW(hwnd, GWL_EXSTYLE, theme > 1 and WS_EX_APPWINDOW |
|
||||
WS_EX_LAYERED or WS_EX_APPWINDOW) # Add to taskbar
|
||||
root.deiconify()
|
||||
root.wait_visibility() # need main window to be displayed before returning
|
||||
root.wait_visibility() # need main window to be displayed before returning
|
||||
|
||||
else:
|
||||
root.withdraw()
|
||||
root.update_idletasks() # Size gets recalculated here
|
||||
root.update_idletasks() # Size gets recalculated here
|
||||
if dpy:
|
||||
xroot = Window()
|
||||
parent = Window()
|
||||
children = Window()
|
||||
nchildren = c_uint()
|
||||
XQueryTree(dpy, root.winfo_id(), byref(xroot), byref(parent), byref(children), byref(nchildren))
|
||||
XChangeProperty(dpy, parent, motif_wm_hints_property, motif_wm_hints_property, 32, PropModeReplace, theme and motif_wm_hints_dark or motif_wm_hints_normal, 5)
|
||||
XChangeProperty(dpy, parent, motif_wm_hints_property, motif_wm_hints_property, 32,
|
||||
PropModeReplace, theme and motif_wm_hints_dark or motif_wm_hints_normal, 5)
|
||||
XFlush(dpy)
|
||||
else:
|
||||
root.overrideredirect(theme and 1 or 0)
|
||||
root.deiconify()
|
||||
root.wait_visibility() # need main window to be displayed before returning
|
||||
root.wait_visibility() # need main window to be displayed before returning
|
||||
|
||||
if not self.minwidth:
|
||||
self.minwidth = root.winfo_width() # Minimum width = width on first creation
|
||||
self.minwidth = root.winfo_width() # Minimum width = width on first creation
|
||||
root.minsize(self.minwidth, -1)
|
||||
|
||||
|
||||
|
@ -1,22 +1,24 @@
|
||||
|
||||
"""A requests.session with a TimeoutAdapter."""
|
||||
import requests
|
||||
from requests.adapters import HTTPAdapter
|
||||
|
||||
from config import user_agent
|
||||
|
||||
REQUEST_TIMEOUT = 10 # reasonable timeout that all HTTP requests should use
|
||||
|
||||
|
||||
class TimeoutAdapter(HTTPAdapter):
|
||||
"""
|
||||
TimeoutAdapter is an HTTP Adapter that enforces an overridable default timeout on HTTP requests.
|
||||
"""
|
||||
def __init__(self, timeout, *args, **kwargs):
|
||||
"""An HTTP Adapter that enforces an overridable default timeout on HTTP requests."""
|
||||
|
||||
def __init__(self, timeout: int, *args, **kwargs):
|
||||
self.default_timeout = timeout
|
||||
if kwargs.get("timeout") is not None:
|
||||
del kwargs["timeout"]
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def send(self, *args, **kwargs):
|
||||
def send(self, *args, **kwargs) -> requests.Response:
|
||||
"""Send, but with a timeout always set."""
|
||||
if kwargs["timeout"] is None:
|
||||
kwargs["timeout"] = self.default_timeout
|
||||
|
||||
@ -25,7 +27,7 @@ class TimeoutAdapter(HTTPAdapter):
|
||||
|
||||
def new_session(timeout: int = REQUEST_TIMEOUT, session: requests.Session = None) -> requests.Session:
|
||||
"""
|
||||
new_session creates a new requests.Session and overrides the default HTTPAdapter with a TimeoutAdapter.
|
||||
Create a new requests.Session and override the default HTTPAdapter with a TimeoutAdapter.
|
||||
|
||||
:param timeout: the timeout to set the TimeoutAdapter to, defaults to REQUEST_TIMEOUT
|
||||
:param session: the Session object to attach the Adapter to, defaults to a new session
|
||||
@ -33,6 +35,7 @@ def new_session(timeout: int = REQUEST_TIMEOUT, session: requests.Session = None
|
||||
"""
|
||||
if session is None:
|
||||
session = requests.Session()
|
||||
session.headers['User-Agent'] = user_agent
|
||||
|
||||
adapter = TimeoutAdapter(timeout)
|
||||
session.mount("http://", adapter)
|
||||
|
@ -1,14 +1,12 @@
|
||||
import sys
|
||||
import tkinter as tk
|
||||
import webbrowser
|
||||
from sys import platform
|
||||
from tkinter import font as tkFont
|
||||
from tkinter import ttk
|
||||
|
||||
if platform == 'win32':
|
||||
if sys.platform == 'win32':
|
||||
import subprocess
|
||||
from winreg import (
|
||||
HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, CloseKey, OpenKeyEx, QueryValueEx
|
||||
)
|
||||
from winreg import HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, CloseKey, OpenKeyEx, QueryValueEx
|
||||
|
||||
# A clickable ttk Label
|
||||
#
|
||||
@ -18,19 +16,22 @@ if platform == 'win32':
|
||||
# popup_copy: Whether right-click on non-empty label text pops up a context menu with a 'Copy' option. Defaults to no context menu. If popup_copy is a function it will be called with the current label text and should return a boolean.
|
||||
#
|
||||
# May be imported by plugins
|
||||
class HyperlinkLabel(platform == 'darwin' and tk.Label or ttk.Label, object):
|
||||
|
||||
|
||||
class HyperlinkLabel(sys.platform == 'darwin' and tk.Label or ttk.Label, object):
|
||||
|
||||
def __init__(self, master=None, **kw):
|
||||
self.url = 'url' in kw and kw.pop('url') or None
|
||||
self.popup_copy = kw.pop('popup_copy', False)
|
||||
self.underline = kw.pop('underline', None) # override ttk.Label's underline
|
||||
self.underline = kw.pop('underline', None) # override ttk.Label's underline
|
||||
self.foreground = kw.get('foreground') or 'blue'
|
||||
self.disabledforeground = kw.pop('disabledforeground', ttk.Style().lookup('TLabel', 'foreground', ('disabled',))) # ttk.Label doesn't support disabledforeground option
|
||||
self.disabledforeground = kw.pop('disabledforeground', ttk.Style().lookup(
|
||||
'TLabel', 'foreground', ('disabled',))) # ttk.Label doesn't support disabledforeground option
|
||||
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
# Use tk.Label 'cos can't set ttk.Label background - http://www.tkdocs.com/tutorial/styles.html#whydifficult
|
||||
kw['background'] = kw.pop('background', 'systemDialogBackgroundActive')
|
||||
kw['anchor'] = kw.pop('anchor', tk.W) # like ttk.Label
|
||||
kw['anchor'] = kw.pop('anchor', tk.W) # like ttk.Label
|
||||
tk.Label.__init__(self, master, **kw)
|
||||
else:
|
||||
ttk.Label.__init__(self, master, **kw)
|
||||
@ -39,16 +40,16 @@ class HyperlinkLabel(platform == 'darwin' and tk.Label or ttk.Label, object):
|
||||
|
||||
self.menu = tk.Menu(None, tearoff=tk.FALSE)
|
||||
# LANG: Label for 'Copy' as in 'Copy and Paste'
|
||||
self.menu.add_command(label=_('Copy'), command = self.copy) # As in Copy and Paste
|
||||
self.bind(platform == 'darwin' and '<Button-2>' or '<Button-3>', self._contextmenu)
|
||||
self.menu.add_command(label=_('Copy'), command=self.copy) # As in Copy and Paste
|
||||
self.bind(sys.platform == 'darwin' and '<Button-2>' or '<Button-3>', self._contextmenu)
|
||||
|
||||
self.bind('<Enter>', self._enter)
|
||||
self.bind('<Leave>', self._leave)
|
||||
|
||||
# set up initial appearance
|
||||
self.configure(state = kw.get('state', tk.NORMAL),
|
||||
text = kw.get('text'),
|
||||
font = kw.get('font', ttk.Style().lookup('TLabel', 'font')))
|
||||
self.configure(state=kw.get('state', tk.NORMAL),
|
||||
text=kw.get('text'),
|
||||
font=kw.get('font', ttk.Style().lookup('TLabel', 'font')))
|
||||
|
||||
# Change cursor and appearance depending on state and text
|
||||
def configure(self, cnf=None, **kw):
|
||||
@ -70,17 +71,18 @@ class HyperlinkLabel(platform == 'darwin' and tk.Label or ttk.Label, object):
|
||||
|
||||
if 'font' in kw:
|
||||
self.font_n = kw['font']
|
||||
self.font_u = tkFont.Font(font = self.font_n)
|
||||
self.font_u.configure(underline = True)
|
||||
self.font_u = tkFont.Font(font=self.font_n)
|
||||
self.font_u.configure(underline=True)
|
||||
kw['font'] = self.underline is True and self.font_u or self.font_n
|
||||
|
||||
if 'cursor' not in kw:
|
||||
if (kw['state'] if 'state' in kw else str(self['state'])) == tk.DISABLED:
|
||||
kw['cursor'] = 'arrow' # System default
|
||||
kw['cursor'] = 'arrow' # System default
|
||||
elif self.url and (kw['text'] if 'text' in kw else self['text']):
|
||||
kw['cursor'] = platform=='darwin' and 'pointinghand' or 'hand2'
|
||||
kw['cursor'] = sys.platform == 'darwin' and 'pointinghand' or 'hand2'
|
||||
else:
|
||||
kw['cursor'] = (platform=='darwin' and 'notallowed') or (platform=='win32' and 'no') or 'circle'
|
||||
kw['cursor'] = (sys.platform == 'darwin' and 'notallowed') or (
|
||||
sys.platform == 'win32' and 'no') or 'circle'
|
||||
|
||||
super(HyperlinkLabel, self).configure(cnf, **kw)
|
||||
|
||||
@ -89,22 +91,22 @@ class HyperlinkLabel(platform == 'darwin' and tk.Label or ttk.Label, object):
|
||||
|
||||
def _enter(self, event):
|
||||
if self.url and self.underline is not False and str(self['state']) != tk.DISABLED:
|
||||
super(HyperlinkLabel, self).configure(font = self.font_u)
|
||||
super(HyperlinkLabel, self).configure(font=self.font_u)
|
||||
|
||||
def _leave(self, event):
|
||||
if not self.underline:
|
||||
super(HyperlinkLabel, self).configure(font = self.font_n)
|
||||
super(HyperlinkLabel, self).configure(font=self.font_n)
|
||||
|
||||
def _click(self, event):
|
||||
if self.url and self['text'] and str(self['state']) != tk.DISABLED:
|
||||
url = self.url(self['text']) if callable(self.url) else self.url
|
||||
if url:
|
||||
self._leave(event) # Remove underline before we change window to browser
|
||||
self._leave(event) # Remove underline before we change window to browser
|
||||
openurl(url)
|
||||
|
||||
def _contextmenu(self, event):
|
||||
if self['text'] and (self.popup_copy(self['text']) if callable(self.popup_copy) else self.popup_copy):
|
||||
self.menu.post(platform == 'darwin' and event.x_root + 1 or event.x_root, event.y_root)
|
||||
self.menu.post(sys.platform == 'darwin' and event.x_root + 1 or event.x_root, event.y_root)
|
||||
|
||||
def copy(self):
|
||||
self.clipboard_clear()
|
||||
@ -112,13 +114,14 @@ class HyperlinkLabel(platform == 'darwin' and tk.Label or ttk.Label, object):
|
||||
|
||||
|
||||
def openurl(url):
|
||||
if platform == 'win32':
|
||||
if sys.platform == 'win32':
|
||||
# On Windows webbrowser.open calls os.startfile which calls ShellExecute which can't handle long arguments,
|
||||
# so discover and launch the browser directly.
|
||||
# https://blogs.msdn.microsoft.com/oldnewthing/20031210-00/?p=41553
|
||||
|
||||
try:
|
||||
hkey = OpenKeyEx(HKEY_CURRENT_USER, r'Software\Microsoft\Windows\Shell\Associations\UrlAssociations\https\UserChoice')
|
||||
hkey = OpenKeyEx(HKEY_CURRENT_USER,
|
||||
r'Software\Microsoft\Windows\Shell\Associations\UrlAssociations\https\UserChoice')
|
||||
(value, typ) = QueryValueEx(hkey, 'ProgId')
|
||||
CloseKey(hkey)
|
||||
if value in ['IE.HTTP', 'AppXq0fevzme2pys62n3e0fbqa7peapykr8v']:
|
||||
@ -128,7 +131,7 @@ def openurl(url):
|
||||
else:
|
||||
cls = value
|
||||
except:
|
||||
cls = 'https'
|
||||
cls = 'https'
|
||||
|
||||
if cls:
|
||||
try:
|
||||
|
27
util/text.py
Normal file
27
util/text.py
Normal file
@ -0,0 +1,27 @@
|
||||
"""Utilities for dealing with text (and byte representations thereof)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from gzip import compress
|
||||
|
||||
__all__ = ['gzip']
|
||||
|
||||
|
||||
def gzip(data: str | bytes, max_size: int = 512, encoding='utf-8') -> tuple[bytes, bool]:
|
||||
"""
|
||||
Compress the given data if the max size is greater than specified.
|
||||
|
||||
The default was chosen somewhat arbitrarily, see eddn.py for some more careful
|
||||
work towards keeping the data almost always compressed
|
||||
|
||||
:param data: The data to compress
|
||||
:param max_size: The max size of data, in bytes, defaults to 512
|
||||
:param encoding: The encoding to use if data is a str, defaults to 'utf-8'
|
||||
:return: the payload to send, and a bool indicating compression state
|
||||
"""
|
||||
if isinstance(data, str):
|
||||
data = data.encode(encoding=encoding)
|
||||
|
||||
if len(data) <= max_size:
|
||||
return data, False
|
||||
|
||||
return compress(data), True
|
File diff suppressed because it is too large
Load Diff
125
wix/template.wxs
Normal file
125
wix/template.wxs
Normal file
@ -0,0 +1,125 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<?define PRODUCTNAME = "EDMarketConnector"?>
|
||||
<?define PRODUCTLONGNAME = "Elite Dangerous Market Connector"?>
|
||||
<?define PRODUCTVERSION = "!(bind.fileVersion.EDMarketConnector.exe)" ?>
|
||||
<?define UPGRADECODE = "9df571ae-d56d-46e6-af79-4e72ad54efe6" ?>
|
||||
|
||||
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
|
||||
<Product Id="*"
|
||||
Name="$(var.PRODUCTLONGNAME)"
|
||||
Version="$(var.PRODUCTVERSION)"
|
||||
UpgradeCode="$(var.UPGRADECODE)"
|
||||
Language="!(bind.fileLanguage.EDMarketConnector.exe)"
|
||||
Manufacturer="EDCD">
|
||||
|
||||
<Package Id="*" Keywords="Installer"
|
||||
InstallScope="perMachine"
|
||||
Description="$(var.PRODUCTLONGNAME) installer"
|
||||
InstallerVersion="300" Compressed="yes"
|
||||
Platform="x86"
|
||||
Languages="1033,1029,1031,1034,1035,1036,1038,1040,1041,1043,1045,1046,1049,1058,1062,2052,2070,2074,6170,1060,1053,18,0" />
|
||||
<!-- en cs, de es fi fr hu it ja nl pl pt-BR ru uk lv zh-CN pt-PT sr-Latn sr-Latn-BA sl sv-SE ko neutral -->
|
||||
<!-- https://msdn.microsoft.com/en-gb/goglobal/bb964664.aspx -->
|
||||
|
||||
<!-- Always reinstall since patching is problematic -->
|
||||
<!-- http://www.joyofsetup.com/2010/01/16/major-upgrades-now-easier-than-ever/ -->
|
||||
<MajorUpgrade AllowSameVersionUpgrades="yes" DowngradeErrorMessage="A newer version of [ProductName] is already installed." />
|
||||
|
||||
<Media Id="1" Cabinet="product.cab" EmbedCab="yes" />
|
||||
|
||||
<Icon Id="EDMarketConnector.exe" SourceFile="EDMarketConnector.ico"/>
|
||||
|
||||
<!-- For Add/Remove programs -->
|
||||
<Property Id="ARPPRODUCTICON" Value="EDMarketConnector.exe" />
|
||||
<Property Id="ARPNOMODIFY" Value="yes" Secure="yes" /> <!-- Remove modify - also set by WixUI_Minimal -->
|
||||
<Property Id="ARPHELPLINK" Value="https://github.com/EDCD/EDMarketConnector/wiki" />
|
||||
|
||||
<!-- Set INSTALLDIR from ARPINSTALLLOCATION if replacing/upgrading -->
|
||||
<!-- https://wyrdfish.wordpress.com/2012/07/20/msi-writing-guidelines-this-may-be-out-of-date/ -->
|
||||
<Property Id="ARPINSTALLLOCATION">
|
||||
<RegistrySearch Id="GetARPINSTALLLOCATION"
|
||||
Root="HKLM"
|
||||
Key="Software\Microsoft\Windows\CurrentVersion\Uninstall\[WIX_UPGRADE_DETECTED]"
|
||||
Name="InstallLocation"
|
||||
Type="raw" />
|
||||
</Property>
|
||||
<CustomAction Id="SetINSTALLDIR" Property="INSTALLDIR" Value="[ARPINSTALLLOCATION]" />
|
||||
<InstallUISequence>
|
||||
<Custom Action="SetINSTALLDIR" After="AppSearch">
|
||||
WIX_UPGRADE_DETECTED AND ARPINSTALLLOCATION
|
||||
</Custom>
|
||||
</InstallUISequence>
|
||||
<InstallExecuteSequence>
|
||||
<Custom Action="SetINSTALLDIR" After="AppSearch">
|
||||
WIX_UPGRADE_DETECTED AND ARPINSTALLLOCATION
|
||||
</Custom>
|
||||
</InstallExecuteSequence>
|
||||
|
||||
<!-- Set ARPINSTALLLOCATION from INSTALLDIR if new install -->
|
||||
<!-- http://blogs.technet.com/b/alexshev/archive/2008/02/09/from-msi-to-wix-part-2.aspx -->
|
||||
<CustomAction Id="SetARPINSTALLLOCATION" Property="ARPINSTALLLOCATION" Value="[INSTALLDIR]" />
|
||||
<InstallExecuteSequence>
|
||||
<Custom Action="SetARPINSTALLLOCATION" After="InstallValidate">
|
||||
NOT Installed
|
||||
</Custom>
|
||||
</InstallExecuteSequence>
|
||||
|
||||
<!-- Launch app after upgrade -->
|
||||
<Property Id="LAUNCH" Value="yes" />
|
||||
<CustomAction Id="DoLaunch"
|
||||
Directory="INSTALLDIR"
|
||||
ExeCommand='"[INSTALLDIR]EDMarketConnector.exe"'
|
||||
Return="asyncNoWait"
|
||||
Execute="deferred"
|
||||
Impersonate="yes"
|
||||
/>
|
||||
<InstallExecuteSequence>
|
||||
<!-- http://alekdavis.blogspot.co.uk/2013/05/wix-woes-what-is-your-installer-doing.html -->
|
||||
<Custom Action="DoLaunch" Before="InstallFinalize">
|
||||
NOT Installed AND LAUNCH ~= "yes"
|
||||
</Custom>
|
||||
</InstallExecuteSequence>
|
||||
|
||||
<Directory Id="TARGETDIR" Name="SourceDir">
|
||||
|
||||
<!-- http://wixtoolset.org/documentation/manual/v3/howtos/files_and_registry/write_a_registry_entry.html -->
|
||||
<Component Id="RegistryEntries" Guid="*">
|
||||
<RegistryKey Root="HKCR" Key="edmc">
|
||||
<RegistryValue Type="string" Value="$(var.PRODUCTLONGNAME)"/>
|
||||
<RegistryValue Type="string" Name="URL Protocol" Value=""/>
|
||||
<RegistryKey Key="DefaultIcon">
|
||||
<RegistryValue Type="string" Value="[INSTALLDIR]EDMarketConnector.exe,0"/>
|
||||
</RegistryKey>
|
||||
<RegistryKey Key="shell">
|
||||
<RegistryKey Key="open">
|
||||
<RegistryKey Key="command">
|
||||
<RegistryValue Type="string" Value='"[INSTALLDIR]EDMarketConnector.exe" "%1"'/>
|
||||
</RegistryKey>
|
||||
<RegistryKey Key="ddeexec">
|
||||
<RegistryValue Type="string" Value='Open("%1")'/>
|
||||
</RegistryKey>
|
||||
</RegistryKey>
|
||||
</RegistryKey>
|
||||
</RegistryKey>
|
||||
</Component>
|
||||
|
||||
<!-- Contents auto-generated with heat.exe, see setup.py -->
|
||||
<Directory Id="ProgramFilesFolder">
|
||||
</Directory>
|
||||
|
||||
<Directory Id="ProgramMenuFolder" Name="Programs">
|
||||
</Directory>
|
||||
|
||||
</Directory>
|
||||
|
||||
<!-- Contents auto-generated in setup.py -->
|
||||
<Feature Id='Complete' Level='1'>
|
||||
</Feature>
|
||||
|
||||
</Product>
|
||||
</Wix>
|
||||
|
||||
<!-- Local Variables: -->
|
||||
<!-- tab-width: 4 -->
|
||||
<!-- End: -->
|
Loading…
x
Reference in New Issue
Block a user