[gnome-weather/wip/ewlsh/gtk4] Port to GTK4 and libadwaita




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]