mecomp_tui/ui/components/content_view/views/
generic.rs

1use std::sync::Mutex;
2
3use crossterm::event::{KeyCode, KeyEvent, MouseEvent};
4use ratatui::{
5    layout::{Alignment, Margin, Rect},
6    style::{Style, Stylize},
7    text::{Line, Span},
8    widgets::{Block, Borders},
9};
10use tokio::sync::mpsc::UnboundedSender;
11
12use crate::{
13    state::action::{Action, ViewAction},
14    ui::{
15        colors::{BORDER_FOCUSED, BORDER_UNFOCUSED, TEXT_HIGHLIGHT, TEXT_NORMAL},
16        components::{Component, ComponentRender, RenderProps},
17        widgets::tree::{state::CheckTreeState, CheckTree},
18        AppState,
19    },
20};
21
22use super::{
23    checktree_utils::{
24        construct_add_to_playlist_action, construct_add_to_queue_action,
25        construct_start_radio_action,
26    },
27    ItemViewProps,
28};
29
30#[derive(Debug)]
31pub struct ItemView<Props> {
32    /// Action Sender
33    pub action_tx: UnboundedSender<Action>,
34    /// Mapped Props from state
35    pub props: Option<Props>,
36    /// tree state
37    pub tree_state: Mutex<CheckTreeState<String>>,
38}
39
40impl<Props> Component for ItemView<Props>
41where
42    Props: ItemViewProps,
43{
44    fn new(state: &AppState, action_tx: UnboundedSender<Action>) -> Self
45    where
46        Self: Sized,
47    {
48        let props = Props::retrieve(&state.additional_view_data);
49        let tree_state = Mutex::new(CheckTreeState::default());
50        Self {
51            action_tx,
52            props,
53            tree_state,
54        }
55    }
56
57    fn move_with_state(self, state: &AppState) -> Self
58    where
59        Self: Sized,
60    {
61        if let Some(props) = Props::retrieve(&state.additional_view_data) {
62            Self {
63                props: Some(props),
64                tree_state: Mutex::new(CheckTreeState::default()),
65                ..self
66            }
67        } else {
68            self
69        }
70    }
71
72    fn name(&self) -> &str {
73        Props::title()
74    }
75
76    fn handle_key_event(&mut self, key: KeyEvent) {
77        match key.code {
78            // arrow keys
79            KeyCode::Up => {
80                self.tree_state.lock().unwrap().key_up();
81            }
82            KeyCode::Down => {
83                self.tree_state.lock().unwrap().key_down();
84            }
85            KeyCode::Left => {
86                self.tree_state.lock().unwrap().key_left();
87            }
88            KeyCode::Right => {
89                self.tree_state.lock().unwrap().key_right();
90            }
91            KeyCode::Char(' ') => {
92                self.tree_state.lock().unwrap().key_space();
93            }
94            // Enter key opens selected view
95            KeyCode::Enter => {
96                if self.tree_state.lock().unwrap().toggle_selected() {
97                    let things = self.tree_state.lock().unwrap().get_selected_thing();
98
99                    if let Some(thing) = things {
100                        self.action_tx
101                            .send(Action::ActiveView(ViewAction::Set(thing.into())))
102                            .unwrap();
103                    }
104                }
105            }
106            // if there are checked items, add them to the queue, otherwise send the song to the queue
107            KeyCode::Char('q') => {
108                let checked_things = self.tree_state.lock().unwrap().get_checked_things();
109                if let Some(action) = construct_add_to_queue_action(
110                    checked_things,
111                    self.props.as_ref().map(super::ItemViewProps::id),
112                ) {
113                    self.action_tx.send(action).unwrap();
114                }
115            }
116            // if there are checked items, start radio from checked items, otherwise start radio from song
117            KeyCode::Char('r') => {
118                let checked_things = self.tree_state.lock().unwrap().get_checked_things();
119                if let Some(action) = construct_start_radio_action(
120                    checked_things,
121                    self.props.as_ref().map(super::ItemViewProps::id),
122                ) {
123                    self.action_tx.send(action).unwrap();
124                }
125            }
126            // if there are checked items, add them to playlist, otherwise add the song to playlist
127            KeyCode::Char('p') => {
128                let checked_things = self.tree_state.lock().unwrap().get_checked_things();
129                if let Some(action) = construct_add_to_playlist_action(
130                    checked_things,
131                    self.props.as_ref().map(super::ItemViewProps::id),
132                ) {
133                    self.action_tx.send(action).unwrap();
134                }
135            }
136            _ => {}
137        }
138    }
139
140    fn handle_mouse_event(&mut self, mouse: MouseEvent, area: Rect) {
141        // adjust the area to account for the border
142        let area = area.inner(Margin::new(1, 1));
143        let [_, content_area] = Props::split_area(area);
144        let content_area = Rect {
145            y: content_area.y + 2,
146            height: content_area.height - 2,
147            ..content_area
148        };
149
150        let result = self
151            .tree_state
152            .lock()
153            .unwrap()
154            .handle_mouse_event(mouse, content_area);
155        if let Some(action) = result {
156            self.action_tx.send(action).unwrap();
157        }
158    }
159}
160
161impl<Props> ComponentRender<RenderProps> for ItemView<Props>
162where
163    Props: ItemViewProps,
164{
165    fn render_border(&self, frame: &mut ratatui::Frame, props: RenderProps) -> RenderProps {
166        let border_style = if props.is_focused {
167            Style::default().fg(BORDER_FOCUSED.into())
168        } else {
169            Style::default().fg(BORDER_UNFOCUSED.into())
170        };
171
172        // draw borders and get area for content
173        let area = if let Some(state) = &self.props {
174            let border = Block::bordered()
175                .title_top(Props::title())
176                .title_bottom(" \u{23CE} : Open | ←/↑/↓/→: Navigate | \u{2423} Check")
177                .border_style(border_style);
178            let content_area = border.inner(props.area);
179            frame.render_widget(border, props.area);
180
181            // split area to make room for item info
182            let [info_area, content_area] = Props::split_area(content_area);
183
184            // render item info
185            frame.render_widget(state.info_widget(), info_area);
186
187            // draw an additional border around the content area to display additional instructions
188            let border = Block::default()
189                .borders(Borders::TOP)
190                .title_top("q: add to queue | r: start radio | p: add to playlist")
191                .border_style(border_style);
192            frame.render_widget(&border, content_area);
193            let content_area = border.inner(content_area);
194
195            // draw an additional border around the content area to indicate whether operations will be performed on the entire item, or just the checked items
196            let border = Block::default()
197                .borders(Borders::TOP)
198                .title_top(Line::from(vec![
199                    Span::raw("Performing operations on "),
200                    Span::raw(
201                        if self
202                            .tree_state
203                            .lock()
204                            .unwrap()
205                            .get_checked_things()
206                            .is_empty()
207                        {
208                            Props::none_checked_string()
209                        } else {
210                            "checked items"
211                        },
212                    )
213                    .fg(TEXT_HIGHLIGHT),
214                ]))
215                .italic()
216                .border_style(border_style);
217            frame.render_widget(&border, content_area);
218            border.inner(content_area)
219        } else {
220            let border = Block::bordered()
221                .title_top(Props::title())
222                .border_style(border_style);
223            frame.render_widget(&border, props.area);
224            border.inner(props.area)
225        };
226
227        RenderProps { area, ..props }
228    }
229
230    fn render_content(&self, frame: &mut ratatui::Frame, props: RenderProps) {
231        if let Some(state) = &self.props {
232            // create a tree to hold the items children
233            let items = state.tree_items().unwrap();
234
235            // render the tree
236            frame.render_stateful_widget(
237                CheckTree::new(&items)
238                    .unwrap()
239                    .highlight_style(Style::default().fg(TEXT_HIGHLIGHT.into()).bold()),
240                props.area,
241                &mut self.tree_state.lock().unwrap(),
242            );
243        } else {
244            let text = format!("No active {}", Props::name());
245
246            frame.render_widget(
247                Line::from(text)
248                    .style(Style::default().fg(TEXT_NORMAL.into()))
249                    .alignment(Alignment::Center),
250                props.area,
251            );
252        }
253    }
254}