diff --git a/build-aux/flatpak/com.github.GradienceTeam.Gradience.Devel.json b/build-aux/flatpak/com.github.GradienceTeam.Gradience.Devel.json
index f5cfc6b8..f6841977 100644
--- a/build-aux/flatpak/com.github.GradienceTeam.Gradience.Devel.json
+++ b/build-aux/flatpak/com.github.GradienceTeam.Gradience.Devel.json
@@ -14,6 +14,7 @@
"--filesystem=xdg-config/gtk-3.0",
"--filesystem=xdg-config/gtk-4.0",
"--filesystem=xdg-run/gvfsd",
+ "--filesystem=xdg-download:ro",
"--filesystem=~/.mozilla/firefox",
"--filesystem=~/.librewolf",
"--filesystem=~/.var/app/org.mozilla.firefox/.mozilla/firefox",
diff --git a/build-aux/flatpak/com.github.GradienceTeam.Gradience.json b/build-aux/flatpak/com.github.GradienceTeam.Gradience.json
index 719e4049..3b06316d 100644
--- a/build-aux/flatpak/com.github.GradienceTeam.Gradience.json
+++ b/build-aux/flatpak/com.github.GradienceTeam.Gradience.json
@@ -14,6 +14,7 @@
"--filesystem=xdg-config/gtk-3.0",
"--filesystem=xdg-config/gtk-4.0",
"--filesystem=xdg-run/gvfsd",
+ "--filesystem=xdg-download:ro",
"--filesystem=~/.mozilla/firefox",
"--filesystem=~/.librewolf",
"--filesystem=~/.var/app/org.mozilla.firefox/.mozilla/firefox",
diff --git a/gradience/backend/flatpak_overrides.py b/gradience/backend/flatpak_overrides.py
index 666d9ae9..8d79a81c 100644
--- a/gradience/backend/flatpak_overrides.py
+++ b/gradience/backend/flatpak_overrides.py
@@ -20,6 +20,8 @@ import os
from gi.repository import GLib, Gio, Adw
+from gradience.backend.constants import app_id
+
from gradience.backend.logger import Logger
logging = Logger()
@@ -32,10 +34,10 @@ class InvalidGTKVersion(Exception):
pass
-""" Internal helper functions (shouldn't be used outside this file) """
+""" Internal helper functions (shouldn't be used outside this module) """
-def get_system_flatpak_path():
+def __get_system_flatpak_path():
systemPath = GLib.getenv("FLATPAK_SYSTEM_DIR")
logging.debug(f"systemPath: {systemPath}")
@@ -47,7 +49,7 @@ def get_system_flatpak_path():
return GLib.build_filenamev([systemDataDir, "flatpak"])
-def get_user_flatpak_path():
+def __get_user_flatpak_path():
userPath = GLib.getenv("FLATPAK_USER_DIR")
logging.debug(f"userPath: {userPath}")
@@ -60,49 +62,196 @@ def get_user_flatpak_path():
return GLib.build_filenamev([userDataDir, "flatpak"])
-def user_save_keyfile(toast_overlay, settings, user_keyfile, filename, gtk_ver):
+def __user_save_keyfile(user_keyfile, filename, settings=None, gtk_ver=None, toast_overlay=None):
try:
user_keyfile.save_to_file(filename)
except GLib.GError as e:
- toast_overlay.add_toast(Adw.Toast(title=_("Failed to save override")))
+ if toast_overlay:
+ toast_overlay.add_toast(Adw.Toast(title=_("Failed to save override")))
logging.error(f"Failed to save keyfile structure to override. Exc: {e}")
else:
- if gtk_ver == "gtk4":
+ if gtk_ver == "gtk4" and settings:
settings.set_boolean("user-flatpak-theming-gtk4", True)
logging.debug(
f"user-flatpak-theming-gtk4: {settings.get_boolean('user-flatpak-theming-gtk4')}"
)
- elif gtk_ver == "gtk3":
+ elif gtk_ver == "gtk3" and settings:
settings.set_boolean("user-flatpak-theming-gtk3", True)
logging.debug(
f"user-flatpak-theming-gtk3: {settings.get_boolean('user-flatpak-theming-gtk3')}"
)
+ elif not gtk_ver and not settings:
+ logging.debug("DEV WARNING: 'gtk_ver' and 'settings' parameters aren't set for '__user_save_keyfile' function. Unless you aren't using '{create,remove}_*_override' functions, this is a bug.")
-def global_save_keyfile(toast_overlay, settings, global_keyfile, filename, gtk_ver):
+def __global_save_keyfile(global_keyfile, filename, settings=None, gtk_ver=None, toast_overlay=None):
try:
global_keyfile.save_to_file(filename)
except GLib.GError as e:
- toast_overlay.add_toast(Adw.Toast(title=_("Failed to save override")))
+ if toast_overlay:
+ toast_overlay.add_toast(Adw.Toast(title=_("Failed to save override")))
logging.error(f"Failed to save keyfile structure to override. Exc: {e}")
else:
- if gtk_ver == "gtk4":
+ if gtk_ver == "gtk4" and settings:
settings.set_boolean("global-flatpak-theming-gtk4", True)
logging.debug(
f"global-flatpak-theming-gtk4: {settings.get_boolean('global-flatpak-theming-gtk4')}"
)
- elif gtk_ver == "gtk3":
+ elif gtk_ver == "gtk3" and settings:
settings.set_boolean("global-flatpak-theming-gtk3", True)
logging.debug(
f"global-flatpak-theming-gtk3: {settings.get_boolean('global-flatpak-theming-gtk3')}"
)
+ elif not gtk_ver and not settings:
+ logging.debug("DEV WARNING: 'gtk_ver' and 'settings' parameters aren't set for '__global_save_keyfile' function. Unless you aren't using '{create,remove}_*_override' functions, this is a bug.")
""" Main functions """
-def create_gtk_user_override(toast_overlay, settings, gtk_ver):
- override_dir = GLib.build_filenamev([get_user_flatpak_path(), "overrides"])
+def list_file_access():
+ override_dir = GLib.build_filenamev([__get_user_flatpak_path(), "overrides"])
+ logging.debug(f"override_dir: {override_dir}")
+
+ filename = GLib.build_filenamev([override_dir, app_id])
+
+ user_keyfile = GLib.KeyFile.new()
+
+ try:
+ user_keyfile.load_from_file(filename, GLib.KeyFileFlags.NONE)
+ except GLib.GError as e:
+ if e.code == 4:
+ logging.debug("Gradience overrides file doesn't exist")
+ return False
+ else:
+ logging.error(f"Unhandled GLib.FileError error code. Exc: {e}")
+ raise
+ else:
+ try:
+ filesys_list = user_keyfile.get_string_list(
+ "Context", "filesystems")
+ except GLib.GError:
+ logging.debug("No values in 'filesystems' override")
+ return False
+ else:
+ return filesys_list
+
+
+# TODO: Frontend: Show information to user to relaunch Gradience, as this function modifies \
+# Gradience's overrides.
+def allow_file_access(directory, toast_overlay=None):
+ override_dir = GLib.build_filenamev([__get_user_flatpak_path(), "overrides"])
+ logging.debug(f"override_dir: {override_dir}")
+
+ without_access_spec = (
+ not ":ro" in directory
+ and not ":rw" in directory
+ and not ":create" in directory
+ )
+
+ if without_access_spec:
+ directory += ":ro"
+
+ filename = GLib.build_filenamev([override_dir, app_id])
+
+ user_keyfile = GLib.KeyFile.new()
+
+ try:
+ user_keyfile.load_from_file(filename, GLib.KeyFileFlags.NONE)
+ except GLib.GError as e:
+ if e.code == 4:
+ logging.debug("File doesn't exist. Attempting to create one")
+ if not os.path.exists(override_dir):
+ try:
+ dirs = Gio.File.new_for_path(override_dir)
+ dirs.make_directory_with_parents(None)
+ except GLib.GError as e:
+ logging.error(f"Unable to create directories. Exc: {e}")
+ raise
+ else:
+ logging.debug("Directories created.")
+
+ file = Gio.File.new_for_path(filename)
+ file.create(Gio.FileCreateFlags.NONE, None)
+
+ user_keyfile.load_from_file(filename, GLib.KeyFileFlags.NONE)
+ user_keyfile.set_string("Context", "filesystems", directory)
+
+ __user_save_keyfile(user_keyfile, filename,
+ toast_overlay=toast_overlay)
+ else:
+ if toast_overlay:
+ toast_overlay.add_toast(
+ Adw.Toast(title=_("Unexpected file error occurred"))
+ )
+ logging.error(f"Unhandled GLib.FileError error code. Exc: {e}")
+ else:
+ try:
+ filesys_list = user_keyfile.get_string_list(
+ "Context", "filesystems")
+ except GLib.GError:
+ user_keyfile.set_string("Context", "filesystems", directory)
+ __user_save_keyfile(user_keyfile, filename,
+ toast_overlay=toast_overlay)
+ else:
+ if directory not in filesys_list:
+ user_keyfile.set_string_list(
+ "Context", "filesystems", filesys_list + [directory]
+ )
+ __user_save_keyfile(user_keyfile, filename,
+ toast_overlay=toast_overlay)
+ else:
+ logging.info("Path is already allowed")
+
+
+# TODO: Frontend: Show information to user to relaunch Gradience, as this function modifies \
+# Gradience's overrides.
+def disallow_file_access(directory, toast_overlay=None):
+ override_dir = GLib.build_filenamev([__get_user_flatpak_path(), "overrides"])
+ logging.debug(f"override_dir: {override_dir}")
+
+ filename = GLib.build_filenamev([override_dir, app_id])
+
+ user_keyfile = GLib.KeyFile.new()
+
+ try:
+ user_keyfile.load_from_file(filename, GLib.KeyFileFlags.NONE)
+ except GLib.GError as e:
+ if e.code == 4:
+ logging.debug("File doesn't exist")
+ return
+ else:
+ if toast_overlay:
+ toast_overlay.add_toast(
+ Adw.Toast(title=_("Unexpected file error occurred"))
+ )
+ logging.error(f"Unhandled GLib.FileError error code. Exc: {e}")
+ raise
+ else:
+ try:
+ filesys_list = user_keyfile.get_string_list(
+ "Context", "filesystems")
+ except GLib.GError:
+ logging.debug("Group/key not found")
+ return
+ else:
+ if directory in filesys_list:
+ logging.debug(f"before: {filesys_list}")
+ filesys_list.remove(directory)
+ logging.debug(f"after: {filesys_list}")
+
+ user_keyfile.set_string_list(
+ "Context", "filesystems", filesys_list)
+ __user_save_keyfile(user_keyfile, filename,
+ toast_overlay=toast_overlay)
+ logging.debug("Path removed")
+ else:
+ logging.debug("Path doesn't exist in overrides")
+ return
+
+
+def create_gtk_user_override(settings, gtk_ver, toast_overlay=None):
+ override_dir = GLib.build_filenamev([__get_user_flatpak_path(), "overrides"])
logging.debug(f"override_dir: {override_dir}")
filename = GLib.build_filenamev([override_dir, "global"])
@@ -147,12 +296,13 @@ def create_gtk_user_override(toast_overlay, settings, gtk_ver):
user_keyfile.load_from_file(filename, GLib.KeyFileFlags.NONE)
user_keyfile.set_string("Context", "filesystems", gtk_path)
- user_save_keyfile(toast_overlay, settings,
- user_keyfile, filename, gtk_ver)
+ __user_save_keyfile(user_keyfile, filename,
+ settings, gtk_ver, toast_overlay)
else:
- toast_overlay.add_toast(
- Adw.Toast(title=_("Unexpected file error occurred"))
- )
+ if toast_overlay:
+ toast_overlay.add_toast(
+ Adw.Toast(title=_("Unexpected file error occurred"))
+ )
logging.error(f"Unhandled GLib.FileError error code. Exc: {e}")
else:
try:
@@ -160,16 +310,15 @@ def create_gtk_user_override(toast_overlay, settings, gtk_ver):
"Context", "filesystems")
except GLib.GError:
user_keyfile.set_string("Context", "filesystems", gtk_path)
- user_save_keyfile(toast_overlay, settings,
- user_keyfile, filename, gtk_ver)
+ __user_save_keyfile(user_keyfile, filename,
+ settings, gtk_ver, toast_overlay)
else:
if gtk_path not in filesys_list:
user_keyfile.set_string_list(
"Context", "filesystems", filesys_list + [gtk_path]
)
- user_save_keyfile(
- toast_overlay, settings, user_keyfile, filename, gtk_ver
- )
+ __user_save_keyfile(user_keyfile, filename,
+ settings, gtk_ver, toast_overlay)
else:
if is_gtk4:
settings.set_boolean("user-flatpak-theming-gtk4", True)
@@ -178,8 +327,8 @@ def create_gtk_user_override(toast_overlay, settings, gtk_ver):
logging.debug("Value already exists.")
-def remove_gtk_user_override(toast_overlay, settings, gtk_ver):
- override_dir = GLib.build_filenamev([get_user_flatpak_path(), "overrides"])
+def remove_gtk_user_override(settings, gtk_ver, toast_overlay=None):
+ override_dir = GLib.build_filenamev([__get_user_flatpak_path(), "overrides"])
logging.debug(f"override_dir: {override_dir}")
filename = GLib.build_filenamev([override_dir, "global"])
@@ -210,9 +359,10 @@ def remove_gtk_user_override(toast_overlay, settings, gtk_ver):
set_theming()
logging.warning("remove override: File doesn't exist")
else:
- toast_overlay.add_toast(
- Adw.Toast(title=_("Unexpected file error occurred"))
- )
+ if toast_overlay:
+ toast_overlay.add_toast(
+ Adw.Toast(title=_("Unexpected file error occurred"))
+ )
logging.error(f"Unhandled GLib.FileError error code. Exc: {e}")
else:
try:
@@ -229,9 +379,8 @@ def remove_gtk_user_override(toast_overlay, settings, gtk_ver):
user_keyfile.set_string_list(
"Context", "filesystems", filesys_list)
- user_save_keyfile(
- toast_overlay, settings, user_keyfile, filename, gtk_ver
- )
+ __user_save_keyfile(user_keyfile, filename,
+ settings, gtk_ver, toast_overlay)
logging.debug("remove override: Value removed.")
else:
set_theming()
@@ -242,9 +391,9 @@ def remove_gtk_user_override(toast_overlay, settings, gtk_ver):
# TODO: Implement user authentication using Polkit
-def create_gtk_global_override(toast_overlay, settings, gtk_ver):
+def create_gtk_global_override(settings, gtk_ver, toast_overlay=None):
override_dir = GLib.build_filenamev(
- [get_system_flatpak_path(), "overrides"])
+ [__get_system_flatpak_path(), "overrides"])
logging.debug(f"override_dir: {override_dir}")
filename = GLib.build_filenamev([override_dir, "global"])
@@ -289,13 +438,13 @@ def create_gtk_global_override(toast_overlay, settings, gtk_ver):
global_keyfile.load_from_file(filename, GLib.KeyFileFlags.NONE)
global_keyfile.set_string("Context", "filesystems", gtk_path)
- global_save_keyfile(
- toast_overlay, settings, global_keyfile, filename, gtk_ver
- )
+ __global_save_keyfile(global_keyfile, filename,
+ settings, gtk_ver, toast_overlay)
else:
- toast_overlay.add_toast(
- Adw.Toast(title=_("Unexpected file error occurred"))
- )
+ if toast_overlay:
+ toast_overlay.add_toast(
+ Adw.Toast(title=_("Unexpected file error occurred"))
+ )
logging.error(f"Unhandled GLib.FileError error code. Exc: {e}")
else:
try:
@@ -303,17 +452,15 @@ def create_gtk_global_override(toast_overlay, settings, gtk_ver):
"Context", "filesystems")
except GLib.GError:
global_keyfile.set_string("Context", "filesystems", gtk_path)
- global_save_keyfile(
- toast_overlay, settings, global_keyfile, filename, gtk_ver
- )
+ __global_save_keyfile(global_keyfile, filename,
+ settings, gtk_ver, toast_overlay)
else:
if gtk_path not in filesys_list:
global_keyfile.set_string_list(
"Context", "filesystems", filesys_list + [gtk_path]
)
- global_save_keyfile(
- toast_overlay, settings, global_keyfile, filename, gtk_ver
- )
+ __global_save_keyfile(global_keyfile, filename,
+ settings, gtk_ver, toast_overlay)
else:
if is_gtk4:
settings.set_boolean("global-flatpak-theming-gtk4", True)
@@ -322,9 +469,9 @@ def create_gtk_global_override(toast_overlay, settings, gtk_ver):
logging.debug("Value already exists.")
-def remove_gtk_global_override(toast_overlay, settings, gtk_ver):
+def remove_gtk_global_override(settings, gtk_ver, toast_overlay=None):
override_dir = GLib.build_filenamev(
- [get_system_flatpak_path(), "overrides"])
+ [__get_system_flatpak_path(), "overrides"])
logging.debug(f"override_dir: {override_dir}")
filename = GLib.build_filenamev([override_dir, "global"])
@@ -355,9 +502,10 @@ def remove_gtk_global_override(toast_overlay, settings, gtk_ver):
set_theming()
logging.warning("remove override: File doesn't exist")
else:
- toast_overlay.add_toast(
- Adw.Toast(title=_("Unexpected file error occurred"))
- )
+ if toast_overlay:
+ toast_overlay.add_toast(
+ Adw.Toast(title=_("Unexpected file error occurred"))
+ )
logging.error(f"Unhandled GLib.FileError error code. Exc: {e}")
else:
try:
@@ -374,9 +522,8 @@ def remove_gtk_global_override(toast_overlay, settings, gtk_ver):
global_keyfile.set_string_list(
"Context", "filesystems", filesys_list)
- global_save_keyfile(
- toast_overlay, settings, global_keyfile, filename, gtk_ver
- )
+ __global_save_keyfile(global_keyfile, filename,
+ settings, gtk_ver, toast_overlay)
logging.debug("remove override: Value removed.")
else:
set_theming()
diff --git a/gradience/backend/globals.py b/gradience/backend/globals.py
new file mode 100644
index 00000000..1f615b34
--- /dev/null
+++ b/gradience/backend/globals.py
@@ -0,0 +1,58 @@
+# globals.py
+#
+# Change the look of Adwaita, with ease
+# Copyright (C) 2022 Gradience Team
+#
+# 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
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program 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 this program. If not, see .
+
+import os
+
+from gi.repository import Xdp
+
+
+presets_dir = os.path.join(
+ os.environ.get("XDG_CONFIG_HOME", os.environ["HOME"] + "/.config"),
+ "presets"
+)
+
+preset_repos = {
+ "Official": "https://github.com/GradienceTeam/Community/raw/next/official.json",
+ "Curated": "https://github.com/GradienceTeam/Community/raw/next/curated.json"
+}
+
+def get_gtk_theme_dir(app_type):
+ if app_type == "gtk4":
+ theme_dir = os.path.join(
+ os.environ.get("XDG_CONFIG_HOME",
+ os.environ["HOME"] + "/.config"),
+ "gtk-4.0"
+ )
+ elif app_type == "gtk3":
+ theme_dir = os.path.join(
+ os.environ.get("XDG_CONFIG_HOME",
+ os.environ["HOME"] + "/.config"),
+ "gtk-3.0"
+ )
+
+ return theme_dir
+
+def is_sandboxed():
+ portal = Xdp.Portal()
+
+ is_sandboxed = self.portal.running_under_sandbox()
+
+ return is_sandboxed
+
+def get_available_sassc():
+ pass
diff --git a/gradience/backend/logger.py b/gradience/backend/logger.py
index efcc7ed6..f25ae378 100644
--- a/gradience/backend/logger.py
+++ b/gradience/backend/logger.py
@@ -25,9 +25,13 @@ class Logger(logging.getLoggerClass()):
"""
This is a wrapper of `logging` module. It provides
custom formatting for log messages.
+
+ Attributes:
+ logger_name (str): Custom name of the logger.
+ formatter (dict): Custom formatter for the logger.
"""
log_colors = {
- "debug": 37,
+ "debug": 32,
"info": 36,
"warning": 33,
"error": 31,
@@ -35,21 +39,31 @@ class Logger(logging.getLoggerClass()):
}
log_format = {
- 'fmt': '\033[1m[%(levelname)s]\033[0m [%(name)s] %(message)s'
+ 'fmt': '[%(name)s] %(message)s'
}
- def __set_color(self, level, message: str):
+ def __set_level_color(self, level, message: str):
if message is not None and "\n" in message:
message = message.replace("\n", "\n\t") + "\n"
color_id = self.log_colors[level]
- return "\033[%dm%s\033[0m" % (color_id, message)
+ return "\033[1;%dm%s:\033[0m %s" % (color_id, level.upper(), message)
- def __init__(self, formatter=None):
+ def __init__(self, logger_name=None, formatter=None):
+ """
+ The constructor for Logger class.
+
+ When initializing this class, you should specify a logger name for debugging purposes,
+ even if you didn't wrote any debug messages in your code.
+ The logger name should usually be a name of your module's main class or module name.
+ """
if formatter is None:
formatter = self.log_format
formatter = logging.Formatter(**formatter)
- self.root.name = "gradience"
+ if logger_name:
+ self.root.name = "Gradience.%s" % (logger_name)
+ else:
+ self.root.name = "Gradience"
if build_type == "debug":
self.root.setLevel(logging.DEBUG)
@@ -62,19 +76,19 @@ class Logger(logging.getLoggerClass()):
self.root.addHandler(handler)
def debug(self, message, **kwargs):
- self.root.debug(self.__set_color("debug", str(message)), )
+ self.root.debug(self.__set_level_color("debug", str(message)), )
def info(self, message, **kwargs):
- self.root.info(self.__set_color("info", str(message)), )
+ self.root.info(self.__set_level_color("info", str(message)), )
def warning(self, message, **kwargs):
- self.root.warning(self.__set_color("warning", str(message)),)
+ self.root.warning(self.__set_level_color("warning", str(message)),)
def error(self, message, **kwargs):
- self.root.error(self.__set_color("error", str(message)), )
+ self.root.error(self.__set_level_color("error", str(message)), )
def critical(self, message, **kwargs):
- self.root.critical(self.__set_color("critical", str(message)), )
+ self.root.critical(self.__set_level_color("critical", str(message)), )
def set_silent(self):
self.root.handlers = []
diff --git a/gradience/backend/meson.build b/gradience/backend/meson.build
index 874407da..90f2734b 100644
--- a/gradience/backend/meson.build
+++ b/gradience/backend/meson.build
@@ -20,12 +20,14 @@ configure_file(
)
subdir('models')
+subdir('theming')
subdir('utils')
gradience_sources = [
'__init__.py',
'css_parser.py',
'flatpak_overrides.py',
+ 'globals.py',
'logger.py',
'preset_downloader.py'
]
diff --git a/gradience/backend/models/preset.py b/gradience/backend/models/preset.py
index 8d0cf6a9..95f8822b 100644
--- a/gradience/backend/models/preset.py
+++ b/gradience/backend/models/preset.py
@@ -19,23 +19,90 @@
import json
import os
-from gradience.frontend.settings_schema import settings_schema
from gradience.backend.utils.common import to_slug_case
+from gradience.backend.globals import presets_dir
from gradience.backend.logger import Logger
logging = Logger()
-presets_dir = os.path.join(
- os.environ.get("XDG_CONFIG_HOME", os.environ["HOME"] + "/.config"),
- "presets",
-)
+# Adwaita default colors palette
+adw_palette = {
+ "blue_": {
+ "1": "#99c1f1",
+ "2": "#62a0ea",
+ "3": "#3584e4",
+ "4": "#1c71d8",
+ "5": "#1a5fb4",
+ },
+ "green_": {
+ "1": "#8ff0a4",
+ "2": "#57e389",
+ "3": "#33d17a",
+ "4": "#2ec27e",
+ "5": "#26a269",
+ },
+ "yellow_": {
+ "1": "#f9f06b",
+ "2": "#f8e45c",
+ "3": "#f6d32d",
+ "4": "#f5c211",
+ "5": "#e5a50a",
+ },
+ "orange_": {
+ "1": "#ffbe6f",
+ "2": "#ffa348",
+ "3": "#ff7800",
+ "4": "#e66100",
+ "5": "#c64600",
+ },
+ "red_": {
+ "1": "#f66151",
+ "2": "#ed333b",
+ "3": "#e01b24",
+ "4": "#c01c28",
+ "5": "#a51d2d",
+ },
+ "purple_": {
+ "1": "#dc8add",
+ "2": "#c061cb",
+ "3": "#9141ac",
+ "4": "#813d9c",
+ "5": "#613583",
+ },
+ "brown_": {
+ "1": "#cdab8f",
+ "2": "#b5835a",
+ "3": "#986a44",
+ "4": "#865e3c",
+ "5": "#63452c",
+ },
+ "light_": {
+ "1": "#ffffff",
+ "2": "#f6f5f4",
+ "3": "#deddda",
+ "4": "#c0bfbc",
+ "5": "#9a9996",
+ },
+ "dark_": {
+ "1": "#77767b",
+ "2": "#5e5c64",
+ "3": "#3d3846",
+ "4": "#241f31",
+ "5": "#000000",
+ }
+}
+
+custom_css_app_types = [
+ "gtk4",
+ "gtk3"
+]
class Preset:
variables = {}
- palette = {}
+ palette = adw_palette
custom_css = {
"gtk4": "",
"gtk3": "",
@@ -43,33 +110,62 @@ class Preset:
plugins = {}
display_name = "New Preset"
preset_path = "new_preset"
+ plugins_list = {}
badges = {}
- def __init__(self, preset_path=None, text=None, preset=None):
- if preset_path:
- self.load_preset(preset_path=preset_path)
- elif text: # load from resource
- self.load_preset(text=text)
- elif preset: # css or dict
- self.load_preset(preset=preset)
- else:
- raise Exception("Failed to create a new Preset object: Preset created without content")
+ def __init__(self):
+ pass
+
+ def new(self, variables: dict, display_name=None, palette=None, custom_css=None, badges=None):
+ self.variables = variables
+
+ if display_name:
+ self.display_name = display_name
+ if palette:
+ self.palette = palette
+ if custom_css:
+ self.custom_css = custom_css
+ if badges:
+ self.badges = badges
+
+ def new_from_path(self, preset_path: str):
+ self.preset_path = preset_path
- def load_preset(self, preset_path=None, text=None, preset=None):
try:
- if not preset:
- if text:
- preset_text = text
- elif preset_path:
- self.preset_path = preset_path
- with open(self.preset_path, "r", encoding="utf-8") as file:
- preset_text = file.read()
- file.close()
- else:
- raise Exception("load_preset must be called with a path, text, or preset")
+ with open(self.preset_path, "r", encoding="utf-8") as file:
+ preset_text = file.read()
+ file.close()
+ except OSError as e:
+ logging.error(f"Failed to read contents of a preset in location: {self.preset_path}. Exc: {e}")
- preset = json.loads(preset_text)
+ try:
+ preset = json.loads(preset_text)
+ except json.JSONDecodeError as e:
+ logging.error(f"Error while decoding JSON data. Exc: {e}")
+ self.__load_values(preset)
+
+ return self
+
+ def new_from_resource(self, text: str):
+ preset_text = text
+
+ try:
+ preset = json.loads(preset_text)
+ except json.JSONDecodeError as e:
+ logging.error(f"Error while decoding JSON data. Exc: {e}")
+
+ self.__load_values(preset)
+
+ return self
+
+ def new_from_dict(self, preset: dict):
+ self.__load_values(preset)
+
+ return self
+
+ def __load_values(self, preset):
+ try:
self.display_name = preset["name"]
self.variables = preset["variables"]
self.palette = preset["palette"]
@@ -82,31 +178,40 @@ class Preset:
if "custom_css" in preset:
self.custom_css = preset["custom_css"]
else:
- for app_type in settings_schema["custom_css_app_types"]:
+ for app_type in custom_css_app_types:
self.custom_css[app_type] = ""
except Exception as e:
- if self.preset_path:
- logging.error(f"Failed to load preset {self.preset_path}. Exc: {e}")
- else:
- logging.error(f"Failed to load preset with unknown path. Exc: {e}")
+ logging.error(f"Failed to create a new preset object. Exc: {e}")
# Rename an existing preset
- def rename_preset(self, name):
+ def rename(self, name):
self.display_name = name
old_path = self.preset_path
self.preset_path = os.path.join(
os.path.dirname(self.preset_path),
to_slug_case(name) + ".json")
- self.save_preset(to=self.preset_path)
+ self.save_to_file(to=self.preset_path)
os.remove(old_path)
+ def get_preset_json(self, indent=None):
+ preset_dict = {
+ "name": self.display_name,
+ "variables": self.variables,
+ "palette": self.palette,
+ "custom_css": self.custom_css,
+ "plugins": self.plugins_list
+ }
+ json_output = json.dumps(preset_dict, indent=indent)
+
+ return json_output
+
# Save a new user preset (or overwrite one)
- def save_preset(self, name=None, plugins_list=None, to=None):
+ def save_to_file(self, name=None, plugins_list=None, to=None):
self.display_name = name if name else self.display_name
if to is None:
- filename = to_slug_case(name) if name else "new_preset"
+ filename = to_slug_case(name) if name else to_slug_case(self.display_name)
self.preset_path = os.path.join(
presets_dir, "user", filename + ".json")
else:
@@ -118,28 +223,29 @@ class Preset:
"user",
)
):
- os.makedirs(
- os.path.join(
- presets_dir,
- "user",
+ try:
+ os.makedirs(
+ os.path.join(
+ presets_dir,
+ "user",
+ )
)
- )
+ except OSError as e:
+ logging.error(f"Failed to create a new preset directory. Exc: {e}")
+ raise
- if plugins_list is None:
- plugins_list = {}
- else:
+ if plugins_list:
plugins_list = plugins_list.save()
- with open(self.preset_path, "w", encoding="utf-8") as file:
- object_to_write = {
- "name": self.display_name,
- "variables": self.variables,
- "palette": self.palette,
- "custom_css": self.custom_css,
- "plugins": plugins_list,
- }
- file.write(json.dumps(object_to_write, indent=4))
- file.close()
+ try:
+ with open(self.preset_path, "w", encoding="utf-8") as file:
+ content = self.get_preset_json(indent=4)
+ file.write(content)
+ file.close()
+ except OSError as e:
+ logging.error(f"Failed to save preset as a file. Exc: {e}")
+ raise
+ # TODO: Add validation
def validate(self):
return True
diff --git a/gradience/backend/models/repo.py b/gradience/backend/models/repo.py
index b6d9bf4b..1ae42a8c 100644
--- a/gradience/backend/models/repo.py
+++ b/gradience/backend/models/repo.py
@@ -19,7 +19,8 @@
import os
from gradience.backend.utils.common import to_slug_case
-from gradience.backend.models.preset import Preset, presets_dir
+from gradience.backend.globals import presets_dir
+from gradience.backend.models.preset import Preset
class Repo:
@@ -34,5 +35,6 @@ class Repo:
presets = {}
for preset in os.listdir(self.path):
if preset.endswith(".json"):
- presets[preset[:-5]] = Preset(os.path.join(self.path, preset))
+ preset_path = os.path.join(self.path, preset)
+ presets[preset[:-5]] = Preset().new_from_path(preset_path)
return presets
diff --git a/gradience/backend/preset_downloader.py b/gradience/backend/preset_downloader.py
index 4f6329c1..c5c2557a 100644
--- a/gradience/backend/preset_downloader.py
+++ b/gradience/backend/preset_downloader.py
@@ -21,7 +21,7 @@ import json
from gi.repository import GLib, Soup
-from gradience.backend.models.preset import presets_dir
+from gradience.backend.globals import presets_dir
from gradience.backend.utils.common import to_slug_case
from gradience.backend.logger import Logger
@@ -29,74 +29,77 @@ from gradience.backend.logger import Logger
logging = Logger()
-# Open Soup3 session
-session = Soup.Session()
+class PresetDownloader:
+ def __init__(self):
+ # Open Soup3 session
+ self.session = Soup.Session()
-def fetch_presets(repo) -> [dict, list]:
- try:
- request = Soup.Message.new("GET", repo)
- body = session.send_and_read(request, None)
- except GLib.GError as e: # offline
- if e.code == 1:
- logging.error(f"Failed to establish a new connection. Exc: {e}")
- return False, False
- else:
- logging.error(f"Unhandled Libsoup3 GLib.GError error code {e.code}. Exc: {e}")
- return False, False
- try:
- raw = json.loads(body.get_data())
- except json.JSONDecodeError as e:
- logging.error(f"Error with decoding JSON data. Exc: {e}")
- return False, False
+ def fetch_presets(self, repo) -> [dict, list]:
+ try:
+ request = Soup.Message.new("GET", repo)
+ body = self.session.send_and_read(request, None)
+ except GLib.GError as e:
+ if e.code == 1: # offline
+ logging.error(f"Failed to establish a new connection. Exc: {e}")
+ raise
+ else:
+ logging.error(f"Unhandled Libsoup3 GLib.GError error code {e.code}. Exc: {e}")
+ raise
+ try:
+ raw = json.loads(body.get_data())
+ except json.JSONDecodeError as e:
+ logging.error(f"Error while decoding JSON data. Exc: {e}")
+ raise
- preset_dict = {}
- url_list = []
+ preset_dict = {}
+ url_list = []
- for data in raw.items():
- data = list(data)
- data.insert(0, to_slug_case(data[0]))
+ for data in raw.items():
+ data = list(data)
+ data.insert(0, to_slug_case(data[0]))
- url = data[2]
- data.pop(2) # Remove preset URL from list
+ url = data[2]
+ data.pop(2) # Remove preset URL from list
- to_dict = iter(data)
- # Convert list back to dict
- preset_dict.update(dict(zip(to_dict, to_dict)))
+ to_dict = iter(data)
+ # Convert list back to dict
+ preset_dict.update(dict(zip(to_dict, to_dict)))
- url_list.append(url)
+ url_list.append(url)
- return preset_dict, url_list
+ return preset_dict, url_list
-def download_preset(name, repo_name, repo) -> None:
- try:
- request = Soup.Message.new("GET", repo)
- body = session.send_and_read(request, None)
- except GLib.GError as e: # offline
- if e.code == 1:
- logging.error(f"Failed to establish a new connection. Exc: {e}")
- return False, False
- else:
- logging.error(f"Unhandled Libsoup3 GLib.GError error code {e.code}. Exc: {e}")
- return False, False
- try:
- raw = json.loads(body.get_data())
- except json.JSONDecodeError as e:
- logging.error(f"Error with decoding JSON data. Exc: {e}")
- return False, False
+ def download_preset(self, name, repo_name, repo) -> None:
+ try:
+ request = Soup.Message.new("GET", repo)
+ body = self.session.send_and_read(request, None)
+ except GLib.GError as e:
+ if e.code == 1: # offline
+ logging.error(f"Failed to establish a new connection. Exc: {e}")
+ raise
+ else:
+ logging.error(f"Unhandled Libsoup3 GLib.GError error code {e.code}. Exc: {e}")
+ raise
+ try:
+ raw = json.loads(body.get_data())
+ except json.JSONDecodeError as e:
+ logging.error(f"Error while decoding JSON data. Exc: {e}")
+ raise
- data = json.dumps(raw, indent=4)
+ data = json.dumps(raw, indent=4)
- try:
- with open(
- os.path.join(
- presets_dir,
- repo_name,
- to_slug_case(name) + ".json",
- ),
- "w",
- encoding="utf-8",
- ) as f:
- f.write(data)
- f.close()
- except OSError as e:
- logging.error(f"Failed to write data to a file. Exc: {e}")
+ try:
+ with open(
+ os.path.join(
+ presets_dir,
+ repo_name,
+ to_slug_case(name) + ".json",
+ ),
+ "w",
+ encoding="utf-8",
+ ) as f:
+ f.write(data)
+ f.close()
+ except OSError as e:
+ logging.error(f"Failed to write data to a file. Exc: {e}")
+ raise
diff --git a/gradience/backend/theming/__init__.py b/gradience/backend/theming/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/gradience/backend/theming/meson.build b/gradience/backend/theming/meson.build
new file mode 100644
index 00000000..8b40abbc
--- /dev/null
+++ b/gradience/backend/theming/meson.build
@@ -0,0 +1,8 @@
+themingdir = 'gradience/backend/theming'
+
+gradience_sources = [
+ '__init__.py',
+ 'monet.py',
+ 'preset_utils.py'
+]
+PY_INSTALLDIR.install_sources(gradience_sources, subdir: themingdir)
diff --git a/gradience/backend/theming/monet.py b/gradience/backend/theming/monet.py
new file mode 100644
index 00000000..057f4dd8
--- /dev/null
+++ b/gradience/backend/theming/monet.py
@@ -0,0 +1,63 @@
+# monet.py
+#
+# Change the look of Adwaita, with ease
+# Copyright (C) 2022 Gradience Team
+#
+# 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
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program 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 this program. If not, see .
+
+import os
+
+from svglib.svglib import svg2rlg
+from reportlab.graphics import renderPM
+import material_color_utilities_python as monet
+
+from gradience.backend.models.preset import Preset
+
+from gradience.backend.logger import Logger
+
+logging = Logger()
+
+
+class Monet:
+ def __init__(self):
+ self.palette = None
+
+ def generate_from_image(self, image_path: str) -> dict:
+ if image_path.endswith(".svg"):
+ drawing = svg2rlg(image_path)
+ image_path = os.path.join(
+ os.environ.get("XDG_RUNTIME_DIR"), "gradience_bg.png"
+ )
+ renderPM.drawToFile(drawing, image_path, fmt="PNG")
+
+ if image_path.endswith(".xml"):
+ logging.error(f"XML files are unsupported by Gradience's Monet implementation.")
+ return False
+
+ try:
+ monet_img = monet.Image.open(image_path)
+ except Exception as e:
+ logging.error(f"An error occurred while generating a Monet palette. Exc: {e}")
+ return False
+ else:
+ basewidth = 64
+ wpercent = basewidth / float(monet_img.size[0])
+ hsize = int((float(monet_img.size[1]) * float(wpercent)))
+ monet_img = monet_img.resize(
+ (basewidth, hsize), monet.Image.Resampling.LANCZOS
+ )
+
+ self.palette = monet.themeFromImage(monet_img)
+
+ return self.palette
diff --git a/gradience/backend/theming/preset_utils.py b/gradience/backend/theming/preset_utils.py
new file mode 100644
index 00000000..aeb01541
--- /dev/null
+++ b/gradience/backend/theming/preset_utils.py
@@ -0,0 +1,366 @@
+# preset_utils.py
+#
+# Change the look of Adwaita, with ease
+# Copyright (C) 2022 Gradience Team
+#
+# 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
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program 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 this program. If not, see .
+
+import os
+import json
+from pathlib import Path
+
+from gi.repository import GLib, Gio
+
+from gradience.backend.theming.monet import Monet
+from gradience.backend.models.preset import Preset
+from gradience.backend.utils.colors import rgba_from_argb
+
+from gradience.backend.globals import presets_dir, get_gtk_theme_dir
+
+from gradience.backend.logger import Logger
+
+logging = Logger()
+
+
+class PresetUtils:
+ def __init__(self):
+ self.preset = Preset()
+
+ def generate_gtk_css(self, app_type: str, preset: Preset) -> str:
+ variables = preset.variables
+ palette = preset.palette
+ custom_css = preset.custom_css
+
+ final_css = ""
+
+ for key in variables.keys():
+ final_css += f"@define-color {key} {variables[key]};\n"
+
+ for prefix_key in palette.keys():
+ for key in palette[prefix_key].keys():
+ final_css += f"@define-color {prefix_key + key} {palette[prefix_key][key]};\n"
+
+ final_css += custom_css.get(app_type, "")
+
+ return final_css
+
+ def new_preset_from_monet(self, name=None, monet_palette=None, props=None, obj_only=False) -> Preset or None:
+ if props:
+ tone = props[0]
+ theme = props[1]
+ else:
+ raise Exception("Properties 'tone' and/or 'theme' missing")
+
+ if not monet_palette:
+ raise Exception("Property 'monet_palette' missing")
+
+ if theme == "dark":
+ dark_theme = monet_palette["schemes"]["dark"]
+ variable = {
+ "accent_color": rgba_from_argb(dark_theme.primary),
+ "accent_bg_color": rgba_from_argb(dark_theme.primaryContainer),
+ "accent_fg_color": rgba_from_argb(dark_theme.onPrimaryContainer),
+ "destructive_color": rgba_from_argb(dark_theme.error),
+ "destructive_bg_color": rgba_from_argb(dark_theme.errorContainer),
+ "destructive_fg_color": rgba_from_argb(
+ dark_theme.onErrorContainer
+ ),
+ "success_color": rgba_from_argb(dark_theme.tertiary),
+ "success_bg_color": rgba_from_argb(dark_theme.onTertiary),
+ "success_fg_color": rgba_from_argb(dark_theme.onTertiaryContainer),
+ "warning_color": rgba_from_argb(dark_theme.secondary),
+ "warning_bg_color": rgba_from_argb(dark_theme.onSecondary),
+ "warning_fg_color": rgba_from_argb(dark_theme.primary, "0.8"),
+ "error_color": rgba_from_argb(dark_theme.error),
+ "error_bg_color": rgba_from_argb(dark_theme.errorContainer),
+ "error_fg_color": rgba_from_argb(dark_theme.onError),
+ "window_bg_color": rgba_from_argb(dark_theme.surface),
+ "window_fg_color": rgba_from_argb(dark_theme.onSurface),
+ "view_bg_color": rgba_from_argb(dark_theme.surface),
+ "view_fg_color": rgba_from_argb(dark_theme.onSurface),
+ "headerbar_bg_color": rgba_from_argb(dark_theme.surface),
+ "headerbar_fg_color": rgba_from_argb(dark_theme.onSurface),
+ "headerbar_border_color": rgba_from_argb(
+ dark_theme.primary, "0.8"
+ ),
+ "headerbar_backdrop_color": "@headerbar_bg_color",
+ "headerbar_shade_color": rgba_from_argb(dark_theme.shadow),
+ "card_bg_color": rgba_from_argb(dark_theme.primary, "0.05"),
+ "card_fg_color": rgba_from_argb(dark_theme.onSecondaryContainer),
+ "card_shade_color": rgba_from_argb(dark_theme.shadow),
+ "dialog_bg_color": rgba_from_argb(dark_theme.secondaryContainer),
+ "dialog_fg_color": rgba_from_argb(dark_theme.onSecondaryContainer),
+ "popover_bg_color": rgba_from_argb(dark_theme.secondaryContainer),
+ "popover_fg_color": rgba_from_argb(
+ dark_theme.onSecondaryContainer
+ ),
+ "shade_color": rgba_from_argb(dark_theme.shadow),
+ "scrollbar_outline_color": rgba_from_argb(dark_theme.outline),
+ }
+ elif theme == "light":
+ light_theme = monet_palette["schemes"]["light"]
+ variable = {
+ "accent_color": rgba_from_argb(light_theme.primary),
+ "accent_bg_color": rgba_from_argb(light_theme.primary),
+ "accent_fg_color": rgba_from_argb(light_theme.onPrimary),
+ "destructive_color": rgba_from_argb(light_theme.error),
+ "destructive_bg_color": rgba_from_argb(light_theme.errorContainer),
+ "destructive_fg_color": rgba_from_argb(
+ light_theme.onErrorContainer
+ ),
+ "success_color": rgba_from_argb(light_theme.tertiary),
+ "success_bg_color": rgba_from_argb(light_theme.tertiaryContainer),
+ "success_fg_color": rgba_from_argb(
+ light_theme.onTertiaryContainer
+ ),
+ "warning_color": rgba_from_argb(light_theme.secondary),
+ "warning_bg_color": rgba_from_argb(light_theme.secondaryContainer),
+ "warning_fg_color": rgba_from_argb(
+ light_theme.onSecondaryContainer
+ ),
+ "error_color": rgba_from_argb(light_theme.error),
+ "error_bg_color": rgba_from_argb(light_theme.errorContainer),
+ "error_fg_color": rgba_from_argb(light_theme.onError),
+ "window_bg_color": rgba_from_argb(light_theme.secondaryContainer),
+ "window_fg_color": rgba_from_argb(light_theme.onSurface),
+ "view_bg_color": rgba_from_argb(light_theme.secondaryContainer),
+ "view_fg_color": rgba_from_argb(light_theme.onSurface),
+ "headerbar_bg_color": rgba_from_argb(
+ light_theme.secondaryContainer
+ ),
+ "headerbar_fg_color": rgba_from_argb(light_theme.onSurface),
+ "headerbar_border_color": rgba_from_argb(
+ light_theme.primary, "0.8"
+ ),
+ "headerbar_backdrop_color": "@headerbar_bg_color",
+ "headerbar_shade_color": rgba_from_argb(
+ light_theme.secondaryContainer
+ ),
+ "card_bg_color": rgba_from_argb(light_theme.primary, "0.05"),
+ "card_fg_color": rgba_from_argb(light_theme.onSecondaryContainer),
+ "card_shade_color": rgba_from_argb(light_theme.shadow),
+ "dialog_bg_color": rgba_from_argb(light_theme.secondaryContainer),
+ "dialog_fg_color": rgba_from_argb(
+ light_theme.onSecondaryContainer
+ ),
+ "popover_bg_color": rgba_from_argb(light_theme.secondaryContainer),
+ "popover_fg_color": rgba_from_argb(
+ light_theme.onSecondaryContainer
+ ),
+ "shade_color": rgba_from_argb(light_theme.shadow),
+ "scrollbar_outline_color": rgba_from_argb(light_theme.outline),
+ }
+
+ if obj_only == False and not name:
+ raise Exception("You either need to set 'obj_only' property to True, or add value to 'name' property")
+
+ if obj_only:
+ if name:
+ print("with name, obj_only")
+ self.preset.new(variables=variable, display_name=name)
+ else:
+ print("no name, obj_only")
+ self.preset.new(variables=variable)
+ return self.preset
+
+ if obj_only == False:
+ print("no obj_only, name")
+ self.preset.new(variables=variable, display_name=name)
+
+ try:
+ self.preset.save_to_file()
+ except Exception as e:
+ # TODO: Move exception handling to model/preset module
+ logging.error(f"Unexpected file error while trying to generate preset from Monet palette. Exc: {e}")
+ raise
+
+ def get_presets_list(self, json_output=False) -> dict:
+ presets_list = {}
+
+ for repo in Path(presets_dir).iterdir():
+ logging.debug(f"presets_dir.iterdir: {repo}")
+ if repo.is_dir():
+ for file_name in repo.iterdir():
+ file_name = str(file_name)
+ if file_name.endswith(".json"):
+ try:
+ with open(
+ os.path.join(presets_dir, file_name),
+ "r",
+ encoding="utf-8",
+ ) as file:
+ preset_text = file.read()
+ file.close()
+ preset = json.loads(preset_text)
+ if preset.get("variables") is None:
+ raise KeyError("'variables' section missing in loaded preset file")
+ if preset.get("palette") is None:
+ raise KeyError("'palette' section missing in loaded preset file")
+ presets_list[file_name] = preset[
+ "name"
+ ]
+ except (OSError, KeyError) as e:
+ logging.error(f"Failed to load an preset information. Exc: {e}")
+ raise
+ elif repo.is_file():
+ # this exists to keep compatibility with old presets
+ if repo.name.endswith(".json"):
+ logging.warning("Legacy preset found. Moving to new structure.")
+ if not os.path.isdir(os.path.join(presets_dir, "user")):
+ os.mkdir(os.path.join(presets_dir, "user"))
+
+ os.rename(repo, os.path.join(
+ presets_dir, "user", repo.name))
+
+ try:
+ with open(
+ os.path.join(presets_dir, "user", repo),
+ "r",
+ encoding="utf-8",
+ ) as file:
+ preset_text = file.read()
+ preset = json.loads(preset_text)
+ if preset.get("variables") is None:
+ raise KeyError("'variables' section missing in loaded preset file")
+ if preset.get("palette") is None:
+ raise KeyError("'palette' section missing in loaded preset file")
+ presets_list["user"][file_name] = preset[
+ "name"
+ ]
+ except (OSError, KeyError) as e:
+ logging.error(f"Failed to load an preset information. Exc: {e}")
+ raise
+ if json_output:
+ json_output = json.dumps(presets_list)
+ return json_output
+ return presets_list
+
+ def apply_preset(self, app_type: str, preset: Preset) -> None:
+ if app_type == "gtk4":
+ theme_dir = get_gtk_theme_dir(app_type)
+
+ if not os.path.exists(theme_dir):
+ os.makedirs(theme_dir)
+
+ gtk4_css = self.generate_gtk_css("gtk4", preset)
+ contents = ""
+
+ try:
+ with open(
+ os.path.join(theme_dir, "gtk.css"), "r", encoding="utf-8"
+ ) as file:
+ contents = file.read()
+ except FileNotFoundError: # first run
+ pass
+ else:
+ with open(
+ os.path.join(theme_dir, "gtk.css.bak"), "w", encoding="utf-8"
+ ) as file:
+ file.write(contents)
+ finally:
+ with open(
+ os.path.join(theme_dir, "gtk.css"), "w", encoding="utf-8"
+ ) as file:
+ file.write(gtk4_css)
+ elif app_type == "gtk3":
+ theme_dir = get_gtk_theme_dir(app_type)
+
+ if not os.path.exists(theme_dir):
+ os.makedirs(theme_dir)
+
+ gtk3_css = self.generate_gtk_css("gtk3", preset)
+ contents = ""
+
+ try:
+ with open(
+ os.path.join(theme_dir, "gtk.css"), "r", encoding="utf-8"
+ ) as file:
+ contents = file.read()
+ except FileNotFoundError: # first run
+ pass
+ else:
+ with open(
+ os.path.join(theme_dir, "gtk.css.bak"), "w", encoding="utf-8"
+ ) as file:
+ file.write(contents)
+ finally:
+ with open(
+ os.path.join(theme_dir, "gtk.css"), "w", encoding="utf-8"
+ ) as file:
+ file.write(gtk3_css)
+
+ def restore_gtk4_preset(self) -> None:
+ try:
+ with open(
+ os.path.join(
+ os.environ.get(
+ "XDG_CONFIG_HOME", os.environ["HOME"] +
+ "/.config"
+ ),
+ "gtk-4.0/gtk.css.bak",
+ ),
+ "r",
+ encoding="utf-8",
+ ) as backup:
+ contents = backup.read()
+ backup.close()
+
+ with open(
+ os.path.join(
+ os.environ.get(
+ "XDG_CONFIG_HOME", os.environ["HOME"] +
+ "/.config"
+ ),
+ "gtk-4.0/gtk.css",
+ ),
+ "w",
+ encoding="utf-8",
+ ) as gtk4css:
+ gtk4css.write(contents)
+ gtk4css.close()
+ except OSError as e:
+ logging.error(f"Unable to restore Gtk4 backup. Exc: {e}")
+ raise
+
+ def reset_preset(self, app_type: str) -> None:
+ if app_type == "gtk4":
+ file = Gio.File.new_for_path(
+ os.path.join(
+ os.environ.get(
+ "XDG_CONFIG_HOME", os.environ["HOME"] + "/.config"
+ ),
+ "gtk-4.0/gtk.css",
+ )
+ )
+
+ try:
+ file.delete()
+ except GLib.GError as e:
+ logging.error(f"Unable to delete current preset. Exc: {e}")
+ raise
+ elif app_type == "gtk3":
+ file = Gio.File.new_for_path(
+ os.path.join(
+ os.environ.get(
+ "XDG_CONFIG_HOME", os.environ["HOME"] + "/.config"
+ ),
+ "gtk-3.0/gtk.css",
+ )
+ )
+
+ try:
+ file.delete()
+ except GLib.GError as e:
+ logging.error(f"Unable to delete current preset. Exc: {e}")
+ raise
diff --git a/gradience/backend/utils/colors.py b/gradience/backend/utils/colors.py
new file mode 100644
index 00000000..4163e83f
--- /dev/null
+++ b/gradience/backend/utils/colors.py
@@ -0,0 +1,31 @@
+# colors.py
+#
+# Change the look of Adwaita, with ease
+# Copyright (C) 2022 Gradience Team
+#
+# 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
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program 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 this program. If not, see .
+
+import material_color_utilities_python as monet
+
+
+def rgba_from_argb(argb, alpha=None) -> str:
+ base = "rgba({}, {}, {}, {})"
+
+ red = monet.redFromArgb(argb)
+ green = monet.greenFromArgb(argb)
+ blue = monet.blueFromArgb(argb)
+ if not alpha:
+ alpha = monet.alphaFromArgb(argb)
+
+ return base.format(red, green, blue, alpha)
diff --git a/gradience/backend/utils/common.py b/gradience/backend/utils/common.py
index 611ce3ed..34b946ab 100644
--- a/gradience/backend/utils/common.py
+++ b/gradience/backend/utils/common.py
@@ -23,7 +23,7 @@ import subprocess
from anyascii import anyascii
-def to_slug_case(non_slug):
+def to_slug_case(non_slug) -> str:
return re.sub(r"[^0-9a-z]+", "-", anyascii(non_slug).lower()).strip("-")
def run_command(command, *args, **kwargs):
diff --git a/gradience/backend/utils/meson.build b/gradience/backend/utils/meson.build
index 588667a5..30855aa5 100644
--- a/gradience/backend/utils/meson.build
+++ b/gradience/backend/utils/meson.build
@@ -2,6 +2,7 @@ utilsdir = 'gradience/backend/utils'
gradience_sources = [
'__init__.py',
+ 'colors.py',
'common.py'
]
PY_INSTALLDIR.install_sources(gradience_sources, subdir: utilsdir)
diff --git a/gradience/frontend/cli/__init__.py b/gradience/frontend/cli/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/gradience/frontend/cli/cli.in b/gradience/frontend/cli/cli.in
new file mode 100755
index 00000000..f4171847
--- /dev/null
+++ b/gradience/frontend/cli/cli.in
@@ -0,0 +1,416 @@
+#!/usr/bin/env python3
+
+# cli.in
+#
+# Change the look of Adwaita, with ease
+# Copyright (C) 2022 Gradience Team
+#
+# 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
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program 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 this program. If not, see .
+
+import os
+import sys
+import json
+import shutil
+import signal
+import argparse
+import warnings
+
+version = "@VERSION@"
+is_local = @local_build@
+
+if is_local:
+ # In the local use case, use gradience module from the sourcetree
+ sys.path.insert(1, '@PYTHON@')
+
+ # In the local use case the installed schemas go in /data
+ os.environ["XDG_DATA_DIRS"] = '@SCHEMAS_DIR@:' + os.environ.get("XDG_DATA_DIRS", "")
+
+signal.signal(signal.SIGINT, signal.SIG_DFL)
+
+warnings.filterwarnings("ignore") # suppress GTK warnings
+
+from gi.repository import GLib, Gio
+
+from gradience.backend.utils.common import to_slug_case
+from gradience.backend.globals import preset_repos, presets_dir
+
+from gradience.backend.theming.monet import Monet
+from gradience.backend.models.preset import Preset
+from gradience.backend.theming.preset_utils import PresetUtils
+from gradience.backend.preset_downloader import PresetDownloader
+from gradience.backend.flatpak_overrides import list_file_access, allow_file_access, disallow_file_access, create_gtk_user_override, remove_gtk_user_override
+
+from gradience.backend.logger import Logger
+
+logging = Logger()
+
+
+class CLI:
+ settings = Gio.Settings.new("@APP_ID@")
+
+ def __init__(self):
+ self.parser = argparse.ArgumentParser(description="Gradience - Change the look of Adwaita, with ease")
+ self.parser.add_argument("-V", "--version", action="version", version=f"Gradience, version {version}")
+ #self.parser.add_argument("-j", "--json", action="store_true", help="print out a result of the command directly in JSON format")
+ #self.parser.add_argument('-J', '--pretty-json', dest='pretty_json', action='store_true', help='pretty-print JSON output')
+
+ subparsers = self.parser.add_subparsers(dest="command")
+
+ #info_parser = subparsers.add_parser("info", help="show information about Gradience")
+
+ presets_parser = subparsers.add_parser("presets", help="list installed presets")
+ #presets_parser.add_argument("-r", "--remove-preset", metavar="PRESET_NAME", help="remove a preset from the list")
+ presets_parser.add_argument("-j", "--json", action="store_true", help="print out a result of this command directly in JSON format")
+
+ favorites_parser = subparsers.add_parser("favorites", help="list favorite presets")
+ favorites_parser.add_argument("-a", "--add-preset", metavar="PRESET_NAME", help="add a preset to favorites")
+ favorites_parser.add_argument("-r", "--remove-preset", metavar="PRESET_NAME", help="remove a preset from favorites")
+ favorites_parser.add_argument("-j", "--json", action="store_true", help="print out a result of this command directly in JSON format")
+
+ import_parser = subparsers.add_parser("import", help="import a preset")
+ import_parser.add_argument("-p", "--preset-path", help="absolute path to a preset file", required=True)
+
+ apply_parser = subparsers.add_parser("apply", help="apply a preset")
+ apply_group = apply_parser.add_mutually_exclusive_group(required=True)
+ apply_group.add_argument("-n", "--preset-name", help="display name for a preset")
+ apply_group.add_argument("-p", "--preset-path", help="absolute path to a preset file")
+ apply_parser.add_argument("--gtk", choices=["gtk4", "gtk3", "both"], default="gtk4", help="types of applications you want to theme (default: gtk4)")
+ #apply_parser.add_argument("--flatpak", choices=["gtk4", "gtk3", "both"], help="types of Flatpak applications you want to theme (for GTK3 option, make sure you have adw-gtk3 installed as Flatpak)")
+
+ #new_parser = subparsers.add_parser("new", help="create a new preset")
+ #new_parser.add_argument("-i", "--interactive", action="store_true", help="")
+ #new_parser.add_argument("-n", "--name", help="display name for a preset", required=True)
+ #new_parser.add_argument("--colors", help="", required=True)
+ #new_parser.add_argument("--palette", help="")
+ #new_parser.add_argument("--custom-css", help="")
+ #new_parser.add_argument("-j", "--json", action="store_true", help="print out a result of this command directly in JSON format")
+
+ download_parser = subparsers.add_parser("download", help="download preset from a preset repository")
+ #new_parser.add_argument("-i", "--interactive", action="store_true", help="")
+ download_parser.add_argument("-n", "--preset-name", help="name of a preset you want to get", required=True)
+ #download_parser.add_argument("--custom-url", help="use custom repository's presets.json to download other presets")
+
+ monet_parser = subparsers.add_parser("monet", help="generate Material You preset from an image")
+ monet_parser.add_argument("-n", "--preset-name", help="name for a generated preset", required=True)
+ monet_parser.add_argument("-p", "--image-path", help="abosulte path to image", required=True)
+ monet_parser.add_argument("--tone", default=20, help="a tone for colors (default: 20)")
+ monet_parser.add_argument("--theme", choices=["light", "dark"], default="light", help="choose whatever it should be a light or dark theme (default: light)")
+ monet_parser.add_argument("-j", "--json", action="store_true", help="print out a result of this command directly in JSON format")
+
+ access_parser = subparsers.add_parser("access-file", help="allow or disallow Gradience to access a certain file or directory")
+ access_parser.add_argument("-l", "--list", action="store_true", help="list allowed directories and files")
+ access_group = access_parser.add_mutually_exclusive_group(required=False)
+ access_group.add_argument("-a", "--allow", metavar="PATH", help="allow Gradience access to this file or directory")
+ access_group.add_argument("-d", "--disallow", metavar="PATH", help="disallow Gradience access to this file or directory")
+
+ overrides_parser = subparsers.add_parser("flatpak-overrides", help="enable or disable Flatpak theming")
+ overrides_group = overrides_parser.add_mutually_exclusive_group(required=True)
+ overrides_group.add_argument("-e", "--enable-theming", choices=["gtk4", "gtk3", "both"], help="enable overrides for Flatpak theming")
+ overrides_group.add_argument("-d", "--disable-theming", choices=["gtk4", "gtk3", "both"], help="disable overrides for Flatpak theming")
+
+ self.__parse_args()
+
+ def __print_json(self, data, pretty=False):
+ if pretty:
+ print(json.dumps(data, indent=4))
+ else:
+ print(json.dumps(data))
+
+ def __parse_args(self):
+ args = self.parser.parse_args()
+
+ if not args.command:
+ print(self.parser.format_help())
+
+ if args.command == "presets":
+ self.list_presets(args)
+
+ elif args.command == "favorites":
+ self.favorite_presets(args)
+
+ elif args.command == "import":
+ self.import_preset(args)
+
+ elif args.command == "apply":
+ self.apply_preset(args)
+
+ elif args.command == "new":
+ self.new_preset(args)
+
+ elif args.command == "download":
+ self.download_preset(args)
+
+ elif args.command == "monet":
+ self.generate_monet(args)
+
+ elif args.command == "access-file":
+ self.access_file(args)
+
+ elif args.command == "flatpak-overrides":
+ self.flatpak_theming(args)
+
+ def list_presets(self, args):
+ #_remove_preset = args.remove_preset
+ _json = args.json
+
+ if _json:
+ presets_list = PresetUtils().get_presets_list(json_output=True)
+ print(presets_list)
+ exit(0)
+
+ presets_list = PresetUtils().get_presets_list()
+
+ # TODO: Modify this output to look more like a table (maybe use ncurses?)
+ print("\033[1;37mPreset name\033[0m | \033[1;37mPreset path\033[0m")
+ for key in presets_list:
+ print(f"{presets_list[key]} -> {key}")
+
+ def favorite_presets(self, args):
+ _add_preset = args.add_preset
+ _remove_preset = args.remove_preset
+ _json = args.json
+
+ favorite = set(self.settings.get_value("favourite"))
+ presets_list = PresetUtils().get_presets_list()
+ presets_name = list(presets_list.values())
+
+ if _json and not _add_preset and not _remove_preset:
+ favorites_json = {"favorites": list(favorite), "amount": len(favorite)}
+ json_output = json.dumps(favorites_json)
+ print(json_output)
+ exit(0)
+ elif _json and _add_preset or _json and _remove_preset:
+ logging.error("JSON output option isn't available for --add-preset and --remove-preset options.")
+ exit(1)
+
+ if _add_preset:
+ if _add_preset in presets_name:
+ favorite.add(_add_preset)
+ self.settings.set_value("favourite", GLib.Variant("as", favorite))
+ logging.info(f"Preset {_add_preset} has been added to favorites.")
+ exit(0)
+ else:
+ logging.error(f"Preset named {_add_preset} isn't installed in Gradience. "
+ "Check if you typed the correct preset name, or try importing your preset using 'import' command.")
+ exit(1)
+
+ if _remove_preset:
+ if _remove_preset in favorite:
+ favorite.remove(_remove_preset)
+ self.settings.set_value("favourite", GLib.Variant("as", favorite))
+ logging.info(f"Preset {_add_preset} has been removed from favorites.")
+ exit(0)
+ else:
+ logging.error(f"Preset named {_remove_preset} doesn't exist in favorites list. "
+ "Check if you typed the correct preset name.")
+ exit(1)
+
+ logging.info("Favorite presets list:")
+ for i, preset in enumerate(favorite):
+ print(preset)
+
+ logging.info(f"Favorites amount: {len(favorite)}")
+ exit(0)
+
+ def import_preset(self, args):
+ _preset_path = args.preset_path
+
+ preset_file = GLib.path_get_basename(_preset_path)
+ logging.info(f"Importing preset: {preset_file.strip()}")
+
+ # TODO: Check if preset is already imported
+ if _preset_path.endswith(".json"):
+ try:
+ shutil.copy(
+ _preset_path,
+ os.path.join(
+ presets_dir,
+ "user",
+ preset_file.strip()
+ )
+ )
+ except FileNotFoundError as e:
+ logging.error(f"Preset could not be imported. Exc: {e}")
+ exit(1)
+ logging.info("Preset imported successfully.")
+ else:
+ logging.error("Unsupported file format, must be .json")
+ exit(1)
+
+ def apply_preset(self, args):
+ _preset_name = args.preset_name
+ _preset_path = args.preset_path
+ _gtk = args.gtk
+ #_flatpak = args.flatpak
+
+ presets_list = PresetUtils().get_presets_list()
+ presets_name = list(presets_list.values())
+
+ if _preset_name:
+ if _preset_name in presets_name:
+ for path, name in presets_list.items():
+ if name == _preset_name:
+ preset = Preset().new_from_path(path)
+ elif _preset_path:
+ preset = Preset().new_from_path(_preset_path)
+
+ if _gtk == "gtk4":
+ PresetUtils().apply_preset("gtk4", preset)
+ logging.info(f"Preset {preset.display_name} applied successfully for Gtk 4 applications.")
+ elif _gtk == "gtk3":
+ PresetUtils().apply_preset("gtk3", preset)
+ logging.info(f"Preset {preset.display_name} applied successfully for Gtk 3 applications.")
+ logging.info("In order for changes to take full effect, you need to log out.")
+
+ def new_preset(self, args):
+ #_interactive = args.interactive
+ _name = args.name
+ _colors = args.colors
+ _palette = args.palette
+ _custom_css = args.custom_css
+ _json = args.json
+
+ # TODO: Do the logic code for `new` command
+
+ logging.error("This command isn't implemented yet")
+ exit(1)
+
+ def download_preset(self, args):
+ #_interactive = args.interactive
+ _preset_name = args.preset_name
+ #_custom_url = args.custom_url
+
+ repo_no = 1
+ repos_amount = len(preset_repos.items())
+ for repo_name, repo in preset_repos.items():
+ try:
+ explore_presets, urls = PresetDownloader().fetch_presets(repo)
+ except (GLib.GError, json.JSONDecodeError) as e:
+ logging.error(f"An error occurred while fetching presets from remote repository. Exc: {e}")
+ exit(1)
+ else:
+ preset_no = 1
+ presets_amount = len(explore_presets.items())
+ for (preset, preset_name), preset_url in zip(explore_presets.items(), urls):
+ # TODO: Add handling of two or more presets with the same elements in name
+ if _preset_name.lower() in preset_name.lower():
+ logging.info(f"Downloading preset: {preset_name}")
+ try:
+ PresetDownloader().download_preset(preset_name, to_slug_case(repo_name), preset_url)
+ except (GLib.GError, json.JSONDecodeError, OSError) as e:
+ logging.error(f"An error occurred while downloading a preset. Exc: {e}")
+ exit(1)
+ else:
+ logging.info("Preset downloaded successfully.")
+ exit(0)
+ else:
+ if repo_no == repos_amount and preset_no == presets_amount:
+ logging.error(f"No presets found with text: {_preset_name}")
+ exit(1)
+ preset_no += 1
+ continue
+ repo_no += 1
+
+ # NOTE: Possible useful portals to use in future: org.freedesktop.portal.Documents (support missing in libportal, only D-Bus calls), org.freedesktop.portal.FileChooser
+ def generate_monet(self, args):
+ _preset_name = args.preset_name
+ _image_path = args.image_path
+ _tone = args.tone
+ _theme = args.theme
+ _json = args.json
+
+ palette = Monet().generate_from_image(_image_path)
+ props = [_tone, _theme]
+
+ if _json:
+ preset = PresetUtils().new_preset_from_monet(name=_preset_name, monet_palette=palette,
+ props=props, obj_only=True)
+ preset_json = preset.get_preset_json()
+ print(preset_json)
+ exit(0)
+
+ PresetUtils().new_preset_from_monet(_preset_name, palette, props)
+ logging.info("In order for changes to take full effect, you need to log out.")
+
+ # TODO: Add path and xdg-* values parsing
+ def access_file(self, args):
+ _list = args.list
+ _allow = args.allow
+ _disallow = args.disallow
+
+ if _list:
+ try:
+ access_list = list_file_access()
+ except GLib.GError as e:
+ logging.error(f"An error occurred while accessing allowed files list. Exc: {e}")
+ exit(1)
+ else:
+ logging.info("Allowed files:")
+ if access_list:
+ for value in access_list:
+ print(value)
+ exit(0)
+ else:
+ print("0 paths found")
+ exit(0)
+
+ if _allow:
+ try:
+ allow_file_access(_allow)
+ except GLib.GError as e:
+ logging.error(f"An error occurred while setting file access. Exc: {e}")
+ exit(1)
+ else:
+ logging.info(f"Path {_allow} added to access list")
+ exit(0)
+
+ if _disallow:
+ try:
+ disallow_file_access(_disallow)
+ except GLib.GError as e:
+ logging.error(f"An error occurred while setting file access. Exc: {e}")
+ exit(1)
+ else:
+ logging.info(f"Path {_disallow} removed from access list")
+ exit(0)
+
+ def flatpak_theming(self, args):
+ _enable_theming = args.enable_theming
+ _disable_theming = args.disable_theming
+
+ if _enable_theming == "gtk4":
+ create_gtk_user_override(self.settings, "gtk4")
+ logging.info("Flatpak theming for Gtk 4 applications has been enabled.")
+ elif _enable_theming == "gtk3":
+ create_gtk_user_override(self.settings, "gtk3")
+ logging.info("Flatpak theming for Gtk 3 applications has been enabled.")
+ elif _enable_theming == "both":
+ create_gtk_user_override(self.settings, "gtk4")
+ create_gtk_user_override(self.settings, "gtk3")
+ logging.info("Flatpak theming for Gtk 4 and Gtk 3 applications has been enabled.")
+
+ if _disable_theming == "gtk4":
+ remove_gtk_user_override(self.settings, "gtk4")
+ logging.info("Flatpak theming for Gtk 4 applications has been disabled.")
+ elif _disable_theming == "gtk3":
+ remove_gtk_user_override(self.settings, "gtk3")
+ logging.info("Flatpak theming for Gtk 3 applications has been disabled.")
+ elif _disable_theming == "both":
+ remove_gtk_user_override(self.settings, "gtk4")
+ remove_gtk_user_override(self.settings, "gtk3")
+ logging.info("Flatpak theming for Gtk 4 and Gtk 3 applications has been disabled.")
+
+
+if __name__ == "__main__":
+ cli = CLI()
diff --git a/gradience/frontend/cli/meson.build b/gradience/frontend/cli/meson.build
new file mode 100644
index 00000000..84c61f35
--- /dev/null
+++ b/gradience/frontend/cli/meson.build
@@ -0,0 +1,22 @@
+clidir = 'gradience/frontend/cli'
+
+configure_file(
+ input: 'cli.in',
+ output: 'gradience-cli',
+ configuration: conf,
+ install: true,
+ install_dir: get_option('bindir')
+)
+
+configure_file(
+ input: 'cli.in',
+ output: 'gradience-cli',
+ configuration: local_conf,
+ install: true,
+ install_dir: join_paths(meson.project_build_root(), 'gradience', 'frontend')
+)
+
+gradience_sources = [
+ '__init__.py'
+]
+PY_INSTALLDIR.install_sources(gradience_sources, subdir: clidir)
diff --git a/gradience/frontend/main.py b/gradience/frontend/main.py
index f07e3fa2..08efada0 100644
--- a/gradience/frontend/main.py
+++ b/gradience/frontend/main.py
@@ -25,8 +25,11 @@ from pathlib import Path
from material_color_utilities_python import *
from gi.repository import Gtk, Gdk, Gio, Adw, GLib, Xdp, XdpGtk4
+from gradience.backend.globals import presets_dir
from gradience.backend.css_parser import parse_css
-from gradience.backend.models.preset import Preset, presets_dir
+from gradience.backend.models.preset import Preset
+from gradience.backend.theming.preset_utils import PresetUtils
+from gradience.backend.utils.colors import rgba_from_argb
from gradience.backend.utils.common import to_slug_case
from gradience.backend.constants import *
@@ -288,7 +291,7 @@ class GradienceApplication(Adw.Application):
"palette": palette,
"custom_css": {"gtk4": custom_css},
}
- self.preset = Preset(preset=preset)
+ self.preset = Preset().new_from_dict(preset)
self.load_preset_variables_from_preset()
except OSError: # fallback to adwaita
logging.warning("Custom preset not found. Fallback to Adwaita")
@@ -320,13 +323,13 @@ class GradienceApplication(Adw.Application):
def load_preset_from_file(self, preset_path):
logging.debug(f"load preset from file {preset_path}")
- self.preset = Preset(preset_path=preset_path)
+ self.preset = Preset().new_from_path(preset_path)
self.load_preset_variables_from_preset()
def load_preset_from_resource(self, preset_path):
preset_text = Gio.resources_lookup_data(
preset_path, 0).get_data().decode()
- self.preset = Preset(text=preset_text)
+ self.preset = Preset().new_from_resource(text=preset_text)
self.load_preset_variables_from_preset()
def load_preset_variables_from_preset(self, preset=None):
@@ -374,131 +377,27 @@ class GradienceApplication(Adw.Application):
self.reload_variables()
- @staticmethod
- def rgba_from_argb(argb, alpha=None) -> str:
- base = "rgba({}, {}, {}, {})"
-
- red = redFromArgb(argb)
- green = greenFromArgb(argb)
- blue = blueFromArgb(argb)
- if not alpha:
- alpha = alphaFromArgb(argb)
-
- return base.format(red, green, blue, alpha)
-
- def update_theme_from_monet(self, theme, tone, monet_theme):
- palettes = theme["palettes"]
+ def update_theme_from_monet(self, monet, tone, monet_theme):
+ palettes = monet["palettes"]
monet_theme = monet_theme.get_string().lower() # dark / light
palette = {}
- i = 0
- for color in palettes.values():
- i += 1
+
+ for i, color in zip(range(1, 7), palettes.values()):
palette[str(i)] = hexFromArgb(color.tone(int(tone.get_string())))
self.pref_palette_shades["monet"].update_shades(palette)
+
if monet_theme == "auto":
if self.style_manager.get_dark():
monet_theme = "dark"
else:
monet_theme = "light"
- if monet_theme == "dark":
- dark_theme = theme["schemes"]["dark"]
- variable = {
- "accent_color": self.rgba_from_argb(dark_theme.primary),
- "accent_bg_color": self.rgba_from_argb(dark_theme.primaryContainer),
- "accent_fg_color": self.rgba_from_argb(dark_theme.onPrimaryContainer),
- "destructive_color": self.rgba_from_argb(dark_theme.error),
- "destructive_bg_color": self.rgba_from_argb(dark_theme.errorContainer),
- "destructive_fg_color": self.rgba_from_argb(
- dark_theme.onErrorContainer
- ),
- "success_color": self.rgba_from_argb(dark_theme.tertiary),
- "success_bg_color": self.rgba_from_argb(dark_theme.onTertiary),
- "success_fg_color": self.rgba_from_argb(dark_theme.onTertiaryContainer),
- "warning_color": self.rgba_from_argb(dark_theme.secondary),
- "warning_bg_color": self.rgba_from_argb(dark_theme.onSecondary),
- "warning_fg_color": self.rgba_from_argb(dark_theme.primary, "0.8"),
- "error_color": self.rgba_from_argb(dark_theme.error),
- "error_bg_color": self.rgba_from_argb(dark_theme.errorContainer),
- "error_fg_color": self.rgba_from_argb(dark_theme.onError),
- "window_bg_color": self.rgba_from_argb(dark_theme.surface),
- "window_fg_color": self.rgba_from_argb(dark_theme.onSurface),
- "view_bg_color": self.rgba_from_argb(dark_theme.surface),
- "view_fg_color": self.rgba_from_argb(dark_theme.onSurface),
- "headerbar_bg_color": self.rgba_from_argb(dark_theme.surface),
- "headerbar_fg_color": self.rgba_from_argb(dark_theme.onSurface),
- "headerbar_border_color": self.rgba_from_argb(
- dark_theme.primary, "0.8"
- ),
- "headerbar_backdrop_color": "@headerbar_bg_color",
- "headerbar_shade_color": self.rgba_from_argb(dark_theme.shadow),
- "card_bg_color": self.rgba_from_argb(dark_theme.primary, "0.05"),
- "card_fg_color": self.rgba_from_argb(dark_theme.onSecondaryContainer),
- "card_shade_color": self.rgba_from_argb(dark_theme.shadow),
- "dialog_bg_color": self.rgba_from_argb(dark_theme.secondaryContainer),
- "dialog_fg_color": self.rgba_from_argb(dark_theme.onSecondaryContainer),
- "popover_bg_color": self.rgba_from_argb(dark_theme.secondaryContainer),
- "popover_fg_color": self.rgba_from_argb(
- dark_theme.onSecondaryContainer
- ),
- "shade_color": self.rgba_from_argb(dark_theme.shadow),
- "scrollbar_outline_color": self.rgba_from_argb(dark_theme.outline),
- }
- else: # light
- light_theme = theme["schemes"]["light"]
- variable = {
- "accent_color": self.rgba_from_argb(light_theme.primary),
- "accent_bg_color": self.rgba_from_argb(light_theme.primary),
- "accent_fg_color": self.rgba_from_argb(light_theme.onPrimary),
- "destructive_color": self.rgba_from_argb(light_theme.error),
- "destructive_bg_color": self.rgba_from_argb(light_theme.errorContainer),
- "destructive_fg_color": self.rgba_from_argb(
- light_theme.onErrorContainer
- ),
- "success_color": self.rgba_from_argb(light_theme.tertiary),
- "success_bg_color": self.rgba_from_argb(light_theme.tertiaryContainer),
- "success_fg_color": self.rgba_from_argb(
- light_theme.onTertiaryContainer
- ),
- "warning_color": self.rgba_from_argb(light_theme.secondary),
- "warning_bg_color": self.rgba_from_argb(light_theme.secondaryContainer),
- "warning_fg_color": self.rgba_from_argb(
- light_theme.onSecondaryContainer
- ),
- "error_color": self.rgba_from_argb(light_theme.error),
- "error_bg_color": self.rgba_from_argb(light_theme.errorContainer),
- "error_fg_color": self.rgba_from_argb(light_theme.onError),
- "window_bg_color": self.rgba_from_argb(light_theme.secondaryContainer),
- "window_fg_color": self.rgba_from_argb(light_theme.onSurface),
- "view_bg_color": self.rgba_from_argb(light_theme.secondaryContainer),
- "view_fg_color": self.rgba_from_argb(light_theme.onSurface),
- "headerbar_bg_color": self.rgba_from_argb(
- light_theme.secondaryContainer
- ),
- "headerbar_fg_color": self.rgba_from_argb(light_theme.onSurface),
- "headerbar_border_color": self.rgba_from_argb(
- light_theme.primary, "0.8"
- ),
- "headerbar_backdrop_color": "@headerbar_bg_color",
- "headerbar_shade_color": self.rgba_from_argb(
- light_theme.secondaryContainer
- ),
- "card_bg_color": self.rgba_from_argb(light_theme.primary, "0.05"),
- "card_fg_color": self.rgba_from_argb(light_theme.onSecondaryContainer),
- "card_shade_color": self.rgba_from_argb(light_theme.shadow),
- "dialog_bg_color": self.rgba_from_argb(light_theme.secondaryContainer),
- "dialog_fg_color": self.rgba_from_argb(
- light_theme.onSecondaryContainer
- ),
- "popover_bg_color": self.rgba_from_argb(light_theme.secondaryContainer),
- "popover_fg_color": self.rgba_from_argb(
- light_theme.onSecondaryContainer
- ),
- "shade_color": self.rgba_from_argb(light_theme.shadow),
- "scrollbar_outline_color": self.rgba_from_argb(light_theme.outline),
- }
+ preset_object = PresetUtils().new_preset_from_monet(monet_palette=monet,
+ props=[tone, monet_theme], obj_only=True)
+
+ variable = preset_object.variables
for key in variable:
if key in self.pref_variables:
@@ -506,16 +405,6 @@ class GradienceApplication(Adw.Application):
self.reload_variables()
- def generate_gtk_css(self, app_type):
- final_css = ""
- for key in self.variables.keys():
- final_css += f"@define-color {key} {self.variables[key]};\n"
- for prefix_key in self.palette.keys():
- for key in self.palette[prefix_key].keys():
- final_css += f"@define-color {prefix_key + key} {self.palette[prefix_key][key]};\n"
- final_css += self.custom_css.get(app_type, "")
- return final_css
-
def mark_as_dirty(self):
self.is_dirty = True
self.props.active_window.save_preset_button.get_child().set_icon_name(
@@ -540,7 +429,7 @@ class GradienceApplication(Adw.Application):
def reload_variables(self):
parsing_errors = []
- gtk_css = self.generate_gtk_css("gtk4")
+ gtk_css = PresetUtils().generate_gtk_css("gtk4", self.preset)
css_provider = Gtk.CssProvider()
def on_error(_, section, error):
@@ -616,6 +505,7 @@ class GradienceApplication(Adw.Application):
Adw.ResponseAppearance.DESTRUCTIVE,
transient_for=self.props.active_window,
)
+ dialog.gtk3_app_type.set_sensitive(False)
dialog.connect("response", self.restore_color_scheme)
dialog.present()
@@ -782,7 +672,7 @@ class GradienceApplication(Adw.Application):
def save_preset(self, _unused, response, preset_entry):
if response == "save":
- self.preset.save_preset(preset_entry.get_text(), self.plugins_list)
+ self.preset.save_to_file(preset_entry.get_text(), self.plugins_list)
self.clear_dirty()
self.win.toast_overlay.add_toast(
Adw.Toast(title=_("Preset saved")))
@@ -793,60 +683,10 @@ class GradienceApplication(Adw.Application):
def apply_color_scheme(self, widget, response):
if response == "apply":
if widget.get_app_types()["gtk4"]:
- gtk4_dir = os.path.join(
- os.environ.get("XDG_CONFIG_HOME",
- os.environ["HOME"] + "/.config"),
- "gtk-4.0",
- )
- if not os.path.exists(gtk4_dir):
- os.makedirs(gtk4_dir)
- gtk4_css = self.generate_gtk_css("gtk4")
- contents = ""
- try:
- with open(
- os.path.join(gtk4_dir, "gtk.css"), "r", encoding="utf-8"
- ) as file:
- contents = file.read()
- except FileNotFoundError: # first run
- pass
- else:
- with open(
- os.path.join(gtk4_dir, "gtk.css.bak"), "w", encoding="utf-8"
- ) as file:
- file.write(contents)
- finally:
- with open(
- os.path.join(gtk4_dir, "gtk.css"), "w", encoding="utf-8"
- ) as file:
- file.write(gtk4_css)
+ PresetUtils().apply_preset("gtk4", self.preset)
if widget.get_app_types()["gtk3"]:
- gtk3_dir = os.path.join(
- os.environ.get("XDG_CONFIG_HOME",
- os.environ["HOME"] + "/.config"),
- "gtk-3.0",
- )
- if not os.path.exists(gtk3_dir):
- os.makedirs(gtk3_dir)
- gtk3_css = self.generate_gtk_css("gtk3")
- contents = ""
- try:
- with open(
- os.path.join(gtk3_dir, "gtk.css"), "r", encoding="utf-8"
- ) as file:
- contents = file.read()
- except FileNotFoundError: # first run
- pass
- else:
- with open(
- os.path.join(gtk3_dir, "gtk.css.bak"), "w", encoding="utf-8"
- ) as file:
- file.write(contents)
- finally:
- with open(
- os.path.join(gtk3_dir, "gtk.css"), "w", encoding="utf-8"
- ) as file:
- file.write(gtk3_css)
+ PresetUtils().apply_preset("gtk3", self.preset)
self.reload_plugins()
self.plugins_list.apply()
@@ -855,11 +695,12 @@ class GradienceApplication(Adw.Application):
Adw.Toast(title=_("Preset set successfully"))
)
+ # TODO: Make it as a seperate widget
dialog = Adw.MessageDialog(
transient_for=self.props.active_window,
heading=_("Log out"),
body=_(
- "For the changes to take effect, you need to log out. "
+ "For the changes to take full effect, you need to log out."
),
body_use_markup=True,
)
@@ -873,47 +714,14 @@ class GradienceApplication(Adw.Application):
def on_theme_set_dialog_response (self, dialog, response):
if response == "ok":
- print("theme_set_dialog_ok")
+ logging.debug("theme_set_dialog_ok")
def restore_color_scheme(self, widget, response):
if response == "restore":
if widget.get_app_types()["gtk4"]:
- file = Gio.File.new_for_path(
- os.path.join(
- os.environ.get(
- "XDG_CONFIG_HOME", os.environ["HOME"] + "/.config"
- ),
- "gtk-4.0/gtk.css.bak",
- )
- )
try:
- backup = open(
- os.path.join(
- os.environ.get(
- "XDG_CONFIG_HOME", os.environ["HOME"] +
- "/.config"
- ),
- "gtk-4.0/gtk.css.bak",
- ),
- "r",
- encoding="utf-8",
- )
- contents = backup.read()
- backup.close()
- gtk4css = open(
- os.path.join(
- os.environ.get(
- "XDG_CONFIG_HOME", os.environ["HOME"] +
- "/.config"
- ),
- "gtk-4.0/gtk.css",
- ),
- "w",
- encoding="utf-8",
- )
- gtk4css.write(contents)
- gtk4css.close()
- except FileNotFoundError:
+ PresetUtils().restore_gtk4_preset()
+ except Exception:
self.win.toast_overlay.add_toast(
Adw.Toast(title=_("Unable to restore GTK 4 backup"))
)
@@ -922,7 +730,7 @@ class GradienceApplication(Adw.Application):
transient_for=self.props.active_window,
heading=_("Log out"),
body=_(
- "For the changes to take effect, you need to log out. "
+ "For the changes to take full effect, you need to log out."
),
body_use_markup=True,
)
@@ -936,37 +744,21 @@ class GradienceApplication(Adw.Application):
def on_theme_restore_dialog_response (self, dialog, response):
if response == "ok":
- print("theme_restore_dialog_ok")
+ logging.debug("theme_restore_dialog_ok")
def reset_color_scheme(self, widget, response):
if response == "reset":
if widget.get_app_types()["gtk4"]:
- file = Gio.File.new_for_path(
- os.path.join(
- os.environ.get(
- "XDG_CONFIG_HOME", os.environ["HOME"] + "/.config"
- ),
- "gtk-4.0/gtk.css",
- )
- )
try:
- file.delete()
+ PresetUtils().reset_preset("gtk4")
except Exception:
self.win.toast_overlay.add_toast(
Adw.Toast(title=_("Unable to delete current preset"))
)
if widget.get_app_types()["gtk3"]:
- file = Gio.File.new_for_path(
- os.path.join(
- os.environ.get(
- "XDG_CONFIG_HOME", os.environ["HOME"] + "/.config"
- ),
- "gtk-3.0/gtk.css",
- )
- )
try:
- file.delete()
+ PresetUtils().reset_preset("gtk3")
except Exception:
self.win.toast_overlay.add_toast(
Adw.Toast(title=_("Unable to delete current preset"))
@@ -976,7 +768,7 @@ class GradienceApplication(Adw.Application):
transient_for=self.props.active_window,
heading=_("Log out"),
body=_(
- "For the changes to take effect, you need to log out. "
+ "For the changes to take full effect, you need to log out."
),
body_use_markup=True,
)
@@ -990,13 +782,14 @@ class GradienceApplication(Adw.Application):
def on_theme_reset_dialog_response (self, dialog, response):
if response == "ok":
- print("theme_reset_dialog_ok")
+ logging.debug("theme_reset_dialog_ok")
def show_preferences(self, *_args):
prefs = GradiencePreferencesWindow(self.win)
prefs.set_transient_for(self.win)
prefs.present()
+ # TODO: Move it to seperate frontend module
def show_about_window(self, *_args):
about = Adw.AboutWindow(
transient_for=self.props.active_window,
diff --git a/gradience/frontend/meson.build b/gradience/frontend/meson.build
index 89be3ed8..2f2f9092 100644
--- a/gradience/frontend/meson.build
+++ b/gradience/frontend/meson.build
@@ -13,6 +13,7 @@ configure_file(
configuration: local_conf
)
+subdir('cli')
subdir('dialogs')
subdir('utils')
subdir('views')
diff --git a/gradience/frontend/views/main_window.py b/gradience/frontend/views/main_window.py
index f01746fd..20bcc552 100644
--- a/gradience/frontend/views/main_window.py
+++ b/gradience/frontend/views/main_window.py
@@ -23,6 +23,7 @@ from reportlab.graphics import renderPM
from material_color_utilities_python import *
from gi.repository import Gtk, Adw, Gio
+from gradience.backend.theming.monet import Monet
from gradience.backend.constants import rootdir, app_id, build_type
from gradience.frontend.widgets.error_list_row import GradienceErrorListRow
@@ -70,12 +71,12 @@ class GradienceMainWindow(Adw.ApplicationWindow):
self.connect("unrealize", self.save_window_props)
self.style_manager = self.get_application().style_manager
- self.first_apply = True
+ #self.first_apply = True
self.get_default_wallpaper()
- # FIXME: This function works only when building using meson, because Flatpak
- # can't access host's dconf with current config/impl
+ # TODO: Check if org.freedesktop.portal.Settings portal will allow us to \
+ # read org.gnome.desktop.background DConf key
def get_default_wallpaper(self):
background_settings = Gio.Settings("org.gnome.desktop.background")
if self.style_manager.get_dark():
@@ -210,39 +211,23 @@ class GradienceMainWindow(Adw.ApplicationWindow):
def on_apply_button(self, *_args):
if self.monet_image_file:
- if self.monet_image_file.endswith(".svg"):
- drawing = svg2rlg(self.monet_image_file)
- self.monet_image_file = os.path.join(
- os.environ.get("XDG_RUNTIME_DIR"), "gradience_bg.png"
- )
- renderPM.drawToFile(drawing, self.monet_image_file, fmt="PNG")
-
- if self.monet_image_file.endswith(".xml"):
- logging.debug("XML WIP")
-
try:
- self.monet_img = Image.open(self.monet_image_file)
- except Exception:
- self.toast_overlay.add_toast(
- Adw.Toast(title=_("Unsupported background type"))
- )
- else:
- basewidth = 64
- wpercent = basewidth / float(self.monet_img.size[0])
- hsize = int((float(self.monet_img.size[1]) * float(wpercent)))
- self.monet_img = self.monet_img.resize(
- (basewidth, hsize), Image.Resampling.LANCZOS
- )
- self.theme = themeFromImage(self.monet_img)
+ self.theme = Monet().generate_from_image(self.monet_image_file)
+
self.tone = self.tone_row.get_selected_item()
self.monet_theme = self.monet_theme_row.get_selected_item()
self.get_application().update_theme_from_monet(
self.theme, self.tone, self.monet_theme
)
- if not self.first_apply:
- self.toast_overlay.add_toast(
- Adw.Toast(title=_("Palette generated"))
- )
+ except Exception as e:
+ logging.error(f"Failed to generate Monet palette. Exc: {e}")
+ self.toast_overlay.add_toast(
+ Adw.Toast(title=_("Failed to generate Monet palette"))
+ )
+ else:
+ self.toast_overlay.add_toast(
+ Adw.Toast(title=_("Palette generated"))
+ )
else:
self.toast_overlay.add_toast(
Adw.Toast(title=_("Select a background first"))
diff --git a/gradience/frontend/views/preferences_window.py b/gradience/frontend/views/preferences_window.py
index 0c819d6a..b6d59a51 100644
--- a/gradience/frontend/views/preferences_window.py
+++ b/gradience/frontend/views/preferences_window.py
@@ -84,9 +84,9 @@ class GradiencePreferencesWindow(Adw.PreferencesWindow):
state = self.allow_gtk4_flatpak_theming_user.props.state
if not state:
- create_gtk_user_override(self, self.settings, "gtk4")
+ create_gtk_user_override(self.settings, "gtk4", self)
else:
- remove_gtk_user_override(self, self.settings, "gtk4")
+ remove_gtk_user_override(self.settings, "gtk4", self)
logging.debug(
f"user-flatpak-theming-gtk4: {self.settings.get_boolean('user-flatpak-theming-gtk4')}"
@@ -96,9 +96,9 @@ class GradiencePreferencesWindow(Adw.PreferencesWindow):
state = self.allow_gtk3_flatpak_theming_user.props.state
if not state:
- create_gtk_user_override(self, self.settings, "gtk3")
+ create_gtk_user_override(self.settings, "gtk3", self)
else:
- remove_gtk_user_override(self, self.settings, "gtk3")
+ remove_gtk_user_override(self.settings, "gtk3", self)
logging.debug(
f"user-flatpak-theming-gtk3: {self.settings.get_boolean('user-flatpak-theming-gtk3')}"
@@ -108,9 +108,9 @@ class GradiencePreferencesWindow(Adw.PreferencesWindow):
state = self.allow_gtk4_flatpak_theming_global.props.state
if not state:
- create_gtk_global_override(self, self.settings, "gtk4")
+ create_gtk_global_override(self.settings, "gtk4", self)
else:
- remove_gtk_global_override(self, self.settings, "gtk4")
+ remove_gtk_global_override(self.settings, "gtk4", self)
logging.debug(
f"global-flatpak-theming-gtk4: {self.settings.get_boolean('global-flatpak-theming-gtk4')}"
@@ -120,9 +120,9 @@ class GradiencePreferencesWindow(Adw.PreferencesWindow):
state = self.allow_gtk3_flatpak_theming_global.props.state
if not state:
- create_gtk_global_override(self, self.settings, "gtk3")
+ create_gtk_global_override(self.settings, "gtk3", self)
else:
- remove_gtk_global_override(self, self.settings, "gtk3")
+ remove_gtk_global_override(self.settings, "gtk3", self)
logging.debug(
f"global-flatpak-theming-gtk3: {self.settings.get_boolean('global-flatpak-theming-gtk3')}"
diff --git a/gradience/frontend/views/presets_manager_window.py b/gradience/frontend/views/presets_manager_window.py
index eb7720b5..e365fbc0 100644
--- a/gradience/frontend/views/presets_manager_window.py
+++ b/gradience/frontend/views/presets_manager_window.py
@@ -24,8 +24,8 @@ from collections import OrderedDict
from pathlib import Path
from gi.repository import Gtk, Adw, GLib
-from gradience.backend.preset_downloader import fetch_presets
-from gradience.backend.models.preset import presets_dir
+from gradience.backend.preset_downloader import PresetDownloader
+from gradience.backend.globals import presets_dir, preset_repos
from gradience.backend.constants import rootdir
from gradience.frontend.widgets.preset_row import GradiencePresetRow
@@ -65,11 +65,6 @@ class GradiencePresetWindow(Adw.Window):
custom_presets = {}
- official_repositories = {
- "Official": "https://github.com/GradienceTeam/Community/raw/next/official.json",
- "Curated": "https://github.com/GradienceTeam/Community/raw/next/curated.json",
- }
-
search_results_list = []
offline = False
@@ -146,9 +141,19 @@ class GradiencePresetWindow(Adw.Window):
else:
badge = "white"
- explore_presets, urls = fetch_presets(repo)
-
- if explore_presets:
+ try:
+ explore_presets, urls = PresetDownloader().fetch_presets(repo)
+ except GLib.GError as e:
+ if e.code == 1:
+ self.offline = True
+ self.search_spinner.props.visible = False
+ self.search_stack.set_visible_child_name("page_offline")
+ else:
+ self.search_spinner.props.visible = False
+ # TODO: Create a new page to show for other errors eg. "page_error"
+ except json.JSONDecodeError as e:
+ self.search_spinner.props.visible = False
+ else:
self.search_spinner.props.visible = False
for (preset, preset_name), preset_url in zip(
@@ -159,10 +164,6 @@ class GradiencePresetWindow(Adw.Window):
)
self.search_results.append(row)
self.search_results_list.append(row)
- else:
- self.offline = True
- self.search_spinner.props.visible = False
- self.search_stack.set_visible_child_name("page_offline")
def add_repo(self, _unused, response, name_entry, url_entry):
if response == "add":
@@ -315,6 +316,7 @@ class GradiencePresetWindow(Adw.Window):
"pretty-purple": "Pretty Purple",
}
+ # TODO: Move this from frontend to backend
for repo in Path(presets_dir).iterdir():
logging.debug(f"presets_dir.iterdir: {repo}")
if repo.is_dir(): # repo
@@ -426,7 +428,7 @@ class GradiencePresetWindow(Adw.Window):
self.repos_list = Adw.PreferencesGroup()
self.repos_list.set_title(_("Repositories"))
- for repo_name, repo in self.official_repositories.items():
+ for repo_name, repo in preset_repos.items():
row = GradienceRepoRow(repo, repo_name, self, deletable=False)
self.repos_list.add(row)
@@ -436,4 +438,4 @@ class GradiencePresetWindow(Adw.Window):
self.repos.add(self.repos_list)
- self._repos = {**self.user_repositories, **self.official_repositories}
+ self._repos = {**self.user_repositories, **preset_repos}
diff --git a/gradience/frontend/views/welcome_window.py b/gradience/frontend/views/welcome_window.py
index dde8917a..e97fb086 100644
--- a/gradience/frontend/views/welcome_window.py
+++ b/gradience/frontend/views/welcome_window.py
@@ -176,7 +176,7 @@ class GradienceWelcomeWindow(Adw.Window):
self.allow_flatpak_theming_user_toggled()
def allow_flatpak_theming_user_toggled(self, *args):
- create_gtk_user_override(self, self.gio_settings, "gtk4")
+ create_gtk_user_override(self.gio_settings, "gtk4", self)
def install_runner(self, widget):
def set_completed(result, error=False):
diff --git a/gradience/frontend/widgets/explore_preset_row.py b/gradience/frontend/widgets/explore_preset_row.py
index fb9c5554..cba7c405 100644
--- a/gradience/frontend/widgets/explore_preset_row.py
+++ b/gradience/frontend/widgets/explore_preset_row.py
@@ -21,7 +21,7 @@ import os
from gi.repository import Gtk, Adw
from gradience.backend.utils.common import to_slug_case
-from gradience.backend.preset_downloader import download_preset
+from gradience.backend.preset_downloader import PresetDownloader
from gradience.backend.constants import rootdir
from gradience.backend.logger import Logger
@@ -60,7 +60,7 @@ class GradienceExplorePresetRow(Adw.ActionRow):
@Gtk.Template.Callback()
def on_apply_button_clicked(self, *_args):
try:
- download_preset(to_slug_case(self.name), self.prefix, self.url)
+ PresetDownloader().download_preset(to_slug_case(self.name), self.prefix, self.url)
except Exception as e:
self.toast_overlay.add_toast(
Adw.Toast(title=_("Preset could not be downloaded"))
@@ -86,7 +86,7 @@ class GradienceExplorePresetRow(Adw.ActionRow):
@Gtk.Template.Callback()
def on_download_button_clicked(self, *_args):
try:
- download_preset(to_slug_case(self.name), self.prefix, self.url)
+ PresetDownloader().download_preset(to_slug_case(self.name), self.prefix, self.url)
except Exception as e:
self.toast_overlay.add_toast(
Adw.Toast(title=_("Preset could not be downloaded"))
diff --git a/gradience/frontend/widgets/preset_row.py b/gradience/frontend/widgets/preset_row.py
index 4cc831ef..6999055c 100644
--- a/gradience/frontend/widgets/preset_row.py
+++ b/gradience/frontend/widgets/preset_row.py
@@ -22,7 +22,7 @@ from gi.repository import Gtk, Adw, Xdp, XdpGtk4
from gradience.frontend.views.share_window import GradienceShareWindow
from gradience.backend.utils.common import to_slug_case
-from gradience.backend.models.preset import Preset, presets_dir
+from gradience.backend.models.preset import Preset
from gradience.backend.constants import rootdir
from gradience.backend.logger import Logger
@@ -61,7 +61,7 @@ class GradiencePresetRow(Adw.ExpanderRow):
self.win = win
self.toast_overlay = self.win.toast_overlay
- self.preset = Preset(preset_path)
+ self.preset = Preset().new_from_path(preset_path)
if self.preset.badges:
self.has_badges = True
@@ -124,7 +124,7 @@ class GradiencePresetRow(Adw.ExpanderRow):
if self.name_entry_toggle.get_active():
self.value_stack.set_visible_child(self.name_entry)
else:
- self.preset.rename_preset(self.name_entry.get_text())
+ self.preset.rename(self.name_entry.get_text())
self.value_stack.set_visible_child(self.apply_button)
def on_report_btn_clicked(self, *_args):
diff --git a/local_cli.sh b/local_cli.sh
new file mode 100755
index 00000000..78803dc3
--- /dev/null
+++ b/local_cli.sh
@@ -0,0 +1,21 @@
+#!/usr/bin/bash
+
+# local_cli.sh
+#
+# Change the look of Adwaita, with ease
+# Copyright (C) 2022 Gradience Team
+#
+# 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
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program 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 this program. If not, see .
+
+python builddir/gradience/frontend/gradience-cli "$@"