use std::sync::Arc;
use std::time::Instant;
use tokio::sync::RwLock;
use ratatui::layout::Rect;
use crate::app::models::SongOption;
use crate::config::Config;
use crate::subsonic::models::{Album, Artist, Child, Playlist};
use crate::ui::theme::{ThemeColors, ThemeData};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Page {
#[default]
Songs,
Artists,
Queue,
Playlists,
Server,
Settings,
}
impl Page {
pub fn index(&self) -> usize {
match self {
Page::Songs => 0,
Page::Artists => 1,
Page::Queue => 2,
Page::Playlists => 3,
Page::Server => 4,
Page::Settings => 5,
}
}
pub fn label(&self) -> &'static str {
match self {
Page::Songs => "Songs",
Page::Artists => "Artists",
Page::Queue => "Queue",
Page::Playlists => "Playlists",
Page::Server => "Server",
Page::Settings => "Settings",
}
}
pub fn shortcut(&self) -> &'static str {
match self {
Page::Songs => "F1",
Page::Artists => "F2",
Page::Queue => "F3",
Page::Playlists => "F4",
Page::Server => "F5",
Page::Settings => "F6",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PlaybackState {
#[default]
Stopped,
Playing,
Paused,
}
#[derive(Debug, Clone, Default)]
pub struct NowPlaying {
pub song: Option<Child>,
pub state: PlaybackState,
pub position: f64,
pub duration: f64,
pub sample_rate: Option<u32>,
pub bit_depth: Option<u32>,
pub format: Option<String>,
pub channels: Option<String>,
pub scrobbled: bool,
}
impl NowPlaying {
pub fn progress_percent(&self) -> f64 {
if self.duration > 0.0 {
(self.position / self.duration).clamp(0.0, 1.0)
} else {
0.0
}
}
pub fn format_position(&self) -> String {
format_duration(self.position)
}
pub fn format_duration(&self) -> String {
format_duration(self.duration)
}
}
pub fn format_duration(seconds: f64) -> String {
let total_secs = seconds as u64;
let hours = total_secs / 3600;
let mins = (total_secs % 3600) / 60;
let secs = total_secs % 60;
if hours > 0 {
format!("{:02}:{:02}:{:02}", hours, mins, secs)
} else {
format!("{:02}:{:02}", mins, secs)
}
}
#[derive(Debug, Clone, Default)]
pub struct SongsState {
pub songs: Vec<Child>,
pub selected_option: Option<SongOption>,
pub selected_index: Option<usize>,
pub focus: usize,
pub scroll_offset: usize,
pub is_starred_dirty: bool,
}
#[derive(Debug, Clone, Default)]
pub struct ArtistsState {
pub artists: Vec<Artist>,
pub selected_index: Option<usize>,
pub expanded: std::collections::HashSet<String>,
pub albums_cache: std::collections::HashMap<String, Vec<Album>>,
pub songs: Vec<Child>,
pub selected_song: Option<usize>,
pub filter: String,
pub filter_active: bool,
pub focus: usize,
pub tree_scroll_offset: usize,
pub song_scroll_offset: usize,
}
#[derive(Debug, Clone, Default)]
pub struct QueueState {
pub selected: Option<usize>,
pub scroll_offset: usize,
}
#[derive(Debug, Clone, Default)]
pub struct PlaylistsState {
pub playlists: Vec<Playlist>,
pub selected_playlist: Option<usize>,
pub songs: Vec<Child>,
pub selected_song: Option<usize>,
pub focus: usize,
pub playlist_scroll_offset: usize,
pub song_scroll_offset: usize,
}
#[derive(Debug, Clone, Default)]
pub struct ServerState {
pub selected_field: usize,
pub base_url: String,
pub username: String,
pub password: String,
pub status: Option<String>,
}
#[derive(Debug, Clone)]
pub struct SettingsState {
pub selected_field: usize,
pub themes: Vec<ThemeData>,
pub theme_index: usize,
pub cava_enabled: bool,
pub cava_size: u8,
pub notifications_enabled: bool,
pub scrobble_enabled: bool,
}
impl Default for SettingsState {
fn default() -> Self {
Self {
selected_field: 0,
themes: vec![ThemeData::default_theme()],
theme_index: 0,
cava_enabled: false,
cava_size: 40,
notifications_enabled: false,
scrobble_enabled: true,
}
}
}
impl SettingsState {
pub fn theme_name(&self) -> &str {
&self.themes[self.theme_index].name
}
pub fn theme_colors(&self) -> &ThemeColors {
&self.themes[self.theme_index].colors
}
pub fn current_theme(&self) -> &ThemeData {
&self.themes[self.theme_index]
}
pub fn next_theme(&mut self) {
self.theme_index = (self.theme_index + 1) % self.themes.len();
}
pub fn prev_theme(&mut self) {
self.theme_index = (self.theme_index + self.themes.len() - 1) % self.themes.len();
}
pub fn set_theme_by_name(&mut self, name: &str) -> bool {
if let Some(idx) = self
.themes
.iter()
.position(|t| t.name.eq_ignore_ascii_case(name))
{
self.theme_index = idx;
true
} else {
self.theme_index = 0; false
}
}
}
#[derive(Debug, Clone)]
pub struct Notification {
pub message: String,
pub is_error: bool,
pub created_at: Instant,
}
#[derive(Debug, Clone, Default)]
pub struct LayoutAreas {
pub header: Rect,
pub content: Rect,
pub now_playing: Rect,
pub content_left: Option<Rect>,
pub content_right: Option<Rect>,
}
#[derive(Debug, Default)]
pub struct AppState {
pub config: Config,
pub page: Page,
pub now_playing: NowPlaying,
pub queue: Vec<Child>,
pub queue_position: Option<usize>,
pub songs: SongsState,
pub artists: ArtistsState,
pub queue_state: QueueState,
pub playlists: PlaylistsState,
pub server_state: ServerState,
pub settings_state: SettingsState,
pub notification: Option<Notification>,
pub should_quit: bool,
pub cava_screen: Vec<CavaRow>,
pub cava_available: bool,
pub layout: LayoutAreas,
}
#[derive(Debug, Clone, Default)]
pub struct CavaRow {
pub spans: Vec<CavaSpan>,
}
#[derive(Debug, Clone)]
pub struct CavaSpan {
pub text: String,
pub fg: CavaColor,
pub bg: CavaColor,
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum CavaColor {
#[default]
Default,
Indexed(u8),
Rgb(u8, u8, u8),
}
impl AppState {
pub fn new(config: Config) -> Self {
let mut state = Self {
config: config.clone(),
..Default::default()
};
state.server_state.base_url = config.base_url.clone();
state.server_state.username = config.username.clone();
state.server_state.password = config.password.clone();
state.settings_state.cava_enabled = config.cava;
state.settings_state.cava_size = config.cava_size.clamp(10, 80);
state.settings_state.notifications_enabled = config.notifications;
state.settings_state.scrobble_enabled = config.scrobble;
state
}
pub fn current_song(&self) -> Option<&Child> {
self.queue_position.and_then(|pos| self.queue.get(pos))
}
pub fn notify(&mut self, message: impl Into<String>) {
self.notification = Some(Notification {
message: message.into(),
is_error: false,
created_at: Instant::now(),
});
}
pub fn notify_error(&mut self, message: impl Into<String>) {
self.notification = Some(Notification {
message: message.into(),
is_error: true,
created_at: Instant::now(),
});
}
pub fn check_notification_timeout(&mut self) {
if let Some(ref notif) = self.notification {
if notif.created_at.elapsed().as_secs() >= 2 {
self.notification = None;
}
}
}
pub fn clear_notification(&mut self) {
self.notification = None;
}
}
pub type SharedState = Arc<RwLock<AppState>>;
pub fn new_shared_state(config: Config) -> SharedState {
Arc::new(RwLock::new(AppState::new(config)))
}