use crate::cache::Cache;
use crate::gui::async_ops::*;
use crate::gui::message::Message;
use crate::gui::state::{AppState, GuiApp};
use crate::gui::update::common::{apply_alias_retroactively, refresh_filtered_tasks, save_config};
use crate::model::parser::validate_alias_integrity;
use crate::storage::{LOCAL_CALENDAR_HREF, LOCAL_TRASH_HREF, LocalCalendarRegistry, LocalStorage};
use iced::Task;
fn update_tab_visibility<F>(
app: &mut GuiApp,
val: bool,
field: F,
mode: crate::gui::state::SidebarMode,
) -> bool
where
F: FnOnce(&mut GuiApp) -> &mut bool,
{
if !val {
let others = [
app.show_calendars_tab,
app.show_tags_tab,
app.show_locations_tab,
app.show_goals_tab,
app.show_journal_tab,
]
.iter()
.filter(|&&v| v)
.count();
if others <= 1 {
return false;
}
}
*field(app) = val;
if !val && app.sidebar_mode == mode {
app.sidebar_mode = if app.show_calendars_tab {
crate::gui::state::SidebarMode::Calendars
} else if app.show_tags_tab {
crate::gui::state::SidebarMode::Categories
} else if app.show_locations_tab {
crate::gui::state::SidebarMode::Locations
} else if app.show_goals_tab {
crate::gui::state::SidebarMode::Goals
} else {
crate::gui::state::SidebarMode::Journal
};
}
true
}
fn apply_config_to_app(app: &mut GuiApp, config: &crate::config::Config) {
app.core_config = config.clone();
app.sort_calendars();
app.hidden_calendars = config.hidden_calendars.clone().into_iter().collect();
app.disabled_calendars = config.disabled_calendars.clone().into_iter().collect();
app.sort_cutoff_days = config.sort_cutoff_days;
app.tag_aliases = config.tag_aliases.clone();
app.hide_completed = config.hide_completed;
app.hide_fully_completed_tags = config.hide_fully_completed_tags;
app.hide_aliases_in_sidebar = config.hide_aliases_in_sidebar;
app.blur_when_unfocused = config.blur_when_unfocused;
app.show_inline_descriptions = config.show_inline_descriptions;
app.sort_standard_by_priority = config.sort_standard_by_priority;
app.paused_sort_behavior = config.paused_sort_behavior;
app.sort_tiebreak_recent = config.sort_tiebreak_recent;
app.sort_preset = config.sort_preset;
app.ui_scale = config.ui_scale;
app.show_priority_numbers = config.show_priority_numbers;
app.sync_settings = config.sync_settings;
app.current_theme = config.theme;
app.quick_filter_term = config.quick_filter_term.clone();
app.quick_filter_icon = config.quick_filter_icon.clone();
app.show_quick_filter = config.show_quick_filter;
app.show_calendars_tab = config.show_calendars_tab;
app.show_tags_tab = config.show_tags_tab;
app.show_locations_tab = config.show_locations_tab;
app.show_goals_tab = config.show_goals_tab;
app.show_journal_tab = config.show_journal_tab;
let at_least_one_tab = app.show_calendars_tab
|| app.show_tags_tab
|| app.show_locations_tab
|| app.show_goals_tab
|| app.show_journal_tab;
if !at_least_one_tab {
app.show_calendars_tab = true;
}
app.verify_sidebar_mode();
app.sidebar_is_hidden = config.sidebar_is_hidden;
app.sort_collections_by_size = config.sort_collections_by_size;
app.urgent_days = config.urgent_days_horizon;
app.urgent_prio = config.urgent_priority_threshold;
app.default_priority = config.default_priority;
app.start_grace_period_days = config.start_grace_period_days;
app.auto_reminders = config.auto_reminders;
app.default_reminder_time = config.default_reminder_time.clone();
app.snooze_short_mins = config.snooze_short_mins;
app.create_events_for_tasks = config.create_events_for_tasks;
app.delete_events_on_completion = config.delete_events_on_completion;
app.snooze_long_mins = config.snooze_long_mins;
app.auto_refresh_interval_mins = config.auto_refresh_interval_mins;
app.strikethrough_completed = config.strikethrough_completed;
app.trash_retention_days = config.trash_retention_days;
app.sessions_count_as_completions = config.sessions_count_as_completions;
app.session.expanded_tags = config.expanded_tags.clone();
app.session.expanded_locations = config.expanded_locations.clone();
app.pinned_actions = config.pinned_actions.clone();
app.ob_sort_days_input = match config.sort_cutoff_days {
Some(d) => d.to_string(),
None => "".to_string(),
};
app.ob_urgent_days_input = app.urgent_days.to_string();
app.ob_urgent_prio_input = app.urgent_prio.to_string();
app.ob_default_priority_input = app.default_priority.to_string();
app.ob_start_grace_input = app.start_grace_period_days.to_string();
app.ob_snooze_short_input =
crate::model::parser::format_duration_compact(config.snooze_short_mins);
app.ob_snooze_long_input =
crate::model::parser::format_duration_compact(config.snooze_long_mins);
app.ob_auto_refresh_input =
crate::model::parser::format_duration_compact(config.auto_refresh_interval_mins);
app.ob_trash_retention_input = config.trash_retention_days.to_string();
app.ob_default_duration_goal_mins_input = config.default_duration_goal_mins.to_string();
app.ob_max_done_roots_input = config.max_done_roots.to_string();
app.ob_max_done_subtasks_input = config.max_done_subtasks.to_string();
app.ob_quick_filter_term_input = config.quick_filter_term.clone();
app.ob_quick_filter_icon_input = config.quick_filter_icon.clone();
app.log_level = config.log_level;
app.first_day_of_week = config.first_day_of_week;
crate::system::set_log_level(config.log_level.to_level_filter());
}
pub fn handle(app: &mut GuiApp, message: Message) -> Task<Message> {
match message {
Message::ConfigLoaded(Ok(config_box)) => {
let config = *config_box;
app.ob_password_visible = false;
let locals = LocalCalendarRegistry::load(app.ctx.as_ref()).unwrap_or_default();
app.local_cals_editing = locals.clone();
app.remote_cals_editing = app
.calendars
.iter()
.filter(|c| !c.href.starts_with("local://"))
.cloned()
.collect();
app.ob_url = config.url.clone();
app.ob_user = config.username.clone();
app.ob_pass = config.password.clone();
app.ob_insecure = config.allow_insecure_certs;
app.ob_tls_client_cert_path = config.tls_client_cert_path.clone().unwrap_or_default();
app.ob_tls_client_key_path = config.tls_client_key_path.clone().unwrap_or_default();
app.ob_default_cal = config.default_calendar.clone();
apply_config_to_app(app, &config);
let mut cached_cals = Cache::load_calendars(app.ctx.as_ref()).unwrap_or_default();
for loc in locals {
if !cached_cals.iter().any(|c| c.href == loc.href) {
cached_cals.push(loc);
}
}
app.calendars = cached_cals;
app.sort_calendars();
app.store.clear();
for cal in &app.calendars {
if cal.href.starts_with("local://") {
if let Ok(mut tasks) = LocalStorage::load_for_href(app.ctx.as_ref(), &cal.href)
{
crate::journal::Journal::apply_to_tasks(
app.ctx.as_ref(),
&mut tasks,
&cal.href,
);
app.store.insert(cal.href.clone(), tasks);
}
} else if let Ok((mut tasks, _)) = Cache::load(app.ctx.as_ref(), &cal.href) {
crate::journal::Journal::apply_to_tasks(
app.ctx.as_ref(),
&mut tasks,
&cal.href,
);
app.store.insert(cal.href.clone(), tasks);
}
}
let mut target_href = None;
if let Some(def) = &app.ob_default_cal
&& let Some(cal) = app
.calendars
.iter()
.find(|c| c.name == *def || c.href == *def)
{
if app.hidden_calendars.contains(&cal.href) {
app.hidden_calendars.remove(&cal.href);
}
target_href = Some(cal.href.clone());
}
if target_href.is_none() {
target_href = app
.calendars
.iter()
.find(|c| {
!app.hidden_calendars.contains(&c.href)
&& !app.disabled_calendars.contains(&c.href)
&& c.href != LOCAL_TRASH_HREF
&& c.href != "local://recovery"
})
.map(|c| c.href.clone());
}
if target_href.is_none() {
target_href = Some(LOCAL_CALENDAR_HREF.to_string());
}
app.active_cal_href = target_href.clone();
app.ob_default_cal = target_href;
refresh_filtered_tasks(app);
app.state = AppState::Active;
app.loading = true;
Task::perform(connect_and_fetch_wrapper(app.ctx.clone(), config), |res| {
Message::Loaded(res.map_err(|e| e.to_string()))
})
}
Message::ConfigUpdated(config_box) => {
let config = *config_box;
apply_config_to_app(app, &config);
refresh_filtered_tasks(app);
Task::none()
}
Message::ConfigLoaded(Err(e)) => {
app.state = AppState::Onboarding;
if !e.contains("Config file not found") {
app.error_msg = Some(
rust_i18n::t!("config_error_prefix", error = e)
.to_string()
.trim()
.to_owned(),
);
app.config_was_corrupted = true;
}
Task::none()
}
Message::ObUrlChanged(v) => {
app.ob_url = v;
Task::none()
}
Message::ObUserChanged(v) => {
app.ob_user = v;
Task::none()
}
Message::ObPassChanged(v) => {
app.ob_pass = v;
Task::none()
}
Message::ToggleObPasswordVisibility => {
app.ob_password_visible = !app.ob_password_visible;
Task::none()
}
Message::ObDefaultCalChanged(v) => {
app.ob_default_cal = Some(v);
save_config(app);
Task::none()
}
Message::SetTlsClientCertPath(val) => {
app.ob_tls_client_cert_path = val;
Task::none()
}
Message::SetTlsClientKeyPath(val) => {
app.ob_tls_client_key_path = val;
Task::none()
}
Message::ThemeChanged(theme) => {
app.current_theme = theme;
save_config(app);
Task::none()
}
Message::ObSubmit => {
app.ob_password_visible = false;
app.calendars.retain(|c| !c.href.starts_with("local://"));
app.calendars.extend(app.local_cals_editing.clone());
app.sort_calendars();
if app.ob_sort_days_input.trim().is_empty() {
app.sort_cutoff_days = None;
} else if let Ok(n) = app.ob_sort_days_input.trim().parse::<u32>() {
app.sort_cutoff_days = Some(n);
}
let config_to_save = save_config(app);
app.state = AppState::Loading;
app.error_msg = Some(rust_i18n::t!("connecting").to_string());
Task::perform(
connect_and_fetch_wrapper(app.ctx.clone(), config_to_save),
|res| Message::Loaded(res.map_err(|e| e.to_string())),
)
}
Message::OpenSettings => {
let cfg = &app.core_config;
app.ob_password_visible = false;
app.ob_url = cfg.url.clone();
app.ob_user = cfg.username.clone();
app.remote_cals_editing = app
.calendars
.iter()
.filter(|c| !c.href.starts_with("local://"))
.cloned()
.collect();
let mut target_href = cfg.default_calendar.clone();
if let Some(ref def) = target_href
&& let Some(found) = app
.calendars
.iter()
.find(|c| c.name == *def || c.href == *def)
{
target_href = Some(found.href.clone());
}
app.ob_default_cal = target_href;
app.hide_completed = cfg.hide_completed;
app.hide_fully_completed_tags = cfg.hide_fully_completed_tags;
app.hide_aliases_in_sidebar = cfg.hide_aliases_in_sidebar;
app.ob_insecure = cfg.allow_insecure_certs;
app.ob_tls_client_cert_path = cfg.tls_client_cert_path.clone().unwrap_or_default();
app.ob_tls_client_key_path = cfg.tls_client_key_path.clone().unwrap_or_default();
app.hidden_calendars = cfg.hidden_calendars.iter().cloned().collect();
app.tag_aliases = cfg.tag_aliases.clone();
app.sort_cutoff_days = cfg.sort_cutoff_days;
app.current_theme = cfg.theme;
app.ob_sort_days_input = match cfg.sort_cutoff_days {
Some(d) => d.to_string(),
None => "".to_string(),
};
app.trash_retention_days = cfg.trash_retention_days;
app.ob_trash_retention_input = cfg.trash_retention_days.to_string();
app.ob_quick_filter_term_input = cfg.quick_filter_term.clone();
app.ob_quick_filter_icon_input = cfg.quick_filter_icon.clone();
app.show_quick_filter = cfg.show_quick_filter;
app.sidebar_is_hidden = cfg.sidebar_is_hidden;
app.log_level = cfg.log_level;
app.state = AppState::Settings;
app.editing_alias_key = None;
app.alias_input_key.clear();
app.alias_input_values.clear();
Task::none()
}
Message::CancelSettings => {
app.ob_password_visible = false;
app.verify_sidebar_mode();
app.calendars.retain(|c| !c.href.starts_with("local://"));
app.calendars.extend(app.local_cals_editing.clone());
app.sort_calendars();
app.editing_alias_key = None;
app.alias_input_key.clear();
app.alias_input_values.clear();
save_config(app);
refresh_filtered_tasks(app);
app.state = AppState::Active;
Task::none()
}
Message::ObSubmitOffline => {
app.ob_password_visible = false;
app.ob_url.clear();
app.ob_user.clear();
app.ob_pass.clear();
let config_to_save = save_config(app);
app.state = AppState::Loading;
Task::perform(
connect_and_fetch_wrapper(app.ctx.clone(), config_to_save),
|res| Message::Loaded(res.map_err(|e| e.to_string())),
)
}
Message::AliasKeyInput(v) => {
app.alias_input_key = v;
Task::none()
}
Message::AliasValueInput(v) => {
app.alias_input_values = v;
Task::none()
}
Message::EditAlias(key, vals) => {
app.editing_alias_key = Some(key.clone());
app.alias_input_key = if key.starts_with("@@") {
key
} else {
format!("#{}", key)
};
app.alias_input_values = vals;
Task::none()
}
Message::CancelEditAlias => {
app.editing_alias_key = None;
app.alias_input_key.clear();
app.alias_input_values.clear();
app.editing_goal_key = None;
app.goal_input_key.clear();
app.goal_input_target.clear();
app.goal_input_amount = "1".to_string();
app.goal_input_type = crate::config::GoalType::Count;
app.goal_input_unit = crate::config::IntervalUnit::Weeks;
Task::none()
}
Message::AddAlias => {
if !app.alias_input_key.is_empty() && !app.alias_input_values.is_empty() {
let tags = crate::model::parser::parse_alias_values(&app.alias_input_values);
if !tags.is_empty() {
let raw_key = app.alias_input_key.trim();
let key = if raw_key.starts_with("@@") {
raw_key.to_string()
} else if raw_key.to_lowercase().starts_with("loc:") {
format!("@@{}", raw_key[4..].trim())
} else {
raw_key.trim_start_matches('#').to_string()
};
match validate_alias_integrity(&key, &tags, &app.tag_aliases) {
Ok(_) => {
if let Some(old_key) = &app.editing_alias_key
&& old_key != &key
{
app.tag_aliases.remove(old_key);
}
app.editing_alias_key = None;
app.tag_aliases.insert(key.clone(), tags.clone());
app.alias_input_key.clear();
app.alias_input_values.clear();
app.error_msg = None;
save_config(app);
let modified_tasks = apply_alias_retroactively(app, &key, &tags);
if !modified_tasks.is_empty() {
let actions: Vec<_> = modified_tasks
.into_iter()
.map(crate::journal::Action::Update)
.collect();
if let Some(tx) = &app.bg_tx {
let _ = tx.try_send(
crate::gui::async_ops::WorkerCommand::Batch(actions),
);
}
}
}
Err(e) => {
app.error_msg = Some(format!(
"{}",
rust_i18n::t!("error_adding_alias", error = e)
));
}
}
}
}
Task::none()
}
Message::RemoveAlias(key) => {
if app.editing_alias_key.as_ref() == Some(&key) {
app.editing_alias_key = None;
app.alias_input_key.clear();
app.alias_input_values.clear();
}
app.tag_aliases.remove(&key);
save_config(app);
Task::none()
}
Message::GoalKeyInput(v) => {
app.goal_input_key = v;
Task::none()
}
Message::GoalTargetInput(v) => {
if v.is_empty() || v.chars().all(|c| c.is_ascii_alphanumeric()) {
app.goal_input_target = v;
}
Task::none()
}
Message::GoalTypeChanged(t) => {
app.goal_input_type = t;
Task::none()
}
Message::GoalAmountChanged(v) => {
if v.is_empty() || v.chars().all(|c| c.is_numeric()) {
app.goal_input_amount = v;
}
Task::none()
}
Message::GoalUnitChanged(u) => {
app.goal_input_unit = u;
Task::none()
}
Message::EditGoal(key, goal) => {
app.editing_goal_key = Some(key.clone());
app.goal_input_key = key;
app.goal_input_type = goal.goal_type;
app.goal_input_target = goal.target.to_string();
app.goal_input_amount = goal.interval.amount.to_string();
app.goal_input_unit = goal.interval.unit;
Task::none()
}
Message::CancelEditGoal => {
app.editing_goal_key = None;
app.goal_input_key.clear();
app.goal_input_target.clear();
app.goal_input_amount = "1".to_string();
app.goal_input_type = crate::config::GoalType::Count;
app.goal_input_unit = crate::config::IntervalUnit::Weeks;
Task::none()
}
Message::AddGoal => {
let mut key = app.goal_input_key.trim().to_string();
if key.to_lowercase().starts_with("loc:") {
key = format!("@@{}", key[4..].trim());
} else if !key.starts_with('#') && !key.starts_with("@@") {
key = format!("#{}", key);
}
let target_str = app.goal_input_target.trim();
let target = if app.goal_input_type == crate::config::GoalType::Duration {
crate::model::parser::parse_duration(target_str)
.unwrap_or_else(|| target_str.parse().unwrap_or(0))
} else {
target_str.parse().unwrap_or(0)
};
if !key.is_empty() && target > 0 {
let old_config = app.core_config.clone();
if let Some(old_key) = &app.editing_goal_key
&& old_key != &key
{
app.core_config.goals.remove(old_key);
}
let amount = app.goal_input_amount.parse().unwrap_or(1).max(1);
let goal = crate::config::Goal {
goal_type: app.goal_input_type,
target,
interval: crate::config::Interval {
amount,
unit: app.goal_input_unit,
},
};
app.core_config.goals.insert(key, goal);
app.core_config
.update_sync_timestamp_if_changed(&old_config);
if app.core_config.settings_updated_at != old_config.settings_updated_at
&& let Some(tx) = &app.bg_tx
{
let _ = tx.try_send(crate::gui::async_ops::WorkerCommand::SyncNow);
}
app.editing_goal_key = None;
app.goal_input_key.clear();
app.goal_input_target.clear();
app.goal_input_amount = "1".to_string();
app.goal_input_type = crate::config::GoalType::Count;
app.goal_input_unit = crate::config::IntervalUnit::Weeks;
save_config(app);
}
Task::none()
}
Message::RemoveGoal(key) => {
if app.editing_goal_key.as_ref() == Some(&key) {
app.editing_goal_key = None;
app.goal_input_key.clear();
app.goal_input_target.clear();
}
let old_config = app.core_config.clone();
app.core_config.goals.remove(&key);
app.core_config
.update_sync_timestamp_if_changed(&old_config);
if app.core_config.settings_updated_at != old_config.settings_updated_at
&& let Some(tx) = &app.bg_tx
{
let _ = tx.try_send(crate::gui::async_ops::WorkerCommand::SyncNow);
}
save_config(app);
Task::none()
}
Message::SetDefaultReminderTime(val) => {
app.default_reminder_time = val;
save_config(app);
Task::none()
}
Message::SetLanguage(lang) => {
let lang_opt = if lang == "auto" {
None
} else {
Some(lang.clone())
};
app.language = lang_opt.clone();
if let Some(ref l) = lang_opt {
crate::config::set_locale_with_fallback(l);
} else if let Some(sys_lang) = sys_locale::get_locale() {
crate::config::set_locale_with_fallback(&sys_lang);
} else {
crate::model::parser::rebuild_lexicon();
}
let mut cfg = app.core_config.clone();
cfg.language = app.language.clone();
app.core_config = cfg.clone();
let _ = cfg.save(app.ctx.as_ref());
Task::none()
}
Message::SetFirstDayOfWeek(val) => {
app.first_day_of_week = val;
save_config(app);
Task::none()
}
Message::ToggleAdvancedSettings(val) => {
app.show_advanced_settings = val;
Task::none()
}
Message::SetLogLevel(level) => {
app.log_level = level;
app.core_config.log_level = level;
save_config(app);
crate::system::set_log_level(level.to_level_filter());
Task::none()
}
Message::TogglePinnedAction(action, enabled) => {
if enabled {
if !app.pinned_actions.contains(&action) {
app.pinned_actions.push(action);
}
} else {
app.pinned_actions.retain(|a| *a != action);
}
crate::gui::update::common::save_config(app);
Task::none()
}
Message::MoveCalendar(href, direction) => {
let mut current_order = app.core_config.collection_order.clone();
for cal in &app.calendars {
if !current_order.contains(&cal.href)
&& cal.href != crate::storage::LOCAL_TRASH_HREF
&& cal.href != "local://recovery"
{
current_order.push(cal.href.clone());
}
}
if let Some(idx) = current_order.iter().position(|h| h == &href) {
let new_idx = (idx as i32 + direction as i32)
.clamp(0, (current_order.len() - 1) as i32)
as usize;
if idx != new_idx {
current_order.swap(idx, new_idx);
app.core_config.collection_order = current_order;
crate::gui::update::common::save_config(app);
app.sort_calendars();
crate::gui::update::common::refresh_filtered_tasks(app);
}
}
Task::none()
}
Message::SetNumericField(field, val) => {
if val.is_empty() || val.chars().all(|c| c.is_numeric()) {
macro_rules! set_num {
($input:ident, $target:expr) => {{
app.$input = val.clone();
if let Ok(n) = val.trim().parse() {
$target = n;
}
}};
($input:ident) => {{
app.$input = val.clone();
}};
}
match field {
crate::gui::message::NumericField::SortDays => {
app.ob_sort_days_input = val.clone();
app.sort_cutoff_days = if val.trim().is_empty() {
None
} else {
val.trim().parse().ok()
};
}
crate::gui::message::NumericField::UrgentDays => {
set_num!(ob_urgent_days_input, app.urgent_days);
}
crate::gui::message::NumericField::UrgentPrio => {
set_num!(ob_urgent_prio_input, app.urgent_prio);
}
crate::gui::message::NumericField::DefaultPriority => {
app.ob_default_priority_input = val.clone();
if let Ok(n) = val.trim().parse()
&& n > 0
{
app.default_priority = n;
}
}
crate::gui::message::NumericField::StartGrace => {
set_num!(ob_start_grace_input, app.start_grace_period_days);
}
crate::gui::message::NumericField::TrashRetention => {
set_num!(ob_trash_retention_input, app.trash_retention_days);
}
crate::gui::message::NumericField::DefaultDurationGoal => {
app.ob_default_duration_goal_mins_input = val.clone();
if let Ok(n) = val.trim().parse() {
app.core_config.default_duration_goal_mins = n;
}
}
crate::gui::message::NumericField::MaxDoneRoots => {
set_num!(ob_max_done_roots_input);
}
crate::gui::message::NumericField::MaxDoneSubtasks => {
set_num!(ob_max_done_subtasks_input);
}
}
save_config(app);
refresh_filtered_tasks(app);
}
Task::none()
}
Message::SetStringField(field, val) => {
match field {
crate::gui::message::StringField::SnoozeShort => {
app.ob_snooze_short_input = val.clone();
if let Some(n) = crate::model::parser::parse_duration(&val) {
app.snooze_short_mins = n;
}
}
crate::gui::message::StringField::SnoozeLong => {
app.ob_snooze_long_input = val.clone();
if let Some(n) = crate::model::parser::parse_duration(&val) {
app.snooze_long_mins = n;
}
}
crate::gui::message::StringField::AutoRefresh => {
app.ob_auto_refresh_input = val.clone();
if let Some(n) = crate::model::parser::parse_duration(&val) {
app.auto_refresh_interval_mins = n;
}
}
crate::gui::message::StringField::QuickFilterTerm => {
app.ob_quick_filter_term_input = val.clone();
app.quick_filter_term = val;
refresh_filtered_tasks(app);
}
crate::gui::message::StringField::QuickFilterIcon => {
app.ob_quick_filter_icon_input = val.clone();
app.quick_filter_icon = val;
}
}
save_config(app);
Task::none()
}
Message::ToggleField(field, val) => {
use crate::gui::message::BoolField;
match field {
BoolField::HideCompleted => app.hide_completed = val,
BoolField::HideFullyCompletedTags => app.hide_fully_completed_tags = val,
BoolField::HideAliasesInSidebar => app.hide_aliases_in_sidebar = val,
BoolField::SortStandardByPriority => app.sort_standard_by_priority = val,
BoolField::SortTiebreakRecent => app.sort_tiebreak_recent = val,
BoolField::AutoReminders => app.auto_reminders = val,
BoolField::DeleteEventsOnCompletion => app.delete_events_on_completion = val,
BoolField::SessionsCountAsCompletions => {
app.sessions_count_as_completions = val;
app.core_config.sessions_count_as_completions = val;
}
BoolField::ShowPriorityNumbers => app.show_priority_numbers = val,
BoolField::ShowInlineDescriptions => app.show_inline_descriptions = val,
BoolField::SyncSettings => app.sync_settings = val,
BoolField::BlurWhenUnfocused => app.blur_when_unfocused = val,
BoolField::ShowQuickFilter => app.show_quick_filter = val,
BoolField::ShowTaskGoalsInSidebar => {
app.core_config.show_task_goals_in_sidebar = val
}
BoolField::ObInsecure => app.ob_insecure = val,
BoolField::CreateEventsForTasks => {
let was_disabled = !app.create_events_for_tasks;
app.create_events_for_tasks = val;
save_config(app);
if val
&& was_disabled
&& let Some(client) = &app.client
{
let all_tasks: Vec<_> = app
.store
.calendars
.values()
.flat_map(|m| m.values())
.cloned()
.collect();
return Task::perform(
async_backfill_events_wrapper(
client.clone(),
all_tasks,
val,
app.delete_events_on_completion,
),
Message::BackfillEventsComplete,
);
}
return Task::none();
}
BoolField::ShowCalendarsTab => {
update_tab_visibility(
app,
val,
|a| &mut a.show_calendars_tab,
crate::gui::state::SidebarMode::Calendars,
);
}
BoolField::ShowTagsTab => {
update_tab_visibility(
app,
val,
|a| &mut a.show_tags_tab,
crate::gui::state::SidebarMode::Categories,
);
}
BoolField::ShowLocationsTab => {
update_tab_visibility(
app,
val,
|a| &mut a.show_locations_tab,
crate::gui::state::SidebarMode::Locations,
);
}
BoolField::ShowGoalsTab => {
update_tab_visibility(
app,
val,
|a| &mut a.show_goals_tab,
crate::gui::state::SidebarMode::Goals,
);
}
BoolField::ShowJournalTab => {
update_tab_visibility(
app,
val,
|a| &mut a.show_journal_tab,
crate::gui::state::SidebarMode::Journal,
);
}
BoolField::SortCollectionsBySize => {
app.sort_collections_by_size = val;
app.sort_calendars();
}
BoolField::StrikethroughCompleted => app.strikethrough_completed = val,
}
save_config(app);
if field != BoolField::ObInsecure
&& field != BoolField::AutoReminders
&& field != BoolField::DeleteEventsOnCompletion
{
refresh_filtered_tasks(app);
}
Task::none()
}
Message::DeleteAllCalendarEvents => {
let client_opt = app.client.clone();
if let Some(client) = client_opt {
app.deleting_events = true;
app.create_events_for_tasks = false;
save_config(app);
let cals: Vec<String> = app.store.calendars.keys().cloned().collect();
return Task::perform(async_delete_all_events_wrapper(client, cals), |res| {
Message::BackfillEventsComplete(res.map_err(|e| e.to_string()))
});
}
Task::none()
}
Message::BackfillEventsComplete(Ok(count)) => {
app.deleting_events = false;
app.last_sync_failed = false;
if count > 0 {
let action = if app.create_events_for_tasks {
rust_i18n::t!("calendar_action_created")
} else {
rust_i18n::t!("calendar_action_deleted")
};
let translated = if count == 1 {
rust_i18n::t!(
"calendar_events_changed.one",
action = action,
count = count
)
.to_string()
} else {
rust_i18n::t!(
"calendar_events_changed.other",
action = action,
count = count
)
.to_string()
};
app.error_msg = Some(format!("✓ {}", translated));
} else {
app.error_msg = Some(rust_i18n::t!("no_events_changed").to_string());
}
Task::none()
}
Message::BackfillEventsComplete(Err(e)) => {
app.deleting_events = false;
app.last_sync_failed = true;
app.error_msg = Some(format!("Backfill error: {}", e));
Task::none()
}
Message::ExportLocalIcs(calendar_href) => {
let tasks_result = if calendar_href.starts_with("local://") {
LocalStorage::load_for_href(app.ctx.as_ref(), &calendar_href)
} else {
Ok(app
.store
.calendars
.get(&calendar_href)
.map(|m| m.values().cloned().collect::<Vec<_>>())
.unwrap_or_default())
};
let cal_name = calendar_href.clone();
Task::perform(
async move {
let tasks = tasks_result.map_err(|e| e.to_string())?;
let ics_content = LocalStorage::to_ics_string(&tasks);
let cal_id = if let Some(local_id) = cal_name.strip_prefix("local://") {
local_id.to_string()
} else {
cal_name
.trim_end_matches('/')
.rsplit('/')
.next()
.unwrap_or("backup")
.to_string()
};
let filename = format!("cfait_{}.ics", cal_id);
let file_handle = rfd::AsyncFileDialog::new()
.add_filter("Calendar", &["ics"])
.set_file_name(&filename)
.save_file()
.await;
if let Some(handle) = file_handle {
let path = handle.path().to_path_buf();
#[cfg(not(target_arch = "wasm32"))]
{
use tokio::io::AsyncWriteExt;
let mut file = tokio::fs::File::create(&path)
.await
.map_err(|e| e.to_string())?;
file.write_all(ics_content.as_bytes())
.await
.map_err(|e| e.to_string())?;
}
Ok(path)
} else {
Err(rust_i18n::t!("export_cancelled").to_string())
}
},
Message::ExportSaved,
)
}
Message::ExportSaved(Ok(path)) => {
app.error_msg = Some(
rust_i18n::t!(
"export_success",
file = path.file_name().unwrap_or_default().to_string_lossy()
)
.to_string(),
);
Task::none()
}
Message::ExportSaved(Err(e)) => {
if e != rust_i18n::t!("export_cancelled") {
app.error_msg = Some(rust_i18n::t!("export_error", error = e).to_string());
}
Task::none()
}
Message::ImportLocalIcs(calendar_href) => {
let ctx = app.ctx.clone();
let controller = app.controller.clone();
let is_local = calendar_href.starts_with("local://");
Task::perform(
async move {
if let Some(file) = rfd::AsyncFileDialog::new()
.add_filter("iCalendar", &["ics"])
.pick_file()
.await
{
let content = file.read().await;
match String::from_utf8(content) {
Ok(ics_content) => {
let result: Result<usize, String> = if is_local {
LocalStorage::import_from_ics(
ctx.as_ref(),
&calendar_href,
&ics_content,
)
.map_err(|e| e.to_string())
} else {
let tasks =
match LocalStorage::parse_ics(&calendar_href, &ics_content)
{
Ok(t) => t,
Err(e) => {
return Err(rust_i18n::t!(
"import_failed",
error = e.to_string()
)
.to_string());
}
};
let mut count = 0;
for task in tasks {
if controller.create_task(task).await.is_ok() {
count += 1;
}
}
if count == 0 {
return Err("Failed to import any tasks".to_string());
}
Ok(count)
};
match result {
Ok(count) => {
let file_name = file.file_name();
let msg = if count == 1 {
rust_i18n::t!(
"import_success_from_file.one",
file = file_name
)
.to_string()
} else {
rust_i18n::t!(
"import_success_from_file.other",
count = count,
file = file_name
)
.to_string()
};
Ok(msg)
}
Err(e) => {
Err(rust_i18n::t!("import_failed", error = e).to_string())
}
}
}
Err(e) => Err(rust_i18n::t!(
"import_failed_read_text",
error = e.to_string()
)
.to_string()),
}
} else {
Err(rust_i18n::t!("import_cancelled").to_string())
}
},
Message::ImportCompleted,
)
}
Message::ImportCompleted(Ok(msg)) => {
app.error_msg = Some(msg);
if let Some(client) = &app.client {
app.loading = true;
app.pending_refresh_generation = app.edit_generation;
return Task::perform(
async_fetch_all_wrapper(client.clone(), app.calendars.clone()),
|res| Message::RefreshedAll(res.map_err(|e| e.to_string())),
);
}
Task::none()
}
Message::ImportCompleted(Err(e)) => {
if e != rust_i18n::t!("import_cancelled") {
app.error_msg = Some(e);
}
Task::none()
}
Message::AddLocalCalendar => {
use uuid::Uuid;
let id = Uuid::new_v4().to_string();
let new_cal = crate::model::CalendarListEntry {
name: rust_i18n::t!("new_calendar_name").to_string(),
href: format!("local://{}", id),
color: None,
supports_vjournal: Some(true),
};
app.local_cals_editing.push(new_cal.clone());
let _ = LocalCalendarRegistry::save(app.ctx.as_ref(), &app.local_cals_editing);
if !app.calendars.iter().any(|c| c.href == new_cal.href) {
app.calendars.push(new_cal.clone());
}
app.sort_calendars();
app.edit_generation = app.edit_generation.wrapping_add(1);
app.store.insert(new_cal.href.clone(), vec![]);
refresh_filtered_tasks(app);
Task::none()
}
Message::DeleteLocalCalendar(href) => {
if href == LOCAL_CALENDAR_HREF {
return Task::none();
}
if let Some(idx) = app.local_cals_editing.iter().position(|c| c.href == href) {
app.local_cals_editing.remove(idx);
let _ = LocalCalendarRegistry::save(app.ctx.as_ref(), &app.local_cals_editing);
app.calendars.retain(|c| c.href != href);
app.edit_generation = app.edit_generation.wrapping_add(1);
app.store.remove(&href);
if let Some(path) = LocalStorage::get_path_for_href(app.ctx.as_ref(), &href) {
let _ = std::fs::remove_file(path);
}
refresh_filtered_tasks(app);
}
Task::none()
}
Message::LocalCalendarNameChanged(href, name) => {
if let Some(cal) = app.local_cals_editing.iter_mut().find(|c| c.href == href) {
cal.name = name.clone();
let _ = LocalCalendarRegistry::save(app.ctx.as_ref(), &app.local_cals_editing);
if let Some(main_cal) = app.calendars.iter_mut().find(|c| c.href == href) {
main_cal.name = name;
}
}
Task::none()
}
Message::OpenColorPicker(href, current) => {
app.color_picker_active_href = Some(href);
app.temp_color = current;
Task::none()
}
Message::CancelColorPicker => {
app.color_picker_active_href = None;
Task::none()
}
Message::SubmitColorPicker(color) => {
if let Some(active_href) = &app.color_picker_active_href.clone() {
if let Some(cal) = app
.local_cals_editing
.iter_mut()
.find(|c| c.href == *active_href)
{
let r = (color.r * 255.0) as u8;
let g = (color.g * 255.0) as u8;
let b = (color.b * 255.0) as u8;
let hex = format!("#{:02X}{:02X}{:02X}", r, g, b);
cal.color = Some(hex.clone());
let _ = LocalCalendarRegistry::save(app.ctx.as_ref(), &app.local_cals_editing);
if let Some(main_cal) =
app.calendars.iter_mut().find(|c| c.href == *active_href)
{
main_cal.color = Some(hex.clone());
}
}
if let Some(cal) = app
.remote_cals_editing
.iter_mut()
.find(|c| c.href == *active_href)
{
let r = (color.r * 255.0) as u8;
let g = (color.g * 255.0) as u8;
let b = (color.b * 255.0) as u8;
let hex = format!("#{:02X}{:02X}{:02X}", r, g, b);
cal.color = Some(hex);
}
}
app.color_picker_active_href = None;
Task::none()
}
Message::AddRemoteCalendar => {
use uuid::Uuid;
let id = Uuid::new_v4().to_string();
let new_cal = crate::model::CalendarListEntry {
name: rust_i18n::t!("new_calendar_name").to_string(),
href: format!("new_remote_{}", id),
color: None,
supports_vjournal: Some(true),
};
app.remote_cals_editing.push(new_cal);
Task::none()
}
Message::RemoteCalendarNameChanged(href, name) => {
if let Some(cal) = app.remote_cals_editing.iter_mut().find(|c| c.href == href) {
cal.name = name;
}
Task::none()
}
Message::SubmitRemoteCalendar(href) => {
if let Some(cal) = app.remote_cals_editing.iter().find(|c| c.href == href)
&& let Some(client) = &app.client
{
let name = cal.name.clone();
let color = cal.color.clone();
let old_href = href.clone();
app.loading = true;
if href.starts_with("new_remote_") {
return Task::perform(
async_create_remote_calendar_wrapper(client.clone(), name, color),
move |res| {
Message::RemoteCalendarCreated(
old_href.clone(),
res.map_err(|e| e.to_string()),
)
},
);
} else {
let h = href.clone();
return Task::perform(
async_update_remote_calendar_wrapper(client.clone(), h, name, color),
move |res| {
Message::RemoteCalendarUpdated(
old_href.clone(),
res.map_err(|e| e.to_string()),
)
},
);
}
}
Task::none()
}
Message::RemoteCalendarCreated(old_href, Ok(new_href)) => {
app.loading = false;
app.error_msg = Some(rust_i18n::t!("collection_created").to_string());
if let Some(cal) = app
.remote_cals_editing
.iter_mut()
.find(|c| c.href == old_href)
{
cal.href = new_href.clone();
app.calendars.push(cal.clone()); }
app.sort_calendars();
Task::perform(async { Ok::<(), String>(()) }, |_| Message::Refresh)
}
Message::RemoteCalendarUpdated(href, Ok(_)) => {
app.loading = false;
app.error_msg = Some(rust_i18n::t!("collection_updated").to_string());
if let Some(cal) = app.remote_cals_editing.iter().find(|c| c.href == href)
&& let Some(main_cal) = app.calendars.iter_mut().find(|c| c.href == href)
{
main_cal.name = cal.name.clone();
main_cal.color = cal.color.clone();
}
Task::perform(async { Ok::<(), String>(()) }, |_| Message::Refresh)
}
Message::RemoteCalendarCreated(_, Err(e)) | Message::RemoteCalendarUpdated(_, Err(e)) => {
app.loading = false;
app.error_msg = Some(e);
Task::none()
}
Message::IcsFileLoaded(Ok((file_path, content))) => {
let vtodo_count = content.split("BEGIN:VTODO").count().saturating_sub(1);
let vjournal_count = content.split("BEGIN:VJOURNAL").count().saturating_sub(1);
let task_count = vtodo_count + vjournal_count;
app.ics_import_dialog_open = true;
app.ics_import_file_path = Some(file_path);
app.ics_import_content = Some(content);
app.ics_import_task_count = Some(task_count);
app.ics_import_selected_calendar = app
.calendars
.iter()
.filter(|c| {
!app.disabled_calendars.contains(&c.href)
&& c.href != crate::storage::LOCAL_TRASH_HREF
&& c.href != "local://recovery"
})
.map(|c| c.href.clone())
.next();
Task::none()
}
Message::IcsFileLoaded(Err(e)) => {
app.error_msg = Some(format!(
"{}",
rust_i18n::t!("import_failed_read_text", error = e)
));
Task::none()
}
Message::IcsImportDialogCalendarSelected(href) => {
app.ics_import_selected_calendar = Some(href);
Task::none()
}
Message::IcsImportDialogCancel => {
app.ics_import_dialog_open = false;
app.ics_import_file_path = None;
app.ics_import_content = None;
app.ics_import_selected_calendar = None;
app.ics_import_task_count = None;
Task::none()
}
Message::IcsImportDialogConfirm => {
if let Some(calendar_href) = &app.ics_import_selected_calendar.clone()
&& let Some(ics_content) = &app.ics_import_content.clone()
{
app.ics_import_dialog_open = false;
let file_path = app.ics_import_file_path.take();
app.ics_import_content = None;
app.ics_import_selected_calendar = None;
app.ics_import_task_count = None;
let href = calendar_href.clone();
let content = ics_content.clone();
let ctx = app.ctx.clone();
let controller = app.controller.clone();
let is_local = href.starts_with("local://");
return Task::perform(
async move {
let result: Result<usize, String> = if is_local {
LocalStorage::import_from_ics(ctx.as_ref(), &href, &content)
.map_err(|e| e.to_string())
} else {
let tasks = match LocalStorage::parse_ics(&href, &content) {
Ok(t) => t,
Err(e) => {
return Err(rust_i18n::t!(
"import_failed",
error = e.to_string()
)
.to_string());
}
};
let mut count = 0;
for task in tasks {
if controller.create_task(task).await.is_ok() {
count += 1;
}
}
if count == 0 {
return Err("Failed to import any tasks".to_string());
}
Ok(count)
};
match result {
Ok(count) => {
let file_name = file_path
.as_ref()
.and_then(|p| std::path::Path::new(p).file_name())
.and_then(|n| n.to_str())
.unwrap_or("file");
let msg = if count == 1 {
rust_i18n::t!("import_success_from_file.one", file = file_name)
.to_string()
} else {
rust_i18n::t!(
"import_success_from_file.other",
count = count,
file = file_name
)
.to_string()
};
Ok(msg)
}
Err(e) => Err(format!("{}", rust_i18n::t!("import_failed", error = e))),
}
},
Message::ImportCompleted,
);
}
Task::none()
}
_ => Task::none(),
}
}