cs2tracker 2.1.11__py3-none-any.whl → 2.1.13__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 CHANGED
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '2.1.11'
21
- __version_tuple__ = version_tuple = (2, 1, 11)
20
+ __version__ = version = '2.1.13'
21
+ __version_tuple__ = version_tuple = (2, 1, 13)
@@ -15,6 +15,7 @@ from cs2tracker.app.scraper_frame import ScraperFrame
15
15
  from cs2tracker.constants import ICON_FILE, OS, OUTPUT_FILE, OSType
16
16
  from cs2tracker.scraper import BackgroundTask, Scraper
17
17
  from cs2tracker.util import PriceLogs
18
+ from cs2tracker.util.validated_config import get_config
18
19
 
19
20
  APPLICATION_NAME = "CS2Tracker"
20
21
  WINDOW_SIZE = "630x335"
@@ -24,7 +25,10 @@ SCRAPER_WINDOW_TITLE = "CS2Tracker Scraper"
24
25
  SCRAPER_WINDOW_SIZE = "900x750"
25
26
 
26
27
  CONFIG_EDITOR_TITLE = "Config Editor"
27
- CONFIG_EDITOR_SIZE = "800x750"
28
+ CONFIG_EDITOR_SIZE = "900x750"
29
+
30
+
31
+ config = get_config()
28
32
 
29
33
 
30
34
  class Application:
@@ -92,9 +96,7 @@ class Application:
92
96
  )
93
97
 
94
98
  discord_webhook_checkbox_value = tk.BooleanVar(
95
- value=self.scraper.config.getboolean(
96
- "App Settings", "discord_notifications", fallback=False
97
- )
99
+ value=config.getboolean("App Settings", "discord_notifications", fallback=False)
98
100
  )
99
101
  self._add_checkbox(
100
102
  checkbox_frame,
@@ -107,7 +109,7 @@ class Application:
107
109
  )
108
110
 
109
111
  use_proxy_checkbox_value = tk.BooleanVar(
110
- value=self.scraper.config.getboolean("App Settings", "use_proxy", fallback=False)
112
+ value=config.getboolean("App Settings", "use_proxy", fallback=False)
111
113
  )
112
114
  self._add_checkbox(
113
115
  checkbox_frame,
@@ -181,7 +183,7 @@ class Application:
181
183
  config_editor_window.geometry(CONFIG_EDITOR_SIZE)
182
184
  config_editor_window.title(CONFIG_EDITOR_TITLE)
183
185
 
184
- editor_frame = ConfigEditorFrame(config_editor_window, self.scraper)
186
+ editor_frame = ConfigEditorFrame(config_editor_window)
185
187
  editor_frame.pack(expand=True, fill="both")
186
188
 
187
189
  def _draw_plot(self):
@@ -228,7 +230,7 @@ class Application:
228
230
 
229
231
  def _toggle_use_proxy(self, enabled: bool):
230
232
  """Toggle whether the scraper should use proxy servers for requests."""
231
- proxy_api_key = self.scraper.config.get("User Settings", "proxy_api_key", fallback=None)
233
+ proxy_api_key = config.get("User Settings", "proxy_api_key", fallback=None)
232
234
  if not proxy_api_key and enabled:
233
235
  messagebox.showerror(
234
236
  "Config Error",
@@ -236,14 +238,12 @@ class Application:
236
238
  )
237
239
  return False
238
240
 
239
- self.scraper.toggle_use_proxy(enabled)
241
+ config.toggle_use_proxy(enabled)
240
242
  return True
241
243
 
242
244
  def _toggle_discord_webhook(self, enabled: bool):
243
245
  """Toggle whether the scraper should send notifications to a Discord webhook."""
244
- discord_webhook_url = self.scraper.config.get(
245
- "User Settings", "discord_webhook_url", fallback=None
246
- )
246
+ discord_webhook_url = config.get("User Settings", "discord_webhook_url", fallback=None)
247
247
  if not discord_webhook_url and enabled:
248
248
  messagebox.showerror(
249
249
  "Config Error",
@@ -251,5 +251,5 @@ class Application:
251
251
  )
252
252
  return False
253
253
 
254
- self.scraper.toggle_discord_webhook(enabled)
254
+ config.toggle_discord_webhook(enabled)
255
255
  return True
@@ -1,42 +1,44 @@
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
7
+ from urllib.parse import unquote
4
8
 
5
- from cs2tracker.constants import CONFIG_FILE, CONFIG_FILE_BACKUP
9
+ from nodejs import node
10
+ from ttk_text import ThemedText
6
11
 
7
- # from tksheet import Sheet
12
+ from cs2tracker.constants import (
13
+ CONFIG_FILE,
14
+ CONFIG_FILE_BACKUP,
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 = "500x450"
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
23
40
  self._add_widgets()
24
41
 
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])
39
-
40
42
  def _add_widgets(self):
