root/src/controls.rs

// controls.rs
//
// Copyright 2023 nee <nee-git@patchouli.garden>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
//
// SPDX-License-Identifier: GPL-3.0-or-later

use crate::data::{make_time_string, track_triplet_string, Action};
use crate::send;
use adw::prelude::*;
use anyhow::Result;
use glib::clone;
use gtk::subclass::prelude::*;
use gtk::{gio, glib};
use mpd;
use std::cell::RefCell;

#[derive(Debug, Default)]
pub struct InternalState {
    last_played_index: Option<usize>,
    volume: u16,
}

mod imp {
    use super::*;

    use gtk::CompositeTemplate;

    #[derive(Debug, CompositeTemplate, Default)]
    #[template(resource = "/blue/hidamari/pmpdc/ui/controls.ui")]
    pub struct Controls {
        #[template_child]
        pub pause_button: TemplateChild<gtk::Button>,
        #[template_child]
        pub play_button: TemplateChild<gtk::Button>,
        #[template_child]
        pub play_pause_stack: TemplateChild<gtk::Stack>,

        #[template_child]
        pub set_endless_button: TemplateChild<gtk::Button>,
        #[template_child]
        pub unset_endless_button: TemplateChild<gtk::Button>,
        #[template_child]
        pub endless_stack: TemplateChild<gtk::Stack>,

        #[template_child]
        pub set_shuffle_button: TemplateChild<gtk::Button>,
        #[template_child]
        pub unset_shuffle_button: TemplateChild<gtk::Button>,
        #[template_child]
        pub shuffle_stack: TemplateChild<gtk::Stack>,

        #[template_child]
        pub previous_track_button: TemplateChild<gtk::Button>,
        #[template_child]
        pub next_track_button: TemplateChild<gtk::Button>,

        #[template_child]
        pub volume_stack: TemplateChild<gtk::Stack>,
        #[template_child]
        pub volume: TemplateChild<gtk::Scale>,

        #[template_child]
        pub progress: TemplateChild<gtk::Scale>,
        #[template_child]
        pub progress_label: TemplateChild<gtk::Inscription>,
        #[template_child]
        pub duration_label: TemplateChild<gtk::Inscription>,

        #[template_child]
        pub now_playing_label: TemplateChild<gtk::Label>,

        #[template_child]
        pub cover: TemplateChild<gtk::Image>,
        #[template_child]
        pub cover_revealer: TemplateChild<gtk::Revealer>,
        #[template_child]
        pub cover_button: TemplateChild<gtk::Button>,

        pub state: RefCell<Option<InternalState>>,
    }

    #[glib::object_subclass]
    impl ObjectSubclass for Controls {
        const NAME: &'static str = "Controls";
        type Type = super::Controls;
        type ParentType = gtk::Box;

        fn class_init(klass: &mut Self::Class) {
            Self::bind_template(klass);
        }

        // You must call `Widget`'s `init_template()` within `instance_init()`.
        fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
            obj.init_template();
        }
    }
    impl ObjectImpl for Controls {}
    impl WidgetImpl for Controls {}
    impl BoxImpl for Controls {}
}

glib::wrapper! {
    pub struct Controls(ObjectSubclass<imp::Controls>)
        @extends gtk::Widget, gtk::Box,
        @implements gio::ActionMap, gio::ActionGroup;
}

