1
0
mirror of https://github.com/EDCD/EDMarketConnector.git synced 2025-04-23 04:10:29 +03:00

Merge branch 'stable' into releases

This commit is contained in:
David Sangrey 2024-09-24 19:43:07 -04:00
commit c0bff277e9
No known key found for this signature in database
GPG Key ID: 3AEADBB0186884BC
58 changed files with 1220 additions and 834 deletions

View File

@ -6,6 +6,41 @@ This is the master changelog for Elite Dangerous Market Connector. Entries are
in the source (not distributed with the Windows installer) for the
currently used version.
---
Release 5.12.0
===
This release brings a number of performance enhancements and functionality updates requested by the community to EDMC.
Notably, integration with Inara's SLEF notation, custom plugin directories, streamlined logging locations, and
performance enhancements are included.
This release also fixes a few administrative issues regarding licenses to ensure compliance with included libraries.
**Changes and Enhancements**
* Added the ability to export a ship's loadout to Inara SLEF notation
* Added the ability for EDMC to restart itself if required after settings changes
* Added the ability to change the custom plugins directory to allow for multiple plugin profiles
* Added Basic Type 8 Support
* Updated the default logging directory from $TEMPDIR or %TEMP% and to the current app data directory
* Updated a number of direct win32API calls to use proper prototyped library calls
* Updated a number of translations
* Updated a number of dependencies
* Updated included and bundled licenses to comply with dependency requirements
* Updated the game_running check to be more efficient on Windows to reduce program hangs
* Minor logic enhancements
* Retired most usages of os.path in favor of the preferred PathLib
**Bug Fixes**
* Fixed a bug that would result in Horizons and Odyssey flags not being passed to EDDN
**Plugin Developers**
* nb.Entry is deprecated, and is slated for removal in 6.0 or later. Please migrate to nb.EntryMenu
* nb.ColoredButton is deprecated, and is slated for removal in 6.0 or later. Please migrate to tk.Button
* Calling internal translations with `_()` is deprecated, and is slated for removal in 6.0 or later. Please migrate to importing `translations` and calling `translations.translate` or `translations.tl` directly
* `Translations` as the translate system singleton is deprecated, and is slated for removal in 6.0 or later. Please migrate to the `translations` singleton
* `help_open_log_folder()` is deprecated, and is slated for removal in 6.0 or later. Please migrate to open_folder()
* `update_feed` is deprecated, and is slated for removal in 6.0 or later. Please migrate to `get_update_feed()`.
Release 5.11.3
===
@ -33,7 +68,7 @@ This release fixes a bug where minimizing to the system tray could cause the pro
**Changes and Enhancements**
* Updated Translations
* Added a developer utilty to help speed up changelog development
* Added a developer utility to help speed up changelog development
**Bug Fixes**
* Fixed a bug where minimizing to the system tray could cause the program to not un-minimize.

11
EDMC.py
View File

@ -14,6 +14,7 @@ import locale
import os
import queue
import sys
from pathlib import Path
from time import sleep, time
from typing import TYPE_CHECKING, Any
@ -212,22 +213,24 @@ def main(): # noqa: C901, CCR001
# system, chances are its the current locale, and not utf-8. Otherwise if it was copied, its probably
# utf8. Either way, try the system FIRST because reading something like cp1251 in UTF-8 results in garbage
# but the reverse results in an exception.
json_file = os.path.abspath(args.j)
json_file = Path(args.j).resolve()
try:
with open(json_file) as file_handle:
data = json.load(file_handle)
except UnicodeDecodeError:
with open(json_file, encoding='utf-8') as file_handle:
data = json.load(file_handle)
config.set('querytime', int(os.path.getmtime(args.j)))
file_path = Path(args.j)
modification_time = file_path.stat().st_mtime
config.set('querytime', int(modification_time))
else:
# Get state from latest Journal file
logger.debug('Getting state from latest journal file')
try:
monitor.currentdir = config.get_str('journaldir', default=config.default_journal_dir)
monitor.currentdir = Path(config.get_str('journaldir', default=config.default_journal_dir))
if not monitor.currentdir:
monitor.currentdir = config.default_journal_dir
monitor.currentdir = config.default_journal_dir_path
logger.debug(f'logdir = "{monitor.currentdir}"')
logfile = monitor.journal_newest_filename(monitor.currentdir)

View File

@ -26,12 +26,13 @@ To utilise logging in core code, or internal plugins, include this:
To utilise logging in a 'found' (third-party) plugin, include this:
import os
from pathlib import Path
import logging
plugin_name = os.path.basename(os.path.dirname(__file__))
# Retrieve the name of the plugin folder
plugin_name = Path(__file__).resolve().parent.name
# Set up logger with hierarchical name including appname and plugin_name
# plugin_name here *must* be the name of the folder the plugin resides in
# See, plug.py:load_plugins()
logger = logging.getLogger(f'{appname}.{plugin_name}')
"""
from __future__ import annotations
@ -41,7 +42,6 @@ import logging
import logging.handlers
import os
import pathlib
import tempfile
import warnings
from contextlib import suppress
from fnmatch import fnmatch
@ -51,9 +51,7 @@ from threading import get_native_id as thread_native_id
from time import gmtime
from traceback import print_exc
from typing import TYPE_CHECKING, cast
import config as config_mod
from config import appcmdname, appname, config
from config import appcmdname, appname, config, trace_on
# TODO: Tests:
#
@ -104,7 +102,7 @@ warnings.simplefilter('default', DeprecationWarning)
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):
if any(fnmatch(condition, p) for p in trace_on):
self._log(logging.TRACE, message, args, **kwargs) # type: ignore # we added it
return
@ -184,8 +182,7 @@ class Logger:
# We want the files in %TEMP%\{appname}\ as {logger_name}-debug.log and
# rotated versions.
# This is {logger_name} so that EDMC.py logs to a different file.
logfile_rotating = pathlib.Path(tempfile.gettempdir())
logfile_rotating /= f'{appname}'
logfile_rotating = pathlib.Path(config.app_dir_path / 'logs')
logfile_rotating.mkdir(exist_ok=True)
logfile_rotating /= f'{logger_name}-debug.log'
@ -489,8 +486,8 @@ class EDMCContextFilter(logging.Filter):
:return: The munged module_name.
"""
file_name = pathlib.Path(frame_info.filename).expanduser()
plugin_dir = pathlib.Path(config.plugin_dir_path).expanduser()
internal_plugin_dir = pathlib.Path(config.internal_plugin_dir_path).expanduser()
plugin_dir = config.plugin_dir_path.expanduser()
internal_plugin_dir = config.internal_plugin_dir_path.expanduser()
# Find the first parent called 'plugins'
plugin_top = file_name
while plugin_top and plugin_top.name != '':

View File

