use crate::app::{ActionRequest, UnboundedChannel};
use crate::app_colours::{AppColours, ColourTheme};
use crate::config::{Config, SharedConfig};
use eframe::egui::{self, Context, Response, Spinner, Ui};
use log::{error, info};
use open_timeline_crud::{CrudError, db_url_from_path};
use open_timeline_gui_core::Draw;
use open_timeline_gui_core::{DisplayStatus, GuiStatus};
use sqlx::SqlitePool;
use tokio::sync::mpsc::error::TryRecvError;
use tokio::sync::mpsc::{Receiver, Sender, UnboundedSender};
#[derive(Debug)]
pub struct SettingsGui {
config: Config,
status: Status,
shared_config: SharedConfig,
show_save_colours_button: bool,
tx_crud_operation_executed: UnboundedSender<()>,
tx_action_request: UnboundedSender<ActionRequest>,
channel_app_colours: UnboundedChannel<AppColours>,
rx_database_config_update: Option<Receiver<Result<(), CrudError>>>,
rx_theme_update: Option<Receiver<Result<(), CrudError>>>,
rx_switch_database_update: Option<Receiver<Result<(), CrudError>>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum Status {
Ready,
WaitingForResponse,
SuccessfullyChangedDatabase,
DatabaseHasDifferentSchema,
SuccessfullyChangedTheme,
CrudError(CrudError),
}
impl DisplayStatus for Status {
fn status_display(&self, ui: &mut Ui) -> Response {
match &self {
Self::Ready => ui.add(egui::Label::new(String::from("Ready")).truncate()),
Self::WaitingForResponse => ui.add(Spinner::new()),
Self::SuccessfullyChangedDatabase => {
ui.add(egui::Label::new(String::from("Successfully switched database")).truncate())
}
Self::DatabaseHasDifferentSchema => ui.add(
egui::Label::new(String::from(
"Error: selected database has incompatible schema",
))
.truncate(),
),
Self::SuccessfullyChangedTheme => {
ui.add(egui::Label::new(String::from("Successfully switched theme")).truncate())
}
Self::CrudError(error) => {
ui.add(egui::Label::new(format!("Error: {error}")).truncate())
}
}
}
}
impl SettingsGui {
pub fn new(
config: Config,
shared_config: SharedConfig,
tx_action_request: UnboundedSender<ActionRequest>,
tx_crud_operation_executed: UnboundedSender<()>,
) -> Self {
debug!("New SettingsGui. config = {config:?}");
Self {
config,
status: Status::Ready,
shared_config,
show_save_colours_button: false,
tx_crud_operation_executed,
tx_action_request,
channel_app_colours: tokio::sync::mpsc::unbounded_channel().into(),
rx_database_config_update: None,
rx_theme_update: None,
rx_switch_database_update: None,
}
}
pub fn theme(&self) -> ColourTheme {
self.config.colour_theme
}
fn draw_database_settings(&mut self, _ctx: &Context, ui: &mut Ui) {
open_timeline_gui_core::Label::sub_heading(ui, "Database");
let database_path = self.config.database_path().to_string_lossy().to_string();
ui.monospace(database_path);
ui.add_space(5.0);
self.select_existing_database(ui);
self.select_new_database(ui);
self.use_default_database(ui);
}
fn draw_app_colour_settings(&mut self, _ctx: &Context, ui: &mut Ui) {
open_timeline_gui_core::Label::sub_heading(ui, "Colour Theme");
let mut theme_changed = false;
ui.horizontal(|ui| {
theme_changed |= ui
.radio_value(&mut self.config.colour_theme, ColourTheme::System, "System")
.changed();
theme_changed |= ui
.radio_value(&mut self.config.colour_theme, ColourTheme::Light, "Light")
.changed();
theme_changed |= ui
.radio_value(&mut self.config.colour_theme, ColourTheme::Dark, "Dark")
.changed();
});
ui.horizontal(|ui| {
theme_changed |= ui
.radio_value(
&mut self.config.colour_theme,
ColourTheme::Siphonophore,
"Siphonophore",
)
.changed();
});
ui.horizontal(|ui| {
let app_colours = match self.config.colour_theme {
ColourTheme::Custom(app_colours) => app_colours,
_ => self.config.custom_theme,
};
theme_changed |= ui
.radio_value(
&mut self.config.colour_theme,
ColourTheme::Custom(app_colours),
"Custom",
)
.changed();
});
let mut user_requested_save_custom_colours = false;
if self.show_save_colours_button {
if open_timeline_gui_core::Button::tall_full_width(ui, "Save Custom Theme").clicked() {
user_requested_save_custom_colours = true;
}
}
if theme_changed || user_requested_save_custom_colours {
let (tx, rx) = tokio::sync::mpsc::channel(1);
self.rx_theme_update = Some(rx);
if let ColourTheme::Custom(custom_colours) = self.config.colour_theme {
self.config.custom_theme = custom_colours
}
self.switch_shared_colour_theme();
self.request_save(tx);
}
if let ColourTheme::Custom(_) = self.config.colour_theme {
if open_timeline_gui_core::Button::tall_full_width(ui, "Edit Custom Colours").clicked()
{
debug!("Requesting new window for custom app colour selection");
let _ = self.tx_action_request.send(ActionRequest::AppColours(
self.channel_app_colours.tx.clone(),
));
}
};
}
fn select_existing_database(&mut self, ui: &mut Ui) {
if open_timeline_gui_core::Button::tall_full_width(ui, "Use Existing Database File")
.clicked()
{
if let Some(db_path) = rfd::FileDialog::new().pick_file() {
println!("Selected file: {}", db_path.display());
self.config.set_database_path(&db_path);
let (tx, rx) = tokio::sync::mpsc::channel(1);
self.rx_database_config_update = Some(rx);
self.request_save(tx);
}
}
}
fn select_new_database(&mut self, ui: &mut Ui) {
if open_timeline_gui_core::Button::tall_full_width(ui, "Create & Use New Database File")
.clicked()
{
if let Some(db_path) = rfd::FileDialog::new().save_file() {
self.config.set_database_path(&db_path);
let (tx, rx) = tokio::sync::mpsc::channel(1);
self.rx_database_config_update = Some(rx);
self.request_save(tx);
}
}
}
fn use_default_database(&mut self, ui: &mut Ui) {
if open_timeline_gui_core::Button::tall_full_width(ui, "Use Default Database").clicked() {
self.config.set_to_default();
let (tx, rx) = tokio::sync::mpsc::channel(1);
self.rx_database_config_update = Some(rx);
self.request_save(tx);
}
}
fn request_save(&mut self, tx: Sender<Result<(), CrudError>>) {
self.status = Status::WaitingForResponse;
let config = self.config.clone();
tokio::spawn(async move {
let result = config.save().await;
let _ = tx.send(result).await;
});
}
fn request_switch_database_pools(&mut self) {
let shared_config = self.shared_config.clone();
let db_path = self.config.database_path();
let (tx, rx) = tokio::sync::mpsc::channel(1);
self.rx_switch_database_update = Some(rx);
tokio::spawn(async move {
let result = async move {
let mut shared_config = shared_config.write().await;
let db_url = db_url_from_path(&db_path);
(*shared_config).db_pool = SqlitePool::connect(&db_url).await?;
Ok(())
}
.await;
let _ = tx.send(result).await;
});
}
fn switch_shared_colour_theme(&mut self) {
let shared_config = self.shared_config.clone();
let config = self.config.clone();
tokio::spawn(async move {
let mut shared_config = shared_config.write().await;
(*shared_config).config = config;
debug!("Updated shared config = {shared_config:?}");
});
}
fn check_for_database_selection_update(&mut self) {
if let Some(rx) = self.rx_database_config_update.as_mut() {
match rx.try_recv() {
Ok(result) => {
self.rx_database_config_update = None;
match result {
Ok(()) => self.request_switch_database_pools(),
Err(CrudError::DbMigrate(error)) => {
self.status = Status::DatabaseHasDifferentSchema;
error!(
"Error - database is likely for a different application: {error}"
)
}
Err(error) => {
self.status = Status::CrudError(error.clone());
error!("Error: {error}");
}
}
}
Err(TryRecvError::Empty) => (),
Err(TryRecvError::Disconnected) => (),
}
}
}
fn check_for_theme_selection_update(&mut self) {
if let Some(rx) = self.rx_theme_update.as_mut() {
match rx.try_recv() {
Ok(result) => {
self.rx_theme_update = None;
self.show_save_colours_button = false;
match result {
Ok(()) => self.status = Status::SuccessfullyChangedTheme,
Err(error) => {
self.status = Status::CrudError(error.clone());
error!("Error: {error}");
}
}
}
Err(TryRecvError::Empty) => (),
Err(TryRecvError::Disconnected) => (),
}
}
}
fn check_for_database_pool_switch_update(&mut self) {
if let Some(rx) = self.rx_switch_database_update.as_mut() {
match rx.try_recv() {
Ok(result) => {
self.rx_switch_database_update = None;
match result {
Ok(()) => {
self.status = Status::SuccessfullyChangedDatabase;
info!("Database pool switched");
info!("Requesting search refresh");
let _ = self.tx_crud_operation_executed.send(());
}
Err(error) => {
self.status = Status::CrudError(error.clone());
error!("Error: {error}");
}
}
}
Err(TryRecvError::Empty) => (),
Err(TryRecvError::Disconnected) => (),
}
}
}
pub fn check_for_app_colours_update(&mut self) {
match self.channel_app_colours.rx.try_recv() {
Ok(app_colours) => {
debug!("Received app colours");
self.config.colour_theme = ColourTheme::Custom(app_colours);
self.show_save_colours_button = true;
self.switch_shared_colour_theme();
}
Err(TryRecvError::Empty) => (),
Err(TryRecvError::Disconnected) => (),
}
}
}
impl Draw for SettingsGui {
fn draw(&mut self, ctx: &Context, ui: &mut Ui) {
self.check_for_database_selection_update();
self.check_for_theme_selection_update();
self.check_for_database_pool_switch_update();
self.check_for_app_colours_update();
GuiStatus::display(ui, &self.status);
ui.separator();
ui.add_enabled_ui(self.status != Status::WaitingForResponse, |ui| {
self.draw_database_settings(ctx, ui);
self.draw_app_colour_settings(ctx, ui);
});
}
}