cs2tracker 2.1.13__py3-none-any.whl → 2.1.15__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of cs2tracker might be problematic. Click here for more details.
- cs2tracker/_version.py +2 -2
- cs2tracker/app/__init__.py +0 -3
- cs2tracker/app/app.py +283 -0
- cs2tracker/app/editor_frame.py +354 -158
- cs2tracker/app/history_frame.py +61 -0
- cs2tracker/app/scraper_frame.py +34 -11
- cs2tracker/config.py +263 -0
- cs2tracker/constants.py +58 -321
- cs2tracker/data/config.ini +157 -154
- cs2tracker/data/convert_inventory.js +108 -28
- cs2tracker/data/get_inventory.js +96 -32
- cs2tracker/logs.py +143 -0
- cs2tracker/main.py +4 -19
- cs2tracker/scraper/__init__.py +0 -9
- cs2tracker/scraper/background_task.py +1 -1
- cs2tracker/scraper/discord_notifier.py +34 -32
- cs2tracker/scraper/parser.py +189 -0
- cs2tracker/scraper/scraper.py +166 -246
- cs2tracker/util/__init__.py +0 -9
- cs2tracker/util/currency_conversion.py +84 -0
- cs2tracker/util/padded_console.py +24 -0
- cs2tracker/util/tkinter.py +55 -0
- {cs2tracker-2.1.13.dist-info → cs2tracker-2.1.15.dist-info}/METADATA +6 -6
- cs2tracker-2.1.15.dist-info/RECORD +31 -0
- cs2tracker/app/application.py +0 -255
- cs2tracker/util/price_logs.py +0 -100
- cs2tracker/util/validated_config.py +0 -164
- cs2tracker-2.1.13.dist-info/RECORD +0 -27
- {cs2tracker-2.1.13.dist-info → cs2tracker-2.1.15.dist-info}/WHEEL +0 -0
- {cs2tracker-2.1.13.dist-info → cs2tracker-2.1.15.dist-info}/entry_points.txt +0 -0
- {cs2tracker-2.1.13.dist-info → cs2tracker-2.1.15.dist-info}/licenses/LICENSE +0 -0
- {cs2tracker-2.1.13.dist-info → cs2tracker-2.1.15.dist-info}/top_level.txt +0 -0
cs2tracker/app/editor_frame.py
CHANGED
|
@@ -4,24 +4,25 @@ from shutil import copy
|
|
|
4
4
|
from subprocess import PIPE, STDOUT
|
|
5
5
|
from threading import Thread
|
|
6
6
|
from tkinter import messagebox, ttk
|
|
7
|
-
from urllib.parse import unquote
|
|
8
7
|
|
|
9
8
|
from nodejs import node
|
|
10
9
|
from ttk_text import ThemedText
|
|
11
10
|
|
|
11
|
+
from cs2tracker.config import get_config
|
|
12
12
|
from cs2tracker.constants import (
|
|
13
13
|
CONFIG_FILE,
|
|
14
14
|
CONFIG_FILE_BACKUP,
|
|
15
|
+
DATA_DIR,
|
|
15
16
|
INVENTORY_IMPORT_FILE,
|
|
16
17
|
INVENTORY_IMPORT_SCRIPT,
|
|
17
18
|
)
|
|
18
|
-
from cs2tracker.util import
|
|
19
|
+
from cs2tracker.util.tkinter import centered, size_info
|
|
19
20
|
|
|
20
21
|
ADD_CUSTOM_ITEM_TITLE = "Add Custom Item"
|
|
21
|
-
ADD_CUSTOM_ITEM_SIZE = "
|
|
22
|
+
ADD_CUSTOM_ITEM_SIZE = "500x230"
|
|
22
23
|
|
|
23
24
|
IMPORT_INVENTORY_TITLE = "Import Steam Inventory"
|
|
24
|
-
IMPORT_INVENTORY_SIZE = "
|
|
25
|
+
IMPORT_INVENTORY_SIZE = "700x350"
|
|
25
26
|
|
|
26
27
|
IMPORT_INVENTORY_PROCESS_TITLE = "Importing Steam Inventory..."
|
|
27
28
|
IMPORT_INVENTORY_PROCESS_SIZE = "700x500"
|
|
@@ -30,15 +31,18 @@ config = get_config()
|
|
|
30
31
|
|
|
31
32
|
|
|
32
33
|
class ConfigEditorFrame(ttk.Frame):
|
|
33
|
-
def __init__(self,
|
|
34
|
+
def __init__(self, window):
|
|
34
35
|
"""Initialize the configuration editor frame that allows users to view and edit
|
|
35
36
|
the configuration options.
|
|
36
37
|
"""
|
|
37
|
-
super().__init__(
|
|
38
|
+
super().__init__(window, padding=15)
|
|
38
39
|
|
|
39
|
-
self.
|
|
40
|
+
self.window = window
|
|
41
|
+
self.edit_entry = None
|
|
40
42
|
self._add_widgets()
|
|
41
43
|
|
|
44
|
+
self.tree.focus_set()
|
|
45
|
+
|
|
42
46
|
def _add_widgets(self):
|
|
43
47
|
"""Configure the main editor frame which displays the configuration options in a
|
|
44
48
|
structured way.
|
|
@@ -46,82 +50,176 @@ class ConfigEditorFrame(ttk.Frame):
|
|
|
46
50
|
self._configure_treeview()
|
|
47
51
|
self.tree.pack(expand=True, fill="both")
|
|
48
52
|
|
|
49
|
-
button_frame = ConfigEditorButtonFrame(self
|
|
53
|
+
button_frame = ConfigEditorButtonFrame(self)
|
|
50
54
|
button_frame.pack(side="bottom", padx=10, pady=(0, 10))
|
|
51
55
|
|
|
52
|
-
def
|
|
56
|
+
def save_config(self):
|
|
57
|
+
"""Save the current configuration from the treeview to the config file."""
|
|
58
|
+
config.delete_display_sections()
|
|
59
|
+
for section in self.tree.get_children():
|
|
60
|
+
config.add_section(section)
|
|
61
|
+
for item in self.tree.get_children(section):
|
|
62
|
+
item_name = self.tree.item(item, "text")
|
|
63
|
+
if section not in ("App Settings", "User Settings"):
|
|
64
|
+
config_option = config.name_to_option(item_name, href=True)
|
|
65
|
+
else:
|
|
66
|
+
config_option = config.name_to_option(item_name)
|
|
67
|
+
value = self.tree.item(item, "values")[0]
|
|
68
|
+
config.set(section, config_option, value)
|
|
69
|
+
|
|
70
|
+
config.write_to_file()
|
|
71
|
+
if not config.valid:
|
|
72
|
+
config.load_from_file()
|
|
73
|
+
self.reload_config_into_tree()
|
|
74
|
+
messagebox.showerror(
|
|
75
|
+
"Config Error",
|
|
76
|
+
f"The configuration is invalid. ({config.last_error})",
|
|
77
|
+
parent=self.window,
|
|
78
|
+
)
|
|
79
|
+
self.window.focus_set()
|
|
80
|
+
self.tree.focus_set()
|
|
81
|
+
|
|
82
|
+
def _save_edit(self, event, row, column):
|
|
83
|
+
"""Save the edited value in the treeview and destroy the entry widget."""
|
|
84
|
+
self.tree.set(row, column=column, value=event.widget.get())
|
|
85
|
+
self.save_config()
|
|
86
|
+
event.widget.destroy()
|
|
87
|
+
|
|
88
|
+
def _set_cell_value(self, event, row=None, column=None):
|
|
53
89
|
"""
|
|
54
|
-
Set the value of a cell in the treeview to be editable when double-
|
|
90
|
+
Set the value of a cell in the treeview to be editable when double-clicked.
|
|
55
91
|
|
|
56
92
|
Source: https://stackoverflow.com/questions/75787251/create-an-editable-tkinter-treeview-with-keyword-connection
|
|
57
93
|
"""
|
|
94
|
+
try:
|
|
95
|
+
if not row or not column:
|
|
96
|
+
row = self.tree.identify_row(event.y)
|
|
97
|
+
column = self.tree.identify_column(event.x)
|
|
58
98
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
99
|
+
item_text = self.tree.item(row, "text")
|
|
100
|
+
if column == "#0" or any(item_text == section for section in config.sections()):
|
|
101
|
+
return
|
|
102
|
+
item_value = self.tree.item(row, "values")[0]
|
|
62
103
|
|
|
63
|
-
try:
|
|
64
|
-
row = self.tree.identify_row(event.y)
|
|
65
|
-
column = self.tree.identify_column(event.x)
|
|
66
|
-
item_text = self.tree.set(row, column)
|
|
67
|
-
if item_text.strip() == "":
|
|
68
|
-
left_item_text = self.tree.item(row, "text")
|
|
69
|
-
# Don't allow editing of section headers
|
|
70
|
-
if any(left_item_text == section for section in config.sections()):
|
|
71
|
-
return
|
|
72
104
|
x, y, w, h = self.tree.bbox(row, column)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
105
|
+
self.edit_entry = ttk.Entry(self, justify="center", font=("Helvetica", 11))
|
|
106
|
+
self.edit_entry.place(x=x, y=y, width=w, height=h + 3) # type: ignore
|
|
107
|
+
self.edit_entry.insert("end", item_value)
|
|
108
|
+
self.edit_entry.bind("<Return>", lambda e: self._save_edit(e, row, column))
|
|
109
|
+
self.edit_entry.focus_set()
|
|
110
|
+
self.edit_entry.grab_set()
|
|
79
111
|
except Exception:
|
|
80
|
-
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
def _set_selection_value(self, _):
|
|
115
|
+
"""Set the value of the currently selected cell in the treeview to be
|
|
116
|
+
editable.
|
|
117
|
+
"""
|
|
118
|
+
selected = self.tree.selection()
|
|
119
|
+
if selected:
|
|
120
|
+
row = selected[0]
|
|
121
|
+
column = "#1"
|
|
122
|
+
self._set_cell_value(None, row=row, column=column)
|
|
81
123
|
|
|
82
|
-
def
|
|
124
|
+
def _delete_selection_value(self, _):
|
|
125
|
+
"""
|
|
126
|
+
Delete the value of the currently selected cell in the treeview.
|
|
127
|
+
|
|
128
|
+
This only works for custom items, as other sections are not editable.
|
|
129
|
+
"""
|
|
130
|
+
selected = self.tree.selection()
|
|
131
|
+
if selected:
|
|
132
|
+
item = selected[0]
|
|
133
|
+
section_name = self.tree.parent(item)
|
|
134
|
+
if section_name in ("Stickers", "Skins"):
|
|
135
|
+
next_option = self.tree.next(item)
|
|
136
|
+
self.tree.delete(item)
|
|
137
|
+
self.save_config()
|
|
138
|
+
if next_option:
|
|
139
|
+
self.tree.focus(next_option)
|
|
140
|
+
self.tree.selection_set(next_option)
|
|
141
|
+
else:
|
|
142
|
+
self.tree.focus(section_name)
|
|
143
|
+
self.tree.selection_set(section_name)
|
|
144
|
+
|
|
145
|
+
def _destroy_entry(self, _):
|
|
83
146
|
"""Destroy any entry widgets in the treeview on an event, such as a mouse wheel
|
|
84
147
|
movement.
|
|
85
148
|
"""
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
def _destroy_entry(self, event):
|
|
91
|
-
"""Destroy the entry widget on an even targeting it."""
|
|
92
|
-
if isinstance(event.widget, ttk.Entry):
|
|
93
|
-
event.widget.destroy()
|
|
149
|
+
if self.edit_entry:
|
|
150
|
+
self.edit_entry.destroy()
|
|
151
|
+
self.edit_entry = None
|
|
152
|
+
self.tree.focus_set()
|
|
94
153
|
|
|
95
154
|
def _make_tree_editable(self):
|
|
96
155
|
"""Add a binding to the treeview that allows double-clicking on a cell to edit
|
|
97
156
|
its value.
|
|
98
157
|
"""
|
|
99
158
|
self.tree.bind("<Double-1>", self._set_cell_value)
|
|
100
|
-
self.
|
|
101
|
-
self.
|
|
159
|
+
self.tree.bind("<Return>", self._set_selection_value)
|
|
160
|
+
self.tree.bind("<BackSpace>", self._delete_selection_value)
|
|
161
|
+
self.window.bind("<MouseWheel>", self._destroy_entry)
|
|
162
|
+
self.window.bind("<Escape>", self._destroy_entry)
|
|
102
163
|
|
|
103
164
|
def _load_config_into_tree(self):
|
|
104
165
|
"""Load the configuration options into the treeview for display and editing."""
|
|
105
166
|
for section in config.sections():
|
|
167
|
+
# App Settings are internal and shouldn't be displayed to the user
|
|
106
168
|
if section == "App Settings":
|
|
107
169
|
continue
|
|
170
|
+
|
|
108
171
|
section_level = self.tree.insert("", "end", iid=section, text=section)
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
172
|
+
|
|
173
|
+
# Items in the Stickers, Cases, and Skins sections should be displayed alphabetically sorted
|
|
174
|
+
section_items = config.items(section)
|
|
175
|
+
if section in ("Stickers", "Cases", "Skins"):
|
|
176
|
+
section_items = sorted(section_items)
|
|
177
|
+
|
|
178
|
+
for config_option, value in section_items:
|
|
179
|
+
if section not in ("User Settings", "App Settings"):
|
|
180
|
+
option_name = config.option_to_name(config_option, href=True)
|
|
181
|
+
self.tree.insert(
|
|
182
|
+
section_level,
|
|
183
|
+
"end",
|
|
184
|
+
iid=f"{section}-{option_name}",
|
|
185
|
+
text=option_name,
|
|
186
|
+
values=[value],
|
|
187
|
+
)
|
|
113
188
|
else:
|
|
114
|
-
option_name =
|
|
115
|
-
self.tree.insert(
|
|
189
|
+
option_name = config.option_to_name(config_option)
|
|
190
|
+
self.tree.insert(
|
|
191
|
+
section_level,
|
|
192
|
+
"end",
|
|
193
|
+
iid=f"{section}-{option_name}",
|
|
194
|
+
text=option_name,
|
|
195
|
+
values=[value],
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
self.tree.focus("User Settings")
|
|
199
|
+
self.tree.selection_set("User Settings")
|
|
116
200
|
|
|
117
201
|
def reload_config_into_tree(self):
|
|
118
|
-
"""Reload the configuration options into the treeview for display and
|
|
119
|
-
|
|
202
|
+
"""Reload the configuration options into the treeview for display and editing
|
|
203
|
+
and maintain the users current selection.
|
|
120
204
|
"""
|
|
205
|
+
selected = self.tree.selection()
|
|
206
|
+
selected_text, selected_section = None, None
|
|
207
|
+
if selected:
|
|
208
|
+
selected_text = self.tree.item(selected[0], "text")
|
|
209
|
+
selected_section = self.tree.parent(selected[0])
|
|
210
|
+
|
|
121
211
|
for item in self.tree.get_children():
|
|
122
212
|
self.tree.delete(item)
|
|
123
213
|
self._load_config_into_tree()
|
|
124
214
|
|
|
215
|
+
if selected_section:
|
|
216
|
+
self.tree.item(selected_section, open=True)
|
|
217
|
+
self.tree.focus(f"{selected_section}-{selected_text}")
|
|
218
|
+
self.tree.selection_set(f"{selected_section}-{selected_text}")
|
|
219
|
+
elif selected:
|
|
220
|
+
self.tree.focus(selected_text)
|
|
221
|
+
self.tree.selection_set(selected_text) # type: ignore
|
|
222
|
+
|
|
125
223
|
def _configure_treeview(self):
|
|
126
224
|
"""Configure a treeview to display and edit configuration options."""
|
|
127
225
|
scrollbar = ttk.Scrollbar(self)
|
|
@@ -146,14 +244,13 @@ class ConfigEditorFrame(ttk.Frame):
|
|
|
146
244
|
|
|
147
245
|
|
|
148
246
|
class ConfigEditorButtonFrame(ttk.Frame):
|
|
149
|
-
def __init__(self,
|
|
247
|
+
def __init__(self, editor_frame):
|
|
150
248
|
"""Initialize the button frame that contains buttons for saving the updated
|
|
151
249
|
configuration and adding custom items.
|
|
152
250
|
"""
|
|
153
|
-
super().__init__(
|
|
251
|
+
super().__init__(editor_frame, padding=10)
|
|
154
252
|
|
|
155
|
-
self.
|
|
156
|
-
self.tree = tree
|
|
253
|
+
self.editor_frame = editor_frame
|
|
157
254
|
self.custom_item_dialog = None
|
|
158
255
|
|
|
159
256
|
self._add_widgets()
|
|
@@ -162,13 +259,10 @@ class ConfigEditorButtonFrame(ttk.Frame):
|
|
|
162
259
|
"""Add buttons to the button frame for saving the configuration and adding
|
|
163
260
|
custom items.
|
|
164
261
|
"""
|
|
165
|
-
save_button = ttk.Button(self, text="Save", command=self._save_config)
|
|
166
|
-
save_button.pack(side="left", expand=True, padx=5)
|
|
167
|
-
|
|
168
262
|
reset_button = ttk.Button(self, text="Reset", command=self._reset_config)
|
|
169
263
|
reset_button.pack(side="left", expand=True, padx=5)
|
|
170
264
|
|
|
171
|
-
custom_item_button = ttk.Button(self, text="Add
|
|
265
|
+
custom_item_button = ttk.Button(self, text="Add Item", command=self._add_custom_item)
|
|
172
266
|
custom_item_button.pack(side="left", expand=True, padx=5)
|
|
173
267
|
|
|
174
268
|
import_inventory_button = ttk.Button(
|
|
@@ -176,72 +270,66 @@ class ConfigEditorButtonFrame(ttk.Frame):
|
|
|
176
270
|
)
|
|
177
271
|
import_inventory_button.pack(side="left", expand=True, padx=5)
|
|
178
272
|
|
|
179
|
-
def _save_config(self):
|
|
180
|
-
"""Save the current configuration from the treeview to the config file."""
|
|
181
|
-
for child in self.tree.get_children():
|
|
182
|
-
for item in self.tree.get_children(child):
|
|
183
|
-
title_option = self.tree.item(item, "text")
|
|
184
|
-
config_option = title_option.lower().replace(" ", "_")
|
|
185
|
-
value = self.tree.item(item, "values")[0]
|
|
186
|
-
section = self.tree.parent(item)
|
|
187
|
-
section_name = self.tree.item(section, "text")
|
|
188
|
-
if section_name == "Custom Items":
|
|
189
|
-
# custom items are already saved upon creation (Saving them again would result in duplicates)
|
|
190
|
-
continue
|
|
191
|
-
config.set(section_name, config_option, value)
|
|
192
|
-
|
|
193
|
-
config.write_to_file()
|
|
194
|
-
if config.valid:
|
|
195
|
-
messagebox.showinfo("Config Saved", "The configuration has been saved successfully.")
|
|
196
|
-
else:
|
|
197
|
-
config.load()
|
|
198
|
-
self.parent.reload_config_into_tree()
|
|
199
|
-
messagebox.showerror(
|
|
200
|
-
"Config Error",
|
|
201
|
-
f"The configuration is invalid. ({config.last_error})",
|
|
202
|
-
)
|
|
203
|
-
|
|
204
273
|
def _reset_config(self):
|
|
205
274
|
"""Reset the configuration file to its default state."""
|
|
206
275
|
confirm = messagebox.askokcancel(
|
|
207
|
-
"Reset Config",
|
|
276
|
+
"Reset Config",
|
|
277
|
+
"Are you sure you want to reset the configuration?",
|
|
278
|
+
parent=self.editor_frame,
|
|
208
279
|
)
|
|
209
280
|
if confirm:
|
|
210
281
|
copy(CONFIG_FILE_BACKUP, CONFIG_FILE)
|
|
211
|
-
config.
|
|
212
|
-
self.
|
|
282
|
+
config.load_from_file()
|
|
283
|
+
self.editor_frame.reload_config_into_tree()
|
|
284
|
+
self.editor_frame.focus_set()
|
|
285
|
+
self.editor_frame.tree.focus_set()
|
|
213
286
|
|
|
214
287
|
def _add_custom_item(self):
|
|
215
288
|
"""Open a window to add a new custom item."""
|
|
216
|
-
custom_item_window = tk.Toplevel(self.
|
|
289
|
+
custom_item_window = tk.Toplevel(self.editor_frame)
|
|
217
290
|
custom_item_window.title(ADD_CUSTOM_ITEM_TITLE)
|
|
218
|
-
custom_item_window.geometry(ADD_CUSTOM_ITEM_SIZE)
|
|
291
|
+
custom_item_window.geometry(centered(custom_item_window, ADD_CUSTOM_ITEM_SIZE))
|
|
292
|
+
custom_item_window.minsize(*size_info(ADD_CUSTOM_ITEM_SIZE))
|
|
293
|
+
custom_item_window.focus_set()
|
|
219
294
|
|
|
220
|
-
|
|
295
|
+
def on_close():
|
|
296
|
+
custom_item_window.destroy()
|
|
297
|
+
self.editor_frame.tree.focus_set()
|
|
298
|
+
|
|
299
|
+
custom_item_window.protocol("WM_DELETE_WINDOW", on_close)
|
|
300
|
+
|
|
301
|
+
custom_item_frame = CustomItemFrame(custom_item_window, self.editor_frame)
|
|
221
302
|
custom_item_frame.pack(expand=True, fill="both", padx=15, pady=15)
|
|
222
303
|
|
|
223
304
|
def _import_steam_inventory(self):
|
|
224
305
|
"""Open a window to import the user's Steam inventory."""
|
|
225
|
-
steam_inventory_window = tk.Toplevel(self.
|
|
306
|
+
steam_inventory_window = tk.Toplevel(self.editor_frame)
|
|
226
307
|
steam_inventory_window.title(IMPORT_INVENTORY_TITLE)
|
|
227
|
-
steam_inventory_window.geometry(IMPORT_INVENTORY_SIZE)
|
|
308
|
+
steam_inventory_window.geometry(centered(steam_inventory_window, IMPORT_INVENTORY_SIZE))
|
|
309
|
+
steam_inventory_window.minsize(*size_info(IMPORT_INVENTORY_SIZE))
|
|
310
|
+
steam_inventory_window.focus_set()
|
|
311
|
+
|
|
312
|
+
def on_close():
|
|
313
|
+
steam_inventory_window.destroy()
|
|
314
|
+
self.editor_frame.tree.focus_set()
|
|
228
315
|
|
|
229
|
-
|
|
316
|
+
steam_inventory_window.protocol("WM_DELETE_WINDOW", on_close)
|
|
317
|
+
|
|
318
|
+
steam_inventory_frame = InventoryImportFrame(steam_inventory_window, self.editor_frame)
|
|
230
319
|
steam_inventory_frame.pack(expand=True, fill="both", padx=15, pady=15)
|
|
231
320
|
|
|
232
321
|
|
|
233
322
|
class CustomItemFrame(ttk.Frame):
|
|
234
|
-
def __init__(self,
|
|
323
|
+
def __init__(self, window, editor_frame):
|
|
235
324
|
"""Initialize the custom item frame that allows users to add custom items."""
|
|
236
|
-
super().__init__(
|
|
237
|
-
self.
|
|
238
|
-
self.
|
|
239
|
-
self.tree = tree
|
|
325
|
+
super().__init__(window, style="Card.TFrame", padding=15)
|
|
326
|
+
self.window = window
|
|
327
|
+
self.editor_frame = editor_frame
|
|
240
328
|
self._add_widgets()
|
|
241
329
|
|
|
242
330
|
def _add_widgets(self):
|
|
243
331
|
"""Add widgets to the custom item frame for entering item details."""
|
|
244
|
-
ttk.Label(self, text="
|
|
332
|
+
ttk.Label(self, text="Steam Market Listing URL:").pack(pady=5)
|
|
245
333
|
item_url_entry = ttk.Entry(self)
|
|
246
334
|
item_url_entry.pack(fill="x", padx=10)
|
|
247
335
|
|
|
@@ -255,92 +343,155 @@ class CustomItemFrame(ttk.Frame):
|
|
|
255
343
|
command=lambda: self._add_custom_item(item_url_entry.get(), item_owned_entry.get()),
|
|
256
344
|
)
|
|
257
345
|
add_button.pack(pady=10)
|
|
346
|
+
self.window.bind("<Return>", lambda _: add_button.invoke())
|
|
347
|
+
|
|
348
|
+
def _update_existing(self, section, item_name, item_owned):
|
|
349
|
+
"""
|
|
350
|
+
Try to update an item in the treeview with the new item details.
|
|
258
351
|
|
|
259
|
-
|
|
352
|
+
:return: True if the item was updated, False if it was not found.
|
|
353
|
+
"""
|
|
354
|
+
for existing_item in self.editor_frame.tree.get_children(section):
|
|
355
|
+
existing_item_name = self.editor_frame.tree.item(existing_item, "text")
|
|
356
|
+
if item_name == existing_item_name:
|
|
357
|
+
self.editor_frame.tree.set(existing_item, column="#1", value=item_owned)
|
|
358
|
+
self.editor_frame.focus_set()
|
|
359
|
+
self.editor_frame.save_config()
|
|
360
|
+
self.window.destroy()
|
|
361
|
+
return True
|
|
362
|
+
return False
|
|
363
|
+
|
|
364
|
+
def _get_insert_index(self, item_name, section):
|
|
365
|
+
"""Get the index to insert the new item in alphabetical order."""
|
|
366
|
+
insert_index = "end"
|
|
367
|
+
for existing_item_index, existing_item in enumerate(
|
|
368
|
+
self.editor_frame.tree.get_children(section)
|
|
369
|
+
):
|
|
370
|
+
existing_item_name = self.editor_frame.tree.item(existing_item, "text")
|
|
371
|
+
if item_name < existing_item_name:
|
|
372
|
+
insert_index = existing_item_index
|
|
373
|
+
break
|
|
374
|
+
return insert_index
|
|
375
|
+
|
|
376
|
+
def _add_custom_item(self, item_href, item_owned):
|
|
260
377
|
"""Add a custom item to the configuration."""
|
|
261
|
-
if not
|
|
262
|
-
messagebox.showerror(
|
|
378
|
+
if not item_href or not item_owned:
|
|
379
|
+
messagebox.showerror(
|
|
380
|
+
"Input Error", "All fields must be filled out.", parent=self.window
|
|
381
|
+
)
|
|
382
|
+
return
|
|
383
|
+
if config.option_exists(item_href, exclude_sections=("Stickers", "Skins")):
|
|
384
|
+
messagebox.showerror(
|
|
385
|
+
"Item Exists", "This item already exists in another section.", parent=self.window
|
|
386
|
+
)
|
|
263
387
|
return
|
|
388
|
+
|
|
264
389
|
try:
|
|
265
|
-
|
|
266
|
-
raise ValueError("Owned count must be a non-negative integer.")
|
|
390
|
+
item_name = config.option_to_name(item_href, href=True)
|
|
267
391
|
except ValueError as error:
|
|
268
|
-
messagebox.showerror("
|
|
392
|
+
messagebox.showerror("Invalid URL", str(error), parent=self.window)
|
|
269
393
|
return
|
|
270
394
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
if
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
395
|
+
if self._update_existing("Stickers", item_name, item_owned):
|
|
396
|
+
return
|
|
397
|
+
if self._update_existing("Skins", item_name, item_owned):
|
|
398
|
+
return
|
|
399
|
+
|
|
400
|
+
section = "Stickers" if item_name.startswith("Sticker") else "Skins"
|
|
401
|
+
insert_index = self._get_insert_index(item_name, section)
|
|
402
|
+
self.editor_frame.tree.insert(
|
|
403
|
+
section,
|
|
404
|
+
insert_index,
|
|
405
|
+
iid=f"{section}-{item_name}",
|
|
406
|
+
text=item_name,
|
|
407
|
+
values=[item_owned],
|
|
408
|
+
)
|
|
409
|
+
self.editor_frame.save_config()
|
|
410
|
+
self.window.destroy()
|
|
283
411
|
|
|
284
412
|
|
|
285
413
|
class InventoryImportFrame(ttk.Frame):
|
|
286
414
|
# pylint: disable=too-many-instance-attributes
|
|
287
|
-
def __init__(self,
|
|
415
|
+
def __init__(self, window, editor_frame):
|
|
288
416
|
"""Initialize the inventory import frame that allows users to import their Steam
|
|
289
417
|
inventory.
|
|
290
418
|
"""
|
|
291
|
-
super().__init__(
|
|
292
|
-
self.
|
|
293
|
-
self.
|
|
419
|
+
super().__init__(window, padding=10)
|
|
420
|
+
self.window = window
|
|
421
|
+
self.editor_frame = editor_frame
|
|
294
422
|
self._add_widgets()
|
|
295
423
|
|
|
296
424
|
def _add_widgets(self):
|
|
297
425
|
"""Add widgets to the inventory import frame."""
|
|
426
|
+
self._configure_entries()
|
|
427
|
+
self.user_name_label.pack(pady=5)
|
|
428
|
+
self.user_name_entry.pack(fill="x", expand=True, padx=10)
|
|
429
|
+
self.password_label.pack(pady=5)
|
|
430
|
+
self.password_entry.pack(fill="x", expand=True, padx=10)
|
|
431
|
+
self.two_factor_label.pack(pady=5)
|
|
432
|
+
self.two_factor_entry.pack(fill="x", expand=True, padx=10)
|
|
433
|
+
self.import_button.pack(pady=10)
|
|
434
|
+
self.entry_frame.pack(side="left", padx=10, pady=(0, 20), fill="both", expand=True)
|
|
435
|
+
|
|
298
436
|
self._configure_checkboxes()
|
|
437
|
+
self.storage_units_checkbox.pack(anchor="w", padx=10, pady=5)
|
|
438
|
+
self.regular_inventory_checkbox.pack(anchor="w", padx=10, pady=5)
|
|
299
439
|
self.import_cases_checkbox.pack(anchor="w", padx=10, pady=5)
|
|
300
440
|
self.import_sticker_capsules_checkbox.pack(anchor="w", padx=10, pady=5)
|
|
301
441
|
self.import_stickers_checkbox.pack(anchor="w", padx=10, pady=5)
|
|
302
442
|
self.import_others_checkbox.pack(anchor="w", padx=10, pady=5)
|
|
303
|
-
|
|
304
|
-
self._configure_entries()
|
|
305
|
-
self.user_name_label.pack(pady=10)
|
|
306
|
-
self.user_name_entry.pack(fill="x", padx=50)
|
|
307
|
-
self.password_label.pack(pady=10)
|
|
308
|
-
self.password_entry.pack(fill="x", padx=50)
|
|
309
|
-
self.two_factor_label.pack(pady=10)
|
|
310
|
-
self.two_factor_entry.pack(fill="x", padx=50)
|
|
311
|
-
|
|
312
|
-
self.import_button = ttk.Button(self, text="Import", command=self._import_inventory)
|
|
313
|
-
self.import_button.pack(pady=10)
|
|
443
|
+
self.checkbox_frame.pack(side="left", padx=10, pady=(0, 20), fill="both", expand=True)
|
|
314
444
|
|
|
315
445
|
def _configure_checkboxes(self):
|
|
316
446
|
# pylint: disable=attribute-defined-outside-init
|
|
317
447
|
"""Configure the checkboxes for selecting what to import from the Steam
|
|
318
448
|
inventory.
|
|
319
449
|
"""
|
|
450
|
+
self.checkbox_frame = ttk.LabelFrame(self, text="Import Settings", padding=15)
|
|
451
|
+
|
|
452
|
+
self.regular_inventory_value = tk.BooleanVar(value=False)
|
|
453
|
+
self.regular_inventory_checkbox = ttk.Checkbutton(
|
|
454
|
+
self.checkbox_frame,
|
|
455
|
+
text="Regular Inventory",
|
|
456
|
+
variable=self.regular_inventory_value,
|
|
457
|
+
style="Switch.TCheckbutton",
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
self.storage_units_value = tk.BooleanVar(value=True)
|
|
461
|
+
self.storage_units_checkbox = ttk.Checkbutton(
|
|
462
|
+
self.checkbox_frame,
|
|
463
|
+
text="Storage Units",
|
|
464
|
+
variable=self.storage_units_value,
|
|
465
|
+
style="Switch.TCheckbutton",
|
|
466
|
+
)
|
|
467
|
+
|
|
320
468
|
self.import_cases_value = tk.BooleanVar(value=True)
|
|
321
469
|
self.import_cases_checkbox = ttk.Checkbutton(
|
|
322
|
-
self
|
|
470
|
+
self.checkbox_frame,
|
|
471
|
+
text="Import Cases",
|
|
472
|
+
variable=self.import_cases_value,
|
|
473
|
+
style="Switch.TCheckbutton",
|
|
323
474
|
)
|
|
324
475
|
|
|
325
476
|
self.import_sticker_capsules_value = tk.BooleanVar(value=True)
|
|
326
477
|
self.import_sticker_capsules_checkbox = ttk.Checkbutton(
|
|
327
|
-
self,
|
|
478
|
+
self.checkbox_frame,
|
|
328
479
|
text="Import Sticker Capsules",
|
|
329
480
|
variable=self.import_sticker_capsules_value,
|
|
330
481
|
style="Switch.TCheckbutton",
|
|
331
482
|
)
|
|
332
483
|
|
|
333
|
-
self.import_stickers_value = tk.BooleanVar(value=
|
|
484
|
+
self.import_stickers_value = tk.BooleanVar(value=False)
|
|
334
485
|
self.import_stickers_checkbox = ttk.Checkbutton(
|
|
335
|
-
self,
|
|
486
|
+
self.checkbox_frame,
|
|
336
487
|
text="Import Stickers",
|
|
337
488
|
variable=self.import_stickers_value,
|
|
338
489
|
style="Switch.TCheckbutton",
|
|
339
490
|
)
|
|
340
491
|
|
|
341
|
-
self.import_others_value = tk.BooleanVar(value=
|
|
492
|
+
self.import_others_value = tk.BooleanVar(value=False)
|
|
342
493
|
self.import_others_checkbox = ttk.Checkbutton(
|
|
343
|
-
self,
|
|
494
|
+
self.checkbox_frame,
|
|
344
495
|
text="Import Other Items",
|
|
345
496
|
variable=self.import_others_value,
|
|
346
497
|
style="Switch.TCheckbutton",
|
|
@@ -351,14 +502,37 @@ class InventoryImportFrame(ttk.Frame):
|
|
|
351
502
|
"""Configure the entry fields for Steam username, password, and two-factor
|
|
352
503
|
code.
|
|
353
504
|
"""
|
|
354
|
-
self.
|
|
355
|
-
|
|
505
|
+
self.entry_frame = ttk.Frame(self, style="Card.TFrame", padding=15)
|
|
506
|
+
|
|
507
|
+
self.user_name_label = ttk.Label(self.entry_frame, text="Steam Username:")
|
|
508
|
+
self.user_name_entry = ttk.Entry(self.entry_frame, justify="center", font=("Helvetica", 11))
|
|
356
509
|
|
|
357
|
-
self.password_label = ttk.Label(self, text="Steam Password:")
|
|
358
|
-
self.password_entry = ttk.Entry(
|
|
510
|
+
self.password_label = ttk.Label(self.entry_frame, text="Steam Password:")
|
|
511
|
+
self.password_entry = ttk.Entry(
|
|
512
|
+
self.entry_frame, show="*", justify="center", font=("Helvetica", 11)
|
|
513
|
+
)
|
|
359
514
|
|
|
360
|
-
self.two_factor_label = ttk.Label(self, text="Steam Guard Code
|
|
361
|
-
self.two_factor_entry = ttk.Entry(
|
|
515
|
+
self.two_factor_label = ttk.Label(self.entry_frame, text="Steam Guard Code:")
|
|
516
|
+
self.two_factor_entry = ttk.Entry(
|
|
517
|
+
self.entry_frame, justify="center", font=("Helvetica", 11)
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
self.import_button = ttk.Button(
|
|
521
|
+
self.entry_frame, text="Import", command=self._import_inventory, state="disabled"
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
def check_form(_):
|
|
525
|
+
if (
|
|
526
|
+
len(self.user_name_entry.get().strip()) > 0
|
|
527
|
+
and len(self.password_entry.get().strip()) > 0
|
|
528
|
+
and len(self.two_factor_entry.get().strip()) > 0
|
|
529
|
+
):
|
|
530
|
+
self.import_button.configure(state="normal")
|
|
531
|
+
else:
|
|
532
|
+
self.import_button.configure(state="disabled")
|
|
533
|
+
|
|
534
|
+
self.window.bind("<KeyRelease>", check_form)
|
|
535
|
+
self.window.bind("<Return>", lambda _: self.import_button.invoke())
|
|
362
536
|
|
|
363
537
|
def _import_inventory(self):
|
|
364
538
|
"""
|
|
@@ -367,6 +541,9 @@ class InventoryImportFrame(ttk.Frame):
|
|
|
367
541
|
This will also install the necessary npm packages if they are not already
|
|
368
542
|
installed.
|
|
369
543
|
"""
|
|
544
|
+
regular_inventory = self.regular_inventory_value.get()
|
|
545
|
+
storage_units = self.storage_units_value.get()
|
|
546
|
+
|
|
370
547
|
import_cases = self.import_cases_value.get()
|
|
371
548
|
import_sticker_capsules = self.import_sticker_capsules_value.get()
|
|
372
549
|
import_stickers = self.import_stickers_value.get()
|
|
@@ -380,6 +557,8 @@ class InventoryImportFrame(ttk.Frame):
|
|
|
380
557
|
[
|
|
381
558
|
INVENTORY_IMPORT_SCRIPT,
|
|
382
559
|
INVENTORY_IMPORT_FILE,
|
|
560
|
+
str(regular_inventory),
|
|
561
|
+
str(storage_units),
|
|
383
562
|
str(import_cases),
|
|
384
563
|
str(import_sticker_capsules),
|
|
385
564
|
str(import_stickers),
|
|
@@ -390,26 +569,37 @@ class InventoryImportFrame(ttk.Frame):
|
|
|
390
569
|
]
|
|
391
570
|
)
|
|
392
571
|
|
|
393
|
-
self.
|
|
572
|
+
self.window.destroy()
|
|
394
573
|
|
|
395
574
|
def _display_node_subprocess(self, node_cmd):
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
575
|
+
console_window = tk.Toplevel(self.editor_frame)
|
|
576
|
+
console_window.title(IMPORT_INVENTORY_PROCESS_TITLE)
|
|
577
|
+
console_window.geometry(centered(console_window, IMPORT_INVENTORY_PROCESS_SIZE))
|
|
578
|
+
console_window.minsize(*size_info(IMPORT_INVENTORY_PROCESS_SIZE))
|
|
579
|
+
console_window.focus_force()
|
|
580
|
+
|
|
581
|
+
def on_close():
|
|
582
|
+
console_window.destroy()
|
|
583
|
+
config.read_from_inventory_file()
|
|
584
|
+
self.editor_frame.reload_config_into_tree()
|
|
585
|
+
self.editor_frame.tree.focus_set()
|
|
586
|
+
|
|
587
|
+
console_window.protocol("WM_DELETE_WINDOW", on_close)
|
|
399
588
|
|
|
400
|
-
process_frame = InventoryImportProcessFrame(
|
|
589
|
+
process_frame = InventoryImportProcessFrame(console_window, self.editor_frame)
|
|
401
590
|
process_frame.pack(expand=True, fill="both", padx=15, pady=15)
|
|
591
|
+
process_frame.console.focus_force()
|
|
402
592
|
process_frame.start(node_cmd)
|
|
403
|
-
process_frame.console.focus_set()
|
|
404
593
|
|
|
405
594
|
|
|
406
595
|
class InventoryImportProcessFrame(ttk.Frame):
|
|
407
596
|
# pylint: disable=attribute-defined-outside-init
|
|
408
597
|
# Source: https://stackoverflow.com/questions/27327886/issues-intercepting-subprocess-output-in-real-time
|
|
409
|
-
def __init__(self,
|
|
598
|
+
def __init__(self, window, editor_frame):
|
|
410
599
|
"""Initialize the frame that displays the output of the subprocess."""
|
|
411
|
-
super().__init__(
|
|
412
|
-
self.
|
|
600
|
+
super().__init__(window)
|
|
601
|
+
self.window = window
|
|
602
|
+
self.editor_frame = editor_frame
|
|
413
603
|
self._add_widgets()
|
|
414
604
|
|
|
415
605
|
def _add_widgets(self):
|
|
@@ -419,6 +609,7 @@ class InventoryImportProcessFrame(ttk.Frame):
|
|
|
419
609
|
|
|
420
610
|
self.console = ThemedText(self, wrap="word", yscrollcommand=self.scrollbar.set)
|
|
421
611
|
self.console.config(state="disabled")
|
|
612
|
+
self.console.tag_configure("error", foreground="red")
|
|
422
613
|
self.console.pack(expand=True, fill="both", padx=10, pady=10)
|
|
423
614
|
|
|
424
615
|
self.scrollbar.config(command=self.console.yview)
|
|
@@ -438,28 +629,31 @@ class InventoryImportProcessFrame(ttk.Frame):
|
|
|
438
629
|
stdin=PIPE,
|
|
439
630
|
stderr=STDOUT,
|
|
440
631
|
text=True,
|
|
441
|
-
bufsize=1,
|
|
442
632
|
encoding="utf-8",
|
|
633
|
+
shell=True,
|
|
634
|
+
cwd=DATA_DIR,
|
|
443
635
|
)
|
|
444
636
|
self.queue = Queue()
|
|
445
637
|
self.thread = Thread(target=self._read_lines, args=(self.process, self.queue), daemon=True)
|
|
446
638
|
self.thread.start()
|
|
447
|
-
|
|
448
|
-
self.after(100, self._update_lines)
|
|
639
|
+
self._update_lines()
|
|
449
640
|
|
|
450
641
|
def _update_lines(self):
|
|
451
642
|
"""Update the text widget with lines from the subprocess output."""
|
|
452
643
|
try:
|
|
453
644
|
line = self.queue.get(block=False)
|
|
454
645
|
self.console.config(state="normal")
|
|
455
|
-
|
|
646
|
+
if "[ERROR]" in line:
|
|
647
|
+
self.console.insert("end", line, "error")
|
|
648
|
+
else:
|
|
649
|
+
self.console.insert("end", line)
|
|
456
650
|
self.console.config(state="disabled")
|
|
457
651
|
self.console.yview("end")
|
|
458
652
|
except Empty:
|
|
459
653
|
pass
|
|
460
654
|
|
|
461
655
|
if self.process.poll() is None or not self.queue.empty():
|
|
462
|
-
self.after(
|
|
656
|
+
self.after(35, self._update_lines)
|
|
463
657
|
else:
|
|
464
658
|
self._cleanup()
|
|
465
659
|
|
|
@@ -467,8 +661,10 @@ class InventoryImportProcessFrame(ttk.Frame):
|
|
|
467
661
|
"""Clean up the process and thread after completion and trigger a config update
|
|
468
662
|
from the newly written inventory file.
|
|
469
663
|
"""
|
|
470
|
-
config.read_from_inventory_file()
|
|
471
|
-
self.parent.master.master.reload_config_into_tree()
|
|
472
|
-
|
|
473
664
|
self.process.wait()
|
|
474
665
|
self.thread.join()
|
|
666
|
+
|
|
667
|
+
config.read_from_inventory_file()
|
|
668
|
+
self.editor_frame.reload_config_into_tree()
|
|
669
|
+
self.editor_frame.tree.focus_set()
|
|
670
|
+
self.window.destroy()
|