ankiconnect_rs/client/
cards.rs

1//! Client for Anki card and note operations
2
3use std::sync::Arc;
4
5use crate::builders::{Flag, Query};
6use crate::error::Result;
7use crate::http::{HttpRequestSender, RequestSender};
8use crate::models::{CardId, Deck, Note, NoteId};
9
10use super::request::{
11    self, AddNoteOptions, AddNoteParams, CardsReordering, DuplicateScopeDto, FindCardsParams,
12    GuiBrowseParams, Media, NoteDto,
13};
14
15/// Client for card-related operations
16pub struct CardClient {
17    sender: Arc<HttpRequestSender>,
18}
19
20impl CardClient {
21    /// Creates a new CardClient with the given request sender
22    pub(crate) fn new(sender: Arc<HttpRequestSender>) -> Self {
23        Self { sender }
24    }
25
26    /// Gets the version of the AnkiConnect plugin
27    pub(crate) fn get_version(&self) -> Result<u16> {
28        self.sender.send::<(), u16>("version", None)
29    }
30
31    /// Adds a new note to Anki.
32    ///
33    /// Note that it doesn't check validity of the fields contained in `note` and will fail
34    /// silently if `note` contains fields that are not existent in Anki.
35    ///
36    /// # Arguments
37    ///
38    /// * `deck` - The deck where the note will be added
39    /// * `note` - The note to add
40    /// * `allow_duplicate` - Whether to allow duplicate notes
41    /// * `duplicate_scope` - Optional scope for duplicate checking
42    ///
43    /// # Returns
44    ///
45    /// The ID of the created note
46    pub fn add_note(
47        &self,
48        deck: &Deck,
49        note: Note,
50        allow_duplicate: bool,
51        duplicate_scope: Option<DuplicateScope>,
52    ) -> Result<NoteId> {
53        // TODO: Probably add a validity check for missing fields
54        // Convert the domain note to the API format
55        let note_dto = self.prepare_note_dto(deck, &note, allow_duplicate, duplicate_scope);
56
57        // Send the request to add the note
58        let params = AddNoteParams { note: note_dto };
59        let note_id = self.sender.send("addNote", Some(params))?;
60
61        Ok(NoteId(note_id))
62    }
63
64    /// Finds cards matching the given query
65    ///
66    /// # Arguments
67    ///
68    /// * `query` - The search query for cards
69    ///
70    /// # Returns
71    ///
72    /// A list of card IDs matching the query
73    pub fn find(&self, query: &Query) -> Result<Vec<CardId>> {
74        let params = FindCardsParams {
75            query: query.as_str(),
76        };
77        let ids = self.sender.send::<_, Vec<u64>>("findCards", Some(params))?;
78        Ok(ids.into_iter().map(CardId).collect())
79    }
80
81    /// Opens the Anki card browser with the given query
82    ///
83    /// # Arguments
84    ///
85    /// * `query` - The search query for cards
86    ///
87    /// # Returns
88    ///
89    /// A list of card IDs that were found
90    pub fn browse(&self, query: &str) -> Result<Vec<CardId>> {
91        let params = GuiBrowseParams {
92            query: query.to_string(),
93            reorder_cards: None,
94        };
95        let ids = self.sender.send::<_, Vec<u64>>("guiBrowse", Some(params))?;
96        Ok(ids.into_iter().map(CardId).collect())
97    }
98
99    /// Opens the Anki card browser with the given query and sorts the results
100    ///
101    /// # Arguments
102    ///
103    /// * `query` - The search query for cards
104    /// * `column` - The column to sort by
105    /// * `ascending` - Whether to sort in ascending order
106    ///
107    /// # Returns
108    ///
109    /// A list of card IDs that were found
110    pub fn browse_sorted(
111        &self,
112        query: &str,
113        column: SortColumn,
114        sort_direction: SortDirection,
115    ) -> Result<Vec<CardId>> {
116        let params = GuiBrowseParams {
117            query: query.to_string(),
118            reorder_cards: Some(CardsReordering {
119                order: sort_direction.into(),
120                column_id: column.into(),
121            }),
122        };
123
124        let ids = self.sender.send::<_, Vec<u64>>("guiBrowse", Some(params))?;
125        Ok(ids.into_iter().map(CardId).collect())
126    }
127
128    /// Deletes the specified notes
129    ///
130    /// # Arguments
131    ///
132    /// * `note_ids` - The IDs of the notes to delete
133    pub fn delete_notes(&self, note_ids: &[NoteId]) -> Result<()> {
134        let ids: Vec<u64> = note_ids.iter().map(|id| id.0).collect();
135        let params = request::DeleteNotesParams { notes: ids };
136        self.sender.send::<_, ()>("deleteNotes", Some(params))
137    }
138
139    /// Suspends the specified cards
140    ///
141    /// # Arguments
142    ///
143    /// * `card_ids` - The IDs of the cards to suspend
144    pub fn suspend_cards(&self, card_ids: &[CardId]) -> Result<()> {
145        let ids: Vec<u64> = card_ids.iter().map(|id| id.0).collect();
146        let params = request::CardIdsParams { cards: ids };
147        self.sender.send::<_, ()>("suspend", Some(params))
148    }
149
150    /// Unsuspends the specified cards
151    ///
152    /// # Arguments
153    ///
154    /// * `card_ids` - The IDs of the cards to unsuspend
155    pub fn unsuspend_cards(&self, card_ids: &[CardId]) -> Result<()> {
156        let ids: Vec<u64> = card_ids.iter().map(|id| id.0).collect();
157        let params = request::CardIdsParams { cards: ids };
158        self.sender.send::<_, ()>("unsuspend", Some(params))
159    }
160
161    /// Sets the flag color of the specified cards
162    ///
163    /// # Arguments
164    ///
165    /// * `card_ids` - The IDs of the cards to flag
166    /// * `flag` - The flag color to set (0 = no flag, 1 = red, 2 = orange, etc.)
167    pub fn set_flag(&self, card_ids: &[CardId], flag: Flag) -> Result<()> {
168        let ids: Vec<u64> = card_ids.iter().map(|id| id.0).collect();
169        let params = request::SetFlagParams {
170            cards: ids,
171            flag: flag as u8,
172        };
173
174        self.sender.send::<_, ()>("setFlag", Some(params))
175    }
176
177    /// Gets info about the specified note
178    ///
179    /// # Arguments
180    ///
181    /// * `note_id` - The ID of the note to get info for
182    ///
183    /// # Returns
184    ///
185    /// Detailed information about the note
186    pub fn get_note_info(&self, note_id: NoteId) -> Result<request::NoteInfo> {
187        let params = request::NoteIdParam { note: note_id.0 };
188        self.sender.send("noteInfo", Some(params))
189    }
190
191    /// Converts a domain note to a NoteDto for the API
192    fn prepare_note_dto(
193        &self,
194        deck: &Deck,
195        note: &Note,
196        allow_duplicate: bool,
197        duplicate_scope: Option<DuplicateScope>,
198    ) -> NoteDto {
199        // Prepare media
200        let mut audio = Vec::new();
201        let mut video = Vec::new();
202        let mut picture = Vec::new();
203
204        for field_media in note.media() {
205            let media = Media {
206                path: field_media.media.source().path().map(|p| p.to_path_buf()),
207                url: field_media.media.source().url().map(|u| u.to_string()),
208                data: field_media.media.source().data().map(|d| d.to_string()),
209                filename: field_media.media.filename().to_string(),
210                fields: vec![field_media.field.clone()],
211            };
212
213            match field_media.media.media_type() {
214                crate::models::MediaType::Audio => audio.push(media),
215                crate::models::MediaType::Video => video.push(media),
216                crate::models::MediaType::Image => picture.push(media),
217            }
218        }
219
220        // Configure duplicate handling
221        let duplicate_scope_options = if let Some(_scope) = &duplicate_scope {
222            // TODO: Not implemented yet
223            None
224        } else {
225            None
226        };
227
228        // Create the note DTO
229        NoteDto {
230            deck_name: deck.name().to_string(),
231            model_name: note.model().name().to_string(),
232            fields: note.field_values().clone(),
233            options: AddNoteOptions {
234                allow_duplicate,
235                duplicate_scope: duplicate_scope.map(|ds| ds.into()),
236                duplicate_scope_options,
237            },
238            tags: note.tags().iter().cloned().collect(),
239            audio,
240            video,
241            picture,
242        }
243    }
244}
245
246/// Controls how duplicate notes are detected when adding new notes.
247#[derive(Debug, Clone, Copy, PartialEq, Eq)]
248pub enum DuplicateScope {
249    /// Check for duplicates only within the specified deck
250    Deck,
251
252    /// Check for duplicates across the entire collection
253    Collection,
254}
255
256impl From<DuplicateScope> for DuplicateScopeDto {
257    fn from(value: DuplicateScope) -> Self {
258        match value {
259            DuplicateScope::Deck => Self::Deck,
260            DuplicateScope::Collection => Self::Collection,
261        }
262    }
263}
264
265/// Columns that can be used for sorting in the card browser
266#[derive(Debug, Clone, Copy, PartialEq, Eq)]
267pub enum SortColumn {
268    Answer,
269    CardModified,
270    Cards,
271    Deck,
272    Due,
273    Ease,
274    Lapses,
275    Interval,
276    NoteCreation,
277    NoteMod,
278    NoteType,
279    OriginalPosition,
280    Question,
281    Reps,
282    SortField,
283    Tags,
284    Stability,
285    Difficulty,
286    Retrievability,
287}
288
289impl From<SortColumn> for request::ColumnIdentifier {
290    fn from(value: SortColumn) -> Self {
291        match value {
292            SortColumn::Answer => Self::Answer,
293            SortColumn::CardModified => Self::CardMod,
294            SortColumn::Cards => Self::Cards,
295            SortColumn::Deck => Self::Deck,
296            SortColumn::Due => Self::Due,
297            SortColumn::Ease => Self::Ease,
298            SortColumn::Lapses => Self::Lapses,
299            SortColumn::Interval => Self::Interval,
300            SortColumn::NoteCreation => Self::NoteCreation,
301            SortColumn::NoteMod => Self::NoteMod,
302            SortColumn::NoteType => Self::Notetype,
303            SortColumn::OriginalPosition => Self::OriginalPosition,
304            SortColumn::Question => Self::Question,
305            SortColumn::Reps => Self::Reps,
306            SortColumn::SortField => Self::SortField,
307            SortColumn::Tags => Self::Tags,
308            SortColumn::Stability => Self::Stability,
309            SortColumn::Difficulty => Self::Difficulty,
310            SortColumn::Retrievability => Self::Retrievability,
311        }
312    }
313}
314
315/// Sort direction
316#[derive(Debug, Clone, Copy, PartialEq, Eq)]
317pub enum SortDirection {
318    Ascending,
319    Descending,
320}
321
322impl From<SortDirection> for request::SortOrder {
323    fn from(value: SortDirection) -> Self {
324        match value {
325            SortDirection::Ascending => Self::Ascending,
326            SortDirection::Descending => Self::Descending,
327        }
328    }
329}