From 377e5d8733add9b3062493a95db821b4fc9a26e8 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Sun, 27 Jul 2025 04:24:27 +0545 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(dashboard):=20implement=20Blue?= =?UTF-8?q?tooth=20and=20WiFi=20tiles=20with=20detailed=20content=20views?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main.css | 176 ++++++++++++++- modules/dashboard/bluetooth.py | 201 ++++++++++++++++- modules/dashboard/main.py | 113 +++++++--- modules/dashboard/network.py | 207 ++++++++++++++++- modules/dashboard/tile.py | 200 ++++++++++++----- "styles/\\" | 395 --------------------------------- utils/icons.py | 6 +- 7 files changed, 810 insertions(+), 488 deletions(-) delete mode 100644 "styles/\\" diff --git a/main.css b/main.css index a257f4c..45ab6a5 100644 --- a/main.css +++ b/main.css @@ -69,7 +69,6 @@ tooltip > * { box-shadow: 0 0 3px alpha(black, 0.7); } - #dashboard-window { transition: margin 0.25s cubic-bezier(0.5, 0.25, 0, 1); } @@ -150,4 +149,179 @@ tooltip > * { border-color: var(--primary); } +#matugen-switcher, +#bluetooth-switch, +#wifi-switch, +#dnd-switch { + min-width: 40px; + min-height: 20px; + background-color: var(--surface); + border-radius: 15px; + padding: 2px; + transition: background-color 0.3s ease; +} +/* Style the switch's slider */ +#matugen-switcher slider, +#bluetooth-switch slider, +#wifi-switch slider, +#dnd-switch slider { + background-color: var(--primary); + border-radius: 16px; + min-width: 16px; + min-height: 8px; + transition: + background-color 0.1s cubic-bezier(0.5, 0.25, 0, 1.25), + transform 0.25s cubic-bezier(0.5, 0.25, 0, 1.25); +} + +/* When the switch is active (checked) */ +#matugen-switcher:checked, +#bluetooth-switch:checked, +#wifi-switch:checked, +#dnd-switch:checked { + background-color: var(--primary); +} + +/* Optional: additional styling for the slider when active */ +#matugen-switcher:checked slider, +#bluetooth-switch:checked slider, +#wifi-switch:checked slider, +#dnd-switch:checked slider { + background-color: var(--shadow); +} + +#matugen-switcher:checked image, +#bluetooth-switch:checked image, +#wifi-switch:checked image, +#dnd-switch:checked image { + color: var(--shadow); +} + + +#bluetooth-header, +#network-header { + border: 2px solid var(--surface); + padding: 4px; + border-radius: 12px; +} + +#bluetooth-device, +#wifi-ap-slot { + border: 2px solid var(--surface); + border-radius: 12px; + padding: 4px; +} + +#bluetooth-scan, +#bluetooth-back, +#network-refresh, +#network-back { + background-color: var(--surface); + border-radius: 8px; + padding: 4px; +} + +#bluetooth-back-label, +#network-back-label { + font-size: 20px; +} + +#bluetooth-scan:hover, +#bluetooth-back:hover, +#network-refresh:hover, +#network-back:hover { + background-color: var(--surface-bright); +} + +#bluetooth-scan label, +#bluetooth-toggle label, +#wifi-connect-button label { + font-weight: bold; +} + +#bluetooth-text, +#bluetooth-section { + font-weight: bold; +} + +#bluetooth-section { + background-color: var(--surface); + border-radius: 12px; + padding: 8px; +} + +#bluetooth-connection { + font-size: 20px; +} + +#bluetooth-connect, +#wifi-connect-button { + font-weight: bold; + background-color: var(--surface); + border-radius: 8px; + padding: 8px; +} + +#wifi-connect-button.connected { + background-color: var(--green); +} + +#wifi-connect-button.connected label { + color: var(--shadow); +} + +#bluetooth-connect.connected { + background-color: var(--blue); +} + +#bluetooth-connect.connected label { + color: var(--shadow); +} + +#bluetooth-connect:hover { + background-color: var(--surface-bright); +} + +#bluetooth-paired, +#bluetooth-available { + background-color: var(--surface); + border-radius: 20px; + padding: 4px; +} + +#bt-sep { + /*padding: 1px;*/ + border-radius: 16px; + /*background-color: var(--surface);*/ + /*margin: 0 16px;*/ +} + +#bluetooth-scan-label, +#network-refresh-label { + font-size: 20px; + color: var(--primary); +} + +#bluetooth-scan.scanning { + animation: blink 0.5s ease infinite; +} + +#bluetooth-scan-label.scanning { + animation: blink 0.5s ease infinite; +} + +@keyframes blink { + 0% { + background-color: var(--blue); + color: var(--shadow); + } + 50% { + background-color: var(--shadow); + color: var(--blue); + } + 100% { + background-color: var(--blue); + color: var(--shadow); + } +} \ No newline at end of file diff --git a/modules/dashboard/bluetooth.py b/modules/dashboard/bluetooth.py index 1726eeb..64d8483 100644 --- a/modules/dashboard/bluetooth.py +++ b/modules/dashboard/bluetooth.py @@ -1,12 +1,85 @@ +from fabric.bluetooth import BluetoothClient, BluetoothDevice +from fabric.widgets.box import Box +from fabric.widgets.button import Button +from fabric.widgets.centerbox import CenterBox +from fabric.widgets.image import Image from fabric.widgets.label import Label +from fabric.widgets.scrolledwindow import ScrolledWindow +from gi.repository import Gtk from modules.dashboard.tile import Tile - import utils.icons as icons +class BluetoothDeviceSlot(CenterBox): + """A widget representing a single Bluetooth device in the dashboard.""" + + def __init__(self, device: BluetoothDevice, **kwargs): + super().__init__(name="bluetooth-device", **kwargs) + self.device = device + self.device.connect("changed", self.on_changed) + self.device.connect( + "notify::closed", lambda *_: self.device.closed and self.destroy() + ) + + self.connection_label = Label( + name="bluetooth-connection", + markup=icons.bluetooth_disconnected + ) + self.connect_button = Button( + name="bluetooth-connect", + label="Connect", + on_clicked=lambda *_: self.device.set_connecting(not self.device.connected), + style_classes=["connected"] if self.device.connected else None, + ) + + self.start_children = [ + Box( + spacing=8, + h_expand=True, + h_align="fill", + children=[ + Image(icon_name=device.icon_name + "-symbolic", size=16), + Label( + label=device.name, + h_expand=True, + h_align="start", + ellipsization="end", + ), + self.connection_label, + ], + ) + ] + self.end_children = self.connect_button + + self.device.emit("changed") + + def on_changed(self, *_): + """Handle device state changes.""" + self.connection_label.set_markup( + icons.bluetooth_connected + if self.device.connected + else icons.bluetooth_disconnected + ) + if self.device.connecting: + self.connect_button.set_label( + "Connecting..." if not self.device.connecting else "..." + ) + else: + self.connect_button.set_label( + "Connect" if not self.device.connected else "Disconnect" + ) + if self.device.connected: + self.connect_button.add_style_class("connected") + else: + self.connect_button.remove_style_class("connected") + + class Bluetooth(Tile): + """Bluetooth tile for the dashboard that shows Bluetooth status and devices.""" + def __init__(self, **kwargs): + # Create status label self.label = Label( style_classes=["desc-label", "off"], label="Off", @@ -22,7 +95,133 @@ class Bluetooth(Tile): **kwargs, ) + # Initialize bluetooth client with device handling + self.bluetooth_client = BluetoothClient(on_device_added=self.on_device_added) + self.bluetooth_client.connect("notify::enabled", self._on_bluetooth_state_changed) + self.bluetooth_client.connect("notify::scanning", self._update_scan_label) + + # Initialize state + self._update_tile_state() + + def _update_tile_state(self): + """Update tile visual state based on Bluetooth status.""" + self.state = self.bluetooth_client.enabled if self.state: + self.remove_style_class("off") self.add_style_class("on") + self.label.set_label("On") else: + self.remove_style_class("on") self.add_style_class("off") + self.label.set_label("Off") + + def create_content(self): + """Create the detailed bluetooth content for the dashboard.""" + # Create Bluetooth toggle switch (like notifications DND switch) + self.bluetooth_switch = Gtk.Switch(name="bluetooth-switch") + self.bluetooth_switch.set_vexpand(False) + self.bluetooth_switch.set_valign(Gtk.Align.CENTER) + self.bluetooth_switch.set_active(self.bluetooth_client.enabled) + self.bluetooth_switch.connect("notify::active", self._on_bluetooth_toggle) + + # Create Bluetooth status icon + self.bluetooth_status_icon = Label( + name="bluetooth-status-icon", + markup=icons.bluetooth if self.bluetooth_client.enabled else icons.bluetooth_off + ) + + # Create scan button with icon + self.scan_label = Label(name="bluetooth-scan-label", markup=icons.radar) + self.scan_button = Button( + name="bluetooth-scan", + child=self.scan_label, + tooltip_text="Scan for Bluetooth devices", + on_clicked=lambda *_: self.bluetooth_client.toggle_scan(), + ) + + # Create device containers + self.paired_box = Box(spacing=2, orientation="v") + self.available_box = Box(spacing=2, orientation="v") + + # Create content structure + content_box = Box(spacing=4, orientation="v") + content_box.add(self.paired_box) + content_box.add(Label(name="bluetooth-section", label="Available")) + content_box.add(self.available_box) + + # Main container + main_container = Box( + name="bluetooth-content", + orientation="v", + spacing=8, + h_expand=True, + v_expand=True, + h_align="fill", + style_classes=["tile-content"], + children=[ + CenterBox( + name="bluetooth-header", + start_children=[self.bluetooth_switch, self.bluetooth_status_icon], + center_children=Label(name="bluetooth-text", label="Bluetooth Devices"), + end_children=self.scan_button, + ), + ScrolledWindow( + name="bluetooth-devices", + min_content_size=(-1, -1), + child=content_box, + v_expand=True, + propagate_width=False, + propagate_height=False, + ), + ] + ) + + # Initialize notifications + self.bluetooth_client.notify("scanning") + self.bluetooth_client.notify("enabled") + + return main_container + + def on_device_added(self, client: BluetoothClient, address: str): + """Handle new device discovery.""" + if not (device := client.get_device(address)): + return + slot = BluetoothDeviceSlot(device) + + if device.paired: + return self.paired_box.add(slot) + return self.available_box.add(slot) + + def _update_scan_label(self, *_): + """Update scan button appearance based on scanning state.""" + if self.bluetooth_client.scanning: + self.scan_label.add_style_class("scanning") + self.scan_button.add_style_class("scanning") + self.scan_button.set_tooltip_text("Stop scanning for Bluetooth devices") + else: + self.scan_label.remove_style_class("scanning") + self.scan_button.remove_style_class("scanning") + self.scan_button.set_tooltip_text("Scan for Bluetooth devices") + + def _on_bluetooth_toggle(self, switch, *_): + """Handle Bluetooth toggle switch.""" + enabled = switch.get_active() + self.bluetooth_client.enabled = enabled + self._update_content_state() + + def _on_bluetooth_state_changed(self, *_): + """Handle bluetooth state change.""" + self._update_tile_state() + self._update_content_state() + + def _update_content_state(self): + """Update content area based on Bluetooth state.""" + if hasattr(self, 'bluetooth_switch'): + self.bluetooth_switch.set_active(self.bluetooth_client.enabled) + + if hasattr(self, 'bluetooth_status_icon'): + self.bluetooth_status_icon.set_markup( + icons.bluetooth if self.bluetooth_client.enabled else icons.bluetooth_off + ) + + diff --git a/modules/dashboard/main.py b/modules/dashboard/main.py index 5b24b65..6186503 100644 --- a/modules/dashboard/main.py +++ b/modules/dashboard/main.py @@ -1,5 +1,5 @@ from fabric.widgets.box import Box -from fabric.widgets.revealer import Revealer +from fabric.widgets.stack import Stack from fabric.core.service import Property from modules.dashboard.bluetooth import Bluetooth @@ -21,8 +21,8 @@ class Dashboard(Box): all_visible=True, **kwargs, ) - self.wifi = Network() - self.bluetooth = Bluetooth() + self.wifi = Network(name="wifi-tile") + self.bluetooth = Bluetooth(name="bluetooth-tile") self.mini_player = PlayerContainerMini() self.mini_player_tile = TileSpecial( props=self.mini_player, mini_props=self.mini_player.get_mini_view() @@ -35,6 +35,9 @@ class Dashboard(Box): # Create real notifications component self.dashboard_notifications = DashboardNotifications() + # Track currently active tile + self.active_tile = None + self.tiles = Box( children=[ self.wifi, @@ -43,61 +46,103 @@ class Dashboard(Box): ], ) - self.notification_container = Revealer( - transition_duration=250, - transition_type="slide-down", + # Create content containers for each tile + self.wifi_content = self.wifi.create_content() + self.bluetooth_content = self.bluetooth.create_content() + + # Default content (notifications) + self.default_content = Box( + name="notification-history-popup-content", + orientation="v", + spacing=8, h_expand=True, - child=Box( - name="notification-history-popup-content", - orientation="v", - spacing=8, - h_expand=True, - v_expand=True, - children=[self.dashboard_notifications], - style_classes=["notification-history-popup"], - ), - child_revealed=True, + v_expand=True, + children=[self.dashboard_notifications], + style_classes=["notification-history-popup"], ) + + # Content stack to switch between different tile contents + self.content_stack = Stack( + transition_type="crossfade", + transition_duration=200, + h_expand=True, + v_expand=True, + h_align="fill", + children=[ + self.default_content, + self.wifi_content, + self.bluetooth_content, + ], + ) + + # Set default visible child + self.content_stack.set_visible_child(self.default_content) + self.children = [ Box(name="inner", children=self.tiles), Box( children=[ - self.notification_container, + self.content_stack, ] ), ] def handle_tile_menu_expand(self, tile: str, toggle: bool): + print(f"Tile menu expand: {tile}, toggle: {toggle}, active_tile: {self.active_tile}") + + # If clicking the same tile that's already active, deactivate it + if self.active_tile == tile and toggle: + toggle = False + # Update the tile's toggle state + for i in self.tiles: + if i.get_name() == tile: + i.toggle = False + break + if toggle: - self.notification_container.set_reveal_child(False) + # Show the specific tile's content in the content area + self.active_tile = tile + if tile == "wifi-tile": + self.content_stack.set_visible_child(self.wifi_content) + print("Showing WiFi content") + elif tile == "bluetooth-tile": + self.content_stack.set_visible_child(self.bluetooth_content) + print("Showing Bluetooth content") else: - self.notification_container.set_reveal_child(True) - for i in self.tiles: - if i.get_name() == tile: - print("found") - else: - if toggle: - i.mini_view() - i.icon.add_style_class("mini") - i.icon.remove_style_class("maxi") - else: - i.maxi_view() - i.icon.add_style_class("maxi") - i.icon.remove_style_class("mini") - print("search complete") + # Show default content (notifications) + self.active_tile = None + self.content_stack.set_visible_child(self.default_content) + print("Showing default content") + + # Don't change tile visual states - keep all tiles unchanged + print("Tile handling complete") + + def reset_to_default(self): + """Reset dashboard to default state (notifications visible, no active tile)""" + print("Resetting dashboard to default state") + self.active_tile = None + self.content_stack.set_visible_child(self.default_content) + + # Reset all tile toggle states + for tile in self.tiles: + if hasattr(tile, 'toggle'): + tile.toggle = False class DashboardWindow(Window): visible = Property(bool, flags="read-write", default_value=False) def __init__(self, **kwargs): + # Create dashboard instance and store reference + self.dashboard = Dashboard() + super().__init__( name="dashboard-window", layer="top", anchor="bottom", exclusivity="none", keyboard_mode="on-demand", - child=Dashboard(), + child=self.dashboard, visible=False, all_visible=False, **kwargs, @@ -112,6 +157,8 @@ class DashboardWindow(Window): self.visible = True def close_dashboard(self): + # Reset dashboard to default state when closing + self.dashboard.reset_to_default() self.hide() self.visible = False diff --git a/modules/dashboard/network.py b/modules/dashboard/network.py index 033b18d..a54ad2d 100644 --- a/modules/dashboard/network.py +++ b/modules/dashboard/network.py @@ -1,4 +1,8 @@ from fabric.widgets.label import Label +from fabric.widgets.box import Box +from fabric.widgets.button import Button +from fabric.widgets.scrolledwindow import ScrolledWindow +from gi.repository import Gtk from modules.dashboard.tile import Tile from services.network import NetworkClient @@ -26,7 +30,7 @@ class Network(Tile): self.nm.connect("changed", self.handle_connection_change) self.nm.connect("ready", self.handle_connection_change) - def handle_connection_change(self, *args): + def handle_connection_change(self, *_): # Get current network state from NetworkClient properties connected = False con_label = "Disconnected" @@ -48,3 +52,204 @@ class Network(Tile): print("State change:", connected) self.label.set_label(con_label) + + + def create_content(self): + """Create the detailed network content for the dashboard""" + content_box = Box( + name="network-content", + orientation="v", + spacing=8, + h_expand=True, + v_expand=True, + h_align="fill", + style_classes=["tile-content"], + ) + + # Header with WiFi toggle + header_box = Box( + orientation="h", + spacing=12, + h_expand=True, + h_align="fill", + ) + + wifi_label = Label( + markup="Wi-Fi", + h_align="start", + h_expand=True, + ) + + self.wifi_switch = Gtk.Switch( + name="wifi-switch", + active=self.nm.wireless_enabled if self.nm.wifi_device else False, + ) + self.wifi_switch.connect("notify::active", self._on_wifi_toggle) + + header_box.children = [wifi_label, self.wifi_switch] + content_box.add(header_box) + + # Networks list + self.networks_container = Box( + orientation="v", + spacing=4, + h_expand=True, + ) + + # Scrolled window for networks + networks_scroll = ScrolledWindow( + h_scrollbar_policy="never", + v_scrollbar_policy="automatic", + h_expand=True, + v_expand=True, + child=self.networks_container, + ) + networks_scroll.set_size_request(-1, 200) + + content_box.add(networks_scroll) + + # Scan button + scan_button = Button( + label="Scan for Networks", + h_align="center", + on_clicked=self._on_scan_clicked, + ) + content_box.add(scan_button) + + # Populate networks initially + self._refresh_networks() + + return content_box + + def _on_wifi_toggle(self, *_): + """Handle WiFi toggle""" + if self.nm.wifi_device: + self.nm.wifi_device.toggle_wifi() + self._refresh_networks() + + def _on_scan_clicked(self, *_): + """Handle scan button click""" + if self.nm.wifi_device: + self.nm.wifi_device.scan() + self._refresh_networks() + + def _refresh_networks(self): + """Refresh the networks list""" + # Clear existing networks + for child in self.networks_container.get_children(): + self.networks_container.remove(child) + + if not self.nm.wifi_device or not self.nm.wifi_device.wireless_enabled: + no_wifi_label = Label( + label="WiFi is disabled", + h_align="center", + style_classes=["dim-label"], + ) + self.networks_container.add(no_wifi_label) + return + + access_points = self.nm.wifi_device.access_points + if not access_points: + no_networks_label = Label( + label="No networks found\nClick 'Scan for Networks' to refresh", + h_align="center", + style_classes=["dim-label"], + ) + self.networks_container.add(no_networks_label) + return + + # Sort access points by signal strength (connected first, then by strength) + sorted_aps = sorted( + access_points, key=lambda ap: (not ap.is_active, -ap.strength) + ) + + for ap in sorted_aps: + self._create_network_item(ap) + + self.networks_container.show_all() + + def _create_network_item(self, ap): + """Create a network item widget""" + network_box = Box( + orientation="h", + spacing=8, + h_expand=True, + style_classes=["network-item"], + ) + + # Signal strength icon + signal_strength = min(100, max(0, ap.strength)) + if signal_strength >= 75: + signal_icon = icons.wifi_3 + elif signal_strength >= 50: + signal_icon = icons.wifi_2 + elif signal_strength >= 25: + signal_icon = icons.wifi_1 + else: + signal_icon = icons.wifi_0 + + signal_label = Label( + markup=signal_icon, + style_classes=["network-signal"], + ) + network_box.add(signal_label) + + # Network name and status + info_box = Box( + orientation="v", + h_expand=True, + h_align="start", + ) + + if ap.is_active: + name_markup = f"{ap.ssid}" + status_label = Label( + label="Connected", + h_align="start", + style_classes=["network-status", "connected"], + ) + info_box.add(Label(markup=name_markup, h_align="start")) + info_box.add(status_label) + else: + name_label = Label( + label=ap.ssid, + h_align="start", + style_classes=["network-name"], + ) + info_box.add(name_label) + + network_box.add(info_box) + + # Lock icon for secured networks + if ap.requires_password: + lock_label = Label( + markup=icons.lock, + style_classes=["network-lock"], + ) + network_box.add(lock_label) + + # Make the whole item clickable + network_button = Button( + child=network_box, + style_classes=["network-button"], + on_clicked=lambda *_: self._on_network_clicked(ap), + ) + + self.networks_container.add(network_button) + + def _on_network_clicked(self, ap): + """Handle network item click""" + if ap.is_active: + # Disconnect from current network + if self.nm.wifi_device: + self.nm.wifi_device.disconnect_wifi() + else: + # Connect to network (simplified - would need password handling for secured networks) + if self.nm.wifi_device: + try: + self.nm.wifi_device.connect_to_access_point(ap) + except Exception as e: + print(f"Failed to connect to {ap.ssid}: {e}") + + # Refresh networks after action + self._refresh_networks() diff --git a/modules/dashboard/tile.py b/modules/dashboard/tile.py index 89035e6..b3a9c92 100644 --- a/modules/dashboard/tile.py +++ b/modules/dashboard/tile.py @@ -1,26 +1,82 @@ +""" +Dashboard Tile Components + +This module provides tile widgets for the dashboard interface. + +Overview: +- Tiles are interactive components that display an icon, label, and status +- When clicked, tiles show their detailed content in the dashboard's content area +- Tiles can switch between mini (compact) and maxi (full) views +- Two tile types are available: Tile (standard) and TileSpecial (custom layouts) + +Architecture: +- Tile: Standard tile with icon, label, properties, and optional menu button +- TileSpecial: Custom tile that can switch between different view layouts +- Both tiles communicate with the parent dashboard to show content + +Usage: +- Create tiles with appropriate properties and add them to the dashboard +- The dashboard handles content switching and tile state management +- Tiles automatically notify the dashboard when clicked +""" + from fabric.widgets.box import Box from fabric.widgets.button import Button from fabric.widgets.label import Label from fabric.widgets.stack import Stack from fabric.widgets.revealer import Revealer -from fabric.widgets.centerbox import CenterBox import utils.icons as icons class Tile(Box): + """ + A dashboard tile widget that displays an icon, label, and properties. + + When clicked, the tile notifies the dashboard to show its detailed content + in the dashboard's content area instead of expanding inline. + + Args: + menu (bool): Whether this tile has a menu button + markup (str): Icon markup to display + label (str): Text label for the tile + props (Label): Properties widget to show tile status + **kwargs: Additional widget arguments + """ + def __init__(self, *, menu: bool, markup: str, label: str, props: Label, **kwargs): + # Setup CSS classes default_classes = ["tile"] extra_classes = kwargs.pop("style_classes", []) - merged_classes = default_classes + extra_classes - super().__init__(style_classes=merged_classes, v_align="start", **kwargs) - self.props = props - self.icon = Label(style_classes="tile-icon", markup=markup) - self.tile_label = Label( - style_classes="tile-label", label=label, h_align="start" - ) - self.toggle = False - self.dashboard_instance = None # Will be set by the dashboard + all_classes = default_classes + extra_classes + super().__init__(style_classes=all_classes, v_align="start", **kwargs) + + # Store tile properties + self.props = props + self.toggle = False # Track if tile is active + self.dashboard_instance = None # Reference to parent dashboard + + # Create tile components + self._create_tile_icon(markup) + self._create_tile_content(label) + self._create_tile_layout(menu) + + def _create_tile_icon(self, markup: str): + """Create the tile's icon label.""" + self.icon = Label( + style_classes="tile-icon", + markup=markup + ) + + def _create_tile_content(self, label: str): + """Create the tile's text content area.""" + self.tile_label = Label( + style_classes="tile-label", + label=label, + h_align="start" + ) + + # Container for label and properties self.type_box = Box( style_classes="tile-type", orientation="v", @@ -30,41 +86,39 @@ class Tile(Box): children=[self.tile_label, self.props], ) - self.menu_button = Button( - style_classes="tile-button", - h_expand=True, - child=Label(style_classes="tile-icon", markup=icons.arrow_head), - on_clicked=self.handle_click, - ) - - self.content_button = None - - if menu: - self.content_button = Revealer( - transition_duration=150, - transition_type="slide-left", + def _create_tile_layout(self, has_menu: bool): + """Create the tile's layout structure.""" + # Create menu button if needed + if has_menu: + self.menu_button = Button( + style_classes="tile-button", h_expand=True, - child=Box(children=[self.type_box, self.menu_button]), - child_revealed=True, + child=Label(style_classes="tile-icon", markup=icons.arrow_head), + on_clicked=self.handle_click, ) + content_children = [self.type_box, self.menu_button] else: - self.content_button = Revealer( - transition_duration=150, - transition_type="slide-left", - child=Box(children=[self.type_box]), - child_revealed=True, - ) + content_children = [self.type_box] - self.normal_view = Box( - children=[self.icon, self.content_button], + # Create revealer for smooth transitions + self.content_button = Revealer( + transition_duration=150, + transition_type="slide-left", + h_expand=True, + child=Box(children=content_children), + child_revealed=True, ) - self.menu = Button( + # Create the main tile view + self.normal_view = Box(children=[self.icon, self.content_button]) + + # Create placeholder menu (content is shown in dashboard content area) + self.menu = Box( style_classes="tile-menu", - child=Label(label="hi!"), - on_clicked=self.handle_click, + children=[Label(label="Tile content will be shown in dashboard content area")], ) + # Stack to switch between normal and menu views self.stack = Stack( transition_type="crossfade", transition_duration=150, @@ -74,30 +128,39 @@ class Tile(Box): self.children = self.stack - def handle_click(self, source): - if self.toggle: - self.toggle = False - self.menu.add_style_class("contract") - self.menu.remove_style_class("expand") - self.stack.set_visible_child(self.normal_view) - else: - self.toggle = True - self.stack.set_visible_child(self.menu) - self.menu.add_style_class("expand") - self.menu.remove_style_class("contract") - print(self.toggle, self.get_name()) + def handle_click(self, *_): + """ + Handle tile click events. - # Call dashboard method directly instead of using fabric-cli + When clicked, the tile toggles its active state and notifies the dashboard + to show/hide its content in the dashboard's content area. + """ + self.toggle = not self.toggle + print(f"Tile {self.get_name()} clicked, toggle: {self.toggle}") + + # Notify dashboard to handle content switching if self.dashboard_instance: self.dashboard_instance.handle_tile_menu_expand(self.get_name(), self.toggle) def mini_view(self): + """ + Switch tile to mini (compact) view. + + In mini view, the tile content is hidden and only the icon is shown. + This is used when other tiles are active. + """ self.content_button.set_reveal_child(False) self.icon.set_h_expand(True) self.content_button.set_h_expand(False) self.add_style_class("mini") def maxi_view(self): + """ + Switch tile to maxi (full) view. + + In maxi view, the tile shows its full content including label and properties. + This is the default state when no tiles are active. + """ self.content_button.set_reveal_child(True) self.icon.set_h_expand(False) self.content_button.set_h_expand(True) @@ -105,19 +168,37 @@ class Tile(Box): class TileSpecial(Box): + """ + A special tile widget that can switch between normal and mini views. + + This tile type is used for components that need different layouts + in normal vs compact modes (e.g., media player). + + Args: + mini_props: Widget(s) to show in mini view + props: Widget(s) to show in normal view + **kwargs: Additional widget arguments + """ + def __init__(self, *, mini_props, props, **kwargs): super().__init__(**kwargs) + + # Store view components self.props = props self.mini_props = mini_props self.toggle = False - self.content_button = None + # Create view containers + self._create_views() + self._setup_layout() - self.normal_view = Box( - children=self.props, - ) + def _create_views(self): + """Create the normal and collapsed view containers.""" + self.normal_view = Box(children=self.props) self.collapsed_view = Box(children=self.mini_props) + def _setup_layout(self): + """Setup the stack layout for switching between views.""" self.stack = Stack( transition_type="crossfade", transition_duration=150, @@ -125,15 +206,26 @@ class TileSpecial(Box): v_align="start", children=[self.normal_view, self.collapsed_view], ) - self.children = self.stack def mini_view(self): + """ + Switch to mini (collapsed) view. + + Hides the normal view and shows the compact mini view. + Uses negative margins to hide the normal view completely. + """ self.normal_view.set_style("margin:-999px") self.collapsed_view.set_style("margin:0px;") self.stack.set_visible_child(self.collapsed_view) def maxi_view(self): + """ + Switch to maxi (normal) view. + + Hides the mini view and shows the full normal view. + Uses negative margins to hide the collapsed view completely. + """ self.collapsed_view.set_style("margin:-999px") self.normal_view.set_style("margin:0px;") self.stack.set_visible_child(self.normal_view) diff --git "a/styles/\\" "b/styles/\\" deleted file mode 100644 index 1f88b04..0000000 --- "a/styles/\\" +++ /dev/null @@ -1,395 +0,0 @@ -.player { - /* background-color: aqua; */ - min-height: 100px; - min-width: 400px; - box-shadow: - inset 0 100px 100px black, - inset 10px 0px 10px rgba(0, 0, 0, 0.5), - inset -10px 0px 10px rgba(0, 0, 0, 0.5); - border-bottom-right-radius: 20px; - border-bottom-left-radius: 20px; - padding: 20px; - background-position: center; - background-size: cover; - margin: 4px; -} - -.player.vertical { - box-shadow: - inset 100px 0px 150px black, - inset 0px 10px 10px rgba(0, 0, 0, 0.5), - inset 0px -10px 10px rgba(0, 0, 0, 0.5); - border-top-right-radius: 20px; - border-bottom-right-radius: 20px; - min-width: 400px; - min-height: 150px; - margin: 10px; -} - -.player-icon { - font-size: 24px; -} -#artist { - color: var(--primary); -} - -#song { - color: var(--surface); - font-size: 16px; - margin-top: 5px; - font-weight: 900; -} - -#wiggle-bar { - color: var(--primary); -} - -#source { - /* background-color: antiquewhite; */ - margin-top: -10px; -} - -#controls { - margin-bottom: -10px; -} - -#play-next, -#play-previous { - font-size: 24px; -} - -#progress { - /* min-height: 1px; */ - background-color: antiquewhite; -} - -#pause-button { - background-color: var(--surface); - border-radius: 20px; - padding: 8px; -} - -#pause-button.pause-track { - border-radius: 100%; -} - -#pause-label, -#play-label { - color: var(--primary); - font-size: 20px; - padding: 2px; - border-radius: 999px; - min-width: 25px; -} - -#shuffle-button, -#next-button, -#prev-button { - border: none; - padding: 0px; - padding-left: 11px; - padding-right: 11px; -} - -#shuffle-button:hover, -#next-button:hover, -#prev-button:hover { - background-color: var(--surface-bright); -} - -#shuffle { - font-size: 17px; -} - -.player-button { - border-radius: 5px; - /* background-color: var(--secondary); */ - background-color: white; - opacity: 0.5; - min-width: 10px; - min-height: 10px; - padding: 0px; - margin: 0px; - border: none; - margin-left: 5px; -} - -.player-button.active { - opacity: 1; -} - -#player-switch-container.vertical-player { - margin-top: -23px; - padding-bottom: 13px; -} - -#player-switch-container { - margin-top: -18px; - padding-bottom: 13px; -} - -#controls.vertical { - padding-bottom: 10px; - margin-bottom: 0px; -} - -#disable-shuffle { - font-size: 27px; -} - -#progress-slider { - margin-left: -3px; - margin-right: -2px; - margin-top: 8px; - margin-bottom: 8px; - border-radius: 2px; -} - -#progress-other-end { - min-width: 50px; -} - -.player-mini { - min-height: 60px; - min-width: 220px; - padding-right: 20px; - /* box-shadow: inset 0 0 100px rgb(148, 148, 148), - inset 60px 0px 10px rgba(255, 255, 255, 1); */ - - box-shadow: - inset 0 0 100px rgba(148, 148, 148, 0.2), - inset 70px 0 20px rgba(255, 255, 255, 0.2), - inset 0px 0px 20px 5px rgba(255, 255, 255, 0.2); - border-top-right-radius: 15px; - border-bottom-right-radius: 15px; - border-top-left-radius: 20px; - border-bottom-left-radius: 20px; - background-position: center; - background-size: cover; -} - -#player-container.mini { - min-height: 60px; - border-radius: 15px; - background-position: center; - background-size: cover; -} - -.album-image { - min-width: 65px; - min-height: 60px; - background-position: center; - background-size: cover; - /* border-top-left-radius: 15px; - border-bottom-left-radius: 15px; */ - border-radius: 15px; - /* margin: 2px; */ -} - -.hide-player { - margin: -350px; - transition: margin 0.25s cubic-bezier(0.5, 0.25, 0, 1); - /* animation: hidePlayer 0.5s ease; */ -} - -@keyframes hidePlayer { - 0% { - margin: 0px; - } - 100% { - margin: -999px; - } -} - -.reveal-player { - margin: 0px; - transition: margin 0.25s cubic-bezier(0.5, 0.25, 0, 1); - /* animation: revealPlayer 0.5s ease; */ -} - -#artist { - color: var(--primary); -} - -#song { - color: whitesmoke; - font-size: 16px; - margin-top: 5px; -} -#song.mini { - color: var(--surface); - font-size: 15px; - margin-top: 5px; -} - -#source { - /* background-color: antiquewhite; */ - margin-top: -10px; -} -#source.mini { - margin-left: -20px; -} - -#controls { - margin-bottom: -10px; -} -#play-next, -#play-previous { - text-shadow: none; - font-size: 30px; -} -#play-next.mini, -#play-previous.mini { - text-shadow: none; - font-size: 30px; - color: var(--surface); -} -#play-pause-label { - color: var(--surface); - text-shadow: none; -} - -#progress { - /* min-height: 1px; */ - background-color: antiquewhite; -} - -#pause-button { - /* background-color: var(--surface); */ - background-color: whitesmoke; - border-radius: 16px; - padding: 10px; -} - -.vertical #pause-button { - margin-bottom: 5px; -} - -#pause-button.pause-track { - border-radius: 100%; -} - -#pause-label, -#play-label { - color: var(--shadow); - /* background-color: var(--error-dim); */ - font-size: 20px; - padding: 2px; - border-radius: 999px; - min-width: 25px; -} - -#pause-label.mini, -#play-label.mini { - color: white; - /* background-color: var(--error-dim); */ - font-size: 20px; - padding: 2px; - border-radius: 999px; - min-width: 25px; -} - -#shuffle-button, -#next-button, -#prev-button, -#play-pause-button { - border: none; - padding: 0px; - padding-left: 11px; - padding-right: 11px; -} - -#shuffle-button:hover, -#next-button:hover, -#prev-button:hover, -#play-pause-button:hover { - border-radius: 100%; - /* background-color: white; */ - /* opacity: .3; */ - box-shadow: inset rgba(255, 255, 255, 0.5) 0 0 10px; -} - -.player-button { - border-radius: 5px; - /* background-color: var(--secondary); */ - background-color: white; - opacity: 0.5; - min-width: 10px; - min-height: 10px; - padding: 0px; - margin: 0px; - border: none; - margin-left: 5px; -} - -.player-button.mini { - border-radius: 5px; - background-color: var(--surface); - /* background-color: white; */ - opacity: 0.5; - min-width: 10px; - min-height: 10px; - padding: 0px; - margin: 0px; - border: none; - margin-top: 5px; -} - -.player-button.active { - opacity: 1; - /* min-width: 40px; */ - /* border-radius: 5px; */ -} - -#player-switch-container.vertical-player { - margin-top: -23px; - padding-bottom: 13px; -} - -#player-switch-container { - margin-top: -17px; - padding-bottom: 7px; -} - -#player-switch-container.mini { - margin-top: 0px; - margin-left: -20px; - padding-right: 10px; -} - -#controls.vertical { - padding-bottom: 10px; - margin-bottom: 0px; -} - -.player-icon { - font-size: 20px; -} - -.player-icon.mini { - font-size: 20px; - color: black; - text-shadow: 0 0 5px white; -} - -#shuffle { - font-size: 17px; -} - -#disable-shuffle { - font-size: 27px; -} - -#progress-slider { - background-color: white; - margin-left: -3px; - margin-right: -2px; - margin-top: 8px; - margin-bottom: 8px; - border-radius: 2px; -} - -#progress-other-end { - min-width: 50px; - background-color: white; -} diff --git a/utils/icons.py b/utils/icons.py index 896dcae..ed187ea 100644 --- a/utils/icons.py +++ b/utils/icons.py @@ -76,21 +76,21 @@ bat_full: str = "" # Applets # Applets -wifi: str = chr(0xF1EB) +wifi: str = "" wifi_0: str = "" wifi_1: str = "" wifi_2: str = "" wifi_3: str = "" world: str = "" world_off: str = "" -bluetooth: str = chr(0xF294) +bluetooth: str = "" night: str = "" coffee: str = "" notifications: str = "" notifications_dots: str = "" blur: str = chr(0xF00B5) -arrow_head: str = chr(0xE285) +arrow_head: str = "" wifi_off: str = "" bluetooth_off: str = "" night_off: str = ""