[gnome-weather/wip/ewlsh/gtk4] Port to GTK4 and libadwaita
- From: Evan Welsh <ewlsh src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-weather/wip/ewlsh/gtk4] Port to GTK4 and libadwaita
- Date: Mon, 14 Feb 2022 04:15:47 +0000 (UTC)
commit 285e357c0aa3c4bf40335f998d476f56507fa789
Author: Evan Welsh <contact evanwelsh com>
Date: Sat Feb 12 21:45:37 2022 -0800
Port to GTK4 and libadwaita
- Rework code base to use ListModels
- Remove Devel variant
- Move UI definitions into templates
- Port to ESM
- Remove automatic location setting
- Port custom rendering to render nodes
.editorconfig | 9 +
.gitignore | 1 +
.gitlab-ci.yml | 2 +-
README.md | 2 +-
data/application.css | 110 -----
data/city.ui | 61 ++-
data/day-entry.ui | 329 ++++++--------
data/hour-entry.ui | 34 +-
.../scalable/status/weather-hourly-symbolic.svg | 7 +
data/icons/meson.build | 8 +-
data/org.gnome.Weather.data.gresource.xml | 4 +-
data/org.gnome.Weather.gschema.xml | 8 -
data/org.gnome.Weather.search-provider.ini.in | 2 +-
data/places-popover.ui | 261 +++---------
data/primary-menu.ui | 2 +
data/style-dark.css | 4 +
data/style.css | 165 ++++++++
data/weather-widget.ui | 471 +++++++++------------
data/window.ui | 143 +++----
meson.build | 10 +-
org.gnome.Weather.json | 29 +-
src/app/application.js | 230 ++++++++++
src/app/city.js | 209 ++++-----
src/app/currentLocationController.js | 46 +-
src/app/dailyForecast.js | 405 +++++++-----------
src/app/entry.js | 243 +++++++++++
src/app/hourlyForecast.js | 121 +++---
src/app/locationRow.js | 48 +++
src/app/locationRow.ui | 52 +++
src/app/main.js | 265 +-----------
src/app/shell.js | 54 +++
src/app/thermometer.js | 242 +++++------
src/app/thermometer.ui | 24 ++
src/app/window.js | 167 +++-----
src/app/world.js | 254 ++++-------
src/misc/util.js | 153 ++++---
src/org.gnome.Weather.BackgroundService.in | 8 +-
....Weather.BackgroundService.src.gresource.xml.in | 2 +-
src/org.gnome.Weather.in | 6 +-
src/org.gnome.Weather.src.gresource.xml.in | 8 +-
src/service/main.js | 23 +-
src/service/searchProvider.js | 68 ++-
src/shared/world.js | 195 +++++----
tests/testutil.py | 3 -
44 files changed, 2194 insertions(+), 2294 deletions(-)
---
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..ee14c01
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,9 @@
+[*]
+indent_style = space
+indent_size = 4
+
+[javascript]
+quote_style = single
+
+[*.{xml,ui}]
+indent_size = 2
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index c11c957..6f9a211 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,4 @@ _build
**/__pycache__
.fenv/
.flatpak-builder/
+.flatpak*/
\ No newline at end of file
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 2ae78be..7ff58b1 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -9,7 +9,7 @@ flatpak:
MANIFEST_PATH: "org.gnome.Weather.json"
FLATPAK_MODULE: "gnome-weather"
RUNTIME_REPO: "https://nightly.gnome.org/gnome-nightly.flatpakrepo"
- APP_ID: "org.gnome.WeatherDevel"
+ APP_ID: "org.gnome.Weather"
extends: .flatpak
nightly:
diff --git a/README.md b/README.md
index 21475ea..393dc96 100644
--- a/README.md
+++ b/README.md
@@ -33,7 +33,7 @@ $ flatpak remote-add gnome-nightly https://nightly.gnome.org/gnome-nightly.flatp
Then, run the following command to install the development version of Weather:
```
-$ flatpak install org.gnome.WeatherDevel
+$ flatpak install org.gnome.Weather
```
## Hacking on Weather
diff --git a/data/city.ui b/data/city.ui
index 8d6c66b..6792586 100644
--- a/data/city.ui
+++ b/data/city.ui
@@ -1,44 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
-<!-- Generated with glade 3.16.1 -->
<interface>
- <requires lib="gtk+" version="3.0"/>
- <template class="Gjs_WeatherView" parent="GtkStack">
- <property name="transition_type">crossfade</property>
- <child internal-child="accessible">
- <object class="AtkObject" id="weather-view-accessible">
- <property name="accessible-name" translatable="yes">City view</property>
- </object>
- </child>
+ <requires lib="gtk" version="4.0" />
+ <template class="Gjs_WeatherView">
<child>
- <object class="GtkBox" id="loading-box">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="valign">center</property>
- <property name="orientation">vertical</property>
- <child>
- <object class="GtkSpinner" id="spinner">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="height_request">128</property>
- <property name="width_request">128</property>
- <property name="margin_bottom">18</property>
- <property name="active">True</property>
- </object>
- </child>
+ <object class="GtkStack" id="stack">
<child>
- <object class="GtkLabel">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="label" translatable="yes">Loading…</property>
- <style>
- <class name="large-title"/>
- </style>
+ <object class="GtkStackPage">
+ <property name="name">loading</property>
+ <property name="child">
+ <object class="GtkBox" id="loading-grid">
+ <property name="orientation">vertical</property>
+ <property name="spacing">20</property>
+ <property name="halign">center</property>
+ <property name="valign">center</property>
+ <child>
+ <object class="GtkSpinner" id="spinner">
+ <property name="height_request">64</property>
+ <property name="width_request">64</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="name">loadingLabel</property>
+ <property name="label" translatable="yes">Loading…</property>
+ </object>
+ </child>
+ </object>
+ </property>
</object>
</child>
</object>
- <packing>
- <property name="name">loading</property>
- </packing>
</child>
</template>
-</interface>
+</interface>
\ No newline at end of file
diff --git a/data/day-entry.ui b/data/day-entry.ui
index d6648be..b20a560 100644
--- a/data/day-entry.ui
+++ b/data/day-entry.ui
@@ -1,390 +1,313 @@
<?xml version="1.0" encoding="UTF-8"?>
-<!-- Generated with glade 3.22.0 -->
<interface>
- <requires lib="gtk+" version="3.20"/>
- <object class="GtkPopoverMenu" id="more_menu">
- <property name="can_focus">False</property>
+ <requires lib="gtk" version="4.0"/>
+ <object class="GtkPopover" id="more_menu">
<style>
<class name="day-popover"/>
</style>
- <child>
+ <property name="child">
<object class="GtkGrid">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
<property name="row_spacing">8</property>
<property name="column_spacing">16</property>
- <property name="row_homogeneous">True</property>
+
<property name="margin_top">16</property>
<property name="margin_bottom">16</property>
<property name="margin_start">16</property>
<property name="margin_end">16</property>
<child>
<object class="GtkLabel">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
<property name="halign">end</property>
<property name="label" translatable="yes">Night</property>
+ <layout>
+ <property name="column">0</property>
+ <property name="row">1</property>
+ </layout>
<style>
<class name="small-label"/>
</style>
</object>
- <packing>
- <property name="left_attach">0</property>
- <property name="top_attach">1</property>
- </packing>
</child>
<child>
<object class="GtkLabel">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
<property name="halign">end</property>
<property name="label" translatable="yes">Morning</property>
+ <layout>
+ <property name="column">0</property>
+ <property name="row">2</property>
+ </layout>
<style>
<class name="small-label"/>
</style>
</object>
- <packing>
- <property name="left_attach">0</property>
- <property name="top_attach">2</property>
- </packing>
</child>
<child>
<object class="GtkLabel">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
<property name="halign">end</property>
<property name="label" translatable="yes">Afternoon</property>
+ <layout>
+ <property name="column">0</property>
+ <property name="row">3</property>
+ </layout>
<style>
<class name="small-label"/>
</style>
</object>
- <packing>
- <property name="left_attach">0</property>
- <property name="top_attach">3</property>
- </packing>
</child>
<child>
<object class="GtkLabel">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
<property name="halign">end</property>
<property name="label" translatable="yes">Evening</property>
+ <layout>
+ <property name="column">0</property>
+ <property name="row">4</property>
+ </layout>
<style>
<class name="small-label"/>
</style>
</object>
- <packing>
- <property name="left_attach">0</property>
- <property name="top_attach">4</property>
- </packing>
</child>
<child>
<object class="GtkImage" id="nightImage">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
<property name="icon_name">weather-showers-symbolic</property>
+ <layout>
+ <property name="column">1</property>
+ <property name="row">1</property>
+ </layout>
</object>
- <packing>
- <property name="left_attach">1</property>
- <property name="top_attach">1</property>
- </packing>
</child>
<child>
<object class="GtkImage" id="morningImage">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
<property name="icon_name">weather-showers-symbolic</property>
+ <layout>
+ <property name="column">1</property>
+ <property name="row">2</property>
+ </layout>
</object>
- <packing>
- <property name="left_attach">1</property>
- <property name="top_attach">2</property>
- </packing>
</child>
<child>
<object class="GtkImage" id="afternoonImage">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
<property name="icon_name">weather-showers-symbolic</property>
+ <layout>
+ <property name="column">1</property>
+ <property name="row">3</property>
+ </layout>
</object>
- <packing>
- <property name="left_attach">1</property>
- <property name="top_attach">3</property>
- </packing>
</child>
<child>
<object class="GtkImage" id="eveningImage">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
<property name="icon_name">weather-showers-symbolic</property>
+ <layout>
+ <property name="column">1</property>
+ <property name="row">4</property>
+ </layout>
</object>
- <packing>
- <property name="left_attach">1</property>
- <property name="top_attach">4</property>
- </packing>
</child>
<child>
<object class="GtkGrid">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
<property name="vexpand">True</property>
<property name="row_spacing">8</property>
- <property name="row_homogeneous">True</property>
- <property name="column_homogeneous">True</property>
+ <property name="row_homogeneous">1</property>
+ <property name="column_homogeneous">1</property>
<child>
<object class="GtkLabel" id="nightTemperatureLabel">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
<property name="label">16°</property>
+ <layout>
+ <property name="column">0</property>
+ <property name="row">1</property>
+ </layout>
</object>
- <packing>
- <property name="left_attach">0</property>
- <property name="top_attach">1</property>
- </packing>
</child>
<child>
<object class="GtkLabel" id="morningTemperatureLabel">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
<property name="label">16°</property>
+ <layout>
+ <property name="column">0</property>
+ <property name="row">2</property>
+ </layout>
</object>
- <packing>
- <property name="left_attach">0</property>
- <property name="top_attach">2</property>
- </packing>
</child>
<child>
<object class="GtkLabel" id="afternoonTemperatureLabel">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
<property name="label">16°</property>
+ <layout>
+ <property name="column">0</property>
+ <property name="row">3</property>
+ </layout>
</object>
- <packing>
- <property name="left_attach">0</property>
- <property name="top_attach">3</property>
- </packing>
</child>
<child>
<object class="GtkLabel" id="eveningTemperatureLabel">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
<property name="label">16°</property>
+ <layout>
+ <property name="column">0</property>
+ <property name="row">4</property>
+ </layout>
</object>
- <packing>
- <property name="left_attach">0</property>
- <property name="top_attach">4</property>
- </packing>
</child>
<child>
<object class="GtkImage">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
<property name="icon_name">temperature-symbolic</property>
+ <layout>
+ <property name="column">0</property>
+ <property name="row">0</property>
+ </layout>
</object>
- <packing>
- <property name="left_attach">0</property>
- <property name="top_attach">0</property>
- </packing>
</child>
<child>
<object class="GtkImage">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
<property name="icon_name">weather-showers-scattered-symbolic</property>
+ <layout>
+ <property name="column">1</property>
+ <property name="row">0</property>
+ </layout>
</object>
- <packing>
- <property name="left_attach">1</property>
- <property name="top_attach">0</property>
- </packing>
</child>
<child>
<object class="GtkImage">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
<property name="icon_name">weather-windy</property>
+ <layout>
+ <property name="column">2</property>
+ <property name="row">0</property>
+ </layout>
</object>
- <packing>
- <property name="left_attach">2</property>
- <property name="top_attach">0</property>
- </packing>
</child>
<child>
<object class="GtkLabel" id="nightHumidity">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
<property name="label">2.1 mm</property>
+ <layout>
+ <property name="column">1</property>
+ <property name="row">1</property>
+ </layout>
</object>
- <packing>
- <property name="left_attach">1</property>
- <property name="top_attach">1</property>
- </packing>
</child>
<child>
<object class="GtkLabel" id="morningHumidity">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
<property name="label">2.1 mm</property>
+ <layout>
+ <property name="column">1</property>
+ <property name="row">2</property>
+ </layout>
</object>
- <packing>
- <property name="left_attach">1</property>
- <property name="top_attach">2</property>
- </packing>
</child>
<child>
<object class="GtkLabel" id="afternoonHumidity">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
<property name="label">2.1 mm</property>
+ <layout>
+ <property name="column">1</property>
+ <property name="row">3</property>
+ </layout>
</object>
- <packing>
- <property name="left_attach">1</property>
- <property name="top_attach">3</property>
- </packing>
</child>
<child>
<object class="GtkLabel" id="eveningHumidity">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
<property name="label">2.1 mm</property>
+ <layout>
+ <property name="column">1</property>
+ <property name="row">4</property>
+ </layout>
</object>
- <packing>
- <property name="left_attach">1</property>
- <property name="top_attach">4</property>
- </packing>
</child>
<child>
<object class="GtkLabel" id="nightWind">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
<property name="label">5 m/s</property>
+ <layout>
+ <property name="column">2</property>
+ <property name="row">1</property>
+ </layout>
</object>
- <packing>
- <property name="left_attach">2</property>
- <property name="top_attach">1</property>
- </packing>
</child>
<child>
<object class="GtkLabel" id="morningWind">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
<property name="label">5 m/s</property>
+ <layout>
+ <property name="column">2</property>
+ <property name="row">2</property>
+ </layout>
</object>
- <packing>
- <property name="left_attach">2</property>
- <property name="top_attach">2</property>
- </packing>
</child>
<child>
<object class="GtkLabel" id="afternoonWind">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
<property name="label">5 m/s</property>
+ <layout>
+ <property name="column">2</property>
+ <property name="row">3</property>
+ </layout>
</object>
- <packing>
- <property name="left_attach">2</property>
- <property name="top_attach">3</property>
- </packing>
</child>
<child>
<object class="GtkLabel" id="eveningWind">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
<property name="label">5 m/s</property>
+ <layout>
+ <property name="column">2</property>
+ <property name="row">4</property>
+ </layout>
</object>
- <packing>
- <property name="left_attach">2</property>
- <property name="top_attach">4</property>
- </packing>
</child>
+ <layout>
+ <property name="column">2</property>
+ <property name="row">0</property>
+ <property name="row-span">5</property>
+ </layout>
</object>
- <packing>
- <property name="left_attach">2</property>
- <property name="top_attach">0</property>
- <property name="height">5</property>
- </packing>
- </child>
- <child>
- <placeholder/>
- </child>
- <child>
- <placeholder/>
</child>
</object>
- </child>
+ </property>
</object>
- <template class="Gjs_DayEntry" parent="GtkBox">
+ <template class="Gjs_DayEntry">
<property name="width_request">100</property>
<property name="height_request">200</property>
- <property name="visible">True</property>
- <property name="can_focus">False</property>
<property name="margin_top">18</property>
<property name="margin_bottom">18</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
- <property name="orientation">vertical</property>
- <property name="spacing">18</property>
<child>
- <object class="GtkBox">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="orientation">vertical</property>
- <property name="spacing">6</property>
- <child>
- <object class="GtkLabel" id="nameLabel">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="label">Tues</property>
- </object>
- </child>
- <child>
- <object class="GtkLabel" id="dateLabel">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="label">7 June</property>
- <style>
- <class name="small-label"/>
- </style>
- </object>
- </child>
+ <object class="GtkLabel" id="nameLabel">
+ <property name="margin_top">8</property>
+ <property name="label">Tues</property>
+ <style>
+ <class name="day-label"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="dateLabel">
+ <property name="margin_top">8</property>
+ <property name="label">7 June</property>
+ <style>
+ <class name="date-label"/>
+ </style>
</object>
</child>
<child>
<object class="GtkImage" id="image">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
<property name="valign">start</property>
<property name="pixel_size">32</property>
<property name="icon_name">weather-showers-symbolic</property>
+ <style>
+ <class name="forecast-graphic"/>
+ </style>
</object>
</child>
<child>
<object class="Gjs_Thermometer" id="thermometer">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
<property name="vexpand">True</property>
<property name="hexpand">True</property>
</object>
</child>
<child>
<object class="GtkMenuButton">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="receives_default">False</property>
<property name="halign">center</property>
+ <property name="icon_name">view-more-symbolic</property>
<property name="valign">center</property>
<property name="popover">more_menu</property>
- <child>
- <object class="GtkImage">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="icon_name">view-more-symbolic</property>
- </object>
- </child>
<style>
+ <class name="forecast-button"/>
<class name="image-button"/>
<class name="circular"/>
<class name="flat"/>
</style>
</object>
</child>
+
</template>
</interface>
diff --git a/data/hour-entry.ui b/data/hour-entry.ui
index 0c27049..ba16181 100644
--- a/data/hour-entry.ui
+++ b/data/hour-entry.ui
@@ -1,40 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
-<!-- Generated with glade 3.22.0 -->
<interface>
- <requires lib="gtk+" version="3.20"/>
- <template class="Gjs_HourEntry" parent="GtkBox">
+ <requires lib="gtk" version="4.0"/>
+ <template class="Gjs_HourEntry">
<property name="width_request">75</property>
<property name="height_request">200</property>
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="hexpand">True</property>
- <property name="vexpand">True</property>
- <property name="orientation">vertical</property>
- <property name="spacing">18</property>
+ <property name="hexpand">1</property>
+ <property name="vexpand">1</property>
+
<property name="margin_top">18</property>
<property name="margin_bottom">18</property>
+ <layout>
+ <property name="orientation">vertical</property>
+ <property name="spacing">18</property>
+ </layout>
<child>
<object class="GtkLabel" id="timeLabel">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
<property name="label">Now</property>
</object>
</child>
<child>
<object class="GtkImage" id="image">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="valign">start</property>
- <property name="vexpand">True</property>
+ <property name="valign">1</property>
+ <property name="vexpand">1</property>
<property name="pixel_size">32</property>
<property name="icon_name">weather-showers-symbolic</property>
+ <style>
+ <class name="forecast-graphic"/>
+ </style>
</object>
</child>
<child>
<object class="GtkLabel" id="temperatureLabel">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="valign">end</property>
+
+ <property name="valign">2</property>
<property name="label">13°</property>
<style>
<class name="forecast-temperature-label"/>
diff --git a/data/icons/hicolor/scalable/status/weather-hourly-symbolic.svg
b/data/icons/hicolor/scalable/status/weather-hourly-symbolic.svg
new file mode 100644
index 0000000..ef275dc
--- /dev/null
+++ b/data/icons/hicolor/scalable/status/weather-hourly-symbolic.svg
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
+ <g fill="#2e3436">
+ <path d="m 8 0 c -4.40625 0 -8 3.59375 -8 8 s 3.59375 8 8 8 s 8 -3.59375 8 -8 s -3.59375 -8 -8 -8 z
m 0 2 c 3.324219 0 6 2.671875 6 6 c 0 3.324219 -2.675781 6 -6 6 s -6 -2.675781 -6 -6 c 0 -3.328125 2.675781
-6 6 -6 z m 0 0"/>
+ <path d="m 4.929688 4.953125 c -0.132813 0.003906 -0.257813 0.058594 -0.351563 0.152344 c -0.191406
0.195312 -0.1875 0.511719 0.007813 0.707031 l 3.113281 3.042969 c 0.105469 0.097656 0.246093 0.144531
0.386719 0.128906 h 2.914062 c 0.277344 0 0.5 -0.222656 0.5 -0.5 c 0 -0.273437 -0.222656 -0.5 -0.5 -0.5 h
-2.761719 l -2.953125 -2.886719 c -0.09375 -0.09375 -0.222656 -0.144531 -0.355468 -0.144531 z m 0 0"/>
+ </g>
+</svg>
\ No newline at end of file
diff --git a/data/icons/meson.build b/data/icons/meson.build
index 22e3cc6..1fd6b0d 100644
--- a/data/icons/meson.build
+++ b/data/icons/meson.build
@@ -1,8 +1,6 @@
-if profile == 'Devel'
- icon = '@0@.svg'.format(weather_id)
-else
- icon = '@0@.svg'.format(default_id)
-endif
+
+icon = '@0@.svg'.format(default_id)
+
scalable_icondir = join_paths('hicolor', 'scalable', 'apps')
install_data (
diff --git a/data/org.gnome.Weather.data.gresource.xml b/data/org.gnome.Weather.data.gresource.xml
index eb357d3..2f525ee 100644
--- a/data/org.gnome.Weather.data.gresource.xml
+++ b/data/org.gnome.Weather.data.gresource.xml
@@ -3,12 +3,12 @@
<gresource prefix="/org/gnome/Weather">
<file preprocess="xml-stripblanks">city.ui</file>
<file preprocess="xml-stripblanks">places-popover.ui</file>
- <file preprocess="xml-stripblanks">primary-menu.ui</file>
<file preprocess="xml-stripblanks">weather-widget.ui</file>
<file preprocess="xml-stripblanks">window.ui</file>
<file preprocess="xml-stripblanks">hour-entry.ui</file>
<file preprocess="xml-stripblanks">day-entry.ui</file>
- <file>application.css</file>
+ <file>style.css</file>
+ <file>style-dark.css</file>
</gresource>
<gresource prefix="/org/gnome/shell">
<file>ShellWeatherIntegration.xml</file>
diff --git a/data/org.gnome.Weather.gschema.xml b/data/org.gnome.Weather.gschema.xml
index f3c98f7..17b7d2d 100644
--- a/data/org.gnome.Weather.gschema.xml
+++ b/data/org.gnome.Weather.gschema.xml
@@ -9,13 +9,5 @@
GVariant returned by gweather_location_serialize().
</description>
</key>
- <key name="automatic-location" type="b">
- <default>true</default>
- <summary>Automatic location</summary>
- <description>
- The automatic location is the value of automatic-location switch which decides whether
- to fetch current location or not.
- </description>
- </key>
</schema>
</schemalist>
diff --git a/data/org.gnome.Weather.search-provider.ini.in b/data/org.gnome.Weather.search-provider.ini.in
index 0e57c24..0b1b678 100644
--- a/data/org.gnome.Weather.search-provider.ini.in
+++ b/data/org.gnome.Weather.search-provider.ini.in
@@ -1,6 +1,6 @@
[Shell Search Provider]
DesktopId=@APP_ID@.desktop
BusName=@APP_ID@.BackgroundService
-ObjectPath=/org/gnome/Weather@PROFILE@/BackgroundService
+ObjectPath=/org/gnome/Weather/BackgroundService
Version=2
DefaultDisabled=true
diff --git a/data/places-popover.ui b/data/places-popover.ui
index c96d16f..4e52b8e 100644
--- a/data/places-popover.ui
+++ b/data/places-popover.ui
@@ -1,228 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
-<!-- Generated with glade 3.16.1 -->
<interface>
- <requires lib="gtk+" version="3.0"/>
- <object class="GtkGrid" id="popover-grid">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
+ <requires lib="gtk" version="4.0" />
+
+ <object class="GtkBox" id="popoverBox">
+
<property name="orientation">vertical</property>
- <property name="row_spacing">10</property>
- <property name="margin">12</property>
- <property name="vexpand">False</property>
+ <property name="width-request">320</property>
+ <property name="height-request">300</property>
<child>
- <object class="GWeatherLocationEntry" id="location-entry">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="width-request">300</property>
- <property name="activates_default">True</property>
+ <object class="Gjs_LocationSearchEntry" id="location-entry">
+ <property name="name">locationEntry</property>
+ <property name="hexpand">True</property>
+
+ <property name="placeholder-text" translatable="yes">Search for a city</property>
</object>
- <packing>
- <property name="left_attach">0</property>
- <property name="top_attach">0</property>
- <property name="width">1</property>
- <property name="height">1</property>
- </packing>
- </child>
- <child>
- <object class="GtkStack" id="auto-location-stack">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="vexpand">False</property>
- <property name="homogeneous">False</property>
- <property name="transition_type">crossfade</property>
- <child>
- <object class="GtkGrid" id="auto-location-grid">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="orientation">horizontal</property>
- <property name="column_homogeneous">True</property>
- <property name="margin_top">6</property>
- <property name="margin_bottom">6</property>
- <property name="vexpand">False</property>
- <child>
- <object class="GtkLabel" id="auto-location-label">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="label" translatable="yes">Automatic Location</property>
- <property name="halign">start</property>
- <property name="vexpand">False</property>
- <attributes>
- <attribute name="weight" value="PANGO_WEIGHT_BOLD"/>
- </attributes>
- <style>
- <class name="dim-label"/>
- </style>
- </object>
- <packing>
- <property name="left_attach">0</property>
- <property name="top_attach">0</property>
- <property name="width">1</property>
- <property name="height">1</property>
- </packing>
- </child>
- <child>
- <object class="GtkSwitch" id="auto-location-switch">
- <property name="visible">True</property>
- <property name="halign">end</property>
- <property name="vexpand">False</property>
- </object>
- <packing>
- <property name="left_attach">1</property>
- <property name="top_attach">0</property>
- <property name="width">1</property>
- <property name="height">1</property>
- </packing>
- </child>
- </object>
- <packing>
- <property name="name">auto-location-switch-grid</property>
- </packing>
- </child>
- <child>
- <object class="GtkLabel" id="locating-label">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="label" translatable="yes">Locating…</property>
- <property name="halign">center</property>
- <property name="valign">center</property>
- <property name="vexpand">False</property>
- </object>
- <packing>
- <property name="name">locating-label</property>
- </packing>
- </child>
- </object>
- <packing>
- <property name="left_attach">0</property>
- <property name="top_attach">1</property>
- <property name="width">1</property>
- <property name="height">1</property>
- </packing>
</child>
<child>
<object class="GtkStack" id="popover-stack">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="vexpand">False</property>
- <property name="homogeneous">False</property>
+ <property name="hexpand">True</property>
+ <property name="vexpand">True</property>
+ <property name="width-request">300</property>
<child>
- <object class="GtkGrid" id="search-grid">
- <property name="visible">True</property>
- <property name="name">search-city-grid</property>
- <property name="can_focus">False</property>
- <property name="orientation">vertical</property>
- <property name="margin_top">25</property>
- <property name="margin_bottom">25</property>
- <property name="halign">center</property>
- <property name="valign">center</property>
- <property name="row_homogeneous">True</property>
- <property name="vexpand">False</property>
+ <object class="GtkScrolledWindow" id="search-list-scroll-window">
+ <property name="hscrollbar-policy">never</property>
+ <property name="vexpand">True</property>
<child>
- <object class="GtkImage" id="search-image">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="icon_name">edit-find-symbolic</property>
- <property name="icon_size">6</property>
- <property name="use_fallback">True</property>
- <property name="halign">center</property>
- <property name="valign">center</property>
- <property name="vexpand">False</property>
+ <object class="GtkListView" id="search-list-view">
+ <property name="name">search-list-view</property>
+ <property name="hscroll-policy">minimum</property>
</object>
- <packing>
- <property name="left_attach">0</property>
- <property name="top_attach">0</property>
- <property name="width">1</property>
- <property name="height">1</property>
- </packing>
- </child>
- <child>
- <object class="GtkLabel" id="search-label">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="label" translatable="yes">Search for a city</property>
- <property name="halign">center</property>
- <property name="valign">center</property>
- <property name="vexpand">False</property>
- <style>
- <class name="dim-label"/>
- </style>
- </object>
- <packing>
- <property name="left_attach">0</property>
- <property name="top_attach">1</property>
- <property name="width">1</property>
- <property name="height">1</property>
- </packing>
</child>
</object>
- <packing>
- <property name="name">search-grid</property>
- </packing>
</child>
<child>
- <object class="GtkGrid" id="locations-grid">
- <property name="visible">True</property>
- <property name="name">locations-grid</property>
- <property name="can_focus">False</property>
- <property name="orientation">vertical</property>
- <property name="row_spacing">10</property>
- <property name="vexpand">False</property>
- <child>
- <object class="GtkLabel" id="recently-viewed-label">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="label" translatable="yes">Viewed Recently</property>
- <property name="halign">start</property>
- <property name="vexpand">False</property>
- <attributes>
- <attribute name="weight" value="PANGO_WEIGHT_BOLD"/>
- </attributes>
- <style>
- <class name="dim-label"/>
- </style>
- </object>
- <packing>
- <property name="left_attach">0</property>
- <property name="top_attach">0</property>
- <property name="width">1</property>
- <property name="height">1</property>
- </packing>
- </child>
+ <object class="GtkScrolledWindow" id="locations-list-scroll-window">
+ <property name="hscrollbar-policy">never</property>
<child>
- <object class="GtkFrame" id="locations-frame">
- <property name="name">locations-frame</property>
- <property name="visible">True</property>
- <property name="can-focus">False</property>
+ <object class="GtkViewport">
<child>
<object class="GtkListBox" id="locations-list-box">
<property name="name">locations-list-box</property>
- <property name="visible">True</property>
- <property name="can-focus">False</property>
<property name="hexpand">True</property>
- <property name="vexpand">False</property>
<property name="selection-mode">none</property>
+ <property name="show-separators">False</property>
+ <child type="placeholder">
+ <object class="GtkGrid" id="empty-search-grid">
+ <property name="name">search-city-grid</property>
+ <property name="orientation">vertical</property>
+ <property name="margin_top">25</property>
+ <property name="margin_bottom">25</property>
+ <property name="halign">center</property>
+ <property name="valign">center</property>
+ <property name="row_homogeneous">1</property>
+ <child>
+ <object class="GtkImage" id="search-image">
+ <property name="icon_name">edit-find-symbolic</property>
+ <property name="icon_size">2</property>
+ <property name="use_fallback">1</property>
+ <property name="halign">center</property>
+ <property name="valign">center</property>
+ <layout>
+ <property name="column">0</property>
+ <property name="row">0</property>
+ </layout>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="search-label">
+ <property name="label" translatable="yes">Search for a city</property>
+ <property name="halign">center</property>
+ <property name="valign">center</property>
+ <style>
+ <class name="dim-label" />
+ </style>
+ <layout>
+ <property name="column">0</property>
+ <property name="row">1</property>
+ </layout>
+ </object>
+ </child>
+ </object>
+ </child>
</object>
</child>
</object>
- <packing>
- <property name="left_attach">0</property>
- <property name="top_attach">1</property>
- <property name="width">1</property>
- <property name="height">1</property>
- </packing>
</child>
</object>
- <packing>
- <property name="name">locations-grid</property>
- </packing>
</child>
</object>
- <packing>
- <property name="left-attach">0</property>
- <property name="top-attach">2</property>
- <property name="width">1</property>
- <property name="height">1</property>
- </packing>
</child>
</object>
-</interface>
+</interface>
\ No newline at end of file
diff --git a/data/primary-menu.ui b/data/primary-menu.ui
index c6d55f0..67f0b35 100644
--- a/data/primary-menu.ui
+++ b/data/primary-menu.ui
@@ -1,4 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
<interface>
+ <requires lib="gtk" version="4.0"/>
<menu id="primary-menu">
<submenu>
<attribute translatable="yes" name="label">_Temperature Unit</attribute>
diff --git a/data/style-dark.css b/data/style-dark.css
new file mode 100644
index 0000000..b419d1e
--- /dev/null
+++ b/data/style-dark.css
@@ -0,0 +1,4 @@
+@define-color weather_temp_chart_fill_color rgba(248, 228, 92, 0.15);
+@define-color weather_thermometer_high_color @yellow_1;
+@define-color weather_thermometer_low_color @blue_1;
+@define-color weather_forecast_color @light_1;
diff --git a/data/style.css b/data/style.css
new file mode 100644
index 0000000..6ad5eae
--- /dev/null
+++ b/data/style.css
@@ -0,0 +1,165 @@
+@define-color weather_temp_chart_fill_color rgba(248, 228, 92, 0.5);
+@define-color weather_temp_chart_stroke_color rgba(246, 211, 45, 1.0);
+
+@define-color weather_thermometer_warm_color rgb(245, 194, 17);
+@define-color weather_thermometer_cold_color rgb(28, 113, 216);
+
+@define-color weather_thermometer_high_color #c89009;
+@define-color weather_thermometer_low_color #2174d9;
+@define-color weather_forecast_color #c89009;
+
+#places-label {
+ font-weight: bold;
+}
+
+#temperature-label {
+ font-size: 32pt;
+ font-weight: 900;
+ margin-left: 16px;
+}
+
+#loadingLabel {
+ font-size: 16pt;
+}
+
+#apparent-label {
+ font-size: 9pt;
+}
+
+#weather-page-placeholder-title {
+ font-weight: bold;
+ font-size: 1.2em;
+}
+
+#loading-label {
+ padding-top: 24px;
+ font-size: 1.5em;
+}
+
+#attribution-label {
+ font-size: small;
+}
+
+#conditions-grid *:backdrop {
+ color: @theme_fg_color;
+}
+
+.content-view.cell {
+ font-weight: bold;
+}
+
+#locationEntry {
+ margin: 10px;
+}
+
+.weather-popover {
+ margin-top: 10px;
+}
+
+.weather-popover contents {
+ padding: 0;
+}
+
+WeatherLocationRow {
+ padding: 10px;
+}
+
+WeatherLocationRow #label {
+ margin-bottom: 10px;
+}
+
+#currentIcon {
+ padding: 10px;
+}
+
+#locationIcon {
+ padding: 10px;
+}
+
+.forecast-card {
+ transition: border-radius 100ms ease-out;
+ border-radius: 6px;
+}
+
+#conditions-grid,
+#attributionGrid {
+ margin-left: 18px;
+ margin-right: 18px;
+}
+
+#weather-page .small .forecast-card {
+ margin-left: 0;
+ margin-right: 0;
+ border-radius: 0;
+}
+
+.forecast-temperature-label {
+ font-weight: bold;
+ font-size: 12pt;
+ color: @weather_forecast_color;
+}
+
+WeatherThermometer > label.high {
+ font-weight: bold;
+ font-size: 13pt;
+ color: @weather_thermometer_high_color;
+}
+
+WeatherThermometer > label.low {
+ font-weight: bold;
+ font-size: 13pt;
+ color: @weather_thermometer_low_color;
+}
+
+.day-label {
+ font-size: 13pt;
+}
+
+.date-label {
+ font-size: 9pt;
+}
+
+.forecast-button {
+ margin: 10px;
+}
+
+.forecast-graphic {
+ margin: 20px;
+}
+
+#updated-time-label {
+ font-size: 9pt;
+}
+
+#attribution-label {
+ font-size: 9pt;
+ color: rgba(154, 153, 150, 1);
+}
+
+viewswitchertitle viewswitcher {
+ margin-left: 100px;
+ margin-right: 100px;
+}
+
+button.osd.circular {
+ border-radius: 9999px;
+ min-width: 24px;
+ min-height: 24px;
+}
+
+button.osd.circular > image {
+ padding: 12px;
+}
+
+.small-label {
+ font-size: 9pt;
+}
+
+.search-view scrolledwindow {
+ background-color: @accent_bg_color;
+ color: @accent_fg_color;
+}
+
+.search-view .large-title {
+ font-weight: bold;
+}
diff --git a/data/weather-widget.ui b/data/weather-widget.ui
index a2d7753..ef1afe6 100644
--- a/data/weather-widget.ui
+++ b/data/weather-widget.ui
@@ -1,306 +1,235 @@
<?xml version="1.0" encoding="UTF-8"?>
-<!-- Generated with glade 3.22.0 -->
<interface>
- <requires lib="gtk+" version="3.0"/>
- <template class="Gjs_WeatherWidget" parent="GtkFrame">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="label_xalign">0</property>
+ <requires lib="gtk" version="4.0"/>
+ <template class="Gjs_WeatherWidget">
<child>
- <object class="GtkFrame" id="contentFrame">
- <property name="name">weather-page-content-view</property>
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="label_xalign">0</property>
- <property name="shadow_type">none</property>
+ <object class="GtkBox" id="outerBox">
+ <property name="orientation">vertical</property>
+ <property name="margin-start">0</property>
+ <property name="margin-end">0</property>
+ <property name="margin-top">18</property>
+ <property name="margin-bottom">18</property>
+ <property name="spacing">20</property>
<child>
- <object class="HdyClamp">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="maximum_size">1010</property>
- <property name="tightening_threshold">600</property>
+ <object class="GtkGrid">
+ <property name="name">conditions-grid</property>
+ <property name="column_spacing">10</property>
<child>
- <object class="GtkBox" id="outerBox">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="orientation">vertical</property>
- <property name="margin">18</property>
- <property name="spacing">18</property>
+ <object class="GtkImage" id="conditionsImage">
+ <property name="halign">start</property>
+ <property name="valign">center</property>
+ <property name="pixel_size">84</property>
+ <style>
+ <class name="icon-dropshadow"/>
+ </style>
+ <layout>
+ <property name="column">0</property>
+ <property name="row">0</property>
+ <property name="row-span">2</property>
+ </layout>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuButton" id="placesButton">
+ <property name="receives_default">1</property>
+ <property name="halign">start</property>
+ <property name="valign">start</property>
<child>
- <object class="GtkGrid" id="inner-grid">
- <property name="name">conditions-grid</property>
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="column_spacing">10</property>
+ <object class="GtkBox" id="placesBox">
+ <property name="spacing">12</property>
<child>
- <object class="GtkImage" id="conditionsImage">
- <property name="name">conditions-image</property>
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="halign">start</property>
- <property name="valign">center</property>
- <property name="pixel_size">84</property>
- <style>
- <class name="icon-dropshadow"/>
- </style>
- </object>
- <packing>
- <property name="left_attach">0</property>
- <property name="top_attach">0</property>
- <property name="height">2</property>
- </packing>
- </child>
- <child>
- <object class="GtkMenuButton" id="placesButton">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="focus_on_click">False</property>
- <property name="receives_default">True</property>
- <property name="halign">start</property>
- <property name="valign">start</property>
- <child>
- <object class="GtkBox" id="placesBox">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="spacing">12</property>
- <child>
- <object class="GtkLabel" id="placesLabel">
- <property name="name">places-label</property>
- <property name="wrap">True</property>
- <property name="wrap-mode">word-char</property>
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="label" translatable="yes">Places</property>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkImage" id="placesImage">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="icon_name">pan-down-symbolic</property>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">1</property>
- </packing>
- </child>
- </object>
- </child>
- <style>
- <class name="text-button"/>
- <class name="flat"/>
- </style>
+ <object class="GtkLabel" id="placesLabel">
+ <property name="name">places-label</property>
+ <property name="wrap">1</property>
+ <property name="wrap-mode">word-char</property>
+ <property name="label" translatable="yes">Places</property>
</object>
- <packing>
- <property name="left_attach">1</property>
- <property name="top_attach">0</property>
- </packing>
</child>
<child>
- <object class="GtkBox" id="temperatureBox">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="halign">start</property>
- <property name="valign">start</property>
- <property name="spacing">8</property>
- <property name="baseline_position">bottom</property>
- <child>
- <object class="GtkLabel" id="temperatureLabel">
- <property name="name">temperature-label</property>
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="halign">start</property>
- <property name="valign">baseline</property>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkLabel" id="apparentLabel">
- <property name="name">apparent-label</property>
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="halign">start</property>
- <property name="valign">baseline</property>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">1</property>
- </packing>
- </child>
- </object>
- <packing>
- <property name="left_attach">1</property>
- <property name="top_attach">1</property>
- </packing>
- </child>
- <child internal-child="accessible">
- <object class="AtkObject" id="inner-grid-atkobject">
- <property name="AtkObject::accessible-name" translatable="yes">Current
conditions</property>
+ <object class="GtkImage">
+ <property name="icon_name">pan-down-symbolic</property>
</object>
</child>
</object>
</child>
+ <style>
+ <class name="text-button"/>
+ <class name="flat"/>
+ </style>
+ <layout>
+ <property name="column">1</property>
+ <property name="row">0</property>
+ </layout>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox" id="temperatureBox">
+ <property name="halign">start</property>
+ <property name="valign">start</property>
+ <property name="spacing">8</property> -->
+ <property name="baseline_position">bottom</property>
<child>
- <object class="GtkOverlay" id="forecast-overlay">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <child>
- <object class="GtkFrame" id="forecastFrame">
- <property name="name">forecast-frame</property>
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="shadow-type">GTK_SHADOW_IN</property>
- <child>
- <object class="GtkStack" id="forecastStack">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="transition_type">crossfade</property>
- <child>
- <object class="GtkScrolledWindow" id="forecast-hourly">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="vscrollbar_policy">never</property>
- <property name="min_content_width">308</property>
- <child>
- <object class="GtkViewport" id="forecast-hourly-viewport">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="hscroll_policy">natural</property>
- <property name="vscroll_policy">natural</property>
- </object>
- </child>
- </object>
- <packing>
- <property name="name">hourly</property>
- <property name="title" translatable="yes">Hourly</property>
- <property name="icon-name">preferences-system-time-symbolic</property>
- </packing>
- </child>
+ <object class="GtkLabel" id="temperatureLabel">
+ <property name="name">temperature-label</property>
+ <property name="halign">start</property>
+ <property name="valign">baseline</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="apparentLabel">
+ <property name="name">apparent-label</property>
+ <property name="halign">start</property>
+ <property name="valign">baseline</property>
+ </object>
+ </child>
+ <layout>
+ <property name="column">1</property>
+ <property name="row">1</property>
+ </layout>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkOverlay">
+ <property name="child">
+ <object class="AdwViewStack" id="forecastStack">
+ <child>
+ <object class="AdwViewStackPage">
+ <property name="name">hourly</property>
+ <property name="title" translatable="yes">Hourly</property>
+ <property name="icon-name">weather-hourly-symbolic</property>
+ <property name="child">
+ <object class="GtkScrolledWindow" id="forecastHourlyScrollWindow">
+ <style>
+ <class name="forecast-card"/>
+ <class name="card"/>
+ </style>
+ <property name="vscrollbar_policy">never</property>
+ <property name="hscrollbar_policy">external</property>
+ <property name="overflow">hidden</property>
+ <property name="hadjustment">
+ <object class="GtkAdjustment" id="forecastHourlyAdjustment" />
+ </property>
+ <property name="min_content_width">308</property>
+ <property name="child">
+ <object class="GtkViewport">
+ <property name="hscroll_policy">natural</property>
+ <property name="vscroll_policy">natural</property>
<child>
- <object class="GtkScrolledWindow" id="forecast-daily">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="vscrollbar_policy">never</property>
- <property name="min_content_width">308</property>
- <child>
- <object class="GtkViewport" id="forecast-daily-viewport">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="hscroll_policy">natural</property>
- <property name="vscroll_policy">natural</property>
- </object>
- </child>
- </object>
- <packing>
- <property name="name">daily</property>
- <property name="title" translatable="yes">Daily</property>
- <property name="icon-name">x-office-calendar-symbolic</property>
- </packing>
+ <object class="Gjs_HourlyForecastBox" id="forecastHourly" />
</child>
</object>
- </child>
+ </property>
</object>
- <packing>
- <property name="index">-1</property>
- </packing>
- </child>
- <child type="overlay">
- <object class="GtkButton" id="rightButton">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="receives_default">True</property>
- <property name="halign">end</property>
- <property name="valign">center</property>
- <property name="margin_end">28</property>
- <child>
- <object class="GtkImage" id="right-image">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="icon_name">go-next-symbolic</property>
- </object>
- </child>
+ </property>
+ </object>
+ </child>
+ <child>
+ <object class="AdwViewStackPage">
+ <property name="name">daily</property>
+ <property name="title" translatable="yes">Daily</property>
+ <property name="icon-name">x-office-calendar-symbolic</property>
+ <property name="child">
+ <object class="GtkScrolledWindow" id="forecastDailyScrollWindow">
<style>
- <class name="osd"/>
- <class name="circular"/>
+ <class name="forecast-card"/>
+ <class name="card"/>
</style>
- </object>
- </child>
- <child type="overlay">
- <object class="GtkButton" id="leftButton">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="receives_default">True</property>
- <property name="halign">start</property>
- <property name="valign">center</property>
- <property name="margin_start">28</property>
- <child>
- <object class="GtkImage" id="left-image">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="icon_name">go-previous-symbolic</property>
- <property name="icon_size">1</property>
+ <property name="vscrollbar_policy">never</property>
+ <property name="hscrollbar_policy">external</property>
+ <property name="overflow">hidden</property>
+ <property name="hadjustment">
+ <object class="GtkAdjustment" id="forecastDailyAdjustment" />
+ </property>
+ <property name="min_content_width">308</property>
+ <property name="child">
+ <object class="GtkViewport">
+ <property name="hscroll_policy">natural</property>
+ <property name="vscroll_policy">natural</property>
+ <child>
+ <object class="Gjs_DailyForecastBox" id="forecastDaily"/>
+ </child>
</object>
- </child>
- <style>
- <class name="osd"/>
- <class name="circular"/>
- </style>
+ </property>
</object>
- <packing>
- <property name="index">1</property>
- </packing>
- </child>
+ </property>
</object>
</child>
+ </object>
+ </property>
+ <child type="overlay">
+ <object class="GtkButton" id="rightButton">
+ <property name="receives_default">1</property>
+ <property name="halign">end</property>
+ <property name="valign">center</property>
+ <property name="margin_end">28</property>
<child>
- <object class="GtkGrid">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="row_spacing">8</property>
- <child>
- <object class="GtkLabel" id="updatedTimeLabel">
- <property name="name">updated-time-label</property>
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="halign">start</property>
- </object>
- <packing>
- <property name="left_attach">0</property>
- <property name="top_attach">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkLabel" id="attributionLabel">
- <property name="name">attribution-label</property>
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="use_markup">True</property>
- <property name="wrap">True</property>
- <property name="track_visited_links">False</property>
- <property name="xalign">0</property>
- </object>
- <packing>
- <property name="left_attach">0</property>
- <property name="top_attach">1</property>
- </packing>
- </child>
+ <object class="GtkImage" id="right-image">
+ <property name="icon_name">go-next-symbolic</property>
</object>
</child>
+ <style>
+ <class name="osd"/>
+ <class name="circular"/>
+ </style>
+ </object>
+ </child>
+ <child type="overlay">
+ <object class="GtkButton" id="leftButton">
+ <property name="receives_default">1</property>
+ <property name="halign">start</property>
+ <property name="valign">center</property>
+ <property name="margin_start">28</property>
+ <child>
+ <object class="GtkImage" id="left-image">
+ <property name="icon_name">go-previous-symbolic</property>
+ <property name="icon_size">1</property>
+ </object>
+ </child>
+ <style>
+ <class name="osd"/>
+ <class name="circular"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkGrid">
+ <property name="name">attributionGrid</property>
+ <property name="row_spacing">8</property>
+ <child>
+ <object class="GtkLabel" id="updatedTimeLabel">
+ <property name="name">updated-time-label</property>
+ <property name="halign">start</property>
+ <layout>
+ <property name="column">0</property>
+ <property name="row">0</property>
+ </layout>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="attributionLabel">
+ <property name="name">attribution-label</property>
+ <property name="use_markup">1</property>
+ <property name="wrap">1</property>
+ <property name="xalign">0</property>
+ <!-- ellipsize this text so that on small layouts we
+ don't cause the bottom navigation to overflow -->
+ <property name="ellipsize">end</property>
+ <layout>
+ <property name="column">0</property>
+ <property name="row">1</property>
+ </layout>
</object>
</child>
</object>
</child>
+ <layout>
+ <property name="column">0</property>
+ <property name="row">2</property>
+ </layout>
</object>
</child>
</template>
diff --git a/data/window.ui b/data/window.ui
index b16a804..2573ccf 100644
--- a/data/window.ui
+++ b/data/window.ui
@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
-<!-- Generated with glade 3.22.0 -->
<interface>
- <requires lib="gtk+" version="3.10"/>
+ <requires lib="gtk" version="4.0" />
<menu id="primary-menu">
<submenu>
<attribute translatable="yes" name="label">_Temperature Unit</attribute>
@@ -23,136 +22,108 @@
</item>
</section>
</menu>
- <template class="Gjs_MainWindow" parent="HdyApplicationWindow">
- <property name="visible">True</property>
+ <template class="Gjs_MainWindow">
<property name="default_width">760</property>
<property name="default_height">520</property>
<child>
<object class="GtkBox">
- <property name="visible">True</property>
<property name="orientation">vertical</property>
<child>
- <object class="HdyHeaderBar" id="header">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="vexpand">False</property>
- <property name="show_close_button">True</property>
+ <object class="AdwHeaderBar" id="header">
<property name="centering_policy">strict</property>
- <child>
+ <child type="start">
<object class="GtkRevealer" id="refreshRevealer">
- <property name="visible">True</property>
<property name="transition_type">crossfade</property>
- <child>
+ <property name="child">
<object class="GtkButton" id="refresh">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="receives_default">False</property>
<property name="valign">center</property>
<property name="tooltip-text" translatable="yes">Refresh</property>
<property name="action_name">win.refresh</property>
- <child>
- <object class="GtkImage" id="refresh-button-image">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="icon_name">view-refresh-symbolic</property>
- </object>
- </child>
+ <property name="icon_name">view-refresh-symbolic</property>
</object>
- </child>
+ </property>
</object>
</child>
<child type="title">
<object class="GtkStack" id="titleStack">
- <property name="visible">True</property>
- <property name="visible-child-name" bind-source="stack" bind-property="visible-child-name"
bind-flags="bidirectional|sync-create"/>
+ <property name="visible-child-name" bind-source="stack" bind-property="visible-child-name"
bind-flags="bidirectional|sync-create" />
<property name="transition_type">crossfade</property>
<child>
- <object class="GtkLabel">
- <property name="visible">True</property>
- <property name="ellipsize">end</property>
- <property name="halign">center</property>
- <property name="wrap">False</property>
- <property name="single-line-mode">True</property>
- <property name="width-chars">5</property>
- <property name="label" translatable="yes">Select Location</property>
- <style>
- <class name="title"/>
- </style>
- </object>
- <packing>
+ <object class="GtkStackPage">
<property name="name">search</property>
- </packing>
+ <property name="child">
+ <object class="GtkLabel">
+ <property name="ellipsize">end</property>
+ <property name="halign">center</property>
+ <property name="single-line-mode">1</property>
+ <property name="width-chars">5</property>
+ <property name="label" translatable="yes">Select Location</property>
+ <style>
+ <class name="title" />
+ </style>
+ </object>
+ </property>
+ </object>
</child>
<child>
- <object class="HdyViewSwitcherTitle" id="forecastStackSwitcher">
- <property name="visible">True</property>
- <property name="title" translatable="yes">Weather</property>
- </object>
- <packing>
+ <object class="GtkStackPage">
<property name="name">city</property>
- </packing>
+ <property name="child">
+ <object class="AdwViewSwitcherTitle" id="forecastStackSwitcher">
+ <property name="title" translatable="yes">Weather</property>
+ </object>
+ </property>
+ </object>
</child>
</object>
</child>
- <child>
- <object class="GtkMenuButton" id="primary-menu-button">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="receives_default">False</property>
+ <child type="end">
+ <object class="GtkMenuButton">
<property name="valign">center</property>
<property name="menu_model">primary-menu</property>
- <child>
- <object class="GtkImage" id="primary-menu-img">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="icon_name">open-menu-symbolic</property>
- </object>
- </child>
+ <property name="icon_name">open-menu-symbolic</property>
</object>
- <packing>
- <property name="pack_type">end</property>
- </packing>
</child>
</object>
</child>
<child>
<object class="GtkStack" id="stack">
- <property name="can_focus">False</property>
+
<property name="transition_type">crossfade</property>
<child>
- <object class="HdyStatusPage" id="searchView">
- <property name="visible">True</property>
- <property name="icon_name">mark-location-symbolic</property>
- <property name="title" translatable="yes">Welcome to Weather!</property>
- <property name="description" translatable="yes">To get started, select a location.</property>
- <child>
- <object class="GWeatherLocationEntry" id="searchEntry">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="activates_default">True</property>
- <property name="hexpand">False</property>
- <property name="halign">center</property>
- <property name="width-request">246</property>
- <property name="placeholder_text" translatable="yes">Search for a city or
country</property>
+ <object class="GtkStackPage">
+ <property name="name">search</property>
+ <property name="child">
+ <object class="AdwStatusPage" id="searchView">
+
+ <property name="icon_name">mark-location-symbolic</property>
+ <property name="title" translatable="yes">Welcome to Weather!</property>
+ <property name="description" translatable="yes">To get started, select a
location.</property>
+ <child>
+ <object class="GtkMenuButton" id="searchButton">
+ <property name="hexpand">False</property>
+ <property name="halign">center</property>
+ <property name="width-request">146</property>
+ <property name="label" translatable="yes">Search for a city or country</property>
+ </object>
+ </child>
+
+ <style>
+ <class name="search-view" />
+ </style>
</object>
- </child>
- <style>
- <class name="search-view"/>
- </style>
+ </property>
</object>
- <packing>
- <property name="name">search</property>
- </packing>
</child>
+
</object>
</child>
<child>
- <object class="HdyViewSwitcherBar" id="forecastStackSwitcherBar">
- <property name="visible">True</property>
+ <object class="AdwViewSwitcherBar" id="forecastStackSwitcherBar">
<property name="reveal" bind-source="forecastStackSwitcher" bind-property="title-visible"
bind-flags="sync-create" />
</object>
</child>
</object>
</child>
</template>
-</interface>
+</interface>
\ No newline at end of file
diff --git a/meson.build b/meson.build
index dc0f1b8..28246da 100644
--- a/meson.build
+++ b/meson.build
@@ -9,11 +9,11 @@ gnome = import('gnome')
dependency('glib-2.0')
dependency('gobject-introspection-1.0', version: '>=1.35.9')
-dependency('gtk+-3.0', version :'>=3.20')
-dependency('gjs-1.0', version: '>= 1.50.0')
+dependency('gtk4', version :'>=4.5')
+dependency('gjs-1.0', version: '>= 1.71.0')
dependency('geoclue-2.0', version: '>= 0.12.99')
-dependency('gweather-3.0', version: '>= 40.0')
-dependency('libhandy-1', version: '>= 1.1.90')
+dependency('libadwaita-1')
+dependency('gweather4', version: '>= 3.90.0')
# Profiles
if get_option('profile') == 'development'
@@ -30,7 +30,7 @@ else
endif
default_id = 'org.gnome.Weather'
-weather_id = default_id + profile
+weather_id = default_id
weather_prefix = get_option('prefix')
weather_libdir = join_paths(weather_prefix, get_option('libdir'))
diff --git a/org.gnome.Weather.json b/org.gnome.Weather.json
index 36b18fe..83cf3bd 100644
--- a/org.gnome.Weather.json
+++ b/org.gnome.Weather.json
@@ -1,5 +1,5 @@
{
- "app-id" : "org.gnome.WeatherDevel",
+ "app-id" : "org.gnome.Weather",
"runtime" : "org.gnome.Platform",
"runtime-version" : "master",
"sdk" : "org.gnome.Sdk",
@@ -54,32 +54,7 @@
{
"type" : "git",
"url" : "https://gitlab.gnome.org/GNOME/libgweather.git",
- "tag" : "40.0"
- }
- ]
- },
- {
- "name" : "gnome-desktop",
- "buildsystem" : "meson",
- "config-opts" : [
- "-Ddebug_tools=false",
- "-Dudev=disabled",
- "-Ddesktop_docs=false"
- ],
- "sources" : [
- {
- "type" : "git",
- "url" : "https://gitlab.gnome.org/GNOME/gnome-desktop.git"
- }
- ]
- },
- {
- "name" : "libhandy",
- "buildsystem" : "meson",
- "sources" : [
- {
- "type" : "git",
- "url" : "https://gitlab.gnome.org/GNOME/libhandy.git"
+ "branch" : "main"
}
]
},
diff --git a/src/app/application.js b/src/app/application.js
new file mode 100644
index 0000000..58375ef
--- /dev/null
+++ b/src/app/application.js
@@ -0,0 +1,230 @@
+//
+// Copyright (c) 2012 Giovanni Campagna <scampa giovanni gmail com>
+//
+// Gnome Weather is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by the
+// Free Software Foundation; either version 2 of the License, or (at your
+// option) any later version.
+//
+// Gnome Weather is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+// for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with Gnome Weather; if not, write to the Free Software Foundation,
+// Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import Adw from 'gi://Adw';
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import Gtk from 'gi://Gtk';
+import GWeather from 'gi://GWeather';
+
+// ensure the type before we call to GtkBuilder
+import './entry.js';
+
+import * as Window from './window.js';
+import * as World from '../shared/world.js';
+import * as CurrentLocationController from './currentLocationController.js';
+
+import { ShellIntegration } from './shell.js';
+
+export class WeatherApplication extends Adw.Application {
+
+ constructor() {
+ super({
+ applicationId: pkg.name,
+ resourceBasePath: '/org/gnome/Weather',
+ });
+ let name_prefix = '';
+
+ GLib.set_application_name(name_prefix + _("Weather"));
+ Gtk.Window.set_default_icon_name(pkg.name);
+
+ this._mainWindow = undefined;
+ }
+
+ get mainWindow() {
+ return this._mainWindow;
+ }
+
+ set mainWindow(value) {
+ this._mainWindow = value;
+ }
+
+ _onQuit() {
+ this.quit();
+ }
+
+ _onShowLocation(action, parameter) {
+ let location = this.world.deserialize(parameter.deep_unpack());
+ let win = this._createWindow();
+
+ let info = this.model.addNewLocation(location, false);
+ win.showInfo(info, false);
+ this._showWindowWhenReady(win);
+ }
+
+ _onShowSearch(action, parameter) {
+ let text = parameter.deep_unpack();
+ let win = this._createWindow();
+
+ win.showSearch(text);
+ this._showWindowWhenReady(win);
+ }
+
+ vfunc_startup() {
+ super.vfunc_startup();
+
+ this.world = GWeather.Location.get_world();
+ this.model = new World.WorldModel(this.world, true);
+ this.currentLocationController = new CurrentLocationController.CurrentLocationController(this.model);
+
+ this.model.load();
+
+
+ this.model.connect('notify::loading', () => {
+ if (this.model.loading)
+ this.mark_busy();
+ else
+ this.unmark_busy();
+ });
+ if (this.model.loading)
+ this.mark_busy();
+
+ let quitAction = new Gio.SimpleAction({
+ enabled: true,
+ name: 'quit'
+ });
+ quitAction.connect('activate', () => this._onQuit());
+ this.add_action(quitAction);
+
+ let showLocationAction = new Gio.SimpleAction({
+ enabled: true,
+ name: 'show-location',
+ parameter_type: new GLib.VariantType('v'),
+ });
+ showLocationAction.connect('activate', (action, parameter) => {
+ this._onShowLocation(action, parameter);
+ });
+ this.add_action(showLocationAction);
+
+ let showSearchAction = new Gio.SimpleAction({
+ enabled: true,
+ name: 'show-search',
+ parameter_type: new GLib.VariantType('v'),
+ })
+ showSearchAction.connect('activate', (action, parameter) => {
+ this._onShowSearch(action, parameter);
+ });
+ this.add_action(showSearchAction);
+
+ let gwSettings = new Gio.Settings({ schema_id: 'org.gnome.GWeather4' });
+ // Sync settings changes to the legacy GTK3 GWeather interface if it is
+ // available
+ let legacyGwSettings;
+ try {
+ legacyGwSettings = new Gio.Settings({ schema_id: 'org.gnome.GWeather' });
+ } catch { }
+
+ // we would like to use g_settings_create_action() here
+ // but that does not handle correctly the case of 'default'
+ // we would also like to use g_settings_bind_with_mapping(), but that
+ // function is not introspectable (two callbacks, one destroy notify)
+ // so we hand code the behavior we want
+ function resolveDefaultTemperatureUnit(unit) {
+ unit = GWeather.TemperatureUnit.to_real(unit);
+ if (unit == GWeather.TemperatureUnit.CENTIGRADE)
+ return new GLib.Variant('s', 'centigrade');
+ else if (unit == GWeather.TemperatureUnit.FAHRENHEIT)
+ return new GLib.Variant('s', 'fahrenheit');
+ else
+ return new GLib.Variant('s', 'default');
+ }
+ let temperatureAction = new Gio.SimpleAction({
+ enabled: true,
+ name: 'temperature-unit',
+ state: resolveDefaultTemperatureUnit(gwSettings.get_enum('temperature-unit')),
+ parameter_type: new GLib.VariantType('s')
+ });
+ temperatureAction.connect('activate', function (_, parameter) {
+ gwSettings.set_value('temperature-unit', parameter);
+ if (legacyGwSettings) {
+ legacyGwSettings.set_value('temperature-unit', parameter);
+ }
+ });
+ gwSettings.connect('changed::temperature-unit', function () {
+ temperatureAction.state = resolveDefaultTemperatureUnit(gwSettings.get_enum('temperature-unit'));
+ });
+ this.add_action(temperatureAction);
+
+ this.set_accels_for_action("win.selection-mode", ["Escape"]);
+ this.set_accels_for_action("win.select-all", ["<Control>a"]);
+ this.set_accels_for_action("app.quit", ["<Control>q"]);
+ }
+
+ vfunc_dbus_register(conn, path) {
+ this._shellIntegration = new ShellIntegration();
+ this._shellIntegration.export(conn, path);
+ return true;
+ }
+
+ vfunc_dbus_unregister(conn, path) {
+ this._shellIntegration.unexport(conn);
+ }
+
+ _createWindow() {
+ const window = new Window.MainWindow({ application: this });
+
+ // Store a weak reference to the window for cleanup...
+ this.mainWindow = window;
+
+ return window;
+ }
+
+ _showWindowWhenReady(win) {
+ let notifyId;
+ win.present();
+ if (this.model.loading) {
+ let timeoutId;
+ let model = this.model;
+
+ timeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 1000, function () {
+ log('Timeout during model load, perhaps the network is not available?');
+ model.disconnect(notifyId);
+
+ return false;
+ });
+ notifyId = this.model.connect('notify::loading', function (model) {
+ if (model.loading)
+ return;
+
+ model.disconnect(notifyId);
+ GLib.source_remove(timeoutId);
+ });
+ }
+
+ return win;
+ }
+
+ vfunc_activate() {
+ let win = this._createWindow();
+ win.showDefault();
+ this._showWindowWhenReady(win);
+ }
+
+ vfunc_shutdown() {
+ GWeather.Info.store_cache();
+ this.model.saveSettingsNow();
+
+ // Ensure our main window is cleaned up before we exit.
+ this.mainWindow?.run_dispose();
+ this.mainWindow = undefined;
+
+ super.vfunc_shutdown();
+ }
+};
+
+GObject.registerClass(WeatherApplication);
diff --git a/src/app/city.js b/src/app/city.js
index cf669c8..1804443 100644
--- a/src/app/city.js
+++ b/src/app/city.js
@@ -16,68 +16,63 @@
// with Gnome Weather; if not, write to the Free Software Foundation,
// Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
-const Gio = imports.gi.Gio;
-const GLib = imports.gi.GLib;
-const Gnome = imports.gi.GnomeDesktop;
-const GObject = imports.gi.GObject;
-const Gdk = imports.gi.Gdk;
-const Gtk = imports.gi.Gtk;
-const GWeather = imports.gi.GWeather;
+import Adw from 'gi://Adw';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import Gtk from 'gi://Gtk';
+import GWeather from 'gi://GWeather';
-const WorldView = imports.app.world;
-const HourlyForecast = imports.app.hourlyForecast;
-const DailyForecast = imports.app.dailyForecast;
-const Util = imports.misc.util;
+import * as WorldView from './world.js';
+import * as Util from '../misc/util.js';
-const SPINNER_SIZE = 128;
+import './hourlyForecast.js';
+import './dailyForecast.js';
const SCROLLING_ANIMATION_TIME = 400000; //us
const UPDATED_TIME_TIMEOUT = 60; //s
-var WeatherWidget = GObject.registerClass({
+export const WeatherWidget = GObject.registerClass({
Template: 'resource:///org/gnome/Weather/weather-widget.ui',
- InternalChildren: ['contentFrame', 'outerBox',
- 'conditionsImage', 'placesButton', 'placesLabel',
- 'temperatureLabel', 'apparentLabel',
- 'forecastFrame', 'forecastStack',
- 'leftButton', 'rightButton',
- 'forecast-hourly', 'forecast-hourly-viewport',
- 'forecast-daily', 'forecast-daily-viewport',
- 'updatedTimeLabel', 'attributionLabel'],
-}, class WeatherWidget extends Gtk.Frame {
-
- _init(application, window, params) {
- super._init(Object.assign({
- shadow_type: Gtk.ShadowType.NONE,
+ InternalChildren: [
+ 'conditionsImage',
+ 'placesButton',
+ 'temperatureLabel',
+ 'apparentLabel',
+ 'forecastStack',
+ 'leftButton',
+ 'rightButton',
+ 'forecastHourly',
+ 'forecastHourlyScrollWindow',
+ 'forecastHourlyAdjustment',
+ 'forecastDaily',
+ 'forecastDailyScrollWindow',
+ 'forecastDailyAdjustment',
+ 'updatedTimeLabel',
+ 'attributionLabel'
+ ],
+}, class WeatherWidget extends Adw.Bin {
+ constructor(application, window) {
+ super({
name: 'weather-page'
- }, params));
+ });
+
+ Object.assign(this.layoutManager, {
+ maximumSize: 1010,
+ // Ensures ~18px of margin on the right side
+ tighteningThreshold: 992,
+ });
this._info = null;
- this._worldView = new WorldView.WorldContentView(application, window);
+ this._worldView = new WorldView.WorldContentView(application, window, {
+ align: Gtk.Align.START,
+ });
this._placesButton.set_popover(this._worldView);
- this._forecasts = { };
-
- for (let t of ['hourly', 'daily']) {
- let box;
- if (t == 'hourly') {
- box = new HourlyForecast.HourlyForecastBox();
- } else {
- box = new DailyForecast.DailyForecastBox();
- }
-
- this._forecasts[t] = box;
- this['_forecast_' + t + '_viewport'].add(box);
-
- let fsw = this['_forecast_' + t];
- let hscrollbar = fsw.get_hscrollbar();
- hscrollbar.set_opacity(0.0);
- hscrollbar.hide();
- let hadjustment = fsw.get_hadjustment();
- hadjustment.connect('changed', () => this._syncLeftRightButtons());
- hadjustment.connect('value-changed', () => this._syncLeftRightButtons());
+ for (const adjustment of [this._forecastHourlyAdjustment, this._forecastDailyAdjustment]) {
+ adjustment.connect('changed', () => this._syncLeftRightButtons());
+ adjustment.connect('value-changed', () => this._syncLeftRightButtons());
}
this._forecastStack.connect('notify::visible-child', () => {
@@ -111,52 +106,36 @@ var WeatherWidget = GObject.registerClass({
this._beginScrollAnimation(target);
});
- this._forecastFrame.connect('draw', (frame, cr) => {
- const width = frame.get_allocated_width();
- const height = frame.get_allocated_height();
-
- const borderRadius = 8;
-
- const arc0 = 0.0;
- const arc1 = Math.PI * 0.5
- const arc2 = Math.PI;
- const arc3 = Math.PI * 1.5
-
- cr.newSubPath();
- cr.arc(width - borderRadius, borderRadius, borderRadius, arc3, arc0);
- cr.arc(width - borderRadius, height - borderRadius, borderRadius, arc0, arc1);
- cr.arc(borderRadius, height - borderRadius, borderRadius, arc1, arc2);
- cr.arc(borderRadius, borderRadius, borderRadius, arc2, arc3);
- cr.closePath();
-
- cr.clip();
- cr.fill();
-
- return false;
- });
-
this._updatedTime = null;
this._updatedTimeTimeoutId = 0;
+ }
+
+ vfunc_unroot() {
+ this._worldView.unparent();
+ this._worldView = null;
- this.connect('destroy', () => this._onDestroy());
+ super.vfunc_unroot();
}
- _onDestroy() {
+ vfunc_unmap() {
if (this._updatedTimeTimeoutId) {
GLib.Source.remove(this._updatedTimeTimeoutId);
this._updatedTimeTimeoutId = 0;
}
+
+ super.vfunc_unmap();
}
_syncLeftRightButtons() {
- let hadjustment = this._forecastStack.visible_child.get_hadjustment();
+ const visible_child = this._forecastStack.visible_child;
+ let hadjustment = visible_child.get_hadjustment();
if ((hadjustment.get_upper() - hadjustment.get_lower()) == hadjustment.page_size) {
this._leftButton.hide();
this._rightButton.hide();
- } else if (hadjustment.value == hadjustment.get_lower()){
+ } else if (hadjustment.value == hadjustment.get_lower()) {
this._leftButton.hide();
this._rightButton.show();
- } else if (hadjustment.value >= (hadjustment.get_upper() - hadjustment.page_size)){
+ } else if (hadjustment.value >= (hadjustment.get_upper() - hadjustment.page_size)) {
this._leftButton.show();
this._rightButton.hide();
} else {
@@ -183,7 +162,7 @@ var WeatherWidget = GObject.registerClass({
if (now < end) {
t = (now - start) / SCROLLING_ANIMATION_TIME;
- t = Util.easeOutCubic (t);
+ t = Util.easeOutCubic(t);
hadjustment.value = value + t * (target - value);
return true;
} else {
@@ -194,8 +173,8 @@ var WeatherWidget = GObject.registerClass({
}
clear() {
- for (let t of ['hourly', 'daily'])
- this._forecasts[t].clear();
+ this._forecastHourly.clear();
+ this._forecastDaily.clear();
if (this._tickId) {
this.remove_tick_callback(this._tickId);
@@ -210,23 +189,12 @@ var WeatherWidget = GObject.registerClass({
update(info) {
this._info = info;
- let location = info.location;
- let city = location;
- if (location.get_level() == GWeather.LocationLevel.WEATHER_STATION)
- city = location.get_parent();
-
- let country = city.get_parent();
- while (country && country.get_level() > GWeather.LocationLevel.COUNTRY)
- country = country.get_parent();
-
- if (country)
- this._placesLabel.set_text(city.get_name() + ', ' + country.get_name());
- else
- this._placesLabel.set_text(city.get_name());
+ const label = Util.getNameAndCountry(info.location);
+ this._placesButton.set_label(label.join(', '));
this._worldView.refilter();
- this._conditionsImage.iconName = info.get_icon_name() + '-large';
+ this._conditionsImage.iconName = `${info.get_icon_name()}-large`;
const [, tempValue] = info.get_value_temp(GWeather.TemperatureUnit.DEFAULT);
this._temperatureLabel.label = '%d°'.format(Math.round(tempValue));
@@ -234,8 +202,8 @@ var WeatherWidget = GObject.registerClass({
const [, apparentValue] = info.get_value_apparent(GWeather.TemperatureUnit.DEFAULT);
this._apparentLabel.label = _('Feels like %.0f°').format(apparentValue);
- for (let t of ['hourly', 'daily'])
- this._forecasts[t].update(info);
+ this._forecastHourly.update(info);
+ this._forecastDaily.update(info);
if (this._updatedTimeTimeoutId)
GLib.Source.remove(this._updatedTimeTimeoutId);
@@ -295,26 +263,21 @@ var WeatherWidget = GObject.registerClass({
}
});
-var WeatherView = GObject.registerClass({
+WeatherWidget.set_layout_manager_type(Adw.ClampLayout);
+
+export const WeatherView = GObject.registerClass({
Template: 'resource:///org/gnome/Weather/city.ui',
- InternalChildren: ['spinner']
-}, class WeatherView extends Gtk.Stack {
+ InternalChildren: ['spinner', 'stack']
+}, class WeatherView extends Adw.Bin {
- _init(application, window, params) {
- super._init(params);
+ constructor(application, window, params) {
+ super(params);
this._infoPage = new WeatherWidget(application, window);
- this.add_named(this._infoPage, 'info');
+ this._stack.add_named(this._infoPage, 'info');
this._info = null;
this._updateId = 0;
-
- this.connect('destroy', () => this._onDestroy());
-
- this._wallClock = new Gnome.WallClock();
- this._clockHandlerId = 0;
-
- this._desktopSettings = new Gio.Settings({ schema_id: 'org.gnome.desktop.interface' });
}
get info() {
@@ -332,23 +295,39 @@ var WeatherView = GObject.registerClass({
this._info = info;
if (info) {
+ this._stack.visible_child_name = 'loading';
+ this._spinner.start();
this._updateId = this._info.connect('updated', (info) => {
this._onUpdate(info)
});
- if (info.is_valid())
+
+ if (info.is_valid()) {
this._onUpdate(info);
+ } else {
+ info.update();
+ }
}
}
- _onDestroy() {
+ vfunc_map() {
+ super.vfunc_map();
+
+ this._spinner.start();
+ }
+
+ vfunc_unmap() {
if (this._updateId) {
this._info.disconnect(this._updateId);
this._updateId = 0;
}
+
+ this._spinner.stop();
+
+ super.vfunc_unmap();
}
update() {
- this.visible_child_name = 'loading';
+ this._stack.visible_child_name = 'loading';
this._spinner.start();
this._infoPage.clear();
@@ -359,10 +338,10 @@ var WeatherView = GObject.registerClass({
this._infoPage.clear();
this._infoPage.update(info);
this._spinner.stop();
- this.visible_child_name = 'info';
+ this._stack.visible_child_name = 'info';
}
- getInfoPage() {
- return this._infoPage;
+ getForecastStack() {
+ return this._infoPage.getForecastStack();
}
});
diff --git a/src/app/currentLocationController.js b/src/app/currentLocationController.js
index 644ac63..fc66102 100644
--- a/src/app/currentLocationController.js
+++ b/src/app/currentLocationController.js
@@ -16,29 +16,19 @@
// with Gnome Weather; if not, write to the Free Software Foundation,
// Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
-const GLib = imports.gi.GLib;
-const Gio = imports.gi.Gio;
-const Lang = imports.lang;
-const GWeather = imports.gi.GWeather;
-const Geoclue = imports.gi.Geoclue;
+import GLib from 'gi://GLib';
+import GWeather from 'gi://GWeather';
+import Geoclue from 'gi://Geoclue';
-const Util = imports.misc.util;
-
-var AutoLocation = {
- DISABLED: 0,
- ENABLED: 1,
- NOT_AVAILABLE: 2
-};
-
-var CurrentLocationController = class CurrentLocationController {
+import * as Util from '../misc/util.js';
+export class CurrentLocationController {
constructor(world) {
this._world = world;
this._processStarted = false;
this._settings = Util.getSettings('org.gnome.Weather');
- let autoLocation = this._settings.get_value('automatic-location').deep_unpack();
- this._syncAutoLocation(autoLocation);
- if (this.autoLocation == AutoLocation.ENABLED)
- this._startGeolocationService();
+
+ this.autoLocationAvailable = false;
+ this._startGeolocationService();
this.currentLocation = null;
}
@@ -54,7 +44,7 @@ var CurrentLocationController = class CurrentLocationController {
_geoLocationFailed(e) {
log ("Failed to connect to GeoClue2 service: " + e.message);
- this.autoLocation = AutoLocation.NOT_AVAILABLE;
+ this.autoLocationAvailable = false;
GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
this._world.currentLocationChanged(null);
});
@@ -73,6 +63,8 @@ var CurrentLocationController = class CurrentLocationController {
client.distance_threshold = 100;
this._findLocation();
+
+ this.autoLocationAvailable = true;
}
_findLocation() {
@@ -95,22 +87,6 @@ var CurrentLocationController = class CurrentLocationController {
this._world.currentLocationChanged(this.currentLocation);
}
- setAutoLocation(active) {
- this._settings.set_value('automatic-location', new GLib.Variant('b', active));
-
- if (this.autoLocation == AutoLocation.NOT_AVAILABLE)
- return;
- this._autoLocationChanged(active);
- this._syncAutoLocation(active);
- }
-
- _syncAutoLocation(autoLocation) {
- if (autoLocation)
- this.autoLocation = AutoLocation.ENABLED;
- else
- this.autoLocation = AutoLocation.DISABLED;
- }
-
_autoLocationChanged(active) {
if (active) {
if (!this._processStarted) {
diff --git a/src/app/dailyForecast.js b/src/app/dailyForecast.js
index e3c71e8..0168c43 100644
--- a/src/app/dailyForecast.js
+++ b/src/app/dailyForecast.js
@@ -16,26 +16,24 @@
// with Gnome Weather; if not, write to the Free Software Foundation,
// Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
-const Gio = imports.gi.Gio;
-const GLib = imports.gi.GLib;
-const GObject = imports.gi.GObject;
-const Gtk = imports.gi.Gtk;
-const GWeather = imports.gi.GWeather;
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import Gtk from 'gi://Gtk';
+import GWeather from 'gi://GWeather';
-const Thermometer = imports.app.thermometer;
+import * as Thermometer from './thermometer.js';
+import * as Util from '../misc/util.js';
-const Util = imports.misc.util;
+export class DailyForecastBox extends Gtk.Box {
-var DailyForecastBox = GObject.registerClass(class DailyForecastBox extends Gtk.Box {
-
- _init(params) {
- super._init(Object.assign({
+ constructor() {
+ super({
orientation: Gtk.Orientation.HORIZONTAL,
spacing: 0,
name: 'daily-forecast-box',
- }, params));
+ });
- this.get_accessible().accessible_name = _('Daily Forecast');
+ this.update_property([Gtk.AccessibleProperty.LABEL], [_('Daily Forecast')]);
}
// get infos for the correct day
@@ -60,8 +58,8 @@ var DailyForecastBox = GObject.registerClass(class DailyForecastBox extends Gtk.
let weekInfos = [];
while (i < infos.length) {
- let dayInfos = {day: day, infos: []};
- for ( ; i < infos.length; i++) {
+ let dayInfos = { day: day, infos: [] };
+ for (; i < infos.length; i++) {
let info = infos[i];
let datetime = Util.getDateTime(info);
@@ -73,269 +71,194 @@ var DailyForecastBox = GObject.registerClass(class DailyForecastBox extends Gtk.
weekInfos.push(dayInfos);
day = day.add_days(1);
}
- return weekInfos;
- }
-
- update(info) {
- let forecasts = info.get_forecast_list();
- let weekInfos = this._preprocess(forecasts);
+ const temperatures = weekInfos.map(dayInfos => dayInfos.infos)
+ .flat()
+ .map(info => Util.getTemp(info));
- if (weekInfos.length > 0) {
- let weekHighestTemp = -Infinity;
- let weekLowestTemp = Infinity;
+ const weekHighestTemp = Math.max(...temperatures);
+ const weekLowestTemp = Math.min(...temperatures);
- weekInfos.map(dayInfos => dayInfos.infos).flat().forEach(info => {
- const temp = Util.getTemp(info);
+ return {
+ weekHighestTemp,
+ weekLowestTemp,
+ days: weekInfos
+ };
+ }
- weekHighestTemp = Math.max(weekHighestTemp, temp);
- weekLowestTemp = Math.min(weekLowestTemp, temp);
- });
+ update(info) {
+ let forecasts = info.get_forecast_list();
- for (let i = 0; i < weekInfos.length; i++) {
- let dayInfos = weekInfos[i];
- this._addDayEntry(dayInfos, weekHighestTemp, weekLowestTemp);
+ let forecast = this._preprocess(forecasts);
- if (i < weekInfos.length - 1)
- this._addSeparator();
- }
+ if (forecast.days.length > 1) {
+ forecast.days.reduce((_, dayInfos) => {
+ this.append(this._buildDayEntry(dayInfos, forecast.weekHighestTemp,
forecast.weekLowestTemp));
+ this.append(this._buildSeparator());
+ }, null);
} else {
- let label = new Gtk.Label({ label: _('Forecast not available'),
- use_markup: true,
- visible: true });
- this.pack_start(label, true, false, 0);
+ let label = new Gtk.Label({
+ label: _('Forecast not available'),
+ use_markup: true,
+ visible: true
+ });
+ this.prepend(label);
}
}
- _addDayEntry({day, infos}, weekHighestTemp, weekLowestTemp) {
- let maxInfo;
- let maxTemp = -Infinity;
+ _buildDayEntry({ day, infos }, weekHighestTemp, weekLowestTemp) {
+ let datetime = Util.getDay(day);
- let minInfo;
- let minTemp = Infinity;
+ const temperatures = infos.map(info => Util.getTemp(info));
+ const minTemp = Math.min(...temperatures);
+ const maxTemp = Math.max(...temperatures);
- day = Util.getDay(day);
- let dayInfo;
- let dayDiff = Infinity;
+ let periodInfos = {}, times = {
+ day: Util.getDay(datetime),
+ night: Util.getNight(datetime),
+ morning: Util.getMorning(datetime),
+ afternoon: Util.getAfternoon(datetime),
+ evening: Util.getEvening(datetime)
+ };
- let night = Util.getNight(day);
- let nightInfo;
- let nightDiff = Infinity;
+ const datetimes = infos.map(info => Util.getDateTime(info));
- let morning = Util.getMorning(day);
- let morningInfo;
- let morningDiff = Infinity;
+ for (const period of ['day', 'night', 'morning', 'afternoon', 'evening']) {
+ const differences = datetimes.map(datetime => Math.abs(datetime.difference(times[period])));
- let afternoon = Util.getAfternoon(day);
- let afternoonInfo;
- let afternoonDiff = Infinity;
+ const index = differences.indexOf(Math.min(...differences))
- let evening = Util.getEvening(day);
- let eveningInfo;
- let eveningDiff = Infinity;
-
- for (let i = 0; i < infos.length; i++) {
- let info = infos[i];
-
- let temp = Util.getTemp(info);
- if (temp > maxTemp) {
- maxInfo = info;
- maxTemp = temp;
- }
- if (temp < minTemp) {
- minInfo = info;
- minTemp = temp;
- }
-
- let datetime = Util.getDateTime(info);
-
- let diff = Math.abs(datetime.difference(day));
- if (diff < dayDiff) {
- dayInfo = info;
- dayDiff = diff;
- }
+ periodInfos[period] = infos[index];
+ }
- diff = Math.abs(datetime.difference(night));
- if (diff < nightDiff) {
- nightInfo = info;
- nightDiff = diff;
- }
- diff = Math.abs(datetime.difference(morning));
- if (diff < morningDiff) {
- morningInfo = info;
- morningDiff = diff;
- }
+ const { day: dayInfo, night, morning, afternoon, evening } = periodInfos;
+
+ return new DayEntry({
+ datetime,
+ weekHighestTemp,
+ weekLowestTemp,
+ maxTemp,
+ minTemp,
+ day: dayInfo,
+ night,
+ morning,
+ afternoon,
+ evening
+ });
+ }
- diff = Math.abs(datetime.difference(afternoon));
- if (diff < afternoonDiff) {
- afternoonInfo = info;
- afternoonDiff = diff;
- }
+ _buildSeparator() {
+ return new Gtk.Separator({
+ orientation: Gtk.Orientation.VERTICAL,
+ visible: true
+ });
+ }
- diff = Math.abs(datetime.difference(evening));
- if (diff < eveningDiff) {
- eveningInfo = info;
- eveningDiff = diff;
- }
+ clear() {
+ for (const entry of Array.from(this)) {
+ entry.unparent();
}
+ }
+};
+GObject.registerClass(DailyForecastBox);
+
+export const DayEntry = GObject.registerClass({
+ Template: 'resource:///org/gnome/Weather/day-entry.ui',
+ InternalChildren: ['nameLabel', 'dateLabel', 'image',
+ 'thermometer',
+ 'nightTemperatureLabel', 'nightImage',
+ 'nightHumidity', 'nightWind',
+ 'morningTemperatureLabel', 'morningImage',
+ 'morningHumidity', 'morningWind',
+ 'afternoonTemperatureLabel', 'afternoonImage',
+ 'afternoonHumidity', 'afternoonWind',
+ 'eveningTemperatureLabel', 'eveningImage',
+ 'eveningHumidity', 'eveningWind'],
+}, class DayEntry extends Gtk.Widget {
+
+ constructor(params) {
+ const {
+ datetime,
+ maxTemp,
+ minTemp,
+ weekHighestTemp,
+ weekLowestTemp,
+ day,
+ night,
+ morning,
+ afternoon,
+ evening
+ } = params;
+
+ super();
+
+
+ this.datetime = datetime;
+ this.info = {
+ day,
+ night,
+ morning,
+ afternoon,
+ evening
+ };
+ this.maxTemp = maxTemp;
+ this.minTemp = minTemp;
+ this.weekHighestTemp = weekHighestTemp;
+ this.weekLowestTemp = weekLowestTemp;
+ this.layoutManager.orientation = Gtk.Orientation.VERTICAL;
+ }
- let dayEntry = new DayEntry();
+ vfunc_root() {
+ super.vfunc_root();
- let nameFormat = '%a';
- dayEntry.nameLabel.label = day.format(nameFormat);
+ const { datetime } = this;
+ const { day: dayInfo, evening: eveningInfo, night: nightInfo, morning: morningInfo, afternoon:
afternoonInfo } = this.info;
+ this._nameLabel.label = datetime.format('%a');
/* Translators: this is the time format for day and month name according to the current locale */
let dateFormat = _('%b %e');
- dayEntry.dateLabel.label = day.format(dateFormat);
-
- dayEntry.image.iconName = dayInfo.get_icon_name() + '-small';
-
- const adjustment = Gtk.Adjustment.new(minTemp,
- weekLowestTemp, weekHighestTemp,
- 0, 0,
- maxTemp - minTemp);
- dayEntry.thermometer.adjustment = adjustment;
-
- dayEntry.nightTemperatureLabel.label = Util.getTempString(nightInfo);
- dayEntry.nightImage.iconName = nightInfo.get_icon_name() + '-small';
- dayEntry.nightHumidity.label = nightInfo.get_humidity();
- this._setWindInfo(nightInfo, dayEntry.nightWind);
-
- dayEntry.morningTemperatureLabel.label = Util.getTempString(morningInfo);
- dayEntry.morningImage.iconName = morningInfo.get_icon_name() + '-small';
- dayEntry.morningHumidity.label = morningInfo.get_humidity();
- this._setWindInfo(morningInfo, dayEntry.morningWind);
-
- dayEntry.afternoonTemperatureLabel.label = Util.getTempString(afternoonInfo);
- dayEntry.afternoonImage.iconName = afternoonInfo.get_icon_name() + '-small';
- dayEntry.afternoonHumidity.label = afternoonInfo.get_humidity();
- this._setWindInfo(afternoonInfo, dayEntry.afternoonWind);
-
- dayEntry.eveningTemperatureLabel.label = Util.getTempString(eveningInfo);
- dayEntry.eveningImage.iconName = eveningInfo.get_icon_name() + '-small';
- dayEntry.eveningHumidity.label = eveningInfo.get_humidity();
- this._setWindInfo(eveningInfo, dayEntry.eveningWind);
-
- this.pack_start(dayEntry, false, false, 0);
+ this._dateLabel.label = datetime.format(dateFormat);
+
+ this._image.iconName = `${dayInfo.get_icon_name()}-small`;
+
+ this._thermometer.range = new Thermometer.TemperatureRange({ dailyLow: this.minTemp, dailyHigh:
this.maxTemp, weeklyLow: this.weekLowestTemp, weeklyHigh: this.weekHighestTemp });
+ this._nightTemperatureLabel.label = Util.getTempString(nightInfo);
+ this._nightImage.iconName = nightInfo.get_icon_name() + '-small';
+ this._nightHumidity.label = nightInfo.get_humidity();
+ this._setWindInfo(nightInfo, this._nightWind);
+
+ this._morningTemperatureLabel.label = Util.getTempString(morningInfo);
+ this._morningImage.iconName = morningInfo.get_icon_name() + '-small';
+ this._morningHumidity.label = morningInfo.get_humidity();
+ this._setWindInfo(morningInfo, this._morningWind);
+
+ this._afternoonTemperatureLabel.label = Util.getTempString(afternoonInfo);
+ this._afternoonImage.iconName = afternoonInfo.get_icon_name() + '-small';
+ this._afternoonHumidity.label = afternoonInfo.get_humidity();
+ this._setWindInfo(afternoonInfo, this._afternoonWind);
+
+ this._eveningTemperatureLabel.label = Util.getTempString(eveningInfo);
+ this._eveningImage.iconName = eveningInfo.get_icon_name() + '-small';
+ this._eveningHumidity.label = eveningInfo.get_humidity();
+ this._setWindInfo(eveningInfo, this._eveningWind);
}
- _addSeparator() {
- let separator = new Gtk.Separator({ orientation: Gtk.Orientation.VERTICAL,
- visible: true});
- this.pack_start(separator, false, false, 0);
+ vfunc_unroot() {
+ [...this].forEach(child => child.unparent());
+
+ super.vfunc_unroot();
}
_setWindInfo(info, label) {
let [ok, speed, direction] = info.get_value_wind(GWeather.SpeedUnit.DEFAULT);
if (ok) {
- label.label = speed.toFixed(1).toString() + ' ' +
GWeather.speed_unit_to_string(GWeather.SpeedUnit.DEFAULT);
+ label.label = `${speed.toFixed(1).toString()}
${GWeather.speed_unit_to_string(GWeather.SpeedUnit.DEFAULT)}`;
} else {
/* Fall back to get_wind() */
label.label = info.get_wind();
}
}
-
- clear() {
- this.foreach(function(w) { w.destroy(); });
- }
});
-var DayEntry = GObject.registerClass({
- Template: 'resource:///org/gnome/Weather/day-entry.ui',
- InternalChildren: ['nameLabel', 'dateLabel', 'image',
- 'thermometer',
- 'nightTemperatureLabel', 'nightImage',
- 'nightHumidity', 'nightWind',
- 'morningTemperatureLabel', 'morningImage',
- 'morningHumidity', 'morningWind',
- 'afternoonTemperatureLabel', 'afternoonImage',
- 'afternoonHumidity', 'afternoonWind',
- 'eveningTemperatureLabel', 'eveningImage',
- 'eveningHumidity', 'eveningWind'],
-}, class DayEntry extends Gtk.Box {
-
- _init(params) {
- super._init(params);
- }
-
- get nameLabel() {
- return this._nameLabel;
- }
-
- get dateLabel() {
- return this._dateLabel;
- }
-
- get image() {
- return this._image;
- }
-
- get thermometer() {
- return this._thermometer;
- }
-
- get nightTemperatureLabel() {
- return this._nightTemperatureLabel;
- }
-
- get nightImage() {
- return this._nightImage;
- }
-
- get nightHumidity() {
- return this._nightHumidity;
- }
-
- get nightWind() {
- return this._nightWind;
- }
-
- get morningTemperatureLabel() {
- return this._morningTemperatureLabel;
- }
-
- get morningImage() {
- return this._morningImage;
- }
-
- get morningHumidity() {
- return this._morningHumidity;
- }
-
- get morningWind() {
- return this._morningWind;
- }
-
- get afternoonTemperatureLabel() {
- return this._afternoonTemperatureLabel;
- }
-
- get afternoonImage() {
- return this._afternoonImage;
- }
-
- get afternoonHumidity() {
- return this._afternoonHumidity;
- }
-
- get afternoonWind() {
- return this._afternoonWind;
- }
-
- get eveningTemperatureLabel() {
- return this._eveningTemperatureLabel;
- }
-
- get eveningImage() {
- return this._eveningImage;
- }
-
- get eveningHumidity() {
- return this._eveningHumidity;
- }
-
- get eveningWind() {
- return this._eveningWind;
- }
-});
+DayEntry.set_layout_manager_type(Gtk.BoxLayout);
diff --git a/src/app/entry.js b/src/app/entry.js
new file mode 100644
index 0000000..041441b
--- /dev/null
+++ b/src/app/entry.js
@@ -0,0 +1,243 @@
+import Adw from 'gi://Adw';
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import Gtk from 'gi://Gtk';
+import GWeather from 'gi://GWeather';
+
+import * as Util from '../misc/util.js';
+import { LocationRow } from './locationRow.js';
+
+GWeather.Location.prototype[Symbol.iterator] = function* () {
+ let child = this.next_child(null);
+
+ while (child != null) {
+ yield child;
+ child = this.next_child(child);
+ }
+}
+
+function getAllCitiesAndWeatherStations() {
+ const locations = new Set();
+ for (const region of GWeather.Location.get_world()) {
+ for (const country of region) {
+ for (const location of country) {
+ const level = location.get_level();
+ if (level === GWeather.LocationLevel.ADM1) {
+ for (const cityOrStation of location) {
+ const level = cityOrStation.get_level();
+
+ if (level === GWeather.LocationLevel.CITY) {
+ locations.add(cityOrStation);
+ } else if (level === GWeather.LocationLevel.WEATHER_STATION) {
+ locations.add(cityOrStation);
+ }
+ }
+
+ } else if (level === GWeather.LocationLevel.CITY) {
+ locations.add(location);
+ } else if (level === GWeather.LocationLevel.WEATHER_STATION) {
+ locations.add(location);
+ }
+ }
+ }
+ }
+
+ return [...locations.values()];
+}
+
+const LocationListModel = GObject.registerClass(
+ {
+ Implements: [Gio.ListModel]
+ },
+ class LocationListModel extends GObject.Object {
+ constructor() {
+ super();
+
+ this._show_named_timezones = false;
+
+ this._list = [];
+ }
+
+ /**
+ * @this {ListModel & this}
+ */
+ load() {
+ const items = getAllCitiesAndWeatherStations()
+ this._list.push(...items);
+
+ this.items_changed(0, 0, this._list.length);
+ }
+
+ vfunc_get_item_type() {
+ return GWeather.Location.$gtype;
+ }
+
+ vfunc_get_n_items() {
+ return this._list.length;
+ }
+
+ /**
+ * @param {number} n
+ */
+ vfunc_get_item(n) {
+ return this._list[n] ?? null;
+ }
+ }
+);
+
+const locationListModel = new LocationListModel();
+imports.mainloop.idle_add(() => {
+ try {
+ locationListModel.load();
+ } catch (error) {
+ console.error(error);
+ }
+
+ return false;
+});
+
+// Avoid the overhead of closures and Gtk.StringFilter
+
+const LocationFilter = GObject.registerClass(
+ class LocationFilter extends Gtk.Filter {
+ constructor() {
+ super();
+
+ /** @type {WeakMap<GWeather.Location, string>} */
+ this._itemMap = new WeakMap();
+ this._filter = null;
+ this._filterLowerCase = null;
+ }
+
+ setFilterString(filter) {
+ if (filter !== this._filter) {
+ this._filter = filter;
+ this._filterLowerCase = this._filter?.toLowerCase() ?? null;
+ this.changed(Gtk.FilterChange.DIFFERENT);
+ }
+ }
+
+ vfunc_match(item) {
+ if (!this._filter) return false;
+
+ const cached = this._itemMap.get(item);
+ const string = cached ?? item.get_name().toLowerCase();
+ if (!cached)
+ this._itemMap.set(item, string);
+
+ return string.includes(this._filterLowerCase);
+ }
+ }
+);
+
+export const LocationSearchEntry = GObject.registerClass(
+ {
+ Properties: {
+ 'text': GObject.ParamSpec.string('text', 'text', 'text', GObject.ParamFlags.READWRITE, ''),
+ 'placeholder-text': GObject.ParamSpec.string('placeholder-text', 'placeholder-text',
'placeholder-text', GObject.ParamFlags.READWRITE, ''),
+ 'location': GObject.ParamSpec.object('location', 'location', 'location',
GObject.ParamFlags.READWRITE, GWeather.Location.$gtype)
+ },
+ Signals: {
+ 'search-updated': { param_types: [GObject.String] },
+ }
+ },
+ class LocationSearchEntry extends Adw.Bin {
+ #entry = new Gtk.SearchEntry({
+ hexpand: true,
+ });
+ #location = null;
+ #listView = null;
+ #text = '';
+
+ #filter = new LocationFilter();
+ #model = new Gtk.SingleSelection({
+ selected: GLib.MAXUINT32,
+ autoselect: false,
+ model: new Gtk.FilterListModel({
+ model: locationListModel,
+ filter: this.#filter,
+ incremental: true,
+ })
+ });
+ #factory = new Gtk.SignalListItemFactory();
+
+ constructor() {
+ super();
+
+ this.set_child(this.#entry);
+
+ this.bind_property('placeholder-text', this.#entry, 'placeholder-text',
GObject.BindingFlags.DEFAULT);
+ this.bind_property('text', this.#entry, 'text', GObject.BindingFlags.BIDRECTIONAL);
+
+ this.#entry.connect('search-changed', source => {
+ const text = source.text || null;
+
+ this.#filter.setFilterString(text);
+ this.emit('search-updated', text);
+ });
+
+ this.#model.connect('notify::selected', ({ selectedItem }) => {
+ if (selectedItem instanceof GWeather.Location) {
+ this.location = selectedItem;
+ }
+ });
+
+ this.#factory.connect('setup', (_, item) => {
+ const row = new LocationRow({ name: '', countryName: '' });
+ item.set_child(row);
+ });
+
+ this.#factory.connect('bind', (_, { child, item }) => {
+ if (child instanceof LocationRow && item instanceof GWeather.Location) {
+ const [name, countryName = ''] = Util.getNameAndCountry(item);
+
+ child.name = name;
+ child.countryName = countryName;
+ }
+ });
+ }
+
+ get text() {
+ return this.#text;
+ }
+
+ set text(text) {
+ this.#text = text;
+
+ this.notify('text');
+ }
+
+ set location(location) {
+ this.#location = location;
+
+ this.notify('location');
+ }
+
+ get location() {
+ return this.#location;
+ }
+
+ /**
+ * @param {Gtk.ListView} listView
+ */
+ setListView(listView) {
+ if (this.#listView)
+ this.#listView.model = null;
+
+ this.#listView = listView;
+ listView.factory = this.#factory;
+ listView.model = this.#model;
+ }
+
+ vfunc_unroot() {
+ if (this.#listView)
+ this.#listView.model = null;
+
+ super.vfunc_unroot();
+ }
+ }
+);
+
+LocationSearchEntry.set_layout_manager_type(Gtk.BinLayout);
+
diff --git a/src/app/hourlyForecast.js b/src/app/hourlyForecast.js
index 543753a..5fd9d7b 100644
--- a/src/app/hourlyForecast.js
+++ b/src/app/hourlyForecast.js
@@ -16,29 +16,28 @@
// with Gnome Weather; if not, write to the Free Software Foundation,
// Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
-const Gio = imports.gi.Gio;
-const GLib = imports.gi.GLib;
-const GObject = imports.gi.GObject;
-const Gdk = imports.gi.Gdk;
-const Gtk = imports.gi.Gtk;
-const GWeather = imports.gi.GWeather;
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import Gtk from 'gi://Gtk';
+import Gdk from 'gi://Gdk';
+import GWeather from 'gi://GWeather';
+import Graphene from 'gi://Graphene';
-const Util = imports.misc.util;
+import * as Util from '../misc/util.js';
// In microseconds
const TWENTY_FOUR_HOURS = 24 * 3600 * 1000 * 1000;
-var HourlyForecastBox = GObject.registerClass(class HourlyForecastBox extends Gtk.Box {
-
- _init(params) {
- super._init(Object.assign({
+export class HourlyForecastBox extends Gtk.Box {
+ constructor() {
+ super({
orientation: Gtk.Orientation.HORIZONTAL,
spacing: 0,
name: 'hourly-forecast-box',
- }, params));
-
- this.get_accessible().accessible_name = _('Hourly Forecast');
+ });
+ this.update_property([Gtk.AccessibleProperty.LABEL], [_('Hourly Forecast')]);
this._settings = new Gio.Settings({ schema_id: 'org.gnome.desktop.interface' });
this._hourlyInfo = [];
@@ -54,7 +53,7 @@ var HourlyForecastBox = GObject.registerClass(class HourlyForecastBox extends Gt
for (let i = 0; i < infos.length; i++) {
let info = infos[i];
- let [ok, date] = info.get_value_update();
+ let [, date] = info.get_value_update();
let datetime = GLib.DateTime.new_from_unix_utc(date).to_timezone(now.get_timezone());
if (datetime.difference(now) <= 0)
@@ -74,7 +73,7 @@ var HourlyForecastBox = GObject.registerClass(class HourlyForecastBox extends Gt
let coords = info.location.get_coords();
let nearestCity = GWeather.Location.get_world().find_nearest_city(coords[0], coords[1]);
- let tz = GLib.TimeZone.new(nearestCity.get_timezone().get_tzid());
+ let tz = nearestCity.get_timezone();
let now = GLib.DateTime.new_now(tz);
let hourlyInfo = this._preprocess(now, forecasts);
@@ -91,17 +90,18 @@ var HourlyForecastBox = GObject.registerClass(class HourlyForecastBox extends Gt
this._addSeparator();
}
} else {
- let label = new Gtk.Label({ label: _('Forecast not available'),
- use_markup: true,
- visible: true });
- this.pack_start(label, true, false, 0);
+ let label = new Gtk.Label({
+ label: _('Forecast not available'),
+ use_markup: true,
+ visible: true
+ });
+ this.prepend(label);
}
this._hourlyInfo = hourlyInfo;
}
_addHourEntry(info, tz, timeLabel) {
- let hourEntry = new HourEntry();
if (!timeLabel) {
let [ok, date] = info.get_value_update();
@@ -118,36 +118,39 @@ var HourlyForecastBox = GObject.registerClass(class HourlyForecastBox extends Gt
timeLabel = datetime.format(timeFormat);
}
- hourEntry.timeLabel.label = timeLabel;
- hourEntry.image.iconName = info.get_icon_name() + '-small';
- hourEntry.temperatureLabel.label = Util.getTempString(info);
- if (Util.isDarkTheme()) {
- const color = "#f6d32d";
- const label = "<span color=\""+ color + "\">" + hourEntry.temperatureLabel.label + "</span>";
- hourEntry.temperatureLabel.set_markup(label);
- };
+ let hourEntry = new HourEntry({ info, timeLabel });
- this.pack_start(hourEntry, false, false, 0);
+ this.append(hourEntry);
this._hasForecastInfo = true;
}
_addSeparator() {
- let separator = new Gtk.Separator({ orientation: Gtk.Orientation.VERTICAL,
- visible: true});
- this.pack_start(separator, false, false, 0);
+ let separator = new Gtk.Separator({
+ orientation: Gtk.Orientation.VERTICAL,
+ visible: true
+ });
+ this.append(separator);
}
clear() {
- this.foreach(function(w) { w.destroy(); });
+ for (const w of Array.from(this)) {
+ this.remove(w);
+ }
}
hasForecastInfo() {
return this._hasForecastInfo;
}
- vfunc_draw(cr) {
+ vfunc_snapshot(snapshot) {
+ const allocation = this.get_allocation();
+
+ const rect = new Graphene.Rect();
+ rect.init(0, 0, allocation.width, allocation.height);
+
+ let cr = snapshot.append_cairo(rect);
const temps = this._hourlyInfo.map(info => Util.getTemp(info));
const maxTemp = Math.max(...temps);
@@ -163,7 +166,7 @@ var HourlyForecastBox = GObject.registerClass(class HourlyForecastBox extends Gt
const width = this.get_allocated_width();
const height = this.get_allocated_height();
- const entryWidth = 75 ;
+ const entryWidth = 75;
const separatorWidth = 1;
const lineWidth = 2;
@@ -177,11 +180,11 @@ var HourlyForecastBox = GObject.registerClass(class HourlyForecastBox extends Gt
const graphMaxY = height - lineWidth / 2 - spacing - entryTemperatureLabelHeight - spacing;
const graphHeight = graphMaxY - graphMinY;
- let [, strokeColor] = this.get_style_context().lookup_color('temp_chart_stroke_color');
+ let [, strokeColor] = this.get_style_context().lookup_color('weather_temp_chart_stroke_color');
Gdk.cairo_set_source_rgba(cr, strokeColor);
let x = 0;
- cr.moveTo (x, graphMinY + ((1 - values[0]) * graphHeight));
+ cr.moveTo(x, graphMinY + ((1 - values[0]) * graphHeight));
x += entryWidth / 2;
cr.lineTo(x, graphMinY + ((1 - values[0]) * graphHeight));
@@ -196,12 +199,7 @@ var HourlyForecastBox = GObject.registerClass(class HourlyForecastBox extends Gt
cr.setLineWidth(lineWidth);
cr.strokePreserve();
- let [, fillColor] = this.get_style_context().lookup_color('temp_chart_fill_color');
-
- if (Util.isDarkTheme()) {
- fillColor = new Gdk.RGBA();
- fillColor.parse("rgba(248, 228, 92, 0.15)");
- };
+ let [, fillColor] = this.get_style_context().lookup_color('weather_temp_chart_fill_color');
Gdk.cairo_set_source_rgba(cr, fillColor);
@@ -209,31 +207,34 @@ var HourlyForecastBox = GObject.registerClass(class HourlyForecastBox extends Gt
cr.lineTo(0, height);
cr.fill();
- super.vfunc_draw(cr);
+ super.vfunc_snapshot(snapshot);
cr.$dispose();
-
- return Gdk.EVENT_PROPAGATE;
}
-});
+};
+GObject.registerClass(HourlyForecastBox);
-var HourEntry = GObject.registerClass({
+export const HourEntry = GObject.registerClass({
Template: 'resource:///org/gnome/Weather/hour-entry.ui',
+
InternalChildren: ['timeLabel', 'image', 'temperatureLabel'],
-}, class HourEntry extends Gtk.Box {
+}, class HourEntry extends Gtk.Widget {
+ constructor({ timeLabel, info, ...params }) {
+ super({ ...params });
- _init(params) {
- super._init(params);
- }
+ Object.assign(this.layoutManager, {
+ orientation: Gtk.Orientation.VERTICAL,
+ });
- get timeLabel() {
- return this._timeLabel;
+ this._timeLabel.label = timeLabel;
+ this._image.iconName = info.get_icon_name() + '-small';
+ this._temperatureLabel.label = Util.getTempString(info);
}
- get image() {
- return this._image;
- }
+ vfunc_unroot() {
+ [...this].forEach(child => child.unparent());
- get temperatureLabel() {
- return this._temperatureLabel;
+ super.vfunc_unroot();
}
});
+
+HourEntry.set_layout_manager_type(Gtk.BoxLayout);
\ No newline at end of file
diff --git a/src/app/locationRow.js b/src/app/locationRow.js
new file mode 100644
index 0000000..87a528f
--- /dev/null
+++ b/src/app/locationRow.js
@@ -0,0 +1,48 @@
+import GObject from 'gi://GObject';
+import Gtk from 'gi://Gtk';
+import GLib from 'gi://GLib';
+
+export const LocationRow = GObject.registerClass({
+ CssName: 'WeatherLocationRow',
+ Template: GLib.Uri.resolve_relative(import.meta.url, './locationRow.ui', 0),
+ InternalChildren: ['label', 'countryLabel', 'labelContainer', 'locationIcon', 'currentIcon'],
+}, class LocationRow extends Gtk.Widget {
+ constructor({ name, countryName, isSelected = false, isCurrentLocation = false }) {
+ super({ widthRequest: 320 });
+
+ Object.assign(this.layoutManager, {
+ orientation: Gtk.Orientation.HORIZONTAL,
+ });
+
+ this.name = name;
+ this.countryName = countryName ?? '';
+ this.isSelected = isSelected;
+ this.isCurrentLocation = isCurrentLocation;
+ }
+
+ set name(name) {
+ this._label.label = name;
+ }
+
+ set countryName(name) {
+ this._countryLabel.label = name;
+ }
+
+ set isCurrentLocation(is) {
+ this._locationIcon.visible = is;
+ }
+
+ set isSelected(is) {
+ this._currentIcon.visible = is;
+ }
+
+ vfunc_unroot() {
+ this._labelContainer.unparent();
+ this._currentIcon.unparent();
+ this._locationIcon.unparent();
+
+ super.vfunc_unroot();
+ }
+});
+
+LocationRow.set_layout_manager_type(Gtk.BoxLayout);
diff --git a/src/app/locationRow.ui b/src/app/locationRow.ui
new file mode 100644
index 0000000..0417134
--- /dev/null
+++ b/src/app/locationRow.ui
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk" version="4.0" />
+ <template class="Gjs_LocationRow">
+ <child>
+ <object class="GtkBox" id="labelContainer">
+ <property name="halign">start</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel" id="label">
+ <property name="name">label</property>
+ <property name="justify">left</property>
+ <property name="halign">start</property>
+ <property name="ellipsize">end</property>
+ <style>
+ <class name="title-4" />
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="countryLabel">
+ <property name="name">countryLabel</property>
+ <property name="justify">left</property>
+ <property name="halign">start</property>
+ <property name="ellipsize">end</property>
+ <style>
+ <class name="body" />
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImage" id="currentIcon">
+ <property name="name">currentIcon</property>
+ <property name="visible">False</property>
+ <property name="icon-name">emblem-ok-symbolic</property>
+ <property name="margin-start">12</property>
+ <property name="halign">start</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImage" id="locationIcon">
+ <property name="name">locationIcon</property>
+ <property name="visible">False</property>
+ <property name="hexpand">True</property>
+ <property name="icon-name">find-location-symbolic</property>
+ <property name="halign">end</property>
+ </object>
+ </child>
+ </template>
+</interface>
\ No newline at end of file
diff --git a/src/app/main.js b/src/app/main.js
index 3b554ee..52adc9b 100644
--- a/src/app/main.js
+++ b/src/app/main.js
@@ -16,259 +16,26 @@
// with Gnome Weather; if not, write to the Free Software Foundation,
// Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
-pkg.initFormat();
-pkg.initGettext();
-window.ngettext = imports.gettext.ngettext;
-
-pkg.require({ 'Gdk': '3.0',
- 'Gio': '2.0',
- 'GLib': '2.0',
- 'GObject': '2.0',
- 'Gtk': '3.0',
- 'GWeather': '3.0' });
-
-const ByteArray = imports.byteArray;
-const Handy = imports.gi.Handy;
-const Gdk = imports.gi.Gdk;
-const Gio = imports.gi.Gio;
-const GLib = imports.gi.GLib;
-const GObject = imports.gi.GObject;
-const Gtk = imports.gi.Gtk;
-const GWeather = imports.gi.GWeather;
-
-const Util = imports.misc.util;
-const Window = imports.app.window;
-const World = imports.shared.world;
-const CurrentLocationController = imports.app.currentLocationController;
-
-const ShellIntegrationInterface = ByteArray.toString(
- Gio.resources_lookup_data('/org/gnome/shell/ShellWeatherIntegration.xml', 0).get_data());
-
-function initEnvironment() {
- window.getApp = function() {
- return Gio.Application.get_default();
- };
-}
-
-const Application = GObject.registerClass(
- class WeatherApplication extends Gtk.Application {
-
- _init() {
- super._init({ application_id: pkg.name,
- flags: (Gio.ApplicationFlags.CAN_OVERRIDE_APP_ID | Gio.ApplicationFlags.FLAGS_NONE)
});
-
- let name_prefix = '';
- if (pkg.name.endsWith('Devel')) {
- name_prefix = '(Development) ';
- }
- GLib.set_application_name(name_prefix + _("Weather"));
- Gtk.Window.set_default_icon_name(pkg.name);
- }
-
- _onQuit() {
- this.quit();
- }
-
- _onShowLocation(action, parameter) {
- let location = this.world.deserialize(parameter.deep_unpack());
- let win = this._createWindow();
-
- let info = this.model.addNewLocation(location, false);
- win.showInfo(info, false);
- this._showWindowWhenReady(win);
- }
-
- _onShowSearch(action, parameter) {
- let text = parameter.deep_unpack();
- let win = this._createWindow();
-
- win.showSearch(text);
- this._showWindowWhenReady(win);
- }
-
- vfunc_startup() {
- super.vfunc_startup();
- Handy.init();
- // ensure the type before we call to GtkBuilder
- GWeather.LocationEntry;
-
- Util.loadStyleSheet('/org/gnome/Weather/application.css');
-
- Handy.StyleManager
- .get_default()
- .set_color_scheme(Handy.ColorScheme.PREFER_LIGHT);
-
- this.world = GWeather.Location.get_world();
- this.model = new World.WorldModel(this.world, true);
- this.currentLocationController = new CurrentLocationController.CurrentLocationController(this.model);
- this.model.load();
-
-
- this.model.connect('notify::loading', () => {
- if (this.model.loading)
- this.mark_busy();
- else
- this.unmark_busy();
- });
- if (this.model.loading)
- this.mark_busy();
+import 'gi://Gdk?version=4.0';
+import 'gi://Gio?version=2.0';
+import 'gi://GLib?version=2.0';
+import 'gi://GObject?version=2.0';
+import 'gi://Gtk?version=4.0';
+import 'gi://Adw?version=1';
+import 'gi://GWeather?version=4.0';
- let quitAction = new Gio.SimpleAction({
- enabled: true,
- name: 'quit'
- });
- quitAction.connect('activate', () => this._onQuit());
- this.add_action(quitAction);
+import * as system from 'system';
- let showLocationAction = new Gio.SimpleAction({
- enabled: true,
- name: 'show-location',
- parameter_type: new GLib.VariantType('v'),
- });
- showLocationAction.connect('activate', (action, parameter) => {
- this._onShowLocation(action, parameter);
- });
- this.add_action(showLocationAction);
+import Gio from 'gi://Gio';
- let showSearchAction = new Gio.SimpleAction({
- enabled: true,
- name: 'show-search',
- parameter_type: new GLib.VariantType('v'),
- })
- showSearchAction.connect('activate', (action, parameter) => {
- this._onShowSearch(action, parameter);
- });
- this.add_action(showSearchAction);
+import {WeatherApplication} from './application.js';
- let gwSettings = new Gio.Settings({ schema_id: 'org.gnome.GWeather' });
- // we would like to use g_settings_create_action() here
- // but that does not handle correctly the case of 'default'
- // we would also like to use g_settings_bind_with_mapping(), but that
- // function is not introspectable (two callbacks, one destroy notify)
- // so we hand code the behavior we want
- function resolveDefaultTemperatureUnit(unit) {
- unit = GWeather.TemperatureUnit.to_real(unit);
- if (unit == GWeather.TemperatureUnit.CENTIGRADE)
- return new GLib.Variant('s', 'centigrade');
- else if (unit == GWeather.TemperatureUnit.FAHRENHEIT)
- return new GLib.Variant('s', 'fahrenheit');
- else
- return new GLib.Variant('s', 'default');
- }
- let temperatureAction = new Gio.SimpleAction({
- enabled: true,
- name: 'temperature-unit',
- state: resolveDefaultTemperatureUnit(gwSettings.get_enum('temperature-unit')),
- parameter_type: new GLib.VariantType('s')
- });
- temperatureAction.connect('activate', function(action, parameter) {
- action.change_state(parameter);
- })
- temperatureAction.connect('change-state', function(action, state) {
- gwSettings.set_value('temperature-unit', state);
- });
- gwSettings.connect('changed::temperature-unit', function() {
- temperatureAction.state = resolveDefaultTemperatureUnit(gwSettings.get_enum('temperature-unit'));
- });
- this.add_action(temperatureAction);
-
- this.add_accelerator("Escape", "win.selection-mode", new GLib.Variant('b', false));
- this.add_accelerator("<Primary>a", "win.select-all", null);
- this.add_accelerator("<Primary>q", "app.quit", null);
- }
-
- vfunc_dbus_register(conn, path) {
- this._shellIntegration = new ShellIntegration();
- this._shellIntegration.export(conn, path);
- return true;
- }
-
- vfunc_dbus_unregister(conn, path) {
- this._shellIntegration.unexport(conn);
- }
-
- _createWindow() {
- return new Window.MainWindow({ application: this });
- }
-
- _showWindowWhenReady(win) {
- let notifyId;
-
- if (this.model.loading) {
- let timeoutId;
- let model = this.model;
-
- timeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 1000, function() {
- log('Timeout during model load, perhaps the network is not available?');
- model.disconnect(notifyId);
- win.show();
- return false;
- });
- notifyId = this.model.connect('notify::loading', function(model) {
- if (model.loading)
- return;
-
- model.disconnect(notifyId);
- GLib.source_remove(timeoutId);
- win.show();
- });
- } else {
- win.show();
- }
-
- return win;
- }
-
- vfunc_activate() {
- let win = this._createWindow();
- win.showDefault();
- this._showWindowWhenReady(win);
- }
-
- vfunc_shutdown() {
- GWeather.Info.store_cache();
- this.model.saveSettingsNow();
-
- super.vfunc_shutdown();
- }
-});
-
-let ShellIntegration = class ShellIntegration {
- constructor() {
- this._impl = Gio.DBusExportedObject.wrapJSObject(
- ShellIntegrationInterface, this);
-
- this._settings = new Gio.Settings({ schema_id: 'org.gnome.Weather' });
-
- this._settings.connect('changed::automatic-location', () => {
- this._impl.emit_property_changed('AutomaticLocation',
- new GLib.Variant('b', this.AutomaticLocation));
- });
- this._settings.connect('changed::locations', () => {
- this._impl.emit_property_changed('Locations',
- new GLib.Variant('av', this.Locations));
- });
- }
-
- export(connection, path) {
- return this._impl.export(connection, path);
- }
-
- unexport(connection) {
- return this._impl.unexport_from_connection(connection);
- }
-
- get AutomaticLocation() {
- return this._settings.get_boolean('automatic-location');
- }
+pkg.initFormat();
+pkg.initGettext();
- get Locations() {
- return this._settings.get_value('locations').deep_unpack();
- }
+globalThis.ngettext = imports.gettext.ngettext;
+globalThis.getApp = function () {
+ return Gio.Application.get_default();
};
-function main(argv) {
- initEnvironment();
-
- return (new Application()).run(argv);
-}
+new WeatherApplication().run([system.programInvocationName, ...system.programArgs]);
diff --git a/src/app/shell.js b/src/app/shell.js
new file mode 100644
index 0000000..3335454
--- /dev/null
+++ b/src/app/shell.js
@@ -0,0 +1,54 @@
+//
+// Copyright (c) 2012 Giovanni Campagna <scampa giovanni gmail com>
+//
+// Gnome Weather is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by the
+// Free Software Foundation; either version 2 of the License, or (at your
+// option) any later version.
+//
+// Gnome Weather is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+// for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with Gnome Weather; if not, write to the Free Software Foundation,
+// Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+
+const ShellIntegrationInterface = new TextDecoder().decode(
+ Gio.resources_lookup_data('/org/gnome/shell/ShellWeatherIntegration.xml', 0).get_data()
+);
+
+export class ShellIntegration {
+ constructor() {
+ this._impl = Gio.DBusExportedObject.wrapJSObject(
+ ShellIntegrationInterface, this);
+
+ this._settings = new Gio.Settings({ schema_id: 'org.gnome.Weather' });
+
+ this._settings.connect('changed::locations', () => {
+ this._impl.emit_property_changed('Locations',
+ new GLib.Variant('av', this.Locations));
+ });
+ }
+
+ export(connection, path) {
+ return this._impl.export(connection, path);
+ }
+
+ unexport(connection) {
+ return this._impl.unexport_from_connection(connection);
+ }
+
+ get AutomaticLocation() {
+ // We follow whether the user has location services on.
+ return true;
+ }
+
+ get Locations() {
+ return this._settings.get_value('locations').deep_unpack();
+ }
+};
diff --git a/src/app/thermometer.js b/src/app/thermometer.js
index 8356ea4..d040378 100644
--- a/src/app/thermometer.js
+++ b/src/app/thermometer.js
@@ -1,6 +1,7 @@
/* thermometer.js
*
* Copyright 2021 Vitaly Dyachkov <obyknovenius me com>
+ * Copyright 2022 Evan Welsh <contact evanwelsh com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -18,175 +19,146 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
-const GObject = imports.gi.GObject;
-const Gdk = imports.gi.Gdk;
-const Gtk = imports.gi.Gtk;
-const Pango = imports.gi.Pango;
-const Cairo = imports.cairo;
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import Gtk from 'gi://Gtk';
+import Graphene from 'gi://Graphene';
+import Gsk from 'gi://Gsk';
+
+import * as Util from '../misc/util.js';
+
+export class TemperatureRange {
+ dailyLow;
+ dailyHigh;
+ weeklyLow;
+ weeklyHigh;
+
+ constructor({ dailyLow, dailyHigh, weeklyLow, weeklyHigh }) {
+ this.dailyLow = dailyLow;
+ this.dailyHigh = dailyHigh;
+ this.weeklyLow = weeklyLow;
+ this.weeklyHigh = weeklyHigh;
+ }
+}
-const Thermometer = GObject.registerClass({
+GObject.registerClass({
+ CssName: 'WeatherThermometerScale',
Properties: {
- 'adjustment': GObject.ParamSpec.object(
- 'adjustment',
- 'Adjustment',
- 'The GtkAdjustment that contains the current value of this thermometer object',
+ 'range': GObject.ParamSpec.jsobject(
+ 'range',
+ 'range',
+ 'The TemperatureRange instance representing this thermometer scale',
GObject.ParamFlags.READWRITE,
- Gtk.Adjustment,
),
},
- CssName: 'thermometer',
-},class Thermometer extends Gtk.DrawingArea {
-
- _init(params) {
- super._init(params);
-
- const styleContext = this.get_style_context();
-
- const createStyleContext = (selector) => {
- const path = styleContext.get_path().copy();
-
- const pos = path.append_type(GObject.TYPE_NONE);
- path.iter_set_object_name(pos, selector);
+}, class ThermometerScale extends Gtk.Widget {
- const context = Gtk.StyleContext.new();
- context.set_parent(styleContext);
- context.set_path(path);
-
- return context;
- }
-
- this._highStyleContext = createStyleContext('high');
- this._lowStyleContext = createStyleContext('low');
-
- this._radius = 12;
- this._margin = 12;
- }
+ constructor({ range = null, ...params }) {
+ super({
+ vexpand: true,
+ halign: Gtk.Align.FILL,
+ overflow: Gtk.Overflow.HIDDEN,
+ ...params
+ });
- get adjustment() {
- return this._adjustment;
+ this.range = range;
}
- set adjustment(adjustment) {
- this._adjustment = adjustment;
+ vfunc_map() {
+ super.vfunc_map();
- this._updatePangoLayouts(adjustment);
+ this._rangeChangedId = this.connect('notify::range', () => {
+ this.queue_draw();
+ });
}
- vfunc_get_preferred_width() {
- const [highWidth] = this._highLayout.get_pixel_size();
- const [lowWidth] = this._lowLayout.get_pixel_size();
+ vfunc_unmap() {
+ this.disconnect(this._rangeChangedId);
- const width = Math.max(this._radius, highWidth, lowWidth);
- return [width, width];
+ super.vfunc_unmap();
}
- vfunc_get_preferred_height() {
- const [, highHeight] = this._highLayout.get_pixel_size();
- const [, lowHeight] = this._lowLayout.get_pixel_size();
+ vfunc_snapshot(snapshot) {
+ super.vfunc_snapshot(snapshot);
- const height = highHeight + this._maring + lowHeight;
- return [height, height];
- }
-
- _updatePangoLayouts(adjustment) {
- const value = adjustment.get_value();
- const pageSize = adjustment.get_page_size();
-
- const highLabel = Math.round(value + pageSize) + "°";
- this._highLayout = this._createPangoLayout(this._highStyleContext, highLabel);
-
- const lowLabel = Math.round(value) + "°";
- this._lowLayout = this._createPangoLayout(this._lowStyleContext, lowLabel);
- }
-
- _createPangoLayout(styleContext, text) {
- const context = this._createPangoContext(styleContext);
- const layout = Pango.Layout.new(context);
+ if (!this.range) return;
- layout.set_text(text, -1);
+ const { width, height } = this.get_allocation();
- return layout;
- }
+ // Don't render when allocation is shorter than 64
+ if (height < 64) return;
- _createPangoContext(styleContext) {
- const display = this.get_display();
- const context = Gdk.pango_context_get_for_display(display);
+ const { dailyHigh, dailyLow, weeklyHigh, weeklyLow } = this.range;
- const font = styleContext.get_property('font', styleContext.get_state());
- context.set_font_description (font);
+ const scaleFactor = height / (weeklyHigh - weeklyLow);
- return context;
- }
+ const scaleWidth = 24;
+ const scaleHeight = scaleFactor * (dailyHigh - dailyLow);
+ const scaleRadius = 12;
- vfunc_draw(cr) {
- const lower = this._adjustment.get_lower();
- const upper = this._adjustment.get_upper();
- const value = this._adjustment.get_value();
- const pageSize = this._adjustment.get_page_size();
+ const x = (width - scaleWidth) / 2;
+ const y = scaleFactor * (weeklyHigh - dailyHigh);
- const width = this.get_allocated_width();
- const height = this.get_allocated_height();
+ const bounds = new Graphene.Rect();
+ bounds.init(x, y, scaleWidth, scaleHeight);
- const [highWidth, highHeight] = this._highLayout.get_pixel_size();
- const [lowWidth, lowHeight] = this._lowLayout.get_pixel_size();
+ const outline = new Gsk.RoundedRect();
+ outline.init_from_rect(bounds, scaleRadius);
- const radius = this._radius;
- const margin = this._margin;
+ snapshot.push_rounded_clip(outline);
- const maxScaleHeight = height - highHeight - lowHeight - 2 * radius - 2 * margin;
+ const [, warmColor] = this.get_style_context().lookup_color('weather_thermometer_warm_color');
+ const [, coolColor] = this.get_style_context().lookup_color('weather_thermometer_cold_color');
- const factor = maxScaleHeight / (upper - lower);
- const scaleY = highHeight + radius + margin + (upper - value - pageSize) * factor;
- const scaleHeight = pageSize * factor;
+ snapshot.append_linear_gradient(
+ bounds,
+ new Graphene.Point({ x: x + scaleWidth / 2, y: 0 }),
+ new Graphene.Point({ x: x + scaleWidth / 2, y: height }),
+ [
+ new Gsk.ColorStop({ offset: 0.0, color: warmColor }),
+ new Gsk.ColorStop({ offset: 1.0, color: coolColor })
+ ]
+ );
- let highY = 0;
- let lowY = height - lowHeight;
-
- cr.save();
-
- if (maxScaleHeight > 0) {
- this._renderScale(cr, width / 2 - radius, scaleY, radius, scaleHeight);
-
- highY = scaleY - radius - margin - highHeight;
- lowY = scaleY + scaleHeight + radius + margin;
- }
-
- Gtk.render_layout(this._highStyleContext, cr,
- width / 2 - highWidth / 2, highY,
- this._highLayout);
-
- Gtk.render_layout(this._lowStyleContext, cr,
- width / 2 - lowWidth / 2, lowY,
- this._lowLayout);
-
- cr.restore();
-
- return false;
+ snapshot.pop();
}
+});
- _renderScale(cr, x, y, radius, height) {
- const gradient = this._createGradient(y - radius, y + height + radius);
- cr.setSource(gradient);
-
- cr.newSubPath();
- cr.arc(x + radius, y, radius, Math.PI, 0);
- cr.arc(x + radius, y + height, radius, 0, Math.PI);
- cr.closePath();
- cr.fill();
+export const Thermometer = GObject.registerClass({
+ CssName: 'WeatherThermometer',
+ Template: GLib.Uri.resolve_relative(import.meta.url, './thermometer.ui', 0),
+ InternalChildren: ['scale', 'highLabel', 'lowLabel'],
+ Properties: {
+ 'range': GObject.ParamSpec.jsobject(
+ 'range',
+ 'range',
+ 'The TemperatureRange instance representing this thermometer scale',
+ GObject.ParamFlags.READWRITE,
+ ),
+ },
+}, class Thermometer extends Gtk.Widget {
+ constructor({ ...params }) {
+ super(params);
+
+ Object.assign(this.layoutManager, {
+ orientation: Gtk.Orientation.VERTICAL,
+ spacing: 20
+ });
}
- _createGradient(start, end) {
- const pattern = new Cairo.LinearGradient(0, start, 0, end);
-
- const styleContext = this.get_style_context();
+ vfunc_root() {
+ super.vfunc_root();
- const [, warmColor] = styleContext.lookup_color('thermometer_warm_color');
- pattern.addColorStopRGB(0.0, warmColor.red, warmColor.green, warmColor.blue);
+ this.bind_property('range', this._scale, 'range', GObject.BindingFlags.DEFAULT);
- const [, coldColor] = styleContext.lookup_color('thermometer_cold_color');
- pattern.addColorStopRGB(1.0, coldColor.red, coldColor.green, coldColor.blue);
+ this.bind_property_full('range', this._lowLabel, 'label', GObject.BindingFlags.DEFAULT, (_, range) => {
+ return [!!range, Util.formatTemperature(range?.dailyLow) ?? ''];
+ }, null);
- return pattern;
+ this.bind_property_full('range', this._highLabel, 'label', GObject.BindingFlags.DEFAULT, (_, range) => {
+ return [!!range, Util.formatTemperature(range?.dailyHigh) ?? ''];
+ }, null);
}
-
});
+
+Thermometer.set_layout_manager_type(Gtk.BoxLayout);
diff --git a/src/app/thermometer.ui b/src/app/thermometer.ui
new file mode 100644
index 0000000..81af798
--- /dev/null
+++ b/src/app/thermometer.ui
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk" version="4.0"/>
+ <template class="Gjs_Thermometer">
+ <child>
+ <object class="Gjs_ThermometerScale" id="scale">
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="highLabel">
+ <style>
+ <class name="high" />
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="lowLabel">
+ <style>
+ <class name="low" />
+ </style>
+ </object>
+ </child>
+ </template>
+</interface>
\ No newline at end of file
diff --git a/src/app/window.js b/src/app/window.js
index 22d0894..fe8409d 100644
--- a/src/app/window.js
+++ b/src/app/window.js
@@ -16,36 +16,31 @@
// with Gnome Weather; if not, write to the Free Software Foundation,
// Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
-const Handy = imports.gi.Handy;
-const Gio = imports.gi.Gio;
-const GObject = imports.gi.GObject;
-const Gtk = imports.gi.Gtk;
-const GWeather = imports.gi.GWeather;
-
-const City = imports.app.city;
-const CurrentLocationController = imports.app.currentLocationController;
-const World = imports.shared.world;
-const WorldView = imports.app.world;
-const Util = imports.misc.util;
+import Adw from 'gi://Adw';
+import Gio from 'gi://Gio';
+import GObject from 'gi://GObject';
+import Gtk from 'gi://Gtk';
+
+import * as City from './city.js';
+import { WorldContentView } from './world.js';
const Page = {
SEARCH: 0,
CITY: 1
};
-var MainWindow = GObject.registerClass({
+export const MainWindow = GObject.registerClass({
Template: 'resource:///org/gnome/Weather/window.ui',
InternalChildren: ['header', 'refreshRevealer', 'refresh', 'forecastStackSwitcher', 'stack',
- 'titleStack', 'searchView', 'searchEntry', 'forecastStackSwitcherBar']
-}, class MainWindow extends Handy.ApplicationWindow {
-
- _init(params) {
- super._init(params);
+ 'titleStack', 'searchButton', 'searchView', 'forecastStackSwitcherBar']
+}, class MainWindow extends Adw.ApplicationWindow {
+ constructor(params) {
+ super(params);
this._world = this.application.world;
this.currentInfo = null;
this._currentPage = Page.SEARCH;
- this._pageWidgets = [[],[]];
+ this._pageWidgets = [[], []];
let aboutAction = new Gio.SimpleAction({
enabled: true,
@@ -54,13 +49,6 @@ var MainWindow = GObject.registerClass({
aboutAction.connect('activate', () => this._showAbout());
this.add_action(aboutAction);
- let closeAction = new Gio.SimpleAction({
- enabled: true,
- name: 'close'
- });
- closeAction.connect('activate', () => this._close());
- this.add_action(closeAction);
-
let refreshAction = new Gio.SimpleAction({
enabled: true,
name: 'refresh'
@@ -72,43 +60,40 @@ var MainWindow = GObject.registerClass({
this._searchView.icon_name = pkg.name;
- this._searchEntry.connect('notify::location', (entry) => {
- this._searchLocationChanged(entry);
+ this._worldView = new WorldContentView(this.application, this, {
+ align: Gtk.Align.CENTER,
});
+ this._searchButton.set_popover(this._worldView);
this._pageWidgets[Page.CITY].push(this._refresh);
this._cityView = new City.WeatherView(this.application, this,
- { hexpand: true, vexpand: true });
- this._stack.add_named(this._cityView, 'city');
+ { hexpand: true, vexpand: true });
- this._forecastStackSwitcher.set_stack(this._cityView.getInfoPage().getForecastStack());
+ this._stack.add_named(this._cityView, 'city');
- this._forecastStackSwitcherBar.set_stack(this._cityView.getInfoPage().getForecastStack());
+ this._forecastStackSwitcher.stack = this._cityView.getForecastStack();
+ this._forecastStackSwitcherBar.stack = this._cityView.getForecastStack();
this._stack.set_visible_child(this._searchView);
for (let i = 0; i < this._pageWidgets[Page.CITY].length; i++)
this._pageWidgets[Page.CITY][i].hide();
- if (pkg.name.endsWith('Devel')) {
- let ctx = this.get_style_context();
- ctx.add_class('devel')
- }
-
this._showingDefault = false;
- this.show_all();
}
- update() {
- this._cityView.update();
+ vfunc_unroot() {
+ this._cityView.unparent();
+ this._cityView = null;
+ this._worldView.unparent();
+ this._worldView = null;
+
+ super.vfunc_unroot();
}
- _searchLocationChanged(entry) {
- if (entry.location) {
- let info = this._model.addNewLocation(entry.location, false);
- this.showInfo(info, false);
- }
+ update() {
+ this._cityView.update();
}
_goToPage(page) {
@@ -116,9 +101,7 @@ var MainWindow = GObject.registerClass({
this._pageWidgets[this._currentPage][i].hide();
for (let i = 0; i < this._pageWidgets[page].length; i++) {
- let widget = this._pageWidgets[page][i];
- if (!widget.no_show_all)
- this._pageWidgets[page][i].show();
+ this._pageWidgets[page][i].show();
}
this._currentPage = page;
@@ -128,47 +111,29 @@ var MainWindow = GObject.registerClass({
this._showingDefault = true;
this._refreshRevealer.reveal_child = false;
let clc = this.application.currentLocationController;
- let autoLocation = clc.autoLocation;
- let currentLocation = clc.currentLocation;
- if (currentLocation)
- this.showInfo(this._model.getCurrentLocation(), false);
- else if (autoLocation != CurrentLocationController.AutoLocation.ENABLED)
- this.showInfo(this._model.getRecent(), false);
+
+ let mostRecent = this._model.getRecent();
+ if (mostRecent)
+ this.showInfo(mostRecent);
+ else
+ this.showSearch();
}
showSearch(text) {
this._showingDefault = false;
this._refreshRevealer.reveal_child = true;
- this._cityView.setTimeVisible(false);
this._stack.set_visible_child(this._searchView);
this._goToPage(Page.SEARCH);
- this._searchEntry.text = text;
- if (text.length > 0)
- this._searchEntry.get_completion().complete();
}
- showInfo(info, isCurrentLocation) {
+ updateCurrentLocation(info) { }
+
+ showInfo(info) {
if (!info) {
- if (isCurrentLocation && this._showingDefault)
- this.showDefault();
+ this.showDefault();
return;
}
- /*
- * Only show location updates if we have no loaded info and no
- * search text or if we are currently showing the previous
- * current location.
- */
- if (isCurrentLocation) {
- if (this._currentPage == Page.CITY) {
- if (!this._cityView.info._isCurrentLocation)
- return;
- } else if (this._currentPage == Page.SEARCH) {
- if (this._searchEntry.text.length > 0)
- return;
- }
- }
-
this._showingDefault = false;
this._refreshRevealer.reveal_child = true;
this.currentInfo = info;
@@ -179,37 +144,34 @@ var MainWindow = GObject.registerClass({
}
_showAbout() {
- let artists = [ 'Jakub Steiner <jimmac gmail com>',
- 'Pink Sherbet Photography (D. Sharon Pruitt)',
- 'Elliott Brown',
- 'Analogick',
- 'DBduo Photography (Daniel R. Blume)',
- 'davharuk',
- 'Tech Haven Ministries',
- 'Jim Pennucci' ];
+ let artists = ['Jakub Steiner <jimmac gmail com>',
+ 'Pink Sherbet Photography (D. Sharon Pruitt)',
+ 'Elliott Brown',
+ 'Analogick',
+ 'DBduo Photography (Daniel R. Blume)',
+ 'davharuk',
+ 'Tech Haven Ministries',
+ 'Jim Pennucci'];
let name_prefix = '';
- if (pkg.name.endsWith('Devel')) {
- name_prefix = '(Development) ';
- }
let copyright = 'Copyright 2013-2015 The Weather Developers';
let attribution = this._cityView.info ? this._cityView.info.get_attribution() : '';
copyright += attribution ? '\n' + attribution : '';
let aboutDialog = new Gtk.AboutDialog(
- { artists: artists,
- authors: [ 'Giovanni Campagna <gcampagna src gnome org>' ],
- translator_credits: _("translator-credits"),
- program_name: name_prefix + _("Weather"),
- comments: _("A weather application"),
- license_type: Gtk.License.GPL_2_0,
- logo_icon_name: pkg.name,
- version: pkg.version,
- website: 'https://wiki.gnome.org/Apps/Weather',
- wrap_license: true,
- modal: true,
- transient_for: this,
- use_header_bar: true
+ {
+ artists: artists,
+ authors: ['Giovanni Campagna <gcampagna src gnome org>'],
+ translator_credits: _("translator-credits"),
+ program_name: name_prefix + _("Weather"),
+ comments: _("A weather application"),
+ license_type: Gtk.License.GPL_2_0,
+ logo_icon_name: pkg.name,
+ version: pkg.version,
+ website: 'https://wiki.gnome.org/Apps/Weather',
+ wrap_license: true,
+ modal: true,
+ transient_for: this
});
// HACK: we need to poke into gtkaboutdialog internals
@@ -221,12 +183,5 @@ var MainWindow = GObject.registerClass({
copyrightLabel.show();
aboutDialog.show();
- aboutDialog.connect('response', function() {
- aboutDialog.destroy();
- });
- }
-
- _close() {
- this.destroy();
}
});
diff --git a/src/app/world.js b/src/app/world.js
index 1815629..90ef713 100644
--- a/src/app/world.js
+++ b/src/app/world.js
@@ -17,225 +17,129 @@
// with Gnome Weather; if not, write to the Free Software Foundation,
// Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
-const GLib = imports.gi.GLib;
-const GObject = imports.gi.GObject;
-const Gtk = imports.gi.Gtk;
-const GWeather = imports.gi.GWeather;
+import GObject from 'gi://GObject';
+import Gtk from 'gi://Gtk';
-const CurrentLocationController = imports.app.currentLocationController;
-const Util = imports.misc.util;
+import * as Util from '../misc/util.js';
+import { LocationRow } from './locationRow.js';
-
-var WorldContentView = GObject.registerClass(
- class WorldContentView extends Gtk.Popover {
-
- _init(application, window, params) {
- super._init(Object.assign({
+export class WorldContentView extends Gtk.Popover {
+ constructor(application, window, { align, ...params } = {}) {
+ super({
+ ...params,
hexpand: false,
- vexpand: false
- }, params));
+ halign: align,
+ vexpand: false,
+ hasArrow: false,
+ });
- this.get_accessible().accessible_name = _("World view");
+ this.add_css_class('weather-popover');
+ this.update_property([Gtk.AccessibleProperty.LABEL], [_("World view")]);
let builder = new Gtk.Builder();
builder.add_from_resource('/org/gnome/Weather/places-popover.ui');
- let grid = builder.get_object('popover-grid');
- this.add(grid);
+ const box = builder.get_object('popoverBox');
+ this.set_child(box);
+
+ this._searchListView = builder.get_object('search-list-view');
+ this._searchListScrollWindow = builder.get_object('search-list-scroll-window');
this.model = application.model;
this._window = window;
+ this._listboxScrollWindow = builder.get_object('locations-list-scroll-window');
this._listbox = builder.get_object('locations-list-box');
- this._listbox.set_header_func((row, previous) => {
- let hasHeader = row.get_header() != null;
- let shouldHaveHeader = previous != null;
- if (hasHeader != shouldHaveHeader) {
- if (shouldHaveHeader)
- row.set_header(new Gtk.Separator());
- else
- row.set_header(null);
- }
+ this._listbox.bind_model(this.model, (info) => {
+ return this._buildLocation(this.model, info);
});
- let locationEntry = builder.get_object('location-entry');
- locationEntry.connect('notify::location', (entry) => this._locationChanged(entry));
-
- this.connect('show', () => {
- locationEntry.grab_focus();
- });
+ this._locationEntry = builder.get_object('location-entry');
- let autoLocStack = builder.get_object('auto-location-stack');
- let autoLocSwitch = builder.get_object('auto-location-switch');
- this._currentLocationController = application.currentLocationController;
+ this._locationEntry.setListView(this._searchListView);
+ this._locationEntry.connect('search-updated', (entry, text) => {
+ if (!text) {
+ this._stackPopover.set_visible_child(this._listboxScrollWindow);
+ entry.text = '';
+ return;
+ }
- if(this._currentLocationController.autoLocation == CurrentLocationController.AutoLocation.ENABLED) {
- autoLocStack.visible_child_name = 'locating-label';
- } else {
- autoLocStack.visible_child_name = 'auto-location-switch-grid';
- autoLocSwitch.active = false;
- autoLocSwitch.sensitive = (this._currentLocationController.autoLocation !=
CurrentLocationController.AutoLocation.NOT_AVAILABLE);
- }
+ this._stackPopover.set_visible_child(this._searchListScrollWindow);
+ });
+ this._locationEntry.connect('notify::location', (entry) => {
+ const location = entry.location;
+ entry.text = '';
- let handlerId = autoLocSwitch.connect('notify::active', () => {
- this._currentLocationController.setAutoLocation(autoLocSwitch.active);
+ this._locationChanged(location);
- if (autoLocSwitch.active && !this.model.addedCurrentLocation)
- autoLocStack.visible_child_name = 'locating-label';
+ this._stackPopover.set_visible_child(this._listboxScrollWindow);
- this.hide();
+ // Defer the popdown to allow the stack to re-render
+ imports.mainloop.idle_add(() => {
+ this.popdown();
+ return false;
+ });
});
- this._listbox.connect('row-activated', (listbox, row) => {
- this._window.showInfo(row._info, false);
- this.model.moveLocationToFront(row._info);
- this.hide();
+ this.connect('show', () => {
+ this._locationEntry.grab_focus();
});
- this.model.connect('current-location-changed', (model, info) => {
- autoLocStack.visible_child_name = 'auto-location-switch-grid';
- GObject.signal_handler_block(autoLocSwitch, handlerId);
- autoLocSwitch.active = (this._currentLocationController.autoLocation ==
CurrentLocationController.AutoLocation.ENABLED);
- autoLocSwitch.sensitive = (this._currentLocationController.autoLocation !=
CurrentLocationController.AutoLocation.NOT_AVAILABLE);
- GObject.signal_handler_unblock(autoLocSwitch, handlerId);
+ this._currentLocationController = application.currentLocationController;
- this._window.showInfo(info, true);
+ this._listbox.connect('row-activated', (listbox, row) => {
+ if (row._info)
+ this.model.setSelectedLocation(row._info);
+
+ // Defer the popdown to allow the stack to re-render
+ imports.mainloop.idle_add(() => {
+ this.popdown();
+ return false;
+ });
});
- this._stackPopover = builder.get_object('popover-stack');
- this._listbox.set_filter_func((row) => this._filterListbox(row));
-
- this.model.connect('location-added', (model, info, is_current) => {
- this._onLocationAdded(model, info, is_current);
+ this.model.connect('selected-location-changed', (_, info) => {
+ this._window.showInfo(info);
});
- this.model.connect('location-removed', (model, info) => {
- this._onLocationRemoved(model, info);
- });
+ this._stackPopover = builder.get_object('popover-stack');
+ this._stackPopover.set_visible_child(this._listboxScrollWindow);
this._currentLocationAdded = false;
- let list = this.model.getAll();
- for (let i = list.length - 1; i >= 0; i--)
- this._onLocationAdded(this.model, list[i], list[i]._isCurrentLocation);
- }
- refilter() {
- this._listbox.invalidate_filter();
}
- _syncStackPopover() {
- if (this.model.length == 1)
- this._stackPopover.set_visible_child_name("search-grid");
- else
- this._stackPopover.set_visible_child_name("locations-grid");
+ vfunc_unroot() {
+ this._listbox.bind_model(null, null);
+
+ this._window = null;
+
+ super.vfunc_unroot();
}
- _filterListbox(row) {
- return this._window.currentInfo == null ||
- row._info != this._window.currentInfo;
+ refilter() {
+ this._listbox.invalidate_filter();
}
- _locationChanged(entry) {
- if (entry.location) {
- let info = this.model.addNewLocation(entry.location, false);
- this._window.showInfo(info, false);
- this.hide();
- entry.location = null;
+ _locationChanged(location) {
+ if (location) {
+ let info = this.model.addNewLocation(location);
+ this._window.showInfo(info);
}
}
- _onLocationAdded(model, info, isCurrentLocation) {
- let location = info.location;
+ _buildLocation(model, info) {
+ if (!info) return null;
- let grid = new Gtk.Grid({ orientation: Gtk.Orientation.HORIZONTAL,
- column_spacing: 12,
- margin: 12,
- visible: true });
-
- let name = location.get_city_name();
- let locationGrid = new Gtk.Grid({ orientation: Gtk.Orientation.HORIZONTAL,
- column_spacing: 12,
- halign: Gtk.Align.START,
- hexpand: true,
- visible: true });
- let locationLabel = new Gtk.Label({ label: name,
- use_markup: true,
- halign: Gtk.Align.START,
- visible: true });
- locationGrid.attach(locationLabel, 0, 0, 1, 1);
- grid.attach(locationGrid, 0, 0, 1, 1);
-
- let tempLabel = new Gtk.Label({ use_markup: true,
- halign: Gtk.Align.END,
- margin_start: 12,
- visible: true });
- grid.attach(tempLabel, 1, 0, 1, 1);
-
- if (isCurrentLocation) {
- let image = new Gtk.Image({ icon_size: Gtk.IconSize.LARGE_TOOLBAR,
- icon_name: 'mark-location-symbolic',
- use_fallback: true,
- halign: Gtk.Align.START,
- visible: true });
- locationGrid.attach(image, 1, 0, 1, 1);
- }
+ let location = info.location;
- let image = new Gtk.Image({ icon_size: Gtk.IconSize.LARGE_TOOLBAR,
- use_fallback: true,
- halign: Gtk.Align.END,
- visible: true });
- grid.attach(image, 2, 0, 1, 1);
+ const [name, countryName = ''] = Util.getNameAndCountry(location);
- let row = new Gtk.ListBoxRow({ visible: true });
- row.add(grid);
+ const grid = new LocationRow({ name, countryName, isSelected: model.isSelectedLocation(info),
isCurrentLocation: model.isCurrentLocation(info) });
+ const row = new Gtk.ListBoxRow({ child: grid });
row._info = info;
- row._isCurrentLocation = isCurrentLocation;
-
- if (isCurrentLocation) {
- if (this._currentLocationAdded) {
- let row0 = this._listbox.get_row_at_index(0);
- if (row0)
- row0.destroy();
- }
-
- this._currentLocationAdded = true;
- this._listbox.insert(row, 0);
- } else {
- if (this._currentLocationAdded)
- this._listbox.insert(row, 1);
- else
- this._listbox.insert(row, 0);
- }
-
- if (info._updatedId)
- return;
-
- info._updatedId = info.connect('updated', (info) => {
- tempLabel.label = info.get_temp_summary();
- image.icon_name = info.get_symbolic_icon_name();
- });
-
- this._syncStackPopover();
- this._currentLocationController.currentLocation = info
+ return row;
}
+};
- _onLocationRemoved(model, info) {
- let rows = this._listbox.get_children();
-
- for (let row of rows) {
- if (row._info == info) {
- row.destroy();
- break;
- }
- }
-
- if (info._updatedId) {
- info.disconnect(info._updatedId);
- info._updatedId = 0;
- }
- if (info._isCurrentLocation)
- this._currentLocationAdded = false;
-
- this._syncStackPopover();
- }
-});
+GObject.registerClass(WorldContentView);
diff --git a/src/misc/util.js b/src/misc/util.js
index bd00d83..28207f0 100644
--- a/src/misc/util.js
+++ b/src/misc/util.js
@@ -24,13 +24,12 @@
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-const Gdk = imports.gi.Gdk;
-const Gio = imports.gi.Gio;
-const GLib = imports.gi.GLib;
-const Gtk = imports.gi.Gtk;
-const Handy = imports.gi.Handy;
-const System = imports.system;
-const GWeather = imports.gi.GWeather;
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import Gtk from 'gi://Gtk';
+import GWeather from 'gi://GWeather';
+
+import * as System from 'system';
function loadUI(resourcePath, objects) {
let ui = new Gtk.Builder();
@@ -44,57 +43,23 @@ function loadUI(resourcePath, objects) {
return ui;
}
-function loadStyleSheet(resource) {
- let provider = new Gtk.CssProvider();
- provider.load_from_file(Gio.File.new_for_uri('resource://' + resource));
- Gtk.StyleContext.add_provider_for_screen(Gdk.Screen.get_default(),
- provider,
- Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
-}
-
function arrayEqual(one, two) {
- if (one.length != two.length)
+ if (one.length !== two.length)
return false;
- for (let i = 0; i < one.length; i++)
- if (one[i] != two[i])
- return false;
-
- return true;
+ return one.every((a, i) => a === two[i]);
}
-function getSettings(schemaId, path) {
- const GioSSS = Gio.SettingsSchemaSource;
- let schemaSource;
-
- if (!pkg.moduledir.startsWith('resource://')) {
- // Running from the source tree
- schemaSource = GioSSS.new_from_directory(pkg.pkgdatadir,
- GioSSS.get_default(),
- false);
- } else {
- schemaSource = GioSSS.get_default();
- }
+function getSettings(schemaId) {
+ const schemaSource = Gio.SettingsSchemaSource.get_default();
+ const schemaObj = schemaSource.lookup(schemaId, true);
- let schemaObj = schemaSource.lookup(schemaId, true);
if (!schemaObj) {
- log('Missing GSettings schema ' + schemaId);
+ log(`Missing GSettings schema ${schemaId}`);
System.exit(1);
}
- if (path === undefined)
- return new Gio.Settings({ settings_schema: schemaObj });
- else
- return new Gio.Settings({ settings_schema: schemaObj,
- path: path });
-}
-
-function loadIcon(iconName, size) {
- let theme = Gtk.IconTheme.get_default();
-
- return theme.load_icon(iconName,
- size,
- Gtk.IconLookupFlags.GENERIC_FALLBACK);
+ return new Gio.Settings({ settings_schema: schemaObj });
}
function getWeatherConditions(info) {
@@ -104,13 +69,6 @@ function getWeatherConditions(info) {
return conditions;
}
-function isCdm(c) {
- return ((c >= 0x0300 && c <= 0x036F) ||
- (c >= 0x1DC0 && c <= 0x1DFF) ||
- (c >= 0x20D0 && c <= 0x20FF) ||
- (c >= 0xFE20 && c <= 0xFE2F));
-}
-
function normalizeCasefoldAndUnaccent(str) {
// The one and only!
// Travelled all over gnome, from tracker to gnome-shell to gnome-control-center,
@@ -131,8 +89,8 @@ function normalizeCasefoldAndUnaccent(str) {
}
function getTemperature(info) {
- let [ok1, ] = info.get_value_temp_min(GWeather.TemperatureUnit.DEFAULT);
- let [ok2, ] = info.get_value_temp_max(GWeather.TemperatureUnit.DEFAULT);
+ let [ok1,] = info.get_value_temp_min(GWeather.TemperatureUnit.DEFAULT);
+ let [ok2,] = info.get_value_temp_max(GWeather.TemperatureUnit.DEFAULT);
if (ok1 && ok2) {
/* TRANSLATORS: this is the temperature string, minimum and maximum.
@@ -160,37 +118,37 @@ function easeOutCubic(value) {
function getNight(date) {
return GLib.DateTime.new_local(date.get_year(),
- date.get_month(),
- date.get_day_of_month(),
- 2, 0, 0);
+ date.get_month(),
+ date.get_day_of_month(),
+ 2, 0, 0);
}
function getMorning(date) {
return GLib.DateTime.new_local(date.get_year(),
- date.get_month(),
- date.get_day_of_month(),
- 7, 0, 0);
+ date.get_month(),
+ date.get_day_of_month(),
+ 7, 0, 0);
}
function getDay(date) {
return GLib.DateTime.new_local(date.get_year(),
- date.get_month(),
- date.get_day_of_month(),
- 12, 0, 0);
+ date.get_month(),
+ date.get_day_of_month(),
+ 12, 0, 0);
}
function getAfternoon(date) {
return GLib.DateTime.new_local(date.get_year(),
- date.get_month(),
- date.get_day_of_month(),
- 17, 0, 0);
+ date.get_month(),
+ date.get_day_of_month(),
+ 17, 0, 0);
}
function getEvening(date) {
return GLib.DateTime.new_local(date.get_year(),
- date.get_month(),
- date.get_day_of_month(),
- 22, 0, 0);
+ date.get_month(),
+ date.get_day_of_month(),
+ 22, 0, 0);
}
function getDateTime(info) {
@@ -203,13 +161,50 @@ function getTemp(info) {
return temp;
}
+function formatTemperature(value) {
+ return typeof value === 'number' ? `${Math.round(value).toFixed(0)}°` : undefined;
+};
+
function getTempString(info) {
- let [ok, temp] = info.get_value_temp(GWeather.TemperatureUnit.DEFAULT);
- if (!ok)
- return "--";
- return Math.round(temp) + "°";
+ try {
+ let [, temp] = info.get_value_temp(GWeather.TemperatureUnit.DEFAULT);
+ return formatTemperature(temp);
+ } catch {
+ return "";
+ }
}
-function isDarkTheme() {
- return Handy.StyleManager.get_default().dark;
-}
+/**
+ * @returns {[string] | [string, string]}
+ */
+function getNameAndCountry(location) {
+ let country = location.get_parent();
+ while (country && country.get_level() > GWeather.LocationLevel.COUNTRY)
+ country = country.get_parent();
+
+ if (country)
+ return [location.get_name(), country.get_name()];
+ else
+ return [location.get_name()];
+}
+
+export {
+ loadUI,
+ formatTemperature,
+ getDateTime,
+ getTemp,
+ getEvening,
+ getAfternoon,
+ getTempString,
+ getNight,
+ normalizeCasefoldAndUnaccent,
+ arrayEqual,
+ getSettings,
+ getMorning,
+ getTemperature,
+ getDay,
+ easeOutCubic,
+ getEnabledProviders,
+ getWeatherConditions,
+ getNameAndCountry
+}
\ No newline at end of file
diff --git a/src/org.gnome.Weather.BackgroundService.in b/src/org.gnome.Weather.BackgroundService.in
index 9e44933..53c9ccc 100755
--- a/src/org.gnome.Weather.BackgroundService.in
+++ b/src/org.gnome.Weather.BackgroundService.in
@@ -3,4 +3,10 @@ imports.package.init({ name: "@APP_ID@",
version: "@VERSION@",
prefix: "@prefix@",
libdir: "@libdir@" });
-imports.package.run(imports.service.main);
+
+import('resource:///org/gnome/Weather/js/service/main.js').then(({ main }) => {
+ main([imports.system.programInvocationName, ...imports.system.programArgs]);
+}).catch(error => {
+ console.error(error);
+ System.exit(1);
+});
diff --git a/src/org.gnome.Weather.BackgroundService.src.gresource.xml.in
b/src/org.gnome.Weather.BackgroundService.src.gresource.xml.in
index 74aedf0..d4dcd96 100644
--- a/src/org.gnome.Weather.BackgroundService.src.gresource.xml.in
+++ b/src/org.gnome.Weather.BackgroundService.src.gresource.xml.in
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
- <gresource prefix="/org/gnome/Weather@profile@/BackgroundService/js">
+ <gresource prefix="/org/gnome/Weather/BackgroundService/js">
<file>service/main.js</file>
<file>service/searchProvider.js</file>
<file>misc/util.js</file>
diff --git a/src/org.gnome.Weather.in b/src/org.gnome.Weather.in
index 9c26b18..be7353d 100755
--- a/src/org.gnome.Weather.in
+++ b/src/org.gnome.Weather.in
@@ -3,4 +3,8 @@ imports.package.init({ name: "@APP_ID@",
version: "@VERSION@",
prefix: "@prefix@",
libdir: "@libdir@" });
-imports.package.run(imports.app.main);
+
+import(`resource:///org/gnome/Weather/js/app/main.js`).catch(error => {
+ console.error(error);
+ imports.system.exit(1);
+});
diff --git a/src/org.gnome.Weather.src.gresource.xml.in b/src/org.gnome.Weather.src.gresource.xml.in
index 9613401..1631802 100644
--- a/src/org.gnome.Weather.src.gresource.xml.in
+++ b/src/org.gnome.Weather.src.gresource.xml.in
@@ -1,12 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
- <gresource prefix="/org/gnome/Weather@profile@/js">
+ <gresource prefix="/org/gnome/Weather/js">
+ <file>app/application.js</file>
<file>app/city.js</file>
<file>app/currentLocationController.js</file>
<file>app/hourlyForecast.js</file>
+ <file>app/locationRow.js</file>
+ <file>app/locationRow.ui</file>
<file>app/thermometer.js</file>
+ <file>app/thermometer.ui</file>
<file>app/dailyForecast.js</file>
+ <file>app/entry.js</file>
<file>app/main.js</file>
+ <file>app/shell.js</file>
<file>app/window.js</file>
<file>app/world.js</file>
<file>misc/util.js</file>
diff --git a/src/service/main.js b/src/service/main.js
index 113bf93..b0ace5e 100644
--- a/src/service/main.js
+++ b/src/service/main.js
@@ -21,16 +21,15 @@ pkg.initFormat();
pkg.require({ 'Gio': '2.0',
'GLib': '2.0',
'GObject': '2.0',
- 'GWeather': '3.0' });
+ 'GWeather': '4.0' });
-const Gio = imports.gi.Gio;
-const GLib = imports.gi.GLib;
-const GObject = imports.gi.GObject;
-const GWeather = imports.gi.GWeather;
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import GWeather from 'gi://GWeather';
-const Util = imports.misc.util;
-const SearchProvider = imports.service.searchProvider;
-const World = imports.shared.world;
+import * as SearchProvider from './searchProvider.js';
+import * as World from '../shared/world.js';
function initEnvironment() {
window.getApp = function() {
@@ -41,13 +40,13 @@ function initEnvironment() {
const BackgroundService = GObject.registerClass(
class WeatherBackgroundService extends Gio.Application {
- _init() {
- super._init({ application_id: pkg.name,
+ constructor() {
+ super({ application_id: pkg.name,
flags: Gio.ApplicationFlags.IS_SERVICE,
inactivity_timeout: 60000 });
GLib.set_application_name(_("Weather"));
- this._searchProvider = new SearchProvider.SearchProvider(this);
+ this._searchProvider = new SearchProvider.WeatherSearchProvider(this);
if (!pkg.moduledir.startsWith('resource://'))
this.debug = true;
@@ -106,7 +105,7 @@ const BackgroundService = GObject.registerClass(
}
});
-function main(argv) {
+export function main(argv) {
initEnvironment();
return (new BackgroundService()).run(argv);
diff --git a/src/service/searchProvider.js b/src/service/searchProvider.js
index 8936afb..e302771 100644
--- a/src/service/searchProvider.js
+++ b/src/service/searchProvider.js
@@ -16,26 +16,26 @@
// with Gnome Weather; if not, write to the Free Software Foundation,
// Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
-const ByteArray = imports.byteArray;
-const Gio = imports.gi.Gio;
-const GLib = imports.gi.GLib;
-const GWeather = imports.gi.GWeather;
-const Lang = imports.lang;
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import GWeather from 'gi://GWeather';
-const Util = imports.misc.util;
-const World = imports.shared.world;
+import * as Util from '../misc/util.js';
-const SearchProviderInterface =
ByteArray.toString(Gio.resources_lookup_data('/org/gnome/shell/ShellSearchProvider2.xml', 0).get_data());
+
+const SearchProviderInterface = new TextDecoder().decode(
+ Gio.resources_lookup_data('/org/gnome/shell/ShellSearchProvider2.xml', 0).get_data()
+);
function getCountryName(location) {
while (location &&
- location.get_level() > GWeather.LocationLevel.COUNTRY)
+ location.get_level() > GWeather.LocationLevel.COUNTRY)
location = location.get_parent();
return location.get_name();
}
-var SearchProvider = class WeatherSearchProvider {
+export class WeatherSearchProvider {
constructor(application) {
this._app = application;
@@ -180,11 +180,12 @@ var SearchProvider = class WeatherSearchProvider {
It's the current weather conditions followed by the temperature,
like "Clear sky, 14 °C" */
let summary = _("%s, %s").format(conditions, info.get_temp());
- ret.push({ name: new GLib.Variant('s', name),
- id: new GLib.Variant('s', identifiers[i]),
- description: new GLib.Variant('s', summary),
- icon: (new Gio.ThemedIcon({ name: info.get_icon_name() })).serialize()
- });
+ ret.push({
+ name: new GLib.Variant('s', name),
+ id: new GLib.Variant('s', identifiers[i]),
+ description: new GLib.Variant('s', summary),
+ icon: (new Gio.ThemedIcon({ name: info.get_icon_name() })).serialize()
+ });
}
this._app.release();
@@ -193,7 +194,7 @@ var SearchProvider = class WeatherSearchProvider {
}
_getPlatformData(timestamp) {
- return {'desktop-startup-id': new GLib.Variant('s', '_TIME' + timestamp) };
+ return { 'desktop-startup-id': new GLib.Variant('s', '_TIME' + timestamp) };
}
_activateAction(action, parameter, timestamp) {
@@ -204,27 +205,24 @@ var SearchProvider = class WeatherSearchProvider {
wrappedParam = [];
profile = '';
- if (pkg.name.endsWith('Devel')) {
- profile = 'Devel';
- }
Gio.DBus.session.call(pkg.name,
- '/org/gnome/Weather' + profile,
- 'org.freedesktop.Application',
- 'ActivateAction',
- new GLib.Variant('(sava{sv})', [action, wrappedParam,
- this._getPlatformData(timestamp)]),
- null,
- Gio.DBusCallFlags.NONE,
- -1, null, (connection, result) => {
- try {
- connection.call_finish(result);
- } catch(e) {
- log('Failed to launch application: ' + e);
- }
-
- this._app.release();
- });
+ '/org/gnome/Weather' + profile,
+ 'org.freedesktop.Application',
+ 'ActivateAction',
+ new GLib.Variant('(sava{sv})', [action, wrappedParam,
+ this._getPlatformData(timestamp)]),
+ null,
+ Gio.DBusCallFlags.NONE,
+ -1, null, (connection, result) => {
+ try {
+ connection.call_finish(result);
+ } catch (e) {
+ log('Failed to launch application: ' + e);
+ }
+
+ this._app.release();
+ });
}
ActivateResult(id, terms, timestamp) {
diff --git a/src/shared/world.js b/src/shared/world.js
index 081edf0..ff2a77f 100644
--- a/src/shared/world.js
+++ b/src/shared/world.js
@@ -16,25 +16,25 @@
// with Gnome Weather; if not, write to the Free Software Foundation,
// Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
-const GLib = imports.gi.GLib;
-const GObject = imports.gi.GObject;
-const GWeather = imports.gi.GWeather;
+import GObject from 'gi://GObject';
+import GLib from 'gi://GLib';
+import Gio from 'gi://Gio';
+import GWeather from 'gi://GWeather';
-const Util = imports.misc.util;
+import * as Util from '../misc/util.js';
-var WorldModel = GObject.registerClass({
+export const WorldModel = GObject.registerClass({
Signals: {
- 'current-location-changed': { param_types: [ GWeather.Info ] },
- 'location-added': { param_types: [ GWeather.Info, GObject.Boolean ] },
- 'location-removed': { param_types: [ GWeather.Info ] }
+ 'selected-location-changed': { param_types: [GWeather.Info] },
},
Properties: {
'loading': GObject.ParamSpec.boolean('loading', '', '', GObject.ParamFlags.READABLE, false)
},
+ Implements: [Gio.ListModel]
}, class WorldModel extends GObject.Object {
- _init(world, enableGtk) {
- super._init();
+ constructor(world) {
+ super();
this._world = world;
@@ -44,23 +44,39 @@ var WorldModel = GObject.registerClass({
this._loadingCount = 0;
this._currentLocationInfo = null;
+ this._selectedLocation = null;
this._infoList = [];
+ this.getAll();
}
get length() {
- return this._infoList.length + (this._currentLocationInfo ? 1 : 0);
+ return this._allInfos.length
}
getAll() {
+ // Ensure the current location and selected location are returned first...
+ const infos = [...this._infoList].filter(info => !this.isCurrentLocation(info) &&
!this.isSelectedLocation(info));
+
if (this._currentLocationInfo)
- return [this._currentLocationInfo].concat(this._infoList);
- else
- return [].concat(this._infoList);
+ infos.unshift(this._currentLocationInfo);
+
+ if (this._selectedLocation && this._currentLocationInfo !== this._selectedLocation) {
+ infos.unshift(this._selectedLocation);
+ }
+
+ this._allInfos = infos;
+ return infos;
}
getAtIndex(index) {
- if (this._currentLocationInfo) {
+ if (this._selectedLocation) {
if (index == 0)
+ return this._selectedLocation;
+ else
+ index--;
+ }
+ if (this._currentLocationInfo) {
+ if (index == 1)
return this._currentLocationInfo;
else
index--;
@@ -74,15 +90,11 @@ var WorldModel = GObject.registerClass({
}
currentLocationChanged(location) {
- if (this._currentLocationInfo)
- this._removeLocationInternal(this._currentLocationInfo, false);
-
- let info;
- if (location)
- info = this.addNewLocation(location, true);
- else
- info = null;
- this.emit('current-location-changed', info);
+ if (location) {
+ this._currentLocationInfo = this.buildInfo(location);
+ this.addCurrentLocation(this._currentLocationInfo);
+ this.#invalidate();
+ }
}
getRecent() {
@@ -92,11 +104,11 @@ var WorldModel = GObject.registerClass({
return null;
}
- load () {
+ load() {
let locations = this._settings.get_value('locations').deep_unpack();
- if (locations.length > 5) {
- locations = locations.slice(0, 5);
+ if (locations.length > 10) {
+ locations = locations.slice(0, 10).filter(location => !!location);
this._settings.set_value('locations', new GLib.Variant('av', locations));
}
@@ -105,9 +117,19 @@ var WorldModel = GObject.registerClass({
let variant = locations[i];
let location = this._world.deserialize(variant);
- info = this._addLocationInternal(location, false);
+ info = this._addLocationInternal(location);
}
- this._currentLocationInfo = info
+
+ if (info) {
+ this.setSelectedLocation(info);
+ }
+
+ this.#invalidate();
+ }
+
+ #invalidate() {
+ this.getAll();
+ this.items_changed(0, this._allInfos.length, this._allInfos.length);
}
_updateLoadingCount(delta) {
@@ -138,22 +160,28 @@ var WorldModel = GObject.registerClass({
return this._loadingCount > 0;
}
- addNewLocation(newLocation, isCurrentLocation) {
- if (!isCurrentLocation) {
- for (let info of this._infoList) {
- let location = info.location;
- if (location.equal(newLocation)) {
- this.moveLocationToFront(info);
- return info;
- }
- }
- }
+ setSelectedLocation(info) {
+ const newInfo = this.addNewLocation(info.get_location());
+ this._selectedLocation = newInfo;
+ this.emit('selected-location-changed', info);
+ }
+
+ isSelectedLocation(info) {
+ return !!this._selectedLocation && this._selectedLocation === info;
+ }
- let info = this._addLocationInternal(newLocation, isCurrentLocation);
+ isCurrentLocation(info) {
+ return !!this._currentLocationInfo && this._currentLocationInfo === info;
+ }
- if (!isCurrentLocation)
- this._queueSaveSettings();
+ addNewLocation(newLocation) {
+ let info = this._addLocationInternal(newLocation);
+ this._selectedLocation = info;
+ info._isCurrentLocation = false;
+ this.#invalidate();
+
+ this._queueSaveSettings();
return info;
}
@@ -161,7 +189,7 @@ var WorldModel = GObject.registerClass({
if (this._queueSaveSettingsId)
return;
- let id = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 10, () => {
+ let id = GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
this._queueSaveSettingsId = 0;
this._saveSettingsInternal();
return false;
@@ -172,9 +200,18 @@ var WorldModel = GObject.registerClass({
_saveSettingsInternal() {
let locations = [];
- for (let i = 0; i < this._infoList.length; i++) {
- if (!this._infoList[i]._isCurrentLocation)
- locations.push(this._infoList[i].location.serialize());
+ for (const info of this._allInfos) {
+ if (!info._isCurrentLocation) {
+ let serialized = null;
+ try {
+ serialized = info.location.serialize();
+ } catch (error) {
+ console.error(error);
+ }
+
+ if (serialized)
+ locations.push(serialized);
+ }
}
this._settings.set_value('locations', new GLib.Variant('av', locations));
@@ -190,21 +227,23 @@ var WorldModel = GObject.registerClass({
this._saveSettingsInternal();
}
- moveLocationToFront(info) {
- if (this._infoList.length == 0 || this._infoList[0] == info)
+ addCurrentLocation(info) {
+ if (this._infoList.includes(info))
return;
- this._removeLocationInternal(info, true);
- this._addInfoInternal(info, info._isCurrentLocation);
-
- // mark info as a manually chosen location so that we
- // save it
- info._isCurrentLocation = false;
+ const existingInfo = this._infoList.find(i => i.get_location().equal(info.location));
+ if (existingInfo) {
+ this._currentLocationInfo = existingInfo;
+ return;
+ }
- this._queueSaveSettings();
+ info._isCurrentLocation = true;
+ this._addInfoInternal(info);
}
_removeLocationInternal(oldInfo, skipDisconnect) {
+ if (!oldInfo) return;
+
if (oldInfo._loadingId && !skipDisconnect) {
oldInfo.disconnect(oldInfo._loadingId);
oldInfo._loadingId = 0;
@@ -221,40 +260,48 @@ var WorldModel = GObject.registerClass({
}
}
- this.emit('location-removed', oldInfo);
+ this.#invalidate();
}
- _addLocationInternal(newLocation, isCurrentLocation) {
- for (let i = 0; i < this._infoList.length; i++) {
- let info = this._infoList[i];
- if (info.get_location().equal(newLocation))
- return info;
- }
-
- let info = new GWeather.Info({
+ buildInfo(location) {
+ return new GWeather.Info({
application_id: pkg.name,
contact_info: 'https://gitlab.gnome.org/GNOME/gnome-weather/-/raw/master/gnome-weather.doap',
- location: newLocation,
+ location,
enabled_providers: this._providers
});
- this._addInfoInternal(info, isCurrentLocation);
+ }
+
+ _addLocationInternal(newLocation) {
+ const existingInfo = this._infoList.find(info => info.get_location().equal(newLocation));
+ if (existingInfo)
+ return existingInfo;
+
+ let info = this.buildInfo(newLocation);
+ this._addInfoInternal(info);
return info;
}
- _addInfoInternal(info, isCurrentLocation) {
- info._isCurrentLocation = isCurrentLocation;
+ _addInfoInternal(info) {
this._infoList.unshift(info);
this.updateInfo(info);
- if (isCurrentLocation)
- this._currentLocationInfo = info;
-
- this.emit('location-added', info, isCurrentLocation);
-
- if (this._infoList.length > 5) {
+ if (this._infoList.length > 10) {
let oldInfo = this._infoList.pop();
this._removeLocationInternal(oldInfo);
}
}
+
+ vfunc_get_item_type() {
+ return GWeather.Info.$gtype;
+ }
+
+ vfunc_get_n_items() {
+ return this._allInfos.length;
+ }
+
+ vfunc_get_item(n) {
+ return this._allInfos[n] ?? null;
+ }
});
diff --git a/tests/testutil.py b/tests/testutil.py
index 25233ad..43dc04c 100644
--- a/tests/testutil.py
+++ b/tests/testutil.py
@@ -70,7 +70,6 @@ def reset_settings():
"(0.79354303905785273, "
"0.16057029118347829))>)>]")
settings.set_value("locations", parsed)
- settings.set_value("automatic-location", GLib.Variant.new_boolean(False))
def init():
@@ -78,12 +77,10 @@ def init():
settings = Gio.Settings("org.gnome.Weather")
_previous_locations = settings.get_value("locations")
- _automatic_location = settings.get_value("automatic-location")
reset_settings()
def fini():
settings.set_value("locations", _previous_locations)
- settings.set_value("automatic-location", _automatic_location)
_do_bus_call("ActivateAction",
GLib.Variant('(sava{sv})', ('quit', [], [])))
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]