modified: Cargo.lock
modified: Cargo.toml modified: src/main.rs modified: src/main_screen.rs new file: src/playlist_icon.rs
This commit is contained in:
parent
f337a6de1f
commit
ded8897ece
21
Cargo.lock
generated
21
Cargo.lock
generated
@ -1,6 +1,6 @@
|
|||||||
# This file is automatically @generated by Cargo.
|
# This file is automatically @generated by Cargo.
|
||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 3
|
version = 4
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "addr2line"
|
name = "addr2line"
|
||||||
@ -318,6 +318,15 @@ dependencies = [
|
|||||||
"tracing-error",
|
"tracing-error",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "color-thief"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6460d760cf38ce67c9e0318f896538820acc54f2d0a3bfc5b2c557211066c98"
|
||||||
|
dependencies = [
|
||||||
|
"rgb",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "color_quant"
|
name = "color_quant"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
@ -1311,6 +1320,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"color-eyre",
|
"color-eyre",
|
||||||
|
"color-thief",
|
||||||
"crossterm",
|
"crossterm",
|
||||||
"dirs",
|
"dirs",
|
||||||
"futures",
|
"futures",
|
||||||
@ -1819,6 +1829,15 @@ dependencies = [
|
|||||||
"windows-registry",
|
"windows-registry",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rgb"
|
||||||
|
version = "0.8.50"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a"
|
||||||
|
dependencies = [
|
||||||
|
"bytemuck",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ring"
|
name = "ring"
|
||||||
version = "0.17.8"
|
version = "0.17.8"
|
||||||
|
@ -27,3 +27,4 @@ throbber-widgets-tui = "0.8.0"
|
|||||||
image = { version = "0.24.9", default-features = false, features = ["jpeg", "png"] }
|
image = { version = "0.24.9", default-features = false, features = ["jpeg", "png"] }
|
||||||
ureq = "3.0.5"
|
ureq = "3.0.5"
|
||||||
rascii_art = "0.4.5"
|
rascii_art = "0.4.5"
|
||||||
|
color-thief = "0.2"
|
51
src/main.rs
51
src/main.rs
@ -1,28 +1,40 @@
|
|||||||
use std::{collections::HashMap, error::Error, io};
|
use std::{collections::HashMap, error::Error, io};
|
||||||
|
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
use crossterm::{event::{DisableMouseCapture, EnableMouseCapture, Event, EventStream, KeyCode, KeyEvent, KeyEventKind}, execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}};
|
use crossterm::{
|
||||||
|
event::{
|
||||||
|
DisableMouseCapture, EnableMouseCapture, Event, EventStream, KeyCode, KeyEvent,
|
||||||
|
KeyEventKind,
|
||||||
|
},
|
||||||
|
execute,
|
||||||
|
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||||
|
};
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use ratatui::{prelude::{Backend, CrosstermBackend}, widgets::Widget, Frame, Terminal};
|
|
||||||
use main_screen::MainScreen;
|
use main_screen::MainScreen;
|
||||||
|
use ratatui::{
|
||||||
|
prelude::{Backend, CrosstermBackend},
|
||||||
|
widgets::Widget,
|
||||||
|
Frame, Terminal,
|
||||||
|
};
|
||||||
use screen::AppScreen;
|
use screen::AppScreen;
|
||||||
use sync::AppEvent;
|
use sync::AppEvent;
|
||||||
use tokio::sync::mpsc::{self, Receiver, UnboundedSender};
|
use tokio::sync::mpsc::{self, Receiver, UnboundedSender};
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
use wait_screen::WaitScreen;
|
use wait_screen::WaitScreen;
|
||||||
|
|
||||||
mod dlp;
|
|
||||||
mod util;
|
|
||||||
mod config;
|
mod config;
|
||||||
mod screen;
|
mod dlp;
|
||||||
mod main_screen;
|
mod main_screen;
|
||||||
mod wait_screen;
|
mod playlist_icon;
|
||||||
|
mod screen;
|
||||||
mod sync;
|
mod sync;
|
||||||
|
mod util;
|
||||||
|
mod wait_screen;
|
||||||
|
|
||||||
#[derive(Eq, Hash, PartialEq)]
|
#[derive(Eq, Hash, PartialEq)]
|
||||||
enum AppState {
|
enum AppState {
|
||||||
IPodWait,
|
IPodWait,
|
||||||
MainScreen
|
MainScreen,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct App {
|
pub struct App {
|
||||||
@ -40,14 +52,20 @@ impl Default for App {
|
|||||||
let token = CancellationToken::new();
|
let token = CancellationToken::new();
|
||||||
|
|
||||||
sync::initialize_async_service(tx, jr, token.clone());
|
sync::initialize_async_service(tx, jr, token.clone());
|
||||||
|
|
||||||
let _ = jx.send(AppEvent::SearchIPod);
|
let _ = jx.send(AppEvent::SearchIPod);
|
||||||
|
|
||||||
let mut screens: HashMap<AppState, Box<dyn AppScreen>> = HashMap::new();
|
let mut screens: HashMap<AppState, Box<dyn AppScreen>> = HashMap::new();
|
||||||
screens.insert(AppState::IPodWait, Box::new(WaitScreen::default()));
|
screens.insert(AppState::IPodWait, Box::new(WaitScreen::default()));
|
||||||
screens.insert(AppState::MainScreen, Box::new(MainScreen::new(jx.clone())));
|
screens.insert(AppState::MainScreen, Box::new(MainScreen::new(jx.clone())));
|
||||||
|
|
||||||
Self { receiver: rx, sender: jx, token, state: AppState::IPodWait, screens }
|
Self {
|
||||||
|
receiver: rx,
|
||||||
|
sender: jx,
|
||||||
|
token,
|
||||||
|
state: AppState::IPodWait,
|
||||||
|
screens,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,7 +103,7 @@ impl App {
|
|||||||
let _ = self.sender.send(AppEvent::SearchIPod);
|
let _ = self.sender.send(AppEvent::SearchIPod);
|
||||||
},
|
},
|
||||||
AppEvent::ITunesParsed(xdb) => {
|
AppEvent::ITunesParsed(xdb) => {
|
||||||
|
|
||||||
},
|
},
|
||||||
AppEvent::SoundcloudGot(playlists) => {
|
AppEvent::SoundcloudGot(playlists) => {
|
||||||
let a = self.screens.get_mut(&AppState::MainScreen).unwrap();
|
let a = self.screens.get_mut(&AppState::MainScreen).unwrap();
|
||||||
@ -105,8 +123,13 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn handle_key_event(&mut self, key_event: KeyEvent) {
|
fn handle_key_event(&mut self, key_event: KeyEvent) {
|
||||||
self.screens.get_mut(&self.state).unwrap().handle_key_event(key_event);
|
self.screens
|
||||||
if let KeyCode::Char('q') = key_event.code { self.exit() }
|
.get_mut(&self.state)
|
||||||
|
.unwrap()
|
||||||
|
.handle_key_event(key_event);
|
||||||
|
if let KeyCode::Char('q') = key_event.code {
|
||||||
|
self.exit()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn exit(&mut self) {
|
fn exit(&mut self) {
|
||||||
@ -136,4 +159,4 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
terminal.show_cursor()?;
|
terminal.show_cursor()?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,22 @@
|
|||||||
use color_eyre::owo_colors::OwoColorize;
|
use color_eyre::owo_colors::OwoColorize;
|
||||||
use crossterm::event::{KeyCode, KeyEvent};
|
use crossterm::event::{KeyCode, KeyEvent};
|
||||||
use rascii_art::{charsets, render_image_to, RenderOptions};
|
use ratatui::{
|
||||||
use ratatui::{layout::{Constraint, Direction, Layout, Rect}, style::{Color, Modifier, Style, Stylize}, text::{Line, Span}, widgets::{Block, Borders, Gauge, Paragraph, Tabs}, Frame};
|
layout::{Constraint, Direction, Layout, Rect},
|
||||||
|
style::{Color, Modifier, Style, Stylize},
|
||||||
|
text::{Line, Span},
|
||||||
|
widgets::{Block, Borders, Gauge, Paragraph, Tabs},
|
||||||
|
Frame,
|
||||||
|
};
|
||||||
use soundcloud::sobjects::CloudPlaylists;
|
use soundcloud::sobjects::CloudPlaylists;
|
||||||
use strum::IntoEnumIterator;
|
use strum::IntoEnumIterator;
|
||||||
use tokio::sync::mpsc::UnboundedSender;
|
use tokio::sync::mpsc::UnboundedSender;
|
||||||
|
|
||||||
use crate::{screen::AppScreen, sync::AppEvent};
|
use crate::{playlist_icon::PlaylistIcon, screen::AppScreen, sync::AppEvent};
|
||||||
|
|
||||||
struct Playlist {
|
struct Playlist {
|
||||||
name: String,
|
name: String,
|
||||||
thumbnail_url: String,
|
thumbnail: PlaylistIcon,
|
||||||
link: String
|
link: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct MainScreen {
|
pub struct MainScreen {
|
||||||
@ -21,7 +26,7 @@ pub struct MainScreen {
|
|||||||
tab_titles: Vec<String>,
|
tab_titles: Vec<String>,
|
||||||
soundcloud: Option<Vec<Playlist>>,
|
soundcloud: Option<Vec<Playlist>>,
|
||||||
pub progress: Option<(u32, u32)>,
|
pub progress: Option<(u32, u32)>,
|
||||||
sender: UnboundedSender<AppEvent>
|
sender: UnboundedSender<AppEvent>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppScreen for MainScreen {
|
impl AppScreen for MainScreen {
|
||||||
@ -40,19 +45,26 @@ impl AppScreen for MainScreen {
|
|||||||
let chunks = Layout::default()
|
let chunks = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints([
|
.constraints([
|
||||||
Constraint::Length(3), // Tabs
|
Constraint::Length(3), // Tabs
|
||||||
Constraint::Min(0), // Main content area
|
Constraint::Min(0), // Main content area
|
||||||
Constraint::Length(1), // Status bar
|
Constraint::Length(1), // Status bar
|
||||||
])
|
])
|
||||||
.split(frame.area());
|
.split(frame.area());
|
||||||
|
|
||||||
let tabs = Tabs::new(
|
let tabs = Tabs::new(
|
||||||
self.tab_titles.iter().map(|t| Span::raw(t.clone())).collect::<Vec<Span>>(),
|
self.tab_titles
|
||||||
)
|
.iter()
|
||||||
.block(Block::default().borders(Borders::ALL))
|
.map(|t| Span::raw(t.clone()))
|
||||||
.highlight_style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD))
|
.collect::<Vec<Span>>(),
|
||||||
.select(self.selected_tab as usize)
|
)
|
||||||
.style(Style::default().fg(Color::White));
|
.block(Block::default().borders(Borders::ALL))
|
||||||
|
.highlight_style(
|
||||||
|
Style::default()
|
||||||
|
.fg(Color::Cyan)
|
||||||
|
.add_modifier(Modifier::BOLD),
|
||||||
|
)
|
||||||
|
.select(self.selected_tab as usize)
|
||||||
|
.style(Style::default().fg(Color::White));
|
||||||
|
|
||||||
frame.render_widget(tabs, chunks[0]);
|
frame.render_widget(tabs, chunks[0]);
|
||||||
|
|
||||||
@ -64,13 +76,19 @@ impl AppScreen for MainScreen {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Render Status Bar
|
// Render Status Bar
|
||||||
let status_bar = Paragraph::new(
|
let status_bar = Paragraph::new(Line::from(vec![
|
||||||
Line::from(
|
"◄ ► to change tab".bold(),
|
||||||
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()]
|
" | ".dark_gray(),
|
||||||
)
|
"<F5> SAVE FS".bold(),
|
||||||
)
|
" | ".dark_gray(),
|
||||||
|
"<F6> DL".bold(),
|
||||||
|
" | ".dark_gray(),
|
||||||
|
"<F8> DEL".bold(),
|
||||||
|
" | ".dark_gray(),
|
||||||
|
"<Q> QUIT".bold(),
|
||||||
|
]))
|
||||||
.centered();
|
.centered();
|
||||||
frame.render_widget(status_bar, chunks[2]); // Render into third chunk
|
frame.render_widget(status_bar, chunks[2]); // Render into third chunk
|
||||||
}
|
}
|
||||||
|
|
||||||
fn as_any(&mut self) -> &mut dyn std::any::Any {
|
fn as_any(&mut self) -> &mut dyn std::any::Any {
|
||||||
@ -79,15 +97,20 @@ impl AppScreen for MainScreen {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl MainScreen {
|
impl MainScreen {
|
||||||
pub fn new( sender: UnboundedSender<AppEvent> ) -> Self {
|
pub fn new(sender: UnboundedSender<AppEvent>) -> Self {
|
||||||
MainScreen {
|
MainScreen {
|
||||||
selected_row: -1,
|
selected_row: -1,
|
||||||
max_rows: 0,
|
max_rows: 0,
|
||||||
soundcloud: None,
|
soundcloud: None,
|
||||||
progress: None,
|
progress: None,
|
||||||
selected_tab: 0,
|
selected_tab: 0,
|
||||||
tab_titles: vec!["YouTube".to_string(), "SoundCloud".to_string(), "Local Playlists".to_string(), "Settings".to_string()],
|
tab_titles: vec![
|
||||||
sender
|
"YouTube".to_string(),
|
||||||
|
"SoundCloud".to_string(),
|
||||||
|
"Local Playlists".to_string(),
|
||||||
|
"Settings".to_string(),
|
||||||
|
],
|
||||||
|
sender,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,23 +120,28 @@ impl MainScreen {
|
|||||||
|
|
||||||
fn update_max_rows(&mut self) {
|
fn update_max_rows(&mut self) {
|
||||||
self.max_rows = match self.selected_tab {
|
self.max_rows = match self.selected_tab {
|
||||||
1 => self.soundcloud.as_deref().unwrap_or( &[]).len(),
|
1 => self.soundcloud.as_deref().unwrap_or(&[]).len(),
|
||||||
_ => 0
|
_ => 0,
|
||||||
}.try_into().unwrap();
|
}
|
||||||
|
.try_into()
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn next_tab(&mut self) {
|
fn next_tab(&mut self) {
|
||||||
self.selected_tab = std::cmp::min(self.selected_tab+1, (self.tab_titles.len()-1).try_into().unwrap());
|
self.selected_tab = std::cmp::min(
|
||||||
|
self.selected_tab + 1,
|
||||||
|
(self.tab_titles.len() - 1).try_into().unwrap(),
|
||||||
|
);
|
||||||
self.update_max_rows();
|
self.update_max_rows();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn previous_tab(&mut self) {
|
fn previous_tab(&mut self) {
|
||||||
self.selected_tab = std::cmp::max(0, self.selected_tab-1);
|
self.selected_tab = std::cmp::max(0, self.selected_tab - 1);
|
||||||
self.update_max_rows();
|
self.update_max_rows();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn previous_row(&mut self) {
|
fn previous_row(&mut self) {
|
||||||
self.selected_row = std::cmp::max(0, self.selected_row-1);
|
self.selected_row = std::cmp::max(0, self.selected_row - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn next_row(&mut self) {
|
fn next_row(&mut self) {
|
||||||
@ -121,42 +149,55 @@ impl MainScreen {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn download_row(&mut self) {
|
fn download_row(&mut self) {
|
||||||
if self.selected_tab == 1 {// SC
|
if self.selected_tab == 1 {
|
||||||
let playlist_url = self.soundcloud.as_ref().unwrap().get(self.selected_row as usize).unwrap().link.clone();
|
// SC
|
||||||
|
let playlist_url = self
|
||||||
|
.soundcloud
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.get(self.selected_row as usize)
|
||||||
|
.unwrap()
|
||||||
|
.link
|
||||||
|
.clone();
|
||||||
let _ = self.sender.send(AppEvent::DownloadPlaylist(playlist_url));
|
let _ = self.sender.send(AppEvent::DownloadPlaylist(playlist_url));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_soundcloud_playlists(&mut self, pl: CloudPlaylists) {
|
pub fn set_soundcloud_playlists(&mut self, pl: CloudPlaylists) {
|
||||||
self.soundcloud = Some(
|
self.soundcloud = Some(
|
||||||
pl.collection.iter().map(|p| Playlist { name: p.title.clone(), thumbnail_url: p.artwork_url.as_deref().map_or(String::new(), |u| self.ascii_art_from_url(u)), link: p.permalink_url.clone() }).collect()
|
pl.collection
|
||||||
|
.iter()
|
||||||
|
.map(|p| Playlist {
|
||||||
|
name: p.title.clone(),
|
||||||
|
thumbnail: p
|
||||||
|
.artwork_url
|
||||||
|
.as_deref()
|
||||||
|
.map_or(PlaylistIcon::default(), |u| self.ascii_art_from_url(u)),
|
||||||
|
link: p.permalink_url.clone(),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ascii_art_from_url(&self, url: &str) -> String {
|
fn ascii_art_from_url(&self, url: &str) -> PlaylistIcon {
|
||||||
let mut buf = String::new();
|
let img = image::load_from_memory(
|
||||||
let img = image::load_from_memory(&ureq::get(url).call().unwrap().body_mut().read_to_vec().unwrap() ).unwrap();
|
&ureq::get(url)
|
||||||
render_image_to(
|
.call()
|
||||||
&img,
|
.unwrap()
|
||||||
&mut buf,
|
.body_mut()
|
||||||
&RenderOptions {
|
.read_to_vec()
|
||||||
width: Some(16),
|
.unwrap(),
|
||||||
height: Some(16),
|
)
|
||||||
colored: true, // true
|
|
||||||
invert: false,
|
|
||||||
charset: charsets::BLOCK // BLOCK
|
|
||||||
})
|
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
PlaylistIcon::new(img.clone())
|
||||||
buf
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_progress(&self, frame: &mut Frame, area: Rect) {
|
fn render_progress(&self, frame: &mut Frame, area: Rect) {
|
||||||
let chunks = Layout::default()
|
let chunks = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints([
|
.constraints([
|
||||||
Constraint::Min(0), // Main content
|
Constraint::Min(0), // Main content
|
||||||
Constraint::Length(3), // Progress bar
|
Constraint::Length(3), // Progress bar
|
||||||
])
|
])
|
||||||
.split(area);
|
.split(area);
|
||||||
|
|
||||||
@ -166,16 +207,26 @@ impl MainScreen {
|
|||||||
frame.render_widget(main_content, chunks[0]);
|
frame.render_widget(main_content, chunks[0]);
|
||||||
|
|
||||||
let gauge = Gauge::default()
|
let gauge = Gauge::default()
|
||||||
.block(Block::default().borders(Borders::ALL).title(" Downloading Playlist "))
|
.block(
|
||||||
|
Block::default()
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.title(" Downloading Playlist "),
|
||||||
|
)
|
||||||
.gauge_style(Style::default().fg(Color::Green))
|
.gauge_style(Style::default().fg(Color::Green))
|
||||||
.ratio(self.progress.unwrap().0 as f64 / self.progress.unwrap().1 as f64)
|
.ratio(self.progress.unwrap().0 as f64 / self.progress.unwrap().1 as f64)
|
||||||
.label(format!("{:}/{:}", self.progress.unwrap().0, self.progress.unwrap().1));
|
.label(format!(
|
||||||
|
"{:}/{:}",
|
||||||
|
self.progress.unwrap().0,
|
||||||
|
self.progress.unwrap().1
|
||||||
|
));
|
||||||
|
|
||||||
frame.render_widget(gauge, chunks[1]);
|
frame.render_widget(gauge, chunks[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_tab(&self, frame: &mut Frame, area: Rect) /*-> Table<'_>*/ {
|
fn render_tab(&self, frame: &mut Frame, area: Rect) /*-> Table<'_>*/
|
||||||
if self.selected_tab == 1 { // SC
|
{
|
||||||
|
if self.selected_tab == 1 {
|
||||||
|
// SC
|
||||||
/*let mut v = Vec::new();
|
/*let mut v = Vec::new();
|
||||||
v.push(Row::new(vec!["Id", "Title", "Songs Count", "Date", "IS"]).style(Style::default().fg(Color::Gray)));
|
v.push(Row::new(vec!["Id", "Title", "Songs Count", "Date", "IS"]).style(Style::default().fg(Color::Gray)));
|
||||||
if let Some(s) = &self.soundcloud {
|
if let Some(s) = &self.soundcloud {
|
||||||
@ -183,9 +234,9 @@ impl MainScreen {
|
|||||||
let date: DateTime<Utc> = playlist.created_at.parse().unwrap();
|
let date: DateTime<Utc> = playlist.created_at.parse().unwrap();
|
||||||
let mut row = Row::new(
|
let mut row = Row::new(
|
||||||
vec![
|
vec![
|
||||||
playlist.id.to_string(),
|
playlist.id.to_string(),
|
||||||
playlist.title.clone(),
|
playlist.title.clone(),
|
||||||
[playlist.track_count.to_string(), " songs".to_string()].concat(),
|
[playlist.track_count.to_string(), " songs".to_string()].concat(),
|
||||||
format!("{}", date.format("%Y-%m-%d %H:%M")),
|
format!("{}", date.format("%Y-%m-%d %H:%M")),
|
||||||
"NO".to_string()
|
"NO".to_string()
|
||||||
]
|
]
|
||||||
@ -198,11 +249,13 @@ impl MainScreen {
|
|||||||
}
|
}
|
||||||
v*/
|
v*/
|
||||||
let v = self.soundcloud.as_deref().unwrap_or(&[]);
|
let v = self.soundcloud.as_deref().unwrap_or(&[]);
|
||||||
|
|
||||||
|
|
||||||
let rows = Layout::default()
|
let rows = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints(vec![Constraint::Percentage(100); math::round::ceil(v.len() as f64 / 3_f64, 0) as usize]) // Two rows
|
.constraints(vec![
|
||||||
|
Constraint::Percentage(100);
|
||||||
|
math::round::ceil(v.len() as f64 / 3_f64, 0) as usize
|
||||||
|
]) // Two rows
|
||||||
.split(area);
|
.split(area);
|
||||||
|
|
||||||
for (i, row) in rows.iter().enumerate() {
|
for (i, row) in rows.iter().enumerate() {
|
||||||
@ -210,35 +263,34 @@ impl MainScreen {
|
|||||||
.direction(Direction::Horizontal)
|
.direction(Direction::Horizontal)
|
||||||
.constraints(vec![Constraint::Length(16); 2]) // Three columns
|
.constraints(vec![Constraint::Length(16); 2]) // Three columns
|
||||||
.split(*row);
|
.split(*row);
|
||||||
|
|
||||||
for (j, col) in cols.iter().enumerate() {
|
for (j, col) in cols.iter().enumerate() {
|
||||||
let index = i * 3 + j;
|
let index = i * 3 + j;
|
||||||
if index < v.len() {
|
if index < v.len() {
|
||||||
let p = &v[index];
|
let p = &v[index];
|
||||||
|
|
||||||
let url_cl = p.thumbnail_url.clone();
|
/*let url_cl = p.thumbnail_url.clone();
|
||||||
let s = url_cl.lines().map(Line::from).collect::<Vec<Line>>();
|
let s = url_cl.lines().map(Line::from).collect::<Vec<Line>>();
|
||||||
|
|
||||||
let paragraph = Paragraph::new(s)
|
let paragraph = Paragraph::new(s)
|
||||||
.block(Block::default().borders(Borders::ALL))
|
.block(Block::default().borders(Borders::ALL))
|
||||||
.style(Style::default());
|
.style(Style::default());*/
|
||||||
|
|
||||||
|
frame.render_widget(p.thumbnail.clone(), *col);
|
||||||
frame.render_widget(paragraph, *col);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create the table
|
// Create the table
|
||||||
/* Table::new(rows, &[
|
/* Table::new(rows, &[
|
||||||
Constraint::Length(3), // ID column
|
Constraint::Length(3), // ID column
|
||||||
Constraint::Percentage(50), // Playlist name column
|
Constraint::Percentage(50), // Playlist name column
|
||||||
Constraint::Percentage(20), // Song count column
|
Constraint::Percentage(20), // Song count column
|
||||||
Constraint::Percentage(30),
|
Constraint::Percentage(30),
|
||||||
Constraint::Length(2)
|
Constraint::Length(2)
|
||||||
])
|
])
|
||||||
.block(Block::default().borders(Borders::ALL).title(" Playlists "))
|
.block(Block::default().borders(Borders::ALL).title(" Playlists "))
|
||||||
.style(Style::default().fg(Color::White)) */
|
.style(Style::default().fg(Color::White)) */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
52
src/playlist_icon.rs
Normal file
52
src/playlist_icon.rs
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use color_eyre::owo_colors::OwoColorize;
|
||||||
|
use image::{DynamicImage, GenericImageView};
|
||||||
|
use ratatui::{
|
||||||
|
buffer::Buffer,
|
||||||
|
layout::Rect,
|
||||||
|
style::{Color, Style, Stylize},
|
||||||
|
widgets::Widget,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Default, Clone)]
|
||||||
|
pub struct PlaylistIcon {
|
||||||
|
colors: [[u8; 3]; 8],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PlaylistIcon {
|
||||||
|
pub fn new(img: DynamicImage) -> Self {
|
||||||
|
let pixels = img
|
||||||
|
.resize_exact(8, 8, image::imageops::FilterType::Nearest)
|
||||||
|
.to_rgb8()
|
||||||
|
.pixels()
|
||||||
|
.map(|p| p.0)
|
||||||
|
.collect::<HashSet<[u8; 3]>>()
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.collect::<Vec<[u8; 3]>>();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
colors: pixels[..8].try_into().unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget for PlaylistIcon {
|
||||||
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
|
let mut i = 0;
|
||||||
|
|
||||||
|
for x in area.left()..area.right() {
|
||||||
|
for y in area.top()..area.bottom() {
|
||||||
|
let color = self.colors[i];
|
||||||
|
buf.set_string(
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
"█",
|
||||||
|
Style::default().fg(Color::Rgb(color[0], color[1], color[2])),
|
||||||
|
);
|
||||||
|
i = if i >= self.colors.len() - 1 { 0 } else { i + 1 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user