From 0e7d9aa8d936230e218d47058212218903d80e51 Mon Sep 17 00:00:00 2001 From: alterwain Date: Mon, 10 Feb 2025 02:47:15 +0300 Subject: [PATCH] modified: src/config.rs modified: src/main.rs new file: src/main_screen.rs modified: src/screen.rs new file: src/sync.rs deleted: src/tabs.rs new file: src/wait_screen.rs --- src/config.rs | 8 ++ src/main.rs | 181 ++++++++------------------------------------- src/main_screen.rs | 73 ++++++++++++++++++ src/screen.rs | 58 ++------------- src/sync.rs | 74 ++++++++++++++++++ src/tabs.rs | 97 ------------------------ src/wait_screen.rs | 44 +++++++++++ 7 files changed, 237 insertions(+), 298 deletions(-) create mode 100644 src/main_screen.rs create mode 100644 src/sync.rs delete mode 100644 src/tabs.rs create mode 100644 src/wait_screen.rs diff --git a/src/config.rs b/src/config.rs index 0375481..b5739e2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,5 +1,13 @@ +use std::path::PathBuf; + use serde::Deserialize; +pub fn get_configs_dir() -> PathBuf { + let mut p = dirs::home_dir().unwrap(); + p.push(".lyrica"); + p +} + #[derive(Debug, Deserialize)] pub struct YouTubeConfiguration { pub user_id: u64 diff --git a/src/main.rs b/src/main.rs index 51630f3..ef7c9e8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,103 +1,32 @@ -use std::{error::Error, io, path::{Path, PathBuf}}; +use std::{any::Any, cell::RefCell, collections::HashMap, error::Error, io, ops::Deref, 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::{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 main_screen::MainScreen; +use screen::AppScreen; +use sync::AppEvent; 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}; +use wait_screen::WaitScreen; mod util; mod config; -mod tabs; mod screen; +mod main_screen; +mod wait_screen; +mod sync; -fn get_configs_dir() -> PathBuf { - let mut p = dirs::home_dir().unwrap(); - p.push(".lyrica"); - p -} - -#[derive(Debug, Clone)] +#[derive(Eq, Hash, PartialEq)] enum AppState { IPodWait, - MainScreen(crate::screen::MainScreen) + MainScreen } -enum AppEvent { - SearchIPod, - IPodFound(String), - IPodNotFound, - ParseItunes(String), - ITunesParsed(XDatabase), - SoundcloudGot(CloudPlaylists) -} - -fn initialize_async_service(sender: Sender, receiver: UnboundedReceiver, token: CancellationToken) { - tokio::spawn(async move { - let mut receiver = receiver; - loop { - tokio::select! { - _ = token.cancelled() => { return; } - r = receiver.recv() => { - if let Some(request) = r { - match request { - AppEvent::SearchIPod => { - /*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: PathBuf = Path::new(&path).into(); - // 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; - }, - _ => {} - } - } - } - } - } - }); -} - -#[derive(Debug)] pub struct App { state: AppState, + screens: HashMap>, receiver: Receiver, sender: UnboundedSender, token: CancellationToken, @@ -108,9 +37,16 @@ impl Default for App { let (tx, mut rx) = mpsc::channel(1); let (jx, mut jr) = mpsc::unbounded_channel(); let token = CancellationToken::new(); - initialize_async_service(tx, jr, token.clone()); + + sync::initialize_async_service(tx, jr, token.clone()); + let _ = jx.send(AppEvent::SearchIPod); - Self { state: AppState::IPodWait, receiver: rx, sender: jx, token } + + let mut screens: HashMap> = HashMap::new(); + screens.insert(AppState::IPodWait, Box::new(WaitScreen::default())); + screens.insert(AppState::MainScreen, Box::new(MainScreen::new())); + + Self { receiver: rx, sender: jx, token, state: AppState::IPodWait, screens } } } @@ -123,8 +59,8 @@ impl App { Ok(()) } - fn draw(&self, frame: &mut Frame) { - frame.render_widget(self.state.clone(), frame.area()); + fn draw(&mut self, frame: &mut Frame) { + self.screens.get(&self.state).unwrap().render(frame); } fn handle_events(&mut self) -> io::Result<()> { @@ -137,7 +73,7 @@ impl App { if let Ok(event) = self.receiver.try_recv() { match event { AppEvent::IPodFound(path) => { - self.state = AppState::MainScreen(MainScreen::new()); + self.state = AppState::MainScreen; let _ = self.sender.send(AppEvent::ParseItunes(path)); }, AppEvent::IPodNotFound => { @@ -147,11 +83,12 @@ impl App { }, AppEvent::SoundcloudGot(playlists) => { - if let AppState::MainScreen(screen) = &self.state { - let mut screen = screen.clone(); - screen.soundcloud = Some(playlists); - self.state = AppState::MainScreen(screen); - } + let a = self.screens.get_mut(&AppState::MainScreen).unwrap(); + let screen: &mut MainScreen = match a.as_any().downcast_mut::() { + Some(b) => b, + None => panic!("&a isn't a B!"), + }; + screen.soundcloud = Some(playlists); } _ => {} } @@ -160,11 +97,7 @@ impl App { } fn handle_key_event(&mut self, key_event: KeyEvent) { - if let AppState::MainScreen(screen) = &self.state { - let mut screen = screen.clone(); - screen.handle_key_event(key_event); - self.state = AppState::MainScreen(screen); - } + self.screens.get_mut(&self.state).unwrap().handle_key_event(key_event); match key_event.code { KeyCode::Char('q') => self.exit(), _ => {} @@ -176,62 +109,10 @@ impl App { } } -impl AppState { - 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); - - 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) { - 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![ - "Searching for iPod...".into() - ] - ) - ] - ); - - Paragraph::new(counter_text) - .centered() - .block(block) - .render(area, buf); - } -} - -impl Widget for AppState { - fn render(self, area: Rect, buf: &mut Buffer) { - match self { - AppState::IPodWait => AppState::render_waiting_screen(area, buf), - AppState::MainScreen(mut s) => AppState::render_main_screen(area, buf, &mut s), - _ => {} - } - } -} - #[tokio::main] async fn main() -> Result<(), Box> { enable_raw_mode()?; - let mut stderr = io::stderr(); // This is a special case. Normally using stdout is fine + let mut stderr = io::stdout(); execute!(stderr, EnterAlternateScreen, EnableMouseCapture)?; let backend = CrosstermBackend::new(stderr); let mut terminal = Terminal::new(backend)?; diff --git a/src/main_screen.rs b/src/main_screen.rs new file mode 100644 index 0000000..1247586 --- /dev/null +++ b/src/main_screen.rs @@ -0,0 +1,73 @@ +use crossterm::event::{KeyCode, KeyEvent}; +use ratatui::{buffer::Buffer, layout::{Constraint, Direction, Layout, Rect}, style::{Color, Modifier, Style, Stylize}, text::{Line, Span}, widgets::{Block, Borders, Paragraph, Tabs, Widget}, Frame}; +use soundcloud::sobjects::CloudPlaylists; +use strum::IntoEnumIterator; + +use crate::screen::AppScreen; + +#[derive(Debug, Clone)] +pub struct MainScreen { + selected_tab: u8, + tab_titles: Vec, + pub soundcloud: Option +} + +impl AppScreen for MainScreen { + 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(), + _ => {} + }*/ + } + + fn render(&self, frame: &mut Frame) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(3), // Tabs + Constraint::Min(0), // Main content area + Constraint::Length(1), // Status bar + ]) + .split(frame.area()); + + let tabs = Tabs::new( + self.tab_titles.iter().map(|t| Span::raw(t.clone())).collect::>(), + ) + .block(Block::default().borders(Borders::ALL)) + .highlight_style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)) + .select(self.selected_tab as usize) + .style(Style::default().fg(Color::White)); + + frame.render_widget(tabs, chunks[0]); + + let main_content = Paragraph::new("Main content goes here!") + .block(Block::default().borders(Borders::ALL).title("Main")); + frame.render_widget(main_content, chunks[1]); // Render into second chunk + + // Render Status Bar + let status_bar = Paragraph::new("Press 'q' to quit | Arrow keys to navigate") + .style(Style::default().fg(Color::Cyan)); + frame.render_widget(status_bar, chunks[2]); // Render into third chunk + } + + fn as_any(&mut self) -> &mut dyn std::any::Any { + self + } +} + +impl MainScreen { + pub fn new() -> Self { + MainScreen { soundcloud: None, selected_tab: 0, tab_titles: vec!["YouTube".to_string(), "SoundCloud".to_string(), "Local Playlists".to_string(), "Settings".to_string()] } + } + + 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); + } +} \ No newline at end of file diff --git a/src/screen.rs b/src/screen.rs index 253320d..59db19c 100644 --- a/src/screen.rs +++ b/src/screen.rs @@ -1,56 +1,12 @@ -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 std::any::Any; -use crate::tabs::SelectedTab; +use crossterm::event::KeyEvent; +use ratatui::{buffer::Buffer, layout::Rect, Frame}; -#[derive(Debug, Clone)] -pub struct MainScreen { - pub selected_tab: SelectedTab, - pub soundcloud: Option -} +pub trait AppScreen { + fn handle_key_event(&mut self, key_event: KeyEvent); -impl MainScreen { - pub fn new() -> Self { - MainScreen { selected_tab: SelectedTab::Playlists, soundcloud: None } - } + fn render(&self, frame: &mut Frame); - 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(); - } + fn as_any(&mut self) -> &mut dyn Any; } \ No newline at end of file diff --git a/src/sync.rs b/src/sync.rs new file mode 100644 index 0000000..d49acf6 --- /dev/null +++ b/src/sync.rs @@ -0,0 +1,74 @@ +use std::path::{Path, PathBuf}; + +use itunesdb::xobjects::XDatabase; +use soundcloud::sobjects::CloudPlaylists; +use tokio::{fs::File, io::AsyncReadExt, sync::mpsc::{Sender, UnboundedReceiver}}; +use tokio_util::sync::CancellationToken; + +use crate::config::{get_configs_dir, LyricaConfiguration}; + +pub enum AppEvent { + SearchIPod, + IPodFound(String), + IPodNotFound, + ParseItunes(String), + ITunesParsed(XDatabase), + SoundcloudGot(CloudPlaylists) +} + +pub fn initialize_async_service(sender: Sender, receiver: UnboundedReceiver, token: CancellationToken) { + tokio::spawn(async move { + let mut receiver = receiver; + loop { + tokio::select! { + _ = token.cancelled() => { return; } + r = receiver.recv() => { + if let Some(request) = r { + match request { + AppEvent::SearchIPod => { + /*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: PathBuf = Path::new(&path).into(); + // 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; + }, + _ => {} + } + } + } + } + } + }); +} \ No newline at end of file diff --git a/src/tabs.rs b/src/tabs.rs deleted file mode 100644 index 7703cfd..0000000 --- a/src/tabs.rs +++ /dev/null @@ -1,97 +0,0 @@ -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 diff --git a/src/wait_screen.rs b/src/wait_screen.rs new file mode 100644 index 0000000..2d67d9b --- /dev/null +++ b/src/wait_screen.rs @@ -0,0 +1,44 @@ +use ratatui::{style::Stylize, symbols::border, text::{Line, Text}, widgets::{Block, Paragraph, Widget}, Frame}; + +use crate::screen::AppScreen; + +#[derive(Debug, Clone, Default)] +pub struct WaitScreen {} + +impl AppScreen for WaitScreen { + fn handle_key_event(&mut self, key_event: crossterm::event::KeyEvent) { + todo!() + } + + fn render(&self, frame: &mut Frame) { + 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![ + "Searching for iPod...".into() + ] + ) + ] + ); + + let par = Paragraph::new(counter_text) + .centered() + .block(block); + + frame.render_widget(par, frame.area()); + } + + fn as_any(&mut self) -> &mut dyn std::any::Any { + self + } +} \ No newline at end of file