@@ -195,9 +195,85 @@ async fn build_tray_menu<R: tauri::Runtime>(
195195 None
196196 } ;
197197
198+ // Recent transcriptions (last 5)
199+ use tauri_plugin_store:: StoreExt ;
200+ let mut recent_owned: Vec < tauri:: menu:: MenuItem < R > > = Vec :: new ( ) ;
201+ {
202+ if let Ok ( store) = app. store ( "transcriptions" ) {
203+ let mut entries: Vec < ( String , serde_json:: Value ) > = Vec :: new ( ) ;
204+ for key in store. keys ( ) {
205+ if let Some ( value) = store. get ( & key) {
206+ entries. push ( ( key. to_string ( ) , value) ) ;
207+ }
208+ }
209+ entries. sort_by ( |a, b| b. 0 . cmp ( & a. 0 ) ) ;
210+ entries. truncate ( 5 ) ;
211+
212+ for ( ts, entry) in entries {
213+ let mut label = entry
214+ . get ( "text" )
215+ . and_then ( |v| v. as_str ( ) )
216+ . map ( |s| {
217+ let first_line = s. lines ( ) . next ( ) . unwrap_or ( "" ) . trim ( ) ;
218+ if first_line. len ( ) > 40 {
219+ format ! ( "{}…" , & first_line[ ..40 ] )
220+ } else if first_line. is_empty ( ) {
221+ "(empty)" . to_string ( )
222+ } else {
223+ first_line. to_string ( )
224+ }
225+ } )
226+ . unwrap_or_else ( || "(unknown)" . to_string ( ) ) ;
227+
228+ if label. is_empty ( ) { label = "(empty)" . to_string ( ) ; }
229+
230+ let item = tauri:: menu:: MenuItem :: with_id (
231+ app,
232+ & format ! ( "recent_copy_{}" , ts) ,
233+ label,
234+ true ,
235+ None :: < & str > ,
236+ ) ?;
237+ recent_owned. push ( item) ;
238+ }
239+ }
240+ }
241+ let mut recent_refs: Vec < & dyn tauri:: menu:: IsMenuItem < _ > > = Vec :: new ( ) ;
242+ for item in & recent_owned { recent_refs. push ( item) ; }
243+
244+ // Recording mode submenu (Toggle / Push-to-Talk)
245+ let ( toggle_item, ptt_item) = {
246+ let recording_mode = match app. store ( "settings" ) {
247+ Ok ( store) => store
248+ . get ( "recording_mode" )
249+ . and_then ( |v| v. as_str ( ) . map ( |s| s. to_string ( ) ) )
250+ . unwrap_or_else ( || "toggle" . to_string ( ) ) ,
251+ Err ( _) => "toggle" . to_string ( ) ,
252+ } ;
253+
254+ let toggle = tauri:: menu:: CheckMenuItem :: with_id (
255+ app,
256+ "recording_mode_toggle" ,
257+ "Toggle" ,
258+ true ,
259+ recording_mode == "toggle" ,
260+ None :: < & str > ,
261+ ) ?;
262+ let ptt = tauri:: menu:: CheckMenuItem :: with_id (
263+ app,
264+ "recording_mode_push_to_talk" ,
265+ "Push-to-Talk" ,
266+ true ,
267+ recording_mode == "push_to_talk" ,
268+ None :: < & str > ,
269+ ) ?;
270+ ( toggle, ptt)
271+ } ;
272+
198273 // Create menu items
199274 let separator1 = PredefinedMenuItem :: separator ( app) ?;
200275 let settings_i = MenuItem :: with_id ( app, "settings" , "Dashboard" , true , None :: < & str > ) ?;
276+ let check_updates_i = MenuItem :: with_id ( app, "check_updates" , "Check for Updates" , true , None :: < & str > ) ?;
201277 let separator2 = PredefinedMenuItem :: separator ( app) ?;
202278 let quit_i = MenuItem :: with_id ( app, "quit" , "Quit VoiceTypr" , true , None :: < & str > ) ?;
203279
@@ -211,9 +287,27 @@ async fn build_tray_menu<R: tauri::Runtime>(
211287 menu_builder = menu_builder. item ( & microphone_submenu) ;
212288 }
213289
290+ // Add Recent Transcriptions submenu if we have items
291+ if !recent_refs. is_empty ( ) {
292+ let recent_submenu = Submenu :: with_id_and_items (
293+ app,
294+ "recent" ,
295+ "Recent Transcriptions" ,
296+ true ,
297+ & recent_refs,
298+ ) ?;
299+ menu_builder = menu_builder. item ( & recent_submenu) ;
300+ }
301+
302+ // Recording mode submenu
303+ let mode_items: Vec < & dyn tauri:: menu:: IsMenuItem < _ > > = vec ! [ & toggle_item, & ptt_item] ;
304+ let mode_submenu = Submenu :: with_id_and_items ( app, "recording_mode" , "Recording Mode" , true , & mode_items) ?;
305+ menu_builder = menu_builder. item ( & mode_submenu) ;
306+
214307 let menu = menu_builder
215308 . item ( & separator1)
216309 . item ( & settings_i)
310+ . item ( & check_updates_i)
217311 . item ( & separator2)
218312 . item ( & quit_i)
219313 . build ( ) ?;
@@ -1014,7 +1108,7 @@ pub fn run() -> Result<(), Box<dyn std::error::Error>> {
10141108 . menu ( & menu)
10151109 . on_menu_event ( move |app, event| {
10161110 log:: info!( "Tray menu event: {:?}" , event. id) ;
1017- let event_id = event. id . as_ref ( ) ;
1111+ let event_id = event. id . as_ref ( ) . to_string ( ) ;
10181112
10191113 if event_id == "settings" {
10201114 if let Some ( window) = app. get_webview_window ( "main" ) {
@@ -1025,6 +1119,8 @@ pub fn run() -> Result<(), Box<dyn std::error::Error>> {
10251119 }
10261120 } else if event_id == "quit" {
10271121 app. exit ( 0 ) ;
1122+ } else if event_id == "check_updates" {
1123+ let _ = app. emit ( "tray-check-updates" , ( ) ) ;
10281124 } else if event_id. starts_with ( "model_" ) {
10291125 // Handle model selection
10301126 let model_name = match event_id. strip_prefix ( "model_" ) {
@@ -1086,6 +1182,59 @@ pub fn run() -> Result<(), Box<dyn std::error::Error>> {
10861182 }
10871183 } ) ;
10881184 }
1185+ // Recent transcriptions copy handler
1186+ else if let Some ( ts) = event_id. strip_prefix ( "recent_copy_" ) {
1187+ let ts_owned = ts. to_string ( ) ;
1188+ let app_handle = app. app_handle ( ) . clone ( ) ;
1189+ tauri:: async_runtime:: spawn ( async move {
1190+ // Read text by timestamp and copy
1191+ match app_handle. store ( "transcriptions" ) {
1192+ Ok ( store) => {
1193+ if let Some ( val) = store. get ( & ts_owned) {
1194+ if let Some ( text) = val. get ( "text" ) . and_then ( |v| v. as_str ( ) ) {
1195+ if let Err ( e) = crate :: commands:: text:: copy_text_to_clipboard ( text. to_string ( ) ) . await {
1196+ log:: error!( "Failed to copy recent transcription: {}" , e) ;
1197+ let _ = app_handle. emit ( "tray-action-error" , & format ! ( "Failed to copy: {}" , e) ) ;
1198+ } else {
1199+ log:: info!( "Copied recent transcription to clipboard" ) ;
1200+ }
1201+ }
1202+ }
1203+ }
1204+ Err ( e) => {
1205+ log:: error!( "Failed to open transcriptions store: {}" , e) ;
1206+ }
1207+ }
1208+ } ) ;
1209+ }
1210+ // Recording mode switchers
1211+ else if event_id == "recording_mode_toggle" || event_id == "recording_mode_push_to_talk" {
1212+ let app_handle = app. app_handle ( ) . clone ( ) ;
1213+ let mode = if event_id. ends_with ( "push_to_talk" ) { "push_to_talk" } else { "toggle" } ;
1214+ tauri:: async_runtime:: spawn ( async move {
1215+ match crate :: commands:: settings:: get_settings ( app_handle. clone ( ) ) . await {
1216+ Ok ( mut s) => {
1217+ s. recording_mode = mode. to_string ( ) ;
1218+ match crate :: commands:: settings:: save_settings ( app_handle. clone ( ) , s) . await {
1219+ Err ( e) => {
1220+ log:: error!( "Failed to save recording mode from tray: {}" , e) ;
1221+ let _ = app_handle. emit ( "tray-action-error" , & format ! ( "Failed to change recording mode: {}" , e) ) ;
1222+ }
1223+ Ok ( ( ) ) => {
1224+ if let Err ( e) = crate :: commands:: settings:: update_tray_menu ( app_handle. clone ( ) ) . await {
1225+ log:: warn!( "Failed to refresh tray after mode change: {}" , e) ;
1226+ }
1227+ // Notify frontend so SettingsContext refreshes
1228+ let _ = app_handle. emit ( "settings-changed" , ( ) ) ;
1229+ }
1230+ }
1231+ }
1232+ Err ( e) => {
1233+ log:: error!( "Failed to get settings for mode change: {}" , e) ;
1234+ }
1235+ }
1236+ } ) ;
1237+ }
10891238 } )
10901239 . on_tray_icon_event ( |tray, event| {
10911240 if let TrayIconEvent :: Click {
@@ -1479,6 +1628,7 @@ pub fn run() -> Result<(), Box<dyn std::error::Error>> {
14791628 reset_app_data,
14801629 copy_image_to_clipboard,
14811630 save_image_to_file,
1631+ copy_text_to_clipboard,
14821632 get_ai_settings,
14831633 get_ai_settings_for_provider,
14841634 cache_ai_api_key,
0 commit comments