Upgrade to Agsv2 + Astal (#533)

* migrate to astal

* Reorganize project structure.

* progress

* Migrate Dashboard and Window Title modules.

* Migrate clock and notification bar modules.

* Remove unused code

* Media menu

* Rework network and volume modules

* Finish custom modules.

* Migrate battery bar module.

* Update battery module and organize helpers.

* Migrate workspace module.

* Wrap up bar modules.

* Checkpoint before I inevitbly blow something up.

* Updates

* Fix event propagation logic.

* Type fixes

* More type fixes

* Fix padding for event boxes.

* Migrate volume menu and refactor scroll event handlers.

* network module WIP

* Migrate network service.

* Migrate bluetooth menu

* Updates

* Migrate notifications

* Update scrolling behavior for custom modules.

* Improve popup notifications and add timer functionality.

* Migration notifications menu header/controls.

* Migrate notifications menu and consolidate notifications menu code.

* Migrate power menu.

* Dashboard progress

* Migrate dashboard

* Migrate media menu.

* Reduce media menu nesting.

* Finish updating media menu bindings to navigate active player.

* Migrate battery menu

* Consolidate code

* Migrate calendar menu

* Fix workspace logic to update on client add/change/remove and consolidate code.

* Migrate osd

* Consolidate hyprland service connections.

* Implement startup dropdown menu position allocation.

* Migrate settings menu (WIP)

* Settings dialo menu fixes

* Finish Dashboard menu

* Type updates

* update submoldule for types

* update github ci

* ci

* Submodule update

* Ci updates

* Remove type checking for now.

* ci fix

* Fix a bunch of stuff, losing track... need rest. Brb coffee

* Validate dropdown menu before render.

* Consolidate code and add auto-hide functionality.

* Improve auto-hide behavior.

* Consolidate audio menu code

* Organize bluetooth code

* Improve active player logic

* Properly dismiss a notification on action button resolution.

* Implement CLI command engine and migrate CLI commands.

* Handle variable disposal

* Bar component fixes and add hyprland startup rules.

* Handle potentially null bindings network and bluetooth bindings.

* Handle potentially null wired adapter.

* Fix GPU stats

* Handle poller for GPU

* Fix gpu bar logic.

* Clean up logic for stat bars.

* Handle wifi and wired bar icon bindings.

* Fix battery percentages

* Fix switch behavior

* Wifi staging fixes

* Reduce redundant hyprland service calls.

* Code cleanup

* Document the option code and reduce redundant calls to optimize performance.

* Remove outdated comment.

* Add JSDocs

* Add meson to build hyprpanel

* Consistency updates

* Organize commands

* Fix images not showing up on notifications.

* Remove todo

* Move hyprpanel configuration to the ~/.config/hyprpanel directory and add utility commands.

* Handle SRC directory for the bundled/built hyprpanel.

* Add namespaces to all windows

* Migrate systray

* systray updates

* Update meson to include ts, tsx and scss files.

* Remove log from meson

* Fix file choose path and make it float.

* Added a command to check the dependency status

* Update dep names.

* Get scale directly from env

* Add todo
This commit is contained in:
Jas Singh
2024-12-20 18:10:10 -08:00
committed by GitHub
parent 955eed6c60
commit 2ffd602910
605 changed files with 19543 additions and 15999 deletions

View File

@@ -0,0 +1,40 @@
import { bind } from 'astal';
import { Gtk } from 'astal/gtk3';
import AstalWp from 'gi://AstalWp?version=0.1';
import options from 'src/options';
const { raiseMaximumVolume } = options.menus.volume;
export const Slider = ({ device, type }: SliderProps): JSX.Element => {
return (
<box vertical>
<label
className={`menu-active ${type}`}
halign={Gtk.Align.START}
truncate
expand
wrap
label={bind(device, 'description').as((description) => description ?? `Unknown ${type} Device`)}
/>
<slider
value={bind(device, 'volume')}
className={`menu-active-slider menu-slider ${type}`}
drawValue={false}
hexpand
min={0}
max={type === 'playback' ? bind(raiseMaximumVolume).as((raise) => (raise ? 1.5 : 1)) : 1}
onDragged={({ value, dragging }) => {
if (dragging) {
device.volume = value;
device.mute = false;
}
}}
/>
</box>
);
};
interface SliderProps {
device: AstalWp.Endpoint;
type: 'playback' | 'input';
}

View File

@@ -0,0 +1,39 @@
import { bind, Variable } from 'astal';
import { Gtk } from 'astal/gtk3';
import { isPrimaryClick } from 'src/lib/utils';
import { getIcon } from '../../utils';
import AstalWp from 'gi://AstalWp?version=0.1';
export const SliderIcon = ({ type, device }: SliderIconProps): JSX.Element => {
const iconBinding = Variable.derive([bind(device, 'volume'), bind(device, 'mute')], (volume, isMuted) => {
const iconType = type === 'playback' ? 'spkr' : 'mic';
const effectiveVolume = volume > 0 ? volume : 100;
const mutedState = volume > 0 ? isMuted : true;
return getIcon(effectiveVolume, mutedState)[iconType];
});
return (
<button
className={bind(device, 'mute').as((isMuted) => `menu-active-button ${type} ${isMuted ? 'muted' : ''}`)}
vexpand={false}
valign={Gtk.Align.END}
onClick={(_, event) => {
if (isPrimaryClick(event)) {
device.mute = !device.mute;
}
}}
onDestroy={() => {
iconBinding.drop();
}}
>
<icon className={`menu-active-icon ${type}`} icon={iconBinding()} />
</button>
);
};
interface SliderIconProps {
type: 'playback' | 'input';
device: AstalWp.Endpoint;
}

View File

@@ -0,0 +1,18 @@
import { bind } from 'astal';
import { Gtk } from 'astal/gtk3';
import AstalWp from 'gi://AstalWp?version=0.1';
export const SliderPercentage = ({ type, device }: SliderPercentageProps): JSX.Element => {
return (
<label
className={`menu-active-percentage ${type}`}
valign={Gtk.Align.END}
label={bind(device, 'volume').as((vol) => `${Math.round(vol * 100)}%`)}
/>
);
};
interface SliderPercentageProps {
type: 'playback' | 'input';
device: AstalWp.Endpoint;
}

View File

@@ -0,0 +1,21 @@
import AstalWp from 'gi://AstalWp?version=0.1';
import { SliderIcon } from './SliderIcon';
import { Slider } from './Slider';
import { SliderPercentage } from './SliderPercentage';
export const ActiveDevice = ({ type, device }: ActiveDeviceProps): JSX.Element => {
return (
<box className={`menu-active-container ${type}`} vertical>
<box className={`menu-slider-container ${type}`}>
<SliderIcon type={type} device={device} />
<Slider type={type} device={device} />
<SliderPercentage type={type} device={device} />
</box>
</box>
);
};
interface ActiveDeviceProps {
type: 'playback' | 'input';
device: AstalWp.Endpoint;
}

View File

@@ -0,0 +1,34 @@
import { Gtk } from 'astal/gtk3';
import { ActiveDevice } from './device/index.js';
import { audioService } from 'src/lib/constants/services.js';
import { BindableChild } from 'astal/gtk3/astalify.js';
export const SelectedDevices = (): JSX.Element => {
const Header = (): JSX.Element => (
<box className={'menu-label-container volume selected'} halign={Gtk.Align.FILL}>
<label className={'menu-label audio volume'} halign={Gtk.Align.START} hexpand label={'Volume'} />
</box>
);
const ActiveDeviceContainer = ({ children }: ActiveDeviceContainerProps): JSX.Element => {
return (
<box className={'menu-items-section selected'} vertical>
{children}
</box>
);
};
return (
<box className={'menu-section-container volume'} vertical>
<Header />
<ActiveDeviceContainer>
<ActiveDevice type={'playback'} device={audioService.defaultSpeaker} />
<ActiveDevice type={'input'} device={audioService.defaultMicrophone} />
</ActiveDeviceContainer>
</box>
);
};
interface ActiveDeviceContainerProps {
children?: BindableChild | BindableChild[];
}

View File

@@ -0,0 +1,52 @@
import { Gtk } from 'astal/gtk3';
import AstalWp from 'gi://AstalWp?version=0.1';
import { isPrimaryClick } from 'src/lib/utils';
import { bind } from 'astal';
const DeviceIcon = ({ device, type, icon }: AudioDeviceProps): JSX.Element => {
return (
<label
className={bind(device, 'isDefault').as((isDefault) => {
return `menu-button-icon ${isDefault ? 'active' : ''} ${type} txt-icon`;
})}
label={icon}
/>
);
};
const DeviceName = ({ device, type }: Omit<AudioDeviceProps, 'icon'>): JSX.Element => {
return (
<label
truncate
wrap
className={bind(device, 'description').as((currentDesc) =>
device.description === currentDesc ? `menu-button-name active ${type}` : `menu-button-name ${type}`,
)}
label={device.description}
/>
);
};
export const AudioDevice = ({ device, type, icon }: AudioDeviceProps): JSX.Element => {
return (
<button
className={`menu-button audio ${type} ${device.id}`}
onClick={(_, event) => {
if (isPrimaryClick(event)) {
device.set_is_default(true);
}
}}
>
<box halign={Gtk.Align.START}>
<DeviceIcon device={device} type={type} icon={icon} />
<DeviceName device={device} type={type} />
</box>
</button>
);
};
interface AudioDeviceProps {
device: AstalWp.Endpoint;
type: 'playback' | 'input';
icon: string;
}

View File

@@ -0,0 +1,14 @@
import { Gtk } from 'astal/gtk3';
export const Header = ({ type, label }: HeaderProps): JSX.Element => {
return (
<box className={`menu-label-container ${type}`} halign={Gtk.Align.FILL}>
<label className={`menu-label audio ${type}`} halign={Gtk.Align.START} hexpand label={label} />
</box>
);
};
interface HeaderProps {
type: 'playback' | 'input';
label: string;
}

View File

@@ -0,0 +1,24 @@
import { audioService } from 'src/lib/constants/services.js';
import { bind } from 'astal/binding.js';
import { AudioDevice } from './Device';
import { NotFoundButton } from './NotFoundButton';
export const InputDevices = (): JSX.Element => {
const inputDevices = bind(audioService, 'microphones');
return (
<box className={'menu-items-section input'} vertical>
<box className={'menu-container input'} vertical>
{inputDevices.as((devices) => {
if (!devices || devices.length === 0) {
return <NotFoundButton type={'input'} />;
}
return devices.map((device) => {
return <AudioDevice device={device} type={'input'} icon={''} />;
});
})}
</box>
</box>
);
};

View File

@@ -0,0 +1,13 @@
import { Gtk } from 'astal/gtk3';
export const NotFoundButton = ({ type }: { type: string }): JSX.Element => {
return (
<button className={`menu-unfound-button ${type}`} sensitive={false}>
<box>
<box halign={Gtk.Align.START}>
<label className={`menu-button-name ${type}`} label={`No ${type} devices found...`} />
</box>
</box>
</button>
);
};

View File

@@ -0,0 +1,24 @@
import { audioService } from 'src/lib/constants/services.js';
import { bind } from 'astal/binding.js';
import { AudioDevice } from './Device';
import { NotFoundButton } from './NotFoundButton';
export const PlaybackDevices = (): JSX.Element => {
const playbackDevices = bind(audioService, 'speakers');
return (
<box className={'menu-items-section playback'} vertical>
<box className={'menu-container playback'} vertical>
{playbackDevices.as((devices) => {
if (!devices || devices.length === 0) {
return <NotFoundButton type={'playback'} />;
}
return devices.map((device) => {
return <AudioDevice device={device} type={'playback'} icon={''} />;
});
})}
</box>
</box>
);
};

View File

@@ -0,0 +1,15 @@
import { PlaybackDevices } from './PlaybackDevices.js';
import { InputDevices } from './InputDevices.js';
import { Header } from './Header.js';
export const AvailableDevices = (): JSX.Element => {
return (
<box vertical className={'menu-section-container playback'}>
<Header type={'playback'} label={'Playback Device'} />
<PlaybackDevices />
<Header type={'input'} label={'Input Device'} />
<InputDevices />
</box>
);
};

View File

@@ -0,0 +1,23 @@
import DropdownMenu from '../shared/dropdown/index.js';
import { SelectedDevices } from './active/index.js';
import options from 'src/options.js';
import { bind } from 'astal/binding.js';
import { Gtk } from 'astal/gtk3';
import { AvailableDevices } from './available/index.js';
import { RevealerTransitionMap } from 'src/lib/constants/options.js';
export default (): JSX.Element => {
return (
<DropdownMenu
name="audiomenu"
transition={bind(options.menus.transition).as((transition) => RevealerTransitionMap[transition])}
>
<box className={'menu-items audio'} halign={Gtk.Align.FILL} hexpand>
<box className={'menu-items-container audio'} halign={Gtk.Align.FILL} vertical hexpand>
<SelectedDevices />
<AvailableDevices />
</box>
</box>
</DropdownMenu>
);
};

View File

@@ -0,0 +1,40 @@
const speakerIcons = {
101: 'audio-volume-overamplified-symbolic',
66: 'audio-volume-high-symbolic',
34: 'audio-volume-medium-symbolic',
1: 'audio-volume-low-symbolic',
0: 'audio-volume-muted-symbolic',
} as const;
const inputIcons = {
101: 'microphone-sensitivity-high-symbolic',
66: 'microphone-sensitivity-high-symbolic',
34: 'microphone-sensitivity-medium-symbolic',
1: 'microphone-sensitivity-low-symbolic',
0: 'microphone-disabled-symbolic',
};
type IconVolumes = keyof typeof speakerIcons;
/**
* Retrieves the appropriate speaker and microphone icons based on the audio volume and mute status.
*
* This function determines the correct icons for both the speaker and microphone based on the provided audio volume and mute status.
* It uses predefined thresholds to select the appropriate icons from the `speakerIcons` and `inputIcons` objects.
*
* @param audioVol The current audio volume as a number between 0 and 1.
* @param isMuted A boolean indicating whether the audio is muted.
*
* @returns An object containing the speaker and microphone icons.
*/
const getIcon = (audioVol: number, isMuted: boolean): Record<string, string> => {
const thresholds: IconVolumes[] = [101, 66, 34, 1, 0];
const icon = isMuted ? 0 : thresholds.find((threshold) => threshold <= audioVol * 100) || 0;
return {
spkr: speakerIcons[icon],
mic: inputIcons[icon],
};
};
export { getIcon };