diff --git a/docs/widgets/(Widget)-Image-Gif.md b/docs/widgets/(Widget)-Image-Gif.md
new file mode 100644
index 00000000..5baa475f
--- /dev/null
+++ b/docs/widgets/(Widget)-Image-Gif.md
@@ -0,0 +1,61 @@
+# Image/Gif Widget Configuration
+
+| Option | Type | Default | Description |
+|-------------------------|---------|----------------------------------------------|-----------------------------------------------------------------------------|
+| `label` | string | `` | The primary label format. |
+| `label_alt` | string | `{file_path}` | The alternative label format.
+| `update_interval` | integer | `5000` | The interval in milliseconds to update the widget. |
+| `file_path` | string | `` | Path to file. Can be : png, jpg, gif, webp |
+| `speed`| integer | `100` | Playback speed (percentage) |
+| `height`| integer | `24` | Height of the image/gif inside the bar. Can act differently if **KeepAspectRatio** is **True** or **False** |
+| `width` | integer | `24` | Width of the image/gif inside the bar. Can act differently if **KeepAspectRatio** is **True** or **False** |
+| `keep_aspect_ratio` | boolean | `True` | Keep aspect ratio of current image/gif
+| `callbacks` | dict | `{on_left: 'toggle_label', on_middle: 'pause_gif', on_right: 'do_nothing'}` | Callback functions for different mouse button actions. |
+| `animation` | dict | `{'enabled': True, 'type': 'fadeInOut', 'duration': 200}` | Animation settings for the widget. |
+| `container_shadow` | dict | `None` | Container shadow options. |
+| `label_shadow` | dict | `None` | Label shadow options. |
+
+## Example Configuration
+```yaml
+gif:
+ type: "yasb.image.ImageWidget"
+ options:
+ label: ""
+ label_alt: "{file_path}"
+ file_path: "C:\\Users\\stant\\Desktop\\your_file.gif"
+ update_interval: 5000
+ callbacks:
+ on_left: "toggle_label"
+ on_middle: "pause_gif"
+ on_right: "do_nothing"
+ speed: 100
+ height: 24
+ width: 24
+ keep_aspect_ratio: True
+```
+
+## Description of Options
+
+- **label**: The primary label format for the widget. You can use placeholders like `{file_path}`, `{speed}` or `{file_name}` here.
+- **label_alt**: The alternative label format for the widget.
+- **update_interval**: The interval in milliseconds to update the widget.
+- **file_path**: A string that contain the path to the file. It can be : **png, jpg, gif, webp**
+- **speed**: Playback speed. Only useful if current file is a gif or a webp. 100 = normal speed, 50 = half speed, 200 = x2 speed
+- **height**: Height of the image/gif inside the bar. Can act differently if **KeepAspectRatio** is **True** or **False**
+- **width**: Width of the image/gif inside the bar. Can act differently if **KeepAspectRatio** is **True** or **False**
+- **keep_aspect_ratio**: A boolean indicating whether to keep current file aspect ratio when displayed.
+- **callbacks**: A dictionary specifying the callbacks for mouse events. It contains:
+ - **on_left**: The name of the callback function for left mouse button click.
+ - **on_middle**: The name of the callback function for middle mouse button click.
+ - **on_right**: The name of the callback function for right mouse button click.
+- **animation**: A dictionary specifying the animation settings for the widget. It contains three keys: `enabled`, `type`, and `duration`. The `type` can be `fadeInOut` and the `duration` is the animation duration in milliseconds.
+- **container_shadow**: Container shadow options.
+- **label_shadow**: Label shadow options.
+
+## Example Style
+```css
+.image-gif-widget {}
+.image-gif-widget .widget-container {}
+.image-gif-widget .widget-container .label {}
+.image-gif-widget .widget-container .label.alt {}
+```
\ No newline at end of file
diff --git a/src/config.yaml b/src/config.yaml
index 0a563d5a..d75b30cd 100644
--- a/src/config.yaml
+++ b/src/config.yaml
@@ -39,29 +39,29 @@ bars:
right: 4
widgets:
left:
- - "home"
- - "komorebi_workspaces"
- - "komorebi_active_layout"
- - "active_window"
+ - "home"
+ - "komorebi_workspaces"
+ - "komorebi_active_layout"
+ - "active_window"
center:
- - "clock"
+ - "clock"
right:
- - "media"
- - "weather"
- - "microphone"
- - "volume"
- - "notifications"
- - "power_menu"
+ - "media"
+ - "weather"
+ - "microphone"
+ - "volume"
+ - "notifications"
+ - "power_menu"
widgets:
home:
type: "yasb.home.HomeWidget"
options:
label: "\udb81\udf17"
menu_list:
- - { title: "User Home", path: "~" }
- - { title: "Download", path: "~\\Downloads" }
- - { title: "Documents", path: "~\\Documents" }
- - { title: "Pictures", path: "~\\Pictures" }
+ - { title: "User Home", path: "~" }
+ - { title: "Download", path: "~\\Downloads" }
+ - { title: "Documents", path: "~\\Documents" }
+ - { title: "Pictures", path: "~\\Pictures" }
system_menu: true
power_menu: true
blur: false
@@ -131,7 +131,7 @@ widgets:
label: "{%a, %d %b %H:%M}"
label_alt: "{%A, %d %B %Y %H:%M}"
timezones: []
- calendar:
+ calendar:
blur: false
round_corners: false
alignment: "center"
diff --git a/src/core/validation/widgets/yasb/image_gif.py b/src/core/validation/widgets/yasb/image_gif.py
new file mode 100644
index 00000000..2905ab84
--- /dev/null
+++ b/src/core/validation/widgets/yasb/image_gif.py
@@ -0,0 +1,108 @@
+DEFAULTS = {
+ "label": "",
+ "label_alt": "{file_path}",
+ "file_path": "",
+ "width": 24,
+ "height": 24,
+ "speed": 100,
+ "keep_aspect_ratio": True,
+ "update_interval": 5000,
+ "animation": {"enabled": True, "type": "fadeInOut", "duration": 200},
+ "container_padding": {"top": 0, "left": 0, "bottom": 0, "right": 0},
+ "callbacks": {"on_left": "toggle_label", "on_middle": "pause_gif", "on_right": "do_nothing"},
+}
+
+VALIDATION_SCHEMA = {
+ "label": {
+ "type": "string",
+ "default": DEFAULTS["label"],
+ },
+ "label_alt": {
+ "type": "string",
+ "default": DEFAULTS["label_alt"],
+ },
+ "file_path": {
+ "type": "string",
+ "default": DEFAULTS["file_path"],
+ },
+ "width": {
+ "type": "integer",
+ "default": DEFAULTS["width"],
+ },
+ "height": {
+ "type": "integer",
+ "default": DEFAULTS["height"],
+ },
+ "speed": {
+ "type": "integer",
+ "default": DEFAULTS["speed"],
+ },
+ "keep_aspect_ratio": {
+ "type": "boolean",
+ "default": DEFAULTS["keep_aspect_ratio"],
+ },
+ "update_interval": {
+ "type": "integer",
+ "default": DEFAULTS["update_interval"],
+ },
+ "callbacks": {
+ "type": "dict",
+ "schema": {
+ "on_left": {
+ "type": "string",
+ "nullable": True,
+ "default": DEFAULTS["callbacks"]["on_left"],
+ },
+ "on_middle": {
+ "type": "string",
+ "nullable": True,
+ "default": DEFAULTS["callbacks"]["on_middle"],
+ },
+ "on_right": {"type": "string", "nullable": True, "default": DEFAULTS["callbacks"]["on_right"]},
+ },
+ "default": DEFAULTS["callbacks"],
+ },
+ "container_padding": {
+ "type": "dict",
+ "required": False,
+ "schema": {
+ "top": {"type": "integer", "default": DEFAULTS["container_padding"]["top"]},
+ "left": {"type": "integer", "default": DEFAULTS["container_padding"]["left"]},
+ "bottom": {"type": "integer", "default": DEFAULTS["container_padding"]["bottom"]},
+ "right": {"type": "integer", "default": DEFAULTS["container_padding"]["right"]},
+ },
+ "default": DEFAULTS["container_padding"],
+ },
+ "animation": {
+ "type": "dict",
+ "required": False,
+ "schema": {
+ "enabled": {"type": "boolean", "default": DEFAULTS["animation"]["enabled"]},
+ "type": {"type": "string", "default": DEFAULTS["animation"]["type"]},
+ "duration": {"type": "integer", "default": DEFAULTS["animation"]["duration"], "min": 0},
+ },
+ "default": DEFAULTS["animation"],
+ },
+ "label_shadow": {
+ "type": "dict",
+ "required": False,
+ "schema": {
+ "enabled": {"type": "boolean", "default": False},
+ "color": {"type": "string", "default": "black"},
+ "offset": {"type": "list", "default": [1, 1]},
+ "radius": {"type": "integer", "default": 3},
+ },
+ "default": {"enabled": False, "color": "black", "offset": [1, 1], "radius": 3},
+ },
+ "container_shadow": {
+ "type": "dict",
+ "required": False,
+ "schema": {
+ "enabled": {"type": "boolean", "default": False},
+ "color": {"type": "string", "default": "black"},
+ "offset": {"type": "list", "default": [1, 1]},
+ "radius": {"type": "integer", "default": 3},
+ },
+ "default": {"enabled": False, "color": "black", "offset": [1, 1], "radius": 3},
+ },
+}
diff --git a/src/core/widgets/yasb/image_gif.py b/src/core/widgets/yasb/image_gif.py
new file mode 100644
index 00000000..c27eda0f
--- /dev/null
+++ b/src/core/widgets/yasb/image_gif.py
@@ -0,0 +1,183 @@
+import os
+import re
+
+from PyQt6.QtCore import QSize, Qt, QTimer
+from PyQt6.QtGui import QImageReader, QMovie
+from PyQt6.QtWidgets import QFrame, QHBoxLayout, QLabel
+
+from core.utils.utilities import add_shadow, build_widget_label
+from core.utils.widgets.animation_manager import AnimationManager
+from core.validation.widgets.yasb.image_gif import VALIDATION_SCHEMA
+from core.widgets.base import BaseWidget
+
+
+class ImageGifWidget(BaseWidget):
+ validation_schema = VALIDATION_SCHEMA
+
+ _instances: list["ImageGifWidget"] = []
+ _shared_timer: QTimer | None = None
+
+ def __init__(
+ self,
+ label: str,
+ label_alt: str,
+ file_path: str,
+ width: int,
+ height: int,
+ speed: int,
+ keep_aspect_ratio: bool,
+ animation: dict[str, str],
+ update_interval: int = 0,
+ callbacks: dict = None,
+ container_padding: dict = None,
+ label_shadow: dict = None,
+ container_shadow: dict = None,
+ **kwargs,
+ ):
+ super().__init__(class_name="image-gif-widget", **kwargs)
+ self._show_alt_label = False
+ self._label_content = label
+ self._label_alt_content = label_alt
+ self._update_interval = update_interval
+ self._padding = container_padding
+ self._label_shadow = label_shadow
+ self._container_shadow = container_shadow
+ self._speed = speed
+ self._keep_aspect_ratio = keep_aspect_ratio
+ self._animation = animation
+ self._file_path = file_path
+ self._width = width
+ self._height = height
+
+ self._movie_label = QLabel()
+ self._movie_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
+ self._movie = QMovie()
+
+ # Construct container
+ self._widget_container_layout: QHBoxLayout = QHBoxLayout()
+ self._widget_container_layout.setSpacing(0)
+ self._widget_container_layout.setContentsMargins(
+ self._padding["left"], self._padding["top"], self._padding["right"], self._padding["bottom"]
+ )
+ # Initialize container
+ self._widget_container = QFrame()
+ self._widget_container.setLayout(self._widget_container_layout)
+ self._widget_container.setProperty("class", "widget-container")
+ add_shadow(self._widget_container, self._container_shadow)
+
+ # Add the container to the main widget layout
+ self._widget_container_layout.addWidget(self._movie_label)
+ self.widget_layout.addWidget(self._widget_container)
+
+ build_widget_label(self, self._label_content, self._label_alt_content, None)
+
+ self._setup()
+
+ # Callbacks
+ self.callback_left = callbacks.get("on_left", "do_nothing")
+ self.callback_right = callbacks.get("on_right", "do_nothing")
+ self.callback_middle = callbacks.get("on_middle", "do_nothing")
+
+ self.register_callback("toggle_label", self._toggle_label)
+ self.register_callback("pause_gif", self._pause_gif)
+
+ if self not in ImageGifWidget._instances:
+ ImageGifWidget._instances.append(self)
+
+ if update_interval > 0 and ImageGifWidget._shared_timer is None:
+ ImageGifWidget._shared_timer = QTimer(self)
+ ImageGifWidget._shared_timer.setInterval(update_interval)
+ ImageGifWidget._shared_timer.timeout.connect(self._update_label)
+ ImageGifWidget._shared_timer.start()
+
+ self._update_label()
+
+ def _setup(self):
+ """Image/Gif setup"""
+ file_path = self._file_path
+
+ if not file_path or not os.path.exists(file_path):
+ self._show_error_placeholder()
+ return
+
+ try:
+ if self._movie:
+ self._movie.stop()
+ self._movie.deleteLater()
+
+ self._movie = QMovie(file_path)
+
+ if self._width or self._height:
+ self._movie.setScaledSize(self._get_scaled_size())
+
+ self._movie.setSpeed(self._speed)
+
+ self._movie_label.setMovie(self._movie)
+ self._movie.start()
+
+ except Exception as e:
+ print(f"Error when loading file : {file_path}: {e}")
+ self._show_error_placeholder()
+
+ def _show_error_placeholder(self):
+ """Display error when file could not be loaded."""
+ self._movie_label.setText("Error loading file")
+
+ def _toggle_label(self):
+ AnimationManager.animate(self, self._animation["type"], self._animation["duration"])
+ self._show_alt_label = not self._show_alt_label
+ for widget in self._widgets:
+ widget.setVisible(not self._show_alt_label)
+ for widget in self._widgets_alt:
+ widget.setVisible(self._show_alt_label)
+ self._update_label()
+
+ def _pause_gif(self):
+ if self._movie:
+ if self._movie.state() == QMovie.MovieState.Paused:
+ self._movie.setPaused(False)
+ else:
+ self._movie.setPaused(True)
+
+ def _get_scaled_size(self):
+ """Get correct size for displaying image/gif."""
+ if not self._movie:
+ return QSize(self._width, self._height)
+
+ reader = QImageReader(self._file_path)
+ size = reader.size()
+
+ target_width = self._width
+ target_height = self._height
+
+ if self._keep_aspect_ratio:
+ return size.scaled(target_width, target_height, Qt.AspectRatioMode.KeepAspectRatio)
+ else:
+ return size.scaled(target_width, target_height, Qt.AspectRatioMode.IgnoreAspectRatio)
+
+ def _update_label(self):
+ """Update label using current playback speed and file path."""
+ active_widgets = self._widgets_alt if self._show_alt_label else self._widgets
+ active_label_content = self._label_alt_content if self._show_alt_label else self._label_content
+ label_parts = re.split("(.*?)", active_label_content)
+ label_parts = [part for part in label_parts if part]
+ widget_index = 0
+
+ label_options = {
+ "{speed}": self._speed,
+ "{file_path}": self._file_path,
+ "{file_name}": os.path.basename(self._file_path),
+ }
+
+ for part in label_parts:
+ part = part.strip()
+ for fmt_str, value in label_options.items():
+ part = part.replace(fmt_str, str(value))
+
+ if part and widget_index < len(active_widgets) and isinstance(active_widgets[widget_index], QLabel):
+ if "" in part:
+ icon = re.sub(r"|", "", part).strip()
+ active_widgets[widget_index].setText(icon)
+ else:
+ active_widgets[widget_index].setText(part)
+ widget_index += 1