diff --git a/config/assets/icons/applets/bluetooth-clear.svg b/config/assets/icons/applets/bluetooth-clear.svg old mode 100755 new mode 100644 index 69e7b25..bf893b0 --- a/config/assets/icons/applets/bluetooth-clear.svg +++ b/config/assets/icons/applets/bluetooth-clear.svg @@ -1,3 +1,6 @@ - - + + + + + diff --git a/config/assets/icons/applets/bluetooth-off-clear.svg b/config/assets/icons/applets/bluetooth-off-clear.svg old mode 100755 new mode 100644 index ecc1ab0..e77c35b --- a/config/assets/icons/applets/bluetooth-off-clear.svg +++ b/config/assets/icons/applets/bluetooth-off-clear.svg @@ -1,30 +1,7 @@ - - - - - - image/svg+xml - - - - - - - + + + + diff --git a/config/assets/icons/applets/bluetooth-paired.svg b/config/assets/icons/applets/bluetooth-paired.svg new file mode 100644 index 0000000..60fe35c --- /dev/null +++ b/config/assets/icons/applets/bluetooth-paired.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/config/assets/icons/applets/wifi-clear.svg b/config/assets/icons/applets/wifi-clear.svg old mode 100755 new mode 100644 index 55e3494..6bd8a6a --- a/config/assets/icons/applets/wifi-clear.svg +++ b/config/assets/icons/applets/wifi-clear.svg @@ -1,6 +1,8 @@ - - - - - + + + + + + + diff --git a/config/assets/icons/applets/wifi-off-clear.svg b/config/assets/icons/applets/wifi-off-clear.svg old mode 100755 new mode 100644 diff --git a/config/assets/icons/misc/search.svg b/config/assets/icons/misc/search.svg index 9b396bb..73da906 100644 --- a/config/assets/icons/misc/search.svg +++ b/config/assets/icons/misc/search.svg @@ -1,5 +1 @@ - - - - \ No newline at end of file + \ No newline at end of file diff --git a/config/assets/icons/notification.png b/config/assets/icons/notification.png new file mode 100644 index 0000000..41d66d6 Binary files /dev/null and b/config/assets/icons/notification.png differ diff --git a/config/assets/launcher.json b/config/assets/launcher.json index 6af954b..9d411e0 100644 --- a/config/assets/launcher.json +++ b/config/assets/launcher.json @@ -1,85 +1,98 @@ { - "launcher_config": { - "em": { - "icon": "emoji", - "description": "Emoji - Search and copy emojis" - }, - "clip": { - "icon": "clipboard", - "description": "Clipboard - Search and manage clipboard history" - }, - "=": { - "examples": ["= 10*5", "= 2^8", "= sin(30)"], - "icon": "calculator", - "description": "Quick Math - Fast mathematical expressions" - }, - "app": { - "icon": "apps", - "description": "Applications - Launch installed applications" - }, - "bin": { - "icon": "terminal", - "description": "Bins - Search and run executable binaries" - }, - "power": { - "icon": "shutdown", - "description": "Power - System power management and session control" - }, - "caffeine": { - "examples": ["caffeine 30m", "caffeine 1h", "caffeine 2h", "caffeine on"], - "icon": "coffee", - "description": "Caffeine - Prevent system from going idle" - }, - "sc": { - "icon": "toolbox", - "description": "Screencapture - Record screen and audio" - }, - "wall": { - "icon": "wallpapers", - "description": "Wallpapers - Set wallpapers, and tons of features" - }, - "?": { - "icon": "world", - "description": "Quick Web Search - Fast web search with question mark", - "examples": ["? cats", "? google cats", "? youtube music"] - }, - "remind": { - "icon": "timer_on", - "description": "Reminders - Set time-based reminders with notifications" - }, - "otp": { - "icon": "auth", - "description": "Manage TOTP codes and 2FA authentication" - }, - "pass": { - "icon": "key", - "description": "Password Manager - Search and manage passwords" - }, - "bm": { - "icon": "bookmark", - "description": "Bookmarks - Search and manage bookmarks" - }, - "script": { - "icon": "terminal", - "description": "Bash Scripts - Manage and execute bash scripts" - }, - "bash": { - "icon": "terminal", - "description": "Bash Scripts - Manage and execute bash scripts" - }, - "sh": { - "icon": "terminal", - "description": "Bash Scripts - Manage and execute bash scripts" - }, - "tmux": { - "icon": "terminal", - "description": "Tmux Manager - Create, attach, rename, and kill tmux sessions" - } - }, - "settings": { - "max_examples_shown": 2, - "default_icon": "apps", - "fallback_example_template": "{trigger} ", - "config_version": "1.0" - } + "launcher_config": { + "em": { + "icon": "emoji-people-symbolic", + "description": "Emoji - Search and copy emojis" + }, + "clip": { + "icon": "app.getclipboard.Clipboard", + "description": "Clipboard - Search and manage clipboard history" + }, + "=": { + "examples": [ + "= 10*5", + "= 2^8", + "= sin(30)" + ], + "icon": "calculator", + "description": "Quick Math - Fast mathematical expressions" + }, + "app": { + "icon": "apps", + "description": "Applications - Launch installed applications" + }, + "bin": { + "icon": "terminal", + "description": "Bins - Search and run executable binaries" + }, + "power": { + "icon": "shutdown", + "description": "Power - System power management and session control" + }, + "caffeine": { + "examples": [ + "caffeine 30m", + "caffeine 1h", + "caffeine 2h", + "caffeine on" + ], + "icon": "caffeine", + "description": "Caffeine - Prevent system from going idle" + }, + "sc": { + "icon": "cs-screen", + "description": "Screencapture - Record screen and audio" + }, + "wall": { + "icon": "daily-wallpaper", + "description": "Wallpapers - Set wallpapers, and tons of features" + }, + "?": { + "icon": "search-symbolic", + "description": "Quick Web Search - Fast web search with question mark", + "examples": [ + "? cats", + "? google cats", + "? youtube music" + ] + }, + "remind": { + "icon": "alarm-timer", + "description": "Reminders - Set time-based reminders with notifications" + }, + "otp": { + "icon": "auth-otp-symbolic", + "description": "Manage TOTP codes and 2FA authentication" + }, + "pass": { + "icon": "nextcloud-password-client", + "description": "Password Manager - Search and manage passwords" + }, + "bm": { + "icon": "bookmark-add-symbolic", + "description": "Bookmarks - Search and manage bookmarks" + }, + "script": { + "icon": "terminal", + "description": "Bash Scripts - Manage and execute bash scripts" + }, + "bash": { + "icon": "terminal", + "description": "Bash Scripts - Manage and execute bash scripts" + }, + "sh": { + "icon": "terminal", + "description": "Bash Scripts - Manage and execute bash scripts" + }, + "tmux": { + "icon": "terminal", + "description": "Tmux Manager - Create, attach, rename, and kill tmux sessions" + } + }, + "settings": { + "max_examples_shown": 2, + "default_icon": "apps", + "fallback_example_template": "{trigger} ", + "config_version": "1.0" + } } diff --git a/main.css b/main.css index 58cff50..3cdb2ed 100644 --- a/main.css +++ b/main.css @@ -33,7 +33,7 @@ tooltip { border-radius: 16px; border-color: var(--surface); background-color: var(--shadow); - animation: tooltipShow 0.25s cubic-bezier(0.5, 0.25, 0, 1); + /* animation: tooltipShow 0.25s cubic-bezier(0.5, 0.25, 0, 1); */ } @keyframes tooltipShow { @@ -71,3 +71,96 @@ menu > menuitem:hover { menu > menuitem:hover > label { color: var(--shadow); } + + + + + + + + + + + +#weather-widget label{ + font-family: "SF Pro Rounded"; + /* font-size: 20px; */ + font-weight: 600; + margin-left: 10px; +} + +#city { + margin-top: 10px; + font-size: 20px; + +} +#temperature { + font-size: 50px; +} + +#condition-emoji{ + margin-top: 10px; + font-size: 16px; +} + +#condition { + font-size: 15px; +} +#feels-like { + + font-size: 15px; +} +/* # */ + + + + + + + + + + + + + + + + + +#date-widget label { + font-family: "SF Pro Rounded"; + margin-top: 10px; +} +#day label, +#month label{ + color: var(--foreground); + padding: 5px; + margin-top: 10px; + font-size: 30px; + color: alpha(#fff, 0.7); + font-weight: 600; +} + +#date label{ + color: alpha(#fff, 0.7); + margin-top: -10px; + font-size: 90px; + font-weight: 600; +} +#day label{ + color: alpha(#FF0000, 0.7); +} +#box-widget{ + background-color: alpha(#232323, 1); + margin: 20px 20px 20px 20px; + min-width: 170px; + min-height: 170px; + border-radius: 16px; +} +#box-widget-2{ + background-color: alpha(#425D8A, 1); + min-width: 170px; + min-height: 170px; + border-radius: 16px; +} diff --git a/main.py b/main.py index 7625575..c659ff5 100644 --- a/main.py +++ b/main.py @@ -1,3 +1,4 @@ +from fabric.core import widgets import setproctitle from loguru import logger @@ -7,6 +8,7 @@ from fabric.utils import get_relative_path, monitor_file from modules.dock import Dock from modules.launcher.main import Launcher from modules.notification.notification import ModusNoti +from modules.widget import Deskwidgets from modules.osd import OSD # from modules.corners import Corners @@ -41,6 +43,7 @@ if __name__ == "__main__": panel.launcher = launcher osd = OSD() + widgets = Deskwidgets() # Set corners visibility based on config # corners_visible = config.get("corners_visible", True) # corners.set_visible(corners_visible) @@ -50,7 +53,9 @@ if __name__ == "__main__": _ = css_file.connect("changed", lambda *_: set_css()) # Make sure corners is added to the app - app = Application(f"{APP_NAME}", panel, dock, switcher, launcher, modusnoti, osd) + app = Application( + f"{APP_NAME}", panel, dock, switcher, launcher, modusnoti, osd, widgets + ) def set_css(): app.set_stylesheet_from_file( diff --git a/modules/controlcenter/player.py b/modules/controlcenter/player.py index 9bea977..9c65c42 100644 --- a/modules/controlcenter/player.py +++ b/modules/controlcenter/player.py @@ -88,7 +88,6 @@ class Player(Box): f"background-image:url('{data.HOME_DIR}/.current.wall')" ) - self.children = [ self.album_cover, Box( @@ -201,9 +200,6 @@ class Player(Box): return self.duration = dur - - - def on_seek(self, sender, ratio): pos = ratio * self.duration # duration in seconds self._player.set_position(int(pos)) @@ -269,7 +265,7 @@ class Player(Box): self._is_updating = True update_func, args, kwargs = self._update_queue.pop(0) - + try: update_func(*args, **kwargs) except Exception as e: @@ -284,13 +280,14 @@ class Player(Box): """Finish current update and process next item in queue.""" self._is_updating = False self._update_timeout_id = None - + if self._update_queue: self._process_queue() return False def _handle_thumbnail(self, metadata, player): """Queue thumbnail update to prevent overlapping operations.""" + def _update_thumbnail(): keys = metadata.keys() player_name = player.props.player_name @@ -305,16 +302,24 @@ class Player(Box): logger.debug(f"Found MPRIS art for {player_name}: {art_url}") # Check for browser players and YouTube URLs - elif player_name in ["firefox", "chromium", "vivaldi", "brave"] and "xesam:url" in keys: + elif ( + player_name in ["firefox", "chromium", "vivaldi", "brave"] + and "xesam:url" in keys + ): webpage_url = metadata["xesam:url"] if webpage_url and "youtube.com" in webpage_url: # Simple YouTube thumbnail extraction import re + match = re.search(r"(?:v=|/)([a-zA-Z0-9_-]{11})", webpage_url) if match: video_id = match.group(1) - new_thumbnail = f"https://img.youtube.com/vi/{video_id}/hqdefault.jpg" - logger.debug(f"Found YouTube thumbnail for {player_name}: {new_thumbnail}") + new_thumbnail = ( + f"https://img.youtube.com/vi/{video_id}/hqdefault.jpg" + ) + logger.debug( + f"Found YouTube thumbnail for {player_name}: {new_thumbnail}" + ) # Only update if thumbnail has changed if new_thumbnail and new_thumbnail != self._current_thumbnail: @@ -324,20 +329,20 @@ class Player(Box): elif not new_thumbnail and self._current_thumbnail: # Reset to default if no thumbnail found self._current_thumbnail = None - self.album_cover.set_style(f"background-image:url('{data.HOME_DIR}/.current.wall')") + self.album_cover.set_style( + f"background-image:url('{data.HOME_DIR}/.current.wall')" + ) logger.debug(f"Reset to default wallpaper for {player_name}") self._queue_update(_update_thumbnail) - - def handle_next(self, player): def _do_next(): try: self._player._player.next() except Exception as e: logger.warning(f"Failed to go to next track: {e}") - + self._queue_update(_do_next) def handle_prev(self, player): @@ -346,11 +351,9 @@ class Player(Box): self._player._player.previous() except Exception as e: logger.warning(f"Failed to go to previous track: {e}") - + self._queue_update(_do_prev) - - def handle_play_pause(self, player): def _do_play_pause(): is_playing = ( @@ -431,9 +434,7 @@ class Placeholder(Box): children=[ CenterBox( name="details", - center_children=Label( - label="Nothing Playing", style="color:black;" - ), + center_children=Label(label="Nothing Playing"), ) ], ), diff --git a/modules/launcher/plugins/applications.py b/modules/launcher/plugins/applications.py index 50503fe..d9463b5 100644 --- a/modules/launcher/plugins/applications.py +++ b/modules/launcher/plugins/applications.py @@ -1,6 +1,7 @@ import json import re from typing import List +import subprocess from fabric.utils import DesktopApp from fabric.utils.helpers import get_desktop_applications, get_relative_path @@ -155,8 +156,21 @@ class ApplicationsPlugin(PluginBase): pattern = ".*".join(re.escape(char) for char in query) return bool(re.search(pattern, text, re.IGNORECASE)) + import subprocess + def _launch_application(self, app: DesktopApp): - app.launch() + print("Original:", app.command_line) + + # Remove ALL % codes (e.g., %u, %U, %f, %F, %i, %c, etc.) + cleaned_command = re.sub(r"%\w+", "", app.command_line).strip() + + print("Cleaned:", cleaned_command) + + # Final command with hyprctl dispatch + final_command = f"hyprctl dispatch exec 'uwsm app -- {cleaned_command}'" + subprocess.Popen(final_command, shell=True) + + # app.launch() def _get_all_applications(self) -> List[Result]: """Get a list of all available applications.""" diff --git a/modules/notification/notification.py b/modules/notification/notification.py index 2377158..6fea633 100644 --- a/modules/notification/notification.py +++ b/modules/notification/notification.py @@ -1,5 +1,6 @@ import os +from fabric.utils import get_relative_path from gi.repository import Gdk, GdkPixbuf, GLib, Gtk # type: ignore from loguru import logger @@ -103,7 +104,7 @@ class NotificationWidget(Box): if "file://" in notification.app_icon else Image( name="notification-icon", - icon_name="dialog-information-symbolic" or notification.app_icon, + icon_name="notifications" or notification.app_icon, icon_size=24, ) ) @@ -175,31 +176,24 @@ class NotificationWidget(Box): ellipsization="end", ) if notification.body - else Box() + else Label( + markup="", + h_align="start", + ellipsization="end", + ) ), ], ), Box(h_expand=True), - Box( - orientation="v", - children=[ - self.create_close_button() if self.show_close_button else Box(), - Box(v_expand=True), - ], + Button( + name="notification-close-button", + image=CustomImage(icon_name="close-symbolic", icon_size=18), + visible=True, # Initially hidden + on_clicked=lambda *_: self.notification.close("dismissed-by-user"), ), ], ) - def create_close_button(self): - if self.close_button is None: - self.close_button = Button( - name="notification-close-button", - visible=False, # Initially hidden - on_clicked=lambda *_: self.notification.close("dismissed-by-user"), - child=Label(label="×", name="close-button-label"), - ) - return self.close_button - def _on_enter_notify(self, widget, event): self._is_hovered = True if self.close_button: @@ -215,12 +209,12 @@ class NotificationWidget(Box): return False def get_pixbuf(self, icon_path, width, height): - if icon_path.startswith("file://"): + if icon_path.startswith("file://") and os.path.exists(icon_path): icon_path = icon_path[7:] if not os.path.exists(icon_path): logger.warning(f"Icon path does not exist: {icon_path}") - return None + icon_path = get_relative_path("../../config/assets/icons/notification.png") try: pixbuf = GdkPixbuf.Pixbuf.new_from_file(icon_path) @@ -297,18 +291,20 @@ class NotificationRevealer(SlideRevealer): self._is_closing = False # Enhanced swipe detection variables for Android-style animation - self._drag_start_x = 0 self._drag_start_y = 0 + self._drag_start_x = 0 self._is_dragging = False self._swipe_threshold = 80 # Distance to trigger auto-dismiss - self._swipe_velocity_threshold = 150 # Velocity to trigger dismiss even on shorter swipes + self._swipe_velocity_threshold = ( + 150 # Velocity to trigger dismiss even on shorter swipes + ) self._swipe_in_progress = False self._current_offset = 0 self._last_drag_time = 0 self._drag_velocity = 0 self._spring_back_duration = 200 # Duration for spring-back animation self._dismiss_threshold = 0.3 # Dismiss if swiped 30% of width - + # Animation state self._animation_in_progress = False self._spring_timer_id = None @@ -324,15 +320,10 @@ class NotificationRevealer(SlideRevealer): child=self.notif_box, ) - self.event_box.connect("button-press-event", self._on_button_press) - self.event_box.connect("button-release-event", self._on_button_release) - self.event_box.connect("motion-notify-event", self._on_motion) - self.event_box.connect("realize", self._on_realize) - super().__init__( child=self.event_box, direction="right", - duration=350, # Optimized duration for macOS-like feel + duration=600, # Optimized duration for macOS-like feel ) smooth_revealer_animation(self) @@ -340,90 +331,7 @@ class NotificationRevealer(SlideRevealer): # Connect our own handler that manages the slide animation self.notification.connect("closed", self.on_resolved) - def _on_realize(self, widget): - """Setup CSS provider when widget is realized""" - try: - self._css_provider = Gtk.CssProvider() - context = widget.get_style_context() - context.add_provider(self._css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) - except Exception as e: - logger.warning(f"Could not setup CSS provider for notification animation: {e}") - - def _apply_transform(self, offset_x, opacity=1.0, scale=1.0): - """Apply transform to the notification widget for smooth animation""" - if not self.event_box or not self.event_box.get_realized(): - return - - # Clamp values - offset_x = max(0, offset_x) # Only allow rightward movement - opacity = max(0.1, min(1.0, opacity)) # Keep some visibility - scale = max(0.8, min(1.0, scale)) # Subtle scale effect - - try: - if self._css_provider: - # Apply CSS transform using the provider - css_data = f""" - * {{ - margin-left: {int(offset_x)}px; - opacity: {opacity}; - transform: scale({scale}); - }} - """ - self._css_provider.load_from_data(css_data.encode('utf-8')) - else: - # Fallback: use widget margins for offset - current_margin = self.event_box.get_margin_start() - if current_margin != int(offset_x): - self.event_box.set_margin_start(int(offset_x)) - - # Set opacity directly on the widget - self.event_box.set_opacity(opacity) - - except Exception as e: - # Final fallback: just use margin - try: - self.event_box.set_margin_start(int(offset_x)) - self.event_box.set_opacity(opacity) - except: - pass - - def _animate_spring_back(self, start_offset, target_offset=0, duration=None): - """Animate the notification springing back to its original position""" - if duration is None: - duration = self._spring_back_duration - - if self._spring_timer_id: - GLib.source_remove(self._spring_timer_id) - - start_time = GLib.get_monotonic_time() / 1000 - offset_diff = target_offset - start_offset - - def animate_step(): - current_time = GLib.get_monotonic_time() / 1000 - elapsed = current_time - start_time - progress = min(1.0, elapsed / duration) - - # Use easing function for spring-like animation - eased_progress = self._ease_out_back(progress) - current_offset = start_offset + (offset_diff * eased_progress) - - # Calculate opacity and scale based on offset - max_offset = NOTIFICATION_WIDTH * 0.8 - offset_ratio = abs(current_offset) / max_offset - opacity = 1.0 - (offset_ratio * 0.3) # Fade slightly - scale = 1.0 - (offset_ratio * 0.05) # Slight scale down - - self._apply_transform(current_offset, opacity, scale) - - if progress >= 1.0: - self._animation_in_progress = False - self._current_offset = target_offset - return False - - return True - self._animation_in_progress = True - self._spring_timer_id = GLib.timeout_add(16, animate_step) # ~60fps def _ease_out_back(self, t): """Easing function for spring-back animation""" @@ -435,28 +343,30 @@ class NotificationRevealer(SlideRevealer): """Animate the notification sliding out completely - now much faster""" target_offset = NOTIFICATION_WIDTH + 50 # Slide completely out of view duration = 120 # Much faster dismiss animation (was 250) - + if self._spring_timer_id: GLib.source_remove(self._spring_timer_id) - + start_time = GLib.get_monotonic_time() / 1000 offset_diff = target_offset - start_offset - + def animate_step(): current_time = GLib.get_monotonic_time() / 1000 elapsed = current_time - start_time progress = min(1.0, elapsed / duration) - + # Use faster easing for snappier exit - eased_progress = progress * progress # Quadratic ease out - faster than cubic + eased_progress = ( + progress * progress + ) # Quadratic ease out - faster than cubic current_offset = start_offset + (offset_diff * eased_progress) - + # Fade out and scale down during dismiss opacity = 1.0 - (progress * 1.2) # Faster fade out - scale = 1.0 - (progress * 0.15) # Slightly more scale change - + scale = 1.0 - (progress * 0.15) # Slightly more scale change + self._apply_transform(current_offset, opacity, scale) - + if progress >= 1.0: # Trigger the actual notification dismissal immediately try: @@ -464,22 +374,24 @@ class NotificationRevealer(SlideRevealer): except: pass return False - + return True - + self._animation_in_progress = True - self._spring_timer_id = GLib.timeout_add(12, animate_step) # Higher fps for smoother animation + self._spring_timer_id = GLib.timeout_add( + 12, animate_step + ) # Higher fps for smoother animation def _calculate_drag_velocity(self, current_x): """Calculate the velocity of the drag gesture""" current_time = GLib.get_monotonic_time() / 1000 - + if self._last_drag_time > 0: time_diff = current_time - self._last_drag_time if time_diff > 0: distance_diff = current_x - self._drag_start_x - self._current_offset self._drag_velocity = abs(distance_diff / time_diff) - + self._last_drag_time = current_time def _on_animation_complete(self, is_hiding=False): @@ -523,109 +435,6 @@ class NotificationRevealer(SlideRevealer): timeout_duration = 80 if self._swipe_in_progress else (self.duration + 30) GLib.timeout_add(timeout_duration, lambda: self._on_animation_complete(True)) - def _on_button_press(self, _widget, event): - if event.button == 1: - self._drag_start_x = event.x - self._drag_start_y = event.y - self._is_dragging = True - self._swipe_in_progress = False - self._current_offset = 0 - self._last_drag_time = GLib.get_monotonic_time() / 1000 - self._drag_velocity = 0 - - # Stop any ongoing animations - if self._spring_timer_id: - GLib.source_remove(self._spring_timer_id) - self._animation_in_progress = False - return False - - def _on_button_release(self, _widget, event): - if event.button == 3: # Right click - try: - if self.parent_window: - self.parent_window.clear_notification_queue() - # Also dismiss current notification - self.notification.close("dismissed-by-user") - except: - pass # Ignore errors - return True - - elif self._is_dragging and event.button == 1: - self._is_dragging = False - - # Calculate final swipe metrics - dx = event.x - self._drag_start_x - dy = abs(event.y - self._drag_start_y) - - # Calculate dismiss threshold based on notification width - dismiss_distance = NOTIFICATION_WIDTH * self._dismiss_threshold - - # Determine if we should dismiss or spring back - should_dismiss = ( - (dx > dismiss_distance and dy < 60) or # Dragged far enough - (dx > 30 and self._drag_velocity > self._swipe_velocity_threshold and dy < 60) # Fast swipe - ) - - if should_dismiss: - try: - self._swipe_in_progress = True - # For fast dismissal, if already dragged far enough, close immediately - if dx > NOTIFICATION_WIDTH * 0.6: - # Skip animation and close immediately for very large swipes - self.notification.close("dismissed-by-user") - else: - # Animate the dismiss for smaller swipes - self._animate_dismiss(self._current_offset) - logger.debug(f"Notification dismissed by swipe: dx={dx}, velocity={self._drag_velocity}") - except Exception as e: - logger.error(f"Error dismissing notification by swipe: {e}") - else: - # Spring back to original position - if abs(self._current_offset) > 5: # Only animate if there's noticeable displacement - self._animate_spring_back(self._current_offset, 0) - else: - self._apply_transform(0, 1.0, 1.0) - self._current_offset = 0 - return False - - def _on_motion(self, _widget, event): - if self._is_dragging and not self._animation_in_progress: - # Calculate current swipe distance - dx = event.x - self._drag_start_x - dy = abs(event.y - self._drag_start_y) - - # Only respond to primarily horizontal gestures - if dy < 60: # Vertical tolerance - # Calculate velocity for smooth interaction - self._calculate_drag_velocity(event.x) - - # Apply real-time transform - only allow rightward movement - if dx > 0: - self._current_offset = dx - - # Calculate visual feedback based on drag distance - max_offset = NOTIFICATION_WIDTH * 0.8 - offset_ratio = min(1.0, dx / max_offset) - - # Apply diminishing returns for large swipes - adjusted_offset = dx * (1.0 - offset_ratio * 0.3) - - # Calculate opacity and scale - opacity = 1.0 - (offset_ratio * 0.4) # Fade as it's dragged - scale = 1.0 - (offset_ratio * 0.1) # Slight scale down - - self._apply_transform(adjusted_offset, opacity, scale) - - # Visual indication of dismiss threshold - if dx > NOTIFICATION_WIDTH * self._dismiss_threshold: - # Could add visual cue here (like changing color) - pass - else: - # Reset to original position if dragging left - self._current_offset = 0 - self._apply_transform(0, 1.0, 1.0) - return False - def destroy(self): # Clean up CSS provider and timers if self._spring_timer_id: diff --git a/modules/panel/components/indicators.py b/modules/panel/components/indicators.py index fc599ff..4a46eb6 100644 --- a/modules/panel/components/indicators.py +++ b/modules/panel/components/indicators.py @@ -15,7 +15,7 @@ class BluetoothIndicator(Box): self.bluetooth = BluetoothClient() self.bt_icon = Svg( name="bt-icon", - size=23, + size=18, svg_file=get_relative_path( "../../../config/assets/icons/applets/bluetooth-clear.svg" ), @@ -49,7 +49,12 @@ class BluetoothIndicator(Box): "../../../config/assets/icons/applets/bluetooth-clear.svg" ) ) - if len(connected_devices) == 1: + if len(connected_devices) >= 1: + self.bt_icon.set_from_file( + get_relative_path( + "../../../config/assets/icons/applets/bluetooth-paired.svg" + ) + ) device = connected_devices[0] tooltip = f"Connected to {device.alias}" if device.battery_percentage > 0: @@ -111,7 +116,7 @@ class NetworkIndicator(Box): self.network_icon = Svg( name="network-icon", - size=25, + size=18, svg_file=get_relative_path( "../../../config/assets/icons/applets/wifi-clear.svg" ), @@ -236,7 +241,7 @@ class BatteryIndicator(Box): self.battery_icon = Svg( name="battery-icon", - size=22, + size=23, svg_file=get_relative_path( "../../../config/assets/icons/battery/battery-100.svg" ), diff --git a/modules/panel/main.py b/modules/panel/main.py index 9bfb111..c34ef44 100644 --- a/modules/panel/main.py +++ b/modules/panel/main.py @@ -41,7 +41,7 @@ class Panel(Window): self.imac = Button( name="panel-button", child=Svg( - size=23, + size=21, svg_file=get_relative_path("../../config/assets/icons/misc/logo.svg"), ), on_clicked=lambda *_: self.menubar.show_system_dropdown(self.imac), @@ -98,7 +98,7 @@ class Panel(Window): name="panel-button", on_clicked=lambda *_: self.search_apps(), child=Svg( - size=22, + size=18, svg_file=get_relative_path("../../config/assets/icons/misc/search.svg"), ), ) diff --git a/modules/widget.py b/modules/widget.py new file mode 100644 index 0000000..2d8deab --- /dev/null +++ b/modules/widget.py @@ -0,0 +1,542 @@ +import psutil +from fabric.widgets.box import Box +from fabric.widgets.label import Label +from fabric.widgets.overlay import Overlay +from fabric.widgets.datetime import DateTime +from fabric.widgets.circularprogressbar import CircularProgressBar +from widgets.wayland import WaylandWindow as Window +from fabric.utils import invoke_repeater +import requests +import urllib.parse +import datetime +from gi.repository import GLib +from concurrent.futures import ThreadPoolExecutor +import time +import config.data as data +from config.data import load_config +import subprocess + +executor = ThreadPoolExecutor(max_workers=4) + + +config = load_config() + + +def margin(): + return ( + data.DOCK_ICON_SIZE + 10 + if not data.DOCK_ALWAYS_OCCLUDED and data.DOCK_ENABLED + else 0 + ) + + +def get_location(): + """Fetch location from config file or IP API asynchronously.""" + for attempt in range(5): + try: + response = requests.get("https://ipinfo.io/json", timeout=3) + if response.status_code == 200: + return response.json().get("city", "").replace(" ", "") + except requests.RequestException as e: + print(f"Error getting location: {e}") + if attempt < 4: + time.sleep(10) + else: + return "" + + +def get_location_async(callback): + """Fetch location asynchronously to prevent UI freeze.""" + executor.submit(lambda: GLib.idle_add(callback, get_location())) + + +def get_weather(callback): + """Fetch weather data asynchronously and update UI.""" + + def fetch_weather(): + location = get_location() + if not location: + return GLib.idle_add(callback, None) + + encoded_location = urllib.parse.quote(location) + url = f"https://wttr.in/{encoded_location}?format=j1" + urlemoji = f"https://wttr.in/{encoded_location}?format=%c" + + for attempt in range(5): + try: + response = requests.get(urlemoji, timeout=3) + responseinfo = requests.get(url, timeout=3).json() + + if response.status_code == 200: + temp_unit = "C" + temp = ( + responseinfo["current_condition"][0][f"temp_{temp_unit}"] + "°" + ) + condition = responseinfo["current_condition"][0]["weatherDesc"][0][ + "value" + ] + maxtemp = responseinfo["weather"][0]["maxtempC"] + "°" + mintemp = responseinfo["weather"][0]["mintempC"] + "°" + location = responseinfo["nearest_area"][0]["areaName"][0]["value"] + emoji = response.text.strip() + + GLib.idle_add( + callback, + [ + emoji, + temp, + condition, + location, + maxtemp, + mintemp, + ], + ) + return + except requests.RequestException as e: + print(f"Error fetching weather (attempt {attempt + 1}): {e}") + if attempt < 4: + time.sleep(10) + else: + GLib.idle_add(callback, None) + return + + executor.submit(fetch_weather) + + +def update_weather(widget): + def fetch_and_update(): + get_weather(lambda weather_info: update_widget(widget, weather_info)) + return True + + GLib.timeout_add_seconds(600, fetch_and_update) + fetch_and_update() + + +def update_widget(widget, weather_info): + if weather_info: + widget.weatherinfo = weather_info + widget.update_labels(weather_info) + + +class Sysinfo(Box): + @staticmethod + def bake_progress_bar(name: str = "progress-bar", size: int = 45, **kwargs): + return CircularProgressBar( + name=name, + start_angle=180, + end_angle=540, + min_value=0, + max_value=100, + size=size, + **kwargs, + ) + + @staticmethod + def bake_progress_icon(**kwargs): + return Label(**kwargs).build().add_style_class("progress-icon").unwrap() + + def __init__(self, **kwargs): + super().__init__( + layer="bottom", + title="sysinfo", + name="sysinfo", + visible=False, + all_visible=False, + **kwargs, + ) + + self.cpu_progress = self.bake_progress_bar() + self.ram_progress = self.bake_progress_bar() + self.bat_circular = self.bake_progress_bar().build().set_value(42).unwrap() + + self.progress_container = Box( + name="progress-bar-container", + spacing=12, + children=[ + Box( + children=[ + Overlay( + child=self.cpu_progress, + tooltip_text="", + overlays=[ + self.bake_progress_icon( + label="", + name="progress-icon-cpu", + ) + ], + ), + ], + ), + Box( + children=[ + Overlay( + child=self.ram_progress, + tooltip_text="", + overlays=[ + self.bake_progress_icon( + name="progress-icon-ram", + label="󰘚", + ) + ], + ) + ] + ), + Box( + children=[ + Overlay( + child=self.bat_circular, + tooltip_text="", + overlays=[ + self.bake_progress_icon( + label="󱊣", + name="progress-icon-bat", + ) + ], + ), + ], + ), + ], + ) + + self.update_status() + invoke_repeater(1000, self.update_status) + + self.add( + Box( + name="progress-bar-container-main", + orientation="v", + spacing=24, + children=[self.progress_container], + ), + ) + self.show_all() + + def update_status(self): + """Update system info asynchronously to prevent UI lag.""" + + def update(): + cpu = psutil.cpu_percent() + ram = psutil.virtual_memory().percent + battery = ( + psutil.sensors_battery().percent if psutil.sensors_battery() else 80 + ) + GLib.idle_add(self.cpu_progress.set_value, cpu) + GLib.idle_add(self.ram_progress.set_value, ram) + GLib.idle_add(self.bat_circular.set_value, battery) + + GLib.idle_add(self.cpu_progress.set_tooltip_text, f"{str(round(cpu))}%") + GLib.idle_add(self.ram_progress.set_tooltip_text, f"{str(round(ram))}%") + GLib.idle_add(self.bat_circular.set_tooltip_text, f"{str(round(battery))}%") + + executor.submit(update) + return True + + +def fetch_quote(callback): + """Fetch quotes asynchronously.""" + + def fetch(): + quotes_type = "stoic" + + url = ( + "https://stoic-quotes.com/api/quote" + if quotes_type == "stoic" + else "https://zenquotes.io/api/random" + ) + + for attempt in range(5): + try: + response = requests.get(url, timeout=3) + response.raise_for_status() + respdata = response.json() + quote = ( + f"{respdata[0]['q']} - {respdata[0]['a']}" + if quotes_type == "zen" + else f"{respdata['text']} - {respdata['author']}" + ) + break + except requests.RequestException as e: + print(f"Error fetching quote: {e}") + if attempt < 4: + time.sleep(10) + else: + try: + result = subprocess.run( + ["hyprctl", "splash"], + capture_output=True, + text=True, + check=True, + ) + quote = result.stdout.strip() + " - Team Hyprland" + except subprocess.CalledProcessError as e: + print(f"Error fetching quote from hyprctl: {e}") + quote = "I learn from the mistakes of people who take my advice - Trix" + + GLib.idle_add(callback, quote) + + executor.submit(fetch) + + +def fetch_quote_async(callback): + GLib.idle_add(lambda: fetch_quote(callback)) + + +class QuoteWidget(Label): + def __init__(self, **kwargs): + super().__init__( + name="quote", + label="", + anchor="center", + h_align="center", + v_align="center", + h_expand=True, + justification="center", + v_expand=True, + visible=False, + ) + fetch_quote_async(self.update_label) + + def update_label(self, quote): + """Update quote asynchronously.""" + max_width = 150 # Set the maximum width for the quote + if len(quote) > max_width: + words = quote.split() + line1, line2 = "", "" + for word in words: + if len(line1) + len(word) + 1 <= max_width: + line1 += word + " " + else: + line2 += word + " " + quote = line1.strip() + "\n" + line2.strip() + self.set_label(quote) + self.set_visible(True) + + +class ActivationMainText(Label): + def __init__(self, **kwargs): + super().__init__( + name="activation-main-text", + label="", + anchor="bottom right", + justification="left", + v_align="start", + h_align="start", + h_expand=True, + v_expand=True, + visible=False, + ) + self.set_label("Activate Linux") + + +class ActivationSubText(Label): + def __init__(self, **kwargs): + super().__init__( + name="activation-sub-text", + label="", + anchor="bottom right", + justification="left", + v_align="start", + h_align="start", + h_expand=True, + v_expand=True, + visible=False, + ) + self.set_label("Go to Settings to activate Linux") + + +def create_widgets(config): + widgets = [] + if config.get("widgets_displaytype_visible", True): + if config.get("widgets_clock_visible", True): + widgets.append( + DateTime(formatters=["%A, %d %B"], interval=10000, name="date") + ) + if config.get("widgets_date_visible", True): + widgets.append(DateTime(formatters=["%I:%M %p"], name="clock")) + if config.get("widgets_quote_visible", True): + widgets.append(QuoteWidget()) + if config.get("widgets_weatherwid_visible", True): + widgets.append(WeatherWidget()) + else: + widgets.append(DateTime(formatters=["%I:%M %p"], name="clock")) + widgets.append(DateTime(formatters=["%A. %d %B"], interval=10000, name="date")) + return widgets + + +class Weather(Box): + def __init__(self, parent, **kwargs): + super().__init__( + name="weather-widget", + h_expand=True, + v_expand=True, + justification="right", + orientation="v", + all_visible=False, + **kwargs, + ) + + self.parent = parent + self.city = Label( + name="city", label="New York", justification="right", h_align="start" + ) + self.temperature = Label(name="temperature", label="36", h_align="start") + self.condition_em = Label(name="condition-emoji", label="☁️", h_align="start") + self.condition = Label(name="condition", label="Cloudy", h_align="start") + self.feels_like = Label(name="feels-like", label="H:28 L:20", h_align="start") + self.add(self.city) + self.add(self.temperature) + self.add(self.condition_em) + self.add(self.condition) + self.add(self.feels_like) + update_weather(self) + + def update_labels(self, weather_info): + if not self.weatherinfo: + return + + # Unpack weather info into variables for better readability + emoji, temp, condition, location, maxtemp, mintemp = self.weatherinfo + maxmin = f"H:{maxtemp} L:{mintemp}" + + # Store references to deeply nested children to avoid repeated lookups + + self.city.set_label(location) + self.feels_like.set_label(maxmin) + self.condition.set_label(condition) + self.temperature.set_label(temp) + self.condition_em.set_label(emoji) + self.parent.set_visible(True) + + +class WeatherContainer(Box): + def __init__(self, **kwargs): + super().__init__( + orientation="v", + name="box-widget-2", + v_expand=True, + v_align="center", + visible=True, + h_align="center", + children=[ + Weather(self), + ], + **kwargs, + ) + # self.set_style("background-color: rgba(0, 0, 0, 0.5);") + # self.add(Weather()) + + +class Date(Box): + def __init__(self, **kwargs): + super().__init__( + name="date-widget", + h_expand=True, + v_expand=True, + justification="center", + h_align="center", + v_align="start", + orientation="v", + **kwargs, + ) + + self.top = Box(orientation="h", name="date-top", h_expand=True) + self.dateone = DateTime(formatters=["%a"], interval=10000, name="day") + self.datetwo = DateTime(formatters=["%b"], interval=10000, name="month") + self.datethree = DateTime(formatters=["%-d"], interval=10000, name="date") + self.top.add(self.dateone) + self.top.add(self.datetwo) + self.add(self.top) + self.add(self.datethree) + + +class Deskwidgets(Window): + def __init__(self, **kwargs): + config = load_config() + super().__init__( + name="desktop", + layer="bottom", + title="desktop-widgets", + orientation="v", + exclusivity="none", + child=[ + Window( + anchor="top left", + orientation="h", + layer="bottom", + child=[ + Box( + name="desktop-widgets-container", + children=[ + Box( + orientation="v", + name="box-widget", + v_expand=True, + v_align="center", + h_align="center", + children=[Date()], + ), + WeatherContainer(), + ], + ), + ], + ), + Window( + anchor="bottom left", + orientation="h", + layer="bottom", + child=[], + ), + ], + **kwargs, + ) + + +# else: +# +# class Deskwidgets(Window): +# def __init__(self, **kwargs): +# config = load_config() +# super().__init__(name="desktop", **kwargs) +# desktop_widget = Window( +# layer="bottom", +# anchor="bottom left", +# v_align="start", +# h_align="start", +# h_expand=True, +# v_expand=True, +# justification="left", +# exclusivity="none", +# child=Box( +# orientation="v", +# children=create_widgets(config), +# ), +# all_visible=True, +# ) +# if config.get("widgets_activation_visible", True): +# activationnag = Window( +# name="activation", +# anchor="bottom right", +# layer="top", +# justification="left", +# v_align="start", +# h_align="start", +# h_expand=True, +# v_expand=True, +# child=Box( +# orientation="v", +# children=[ +# ActivationMainText(), +# ActivationSubText(), +# ], +# ), +# all_visible=True, +# ) +# else: +# activationnag = None +# +# else: +# +# class Deskwidgets(Window): +# def __init__(self, **kwargs): +# super().__init__(name="desktop", **kwargs) +# self.set_visible(False) diff --git a/styles/controlcenter.css b/styles/controlcenter.css index 728553f..9e20346 100644 --- a/styles/controlcenter.css +++ b/styles/controlcenter.css @@ -5,14 +5,16 @@ margin: 0; } + #control-center-widgets { - background-color: alpha(#000, 0.3); + background-color: alpha(#fff, 0.09); box-shadow: inset 0 0 0 1px alpha(#aaa, 0.4); border: 1px solid alpha(#111, 0.4); border-radius: 1.25rem; padding: 0.5rem; } + /* Widgets */ #wifi-widget, #bluetooth-widget { diff --git a/styles/launcher.css b/styles/launcher.css index 767653c..9b32d41 100644 --- a/styles/launcher.css +++ b/styles/launcher.css @@ -1,13 +1,14 @@ #launcher { - background-color: alpha(#999, 0.3); - color: #000; + background-color: alpha(#000, 0.3); padding: 0; border-radius: 12px; min-width: 640px; border: 1px solid rgba(255, 255, 255, 0.1); - box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5); } +#launcher-search{ + color: #000; +} #header_box { padding: 0px 20px 0 20px; color: #000; diff --git a/styles/notification-center.css b/styles/notification-center.css index 3e19887..4bb5106 100644 --- a/styles/notification-center.css +++ b/styles/notification-center.css @@ -14,8 +14,11 @@ margin-top: 10px; } +#noti-clear-button { + background-color: alpha(#888, 0.09); +} #noti-clear-button:hover { - background-color: var(--surface-bright); + background-color: alpha(#fff, 0.09); } /* Stacked notification group styles */ diff --git a/styles/notification.css b/styles/notification.css index 09a7598..55ed6b0 100644 --- a/styles/notification.css +++ b/styles/notification.css @@ -1,6 +1,6 @@ #notification { padding: 16px; - background-color: alpha(#000, 0.3); + background-color: alpha(#fff, 0.03); /* border: 0.5px solid alpha(#111, 0.4); */ /* box-shadow: */ /* inset 0 0 200px 0 alpha(#111, 0.5), */ @@ -15,14 +15,14 @@ } #notification-action-buttons button { - background-color: alpha(#000, 0.3); + background-color: alpha(#999, 0.3); padding: 8px; border-radius: 20px; transition: background-color 0.1s ease; } #notification-action-buttons button:hover { - background-color: var(--surface-bright); + background-color: alpha(#000, 0.3); } #notification-action-buttons button:active { diff --git a/styles/panel.css b/styles/panel.css index 41b4a35..bd295c9 100644 --- a/styles/panel.css +++ b/styles/panel.css @@ -16,7 +16,6 @@ } #panel-button { - border-radius: 8px; margin: 0 4px 0 4px; min-width: 20px; min-height: 20px;