mecomp_tui/ui/components/content_view/views/
generic.rs1use 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 pub action_tx: UnboundedSender<Action>,
34 pub props: Option<Props>,
36 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 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 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 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 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 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 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 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 let [info_area, content_area] = Props::split_area(content_area);
183
184 frame.render_widget(state.info_widget(), info_area);
186
187 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 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 let items = state.tree_items().unwrap();
234
235 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}