ferrosonic 0.8.2

A terminal-based Subsonic music client with bit-perfect audio playback
use crossterm::event::{self, MouseButton, MouseEventKind};

use crate::error::Error;

use super::*;

impl App {
    /// Handle mouse input
    pub(super) async fn handle_mouse(&mut self, mouse: event::MouseEvent) -> Result<(), Error> {
        let x = mouse.column;
        let y = mouse.row;

        match mouse.kind {
            MouseEventKind::Down(MouseButton::Left) => self.handle_mouse_click(x, y).await,
            MouseEventKind::ScrollUp => self.handle_mouse_scroll_up().await,
            MouseEventKind::ScrollDown => self.handle_mouse_scroll_down().await,
            _ => Ok(()),
        }
    }

    /// Handle left mouse click
    async fn handle_mouse_click(&mut self, x: u16, y: u16) -> Result<(), Error> {
        use crate::ui::header::{Header, HeaderRegion};

        let state = self.state.read().await;
        let layout = state.layout.clone();
        let page = state.page;
        let visible_pages = state.visible_pages();
        let duration = state.now_playing.duration;
        let radio_active = state.now_playing.radio_station.is_some();
        drop(state);

        // Check header area
        if y >= layout.header.y && y < layout.header.y + layout.header.height {
            if let Some(region) = Header::region_at(layout.header, x, y, &visible_pages) {
                match region {
                    HeaderRegion::Tab(tab_page) => {
                        let mut state = self.state.write().await;
                        state.page = tab_page;

                        let on_starred = state.browse.selected_option == Some(SongOption::Starred);
                        let browse_tab = state.browse.browse_tab.clone();
                        let refresh_songs = on_starred
                            && browse_tab == BrowseTab::Songs
                            && state.browse.starred_songs_dirty;
                        let refresh_albums = on_starred
                            && browse_tab == BrowseTab::Albums
                            && state.browse.starred_albums_dirty;

                        drop(state);

                        if refresh_songs {
                            self.get_starred_songs().await;
                            self.state.write().await.browse.starred_songs_dirty = false;
                        }
                        if refresh_albums {
                            self.get_starred_albums().await;
                            self.state.write().await.browse.starred_albums_dirty = false;
                        }
                    }
                    HeaderRegion::PrevButton => {
                        return self.prev_track().await;
                    }
                    HeaderRegion::PlayButton => {
                        return self.toggle_pause().await;
                    }
                    HeaderRegion::PauseButton => {
                        return self.toggle_pause().await;
                    }
                    HeaderRegion::StopButton => {
                        return self.stop_playback().await;
                    }
                    HeaderRegion::NextButton => {
                        return self.next_track().await;
                    }
                }
            }
            return Ok(());
        }

        // Check now playing area (progress bar seeking)
        if y >= layout.now_playing.y && y < layout.now_playing.y + layout.now_playing.height {
            let inner_bottom = layout.now_playing.y + layout.now_playing.height - 2;
            if y == inner_bottom && duration > 0.0 && !radio_active {
                let inner_x_start = layout.now_playing.x + 1;
                let inner_width = layout.now_playing.width.saturating_sub(2);
                if inner_width > 15 && x >= inner_x_start {
                    let rel_x = x - inner_x_start;
                    let time_width = 15u16;
                    let bar_width = inner_width.saturating_sub(time_width + 2);
                    let bar_start = (inner_width.saturating_sub(time_width + 2 + bar_width)) / 2
                        + time_width
                        + 2;
                    if bar_width > 0 && rel_x >= bar_start && rel_x < bar_start + bar_width {
                        let fraction = (rel_x - bar_start) as f64 / bar_width as f64;
                        let seek_pos = fraction * duration;
                        let _ = self.mpv.seek(seek_pos);
                        let mut state = self.state.write().await;
                        state.now_playing.position = seek_pos;
                    }
                }
            }
            return Ok(());
        }

        // Check content area
        if y >= layout.content.y && y < layout.content.y + layout.content.height {
            return self.handle_content_click(x, y, page, &layout).await;
        }

        Ok(())
    }

