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 = ""