use fuzzy_matcher::skim::SkimMatcherV2;
use fuzzy_matcher::FuzzyMatcher;
use crate::appview::{CommentView, NotificationView, StationInfo};
use crate::player::AudioSettings;
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum View {
Home,
Dsp,
Comments,
Notifications,
Profile,
Help,
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum Overlay {
None,
Search,
Compose,
SignIn,
AddStation,
}
#[derive(Clone, Default)]
pub struct AddStationForm {
pub name: String,
pub stream_url: String,
pub genre: String,
pub homepage: String,
pub logo: String,
pub focus: usize,
}
impl AddStationForm {
pub const FIELD_COUNT: usize = 5;
pub fn labels() -> [&'static str; Self::FIELD_COUNT] {
["Name*", "Stream URL*", "Genre", "Homepage", "Logo URL"]
}
pub fn field(&self, i: usize) -> &str {
match i {
0 => &self.name,
1 => &self.stream_url,
2 => &self.genre,
3 => &self.homepage,
_ => &self.logo,
}
}
pub fn field_mut(&mut self, i: usize) -> &mut String {
match i {
0 => &mut self.name,
1 => &mut self.stream_url,
2 => &mut self.genre,
3 => &mut self.homepage,
_ => &mut self.logo,
}
}
pub fn is_valid(&self) -> bool {
!self.name.trim().is_empty() && !self.stream_url.trim().is_empty()
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum HomeTab {
Trending,
Popular,
Recent,
Favorites,
Yours,
}
impl HomeTab {
pub const ORDER: [HomeTab; 5] = [
HomeTab::Popular,
HomeTab::Recent,
HomeTab::Trending,
HomeTab::Favorites,
HomeTab::Yours,
];
pub fn all() -> [HomeTab; 5] {
Self::ORDER
}
pub fn label(self) -> &'static str {
match self {
HomeTab::Trending => "Trending",
HomeTab::Popular => "Popular",
HomeTab::Recent => "Recent",
HomeTab::Favorites => "Favorites",
HomeTab::Yours => "Yours",
}
}
fn index(self) -> usize {
Self::ORDER.iter().position(|t| *t == self).unwrap_or(0)
}
pub fn next(self) -> HomeTab {
Self::ORDER[(self.index() + 1) % Self::ORDER.len()]
}
pub fn prev(self) -> HomeTab {
Self::ORDER[(self.index() + Self::ORDER.len() - 1) % Self::ORDER.len()]
}
}
#[derive(Clone, Default)]
pub struct Toast {
pub text: String,
pub ttl: u8,
}
impl Toast {
pub fn set(&mut self, text: impl Into<String>) {
self.text = text.into();
self.ttl = 6; }
pub fn tick(&mut self) {
if self.ttl > 0 {
self.ttl -= 1;
if self.ttl == 0 {
self.text.clear();
}
}
}
}
pub struct App {
pub view: View,
pub overlay: Overlay,
pub should_quit: bool,
pub home_tab: HomeTab,
pub trending: Vec<StationInfo>,
pub popular: Vec<StationInfo>,
pub favorites: Vec<StationInfo>,
pub stations: Vec<StationInfo>,
pub recent: Vec<StationInfo>,
pub recent_actors: Vec<String>,
pub profile_recent: Vec<StationInfo>,
pub profile_recent_selected: usize,
pub selected: usize,
pub search_query: String,
pub search_results: Vec<StationInfo>,
pub search_selected: usize,
pub search_dirty: bool,
pub compose_text: String,
pub signin_input: String,
pub pending_oauth: Option<String>,
pub add_form: AddStationForm,
pub comments: Vec<CommentView>,
pub comments_selected: usize,
pub notifications: Vec<NotificationView>,
pub unread: u32,
pub current: Option<StationInfo>,
pub volume: f32,
pub muted: bool,
pub dsp: AudioSettings,
pub dsp_row: usize,
pub logged_in: bool,
pub handle: Option<String>,
pub display_name: Option<String>,
pub did: Option<String>,
pub method: Option<String>,
pub pds: Option<String>,
pub env_creds: Option<(String, String)>,
pub toast: Toast,
matcher: SkimMatcherV2,
}
impl App {
pub fn new(logged_in: bool, handle: Option<String>) -> Self {
Self {
view: View::Home,
overlay: Overlay::None,
should_quit: false,
home_tab: HomeTab::Trending,
trending: Vec::new(),
popular: Vec::new(),
favorites: Vec::new(),
stations: Vec::new(),
recent: Vec::new(),
recent_actors: Vec::new(),
profile_recent: Vec::new(),
profile_recent_selected: 0,
selected: 0,
search_query: String::new(),
search_results: Vec::new(),
search_selected: 0,
search_dirty: false,
compose_text: String::new(),
signin_input: String::new(),
pending_oauth: None,
add_form: AddStationForm::default(),
comments: Vec::new(),
comments_selected: 0,
notifications: Vec::new(),
unread: 0,
current: None,
volume: 0.8,
muted: false,
dsp: AudioSettings::default(),
dsp_row: 0,
logged_in,
handle,
display_name: None,
did: None,
method: None,
pds: None,
env_creds: None,
toast: Toast::default(),
matcher: SkimMatcherV2::default(),
}
}
pub fn active_list(&self) -> &[StationInfo] {
match self.home_tab {
HomeTab::Trending => &self.trending,
HomeTab::Popular => &self.popular,
HomeTab::Recent => &self.recent,
HomeTab::Favorites => &self.favorites,
HomeTab::Yours => &self.stations,
}
}
pub fn selected_station(&self) -> Option<&StationInfo> {
self.active_list().get(self.selected)
}
pub fn ranked_search(&self) -> Vec<(i64, &StationInfo)> {
if self.search_query.is_empty() {
return self
.search_results
.iter()
.map(|s| (0i64, s))
.collect();
}
let mut scored: Vec<(i64, &StationInfo)> = self
.search_results
.iter()
.filter_map(|s| {
self.matcher
.fuzzy_match(&s.name, &self.search_query)
.map(|score| (score, s))
})
.collect();
scored.sort_by(|a, b| b.0.cmp(&a.0));
scored
}
pub fn clamp_selection(&mut self) {
let len = self.active_list().len();
if len == 0 {
self.selected = 0;
} else if self.selected >= len {
self.selected = len - 1;
}
}
pub fn volume_pct(&self) -> u16 {
(self.volume * 100.0).round() as u16
}
}