[AGS] Bar: Done (WiFi still missing, will be added at some later point)
This commit is contained in:
		
							
								
								
									
										74
									
								
								config/astal/components/QuickActions/QuickActions.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								config/astal/components/QuickActions/QuickActions.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,74 @@ | ||||
| import { Gtk } from "astal/gtk4"; | ||||
| import Power from "./modules/Power"; | ||||
| import Audio from "./modules/Audio/Audio"; | ||||
| import Bluetooth from "./modules/Bluetooth/Bluetooth"; | ||||
| import Brightness from "./modules/Brightness/Brightness"; | ||||
| import Player from "./modules/Player/Player"; | ||||
| import { BatteryBox } from "./modules/Battery"; | ||||
| import { exec } from "astal"; | ||||
| import Network from "./modules/Networking/Network"; | ||||
|  | ||||
| const QuickActions = () => { | ||||
|     const popover = new Gtk.Popover({ cssClasses: ["quick-actions-wrapper"] }); | ||||
|     popover.set_child(renderQuickActions()); | ||||
|     return popover; | ||||
| }; | ||||
|  | ||||
| const renderQuickActions = () => { | ||||
|     const user = exec("/bin/sh -c whoami"); | ||||
|     const profile = exec("/bin/fish -c get-profile-picture"); | ||||
|     const cwd = exec("pwd"); | ||||
|     const um = Power.UserMenu(); | ||||
|  | ||||
|     return ( | ||||
|         <box visible cssClasses={["quick-actions", "popover-box"]} vertical> | ||||
|             <centerbox | ||||
|                 startWidget={ | ||||
|                     <button | ||||
|                         onClicked={() => um.popup()} | ||||
|                         cssClasses={["stealthy-button"]} | ||||
|                         child={ | ||||
|                             <box> | ||||
|                                 {um} | ||||
|                                 <Gtk.Frame | ||||
|                                     cssClasses={["avatar-icon"]} | ||||
|                                     child={ | ||||
|                                         <image | ||||
|                                             file={ | ||||
|                                                 profile !== "" | ||||
|                                                     ? profile | ||||
|                                                     : cwd + | ||||
|                                                       "/no-avatar-icon.jpg" | ||||
|                                             } | ||||
|                                         ></image> | ||||
|                                     } | ||||
|                                 ></Gtk.Frame> | ||||
|                                 <label label={user}></label> | ||||
|                             </box> | ||||
|                         } | ||||
|                     ></button> | ||||
|                 } | ||||
|                 endWidget={ | ||||
|                     <box> | ||||
|                         <BatteryBox></BatteryBox> | ||||
|                         <Power.Power></Power.Power> | ||||
|                     </box> | ||||
|                 } | ||||
|             ></centerbox> | ||||
|             <Gtk.Separator marginTop={10} marginBottom={20}></Gtk.Separator> | ||||
|             <box> | ||||
|                 <Bluetooth.BluetoothModule></Bluetooth.BluetoothModule> | ||||
|                 <Network.Network></Network.Network> | ||||
|             </box> | ||||
|             <Gtk.Separator marginTop={10} marginBottom={10}></Gtk.Separator> | ||||
|             <Brightness.BrightnessModule></Brightness.BrightnessModule> | ||||
|             <Audio.AudioModule></Audio.AudioModule> | ||||
|             <Player.PlayerModule></Player.PlayerModule> | ||||
|         </box> | ||||
|     ); | ||||
| }; | ||||
|  | ||||
| // TODO: Expose additional functions to be usable through CLI | ||||
| export default { | ||||
|     QuickActions, | ||||
| }; | ||||
							
								
								
									
										35
									
								
								config/astal/components/QuickActions/dump
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								config/astal/components/QuickActions/dump
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| import { Gtk } from "astal/gtk4"; | ||||
| import Power from "./modules/Power"; | ||||
| import Audio from "./modules/Audio/Audio"; | ||||
| import Bluetooth from "./modules/Bluetooth/Bluetooth"; | ||||
| import Brightness from "./modules/Brightness/Brightness"; | ||||
| import Player from "./modules/Player/Player"; | ||||
| import { BatteryBox } from "./modules/Battery"; | ||||
|  | ||||
| const QuickActions = () => { | ||||
|     const popover = new Gtk.Overlay( { cssClasses: [ 'quick-actions-wrapper' ] } ); | ||||
|     popover.set_child(renderQuickActions()); | ||||
|     return popover; | ||||
| }; | ||||
|  | ||||
| const renderQuickActions = () => { | ||||
|     return ( | ||||
|         <box visible cssClasses={["quick-actions", "popover-box"]} vertical setup={ self }> | ||||
|             <box halign={Gtk.Align.END}> | ||||
|                 <BatteryBox></BatteryBox> | ||||
|                 <Power></Power> | ||||
|             </box> | ||||
|             <Bluetooth.BluetoothModule></Bluetooth.BluetoothModule> | ||||
|             <Gtk.Separator marginTop={10} marginBottom={10}></Gtk.Separator> | ||||
|             <Brightness.BrightnessModule></Brightness.BrightnessModule> | ||||
|             <Audio.AudioModule></Audio.AudioModule> | ||||
|             <Gtk.Separator marginTop={20} marginBottom={10}></Gtk.Separator> | ||||
|             <Player.PlayerModule></Player.PlayerModule> | ||||
|         </box> | ||||
|     ); | ||||
| }; | ||||
|  | ||||
| // TODO: Expose additional functions to be usable through CLI | ||||
| export default { | ||||
|     QuickActions, | ||||
| }; | ||||
| @@ -0,0 +1,3 @@ | ||||
| .audio-box { | ||||
|   min-width: 320px; | ||||
| } | ||||
							
								
								
									
										184
									
								
								config/astal/components/QuickActions/modules/Audio/Audio.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										184
									
								
								config/astal/components/QuickActions/modules/Audio/Audio.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,184 @@ | ||||
