feat(todo): the weather: a forecast on days, tasks tied to it, the days that fit (ADR 0033, D40)

A task can need weather: dry, sunny, calm and a temperature range (not
freezing, warm, or from and to as typed), set from Weather in its
details (a chip beside Priority, then a row of its own once it needs
any) and kept in its sealed payload (`weather`, schema stays 1).
Where its day's forecast misses any of it, its row carries a mark and
its details say what is missing; nothing moves and its reminder stays.
Its date picker lists the coming days the forecast fits, at its time or
from 8:00 to 20:00 without one.

The forecast is for the place set in Settings' new Weather card, kept in
the sealed settings; else, for a task with a place, its first place. The
device's location is never read for it. `ForecastCubit` asks the
instance for each place (`InstanceWeather`, rounded to about a
kilometre) as it comes, as the app opens, hourly and on a pull, and
holds the answers in memory alone. With a place in Settings, each day's
heading in Today and Upcoming shows its sky and temperatures, the 3-day
grid its icon. Settings says where the instance gives no forecasts, and
credits Open-Meteo as its licence asks.

Whats-New: A task can need dry, sunny, calm or mild weather: it is marked when the forecast doesn't fit, and its date picker shows the days that do; days show the forecast for the place set in Settings.
Whats-New-De: Eine Aufgabe kann trockenes, sonniges, windstilles oder mildes Wetter brauchen: Sie wird markiert, wenn die Vorhersage nicht passt, und ihre Datumsauswahl zeigt die passenden Tage; die Tage zeigen die Vorhersage für den Ort aus den Einstellungen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-11-10 14:51:52 +01:00
commit 8502fb38f0
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
5 changed files with 2315 additions and 22 deletions

View file

@ -409,5 +409,33 @@
"projectCalendarAddress": "Kalenderadresse (iCal)",
"projectCalendarHelp": "Seine Termine stehen neben den Aufgaben dieses Projekts, nur zum Lesen. Jedes deiner Geräte ruft ihn selbst ab.",
"projectCalendarInvalid": "Eine Adresse, die mit https:// oder webcal:// beginnt",
"eventFromCalendar": "Aus dem Kalender von {project}"
"eventFromCalendar": "Aus dem Kalender von {project}",
"weatherTitle": "Wetter",
"weatherNeedDry": "Trocken",
"weatherNeedSunny": "Sonnig",
"weatherNeedCalm": "Windstill",
"weatherTemperature": "Temperatur",
"weatherAnyTemperature": "Egal",
"weatherNotFreezing": "Kein Frost",
"weatherWarm": "Warm",
"weatherOwnRange": "Von … bis …",
"weatherFromLabel": "Ab",
"weatherToLabel": "Bis",
"weatherFrom": "ab {degrees} °C",
"weatherUpTo": "bis {degrees} °C",
"weatherBetween": "{low}–{high} °C",
"weatherDone": "Fertig",
"weatherFits": "Die Vorhersage passt",
"weatherDoesNotFit": "Die Vorhersage passt nicht: {missed}",
"weatherMiss": "{need, select, dry{Regen} sunny{Wolken} temperature{Temperatur} calm{Wind} other{Wetter}}",
"weatherConditions": "{conditions, select, clear{Klar} partlyCloudy{Teils bewölkt} cloudy{Bedeckt} fog{Nebel} drizzle{Nieselregen} rain{Regen} freezingRain{Gefrierender Regen} snow{Schnee} rainShowers{Schauer} snowShowers{Schneeschauer} thunderstorm{Gewitter} other{Wetter}}",
"weatherFittingDays": "Tage mit passendem Wetter",
"weatherNoFittingDay": "Kein Tag der Vorhersage passt",
"weatherSettings": "Wetter",
"weatherPlace": "Ort der Vorhersage",
"weatherPlaceNone": "Keiner: der eigene Ort jeder Aufgabe",
"weatherPlaceHelp": "Die Tage zeigen die Vorhersage für diesen Ort, und Aufgaben, die vom Wetter abhängen, richten sich nach ihr. Der Standort deines Geräts wird dafür nie verwendet.",
"weatherPlaceRemove": "Ort entfernen",
"weatherOff": "Diese Instanz liefert keine Vorhersagen.",
"weatherBy": "Wetterdaten von Open-Meteo.com"
}

