[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
+17
View File
@@ -0,0 +1,17 @@
from .bar import Bar
from .control_center import ControlCenter
from .launcher import Launcher
from .notification_popup import NotificationPopup
from .osd import OSD
from .powermenu import Powermenu
from .settings import Settings
__all__ = [
"Bar",
"ControlCenter",
"Launcher",
"NotificationPopup",
"OSD",
"Powermenu",
"Settings",
]
Binary file not shown.
+2 -24
View File
@@ -1,25 +1,3 @@
from ignis.widgets import Widget
from .workspaces import workspaces
from .kb_layout import kb_layout
from .clock import clock
from .pinned_apps import pinned_apps
from .tray import tray
from .battery import battery_widget
# from .wallpaper_engine import wallpaper_engine_widget
from .bar import Bar
def bar(monitor: int) -> Widget.Window:
return Widget.Window(
anchor=["left", "top", "right"],
exclusivity="exclusive",
monitor=monitor,
namespace=f"ignis_BAR_{monitor}",
layer="top",
kb_mode="none",
child=Widget.CenterBox(
css_classes=["bar-widget"],
start_widget=Widget.Box(child=[workspaces()]),
center_widget=Widget.Box(child=[pinned_apps()]),
end_widget=Widget.Box(child=[tray(), kb_layout(), battery_widget(), clock(monitor)]),
), # wallpaper_engine_widget(),
css_classes=["unset"],
)
__all__ = ["Bar"]
Binary file not shown.
+25
View File
@@ -0,0 +1,25 @@
from ignis.widgets import Widget
from .widgets import StatusPill, Tray, KeyboardLayout, Battery, Apps, Workspaces
class Bar(Widget.Window):
__gtype_name__ = "Bar"
def __init__(self, monitor: int):
super().__init__(
anchor=["left", "top", "right"],
exclusivity="exclusive",
monitor=monitor,
namespace=f"ignis_BAR_{monitor}",
layer="top",
kb_mode="none",
child=Widget.CenterBox(
css_classes=["bar-widget"],
start_widget=Widget.Box(child=[Workspaces()]),
center_widget=Widget.Box(child=[Apps()]),
end_widget=Widget.Box(
child=[Tray(), KeyboardLayout(), Battery(), StatusPill(monitor)]
),
),
css_classes=["unset"],
)
+31
View File
@@ -0,0 +1,31 @@
from ignis.widgets import Widget
from ignis.services.network import Ethernet, Wifi
class IndicatorIcon(Widget.Icon):
def __init__(self, css_classes: list[str] = [], **kwargs):
super().__init__(
style="margin-right: 0.5rem;", css_classes=["unset"] + css_classes, **kwargs
)
class NetworkIndicatorIcon(IndicatorIcon):
def __init__(
self, device_type: Ethernet | Wifi, other_device_type: Wifi | Ethernet
):
self._device_type = device_type
self._other_device_type = other_device_type
super().__init__(icon_name=device_type.bind("icon-name"))
for binding in (
device_type.bind("devices", self.__check_visibility),
other_device_type.bind("is_connected", self.__check_visibility),
device_type.bind("is_connected", self.__check_visibility),
):
self.visible = binding # type: ignore
def __check_visibility(self, *args) -> bool:
return len(self._device_type.devices) > 0 and (
not self._other_device_type.is_connected or self._device_type.is_connected
)
+8
View File
@@ -0,0 +1,8 @@
from .pill import StatusPill
from .tray import Tray
from .kb_layout import KeyboardLayout
from .battery import Battery
from .apps import Apps
from .workspaces import Workspaces
__all__ = ["StatusPill", "Tray", "KeyboardLayout", "Battery", "Apps", "Workspaces"]
+52
View File
@@ -0,0 +1,52 @@
from ignis.widgets import Widget
from ignis.app import IgnisApp
from ignis.services.applications import ApplicationsService, Application
applications = ApplicationsService.get_default()
app = IgnisApp.get_default()
TERMINAL_FORMAT = "kitty %command%"
class AppItem(Widget.Button):
def __init__(self, app: Application):
menu = Widget.PopoverMenu(
items=[
Widget.MenuItem(label="Launch", on_activate=lambda x: app.launch()),
Widget.Separator(),
]
+ [
Widget.MenuItem(
label=i.name, on_activate=lambda x, action=i: action.launch()
)
for i in app.actions
]
+ [
Widget.Separator(),
Widget.MenuItem(label="Unpin", on_activate=lambda x: app.unpin()),
]
)
super().__init__(
child=Widget.Box(child=[Widget.Icon(image=app.icon, pixel_size=32), menu]),
on_click=lambda x: app.launch(terminal_format=TERMINAL_FORMAT),
on_right_click=lambda x: menu.popup(),
css_classes=["pinned-app", "unset"],
)
class Apps(Widget.Box):
def __init__(self):
super().__init__(
child=applications.bind(
"pinned",
transform=lambda value: [AppItem(app) for app in value]
+ [
Widget.Button(
child=Widget.Icon(image="start-here-symbolic", pixel_size=32),
on_click=lambda x: app.toggle_window("ignis_LAUNCHER"),
css_classes=["pinned-app", "unset"],
)
],
)
)
+37
View File
@@ -0,0 +1,37 @@
from ignis.widgets import Widget
from ignis.services.upower import UPowerService, UPowerDevice
upower = UPowerService.get_default()
class BatteryItem(Widget.Box):
def __init__(self, device: UPowerDevice):
super().__init__(
css_classes=["battery-item"],
setup=lambda self: device.connect("removed", lambda x: self.unparent()),
child=[
Widget.Icon(
icon_name=device.bind("icon_name"), css_classes=["battery-icon"]
),
Widget.Label(
label=device.bind("percent", lambda x: f"{int(x)}%"),
css_classes=["battery-percent"],
),
Widget.Scale(
min=0,
max=100,
value=device.bind("percent"),
sensitive=False,
css_classes=["battery-scale"],
),
],
)
class Battery(Widget.Box):
def __init__(self):
super().__init__(
setup=lambda self: upower.connect(
"battery-added", lambda x, device: self.append(BatteryItem(device))
),
)
+18
View File
@@ -0,0 +1,18 @@
from ignis.widgets import Widget
from ignis.services.hyprland import HyprlandService
hyprland = HyprlandService.get_default()
class KeyboardLayout(Widget.Button):
def __init__(self):
super().__init__(
css_classes=["kb-layout", "unset"],
on_click=lambda x: hyprland.main_keyboard.switch_layout("1"),
visible=hyprland.is_available,
child=Widget.Label(
label=hyprland.main_keyboard.bind(
"active_keymap", transform=lambda value: value[:2].lower()
)
),
)
+111
View File
@@ -0,0 +1,111 @@
import datetime
from ignis.widgets import Widget
from ignis.app import IgnisApp
from ignis.utils import Utils
from ignis.variable import Variable
from ignis.services.network import NetworkService
from ignis.services.notifications import NotificationService
from ignis.services.recorder import RecorderService
from ignis.services.audio import AudioService
from ..indicator_icon import IndicatorIcon, NetworkIndicatorIcon
from ignis.options import options
network = NetworkService.get_default()
notifications = NotificationService.get_default()
recorder = RecorderService.get_default()
audio = AudioService.get_default()
app = IgnisApp.get_default()
current_time = Variable(
value=Utils.Poll(1000, lambda x: datetime.datetime.now().strftime("%H:%M:%S")).bind(
"output"
)
)
class WifiIcon(NetworkIndicatorIcon):
def __init__(self):
super().__init__(device_type=network.wifi, other_device_type=network.ethernet)
class EthernetIcon(NetworkIndicatorIcon):
def __init__(self):
super().__init__(device_type=network.ethernet, other_device_type=network.wifi)
class VpnIcon(IndicatorIcon):
def __init__(self):
super().__init__(
image=network.vpn.bind("icon_name"),
visible=network.vpn.bind("is_connected"),
)
class DNDIcon(IndicatorIcon):
def __init__(self):
super().__init__(
image="notification-disabled-symbolic",
visible=options.notifications.bind("dnd"),
)
class RecorderIcon(IndicatorIcon):
def __init__(self):
super().__init__(
image="media-record-symbolic",
css_classes=["record-indicator"],
setup=lambda self: recorder.connect(
"notify::is-paused", self.__update_css_class
),
visible=recorder.bind("active"),
)
def __update_css_class(self, *args) -> None:
if recorder.is_paused:
self.remove_css_class("active")
else:
self.add_css_class("active")
class VolumeIcon(IndicatorIcon):
def __init__(self):
super().__init__(
image=audio.speaker.bind("icon_name"),
)
class StatusPill(Widget.Button):
def __init__(self, monitor: int):
self._monitor = monitor
self._window: Widget.Window = app.get_window("ignis_CONTROL_CENTER") # type: ignore
super().__init__(
child=Widget.Box(
child=[
RecorderIcon(),
WifiIcon(),
EthernetIcon(),
VpnIcon(),
VolumeIcon(),
DNDIcon(),
Widget.Label(
label=current_time.bind("value"),
),
]
),
css_classes=self._window.bind(
"visible",
lambda value: ["clock", "unset", "active"]
if value
else ["clock", "unset"],
),
on_click=self.__on_click,
)
def __on_click(self, x) -> None:
if self._window.monitor == self._monitor:
self._window.visible = not self._window.visible
else:
self._window.set_monitor(self._monitor)
self._window.visible = True
+42
View File
@@ -0,0 +1,42 @@
import asyncio
from ignis.widgets import Widget
from ignis.services.system_tray import SystemTrayService, SystemTrayItem
system_tray = SystemTrayService.get_default()
class TrayItem(Widget.Button):
__gtype_name__ = "TrayItem"
def __init__(self, item: SystemTrayItem):
if item.menu:
menu = item.menu.copy()
else:
menu = None
super().__init__(
child=Widget.Box(
child=[
Widget.Icon(image=item.bind("icon"), pixel_size=24),
menu,
]
),
tooltip_text=item.bind("tooltip"),
on_click=lambda x: asyncio.create_task(item.activate_async()),
setup=lambda self: item.connect("removed", lambda x: self.unparent()),
on_right_click=lambda x: menu.popup() if menu else None,
css_classes=["tray-item", "unset"],
)
class Tray(Widget.Box):
__gtype_name__ = "Tray"
def __init__(self):
super().__init__(
css_classes=["tray"],
setup=lambda self: system_tray.connect(
"added", lambda x, item: self.append(TrayItem(item))
),
spacing=10,
)
+49
View File
@@ -0,0 +1,49 @@
from ignis.widgets import Widget
from ignis.services.hyprland import HyprlandService, HyprlandWorkspace
hyprland = HyprlandService.get_default()
class WorkspaceButton(Widget.Button):
def __init__(self, workspace: HyprlandWorkspace) -> None:
super().__init__(
css_classes=["workspace", "unset"],
on_click=lambda x: workspace.switch_to(),
halign="start",
valign="center",
)
if workspace.id == hyprland.active_workspace.id:
self.add_css_class("active")
def scroll_workspaces(direction: str) -> None:
current = hyprland.active_workspace.id
if direction == "up":
target = current - 1
hyprland.switch_to_workspace(target)
else:
target = current + 1
if target == 11:
return
hyprland.switch_to_workspace(target)
class Workspaces(Widget.Box):
def __init__(self):
if hyprland.is_available:
child = [
Widget.EventBox(
on_scroll_up=lambda x: scroll_workspaces("up"),
on_scroll_down=lambda x: scroll_workspaces("down"),
css_classes=["workspaces"],
child=hyprland.bind_many(
["workspaces", "active_workspace"],
transform=lambda workspaces, *_: [
WorkspaceButton(i) for i in workspaces
],
),
)
]
else:
child = []
super().__init__(child=child)
+2 -61
View File
@@ -1,62 +1,3 @@
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
from .control_center import ControlCenter
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,
)
__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,
)
+34
View File
@@ -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
+7 -6
View File
@@ -2,6 +2,7 @@ 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):
@@ -11,13 +12,13 @@ class QSButton(Widget.Button):
icon_name: str | Binding,
on_activate: Callable | None = None,
on_deactivate: Callable | None = None,
content: Widget.Revealer | None = None,
menu: Menu | None = None,
**kwargs,
):
self.on_activate = on_activate
self.on_deactivate = on_deactivate
self._active = False
self._content = content
self._menu = menu
super().__init__(
child=Widget.Box(
child=[
@@ -27,9 +28,9 @@ class QSButton(Widget.Button):
halign="end",
hexpand=True,
pixel_size=20,
rotated=content.bind("reveal_child"),
rotated=menu.bind("reveal_child"),
)
if content
if menu
else None,
]
),
@@ -60,5 +61,5 @@ class QSButton(Widget.Button):
self.remove_css_class("active")
@GObject.Property
def content(self) -> Widget.Revealer | None:
return self._content
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,29 @@
import asyncio
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: asyncio.create_task(backlight.set_brightness_async(x.value)),
),
],
)
@@ -0,0 +1,211 @@
import os
import ignis
import asyncio
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: asyncio.create_task(player.play_pause_async()),
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: asyncio.create_task(self._player.set_position_async(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: asyncio.create_task(player.previous_async()),
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: asyncio.create_task(player.next_async()),
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}"],
)
+2 -211
View File
@@ -1,212 +1,3 @@
import re
from ignis.widgets import Widget
from ignis.app import IgnisApp
from ignis.services.applications import (
ApplicationsService,
Application,
ApplicationAction,
)
from ignis.utils import Utils
from gi.repository import Gio # type: ignore
from .launcher import Launcher
app = IgnisApp.get_default()
applications = ApplicationsService.get_default()
def is_url(url: str) -> bool:
regex = re.compile(
r"^(?:http|ftp)s?://" # http:// or https://
r"(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|" # domain
r"localhost|" # localhost
r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|" # or ipv4
r"\[?[A-F0-9]*:[A-F0-9:]+\]?)" # or ipv6
r"(?::\d+)?" # optional port
r"(?:/?|[/?]\S+)$",
re.IGNORECASE,
)
return re.match(regex, url) is not None
class LauncherAppItem(Widget.Button):
def __init__(self, application: Application) -> None:
self._application = application
super().__init__(
on_click=lambda x: self.launch(),
on_right_click=lambda x: self._menu.popup(),
css_classes=["launcher-app"],
child=Widget.Box(
child=[
Widget.Icon(image=application.icon, pixel_size=48),
Widget.Label(
label=application.name,
ellipsize="end",
max_width_chars=30,
css_classes=["launcher-app-label"],
),
]
),
)
self.__sync_menu()
application.connect("notify::is-pinned", lambda x, y: self.__sync_menu())
def launch(self) -> None:
self._application.launch()
app.close_window("ignis_LAUNCHER")
def launch_action(self, action: ApplicationAction) -> None:
action.launch()
app.close_window("ignis_LAUNCHER")
def __sync_menu(self) -> None:
self._menu = Widget.PopoverMenu(
items=[
Widget.MenuItem(label="Launch", on_activate=lambda x: self.launch()),
Widget.Separator(),
]
+ [
Widget.MenuItem(
label=i.name,
on_activate=lambda x, action=i: self.launch_action(action),
)
for i in self._application.actions
]
+ [
Widget.Separator(),
Widget.MenuItem(
label="Pin", on_activate=lambda x: self._application.pin()
)
if not self._application.is_pinned
else Widget.MenuItem(
label="Unpin", on_activate=lambda x: self._application.unpin()
),
]
)
self.child.append(self._menu)
class SearchWebButton(Widget.Button):
def __init__(self, query: str):
self._query = query
self._url = ""
browser_desktop_file = Utils.exec_sh(
"xdg-settings get default-web-browser"
).stdout.replace("\n", "")
app_info = Gio.DesktopAppInfo.new(desktop_id=browser_desktop_file)
icon_name = "applications-internet-symbolic"
if app_info:
icon_string = app_info.get_string("Icon")
if icon_string:
icon_name = icon_string
if not query.startswith(("http://", "https://")) and "." in query:
query = "https://" + query
if is_url(query):
label = f"Visit {query}"
self._url = query
else:
label = "Search in Google"
self._url = f"https://www.google.com/search?q={query.replace(' ', '+')}"
super().__init__(
on_click=lambda x: self.launch(),
css_classes=["launcher-app"],
child=Widget.Box(
child=[
Widget.Icon(image=icon_name, pixel_size=48),
Widget.Label(
label=label,
css_classes=["launcher-app-label"],
),
]
),
)
def launch(self) -> None:
Utils.exec_sh_async(f"xdg-open {self._url}")
app.close_window("ignis_LAUNCHER")
def launcher() -> Widget.Window:
def search(entry: Widget.Entry, app_list: Widget.Box) -> None:
query = entry.text
if query == "":
entry.grab_focus()
app_list.visible = False
return
apps = applications.search(applications.apps, query)
if apps == []:
app_list.child = [SearchWebButton(query)]
else:
app_list.visible = True
app_list.child = [LauncherAppItem(i) for i in apps[:5]]
def on_open(window: Widget.Window, entry: Widget.Entry) -> None:
if not window.visible:
return
entry.text = ""
entry.grab_focus()
app_list = Widget.Box(vertical=True, visible=False, style="margin-top: 1rem;")
entry = Widget.Entry(
hexpand=True,
placeholder_text="Search",
css_classes=["launcher-search"],
on_change=lambda x: search(x, app_list),
on_accept=lambda x: app_list.child[0].launch()
if len(app_list.child) > 0
else None,
)
main_box = Widget.Box(
vertical=True,
valign="start",
halign="center",
css_classes=["launcher"],
child=[
Widget.Box(
css_classes=["launcher-search-box"],
child=[
Widget.Icon(
icon_name="system-search-symbolic",
pixel_size=24,
style="margin-right: 0.5rem;",
),
entry,
],
),
app_list,
],
)
return Widget.Window(
namespace="ignis_LAUNCHER",
visible=False,
popup=True,
kb_mode="on_demand",
# exclusivity="ignore",
css_classes=["unset"],
setup=lambda self: self.connect(
"notify::visible", lambda x, y: on_open(self, entry)
),
anchor=["top", "right", "bottom", "left"],
child=Widget.Overlay(
child=Widget.Button(
vexpand=True,
hexpand=True,
can_focus=False,
css_classes=["unset"],
on_click=lambda x: app.close_window("ignis_LAUNCHER"),
style="background-color: rgba(0, 0, 0, 0.3);",
),
overlays=[main_box],
),
)
__all__ = ["Launcher"]
+217
View File
@@ -0,0 +1,217 @@
import re
import asyncio
from ignis.widgets import Widget
from ignis.app import IgnisApp
from ignis.services.applications import (
ApplicationsService,
Application,
ApplicationAction,
)
from ignis.utils import Utils
from gi.repository import Gio # type: ignore
app = IgnisApp.get_default()
applications = ApplicationsService.get_default()
TERMINAL_FORMAT = "kitty %command%"
def is_url(url: str) -> bool:
regex = re.compile(
r"^(?:http|ftp)s?://" # http:// or https://
r"(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|" # domain
r"localhost|" # localhost
r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|" # or ipv4
r"\[?[A-F0-9]*:[A-F0-9:]+\]?)" # or ipv6
r"(?::\d+)?" # optional port
r"(?:/?|[/?]\S+)$",
re.IGNORECASE,
)
return re.match(regex, url) is not None
class LauncherAppItem(Widget.Button):
def __init__(self, application: Application) -> None:
self._application = application
super().__init__(
on_click=lambda x: self.launch(),
on_right_click=lambda x: self._menu.popup(),
css_classes=["launcher-app"],
child=Widget.Box(
child=[
Widget.Icon(image=application.icon, pixel_size=48),
Widget.Label(
label=application.name,
ellipsize="end",
max_width_chars=30,
css_classes=["launcher-app-label"],
),
]
),
)
self.__sync_menu()
application.connect("notify::is-pinned", lambda x, y: self.__sync_menu())
def launch(self) -> None:
self._application.launch(terminal_format=TERMINAL_FORMAT)
app.close_window("ignis_LAUNCHER")
def launch_action(self, action: ApplicationAction) -> None:
action.launch()
app.close_window("ignis_LAUNCHER")
def __sync_menu(self) -> None:
self._menu = Widget.PopoverMenu(
items=[
Widget.MenuItem(label="Launch", on_activate=lambda x: self.launch()),
Widget.Separator(),
]
+ [
Widget.MenuItem(
label=i.name,
on_activate=lambda x, action=i: self.launch_action(action),
)
for i in self._application.actions
]
+ [
Widget.Separator(),
Widget.MenuItem(
label="Pin", on_activate=lambda x: self._application.pin()
)
if not self._application.is_pinned
else Widget.MenuItem(
label="Unpin", on_activate=lambda x: self._application.unpin()
),
]
)
self.child.append(self._menu)
class SearchWebButton(Widget.Button):
def __init__(self, query: str):
self._query = query
self._url = ""
browser_desktop_file = Utils.exec_sh(
"xdg-settings get default-web-browser"
).stdout.replace("\n", "")
app_info = Gio.DesktopAppInfo.new(desktop_id=browser_desktop_file)
icon_name = "applications-internet-symbolic"
if app_info:
icon_string = app_info.get_string("Icon")
if icon_string:
icon_name = icon_string
if not query.startswith(("http://", "https://")) and "." in query:
query = "https://" + query
if is_url(query):
label = f"Visit {query}"
self._url = query
else:
label = "Search in Google"
self._url = f"https://www.google.com/search?q={query.replace(' ', '+')}"
super().__init__(
on_click=lambda x: self.launch(),
css_classes=["launcher-app"],
child=Widget.Box(
child=[
Widget.Icon(image=icon_name, pixel_size=48),
Widget.Label(
label=label,
css_classes=["launcher-app-label"],
),
]
),
)
def launch(self) -> None:
asyncio.create_task(Utils.exec_sh_async(f"xdg-open {self._url}"))
app.close_window("ignis_LAUNCHER")
class Launcher(Widget.Window):
def __init__(self):
self._app_list = Widget.Box(
vertical=True, visible=False, style="margin-top: 1rem;"
)
self._entry = Widget.Entry(
hexpand=True,
placeholder_text="Search",
css_classes=["launcher-search"],
on_change=self.__search,
on_accept=self.__on_accept,
)
main_box = Widget.Box(
vertical=True,
valign="start",
halign="center",
css_classes=["launcher"],
child=[
Widget.Box(
css_classes=["launcher-search-box"],
child=[
Widget.Icon(
icon_name="system-search-symbolic",
pixel_size=24,
style="margin-right: 0.5rem;",
),
self._entry,
],
),
self._app_list,
],
)
super().__init__(
namespace="ignis_LAUNCHER",
visible=False,
popup=True,
kb_mode="on_demand",
css_classes=["unset"],
setup=lambda self: self.connect("notify::visible", self.__on_open),
anchor=["top", "right", "bottom", "left"],
child=Widget.Overlay(
child=Widget.Button(
vexpand=True,
hexpand=True,
can_focus=False,
css_classes=["unset"],
on_click=lambda x: app.close_window("ignis_LAUNCHER"),
style="background-color: rgba(0, 0, 0, 0.3);",
),
overlays=[main_box],
),
)
def __on_open(self, *args) -> None:
if not self.visible:
return
self._entry.text = ""
self._entry.grab_focus()
def __on_accept(self, *args) -> None:
if len(self._app_list.child) > 0:
self._app_list.child[0].launch()
def __search(self, *args) -> None:
query = self._entry.text
if query == "":
self._entry.grab_focus()
self._app_list.visible = False
return
apps = applications.search(applications.apps, query)
if apps == []:
self._app_list.child = [SearchWebButton(query)]
else:
self._app_list.visible = True
self._app_list.child = [LauncherAppItem(i) for i in apps[:5]]
+2 -94
View File
@@ -1,95 +1,3 @@
from ignis.widgets import Widget
from ignis.app import IgnisApp
from ignis.utils import Utils
from ignis.services.notifications import Notification, NotificationService
from ..control_center.notification_center import NotificationWidget
from .notification_popup import NotificationPopup
app = IgnisApp.get_default()
notifications = NotificationService.get_default()
class Popup(Widget.Box):
def __init__(self, notification: Notification):
widget = NotificationWidget(notification)
widget.css_classes = ["notification-popup"]
self._inner = Widget.Revealer(transition_type="slide_left", child=widget)
self._outer = Widget.Revealer(transition_type="slide_down", child=self._inner)
super().__init__(child=[self._outer], halign="end")
notification.connect("dismissed", lambda x: self.destroy())
def destroy(self):
def box_destroy():
box: Widget.Box = self.get_parent() # type: ignore
if not box:
return
self.unparent()
if len(notifications.popups) == 0:
window: Widget.Window = box.get_parent() # type: ignore
if not window:
return
window.visible = False
else:
change_window_input_region(box)
def outer_close():
self._outer.reveal_child = False
Utils.Timeout(self._outer.transition_duration, box_destroy)
self._inner.transition_type = "crossfade"
self._inner.reveal_child = False
Utils.Timeout(self._outer.transition_duration, outer_close)
def on_notified(box: Widget.Box, notification: Notification, monitor: int) -> None:
window: Widget.Window = app.get_window("ignis_CONTROL_CENTER") # type: ignore
if window.visible and window.monitor == monitor:
return
app.open_window(f"ignis_NOTIFICATION_POPUP_{monitor}")
popup = Popup(notification)
box.prepend(popup)
popup._outer.reveal_child = True
Utils.Timeout(popup._outer.transition_duration, reveal_popup, box, popup)
def reveal_popup(box: Widget.Box, popup: Popup) -> None:
popup._inner.set_reveal_child(True)
change_window_input_region(box)
def change_window_input_region(box: Widget.Box) -> None:
def callback() -> None:
width = box.get_width()
height = box.get_height()
window: Widget.Window = box.get_parent() # type: ignore
window.input_width = width
window.input_height = height
Utils.Timeout(ms=50, target=callback)
def notification_popup(monitor: int) -> Widget.Window:
notifications_box = Widget.Box(
vertical=True,
valign="start",
setup=lambda self: notifications.connect(
"new_popup",
lambda x, notification: on_notified(self, notification, monitor),
),
)
return Widget.Window(
anchor=["right", "top", "bottom"],
monitor=monitor,
namespace=f"ignis_NOTIFICATION_POPUP_{monitor}",
layer="top",
child=notifications_box,
visible=False,
css_classes=["rec-unset"],
style="min-width: 29rem;",
)
__all__ = ["NotificationPopup"]
@@ -0,0 +1,79 @@
from ignis.widgets import Widget
from ignis.app import IgnisApp
from ignis.utils import Utils
from ignis.services.notifications import Notification, NotificationService
from ..shared_widgets import NotificationWidget
app = IgnisApp.get_default()
notifications = NotificationService.get_default()
class Popup(Widget.Box):
def __init__(
self, box: "PopupBox", window: "NotificationPopup", notification: Notification
):
self._box = box
self._window = window
widget = NotificationWidget(notification)
widget.css_classes = ["notification-popup"]
self._inner = Widget.Revealer(transition_type="slide_left", child=widget)
self._outer = Widget.Revealer(transition_type="slide_down", child=self._inner)
super().__init__(child=[self._outer], halign="end")
notification.connect("dismissed", lambda x: self.destroy())
def destroy(self):
def box_destroy():
self.unparent()
if len(notifications.popups) == 0:
self._window.visible = False
def outer_close():
self._outer.reveal_child = False
Utils.Timeout(self._outer.transition_duration, box_destroy)
self._inner.transition_type = "crossfade"
self._inner.reveal_child = False
Utils.Timeout(self._outer.transition_duration, outer_close)
class PopupBox(Widget.Box):
def __init__(self, window: "NotificationPopup", monitor: int):
self._window = window
self._monitor = monitor
super().__init__(
vertical=True,
valign="start",
setup=lambda self: notifications.connect(
"new_popup",
lambda x, notification: self.__on_notified(notification),
),
)
def __on_notified(self, notification: Notification) -> None:
self._window.visible = True
popup = Popup(box=self, window=self._window, notification=notification)
self.prepend(popup)
popup._outer.reveal_child = True
Utils.Timeout(
popup._outer.transition_duration, popup._inner.set_reveal_child, True
)
class NotificationPopup(Widget.Window):
def __init__(self, monitor: int):
super().__init__(
anchor=["right", "top", "bottom"],
monitor=monitor,
namespace=f"ignis_NOTIFICATION_POPUP_{monitor}",
layer="top",
child=PopupBox(window=self, monitor=monitor),
visible=False,
dynamic_input_region=True,
css_classes=["rec-unset"],
style="min-width: 29rem;",
)
+2 -42
View File
@@ -1,43 +1,3 @@
from ignis.widgets import Widget
from ignis.utils import Utils
from ignis.services.audio import AudioService
from .osd import OSD
audio = AudioService.get_default()
class OSD(Widget.Window):
def __init__(self):
self._timer = None
super().__init__(
layer="overlay",
anchor=["bottom"],
namespace="ignis_OSD",
visible=False,
css_classes=["rec-unset"],
child=Widget.Box(
css_classes=["osd"],
child=[
Widget.Icon(
pixel_size=26,
style="margin-right: 0.5rem;",
image=audio.speaker.bind("icon_name"),
),
Widget.Scale(
value=audio.speaker.bind("volume"),
css_classes=["material-slider"],
sensitive=False,
hexpand=True,
),
],
),
)
def set_property(self, property_name, value):
if property_name == "visible":
if self._timer:
self._timer.cancel()
self._timer = None
self._timer = Utils.Timeout(3000, self.set_visible, False)
super().set_property(property_name, value)
__all__ = ["OSD"]
Binary file not shown.
+38
View File
@@ -0,0 +1,38 @@
from ignis.widgets import Widget
from ignis.utils import Utils
from ignis.services.audio import AudioService
from ..shared_widgets import MaterialVolumeSlider
audio = AudioService.get_default()
class OSD(Widget.Window):
def __init__(self):
super().__init__(
layer="overlay",
anchor=["bottom"],
namespace="ignis_OSD",
visible=False,
css_classes=["rec-unset"],
child=Widget.Box(
css_classes=["osd"],
child=[
Widget.Icon(
pixel_size=26,
style="margin-right: 0.5rem;",
image=audio.speaker.bind("icon_name"),
),
MaterialVolumeSlider(stream=audio.speaker, sensitive=False),
],
),
)
def set_property(self, property_name, value):
if property_name == "visible":
self.__update_visible()
super().set_property(property_name, value)
@Utils.debounce(3000)
def __update_visible(self) -> None:
super().set_property("visible", False)
+2 -97
View File
@@ -1,98 +1,3 @@
from ignis.widgets import Widget
from ignis.utils import Utils
from ignis.app import IgnisApp
from typing import Callable
from .powermenu import Powermenu
app = IgnisApp.get_default()
class PowermenuButton(Widget.Box):
def __init__(self, label: str, icon_name: str, on_click: Callable) -> None:
super().__init__(
child=[
Widget.Button(
child=Widget.Icon(image=icon_name, pixel_size=36),
on_click=on_click,
css_classes=["powermenu-button", "unset"],
),
Widget.Label(label=label, css_classes=["powermenu-button-label"]),
],
vertical=True,
css_classes=["powermenu-button-box"],
)
def poweroff(*args) -> None:
Utils.exec_sh_async("poweroff")
def reboot(*args) -> None:
Utils.exec_sh_async("reboot")
def suspend(*args) -> None:
app.close_window("ignis_POWERMENU")
Utils.exec_sh_async("systemctl suspend && hyprlock")
def hypr_exit(*args) -> None:
Utils.exec_sh_async("hyprctl dispatch exit 0")
def powermenu():
return Widget.Window(
popup=True,
kb_mode="on_demand",
namespace="ignis_POWERMENU",
exclusivity="ignore",
anchor=["left", "right", "top", "bottom"],
visible=False,
child=Widget.Overlay(
child=Widget.Button(
vexpand=True,
hexpand=True,
can_focus=False,
css_classes=["unset", "powermenu-overlay"],
on_click=lambda x: app.close_window("ignis_POWERMENU"),
),
overlays=[
Widget.Box(
vertical=True,
valign="center",
halign="center",
css_classes=["powermenu"],
child=[
Widget.Box(
child=[
PowermenuButton(
label="Power off",
icon_name="system-shutdown-symbolic",
on_click=poweroff,
),
PowermenuButton(
label="Reboot",
icon_name="system-reboot-symbolic",
on_click=reboot,
),
]
),
Widget.Box(
child=[
PowermenuButton(
label="Suspend",
icon_name="night-light-symbolic",
on_click=suspend,
),
PowermenuButton(
label="Sign out",
icon_name="system-log-out-symbolic",
on_click=hypr_exit,
),
]
),
],
)
],
),
css_classes=["unset"],
)
__all__ = ["Powermenu"]
+106
View File
@@ -0,0 +1,106 @@
import asyncio
from ignis.widgets import Widget
from ignis.utils import Utils
from ignis.app import IgnisApp
from typing import Callable
app = IgnisApp.get_default()
def create_exec_task(cmd: str) -> None:
asyncio.create_task(Utils.exec_sh_async(cmd))
class PowermenuButton(Widget.Box):
def __init__(self, label: str, icon_name: str, on_click: Callable) -> None:
super().__init__(
child=[
Widget.Button(
child=Widget.Icon(image=icon_name, pixel_size=36),
on_click=on_click,
css_classes=["powermenu-button", "unset"],
),
Widget.Label(label=label, css_classes=["powermenu-button-label"]),
],
vertical=True,
css_classes=["powermenu-button-box"],
)
class PowerOffButton(PowermenuButton):
def __init__(self):
super().__init__(
label="Power off",
icon_name="system-shutdown-symbolic",
on_click=lambda *args: create_exec_task("poweroff"),
)
class RebootButton(PowermenuButton):
def __init__(self):
super().__init__(
label="Reboot",
icon_name="system-reboot-symbolic",
on_click=lambda *args: create_exec_task("reboot"),
)
class SuspendButton(PowermenuButton):
def __init__(self):
super().__init__(
label="Suspend", icon_name="night-light-symbolic", on_click=self.__invoke
)
def __invoke(self, *args) -> None:
app.close_window("ignis_POWERMENU")
create_exec_task("systemctl suspend && hyprlock")
class HyprlandExitButton(PowermenuButton):
def __init__(self):
super().__init__(
label="Sign out",
icon_name="system-log-out-symbolic",
on_click=lambda *args: create_exec_task("hyprctl dispatch exit 0"),
)
class Powermenu(Widget.Window):
def __init__(self):
main_box = Widget.Box(
vertical=True,
valign="center",
halign="center",
css_classes=["powermenu"],
child=[
Widget.Box(
child=[
PowerOffButton(),
RebootButton(),
]
),
Widget.Box(
child=[
SuspendButton(),
HyprlandExitButton(),
]
),
],
)
super().__init__(
popup=True,
kb_mode="on_demand",
namespace="ignis_POWERMENU",
exclusivity="ignore",
anchor=["left", "right", "top", "bottom"],
visible=False,
child=Widget.Overlay(
child=Widget.Button(
vexpand=True,
hexpand=True,
can_focus=False,
css_classes=["unset", "powermenu-overlay"],
on_click=lambda x: app.close_window("ignis_POWERMENU"),
),
overlays=[main_box],
),
css_classes=["unset"],
)
+4 -82
View File
@@ -1,83 +1,5 @@
from ignis.widgets import Widget
from ignis.gobject import IgnisGObject
from gi.repository import GObject # type: ignore
from .notifications import notifications_entry
from .about import about_entry
from .appearance import appearance_entry
from .recorder import recorder_entry
from .user import user_entry
from .elements import SettingsPage
from ignis.app import IgnisApp
from ignis.exceptions import WindowNotFoundError
from options import settings_last_page
from .settings import Settings
app = IgnisApp.get_default()
class ActivePage(IgnisGObject):
def __init__(self, name: str | None, page: SettingsPage | None):
super().__init__()
self._name = name
self._page = page
@GObject.Property
def name(self) -> str | None:
return self._name
@name.setter
def name(self, value: str | None) -> None:
self._name = value
@GObject.Property
def page(self) -> SettingsPage | None:
return self._page
@page.setter
def page(self, value: SettingsPage | None) -> None:
self._page = value
def settings_widget():
active_page = ActivePage(name="Settings", page=None)
content = Widget.Box(
hexpand=True,
vexpand=True,
child=active_page.bind("page", transform=lambda value: [value]),
)
listbox = Widget.ListBox(
rows=[
notifications_entry(active_page),
recorder_entry(active_page),
appearance_entry(active_page),
user_entry(active_page),
about_entry(active_page),
],
)
listbox.select_row(listbox.rows[settings_last_page.value])
navigation_sidebar = Widget.Box(
vertical=True,
css_classes=["settings-sidebar"],
child=[
Widget.Label(
label="Settings", halign="start", css_classes=["settings-sidebar-label"]
),
listbox,
],
)
return Widget.Box(child=[navigation_sidebar, content])
def settings_window():
try:
app.get_window("ignis_SETTINGS")
except WindowNotFoundError:
return Widget.RegularWindow(
default_width=900,
default_height=600,
resizable=False,
child=settings_widget(),
namespace="ignis_SETTINGS",
)
__all__ = [
"Settings",
]
+5
View File
@@ -0,0 +1,5 @@
from ignis.variable import Variable
from .elements import SettingsPage
fallback_page = SettingsPage(name="Settings")
active_page = Variable(value=fallback_page)
+2 -8
View File
@@ -1,6 +1,5 @@
from ignis.widgets import Widget
from .page import SettingsPage
from options import settings_last_page
class SettingsEntry(Widget.ListBoxRow):
@@ -8,15 +7,10 @@ class SettingsEntry(Widget.ListBoxRow):
self,
icon: str,
label: str,
active_page,
page: SettingsPage,
**kwargs,
):
def callback(x):
active_page.page = page
active_page.name = label
if self in self.parent.rows:
settings_last_page.set_value(self.parent.rows.index(self))
from ..active_page import active_page # avoid a circular import
super().__init__(
child=Widget.Box(
@@ -26,6 +20,6 @@ class SettingsEntry(Widget.ListBoxRow):
],
),
css_classes=["settings-sidebar-entry"],
on_activate=callback,
on_activate=lambda x: active_page.set_value(page),
**kwargs,
)
+1 -2
View File
@@ -1,4 +1,3 @@
from typing import List
from ignis.widgets import Widget
from .row import SettingsRow
from ignis.base_widget import BaseWidget
@@ -6,7 +5,7 @@ from ignis.base_widget import BaseWidget
class SettingsGroup(Widget.Box):
def __init__(
self, name: str | None, rows: List[SettingsRow | BaseWidget] = [], **kwargs
self, name: str | None, rows: list[SettingsRow | BaseWidget] = [], **kwargs
):
super().__init__(
vertical=True,
+1 -2
View File
@@ -1,11 +1,10 @@
from ignis.widgets import Widget
from typing import List
from .group import SettingsGroup
from ignis.base_widget import BaseWidget
class SettingsPage(Widget.Scroll):
def __init__(self, name: str, groups: List[SettingsGroup | BaseWidget] = []):
def __init__(self, name: str, groups: list[SettingsGroup | BaseWidget] = []):
super().__init__(
hexpand=True,
vexpand=True,
+6 -1
View File
@@ -2,7 +2,12 @@ from ignis.widgets import Widget
class SettingsRow(Widget.ListBoxRow):
def __init__(self, label: str | None = None, sublabel: str | None = None, **kwargs):
def __init__(
self,
label: str | None = None,
sublabel: str | None = None,
**kwargs,
):
super().__init__(
css_classes=["settings-row"],
child=Widget.Box(
+13
View File
@@ -0,0 +1,13 @@
from .about import AboutEntry
from .appearance import AppearanceEntry
from .notifications import NotificationsEntry
from .recorder import RecorderEntry
from .user import UserEntry
__all__ = [
"AboutEntry",
"AppearanceEntry",
"NotificationsEntry",
"RecorderEntry",
"UserEntry",
]

Some files were not shown because too many files have changed in this diff Show More