[AGS] Bar: Done (WiFi still missing, will be added at some later point)
This commit is contained in:
@@ -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
|
||||
};
|
Reference in New Issue
Block a user