Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions assets/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -892,7 +892,8 @@
"provider": {
"custom": "CalDAV",
"google": "Google",
"icloud": "iCloud"
"icloud": "iCloud",
"ics": "ICS URL"
},
"provider-label": "Provider",
"save": "Save",
Expand All @@ -904,6 +905,7 @@
"secret-service-locked": "Calendar credentials are locked or access was denied. Unlock Secret Service and retry.",
"secret-service-unavailable": "No Secret Service provider is available. Start a compatible provider and retry.",
"server-url-label": "Server URL",
"ics-url-label": "ICS URL",
"username-label": "Username",
"username-placeholder": "name@example.com"
},
Expand Down Expand Up @@ -2452,7 +2454,7 @@
},
"calendar-add": {
"button": "Add Account",
"description": "Add iCloud, CalDAV, or Google accounts",
"description": "Add iCloud, CalDAV, Google accounts, or an ICS URL",
"label": "Calendar Accounts"
},
"calendar-credentials": {
Expand Down
39 changes: 39 additions & 0 deletions src/calendar/calendar_service.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
#include "calendar/caldav_discovery.h"
#include "calendar/calendar_cache.h"
#include "calendar/calendar_discovery_state.h"
#include "calendar/ical_parser.h"
#include "config/config_service.h"
#include "core/log.h"
#include "i18n/i18n.h"
#include "net/http_client.h"
#include "net/url_open.h"
#include "notification/notification_manager.h"
#include "security/secret_store.h"
Expand Down Expand Up @@ -351,6 +353,8 @@ void CalendarService::startRefresh() {
fetchCalDav(account);
} else if (account.type == "google") {
fetchGoogle(account);
} else if (account.type == "ics") {
fetchIcs(account);
} else {
kLog.warn("unknown calendar account type '{}' for id {}", account.type, account.id);
accountDone(account.id, false, {});
Expand Down Expand Up @@ -527,6 +531,41 @@ void CalendarService::lookupCalDavPassword(
callback(security::SecretStoreStatus::BackendError, {});
}

void CalendarService::fetchIcs(const CalendarConfig::Account& account) {
const std::string url = account.serverUrl;
if (url.empty()) {
kLog.warn("ics account {} is missing server_url", account.id);
accountDone(account.id, false, {});
return;
}

const std::string accountId = account.id;
const std::string displayName = account.displayName;
const std::string colorHex = account.color;

HttpRequest req;
req.url = url;
req.followRedirects = true;

m_httpClient.request(req, [this, accountId, displayName, colorHex](HttpResponse resp) {
if (!resp.transportOk || resp.status != 200) {
kLog.warn("failed to fetch ics for account {}: http status {}", accountId, resp.status);
accountDone(accountId, false, {});
return;
}

const auto now = std::chrono::system_clock::now();
auto events = calendar::parseICalEvents(resp.body, now - kWindowBefore, now + kWindowAfter);
for (auto& event : events) {
event.calendarName = displayName;
if (!colorHex.empty()) {
event.colorHex = colorHex;
}
}
accountDone(accountId, true, std::move(events));
});
}

void CalendarService::refreshGoogleToken(const std::string& accountId, std::function<void(bool, std::string)> cb) {
m_credentials.lookupRefreshToken(
accountId,
Expand Down
1 change: 1 addition & 0 deletions src/calendar/calendar_service.h
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ class CalendarService {
void lookupCalDavPassword(
const CalendarConfig::Account& account, calendar::CalendarCredentialStore::LookupCallback callback
);
void fetchIcs(const CalendarConfig::Account& account);
void fetchGoogle(const CalendarConfig::Account& account);
void refreshGoogleToken(const std::string& accountId, std::function<void(bool ok, std::string accessToken)> cb);
void googleFetchWithToken(const std::string& accountId, const std::string& accessToken, bool allowRefreshRetry);
Expand Down
2 changes: 1 addition & 1 deletion src/config/config_types.h
Original file line number Diff line number Diff line change
Expand Up @@ -1078,7 +1078,7 @@ struct CalendarConfig {
// are not stored here. id must be [a-z0-9_] because it identifies durable credential records.
struct Account {
std::string id;
std::string type; // "google" | "caldav"
std::string type; // "google" | "caldav" | "ics"
std::string displayName;
std::string color; // optional "#rrggbb" override
std::string provider; // "icloud" | "custom" (caldav only)
Expand Down
7 changes: 7 additions & 0 deletions src/config/schema/config_schema.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include "config/config_types.h"
#include "config/schema/config_sections.h"
#include "config/schema/diagnostics.h"
#include "config/schema/engine.h"
#include "config/schema/ranges.h"
#include "core/input/key_chord.h"
Expand Down Expand Up @@ -585,6 +586,12 @@ namespace noctalia::config::schema {
pathStringField(&CalendarConfig::Account::passwordFile, "password_file"),
finalize<CalendarConfig::Account>([](CalendarConfig::Account& out, std::string_view parentPath,
Diagnostics& diag) {
if (out.type == "ics") {
if (out.serverUrl.empty()) {
diag.error(joinPath(parentPath, "server_url"), "ics accounts require server_url (.ics file URL)");
}
return;
}
if (out.type != "caldav") {
if (out.credentialSource != CalendarCredentialSource::SecretService) {
diag.error(joinPath(parentPath, "credential_source"), "credential_source is only valid for caldav");
Expand Down
67 changes: 54 additions & 13 deletions src/shell/settings/settings_window_popups.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ namespace {
ICloud,
CustomCalDav,
Google,
IcsFileURL,
};

struct CalendarAccountDraft {
Expand Down Expand Up @@ -158,6 +159,8 @@ namespace {
return "custom";
case CalendarAccountProvider::Google:
return "google";
case CalendarAccountProvider::IcsFileURL:
return "ics";
}
return "icloud";
}
Expand All @@ -170,6 +173,8 @@ namespace {
return i18n::tr("settings.calendar-accounts.provider.custom");
case CalendarAccountProvider::Google:
return i18n::tr("settings.calendar-accounts.provider.google");
case CalendarAccountProvider::IcsFileURL:
return i18n::tr("settings.calendar-accounts.provider.ics");
}
return i18n::tr("settings.calendar-accounts.provider.icloud");
}
Expand Down Expand Up @@ -1058,7 +1063,7 @@ void SettingsWindow::openCalendarAccountEditor(std::optional<std::string> accoun
auto draft = std::make_shared<CalendarAccountDraft>();
if (accountId.has_value()) {
const CalendarConfig::Account* account = findCalendarAccount(cfg, *accountId);
if (account == nullptr || (account->type != "caldav" && account->type != "google")) {
if (account == nullptr || (account->type != "caldav" && account->type != "google" && account->type != "ics")) {
return;
}
draft->creating = false;
Expand All @@ -1072,6 +1077,8 @@ void SettingsWindow::openCalendarAccountEditor(std::optional<std::string> accoun
draft->calendars = account->calendars;
if (account->type == "google") {
draft->provider = CalendarAccountProvider::Google;
} else if (account->type == "ics") {
draft->provider = CalendarAccountProvider::IcsFileURL;
} else {
draft->provider =
account->provider == "custom" ? CalendarAccountProvider::CustomCalDav : CalendarAccountProvider::ICloud;
Expand Down Expand Up @@ -1158,6 +1165,8 @@ void SettingsWindow::openCalendarAccountEditor(std::optional<std::string> accoun
return 1;
case CalendarAccountProvider::Google:
return 2;
case CalendarAccountProvider::IcsFileURL:
return 3;
}
return 0;
};
Expand All @@ -1169,6 +1178,7 @@ void SettingsWindow::openCalendarAccountEditor(std::optional<std::string> accoun
{.label = calendarProviderTitle(CalendarAccountProvider::ICloud), .glyph = "brand-apple"},
{.label = calendarProviderTitle(CalendarAccountProvider::CustomCalDav), .glyph = "calendar-cog"},
{.label = calendarProviderTitle(CalendarAccountProvider::Google), .glyph = "brand-google"},
{.label = calendarProviderTitle(CalendarAccountProvider::IcsFileURL), .glyph = "link"}
},
.selectedIndex = providerIndex(draft->provider),
.scale = scale,
Expand All @@ -1180,19 +1190,28 @@ void SettingsWindow::openCalendarAccountEditor(std::optional<std::string> accoun
provider = CalendarAccountProvider::CustomCalDav;
} else if (index == 2) {
provider = CalendarAccountProvider::Google;
} else if (index == 3) {
provider = CalendarAccountProvider::IcsFileURL;
}

draft->provider = provider;
if (provider == CalendarAccountProvider::Google) {
draft->credentialSource = CalendarCredentialSource::SecretService;
draft->passwordFile.clear();
}
if (provider == CalendarAccountProvider::Google && draft->id == "personal_icloud") {
bool isDefaultId = draft->id.empty()
|| draft->id == "personal_icloud"
|| draft->id == "home_nextcloud"
|| draft->id == "personal_google"
|| draft->id == "subscription";
if (provider == CalendarAccountProvider::Google && isDefaultId) {
draft->id = "personal_google";
} else if (provider == CalendarAccountProvider::CustomCalDav && draft->id == "personal_icloud") {
} else if (provider == CalendarAccountProvider::CustomCalDav && isDefaultId) {
draft->id = "home_nextcloud";
} else if (provider == CalendarAccountProvider::ICloud && draft->id == "personal_google") {
} else if (provider == CalendarAccountProvider::ICloud && isDefaultId) {
draft->id = "personal_icloud";
} else if (provider == CalendarAccountProvider::IcsFileURL && isDefaultId) {
draft->id = "subscription";
}
if (m_editorSheetPopup != nullptr) {
m_editorSheetPopup->rebuildBody();
Expand Down Expand Up @@ -1234,7 +1253,7 @@ void SettingsWindow::openCalendarAccountEditor(std::optional<std::string> accoun
Input* passwordInput = nullptr;
Input* passwordFileInput = nullptr;
Input* serverInput = nullptr;
if (draft->provider != CalendarAccountProvider::Google) {
if (draft->provider != CalendarAccountProvider::Google && draft->provider != CalendarAccountProvider::IcsFileURL) {
addField(
body, i18n::tr("settings.calendar-accounts.credential-source-label"),
ui::segmented({
Expand Down Expand Up @@ -1321,6 +1340,21 @@ void SettingsWindow::openCalendarAccountEditor(std::optional<std::string> accoun
})
);
}
if (draft->provider == CalendarAccountProvider::IcsFileURL) {
addField(
body, i18n::tr("settings.calendar-accounts.ics-url-label"),
ui::input(
{.out = &serverInput,
.value = draft->serverUrl,
.placeholder = "https://example.com/calendar.ics",
.invalid = draft->serverUrlInvalid,
.onChange = [draft](const std::string& value) {
draft->serverUrl = value;
draft->serverUrlInvalid = false;
}}
)
);
}

addField(
body, i18n::tr("settings.calendar-accounts.color-label"),
Expand Down Expand Up @@ -1446,11 +1480,13 @@ void SettingsWindow::openCalendarAccountEditor(std::optional<std::string> accoun
draft->idInvalid = true;
}

const bool caldav = draft->provider != CalendarAccountProvider::Google;
const bool caldav = draft->provider == CalendarAccountProvider::ICloud
|| draft->provider == CalendarAccountProvider::CustomCalDav;
const bool ics = draft->provider == CalendarAccountProvider::IcsFileURL;
if (caldav && draft->username.empty()) {
draft->usernameInvalid = true;
}
if (draft->provider == CalendarAccountProvider::CustomCalDav && draft->serverUrl.empty()) {
if ((draft->provider == CalendarAccountProvider::CustomCalDav || ics) && draft->serverUrl.empty()) {
draft->serverUrlInvalid = true;
}
if (caldav
Expand All @@ -1472,9 +1508,14 @@ void SettingsWindow::openCalendarAccountEditor(std::optional<std::string> accoun
overrides.push_back({{"calendar", "enabled"}, true});
}
const std::vector<std::string> base = {"calendar", "account", draft->id};
overrides.push_back(
{{base[0], base[1], base[2], "type"}, caldav ? std::string("caldav") : std::string("google")}
);

std::string type = "caldav";
if (draft->provider == CalendarAccountProvider::Google)
type = "google";
else if (ics)
type = "ics";

overrides.push_back({{base[0], base[1], base[2], "type"}, type});
overrides.push_back({{base[0], base[1], base[2], "name"}, draft->name});
overrides.push_back({{base[0], base[1], base[2], "color"}, draft->color});
// Manual calendar selection is currently populated by CalDAV discovery; Google uses CalendarList selected.
Expand All @@ -1490,9 +1531,9 @@ void SettingsWindow::openCalendarAccountEditor(std::optional<std::string> accoun
);
overrides.push_back({{base[0], base[1], base[2], "password_file"}, draft->passwordFile});
}
if (draft->provider == CalendarAccountProvider::CustomCalDav) {
overrides.push_back({{base[0], base[1], base[2], "server_url"}, draft->serverUrl});
}
}
if (draft->provider == CalendarAccountProvider::CustomCalDav || ics) {
overrides.push_back({{base[0], base[1], base[2], "server_url"}, draft->serverUrl});
}

std::string connectActivationToken;
Expand Down
7 changes: 4 additions & 3 deletions src/shell/settings/settings_window_scene.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1863,13 +1863,13 @@ void SettingsWindow::refreshSettingsRegistry(const Config& cfg) {
.action = [this]() { openCalendarAccountEditor(std::nullopt); },
.glyph = "plus",
},
.searchText = "calendar add account icloud caldav google",
.searchText = "calendar add account icloud caldav google ics ical subscription",
};
it = m_settingsRegistry.insert(it, std::move(addBtn));
++it;

for (const CalendarConfig::Account& account : cfg.calendar.accounts) {
if (account.type != "google" && account.type != "caldav") {
if (account.type != "google" && account.type != "caldav" && account.type != "ics") {
continue;
}
const bool reconnectRequired = account.type == "google"
Expand All @@ -1893,7 +1893,8 @@ void SettingsWindow::refreshSettingsRegistry(const Config& cfg) {
.action = [this, id = account.id]() { openCalendarAccountEditor(id); },
.glyph = reconnectRequired ? "brand-google" : "edit",
},
.searchText = "calendar account edit connect authorize caldav icloud google password " + account.id,
.searchText = "calendar account edit connect authorize caldav icloud google password ics ical subscription"
+ account.id,
.visibleWhen = calendarOn,
};
it = m_settingsRegistry.insert(it, std::move(btn));
Expand Down
Loading