impl Controls {
    pub fn new(
        sender: &glib::Sender<Action>,
        track: Option<(&mpd::Track, usize)>,
        status: Option<&mpd::Status>,
    ) -> Self {
        let widget: Self = glib::Object::new();
        widget.imp().volume.set_range(0.0, 100.0);

        widget
            .imp()
            .progress
            .connect_change_value(clone!(@strong sender => move |_p, _t, v| {
                send!(sender, Action::Seek {position: v});
                gtk::Inhibit(false)
            }));

        let volume_stack = widget.imp().volume_stack.clone();
        widget.imp().volume.connect_value_changed(clone!(@weak volume_stack => move |v| {
            let value = v.value();
            let image_id = ((value / 100.0) * 4.0).round() as u16;
            volume_stack.set_visible_child_full(&format!("{}", image_id), gtk::StackTransitionType::Crossfade);
        }));

        widget
            .imp()
            .volume
            .connect_change_value(clone!(@strong sender => move |_, _, v| {
                send!(sender, Action::SetVolume {volume: v as u16});
                gtk::Inhibit(false)
            }));

        widget
            .imp()
            .set_endless_button
            .connect_clicked(clone!(@strong sender => move |_| {
                send!(sender, Action::SetEndless {endless: true});
            }));
        widget
            .imp()
            .unset_endless_button
            .connect_clicked(clone!(@strong sender => move |_| {
                send!(sender, Action::SetEndless {endless: false});
            }));

        widget
            .imp()
            .previous_track_button
            .connect_clicked(clone!(@strong sender => move |_| {
                send!(sender, Action::PreviousTrack);
            }));

        widget
            .imp()
            .next_track_button
            .connect_clicked(clone!(@strong sender => move |_| {
                send!(sender, Action::NextTrack);
            }));
        widget
            .imp()
            .set_shuffle_button
            .connect_clicked(clone!(@strong sender => move |_| {
                send!(sender, Action::SetRandomize {randomize: true});
            }));
        widget
            .imp()
            .unset_shuffle_button
            .connect_clicked(clone!(@strong sender => move |_| {
                send!(sender, Action::SetRandomize {randomize: false});
            }));

        widget
            .imp()
            .pause_button
            .connect_clicked(clone!(@strong sender => move |_| {
                send!(sender, Action::SetPause {pause:true});
            }));
        widget
            .imp()
            .play_button
            .connect_clicked(clone!(@strong sender => move |_| {
                send!(sender, Action::SetPause {pause:false});
            }));

        *widget.imp().state.borrow_mut() = Some(InternalState {
            last_played_index: track.map(|(_, p)| p),
            volume: status.map(|s| s.volume).unwrap_or(0),
        });

        widget.sync_with_status(status);
        widget.set_track(track);
        let _ = widget.set_cover(None);

        widget
    }

    pub fn sync_with_status(&self, status: Option<&mpd::Status>) {
        if let Some(status) = status {
            self.imp().volume.set_value(status.volume as f64);
            match status.state {
                mpd::Playback::Playing => self
                    .imp()
                    .play_pause_stack
                    .set_visible_child_full("pause", gtk::StackTransitionType::Crossfade),
                mpd::Playback::Paused | mpd::Playback::Stopped => self
                    .imp()
                    .play_pause_stack
                    .set_visible_child_full("play", gtk::StackTransitionType::Crossfade),
            }
            self.imp().shuffle_stack.set_visible_child_full(
                if status.random { "unset" } else { "set" },
                gtk::StackTransitionType::Crossfade,
            );
            self.imp().endless_stack.set_visible_child_full(
                if status.repeat { "unset" } else { "set" },
                gtk::StackTransitionType::Crossfade,
            );
        } else {
            self.imp().volume.set_value(0.0);
        }

        if let Some(s) = status.and_then(|s| s.song.as_ref()) {
            self.imp().progress.set_value(s.elapsed as f64);
            self.imp()
                .progress_label
                .set_text(Some(make_time_string(s.elapsed as u16).as_str()));
        } else {
            self.imp().progress_label.set_text(None);
            self.imp().progress.set_value(0.0);
        }
    }

    pub fn set_track(&self, track: Option<(&mpd::Track, usize)>) {
        if let Some((t, _)) = track {
            self.imp().progress.set_range(0.0, t.time.into());
            self.imp()
                .duration_label
                .set_text(Some(make_time_string(t.time).as_str()));
            self.imp()
                .now_playing_label
                .set_text(&track_triplet_string(t))
        } else {
            self.imp().progress.set_range(0.0, 100.0);
            self.imp().duration_label.set_text(None);
            self.imp().now_playing_label.set_text(&"");
        }
    }

    pub fn set_cover(&self, bytes: Option<gtk::gdk::Texture>) -> Result<()> {
        if let None = bytes {
            self.imp().cover.set_from_pixbuf(None);
            self.imp().cover_revealer.set_reveal_child(false);
            return Ok(());
        }
        let texture = bytes.unwrap();

        self.imp().cover.set_from_paintable(Some(&texture));
        self.imp().cover_revealer.set_reveal_child(true);
        Ok(())
    }

    pub fn add_second(&self) {
        let value = self.imp().progress.value() + 1.0;
        self.imp().progress.set_value(value);
        self.imp()
            .progress_label
            .set_text(Some(make_time_string(value as u16).as_str()));
    }

    pub fn progress_time(&self) -> f64 {
        self.imp().progress.value()
    }

    pub fn last_played_index(&self) -> Option<usize> {
        self.imp()
            .state
            .borrow()
            .as_ref()
            .and_then(|s| s.last_played_index.clone())
    }
}