Backup dotfiles
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
from ignis.widgets import Widget
|
||||
from ignis.app import IgnisApp
|
||||
from .volume import volume_control
|
||||
from .quick_settings import quick_settings
|
||||
from .user import user
|
||||
from .media import media
|
||||
from .notification_center import notification_center
|
||||
from .brightness import brightness_slider
|
||||
|
||||
app = IgnisApp.get_default()
|
||||
|
||||
|
||||
def control_center_widget() -> Widget.Box:
|
||||
return Widget.Box(
|
||||
vertical=True,
|
||||
css_classes=["control-center"],
|
||||
child=[
|
||||
Widget.Box(
|
||||
vertical=True,
|
||||
css_classes=["control-center-widget"],
|
||||
child=[
|
||||
quick_settings(),
|
||||
volume_control(),
|
||||
brightness_slider(),
|
||||
user(),
|
||||
media(),
|
||||
],
|
||||
),
|
||||
notification_center(),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def control_center() -> Widget.RevealerWindow:
|
||||
revealer = Widget.Revealer(
|
||||
transition_type="slide_left",
|
||||
child=control_center_widget(),
|
||||
transition_duration=300,
|
||||
reveal_child=True,
|
||||
)
|
||||
box = Widget.Box(
|
||||
child=[
|
||||
Widget.Button(
|
||||
vexpand=True,
|
||||
hexpand=True,
|
||||
css_classes=["unset"],
|
||||
on_click=lambda x: app.close_window("ignis_CONTROL_CENTER"),
|
||||
),
|
||||
revealer,
|
||||
],
|
||||
)
|
||||
return Widget.RevealerWindow(
|
||||
visible=False,
|
||||
popup=True,
|
||||
kb_mode="on_demand",
|
||||
layer="top",
|
||||
css_classes=["unset"],
|
||||
anchor=["top", "right", "bottom", "left"],
|
||||
namespace="ignis_CONTROL_CENTER",
|
||||
child=box,
|
||||
revealer=revealer,
|
||||
)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,27 @@
|
||||
from ignis.services.backlight import BacklightService
|
||||
from ignis.widgets import Widget
|
||||
|
||||
backlight = BacklightService.get_default()
|
||||
|
||||
|
||||
def brightness_slider() -> Widget.Scale:
|
||||
return Widget.Box(
|
||||
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,14 @@
|
||||
from services.material import MaterialService
|
||||
from .qs_button import QSButton
|
||||
|
||||
material = MaterialService.get_default()
|
||||
|
||||
|
||||
def dark_mode_button() -> QSButton:
|
||||
return QSButton(
|
||||
label="Dark",
|
||||
icon_name="night-light-symbolic",
|
||||
on_activate=lambda x: material.set_dark_mode(True),
|
||||
on_deactivate=lambda x: material.set_dark_mode(False),
|
||||
active=material.bind("dark-mode"),
|
||||
)
|
||||
@@ -0,0 +1,19 @@
|
||||
from .qs_button import QSButton
|
||||
from ignis.services.notifications import NotificationService
|
||||
|
||||
notifications = NotificationService.get_default()
|
||||
|
||||
|
||||
def dnd_button() -> QSButton:
|
||||
return QSButton(
|
||||
label=notifications.bind("dnd", lambda value: "Silent" if value else "Noisy"),
|
||||
icon_name=notifications.bind(
|
||||
"dnd",
|
||||
transform=lambda value: "notification-disabled-symbolic"
|
||||
if value
|
||||
else "notification-symbolic",
|
||||
),
|
||||
on_activate=lambda x: notifications.set_dnd(not notifications.dnd),
|
||||
on_deactivate=lambda x: notifications.set_dnd(not notifications.dnd),
|
||||
active=notifications.bind("dnd"),
|
||||
)
|
||||
@@ -0,0 +1,100 @@
|
||||
from typing import List
|
||||
from ignis.widgets import Widget
|
||||
from ignis.utils import Utils
|
||||
from .qs_button import QSButton
|
||||
from ignis.services.network import NetworkService, EthernetDevice
|
||||
|
||||
network = NetworkService.get_default()
|
||||
|
||||
|
||||
class EthernetConnectionItem(Widget.Button):
|
||||
def __init__(self, device: EthernetDevice):
|
||||
super().__init__(
|
||||
css_classes=["ethernet-connection-item", "unset"],
|
||||
on_click=lambda x: device.disconnect_from()
|
||||
if device.is_connected
|
||||
else 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",
|
||||
css_classes=["ethernet-connection-label"],
|
||||
),
|
||||
Widget.Button(
|
||||
child=Widget.Label(
|
||||
label=device.bind(
|
||||
"is_connected",
|
||||
lambda value: "Disconnect" if value else "Connect",
|
||||
)
|
||||
),
|
||||
css_classes=["ethernet-connection-item-connect-label", "unset"],
|
||||
halign="end",
|
||||
hexpand=True,
|
||||
),
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def ethernet_control() -> List[QSButton]:
|
||||
networks_list = Widget.Revealer(
|
||||
transition_duration=300,
|
||||
transition_type="slide_down",
|
||||
child=Widget.Box(
|
||||
vertical=True,
|
||||
css_classes=["control-center-menu"],
|
||||
child=[
|
||||
Widget.Box(
|
||||
css_classes=["ethernet-header-box"],
|
||||
child=[
|
||||
Widget.Icon(icon_name="network-wired-symbolic", pixel_size=28),
|
||||
Widget.Label(
|
||||
label="Wired connections",
|
||||
css_classes=["ethernet-header-label"],
|
||||
),
|
||||
],
|
||||
),
|
||||
Widget.Box(
|
||||
vertical=True,
|
||||
child=network.ethernet.bind(
|
||||
"devices",
|
||||
lambda value: [EthernetConnectionItem(i) for i in value],
|
||||
),
|
||||
),
|
||||
Widget.Separator(css_classes=["ethernet-network-list-separator"]),
|
||||
Widget.Button(
|
||||
css_classes=["ethernet-connection-item", "unset"],
|
||||
style="margin-bottom: 0;",
|
||||
on_click=lambda x: Utils.exec_sh_async("nm-connection-editor"),
|
||||
child=Widget.Box(
|
||||
child=[
|
||||
Widget.Icon(image="preferences-system-symbolic"),
|
||||
Widget.Label(
|
||||
label="Network Settings",
|
||||
halign="start",
|
||||
css_classes=["ethernet-connection-label"],
|
||||
),
|
||||
]
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
if len(network.ethernet.devices) > 0:
|
||||
return [
|
||||
QSButton(
|
||||
label="Wired",
|
||||
icon_name="network-wired-symbolic",
|
||||
on_activate=lambda x: networks_list.toggle(),
|
||||
on_deactivate=lambda x: networks_list.toggle(),
|
||||
content=networks_list,
|
||||
active=network.ethernet.bind("is_connected"),
|
||||
)
|
||||
]
|
||||
else:
|
||||
return []
|
||||
@@ -0,0 +1,202 @@
|
||||
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() + "/../../scss/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": "", "firefox": "", "chrome": "", None: ""}
|
||||
|
||||
|
||||
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.Label(
|
||||
label=self.get_player_icon(),
|
||||
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 "chromium" in self._player.track_id or "chrome" in self._player.track_id:
|
||||
return PLAYER_ICONS["chrome"]
|
||||
else:
|
||||
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)
|
||||
|
||||
|
||||
def media() -> Widget.Box:
|
||||
def add_player(box: Widget.Box, obj: MprisPlayer) -> None:
|
||||
player = Player(obj)
|
||||
box.append(player)
|
||||
player.set_reveal_child(True)
|
||||
|
||||
return Widget.Box(
|
||||
vertical=True,
|
||||
setup=lambda self: mpris.connect(
|
||||
"player_added", lambda x, player: add_player(self, player)
|
||||
),
|
||||
css_classes=["rec-unset"],
|
||||
)
|
||||
@@ -0,0 +1,242 @@
|
||||
from ignis.widgets import Widget
|
||||
from ignis.services.notifications import Notification, NotificationService
|
||||
from ignis.utils import Utils
|
||||
from gi.repository import GLib # type: ignore
|
||||
|
||||
notifications = NotificationService.get_default()
|
||||
|
||||
|
||||
class ScreenshotLayout(Widget.Box):
|
||||
def __init__(self, notification: Notification) -> None:
|
||||
super().__init__(
|
||||
vertical=True,
|
||||
hexpand=True,
|
||||
child=[
|
||||
Widget.Box(
|
||||
child=[
|
||||
Widget.Picture(
|
||||
image=notification.icon,
|
||||
content_fit="cover",
|
||||
width=1920 // 7,
|
||||
height=1080 // 7,
|
||||
style="border-radius: 1rem; background-color: black;",
|
||||
),
|
||||
Widget.Button(
|
||||
child=Widget.Icon(
|
||||
image="window-close-symbolic", pixel_size=20
|
||||
),
|
||||
halign="end",
|
||||
valign="start",
|
||||
hexpand=True,
|
||||
css_classes=["notification-close"],
|
||||
on_click=lambda x: notification.close(),
|
||||
),
|
||||
],
|
||||
),
|
||||
Widget.Label(
|
||||
label="Screenshot saved",
|
||||
css_classes=["notification-screenshot-label"],
|
||||
),
|
||||
Widget.Box(
|
||||
homogeneous=True,
|
||||
style="margin-top: 0.75rem;",
|
||||
spacing=10,
|
||||
child=[
|
||||
Widget.Button(
|
||||
child=Widget.Label(label="Open"),
|
||||
css_classes=["notification-action"],
|
||||
on_click=lambda x: Utils.exec_sh_async(
|
||||
f"xdg-open {notification.icon}"
|
||||
),
|
||||
),
|
||||
Widget.Button(
|
||||
child=Widget.Label(label="Close"),
|
||||
css_classes=["notification-action"],
|
||||
on_click=lambda x: notification.close(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
class NormalLayout(Widget.Box):
|
||||
def __init__(self, notification: Notification) -> None:
|
||||
super().__init__(
|
||||
vertical=True,
|
||||
hexpand=True,
|
||||
child=[
|
||||
Widget.Box(
|
||||
child=[
|
||||
Widget.Icon(
|
||||
image=notification.icon
|
||||
if notification.icon
|
||||
else "dialog-information-symbolic",
|
||||
pixel_size=48,
|
||||
halign="start",
|
||||
valign="start",
|
||||
),
|
||||
Widget.Box(
|
||||
vertical=True,
|
||||
style="margin-left: 0.75rem;",
|
||||
child=[
|
||||
Widget.Label(
|
||||
ellipsize="end",
|
||||
label=notification.summary,
|
||||
halign="start",
|
||||
visible=notification.summary != "",
|
||||
css_classes=["notification-summary"],
|
||||
),
|
||||
Widget.Label(
|
||||
label=notification.body,
|
||||
ellipsize="end",
|
||||
halign="start",
|
||||
css_classes=["notification-body"],
|
||||
visible=notification.body != "",
|
||||
),
|
||||
],
|
||||
),
|
||||
Widget.Button(
|
||||
child=Widget.Icon(
|
||||
image="window-close-symbolic", pixel_size=20
|
||||
),
|
||||
halign="end",
|
||||
valign="start",
|
||||
hexpand=True,
|
||||
css_classes=["notification-close"],
|
||||
on_click=lambda x: notification.close(),
|
||||
),
|
||||
],
|
||||
),
|
||||
Widget.Box(
|
||||
child=[
|
||||
Widget.Button(
|
||||
child=Widget.Label(label=action.label),
|
||||
on_click=lambda x, action=action: action.invoke(),
|
||||
css_classes=["notification-action"],
|
||||
)
|
||||
for action in notification.actions
|
||||
],
|
||||
homogeneous=True,
|
||||
style="margin-top: 0.75rem;" if notification.actions else "",
|
||||
spacing=10,
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
class NotificationWidget(Widget.Box):
|
||||
def __init__(self, notification: Notification) -> None:
|
||||
layout: NormalLayout | ScreenshotLayout
|
||||
|
||||
if notification.app_name == "grimblast":
|
||||
layout = ScreenshotLayout(notification)
|
||||
else:
|
||||
layout = NormalLayout(notification)
|
||||
|
||||
super().__init__(
|
||||
css_classes=["notification"],
|
||||
child=[layout],
|
||||
)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def loading_notif_label() -> Widget.Label:
|
||||
return Widget.Label(
|
||||
label="Loading notifications...",
|
||||
valign="center",
|
||||
vexpand=True,
|
||||
css_classes=["notification-center-info-label"],
|
||||
)
|
||||
|
||||
|
||||
def no_notifications_label() -> Widget.Label:
|
||||
return Widget.Label(
|
||||
label="No notifications",
|
||||
valign="center",
|
||||
vexpand=True,
|
||||
visible=notifications.bind("notifications", lambda value: len(value) == 0),
|
||||
css_classes=["notification-center-info-label"],
|
||||
)
|
||||
|
||||
|
||||
def on_notified(box: Widget.Box, notification: Notification) -> None:
|
||||
notify = Popup(notification)
|
||||
box.prepend(notify)
|
||||
notify.reveal_child = True
|
||||
|
||||
|
||||
def load_notifications() -> list[Widget.Label | Popup]:
|
||||
widgets = []
|
||||
for i in notifications.notifications:
|
||||
GLib.idle_add(lambda i=i: widgets.append(Popup(i, reveal_child=True)))
|
||||
return widgets
|
||||
|
||||
|
||||
def notification_list() -> Widget.Box:
|
||||
box = Widget.Box(
|
||||
vertical=True,
|
||||
child=[loading_notif_label()],
|
||||
vexpand=True,
|
||||
css_classes=["rec-unset"],
|
||||
setup=lambda self: notifications.connect(
|
||||
"notified",
|
||||
lambda x, notification: on_notified(self, notification),
|
||||
),
|
||||
)
|
||||
|
||||
Utils.ThreadTask(
|
||||
load_notifications,
|
||||
lambda result: box.set_child(result + [no_notifications_label()]),
|
||||
).run()
|
||||
|
||||
return box
|
||||
|
||||
|
||||
def notification_center() -> Widget.Box:
|
||||
main_box = Widget.Box(
|
||||
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=notification_list(),
|
||||
vexpand=True,
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
return main_box
|
||||
@@ -0,0 +1,64 @@
|
||||
from ignis.widgets import Widget
|
||||
from gi.repository import GObject # type: ignore
|
||||
from typing import Callable
|
||||
from ignis.gobject import Binding
|
||||
|
||||
|
||||
class QSButton(Widget.Button):
|
||||
def __init__(
|
||||
self,
|
||||
label: str | Binding,
|
||||
icon_name: str | Binding,
|
||||
on_activate: Callable | None = None,
|
||||
on_deactivate: Callable | None = None,
|
||||
content: Widget.Revealer | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
self.on_activate = on_activate
|
||||
self.on_deactivate = on_deactivate
|
||||
self._active = False
|
||||
self._content = content
|
||||
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=content.bind("reveal_child"),
|
||||
)
|
||||
if content
|
||||
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 content(self) -> Widget.Revealer | None:
|
||||
return self._content
|
||||
@@ -0,0 +1,64 @@
|
||||
from ignis.widgets import Widget
|
||||
from .wifi import wifi_control
|
||||
from .record import record_control
|
||||
from .dnd import dnd_button
|
||||
from .dark_mode import dark_mode_button
|
||||
from .ethernet import ethernet_control
|
||||
from .vpn import vpn_control
|
||||
from .qs_button import QSButton
|
||||
from ignis.services.network import NetworkService
|
||||
|
||||
network = NetworkService.get_default()
|
||||
|
||||
|
||||
def add_button(main_box: Widget.Box, buttons: tuple[QSButton, ...], i: int) -> None:
|
||||
row = Widget.Box(homogeneous=True)
|
||||
if len(main_box.child) > 0:
|
||||
row.style = "margin-top: 0.5rem;"
|
||||
|
||||
main_box.append(row)
|
||||
|
||||
button1 = buttons[i]
|
||||
row.append(button1)
|
||||
|
||||
if button1.content:
|
||||
main_box.append(button1.content)
|
||||
|
||||
if i + 1 < len(buttons):
|
||||
button2 = buttons[i + 1]
|
||||
button2.style = "margin-left: 0.5rem;"
|
||||
row.append(button2)
|
||||
|
||||
if button2.content:
|
||||
main_box.append(button2.content)
|
||||
|
||||
|
||||
def qs_fabric(main_box: Widget.Box, *buttons: QSButton) -> None:
|
||||
for i in range(0, len(buttons), 2):
|
||||
add_button(main_box, buttons, i)
|
||||
|
||||
|
||||
def qs_config(main_box: Widget.Box) -> None:
|
||||
qs_fabric(
|
||||
main_box,
|
||||
*wifi_control(),
|
||||
*ethernet_control(),
|
||||
*vpn_control(),
|
||||
dnd_button(),
|
||||
dark_mode_button(),
|
||||
record_control(),
|
||||
)
|
||||
|
||||
|
||||
def update_box(main_box: Widget.Box):
|
||||
main_box.child = []
|
||||
qs_config(main_box)
|
||||
|
||||
|
||||
def quick_settings() -> Widget.Box:
|
||||
main_box = Widget.Box(vertical=True, css_classes=["qs-main-box"])
|
||||
network.wifi.connect("notify::devices", lambda x, y: update_box(main_box))
|
||||
network.ethernet.connect("notify::devices", lambda x, y: update_box(main_box))
|
||||
network.vpn.connect("notify::connections", lambda x, y: update_box(main_box))
|
||||
|
||||
return main_box
|
||||
@@ -0,0 +1,100 @@
|
||||
from ignis.widgets import Widget
|
||||
from .qs_button import QSButton
|
||||
from ignis.services.recorder import RecorderService
|
||||
|
||||
recorder = RecorderService.get_default()
|
||||
|
||||
|
||||
def record_control() -> QSButton:
|
||||
record_audio_switch = Widget.Switch(halign="end", hexpand=True, valign="center")
|
||||
dropdown = Widget.DropDown(
|
||||
items=["Internal audio", "Microphone", "Both sources"],
|
||||
css_classes=["record-dropdown"],
|
||||
)
|
||||
|
||||
def start_recording(record_menu: Widget.Revealer) -> None:
|
||||
record_menu.set_reveal_child(False)
|
||||
microphone = False
|
||||
internal = False
|
||||
if record_audio_switch.active:
|
||||
if dropdown.selected == "Internal audio":
|
||||
internal = True
|
||||
elif dropdown.selected == "Microphone":
|
||||
microphone = True
|
||||
else:
|
||||
internal = True
|
||||
microphone = True
|
||||
|
||||
recorder.start_recording(
|
||||
record_microphone=microphone, record_internal_audio=internal
|
||||
)
|
||||
|
||||
record_menu = Widget.Revealer(
|
||||
transition_duration=300,
|
||||
transition_type="slide_down",
|
||||
child=Widget.Box(
|
||||
css_classes=["record-menu"],
|
||||
vertical=True,
|
||||
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",
|
||||
),
|
||||
dropdown,
|
||||
],
|
||||
),
|
||||
record_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: record_menu.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: start_recording(record_menu), # type: ignore
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
return QSButton(
|
||||
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"),
|
||||
content=record_menu,
|
||||
)
|
||||
@@ -0,0 +1,76 @@
|
||||
import os
|
||||
from ignis.widgets import Widget
|
||||
from ignis.utils import Utils
|
||||
from ignis.app import IgnisApp
|
||||
from ignis.services.fetch import FetchService
|
||||
from ..settings import settings_window
|
||||
from options import avatar_opt
|
||||
|
||||
fetch = FetchService.get_default()
|
||||
app = IgnisApp.get_default()
|
||||
|
||||
|
||||
def format_uptime(value):
|
||||
days, hours, minutes, seconds = value
|
||||
if days:
|
||||
return f"up {days:02}:{hours:02}:{minutes:02}"
|
||||
else:
|
||||
return f"up {hours:02}:{minutes:02}"
|
||||
|
||||
|
||||
def user_image() -> Widget.Picture:
|
||||
return Widget.Picture(
|
||||
image=avatar_opt.bind(
|
||||
"value",
|
||||
lambda value: "user-info" if not os.path.exists(value) else value,
|
||||
),
|
||||
width=44,
|
||||
height=44,
|
||||
content_fit="cover",
|
||||
style="border-radius: 10rem;",
|
||||
)
|
||||
|
||||
|
||||
def username() -> Widget.Box:
|
||||
return 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"],
|
||||
)
|
||||
|
||||
|
||||
def settings_button() -> Widget.Button:
|
||||
return 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: settings_window(),
|
||||
)
|
||||
|
||||
|
||||
def power_button() -> Widget.Button:
|
||||
return 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"),
|
||||
)
|
||||
|
||||
|
||||
def user() -> Widget.Box:
|
||||
return Widget.Box(
|
||||
child=[user_image(), username(), settings_button(), power_button()],
|
||||
css_classes=["user"],
|
||||
)
|
||||
@@ -0,0 +1,161 @@
|
||||
from ignis.widgets import Widget
|
||||
from ignis.utils import Utils
|
||||
from ignis.services.audio import AudioService, Stream
|
||||
|
||||
audio = AudioService.get_default()
|
||||
|
||||
|
||||
def volume_scale(stream: Stream) -> Widget.Scale:
|
||||
return Widget.Scale(
|
||||
css_classes=["material-slider"],
|
||||
value=stream.bind("volume"),
|
||||
step=5,
|
||||
hexpand=True,
|
||||
on_change=lambda x: stream.set_volume(x.value),
|
||||
sensitive=stream.bind("is_muted", lambda value: not value),
|
||||
)
|
||||
|
||||
|
||||
def device_entry(stream: Stream, _type: str) -> Widget.Button:
|
||||
widget = Widget.Button(
|
||||
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,
|
||||
on_click=lambda x: setattr(audio, _type, stream),
|
||||
)
|
||||
stream.connect("removed", lambda x: widget.unparent())
|
||||
|
||||
return widget
|
||||
|
||||
|
||||
def device_list(
|
||||
header_label: str, header_icon: str, _type: str, **kwargs
|
||||
) -> Widget.Revealer:
|
||||
box = Widget.Box(
|
||||
css_classes=["control-center-menu"],
|
||||
vertical=True,
|
||||
child=[
|
||||
Widget.Box(
|
||||
child=[
|
||||
Widget.Icon(image=header_icon, pixel_size=24),
|
||||
Widget.Label(
|
||||
label=header_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(device_entry(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: Utils.exec_sh_async("pavucontrol"),
|
||||
),
|
||||
],
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
return Widget.Revealer(
|
||||
child=box, transition_type="slide_down", transition_duration=300
|
||||
)
|
||||
|
||||
|
||||
def volume_icon(stream: Stream) -> Widget.Button:
|
||||
return 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),
|
||||
)
|
||||
|
||||
|
||||
def device_list_arrow(device_list: Widget.Revealer) -> Widget.Button:
|
||||
return Widget.ArrowButton(
|
||||
arrow=Widget.Arrow(pixel_size=20),
|
||||
css_classes=["material-slider-arrow", "hover-surface"],
|
||||
on_click=lambda x: device_list.toggle(),
|
||||
)
|
||||
|
||||
|
||||
def volume_control():
|
||||
speakers_list = device_list(
|
||||
header_label="Sound Output",
|
||||
header_icon="audio-headphones-symbolic",
|
||||
_type="speaker",
|
||||
style="margin-bottom: 1rem;",
|
||||
)
|
||||
|
||||
microphones_list = device_list(
|
||||
header_label="Sound Input",
|
||||
header_icon="microphone-sensitivity-high-symbolic",
|
||||
_type="microphone",
|
||||
)
|
||||
|
||||
speaker_icon = volume_icon(audio.speaker)
|
||||
microphone_icon = volume_icon(audio.microphone)
|
||||
|
||||
speaker_scale = volume_scale(audio.speaker)
|
||||
microphone_scale = volume_scale(audio.microphone)
|
||||
|
||||
speaker_arrow = device_list_arrow(speakers_list)
|
||||
microphone_arrow = device_list_arrow(microphones_list)
|
||||
|
||||
speaker_control = Widget.Box(
|
||||
vertical=True,
|
||||
child=[
|
||||
Widget.Box(child=[speaker_icon, speaker_scale, speaker_arrow]),
|
||||
speakers_list,
|
||||
],
|
||||
style="margin-top: 1rem;",
|
||||
)
|
||||
|
||||
microphone_control = Widget.Box(
|
||||
vertical=True,
|
||||
child=[
|
||||
Widget.Box(child=[microphone_icon, microphone_scale, microphone_arrow]),
|
||||
microphones_list,
|
||||
],
|
||||
style="margin-top: 0.25rem;",
|
||||
)
|
||||
|
||||
return Widget.Box(
|
||||
vertical=True,
|
||||
child=[speaker_control, microphone_control],
|
||||
)
|
||||
@@ -0,0 +1,120 @@
|
||||
from ignis.widgets import Widget
|
||||
from ignis.utils import Utils
|
||||
from .qs_button import QSButton
|
||||
from typing import List
|
||||
from ignis.services.network import NetworkService, VpnConnection
|
||||
|
||||
|
||||
network = NetworkService.get_default()
|
||||
|
||||
|
||||
class VpnNetworkItem(Widget.Button):
|
||||
def __init__(self, conn: VpnConnection):
|
||||
|
||||
super().__init__(
|
||||
css_classes=["vpn-connection-item", "unset"],
|
||||
on_click=lambda x: conn.toggle_connection(),
|
||||
child=Widget.Box(
|
||||
child=[
|
||||
Widget.Label(
|
||||
label=conn.name,
|
||||
ellipsize="end",
|
||||
max_width_chars=20,
|
||||
halign="start",
|
||||
css_classes=["vpn-connection-label"],
|
||||
),
|
||||
Widget.Button(
|
||||
child=Widget.Label(
|
||||
label=conn.bind(
|
||||
"is_connected",
|
||||
lambda value: "Disconnect" if value else "Connect",
|
||||
)
|
||||
),
|
||||
css_classes=["vpn-connection-item-connect-label", "unset"],
|
||||
halign="end",
|
||||
hexpand=True,
|
||||
),
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def vpn_qsbutton() -> QSButton:
|
||||
networks_list = Widget.Revealer(
|
||||
transition_duration=300,
|
||||
transition_type="slide_down",
|
||||
child=Widget.Box(
|
||||
vertical=True,
|
||||
css_classes=["control-center-menu"],
|
||||
child=[
|
||||
Widget.Box(
|
||||
css_classes=["vpn-header-box"],
|
||||
child=[
|
||||
Widget.Icon(
|
||||
icon_name="network-vpn-symbolic", pixel_size=28),
|
||||
Widget.Label(
|
||||
label="VPN connections",
|
||||
css_classes=["vpn-header-label"],
|
||||
),
|
||||
],
|
||||
),
|
||||
Widget.Box(
|
||||
vertical=True,
|
||||
child=network.vpn.bind(
|
||||
"connections",
|
||||
transform=lambda value: [
|
||||
VpnNetworkItem(i) for i in value],
|
||||
),
|
||||
),
|
||||
Widget.Separator(
|
||||
css_classes=["vpn-connection-list-separator"]),
|
||||
Widget.Button(
|
||||
css_classes=["vpn-connection-item", "unset"],
|
||||
on_click=lambda x: 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",
|
||||
css_classes=["vpn-connection-label"],
|
||||
),
|
||||
]
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
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"
|
||||
|
||||
def toggle_list(x) -> None:
|
||||
networks_list.toggle()
|
||||
|
||||
return QSButton(
|
||||
label=network.vpn.bind("active_vpn_id", get_label),
|
||||
icon_name=network.vpn.bind("icon-name", get_icon),
|
||||
on_activate=toggle_list,
|
||||
on_deactivate=toggle_list,
|
||||
active=network.vpn.bind("is-connected"),
|
||||
content=networks_list,
|
||||
)
|
||||
|
||||
|
||||
def vpn_control() -> List[QSButton]:
|
||||
if len(network.vpn.connections) > 0:
|
||||
return [vpn_qsbutton()]
|
||||
else:
|
||||
return []
|
||||
@@ -0,0 +1,112 @@
|
||||
from ignis.widgets import Widget
|
||||
from ignis.utils import Utils
|
||||
from .qs_button import QSButton
|
||||
from ignis.services.network import NetworkService, WifiAccessPoint, WifiDevice
|
||||
from typing import List
|
||||
|
||||
network = NetworkService.get_default()
|
||||
|
||||
|
||||
class WifiNetworkItem(Widget.Button):
|
||||
def __init__(self, access_point: WifiAccessPoint):
|
||||
super().__init__(
|
||||
css_classes=["wifi-network-item", "unset"],
|
||||
on_click=lambda x: 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",
|
||||
css_classes=["wifi-network-label"],
|
||||
),
|
||||
Widget.Icon(
|
||||
image="object-select-symbolic",
|
||||
halign="end",
|
||||
hexpand=True,
|
||||
visible=access_point.bind("is_connected"),
|
||||
),
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def wifi_qsbutton(device: WifiDevice) -> QSButton:
|
||||
networks_list = Widget.Revealer(
|
||||
transition_duration=300,
|
||||
transition_type="slide_down",
|
||||
child=Widget.Box(
|
||||
vertical=True,
|
||||
css_classes=["control-center-menu"],
|
||||
child=[
|
||||
Widget.Box(
|
||||
child=[
|
||||
Widget.Label(label="Wi-Fi", css_classes=["wifi-header-label"]),
|
||||
Widget.Switch(
|
||||
halign="end",
|
||||
hexpand=True,
|
||||
active=network.wifi.enabled,
|
||||
on_change=lambda x, state: network.wifi.set_enabled(state),
|
||||
),
|
||||
],
|
||||
css_classes=["toggle-box", "wifi-header-box"],
|
||||
),
|
||||
Widget.Box(
|
||||
vertical=True,
|
||||
child=device.bind(
|
||||
"access_points",
|
||||
transform=lambda value: [WifiNetworkItem(i) for i in value],
|
||||
),
|
||||
),
|
||||
Widget.Separator(css_classes=["wifi-network-list-separator"]),
|
||||
Widget.Button(
|
||||
css_classes=["wifi-network-item", "unset"],
|
||||
on_click=lambda x: 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",
|
||||
css_classes=["wifi-network-label"],
|
||||
),
|
||||
]
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
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:
|
||||
device.scan()
|
||||
networks_list.toggle()
|
||||
|
||||
return QSButton(
|
||||
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"),
|
||||
content=networks_list,
|
||||
)
|
||||
|
||||
|
||||
def wifi_control() -> List[QSButton]:
|
||||
return [wifi_qsbutton(dev) for dev in network.wifi.devices]
|
||||
Reference in New Issue
Block a user