cs2tracker 2.1.14__py3-none-any.whl → 2.1.16__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.

@@ -8,6 +8,7 @@ from tkinter import messagebox, ttk
8
8
  from nodejs import node
9
9
  from ttk_text import ThemedText
10
10
 
11
+ from cs2tracker.config import get_config
11
12
  from cs2tracker.constants import (
12
13
  CONFIG_FILE,
13
14
  CONFIG_FILE_BACKUP,
@@ -15,13 +16,13 @@ from cs2tracker.constants import (
15
16
  INVENTORY_IMPORT_FILE,
16
17
  INVENTORY_IMPORT_SCRIPT,
17
18
  )
18
- from cs2tracker.util import get_config
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 = "500x220"
22
+ ADD_CUSTOM_ITEM_SIZE = "500x230"
22
23
 
23
24
  IMPORT_INVENTORY_TITLE = "Import Steam Inventory"
24
- IMPORT_INVENTORY_SIZE = "600x550"
25
+ IMPORT_INVENTORY_SIZE = "700x350"
25
26
 
26
27
  IMPORT_INVENTORY_PROCESS_TITLE = "Importing Steam Inventory..."
27
28
  IMPORT_INVENTORY_PROCESS_SIZE = "700x500"
@@ -30,13 +31,13 @@ config = get_config()
30
31
 
31
32
 
32
33
  class ConfigEditorFrame(ttk.Frame):
33
- def __init__(self, parent):
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__(parent, padding=15)
38
+ super().__init__(window, padding=15)
38
39
 
39
- self.parent = parent
40
+ self.window = window
40
41
  self.edit_entry = None
41
42
  self._add_widgets()
42
43
 
@@ -71,9 +72,11 @@ class ConfigEditorFrame(ttk.Frame):
71
72
  config.load_from_file()
72
73
  self.reload_config_into_tree()
73
74
  messagebox.showerror(
74
- "Config Error", f"The configuration is invalid. ({config.last_error})", parent=self
75
+ "Config Error",
76
+ f"The configuration is invalid. ({config.last_error})",
77
+ parent=self.window,
75
78
  )
76
- self.parent.focus_set()
79
+ self.window.focus_set()
77
80
  self.tree.focus_set()
78
81
 
79
82
  def _save_edit(self, event, row, column):
@@ -126,13 +129,18 @@ class ConfigEditorFrame(ttk.Frame):
126
129
  """
127
130
  selected = self.tree.selection()
128
131
  if selected:
129
- row = selected[0]
130
- section_name = self.tree.parent(row)
131
- if section_name == "Custom Items":
132
- self.tree.delete(row)
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)
133
137
  self.save_config()
134
- self.tree.focus("Custom Items")
135
- self.tree.selection_set("Custom Items")
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)
136
144
 
137
145
  def _destroy_entry(self, _):
138
146
  """Destroy any entry widgets in the treeview on an event, such as a mouse wheel
@@ -150,34 +158,68 @@ class ConfigEditorFrame(ttk.Frame):
150
158
  self.tree.bind("<Double-1>", self._set_cell_value)
151
159
  self.tree.bind("<Return>", self._set_selection_value)
152
160
  self.tree.bind("<BackSpace>", self._delete_selection_value)
153
- self.parent.bind("<MouseWheel>", self._destroy_entry)
154
- self.parent.bind("<Escape>", self._destroy_entry)
161
+ self.window.bind("<MouseWheel>", self._destroy_entry)
162
+ self.window.bind("<Escape>", self._destroy_entry)
155
163
 
156
164
  def _load_config_into_tree(self):
157
165
  """Load the configuration options into the treeview for display and editing."""
158
166
  for section in config.sections():
167
+ # App Settings are internal and shouldn't be displayed to the user
159
168
  if section == "App Settings":
160
169
  continue
170
+
161
171
  section_level = self.tree.insert("", "end", iid=section, text=section)
162
- for config_option, value in config.items(section):
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:
163
179
  if section not in ("User Settings", "App Settings"):
164
180
  option_name = config.option_to_name(config_option, href=True)
165
- self.tree.insert(section_level, "end", text=option_name, values=[value])
181
+ self.tree.insert(
182
+ section_level,
183
+ "end",
184
+ iid=f"{section}-{option_name}",
185
+ text=option_name,
186
+ values=[value],
187
+ )
166
188
  else:
167
189
  option_name = config.option_to_name(config_option)
168
- self.tree.insert(section_level, "end", text=option_name, values=[value])
190
+ self.tree.insert(
191
+ section_level,
192
+ "end",
193
+ iid=f"{section}-{option_name}",
194
+ text=option_name,
195
+ values=[value],
196
+ )
169
197
 
170
198
  self.tree.focus("User Settings")
171
199
  self.tree.selection_set("User Settings")
172
200
 
173
201
  def reload_config_into_tree(self):
174
- """Reload the configuration options into the treeview for display and
175
- editing.
202
+ """Reload the configuration options into the treeview for display and editing
203
+ and maintain the users current selection.
176
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
+
177
211
  for item in self.tree.get_children():
178
212
  self.tree.delete(item)
179
213
  self._load_config_into_tree()
180
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
+
181
223
  def _configure_treeview(self):
182
224
  """Configure a treeview to display and edit configuration options."""
183
225
  scrollbar = ttk.Scrollbar(self)
@@ -220,7 +262,7 @@ class ConfigEditorButtonFrame(ttk.Frame):
220
262
  reset_button = ttk.Button(self, text="Reset", command=self._reset_config)
221
263
  reset_button.pack(side="left", expand=True, padx=5)
222
264
 
223
- custom_item_button = ttk.Button(self, text="Add Custom Item", command=self._add_custom_item)
265
+ custom_item_button = ttk.Button(self, text="Add Item", command=self._add_custom_item)
224
266
  custom_item_button.pack(side="left", expand=True, padx=5)
225
267
 
226
268
  import_inventory_button = ttk.Button(
@@ -231,7 +273,9 @@ class ConfigEditorButtonFrame(ttk.Frame):
231
273
  def _reset_config(self):
232
274
  """Reset the configuration file to its default state."""
233
275
  confirm = messagebox.askokcancel(
234
- "Reset Config", "Are you sure you want to reset the configuration?", parent=self
276
+ "Reset Config",
277
+ "Are you sure you want to reset the configuration?",
278
+ parent=self.editor_frame,
235
279
  )
236
280
  if confirm:
237
281
  copy(CONFIG_FILE_BACKUP, CONFIG_FILE)
@@ -244,36 +288,48 @@ class ConfigEditorButtonFrame(ttk.Frame):
244
288
  """Open a window to add a new custom item."""
245
289
  custom_item_window = tk.Toplevel(self.editor_frame)
246
290
  custom_item_window.title(ADD_CUSTOM_ITEM_TITLE)
247
- 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))
248
293
  custom_item_window.focus_set()
