modified: Cargo.lock

modified:   Cargo.toml
	modified:   src/config.rs
	modified:   src/main.rs
	new file:   src/screen.rs
	new file:   src/tabs.rs
This commit is contained in:
Michael Wain 2025-02-09 23:24:40 +03:00
parent 41aa3222a8
commit 60e92ee1d7
6 changed files with 249 additions and 55 deletions

35
Cargo.lock generated
View File

@ -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"

View File

@ -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" }
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" }

View File

@ -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
}

View File

@ -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<AppEvent>, receiver: UnboundedReceiver<AppEvent>, token: CancellationToken) {
@ -44,31 +48,43 @@ fn initialize_async_service(sender: Sender<AppEvent>, 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(),
"<Q> ".red().bold(),
]);
let block = Block::bordered()
.title(title.centered())
.title_bottom(instructions.centered())
.border_set(border::ROUNDED);
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 counter_text = Text::from(
vec![
Line::from(
vec![
"Parsing iTunesDB...".into(),
path.blue().bold()
]
)
]
);
let horizontal = Layout::horizontal([Min(0), Length(7)]);
let [tabs_area, title_area] = horizontal.areas(header_area);
Paragraph::new(counter_text)
.centered()
.block(block)
.render(area, buf);
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),
_ => {}
}
}

56
src/screen.rs Normal file
View File

@ -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<CloudPlaylists>
}
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 | <Q> 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();
}
}

97
src/tabs.rs Normal file
View File

@ -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<CloudPlaylists>),
#[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<CloudPlaylists>) {
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
}
}