diff --git a/ks_includes/KlippyRest.py b/ks_includes/KlippyRest.py
index 822a9886..6dfa31e2 100644
--- a/ks_includes/KlippyRest.py
+++ b/ks_includes/KlippyRest.py
@@ -35,18 +35,19 @@ class KlippyRest:
def get_thumbnail_stream(self, thumbnail):
return self.send_request(f"server/files/gcodes/{thumbnail}", json=False)
- def send_request(self, method, json=True):
+ def _do_request(self, method, request_method, data=None, json=None, json_response=True):
url = f"{self.endpoint}/{method}"
headers = {} if self.api_key is False else {"x-api-key": self.api_key}
- data = False
+ response_data = False
try:
- response = requests.get(url, headers=headers, timeout=3)
+ callee = getattr(requests, request_method)
+ response = callee(url, json=json, data=data, headers=headers, timeout=3)
response.raise_for_status()
- if json:
+ if json_response:
logging.debug(f"Sending request to {url}")
- data = response.json()
+ response_data = response.json()
else:
- data = response.content
+ response_data = response.content
except requests.exceptions.HTTPError as h:
self.status = self.format_status(h)
except requests.exceptions.ConnectionError as c:
@@ -59,11 +60,17 @@ class KlippyRest:
self.status = self.format_status(r)
except Exception as e:
self.status = self.format_status(e)
- if data:
+ if response_data:
self.status = ''
else:
logging.error(self.status.replace('\n', '>>'))
- return data
+ return response_data
+
+ def post_request(self, method, data=None, json=None, json_response=True):
+ return self._do_request(method, "post", data, json, json_response)
+
+ def send_request(self, method, json=True):
+ return self._do_request(method, "get", json_response=json)
@staticmethod
def format_status(status):
diff --git a/ks_includes/config.py b/ks_includes/config.py
index 21878ce0..83fe95d2 100644
--- a/ks_includes/config.py
+++ b/ks_includes/config.py
@@ -173,7 +173,7 @@ class KlipperScreenConfig:
strs = (
'moonraker_api_key', 'moonraker_host', 'titlebar_name_type',
'screw_positions', 'power_devices', 'titlebar_items', 'z_babystep_values',
- 'extrude_distances', "extrude_speeds",
+ 'extrude_distances', "extrude_speeds", "camera_url", "spoolman",
)
numbers = (
'moonraker_port', 'move_speed_xy', 'move_speed_z',
@@ -188,7 +188,9 @@ class KlipperScreenConfig:
# This section may be deprecated in favor of moving this options under the printer section
numbers = ('rotation', '')
strs = ('screw_positions', '')
- elif section.startswith('graph') or section.startswith('displayed_macros'):
+ elif section.startswith('graph')\
+ or section.startswith('displayed_macros')\
+ or section.startswith('spoolman'):
bools = [f'{option}' for option in config[section]]
elif section.startswith('z_calibrate_position'):
# This section may be deprecated in favor of moving this options under the printer section
@@ -487,6 +489,7 @@ class KlipperScreenConfig:
extra_sections = [i for i in self.config.sections() if i.startswith("displayed_macros")]
extra_sections.extend([i for i in self.config.sections() if i.startswith("graph")])
+ extra_sections.extend([i for i in self.config.sections() if i.startswith("spoolman")])
for section in extra_sections:
for item in self.config.options(section):
value = self.config[section].getboolean(item, fallback=True)
diff --git a/ks_includes/defaults.conf b/ks_includes/defaults.conf
index 64210b83..fd998299 100644
--- a/ks_includes/defaults.conf
+++ b/ks_includes/defaults.conf
@@ -281,3 +281,15 @@ enable: {{ moonraker_connected }}
name: KlipperScreen
icon: settings
panel: settings
+
+[menu __main more spoolman]
+name: Spoolman
+icon: spoolman
+panel: spoolman
+enable: {{ printer.spoolman }}
+
+[menu __print spoolman]
+name: Spoolman
+icon: spoolman
+panel: spoolman
+enable: {{ printer.spoolman }}
diff --git a/ks_includes/printer.py b/ks_includes/printer.py
index a803e712..17398181 100644
--- a/ks_includes/printer.py
+++ b/ks_includes/printer.py
@@ -26,6 +26,7 @@ class Printer:
self.tempstore_size = 1200
self.cameras = []
self.available_commands = {}
+ self.spoolman = False
def reinit(self, printer_info, data):
self.config = data['configfile']['config']
@@ -37,6 +38,7 @@ class Printer:
self.fancount = 0
self.output_pin_count = 0
self.tempstore = {}
+ self.spoolman = False
self.busy = False
if not self.store_timeout:
self.store_timeout = GLib.timeout_add_seconds(1, self._update_temp_store)
@@ -236,6 +238,7 @@ class Printer:
"pause_resume": {"is_paused": self.state == "paused"},
"power_devices": {"count": len(self.get_power_devices())},
"cameras": {"count": len(self.cameras)},
+ "spoolman": self.spoolman
}
}
@@ -364,3 +367,7 @@ class Printer:
temp = 0
self.tempstore[device][x].append(temp)
return True
+
+ def enable_spoolman(self):
+ logging.info("Enabling Spoolman")
+ self.spoolman = True
diff --git a/panels/extrude.py b/panels/extrude.py
index ba668737..67283c3d 100644
--- a/panels/extrude.py
+++ b/panels/extrude.py
@@ -39,6 +39,7 @@ class Panel(ScreenPanel):
'unload': self._gtk.Button("arrow-up", _("Unload"), "color2"),
'retract': self._gtk.Button("retract", _("Retract"), "color1"),
'temperature': self._gtk.Button("heat-up", _("Temperature"), "color4"),
+ 'spoolman': self._gtk.Button("spoolman", "Spoolman", "color3"),
}
self.buttons['extrude'].connect("clicked", self.extrude, "+")
self.buttons['load'].connect("clicked", self.load_unload, "+")
@@ -48,7 +49,10 @@ class Panel(ScreenPanel):
"name": "Temperature",
"panel": "temperature"
})
-
+ self.buttons['spoolman'].connect("clicked", self.menu_item_clicked, {
+ "name": "Spoolman",
+ "panel": "spoolman"
+ })
extgrid = self._gtk.HomogeneousGrid()
limit = 5
i = 0
@@ -66,6 +70,8 @@ class Panel(ScreenPanel):
i += 1
if i < (limit - 1):
extgrid.attach(self.buttons['temperature'], i + 1, 0, 1, 1)
+ if i < (limit - 2) and self._printer.spoolman:
+ extgrid.attach(self.buttons['spoolman'], i + 2, 0, 1, 1)
distgrid = Gtk.Grid()
for j, i in enumerate(self.distances):
@@ -163,7 +169,7 @@ class Panel(ScreenPanel):
def enable_buttons(self, enable):
for button in self.buttons:
- if button == "temperature":
+ if button in ("temperature", "spoolman"):
continue
self.buttons[button].set_sensitive(enable)
diff --git a/panels/spoolman.py b/panels/spoolman.py
new file mode 100644
index 00000000..6bb60eb8
--- /dev/null
+++ b/panels/spoolman.py
@@ -0,0 +1,416 @@
+import os.path
+import pathlib
+import logging
+import gi
+
+gi.require_version("Gtk", "3.0")
+from gi.repository import Gtk, GdkPixbuf, GObject, Pango, Gdk
+from ks_includes.screen_panel import ScreenPanel
+from ks_includes.KlippyRest import KlippyRest
+from datetime import datetime
+
+try:
+ from zoneinfo import ZoneInfo
+except ImportError:
+ from backports.zoneinfo import ZoneInfo
+
+
+def format_date(date):
+ try:
+ return datetime.strptime(date, '%Y-%m-%dT%H:%M:%S.%f').replace(tzinfo=ZoneInfo('UTC'))
+ except ValueError:
+ try:
+ return datetime.strptime(date, '%Y-%m-%dT%H:%M:%S').replace(tzinfo=ZoneInfo('UTC'))
+ except ValueError:
+ return None
+
+
+class SpoolmanVendor:
+ id: int
+ name: str
+ registered: datetime = None
+
+ def __init__(self, **entries):
+ self.__dict__.update(entries)
+ for date in ["registered"]:
+ if date in entries:
+ self.__setattr__(date, format_date(entries[date]))
+
+
+class SpoolmanFilament:
+ article_number: str
+ color_hex: str
+ comment: str
+ density: float
+ diameter: float
+ id: int
+ material: str
+ name: str
+ price: float
+ registered: datetime = None
+ settings_bed_temp: int
+ settings_extruder_temp: int
+ spool_weight: float
+ vendor: SpoolmanVendor = None
+ weight: float
+
+ def __init__(self, **entries):
+ self.__dict__.update(entries)
+ if "vendor" in entries:
+ self.vendor = SpoolmanVendor(**(entries["vendor"]))
+ for date in ["registered"]:
+ if date in entries:
+ self.__setattr__(date, format_date(entries[date]))
+
+
+class SpoolmanSpool(GObject.GObject):
+ archived: bool
+ id: int
+ remaining_length: float
+ remaining_weight: float
+ used_length: float
+ used_weight: float
+ lot_nr: str
+ filament: SpoolmanFilament = None
+ first_used: datetime = None
+ last_used: datetime = None
+ registered: datetime = None
+ _icon: Gtk.Image = None
+ theme_path: str = None
+ _spool_icon: str = None
+
+ def __init__(self, **entries):
+ GObject.GObject.__init__(self)
+ self.__dict__.update(entries)
+ if "filament" in entries:
+ self.filament = SpoolmanFilament(**(entries["filament"]))
+ for date in ["first_used", "last_used", "registered"]:
+ if date in entries:
+ self.__setattr__(date, format_date(entries[date]))
+
+ @property
+ def name(self):
+ result = self.filament.name
+ if self.filament.vendor:
+ result = " ".join([self.filament.vendor.name, "-", result])
+ return result
+
+ @property
+ def icon(self):
+ if self._icon is None:
+ if SpoolmanSpool._spool_icon is None:
+ klipperscreendir = pathlib.Path(__file__).parent.resolve().parent
+ _spool_icon_path = os.path.join(
+ klipperscreendir, "styles", SpoolmanSpool.theme_path, "images", "spool.svg"
+ )
+ if not os.path.isfile(_spool_icon_path):
+ _spool_icon_path = os.path.join(klipperscreendir, "styles", "spool.svg")
+ SpoolmanSpool._spool_icon = pathlib.Path(_spool_icon_path).read_text()
+
+ loader = GdkPixbuf.PixbufLoader()
+ loader.write(
+ SpoolmanSpool._spool_icon.replace('var(--filament-color)', f'#{self.filament.color_hex}').encode()
+ )
+ loader.close()
+ self._icon = loader.get_pixbuf()
+ return self._icon
+
+
+class Panel(ScreenPanel):
+ apiClient: KlippyRest
+ _active_spool_id: int = None
+
+ @staticmethod
+ def spool_compare_id(model, row1, row2, user_data):
+ spool1 = model.get_value(row1, 0)
+ spool2 = model.get_value(row2, 0)
+ return spool1.id - spool2.id
+
+ @staticmethod
+ def spool_compare_date(model, row1, row2, user_data):
+ spool1 = model.get_value(row1, 0)
+ spool2 = model.get_value(row2, 0)
+ return 1 if (spool1.last_used or datetime.min).replace(tzinfo=None) > \
+ (spool2.last_used or datetime.min).replace(tzinfo=None) else -1
+
+ def _on_material_filter_changed(self, sender):
+ treeiter = sender.get_active_iter()
+ if treeiter is not None:
+ model = sender.get_model()
+ self._filters["material"] = model[treeiter][0]
+ self._filterable.refilter()
+
+ def __init__(self, screen, title):
+ super().__init__(screen, title)
+ self.apiClient = screen.apiclient
+ if self._config.get_main_config().getboolean("24htime", True):
+ self.timeFormat = '%Y-%m-%d %H:%M'
+ else:
+ self.timeFormat = '%Y-%m-%d %I:%M %p'
+
+ SpoolmanSpool.theme_path = screen.theme
+ GObject.type_register(SpoolmanSpool)
+ self._filters = {}
+ self._model = Gtk.TreeStore(SpoolmanSpool.__gtype__)
+ self._materials = Gtk.ListStore(str, str)
+
+ self._filterable = self._model.filter_new()
+ self._filterable.set_visible_func(self._filter_spools)
+
+ sortable = Gtk.TreeModelSort(self._filterable)
+ sortable.set_sort_func(0, self.spool_compare_id)
+ sortable.set_sort_func(1, self.spool_compare_date)
+
+ self.scroll = self._gtk.ScrolledWindow()
+
+ sbox = Gtk.Box(spacing=0)
+ sbox.set_vexpand(False)
+
+ clear_active_spool = self._gtk.Button("cancel", _("Clear"), "color2", self.bts, Gtk.PositionType.LEFT, 1)
+ clear_active_spool.get_style_context().add_class("buttons_slim")
+ clear_active_spool.connect('clicked', self.clear_active_spool)
+
+ refresh = self._gtk.Button("refresh", style="color1", scale=.66)
+ refresh.get_style_context().add_class("buttons_slim")
+ refresh.connect('clicked', self.load_spools)
+
+ sort_btn_id = self._gtk.Button(None, _("ID"), "color4", self.bts, Gtk.PositionType.RIGHT, 1)
+ sort_btn_id.connect("clicked", self.change_sort, "id")
+ sort_btn_id.get_style_context().add_class("buttons_slim")
+
+ sort_btn_used = self._gtk.Button(None, _("Last Used"), "color3", self.bts, Gtk.PositionType.RIGHT, 1)
+ sort_btn_used.connect("clicked", self.change_sort, "last_used")
+ sort_btn_used.get_style_context().add_class("buttons_slim")
+
+ switch = Gtk.Switch()
+ switch.set_hexpand(False)
+ switch.set_vexpand(False)
+ switch.set_active(self._config.get_config().getboolean("spoolman", "hide_archived", fallback=True))
+ switch.connect("notify::active", self.switch_config_option, "spoolman", "hide_archived", self.load_spools)
+
+ name = Gtk.Label()
+ name.set_markup(_("Archived"))
+ name.set_halign(Gtk.Align.START)
+ name.set_valign(Gtk.Align.CENTER)
+ name.set_line_wrap(True)
+ name.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR)
+
+ archived = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
+ archived.set_valign(Gtk.Align.CENTER)
+ archived.add(name)
+ archived.add(switch)
+
+ sbox.pack_start(sort_btn_id, True, True, 0)
+ sbox.pack_start(sort_btn_used, True, True, 0)
+ sbox.pack_start(clear_active_spool, True, True, 0)
+ sbox.pack_start(refresh, True, True, 0)
+ sbox.pack_start(archived, False, False, 5)
+ sbox.set_hexpand(True)
+ sbox.set_vexpand(False)
+
+ filter_box = Gtk.ListBox()
+ filter_box.set_selection_mode(Gtk.SelectionMode.NONE)
+ _filter = Gtk.Expander(label=_("Filter"))
+ _filter.add(filter_box)
+
+ row = Gtk.ListBoxRow()
+ hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
+ row.add(hbox)
+
+ label = Gtk.Label(_("Material"))
+ _material_filter = Gtk.ComboBox()
+ _material_filter.set_model(self._materials)
+ _material_filter.connect("changed", self._on_material_filter_changed)
+ cellrenderertext = Gtk.CellRendererText()
+ _material_filter.pack_start(cellrenderertext, True)
+ _material_filter.add_attribute(cellrenderertext, "text", 1)
+
+ hbox.pack_start(label, False, True, 0)
+ hbox.pack_start(_material_filter, True, True, 0)
+
+ filter_box.add(row)
+
+ self.main = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
+ self.main.set_vexpand(True)
+ self.main.pack_start(sbox, False, False, 0)
+ self.main.pack_start(_filter, False, True, 0)
+ self.main.pack_start(self.scroll, True, True, 0)
+
+ self.load_spools()
+ self.get_active_spool()
+ self._treeview = Gtk.TreeView(model=sortable)
+ self._treeview.set_headers_visible(False)
+ self._treeview.set_show_expanders(False)
+
+ text_renderer = Gtk.CellRendererText()
+ pixbuf_renderer = Gtk.CellRendererPixbuf()
+ pixbuf_renderer.set_padding(5, 5)
+ checkbox_renderer = Gtk.CellRendererToggle()
+ column_id = Gtk.TreeViewColumn(cell_renderer=text_renderer)
+ column_id.set_cell_data_func(
+ text_renderer,
+ lambda column, cell, model, it, data:
+ self._set_cell_background(cell, model.get_value(it, 0)) and
+ cell.set_property('text', f'{model.get_value(it, 0).id}')
+ )
+ column_id.set_sort_column_id(0)
+
+ column_icon = Gtk.TreeViewColumn(cell_renderer=pixbuf_renderer)
+ column_icon.set_cell_data_func(
+ pixbuf_renderer,
+ lambda column, cell, model, it, data:
+ self._set_cell_background(cell, model.get_value(it, 0)) and
+ cell.set_property('pixbuf', model.get_value(it, 0).icon)
+ )
+
+ column_spool = Gtk.TreeViewColumn(cell_renderer=text_renderer)
+ column_spool.set_expand(True)
+ column_spool.set_cell_data_func(
+ text_renderer,
+ lambda column, cell, model, it, data:
+ self._set_cell_background(cell, model.get_value(it, 0)) and
+ cell.set_property('markup', self._get_filament_formated(model.get_value(it, 0)))
+ )
+
+ column_last_used = Gtk.TreeViewColumn(cell_renderer=text_renderer)
+ column_last_used.set_visible(False)
+ column_last_used.set_sort_column_id(1)
+
+ column_material = Gtk.TreeViewColumn(cell_renderer=text_renderer)
+ column_material.set_cell_data_func(
+ text_renderer,
+ lambda column, cell, model, it, data:
+ self._set_cell_background(cell, model.get_value(it, 0)) and
+ cell.set_property('text', model.get_value(it, 0).filament.material)
+ )
+
+ checkbox_renderer.connect("toggled", self._set_active_spool)
+ column_toggle_active_spool = Gtk.TreeViewColumn(cell_renderer=checkbox_renderer)
+ column_toggle_active_spool.set_cell_data_func(
+ checkbox_renderer,
+ lambda column, cell, model, it, data:
+ self._set_cell_background(cell, model.get_value(it, 0)) and
+ cell.set_property('active', model.get_value(it, 0).id == self._active_spool_id)
+ )
+
+ self._treeview.append_column(column_id)
+ self._treeview.append_column(column_icon)
+ self._treeview.append_column(column_spool)
+ self._treeview.append_column(column_last_used)
+ self._treeview.append_column(column_material)
+ self._treeview.append_column(column_toggle_active_spool)
+
+ self.current_sort_widget = sort_btn_id
+ sort_btn_used.clicked()
+
+ self.scroll.add(self._treeview)
+ self.content.add(self.main)
+
+ def _filter_spools(self, model, i, data):
+ spool: SpoolmanSpool = model[i][0]
+ matches = True
+ if ("material" in self._filters) and (self._filters["material"] is not None):
+ matches &= spool.filament.material == self._filters["material"]
+ return matches
+
+ def _set_cell_background(self, cell, spool: SpoolmanSpool):
+ cell.set_property('cell-background-rgba', Gdk.RGBA(1, 1, 1, .1) if spool.id == self._active_spool_id else None)
+ return True
+
+ def _get_filament_formated(self, spool: SpoolmanSpool):
+ if spool.id == self._active_spool_id:
+ result = f'{spool.name}\n'
+ else:
+ result = f'{spool.name}\n'
+ if spool.last_used:
+ result += f'{_("Last used")}: {spool.last_used.astimezone():{self.timeFormat}}\n'
+ if hasattr(spool, "remaining_weight"):
+ result += f'{_("Remaining weight")}: {round(spool.remaining_weight, 2)} g\n'
+ if hasattr(spool, "remaining_length"):
+ result += f'{_("Remaining length")}: {round(spool.remaining_length / 1000, 2)} m\n'
+
+ return result.strip()
+
+ def _set_active_spool(self, sender, path):
+ model = self._treeview.get_model()
+ it = model.get_iter(path)
+ spool = model.get_value(it, 0)
+ if spool.id == self._active_spool_id:
+ self.clear_active_spool()
+ else:
+ self.set_active_spool(spool)
+
+ def change_sort(self, widget, sort_type):
+ self.current_sort_widget.set_image(None)
+ self.current_sort_widget = widget
+ if sort_type == "id":
+ logging.info("Sorting by ID")
+ column = 0
+ elif sort_type == "last_used":
+ logging.info("Sorting by Last Used")
+ column = 1
+ else:
+ logging.error("Unknown sort type")
+ return
+ if self._treeview.get_column(column).get_sort_order() == Gtk.SortType.DESCENDING:
+ new_sort_order = Gtk.SortType.ASCENDING
+ else:
+ new_sort_order = Gtk.SortType.DESCENDING
+ self._treeview.get_column(column).set_sort_order(new_sort_order)
+ self._treeview.get_model().set_sort_column_id(column, new_sort_order)
+ icon = "arrow-down" if new_sort_order == Gtk.SortType.DESCENDING else "arrow-up"
+ widget.set_image(self._gtk.Image(icon, self._gtk.img_scale * self.bts))
+
+ def process_update(self, action, data):
+ if action == "notify_active_spool_set":
+ self._active_spool_id = data['spool_id']
+ self._treeview.get_model().foreach(lambda store, treepath, treeiter:
+ store.row_changed(treepath, treeiter)
+ )
+ self._treeview.queue_draw()
+
+ def load_spools(self, data=None):
+ hide_archived = self._config.get_config().getboolean("spoolman", "hide_archived", fallback=True)
+ self._model.clear()
+ self._materials.clear()
+ spools = self.apiClient.post_request("server/spoolman/proxy", json={
+ "request_method": "GET",
+ "path": f"/v1/spool?allow_archived={not hide_archived}",
+ })
+ if not spools or "result" not in spools:
+ self._screen.show_error_modal("Exception when trying to fetch spools")
+ return
+
+ materials = []
+ for spool in spools["result"]:
+ spoolObject = SpoolmanSpool(**spool)
+ self._model.append(None, [spoolObject])
+ if spoolObject.filament.material not in materials:
+ materials.append(spoolObject.filament.material)
+
+ materials.sort()
+ self._materials.append([None, _("All")])
+ for material in materials:
+ self._materials.append([material, material])
+
+ def clear_active_spool(self, sender: Gtk.Button = None):
+ result = self.apiClient.post_request("server/spoolman/spool_id", json={})
+ if not result:
+ self._screen.show_error_modal("Exception when setting active spool")
+ return
+
+ def set_active_spool(self, spool: SpoolmanSpool):
+ result = self.apiClient.post_request("server/spoolman/spool_id", json={
+ "spool_id": spool.id
+ })
+ if not result:
+ self._screen.show_error_modal("Exception when setting active spool")
+ return
+
+ def get_active_spool(self) -> SpoolmanSpool:
+ result = self.apiClient.send_request("server/spoolman/spool_id")
+ if not result:
+ self._screen.show_error_modal("Exception when getting active spool")
+ return
+ self._active_spool_id = result["result"]["spool_id"]
+ return self._active_spool_id
diff --git a/screen.py b/screen.py
index bdb39646..cbec0e6f 100755
--- a/screen.py
+++ b/screen.py
@@ -894,6 +894,8 @@ class KlipperScreen(Gtk.Window):
self.files.initialize()
self.files.refresh_files()
+ self.init_spoolman()
+
logging.info("Printer initialized")
self.initialized = True
self.reinit_count = 0
@@ -919,6 +921,18 @@ class KlipperScreen(Gtk.Window):
logging.info(f"Temperature store size: {self.printer.tempstore_size}")
except KeyError:
logging.error("Couldn't get the temperature store size")
+ return False
+
+ def init_spoolman(self):
+ server_config = self.apiclient.send_request("server/config")
+ if server_config:
+ try:
+ server_config["result"]["config"]["spoolman"]
+ self.printer.enable_spoolman()
+ except KeyError:
+ logging.warning("Not using Spoolman")
+
+ return False
def show_keyboard(self, entry=None, event=None):
if self.keyboard is not None:
diff --git a/scripts/KlipperScreen-requirements.txt b/scripts/KlipperScreen-requirements.txt
index 3acf4009..4985848e 100644
--- a/scripts/KlipperScreen-requirements.txt
+++ b/scripts/KlipperScreen-requirements.txt
@@ -7,4 +7,5 @@ PyGObject==3.44.1
python-mpv==0.5.2;python_version<"3.10"
python-mpv==1.0.4;python_version>="3.10"
six==1.16.0
-dbus-python==1.3.2
\ No newline at end of file
+dbus-python==1.3.2
+backports.zoneinfo;python_version<"3.9"
diff --git a/styles/base.css b/styles/base.css
index 552152a0..fe35c096 100644
--- a/styles/base.css
+++ b/styles/base.css
@@ -11,11 +11,41 @@ button:disabled {
opacity: .2;
}
+list row,
+treeview.view,
window {
background-color: #13181C;
-gtk-icon-shadow: none;
}
+switch slider {
+ border: 0;
+}
+
+switch, treeview.view check {
+ margin: 0.75em;
+ min-width: 6em;
+ min-height: 3em;
+ border: 0;
+ border-radius: 3em;
+}
+
+treeview.view check {
+ background-color: rgb(32,41,47);
+ -gtk-icon-source: -gtk-icontheme("switch-off-symbolic");
+ background-repeat: no-repeat;
+ background-position: left center;
+ background-size : 3em 3em;
+ background-image: image(url("./styles/circle.svg"));
+ -gtk-icon-transform: translateX(1.5em);
+}
+treeview.view check:checked {
+ background-color : rgb(53,132,228);
+ background-position: right center;
+ -gtk-icon-source: -gtk-icontheme("switch-on-symbolic");
+ -gtk-icon-transform: translateX(-1.5em);
+}
+
button {
background-image: none;
background-color: #13181C;
@@ -110,18 +140,6 @@ combobox arrow {
min-width: 1em;
}
-switch {
- margin: 0.75em;
- min-width: 6em;
- min-height: 3em;
- border: 0;
- border-radius: 3em;
-}
-
-switch slider {
- border: 0;
-}
-
entry {
font-size: 1em;
background-color: #20292F;
diff --git a/styles/circle.svg b/styles/circle.svg
new file mode 100644
index 00000000..14c0d218
--- /dev/null
+++ b/styles/circle.svg
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/styles/colorized/images/spool.svg b/styles/colorized/images/spool.svg
new file mode 100644
index 00000000..32a94d22
--- /dev/null
+++ b/styles/colorized/images/spool.svg
@@ -0,0 +1,90 @@
+
+
diff --git a/styles/colorized/images/spoolman.svg b/styles/colorized/images/spoolman.svg
new file mode 100644
index 00000000..45aa41cb
--- /dev/null
+++ b/styles/colorized/images/spoolman.svg
@@ -0,0 +1,90 @@
+
+
diff --git a/styles/colorized/style.css b/styles/colorized/style.css
index 744ef1a7..156f5c8a 100644
--- a/styles/colorized/style.css
+++ b/styles/colorized/style.css
@@ -5,6 +5,8 @@
color: #fdf6e3; /*base3*/
}
+list row,
+treeview.view,
window {
background-color: #002b36; /*base03*/
}
@@ -78,10 +80,12 @@ combobox box button {
border-color: #002b36; /*base03*/
}
+treeview.view check,
switch {
background-color: #073642; /* base02 */
}
+treeview.view check:checked,
switch:checked {
background-color: #859900; /*solarized-green*/
}
diff --git a/styles/material-dark/images/spool.svg b/styles/material-dark/images/spool.svg
new file mode 100644
index 00000000..3b54bf9f
--- /dev/null
+++ b/styles/material-dark/images/spool.svg
@@ -0,0 +1,86 @@
+
+
diff --git a/styles/material-dark/images/spoolman.svg b/styles/material-dark/images/spoolman.svg
new file mode 100644
index 00000000..2825a532
--- /dev/null
+++ b/styles/material-dark/images/spoolman.svg
@@ -0,0 +1,69 @@
+
+
diff --git a/styles/material-dark/style.css b/styles/material-dark/style.css
index b37f900b..3b750564 100644
--- a/styles/material-dark/style.css
+++ b/styles/material-dark/style.css
@@ -4,6 +4,8 @@
color: #e2e2e2;
}
+list row,
+treeview.view,
window {
background-color: #121212;
}
@@ -75,10 +77,12 @@ combobox box button {
border-color: #121212;
}
+treeview.view check,
switch {
background-color: #1f1f1f;
}
+treeview.view check:checked,
switch:checked {
background-color: #849900;
}
diff --git a/styles/material-darker/images/spool.svg b/styles/material-darker/images/spool.svg
new file mode 100644
index 00000000..3b54bf9f
--- /dev/null
+++ b/styles/material-darker/images/spool.svg
@@ -0,0 +1,86 @@
+
+
diff --git a/styles/material-darker/images/spoolman.svg b/styles/material-darker/images/spoolman.svg
new file mode 100644
index 00000000..2825a532
--- /dev/null
+++ b/styles/material-darker/images/spoolman.svg
@@ -0,0 +1,69 @@
+
+
diff --git a/styles/material-darker/style.css b/styles/material-darker/style.css
index d230469d..c5d94195 100644
--- a/styles/material-darker/style.css
+++ b/styles/material-darker/style.css
@@ -5,6 +5,8 @@
font-family: RobotoMedium;
}
+list row,
+treeview.view,
window {
background-color: #121212;
}
@@ -59,10 +61,12 @@ combobox box button {
border-color: #121212;
}
+treeview.view check,
switch {
background-color: #1c1c1c;
}
+treeview.view check:checked,
switch:checked {
background-color: #2f5631;
}
diff --git a/styles/material-light/images/spool.svg b/styles/material-light/images/spool.svg
new file mode 100644
index 00000000..cdc8f4f4
--- /dev/null
+++ b/styles/material-light/images/spool.svg
@@ -0,0 +1,86 @@
+
+
diff --git a/styles/material-light/images/spoolman.svg b/styles/material-light/images/spoolman.svg
new file mode 100644
index 00000000..5f420ce0
--- /dev/null
+++ b/styles/material-light/images/spoolman.svg
@@ -0,0 +1,68 @@
+
+
diff --git a/styles/material-light/style.css b/styles/material-light/style.css
index 2a6c5824..1e33d2f2 100644
--- a/styles/material-light/style.css
+++ b/styles/material-light/style.css
@@ -4,6 +4,8 @@
color: black;
}
+list row,
+treeview.view,
window {
background-color: #FAFAFA;
}
@@ -88,10 +90,12 @@ combobox box button {
border-color: #BDBDBD;
}
+treeview.view check,
switch {
background-color: #BDBDBD;
}
+treeview.view check:checked,
switch:checked {
background-color: #C5E1A5;
}
diff --git a/styles/spool.svg b/styles/spool.svg
new file mode 100644
index 00000000..66eb18cb
--- /dev/null
+++ b/styles/spool.svg
@@ -0,0 +1,19 @@
+
\ No newline at end of file
diff --git a/styles/z-bolt/images/spool.svg b/styles/z-bolt/images/spool.svg
new file mode 100644
index 00000000..c42db8fe
--- /dev/null
+++ b/styles/z-bolt/images/spool.svg
@@ -0,0 +1,86 @@
+
+
diff --git a/styles/z-bolt/images/spoolman.svg b/styles/z-bolt/images/spoolman.svg
new file mode 100644
index 00000000..0f7246df
--- /dev/null
+++ b/styles/z-bolt/images/spoolman.svg
@@ -0,0 +1,68 @@
+
+
diff --git a/styles/z-bolt/style.css b/styles/z-bolt/style.css
index d3de542d..dcf1ab44 100644
--- a/styles/z-bolt/style.css
+++ b/styles/z-bolt/style.css
@@ -2,6 +2,8 @@
color: white;
}
+list row,
+treeview.view,
window {
background-color: #13181C;
}
@@ -53,10 +55,11 @@ combobox box button {
border-color: #cccccc;
}
+treeview.view check,
switch {
background-color: #20292F;
}
-
+treeview.view check:checked,
switch:checked {
background-color: #3584e4;
}