BlueSky & more on desktop lazurite.stormlightlabs.org/
tauri rust typescript bluesky appview atproto solid
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 174 lines 6.1 kB view raw
1use crate::error::{AppError, Result as AppResult}; 2use crate::settings; 3use crate::state::AppState; 4use std::sync::Mutex; 5use tauri::{ 6 image::Image, 7 menu::{Menu, MenuItem}, 8 tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}, 9 AppHandle, Manager, WebviewUrl, WebviewWindow, WebviewWindowBuilder, 10}; 11use tauri_plugin_global_shortcut::{GlobalShortcutExt, Shortcut, ShortcutState}; 12use tauri_plugin_log::log; 13 14const COMPOSER_WINDOW_LABEL: &str = "composer"; 15const APP_INDEX_PATH: &str = "index.html"; 16const COMPOSER_HASH_ROUTE: &str = "#/composer"; 17const COMPOSER_INIT_SCRIPT: &str = r#" 18if (window.location.hash !== '#/composer') { 19 window.location.replace(`${window.location.pathname}${window.location.search}#/composer`); 20} 21"#; 22const MAIN_WINDOW_LABEL: &str = "main"; 23const MENU_NEW_POST: &str = "new_post"; 24const MENU_TOGGLE_WINDOW: &str = "toggle_window"; 25const MENU_QUIT: &str = "quit"; 26const DEFAULT_GLOBAL_SHORTCUT: &str = "Ctrl+Shift+N"; 27 28#[derive(Default)] 29struct ComposerShortcutState { 30 current_shortcut: Mutex<Option<String>>, 31} 32 33pub fn setup_tray(app: &AppHandle) -> std::result::Result<(), Box<dyn std::error::Error>> { 34 let new_post_i = MenuItem::with_id(app, MENU_NEW_POST, "New Post…", true, None::<&str>)?; 35 let toggle_window_i = MenuItem::with_id(app, MENU_TOGGLE_WINDOW, "Show / Hide", true, None::<&str>)?; 36 let quit_i = MenuItem::with_id(app, MENU_QUIT, "Quit", true, None::<&str>)?; 37 38 let menu = Menu::with_items(app, &[&new_post_i, &toggle_window_i, &quit_i])?; 39 let tray_icon = Image::from_bytes(include_bytes!("../../public/tray-icon.png"))?; 40 41 let tray = TrayIconBuilder::new() 42 .icon(tray_icon) 43 .menu(&menu) 44 .show_menu_on_left_click(false) 45 .on_menu_event(|app, event| match event.id().as_ref() { 46 MENU_NEW_POST => { 47 let _ = open_composer_window(app); 48 } 49 MENU_TOGGLE_WINDOW => toggle_window_visibility(app), 50 MENU_QUIT => app.exit(0), 51 _ => {} 52 }) 53 .on_tray_icon_event(|tray, event| { 54 if let TrayIconEvent::Click { button: MouseButton::Left, button_state: MouseButtonState::Up, .. } = event { 55 let _ = open_composer_window(tray.app_handle()); 56 } 57 }) 58 .build(app)?; 59 60 app.manage(tray); 61 62 Ok(()) 63} 64 65pub fn setup_global_shortcut(app: &AppHandle) -> std::result::Result<(), Box<dyn std::error::Error>> { 66 app.manage(ComposerShortcutState::default()); 67 sync_global_shortcut(app)?; 68 Ok(()) 69} 70 71pub fn sync_global_shortcut(app: &AppHandle) -> AppResult<()> { 72 let configured_shortcut = app 73 .try_state::<AppState>() 74 .and_then(|state| { 75 settings::get_settings(&state) 76 .ok() 77 .map(|settings| settings.global_shortcut) 78 }) 79 .unwrap_or_else(|| DEFAULT_GLOBAL_SHORTCUT.to_string()); 80 81 update_global_shortcut(app, &configured_shortcut) 82} 83 84pub fn update_global_shortcut(app: &AppHandle, shortcut: &str) -> AppResult<()> { 85 let shortcut = shortcut.trim(); 86 if shortcut.is_empty() { 87 return Err(AppError::validation("global shortcut must not be empty")); 88 } 89 90 let parsed_shortcut: Shortcut = shortcut 91 .parse() 92 .map_err(|error| AppError::validation(format!("invalid global shortcut '{shortcut}': {error}")))?; 93 94 let shortcut_state = app.state::<ComposerShortcutState>(); 95 let mut current_shortcut = shortcut_state 96 .current_shortcut 97 .lock() 98 .map_err(|_| AppError::StatePoisoned("composer_shortcut"))?; 99 100 if current_shortcut.as_deref() == Some(shortcut) { 101 return Ok(()); 102 } 103 104 if let Some(existing_shortcut) = current_shortcut.as_ref() { 105 let existing_shortcut = existing_shortcut 106 .parse::<Shortcut>() 107 .map_err(|error| AppError::validation(format!("invalid registered global shortcut: {error}")))?; 108 app.global_shortcut().unregister(existing_shortcut).map_err(|error| { 109 AppError::validation(format!( 110 "failed to unregister existing global shortcut '{}': {error}", 111 current_shortcut.as_deref().unwrap_or_default() 112 )) 113 })?; 114 } 115 116 app.global_shortcut() 117 .on_shortcut(parsed_shortcut, |app, _, event| { 118 if event.state == ShortcutState::Pressed { 119 let _ = open_composer_window(app); 120 } 121 }) 122 .map_err(|error| AppError::validation(format!("failed to register global shortcut '{shortcut}': {error}")))?; 123 124 log::info!("registered global composer shortcut: {shortcut}"); 125 *current_shortcut = Some(shortcut.to_string()); 126 Ok(()) 127} 128 129fn toggle_window_visibility(app: &AppHandle) { 130 if let Some(window) = app.get_webview_window(MAIN_WINDOW_LABEL) { 131 if is_window_visible(&window) { 132 let _ = window.hide(); 133 } else { 134 show_window(&window); 135 } 136 } 137} 138 139fn is_window_visible(window: &WebviewWindow) -> bool { 140 window.is_visible().unwrap_or(false) && !window.is_minimized().unwrap_or(false) 141} 142 143fn open_composer_window(app: &AppHandle) -> std::result::Result<(), Box<dyn std::error::Error>> { 144 if let Some(window) = app.get_webview_window(COMPOSER_WINDOW_LABEL) { 145 route_window_to_composer(&window); 146 show_window(&window); 147 return Ok(()); 148 } 149 150 let window = WebviewWindowBuilder::new(app, COMPOSER_WINDOW_LABEL, WebviewUrl::App(APP_INDEX_PATH.into())) 151 .initialization_script(COMPOSER_INIT_SCRIPT) 152 .title("New Post") 153 .inner_size(720.0, 640.0) 154 .min_inner_size(560.0, 420.0) 155 .resizable(true) 156 .center() 157 .build()?; 158 159 show_window(&window); 160 161 Ok(()) 162} 163 164fn route_window_to_composer(window: &WebviewWindow) { 165 let _ = window.eval(format!( 166 "if (window.location.hash !== '{COMPOSER_HASH_ROUTE}') {{ window.location.hash = '/composer'; }}" 167 )); 168} 169 170fn show_window(window: &WebviewWindow) { 171 let _ = window.unminimize(); 172 let _ = window.show(); 173 let _ = window.set_focus(); 174}