View file

@ -1287,5 +1287,150 @@
"type": "String"
}
}
},
"weatherTitle": "Weather",
"@weatherTitle": {
"description": "A task's weather it needs, in its details and the sheet choosing it (ADR 0033, D40)."
},
"weatherNeedDry": "Dry",
"@weatherNeedDry": {
"description": "A task needs no rain or snow over its hours (D40)."
},
"weatherNeedSunny": "Sunny",
"@weatherNeedSunny": {
"description": "A task needs a clear or partly cloudy sky (D40)."
},
"weatherNeedCalm": "Calm",
"@weatherNeedCalm": {
"description": "A task needs little wind (D40)."
},
"weatherTemperature": "Temperature",
"@weatherTemperature": {
"description": "The temperatures a task needs (D40)."
},
"weatherAnyTemperature": "Any",
"@weatherAnyTemperature": {
"description": "No temperature needed (D40)."
},
"weatherNotFreezing": "Not freezing",
"@weatherNotFreezing": {
"description": "From 1 °C on (D40)."
},
"weatherWarm": "Warm",
"@weatherWarm": {
"description": "From 18 °C on (D40)."
},
"weatherOwnRange": "From … to …",
"@weatherOwnRange": {
"description": "Temperatures typed (D40)."
},
"weatherFromLabel": "From",
"@weatherFromLabel": {
"description": "The lowest temperature typed, in °C (D40)."
},
"weatherToLabel": "To",
"@weatherToLabel": {
"description": "The highest temperature typed, in °C (D40)."
},
"weatherFrom": "from {degrees} °C",
"@weatherFrom": {
"description": "A task needs at least this temperature (D40).",
"placeholders": {
"degrees": {
"type": "String"
}
}
},
"weatherUpTo": "up to {degrees} °C",
"@weatherUpTo": {
"description": "A task needs at most this temperature (D40).",
"placeholders": {
"degrees": {
"type": "String"
}
}
},
"weatherBetween": "{low}–{high} °C",
"@weatherBetween": {
"description": "A range of temperatures (D40).",
"placeholders": {
"low": {
"type": "String"
},
"high": {
"type": "String"
}
}
},
"weatherDone": "Done",
"@weatherDone": {
"description": "Closes the sheet of a task's weather, keeping what was chosen (D40)."
},
"weatherFits": "The forecast fits",
"@weatherFits": {
"description": "Under a task's weather where its day's forecast gives what it needs (D40)."
},
"weatherDoesNotFit": "The forecast doesn't fit: {missed}",
"@weatherDoesNotFit": {
"description": "Under a task's weather, and its row's mark, where its day's forecast doesn't give what it needs (D40).",
"placeholders": {
"missed": {
"type": "String"
}
}
},
"weatherMiss": "{need, select, dry{rain} sunny{clouds} temperature{temperature} calm{wind} other{weather}}",
"@weatherMiss": {
"description": "What of the weather a task needs its forecast misses (D40).",
"placeholders": {
"need": {
"type": "String"
}
}
},
"weatherConditions": "{conditions, select, clear{Clear} partlyCloudy{Partly cloudy} cloudy{Overcast} fog{Fog} drizzle{Drizzle} rain{Rain} freezingRain{Freezing rain} snow{Snow} rainShowers{Showers} snowShowers{Snow showers} thunderstorm{Thunderstorm} other{Weather}}",
"@weatherConditions": {
"description": "A day's sky by its forecast, from Open-Meteo's weather code (D40).",
"placeholders": {
"conditions": {
"type": "String"
}
}
},
"weatherFittingDays": "Days the weather fits",
"@weatherFittingDays": {
"description": "The date picker's days whose forecast gives what the task needs (D40)."
},
"weatherNoFittingDay": "No day of the forecast fits",
"@weatherNoFittingDay": {
"description": "The date picker, where no day of the forecast fits (D40)."
},
"weatherSettings": "Weather",
"@weatherSettings": {
"description": "Settings' group of the forecast (D40)."
},
"weatherPlace": "Place of the forecast",
"@weatherPlace": {
"description": "Settings: the place days' forecasts and tasks' weather are for (D40)."
},
"weatherPlaceNone": "None: each task's own place",
"@weatherPlaceNone": {
"description": "Settings, where no place is set (D40)."
},
"weatherPlaceHelp": "Days show this place's forecast, and tasks tied to the weather are judged by it. Your device's location is never used for it.",
"@weatherPlaceHelp": {
"description": "Settings, below the place of the forecast (D40)."
},
"weatherPlaceRemove": "Remove the place",
"@weatherPlaceRemove": {
"description": "Settings: takes the place of the forecast away (D40)."
},
"weatherOff": "This instance gives no forecasts.",
"@weatherOff": {
"description": "Settings, where the instance's operator switched the weather off (D40)."
},
"weatherBy": "Weather data by Open-Meteo.com",
"@weatherBy": {
"description": "The credit Open-Meteo's licence asks for next to its data (D40)."
}
}

