Add visibility toggles for CalDAV calendars with event filtering

Users can now toggle visibility of CalDAV calendars using checkboxes in
the sidebar, matching the behavior of external calendars. Events from
hidden calendars are automatically filtered out of the calendar view.

Changes:
- Add is_visible field to CalendarInfo (frontend & backend)
- Add visibility checkboxes to CalDAV calendar list items
- Implement real-time event filtering based on calendar visibility
- Add CSS styling matching external calendar checkboxes
- Default new calendars to visible state

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Connor Johnstone
2025-09-03 21:37:46 -04:00
parent 189dd32f8c
commit 6a01a75cce
8 changed files with 112 additions and 36 deletions

View File

@@ -93,6 +93,7 @@ pub async fn get_user_info(
path: path.clone(),
display_name: extract_calendar_name(path),
color: generate_calendar_color(path),
is_visible: true, // Default to visible
})
.collect();

View File

@@ -56,6 +56,7 @@ pub struct CalendarInfo {
pub path: String,
pub display_name: String,
pub color: String,
pub is_visible: bool,
}
#[derive(Debug, Deserialize)]

View File

@@ -1067,6 +1067,21 @@ pub fn App() -> Html {
on_color_picker_toggle={on_color_picker_toggle}
available_colors={(*available_colors).clone()}
on_calendar_context_menu={on_calendar_context_menu}
on_calendar_visibility_toggle={Callback::from({
let user_info = user_info.clone();
move |calendar_path: String| {
let user_info = user_info.clone();
wasm_bindgen_futures::spawn_local(async move {
if let Some(mut info) = (*user_info).clone() {
// Toggle the visibility
if let Some(calendar) = info.calendars.iter_mut().find(|c| c.path == calendar_path) {
calendar.is_visible = !calendar.is_visible;
user_info.set(Some(info));
}
}
});
}
})}
current_view={(*current_view).clone()}
on_view_change={on_view_change}
current_theme={(*current_theme).clone()}

View File

@@ -108,10 +108,11 @@ pub fn Calendar(props: &CalendarProps) -> Html {
let external_events = props.external_calendar_events.clone(); // Clone before the effect
let view = props.view.clone(); // Clone before the effect
use_effect_with((*current_date, view.clone(), external_events.len()), move |(date, _view, _external_len)| {
use_effect_with((*current_date, view.clone(), external_events.len(), props.user_info.clone()), move |(date, _view, _external_len, user_info)| {
let auth_token: Option<String> = LocalStorage::get("auth_token").ok();
let date = *date; // Clone the date to avoid lifetime issues
let external_events = external_events.clone(); // Clone external events to avoid lifetime issues
let user_info = user_info.clone(); // Clone user_info to avoid lifetime issues
if let Some(token) = auth_token {
let events = events.clone();
@@ -148,8 +149,24 @@ pub fn Calendar(props: &CalendarProps) -> Html {
.await
{
Ok(vevents) => {
// Combine regular events with external calendar events
let mut all_events = vevents;
// Filter CalDAV events based on calendar visibility
let mut filtered_events = if let Some(user_info) = user_info.as_ref() {
vevents.into_iter()
.filter(|event| {
if let Some(calendar_path) = event.calendar_path.as_ref() {
// Find the calendar info for this event
user_info.calendars.iter()
.find(|cal| &cal.path == calendar_path)
.map(|cal| cal.is_visible)
.unwrap_or(true) // Default to visible if not found
} else {
true // Show events without calendar path
}
})
.collect()
} else {
vevents // Show all events if no user info
};
// Mark external events as external by adding a special category
let marked_external_events: Vec<VEvent> = external_events
@@ -161,9 +178,9 @@ pub fn Calendar(props: &CalendarProps) -> Html {
})
.collect();
all_events.extend(marked_external_events);
filtered_events.extend(marked_external_events);
let grouped_events = CalendarService::group_events_by_date(all_events);
let grouped_events = CalendarService::group_events_by_date(filtered_events);
events.set(grouped_events);
loading.set(false);
}

View File

@@ -10,6 +10,7 @@ pub struct CalendarListItemProps {
pub on_color_picker_toggle: Callback<String>, // calendar_path
pub available_colors: Vec<String>,
pub on_context_menu: Callback<(MouseEvent, String)>, // (event, calendar_path)
pub on_visibility_toggle: Callback<String>, // calendar_path
}
#[function_component(CalendarListItem)]
@@ -32,8 +33,22 @@ pub fn calendar_list_item(props: &CalendarListItemProps) -> Html {
})
};
let on_visibility_toggle = {
let cal_path = props.calendar.path.clone();
let on_visibility_toggle = props.on_visibility_toggle.clone();
Callback::from(move |_| {
on_visibility_toggle.emit(cal_path.clone());
})
};
html! {
<li key={props.calendar.path.clone()} oncontextmenu={on_context_menu}>
<div class="calendar-info">
<input
type="checkbox"
checked={props.calendar.is_visible}
onchange={on_visibility_toggle}
/>
<span class="calendar-color"
style={format!("background-color: {}", props.calendar.color)}
onclick={on_color_click}>
@@ -70,6 +85,7 @@ pub fn calendar_list_item(props: &CalendarListItemProps) -> Html {
}
</span>
<span class="calendar-name">{&props.calendar.display_name}</span>
</div>
</li>
}
}

View File

@@ -110,6 +110,7 @@ pub struct SidebarProps {
pub on_color_picker_toggle: Callback<String>,
pub available_colors: Vec<String>,
pub on_calendar_context_menu: Callback<(MouseEvent, String)>,
pub on_calendar_visibility_toggle: Callback<String>,
pub current_view: ViewMode,
pub on_view_change: Callback<ViewMode>,
pub current_theme: Theme,
@@ -221,6 +222,7 @@ pub fn sidebar(props: &SidebarProps) -> Html {
on_color_picker_toggle={props.on_color_picker_toggle.clone()}
available_colors={props.available_colors.clone()}
on_context_menu={props.on_calendar_context_menu.clone()}
on_visibility_toggle={props.on_calendar_visibility_toggle.clone()}
/>
}
}).collect::<Html>()

View File

@@ -44,6 +44,7 @@ pub struct CalendarInfo {
pub path: String,
pub display_name: String,
pub color: String,
pub is_visible: bool,
}
// CalendarEvent, EventStatus, and EventClass are now imported from shared library

View File

@@ -3822,6 +3822,29 @@ body {
flex-shrink: 0;
}
/* CalDAV Calendar Styles */
.calendar-info {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
border-radius: 8px;
transition: all 0.2s ease;
cursor: pointer;
}
.calendar-info:hover {
background: rgba(255, 255, 255, 0.1);
transform: translateX(2px);
}
.calendar-info input[type="checkbox"] {
width: 16px;
height: 16px;
accent-color: rgba(255, 255, 255, 0.8);
cursor: pointer;
}
/* Create External Calendar Button */
.create-external-calendar-button {
background: rgba(255, 255, 255, 0.15);