use std::ffi::OsString;
use std::path::PathBuf;
use image::DynamicImage;
use termusiclib::config::v2::tui::{keys::KeyBinding, theme::styles::ColorTermusic};
use termusiclib::player::{GetProgressResponse, PlaylistTracks, UpdateEvents};
use termusiclib::podcast::{PodcastDLResult, PodcastFeed, PodcastSyncResult};
use termusiclib::songtag::{SongtagSearchResult, TrackDLMsg};
use tokio::sync::mpsc;
use crate::ui::components::TETrack;
use crate::ui::ids::{IdCEGeneral, IdCETheme, IdConfigEditor, IdKey, IdKeyGlobal, IdKeyOther};
use crate::ui::model::youtube_options::{YTDLMsg, YoutubeData, YoutubeOptions};
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum Msg {
ConfigEditor(ConfigEditorMsg),
DataBase(DBMsg),
Notification(NotificationMsg),
GeneralSearch(GSMsg),
Layout(MainLayoutMsg),
Library(LIMsg),
Player(PlayerMsg),
Playlist(PLMsg),
Podcast(PCMsg),
SavePlaylist(SavePlaylistMsg),
TagEditor(TEMsg),
YoutubeSearch(YSMsg),
Xywh(XYWHMsg),
LyricMessage(LyricMsg),
DeleteConfirm(DeleteConfirmMsg),
QuitPopup(QuitPopupMsg),
HelpPopup(HelpPopupMsg),
ErrorPopup(ErrorPopupMsg),
UpdatePhoto,
ForceRedraw,
ServerReqResponse(ServerReqResponse),
StreamUpdate(UpdateEvents),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MainLayoutMsg {
TreeView,
DataBase,
Podcast,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PlayerMsg {
ToggleGapless,
TogglePause,
VolumeUp,
VolumeDown,
SpeedUp,
SpeedDown,
SeekForward,
SeekBackward,
}
#[derive(Clone, Debug, Eq, Default)]
pub struct SPUpdateData {
pub path: OsString,
}
impl PartialEq for SPUpdateData {
fn eq(&self, _other: &Self) -> bool {
true
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SavePlaylistMsg {
Show(PathBuf),
Update(SPUpdateData),
CloseOk(PathBuf),
CloseCancel,
OverwriteCancel,
OverwriteOk(PathBuf),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum XYWHMsg {
ToggleHidden,
MoveLeft,
MoveRight,
MoveUp,
MoveDown,
ZoomIn,
ZoomOut,
CoverDLResult(CoverDLResult),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CoverDLResult {
FetchPhotoSuccess(ImageWrapper),
FetchPhotoErr(String),
}
#[derive(Clone, PartialEq, Debug)]
pub struct ImageWrapper {
pub data: DynamicImage,
}
impl Eq for ImageWrapper {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DeleteConfirmMsg {
CloseCancel,
CloseOk(PathBuf, Option<String>),
Show(PathBuf, Option<String>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum QuitPopupMsg {
CloseCancel,
CloseOk,
Show,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HelpPopupMsg {
Show,
Close,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ErrorPopupMsg {
Close,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum LyricMsg {
Cycle,
AdjustDelay(i64),
TextAreaBlurUp,
TextAreaBlurDown,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum IsDir {
No,
YesNotLoaded,
YesLoaded,
YesLoadedEmpty,
}
impl IsDir {
#[inline]
pub fn is_dir(self) -> bool {
!matches!(self, IsDir::No)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RecVec {
pub path: PathBuf,
pub is_dir: IsDir,
pub children: Vec<RecVec>,
}
#[derive(Clone, Debug, Eq, Default)]
pub struct LIReloadData {
pub change_root_path: Option<PathBuf>,
pub focus_node: Option<String>,
}
impl PartialEq for LIReloadData {
fn eq(&self, _other: &Self) -> bool {
true
}
}
#[derive(Clone, Debug, Eq, Default)]
pub struct LIReloadPathData {
pub path: PathBuf,
pub change_focus: bool,
}
impl PartialEq for LIReloadPathData {
fn eq(&self, _other: &Self) -> bool {
true
}
}
#[derive(Clone, Debug, Eq)]
pub struct LINodeReady {
pub vec: RecVec,
pub focus_node: Option<String>,
}
impl Default for LINodeReady {
fn default() -> Self {
let bogus_recvec = RecVec {
path: PathBuf::new(),
is_dir: IsDir::No,
children: Vec::new(),
};
Self {
vec: bogus_recvec,
focus_node: None,
}
}
}
impl PartialEq for LINodeReady {
fn eq(&self, _other: &Self) -> bool {
true
}
}
#[derive(Clone, Debug, Eq)]
pub struct LINodeReadySub {
pub vec: RecVec,
pub focus_node: Option<PathBuf>,
}
impl Default for LINodeReadySub {
fn default() -> Self {
let bogus_recvec = RecVec {
path: PathBuf::new(),
is_dir: IsDir::No,
children: Vec::new(),
};
Self {
vec: bogus_recvec,
focus_node: None,
}
}
}
impl PartialEq for LINodeReadySub {
fn eq(&self, _other: &Self) -> bool {
true
}
}
#[derive(Clone, Debug)]
pub struct LIReqNode {
pub sender: mpsc::Sender<Option<PathBuf>>,
}
impl Default for LIReqNode {
fn default() -> Self {
let (bogus_tx, _) = mpsc::channel(1);
Self { sender: bogus_tx }
}
}
impl Eq for LIReqNode {}
impl PartialEq for LIReqNode {
fn eq(&self, _other: &Self) -> bool {
true
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum LIMsg {
Reload(LIReloadData),
ReloadPath(LIReloadPathData),
TreeBlur,
PlaylistRunDelete,
PasteError(String),
SwitchRoot(PathBuf),
AddRoot(PathBuf),
RemoveRoot(PathBuf),
TreeNodeReady(LINodeReady),
TreeNodeReadySub(LINodeReadySub),
RequestCurrentPath(LIReqNode),
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum ConfigEditorMsg {
CloseCancel,
CloseOk,
ColorChanged(IdConfigEditor, ColorTermusic),
SymbolChanged(IdConfigEditor, String),
KeyChange(IdKey, KeyBinding),
ConfigChanged,
ConfigSaveOk,
ConfigSaveCancel,
Open,
ChangeLayout(KFMsg),
KeyFocusGlobal(KFMsg),
KeyFocusOther(KFMsg),
General(KFMsg),
Theme(KFMsg),
ThemeSelectLoad(usize),
}
pub const GENERAL_FOCUS_ORDER: &[IdCEGeneral] = &[
IdCEGeneral::MusicDir,
IdCEGeneral::ExitConfirmation,
IdCEGeneral::PlaylistDisplaySymbol,
IdCEGeneral::PlaylistRandomTrack,
IdCEGeneral::PlaylistRandomAlbum,
IdCEGeneral::PodcastDir,
IdCEGeneral::PodcastSimulDownload,
IdCEGeneral::PodcastMaxRetries,
IdCEGeneral::AlbumPhotoAlign,
IdCEGeneral::SaveLastPosition,
IdCEGeneral::SeekStep,
IdCEGeneral::KillDamon,
IdCEGeneral::PlayerUseMpris,
IdCEGeneral::PlayerUseDiscord,
IdCEGeneral::PlayerPort,
IdCEGeneral::PlayerAddress,
IdCEGeneral::PlayerProtocol,
IdCEGeneral::PlayerUDSPath,
IdCEGeneral::PlayerBackend,
IdCEGeneral::ExtraYtdlpArgs,
];
pub const THEME_FOCUS_ORDER: &[IdCETheme] = &[
IdCETheme::ThemeSelectTable,
IdCETheme::LibraryForeground,
IdCETheme::LibraryBackground,
IdCETheme::LibraryBorder,
IdCETheme::LibraryHighlight,
IdCETheme::LibraryHighlightSymbol,
IdCETheme::PlaylistForeground,
IdCETheme::PlaylistBackground,
IdCETheme::PlaylistBorder,
IdCETheme::PlaylistHighlight,
IdCETheme::PlaylistHighlightSymbol,
IdCETheme::CurrentlyPlayingTrackSymbol,
IdCETheme::ProgressForeground,
IdCETheme::ProgressBackground,
IdCETheme::ProgressBorder,
IdCETheme::LyricForeground,
IdCETheme::LyricBackground,
IdCETheme::LyricBorder,
IdCETheme::ImportantPopupForeground,
IdCETheme::ImportantPopupBackground,
IdCETheme::ImportantPopupBorder,
IdCETheme::FallbackForeground,
IdCETheme::FallbackBackground,
IdCETheme::FallbackBorder,
IdCETheme::FallbackHighlight,
];
pub const KFGLOBAL_FOCUS_ORDER: &[IdKey] = &[
IdKey::Global(IdKeyGlobal::LayoutTreeview),
IdKey::Global(IdKeyGlobal::LayoutDatabase),
IdKey::Global(IdKeyGlobal::LayoutPodcast),
IdKey::Global(IdKeyGlobal::Quit),
IdKey::Global(IdKeyGlobal::Config),
IdKey::Global(IdKeyGlobal::Help),
IdKey::Global(IdKeyGlobal::SavePlaylist),
IdKey::Global(IdKeyGlobal::Up),
IdKey::Global(IdKeyGlobal::Down),
IdKey::Global(IdKeyGlobal::Left),
IdKey::Global(IdKeyGlobal::Right),
IdKey::Global(IdKeyGlobal::GotoBottom),
IdKey::Global(IdKeyGlobal::GotoTop),
IdKey::Global(IdKeyGlobal::PlayerToggleGapless),
IdKey::Global(IdKeyGlobal::PlayerTogglePause),
IdKey::Global(IdKeyGlobal::PlayerNext),
IdKey::Global(IdKeyGlobal::PlayerPrevious),
IdKey::Global(IdKeyGlobal::PlayerSeekForward),
IdKey::Global(IdKeyGlobal::PlayerSeekBackward),
IdKey::Global(IdKeyGlobal::PlayerSpeedUp),
IdKey::Global(IdKeyGlobal::PlayerSpeedDown),
IdKey::Global(IdKeyGlobal::PlayerVolumeUp),
IdKey::Global(IdKeyGlobal::PlayerVolumeDown),
IdKey::Global(IdKeyGlobal::LyricAdjustForward),
IdKey::Global(IdKeyGlobal::LyricAdjustBackward),
IdKey::Global(IdKeyGlobal::LyricCycle),
IdKey::Global(IdKeyGlobal::XywhMoveUp),
IdKey::Global(IdKeyGlobal::XywhMoveDown),
IdKey::Global(IdKeyGlobal::XywhMoveLeft),
IdKey::Global(IdKeyGlobal::XywhMoveRight),
IdKey::Global(IdKeyGlobal::XywhZoomIn),
IdKey::Global(IdKeyGlobal::XywhZoomOut),
IdKey::Global(IdKeyGlobal::XywhHide),
];
pub const KFOTHER_FOCUS_ORDER: &[IdKey] = &[
IdKey::Other(IdKeyOther::LibraryAddRoot),
IdKey::Other(IdKeyOther::LibraryRemoveRoot),
IdKey::Other(IdKeyOther::LibrarySwitchRoot),
IdKey::Other(IdKeyOther::LibraryDelete),
IdKey::Other(IdKeyOther::LibraryLoadDir),
IdKey::Other(IdKeyOther::LibraryYank),
IdKey::Other(IdKeyOther::LibraryPaste),
IdKey::Other(IdKeyOther::LibrarySearch),
IdKey::Other(IdKeyOther::LibrarySearchYoutube),
IdKey::Other(IdKeyOther::LibraryTagEditor),
IdKey::Other(IdKeyOther::PlaylistShuffle),
IdKey::Other(IdKeyOther::PlaylistModeCycle),
IdKey::Other(IdKeyOther::PlaylistPlaySelected),
IdKey::Other(IdKeyOther::PlaylistSearch),
IdKey::Other(IdKeyOther::PlaylistSwapUp),
IdKey::Other(IdKeyOther::PlaylistSwapDown),
IdKey::Other(IdKeyOther::PlaylistDelete),
IdKey::Other(IdKeyOther::PlaylistDeleteAll),
IdKey::Other(IdKeyOther::PlaylistAddRandomAlbum),
IdKey::Other(IdKeyOther::PlaylistAddRandomTracks),
IdKey::Other(IdKeyOther::DatabaseAddAll),
IdKey::Other(IdKeyOther::DatabaseAddSelected),
IdKey::Other(IdKeyOther::PodcastSearchAddFeed),
IdKey::Other(IdKeyOther::PodcastMarkPlayed),
IdKey::Other(IdKeyOther::PodcastMarkAllPlayed),
IdKey::Other(IdKeyOther::PodcastEpDownload),
IdKey::Other(IdKeyOther::PodcastEpDeleteFile),
IdKey::Other(IdKeyOther::PodcastDeleteFeed),
IdKey::Other(IdKeyOther::PodcastDeleteAllFeeds),
IdKey::Other(IdKeyOther::PodcastRefreshFeed),
IdKey::Other(IdKeyOther::PodcastRefreshAllFeeds),
];
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum KFMsg {
Next,
Previous,
}
#[derive(Debug, PartialEq, Clone, Copy, Eq, Hash)]
pub enum ConfigEditorLayout {
General,
ThemeAndColor,
KeyGlobal,
KeyOther,
}
impl ConfigEditorLayout {
pub fn choice_array() -> [&'static str; 4] {
[
"General Configuration",
"Themes and Colors",
"Keys Global",
"Keys Other",
]
}
pub fn to_array_idx(self) -> usize {
match self {
Self::General => 0,
Self::ThemeAndColor => 1,
Self::KeyGlobal => 2,
Self::KeyOther => 3,
}
}
}
impl Default for ConfigEditorLayout {
fn default() -> Self {
CONFIG_EDITOR_TABS_ORDER[0]
}
}
impl From<ConfigEditorLayout> for IdConfigEditor {
fn from(value: ConfigEditorLayout) -> Self {
match value {
ConfigEditorLayout::General => IdConfigEditor::General(IdCEGeneral::MusicDir),
ConfigEditorLayout::ThemeAndColor => IdConfigEditor::Theme(IdCETheme::ThemeSelectTable),
ConfigEditorLayout::KeyGlobal => KFGLOBAL_FOCUS_ORDER[0].into(),
ConfigEditorLayout::KeyOther => KFOTHER_FOCUS_ORDER[0].into(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct UnsupportedId;
impl TryFrom<IdConfigEditor> for ConfigEditorLayout {
type Error = UnsupportedId;
fn try_from(value: IdConfigEditor) -> Result<Self, Self::Error> {
Ok(match value {
IdConfigEditor::General(_) => Self::General,
IdConfigEditor::Theme(_) => Self::ThemeAndColor,
IdConfigEditor::KeyGlobal(_) => Self::KeyGlobal,
IdConfigEditor::KeyOther(_) => Self::KeyOther,
_ => return Err(UnsupportedId),
})
}
}
pub const CONFIG_EDITOR_TABS_ORDER: &[ConfigEditorLayout] = &[
ConfigEditorLayout::General,
ConfigEditorLayout::ThemeAndColor,
ConfigEditorLayout::KeyGlobal,
ConfigEditorLayout::KeyOther,
];
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DBMsg {
AddAllToPlaylist,
AddPlaylist(usize),
AddAllResultsToPlaylist,
AddResultToPlaylist(usize),
CriteriaBlurDown,
CriteriaBlurUp,
SearchResult(SearchCriteria),
SearchResultBlurDown,
SearchResultBlurUp,
SearchTrack(usize),
SearchTracksBlurDown,
SearchTracksBlurUp,
AddAllResultsConfirmShow,
AddAllResultsConfirmCancel,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum PLMsg {
NextSong,
PrevSong,
PlaylistTableBlurDown,
PlaylistTableBlurUp,
Add(PathBuf),
Delete(usize),
DeleteAll,
LoopModeCycle,
PlaySelected(usize),
Shuffle,
SwapDown(usize),
SwapUp(usize),
AddRandomAlbum,
AddRandomTracks,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum GSMsg {
PopupShowDatabase,
PopupShowLibrary(PathBuf),
PopupShowPlaylist,
PopupShowEpisode,
PopupShowPodcast,
PopupCloseCancel,
InputBlur,
PopupUpdateDatabase(String),
PopupUpdateLibrary(String, PathBuf),
PopupUpdatePlaylist(String),
PopupUpdateEpisode(String),
PopupUpdatePodcast(String),
TableBlur,
PopupCloseEpisodeAddPlaylist,
PopupCloseDatabaseAddPlaylist,
PopupCloseLibraryAddPlaylist,
PopupCloseOkLibraryLocate,
PopupClosePlaylistPlaySelected,
PopupCloseOkPlaylistLocate,
PopupCloseOkEpisodeLocate,
PopupCloseOkPodcastLocate,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum YSMsg {
InputPopupShow(PathBuf),
InputPopupCloseCancel,
InputPopupCloseOk(String, PathBuf),
ReqNextPage,
ReqPreviousPage,
PageLoaded(YoutubeData),
PageLoadError(String),
TablePopupCloseCancel,
TablePopupCloseOk(usize, PathBuf),
YoutubeSearchSuccess(YoutubeOptions),
YoutubeSearchFail(String),
Download(YTDLMsg),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum TEMsg {
Open(PathBuf),
Close,
CounterDeleteOk,
CounterSaveOk,
Embed(usize),
EmbedDone(Box<TETrack>),
EmbedErr(String),
Focus(TFMsg),
Save,
SelectLyricOk(usize),
Search,
SearchLyricResult(SongtagSearchResult),
Download(usize),
TrackDownloadResult(TrackDLMsg),
TrackDownloadPreError(String),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TFMsg {
CounterDeleteBlurDown,
CounterDeleteBlurUp,
CounterSaveBlurDown,
CounterSaveBlurUp,
InputArtistBlurDown,
InputArtistBlurUp,
InputTitleBlurDown,
InputTitleBlurUp,
InputAlbumBlurDown,
InputAlbumBlurUp,
InputGenreBlurDown,
InputGenreBlurUp,
SelectLyricBlurDown,
SelectLyricBlurUp,
TableLyricOptionsBlurDown,
TableLyricOptionsBlurUp,
TextareaLyricBlurDown,
TextareaLyricBlurUp,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum PCMsg {
PodcastBlurDown,
PodcastBlurUp,
EpisodeBlurDown,
EpisodeBlurUp,
PodcastAddPopupShow,
PodcastAddPopupCloseOk(String),
PodcastAddPopupCloseCancel,
PodcastSelected(usize),
DescriptionUpdate,
EpisodeAdd(usize),
EpisodeMarkPlayed(usize),
EpisodeMarkAllPlayed,
PodcastRefreshOne(usize),
PodcastRefreshAll,
EpisodeDownload(usize),
EpisodeDeleteFile(usize),
FeedDeleteShow,
FeedDeleteCloseOk,
FeedDeleteCloseCancel,
FeedsDeleteShow,
FeedsDeleteCloseOk,
FeedsDeleteCloseCancel,
SearchItunesCloseCancel,
SearchItunesCloseOk(usize),
SearchSuccess(Vec<PodcastFeed>),
SearchError(String),
SyncResult(PodcastSyncResult),
DLResult(PodcastDLResult),
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum NotificationMsg {
MessageShow((String, String)),
MessageHide((String, String)),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SearchCriteria {
Artist,
Album,
Genre,
Directory,
Playlist,
}
impl SearchCriteria {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
SearchCriteria::Artist => "artist",
SearchCriteria::Album => "album",
SearchCriteria::Genre => "genre",
SearchCriteria::Directory => "directory",
SearchCriteria::Playlist => "playlist",
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ServerReqResponse {
GetProgress(GetProgressResponse),
FullPlaylist(PlaylistTracks),
}
impl Eq for ServerReqResponse {}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use crate::ui::{ids::IdKey, msg::CONFIG_EDITOR_TABS_ORDER};
use super::{KFGLOBAL_FOCUS_ORDER, KFOTHER_FOCUS_ORDER};
#[test]
#[allow(clippy::const_is_empty)]
fn kfglobal_focus_order_should_be_nonzero() {
assert!(!KFGLOBAL_FOCUS_ORDER.is_empty());
}
#[test]
fn kfglobal_focus_order_should_only_contain_global() {
for entry in KFGLOBAL_FOCUS_ORDER {
assert_eq!(
std::mem::discriminant(entry),
std::mem::discriminant(&IdKey::Global(crate::ui::ids::IdKeyGlobal::Config))
);
}
}
#[test]
#[allow(clippy::const_is_empty)]
fn kfother_focus_order_should_be_nonzero() {
assert!(!KFOTHER_FOCUS_ORDER.is_empty());
}
#[test]
fn kfother_focus_order_should_only_contain_other() {
for entry in KFOTHER_FOCUS_ORDER {
assert_eq!(
std::mem::discriminant(entry),
std::mem::discriminant(&IdKey::Other(crate::ui::ids::IdKeyOther::DatabaseAddAll))
);
}
}
#[test]
#[allow(clippy::const_is_empty)]
fn config_editor_tabs_order_should_be_nonzero() {
assert!(!CONFIG_EDITOR_TABS_ORDER.is_empty());
}
#[test]
fn config_editor_tabs_order_maps_to_enum() {
let mut set = HashSet::new();
for id in CONFIG_EDITOR_TABS_ORDER {
assert!(
set.insert(*id),
"Duplicate value in CONFIG_EDITOR_TABS_ORDER2!"
);
}
}
}