Skip to content

Commit 408cd55

Browse files
authored
Merge pull request #67 from ashiven/Issue-#52
Issue #52
2 parents affcc40 + 0f3fd5f commit 408cd55

File tree

13 files changed

+649
-809
lines changed

13 files changed

+649
-809
lines changed

.isort.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
[settings]
2-
known_third_party = bs4,currency_converter,matplotlib,nodejs,requests,rich,sv_ttk,tenacity,tksheet,ttk_text,urllib3
2+
known_third_party = bs4,currency_converter,matplotlib,nodejs,requests,requests_cache,rich,sv_ttk,tenacity,tksheet,ttk_text,urllib3

cs2tracker/app/application.py

Lines changed: 72 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -34,137 +34,131 @@
3434
class Application:
3535
def __init__(self):
3636
self.scraper = Scraper()
37-
self.application_window = None
3837

3938
def run(self):
40-
"""Run the main application window with buttons for scraping prices, editing the
41-
configuration, showing history in a chart, and editing the log file.
42-
"""
43-
self.application_window = self._configure_window()
39+
"""Run the main application window."""
40+
window = self._configure_window()
4441

4542
if DARK_THEME:
4643
sv_ttk.use_dark_theme()
4744
else:
4845
sv_ttk.use_light_theme()
4946

50-
self.application_window.mainloop()
47+
window.mainloop()
48+
49+
def _configure_window(self):
50+
"""Configure the main application window."""
51+
window = tk.Tk()
52+
window.title(APPLICATION_NAME)
53+
window.geometry(WINDOW_SIZE)
54+
55+
if OS == OSType.WINDOWS:
56+
app_id = "cs2tracker.unique.id"
57+
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(app_id)
58+
59+
icon = tk.PhotoImage(file=ICON_FILE)
60+
window.wm_iconphoto(True, icon)
61+
62+
main_frame = MainFrame(window, self.scraper)
63+
main_frame.pack(expand=True, fill="both")
64+
65+
return window
5166

52-
def _add_button(self, frame, text, command, row):
67+
68+
class MainFrame(ttk.Frame):
69+
# pylint: disable=too-many-instance-attributes,attribute-defined-outside-init
70+
def __init__(self, parent, scraper):
71+
super().__init__(parent, padding=15)
72+
self.parent = parent
73+
self.scraper = scraper
74+
self._add_widgets()
75+
76+
def _add_widgets(self):
77+
"""Add widgets to the main frame."""
78+
self.columnconfigure(0, weight=1)
79+
self.columnconfigure(1, weight=1)
80+
self.rowconfigure(0, weight=1)
81+
82+
self._configure_button_frame()
83+
self.button_frame.grid(row=0, column=0, padx=10, pady=(7, 20), sticky="nsew")
84+
self._configure_checkbox_frame()
85+
self.checkbox_frame.grid(row=0, column=1, padx=10, pady=(0, 20), sticky="nsew")
86+
87+
def _add_button(self, text, command, row):
5388
"""Create and style a button for the button frame."""
5489
grid_pos = {"row": row, "column": 0, "sticky": "ew", "padx": 10, "pady": 10}
55-
button = ttk.Button(frame, text=text, command=command)
90+
button = ttk.Button(self.button_frame, text=text, command=command)
5691
button.grid(**grid_pos)
5792

58-
def _configure_button_frame(self, main_frame):
93+
def _configure_button_frame(self):
5994
"""Configure the button frame of the application main frame."""
60-
button_frame = ttk.Frame(main_frame, style="Card.TFrame", padding=15)
61-
button_frame.columnconfigure(0, weight=1)
62-
button_frame.grid(row=0, column=0, padx=10, pady=(7, 20), sticky="nsew")
95+
self.button_frame = ttk.Frame(self, style="Card.TFrame", padding=15)
96+
self.button_frame.columnconfigure(0, weight=1)
6397