@ -11,7 +11,7 @@ import locale
import webbrowser
import platform
import sys
from os import chdir, environ, path
from os import chdir, environ
import pathlib
import logging
from journal_lock import JournalLock
@ -19,10 +19,10 @@ from journal_lock import JournalLock
if getattr(sys, "frozen", False):
# Under py2exe sys.path[0] is the executable name
if sys.platform == "win32":
chdir(path.dirname(sys.path[0]))
chdir(pathlib.Path(sys.path[0]).parent)
# Allow executable to be invoked from any cwd
environ["TCL_LIBRARY"] = path.join(path.dirname(sys.path[0]), "lib", "tcl")
environ["TK_LIBRARY"] = path.join(path.dirname(sys.path[0]), "lib", "tk")
environ['TCL_LIBRARY'] = str(pathlib.Path(sys.path[0]).parent / 'lib' / 'tcl')
environ['TK_LIBRARY'] = str(pathlib.Path(sys.path[0]).parent / 'lib' / 'tk')
else:
# We still want to *try* to have CWD be where the main script is, even if
@ -44,11 +44,12 @@ def get_sys_report(config: config.AbstractConfig) -> str:
plt = platform.uname()
locale.setlocale(locale.LC_ALL, "")
lcl = locale.getlocale()
monitor.currentdir = config.get_str(
monitor.currentdir = pathlib.Path(config.get_str(
"journaldir", default=config.default_journal_dir
)
)
if not monitor.currentdir:
monitor.currentdir = config.default_journal_dir
monitor.currentdir = config.default_journal_dir_path
try:
logfile = monitor.journal_newest_filename(monitor.currentdir)
if logfile is None:
@ -115,12 +116,12 @@ def main() -> None:
root.withdraw() # Hide the window initially to calculate the dimensions
try:
icon_image = tk.PhotoImage(
file=path.join(cur_config.respath_path, "io.edcd.EDMarketConnector.png")
file=cur_config.respath_path / "io.edcd.EDMarketConnector.png"
)
root.iconphoto(True, icon_image)
except tk.TclError:
root.iconbitmap(path.join(cur_config.respath_path, "EDMarketConnector.ico"))
root.iconbitmap(cur_config.respath_path / "EDMarketConnector.ico")
sys_report = get_sys_report(cur_config)

View File

@ -20,8 +20,7 @@ import subprocess
import sys
import threading
import webbrowser
import tempfile
from os import chdir, environ, path
from os import chdir, environ
from time import localtime, strftime, time
from typing import TYPE_CHECKING, Any, Literal
from constants import applongname, appname, protocolhandler_redirect
@ -32,26 +31,29 @@ from constants import applongname, appname, protocolhandler_redirect
if getattr(sys, 'frozen', False):
# Under py2exe sys.path[0] is the executable name
if sys.platform == 'win32':
chdir(path.dirname(sys.path[0]))
os.chdir(pathlib.Path(sys.path[0]).parent)
# Allow executable to be invoked from any cwd
environ['TCL_LIBRARY'] = path.join(path.dirname(sys.path[0]), 'lib', 'tcl')
environ['TK_LIBRARY'] = path.join(path.dirname(sys.path[0]), 'lib', 'tk')
environ['TCL_LIBRARY'] = str(pathlib.Path(sys.path[0]).parent / 'lib' / 'tcl')
environ['TK_LIBRARY'] = str(pathlib.Path(sys.path[0]).parent / 'lib' / 'tk')
else:
# We still want to *try* to have CWD be where the main script is, even if
# not frozen.
chdir(pathlib.Path(__file__).parent)
# config will now cause an appname logger to be set up, so we need the
# console redirect before this
if __name__ == '__main__':
# Keep this as the very first code run to be as sure as possible of no
# output until after this redirect is done, if needed.
if getattr(sys, 'frozen', False):
from config import config
# By default py2exe tries to write log to dirname(sys.executable) which fails when installed
# unbuffered not allowed for text in python3, so use `1 for line buffering
log_file_path = path.join(tempfile.gettempdir(), f'{appname}.log')
log_file_path = pathlib.Path(config.app_dir_path / 'logs')
log_file_path.mkdir(exist_ok=True)
log_file_path /= f'{appname}.log'
sys.stdout = sys.stderr = open(log_file_path, mode='wt', buffering=1) # Do NOT use WITH here.
# TODO: Test: Make *sure* this redirect is working, else py2exe is going to cause an exit popup
@ -253,24 +255,16 @@ if __name__ == '__main__': # noqa: C901
logger.trace_if('frontier-auth.windows', 'Begin...')
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
if locked != JournalLockResult.LOCKED:
from ctypes import windll, c_int, create_unicode_buffer, WINFUNCTYPE
from ctypes.wintypes import BOOL, HWND, INT, LPARAM, LPCWSTR, LPWSTR
from ctypes import windll, create_unicode_buffer, WINFUNCTYPE
from ctypes.wintypes import BOOL, HWND, LPARAM
import win32gui
import win32api
import win32con
EnumWindows = windll.user32.EnumWindows # noqa: N806
GetClassName = windll.user32.GetClassNameW # noqa: N806
GetClassName.argtypes = [HWND, LPWSTR, c_int]
GetWindowText = windll.user32.GetWindowTextW # noqa: N806
GetWindowText.argtypes = [HWND, LPWSTR, c_int]
GetWindowTextLength = windll.user32.GetWindowTextLengthW # noqa: N806
GetProcessHandleFromHwnd = windll.oleacc.GetProcessHandleFromHwnd # noqa: N806
SW_RESTORE = 9 # noqa: N806
SetForegroundWindow = windll.user32.SetForegroundWindow # noqa: N806
ShowWindow = windll.user32.ShowWindow # noqa: N806
ShowWindowAsync = windll.user32.ShowWindowAsync # noqa: N806
COINIT_MULTITHREADED = 0 # noqa: N806,F841
@ -278,16 +272,9 @@ if __name__ == '__main__': # noqa: C901
COINIT_DISABLE_OLE1DDE = 0x4 # noqa: N806
CoInitializeEx = windll.ole32.CoInitializeEx # noqa: N806
ShellExecute = windll.shell32.ShellExecuteW # noqa: N806
ShellExecute.argtypes = [HWND, LPCWSTR, LPCWSTR, LPCWSTR, LPCWSTR, INT]
def window_title(h: int) -> str | None:
if h:
text_length = GetWindowTextLength(h) + 1
buf = create_unicode_buffer(text_length)
if GetWindowText(h, buf, text_length):
return buf.value
return win32gui.GetWindowText(h)
return None
@WINFUNCTYPE(BOOL, HWND, LPARAM)
@ -309,7 +296,7 @@ if __name__ == '__main__': # noqa: C901
# class name limited to 256 - https://msdn.microsoft.com/en-us/library/windows/desktop/ms633576
cls = create_unicode_buffer(257)
# This conditional is exploded to make debugging slightly easier
if GetClassName(window_handle, cls, 257):
if win32gui.GetClassName(window_handle):
if cls.value == 'TkTopLevel':
if window_title(window_handle) == applongname:
if GetProcessHandleFromHwnd(window_handle):
@ -317,12 +304,12 @@ if __name__ == '__main__': # noqa: C901
if len(sys.argv) > 1 and sys.argv[1].startswith(protocolhandler_redirect):
CoInitializeEx(0, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE)
# Wait for it to be responsive to avoid ShellExecute recursing
ShowWindow(window_handle, SW_RESTORE)
ShellExecute(0, None, sys.argv[1], None, None, SW_RESTORE)
win32gui.ShowWindow(window_handle, win32con.SW_RESTORE)
win32api.ShellExecute(0, None, sys.argv[1], None, None, win32con.SW_RESTORE)
else:
ShowWindowAsync(window_handle, SW_RESTORE)
SetForegroundWindow(window_handle)
ShowWindowAsync(window_handle, win32con.SW_RESTORE)
win32gui.SetForegroundWindow(window_handle)
return False # Indicate window found, so stop iterating
@ -334,7 +321,7 @@ if __name__ == '__main__': # noqa: C901
# enumwindwsproc() on each. When an invocation returns False it
# stops iterating.
# Ref: <https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-enumwindows>
EnumWindows(enumwindowsproc, 0)
win32gui.EnumWindows(enumwindowsproc, 0)
def already_running_popup():
"""Create the "already running" popup."""
@ -468,8 +455,8 @@ class AppWindow:
self.w.wm_iconbitmap(default='EDMarketConnector.ico')
else:
self.w.tk.call('wm', 'iconphoto', self.w, '-default',
tk.PhotoImage(file=path.join(config.respath_path, 'io.edcd.EDMarketConnector.png')))
image_path = config.respath_path / 'io.edcd.EDMarketConnector.png'
self.w.tk.call('wm', 'iconphoto', self.w, '-default', tk.PhotoImage(file=image_path))
# TODO: Export to files and merge from them in future ?
self.theme_icon = tk.PhotoImage(
@ -619,7 +606,7 @@ class AppWindow:
self.help_menu.add_command(command=lambda: self.updater.check_for_updates()) # Check for Updates...
# About E:D Market Connector
self.help_menu.add_command(command=lambda: not self.HelpAbout.showing and self.HelpAbout(self.w))
logfile_loc = pathlib.Path(tempfile.gettempdir()) / appname
logfile_loc = pathlib.Path(config.app_dir_path / 'logs')
self.help_menu.add_command(command=lambda: prefs.open_folder(logfile_loc)) # Open Log Folder
self.help_menu.add_command(command=lambda: prefs.help_open_system_profiler(self)) # Open Log Folde
@ -701,13 +688,14 @@ class AppWindow:
if match:
if sys.platform == 'win32':
# Check that the titlebar will be at least partly on screen
import ctypes
from ctypes.wintypes import POINT
import win32api
import win32con
x = int(match.group(1)) + 16
y = int(match.group(2)) + 16
point = (x, y)
# https://msdn.microsoft.com/en-us/library/dd145064
MONITOR_DEFAULTTONULL = 0 # noqa: N806
if ctypes.windll.user32.MonitorFromPoint(POINT(int(match.group(1)) + 16, int(match.group(2)) + 16),
MONITOR_DEFAULTTONULL):
if win32api.MonitorFromPoint(point, win32con.MONITOR_DEFAULTTONULL):
self.w.geometry(config.get_str('geometry'))
else:
self.w.geometry(config.get_str('geometry'))
@ -845,9 +833,20 @@ class AppWindow:
)
update_msg = update_msg.replace('\\n', '\n')
update_msg = update_msg.replace('\\r', '\r')
stable_popup = tk.messagebox.askyesno(title=title, message=update_msg, parent=postargs.get('Parent'))
stable_popup = tk.messagebox.askyesno(title=title, message=update_msg)
if stable_popup:
webbrowser.open("https://github.com/edCD/eDMarketConnector/releases/latest")
webbrowser.open("https://github.com/EDCD/eDMarketConnector/releases/latest")
if postargs.get('Restart_Req'):
# LANG: Text of Notification Popup for EDMC Restart
restart_msg = tr.tl('A restart of EDMC is required. EDMC will now restart.')
restart_box = tk.messagebox.Message(
title=tr.tl('Restart Required'), # LANG: Title of Notification Popup for EDMC Restart
message=restart_msg,
type=tk.messagebox.OK
)
restart_box.show()
if restart_box:
app.onexit(restart=True)
def set_labels(self):
"""Set main window labels, e.g. after language change."""
@ -1639,7 +1638,7 @@ class AppWindow:
# Avoid file length limits if possible
provider = config.get_str('shipyard_provider', default='EDSY')
target = plug.invoke(provider, 'EDSY', 'shipyard_url', loadout, monitor.is_beta)
file_name = path.join(config.app_dir_path, "last_shipyard.html")
file_name = config.app_dir_path / "last_shipyard.html"
with open(file_name, 'w') as f:
f.write(SHIPYARD_HTML_TEMPLATE.format(
@ -1851,7 +1850,7 @@ class AppWindow:
)
exit_thread.start()
def onexit(self, event=None) -> None:
def onexit(self, event=None, restart: bool = False) -> None:
"""Application shutdown procedure."""
if sys.platform == 'win32':
shutdown_thread = threading.Thread(
@ -1914,6 +1913,8 @@ class AppWindow:
self.w.destroy()
logger.info('Done.')
if restart:
os.execv(sys.executable, ['python'] + sys.argv)
def drag_start(self, event) -> None:
"""Initiate dragging the window."""

View File

@ -346,9 +346,6 @@
/* prefs.py: Label for location of third-party plugins folder; In files: prefs.py:908; */
"Plugins folder" = "Složka pluginů";
/* prefs.py: Label on button used to open a filesystem folder; In files: prefs.py:915; */
"Open" = "Otevřít";
/* prefs.py: Tip/label about how to disable plugins; In files: prefs.py:923; */
"Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name" = "Tip: Plugin můžete vypnout přidáním{CR}'{EXT}' do názvu jeho složky";

View File

@ -1,15 +1,3 @@
/* prefs.py: Catch & Record Profiler Errors; */
"Error in System Profiler" = "Fehler im System Profiler";
/* EDMarketConnector.py: We didn't have the Fleet Carrier callsign when we should have; In files: EDMarketConnector.py:1223; */
"CAPI: Fleet Carrier data incomplete" = "CAPI: Carrier-Daten unvollständig";
/* EDMarketConnector.py: No data was returned for the Fleet Carrier from the Frontier CAPI; In files: EDMarketConnector.py:1219; */
"CAPI: No Fleet Carrier data returned" = "CAPI: Keine Carrier-Daten erhalten";
/* prefs.py: Configuration - Enable or disable the Fleet Carrier CAPI calls; In files: prefs.py:475; */
"Enable Fleet Carrier CAPI Queries" = "Carrier-CAPI-Anfragen aktivieren";
/* edsm.py:Settings>EDSM - Label on checkbox for 'send data'; In files: edsm.py:316; */
"Send flight log and CMDR status to EDSM" = "Sende Fluglog und CMDR-Status an EDSM";
@ -189,12 +177,12 @@
/* EDMarketConnector.py: CAPI fleetcarrier query aborted because of killswitch; In files: EDMarketConnector.py:1157; */
"CAPI fleetcarrier disabled by killswitch" = "CAPI Fleet Carrier durch Killswitch deaktiviert";
/* EDMarketConnector.py: No data was returned for the Fleet Carrier from the Frontier CAPI; In files: EDMarketConnector.py:1219; */
"CAPI: No Fleet Carrier data returned" = "CAPI: Keine Carrier-Daten erhalten";
/* EDMarketConnector.py: No data was returned for the fleetcarrier from the Frontier CAPI; In files: EDMarketConnector.py:1219; */
"CAPI: No fleetcarrier data returned" = "CAPI: Keine Fleet Carrier-Daten erhalten";
/* EDMarketConnector.py: We didn't have the Fleet Carrier callsign when we should have; In files: EDMarketConnector.py:1223; */
"CAPI: Fleet Carrier data incomplete" = "CAPI: Carrier-Daten unvollständig";
/* EDMarketConnector.py: We didn't have the fleetcarrier callsign when we should have; In files: EDMarketConnector.py:1223; */
"CAPI: Fleetcarrier data incomplete" = "CAPI: Fleet Carrier-Daten unvollständig";
/* EDMarketConnector.py: No data was returned for the commander from the Frontier CAPI; In files: EDMarketConnector.py:1242; */
"CAPI: No commander data returned" = "CAPI: Keine Kommandanten-Daten erhalten";
@ -399,9 +387,9 @@
/* prefs.py: Settings > Configuration - Label for CAPI section; In files: prefs.py:469; */
"CAPI Settings" = "CAPI-Einstellungen";
/* prefs.py: Configuration - Enable or disable the Fleet Carrier CAPI calls; In files: prefs.py:475; */
"Enable Fleetcarrier CAPI Queries" = "Fleet Carrier CAPI-Anfragen aktivieren";
"Enable Fleet Carrier CAPI Queries" = "Carrier-CAPI-Anfragen aktivieren";
/* prefs.py: Hotkey/Shortcut settings prompt on Windows; In files: prefs.py:492; */
"Hotkey" = "Hotkey";
@ -480,9 +468,6 @@
/* prefs.py: Label for location of third-party plugins folder; In files: prefs.py:908; */
"Plugins folder" = "Plugin-Ordner";
/* prefs.py: Label on button used to open a filesystem folder; In files: prefs.py:915; */
"Open" = "Öffnen";
/* prefs.py: Tip/label about how to disable plugins; In files: prefs.py:923; */
"Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name" = "Tipp: Du kannst ein Plugin deaktivieren, indem{CR}du '{EXT}' zu seinem Ordnernamen hinzufügst";
@ -501,6 +486,9 @@
/* prefs.py: Lable on list of user-disabled plugins; In files: prefs.py:977; */
"Disabled Plugins" = "Deaktivierte Plugins";
/* prefs.py: Catch & Record Profiler Errors; */
"Error in System Profiler" = "Fehler im System Profiler";
/* stats.py: Cmdr stats; In files: stats.py:58; */
"Balance" = "Kontostand";
@ -792,7 +780,6 @@
/* EDMarketConnector.py: Inform the user the Update Track has changed; */
"Update Track Changed to {TRACK}" = "Update-Kanal geändert auf {TRACK}";
/* EDMarketConnector.py: Inform User of Beta -> Stable Transition Risks; */
"Update track changed to Stable from Beta. You will no longer receive Beta updates. You will stay on your current Beta version until the next Stable release.\r\n\r\nYou can manually revert to the latest Stable version. To do so, you must download and install the latest Stable version manually. Note that this may introduce bugs or break completely if downgrading between major versions with significant changes.\r\n\r\nDo you want to open GitHub to download the latest release?" = "Update-Kanal von Beta auf Stabil geändert. Du wirst keine weiteren Beta-Updates erhalten. Du wirst bis zum nächsten stabilen Release auf der aktuellen Beta-Version bleiben.\n\nDu kannst manuell zur aktuellen stabilen Version zurückkehren. Um dies zu tun, musst du die aktuelle stabile Version manuell herunterladen und installieren. Beachte, dass dies Fehler verursachen oder gar nicht funktionieren könnte, wenn du von einem größeren Versionssprung mit signifikanten Änderungen downgradest.\n\nMöchtest du GitHub öffnen, um den neuesten Release herunterzuladen?";
@ -801,3 +788,4 @@
/* ttkHyperlinkLabel.py: Open Element In Selected Provider; */
"Open in {URL}" = "Öffnen in {URL}";

View File

@ -468,8 +468,11 @@
/* prefs.py: Label for location of third-party plugins folder; In files: prefs.py:908; */
"Plugins folder" = "Plugins folder";
/* prefs.py: Label on button used to open a filesystem folder; In files: prefs.py:915; */
"Open" = "Open";
/* prefs.py: Label on button used to open the Plugin Folder; */
"Open Plugins Folder" = "Open Plugins Folder";
/* prefs.py: Selecting the Location of the Plugin Directory on the Filesystem; */
"Plugin Directory Location" = "Plugin Directory Location";
/* prefs.py: Tip/label about how to disable plugins; In files: prefs.py:923; */
"Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name" = "Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name";
@ -804,12 +807,20 @@
/* EDMarketConnector.py: Inform the user the Update Track has changed; */
"Update Track Changed to {TRACK}" = "Update Track Changed to {TRACK}";
/* EDMarketConnector.py: Inform User of Beta -> Stable Transition Risks; */
"Update track changed to Stable from Beta. You will no longer receive Beta updates. You will stay on your current Beta version until the next Stable release.\r\n\r\nYou can manually revert to the latest Stable version. To do so, you must download and install the latest Stable version manually. Note that this may introduce bugs or break completely if downgrading between major versions with significant changes.\r\n\r\nDo you want to open GitHub to download the latest release?" = "Update track changed to Stable from Beta. You will no longer receive Beta updates. You will stay on your current Beta version until the next Stable release.\r\n\r\nYou can manually revert to the latest Stable version. To do so, you must download and install the latest Stable version manually. Note that this may introduce bugs or break completely if downgrading between major versions with significant changes.\r\n\r\nDo you want to open GitHub to download the latest release?";
/* EDMarketConnector.py: Title of Notification Popup for EDMC Restart; */
"Restart Required" = "Restart Required";
/* EDMarketConnector.py: Text of Notification Popup for EDMC Restart; */
"A restart of EDMC is required. EDMC will now restart." = "A restart of EDMC is required. EDMC will now restart.";
/* myNotebook.py: Can't Paste Images or Files in Text; */
"Cannot paste non-text content." = "Cannot paste non-text content.";
/* ttkHyperlinkLabel.py: Open Element In Selected Provider; */
"Open in {URL}" = "Open in {URL}";
/* ttkHyperlinkLabel.py: Copy the Inara SLEF Format of the active ship to the clipboard; */
"Copy Inara SLEF" = "Copy Inara SLEF";

View File

@ -361,9 +361,6 @@
/* prefs.py: Label for location of third-party plugins folder; In files: prefs.py:908; */
"Plugins folder" = "Carpeta de Plugins";
/* prefs.py: Label on button used to open a filesystem folder; In files: prefs.py:915; */
"Open" = "Abrir";
/* prefs.py: Tip/label about how to disable plugins; In files: prefs.py:923; */
"Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name" = "Puedes desactivar un plugin añadiendo{CR}'{EXT}' al nombre de su carpeta";

View File

@ -319,9 +319,6 @@
/* prefs.py: Label for location of third-party plugins folder; In files: prefs.py:908; */
"Plugins folder" = "Lisäosien hakemistosijainti";
/* prefs.py: Label on button used to open a filesystem folder; In files: prefs.py:915; */
"Open" = "Avaa";
/* prefs.py: Tip/label about how to disable plugins; In files: prefs.py:923; */
"Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name" = "Vihje: Voit poistaa lisäosan käytöstä{CR}lisäämällä '{EXT}' sen hakemistonimeen";

View File

@ -291,9 +291,6 @@
/* prefs.py: Label for location of third-party plugins folder; In files: prefs.py:908; */
"Plugins folder" = "Répertoire des plugins";
/* prefs.py: Label on button used to open a filesystem folder; In files: prefs.py:915; */
"Open" = "Ouvrir";
/* prefs.py: Tip/label about how to disable plugins; In files: prefs.py:923; */
"Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name" = "Astuce : Vous pouvez désactiver un plugin en{CR}ajoutant '{EXT}' à son nom de répertoire";

View File

@ -235,9 +235,6 @@
/* prefs.py: Label for location of third-party plugins folder; In files: prefs.py:908; */
"Plugins folder" = "Plugin mappa";
/* prefs.py: Label on button used to open a filesystem folder; In files: prefs.py:915; */
"Open" = "Nyisd ki";
/* prefs.py: Tip/label about how to disable plugins; In files: prefs.py:923; */
"Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name" = "Tipp:a pluginokat kikapcsolhatod hozzà{CR}adsz a mappa nevében egy '{EXT}'";

View File

@ -1,15 +1,3 @@
/* prefs.py: Catch & Record Profiler Errors; */
"Error in System Profiler" = "Errore nel Sistema di Profilazione";
/* EDMarketConnector.py: We didn't have the Fleet Carrier callsign when we should have; In files: EDMarketConnector.py:1223; */
"CAPI: Fleet Carrier data incomplete" = "CAPI: dati della Fleet Carrier incompleti";
/* EDMarketConnector.py: No data was returned for the Fleet Carrier from the Frontier CAPI; In files: EDMarketConnector.py:1219; */
"CAPI: No Fleet Carrier data returned" = "CAPI: nessun dato della Fleet Carrier ottenuto";
/* prefs.py: Configuration - Enable or disable the Fleet Carrier CAPI calls; In files: prefs.py:475; */
"Enable Fleet Carrier CAPI Queries" = "Abilita le query CAPI per le Fleet Carrier";
/* edsm.py:Settings>EDSM - Label on checkbox for 'send data'; In files: edsm.py:316; */
"Send flight log and CMDR status to EDSM" = "Invia il registro di volo e lo stato del CMDR a EDSM";
@ -189,12 +177,12 @@
/* EDMarketConnector.py: CAPI fleetcarrier query aborted because of killswitch; In files: EDMarketConnector.py:1157; */
"CAPI fleetcarrier disabled by killswitch" = "Fleetcarrier CAPI disabilitato dal killswitch";
/* EDMarketConnector.py: No data was returned for the Fleet Carrier from the Frontier CAPI; In files: EDMarketConnector.py:1219; */
"CAPI: No Fleet Carrier data returned" = "CAPI: nessun dato della Fleet Carrier ottenuto";
/* EDMarketConnector.py: No data was returned for the fleetcarrier from the Frontier CAPI; In files: EDMarketConnector.py:1219; */
"CAPI: No fleetcarrier data returned" = "CAPI: Nessun dato della fleetcarrier ricevuto";
/* EDMarketConnector.py: We didn't have the Fleet Carrier callsign when we should have; In files: EDMarketConnector.py:1223; */
"CAPI: Fleet Carrier data incomplete" = "CAPI: dati della Fleet Carrier incompleti";
/* EDMarketConnector.py: We didn't have the fleetcarrier callsign when we should have; In files: EDMarketConnector.py:1223; */
"CAPI: Fleetcarrier data incomplete" = "CAPI: Dati della Fleetcarrier incompleti";
/* EDMarketConnector.py: No data was returned for the commander from the Frontier CAPI; In files: EDMarketConnector.py:1242; */
"CAPI: No commander data returned" = "CAPI: Nessun dato sul commandante";
@ -399,9 +387,9 @@
/* prefs.py: Settings > Configuration - Label for CAPI section; In files: prefs.py:469; */
"CAPI Settings" = "Impostazioni CAPI";
/* prefs.py: Configuration - Enable or disable the Fleet Carrier CAPI calls; In files: prefs.py:475; */
"Enable Fleetcarrier CAPI Queries" = "Abilita le query CAPI della Fleetcarrier";
"Enable Fleet Carrier CAPI Queries" = "Abilita le query CAPI per le Fleet Carrier";
/* prefs.py: Hotkey/Shortcut settings prompt on Windows; In files: prefs.py:492; */
"Hotkey" = "Hotkey";
@ -480,8 +468,11 @@
/* prefs.py: Label for location of third-party plugins folder; In files: prefs.py:908; */
"Plugins folder" = "Cartella dei plugins";
/* prefs.py: Label on button used to open a filesystem folder; In files: prefs.py:915; */
"Open" = "Sfoglia";
/* prefs.py: Label on button used to open the Plugin Folder; */
"Open Plugins Folder" = "Apri la cartella dei plugin";
/* prefs.py: Selecting the Location of the Plugin Directory on the Filesystem; */
"Plugin Directory Location" = "Posizione della Cartella del Plugin";
/* prefs.py: Tip/label about how to disable plugins; In files: prefs.py:923; */
"Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name" = "Suggerimento: è possibile disabilitare un plugin {CR}aggiungendo '{EXT}' nel nome della sua cartella";
@ -501,6 +492,9 @@
/* prefs.py: Lable on list of user-disabled plugins; In files: prefs.py:977; */
"Disabled Plugins" = "Plugin disabilitati";
/* prefs.py: Catch & Record Profiler Errors; */
"Error in System Profiler" = "Errore nel Sistema di Profilazione";
/* stats.py: Cmdr stats; In files: stats.py:58; */
"Balance" = "Saldo";
@ -813,12 +807,20 @@
/* EDMarketConnector.py: Inform the user the Update Track has changed; */
"Update Track Changed to {TRACK}" = "Canale di aggiornamento cambiato in {TRACK}";
/* EDMarketConnector.py: Inform User of Beta -> Stable Transition Risks; */
"Update track changed to Stable from Beta. You will no longer receive Beta updates. You will stay on your current Beta version until the next Stable release.\r\n\r\nYou can manually revert to the latest Stable version. To do so, you must download and install the latest Stable version manually. Note that this may introduce bugs or break completely if downgrading between major versions with significant changes.\r\n\r\nDo you want to open GitHub to download the latest release?" = "Il canale di aggiornamento è cambiato da Beta a Stabile. Non riceverai più aggiornamenti Beta. Rimarrai sulla tua attuale versione Beta fino alla prossima release Stabile.\n\nPuoi tornare manualmente all'ultima versione Stabile. Per farlo, devi scaricare e installare manualmente l'ultima versione Stabile. Nota che questo potrebbe introdurre bug o causare malfunzionamenti se si esegue il downgrade tra versioni principali con cambiamenti significativi.\n\nVuoi aprire GitHub per scaricare l'ultima release?";
/* EDMarketConnector.py: Title of Notification Popup for EDMC Restart; */
"Restart Required" = "Riavvio necessario";
/* EDMarketConnector.py: Text of Notification Popup for EDMC Restart; */
"A restart of EDMC is required. EDMC will now restart." = "È necessario riavviare EDMC. EDMC si riavvierà ora.";
/* myNotebook.py: Can't Paste Images or Files in Text; */
"Cannot paste non-text content." = "Non si può incollare contenuto non testuale";
/* ttkHyperlinkLabel.py: Open Element In Selected Provider; */
"Open in {URL}" = "Apri {URL}";
/* ttkHyperlinkLabel.py: Copy the Inara SLEF Format of the active ship to the clipboard; */
"Copy Inara SLEF" = "Copia Inara SLEF";

View File

@ -1,15 +1,3 @@
/* prefs.py: Catch & Record Profiler Errors; */
"Error in System Profiler" = "システムプロファイラーでエラー";
/* EDMarketConnector.py: We didn't have the Fleet Carrier callsign when we should have; In files: EDMarketConnector.py:1223; */
"CAPI: Fleet Carrier data incomplete" = "CAPI: フリートキャリアデータが不完全";
/* EDMarketConnector.py: No data was returned for the Fleet Carrier from the Frontier CAPI; In files: EDMarketConnector.py:1219; */
"CAPI: No Fleet Carrier data returned" = "CAPI: フリートキャリアデータの返信なし";
/* prefs.py: Configuration - Enable or disable the Fleet Carrier CAPI calls; In files: prefs.py:475; */
"Enable Fleet Carrier CAPI Queries" = "フリートキャリアCAPIクエリを有効にする";
/* edsm.py:Settings>EDSM - Label on checkbox for 'send data'; In files: edsm.py:316; */
"Send flight log and CMDR status to EDSM" = "フライトログとCMDRステータスをEDSMへ送信する";
@ -189,12 +177,12 @@
/* EDMarketConnector.py: CAPI fleetcarrier query aborted because of killswitch; In files: EDMarketConnector.py:1157; */
"CAPI fleetcarrier disabled by killswitch" = "CAPIフリートキャリアはkillswitchによって無効にされています";
/* EDMarketConnector.py: No data was returned for the Fleet Carrier from the Frontier CAPI; In files: EDMarketConnector.py:1219; */
"CAPI: No Fleet Carrier data returned" = "CAPI: フリートキャリアデータの返信なし";
/* EDMarketConnector.py: No data was returned for the fleetcarrier from the Frontier CAPI; In files: EDMarketConnector.py:1219; */
"CAPI: No fleetcarrier data returned" = "CAPI: フリートキャリアデータの返信なし";
/* EDMarketConnector.py: We didn't have the Fleet Carrier callsign when we should have; In files: EDMarketConnector.py:1223; */
"CAPI: Fleet Carrier data incomplete" = "CAPI: フリートキャリアデータが不完全";
/* EDMarketConnector.py: We didn't have the fleetcarrier callsign when we should have; In files: EDMarketConnector.py:1223; */
"CAPI: Fleetcarrier data incomplete" = "CAPI: フリートキャリアデータが不完全";
/* EDMarketConnector.py: No data was returned for the commander from the Frontier CAPI; In files: EDMarketConnector.py:1242; */
"CAPI: No commander data returned" = "CAPI: コマンダーのデータが返ってきませんでした";
@ -399,9 +387,9 @@
/* prefs.py: Settings > Configuration - Label for CAPI section; In files: prefs.py:469; */
"CAPI Settings" = "CAPI設定";
/* prefs.py: Configuration - Enable or disable the Fleet Carrier CAPI calls; In files: prefs.py:475; */
"Enable Fleetcarrier CAPI Queries" = "フリートキャリアCAPIクエリを有効にする";
"Enable Fleet Carrier CAPI Queries" = "フリートキャリアCAPIクエリを有効にする";
/* prefs.py: Hotkey/Shortcut settings prompt on Windows; In files: prefs.py:492; */
"Hotkey" = "ホットキー";
@ -480,9 +468,6 @@
/* prefs.py: Label for location of third-party plugins folder; In files: prefs.py:908; */
"Plugins folder" = "プラグインフォルダ";
/* prefs.py: Label on button used to open a filesystem folder; In files: prefs.py:915; */
"Open" = "開く";
/* prefs.py: Tip/label about how to disable plugins; In files: prefs.py:923; */
"Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name" = "ヒント: プラグインを無効にするには{CR}フォルダ名に '{EXT}' を追加してください";
@ -501,6 +486,9 @@
/* prefs.py: Lable on list of user-disabled plugins; In files: prefs.py:977; */
"Disabled Plugins" = "無効なプラグイン";
/* prefs.py: Catch & Record Profiler Errors; */
"Error in System Profiler" = "システムプロファイラーでエラー";
/* stats.py: Cmdr stats; In files: stats.py:58; */
"Balance" = "口座残高";
@ -813,7 +801,6 @@
/* EDMarketConnector.py: Inform the user the Update Track has changed; */
"Update Track Changed to {TRACK}" = "アップデートトラックを {TRACK} に変更します";
/* EDMarketConnector.py: Inform User of Beta -> Stable Transition Risks; */
"Update track changed to Stable from Beta. You will no longer receive Beta updates. You will stay on your current Beta version until the next Stable release.\r\n\r\nYou can manually revert to the latest Stable version. To do so, you must download and install the latest Stable version manually. Note that this may introduce bugs or break completely if downgrading between major versions with significant changes.\r\n\r\nDo you want to open GitHub to download the latest release?" = "アップデートトラックをベータから安定版に変更しました。今後ベータ版のアップデートを受け取ることはありません。次の安定版がリリースされるまでこのベータ版をご利用いただけます。\n\n手動で最新の安定版に戻すことができます。そのためには、最新の安定版を手動でダウンロードしてインストールする必要があります。大きな変更を伴うメジャーバージョン間のダウングレードの場合、バグが発生したり、完全に壊れたりする可能性があることに注意してください。\n\nGitHubを開いて最新リリースをダウンロードしますか";
@ -822,3 +809,4 @@
/* ttkHyperlinkLabel.py: Open Element In Selected Provider; */
"Open in {URL}" = "{URL} で開く";

View File

@ -388,9 +388,6 @@
/* prefs.py: Label for location of third-party plugins folder; In files: prefs.py:908; */
"Plugins folder" = "플러그인 폴더";
/* prefs.py: Label on button used to open a filesystem folder; In files: prefs.py:915; */
"Open" = "열기";
/* prefs.py: Tip/label about how to disable plugins; In files: prefs.py:923; */
"Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name" = "팁: 플러그인을 비활성화하려면{CR} '{EXT}'를 폴더명 뒤에 추가하면 됨";

View File

@ -220,9 +220,6 @@
/* prefs.py: Label for location of third-party plugins folder; In files: prefs.py:908; */
"Plugins folder" = "Spraudņu mape";
/* prefs.py: Label on button used to open a filesystem folder; In files: prefs.py:915; */
"Open" = "Atvērt";
/* prefs.py: Label on list of enabled plugins; In files: prefs.py:934; */
"Enabled Plugins" = "Iespējoti Spraudņi";

View File

@ -1,6 +1,3 @@
/* prefs.py: Catch & Record Profiler Errors; */
"Error in System Profiler" = "Fout in systeemprofiler";
/* edsm.py:Settings>EDSM - Label on checkbox for 'send data'; In files: edsm.py:316; */
"Send flight log and CMDR status to EDSM" = "Stuur logboek en CMDR-status naar EDSM";
@ -162,12 +159,6 @@
/* EDMarketConnector.py: CAPI fleetcarrier query aborted because of killswitch; In files: EDMarketConnector.py:1157; */
"CAPI fleetcarrier disabled by killswitch" = "CAPI fleetcarrier uitgeschakeld door killswitch";
/* EDMarketConnector.py: No data was returned for the fleetcarrier from the Frontier CAPI; In files: EDMarketConnector.py:1219; */
"CAPI: No fleetcarrier data returned" = "CAPI: geen fleet carrier data verkregen";
/* EDMarketConnector.py: We didn't have the fleetcarrier callsign when we should have; In files: EDMarketConnector.py:1223; */
"CAPI: Fleetcarrier data incomplete" = "CAPI: data fleet carrier incompleet";
/* EDMarketConnector.py: No data was returned for the commander from the Frontier CAPI; In files: EDMarketConnector.py:1242; */
"CAPI: No commander data returned" = "CAPI: geen commander data verkregen";
@ -375,9 +366,6 @@
/* prefs.py: Label for location of third-party plugins folder; In files: prefs.py:908; */
"Plugins folder" = "Plugins map";
/* prefs.py: Label on button used to open a filesystem folder; In files: prefs.py:915; */
"Open" = "Openen";
/* prefs.py: Tip/label about how to disable plugins; In files: prefs.py:923; */
"Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name" = "Tip: Je kan een plugin deactiveren door '{EXT}'{CR}toe te voegen aan de map naam";
@ -393,6 +381,9 @@
/* prefs.py: Lable on list of user-disabled plugins; In files: prefs.py:977; */
"Disabled Plugins" = "Gedeactiveerde plugins";
/* prefs.py: Catch & Record Profiler Errors; */
"Error in System Profiler" = "Fout in systeemprofiler";
/* stats.py: Cmdr stats; In files: stats.py:58; */
"Balance" = "Balans";

View File

@ -159,12 +159,6 @@
/* EDMarketConnector.py: CAPI fleetcarrier query aborted because of killswitch; In files: EDMarketConnector.py:1157; */
"CAPI fleetcarrier disabled by killswitch" = "Fleetcarier CAPI wyłączony \"kill switchem\"";
/* EDMarketConnector.py: No data was returned for the fleetcarrier from the Frontier CAPI; In files: EDMarketConnector.py:1219; */
"CAPI: No fleetcarrier data returned" = "CAPI: Nie zwrócono danych dotyczących lotniskowca";
/* EDMarketConnector.py: We didn't have the fleetcarrier callsign when we should have; In files: EDMarketConnector.py:1223; */
"CAPI: Fleetcarrier data incomplete" = "CAPI: Niekompletne dane lotniskowca";
/* EDMarketConnector.py: No data was returned for the commander from the Frontier CAPI; In files: EDMarketConnector.py:1242; */
"CAPI: No commander data returned" = "CAPI: Nie zwrócono danych dowódcy";
@ -369,9 +363,6 @@
/* prefs.py: Settings > Configuration - Label for CAPI section; In files: prefs.py:469; */
"CAPI Settings" = "Ustawienia CAPI";
/* prefs.py: Configuration - Enable or disable the Fleet Carrier CAPI calls; In files: prefs.py:475; */
"Enable Fleetcarrier CAPI Queries" = "Włącz zapytania CAPI dla lotniskowca";
/* prefs.py: Hotkey/Shortcut settings prompt on Windows; In files: prefs.py:492; */
"Hotkey" = "Skr. Klaw.";
@ -450,9 +441,6 @@
/* prefs.py: Label for location of third-party plugins folder; In files: prefs.py:908; */
"Plugins folder" = "Folder pluginów";
/* prefs.py: Label on button used to open a filesystem folder; In files: prefs.py:915; */
"Open" = "Otwórz";
/* prefs.py: Tip/label about how to disable plugins; In files: prefs.py:923; */
"Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name" = "Wskazówka: Możesz wyłączyć pluginy{CR}dodając '{EXT}' do nazwy folderu";

View File

@ -1,15 +1,3 @@
/* prefs.py: Catch & Record Profiler Errors; */
"Error in System Profiler" = "Erro no Perfil de Sistema";
/* EDMarketConnector.py: We didn't have the Fleet Carrier callsign when we should have; In files: EDMarketConnector.py:1223; */
"CAPI: Fleet Carrier data incomplete" = "CAPI: Dados de porta-frotas incompletos";
/* EDMarketConnector.py: No data was returned for the Fleet Carrier from the Frontier CAPI; In files: EDMarketConnector.py:1219; */
"CAPI: No Fleet Carrier data returned" = "CAPI: Nenhum dado de porta-frotas retornado";
/* prefs.py: Configuration - Enable or disable the Fleet Carrier CAPI calls; In files: prefs.py:475; */
"Enable Fleet Carrier CAPI Queries" = "Ativar requisições CAPI para porta-frotas";
/* edsm.py:Settings>EDSM - Label on checkbox for 'send data'; In files: edsm.py:316; */
"Send flight log and CMDR status to EDSM" = "Enviar registro de voo e status do CMDT para o EDSM";
@ -189,12 +177,12 @@
/* EDMarketConnector.py: CAPI fleetcarrier query aborted because of killswitch; In files: EDMarketConnector.py:1157; */
"CAPI fleetcarrier disabled by killswitch" = "CAPI para porta-frotas desativado pelo botão de interrupção.";
/* EDMarketConnector.py: No data was returned for the Fleet Carrier from the Frontier CAPI; In files: EDMarketConnector.py:1219; */
"CAPI: No Fleet Carrier data returned" = "CAPI: Nenhum dado de porta-frotas retornado";
/* EDMarketConnector.py: No data was returned for the fleetcarrier from the Frontier CAPI; In files: EDMarketConnector.py:1219; */
"CAPI: No fleetcarrier data returned" = "CAPI: Nenhum dado de porta-fortas retornado";
/* EDMarketConnector.py: We didn't have the Fleet Carrier callsign when we should have; In files: EDMarketConnector.py:1223; */
"CAPI: Fleet Carrier data incomplete" = "CAPI: Dados de porta-frotas incompletos";
/* EDMarketConnector.py: We didn't have the fleetcarrier callsign when we should have; In files: EDMarketConnector.py:1223; */
"CAPI: Fleetcarrier data incomplete" = "CAPI: Dados de porta-frota incompletos";
/* EDMarketConnector.py: No data was returned for the commander from the Frontier CAPI; In files: EDMarketConnector.py:1242; */
"CAPI: No commander data returned" = "CAPI: Nenhum dado de comandante retornado";
@ -399,9 +387,9 @@
/* prefs.py: Settings > Configuration - Label for CAPI section; In files: prefs.py:469; */
"CAPI Settings" = "Configurações de CAPI";
/* prefs.py: Configuration - Enable or disable the Fleet Carrier CAPI calls; In files: prefs.py:475; */
"Enable Fleetcarrier CAPI Queries" = "Ativar requisições CAPI para porta-frotas";
"Enable Fleet Carrier CAPI Queries" = "Ativar requisições CAPI para porta-frotas";
/* prefs.py: Hotkey/Shortcut settings prompt on Windows; In files: prefs.py:492; */
"Hotkey" = "Tecla de atalho";
@ -480,8 +468,11 @@
/* prefs.py: Label for location of third-party plugins folder; In files: prefs.py:908; */
"Plugins folder" = "Pasta dos plugins";
/* prefs.py: Label on button used to open a filesystem folder; In files: prefs.py:915; */
"Open" = "Abrir";
/* prefs.py: Label on button used to open the Plugin Folder; */
"Open Plugins Folder" = "Abrir pasta dos plugins";
/* prefs.py: Selecting the Location of the Plugin Directory on the Filesystem; */
"Plugin Directory Location" = "Localização da pasta dos plugins";
/* prefs.py: Tip/label about how to disable plugins; In files: prefs.py:923; */
"Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name" = "Dica: Você pode desativar um plugin{CR}adicionando '{EXT}' ao nome da pasta";
@ -501,6 +492,9 @@
/* prefs.py: Lable on list of user-disabled plugins; In files: prefs.py:977; */
"Disabled Plugins" = "Plugins desabilitados";
/* prefs.py: Catch & Record Profiler Errors; */
"Error in System Profiler" = "Erro no Perfil de Sistema";
/* stats.py: Cmdr stats; In files: stats.py:58; */
"Balance" = "Balanço";
@ -813,12 +807,20 @@
/* EDMarketConnector.py: Inform the user the Update Track has changed; */
"Update Track Changed to {TRACK}" = "Caminho de Atualizações modificado para {TRACK}";
/* EDMarketConnector.py: Inform User of Beta -> Stable Transition Risks; */
"Update track changed to Stable from Beta. You will no longer receive Beta updates. You will stay on your current Beta version until the next Stable release.\r\n\r\nYou can manually revert to the latest Stable version. To do so, you must download and install the latest Stable version manually. Note that this may introduce bugs or break completely if downgrading between major versions with significant changes.\r\n\r\nDo you want to open GitHub to download the latest release?" = "Caminho de atualizações modificado de Estável para Beta. Você não receberá mais atualizações Beta. Você seguirá na versão Beta atual até a próxima atualização Estável.\n\nVocê pode reverter manualmente para a última versão Estável. Para fazer isso, faça download e instale a última versão Estável manualmente. Fique atento: isto pode introduzir bugs ou quebrar completamente a aplicação caso você o retorno volte para uma versão principal com mudanças significativas.\n\nGostaria de abrir o GitHub para fazer download da última versão?";
/* EDMarketConnector.py: Title of Notification Popup for EDMC Restart; */
"Restart Required" = "Reinicialização necessária";
/* EDMarketConnector.py: Text of Notification Popup for EDMC Restart; */
"A restart of EDMC is required. EDMC will now restart." = "É necessário reinicializar o EDMC. Isso será realizado agora.";
/* myNotebook.py: Can't Paste Images or Files in Text; */
"Cannot paste non-text content." = "Só é possível colar texto.";
/* ttkHyperlinkLabel.py: Open Element In Selected Provider; */
"Open in {URL}" = "Abrir em {URL}";
/* ttkHyperlinkLabel.py: Copy the Inara SLEF Format of the active ship to the clipboard; */
"Copy Inara SLEF" = "Copiar formato SLEF";

View File

@ -1,15 +1,3 @@
/* prefs.py: Catch & Record Profiler Errors; */
"Error in System Profiler" = "Erro nas Informações do Sistema";
/* EDMarketConnector.py: We didn't have the Fleet Carrier callsign when we should have; In files: EDMarketConnector.py:1223; */
"CAPI: Fleet Carrier data incomplete" = "CAPI: Dados de Transportador de Frota incompletos";
/* EDMarketConnector.py: No data was returned for the Fleet Carrier from the Frontier CAPI; In files: EDMarketConnector.py:1219; */
"CAPI: No Fleet Carrier data returned" = "CAPI: Sem dados de Transportador de Frota";
/* prefs.py: Configuration - Enable or disable the Fleet Carrier CAPI calls; In files: prefs.py:475; */
"Enable Fleet Carrier CAPI Queries" = "Ligar Consultas CAPI de Transportador de Frota";
/* edsm.py:Settings>EDSM - Label on checkbox for 'send data'; In files: edsm.py:316; */
"Send flight log and CMDR status to EDSM" = "Enviar registos de voo e estado de CMDR para o EDSM";
@ -189,12 +177,12 @@
/* EDMarketConnector.py: CAPI fleetcarrier query aborted because of killswitch; In files: EDMarketConnector.py:1157; */
"CAPI fleetcarrier disabled by killswitch" = "Consulta à CAPI de Transportador de Frota cancelada por botão";
/* EDMarketConnector.py: No data was returned for the Fleet Carrier from the Frontier CAPI; In files: EDMarketConnector.py:1219; */
"CAPI: No Fleet Carrier data returned" = "CAPI: Sem dados de Transportador de Frota";
/* EDMarketConnector.py: No data was returned for the fleetcarrier from the Frontier CAPI; In files: EDMarketConnector.py:1219; */
"CAPI: No fleetcarrier data returned" = "CAPI: Sem dados de Transportador de Frota";
/* EDMarketConnector.py: We didn't have the Fleet Carrier callsign when we should have; In files: EDMarketConnector.py:1223; */
"CAPI: Fleet Carrier data incomplete" = "CAPI: Dados de Transportador de Frota incompletos";
/* EDMarketConnector.py: We didn't have the fleetcarrier callsign when we should have; In files: EDMarketConnector.py:1223; */
"CAPI: Fleetcarrier data incomplete" = "CAPI: Dados de Transportador de Frota incompletos";
/* EDMarketConnector.py: No data was returned for the commander from the Frontier CAPI; In files: EDMarketConnector.py:1242; */
"CAPI: No commander data returned" = "CAPI: Dados do Comandante não recebidos";
@ -399,9 +387,9 @@
/* prefs.py: Settings > Configuration - Label for CAPI section; In files: prefs.py:469; */
"CAPI Settings" = "Definições CAPI";
/* prefs.py: Configuration - Enable or disable the Fleet Carrier CAPI calls; In files: prefs.py:475; */
"Enable Fleetcarrier CAPI Queries" = "Ligar Consultas CAPI de Transportador de Frota";
"Enable Fleet Carrier CAPI Queries" = "Ligar Consultas CAPI de Transportador de Frota";
/* prefs.py: Hotkey/Shortcut settings prompt on Windows; In files: prefs.py:492; */
"Hotkey" = "Tecla Rápida";
@ -480,9 +468,6 @@
/* prefs.py: Label for location of third-party plugins folder; In files: prefs.py:908; */
"Plugins folder" = "Pasta dos Plugins";
/* prefs.py: Label on button used to open a filesystem folder; In files: prefs.py:915; */
"Open" = "Abrir";
/* prefs.py: Tip/label about how to disable plugins; In files: prefs.py:923; */
"Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name" = "Dica: Pode desactivar um plugin{CR}ao adicionar '{EXT}' ao nome da sua pasta";
@ -501,6 +486,9 @@
/* prefs.py: Lable on list of user-disabled plugins; In files: prefs.py:977; */
"Disabled Plugins" = "Plugins Desactivados";
/* prefs.py: Catch & Record Profiler Errors; */
"Error in System Profiler" = "Erro nas Informações do Sistema";
/* stats.py: Cmdr stats; In files: stats.py:58; */
"Balance" = "Balanço";
@ -813,7 +801,6 @@
/* EDMarketConnector.py: Inform the user the Update Track has changed; */
"Update Track Changed to {TRACK}" = "Caminho de Actualizações alterado para {TRACK}";
/* EDMarketConnector.py: Inform User of Beta -> Stable Transition Risks; */
"Update track changed to Stable from Beta. You will no longer receive Beta updates. You will stay on your current Beta version until the next Stable release.\r\n\r\nYou can manually revert to the latest Stable version. To do so, you must download and install the latest Stable version manually. Note that this may introduce bugs or break completely if downgrading between major versions with significant changes.\r\n\r\nDo you want to open GitHub to download the latest release?" = "Caminho de Actualizações alterado de Estável para Beta. Deixará de receber actualizações Beta. Irá continuar na versão Beta até à próxima actualização Estável.\n\nPode, manualmente, reverter para a última versão Estável. Para isso, faça download e instale a última versão Estável manualmente. Note que o retrocesso para versões anteriores com diferenças significativas poderá introduzir bugs ou danificar a instalação.\n\nDeseja abrir o GitHub para fazer download da última versão?";
@ -822,3 +809,4 @@
/* ttkHyperlinkLabel.py: Open Element In Selected Provider; */
"Open in {URL}" = "Abrir em {URL}";

View File

@ -1,15 +1,3 @@
/* prefs.py: Catch & Record Profiler Errors; */
"Error in System Profiler" = "Ошибка в системном профайлере";
/* EDMarketConnector.py: We didn't have the Fleet Carrier callsign when we should have; In files: EDMarketConnector.py:1223; */
"CAPI: Fleet Carrier data incomplete" = "CAPI: Неполная информация о корабле-носителе";
/* EDMarketConnector.py: No data was returned for the Fleet Carrier from the Frontier CAPI; In files: EDMarketConnector.py:1219; */
"CAPI: No Fleet Carrier data returned" = "CAPI: Нет данных о корабле-носителе";
/* prefs.py: Configuration - Enable or disable the Fleet Carrier CAPI calls; In files: prefs.py:475; */
"Enable Fleet Carrier CAPI Queries" = "Включить запросы к CAPI о корабле-носителе";
/* edsm.py:Settings>EDSM - Label on checkbox for 'send data'; In files: edsm.py:316; */
"Send flight log and CMDR status to EDSM" = "Отправить журнал полетов и статус командира в EDSM";
@ -189,12 +177,12 @@
/* EDMarketConnector.py: CAPI fleetcarrier query aborted because of killswitch; In files: EDMarketConnector.py:1157; */
"CAPI fleetcarrier disabled by killswitch" = "CAPI кораблей-носителей отключен с помощью killswitch";
/* EDMarketConnector.py: No data was returned for the Fleet Carrier from the Frontier CAPI; In files: EDMarketConnector.py:1219; */
"CAPI: No Fleet Carrier data returned" = "CAPI: Нет данных о корабле-носителе";
/* EDMarketConnector.py: No data was returned for the fleetcarrier from the Frontier CAPI; In files: EDMarketConnector.py:1219; */
"CAPI: No fleetcarrier data returned" = "CAPI: Нет данных о флотоносце";
/* EDMarketConnector.py: We didn't have the Fleet Carrier callsign when we should have; In files: EDMarketConnector.py:1223; */
"CAPI: Fleet Carrier data incomplete" = "CAPI: Неполная информация о корабле-носителе";
/* EDMarketConnector.py: We didn't have the fleetcarrier callsign when we should have; In files: EDMarketConnector.py:1223; */
"CAPI: Fleetcarrier data incomplete" = "CAPI: Неполная информация о флотоносце";
/* EDMarketConnector.py: No data was returned for the commander from the Frontier CAPI; In files: EDMarketConnector.py:1242; */
"CAPI: No commander data returned" = "CAPI: Нет данных пилота";
@ -399,9 +387,9 @@
/* prefs.py: Settings > Configuration - Label for CAPI section; In files: prefs.py:469; */
"CAPI Settings" = "Настройки CAPI";
/* prefs.py: Configuration - Enable or disable the Fleet Carrier CAPI calls; In files: prefs.py:475; */
"Enable Fleetcarrier CAPI Queries" = "Включить запросы к CAPI о флотоносце";
"Enable Fleet Carrier CAPI Queries" = "Включить запросы к CAPI о корабле-носителе";
/* prefs.py: Hotkey/Shortcut settings prompt on Windows; In files: prefs.py:492; */
"Hotkey" = "Горячая клавиша";
@ -480,8 +468,11 @@
/* prefs.py: Label for location of third-party plugins folder; In files: prefs.py:908; */
"Plugins folder" = "Папка плагинов";
/* prefs.py: Label on button used to open a filesystem folder; In files: prefs.py:915; */
"Open" = "Открыть";
/* prefs.py: Label on button used to open the Plugin Folder; */
"Open Plugins Folder" = "Открыть папку плагинов";
/* prefs.py: Selecting the Location of the Plugin Directory on the Filesystem; */
"Plugin Directory Location" = "Месторасположение директории плагинов";
/* prefs.py: Tip/label about how to disable plugins; In files: prefs.py:923; */
"Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name" = "Подсказка: Отключить конкретный плагин можно {CR} добавив '{EXT}' к названию папки";
@ -501,6 +492,9 @@
/* prefs.py: Lable on list of user-disabled plugins; In files: prefs.py:977; */
"Disabled Plugins" = "Отключенные плагины";
/* prefs.py: Catch & Record Profiler Errors; */
"Error in System Profiler" = "Ошибка в системном профайлере";
/* stats.py: Cmdr stats; In files: stats.py:58; */
"Balance" = "Баланс";
@ -813,12 +807,20 @@
/* EDMarketConnector.py: Inform the user the Update Track has changed; */
"Update Track Changed to {TRACK}" = "Обновить маршрут. Изменено на {TRACK}";
/* EDMarketConnector.py: Inform User of Beta -> Stable Transition Risks; */
"Update track changed to Stable from Beta. You will no longer receive Beta updates. You will stay on your current Beta version until the next Stable release.\r\n\r\nYou can manually revert to the latest Stable version. To do so, you must download and install the latest Stable version manually. Note that this may introduce bugs or break completely if downgrading between major versions with significant changes.\r\n\r\nDo you want to open GitHub to download the latest release?" = "Изменен путь обновления с \"Бета\" на \"Стабильный\". Вы больше не будете получать обновления \"Бета\". Вы останетесь на текущей бета-версии до следующего стабильного релиза.\n\nВы можете вручную вернуться к последней версии \"Стабильная\". Для этого необходимо загрузить и установить последнюю версию \"Стабильная\" вручную. Обратите внимание, что при переходе между основными версиями со значительными изменениями могут возникнуть ошибки или полная поломка.\n\nХотите открыть GitHub, чтобы загрузить последнюю версию?";
/* EDMarketConnector.py: Title of Notification Popup for EDMC Restart; */
"Restart Required" = "Требуется перезапуск";
/* EDMarketConnector.py: Text of Notification Popup for EDMC Restart; */
"A restart of EDMC is required. EDMC will now restart." = "Требуется перезапуск EDMC. EDMC сейчас перезапустится.";
/* myNotebook.py: Can't Paste Images or Files in Text; */
"Cannot paste non-text content." = "Невозможно добавить нетекстовое содержимое.";
/* ttkHyperlinkLabel.py: Open Element In Selected Provider; */
"Open in {URL}" = "Открыть через {URL}";
/* ttkHyperlinkLabel.py: Copy the Inara SLEF Format of the active ship to the clipboard; */
"Copy Inara SLEF" = "Скопировать данные SLEF Inara";

View File

@ -1,15 +1,3 @@
/* prefs.py: Catch & Record Profiler Errors; */
"Error in System Profiler" = "Greša u System Profileru";
/* EDMarketConnector.py: We didn't have the Fleet Carrier callsign when we should have; In files: EDMarketConnector.py:1223; */
"CAPI: Fleet Carrier data incomplete" = "CAPI: Fleet Carrier podaci nisu kompletni";
/* EDMarketConnector.py: No data was returned for the Fleet Carrier from the Frontier CAPI; In files: EDMarketConnector.py:1219; */
"CAPI: No Fleet Carrier data returned" = "CAPI: Fleet Carrier podaci nisu dobijeni";
/* prefs.py: Configuration - Enable or disable the Fleet Carrier CAPI calls; In files: prefs.py:475; */
"Enable Fleet Carrier CAPI Queries" = "Omogući Fleet Carrier CAPI upite";
/* edsm.py:Settings>EDSM - Label on checkbox for 'send data'; In files: edsm.py:316; */
"Send flight log and CMDR status to EDSM" = "Pošalji log leta i CMDR status na EDSM";
@ -189,12 +177,12 @@
/* EDMarketConnector.py: CAPI fleetcarrier query aborted because of killswitch; In files: EDMarketConnector.py:1157; */
"CAPI fleetcarrier disabled by killswitch" = "CAPI fleetcarrier onemogućen putem sistemskog prekidača";
/* EDMarketConnector.py: No data was returned for the Fleet Carrier from the Frontier CAPI; In files: EDMarketConnector.py:1219; */
"CAPI: No Fleet Carrier data returned" = "CAPI: Fleet Carrier podaci nisu dobijeni";
/* EDMarketConnector.py: No data was returned for the fleetcarrier from the Frontier CAPI; In files: EDMarketConnector.py:1219; */
"CAPI: No fleetcarrier data returned" = "CAPI: Fleetcarrier podaci nisu dobijeni";
/* EDMarketConnector.py: We didn't have the Fleet Carrier callsign when we should have; In files: EDMarketConnector.py:1223; */
"CAPI: Fleet Carrier data incomplete" = "CAPI: Fleet Carrier podaci nisu kompletni";
/* EDMarketConnector.py: We didn't have the fleetcarrier callsign when we should have; In files: EDMarketConnector.py:1223; */
"CAPI: Fleetcarrier data incomplete" = "CAPI: Fleetcarrier podaci nisu potpuni";
/* EDMarketConnector.py: No data was returned for the commander from the Frontier CAPI; In files: EDMarketConnector.py:1242; */
"CAPI: No commander data returned" = "CAPI: Nema podataka o komandantu";
@ -399,9 +387,9 @@
/* prefs.py: Settings > Configuration - Label for CAPI section; In files: prefs.py:469; */
"CAPI Settings" = "CAPI Podešavanja";
/* prefs.py: Configuration - Enable or disable the Fleet Carrier CAPI calls; In files: prefs.py:475; */
"Enable Fleetcarrier CAPI Queries" = "Omogući Fleetcarrier CAPI Upite";
"Enable Fleet Carrier CAPI Queries" = "Omogući Fleet Carrier CAPI upite";
/* prefs.py: Hotkey/Shortcut settings prompt on Windows; In files: prefs.py:492; */
"Hotkey" = "Prečica";
@ -480,9 +468,6 @@
/* prefs.py: Label for location of third-party plugins folder; In files: prefs.py:908; */
"Plugins folder" = "Folder za dodatke (plugins)";
/* prefs.py: Label on button used to open a filesystem folder; In files: prefs.py:915; */
"Open" = "Otvori";
/* prefs.py: Tip/label about how to disable plugins; In files: prefs.py:923; */
"Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name" = "Savjet: Možete da deaktivirate dodatak (plugin){CR}dodavanjem '{EXT}' na ime njegovog foldera";
@ -501,6 +486,9 @@
/* prefs.py: Lable on list of user-disabled plugins; In files: prefs.py:977; */
"Disabled Plugins" = "Deaktivirani dodaci (plugins)";
/* prefs.py: Catch & Record Profiler Errors; */
"Error in System Profiler" = "Greša u System Profileru";
/* stats.py: Cmdr stats; In files: stats.py:58; */
"Balance" = "Balans";
@ -813,7 +801,6 @@
/* EDMarketConnector.py: Inform the user the Update Track has changed; */
"Update Track Changed to {TRACK}" = "Kanal za osvježavanje je promijenjen u {TRACK}";
/* EDMarketConnector.py: Inform User of Beta -> Stable Transition Risks; */
"Update track changed to Stable from Beta. You will no longer receive Beta updates. You will stay on your current Beta version until the next Stable release.\r\n\r\nYou can manually revert to the latest Stable version. To do so, you must download and install the latest Stable version manually. Note that this may introduce bugs or break completely if downgrading between major versions with significant changes.\r\n\r\nDo you want to open GitHub to download the latest release?" = "Kanal za osvježavanje je promijenjen iz Beta u Stabilni. Više nećete primati Beta osvježavanja. Ostaćete na trenutnoj Beta verziji do sljedeće Stabilne verzije.\n\nMožete se ručno vratiti na posljednju Stabilnu verziju. Da biste to učinili morate da skinete i ručno instališete posljednju Stabilnu verziju. Ova procedura može da uvede nove bugove ili da potpuno onesposobi program ako prelazite između verzija sa mnogo značajnih promjena.\n\nDa li želite da otvorite GitHub stranicu za download posljednje verzije?";
@ -822,3 +809,4 @@
/* ttkHyperlinkLabel.py: Open Element In Selected Provider; */
"Open in {URL}" = "Otvori u {URL}";

View File

@ -168,12 +168,6 @@
/* EDMarketConnector.py: CAPI fleetcarrier query aborted because of killswitch; In files: EDMarketConnector.py:1157; */
"CAPI fleetcarrier disabled by killswitch" = "CAPI fleetcarrier deaktiviran preko sistemskog prekidača";
/* EDMarketConnector.py: No data was returned for the fleetcarrier from the Frontier CAPI; In files: EDMarketConnector.py:1219; */
"CAPI: No fleetcarrier data returned" = "CAPI: Podaci o fleetcarrier-u nisu dobijeni";
/* EDMarketConnector.py: We didn't have the fleetcarrier callsign when we should have; In files: EDMarketConnector.py:1223; */
"CAPI: Fleetcarrier data incomplete" = "CAPI: Podaci o fleetcarrier-u su nepotpuni";
/* EDMarketConnector.py: No data was returned for the commander from the Frontier CAPI; In files: EDMarketConnector.py:1242; */
"CAPI: No commander data returned" = "CAPI: Nema podataka o komandiru";
@ -378,9 +372,6 @@
/* prefs.py: Settings > Configuration - Label for CAPI section; In files: prefs.py:469; */
"CAPI Settings" = "CAPI Podešavanja";
/* prefs.py: Configuration - Enable or disable the Fleet Carrier CAPI calls; In files: prefs.py:475; */
"Enable Fleetcarrier CAPI Queries" = "Aktiviraj Fleetcarrier CAPIU Upite";
/* prefs.py: Hotkey/Shortcut settings prompt on Windows; In files: prefs.py:492; */
"Hotkey" = "Skraćenica";
@ -459,9 +450,6 @@
/* prefs.py: Label for location of third-party plugins folder; In files: prefs.py:908; */
"Plugins folder" = "Folder za dodatke (plugins)";
/* prefs.py: Label on button used to open a filesystem folder; In files: prefs.py:915; */
"Open" = "Otvori";
/* prefs.py: Tip/label about how to disable plugins; In files: prefs.py:923; */
"Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name" = "Tip: Možete deaktivirati dodatak (plugin) dodavanjem{CR}'{EXT}' na ime njegovog foldera";
@ -788,3 +776,4 @@
/* ttkHyperlinkLabel.py: Open Element In Selected Provider; */
"Open in {URL}" = "Otvori na {URL}";

View File

@ -241,9 +241,6 @@
/* prefs.py: Label for location of third-party plugins folder; In files: prefs.py:908; */
"Plugins folder" = "Mapp för plugins";
/* prefs.py: Label on button used to open a filesystem folder; In files: prefs.py:915; */
"Open" = "Öppna";
/* prefs.py: Tip/label about how to disable plugins; In files: prefs.py:923; */
"Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name" = "Tips: du kan avaktivera en plugin genom{CR}att lägga till '{EXT}' på slutet av mappens namn";

View File

@ -1,15 +1,3 @@
/* prefs.py: Catch & Record Profiler Errors; */
"Error in System Profiler" = "Sistem Profilcisinde Hata oluştu";
/* EDMarketConnector.py: We didn't have the Fleet Carrier callsign when we should have; In files: EDMarketConnector.py:1223; */
"CAPI: Fleet Carrier data incomplete" = "CAPI: Filo Taşıyıcısı verileri eksik";
/* EDMarketConnector.py: No data was returned for the Fleet Carrier from the Frontier CAPI; In files: EDMarketConnector.py:1219; */
"CAPI: No Fleet Carrier data returned" = "CAPI: Filo Taşıyıcısı verisi bulunamadı";
/* prefs.py: Configuration - Enable or disable the Fleet Carrier CAPI calls; In files: prefs.py:475; */
"Enable Fleet Carrier CAPI Queries" = "Filo Taşıyıcı CAPI Sorgularını Etkinleştir";
/* edsm.py:Settings>EDSM - Label on checkbox for 'send data'; In files: edsm.py:316; */
"Send flight log and CMDR status to EDSM" = "Uçuş günlüğünü ve CMDR durumunu EDSM'e gönder";
@ -189,12 +177,12 @@
/* EDMarketConnector.py: CAPI fleetcarrier query aborted because of killswitch; In files: EDMarketConnector.py:1157; */
"CAPI fleetcarrier disabled by killswitch" = "CAPI filo taşıyıcısı killswitch tarafından devre dışı bırakıldı";
/* EDMarketConnector.py: No data was returned for the Fleet Carrier from the Frontier CAPI; In files: EDMarketConnector.py:1219; */
"CAPI: No Fleet Carrier data returned" = "CAPI: Filo Taşıyıcısı verisi bulunamadı";
/* EDMarketConnector.py: No data was returned for the fleetcarrier from the Frontier CAPI; In files: EDMarketConnector.py:1219; */
"CAPI: No fleetcarrier data returned" = "CAPI: Filo taşıyıcı verisi gelmedi.";
/* EDMarketConnector.py: We didn't have the Fleet Carrier callsign when we should have; In files: EDMarketConnector.py:1223; */
"CAPI: Fleet Carrier data incomplete" = "CAPI: Filo Taşıyıcısı verileri eksik";
/* EDMarketConnector.py: We didn't have the fleetcarrier callsign when we should have; In files: EDMarketConnector.py:1223; */
"CAPI: Fleetcarrier data incomplete" = "CAPI: Filo taşıyıcı verileri eksik";
/* EDMarketConnector.py: No data was returned for the commander from the Frontier CAPI; In files: EDMarketConnector.py:1242; */
"CAPI: No commander data returned" = "CAPI: Cmdr verisi döndürülmedi";
@ -399,9 +387,9 @@
/* prefs.py: Settings > Configuration - Label for CAPI section; In files: prefs.py:469; */
"CAPI Settings" = "CAPI ayarları";
/* prefs.py: Configuration - Enable or disable the Fleet Carrier CAPI calls; In files: prefs.py:475; */
"Enable Fleetcarrier CAPI Queries" = "Filo Taşıyıcı CAPI sorgulamalarını etkinleştir";
"Enable Fleet Carrier CAPI Queries" = "Filo Taşıyıcı CAPI Sorgularını Etkinleştir";
/* prefs.py: Hotkey/Shortcut settings prompt on Windows; In files: prefs.py:492; */
"Hotkey" = "Kısayoltuşu";
@ -480,9 +468,6 @@
/* prefs.py: Label for location of third-party plugins folder; In files: prefs.py:908; */
"Plugins folder" = "Eklentiler klasörü";
/* prefs.py: Label on button used to open a filesystem folder; In files: prefs.py:915; */
"Open" = "Aç";
/* prefs.py: Tip/label about how to disable plugins; In files: prefs.py:923; */
"Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name" = "İpucu: Bir eklentiyi, klasör adına{CR}'{EXT}' ekleyerek devre dışı bırakabilirsiniz";
@ -501,6 +486,9 @@
/* prefs.py: Lable on list of user-disabled plugins; In files: prefs.py:977; */
"Disabled Plugins" = "Devre Dışı Eklentiler";
/* prefs.py: Catch & Record Profiler Errors; */
"Error in System Profiler" = "Sistem Profilcisinde Hata oluştu";
/* stats.py: Cmdr stats; In files: stats.py:58; */
"Balance" = "Bakiye";
@ -813,7 +801,6 @@
/* EDMarketConnector.py: Inform the user the Update Track has changed; */
"Update Track Changed to {TRACK}" = "Aktif Takip {TRACK} olarak güncellendi.";
/* EDMarketConnector.py: Inform User of Beta -> Stable Transition Risks; */
"Update track changed to Stable from Beta. You will no longer receive Beta updates. You will stay on your current Beta version until the next Stable release.\r\n\r\nYou can manually revert to the latest Stable version. To do so, you must download and install the latest Stable version manually. Note that this may introduce bugs or break completely if downgrading between major versions with significant changes.\r\n\r\nDo you want to open GitHub to download the latest release?" = "Güncelleme takibi Beta'dan Kararlı olarak değiştirildi. Artık Beta güncellemelerini almayacaksınız. Bir sonraki Kararlı sürüme kadar mevcut Beta sürümünüzde kalacaksınız.\n\nEn son Stabil sürüme manuel olarak geri dönebilirsiniz. Bunu yapmak için en son Stabil sürümünü manuel olarak indirip yüklemeniz gerekir. Önemli değişiklikler içeren ana sürümler arasında sürüm düşürme durumunda bunun hatalara yol açabileceğini veya tamamen bozulabileceğini unutmayın.\n\nEn son sürümü indirmek için GitHub'u açmak istiyor musunuz?";
@ -822,3 +809,4 @@
/* ttkHyperlinkLabel.py: Open Element In Selected Provider; */
"Open in {URL}" = "Şurada aç {URL}";

View File

@ -1,15 +1,3 @@
/* prefs.py: Catch & Record Profiler Errors; */
"Error in System Profiler" = "Помилка у Профайлері Систем";
/* EDMarketConnector.py: We didn't have the Fleet Carrier callsign when we should have; In files: EDMarketConnector.py:1223; */
"CAPI: Fleet Carrier data incomplete" = "CAPI: Дані корабля-носія неповні";
/* EDMarketConnector.py: No data was returned for the Fleet Carrier from the Frontier CAPI; In files: EDMarketConnector.py:1219; */
"CAPI: No Fleet Carrier data returned" = "CAPI: Немає даних корабля-носія";
/* prefs.py: Configuration - Enable or disable the Fleet Carrier CAPI calls; In files: prefs.py:475; */
"Enable Fleet Carrier CAPI Queries" = "Ввімкнути запити CAPI кораблів-носіїв";
/* edsm.py:Settings>EDSM - Label on checkbox for 'send data'; In files: edsm.py:316; */
"Send flight log and CMDR status to EDSM" = "Відправляти дані бортового журналу до EDSM";
@ -189,12 +177,12 @@
/* EDMarketConnector.py: CAPI fleetcarrier query aborted because of killswitch; In files: EDMarketConnector.py:1157; */
"CAPI fleetcarrier disabled by killswitch" = "Запит CAPI корабля-носія скасований функцією аварійного відключення";
/* EDMarketConnector.py: No data was returned for the Fleet Carrier from the Frontier CAPI; In files: EDMarketConnector.py:1219; */
"CAPI: No Fleet Carrier data returned" = "CAPI: Немає даних корабля-носія";
/* EDMarketConnector.py: No data was returned for the fleetcarrier from the Frontier CAPI; In files: EDMarketConnector.py:1219; */
"CAPI: No fleetcarrier data returned" = "CAPI: Немає даних корабля-носія";
/* EDMarketConnector.py: We didn't have the Fleet Carrier callsign when we should have; In files: EDMarketConnector.py:1223; */
"CAPI: Fleet Carrier data incomplete" = "CAPI: Дані корабля-носія неповні";
/* EDMarketConnector.py: We didn't have the fleetcarrier callsign when we should have; In files: EDMarketConnector.py:1223; */
"CAPI: Fleetcarrier data incomplete" = "CAPI: Дані корабля-носія неповні";
/* EDMarketConnector.py: No data was returned for the commander from the Frontier CAPI; In files: EDMarketConnector.py:1242; */
"CAPI: No commander data returned" = "CAPI: Не отримано данних пілота";
@ -399,9 +387,9 @@
/* prefs.py: Settings > Configuration - Label for CAPI section; In files: prefs.py:469; */
"CAPI Settings" = "Налаштування CAPI";
/* prefs.py: Configuration - Enable or disable the Fleet Carrier CAPI calls; In files: prefs.py:475; */
"Enable Fleetcarrier CAPI Queries" = "Ввімкнути запити CAPI кораблів-носіїв";
"Enable Fleet Carrier CAPI Queries" = "Ввімкнути запити CAPI кораблів-носіїв";
/* prefs.py: Hotkey/Shortcut settings prompt on Windows; In files: prefs.py:492; */
"Hotkey" = "Гаряча клавіша";
@ -480,8 +468,11 @@
/* prefs.py: Label for location of third-party plugins folder; In files: prefs.py:908; */
"Plugins folder" = "Сховище плагінів";
/* prefs.py: Label on button used to open a filesystem folder; In files: prefs.py:915; */
"Open" = "Відкрити";
/* prefs.py: Label on button used to open the Plugin Folder; */
"Open Plugins Folder" = "Відкрити папку плагінів";
/* prefs.py: Selecting the Location of the Plugin Directory on the Filesystem; */
"Plugin Directory Location" = "Місцезнаходження папки плагінів";
/* prefs.py: Tip/label about how to disable plugins; In files: prefs.py:923; */
"Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name" = "Підказка: Ви можете відключити плагін через {CR} додання '{EXT}' до назви папки";
@ -501,6 +492,9 @@
/* prefs.py: Lable on list of user-disabled plugins; In files: prefs.py:977; */
"Disabled Plugins" = "Вимкнені плагіни";
/* prefs.py: Catch & Record Profiler Errors; */
"Error in System Profiler" = "Помилка у Профайлері Систем";
/* stats.py: Cmdr stats; In files: stats.py:58; */
"Balance" = "Баланс";
@ -813,12 +807,20 @@
/* EDMarketConnector.py: Inform the user the Update Track has changed; */
"Update Track Changed to {TRACK}" = "Шлях оновлень змінено на {TRACK}";
/* EDMarketConnector.py: Inform User of Beta -> Stable Transition Risks; */
"Update track changed to Stable from Beta. You will no longer receive Beta updates. You will stay on your current Beta version until the next Stable release.\r\n\r\nYou can manually revert to the latest Stable version. To do so, you must download and install the latest Stable version manually. Note that this may introduce bugs or break completely if downgrading between major versions with significant changes.\r\n\r\nDo you want to open GitHub to download the latest release?" = "Шлях оновлень змінено з \"Бета\" на \"Стабільний\". Ви більше не будете отримувати бета оновлень. Ви залишитеся на версії бета до наступного стабільного релізу.\n\nВи можете вручну вернутися до останньої стабільної версії. Щоб це зробити, вам необхідно завантажити останню стабільну версію вручну. Зауважте що заниження версії між релізами з багатьма змінами може спричинити помилки чи повністю унеможливити роботу програми.\n\nБажаєте відкрити сторінку GitHub для завантаження останнього релізу?";
/* EDMarketConnector.py: Title of Notification Popup for EDMC Restart; */
"Restart Required" = "Необхідно перезавантаження";
/* EDMarketConnector.py: Text of Notification Popup for EDMC Restart; */
"A restart of EDMC is required. EDMC will now restart." = "Необхідно перезавантажити EDMC. EDMC буде зараз перезавантажений.";
/* myNotebook.py: Can't Paste Images or Files in Text; */
"Cannot paste non-text content." = "Неможливо вставити нетекстовий вміст буфера обміну.";
/* ttkHyperlinkLabel.py: Open Element In Selected Provider; */
"Open in {URL}" = "Відкрити у {URL}";
/* ttkHyperlinkLabel.py: Copy the Inara SLEF Format of the active ship to the clipboard; */
"Copy Inara SLEF" = "Копіювати Inara SLEF";

View File

@ -151,12 +151,6 @@
/* EDMarketConnector.py: CAPI fleetcarrier query aborted because of killswitch; In files: EDMarketConnector.py:1157; */
"CAPI fleetcarrier disabled by killswitch" = "CAPI 舰队母舰 (fleet carrier) 被 killswitch 禁用";
/* EDMarketConnector.py: No data was returned for the fleetcarrier from the Frontier CAPI; In files: EDMarketConnector.py:1219; */
"CAPI: No fleetcarrier data returned" = "CAPI无舰队母舰 (fleet carrier) 数据";
/* EDMarketConnector.py: We didn't have the fleetcarrier callsign when we should have; In files: EDMarketConnector.py:1223; */
"CAPI: Fleetcarrier data incomplete" = "CAPI舰队母舰 (fleet carrier) 数据不完整";
/* EDMarketConnector.py: No data was returned for the commander from the Frontier CAPI; In files: EDMarketConnector.py:1242; */
"CAPI: No commander data returned" = "CAPI没有指挥官数据";
@ -343,9 +337,6 @@
/* prefs.py: Settings > Configuration - Label for CAPI section; In files: prefs.py:469; */
"CAPI Settings" = "CAPI 设置";
/* prefs.py: Configuration - Enable or disable the Fleet Carrier CAPI calls; In files: prefs.py:475; */
"Enable Fleetcarrier CAPI Queries" = "开启舰队母舰 (fleet carrier) CAPI 访问";
/* prefs.py: Hotkey/Shortcut settings prompt on Windows; In files: prefs.py:492; */
"Hotkey" = "快捷键";
@ -424,9 +415,6 @@
/* prefs.py: Label for location of third-party plugins folder; In files: prefs.py:908; */
"Plugins folder" = "插件文件夹";
/* prefs.py: Label on button used to open a filesystem folder; In files: prefs.py:915; */
"Open" = "打开";
/* prefs.py: Tip/label about how to disable plugins; In files: prefs.py:923; */
"Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name" = "提示:想要禁用某一项插件,{CR}你可以添加 {EXT} 到其文件夹名称中";

View File

@ -10,7 +10,6 @@ import shutil
import sys
import pathlib
from string import Template
from os.path import join, isdir
import py2exe
from config import (
appcmdname,
@ -37,7 +36,7 @@ def iss_build(template_path: str, output_file: str) -> None:
new_file.write(newfile)
def system_check(dist_dir: str) -> str:
def system_check(dist_dir: pathlib.Path) -> str:
"""Check if the system is able to build."""
if sys.version_info < (3, 11):
sys.exit(f"Unexpected Python version {sys.version}")
@ -55,17 +54,18 @@ def system_check(dist_dir: str) -> str:
print(f"Git short hash: {git_shorthash}")
if dist_dir and len(dist_dir) > 1 and isdir(dist_dir):
if dist_dir and pathlib.Path.is_dir(dist_dir):
shutil.rmtree(dist_dir)
return gitversion_file
def generate_data_files(
app_name: str, gitversion_file: str, plugins: list[str]
) -> list[tuple[str, list[str]]]:
) -> list[tuple[object, object]]:
"""Create the required datafiles to build."""
l10n_dir = "L10n"
fdevids_dir = "FDevIDs"
fdevids_dir = pathlib.Path("FDevIDs")
license_dir = pathlib.Path("docs/Licenses")
data_files = [
(
"",
@ -88,23 +88,29 @@ def generate_data_files(
),
(
l10n_dir,
[join(l10n_dir, x) for x in os.listdir(l10n_dir) if x.endswith(".strings")],
[pathlib.Path(l10n_dir) / x for x in os.listdir(l10n_dir) if x.endswith(".strings")]
),
(
fdevids_dir,
[
join(fdevids_dir, "commodity.csv"),
join(fdevids_dir, "rare_commodity.csv"),
pathlib.Path(fdevids_dir / "commodity.csv"),
pathlib.Path(fdevids_dir / "rare_commodity.csv"),
],
),
("plugins", plugins),
]
# Add all files recursively from license directories
for root, dirs, files in os.walk(license_dir):
file_list = [os.path.join(root, f) for f in files]
dest_dir = os.path.join(license_dir, os.path.relpath(root, license_dir))
data_files.append((dest_dir, file_list))
return data_files
def build() -> None:
"""Build EDMarketConnector using Py2Exe."""
dist_dir: str = "dist.win32"
dist_dir: pathlib.Path = pathlib.Path("dist.win32")
gitversion_filename: str = system_check(dist_dir)
# Constants
@ -142,7 +148,7 @@ def build() -> None:
}
# Function to generate DATA_FILES list
data_files: list[tuple[str, list[str]]] = generate_data_files(
data_files: list[tuple[object, object]] = generate_data_files(
appname, gitversion_filename, plugins
)

View File

@ -17,7 +17,6 @@ import json
import os
import pathlib
import sys
from os.path import isfile
from traceback import print_exc
import companion
@ -35,7 +34,7 @@ def __make_backup(file_name: pathlib.Path, suffix: str = '.bak') -> None:
"""
backup_name = file_name.parent / (file_name.name + suffix)
if isfile(backup_name):
if pathlib.Path.is_file(backup_name):
os.unlink(backup_name)
os.rename(file_name, backup_name)
@ -52,13 +51,13 @@ def addcommodities(data) -> None: # noqa: CCR001
return
try:
commodityfile = pathlib.Path(config.app_dir_path / 'FDevIDs' / 'commodity.csv')
commodityfile = config.app_dir_path / 'FDevIDs' / 'commodity.csv'
except FileNotFoundError:
commodityfile = pathlib.Path('FDevIDs/commodity.csv')
commodities = {}
# slurp existing
if isfile(commodityfile):
if pathlib.Path.is_file(commodityfile):
with open(commodityfile) as csvfile:
reader = csv.DictReader(csvfile)
for row in reader:
@ -86,7 +85,7 @@ def addcommodities(data) -> None: # noqa: CCR001
if len(commodities) <= size_pre:
return
if isfile(commodityfile):
if pathlib.Path.is_file(commodityfile):
__make_backup(commodityfile)
with open(commodityfile, 'w', newline='\n') as csvfile:
@ -109,7 +108,7 @@ def addmodules(data): # noqa: C901, CCR001
fields = ('id', 'symbol', 'category', 'name', 'mount', 'guidance', 'ship', 'class', 'rating', 'entitlement')
# slurp existing
if isfile(outfile):
if pathlib.Path.is_file(outfile):
with open(outfile) as csvfile:
reader = csv.DictReader(csvfile, restval='')
for row in reader:
@ -147,7 +146,7 @@ def addmodules(data): # noqa: C901, CCR001
if not len(modules) > size_pre:
return
if isfile(outfile):
if pathlib.Path.is_file(outfile):
__make_backup(outfile)
with open(outfile, 'w', newline='\n') as csvfile:
@ -170,7 +169,7 @@ def addships(data) -> None: # noqa: CCR001
fields = ('id', 'symbol', 'name')
# slurp existing
if isfile(shipfile):
if pathlib.Path.is_file(shipfile):
with open(shipfile) as csvfile:
reader = csv.DictReader(csvfile, restval='')
for row in reader:
@ -200,7 +199,7 @@ def addships(data) -> None: # noqa: CCR001
if not len(ships) > size_pre:
return
if isfile(shipfile):
if pathlib.Path.is_file(shipfile):
__make_backup(shipfile)
with open(shipfile, 'w', newline='\n') as csvfile:

View File

@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
import time
from os.path import join
from pathlib import Path
from config import config
from edmc_data import commodity_bracketmap as bracketmap
@ -29,7 +29,7 @@ def export(data, kind=COMMODITY_DEFAULT, filename=None) -> None:
filename_time = time.strftime('%Y-%m-%dT%H.%M.%S', time.localtime(querytime))
filename_kind = 'csv'
filename = f'{filename_system}.{filename_starport}.{filename_time}.{filename_kind}'
filename = join(config.get_str('outdir'), filename)
filename = Path(config.get_str('outdir')) / filename
if kind == COMMODITY_CSV:
sep = ';' # BUG: for fixing later after cleanup

View File

@ -27,6 +27,7 @@ import tkinter as tk
import urllib.parse
import webbrowser
from email.utils import parsedate
from pathlib import Path
from queue import Queue
from typing import TYPE_CHECKING, Any, Mapping, TypeVar
import requests
@ -1135,7 +1136,7 @@ class Session:
def dump_capi_data(self, data: CAPIData) -> None:
"""Dump CAPI data to file for examination."""
if os.path.isdir('dump'):
if Path('dump').is_dir():
file_name: str = ""
if data.source_endpoint == self.FRONTIER_CAPI_PATH_FLEETCARRIER:
file_name += f"FleetCarrier.{data['name']['callsign']}"
@ -1203,7 +1204,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'):
if not os.path.isfile(config.app_dir_path / 'FDevIDs/' / f):
if not (config.app_dir_path / 'FDevIDs' / f).is_file():
logger.warning(f'FDevID file {f} not found! Generating output without these commodity name rewrites.')
continue
with open(config.app_dir_path / 'FDevIDs' / f, 'r') as csvfile:

View File

@ -52,7 +52,7 @@ appcmdname = 'EDMC'
# <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.11.3'
_static_appversion = '5.12.0'
_cached_version: semantic_version.Version | None = None
copyright = '© 2015-2019 Jonathan Harris, 2020-2024 EDCD'
@ -189,6 +189,7 @@ class AbstractConfig(abc.ABC):
app_dir_path: pathlib.Path
plugin_dir_path: pathlib.Path
default_plugin_dir_path: pathlib.Path
internal_plugin_dir_path: pathlib.Path
respath_path: pathlib.Path
home_path: pathlib.Path
@ -279,6 +280,11 @@ class AbstractConfig(abc.ABC):
"""Return a string version of plugin_dir."""
return str(self.plugin_dir_path)
@property
def default_plugin_dir(self) -> str:
"""Return a string version of plugin_dir."""
return str(self.default_plugin_dir_path)
@property
def internal_plugin_dir(self) -> str:
"""Return a string version of internal_plugin_dir."""

View File

@ -31,9 +31,7 @@ class LinuxConfig(AbstractConfig):
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.default_plugin_dir_path = self.app_dir_path / 'plugins'
self.respath_path = pathlib.Path(__file__).parent.parent
self.internal_plugin_dir_path = self.respath_path / 'plugins'
@ -62,6 +60,12 @@ class LinuxConfig(AbstractConfig):
self.config.add_section(self.SECTION)
if (plugdir_str := self.get_str('plugin_dir')) is None or not pathlib.Path(plugdir_str).is_dir():
self.set("plugin_dir", str(self.default_plugin_dir_path))
plugdir_str = self.default_plugin_dir
self.plugin_dir_path = pathlib.Path(plugdir_str)
self.plugin_dir_path.mkdir(exist_ok=True)
if (outdir := self.get_str('outdir')) is None or not pathlib.Path(outdir).is_dir():
self.set('outdir', self.home)

View File

@ -7,41 +7,23 @@ See LICENSE file.
"""
from __future__ import annotations
import ctypes
import functools
import pathlib
import sys
import uuid
import winreg
from ctypes.wintypes import DWORD, HANDLE
from typing import Literal
from config import AbstractConfig, applongname, appname, logger
from win32comext.shell import shell
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) -> str | None:
"""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
return shell.SHGetKnownFolderPath(guid, 0, 0)
class WinConfig(AbstractConfig):
@ -49,24 +31,6 @@ class WinConfig(AbstractConfig):
def __init__(self) -> None:
super().__init__()
self.app_dir_path = pathlib.Path(known_folder_path(FOLDERID_LocalAppData)) / appname # type: ignore
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_path = pathlib.Path(
known_folder_path(FOLDERID_SavedGames)) / 'Frontier Developments' / 'Elite Dangerous' # type: ignore
self.default_journal_dir_path = journal_dir_path if journal_dir_path.is_dir() else None # type: ignore
REGISTRY_SUBKEY = r'Software\Marginal\EDMarketConnector' # noqa: N806
create_key_defaults = functools.partial(
@ -82,9 +46,33 @@ class WinConfig(AbstractConfig):
logger.exception('Could not create required registry keys')
raise
if local_appdata := known_folder_path(shell.FOLDERID_LocalAppData):
self.app_dir_path = pathlib.Path(local_appdata) / appname
self.app_dir_path.mkdir(exist_ok=True)
self.default_plugin_dir_path = self.app_dir_path / 'plugins'
if (plugdir_str := self.get_str('plugin_dir')) is None or not pathlib.Path(plugdir_str).is_dir():
self.set("plugin_dir", str(self.default_plugin_dir_path))
plugdir_str = self.default_plugin_dir
self.plugin_dir_path = pathlib.Path(plugdir_str)
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_path = pathlib.Path(
known_folder_path(shell.FOLDERID_SavedGames)) / 'Frontier Developments' / 'Elite Dangerous' # type: ignore
self.default_journal_dir_path = journal_dir_path if journal_dir_path.is_dir() else None # type: ignore
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)
docs = known_folder_path(shell.FOLDERID_Documents)
self.set("outdir", docs if docs is not None else self.home)
def __get_regentry(self, key: str) -> None | list | str | int:

View File

@ -51,7 +51,8 @@ if __name__ == "__main__":
for m in list(data['Ships'].values()):
name = coriolis_ship_map.get(m['properties']['name'], str(m['properties']['name']))
assert name in reverse_ship_map, name
ships[name] = {'hullMass': m['properties']['hullMass']}
ships[name] = {'hullMass': m['properties']['hullMass'],
'reserveFuelCapacity': m['properties']['reserveFuelCapacity']}
for i, bulkhead in enumerate(bulkheads):
modules['_'.join([reverse_ship_map[name], 'armour', bulkhead])] = {'mass': m['bulkheads'][i]['mass']}

View File

@ -12,7 +12,7 @@ import sys
import time
import tkinter as tk
from calendar import timegm
from os.path import getsize, isdir, isfile, join
from pathlib import Path
from typing import Any, cast
from watchdog.observers.api import BaseObserver
from config import config
@ -57,7 +57,7 @@ class Dashboard(FileSystemEventHandler):
logdir = config.get_str('journaldir', default=config.default_journal_dir)
logdir = logdir or config.default_journal_dir
if not isdir(logdir):
if not Path.is_dir(Path(logdir)):
logger.info(f"No logdir, or it isn't a directory: {logdir=}")
self.stop()
return False
@ -164,7 +164,8 @@ class Dashboard(FileSystemEventHandler):
:param event: Watchdog event.
"""
if event.is_directory or (isfile(event.src_path) and getsize(event.src_path)):
modpath = Path(event.src_path)
if event.is_directory or (modpath.is_file() and modpath.stat().st_size):
# Can get on_modified events when the file is emptied
self.process(event.src_path if not event.is_directory else None)
@ -177,7 +178,7 @@ class Dashboard(FileSystemEventHandler):
if config.shutting_down:
return
try:
status_json_path = join(self.currentdir, 'Status.json')
status_json_path = Path(self.currentdir) / 'Status.json'
with open(status_json_path, 'rb') as h:
data = h.read().strip()
if data: # Can be empty if polling while the file is being re-written

View File

@ -4,21 +4,19 @@ from __future__ import annotations
import gzip
import json
import pathlib
import tempfile
import threading
import zlib
from http import server
from typing import Any, Callable, Literal
from urllib.parse import parse_qs
from config import appname
import config
from EDMCLogging import get_main_logger
logger = get_main_logger()
output_lock = threading.Lock()
output_data_path = pathlib.Path(tempfile.gettempdir()) / f'{appname}' / 'http_debug'
SAFE_TRANSLATE = str.maketrans({x: '_' for x in "!@#$%^&*()./\\\r\n[]-+='\";:?<>,~`"})
output_data_path = pathlib.Path(config.app_dir_path / 'logs' / 'http_debug')
SAFE_TRANSLATE = str.maketrans(dict.fromkeys("!@#$%^&*()./\\\r\n[]-+='\";:?<>,~`", '_'))
class LoggingHandler(server.BaseHTTPRequestHandler):

View File

@ -1,28 +0,0 @@
Copyright (c) .NET Foundation and contributors.
This software is released under the Microsoft Reciprocal License (MS-RL) (the "License"); you may not use the software except in compliance with the License.
The text of the Microsoft Reciprocal License (MS-RL) can be found online at:
http://opensource.org/licenses/ms-rl
Microsoft Reciprocal License (MS-RL)
This license governs use of the accompanying software. If you use the software, you accept this license. If you do not accept the license, do not use the software.
1. Definitions
The terms "reproduce," "reproduction," "derivative works," and "distribution" have the same meaning here as under U.S. copyright law.
A "contribution" is the original software, or any additions or changes to the software.
A "contributor" is any person that distributes its contribution under this license.
"Licensed patents" are a contributor's patent claims that read directly on its contribution.
2. Grant of Rights
(A) Copyright Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free copyright license to reproduce its contribution, prepare derivative works of its contribution, and distribute its contribution or any derivative works that you create.
(B) Patent Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free license under its licensed patents to make, have made, use, sell, offer for sale, import, and/or otherwise dispose of its contribution in the software or derivative works of the contribution in the software.
3. Conditions and Limitations
(A) Reciprocal Grants- For any file you distribute that contains code from the software (in source code or binary format), you must provide recipients the source code to that file along with a copy of this license, which license will govern that file. You may license other files that are entirely your own work and do not contain code from the software under any terms you choose.
(B) No Trademark License- This license does not grant you rights to use any contributors' name, logo, or trademarks.
(C) If you bring a patent claim against any contributor over patents that you claim are infringed by the software, your patent license from such contributor to the software ends automatically.
(D) If you distribute any portion of the software, you must retain all copyright, patent, trademark, and attribution notices that are present in the software.
(E) If you distribute any portion of the software in source code form, you may do so only under this license by including a complete copy of this license with your distribution. If you distribute any portion of the software in compiled or object code form, you may only do so under a license that complies with this license.
(F) The software is licensed "as-is." You bear the risk of using it. The contributors give no express warranties, guarantees or conditions. You may have additional consumer rights under your local laws which this license cannot change. To the extent permitted under your local laws, the contributors exclude the implied warranties of merchantability, fitness for a particular purpose and non-infringement.

View File

@ -0,0 +1,30 @@
The Python Imaging Library (PIL) is
Copyright © 1997-2011 by Secret Labs AB
Copyright © 1995-2011 by Fredrik Lundh and contributors
Pillow is the friendly PIL fork. It is
Copyright © 2010-2024 by Jeffrey A. Clark and contributors
Like PIL, Pillow is licensed under the open source HPND License:
By obtaining, using, and/or copying this software and/or its associated
documentation, you agree that you have read, understood, and will comply
with the following terms and conditions:
Permission to use, copy, modify and distribute this software and its
documentation for any purpose and without fee is hereby granted,
provided that the above copyright notice appears in all copies, and that
both that copyright notice and this permission notice appear in supporting
documentation, and that the name of Secret Labs AB or the author not be
used in advertising or publicity pertaining to distribution of the software
without specific, written prior permission.
SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS
SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS.
IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR BE LIABLE FOR ANY SPECIAL,
INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.

View File

@ -0,0 +1,29 @@
BSD 3-Clause License
Copyright (c) 2009, Jay Loden, Dave Daeschler, Giampaolo Rodola
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the psutil authors nor the names of its contributors
may be used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -0,0 +1,396 @@
Attribution 4.0 International
=======================================================================
Creative Commons Corporation ("Creative Commons") is not a law firm and
does not provide legal services or legal advice. Distribution of
Creative Commons public licenses does not create a lawyer-client or
other relationship. Creative Commons makes its licenses and related
information available on an "as-is" basis. Creative Commons gives no
warranties regarding its licenses, any material licensed under their
terms and conditions, or any related information. Creative Commons
disclaims all liability for damages resulting from their use to the
fullest extent possible.
Using Creative Commons Public Licenses
Creative Commons public licenses provide a standard set of terms and
conditions that creators and other rights holders may use to share
original works of authorship and other material subject to copyright
and certain other rights specified in the public license below. The
following considerations are for informational purposes only, are not
exhaustive, and do not form part of our licenses.
Considerations for licensors: Our public licenses are
intended for use by those authorized to give the public
permission to use material in ways otherwise restricted by
copyright and certain other rights. Our licenses are
irrevocable. Licensors should read and understand the terms
and conditions of the license they choose before applying it.
Licensors should also secure all rights necessary before
applying our licenses so that the public can reuse the
material as expected. Licensors should clearly mark any
material not subject to the license. This includes other CC-
licensed material, or material used under an exception or
limitation to copyright. More considerations for licensors:
wiki.creativecommons.org/Considerations_for_licensors
Considerations for the public: By using one of our public
licenses, a licensor grants the public permission to use the
licensed material under specified terms and conditions. If
the licensor's permission is not necessary for any reason--for
example, because of any applicable exception or limitation to
copyright--then that use is not regulated by the license. Our
licenses grant only permissions under copyright and certain
other rights that a licensor has authority to grant. Use of
the licensed material may still be restricted for other
reasons, including because others have copyright or other
rights in the material. A licensor may make special requests,
such as asking that all changes be marked or described.
Although not required by our licenses, you are encouraged to
respect those requests where reasonable. More considerations
for the public:
wiki.creativecommons.org/Considerations_for_licensees
=======================================================================
Creative Commons Attribution 4.0 International Public License
By exercising the Licensed Rights (defined below), You accept and agree
to be bound by the terms and conditions of this Creative Commons
Attribution 4.0 International Public License ("Public License"). To the
extent this Public License may be interpreted as a contract, You are
granted the Licensed Rights in consideration of Your acceptance of
these terms and conditions, and the Licensor grants You such rights in
consideration of benefits the Licensor receives from making the
Licensed Material available under these terms and conditions.
Section 1 -- Definitions.
a. Adapted Material means material subject to Copyright and Similar
Rights that is derived from or based upon the Licensed Material
and in which the Licensed Material is translated, altered,
arranged, transformed, or otherwise modified in a manner requiring
permission under the Copyright and Similar Rights held by the
Licensor. For purposes of this Public License, where the Licensed
Material is a musical work, performance, or sound recording,
Adapted Material is always produced where the Licensed Material is
synched in timed relation with a moving image.
b. Adapter's License means the license You apply to Your Copyright
and Similar Rights in Your contributions to Adapted Material in
accordance with the terms and conditions of this Public License.
c. Copyright and Similar Rights means copyright and/or similar rights
closely related to copyright including, without limitation,
performance, broadcast, sound recording, and Sui Generis Database
Rights, without regard to how the rights are labeled or
categorized. For purposes of this Public License, the rights
specified in Section 2(b)(1)-(2) are not Copyright and Similar
Rights.
d. Effective Technological Measures means those measures that, in the
absence of proper authority, may not be circumvented under laws
fulfilling obligations under Article 11 of the WIPO Copyright
Treaty adopted on December 20, 1996, and/or similar international
agreements.
e. Exceptions and Limitations means fair use, fair dealing, and/or
any other exception or limitation to Copyright and Similar Rights
that applies to Your use of the Licensed Material.
f. Licensed Material means the artistic or literary work, database,
or other material to which the Licensor applied this Public
License.
g. Licensed Rights means the rights granted to You subject to the
terms and conditions of this Public License, which are limited to
all Copyright and Similar Rights that apply to Your use of the
Licensed Material and that the Licensor has authority to license.
h. Licensor means the individual(s) or entity(ies) granting rights
under this Public License.
i. Share means to provide material to the public by any means or
process that requires permission under the Licensed Rights, such
as reproduction, public display, public performance, distribution,
dissemination, communication, or importation, and to make material
available to the public including in ways that members of the
public may access the material from a place and at a time
individually chosen by them.
j. Sui Generis Database Rights means rights other than copyright
resulting from Directive 96/9/EC of the European Parliament and of
the Council of 11 March 1996 on the legal protection of databases,
as amended and/or succeeded, as well as other essentially
equivalent rights anywhere in the world.
k. You means the individual or entity exercising the Licensed Rights
under this Public License. Your has a corresponding meaning.
Section 2 -- Scope.
a. License grant.
1. Subject to the terms and conditions of this Public License,
the Licensor hereby grants You a worldwide, royalty-free,
non-sublicensable, non-exclusive, irrevocable license to
exercise the Licensed Rights in the Licensed Material to:
a. reproduce and Share the Licensed Material, in whole or
in part; and
b. produce, reproduce, and Share Adapted Material.
2. Exceptions and Limitations. For the avoidance of doubt, where
Exceptions and Limitations apply to Your use, this Public
License does not apply, and You do not need to comply with
its terms and conditions.
3. Term. The term of this Public License is specified in Section
6(a).
4. Media and formats; technical modifications allowed. The
Licensor authorizes You to exercise the Licensed Rights in
all media and formats whether now known or hereafter created,
and to make technical modifications necessary to do so. The
Licensor waives and/or agrees not to assert any right or
authority to forbid You from making technical modifications
necessary to exercise the Licensed Rights, including
technical modifications necessary to circumvent Effective
Technological Measures. For purposes of this Public License,
simply making modifications authorized by this Section 2(a)
(4) never produces Adapted Material.
5. Downstream recipients.
a. Offer from the Licensor -- Licensed Material. Every
recipient of the Licensed Material automatically
receives an offer from the Licensor to exercise the
Licensed Rights under the terms and conditions of this
Public License.
b. No downstream restrictions. You may not offer or impose
any additional or different terms or conditions on, or
apply any Effective Technological Measures to, the
Licensed Material if doing so restricts exercise of the
Licensed Rights by any recipient of the Licensed
Material.
6. No endorsement. Nothing in this Public License constitutes or
may be construed as permission to assert or imply that You
are, or that Your use of the Licensed Material is, connected
with, or sponsored, endorsed, or granted official status by,
the Licensor or others designated to receive attribution as
provided in Section 3(a)(1)(A)(i).
b. Other rights.
1. Moral rights, such as the right of integrity, are not
licensed under this Public License, nor are publicity,
privacy, and/or other similar personality rights; however, to
the extent possible, the Licensor waives and/or agrees not to
assert any such rights held by the Licensor to the limited
extent necessary to allow You to exercise the Licensed
Rights, but not otherwise.
2. Patent and trademark rights are not licensed under this
Public License.
3. To the extent possible, the Licensor waives any right to
collect royalties from You for the exercise of the Licensed
Rights, whether directly or through a collecting society
under any voluntary or waivable statutory or compulsory
licensing scheme. In all other cases the Licensor expressly
reserves any right to collect such royalties.
Section 3 -- License Conditions.
Your exercise of the Licensed Rights is expressly made subject to the
following conditions.
a. Attribution.
1. If You Share the Licensed Material (including in modified
form), You must:
a. retain the following if it is supplied by the Licensor
with the Licensed Material:
i. identification of the creator(s) of the Licensed
Material and any others designated to receive
attribution, in any reasonable manner requested by
the Licensor (including by pseudonym if
designated);
ii. a copyright notice;
iii. a notice that refers to this Public License;
iv. a notice that refers to the disclaimer of
warranties;
v. a URI or hyperlink to the Licensed Material to the
extent reasonably practicable;
b. indicate if You modified the Licensed Material and
retain an indication of any previous modifications; and
c. indicate the Licensed Material is licensed under this
Public License, and include the text of, or the URI or
hyperlink to, this Public License.
2. You may satisfy the conditions in Section 3(a)(1) in any
reasonable manner based on the medium, means, and context in
which You Share the Licensed Material. For example, it may be
reasonable to satisfy the conditions by providing a URI or
hyperlink to a resource that includes the required
information.
3. If requested by the Licensor, You must remove any of the
information required by Section 3(a)(1)(A) to the extent
reasonably practicable.
4. If You Share Adapted Material You produce, the Adapter's
License You apply must not prevent recipients of the Adapted
Material from complying with this Public License.
Section 4 -- Sui Generis Database Rights.
Where the Licensed Rights include Sui Generis Database Rights that
apply to Your use of the Licensed Material:
a. for the avoidance of doubt, Section 2(a)(1) grants You the right
to extract, reuse, reproduce, and Share all or a substantial
portion of the contents of the database;
b. if You include all or a substantial portion of the database
contents in a database in which You have Sui Generis Database
Rights, then the database in which You have Sui Generis Database
Rights (but not its individual contents) is Adapted Material; and
c. You must comply with the conditions in Section 3(a) if You Share
all or a substantial portion of the contents of the database.
For the avoidance of doubt, this Section 4 supplements and does not
replace Your obligations under this Public License where the Licensed
Rights include other Copyright and Similar Rights.
Section 5 -- Disclaimer of Warranties and Limitation of Liability.
a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
c. The disclaimer of warranties and limitation of liability provided
above shall be interpreted in a manner that, to the extent
possible, most closely approximates an absolute disclaimer and
waiver of all liability.
Section 6 -- Term and Termination.
a. This Public License applies for the term of the Copyright and
Similar Rights licensed here. However, if You fail to comply with
this Public License, then Your rights under this Public License
terminate automatically.
b. Where Your right to use the Licensed Material has terminated under
Section 6(a), it reinstates:
1. automatically as of the date the violation is cured, provided
it is cured within 30 days of Your discovery of the
violation; or
2. upon express reinstatement by the Licensor.
For the avoidance of doubt, this Section 6(b) does not affect any
right the Licensor may have to seek remedies for Your violations
of this Public License.
c. For the avoidance of doubt, the Licensor may also offer the
Licensed Material under separate terms or conditions or stop
distributing the Licensed Material at any time; however, doing so
will not terminate this Public License.
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
License.
Section 7 -- Other Terms and Conditions.
a. The Licensor shall not be bound by any additional or different
terms or conditions communicated by You unless expressly agreed.
b. Any arrangements, understandings, or agreements regarding the
Licensed Material not stated herein are separate from and
independent of the terms and conditions of this Public License.
Section 8 -- Interpretation.
a. For the avoidance of doubt, this Public License does not, and
shall not be interpreted to, reduce, limit, restrict, or impose
conditions on any use of the Licensed Material that could lawfully
be made without permission under this Public License.
b. To the extent possible, if any provision of this Public License is
deemed unenforceable, it shall be automatically reformed to the
minimum extent necessary to make it enforceable. If the provision
cannot be reformed, it shall be severed from this Public License
without affecting the enforceability of the remaining terms and
conditions.
c. No term or condition of this Public License will be waived and no
failure to comply consented to unless expressly agreed to by the
Licensor.
d. Nothing in this Public License constitutes or may be interpreted
as a limitation upon, or waiver of, any privileges and immunities
that apply to the Licensor or You, including from the legal
processes of any jurisdiction or authority.
=======================================================================
Creative Commons is not a party to its public
licenses. Notwithstanding, Creative Commons may elect to apply one of
its public licenses to material it publishes and in those instances
will be considered the “Licensor.” The text of the Creative Commons
public licenses is dedicated to the public domain under the CC0 Public
Domain Dedication. Except for the limited purpose of indicating that
material is shared under a Creative Commons public license or as
otherwise permitted by the Creative Commons policies published at
creativecommons.org/policies, Creative Commons does not authorize the
use of the trademark "Creative Commons" or any other trademark or logo
of Creative Commons without its prior written consent including,
without limitation, in connection with any unauthorized modifications
to any of its public licenses or any other arrangements,
understandings, or agreements concerning use of licensed material. For
the avoidance of doubt, this paragraph does not form part of the
public licenses.
Creative Commons may be contacted at creativecommons.org.

View File

@ -3,10 +3,10 @@
"""Plugin that tests that modules we bundle for plugins are present and working."""
import logging
import os
import shutil
import sqlite3
import zipfile
from pathlib import Path
import semantic_version
from SubA import SubA
@ -14,7 +14,7 @@ from SubA import SubA
from config import appname, appversion, config
# This could also be returned from plugin_start3()
plugin_name = os.path.basename(os.path.dirname(__file__))
plugin_name = Path(__file__).resolve().parent.name
# Logger per found plugin, so the folder name is included in
# the logging format.
@ -49,17 +49,17 @@ class PluginTest:
def __init__(self, directory: str):
logger.debug(f'directory = "{directory}')
dbfile = os.path.join(directory, this.DBFILE)
dbfile = Path(directory) / this.DBFILE
# Test 'import zipfile'
with zipfile.ZipFile(dbfile + '.zip', 'w') as zip:
if os.path.exists(dbfile):
with zipfile.ZipFile(str(dbfile) + '.zip', 'w') as zip:
if dbfile.exists():
zip.write(dbfile)
zip.close()
# Testing 'import shutil'
if os.path.exists(dbfile):
shutil.copyfile(dbfile, dbfile + '.bak')
if dbfile.exists():
shutil.copyfile(dbfile, str(dbfile) + '.bak')
# Testing 'import sqlite3'
self.sqlconn = sqlite3.connect(dbfile)

View File

@ -507,6 +507,7 @@ ship_name_map = {
'testbuggy': 'Scarab',
'type6': 'Type-6 Transporter',
'type7': 'Type-7 Transporter',
'type8': 'Type-8 Transporter',
'type9': 'Type-9 Heavy',
'type9_military': 'Type-10 Defender',
'typex': 'Alliance Chieftain',

View File

@ -8,7 +8,11 @@ import sys
import threading
import tkinter as tk
import winsound
from ctypes.wintypes import DWORD, HWND, LONG, LPWSTR, MSG, ULONG, WORD
from ctypes.wintypes import DWORD, LONG, MSG, ULONG, WORD, HWND, BOOL, UINT
import pywintypes
import win32api
import win32gui
import win32con
from config import config
from EDMCLogging import get_main_logger
from hotkey import AbstractHotkeyMgr
@ -17,53 +21,22 @@ assert sys.platform == 'win32'
logger = get_main_logger()
RegisterHotKey = ctypes.windll.user32.RegisterHotKey
UnregisterHotKey = ctypes.windll.user32.UnregisterHotKey
MOD_ALT = 0x0001
MOD_CONTROL = 0x0002
MOD_SHIFT = 0x0004
MOD_WIN = 0x0008
UnregisterHotKey = ctypes.windll.user32.UnregisterHotKey # TODO: Coming Soon
UnregisterHotKey.argtypes = [HWND, ctypes.c_int]
UnregisterHotKey.restype = BOOL
MOD_NOREPEAT = 0x4000
GetMessage = ctypes.windll.user32.GetMessageW
TranslateMessage = ctypes.windll.user32.TranslateMessage
DispatchMessage = ctypes.windll.user32.DispatchMessageW
PostThreadMessage = ctypes.windll.user32.PostThreadMessageW
WM_QUIT = 0x0012
WM_HOTKEY = 0x0312
WM_APP = 0x8000
WM_SND_GOOD = WM_APP + 1
WM_SND_BAD = WM_APP + 2
GetKeyState = ctypes.windll.user32.GetKeyState
MapVirtualKey = ctypes.windll.user32.MapVirtualKeyW
VK_BACK = 0x08
VK_CLEAR = 0x0c
VK_RETURN = 0x0d
VK_SHIFT = 0x10
VK_CONTROL = 0x11
VK_MENU = 0x12
VK_CAPITAL = 0x14
VK_MODECHANGE = 0x1f
VK_ESCAPE = 0x1b
VK_SPACE = 0x20
VK_DELETE = 0x2e
VK_LWIN = 0x5b
VK_RWIN = 0x5c
VK_NUMPAD0 = 0x60
VK_DIVIDE = 0x6f
VK_F1 = 0x70
VK_F24 = 0x87
WM_SND_GOOD = win32con.WM_APP + 1
WM_SND_BAD = win32con.WM_APP + 2
VK_OEM_MINUS = 0xbd
VK_NUMLOCK = 0x90
VK_SCROLL = 0x91
VK_PROCESSKEY = 0xe5
VK_OEM_CLEAR = 0xfe
GetForegroundWindow = ctypes.windll.user32.GetForegroundWindow
GetWindowText = ctypes.windll.user32.GetWindowTextW
GetWindowText.argtypes = [HWND, LPWSTR, ctypes.c_int]
GetWindowTextLength = ctypes.windll.user32.GetWindowTextLengthW
# VirtualKey mapping values
# <https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-mapvirtualkeyexa>
MAPVK_VK_TO_VSC = 0
MAPVK_VSC_TO_VK = 1
MAPVK_VK_TO_CHAR = 2
MAPVK_VSC_TO_VK_EX = 3
MAPVK_VK_TO_VSC_EX = 4
def window_title(h) -> str:
@ -74,11 +47,7 @@ def window_title(h) -> str:
:return: Window title.
"""
if h:
title_length = GetWindowTextLength(h) + 1
buf = ctypes.create_unicode_buffer(title_length)
if GetWindowText(h, buf, title_length):
return buf.value
return win32gui.GetWindowText(h)
return ''
@ -138,6 +107,7 @@ class INPUT(ctypes.Structure):
SendInput = ctypes.windll.user32.SendInput
SendInput.argtypes = [ctypes.c_uint, ctypes.POINTER(INPUT), ctypes.c_int]
SendInput.restype = UINT
INPUT_MOUSE = 0
INPUT_KEYBOARD = 1
@ -197,7 +167,7 @@ class WindowsHotkeyMgr(AbstractHotkeyMgr):
logger.debug('Thread is/was running')
self.thread = None # type: ignore
logger.debug('Telling thread WM_QUIT')
PostThreadMessage(thread.ident, WM_QUIT, 0, 0)
win32gui.PostThreadMessage(thread.ident, win32con.WM_QUIT, 0, 0)
logger.debug('Joining thread')
thread.join() # Wait for it to unregister hotkey and quit
@ -210,8 +180,10 @@ class WindowsHotkeyMgr(AbstractHotkeyMgr):
"""Handle hotkeys."""
logger.debug('Begin...')
# Hotkey must be registered by the thread that handles it
if not RegisterHotKey(None, 1, modifiers | MOD_NOREPEAT, keycode):
logger.debug("We're not the right thread?")
try:
win32gui.RegisterHotKey(None, 1, modifiers | MOD_NOREPEAT, keycode)
except pywintypes.error:
logger.exception("We're not the right thread?")
self.thread = None # type: ignore
return
@ -219,14 +191,14 @@ class WindowsHotkeyMgr(AbstractHotkeyMgr):
msg = MSG()
logger.debug('Entering GetMessage() loop...')
while GetMessage(ctypes.byref(msg), None, 0, 0) != 0:
while win32gui.GetMessage(ctypes.byref(msg), None, 0, 0) != 0:
logger.debug('Got message')
if msg.message == WM_HOTKEY:
if msg.message == win32con.WM_HOTKEY:
logger.debug('WM_HOTKEY')
if (
config.get_int('hotkey_always')
or window_title(GetForegroundWindow()).startswith('Elite - Dangerous')
config.get_int('hotkey_always')
or window_title(win32gui.GetForegroundWindow()).startswith('Elite - Dangerous')
):
if not config.shutting_down:
logger.debug('Sending event <<Invoke>>')
@ -236,8 +208,10 @@ class WindowsHotkeyMgr(AbstractHotkeyMgr):
logger.debug('Passing key on')
UnregisterHotKey(None, 1)
SendInput(1, fake, ctypes.sizeof(INPUT))
if not RegisterHotKey(None, 1, modifiers | MOD_NOREPEAT, keycode):
logger.debug("We aren't registered for this ?")
try:
win32gui.RegisterHotKey(None, 1, modifiers | MOD_NOREPEAT, keycode)
except pywintypes.error:
logger.exception("We aren't registered for this ?")
break
elif msg.message == WM_SND_GOOD:
@ -250,8 +224,8 @@ class WindowsHotkeyMgr(AbstractHotkeyMgr):
else:
logger.debug('Something else')
TranslateMessage(ctypes.byref(msg))
DispatchMessage(ctypes.byref(msg))
win32gui.TranslateMessage(ctypes.byref(msg))
win32gui.DispatchMessage(ctypes.byref(msg))
logger.debug('Exited GetMessage() loop.')
UnregisterHotKey(None, 1)
@ -277,40 +251,42 @@ class WindowsHotkeyMgr(AbstractHotkeyMgr):
:param event: tk event ?
:return: False to retain previous, None to not use, else (keycode, modifiers)
"""
modifiers = ((GetKeyState(VK_MENU) & 0x8000) and MOD_ALT) \
| ((GetKeyState(VK_CONTROL) & 0x8000) and MOD_CONTROL) \
| ((GetKeyState(VK_SHIFT) & 0x8000) and MOD_SHIFT) \
| ((GetKeyState(VK_LWIN) & 0x8000) and MOD_WIN) \
| ((GetKeyState(VK_RWIN) & 0x8000) and MOD_WIN)
modifiers = ((win32api.GetKeyState(win32con.VK_MENU) & 0x8000) and win32con.MOD_ALT) \
| ((win32api.GetKeyState(win32con.VK_CONTROL) & 0x8000) and win32con.MOD_CONTROL) \
| ((win32api.GetKeyState(win32con.VK_SHIFT) & 0x8000) and win32con.MOD_SHIFT) \
| ((win32api.GetKeyState(win32con.VK_LWIN) & 0x8000) and win32con.MOD_WIN) \
| ((win32api.GetKeyState(win32con.VK_RWIN) & 0x8000) and win32con.MOD_WIN)
keycode = event.keycode
if keycode in (VK_SHIFT, VK_CONTROL, VK_MENU, VK_LWIN, VK_RWIN):
if keycode in (win32con.VK_SHIFT, win32con.VK_CONTROL, win32con.VK_MENU, win32con.VK_LWIN, win32con.VK_RWIN):
return 0, modifiers
if not modifiers:
if keycode == VK_ESCAPE: # Esc = retain previous
if keycode == win32con.VK_ESCAPE: # Esc = retain previous
return False
if keycode in (VK_BACK, VK_DELETE, VK_CLEAR, VK_OEM_CLEAR): # BkSp, Del, Clear = clear hotkey
if keycode in (win32con.VK_BACK, win32con.VK_DELETE,
win32con.VK_CLEAR, win32con.VK_OEM_CLEAR): # BkSp, Del, Clear = clear hotkey
return None
if (
keycode in (VK_RETURN, VK_SPACE, VK_OEM_MINUS) or ord('A') <= keycode <= ord('Z')
keycode in (win32con.VK_RETURN, win32con.VK_SPACE, VK_OEM_MINUS) or ord('A') <= keycode <= ord('Z')
): # don't allow keys needed for typing in System Map
winsound.MessageBeep()
return None
if (keycode in (VK_NUMLOCK, VK_SCROLL, VK_PROCESSKEY)
or VK_CAPITAL <= keycode <= VK_MODECHANGE): # ignore unmodified mode switch keys
if (keycode in (win32con.VK_NUMLOCK, win32con.VK_SCROLL, win32con.VK_PROCESSKEY)
or win32con.VK_CAPITAL <= keycode <= win32con.VK_MODECHANGE): # ignore unmodified mode switch keys
return 0, modifiers
# See if the keycode is usable and available
if RegisterHotKey(None, 2, modifiers | MOD_NOREPEAT, keycode):
try:
win32gui.RegisterHotKey(None, 2, modifiers | MOD_NOREPEAT, keycode)
UnregisterHotKey(None, 2)
return keycode, modifiers
winsound.MessageBeep()
return None
except pywintypes.error:
winsound.MessageBeep()
return None
def display(self, keycode, modifiers) -> str:
"""
@ -321,32 +297,32 @@ class WindowsHotkeyMgr(AbstractHotkeyMgr):
:return: string form
"""
text = ''
if modifiers & MOD_WIN:
if modifiers & win32con.MOD_WIN:
text += '❖+'
if modifiers & MOD_CONTROL:
if modifiers & win32con.MOD_CONTROL:
text += 'Ctrl+'
if modifiers & MOD_ALT:
if modifiers & win32con.MOD_ALT:
text += 'Alt+'
if modifiers & MOD_SHIFT:
if modifiers & win32con.MOD_SHIFT:
text += '⇧+'
if VK_NUMPAD0 <= keycode <= VK_DIVIDE:
if win32con.VK_NUMPAD0 <= keycode <= win32con.VK_DIVIDE:
text += ''
if not keycode:
pass
elif VK_F1 <= keycode <= VK_F24:
text += f'F{keycode + 1 - VK_F1}'
elif win32con.VK_F1 <= keycode <= win32con.VK_F24:
text += f'F{keycode + 1 - win32con.VK_F1}'
elif keycode in WindowsHotkeyMgr.DISPLAY: # specials
text += WindowsHotkeyMgr.DISPLAY[keycode]
else:
c = MapVirtualKey(keycode, 2) # printable ?
c = win32api.MapVirtualKey(keycode, MAPVK_VK_TO_CHAR)
if not c: # oops not printable
text += ''
@ -361,9 +337,9 @@ class WindowsHotkeyMgr(AbstractHotkeyMgr):
def play_good(self) -> None:
"""Play the 'good' sound."""
if self.thread:
PostThreadMessage(self.thread.ident, WM_SND_GOOD, 0, 0)
win32gui.PostThreadMessage(self.thread.ident, WM_SND_GOOD, 0, 0)
def play_bad(self) -> None:
"""Play the 'bad' sound."""
if self.thread:
PostThreadMessage(self.thread.ident, WM_SND_BAD, 0, 0)
win32gui.PostThreadMessage(self.thread.ident, WM_SND_BAD, 0, 0)

View File

@ -5,23 +5,23 @@ Copyright (c) EDCD, All Rights Reserved
Licensed under the GNU General Public License.
See LICENSE file.
"""
import os
import subprocess
from pathlib import Path
from build import build
def run_inno_setup_installer(iss_path: str) -> None:
def run_inno_setup_installer(iss_path: Path) -> None:
"""Run the Inno installer, building the installation exe."""
# Get the path to the Inno Setup compiler (iscc.exe) (Currently set to default path)
inno_setup_compiler_path: str = "C:\\Program Files (x86)\\Inno Setup 6\\ISCC.exe"
inno_setup_compiler_path = Path("C:\\Program Files (x86)\\Inno Setup 6\\ISCC.exe")
# Check if the Inno Setup compiler executable exists
if not os.path.isfile(inno_setup_compiler_path):
if not inno_setup_compiler_path.exists():
print(f"Error: Inno Setup compiler not found at '{inno_setup_compiler_path}'.")
return
# Check if the provided .iss file exists
if not os.path.isfile(iss_file_path):
if not iss_file_path.exists():
print(f"Error: The provided .iss file '{iss_path}' not found.")
return
@ -40,6 +40,6 @@ def run_inno_setup_installer(iss_path: str) -> None:
if __name__ == "__main__":
build()
# Add the ISS Template File
iss_file_path: str = "./EDMC_Installer_Config.iss"
iss_file_path = Path("./EDMC_Installer_Config.iss")
# Build the ISS file
run_inno_setup_installer(iss_file_path)

45
l10n.py
View File

@ -17,10 +17,9 @@ import re
import sys
import warnings
from contextlib import suppress
from os import listdir, sep, makedirs
from os.path import basename, dirname, isdir, join, abspath, exists
from os import listdir, sep
from typing import TYPE_CHECKING, Iterable, TextIO, cast
import pathlib
from config import config
from EDMCLogging import get_main_logger
@ -35,7 +34,7 @@ logger = get_main_logger()
# Language name
LANGUAGE_ID = '!Language'
LOCALISATION_DIR = 'L10n'
LOCALISATION_DIR: pathlib.Path = pathlib.Path('L10n')
if sys.platform == 'win32':
import ctypes
@ -50,7 +49,6 @@ if sys.platform == 'win32':
GetUserPreferredUILanguages.argtypes = [
DWORD, ctypes.POINTER(ctypes.c_ulong), LPCVOID, ctypes.POINTER(ctypes.c_ulong)
]
GetUserPreferredUILanguages.restype = BOOL
LOCALE_NAME_USER_DEFAULT = None
@ -119,10 +117,10 @@ class Translations:
self.translations = {None: self.contents(cast(str, lang))}
for plugin in listdir(config.plugin_dir_path):
plugin_path = join(config.plugin_dir_path, plugin, LOCALISATION_DIR)
if isdir(plugin_path):
plugin_path = config.plugin_dir_path / plugin / LOCALISATION_DIR
if pathlib.Path.is_dir(plugin_path):
try:
self.translations[plugin] = self.contents(cast(str, lang), str(plugin_path))
self.translations[plugin] = self.contents(cast(str, lang), plugin_path)
except UnicodeDecodeError as e:
logger.warning(f'Malformed file {lang}.strings in plugin {plugin}: {e}')
@ -133,7 +131,7 @@ class Translations:
# DEPRECATED: Migrate to translations.translate or tr.tl. Will remove in 6.0 or later.
builtins.__dict__['_'] = self.translate
def contents(self, lang: str, plugin_path: str | None = None) -> dict[str, str]:
def contents(self, lang: str, plugin_path: pathlib.Path | None = None) -> dict[str, str]:
"""Load all the translations from a translation file."""
assert lang in self.available()
translations = {}
@ -173,12 +171,12 @@ class Translations:
:return: The translated string
"""
plugin_name: str | None = None
plugin_path: str | None = None
plugin_path: pathlib.Path | None = None
if context:
# TODO: There is probably a better way to go about this now.
plugin_name = context[len(config.plugin_dir)+1:].split(sep)[0]
plugin_path = join(config.plugin_dir_path, plugin_name, LOCALISATION_DIR)
plugin_path = config.plugin_dir_path / plugin_name / LOCALISATION_DIR
if lang:
contents: dict[str, str] = self.contents(lang=lang, plugin_path=plugin_path)
@ -225,17 +223,17 @@ class Translations:
return names
def respath(self) -> str:
def respath(self) -> pathlib.Path:
"""Path to localisation files."""
if getattr(sys, 'frozen', False):
return abspath(join(dirname(sys.executable), LOCALISATION_DIR))
return pathlib.Path(sys.executable).parent.joinpath(LOCALISATION_DIR).resolve()
if __file__:
return abspath(join(dirname(__file__), LOCALISATION_DIR))
return pathlib.Path(__file__).parent.joinpath(LOCALISATION_DIR).resolve()
return abspath(LOCALISATION_DIR)
return LOCALISATION_DIR.resolve()
def file(self, lang: str, plugin_path: str | None = None) -> TextIO | None:
def file(self, lang: str, plugin_path: pathlib.Path | None = None) -> TextIO | None:
"""
Open the given lang file for reading.
@ -244,8 +242,8 @@ class Translations:
:return: the opened file (Note: This should be closed when done)
"""
if plugin_path:
file_path = join(plugin_path, f'{lang}.strings')
if not exists(file_path):
file_path = plugin_path / f"{lang}.strings"
if not file_path.exists():
return None
try:
@ -253,7 +251,7 @@ class Translations:
except OSError:
logger.exception(f'could not open {file_path}')
res_path = join(self.respath(), f'{lang}.strings')
res_path = self.respath() / f'{lang}.strings'
return open(res_path, encoding='utf-8')
@ -382,9 +380,10 @@ Translations: Translations = translations # type: ignore
if __name__ == "__main__":
regexp = re.compile(r'''_\([ur]?(['"])(((?<!\\)\\\1|.)+?)\1\)[^#]*(#.+)?''') # match a single line python literal
seen: dict[str, str] = {}
plugin_dir = pathlib.Path('plugins')
for f in (
sorted(x for x in listdir('.') if x.endswith('.py')) +
sorted(join('plugins', x) for x in (listdir('plugins') if isdir('plugins') else []) if x.endswith('.py'))
sorted(plugin_dir.glob('*.py')) if plugin_dir.is_dir() else []
):
with open(f, encoding='utf-8') as h:
lineno = 0
@ -393,11 +392,11 @@ if __name__ == "__main__":
match = regexp.search(line)
if match and not seen.get(match.group(2)): # only record first commented instance of a string
seen[match.group(2)] = (
(match.group(4) and (match.group(4)[1:].strip()) + '. ' or '') + f'[{basename(f)}]'
(match.group(4) and (match.group(4)[1:].strip()) + '. ' or '') + f'[{pathlib.Path(f).name}]'
)
if seen:
target_path = join(LOCALISATION_DIR, 'en.template.new')
makedirs(dirname(target_path), exist_ok=True)
target_path = LOCALISATION_DIR / 'en.template.new'
target_path.parent.mkdir(parents=True, exist_ok=True)
with open(target_path, 'w', encoding='utf-8') as target_file:
target_file.write(f'/* Language name */\n"{LANGUAGE_ID}" = "English";\n\n')
for thing in sorted(seen, key=str.lower):

View File

@ -11,7 +11,7 @@ import json
import re
import time
from os import listdir
from os.path import join
from pathlib import Path
import companion
import util_ships
from config import config
@ -45,7 +45,7 @@ def export(data: companion.CAPIData, requested_filename: str | None = None) -> N
regexp = re.compile(re.escape(ship) + r'\.\d\d\d\d-\d\d-\d\dT\d\d\.\d\d\.\d\d\.txt')
oldfiles = sorted([x for x in listdir(config.get_str('outdir')) if regexp.match(x)])
if oldfiles:
with open(join(config.get_str('outdir'), oldfiles[-1]), 'rU') as h:
with open(Path(config.get_str('outdir')) / Path(oldfiles[-1]), 'rU') as h:
if h.read() == string:
return # same as last time - don't write
@ -53,9 +53,9 @@ def export(data: companion.CAPIData, requested_filename: str | None = None) -> N
# Write
output_directory = config.get_str('outdir')
output_directory = Path(config.get_str('outdir'))
ship_time = time.strftime('%Y-%m-%dT%H.%M.%S', time.localtime(query_time))
file_path = join(output_directory, f"{ship}.{ship_time}.txt")
file_path = output_directory / f"{ship}.{ship_time}.txt"
with open(file_path, 'wt') as h:
h.write(string)

View File

@ -8,7 +8,7 @@ See LICENSE file.
from __future__ import annotations
import json
import pathlib
from pathlib import Path
import queue
import re
import sys
@ -16,14 +16,15 @@ import threading
from calendar import timegm
from collections import defaultdict
from os import SEEK_END, SEEK_SET, listdir
from os.path import basename, expanduser, getctime, isdir, join
from time import gmtime, localtime, mktime, sleep, strftime, strptime, time
from typing import TYPE_CHECKING, Any, BinaryIO, MutableMapping
import psutil
import semantic_version
import util_ships
from config import config
from edmc_data import edmc_suit_shortnames, edmc_suit_symbol_localised
from config import config, appname, appversion
from edmc_data import edmc_suit_shortnames, edmc_suit_symbol_localised, ship_name_map
from EDMCLogging import get_main_logger
from edshipyard import ships
if TYPE_CHECKING:
import tkinter
@ -35,26 +36,10 @@ MAX_NAVROUTE_DISCREPANCY = 5 # Timestamp difference in seconds
MAX_FCMATERIALS_DISCREPANCY = 5 # Timestamp difference in seconds
if sys.platform == 'win32':
import ctypes
from ctypes.wintypes import BOOL, HWND, LPARAM, LPWSTR
from watchdog.events import FileSystemEventHandler, FileSystemEvent
from watchdog.observers import Observer
from watchdog.observers.api import BaseObserver
EnumWindows = ctypes.windll.user32.EnumWindows
EnumWindowsProc = ctypes.WINFUNCTYPE(BOOL, HWND, LPARAM)
CloseHandle = ctypes.windll.kernel32.CloseHandle
GetWindowText = ctypes.windll.user32.GetWindowTextW
GetWindowText.argtypes = [HWND, LPWSTR, ctypes.c_int]
GetWindowTextLength = ctypes.windll.user32.GetWindowTextLengthW
GetWindowTextLength.argtypes = [ctypes.wintypes.HWND]
GetWindowTextLength.restype = ctypes.c_int
GetProcessHandleFromHwnd = ctypes.windll.oleacc.GetProcessHandleFromHwnd
else:
# Linux's inotify doesn't work over CIFS or NFS, so poll
FileSystemEventHandler = object # dummy
@ -70,7 +55,8 @@ class EDLogs(FileSystemEventHandler):
"""Monitoring of Journal files."""
# Magic with FileSystemEventHandler can confuse type checkers when they do not have access to every import
_POLL = 1 # Polling is cheap, so do it often
_POLL = 1 # Polling while running is cheap, so do it often
_INACTIVE_POLL = 10 # Polling while not running isn't as cheap, so do it less often
_RE_CANONICALISE = re.compile(r'\$(.+)_name;')
_RE_CATEGORY = re.compile(r'\$MICRORESOURCE_CATEGORY_(.+);')
_RE_LOGFILE = re.compile(r'^Journal(Alpha|Beta)?\.[0-9]{2,4}(-)?[0-9]{2}(-)?[0-9]{2}(T)?[0-9]{2}[0-9]{2}[0-9]{2}'
@ -81,7 +67,7 @@ class EDLogs(FileSystemEventHandler):
# TODO(A_D): A bunch of these should be switched to default values (eg '' for strings) and no longer be Optional
FileSystemEventHandler.__init__(self) # futureproofing - not need for current version of watchdog
self.root: 'tkinter.Tk' = None # type: ignore # Don't use Optional[] - mypy thinks no methods
self.currentdir: str | None = None # The actual logdir that we're monitoring
self.currentdir: Path | None = None # The actual logdir that we're monitoring
self.logfile: str | None = None
self.observer: BaseObserver | None = None
self.observed = None # a watchdog ObservedWatch, or None if polling
@ -100,6 +86,7 @@ class EDLogs(FileSystemEventHandler):
self.catching_up = False
self.game_was_running = False # For generation of the "ShutDown" event
self.running_process = None
# Context for journal handling
self.version: str | None = None
@ -109,6 +96,7 @@ class EDLogs(FileSystemEventHandler):
self.group: str | None = None
self.cmdr: str | None = None
self.started: int | None = None # Timestamp of the LoadGame event
self.slef: str | None = None
self._navroute_retries_remaining = 0
self._last_navroute_journal_timestamp: float | None = None
@ -202,9 +190,9 @@ class EDLogs(FileSystemEventHandler):
if journal_dir == '' or journal_dir is None:
journal_dir = config.default_journal_dir
logdir = expanduser(journal_dir)
logdir = Path(journal_dir).expanduser()
if not logdir or not isdir(logdir):
if not logdir or not Path.is_dir(logdir):
logger.error(f'Journal Directory is invalid: "{logdir}"')
self.stop()
return False
@ -277,9 +265,10 @@ class EDLogs(FileSystemEventHandler):
# Odyssey Update 11 has, e.g. Journal.2022-03-15T152503.01.log
# Horizons Update 11 equivalent: Journal.220315152335.01.log
# So we can no longer use a naive sort.
journals_dir_path = pathlib.Path(journals_dir)
journal_files = (journals_dir_path / pathlib.Path(x) for x in journal_files)
return str(max(journal_files, key=getctime))
journals_dir_path = Path(journals_dir)
journal_files = (journals_dir_path / Path(x) for x in journal_files)
latest_file = max(journal_files, key=lambda f: Path(f).stat().st_ctime)
return str(latest_file)
return None
@ -348,7 +337,7 @@ class EDLogs(FileSystemEventHandler):
def on_created(self, event: 'FileSystemEvent') -> None:
"""Watchdog callback when, e.g. client (re)started."""
if not event.is_directory and self._RE_LOGFILE.search(basename(event.src_path)):
if not event.is_directory and self._RE_LOGFILE.search(Path(event.src_path).name):
self.logfile = event.src_path
@ -472,7 +461,10 @@ class EDLogs(FileSystemEventHandler):
loghandle = open(logfile, 'rb', 0) # unbuffered
log_pos = 0
sleep(self._POLL)
if self.game_was_running:
sleep(self._POLL)
else:
sleep(self._INACTIVE_POLL)
# Check whether we're still supposed to be running
if threading.current_thread() != self.thread:
@ -701,6 +693,34 @@ class EDLogs(FileSystemEventHandler):
module.pop('AmmoInHopper')
self.state['Modules'][module['Slot']] = module
# SLEF
initial_dict: dict[str, dict[str, Any]] = {
"header": {"appName": appname, "appVersion": str(appversion())}
}
data_dict = {}
for module in entry['Modules']:
if module.get('Slot') == 'FuelTank':
cap = module['Item'].split('size')
cap = cap[1].split('_')
cap = 2 ** int(cap[0])
ship = ship_name_map[entry["Ship"]]
fuel = {'Main': cap, 'Reserve': ships[ship]['reserveFuelCapacity']}
data_dict.update({"FuelCapacity": fuel})
data_dict.update({
'Ship': entry["Ship"],
'ShipName': entry['ShipName'],
'ShipIdent': entry['ShipIdent'],
'HullValue': entry['HullValue'],
'ModulesValue': entry['ModulesValue'],
'Rebuy': entry['Rebuy'],
'MaxJumpRange': entry['MaxJumpRange'],
'UnladenMass': entry['UnladenMass'],
'CargoCapacity': entry['CargoCapacity'],
'Modules': entry['Modules'],
})
initial_dict.update({'data': data_dict})
output = json.dumps(initial_dict, indent=4)
self.slef = str(f"[{output}]")
elif event_type == 'modulebuy':
self.state['Modules'][entry['Slot']] = {
@ -1056,7 +1076,7 @@ class EDLogs(FileSystemEventHandler):
self.state['Cargo'] = defaultdict(int)
# From 3.3 full Cargo event (after the first one) is written to a separate file
if 'Inventory' not in entry:
with open(join(self.currentdir, 'Cargo.json'), 'rb') as h: # type: ignore
with open(self.currentdir / 'Cargo.json', 'rb') as h: # type: ignore
entry = json.load(h)
self.state['CargoJSON'] = entry
@ -1083,7 +1103,7 @@ class EDLogs(FileSystemEventHandler):
# Always attempt loading of this, but if it fails we'll hope this was
# a startup/boarding version and thus `entry` contains
# the data anyway.
currentdir_path = pathlib.Path(str(self.currentdir))
currentdir_path = Path(str(self.currentdir))
shiplocker_filename = currentdir_path / 'ShipLocker.json'
shiplocker_max_attempts = 5
shiplocker_fail_sleep = 0.01
@ -1152,7 +1172,7 @@ class EDLogs(FileSystemEventHandler):
# TODO: v31 doc says this is`backpack.json` ... but Howard Chalkley
# said it's `Backpack.json`
backpack_file = pathlib.Path(str(self.currentdir)) / 'Backpack.json'
backpack_file = Path(str(self.currentdir)) / 'Backpack.json'
backpack_data = None
if not backpack_file.exists():
@ -1528,7 +1548,7 @@ class EDLogs(FileSystemEventHandler):
entry = fcmaterials
elif event_type == 'moduleinfo':
with open(join(self.currentdir, 'ModulesInfo.json'), 'rb') as mf: # type: ignore
with open(self.currentdir / 'ModulesInfo.json', 'rb') as mf: # type: ignore
try:
entry = json.load(mf)
@ -2120,36 +2140,33 @@ class EDLogs(FileSystemEventHandler):
return entry
def game_running(self) -> bool: # noqa: CCR001
def game_running(self) -> bool:
"""
Determine if the game is currently running.
TODO: Implement on Linux
:return: bool - True if the game is running.
"""
if sys.platform == 'win32':
def WindowTitle(h): # noqa: N802
if h:
length = GetWindowTextLength(h) + 1
buf = ctypes.create_unicode_buffer(length)
if GetWindowText(h, buf, length):
return buf.value
return None
def callback(hWnd, lParam): # noqa: N803
name = WindowTitle(hWnd)
if name and name.startswith('Elite - Dangerous'):
handle = GetProcessHandleFromHwnd(hWnd)
if handle: # If GetProcessHandleFromHwnd succeeds then the app is already running as this user
CloseHandle(handle)
return False # stop enumeration
return True
return not EnumWindows(EnumWindowsProc(callback), 0)
return False
if self.running_process:
p = self.running_process
try:
with p.oneshot():
if p.status() not in [psutil.STATUS_RUNNING, psutil.STATUS_SLEEPING]:
raise psutil.NoSuchProcess
except psutil.NoSuchProcess:
# Process likely expired
self.running_process = None
if not self.running_process:
try:
edmc_process = psutil.Process()
edmc_user = edmc_process.username()
for proc in psutil.process_iter(['name', 'username']):
if 'EliteDangerous' in proc.info['name'] and proc.info['username'] == edmc_user:
self.running_process = proc
return True
except psutil.NoSuchProcess:
pass
return False
return bool(self.running_process)
def ship(self, timestamped=True) -> MutableMapping[str, Any] | None:
"""
@ -2242,14 +2259,14 @@ class EDLogs(FileSystemEventHandler):
oldfiles = sorted((x for x in listdir(config.get_str('outdir')) if regexp.match(x)))
if oldfiles:
try:
with open(join(config.get_str('outdir'), oldfiles[-1]), encoding='utf-8') as h:
with open(config.get_str('outdir') / Path(oldfiles[-1]), encoding='utf-8') as h:
if h.read() == string:
return # same as last time - don't write
except UnicodeError:
logger.exception("UnicodeError reading old ship loadout with utf-8 encoding, trying without...")
try:
with open(join(config.get_str('outdir'), oldfiles[-1])) as h:
with open(config.get_str('outdir') / Path(oldfiles[-1])) as h:
if h.read() == string:
return # same as last time - don't write
@ -2268,7 +2285,7 @@ class EDLogs(FileSystemEventHandler):
# Write
ts = strftime('%Y-%m-%dT%H.%M.%S', localtime(time()))
filename = join(config.get_str('outdir'), f'{ship}.{ts}.txt')
filename = config.get_str('outdir') / Path(f'{ship}.{ts}.txt')
try:
with open(filename, 'wt', encoding='utf-8') as h:
@ -2355,7 +2372,7 @@ class EDLogs(FileSystemEventHandler):
try:
with open(join(self.currentdir, 'NavRoute.json')) as f:
with open(self.currentdir / 'NavRoute.json') as f:
raw = f.read()
except Exception as e:
@ -2381,7 +2398,7 @@ class EDLogs(FileSystemEventHandler):
try:
with open(join(self.currentdir, 'FCMaterials.json')) as f:
with open(self.currentdir / 'FCMaterials.json') as f:
raw = f.read()
except Exception as e:

22
plug.py
View File

@ -14,6 +14,7 @@ import operator
import os
import sys
import tkinter as tk
from pathlib import Path
from tkinter import ttk
from typing import Any, Mapping, MutableMapping
@ -47,7 +48,7 @@ last_error = LastError()
class Plugin:
"""An EDMC plugin."""
def __init__(self, name: str, loadfile: str | None, plugin_logger: logging.Logger | None): # noqa: CCR001
def __init__(self, name: str, loadfile: Path | None, plugin_logger: logging.Logger | None): # noqa: CCR001
"""
Load a single plugin.
@ -73,7 +74,7 @@ class Plugin:
sys.modules[module.__name__] = module
spec.loader.exec_module(module)
if getattr(module, 'plugin_start3', None):
newname = module.plugin_start3(os.path.dirname(loadfile))
newname = module.plugin_start3(Path(loadfile).resolve().parent)
self.name = str(newname) if newname else self.name
self.module = module
elif getattr(module, 'plugin_start', None):
@ -171,7 +172,9 @@ def _load_internal_plugins():
for name in sorted(os.listdir(config.internal_plugin_dir_path)):
if name.endswith('.py') and name[0] not in ('.', '_'):
try:
plugin = Plugin(name[:-3], os.path.join(config.internal_plugin_dir_path, name), logger)
plugin_name = name[:-3]
plugin_path = config.internal_plugin_dir_path / name
plugin = Plugin(plugin_name, plugin_path, logger)
plugin.folder = None
internal.append(plugin)
except Exception:
@ -186,9 +189,12 @@ def _load_found_plugins():
# The intent here is to e.g. have EDMC-Overlay load before any plugins
# that depend on it.
for name in sorted(os.listdir(config.plugin_dir_path), key=lambda n: (
not os.path.isfile(os.path.join(config.plugin_dir_path, n, '__init__.py')), n.lower())):
if not os.path.isdir(os.path.join(config.plugin_dir_path, name)) or name[0] in ('.', '_'):
plugin_files = sorted(config.plugin_dir_path.iterdir(), key=lambda p: (
not (p / '__init__.py').is_file(), p.name.lower()))
for plugin_file in plugin_files:
name = plugin_file.name
if not (config.plugin_dir_path / name).is_dir() or name.startswith(('.', '_')):
pass
elif name.endswith('.disabled'):
name, discard = name.rsplit('.', 1)
@ -196,12 +202,12 @@ def _load_found_plugins():
else:
try:
# Add plugin's folder to load path in case plugin has internal package dependencies
sys.path.append(os.path.join(config.plugin_dir_path, name))
sys.path.append(str(config.plugin_dir_path / name))
import EDMCLogging
# Create a logger for this 'found' plugin. Must be before the load.py is loaded.
plugin_logger = EDMCLogging.get_plugin_logger(name)
found.append(Plugin(name, os.path.join(config.plugin_dir_path, name, 'load.py'), plugin_logger))
found.append(Plugin(name, config.plugin_dir_path / name / 'load.py', plugin_logger))
except Exception:
PLUGINS_broken.append(Plugin(name, None, logger))
logger.exception(f'Failure loading found Plugin "{name}"')

View File

@ -1416,15 +1416,6 @@ class EDDN:
#######################################################################
# Elisions
#######################################################################
# WORKAROUND WIP EDDN schema | 2021-10-17: This will reject with the Odyssey or Horizons flags present
if 'odyssey' in entry:
del entry['odyssey']
if 'horizons' in entry:
del entry['horizons']
# END WORKAROUND
# In case Frontier ever add any
entry = filter_localised(entry)
#######################################################################

126
prefs.py
View File

@ -4,14 +4,13 @@ from __future__ import annotations
import contextlib
import logging
import pathlib
from os.path import expandvars
from pathlib import Path
import subprocess
import sys
import tempfile
import tkinter as tk
import warnings
from os import system
from os.path import expanduser, expandvars, join, normpath
from tkinter import colorchooser as tkColorChooser # type: ignore # noqa: N812
from tkinter import ttk
from types import TracebackType
@ -20,7 +19,6 @@ import myNotebook as nb # noqa: N813
import plug
from config import appversion_nobuild, config
from EDMCLogging import edmclogger, get_main_logger
from constants import appname
from hotkey import hotkeymgr
from l10n import translations as tr
from monitor import monitor
@ -43,10 +41,10 @@ def help_open_log_folder() -> None:
"""Open the folder logs are stored in."""
warnings.warn('prefs.help_open_log_folder is deprecated, use open_log_folder instead. '
'This function will be removed in 6.0 or later', DeprecationWarning, stacklevel=2)
open_folder(pathlib.Path(tempfile.gettempdir()) / appname)
open_folder(Path(config.app_dir_path / 'logs'))
def open_folder(file: pathlib.Path) -> None:
def open_folder(file: Path) -> None:
"""Open the given file in the OS file explorer."""
if sys.platform.startswith('win'):
# On Windows, use the "start" command to open the folder
@ -58,7 +56,7 @@ def open_folder(file: pathlib.Path) -> None:
def help_open_system_profiler(parent) -> None:
"""Open the EDMC System Profiler."""
profiler_path = pathlib.Path(config.respath_path)
profiler_path = config.respath_path
try:
if getattr(sys, 'frozen', False):
profiler_path /= 'EDMCSystemProfiler.exe'
@ -188,7 +186,9 @@ class AutoInc(contextlib.AbstractContextManager):
if sys.platform == 'win32':
import ctypes
import winreg
from ctypes.wintypes import HINSTANCE, HWND, LPCWSTR, LPWSTR, MAX_PATH, POINT, RECT, SIZE, UINT
from ctypes.wintypes import LPCWSTR, LPWSTR, MAX_PATH, POINT, RECT, SIZE, UINT, BOOL
import win32gui
import win32api
is_wine = False
try:
WINE_REGISTRY_KEY = r'HKEY_LOCAL_MACHINE\Software\Wine'
@ -203,6 +203,8 @@ if sys.platform == 'win32':
if not is_wine:
try:
CalculatePopupWindowPosition = ctypes.windll.user32.CalculatePopupWindowPosition
CalculatePopupWindowPosition.argtypes = [POINT, SIZE, UINT, RECT, RECT]
CalculatePopupWindowPosition.restype = BOOL
except AttributeError as e:
logger.error(
@ -219,17 +221,9 @@ if sys.platform == 'win32':
ctypes.POINTER(RECT)
]
GetParent = ctypes.windll.user32.GetParent
GetParent.argtypes = [HWND]
GetWindowRect = ctypes.windll.user32.GetWindowRect
GetWindowRect.argtypes = [HWND, ctypes.POINTER(RECT)]
SHGetLocalizedName = ctypes.windll.shell32.SHGetLocalizedName
SHGetLocalizedName.argtypes = [LPCWSTR, LPWSTR, UINT, ctypes.POINTER(ctypes.c_int)]
LoadString = ctypes.windll.user32.LoadStringW
LoadString.argtypes = [HINSTANCE, UINT, LPWSTR, ctypes.c_int]
class PreferencesDialog(tk.Toplevel):
"""The EDMC preferences dialog."""
@ -239,6 +233,7 @@ class PreferencesDialog(tk.Toplevel):
self.parent = parent
self.callback = callback
self.req_restart = False
# LANG: File > Settings (macOS)
self.title(tr.tl('Settings'))
@ -314,7 +309,7 @@ class PreferencesDialog(tk.Toplevel):
# Ensure fully on-screen
if sys.platform == 'win32' and CalculatePopupWindowPosition:
position = RECT()
GetWindowRect(GetParent(self.winfo_id()), position)
win32gui.GetWindowRect(win32gui.GetParent(self.winfo_id()))
if CalculatePopupWindowPosition(
POINT(parent.winfo_rootx(), parent.winfo_rooty()),
SIZE(position.right - position.left, position.bottom - position.top), # type: ignore
@ -323,7 +318,7 @@ class PreferencesDialog(tk.Toplevel):
self.geometry(f"+{position.left}+{position.top}")
# Set Log Directory
self.logfile_loc = pathlib.Path(tempfile.gettempdir()) / appname
self.logfile_loc = Path(config.app_dir_path / 'logs')
# Set minimum size to prevent content cut-off
self.update_idletasks() # Update "requested size" from geometry manager
@ -911,20 +906,15 @@ class PreferencesDialog(tk.Toplevel):
# Plugin settings and info
plugins_frame = nb.Frame(notebook)
plugins_frame.columnconfigure(0, weight=1)
plugdir = tk.StringVar()
plugdir.set(config.plugin_dir)
row = AutoInc(start=0)
# Section heading in settings
self.plugdir = tk.StringVar()
self.plugdir.set(str(config.get_str('plugin_dir')))
# LANG: Label for location of third-party plugins folder
nb.Label(plugins_frame, text=tr.tl('Plugins folder') + ':').grid(
padx=self.PADX, pady=self.PADY, sticky=tk.W, row=row.get()
)
plugdirentry = ttk.Entry(plugins_frame, justify=tk.LEFT)
self.displaypath(plugdir, plugdirentry)
plugdirentry.grid(columnspan=2, padx=self.PADX, pady=self.BOXY, sticky=tk.EW, row=row.get())
self.plugdir_label = nb.Label(plugins_frame, text=tr.tl('Plugins folder') + ':')
self.plugdir_label.grid(padx=self.PADX, pady=self.PADY, sticky=tk.W, row=row.get())
self.plugdir_entry = ttk.Entry(plugins_frame, takefocus=False,
textvariable=self.plugdir) # Link StringVar to Entry widget
self.plugdir_entry.grid(columnspan=4, padx=self.PADX, pady=self.BOXY, sticky=tk.EW, row=row.get())
with row as cur_row:
nb.Label(
plugins_frame,
@ -932,19 +922,41 @@ class PreferencesDialog(tk.Toplevel):
# LANG: Tip/label about how to disable plugins
text=tr.tl(
"Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name").format(EXT='.disabled')
).grid(columnspan=2, padx=self.PADX, pady=self.PADY, sticky=tk.EW, row=cur_row)
).grid(columnspan=1, padx=self.PADX, pady=self.PADY, sticky=tk.EW, row=cur_row)
ttk.Button(
# Open Plugin Folder Button
self.open_plug_folder_btn = ttk.Button(
plugins_frame,
# LANG: Label on button used to open a filesystem folder
text=tr.tl('Open'), # Button that opens a folder in Explorer/Finder
# LANG: Label on button used to open the Plugin Folder
text=tr.tl('Open Plugins Folder'),
command=lambda: open_folder(config.plugin_dir_path)
).grid(column=1, padx=self.PADX, pady=self.PADY, sticky=tk.N, row=cur_row)
)
self.open_plug_folder_btn.grid(column=1, padx=self.PADX, pady=self.PADY, sticky=tk.EW, row=cur_row)
# Browse Button
text = tr.tl('Browse...') # LANG: NOT-macOS Settings - files location selection button
self.plugbutton = ttk.Button(
plugins_frame,
text=text,
# LANG: Selecting the Location of the Plugin Directory on the Filesystem
command=lambda: self.filebrowse(tr.tl('Plugin Directory Location'), self.plugdir)
)
self.plugbutton.grid(column=2, padx=self.PADX, pady=self.PADY, sticky=tk.EW, row=cur_row)
if config.default_journal_dir_path:
# Appearance theme and language setting
ttk.Button(
plugins_frame,
# LANG: Settings > Configuration - Label on 'reset journal files location to default' button
text=tr.tl('Default'),
command=self.plugdir_reset,
state=tk.NORMAL if config.get_str('plugin_dir') else tk.DISABLED
).grid(column=3, padx=self.PADX, pady=self.PADY, sticky=tk.EW, row=cur_row)
enabled_plugins = list(filter(lambda x: x.folder and x.module, plug.PLUGINS))
if enabled_plugins:
ttk.Separator(plugins_frame, orient=tk.HORIZONTAL).grid(
columnspan=3, padx=self.PADX, pady=self.SEPY, sticky=tk.EW, row=row.get()
columnspan=4, padx=self.PADX, pady=self.SEPY, sticky=tk.EW, row=row.get()
)
nb.Label(
plugins_frame,
@ -1066,7 +1078,7 @@ class PreferencesDialog(tk.Toplevel):
import tkinter.filedialog
directory = tkinter.filedialog.askdirectory(
parent=self,
initialdir=expanduser(pathvar.get()),
initialdir=Path(pathvar.get()).expanduser(),
title=title,
mustexist=tk.TRUE
)
@ -1088,13 +1100,14 @@ class PreferencesDialog(tk.Toplevel):
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('\\')
components = Path(pathvar.get()).resolve().parts
buf = ctypes.create_unicode_buffer(MAX_PATH)
pidsRes = ctypes.c_int() # noqa: N806 # Windows convention
for i in range(start, len(components)):
try:
if (not SHGetLocalizedName('\\'.join(components[:i+1]), buf, MAX_PATH, ctypes.byref(pidsRes)) and
LoadString(ctypes.WinDLL(expandvars(buf.value))._handle, pidsRes.value, buf, MAX_PATH)):
win32api.LoadString(ctypes.WinDLL(expandvars(buf.value))._handle,
pidsRes.value, buf, MAX_PATH)):
display.append(buf.value)
else:
@ -1122,6 +1135,13 @@ class PreferencesDialog(tk.Toplevel):
self.outvarchanged()
def plugdir_reset(self) -> None:
"""Reset the log dir to the default."""
if config.default_plugin_dir_path:
self.plugdir.set(config.default_plugin_dir)
self.outvarchanged()
def disable_autoappupdatecheckingame_changed(self) -> None:
"""Save out the auto update check in game config."""
config.set('disable_autoappupdatecheckingame', self.disable_autoappupdatecheckingame.get())
@ -1217,7 +1237,7 @@ class PreferencesDialog(tk.Toplevel):
return 'break' # stops further processing - insertion, Tab traversal etc
def apply(self) -> None:
def apply(self) -> None: # noqa: CCR001
"""Update the config with the options set on the dialog."""
config.set('PrefsVersion', prefsVersion.stringToSerial(appversion_nobuild()))
config.set(
@ -1233,7 +1253,7 @@ class PreferencesDialog(tk.Toplevel):
config.set(
'outdir',
join(config.home_path, self.outdir.get()[2:]) if self.outdir.get().startswith('~') else self.outdir.get()
str(config.home_path / self.outdir.get()[2:]) if self.outdir.get().startswith('~') else self.outdir.get()
)
logdir = self.logdir.get()
@ -1273,20 +1293,30 @@ class PreferencesDialog(tk.Toplevel):
config.set('dark_text', self.theme_colors[0])
config.set('dark_highlight', self.theme_colors[1])
theme.apply(self.parent)
if self.plugdir.get() != config.get('plugin_dir'):
config.set(
'plugin_dir',
str(Path(config.home_path, self.plugdir.get()[2:])) if self.plugdir.get().startswith('~') else
str(Path(self.plugdir.get()))
)
self.req_restart = True
# Send to the Post Config if we updated the update branch
post_flags = {
'Update': True if self.curr_update_track != self.update_paths.get() else False,
'Track': self.update_paths.get(),
'Parent': self
}
# Notify
if self.callback:
self.callback(**post_flags)
self.callback()
plug.notify_prefs_changed(monitor.cmdr, monitor.is_beta)
self._destroy()
# Send to the Post Config if we updated the update branch or need to restart
post_flags = {
'Update': True if self.curr_update_track != self.update_paths.get() else False,
'Track': self.update_paths.get(),
'Parent': self,
'Restart_Req': True if self.req_restart else False
}
if self.callback:
self.callback(**post_flags)
def _destroy(self) -> None:
"""widget.destroy wrapper that does some extra cleanup."""

View File

@ -69,13 +69,16 @@ if (config.auth_force_edmc_protocol # noqa: C901
# This could be false if you use auth_force_edmc_protocol, but then you get to keep the pieces
assert sys.platform == 'win32'
# spell-checker: words HBRUSH HICON WPARAM wstring WNDCLASS HMENU HGLOBAL
from ctypes import ( # type: ignore
windll, POINTER, WINFUNCTYPE, Structure, byref, c_long, c_void_p, create_unicode_buffer, wstring_at
from ctypes import (
windll, WINFUNCTYPE, Structure, byref, c_long, c_void_p, create_unicode_buffer, wstring_at
)
from ctypes.wintypes import (
ATOM, BOOL, DWORD, HBRUSH, HGLOBAL, HICON, HINSTANCE, HMENU, HWND, INT, LPARAM, LPCWSTR, LPMSG, LPVOID, LPWSTR,
ATOM, HBRUSH, HICON, HINSTANCE, HWND, INT, LPARAM, LPCWSTR, LPWSTR,
MSG, UINT, WPARAM
)
import win32gui
import win32con
import win32api
class WNDCLASS(Structure):
"""
@ -98,41 +101,8 @@ if (config.auth_force_edmc_protocol # noqa: C901
('lpszClassName', LPCWSTR)
]
CW_USEDEFAULT = 0x80000000
CreateWindowExW = windll.user32.CreateWindowExW
CreateWindowExW.argtypes = [DWORD, LPCWSTR, LPCWSTR, DWORD, INT, INT, INT, INT, HWND, HMENU, HINSTANCE, LPVOID]
CreateWindowExW.restype = HWND
RegisterClassW = windll.user32.RegisterClassW
RegisterClassW.argtypes = [POINTER(WNDCLASS)]
# DefWindowProcW
# Ref: <https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-defwindowprocw>
# LRESULT DefWindowProcW([in] HWND hWnd,[in] UINT Msg,[in] WPARAM wParam,[in] LPARAM lParam);
# As per example at <https://docs.python.org/3/library/ctypes.html#ctypes.WINFUNCTYPE>
prototype = WINFUNCTYPE(c_long, HWND, UINT, WPARAM, LPARAM)
paramflags = (1, "hWnd"), (1, "Msg"), (1, "wParam"), (1, "lParam")
DefWindowProcW = prototype(("DefWindowProcW", windll.user32), paramflags)
GetParent = windll.user32.GetParent
SetForegroundWindow = windll.user32.SetForegroundWindow
# <https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getmessagew>
# NB: Despite 'BOOL' return type, it *can* be >0, 0 or -1, so is actually
# c_long
prototype = WINFUNCTYPE(c_long, LPMSG, HWND, UINT, UINT)
paramflags = (1, "lpMsg"), (1, "hWnd"), (1, "wMsgFilterMin"), (1, "wMsgFilterMax")
GetMessageW = prototype(("GetMessageW", windll.user32), paramflags)
TranslateMessage = windll.user32.TranslateMessage
DispatchMessageW = windll.user32.DispatchMessageW
PostThreadMessageW = windll.user32.PostThreadMessageW
SendMessageW = windll.user32.SendMessageW
SendMessageW.argtypes = [HWND, UINT, WPARAM, LPARAM]
PostMessageW = windll.user32.PostMessageW
PostMessageW.argtypes = [HWND, UINT, WPARAM, LPARAM]
WM_QUIT = 0x0012
# https://docs.microsoft.com/en-us/windows/win32/dataxchg/wm-dde-initiate
WM_DDE_INITIATE = 0x03E0
WM_DDE_TERMINATE = 0x03E1
@ -148,12 +118,6 @@ if (config.auth_force_edmc_protocol # noqa: C901
GlobalGetAtomNameW = windll.kernel32.GlobalGetAtomNameW
GlobalGetAtomNameW.argtypes = [ATOM, LPWSTR, INT]
GlobalGetAtomNameW.restype = UINT
GlobalLock = windll.kernel32.GlobalLock
GlobalLock.argtypes = [HGLOBAL]
GlobalLock.restype = LPVOID
GlobalUnlock = windll.kernel32.GlobalUnlock
GlobalUnlock.argtypes = [HGLOBAL]
GlobalUnlock.restype = BOOL
# Windows Message handler stuff (IPC)
# https://docs.microsoft.com/en-us/previous-versions/windows/desktop/legacy/ms633573(v=vs.85)
@ -171,7 +135,7 @@ if (config.auth_force_edmc_protocol # noqa: C901
if message != WM_DDE_INITIATE:
# Not a DDE init message, bail and tell windows to do the default
# https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-defwindowproca?redirectedfrom=MSDN
return DefWindowProcW(hwnd, message, wParam, lParam)
return win32gui.DefWindowProc(hwnd, message, wParam, lParam)
service = create_unicode_buffer(256)
topic = create_unicode_buffer(256)
@ -196,7 +160,7 @@ if (config.auth_force_edmc_protocol # noqa: C901
if target_is_valid and topic_is_valid:
# if everything is happy, send an acknowledgement of the DDE request
SendMessageW(
win32gui.SendMessage(
wParam, WM_DDE_ACK, hwnd, PackDDElParam(WM_DDE_ACK, GlobalAddAtomW(appname), GlobalAddAtomW('System'))
)
@ -229,7 +193,7 @@ if (config.auth_force_edmc_protocol # noqa: C901
thread = self.thread
if thread:
self.thread = None
PostThreadMessageW(thread.ident, WM_QUIT, 0, 0)
win32api.PostThreadMessage(thread.ident, win32con.WM_QUIT, 0, 0)
thread.join() # Wait for it to quit
def worker(self) -> None:
@ -239,24 +203,25 @@ if (config.auth_force_edmc_protocol # noqa: C901
wndclass.lpfnWndProc = WndProc
wndclass.cbClsExtra = 0
wndclass.cbWndExtra = 0
wndclass.hInstance = windll.kernel32.GetModuleHandleW(0)
wndclass.hInstance = win32gui.GetModuleHandle(0)
wndclass.hIcon = None
wndclass.hCursor = None
wndclass.hbrBackground = None
wndclass.lpszMenuName = None
wndclass.lpszClassName = 'DDEServer'
if not RegisterClassW(byref(wndclass)):
if not win32gui.RegisterClass(byref(wndclass)):
print('Failed to register Dynamic Data Exchange for cAPI')
return
# https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-createwindowexw
hwnd = CreateWindowExW(
hwnd = win32gui.CreateWindowEx(
0, # dwExStyle
wndclass.lpszClassName, # lpClassName
"DDE Server", # lpWindowName
0, # dwStyle
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, # X, Y, nWidth, nHeight
# X, Y, nWidth, nHeight
win32con.CW_USEDEFAULT, win32con.CW_USEDEFAULT, win32con.CW_USEDEFAULT, win32con.CW_USEDEFAULT,
self.master.winfo_id(), # hWndParent # Don't use HWND_MESSAGE since the window won't get DDE broadcasts
None, # hMenu
wndclass.hInstance, # hInstance
@ -276,13 +241,13 @@ if (config.auth_force_edmc_protocol # noqa: C901
#
# But it does actually work. Either getting a non-0 value and
# entering the loop, or getting 0 and exiting it.
while GetMessageW(byref(msg), None, 0, 0) != 0:
while win32gui.GetMessage(byref(msg), None, 0, 0) != 0:
logger.trace_if('frontier-auth.windows', f'DDE message of type: {msg.message}')
if msg.message == WM_DDE_EXECUTE:
# GlobalLock does some sort of "please dont move this?"
# https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-globallock
args = wstring_at(GlobalLock(msg.lParam)).strip()
GlobalUnlock(msg.lParam) # Unlocks the GlobalLock-ed object
args = wstring_at(win32gui.GlobalLock(msg.lParam)).strip()
win32gui.GlobalUnlock(msg.lParam) # Unlocks the GlobalLock-ed object
if args.lower().startswith('open("') and args.endswith('")'):
logger.trace_if('frontier-auth.windows', f'args are: {args}')
@ -291,20 +256,20 @@ if (config.auth_force_edmc_protocol # noqa: C901
logger.debug(f'Message starts with {self.redirect}')
self.event(url)
SetForegroundWindow(GetParent(self.master.winfo_id())) # raise app window
win32gui.SetForegroundWindow(win32gui.GetParent(self.master.winfo_id())) # raise app window
# Send back a WM_DDE_ACK. this is _required_ with WM_DDE_EXECUTE
PostMessageW(msg.wParam, WM_DDE_ACK, hwnd, PackDDElParam(WM_DDE_ACK, 0x80, msg.lParam))
win32gui.PostMessage(msg.wParam, WM_DDE_ACK, hwnd, PackDDElParam(WM_DDE_ACK, 0x80, msg.lParam))
else:
# Send back a WM_DDE_ACK. this is _required_ with WM_DDE_EXECUTE
PostMessageW(msg.wParam, WM_DDE_ACK, hwnd, PackDDElParam(WM_DDE_ACK, 0, msg.lParam))
win32gui.PostMessage(msg.wParam, WM_DDE_ACK, hwnd, PackDDElParam(WM_DDE_ACK, 0, msg.lParam))
elif msg.message == WM_DDE_TERMINATE:
PostMessageW(msg.wParam, WM_DDE_TERMINATE, hwnd, 0)
win32gui.PostMessage(msg.wParam, WM_DDE_TERMINATE, hwnd, 0)
else:
TranslateMessage(byref(msg)) # "Translates virtual key messages into character messages" ???
DispatchMessageW(byref(msg))
win32gui.DispatchMessage(byref(msg))
else: # Linux / Run from source

View File

@ -8,10 +8,10 @@ wheel
setuptools==70.0.0
# Static analysis tools
flake8==7.0.0
flake8==7.1.0
flake8-annotations-coverage==0.0.6
flake8-cognitive-complexity==0.1.0
flake8-comprehensions==3.14.0
flake8-comprehensions==3.15.0
flake8-docstrings==1.7.0
flake8-json==24.4.0
flake8-noqa==1.4.0
@ -19,16 +19,16 @@ flake8-polyfill==1.0.2
flake8-use-fstring==1.4
mypy==1.10.0
pep8-naming==0.13.3
safety==3.2.0
types-requests==2.31.0.20240311
pep8-naming==0.14.1
safety==3.2.5
types-requests==2.32.0.20240712
types-pkg-resources==0.1.3
# Code formatting tools
autopep8==2.2.0
autopep8==2.3.1
# Git pre-commit checking
pre-commit==3.7.1
pre-commit==3.8.0
# HTML changelogs
mistune==3.0.2
@ -38,12 +38,10 @@ mistune==3.0.2
py2exe==0.13.0.1; sys_platform == 'win32'
# Testing
pytest==8.2.0
pytest==8.3.2
pytest-cov==5.0.0 # Pytest code coverage support
coverage[toml]==7.5.0 # pytest-cov dep. This is here to ensure that it includes TOML support for pyproject.toml configs
coverage[toml]==7.6.1 # pytest-cov dep. This is here to ensure that it includes TOML support for pyproject.toml configs
coverage-conditional-plugin==0.9.0
# For manipulating folder permissions and the like.
pywin32==306; sys_platform == 'win32'
# All of the normal requirements

View File

@ -1,5 +1,8 @@
requests==2.32.3
pillow==10.3.0
watchdog==4.0.0
pillow==10.4.0
watchdog==4.0.1
simplesystray==0.1.0; sys_platform == 'win32'
semantic-version==2.10.0
# For manipulating folder permissions and the like.
pywin32==306; sys_platform == 'win32'
psutil==6.0.0

View File

@ -1,119 +1,158 @@
{
"Adder": {
"hullMass": 35
"hullMass": 35,
"reserveFuelCapacity": 0.36
},
"Alliance Challenger": {
"hullMass": 450
"hullMass": 450,
"reserveFuelCapacity": 0.77
},
"Alliance Chieftain": {
"hullMass": 400
"hullMass": 400,
"reserveFuelCapacity": 0.77
},
"Alliance Crusader": {
"hullMass": 500
"hullMass": 500,
"reserveFuelCapacity": 0.77
},
"Anaconda": {
"hullMass": 400
"hullMass": 400,
"reserveFuelCapacity": 1.07
},
"Asp Explorer": {
"hullMass": 280
"hullMass": 280,
"reserveFuelCapacity": 0.63
},
"Asp Scout": {
"hullMass": 150
"hullMass": 150,
"reserveFuelCapacity": 0.47
},
"Beluga Liner": {
"hullMass": 950
"hullMass": 950,
"reserveFuelCapacity": 0.81
},
"Cobra MkIII": {
"hullMass": 180
"hullMass": 180,
"reserveFuelCapacity": 0.49
},
"Cobra MkIV": {
"hullMass": 210
"hullMass": 210,
"reserveFuelCapacity": 0.51
},
"Diamondback Explorer": {
"hullMass": 260
"hullMass": 260,
"reserveFuelCapacity": 0.52
},
"Diamondback Scout": {
"hullMass": 170
"hullMass": 170,
"reserveFuelCapacity": 0.49
},
"Dolphin": {
"hullMass": 140
"hullMass": 140,
"reserveFuelCapacity": 0.5
},
"Eagle": {
"hullMass": 50
"hullMass": 50,
"reserveFuelCapacity": 0.34
},
"Federal Assault Ship": {
"hullMass": 480
"hullMass": 480,
"reserveFuelCapacity": 0.72
},
"Federal Corvette": {
"hullMass": 900
"hullMass": 900,
"reserveFuelCapacity": 1.13
},
"Federal Dropship": {
"hullMass": 580
"hullMass": 580,
"reserveFuelCapacity": 0.83
},
"Federal Gunship": {
"hullMass": 580
"hullMass": 580,
"reserveFuelCapacity": 0.82
},
"Fer-de-Lance": {
"hullMass": 250
"hullMass": 250,
"reserveFuelCapacity": 0.67
},
"Hauler": {
"hullMass": 14
"hullMass": 14,
"reserveFuelCapacity": 0.25
},
"Imperial Clipper": {
"hullMass": 400
"hullMass": 400,
"reserveFuelCapacity": 0.74
},
"Imperial Courier": {
"hullMass": 35
"hullMass": 35,
"reserveFuelCapacity": 0.41
},
"Imperial Cutter": {
"hullMass": 1100
"hullMass": 1100,
"reserveFuelCapacity": 1.16
},
"Imperial Eagle": {
"hullMass": 50
"hullMass": 50,
"reserveFuelCapacity": 0.37
},
"Keelback": {
"hullMass": 180
"hullMass": 180,
"reserveFuelCapacity": 0.39
},
"Krait MkII": {
"hullMass": 320
"hullMass": 320,
"reserveFuelCapacity": 0.63
},
"Krait Phantom": {
"hullMass": 270
"hullMass": 270,
"reserveFuelCapacity": 0.63
},
"Mamba": {
"hullMass": 250
"hullMass": 250,
"reserveFuelCapacity": 0.5
},
"Orca": {
"hullMass": 290
"hullMass": 290,
"reserveFuelCapacity": 0.79
},
"Python": {
"hullMass": 350
"hullMass": 350,
"reserveFuelCapacity": 0.83
},
"Python Mk II": {
"hullMass": 450
"hullMass": 450,
"reserveFuelCapacity": 0.83
},
"Sidewinder": {
"hullMass": 25
"hullMass": 25,
"reserveFuelCapacity": 0.3
},
"Type-10 Defender": {
"hullMass": 1200
"hullMass": 1200,
"reserveFuelCapacity": 0.77
},
"Type-6 Transporter": {
"hullMass": 155
"hullMass": 155,
"reserveFuelCapacity": 0.39
},
"Type-7 Transporter": {
"hullMass": 350
"hullMass": 350,
"reserveFuelCapacity": 0.52
},
"Type-9 Heavy": {
"hullMass": 850
"hullMass": 850,
"reserveFuelCapacity": 0.77
},
"Viper MkIII": {
"hullMass": 50
"hullMass": 50,
"reserveFuelCapacity": 0.41
},
"Viper MkIV": {
"hullMass": 190
"hullMass": 190,
"reserveFuelCapacity": 0.46
},
"Vulture": {
"hullMass": 230
"hullMass": 230,
"reserveFuelCapacity": 0.57
}
}

View File

@ -25,17 +25,15 @@ logger = EDMCLogging.get_main_logger()
if sys.platform == 'win32':
import ctypes
from ctypes.wintypes import HWND, POINT, RECT, SIZE, UINT
from ctypes.wintypes import POINT, RECT, SIZE, UINT, BOOL
import win32gui
try:
CalculatePopupWindowPosition = ctypes.windll.user32.CalculatePopupWindowPosition
CalculatePopupWindowPosition.argtypes = [
ctypes.POINTER(POINT), ctypes.POINTER(SIZE), UINT, ctypes.POINTER(RECT), ctypes.POINTER(RECT)
]
GetParent = ctypes.windll.user32.GetParent
GetParent.argtypes = [HWND]
GetWindowRect = ctypes.windll.user32.GetWindowRect
GetWindowRect.argtypes = [HWND, ctypes.POINTER(RECT)]
CalculatePopupWindowPosition.restype = BOOL
except Exception: # Not supported under Wine 4.0
CalculatePopupWindowPosition = None # type: ignore
@ -243,7 +241,7 @@ def ships(companion_data: dict[str, Any]) -> list[ShipRet]:
"""
Return a list of 5 tuples of ship information.
:param data: [description]
:param companion_data: [description]
:return: A 5 tuple of strings containing: Ship ID, Ship Type Name (internal), Ship Name, System, Station, and Value
"""
ships: list[dict[str, Any]] = companion.listify(cast(list, companion_data.get('ships')))
@ -253,16 +251,15 @@ def ships(companion_data: dict[str, Any]) -> list[ShipRet]:
ships.insert(0, ships.pop(current)) # Put current ship first
if not companion_data['commander'].get('docked'):
out: list[ShipRet] = []
# Set current system, not last docked
out.append(ShipRet(
out: list[ShipRet] = [ShipRet(
id=str(ships[0]['id']),
type=ship_name_map.get(ships[0]['name'].lower(), ships[0]['name']),
name=str(ships[0].get('shipName', '')),
system=companion_data['lastSystem']['name'],
station='',
value=str(ships[0]['value']['total'])
))
)]
out.extend(
ShipRet(
id=str(ship['id']),
@ -302,7 +299,7 @@ def export_ships(companion_data: dict[str, Any], filename: AnyStr) -> None:
h.writerow(list(thing))
class StatsDialog():
class StatsDialog:
"""Status dialog containing all of the current cmdr's stats."""
def __init__(self, parent: tk.Tk, status: tk.Label) -> None:
@ -423,7 +420,7 @@ class StatsResults(tk.Toplevel):
# Ensure fully on-screen
if sys.platform == 'win32' and CalculatePopupWindowPosition:
position = RECT()
GetWindowRect(GetParent(self.winfo_id()), position)
win32gui.GetWindowRect(win32gui.GetParent(self.winfo_id()))
if CalculatePopupWindowPosition(
POINT(parent.winfo_rootx(), parent.winfo_rooty()),
# - is evidently supported on the C side

View File

@ -13,7 +13,6 @@ from __future__ import annotations
import os
import sys
import tkinter as tk
from os.path import join
from tkinter import font as tk_font
from tkinter import ttk
from typing import Callable
@ -34,11 +33,13 @@ if sys.platform == "linux":
if sys.platform == 'win32':
import ctypes
from ctypes.wintypes import DWORD, LPCVOID, LPCWSTR
import win32gui
AddFontResourceEx = ctypes.windll.gdi32.AddFontResourceExW
AddFontResourceEx.restypes = [LPCWSTR, DWORD, LPCVOID] # type: ignore
FR_PRIVATE = 0x10
FR_NOT_ENUM = 0x20
AddFontResourceEx(join(config.respath, 'EUROCAPS.TTF'), FR_PRIVATE, 0)
font_path = config.respath_path / 'EUROCAPS.TTF'
AddFontResourceEx(str(font_path), FR_PRIVATE, 0)
elif sys.platform == 'linux':
# pyright: reportUnboundVariable=false
@ -421,14 +422,7 @@ class _Theme:
self.active = theme
if sys.platform == 'win32':
GWL_STYLE = -16 # noqa: N806 # ctypes
WS_MAXIMIZEBOX = 0x00010000 # noqa: N806 # ctypes
# tk8.5.9/win/tkWinWm.c:342
GWL_EXSTYLE = -20 # noqa: N806 # ctypes
WS_EX_APPWINDOW = 0x00040000 # noqa: N806 # ctypes
WS_EX_LAYERED = 0x00080000 # noqa: N806 # ctypes
GetWindowLongW = ctypes.windll.user32.GetWindowLongW # noqa: N806 # ctypes
SetWindowLongW = ctypes.windll.user32.SetWindowLongW # noqa: N806 # ctypes
import win32con
# FIXME: Lose the "treat this like a boolean" bullshit
if theme == self.THEME_DEFAULT:
@ -445,14 +439,17 @@ class _Theme:
root.withdraw()
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
hwnd = win32gui.GetParent(root.winfo_id())
win32gui.SetWindowLong(hwnd, win32con.GWL_STYLE,
win32gui.GetWindowLong(hwnd, win32con.GWL_STYLE)
& ~win32con.WS_MAXIMIZEBOX) # disable maximize
if theme == self.THEME_TRANSPARENT:
SetWindowLongW(hwnd, GWL_EXSTYLE, WS_EX_APPWINDOW | WS_EX_LAYERED) # Add to taskbar
win32gui.SetWindowLong(hwnd, win32con.GWL_EXSTYLE,
win32con.WS_EX_APPWINDOW | win32con.WS_EX_LAYERED) # Add to taskbar
else:
SetWindowLongW(hwnd, GWL_EXSTYLE, WS_EX_APPWINDOW) # Add to taskbar
win32gui.SetWindowLong(hwnd, win32con.GWL_EXSTYLE, win32con.WS_EX_APPWINDOW) # Add to taskbar
root.deiconify()
root.wait_visibility() # need main window to be displayed before returning

View File

@ -28,7 +28,6 @@ from tkinter import font as tk_font
from tkinter import ttk
from typing import Any
import plug
from os import path
from config import config, logger
from l10n import translations as tr
from monitor import monitor
@ -96,7 +95,7 @@ class HyperlinkLabel(tk.Label or ttk.Label): # type: ignore
else:
# Avoid file length limits if possible
target = plug.invoke(url, 'EDSY', 'shipyard_url', loadout, monitor.is_beta)
file_name = path.join(config.app_dir_path, "last_shipyard.html")
file_name = config.app_dir_path / "last_shipyard.html"
with open(file_name, 'w') as f:
f.write(SHIPYARD_HTML_TEMPLATE.format(
@ -194,6 +193,10 @@ class HyperlinkLabel(tk.Label or ttk.Label): # type: ignore
menu.add_command(label=tr.tl('Copy'), command=self.copy) # As in Copy and Paste
if self.name == 'ship':
# LANG: Copy the Inara SLEF Format of the active ship to the clipboard
menu.add_command(label=tr.tl('Copy Inara SLEF'), command=self.copy_slef, state=tk.DISABLED)
menu.entryconfigure(1, state=monitor.slef and tk.NORMAL or tk.DISABLED)
menu.add_separator()
for url in plug.provides('shipyard_url'):
menu.add_command(
@ -224,3 +227,8 @@ class HyperlinkLabel(tk.Label or ttk.Label): # type: ignore
"""Copy the current text to the clipboard."""
self.clipboard_clear()
self.clipboard_append(self['text'])
def copy_slef(self) -> None:
"""Copy the current text to the clipboard."""
self.clipboard_clear()
self.clipboard_append(monitor.slef)