376 lines
13 KiB
Rust
376 lines
13 KiB
Rust
use chrono::{DateTime, TimeZone, Utc};
|
|
use crossterm::event::{KeyCode, KeyEvent};
|
|
use ratatui::{
|
|
layout::{Constraint, Direction, Layout, Rect},
|
|
style::{Color, Modifier, Style, Stylize},
|
|
text::{Line, Span},
|
|
widgets::{Block, Borders, Paragraph, Row, Table, Tabs},
|
|
Frame,
|
|
};
|
|
use soundcloud::sobjects::{CloudPlaylist, CloudPlaylists};
|
|
use tokio::sync::mpsc::UnboundedSender;
|
|
|
|
use crate::sync::DBPlaylist;
|
|
use crate::{screen::AppScreen, sync::AppEvent, theme::Theme, AppState};
|
|
|
|
pub struct MainScreen {
|
|
mode: bool,
|
|
selected_tab: i8,
|
|
selected_playlist: i32,
|
|
selected_song: i32,
|
|
max_pls: i32,
|
|
max_songs: i32,
|
|
tab_titles: Vec<String>,
|
|
soundcloud: Option<Vec<CloudPlaylist>>,
|
|
playlists: Option<Vec<DBPlaylist>>,
|
|
sender: UnboundedSender<AppEvent>,
|
|
}
|
|
|
|
impl AppScreen for MainScreen {
|
|
fn handle_key_event(&mut self, key_event: KeyEvent) {
|
|
match key_event.code {
|
|
KeyCode::Right => self.next_tab(),
|
|
KeyCode::Left => self.previous_tab(),
|
|
KeyCode::Up => self.previous_row(),
|
|
KeyCode::Down => self.next_row(),
|
|
KeyCode::F(6) => self.download_row(),
|
|
KeyCode::Tab => self.switch_mode(),
|
|
KeyCode::F(2) => {
|
|
self.sender
|
|
.send(AppEvent::SwitchScreen(AppState::FileSystem));
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
fn render(&self, frame: &mut Frame, theme: &Theme) {
|
|
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::<Vec<Span>>(),
|
|
)
|
|
.block(Block::default().borders(Borders::ALL))
|
|
.highlight_style(
|
|
Style::default()
|
|
.fg(Color::LightBlue)
|
|
.add_modifier(Modifier::BOLD),
|
|
)
|
|
.select(self.selected_tab as usize)
|
|
.style(Style::default().fg(Color::Black));
|
|
|
|
frame.render_widget(tabs, chunks[0]);
|
|
|
|
self.render_tab(frame, chunks[1]);
|
|
|
|
// Render Status Bar
|
|
let status_bar = Paragraph::new(Line::from(vec![
|
|
"◄ ► to change tab".bold(),
|
|
" | ".dark_gray(),
|
|
"<F5> SAVE FS".bold(),
|
|
" | ".dark_gray(),
|
|
"<F6> DL".bold(),
|
|
" | ".dark_gray(),
|
|
"<F8> DEL".bold(),
|
|
" | ".dark_gray(),
|
|
"<Q> QUIT".bold(),
|
|
]))
|
|
.centered();
|
|
frame.render_widget(status_bar, chunks[2]);
|
|
}
|
|
|
|
fn as_any(&mut self) -> &mut dyn std::any::Any {
|
|
self
|
|
}
|
|
}
|
|
|
|
impl MainScreen {
|
|
pub fn new(sender: UnboundedSender<AppEvent>) -> Self {
|
|
MainScreen {
|
|
mode: false,
|
|
selected_playlist: 0,
|
|
selected_song: 0,
|
|
max_pls: 0,
|
|
max_songs: 0,
|
|
soundcloud: None,
|
|
playlists: None,
|
|
selected_tab: 0,
|
|
tab_titles: vec![
|
|
"YouTube".to_string(),
|
|
"SoundCloud".to_string(),
|
|
"iPod".to_string(),
|
|
"Settings".to_string(),
|
|
],
|
|
sender,
|
|
}
|
|
}
|
|
|
|
fn update_max_rows(&mut self) {
|
|
self.selected_song = 0;
|
|
self.selected_playlist = 0;
|
|
self.max_songs = 0;
|
|
self.max_pls = match self.selected_tab {
|
|
1 => self.soundcloud.as_deref().unwrap_or(&[]).len(),
|
|
2 => self.playlists.as_deref().unwrap_or(&[]).len(),
|
|
_ => 0,
|
|
}
|
|
.try_into()
|
|
.unwrap();
|
|
self.update_max_songs();
|
|
}
|
|
|
|
fn update_max_songs(&mut self) {
|
|
if self.max_pls > 0 {
|
|
self.max_songs = match self.selected_tab {
|
|
1 => self
|
|
.soundcloud
|
|
.as_deref()
|
|
.unwrap()
|
|
.get(self.selected_playlist as usize)
|
|
.unwrap()
|
|
.tracks
|
|
.len(),
|
|
_ => 0,
|
|
}
|
|
.try_into()
|
|
.unwrap();
|
|
|
|
self.selected_song = 0;
|
|
}
|
|
}
|
|
|
|
fn switch_mode(&mut self) {
|
|
self.mode = !self.mode;
|
|
}
|
|
|
|
fn next_tab(&mut self) {
|
|
self.selected_tab = std::cmp::min(
|
|
self.selected_tab + 1,
|
|
(self.tab_titles.len() - 1).try_into().unwrap(),
|
|
);
|
|
self.update_max_rows();
|
|
}
|
|
|
|
fn previous_tab(&mut self) {
|
|
self.selected_tab = std::cmp::max(0, self.selected_tab - 1);
|
|
self.update_max_rows();
|
|
}
|
|
|
|
fn previous_row(&mut self) {
|
|
match self.mode {
|
|
true => self.selected_song = std::cmp::max(0, self.selected_song - 1),
|
|
false => {
|
|
self.selected_playlist = std::cmp::max(0, self.selected_playlist - 1);
|
|
self.update_max_songs();
|
|
}
|
|
}
|
|
}
|
|
|
|
fn next_row(&mut self) {
|
|
match self.mode {
|
|
true => self.selected_song = std::cmp::min(self.selected_song + 1, self.max_songs - 1),
|
|
false => {
|
|
self.selected_playlist =
|
|
std::cmp::min(self.selected_playlist + 1, self.max_pls - 1);
|
|
self.update_max_songs();
|
|
}
|
|
}
|
|
}
|
|
|
|
fn download_row(&mut self) {
|
|
if self.selected_tab == 1 {
|
|
// SC
|
|
let playlist = self
|
|
.soundcloud
|
|
.as_ref()
|
|
.unwrap()
|
|
.get(self.selected_playlist as usize)
|
|
.unwrap()
|
|
.clone();
|
|
let _ = self.sender.send(AppEvent::DownloadPlaylist(playlist));
|
|
}
|
|
}
|
|
|
|
pub fn set_soundcloud_playlists(&mut self, pl: CloudPlaylists) {
|
|
self.soundcloud = Some(pl.collection);
|
|
if self.selected_tab == 1 {
|
|
self.update_max_rows();
|
|
}
|
|
}
|
|
|
|
pub fn set_itunes(&mut self, pl: Vec<DBPlaylist>) {
|
|
self.playlists = Some(pl);
|
|
if self.selected_tab == 2 {
|
|
self.update_max_rows();
|
|
}
|
|
}
|
|
|
|
fn render_tab(&self, frame: &mut Frame, area: Rect) {
|
|
let rows = match self.selected_tab {
|
|
1 => {
|
|
// SC
|
|
let mut v = Vec::new();
|
|
v.push(
|
|
Row::new(vec!["Id", "Title", "Songs Count", "Date", "IS"])
|
|
.style(Style::default().fg(Color::Gray)),
|
|
);
|
|
if let Some(s) = &self.soundcloud {
|
|
for (i, playlist) in s.iter().enumerate() {
|
|
let date: DateTime<Utc> = playlist.created_at.parse().unwrap();
|
|
let mut row = Row::new(vec![
|
|
playlist.id.to_string(),
|
|
playlist.title.clone(),
|
|
[playlist.track_count.to_string(), " songs".to_string()].concat(),
|
|
format!("{}", date.format("%Y-%m-%d %H:%M")),
|
|
"NO".to_string(),
|
|
]);
|
|
if self.selected_playlist == i as i32 {
|
|
row = row.style(Style::default().bg(Color::LightBlue).fg(Color::White));
|
|
}
|
|
v.push(row);
|
|
}
|
|
}
|
|
v
|
|
}
|
|
2 => {
|
|
// local
|
|
let mut v = Vec::new();
|
|
v.push(
|
|
Row::new(vec!["Id", "Title", "Songs Count", "Date", "IS"])
|
|
.style(Style::default().fg(Color::Gray)),
|
|
);
|
|
if let Some(s) = &self.playlists {
|
|
for (i, playlist) in s.iter().enumerate() {
|
|
let date = Utc.timestamp_millis_opt(playlist.timestamp as i64).unwrap();
|
|
let mut row = Row::new(vec![
|
|
playlist.id.to_string(),
|
|
"".to_string(),
|
|
playlist.tracks.len().to_string(),
|
|
format!("{}", date.format("%Y-%m-%d %H:%M")),
|
|
"YES".to_string(),
|
|
]);
|
|
if self.selected_playlist == i as i32 {
|
|
row = row.style(Style::default().bg(Color::LightBlue).fg(Color::White));
|
|
}
|
|
v.push(row);
|
|
}
|
|
}
|
|
v
|
|
}
|
|
_ => Vec::new(),
|
|
};
|
|
|
|
let chunks = Layout::default()
|
|
.direction(Direction::Vertical)
|
|
.constraints([
|
|
Constraint::Percentage(30), // Playlists
|
|
Constraint::Min(0), // Tracks
|
|
])
|
|
.split(area);
|
|
|
|
// Create the table
|
|
let table = Table::new(
|
|
rows,
|
|
&[
|
|
Constraint::Length(3), // ID column
|
|
Constraint::Percentage(50), // Playlist name column
|
|
Constraint::Percentage(20), // Song count column
|
|
Constraint::Percentage(30),
|
|
Constraint::Length(2),
|
|
],
|
|
)
|
|
.block(Block::default().borders(Borders::ALL).title(" Playlists "))
|
|
.style(Style::default().fg(Color::Black));
|
|
|
|
frame.render_widget(table, chunks[0]);
|
|
|
|
let mut rows = match self.selected_tab {
|
|
1 => {
|
|
// sc
|
|
let mut v = Vec::new();
|
|
v.push(
|
|
Row::new(vec!["Id", "Title", "Artist", "Duration", "Genre"])
|
|
.style(Style::default().fg(Color::Gray)),
|
|
);
|
|
if let Some(pls) = &self.soundcloud {
|
|
let s = &pls.get(self.selected_playlist as usize).unwrap().tracks;
|
|
for (i, track) in s.iter().enumerate() {
|
|
let mut row = Row::new(vec![
|
|
track.id.to_string(),
|
|
track.title.as_deref().unwrap().to_string(),
|
|
track
|
|
.user
|
|
.clone()
|
|
.unwrap()
|
|
.username
|
|
.unwrap_or(track.user.as_ref().unwrap().permalink.clone()),
|
|
track.duration.unwrap_or(0).to_string(),
|
|
track.genre.as_ref().unwrap_or(&String::new()).to_string(),
|
|
]);
|
|
if self.selected_song == i as i32 {
|
|
row = row.style(Style::default().bg(Color::LightBlue).fg(Color::White));
|
|
}
|
|
v.push(row);
|
|
}
|
|
}
|
|
v
|
|
}
|
|
2 => {
|
|
// local
|
|
let mut v = Vec::new();
|
|
v.push(
|
|
Row::new(vec!["Id", "Title", "Artist", "Bitrate", "Genre"])
|
|
.style(Style::default().fg(Color::Gray)),
|
|
);
|
|
if let Some(pls) = &self.playlists {
|
|
let s = &pls.get(self.selected_playlist as usize).unwrap().tracks;
|
|
for (i, track) in s.iter().enumerate() {
|
|
let mut row = Row::new(vec![
|
|
track.data.unique_id.to_string(),
|
|
track.get_title(),
|
|
track.get_location(),
|
|
track.data.bitrate.to_string(),
|
|
track.get_genre(),
|
|
]);
|
|
if self.selected_song == i as i32 {
|
|
row = row.style(Style::default().bg(Color::LightBlue).fg(Color::White));
|
|
}
|
|
v.push(row);
|
|
}
|
|
}
|
|
v
|
|
}
|
|
_ => Vec::new(),
|
|
};
|
|
|
|
if chunks[1].rows().count() <= self.selected_song as usize {
|
|
rows = rows[self.selected_song as usize..].to_vec();
|
|
}
|
|
|
|
// Create the table
|
|
let table = Table::new(
|
|
rows,
|
|
&[
|
|
Constraint::Length(3), // ID column
|
|
Constraint::Percentage(50), // Playlist name column
|
|
Constraint::Percentage(20), // Song count column
|
|
Constraint::Length(5),
|
|
Constraint::Min(0),
|
|
],
|
|
)
|
|
.block(Block::default().borders(Borders::ALL).title(" Songs "))
|
|
.style(Style::default().fg(Color::Black));
|
|
|
|
frame.render_widget(table, chunks[1]);
|
|
}
|
|
}
|