64-
self._add_button(button_frame, "Run!", self.scrape_prices, 0)
65-
self._add_button(button_frame, "Edit Config", self._edit_config, 1)
66-
self._add_button(button_frame, "Show History", self._draw_plot, 2)
67-
self._add_button(button_frame, "Export History", self._export_log_file, 3)
68-
self._add_button(button_frame, "Import History", self._import_log_file, 4)
98+
self._add_button("Run!", self.scrape_prices, 0)
99+
self._add_button("Edit Config", self._edit_config, 1)
100+
self._add_button("Show History", self._draw_plot, 2)
101+
self._add_button("Export History", self._export_log_file, 3)
102+
self._add_button("Import History", self._import_log_file, 4)
69103

70104
def _add_checkbox(
71-
self, frame, text, variable, command, row
72-
): # pylint: disable=too-many-arguments,too-many-positional-arguments
105+
self, text, variable, command, row
106+
): # pylint: disable=too-many-arguments,too-many-positional-arguments,attribute-defined-outside-init
73107
"""Create and style a checkbox for the checkbox frame."""
74108
grid_pos = {"row": row, "column": 0, "sticky": "w", "padx": (10, 0), "pady": 5}
75109
checkbox = ttk.Checkbutton(
76-
frame,
110+
self.checkbox_frame,
77111
text=text,
78112
variable=variable,
79113
command=command,
80114
style="Switch.TCheckbutton",
81115
)
82116
checkbox.grid(**grid_pos)
83117

84-
def _configure_checkbox_frame(self, main_frame):
118+
def _configure_checkbox_frame(self):
85119
"""Configure the checkbox frame for background tasks and settings."""
86-
checkbox_frame = ttk.LabelFrame(main_frame, text="Settings", padding=15)
87-
checkbox_frame.grid(row=0, column=1, padx=10, pady=(0, 20), sticky="nsew")
120+
self.checkbox_frame = ttk.LabelFrame(self, text="Settings", padding=15)
88121

89-
background_checkbox_value = tk.BooleanVar(value=BackgroundTask.identify())
122+
self.background_checkbox_value = tk.BooleanVar(value=BackgroundTask.identify())
90123
self._add_checkbox(
91-
checkbox_frame,
92124
"Background Task",
93-
background_checkbox_value,
94-
lambda: self._toggle_background_task(background_checkbox_value.get()),
125+
self.background_checkbox_value,
126+
lambda: self._toggle_background_task(self.background_checkbox_value.get()),
95127
0,
96128
)
97129

