1use 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#[derive(Debug, Default, Clone, Eq, Serialize)]
17pub struct Annotation {
18 pub body: String,
20
21 pub style: AnnotationStyle,
23
24 pub notes: String,
26
27 pub tags: BTreeSet<String>,
29
30 pub metadata: AnnotationMetadata,
32}
33
34impl 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
76impl 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#[derive(Debug, Default, Clone, Eq, Serialize)]
118pub struct AnnotationMetadata {
119 pub id: String,
121
122 pub book_id: String,
124
125 pub created: DateTimeUtc,
127
128 pub modified: DateTimeUtc,
130
131 pub location: String,
134
135 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#[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 #[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}