diff --git a/vanilla_installer/core/timezones.py b/vanilla_installer/core/timezones.py index 83f60858..c6f5e411 100644 --- a/vanilla_installer/core/timezones.py +++ b/vanilla_installer/core/timezones.py @@ -1,11 +1,13 @@ import datetime +import logging import requests -from gi.repository import GnomeDesktop -from gi.repository import GWeather +from gi.repository import GLib, GWeather from zoneinfo import ZoneInfo -regions = {} +logger = logging.getLogger("VanillaInstaller::Timezones") + +regions: dict[str, dict[str, dict[str, str]]] = {} world = GWeather.Location.get_world() parents = [] base = world @@ -20,7 +22,9 @@ regions[current_region][child.get_name()] = {} current_country = child.get_name() elif child.get_level() == GWeather.LocationLevel.CITY: - regions[current_region][current_country][child.get_city_name()] = child.get_timezone_str() + regions[current_region][current_country][child.get_city_name()] = ( + child.get_timezone_str() + ) if child.next_child(None) is not None: parents.append(child) @@ -34,21 +38,37 @@ all_timezones = dict(sorted(regions.items())) -def get_location(): + +def get_location(callback=None): + logger.info("Trying to retrieve timezone automatically") try: res = requests.get("http://ip-api.com/json?fields=49344", timeout=3).json() if res["status"] != "success": raise Exception(f"get_location: request failed with message '{res['message']}'") nearest = world.find_nearest_city(res["lat"], res["lon"]) except Exception as e: - print(e) + logger.error(f"Failed to retrieve timezone: {e}") nearest = None - return nearest + logger.info("Done retrieving timezone") + if callback: + logger.info("Running callback") + GLib.idle_add(callback, nearest) -def get_timezone_preview(tzname): - timezone = ZoneInfo(tzname) - now = datetime.datetime.now(timezone) - return now.strftime("%H:%M"), now.strftime("%A, %d %B %Y") +tz_preview_cache: dict[str, tuple[str, str]] = {} + + +def get_timezone_preview(tzname): + if tzname in tz_preview_cache: + return tz_preview_cache[tzname] + else: + timezone = ZoneInfo(tzname) + now = datetime.datetime.now(timezone) + now_str = ( + "%02d:%02d" % (now.hour, now.minute), + now.strftime("%A, %d %B %Y"), + ) + tz_preview_cache[tzname] = now_str + return now_str diff --git a/vanilla_installer/defaults/disk.py b/vanilla_installer/defaults/disk.py index da565d63..b5589e8a 100644 --- a/vanilla_installer/defaults/disk.py +++ b/vanilla_installer/defaults/disk.py @@ -578,7 +578,7 @@ def __init__(self, window, partition_recipe, **kwargs): ) ) self.group_partitions.add(entry) - + if "auto" in partition_recipe: for vg in partition_recipe["auto"]["vgs_to_remove"]: entry = Adw.ActionRow() diff --git a/vanilla_installer/defaults/network.py b/vanilla_installer/defaults/network.py index c744f72c..29cbf5db 100644 --- a/vanilla_installer/defaults/network.py +++ b/vanilla_installer/defaults/network.py @@ -22,7 +22,7 @@ from operator import attrgetter from threading import Lock, Timer -from gi.repository import NM, NMA4, Adw, Gtk, GLib +from gi.repository import NM, NMA4, Adw, GLib, Gtk from vanilla_installer.utils.run_async import RunAsync @@ -115,9 +115,7 @@ def refresh_ui(self): if not secure: self.secure_icon.set_from_icon_name("warning-small-symbolic") else: - self.secure_icon.set_from_icon_name( - "network-wireless-encrypted-symbolic" - ) + self.secure_icon.set_from_icon_name("network-wireless-encrypted-symbolic") self.secure_icon.set_visible(secure is not None) if tooltip is not None: @@ -254,6 +252,7 @@ def __init__(self, window, distro_info, key, step, **kwargs): self.__key = key self.__step = step self.__nm_client = NM.Client.new() + self.__step_num = step["num"] self.__devices = [] self.__wired_children = [] @@ -280,9 +279,12 @@ def __init__(self, window, distro_info, key, step, **kwargs): self.__nm_client.connect("device-added", self.__add_new_device) self.__nm_client.connect("device-added", self.__remove_device) self.btn_next.connect("clicked", self.__window.next) - self.connect("realize", self.__try_skip_page) + self.__window.carousel.connect("page-changed", self.__try_skip_page) + + def __try_skip_page(self, carousel=None, idx=None): + if idx is not None and idx != self.__step_num: + return - def __try_skip_page(self, data): # Skip page if already connected to the internet if self.has_eth_connection or self.has_wifi_connection: self.__window.next() @@ -316,9 +318,7 @@ def __get_network_devices(self): eth_devices += 1 elif device_type == NM.DeviceType.WIFI: device.connect("state-changed", self.__on_state_changed) - self.has_wifi_connection = ( - device.get_active_connection() is not None - ) + self.has_wifi_connection = device.get_active_connection() is not None self.__refresh_wifi_list(device) wifi_devices += 1 else: @@ -419,7 +419,7 @@ def __poll_wifi_scan(self, conn: NM.DeviceWifi, last_known_scan: int): GLib.idle_add(self.__refresh_wifi_list, conn) def __refresh_wifi_list(self, conn: NM.DeviceWifi): - networks = {} + networks: dict[str, list[NM.AccessPoint]] = {} for ap in conn.get_access_points(): ssid = ap.get_ssid() if ssid is None: diff --git a/vanilla_installer/defaults/timezone.py b/vanilla_installer/defaults/timezone.py index 8e0ef7ce..b8210944 100644 --- a/vanilla_installer/defaults/timezone.py +++ b/vanilla_installer/defaults/timezone.py @@ -14,18 +14,21 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import logging import re +import threading import unicodedata - -from gi.repository import Adw, Gtk, GLib from gettext import gettext as _ +from gi.repository import Adw, GLib, Gtk + from vanilla_installer.core.timezones import ( all_timezones, get_location, get_timezone_preview, ) -from vanilla_installer.utils.run_async import RunAsync + +logger = logging.getLogger("VanillaInstaller::Timezone") @Gtk.Template(resource_path="/org/vanillaos/Installer/gtk/widget-timezone.ui") @@ -35,30 +38,22 @@ class TimezoneRow(Adw.ActionRow): select_button = Gtk.Template.Child() country_label = Gtk.Template.Child() - def __init__(self, title, subtitle, tz_name, parent, **kwargs): + def __init__(self, title, subtitle, tz_name, toggled_callback, parent_expander, **kwargs): super().__init__(**kwargs) self.title = title self.subtitle = subtitle - self.parent = parent self.tz_name = tz_name - - tz_time, tz_date = get_timezone_preview(tz_name) + self.parent_expander = parent_expander self.set_title(title) - self.set_subtitle(f"{tz_time} • {tz_date}") self.country_label.set_label(tz_name) - self.select_button.connect("toggled", self.__on_check_button_toggled) + self.select_button.connect("toggled", toggled_callback, self) + self.parent_expander.connect("notify::expanded", self.update_time_preview) - def __on_check_button_toggled(self, widget): - tz_split = self.tz_name.split("/", 1) - self.parent.selected_timezone["region"] = tz_split[0] - self.parent.selected_timezone["zone"] = tz_split[1] - self.parent.current_tz_label.set_label(self.tz_name) - self.parent.current_location_label.set_label( - _("(at %s, %s)") % (self.title, self.subtitle) - ) - self.parent.timezone_verify() + def update_time_preview(self, *args): + tz_time, tz_date = get_timezone_preview(self.tz_name) + self.set_subtitle(f"{tz_time} • {tz_date}") @Gtk.Template(resource_path="/org/vanillaos/Installer/gtk/default-timezone.ui") @@ -74,11 +69,15 @@ class VanillaDefaultTimezone(Adw.Bin): search_controller = Gtk.EventControllerKey.new() selected_timezone = {"region": "Europe", "zone": None} - expanders_list = { - country: region - for region, countries in all_timezones.items() - for country in countries.keys() - } + expanders_list = dict( + sorted( + { + country: region + for region, countries in all_timezones.items() + for country in countries.keys() + }.items() + ) + ) def __init__(self, window, distro_info, key, step, **kwargs): super().__init__(**kwargs) @@ -86,6 +85,7 @@ def __init__(self, window, distro_info, key, step, **kwargs): self.__distro_info = distro_info self.__key = key self.__step = step + self.__step_num = step["num"] self.__expanders = [] self.__tz_entries = [] @@ -98,12 +98,24 @@ def __init__(self, window, distro_info, key, step, **kwargs): self.search_controller.connect("key-released", self.__on_search_key_pressed) self.entry_search_timezone.add_controller(self.search_controller) - def timezone_verify(self, *args): - valid = ( - self.selected_timezone["region"] - and self.selected_timezone["zone"] is not None - ) - self.btn_next.set_sensitive(valid) + def timezone_verify(self, carousel=None, idx=None): + if idx is not None and idx != self.__step_num: + return + + def timezone_verify_callback(result, *args): + if result: + current_city = result.get_city_name() + current_country = result.get_country_name() + for entry in self.__tz_entries: + if current_city == entry.title and current_country == entry.subtitle: + self.selected_timezone["zone"] = current_city + self.selected_timezone["region"] = current_country + entry.select_button.set_active(True) + return + self.btn_next.set_sensitive(True) + + thread = threading.Thread(target=get_location, args=(timezone_verify_callback,)) + thread.start() def get_finals(self): try: @@ -118,11 +130,7 @@ def get_finals(self): def __on_search_key_pressed(self, *args): def remove_accents(msg: str): - out = ( - unicodedata.normalize("NFD", msg) - .encode("ascii", "ignore") - .decode("utf-8") - ) + out = unicodedata.normalize("NFD", msg).encode("ascii", "ignore").decode("utf-8") return str(out) search_entry = self.entry_search_timezone.get_text().lower() @@ -147,48 +155,30 @@ def remove_accents(msg: str): visible_entries = 0 current_country = entry.subtitle current_expander += 1 - visible_entries += 1 if match else 0 + visible_entries += match + + def __on_row_toggle(self, __check_button, widget): + tz_split = widget.tz_name.split("/", 1) + self.selected_timezone["region"] = tz_split[0] + self.selected_timezone["zone"] = tz_split[1] + self.current_tz_label.set_label(widget.tz_name) + self.current_location_label.set_label(_("(at %s, %s)") % (widget.title, widget.subtitle)) + self.btn_next.set_sensitive(True) def __generate_timezone_list_widgets(self): - def __populate_expanders(): - widgets = {} - first_elem = None - for country, region in dict(sorted(self.expanders_list.items())).items(): - if len(all_timezones[region][country]) > 0: - country_tz_expander_row = Adw.ExpanderRow.new() - country_tz_expander_row.set_title(country) - country_tz_expander_row.set_subtitle(region) - widgets[country_tz_expander_row] = [] - for city, tzname in sorted(all_timezones[region][country].items()): - timezone_row = TimezoneRow(city, country, tzname, self) - if first_elem is None: - first_elem = timezone_row - else: - timezone_row.select_button.set_group( - first_elem.select_button - ) - widgets[country_tz_expander_row].append(timezone_row) - return widgets - - def __set_located_timezone(result, *args): - if not result: - return - current_city = result.get_city_name() - current_country = result.get_country_name() - for entry in self.__tz_entries: - if current_city == entry.title and current_country == entry.subtitle: - self.selected_timezone["zone"] = current_city - self.selected_timezone["region"] = current_country - entry.select_button.set_active(True) - return - - def __callback(widgets, *args): - for expander, timezone_rows in widgets.items(): + def __populate_expander(expander, region, country, *args): + for city, tzname in all_timezones[region][country].items(): + timezone_row = TimezoneRow(city, country, tzname, self.__on_row_toggle, expander) + self.__tz_entries.append(timezone_row) + if len(self.__tz_entries) > 0: + timezone_row.select_button.set_group(self.__tz_entries[0].select_button) + expander.add_row(timezone_row) + + for country, region in self.expanders_list.items(): + if len(all_timezones[region][country]) > 0: + expander = Adw.ExpanderRow.new() + expander.set_title(country) + expander.set_subtitle(region) self.all_timezones_group.add(expander) self.__expanders.append(expander) - for timezone_row in timezone_rows: - expander.add_row(timezone_row) - self.__tz_entries.append(timezone_row) - RunAsync(get_location, __set_located_timezone) - - RunAsync(__populate_expanders, __callback) + GLib.idle_add(__populate_expander, expander, region, country) diff --git a/vanilla_installer/defaults/welcome.py b/vanilla_installer/defaults/welcome.py index fba45711..2b9f6305 100644 --- a/vanilla_installer/defaults/welcome.py +++ b/vanilla_installer/defaults/welcome.py @@ -19,6 +19,7 @@ from vanilla_installer.windows.dialog_recovery import VanillaRecoveryDialog from vanilla_installer.windows.dialog_poweroff import VanillaPoweroffDialog + @Gtk.Template(resource_path="/org/vanillaos/Installer/gtk/default-welcome.ui") class VanillaDefaultWelcome(Adw.Bin): __gtype_name__ = "VanillaDefaultWelcome" @@ -37,7 +38,7 @@ def __init__(self, window, distro_info, key, step, **kwargs): distro_name = self.__distro_info.get("name", "Vanilla OS") distro_logo = self.__distro_info.get("logo", "org.vanillaos.Installer-flower") - + self.status_page.set_icon_name(distro_logo) self.status_page.set_title(f"Welcome to {distro_name}!") diff --git a/vanilla_installer/windows/main_window.py b/vanilla_installer/windows/main_window.py index ee178ab4..8f10a59d 100644 --- a/vanilla_installer/windows/main_window.py +++ b/vanilla_installer/windows/main_window.py @@ -16,8 +16,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import logging import json +import logging import os from gi.repository import Adw, Gtk @@ -62,14 +62,12 @@ def __connect_signals(self): self.btn_back.connect("clicked", self.back) self.carousel.connect("page-changed", self.__on_page_changed) self.__builder.widgets[-1].btn_next.connect("clicked", self.update_finals) - self.__view_confirm.connect( - "installation-confirmed", self.on_installation_confirmed - ) + self.__view_confirm.connect("installation-confirmed", self.on_installation_confirmed) def __build_ui(self): if "VANILLA_FORCE_TOUR" not in os.environ: for widget in self.__builder.widgets: - logger.info(f"(%s) Adding widget to carousel", widget) + logger.info("(%s) Adding widget to carousel", widget.__gtype_name__) self.carousel.append(widget) else: self.__on_page_changed() @@ -82,16 +80,16 @@ def __on_page_changed(self, *args): cur_index = self.carousel.get_position() page = self.carousel.get_nth_page(cur_index) - logger.info(f"(%s) Page is changing...", page) + logger.info("(%s) Page is changing...", page.__gtype_name__) if page not in [self.__view_progress, self.__view_done]: - logger.info(f"(%s) It is not a final page", page) + logger.info("(%s) It is not a final page", page.__gtype_name__) self.btn_back.set_visible(cur_index != 0.0) self.btn_back.set_sensitive(cur_index != 0.0) self.carousel_indicator_dots.set_visible(cur_index != 0.0) return - logger.info(f"(%s) It is a final page", page) + logger.info("(%s) It is a final page", page.__gtype_name__) self.btn_back.set_visible(False) self.btn_back.set_sensitive(False) @@ -99,7 +97,7 @@ def __on_page_changed(self, *args): # keep the btn_back button locked if this is the last page if page == self.__view_done: - logger.info(f"(%s) It is the DONE page", page) + logger.info("(%s) It is the DONE page", page.__gtype_name__) return def update_finals(self, *args): @@ -123,10 +121,14 @@ def on_installation_confirmed(self, *args): def next(self, widget=None, fn=None, *args): logger.info("Going to next page") + cur_index = self.carousel.get_position() + logger.info(f"Next page is {int(cur_index + 1)}") + if fn is not None: + logger.info("Executing page's custom function") fn() + logger.info("Finished executing page's custom function") - cur_index = self.carousel.get_position() page = self.carousel.get_nth_page(cur_index + 1) self.carousel.scroll_to(page, True) @@ -134,6 +136,8 @@ def back(self, *args): logger.info("Going to previous page") cur_index = self.carousel.get_position() + logger.info(f"Previous page is {int(cur_index - 1)}") + page = self.carousel.get_nth_page(cur_index - 1) self.carousel.scroll_to(page, True)