lib/models/
annotation.rs

1//! Defines the [`Annotation`] struct.
2
3use std::cmp::Ordering;
4use std::collections::BTreeSet;
5
6use rusqlite::Row;
7use serde::Serialize;
8
9use crate::applebooks::ios::models::AnnotationRaw;
10use crate::applebooks::macos::ABQuery;
11
12use super::datetime::DateTimeUtc;
13use super::epubcfi;
14
15/// A struct representing an annotation and its metadata.
16#[derive(Debug, Default, Clone, Eq, Serialize)]
17pub struct Annotation {
18    /// The body of the annotation.
19    pub body: String,
20
21    /// The annotation's highlight style.
22    pub style: AnnotationStyle,
23
24    /// The annotation's notes.
25    pub notes: String,
26
27    /// The annotation's `#tags`.
28    pub tags: BTreeSet<String>,
29
30    /// The annotation's metadata.
31    pub metadata: AnnotationMetadata,
32}
33
34// For macOS.
35impl ABQuery for Annotation {
36    const QUERY: &'static str = {
37        "SELECT
38            ZANNOTATIONSELECTEDTEXT,           -- 0 body
39            ZANNOTATIONNOTE,                   -- 1 notes
40            ZANNOTATIONSTYLE,                  -- 2 style
41            ZANNOTATIONUUID,                   -- 3 id
42            ZAEANNOTATION.ZANNOTATIONASSETID,  -- 4 book_id
43            ZANNOTATIONCREATIONDATE,           -- 5 created
44            ZANNOTATIONMODIFICATIONDATE,       -- 6 modified
45            ZANNOTATIONLOCATION                -- 7 location
46        FROM ZAEANNOTATION
47        WHERE ZANNOTATIONSELECTEDTEXT IS NOT NULL
48            AND ZANNOTATIONDELETED = 0
49        ORDER BY ZANNOTATIONASSETID;"
50    };
51
52    fn from_row(row: &Row<'_>) -> Self {
53        let notes: Option<String> = row.get_unwrap(1);
54        let style: u8 = row.get_unwrap(2);
55        let created: f64 = row.get_unwrap(5);
56        let modified: f64 = row.get_unwrap(6);
57        let epubcfi: String = row.get_unwrap(7);
58
59        Self {
60            body: row.get_unwrap(0),
61            style: AnnotationStyle::from(style as usize),
62            notes: notes.unwrap_or_default(),
63            tags: BTreeSet::new(),
64            metadata: AnnotationMetadata {
65                id: row.get_unwrap(3),
66                book_id: row.get_unwrap(4),
67                created: DateTimeUtc::from(created),
68                modified: DateTimeUtc::from(modified),
69                location: epubcfi::parse(&epubcfi),
70                epubcfi,
71            },
72        }
73    }
74}
75
76// For iOS.
77impl From<AnnotationRaw> for Annotation {
78    fn from(annotation: AnnotationRaw) -> Self {
79        Self {
80            body: annotation.body,
81            style: AnnotationStyle::from(annotation.style),
82            notes: annotation.notes.unwrap_or_default(),
83            tags: BTreeSet::new(),
84            metadata: AnnotationMetadata {
85                id: annotation.id,
86                book_id: annotation.book_id,
87                created: DateTimeUtc::from(annotation.created),
88                modified: DateTimeUtc::from(annotation.modified),
89                location: epubcfi::parse(&annotation.epubcfi),
90                epubcfi: annotation.epubcfi,
91            },
92        }
93    }
94}
95
96impl Ord for Annotation {
97    fn cmp(&self, other: &Self) -> Ordering {
98        self.metadata.cmp(&other.metadata)
99    }
100}
101
102impl PartialOrd for Annotation {
103    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
104        Some(self.metadata.cmp(&other.metadata))
105    }
106}
107
108impl PartialEq for Annotation {
109    fn eq(&self, other: &Self) -> bool {
110        self.metadata == other.metadata
111    }
112}
113
114/// A struct representing an annotation's metadata.
115///
116/// This is all the data that is not directly editable by the user.
117#[derive(Debug, Default, Clone, Eq, Serialize)]
118pub struct AnnotationMetadata {
119    /// The annotation's unique id.
120    pub id: String,
121
122    /// The book id this annotation belongs to.
123    pub book_id: String,
124
125    /// The date the annotation was created.
126    pub created: DateTimeUtc,
127
128    /// The date the annotation was last modified.
129    pub modified: DateTimeUtc,
130
131    /// A location string used for sorting annotations into their order of appearance inside their
132    /// respective book. This string is generated from the annotation's `epubcfi`.
133    pub location: String,
134
135    /// The annotation's raw `epubcfi`.
136    pub epubcfi: String,
137}
138
139impl Ord for AnnotationMetadata {
140    fn cmp(&self, other: &Self) -> Ordering {
141        self.location.cmp(&other.location)
142    }
143}
144
145impl PartialOrd for AnnotationMetadata {
146    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
147        Some(self.location.cmp(&other.location))
148    }
149}
150
151impl PartialEq for AnnotationMetadata {
152    fn eq(&self, other: &Self) -> bool {
153        self.location == other.location
154    }
155}
156
157/// An enum represening all possible annotation highlight styles.
158#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize)]
159#[serde(rename_all = "lowercase")]
160pub enum AnnotationStyle {
161    #[default]
162    #[allow(missing_docs)]
163    None,
164    #[allow(missing_docs)]
165    Underline,
166    #[allow(missing_docs)]
167    Green,
168    #[allow(missing_docs)]
169    Blue,
170    #[allow(missing_docs)]
171    Yellow,
172    #[allow(missing_docs)]
173    Red,
174    #[allow(missing_docs)]
175    Purple,
176}
177
178impl From<usize> for AnnotationStyle {
179    fn from(value: usize) -> Self {
180        match value {
181            0 => Self::Underline,
182            1 => Self::Green,
183            2 => Self::Blue,
184            3 => Self::Yellow,
185            4 => Self::Red,
186            5 => Self::Purple,
187            _ => Self::None,
188        }
189    }
190}
191
192#[cfg(test)]
193mod test {
194
195    use super::*;
196
197    // Tests that annotation ordering is properly evaluated from an `epubcfi` string.
198    // TODO: Base function to start testing annotation order using `<` and `>`.
199    #[test]
200    fn cmp_annotations() {
201        let mut a1 = Annotation::default();
202        a1.metadata.location = epubcfi::parse("epubcfi(/6/10[c01]!/4/10/3,:335,:749)");
203
204        let mut a2 = Annotation::default();
205        a2.metadata.location = epubcfi::parse("epubcfi(/6/12[c02]!/4/26/3,:68,:493)");
206
207        assert!(a1 < a2);
208    }
209}