tuisky/components/modals/
embed.rs1use super::super::views::types::Action as ViewsAction;
2use super::embed_images::EmbedImagesModalComponent;
3use super::embed_record::EmbedRecordModalComponent;
4use super::types::{Data, EmbedData, ImageData};
5use super::{Action, ModalComponent};
6use bsky_sdk::api::com::atproto::repo::strong_ref;
7use color_eyre::Result;
8use crossterm::event::KeyEvent;
9use ratatui::Frame;
10use ratatui::layout::{Constraint, Layout, Margin, Rect};
11use ratatui::style::{Color, Style, Stylize};
12use ratatui::text::{Line, Text};
13use ratatui::widgets::{Block, BorderType, Clear, List, ListState, Padding};
14use tokio::sync::mpsc::UnboundedSender;
15
16pub struct EmbedModalComponent {
17 action_tx: UnboundedSender<ViewsAction>,
18 embeds_state: ListState,
19 actions_state: ListState,
20 record: Option<strong_ref::Main>,
21 images: Vec<ImageData>,
22 child: Option<Box<dyn ModalComponent>>,
23}
24
25impl EmbedModalComponent {
26 pub fn new(action_tx: UnboundedSender<ViewsAction>, init: Option<EmbedData>) -> Self {
27 let (record, images) = if let Some(data) = init {
28 (data.record, data.images)
29 } else {
30 (None, Vec::new())
31 };
32 Self {
33 action_tx,
34 embeds_state: Default::default(),
35 actions_state: Default::default(),
36 record,
37 images,
38 child: None,
39 }
40 }
41}
42
43impl ModalComponent for EmbedModalComponent {
44 fn handle_key_events(&mut self, key: KeyEvent) -> Result<Option<Action>> {
45 if let Some(child) = self.child.as_mut() {
46 child.handle_key_events(key)
47 } else {
48 Ok(None)
49 }
50 }
51 fn update(&mut self, action: ViewsAction) -> Result<Option<Action>> {
52 if let Some(child) = self.child.as_mut() {
53 return Ok(match child.update(action)? {
54 Some(Action::Ok(data)) => {
55 match *data {
56 Data::Image((image, index)) => {
57 if let Some(i) = index {
58 self.images[i] = image;
59 } else {
60 self.images.push(image)
61 }
62 }
63 Data::Record(strong_ref) => {
64 self.record = Some(strong_ref);
65 }
66 _ => {
67 }
69 }
70 self.child = None;
71 Some(Action::Render)
72 }
73 Some(Action::Delete(index)) => {
74 match index {
75 Some(i) => {
76 self.images.remove(i);
77 }
78 None => self.record = None,
79 }
80 self.child = None;
81 self.embeds_state.select(None);
82 Some(Action::Render)
83 }
84 Some(Action::Cancel) => {
85 self.child = None;
86 Some(Action::Render)
87 }
88 action => action,
89 });
90 }
91 Ok(match action {
92 ViewsAction::NextItem => {
93 match (self.embeds_state.selected(), self.actions_state.selected()) {
94 (Some(i), None) => {
95 if i == usize::from(self.record.is_some()) + self.images.len() - 1 {
96 self.embeds_state.select(None);
97 self.actions_state.select_first();
98 } else {
99 self.embeds_state.select_next();
100 }
101 }
102 (None, Some(i)) => {
103 self.actions_state.select(Some((i + 1).min(3)));
104 }
105 _ => {
106 self.actions_state.select_first();
107 }
108 }
109 Some(Action::Render)
110 }
111 ViewsAction::PrevItem => {
112 match (self.embeds_state.selected(), self.actions_state.selected()) {
113 (Some(i), None) => {
114 self.embeds_state.select(Some(i.max(1) - 1));
115 }
116 (None, Some(0)) => {
117 if usize::from(self.record.is_some()) + self.images.len() > 0 {
118 self.actions_state.select(None);
119 self.embeds_state.select_last();
120 }
121 }
122 (None, Some(i)) => {
123 self.actions_state.select(Some(i - 1));
124 }
125 _ => {
126 self.actions_state.select_last();
127 }
128 }
129 Some(Action::Render)
130 }
131 ViewsAction::Enter => {
132 match self.embeds_state.selected() {
133 Some(0) if self.record.is_some() => {
134 if let Some(record) = &self.record {
135 self.child = Some(Box::new(EmbedRecordModalComponent::new(
136 self.action_tx.clone(),
137 Some(record.uri.clone()),
138 )));
139 }
140 }
141 Some(i) => {
142 let i = i - usize::from(self.record.is_some());
143 self.child = Some(Box::new(EmbedImagesModalComponent::new(Some((
144 i,
145 self.images[i].clone(),
146 )))));
147 }
148 None => {}
149 }
150 match self.actions_state.selected() {
151 Some(0) if self.images.len() < 4 => {
152 self.child = Some(Box::new(EmbedImagesModalComponent::new(None)));
153 }
154 Some(1) => {
155 }
157 Some(2) => {
158 self.child = Some(Box::new(EmbedRecordModalComponent::new(
159 self.action_tx.clone(),
160 None,
161 )));
162 }
163 Some(3) => {
164 return Ok(Some(Action::Ok(Box::new(Data::Embed(EmbedData {
165 images: self.images.clone(),
166 record: self.record.clone(),
167 })))));
168 }
169 _ => {}
170 }
171 Some(Action::Render)
172 }
173 ViewsAction::Back => Some(Action::Cancel),
174 _ => None,
175 })
176 }
177 fn draw(&mut self, f: &mut Frame<'_>, area: Rect) -> Result<()> {
178 let area = area.inner(Margin {
179 horizontal: 2,
180 vertical: 4,
181 });
182 let [area] = Layout::vertical([Constraint::Max(2 + 2 * 4 + 2 + 3 + 1 + 2)]).areas(area);
183
184 let block = Block::bordered().title("Embed");
185 let inner = block.inner(area);
186 f.render_widget(Clear, area);
187 f.render_widget(block, area);
188
189 let [embeds, actions] = Layout::vertical([
190 Constraint::Length(
191 2 + 2 * u16::from(self.record.is_some()) + 2 * self.images.len() as u16,
192 ),
193 Constraint::Length(4),
194 ])
195 .areas(inner);
196
197 let mut embed_items = Vec::new();
198 if let Some(record) = &self.record {
199 embed_items.push(Text::from(vec![
200 Line::from(format!("record: {}", record.uri)),
201 Line::from(format!(" {}", record.cid.as_ref())).dim(),
202 ]));
203 }
204 for (i, image) in self.images.iter().enumerate() {
205 embed_items.push(Text::from(vec![
206 Line::from(format!("image{}: {}", i + 1, image.path)),
207 Line::from(format!(" {}", image.alt)).dim(),
208 ]));
209 }
210 f.render_stateful_widget(
211 List::new(embed_items)
212 .block(
213 Block::bordered()
214 .border_type(BorderType::Rounded)
215 .border_style(Color::Yellow),
216 )
217 .highlight_style(Style::reset().reversed()),
218 embeds,
219 &mut self.embeds_state,
220 );
221 f.render_stateful_widget(
222 List::new([
223 Line::from("Add images"),
224 Line::from("Add external").dim(),
225 Line::from("Add record"),
226 Line::from("OK").centered().blue(),
227 ])
228 .block(Block::default().padding(Padding::horizontal(1)))
229 .highlight_style(Style::default().reversed()),
230 actions,
231 &mut self.actions_state,
232 );
233 if let Some(child) = self.child.as_mut() {
234 child.draw(f, area)?;
235 }
236 Ok(())
237 }
238}