use crate::app::ActionRequest;
use crate::common::*;
use crate::components::TagGui;
use crate::config::SharedConfig;
use crate::consts::DEFAULT_WINDOW_SIZES;
use crate::shortcuts::global_shortcuts;
use crate::windows::{Deleted, DeletedStatus};
use bool_tag_expr::Tag;
use eframe::egui::{CentralPanel, Context, Vec2, ViewportId};
use open_timeline_crud::{CrudError, delete_all_matching_tags, update_all_matching_entity_tags};
use open_timeline_gui_core::{
BreakOutWindow, Draw, Reload, Valid, ValidityAsynchronous, window_has_focus,
};
use open_timeline_gui_core::{Shortcut, ShowRemoveButton};
use std::hash::{DefaultHasher, Hash, Hasher};
use std::sync::Arc;
use std::time::Instant;
use tokio::sync::mpsc::UnboundedSender;
use tokio::sync::mpsc::{Receiver, error::TryRecvError};
#[derive(Debug)]
pub struct TagBulkEditGui {
database_entry: Tag,
new_tag_gui: TagGui,
deleted_status: DeletedStatus,
status_str: String,
rx_update: Option<Receiver<Result<(), CrudError>>>,
rx_delete: Option<Receiver<Result<(), CrudError>>>,
tx_action_request: UnboundedSender<ActionRequest>,
tx_crud_operation_executed: UnboundedSender<()>,
wants_to_be_closed: bool,
shared_config: SharedConfig,
}
impl TagBulkEditGui {
pub fn new(
shared_config: SharedConfig,
tx_action_request: UnboundedSender<ActionRequest>,
tx_crud_operation_executed: UnboundedSender<()>,
tag: Tag,
) -> Self {
TagBulkEditGui {
database_entry: tag.clone(),
new_tag_gui: TagGui::from_tag(tag, ShowRemoveButton::No),
deleted_status: DeletedStatus::NotDeleted,
status_str: String::from("No changes to save"),
rx_update: None,
rx_delete: None,
tx_action_request,
tx_crud_operation_executed,
wants_to_be_closed: false,
shared_config,
}
}
pub fn tag(&self) -> &Tag {
&self.database_entry
}
fn request_update(&mut self) {
if self.has_been_deleted() {
return;
}
let validity = self.new_tag_gui.validity();
match validity {
ValidityAsynchronous::Valid => {
let as_opentimeline_type = self.new_tag_gui.to_opentimeline_type();
self.update(as_opentimeline_type);
}
ValidityAsynchronous::Invalid(error) => {
self.status_str = format!("Tag can't be updated (error: {error})");
}
ValidityAsynchronous::Waiting => {
self.status_str = String::from("Waiting for validation")
}
}
}
fn request_delete(&mut self) {
let validity = self.new_tag_gui.validity();
match validity {
ValidityAsynchronous::Valid => {
let as_opentimeline_type = self.new_tag_gui.to_opentimeline_type();
self.delete(as_opentimeline_type);
}
ValidityAsynchronous::Invalid(error) => {
self.status_str = format!("Tag can't be deleted (error: {error})");
}
ValidityAsynchronous::Waiting => {
self.status_str = String::from("Waiting for validation")
}
}
}
fn update(&mut self, new_tag: Tag) {
let (tx, rx) = tokio::sync::mpsc::channel(1);
self.rx_update = Some(rx);
let old_tag = self.tag().to_owned();
let shared_config = Arc::clone(&self.shared_config);
tokio::spawn(async move {
let result = async {
let mut transaction = shared_config.read().await.db_pool.begin().await?;
let _ = update_all_matching_entity_tags(&mut transaction, old_tag, new_tag).await?;
transaction.commit().await.map_err(|_| CrudError::DbError)?;
Ok(())
}
.await;
let _ = tx.send(result).await;
});
}
fn delete(&mut self, tag: Tag) {
let (tx, rx) = tokio::sync::mpsc::channel(1);
self.rx_delete = Some(rx);
let shared_config = Arc::clone(&self.shared_config);
tokio::spawn(async move {
let result = async {
let mut transaction = shared_config.read().await.db_pool.begin().await?;
delete_all_matching_tags(&mut transaction, tag).await?;
transaction.commit().await.map_err(|_| CrudError::DbError)?;
Ok(())
}
.await;
let _ = tx.send(result).await;
});
}
fn receive_any_crud_status_updates(&mut self) {
if let Some(rx) = self.rx_update.as_mut() {
match rx.try_recv() {
Ok(result) => match result {
Ok(()) => {
self.rx_update = None;
self.status_str = String::from("Updated tag");
self.database_entry = self.new_tag_gui.to_opentimeline_type();
let _ = self.tx_crud_operation_executed.send(());
}
Err(error) => {
self.rx_update = None;
self.status_str = format!("Failed to update tag: {error}");
}
},
Err(TryRecvError::Empty) => (),
Err(TryRecvError::Disconnected) => (),
}
}
if let Some(rx) = self.rx_delete.as_mut() {
let deleted_tag = self.database_entry.clone();
match rx.try_recv() {
Ok(result) => match result {
Ok(()) => {
self.rx_delete = None;
self.status_str = format!("Sucessfully deleted '{deleted_tag}'");
self.set_deleted_status(DeletedStatus::Deleted(Instant::now()));
let _ = self.tx_crud_operation_executed.send(());
}
Err(error) => {
self.rx_delete = None;
self.status_str = format!("Failed to delete '{deleted_tag}': {error}")
}
},
Err(TryRecvError::Empty) => (),
Err(TryRecvError::Disconnected) => (),
}
}
}
fn reset(&mut self) {
self.new_tag_gui = TagGui::from_tag(self.database_entry.clone(), ShowRemoveButton::No)
}
fn differs_from_database_entry(&self) -> Option<bool> {
if self.new_tag_gui.validity() == ValidityAsynchronous::Valid {
let current_tag = self.new_tag_gui.to_opentimeline_type();
Some(current_tag != self.database_entry)
} else {
None
}
}
}
impl Reload for TagBulkEditGui {
fn request_reload(&mut self) {
}
fn check_reload_response(&mut self) {
}
}
impl Deleted for TagBulkEditGui {
fn set_deleted_status(&mut self, deleted_status: DeletedStatus) {
self.deleted_status = deleted_status;
}
fn deleted_status(&self) -> DeletedStatus {
self.deleted_status
}
}
impl BreakOutWindow for TagBulkEditGui {
fn draw(&mut self, ctx: &Context) {
if window_has_focus(ctx) {
if Shortcut::save(ctx) {
self.request_update();
}
if Shortcut::close_window(ctx) {
self.wants_to_be_closed = true;
}
}
global_shortcuts(ctx, &mut self.tx_action_request);
self.check_reload_response();
CentralPanel::default().show(ctx, |ui| {
open_timeline_gui_core::Label::heading(ui, "Tag");
ui.separator();
ui.label(&self.status_str);
ui.separator();
if self.has_been_deleted() {
self.draw_deleted_message(ctx, ui);
if let DeletedStatus::Deleted(deleted_at) = self.deleted_status() {
let elapsed_secs = deleted_at.elapsed().as_secs() as i32;
let remaining_seconds = 5 - elapsed_secs;
if remaining_seconds < 1 {
self.wants_to_be_closed = true;
}
}
return;
}
self.receive_any_crud_status_updates();
ui.horizontal(|ui| {
if open_timeline_gui_core::Button::delete(ui).clicked() {
self.request_delete();
}
if self.differs_from_database_entry() != Some(false)
&& open_timeline_gui_core::Button::reset(ui).clicked()
{
self.reset();
}
if self.differs_from_database_entry() == Some(true)
&& self.new_tag_gui.validity() == ValidityAsynchronous::Valid
&& open_timeline_gui_core::Button::update(ui).clicked()
{
self.request_update();
}
});
ui.separator();
open_timeline_gui_core::Label::sub_heading(ui, "Existing");
ui.label(format!("{}", self.database_entry));
ui.separator();
open_timeline_gui_core::Label::sub_heading(ui, "New");
ui.add_enabled_ui(true, |ui| self.new_tag_gui.draw(ctx, ui));
});
}
fn default_size(&self) -> Vec2 {
Vec2::new(
DEFAULT_WINDOW_SIZES.tag_edit.width,
DEFAULT_WINDOW_SIZES.tag_edit.height,
)
}
fn viewport_id(&mut self) -> ViewportId {
ViewportId(eframe::egui::Id::from({
let mut hasher = DefaultHasher::new();
self.tag().hash(&mut hasher);
format!("tag_edit_{}", hasher.finish())
}))
}
fn title(&mut self) -> String {
format!("Edit Tag • {}", self.tag())
}
fn wants_to_be_closed(&mut self) -> bool {
self.wants_to_be_closed
}
}