cs2tracker 2.1.14__py3-none-any.whl → 2.1.15__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of cs2tracker might be problematic. Click here for more details.
- cs2tracker/_version.py +2 -2
- cs2tracker/app/__init__.py +0 -3
- cs2tracker/app/{application.py → app.py} +77 -45
- cs2tracker/app/editor_frame.py +223 -108
- cs2tracker/app/history_frame.py +61 -0
- cs2tracker/app/scraper_frame.py +13 -7
- cs2tracker/{util/validated_config.py → config.py} +97 -52
- cs2tracker/data/config.ini +24 -21
- cs2tracker/data/convert_inventory.js +108 -28
- cs2tracker/data/get_inventory.js +34 -13
- cs2tracker/logs.py +143 -0
- cs2tracker/main.py +3 -3
- cs2tracker/scraper/__init__.py +0 -9
- cs2tracker/scraper/background_task.py +1 -1
- cs2tracker/scraper/discord_notifier.py +34 -32
- cs2tracker/scraper/{parsers.py → parser.py} +29 -32
- cs2tracker/scraper/scraper.py +113 -83
- cs2tracker/util/__init__.py +0 -9
- cs2tracker/util/currency_conversion.py +84 -0
- cs2tracker/util/padded_console.py +5 -0
- cs2tracker/util/tkinter.py +55 -0
- {cs2tracker-2.1.14.dist-info → cs2tracker-2.1.15.dist-info}/METADATA +4 -5
- cs2tracker-2.1.15.dist-info/RECORD +31 -0
- cs2tracker/util/price_logs.py +0 -100
- cs2tracker-2.1.14.dist-info/RECORD +0 -28
- {cs2tracker-2.1.14.dist-info → cs2tracker-2.1.15.dist-info}/WHEEL +0 -0
- {cs2tracker-2.1.14.dist-info → cs2tracker-2.1.15.dist-info}/entry_points.txt +0 -0
- {cs2tracker-2.1.14.dist-info → cs2tracker-2.1.15.dist-info}/licenses/LICENSE +0 -0
- {cs2tracker-2.1.14.dist-info → cs2tracker-2.1.15.dist-info}/top_level.txt +0 -0
cs2tracker/app/editor_frame.py
CHANGED
|
@@ -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
|
|
19
|
+
from cs2tracker.util.tkinter import centered, size_info
|
|
19
20
|
|
|
20
21
|
ADD_CUSTOM_ITEM_TITLE = "Add Custom Item"
|
|
21
|
-
ADD_CUSTOM_ITEM_SIZE = "
|
|
22
|
+
ADD_CUSTOM_ITEM_SIZE = "500x230"
|
|
22
23
|
|
|
23
24
|
IMPORT_INVENTORY_TITLE = "Import Steam Inventory"
|
|
24
|
-
IMPORT_INVENTORY_SIZE = "
|
|
25
|
+
IMPORT_INVENTORY_SIZE = "700x350"
|
|
25
26
|
|
|
26
27
|
IMPORT_INVENTORY_PROCESS_TITLE = "Importing Steam Inventory..."
|
|
27
28
|
IMPORT_INVENTORY_PROCESS_SIZE = "700x500"
|
|
@@ -30,13 +31,13 @@ config = get_config()
|
|
|
30
31
|
|
|
31
32
|
|
|
32
33
|
class ConfigEditorFrame(ttk.Frame):
|
|
33
|
-
def __init__(self,
|
|
34
|
+
def __init__(self, window):
|
|
34
35
|
"""Initialize the configuration editor frame that allows users to view and edit
|
|
35
36
|
the configuration options.
|
|
36
37
|
"""
|
|
37
|
-
super().__init__(
|
|
38
|
+
super().__init__(window, padding=15)
|
|
38
39
|
|
|
39
|
-
self.
|
|
40
|
+
self.window = window
|
|
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",
|
|
75
|
+
"Config Error",
|
|
76
|
+
f"The configuration is invalid. ({config.last_error})",
|
|
77
|
+
parent=self.window,
|
|
75
78
|
)
|
|
76
|
-
self.
|
|
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
|
-
|
|
130
|
-
section_name = self.tree.parent(
|
|
131
|
-
if section_name
|
|
132
|
-
self.tree.
|
|
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
|
-
|
|
135
|
-
|
|
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.
|
|
154
|
-
self.
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
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",
|
|
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,
|
|
323
|
+
def __init__(self, window, editor_frame):
|
|
268
324
|
"""Initialize the custom item frame that allows users to add custom items."""
|
|
269
|
-
super().__init__(
|
|
270
|
-
self.
|
|
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="
|
|
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.
|
|
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(
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
314
|
-
|
|
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.
|
|
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,
|
|
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__(
|
|
330
|
-
self.
|
|
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=
|
|
347
|
-
self.user_name_entry.pack(fill="x", padx=
|
|
348
|
-
self.password_label.pack(pady=
|
|
349
|
-
self.password_entry.pack(fill="x", padx=
|
|
350
|
-
self.two_factor_label.pack(pady=
|
|
351
|
-
self.two_factor_entry.pack(fill="x", padx=
|
|
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.
|
|
434
|
+
self.entry_frame.pack(side="left", padx=10, pady=(0, 20), fill="both", expand=True)
|
|
358
435
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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
|
|
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.
|
|
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.
|
|
430
|
-
self.
|
|
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.
|
|
433
|
-
self.
|
|
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.
|
|
572
|
+
self.window.destroy()
|
|
471
573
|
|
|
472
574
|
def _display_node_subprocess(self, node_cmd):
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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
|
-
|
|
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.
|
|
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,
|
|
598
|
+
def __init__(self, window, editor_frame):
|
|
488
599
|
"""Initialize the frame that displays the output of the subprocess."""
|
|
489
|
-
super().__init__(
|
|
490
|
-
self.
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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)
|