    /// Handle click within the content area
    async fn handle_content_click(
        &mut self,
        x: u16,
        y: u16,
        page: Page,
        layout: &LayoutAreas,
    ) -> Result<(), Error> {
        match page {
            Page::Browse => self.handle_browse_click(x, y, layout).await,
            Page::Artists => self.handle_artists_click(x, y, layout).await,
            Page::Queue => self.handle_queue_click(y, layout).await,
            Page::Playlists => self.handle_playlists_click(x, y, layout).await,
            Page::Radio => self.handle_radio_click(x, y, layout).await,
            _ => Ok(()),
        }
    }

    /// Handle click on queue page
    async fn handle_queue_click(&mut self, y: u16, layout: &LayoutAreas) -> Result<(), Error> {
        let mut state = self.state.write().await;
        let content = layout.content;

        // Account for border (1 row top)
        let row_in_viewport = y.saturating_sub(content.y + 1) as usize;
        let item_index = state.queue_state.scroll_offset + row_in_viewport;

        if item_index < state.queue.len() {
            let was_selected = state.queue_state.selected == Some(item_index);
            state.queue_state.selected = Some(item_index);

            let is_second_click = was_selected
                && self
                    .last_click
                    .is_some_and(|(_, ly, t)| ly == y && t.elapsed().as_millis() < 500);

            if is_second_click {
                drop(state);
                self.last_click = Some((0, y, std::time::Instant::now()));
                return self.play_queue_position(item_index).await;
            }
        }

        self.last_click = Some((0, y, std::time::Instant::now()));
        Ok(())
    }

    /// Handle mouse scroll up (move selection up in current list)
    async fn handle_mouse_scroll_up(&mut self) -> Result<(), Error> {
        let mut state = self.state.write().await;
        match state.page {
            Page::Browse => {
                if state.browse.focus == 1 {
                    match state.browse.browse_tab {
                        BrowseTab::Songs => {
                            if let Some(sel) = state.browse.selected_index {
                                if sel > 0 {
                                    state.browse.selected_index = Some(sel - 1);
                                }
                            }
                        }
                        BrowseTab::Albums => {
                            if let Some(sel) = state.browse.selected_album {
                                if sel > 0 {
                                    state.browse.selected_album = Some(sel - 1);
                                }
                            }
                        }
                    }
                }
            }
            Page::Artists => {
                if state.artists.focus == 0 {
                    if let Some(sel) = state.artists.selected_index {
                        if sel > 0 {
                            state.artists.selected_index = Some(sel - 1);
                        }
                    }
                } else if let Some(sel) = state.artists.selected_song {
                    if sel > 0 {
                        state.artists.selected_song = Some(sel - 1);
                    }
                }
            }
            Page::Queue => {
                if let Some(sel) = state.queue_state.selected {
                    if sel > 0 {
                        state.queue_state.selected = Some(sel - 1);
                    }
                } else if !state.queue.is_empty() {
                    state.queue_state.selected = Some(0);
                }
            }
            Page::Playlists => {
                if state.playlists.focus == 0 {
                    if let Some(sel) = state.playlists.selected_playlist {
                        if sel > 0 {
                            state.playlists.selected_playlist = Some(sel - 1);
                        }
                    }
                } else if let Some(sel) = state.playlists.selected_song {
                    if sel > 0 {
                        state.playlists.selected_song = Some(sel - 1);
                    }
                }
            }
            Page::Radio => {
                if let Some(sel) = state.radio.selected {
                    if sel > 0 {
                        let new_sel = sel - 1;
                        state.radio.selected = Some(new_sel);
                        if new_sel < state.radio.scroll_offset {
                            state.radio.scroll_offset = new_sel;
                        }
                    }
                } else if !state.radio.stations.is_empty() {
                    state.radio.selected = Some(0);
                    state.radio.scroll_offset = 0;
                }
            }
            _ => {}
        }
        Ok(())
    }

