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:
40
src/components/menus/audio/active/device/Slider.tsx
Normal file
40
src/components/menus/audio/active/device/Slider.tsx
Normal 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';
|
||||
}
|
||||
39
src/components/menus/audio/active/device/SliderIcon.tsx
Normal file
39
src/components/menus/audio/active/device/SliderIcon.tsx
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
21
src/components/menus/audio/active/device/index.tsx
Normal file
21
src/components/menus/audio/active/device/index.tsx
Normal 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;
|
||||
}
|
||||
34
src/components/menus/audio/active/index.tsx
Normal file
34
src/components/menus/audio/active/index.tsx
Normal 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[];
|
||||
}
|
||||
52
src/components/menus/audio/available/Device.tsx
Normal file
52
src/components/menus/audio/available/Device.tsx
Normal 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;
|
||||
}
|
||||
14
src/components/menus/audio/available/Header.tsx
Normal file
14
src/components/menus/audio/available/Header.tsx
Normal 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;
|
||||
}
|
||||
24
src/components/menus/audio/available/InputDevices.tsx
Normal file
24
src/components/menus/audio/available/InputDevices.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
13
src/components/menus/audio/available/NotFoundButton.tsx
Normal file
13
src/components/menus/audio/available/NotFoundButton.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
24
src/components/menus/audio/available/PlaybackDevices.tsx
Normal file
24
src/components/menus/audio/available/PlaybackDevices.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
15
src/components/menus/audio/available/index.tsx
Normal file
15
src/components/menus/audio/available/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
23
src/components/menus/audio/index.tsx
Normal file
23
src/components/menus/audio/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
40
src/components/menus/audio/utils.ts
Normal file
40
src/components/menus/audio/utils.ts
Normal 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 };
|
||||
Reference in New Issue
Block a user