legacygui 0.1.0

A simple GUI audio player with strange features.
Documentation
#![warn(
    clippy::all,
    clippy::pedantic,
    clippy::nursery,
    clippy::cargo_common_metadata
)]
// Anachronism
#![allow(clippy::non_ascii_literal)]
// More or less manual checked and documentation agrees with me that
// it's usually not needed.
#![allow(
    clippy::cast_possible_truncation,
    clippy::cast_sign_loss,
    clippy::cast_precision_loss,
    clippy::cast_lossless
)]
// Explicitly decided against; I think `let _ = …` is better than
// `mem::drop(…)`. TODO: align my opinion and community's one with
// each other.
#![allow(let_underscore_drop)]

use std::{
    io::stdin,
    process::exit,
    sync::{
        atomic::{AtomicBool, Ordering},
        Arc,
    },
    thread,
    time::Duration,
};

use crossbeam_channel::{bounded, Sender};
use diskit::StdDiskit;
use eframe::egui;
use egui::Slider;
use legacylisten::{
    api::ApiRequester, audio::RodioAudioHandler, commands::Command, conffile::Conffile,
    l10n::messages::Message, runner, songs::Repeat,
};

use legacygui::writer::MessageList;

struct MyApp
{
    all_messages: MessageList,
    is_quit: Arc<AtomicBool>,
    api: ApiRequester,
    old_likelihood: f64,
    quit_requester: Sender<()>,
    strings: Vec<String>,
    info: Vec<String>,
}

impl MyApp
{
    fn new() -> Self
    {
        let is_quit = Arc::new(AtomicBool::new(false));
        let is_quit_clone = is_quit.clone();
        let (all_messages, writer_handler) = MessageList::new();
        let writer_handler_clone = writer_handler;
        let (api, requester) = ApiRequester::new();
        let (quit_requester, quit_request) = bounded(1);

        thread::spawn(move || {
            let writer_handler = writer_handler_clone;
            if let Err(err) = runner::run::<_, RodioAudioHandler, _, _>(
                Conffile::new,
                writer_handler,
                || stdin().lock(),
                requester,
                quit_request,
                StdDiskit::default(),
            )
            {
                eprintln!("Error with legacylisten: {err:?}");
            }

            is_quit_clone.store(true, Ordering::SeqCst);
        });

        let strings = vec![
            api.localize(Message::Description(Command::Quit)).unwrap(),
            api.localize(Message::Description(Command::SkipToPrevious))
                .unwrap(),
            api.localize(Message::Description(Command::Pause)).unwrap(),
            api.localize(Message::Description(Command::Resume)).unwrap(),
            api.localize(Message::Description(Command::Skip)).unwrap(),
            api.localize(Message::Description(Command::IncreaseLikelihood))
                .unwrap(),
            api.localize(Message::Description(Command::DecreaseLikelihood))
                .unwrap(),
            api.localize(Message::Volume).unwrap(),
            api.localize(Message::Description(Command::DisableRepeat))
                .unwrap(),
            api.localize(Message::Description(Command::RepeatOnce))
                .unwrap(),
            api.localize(Message::Description(Command::RepeatForever))
                .unwrap(),
            api.localize(Message::Description(Command::QuitAfterSong))
                .unwrap(),
            api.localize(Message::Description(Command::PauseAfterSong))
                .unwrap(),
            api.localize(Message::Description(Command::ShowInfo))
                .unwrap(),
            api.localize(Message::HideInfo).unwrap(),
        ];

        Self {
            all_messages,
            is_quit,
            api,
            old_likelihood: 10.0,
            quit_requester,
            strings,
            info: vec![],
        }
    }
}

impl eframe::App for MyApp
{
    fn on_close_event(&mut self) -> bool
    {
        let _ = self.quit_requester.send(());
        if self.api.quit().is_none()
        {
            exit(0);
        }

        thread::sleep(Duration::from_millis(1000));

        true
    }

