wip: update libcosmic (#93)
* wip: update libcosmic * fix: damge issue resolved by updating iced * fix: high cpu usage by time applet and app-list * refactor subscriptions to produce fewer events * refactor network applet to use less cpu * fix: text size * refactor: i18n for audio applet * refactor: power applet i18n setup * fix (battery): always send profile update * fix (battery): set toggler width to layout correctly * fix (app-list): backoff for restarts of toplevel subscription * fix (network): alignment * feat: ask for comfirmation before applying power applet actions * wip: integrate cosmic-config * update zbus * feat: update to use latest libcosmic * update iced * udpate deps * update deps * refactor: move applet helpers to this repo, outside of libcosmic. this should help alleviate some dependency hell * chore update deps * update deps * cleanup
This commit is contained in:
parent
8b46cc209f
commit
9ebd9b511a
48 changed files with 2841 additions and 1681 deletions
47
cosmic-applet-audio/src/localize.rs
Normal file
47
cosmic-applet-audio/src/localize.rs
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
// SPDX-License-Identifier: MPL-2.0-only
|
||||
|
||||
use i18n_embed::{
|
||||
fluent::{fluent_language_loader, FluentLanguageLoader},
|
||||
DefaultLocalizer, LanguageLoader, Localizer,
|
||||
};
|
||||
use once_cell::sync::Lazy;
|
||||
use rust_embed::RustEmbed;
|
||||
|
||||
#[derive(RustEmbed)]
|
||||
#[folder = "i18n/"]
|
||||
struct Localizations;
|
||||
|
||||
pub static LANGUAGE_LOADER: Lazy<FluentLanguageLoader> = Lazy::new(|| {
|
||||
let loader: FluentLanguageLoader = fluent_language_loader!();
|
||||
|
||||
loader
|
||||
.load_fallback_language(&Localizations)
|
||||
.expect("Error while loading fallback language");
|
||||
|
||||
loader
|
||||
});
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! fl {
|
||||
($message_id:literal) => {{
|
||||
i18n_embed_fl::fl!($crate::localize::LANGUAGE_LOADER, $message_id)
|
||||
}};
|
||||
|
||||
($message_id:literal, $($args:expr),*) => {{
|
||||
i18n_embed_fl::fl!($crate::localize::LANGUAGE_LOADER, $message_id, $($args), *)
|
||||
}};
|
||||
}
|
||||
|
||||
// Get the `Localizer` to be used for localizing this library.
|
||||
pub fn localizer() -> Box<dyn Localizer> {
|
||||
Box::from(DefaultLocalizer::new(&*LANGUAGE_LOADER, &Localizations))
|
||||
}
|
||||
|
||||
pub fn localize() {
|
||||
let localizer = localizer();
|
||||
let requested_languages = i18n_embed::DesktopLanguageRequester::requested_languages();
|
||||
|
||||
if let Err(error) = localizer.select(&requested_languages) {
|
||||
eprintln!("Error while loading language for App List {}", error);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,13 @@
|
|||
mod localize;
|
||||
|
||||
use cosmic::iced::widget;
|
||||
use cosmic::iced_native::alignment::Horizontal;
|
||||
use cosmic::iced_native::layout::Limits;
|
||||
use cosmic::iced::Limits;
|
||||
use cosmic::iced_runtime::core::alignment::Horizontal;
|
||||
use cosmic::theme::Svg;
|
||||
|
||||
use cosmic::applet::{CosmicAppletHelper, APPLET_BUTTON_THEME};
|
||||
use cosmic::widget::{button, divider, icon};
|
||||
use cosmic::Renderer;
|
||||
use cosmic_applet::{applet_button_theme, CosmicAppletHelper};
|
||||
|
||||
use cosmic::iced::{
|
||||
self,
|
||||
|
|
@ -20,12 +22,16 @@ use iced::widget::container;
|
|||
use iced::Color;
|
||||
|
||||
mod pulse;
|
||||
use crate::localize::localize;
|
||||
use crate::pulse::DeviceInfo;
|
||||
use libpulse_binding::volume::VolumeLinear;
|
||||
|
||||
pub fn main() -> cosmic::iced::Result {
|
||||
pretty_env_logger::init();
|
||||
|
||||
// Prepare i18n
|
||||
localize();
|
||||
|
||||
let helper = CosmicAppletHelper::default();
|
||||
Audio::run(helper.window_settings())
|
||||
}
|
||||
|
|
@ -43,7 +49,7 @@ struct Audio {
|
|||
theme: Theme,
|
||||
popup: Option<window::Id>,
|
||||
show_media_controls_in_top_panel: bool,
|
||||
id_ctr: u32,
|
||||
id_ctr: u128,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
|
|
@ -93,7 +99,7 @@ impl Application for Audio {
|
|||
}
|
||||
|
||||
fn theme(&self) -> Theme {
|
||||
self.theme
|
||||
self.theme.clone()
|
||||
}
|
||||
|
||||
fn close_requested(&self, _id: window::Id) -> Self::Message {
|
||||
|
|
@ -101,10 +107,10 @@ impl Application for Audio {
|
|||
}
|
||||
|
||||
fn style(&self) -> <Self::Theme as application::StyleSheet>::Style {
|
||||
<Self::Theme as application::StyleSheet>::Style::Custom(|theme| Appearance {
|
||||
<Self::Theme as application::StyleSheet>::Style::Custom(Box::new(|theme| Appearance {
|
||||
background_color: Color::from_rgba(0.0, 0.0, 0.0, 0.0),
|
||||
text_color: theme.cosmic().on_bg_color().into(),
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Message) -> Command<Message> {
|
||||
|
|
@ -117,21 +123,21 @@ impl Application for Audio {
|
|||
conn.send(pulse::Message::UpdateConnection);
|
||||
}
|
||||
self.id_ctr += 1;
|
||||
let new_id = window::Id::new(self.id_ctr);
|
||||
let new_id = window::Id(self.id_ctr);
|
||||
self.popup.replace(new_id);
|
||||
|
||||
let mut popup_settings = self.applet_helper.get_popup_settings(
|
||||
window::Id::new(0),
|
||||
window::Id(0),
|
||||
new_id,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
popup_settings.positioner.size_limits = Limits::NONE
|
||||
.min_height(1)
|
||||
.min_width(1)
|
||||
.max_width(400)
|
||||
.max_height(1080);
|
||||
.min_height(1.0)
|
||||
.min_width(1.0)
|
||||
.max_width(400.0)
|
||||
.max_height(1080.0);
|
||||
|
||||
if let Some(conn) = self.pulse_state.connection() {
|
||||
conn.send(pulse::Message::GetDefaultSink);
|
||||
|
|
@ -268,7 +274,7 @@ impl Application for Audio {
|
|||
}
|
||||
|
||||
fn view(&self, id: window::Id) -> Element<Message> {
|
||||
if id == window::Id::new(0) {
|
||||
if id == window::Id(0) {
|
||||
self.applet_helper
|
||||
.icon_button(&self.icon_name)
|
||||
.on_press(Message::TogglePopup)
|
||||
|
|
@ -291,20 +297,18 @@ impl Application for Audio {
|
|||
.0 * 100.0;
|
||||
|
||||
let audio_content = if audio_disabled {
|
||||
column![text("PulseAudio Disconnected")
|
||||
column![text(fl!("disconnected"))
|
||||
.width(Length::Fill)
|
||||
.horizontal_alignment(Horizontal::Center)
|
||||
.size(24),]
|
||||
} else {
|
||||
column![
|
||||
row![
|
||||
icon("audio-volume-high-symbolic", 32)
|
||||
.width(Length::Units(24))
|
||||
.height(Length::Units(24))
|
||||
.style(Svg::Symbolic),
|
||||
icon("audio-volume-high-symbolic", 24).style(Svg::Symbolic),
|
||||
slider(0.0..=100.0, out_f64, Message::SetOutputVolume)
|
||||
.width(Length::FillPortion(5)),
|
||||
text(format!("{}%", out_f64.round()))
|
||||
.size(16)
|
||||
.width(Length::FillPortion(1))
|
||||
.horizontal_alignment(Horizontal::Right)
|
||||
]
|
||||
|
|
@ -312,13 +316,11 @@ impl Application for Audio {
|
|||
.align_items(Alignment::Center)
|
||||
.padding([8, 24]),
|
||||
row![
|
||||
icon("audio-input-microphone-symbolic", 32)
|
||||
.width(Length::Units(24))
|
||||
.height(Length::Units(24))
|
||||
.style(Svg::Symbolic),
|
||||
icon("audio-input-microphone-symbolic", 24).style(Svg::Symbolic),
|
||||
slider(0.0..=100.0, in_f64, Message::SetInputVolume)
|
||||
.width(Length::FillPortion(5)),
|
||||
text(format!("{}%", in_f64.round()))
|
||||
.size(16)
|
||||
.width(Length::FillPortion(1))
|
||||
.horizontal_alignment(Horizontal::Right)
|
||||
]
|
||||
|
|
@ -330,7 +332,7 @@ impl Application for Audio {
|
|||
.width(Length::Fill),
|
||||
revealer(
|
||||
self.is_open == IsOpen::Output,
|
||||
"Output",
|
||||
fl!("output"),
|
||||
match &self.current_output {
|
||||
Some(output) => pretty_name(output.description.clone()),
|
||||
None => String::from("No device selected"),
|
||||
|
|
@ -348,10 +350,10 @@ impl Application for Audio {
|
|||
),
|
||||
revealer(
|
||||
self.is_open == IsOpen::Input,
|
||||
"Input",
|
||||
fl!("input"),
|
||||
match &self.current_input {
|
||||
Some(input) => pretty_name(input.description.clone()),
|
||||
None => String::from("No device selected"),
|
||||
None => fl!("no-device"),
|
||||
},
|
||||
self.inputs
|
||||
.clone()
|
||||
|
|
@ -372,17 +374,20 @@ impl Application for Audio {
|
|||
container(divider::horizontal::light())
|
||||
.padding([12, 24])
|
||||
.width(Length::Fill),
|
||||
container(toggler(
|
||||
Some("Show Media Controls on Top Panel".into()),
|
||||
self.show_media_controls_in_top_panel,
|
||||
Message::ToggleMediaControlsInTopPanel,
|
||||
))
|
||||
container(
|
||||
toggler(
|
||||
Some(fl!("show-media-controls")),
|
||||
self.show_media_controls_in_top_panel,
|
||||
Message::ToggleMediaControlsInTopPanel,
|
||||
)
|
||||
.text_size(14)
|
||||
)
|
||||
.padding([0, 24]),
|
||||
container(divider::horizontal::light())
|
||||
.padding([12, 24])
|
||||
.width(Length::Fill),
|
||||
button(APPLET_BUTTON_THEME)
|
||||
.text("Sound Settings...")
|
||||
button(applet_button_theme())
|
||||
.custom(vec![text(fl!("sound-settings")).size(14).into()])
|
||||
.padding([8, 24])
|
||||
.width(Length::Fill)
|
||||
]
|
||||
|
|
@ -398,19 +403,19 @@ impl Application for Audio {
|
|||
|
||||
fn revealer(
|
||||
open: bool,
|
||||
title: &str,
|
||||
title: String,
|
||||
selected: String,
|
||||
options: Vec<(String, String)>,
|
||||
toggle: Message,
|
||||
mut change: impl FnMut(String) -> Message + 'static,
|
||||
) -> widget::Column<Message, Renderer> {
|
||||
) -> widget::Column<'static, Message, Renderer> {
|
||||
if open {
|
||||
options.iter().fold(
|
||||
column![revealer_head(open, title, selected, toggle)].width(Length::Fill),
|
||||
|col, (id, name)| {
|
||||
col.push(
|
||||
button(APPLET_BUTTON_THEME)
|
||||
.custom(vec![text(name).into()])
|
||||
button(applet_button_theme())
|
||||
.custom(vec![text(name).size(14).into()])
|
||||
.on_press(change(id.clone()))
|
||||
.width(Length::Fill)
|
||||
.padding([8, 48]),
|
||||
|
|
@ -424,14 +429,14 @@ fn revealer(
|
|||
|
||||
fn revealer_head(
|
||||
_open: bool,
|
||||
title: &str,
|
||||
title: String,
|
||||
selected: String,
|
||||
toggle: Message,
|
||||
) -> widget::Button<Message, Renderer> {
|
||||
button(APPLET_BUTTON_THEME)
|
||||
) -> widget::Button<'static, Message, Renderer> {
|
||||
button(applet_button_theme())
|
||||
.custom(vec![
|
||||
text(title).width(Length::Fill).into(),
|
||||
text(selected).into(),
|
||||
text(title).width(Length::Fill).size(14).into(),
|
||||
text(selected).size(10).into(),
|
||||
])
|
||||
.padding([8, 24])
|
||||
.width(Length::Fill)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
use cosmic::iced_native::subscription::{self, Subscription};
|
||||
use std::cell::RefCell;
|
||||
use std::{rc::Rc, thread};
|
||||
|
||||
extern crate libpulse_binding as pulse;
|
||||
use cosmic::iced::{subscription, Subscription};
|
||||
//use futures::channel::mpsc;
|
||||
use libpulse_binding::{
|
||||
callbacks::ListResult,
|
||||
|
|
@ -15,71 +15,80 @@ use libpulse_binding::{
|
|||
proplist::Proplist,
|
||||
volume::ChannelVolumes,
|
||||
};
|
||||
|
||||
pub fn connect() -> Subscription<Event> {
|
||||
struct Connect;
|
||||
|
||||
subscription::unfold(
|
||||
std::any::TypeId::of::<Connect>(),
|
||||
State::Init,
|
||||
|state| async move {
|
||||
match state {
|
||||
State::Init => {
|
||||
let PulseHandle {
|
||||
to_pulse,
|
||||
from_pulse,
|
||||
} = PulseHandle::new();
|
||||
(
|
||||
Some(Event::Init(Connection(to_pulse))),
|
||||
State::Connecting(from_pulse),
|
||||
)
|
||||
}
|
||||
// Waiting for Connection to succeed
|
||||
// The GUI doesn't have to monitor this state, as it is never sent to the GUI
|
||||
State::Connecting(mut from_pulse) => match from_pulse.recv().await {
|
||||
Some(Message::Connected) => {
|
||||
(Some(Event::Connected), State::Connected(from_pulse))
|
||||
}
|
||||
Some(Message::Disconnected) => {
|
||||
(Some(Event::Disconnected), State::Connecting(from_pulse))
|
||||
}
|
||||
Some(m) => {
|
||||
panic!("Unexpected message: {:?}", m);
|
||||
}
|
||||
None => {
|
||||
panic!("Pulse Sender dropped, something has gone wrong!");
|
||||
}
|
||||
},
|
||||
State::Connected(mut from_pulse) => {
|
||||
// This is where we match messages from the pulse server to pass to the gui
|
||||
match from_pulse.recv().await {
|
||||
Some(Message::SetSinks(sinks)) => (
|
||||
Some(Event::MessageReceived(Message::SetSinks(sinks))),
|
||||
State::Connected(from_pulse),
|
||||
),
|
||||
Some(Message::SetSources(sources)) => (
|
||||
Some(Event::MessageReceived(Message::SetSources(sources))),
|
||||
State::Connected(from_pulse),
|
||||
),
|
||||
Some(Message::SetDefaultSink(sink)) => (
|
||||
Some(Event::MessageReceived(Message::SetDefaultSink(sink))),
|
||||
State::Connected(from_pulse),
|
||||
),
|
||||
Some(Message::SetDefaultSource(source)) => (
|
||||
Some(Event::MessageReceived(Message::SetDefaultSource(source))),
|
||||
State::Connected(from_pulse),
|
||||
),
|
||||
Some(Message::Disconnected) => {
|
||||
(Some(Event::Disconnected), State::Connecting(from_pulse))
|
||||
}
|
||||
None => (Some(Event::Disconnected), State::Connecting(from_pulse)),
|
||||
_ => (None, State::Connected(from_pulse)),
|
||||
}
|
||||
|mut state| async move {
|
||||
loop {
|
||||
let (update, new_state) = connection(state).await;
|
||||
state = new_state;
|
||||
if let Some(update) = update {
|
||||
return (update, state);
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
async fn connection(state: State) -> (Option<Event>, State) {
|
||||
match state {
|
||||
State::Init => {
|
||||
let PulseHandle {
|
||||
to_pulse,
|
||||
from_pulse,
|
||||
} = PulseHandle::new();
|
||||
(
|
||||
Some(Event::Init(Connection(to_pulse))),
|
||||
State::Connecting(from_pulse),
|
||||
)
|
||||
}
|
||||
// Waiting for Connection to succeed
|
||||
// The GUI doesn't have to monitor this state, as it is never sent to the GUI
|
||||
State::Connecting(mut from_pulse) => match from_pulse.recv().await {
|
||||
Some(Message::Connected) => (Some(Event::Connected), State::Connected(from_pulse)),
|
||||
Some(Message::Disconnected) => {
|
||||
(Some(Event::Disconnected), State::Connecting(from_pulse))
|
||||
}
|
||||
Some(m) => {
|
||||
panic!("Unexpected message: {:?}", m);
|
||||
}
|
||||
None => {
|
||||
panic!("Pulse Sender dropped, something has gone wrong!");
|
||||
}
|
||||
},
|
||||
State::Connected(mut from_pulse) => {
|
||||
// This is where we match messages from the pulse server to pass to the gui
|
||||
match from_pulse.recv().await {
|
||||
Some(Message::SetSinks(sinks)) => (
|
||||
Some(Event::MessageReceived(Message::SetSinks(sinks))),
|
||||
State::Connected(from_pulse),
|
||||
),
|
||||
Some(Message::SetSources(sources)) => (
|
||||
Some(Event::MessageReceived(Message::SetSources(sources))),
|
||||
State::Connected(from_pulse),
|
||||
),
|
||||
Some(Message::SetDefaultSink(sink)) => (
|
||||
Some(Event::MessageReceived(Message::SetDefaultSink(sink))),
|
||||
State::Connected(from_pulse),
|
||||
),
|
||||
Some(Message::SetDefaultSource(source)) => (
|
||||
Some(Event::MessageReceived(Message::SetDefaultSource(source))),
|
||||
State::Connected(from_pulse),
|
||||
),
|
||||
Some(Message::Disconnected) => {
|
||||
(Some(Event::Disconnected), State::Connecting(from_pulse))
|
||||
}
|
||||
None => (Some(Event::Disconnected), State::Connecting(from_pulse)),
|
||||
_ => (None, State::Connected(from_pulse)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// #[derive(Debug)]
|
||||
enum State {
|
||||
Init,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue