git/grrs/main
src/input.rs
381 lines19 KB last change raw · history
use std::{fs, io::Write, os::unix::net::UnixStream};

use crossterm::event::{Event as CEvent, KeyCode, KeyModifiers, MouseButton, MouseEventKind};
use ratatui::layout::{Position, Rect};
use ratatui_image::picker::Picker;
use tokio::sync::mpsc;

use crate::{
    app::{AppEvent, AppMode, AppState},
    ui::help_hint_rect,
};

pub struct InputOutcome {
    pub should_quit: bool,
    pub should_clear: bool,
}

pub async fn handle_input(
    evt: CEvent,
    state: &mut AppState,
    tx: &mpsc::Sender<AppEvent>,
    picker: &Picker,
    _loop_picker: &Picker,
    socket_path: &str,
    http_client: &reqwest::Client,
) -> InputOutcome {
    let mut clear = false;

    if let AppMode::ConfigError(_) = state.mode {
        match evt {
            CEvent::Key(key) => {
                if key.code == KeyCode::Char('q')
                    || key.code == KeyCode::Esc
                    || (key.code == KeyCode::Char('c') && key.modifiers.contains(KeyModifiers::CONTROL))
                {
                    return InputOutcome { should_quit: true, should_clear: false };
                } else if key.code == KeyCode::Down || key.code == KeyCode::Char('j') {
                    state.config_scroll = state.config_scroll.saturating_add(1);
                } else if key.code == KeyCode::Up || key.code == KeyCode::Char('k') {
                    state.config_scroll = state.config_scroll.saturating_sub(1);
                }
            },
            CEvent::Mouse(mouse) => {
                if mouse.kind == MouseEventKind::ScrollDown {
                    state.config_scroll = state.config_scroll.saturating_add(1);
                } else if mouse.kind == MouseEventKind::ScrollUp {
                    state.config_scroll = state.config_scroll.saturating_sub(1);
                }
            },
            _ => {},
        }
        return InputOutcome { should_quit: false, should_clear: false };
    }

    if state.is_searching {
        match evt {
            CEvent::Key(key) => {
                if key.code == KeyCode::Esc || key.code == KeyCode::Enter {
                    state.is_searching = false;
                    if key.code == KeyCode::Esc {
                        state.search_query.clear();
                        state.update_search();
                        if let Some(idx) = state.liked_list_state.selected() {
                            if let Some(song) = state.filtered_likes.get(idx) {
                                load_local_art(song.id, state.art_path(song.id), tx.clone(), picker.clone());
                            }
                        }
                    }
                    clear = true;
                } else if key.code == KeyCode::Backspace {
                    let _ = state.search_query.pop();
                    state.update_search();
                    if let Some(idx) = state.liked_list_state.selected() {
                        if let Some(song) = state.filtered_likes.get(idx) {
                            load_local_art(song.id, state.art_path(song.id), tx.clone(), picker.clone());
                        }
                    } else {
                        state.selected_art = None;
                    }
                    clear = true;
                } else if let KeyCode::Char(c) = key.code {
                    state.search_query.push(c);
                    state.update_search();
                    if let Some(idx) = state.liked_list_state.selected() {
                        if let Some(song) = state.filtered_likes.get(idx) {
                            load_local_art(song.id, state.art_path(song.id), tx.clone(), picker.clone());
                        }
                    } else {
                        state.selected_art = None;
                    }
                    clear = true;
                }
                return InputOutcome { should_quit: false, should_clear: clear };
            },
            _ => return InputOutcome { should_quit: false, should_clear: clear },
        }
    }

    match evt {
        CEvent::Key(key) => {
            if key.code == KeyCode::Char('q')
                || (key.code == KeyCode::Char('c') && key.modifiers.contains(KeyModifiers::CONTROL))
            {
                return InputOutcome { should_quit: true, should_clear: false };
            }

            if state.show_unlike_confirm.is_some() {
                if key.code == KeyCode::Char('y') || key.code == KeyCode::Char('Y') {
                    if let Some(id) = state.show_unlike_confirm {
                        state.likes.remove(&id);
                        if let Ok(data) = serde_json::to_string_pretty(&state.likes) {
                            fs::write(&state.likes_file, data).ok();
                        }
                        let path_to_remove = state.art_path(id);
                        tokio::spawn(async move {
                            let _ = tokio::fs::remove_file(path_to_remove).await;
                        });
                        state.update_search();
                    }
                    state.show_unlike_confirm = None;
                    clear = true;

                    let sorted = state.filtered_likes.clone();
                    if sorted.is_empty() {
                        state.liked_list_state.select(None);
                        state.selected_art = None;
                    } else {
                        let next = state.liked_list_state.selected().unwrap_or(0).min(sorted.len().saturating_sub(1));
                        state.liked_list_state.select(Some(next));
                        load_local_art(sorted[next].id, state.art_path(sorted[next].id), tx.clone(), picker.clone());
                    }
                } else if key.code == KeyCode::Char('n') || key.code == KeyCode::Char('N') || key.code == KeyCode::Esc {
                    state.show_unlike_confirm = None;
                    clear = true;
                }
                return InputOutcome { should_quit: false, should_clear: clear };
            }

            if state.show_help_overlay {
                if key.code == KeyCode::Char('?') || key.code == KeyCode::Esc {
                    state.show_help_overlay = false;
                    state.help_tab = 0;
                    clear = true;
                } else if key.code == KeyCode::Tab {
                    state.help_tab = (state.help_tab + 1) % 2;
                }
                return InputOutcome { should_quit: false, should_clear: clear };
            }
            if key.code == KeyCode::Char('?') {
                state.show_help_overlay = true;
                return InputOutcome { should_quit: false, should_clear: clear };
            }

            if key.code == KeyCode::Char('m') {
                state.is_muted = !state.is_muted;
                if let Ok(mut stream) = UnixStream::connect(socket_path) {
                    let _ = stream.write_all(b"{\"command\": [\"cycle\", \"mute\"]}\n");
                }
                return InputOutcome { should_quit: false, should_clear: clear };
            } else if key.code == KeyCode::Char('-') {
                state.volume = state.volume.saturating_sub(5);
                state.volume_time = Some(std::time::Instant::now());
                if let Ok(mut stream) = UnixStream::connect(socket_path) {
                    let cmd = format!("{{\"command\": [\"set_property\", \"volume\", {}]}}\n", state.volume);
                    let _ = stream.write_all(cmd.as_bytes());
                }
                return InputOutcome { should_quit: false, should_clear: clear };
            } else if key.code == KeyCode::Char('=') || key.code == KeyCode::Char('+') {
                state.volume = state.volume.saturating_add(5).min(100);
                state.volume_time = Some(std::time::Instant::now());
                if let Ok(mut stream) = UnixStream::connect(socket_path) {
                    let cmd = format!("{{\"command\": [\"set_property\", \"volume\", {}]}}\n", state.volume);
                    let _ = stream.write_all(cmd.as_bytes());
                }
                return InputOutcome { should_quit: false, should_clear: clear };
            } else if key.code == KeyCode::Char('h') {
                if webbrowser::open("https://app.gensokyoradio.net/history").is_ok() {
                    state.status_message = "opened history in browser!".to_string();
                } else {
                    state.status_message = "failed to open browser".to_string();
                }
                state.status_message_time = Some(std::time::Instant::now());
                return InputOutcome { should_quit: false, should_clear: clear };
            }

            if state.mode == AppMode::Playing {
                if key.code == KeyCode::Tab {
                    state.mode = AppMode::LikedSongs;
                    state.nav_time = Some(std::time::Instant::now());
                    let sorted = state.filtered_likes.clone();
                    if !sorted.is_empty() {
                        state.liked_list_state.select(Some(0));
                        load_local_art(sorted[0].id, state.art_path(sorted[0].id), tx.clone(), picker.clone());
                    }
                } else if key.code == KeyCode::Char('o') || key.code == KeyCode::Enter {
                    if !state.song.album.is_empty() {
                        let query = urlencoding::encode(&state.song.album);
                        let url = format!("https://touhoudb.com/Search?searchType=Album&filter={query}");
                        if webbrowser::open(&url).is_ok() {
                            state.status_message = "opened touhoudb in browser!".to_string();
                        } else {
                            state.status_message = "failed to open browser".to_string();
                        }
                        state.status_message_time = Some(std::time::Instant::now());
                    }
                } else if key.code == KeyCode::Char('l') {
                    let was_liked = state.likes.contains_key(&state.song.id);
                    state.toggle_like();
                    if state.likes.contains_key(&state.song.id) && !was_liked && !state.song.art_url.is_empty() {
                        let art_dir =
                            state.likes_file.parent().expect("likes_file has no parent directory").join("art");
                        fs::create_dir_all(&art_dir).ok();
                        let url = if state.song.art_url.starts_with("http") {
                            state.song.art_url.clone()
                        } else {
                            format!("https://gensokyoradio.net{}", state.song.art_url)
                        };
                        let path = state.art_path(state.song.id);
                        let client = http_client.clone();
                        tokio::spawn(async move {
                            if let Ok(resp) = client.get(&url).send().await
                                && let Ok(bytes) = resp.bytes().await
                            {
                                let _ = tokio::fs::write(path, bytes).await;
                            }
                        });
                    } else if !state.likes.contains_key(&state.song.id) && was_liked {
                        let path_to_remove = state.art_path(state.song.id);
                        tokio::spawn(async move {
                            let _ = tokio::fs::remove_file(path_to_remove).await;
                        });
                    }
                } else if (key.code == KeyCode::Char('c') || key.code == KeyCode::Char('C'))
                    && let Ok(mut clipboard) = arboard::Clipboard::new()
                {
                    let text = if key.code == KeyCode::Char('c') {
                        format!("{} - {}", state.song.artist, state.song.title)
                    } else {
                        format!(
                            "Title: {}\nArtist: {}\nAlbum: {}\nCircle: {}\nYear: {}",
                            state.song.title, state.song.artist, state.song.album, state.song.circle, state.song.year
                        )
                    };
                    let _ = clipboard.set_text(text);
                    state.status_message = if key.code == KeyCode::Char('c') {
                        "copied track/artist!".to_string()
                    } else {
                        "copied full metadata!".to_string()
                    };
                    state.status_message_time = Some(std::time::Instant::now());
                }
            } else if state.mode == AppMode::LikedSongs {
                if key.code == KeyCode::Tab || key.code == KeyCode::Esc {
                    state.mode = AppMode::Playing;
                    state.selected_art = None;
                    state.nav_time = Some(std::time::Instant::now());
                } else if key.code == KeyCode::Char('/') {
                    state.is_searching = true;
                    clear = true;
                } else if key.code == KeyCode::Up || key.code == KeyCode::Char('k') {
                    let sorted = state.filtered_likes.clone();
                    if !sorted.is_empty() {
                        let current = state.liked_list_state.selected().unwrap_or(0);
                        let next = if current == 0 { sorted.len() - 1 } else { current - 1 };
                        state.liked_list_state.select(Some(next));
                        if current != next {
                            state.selected_art = None;
                            load_local_art(
                                sorted[next].id,
                                state.art_path(sorted[next].id),
                                tx.clone(),
                                picker.clone(),
                            );
                        }
                    }
                } else if key.code == KeyCode::Down || key.code == KeyCode::Char('j') {
                    let sorted = state.filtered_likes.clone();
                    if !sorted.is_empty() {
                        let current = state.liked_list_state.selected().unwrap_or(0);
                        let next = if current == sorted.len() - 1 { 0 } else { current + 1 };
                        state.liked_list_state.select(Some(next));
                        if current != next {
                            state.selected_art = None;
                            load_local_art(
                                sorted[next].id,
                                state.art_path(sorted[next].id),
                                tx.clone(),
                                picker.clone(),
                            );
                        }
                    }
                } else if key.code == KeyCode::Char('c') || key.code == KeyCode::Char('C') {
                    if let Some(idx) = state.liked_list_state.selected() {
                        let sorted_likes = state.filtered_likes.clone();
                        if let Some(song) = sorted_likes.get(idx)
                            && let Ok(mut clipboard) = arboard::Clipboard::new()
                        {
                            let text = if key.code == KeyCode::Char('c') {
                                format!("{} - {}", song.artist, song.title)
                            } else {
                                format!(
                                    "Title: {}\nArtist: {}\nAlbum: {}\nCircle: {}\nYear: {}",
                                    song.title, song.artist, song.album, song.circle, song.year
                                )
                            };
                            let _ = clipboard.set_text(text);
                            state.status_message = if key.code == KeyCode::Char('c') {
                                "copied track/artist!".to_string()
                            } else {
                                "copied full metadata!".to_string()
                            };
                            state.status_message_time = Some(std::time::Instant::now());
                        }
                    }
                } else if key.code == KeyCode::Char('l') {
                    if let Some(idx) = state.liked_list_state.selected() {
                        let sorted_likes = state.filtered_likes.clone();
                        if let Some(song) = sorted_likes.get(idx) {
                            state.show_unlike_confirm = Some(song.id);
                        }
                    }
                } else if (key.code == KeyCode::Char('o') || key.code == KeyCode::Enter)
                    && let Some(idx) = state.liked_list_state.selected()
                {
                    let sorted_likes = state.filtered_likes.clone();
                    if let Some(song) = sorted_likes.get(idx) {
                        let query = urlencoding::encode(&song.album);
                        let url = format!("https://touhoudb.com/Search?searchType=Album&filter={query}");
                        if webbrowser::open(&url).is_ok() {
                            state.status_message = "opened touhoudb in browser!".to_string();
                        } else {
                            state.status_message = "failed to open browser".to_string();
                        }
                        state.status_message_time = Some(std::time::Instant::now());
                    }
                }
            }
        },
        CEvent::Mouse(mouse) => {
            if mouse.kind == MouseEventKind::Down(MouseButton::Left)
                && let Ok((width, height)) = crossterm::terminal::size()
            {
                let area = Rect::new(0, 0, width, height);
                let hint = help_hint_rect(area, state.config.get_layout_preference());
                if hint.contains(Position::new(mouse.column, mouse.row)) {
                    state.show_help_overlay = !state.show_help_overlay;
                    if !state.show_help_overlay {
                        clear = true;
                    }
                }
            }
        },
        _ => {},
    }

    InputOutcome { should_quit: false, should_clear: clear }
}

fn load_local_art(id: i64, path: std::path::PathBuf, tx: mpsc::Sender<AppEvent>, picker: Picker) {
    tokio::spawn(async move {
        if let Ok(bytes) = tokio::fs::read(&path).await {
            let p_clone = picker.clone();
            let decode_result = tokio::task::spawn_blocking(move || {
                if let Ok(img) = image::load_from_memory(&bytes) {
                    Some(p_clone.new_resize_protocol(img))
                } else {
                    None
                }
            })
            .await
            .unwrap_or(None);

            if let Some(protocol) = decode_result {
                let _ = tx.send(AppEvent::LocalArtLoaded(id, Some(protocol))).await;
                return;
            }
        }
        let _ = tx.send(AppEvent::LocalArtLoaded(id, None)).await;
    });
}