[SCRIPT] Updated

This commit is contained in:
t
2025-03-31 20:14:11 +02:00
parent 810f54cb65
commit 314a27eb42
472 changed files with 11721 additions and 921 deletions
@@ -0,0 +1,3 @@
from .control_center import ControlCenter
__all__ = ["ControlCenter"]
@@ -0,0 +1,66 @@
from ignis.widgets import Widget
from ignis.app import IgnisApp
from .widgets import (
QuickSettings,
Brightness,
VolumeSlider,
User,
Media,
NotificationCenter,
)
from .menu import opened_menu
app = IgnisApp.get_default()
class ControlCenter(Widget.RevealerWindow):
def __init__(self):
revealer = Widget.Revealer(
transition_type="slide_left",
child=Widget.Box(
vertical=True,
css_classes=["control-center"],
child=[
Widget.Box(
vertical=True,
css_classes=["control-center-widget"],
child=[
QuickSettings(),
VolumeSlider("speaker"),
VolumeSlider("microphone"),
Brightness(),
User(),
Media(),
],
),
NotificationCenter(),
],
),
transition_duration=300,
reveal_child=True,
)
super().__init__(
visible=False,
popup=True,
kb_mode="on_demand",
layer="top",
css_classes=["unset"],
anchor=["top", "right", "bottom", "left"],
namespace="ignis_CONTROL_CENTER",
child=Widget.Box(
child=[
Widget.Button(
vexpand=True,
hexpand=True,
css_classes=["unset"],
on_click=lambda x: app.close_window("ignis_CONTROL_CENTER"),
),
revealer,
],
),
setup=lambda self: self.connect(
"notify::visible", lambda x, y: opened_menu.set_value("")
),
revealer=revealer,
)
@@ -0,0 +1,34 @@
from gi.repository import GObject # type: ignore
from ignis.widgets import Widget
from ignis.variable import Variable
from ignis.base_widget import BaseWidget
opened_menu = Variable()
class Menu(Widget.Revealer):
def __init__(self, name: str, child: list[BaseWidget], **kwargs):
self._name = name
self._box = Widget.Box(
vertical=True,
css_classes=["control-center-menu"],
child=child,
)
super().__init__(
transition_type="slide_down",
transition_duration=300,
reveal_child=opened_menu.bind("value", lambda value: value == self._name),
child=self._box,
**kwargs,
)
def toggle(self) -> None:
if self.reveal_child:
opened_menu.value = ""
else:
opened_menu.value = self._name
@GObject.Property
def box(self) -> Widget.Box:
return self._box
@@ -0,0 +1,65 @@
from ignis.widgets import Widget
from gi.repository import GObject # type: ignore
from typing import Callable
from ignis.gobject import Binding
from .menu import Menu
class QSButton(Widget.Button):
def __init__(
self,
label: str | Binding,
icon_name: str | Binding,
on_activate: Callable | None = None,
on_deactivate: Callable | None = None,
menu: Menu | None = None,
**kwargs,
):
self.on_activate = on_activate
self.on_deactivate = on_deactivate
self._active = False
self._menu = menu
super().__init__(
child=Widget.Box(
child=[
Widget.Icon(image=icon_name),
Widget.Label(label=label, css_classes=["qs-button-label"]),
Widget.Arrow(
halign="end",
hexpand=True,
pixel_size=20,
rotated=menu.bind("reveal_child"),
)
if menu
else None,
]
),
on_click=self.__callback,
css_classes=["qs-button", "unset"],
hexpand=True,
**kwargs,
)
def __callback(self, *args) -> None:
if self.active:
if self.on_deactivate:
self.on_deactivate(self)
else:
if self.on_activate:
self.on_activate(self)
@GObject.Property
def active(self) -> bool:
return self._active
@active.setter
def active(self, value: bool) -> None:
self._active = value
if value:
self.add_css_class("active")
else:
self.remove_css_class("active")
@GObject.Property
def menu(self) -> Menu | None:
return self._menu
@@ -0,0 +1,15 @@
from .quick_settings import QuickSettings
from .brightness import Brightness
from .volume import VolumeSlider
from .user import User
from .media import Media
from .notification_center import NotificationCenter
__all__ = [
"QuickSettings",
"Brightness",
"VolumeSlider",
"User",
"Media",
"NotificationCenter",
]
@@ -0,0 +1,28 @@
from ignis.services.backlight import BacklightService
from ignis.widgets import Widget
backlight = BacklightService.get_default()
class Brightness(Widget.Box):
def __init__(self):
super().__init__(
visible=backlight.bind("available"),
hexpand=True,
style="margin-top: 0.25rem;",
child=[
Widget.Icon(
image="display-brightness-symbolic",
css_classes=["material-slider-icon"],
pixel_size=18,
),
Widget.Scale(
min=0,
max=backlight.max_brightness,
hexpand=True,
value=backlight.bind("brightness"),
css_classes=["material-slider"],
on_change=lambda x: backlight.set_brightness(x.value),
),
],
)
@@ -0,0 +1,210 @@
import os
import ignis
from ignis.widgets import Widget
from ignis.services.mpris import MprisService, MprisPlayer
from ignis.utils import Utils
from services.material import MaterialService
from ignis.app import IgnisApp
from ignis.exceptions import StylePathNotFoundError
mpris = MprisService.get_default()
app = IgnisApp.get_default()
material = MaterialService.get_default()
MEDIA_TEMPLATE = Utils.get_current_dir() + "/media.scss"
MEDIA_SCSS_CACHE_DIR = ignis.CACHE_DIR + "/media" # type: ignore
MEDIA_ART_FALLBACK = Utils.get_current_dir() + "/../../../misc/media-art-fallback.png"
os.makedirs(MEDIA_SCSS_CACHE_DIR, exist_ok=True)
PLAYER_ICONS = {
"spotify": "spotify-symbolic",
"firefox": "firefox-browser-symbolic",
"chrome": "chrome-symbolic",
None: "folder-music-symbolic",
}
class Player(Widget.Revealer):
def __init__(self, player: MprisPlayer) -> None:
self._player = player
self._colors_path = f"{MEDIA_SCSS_CACHE_DIR}/{self._player.desktop_entry}.scss"
player.connect("closed", lambda x: self.destroy())
player.connect("notify::art-url", lambda x, y: self.load_colors())
self.load_colors()
super().__init__(
transition_type="slide_down",
reveal_child=False,
css_classes=[self.get_css("media")],
child=Widget.Overlay(
child=Widget.Box(css_classes=[self.get_css("media-image")]),
overlays=[
Widget.Box(
hexpand=True,
vexpand=True,
css_classes=[self.get_css("media-image-gradient")],
),
Widget.Icon(
icon_name=self.get_player_icon(),
pixel_size=22,
halign="start",
valign="start",
css_classes=[self.get_css("media-player-icon")],
),
Widget.Box(
vertical=True,
hexpand=True,
css_classes=[self.get_css("media-content")],
child=[
Widget.Box(
vexpand=True,
valign="center",
child=[
Widget.Box(
hexpand=True,
vertical=True,
child=[
Widget.Label(
ellipsize="end",
label=player.bind("title"),
max_width_chars=30,
halign="start",
css_classes=[
self.get_css("media-title")
],
),
Widget.Label(
label=player.bind("artist"),
max_width_chars=30,
ellipsize="end",
halign="start",
css_classes=[
self.get_css("media-artist")
],
),
],
),
Widget.Button(
child=Widget.Icon(
image=player.bind(
"playback_status",
lambda value: "media-playback-pause-symbolic"
if value == "Playing"
else "media-playback-start-symbolic",
),
pixel_size=18,
),
on_click=lambda x: player.play_pause(),
visible=player.bind("can_play"),
css_classes=player.bind(
"playback_status",
lambda value: [
self.get_css("media-playback-button"),
"playing",
]
if value == "Playing"
else [
self.get_css("media-playback-button"),
"paused",
],
),
),
],
),
],
),
Widget.Box(
vexpand=True,
valign="end",
style="padding: 1rem;",
child=[
Widget.Scale(
value=player.bind("position"),
max=player.bind("length"),
hexpand=True,
css_classes=[self.get_css("media-scale")],
on_change=lambda x: player.set_position(x.value),
visible=player.bind(
"position", lambda value: value != -1
),
),
Widget.Button(
child=Widget.Icon(
image="media-skip-backward-symbolic",
pixel_size=20,
),
css_classes=[self.get_css("media-skip-button")],
on_click=lambda x: player.previous(),
visible=player.bind("can_go_previous"),
style="margin-left: 1rem;",
),
Widget.Button(
child=Widget.Icon(
image="media-skip-forward-symbolic",
pixel_size=20,
),
css_classes=[self.get_css("media-skip-button")],
on_click=lambda x: player.next(),
visible=player.bind("can_go_next"),
style="margin-left: 1rem;",
),
],
),
],
),
)
def get_player_icon(self) -> str:
if self._player.desktop_entry == "firefox":
return PLAYER_ICONS["firefox"]
elif self._player.desktop_entry == "spotify":
return PLAYER_ICONS["spotify"]
elif self._player.track_id is not None:
if "chromium" in self._player.track_id or "chrome" in self._player.track_id:
return PLAYER_ICONS["chrome"]
return PLAYER_ICONS[None]
def destroy(self) -> None:
self.set_reveal_child(False)
Utils.Timeout(self.transition_duration, super().unparent)
def get_css(self, class_name: str) -> str:
return f"{class_name}-{self._player.desktop_entry}"
def load_colors(self) -> None:
if not self._player.art_url:
art_url = MEDIA_ART_FALLBACK
else:
art_url = self._player.art_url
try:
app.remove_css(self._colors_path)
except StylePathNotFoundError:
pass
colors = material.get_colors_from_img(art_url, True)
colors["art_url"] = art_url
colors["desktop_entry"] = self._player.desktop_entry
material.render_template(
colors, input_file=MEDIA_TEMPLATE, output_file=self._colors_path
)
app.apply_css(self._colors_path)
class Media(Widget.Box):
def __init__(self):
super().__init__(
vertical=True,
setup=lambda self: mpris.connect(
"player_added", lambda x, player: self.__add_player(player)
),
css_classes=["rec-unset"],
)
def __add_player(self, obj: MprisPlayer) -> None:
player = Player(obj)
self.append(player)
player.set_reveal_child(True)
@@ -0,0 +1,84 @@
// JINJA2 TEMPLATE
// WARNING: Do not import this file in style.scss
.media-{{ desktop_entry }} {
margin-top: 1rem;
}
.media-image-{{ desktop_entry }} {
background-image: url('file:///{{ art_url }}');
background-repeat: no-repeat;
background-size: cover;
background-position: center;
min-height: 11rem;
border-radius: 1.5rem;
}
.media-image-gradient-{{ desktop_entry }} {
border-radius: 1.3rem;
background: linear-gradient(90deg, rgba({{ onPrimary }}, 0.9) 0%, rgba({{ onPrimary }}, 0.4) 50%, rgba({{ onPrimary }}, 0.9) 100%);
}
.media-scale-{{ desktop_entry }} trough,
.media-scale-{{ desktop_entry }} trough highlight {
background-color: rgba({{ onSurface }}, 0.2);
border-radius: 1rem;
min-height: 0.2rem;
}
.media-scale-{{ desktop_entry }} trough highlight {
background-color: {{ onSurface }};
}
.media-scale-{{ desktop_entry }} slider {
background-color: {{ onSurface }};
padding: 0.5rem 0.1rem;
margin: -0.5rem -0.1rem;
}
.media-title-{{ desktop_entry }} {
color: {{ onSurface }};
}
.media-artist-{{ desktop_entry }} {
color: {{ onSurfaceVariant }};
font-weight: normal;
}
.media-content-{{ desktop_entry }} {
padding: 1rem;
}
.media-playback-button-{{ desktop_entry }} {
background-color: {{ primary }};
color: {{ onPrimary }};
border-radius: 4rem;
min-width: 2.7rem;
min-height: 2.7rem;
transition: 0.3s;
&:hover {
background-color: lighten({{ primary }}, 5%);
}
&.playing {
border-radius: 1rem;
}
}
.media-player-icon-{{ desktop_entry }} {
color: {{ primary }};
padding: 1rem;
padding-top: 1.2rem;
}
.media-skip-button-{{ desktop_entry }} {
transition: 0.3s;
color: {{ onSurface }};
&:hover {
color: {{ primary }};
}
}
@@ -0,0 +1,109 @@
from ignis.widgets import Widget
from ignis.services.notifications import Notification, NotificationService
from ignis.utils import Utils
from gi.repository import GLib # type: ignore
from ...shared_widgets import NotificationWidget
notifications = NotificationService.get_default()
class Popup(Widget.Revealer):
def __init__(self, notification: Notification, **kwargs):
widget = NotificationWidget(notification)
super().__init__(child=widget, transition_type="slide_down", **kwargs)
notification.connect("closed", lambda x: self.destroy())
def destroy(self):
self.reveal_child = False
Utils.Timeout(self.transition_duration, self.unparent)
class NotificationList(Widget.Box):
__gtype_name__ = "NotificationList"
def __init__(self):
loading_notifications_label = Widget.Label(
label="Loading notifications...",
valign="center",
vexpand=True,
css_classes=["notification-center-info-label"],
)
super().__init__(
vertical=True,
child=[loading_notifications_label],
vexpand=True,
css_classes=["rec-unset"],
setup=lambda self: notifications.connect(
"notified",
lambda x, notification: self.__on_notified(notification),
),
)
Utils.ThreadTask(
self.__load_notifications,
lambda result: self.set_child(result),
).run()
def __on_notified(self, notification: Notification) -> None:
notify = Popup(notification)
self.prepend(notify)
notify.reveal_child = True
def __load_notifications(self) -> list[Widget.Label | Popup]:
widgets: list[Widget.Label | Popup] = []
for i in notifications.notifications:
GLib.idle_add(lambda i=i: widgets.append(Popup(i, reveal_child=True)))
widgets.append(
Widget.Label(
label="No notifications",
valign="center",
vexpand=True,
visible=notifications.bind(
"notifications", lambda value: len(value) == 0
),
css_classes=["notification-center-info-label"],
)
)
return widgets
class NotificationCenter(Widget.Box):
__gtype_name__ = "NotificationCenter"
def __init__(self):
super().__init__(
vertical=True,
vexpand=True,
css_classes=["notification-center"],
child=[
Widget.Box(
css_classes=["notification-center-header", "rec-unset"],
child=[
Widget.Label(
label=notifications.bind(
"notifications", lambda value: str(len(value))
),
css_classes=["notification-count"],
),
Widget.Label(
label="notifications",
css_classes=["notification-header-label"],
),
Widget.Button(
child=Widget.Label(label="Clear all"),
halign="end",
hexpand=True,
on_click=lambda x: notifications.clear_all(),
css_classes=["notification-clear-all"],
),
],
),
Widget.Scroll(
child=NotificationList(),
vexpand=True,
),
],
)
@@ -0,0 +1,3 @@
from .quick_settings import QuickSettings
__all__ = ["QuickSettings"]
@@ -0,0 +1,87 @@
from ignis.widgets import Widget
from ...qs_button import QSButton
from ...menu import Menu
from ....shared_widgets import ToggleBox
from ignis.services.bluetooth import BluetoothService, BluetoothDevice
bluetooth = BluetoothService.get_default()
class BluetoothDeviceItem(Widget.Button):
def __init__(self, device: BluetoothDevice):
super().__init__(
css_classes=["network-item", "unset"],
on_click=lambda x: device.disconnect_from()
if device.connected
else device.connect_to(),
child=Widget.Box(
child=[
Widget.Icon(
image=device.bind("icon_name"),
),
Widget.Label(
label=device.alias,
halign="start",
css_classes=["wifi-network-label"],
),
Widget.Icon(
image="object-select-symbolic",
halign="end",
hexpand=True,
visible=device.bind("connected"),
),
]
),
)
class BluetoothMenu(Menu):
def __init__(self):
super().__init__(
name="bluetooth",
child=[
ToggleBox(
label="Bluetooth",
active=bluetooth.powered,
on_change=lambda x, state: bluetooth.set_powered(state),
css_classes=["network-header-box"],
),
Widget.Box(
vertical=True,
child=bluetooth.bind(
"devices",
transform=lambda value: [BluetoothDeviceItem(i) for i in value],
),
),
],
)
class BluetoothButton(QSButton):
def __init__(self):
menu = BluetoothMenu()
def get_label(devices: list[BluetoothDevice]) -> str:
if len(devices) == 0:
return "Bluetooth"
elif len(devices) == 1:
return devices[0].alias
else:
return f"{len(devices)} pairs"
def toggle_menu(x) -> None:
bluetooth.set_setup_mode(True)
menu.toggle()
super().__init__(
label=bluetooth.bind("connected_devices", get_label),
icon_name="bluetooth-active-symbolic",
on_activate=toggle_menu,
on_deactivate=toggle_menu,
active=bluetooth.bind("powered"),
menu=menu,
)
def bluetooth_control() -> list[QSButton]:
return [] if bluetooth.state == "absent" else [BluetoothButton()]
@@ -0,0 +1,15 @@
from ...qs_button import QSButton
from user_options import user_options
class DarkModeButton(QSButton):
__gtype_name__ = "DarkModeButton"
def __init__(self):
super().__init__(
label="Dark",
icon_name="night-light-symbolic",
on_activate=lambda x: user_options.material.set_dark_mode(True),
on_deactivate=lambda x: user_options.material.set_dark_mode(False),
active=user_options.material.bind("dark_mode"),
)
@@ -0,0 +1,28 @@
from ...qs_button import QSButton
from ignis.services.notifications import NotificationService
from ignis.options import options
notifications = NotificationService.get_default()
class DNDButton(QSButton):
__gtype_name__ = "DNDButton"
def __init__(self):
super().__init__(
label=options.notifications.bind(
"dnd", lambda value: "Silent" if value else "Noisy"
),
icon_name=options.notifications.bind(
"dnd",
transform=lambda value: "notification-disabled-symbolic"
if value
else "notification-symbolic",
),
on_activate=lambda x: self.__activate(True),
on_deactivate=lambda x: self.__activate(False),
active=options.notifications.bind("dnd"),
)
def __activate(self, state: bool) -> None:
options.notifications.dnd = state
@@ -0,0 +1,102 @@
import asyncio
from ignis.widgets import Widget
from ignis.utils import Utils
from ...qs_button import QSButton
from ...menu import Menu
from ignis.services.network import NetworkService, EthernetDevice
network = NetworkService.get_default()
class EthernetConnectionItem(Widget.Button):
def __init__(self, device: EthernetDevice):
super().__init__(
css_classes=["network-item", "unset"],
on_click=lambda x: asyncio.create_task(device.disconnect_from())
if device.is_connected
else asyncio.create_task(device.connect_to()),
child=Widget.Box(
child=[
Widget.Icon(image="network-wired-symbolic"),
Widget.Label(
label=device.name,
ellipsize="end",
max_width_chars=20,
halign="start",
),
Widget.Button(
child=Widget.Label(
label=device.bind(
"is_connected",
lambda value: "Disconnect" if value else "Connect",
)
),
css_classes=["connect-label", "unset"],
halign="end",
hexpand=True,
),
]
),
)
class EthernetMenu(Menu):
def __init__(self):
super().__init__(
name="ethernet",
child=[
Widget.Box(
css_classes=["network-header-box"],
child=[
Widget.Icon(icon_name="network-wired-symbolic", pixel_size=28),
Widget.Label(
label="Wired connections",
css_classes=["network-header-label"],
),
],
),
Widget.Box(
vertical=True,
child=network.ethernet.bind(
"devices",
lambda value: [EthernetConnectionItem(i) for i in value],
),
),
Widget.Separator(),
Widget.Button(
css_classes=["network-item", "unset"],
style="margin-bottom: 0;",
on_click=lambda x: asyncio.create_task(Utils.exec_sh_async("nm-connection-editor")),
child=Widget.Box(
child=[
Widget.Icon(image="preferences-system-symbolic"),
Widget.Label(
label="Network Settings",
halign="start",
),
]
),
),
],
)
class EthernetButton(QSButton):
def __init__(self):
menu = EthernetMenu()
super().__init__(
label="Wired",
icon_name="network-wired-symbolic",
on_activate=lambda x: menu.toggle(),
on_deactivate=lambda x: menu.toggle(),
menu=menu,
active=network.ethernet.bind("is_connected"),
)
def ethernet_control() -> list[QSButton]:
if len(network.ethernet.devices) > 0:
return [EthernetButton()]
else:
return []
@@ -0,0 +1,68 @@
from ignis.widgets import Widget
from .wifi import wifi_control
from .record import RecordButton
from .dnd import DNDButton
from .dark_mode import DarkModeButton
from .ethernet import ethernet_control
from .vpn import vpn_control
from .bluetooth import bluetooth_control
from ...qs_button import QSButton
from ignis.services.network import NetworkService
network = NetworkService.get_default()
class QuickSettings(Widget.Box):
def __init__(self):
super().__init__(vertical=True, css_classes=["qs-main-box"])
network.wifi.connect("notify::devices", lambda x, y: self.__refresh())
network.ethernet.connect("notify::devices", lambda x, y: self.__refresh())
network.vpn.connect("notify::connections", lambda x, y: self.__refresh())
self.__refresh()
def __refresh(self) -> None:
self.child = []
self.__configure()
def __configure(self) -> None:
self.__qs_fabric(
*wifi_control(),
*ethernet_control(),
*vpn_control(),
*bluetooth_control(),
DNDButton(),
DarkModeButton(),
RecordButton(),
)
def __qs_fabric(self, *buttons: QSButton) -> None:
for i in range(0, len(buttons), 2):
self.__add_row(buttons, i)
def __add_row(self, buttons: tuple[QSButton, ...], i: int) -> None:
row = Widget.Box(homogeneous=True)
if len(self.child) > 0:
row.style = "margin-top: 0.5rem;"
self.append(row)
button1 = buttons[i]
self.__add_button(row, button1, buttons, i)
if i + 1 < len(buttons):
button2 = buttons[i + 1]
button2.style = "margin-left: 0.5rem;"
self.__add_button(row, button2, buttons, i)
def __add_button(
self, row: Widget.Box, button: QSButton, buttons: tuple[QSButton, ...], i: int
) -> None:
row.append(button)
if button.menu:
self.append(button.menu)
if i == len(buttons) - 1 or i == len(buttons) - 2:
button.menu.box.add_css_class("control-center-menu-last-row")
@@ -0,0 +1,102 @@
from ignis.widgets import Widget
from ...qs_button import QSButton
from ...menu import Menu
from ignis.services.recorder import RecorderService
recorder = RecorderService.get_default()
class RecordMenu(Menu):
def __init__(self):
self._audio_switch = Widget.Switch(halign="end", hexpand=True, valign="center")
self._dropdown = Widget.DropDown(
items=["Internal audio", "Microphone", "Both sources"],
css_classes=["record-dropdown"],
)
super().__init__(
name="recording",
child=[
Widget.Icon(
image="media-record-symbolic",
pixel_size=36,
halign="center",
css_classes=["record-icon"],
),
Widget.Label(
label="Start recording?",
halign="center",
style="font-size: 1.2rem;",
),
Widget.Box(
style="margin-top: 0.5rem;",
child=[
Widget.Icon(
image="microphone-sensitivity-medium-symbolic",
pixel_size=20,
style="margin-right: 0.5rem;",
),
Widget.Box(
vertical=True,
child=[
Widget.Label(
label="Record audio",
style="font-size: 1.1rem;",
halign="start",
),
self._dropdown,
],
),
self._audio_switch,
],
),
Widget.Box(
style="margin-top: 1rem;",
child=[
Widget.Button(
child=Widget.Label(label="Cancel"),
css_classes=["record-cancel-button", "unset"],
on_click=lambda x: self.set_reveal_child(False), # type: ignore
),
Widget.Button(
child=Widget.Label(label="Start recording"),
halign="end",
hexpand=True,
css_classes=["record-start-button", "unset"],
on_click=lambda x: self.__start_recording(), # type: ignore
),
],
),
],
)
def __start_recording(self) -> None:
self.set_reveal_child(False)
microphone = False
internal = False
if self._audio_switch.active:
if self._dropdown.selected == "Internal audio":
internal = True
elif self._dropdown.selected == "Microphone":
microphone = True
else:
internal = True
microphone = True
recorder.start_recording(
record_microphone=microphone, record_internal_audio=internal
)
class RecordButton(QSButton):
def __init__(self):
record_menu = RecordMenu()
super().__init__(
label="Recording",
icon_name="media-record-symbolic",
on_activate=lambda x: record_menu.toggle(),
on_deactivate=lambda x: recorder.stop_recording(),
active=recorder.bind("active"),
menu=record_menu,
)
@@ -0,0 +1,112 @@
import asyncio
from ignis.widgets import Widget
from ignis.utils import Utils
from ...qs_button import QSButton
from ...menu import Menu
from ignis.services.network import NetworkService, VpnConnection
network = NetworkService.get_default()
class VpnNetworkItem(Widget.Button):
def __init__(self, conn: VpnConnection):
super().__init__(
css_classes=["network-item", "unset"],
on_click=lambda x: asyncio.create_task(conn.toggle_connection()),
child=Widget.Box(
child=[
Widget.Label(
label=conn.name,
ellipsize="end",
max_width_chars=20,
halign="start",
),
Widget.Button(
child=Widget.Label(
label=conn.bind(
"is_connected",
lambda value: "Disconnect" if value else "Connect",
)
),
css_classes=["connect-label", "unset"],
halign="end",
hexpand=True,
),
]
),
)
class VpnMenu(Menu):
def __init__(self):
super().__init__(
name="vpn",
child=[
Widget.Box(
css_classes=["network-header-box"],
child=[
Widget.Icon(icon_name="network-vpn-symbolic", pixel_size=28),
Widget.Label(
label="VPN connections",
css_classes=["network-header-label"],
),
],
),
Widget.Box(
vertical=True,
child=network.vpn.bind(
"connections",
transform=lambda value: [VpnNetworkItem(i) for i in value],
),
),
Widget.Separator(),
Widget.Button(
css_classes=["network-item", "unset"],
on_click=lambda x: asyncio.create_task(Utils.exec_sh_async("nm-connection-editor")),
style="margin-bottom: 0;",
child=Widget.Box(
child=[
Widget.Icon(image="preferences-system-symbolic"),
Widget.Label(
label="Network Manager",
halign="start",
),
]
),
),
],
)
class VpnButton(QSButton):
def __init__(self):
menu = VpnMenu()
def get_label(id: str) -> str:
if id:
return id
else:
return "VPN"
def get_icon(icon_name: str) -> str:
if network.vpn.is_connected:
return icon_name
else:
return "network-vpn-symbolic"
super().__init__(
label=network.vpn.bind("active_vpn_id", get_label),
icon_name=network.vpn.bind("icon-name", get_icon),
on_activate=lambda x: menu.toggle(),
on_deactivate=lambda x: menu.toggle(),
active=network.vpn.bind("is-connected"),
menu=menu,
)
def vpn_control() -> list[QSButton]:
if len(network.vpn.connections) > 0:
return [VpnButton()]
else:
return []
@@ -0,0 +1,107 @@
import asyncio
from ignis.widgets import Widget
from ignis.utils import Utils
from ...qs_button import QSButton
from ...menu import Menu
from ....shared_widgets import ToggleBox
from ignis.services.network import NetworkService, WifiAccessPoint, WifiDevice
network = NetworkService.get_default()
class WifiNetworkItem(Widget.Button):
def __init__(self, access_point: WifiAccessPoint):
super().__init__(
css_classes=["network-item", "unset"],
on_click=lambda x: asyncio.create_task(access_point.connect_to_graphical()),
child=Widget.Box(
child=[
Widget.Icon(
image=access_point.bind(
"strength", transform=lambda value: access_point.icon_name
),
),
Widget.Label(
label=access_point.ssid,
halign="start",
),
Widget.Icon(
image="object-select-symbolic",
halign="end",
hexpand=True,
visible=access_point.bind("is_connected"),
),
]
),
)
class WifiMenu(Menu):
def __init__(self, device: WifiDevice):
super().__init__(
name="wifi",
child=[
ToggleBox(
label="Wi-Fi",
active=network.wifi.enabled,
on_change=lambda x, state: network.wifi.set_enabled(state),
css_classes=["network-header-box"],
),
Widget.Box(
vertical=True,
child=device.bind(
"access_points",
transform=lambda value: [WifiNetworkItem(i) for i in value],
),
),
Widget.Separator(),
Widget.Button(
css_classes=["network-item", "unset"],
on_click=lambda x: asyncio.create_task(Utils.exec_sh_async("nm-connection-editor")),
style="margin-bottom: 0;",
child=Widget.Box(
child=[
Widget.Icon(image="preferences-system-symbolic"),
Widget.Label(
label="Network Settings",
halign="start",
),
]
),
),
],
)
class WifiButton(QSButton):
def __init__(self, device: WifiDevice):
menu = WifiMenu(device)
def get_label(ssid: str) -> str:
if ssid:
return ssid
else:
return "Wi-Fi"
def get_icon(icon_name: str) -> str:
if device.ap.is_connected:
return icon_name
else:
return "network-wireless-symbolic"
def toggle_list(x) -> None:
asyncio.create_task(device.scan())
menu.toggle()
super().__init__(
label=device.ap.bind("ssid", get_label),
icon_name=device.ap.bind("icon-name", get_icon),
on_activate=toggle_list,
on_deactivate=toggle_list,
active=network.wifi.bind("enabled"),
menu=menu,
)
def wifi_control() -> list[QSButton]:
return [WifiButton(dev) for dev in network.wifi.devices]
@@ -0,0 +1,71 @@
import os
from ignis.widgets import Widget
from ignis.utils import Utils
from ignis.app import IgnisApp
from ignis.services.fetch import FetchService
from user_options import user_options
fetch = FetchService.get_default()
app = IgnisApp.get_default()
def format_uptime(value: tuple[int, int, int, int]) -> str:
days, hours, minutes, seconds = value
if days:
return f"up {days:02}:{hours:02}:{minutes:02}"
else:
return f"up {hours:02}:{minutes:02}"
class User(Widget.Box):
def __init__(self):
user_image = Widget.Picture(
image=user_options.user.bind(
"avatar",
lambda value: "user-info" if not os.path.exists(value) else value,
),
width=44,
height=44,
content_fit="cover",
style="border-radius: 10rem;",
)
username = Widget.Box(
child=[
Widget.Label(
label=os.getenv("USER"), css_classes=["user-name"], halign="start"
),
Widget.Label(
label=Utils.Poll(
timeout=60 * 1000, callback=lambda x: fetch.uptime
).bind("output", lambda value: format_uptime(value)),
halign="start",
css_classes=["user-name-secondary"],
),
],
vertical=True,
css_classes=["user-name-box"],
)
settings_button = Widget.Button(
child=Widget.Icon(image="emblem-system-symbolic", pixel_size=20),
halign="end",
hexpand=True,
css_classes=["user-settings", "unset"],
on_click=lambda x: self.__on_settings_button_click(),
)
power_button = Widget.Button(
child=Widget.Icon(image="system-shutdown-symbolic", pixel_size=20),
halign="end",
css_classes=["user-power", "unset"],
on_click=lambda x: app.toggle_window("ignis_POWERMENU"),
)
super().__init__(
child=[user_image, username, settings_button, power_button],
css_classes=["user"],
)
def __on_settings_button_click(self) -> None:
window = app.get_window("ignis_SETTINGS")
window.visible = not window.visible # type: ignore
@@ -0,0 +1,128 @@
import asyncio
from ignis.widgets import Widget
from ignis.utils import Utils
from ignis.services.audio import AudioService, Stream
from typing import Literal
from ..menu import Menu
from ...shared_widgets import MaterialVolumeSlider
audio = AudioService.get_default()
AUDIO_TYPES = {
"speaker": {"menu_icon": "audio-headphones-symbolic", "menu_label": "Sound Output"},
"microphone": {
"menu_icon": "microphone-sensitivity-high-symbolic",
"menu_label": "Sound Input",
},
}
class DeviceItem(Widget.Button):
def __init__(self, stream: Stream, _type: Literal["speaker", "microphone"]):
super().__init__(
child=Widget.Box(
child=[
Widget.Icon(image="audio-card-symbolic"),
Widget.Label(
label=stream.description,
ellipsize="end",
max_width_chars=30,
halign="start",
css_classes=["volume-entry-label"],
),
Widget.Icon(
image="object-select-symbolic",
halign="end",
hexpand=True,
visible=stream.bind("is_default"),
),
]
),
css_classes=["volume-entry", "unset"],
hexpand=True,
setup=lambda self: stream.connect("removed", lambda x: self.unparent()),
on_click=lambda x: setattr(audio, _type, stream),
)
class DeviceMenu(Menu):
def __init__(self, _type: Literal["speaker", "microphone"], style: str = ""):
data = AUDIO_TYPES[_type]
super().__init__(
name=f"volume-{_type}",
child=[
Widget.Box(
child=[
Widget.Icon(image=data["menu_icon"], pixel_size=24),
Widget.Label(
label=data["menu_label"],
halign="start",
css_classes=["volume-entry-list-header-label"],
),
],
css_classes=["volume-entry-list-header-box"],
),
Widget.Box(
vertical=True,
setup=lambda self: audio.connect(
f"{_type}-added",
lambda x, stream: self.append(DeviceItem(stream, _type)),
),
),
Widget.Separator(css_classes=["volume-entry-list-separator"]),
Widget.Button(
child=Widget.Box(
child=[
Widget.Icon(image="preferences-system-symbolic"),
Widget.Label(
label="Sound Settings",
halign="start",
css_classes=["volume-entry-label"],
),
]
),
css_classes=["volume-entry", "unset"],
style="margin-bottom: 0;",
on_click=lambda x: asyncio.create_task(Utils.exec_sh_async("pavucontrol")),
),
],
)
self.box.add_css_class(f"volume-menubox-{_type}")
class VolumeSlider(Widget.Box):
def __init__(self, _type: Literal["speaker", "microphone"]):
stream = getattr(audio, _type)
icon = Widget.Button(
child=Widget.Icon(
image=stream.bind("icon_name"),
pixel_size=18,
),
css_classes=["material-slider-icon", "unset", "hover-surface"],
on_click=lambda x: stream.set_is_muted(not stream.is_muted),
)
device_menu = DeviceMenu(_type=_type)
scale = MaterialVolumeSlider(
stream=stream,
on_change=lambda x: stream.set_volume(x.value),
sensitive=stream.bind("is_muted", lambda value: not value),
)
arrow = Widget.Button(
child=Widget.Arrow(pixel_size=20, rotated=device_menu.bind("reveal_child")),
css_classes=["material-slider-arrow", "hover-surface"],
on_click=lambda x: device_menu.toggle(),
)
super().__init__(
vertical=True,
child=[
Widget.Box(child=[icon, scale, arrow]),
device_menu,
],
css_classes=[f"volume-mainbox-{_type}"],
)