super-stt-app/src/core/app/init.rs
Line | Count | Source |
1 | | // SPDX-License-Identifier: GPL-3.0-only |
2 | | |
3 | | use crate::daemon::client::load_audio_themes; |
4 | | use crate::state::{AudioTheme, ContextPage, DaemonStatus, RecordingStatus}; |
5 | | use crate::ui::icons; |
6 | | use crate::ui::messages::{Message, ModelMessage, RecordingMessage}; |
7 | | use cosmic::prelude::*; |
8 | | use cosmic::widget::nav_bar; |
9 | | use std::collections::HashMap; |
10 | | |
11 | | use super::{AppModel, DeviceState}; |
12 | | |
13 | | /// Builds the navigation bar with all Super STT pages inserted in order. |
14 | 0 | fn build_nav() -> nav_bar::Model { |
15 | 0 | let mut nav = nav_bar::Model::default(); |
16 | | |
17 | | // Models is the primary page (the active backend) — first in the rail and |
18 | | // active on launch. Library (manage/install backends) sits directly below. |
19 | 0 | nav.insert() |
20 | 0 | .text("Models") |
21 | 0 | .data::<crate::state::Page>(crate::state::Page::Models) |
22 | 0 | .icon(icons::phosphor(icons::BRAIN)) |
23 | 0 | .activate(); |
24 | | |
25 | 0 | nav.insert() |
26 | 0 | .text("Library") |
27 | 0 | .data::<crate::state::Page>(crate::state::Page::Library) |
28 | 0 | .icon(icons::phosphor(icons::BOOKS)); |
29 | | |
30 | 0 | nav.insert() |
31 | 0 | .text("Contexts") |
32 | 0 | .data::<crate::state::Page>(crate::state::Page::Contexts) |
33 | 0 | .icon(icons::phosphor(icons::CARDS)); |
34 | | |
35 | 0 | nav.insert() |
36 | 0 | .text("Customization") |
37 | 0 | .data::<crate::state::Page>(crate::state::Page::Customization) |
38 | 0 | .icon(icons::phosphor(icons::GEAR)); |
39 | | |
40 | 0 | nav.insert() |
41 | 0 | .text("Recording") |
42 | 0 | .data::<crate::state::Page>(crate::state::Page::Recording) |
43 | 0 | .icon(icons::phosphor(icons::MICROPHONE)); |
44 | | |
45 | 0 | nav.insert() |
46 | 0 | .text("Input Simulation") |
47 | 0 | .data::<crate::state::Page>(crate::state::Page::InputSimulation) |
48 | 0 | .icon(icons::phosphor(icons::KEYBOARD)); |
49 | | |
50 | 0 | nav.insert() |
51 | 0 | .text("Connection") |
52 | 0 | .data::<crate::state::Page>(crate::state::Page::Connection) |
53 | 0 | .icon(icons::phosphor(icons::PLUG)); |
54 | | |
55 | 0 | nav.insert() |
56 | 0 | .text("Updates") |
57 | 0 | .data::<crate::state::Page>(crate::state::Page::Updates) |
58 | 0 | .icon(icons::phosphor(icons::ARROWS_CLOCKWISE)); |
59 | | |
60 | 0 | nav |
61 | 0 | } |
62 | | |
63 | | /// Builds the initial batch of startup tasks (audio themes, daemon ping, data load). |
64 | 0 | fn initial_load_tasks( |
65 | 0 | title_command: Task<cosmic::Action<Message>>, |
66 | 0 | ) -> Task<cosmic::Action<Message>> { |
67 | | // Load audio themes on startup (always available) |
68 | 0 | let load_themes = Task::perform(load_audio_themes(), |themes| { |
69 | 0 | cosmic::Action::App(Message::Recording(RecordingMessage::AudioThemesLoaded( |
70 | 0 | themes, |
71 | 0 | ))) |
72 | 0 | }); |
73 | | |
74 | | // Try to ping the daemon on startup |
75 | 0 | let initial_ping = crate::core::app::handlers::tasks::ping_task(); |
76 | | |
77 | | // Load initial data (models + device info) on startup |
78 | 0 | let load_initial_data = Task::perform( |
79 | 0 | async move { |
80 | | // Small delay to let daemon connection establish |
81 | 0 | tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; |
82 | 0 | }, |
83 | 0 | |()| cosmic::Action::App(Message::Model(ModelMessage::LoadInitialData)), |
84 | | ); |
85 | | |
86 | 0 | Task::batch([title_command, load_themes, initial_ping, load_initial_data]) |
87 | 0 | } |
88 | | |
89 | | impl AppModel { |
90 | | /// Initializes the application with any given flags and startup commands. |
91 | 0 | pub(super) fn init_model( |
92 | 0 | core: cosmic::Core, |
93 | 0 | _flags: crate::Flags, |
94 | 0 | ) -> (Self, Task<cosmic::Action<Message>>) { |
95 | 0 | let nav = build_nav(); |
96 | | |
97 | | // Construct the app model with the runtime's core. |
98 | 0 | let mut app = AppModel { |
99 | 0 | core, |
100 | 0 | context_page: ContextPage::default(), |
101 | 0 | contexts: crate::state::contexts::ContextsState::default(), |
102 | 0 | nav, |
103 | 0 | // Initialize Super STT state using proper socket path |
104 | 0 | socket_path: super_stt_shared::validation::get_http_socket_path(), |
105 | 0 | daemon_status: DaemonStatus::Disconnected, |
106 | 0 | reconnect_retry: super_stt_shared::daemon::retry::RetryStrategy::for_initial_connection( |
107 | 0 | ), |
108 | 0 | recording_status: RecordingStatus::Idle, |
109 | 0 | transcription_text: String::new(), |
110 | 0 | preview_text: String::new(), |
111 | 0 | preview_source: None, |
112 | 0 | audio_level: 0.0, |
113 | 0 | is_speech_detected: false, |
114 | 0 | audio_themes: Vec::new(), |
115 | 0 | selected_audio_theme: AudioTheme::default(), |
116 | 0 | last_non_silent_theme: AudioTheme::default(), |
117 | 0 | udp_restart_counter: 0, |
118 | 0 | last_udp_data: std::time::Instant::now(), |
119 | 0 |
|
120 | 0 | // Initialize model state |
121 | 0 | current_model: String::new(), |
122 | 0 | current_source: String::new(), |
123 | 0 | current_model_epoch: 0, |
124 | 0 | model_operations: crate::state::model_operations::ModelOperations::opening( |
125 | 0 | "Loading initial model state...".to_string(), |
126 | 0 | ), |
127 | 0 |
|
128 | 0 | // Initialize device state |
129 | 0 | current_device: String::new(), // Empty until loaded from daemon |
130 | 0 | gpu_info: Vec::new(), |
131 | 0 | device_state: DeviceState::Ready, |
132 | 0 | last_event_timestamp: None, |
133 | 0 |
|
134 | 0 | // Initialize preview typing state (disabled by default as beta feature) |
135 | 0 | preview_typing_enabled: false, |
136 | 0 | // Replaced by the daemon's own state as soon as the settings load |
137 | 0 | // completes; the default is the daemon's default too (off). |
138 | 0 | post_processor: crate::daemon::client::StageState::default(), |
139 | 0 | staged_picks: crate::state::staged_picks::StagedPicks::default(), |
140 | 0 | device_offers: crate::state::device_offers::DeviceOffers::default(), |
141 | 0 | stage_catalog: crate::state::stage_catalog::StageCatalog::default(), |
142 | 0 | recording_stop_mode: |
143 | 0 | super_stt_shared::models::recording_stop_mode::RecordingStopMode::default(), |
144 | 0 | write_method: super_stt_shared::models::write_method::WriteMethod::default(), |
145 | 0 | write_method_test_text: String::new(), |
146 | 0 | resolved_write_method: None, |
147 | 0 | write_method_test_countdown: None, |
148 | 0 | notification_method: |
149 | 0 | super_stt_shared::models::notification_method::NotificationMethod::default(), |
150 | 0 | volume: 100, |
151 | 0 | last_committed_volume: 100, |
152 | 0 |
|
153 | 0 | // Custom models directory |
154 | 0 | custom_models_dir: None, |
155 | 0 | custom_models_dir_input: String::new(), |
156 | 0 |
|
157 | 0 | // Models page UI state |
158 | 0 | models_page: crate::state::models_page::ModelsPageState::default(), |
159 | 0 |
|
160 | 0 | // Transcription language state |
161 | 0 | language: crate::state::language::LanguageState::default(), |
162 | 0 |
|
163 | 0 | // Backend catalog + per-backend configuration state |
164 | 0 | backends: Vec::new(), |
165 | 0 | backend_secret_inputs: HashMap::new(), |
166 | 0 | backend_secret_configured: HashMap::new(), |
167 | 0 | backend_option_inputs: HashMap::new(), |
168 | 0 |
|
169 | 0 | // Registry state |
170 | 0 | registry: crate::state::registry::RegistryState::default(), |
171 | 0 |
|
172 | 0 | // Self-update state |
173 | 0 | update: crate::state::update::UpdateState::default(), |
174 | 0 |
|
175 | 0 | // No pending scoped action error at startup. |
176 | 0 | action_error: None, |
177 | 0 |
|
178 | 0 | // Measured once the window opens. |
179 | 0 | #[cfg(target_os = "macos")] |
180 | 0 | title_bar: super::macos::TitleBar::default(), |
181 | 0 |
|
182 | 0 | // The look `main` gave the app its theme from. |
183 | 0 | #[cfg(target_os = "macos")] |
184 | 0 | look: super::macos::appearance::launch_look(), |
185 | 0 |
|
186 | 0 | #[cfg(target_os = "macos")] |
187 | 0 | daemon_agent: None, |
188 | 0 | }; |
189 | | |
190 | | // On macOS the native frame and the app's own toolbar replace |
191 | | // libcosmic's header bar; see `macos`. |
192 | | // |
193 | | // libcosmic also draws a window edge — a 1px border, rounded corners, |
194 | | // and a transparent backdrop outside them — for compositors that |
195 | | // leave that to the client. It skips all three for a maximized |
196 | | // window, so the app tells it the window is maximized: the native |
197 | | // frame draws the edge. Only Wayland ever resets the flag. |
198 | | #[cfg(target_os = "macos")] |
199 | | { |
200 | | app.core.window.show_headerbar = false; |
201 | | app.core.window.is_maximized = true; |
202 | | } |
203 | | |
204 | | // Opened from `Super STT.app`, the app is what installs the daemon; |
205 | | // see `macos::bundle`. |
206 | | #[cfg(target_os = "macos")] |
207 | | if super::macos::bundle::in_bundle() { |
208 | | super::macos::bundle::request_notification_permission(); |
209 | | std::thread::spawn(super::macos::bundle::register_agents); |
210 | | } |
211 | | |
212 | | // Create startup commands |
213 | 0 | let title_command = app.update_title(); |
214 | 0 | (app, initial_load_tasks(title_command)) |
215 | 0 | } |
216 | | } |