Merge branch 'feat-weather' of https://github.com/jpirnay/crosspoint-reader into mybuild

This commit is contained in:
jpirnay
2026-04-02 20:19:43 +02:00
18 changed files with 200 additions and 447 deletions
+1 -1
View File
@@ -230,7 +230,7 @@ void HomeActivity::render(RenderLock&&) {
// Build menu items dynamically
std::vector<const char*> menuItems = {tr(STR_BROWSE_FILES), tr(STR_MENU_RECENT_BOOKS), tr(STR_FILE_TRANSFER),
tr(STR_WEATHER), tr(STR_SETTINGS_TITLE)};
std::vector<UIIcon> menuIcons = {Folder, Recent, Library, Transfer, Settings};
std::vector<UIIcon> menuIcons = {Folder, Recent, Transfer, Library, Settings};
if (hasOpdsUrl) {
// Insert OPDS Browser after Recents (before File Transfer)
+34 -40
View File
@@ -232,6 +232,27 @@ void WeatherActivity::fetchWeather() {
requestUpdate();
}
void WeatherActivity::openSettingsActivity() {
renderer.setOrientation(GfxRenderer::Orientation::Portrait);
startActivityForResult(std::make_unique<WeatherSettingsActivity>(renderer, mappedInput),
[this](const ActivityResult&) {
renderer.setOrientation(GfxRenderer::Orientation::LandscapeClockwise);
forceRefresh = true;
loadAndDisplay();
});
}
void WeatherActivity::triggerRefresh(const bool showPopup) {
showRefreshPopup = showPopup;
if (WiFi.status() == WL_CONNECTED && WiFi.localIP() != IPAddress(0, 0, 0, 0)) {
state = State::FETCHING;
requestUpdate(true);
fetchWeather();
} else {
launchWifiSelection();
}
}
void WeatherActivity::loop() {
if (state == State::WIFI_SELECTION) {
return; // Handled by WifiSelectionActivity
@@ -239,24 +260,10 @@ void WeatherActivity::loop() {
if (state == State::ERROR) {
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
// Open weather settings (useful when no location is configured)
renderer.setOrientation(GfxRenderer::Orientation::Portrait);
startActivityForResult(std::make_unique<WeatherSettingsActivity>(renderer, mappedInput),
[this](const ActivityResult&) {
renderer.setOrientation(GfxRenderer::Orientation::LandscapeClockwise);
forceRefresh = true;
loadAndDisplay();
});
openSettingsActivity();
} else if (mappedInput.wasReleased(MappedInputManager::Button::Left) ||
mappedInput.wasReleased(MappedInputManager::Button::Right)) {
// Retry fetch
if (WiFi.status() == WL_CONNECTED && WiFi.localIP() != IPAddress(0, 0, 0, 0)) {
state = State::FETCHING;
requestUpdate(true);
fetchWeather();
} else {
launchWifiSelection();
}
triggerRefresh(false);
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
onGoHome();
}
@@ -299,27 +306,10 @@ void WeatherActivity::loop() {
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
onGoHome();
} else if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
// Open weather settings
renderer.setOrientation(GfxRenderer::Orientation::Portrait);
startActivityForResult(std::make_unique<WeatherSettingsActivity>(renderer, mappedInput),
[this](const ActivityResult&) {
// Re-apply landscape after returning from settings
renderer.setOrientation(GfxRenderer::Orientation::LandscapeClockwise);
// Refresh after settings change
forceRefresh = true;
loadAndDisplay();
});
openSettingsActivity();
} else if (mappedInput.wasReleased(MappedInputManager::Button::Left) ||
mappedInput.wasReleased(MappedInputManager::Button::Right)) {
// Manual refresh with popup feedback.
showRefreshPopup = true;
if (WiFi.status() == WL_CONNECTED && WiFi.localIP() != IPAddress(0, 0, 0, 0)) {
state = State::FETCHING;
requestUpdate(true);
fetchWeather();
} else {
launchWifiSelection();
}
triggerRefresh(true);
}
}
@@ -387,9 +377,9 @@ void WeatherActivity::render(RenderLock&&) {
if (state == State::FETCHING && showRefreshPopup) {
GUI.drawPopup(renderer, tr(STR_LOADING_POPUP));
} else {
renderer.displayBuffer();
}
renderer.displayBuffer();
}
void WeatherActivity::renderCurrentConditions(int x, int y, int w, int h) {
@@ -490,6 +480,11 @@ void WeatherActivity::renderDailyForecast(int x, int y, int w, int h) {
const StrId dayNameIds[] = {StrId::STR_WEATHER_DAY_SUN, StrId::STR_WEATHER_DAY_MON, StrId::STR_WEATHER_DAY_TUE,
StrId::STR_WEATHER_DAY_WED, StrId::STR_WEATHER_DAY_THU, StrId::STR_WEATHER_DAY_FRI,
StrId::STR_WEATHER_DAY_SAT};
const StrId monthNameIds[] = {
StrId::STR_WEATHER_MONTH_JAN, StrId::STR_WEATHER_MONTH_FEB, StrId::STR_WEATHER_MONTH_MAR,
StrId::STR_WEATHER_MONTH_APR, StrId::STR_WEATHER_MONTH_MAY, StrId::STR_WEATHER_MONTH_JUN,
StrId::STR_WEATHER_MONTH_JUL, StrId::STR_WEATHER_MONTH_AUG, StrId::STR_WEATHER_MONTH_SEP,
StrId::STR_WEATHER_MONTH_OCT, StrId::STR_WEATHER_MONTH_NOV, StrId::STR_WEATHER_MONTH_DEC};
const char* unitSuffix = WEATHER_SETTINGS.getTempUnit() == WeatherTempUnit::CELSIUS ? "C" : "F";
int numDays = static_cast<int>(weatherData.daily.size());
@@ -527,8 +522,8 @@ void WeatherActivity::renderDailyForecast(int x, int y, int w, int h) {
// Date (e.g. "Apr 3") - for all days
char dateBuf[16];
const char* monthNames[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
snprintf(dateBuf, sizeof(dateBuf), "%s %d", monthNames[timeinfo.tm_mon], timeinfo.tm_mday);
const char* monthName = I18N.get(monthNameIds[timeinfo.tm_mon]);
snprintf(dateBuf, sizeof(dateBuf), "%s %d", monthName, timeinfo.tm_mday);
int dateWidth = renderer.getTextWidth(SMALL_FONT_ID, dateBuf);
renderer.drawText(SMALL_FONT_ID, cardX + (cardWidth - dateWidth) / 2, textY, dateBuf);
textY += 18;
@@ -638,7 +633,6 @@ void WeatherActivity::renderHourlyGraph(int x, int y, int w, int h) {
float mid = (tempMax + tempMin) / 2.0f;
tempMin = mid - 2.5f;
tempMax = mid + 2.5f;
tempRange = 5.0f;
}
tempMin -= 1.0f;
tempMax += 1.0f;
+2
View File
@@ -43,6 +43,8 @@ class WeatherActivity final : public Activity {
void launchWifiSelection();
void onWifiSelectionComplete(bool connected);
void fetchWeather();
void openSettingsActivity();
void triggerRefresh(bool showPopup);
// Render sub-sections (landscape 800x480)
void renderCurrentConditions(int x, int y, int w, int h);
@@ -125,21 +125,13 @@ void WeatherSettingsActivity::launchCitySearch() {
[this, query = kb.text](const ActivityResult& wifiResult) {
if (wifiResult.isCancelled) return;
searchResults = WeatherClient::searchCity(query);
if (searchResults.empty()) {
requestUpdate();
return;
}
showingSearchResults = true;
showingSearchResults = !searchResults.empty();
selectedIndex = 0;
requestUpdate();
});
} else {
searchResults = WeatherClient::searchCity(kb.text);
if (searchResults.empty()) {
requestUpdate();
return;
}
showingSearchResults = true;
showingSearchResults = !searchResults.empty();
selectedIndex = 0;
requestUpdate();
}
@@ -153,10 +145,14 @@ void WeatherSettingsActivity::launchLatitudeEntry() {
[this](const ActivityResult& result) {
if (!result.isCancelled) {
const auto& kb = std::get<KeyboardResult>(result.data);
float lat = strtof(kb.text.c_str(), nullptr);
if (lat >= -90.0f && lat <= 90.0f) {
WEATHER_SETTINGS.setLocation(lat, WEATHER_SETTINGS.getLongitude(), WEATHER_SETTINGS.getLocationName());
char* end = nullptr;
const float lat = strtof(kb.text.c_str(), &end);
if (end != kb.text.c_str() && *end == '\0' && lat >= -90.0f && lat <= 90.0f) {
if (lat != WEATHER_SETTINGS.getLatitude()) {
WEATHER_SETTINGS.setLocation(lat, WEATHER_SETTINGS.getLongitude(), "");
}
WEATHER_SETTINGS.saveToFile();
requestUpdate();
}
}
});
@@ -169,10 +165,14 @@ void WeatherSettingsActivity::launchLongitudeEntry() {
[this](const ActivityResult& result) {
if (!result.isCancelled) {
const auto& kb = std::get<KeyboardResult>(result.data);
float lon = strtof(kb.text.c_str(), nullptr);
if (lon >= -180.0f && lon <= 180.0f) {
WEATHER_SETTINGS.setLocation(WEATHER_SETTINGS.getLatitude(), lon, WEATHER_SETTINGS.getLocationName());
char* end = nullptr;
const float lon = strtof(kb.text.c_str(), &end);
if (end != kb.text.c_str() && *end == '\0' && lon >= -180.0f && lon <= 180.0f) {
if (lon != WEATHER_SETTINGS.getLongitude()) {
WEATHER_SETTINGS.setLocation(WEATHER_SETTINGS.getLatitude(), lon, "");
}
WEATHER_SETTINGS.saveToFile();
requestUpdate();
}
}
});
@@ -30,7 +30,7 @@ class WeatherSettingsActivity final : public Activity {
std::vector<GeocodingResult> searchResults;
bool showingSearchResults = false;
static constexpr int MENU_ITEMS = 6; // Location, Lat, Lon, TempUnit, WindUnit, PrecipUnit
static constexpr size_t MENU_ITEMS = 6; // Location, Lat, Lon, TempUnit, WindUnit, PrecipUnit
void handleSelection();
void launchCitySearch();
-30
View File
@@ -55,52 +55,37 @@ class FileWriteStream final : public Stream {
bool HttpDownloader::fetchUrl(const std::string& url, Stream& outContent) {
// Use NetworkClientSecure for HTTPS, regular NetworkClient for HTTP
std::unique_ptr<NetworkClient> client;
LOG_DBG("HTTP", "fetchUrl[1] start (https=%d)", UrlUtils::isHttpsUrl(url) ? 1 : 0);
if (UrlUtils::isHttpsUrl(url)) {
auto* secureClient = new NetworkClientSecure();
LOG_DBG("HTTP", "fetchUrl[2] created NetworkClientSecure");
secureClient->setInsecure();
LOG_DBG("HTTP", "fetchUrl[3] setInsecure done");
client.reset(secureClient);
} else {
client.reset(new NetworkClient());
LOG_DBG("HTTP", "fetchUrl[2] created NetworkClient");
}
HTTPClient http;
LOG_DBG("HTTP", "Fetching: %s", url.c_str());
LOG_DBG("HTTP", "fetchUrl[4] begin() before");
http.begin(*client, url.c_str());
LOG_DBG("HTTP", "fetchUrl[5] begin() after");
http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
LOG_DBG("HTTP", "fetchUrl[6] redirects configured");
http.addHeader("User-Agent", "CrossPoint-ESP32-" CROSSPOINT_VERSION);
LOG_DBG("HTTP", "fetchUrl[7] user-agent header added");
// Add Basic HTTP auth if credentials are configured
if (strlen(SETTINGS.opdsUsername) > 0 && strlen(SETTINGS.opdsPassword) > 0) {
std::string credentials = std::string(SETTINGS.opdsUsername) + ":" + SETTINGS.opdsPassword;
String encoded = base64::encode(credentials.c_str());
http.addHeader("Authorization", "Basic " + encoded);
LOG_DBG("HTTP", "fetchUrl[8] auth header added");
}
LOG_DBG("HTTP", "fetchUrl[9] GET() before");
const int httpCode = http.GET();
LOG_DBG("HTTP", "fetchUrl[10] GET() after code=%d", httpCode);
if (httpCode != HTTP_CODE_OK) {
LOG_ERR("HTTP", "Fetch failed: %d", httpCode);
LOG_DBG("HTTP", "fetchUrl[11] end() after GET failure");
http.end();
return false;
}
LOG_DBG("HTTP", "fetchUrl[12] writeToStream() before");
http.writeToStream(&outContent);
LOG_DBG("HTTP", "fetchUrl[13] writeToStream() after");
LOG_DBG("HTTP", "fetchUrl[14] end() before success return");
http.end();
LOG_DBG("HTTP", "Fetch success");
@@ -120,41 +105,30 @@ HttpDownloader::DownloadError HttpDownloader::downloadToFile(const std::string&
ProgressCallback progress) {
// Use NetworkClientSecure for HTTPS, regular NetworkClient for HTTP
std::unique_ptr<NetworkClient> client;
LOG_DBG("HTTP", "downloadToFile[1] start (https=%d)", UrlUtils::isHttpsUrl(url) ? 1 : 0);
if (UrlUtils::isHttpsUrl(url)) {
auto* secureClient = new NetworkClientSecure();
LOG_DBG("HTTP", "downloadToFile[2] created NetworkClientSecure");
secureClient->setInsecure();
LOG_DBG("HTTP", "downloadToFile[3] setInsecure done");
client.reset(secureClient);
} else {
client.reset(new NetworkClient());
LOG_DBG("HTTP", "downloadToFile[2] created NetworkClient");
}
HTTPClient http;
LOG_DBG("HTTP", "Downloading: %s", url.c_str());
LOG_DBG("HTTP", "Destination: %s", destPath.c_str());
LOG_DBG("HTTP", "downloadToFile[4] begin() before");
http.begin(*client, url.c_str());
LOG_DBG("HTTP", "downloadToFile[5] begin() after");
http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
LOG_DBG("HTTP", "downloadToFile[6] redirects configured");
http.addHeader("User-Agent", "CrossPoint-ESP32-" CROSSPOINT_VERSION);
LOG_DBG("HTTP", "downloadToFile[7] user-agent header added");
// Add Basic HTTP auth if credentials are configured
if (strlen(SETTINGS.opdsUsername) > 0 && strlen(SETTINGS.opdsPassword) > 0) {
std::string credentials = std::string(SETTINGS.opdsUsername) + ":" + SETTINGS.opdsPassword;
String encoded = base64::encode(credentials.c_str());
http.addHeader("Authorization", "Basic " + encoded);
LOG_DBG("HTTP", "downloadToFile[8] auth header added");
}
LOG_DBG("HTTP", "downloadToFile[9] GET() before");
const int httpCode = http.GET();
LOG_DBG("HTTP", "downloadToFile[10] GET() after code=%d", httpCode);
if (httpCode != HTTP_CODE_OK) {
LOG_ERR("HTTP", "Download failed: %d", httpCode);
http.end();
@@ -184,14 +158,10 @@ HttpDownloader::DownloadError HttpDownloader::downloadToFile(const std::string&
// Let HTTPClient handle chunked decoding and stream body bytes into the file.
FileWriteStream fileStream(file, contentLength, progress);
LOG_DBG("HTTP", "downloadToFile[11] writeToStream() before");
const int writeResult = http.writeToStream(&fileStream);
LOG_DBG("HTTP", "downloadToFile[12] writeToStream() after result=%d", writeResult);
file.close();
LOG_DBG("HTTP", "downloadToFile[13] file closed");
http.end();
LOG_DBG("HTTP", "downloadToFile[14] http end done");
if (writeResult < 0) {
LOG_ERR("HTTP", "writeToStream error: %d", writeResult);