    // I think this function can't be refactored well and it's a
    // pedantic lint.
    #[allow(clippy::too_many_lines)]
    fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame)
    {
        egui::CentralPanel::default().show(ctx, |ui| {
            if self.is_quit.load(Ordering::SeqCst)
            {
                frame.close();
            }

            let is_playing = self.api.is_playing().unwrap();

            if ui.button(&self.strings[0]).clicked()
            {
                frame.close();
            }
            ui.horizontal(|ui| {
                if ui.button(&self.strings[1]).clicked()
                {
                    self.api.previous(2).unwrap();
                    self.api.skip(1).unwrap();
                }
                if is_playing
                {
                    if ui.button(&self.strings[2]).clicked()
                    {
                        self.api.pause().unwrap();
                    }
                }
                else if ui.button(&self.strings[3]).clicked()
                {
                    self.api.resume().unwrap();
                }
                if ui.button(&self.strings[4]).clicked()
                {
                    self.api.skip(1).unwrap();
                }
            });

            ui.horizontal(|ui| {
                let mut likelihood = self.api.get_likelihood().unwrap();
                let orig_likelihood = likelihood;

                if self.old_likelihood > likelihood as f64
                {
                    self.old_likelihood = likelihood as _;
                }
                else
                {
                    self.old_likelihood =
                        15.0f64.mul_add(self.old_likelihood, likelihood as f64) / 16.0;
                }

                if ui.button(&self.strings[5]).clicked()
                {
                    likelihood += 1;
                }
                if ui.button(&self.strings[6]).clicked()
                {
                    likelihood -= 1;
                }
                ui.add(Slider::new(
                    &mut likelihood,
                    0..=((2.0 * self.old_likelihood) as u32 + 10),
                ));
                if likelihood != orig_likelihood
                {
                    self.api.set_likelihood(likelihood).unwrap();
                }
            });
            ui.horizontal(|ui| {
                let mut loud = self.api.get_volume().unwrap() * 100.0;
                let old_loud = loud;
                ui.add(Slider::new(&mut loud, 0.0..=100.0).text(&self.strings[7]));

                if (loud - old_loud).abs() > 0.01
                {
                    println!("{loud}");
                    self.api.set_volume(loud / 100.0).unwrap();
                }
            });

            let (cur, len) = self.api.get_duration().unwrap();
            // I think this way is better and the lint is in nursey.
            #[allow(clippy::option_if_let_else)]
            let mes = if let Some(len) = len
            {
                Message::DurationKnown((cur * 100.0).round() / 100.0, (len * 100.0).round() / 100.0)
            }
            else
            {
                Message::DurationUnknown((cur * 100.0).round() / 100.0)
            };

            ui.label(self.api.localize(mes).unwrap());

            ui.horizontal(|ui| {
                if ui.button(&self.strings[8]).clicked()
                {
                    self.api.repeat(Repeat::Not).unwrap();
                }
                if ui.button(&self.strings[9]).clicked()
                {
                    self.api.repeat(Repeat::Once).unwrap();
                }
                if ui.button(&self.strings[10]).clicked()
                {
                    self.api.repeat(Repeat::Always).unwrap();
                }
            });

            ui.horizontal(|ui| {
                if ui.button(&self.strings[11]).clicked()
                {
                    self.api.quit_after_song().unwrap();
                }
                if ui.button(&self.strings[12]).clicked()
                {
                    self.api.pause_after_song().unwrap();
                }
            });

            if self.info.is_empty()
            {
                if ui.button(&self.strings[13]).clicked()
                {
                    self.info = self
                        .api
                        .get_info()
                        .unwrap()
                        .into_iter()
                        .map(|mes| self.api.localize(mes).unwrap())
                        .collect();
                }
            }
            else
            {
                ui.separator();
                if ui.button(&self.strings[14]).clicked()
                {
                    self.info.clear();
                }
                else
                {
                    ui.vertical(|ui| {
                        self.info.iter().for_each(|mes| {
                            ui.label(mes);
                        });
                    });
                }
            }

            ui.separator();

            ui.vertical(|ui| {
                for mes in self.all_messages.get_list().into_iter().rev()
                {
                    ui.label(mes);
                }
            });
        });

        ctx.request_repaint_after(Duration::from_millis(50));
    }
}

fn main()
{
    let options = eframe::NativeOptions::default();
    eframe::run_native("legacygui", options, Box::new(|_cc| Box::new(MyApp::new())));
}