249
294
 
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
+
250
301
  custom_item_frame = CustomItemFrame(custom_item_window, self.editor_frame)
251
302
  custom_item_frame.pack(expand=True, fill="both", padx=15, pady=15)
252
- self.editor_frame.tree.focus_set()
253
303
 
254
304
  def _import_steam_inventory(self):
255
305
  """Open a window to import the user's Steam inventory."""
256
306
  steam_inventory_window = tk.Toplevel(self.editor_frame)
257
307
  steam_inventory_window.title(IMPORT_INVENTORY_TITLE)
258
- 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))
259
310
  steam_inventory_window.focus_set()
260
311
 
312
+ def on_close():
313
+ steam_inventory_window.destroy()
314
+ self.editor_frame.tree.focus_set()
315
+
316
+ steam_inventory_window.protocol("WM_DELETE_WINDOW", on_close)
317
+
261
318
  steam_inventory_frame = InventoryImportFrame(steam_inventory_window, self.editor_frame)
262
319
  steam_inventory_frame.pack(expand=True, fill="both", padx=15, pady=15)
263
- self.editor_frame.tree.focus_set()
264
320
 
265
321
 
266
322
  class CustomItemFrame(ttk.Frame):
267
- def __init__(self, parent, editor_frame):
323
+ def __init__(self, window, editor_frame):
268
324
  """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
325
+ super().__init__(window, style="Card.TFrame", padding=15)
326
+ self.window = window
271
327
  self.editor_frame = editor_frame
272
328
  self._add_widgets()
273
329
 
274
330
  def _add_widgets(self):
275
331
  """Add widgets to the custom item frame for entering item details."""
276
- ttk.Label(self, text="Item URL:").pack(pady=5)
332
+ ttk.Label(self, text="Steam Market Listing URL:").pack(pady=5)
277
333
  item_url_entry = ttk.Entry(self)
278
334
  item_url_entry.pack(fill="x", padx=10)
279
335
 
@@ -287,95 +343,115 @@ class CustomItemFrame(ttk.Frame):
287
343
  command=lambda: self._add_custom_item(item_url_entry.get(), item_owned_entry.get()),
288
344
  )
289
345
  add_button.pack(pady=10)
290
- self.parent.bind("<Return>", lambda _: add_button.invoke())
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.
351
+
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
291
375
 
292
376
  def _add_custom_item(self, item_href, item_owned):
293
377
  """Add a custom item to the configuration."""
294
378
  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()
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
+ )
298
387
  return
299
388
 
300
- item_name = config.option_to_name(item_href, href=True)
389
+ try:
390
+ item_name = config.option_to_name(item_href, href=True)
391
+ except ValueError as error:
392
+ messagebox.showerror("Invalid URL", str(error), parent=self.window)
393
+ return
301
394
 
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
395
+ if self._update_existing("Stickers", item_name, item_owned):
396
+ return
397
+ if self._update_existing("Skins", item_name, item_owned):
398
+ return
311
399
 
400
+ section = "Stickers" if item_name.startswith("Sticker") else "Skins"
401
+ insert_index = self._get_insert_index(item_name, section)
312
402
  self.editor_frame.tree.insert(
313
- "Custom Items",
314
- "end",
403
+ section,
404
+ insert_index,
405
+ iid=f"{section}-{item_name}",
315
406
  text=item_name,
316
407
  values=[item_owned],
317
408
  )
318
- self.editor_frame.focus_set()
319
409
  self.editor_frame.save_config()
320
- self.parent.destroy()
410
+ self.window.destroy()
321
411
 
322
412
 
323
413
  class InventoryImportFrame(ttk.Frame):
324
414
  # pylint: disable=too-many-instance-attributes
325
- def __init__(self, parent, editor_frame):
415
+ def __init__(self, window, editor_frame):
326
416
  """Initialize the inventory import frame that allows users to import their Steam