| import { bind, Binding } from "astal"; | ||||
| import { Gtk } from "astal/gtk4"; | ||||
| import AstalWp from "gi://AstalWp"; | ||||
|  | ||||
| const wp = AstalWp.get_default()!; | ||||
|  | ||||
| const AudioModule = () => { | ||||
|     const setVolumeSpeaker = (volume: number) => { | ||||
|         wp.defaultSpeaker.set_volume(volume / 100); | ||||
|     }; | ||||
|  | ||||
|     const setVolumeMicrophone = (volume: number) => { | ||||
|         wp.defaultMicrophone.set_volume(volume / 100); | ||||
|     }; | ||||
|  | ||||
|     const speakerSelector = SinkSelectPopover(AstalWp.MediaClass.AUDIO_SPEAKER); | ||||
|     const micSelector = SinkSelectPopover(AstalWp.MediaClass.AUDIO_MICROPHONE); | ||||
|  | ||||
|     return ( | ||||
|         <box cssClasses={["audio-box"]} vertical> | ||||
|             <box hexpand vexpand> | ||||
|                 <button | ||||
|                     onClicked={() => | ||||
|                         wp.defaultSpeaker.set_mute( | ||||
|                             !wp.defaultSpeaker.get_mute(), | ||||
|                         ) | ||||
|                     } | ||||
|                     tooltipText={"Mute audio output"} | ||||
|                     child={ | ||||
|                         <image | ||||
|                             iconName={bind(wp.defaultSpeaker, "volumeIcon")} | ||||
|                             marginEnd={3} | ||||
|                         ></image> | ||||
|                     } | ||||
|                 ></button> | ||||
|                 <label | ||||
|                     label={bind(wp.defaultSpeaker, "volume").as( | ||||
|                         v => Math.round(100 * v) + "%", | ||||
|                     )} | ||||
|                 ></label> | ||||
|                 <slider | ||||
|                     value={bind(wp.defaultSpeaker, "volume").as(v => 100 * v)} | ||||
|                     max={100} | ||||
|                     min={0} | ||||
|                     step={1} | ||||
|                     hexpand | ||||
|                     vexpand | ||||
|                     onChangeValue={self => setVolumeSpeaker(self.value)} | ||||
|                 ></slider> | ||||
|                 <button | ||||
|                     cssClasses={["sink-select-button"]} | ||||
|                     tooltipText={"Pick audio output"} | ||||
|                     child={ | ||||
|                         <box> | ||||
|                             <image iconName={"speaker-symbolic"}></image> | ||||
|                             {speakerSelector} | ||||
|                         </box> | ||||
|                     } | ||||
|                     onClicked={() => speakerSelector.popup()} | ||||
|                 ></button> | ||||
|             </box> | ||||
|             <box hexpand vexpand> | ||||
|                 <button | ||||
|                     onClicked={() => | ||||
|                         wp.defaultMicrophone.set_mute( | ||||
|                             !wp.defaultMicrophone.get_mute(), | ||||
|                         ) | ||||
|                     } | ||||
|                     tooltipText={"Mute audio input"} | ||||
|                     child={ | ||||
|                         <image | ||||
|                             iconName={bind(wp.defaultMicrophone, "volumeIcon")} | ||||
|                             marginEnd={3} | ||||
|                         ></image> | ||||
|                     } | ||||
|                 ></button> | ||||
|                 <label | ||||
|                     label={bind(wp.defaultMicrophone, "volume").as( | ||||
|                         v => Math.round(100 * v) + "%", | ||||
|                     )} | ||||
|                 ></label> | ||||
|                 <slider | ||||
|                     value={bind(wp.defaultMicrophone, "volume").as( | ||||
|                         v => 100 * v, | ||||
|                     )} | ||||
|                     max={100} | ||||
|                     min={0} | ||||
|                     step={1} | ||||
|                     hexpand | ||||
|                     vexpand | ||||
|                     onChangeValue={self => setVolumeMicrophone(self.value)} | ||||
|                 ></slider> | ||||
|                 <button | ||||
|                     cssClasses={["sink-select-button"]} | ||||
|                     tooltipText={"Select audio input"} | ||||
|                     child={ | ||||
|                         <box> | ||||
|                             <image iconName={"microphone"}></image> | ||||
|                             {micSelector} | ||||
|                         </box> | ||||
|                     } | ||||
|                     onClicked={() => micSelector.popup()} | ||||
|                 ></button> | ||||
|             </box> | ||||
|         </box> | ||||
|     ); | ||||
| }; | ||||
|  | ||||
| const SinkPicker = (type: AstalWp.MediaClass) => { | ||||
|     const devices = bind(wp, "endpoints"); | ||||
|  | ||||
|     return ( | ||||
|         <box vertical> | ||||
|             <label | ||||
|                 label={`Available Audio ${type === AstalWp.MediaClass.AUDIO_SPEAKER ? "Output" : type === AstalWp.MediaClass.AUDIO_MICROPHONE ? "Input" : ""} Devices`} | ||||
|             ></label> | ||||
|             <Gtk.Separator marginBottom={5} marginTop={3}></Gtk.Separator> | ||||
|             <box vertical cssClasses={["sink-picker"]}> | ||||
|                 {devices.as(d => { | ||||
|                     return d.map(device => { | ||||
|                         if (device.get_media_class() !== type) { | ||||
|                             return <box cssClasses={[ 'empty' ]}></box>; | ||||
|                         } | ||||
|                         return ( | ||||
|                             <button | ||||
|                                 cssClasses={bind(device, "id").as(id => { | ||||
|                                     if ( | ||||
|                                         id === | ||||
|                                         (type === | ||||
|                                         AstalWp.MediaClass.AUDIO_SPEAKER | ||||
|                                             ? wp.defaultSpeaker.id | ||||
|                                             : type === | ||||
|                                                 AstalWp.MediaClass | ||||
|                                                     .AUDIO_MICROPHONE | ||||
|                                               ? wp.defaultMicrophone.id | ||||
|                                               : "") | ||||
|                                     ) { | ||||
|                                         return [ | ||||
|                                             "sink-option", | ||||
|                                             "currently-selected-sink-option", | ||||
|                                         ]; | ||||
|                                     } else { | ||||
|                                         return ["sink-option"]; | ||||
|                                     } | ||||
|                                 })} | ||||
|                                 child={ | ||||
|                                     <box halign={Gtk.Align.START}> | ||||
|                                         <image | ||||
|                                             iconName={bind(device, "icon").as( | ||||
|                                                 icon => icon, | ||||
|                                             )} | ||||
|                                             marginEnd={3} | ||||
|                                         ></image> | ||||
|                                         <label | ||||
|                                             label={bind( | ||||
|                                                 device, | ||||
|                                                 "description", | ||||
|                                             ).as(t => t ?? "")} | ||||
|                                         ></label> | ||||
|                                     </box> | ||||
|                                 } | ||||
|                                 onClicked={() => { | ||||
|                                     device.set_is_default(true); | ||||
|                                 }} | ||||
|                             ></button> | ||||
|                         ); | ||||
|                     }); | ||||
|                 })} | ||||
|             </box> | ||||
|         </box> | ||||
|     ); | ||||
| }; | ||||
|  | ||||
| const SinkSelectPopover = (type: AstalWp.MediaClass) => { | ||||
|     const popover = new Gtk.Popover(); | ||||
|  | ||||
|     popover.set_child(SinkPicker(type)); | ||||
|  | ||||
|     return popover; | ||||
| }; | ||||
|  | ||||
| export default { | ||||
|     AudioModule, | ||||
| }; | ||||
							
								
								
									
										52
									
								
								config/astal/components/QuickActions/modules/Battery.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								config/astal/components/QuickActions/modules/Battery.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | ||||
