tuisky/components/modals/
embed.rs

1use 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                            // TODO
68                        }
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                        // TODO: Add external
156                    }
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}