use crate::config::Config;
use crate::gui::message::Message;
use crate::gui::state::GuiApp;
use crate::gui::view::focusable::{clear_focus_bounds, get_all_focus_bounds, get_focus_bounds};
use crate::system::SystemEvent;
use iced::Task;
use iced::widget::operation;
use iced::widget::scrollable::RelativeOffset;
use std::time::Duration as StdDuration;
pub fn refresh_filtered_tasks(app: &mut GuiApp) {
clear_focus_bounds();
app.sort_calendars();
let config = &app.core_config;
app.session.active_calendar_href = app.active_cal_href.clone();
app.session.search_term = app.search_value.text();
let filter_res = app.session.get_filtered_view(&app.store, config);
app.tasks = filter_res.items;
app.cached_categories = filter_res.categories;
app.cached_locations = filter_res.locations;
app.cached_journal_pages = filter_res.journal_pages;
for item in &mut app.tasks {
if let crate::store::TaskListItem::Task(task) = item {
app.task_ids
.entry(task.uid.clone())
.or_insert_with(iced::widget::Id::unique);
}
}
let mut goals_progress = std::collections::HashMap::new();
for (key, goal) in &app.core_config.goals {
let prog = app.store.calculate_goal_progress(key, goal);
let history = app.store.calculate_goal_history(key, goal, 7);
goals_progress.insert(key.clone(), (prog, history));
}
app.cached_goals_progress = goals_progress;
let mut task_goals = Vec::new();
if config.show_task_goals_in_sidebar {
for (href, map) in app.store.calendars.iter() {
if app.hidden_calendars.contains(href)
|| app.disabled_calendars.contains(href)
|| href == crate::storage::LOCAL_TRASH_HREF
|| href == "local://recovery"
{
continue;
}
for t in map.values() {
if t.unmapped_properties
.iter()
.any(|p| p.key == "X-CFAIT-HISTORY-OF")
{
continue;
}
if let Some(goal) = &t.goal {
let progress = app
.store
.calculate_goal_progress(&format!("task:{}", t.uid), goal);
let history =
app.store
.calculate_goal_history(&format!("task:{}", t.uid), goal, 7);
task_goals.push((
t.uid.clone(),
t.summary.clone(),
goal.clone(),
progress,
history,
));
}
}
}
}
task_goals.sort_by(|a, b| a.1.cmp(&b.1));
app.cached_task_goals = task_goals;
if let Some(tx) = &app.alarm_tx {
let all_tasks: Vec<crate::model::Task> = app
.store
.calendars
.values()
.flat_map(|m| m.values())
.cloned()
.collect();
let _ = tx.try_send(SystemEvent::UpdateTasks(all_tasks));
}
let search_text_str = app.search_value.text();
app.search_highlight_regex = if !search_text_str.trim().is_empty() {
let terms = crate::model::matcher::extract_highlight_terms(&search_text_str);
if terms.is_empty() {
None
} else {
let pattern = format!("(?i)({})", terms.join("|"));
regex::Regex::new(&pattern).ok().map(std::rc::Rc::new)
}
} else {
None
};
}
pub fn save_config(app: &mut GuiApp) -> Config {
let mut cfg = app.core_config.clone();
cfg.url = app.ob_url.clone();
cfg.username = app.ob_user.clone();
cfg.password = app.ob_pass.clone();
cfg.tls_client_cert_path = if app.ob_tls_client_cert_path.trim().is_empty() {
None
} else {
Some(app.ob_tls_client_cert_path.trim().to_string())
};
cfg.tls_client_key_path = if app.ob_tls_client_key_path.trim().is_empty() {
None
} else {
Some(app.ob_tls_client_key_path.trim().to_string())
};
cfg.default_calendar = app.ob_default_cal.clone();
cfg.allow_insecure_certs = app.ob_insecure;
cfg.hidden_calendars = app.hidden_calendars.iter().cloned().collect();
cfg.disabled_calendars = app.disabled_calendars.iter().cloned().collect();
cfg.hide_completed = app.hide_completed;
cfg.hide_fully_completed_tags = app.hide_fully_completed_tags;
cfg.hide_aliases_in_sidebar = app.hide_aliases_in_sidebar;
cfg.show_inline_descriptions = app.show_inline_descriptions;
cfg.sort_standard_by_priority = app.sort_standard_by_priority;
cfg.paused_sort_behavior = app.paused_sort_behavior;
cfg.sort_tiebreak_recent = app.sort_tiebreak_recent;
cfg.sort_preset = app.sort_preset;
cfg.ui_scale = app.ui_scale;
cfg.show_priority_numbers = app.show_priority_numbers;
cfg.tag_aliases = app.tag_aliases.clone();
cfg.sort_cutoff_days = app.sort_cutoff_days;
cfg.theme = app.current_theme;
cfg.urgent_days_horizon = app.urgent_days;
cfg.urgent_priority_threshold = app.urgent_prio;
cfg.default_priority = app.default_priority;
cfg.start_grace_period_days = app.start_grace_period_days;
cfg.auto_reminders = app.auto_reminders;
cfg.default_reminder_time = app.default_reminder_time.clone();
cfg.snooze_short_mins = app.snooze_short_mins;
cfg.snooze_long_mins = app.snooze_long_mins;
cfg.create_events_for_tasks = app.create_events_for_tasks;
cfg.delete_events_on_completion = app.delete_events_on_completion;
cfg.strikethrough_completed = app.strikethrough_completed;
cfg.auto_refresh_interval_mins = app.auto_refresh_interval_mins;
cfg.trash_retention_days = app.trash_retention_days;
cfg.pinned_actions = app.pinned_actions.clone();
cfg.max_done_roots = app.ob_max_done_roots_input.parse().unwrap_or(20);
cfg.max_done_subtasks = app.ob_max_done_subtasks_input.parse().unwrap_or(5);
cfg.quick_filter_term = app.quick_filter_term.clone();
cfg.quick_filter_icon = app.quick_filter_icon.clone();
cfg.show_quick_filter = app.show_quick_filter;
cfg.show_calendars_tab = app.show_calendars_tab;
cfg.show_tags_tab = app.show_tags_tab;
cfg.show_locations_tab = app.show_locations_tab;
cfg.show_goals_tab = app.show_goals_tab;
cfg.sidebar_is_hidden = app.sidebar_is_hidden;
cfg.blur_when_unfocused = app.blur_when_unfocused;
cfg.sort_collections_by_size = app.sort_collections_by_size;
cfg.log_level = app.log_level;
cfg.sync_settings = app.sync_settings;
cfg.first_day_of_week = app.first_day_of_week;
cfg.expanded_tags = app.session.expanded_tags.clone();
cfg.expanded_locations = app.session.expanded_locations.clone();
let old_timestamp = cfg.settings_updated_at;
cfg.update_sync_timestamp_if_changed(&app.core_config);
let timestamp_changed = cfg.settings_updated_at != old_timestamp;
app.core_config = cfg.clone();
if timestamp_changed && let Some(tx) = &app.bg_tx {
let _ = tx.try_send(crate::gui::async_ops::WorkerCommand::SyncNow);
}
let ctx_clone = app.ctx.clone();
let cfg_clone = cfg.clone();
std::thread::spawn(move || {
let _ = cfg_clone.save_with_credentials(ctx_clone.as_ref());
});
cfg
}
pub fn apply_alias_retroactively(
app: &mut GuiApp,
alias_key: &str,
target_tags: &[String],
) -> Vec<crate::model::Task> {
let modified_tasks = app.store.apply_alias_retroactively(alias_key, target_tags);
if modified_tasks.is_empty() {
return Vec::new();
}
app.edit_generation = app.edit_generation.wrapping_add(1);
refresh_filtered_tasks(app);
modified_tasks
}
pub fn scroll_to_selected(app: &GuiApp, focus: bool) -> Task<Message> {
if let Some(uid) = &app.selected_uid {
let id_opt = app.task_ids.get(uid).cloned();
let idx_opt = app.find_task_index_by_uid(uid);
if idx_opt.is_none() {
return Task::none();
}
if let (Some(id), Some(idx)) = (id_opt.clone(), idx_opt) {
if let Some(rect) = get_focus_bounds(&id) {
let all = get_all_focus_bounds();
let mut min_y: f32 = f32::INFINITY;
let mut max_y: f32 = 0.0;
for r in all.values() {
min_y = min_y.min(r.y);
max_y = max_y.max(r.y + r.height);
}
if min_y.is_finite() && max_y > min_y {
let content_h = max_y - min_y;
let viewport_h = (app.current_window_size.height - 180.0).max(100.0);
let viewport_top = 80.0;
let viewport_bottom = app.current_window_size.height - 100.0;
let top_visible = rect.y >= viewport_top && rect.y <= viewport_bottom;
let bottom_visible = (rect.y + rect.height) >= viewport_top
&& (rect.y + rect.height) <= viewport_bottom;
let spans_viewport =
rect.y <= viewport_top && (rect.y + rect.height) >= viewport_bottom;
let is_visible = top_visible || bottom_visible || spans_viewport;
if is_visible {
if focus {
return operation::focus(id);
} else {
return Task::none();
}
}
let item_center_rel = (rect.y - min_y) + rect.height / 2.0;
let max_scroll = (content_h - viewport_h).max(0.0);
let desired_offset_px =
(item_center_rel - viewport_h / 2.0).clamp(0.0, max_scroll);
let max_scroll_px = (content_h - viewport_h).max(0.0);
let y = if max_scroll_px > 0.0 {
(desired_offset_px / max_scroll_px).clamp(0.0, 1.0)
} else {
0.0
};
let snap =
operation::snap_to(app.scrollable_id.clone(), RelativeOffset { x: 0.0, y });
if focus {
return Task::batch(vec![snap, operation::focus(id)]);
} else {
return snap;
}
}
}
let avg_item_h: f32 = 34.0;
let total_items = app.tasks.len() as f32;
let content_h = (avg_item_h * total_items).max(1.0);
let viewport_h = (app.current_window_size.height - 180.0).max(100.0);
let item_center = (idx as f32 + 0.5) * avg_item_h;
let max_scroll = (content_h - viewport_h).max(0.0);
let desired_offset_px = (item_center - viewport_h / 2.0).clamp(0.0, max_scroll);
let max_scroll_px = (content_h - viewport_h).max(0.0);
let y = if max_scroll_px > 0.0 {
(desired_offset_px / max_scroll_px).clamp(0.0, 1.0)
} else {
0.0
};
let snap = operation::snap_to(app.scrollable_id.clone(), RelativeOffset { x: 0.0, y });
if focus {
return Task::batch(vec![snap, operation::focus(id)]);
} else {
return snap;
}
}
if let Some(idx) = idx_opt {
let mut content_h = 0.0;
let mut item_center = 0.0;
for (i, item) in app.tasks.iter().enumerate() {
let mut h = 36.0;
if let crate::store::TaskListItem::Task(t) = item {
h += (t.summary.len().saturating_sub(60) as f32 / 60.0).floor() * 20.0;
if app.expanded_tasks.contains(&t.uid) {
h += 15.0; if !t.description.is_empty() {
h += t.description.lines().count() as f32 * 18.0;
}
h += t.dependencies.len() as f32 * 24.0;
h += t.related_to.len() as f32 * 24.0;
h += app.store.get_tasks_blocking(&t.uid).len() as f32 * 24.0;
h += app.store.get_tasks_related_to(&t.uid).len() as f32 * 24.0;
if !t.sessions.is_empty() || app.adding_session_uid.as_ref() == Some(&t.uid)
{
h += 30.0 + (t.sessions.len().min(3) as f32 * 20.0);
}
}
} else {
h = 28.0; }
if i < idx {
item_center += h;
} else if i == idx {
item_center += h / 2.0;
}
content_h += h;
}
content_h = content_h.max(1.0);
let viewport_h = (app.current_window_size.height - 180.0).max(100.0);
let max_scroll = (content_h - viewport_h).max(0.0);
let desired_offset_px = (item_center - viewport_h / 2.0).clamp(0.0, max_scroll);
let max_scroll_px = (content_h - viewport_h).max(0.0);
let y = if max_scroll_px > 0.0 {
(desired_offset_px / max_scroll_px).clamp(0.0, 1.0)
} else {
0.0
};
return operation::snap_to(app.scrollable_id.clone(), RelativeOffset { x: 0.0, y });
}
if let Some(id) = id_opt
&& focus
{
return operation::focus(id);
}
}
Task::none()
}
pub fn scroll_to_selected_delayed(_app: &GuiApp, focus: bool) -> Task<Message> {
if let Some(uid) = &_app.selected_uid
&& let Some(id) = _app.task_ids.get(uid).cloned()
{
return Task::perform(
async move {
let mut attempts = 0u8;
loop {
if crate::gui::view::focusable::get_focus_bounds(&id).is_some() {
break;
}
attempts = attempts.saturating_add(1);
if attempts >= 20 {
break;
}
tokio::time::sleep(StdDuration::from_millis(50)).await;
}
},
move |_| Message::SnapToSelected { focus },
);
}
Task::batch(vec![
Task::perform(
async {
tokio::time::sleep(StdDuration::from_millis(120)).await;
},
move |_| Message::SnapToSelected { focus },
),
Task::perform(
async {
tokio::time::sleep(StdDuration::from_millis(360)).await;
},
move |_| Message::SnapToSelected { focus },
),
Task::perform(
async {
tokio::time::sleep(StdDuration::from_millis(720)).await;
},
move |_| Message::SnapToSelected { focus },
),
])
}
use crate::model::AppIntent;
pub fn dispatch_intent(app: &mut GuiApp, intent: AppIntent) {
let config = &app.core_config;
app.session.apply_session_intent(&intent);
let (actions, reverse, desc, primary_uid) = app.store.apply_task_intent(&intent, config);
if !actions.is_empty() {
app.edit_generation = app.edit_generation.wrapping_add(1);
app.undo_history.push(crate::journal::UndoRecord {
description: desc,
primary_uid,
forward: actions.clone(),
reverse,
});
app.info_msg = None; }
refresh_filtered_tasks(app);
if !actions.is_empty()
&& let Some(tx) = &app.bg_tx
{
let _ = tx.try_send(crate::gui::async_ops::WorkerCommand::Batch(actions));
}
}
pub fn update_journal_state(app: &mut GuiApp) {
let journal = crate::journal::Journal::load(app.ctx.as_ref());
app.unsynced_changes = !journal.is_empty();
if app.unsynced_changes {
let mut lines = vec![rust_i18n::t!("unsynced").to_string()];
for (i, action) in journal.queue.iter().enumerate() {
if i >= 10 {
lines.push(
rust_i18n::t!("unsynced_and_more", count = journal.queue.len() - 10)
.to_string(),
);
break;
}
let (verb, summary) = match action {
crate::journal::Action::Create(t) => (
rust_i18n::t!("unsynced_action_create").to_string(),
&t.summary,
),
crate::journal::Action::Update(t) => (
rust_i18n::t!("unsynced_action_update").to_string(),
&t.summary,
),
crate::journal::Action::Delete(t) => (
rust_i18n::t!("calendar_action_deleted").to_string(),
&t.summary,
),
crate::journal::Action::Move(t, _) => (
rust_i18n::t!("unsynced_action_move").to_string(),
&t.summary,
),
};
let trunc_summary = if summary.chars().count() > 40 {
format!("{}...", summary.chars().take(37).collect::<String>())
} else {
summary.clone()
};
lines.push(format!("• {}: {}", verb, trunc_summary));
}
app.unsynced_tooltip = lines.join("\n");
} else {
app.unsynced_tooltip = rust_i18n::t!("force_sync").to_string();
}
}