From 60e92ee1d72fbc0396cddfc5109ab480152e0490 Mon Sep 17 00:00:00 2001 From: alterwain Date: Sun, 9 Feb 2025 23:24:40 +0300 Subject: [PATCH] modified: Cargo.lock modified: Cargo.toml modified: src/config.rs modified: src/main.rs new file: src/screen.rs new file: src/tabs.rs --- Cargo.lock | 35 ++++++++++++++--- Cargo.toml | 5 ++- src/config.rs | 4 +- src/main.rs | 107 +++++++++++++++++++++++++++++--------------------- src/screen.rs | 56 ++++++++++++++++++++++++++ src/tabs.rs | 97 +++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 249 insertions(+), 55 deletions(-) create mode 100644 src/screen.rs create mode 100644 src/tabs.rs diff --git a/Cargo.lock b/Cargo.lock index bbe0240..00d0e97 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -827,8 +827,8 @@ checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "itunesdb" -version = "0.1.0" -source = "git+https://gitea.awain.net/alterwain/ITunesDB.git#9e20fe785dc9cd1268641dad9730a8fb3ff246c5" +version = "0.1.1" +source = "git+https://gitea.awain.net/alterwain/ITunesDB.git#5a6ca7a9f5eca42959e3498a6eb2700f502b5509" dependencies = [ "bincode", "env_logger", @@ -932,6 +932,7 @@ dependencies = [ "rusb", "serde", "soundcloud", + "strum 0.27.0", "tokio", "tokio-util", "toml", @@ -1204,7 +1205,7 @@ dependencies = [ "itertools", "lru", "paste", - "strum", + "strum 0.26.3", "time", "unicode-segmentation", "unicode-truncate", @@ -1562,8 +1563,8 @@ dependencies = [ [[package]] name = "soundcloud" -version = "0.1.0" -source = "git+https://gitea.awain.net/alterwain/soundcloud_api.git#d4d51c64e9225763f6d40a7f450c673ab6e36ddf" +version = "0.1.1" +source = "git+https://gitea.awain.net/alterwain/soundcloud_api.git#87614c2a10c30f7d3e4b3cb0f8973d62ffec7916" dependencies = [ "hyper-util", "regex", @@ -1603,7 +1604,16 @@ version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" dependencies = [ - "strum_macros", + "strum_macros 0.26.4", +] + +[[package]] +name = "strum" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce1475c515a4f03a8a7129bb5228b81a781a86cb0b3fbbc19e1c556d491a401f" +dependencies = [ + "strum_macros 0.27.0", ] [[package]] @@ -1619,6 +1629,19 @@ dependencies = [ "syn", ] +[[package]] +name = "strum_macros" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9688894b43459159c82bfa5a5fa0435c19cbe3c9b427fa1dd7b1ce0c279b18a7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "subtle" version = "2.6.1" diff --git a/Cargo.toml b/Cargo.toml index f54c06e..76993be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,5 +16,6 @@ color-eyre = "0.6.3" crossterm = "0.28.1" tokio = { version = "1", features = ["full"] } tokio-util = { version = "0.7.12", features = ["codec"] } -soundcloud = { git = "https://gitea.awain.net/alterwain/soundcloud_api.git" } -itunesdb = { git = "https://gitea.awain.net/alterwain/ITunesDB.git" } \ No newline at end of file +strum = { version = "0.27", features = ["derive"] } +soundcloud = { version = "0.1.1", git = "https://gitea.awain.net/alterwain/soundcloud_api.git" } +itunesdb = { version = "0.1.1", git = "https://gitea.awain.net/alterwain/ITunesDB.git" } \ No newline at end of file diff --git a/src/config.rs b/src/config.rs index 7406d12..0375481 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,12 +1,12 @@ use serde::Deserialize; #[derive(Debug, Deserialize)] -struct YouTubeConfiguration { +pub struct YouTubeConfiguration { pub user_id: u64 } #[derive(Debug, Deserialize)] -struct SoundCloudConfiguration { +pub struct SoundCloudConfiguration { pub user_id: u64 } diff --git a/src/main.rs b/src/main.rs index 7cee4e5..51630f3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,15 +1,21 @@ use std::{error::Error, io, path::{Path, PathBuf}}; use color_eyre::Result; +use config::LyricaConfiguration; use crossterm::{event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent, KeyEventKind}, execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}}; -use ratatui::{buffer::Buffer, layout::Rect, prelude::{Backend, CrosstermBackend}, style::Stylize, symbols::border, text::{Line, Text}, widgets::{Block, Paragraph, Widget}, DefaultTerminal, Frame, Terminal}; +use ratatui::{buffer::Buffer, layout::{Layout, Rect}, prelude::{Backend, CrosstermBackend}, style::{Color, Stylize}, symbols::border, text::{Line, Text}, widgets::{Block, Paragraph, Tabs, Widget}, DefaultTerminal, Frame, Terminal}; +use screen::MainScreen; +use soundcloud::sobjects::CloudPlaylists; +use strum::IntoEnumIterator; use tokio::{fs::File, io::AsyncReadExt, sync::mpsc::{self, Receiver, Sender, UnboundedReceiver, UnboundedSender}}; use tokio_util::sync::CancellationToken; - use itunesdb::xobjects::XDatabase; +use ratatui::prelude::Constraint::{Length, Min}; mod util; mod config; +mod tabs; +mod screen; fn get_configs_dir() -> PathBuf { let mut p = dirs::home_dir().unwrap(); @@ -20,10 +26,7 @@ fn get_configs_dir() -> PathBuf { #[derive(Debug, Clone)] enum AppState { IPodWait, - MainScreen(String), - SoundCloud, - Youtube, - Preferences + MainScreen(crate::screen::MainScreen) } enum AppEvent { @@ -31,7 +34,8 @@ enum AppEvent { IPodFound(String), IPodNotFound, ParseItunes(String), - ITunesParsed(XDatabase) + ITunesParsed(XDatabase), + SoundcloudGot(CloudPlaylists) } fn initialize_async_service(sender: Sender, receiver: UnboundedReceiver, token: CancellationToken) { @@ -44,31 +48,43 @@ fn initialize_async_service(sender: Sender, receiver: UnboundedReceive if let Some(request) = r { match request { AppEvent::SearchIPod => { - if let Some(p) = util::search_ipod() { + /*if let Some(p) = util::search_ipod() { let _ = sender.send(AppEvent::IPodFound(p)).await; } else { let _ = sender.send(AppEvent::IPodNotFound).await; - } + }*/ + let _ = sender.send(AppEvent::IPodFound("D:\\Documents\\RustroverProjects\\itunesdb\\ITunesDB\\two_tracks".to_string())).await; }, AppEvent::ParseItunes(path) => { // todo: parse itunes let _ = std::fs::create_dir_all(get_configs_dir()); let mut cd = get_configs_dir(); cd.push("idb"); - /*let mut p = get_configs_dir(); - p.push("config"); - p.set_extension(".toml"); - p.exists()*/ let mut p: PathBuf = Path::new(&path).into(); - p.push("iPod_Control"); - p.push("iTunes"); - p.set_file_name("iTunesDB"); + // p.push("iPod_Control"); + // p.push("iTunes"); + // p.set_file_name("iTunesDB"); let _ = std::fs::copy(p, &cd); let mut file = File::open(cd).await.unwrap(); let mut contents = vec![]; file.read_to_end(&mut contents).await.unwrap(); let xdb = itunesdb::deserializer::parse_bytes(&contents); let _ = sender.send(AppEvent::ITunesParsed(xdb)).await; + + let mut p = get_configs_dir(); + p.push("config"); + p.set_extension(".toml"); + if !p.exists() { return; } + let mut file = File::open(p).await.unwrap(); + let mut content = String::new(); + file.read_to_string(&mut content).await.unwrap(); + let config: LyricaConfiguration = toml::from_str(&content).unwrap(); + + let app_version = soundcloud::get_app().await.unwrap().unwrap(); + let client_id = soundcloud::get_client_id().await.unwrap().unwrap(); + let playlists = soundcloud::get_playlists(config.get_soundcloud().user_id, client_id, app_version).await.unwrap(); + + let _ = sender.send(AppEvent::SoundcloudGot(playlists)).await; }, _ => {} } @@ -121,11 +137,21 @@ impl App { if let Ok(event) = self.receiver.try_recv() { match event { AppEvent::IPodFound(path) => { - self.state = AppState::MainScreen(path.clone()); + self.state = AppState::MainScreen(MainScreen::new()); let _ = self.sender.send(AppEvent::ParseItunes(path)); }, AppEvent::IPodNotFound => { let _ = self.sender.send(AppEvent::SearchIPod); + }, + AppEvent::ITunesParsed(xdb) => { + + }, + AppEvent::SoundcloudGot(playlists) => { + if let AppState::MainScreen(screen) = &self.state { + let mut screen = screen.clone(); + screen.soundcloud = Some(playlists); + self.state = AppState::MainScreen(screen); + } } _ => {} } @@ -134,8 +160,14 @@ impl App { } fn handle_key_event(&mut self, key_event: KeyEvent) { - if key_event.code == KeyCode::Char('q') { - self.exit(); + if let AppState::MainScreen(screen) = &self.state { + let mut screen = screen.clone(); + screen.handle_key_event(key_event); + self.state = AppState::MainScreen(screen); + } + match key_event.code { + KeyCode::Char('q') => self.exit(), + _ => {} } } @@ -145,32 +177,17 @@ impl App { } impl AppState { - fn render_main_screen(area: Rect, buf: &mut Buffer, path: String) { - let title = Line::from(" Lyrica ".bold()); - let instructions = Line::from(vec![ - " Quit ".into(), - " ".red().bold(), - ]); - let block = Block::bordered() - .title(title.centered()) - .title_bottom(instructions.centered()) - .border_set(border::ROUNDED); - - let counter_text = Text::from( - vec![ - Line::from( - vec![ - "Parsing iTunesDB...".into(), - path.blue().bold() - ] - ) - ] - ); + fn render_main_screen(area: Rect, buf: &mut Buffer, screen: &mut MainScreen) { + let vertical = Layout::vertical([Length(1), Min(0), Length(1)]); + let [header_area, inner_area, footer_area] = vertical.areas(area); - Paragraph::new(counter_text) - .centered() - .block(block) - .render(area, buf); + let horizontal = Layout::horizontal([Min(0), Length(7)]); + let [tabs_area, title_area] = horizontal.areas(header_area); + + MainScreen::render_title(title_area, buf); + screen.render_tabs(tabs_area, buf); + screen.selected_tab.render(inner_area, buf); + MainScreen::render_footer(footer_area, buf); } fn render_waiting_screen(area: Rect, buf: &mut Buffer) { @@ -205,7 +222,7 @@ impl Widget for AppState { fn render(self, area: Rect, buf: &mut Buffer) { match self { AppState::IPodWait => AppState::render_waiting_screen(area, buf), - AppState::MainScreen(s) => AppState::render_main_screen(area, buf, s), + AppState::MainScreen(mut s) => AppState::render_main_screen(area, buf, &mut s), _ => {} } } diff --git a/src/screen.rs b/src/screen.rs new file mode 100644 index 0000000..253320d --- /dev/null +++ b/src/screen.rs @@ -0,0 +1,56 @@ +use crossterm::event::{KeyCode, KeyEvent}; +use ratatui::{buffer::Buffer, layout::Rect, style::{Color, Stylize}, text::Line, widgets::{Tabs, Widget}}; +use soundcloud::sobjects::CloudPlaylists; +use strum::IntoEnumIterator; + +use crate::tabs::SelectedTab; + +#[derive(Debug, Clone)] +pub struct MainScreen { + pub selected_tab: SelectedTab, + pub soundcloud: Option +} + +impl MainScreen { + pub fn new() -> Self { + MainScreen { selected_tab: SelectedTab::Playlists, soundcloud: None } + } + + pub fn handle_key_event(&mut self, key_event: KeyEvent) { + match key_event.code { + KeyCode::Char('l') | KeyCode::Right => self.next_tab(), + KeyCode::Char('h') | KeyCode::Left => self.previous_tab(), + _ => {} + } + } + + pub fn render_title(area: Rect, buf: &mut Buffer) { + "Lyrica".bold().render(area, buf); + } + + pub fn render_footer(area: Rect, buf: &mut Buffer) { + Line::raw("◄ ► to change tab | to quit") + .centered() + .render(area, buf); + } + + pub fn render_tabs(&self, area: Rect, buf: &mut Buffer) { + let titles = SelectedTab::iter().map(SelectedTab::title); + let highlight_style = (Color::default(), self.selected_tab.palette().c700); + let selected_tab_index = self.selected_tab.to_usize(); + Tabs::new(titles) + .highlight_style(highlight_style) + .select(selected_tab_index) + .padding("", "") + .divider(" ") + .render(area, buf); + } + + fn next_tab(&mut self) { + self.selected_tab = self.selected_tab.next(); + } + + fn previous_tab(&mut self) { + self.selected_tab = self.selected_tab.previous(); + } +} \ No newline at end of file diff --git a/src/tabs.rs b/src/tabs.rs new file mode 100644 index 0000000..7703cfd --- /dev/null +++ b/src/tabs.rs @@ -0,0 +1,97 @@ +use ratatui::{buffer::Buffer, layout::Rect, style::{palette::tailwind, Stylize}, symbols, text::Line, widgets::{Block, Padding, Paragraph, Widget}}; +use soundcloud::sobjects::CloudPlaylists; +use strum::{AsRefStr, Display, EnumIter, FromRepr, IntoEnumIterator}; + +use crate::screen::MainScreen; + +#[derive(Debug, Default, Clone, Display, FromRepr, EnumIter, AsRefStr)] +pub enum SelectedTab { + #[default] + #[strum(to_string = "Playlists")] + Playlists, + #[strum(to_string = "Albums")] + Albums, + #[strum(to_string = "Soundcloud")] + Soundcloud(Option), + #[strum(to_string = "Youtube")] + Youtube, +} + +impl Widget for SelectedTab { + fn render(self, area: Rect, buf: &mut Buffer) { + let block = self.block(); + match self { + Self::Albums => self.render_albums(area, buf), + Self::Playlists => self.render_playlists(area, buf), + Self::Soundcloud(playlists) => SelectedTab::render_soundcloud(block,area, buf, playlists), + Self::Youtube => self.render_youtube(area, buf), + } + } +} + +impl SelectedTab { + /// Return tab's name as a styled `Line` + pub fn title(self) -> Line<'static> { + format!(" {self} ") + .fg(tailwind::SLATE.c200) + .bg(self.palette().c900) + .into() + } + + fn render_albums(self, area: Rect, buf: &mut Buffer) { + Paragraph::new("Hello, World!") + .block(self.block()) + .render(area, buf); + } + + fn render_playlists(self, area: Rect, buf: &mut Buffer) { + Paragraph::new("Welcome to the Ratatui tabs example!") + .block(self.block()) + .render(area, buf); + } + + fn render_soundcloud(block: Block<'static>, area: Rect, buf: &mut Buffer, playlists: Option) { + Paragraph::new("Your playlists from soundcloud:") + .block(block) + .render(area, buf); + } + + fn render_youtube(self, area: Rect, buf: &mut Buffer) { + Paragraph::new("I know, these are some basic changes. But I think you got the main idea.") + .block(self.block()) + .render(area, buf); + } + + /// A block surrounding the tab's content + fn block(&self) -> Block<'static> { + Block::bordered() + .border_set(symbols::border::THICK) + .padding(Padding::horizontal(1)) + .border_style(self.palette().c700) + } + + pub fn palette(&self) -> tailwind::Palette { + match self { + Self::Albums => tailwind::INDIGO, + Self::Playlists => tailwind::EMERALD, + Self::Soundcloud(_) => tailwind::ORANGE, + Self::Youtube => tailwind::RED, + } + } + + pub fn previous(self) -> Self { + let current_index = self.clone().to_usize(); + let previous_index = current_index.saturating_sub(1); + Self::from_repr(previous_index).unwrap_or(self) + } + + pub fn next(self) -> Self { + let current_index = self.clone().to_usize(); + let next_index = current_index.saturating_add(1); + Self::from_repr(next_index).unwrap_or(self) + } + + pub fn to_usize(self) -> usize { + SelectedTab::iter().enumerate().find(|(_i, el)| el.as_ref() == self.as_ref()).unwrap().0 + } +} \ No newline at end of file