View file

@ -2559,6 +2559,174 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'From {project}\'s calendar'**
String eventFromCalendar(String project);
/// A task's weather it needs, in its details and the sheet choosing it (ADR 0033, D40).
///
/// In en, this message translates to:
/// **'Weather'**
String get weatherTitle;
/// A task needs no rain or snow over its hours (D40).
///
/// In en, this message translates to:
/// **'Dry'**
String get weatherNeedDry;
/// A task needs a clear or partly cloudy sky (D40).
///
/// In en, this message translates to:
/// **'Sunny'**
String get weatherNeedSunny;
/// A task needs little wind (D40).
///
/// In en, this message translates to:
/// **'Calm'**
String get weatherNeedCalm;
/// The temperatures a task needs (D40).
///
/// In en, this message translates to:
/// **'Temperature'**
String get weatherTemperature;
/// No temperature needed (D40).
///
/// In en, this message translates to:
/// **'Any'**
String get weatherAnyTemperature;
/// From 1 °C on (D40).
///
/// In en, this message translates to:
/// **'Not freezing'**
String get weatherNotFreezing;
/// From 18 °C on (D40).
///
/// In en, this message translates to:
/// **'Warm'**
String get weatherWarm;
/// Temperatures typed (D40).
///
/// In en, this message translates to:
/// **'From … to …'**
String get weatherOwnRange;
/// The lowest temperature typed, in °C (D40).
///
/// In en, this message translates to:
/// **'From'**
String get weatherFromLabel;
/// The highest temperature typed, in °C (D40).
///
/// In en, this message translates to:
/// **'To'**
String get weatherToLabel;
/// A task needs at least this temperature (D40).
///
/// In en, this message translates to:
/// **'from {degrees} °C'**
String weatherFrom(String degrees);
/// A task needs at most this temperature (D40).
///
/// In en, this message translates to:
/// **'up to {degrees} °C'**
String weatherUpTo(String degrees);
/// A range of temperatures (D40).
///
/// In en, this message translates to:
/// **'{low}–{high} °C'**
String weatherBetween(String low, String high);
/// Closes the sheet of a task's weather, keeping what was chosen (D40).
///
/// In en, this message translates to:
/// **'Done'**
String get weatherDone;
/// Under a task's weather where its day's forecast gives what it needs (D40).
///
/// In en, this message translates to:
/// **'The forecast fits'**
String get weatherFits;
/// Under a task's weather, and its row's mark, where its day's forecast doesn't give what it needs (D40).
///
/// In en, this message translates to:
/// **'The forecast doesn\'t fit: {missed}'**
String weatherDoesNotFit(String missed);
/// What of the weather a task needs its forecast misses (D40).
///
/// In en, this message translates to:
/// **'{need, select, dry{rain} sunny{clouds} temperature{temperature} calm{wind} other{weather}}'**
String weatherMiss(String need);
/// A day's sky by its forecast, from Open-Meteo's weather code (D40).
///
/// In en, this message translates to:
/// **'{conditions, select, clear{Clear} partlyCloudy{Partly cloudy} cloudy{Overcast} fog{Fog} drizzle{Drizzle} rain{Rain} freezingRain{Freezing rain} snow{Snow} rainShowers{Showers} snowShowers{Snow showers} thunderstorm{Thunderstorm} other{Weather}}'**
String weatherConditions(String conditions);
/// The date picker's days whose forecast gives what the task needs (D40).
///
/// In en, this message translates to:
/// **'Days the weather fits'**
String get weatherFittingDays;
/// The date picker, where no day of the forecast fits (D40).
///
/// In en, this message translates to:
/// **'No day of the forecast fits'**
String get weatherNoFittingDay;
/// Settings' group of the forecast (D40).
///
/// In en, this message translates to:
/// **'Weather'**
String get weatherSettings;
/// Settings: the place days' forecasts and tasks' weather are for (D40).
///
/// In en, this message translates to:
/// **'Place of the forecast'**
String get weatherPlace;
/// Settings, where no place is set (D40).
///
/// In en, this message translates to:
/// **'None: each task\'s own place'**
String get weatherPlaceNone;
/// Settings, below the place of the forecast (D40).
///
/// In en, this message translates to:
/// **'Days show this place\'s forecast, and tasks tied to the weather are judged by it. Your device\'s location is never used for it.'**
String get weatherPlaceHelp;
/// Settings: takes the place of the forecast away (D40).
///
/// In en, this message translates to:
/// **'Remove the place'**
String get weatherPlaceRemove;
/// Settings, where the instance's operator switched the weather off (D40).
///
/// In en, this message translates to:
/// **'This instance gives no forecasts.'**
String get weatherOff;
/// The credit Open-Meteo's licence asks for next to its data (D40).
///
/// In en, this message translates to:
/// **'Weather data by Open-Meteo.com'**
String get weatherBy;
}
class _AppLocalizationsDelegate