41
43
  """Configure the main editor frame which displays the configuration options in a
42
44
  structured way.
@@ -44,12 +46,14 @@ class ConfigEditorFrame(ttk.Frame):
44
46
  self._configure_treeview()
45
47
  self.tree.pack(expand=True, fill="both")
46
48
 
47
- button_frame = ConfigEditorButtonFrame(self, self.scraper, self.tree)
49
+ button_frame = ConfigEditorButtonFrame(self, self.tree)
48
50
  button_frame.pack(side="bottom", padx=10, pady=(0, 10))
49
51
 
50
52
  def _set_cell_value(self, event):
51
- """Set the value of a cell in the treeview to be editable when double-
52
- clicked.
53
+ """
54
+ Set the value of a cell in the treeview to be editable when double- clicked.
55
+
56
+ Source: https://stackoverflow.com/questions/75787251/create-an-editable-tkinter-treeview-with-keyword-connection
53
57
  """
54
58
 
55
59
  def save_edit(event):
@@ -63,7 +67,7 @@ class ConfigEditorFrame(ttk.Frame):
63
67
  if item_text.strip() == "":
64
68
  left_item_text = self.tree.item(row, "text")
65
69
  # Don't allow editing of section headers
66
- if any(left_item_text == section for section in self.scraper.config.sections()):
70
+ if any(left_item_text == section for section in config.sections()):
67
71
  return
68
72
  x, y, w, h = self.tree.bbox(row, column)
69
73
  entryedit = ttk.Entry(self)
@@ -89,11 +93,8 @@ class ConfigEditorFrame(ttk.Frame):
89
93
  event.widget.destroy()
90
94
 
91
95
  def _make_tree_editable(self):
92
- """
93
- Add a binding to the treeview that allows double-clicking on a cell to edit its
94
- value.
95
-
96
- Source: https://stackoverflow.com/questions/75787251/create-an-editable-tkinter-treeview-with-keyword-connection
96
+ """Add a binding to the treeview that allows double-clicking on a cell to edit
97
+ its value.
97
98
  """
98
99
  self.tree.bind("<Double-1>", self._set_cell_value)
99
100
  self.parent.bind("<MouseWheel>", self._destroy_entries) # type: ignore
@@ -101,13 +102,25 @@ class ConfigEditorFrame(ttk.Frame):
101
102
 
102
103
  def _load_config_into_tree(self):
103
104
  """Load the configuration options into the treeview for display and editing."""
104
- for section in self.scraper.config.sections():
105
+ for section in config.sections():
105
106
  if section == "App Settings":
106
107
  continue
107
108
  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])
109
+ for config_option, value in config.items(section):
110
+ if section == "Custom Items":
111
+ custom_item_name = unquote(config_option.split("/")[-1])
112
+ self.tree.insert(section_level, "end", text=custom_item_name, values=[value])
113
+ else:
114
+ option_name = config_option.replace("_", " ").title()
115
+ self.tree.insert(section_level, "end", text=option_name, values=[value])
116
+
117
+ def reload_config_into_tree(self):
118
+ """Reload the configuration options into the treeview for display and
119
+ editing.
120
+ """
121
+ for item in self.tree.get_children():
122
+ self.tree.delete(item)
123
+ self._load_config_into_tree()
111
124
 
112
125
  def _configure_treeview(self):
113
126
  """Configure a treeview to display and edit configuration options."""
@@ -124,7 +137,7 @@ class ConfigEditorFrame(ttk.Frame):
124
137
  scrollbar.config(command=self.tree.yview)
125
138
 
126
139
  self.tree.column("#0", anchor="w", width=200)
127
- self.tree.column(1, anchor="w", width=25)
140
+ self.tree.column(1, anchor="center", width=25)
128
141
  self.tree.heading("#0", text="Option")
129
142
  self.tree.heading(1, text="Value")
130
143
 
@@ -133,15 +146,13 @@ class ConfigEditorFrame(ttk.Frame):
133
146
 
134
147
 
135
148
  class ConfigEditorButtonFrame(ttk.Frame):
136
- def __init__(self, parent, scraper, tree):
149
+ def __init__(self, parent, tree):
137
150
  """Initialize the button frame that contains buttons for saving the updated