| import { bind } from "astal"; | ||||
| import Battery from "gi://AstalBattery"; | ||||
| import { Gtk } from "astal/gtk4"; | ||||
|  | ||||
| export const BatteryBox = () => { | ||||
|     const battery = Battery.get_default(); | ||||
|     const batteryEnergy = (energyRate: number) => { | ||||
|         return energyRate > 0.1 ? `${Math.round(energyRate * 10) / 10} W ` : ""; | ||||
|     }; | ||||
|     return ( | ||||
|         <box | ||||
|             cssClasses={["battery-info"]} | ||||
|             visible={bind(battery, "isBattery")} | ||||
|         > | ||||
|             <box cssClasses={["battery-box"]}> | ||||
|                 <image | ||||
|                     iconName={bind(battery, "batteryIconName")} | ||||
|                     tooltipText={bind(battery, "energyRate").as(er => | ||||
|                         batteryEnergy(er), | ||||
|                     )} | ||||
|                 /> | ||||
|                 <label | ||||
|                     label={bind(battery, "percentage").as( | ||||
|                         p => ` ${Math.round(p * 100)}%`, | ||||
|                     )} | ||||
|                 /> | ||||
|                 <label | ||||
|                     cssClasses={["time"]} | ||||
|                     hexpand={true} | ||||
|                     halign={Gtk.Align.END} | ||||
|                     visible={bind(battery, "charging").as(c => !c)} | ||||
|                     label={bind(battery, "timeToEmpty").as(t => toTime(t))} | ||||
|                 /> | ||||
|             </box> | ||||
|         </box> | ||||
|     ); | ||||
| }; | ||||
|  | ||||
| const toTime = (time: number) => { | ||||
|     const MINUTE = 60; | ||||
|     const HOUR = MINUTE * 60; | ||||
|  | ||||
|     if (time > 24 * HOUR) return ""; | ||||
|  | ||||
|     const hours = Math.round(time / HOUR); | ||||
|     const minutes = Math.round((time - hours * HOUR) / MINUTE); | ||||
|  | ||||
|     const hoursDisplay = hours > 0 ? `${hours}h ` : ""; | ||||
|     const minutesDisplay = minutes > 0 ? `${minutes}m ` : ""; | ||||
|  | ||||
|     return `${hoursDisplay}${minutesDisplay}`; | ||||
| }; | ||||
| @@ -0,0 +1,187 @@ | ||||
| import { bind, readFile, Variable, writeFile } from "astal"; | ||||
| import { Gtk } from "astal/gtk4"; | ||||
| import AstalBluetooth from "gi://AstalBluetooth"; | ||||
| import BTDevice from "./Device"; | ||||
| const ALIGN = Gtk.Align; | ||||
|  | ||||
| const bt = AstalBluetooth.get_default(); | ||||
|  | ||||
| const BluetoothModule = () => { | ||||
|     return ( | ||||
|         <box> | ||||
|             <button | ||||
|                 cssClasses={bind(bt.adapter, "powered").as(powered => | ||||
|                     powered | ||||
|                         ? ["toggle-button", "toggle-on"] | ||||
|                         : ["toggle-button"], | ||||
|                 )} | ||||
|                 onClicked={() => | ||||
|                     bt.adapter.set_powered(!bt.adapter.get_powered()) | ||||
|                 } | ||||
|                 child={ | ||||
|                     <box vertical> | ||||
|                         <label | ||||
|                             cssClasses={["title-2"]} | ||||
|                             label={"Bluetooth"} | ||||
|                             halign={ALIGN.CENTER} | ||||
|                             valign={ALIGN.CENTER} | ||||
|                         ></label> | ||||
|                         <box halign={ALIGN.CENTER} valign={ALIGN.CENTER}> | ||||
|                             <label | ||||
|                                 visible={bind(bt.adapter, "powered").as( | ||||
|                                     p => !p, | ||||
|                                 )} | ||||
|                                 label="Disabled" | ||||
|                             ></label> | ||||
|                             <label | ||||
|                                 visible={bind(bt.adapter, "powered")} | ||||
|                                 label={bind(bt, "devices").as(devices => { | ||||
|                                     let count = 0; | ||||
|                                     devices.forEach(device => { | ||||
|                                         if (device.connected) { | ||||
|                                             count++; | ||||
|                                         } | ||||
|                                     }); | ||||
|                                     return `On (${count} ${count === 1 ? "client" : "clients"} connected)`; | ||||
|                                 })} | ||||
|                             ></label> | ||||
|                         </box> | ||||
|                         <label></label> | ||||
|                     </box> | ||||
|                 } | ||||
|             ></button> | ||||
|             <button | ||||
|                 cssClasses={["actions-button"]} | ||||
|                 visible={bind(bt.adapter, "powered")} | ||||
|                 child={ | ||||
|                     <box> | ||||
|                         <image iconName={"arrow-right-symbolic"}></image> | ||||
|                         {picker} | ||||
|                     </box> | ||||
|                 } | ||||
|                 tooltipText={"View available devices"} | ||||
|                 onClicked={() => openBTPicker()} | ||||
|             ></button> | ||||
|         </box> | ||||
|     ); | ||||
| }; | ||||
|  | ||||
| const openBTPicker = () => { | ||||
|     picker.popup(); | ||||
|     try { | ||||
|         bt.adapter.start_discovery(); | ||||
|     } catch (_) {} | ||||
| }; | ||||
|  | ||||
| const BluetoothPickerList = () => { | ||||
|     let btEnableState = readFile("./btconf") === "true" ? true : false; | ||||
|     bt.adapter.set_powered(btEnableState); | ||||
|  | ||||
|     const updateState = () => { | ||||
|         btEnableState = !btEnableState; | ||||
|         writeFile("./btconf", "" + btEnableState); | ||||
|     }; | ||||
|  | ||||
|     return ( | ||||
|         <box | ||||
|             vertical | ||||
|             onDestroy={() => bt.adapter.stop_discovery()} | ||||
|             cssClasses={["popover-box"]} | ||||
|         > | ||||
|             <label cssClasses={["title"]} label={"Bluetooth"}></label> | ||||
|             <Gtk.Separator marginTop={3} marginBottom={5}></Gtk.Separator> | ||||
|             <centerbox | ||||
|                 startWidget={<label label={"Turn on at startup"}></label>} | ||||
|                 endWidget={ | ||||
|                     <switch | ||||
|                         valign={ALIGN.END} | ||||
|                         halign={ALIGN.END} | ||||
|                         active={btEnableState} | ||||
|                         onButtonPressed={() => updateState()} | ||||
|                     ></switch> | ||||
|                 } | ||||
|             ></centerbox> | ||||
|             <label | ||||
|                 marginTop={10} | ||||
|                 label={"Connected & Trusted devices"} | ||||
|                 cssClasses={["title-2"]} | ||||
|             ></label> | ||||
|             <Gtk.Separator marginTop={3} marginBottom={5}></Gtk.Separator> | ||||
|             <box vertical cssClasses={["devices-list"]}> | ||||
|                 {bind(bt, "devices").as(devices => { | ||||
|                     return devices | ||||
|                         .filter(device => { | ||||
|                             if (device.get_connected() || device.get_paired()) { | ||||
|                                 return device; | ||||
|                             } | ||||
|                         }) | ||||
|                         .map(device => { | ||||
|                             return <BTDevice device={device}></BTDevice>; | ||||
|                         }); | ||||
|                 })} | ||||
|             </box> | ||||
|             <label | ||||
|                 visible={bind(bt, "devices").as(devices => { | ||||
|                     return ( | ||||
|                         devices.filter(device => { | ||||
|                             if (device.get_connected() || device.get_paired()) { | ||||
|                                 return device; | ||||
|                             } | ||||
|                         }).length === 0 | ||||
|                     ); | ||||
|                 })} | ||||
|                 label={"No connected / trusted devices"} | ||||
|                 cssClasses={["bt-no-found", "bt-conn-list"]} | ||||
|             ></label> | ||||
|             <label | ||||
|                 label={"Discovered bluetooth devices"} | ||||
|                 cssClasses={["title-2"]} | ||||
|             ></label> | ||||
|             <Gtk.Separator marginBottom={5} marginTop={3}></Gtk.Separator> | ||||
|             <box vertical> | ||||
|                 {bind(bt, "devices").as(devices => { | ||||
|                     return devices | ||||
|                         .filter(data => { | ||||
|                             if (!data.get_connected() && !data.get_paired()) { | ||||
|                                 return data; | ||||
|                             } | ||||
|                         }) | ||||
|                         .map(device => { | ||||
|                             return <BTDevice device={device}></BTDevice>; | ||||
|                         }); | ||||
|                 })} | ||||
|             </box> | ||||
|             <label | ||||
|                 visible={bind(bt, "devices").as(devices => { | ||||
|                     return ( | ||||
|                         devices.filter(device => { | ||||
|                             if ( | ||||
|                                 !device.get_connected() && | ||||
|                                 !device.get_paired() | ||||
|                             ) { | ||||
|                                 return device; | ||||
|                             } | ||||
|                         }).length === 0 | ||||
|                     ); | ||||
|                 })} | ||||
|                 label={"No discovered devices"} | ||||
|                 cssClasses={["bt-no-found"]} | ||||
|             ></label> | ||||
|         </box> | ||||
|     ); | ||||
| }; | ||||
|  | ||||
| const BluetoothPicker = () => { | ||||
|     const popover = new Gtk.Popover(); | ||||
|  | ||||
|     popover.set_child(BluetoothPickerList()); | ||||
|     popover.connect("closed", () => bt.adapter.stop_discovery()); | ||||
|  | ||||
|     return popover; | ||||
| }; | ||||
|  | ||||
| const picker = BluetoothPicker(); | ||||
|  | ||||
| export default { | ||||
|     BluetoothModule, | ||||
| }; | ||||
| @@ -0,0 +1,72 @@ | ||||
| import { bind } from "astal"; | ||||
| import AstalBluetooth from "gi://AstalBluetooth"; | ||||
|  | ||||
| const BTDevice = ({ device }: { device: AstalBluetooth.Device }) => { | ||||
|     return ( | ||||
|         <button | ||||
|             visible={bind(device, "name").as(n => n !== null)} | ||||
|             child={ | ||||
|                 <centerbox | ||||
|                     startWidget={ | ||||
|                         <box> | ||||
|                             <image | ||||
|                                 iconName={"chronometer-reset"} | ||||
|                                 tooltipText={"Device is currently connecting"} | ||||
|                                 visible={bind(device, "connecting")} | ||||
|                             ></image> | ||||
|                             <image | ||||
|                                 iconName={bind(device, "icon")} | ||||
|                                 marginEnd={3} | ||||
|                             ></image> | ||||
|                         </box> | ||||
|                     } | ||||
|                     centerWidget={ | ||||
|                         <label | ||||
|                             label={bind(device, "name").as(n => n ?? "No name")} | ||||
|                             marginEnd={5} | ||||
|                         ></label> | ||||
|                     } | ||||
|                     endWidget={ | ||||
|                         <box> | ||||
|                             <label | ||||
|                                 label={bind(device, "batteryPercentage").as( | ||||
|                                     bat => (bat >= 0 ? bat + "%" : "?%"), | ||||
|                                 )} | ||||
|                                 tooltipText={"Device's battery percentage"} | ||||
|                                 marginEnd={3} | ||||
|                             ></label> | ||||
|                             <image | ||||
|                                 iconName={bind(device, "paired").as(v => | ||||
|                                     v ? "network-bluetooth-activated-symbolic" : "bluetooth-disconnected-symbolic", | ||||
|                                 )} | ||||
|                             ></image> | ||||
|                             <button tooltipText={"Device trusted status"} child={ | ||||
|                                 <image | ||||
|                                     iconName={bind(device, "trusted").as(v => | ||||
|                                         v ? "checkbox" : "window-close-symbolic", | ||||
|                                     )} | ||||
|                                 ></image> | ||||
|                             } onClicked={() => device.set_trusted( !device.get_trusted() )} | ||||
|                             cssClasses={[ 'button-no-margin' ]} | ||||
|                             ></button> | ||||
|                         </box> | ||||
|                     } | ||||
|                 ></centerbox> | ||||
|             } | ||||
|             onClicked={() => { | ||||
|                 connectOrPair( device ); | ||||
|             }} | ||||
|         ></button> | ||||
|     ); | ||||
| }; | ||||
|  | ||||
| const connectOrPair = (device: AstalBluetooth.Device) => { | ||||
|     if ( device.get_paired() ) { | ||||
|         device.connect_device(() => { }); | ||||
|         // Show failed message if tried to connect and failed | ||||
|     } else { | ||||
|         device.pair(); | ||||
|     } | ||||
| }; | ||||
|  | ||||
| export default BTDevice; | ||||
| @@ -0,0 +1,18 @@ | ||||
| import { bind } from "astal"; | ||||
| import Brightness from "../../../../util/brightness"; | ||||
|  | ||||
| const brightness = Brightness.get_default(); | ||||
|  | ||||
| const BrightnessModule = () => { | ||||
|     return ( | ||||
|         <box visible={bind(brightness, 'screenAvailable')}> | ||||
|             <image iconName={"brightness-high-symbolic"}></image> | ||||
|             <label label={bind(brightness, "screen").as(b => b + "%")}></label> | ||||
|             <slider></slider> | ||||
|         </box> | ||||
|     ); | ||||
| }; | ||||
|  | ||||
| export default { | ||||
|     BrightnessModule | ||||
| }; | ||||
| @@ -0,0 +1,221 @@ | ||||
| import { execAsync, bind } from "astal"; | ||||
| import Network from "gi://AstalNetwork"; | ||||
| import { App, Gtk } from "astal/gtk4"; | ||||
| import { NetworkItem } from "./modules/NetworkItem"; | ||||
| import { PasswordDialog } from "./modules/PasswordDialog"; | ||||
| import { | ||||
|   availableNetworks, | ||||
|   savedNetworks, | ||||
|   activeNetwork, | ||||
|   showPasswordDialog, | ||||
|   scanNetworks, | ||||
|   getSavedNetworks, | ||||
|   disconnectNetwork, | ||||
|   forgetNetwork, | ||||
|   isExpanded, | ||||
|   refreshIntervalId, | ||||
| } from "./networkinghelper"; | ||||
|  | ||||
| // Main WiFi Box component | ||||
| export const WiFiBox = () => { | ||||
|   const network = Network.get_default(); | ||||
|  | ||||
|   // Initial scan when component is first used | ||||
|   setTimeout(() => { | ||||
|     scanNetworks(); | ||||
|     getSavedNetworks(); | ||||
|   }, 100); | ||||
|  | ||||
|   return ( | ||||
|     <box vertical cssClasses={["wifi-menu", "toggle"]}> | ||||
|       {/* WiFi Toggle Header */} | ||||
|       <box cssClasses={["toggle", "wifi-toggle"]}> | ||||
|         <button | ||||
|           onClicked={() => { | ||||
|             if (network.wifi.enabled) { | ||||
|               network.wifi.set_enabled(false); | ||||
|             } else network.wifi.set_enabled(true); | ||||
|           }} | ||||
|           cssClasses={bind(network.wifi, "enabled").as((enabled) => | ||||
|             enabled ? ["button"] : ["button-disabled"], | ||||
|           )} | ||||
|         > | ||||
|           <image iconName={bind(network.wifi, "icon_name")} /> | ||||
|         </button> | ||||
|         <button | ||||
|           hexpand={true} | ||||
|           onClicked={() => { | ||||
|             if (network.wifi.enabled) { | ||||
|               isExpanded.set(!isExpanded.get()); | ||||
|               if (isExpanded.get()) { | ||||
|                 scanNetworks(); | ||||
|                 getSavedNetworks(); | ||||
|               } | ||||
|             } | ||||
|           }} | ||||
|         > | ||||
|           <box hexpand={true}> | ||||
|             <label | ||||
|               hexpand={true} | ||||
|               xalign={0} | ||||
|               label={bind(network.wifi, "ssid").as( | ||||
|                 (ssid) => | ||||
|                   ssid || (network.wifi.enabled ? "Not Connected" : "WiFi Off"), | ||||
|               )} | ||||
|             /> | ||||
|             <image | ||||
|               iconName="pan-end-symbolic" | ||||
|               halign={Gtk.Align.END} | ||||
|               cssClasses={bind(isExpanded).as((expanded) => | ||||
|                 expanded | ||||
|                   ? ["arrow-indicator", "arrow-down"] | ||||
|                   : ["arrow-indicator"], | ||||
|               )} | ||||
|             /> | ||||
|           </box> | ||||
|         </button> | ||||
|       </box> | ||||
|  | ||||
|       {/* Networks List Revealer */} | ||||
|       <revealer | ||||
|         transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN} | ||||
|         transitionDuration={300} | ||||
|         revealChild={bind(isExpanded)} | ||||
|         setup={() => { | ||||
|           const clearScanInterval = () => { | ||||
|             if (refreshIntervalId.get()) { | ||||
|               clearInterval( parseInt( '' + refreshIntervalId.get() )); | ||||
|               refreshIntervalId.set(null); | ||||
|             } | ||||
|           }; | ||||
|           bind(isExpanded).subscribe((expanded) => { | ||||
|             // Clear existing interval | ||||
|             clearScanInterval(); | ||||
|  | ||||
|             if (expanded) { | ||||
|               // Scan networks | ||||
|               network.wifi?.scan(); | ||||
|  | ||||
|               // Set up new interval if WiFi is enabled | ||||
|               if (network.wifi?.enabled) { | ||||
|                 refreshIntervalId.set( | ||||
|                   setInterval(() => { | ||||
|                     scanNetworks(); | ||||
|                     getSavedNetworks(); | ||||
|                     print("updated"); | ||||
|                   }, 10000), | ||||
|                 ); | ||||
|               } | ||||
|             } else { | ||||
|               // Apply revealer bug fix when collapsed | ||||
|               App.toggle_window("system-menu"); | ||||
|               App.toggle_window("system-menu"); | ||||
|             } | ||||
|           }); | ||||
|  | ||||
|           // Monitor window toggling | ||||
|           const windowListener = App.connect("window-toggled", (_, window) => { | ||||
|             if (window.name === "system-menu" && isExpanded.get()) { | ||||
|               isExpanded.set(false); | ||||
|             } | ||||
|           }); | ||||
|  | ||||
|           // Clean up resources when component is destroyed | ||||
|           return () => { | ||||
|             App.disconnect(windowListener); | ||||
|             clearScanInterval(); | ||||
|           }; | ||||
|         }} | ||||
|       > | ||||
|         <box vertical cssClasses={["network-list"]}> | ||||
|             <box visible={showPasswordDialog( v => v )}> | ||||
|                 <PasswordDialog /> | ||||
|             </box> | ||||
|  | ||||
|           <label label="Available Networks" cssClasses={["section-label"]} /> | ||||
|         <label label="No networks found" cssClasses={["empty-label"]} visible={availableNetworks( net => net.length === 0 )}/> | ||||
|           <box visible={availableNetworks( networks => networks.length > 1 )}> | ||||
|             {availableNetworks( networks => | ||||
|                 networks.map( (network) => <NetworkItem network={network} />) | ||||
|             )} | ||||
|           </box> | ||||
|  | ||||
|           {savedNetworks((networks) => { | ||||
|             // Filter out networks already shown in available networks | ||||
|             const filteredNetworks = networks.filter( | ||||
|               (ssid) => !availableNetworks.get().some((n) => n.ssid === ssid) | ||||
|             ); | ||||
|  | ||||
|             // Only render the section if there are filtered networks to show | ||||
|             return filteredNetworks.length > 0 ? ( | ||||
|               <box vertical> | ||||
|                 <label label="Saved Networks" cssClasses={["section-label"]} /> | ||||
|                 {filteredNetworks.map((ssid) => ( | ||||
|                   <box cssClasses={["saved-network"]}> | ||||
|                     <label label={ssid} /> | ||||
|                     <box hexpand={true} /> | ||||
|                     <button | ||||
|                       label="Forget" | ||||
|                       cssClasses={["forget-button", "button"]} | ||||
|                       onClicked={() => forgetNetwork(ssid)} | ||||
|                     /> | ||||
|                   </box> | ||||
|                 ))} | ||||
|               </box> | ||||
|             ) : ( | ||||
|               <box></box> | ||||
|             ); | ||||
|           })} | ||||
|  | ||||
|           <box hexpand> | ||||
|             <button | ||||
|               halign={Gtk.Align.START} | ||||
|               cssClasses={["refresh-button"]} | ||||
|               onClicked={() => { | ||||
|                 scanNetworks(); | ||||
|                 getSavedNetworks(); | ||||
|               }} | ||||
|             > | ||||
|               <image iconName="view-refresh-symbolic" /> | ||||
|             </button> | ||||
|             {/* Connected Network Options */} | ||||
|             <box hexpand> | ||||
|               {activeNetwork((active) => | ||||
|                 active ? ( | ||||
|                   <box vertical cssClasses={["connected-network"]} hexpand> | ||||
|                     <button | ||||
|                       label="Disconnect" | ||||
|                       cssClasses={["disconnect-button"]} | ||||
|                       onClicked={() => disconnectNetwork(active.ssid)} | ||||
|                     /> | ||||
|                   </box> | ||||
|                 ) : ( | ||||
|                   "" | ||||
|                 ), | ||||
|               )} | ||||
|             </box> | ||||
|                 <button | ||||
|                   cssClasses={["settings-button"]} | ||||
|                   halign={Gtk.Align.END} | ||||
|                   hexpand={false} | ||||
|                   onClicked={() => { | ||||
|                     execAsync([ | ||||
|                       "sh", | ||||
|                       "-c", | ||||
|                       "XDG_CURRENT_DESKTOP=GNOME gnome-control-center wifi", | ||||
|                     ]); | ||||
|                     isExpanded.set(false); | ||||
|                   }} | ||||
|                 > | ||||
|                   <image iconName={"emblem-system-symbolic"} /> | ||||
|                 </button> | ||||
|               ) : ( | ||||
|                 "" | ||||
|               ), | ||||
|             )} | ||||
|           </box> | ||||
|         </box> | ||||
|       </revealer> | ||||
|     </box> | ||||
|   ); | ||||
| }; | ||||
| @@ -0,0 +1,2 @@ | ||||
| # Source | ||||
| This is a modified version from [MatShell](https://github.com/Neurian/matshell) | ||||
							
								
								
									
										14
									
								
								config/astal/components/QuickActions/modules/Networking-old/network.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								config/astal/components/QuickActions/modules/Networking-old/network.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| import AstalNetwork from "gi://AstalNetwork?version=0.1"; | ||||
|  | ||||
| interface CurrentWiFi { | ||||
|     ssid: string; | ||||
|     strength: number; | ||||
|     secured: boolean; | ||||
| } | ||||
|  | ||||
| interface WiFiDetails extends CurrentWiFi { | ||||
|     active: boolean; | ||||
|     accessPoint: AstalNetwork.AccessPoint; | ||||
|     iconName: string; | ||||
| } | ||||
|  | ||||
| @@ -0,0 +1,161 @@ | ||||
| // From https://github.com/Neurarian/matshell/blob/master/utils/wifi.ts | ||||
|  | ||||
| import { execAsync, Variable } from "astal"; | ||||
| import Network from "gi://AstalNetwork"; | ||||
| import { CurrentWiFi, WiFiDetails } from "./network"; | ||||
|  | ||||
| // State trackers | ||||
| export const availableNetworks: Variable<WiFiDetails[]> = Variable([]); | ||||
| export const savedNetworks: Variable<string[]> = Variable([]); | ||||
| export const activeNetwork: Variable<CurrentWiFi | null> = Variable(null); | ||||
| export const isConnecting: Variable<boolean> = Variable(false); | ||||
| export const showPasswordDialog: Variable<boolean> = Variable(false); | ||||
| export const errorMessage: Variable<string> = Variable(""); | ||||
| export const isExpanded: Variable<boolean> = Variable(false); | ||||
| export const passwordInput: Variable<string> = Variable(""); | ||||
| export const selectedNetwork: Variable<null | WiFiDetails> = Variable(null); | ||||
| export const refreshIntervalId: Variable< | ||||
|     number | null | ReturnType<typeof setTimeout> | ||||
| > = Variable(null); | ||||
|  | ||||
| // Function to scan for available networks | ||||
| export const scanNetworks = () => { | ||||
|     const network = Network.get_default(); | ||||
|     if (network && network.wifi) { | ||||
|         network.wifi.scan(); | ||||
|  | ||||
|         // Get available networks from access points | ||||
|         const networks: WiFiDetails[] = network.wifi.accessPoints | ||||
|             .map(ap => ({ | ||||
|                 ssid: ap.ssid, | ||||
|                 strength: ap.strength, | ||||
|                 secured: ap.flags !== 0, | ||||
|                 active: network.wifi.activeAccessPoint?.ssid === ap.ssid, | ||||
|                 accessPoint: ap, | ||||
|                 iconName: ap.iconName, | ||||
|             })) | ||||
|             .filter(n => n.ssid); | ||||
|  | ||||
|         // Sort by signal strength | ||||
|         networks.sort((a, b) => b.strength - a.strength); | ||||
|  | ||||
|         // Remove duplicates (same SSID) | ||||
|         const uniqueNetworks: WiFiDetails[] = []; | ||||
|         const seen = new Set(); | ||||
|         networks.forEach(network => { | ||||
|             if (!seen.has(network.ssid)) { | ||||
|                 seen.add(network.ssid); | ||||
|                 uniqueNetworks.push(network); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         availableNetworks.set(uniqueNetworks); | ||||
|  | ||||
|         // Update active network | ||||
|         if (network.wifi.activeAccessPoint) { | ||||
|             activeNetwork.set({ | ||||
|                 ssid: network.wifi.activeAccessPoint.ssid, | ||||
|                 strength: network.wifi.activeAccessPoint.strength, | ||||
|                 secured: network.wifi.activeAccessPoint.flags !== 0, | ||||
|             }); | ||||
|         } else { | ||||
|             activeNetwork.set(null); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|  | ||||
| // Function to list saved networks | ||||
| export const getSavedNetworks = () => { | ||||
|     execAsync(["bash", "-c", "nmcli -t -f NAME,TYPE connection show"]) | ||||
|         .then(output => { | ||||
|             if (typeof output === "string") { | ||||
|                 const savedWifiNetworks = output | ||||
|                     .split("\n") | ||||
|                     .filter(line => line.includes("802-11-wireless")) | ||||
|                     .map(line => line.split(":")[0].trim()); | ||||
|                 savedNetworks.set(savedWifiNetworks); | ||||
|             } | ||||
|         }) | ||||
|         .catch(error => console.error("Error fetching saved networks:", error)); | ||||
| }; | ||||
|  | ||||
| // Function to connect to a network | ||||
|  | ||||
| export const connectToNetwork = ( | ||||
|     ssid: string, | ||||
|     password: null | string = null, | ||||
| ) => { | ||||
|     isConnecting.set(true); | ||||
|     errorMessage.set(""); | ||||
|     const network = Network.get_default(); | ||||
|     const currentSsid = network.wifi.ssid; | ||||
|  | ||||
|     // Function to perform the actual connection | ||||
|     const performConnection = () => { | ||||
|         let command = ""; | ||||
|         if (password) { | ||||
|             // Connect with password | ||||
|             command = `echo '${password}' | nmcli device wifi connect "${ssid}" --ask`; | ||||
|         } else { | ||||
|             // Connect without password (saved or open network) | ||||
|             command = `nmcli connection up "${ssid}" || nmcli device wifi connect "${ssid}"`; | ||||
|         } | ||||
|  | ||||
|         execAsync(["bash", "-c", command]) | ||||
|             .then(() => { | ||||
|                 showPasswordDialog.set(false); | ||||
|                 isConnecting.set(false); | ||||
|                 scanNetworks(); // Refresh network list | ||||
|             }) | ||||
|             .catch(error => { | ||||
|                 console.error("Connection error:", error); | ||||
|                 errorMessage.set("Failed to connect. Check password."); | ||||
|                 isConnecting.set(false); | ||||
|             }); | ||||
|     }; | ||||
|  | ||||
|     // If already connected to a network, disconnect first | ||||
|     if (currentSsid && currentSsid !== ssid) { | ||||
|         console.log( | ||||
|             `Disconnecting from ${currentSsid} before connecting to ${ssid}`, | ||||
|         ); | ||||
|         execAsync(["bash", "-c", `nmcli connection down "${currentSsid}"`]) | ||||
|             .then(() => { | ||||
|                 // Wait a moment for the disconnection to complete fully | ||||
|                 setTimeout(() => { | ||||
|                     performConnection(); | ||||
|                 }, 500); // 500ms delay for clean disconnection | ||||
|             }) | ||||
|             .catch(error => { | ||||
|                 console.error("Disconnect error:", error); | ||||
|                 // Continue with connection attempt even if disconnect fails | ||||
|                 performConnection(); | ||||
|             }); | ||||
|     } else { | ||||
|         // No active connection or connecting to same network (reconnect case) | ||||
|         performConnection(); | ||||
|     } | ||||
| }; | ||||
|  | ||||
| // Function to disconnect from a network | ||||
| export const disconnectNetwork = (ssid: string) => { | ||||
|     execAsync(["bash", "-c", `nmcli connection down "${ssid}"`]) | ||||
|         .then(() => { | ||||
|             scanNetworks(); // Refresh network list | ||||
|         }) | ||||
|         .catch(error => { | ||||
|             console.error("Disconnect error:", error); | ||||
|         }); | ||||
| }; | ||||
|  | ||||
| // Function to forget a saved network | ||||
| export const forgetNetwork = (ssid: string) => { | ||||
|     execAsync(["bash", "-c", `nmcli connection delete "${ssid}"`]) | ||||
|         .then(() => { | ||||
|             getSavedNetworks(); // Refresh saved networks list | ||||
|             scanNetworks(); // Refresh network list | ||||
|         }) | ||||
|         .catch(error => { | ||||
|             console.error("Forget network error:", error); | ||||
|         }); | ||||
| }; | ||||
| @@ -0,0 +1,97 @@ | ||||
| import { bind } from "astal"; | ||||
| import { Gtk } from "astal/gtk4"; | ||||
| import AstalNetwork from "gi://AstalNetwork"; | ||||
| import networkHelper from "./network-helper"; | ||||
| import NetworkMenu from "./NetworkMenu"; | ||||
|  | ||||
| const net = AstalNetwork.get_default(); | ||||
| const STATE = AstalNetwork.DeviceState; | ||||
|  | ||||
| const Network = () => { | ||||
|     const netMenu = NetworkMenu.NetworkMenu(); | ||||
|     return ( | ||||
|         <box> | ||||
|             <button | ||||
|                 cssClasses={networkHelper.networkEnabled(en => { | ||||
|                     if (en) return ["toggle-button", "toggle-on"]; | ||||
|                     else return ["toggle-button"]; | ||||
|                 })} | ||||
|                 onClicked={() => | ||||
|                     networkHelper.setNetworking( | ||||
|                         !networkHelper.networkEnabled.get(), | ||||
|                     ) | ||||
|                 } | ||||
|                 child={ | ||||
|                     <box vertical> | ||||
|                         <label | ||||
|                             label={bind(net.wifi, "enabled").as( | ||||
|                                 stat => `Network (${stat ? "WiFi" : "Wired"})`, | ||||
|                             )} | ||||
|                             cssClasses={["title-2"]} | ||||
|                         ></label> | ||||
|                         <label | ||||
|                             label={bind(net.wired, "state").as(state => { | ||||
|                                 if (state === STATE.ACTIVATED) { | ||||
|                                     return ( | ||||
|                                         "Wired. IP: " + networkHelper.getIP() | ||||
|                                     ); | ||||
|                                 } else if (state === STATE.DISCONNECTED) { | ||||
|                                     return "Disconnected"; | ||||
|                                 } else if (state === STATE.FAILED) { | ||||
|                                     return "Error"; | ||||
|                                 } else if ( | ||||
|                                     state === STATE.PREPARE || | ||||
|                                     state === STATE.CONFIG || | ||||
|                                     state === STATE.IP_CHECK || | ||||
|                                     state === STATE.IP_CONFIG | ||||
|                                 ) { | ||||
|                                     return "Connecting..."; | ||||
|                                 } else { | ||||
|                                     return "Unavailable"; | ||||
|                                 } | ||||
|                             })} | ||||
|                             visible={bind(net.wifi, "enabled").as(v => !v)} | ||||
|                         ></label> | ||||
|                         <label | ||||
|                             label={bind(net.wifi, "state").as(state => { | ||||
|                                 if (state === STATE.ACTIVATED) { | ||||
|                                     return `${net.wifi.get_ssid()} (${networkHelper.getIP()})`; | ||||
|                                 } else if (state === STATE.DISCONNECTED) { | ||||
|                                     return "Disconnected"; | ||||
|                                 } else if (state === STATE.FAILED) { | ||||
|                                     return "Error"; | ||||
|                                 } else if ( | ||||
|                                     state === STATE.PREPARE || | ||||
|                                     state === STATE.CONFIG || | ||||
|                                     state === STATE.IP_CHECK || | ||||
|                                     state === STATE.IP_CONFIG | ||||
|                                 ) { | ||||
|                                     return "Connecting..."; | ||||
|                                 } else { | ||||
|                                     return "Unavailable"; | ||||
|                                 } | ||||
|                             })} | ||||
|                             visible={bind(net.wifi, "enabled")} | ||||
|                         ></label> | ||||
|                     </box> | ||||
|                 } | ||||
|             ></button> | ||||
|             <button | ||||
|                 cssClasses={["actions-button"]} | ||||
|                 visible={networkHelper.networkEnabled()} | ||||
|                 onClicked={() => netMenu.popup()} | ||||
|                 child={ | ||||
|                     <box> | ||||
|                         <image iconName={"arrow-right-symbolic"}></image> | ||||
|                         { netMenu } | ||||
|                     </box> | ||||
|                 } | ||||
|                 tooltipText={"View available devices"} | ||||
|             ></button> | ||||
|         </box> | ||||
|     ); | ||||
| }; | ||||
|  | ||||
| export default { | ||||
|     Network, | ||||
| }; | ||||
| @@ -0,0 +1,18 @@ | ||||
| import { Gtk } from "astal/gtk4"; | ||||
|  | ||||
| const NetworkMenu = () => { | ||||
|     const popover = new Gtk.Popover(); | ||||
|     popover.set_child( renderMenu() ); | ||||
|     return popover; | ||||
| }; | ||||
|  | ||||
| const renderMenu = () => { | ||||
|     return <box vertical> | ||||
|         <image iconName={"appointment-soon-symbolic"} iconSize={Gtk.IconSize.LARGE}></image> | ||||
|         <label label={"Coming later"}></label> | ||||
|     </box>; | ||||
| }; | ||||
|  | ||||
| export default { | ||||
|     NetworkMenu, | ||||
| }; | ||||
							
								
								
									
										91
									
								
								config/astal/components/QuickActions/modules/Networking/dump
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								config/astal/components/QuickActions/modules/Networking/dump
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,91 @@ | ||||
| import { bind } from "astal"; | ||||
| import { Gtk } from "astal/gtk4"; | ||||
| import AstalNetwork from "gi://AstalNetwork"; | ||||
| import networkHelper from "./network-helper"; | ||||
|  | ||||
| const net = AstalNetwork.get_default(); | ||||
| const STATE = AstalNetwork.DeviceState; | ||||
|  | ||||
| const WiFiList = () => { | ||||
|     const popover = new Gtk.Popover({ cssClasses: ["WiFiPicker"] }); | ||||
|  | ||||
|     return popover; | ||||
| }; | ||||
|  | ||||
| const renderWiFiList = () => { | ||||
|     return <box> | ||||
|         <label label="Test"></label> | ||||
|     </box> | ||||
| } | ||||
|  | ||||
| const Network = () => { | ||||
|     const wifiList = WiFiList(); | ||||
|  | ||||
|     return ( | ||||
|         <box> | ||||
|             <button | ||||
|                 cssClasses={networkHelper.networkEnabled(en => { | ||||
|                     if (en) return ["network-button", "net-on"]; | ||||
|                     else return ["network-button"]; | ||||
|                 })} | ||||
|                 onClicked={() => | ||||
|                     networkHelper.setNetworking( | ||||
|                         !networkHelper.networkEnabled.get(), | ||||
|                     ) | ||||
|                 } | ||||
|                 child={<box vertical> | ||||
|                     <label label="Wired" cssClasses={[ 'button-name' ]}></label> | ||||
|                     <label label={bind( net.wired, 'state' ).as( state => { | ||||
|                         if ( state === STATE.ACTIVATED ) { | ||||
|                             return 'Connected. IP: ' + networkHelper.getIP(); | ||||
|                         } else if ( state === STATE.DISCONNECTED ) { | ||||
|                             return 'Disconnected'; | ||||
|                         } else if ( state === STATE.FAILED ) { | ||||
|                             return 'Error'; | ||||
|                         } else if ( state === STATE.PREPARE || state === STATE.CONFIG || state === STATE.IP_CHECK || state === STATE.IP_CONFIG ) { | ||||
|                             return 'Connecting...'; | ||||
|                         } else { | ||||
|                             return 'Unavailable'; | ||||
|                         } | ||||
|                     } )}></label> | ||||
|                 </box>} | ||||
|             ></button> | ||||
|             <box> | ||||
|                 <button | ||||
|                     cssClasses={bind(net.wifi, "enabled").as(b => { | ||||
|                         const classes = ["network-button"]; | ||||
|                         if (b) { | ||||
|                             classes.push("wifi-on"); | ||||
|                         } | ||||
|                         return classes; | ||||
|                     })} | ||||
|                     child={<box vertical> | ||||
|                         <label label="WiFi" cssClasses={[ 'button-name' ]}></label> | ||||
|                         <label label={bind( net.wifi, 'state' ).as( state => { | ||||
|                             if ( state === STATE.ACTIVATED ) { | ||||
|                                 return 'Connected. IP: ' + networkHelper.getIP(); | ||||
|                             } else if ( state === STATE.DISCONNECTED ) { | ||||
|                                 return 'Disconnected'; | ||||
|                             } else if ( state === STATE.FAILED ) { | ||||
|                                 return 'Error'; | ||||
|                             } else if ( state === STATE.PREPARE || state === STATE.CONFIG || state === STATE.IP_CHECK || state === STATE.IP_CONFIG ) { | ||||
|                                 return 'Connecting...'; | ||||
|                             } else { | ||||
|                                 return 'Unavailable'; | ||||
|                             } | ||||
|                         } )} visible={bind(net.wifi, 'enabled').as( en => en )}></label> | ||||
|                         <label label="Disabled" visible={bind(net.wifi, 'enabled').as( en => !en )}></label> | ||||
|                     </box>} | ||||
|                 ></button> | ||||
|                 <button | ||||
|                     cssClasses={["network-button-context"]} | ||||
|                     visible={bind(net.wifi, "enabled").as(b => b)} | ||||
|                     onClicked={() => wifiList.popup()} | ||||
|                 ></button> | ||||
|             </box> | ||||
|             {wifiList} | ||||
|         </box> | ||||
|     ); | ||||
| }; | ||||
|  | ||||
| export default Network; | ||||
| @@ -0,0 +1,28 @@ | ||||
| import { exec, Variable } from "astal"; | ||||
| import AstalNetwork from "gi://AstalNetwork"; | ||||
|  | ||||
| const networkEnabled = Variable( exec( 'nmcli networking connectivity' ) !== 'none' ); | ||||
| const network = AstalNetwork.get_default(); | ||||
|  | ||||
|  | ||||
| const setNetworking = ( status: boolean ) => { | ||||
|     if ( status === true ) { | ||||
|         exec( 'nmcli networking on' ); | ||||
|         networkEnabled.set( true ); | ||||
|     } else { | ||||
|         exec( 'nmcli networking off' ); | ||||
|         networkEnabled.set( false ); | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
| const getIP = () => { | ||||
|     return exec( `/bin/bash -c "ip addr show | grep 'inet ' | awk '{print $2}' | grep -v '127'"` ).split( '/' )[ 0 ]; | ||||
| } | ||||
|  | ||||
|  | ||||
| export default { | ||||
|     networkEnabled, | ||||
|     setNetworking, | ||||
|     getIP | ||||
| } | ||||
							
								
								
									
										14
									
								
								config/astal/components/QuickActions/modules/Networking/network.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								config/astal/components/QuickActions/modules/Networking/network.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| import AstalNetwork from "gi://AstalNetwork?version=0.1"; | ||||
|  | ||||
| interface CurrentWiFi { | ||||
|     ssid: string; | ||||
|     strength: number; | ||||
|     secured: boolean; | ||||
| } | ||||
|  | ||||
| interface WiFiDetails extends CurrentWiFi { | ||||
|     active: boolean; | ||||
|     accessPoint: AstalNetwork.AccessPoint; | ||||
|     iconName: string; | ||||
| } | ||||
|  | ||||
| @@ -0,0 +1,161 @@ | ||||
| // From https://github.com/Neurarian/matshell/blob/master/utils/wifi.ts | ||||
|  | ||||
| import { execAsync, Variable } from "astal"; | ||||
| import Network from "gi://AstalNetwork"; | ||||
| import { CurrentWiFi, WiFiDetails } from "./network"; | ||||
|  | ||||
| // State trackers | ||||
| export const availableNetworks: Variable<WiFiDetails[]> = Variable([]); | ||||
| export const savedNetworks: Variable<string[]> = Variable([]); | ||||
| export const activeNetwork: Variable<CurrentWiFi | null> = Variable(null); | ||||
| export const isConnecting: Variable<boolean> = Variable(false); | ||||
| export const showPasswordDialog: Variable<boolean> = Variable(false); | ||||
| export const errorMessage: Variable<string> = Variable(""); | ||||
| export const isExpanded: Variable<boolean> = Variable(false); | ||||
| export const passwordInput: Variable<string> = Variable(""); | ||||
| export const selectedNetwork: Variable<null | WiFiDetails> = Variable(null); | ||||
| export const refreshIntervalId: Variable< | ||||
|     number | null | ReturnType<typeof setTimeout> | ||||
| > = Variable(null); | ||||
|  | ||||
| // Function to scan for available networks | ||||
| export const scanNetworks = () => { | ||||
|     const network = Network.get_default(); | ||||
|     if (network && network.wifi) { | ||||
|         network.wifi.scan(); | ||||
|  | ||||
|         // Get available networks from access points | ||||
|         const networks: WiFiDetails[] = network.wifi.accessPoints | ||||
|             .map(ap => ({ | ||||
|                 ssid: ap.ssid, | ||||
|                 strength: ap.strength, | ||||
|                 secured: ap.flags !== 0, | ||||
|                 active: network.wifi.activeAccessPoint?.ssid === ap.ssid, | ||||
|                 accessPoint: ap, | ||||
|                 iconName: ap.iconName, | ||||
|             })) | ||||
|             .filter(n => n.ssid); | ||||
|  | ||||
|         // Sort by signal strength | ||||
|         networks.sort((a, b) => b.strength - a.strength); | ||||
|  | ||||
|         // Remove duplicates (same SSID) | ||||
|         const uniqueNetworks: WiFiDetails[] = []; | ||||
|         const seen = new Set(); | ||||
|         networks.forEach(network => { | ||||
|             if (!seen.has(network.ssid)) { | ||||
|                 seen.add(network.ssid); | ||||
|                 uniqueNetworks.push(network); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         availableNetworks.set(uniqueNetworks); | ||||
|  | ||||
|         // Update active network | ||||
|         if (network.wifi.activeAccessPoint) { | ||||
|             activeNetwork.set({ | ||||
|                 ssid: network.wifi.activeAccessPoint.ssid, | ||||
|                 strength: network.wifi.activeAccessPoint.strength, | ||||
|                 secured: network.wifi.activeAccessPoint.flags !== 0, | ||||
|             }); | ||||
|         } else { | ||||
|             activeNetwork.set(null); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|  | ||||
| // Function to list saved networks | ||||
| export const getSavedNetworks = () => { | ||||
|     execAsync(["bash", "-c", "nmcli -t -f NAME,TYPE connection show"]) | ||||
|         .then(output => { | ||||
|             if (typeof output === "string") { | ||||
|                 const savedWifiNetworks = output | ||||
|                     .split("\n") | ||||
|                     .filter(line => line.includes("802-11-wireless")) | ||||
|                     .map(line => line.split(":")[0].trim()); | ||||
|                 savedNetworks.set(savedWifiNetworks); | ||||
|             } | ||||
|         }) | ||||
|         .catch(error => console.error("Error fetching saved networks:", error)); | ||||
| }; | ||||
|  | ||||
| // Function to connect to a network | ||||
|  | ||||
| export const connectToNetwork = ( | ||||
|     ssid: string, | ||||
|     password: null | string = null, | ||||
| ) => { | ||||
|     isConnecting.set(true); | ||||
|     errorMessage.set(""); | ||||
|     const network = Network.get_default(); | ||||
|     const currentSsid = network.wifi.ssid; | ||||
|  | ||||
|     // Function to perform the actual connection | ||||
|     const performConnection = () => { | ||||
|         let command = ""; | ||||
|         if (password) { | ||||
|             // Connect with password | ||||
|             command = `echo '${password}' | nmcli device wifi connect "${ssid}" --ask`; | ||||
|         } else { | ||||
|             // Connect without password (saved or open network) | ||||
|             command = `nmcli connection up "${ssid}" || nmcli device wifi connect "${ssid}"`; | ||||
|         } | ||||
|  | ||||
|         execAsync(["bash", "-c", command]) | ||||
|             .then(() => { | ||||
|                 showPasswordDialog.set(false); | ||||
|                 isConnecting.set(false); | ||||
|                 scanNetworks(); // Refresh network list | ||||
|             }) | ||||
|             .catch(error => { | ||||
|                 console.error("Connection error:", error); | ||||
|                 errorMessage.set("Failed to connect. Check password."); | ||||
|                 isConnecting.set(false); | ||||
|             }); | ||||
|     }; | ||||
|  | ||||
|     // If already connected to a network, disconnect first | ||||
|     if (currentSsid && currentSsid !== ssid) { | ||||
|         console.log( | ||||
|             `Disconnecting from ${currentSsid} before connecting to ${ssid}`, | ||||
|         ); | ||||
|         execAsync(["bash", "-c", `nmcli connection down "${currentSsid}"`]) | ||||
|             .then(() => { | ||||
|                 // Wait a moment for the disconnection to complete fully | ||||
|                 setTimeout(() => { | ||||
|                     performConnection(); | ||||
|                 }, 500); // 500ms delay for clean disconnection | ||||
|             }) | ||||
|             .catch(error => { | ||||
|                 console.error("Disconnect error:", error); | ||||
|                 // Continue with connection attempt even if disconnect fails | ||||
|                 performConnection(); | ||||
|             }); | ||||
|     } else { | ||||
|         // No active connection or connecting to same network (reconnect case) | ||||
|         performConnection(); | ||||
|     } | ||||
| }; | ||||
|  | ||||
| // Function to disconnect from a network | ||||
| export const disconnectNetwork = (ssid: string) => { | ||||
|     execAsync(["bash", "-c", `nmcli connection down "${ssid}"`]) | ||||
|         .then(() => { | ||||
|             scanNetworks(); // Refresh network list | ||||
|         }) | ||||
|         .catch(error => { | ||||
|             console.error("Disconnect error:", error); | ||||
|         }); | ||||
| }; | ||||
|  | ||||
| // Function to forget a saved network | ||||
| export const forgetNetwork = (ssid: string) => { | ||||
|     execAsync(["bash", "-c", `nmcli connection delete "${ssid}"`]) | ||||
|         .then(() => { | ||||
|             getSavedNetworks(); // Refresh saved networks list | ||||
|             scanNetworks(); // Refresh network list | ||||
|         }) | ||||
|         .catch(error => { | ||||
|             console.error("Forget network error:", error); | ||||
|         }); | ||||
| }; | ||||
| @@ -0,0 +1,56 @@ | ||||
| $fg-color: #{"@theme_fg_color"}; | ||||
| $bg-color: #{"@theme_bg_color"}; | ||||
|  | ||||
| box.players-box { | ||||
|   margin-top: 20px; | ||||
| } | ||||
|  | ||||
| box.player { | ||||
|   padding: 0.6rem; | ||||
|  | ||||
|   .cover-art { | ||||
|     min-width: 100px; | ||||
|     min-height: 100px; | ||||
|     border-radius: 9px; | ||||
|     margin-right: 0.6rem; | ||||
|     background-size: contain; | ||||
|     background-position: center; | ||||
|   } | ||||
|  | ||||
|   .title { | ||||
|     font-weight: bold; | ||||
|     font-size: 1.1em; | ||||
|   } | ||||
|  | ||||
|   scale { | ||||
|     padding: 0; | ||||
|     margin: 0.4rem 0; | ||||
|     border-radius: 20px; | ||||
|  | ||||
|     trough { | ||||
|       min-height: 8px; | ||||
|       border-radius: 20px; | ||||
|     } | ||||
|  | ||||
|     highlight { | ||||
|       background-color: $fg-color; | ||||
|       border-radius: 20px; | ||||
|     } | ||||
|  | ||||
|     slider { | ||||
|       all: unset; | ||||
|       border-radius: 20px; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   centerbox.actions { | ||||
|     min-width: 220px; | ||||
|  | ||||
|     button { | ||||
|       min-width: 0; | ||||
|       min-height: 0; | ||||
|       padding: 0.4rem; | ||||
|       margin: 0 0.2rem; | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										154
									
								
								config/astal/components/QuickActions/modules/Player/Player.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										154
									
								
								config/astal/components/QuickActions/modules/Player/Player.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,154 @@ | ||||
| import { bind } from "astal"; | ||||
| import { Gtk } from "astal/gtk4"; | ||||
| import AstalMpris from "gi://AstalMpris"; | ||||
| import Pango from "gi://Pango?version=1.0"; | ||||
| const ALIGN = Gtk.Align; | ||||
|  | ||||
| const mpris = AstalMpris.get_default(); | ||||
| mpris.connect("player-added", p => { | ||||
|     print("Player added:", p); | ||||
| }); | ||||
|  | ||||
| const PlayerModule = () => { | ||||
|     return ( | ||||
|         <box vertical cssClasses={ [ 'players-box' ] }> | ||||
|             <label label={"Music Players"} halign={ALIGN.CENTER} cssClasses={[ 'title-2' ]}></label> | ||||
|             <Gtk.Separator marginTop={3} marginBottom={5}></Gtk.Separator> | ||||
|             <box cssClasses={["players"]}> | ||||
|                 {bind(mpris, "players").as(players => { | ||||
|                     return players.map(player => { | ||||
|                         return <PlayerItem player={player}></PlayerItem>; | ||||
|                     }); | ||||
|                 })} | ||||
|             </box> | ||||
|             <label label={"No playback active"} visible={bind(mpris, "players").as( players => players.length === 0 )}></label> | ||||
|         </box> | ||||
|     ); | ||||
| }; | ||||
|  | ||||
| // TODO: Update widths | ||||
| const pbStatus = AstalMpris.PlaybackStatus; | ||||
| const PlayerItem = ({ player }: { player: AstalMpris.Player }) => { | ||||
|     return ( | ||||
|         <box cssClasses={["player"]}> | ||||
|             <image | ||||
|                 cssClasses={["cover-art"]} | ||||
|                 file={bind(player, "coverArt")} | ||||
|                 hexpand | ||||
|                 vexpand | ||||
|             ></image> | ||||
|             <box vertical> | ||||
|                 <label | ||||
|                     label={bind(player, "title").as( | ||||
|                         title => title ?? "Unknown title", | ||||
|                     )} | ||||
|                     cssClasses={["title"]} | ||||
|                     halign={ALIGN.START} | ||||
|                     valign={ALIGN.START} | ||||
|                     maxWidthChars={30} | ||||
|                     ellipsize={Pango.EllipsizeMode.END} | ||||
|                 ></label> | ||||
|                 <label | ||||
|                     label={bind(player, "artist").as( | ||||
|                         artist => artist ?? "Unknown artist", | ||||
|                     )} | ||||
|                     halign={ALIGN.START} | ||||
|                     valign={ALIGN.START} | ||||
|                     maxWidthChars={30} | ||||
|                     ellipsize={Pango.EllipsizeMode.END} | ||||
|                 ></label> | ||||
|                 <slider | ||||
|                     visible={bind(player, "length").as(l => l > 0)} | ||||
|                     value={bind(player, "position")} | ||||
|                     min={0} | ||||
|                     max={bind(player, "length")} | ||||
|                     onChangeValue={v => | ||||
|                         player.set_position(v.get_value()) | ||||
|                     } | ||||
|                 ></slider> | ||||
|                 <centerbox | ||||
|                     cssClasses={["actions"]} | ||||
|                     startWidget={ | ||||
|                         <label | ||||
|                             label={bind(player, "position").as(v => | ||||
|                                 secondsToFriendlyTime(v), | ||||
|                             )} | ||||
|                             hexpand | ||||
|                             cssClasses={["position"]} | ||||
|                         ></label> | ||||
|                     } | ||||
|                     centerWidget={ | ||||
|                         <box> | ||||
|                             <button | ||||
|                                 visible={bind(player, "canGoPrevious")} | ||||
|                                 child={ | ||||
|                                     <image | ||||
|                                         iconName={ | ||||
|                                             "media-skip-backward-symbolic" | ||||
|                                         } | ||||
|                                     ></image> | ||||
|                                 } | ||||
|                                 onClicked={() => player.previous()} | ||||
|                             ></button> | ||||
|                             <button | ||||
|                                 visible={bind(player, "canControl")} | ||||
|                                 child={ | ||||
|                                     <image | ||||
|                                         iconName={bind( | ||||
|                                             player, | ||||
|                                             "playbackStatus", | ||||
|                                         ).as(status => { | ||||
|                                             if (status === pbStatus.PLAYING) { | ||||
|                                                 return "media-playback-pause-symbolic"; | ||||
|                                             } else { | ||||
|                                                 return "media-playback-start-symbolic"; | ||||
|                                             } | ||||
|                                         })} | ||||
|                                     ></image> | ||||
|                                 } | ||||
|                                 onClicked={() => player.play_pause()} | ||||
|                             ></button> | ||||
|                             <button | ||||
|                                 visible={bind(player, "canGoNext")} | ||||
|                                 child={ | ||||
|                                     <image | ||||
|                                         iconName={"media-skip-forward-symbolic"} | ||||
|                                     ></image> | ||||
|                                 } | ||||
|                                 onClicked={() => player.next()} | ||||
|                             ></button> | ||||
|                         </box> | ||||
|                     } | ||||
|                     endWidget={ | ||||
|                         <label | ||||
|                             cssClasses={["length"]} | ||||
|                             hexpand | ||||
|                             label={bind(player, "length").as(v => | ||||
|                                 secondsToFriendlyTime(v), | ||||
|                             )} | ||||
|                         ></label> | ||||
|                     } | ||||
|                 ></centerbox> | ||||
|             </box> | ||||
|         </box> | ||||
|     ); | ||||
| }; | ||||
|  | ||||
| const secondsToFriendlyTime = (time: number) => { | ||||
|     const minutes = Math.floor(time / 60); | ||||
|     const hours = Math.floor(minutes / 60); | ||||
|     const seconds = Math.floor(time % 60); | ||||
|     if (hours > 0) { | ||||
|         return `${hours}:${expandTime(minutes)}:${expandTime(seconds)}`; | ||||
|     } else { | ||||
|         return `${minutes}:${expandTime(seconds)}`; | ||||
|     } | ||||
| }; | ||||
|  | ||||
| const expandTime = (time: number): string => { | ||||
|     return time < 10 ? `0${time}` : "" + time; | ||||
| }; | ||||
|  | ||||
| export default { | ||||
|     PlayerModule, | ||||
| }; | ||||
							
								
								
									
										85
									
								
								config/astal/components/QuickActions/modules/Power.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								config/astal/components/QuickActions/modules/Power.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,85 @@ | ||||
| import { exec } from "astal"; | ||||
| import { Gtk } from "astal/gtk4"; | ||||
|  | ||||
| const PowerMenu = (): Gtk.Popover => { | ||||
|     const popover = new Gtk.Popover({ cssClasses: ["PowerMenu"] }); | ||||
|  | ||||
|     const powerMenuBox = () => { | ||||
|         return ( | ||||
|             <box> | ||||
|                 <button | ||||
|                     cssClasses={["power-button"]} | ||||
|                     child={ | ||||
|                         <image iconName={"system-shutdown-symbolic"}></image> | ||||
|                     } | ||||
|                     onClicked={() => exec("/bin/sh -c 'shutdown now'")} | ||||
|                 ></button> | ||||
|                 <button | ||||
|                     cssClasses={["power-button"]} | ||||
|                     child={<image iconName={"system-reboot-symbolic"}></image>} | ||||
|                     onClicked={() => exec("/bin/sh -c 'reboot'")} | ||||
|                 ></button> | ||||
|                 <button | ||||
|                     cssClasses={["power-button"]} | ||||
|                     child={<image iconName={"system-suspend-symbolic"}></image>} | ||||
|                     onClicked={() => exec("/bin/sh -c 'systemctl suspend'")} | ||||
|                 ></button> | ||||
|             </box> | ||||
|         ); | ||||
|     }; | ||||
|  | ||||
|     popover.set_child(powerMenuBox()); | ||||
|     return popover; | ||||
| }; | ||||
|  | ||||
| const Power = () => { | ||||
|     const pm = PowerMenu(); | ||||
|     return ( | ||||
|         <button | ||||
|             widthRequest={0} | ||||
|             hexpand={false} | ||||
|             vexpand={false} | ||||
|             cssClasses={["power-menu-button"]} | ||||
|             child={ | ||||
|                 <box> | ||||
|                     <image iconName={"system-shutdown-symbolic"}></image> | ||||
|                     {pm} | ||||
|                 </box> | ||||
|             } | ||||
|             onClicked={() => pm.popup()} | ||||
|         /> | ||||
|     ); | ||||
| }; | ||||
|  | ||||
| const UserMenu = (): Gtk.Popover => { | ||||
|     const popover = new Gtk.Popover(); | ||||
|  | ||||
|     const powerMenuBox = () => { | ||||
|         return ( | ||||
|             <box> | ||||
|                 <button | ||||
|                     cssClasses={["power-button"]} | ||||
|                     child={ | ||||
|                         <image iconName={"system-lock-screen-symbolic"}></image> | ||||
|                     } | ||||
|                     onClicked={() => exec("/bin/sh -c 'hyprlock'")} | ||||
|                 ></button> | ||||
|                 <button | ||||
|                     cssClasses={["power-button"]} | ||||
|                     child={<image iconName={"system-log-out-symbolic"}></image>} | ||||
|                     onClicked={() => | ||||
|                         exec("/bin/sh -c 'hyprctl dispatch exit 0'") | ||||
|                     } | ||||
|                 ></button> | ||||
|             </box> | ||||
|         ); | ||||
|     }; | ||||
|  | ||||
|     popover.set_child(powerMenuBox()); | ||||
|     return popover; | ||||
| }; | ||||
|  | ||||
| export default { | ||||
|     Power, | ||||
|     UserMenu | ||||
| }; | ||||
							
								
								
									
										56
									
								
								config/astal/components/QuickActions/quickactions.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								config/astal/components/QuickActions/quickactions.scss
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,56 @@ | ||||
| @use "./modules/Player/Player.scss"; | ||||
| @use "./modules/Audio/Audio.scss"; | ||||
| @use "../../util/colours.scss" as *; | ||||
|  | ||||
| .quick-actions-wrapper { | ||||
|   min-width: 520px; | ||||
| } | ||||
|  | ||||
| box.quick-actions { | ||||
|   padding: 10px; | ||||
| } | ||||
|  | ||||
| popover * { | ||||
|   border-radius: 20px; | ||||
| } | ||||
|  | ||||
| button { | ||||
|   margin: 4px; | ||||
| } | ||||
|  | ||||
| .button-no-margin { | ||||
|   margin-top: 0; | ||||
|   margin-bottom: 0; | ||||
| } | ||||
|  | ||||
| .devices-list { | ||||
|   margin-bottom: 20px; | ||||
| } | ||||
|  | ||||
| button.toggle-button { | ||||
|   min-width: 220px; | ||||
|   border-radius: 50px; | ||||
|  | ||||
|   &.toggle-on { | ||||
|     min-width: 190px; | ||||
|     margin-right: 0; | ||||
|     border-top-right-radius: 0; | ||||
|     border-bottom-right-radius: 0; | ||||
|     background-color: $accent-color; | ||||
|   } | ||||
| } | ||||
|  | ||||
| button.actions-button { | ||||
|   margin-left: 0; | ||||
|   border-radius: 0; | ||||
|   background-color: $accent-color; | ||||
|   border-top-right-radius: 50px; | ||||
|   border-bottom-right-radius: 50px; | ||||
| } | ||||
|  | ||||
| .avatar-icon { | ||||
|   border-radius: 100px; | ||||
|   min-height: 40px; | ||||
|   min-width: 40px; | ||||
|   margin-right: 10px; | ||||
| } | ||||
		Reference in New Issue
	
	Block a user