View file

@ -1608,4 +1608,128 @@ class AppLocalizationsDe extends AppLocalizations {
String eventFromCalendar(String project) {
return 'Aus dem Kalender von $project';
}
@override
String get weatherTitle => 'Wetter';
@override
String get weatherNeedDry => 'Trocken';
@override
String get weatherNeedSunny => 'Sonnig';
@override
String get weatherNeedCalm => 'Windstill';
@override
String get weatherTemperature => 'Temperatur';
@override
String get weatherAnyTemperature => 'Egal';
@override
String get weatherNotFreezing => 'Kein Frost';
@override
String get weatherWarm => 'Warm';
@override
String get weatherOwnRange => 'Von … bis …';
@override
String get weatherFromLabel => 'Ab';
@override
String get weatherToLabel => 'Bis';
@override
String weatherFrom(String degrees) {
return 'ab $degrees °C';
}
@override
String weatherUpTo(String degrees) {
return 'bis $degrees °C';
}
@override
String weatherBetween(String low, String high) {
return '$low–$high °C';
}
@override
String get weatherDone => 'Fertig';
@override
String get weatherFits => 'Die Vorhersage passt';
@override
String weatherDoesNotFit(String missed) {
return 'Die Vorhersage passt nicht: $missed';
}
@override
String weatherMiss(String need) {
String _temp0 = intl.Intl.selectLogic(
need,
{
'dry': 'Regen',
'sunny': 'Wolken',
'temperature': 'Temperatur',
'calm': 'Wind',
'other': 'Wetter',
},
);
return '$_temp0';
}
@override
String weatherConditions(String conditions) {
String _temp0 = intl.Intl.selectLogic(
conditions,
{
'clear': 'Klar',
'partlyCloudy': 'Teils bewölkt',
'cloudy': 'Bedeckt',
'fog': 'Nebel',
'drizzle': 'Nieselregen',
'rain': 'Regen',
'freezingRain': 'Gefrierender Regen',
'snow': 'Schnee',
'rainShowers': 'Schauer',
'snowShowers': 'Schneeschauer',
'thunderstorm': 'Gewitter',
'other': 'Wetter',
},
);
return '$_temp0';
}
@override
String get weatherFittingDays => 'Tage mit passendem Wetter';
@override
String get weatherNoFittingDay => 'Kein Tag der Vorhersage passt';
@override
String get weatherSettings => 'Wetter';
@override
String get weatherPlace => 'Ort der Vorhersage';
@override
String get weatherPlaceNone => 'Keiner: der eigene Ort jeder Aufgabe';
@override
String get weatherPlaceHelp =>
'Die Tage zeigen die Vorhersage für diesen Ort, und Aufgaben, die vom Wetter abhängen, richten sich nach ihr. Der Standort deines Geräts wird dafür nie verwendet.';
@override
String get weatherPlaceRemove => 'Ort entfernen';
@override
String get weatherOff => 'Diese Instanz liefert keine Vorhersagen.';
@override
String get weatherBy => 'Wetterdaten von Open-Meteo.com';
}

