1use chrono::{DateTime, Utc};
3#[cfg(feature = "server")]
4use rmcp::schemars;
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
8#[cfg_attr(feature = "server", derive(rmcp::schemars::JsonSchema))]
9#[serde(rename_all = "snake_case")]
10pub enum NoteKind {
11 #[default]
12 Moment,
13 Topic,
14}
15
16impl NoteKind {
17 pub fn as_str(self) -> &'static str {
18 match self {
19 Self::Moment => "moment",
20 Self::Topic => "topic",
21 }
22 }
23 pub fn label(self) -> &'static str {
24 match self {
25 Self::Moment => "随记",
26 Self::Topic => "主题笔记",
27 }
28 }
29}
30
31#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
32#[cfg_attr(feature = "server", derive(rmcp::schemars::JsonSchema))]
33pub struct NoteDraft {
34 pub id: Option<i32>,
35 pub expected_version: Option<i32>,
37 #[serde(default)]
38 pub kind: NoteKind,
39 #[serde(default)]
40 pub title: String,
41 pub content_md: String,
42 #[serde(default)]
43 pub tags: Vec<String>,
44 #[serde(default)]
45 pub notebook_ids: Vec<i32>,
46}
47
48#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
49pub struct Note {
50 pub id: i32,
51 pub slug: String,
52 pub version: i32,
53 pub kind: NoteKind,
54 pub title: String,
55 #[serde(default)]
62 pub content_md: String,
63 pub content_html: String,
64 pub toc_html: String,
65 pub summary: String,
66 pub tags: Vec<String>,
67 #[cfg_attr(
68 target_arch = "wasm32",
69 serde(deserialize_with = "crate::models::rfc3339::deserialize")
70 )]
71 pub created_at: DateTime<Utc>,
72 #[cfg_attr(
73 target_arch = "wasm32",
74 serde(deserialize_with = "crate::models::rfc3339::deserialize")
75 )]
76 pub updated_at: DateTime<Utc>,
77 #[cfg_attr(
78 target_arch = "wasm32",
79 serde(
80 default,
81 deserialize_with = "crate::models::rfc3339::deserialize_option"
82 )
83 )]
84 pub published_at: Option<DateTime<Utc>>,
85 #[serde(default)]
87 pub published_version: Option<i32>,
88 #[serde(default)]
89 pub knowledge_version: Option<i32>,
90 #[serde(default)]
91 #[cfg_attr(
92 target_arch = "wasm32",
93 serde(deserialize_with = "crate::models::rfc3339::deserialize_option")
94 )]
95 pub deleted_at: Option<DateTime<Utc>>,
96 #[serde(default)]
97 pub notebook_ids: Vec<i32>,
98 #[serde(default)]
100 pub sections: u32,
101}
102
103pub fn display_title(kind: NoteKind, title: &str, summary: &str) -> String {
105 if title.trim().is_empty() {
106 if summary.trim().is_empty() {
107 kind.label().to_string()
108 } else {
109 summary.chars().take(48).collect()
110 }
111 } else {
112 title.to_string()
113 }
114}
115
116impl Note {
117 pub fn display_title(&self) -> String {
118 display_title(self.kind, &self.title, &self.summary)
119 }
120 pub fn draft(&self) -> NoteDraft {
121 NoteDraft {
122 id: Some(self.id),
123 expected_version: Some(self.version),
124 kind: self.kind,
125 title: self.title.clone(),
126 content_md: self.content_md.clone(),
127 tags: self.tags.clone(),
128 notebook_ids: self.notebook_ids.clone(),
129 }
130 }
131}
132
133#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
134pub struct NotePage {
135 pub notes: Vec<Note>,
136 pub total: i64,
137}
138
139#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
140#[cfg_attr(feature = "server", derive(rmcp::schemars::JsonSchema))]
141pub struct NoteFilter {
142 #[serde(default)]
143 pub query: String,
144 pub kind: Option<NoteKind>,
145 pub notebook_id: Option<i32>,
146 #[serde(default)]
147 pub tag: String,
148 #[serde(default)]
149 pub trash: bool,
150 pub published: Option<bool>,
152 pub knowledge: Option<bool>,
153 #[serde(default)]
154 pub page: i32,
155}
156
157#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
158pub struct Notebook {
159 pub id: i32,
160 pub title: String,
161 pub description: String,
162 pub is_public: bool,
163 #[cfg_attr(
164 target_arch = "wasm32",
165 serde(
166 default,
167 deserialize_with = "crate::models::rfc3339::deserialize_option"
168 )
169 )]
170 pub archived_at: Option<DateTime<Utc>>,
171 pub note_count: i64,
172 #[cfg_attr(
173 target_arch = "wasm32",
174 serde(deserialize_with = "crate::models::rfc3339::deserialize")
175 )]
176 pub updated_at: DateTime<Utc>,
177}
178
179#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
181pub struct NoteStats {
182 pub moments: i64,
183 pub topics: i64,
184 pub tags: Vec<(String, i64)>,
186}
187
188impl NoteStats {
189 pub fn total(&self) -> i64 {
190 self.moments + self.topics
191 }
192}
193
194#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
196pub struct NoteLink {
197 pub slug: String,
198 pub title: String,
199}
200
201#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
203pub struct NoteContext {
204 pub notebook: Notebook,
205 pub position: usize,
206 pub total: usize,
207 pub prev: Option<NoteLink>,
208 pub next: Option<NoteLink>,
209 pub siblings: Vec<NoteLink>,
210}
211
212#[cfg(any(feature = "server", test))]
213impl NoteContext {
214 pub fn locate(notebook: Notebook, siblings: Vec<NoteLink>, slug: &str) -> Option<Self> {
216 let index = siblings.iter().position(|s| s.slug == slug)?;
217 Some(Self {
218 notebook,
219 position: index + 1,
220 total: siblings.len(),
221 prev: index.checked_sub(1).map(|i| siblings[i].clone()),
222 next: siblings.get(index + 1).cloned(),
223 siblings,
224 })
225 }
226}
227
228#[cfg(any(feature = "server", test))]
230pub fn choose_notebook(candidates: &[i32], requested: Option<i32>) -> Option<i32> {
231 match requested {
232 Some(id) if candidates.contains(&id) => Some(id),
233 _ if candidates.len() == 1 => Some(candidates[0]),
234 _ => None,
235 }
236}
237
238#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
240pub struct NotebookEntry {
241 pub id: i32,
242 pub title: String,
243 pub published: bool,
245 pub ahead: bool,
247 pub trashed: bool,
249}
250
251impl NotebookEntry {
252 pub fn state_label(&self) -> &'static str {
253 match (self.trashed, self.published, self.ahead) {
254 (true, _, _) => "在回收站 · 访客不可见",
255 (false, true, true) => "已发布 · 有未发布修改",
256 (false, true, false) => "已发布",
257 (false, false, _) => "私密草稿 · 访客不可见",
258 }
259 }
260}
261
262#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
263pub struct NotebookInput {
264 pub id: Option<i32>,
265 pub title: String,
266 pub description: String,
267 pub is_public: bool,
268}
269
270#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
271pub struct NoteRevision {
272 pub version: i32,
273 pub title: String,
274 #[cfg_attr(
275 target_arch = "wasm32",
276 serde(deserialize_with = "crate::models::rfc3339::deserialize")
277 )]
278 pub created_at: DateTime<Utc>,
279}
280
281#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
282pub struct NoteBatchTarget {
283 pub id: i32,
284 pub expected_version: i32,
285}
286
287#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
288#[serde(rename_all = "snake_case")]
289pub enum NoteAction {
290 Publish,
291 Unpublish,
292 IncludeKnowledge,
293 ExcludeKnowledge,
294 Trash,
295 Restore,
296}
297
298#[cfg(test)]
299mod tests {
300 use super::*;
301
302 fn link(slug: &str) -> NoteLink {
303 NoteLink {
304 slug: slug.into(),
305 title: slug.to_uppercase(),
306 }
307 }
308
309 #[test]
310 fn notebook_entry_label_explains_visitor_visibility() {
311 let entry = |published, ahead, trashed| NotebookEntry {
312 id: 1,
313 title: String::new(),
314 published,
315 ahead,
316 trashed,
317 };
318 assert_eq!(
319 entry(true, true, false).state_label(),
320 "已发布 · 有未发布修改"
321 );
322 assert_eq!(entry(true, false, false).state_label(), "已发布");
323 assert_eq!(
324 entry(false, false, false).state_label(),
325 "私密草稿 · 访客不可见"
326 );
327 assert_eq!(
328 entry(true, false, true).state_label(),
329 "在回收站 · 访客不可见"
330 );
331 }
332
333 #[test]
334 fn requested_notebook_wins_only_when_it_is_a_public_candidate() {
335 assert_eq!(choose_notebook(&[3, 7], Some(7)), Some(7));
336 assert_eq!(choose_notebook(&[3, 7], None), None);
338 assert_eq!(choose_notebook(&[3], Some(9)), Some(3));
340 assert_eq!(choose_notebook(&[3, 7], Some(9)), None);
341 assert_eq!(choose_notebook(&[], Some(3)), None);
342 }
343
344 #[test]
345 fn locate_gives_neighbours_and_leaves_the_ends_empty() {
346 let siblings = vec![link("a"), link("b"), link("c")];
347 let first = NoteContext::locate(Notebook::default(), siblings.clone(), "a").unwrap();
348 assert_eq!((first.position, first.total), (1, 3));
349 assert_eq!(first.prev, None);
350 assert_eq!(first.next, Some(link("b")));
351 let last = NoteContext::locate(Notebook::default(), siblings.clone(), "c").unwrap();
352 assert_eq!(last.prev, Some(link("b")));
353 assert_eq!(last.next, None);
354 assert!(NoteContext::locate(Notebook::default(), siblings, "draft").is_none());
355 }
356}