mirror of
https://github.com/Misterio77/Foundry.git
synced 2026-08-24 10:04:09 -05:00
feat(projects/khora): show event details
Open timed, all-day, and agenda events in a read-only details dialog with their time, calendar, location, description, URL, organizer, and attendees. Assisted-by: pi (gpt-5.6-sol)
This commit is contained in:
@@ -3,8 +3,8 @@
|
||||
Planned improvements, ordered roughly from least to most implementation complexity.
|
||||
The order reflects engineering dependencies rather than product priority.
|
||||
|
||||
1. **Event details** — open an event from any view and show its complete time,
|
||||
calendar, location, description, URL, organizer, and attendees.
|
||||
1. **Event details — done.** Open an event from any view and show its complete
|
||||
time, calendar, location, description, URL, organizer, and attendees.
|
||||
2. **Keyboard navigation** — previous/next period, Today, view switching, refresh,
|
||||
and zoom shortcuts.
|
||||
3. **Session persistence** — remember the selected view, zoom, sidebar width,
|
||||
|
||||
@@ -62,6 +62,11 @@ class KhalRepository:
|
||||
all_day=event.allday,
|
||||
location=event.location or "",
|
||||
color=event.color,
|
||||
uid=str(event.uid or ""),
|
||||
description=event.description or "",
|
||||
url=event.url or "",
|
||||
organizer=event.organizer or "",
|
||||
attendees=event.attendees or "",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -71,6 +71,11 @@ class Event:
|
||||
all_day: bool = False
|
||||
location: str = ""
|
||||
color: str | None = None
|
||||
uid: str = ""
|
||||
description: str = ""
|
||||
url: str = ""
|
||||
organizer: str = ""
|
||||
attendees: str = ""
|
||||
|
||||
@property
|
||||
def time_label(self) -> str:
|
||||
@@ -80,3 +85,28 @@ class Event:
|
||||
assert isinstance(self.start, dt.datetime)
|
||||
assert isinstance(self.end, dt.datetime)
|
||||
return f"{self.start:%H:%M}–{self.end:%H:%M}"
|
||||
|
||||
@property
|
||||
def when_label(self) -> str:
|
||||
if self.all_day:
|
||||
assert isinstance(self.start, dt.date)
|
||||
assert isinstance(self.end, dt.date)
|
||||
if self.end <= self.start + dt.timedelta(days=1):
|
||||
return f"{self.start:%A, %B} {self.start.day}, {self.start.year} · All day"
|
||||
last_day = self.end - dt.timedelta(days=1)
|
||||
return (
|
||||
f"{self.start:%B} {self.start.day} – "
|
||||
f"{last_day:%B} {last_day.day}, {last_day.year} · All day"
|
||||
)
|
||||
|
||||
assert isinstance(self.start, dt.datetime)
|
||||
assert isinstance(self.end, dt.datetime)
|
||||
if self.start.date() == self.end.date():
|
||||
return (
|
||||
f"{self.start:%A, %B} {self.start.day}, {self.start.year} · "
|
||||
f"{self.time_label}"
|
||||
)
|
||||
return (
|
||||
f"{self.start:%B} {self.start.day}, {self.start:%H:%M} – "
|
||||
f"{self.end:%B} {self.end.day}, {self.end:%H:%M}, {self.end.year}"
|
||||
)
|
||||
|
||||
@@ -307,10 +307,18 @@ class KhoraWindow(Adw.ApplicationWindow):
|
||||
label=event.summary,
|
||||
xalign=0,
|
||||
ellipsize=Pango.EllipsizeMode.END,
|
||||
tooltip_text=event.summary,
|
||||
css_classes=["all-day-event", self._event_color_class(event.color)],
|
||||
)
|
||||
box.append(event_label)
|
||||
event_button = Gtk.Button(
|
||||
child=event_label,
|
||||
tooltip_text=event.summary,
|
||||
css_classes=[
|
||||
"flat",
|
||||
"all-day-event",
|
||||
self._event_color_class(event.color),
|
||||
],
|
||||
)
|
||||
event_button.connect("clicked", lambda _button, item=event: self._show_event(item))
|
||||
box.append(event_button)
|
||||
headers.append(box)
|
||||
row.append(headers)
|
||||
return row
|
||||
@@ -343,24 +351,32 @@ class KhoraWindow(Adw.ApplicationWindow):
|
||||
label = Gtk.Label(
|
||||
xalign=0,
|
||||
yalign=0,
|
||||
valign=Gtk.Align.START,
|
||||
halign=Gtk.Align.FILL,
|
||||
wrap=event_height >= 40,
|
||||
lines=2 if event_height >= 40 else 1,
|
||||
ellipsize=Pango.EllipsizeMode.END,
|
||||
margin_top=start * self._slot_height,
|
||||
height_request=event_height,
|
||||
tooltip_text=f"{event.time_label} · {event.summary}",
|
||||
css_classes=["timed-event", self._event_color_class(event.color)],
|
||||
)
|
||||
summary = GLib.markup_escape_text(event.summary)
|
||||
if event_height >= 40:
|
||||
label.set_markup(f"<b>{summary}</b>\n<small>{event.time_label}</small>")
|
||||
elif event_height >= 22:
|
||||
label.set_markup(f"<b>{summary}</b>")
|
||||
overlay.add_overlay(label)
|
||||
overlay.set_measure_overlay(label, False)
|
||||
overlay.set_clip_overlay(label, True)
|
||||
event_button = Gtk.Button(
|
||||
child=label,
|
||||
valign=Gtk.Align.START,
|
||||
halign=Gtk.Align.FILL,
|
||||
margin_top=start * self._slot_height,
|
||||
height_request=event_height,
|
||||
tooltip_text=f"{event.time_label} · {event.summary}",
|
||||
css_classes=[
|
||||
"flat",
|
||||
"timed-event",
|
||||
self._event_color_class(event.color),
|
||||
],
|
||||
)
|
||||
event_button.connect("clicked", lambda _button, item=event: self._show_event(item))
|
||||
overlay.add_overlay(event_button)
|
||||
overlay.set_measure_overlay(event_button, False)
|
||||
overlay.set_clip_overlay(event_button, True)
|
||||
|
||||
if day == dt.date.today():
|
||||
indicator = self._current_time_indicator()
|
||||
@@ -426,15 +442,51 @@ class KhoraWindow(Adw.ApplicationWindow):
|
||||
self._event_color_providers[class_name] = provider
|
||||
return class_name
|
||||
|
||||
@staticmethod
|
||||
def _event_row(event: Event) -> Adw.ActionRow:
|
||||
def _event_row(self, event: Event) -> Adw.ActionRow:
|
||||
details = f"{event.time_label} · {event.calendar}"
|
||||
if event.location:
|
||||
details += f" · {event.location}"
|
||||
row = Adw.ActionRow(title=event.summary, subtitle=details)
|
||||
row.add_prefix(KhoraWindow._color_dot(event.color))
|
||||
row = Adw.ActionRow(title=event.summary, subtitle=details, activatable=True)
|
||||
row.add_prefix(self._color_dot(event.color))
|
||||
row.connect("activated", lambda _row: self._show_event(event))
|
||||
return row
|
||||
|
||||
def _show_event(self, event: Event) -> None:
|
||||
details = Adw.PreferencesGroup()
|
||||
calendar = Adw.ActionRow(title="Calendar", subtitle=event.calendar)
|
||||
calendar.add_prefix(self._color_dot(event.color))
|
||||
details.add(calendar)
|
||||
|
||||
for title, value in (
|
||||
("Location", event.location),
|
||||
("Organizer", event.organizer),
|
||||
("Attendees", event.attendees),
|
||||
("Description", event.description),
|
||||
):
|
||||
if value:
|
||||
details.add(
|
||||
Adw.ActionRow(
|
||||
title=title,
|
||||
subtitle=value,
|
||||
subtitle_lines=0,
|
||||
subtitle_selectable=True,
|
||||
)
|
||||
)
|
||||
|
||||
if event.url:
|
||||
website = Adw.ActionRow(title="Website")
|
||||
website.add_suffix(Gtk.LinkButton(uri=event.url, label="Open link"))
|
||||
details.add(website)
|
||||
|
||||
dialog = Adw.AlertDialog(
|
||||
heading=event.summary,
|
||||
body=event.when_label,
|
||||
extra_child=details,
|
||||
content_width=520,
|
||||
)
|
||||
dialog.add_response("close", "Close")
|
||||
dialog.present(self)
|
||||
|
||||
@staticmethod
|
||||
def _color_dot(color: str | None) -> Gtk.Label:
|
||||
dot = Gtk.Label(valign=Gtk.Align.CENTER)
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import datetime as dt
|
||||
from types import SimpleNamespace
|
||||
|
||||
from khora.khal_adapter import KhalRepository
|
||||
|
||||
|
||||
@@ -22,3 +25,28 @@ def test_calendar_accounts_come_from_their_parent_directory() -> None:
|
||||
calendars = repository.calendars
|
||||
|
||||
assert tuple(calendar.account for calendar in calendars) == ("personal", "university")
|
||||
|
||||
|
||||
def test_event_details_cross_the_khal_boundary() -> None:
|
||||
event = KhalRepository._to_event(
|
||||
SimpleNamespace(
|
||||
summary="Standup",
|
||||
calendar="Work",
|
||||
start_local=dt.datetime(2026, 8, 15, 9, 0),
|
||||
end_local=dt.datetime(2026, 8, 15, 9, 30),
|
||||
allday=False,
|
||||
location="Meeting room",
|
||||
color="#3584e4",
|
||||
uid="event-id",
|
||||
description="Discuss the calendar mines",
|
||||
url="https://example.com/meeting",
|
||||
organizer="organizer@example.com",
|
||||
attendees="one@example.com,two@example.com",
|
||||
)
|
||||
)
|
||||
|
||||
assert event.uid == "event-id"
|
||||
assert event.description == "Discuss the calendar mines"
|
||||
assert event.url == "https://example.com/meeting"
|
||||
assert event.organizer == "organizer@example.com"
|
||||
assert event.attendees == "one@example.com,two@example.com"
|
||||
|
||||
@@ -69,6 +69,29 @@ def test_event_slot_range_clamps_events_to_the_day() -> None:
|
||||
assert event_slot_range(event, dt.date(2026, 8, 15)) == (0, 48)
|
||||
|
||||
|
||||
def test_event_when_label_includes_its_date() -> None:
|
||||
event = Event(
|
||||
summary="Write a calendar",
|
||||
calendar="Personal",
|
||||
start=dt.datetime(2026, 8, 15, 13, 30),
|
||||
end=dt.datetime(2026, 8, 15, 15, 0),
|
||||
)
|
||||
|
||||
assert event.when_label == "Saturday, August 15, 2026 · 13:30–15:00"
|
||||
|
||||
|
||||
def test_multiday_all_day_event_when_label_uses_inclusive_last_day() -> None:
|
||||
event = Event(
|
||||
summary="Conference",
|
||||
calendar="Work",
|
||||
start=dt.date(2026, 8, 15),
|
||||
end=dt.date(2026, 8, 18),
|
||||
all_day=True,
|
||||
)
|
||||
|
||||
assert event.when_label == "August 15 – August 17, 2026 · All day"
|
||||
|
||||
|
||||
def test_all_day_event_label() -> None:
|
||||
event = Event(
|
||||
summary="Escape the calendar mines",
|
||||
|
||||
Reference in New Issue
Block a user