    /// Handle mouse scroll down (move selection down in current list)
    async fn handle_mouse_scroll_down(&mut self) -> Result<(), Error> {
        let mut state = self.state.write().await;
        match state.page {
            Page::Browse => {
                if state.browse.focus == 1 {
                    match state.browse.browse_tab {
                        BrowseTab::Songs => {
                            let max = state.browse.songs.len().saturating_sub(1);
                            if let Some(sel) = state.browse.selected_index {
                                if sel < max {
                                    state.browse.selected_index = Some(sel + 1);
                                }
                            } else if !state.browse.songs.is_empty() {
                                state.browse.selected_index = Some(0);
                            }

                            let should_load_more = state.browse.selected_option
                                == Some(SongOption::All)
                                && state.browse.all_songs_has_more
                                && !state.browse.all_songs_loading
                                && state
                                    .browse
                                    .selected_index
                                    .map(|i| {
                                        i + INFINITE_SCROLL_LOOKAHEAD >= state.browse.songs.len()
                                    })
                                    .unwrap_or(false);

                            drop(state);

                            if should_load_more {
                                self.get_all_songs(true).await;
                            }
                        }
                        BrowseTab::Albums => {
                            let max = state.browse.albums.len().saturating_sub(1);
                            if let Some(sel) = state.browse.selected_album {
                                if sel < max {
                                    state.browse.selected_album = Some(sel + 1);
                                }
                            } else if !state.browse.albums.is_empty() {
                                state.browse.selected_album = Some(0);
                            }

                            let should_load_more = state.browse.selected_option
                                == Some(SongOption::All)
                                && state.browse.albums_has_more
                                && !state.browse.albums_loading
                                && state
                                    .browse
                                    .selected_album
                                    .map(|i| {
                                        i + INFINITE_SCROLL_LOOKAHEAD >= state.browse.albums.len()
                                    })
                                    .unwrap_or(false);

                            drop(state);

                            if should_load_more {
                                self.get_browse_albums(true).await;
                            }
                        }
                    }
                }
            }
            Page::Artists => {
                if state.artists.focus == 0 {
                    let tree_items = crate::ui::pages::artists::build_tree_items(&state);
                    let max = tree_items.len().saturating_sub(1);
                    if let Some(sel) = state.artists.selected_index {
                        if sel < max {
                            state.artists.selected_index = Some(sel + 1);
                        }
                    } else if !tree_items.is_empty() {
                        state.artists.selected_index = Some(0);
                    }
                } else {
                    let max = state.artists.songs.len().saturating_sub(1);
                    if let Some(sel) = state.artists.selected_song {
                        if sel < max {
                            state.artists.selected_song = Some(sel + 1);
                        }
                    } else if !state.artists.songs.is_empty() {
                        state.artists.selected_song = Some(0);
                    }
                }
            }
            Page::Queue => {
                let max = state.queue.len().saturating_sub(1);
                if let Some(sel) = state.queue_state.selected {
                    if sel < max {
                        state.queue_state.selected = Some(sel + 1);
                    }
                } else if !state.queue.is_empty() {
                    state.queue_state.selected = Some(0);
                }
            }
            Page::Playlists => {
                if state.playlists.focus == 0 {
                    let max = state.playlists.playlists.len().saturating_sub(1);
                    if let Some(sel) = state.playlists.selected_playlist {
                        if sel < max {
                            state.playlists.selected_playlist = Some(sel + 1);
                        }
                    } else if !state.playlists.playlists.is_empty() {
                        state.playlists.selected_playlist = Some(0);
                    }
                } else {
                    let max = state.playlists.songs.len().saturating_sub(1);
                    if let Some(sel) = state.playlists.selected_song {
                        if sel < max {
                            state.playlists.selected_song = Some(sel + 1);
                        }
                    } else if !state.playlists.songs.is_empty() {
                        state.playlists.selected_song = Some(0);
                    }
                }
            }
            Page::Radio => {
                let max = state.radio.stations.len().saturating_sub(1);
                if let Some(sel) = state.radio.selected {
                    if sel < max {
                        let new_sel = sel + 1;
                        state.radio.selected = Some(new_sel);
                        let viewport = state.layout.content.height.saturating_sub(2) as usize;
                        if viewport > 0 && new_sel >= state.radio.scroll_offset + viewport {
                            state.radio.scroll_offset = new_sel + 1 - viewport;
                        }
                    }
                } else if !state.radio.stations.is_empty() {
                    state.radio.selected = Some(0);
                    state.radio.scroll_offset = 0;
                }
            }
            _ => {}
        }
        Ok(())
    }
}