talks-reducer 0.6.3__py3-none-any.whl → 0.7.1__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.
@@ -0,0 +1,8 @@
1
+ """Run the Talks Reducer GUI as a module."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from . import main
6
+
7
+ if __name__ == "__main__":
8
+ main()
@@ -0,0 +1,126 @@
1
+ """Discovery helpers for the Talks Reducer GUI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import threading
6
+ from typing import TYPE_CHECKING, List
7
+
8
+ from ..discovery import discover_servers as core_discover_servers
9
+
10
+ if TYPE_CHECKING: # pragma: no cover - imported for type checking only
11
+ from . import TalksReducerGUI
12
+
13
+
14
+ def start_discovery(gui: "TalksReducerGUI") -> None:
15
+ """Search the local network for running Talks Reducer servers."""
16
+
17
+ if gui._discovery_thread and gui._discovery_thread.is_alive():
18
+ return
19
+
20
+ gui.server_discover_button.configure(state=gui.tk.DISABLED, text="Discovering…")
21
+ gui._append_log("Discovering Talks Reducer servers on port 9005…")
22
+
23
+ def worker() -> None:
24
+ try:
25
+ urls = core_discover_servers(
26
+ progress_callback=lambda current, total: gui._schedule_on_ui_thread(
27
+ lambda c=current, t=total: on_discovery_progress(gui, c, t)
28
+ )
29
+ )
30
+ except Exception as exc: # pragma: no cover - network failure safeguard
31
+ gui._schedule_on_ui_thread(lambda: on_discovery_failed(gui, exc))
32
+ return
33
+ gui._schedule_on_ui_thread(lambda: on_discovery_complete(gui, urls))
34
+
35
+ gui._discovery_thread = threading.Thread(target=worker, daemon=True)
36
+ gui._discovery_thread.start()
37
+
38
+
39
+ def on_discovery_failed(gui: "TalksReducerGUI", exc: Exception) -> None:
40
+ gui.server_discover_button.configure(state=gui.tk.NORMAL, text="Discover")
41
+ message = f"Discovery failed: {exc}"
42
+ gui._append_log(message)
43
+ gui.messagebox.showerror("Discovery failed", message)
44
+
45
+
46
+ def on_discovery_progress(gui: "TalksReducerGUI", current: int, total: int) -> None:
47
+ if total > 0:
48
+ bounded = max(0, min(current, total))
49
+ label = f"{bounded} / {total}"
50
+ else:
51
+ label = "Discovering…"
52
+ gui.server_discover_button.configure(text=label)
53
+
54
+
55
+ def on_discovery_complete(gui: "TalksReducerGUI", urls: List[str]) -> None:
56
+ gui.server_discover_button.configure(state=gui.tk.NORMAL, text="Discover")
57
+ if not urls:
58
+ gui._append_log("No Talks Reducer servers were found.")
59
+ gui.messagebox.showinfo(
60
+ "No servers found",
61
+ "No Talks Reducer servers responded on port 9005.",
62
+ )
63
+ return
64
+
65
+ gui._append_log(f"Discovered {len(urls)} server{'s' if len(urls) != 1 else ''}.")
66
+
67
+ if len(urls) == 1:
68
+ gui.server_url_var.set(urls[0])
69
+ return
70
+
71
+ show_discovery_results(gui, urls)
72
+
73
+
74
+ def show_discovery_results(gui: "TalksReducerGUI", urls: List[str]) -> None:
75
+ dialog = gui.tk.Toplevel(gui.root)
76
+ dialog.title("Select server")
77
+ dialog.transient(gui.root)
78
+ dialog.grab_set()
79
+
80
+ gui.ttk.Label(dialog, text="Select a Talks Reducer server:").grid(
81
+ row=0, column=0, columnspan=2, sticky="w", padx=gui.PADDING, pady=(12, 4)
82
+ )
83
+
84
+ listbox = gui.tk.Listbox(
85
+ dialog,
86
+ height=min(10, len(urls)),
87
+ selectmode=gui.tk.SINGLE,
88
+ )
89
+ listbox.grid(
90
+ row=1,
91
+ column=0,
92
+ columnspan=2,
93
+ padx=gui.PADDING,
94
+ sticky="nsew",
95
+ )
96
+ dialog.columnconfigure(0, weight=1)
97
+ dialog.columnconfigure(1, weight=1)
98
+ dialog.rowconfigure(1, weight=1)
99
+
100
+ for url in urls:
101
+ listbox.insert(gui.tk.END, url)
102
+ listbox.select_set(0)
103
+
104
+ def choose(_: object | None = None) -> None:
105
+ selection = listbox.curselection()
106
+ if not selection:
107
+ return
108
+ index = selection[0]
109
+ gui.server_url_var.set(urls[index])
110
+ dialog.grab_release()
111
+ dialog.destroy()
112
+
113
+ def cancel() -> None:
114
+ dialog.grab_release()
115
+ dialog.destroy()
116
+
117
+ listbox.bind("<Double-Button-1>", choose)
118
+ listbox.bind("<Return>", choose)
119
+
120
+ button_frame = gui.ttk.Frame(dialog)
121
+ button_frame.grid(row=2, column=0, columnspan=2, pady=(8, 12))
122
+ gui.ttk.Button(button_frame, text="Use server", command=choose).pack(
123
+ side=gui.tk.LEFT, padx=(0, 8)
124
+ )
125
+ gui.ttk.Button(button_frame, text="Cancel", command=cancel).pack(side=gui.tk.LEFT)
126
+ dialog.protocol("WM_DELETE_WINDOW", cancel)
@@ -0,0 +1,526 @@
1
+ """Layout helpers for the Talks Reducer GUI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING, Callable
8
+
9
+ from ..models import default_temp_folder
10
+
11
+ if TYPE_CHECKING: # pragma: no cover - imported for type checking only
12
+ import tkinter as tk
13
+
14
+ from . import TalksReducerGUI
15
+
16
+
17
+ def build_layout(gui: "TalksReducerGUI") -> None:
18
+ """Construct the main layout for the GUI."""
19
+
20
+ main = gui.ttk.Frame(gui.root, padding=gui.PADDING)
21
+ main.grid(row=0, column=0, sticky="nsew")
22
+ gui.root.columnconfigure(0, weight=1)
23
+ gui.root.rowconfigure(0, weight=1)
24
+
25
+ # Input selection frame
26
+ input_frame = gui.ttk.Frame(main, padding=gui.PADDING)
27
+ input_frame.grid(row=0, column=0, sticky="nsew")
28
+ main.rowconfigure(0, weight=1)
29
+ main.columnconfigure(0, weight=1)
30
+ input_frame.columnconfigure(0, weight=1)
31
+ input_frame.rowconfigure(0, weight=1)
32
+
33
+ gui.drop_zone = gui.tk.Label(
34
+ input_frame,
35
+ text="Drop video here",
36
+ relief=gui.tk.FLAT,
37
+ borderwidth=0,
38
+ padx=gui.PADDING,
39
+ pady=gui.PADDING,
40
+ highlightthickness=0,
41
+ )
42
+ gui.drop_zone.grid(row=0, column=0, sticky="nsew")
43
+ gui._configure_drop_targets(gui.drop_zone)
44
+ gui.drop_zone.configure(cursor="hand2", takefocus=1)
45
+ gui.drop_zone.bind("<Button-1>", gui._on_drop_zone_click)
46
+ gui.drop_zone.bind("<Return>", gui._on_drop_zone_click)
47
+ gui.drop_zone.bind("<space>", gui._on_drop_zone_click)
48
+
49
+ # Options frame
50
+ gui.options_frame = gui.ttk.Frame(main, padding=gui.PADDING)
51
+ gui.options_frame.grid(row=2, column=0, pady=(0, 0), sticky="ew")
52
+ gui.options_frame.columnconfigure(0, weight=1)
53
+
54
+ checkbox_frame = gui.ttk.Frame(gui.options_frame)
55
+ checkbox_frame.grid(row=0, column=0, columnspan=2, sticky="w")
56
+
57
+ gui.ttk.Checkbutton(
58
+ checkbox_frame,
59
+ text="Small video",
60
+ variable=gui.small_var,
61
+ ).grid(row=0, column=0, sticky="w")
62
+
63
+ gui.ttk.Checkbutton(
64
+ checkbox_frame,
65
+ text="Open after convert",
66
+ variable=gui.open_after_convert_var,
67
+ ).grid(row=0, column=1, sticky="w", padx=(12, 0))
68
+
69
+ gui.simple_mode_check = gui.ttk.Checkbutton(
70
+ checkbox_frame,
71
+ text="Simple mode",
72
+ variable=gui.simple_mode_var,
73
+ command=gui._toggle_simple_mode,
74
+ )
75
+ gui.simple_mode_check.grid(row=1, column=0, columnspan=3, sticky="w", pady=(8, 0))
76
+
77
+ gui.advanced_visible = gui.tk.BooleanVar(value=False)
78
+
79
+ basic_label_container = gui.ttk.Frame(gui.options_frame)
80
+ basic_label = gui.ttk.Label(basic_label_container, text="Basic options")
81
+ basic_label.pack(side=gui.tk.LEFT)
82
+
83
+ gui.reset_basic_button = gui.ttk.Button(
84
+ basic_label_container,
85
+ text="Reset to defaults",
86
+ command=gui._reset_basic_defaults,
87
+ state=gui.tk.DISABLED,
88
+ style="Link.TButton",
89
+ )
90
+
91
+ gui.basic_options_frame = gui.ttk.Labelframe(
92
+ gui.options_frame, padding=0, labelwidget=basic_label_container
93
+ )
94
+ gui.basic_options_frame.grid(
95
+ row=2, column=0, columnspan=2, sticky="ew", pady=(12, 0)
96
+ )
97
+ gui.basic_options_frame.columnconfigure(1, weight=1)
98
+
99
+ gui._reset_button_visible = False
100
+
101
+ gui.silent_speed_var = gui.tk.DoubleVar(
102
+ value=min(max(gui.preferences.get_float("silent_speed", 4.0), 1.0), 10.0)
103
+ )
104
+ add_slider(
105
+ gui,
106
+ gui.basic_options_frame,
107
+ "Silent speed",
108
+ gui.silent_speed_var,
109
+ row=0,
110
+ setting_key="silent_speed",
111
+ minimum=1.0,
112
+ maximum=10.0,
113
+ resolution=0.5,
114
+ display_format="{:.1f}×",
115
+ default_value=4.0,
116
+ )
117
+
118
+ gui.sounded_speed_var = gui.tk.DoubleVar(
119
+ value=min(max(gui.preferences.get_float("sounded_speed", 1.0), 0.75), 2.0)
120
+ )
121
+ add_slider(
122
+ gui,
123
+ gui.basic_options_frame,
124
+ "Sounded speed",
125
+ gui.sounded_speed_var,
126
+ row=1,
127
+ setting_key="sounded_speed",
128
+ minimum=0.75,
129
+ maximum=2.0,
130
+ resolution=0.25,
131
+ display_format="{:.2f}×",
132
+ default_value=1.0,
133
+ )
134
+
135
+ gui.silent_threshold_var = gui.tk.DoubleVar(
136
+ value=min(max(gui.preferences.get_float("silent_threshold", 0.05), 0.0), 1.0)
137
+ )
138
+ add_slider(
139
+ gui,
140
+ gui.basic_options_frame,
141
+ "Silent threshold",
142
+ gui.silent_threshold_var,
143
+ row=2,
144
+ setting_key="silent_threshold",
145
+ minimum=0.0,
146
+ maximum=1.0,
147
+ resolution=0.01,
148
+ display_format="{:.2f}",
149
+ default_value=0.05,
150
+ )
151
+
152
+ gui.ttk.Label(gui.basic_options_frame, text="Processing mode").grid(
153
+ row=3, column=0, sticky="w", pady=(8, 0)
154
+ )
155
+ mode_choice = gui.ttk.Frame(gui.basic_options_frame)
156
+ mode_choice.grid(row=3, column=1, sticky="w", pady=(8, 0))
157
+
158
+ gui.ttk.Radiobutton(
159
+ mode_choice,
160
+ text="Local",
161
+ value="local",
162
+ variable=gui.processing_mode_var,
163
+ ).pack(side=gui.tk.LEFT, padx=(0, 8))
164
+
165
+ gui.remote_mode_button = gui.ttk.Radiobutton(
166
+ mode_choice,
167
+ text="Remote",
168
+ value="remote",
169
+ variable=gui.processing_mode_var,
170
+ )
171
+ gui.remote_mode_button.pack(side=gui.tk.LEFT, padx=(0, 8))
172
+
173
+ gui.ttk.Label(gui.basic_options_frame, text="Server URL").grid(
174
+ row=4, column=0, sticky="w", pady=(8, 0)
175
+ )
176
+ gui.server_entry = gui.ttk.Entry(
177
+ gui.basic_options_frame,
178
+ textvariable=gui.server_url_var,
179
+ width=40,
180
+ )
181
+ gui.server_entry.grid(row=4, column=1, sticky="ew", pady=(8, 0))
182
+
183
+ gui.server_discover_button = gui.ttk.Button(
184
+ gui.basic_options_frame, text="Discover", command=gui._start_discovery
185
+ )
186
+ gui.server_discover_button.grid(row=4, column=2, padx=(8, 0))
187
+
188
+ gui.ttk.Label(gui.basic_options_frame, text="Theme").grid(
189
+ row=5, column=0, sticky="w", pady=(8, 0)
190
+ )
191
+ theme_choice = gui.ttk.Frame(gui.basic_options_frame)
192
+ theme_choice.grid(row=5, column=1, columnspan=2, sticky="w", pady=(8, 0))
193
+ for value, label in ("os", "OS"), ("light", "Light"), ("dark", "Dark"):
194
+ gui.ttk.Radiobutton(
195
+ theme_choice,
196
+ text=label,
197
+ value=value,
198
+ variable=gui.theme_var,
199
+ command=gui._refresh_theme,
200
+ ).pack(side=gui.tk.LEFT, padx=(0, 8))
201
+
202
+ gui.advanced_button = gui.ttk.Button(
203
+ gui.options_frame,
204
+ text="Advanced",
205
+ command=gui._toggle_advanced,
206
+ )
207
+ gui.advanced_button.grid(row=3, column=0, columnspan=2, sticky="w", pady=(12, 0))
208
+
209
+ gui.advanced_frame = gui.ttk.Frame(gui.options_frame, padding=0)
210
+ gui.advanced_frame.grid(row=4, column=0, columnspan=2, sticky="nsew")
211
+ gui.advanced_frame.columnconfigure(1, weight=1)
212
+
213
+ gui.output_var = gui.tk.StringVar()
214
+ add_entry(
215
+ gui,
216
+ gui.advanced_frame,
217
+ "Output file",
218
+ gui.output_var,
219
+ row=0,
220
+ browse=True,
221
+ )
222
+
223
+ gui.temp_var = gui.tk.StringVar(value=str(default_temp_folder()))
224
+ add_entry(
225
+ gui,
226
+ gui.advanced_frame,
227
+ "Temp folder",
228
+ gui.temp_var,
229
+ row=1,
230
+ browse=True,
231
+ )
232
+
233
+ gui.sample_rate_var = gui.tk.StringVar(value="48000")
234
+ add_entry(gui, gui.advanced_frame, "Sample rate", gui.sample_rate_var, row=2)
235
+
236
+ frame_margin_setting = gui.preferences.get("frame_margin", 2)
237
+ try:
238
+ frame_margin_default = int(frame_margin_setting)
239
+ except (TypeError, ValueError):
240
+ frame_margin_default = 2
241
+ gui.preferences.update("frame_margin", frame_margin_default)
242
+
243
+ gui.frame_margin_var = gui.tk.StringVar(value=str(frame_margin_default))
244
+ add_entry(gui, gui.advanced_frame, "Frame margin", gui.frame_margin_var, row=3)
245
+
246
+ gui._toggle_advanced(initial=True)
247
+ gui._update_processing_mode_state()
248
+ update_basic_reset_state(gui)
249
+
250
+ # Action buttons and log output
251
+ status_frame = gui.ttk.Frame(main, padding=gui.PADDING)
252
+ status_frame.grid(row=1, column=0, sticky="ew")
253
+ status_frame.columnconfigure(0, weight=0)
254
+ status_frame.columnconfigure(1, weight=1)
255
+ status_frame.columnconfigure(2, weight=0)
256
+ gui.status_frame = status_frame
257
+
258
+ gui.ttk.Label(status_frame, text="Status:").grid(row=0, column=0, sticky="w")
259
+ gui.status_label = gui.tk.Label(
260
+ status_frame, textvariable=gui.status_var, anchor="e"
261
+ )
262
+ gui.status_label.grid(row=0, column=1, sticky="e")
263
+
264
+ # Progress bar
265
+ gui.progress_bar = gui.ttk.Progressbar(
266
+ status_frame,
267
+ variable=gui.progress_var,
268
+ maximum=100,
269
+ mode="determinate",
270
+ style="Idle.Horizontal.TProgressbar",
271
+ )
272
+ gui.progress_bar.grid(row=1, column=0, columnspan=3, sticky="ew", pady=(0, 0))
273
+
274
+ gui.stop_button = gui.ttk.Button(
275
+ status_frame, text="Stop", command=gui._stop_processing
276
+ )
277
+ gui.stop_button.grid(row=2, column=0, columnspan=3, sticky="ew", pady=gui.PADDING)
278
+ gui.stop_button.grid_remove() # Hidden by default
279
+
280
+ gui.open_button = gui.ttk.Button(
281
+ status_frame,
282
+ text="Open last",
283
+ command=gui._open_last_output,
284
+ state=gui.tk.DISABLED,
285
+ )
286
+ gui.open_button.grid(row=2, column=0, columnspan=3, sticky="ew", pady=gui.PADDING)
287
+ gui.open_button.grid_remove()
288
+
289
+ # Button shown when no other action buttons are visible
290
+ gui.drop_hint_button = gui.ttk.Button(
291
+ status_frame,
292
+ text="Drop video to convert",
293
+ state=gui.tk.DISABLED,
294
+ )
295
+ gui.drop_hint_button.grid(
296
+ row=2, column=0, columnspan=3, sticky="ew", pady=gui.PADDING
297
+ )
298
+ gui.drop_hint_button.grid_remove() # Hidden by default
299
+ gui._configure_drop_targets(gui.drop_hint_button)
300
+
301
+ gui.log_frame = gui.ttk.Frame(main, padding=gui.PADDING)
302
+ gui.log_frame.grid(row=3, column=0, pady=(16, 0), sticky="nsew")
303
+ main.rowconfigure(4, weight=1)
304
+ gui.log_frame.columnconfigure(0, weight=1)
305
+ gui.log_frame.rowconfigure(0, weight=1)
306
+
307
+ gui.log_text = gui.tk.Text(
308
+ gui.log_frame, wrap="word", height=10, state=gui.tk.DISABLED
309
+ )
310
+ gui.log_text.grid(row=0, column=0, sticky="nsew")
311
+ log_scroll = gui.ttk.Scrollbar(
312
+ gui.log_frame, orient=gui.tk.VERTICAL, command=gui.log_text.yview
313
+ )
314
+ log_scroll.grid(row=0, column=1, sticky="ns")
315
+ gui.log_text.configure(yscrollcommand=log_scroll.set)
316
+
317
+
318
+ def add_entry(
319
+ gui: "TalksReducerGUI",
320
+ parent: "tk.Misc",
321
+ label: str,
322
+ variable: "tk.StringVar",
323
+ *,
324
+ row: int,
325
+ browse: bool = False,
326
+ ) -> None:
327
+ """Add a labeled entry widget to the given *parent* container."""
328
+
329
+ gui.ttk.Label(parent, text=label).grid(row=row, column=0, sticky="w", pady=4)
330
+ entry = gui.ttk.Entry(parent, textvariable=variable)
331
+ entry.grid(row=row, column=1, sticky="ew", pady=4)
332
+ if browse:
333
+ button = gui.ttk.Button(
334
+ parent,
335
+ text="Browse",
336
+ command=lambda var=variable: gui._browse_path(var, label),
337
+ )
338
+ button.grid(row=row, column=2, padx=(8, 0))
339
+
340
+
341
+ def add_slider(
342
+ gui: "TalksReducerGUI",
343
+ parent: "tk.Misc",
344
+ label: str,
345
+ variable: "tk.DoubleVar",
346
+ *,
347
+ row: int,
348
+ setting_key: str,
349
+ minimum: float,
350
+ maximum: float,
351
+ resolution: float,
352
+ display_format: str,
353
+ default_value: float,
354
+ ) -> None:
355
+ """Add a labeled slider to the given *parent* container."""
356
+
357
+ gui.ttk.Label(parent, text=label).grid(row=row, column=0, sticky="w", pady=4)
358
+
359
+ value_label = gui.ttk.Label(parent)
360
+ value_label.grid(row=row, column=2, sticky="e", pady=4)
361
+
362
+ def update(value: str) -> None:
363
+ numeric = float(value)
364
+ clamped = max(minimum, min(maximum, numeric))
365
+ steps = round((clamped - minimum) / resolution)
366
+ quantized = minimum + steps * resolution
367
+ if abs(variable.get() - quantized) > 1e-9:
368
+ variable.set(quantized)
369
+ value_label.configure(text=display_format.format(quantized))
370
+ gui.preferences.update(setting_key, float(f"{quantized:.6f}"))
371
+ update_basic_reset_state(gui)
372
+
373
+ slider = gui.tk.Scale(
374
+ parent,
375
+ variable=variable,
376
+ from_=minimum,
377
+ to=maximum,
378
+ orient=gui.tk.HORIZONTAL,
379
+ resolution=resolution,
380
+ showvalue=False,
381
+ command=update,
382
+ length=240,
383
+ highlightthickness=0,
384
+ )
385
+ slider.grid(row=row, column=1, sticky="ew", pady=4, padx=(0, 8))
386
+
387
+ update(str(variable.get()))
388
+
389
+ gui._slider_updaters[setting_key] = update
390
+ gui._basic_defaults[setting_key] = default_value
391
+ gui._basic_variables[setting_key] = variable
392
+ variable.trace_add("write", lambda *_: update_basic_reset_state(gui))
393
+ gui._sliders.append(slider)
394
+
395
+
396
+ def update_basic_reset_state(gui: "TalksReducerGUI") -> None:
397
+ """Enable or disable the reset control based on slider values."""
398
+
399
+ if not hasattr(gui, "reset_basic_button"):
400
+ return
401
+
402
+ should_enable = False
403
+ for key, default_value in gui._basic_defaults.items():
404
+ variable = gui._basic_variables.get(key)
405
+ if variable is None:
406
+ continue
407
+ try:
408
+ current_value = float(variable.get())
409
+ except (TypeError, ValueError):
410
+ should_enable = True
411
+ break
412
+ if abs(current_value - default_value) > 1e-9:
413
+ should_enable = True
414
+ break
415
+
416
+ if should_enable:
417
+ if not getattr(gui, "_reset_button_visible", False):
418
+ gui.reset_basic_button.pack(side=gui.tk.LEFT, padx=(8, 0))
419
+ gui._reset_button_visible = True
420
+ gui.reset_basic_button.configure(state=gui.tk.NORMAL)
421
+ else:
422
+ if getattr(gui, "_reset_button_visible", False):
423
+ gui.reset_basic_button.pack_forget()
424
+ gui._reset_button_visible = False
425
+ gui.reset_basic_button.configure(state=gui.tk.DISABLED)
426
+
427
+
428
+ def reset_basic_defaults(gui: "TalksReducerGUI") -> None:
429
+ """Restore the basic numeric controls to their default values."""
430
+
431
+ for key, default_value in gui._basic_defaults.items():
432
+ variable = gui._basic_variables.get(key)
433
+ if variable is None:
434
+ continue
435
+
436
+ try:
437
+ current_value = float(variable.get())
438
+ except (TypeError, ValueError):
439
+ current_value = default_value
440
+
441
+ if abs(current_value - default_value) <= 1e-9:
442
+ continue
443
+
444
+ variable.set(default_value)
445
+ updater: Callable[[str], None] | None = gui._slider_updaters.get(key)
446
+ if updater is not None:
447
+ updater(str(default_value))
448
+ else:
449
+ gui.preferences.update(key, float(f"{default_value:.6f}"))
450
+
451
+ update_basic_reset_state(gui)
452
+
453
+
454
+ def apply_window_icon(gui: "TalksReducerGUI") -> None:
455
+ """Configure the application icon when the asset is available."""
456
+
457
+ base_path = Path(getattr(sys, "_MEIPASS", Path(__file__).resolve().parent.parent))
458
+
459
+ icon_candidates: list[tuple[Path, str]] = []
460
+ if sys.platform.startswith("win"):
461
+ icon_candidates.append(
462
+ (
463
+ base_path / "talks_reducer" / "resources" / "icons" / "icon.ico",
464
+ "ico",
465
+ )
466
+ )
467
+ icon_candidates.append(
468
+ (
469
+ base_path / "talks_reducer" / "resources" / "icons" / "icon.png",
470
+ "png",
471
+ )
472
+ )
473
+
474
+ for icon_path, icon_type in icon_candidates:
475
+ if not icon_path.is_file():
476
+ continue
477
+
478
+ try:
479
+ if icon_type == "ico" and sys.platform.startswith("win"):
480
+ # On Windows, iconbitmap works better without the 'default' parameter
481
+ gui.root.iconbitmap(str(icon_path))
482
+ else:
483
+ gui.root.iconphoto(False, gui.tk.PhotoImage(file=str(icon_path)))
484
+ return
485
+ except (gui.tk.TclError, Exception):
486
+ # Missing Tk image support or invalid icon format - try next candidate
487
+ continue
488
+
489
+
490
+ def apply_window_size(gui: "TalksReducerGUI", *, simple: bool) -> None:
491
+ """Apply the appropriate window geometry for the current mode."""
492
+
493
+ width, height = gui._simple_size if simple else gui._full_size
494
+ gui.root.update_idletasks()
495
+ gui.root.minsize(width, height)
496
+ if simple:
497
+ gui.root.geometry(f"{width}x{height}")
498
+ else:
499
+ current_width = gui.root.winfo_width()
500
+ current_height = gui.root.winfo_height()
501
+ if current_width < width or current_height < height:
502
+ gui.root.geometry(f"{width}x{height}")
503
+
504
+
505
+ def apply_simple_mode(gui: "TalksReducerGUI", *, initial: bool = False) -> None:
506
+ """Toggle between simple and full layouts."""
507
+
508
+ simple = gui.simple_mode_var.get()
509
+ if simple:
510
+ gui.basic_options_frame.grid_remove()
511
+ gui.log_frame.grid_remove()
512
+ gui.advanced_button.grid_remove()
513
+ gui.advanced_frame.grid_remove()
514
+ gui.run_after_drop_var.set(True)
515
+ apply_window_size(gui, simple=True)
516
+ else:
517
+ gui.basic_options_frame.grid()
518
+ gui.log_frame.grid()
519
+ gui.advanced_button.grid()
520
+ if gui.advanced_visible.get():
521
+ gui.advanced_frame.grid()
522
+ apply_window_size(gui, simple=False)
523
+
524
+ if initial and simple:
525
+ # Ensure the hidden widgets do not retain focus outlines on start.
526
+ gui.drop_zone.focus_set()