1//! Recording through the editor, as Snowbound's Record Audio stores it: the ops the editor
2//! records seal into a section that reads back as the editor's page, with the line saying
3//! when, the notes written meanwhile linked to their moments, and the file where recording
4//! started.
5
6use canvas::{editor::CanvasEditor, layout::TextEngine};
7use draw::edit::Movement;
8use onestore::{
9 Arena, Section,
10 op::{Edit, Op},
11 page::{Attachment, MediaIndex, Page, PageObject, ParagraphContent, Recording},
12};
13
14fn body(page: &Page) -> Vec<(String, MediaIndex)> {
15 page.objects
16 .iter()
17 .find_map(|object| match object {
18 PageObject::Outline(outline) if !outline.title => Some(outline),
19 _ => None,
20 })
21 .unwrap()
22 .paragraphs
23 .iter()
24 .map(|paragraph| {
25 let shown = match &paragraph.content {
26 ParagraphContent::Text(text) => text.text.text().to_owned(),
27 ParagraphContent::Attachment(file) => format!("[{}]", file.filename),
28 _ => panic!(),
29 };
30 (shown, paragraph.media.clone())
31 })
32 .collect()
33}
34
35#[test]
36fn a_recording_stores_its_line_its_linked_notes_and_its_file() {
37 let source = onestore::create_section("files.one", "Recorded", "Author").unwrap();
38 let arena = Arena::default();
39 let mut section = Section::open(&arena, source.clone()).unwrap();
40 let (space, ..) = section.pages().unwrap()[0].clone();
41 let mut engine = TextEngine::default();
42 let mut editor = CanvasEditor::from_page(section.page(space).unwrap(), &mut engine).unwrap();
43 let outline = editor
44 .outlines()
45 .iter()
46 .find(|outline| !outline.title)
47 .unwrap()
48 .id;
49 editor.focus_outline(outline).unwrap();
50 editor
51 .move_selection(&mut engine, Movement::DocumentEnd, false)
52 .unwrap();
53 let mut at = 133_000_000_000_000_000;
54 let mut store = |editor: &mut CanvasEditor| {
55 let ops = editor.take_ops().unwrap();
56 at += 10_000_000;
57 let ops = ops.into_iter().map(|op| Op::Page { space, op }).collect();
58 section.apply("Author", &Edit { at, ops }).unwrap();
59 let stored = section.page(space).unwrap();
60 assert_eq!(
61 Page {
62 title: stored.title.clone(),
63 ..editor.page().unwrap()
64 },
65 stored
66 );
67 stored
68 };
69 let id = editor
70 .start_recording(&mut engine, "Audio recording started: 5:18 PM")
71 .unwrap();
72 assert_eq!(editor.recording(), Some(id));
73 store(&mut editor);
74 std::thread::sleep(std::time::Duration::from_millis(20));
75 editor.insert(&mut engine, "A linked note").unwrap();
76 store(&mut editor);
77 editor.insert(&mut engine, "\n").unwrap();
78 store(&mut editor);
79 let file = Attachment {
80 id: onestore::page::text::new_id().unwrap(),
81 filename: "Recorded.wav".into(),
82 source_path: None,
83 size: Some([24.0, 24.0]),
84 layout: Default::default(),
85 bytes: Some(b"RIFF".as_slice().into()),
86 preview: Some(canvas::gpu::page::file_icon().into()),
87 recording: Some(Recording {
88 id,
89 kind: 1,
90 duration_ms: Some(1000),
91 }),
92 tags: Vec::new(),
93 };
94 editor.finish_recording(&mut engine, file).unwrap();
95 assert_eq!(editor.recording(), None);
96 let stored = store(&mut editor);
97 let shown = body(&stored);
98 let texts: Vec<&str> = shown.iter().map(|(text, _)| text.as_str()).collect();
99 assert_eq!(
100 texts,
101 [
102 "Recorded",
103 "[Recorded.wav]",
104 "",
105 "Audio recording started: 5:18 PM",
106 "A linked note",
107 ""
108 ]
109 );
110 let linked = |time| MediaIndex {
111 recordings: vec![id],
112 time_ms: Some(time),
113 };
114 assert_eq!(shown[0].1, MediaIndex::default());
115 assert_eq!(shown[1].1, linked(0));
116 assert_eq!(shown[3].1, linked(0));
117 assert!(shown[4].1.time_ms.is_some_and(|time| time >= 20));
118 assert_eq!(shown[5].1, MediaIndex::default());
119
120 // Undo takes the file and the links away as stored, and redo brings them back.
121 let before = stored;
122 editor.undo(&mut engine).unwrap();
123 let undone = store(&mut editor);
124 assert!(!body(&undone).iter().any(|(text, _)| text.starts_with('[')));
125 editor.redo(&mut engine).unwrap();
126 assert_eq!(store(&mut editor), before);
127}
128
129/// With the caret in the title, OneNote 2010 records at the start of the body, the file first
130/// (`corpus/recording`); an empty first half of the caret's paragraph is the blank line.
131#[test]
132fn a_recording_from_the_title_goes_to_the_body_s_start() {
133 let source = include_bytes!("../../../corpus/recording/native/notebook/files.one");
134 let arena = Arena::default();
135 let mut section = Section::open(&arena, source.to_vec()).unwrap();
136 let (space, ..) = section
137 .pages()
138 .unwrap()
139 .into_iter()
140 .find(|(_, title, _)| title == "Recorded")
141 .unwrap();
142 let mut engine = TextEngine::default();
143 let mut editor = CanvasEditor::from_page(section.page(space).unwrap(), &mut engine).unwrap();
144 let title = editor
145 .outlines()
146 .iter()
147 .find(|outline| outline.title)
148 .unwrap()
149 .id;
150 editor.focus_outline(title).unwrap();
151 let id = editor.start_recording(&mut engine, "Started").unwrap();
152 let file = Attachment {
153 id: onestore::page::text::new_id().unwrap(),
154 filename: "Recorded.wav".into(),
155 source_path: None,
156 size: Some([24.0, 24.0]),
157 layout: Default::default(),
158 bytes: Some(b"RIFF".as_slice().into()),
159 preview: Some(canvas::gpu::page::file_icon().into()),
160 recording: Some(Recording {
161 id,
162 kind: 1,
163 duration_ms: None,
164 }),
165 tags: Vec::new(),
166 };
167 editor.finish_recording(&mut engine, file).unwrap();
168 let ops = editor.take_ops().unwrap();
169 let ops = ops.into_iter().map(|op| Op::Page { space, op }).collect();
170 section
171 .apply(
172 "Author",
173 &Edit {
174 at: 133_000_000_000_000_000,
175 ops,
176 },
177 )
178 .unwrap();
179 let stored = section.page(space).unwrap();
180 let shown = body(&stored);
181 let texts: Vec<&str> = shown.iter().map(|(text, _)| text.as_str()).collect();
182 assert_eq!(
183 texts[..4],
184 ["[Recorded.wav]", "", "Started", "Before recording edited"]
185 );
186 // The note keeps its link to OneNote's recording.
187 assert_eq!(shown[3].1.time_ms, Some(20131));
188 assert_eq!(shown[1].1, MediaIndex::default());
189}
190
191/// A note written while recording is paused links to nothing, and the pause is left out of
192/// later moments, as OneNote 2010 links them (`corpus/recording/native/read/paused-while-recording.xml`).
193/// See Playback then highlights the note linked last at or before the moment playing.
194#[test]
195fn notes_written_while_paused_stay_unlinked_and_see_playback_follows_the_rest() {
196 let source = onestore::create_section("files.one", "Recorded", "Author").unwrap();
197 let arena = Arena::default();
198 let mut section = Section::open(&arena, source).unwrap();
199 let (space, ..) = section.pages().unwrap()[0].clone();
200 let mut engine = TextEngine::default();
201 let mut editor = CanvasEditor::from_page(section.page(space).unwrap(), &mut engine).unwrap();
202 let outline = editor
203 .outlines()
204 .iter()
205 .find(|outline| !outline.title)
206 .unwrap()
207 .id;
208 editor.focus_outline(outline).unwrap();
209 editor
210 .move_selection(&mut engine, Movement::DocumentEnd, false)
211 .unwrap();
212 let id = editor.start_recording(&mut engine, "Started").unwrap();
213 let wait = |ms| std::thread::sleep(std::time::Duration::from_millis(ms));
214 wait(30);
215 editor.insert(&mut engine, "Before the pause").unwrap();
216 editor.pause_recording(true);
217 assert!(editor.recording_paused());
218 let paused_at = editor.recording_ms().unwrap();
219 editor.insert(&mut engine, "\nWhile paused").unwrap();
220 wait(300);
221 assert_eq!(editor.recording_ms(), Some(paused_at));
222 editor.pause_recording(false);
223 wait(30);
224 editor.insert(&mut engine, "\nAfter the pause").unwrap();
225 let page = editor.page().unwrap();
226 let shown = body(&page);
227 let moment = |text: &str| {
228 shown
229 .iter()
230 .find(|(shown, _)| shown == text)
231 .unwrap()
232 .1
233 .time_ms
234 };
235 let before = moment("Before the pause").unwrap();
236 let after = moment("After the pause").unwrap();
237 assert_eq!(moment("While paused"), None);
238 assert!(
239 before >= 30 && after >= before + 30 && after < before + 300,
240 "{before} {after}"
241 );
242
243 let note = |at| {
244 editor
245 .played_note(id, at)
246 .and_then(|note| canvas::search::paragraph_match(&editor, note))
247 .map(|(_, selection)| selection.positions[0].paragraph)
248 };
249 // Before the first note only the line saying when, which is never highlighted.
250 assert_eq!(note(before - 1), None);
251 let first = note(before).unwrap();
252 assert_eq!(note(after - 1), Some(first));
253 assert!(note(after).unwrap() > first);
254}