98-
discord_webhook_checkbox_value = tk.BooleanVar(
130+
self.discord_webhook_checkbox_value = tk.BooleanVar(
99131
value=config.getboolean("App Settings", "discord_notifications", fallback=False)
100132
)
101133
self._add_checkbox(
102-
checkbox_frame,
103134
"Discord Notifications",
104-
discord_webhook_checkbox_value,
105-
lambda: discord_webhook_checkbox_value.set(
106-
self._toggle_discord_webhook(discord_webhook_checkbox_value.get())
135+
self.discord_webhook_checkbox_value,
136+
lambda: self.discord_webhook_checkbox_value.set(
137+
self._toggle_discord_webhook(self.discord_webhook_checkbox_value.get())
107138
),
108139
1,
109140
)
110141

111-
use_proxy_checkbox_value = tk.BooleanVar(
142+
self.use_proxy_checkbox_value = tk.BooleanVar(
112143
value=config.getboolean("App Settings", "use_proxy", fallback=False)
113144
)
114145
self._add_checkbox(
115-
checkbox_frame,
116146
"Proxy Requests",
117-
use_proxy_checkbox_value,
118-
lambda: use_proxy_checkbox_value.set(
119-
self._toggle_use_proxy(use_proxy_checkbox_value.get())
147+
self.use_proxy_checkbox_value,
148+
lambda: self.use_proxy_checkbox_value.set(
149+
self._toggle_use_proxy(self.use_proxy_checkbox_value.get())
120150
),
121151
2,
122152
)
123153

124-
# pylint: disable=attribute-defined-outside-init
125154
self.dark_theme_checkbox_value = tk.BooleanVar(value=DARK_THEME)
126-
self._add_checkbox(
127-
checkbox_frame, "Dark Theme", self.dark_theme_checkbox_value, sv_ttk.toggle_theme, 3
128-
)
129-
130-
def _configure_main_frame(self, window):
131-
"""Configure the main frame of the application window with buttons and
132-
checkboxes.
133-
"""
134-
main_frame = ttk.Frame(window, padding=15)
135-
main_frame.columnconfigure(0, weight=1)
136-
main_frame.columnconfigure(1, weight=1)
137-
main_frame.rowconfigure(0, weight=1)
138-
139-
self._configure_button_frame(main_frame)
140-
self._configure_checkbox_frame(main_frame)
141-
142-
main_frame.pack(expand=True, fill="both")
143-
144-
def _configure_window(self):
145-
"""Configure the main application window UI and add buttons for the main
146-
functionalities.
147-
"""
148-
window = tk.Tk()
149-
window.title(APPLICATION_NAME)
150-
window.geometry(WINDOW_SIZE)
151-
152-
if OS == OSType.WINDOWS:
153-
app_id = "cs2tracker.unique.id"
154-
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(app_id)
155-
156-
icon = tk.PhotoImage(file=ICON_FILE)
157-
window.wm_iconphoto(True, icon)
158-
159-
self._configure_main_frame(window)
160-
161-
return window
155+
self._add_checkbox("Dark Theme", self.dark_theme_checkbox_value, sv_ttk.toggle_theme, 3)
162156

163157
def scrape_prices(self):
164158
"""Scrape prices from the configured sources, print the total, and save the
165159
results to a file.
166160
"""
167-
scraper_window = tk.Toplevel(self.application_window)
161+
scraper_window = tk.Toplevel(self.parent)
168162
scraper_window.geometry(SCRAPER_WINDOW_SIZE)
169163
scraper_window.title(SCRAPER_WINDOW_TITLE)
170164

@@ -179,7 +173,7 @@ def scrape_prices(self):
179173

180174
def _edit_config(self):
181175
"""Open a new window with a config editor GUI."""
182-
config_editor_window = tk.Toplevel(self.application_window)
176+
config_editor_window = tk.Toplevel(self.parent)
183177
config_editor_window.geometry(CONFIG_EDITOR_SIZE)
184178
config_editor_window.title(CONFIG_EDITOR_TITLE)
185179

@@ -235,7 +229,7 @@ def _toggle_use_proxy(self, enabled: bool):
235229
messagebox.showerror(
236230
"Config Error",
237231
"You need to enter a valid crawlbase API key into the configuration to use this feature.",
238-
parent=self.application_window,
232+
parent=self.parent,
239233
)
240234
return False
241235

@@ -249,7 +243,7 @@ def _toggle_discord_webhook(self, enabled: bool):
249243
messagebox.showerror(
250244
"Config Error",
251245
"You need to enter a valid Discord webhook URL into the configuration to use this feature.",
252-
parent=self.application_window,
246+
parent=self.parent,
253247
)
254248
return False
255249

cs2tracker/app/editor_frame.py

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,10 @@ def save_config(self):
5959
config.add_section(section)
6060
for item in self.tree.get_children(section):
6161
item_name = self.tree.item(item, "text")
62-
config_option = (
63-
config.name_to_option(item_name, custom=True)
64-
if section == "Custom Items"
65-
else config.name_to_option(item_name)
66-
)
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)
6766
value = self.tree.item(item, "values")[0]
6867
config.set(section, config_option, value)
6968

@@ -95,7 +94,7 @@ def _set_cell_value(self, event, row=None, column=None):
9594
column = self.tree.identify_column(event.x)
9695

9796
item_text = self.tree.item(row, "text")
98-
if any(item_text == section for section in config.sections()):
97+
if column == "#0" or any(item_text == section for section in config.sections()):
9998
return
10099
item_value = self.tree.item(row, "values")[0]
101100

@@ -161,9 +160,9 @@ def _load_config_into_tree(self):
161160
continue
162161
section_level = self.tree.insert("", "end", iid=section, text=section)
163162
for config_option, value in config.items(section):
164-
if section == "Custom Items":
165-
custom_item_name = config.option_to_name(config_option, custom=True)
166-
self.tree.insert(section_level, "end", text=custom_item_name, values=[value])
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])
167166
else:
168167
option_name = config.option_to_name(config_option)
169168
self.tree.insert(section_level, "end", text=option_name, values=[value])
@@ -290,18 +289,30 @@ def _add_widgets(self):
290289
add_button.pack(pady=10)
291290
self.parent.bind("<Return>", lambda _: add_button.invoke())
292291

293-
def _add_custom_item(self, item_url, item_owned):
292+
def _add_custom_item(self, item_href, item_owned):
294293
"""Add a custom item to the configuration."""
295-
if not item_url or not item_owned:
294+
if not item_href or not item_owned:
296295
messagebox.showerror("Input Error", "All fields must be filled out.", parent=self)
297296
self.editor_frame.focus_set()
298297
self.parent.focus_set()
299298
return
300299

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+
301312
self.editor_frame.tree.insert(
302313
"Custom Items",
303314
"end",
304-
text=config.option_to_name(item_url, custom=True),
315+
text=item_name,
305316
values=[item_owned],
306317
)
307318
self.editor_frame.focus_set()

cs2tracker/app/scraper_frame.py

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
from tksheet import Sheet
66

7+
from cs2tracker.scraper.scraper import ParsingError
8+
79

810
class ScraperFrame(ttk.Frame):
911
def __init__(self, parent, scraper, sheet_size, dark_theme):
@@ -43,18 +45,36 @@ def _configure_sheet(self):
4345
height=self.sheet_height,
4446
width=self.sheet_width,
4547
auto_resize_columns=150,
48+
default_column_width=150,
4649
sticky="nsew",
4750
)
4851
self.sheet.enable_bindings()
52+
53+
source_titles = []
54+
for price_source in self.scraper.parser.SOURCES:
55+
source_titles += [
56+
f"{price_source.value.title()} (USD)",
57+
f"{price_source.value.title()} Owned (USD)",
58+
]
4959
self.sheet.insert_row(
50-
["Item Name", "Item Owned", "Steam Market Price (USD)", "Total Value Owned (USD)"]
60+
[
61+
"Item Name",
62+
"Item Owned",
63+
]
64+
+ source_titles
5165
)
52-
self.sheet.column_width(0, 220)
53-
self.sheet.column_width(1, 20)
5466
self.sheet.align_rows([0], "c")
55-
self.sheet.align_columns([1, 2, 3], "c")
56-
self.sheet.popup_menu_add_command("Save Sheet", self._save_sheet)
5767

