TonieToolbox 0.6.4__py3-none-any.whl → 0.6.5__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.
- TonieToolbox/__init__.py +1 -1
- TonieToolbox/__main__.py +4 -4
- TonieToolbox/integration_kde.py +2 -2
- TonieToolbox/player_gui.py +1212 -0
- TonieToolbox/tonie_header_pb2.py +26 -87
- {tonietoolbox-0.6.4.dist-info → tonietoolbox-0.6.5.dist-info}/METADATA +4 -13
- {tonietoolbox-0.6.4.dist-info → tonietoolbox-0.6.5.dist-info}/RECORD +11 -10
- {tonietoolbox-0.6.4.dist-info → tonietoolbox-0.6.5.dist-info}/WHEEL +0 -0
- {tonietoolbox-0.6.4.dist-info → tonietoolbox-0.6.5.dist-info}/entry_points.txt +0 -0
- {tonietoolbox-0.6.4.dist-info → tonietoolbox-0.6.5.dist-info}/licenses/LICENSE.md +0 -0
- {tonietoolbox-0.6.4.dist-info → tonietoolbox-0.6.5.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,1212 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
GUI Player Module for TonieToolbox
|
|
4
|
+
|
|
5
|
+
This module provides a minimal tkinter-based GUI for playing TAF (Tonie Audio Format) files.
|
|
6
|
+
It wraps the existing TAFPlayer functionality in a user-friendly graphical interface.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
import time
|
|
12
|
+
import threading
|
|
13
|
+
from typing import Optional, Dict, Any
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from .logger import get_logger
|
|
17
|
+
from .player import TAFPlayer, TAFPlayerError
|
|
18
|
+
from .constants import ICON_BASE64
|
|
19
|
+
from . import __version__
|
|
20
|
+
from .tonie_analysis import split_to_opus_files, extract_to_mp3_files, extract_full_audio_to_mp3
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
import tkinter as tk
|
|
24
|
+
from tkinter import ttk, messagebox, filedialog
|
|
25
|
+
import base64
|
|
26
|
+
import io
|
|
27
|
+
TKINTER_AVAILABLE = True
|
|
28
|
+
except ImportError as e:
|
|
29
|
+
TKINTER_AVAILABLE = False
|
|
30
|
+
TKINTER_ERROR = str(e)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
logger = get_logger(__name__)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class TAFPlayerGUI:
|
|
37
|
+
"""
|
|
38
|
+
A minimal GUI player for TAF files using tkinter.
|
|
39
|
+
|
|
40
|
+
This player provides a simple graphical interface with basic playback controls,
|
|
41
|
+
progress tracking, and file information display.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(self):
|
|
45
|
+
"""Initialize the GUI player."""
|
|
46
|
+
self.root = tk.Tk()
|
|
47
|
+
self.root.title("🎵 TonieToolbox - TAF Player")
|
|
48
|
+
optimal_width = 1024
|
|
49
|
+
optimal_height = 1500
|
|
50
|
+
|
|
51
|
+
self.root.geometry(f"{optimal_width}x{optimal_height}")
|
|
52
|
+
self.root.resizable(True, True)
|
|
53
|
+
self.root.minsize(1024, 768)
|
|
54
|
+
self._center_window(optimal_width, optimal_height)
|
|
55
|
+
self.root.lift()
|
|
56
|
+
self.root.attributes('-topmost', True)
|
|
57
|
+
self.root.after_idle(lambda: self.root.attributes('-topmost', False))
|
|
58
|
+
self.colors = {
|
|
59
|
+
'bg_dark': '#2b2b2b',
|
|
60
|
+
'bg_medium': '#3c3c3c',
|
|
61
|
+
'bg_light': '#4a4a4a',
|
|
62
|
+
'accent': '#ff6b35',
|
|
63
|
+
'accent_hover': '#ff8c5a',
|
|
64
|
+
'text_light': '#ffffff',
|
|
65
|
+
'text_medium': '#cccccc',
|
|
66
|
+
'success': '#4ade80',
|
|
67
|
+
'warning': '#fbbf24',
|
|
68
|
+
'error': '#f87171'
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
self.root.configure(bg=self.colors['bg_dark'])
|
|
72
|
+
self._set_window_icon()
|
|
73
|
+
self._configure_styles()
|
|
74
|
+
self.player: Optional[TAFPlayer] = None
|
|
75
|
+
self.current_file: Optional[Path] = None
|
|
76
|
+
self.is_playing = False
|
|
77
|
+
self.is_paused = False
|
|
78
|
+
self.update_thread: Optional[threading.Thread] = None
|
|
79
|
+
self.stop_updates = threading.Event()
|
|
80
|
+
self.progress_var = tk.DoubleVar()
|
|
81
|
+
self.current_time_var = tk.StringVar(value="00:00")
|
|
82
|
+
self.total_time_var = tk.StringVar(value="00:00")
|
|
83
|
+
self.file_name_var = tk.StringVar(value="No file loaded")
|
|
84
|
+
self.status_var = tk.StringVar(value="🔄 Ready to load TAF file")
|
|
85
|
+
self.selected_input_file: Optional[str] = None
|
|
86
|
+
self._create_widgets()
|
|
87
|
+
self._setup_layout()
|
|
88
|
+
self.root.protocol("WM_DELETE_WINDOW", self._on_close)
|
|
89
|
+
|
|
90
|
+
logger.info("TAF Player GUI initialized")
|
|
91
|
+
|
|
92
|
+
def _center_window(self, width: int, height: int):
|
|
93
|
+
"""Center the window on the screen."""
|
|
94
|
+
try:
|
|
95
|
+
screen_width = self.root.winfo_screenwidth()
|
|
96
|
+
screen_height = self.root.winfo_screenheight()
|
|
97
|
+
x = (screen_width - width) // 2
|
|
98
|
+
y = (screen_height - height) // 2
|
|
99
|
+
x = max(0, x)
|
|
100
|
+
y = max(0, y)
|
|
101
|
+
self.root.geometry(f"{width}x{height}+{x}+{y}")
|
|
102
|
+
logger.debug(f"Window centered at {x},{y} with size {width}x{height}")
|
|
103
|
+
|
|
104
|
+
except Exception as e:
|
|
105
|
+
logger.debug(f"Could not center window: {e}")
|
|
106
|
+
|
|
107
|
+
def _configure_styles(self):
|
|
108
|
+
"""Configure modern dark theme styles for ttk widgets."""
|
|
109
|
+
style = ttk.Style()
|
|
110
|
+
style.theme_use('clam')
|
|
111
|
+
style.configure('Dark.TFrame',
|
|
112
|
+
background=self.colors['bg_dark'],
|
|
113
|
+
borderwidth=0)
|
|
114
|
+
style.configure('Dark.TLabelframe',
|
|
115
|
+
background=self.colors['bg_medium'],
|
|
116
|
+
borderwidth=2,
|
|
117
|
+
relief='raised')
|
|
118
|
+
style.configure('Dark.TLabelframe.Label',
|
|
119
|
+
background=self.colors['bg_medium'],
|
|
120
|
+
foreground=self.colors['text_light'],
|
|
121
|
+
font=('Arial', 11, 'bold'))
|
|
122
|
+
style.configure('Accent.TButton',
|
|
123
|
+
background=self.colors['accent'],
|
|
124
|
+
foreground=self.colors['text_light'],
|
|
125
|
+
borderwidth=0,
|
|
126
|
+
focuscolor='none',
|
|
127
|
+
padding=(20, 10),
|
|
128
|
+
font=('Arial', 10, 'bold'))
|
|
129
|
+
style.map('Accent.TButton',
|
|
130
|
+
background=[('active', self.colors['accent_hover']),
|
|
131
|
+
('pressed', self.colors['accent'])])
|
|
132
|
+
style.configure('Large.TButton',
|
|
133
|
+
background=self.colors['bg_light'],
|
|
134
|
+
foreground=self.colors['text_light'],
|
|
135
|
+
borderwidth=1,
|
|
136
|
+
relief='raised',
|
|
137
|
+
focuscolor='none',
|
|
138
|
+
padding=(25, 15),
|
|
139
|
+
font=('Arial', 12, 'bold'))
|
|
140
|
+
style.map('Large.TButton',
|
|
141
|
+
background=[('active', self.colors['accent']),
|
|
142
|
+
('pressed', self.colors['bg_medium']),
|
|
143
|
+
('disabled', '#666666')])
|
|
144
|
+
style.configure('Title.TLabel',
|
|
145
|
+
background=self.colors['bg_dark'],
|
|
146
|
+
foreground=self.colors['text_light'],
|
|
147
|
+
font=('Arial', 14, 'bold'))
|
|
148
|
+
style.configure('Text.TLabel',
|
|
149
|
+
background=self.colors['bg_dark'],
|
|
150
|
+
foreground=self.colors['text_light'])
|
|
151
|
+
style.configure('Info.TLabel',
|
|
152
|
+
background=self.colors['bg_medium'],
|
|
153
|
+
foreground='#ffffff',
|
|
154
|
+
font=('Arial', 10))
|
|
155
|
+
style.configure('Time.TLabel',
|
|
156
|
+
background=self.colors['bg_dark'],
|
|
157
|
+
foreground=self.colors['accent'],
|
|
158
|
+
font=('Consolas', 11, 'bold'))
|
|
159
|
+
style.configure('Status.TLabel',
|
|
160
|
+
background=self.colors['bg_dark'],
|
|
161
|
+
foreground=self.colors['success'],
|
|
162
|
+
font=('Arial', 9))
|
|
163
|
+
style.configure('Title.TLabel',
|
|
164
|
+
background=self.colors['bg_dark'],
|
|
165
|
+
foreground=self.colors['text_light'],
|
|
166
|
+
font=('Arial', 32, 'bold'))
|
|
167
|
+
style.configure('Modern.Horizontal.TScale',
|
|
168
|
+
background=self.colors['bg_medium'],
|
|
169
|
+
troughcolor=self.colors['bg_light'],
|
|
170
|
+
borderwidth=0,
|
|
171
|
+
lightcolor=self.colors['accent'],
|
|
172
|
+
darkcolor=self.colors['accent'])
|
|
173
|
+
style.configure('Dark.TNotebook',
|
|
174
|
+
background=self.colors['bg_dark'],
|
|
175
|
+
borderwidth=0)
|
|
176
|
+
style.configure('Dark.TNotebook.Tab',
|
|
177
|
+
background=self.colors['bg_medium'],
|
|
178
|
+
foreground=self.colors['text_medium'],
|
|
179
|
+
padding=(20, 10),
|
|
180
|
+
font=('Arial', 10, 'bold'))
|
|
181
|
+
style.map('Dark.TNotebook.Tab',
|
|
182
|
+
background=[('selected', self.colors['bg_light']),
|
|
183
|
+
('active', self.colors['accent'])],
|
|
184
|
+
foreground=[('selected', self.colors['text_light']),
|
|
185
|
+
('active', self.colors['text_light'])])
|
|
186
|
+
style.configure('Dark.TRadiobutton',
|
|
187
|
+
background=self.colors['bg_medium'],
|
|
188
|
+
foreground=self.colors['text_medium'],
|
|
189
|
+
font=('Arial', 10),
|
|
190
|
+
focuscolor='none')
|
|
191
|
+
style.map('Dark.TRadiobutton',
|
|
192
|
+
background=[('active', self.colors['bg_light'])],
|
|
193
|
+
foreground=[('active', self.colors['text_light'])])
|
|
194
|
+
style.configure('Dark.TEntry',
|
|
195
|
+
background='#2d2d2d',
|
|
196
|
+
foreground='#ffffff',
|
|
197
|
+
borderwidth=2,
|
|
198
|
+
relief='solid',
|
|
199
|
+
insertcolor='#ffffff',
|
|
200
|
+
selectbackground=self.colors['accent'],
|
|
201
|
+
selectforeground='#ffffff')
|
|
202
|
+
style.map('Dark.TEntry',
|
|
203
|
+
focuscolor=[('focus', self.colors['accent'])],
|
|
204
|
+
bordercolor=[('focus', self.colors['accent'])])
|
|
205
|
+
style.configure('Modern.Horizontal.TProgressbar',
|
|
206
|
+
background=self.colors['accent'],
|
|
207
|
+
troughcolor=self.colors['bg_light'],
|
|
208
|
+
borderwidth=0,
|
|
209
|
+
lightcolor=self.colors['accent'],
|
|
210
|
+
darkcolor=self.colors['accent'])
|
|
211
|
+
|
|
212
|
+
def _set_window_icon(self):
|
|
213
|
+
"""Set the window icon using the base64 encoded icon from constants."""
|
|
214
|
+
try:
|
|
215
|
+
icon_data = base64.b64decode(ICON_BASE64)
|
|
216
|
+
logger.debug(f"Decoded icon data: {len(icon_data)} bytes")
|
|
217
|
+
is_ico = icon_data.startswith(b'\x00\x00\x01\x00')
|
|
218
|
+
success = False
|
|
219
|
+
if is_ico and not success:
|
|
220
|
+
success = self._try_ico_to_png_icon(icon_data)
|
|
221
|
+
if not success:
|
|
222
|
+
success = self._try_photoimage_icon()
|
|
223
|
+
if not success and is_ico:
|
|
224
|
+
success = self._try_iconbitmap_icon(icon_data)
|
|
225
|
+
if success:
|
|
226
|
+
logger.info("Successfully set window icon")
|
|
227
|
+
else:
|
|
228
|
+
logger.debug("Could not set window icon - using system default")
|
|
229
|
+
|
|
230
|
+
except Exception as e:
|
|
231
|
+
logger.debug(f"Failed to set window icon: {e}")
|
|
232
|
+
|
|
233
|
+
def _try_photoimage_icon(self):
|
|
234
|
+
"""Try setting icon using PhotoImage (for PNG/GIF formats)."""
|
|
235
|
+
try:
|
|
236
|
+
icon_image = tk.PhotoImage(data=ICON_BASE64)
|
|
237
|
+
self.root.iconphoto(True, icon_image)
|
|
238
|
+
logger.debug("Icon set using PhotoImage method")
|
|
239
|
+
return True
|
|
240
|
+
except tk.TclError as e:
|
|
241
|
+
logger.debug(f"PhotoImage method failed: {e}")
|
|
242
|
+
return False
|
|
243
|
+
|
|
244
|
+
def _try_iconbitmap_icon(self, icon_data):
|
|
245
|
+
"""Try setting icon using iconbitmap with temporary file."""
|
|
246
|
+
try:
|
|
247
|
+
import tempfile
|
|
248
|
+
with tempfile.NamedTemporaryFile(suffix='.ico', delete=False) as tmp_file:
|
|
249
|
+
tmp_file.write(icon_data)
|
|
250
|
+
tmp_file.flush()
|
|
251
|
+
self.root.iconbitmap(tmp_file.name)
|
|
252
|
+
logger.debug("Icon set using iconbitmap method")
|
|
253
|
+
os.unlink(tmp_file.name)
|
|
254
|
+
return True
|
|
255
|
+
except Exception as e:
|
|
256
|
+
logger.debug(f"iconbitmap method failed: {e}")
|
|
257
|
+
return False
|
|
258
|
+
|
|
259
|
+
def _try_ico_to_png_icon(self, icon_data):
|
|
260
|
+
"""Try extracting PNG from ICO and setting as PhotoImage."""
|
|
261
|
+
try:
|
|
262
|
+
png_start = icon_data.find(b'\x89PNG\r\n\x1a\n')
|
|
263
|
+
if png_start > 0:
|
|
264
|
+
png_data = icon_data[png_start:]
|
|
265
|
+
png_end = png_data.find(b'IEND') + 8
|
|
266
|
+
if png_end > 8:
|
|
267
|
+
png_data = png_data[:png_end]
|
|
268
|
+
png_b64 = base64.b64encode(png_data).decode('ascii')
|
|
269
|
+
icon_image = tk.PhotoImage(data=png_b64)
|
|
270
|
+
self.root.iconphoto(True, icon_image)
|
|
271
|
+
logger.debug("Icon set using PNG extraction from ICO")
|
|
272
|
+
return True
|
|
273
|
+
except Exception as e:
|
|
274
|
+
logger.debug(f"ICO to PNG extraction failed: {e}")
|
|
275
|
+
return False
|
|
276
|
+
|
|
277
|
+
def _create_widgets(self):
|
|
278
|
+
"""Create all GUI widgets with modern styling and tabbed interface."""
|
|
279
|
+
self.main_frame = ttk.Frame(self.root, style='Dark.TFrame', padding="20")
|
|
280
|
+
self.header_frame = ttk.Frame(self.main_frame, style='Dark.TFrame')
|
|
281
|
+
self.title_label = ttk.Label(
|
|
282
|
+
self.header_frame,
|
|
283
|
+
text="🎵 TonieToolbox TAF Player",
|
|
284
|
+
style='Text.TLabel'
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
self.notebook = ttk.Notebook(self.main_frame, style='Dark.TNotebook')
|
|
288
|
+
|
|
289
|
+
self._create_player_tab()
|
|
290
|
+
self._create_tools_tab()
|
|
291
|
+
self._create_about_tab()
|
|
292
|
+
|
|
293
|
+
def _create_player_tab(self):
|
|
294
|
+
"""Create the main player tab with all playback controls."""
|
|
295
|
+
self.player_tab = ttk.Frame(self.notebook, style='Dark.TFrame', padding="15")
|
|
296
|
+
|
|
297
|
+
self.file_frame = ttk.LabelFrame(
|
|
298
|
+
self.player_tab,
|
|
299
|
+
text="📁 Audio File",
|
|
300
|
+
style='Dark.TLabelframe',
|
|
301
|
+
padding="20"
|
|
302
|
+
)
|
|
303
|
+
self.file_display_frame = ttk.Frame(self.file_frame, style='Dark.TFrame')
|
|
304
|
+
self.file_icon_label = ttk.Label(
|
|
305
|
+
self.file_display_frame,
|
|
306
|
+
text="🎵",
|
|
307
|
+
style='Text.TLabel'
|
|
308
|
+
)
|
|
309
|
+
self.file_label = ttk.Label(
|
|
310
|
+
self.file_display_frame,
|
|
311
|
+
textvariable=self.file_name_var,
|
|
312
|
+
style='Text.TLabel'
|
|
313
|
+
)
|
|
314
|
+
self.load_button = ttk.Button(
|
|
315
|
+
self.file_frame,
|
|
316
|
+
text="📂 Load TAF File",
|
|
317
|
+
command=self._load_file,
|
|
318
|
+
style='Accent.TButton'
|
|
319
|
+
)
|
|
320
|
+
self.details_frame = ttk.Frame(self.file_frame, style='Dark.TFrame')
|
|
321
|
+
self.details_text = tk.Text(
|
|
322
|
+
self.details_frame,
|
|
323
|
+
height=10,
|
|
324
|
+
width=90,
|
|
325
|
+
state=tk.DISABLED,
|
|
326
|
+
wrap=tk.WORD,
|
|
327
|
+
bg=self.colors['bg_light'],
|
|
328
|
+
fg=self.colors['text_medium'],
|
|
329
|
+
font=('Consolas', 10),
|
|
330
|
+
borderwidth=0,
|
|
331
|
+
highlightthickness=0,
|
|
332
|
+
padx=15,
|
|
333
|
+
pady=10
|
|
334
|
+
)
|
|
335
|
+
self.details_scrollbar = ttk.Scrollbar(
|
|
336
|
+
self.details_frame,
|
|
337
|
+
orient=tk.VERTICAL,
|
|
338
|
+
command=self.details_text.yview
|
|
339
|
+
)
|
|
340
|
+
self.details_text.config(yscrollcommand=self.details_scrollbar.set)
|
|
341
|
+
self.controls_frame = ttk.LabelFrame(
|
|
342
|
+
self.player_tab,
|
|
343
|
+
text="🎮 Player Controls",
|
|
344
|
+
style='Dark.TLabelframe',
|
|
345
|
+
padding="20"
|
|
346
|
+
)
|
|
347
|
+
self.button_frame = ttk.Frame(self.controls_frame, style='Dark.TFrame')
|
|
348
|
+
self.play_button = ttk.Button(
|
|
349
|
+
self.button_frame,
|
|
350
|
+
text="▶️ Play",
|
|
351
|
+
command=self._toggle_play,
|
|
352
|
+
state=tk.DISABLED,
|
|
353
|
+
style='Large.TButton'
|
|
354
|
+
)
|
|
355
|
+
self.stop_button = ttk.Button(
|
|
356
|
+
self.button_frame,
|
|
357
|
+
text="⏹️ Stop",
|
|
358
|
+
command=self._stop_playback,
|
|
359
|
+
state=tk.DISABLED,
|
|
360
|
+
style='Large.TButton'
|
|
361
|
+
)
|
|
362
|
+
self.chapter_frame = ttk.Frame(self.controls_frame, style='Dark.TFrame')
|
|
363
|
+
self.prev_chapter_button = ttk.Button(
|
|
364
|
+
self.chapter_frame,
|
|
365
|
+
text="⏮️ Previous",
|
|
366
|
+
command=self._prev_chapter,
|
|
367
|
+
state=tk.DISABLED,
|
|
368
|
+
style='Accent.TButton'
|
|
369
|
+
)
|
|
370
|
+
self.next_chapter_button = ttk.Button(
|
|
371
|
+
self.chapter_frame,
|
|
372
|
+
text="⏭️ Next",
|
|
373
|
+
command=self._next_chapter,
|
|
374
|
+
state=tk.DISABLED,
|
|
375
|
+
style='Accent.TButton'
|
|
376
|
+
)
|
|
377
|
+
self.progress_section = ttk.Frame(self.controls_frame, style='Dark.TFrame')
|
|
378
|
+
self.time_frame = ttk.Frame(self.progress_section, style='Dark.TFrame')
|
|
379
|
+
self.current_time_label = ttk.Label(
|
|
380
|
+
self.time_frame,
|
|
381
|
+
textvariable=self.current_time_var,
|
|
382
|
+
style='Time.TLabel'
|
|
383
|
+
)
|
|
384
|
+
self.time_separator = ttk.Label(
|
|
385
|
+
self.time_frame,
|
|
386
|
+
text=" / ",
|
|
387
|
+
style='Time.TLabel'
|
|
388
|
+
)
|
|
389
|
+
self.total_time_label = ttk.Label(
|
|
390
|
+
self.time_frame,
|
|
391
|
+
textvariable=self.total_time_var,
|
|
392
|
+
style='Time.TLabel'
|
|
393
|
+
)
|
|
394
|
+
self.progress_bar = ttk.Scale(
|
|
395
|
+
self.progress_section,
|
|
396
|
+
from_=0,
|
|
397
|
+
to=100,
|
|
398
|
+
orient=tk.HORIZONTAL,
|
|
399
|
+
variable=self.progress_var,
|
|
400
|
+
command=self._on_seek,
|
|
401
|
+
style='Modern.Horizontal.TScale',
|
|
402
|
+
length=800
|
|
403
|
+
)
|
|
404
|
+
self.status_frame = ttk.Frame(self.player_tab, style='Dark.TFrame')
|
|
405
|
+
self.status_icon = ttk.Label(
|
|
406
|
+
self.status_frame,
|
|
407
|
+
text="ℹ️",
|
|
408
|
+
style='Status.TLabel'
|
|
409
|
+
)
|
|
410
|
+
self.status_label = ttk.Label(
|
|
411
|
+
self.status_frame,
|
|
412
|
+
textvariable=self.status_var,
|
|
413
|
+
style='Status.TLabel'
|
|
414
|
+
)
|
|
415
|
+
self.notebook.add(self.player_tab, text='🎵 Player')
|
|
416
|
+
|
|
417
|
+
def _create_about_tab(self):
|
|
418
|
+
"""Create the About tab with TonieToolbox information."""
|
|
419
|
+
self.about_tab = ttk.Frame(self.notebook, style='Dark.TFrame', padding="15")
|
|
420
|
+
self.notebook.add(self.about_tab, text='ℹ️ About')
|
|
421
|
+
self.about_container = ttk.Frame(self.about_tab, style='Dark.TFrame')
|
|
422
|
+
self.about_container.pack(fill=tk.BOTH, expand=True, padx=40, pady=40)
|
|
423
|
+
self.about_title = ttk.Label(
|
|
424
|
+
self.about_container,
|
|
425
|
+
text="TonieToolbox",
|
|
426
|
+
style='Title.TLabel',
|
|
427
|
+
font=('Arial', 32, 'bold')
|
|
428
|
+
)
|
|
429
|
+
self.logo_frame = ttk.Frame(self.about_container, style='Dark.TFrame')
|
|
430
|
+
try:
|
|
431
|
+
icon_data = base64.b64decode(ICON_BASE64)
|
|
432
|
+
logo_image = None
|
|
433
|
+
try:
|
|
434
|
+
logo_image = tk.PhotoImage(data=ICON_BASE64)
|
|
435
|
+
logo_image = logo_image.subsample(2, 2)
|
|
436
|
+
except tk.TclError:
|
|
437
|
+
png_start = icon_data.find(b'\x89PNG\r\n\x1a\n')
|
|
438
|
+
if png_start > 0:
|
|
439
|
+
png_data = icon_data[png_start:]
|
|
440
|
+
png_end = png_data.find(b'IEND') + 8
|
|
441
|
+
if png_end > 8:
|
|
442
|
+
png_data = png_data[:png_end]
|
|
443
|
+
png_b64 = base64.b64encode(png_data).decode('ascii')
|
|
444
|
+
logo_image = tk.PhotoImage(data=png_b64)
|
|
445
|
+
logo_image = logo_image.subsample(2, 2)
|
|
446
|
+
|
|
447
|
+
if logo_image:
|
|
448
|
+
self.logo_label = ttk.Label(
|
|
449
|
+
self.logo_frame,
|
|
450
|
+
image=logo_image,
|
|
451
|
+
style='Text.TLabel'
|
|
452
|
+
)
|
|
453
|
+
self.logo_label.image = logo_image
|
|
454
|
+
else:
|
|
455
|
+
self.logo_label = ttk.Label(
|
|
456
|
+
self.logo_frame,
|
|
457
|
+
text="🦜",
|
|
458
|
+
style='Text.TLabel',
|
|
459
|
+
font=('Arial', 48)
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
except Exception as e:
|
|
463
|
+
logger.debug(f"Failed to load logo: {e}")
|
|
464
|
+
self.logo_label = ttk.Label(
|
|
465
|
+
self.logo_frame,
|
|
466
|
+
text="🦜",
|
|
467
|
+
style='Text.TLabel',
|
|
468
|
+
font=('Arial', 48)
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
self.version_frame = ttk.Frame(self.about_container, style='Dark.TFrame')
|
|
472
|
+
self.version_label = ttk.Label(
|
|
473
|
+
self.version_frame,
|
|
474
|
+
text=f"📋 Version: {__version__}",
|
|
475
|
+
style='Text.TLabel',
|
|
476
|
+
font=('Arial', 14)
|
|
477
|
+
)
|
|
478
|
+
self.author_frame = ttk.Frame(self.about_container, style='Dark.TFrame')
|
|
479
|
+
self.author_label = ttk.Label(
|
|
480
|
+
self.author_frame,
|
|
481
|
+
text="👨💻 Author: Quentendo64",
|
|
482
|
+
style='Text.TLabel',
|
|
483
|
+
font=('Arial', 14)
|
|
484
|
+
)
|
|
485
|
+
self.desc_frame = ttk.Frame(self.about_container, style='Dark.TFrame')
|
|
486
|
+
description_text = (
|
|
487
|
+
"A comprehensive tool for working with Tonie audio files (TAF).\n"
|
|
488
|
+
"Features audio playback, file analysis, and media conversion."
|
|
489
|
+
)
|
|
490
|
+
self.desc_label = ttk.Label(
|
|
491
|
+
self.desc_frame,
|
|
492
|
+
text=description_text,
|
|
493
|
+
style='Text.TLabel',
|
|
494
|
+
font=('Arial', 12),
|
|
495
|
+
justify=tk.CENTER,
|
|
496
|
+
wraplength=600
|
|
497
|
+
)
|
|
498
|
+
self.repo_frame = ttk.Frame(self.about_container, style='Dark.TFrame')
|
|
499
|
+
self.repo_label = ttk.Label(
|
|
500
|
+
self.repo_frame,
|
|
501
|
+
text="🔗 Repository:",
|
|
502
|
+
style='Text.TLabel',
|
|
503
|
+
font=('Arial', 14)
|
|
504
|
+
)
|
|
505
|
+
self.repo_button = ttk.Button(
|
|
506
|
+
self.repo_frame,
|
|
507
|
+
text="🌐 GitHub Repository",
|
|
508
|
+
command=self._open_repository,
|
|
509
|
+
style='Accent.TButton'
|
|
510
|
+
)
|
|
511
|
+
self.pypi_button = ttk.Button(
|
|
512
|
+
self.repo_frame,
|
|
513
|
+
text="📦 PyPI Page",
|
|
514
|
+
command=self._open_pypi,
|
|
515
|
+
style='Accent.TButton'
|
|
516
|
+
)
|
|
517
|
+
self.license_frame = ttk.Frame(self.about_container, style='Dark.TFrame')
|
|
518
|
+
self.license_label = ttk.Label(
|
|
519
|
+
self.license_frame,
|
|
520
|
+
text="📄 Licensed under GPL3 - see repository for details",
|
|
521
|
+
style='Text.TLabel',
|
|
522
|
+
font=('Arial', 10),
|
|
523
|
+
foreground=self.colors['text_medium']
|
|
524
|
+
)
|
|
525
|
+
self.attribution_frame = ttk.Frame(self.about_container, style='Dark.TFrame')
|
|
526
|
+
self.attribution_label = ttk.Label(
|
|
527
|
+
self.attribution_frame,
|
|
528
|
+
text="Parrot Icon created by Freepik - Flaticon",
|
|
529
|
+
style='Title.TLabel',
|
|
530
|
+
font=('Arial', 8),
|
|
531
|
+
foreground=self.colors['text_medium']
|
|
532
|
+
)
|
|
533
|
+
self.attribution_button = ttk.Button(
|
|
534
|
+
self.attribution_frame,
|
|
535
|
+
text="🎨 Icon Attribution",
|
|
536
|
+
command=self._open_attribution_link,
|
|
537
|
+
style='Accent.TButton'
|
|
538
|
+
)
|
|
539
|
+
self.about_title.pack(pady=(0, 20))
|
|
540
|
+
self.logo_frame.pack(pady=(0, 30))
|
|
541
|
+
self.logo_label.pack()
|
|
542
|
+
|
|
543
|
+
self.version_frame.pack(pady=10, fill=tk.X)
|
|
544
|
+
self.version_label.pack()
|
|
545
|
+
|
|
546
|
+
self.author_frame.pack(pady=10, fill=tk.X)
|
|
547
|
+
self.author_label.pack()
|
|
548
|
+
|
|
549
|
+
self.desc_frame.pack(pady=20, fill=tk.X)
|
|
550
|
+
self.desc_label.pack()
|
|
551
|
+
|
|
552
|
+
self.repo_frame.pack(pady=20, fill=tk.X)
|
|
553
|
+
self.repo_label.pack(pady=(0, 10))
|
|
554
|
+
self.repo_button.pack(pady=10)
|
|
555
|
+
self.pypi_button.pack(pady=10)
|
|
556
|
+
self.license_frame.pack(pady=30, fill=tk.X)
|
|
557
|
+
self.license_label.pack()
|
|
558
|
+
self.attribution_frame.pack(pady=20, fill=tk.X)
|
|
559
|
+
self.attribution_label.pack(pady=(0, 5))
|
|
560
|
+
self.attribution_button.pack()
|
|
561
|
+
|
|
562
|
+
def _open_repository(self):
|
|
563
|
+
"""Open the TonieToolbox repository in web browser."""
|
|
564
|
+
import webbrowser
|
|
565
|
+
try:
|
|
566
|
+
webbrowser.open('https://github.com/Quentendo64/TonieToolbox')
|
|
567
|
+
self.status_var.set("🌐 Opening repository in browser...")
|
|
568
|
+
except Exception as e:
|
|
569
|
+
self.status_var.set(f"❌ Could not open browser: {str(e)}")
|
|
570
|
+
def _open_pypi(self):
|
|
571
|
+
"""Open the PyPI page for TonieToolbox in web browser."""
|
|
572
|
+
import webbrowser
|
|
573
|
+
try:
|
|
574
|
+
webbrowser.open('https://pypi.org/project/TonieToolbox/')
|
|
575
|
+
self.status_var.set("🌐 Opening PyPI page in browser...")
|
|
576
|
+
except Exception as e:
|
|
577
|
+
self.status_var.set(f"❌ Could not open browser: {str(e)}")
|
|
578
|
+
|
|
579
|
+
def _open_attribution_link(self):
|
|
580
|
+
"""Open the Flaticon attribution page in web browser."""
|
|
581
|
+
import webbrowser
|
|
582
|
+
try:
|
|
583
|
+
webbrowser.open('https://www.flaticon.com/free-animated-icons/parrot')
|
|
584
|
+
self.status_var.set("🎨 Opening icon attribution page...")
|
|
585
|
+
except Exception as e:
|
|
586
|
+
self.status_var.set(f"❌ Could not open browser: {str(e)}")
|
|
587
|
+
|
|
588
|
+
def _create_tools_tab(self):
|
|
589
|
+
"""Create the Tools tab with conversion and utility functions."""
|
|
590
|
+
self.tools_tab = ttk.Frame(self.notebook, style='Dark.TFrame', padding="15")
|
|
591
|
+
self.notebook.add(self.tools_tab, text='🔧 Tools')
|
|
592
|
+
self.tools_container = ttk.Frame(self.tools_tab, style='Dark.TFrame')
|
|
593
|
+
self.tools_container.pack(fill=tk.BOTH, expand=True, padx=20, pady=20)
|
|
594
|
+
self.tools_notebook = ttk.Notebook(self.tools_container, style='Dark.TNotebook')
|
|
595
|
+
self.tools_notebook.pack(fill=tk.BOTH, expand=True)
|
|
596
|
+
self._create_convert_tab()
|
|
597
|
+
self._create_analyze_tab()
|
|
598
|
+
if hasattr(self, 'selected_input_file') and self.selected_input_file:
|
|
599
|
+
self._analyze_selected_file()
|
|
600
|
+
|
|
601
|
+
def _create_convert_tab(self):
|
|
602
|
+
"""Create the Convert sub-tab with conversion options."""
|
|
603
|
+
self.convert_tab = ttk.Frame(self.tools_notebook, style='Dark.TFrame', padding="20")
|
|
604
|
+
self.tools_notebook.add(self.convert_tab, text='🔄 Convert')
|
|
605
|
+
self.input_section = ttk.LabelFrame(
|
|
606
|
+
self.convert_tab,
|
|
607
|
+
text="📁 Input File",
|
|
608
|
+
style='Dark.TLabelframe',
|
|
609
|
+
padding="15"
|
|
610
|
+
)
|
|
611
|
+
self.input_section.pack(fill=tk.X, pady=(0, 15))
|
|
612
|
+
self.input_file_frame = ttk.Frame(self.input_section, style='Dark.TFrame')
|
|
613
|
+
self.input_file_frame.pack(fill=tk.X, pady=(0, 10))
|
|
614
|
+
self.input_file_var = tk.StringVar()
|
|
615
|
+
if self.current_file:
|
|
616
|
+
self.input_file_var.set(self.current_file.name)
|
|
617
|
+
self.selected_input_file = str(self.current_file)
|
|
618
|
+
else:
|
|
619
|
+
self.input_file_var.set("No file selected")
|
|
620
|
+
self.input_file_label = ttk.Label(
|
|
621
|
+
self.input_file_frame,
|
|
622
|
+
textvariable=self.input_file_var,
|
|
623
|
+
style='Info.TLabel'
|
|
624
|
+
)
|
|
625
|
+
self.input_file_label.pack(side=tk.LEFT, fill=tk.X, expand=True)
|
|
626
|
+
self.browse_button = ttk.Button(
|
|
627
|
+
self.input_file_frame,
|
|
628
|
+
text="📂 Browse File",
|
|
629
|
+
command=self._browse_input_file,
|
|
630
|
+
style='Accent.TButton'
|
|
631
|
+
)
|
|
632
|
+
self.browse_button.pack(side=tk.RIGHT, padx=(10, 0))
|
|
633
|
+
self.convert_options = ttk.LabelFrame(
|
|
634
|
+
self.convert_tab,
|
|
635
|
+
text="⚙️ Conversion Options",
|
|
636
|
+
style='Dark.TLabelframe',
|
|
637
|
+
padding="15"
|
|
638
|
+
)
|
|
639
|
+
self.convert_options.pack(fill=tk.X, pady=(0, 15))
|
|
640
|
+
self.convert_type_frame = ttk.Frame(self.convert_options, style='Dark.TFrame')
|
|
641
|
+
self.convert_type_frame.pack(fill=tk.X, pady=(0, 15))
|
|
642
|
+
ttk.Label(
|
|
643
|
+
self.convert_type_frame,
|
|
644
|
+
text="Convert to:",
|
|
645
|
+
style='Text.TLabel'
|
|
646
|
+
).pack(side=tk.LEFT)
|
|
647
|
+
|
|
648
|
+
self.convert_type_var = tk.StringVar(value="separate_mp3")
|
|
649
|
+
self.mp3_separate_radio = ttk.Radiobutton(
|
|
650
|
+
self.convert_type_frame,
|
|
651
|
+
text="🎵 Separate MP3 tracks",
|
|
652
|
+
variable=self.convert_type_var,
|
|
653
|
+
value="separate_mp3",
|
|
654
|
+
style='Dark.TRadiobutton'
|
|
655
|
+
)
|
|
656
|
+
self.mp3_separate_radio.pack(anchor=tk.W, pady=2)
|
|
657
|
+
self.mp3_single_radio = ttk.Radiobutton(
|
|
658
|
+
self.convert_type_frame,
|
|
659
|
+
text="🎵 Single MP3 file",
|
|
660
|
+
variable=self.convert_type_var,
|
|
661
|
+
value="single_mp3",
|
|
662
|
+
style='Dark.TRadiobutton'
|
|
663
|
+
)
|
|
664
|
+
self.mp3_single_radio.pack(anchor=tk.W, pady=2)
|
|
665
|
+
self.opus_radio = ttk.Radiobutton(
|
|
666
|
+
self.convert_type_frame,
|
|
667
|
+
text="🎶 Opus tracks",
|
|
668
|
+
variable=self.convert_type_var,
|
|
669
|
+
value="opus",
|
|
670
|
+
style='Dark.TRadiobutton'
|
|
671
|
+
)
|
|
672
|
+
self.opus_radio.pack(anchor=tk.W, pady=2)
|
|
673
|
+
self.output_dir_frame = ttk.Frame(self.convert_options, style='Dark.TFrame')
|
|
674
|
+
self.output_dir_frame.pack(fill=tk.X, pady=(0, 15))
|
|
675
|
+
ttk.Label(
|
|
676
|
+
self.output_dir_frame,
|
|
677
|
+
text="Output directory:",
|
|
678
|
+
style='Text.TLabel'
|
|
679
|
+
).pack(anchor=tk.W)
|
|
680
|
+
|
|
681
|
+
self.output_dir_var = tk.StringVar()
|
|
682
|
+
self.output_dir_var.set(os.path.join(os.getcwd(), "output"))
|
|
683
|
+
self.output_dir_display_frame = ttk.Frame(self.output_dir_frame, style='Dark.TFrame')
|
|
684
|
+
self.output_dir_display_frame.pack(fill=tk.X, pady=(5, 0))
|
|
685
|
+
self.output_dir_label = ttk.Label(
|
|
686
|
+
self.output_dir_display_frame,
|
|
687
|
+
textvariable=self.output_dir_var,
|
|
688
|
+
style='Info.TLabel'
|
|
689
|
+
)
|
|
690
|
+
self.output_dir_label.pack(side=tk.LEFT, fill=tk.X, expand=True)
|
|
691
|
+
self.output_browse_button = ttk.Button(
|
|
692
|
+
self.output_dir_display_frame,
|
|
693
|
+
text="📁 Browse Folder",
|
|
694
|
+
command=self._browse_output_dir,
|
|
695
|
+
style='Accent.TButton'
|
|
696
|
+
)
|
|
697
|
+
self.output_browse_button.pack(side=tk.RIGHT, padx=(5, 0))
|
|
698
|
+
self.convert_action_frame = ttk.Frame(self.convert_options, style='Dark.TFrame')
|
|
699
|
+
self.convert_action_frame.pack(fill=tk.X)
|
|
700
|
+
self.convert_button = ttk.Button(
|
|
701
|
+
self.convert_action_frame,
|
|
702
|
+
text="🔄 Start Conversion",
|
|
703
|
+
command=self._start_conversion,
|
|
704
|
+
style='Large.TButton'
|
|
705
|
+
)
|
|
706
|
+
self.convert_button.pack(pady=10)
|
|
707
|
+
self.convert_progress = ttk.Progressbar(
|
|
708
|
+
self.convert_action_frame,
|
|
709
|
+
mode='indeterminate',
|
|
710
|
+
style='Modern.Horizontal.TProgressbar'
|
|
711
|
+
)
|
|
712
|
+
self.convert_progress.pack(fill=tk.X, pady=(10, 0))
|
|
713
|
+
self.convert_status_var = tk.StringVar()
|
|
714
|
+
self.convert_status_var.set("Ready to convert")
|
|
715
|
+
self.convert_status_label = ttk.Label(
|
|
716
|
+
self.convert_action_frame,
|
|
717
|
+
textvariable=self.convert_status_var,
|
|
718
|
+
style='Status.TLabel'
|
|
719
|
+
)
|
|
720
|
+
self.convert_status_label.pack(pady=(5, 0))
|
|
721
|
+
|
|
722
|
+
def _create_analyze_tab(self):
|
|
723
|
+
"""Create the Analyze sub-tab with file analysis tools."""
|
|
724
|
+
self.analyze_tab = ttk.Frame(self.tools_notebook, style='Dark.TFrame', padding="20")
|
|
725
|
+
self.tools_notebook.add(self.analyze_tab, text='🔍 Analyze')
|
|
726
|
+
self.analyze_info = ttk.LabelFrame(
|
|
727
|
+
self.analyze_tab,
|
|
728
|
+
text="📊 File Analysis",
|
|
729
|
+
style='Dark.TLabelframe',
|
|
730
|
+
padding="15"
|
|
731
|
+
)
|
|
732
|
+
self.analyze_info.pack(fill=tk.BOTH, expand=True)
|
|
733
|
+
self.analyze_text = tk.Text(
|
|
734
|
+
self.analyze_info,
|
|
735
|
+
height=20,
|
|
736
|
+
width=80,
|
|
737
|
+
state=tk.DISABLED,
|
|
738
|
+
wrap=tk.WORD,
|
|
739
|
+
bg=self.colors['bg_light'],
|
|
740
|
+
fg=self.colors['text_medium'],
|
|
741
|
+
font=('Consolas', 10),
|
|
742
|
+
borderwidth=0,
|
|
743
|
+
highlightthickness=0,
|
|
744
|
+
padx=15,
|
|
745
|
+
pady=10
|
|
746
|
+
)
|
|
747
|
+
self.analyze_scrollbar = ttk.Scrollbar(
|
|
748
|
+
self.analyze_info,
|
|
749
|
+
orient=tk.VERTICAL,
|
|
750
|
+
command=self.analyze_text.yview
|
|
751
|
+
)
|
|
752
|
+
self.analyze_text.config(yscrollcommand=self.analyze_scrollbar.set)
|
|
753
|
+
self.analyze_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
|
754
|
+
self.analyze_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
|
755
|
+
self._update_analyze_text("Select a TAF file to see detailed analysis information.")
|
|
756
|
+
|
|
757
|
+
def _browse_input_file(self):
|
|
758
|
+
"""Browse for input TAF file."""
|
|
759
|
+
file_path = filedialog.askopenfilename(
|
|
760
|
+
title="Select TAF File for Conversion",
|
|
761
|
+
filetypes=[("TAF files", "*.taf"), ("All files", "*.*")],
|
|
762
|
+
initialdir=os.getcwd()
|
|
763
|
+
)
|
|
764
|
+
|
|
765
|
+
if file_path:
|
|
766
|
+
self.input_file_var.set(os.path.basename(file_path))
|
|
767
|
+
self.selected_input_file = file_path
|
|
768
|
+
self._analyze_selected_file()
|
|
769
|
+
|
|
770
|
+
def _update_tools_input_file(self, file_path: str):
|
|
771
|
+
"""Update the Tools tab input file when a file is loaded in the Player tab."""
|
|
772
|
+
if hasattr(self, 'input_file_var') and hasattr(self, 'selected_input_file'):
|
|
773
|
+
self.input_file_var.set(os.path.basename(file_path))
|
|
774
|
+
self.selected_input_file = file_path
|
|
775
|
+
self._analyze_selected_file()
|
|
776
|
+
|
|
777
|
+
def _browse_output_dir(self):
|
|
778
|
+
"""Browse for output directory."""
|
|
779
|
+
dir_path = filedialog.askdirectory(
|
|
780
|
+
title="Select Output Directory",
|
|
781
|
+
initialdir=self.output_dir_var.get()
|
|
782
|
+
)
|
|
783
|
+
|
|
784
|
+
if dir_path:
|
|
785
|
+
self.output_dir_var.set(dir_path)
|
|
786
|
+
|
|
787
|
+
def _start_conversion(self):
|
|
788
|
+
"""Start the conversion process in a separate thread."""
|
|
789
|
+
if not hasattr(self, 'selected_input_file'):
|
|
790
|
+
messagebox.showerror("Error", "Please select an input TAF file first.")
|
|
791
|
+
return
|
|
792
|
+
|
|
793
|
+
if not os.path.exists(self.selected_input_file):
|
|
794
|
+
messagebox.showerror("Error", "Selected input file does not exist.")
|
|
795
|
+
return
|
|
796
|
+
output_dir = self.output_dir_var.get()
|
|
797
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
798
|
+
self.convert_button.config(state=tk.DISABLED)
|
|
799
|
+
self.convert_progress.start()
|
|
800
|
+
self.convert_status_var.set("Converting...")
|
|
801
|
+
threading.Thread(
|
|
802
|
+
target=self._perform_conversion,
|
|
803
|
+
daemon=True
|
|
804
|
+
).start()
|
|
805
|
+
|
|
806
|
+
def _perform_conversion(self):
|
|
807
|
+
"""Perform the actual conversion."""
|
|
808
|
+
try:
|
|
809
|
+
convert_type = self.convert_type_var.get()
|
|
810
|
+
input_file = self.selected_input_file
|
|
811
|
+
output_dir = self.output_dir_var.get()
|
|
812
|
+
base_name = os.path.splitext(os.path.basename(input_file))[0]
|
|
813
|
+
|
|
814
|
+
if convert_type == "separate_mp3":
|
|
815
|
+
extract_to_mp3_files(input_file, output_dir)
|
|
816
|
+
self.root.after(0, lambda: self.convert_status_var.set("✅ Converted to separate MP3 files"))
|
|
817
|
+
elif convert_type == "single_mp3":
|
|
818
|
+
output_file = os.path.join(output_dir, f"{base_name}.mp3")
|
|
819
|
+
extract_full_audio_to_mp3(input_file, output_file)
|
|
820
|
+
self.root.after(0, lambda: self.convert_status_var.set("✅ Converted to single MP3 file"))
|
|
821
|
+
elif convert_type == "opus":
|
|
822
|
+
split_to_opus_files(input_file, output_dir)
|
|
823
|
+
self.root.after(0, lambda: self.convert_status_var.set("✅ Converted to Opus files"))
|
|
824
|
+
|
|
825
|
+
except Exception as e:
|
|
826
|
+
error_msg = f"❌ Conversion failed: {str(e)}"
|
|
827
|
+
self.root.after(0, lambda: self.convert_status_var.set(error_msg))
|
|
828
|
+
logger.error(f"Conversion error: {e}")
|
|
829
|
+
|
|
830
|
+
finally:
|
|
831
|
+
self.root.after(0, self._conversion_finished)
|
|
832
|
+
|
|
833
|
+
def _conversion_finished(self):
|
|
834
|
+
"""Called when conversion is finished to update UI."""
|
|
835
|
+
self.convert_button.config(state=tk.NORMAL)
|
|
836
|
+
self.convert_progress.stop()
|
|
837
|
+
|
|
838
|
+
def _analyze_selected_file(self):
|
|
839
|
+
"""Analyze the selected file and update the analyze tab."""
|
|
840
|
+
if hasattr(self, 'selected_input_file'):
|
|
841
|
+
try:
|
|
842
|
+
from .tonie_analysis import check_tonie_file
|
|
843
|
+
file_info = check_tonie_file(self.selected_input_file)
|
|
844
|
+
analysis_text = f"""File Analysis: {os.path.basename(self.selected_input_file)}
|
|
845
|
+
{'='*60}
|
|
846
|
+
|
|
847
|
+
File Path: {self.selected_input_file}
|
|
848
|
+
File Size: {os.path.getsize(self.selected_input_file):,} bytes
|
|
849
|
+
|
|
850
|
+
{file_info}
|
|
851
|
+
|
|
852
|
+
Analysis completed at: {time.strftime('%Y-%m-%d %H:%M:%S')}
|
|
853
|
+
"""
|
|
854
|
+
|
|
855
|
+
self._update_analyze_text(analysis_text)
|
|
856
|
+
|
|
857
|
+
except Exception as e:
|
|
858
|
+
error_text = f"""Analysis Error
|
|
859
|
+
{'='*60}
|
|
860
|
+
|
|
861
|
+
Could not analyze file: {self.selected_input_file}
|
|
862
|
+
Error: {str(e)}
|
|
863
|
+
|
|
864
|
+
Please ensure the file is a valid TAF file.
|
|
865
|
+
"""
|
|
866
|
+
self._update_analyze_text(error_text)
|
|
867
|
+
logger.error(f"Analysis error: {e}")
|
|
868
|
+
|
|
869
|
+
def _update_analyze_text(self, text):
|
|
870
|
+
"""Update the analyze text widget with new content."""
|
|
871
|
+
self.analyze_text.config(state=tk.NORMAL)
|
|
872
|
+
self.analyze_text.delete(1.0, tk.END)
|
|
873
|
+
self.analyze_text.insert(1.0, text)
|
|
874
|
+
self.analyze_text.config(state=tk.DISABLED)
|
|
875
|
+
|
|
876
|
+
def _setup_layout(self):
|
|
877
|
+
"""Setup the modern layout with tabbed interface."""
|
|
878
|
+
self.main_frame.pack(fill=tk.BOTH, expand=True)
|
|
879
|
+
self.header_frame.pack(fill=tk.X, pady=(0, 20))
|
|
880
|
+
#self.title_label.pack()
|
|
881
|
+
self.notebook.pack(fill=tk.BOTH, expand=True, pady=(0, 20))
|
|
882
|
+
self._layout_player_tab()
|
|
883
|
+
|
|
884
|
+
def _layout_player_tab(self):
|
|
885
|
+
"""Layout widgets within the player tab."""
|
|
886
|
+
self.file_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 15))
|
|
887
|
+
self.file_display_frame.pack(fill=tk.X, pady=(0, 15))
|
|
888
|
+
self.file_icon_label.pack(side=tk.LEFT, padx=(0, 10))
|
|
889
|
+
self.file_label.pack(side=tk.LEFT, fill=tk.X, expand=True)
|
|
890
|
+
self.load_button.pack(pady=(0, 15))
|
|
891
|
+
self.details_frame.pack(fill=tk.BOTH, expand=True)
|
|
892
|
+
self.details_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
|
893
|
+
self.details_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
|
894
|
+
self.controls_frame.pack(fill=tk.X, pady=(0, 15))
|
|
895
|
+
self.button_frame.pack(pady=(0, 20))
|
|
896
|
+
self.play_button.pack(side=tk.LEFT, padx=(0, 15))
|
|
897
|
+
self.stop_button.pack(side=tk.LEFT)
|
|
898
|
+
# Hide chapter buttons - feature not is implemented yet
|
|
899
|
+
# self.chapter_frame.pack(pady=(0, 20))
|
|
900
|
+
# self.prev_chapter_button.pack(side=tk.LEFT, padx=(0, 15))
|
|
901
|
+
# self.next_chapter_button.pack(side=tk.LEFT)
|
|
902
|
+
self.progress_section.pack(fill=tk.X)
|
|
903
|
+
self.time_frame.pack(pady=(0, 10))
|
|
904
|
+
self.current_time_label.pack(side=tk.LEFT)
|
|
905
|
+
self.time_separator.pack(side=tk.LEFT)
|
|
906
|
+
self.total_time_label.pack(side=tk.LEFT)
|
|
907
|
+
self.progress_bar.pack(fill=tk.X, padx=40)
|
|
908
|
+
self.status_frame.pack(fill=tk.X, pady=(20, 0), padx=10)
|
|
909
|
+
self.status_icon.pack(side=tk.LEFT, padx=(0, 5))
|
|
910
|
+
self.status_label.pack(side=tk.LEFT, fill=tk.X, expand=True)
|
|
911
|
+
|
|
912
|
+
def _load_file(self):
|
|
913
|
+
"""Load a TAF file using file dialog."""
|
|
914
|
+
file_path = filedialog.askopenfilename(
|
|
915
|
+
title="Select TAF File",
|
|
916
|
+
filetypes=[("TAF files", "*.taf"), ("All files", "*.*")],
|
|
917
|
+
initialdir=os.getcwd()
|
|
918
|
+
)
|
|
919
|
+
|
|
920
|
+
if file_path:
|
|
921
|
+
self.load_taf_file(file_path)
|
|
922
|
+
|
|
923
|
+
def load_taf_file(self, file_path: str):
|
|
924
|
+
"""
|
|
925
|
+
Load a TAF file for playback.
|
|
926
|
+
|
|
927
|
+
Args:
|
|
928
|
+
file_path: Path to the TAF file to load
|
|
929
|
+
"""
|
|
930
|
+
try:
|
|
931
|
+
self.status_var.set("📂 Loading TAF file...")
|
|
932
|
+
self.root.update()
|
|
933
|
+
if self.player:
|
|
934
|
+
self._stop_playback()
|
|
935
|
+
self.player.cleanup()
|
|
936
|
+
self.player = TAFPlayer()
|
|
937
|
+
self.player.load(file_path)
|
|
938
|
+
|
|
939
|
+
self.current_file = Path(file_path)
|
|
940
|
+
self.file_name_var.set(self.current_file.name)
|
|
941
|
+
self._update_file_info()
|
|
942
|
+
self.play_button.config(state=tk.NORMAL)
|
|
943
|
+
self.stop_button.config(state=tk.NORMAL)
|
|
944
|
+
if self.player.taf_info and self.player.taf_info.get('chapters'):
|
|
945
|
+
self.prev_chapter_button.config(state=tk.NORMAL)
|
|
946
|
+
self.next_chapter_button.config(state=tk.NORMAL)
|
|
947
|
+
if self.player.total_duration > 0:
|
|
948
|
+
self.total_time_var.set(self._format_time(self.player.total_duration))
|
|
949
|
+
self.progress_bar.config(to=self.player.total_duration)
|
|
950
|
+
|
|
951
|
+
self.status_var.set(f"✅ Successfully loaded: {self.current_file.name}")
|
|
952
|
+
logger.info(f"Successfully loaded TAF file: {file_path}")
|
|
953
|
+
self._update_tools_input_file(file_path)
|
|
954
|
+
|
|
955
|
+
except TAFPlayerError as e:
|
|
956
|
+
messagebox.showerror("❌ Loading Error", f"Failed to load TAF file:\n\n{e}")
|
|
957
|
+
self.status_var.set("❌ Error loading file")
|
|
958
|
+
logger.error(f"Failed to load TAF file: {e}")
|
|
959
|
+
except Exception as e:
|
|
960
|
+
messagebox.showerror("❌ Unexpected Error", f"An unexpected error occurred:\n\n{e}")
|
|
961
|
+
self.status_var.set("❌ Unexpected error")
|
|
962
|
+
logger.error(f"Unexpected error loading TAF file: {e}")
|
|
963
|
+
|
|
964
|
+
def _update_file_info(self):
|
|
965
|
+
"""Update the file information display with enhanced formatting."""
|
|
966
|
+
if not self.player or not self.player.taf_info:
|
|
967
|
+
return
|
|
968
|
+
|
|
969
|
+
info = self.player.taf_info
|
|
970
|
+
info_lines = []
|
|
971
|
+
info_lines.append("📋 FILE INFORMATION")
|
|
972
|
+
info_lines.append("─" * 50)
|
|
973
|
+
|
|
974
|
+
if 'sha1_hash' in info and info['sha1_hash']:
|
|
975
|
+
info_lines.append(f"🔐 SHA1 Hash: {info['sha1_hash']}")
|
|
976
|
+
|
|
977
|
+
if 'sample_rate' in info:
|
|
978
|
+
info_lines.append(f"🎵 Sample Rate: {info['sample_rate']:,} Hz")
|
|
979
|
+
|
|
980
|
+
if 'channels' in info:
|
|
981
|
+
channel_text = "Stereo" if info['channels'] == 2 else f"{info['channels']} Channels"
|
|
982
|
+
info_lines.append(f"🔊 Audio: {channel_text}")
|
|
983
|
+
|
|
984
|
+
if 'bitrate' in info:
|
|
985
|
+
info_lines.append(f"📊 Bitrate: {info['bitrate']} kbps")
|
|
986
|
+
|
|
987
|
+
if self.player.total_duration > 0:
|
|
988
|
+
info_lines.append(f"⏱️ Duration: {self._format_time(self.player.total_duration)}")
|
|
989
|
+
|
|
990
|
+
if 'file_size' in info:
|
|
991
|
+
file_size_mb = info['file_size'] / (1024 * 1024)
|
|
992
|
+
info_lines.append(f"💾 File Size: {file_size_mb:.2f} MB")
|
|
993
|
+
if 'chapters' in info and info['chapters']:
|
|
994
|
+
info_lines.append("")
|
|
995
|
+
info_lines.append("📚 CHAPTERS")
|
|
996
|
+
info_lines.append("─" * 50)
|
|
997
|
+
info_lines.append(f"Total Chapters: {len(info['chapters'])}")
|
|
998
|
+
info_lines.append("")
|
|
999
|
+
|
|
1000
|
+
for i, chapter in enumerate(info['chapters'][:8]):
|
|
1001
|
+
start_time = self._format_time(chapter.get('start', 0))
|
|
1002
|
+
title = chapter.get('title', f'Chapter {i+1}')
|
|
1003
|
+
info_lines.append(f" {i+1:2d}. {title:<30} [{start_time}]")
|
|
1004
|
+
|
|
1005
|
+
if len(info['chapters']) > 8:
|
|
1006
|
+
remaining = len(info['chapters']) - 8
|
|
1007
|
+
info_lines.append(f" ... and {remaining} more chapter{'s' if remaining > 1 else ''}")
|
|
1008
|
+
else:
|
|
1009
|
+
info_lines.append("")
|
|
1010
|
+
info_lines.append("📚 CHAPTERS")
|
|
1011
|
+
info_lines.append("─" * 50)
|
|
1012
|
+
info_lines.append("No chapter information available")
|
|
1013
|
+
self.details_text.config(state=tk.NORMAL)
|
|
1014
|
+
self.details_text.delete(1.0, tk.END)
|
|
1015
|
+
self.details_text.insert(1.0, '\n'.join(info_lines))
|
|
1016
|
+
self.details_text.config(state=tk.DISABLED)
|
|
1017
|
+
|
|
1018
|
+
def _toggle_play(self):
|
|
1019
|
+
"""Toggle play/pause."""
|
|
1020
|
+
if not self.player:
|
|
1021
|
+
return
|
|
1022
|
+
|
|
1023
|
+
try:
|
|
1024
|
+
if not self.is_playing:
|
|
1025
|
+
current_pos = self.progress_var.get()
|
|
1026
|
+
self.player.play(start_time=current_pos)
|
|
1027
|
+
self.is_playing = True
|
|
1028
|
+
self.is_paused = False
|
|
1029
|
+
self.play_button.config(text="⏸️ Pause")
|
|
1030
|
+
self.status_var.set("▶️ Playing audio...")
|
|
1031
|
+
self._start_update_thread()
|
|
1032
|
+
|
|
1033
|
+
elif self.is_playing and not self.is_paused:
|
|
1034
|
+
self.player.pause()
|
|
1035
|
+
self.is_paused = True
|
|
1036
|
+
self.play_button.config(text="▶️ Resume")
|
|
1037
|
+
self.status_var.set("⏸️ Playback paused")
|
|
1038
|
+
|
|
1039
|
+
elif self.is_paused:
|
|
1040
|
+
self.player.resume()
|
|
1041
|
+
self.is_paused = False
|
|
1042
|
+
self.play_button.config(text="⏸️ Pause")
|
|
1043
|
+
self.status_var.set("▶️ Playing audio...")
|
|
1044
|
+
|
|
1045
|
+
except TAFPlayerError as e:
|
|
1046
|
+
messagebox.showerror("🚫 Playback Error", f"Unable to control playback:\n\n{e}")
|
|
1047
|
+
self.status_var.set("❌ Playback error occurred")
|
|
1048
|
+
logger.error(f"Playback error: {e}")
|
|
1049
|
+
|
|
1050
|
+
def _stop_playback(self):
|
|
1051
|
+
"""Stop playback."""
|
|
1052
|
+
if not self.player:
|
|
1053
|
+
return
|
|
1054
|
+
|
|
1055
|
+
try:
|
|
1056
|
+
self.player.stop()
|
|
1057
|
+
self.is_playing = False
|
|
1058
|
+
self.is_paused = False
|
|
1059
|
+
self.play_button.config(text="▶️ Play")
|
|
1060
|
+
self.status_var.set("⏹️ Playback stopped")
|
|
1061
|
+
self._stop_update_thread()
|
|
1062
|
+
self.progress_var.set(0)
|
|
1063
|
+
self.current_time_var.set("00:00")
|
|
1064
|
+
|
|
1065
|
+
except TAFPlayerError as e:
|
|
1066
|
+
logger.error(f"Stop error: {e}")
|
|
1067
|
+
|
|
1068
|
+
def _on_seek(self, value):
|
|
1069
|
+
"""Handle seek bar changes."""
|
|
1070
|
+
if not self.player or not self.is_playing:
|
|
1071
|
+
return
|
|
1072
|
+
|
|
1073
|
+
try:
|
|
1074
|
+
seek_time = float(value)
|
|
1075
|
+
self.player.seek(seek_time)
|
|
1076
|
+
except (TAFPlayerError, ValueError) as e:
|
|
1077
|
+
logger.error(f"Seek error: {e}")
|
|
1078
|
+
|
|
1079
|
+
def _prev_chapter(self):
|
|
1080
|
+
"""Go to previous chapter."""
|
|
1081
|
+
# Implementation would depend on chapter navigation support in TAFPlayer
|
|
1082
|
+
pass
|
|
1083
|
+
|
|
1084
|
+
def _next_chapter(self):
|
|
1085
|
+
"""Go to next chapter."""
|
|
1086
|
+
# Implementation would depend on chapter navigation support in TAFPlayer
|
|
1087
|
+
pass
|
|
1088
|
+
|
|
1089
|
+
def _start_update_thread(self):
|
|
1090
|
+
"""Start the progress update thread."""
|
|
1091
|
+
if self.update_thread and self.update_thread.is_alive():
|
|
1092
|
+
return
|
|
1093
|
+
|
|
1094
|
+
self.stop_updates.clear()
|
|
1095
|
+
self.update_thread = threading.Thread(target=self._update_progress, daemon=True)
|
|
1096
|
+
self.update_thread.start()
|
|
1097
|
+
|
|
1098
|
+
def _stop_update_thread(self):
|
|
1099
|
+
"""Stop the progress update thread."""
|
|
1100
|
+
self.stop_updates.set()
|
|
1101
|
+
if self.update_thread and self.update_thread.is_alive():
|
|
1102
|
+
self.update_thread.join(timeout=1.0)
|
|
1103
|
+
|
|
1104
|
+
def _update_progress(self):
|
|
1105
|
+
"""Update progress bar and time display (runs in separate thread)."""
|
|
1106
|
+
while not self.stop_updates.is_set() and self.is_playing:
|
|
1107
|
+
try:
|
|
1108
|
+
if self.player:
|
|
1109
|
+
status = self.player.get_status()
|
|
1110
|
+
current_time = status.get('current_position', 0)
|
|
1111
|
+
self.root.after(0, self._update_progress_gui, current_time)
|
|
1112
|
+
|
|
1113
|
+
time.sleep(0.1)
|
|
1114
|
+
|
|
1115
|
+
except Exception as e:
|
|
1116
|
+
logger.error(f"Progress update error: {e}")
|
|
1117
|
+
break
|
|
1118
|
+
|
|
1119
|
+
def _update_progress_gui(self, current_time: float):
|
|
1120
|
+
"""Update GUI progress elements (called from main thread)."""
|
|
1121
|
+
self.progress_var.set(current_time)
|
|
1122
|
+
self.current_time_var.set(self._format_time(current_time))
|
|
1123
|
+
if self.player and current_time >= self.player.total_duration:
|
|
1124
|
+
self._stop_playback()
|
|
1125
|
+
|
|
1126
|
+
def _format_time(self, seconds: float) -> str:
|
|
1127
|
+
"""Format time in MM:SS or HH:MM:SS format."""
|
|
1128
|
+
if seconds < 0:
|
|
1129
|
+
return "00:00"
|
|
1130
|
+
|
|
1131
|
+
hours = int(seconds // 3600)
|
|
1132
|
+
minutes = int((seconds % 3600) // 60)
|
|
1133
|
+
secs = int(seconds % 60)
|
|
1134
|
+
|
|
1135
|
+
if hours > 0:
|
|
1136
|
+
return f"{hours:02d}:{minutes:02d}:{secs:02d}"
|
|
1137
|
+
else:
|
|
1138
|
+
return f"{minutes:02d}:{secs:02d}"
|
|
1139
|
+
|
|
1140
|
+
def _on_close(self):
|
|
1141
|
+
"""Handle window close event."""
|
|
1142
|
+
try:
|
|
1143
|
+
self._stop_playback()
|
|
1144
|
+
self._stop_update_thread()
|
|
1145
|
+
|
|
1146
|
+
if self.player:
|
|
1147
|
+
self.player.cleanup()
|
|
1148
|
+
|
|
1149
|
+
self.root.destroy()
|
|
1150
|
+
|
|
1151
|
+
except Exception as e:
|
|
1152
|
+
logger.error(f"Error during cleanup: {e}")
|
|
1153
|
+
self.root.destroy()
|
|
1154
|
+
|
|
1155
|
+
def run(self, taf_file_path: Optional[str] = None):
|
|
1156
|
+
"""
|
|
1157
|
+
Start the GUI player.
|
|
1158
|
+
|
|
1159
|
+
Args:
|
|
1160
|
+
taf_file_path: Optional path to a TAF file to load on startup
|
|
1161
|
+
"""
|
|
1162
|
+
if taf_file_path:
|
|
1163
|
+
self.root.after(100, lambda: self.load_taf_file(taf_file_path))
|
|
1164
|
+
|
|
1165
|
+
try:
|
|
1166
|
+
self.root.mainloop()
|
|
1167
|
+
except KeyboardInterrupt:
|
|
1168
|
+
logger.info("GUI player interrupted by user")
|
|
1169
|
+
except Exception as e:
|
|
1170
|
+
logger.error(f"GUI player error: {e}")
|
|
1171
|
+
raise
|
|
1172
|
+
|
|
1173
|
+
|
|
1174
|
+
def gui_player(taf_file_path: Optional[str] = None) -> None:
|
|
1175
|
+
"""
|
|
1176
|
+
Start the GUI TAF player.
|
|
1177
|
+
|
|
1178
|
+
Args:
|
|
1179
|
+
taf_file_path: Optional path to the TAF file to play. If None, user can load via dialog.
|
|
1180
|
+
"""
|
|
1181
|
+
|
|
1182
|
+
if not TKINTER_AVAILABLE:
|
|
1183
|
+
print("Error: GUI player requires tkinter, which is not available on this system.")
|
|
1184
|
+
print(f"Tkinter error: {TKINTER_ERROR}")
|
|
1185
|
+
print("\nTo use the GUI player, you need to install tkinter support:")
|
|
1186
|
+
print(" - On Ubuntu/Debian: sudo apt-get install python3-tk")
|
|
1187
|
+
print(" - On Fedora/RHEL: sudo dnf install tkinter")
|
|
1188
|
+
print(" - On Arch Linux: sudo pacman -S tk")
|
|
1189
|
+
print(" - On macOS: tkinter should be included with Python")
|
|
1190
|
+
print(" - On Windows: tkinter should be included with Python")
|
|
1191
|
+
print("\nAlternatively, use the command-line player with: --play")
|
|
1192
|
+
return
|
|
1193
|
+
|
|
1194
|
+
try:
|
|
1195
|
+
logger.info(f"Starting GUI player for: {taf_file_path}")
|
|
1196
|
+
if not os.path.exists(taf_file_path):
|
|
1197
|
+
print(f"Error: TAF file not found: {taf_file_path}")
|
|
1198
|
+
return
|
|
1199
|
+
player_gui = TAFPlayerGUI()
|
|
1200
|
+
player_gui.run(taf_file_path)
|
|
1201
|
+
|
|
1202
|
+
except Exception as e:
|
|
1203
|
+
logger.error(f"Failed to start GUI player: {e}")
|
|
1204
|
+
print(f"Error starting GUI player: {e}")
|
|
1205
|
+
|
|
1206
|
+
|
|
1207
|
+
if __name__ == "__main__":
|
|
1208
|
+
if len(sys.argv) > 1:
|
|
1209
|
+
gui_player(sys.argv[1])
|
|
1210
|
+
else:
|
|
1211
|
+
player_gui = TAFPlayerGUI()
|
|
1212
|
+
player_gui.run()
|