View file

@ -1605,6 +1605,130 @@ class AppLocalizationsEn extends AppLocalizations {
String eventFromCalendar(String project) {
return 'From $project\'s calendar';
}
@override
String get weatherTitle => 'Weather';
@override
String get weatherNeedDry => 'Dry';
@override
String get weatherNeedSunny => 'Sunny';
@override
String get weatherNeedCalm => 'Calm';
@override
String get weatherTemperature => 'Temperature';
@override
String get weatherAnyTemperature => 'Any';
@override
String get weatherNotFreezing => 'Not freezing';
@override
String get weatherWarm => 'Warm';
@override
String get weatherOwnRange => 'From … to …';
@override
String get weatherFromLabel => 'From';
@override
String get weatherToLabel => 'To';
@override
String weatherFrom(String degrees) {
return 'from $degrees °C';
}
@override
String weatherUpTo(String degrees) {
return 'up to $degrees °C';
}
@override
String weatherBetween(String low, String high) {
return '$low–$high °C';
}
@override
String get weatherDone => 'Done';
@override
String get weatherFits => 'The forecast fits';
@override
String weatherDoesNotFit(String missed) {
return 'The forecast doesn\'t fit: $missed';
}
@override
String weatherMiss(String need) {
String _temp0 = intl.Intl.selectLogic(
need,
{
'dry': 'rain',
'sunny': 'clouds',
'temperature': 'temperature',
'calm': 'wind',
'other': 'weather',
},
);
return '$_temp0';
}
@override
String weatherConditions(String conditions) {
String _temp0 = intl.Intl.selectLogic(
conditions,
{
'clear': 'Clear',
'partlyCloudy': 'Partly cloudy',
'cloudy': 'Overcast',
'fog': 'Fog',
'drizzle': 'Drizzle',
'rain': 'Rain',
'freezingRain': 'Freezing rain',
'snow': 'Snow',
'rainShowers': 'Showers',
'snowShowers': 'Snow showers',
'thunderstorm': 'Thunderstorm',
'other': 'Weather',
},
);
return '$_temp0';
}
@override
String get weatherFittingDays => 'Days the weather fits';
@override
String get weatherNoFittingDay => 'No day of the forecast fits';
@override
String get weatherSettings => 'Weather';
@override
String get weatherPlace => 'Place of the forecast';
@override
String get weatherPlaceNone => 'None: each task\'s own place';
@override
String get weatherPlaceHelp =>
'Days show this place\'s forecast, and tasks tied to the weather are judged by it. Your device\'s location is never used for it.';
@override
String get weatherPlaceRemove => 'Remove the place';
@override
String get weatherOff => 'This instance gives no forecasts.';
@override
String get weatherBy => 'Weather data by Open-Meteo.com';
}
/// The translations for English, as used in the United Kingdom (`en_GB`).