feat: add recurrence and time zone support
This commit is contained in:
@@ -46,20 +46,12 @@ std::size_t complete_sequence_length(const std::string& input) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::string event_identity(const std::span<const Event> events, const std::size_t index) {
|
||||
const auto& uid = events[index].uid;
|
||||
const bool unique = !uid.empty() &&
|
||||
std::count_if(events.begin(), events.end(), [&uid](const Event& event) {
|
||||
return event.uid == uid;
|
||||
}) == 1;
|
||||
return unique ? uid : "@nocal:" + std::to_string(index);
|
||||
}
|
||||
|
||||
std::size_t source_index(const std::span<const Event> events, const Event& event) {
|
||||
const auto found = std::find_if(events.begin(), events.end(), [&event](const Event& candidate) {
|
||||
return &candidate == &event;
|
||||
});
|
||||
return static_cast<std::size_t>(std::distance(events.begin(), found));
|
||||
std::optional<std::size_t> source_index(const std::span<const Event> events,
|
||||
const Event* source) {
|
||||
for (std::size_t index = 0; index < events.size(); ++index) {
|
||||
if (&events[index] == source) return index;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -77,15 +69,23 @@ TuiApp::TuiApp(std::vector<Event>& events, SaveCallback saver, const int input_f
|
||||
state_.visible_month = state_.today.year() / state_.today.month();
|
||||
}
|
||||
|
||||
std::optional<std::size_t> TuiApp::focused_event_index() const {
|
||||
std::optional<EventOccurrence> TuiApp::focused_occurrence() const {
|
||||
if (!state_.focused_event_id) return std::nullopt;
|
||||
const auto event_span = std::span<const Event>{events_};
|
||||
for (std::size_t index = 0; index < events_.size(); ++index) {
|
||||
if (event_identity(event_span, index) == *state_.focused_event_id) return index;
|
||||
for (const auto& occurrence : occurrences_on_day(event_span, state_.selected_day)) {
|
||||
if (occurrence_focus_id(event_span, occurrence) == *state_.focused_event_id) {
|
||||
return occurrence;
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<std::size_t> TuiApp::focused_event_index() const {
|
||||
const auto occurrence = focused_occurrence();
|
||||
return occurrence ? source_index(std::span<const Event>{events_}, occurrence->source)
|
||||
: std::nullopt;
|
||||
}
|
||||
|
||||
void TuiApp::set_notification(std::string message, const bool error) {
|
||||
state_.notification = std::move(message);
|
||||
state_.notification_is_error = error;
|
||||
@@ -190,8 +190,13 @@ void TuiApp::begin_edit() {
|
||||
set_notification("Focus an appointment before editing.", true);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
editor_.emplace(events_[*index]);
|
||||
} catch (const std::exception& error) {
|
||||
set_notification("Cannot edit this appointment: " + std::string{error.what()}, true);
|
||||
return;
|
||||
}
|
||||
pending_mutation_before_ = capture_history_snapshot();
|
||||
editor_.emplace(events_[*index]);
|
||||
editing_index_ = index;
|
||||
state_.show_event_details = false;
|
||||
state_.show_help = false;
|
||||
@@ -240,11 +245,33 @@ void TuiApp::submit_editor() {
|
||||
|
||||
editor_.reset();
|
||||
editing_index_.reset();
|
||||
state_.selected_day = local_date(events_[changed_index].start);
|
||||
state_.visible_month = state_.selected_day.year() / state_.selected_day.month();
|
||||
state_.focused_event_id = event_identity(std::span<const Event>{events_}, changed_index);
|
||||
const auto event_span = std::span<const Event>{events_};
|
||||
auto preferred = occurrences_on_day(event_span, state_.selected_day);
|
||||
const auto matching = std::find_if(preferred.begin(), preferred.end(), [&](const auto& value) {
|
||||
const auto index = source_index(event_span, value.source);
|
||||
return index && *index == changed_index;
|
||||
});
|
||||
if (matching != preferred.end()) {
|
||||
state_.focused_event_id = occurrence_focus_id(event_span, *matching);
|
||||
} else {
|
||||
state_.selected_day = local_date(events_[changed_index].start);
|
||||
state_.visible_month = state_.selected_day.year() / state_.selected_day.month();
|
||||
preferred = occurrences_on_day(event_span, state_.selected_day);
|
||||
const auto first = std::find_if(preferred.begin(), preferred.end(), [&](const auto& value) {
|
||||
const auto index = source_index(event_span, value.source);
|
||||
return index && *index == changed_index;
|
||||
});
|
||||
state_.focused_event_id = first == preferred.end()
|
||||
? std::nullopt
|
||||
: std::optional{occurrence_focus_id(event_span, *first)};
|
||||
}
|
||||
state_.show_event_details = false;
|
||||
record_mutation(editing ? "edit appointment" : "add appointment");
|
||||
record_mutation(editing && events_[changed_index].recurrence
|
||||
? "edit recurring series"
|
||||
: editing ? "edit appointment" : "add appointment");
|
||||
if (editing && events_[changed_index].recurrence) {
|
||||
success = "Recurring series updated.";
|
||||
}
|
||||
set_notification(std::move(success));
|
||||
}
|
||||
|
||||
@@ -259,6 +286,7 @@ void TuiApp::confirm_delete() {
|
||||
|
||||
auto original = events_;
|
||||
const auto original_focus = state_.focused_event_id;
|
||||
const bool recurring = events_[*index].recurrence.has_value();
|
||||
events_.erase(events_.begin() + static_cast<std::ptrdiff_t>(*index));
|
||||
if (!persist(std::span<const Event>{events_})) {
|
||||
events_.swap(original);
|
||||
@@ -271,8 +299,8 @@ void TuiApp::confirm_delete() {
|
||||
state_.focused_event_id.reset();
|
||||
state_.show_event_details = false;
|
||||
state_.confirm_delete = false;
|
||||
record_mutation("delete appointment");
|
||||
set_notification("Appointment deleted.");
|
||||
record_mutation(recurring ? "delete recurring series" : "delete appointment");
|
||||
set_notification(recurring ? "Recurring series deleted." : "Appointment deleted.");
|
||||
}
|
||||
|
||||
void TuiApp::cancel_delete() {
|
||||
@@ -347,7 +375,7 @@ void TuiApp::dispatch(const Action action) {
|
||||
};
|
||||
const auto cycle_event = [this](const int direction) {
|
||||
const auto event_span = std::span<const Event>{events_};
|
||||
const auto day_events = events_on_day(event_span, state_.selected_day);
|
||||
const auto day_events = occurrences_on_day(event_span, state_.selected_day);
|
||||
if (day_events.empty()) {
|
||||
state_.focused_event_id.reset();
|
||||
return;
|
||||
@@ -356,8 +384,8 @@ void TuiApp::dispatch(const Action action) {
|
||||
std::size_t current = day_events.size();
|
||||
if (state_.focused_event_id) {
|
||||
for (std::size_t index = 0; index < day_events.size(); ++index) {
|
||||
const auto event_index = source_index(event_span, day_events[index].get());
|
||||
if (event_identity(event_span, event_index) == *state_.focused_event_id) {
|
||||
if (occurrence_focus_id(event_span, day_events[index]) ==
|
||||
*state_.focused_event_id) {
|
||||
current = index;
|
||||
break;
|
||||
}
|
||||
@@ -372,8 +400,7 @@ void TuiApp::dispatch(const Action action) {
|
||||
} else {
|
||||
target = (current + 1) % day_events.size();
|
||||
}
|
||||
const auto event_index = source_index(event_span, day_events[target].get());
|
||||
state_.focused_event_id = event_identity(event_span, event_index);
|
||||
state_.focused_event_id = occurrence_focus_id(event_span, day_events[target]);
|
||||
};
|
||||
|
||||
if (state_.show_event_details) {
|
||||
|
||||
Reference in New Issue
Block a user