# GTK from gi.repository import Gio, Gdk, GLib # Packages import os, time, locale, subprocess, getpass from PIL import Image # Local scripts from service.cinnamon_pref_handler import * from service.suntimes import * from service.time_bar_chart import * from service.location import * from enums.PeriodSourceEnum import * class Main_View_Model: """ The main ViewModel for the application """ def __init__(self) -> None: """ Initialization """ # Paths self.WORKING_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) self.RES_DIR = self.WORKING_DIR + "/res" self.IMAGES_DIR = self.RES_DIR + "/images" self.GLADE_URI = self.RES_DIR + "/preferences.glade" self.TIMEBAR_URI = self.WORKING_DIR + "/src/time_bar.svg" self.TIMEBAR_URI_POLYLINES = self.WORKING_DIR + "/src/time_bar_polylines.svg" self.PREF_URI = os.path.expanduser("~") + \ "/.config/cinnamon/spices/cinnamon-dynamic-wallpaper@TobiZog/cinnamon-dynamic-wallpaper@TobiZog.json" # Datasets self.image_sets = ["aurora", "beach", "bitday", "cliffs", "desert", "earth", "gradient", "island", "lake", "lakeside", "mountains", "sahara"] self.picture_aspects = ["centered", "scaled", "stretched", "zoom", "spanned"] self.network_location_provider = ["geojs.io", "ip-api.com", "ipwho.is"] # Objects from scripts self.cinnamon_prefs = Cinnamon_Pref_Handler() self.time_bar_chart = Time_Bar_Chart() self.suntimes = Suntimes() self.location = Location() self.background_settings = Gio.Settings.new("org.cinnamon.desktop.background") # Language support self.UUID = "cinnamon-dynamic-wallpaper@TobiZog" self.localeDir = os.path.expanduser("~") + "/.local/share/locale" locale.bindtextdomain(self.UUID, self.localeDir) # Other Variables self.display = Gdk.Display.get_default() self.screen_height = self.display.get_monitor(0).get_geometry().height self.breakpoint_ui = 1000 def refresh_charts(self): """ Refreshes the two variants of the time bar charts """ # Stores the start times of the periods in minutes since midnight time_periods_min = [] if self.cinnamon_prefs.period_source == PeriodSourceEnum.CUSTOMTIMEPERIODS: for i in range(0, 10): time_str = self.cinnamon_prefs.period_custom_start_time[i] time_periods_min.append(int(time_str[0:2]) * 60 + int(time_str[3:5])) else: if self.cinnamon_prefs.period_source == PeriodSourceEnum.NETWORKLOCATION: self.suntimes.calc_suntimes(float(self.cinnamon_prefs.latitude_auto), float(self.cinnamon_prefs.longitude_auto)) else: self.suntimes.calc_suntimes(float(self.cinnamon_prefs.latitude_custom), float(self.cinnamon_prefs.longitude_custom)) # Get all time periods. Store the minutes to the list and print the values to the text views for i in range(0, 10): time_range_now = self.suntimes.day_periods[i] time_periods_min.append(time_range_now.hour * 60 + time_range_now.minute) # Create time bar # Reduce size for small displays if self.screen_height < self.breakpoint_ui: bar_width = 1150 bar_height = 110 else: bar_width = 1300 bar_height = 150 self.time_bar_chart.create_bar_chart_with_polylines(self.TIMEBAR_URI_POLYLINES, bar_width, bar_height, time_periods_min) self.time_bar_chart.create_bar_chart(self.TIMEBAR_URI, bar_width, bar_height, time_periods_min) def refresh_location(self) -> bool: """ Updating the location by IP, store the result to cinnamon_prefs Run it in a parallel thread to avoid UI freeze! Returns: bool: Successful or not """ current_location = self.location.get_location(self.cinnamon_prefs.network_location_provider) if current_location['success']: self.cinnamon_prefs.latitude_auto = current_location['latitude'] self.cinnamon_prefs.longitude_auto = current_location['longitude'] return current_location['success'] def string_to_time_converter(self, raw_str: str) -> time: """ Convert a time string like "12:34" to a time object Args: raw_str (str): Raw string Returns: time: Time object """ hour = raw_str[0:raw_str.find(":")] minute = raw_str[raw_str.find(":") + 1:] return time(hour=int(hour), minute=int(minute)) def time_to_string_converter(self, _time: time) -> str: """ Convert a time object to a string like "12:34" Args: time (time): Given time object to convert Returns: str: Converted string """ return "{:0>2}:{:0>2}".format(_time.hour, _time.minute) def calulate_time_periods(self) -> list[time]: """ Calculate the ten time periods based on the period source in the preferences Returns: list[time]: Time periods """ result = [] if self.cinnamon_prefs.period_source == PeriodSourceEnum.CUSTOMTIMEPERIODS: # User uses custom time periods for i in range(0, 10): result.append(self.string_to_time_converter(self.cinnamon_prefs.period_custom_start_time[i])) else: # Time periods have to be estimate by coordinates if self.cinnamon_prefs.period_source == PeriodSourceEnum.NETWORKLOCATION: # Get coordinates from the network self.suntimes.calc_suntimes(self.cinnamon_prefs.latitude_auto, self.cinnamon_prefs.longitude_auto) elif self.cinnamon_prefs.period_source == PeriodSourceEnum.CUSTOMLOCATION: # Get coordinates from user input self.suntimes.calc_suntimes(self.cinnamon_prefs.latitude_custom, self.cinnamon_prefs.longitude_custom) # Return the time periods result = self.suntimes.day_periods return result def refresh_image(self): """ Replace the desktop image if needed """ start_times = self.calulate_time_periods() # Get the time of day time_now = time(datetime.now().hour, datetime.now().minute) # Assign the last image as fallback self.current_image_uri = self.cinnamon_prefs.source_folder + self.cinnamon_prefs.period_images[9] for i in range(0, 9): # Replace the image URI, if it's not the last time period of the day if start_times[i] <= time_now and time_now < start_times[i + 1]: self.current_image_uri = self.cinnamon_prefs.source_folder + self.cinnamon_prefs.period_images[i] break # Update the background self.background_settings['picture-uri'] = "file://" + self.current_image_uri # Update the login_image if self.cinnamon_prefs.login_image: # Create the folder in /tmp directory = '/usr/share/pixmaps/cinnamon_dynamic_wallpaper' if not os.path.isdir(directory): subprocess.run(['pkexec', 'install', '-o', getpass.getuser(), '-d', directory]) # Copy the current image to the temp folder for the login screen os.system("cp " + self.current_image_uri + " " + directory + "/login_image.jpg") # Set background stretching self.background_settings['picture-options'] = self.cinnamon_prefs.picture_aspect def get_images_from_folder(self, URI: str) -> list: """ List all images in a folder Args: URI (str): Absolute path of the folder Returns: list: List of file names which are images """ items = [] for file in os.listdir(URI): if file.endswith(("jpg", "jpeg", "png", "bmp", "svg")): items.append(file) items.sort() return items def extract_heic_file(self, file_uri: str) -> bool: """ Extract a heic file to an internal folder Args: file_uri (str): Absolute path to the heic file Returns: bool: Extraction was successful """ try: extract_folder = self.IMAGES_DIR + "/extracted_images/" file_name: str = file_uri[file_uri.rfind("/") + 1:] file_name = file_name[:file_name.rfind(".")] # Create the buffer folder if its not existing try: os.mkdir(extract_folder) except: pass # Cleanup the folder for file in self.get_images_from_folder(extract_folder): os.remove(extract_folder + file) # Extract the HEIC file print(self.get_imagemagick_prompt() + " " + file_uri + " -quality 100% " + extract_folder + file_name + ".jpg") os.system(self.get_imagemagick_prompt() + " " + file_uri + " -quality 100% " + extract_folder + file_name + ".jpg") return True except: return False def set_background_gradient(self): """ Setting a gradient background to hide images, which are not high enough """ # Load the image try: im = Image.open(self.current_image_uri) pix = im.load() # Width and height of the current setted image width, height = im.size # Color of the top and bottom pixel in the middle of the image top_color = pix[width / 2,0] bottom_color = pix[width / 2, height - 1] # Create the gradient self.background_settings['color-shading-type'] = "vertical" if self.cinnamon_prefs.dynamic_background_color: self.background_settings['primary-color'] = f"#{top_color[0]:x}{top_color[1]:x}{top_color[2]:x}" self.background_settings['secondary-color'] = f"#{bottom_color[0]:x}{bottom_color[1]:x}{bottom_color[2]:x}" else: self.background_settings['primary-color'] = "#000000" self.background_settings['secondary-color'] = "#000000" except: self.background_settings['primary-color'] = "#000000" self.background_settings['secondary-color'] = "#000000" def set_login_image(self): """ Writes a path to file in /tmp/cinnamon_dynamic_wallpaper to display the wallpaper on the login screen """ # New config file content file_content = "" # Location of the config file file_location = self.WORKING_DIR + "/slick-greeter.conf" if self.cinnamon_prefs.login_image: self.refresh_image() if os.path.isfile("/etc/lightdm/slick-greeter.conf"): # File already exists, make a copy of the config with open("/etc/lightdm/slick-greeter.conf", "r") as conf_file: for line in conf_file.readlines(): if not line.startswith("background"): file_content += line elif line.endswith("cinnamon_dynamic_wallpaper/login_image.jpg"): # Skip the configuration. It's already perfect! return else: # File doesn't exists file_content = "[Greeter]\n" file_content += "background=/usr/share/pixmaps/cinnamon_dynamic_wallpaper/login_image.jpg" # Create the file with open(file_location, "w") as conf_file: conf_file.write(file_content) conf_file.close() # Move it to /etc/lightdm if os.path.isfile("/etc/lightdm/slick-greeter.conf"): subprocess.call(['pkexec', 'mv', '/etc/lightdm/slick-greeter.conf', '/etc/lightdm/slick-greeter.conf.backup']) subprocess.call(['pkexec', 'mv', file_location, '/etc/lightdm/']) else: subprocess.call(['pkexec', 'mv', file_location, '/etc/lightdm/']) else: self.reset_login_image() def reset_login_image(self): if os.path.isfile('/etc/lightdm/slick-greeter.conf.backup'): subprocess.call(['pkexec', 'rm', '/etc/lightdm/slick-greeter.conf']) subprocess.call(['pkexec', 'mv', '/etc/lightdm/slick-greeter.conf.backup', '/etc/lightdm/slick-greeter.conf']) def check_for_imagemagick(self) -> bool: # Imagemagick < v.7.0 if GLib.find_program_in_path("convert") != None: return True # Imagemagick >= v.7.0 elif GLib.find_program_in_path("imagemagick") != None: return True # Not installed else: return False def get_imagemagick_prompt(self) -> str: # Imagemagick < v.7.0 if GLib.find_program_in_path("convert") != None: return "convert" # Imagemagick >= v.7.0 elif GLib.find_program_in_path("imagemagick") != None: return "imagemagick convert"