138
151
  configuration and adding custom items.
139
152
  """
140
-
141
153
  super().__init__(parent, padding=10)
142
154
 
143
155
  self.parent = parent
144
- self.scraper = scraper
145
156
  self.tree = tree
146
157
  self.custom_item_dialog = None
147
158
 
@@ -157,11 +168,14 @@ class ConfigEditorButtonFrame(ttk.Frame):
157
168
  reset_button = ttk.Button(self, text="Reset", command=self._reset_config)
158
169
  reset_button.pack(side="left", expand=True, padx=5)
159
170
 
160
- custom_item_button = ttk.Button(
161
- self, text="Add Custom Item", command=self._open_custom_item_dialog
162
- )
171
+ custom_item_button = ttk.Button(self, text="Add Custom Item", command=self._add_custom_item)
163
172
  custom_item_button.pack(side="left", expand=True, padx=5)
164
173
 
174
+ import_inventory_button = ttk.Button(
175
+ self, text="Import Steam Inventory", command=self._import_steam_inventory
176
+ )
177
+ import_inventory_button.pack(side="left", expand=True, padx=5)
178
+
165
179
  def _save_config(self):
166
180
  """Save the current configuration from the treeview to the config file."""
167
181
  for child in self.tree.get_children():
@@ -174,15 +188,17 @@ class ConfigEditorButtonFrame(ttk.Frame):
174
188
  if section_name == "Custom Items":
175
189
  # custom items are already saved upon creation (Saving them again would result in duplicates)
176
190
  continue
177
- self.scraper.config.set(section_name, config_option, value)
191
+ config.set(section_name, config_option, value)
178
192
 
179
- self.scraper.config.write_to_file()
180
- if self.scraper.config.valid:
193
+ config.write_to_file()
194
+ if config.valid:
181
195
  messagebox.showinfo("Config Saved", "The configuration has been saved successfully.")
182
196
  else:
197
+ config.load()
198
+ self.parent.reload_config_into_tree()
183
199
  messagebox.showerror(
184
200
  "Config Error",
185
- f"The configuration is invalid. ({self.scraper.config.last_error})",
201
+ f"The configuration is invalid. ({config.last_error})",
186
202
  )
187
203
 
188
204
  def _reset_config(self):
@@ -192,15 +208,59 @@ class ConfigEditorButtonFrame(ttk.Frame):
192
208
  )
193
209
  if confirm:
194
210
  copy(CONFIG_FILE_BACKUP, CONFIG_FILE)
195
- self.scraper.load_config()
211
+ config.load()
196
212
  self.parent.reload_config_into_tree()
197
213
 
214
+ def _add_custom_item(self):
215
+ """Open a window to add a new custom item."""
216
+ custom_item_window = tk.Toplevel(self.parent)
217
+ custom_item_window.title(ADD_CUSTOM_ITEM_TITLE)
218
+ custom_item_window.geometry(ADD_CUSTOM_ITEM_SIZE)
219
+
220
+ custom_item_frame = CustomItemFrame(custom_item_window, self.parent, self.tree)
221
+ custom_item_frame.pack(expand=True, fill="both", padx=15, pady=15)
222
+
223
+ def _import_steam_inventory(self):
224
+ """Open a window to import the user's Steam inventory."""
225
+ steam_inventory_window = tk.Toplevel(self.parent)
226
+ steam_inventory_window.title(IMPORT_INVENTORY_TITLE)
227
+ steam_inventory_window.geometry(IMPORT_INVENTORY_SIZE)
228
+
229
+ steam_inventory_frame = InventoryImportFrame(steam_inventory_window, self)
230
+ steam_inventory_frame.pack(expand=True, fill="both", padx=15, pady=15)
231
+
232
+
233
+ class CustomItemFrame(ttk.Frame):
234
+ def __init__(self, parent, grandparent, tree):
235
+ """Initialize the custom item frame that allows users to add custom items."""
236
+ super().__init__(parent, style="Card.TFrame", padding=15)
237
+ self.parent = parent
238
+ self.grandparent = grandparent
239
+ self.tree = tree
240
+ self._add_widgets()
241
+
242
+ def _add_widgets(self):
243
+ """Add widgets to the custom item frame for entering item details."""
244
+ ttk.Label(self, text="Item URL:").pack(pady=5)
245
+ item_url_entry = ttk.Entry(self)
246
+ item_url_entry.pack(fill="x", padx=10)
247
+
248
+ ttk.Label(self, text="Owned Count:").pack(pady=5)
249
+ item_owned_entry = ttk.Entry(self)
250
+ item_owned_entry.pack(fill="x", padx=10)
251
+
252
+ add_button = ttk.Button(
253
+ self,
254
+ text="Add",
255
+ command=lambda: self._add_custom_item(item_url_entry.get(), item_owned_entry.get()),
256
+ )
257
+ add_button.pack(pady=10)
258
+
198
259
  def _add_custom_item(self, item_url, item_owned):
