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        AppState,
16        colors::{TEXT_HIGHLIGHT, TEXT_NORMAL, border_color},
17        components::{Component, ComponentRender, RenderProps},
18        widgets::tree::{CheckTree, state::CheckTreeState},
19    },
20};
21
22use super::{
23    ItemViewProps,
24    checktree_utils::{
25        construct_add_to_playlist_action, construct_add_to_queue_action,
26        construct_start_radio_action,
27    },
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, false);
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 = Style::default().fg(border_color(props.is_focused).into());
167
168        // draw borders and get area for content
169        let area = if let Some(state) = &self.props {
170            let border = Block::bordered()
171                .title_top(Props::title())
172                .title_bottom(" \u{23CE} : Open | ←/↑/↓/→: Navigate | \u{2423} Check")
173                .border_style(border_style);
174            let content_area = border.inner(props.area);
175            frame.render_widget(border, props.area);
176
177            // split area to make room for item info
178            let [info_area, content_area] = Props::split_area(content_area);
179
180            // render item info
181            frame.render_widget(state.info_widget(), info_area);
182
183            // draw an additional border around the content area to display additional instructions
184            let border = Block::default()
185                .borders(Borders::TOP)
186                .title_top("q: add to queue | r: start radio | p: add to playlist")
187                .border_style(border_style);
188            frame.render_widget(&border, content_area);
189            let content_area = border.inner(content_area);
190
191            // draw an additional border around the content area to indicate whether operations will be performed on the entire item, or just the checked items
192            let border = Block::default()
193                .borders(Borders::TOP)
194                .title_top(Line::from(vec![
195                    Span::raw("Performing operations on "),
196                    Span::raw(
197                        if self
198                            .tree_state
199                            .lock()
200                            .unwrap()
201                            .get_checked_things()
202                            .is_empty()
203                        {
204                            Props::none_checked_string()
205                        } else {
206                            "checked items"
207                        },
208                    )
209                    .fg(*TEXT_HIGHLIGHT),
210                ]))
211                .italic()
212                .border_style(border_style);
213            frame.render_widget(&border, content_area);
214            border.inner(content_area)
215        } else {
216            let border = Block::bordered()
217                .title_top(Props::title())
218                .border_style(border_style);
219            frame.render_widget(&border, props.area);
220            border.inner(props.area)
221        };
222
223        RenderProps { area, ..props }
224    }
225
226    fn render_content(&self, frame: &mut ratatui::Frame, props: RenderProps) {
227        let Some(state) = &self.props else {
228            let text = format!("No active {}", Props::name());
229
230            frame.render_widget(
231                Line::from(text)
232                    .style(Style::default().fg((*TEXT_NORMAL).into()))
233                    .alignment(Alignment::Center),
234                props.area,
235            );
236            return;
237        };
238
239        // create a tree to hold the items children
240        let items = state.tree_items().unwrap();
241
242        // render the tree
243        frame.render_stateful_widget(
244            CheckTree::new(&items)
245                .unwrap()
246                .highlight_style(Style::default().fg((*TEXT_HIGHLIGHT).into()).bold()),
247            props.area,
248            &mut self.tree_state.lock().unwrap(),
249        );
250    }
251}