@@ -19,29 +19,73 @@ class InfoManager:
1919 def __init__ (self , frame ):
2020 self .frame = frame
2121
22+ def _get_spoken_duration (self , ms ):
23+ """Helper to convert ms to spoken string like '2 hours, 5 minutes'."""
24+ if ms < 0 : ms = 0
25+ total_seconds = int (ms / 1000 )
26+ hours = total_seconds // 3600
27+ minutes = (total_seconds % 3600 ) // 60
28+ seconds = total_seconds % 60
29+
30+ parts = []
31+
32+ # Handle Hours
33+ if hours > 0 :
34+ label = "hour" if hours == 1 else "hours"
35+ parts .append (f"{ hours } { label } " )
36+
37+ # Handle Minutes (Always show unless 0 and we have hours, but user wants detail)
38+ if minutes > 0 :
39+ label = "minute" if minutes == 1 else "minutes"
40+ parts .append (f"{ minutes } { label } " )
41+
42+ # Handle Seconds (Show if it's the only thing, or generally for precision)
43+ if seconds > 0 or not parts :
44+ label = "second" if seconds == 1 else "seconds"
45+ parts .append (f"{ seconds } { label } " )
46+
47+ return ", " .join (parts )
48+
2249 def announce_time (self , should_speak_time : bool ):
2350 """
24- Updates the time display label on the UI.
25- Optionally speaks the current time (used by the 'I' hotkey).
26-
27- Args:
28- should_speak_time: If True, triggers NVDA speech.
51+ Announces: You have listened to X of Y.
2952 """
3053 if self .frame .is_exiting or not self .frame .engine or self .frame .IsBeingDeleted () or not self .frame .time_text :
3154 return
3255
3356 try :
34- current_time = self .frame .engine .get_time ()
35- total_time = self .frame .current_file_duration_ms
36- time_str = f"{ format_time (current_time )} / { format_time (total_time if total_time > 0 else 0 )} "
57+ current_ms = self .frame .engine .get_time ()
58+ total_ms = self .frame .current_file_duration_ms
3759
38- wx .CallAfter (self ._update_time_label , time_str )
60+ # Update visual label (Keep mathematical for visual, or change if you like)
61+ # For visuals, we stick to standard format usually, but here is the logic:
62+ time_str_visual = f"{ format_time (current_ms )} / { format_time (total_ms if total_ms > 0 else 0 )} "
63+ wx .CallAfter (self ._update_time_label , time_str_visual )
3964
4065 if should_speak_time :
41- speak (time_str , LEVEL_CRITICAL )
66+ spoken_current = self ._get_spoken_duration (current_ms )
67+ spoken_total = self ._get_spoken_duration (total_ms )
68+ # Spoken: "You have listened to 5 minutes of 10 minutes"
69+ msg = _ ("You have listened to {0} of {1}" ).format (spoken_current , spoken_total )
70+ speak (msg , LEVEL_CRITICAL )
4271 except Exception as e :
4372 logging .debug (f"Ignoring exception during announce_time: { e } " )
4473
74+ def copy_current_time (self ):
75+ if not self .frame .engine :
76+ return
77+
78+ try :
79+ current_ms = self .frame .engine .get_time ()
80+ time_str = format_time (current_ms )
81+
82+ if wx .TheClipboard .Open ():
83+ wx .TheClipboard .SetData (wx .TextDataObject (time_str ))
84+ wx .TheClipboard .Close ()
85+ speak (_ ("Time copied." ), LEVEL_MINIMAL )
86+ except Exception as e :
87+ logging .error (f"Failed to copy time to clipboard: { e } " )
88+
4589 def _update_time_label (self , time_str : str ):
4690 """Safely updates the time label on the main thread."""
4791 if self .frame and not self .frame .IsBeingDeleted () and self .frame .time_text :
@@ -51,76 +95,56 @@ def _update_time_label(self, time_str: str):
5195 except wx .PyDeadObjectError :
5296 logging .warning ("Failed to update time label, object likely destroyed." )
5397
54- def announce_info (self ):
55- """Speaks the current book title and file name."""
56- book_title = self .frame .book_title
57- file_path = self .frame .current_file_path
58-
59- if not file_path :
60- file_name = _ ("Unknown File" )
61- else :
62- file_name = os .path .basename (file_path )
63-
64- speak (f"{ _ ('Book' )} : { book_title } . { file_name } " , LEVEL_CRITICAL )
65-
66- def announce_file_only (self ):
67- """Speaks only the current file name (used on file change)."""
68- file_path = self .frame .current_file_path
69-
70- if not file_path :
71- file_name = _ ("Unknown File" )
72- else :
73- file_name = os .path .basename (file_path )
74-
75- speak (f"{ file_name } " , LEVEL_MINIMAL )
76-
7798 def announce_remaining_file_time (self ):
78- """Calculates and announces the time remaining in the current file."""
99+ """
100+ Announces: X remaining of Y.
101+ """
79102 if not self .frame .engine :
80103 return
81104
82105 try :
83- duration_ms = self .frame .current_file_duration_ms
84- if duration_ms <= 0 :
106+ total_ms = self .frame .current_file_duration_ms
107+ if total_ms <= 0 :
85108 speak (_ ("File duration not yet known." ), LEVEL_CRITICAL )
86109 return
87110
88- current_time_ms = self .frame .engine .get_time ()
89- remaining_ms = duration_ms - current_time_ms
90- if remaining_ms < 0 :
91- remaining_ms = 0
111+ current_ms = self .frame .engine .get_time ()
112+ remaining_ms = max (0 , total_ms - current_ms )
92113
93- speak (_ ("Time remaining: {0}" ).format (format_time (remaining_ms )), LEVEL_CRITICAL )
114+ spoken_remaining = self ._get_spoken_duration (remaining_ms )
115+ spoken_total = self ._get_spoken_duration (total_ms )
116+
117+ # Spoken: "5 minutes remaining of 10 minutes"
118+ speak (_ ("{0} remaining of {1}" ).format (spoken_remaining , spoken_total ), LEVEL_CRITICAL )
94119 except Exception as e :
95120 logging .error (f"Error announcing remaining file time: { e } " , exc_info = True )
96121
97122 def announce_adjusted_remaining_file_time (self ):
98123 """
99- Calculates and announces time remaining in the current file,
100- adjusted for the current playback speed.
124+ Announces remaining time adjusted by speed.
101125 """
102126 if not self .frame .engine :
103127 return
104128
105129 try :
106- duration_ms = self .frame .current_file_duration_ms
130+ total_ms = self .frame .current_file_duration_ms
107131 current_rate = self .frame .current_target_rate
108132
109- if duration_ms <= 0 :
133+ if total_ms <= 0 :
110134 speak (_ ("File duration not yet known." ), LEVEL_MINIMAL )
111135 return
112136 if current_rate == 0 :
113137 speak (_ ("Playback speed is zero." ), LEVEL_MINIMAL )
114138 return
115139
116- current_time_ms = self .frame .engine .get_time ()
117- real_remaining_ms = duration_ms - current_time_ms
118- if real_remaining_ms < 0 :
119- real_remaining_ms = 0
120-
140+ current_ms = self .frame .engine .get_time ()
141+ real_remaining_ms = max (0 , total_ms - current_ms )
121142 adjusted_remaining_ms = int (real_remaining_ms / current_rate )
122- speak (_ ("Time remaining at current speed: {0}" ).format (format_time (adjusted_remaining_ms )),
123- LEVEL_CRITICAL )
143+
144+ spoken_adjusted = self ._get_spoken_duration (adjusted_remaining_ms )
145+
146+ # Spoken: "3 minutes remaining at current speed"
147+ speak (_ ("{0} remaining at current speed" ).format (spoken_adjusted ), LEVEL_CRITICAL )
124148 except Exception as e :
125149 logging .error (f"Error announcing adjusted remaining file time: { e } " , exc_info = True )
126150
@@ -181,41 +205,44 @@ def _calculate_total_elapsed_ms(self) -> int:
181205 return total_elapsed_ms
182206
183207 def announce_total_elapsed_time (self ):
184- """Announces total elapsed time and total book duration."""
208+ """Announces total elapsed vs total book duration verbally ."""
185209 if not hasattr (self .frame , 'total_book_duration_ms' ):
186210 speak (_ ("Book duration data not available." ), LEVEL_MINIMAL )
187211 return
188212
189213 try :
190214 elapsed_ms = self ._calculate_total_elapsed_ms ()
191215 total_ms = self .frame .total_book_duration_ms
192- speak (_ ("Elapsed: {0} / Total: {1}" ).format (
193- format_time (elapsed_ms ), format_time (total_ms )), LEVEL_CRITICAL )
216+
217+ spoken_elapsed = self ._get_spoken_duration (elapsed_ms )
218+ spoken_total = self ._get_spoken_duration (total_ms )
219+
220+ msg = _ ("You have listened to {0} of {1}" ).format (spoken_elapsed , spoken_total )
221+ speak (msg , LEVEL_CRITICAL )
194222 except Exception as e :
195223 logging .error (f"Error announcing total elapsed time: { e } " , exc_info = True )
196224
197225 def announce_total_remaining_time (self ):
198- """Announces the total time remaining in the entire book ."""
226+ """Announces total remaining time verbally ."""
199227 if not hasattr (self .frame , 'total_book_duration_ms' ):
200228 speak (_ ("Book duration data not available." ), LEVEL_MINIMAL )
201229 return
202230
203231 try :
204232 elapsed_ms = self ._calculate_total_elapsed_ms ()
205233 total_ms = self .frame .total_book_duration_ms
206- remaining_ms = total_ms - elapsed_ms
207- if remaining_ms < 0 :
208- remaining_ms = 0
234+ remaining_ms = max (0 , total_ms - elapsed_ms )
209235
210- speak (_ ("Total time remaining: {0}" ).format (format_time (remaining_ms )), LEVEL_CRITICAL )
236+ spoken_remaining = self ._get_spoken_duration (remaining_ms )
237+ spoken_total = self ._get_spoken_duration (total_ms )
238+
239+ msg = _ ("{0} remaining of {1}" ).format (spoken_remaining , spoken_total )
240+ speak (msg , LEVEL_CRITICAL )
211241 except Exception as e :
212242 logging .error (f"Error announcing total remaining time: { e } " , exc_info = True )
213243
214244 def announce_adjusted_total_remaining_time (self ):
215- """
216- Announces total time remaining in the book, adjusted for the
217- current playback speed.
218- """
245+ """Announces adjusted total remaining time verbally."""
219246 if not hasattr (self .frame , 'total_book_duration_ms' ) or not hasattr (self .frame , 'current_target_rate' ):
220247 speak (_ ("Book duration data not available." ), LEVEL_MINIMAL )
221248 return
@@ -228,12 +255,11 @@ def announce_adjusted_total_remaining_time(self):
228255
229256 elapsed_ms = self ._calculate_total_elapsed_ms ()
230257 total_ms = self .frame .total_book_duration_ms
231- real_remaining_ms = total_ms - elapsed_ms
232- if real_remaining_ms < 0 :
233- real_remaining_ms = 0
234-
258+ real_remaining_ms = max (0 , total_ms - elapsed_ms )
235259 adjusted_remaining_ms = int (real_remaining_ms / current_rate )
236- speak (_ ("Total time remaining at current speed: {0}" ).format (
237- format_time (adjusted_remaining_ms )), LEVEL_CRITICAL )
260+
261+ spoken_adjusted = self ._get_spoken_duration (adjusted_remaining_ms )
262+
263+ speak (_ ("{0} remaining of book at current speed" ).format (spoken_adjusted ), LEVEL_CRITICAL )
238264 except Exception as e :
239- logging .error (f"Error announcing adjusted total remaining time: { e } " , exc_info = True )
265+ logging .error (f"Error announcing adjusted total remaining time: { e } " , exc_info = True )
0 commit comments