mirror of
https://github.com/Misterio77/Foundry.git
synced 2026-08-24 02:14:13 -05:00
fix(projects/khora): adopt calendar-style header navigation
Arrange branding, today and date navigation around a dynamic period title, with refresh and view controls on the right. Assisted-by: pi (gpt-5.6-sol)
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import calendar
|
||||
import datetime as dt
|
||||
from dataclasses import dataclass
|
||||
|
||||
@@ -9,6 +10,31 @@ def week_dates(day: dt.date) -> tuple[dt.date, ...]:
|
||||
return tuple(monday + dt.timedelta(days=offset) for offset in range(7))
|
||||
|
||||
|
||||
def period_label(day: dt.date, mode: str) -> str:
|
||||
if mode == "month":
|
||||
return f"{day:%B} {day.year}"
|
||||
if mode != "week":
|
||||
return f"{day:%A, %B} {day.day}, {day.year}"
|
||||
|
||||
start, *_, end = week_dates(day)
|
||||
if start.year != end.year:
|
||||
return f"{start:%B} {start.day}, {start.year} – {end:%B} {end.day}, {end.year}"
|
||||
if start.month != end.month:
|
||||
return f"{start:%B} {start.day} – {end:%B} {end.day}, {start.year}"
|
||||
return f"{start:%B} {start.day}–{end.day}, {start.year}"
|
||||
|
||||
|
||||
def shifted_date(day: dt.date, mode: str, direction: int) -> dt.date:
|
||||
if mode != "month":
|
||||
step = 7 if mode == "week" else 1
|
||||
return day + dt.timedelta(days=step * direction)
|
||||
|
||||
month_index = day.year * 12 + day.month - 1 + direction
|
||||
year, zero_based_month = divmod(month_index, 12)
|
||||
month = zero_based_month + 1
|
||||
return dt.date(year, month, min(day.day, calendar.monthrange(year, month)[1]))
|
||||
|
||||
|
||||
def event_slot_range(event: Event, day: dt.date) -> tuple[int, int]:
|
||||
"""Return the half-hour rows occupied by a timed event on one day."""
|
||||
assert isinstance(event.start, dt.datetime)
|
||||
|
||||
@@ -10,7 +10,7 @@ from gi.repository import Adw, Gio, GLib, Gtk, Pango
|
||||
|
||||
from .colors import display_color
|
||||
from .khal_adapter import KhalRepository
|
||||
from .model import Event, event_slot_range, week_dates
|
||||
from .model import Event, event_slot_range, period_label, shifted_date, week_dates
|
||||
|
||||
|
||||
class KhoraWindow(Adw.ApplicationWindow):
|
||||
@@ -53,16 +53,53 @@ class KhoraWindow(Adw.ApplicationWindow):
|
||||
|
||||
def _build_header(self) -> Adw.HeaderBar:
|
||||
header = Adw.HeaderBar()
|
||||
header.set_title_widget(Gtk.Box())
|
||||
|
||||
brand = Gtk.Box(spacing=8, margin_end=12)
|
||||
brand.append(Gtk.Image(icon_name="x-office-calendar-symbolic", pixel_size=24))
|
||||
brand.append(Gtk.Label(label="Khora", css_classes=["title"]))
|
||||
header.pack_start(brand)
|
||||
header.pack_start(Gtk.Button(label="Today", action_name="win.today"))
|
||||
|
||||
navigation = Gtk.Box()
|
||||
previous = Gtk.Button(
|
||||
icon_name="go-previous-symbolic",
|
||||
action_name="win.previous",
|
||||
css_classes=["flat"],
|
||||
tooltip_text="Previous period",
|
||||
)
|
||||
following = Gtk.Button(
|
||||
icon_name="go-next-symbolic",
|
||||
action_name="win.next",
|
||||
css_classes=["flat"],
|
||||
tooltip_text="Next period",
|
||||
)
|
||||
navigation.append(previous)
|
||||
navigation.append(following)
|
||||
header.pack_start(navigation)
|
||||
|
||||
self._period_label = Gtk.Label(css_classes=["title"], margin_start=6)
|
||||
header.pack_start(self._period_label)
|
||||
|
||||
menu = Gio.Menu()
|
||||
for mode in ("day", "week", "month", "agenda"):
|
||||
menu.append(mode.title(), f"win.view::{mode}")
|
||||
self._view_button = Gtk.MenuButton(label="Week", menu_model=menu)
|
||||
header.set_title_widget(self._view_button)
|
||||
header.pack_end(Gtk.Button(icon_name="view-refresh-symbolic", action_name="win.refresh"))
|
||||
|
||||
controls = Gtk.Box(spacing=6)
|
||||
controls.append(
|
||||
Gtk.Button(
|
||||
icon_name="view-refresh-symbolic",
|
||||
action_name="win.refresh",
|
||||
tooltip_text="Refresh calendars",
|
||||
)
|
||||
)
|
||||
controls.append(self._view_button)
|
||||
header.pack_end(controls)
|
||||
|
||||
self._install_action("today", self._on_today)
|
||||
self._install_action("previous", lambda *_: self._navigate(-1))
|
||||
self._install_action("next", lambda *_: self._navigate(1))
|
||||
self._install_action("refresh", lambda *_: self._refresh())
|
||||
view_action = Gio.SimpleAction.new("view", GLib.VariantType.new("s"))
|
||||
view_action.connect("activate", self._on_view_selected)
|
||||
@@ -122,6 +159,7 @@ class KhoraWindow(Adw.ApplicationWindow):
|
||||
if self._repository is None or not hasattr(self, "_view_content"):
|
||||
return
|
||||
|
||||
self._period_label.set_label(period_label(self._selected_day(), self._view_mode))
|
||||
self._clear_view()
|
||||
self._empty.set_visible(False)
|
||||
if self._view_mode == "month":
|
||||
@@ -292,6 +330,12 @@ class KhoraWindow(Adw.ApplicationWindow):
|
||||
self._calendar.select_day(GLib.DateTime.new_now_local())
|
||||
self._refresh()
|
||||
|
||||
def _navigate(self, direction: int) -> None:
|
||||
target = shifted_date(self._selected_day(), self._view_mode, direction)
|
||||
selected = GLib.DateTime.new_local(target.year, target.month, target.day, 0, 0, 0)
|
||||
self._calendar.select_day(selected)
|
||||
self._refresh()
|
||||
|
||||
def _on_view_selected(self, _action: Gio.SimpleAction, parameter: GLib.Variant) -> None:
|
||||
self._view_mode = parameter.get_string()
|
||||
self._view_button.set_label(self._view_mode.title())
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import datetime as dt
|
||||
|
||||
from khora.model import Event, event_slot_range, week_dates
|
||||
from khora.model import Event, event_slot_range, period_label, shifted_date, week_dates
|
||||
|
||||
|
||||
def test_timed_event_label_uses_24_hour_time() -> None:
|
||||
@@ -26,6 +26,27 @@ def test_week_dates_runs_from_monday_through_sunday() -> None:
|
||||
)
|
||||
|
||||
|
||||
def test_period_labels_follow_the_active_view() -> None:
|
||||
day = dt.date(2026, 8, 15)
|
||||
|
||||
assert period_label(day, "day") == "Saturday, August 15, 2026"
|
||||
assert period_label(day, "week") == "August 10–16, 2026"
|
||||
assert period_label(day, "month") == "August 2026"
|
||||
assert period_label(day, "agenda") == "Saturday, August 15, 2026"
|
||||
|
||||
|
||||
def test_week_label_handles_month_boundaries() -> None:
|
||||
assert period_label(dt.date(2026, 9, 1), "week") == "August 31 – September 6, 2026"
|
||||
|
||||
|
||||
def test_shifted_date_uses_the_active_view_interval() -> None:
|
||||
day = dt.date(2026, 8, 15)
|
||||
|
||||
assert shifted_date(day, "day", 1) == dt.date(2026, 8, 16)
|
||||
assert shifted_date(day, "week", -1) == dt.date(2026, 8, 8)
|
||||
assert shifted_date(dt.date(2026, 1, 31), "month", 1) == dt.date(2026, 2, 28)
|
||||
|
||||
|
||||
def test_event_slot_range_rounds_to_half_hours() -> None:
|
||||
event = Event(
|
||||
summary="Oddly timed meeting",
|
||||
|
||||
Reference in New Issue
Block a user