190 lines
6.9 KiB
TypeScript
190 lines
6.9 KiB
TypeScript
import Gtk from "gi://Gtk?version=3.0"
|
|
import GLib from "gi://GLib"
|
|
import AstalWp from "gi://AstalWp"
|
|
import { execAsync, Variable, bind } from "astal"
|
|
import Dropdown from "./Dropdown"
|
|
import { wifiIcon, WifiSection } from "./Wifi"
|
|
import { bluetoothIndicator, BluetoothSection } from "./Bluetooth"
|
|
import { BrightnessRow } from "./Brightness"
|
|
import { MediaSection } from "./Media"
|
|
import { batteryIcon, batteryPercent, BatterySection } from "./Battery"
|
|
import {
|
|
clearNotifications,
|
|
dndIcon,
|
|
DndToggle,
|
|
history,
|
|
notifCount,
|
|
NotificationCard,
|
|
} from "./Notifications"
|
|
|
|
const speaker = AstalWp.get_default().audio.default_speaker!
|
|
const mic = AstalWp.get_default().audio.default_microphone
|
|
|
|
const speakerIcon = Variable.derive(
|
|
[bind(speaker, "volume"), bind(speaker, "mute")],
|
|
(volume, mute) => {
|
|
if (mute || volume <= 0.01) return "audio-volume-muted-symbolic"
|
|
if (volume < 0.33) return "audio-volume-low-symbolic"
|
|
if (volume < 0.66) return "audio-volume-medium-symbolic"
|
|
return "audio-volume-high-symbolic"
|
|
},
|
|
)
|
|
|
|
const micIcon = mic
|
|
? Variable.derive([bind(mic, "volume"), bind(mic, "mute")], (volume, mute) =>
|
|
mute || volume <= 0.01
|
|
? "microphone-sensitivity-muted-symbolic"
|
|
: "microphone-sensitivity-high-symbolic",
|
|
)
|
|
: null
|
|
|
|
const user = GLib.get_user_name()
|
|
|
|
const actions: { icon: string; cmd?: string[] }[] = [
|
|
{ icon: "emblem-system-symbolic" },
|
|
{ icon: "system-lock-screen-symbolic", cmd: ["hyprlock"] },
|
|
{ icon: "weather-clear-night-symbolic", cmd: ["systemctl", "suspend"] },
|
|
{ icon: "system-reboot-symbolic", cmd: ["systemctl", "reboot"] },
|
|
{ icon: "system-log-out-symbolic", cmd: ["hyprctl", "dispatch", "exit"] },
|
|
{ icon: "system-shutdown-symbolic", cmd: ["systemctl", "poweroff"] },
|
|
]
|
|
|
|
function Header() {
|
|
return (
|
|
<box className="header" spacing={8}>
|
|
<box className="avatar">
|
|
<icon icon="nix-snowflake-white" pixelSize={16} />
|
|
</box>
|
|
<label className="username" label={user} valign={Gtk.Align.CENTER} />
|
|
<box hexpand />
|
|
{actions.map((action) => (
|
|
<button onClicked={() => action.cmd && execAsync(action.cmd)}>
|
|
<icon icon={action.icon} />
|
|
</button>
|
|
))}
|
|
</box>
|
|
)
|
|
}
|
|
|
|
export default function QuickSettings(gdkmonitor: Gdk.Monitor) {
|
|
const open = Variable(false)
|
|
|
|
const wifiSection = WifiSection({ open })
|
|
const btSection = BluetoothSection({ open })
|
|
const toggles = [wifiSection?.toggle, btSection?.toggle].filter(
|
|
(t) => t !== undefined,
|
|
)
|
|
|
|
const button = (
|
|
<button
|
|
className="quicksettings end"
|
|
onClicked={() => open.set(!open.get())}
|
|
onScroll={(_, e) => {
|
|
const volume = Math.min(
|
|
1,
|
|
Math.max(0, speaker.volume - e.delta_y * 0.02),
|
|
)
|
|
speaker.volume = volume
|
|
}}
|
|
>
|
|
<box spacing={6}>
|
|
<box className="notif-indicator" spacing={3}>
|
|
<icon icon={dndIcon()} />
|
|
<label
|
|
className="notif-count"
|
|
label={notifCount.as((n) => `${n}`)}
|
|
visible={notifCount.as((n) => n > 0)}
|
|
/>
|
|
</box>
|
|
{wifiIcon && <icon icon={wifiIcon} />}
|
|
{bluetoothIndicator}
|
|
<icon icon={speakerIcon()} />
|
|
{micIcon && <icon icon={micIcon()} />}
|
|
{batteryIcon && batteryPercent && (
|
|
<box spacing={3}>
|
|
<icon icon={batteryIcon} />
|
|
<label className="battery-level" label={batteryPercent} />
|
|
</box>
|
|
)}
|
|
</box>
|
|
</button>
|
|
)
|
|
|
|
const popup = (
|
|
<Dropdown
|
|
gdkmonitor={gdkmonitor}
|
|
target={button}
|
|
open={open}
|
|
width={280}
|
|
maxHeight={gdkmonitor.get_geometry().height - 200}
|
|
>
|
|
<Header />
|
|
<BatterySection />
|
|
<box className="sliders-box" vertical spacing={2}>
|
|
<box className="volume" spacing={6}>
|
|
<button onClicked={() => (speaker.mute = !speaker.mute)}>
|
|
<icon icon={speakerIcon()} />
|
|
</button>
|
|
<slider
|
|
hexpand
|
|
className={bind(speaker, "mute").as((m) => (m ? "muted" : ""))}
|
|
value={bind(speaker, "volume")}
|
|
onDragged={(self) => (speaker.volume = self.value)}
|
|
/>
|
|
</box>
|
|
{mic && micIcon && (
|
|
<box className="microphone" spacing={6}>
|
|
<button onClicked={() => (mic.mute = !mic.mute)}>
|
|
<icon icon={micIcon()} />
|
|
</button>
|
|
<slider
|
|
hexpand
|
|
className={bind(mic, "mute").as((m) => (m ? "muted" : ""))}
|
|
value={bind(mic, "volume")}
|
|
onDragged={(self) => (mic.volume = self.value)}
|
|
/>
|
|
</box>
|
|
)}
|
|
<BrightnessRow />
|
|
</box>
|
|
<DndToggle />
|
|
{toggles.length > 0 && (
|
|
<box homogeneous spacing={6}>
|
|
{toggles}
|
|
</box>
|
|
)}
|
|
{wifiSection?.menu}
|
|
{btSection?.menu}
|
|
<MediaSection />
|
|
<box className="notif-section" vertical spacing={6}>
|
|
<box className="notif-header-row" spacing={6}>
|
|
<box className="notif-header" spacing={6}>
|
|
<icon icon="preferences-system-notifications-symbolic" />
|
|
<label label="Notifications" />
|
|
</box>
|
|
<box hexpand />
|
|
<button
|
|
className="notif-clear"
|
|
onClicked={clearNotifications}
|
|
>
|
|
<label label="Clear" />
|
|
</button>
|
|
</box>
|
|
<box className="notifications" vertical>
|
|
<label
|
|
className="notif-empty"
|
|
label="no notifications"
|
|
visible={notifCount.as((n) => n === 0)}
|
|
/>
|
|
{bind(history).as((ns) =>
|
|
[...ns]
|
|
.sort((a, b) => b.time - a.time)
|
|
.map((n) => NotificationCard(n)),
|
|
)}
|
|
</box>
|
|
</box>
|
|
</Dropdown>
|
|
)
|
|
|
|
return { button, popup }
|
|
} |