Commit 3d1b82ab authored by Felix Häcker's avatar Felix Häcker

Implement notifications

parent d0c01396
Pipeline #201085 passed with stages
in 25 minutes and 14 seconds
......@@ -7,6 +7,9 @@
<key name="dark-mode" type="b">
<default>false</default>
</key>
<key name="notifications" type="b">
<default>true</default>
</key>
<key name="window-width" type="i">
<default>1000</default>
</key>
......
......@@ -2,7 +2,7 @@
<interface>
<requires lib="gtk+" version="3.0"/>
<object class="HdyPreferencesWindow" id="settings_window">
<property name="default_height">300</property>
<property name="default_height">400</property>
<property name="default_width">640</property>
<property name="width_request">300</property>
<child>
......@@ -16,7 +16,8 @@
<property name="visible">True</property>
<child>
<object class="HdyActionRow">
<property name="title" translatable="yes">Enable dark mode</property>
<property name="title" translatable="yes">Dark Mode</property>
<property name="subtitle" translatable="yes">Whether the application should use a dark theme</property>
<property name="activatable_widget">dark_mode_button</property>
<property name="visible">True</property>
<child>
......@@ -30,6 +31,27 @@
</child>
</object>
</child>
<child>
<object class="HdyPreferencesGroup">
<property name="title" translatable="yes">Features</property>
<property name="visible">True</property>
<child>
<object class="HdyActionRow">
<property name="title" translatable="yes">Notifications</property>
<property name="subtitle" translatable="yes">Show desktop notifications when a new song gets played</property>
<property name="activatable_widget">show_notifications_button</property>
<property name="visible">True</property>
<child>
<object class="GtkSwitch" id="show_notifications_button">
<property name="can_focus">True</property>
<property name="valign">center</property>
<property name="visible">True</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
......
......@@ -66,7 +66,7 @@ pub struct SwApplicationPrivate {
receiver: RefCell<Option<Receiver<Action>>>,
window: RefCell<Option<SwApplicationWindow>>,
pub player: Player,
pub player: Rc<Player>,
pub library: Library,
pub storefront: StoreFront,
......
......@@ -14,6 +14,7 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use gio::prelude::*;
use glib::Sender;
use gtk::prelude::*;
......@@ -25,7 +26,7 @@ use std::rc::Rc;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use crate::api::Station;
use crate::api::{FaviconDownloader, Station};
use crate::app::Action;
use crate::audio::backend::*;
#[cfg(unix)]
......@@ -76,18 +77,19 @@ pub struct Player {
pub header: gtk::HeaderBar,
pub toolbar_controller_widget: gtk::Box,
pub mini_controller_widget: gtk::Box,
controller: Rc<Vec<Box<dyn Controller>>>,
controller: Vec<Box<dyn Controller>>,
gcast_controller: Rc<GCastController>,
backend: Arc<Mutex<Backend>>,
song_title: Rc<RefCell<SongTitle>>,
current_station: RefCell<Option<Station>>,
song_title: RefCell<SongTitle>,
builder: gtk::Builder,
sender: Sender<Action>,
}
impl Player {
pub fn new(sender: Sender<Action>) -> Self {
pub fn new(sender: Sender<Action>) -> Rc<Self> {
let builder = gtk::Builder::from_resource("/de/haeckerfelix/Shortwave/gtk/player.ui");
get_widget!(builder, gtk::Box, player);
get_widget!(builder, gtk::HeaderBar, header);
......@@ -120,7 +122,7 @@ impl Player {
let gcast_controller = GCastController::new(sender.clone());
controller.push(Box::new(gcast_controller.clone()));
let controller: Rc<Vec<Box<dyn Controller>>> = Rc::new(controller);
let controller: Vec<Box<dyn Controller>> = controller;
// Backend
let backend = Backend::new(sender.clone());
......@@ -128,10 +130,13 @@ impl Player {
player_box.reorder_child(&backend.song.listbox.widget, 3);
let backend = Arc::new(Mutex::new(backend));
// Current station (needed for notifications)
let current_station = RefCell::new(None);
// Song title -> [Current Song] - [Previous Song]
let song_title = Rc::new(RefCell::new(SongTitle::new()));
let song_title = RefCell::new(SongTitle::new());
let player = Self {
let player = Rc::new(Self {
widget: player,
header,
toolbar_controller_widget,
......@@ -139,20 +144,22 @@ impl Player {
controller,
gcast_controller,
backend,
current_station,
song_title,
builder,
sender,
};
});
// Set volume
let volume = settings_manager::get_double(Key::PlaybackVolume);
player.set_volume(volume);
player.setup_signals();
player.clone().setup_signals();
player
}
pub fn set_station(&self, station: Station) {
*self.current_station.borrow_mut() = Some(station.clone());
self.set_playback(PlaybackState::Stopped);
// Station is broken, we refuse to play it
......@@ -249,13 +256,10 @@ impl Player {
self.widget.set_hexpand(expand);
}
fn setup_signals(&self) {
fn setup_signals(self: Rc<Self>) {
// Wait for new messages from the Gstreamer backend
let song_title = self.song_title.clone();
let controller = self.controller.clone();
let backend = self.backend.clone();
let receiver = self.backend.clone().lock().unwrap().gstreamer_receiver.take().unwrap();
receiver.attach(None, move |message| Self::process_gst_message(message, song_title.clone(), controller.clone(), backend.clone()));
receiver.attach(None, clone!(@strong self as this => move |message| this.clone().process_gst_message(message)));
// Disconnect from gcast device
get_widget!(self.builder, gtk::Button, disconnect_button);
......@@ -264,10 +268,10 @@ impl Player {
}));
}
fn process_gst_message(message: GstreamerMessage, song_title: Rc<RefCell<SongTitle>>, controller: Rc<Vec<Box<dyn Controller>>>, backend: Arc<Mutex<Backend>>) -> glib::Continue {
fn process_gst_message(&self, message: GstreamerMessage) -> glib::Continue {
match message {
GstreamerMessage::SongTitleChanged(title) => {
let backend = &mut backend.lock().unwrap();
let backend = &mut self.backend.lock().unwrap();
debug!("Song title has changed to: \"{}\"", title);
// If we're already recording something, we need to stop it first.
......@@ -278,7 +282,7 @@ impl Player {
backend.gstreamer.stop_recording(false);
let duration = Duration::from_secs(duration.try_into().unwrap());
let song = song_title.borrow().create_song(duration).expect("Unable to create new song");
let song = self.song_title.borrow().create_song(duration).expect("Unable to create new song");
backend.song.add_song(song);
} else {
......@@ -288,33 +292,60 @@ impl Player {
}
// Set new song title
song_title.borrow_mut().set_current_title(title.clone());
for con in &*controller {
self.song_title.borrow_mut().set_current_title(title.clone());
for con in &*self.controller {
con.set_song_title(&title);
}
// Start recording new song
// We don't start recording the "first" detected song, since it is going to be incomplete
if !song_title.borrow().is_first_song() {
backend.gstreamer.start_recording(song_title.borrow().get_path().expect("Unable to get song path"));
if !self.song_title.borrow().is_first_song() {
backend.gstreamer.start_recording(self.song_title.borrow().get_path().expect("Unable to get song path"));
} else {
debug!("Song will not be recorded because it may be incomplete (first song for this station).")
}
// Show desktop notification
if settings_manager::get_boolean(Key::Notifications) {
self.show_song_notification();
}
}
GstreamerMessage::PlaybackStateChanged(state) => {
for con in &*controller {
for con in &*self.controller {
con.set_playback_state(&state);
}
// Discard recorded data when a failure occurs,
// since the song has not been recorded completely.
if backend.lock().unwrap().gstreamer.is_recording() && matches!(state, PlaybackState::Failure(_)) {
backend.lock().unwrap().gstreamer.stop_recording(true);
if self.backend.lock().unwrap().gstreamer.is_recording() && matches!(state, PlaybackState::Failure(_)) {
self.backend.lock().unwrap().gstreamer.stop_recording(true);
}
}
}
glib::Continue(true)
}
fn show_song_notification(&self) {
let current_station = self.current_station.borrow().clone().unwrap();
let notification = gio::Notification::new(&self.song_title.borrow().get_current_title().unwrap());
notification.set_body(Some(&current_station.name));
//notification.add_button("Record and save this song", "app.record-and-save-song");
/* Icons won't work at the moment, no idea
current_station.favicon.map(|favicon| {
FaviconDownloader::get_file(&favicon).map(|file| {
let path = file.get_path().unwrap().to_str().unwrap().to_owned();
dbg!(&path);
let file = gio::File::new_for_path(&path);
let icon = gio::FileIcon::new(&file);
notification.set_icon(&icon);
})
});
*/
let app = self.builder.get_application().unwrap();
app.send_notification(None, &notification);
}
}
pub struct SongTitle {
......@@ -339,6 +370,10 @@ impl SongTitle {
}
}
pub fn get_current_title(&self) -> Option<String> {
self.current_title.clone()
}
/// Returns song for current title
pub fn create_song(&self, duration: Duration) -> Option<Song> {
if let Some(title) = &self.current_title {
......
......@@ -22,6 +22,7 @@ pub enum Key {
/* User Interface */
DarkMode,
Notifications,
WindowWidth,
WindowHeight,
ViewSorting,
......
......@@ -45,5 +45,8 @@ impl SettingsWindow {
fn setup_signals(&self) {
get_widget!(self.builder, gtk::Switch, dark_mode_button);
settings_manager::bind_property(Key::DarkMode, &dark_mode_button, "active");
get_widget!(self.builder, gtk::Switch, show_notifications_button);
settings_manager::bind_property(Key::Notifications, &show_notifications_button, "active");
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment