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 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 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, 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 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 let [info_area, content_area] = Props::split_area(content_area);
179
180 frame.render_widget(state.info_widget(), info_area);
182
183 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 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 let items = state.tree_items().unwrap();
241
242 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}