199
260
  """Add a custom item to the configuration."""
200
261
  if not item_url or not item_owned:
201
262
  messagebox.showerror("Input Error", "All fields must be filled out.")
202
263
  return
203
-
204
264
  try:
205
265
  if int(item_owned) < 0:
206
266
  raise ValueError("Owned count must be a non-negative integer.")
@@ -208,40 +268,207 @@ class ConfigEditorButtonFrame(ttk.Frame):
208
268
  messagebox.showerror("Input Error", f"Invalid owned count: {error}")
209
269
  return
210
270
 
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
271
+ config.set("Custom Items", item_url, item_owned)
272
+ config.write_to_file()
273
+ if config.valid:
274
+ config.load()
275
+ self.grandparent.reload_config_into_tree()
276
+ self.parent.destroy()
218
277
  else:
219
- self.scraper.config.remove_option("Custom Items", item_url)
278
+ config.remove_option("Custom Items", item_url)
220
279
  messagebox.showerror(
221
280
  "Config Error",
222
- f"The configuration is invalid. ({self.scraper.config.last_error})",
281
+ f"The configuration is invalid. ({config.last_error})",
223
282
  )
224
283
 
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)
230
284
 
231
- dialog_frame = ttk.Frame(self.custom_item_dialog, padding=10)
232
- dialog_frame.pack(expand=True, fill="both")
285
+ class InventoryImportFrame(ttk.Frame):
286
+ # pylint: disable=too-many-instance-attributes
287
+ def __init__(self, parent, grandparent):
288
+ """Initialize the inventory import frame that allows users to import their Steam
289
+ inventory.
290
+ """
291
+ super().__init__(parent, style="Card.TFrame", padding=10)
292
+ self.parent = parent
293
+ self.grandparent = grandparent
294
+ self._add_widgets()
233
295
 
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)
296
+ def _add_widgets(self):
297
+ """Add widgets to the inventory import frame."""
298
+ self._configure_checkboxes()
299
+ self.import_cases_checkbox.pack(anchor="w", padx=10, pady=5)
300
+ self.import_sticker_capsules_checkbox.pack(anchor="w", padx=10, pady=5)
301
+ self.import_stickers_checkbox.pack(anchor="w", padx=10, pady=5)
302
+ 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)
314
+
315
+ def _configure_checkboxes(self):
316
+ # pylint: disable=attribute-defined-outside-init
317
+ """Configure the checkboxes for selecting what to import from the Steam
318
+ inventory.
319
+ """
320
+ self.import_cases_value = tk.BooleanVar(value=True)
321
+ self.import_cases_checkbox = ttk.Checkbutton(
322
+ self, text="Import Cases", variable=self.import_cases_value, style="Switch.TCheckbutton"
323
+ )
237
324
 
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)
325
+ self.import_sticker_capsules_value = tk.BooleanVar(value=True)
326
+ self.import_sticker_capsules_checkbox = ttk.Checkbutton(
327
+ self,
328
+ text="Import Sticker Capsules",
329
+ variable=self.import_sticker_capsules_value,
330
+ style="Switch.TCheckbutton",
331
+ )
241
332
 
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()),
333
+ self.import_stickers_value = tk.BooleanVar(value=True)
334
+ self.import_stickers_checkbox = ttk.Checkbutton(
335
+ self,
336
+ text="Import Stickers",
337
+ variable=self.import_stickers_value,
338
+ style="Switch.TCheckbutton",
246
339
  )