327
417
  inventory.
328
418
  """
329
- super().__init__(parent, style="Card.TFrame", padding=10)
330
- self.parent = parent
419
+ super().__init__(window, padding=10)
420
+ self.window = window
331
421
  self.editor_frame = editor_frame
332
422
  self._add_widgets()
333
423
 
334
424
  def _add_widgets(self):
335
425
  """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
426
  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
- )
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)
356
433
  self.import_button.pack(pady=10)
357
- self.parent.bind("<Return>", lambda _: self.import_button.invoke())
434
+ self.entry_frame.pack(side="left", padx=10, pady=(0, 20), fill="both", expand=True)
358
435
 
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)
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)
439
+ self.import_cases_checkbox.pack(anchor="w", padx=10, pady=5)
440
+ self.import_sticker_capsules_checkbox.pack(anchor="w", padx=10, pady=5)
441
+ self.import_stickers_checkbox.pack(anchor="w", padx=10, pady=5)
442
+ self.import_others_checkbox.pack(anchor="w", padx=10, pady=5)
443
+ self.checkbox_frame.pack(side="left", padx=10, pady=(0, 20), fill="both", expand=True)
370
444
 
371
445
  def _configure_checkboxes(self):
372
446
  # pylint: disable=attribute-defined-outside-init
373
447
  """Configure the checkboxes for selecting what to import from the Steam
374
448
  inventory.
375
449
  """
450
+ self.checkbox_frame = ttk.LabelFrame(self, text="Import Settings", padding=15)
451
+
376
452
  self.regular_inventory_value = tk.BooleanVar(value=False)
