✨ feat: add dashboard module with network, bluetooth and player controls
This commit is contained in:
18 files changed
+1946
-849
No files matched your search
+3
-3
@@ -70,7 +70,9 @@ if os.path.exists(CONFIG_FILE):
|
||||
WORKSPACE_DOTS = config.get("workspace_dots", False)
|
||||
WINDOW_SWITCHER_ITEMS_PER_ROW = config.get("window_switcher_items_per_row", 13)
|
||||
DOCK_HIDE_SPECIAL_WORKSPACE = config.get("dock_hide_special_workspace", True)
|
||||
DOCK_HIDE_SPECIAL_WORKSPACE_APPS = config.get("dock_hide_special_workspace_apps", True)
|
||||
DOCK_HIDE_SPECIAL_WORKSPACE_APPS = config.get(
|
||||
"dock_hide_special_workspace_apps", True
|
||||
)
|
||||
|
||||
NOTIF_POS = config.get(NOTIF_POS_KEY, NOTIF_POS_DEFAULT)
|
||||
|
||||
@@ -81,7 +83,6 @@ if os.path.exists(CONFIG_FILE):
|
||||
"date_time": config.get("dock_date_time_visible", True),
|
||||
"controls": config.get("dock_controls_visible", True),
|
||||
"indicators": config.get("dock_indicators_visible", True),
|
||||
"music_player": config.get("dock_music_player_visible", True),
|
||||
"notifications": config.get("dock_notifications_visible", True),
|
||||
"systray": config.get("dock_tray_visible", True),
|
||||
"applications": config.get("dock_applications_visible", True),
|
||||
@@ -115,7 +116,6 @@ else:
|
||||
"date_time": True,
|
||||
"controls": True,
|
||||
"indicators": True,
|
||||
"music_player": True,
|
||||
"notifications": True,
|
||||
"systray": True,
|
||||
"applications": True,
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
from gi.repository import Gtk
|
||||
import os
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
|
||||
ALLOWED_PLAYERS = ["vlc", "cmus", "firefox", "spotify", "chromium"]
|
||||
USERNAME = os.getlogin()
|
||||
HOSTNAME = os.uname().nodename
|
||||
HOME_DIR = os.path.expanduser("~")
|
||||
WALLPAPERS_DIR = os.path.expanduser("~/Pictures/wallpapers/")
|
||||
VERTICAL = False
|
||||
BRIGHTNESS_DEV = "intel_backlight"
|
||||
print(USERNAME, HOSTNAME, HOME_DIR)
|
||||
print(Gtk)
|
||||
@@ -52,9 +52,6 @@ tooltip > * {
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
#workspaces,
|
||||
#workspaces-container,
|
||||
#launcher,
|
||||
@@ -72,3 +69,111 @@ tooltip > * {
|
||||
box-shadow: 0 0 3px alpha(black, 0.7);
|
||||
}
|
||||
|
||||
#dashboard.hide {
|
||||
margin: -999px;
|
||||
transition: margin 0.25s cubic-bezier(0.5, 0.25, 0, 1);
|
||||
}
|
||||
#dashboard.reveal {
|
||||
margin: 0px;
|
||||
transition: margin 0.25s cubic-bezier(0.5, 0.25, 0, 1);
|
||||
}
|
||||
#dashboard {
|
||||
background-color: black;
|
||||
border-radius: 25px;
|
||||
padding: 10px;
|
||||
}
|
||||
#inner {
|
||||
margin: 3px;
|
||||
}
|
||||
|
||||
.tile {
|
||||
margin: 2px;
|
||||
min-height: 60px;
|
||||
min-width: 140px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
.tile.mini {
|
||||
min-height: 46px;
|
||||
min-width: 46px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
.tile.on {
|
||||
background-color: var(--primary);
|
||||
}
|
||||
.tile.off {
|
||||
background-color: var(--surface-bright);
|
||||
}
|
||||
.tile.on .desc-label,
|
||||
.tile.on .tile-label,
|
||||
.tile.on .tile-icon {
|
||||
color: black;
|
||||
transition: all;
|
||||
}
|
||||
.tile.off .desc-label,
|
||||
.tile.off .tile-label,
|
||||
.tile.off .tile-icon {
|
||||
color: var(--foreground);
|
||||
transition: all;
|
||||
}
|
||||
|
||||
.tile-icon {
|
||||
font-size: 16px;
|
||||
margin: 10px;
|
||||
}
|
||||
.tile-icon.mini {
|
||||
font-size: 20px;
|
||||
transition: font-size 0.25s cubic-bezier(0.5, 0.25, 0, 1);
|
||||
}
|
||||
.tile-icon.maxi {
|
||||
font-size: 16px;
|
||||
transition: font-size 0.25s cubic-bezier(0.5, 0.25, 0, 1);
|
||||
}
|
||||
|
||||
.tile-button {
|
||||
all: unset;
|
||||
}
|
||||
|
||||
.tile-menu {
|
||||
all: unset;
|
||||
min-width: 400px;
|
||||
min-height: 300px;
|
||||
margin: -999px;
|
||||
}
|
||||
.tile-menu.expand {
|
||||
margin: 0px;
|
||||
transition: margin 0.15s cubic-bezier(0.5, 0.25, 0, 1);
|
||||
}
|
||||
.tile-menu.contract {
|
||||
margin: -200px;
|
||||
transition: margin 0.25s cubic-bezier(0.5, 0.25, 0, 1);
|
||||
}
|
||||
|
||||
.wavy-circle {
|
||||
color: var(--primary);
|
||||
background-color: var(--surface);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
#notification-source {
|
||||
color: var(--outline);
|
||||
}
|
||||
|
||||
#notificationss-icon {
|
||||
font-size: 36px;
|
||||
margin-right: 27px;
|
||||
margin-left: 7px;
|
||||
color: var(--surface-bright);
|
||||
}
|
||||
|
||||
#notification-container {
|
||||
background-color: var(--surface);
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
#notification-box {
|
||||
background-color: black;
|
||||
border-radius: 25px;
|
||||
padding: 10px;
|
||||
min-width: 364px; /* 290+74 */
|
||||
margin-top: 3px;
|
||||
}
|
||||
@@ -12,6 +12,7 @@ from modules.launcher import Launcher
|
||||
from modules.notification_popup import NotificationPopup
|
||||
from modules.osd import OSD
|
||||
from modules.switcher import ApplicationSwitcher
|
||||
from modules.dashboard.main import DashboardWindow
|
||||
|
||||
|
||||
for log in [
|
||||
@@ -48,6 +49,7 @@ if __name__ == "__main__":
|
||||
osd = OSD()
|
||||
launcher = Launcher()
|
||||
nc = NotificationPopup()
|
||||
dashboard = DashboardWindow()
|
||||
|
||||
# Set corners visibility based on config
|
||||
corners_visible = config.get("corners_visible", True)
|
||||
@@ -61,7 +63,7 @@ if __name__ == "__main__":
|
||||
_ = color_css_file.connect("changed", lambda *_: set_css())
|
||||
|
||||
app = Application(
|
||||
f"{APP_NAME}", dock, corners, switcher, osd, launcher, nc
|
||||
f"{APP_NAME}", dock, corners, switcher, osd, launcher, nc, dashboard
|
||||
) # Make sure corners is added to the app
|
||||
|
||||
def set_css():
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
from fabric.widgets.label import Label
|
||||
|
||||
from modules.dashboard.tile import Tile
|
||||
|
||||
import utils.icons as icons
|
||||
|
||||
|
||||
class Bluetooth(Tile):
|
||||
def __init__(self, **kwargs):
|
||||
self.label = Label(
|
||||
style_classes=["desc-label", "off"],
|
||||
label="Off",
|
||||
h_align="start",
|
||||
)
|
||||
self.state = False
|
||||
|
||||
super().__init__(
|
||||
label="Bluetooth",
|
||||
props=self.label,
|
||||
markup=icons.bluetooth,
|
||||
menu=True,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
if self.state:
|
||||
self.add_style_class("on")
|
||||
else:
|
||||
self.add_style_class("off")
|
||||
@@ -0,0 +1,186 @@
|
||||
from fabric.widgets.box import Box
|
||||
from fabric.widgets.label import Label
|
||||
from fabric.widgets.centerbox import CenterBox
|
||||
from fabric.widgets.button import Button
|
||||
from fabric.widgets.revealer import Revealer
|
||||
|
||||
from modules.dashboard.bluetooth import Bluetooth
|
||||
from modules.dashboard.network import Network
|
||||
from modules.dashboard.tile import Tile, TileSpecial
|
||||
from utils.wavy_clock import WavyCircle
|
||||
from modules.dashboard.player_mini import PlayerContainerMini
|
||||
|
||||
from utils.wayland import WaylandWindow as Window
|
||||
|
||||
import utils.icons as icons
|
||||
import config.info as info
|
||||
|
||||
|
||||
class Dashboard(Box):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(
|
||||
name="dashboard",
|
||||
visible=True,
|
||||
orientation="v",
|
||||
spacing=8,
|
||||
all_visible=True,
|
||||
**kwargs,
|
||||
)
|
||||
self.wifi = Network()
|
||||
self.bluetooth = Bluetooth()
|
||||
self.mini_player = PlayerContainerMini()
|
||||
self.mini_player_tile = TileSpecial(
|
||||
props=self.mini_player, mini_props=self.mini_player.get_mini_view()
|
||||
)
|
||||
|
||||
# self.controls = controls
|
||||
# self.brightness_revealer_mui = self.controls.get_brightness_slider_mui()
|
||||
#
|
||||
self.wavy = self.gtk_wrapper = CenterBox(
|
||||
orientation="v",
|
||||
h_expand=True,
|
||||
v_expand=True,
|
||||
h_align="fill",
|
||||
v_align="fill",
|
||||
center_children=WavyCircle(),
|
||||
)
|
||||
|
||||
self.low_bat_msg = Label(label="Low Battery fam (<15%)")
|
||||
|
||||
self.content_box = Box(
|
||||
orientation="v",
|
||||
children=[
|
||||
Label(name="notification-source", label="Modus", h_align=True),
|
||||
self.low_bat_msg,
|
||||
],
|
||||
)
|
||||
|
||||
close_btn = Button(
|
||||
name="close-button",
|
||||
child=Label(name="close-label", markup=icons.cancel),
|
||||
tooltip_text="Exit",
|
||||
on_clicked=lambda *_: print("clicked close"),
|
||||
)
|
||||
|
||||
self.notification_box = Box(
|
||||
name="notification-box",
|
||||
style_classes="vertical" if info.VERTICAL else "horizontal",
|
||||
children=[
|
||||
Label(name="notificationss-icon", h_align=True, markup=icons.blur),
|
||||
Box(h_expand=True, children=self.content_box),
|
||||
Box(children=close_btn),
|
||||
],
|
||||
)
|
||||
|
||||
self.tiles = Box(
|
||||
children=[
|
||||
self.wifi,
|
||||
self.bluetooth,
|
||||
self.mini_player_tile,
|
||||
],
|
||||
)
|
||||
self.low_bat_msg_2 = Label(label="Notification ctl test")
|
||||
|
||||
self.content_box_2 = Box(
|
||||
orientation="v",
|
||||
children=[
|
||||
Label(name="notification-source", label="System", h_align=True),
|
||||
self.low_bat_msg_2,
|
||||
],
|
||||
)
|
||||
|
||||
close_btn_2 = Button(
|
||||
name="close-button",
|
||||
child=Label(name="close-label", markup=icons.cancel),
|
||||
tooltip_text="Exit",
|
||||
on_clicked=lambda *_: print("clicked close"),
|
||||
)
|
||||
|
||||
self.notification_box_2 = Box(
|
||||
name="notification-box",
|
||||
style_classes="vertical" if info.VERTICAL else "horizontal",
|
||||
children=[
|
||||
Label(name="notificationss-icon", h_align=True, markup=icons.blur),
|
||||
Box(h_expand=True, children=self.content_box_2),
|
||||
Box(children=close_btn_2),
|
||||
],
|
||||
)
|
||||
|
||||
self.notification_container = Revealer(
|
||||
transition_duration=250,
|
||||
transition_type="slide-down",
|
||||
style="background-color:blue;",
|
||||
h_expand=True,
|
||||
child=Box(
|
||||
h_expand=True,
|
||||
v_expand=True,
|
||||
children=[
|
||||
Box(
|
||||
name="notification-container",
|
||||
orientation="v",
|
||||
v_expand=True,
|
||||
style="padding:3px; padding-top:0px",
|
||||
children=[self.notification_box, self.notification_box_2],
|
||||
),
|
||||
Box(
|
||||
v_expand=True,
|
||||
h_expand=True,
|
||||
children=self.wavy,
|
||||
),
|
||||
],
|
||||
),
|
||||
child_revealed=True,
|
||||
)
|
||||
self.children = [
|
||||
# Box(
|
||||
# name="inner",
|
||||
# orientation="h",
|
||||
# children=[
|
||||
# CenterBox(
|
||||
# style_classes="scale-icon-container",
|
||||
# center_children=Label(
|
||||
# style_classes="scale-icon", markup=icons.brightness
|
||||
# ),
|
||||
# ),
|
||||
# self.brightness_revealer_mui,
|
||||
# ],
|
||||
# ),
|
||||
Box(name="inner", children=self.tiles),
|
||||
Box(
|
||||
children=[
|
||||
self.notification_container,
|
||||
]
|
||||
),
|
||||
]
|
||||
|
||||
def handle_tile_menu_expand(self, tile: str, toggle: bool):
|
||||
if toggle:
|
||||
self.notification_container.set_reveal_child(False)
|
||||
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")
|
||||
|
||||
|
||||
class DashboardWindow(Window):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(
|
||||
layer="top",
|
||||
anchor="bottom",
|
||||
child=Dashboard(),
|
||||
visible=True,
|
||||
all_visible=True,
|
||||
)
|
||||
|
||||
self.hide()
|
||||
@@ -0,0 +1,52 @@
|
||||
from fabric.widgets.label import Label
|
||||
|
||||
from modules.dashboard.tile import Tile
|
||||
from services.nett import NetworkService
|
||||
|
||||
import utils.icons as icons
|
||||
|
||||
|
||||
class Network(Tile):
|
||||
def __init__(self, **kwargs):
|
||||
self.label = Label(
|
||||
style_classes=["desc-label", "off"],
|
||||
label="Disconnected",
|
||||
h_align="start",
|
||||
)
|
||||
self.state = False
|
||||
|
||||
super().__init__(
|
||||
label="Wi-Fi",
|
||||
props=self.label,
|
||||
markup=icons.wifi,
|
||||
menu=True,
|
||||
**kwargs,
|
||||
)
|
||||
self.nm = NetworkService()
|
||||
self.nm.connect("connection-change", self.handle_connection_change)
|
||||
self.nm.init_props()
|
||||
|
||||
def handle_connection_change(
|
||||
self, source, con_label: str, connected: bool, status: str
|
||||
):
|
||||
if self.state != connected:
|
||||
self.state = connected
|
||||
if self.state:
|
||||
self.remove_style_class("off")
|
||||
self.add_style_class("on")
|
||||
else:
|
||||
self.remove_style_class("on")
|
||||
self.add_style_class("off")
|
||||
print("State change:", connected)
|
||||
|
||||
if not connected:
|
||||
if status == "Wi-Fi Off":
|
||||
self.label.set_label("Off")
|
||||
elif status == "Wi-Fi On (No Connection)":
|
||||
self.label.set_label("Disconnected")
|
||||
elif status == "Connecting…":
|
||||
self.label.set_label("Connecting…")
|
||||
else:
|
||||
self.label.set_label("Unknown")
|
||||
else:
|
||||
self.label.set_label(con_label or "Connected")
|
||||
@@ -0,0 +1,421 @@
|
||||
from gi.repository import Gdk
|
||||
from fabric.widgets.box import Box
|
||||
from fabric.widgets.label import Label
|
||||
from fabric.widgets.centerbox import CenterBox
|
||||
from fabric.widgets.button import Button
|
||||
from fabric.widgets.stack import Stack
|
||||
from fabric.widgets.eventbox import EventBox
|
||||
|
||||
from services.mpris import PlayerManager, PlayerService
|
||||
from utils.wiggle_bar import WigglyWidget
|
||||
|
||||
import utils.icons as icons
|
||||
import config.data as data
|
||||
|
||||
from loguru import logger
|
||||
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
|
||||
|
||||
class PlayerMini(Box):
|
||||
def __init__(self, player, **kwargs):
|
||||
super().__init__(style_classes="player-mini", **kwargs)
|
||||
|
||||
if not data.VERTICAL:
|
||||
self.remove_style_class(
|
||||
"vertical"
|
||||
) # vertical class binding from unknown source
|
||||
|
||||
self._player = PlayerService(player=player)
|
||||
|
||||
self.duration = 0.0
|
||||
|
||||
self._player.connect("pause", self.on_pause)
|
||||
self._player.connect("play", self.on_play)
|
||||
self._player.connect("meta-change", self.on_metadata)
|
||||
self._player.connect("shuffle-toggle", self.on_shuffle)
|
||||
self._player.connect("track-position", self.on_update_track_position)
|
||||
|
||||
self.player_name = Label(
|
||||
name=player.props.player_name,
|
||||
style_classes=["player-icon", "mini"],
|
||||
markup=getattr(icons, player.props.player_name, icons.disc),
|
||||
)
|
||||
|
||||
self.set_style(f"background-image:url('{data.HOME_DIR}/.current.wall')")
|
||||
|
||||
self.song = Label(
|
||||
name="song",
|
||||
label="song",
|
||||
style_classes="mini",
|
||||
justification="left",
|
||||
h_align="start",
|
||||
ellipsization="end",
|
||||
max_chars_width=15,
|
||||
)
|
||||
self.artist = Label(
|
||||
name="artist",
|
||||
label="artist",
|
||||
style_classes="mini",
|
||||
justification="left",
|
||||
h_align="start",
|
||||
)
|
||||
self.music = Box(
|
||||
name="music",
|
||||
h_expand=True,
|
||||
v_expand=True,
|
||||
children=[self.song],
|
||||
)
|
||||
|
||||
self.play_pause_button = Button(
|
||||
name="play-pause-button",
|
||||
child=Label(name="play-pause-label", markup=icons.play),
|
||||
style_classes="mini",
|
||||
tooltip_text="Play/Pause",
|
||||
on_clicked=lambda b, *_: self.handle_play_pause(player),
|
||||
)
|
||||
|
||||
self.shuffle_button = Button(
|
||||
name="shuffle-button",
|
||||
child=Label(name="shuffle", markup=icons.shuffle),
|
||||
on_clicked=lambda b, *_: self.handle_shuffle(b, player),
|
||||
)
|
||||
|
||||
self.wiggly = WigglyWidget()
|
||||
self.wiggly.connect("on-seek", self.on_seek)
|
||||
self.wiggly_bar = Box(
|
||||
orientation="v",
|
||||
h_expand=True,
|
||||
v_expand=True,
|
||||
h_align="fill",
|
||||
v_align="fill",
|
||||
children=self.wiggly,
|
||||
)
|
||||
|
||||
self.album_cover = Box(style_classes="album-image")
|
||||
self.album_cover.set_style(
|
||||
f"background-image:url('{data.HOME_DIR}/.current_wall')"
|
||||
)
|
||||
|
||||
self.children = [
|
||||
self.album_cover,
|
||||
Box(
|
||||
name="source",
|
||||
style_classes="mini",
|
||||
v_align="end",
|
||||
children=self.player_name,
|
||||
),
|
||||
# CenterBox(
|
||||
# name="progress-container",
|
||||
# h_expand=True,
|
||||
# v_expand=True,
|
||||
# orientation="v",
|
||||
# center_children=[self.wiggly_bar],
|
||||
# ),
|
||||
Box(
|
||||
orientation="v",
|
||||
v_expand=True,
|
||||
h_expand=True,
|
||||
style="padding-left:10px;",
|
||||
children=[
|
||||
CenterBox(
|
||||
name="details",
|
||||
center_children=self.music,
|
||||
),
|
||||
Box(
|
||||
name="controls",
|
||||
style_classes="horizontal" if not data.VERTICAL else "vertical",
|
||||
spacing=5,
|
||||
h_expand=True,
|
||||
children=[
|
||||
CenterBox(
|
||||
h_expand=True,
|
||||
v_expand=True,
|
||||
center_children=[
|
||||
Button(
|
||||
name="prev-button",
|
||||
child=Label(
|
||||
name="play-previous",
|
||||
style_classes="mini",
|
||||
markup=icons.previous_fill,
|
||||
),
|
||||
on_clicked=lambda b, *_: self.handle_prev(
|
||||
player
|
||||
),
|
||||
),
|
||||
self.play_pause_button,
|
||||
Button(
|
||||
name="next-button",
|
||||
child=Label(
|
||||
name="play-next",
|
||||
style_classes="mini",
|
||||
markup=icons.next_fill,
|
||||
),
|
||||
on_clicked=lambda b, *_: self.handle_next(
|
||||
player
|
||||
),
|
||||
),
|
||||
# self.shuffle_button,
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
||||
self.on_metadata(self._player, metadata=player.props.metadata, player=player)
|
||||
|
||||
def on_update_track_position(self, sender, pos, dur):
|
||||
if dur == 0:
|
||||
return
|
||||
self.duration = dur
|
||||
self.wiggly.update_value_from_signal(pos / dur)
|
||||
|
||||
def on_seek(self, sender, ratio):
|
||||
pos = ratio * self.duration # duration in seconds
|
||||
print(f"Seeking to {pos:.2f}s")
|
||||
self._player.set_position(int(pos))
|
||||
|
||||
def skip_forward(self, seconds=10):
|
||||
self._player._player.seek(seconds * 1000000)
|
||||
|
||||
def skip_backward(self, seconds=10):
|
||||
self._player._player.seek(-1 * seconds * 1000000)
|
||||
|
||||
def on_metadata(self, sender, metadata, player):
|
||||
keys = metadata.keys()
|
||||
if "xesam:artist" in keys and "xesam:title" in keys:
|
||||
# _max_chars = 20 if not info.VERTICAL else 30
|
||||
song_title = metadata["xesam:title"]
|
||||
# if len(song_title) > _max_chars:
|
||||
# song_title = song_title[: _max_chars - 1] + "…"
|
||||
self.song.set_label(song_title)
|
||||
|
||||
artist_list = metadata["xesam:artist"]
|
||||
artist_name = artist_list[0] if artist_list else "Unknown Artist"
|
||||
# if len(artist_name) > _max_chars:
|
||||
# artist_name = artist_name[: _max_chars - 1] + "…"
|
||||
self.artist.set_label(artist_name)
|
||||
if "mpris:artUrl" in keys:
|
||||
self.set_style(f"background-image:url('{metadata['mpris:artUrl']}')")
|
||||
self.album_cover.set_style(
|
||||
f"background-image:url('{metadata['mpris:artUrl']}')"
|
||||
)
|
||||
|
||||
if (
|
||||
player.props.playback_status.value_name
|
||||
== "PLAYERCTL_PLAYBACK_STATUS_PLAYING"
|
||||
):
|
||||
self.on_play(self._player)
|
||||
|
||||
if player.props.shuffle == True:
|
||||
self.shuffle_button.get_child().set_markup(icons.disable_shuffle)
|
||||
self.shuffle_button.get_child().set_name("disable-shuffle")
|
||||
else:
|
||||
self.shuffle_button.get_child().set_markup(icons.shuffle)
|
||||
self.shuffle_button.get_child().set_name("shuffle")
|
||||
|
||||
def on_pause(self, sender):
|
||||
self.play_pause_button.get_child().set_markup(icons.play)
|
||||
self.wiggly.dragging = True
|
||||
self.wiggly.update_amplitude(True)
|
||||
self.wiggly.pause = True
|
||||
# self.play_pause_button.add_style_class("pause-track")
|
||||
|
||||
def on_play(self, sender):
|
||||
self.play_pause_button.get_child().set_markup(icons.pause)
|
||||
self.wiggly.pause = False
|
||||
self.wiggly.dragging = False
|
||||
self.wiggly.update_amplitude(False)
|
||||
# self.play_pause_button.remove_style_class("pause-track")
|
||||
|
||||
def on_shuffle(self, sender, player, status):
|
||||
print("callback status", status)
|
||||
if status == False:
|
||||
self.shuffle_button.get_child().set_markup(icons.shuffle)
|
||||
self.shuffle_button.get_child().set_name("shuffle")
|
||||
else:
|
||||
self.shuffle_button.get_child().set_markup(icons.disable_shuffle)
|
||||
self.shuffle_button.get_child().set_name("disable-shuffle")
|
||||
|
||||
self.shuffle_button.get_child().set_style("color: white")
|
||||
|
||||
def handle_next(self, player):
|
||||
self._player._player.next()
|
||||
|
||||
def handle_prev(self, player):
|
||||
self._player._player.previous()
|
||||
|
||||
def handle_play_pause(self, player):
|
||||
is_playing = (
|
||||
self._player._player.props.playback_status.value_name
|
||||
== "PLAYERCTL_PLAYBACK_STATUS_PLAYING"
|
||||
)
|
||||
|
||||
def _set_play_ui():
|
||||
self.play_pause_button.get_child().set_markup(icons.pause)
|
||||
# self.play_pause_button.remove_style_class("pause-track")
|
||||
# self.play_pause_button.get_child().set_name("pause-label")
|
||||
|
||||
def _set_pause_ui():
|
||||
self.play_pause_button.get_child().set_markup(icons.play)
|
||||
# self.play_pause_button.add_style_class("pause-track")
|
||||
# self.play_pause_button.get_child().set_name("play-label")
|
||||
|
||||
if is_playing:
|
||||
_set_pause_ui()
|
||||
else:
|
||||
_set_play_ui()
|
||||
|
||||
try:
|
||||
self._player._player.play_pause()
|
||||
except Exception as e:
|
||||
# revert if signal failed
|
||||
if is_playing:
|
||||
_set_pause_ui()
|
||||
else:
|
||||
_set_play_ui()
|
||||
logger.warning("Failed to toggle playback:", e)
|
||||
|
||||
def handle_shuffle(self, shuffle_button, player):
|
||||
print("shuffle", player.props.shuffle)
|
||||
if player.props.shuffle == False:
|
||||
player.set_shuffle(True)
|
||||
print("setting to true", player.props.player_name)
|
||||
else:
|
||||
player.set_shuffle(False)
|
||||
shuffle_button.get_child().set_style("color: var(--outline)")
|
||||
|
||||
|
||||
class PlayerContainerMini(Box):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(name="player-container", style_classes="mini", **kwargs)
|
||||
|
||||
self.manager = PlayerManager()
|
||||
self.manager.connect("new-player", self.new_player)
|
||||
self.manager.connect("player-vanish", self.on_player_vanish)
|
||||
self.stack = Stack(
|
||||
# name="player-container",
|
||||
transition_type="crossfade",
|
||||
transition_duration=100,
|
||||
children=[],
|
||||
)
|
||||
|
||||
self.player_stack = Stack(
|
||||
name="player-stack",
|
||||
transition_type="crossfade",
|
||||
transition_duration=100,
|
||||
children=[],
|
||||
)
|
||||
|
||||
self.player_switch_container = CenterBox(
|
||||
name="player-switch-container",
|
||||
orientation="v",
|
||||
style_classes=(
|
||||
["horizontal-player", "mini"]
|
||||
if not data.VERTICAL
|
||||
else "vertical-player"
|
||||
),
|
||||
center_children=[],
|
||||
)
|
||||
self.mini_tile_icon = Label(
|
||||
name="disc",
|
||||
style_classes=["tile-icon"],
|
||||
style="font-size:30px; margin:0px; padding:0px;",
|
||||
markup=icons.disc,
|
||||
justification="center",
|
||||
)
|
||||
|
||||
self.mini_tile_view = CenterBox(
|
||||
style_classes=["tile", "mini", "on"],
|
||||
v_expand=False,
|
||||
center_children=self.mini_tile_icon,
|
||||
)
|
||||
self.event_box = EventBox(
|
||||
events=["scroll"],
|
||||
child=Box(children=[self.stack, self.player_switch_container]),
|
||||
)
|
||||
self.children = self.event_box
|
||||
self.players = []
|
||||
self.manager.init_all_players()
|
||||
self.event_box.connect("scroll-event", self.on_scroll)
|
||||
self.stack.connect("notify::visible-child", self.on_visible_child_changed)
|
||||
|
||||
def new_player(self, manager, player):
|
||||
print(player.props.player_name, "new player")
|
||||
print(player)
|
||||
new_player = PlayerMini(player=player)
|
||||
new_player.wiggly_bar.queue_draw()
|
||||
new_player.set_name(player.props.player_name)
|
||||
self.players.append(new_player)
|
||||
print("stacking dis bitvch")
|
||||
self.stack.add_named(new_player, player.props.player_name)
|
||||
|
||||
self.player_switch_container.add_center(
|
||||
Button(
|
||||
name=player.props.player_name,
|
||||
style_classes=["player-button", "mini"],
|
||||
on_clicked=lambda b: self.switch_player(player.props.player_name, b),
|
||||
)
|
||||
)
|
||||
self.update_player_list()
|
||||
|
||||
def switch_player(self, player_name, button):
|
||||
self.stack.set_visible_child_name(player_name)
|
||||
|
||||
for btn in self.player_switch_container.center_children:
|
||||
btn.remove_style_class("active")
|
||||
button.add_style_class("active")
|
||||
|
||||
def on_player_vanish(self, manager, player):
|
||||
for player_instance in self.players:
|
||||
if player_instance.get_name() == player.props.player_name:
|
||||
self.stack.remove(player_instance)
|
||||
self.players.remove(player_instance)
|
||||
for btn in self.player_switch_container.center_children:
|
||||
if btn.get_name() == player_instance.get_name():
|
||||
self.player_switch_container.remove_center(btn)
|
||||
self.update_player_list()
|
||||
break
|
||||
|
||||
def update_player_list(self):
|
||||
curr = self.stack.get_visible_child()
|
||||
for btn in self.player_switch_container.center_children:
|
||||
print(btn.get_name())
|
||||
if btn.get_name() == curr.get_name():
|
||||
btn.add_style_class("active")
|
||||
else:
|
||||
btn.remove_style_class("active")
|
||||
|
||||
def on_scroll(self, widget, event):
|
||||
match event.direction:
|
||||
case 0:
|
||||
self.switch_relative_player(forward=False)
|
||||
case 1:
|
||||
self.switch_relative_player(forward=True)
|
||||
|
||||
def switch_relative_player(self, forward=True):
|
||||
if not self.players:
|
||||
return
|
||||
|
||||
current_player = self.stack.get_visible_child()
|
||||
current_index = self.players.index(current_player)
|
||||
|
||||
next_index = (current_index + (1 if forward else -1)) % len(self.players)
|
||||
next_player = self.players[next_index]
|
||||
|
||||
for btn in self.player_switch_container.center_children:
|
||||
if btn.get_name() == next_player.get_name():
|
||||
self.switch_player(next_player.get_name(), btn)
|
||||
break
|
||||
|
||||
def on_visible_child_changed(self, *args):
|
||||
curr_player = self.stack.get_visible_child().get_name()
|
||||
self.mini_tile_icon.set_name(curr_player)
|
||||
self.mini_tile_icon.set_markup(getattr(icons, curr_player, icons.disc))
|
||||
|
||||
def get_mini_view(self):
|
||||
return self.mini_tile_view
|
||||
@@ -0,0 +1,141 @@
|
||||
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
|
||||
from fabric.utils.helpers import exec_shell_command_async
|
||||
|
||||
|
||||
class Tile(Box):
|
||||
def __init__(self, *, menu: bool, markup: str, label: str, props: Label, **kwargs):
|
||||
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.type_box = Box(
|
||||
style_classes="tile-type",
|
||||
orientation="v",
|
||||
v_expand=True,
|
||||
h_expand=True,
|
||||
v_align="center",
|
||||
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",
|
||||
h_expand=True,
|
||||
child=Box(children=[self.type_box, self.menu_button]),
|
||||
child_revealed=True,
|
||||
)
|
||||
else:
|
||||
self.content_button = Revealer(
|
||||
transition_duration=150,
|
||||
transition_type="slide-left",
|
||||
child=Box(children=[self.type_box]),
|
||||
child_revealed=True,
|
||||
)
|
||||
|
||||
self.normal_view = Box(
|
||||
children=[self.icon, self.content_button],
|
||||
)
|
||||
|
||||
self.menu = Button(
|
||||
style_classes="tile-menu",
|
||||
child=Label(label="hi!"),
|
||||
on_clicked=self.handle_click,
|
||||
)
|
||||
|
||||
self.stack = Stack(
|
||||
transition_type="crossfade",
|
||||
transition_duration=150,
|
||||
h_expand=True,
|
||||
children=[self.normal_view, self.menu],
|
||||
)
|
||||
|
||||
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())
|
||||
name = self.get_name()
|
||||
exec_shell_command_async(
|
||||
f"fabric-cli exec bar-example \"notch.dashboard.handle_tile_menu_expand('{
|
||||
name
|
||||
}', {self.toggle})\""
|
||||
)
|
||||
|
||||
def mini_view(self):
|
||||
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):
|
||||
self.content_button.set_reveal_child(True)
|
||||
self.icon.set_h_expand(False)
|
||||
self.content_button.set_h_expand(True)
|
||||
self.remove_style_class("mini")
|
||||
|
||||
|
||||
class TileSpecial(Box):
|
||||
def __init__(self, *, mini_props, props, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.props = props
|
||||
self.mini_props = mini_props
|
||||
self.toggle = False
|
||||
|
||||
self.content_button = None
|
||||
|
||||
self.normal_view = Box(
|
||||
children=self.props,
|
||||
)
|
||||
self.collapsed_view = Box(children=self.mini_props)
|
||||
|
||||
self.stack = Stack(
|
||||
transition_type="crossfade",
|
||||
transition_duration=150,
|
||||
h_expand=True,
|
||||
v_align="start",
|
||||
children=[self.normal_view, self.collapsed_view],
|
||||
)
|
||||
|
||||
self.children = self.stack
|
||||
|
||||
def mini_view(self):
|
||||
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):
|
||||
self.collapsed_view.set_style("margin:-999px")
|
||||
self.normal_view.set_style("margin:0px;")
|
||||
self.stack.set_visible_child(self.normal_view)
|
||||
@@ -20,7 +20,6 @@ from .battery import Battery
|
||||
from .controls import Controls
|
||||
from .indicators import Indicators
|
||||
from .metrics import Metrics
|
||||
from .music_player import MusicPlayer
|
||||
from .workspaces import workspace
|
||||
from .systemtray import SystemTray
|
||||
|
||||
@@ -66,7 +65,6 @@ class DockComponents(Box):
|
||||
)
|
||||
self.controls = Controls()
|
||||
self.indicators = Indicators()
|
||||
self.music_player = MusicPlayer()
|
||||
self.systray = SystemTray()
|
||||
|
||||
# Create list of components to add
|
||||
@@ -74,7 +72,6 @@ class DockComponents(Box):
|
||||
self.workspaces,
|
||||
self.metrics,
|
||||
self.controls,
|
||||
self.music_player,
|
||||
self.applications,
|
||||
self.indicators,
|
||||
self.battery,
|
||||
@@ -109,7 +106,6 @@ class DockComponents(Box):
|
||||
"date_time": self.date_time,
|
||||
"controls": self.controls,
|
||||
"indicators": self.indicators,
|
||||
"music_player": self.music_player,
|
||||
"systray": self.systray,
|
||||
"applications": self.applications,
|
||||
"language": self.language,
|
||||
@@ -130,11 +126,6 @@ class DockComponents(Box):
|
||||
# Battery component manages its own visibility based on config and battery presence
|
||||
# Don't override its visibility here, let it handle it internally
|
||||
pass
|
||||
# Special handling for music player - let it manage its own visibility based on media content
|
||||
elif component_name == "music_player":
|
||||
# Music player component manages its own visibility based on config and media presence
|
||||
# Don't override its visibility here, let it handle it internally
|
||||
pass
|
||||
else:
|
||||
widget.set_visible(visibility)
|
||||
|
||||
@@ -146,7 +137,6 @@ class DockComponents(Box):
|
||||
"date_time": self.date_time,
|
||||
"controls": self.controls,
|
||||
"indicators": self.indicators,
|
||||
"music_player": self.music_player,
|
||||
"systray": self.systray,
|
||||
"applications": self.applications,
|
||||
"language": self.language,
|
||||
|
||||
@@ -39,8 +39,6 @@ class Battery(Box):
|
||||
child=self.circle,
|
||||
)
|
||||
|
||||
|
||||
|
||||
# Create percentage label
|
||||
self.level = Label(
|
||||
name="battery-level",
|
||||
@@ -76,8 +74,6 @@ class Battery(Box):
|
||||
# Initial update
|
||||
GLib.idle_add(self.update_battery)
|
||||
|
||||
|
||||
|
||||
def on_query_tooltip(self, _widget, _x, _y, _keyboard_mode, tooltip):
|
||||
"""Handle tooltip query"""
|
||||
tooltip.set_markup(self.get_tooltip_text())
|
||||
@@ -161,7 +157,6 @@ class Battery(Box):
|
||||
else:
|
||||
self.level.set_visible(False)
|
||||
|
||||
|
||||
# Apply alert styling if battery is low AND not charging
|
||||
if percentage <= 15 and not charging:
|
||||
self.icon.add_style_class("alert")
|
||||
|
||||
@@ -1,826 +0,0 @@
|
||||
import time
|
||||
|
||||
from fabric.widgets.box import Box
|
||||
from fabric.widgets.button import Button
|
||||
from fabric.widgets.circularprogressbar import CircularProgressBar
|
||||
|
||||
from fabric.widgets.label import Label
|
||||
from fabric.widgets.overlay import Overlay
|
||||
from fabric.widgets.revealer import Revealer
|
||||
from gi.repository import Gdk, GLib, Playerctl
|
||||
|
||||
import config.data as data
|
||||
import utils.icons as icons
|
||||
from modules.dock.components.player import PlayerContainer
|
||||
from services.mpris import PlayerManager, PlayerService
|
||||
from utils.wayland import WaylandWindow as Window
|
||||
|
||||
|
||||
class MusicPlayerPopup(Window):
|
||||
"""Popup window that shows the full music player interface"""
|
||||
|
||||
def __init__(self, dock_component=None, **kwargs):
|
||||
# Get dock position for proper anchoring
|
||||
dock_position = data.DOCK_POSITION
|
||||
popup_anchor = self._get_popup_anchor(dock_position)
|
||||
margin = self._get_popup_margin(dock_position)
|
||||
|
||||
super().__init__(
|
||||
name="music-player-popup",
|
||||
layer="top",
|
||||
anchor=popup_anchor,
|
||||
margin=margin,
|
||||
exclusive=False,
|
||||
keyboard_mode="on-demand",
|
||||
visible=False,
|
||||
all_visible=False,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
self.dock_component = dock_component
|
||||
|
||||
# Create player container first
|
||||
self.player_container = PlayerContainer()
|
||||
|
||||
# Get transition type based on dock position
|
||||
transition_type = self._get_revealer_transition(dock_position)
|
||||
|
||||
# Create revealer with position-appropriate transition
|
||||
self.revealer = Revealer(
|
||||
transition_type=transition_type,
|
||||
transition_duration=300,
|
||||
child=self.player_container,
|
||||
reveal_child=False,
|
||||
)
|
||||
|
||||
self.add(self.revealer)
|
||||
|
||||
# Connect player container events to sync with dock
|
||||
if self.dock_component:
|
||||
self.player_container.connect(
|
||||
"active-player-changed", self.on_active_player_changed
|
||||
)
|
||||
|
||||
# Connect to escape key to close
|
||||
self.connect("key-press-event", self.on_key_press)
|
||||
self.connect("button-press-event", self.on_button_press)
|
||||
self.set_can_focus(True)
|
||||
|
||||
def on_active_player_changed(self, container, player):
|
||||
"""Handle active player change from player container"""
|
||||
if self.dock_component and player:
|
||||
self.dock_component.set_current_player(player)
|
||||
|
||||
def on_key_press(self, widget, event):
|
||||
"""Handle key press events"""
|
||||
if event.keyval == Gdk.KEY_Escape:
|
||||
if self.dock_component:
|
||||
self.dock_component._popup_visible = False
|
||||
self.hide_popup()
|
||||
return True
|
||||
return False
|
||||
|
||||
def show_popup(self):
|
||||
"""Show the music player popup"""
|
||||
self.set_visible(True)
|
||||
self.show_all()
|
||||
# Use GLib.idle_add to ensure the window is shown before revealing
|
||||
GLib.idle_add(self._reveal_popup)
|
||||
|
||||
def _reveal_popup(self):
|
||||
"""Reveal the popup after window is shown"""
|
||||
self.revealer.set_reveal_child(True)
|
||||
self.grab_focus()
|
||||
return False # Don't repeat
|
||||
|
||||
def _get_popup_anchor(self, dock_position):
|
||||
"""Get popup anchor based on dock position"""
|
||||
anchor_map = {
|
||||
"Top": "top",
|
||||
"Bottom": "bottom",
|
||||
"Left": "top left",
|
||||
"Right": "top right",
|
||||
}
|
||||
return anchor_map.get(dock_position, "bottom")
|
||||
|
||||
def _get_popup_margin(self, dock_position):
|
||||
"""Get popup margin based on dock position"""
|
||||
margin_map = {
|
||||
"Top": "60px 10px 10px 10px",
|
||||
"Bottom": "10px 10px 60px 10px",
|
||||
"Left": "330px 10px 10px 60px",
|
||||
"Right": "330px 60px 10px 10px",
|
||||
}
|
||||
return margin_map.get(dock_position, "10px 10px 60px 10px")
|
||||
|
||||
def _get_revealer_transition(self, dock_position):
|
||||
"""Get revealer transition type based on dock position"""
|
||||
transition_map = {
|
||||
"Top": "slide-down",
|
||||
"Bottom": "slide-up",
|
||||
"Left": "slide-right",
|
||||
"Right": "slide-left",
|
||||
}
|
||||
return transition_map.get(dock_position, "slide-up")
|
||||
|
||||
def on_button_press(self, widget, event):
|
||||
"""Handle button press events - close popup when clicking outside"""
|
||||
if self.dock_component:
|
||||
self.dock_component._popup_visible = False
|
||||
self.hide_popup()
|
||||
return False
|
||||
|
||||
def hide_popup(self):
|
||||
"""Hide the music player popup"""
|
||||
self.revealer.set_reveal_child(False)
|
||||
# Hide window after transition completes and notify dock component
|
||||
GLib.timeout_add(350, self._complete_hide)
|
||||
|
||||
def _complete_hide(self):
|
||||
"""Complete the hide process"""
|
||||
self.set_visible(False)
|
||||
if self.dock_component:
|
||||
self.dock_component._popup_visible = False
|
||||
return False
|
||||
|
||||
|
||||
class MusicPlayer(Box):
|
||||
"""Music player component for the dock with circular progress bar"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(name="music-player", **kwargs)
|
||||
|
||||
# Initialize player manager
|
||||
self.manager = PlayerManager()
|
||||
self.manager.connect("new-player", self.on_new_player)
|
||||
self.manager.connect("player-vanish", self.on_player_vanish)
|
||||
|
||||
# Current player tracking
|
||||
self.current_player = None
|
||||
self.current_player_service = None
|
||||
self.duration = 0.0
|
||||
self.position = 0.0
|
||||
self._current_track_info = "Music Player"
|
||||
|
||||
# Debouncing for player switching to prevent rapid changes
|
||||
self._last_player_switch = 0
|
||||
self._switch_debounce_time = 1.0 # 1 second minimum between switches
|
||||
|
||||
# Visibility debouncing to prevent immediate hiding when player closes
|
||||
self._visibility_timeout = None
|
||||
self._hide_delay = 2.0 # 2 seconds delay before hiding when no players
|
||||
|
||||
# Create circular progress bar
|
||||
self.progress_bar = CircularProgressBar(
|
||||
name="player-circle",
|
||||
size=28,
|
||||
line_width=2,
|
||||
start_angle=150,
|
||||
end_angle=390,
|
||||
value=0.0, # Initialize with 0 progress
|
||||
)
|
||||
|
||||
# Create music icon label - always use generic music icon
|
||||
self.music_label = Label(name="music-label", markup=icons.music)
|
||||
|
||||
# Create button for the music icon with click handler
|
||||
self.music_button = Button(child=self.music_label)
|
||||
self.music_button.connect("clicked", self.on_music_button_clicked)
|
||||
|
||||
# Create overlay without EventBox to fix click issues
|
||||
self.overlay = Overlay(child=self.progress_bar, overlays=self.music_button)
|
||||
|
||||
# Add scroll events directly to the overlay for seeking
|
||||
self.overlay.add_events(Gdk.EventMask.SCROLL_MASK | Gdk.EventMask.SMOOTH_SCROLL_MASK)
|
||||
self.overlay.connect("scroll-event", self.on_scroll)
|
||||
|
||||
self.add(self.overlay)
|
||||
|
||||
# Pre-create popup for immediate display on click
|
||||
self.popup = MusicPlayerPopup(dock_component=self)
|
||||
|
||||
# Track popup visibility state
|
||||
self._popup_visible = False
|
||||
|
||||
# Initialize players with delayed visibility check
|
||||
GLib.timeout_add(500, self._delayed_init)
|
||||
|
||||
# Hide initially - will show only when media players are detected
|
||||
self.hide()
|
||||
|
||||
# Set up periodic check for active player
|
||||
self.active_player_check_timeout = GLib.timeout_add_seconds(
|
||||
2, self.check_active_player
|
||||
)
|
||||
|
||||
# Add recursion protection and debouncing
|
||||
self._in_player_switch = False
|
||||
self._last_player_switch_time = 0
|
||||
self._switch_debounce_ms = 500 # 500ms debounce
|
||||
|
||||
# Connect to destroy signal for cleanup
|
||||
self.connect("destroy", self.on_destroy)
|
||||
|
||||
def on_destroy(self, widget):
|
||||
"""Clean up popup when component is destroyed"""
|
||||
try:
|
||||
# Remove timeout
|
||||
if hasattr(self, "active_player_check_timeout"):
|
||||
GLib.source_remove(self.active_player_check_timeout)
|
||||
|
||||
# No hover timeout to clean up anymore
|
||||
|
||||
# Remove visibility timeout
|
||||
if hasattr(self, "_visibility_timeout") and self._visibility_timeout:
|
||||
GLib.source_remove(self._visibility_timeout)
|
||||
self._visibility_timeout = None
|
||||
|
||||
# Clean up current player service
|
||||
if hasattr(self, 'current_player_service') and self.current_player_service:
|
||||
try:
|
||||
if hasattr(self.current_player_service, 'cleanup'):
|
||||
self.current_player_service.cleanup()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Clean up popup
|
||||
if hasattr(self, 'popup') and self.popup:
|
||||
try:
|
||||
self.popup.destroy()
|
||||
self.popup = None
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def on_new_player(self, manager, player):
|
||||
"""Handle new player detection"""
|
||||
# If this is our first player, make it current
|
||||
if not self.current_player:
|
||||
self.set_current_player(player)
|
||||
else:
|
||||
# If the new player is playing, switch to it
|
||||
if (
|
||||
player.props.playback_status.value_name
|
||||
== "PLAYERCTL_PLAYBACK_STATUS_PLAYING"
|
||||
and self.current_player.props.playback_status.value_name
|
||||
!= "PLAYERCTL_PLAYBACK_STATUS_PLAYING"
|
||||
):
|
||||
self.set_current_player(player)
|
||||
|
||||
self.update_visibility()
|
||||
|
||||
def on_player_vanish(self, manager, player):
|
||||
"""Handle player disappearing"""
|
||||
|
||||
# If the current player vanished, find another one
|
||||
if (
|
||||
self.current_player
|
||||
and self.current_player.props.player_name == player.props.player_name
|
||||
):
|
||||
self.current_player = None
|
||||
self.current_player_service = None
|
||||
|
||||
# Try to find another player with media content
|
||||
if hasattr(self.manager, "_manager") and hasattr(
|
||||
self.manager._manager, "props"
|
||||
):
|
||||
for player_name in self.manager._manager.props.player_names:
|
||||
try:
|
||||
candidate_player = Playerctl.Player.new_from_name(player_name)
|
||||
|
||||
# Check if this player has media content
|
||||
if hasattr(candidate_player, "props") and hasattr(
|
||||
candidate_player.props, "metadata"
|
||||
):
|
||||
metadata = candidate_player.props.metadata
|
||||
if metadata and len(metadata.keys()) > 0:
|
||||
keys = metadata.keys()
|
||||
if (
|
||||
"xesam:title" in keys
|
||||
or "xesam:artist" in keys
|
||||
or "mpris:length" in keys
|
||||
):
|
||||
pass
|
||||
self.set_current_player(candidate_player)
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
self.update_visibility()
|
||||
|
||||
def get_playing_player(self):
|
||||
"""Find a currently playing player from the manager"""
|
||||
try:
|
||||
if not hasattr(self.manager, "_manager") or not hasattr(self.manager._manager, "props"):
|
||||
return None
|
||||
|
||||
player_names = self.manager._manager.props.player_names
|
||||
if not player_names:
|
||||
return None
|
||||
|
||||
for player_name in player_names:
|
||||
try:
|
||||
# Validate player_name before creating player
|
||||
if not player_name or not hasattr(player_name, 'name'):
|
||||
continue
|
||||
|
||||
player = Playerctl.Player.new_from_name(player_name)
|
||||
|
||||
# Comprehensive validation of player object
|
||||
if not player:
|
||||
continue
|
||||
|
||||
if not hasattr(player, "props"):
|
||||
continue
|
||||
|
||||
if not hasattr(player.props, "playback_status"):
|
||||
continue
|
||||
|
||||
if not hasattr(player.props, "player_name"):
|
||||
continue
|
||||
|
||||
# Check if player is actually playing
|
||||
try:
|
||||
status = player.props.playback_status
|
||||
if status and hasattr(status, 'value_name'):
|
||||
if status.value_name == "PLAYERCTL_PLAYBACK_STATUS_PLAYING":
|
||||
return player
|
||||
except Exception as e:
|
||||
continue
|
||||
|
||||
except Exception as e:
|
||||
continue
|
||||
|
||||
except Exception as e:
|
||||
print(f"[DEBUG] Error in get_playing_player: {e}")
|
||||
return None
|
||||
|
||||
def check_active_player(self):
|
||||
"""Periodically check for the currently playing player and switch if needed"""
|
||||
# Prevent recursion during player checking
|
||||
if self._in_player_switch:
|
||||
return True # Continue the timeout
|
||||
|
||||
try:
|
||||
current_time = time.time()
|
||||
|
||||
# Debounce player switching to prevent rapid changes
|
||||
if current_time - self._last_player_switch < self._switch_debounce_time:
|
||||
return True
|
||||
|
||||
# Find the currently playing player
|
||||
playing_player = self.get_playing_player()
|
||||
|
||||
if playing_player:
|
||||
# If we found a playing player and it's different from current, switch to it
|
||||
should_switch = False
|
||||
|
||||
if not self.current_player:
|
||||
should_switch = True
|
||||
else:
|
||||
try:
|
||||
# Safely compare player names
|
||||
if (hasattr(self.current_player, 'props') and
|
||||
hasattr(self.current_player.props, 'player_name') and
|
||||
hasattr(playing_player, 'props') and
|
||||
hasattr(playing_player.props, 'player_name')):
|
||||
|
||||
if self.current_player.props.player_name != playing_player.props.player_name:
|
||||
should_switch = True
|
||||
else:
|
||||
# If we can't safely compare, assume we should switch
|
||||
should_switch = True
|
||||
except Exception as e:
|
||||
print(f"[DEBUG] Error comparing players: {e}")
|
||||
should_switch = True
|
||||
|
||||
if should_switch:
|
||||
self.set_current_player(playing_player)
|
||||
self._last_player_switch = current_time
|
||||
|
||||
elif self.current_player:
|
||||
# If no player is playing, check if current player is still valid and paused
|
||||
try:
|
||||
if (hasattr(self.current_player, 'props') and
|
||||
hasattr(self.current_player.props, 'playback_status') and
|
||||
hasattr(self.current_player.props.playback_status, 'value_name')):
|
||||
|
||||
if self.current_player.props.playback_status.value_name != "PLAYERCTL_PLAYBACK_STATUS_PLAYING":
|
||||
# Current player is paused, see if there's another player available
|
||||
self._try_switch_to_alternative_player(current_time)
|
||||
else:
|
||||
# Current player is invalid, try to find another
|
||||
self._try_switch_to_alternative_player(current_time)
|
||||
|
||||
except Exception as e:
|
||||
# Current player seems invalid, try to find another
|
||||
self._try_switch_to_alternative_player(current_time)
|
||||
|
||||
# Update visibility based on current state
|
||||
self.update_visibility()
|
||||
|
||||
except Exception as e:
|
||||
print(f"[DEBUG] Error in check_active_player: {e}")
|
||||
|
||||
# Return True to continue the timeout
|
||||
return True
|
||||
|
||||
def _delayed_init(self):
|
||||
"""Initialize players after a delay to prevent showing during dock startup"""
|
||||
try:
|
||||
self.manager.init_all_players()
|
||||
self.update_visibility()
|
||||
except Exception as e:
|
||||
print(f"[DEBUG] Error in delayed music player init: {e}")
|
||||
return False # Don't repeat this timeout
|
||||
|
||||
def _try_switch_to_alternative_player(self, current_time):
|
||||
"""Try to switch to an alternative player when current is paused/invalid"""
|
||||
try:
|
||||
if not hasattr(self.manager, "_manager") or not hasattr(self.manager._manager, "props"):
|
||||
return
|
||||
|
||||
player_names = self.manager._manager.props.player_names
|
||||
if not player_names:
|
||||
return
|
||||
|
||||
for player_name in player_names:
|
||||
try:
|
||||
# Validate player_name
|
||||
if not player_name or not hasattr(player_name, 'name'):
|
||||
continue
|
||||
|
||||
player = Playerctl.Player.new_from_name(player_name)
|
||||
|
||||
# Validate player object
|
||||
if not player or not hasattr(player, 'props') or not hasattr(player.props, 'player_name'):
|
||||
continue
|
||||
|
||||
# Skip if this is the same as current player
|
||||
if (self.current_player and
|
||||
hasattr(self.current_player, 'props') and
|
||||
hasattr(self.current_player.props, 'player_name') and
|
||||
player.props.player_name == self.current_player.props.player_name):
|
||||
continue
|
||||
|
||||
# Switch to a different available player
|
||||
self.set_current_player(player)
|
||||
self._last_player_switch = current_time
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
continue
|
||||
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
def set_current_player(self, player):
|
||||
"""Set the current active player"""
|
||||
# Prevent recursion during player switching
|
||||
if self._in_player_switch:
|
||||
return
|
||||
|
||||
# Add debouncing to prevent rapid switching
|
||||
import time
|
||||
current_time = time.time() * 1000 # Convert to milliseconds
|
||||
if current_time - self._last_player_switch_time < self._switch_debounce_ms:
|
||||
return
|
||||
|
||||
self._in_player_switch = True
|
||||
self._last_player_switch_time = current_time
|
||||
try:
|
||||
# Validate player before proceeding
|
||||
if not player or not hasattr(player, 'props') or not hasattr(player.props, 'player_name'):
|
||||
return
|
||||
|
||||
|
||||
# Properly cleanup previous player service
|
||||
if self.current_player_service:
|
||||
try:
|
||||
# Use the cleanup method if available
|
||||
if hasattr(self.current_player_service, 'cleanup'):
|
||||
self.current_player_service.cleanup()
|
||||
else:
|
||||
# Fallback: Stop any running fabricators/timers manually
|
||||
if hasattr(self.current_player_service, 'pos_fabricator'):
|
||||
try:
|
||||
self.current_player_service.pos_fabricator.stop()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Disconnect signals safely - avoid bulk disconnect which can cause recursion
|
||||
# Store signal handler IDs for proper cleanup
|
||||
if hasattr(self.current_player_service, '_signal_handlers'):
|
||||
try:
|
||||
for handler_id in self.current_player_service._signal_handlers:
|
||||
self.current_player_service.disconnect(handler_id)
|
||||
self.current_player_service._signal_handlers.clear()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.current_player = player
|
||||
self.current_player_service = None
|
||||
|
||||
# Create new player service with error handling and recursion prevention
|
||||
try:
|
||||
# Validate player before creating service
|
||||
if (hasattr(player, 'props') and
|
||||
hasattr(player.props, 'player_name') and
|
||||
player.props.player_name):
|
||||
|
||||
self.current_player_service = PlayerService(player=player)
|
||||
|
||||
# Connect to player events with error handling and store handler IDs
|
||||
if self.current_player_service:
|
||||
try:
|
||||
# Initialize signal handlers list for proper cleanup
|
||||
self.current_player_service._signal_handlers = []
|
||||
|
||||
# Connect signals and store handler IDs
|
||||
handler_id = self.current_player_service.connect("track-position", self.on_track_position)
|
||||
self.current_player_service._signal_handlers.append(handler_id)
|
||||
|
||||
handler_id = self.current_player_service.connect("play", self.on_play)
|
||||
self.current_player_service._signal_handlers.append(handler_id)
|
||||
|
||||
handler_id = self.current_player_service.connect("pause", self.on_pause)
|
||||
self.current_player_service._signal_handlers.append(handler_id)
|
||||
|
||||
handler_id = self.current_player_service.connect("meta-change", self.on_metadata)
|
||||
self.current_player_service._signal_handlers.append(handler_id)
|
||||
except Exception:
|
||||
# If signal connection fails, clean up the service
|
||||
if hasattr(self.current_player_service, 'cleanup'):
|
||||
self.current_player_service.cleanup()
|
||||
self.current_player_service = None
|
||||
else:
|
||||
pass
|
||||
|
||||
except Exception:
|
||||
self.current_player_service = None
|
||||
|
||||
# Always use generic music icon, not player-specific icons
|
||||
self.music_label.set_markup(icons.music)
|
||||
|
||||
# Update initial state only if we have a valid service
|
||||
if self.current_player_service:
|
||||
self.update_playback_state()
|
||||
|
||||
except Exception as e:
|
||||
# Reset to safe state
|
||||
self.current_player = None
|
||||
self.current_player_service = None
|
||||
finally:
|
||||
# Always reset recursion protection flag
|
||||
self._in_player_switch = False
|
||||
|
||||
def on_track_position(self, service, pos, dur):
|
||||
"""Handle track position updates"""
|
||||
try:
|
||||
# Validate input parameters
|
||||
if pos is None or dur is None:
|
||||
return
|
||||
|
||||
# Ensure pos and dur are numeric
|
||||
try:
|
||||
pos = float(pos)
|
||||
dur = float(dur)
|
||||
except (ValueError, TypeError):
|
||||
return
|
||||
|
||||
# Validate ranges
|
||||
if pos < 0 or dur < 0:
|
||||
return
|
||||
|
||||
self.position = pos
|
||||
self.duration = dur
|
||||
|
||||
if dur > 0:
|
||||
progress = min(pos / dur, 1.0) # Ensure progress doesn't exceed 1.0
|
||||
|
||||
# Safely update progress bar
|
||||
try:
|
||||
self.progress_bar.value = progress
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
else:
|
||||
# Duration is 0 or unknown - show position only if available
|
||||
try:
|
||||
if pos > 0:
|
||||
# Set progress bar to indeterminate state or small progress
|
||||
# Use a pulsing animation or small fixed progress to show activity
|
||||
self.progress_bar.value = 0.1 # Small progress to show activity
|
||||
else:
|
||||
self.progress_bar.value = 0
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def on_play(self, service):
|
||||
"""Handle play state"""
|
||||
self.music_label.set_markup(icons.music)
|
||||
self.progress_bar.remove_style_class("paused")
|
||||
|
||||
# Trigger immediate check for active player when play state changes
|
||||
self.check_active_player()
|
||||
# Update visibility when playback state changes
|
||||
self.update_visibility()
|
||||
|
||||
def on_pause(self, service):
|
||||
"""Handle pause state"""
|
||||
# Always use generic music icon when paused
|
||||
self.music_label.set_markup(icons.music)
|
||||
self.progress_bar.add_style_class("paused")
|
||||
|
||||
# Trigger immediate check for active player when pause state changes
|
||||
self.check_active_player()
|
||||
# Update visibility when playback state changes
|
||||
self.update_visibility()
|
||||
|
||||
def on_metadata(self, service, metadata, player):
|
||||
"""Handle metadata changes"""
|
||||
# Store current track info for internal use
|
||||
keys = metadata.keys()
|
||||
if "xesam:artist" in keys and "xesam:title" in keys:
|
||||
artist = (
|
||||
metadata["xesam:artist"][0]
|
||||
if metadata["xesam:artist"]
|
||||
else "Unknown Artist"
|
||||
)
|
||||
title = metadata["xesam:title"]
|
||||
self._current_track_info = f"{artist} - {title}"
|
||||
else:
|
||||
self._current_track_info = "Music Player"
|
||||
|
||||
# Update visibility when metadata changes (media content appears/disappears)
|
||||
self.update_visibility()
|
||||
|
||||
def update_playback_state(self):
|
||||
"""Update the visual state based on current playback"""
|
||||
if not self.current_player:
|
||||
return
|
||||
|
||||
if (
|
||||
self.current_player.props.playback_status.value_name
|
||||
== "PLAYERCTL_PLAYBACK_STATUS_PLAYING"
|
||||
):
|
||||
self.on_play(None)
|
||||
else:
|
||||
self.on_pause(None)
|
||||
|
||||
def on_music_button_clicked(self, button):
|
||||
"""Handle music button click to toggle popup"""
|
||||
if self._popup_visible:
|
||||
# Hide popup if currently visible
|
||||
self.popup.hide_popup()
|
||||
self._popup_visible = False
|
||||
else:
|
||||
# Show popup if currently hidden
|
||||
self.popup.show_popup()
|
||||
self._popup_visible = True
|
||||
return True
|
||||
|
||||
def on_scroll(self, widget, event):
|
||||
"""Handle scroll events for seeking"""
|
||||
if not self.current_player or self.duration <= 0:
|
||||
return False
|
||||
|
||||
# Calculate seek amount (5 seconds per scroll)
|
||||
seek_amount = 5.0
|
||||
|
||||
if event.direction == Gdk.ScrollDirection.UP:
|
||||
new_position = min(self.position + seek_amount, self.duration)
|
||||
elif event.direction == Gdk.ScrollDirection.DOWN:
|
||||
new_position = max(self.position - seek_amount, 0)
|
||||
else:
|
||||
return False
|
||||
|
||||
# Seek to new position
|
||||
if self.current_player_service:
|
||||
self.current_player_service.set_position(int(new_position))
|
||||
|
||||
return True
|
||||
|
||||
def has_running_players(self):
|
||||
"""Check if there are any players that actually have media content running"""
|
||||
try:
|
||||
# First check if we have a current player that's still valid
|
||||
if self.current_player and self.current_player_service:
|
||||
try:
|
||||
# Check if current player still exists and has media
|
||||
if hasattr(self.current_player.props, "metadata"):
|
||||
metadata = self.current_player.props.metadata
|
||||
if metadata and len(metadata.keys()) > 0:
|
||||
keys = metadata.keys()
|
||||
if ("xesam:title" in keys or "xesam:artist" in keys or "mpris:length" in keys):
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Check all available players
|
||||
if not hasattr(self.manager, "_manager") or not hasattr(self.manager._manager, "props"):
|
||||
return False
|
||||
|
||||
player_names = self.manager._manager.props.player_names
|
||||
if not player_names:
|
||||
return False
|
||||
|
||||
for player_name in player_names:
|
||||
try:
|
||||
# Validate player_name before creating player
|
||||
if not player_name or not hasattr(player_name, 'name'):
|
||||
continue
|
||||
|
||||
player = Playerctl.Player.new_from_name(player_name)
|
||||
|
||||
# Validate player object
|
||||
if not player or not hasattr(player, "props"):
|
||||
continue
|
||||
|
||||
# Check if player has metadata indicating actual media content
|
||||
if hasattr(player.props, "metadata"):
|
||||
try:
|
||||
metadata = player.props.metadata
|
||||
if metadata and len(metadata.keys()) > 0:
|
||||
# Check for essential metadata that indicates real media
|
||||
keys = metadata.keys()
|
||||
if (
|
||||
"xesam:title" in keys
|
||||
or "xesam:artist" in keys
|
||||
or "mpris:length" in keys
|
||||
):
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Also check if player is currently playing (even without full metadata)
|
||||
# But be more strict - only count as "has media" if actually playing
|
||||
if hasattr(player.props, "playback_status"):
|
||||
try:
|
||||
status = player.props.playback_status
|
||||
if (status and hasattr(status, 'value_name') and
|
||||
status.value_name == "PLAYERCTL_PLAYBACK_STATUS_PLAYING"):
|
||||
# Double-check that this isn't just a phantom player
|
||||
# by trying to get position or metadata
|
||||
try:
|
||||
player.get_position() # This will fail if player is dead
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return False
|
||||
|
||||
def update_visibility(self):
|
||||
"""Update component visibility based on players with actual media content"""
|
||||
# Check if music player is enabled in configuration
|
||||
if hasattr(data, "DOCK_COMPONENTS_VISIBILITY"):
|
||||
music_visible = data.DOCK_COMPONENTS_VISIBILITY.get("music_player", True)
|
||||
if not music_visible:
|
||||
self.set_visible(False)
|
||||
return
|
||||
|
||||
# Check if there are players with actual media content
|
||||
has_media_players = self.has_running_players()
|
||||
|
||||
if has_media_players:
|
||||
# Cancel any pending hide timeout
|
||||
if self._visibility_timeout:
|
||||
GLib.source_remove(self._visibility_timeout)
|
||||
self._visibility_timeout = None
|
||||
# Show immediately when media is available
|
||||
self.set_visible(True)
|
||||
else:
|
||||
# Don't hide immediately - use a delay to prevent flickering
|
||||
# when players are restarting or switching
|
||||
if not self._visibility_timeout:
|
||||
self._visibility_timeout = GLib.timeout_add_seconds(
|
||||
int(self._hide_delay),
|
||||
self._delayed_hide
|
||||
)
|
||||
|
||||
def _delayed_hide(self):
|
||||
"""Hide the component after delay if still no media players"""
|
||||
# Double-check that we still don't have media players
|
||||
if not self.has_running_players():
|
||||
self.set_visible(False)
|
||||
|
||||
# Clear the timeout
|
||||
self._visibility_timeout = None
|
||||
return False # Don't repeat the timeout
|
||||
@@ -0,0 +1,51 @@
|
||||
import os
|
||||
from loguru import logger
|
||||
from gi.repository import GLib
|
||||
|
||||
from fabric.core.service import Service, Signal
|
||||
from fabric.utils.helpers import monitor_file
|
||||
|
||||
from config.info import BRIGHTNESS_DEV
|
||||
|
||||
|
||||
class BrightnessService(Service):
|
||||
|
||||
_instance = None
|
||||
|
||||
@Signal
|
||||
def value_changed(self, new_value: int, max_value: int) -> None:...
|
||||
|
||||
def __new__(cls):
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
cls._instance._init_singleton()
|
||||
return cls._instance
|
||||
|
||||
def _init_singleton(self):
|
||||
super().__init__()
|
||||
|
||||
self.max_brightness = -1
|
||||
self.brightness_monitor = None
|
||||
|
||||
backlight_path = f"/sys/class/backlight/{BRIGHTNESS_DEV}"
|
||||
try:
|
||||
self.brightness_monitor = monitor_file(backlight_path)
|
||||
self.max_brightness = self._read_max_brightness(backlight_path)
|
||||
self.brightness_monitor.connect("changed", self._on_brightness_changed)
|
||||
except Exception as e:
|
||||
logger.error(f"Brightness device not found at {backlight_path}: {e}")
|
||||
|
||||
def _read_max_brightness(self, path: str) -> int:
|
||||
max_path = os.path.join(path, "max_brightness")
|
||||
if os.path.exists(max_path):
|
||||
with open(max_path) as f:
|
||||
return int(f.readline())
|
||||
return -1
|
||||
|
||||
def _on_brightness_changed(self, monitor, file, *args):
|
||||
try:
|
||||
raw = file.load_bytes()[0].get_data()
|
||||
new_value = round(int(raw))
|
||||
self.value_changed(new_value, self.max_brightness)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to read brightness: {e}")
|
||||
@@ -0,0 +1,112 @@
|
||||
import gi
|
||||
from fabric.core.service import Service, Signal
|
||||
from loguru import logger
|
||||
|
||||
try:
|
||||
gi.require_version("NM", "1.0")
|
||||
from gi.repository import NM
|
||||
except ValueError:
|
||||
logger.error("Failed to start network manager")
|
||||
|
||||
|
||||
class NetworkService(Service):
|
||||
|
||||
@Signal
|
||||
def connection_change(
|
||||
self, ssid: str, connected: bool, status: str = ""
|
||||
) -> None: ...
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.client = NM.Client.new(None)
|
||||
self.client.connect("device-added", self.on_device_added)
|
||||
self.client.connect("device-removed", self.on_device_removed)
|
||||
# self.client.connect("notify::state", self.handle_client_state_change)
|
||||
|
||||
# active_connections = self.client.get_active_connections()
|
||||
# for ac in active_connections:
|
||||
# connection = ac.get_connection()
|
||||
# type = ac.get_connection_type()
|
||||
# print("Active connection:", connection.get_id(), type)
|
||||
|
||||
devices = self.client.get_devices()
|
||||
for dev in devices:
|
||||
if dev.get_device_type() == NM.DeviceType.WIFI:
|
||||
self.wifi_dev = dev
|
||||
active_ap = self.wifi_dev.get_active_access_point()
|
||||
if active_ap:
|
||||
ssid = active_ap.get_ssid()
|
||||
print("Connected SSID:", ssid.get_data().decode("utf-8"))
|
||||
|
||||
if self.wifi_dev:
|
||||
self.wifi_dev.connect("state-changed", self.handle_device_state_change)
|
||||
self.wifi_dev.connect(
|
||||
"notify::active-access-point", self.handle_prop_change
|
||||
)
|
||||
|
||||
def init_props(self):
|
||||
self.handle_prop_change(self.wifi_dev, None)
|
||||
|
||||
def handle_prop_change(self, source, pspec):
|
||||
if source is None:
|
||||
self.connection_change("", False, "No Device")
|
||||
return
|
||||
|
||||
device_state = source.get_state()
|
||||
if device_state == NM.DeviceState.UNAVAILABLE:
|
||||
self.connection_change("", False, "Wi-Fi Off")
|
||||
return
|
||||
|
||||
active_ap = source.get_active_access_point()
|
||||
if active_ap:
|
||||
ssid = active_ap.get_ssid().get_data().decode("utf-8")
|
||||
print("Connected SSID:", ssid)
|
||||
self.connection_change(ssid, True, "Connected")
|
||||
else:
|
||||
self.connection_change("", False, "Wi-Fi On (No Connection)")
|
||||
|
||||
def handle_device_state_change(self, device, new_state, old_state, reason):
|
||||
print("Device state changed:", old_state, "→", new_state)
|
||||
match new_state:
|
||||
case NM.DeviceState.UNAVAILABLE:
|
||||
self.connection_change("", False, "Wi-Fi Off")
|
||||
case NM.DeviceState.DISCONNECTED:
|
||||
self.connection_change("", False, "Wi-Fi On (No Connection)")
|
||||
case (
|
||||
NM.DeviceState.PREPARE
|
||||
| NM.DeviceState.CONFIG
|
||||
| NM.DeviceState.NEED_AUTH
|
||||
| NM.DeviceState.IP_CONFIG
|
||||
| NM.DeviceState.IP_CHECK
|
||||
| NM.DeviceState.SECONDARIES
|
||||
):
|
||||
self.connection_change("", False, "Connecting…")
|
||||
case NM.DeviceState.ACTIVATED:
|
||||
active_ap = device.get_active_access_point()
|
||||
if active_ap:
|
||||
ssid = active_ap.get_ssid().get_data().decode("utf-8")
|
||||
self.connection_change(ssid, True, "Connected")
|
||||
else:
|
||||
self.connection_change("", True, "Connected (No SSID)")
|
||||
case _:
|
||||
self.connection_change("", False, "Unknown")
|
||||
|
||||
def on_device_added(self, source, device):
|
||||
print("Added device")
|
||||
if device.get_device_type() == NM.DeviceType.WIFI:
|
||||
self.wifi_dev = device
|
||||
self.wifi_dev.connect("state-changed", self.handle_device_state_change)
|
||||
self.wifi_dev.connect(
|
||||
"notify::active-access-point", self.handle_prop_change
|
||||
)
|
||||
|
||||
def on_device_removed(self, client, device):
|
||||
print("Removed device")
|
||||
if self.wifi_dev and device.get_path() == self.wifi_dev.get_path():
|
||||
self.wifi_dev = None
|
||||
self.handle_prop_change(self.wifi_dev, None)
|
||||
|
||||
def print_state(self):
|
||||
print(
|
||||
"wifi: ", self.wifi_dev.get_state(), " client: ", self.client.get_state()
|
||||
)
|
||||
@@ -0,0 +1,395 @@
|
||||
.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;
|
||||
}
|
||||
@@ -37,6 +37,7 @@
|
||||
color: whitesmoke;
|
||||
font-size: 16px;
|
||||
margin-top: 5px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
#wiggle-bar {
|
||||
@@ -147,3 +148,248 @@
|
||||
#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;
|
||||
}
|
||||
+9
-1
@@ -74,18 +74,23 @@ bat_full: str = ""
|
||||
|
||||
|
||||
# Applets
|
||||
|
||||
# Applets
|
||||
wifi: str = chr(0xF1EB)
|
||||
wifi_0: str = ""
|
||||
wifi_1: str = ""
|
||||
wifi_2: str = ""
|
||||
wifi_3: str = ""
|
||||
world: str = ""
|
||||
world_off: str = ""
|
||||
bluetooth: str = ""
|
||||
bluetooth: str = chr(0xF294)
|
||||
night: str = ""
|
||||
coffee: str = ""
|
||||
notifications: str = ""
|
||||
notifications_dots: str = ""
|
||||
|
||||
blur: str = chr(0xF00B5)
|
||||
arrow_head: str = chr(0xE285)
|
||||
wifi_off: str = ""
|
||||
bluetooth_off: str = ""
|
||||
night_off: str = ""
|
||||
@@ -114,6 +119,9 @@ music: str = ""
|
||||
rewind_backward_5: str = "謹"
|
||||
rewind_forward_5: str = "難"
|
||||
|
||||
next_fill: str = chr(0xF04AD)
|
||||
previous_fill: str = chr(0xF04AE)
|
||||
|
||||
# Volume
|
||||
vol_off: str = ""
|
||||
vol_mute: str = ""
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
from gi.repository import Gtk, GLib, Gdk
|
||||
import cairo
|
||||
import datetime
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
|
||||
|
||||
class WavyCircle(Gtk.DrawingArea):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.connect("draw", self.on_draw)
|
||||
self.set_size_request(-1, 153)
|
||||
|
||||
GLib.timeout_add_seconds(1, self.on_tick)
|
||||
|
||||
self.show()
|
||||
|
||||
def _hex_to_rgba(self, hex_color: str) -> Gdk.RGBA:
|
||||
"""Convert hex color string to Gdk.RGBA object."""
|
||||
# Remove # if present
|
||||
hex_color = hex_color.lstrip("#")
|
||||
|
||||
# Convert hex to RGB values (0-255)
|
||||
r = int(hex_color[0:2], 16) / 255.0
|
||||
g = int(hex_color[2:4], 16) / 255.0
|
||||
b = int(hex_color[4:6], 16) / 255.0
|
||||
|
||||
return Gdk.RGBA(r, g, b, 1.0)
|
||||
|
||||
def _read_css_colors(self) -> dict:
|
||||
"""Read color values from styles/colors.css file."""
|
||||
colors = {}
|
||||
|
||||
# Get the path to colors.css relative to this file
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
colors_css_path = os.path.join(current_dir, "..", "styles", "colors.css")
|
||||
colors_css_path = os.path.normpath(colors_css_path)
|
||||
|
||||
try:
|
||||
with open(colors_css_path, "r") as f:
|
||||
content = f.read()
|
||||
|
||||
# Parse CSS variables using regex
|
||||
# Match patterns like: --primary: #ffb598;
|
||||
pattern = r"--([a-zA-Z0-9-]+):\s*(#[0-9a-fA-F]{6});"
|
||||
matches = re.findall(pattern, content)
|
||||
|
||||
for var_name, hex_value in matches:
|
||||
colors[var_name] = self._hex_to_rgba(hex_value)
|
||||
|
||||
except (FileNotFoundError, IOError) as e:
|
||||
print(f"Error reading colors.css: {e}")
|
||||
# Return empty dict, fallback colors will be used
|
||||
|
||||
return colors
|
||||
|
||||
def on_tick(self):
|
||||
self.queue_draw()
|
||||
return True
|
||||
|
||||
def on_draw(self, widget, ctx):
|
||||
width = self.get_allocated_width()
|
||||
height = self.get_allocated_height()
|
||||
cx, cy = width / 2, height / 2
|
||||
|
||||
base_radius = min(width, height) * 0.4
|
||||
amplitude = base_radius * 0.05
|
||||
frequency = 10
|
||||
|
||||
# Get colors from CSS file
|
||||
css_colors = self._read_css_colors()
|
||||
|
||||
# Use colors from CSS with fallbacks
|
||||
primary_color = css_colors.get(
|
||||
"primary", Gdk.RGBA(0.29, 0.56, 0.89, 1.0)
|
||||
) # Blue fallback
|
||||
secondary_color = css_colors.get(
|
||||
"foreground", Gdk.RGBA(0.17, 0.24, 0.31, 1.0)
|
||||
) # Dark fallback
|
||||
accent_color = css_colors.get(
|
||||
"foreground", Gdk.RGBA(0.91, 0.3, 0.24, 1.0)
|
||||
) # Use foreground color
|
||||
|
||||
# wavy outer circle
|
||||
ctx.set_line_width(4)
|
||||
angle_step = 2 * math.pi / 500
|
||||
ctx.move_to(
|
||||
cx + (base_radius + amplitude * math.sin(frequency * 0)) * math.cos(0),
|
||||
cy + (base_radius + amplitude * math.sin(frequency * 0)) * math.sin(0),
|
||||
)
|
||||
|
||||
angle = 0
|
||||
while angle <= math.tau:
|
||||
r = base_radius + amplitude * math.sin(frequency * angle)
|
||||
x = cx + r * math.cos(angle)
|
||||
y = cy + r * math.sin(angle)
|
||||
ctx.line_to(x, y)
|
||||
angle += angle_step
|
||||
|
||||
ctx.set_source_rgba(
|
||||
primary_color.red,
|
||||
primary_color.green,
|
||||
primary_color.blue,
|
||||
primary_color.alpha,
|
||||
)
|
||||
ctx.close_path()
|
||||
ctx.fill_preserve()
|
||||
ctx.set_source_rgba(
|
||||
primary_color.red,
|
||||
primary_color.green,
|
||||
primary_color.blue,
|
||||
primary_color.alpha,
|
||||
)
|
||||
ctx.stroke()
|
||||
|
||||
ANGLE_OFFSET = 0.25
|
||||
now = datetime.datetime.now()
|
||||
seconds = now.second + now.microsecond / 1e6
|
||||
hour = now.hour % 12 + now.minute / 60.0
|
||||
minute = now.minute + now.second / 60.0
|
||||
|
||||
second_angle = (seconds / 60.0 - ANGLE_OFFSET) * math.tau
|
||||
hour_angle = (hour / 12.0 - ANGLE_OFFSET) * math.tau
|
||||
minute_angle = (minute / 60.0 - ANGLE_OFFSET) * math.tau
|
||||
|
||||
hour_orbit = base_radius * 0.8 - 28
|
||||
minute_orbit = base_radius * 0.8 - 14
|
||||
second_orbit = base_radius * 0.8
|
||||
dot_radius = 9
|
||||
|
||||
ctx.set_line_cap(cairo.LINE_CAP_ROUND)
|
||||
|
||||
# hour hand
|
||||
ctx.set_line_width(6) # Reduced width for better visibility
|
||||
ctx.set_source_rgba(
|
||||
secondary_color.red, secondary_color.green, secondary_color.blue, 1.0
|
||||
)
|
||||
ctx.move_to(cx, cy)
|
||||
ctx.line_to(
|
||||
cx + hour_orbit * math.cos(hour_angle),
|
||||
cy + hour_orbit * math.sin(hour_angle),
|
||||
)
|
||||
ctx.stroke()
|
||||
|
||||
# minute hand
|
||||
ctx.set_line_width(4) # Reduced width for better visibility
|
||||
ctx.set_source_rgba(
|
||||
secondary_color.red, secondary_color.green, secondary_color.blue, 1.0
|
||||
)
|
||||
ctx.move_to(cx, cy)
|
||||
ctx.line_to(
|
||||
cx + minute_orbit * math.cos(minute_angle),
|
||||
cy + minute_orbit * math.sin(minute_angle),
|
||||
)
|
||||
ctx.stroke()
|
||||
|
||||
# second dot
|
||||
x = cx + second_orbit * math.cos(second_angle)
|
||||
y = cy + second_orbit * math.sin(second_angle)
|
||||
|
||||
ctx.arc(x, y, dot_radius, 0, math.tau)
|
||||
ctx.set_source_rgba(
|
||||
accent_color.red, accent_color.green, accent_color.blue, 1.0
|
||||
)
|
||||
ctx.fill()
|
||||
|
||||
# center dot to anchor the hands
|
||||
ctx.arc(cx, cy, 4, 0, math.tau)
|
||||
ctx.set_source_rgba(
|
||||
secondary_color.red, secondary_color.green, secondary_color.blue, 1.0
|
||||
)
|
||||
ctx.fill()
|
||||
Reference in new issue
Block a user