247
- add_button.pack(pady=10)
340
+
341
+ self.import_others_value = tk.BooleanVar(value=True)
342
+ self.import_others_checkbox = ttk.Checkbutton(
343
+ self,
344
+ text="Import Other Items",
345
+ variable=self.import_others_value,
346
+ style="Switch.TCheckbutton",
347
+ )
348
+
349
+ def _configure_entries(self):
350
+ # pylint: disable=attribute-defined-outside-init
351
+ """Configure the entry fields for Steam username, password, and two-factor
352
+ code.
353
+ """
354
+ self.user_name_label = ttk.Label(self, text="Steam Username:")
355
+ self.user_name_entry = ttk.Entry(self)
356
+
357
+ self.password_label = ttk.Label(self, text="Steam Password:")
358
+ self.password_entry = ttk.Entry(self, show="*")
359
+
360
+ self.two_factor_label = ttk.Label(self, text="Steam Guard Code (if enabled):")
361
+ self.two_factor_entry = ttk.Entry(self)
362
+
363
+ def _import_inventory(self):
364
+ """
365
+ Call the node.js script to import the user's Steam inventory.
366
+
367
+ This will also install the necessary npm packages if they are not already
368
+ installed.
369
+ """
370
+ import_cases = self.import_cases_value.get()
371
+ import_sticker_capsules = self.import_sticker_capsules_value.get()
372
+ import_stickers = self.import_stickers_value.get()
373
+ import_others = self.import_others_value.get()
374
+
375
+ username = self.user_name_entry.get().strip()
376
+ password = self.password_entry.get().strip()
377
+ two_factor_code = self.two_factor_entry.get().strip()
378
+
379
+ self._display_node_subprocess(
380
+ [
381
+ INVENTORY_IMPORT_SCRIPT,
382
+ INVENTORY_IMPORT_FILE,
383
+ str(import_cases),
384
+ str(import_sticker_capsules),
385
+ str(import_stickers),
386
+ str(import_others),
387
+ username,
388
+ password,
389
+ two_factor_code,
390
+ ]
391
+ )
392
+
393
+ self.parent.destroy()
394
+
395
+ def _display_node_subprocess(self, node_cmd):
396
+ text_window = tk.Toplevel(self.grandparent)
397
+ text_window.title(IMPORT_INVENTORY_PROCESS_TITLE)
398
+ text_window.geometry(IMPORT_INVENTORY_PROCESS_SIZE)
399
+
400
+ process_frame = InventoryImportProcessFrame(text_window)
401
+ process_frame.pack(expand=True, fill="both", padx=15, pady=15)
402
+ process_frame.start(node_cmd)
403
+ process_frame.console.focus_set()
404
+
405
+
406
+ class InventoryImportProcessFrame(ttk.Frame):
407
+ # pylint: disable=attribute-defined-outside-init
408
+ # Source: https://stackoverflow.com/questions/27327886/issues-intercepting-subprocess-output-in-real-time
409
+ def __init__(self, parent):
410
+ """Initialize the frame that displays the output of the subprocess."""
411
+ super().__init__(parent)
412
+ self.parent = parent
413
+ self._add_widgets()
414
+
415
+ def _add_widgets(self):
416
+ """Add a text widget to display the output of the subprocess."""
417
+ self.scrollbar = ttk.Scrollbar(self)
418
+ self.scrollbar.pack(side="right", fill="y", padx=(5, 0))
419
+
420
+ self.console = ThemedText(self, wrap="word", yscrollcommand=self.scrollbar.set)
421
+ self.console.config(state="disabled")
422
+ self.console.pack(expand=True, fill="both", padx=10, pady=10)
423
+
424
+ self.scrollbar.config(command=self.console.yview)
425
+
426
+ def _read_lines(self, process, queue):
427
+ """Read lines from the subprocess output and put them in a queue."""
428
+ while process.poll() is None:
429
+ line = process.stdout.readline()
430
+ if line:
431
+ queue.put(line)
432
+
433
+ def start(self, cmd):
434
+ """Start the NodeJS subprocess with the given command and read its output."""
435
+ self.process = node.Popen(
436
+ cmd,
437
+ stdout=PIPE,
438
+ stdin=PIPE,
439
+ stderr=STDOUT,
440
+ text=True,
441
+ bufsize=1,
442
+ encoding="utf-8",
443
+ )
444
+ self.queue = Queue()
445
+ self.thread = Thread(target=self._read_lines, args=(self.process, self.queue), daemon=True)
446
+ self.thread.start()
447
+
448
+ self.after(100, self._update_lines)
449
+
450
+ def _update_lines(self):
451
+ """Update the text widget with lines from the subprocess output."""
452
+ try:
453
+ line = self.queue.get(block=False)
454
+ self.console.config(state="normal")
455
+ self.console.insert("end", line)
456
+ self.console.config(state="disabled")
457
+ self.console.yview("end")
458
+ except Empty:
459
+ pass
460
+
461
+ if self.process.poll() is None or not self.queue.empty():
462
+ self.after(100, self._update_lines)
463
+ else:
464
+ self._cleanup()
465
+
466
+ def _cleanup(self):
467
+ """Clean up the process and thread after completion and trigger a config update
468
+ from the newly written inventory file.
469
+ """
470
+ config.read_from_inventory_file()
471
+ self.parent.master.master.reload_config_into_tree()
472
+
473
+ self.process.wait()
474
+ self.thread.join()
@@ -1,5 +1,5 @@
1
1
  import csv
