✨ feat(dashboard): implement Bluetooth and WiFi tiles with detailed content views

This commit is contained in:
sankalp committed 2025-07-27 04:24:27 +05:45
1 parent eb35ab53f8
commit 377e5d8733
7 files changed
+810 -488

No files matched your search

+175 -1
View File
@@ -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);
}
}
+200 -1
View File
@@ -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
)
+80 -33
View File
@@ -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
+206 -1
View File
@@ -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="<b>Wi-Fi</b>",
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"<b>{ap.ssid}</b>"
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()
+146 -54
View File
@@ -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)
-395
View File
@@ -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;
}
+3 -3
View File
@@ -76,21 +76,21 @@ bat_full: str = "&#xea38;"
# Applets
# Applets
wifi: str = chr(0xF1EB)
wifi: str = "&#xeb52;"
wifi_0: str = "&#xeba3;"
wifi_1: str = "&#xeba4;"
wifi_2: str = "&#xeba5;"
wifi_3: str = "&#xeb52;"
world: str = "&#xeb54;"
world_off: str = "&#xf1ca;"
bluetooth: str = chr(0xF294)
bluetooth: str = "&#xea37;"
night: str = "&#xeaf8;"
coffee: str = "&#xef0e;"
notifications: str = "&#xea35;"
notifications_dots: str = "&#xede3;"
blur: str = chr(0xF00B5)
arrow_head: str = chr(0xE285)
arrow_head: str = "&#xea61;"
wifi_off: str = "&#xecfa;"
bluetooth_off: str = "&#xeceb;"
night_off: str = "&#xf162;"