Move language selection to bottom bar, implement volume

This commit is contained in:
Jeremy Soller 2024-12-05 13:41:58 -07:00
parent 52a7697806
commit b26005d780
No known key found for this signature in database
GPG key ID: D02FD439211AF56F
3 changed files with 192 additions and 41 deletions

View file

@ -1,3 +1,6 @@
audio = Audio
subtitles = Subtitles
# Context Pages
## Settings

View file

@ -10,7 +10,7 @@ use cosmic::{
keyboard::{Event as KeyEvent, Key, Modifiers},
mouse::Event as MouseEvent,
subscription::Subscription,
window, Alignment, Color, Length, Limits,
window, Alignment, Background, Border, Color, Length, Limits,
},
theme,
widget::{self, menu::action::MenuAction, Slider},
@ -150,16 +150,26 @@ pub struct Flags {
url_opt: Option<url::Url>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DropdownKind {
Audio,
Subtitle,
}
/// Messages that are used specifically by our [`App`].
#[derive(Clone, Debug)]
pub enum Message {
None,
Config(Config),
DropdownToggle(DropdownKind),
FileClose,
FileLoad(url::Url),
FileOpen,
Fullscreen,
Key(Modifiers, Key),
AudioCode(usize),
AudioToggle,
AudioVolume(f64),
TextCode(usize),
PlayPause,
Seek(f64),
@ -180,6 +190,7 @@ pub struct App {
flags: Flags,
controls: bool,
controls_time: Instant,
dropdown_opt: Option<DropdownKind>,
fullscreen: bool,
key_binds: HashMap<KeyBind, Action>,
video_opt: Option<Video>,
@ -373,6 +384,7 @@ impl Application for App {
flags,
controls: true,
controls_time: Instant::now(),
dropdown_opt: None,
fullscreen: false,
key_binds: key_binds(),
video_opt: None,
@ -400,6 +412,7 @@ impl Application for App {
/// Handle application events here.
fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
match message {
Message::None => {}
Message::Config(config) => {
if config != self.flags.config {
log::info!("update config");
@ -407,6 +420,11 @@ impl Application for App {
return self.update_config();
}
}
Message::DropdownToggle(menu_kind) => {
if self.dropdown_opt.take() != Some(menu_kind) {
self.dropdown_opt = Some(menu_kind);
}
}
Message::FileClose => {
self.close();
}
@ -435,6 +453,9 @@ impl Application for App {
);
}
Message::Fullscreen => {
//TODO: cleanest way to close dropdowns
self.dropdown_opt = None;
self.fullscreen = !self.fullscreen;
self.core.window.show_headerbar = !self.fullscreen;
return window::change_mode(
@ -462,6 +483,18 @@ impl Application for App {
}
}
}
Message::AudioToggle => {
if let Some(video) = &mut self.video_opt {
video.set_muted(!video.muted());
self.update_controls(true);
}
}
Message::AudioVolume(volume) => {
if let Some(video) = &mut self.video_opt {
video.set_volume(volume);
self.update_controls(true);
}
}
Message::TextCode(code) => {
if let Ok(code) = i32::try_from(code) {
if let Some(video) = &self.video_opt {
@ -472,12 +505,18 @@ impl Application for App {
}
}
Message::PlayPause => {
//TODO: cleanest way to close dropdowns
self.dropdown_opt = None;
if let Some(video) = &mut self.video_opt {
video.set_paused(!video.paused());
self.update_controls(true);
}
}
Message::Seek(secs) => {
//TODO: cleanest way to close dropdowns
self.dropdown_opt = None;
if let Some(video) = &mut self.video_opt {
self.dragging = true;
self.position = secs;
@ -496,6 +535,9 @@ impl Application for App {
}
}
Message::SeekRelease => {
//TODO: cleanest way to close dropdowns
self.dropdown_opt = None;
if let Some(video) = &mut self.video_opt {
self.dragging = false;
let duration = Duration::try_from_secs_f64(self.position).unwrap_or_default();
@ -547,7 +589,7 @@ impl Application for App {
if let Some(video) = &self.video_opt {
if !self.dragging {
self.position = video.position().as_secs_f64();
self.update_controls(false);
self.update_controls(self.dropdown_opt.is_some());
}
}
}
@ -568,29 +610,7 @@ impl Application for App {
}
fn header_start(&self) -> Vec<Element<Self::Message>> {
let mut row = widget::row::with_capacity(5)
.align_items(Alignment::Center)
.spacing(8);
row = row.push(menu::menu_bar(&self.flags.config, &self.key_binds));
if !self.audio_codes.is_empty() {
//TODO: allow mute/unmute/change volume
row = row.push(widget::icon::from_name("audio-volume-high-symbolic").size(16));
row = row.push(widget::dropdown(
&self.audio_codes,
usize::try_from(self.current_audio).ok(),
Message::AudioCode,
));
}
if !self.text_codes.is_empty() {
//TODO: allow toggling subtitles
row = row.push(widget::icon::from_name("media-view-subtitles-symbolic").size(16));
row = row.push(widget::dropdown(
&self.text_codes,
usize::try_from(self.current_text).ok(),
Message::TextCode,
));
}
vec![row.into()]
vec![menu::menu_bar(&self.flags.config, &self.key_binds)]
}
/// Creates a view after each update.
@ -598,6 +618,7 @@ impl Application for App {
let cosmic_theme::Spacing {
space_xxs,
space_xs,
space_m,
..
} = theme::active().cosmic().spacing;
@ -618,6 +639,9 @@ impl Application for App {
.into();
};
let muted = video.muted();
let volume = video.volume();
let video_player = VideoPlayer::new(video)
.mouse_hidden(!self.controls)
.on_end_of_stream(Message::EndOfStream)
@ -626,13 +650,108 @@ impl Application for App {
.width(Length::Fill)
.height(Length::Fill);
let mouse_area = widget::mouse_area(video_player).on_double_press(Message::Fullscreen);
let mouse_area = widget::mouse_area(video_player)
.on_press(Message::PlayPause)
.on_double_press(Message::Fullscreen);
let mut popover = widget::popover(mouse_area).position(widget::popover::Position::Bottom);
let mut popup_items = Vec::<Element<_>>::with_capacity(2);
if let Some(dropdown) = self.dropdown_opt {
let mut items = Vec::<Element<_>>::new();
match dropdown {
DropdownKind::Audio => {
items.push(
widget::row::with_children(vec![
widget::button::icon(
widget::icon::from_name({
if muted {
"audio-volume-muted-symbolic"
} else {
if volume >= (2.0 / 3.0) {
"audio-volume-high-symbolic"
} else if volume >= (1.0 / 3.0) {
"audio-volume-medium-symbolic"
} else {
"audio-volume-low-symbolic"
}
}
})
.size(16),
)
.on_press(Message::AudioToggle)
.into(),
//TODO: disable slider when muted?
Slider::new(0.0..=1.0, volume, Message::AudioVolume)
.step(0.01)
.into(),
])
.align_items(Alignment::Center)
.into(),
);
}
DropdownKind::Subtitle => {
if !self.audio_codes.is_empty() {
items.push(widget::text::heading(fl!("audio")).into());
items.push(
widget::dropdown(
&self.audio_codes,
usize::try_from(self.current_audio).ok(),
Message::AudioCode,
)
.into(),
);
}
if !self.text_codes.is_empty() {
//TODO: allow toggling subtitles
items.push(widget::text::heading(fl!("subtitles")).into());
items.push(
widget::dropdown(
&self.text_codes,
usize::try_from(self.current_text).ok(),
Message::TextCode,
)
.into(),
);
}
}
}
let mut column = widget::column::with_capacity(items.len());
for item in items {
column = column.push(widget::container(item).padding([space_xxs, space_m]));
}
popup_items.push(
widget::row::with_children(vec![
widget::horizontal_space(Length::Fill).into(),
widget::container(column)
.padding(1)
//TODO: move style to libcosmic
.style(theme::Container::custom(|theme| {
let cosmic = theme.cosmic();
let component = &cosmic.background.component;
widget::container::Appearance {
icon_color: Some(component.on.into()),
text_color: Some(component.on.into()),
background: Some(Background::Color(component.base.into())),
border: Border {
radius: 8.0.into(),
width: 1.0,
color: component.divider.into(),
},
..Default::default()
}
}))
.width(Length::Fixed(240.0))
.into(),
])
.into(),
);
}
if self.controls {
popover = popover.popup(
popup_items.push(
widget::container(
widget::row::with_capacity(5)
widget::row::with_capacity(7)
.align_items(Alignment::Center)
.spacing(space_xxs)
.push(
@ -657,17 +776,47 @@ impl Application for App {
widget::text(format_time(self.duration - self.position))
.font(font::mono()),
)
.push(
widget::button::icon(
widget::icon::from_name("media-view-subtitles-symbolic").size(16),
)
.on_press(Message::DropdownToggle(DropdownKind::Subtitle)),
)
.push(
widget::button::icon(
widget::icon::from_name("view-fullscreen-symbolic").size(16),
)
.on_press(Message::Fullscreen),
)
.push(
//TODO: scroll up/down on icon to change volume
widget::button::icon(
widget::icon::from_name({
if muted {
"audio-volume-muted-symbolic"
} else {
if volume >= (2.0 / 3.0) {
"audio-volume-high-symbolic"
} else if volume >= (1.0 / 3.0) {
"audio-volume-medium-symbolic"
} else {
"audio-volume-low-symbolic"
}
}
})
.size(16),
)
.on_press(Message::DropdownToggle(DropdownKind::Audio)),
),
)
.padding([space_xxs, space_xs])
.style(theme::Container::WindowBackground),
.style(theme::Container::WindowBackground)
.into(),
);
}
if !popup_items.is_empty() {
popover = popover.popup(widget::column::with_children(popup_items));
}
widget::container(popover)
.width(Length::Fill)

View file

@ -1,9 +1,8 @@
// SPDX-License-Identifier: GPL-3.0-only
use cosmic::widget::menu::key_bind::KeyBind;
use cosmic::widget::menu::{items as menu_items, root as menu_root, Item as MenuItem};
use cosmic::{
widget::menu::{ItemHeight, ItemWidth, MenuBar, Tree as MenuTree},
theme,
widget::menu::{self, key_bind::KeyBind, ItemHeight, ItemWidth, MenuBar},
Element,
};
use std::collections::HashMap;
@ -13,21 +12,21 @@ use crate::{fl, Action, Config, Message};
pub fn menu_bar<'a>(config: &Config, key_binds: &HashMap<KeyBind, Action>) -> Element<'a, Message> {
let mut recent_items = Vec::new();
MenuBar::new(vec![MenuTree::with_children(
menu_root(fl!("file")),
menu_items(
MenuBar::new(vec![menu::Tree::with_children(
menu::root(fl!("file")),
menu::items(
key_binds,
vec![
MenuItem::Button(fl!("open-media"), Action::FileOpen),
MenuItem::Folder(fl!("open-recent-media"), recent_items),
MenuItem::Button(fl!("close-file"), Action::FileClose),
MenuItem::Divider,
MenuItem::Button(fl!("quit"), Action::WindowClose),
menu::Item::Button(fl!("open-media"), Action::FileOpen),
menu::Item::Folder(fl!("open-recent-media"), recent_items),
menu::Item::Button(fl!("close-file"), Action::FileClose),
menu::Item::Divider,
menu::Item::Button(fl!("quit"), Action::WindowClose),
],
),
)])
.item_height(ItemHeight::Dynamic(40))
.item_width(ItemWidth::Uniform(240))
.spacing(4.0)
.spacing(theme::active().cosmic().spacing.space_xxxs.into())
.into()
}