377
453
  self.regular_inventory_checkbox = ttk.Checkbutton(
378
- self,
454
+ self.checkbox_frame,
379
455
  text="Regular Inventory",
380
456
  variable=self.regular_inventory_value,
381
457
  style="Switch.TCheckbutton",
@@ -383,7 +459,7 @@ class InventoryImportFrame(ttk.Frame):
383
459
 
384
460
  self.storage_units_value = tk.BooleanVar(value=True)
385
461
  self.storage_units_checkbox = ttk.Checkbutton(
386
- self,
462
+ self.checkbox_frame,
387
463
  text="Storage Units",
388
464
  variable=self.storage_units_value,
389
465
  style="Switch.TCheckbutton",
@@ -391,12 +467,15 @@ class InventoryImportFrame(ttk.Frame):
391
467
 
392
468
  self.import_cases_value = tk.BooleanVar(value=True)
393
469
  self.import_cases_checkbox = ttk.Checkbutton(
394
- self, text="Import Cases", variable=self.import_cases_value, style="Switch.TCheckbutton"
470
+ self.checkbox_frame,
471
+ text="Import Cases",
472
+ variable=self.import_cases_value,
473
+ style="Switch.TCheckbutton",
395
474
  )
396
475
 
397
476
  self.import_sticker_capsules_value = tk.BooleanVar(value=True)
398
477
  self.import_sticker_capsules_checkbox = ttk.Checkbutton(
399
- self,
478
+ self.checkbox_frame,
400
479
  text="Import Sticker Capsules",
401
480
  variable=self.import_sticker_capsules_value,
402
481
  style="Switch.TCheckbutton",
@@ -404,7 +483,7 @@ class InventoryImportFrame(ttk.Frame):
404
483
 
405
484
  self.import_stickers_value = tk.BooleanVar(value=False)
406
485
  self.import_stickers_checkbox = ttk.Checkbutton(
407
- self,
486
+ self.checkbox_frame,
408
487
  text="Import Stickers",
409
488
  variable=self.import_stickers_value,
410
489
  style="Switch.TCheckbutton",
@@ -412,7 +491,7 @@ class InventoryImportFrame(ttk.Frame):
412
491
 
413
492
  self.import_others_value = tk.BooleanVar(value=False)
414
493
  self.import_others_checkbox = ttk.Checkbutton(
415
- self,
494
+ self.checkbox_frame,
416
495
  text="Import Other Items",
417
496
  variable=self.import_others_value,
418
497
  style="Switch.TCheckbutton",
@@ -423,14 +502,37 @@ class InventoryImportFrame(ttk.Frame):
423
502
  """Configure the entry fields for Steam username, password, and two-factor
424
503
  code.
425
504
  """
426
- self.user_name_label = ttk.Label(self, text="Steam Username:")
427
- self.user_name_entry = ttk.Entry(self, justify="center", font=("Helvetica", 11))
505
+ self.entry_frame = ttk.Frame(self, style="Card.TFrame", padding=15)
428
506
 
429
- self.password_label = ttk.Label(self, text="Steam Password:")
430
- self.password_entry = ttk.Entry(self, show="*", justify="center", font=("Helvetica", 11))
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))
431
509
 
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))
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
+ )
514
+
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())
434
536
 
435
537
  def _import_inventory(self):
436
538
  """
@@ -467,27 +569,36 @@ class InventoryImportFrame(ttk.Frame):
467
569
  ]
468
570
  )
469
571
 
470
- self.parent.destroy()
572
+ self.window.destroy()
471
573
 
472
574
  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()
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()
477
586
 
478
- process_frame = InventoryImportProcessFrame(text_window, self.editor_frame)
587
+ console_window.protocol("WM_DELETE_WINDOW", on_close)
588
+
589
+ process_frame = InventoryImportProcessFrame(console_window, self.editor_frame)
479
590
  process_frame.pack(expand=True, fill="both", padx=15, pady=15)
480
- process_frame.console.focus_set()
591
+ process_frame.console.focus_force()
481
592
  process_frame.start(node_cmd)
482
593
 
483
594
 
484
595
  class InventoryImportProcessFrame(ttk.Frame):
485
596
  # pylint: disable=attribute-defined-outside-init
486
597
  # Source: https://stackoverflow.com/questions/27327886/issues-intercepting-subprocess-output-in-real-time
487
- def __init__(self, parent, editor_frame):
598
+ def __init__(self, window, editor_frame):
488
599
  """Initialize the frame that displays the output of the subprocess."""
489
- super().__init__(parent)
490
- self.parent = parent
600
+ super().__init__(window)
601
+ self.window = window
491
602
  self.editor_frame = editor_frame
492
603
  self._add_widgets()
493
604
 
@@ -498,6 +609,7 @@ class InventoryImportProcessFrame(ttk.Frame):
498
609
 
499
610
  self.console = ThemedText(self, wrap="word", yscrollcommand=self.scrollbar.set)
500
611
  self.console.config(state="disabled")
612
+ self.console.tag_configure("error", foreground="red")
501
613
  self.console.pack(expand=True, fill="both", padx=10, pady=10)
502
614
 
503
615
  self.scrollbar.config(command=self.console.yview)
@@ -531,14 +643,17 @@ class InventoryImportProcessFrame(ttk.Frame):
531
643
  try:
532
644
  line = self.queue.get(block=False)
533
645
  self.console.config(state="normal")
534
- self.console.insert("end", line)
646
+ if "[ERROR]" in line:
647
+ self.console.insert("end", line, "error")
648
+ else:
649
+ self.console.insert("end", line)
535
650
  self.console.config(state="disabled")
536
651
  self.console.yview("end")
537
652
  except Empty:
538
653
  pass
539
654
 
540
655
  if self.process.poll() is None or not self.queue.empty():
541
- self.after(50, self._update_lines)
656
+ self.after(35, self._update_lines)
542
657
  else:
543
658
  self._cleanup()
544
659
 
@@ -552,4 +667,4 @@ class InventoryImportProcessFrame(ttk.Frame):
552
667
  config.read_from_inventory_file()
553
668
  self.editor_frame.reload_config_into_tree()
554
669
  self.editor_frame.tree.focus_set()
555
- self.parent.destroy()
670
+ self.window.destroy()
@@ -0,0 +1,61 @@
1
+ from tkinter import ttk
2
+ from typing import cast
3
+
4
+ import matplotlib.pyplot as plt
5
+ from matplotlib.axes import Axes
6
+ from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
7
+ from matplotlib.dates import DateFormatter
8
+
9
+ from cs2tracker.config import get_config
10
+ from cs2tracker.logs import PriceLogs
11
+ from cs2tracker.scraper.parser import Parser
12
+
13
+ config = get_config()
14
+
15
+
16
+ class PriceHistoryFrame(ttk.Frame):
17
+ # pylint: disable=attribute-defined-outside-init
18
+ def __init__(self, parent):
19
+ """Initialize the price history frame."""
20
+ super().__init__(parent)
21
+
22
+ self._add_widgets()
23
+
24
+ def _add_widgets(self):
25
+ """Add widgets to the frame."""
26
+ self._configure_canvas()
27
+ self.canvas.get_tk_widget().pack(fill="both", expand=True)
28
+
29
+ self.toolbar = NavigationToolbar2Tk(self.canvas, self)
30
+ self.toolbar.update()
31
+ self.toolbar.pack()
32
+
33
+ def _configure_canvas(self):
34
+ """Configure the canvas on which the price history chart is drawn."""
35
+ self._draw_plot()
36
+ plt.close(self.fig)
37
+
38
+ self.canvas = FigureCanvasTkAgg(self.fig, master=self)
39
+ self.canvas.draw()
40
+
41
+ def _draw_plot(self):
42
+ """Draw a chart of the price history."""
43
+
44
+ self.fig, ax_raw = plt.subplots(dpi=100)
45
+ self.fig.autofmt_xdate()
46
+ ax = cast(Axes, ax_raw)
47
+
48
+ dates, totals = PriceLogs.read()
49
+ for price_source in Parser.SOURCES:
50
+ usd_prices = totals[price_source]["USD"]
51
+ converted_prices = totals[price_source][config.conversion_currency]
52
+ ax.plot(dates, usd_prices, label=f"{price_source.name.title()}: USD")
53
+ ax.plot(
54
+ dates,
55
+ converted_prices,
56
+ label=f"{price_source.name.title()}: {config.conversion_currency}",
57
+ )
58
+
59
+ ax.legend(loc="upper left", fontsize="small")
60
+ date_formatter = DateFormatter("%Y-%m-%d")
61
+ ax.xaxis.set_major_formatter(date_formatter)