├── .gitignore ├── requirements.txt ├── logo.ico ├── themes ├── ed-azure-dark │ ├── up.png │ ├── down.png │ ├── empty.png │ ├── right.png │ ├── size.png │ ├── notebook.png │ ├── on-basic.png │ ├── on-hover.png │ ├── box-accent.png │ ├── box-basic.png │ ├── box-hover.png │ ├── hor-accent.png │ ├── hor-basic.png │ ├── hor-hover.png │ ├── off-basic.png │ ├── off-hover.png │ ├── on-accent.png │ ├── rect-basic.png │ ├── rect-hover.png │ ├── separator.png │ ├── tab-basic.png │ ├── tab-hover.png │ ├── tree-basic.png │ ├── tri-accent.png │ ├── tri-basic.png │ ├── tri-hover.png │ ├── up-accent.png │ ├── vert-basic.png │ ├── vert-hover.png │ ├── box-invalid.png │ ├── button-hover.png │ ├── check-accent.png │ ├── check-basic.png │ ├── check-hover.png │ ├── circle-accent.png │ ├── circle-basic.png │ ├── circle-hover.png │ ├── down-accent.png │ ├── outline-basic.png │ ├── outline-hover.png │ ├── radio-accent.png │ ├── radio-basic.png │ ├── radio-hover.png │ ├── rect-accent.png │ ├── tab-disabled.png │ ├── tree-pressed.png │ ├── vert-accent.png │ └── rect-accent-hover.png └── ed-azure-dark.tcl ├── screenshots ├── screenshot_exact_route.png └── screenshot_simple_route.png ├── LICENSE ├── setup.py ├── autocomplete.py ├── menu.py ├── README.md ├── utils.py ├── api_access.py ├── gui.py └── EDNeutronAssistant.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | build 3 | build.log 4 | dist 5 | __* 6 | *.spec -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | future~=0.18.2 2 | requests~=2.25.1 3 | clipboard~=0.0.4 -------------------------------------------------------------------------------- /logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/EDNeutronAssistant/main/logo.ico -------------------------------------------------------------------------------- /themes/ed-azure-dark/up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/EDNeutronAssistant/main/themes/ed-azure-dark/up.png -------------------------------------------------------------------------------- /themes/ed-azure-dark/down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/EDNeutronAssistant/main/themes/ed-azure-dark/down.png -------------------------------------------------------------------------------- /themes/ed-azure-dark/empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/EDNeutronAssistant/main/themes/ed-azure-dark/empty.png -------------------------------------------------------------------------------- /themes/ed-azure-dark/right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/EDNeutronAssistant/main/themes/ed-azure-dark/right.png -------------------------------------------------------------------------------- /themes/ed-azure-dark/size.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/EDNeutronAssistant/main/themes/ed-azure-dark/size.png -------------------------------------------------------------------------------- /themes/ed-azure-dark/notebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/EDNeutronAssistant/main/themes/ed-azure-dark/notebook.png -------------------------------------------------------------------------------- /themes/ed-azure-dark/on-basic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/EDNeutronAssistant/main/themes/ed-azure-dark/on-basic.png -------------------------------------------------------------------------------- /themes/ed-azure-dark/on-hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/EDNeutronAssistant/main/themes/ed-azure-dark/on-hover.png -------------------------------------------------------------------------------- /themes/ed-azure-dark/box-accent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/EDNeutronAssistant/main/themes/ed-azure-dark/box-accent.png -------------------------------------------------------------------------------- /themes/ed-azure-dark/box-basic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/EDNeutronAssistant/main/themes/ed-azure-dark/box-basic.png -------------------------------------------------------------------------------- /themes/ed-azure-dark/box-hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/EDNeutronAssistant/main/themes/ed-azure-dark/box-hover.png -------------------------------------------------------------------------------- /themes/ed-azure-dark/hor-accent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/EDNeutronAssistant/main/themes/ed-azure-dark/hor-accent.png -------------------------------------------------------------------------------- /themes/ed-azure-dark/hor-basic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/EDNeutronAssistant/main/themes/ed-azure-dark/hor-basic.png -------------------------------------------------------------------------------- /themes/ed-azure-dark/hor-hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/EDNeutronAssistant/main/themes/ed-azure-dark/hor-hover.png -------------------------------------------------------------------------------- /themes/ed-azure-dark/off-basic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/EDNeutronAssistant/main/themes/ed-azure-dark/off-basic.png -------------------------------------------------------------------------------- /themes/ed-azure-dark/off-hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/EDNeutronAssistant/main/themes/ed-azure-dark/off-hover.png -------------------------------------------------------------------------------- /themes/ed-azure-dark/on-accent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/EDNeutronAssistant/main/themes/ed-azure-dark/on-accent.png -------------------------------------------------------------------------------- /themes/ed-azure-dark/rect-basic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/EDNeutronAssistant/main/themes/ed-azure-dark/rect-basic.png -------------------------------------------------------------------------------- /themes/ed-azure-dark/rect-hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/EDNeutronAssistant/main/themes/ed-azure-dark/rect-hover.png -------------------------------------------------------------------------------- /themes/ed-azure-dark/separator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/EDNeutronAssistant/main/themes/ed-azure-dark/separator.png -------------------------------------------------------------------------------- /themes/ed-azure-dark/tab-basic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/EDNeutronAssistant/main/themes/ed-azure-dark/tab-basic.png -------------------------------------------------------------------------------- /themes/ed-azure-dark/tab-hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/EDNeutronAssistant/main/themes/ed-azure-dark/tab-hover.png -------------------------------------------------------------------------------- /themes/ed-azure-dark/tree-basic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/EDNeutronAssistant/main/themes/ed-azure-dark/tree-basic.png -------------------------------------------------------------------------------- /themes/ed-azure-dark/tri-accent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/EDNeutronAssistant/main/themes/ed-azure-dark/tri-accent.png -------------------------------------------------------------------------------- /themes/ed-azure-dark/tri-basic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/EDNeutronAssistant/main/themes/ed-azure-dark/tri-basic.png -------------------------------------------------------------------------------- /themes/ed-azure-dark/tri-hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/EDNeutronAssistant/main/themes/ed-azure-dark/tri-hover.png -------------------------------------------------------------------------------- /themes/ed-azure-dark/up-accent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/EDNeutronAssistant/main/themes/ed-azure-dark/up-accent.png -------------------------------------------------------------------------------- /themes/ed-azure-dark/vert-basic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/EDNeutronAssistant/main/themes/ed-azure-dark/vert-basic.png -------------------------------------------------------------------------------- /themes/ed-azure-dark/vert-hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/EDNeutronAssistant/main/themes/ed-azure-dark/vert-hover.png -------------------------------------------------------------------------------- /screenshots/screenshot_exact_route.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/EDNeutronAssistant/main/screenshots/screenshot_exact_route.png -------------------------------------------------------------------------------- /themes/ed-azure-dark/box-invalid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/EDNeutronAssistant/main/themes/ed-azure-dark/box-invalid.png -------------------------------------------------------------------------------- /themes/ed-azure-dark/button-hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/EDNeutronAssistant/main/themes/ed-azure-dark/button-hover.png -------------------------------------------------------------------------------- /themes/ed-azure-dark/check-accent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/EDNeutronAssistant/main/themes/ed-azure-dark/check-accent.png -------------------------------------------------------------------------------- /themes/ed-azure-dark/check-basic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/EDNeutronAssistant/main/themes/ed-azure-dark/check-basic.png -------------------------------------------------------------------------------- /themes/ed-azure-dark/check-hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/EDNeutronAssistant/main/themes/ed-azure-dark/check-hover.png -------------------------------------------------------------------------------- /themes/ed-azure-dark/circle-accent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/EDNeutronAssistant/main/themes/ed-azure-dark/circle-accent.png -------------------------------------------------------------------------------- /themes/ed-azure-dark/circle-basic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/EDNeutronAssistant/main/themes/ed-azure-dark/circle-basic.png -------------------------------------------------------------------------------- /themes/ed-azure-dark/circle-hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/EDNeutronAssistant/main/themes/ed-azure-dark/circle-hover.png -------------------------------------------------------------------------------- /themes/ed-azure-dark/down-accent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/EDNeutronAssistant/main/themes/ed-azure-dark/down-accent.png -------------------------------------------------------------------------------- /themes/ed-azure-dark/outline-basic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/EDNeutronAssistant/main/themes/ed-azure-dark/outline-basic.png -------------------------------------------------------------------------------- /themes/ed-azure-dark/outline-hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/EDNeutronAssistant/main/themes/ed-azure-dark/outline-hover.png -------------------------------------------------------------------------------- /themes/ed-azure-dark/radio-accent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/EDNeutronAssistant/main/themes/ed-azure-dark/radio-accent.png -------------------------------------------------------------------------------- /themes/ed-azure-dark/radio-basic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/EDNeutronAssistant/main/themes/ed-azure-dark/radio-basic.png -------------------------------------------------------------------------------- /themes/ed-azure-dark/radio-hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/EDNeutronAssistant/main/themes/ed-azure-dark/radio-hover.png -------------------------------------------------------------------------------- /themes/ed-azure-dark/rect-accent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/EDNeutronAssistant/main/themes/ed-azure-dark/rect-accent.png -------------------------------------------------------------------------------- /themes/ed-azure-dark/tab-disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/EDNeutronAssistant/main/themes/ed-azure-dark/tab-disabled.png -------------------------------------------------------------------------------- /themes/ed-azure-dark/tree-pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/EDNeutronAssistant/main/themes/ed-azure-dark/tree-pressed.png -------------------------------------------------------------------------------- /themes/ed-azure-dark/vert-accent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/EDNeutronAssistant/main/themes/ed-azure-dark/vert-accent.png -------------------------------------------------------------------------------- /screenshots/screenshot_simple_route.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/EDNeutronAssistant/main/screenshots/screenshot_simple_route.png -------------------------------------------------------------------------------- /themes/ed-azure-dark/rect-accent-hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gobidev/EDNeutronAssistant/main/themes/ed-azure-dark/rect-accent-hover.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Gobidev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from cx_Freeze import setup, Executable 2 | from EDNeutronAssistant import __version__ 3 | 4 | NAME = "EDNeutronAssistant" 5 | 6 | shortcut_table = [ 7 | ( 8 | "DesktopShortcut", 9 | "DesktopFolder", 10 | NAME, 11 | "TARGETDIR", 12 | "[TARGETDIR]EDNeutronAssistant.exe", 13 | None, 14 | None, 15 | None, 16 | None, 17 | None, 18 | None, 19 | 'TARGETDIR' 20 | ), 21 | ( 22 | "StartMenuShortcut", 23 | "StartMenuFolder", 24 | NAME, 25 | "TARGETDIR", 26 | "[TARGETDIR]EDNeutronAssistant.exe", 27 | None, 28 | None, 29 | "logo.ico", 30 | None, 31 | None, 32 | None, 33 | 'TARGETDIR' 34 | ) 35 | ] 36 | 37 | setup( 38 | name="EDNeutronAssistant", 39 | version=__version__[1:], 40 | options={ 41 | "build_exe": { 42 | "packages": ["os", "sys", "time", "requests", "urllib.parse", "tkinter", "tkinter.ttk", "clipboard", "json", 43 | "threading", "tkinter.messagebox", "webbrowser", "io", "base64", "gzip"], 44 | "include_files": ["logo.ico", "themes"], 45 | "include_msvcr": True 46 | }, 47 | "bdist_msi": { 48 | "install_icon": "logo.ico", 49 | "target_name": "EDNeutronAssistant_Installer.msi", 50 | "data": {"Shortcut": shortcut_table} 51 | } 52 | }, 53 | executables=[ 54 | Executable( 55 | "EDNeutronAssistant.py", 56 | base="Win32GUI", 57 | icon="logo.ico", 58 | ) 59 | ] 60 | ) 61 | -------------------------------------------------------------------------------- /autocomplete.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | import tkinter.ttk as ttk 3 | import json 4 | import requests 5 | import threading 6 | import urllib.parse 7 | 8 | 9 | class SystemAutocompleteCombobox(ttk.Combobox): 10 | 11 | def __init__(self, *args, **kwargs): 12 | ttk.Combobox.__init__(self, *args, **kwargs) 13 | self.completion_list = [] 14 | self.hits = [] 15 | self.hit_index = 0 16 | self.position = 0 17 | self.bind('', self.handle_keyrelease) 18 | self['values'] = self.completion_list 19 | 20 | def set_completion_list(self, completion_list): 21 | self.completion_list = sorted(completion_list, key=str.lower) 22 | self.hits = [] 23 | self.hit_index = 0 24 | self.position = 0 25 | self.bind('', self.handle_keyrelease) 26 | self['values'] = self.completion_list 27 | 28 | def autocomplete(self, delta=0): 29 | if delta: 30 | self.delete(self.position, tk.END) 31 | else: 32 | self.position = len(self.get()) 33 | 34 | hits = [] 35 | for element in self.completion_list: 36 | if element.lower().startswith(self.get().lower()): 37 | hits.append(element) 38 | 39 | if hits != self.hits: 40 | self.hit_index = 0 41 | self.hits = hits 42 | 43 | if hits == self.hits and self.hits: 44 | self.hit_index = (self.hit_index + delta) % len(self.hits) 45 | 46 | if self.hits: 47 | self.delete(0, tk.END) 48 | self.insert(0, self.hits[self.hit_index]) 49 | self.select_range(self.position, tk.END) 50 | 51 | def handle_keyrelease(self, event): 52 | 53 | threading.Thread(target=self.update_completion_list).start() 54 | 55 | if event.keysym == "BackSpace": 56 | self.delete(self.index(tk.INSERT), tk.END) 57 | self.position = self.index(tk.END) 58 | if event.keysym == "Left": 59 | if self.position < self.index(tk.END): 60 | self.delete(self.position, tk.END) 61 | else: 62 | self.position = self.position - 1 63 | self.delete(self.position, tk.END) 64 | if event.keysym == "Right": 65 | self.position = self.index(tk.END) 66 | if len(event.keysym) == 1: 67 | self.autocomplete() 68 | 69 | def update_completion_list(self): 70 | self.set_completion_list( 71 | json.loads(requests.get( 72 | f"https://www.spansh.co.uk/api/systems?q={urllib.parse.quote_plus(self.get())}").text)) 73 | print(self.completion_list) 74 | -------------------------------------------------------------------------------- /menu.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | import tkinter.ttk as ttk 3 | import tkinter.messagebox 4 | import webbrowser 5 | import os 6 | import json 7 | 8 | if os.name == "nt": 9 | USER_SETTINGS_CONFIG_PATH = os.path.join(os.getenv("APPDATA"), "EDNeutronAssistant", "user_settings.json") 10 | else: 11 | USER_SETTINGS_CONFIG_PATH = os.path.join(os.path.expanduser("~"), ".config", "EDNeutronAssistant", 12 | "user_settings.json") 13 | 14 | USER_SETTINGS = {"theme": 1} 15 | if os.path.isfile(USER_SETTINGS_CONFIG_PATH): 16 | USER_SETTINGS = json.load(open(USER_SETTINGS_CONFIG_PATH, "r")) 17 | 18 | 19 | def save_user_settings(): 20 | with open(USER_SETTINGS_CONFIG_PATH, "w") as f: 21 | json.dump(USER_SETTINGS, f, indent=2) 22 | 23 | 24 | class ThemeSelectionFrame(ttk.Frame): 25 | 26 | def __init__(self, master, *args, **kwargs): 27 | super().__init__(*args, **kwargs) 28 | 29 | self.master = master 30 | self.main_application = master.master.master 31 | 32 | self.theme_variable = tk.IntVar() 33 | self.theme_variable.set(USER_SETTINGS["theme"]) 34 | 35 | # Row 0 36 | self.theme_lbl = ttk.Label(self, text="Theme") 37 | self.theme_lbl.grid(row=0, column=0, padx=3, pady=3, sticky="W") 38 | 39 | # Row 1 40 | self.default_theme_radiobutton = ttk.Radiobutton(self, text="Default", variable=self.theme_variable, value=0, 41 | command=self.update_theme) 42 | self.default_theme_radiobutton.grid(row=1, column=0, padx=3, pady=3, sticky="W") 43 | 44 | # Row 2 45 | self.dark_theme_radiobutton = ttk.Radiobutton(self, text="Dark", variable=self.theme_variable, value=1, 46 | command=self.update_theme) 47 | self.dark_theme_radiobutton.grid(row=2, column=0, padx=3, pady=3, sticky="W") 48 | 49 | def update_theme(self): 50 | old_theme = USER_SETTINGS["theme"] 51 | new_theme = self.theme_variable.get() 52 | 53 | if old_theme != new_theme: 54 | USER_SETTINGS["theme"] = new_theme 55 | save_user_settings() 56 | 57 | if new_theme == 0: 58 | tk.messagebox.showinfo("Restart required", "Please restart the program to apply the new theme.") 59 | else: 60 | self.main_application.apply_theme(new_theme) 61 | 62 | 63 | class OptionsMenu(tk.Toplevel): 64 | 65 | def __init__(self, master, *args, **kwargs): 66 | super().__init__(*args, **kwargs) 67 | 68 | self.main_application = master 69 | 70 | # Configure top level 71 | self.focus_set() 72 | self.grab_set() 73 | self.protocol("WM_DELETE_WINDOW", self.terminate) 74 | 75 | # Row 0 76 | self.theme_selection_frame = ThemeSelectionFrame(self, self) 77 | self.theme_selection_frame.grid(row=0, column=0, padx=3, pady=3) 78 | 79 | def terminate(self): 80 | self.grab_release() 81 | self.destroy() 82 | 83 | 84 | class MainMenuButton(ttk.Menubutton): 85 | 86 | def __init__(self, master, *args, **kwargs): 87 | super().__init__(*args, **kwargs) 88 | 89 | self.master = master 90 | 91 | self.menu = tk.Menu(self, tearoff=0) 92 | self["menu"] = self.menu 93 | 94 | self["text"] = "Menu" 95 | 96 | self.menu.add_command(label="Settings", command=self.on_settings) 97 | self.menu.add_command(label="About", command=lambda: webbrowser.open_new_tab( 98 | "https://github.com/Gobidev/EDNeutronAssistant#edneutronassistant")) 99 | self.menu.add_command(label="Exit", command=self.on_exit) 100 | 101 | def on_settings(self): 102 | OptionsMenu(self, self) 103 | 104 | def on_exit(self): 105 | if isinstance(self.master, tk.Tk): 106 | self.master.destroy() 107 | else: 108 | self.master.terminate() 109 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EDNeutronAssistant 2 | [![Github Downloads all Releases](https://img.shields.io/github/downloads/Gobidev/EDNeutronAssistant/total)](https://github.com/Gobidev/EDNeutronAssistant/releases) 3 | [![Github Downloads latest Release](https://img.shields.io/github/downloads/Gobidev/EDNeutronAssistant/latest/total)](https://github.com/Gobidev/EDNeutronAssistant/releases/latest) 4 | [![License](https://img.shields.io/github/license/Gobidev/EDNeutronAssistant)](https://github.com/Gobidev/EDNeutronAssistant/blob/main/LICENSE) 5 | [![GitHub issues](https://img.shields.io/github/issues/Gobidev/EDNeutronAssistant)](https://github.com/Gobidev/EDNeutronAssistant/issues) 6 | 7 | 8 | A tool for Elite Dangerous that helps to use Neutron Highways 9 | 10 | This project is a continuation of [EDRouteManager](https://github.com/Gobidev/EDRouteManager) that I made a while ago 11 | but never really finished. Because I became much better at coding 12 | since then, I decided to rework the project from scratch rather than continuing it. 13 | 14 | The main features of this program are easy calculation of Neutron Highways and automatically copying the next system of 15 | the route to the clipboard to avoid the need to tab out of the game in every system. 16 | 17 | ## Planned Features 18 | - [x] Linux Steam Play support 19 | - [ ] Linux standalone binary 20 | - [ ] custom game log directory 21 | - [ ] Implementation of other Spansh plotters 22 | - [x] dark mode 23 | - [ ] UI redesign 24 | 25 | ## Installation 26 | 27 | ### Windows 28 | Running the program on Windows is as easy as downloading the latest standalone EXE or MSI installer from the 29 | [releases tab](https://github.com/Gobidev/EDNeutronAssistant/releases/) and running it. 30 | 31 | ### Linux 32 | Note: EDNeutronAssistant is still a bit unstable on linux, but support will improve over time. 33 | 34 | To run the program on linux, you can follow these steps: 35 | 36 | - Install Python 3.9 (older versions might work, but not tested) 37 | - Install the required packages with your package manager 38 | 39 | **Debian-based distros (Ubuntu, Linux MINT, etc.):** 40 | 41 | `$ sudo apt install python3 python3-pip python3-tk xsel git` 42 | 43 | **RHEL-based distros (Fedora, etc.):** 44 | 45 | `$ sudo dnf install python3 python3-pip python3-tkinter xsel git` 46 | 47 | - Clone this repository 48 | 49 | `$ git clone https://github.com/Gobidev/EDNeutronAssistant.git` 50 | 51 | - Install the required pip packages 52 | 53 | `$ pip3 install -r EDNeutronAssistant/requirements.txt` 54 | 55 | - Run EDNeutronAssistant.py 56 | 57 | `$ python3 EDNeutronAssistant/EDNeutronAssistant.py` 58 | 59 | To hide the console, you can run the program inside a screen or tmux session and then detach from that. 60 | 61 | ## Usage 62 | The UI of EDNeutronAssistant is split into three main parts. 63 | 64 | ![alt_text](https://github.com/Gobidev/EDNeutronAssistant/raw/main/screenshots/screenshot_simple_route.png) 65 | 66 | At the top, the current status of the route like the current system and information about the next system are displayed. 67 | 68 | Below that, the application log shows exact information about what happened when. This is mainly useful for identifying 69 | issues and better understanding the functionality of the program. 70 | 71 | To calculate a route, the route calculator at the bottom provides two options: 72 | 73 | You can either calculate a simple neutron route, which uses the 74 | [Spansh Neutron Router](https://www.spansh.co.uk/plotter) to quickly calculate a route using Neutron Highways. The Source 75 | System, Efficiency and Jump Range will automatically be filled in based on your current location and ship build. 76 | 77 | ![alt_text](https://github.com/Gobidev/EDNeutronAssistant/raw/main/screenshots/screenshot_exact_route.png) 78 | 79 | The second option is to calculate an exact route, which uses the [Spansh Galaxy Plotter](https://www.spansh.co.uk/exact-plotter) 80 | to calculate a route that considers ship fuel usage, refueling stars and neutron supercharges, but be aware that the calculation 81 | of this kind of route can take up to several minutes. The route will be calculated 82 | based on your current ship build, so make sure to be in the ship that you want to use for the route. 83 | 84 | ## Bug Reporting 85 | If you run into any issues while using the program or have suggestions for additional features, create an 86 | [Issue](https://github.com/Gobidev/EDNeutronAssistant/issues) so I can take a look at it and make the experience as smooth 87 | as possible. 88 | 89 | ## Special Thanks 90 | Special thanks to Gareth Harper (Spansh), the creator of the Spansh plotters. Without these plotters, this program would never have 91 | existed and Neutron Highways in Elite: Dangerous wouldn't be nearly as accessible. 92 | 93 | ## License 94 | This project is licensed under the MIT license -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import clipboard 4 | import gzip 5 | import io 6 | import base64 7 | 8 | import api_access 9 | 10 | 11 | def parse_game_log(log_function=print, verbose=False) -> list: 12 | """Parses the Elite: Dangerous logfiles to retrieve information about the game""" 13 | 14 | # Check OS 15 | if verbose: 16 | log_function("Checking Host OS") 17 | 18 | # Get log file directory 19 | if os.name == "nt": 20 | windows_username = os.getlogin() 21 | file_directory = "C:\\Users\\" + windows_username + "\\Saved Games\\Frontier Developments\\Elite Dangerous" 22 | elif os.name == "posix": 23 | linux_home_dir = os.path.expanduser("~") 24 | file_directory = f"{linux_home_dir}/.local/share/Steam/steamapps/compatdata/359320/pfx/drive_c/users/" \ 25 | f"steamuser/Saved Games/Frontier Developments/Elite Dangerous/" 26 | else: 27 | log_function("Unsupported os") 28 | return [] 29 | 30 | if verbose: 31 | log_function("Searching game log directory") 32 | 33 | if not os.path.isdir(file_directory): 34 | log_function("Game logs not found") 35 | return [] 36 | 37 | if verbose: 38 | log_function(f"Reading game log from {file_directory}") 39 | 40 | all_files = [os.path.join(file_directory, file) for file in os.listdir(file_directory)] 41 | 42 | # Filter files that are no logs 43 | journal_files = [] 44 | for file in all_files: 45 | 46 | if "Journal" in file: 47 | journal_files.append(file) 48 | 49 | newest_log_file = max(journal_files, key=os.path.getctime) 50 | 51 | # Read log file 52 | if verbose: 53 | log_function(f"Reading log file {newest_log_file}") 54 | 55 | with open(newest_log_file, "r", encoding="utf-8") as f: 56 | all_entries = f.readlines() 57 | 58 | entries_parsed = [] 59 | for entry in all_entries: 60 | entries_parsed.append(json.loads(entry)) 61 | 62 | return entries_parsed 63 | 64 | 65 | def get_current_system_from_log(entries_parsed: list, log_function=print, verbose=False) -> str: 66 | """Return name of the last star system the commander visited""" 67 | 68 | if verbose: 69 | log_function("Looking for current system") 70 | 71 | all_session_systems = [] 72 | for entry in entries_parsed: 73 | if "StarSystem" in entry: 74 | if len(all_session_systems) == 0 or all_session_systems[-1] != entry["StarSystem"]: 75 | all_session_systems.append(entry["StarSystem"]) 76 | 77 | if len(all_session_systems) > 0: 78 | current_system = all_session_systems[-1] 79 | if verbose: 80 | log_function(f"Found system {current_system}") 81 | else: 82 | current_system = "" 83 | if verbose: 84 | print("No current system found") 85 | 86 | return current_system 87 | 88 | 89 | def get_commander_name_from_log(entries_parsed: list, log_function=print, verbose=False) -> str: 90 | """Parse log file for commander name""" 91 | 92 | if verbose: 93 | log_function("Looking for commander name") 94 | 95 | commander_name = "" 96 | 97 | for entry in entries_parsed: 98 | if entry["event"] == "Commander": 99 | commander_name = entry["Name"] 100 | break 101 | elif "Commander" in entry: 102 | commander_name = entry["Commander"] 103 | break 104 | 105 | if verbose: 106 | if commander_name: 107 | log_function(f"Found name {commander_name}") 108 | else: 109 | log_function("No commander name found") 110 | 111 | return commander_name 112 | 113 | 114 | def get_latest_loadout_event_from_log(entries_parsed: list) -> dict: 115 | """Parse game log for loadout events and return newest""" 116 | 117 | latest_log_loadout_event = None 118 | for log_entry in reversed(entries_parsed): 119 | if log_entry["event"] == "Loadout": 120 | latest_log_loadout_event = log_entry 121 | break 122 | 123 | return latest_log_loadout_event 124 | 125 | 126 | def get_approx_ship_range(entries_parsed: list, log_function=print, verbose=False) -> float: 127 | """Get the approximate jump range from log file entries""" 128 | 129 | if verbose: 130 | log_function("Looking for ship jump range") 131 | 132 | jump_range = 0 133 | 134 | all_ranges = [] 135 | for entry in entries_parsed: 136 | if entry["event"] == "Loadout": 137 | all_ranges.append(float(entry["MaxJumpRange"])) 138 | 139 | if len(all_ranges): 140 | jump_range = all_ranges[-1] 141 | 142 | if verbose: 143 | if jump_range: 144 | log_function(f"Found jump range {jump_range}") 145 | else: 146 | log_function("No jump range found") 147 | 148 | final_range = round(.95 * jump_range, 2) 149 | 150 | return final_range 151 | 152 | 153 | def parse_plotter_csv(filename: str, log_function=print) -> list: 154 | """Parse a file that was created with the spansh plotter, probably not used in final version""" 155 | 156 | log_function(f"Parsing route file {filename}") 157 | 158 | def parse_line(line_: str) -> dict: 159 | line_elements = line_.replace('"', "").replace("\n", "").split(",") 160 | return dict(zip(line_keys, line_elements)) 161 | 162 | with open(filename, "r") as f: 163 | all_lines = f.readlines() 164 | 165 | output = [] 166 | line_keys = eval(all_lines[0]) 167 | del all_lines[0] 168 | for line in all_lines: 169 | output.append(parse_line(line)) 170 | return output 171 | 172 | 173 | def test_if_builds_has_jump_range(build: dict, jump_range: float) -> bool: 174 | """Test if two builds are equal based on their jump range""" 175 | 176 | return True if round(build["MaxJumpRange"], 2) == jump_range else False 177 | 178 | 179 | def get_nearest_system_in_route(plotter_data: list, current_system: str) -> str: 180 | """Calculate which system of a route is the nearest to the current system, can take a long time to process""" 181 | all_systems = [] 182 | for entry in plotter_data: 183 | all_systems.append(entry["system"]) 184 | 185 | all_distances = {} 186 | for system in all_systems: 187 | distance = api_access.get_distance_between_systems(system, current_system) 188 | all_distances[system] = distance 189 | print(f"Distance to {system} is {distance}") 190 | 191 | return min(all_distances, key=all_distances.get) 192 | 193 | 194 | def copy_system_to_clipboard(system: str, log_function=print): 195 | """Copy a system into the commanders clipboard""" 196 | clipboard.copy(system) 197 | log_function(f"Copied system {system} to clipboard") 198 | 199 | 200 | def get_coriolis_url(loadout_event: dict) -> str: 201 | """Create url from loadout event that imports ship build into coriolis.io""" 202 | 203 | def encode_loadout_event(loadout_event_: dict) -> str: 204 | string = json.dumps(loadout_event_, ensure_ascii=False, sort_keys=True, separators=(',', ':')).encode('utf-8') 205 | out = io.BytesIO() 206 | 207 | with gzip.GzipFile(fileobj=out, mode="w") as f: 208 | f.write(string) 209 | 210 | return base64.urlsafe_b64encode(out.getvalue()).decode().replace("=", "%3D") 211 | 212 | return f"https://coriolis.io/import?data={encode_loadout_event(loadout_event)}" 213 | -------------------------------------------------------------------------------- /api_access.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import urllib.parse 4 | import requests 5 | import time 6 | import hashlib 7 | 8 | from EDNeutronAssistant import __version__ 9 | 10 | REQUEST_HEADERS = {"user-agent": f"EDNeutronAssistant_{__version__}"} 11 | 12 | 13 | def update_available(current_version: str) -> (bool, str): 14 | """Check for available update on GitHub and return version if available""" 15 | 16 | newest_version = requests.get("https://api.github.com/repos/Gobidev/EDNeutronAssistant/releases/latest").json() 17 | newest_version = newest_version["tag_name"] 18 | 19 | if newest_version != current_version: 20 | return True, newest_version 21 | else: 22 | return False, current_version 23 | 24 | 25 | def get_distance_between_systems(system1: str, system2: str, log_function=print, verbose=False) -> float: 26 | """Calculate the distance between two systems using the EDSM API""" 27 | 28 | if not (system1 and system2): 29 | return 0 30 | 31 | if verbose: 32 | log_function(f"Calculating distance between systems {system1} and {system2}") 33 | 34 | def get_coordinates(system: str) -> dict: 35 | """Retrieve the coordinates of a system from the EDSM API""" 36 | 37 | if verbose: 38 | log_function(f"Retrieving coordinates of system {system} from EDSM API") 39 | 40 | system = urllib.parse.quote_plus(system) 41 | response = requests.get(f"https://www.edsm.net/api-v1/system?systemName={system}" 42 | f"&showCoordinates=1", headers=REQUEST_HEADERS) 43 | 44 | coordinates = json.loads(response.text)["coords"] 45 | 46 | if verbose: 47 | log_function(f"Coordinates of system {system} are {coordinates}") 48 | 49 | return coordinates 50 | 51 | s1coordinates = get_coordinates(system1) 52 | s2coordinates = get_coordinates(system2) 53 | 54 | distance = round( 55 | ((s2coordinates["x"] - s1coordinates["x"]) ** 2 + (s2coordinates["y"] - s1coordinates["y"]) ** 2 + 56 | (s2coordinates["z"] - s1coordinates["z"]) ** 2) ** (1 / 2), 2) 57 | 58 | if verbose: 59 | log_function(f"Distance between systems {system1} and {system2} is {distance}") 60 | 61 | return distance 62 | 63 | 64 | def convert_system_name_for_file(system_name: str) -> str: 65 | """Convert ship name to match file name criteria""" 66 | return system_name.replace(" ", "_").replace("*", "") 67 | 68 | 69 | def calc_simple_neutron_route(efficiency: int, ship_range: float, start_system: str, end_system: str, 70 | config_path: str, log_function=print) -> list: 71 | """Use the Spansh API to calculate a neutron star route""" 72 | 73 | log_function(f"Calculating route from {start_system} to {end_system} with efficiency {efficiency} and jump " 74 | f"range {ship_range}") 75 | 76 | routes_dir = os.path.join(config_path, "routes") 77 | 78 | if not os.path.isdir(routes_dir): 79 | os.makedirs(routes_dir) 80 | 81 | filename = f"NeutronAssistantSimpleRoute-{efficiency}-{ship_range}-" \ 82 | f"{convert_system_name_for_file(start_system)}-{convert_system_name_for_file(end_system)}.json" 83 | 84 | filename = os.path.join(routes_dir, filename) 85 | 86 | # Test if route was calculated before 87 | if not os.path.isfile(filename): 88 | log_function("Route was not calculated before, requesting from API") 89 | 90 | payload = {"efficiency": efficiency, "range": ship_range, "from": start_system, "to": end_system} 91 | response = requests.post("https://www.spansh.co.uk/api/route", data=payload, headers=REQUEST_HEADERS) 92 | job = eval(response.text) 93 | 94 | log_function("Request sent, waiting for completion") 95 | 96 | if "error" in job: 97 | log_function(f"ERROR OCCURRED: {job['error']}") 98 | return [] 99 | 100 | # Wait for job completion 101 | while 1: 102 | response = requests.get("https://www.spansh.co.uk/api/results/" + job["job"], headers=REQUEST_HEADERS) 103 | response_dict = json.loads(response.text) 104 | if response_dict["status"] == "ok": 105 | log_function("Route successfully received") 106 | break 107 | time.sleep(1) 108 | 109 | systems = response_dict["result"]["system_jumps"] 110 | 111 | # Write results to file 112 | log_function("Saving route") 113 | 114 | with open(filename, "w") as f: 115 | json.dump(systems, f, indent=2) 116 | log_function("Route has been saved") 117 | 118 | else: 119 | log_function("Found existing route, reading") 120 | systems = json.load(open(filename, "r")) 121 | 122 | return systems 123 | 124 | 125 | def calc_exact_neutron_route(start_system: str, end_system: str, ship_coriolis_build: dict, cargo: int, 126 | already_supercharged: bool, use_supercharge: bool, use_injections: bool, 127 | exclude_secondary_stars: bool, config_path: str, log_function=print) -> list: 128 | """Use the Spansh API to calculate an exact neutron route""" 129 | 130 | fsd_data = requests.get("https://raw.githubusercontent.com/EDCD/coriolis-data/" 131 | "master/modules/standard/frame_shift_drive.json").json()["fsd"] 132 | 133 | def calculate_optimal_mass(coriolis_build: dict) -> float: 134 | build_fsd = coriolis_build["components"]["standard"]["frameShiftDrive"] 135 | 136 | optimal_mass = 0 137 | for fsd in fsd_data: 138 | if fsd["class"] == build_fsd["class"] and fsd["rating"] == build_fsd["rating"]: 139 | optimal_mass = fsd["optmass"] 140 | 141 | # test if modified 142 | if "blueprint" in build_fsd: 143 | modification_grade = build_fsd["blueprint"]["grade"] 144 | multiplier = build_fsd["blueprint"]["grades"][str(modification_grade)]["features"]["optmass"][1] 145 | 146 | # test for experimental effect 147 | if "special" in build_fsd["blueprint"]: 148 | if build_fsd["blueprint"]["special"]["name"] == "Mass Manager": 149 | multiplier += .062 150 | 151 | optimal_mass *= multiplier + 1 152 | 153 | return round(optimal_mass) 154 | 155 | def get_fuel_power_of_build(coriolis_build: dict) -> float: 156 | build_fsd = coriolis_build["components"]["standard"]["frameShiftDrive"] 157 | 158 | fuel_power = 0 159 | for fsd in fsd_data: 160 | if fsd["class"] == build_fsd["class"] and fsd["rating"] == build_fsd["rating"]: 161 | fuel_power = fsd["fuelpower"] 162 | 163 | return fuel_power 164 | 165 | def get_fuel_multiplier_of_build(coriolis_build: dict) -> float: 166 | build_fsd = coriolis_build["components"]["standard"]["frameShiftDrive"] 167 | 168 | fuel_multiplier = 0 169 | for fsd in fsd_data: 170 | if fsd["class"] == build_fsd["class"] and fsd["rating"] == build_fsd["rating"]: 171 | fuel_multiplier = fsd["fuelmul"] 172 | 173 | return fuel_multiplier 174 | 175 | def get_fsd_fuel_usage_of_build(coriolis_build: dict) -> float: 176 | build_fsd = coriolis_build["components"]["standard"]["frameShiftDrive"] 177 | 178 | fsd_fuel_usage = 0 179 | for fsd in fsd_data: 180 | if fsd["class"] == build_fsd["class"] and fsd["rating"] == build_fsd["rating"]: 181 | fsd_fuel_usage = fsd["maxfuel"] 182 | 183 | return fsd_fuel_usage 184 | 185 | def calculate_range_boost(coriolis_build: dict) -> float: 186 | class_boost_dict = {"1": 4.0, "2": 6.0, "3": 7.8, "4": 9.3, "5": 10.5} 187 | 188 | boost = 0.0 189 | for module in coriolis_build["components"]["internal"]: 190 | if module and "group" in module and module["group"] == "Guardian Frame Shift Drive Booster": 191 | boost = class_boost_dict[str(module["class"])] 192 | 193 | return boost 194 | 195 | log_function(f"Calculating exact route from {start_system} to {end_system}") 196 | 197 | routes_dir = os.path.join(config_path, "routes") 198 | 199 | if not os.path.isdir(routes_dir): 200 | os.makedirs(routes_dir) 201 | 202 | ship_code_hash = hashlib.md5(ship_coriolis_build["references"][0]["code"].encode("utf-8")).hexdigest()[:5] 203 | filename = f"NeutronAssistantExactRoute--" \ 204 | f"{convert_system_name_for_file(start_system)}-" \ 205 | f"{convert_system_name_for_file(end_system)}-{ship_code_hash}-{cargo}-" \ 206 | f"{'Y' if already_supercharged else 'N'}-" \ 207 | f"{'Y' if use_supercharge else 'N'}-{'Y' if use_injections else 'N'}-" \ 208 | f"{'Y' if exclude_secondary_stars else 'N'}.json " 209 | 210 | filename = os.path.join(routes_dir, filename) 211 | 212 | # Test if route was calculated before 213 | if not os.path.isfile(filename): 214 | log_function("Route was not calculated before, requesting from API") 215 | log_function("This might take a while") 216 | 217 | payload = { 218 | "source": start_system, 219 | "destination": end_system, 220 | "is_supercharged": 1 if already_supercharged else 0, 221 | "use_supercharge": 1 if use_supercharge else 0, 222 | "exclude_secondary": 1 if exclude_secondary_stars else 0, 223 | "tank_size": ship_coriolis_build["stats"]["fuelCapacity"], 224 | "cargo": cargo, 225 | "optimal_mass": calculate_optimal_mass(ship_coriolis_build), 226 | "base_mass": ship_coriolis_build["stats"]["unladenMass"] + ship_coriolis_build["stats"][ 227 | "reserveFuelCapacity"], 228 | "internal_tank_size": ship_coriolis_build["stats"]["reserveFuelCapacity"], 229 | "max_fuel_per_jump": get_fsd_fuel_usage_of_build(ship_coriolis_build), 230 | "range_boost": calculate_range_boost(ship_coriolis_build), 231 | "fuel_power": get_fuel_power_of_build(ship_coriolis_build), 232 | "fuel_multiplier": get_fuel_multiplier_of_build(ship_coriolis_build), 233 | "ship_build": ship_coriolis_build 234 | } 235 | 236 | response = requests.post("https://www.spansh.co.uk/api/generic/route", data=payload, headers=REQUEST_HEADERS) 237 | job = eval(response.text) 238 | 239 | log_function("Request sent, waiting for completion") 240 | 241 | if "error" in job: 242 | log_function(f"ERROR OCCURRED: {job['error']}") 243 | return [] 244 | 245 | # Wait for job completion 246 | while 1: 247 | response = requests.get("https://www.spansh.co.uk/api/results/" + job["job"], headers=REQUEST_HEADERS) 248 | response_dict = json.loads(response.text) 249 | if response_dict["status"] == "ok": 250 | log_function("Route successfully received") 251 | break 252 | time.sleep(4) 253 | 254 | systems = response_dict["result"]["jumps"] 255 | 256 | # Write results to file 257 | log_function("Saving route") 258 | 259 | with open(filename, "w") as f: 260 | json.dump(systems, f, indent=2) 261 | log_function("Route has been saved") 262 | 263 | else: 264 | log_function("Found existing route, reading") 265 | systems = json.load(open(filename, "r")) 266 | 267 | return systems 268 | 269 | 270 | def convert_loadout_event_to_coriolis(loadout_event: dict) -> dict: 271 | """Convert loadout event to coriolis ship build standard""" 272 | 273 | return json.loads(requests.post("https://coriolis-api.gobidev.de/convert", json=loadout_event, 274 | headers=REQUEST_HEADERS).text) 275 | -------------------------------------------------------------------------------- /gui.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | import tkinter.ttk as ttk 3 | import threading 4 | 5 | try: 6 | from ctypes import windll 7 | # Avoid import error on systems other than windows 8 | except ImportError: 9 | windll = None 10 | 11 | import autocomplete 12 | import api_access 13 | 14 | 15 | class StatusInformation(ttk.Frame): 16 | def __init__(self, master, *args, **kwargs): 17 | super().__init__(*args, **kwargs) 18 | 19 | self.master = master 20 | 21 | # Row 0 22 | self.cmdr_lbl = ttk.Label(self, text="CMDR:") 23 | self.cmdr_lbl.grid(row=0, column=0, padx=3, pady=2, sticky="E") 24 | 25 | self.cmdr_lbl_content = ttk.Label(self, text="") 26 | self.cmdr_lbl_content.grid(row=0, column=1, padx=3, pady=2, sticky="W") 27 | 28 | # Row 1 29 | self.current_system_lbl = ttk.Label(self, text="Current System:") 30 | self.current_system_lbl.grid(row=1, column=0, padx=3, pady=2, sticky="E") 31 | 32 | self.current_system_lbl_content = ttk.Label(self, text="") 33 | self.current_system_lbl_content.grid(row=1, column=1, columnspan=2, padx=3, pady=2, sticky="W") 34 | 35 | # Row 2 36 | self.progress_lbl = ttk.Label(self, text="Progress:") 37 | self.progress_lbl.grid(row=2, column=0, padx=3, pady=2, sticky="E") 38 | 39 | self.progress_lbl_content = ttk.Label(self, text="") 40 | self.progress_lbl_content.grid(row=2, column=1, padx=3, pady=2, sticky="W") 41 | 42 | self.progress_bar = ttk.Progressbar(self, length=160) 43 | self.progress_bar.grid(row=2, column=2, padx=3, pady=2, sticky="W") 44 | 45 | self.progress_percentage = ttk.Label(self, text="") 46 | self.progress_percentage.grid(row=2, column=3, padx=3, pady=2, sticky="W") 47 | 48 | # Row 3 49 | self.next_system_lbl = ttk.Label(self, text="Next System:") 50 | self.next_system_lbl.grid(row=3, column=0, padx=3, pady=2, sticky="E") 51 | 52 | self.next_system_lbl_content = ttk.Label(self, text="") 53 | self.next_system_lbl_content.grid(row=3, column=1, columnspan=2, padx=3, pady=2, sticky="W") 54 | 55 | # Row 4 56 | self.next_system_information_lbl = ttk.Label(self, text="Information:") 57 | self.next_system_information_lbl.grid(row=4, column=0, padx=3, pady=2, sticky="E") 58 | 59 | self.next_system_information_lbl_content = ttk.Label(self, text="") 60 | self.next_system_information_lbl_content.grid(row=4, column=1, columnspan=3, padx=3, pady=2, sticky="W") 61 | 62 | # Row 5 63 | self.destination_information_lbl = ttk.Label(self, text="Destination:") 64 | self.destination_information_lbl.grid(row=5, column=0, padx=3, pady=2, sticky="E") 65 | 66 | self.destination_information_lbl_content = ttk.Label(self, text="") 67 | self.destination_information_lbl_content.grid(row=5, column=1, padx=3, pady=2, sticky="W") 68 | 69 | def update_cmdr_lbl(self, new_name: str): 70 | self.cmdr_lbl_content.configure(text=new_name) 71 | 72 | def update_current_system_lbl(self, new_system: str): 73 | self.current_system_lbl_content.configure(text=new_system) 74 | 75 | def update_next_system_info(self, new_system: str, distance: float, jumps: int, is_neutron: bool): 76 | self.next_system_lbl_content.configure(text=new_system) 77 | self.next_system_information_lbl_content.configure(text=f"{distance} ly {jumps} " 78 | f"{'Jumps' if jumps > 1 else 'Jump'} Neutron: " 79 | f"{'yes' if is_neutron else 'no'}") 80 | 81 | def update_progress_lbl(self, current: int, total: int): 82 | progress_percentage = round((current - 1) / (total - 1) * 100, 2) 83 | self.progress_lbl_content.configure(text=f"[{current}/{total}]") 84 | self.progress_bar.configure(value=progress_percentage) 85 | self.progress_percentage.configure(text=f"{progress_percentage}%") 86 | 87 | def set_destination(self, destination: str): 88 | self.destination_information_lbl_content.configure(text=destination) 89 | 90 | def reset_information(self): 91 | self.next_system_lbl_content.configure(text="") 92 | self.next_system_information_lbl_content.configure(text="") 93 | self.progress_lbl_content.configure(text="") 94 | self.destination_information_lbl_content.configure(text="") 95 | 96 | 97 | class LogFrame(ttk.Frame): 98 | 99 | def __init__(self, *args, **kwargs): 100 | super().__init__(*args, **kwargs) 101 | 102 | self.main_text_box = tk.Text(self, height=7, width=42, wrap="none") 103 | self.scrollbar_y = ttk.Scrollbar(self, orient="vertical", command=self.main_text_box.yview) 104 | self.scrollbar_x = ttk.Scrollbar(self, orient="horizontal", command=self.main_text_box.xview) 105 | self.main_text_box.configure(yscrollcommand=self.scrollbar_y.set, xscrollcommand=self.scrollbar_x.set, 106 | state="disabled") 107 | 108 | self.scrollbar_y.pack(side="right", fill="y") 109 | self.scrollbar_x.pack(side="bottom", fill="x") 110 | self.main_text_box.pack(side="left", fill="both", expand=True) 111 | 112 | def add_to_log(self, txt): 113 | self.main_text_box.configure(state="normal") 114 | self.main_text_box.insert("end", txt + "\n") 115 | self.main_text_box.see("end") 116 | self.main_text_box.configure(state="disabled") 117 | 118 | 119 | class SimpleRouteSelection(ttk.Frame): 120 | def __init__(self, master, *args, **kwargs): 121 | super().__init__(*args, **kwargs) 122 | 123 | self.master = master 124 | 125 | # Row 0 126 | self.from_lbl = ttk.Label(self, text="From:") 127 | self.from_lbl.grid(row=0, column=0, padx=3, pady=2, sticky="E") 128 | 129 | self.from_combobox = autocomplete.SystemAutocompleteCombobox(self) 130 | self.from_combobox.grid(row=0, column=1, columnspan=2, padx=3, pady=2, sticky="W") 131 | 132 | # Row 1 133 | self.to_lbl = ttk.Label(self, text="To:") 134 | self.to_lbl.grid(row=1, column=0, padx=3, pady=2, sticky="E") 135 | 136 | self.to_combobox = autocomplete.SystemAutocompleteCombobox(self) 137 | self.to_combobox.grid(row=1, column=1, columnspan=2, padx=3, pady=2, sticky="W") 138 | 139 | # Row 2 140 | self.efficiency_lbl = ttk.Label(self, text="Efficiency:") 141 | self.efficiency_lbl.grid(row=2, column=0, padx=3, pady=2, sticky="E") 142 | 143 | self.efficiency_entry = ttk.Entry(self) 144 | self.efficiency_entry.insert(0, "60") 145 | self.efficiency_entry.grid(row=2, column=1, padx=3, pady=2) 146 | 147 | # Row 3 148 | self.jump_range_lbl = ttk.Label(self, text="Jump Range:") 149 | self.jump_range_lbl.grid(row=3, column=0, padx=3, pady=2, sticky="E") 150 | 151 | self.jump_range_entry = ttk.Entry(self) 152 | self.jump_range_entry.grid(row=3, column=1, padx=3, pady=2) 153 | 154 | # Row 4 155 | self.calculate_button = ttk.Button(self, text="Calculate", command=self.on_calculate_button) 156 | self.calculate_button.grid(row=4, column=0, padx=3, pady=2) 157 | 158 | def calculate_thread(self): 159 | self.master.change_state_of_all_calculate_buttons("disabled") 160 | 161 | # Get values from ui 162 | from_system = self.from_combobox.get() 163 | to_system = self.to_combobox.get() 164 | try: 165 | efficiency = int(self.efficiency_entry.get()) 166 | jump_range = float(self.jump_range_entry.get()) 167 | except ValueError: 168 | self.master.print_log("Invalid input") 169 | self.master.change_state_of_all_calculate_buttons("normal") 170 | return 171 | 172 | if not (from_system and to_system): 173 | self.master.print_log("Invalid input") 174 | self.master.change_state_of_all_calculate_buttons("normal") 175 | return 176 | 177 | route_systems = api_access.calc_simple_neutron_route(efficiency, jump_range, from_system, to_system, 178 | self.master.config_path, 179 | log_function=self.master.print_log) 180 | 181 | self.master.change_state_of_all_calculate_buttons("normal") 182 | 183 | if len(route_systems) == 0: 184 | return 185 | 186 | self.master.print_log(f"Loaded route of {len(route_systems)} systems") 187 | self.master.configuration["route"] = route_systems 188 | self.master.configuration["route_type"] = "simple" 189 | self.master.write_config() 190 | 191 | def on_calculate_button(self): 192 | threading.Thread(target=self.calculate_thread).start() 193 | 194 | 195 | class ExactRouteSelection(ttk.Frame): 196 | def __init__(self, master, *args, **kwargs): 197 | super().__init__(*args, **kwargs) 198 | 199 | self.master = master 200 | 201 | self.already_supercharged_var = tk.IntVar() 202 | self.already_supercharged_var.set(0) 203 | 204 | self.use_supercharge_var = tk.IntVar() 205 | self.use_supercharge_var.set(1) 206 | 207 | self.use_injections_var = tk.IntVar() 208 | self.use_injections_var.set(0) 209 | 210 | self.exclude_secondary_var = tk.IntVar() 211 | self.exclude_secondary_var.set(1) 212 | 213 | # Row 0 214 | self.from_lbl = ttk.Label(self, text="From:") 215 | self.from_lbl.grid(row=0, column=0, padx=3, pady=2, sticky="E") 216 | 217 | self.from_combobox = autocomplete.SystemAutocompleteCombobox(self) 218 | self.from_combobox.grid(row=0, column=1, columnspan=2, padx=3, pady=2, sticky="W") 219 | 220 | # Row 1 221 | self.to_lbl = ttk.Label(self, text="To:") 222 | self.to_lbl.grid(row=1, column=0, padx=3, pady=2, sticky="E") 223 | 224 | self.to_combobox = autocomplete.SystemAutocompleteCombobox(self) 225 | self.to_combobox.grid(row=1, column=1, columnspan=2, padx=3, pady=2, sticky="W") 226 | 227 | # Row 2 228 | self.cargo_lbl = ttk.Label(self, text="Cargo:") 229 | self.cargo_lbl.grid(row=2, column=0, padx=3, pady=2, sticky="E") 230 | 231 | self.cargo_entry = ttk.Entry(self) 232 | self.cargo_entry.insert(0, "0") 233 | self.cargo_entry.grid(row=2, column=1, padx=3, pady=2, sticky="W") 234 | 235 | # Row 3 236 | self.already_supercharged_lbl = ttk.Label(self, text="Already Supercharged:") 237 | self.already_supercharged_lbl.grid(row=3, column=0, padx=3, pady=2, sticky="E") 238 | 239 | self.already_supercharged_check = ttk.Checkbutton(self, variable=self.already_supercharged_var) 240 | self.already_supercharged_check.grid(row=3, column=1, padx=3, pady=2, sticky="W") 241 | 242 | # Row 4 243 | self.use_supercharge_lbl = ttk.Label(self, text="Use Supercharge:") 244 | self.use_supercharge_lbl.grid(row=4, column=0, padx=3, pady=2, sticky="E") 245 | 246 | self.use_supercharge_check = ttk.Checkbutton(self, variable=self.use_supercharge_var) 247 | self.use_supercharge_check.grid(row=4, column=1, padx=3, pady=2, sticky="W") 248 | 249 | # Row 5 250 | self.use_injections_lbl = ttk.Label(self, text="Use FSD Injections:") 251 | self.use_injections_lbl.grid(row=5, column=0, padx=3, pady=2, sticky="E") 252 | 253 | self.use_injections_check = ttk.Checkbutton(self, variable=self.use_injections_var) 254 | self.use_injections_check.grid(row=5, column=1, padx=3, pady=2, sticky="W") 255 | 256 | # Row 6 257 | self.exclude_secondary_lbl = ttk.Label(self, text="Exclude Secondary Stars:") 258 | self.exclude_secondary_lbl.grid(row=6, column=0, padx=3, pady=2, sticky="E") 259 | 260 | self.exclude_secondary_check = ttk.Checkbutton(self, variable=self.exclude_secondary_var) 261 | self.exclude_secondary_check.grid(row=6, column=1, padx=3, pady=2, sticky="W") 262 | 263 | # Row 7 264 | self.calculate_button = ttk.Button(self, text="Calculate", command=self.on_calculate_button) 265 | self.calculate_button.grid(row=7, column=0, padx=3, pady=2, sticky="W") 266 | 267 | def calculate_thread(self): 268 | self.master.change_state_of_all_calculate_buttons("disabled") 269 | 270 | # Get values from ui 271 | from_system = self.from_combobox.get() 272 | to_system = self.to_combobox.get() 273 | cargo = self.cargo_entry.get() 274 | already_supercharged = True if self.already_supercharged_var.get() else False 275 | use_supercharge = True if self.use_supercharge_var.get() else False 276 | use_injections = True if self.use_injections_var.get() else False 277 | exclude_secondary = True if self.exclude_secondary_var.get() else False 278 | 279 | ship_build = self.master.configuration["ship_coriolis_build"] 280 | 281 | try: 282 | cargo = int(cargo) 283 | except ValueError: 284 | self.master.print_log("Invalid input") 285 | self.master.change_state_of_all_calculate_buttons("normal") 286 | return 287 | 288 | if not (from_system and to_system): 289 | self.master.print_log("Invalid input") 290 | self.master.change_state_of_all_calculate_buttons("normal") 291 | return 292 | 293 | route_systems = api_access.calc_exact_neutron_route(from_system, to_system, ship_build, cargo, 294 | already_supercharged, use_supercharge, use_injections, 295 | exclude_secondary, config_path=self.master.config_path, 296 | log_function=self.master.print_log) 297 | 298 | self.master.change_state_of_all_calculate_buttons("normal") 299 | 300 | if len(route_systems) == 0: 301 | return 302 | 303 | self.master.print_log(f"Loaded route of {len(route_systems)} systems") 304 | self.master.configuration["route"] = route_systems 305 | self.master.configuration["route_type"] = "exact" 306 | self.master.write_config() 307 | 308 | def on_calculate_button(self): 309 | threading.Thread(target=self.calculate_thread).start() 310 | 311 | 312 | def on_tab_changed(event): 313 | """Used in ttk Notebooks to resize height to current frame height""" 314 | 315 | event.widget.update_idletasks() 316 | 317 | tab = event.widget.nametowidget(event.widget.select()) 318 | event.widget.configure(height=tab.winfo_reqheight()) 319 | 320 | 321 | class RouteSelection(ttk.Notebook): 322 | def __init__(self, master, *args, **kwargs): 323 | super().__init__(*args, **kwargs) 324 | 325 | self.bind("<>", on_tab_changed) 326 | 327 | self.simple_route_selection_tab = SimpleRouteSelection(master) 328 | self.add(self.simple_route_selection_tab, text="Neutron Route") 329 | 330 | self.exact_route_selection_tab = ExactRouteSelection(master) 331 | self.add(self.exact_route_selection_tab, text="Exact Route") 332 | 333 | 334 | class TitleBarButton(tk.Button): 335 | def __init__(self, master, text, command=None): 336 | self.master = master 337 | self.text = text 338 | 339 | self.font = ("Arial Rounded MT Bold", 11, "bold") 340 | 341 | super().__init__(master, bd=0, font=self.font, padx=5, pady=2, fg=self.master.fg, bg=self.master.bg, 342 | activebackground=self.master.bga, activeforeground=self.master.fga, highlightthickness=0, 343 | text=self.text, command=command) 344 | 345 | self.bind('', self.on_enter) 346 | self.bind('', self.on_leave) 347 | 348 | def on_enter(self, _): 349 | self['bg'] = self.master.bga 350 | self["fg"] = self.master.fga 351 | 352 | def on_leave(self, _): 353 | self['bg'] = self.master.bg 354 | self["fg"] = self.master.fg 355 | 356 | 357 | class TitleBar(tk.Frame): 358 | 359 | def __init__(self, master, root_, title, bg, fg, bga, fga): 360 | super().__init__(master, bd=1, bg=bg) 361 | 362 | self.title = title 363 | self.master = master 364 | self.root = root_ 365 | self.bg = bg 366 | self.fg = fg 367 | self.bga = bga 368 | self.fga = fga 369 | 370 | self.x_win = 0 371 | self.y_win = 0 372 | 373 | self.minimized = False 374 | 375 | self.title_lbl = tk.Label(self, bg=self.bg, fg=self.fg) 376 | 377 | self.set_title(title) 378 | 379 | if isinstance(self.master, tk.Tk): 380 | close_command = self.root.destroy 381 | else: 382 | close_command = self.master.terminate 383 | 384 | self.close_button = TitleBarButton(self, "X", command=close_command) 385 | 386 | self.minimize_button = TitleBarButton(self, "-", command=self.minimize) 387 | 388 | self.title_lbl.pack(side="left") 389 | self.close_button.pack(side="right") 390 | self.minimize_button.pack(side="right") 391 | 392 | self.bind("", self.on_press) 393 | self.bind("", self.on_move) 394 | self.bind("", self.frame_mapped) 395 | 396 | def set_title(self, title: str): 397 | self.title = title 398 | self.title_lbl.configure(text=title) 399 | 400 | def on_press(self, event): 401 | self.x_win = event.x 402 | self.y_win = event.y 403 | 404 | def on_move(self, event): 405 | x = event.x_root - self.x_win 406 | y = event.y_root - self.y_win 407 | self.root.geometry(f'+{x}+{y}') 408 | 409 | def minimize(self): 410 | self.root.update_idletasks() 411 | self.root.overrideredirect(False) 412 | self.root.state('iconic') 413 | self.minimized = True 414 | 415 | def frame_mapped(self, _): 416 | self.root.update_idletasks() 417 | self.root.overrideredirect(True) 418 | self.root.state('normal') 419 | if self.minimized: 420 | self.root.after(10, lambda: set_app_window(self.root)) 421 | self.minimized = False 422 | 423 | 424 | def set_app_window(root_window: tk.Tk, verbose=False): 425 | hwnd = windll.user32.GetParent(root_window.winfo_id()) 426 | style = windll.user32.GetWindowLongPtrW(hwnd, -20) 427 | style = style & ~0x00000080 428 | style = style | 0x00040000 429 | res = windll.user32.SetWindowLongPtrW(hwnd, -20, style) 430 | if verbose: 431 | print(res) 432 | root_window.wm_withdraw() 433 | root_window.after(10, lambda: root_window.wm_deiconify()) 434 | -------------------------------------------------------------------------------- /themes/ed-azure-dark.tcl: -------------------------------------------------------------------------------- 1 | # This is a modified version of https://github.com/rdbende/Azure-ttk-theme to match the Elite Dangerous theme 2 | 3 | 4 | # Copyright (c) 2021 rdbende 5 | 6 | # Azure theme is a beautiful modern ttk theme inspired by Microsoft's fluent design. 7 | 8 | package require Tk 8.6 9 | 10 | namespace eval ttk::theme::ed-azure-dark { 11 | 12 | variable version 1.3 13 | package provide ttk::theme::ed-azure-dark $version 14 | variable colors 15 | array set colors { 16 | -fg "#FF8000" 17 | -bg "#000000" 18 | -disabledfg "#FF8000" 19 | -disabledbg "#1A1A1A" 20 | -selectfg "#000000" 21 | -selectbg "#FF8000" 22 | } 23 | 24 | proc LoadImages {imgdir} { 25 | variable I 26 | foreach file [glob -directory $imgdir *.png] { 27 | set img [file tail [file rootname $file]] 28 | set I($img) [image create photo -file $file -format png] 29 | } 30 | } 31 | 32 | LoadImages [file join [file dirname [info script]] ed-azure-dark] 33 | 34 | # Settings 35 | ttk::style theme create ed-azure-dark -parent default -settings { 36 | ttk::style configure . \ 37 | -background $colors(-bg) \ 38 | -foreground $colors(-fg) \ 39 | -troughcolor $colors(-bg) \ 40 | -focuscolor $colors(-selectbg) \ 41 | -selectbackground $colors(-selectbg) \ 42 | -selectforeground $colors(-selectfg) \ 43 | -fieldbackground $colors(-selectbg) \ 44 | -insertcolor $colors(-fg) \ 45 | -font TkDefaultFont \ 46 | -borderwidth 1 \ 47 | -relief flat 48 | 49 | ttk::style map . -foreground [list disabled $colors(-disabledfg)] 50 | 51 | tk_setPalette background [ttk::style lookup . -background] \ 52 | foreground [ttk::style lookup . -foreground] \ 53 | highlightColor [ttk::style lookup . -focuscolor] \ 54 | selectBackground [ttk::style lookup . -selectbackground] \ 55 | selectForeground [ttk::style lookup . -selectforeground] \ 56 | activeBackground [ttk::style lookup . -selectbackground] \ 57 | activeForeground [ttk::style lookup . -selectforeground] 58 | option add *font [ttk::style lookup . -font] 59 | 60 | 61 | # Layouts 62 | ttk::style layout TButton { 63 | Button.button -children { 64 | Button.padding -children { 65 | Button.label -side left -expand true 66 | } 67 | } 68 | } 69 | 70 | ttk::style layout Toolbutton { 71 | Toolbutton.button -children { 72 | Toolbutton.padding -children { 73 | Toolbutton.label -side left -expand true 74 | } 75 | } 76 | } 77 | 78 | ttk::style layout TMenubutton { 79 | Menubutton.button -children { 80 | Menubutton.padding -children { 81 | Menubutton.indicator -side right 82 | Menubutton.label -side right -expand true 83 | } 84 | } 85 | } 86 | 87 | ttk::style layout TOptionMenu { 88 | OptionMenu.button -children { 89 | OptionMenu.padding -children { 90 | OptionMenu.indicator -side right 91 | OptionMenu.label -side right -expand true 92 | } 93 | } 94 | } 95 | 96 | ttk::style layout AccentButton { 97 | AccentButton.button -children { 98 | AccentButton.padding -children { 99 | AccentButton.label -side left -expand true 100 | } 101 | } 102 | } 103 | 104 | ttk::style layout TCheckbutton { 105 | Checkbutton.button -children { 106 | Checkbutton.padding -children { 107 | Checkbutton.indicator -side left 108 | Checkbutton.label -side right -expand true 109 | } 110 | } 111 | } 112 | 113 | ttk::style layout Switch { 114 | Switch.button -children { 115 | Switch.padding -children { 116 | Switch.indicator -side left 117 | Switch.label -side right -expand true 118 | } 119 | } 120 | } 121 | 122 | ttk::style layout ToggleButton { 123 | ToggleButton.button -children { 124 | ToggleButton.padding -children { 125 | ToggleButton.label -side left -expand true 126 | } 127 | } 128 | } 129 | 130 | ttk::style layout TRadiobutton { 131 | Radiobutton.button -children { 132 | Radiobutton.padding -children { 133 | Radiobutton.indicator -side left 134 | Radiobutton.label -side right -expand true 135 | } 136 | } 137 | } 138 | 139 | ttk::style layout Vertical.TScrollbar { 140 | Vertical.Scrollbar.trough -sticky ns -children { 141 | Vertical.Scrollbar.thumb -expand true 142 | } 143 | } 144 | 145 | ttk::style layout Horizontal.TScrollbar { 146 | Horizontal.Scrollbar.trough -sticky ew -children { 147 | Horizontal.Scrollbar.thumb -expand true 148 | } 149 | } 150 | 151 | ttk::style layout TCombobox { 152 | Combobox.field -children { 153 | Combobox.downarrow -side right -sticky {} 154 | Combobox.padding -expand 1 -children { 155 | Combobox.textarea 156 | } 157 | } 158 | } 159 | 160 | ttk::style layout TSpinbox { 161 | Spinbox.field -children { 162 | null -side right -sticky {} -children { 163 | Spinbox.uparrow -side top 164 | Spinbox.downarrow -side bottom 165 | } 166 | Spinbox.padding -expand 0 -children { 167 | Spinbox.textarea 168 | } 169 | } 170 | } 171 | 172 | ttk::style layout Horizontal.TSeparator { 173 | Horizontal.separator -sticky nswe 174 | } 175 | 176 | ttk::style layout Vertical.TSeparator { 177 | Vertical.separator -sticky nswe 178 | } 179 | 180 | ttk::style layout TLabelframe { 181 | Labelframe.border { 182 | Labelframe.padding -expand 1 -children { 183 | Labelframe.label -side right 184 | } 185 | } 186 | } 187 | 188 | ttk::style layout TNotebook.Tab { 189 | Notebook.tab -children { 190 | Notebook.padding -side top -children { 191 | Notebook.label -side top -sticky {} 192 | } 193 | } 194 | } 195 | 196 | ttk::style layout Treeview.Item { 197 | Treeitem.padding -sticky nswe -children { 198 | Treeitem.indicator -side left -sticky {} 199 | Treeitem.image -side left -sticky {} 200 | Treeitem.text -side left -sticky {} 201 | } 202 | } 203 | 204 | 205 | # Elements 206 | 207 | # Button 208 | ttk::style configure TButton -padding {8 4 8 4} -width -10 -anchor center 209 | 210 | ttk::style element create Button.button image \ 211 | [list $I(rect-basic) \ 212 | disabled $I(rect-basic) \ 213 | pressed $I(rect-basic) \ 214 | active $I(button-hover) \ 215 | ] -border 4 -sticky ewns 216 | 217 | # Toolbutton 218 | ttk::style configure Toolbutton -padding {8 4 8 4} -width -10 -anchor center 219 | 220 | ttk::style element create Toolbutton.button image \ 221 | [list $I(empty) \ 222 | disabled $I(empty) \ 223 | pressed $I(rect-basic) \ 224 | active $I(rect-basic) \ 225 | ] -border 4 -sticky ewns 226 | 227 | # Menubutton 228 | ttk::style configure TMenubutton -padding {8 4 4 4} 229 | 230 | ttk::style element create Menubutton.button \ 231 | image [list $I(rect-basic) \ 232 | disabled $I(rect-basic) \ 233 | pressed $I(rect-basic) \ 234 | active $I(button-hover) \ 235 | ] -border 4 -sticky ewns 236 | 237 | ttk::style element create Menubutton.indicator \ 238 | image [list $I(down) \ 239 | active $I(down) \ 240 | pressed $I(down) \ 241 | disabled $I(down) \ 242 | ] -width 15 -sticky e 243 | 244 | # OptionMenu 245 | ttk::style configure TOptionMenu -padding {8 4 4 4} 246 | 247 | ttk::style element create OptionMenu.button \ 248 | image [list $I(rect-basic) \ 249 | disabled $I(rect-basic) \ 250 | pressed $I(rect-basic) \ 251 | active $I(button-hover) \ 252 | ] -border 4 -sticky ewns 253 | 254 | ttk::style element create OptionMenu.indicator \ 255 | image [list $I(down) \ 256 | active $I(down) \ 257 | pressed $I(down) \ 258 | disabled $I(down) \ 259 | ] -width 15 -sticky e 260 | 261 | # AccentButton 262 | ttk::style configure AccentButton -padding {8 4 8 4} -width -10 -anchor center 263 | 264 | ttk::style element create AccentButton.button image \ 265 | [list $I(rect-accent) \ 266 | disabled $I(rect-accent-hover) \ 267 | pressed $I(rect-accent) \ 268 | active $I(rect-accent-hover) \ 269 | ] -border 4 -sticky ewns 270 | 271 | # Checkbutton 272 | ttk::style configure TCheckbutton -padding 4 273 | 274 | ttk::style element create Checkbutton.indicator image \ 275 | [list $I(box-basic) \ 276 | {selected disabled} $I(check-basic) \ 277 | disabled $I(box-basic) \ 278 | {pressed alternate} $I(tri-hover) \ 279 | {active alternate} $I(tri-hover) \ 280 | alternate $I(tri-accent) \ 281 | {pressed selected} $I(check-hover) \ 282 | {active selected} $I(check-hover) \ 283 | selected $I(check-accent) \ 284 | {pressed !selected} $I(rect-hover) \ 285 | active $I(box-hover) \ 286 | ] -width 26 -sticky w 287 | 288 | # Switch 289 | ttk::style element create Switch.indicator image \ 290 | [list $I(off-basic) \ 291 | {selected disabled} $I(on-basic) \ 292 | disabled $I(off-basic) \ 293 | {pressed selected} $I(on-hover) \ 294 | {active selected} $I(on-hover) \ 295 | selected $I(on-accent) \ 296 | {pressed !selected} $I(off-hover) \ 297 | active $I(off-hover) \ 298 | ] -width 46 -sticky w 299 | 300 | # ToggleButton 301 | ttk::style configure ToggleButton -padding {8 4 8 4} -width -10 -anchor center 302 | 303 | ttk::style element create ToggleButton.button image \ 304 | [list $I(rect-basic) \ 305 | {selected disabled} $I(rect-accent-hover) \ 306 | disabled $I(rect-basic) \ 307 | {pressed selected} $I(rect-basic) \ 308 | {active selected} $I(rect-accent) \ 309 | selected $I(rect-accent) \ 310 | {pressed !selected} $I(rect-accent) \ 311 | active $I(rect-basic) \ 312 | ] -border 4 -sticky ewns 313 | 314 | # Radiobutton 315 | ttk::style configure TRadiobutton -padding 4 316 | 317 | ttk::style element create Radiobutton.indicator image \ 318 | [list $I(outline-basic) \ 319 | {selected disabled} $I(radio-basic) \ 320 | disabled $I(outline-basic) \ 321 | {pressed selected} $I(radio-hover) \ 322 | {active selected} $I(radio-hover) \ 323 | selected $I(radio-accent) \ 324 | {pressed !selected} $I(circle-hover) \ 325 | active $I(outline-hover) \ 326 | ] -width 26 -sticky w 327 | 328 | # Scrollbar 329 | ttk::style element create Horizontal.Scrollbar.trough image $I(hor-basic) \ 330 | -sticky ew 331 | 332 | ttk::style element create Horizontal.Scrollbar.thumb \ 333 | image [list $I(hor-accent) \ 334 | disabled $I(hor-basic) \ 335 | pressed $I(hor-hover) \ 336 | active $I(hor-hover) \ 337 | ] -sticky ew 338 | 339 | ttk::style element create Vertical.Scrollbar.trough image $I(vert-basic) \ 340 | -sticky ns 341 | 342 | ttk::style element create Vertical.Scrollbar.thumb \ 343 | image [list $I(vert-accent) \ 344 | disabled $I(vert-basic) \ 345 | pressed $I(vert-hover) \ 346 | active $I(vert-hover) \ 347 | ] -sticky ns 348 | 349 | # Scale 350 | ttk::style element create Horizontal.Scale.trough image $I(hor-basic) \ 351 | -border 5 -padding 0 352 | 353 | ttk::style element create Horizontal.Scale.slider \ 354 | image [list $I(circle-accent) \ 355 | disabled $I(circle-basic) \ 356 | pressed $I(circle-hover) \ 357 | active $I(circle-hover) \ 358 | ] -sticky {} 359 | 360 | ttk::style element create Vertical.Scale.trough image $I(vert-basic) \ 361 | -border 5 -padding 0 362 | 363 | ttk::style element create Vertical.Scale.slider \ 364 | image [list $I(circle-accent) \ 365 | disabled $I(circle-basic) \ 366 | pressed $I(circle-hover) \ 367 | active $I(circle-hover) \ 368 | ] -sticky {} 369 | 370 | # Progressbar 371 | ttk::style element create Horizontal.Progressbar.trough image $I(hor-basic) \ 372 | -sticky ew 373 | 374 | ttk::style element create Horizontal.Progressbar.pbar image $I(hor-accent) \ 375 | -sticky ew 376 | 377 | ttk::style element create Vertical.Progressbar.trough image $I(vert-basic) \ 378 | -sticky ns 379 | 380 | ttk::style element create Vertical.Progressbar.pbar image $I(vert-accent) \ 381 | -sticky ns 382 | 383 | # Entry 384 | ttk::style element create Entry.field \ 385 | image [list $I(box-basic) \ 386 | {focus hover} $I(box-accent) \ 387 | invalid $I(box-invalid) \ 388 | disabled $I(box-basic) \ 389 | focus $I(box-accent) \ 390 | hover $I(box-hover) \ 391 | ] -border 5 -padding {8} -sticky news 392 | 393 | # Combobox 394 | ttk::style map TCombobox -selectbackground [list \ 395 | {!focus} $colors(-selectbg) \ 396 | {readonly hover} $colors(-selectbg) \ 397 | {readonly focus} $colors(-selectbg) \ 398 | ] 399 | 400 | ttk::style map TCombobox -selectforeground [list \ 401 | {!focus} $colors(-selectfg) \ 402 | {readonly hover} $colors(-selectfg) \ 403 | {readonly focus} $colors(-selectfg) \ 404 | ] 405 | 406 | ttk::style element create Combobox.field \ 407 | image [list $I(box-basic) \ 408 | {readonly disabled} $I(rect-basic) \ 409 | {readonly pressed} $I(rect-basic) \ 410 | {readonly focus hover} $I(button-hover) \ 411 | {readonly focus} $I(rect-basic) \ 412 | {readonly hover} $I(button-hover) \ 413 | {focus hover} $I(box-accent) \ 414 | readonly $I(rect-basic) \ 415 | disabled $I(box-basic) \ 416 | focus $I(box-accent) \ 417 | hover $I(box-hover) \ 418 | ] -border 5 -padding {8} 419 | 420 | ttk::style element create Combobox.downarrow image $I(down) \ 421 | -width 15 -sticky e 422 | 423 | # Spinbox 424 | ttk::style element create Spinbox.field \ 425 | image [list $I(box-basic) \ 426 | disabled $I(box-basic) \ 427 | focus $I(box-accent) \ 428 | hover $I(box-hover) \ 429 | ] -border 5 -padding {8} -sticky news 430 | 431 | ttk::style element create Spinbox.uparrow \ 432 | image [list $I(up) \ 433 | disabled $I(up) \ 434 | pressed $I(up-accent) \ 435 | active $I(up-accent) \ 436 | ] -border 4 -width 15 -sticky e 437 | 438 | ttk::style element create Spinbox.downarrow \ 439 | image [list $I(down) \ 440 | disabled $I(down) \ 441 | pressed $I(down-accent) \ 442 | active $I(down-accent) \ 443 | ] -border 4 -width 15 -sticky e 444 | 445 | # Sizegrip 446 | ttk::style element create Sizegrip.sizegrip image $I(size) \ 447 | -sticky ewns 448 | 449 | # Separator 450 | ttk::style element create Horizontal.separator image $I(separator) 451 | 452 | ttk::style element create Vertical.separator image $I(separator) 453 | 454 | # Labelframe 455 | ttk::style element create Labelframe.border image $I(box-basic) \ 456 | -border 5 -padding 4 -sticky news 457 | 458 | # Notebook 459 | ttk::style element create Notebook.client \ 460 | image $I(notebook) -border 4 461 | 462 | ttk::style element create Notebook.tab \ 463 | image [list $I(tab-disabled) \ 464 | selected $I(tab-basic) \ 465 | active $I(tab-hover) \ 466 | ] -border 5 -padding {12 4} 467 | 468 | # Treeview 469 | ttk::style element create Treeview.field image $I(box-basic) \ 470 | -border 5 471 | 472 | ttk::style element create Treeheading.cell \ 473 | image [list $I(tree-basic) \ 474 | pressed $I(tree-pressed) 475 | ] -border 5 -padding 4 -sticky ewns 476 | 477 | ttk::style element create Treeitem.indicator \ 478 | image [list $I(right) \ 479 | user2 $I(empty) \ 480 | user1 $I(down) \ 481 | ] -width 15 -sticky w 482 | 483 | ttk::style configure Treeview -background $colors(-bg) 484 | ttk::style configure Treeview.Item -padding {2 0 0 0} 485 | ttk::style map Treeview \ 486 | -background [list selected $colors(-selectbg)] \ 487 | -foreground [list selected $colors(-selectfg)] 488 | 489 | # Sashes 490 | ttk::style configure TPanedwindow \ 491 | -width 1 -padding 0 492 | ttk::style map TPanedwindow \ 493 | -background [list hover $colors(-bg)] 494 | } 495 | } 496 | -------------------------------------------------------------------------------- /EDNeutronAssistant.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import time 4 | import webbrowser 5 | import tkinter as tk 6 | import tkinter.ttk as ttk 7 | import tkinter.messagebox 8 | import json 9 | import threading 10 | 11 | if __name__ == '__main__': 12 | import gui 13 | import utils 14 | import api_access 15 | import menu 16 | 17 | __version__ = "v3.1.1" 18 | 19 | # Find out run path in current environment 20 | if hasattr(sys, "_MEIPASS"): 21 | # noinspection PyProtectedMember 22 | PATH = sys._MEIPASS 23 | elif getattr(sys, "frozen", False): 24 | PATH = os.path.dirname(sys.executable) 25 | else: 26 | PATH = os.getcwd() 27 | 28 | 29 | class MainApplication(ttk.Frame): 30 | def __init__(self, master, *args, **kwargs): 31 | super().__init__(*args, **kwargs) 32 | 33 | self.master = master 34 | 35 | self.title_bar = None 36 | 37 | # UI elements 38 | self.status_information_frame = gui.StatusInformation(self, self) 39 | self.status_information_frame.grid(row=1, column=0, columnspan=2, sticky="W") 40 | 41 | self.menu_button = menu.MainMenuButton(self, self) 42 | self.menu_button.grid(row=1, column=1, sticky="NE") 43 | 44 | self.log_frame = gui.LogFrame(self) 45 | self.log_frame.grid(row=2, column=0, columnspan=2) 46 | 47 | self.route_selection_lbl = ttk.Label(self, text="Route Calculator") 48 | self.route_selection_lbl.grid(row=3, column=0, columnspan=2, sticky="W", pady=4) 49 | 50 | self.route_selection = gui.RouteSelection(self, self) 51 | self.route_selection.grid(row=4, column=0, columnspan=2, ipadx=40, sticky="W") 52 | 53 | # Setting variables 54 | self.verbose = False 55 | self.poll_rate = 1 56 | 57 | if os.name == "nt": 58 | self.config_path = os.path.join(os.getenv("APPDATA"), "EDNeutronAssistant") 59 | else: 60 | self.config_path = os.path.join(os.path.expanduser("~"), ".config", "EDNeutronAssistant") 61 | 62 | self.configuration = {"current_system": "", "ship_coriolis_build": {}, "jump_range_coriolis": 0, 63 | "jump_range_log": 0, "commander_name": ""} 64 | 65 | try: 66 | self.configuration = json.load(open(os.path.join(self.config_path, "data.json"), "r")) 67 | except FileNotFoundError: 68 | pass 69 | 70 | self.configuration["current_system_display"] = "" 71 | self.configuration["jump_range_coriolis_display"] = 0 72 | self.configuration["commander_name_display"] = "" 73 | 74 | self.configuration["exiting"] = False 75 | self.configuration["last_copied"] = "" 76 | 77 | # Creating working directory 78 | if not os.path.isdir(self.config_path): 79 | os.makedirs(self.config_path) 80 | 81 | self.write_config() 82 | 83 | # --- Running initialization --- 84 | self.print_log("Initializing") 85 | 86 | threading.Thread(target=self.application_loop).start() 87 | 88 | if "route" in self.configuration and self.configuration["route"]: 89 | self.print_log("Found existing route") 90 | 91 | self.print_log("Initialization complete") 92 | 93 | def print_log(self, *args): 94 | """Print content to file and console along with a timestamp""" 95 | 96 | t = time.strftime("%T") 97 | print(t, *args) 98 | 99 | entry = t + " " 100 | for arg in args: 101 | entry += arg 102 | 103 | self.log_frame.add_to_log(entry) 104 | 105 | logs_dir = os.path.join(self.config_path, "logs") 106 | 107 | if not os.path.isdir(logs_dir): 108 | os.makedirs(logs_dir) 109 | 110 | with open(os.path.join(self.config_path, "logs", 111 | f"EDNeutronAssistant-{time.strftime('%Y-%m-%d')}.log"), "a") as f: 112 | f.write(entry + "\n") 113 | 114 | def write_config(self): 115 | """Writes the current configuration to the config file""" 116 | with open(os.path.join(self.config_path, "data.json"), "w") as f: 117 | json.dump(self.configuration, f, indent=2) 118 | if self.verbose: 119 | self.print_log("Saved configuration to file") 120 | 121 | def change_state_of_all_calculate_buttons(self, state: str): 122 | self.route_selection.simple_route_selection_tab.calculate_button.configure(state=state) 123 | self.route_selection.exact_route_selection_tab.calculate_button.configure(state=state) 124 | 125 | def apply_theme(self, theme: int): 126 | style = ttk.Style(self.master) 127 | 128 | if theme == 1: 129 | try: 130 | root.tk.call("source", os.path.join(PATH, "themes", "ed-azure-dark.tcl")) 131 | except tk.TclError: 132 | pass 133 | style.theme_use("ed-azure-dark") 134 | 135 | # On windows, use custom title bar to match dark theme 136 | if os.name == "nt": 137 | self.title_bar = gui.TitleBar(self, self.master, TITLE, "#000000", "#FF8000", "#FF8000", "#000000") 138 | self.title_bar.grid(row=0, column=0, columnspan=2, sticky="ew") 139 | 140 | self.master.overrideredirect(True) 141 | self.master.after(10, lambda: gui.set_app_window(self.master)) 142 | 143 | # Fix notebook size 144 | current_page = self.route_selection.index(self.route_selection.select()) 145 | if current_page != 0: 146 | self.route_selection.select(0) 147 | self.route_selection.select(current_page) 148 | elif current_page == 0: 149 | self.route_selection.select(1) 150 | self.route_selection.select(current_page) 151 | 152 | def application_loop(self): 153 | """Main loop of application running checks in time intervals of self.poll_rate""" 154 | 155 | def update_commander_name(parsed_log_: list): 156 | 157 | def set_commander_name(name: str): 158 | self.status_information_frame.update_cmdr_lbl(name) 159 | self.configuration["commander_name_display"] = name 160 | self.configuration["commander_name"] = name 161 | self.write_config() 162 | 163 | log_commander_name = utils.get_commander_name_from_log(parsed_log_, log_function=self.print_log, 164 | verbose=self.verbose) 165 | config_commander_name = self.configuration["commander_name"] 166 | displayed_commander_name = self.configuration["commander_name_display"] 167 | 168 | # case 1: log commander name is not blank 169 | # -> if name not already displayed, set log name 170 | if log_commander_name != "": 171 | if displayed_commander_name != log_commander_name: 172 | set_commander_name(log_commander_name) 173 | 174 | # case 2: log commander name is blank 175 | # -> if name not already displayed, set config name 176 | else: 177 | if displayed_commander_name != config_commander_name: 178 | set_commander_name(config_commander_name) 179 | 180 | def update_current_system(parsed_log_: list): 181 | 182 | def set_current_system(system: str): 183 | self.print_log(f"Entered system {system}") 184 | 185 | # Update configuration 186 | self.configuration["current_system"] = system 187 | self.configuration["current_system_display"] = system 188 | 189 | self.write_config() 190 | 191 | # Update simple route start system 192 | self.route_selection.simple_route_selection_tab.from_combobox.set(system) 193 | self.route_selection.simple_route_selection_tab.from_combobox.set_completion_list([system]) 194 | 195 | # Update exact route start system 196 | self.route_selection.exact_route_selection_tab.from_combobox.set(system) 197 | self.route_selection.exact_route_selection_tab.from_combobox.set_completion_list([system]) 198 | 199 | # Update status information current system 200 | self.status_information_frame.update_current_system_lbl(system) 201 | 202 | log_current_system = utils.get_current_system_from_log(parsed_log_, log_function=self.print_log, 203 | verbose=self.verbose) 204 | config_current_system = self.configuration["current_system"] 205 | displayed_current_system = self.configuration["current_system_display"] 206 | 207 | # case 1: log current system is not blank 208 | # -> if not already set, set log current system 209 | if log_current_system != "": 210 | if displayed_current_system != log_current_system: 211 | set_current_system(log_current_system) 212 | 213 | # case 2: log current system is blank 214 | # -> if not already set, set config current system 215 | else: 216 | if displayed_current_system != config_current_system: 217 | set_current_system(config_current_system) 218 | 219 | def update_ship_build(parsed_log_: list): 220 | 221 | def set_new_ship_build(loadout_event: dict): 222 | 223 | # Convert build to coriolis build 224 | build = api_access.convert_loadout_event_to_coriolis(loadout_event) 225 | jump_range_coriolis = build["stats"]["fullTankRange"] 226 | 227 | self.configuration["ship_coriolis_build"] = build 228 | self.configuration["jump_range_coriolis"] = jump_range_coriolis 229 | self.configuration["jump_range_coriolis_display"] = jump_range_coriolis 230 | self.configuration["jump_range_log"] = round(loadout_event["MaxJumpRange"], 2) 231 | 232 | self.write_config() 233 | 234 | # Update default jump range in simple neutron route calculator 235 | self.route_selection.simple_route_selection_tab.jump_range_entry.delete(0, tk.END) 236 | self.route_selection.simple_route_selection_tab.jump_range_entry.insert(0, jump_range_coriolis) 237 | 238 | def update_displayed_jump_range(jump_range: float): 239 | self.configuration["jump_range_coriolis_display"] = jump_range 240 | 241 | # Update default jump range in simple neutron route calculator 242 | self.route_selection.simple_route_selection_tab.jump_range_entry.delete(0, tk.END) 243 | self.route_selection.simple_route_selection_tab.jump_range_entry.insert(0, jump_range) 244 | 245 | # To test if the ship build has changed, we compare the "MaxJumpRange" attribute of the game log, to avoid 246 | # unnecessary conversions to coriolis builds 247 | 248 | latest_log_loadout_event = utils.get_latest_loadout_event_from_log(parsed_log_) 249 | config_coriolis_build = self.configuration["ship_coriolis_build"] 250 | config_ship_log_range = self.configuration["jump_range_log"] 251 | displayed_ship_jump_range = self.configuration["jump_range_coriolis_display"] 252 | 253 | # case 1: no log loadout events were found 254 | # -> if configuration contains coriolis build, use it to update display 255 | if not latest_log_loadout_event: 256 | if config_coriolis_build: 257 | config_coriolis_build_jump_range = config_coriolis_build["stats"]["fullTankRange"] 258 | if config_coriolis_build_jump_range != displayed_ship_jump_range: 259 | update_displayed_jump_range(config_coriolis_build_jump_range) 260 | 261 | # case 2: log loadout event was found 262 | # -> compare log jump range to config log jump range, if not equal, update build 263 | else: 264 | new_build_log_jump_range = round(latest_log_loadout_event["MaxJumpRange"], 2) 265 | if new_build_log_jump_range != config_ship_log_range: 266 | set_new_ship_build(latest_log_loadout_event) 267 | 268 | # If jump range was not displayed yet, set saved coriolis range 269 | elif not displayed_ship_jump_range: 270 | update_displayed_jump_range(config_coriolis_build["stats"]["fullTankRange"]) 271 | 272 | def update_simple_route(route_: list): 273 | 274 | # Get all route systems 275 | all_route_systems = [] 276 | for route_entry in route_: 277 | if "system" in route_entry: 278 | all_route_systems.append(route_entry["system"]) 279 | 280 | # Get current system from configuration 281 | current_system = self.configuration["current_system"] 282 | 283 | destination = all_route_systems[-1] 284 | 285 | # Get next system and next system information 286 | if current_system in all_route_systems: 287 | # Check if route is completed 288 | if current_system == all_route_systems[-1]: 289 | self.print_log("Route completed") 290 | self.configuration["route"] = [] 291 | self.status_information_frame.update_progress_lbl(len(all_route_systems), len(all_route_systems)) 292 | self.status_information_frame.reset_information() 293 | return 294 | else: 295 | index_current_system = all_route_systems.index(current_system) 296 | index_next_system = all_route_systems.index(current_system) + 1 297 | 298 | next_system = all_route_systems[index_next_system] 299 | next_system_distance = round(route_[index_current_system]["distance_left"] - 300 | route_[index_next_system]["distance_left"], 2) 301 | next_system_jumps = int(route_[index_next_system]["jumps"]) 302 | next_system_is_neutron = route_[index_next_system]["neutron_star"] 303 | 304 | self.configuration["last_route_system"] = current_system 305 | else: 306 | # Current system is off route 307 | index_current_system = all_route_systems.index(self.configuration["last_route_system"]) 308 | next_system = all_route_systems[index_current_system + 1] 309 | next_system_distance = api_access.get_distance_between_systems(current_system, next_system, 310 | log_function=self.print_log, 311 | verbose=self.verbose) 312 | next_system_is_neutron = route_[all_route_systems.index("next_system")]["neutron_star"] 313 | next_system_jumps = round(next_system_distance / self.configuration["ship_build"]["stats"][ 314 | "fullTankRange"], 2) 315 | 316 | if next_system: 317 | if next_system != self.configuration["last_copied"]: 318 | self.status_information_frame.update_next_system_info(next_system, next_system_distance, 319 | next_system_jumps, next_system_is_neutron) 320 | self.status_information_frame.update_progress_lbl(index_current_system + 1, len(all_route_systems)) 321 | self.status_information_frame.set_destination(destination) 322 | utils.copy_system_to_clipboard(next_system, log_function=self.print_log) 323 | self.configuration["last_copied"] = next_system 324 | else: 325 | self.status_information_frame.reset_information() 326 | 327 | def update_exact_route(route_): 328 | # Get all route systems 329 | all_route_systems = [] 330 | for route_entry in route_: 331 | if "name" in route_entry: 332 | all_route_systems.append(route_entry["name"]) 333 | 334 | # Get current system from configuration 335 | current_system = self.configuration["current_system"] 336 | 337 | destination = all_route_systems[-1] 338 | 339 | # Get next system and next system information 340 | if current_system in all_route_systems: 341 | # Check if route is completed 342 | if current_system == all_route_systems[-1]: 343 | self.print_log("Route completed") 344 | self.configuration["route"] = [] 345 | self.status_information_frame.update_progress_lbl(len(all_route_systems), len(all_route_systems)) 346 | self.status_information_frame.reset_information() 347 | return 348 | else: 349 | index_current_system = all_route_systems.index(current_system) 350 | index_next_system = all_route_systems.index(current_system) + 1 351 | 352 | next_system = all_route_systems[index_next_system] 353 | next_system_distance = round(route_[index_next_system]["distance"] - 354 | route_[index_current_system]["distance"], 2) 355 | next_system_jumps = 1 356 | next_system_is_neutron = route_[index_next_system]["has_neutron"] 357 | 358 | self.configuration["last_route_system"] = current_system 359 | else: 360 | # Current system is off route 361 | index_current_system = all_route_systems.index(self.configuration["last_route_system"]) 362 | next_system = all_route_systems[index_current_system + 1] 363 | next_system_distance = api_access.get_distance_between_systems(current_system, next_system, 364 | log_function=self.print_log, 365 | verbose=self.verbose) 366 | next_system_is_neutron = route_[all_route_systems.index("next_system")]["has_neutron"] 367 | next_system_jumps = round(next_system_distance / self.configuration["ship_build"]["stats"][ 368 | "fullTankRange"], 2) 369 | 370 | if next_system: 371 | if next_system != self.configuration["last_copied"]: 372 | self.status_information_frame.update_next_system_info(next_system, next_system_distance, 373 | next_system_jumps, next_system_is_neutron) 374 | self.status_information_frame.update_progress_lbl(index_current_system + 1, len(all_route_systems)) 375 | self.status_information_frame.set_destination(destination) 376 | utils.copy_system_to_clipboard(next_system, log_function=self.print_log) 377 | self.configuration["last_copied"] = next_system 378 | else: 379 | self.status_information_frame.reset_information() 380 | 381 | while 1: 382 | # Parse game log 383 | parsed_log = utils.parse_game_log(verbose=self.verbose, log_function=self.print_log) 384 | 385 | try: 386 | update_commander_name(parsed_log) 387 | update_current_system(parsed_log) 388 | update_ship_build(parsed_log) 389 | except RuntimeError: 390 | continue 391 | 392 | if "route" in self.configuration and self.configuration["route"]: 393 | if self.configuration["route_type"] == "simple": 394 | update_simple_route(self.configuration["route"]) 395 | elif self.configuration["route_type"] == "exact": 396 | update_exact_route(self.configuration["route"]) 397 | 398 | if self.configuration["exiting"]: 399 | break 400 | 401 | time.sleep(self.poll_rate) 402 | 403 | def terminate(self): 404 | self.configuration["exiting"] = True 405 | self.master.destroy() 406 | 407 | 408 | if __name__ == '__main__': 409 | root = tk.Tk() 410 | 411 | root.resizable(False, False) 412 | 413 | TITLE = f"EDNeutronAssistant {__version__}" 414 | root.title(TITLE) 415 | 416 | root.geometry("+200+200") 417 | 418 | # Set logo file path according to environment 419 | icon_path = os.path.join(PATH, "logo.ico") 420 | 421 | if os.name == "nt": 422 | root.iconbitmap(default=icon_path) 423 | 424 | ed_neutron_assistant = MainApplication(root, root) 425 | ed_neutron_assistant.grid(sticky="NEWS", padx=5, pady=5) 426 | 427 | # Enable verbose when called with -v flag 428 | if "-v" in sys.argv or "--verbose" in sys.argv: 429 | ed_neutron_assistant.verbose = True 430 | 431 | # Exit program when closing 432 | root.protocol("WM_DELETE_WINDOW", ed_neutron_assistant.terminate) 433 | 434 | # apply settings theme 435 | if menu.USER_SETTINGS["theme"] != 0: 436 | ed_neutron_assistant.apply_theme(menu.USER_SETTINGS["theme"]) 437 | 438 | # Check for update 439 | ed_neutron_assistant.print_log("Checking GitHub for updates") 440 | update, version = api_access.update_available(__version__) 441 | 442 | if update: 443 | ed_neutron_assistant.print_log(f"Found new version {version}") 444 | update_message = tk.messagebox.askyesno("Update Available", 445 | "A new version of EDNeutronAssistant is available. Download now?") 446 | if update_message: 447 | webbrowser.open_new_tab(f"https://github.com/Gobidev/EDNeutronAssistant/releases/latest") 448 | else: 449 | ed_neutron_assistant.print_log(f"Already running latest version") 450 | 451 | root.mainloop() 452 | --------------------------------------------------------------------------------