68+
price_columns = list(range(2 * len(self.scraper.parser.SOURCES)))
69+
price_columns = [1] + [column_index + 2 for column_index in price_columns]
70+
self.sheet.align_columns(price_columns, "c")
71+
self.sheet.column_width(0, 220)
72+
73+
required_window_width = 220 + 150 * len(price_columns)
74+
if int(self.sheet_width) < required_window_width:
75+
self.parent.geometry(f"{required_window_width}x{self.sheet_height}")
76+
77+
self.sheet.popup_menu_add_command("Save Sheet", self._save_sheet)
5878
self.parent.bind("<Configure>", self._readjust_sheet_size_with_window_size)
5979

6080
def _save_sheet(self):
@@ -84,10 +104,7 @@ def update_sheet_callback(row):
84104

85105
self.scraper.scrape_prices(update_sheet_callback)
86106

87-
row_heights = self.sheet.get_row_heights()
88-
last_row_index = len(row_heights) - 1
89-
self.sheet.align_rows(last_row_index, "c")
90-
91107
if self.scraper.error_stack:
92108
last_error = self.scraper.error_stack[-1]
93-
messagebox.showerror("An Error Occurred", f"{last_error.message}", parent=self)
109+
if not isinstance(last_error, ParsingError):
110+
messagebox.showerror("An Error Occurred", f"{last_error.message}", parent=self)

0 commit comments

Comments
 (0)