cs2tracker 2.1.12__py3-none-any.whl → 2.1.14__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/application.py +84 -88
- cs2tracker/app/editor_frame.py +451 -143
- cs2tracker/app/scraper_frame.py +43 -9
- cs2tracker/constants.py +66 -451
- cs2tracker/data/config.ini +153 -153
- cs2tracker/data/convert_inventory.js +187 -0
- cs2tracker/data/get_inventory.js +191 -0
- cs2tracker/main.py +2 -2
- cs2tracker/scraper/background_task.py +4 -4
- cs2tracker/scraper/discord_notifier.py +4 -4
- cs2tracker/scraper/parsers.py +192 -0
- cs2tracker/scraper/scraper.py +146 -224
- cs2tracker/util/__init__.py +2 -2
- cs2tracker/util/padded_console.py +32 -0
- cs2tracker/util/validated_config.py +117 -16
- cs2tracker-2.1.14.dist-info/METADATA +148 -0
- cs2tracker-2.1.14.dist-info/RECORD +28 -0
- cs2tracker-2.1.14.dist-info/licenses/LICENSE +402 -0
- cs2tracker-2.1.12.dist-info/METADATA +0 -82
- cs2tracker-2.1.12.dist-info/RECORD +0 -25
- cs2tracker-2.1.12.dist-info/licenses/LICENSE.md +0 -21
- {cs2tracker-2.1.12.dist-info → cs2tracker-2.1.14.dist-info}/WHEEL +0 -0
- {cs2tracker-2.1.12.dist-info → cs2tracker-2.1.14.dist-info}/entry_points.txt +0 -0
- {cs2tracker-2.1.12.dist-info → cs2tracker-2.1.14.dist-info}/top_level.txt +0 -0
cs2tracker/app/editor_frame.py
CHANGED
|
@@ -1,41 +1,46 @@
|
|
|
1
1
|
import tkinter as tk
|
|
2
|
+
from queue import Empty, Queue
|
|
2
3
|
from shutil import copy
|
|
4
|
+
from subprocess import PIPE, STDOUT
|
|
5
|
+
from threading import Thread
|
|
3
6
|
from tkinter import messagebox, ttk
|
|
4
7
|
|
|
5
|
-
from
|
|
8
|
+
from nodejs import node
|
|
9
|
+
from ttk_text import ThemedText
|
|
6
10
|
|
|
7
|
-
|
|
11
|
+
from cs2tracker.constants import (
|
|
12
|
+
CONFIG_FILE,
|
|
13
|
+
CONFIG_FILE_BACKUP,
|
|
14
|
+
DATA_DIR,
|
|
15
|
+
INVENTORY_IMPORT_FILE,
|
|
16
|
+
INVENTORY_IMPORT_SCRIPT,
|
|
17
|
+
)
|
|
18
|
+
from cs2tracker.util import get_config
|
|
8
19
|
|
|
9
|
-
|
|
10
|
-
|
|
20
|
+
ADD_CUSTOM_ITEM_TITLE = "Add Custom Item"
|
|
21
|
+
ADD_CUSTOM_ITEM_SIZE = "500x220"
|
|
22
|
+
|
|
23
|
+
IMPORT_INVENTORY_TITLE = "Import Steam Inventory"
|
|
24
|
+
IMPORT_INVENTORY_SIZE = "600x550"
|
|
25
|
+
|
|
26
|
+
IMPORT_INVENTORY_PROCESS_TITLE = "Importing Steam Inventory..."
|
|
27
|
+
IMPORT_INVENTORY_PROCESS_SIZE = "700x500"
|
|
28
|
+
|
|
29
|
+
config = get_config()
|
|
11
30
|
|
|
12
31
|
|
|
13
32
|
class ConfigEditorFrame(ttk.Frame):
|
|
14
|
-
def __init__(self, parent
|
|
33
|
+
def __init__(self, parent):
|
|
15
34
|
"""Initialize the configuration editor frame that allows users to view and edit
|
|
16
35
|
the configuration options.
|
|
17
36
|
"""
|
|
18
|
-
|
|
19
|
-
super().__init__(parent, style="Card.TFrame", padding=15)
|
|
37
|
+
super().__init__(parent, padding=15)
|
|
20
38
|
|
|
21
39
|
self.parent = parent
|
|
22
|
-
self.
|
|
40
|
+
self.edit_entry = None
|
|
23
41
|
self._add_widgets()
|
|
24
42
|
|
|
25
|
-
|
|
26
|
-
"""Reload the configuration options into the treeview for display and
|
|
27
|
-
editing.
|
|
28
|
-
"""
|
|
29
|
-
for item in self.tree.get_children():
|
|
30
|
-
self.tree.delete(item)
|
|
31
|
-
|
|
32
|
-
for section in self.scraper.config.sections():
|
|
33
|
-
if section == "App Settings":
|
|
34
|
-
continue
|
|
35
|
-
section_level = self.tree.insert("", "end", iid=section, text=section)
|
|
36
|
-
for config_option, value in self.scraper.config.items(section):
|
|
37
|
-
title_option = config_option.replace("_", " ").title()
|
|
38
|
-
self.tree.insert(section_level, "end", text=title_option, values=[value])
|
|
43
|
+
self.tree.focus_set()
|
|
39
44
|
|
|
40
45
|
def _add_widgets(self):
|
|
41
46
|
"""Configure the main editor frame which displays the configuration options in a
|
|
@@ -44,70 +49,134 @@ class ConfigEditorFrame(ttk.Frame):
|
|
|
44
49
|
self._configure_treeview()
|
|
45
50
|
self.tree.pack(expand=True, fill="both")
|
|
46
51
|
|
|
47
|
-
button_frame = ConfigEditorButtonFrame(self
|
|
52
|
+
button_frame = ConfigEditorButtonFrame(self)
|
|
48
53
|
button_frame.pack(side="bottom", padx=10, pady=(0, 10))
|
|
49
54
|
|
|
50
|
-
def
|
|
51
|
-
"""
|
|
52
|
-
|
|
53
|
-
|
|
55
|
+
def save_config(self):
|
|
56
|
+
"""Save the current configuration from the treeview to the config file."""
|
|
57
|
+
config.delete_display_sections()
|
|
58
|
+
for section in self.tree.get_children():
|
|
59
|
+
config.add_section(section)
|
|
60
|
+
for item in self.tree.get_children(section):
|
|
61
|
+
item_name = self.tree.item(item, "text")
|
|
62
|
+
if section not in ("App Settings", "User Settings"):
|
|
63
|
+
config_option = config.name_to_option(item_name, href=True)
|
|
64
|
+
else:
|
|
65
|
+
config_option = config.name_to_option(item_name)
|
|
66
|
+
value = self.tree.item(item, "values")[0]
|
|
67
|
+
config.set(section, config_option, value)
|
|
68
|
+
|
|
69
|
+
config.write_to_file()
|
|
70
|
+
if not config.valid:
|
|
71
|
+
config.load_from_file()
|
|
72
|
+
self.reload_config_into_tree()
|
|
73
|
+
messagebox.showerror(
|
|
74
|
+
"Config Error", f"The configuration is invalid. ({config.last_error})", parent=self
|
|
75
|
+
)
|
|
76
|
+
self.parent.focus_set()
|
|
77
|
+
self.tree.focus_set()
|
|
54
78
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
79
|
+
def _save_edit(self, event, row, column):
|
|
80
|
+
"""Save the edited value in the treeview and destroy the entry widget."""
|
|
81
|
+
self.tree.set(row, column=column, value=event.widget.get())
|
|
82
|
+
self.save_config()
|
|
83
|
+
event.widget.destroy()
|
|
58
84
|
|
|
85
|
+
def _set_cell_value(self, event, row=None, column=None):
|
|
86
|
+
"""
|
|
87
|
+
Set the value of a cell in the treeview to be editable when double-clicked.
|
|
88
|
+
|
|
89
|
+
Source: https://stackoverflow.com/questions/75787251/create-an-editable-tkinter-treeview-with-keyword-connection
|
|
90
|
+
"""
|
|
59
91
|
try:
|
|
60
|
-
row
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
92
|
+
if not row or not column:
|
|
93
|
+
row = self.tree.identify_row(event.y)
|
|
94
|
+
column = self.tree.identify_column(event.x)
|
|
95
|
+
|
|
96
|
+
item_text = self.tree.item(row, "text")
|
|
97
|
+
if column == "#0" or any(item_text == section for section in config.sections()):
|
|
98
|
+
return
|
|
99
|
+
item_value = self.tree.item(row, "values")[0]
|
|
100
|
+
|
|
68
101
|
x, y, w, h = self.tree.bbox(row, column)
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
102
|
+
self.edit_entry = ttk.Entry(self, justify="center", font=("Helvetica", 11))
|
|
103
|
+
self.edit_entry.place(x=x, y=y, width=w, height=h + 3) # type: ignore
|
|
104
|
+
self.edit_entry.insert("end", item_value)
|
|
105
|
+
self.edit_entry.bind("<Return>", lambda e: self._save_edit(e, row, column))
|
|
106
|
+
self.edit_entry.focus_set()
|
|
107
|
+
self.edit_entry.grab_set()
|
|
75
108
|
except Exception:
|
|
76
|
-
|
|
109
|
+
return
|
|
77
110
|
|
|
78
|
-
def
|
|
79
|
-
"""
|
|
80
|
-
|
|
111
|
+
def _set_selection_value(self, _):
|
|
112
|
+
"""Set the value of the currently selected cell in the treeview to be
|
|
113
|
+
editable.
|
|
81
114
|
"""
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
115
|
+
selected = self.tree.selection()
|
|
116
|
+
if selected:
|
|
117
|
+
row = selected[0]
|
|
118
|
+
column = "#1"
|
|
119
|
+
self._set_cell_value(None, row=row, column=column)
|
|
85
120
|
|
|
86
|
-
def
|
|
87
|
-
"""
|
|
88
|
-
|
|
89
|
-
event.widget.destroy()
|
|
121
|
+
def _delete_selection_value(self, _):
|
|
122
|
+
"""
|
|
123
|
+
Delete the value of the currently selected cell in the treeview.
|
|
90
124
|
|
|
91
|
-
|
|
125
|
+
This only works for custom items, as other sections are not editable.
|
|
92
126
|
"""
|
|
93
|
-
|
|
94
|
-
|
|
127
|
+
selected = self.tree.selection()
|
|
128
|
+
if selected:
|
|
129
|
+
row = selected[0]
|
|
130
|
+
section_name = self.tree.parent(row)
|
|
131
|
+
if section_name == "Custom Items":
|
|
132
|
+
self.tree.delete(row)
|
|
133
|
+
self.save_config()
|
|
134
|
+
self.tree.focus("Custom Items")
|
|
135
|
+
self.tree.selection_set("Custom Items")
|
|
136
|
+
|
|
137
|
+
def _destroy_entry(self, _):
|
|
138
|
+
"""Destroy any entry widgets in the treeview on an event, such as a mouse wheel
|
|
139
|
+
movement.
|
|
140
|
+
"""
|
|
141
|
+
if self.edit_entry:
|
|
142
|
+
self.edit_entry.destroy()
|
|
143
|
+
self.edit_entry = None
|
|
144
|
+
self.tree.focus_set()
|
|
95
145
|
|
|
96
|
-
|
|
146
|
+
def _make_tree_editable(self):
|
|
147
|
+
"""Add a binding to the treeview that allows double-clicking on a cell to edit
|
|
148
|
+
its value.
|
|
97
149
|
"""
|
|
98
150
|
self.tree.bind("<Double-1>", self._set_cell_value)
|
|
99
|
-
self.
|
|
100
|
-
self.
|
|
151
|
+
self.tree.bind("<Return>", self._set_selection_value)
|
|
152
|
+
self.tree.bind("<BackSpace>", self._delete_selection_value)
|
|
153
|
+
self.parent.bind("<MouseWheel>", self._destroy_entry)
|
|
154
|
+
self.parent.bind("<Escape>", self._destroy_entry)
|
|
101
155
|
|
|
102
156
|
def _load_config_into_tree(self):
|
|
103
157
|
"""Load the configuration options into the treeview for display and editing."""
|
|
104
|
-
for section in
|
|
158
|
+
for section in config.sections():
|
|
105
159
|
if section == "App Settings":
|
|
106
160
|
continue
|
|
107
161
|
section_level = self.tree.insert("", "end", iid=section, text=section)
|
|
108
|
-
for config_option, value in
|
|
109
|
-
|
|
110
|
-
|
|
162
|
+
for config_option, value in config.items(section):
|
|
163
|
+
if section not in ("User Settings", "App Settings"):
|
|
164
|
+
option_name = config.option_to_name(config_option, href=True)
|
|
165
|
+
self.tree.insert(section_level, "end", text=option_name, values=[value])
|
|
166
|
+
else:
|
|
167
|
+
option_name = config.option_to_name(config_option)
|
|
168
|
+
self.tree.insert(section_level, "end", text=option_name, values=[value])
|
|
169
|
+
|
|
170
|
+
self.tree.focus("User Settings")
|
|
171
|
+
self.tree.selection_set("User Settings")
|
|
172
|
+
|
|
173
|
+
def reload_config_into_tree(self):
|
|
174
|
+
"""Reload the configuration options into the treeview for display and
|
|
175
|
+
editing.
|
|
176
|
+
"""
|
|
177
|
+
for item in self.tree.get_children():
|
|
178
|
+
self.tree.delete(item)
|
|
179
|
+
self._load_config_into_tree()
|
|
111
180
|
|
|
112
181
|
def _configure_treeview(self):
|
|
113
182
|
"""Configure a treeview to display and edit configuration options."""
|
|
@@ -124,7 +193,7 @@ class ConfigEditorFrame(ttk.Frame):
|
|
|
124
193
|
scrollbar.config(command=self.tree.yview)
|
|
125
194
|
|
|
126
195
|
self.tree.column("#0", anchor="w", width=200)
|
|
127
|
-
self.tree.column(1, anchor="
|
|
196
|
+
self.tree.column(1, anchor="center", width=25)
|
|
128
197
|
self.tree.heading("#0", text="Option")
|
|
129
198
|
self.tree.heading(1, text="Value")
|
|
130
199
|
|
|
@@ -133,16 +202,13 @@ class ConfigEditorFrame(ttk.Frame):
|
|
|
133
202
|
|
|
134
203
|
|
|
135
204
|
class ConfigEditorButtonFrame(ttk.Frame):
|
|
136
|
-
def __init__(self,
|
|
205
|
+
def __init__(self, editor_frame):
|
|
137
206
|
"""Initialize the button frame that contains buttons for saving the updated
|
|
138
207
|
configuration and adding custom items.
|
|
139
208
|
"""
|
|
209
|
+
super().__init__(editor_frame, padding=10)
|
|
140
210
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
self.parent = parent
|
|
144
|
-
self.scraper = scraper
|
|
145
|
-
self.tree = tree
|
|
211
|
+
self.editor_frame = editor_frame
|
|
146
212
|
self.custom_item_dialog = None
|
|
147
213
|
|
|
148
214
|
self._add_widgets()
|
|
@@ -151,97 +217,339 @@ class ConfigEditorButtonFrame(ttk.Frame):
|
|
|
151
217
|
"""Add buttons to the button frame for saving the configuration and adding
|
|
152
218
|
custom items.
|
|
153
219
|
"""
|
|
154
|
-
save_button = ttk.Button(self, text="Save", command=self._save_config)
|
|
155
|
-
save_button.pack(side="left", expand=True, padx=5)
|
|
156
|
-
|
|
157
220
|
reset_button = ttk.Button(self, text="Reset", command=self._reset_config)
|
|
158
221
|
reset_button.pack(side="left", expand=True, padx=5)
|
|
159
222
|
|
|
160
|
-
custom_item_button = ttk.Button(
|
|
161
|
-
self, text="Add Custom Item", command=self._open_custom_item_dialog
|
|
162
|
-
)
|
|
223
|
+
custom_item_button = ttk.Button(self, text="Add Custom Item", command=self._add_custom_item)
|
|
163
224
|
custom_item_button.pack(side="left", expand=True, padx=5)
|
|
164
225
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
title_option = self.tree.item(item, "text")
|
|
170
|
-
config_option = title_option.lower().replace(" ", "_")
|
|
171
|
-
value = self.tree.item(item, "values")[0]
|
|
172
|
-
section = self.tree.parent(item)
|
|
173
|
-
section_name = self.tree.item(section, "text")
|
|
174
|
-
if section_name == "Custom Items":
|
|
175
|
-
# custom items are already saved upon creation (Saving them again would result in duplicates)
|
|
176
|
-
continue
|
|
177
|
-
self.scraper.config.set(section_name, config_option, value)
|
|
178
|
-
|
|
179
|
-
self.scraper.config.write_to_file()
|
|
180
|
-
if self.scraper.config.valid:
|
|
181
|
-
messagebox.showinfo("Config Saved", "The configuration has been saved successfully.")
|
|
182
|
-
else:
|
|
183
|
-
messagebox.showerror(
|
|
184
|
-
"Config Error",
|
|
185
|
-
f"The configuration is invalid. ({self.scraper.config.last_error})",
|
|
186
|
-
)
|
|
226
|
+
import_inventory_button = ttk.Button(
|
|
227
|
+
self, text="Import Steam Inventory", command=self._import_steam_inventory
|
|
228
|
+
)
|
|
229
|
+
import_inventory_button.pack(side="left", expand=True, padx=5)
|
|
187
230
|
|
|
188
231
|
def _reset_config(self):
|
|
189
232
|
"""Reset the configuration file to its default state."""
|
|
190
233
|
confirm = messagebox.askokcancel(
|
|
191
|
-
"Reset Config", "Are you sure you want to reset the configuration?"
|
|
234
|
+
"Reset Config", "Are you sure you want to reset the configuration?", parent=self
|
|
192
235
|
)
|
|
193
236
|
if confirm:
|
|
194
237
|
copy(CONFIG_FILE_BACKUP, CONFIG_FILE)
|
|
195
|
-
|
|
196
|
-
self.
|
|
238
|
+
config.load_from_file()
|
|
239
|
+
self.editor_frame.reload_config_into_tree()
|
|
240
|
+
self.editor_frame.focus_set()
|
|
241
|
+
self.editor_frame.tree.focus_set()
|
|
242
|
+
|
|
243
|
+
def _add_custom_item(self):
|
|
244
|
+
"""Open a window to add a new custom item."""
|
|
245
|
+
custom_item_window = tk.Toplevel(self.editor_frame)
|
|
246
|
+
custom_item_window.title(ADD_CUSTOM_ITEM_TITLE)
|
|
247
|
+
custom_item_window.geometry(ADD_CUSTOM_ITEM_SIZE)
|
|
248
|
+
custom_item_window.focus_set()
|
|
249
|
+
|
|
250
|
+
custom_item_frame = CustomItemFrame(custom_item_window, self.editor_frame)
|
|
251
|
+
custom_item_frame.pack(expand=True, fill="both", padx=15, pady=15)
|
|
252
|
+
self.editor_frame.tree.focus_set()
|
|
253
|
+
|
|
254
|
+
def _import_steam_inventory(self):
|
|
255
|
+
"""Open a window to import the user's Steam inventory."""
|
|
256
|
+
steam_inventory_window = tk.Toplevel(self.editor_frame)
|
|
257
|
+
steam_inventory_window.title(IMPORT_INVENTORY_TITLE)
|
|
258
|
+
steam_inventory_window.geometry(IMPORT_INVENTORY_SIZE)
|
|
259
|
+
steam_inventory_window.focus_set()
|
|
260
|
+
|
|
261
|
+
steam_inventory_frame = InventoryImportFrame(steam_inventory_window, self.editor_frame)
|
|
262
|
+
steam_inventory_frame.pack(expand=True, fill="both", padx=15, pady=15)
|
|
263
|
+
self.editor_frame.tree.focus_set()
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
class CustomItemFrame(ttk.Frame):
|
|
267
|
+
def __init__(self, parent, editor_frame):
|
|
268
|
+
"""Initialize the custom item frame that allows users to add custom items."""
|
|
269
|
+
super().__init__(parent, style="Card.TFrame", padding=15)
|
|
270
|
+
self.parent = parent
|
|
271
|
+
self.editor_frame = editor_frame
|
|
272
|
+
self._add_widgets()
|
|
273
|
+
|
|
274
|
+
def _add_widgets(self):
|
|
275
|
+
"""Add widgets to the custom item frame for entering item details."""
|
|
276
|
+
ttk.Label(self, text="Item URL:").pack(pady=5)
|
|
277
|
+
item_url_entry = ttk.Entry(self)
|
|
278
|
+
item_url_entry.pack(fill="x", padx=10)
|
|
279
|
+
|
|
280
|
+
ttk.Label(self, text="Owned Count:").pack(pady=5)
|
|
281
|
+
item_owned_entry = ttk.Entry(self)
|
|
282
|
+
item_owned_entry.pack(fill="x", padx=10)
|
|
197
283
|
|
|
198
|
-
|
|
284
|
+
add_button = ttk.Button(
|
|
285
|
+
self,
|
|
286
|
+
text="Add",
|
|
287
|
+
command=lambda: self._add_custom_item(item_url_entry.get(), item_owned_entry.get()),
|
|
288
|
+
)
|
|
289
|
+
add_button.pack(pady=10)
|
|
290
|
+
self.parent.bind("<Return>", lambda _: add_button.invoke())
|
|
291
|
+
|
|
292
|
+
def _add_custom_item(self, item_href, item_owned):
|
|
199
293
|
"""Add a custom item to the configuration."""
|
|
200
|
-
if not
|
|
201
|
-
messagebox.showerror("Input Error", "All fields must be filled out.")
|
|
294
|
+
if not item_href or not item_owned:
|
|
295
|
+
messagebox.showerror("Input Error", "All fields must be filled out.", parent=self)
|
|
296
|
+
self.editor_frame.focus_set()
|
|
297
|
+
self.parent.focus_set()
|
|
202
298
|
return
|
|
203
299
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
300
|
+
item_name = config.option_to_name(item_href, href=True)
|
|
301
|
+
|
|
302
|
+
# Make sure not to reinsert custom items that have already been added
|
|
303
|
+
for option in self.editor_frame.tree.get_children("Custom Items"):
|
|
304
|
+
option_name = self.editor_frame.tree.item(option, "text")
|
|
305
|
+
if option_name == item_name:
|
|
306
|
+
self.editor_frame.tree.set(option, column="#1", value=item_owned)
|
|
307
|
+
self.editor_frame.focus_set()
|
|
308
|
+
self.editor_frame.save_config()
|
|
309
|
+
self.parent.destroy()
|
|
310
|
+
return
|
|
311
|
+
|
|
312
|
+
self.editor_frame.tree.insert(
|
|
313
|
+
"Custom Items",
|
|
314
|
+
"end",
|
|
315
|
+
text=item_name,
|
|
316
|
+
values=[item_owned],
|
|
317
|
+
)
|
|
318
|
+
self.editor_frame.focus_set()
|
|
319
|
+
self.editor_frame.save_config()
|
|
320
|
+
self.parent.destroy()
|
|
210
321
|
|
|
211
|
-
self.scraper.config.set("Custom Items", item_url, item_owned)
|
|
212
|
-
self.scraper.config.write_to_file()
|
|
213
|
-
if self.scraper.config.valid:
|
|
214
|
-
self.tree.insert("Custom Items", "end", text=item_url, values=(item_owned,))
|
|
215
|
-
if self.custom_item_dialog:
|
|
216
|
-
self.custom_item_dialog.destroy()
|
|
217
|
-
self.custom_item_dialog = None
|
|
218
|
-
else:
|
|
219
|
-
self.scraper.config.remove_option("Custom Items", item_url)
|
|
220
|
-
messagebox.showerror(
|
|
221
|
-
"Config Error",
|
|
222
|
-
f"The configuration is invalid. ({self.scraper.config.last_error})",
|
|
223
|
-
)
|
|
224
322
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
323
|
+
class InventoryImportFrame(ttk.Frame):
|
|
324
|
+
# pylint: disable=too-many-instance-attributes
|
|
325
|
+
def __init__(self, parent, editor_frame):
|
|
326
|
+
"""Initialize the inventory import frame that allows users to import their Steam
|
|
327
|
+
inventory.
|
|
328
|
+
"""
|
|
329
|
+
super().__init__(parent, style="Card.TFrame", padding=10)
|
|
330
|
+
self.parent = parent
|
|
331
|
+
self.editor_frame = editor_frame
|
|
332
|
+
self._add_widgets()
|
|
230
333
|
|
|
231
|
-
|
|
232
|
-
|
|
334
|
+
def _add_widgets(self):
|
|
335
|
+
"""Add widgets to the inventory import frame."""
|
|
336
|
+
self._configure_checkboxes()
|
|
337
|
+
self.storage_units_checkbox.pack(anchor="w", padx=20, pady=(15, 5))
|
|
338
|
+
self.regular_inventory_checkbox.pack(anchor="w", padx=20, pady=5)
|
|
339
|
+
|
|
340
|
+
self.import_cases_checkbox.pack(anchor="w", padx=20, pady=5)
|
|
341
|
+
self.import_sticker_capsules_checkbox.pack(anchor="w", padx=20, pady=5)
|
|
342
|
+
self.import_stickers_checkbox.pack(anchor="w", padx=20, pady=5)
|
|
343
|
+
self.import_others_checkbox.pack(anchor="w", padx=20, pady=5)
|
|
344
|
+
|
|
345
|
+
self._configure_entries()
|
|
346
|
+
self.user_name_label.pack(pady=(20, 10))
|
|
347
|
+
self.user_name_entry.pack(fill="x", padx=50)
|
|
348
|
+
self.password_label.pack(pady=10)
|
|
349
|
+
self.password_entry.pack(fill="x", padx=50)
|
|
350
|
+
self.two_factor_label.pack(pady=10)
|
|
351
|
+
self.two_factor_entry.pack(fill="x", padx=50)
|
|
352
|
+
|
|
353
|
+
self.import_button = ttk.Button(
|
|
354
|
+
self, text="Import", command=self._import_inventory, state="disabled"
|
|
355
|
+
)
|
|
356
|
+
self.import_button.pack(pady=10)
|
|
357
|
+
self.parent.bind("<Return>", lambda _: self.import_button.invoke())
|
|
358
|
+
|
|
359
|
+
def form_complete(_):
|
|
360
|
+
if (
|
|
361
|
+
len(self.user_name_entry.get().strip()) > 0
|
|
362
|
+
and len(self.password_entry.get().strip()) > 0
|
|
363
|
+
and len(self.two_factor_entry.get().strip()) > 0
|
|
364
|
+
):
|
|
365
|
+
self.import_button.configure(state="normal")
|
|
366
|
+
else:
|
|
367
|
+
self.import_button.configure(state="disabled")
|
|
368
|
+
|
|
369
|
+
self.parent.bind("<KeyRelease>", form_complete)
|
|
370
|
+
|
|
371
|
+
def _configure_checkboxes(self):
|
|
372
|
+
# pylint: disable=attribute-defined-outside-init
|
|
373
|
+
"""Configure the checkboxes for selecting what to import from the Steam
|
|
374
|
+
inventory.
|
|
375
|
+
"""
|
|
376
|
+
self.regular_inventory_value = tk.BooleanVar(value=False)
|
|
377
|
+
self.regular_inventory_checkbox = ttk.Checkbutton(
|
|
378
|
+
self,
|
|
379
|
+
text="Regular Inventory",
|
|
380
|
+
variable=self.regular_inventory_value,
|
|
381
|
+
style="Switch.TCheckbutton",
|
|
382
|
+
)
|
|
233
383
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
384
|
+
self.storage_units_value = tk.BooleanVar(value=True)
|
|
385
|
+
self.storage_units_checkbox = ttk.Checkbutton(
|
|
386
|
+
self,
|
|
387
|
+
text="Storage Units",
|
|
388
|
+
variable=self.storage_units_value,
|
|
389
|
+
style="Switch.TCheckbutton",
|
|
390
|
+
)
|
|
237
391
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
392
|
+
self.import_cases_value = tk.BooleanVar(value=True)
|
|
393
|
+
self.import_cases_checkbox = ttk.Checkbutton(
|
|
394
|
+
self, text="Import Cases", variable=self.import_cases_value, style="Switch.TCheckbutton"
|
|
395
|
+
)
|
|
241
396
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
397
|
+
self.import_sticker_capsules_value = tk.BooleanVar(value=True)
|
|
398
|
+
self.import_sticker_capsules_checkbox = ttk.Checkbutton(
|
|
399
|
+
self,
|
|
400
|
+
text="Import Sticker Capsules",
|
|
401
|
+
variable=self.import_sticker_capsules_value,
|
|
402
|
+
style="Switch.TCheckbutton",
|
|
246
403
|
)
|
|
247
|
-
|
|
404
|
+
|
|
405
|
+
self.import_stickers_value = tk.BooleanVar(value=False)
|
|
406
|
+
self.import_stickers_checkbox = ttk.Checkbutton(
|
|
407
|
+
self,
|
|
408
|
+
text="Import Stickers",
|
|
409
|
+
variable=self.import_stickers_value,
|
|
410
|
+
style="Switch.TCheckbutton",
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
self.import_others_value = tk.BooleanVar(value=False)
|
|
414
|
+
self.import_others_checkbox = ttk.Checkbutton(
|
|
415
|
+
self,
|
|
416
|
+
text="Import Other Items",
|
|
417
|
+
variable=self.import_others_value,
|
|
418
|
+
style="Switch.TCheckbutton",
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
def _configure_entries(self):
|
|
422
|
+
# pylint: disable=attribute-defined-outside-init
|
|
423
|
+
"""Configure the entry fields for Steam username, password, and two-factor
|
|
424
|
+
code.
|
|
425
|
+
"""
|
|
426
|
+
self.user_name_label = ttk.Label(self, text="Steam Username:")
|
|
427
|
+
self.user_name_entry = ttk.Entry(self, justify="center", font=("Helvetica", 11))
|
|
428
|
+
|
|
429
|
+
self.password_label = ttk.Label(self, text="Steam Password:")
|
|
430
|
+
self.password_entry = ttk.Entry(self, show="*", justify="center", font=("Helvetica", 11))
|
|
431
|
+
|
|
432
|
+
self.two_factor_label = ttk.Label(self, text="Steam Guard Code:")
|
|
433
|
+
self.two_factor_entry = ttk.Entry(self, justify="center", font=("Helvetica", 11))
|
|
434
|
+
|
|
435
|
+
def _import_inventory(self):
|
|
436
|
+
"""
|
|
437
|
+
Call the node.js script to import the user's Steam inventory.
|
|
438
|
+
|
|
439
|
+
This will also install the necessary npm packages if they are not already
|
|
440
|
+
installed.
|
|
441
|
+
"""
|
|
442
|
+
regular_inventory = self.regular_inventory_value.get()
|
|
443
|
+
storage_units = self.storage_units_value.get()
|
|
444
|
+
|
|
445
|
+
import_cases = self.import_cases_value.get()
|
|
446
|
+
import_sticker_capsules = self.import_sticker_capsules_value.get()
|
|
447
|
+
import_stickers = self.import_stickers_value.get()
|
|
448
|
+
import_others = self.import_others_value.get()
|
|
449
|
+
|
|
450
|
+
username = self.user_name_entry.get().strip()
|
|
451
|
+
password = self.password_entry.get().strip()
|
|
452
|
+
two_factor_code = self.two_factor_entry.get().strip()
|
|
453
|
+
|
|
454
|
+
self._display_node_subprocess(
|
|
455
|
+
[
|
|
456
|
+
INVENTORY_IMPORT_SCRIPT,
|
|
457
|
+
INVENTORY_IMPORT_FILE,
|
|
458
|
+
str(regular_inventory),
|
|
459
|
+
str(storage_units),
|
|
460
|
+
str(import_cases),
|
|
461
|
+
str(import_sticker_capsules),
|
|
462
|
+
str(import_stickers),
|
|
463
|
+
str(import_others),
|
|
464
|
+
username,
|
|
465
|
+
password,
|
|
466
|
+
two_factor_code,
|
|
467
|
+
]
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
self.parent.destroy()
|
|
471
|
+
|
|
472
|
+
def _display_node_subprocess(self, node_cmd):
|
|
473
|
+
text_window = tk.Toplevel(self.editor_frame)
|
|
474
|
+
text_window.title(IMPORT_INVENTORY_PROCESS_TITLE)
|
|
475
|
+
text_window.geometry(IMPORT_INVENTORY_PROCESS_SIZE)
|
|
476
|
+
text_window.focus_set()
|
|
477
|
+
|
|
478
|
+
process_frame = InventoryImportProcessFrame(text_window, self.editor_frame)
|
|
479
|
+
process_frame.pack(expand=True, fill="both", padx=15, pady=15)
|
|
480
|
+
process_frame.console.focus_set()
|
|
481
|
+
process_frame.start(node_cmd)
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
class InventoryImportProcessFrame(ttk.Frame):
|
|
485
|
+
# pylint: disable=attribute-defined-outside-init
|
|
486
|
+
# Source: https://stackoverflow.com/questions/27327886/issues-intercepting-subprocess-output-in-real-time
|
|
487
|
+
def __init__(self, parent, editor_frame):
|
|
488
|
+
"""Initialize the frame that displays the output of the subprocess."""
|
|
489
|
+
super().__init__(parent)
|
|
490
|
+
self.parent = parent
|
|
491
|
+
self.editor_frame = editor_frame
|
|
492
|
+
self._add_widgets()
|
|
493
|
+
|
|
494
|
+
def _add_widgets(self):
|
|
495
|
+
"""Add a text widget to display the output of the subprocess."""
|
|
496
|
+
self.scrollbar = ttk.Scrollbar(self)
|
|
497
|
+
self.scrollbar.pack(side="right", fill="y", padx=(5, 0))
|
|
498
|
+
|
|
499
|
+
self.console = ThemedText(self, wrap="word", yscrollcommand=self.scrollbar.set)
|
|
500
|
+
self.console.config(state="disabled")
|
|
501
|
+
self.console.pack(expand=True, fill="both", padx=10, pady=10)
|
|
502
|
+
|
|
503
|
+
self.scrollbar.config(command=self.console.yview)
|
|
504
|
+
|
|
505
|
+
def _read_lines(self, process, queue):
|
|
506
|
+
"""Read lines from the subprocess output and put them in a queue."""
|
|
507
|
+
while process.poll() is None:
|
|
508
|
+
line = process.stdout.readline()
|
|
509
|
+
if line:
|
|
510
|
+
queue.put(line)
|
|
511
|
+
|
|
512
|
+
def start(self, cmd):
|
|
513
|
+
"""Start the NodeJS subprocess with the given command and read its output."""
|
|
514
|
+
self.process = node.Popen(
|
|
515
|
+
cmd,
|
|
516
|
+
stdout=PIPE,
|
|
517
|
+
stdin=PIPE,
|
|
518
|
+
stderr=STDOUT,
|
|
519
|
+
text=True,
|
|
520
|
+
encoding="utf-8",
|
|
521
|
+
shell=True,
|
|
522
|
+
cwd=DATA_DIR,
|
|
523
|
+
)
|
|
524
|
+
self.queue = Queue()
|
|
525
|
+
self.thread = Thread(target=self._read_lines, args=(self.process, self.queue), daemon=True)
|
|
526
|
+
self.thread.start()
|
|
527
|
+
self._update_lines()
|
|
528
|
+
|
|
529
|
+
def _update_lines(self):
|
|
530
|
+
"""Update the text widget with lines from the subprocess output."""
|
|
531
|
+
try:
|
|
532
|
+
line = self.queue.get(block=False)
|
|
533
|
+
self.console.config(state="normal")
|
|
534
|
+
self.console.insert("end", line)
|
|
535
|
+
self.console.config(state="disabled")
|
|
536
|
+
self.console.yview("end")
|
|
537
|
+
except Empty:
|
|
538
|
+
pass
|
|
539
|
+
|
|
540
|
+
if self.process.poll() is None or not self.queue.empty():
|
|
541
|
+
self.after(50, self._update_lines)
|
|
542
|
+
else:
|
|
543
|
+
self._cleanup()
|
|
544
|
+
|
|
545
|
+
def _cleanup(self):
|
|
546
|
+
"""Clean up the process and thread after completion and trigger a config update
|
|
547
|
+
from the newly written inventory file.
|
|
548
|
+
"""
|
|
549
|
+
self.process.wait()
|
|
550
|
+
self.thread.join()
|
|
551
|
+
|
|
552
|
+
config.read_from_inventory_file()
|
|
553
|
+
self.editor_frame.reload_config_into_tree()
|
|
554
|
+
self.editor_frame.tree.focus_set()
|
|
555
|
+
self.parent.destroy()
|