1use crate::Config;
8use crate::app_colours::{AppColours, ColourTheme};
9use crate::config::{RuntimeConfig, SharedConfig};
10use crate::games::{
11 DecadesGameGui, LeftRightGameGui, OrderEntitiesGameGui, WereTheyAliveWhenGameGui,
12 WhichDateGameGui,
13};
14use crate::primary_window::{
15 AppInfoGui, BackupMergeRestoreGui, EntityCountsGui, SearchGui, SettingsGui, StatsGui,
16 TagCountsGui, TimelineCountsGui,
17};
18use crate::shortcuts::global_shortcuts;
19use crate::windows::{
20 AppColoursGui, BreakOutWindows, EntityEditGui, EntityViewGui, TagBulkEditGui, TagViewGui,
21 TimelineEditGui, TimelineViewGui,
22};
23use bool_tag_expr::Tag;
24use eframe::App;
25use eframe::egui::{
26 self, Align, Button, CentralPanel, Context, Layout, OpenUrl, Pos2, SidePanel, Ui, Vec2,
27};
28use open_timeline_core::OpenTimelineId;
29use open_timeline_crud::db_url_from_path;
30use open_timeline_gui_core::{
31 BreakOutWindow, Draw, Reload, using_wayland, widget_x_spacing, widget_y_spacing,
32};
33use sqlx::{Pool, Sqlite, SqlitePool};
34use std::sync::Arc;
35use std::time::Duration;
36use tokio::sync::RwLock;
37use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
38
39#[derive(Debug, PartialEq, Eq, Clone)]
41enum MainTabSelected {
42 Search,
43 Entities,
44 Tags,
45 Timelines,
46 Stats,
47 BackupRestoreMerge,
48
49 GameDecades,
50 GameLeftRight,
51 GameOrderEntities,
52 GameAliveWhen,
53 GameWhichDate,
54
55 Settings,
56 AppInfo,
57}
58
59impl MainTabSelected {
60 fn to_label_text(&self) -> String {
61 match self {
62 Self::Search => String::from("Search"),
63 Self::Entities => String::from("Entities"),
64 Self::Tags => String::from("Tags"),
65 Self::Timelines => String::from("Timelines"),
66 Self::Stats => String::from("Stats"),
67 Self::BackupRestoreMerge => String::from("Backup | Restore | Merge"),
68
69 Self::GameDecades => String::from("Decades"),
70 Self::GameLeftRight => String::from("Left/Right"),
71 Self::GameOrderEntities => String::from("Order Entities"),
72 Self::GameAliveWhen => String::from("Alive When"),
73 Self::GameWhichDate => String::from("Which Date"),
74
75 Self::Settings => String::from("Settings"),
76 Self::AppInfo => String::from("Information"),
77 }
78 }
79}
80
81#[derive(Debug)]
85pub enum ActionRequest {
86 Entity(EntityOrTimelineActionRequest),
87 Timeline(EntityOrTimelineActionRequest),
88 Tag(TagActionRequest),
89
90 AppColours(UnboundedSender<AppColours>),
92}
93
94#[derive(Debug)]
96pub enum EntityOrTimelineActionRequest {
97 CreateNew,
98 ViewExisting(OpenTimelineId),
99 EditExisting(OpenTimelineId),
100}
101
102#[derive(Debug)]
104pub enum TagActionRequest {
105 ViewExisting(Tag),
106 BulkEditExisting(Tag),
107}
108
109#[derive(Debug)]
112pub struct UnboundedChannel<T> {
113 pub tx: UnboundedSender<T>,
114 pub rx: UnboundedReceiver<T>,
115}
116
117impl<T> From<(UnboundedSender<T>, UnboundedReceiver<T>)> for UnboundedChannel<T> {
118 fn from(value: (UnboundedSender<T>, UnboundedReceiver<T>)) -> Self {
119 UnboundedChannel {
120 tx: value.0,
121 rx: value.1,
122 }
123 }
124}
125
126pub struct OpenTimelineApp {
128 position: Option<Pos2>,
130
131 tab_selected: MainTabSelected,
133
134 windows: BreakOutWindows,
136
137 search_gui: SearchGui,
139
140 entity_counts_gui: EntityCountsGui,
142
143 entity_tag_counts_gui: TagCountsGui,
146
147 timeline_counts_gui: TimelineCountsGui,
149
150 stats_gui: StatsGui,
152
153 backup_merge_restore_gui: BackupMergeRestoreGui,
155
156 settings_gui: SettingsGui,
158
159 app_info_gui: AppInfoGui,
161
162 channel_action_request: UnboundedChannel<ActionRequest>,
165
166 channel_crud_operation_executed: UnboundedChannel<()>,
171
172 reload_required: bool,
175
176 game_decades: DecadesGameGui,
178
179 game_left_right: LeftRightGameGui,
181
182 game_order_entities: OrderEntitiesGameGui,
184
185 game_were_they_alive_when: WereTheyAliveWhenGameGui,
187
188 game_which_date: WhichDateGameGui,
190
191 shared_config: SharedConfig,
193}
194
195impl OpenTimelineApp {
196 pub fn new() -> Self {
198 let channel_action_request: UnboundedChannel<ActionRequest> =
199 tokio::sync::mpsc::unbounded_channel().into();
200 let channel_crud_operation_executed: UnboundedChannel<()> =
201 tokio::sync::mpsc::unbounded_channel().into();
202
203 let (tx, rx) = tokio::sync::oneshot::channel();
205 tokio::spawn(async move {
206 let result = async move {
207 Config::ensure_setup().await?;
208 Config::load()
209 }
210 .await;
211 let _ = tx.send(result);
212 });
213 let config = match rx.blocking_recv().unwrap() {
215 Ok(config) => config,
216 Err(error) => panic!("Initial config error: {error}"),
217 };
218
219 let db_path = Arc::new(RwLock::new(config.database_path()));
221
222 let (tx, rx) = tokio::sync::oneshot::channel();
224 tokio::spawn(async move {
225 let result: Result<Pool<Sqlite>, sqlx::Error> = async move {
226 let db_path = db_path.read().await;
227 let db_url = db_url_from_path(&db_path);
228 let db_pool = SqlitePool::connect(&db_url).await?;
229 Ok(db_pool)
230 }
231 .await;
232 let _ = tx.send(result);
233 });
234 let db_pool = match rx.blocking_recv().unwrap() {
235 Ok(db_pool) => db_pool,
236 Err(error) => panic!("Initial SQLite pool error: {error}"),
237 };
238 let shared_config = Arc::new(RwLock::new(RuntimeConfig {
239 db_pool: db_pool,
240 config: config.clone(),
241 }));
242
243 Self {
244 position: None,
245 tab_selected: MainTabSelected::Search,
246 windows: BreakOutWindows::default(),
247 search_gui: SearchGui::new(
248 Arc::clone(&shared_config),
249 channel_action_request.tx.clone(),
250 ),
251 entity_counts_gui: EntityCountsGui::new(
252 Arc::clone(&shared_config),
253 channel_action_request.tx.clone(),
254 ),
255 entity_tag_counts_gui: TagCountsGui::new(
256 Arc::clone(&shared_config),
257 channel_action_request.tx.clone(),
258 ),
259 timeline_counts_gui: TimelineCountsGui::new(
260 Arc::clone(&shared_config),
261 channel_action_request.tx.clone(),
262 ),
263 stats_gui: StatsGui::new(Arc::clone(&shared_config)),
264 backup_merge_restore_gui: BackupMergeRestoreGui::new(
265 Arc::clone(&shared_config),
266 channel_crud_operation_executed.tx.clone(),
267 ),
268 settings_gui: SettingsGui::new(
269 config,
270 Arc::clone(&shared_config),
271 channel_action_request.tx.clone(),
272 channel_crud_operation_executed.tx.clone(),
273 ),
274 app_info_gui: AppInfoGui::new(),
275 channel_action_request,
276 channel_crud_operation_executed,
277 reload_required: false,
278 game_decades: DecadesGameGui::new(Arc::clone(&shared_config)),
279 game_left_right: LeftRightGameGui::new(Arc::clone(&shared_config)),
280 game_order_entities: OrderEntitiesGameGui::new(Arc::clone(&shared_config)),
281 game_were_they_alive_when: WereTheyAliveWhenGameGui::new(Arc::clone(&shared_config)),
282 game_which_date: WhichDateGameGui::new(Arc::clone(&shared_config)),
283 shared_config,
284 }
285 }
286
287 fn draw_side_bar_option(
288 &mut self,
289 _ctx: &Context,
290 ui: &mut Ui,
291 tab_variant: MainTabSelected,
292 separator_after: bool,
293 ) {
294 ui.with_layout(Layout::top_down_justified(Align::LEFT), |ui| {
295 let tab = Button::selectable(
296 self.tab_selected == tab_variant,
297 tab_variant.to_label_text(),
298 );
299 let tab = ui.add(tab);
300 if tab.clicked() {
301 self.tab_selected = tab_variant;
302 }
303 if separator_after {
304 ui.separator();
305 }
306 });
307 }
308
309 fn draw_side_panel(&mut self, ctx: &Context, ui: &mut Ui) {
310 let space = widget_y_spacing(ui);
311 ui.add_space(space * 2.0);
312 open_timeline_gui_core::Label::heading(ui, "OpenTimeline");
313 ui.separator();
314
315 ui.scope(|ui| {
317 let (button_fill, button_text) =
319 match self.shared_config.blocking_read().config.colour_theme {
320 ColourTheme::Custom(app_colours) => {
321 let fill = app_colours.donate_button_fill.into();
322 let text = app_colours.donate_button_text.into();
323 (fill, text)
324 }
325 _ => {
326 let fill = AppColours::default_donate_button_fill();
327 let text = AppColours::default_donate_button_text_colour();
328 (fill, text)
329 }
330 };
331
332 let style = ui.style_mut();
334 style.visuals.widgets.active.weak_bg_fill = button_fill;
335 style.visuals.widgets.inactive.weak_bg_fill = button_fill;
336 style.visuals.widgets.hovered.weak_bg_fill = button_fill;
337 style.visuals.override_text_color = Some(button_text);
338
339 let size = Vec2::new(ui.available_width(), 0.0);
341 let button = egui::Button::new("Donate");
342 if ui.add_sized(size, button).clicked() {
343 ctx.open_url(OpenUrl {
344 url: "https://www.open-timeline.org/donate".to_owned(),
345 new_tab: true,
346 });
347 }
348 });
349 ui.separator();
350
351 self.draw_side_bar_option(ctx, ui, MainTabSelected::Search, true);
352 self.draw_side_bar_option(ctx, ui, MainTabSelected::Entities, true);
353 self.draw_side_bar_option(ctx, ui, MainTabSelected::Tags, true);
354 self.draw_side_bar_option(ctx, ui, MainTabSelected::Timelines, true);
355 self.draw_side_bar_option(ctx, ui, MainTabSelected::Stats, true);
356 self.draw_side_bar_option(ctx, ui, MainTabSelected::BackupRestoreMerge, true);
357 ui.horizontal(|ui| {
358 let space = widget_x_spacing(ui) / 2.0;
359 ui.add_space(space);
360 ui.label("Games");
361 });
362
363 ui.indent("id_salt", |ui| {
364 self.draw_side_bar_option(ctx, ui, MainTabSelected::GameDecades, false);
365 self.draw_side_bar_option(ctx, ui, MainTabSelected::GameLeftRight, false);
366 self.draw_side_bar_option(ctx, ui, MainTabSelected::GameOrderEntities, false);
367 self.draw_side_bar_option(ctx, ui, MainTabSelected::GameAliveWhen, false);
368 self.draw_side_bar_option(ctx, ui, MainTabSelected::GameWhichDate, false);
369 });
370 ui.separator();
371
372 self.draw_side_bar_option(ctx, ui, MainTabSelected::Settings, false);
373 self.draw_side_bar_option(ctx, ui, MainTabSelected::AppInfo, false);
374 }
375
376 fn draw_central_panel(&mut self, ctx: &Context, ui: &mut Ui) {
377 open_timeline_gui_core::Label::heading(ui, &self.tab_selected.to_label_text());
378 ui.separator();
379
380 match self.tab_selected {
381 MainTabSelected::Search => {
382 self.windows.draw(ctx, ui);
383 self.search_gui.draw(ctx, ui);
384 }
385 MainTabSelected::Entities => {
386 self.windows.draw(ctx, ui);
387 self.entity_counts_gui.draw(ctx, ui);
388 }
389 MainTabSelected::Tags => {
390 self.windows.draw(ctx, ui);
391 self.entity_tag_counts_gui.draw(ctx, ui);
392 }
393 MainTabSelected::Timelines => {
394 self.windows.draw(ctx, ui);
395 self.timeline_counts_gui.draw(ctx, ui);
396 }
397 MainTabSelected::Stats => {
398 self.windows.draw(ctx, ui);
399 self.stats_gui.draw(ctx, ui);
400 }
401 MainTabSelected::BackupRestoreMerge => {
402 self.backup_merge_restore_gui.draw(ctx, ui);
403 }
404
405 MainTabSelected::GameDecades => self.game_decades.draw(ctx, ui),
406 MainTabSelected::GameLeftRight => self.game_left_right.draw(ctx, ui),
407 MainTabSelected::GameOrderEntities => self.game_order_entities.draw(ctx, ui),
408 MainTabSelected::GameAliveWhen => self.game_were_they_alive_when.draw(ctx, ui),
409 MainTabSelected::GameWhichDate => self.game_which_date.draw(ctx, ui),
410
411 MainTabSelected::Settings => {
412 self.windows.draw(ctx, ui);
413 self.settings_gui.draw(ctx, ui);
414 }
415 MainTabSelected::AppInfo => {
416 self.windows.draw(ctx, ui);
417 self.app_info_gui.draw(ctx, ui);
418 }
419 }
420 }
421
422 fn create_any_new_windows(&mut self, ctx: &Context) {
428 let db = Arc::clone(&self.shared_config);
429 let tx_crud = self.channel_crud_operation_executed.tx.clone();
430 let tx_req = self.channel_action_request.tx.clone();
431 if let Ok(msg) = self.channel_action_request.rx.try_recv() {
432 let window: Box<dyn BreakOutWindow> = match msg {
433 ActionRequest::Entity(action) => match action {
435 EntityOrTimelineActionRequest::CreateNew => Box::new(
436 EntityEditGui::new_window_for_creating_entity(db, tx_req, tx_crud),
437 ),
438 EntityOrTimelineActionRequest::EditExisting(id) => Box::new(
439 EntityEditGui::new_window_for_editing_entity(db, tx_req, tx_crud, id),
440 ),
441 EntityOrTimelineActionRequest::ViewExisting(id) => {
442 Box::new(EntityViewGui::new(db, tx_req, id))
443 }
444 },
445 ActionRequest::Timeline(action) => match action {
447 EntityOrTimelineActionRequest::CreateNew => Box::new(
448 TimelineEditGui::new_window_for_creating_timeline(db, tx_req, tx_crud),
449 ),
450 EntityOrTimelineActionRequest::EditExisting(id) => Box::new(
451 TimelineEditGui::new_window_for_editing_timeline(db, tx_req, tx_crud, id),
452 ),
453 EntityOrTimelineActionRequest::ViewExisting(id) => {
454 Box::new(TimelineViewGui::new(db, ctx, tx_req, id))
455 }
456 },
457 ActionRequest::Tag(action) => match action {
459 TagActionRequest::BulkEditExisting(tag) => {
460 Box::new(TagBulkEditGui::new(db, tx_req, tx_crud, tag))
461 }
462 TagActionRequest::ViewExisting(tag) => {
463 Box::new(TagViewGui::new(db, tx_req, tag))
464 }
465 },
466 ActionRequest::AppColours(tx_app_colours) => {
468 debug!("recv ActionRequest::AppColours");
469 let config = self.shared_config.blocking_read().config.clone();
471 Box::new(AppColoursGui::new(config, tx_req, tx_app_colours))
472 }
473 };
474 self.windows.insert(ctx, self.position, window);
475 }
476 }
477}
478
479impl App for OpenTimelineApp {
480 fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) {
481 ctx.request_repaint_after(Duration::from_millis(300));
482
483 self.settings_gui.check_for_app_colours_update();
486 AppColours::use_theme(ctx, self.settings_gui.theme());
487
488 self.position = match using_wayland() {
490 false => ctx.input(|i| i.viewport().outer_rect).map(|rect| rect.min),
491 true => None,
492 };
493
494 if let Ok(()) = self.channel_crud_operation_executed.rx.try_recv() {
496 self.reload_required = true;
497 self.windows.request_reload();
498 self.search_gui.request_reload();
499 self.entity_counts_gui.request_reload();
500 self.entity_tag_counts_gui.request_reload();
501 self.timeline_counts_gui.request_reload();
502 self.entity_tag_counts_gui.request_reload();
503 self.stats_gui.request_reload();
504 }
505
506 global_shortcuts(ctx, &mut self.channel_action_request.tx);
508
509 self.create_any_new_windows(ctx);
511
512 SidePanel::left("sidebar").show(ctx, |ui| {
514 self.draw_side_panel(ctx, ui);
515 });
516
517 CentralPanel::default().show(ctx, |ui| {
519 self.draw_central_panel(ctx, ui);
520 });
521
522 self.reload_required = false;
524 }
525}