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.

@@ -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 cs2tracker.constants import CONFIG_FILE, CONFIG_FILE_BACKUP
8
+ from nodejs import node
9
+ from ttk_text import ThemedText
6
10
 
7
- # from tksheet import Sheet
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
- NEW_CUSTOM_ITEM_TITLE = "Add Custom Item"
10
- NEW_CUSTOM_ITEM_SIZE = "500x200"
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, scraper):
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.scraper = scraper
40
+ self.edit_entry = None
23
41
  self._add_widgets()
24
42
 
25
- def reload_config_into_tree(self):
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, self.scraper, self.tree)
52
+ button_frame = ConfigEditorButtonFrame(self)
48
53
  button_frame.pack(side="bottom", padx=10, pady=(0, 10))
49
54
 
50
- def _set_cell_value(self, event):
51
- """Set the value of a cell in the treeview to be editable when double-
52
- clicked.
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
- def save_edit(event):
56
- self.tree.set(row, column=column, value=event.widget.get())
57
- event.widget.destroy()
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 = self.tree.identify_row(event.y)
61
- column = self.tree.identify_column(event.x)
62
- item_text = self.tree.set(row, column)
63
- if item_text.strip() == "":
64
- left_item_text = self.tree.item(row, "text")
65
- # Don't allow editing of section headers
66
- if any(left_item_text == section for section in self.scraper.config.sections()):
67
- return
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
- entryedit = ttk.Entry(self)
70
- entryedit.place(x=x, y=y, width=w, height=h + 3) # type: ignore
71
- entryedit.insert("end", item_text)
72
- entryedit.bind("<Return>", save_edit)
73
- entryedit.focus_set()
74
- entryedit.grab_set()
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
- pass
109
+ return
77
110
 
78
- def _destroy_entries(self, _):
79
- """Destroy any entry widgets in the treeview on an event, such as a mouse wheel
80
- movement.
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
- for widget in self.winfo_children():
83
- if isinstance(widget, ttk.Entry):
84
- widget.destroy()
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 _destroy_entry(self, event):
87
- """Destroy the entry widget on an even targeting it."""
88
- if isinstance(event.widget, ttk.Entry):
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
- def _make_tree_editable(self):
125
+ This only works for custom items, as other sections are not editable.
92
126
  """
93
- Add a binding to the treeview that allows double-clicking on a cell to edit its
94
- value.
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
- Source: https://stackoverflow.com/questions/75787251/create-an-editable-tkinter-treeview-with-keyword-connection
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.parent.bind("<MouseWheel>", self._destroy_entries) # type: ignore
100
- self.parent.bind("<Button-1>", self._destroy_entry) # type: ignore
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 self.scraper.config.sections():
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 self.scraper.config.items(section):
109
- title_option = config_option.replace("_", " ").title()
110
- self.tree.insert(section_level, "end", text=title_option, values=[value])
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="w", width=25)
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, parent, scraper, tree):
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
- super().__init__(parent, padding=10)
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
- def _save_config(self):
166
- """Save the current configuration from the treeview to the config file."""
167
- for child in self.tree.get_children():
168
- for item in self.tree.get_children(child):
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
- self.scraper.load_config()
196
- self.parent.reload_config_into_tree()
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
- def _add_custom_item(self, item_url, item_owned):
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 item_url or not item_owned:
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
- try:
205
- if int(item_owned) < 0:
206
- raise ValueError("Owned count must be a non-negative integer.")
207
- except ValueError as error:
208
- messagebox.showerror("Input Error", f"Invalid owned count: {error}")
209
- return
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
- def _open_custom_item_dialog(self):
226
- """Open a dialog to enter custom item details."""
227
- self.custom_item_dialog = tk.Toplevel(self.parent)
228
- self.custom_item_dialog.title(NEW_CUSTOM_ITEM_TITLE)
229
- self.custom_item_dialog.geometry(NEW_CUSTOM_ITEM_SIZE)
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
- dialog_frame = ttk.Frame(self.custom_item_dialog, padding=10)
232
- dialog_frame.pack(expand=True, fill="both")
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
- ttk.Label(dialog_frame, text="Item URL:").pack(pady=5)
235
- item_url_entry = ttk.Entry(dialog_frame)
236
- item_url_entry.pack(fill="x", padx=10)
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
- ttk.Label(dialog_frame, text="Owned Count:").pack(pady=5)
239
- item_owned_entry = ttk.Entry(dialog_frame)
240
- item_owned_entry.pack(fill="x", padx=10)
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
- add_button = ttk.Button(
243
- dialog_frame,
244
- text="Add",
245
- command=lambda: self._add_custom_item(item_url_entry.get(), item_owned_entry.get()),
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
- add_button.pack(pady=10)
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()