Added the ability to adjust application specific audio levels. (#608)

* Added a playback volume module in audio menu.

* Finish playback source volume adjuster.
This commit is contained in:
Jas Singh
2024-12-23 14:03:01 -08:00
committed by GitHub
parent a3240e6c6d
commit af88c267f4
18 changed files with 368 additions and 46 deletions

View File

@@ -0,0 +1,25 @@
import { BindableChild } from 'astal/gtk3/astalify';
import { audioService } from 'src/lib/constants/services';
import { SliderItem } from '../sliderItem/SliderItem';
import { ActiveDeviceMenu } from '..';
const ActiveDeviceContainer = ({ children }: ActiveDeviceContainerProps): JSX.Element => {
return (
<box className={'menu-items-section selected'} name={ActiveDeviceMenu.Devices} vertical>
{children}
</box>
);
};
export const ActiveDevices = (): JSX.Element => {
return (
<ActiveDeviceContainer>
<SliderItem type={'playback'} device={audioService.defaultSpeaker} />
<SliderItem type={'input'} device={audioService.defaultMicrophone} />
</ActiveDeviceContainer>
);
};
interface ActiveDeviceContainerProps {
children?: BindableChild | BindableChild[];
}

View File

@@ -1,34 +1,56 @@
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';
import { ActiveDevices } from './devices/index.js';
import Variable from 'astal/variable.js';
import { ActivePlaybacks } from './playbacks/index.js';
import { bind } from 'astal/binding.js';
import { isPrimaryClick } from 'src/lib/utils.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>
);
export enum ActiveDeviceMenu {
Devices = 'devices',
Playbacks = 'playbacks',
}
const ActiveDeviceContainer = ({ children }: ActiveDeviceContainerProps): JSX.Element => {
return (
<box className={'menu-items-section selected'} vertical>
{children}
</box>
);
};
const activeMenu: Variable<ActiveDeviceMenu> = Variable(ActiveDeviceMenu.Devices);
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'} />
<button
className={'menu-label slider-toggle'}
onClick={(_, event) => {
if (!isPrimaryClick(event)) {
return;
}
if (activeMenu.get() === ActiveDeviceMenu.Devices) {
activeMenu.set(ActiveDeviceMenu.Playbacks);
} else {
activeMenu.set(ActiveDeviceMenu.Devices);
}
}}
halign={Gtk.Align.END}
hexpand
label={bind(activeMenu).as((menu) => (menu === ActiveDeviceMenu.Devices ? '' : '󰤽'))}
/>
</box>
);
export const VolumeSliders = (): JSX.Element => {
return (
<box className={'menu-section-container volume'} vertical>
<Header />
<ActiveDeviceContainer>
<ActiveDevice type={'playback'} device={audioService.defaultSpeaker} />
<ActiveDevice type={'input'} device={audioService.defaultMicrophone} />
</ActiveDeviceContainer>
<revealer
transitionType={Gtk.RevealerTransitionType.NONE}
revealChild={bind(activeMenu).as((curMenu) => curMenu === ActiveDeviceMenu.Devices)}
>
<ActiveDevices />
</revealer>
<revealer
transitionType={Gtk.RevealerTransitionType.NONE}
revealChild={bind(activeMenu).as((curMenu) => curMenu === ActiveDeviceMenu.Playbacks)}
>
<ActivePlaybacks />
</revealer>
</box>
);
};
interface ActiveDeviceContainerProps {
children?: BindableChild | BindableChild[];
}

View File

@@ -0,0 +1,30 @@
import { bind } from 'astal';
import { audioService } from 'src/lib/constants/services';
import { SliderItem } from '../sliderItem/SliderItem';
import { ActiveDeviceMenu } from '..';
const NoStreams = (): JSX.Element => {
return <label className={'no-playbacks dim'} label={'No active playbacks found.'} expand />;
};
export const ActivePlaybacks = (): JSX.Element => {
return (
<box className={'menu-items-section selected'} name={ActiveDeviceMenu.Playbacks} vertical>
<scrollable className={'menu-scroller active-playbacks-scrollable'}>
<box vertical>
{bind(audioService, 'streams').as((streams) => {
if (!streams || streams.length === 0) {
return <NoStreams />;
}
const currentStreams = streams;
return currentStreams.map((stream) => {
return <SliderItem type={'playback'} device={stream} />;
});
})}
</box>
</scrollable>
</box>
);
};

View File

@@ -1,6 +1,7 @@
import { bind } from 'astal';
import { Gtk } from 'astal/gtk3';
import { Gdk, Gtk } from 'astal/gtk3';
import AstalWp from 'gi://AstalWp?version=0.1';
import { capitalizeFirstLetter } from 'src/lib/utils';
import options from 'src/options';
const { raiseMaximumVolume } = options.menus.volume;
@@ -12,9 +13,11 @@ export const Slider = ({ device, type }: SliderProps): JSX.Element => {
className={`menu-active ${type}`}
halign={Gtk.Align.START}
truncate
expand
hexpand
wrap
label={bind(device, 'description').as((description) => description ?? `Unknown ${type} Device`)}
label={bind(device, 'description').as((description) =>
capitalizeFirstLetter(description ?? `Unknown ${type} Device`),
)}
/>
<slider
value={bind(device, 'volume')}
@@ -29,6 +32,20 @@ export const Slider = ({ device, type }: SliderProps): JSX.Element => {
device.mute = false;
}
}}
setup={(self) => {
self.connect('scroll-event', (_, event: Gdk.Event) => {
const [directionSuccess, direction] = event.get_scroll_direction();
const [deltasSuccess, , yScroll] = event.get_scroll_deltas();
if (directionSuccess) {
const newVolume = device.volume + (direction === Gdk.ScrollDirection.DOWN ? 0.05 : -0.05);
device.set_volume(Math.min(newVolume, 1));
} else if (deltasSuccess) {
const newVolume = device.volume - yScroll / 100;
device.set_volume(Math.min(newVolume, 1));
}
});
}}
/>
</box>
);

View File

@@ -3,7 +3,7 @@ import { SliderIcon } from './SliderIcon';
import { Slider } from './Slider';
import { SliderPercentage } from './SliderPercentage';
export const ActiveDevice = ({ type, device }: ActiveDeviceProps): JSX.Element => {
export const SliderItem = ({ type, device }: SliderItemProps): JSX.Element => {
return (
<box className={`menu-active-container ${type}`} vertical>
<box className={`menu-slider-container ${type}`}>
@@ -15,7 +15,7 @@ export const ActiveDevice = ({ type, device }: ActiveDeviceProps): JSX.Element =
);
};
interface ActiveDeviceProps {
interface SliderItemProps {
type: 'playback' | 'input';
device: AstalWp.Endpoint;
}

View File

@@ -1,5 +1,5 @@
import DropdownMenu from '../shared/dropdown/index.js';
import { SelectedDevices } from './active/index.js';
import { VolumeSliders } from './active/index.js';
import options from 'src/options.js';
import { bind } from 'astal/binding.js';
import { Gtk } from 'astal/gtk3';
@@ -14,7 +14,7 @@ export default (): JSX.Element => {
>
<box className={'menu-items audio'} halign={Gtk.Align.FILL} hexpand>
<box className={'menu-items-container audio'} halign={Gtk.Align.FILL} vertical hexpand>
<SelectedDevices />
<VolumeSliders />
<AvailableDevices />
</box>
</box>