2
- from tkinter import ttk
2
+ from tkinter import messagebox, ttk
3
3
  from tkinter.filedialog import asksaveasfilename
4
4
 
5
5
  from tksheet import Sheet
@@ -23,12 +23,23 @@ class ScraperFrame(ttk.Frame):
23
23
  self._configure_sheet()
24
24
  self.sheet.pack()
25
25
 
26
+ def _readjust_sheet_size_with_window_size(self, event):
27
+ """Ensures that the sheet resizes with the window."""
28
+ if event.widget == self.parent:
29
+ width, height = event.width, event.height
30
+ if width != self.sheet_width or height != self.sheet_height:
31
+ self.sheet_width = width
32
+ self.sheet_height = height
33
+ self.sheet.height_and_width(height, width)
34
+ self.parent.update()
35
+ self.parent.update_idletasks()
36
+
26
37
  def _configure_sheet(self):
27
38
  """Configure the sheet widget with initial data and settings."""
28
39
  self.sheet = Sheet( # pylint: disable=attribute-defined-outside-init
29
40
  self,
30
41
  data=[],
31
- theme="light" if self.dark_theme else "dark",
42
+ theme="light" if self.dark_theme else "dark", # This is on purpose to add contrast
32
43
  height=self.sheet_height,
33
44
  width=self.sheet_width,
34
45
  auto_resize_columns=150,
@@ -44,6 +55,8 @@ class ScraperFrame(ttk.Frame):
44
55
  self.sheet.align_columns([1, 2, 3], "c")
45
56
  self.sheet.popup_menu_add_command("Save Sheet", self._save_sheet)
46
57
 
58
+ self.parent.bind("<Configure>", self._readjust_sheet_size_with_window_size)
59
+
47
60
  def _save_sheet(self):
48
61
  """Export the current sheet data to a CSV file."""
49
62
  filepath = asksaveasfilename(
@@ -74,3 +87,7 @@ class ScraperFrame(ttk.Frame):
74
87
  row_heights = self.sheet.get_row_heights()
75
88
  last_row_index = len(row_heights) - 1
76
89
  self.sheet.align_rows(last_row_index, "c")
90
+
91
+ if self.scraper.error_stack:
92
+ last_error = self.scraper.error_stack[-1]
93
+ messagebox.showerror("An Error Occurred", f"{last_error.message}")