dateMenu: Use graphical weather forecasts

While the current textual forecast is non-intrusive, it may be too
much so, making it less effective to spot the current conditions
at a glance.

Refresh the section to use a more conventional graphical representation,
similar to the one used by gnome-weather itself.

https://gitlab.gnome.org/GNOME/gnome-shell/issues/262
This commit is contained in:
Florian Müllner 2018-12-13 14:45:22 +01:00 committed by Florian Müllner
parent 4590094605
commit 5dedb97fcc
2 changed files with 89 additions and 78 deletions

View File

@ -922,7 +922,8 @@ StScrollBar {
font-weight: bold; font-weight: bold;
} }
.world-clocks-grid { .world-clocks-grid,
.weather-grid {
spacing-rows: 0.4em; spacing-rows: 0.4em;
} }
@ -930,6 +931,19 @@ StScrollBar {
spacing: 0.4em; spacing: 0.4em;
} }
.weather-grid {
spacing-columns: 0.8em;
}
.weather-forecast-icon {
icon-size: 2.18em;
}
.weather-forecast-time {
color: darken($fg_color,40%);
font-size: 0.8em;
}
.calendar-month-label { .calendar-month-label {
color: darken($fg_color,5%); color: darken($fg_color,5%);
font-weight: bold; font-weight: bold;

View File

@ -22,6 +22,8 @@ const Calendar = imports.ui.calendar;
const Weather = imports.misc.weather; const Weather = imports.misc.weather;
const System = imports.system; const System = imports.system;
const MAX_FORECASTS = 5;
function _isToday(date) { function _isToday(date) {
let now = new Date(); let now = new Date();
return now.getYear() == date.getYear() && return now.getYear() == date.getYear() &&
@ -220,106 +222,101 @@ var WeatherSection = class WeatherSection {
x_align: Clutter.ActorAlign.START, x_align: Clutter.ActorAlign.START,
text: _("Weather") })); text: _("Weather") }));
this._conditionsLabel = new St.Label({ style_class: 'weather-conditions', let layout = new Clutter.GridLayout({ orientation: Clutter.Orientation.VERTICAL });
x_align: Clutter.ActorAlign.START }); this._forecastGrid = new St.Widget({ style_class: 'weather-grid',
this._conditionsLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE; layout_manager: layout });
this._conditionsLabel.clutter_text.line_wrap = true; layout.hookup_style(this._forecastGrid);
box.add_child(this._conditionsLabel); box.add_child(this._forecastGrid);
this._weatherClient.connect('changed', this._sync.bind(this)); this._weatherClient.connect('changed', this._sync.bind(this));
this._sync(); this._sync();
} }
_getSummary(info, capitalize=false) { _getInfos() {
let options = capitalize ? GWeather.FormatOptions.SENTENCE_CAPITALIZATION
: GWeather.FormatOptions.NO_CAPITALIZATION;
let [ok, phenomenon, qualifier] = info.get_value_conditions();
if (ok)
return new GWeather.Conditions({ significant: true,
phenomenon,
qualifier }).to_string_full(options);
let [, sky] = info.get_value_sky();
return GWeather.Sky.to_string_full(sky, options);
}
_sameSummary(info1, info2) {
let [ok1, phenom1, qualifier1] = info1.get_value_conditions();
let [ok2, phenom2, qualifier2] = info2.get_value_conditions();
if (ok1 || ok2)
return ok1 == ok2 && phenom1 == phenom2 && qualifier1 == qualifier2;
let [, sky1] = info1.get_value_sky();
let [, sky2] = info2.get_value_sky();
return sky1 == sky2;
}
_getSummaryText() {
let info = this._weatherClient.info; let info = this._weatherClient.info;
let forecasts = info.get_forecast_list(); let forecasts = info.get_forecast_list();
if (forecasts.length == 0) // No forecasts, just current conditions
return '%s.'.format(this._getSummary(info, true));
let current = info; let current = info;
let infos = [info]; let infos = [info];
for (let i = 0; i < forecasts.length; i++) { for (let i = 0; i < forecasts.length; i++) {
let [ok, timestamp] = forecasts[i].get_value_update(); let [ok, timestamp] = forecasts[i].get_value_update();
if (!_isToday(new Date(timestamp * 1000))) let datetime = new Date(timestamp * 1000);
if (!_isToday(datetime))
continue; // Ignore forecasts from other days continue; // Ignore forecasts from other days
if (this._sameSummary(current, forecasts[i])) [ok, timestamp] = current.get_value_update();
continue; // Ignore consecutive runs of equal summaries let currenttime = new Date(timestamp * 1000);
if (currenttime.getHours() == datetime.getHours())
continue; // Enforce a minimum interval of 1h
current = forecasts[i]; current = forecasts[i];
if (infos.push(current) == 3) if (infos.push(current) == MAX_FORECASTS)
break; // Use a maximum of three summaries break; // Use a maximum of five forecasts
}
return infos;
} }
let fmt; _addForecasts() {
switch(infos.length) { let layout = this._forecastGrid.layout_manager;
/* Translators: %s is a weather condition like "Clear sky"; see
libgweather for the possible condition strings. If at all
possible, the sentence should match the grammatical case etc. of
the inserted conditions. */
case 1: fmt = _("%s all day."); break;
/* Translators: %s is a weather condition like "Clear sky"; see let infos = this._getInfos();
libgweather for the possible condition strings. If at all if (this._forecastGrid.text_direction == Clutter.TextDirection.RTL)
possible, the sentence should match the grammatical case etc. of infos.reverse();
the inserted conditions. */
case 2: fmt = _("%s, then %s later."); break;
/* Translators: %s is a weather condition like "Clear sky"; see let col = 0;
libgweather for the possible condition strings. If at all infos.forEach(fc => {
possible, the sentence should match the grammatical case etc. of let [ok, timestamp] = fc.get_value_update();
the inserted conditions. */ let timeStr = Util.formatTime(new Date(timestamp * 1000), {
case 3: fmt = _("%s, then %s, followed by %s later."); break; timeOnly: true
} });
let summaries = infos.map((info, i) => {
let capitalize = i == 0 && fmt.startsWith('%s'); let icon = new St.Icon({ style_class: 'weather-forecast-icon',
return this._getSummary(info, capitalize); icon_name: fc.get_symbolic_icon_name(),
x_align: Clutter.ActorAlign.CENTER,
x_expand: true });
let temp = new St.Label({ style_class: 'weather-forecast-temp',
text: fc.get_temp_summary(),
x_align: Clutter.ActorAlign.CENTER });
let time = new St.Label({ style_class: 'weather-forecast-time',
text: timeStr,
x_align: Clutter.ActorAlign.CENTER });
layout.attach(icon, col, 0, 1, 1);
layout.attach(temp, col, 1, 1, 1);
layout.attach(time, col, 2, 1, 1);
col++;
}); });
return String.prototype.format.apply(fmt, summaries);
} }
_getLabelText() { _setStatusLabel(text) {
if (!this._weatherClient.hasLocation) let layout = this._forecastGrid.layout_manager;
return _("Select a location…"); let label = new St.Label({ text });
layout.attach(label, 0, 0, 1, 1);
}
if (this._weatherClient.loading) _updateForecasts() {
return _("Loading…"); this._forecastGrid.destroy_all_children();
if (!this._weatherClient.hasLocation) {
this._setStatusLabel(_("Select a location…"));
return;
}
if (this._weatherClient.loading) {
this._setStatusLabel(_("Loading…"));
return;
}
let info = this._weatherClient.info; let info = this._weatherClient.info;
if (info.is_valid()) if (info.is_valid()) {
return this._getSummaryText() + ' ' + this._addForecasts();
/* Translators: %s is a temperature with unit, e.g. "23℃" */ return;
_("Feels like %s.").format(info.get_apparent()); }
if (info.network_error()) if (info.network_error())
return _("Go online for weather information"); this._setStatusLabel(_("Go online for weather information"));
else
return _("Weather information is currently unavailable"); this._setStatusLabel(_("Weather information is currently unavailable"));
} }
_sync() { _sync() {
@ -328,7 +325,7 @@ var WeatherSection = class WeatherSection {
if (!this.actor.visible) if (!this.actor.visible)
return; return;
this._conditionsLabel.text = this._getLabelText(); this._updateForecasts();
} }
}; };