summaryrefslogtreecommitdiff
path: root/apps/plugins/speedread.c
diff options
context:
space:
mode:
Diffstat (limited to 'apps/plugins/speedread.c')
-rw-r--r--apps/plugins/speedread.c732
1 files changed, 732 insertions, 0 deletions
diff --git a/apps/plugins/speedread.c b/apps/plugins/speedread.c
new file mode 100644
index 0000000000..c80839e4a9
--- /dev/null
+++ b/apps/plugins/speedread.c
@@ -0,0 +1,732 @@
1/***************************************************************************
2 * __________ __ ___.
3 * Open \______ \ ____ ____ | | _\_ |__ _______ ___
4 * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
5 * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
6 * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
7 * \/ \/ \/ \/ \/
8 * $Id$
9 *
10 * Copyright (C) 2017 Franklin Wei
11 *
12 * This program is free software; you can redistribute it and/or
13 * modify it under the terms of the GNU General Public License
14 * as published by the Free Software Foundation; either version 2
15 * of the License, or (at your option) any later version.
16 *
17 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
18 * KIND, either express or implied.
19 *
20 ***************************************************************************/
21
22/* ideas for improvement:
23 * hyphenation of long words */
24
25#include "plugin.h"
26
27#include "fixedpoint.h"
28
29#include "lib/helper.h"
30#include "lib/pluginlib_actions.h"
31#include "lib/pluginlib_exit.h"
32
33#define LINE_LEN 1024
34#define WORD_MAX 64
35
36#define MIN_WPM 100
37#define MAX_WPM 1000
38#define DEF_WPM 250
39#define WPM_INCREMENT 25
40
41/* mininum bytes to skip when seeking */
42#define SEEK_INTERVAL 100
43
44#define FOCUS_X (7 * LCD_WIDTH / 20)
45#define FOCUS_Y 0
46
47#define FRAME_COLOR LCD_BLACK
48#define BACKGROUND_COLOR LCD_WHITE /* inside frame */
49
50#ifdef HAVE_LCD_COLOR
51#define WORD_COLOR LCD_RGBPACK(48,48,48)
52#define FOCUS_COLOR LCD_RGBPACK(204,0,0)
53#define OUTSIDE_COLOR LCD_RGBPACK(128,128,128)
54#define BAR_COLOR LCD_RGBPACK(230,230,230)
55#else
56#define WORD_COLOR LCD_BLACK
57#define OUTSIDE_COLOR BACKGROUND_COLOR
58#endif
59
60#define BOOKMARK_FILE VIEWERS_DATA_DIR "/speedread.dat"
61#define CONFIG_FILE VIEWERS_DATA_DIR "/speedread.cfg"
62
63#define ANIM_TIME (75 * HZ / 100)
64
65int fd = -1; /* -1 = prescripted demo */
66
67int word_num; /* which word on a line */
68off_t line_offs, begin_offs; /* offsets from the "real" beginning of the file to the current line and end of BOM */
69
70int line_len = -1, custom_font = FONT_UI;
71
72const char *last_word = NULL;
73
74static const char *get_next_word(void)
75{
76 if(fd >= 0)
77 {
78 static char line_buf[LINE_LEN];
79 static char *end = NULL;
80
81 next_line:
82
83 if(line_len < 0)
84 {
85 line_offs = rb->lseek(fd, 0, SEEK_CUR);
86 line_len = rb->read_line(fd, line_buf, LINE_LEN);
87 if(line_len <= 0)
88 return NULL;
89
90 char *word = rb->strtok_r(line_buf, " ", &end);
91
92 word_num = 0;
93
94 if(!word)
95 goto next_line;
96 else
97 {
98 last_word = word;
99 return word;
100 }
101 }
102
103 char *word = rb->strtok_r(NULL, " ", &end);
104 if(!word)
105 {
106 /* end of line */
107 line_len = -1;
108 goto next_line;
109 }
110 ++word_num;
111
112 last_word = word;
113 return word;
114 }
115 else
116 {
117 /* feed the user a quick demo */
118 static const char *words[] = { "This", "plugin", "is", "for", "speed-reading", "plain", "text", "files.",
119 "Please", "open", "a", "plain", "text", "file", "to", "read", "by", "using", "the", "context", "menu.",
120 "Have", "a", "nice", "day!" };
121 static unsigned idx = 0;
122 if(idx + 1 > ARRAYLEN(words))
123 return NULL;
124 last_word = words[idx++];
125 return last_word;
126 }
127}
128
129static const char *get_last_word(void)
130{
131 if(last_word)
132 return last_word;
133 else
134 {
135 last_word = get_next_word();
136 return last_word;
137 }
138}
139
140static void cleanup(void)
141{
142 if(custom_font != FONT_UI)
143 rb->font_unload(custom_font);
144 backlight_use_settings();
145}
146
147/* returns height of drawn area */
148static int reset_drawing(long proportion) /* 16.16 fixed point, goes from 0 --> 1 over time */
149{
150 int h = -1, w;
151 if(h < 0)
152 rb->lcd_getstringsize("X", &w, &h);
153
154 /* clear word area */
155 rb->lcd_set_foreground(BACKGROUND_COLOR);
156 rb->lcd_fillrect(0, 0, LCD_WIDTH, h * 3 / 2);
157
158 if(proportion)
159 {
160#ifdef HAVE_LCD_COLOR
161 rb->lcd_set_foreground(BAR_COLOR);
162#endif
163 rb->lcd_fillrect(fp_mul(proportion, FOCUS_X << 16, 16) >> 16, FOCUS_Y, fp_mul((1 << 16) - proportion, LCD_WIDTH << 16, 16) >> 16, h * 3 / 2);
164 }
165
166 rb->lcd_set_foreground(FRAME_COLOR);
167
168 /* draw frame */
169 rb->lcd_fillrect(0, h * 3 / 2, LCD_WIDTH, w / 2);
170 rb->lcd_fillrect(FOCUS_X - w / 4, FOCUS_Y + h * 5 / 4, w / 2, h / 2);
171
172 rb->lcd_set_foreground(WORD_COLOR);
173
174 return h * 3 / 2 + w / 2;
175}
176
177static void render_word(const char *word, int focus)
178{
179 /* focus char first */
180 char buf[5] = { 0, 0, 0, 0, 0 };
181 int idx = rb->utf8seek(word, focus);
182 rb->memcpy(buf, word + idx, MIN(rb->utf8seek(word, focus + 1) - idx, 4));
183
184 int focus_w;
185 rb->lcd_getstringsize(buf, &focus_w, NULL);
186
187#ifdef HAVE_LCD_COLOR
188 rb->lcd_set_foreground(FOCUS_COLOR);
189#endif
190
191 rb->lcd_putsxy(FOCUS_X - focus_w / 2, FOCUS_Y, buf);
192
193#ifdef HAVE_LCD_COLOR
194 rb->lcd_set_foreground(WORD_COLOR);
195#endif
196
197 /* figure out how far left to shift */
198 static char half[WORD_MAX];
199 rb->strlcpy(half, word, rb->utf8seek(word, focus + 1));
200 int w;
201 rb->lcd_getstringsize(half, &w, NULL);
202
203 int x = FOCUS_X - focus_w / 2 - w;
204
205 /* first half */
206 rb->lcd_putsxy(x, FOCUS_Y, half);
207
208 /* second half */
209 x = FOCUS_X + focus_w / 2;
210 rb->lcd_putsxy(x, FOCUS_Y, word + rb->utf8seek(word, focus + 1));
211}
212
213static int calculate_focus(const char *word)
214{
215#if 0
216 int len = rb->utf8length(word);
217 int focus = -1;
218 for(int i = len / 5; i < len / 2; ++i)
219 {
220 switch(tolower(word[rb->utf8seek(word, i)]))
221 {
222 case 'a': case 'e': case 'i': case 'o': case 'u':
223 focus = i;
224 break;
225 default:
226 break;
227 }
228 }
229
230 if(focus < 0)
231 focus = len / 2;
232 return focus;
233#else
234 int len = rb->utf8length(word);
235 if(rb->utf8length(word) > 13)
236 return 4;
237 else
238 {
239 int tab[] = {0,1,1,1,1,2,2,2,2,3,3,3,3};
240 return tab[len - 1];
241 }
242#endif
243}
244
245static int calculate_delay(const char *word, int wpm)
246{
247 long base = 60 * HZ / wpm;
248 long timeout = base;
249 int len = rb->utf8length(word);
250
251 if(len > 6)
252 timeout += base / 5 * (len - 6);
253
254 if(rb->strchr(word, ',') || rb->strchr(word, '-'))
255 timeout += base / 2;
256
257 if(rb->strchr(word, '.') || rb->strchr(word, '!') || rb->strchr(word, '?') || rb->strchr(word, ';'))
258 timeout += 3 * base / 2;
259 return timeout;
260}
261
262static long render_screen(const char *word, int wpm)
263{
264 /* significant inspiration taken from spread0r */
265 long timeout = calculate_delay(word, wpm);
266 int focus = calculate_focus(word);
267
268 rb->lcd_setfont(custom_font);
269
270 int h = reset_drawing(0);
271
272 render_word(word, focus);
273
274 rb->lcd_setfont(FONT_UI);
275
276 rb->lcd_update_rect(0, 0, LCD_WIDTH, h);
277 return timeout;
278}
279
280static void begin_anim(void)
281{
282 long start = *rb->current_tick;
283 long end = start + ANIM_TIME;
284
285 const char *word = get_last_word();
286
287 int focus = calculate_focus(word);
288
289 rb->lcd_setfont(custom_font);
290
291 while(*rb->current_tick < end)
292 {
293 int h = reset_drawing(fp_div((*rb->current_tick - start) << 16, ANIM_TIME << 16, 16));
294
295 render_word(word, focus);
296 rb->lcd_update_rect(0, 0, LCD_WIDTH, h);
297 }
298
299 rb->lcd_setfont(FONT_UI);
300}
301
302static void init_drawing(void)
303{
304 backlight_ignore_timeout();
305 atexit(cleanup);
306
307 rb->lcd_set_background(OUTSIDE_COLOR);
308 rb->lcd_set_backdrop(NULL);
309 rb->lcd_set_drawmode(DRMODE_FG);
310 rb->lcd_clear_display();
311
312 rb->lcd_update();
313}
314
315enum { NOTHING = 0, SLOWER, FASTER, FFWD, BACK, PAUSE, QUIT };
316
317static const struct button_mapping *plugin_contexts[] = { pla_main_ctx };
318
319static int get_useraction(void)
320{
321 int button = pluginlib_getaction(0, plugin_contexts, ARRAYLEN(plugin_contexts));
322
323 switch(button)
324 {
325#ifdef HAVE_SCROLLWHEEL
326 case PLA_SCROLL_FWD:
327 case PLA_SCROLL_FWD_REPEAT:
328#else
329 case PLA_UP:
330#endif
331 return FASTER;
332#ifdef HAVE_SCROLLWHEEL
333 case PLA_SCROLL_BACK:
334 case PLA_SCROLL_BACK_REPEAT:
335#else
336 case PLA_DOWN:
337#endif
338 return SLOWER;
339 case PLA_SELECT:
340 return PAUSE;
341 case PLA_CANCEL:
342 return QUIT;
343 case PLA_LEFT_REPEAT:
344 case PLA_LEFT:
345 return BACK;
346 case PLA_RIGHT_REPEAT:
347 case PLA_RIGHT:
348 return FFWD;
349 default:
350 exit_on_usb(button); /* handle poweroff and USB events */
351 return 0;
352 }
353}
354
355static void save_bookmark(const char *fname, int wpm)
356{
357 if(!fname)
358 return;
359 rb->splash(0, "Saving...");
360 /* copy every line except the one to be changed */
361 int bookmark_fd = rb->open(BOOKMARK_FILE, O_RDONLY);
362 int tmp_fd = rb->open(BOOKMARK_FILE ".tmp", O_WRONLY | O_CREAT | O_TRUNC, 0666);
363 if(bookmark_fd >= 0)
364 {
365 while(1)
366 {
367 /* space for the filename, 3, integers, and a null */
368 static char line[MAX_PATH + 1 + 10 + 1 + 10 + 1 + 10 + 1];
369 int len = rb->read_line(bookmark_fd, line, sizeof(line));
370 if(len <= 0)
371 break;
372
373 char *end;
374 rb->strtok_r(line, " ", &end);
375 rb->strtok_r(NULL, " ", &end);
376 rb->strtok_r(NULL, " ", &end);
377 char *bookmark_name = rb->strtok_r(NULL, "", &end);
378
379 if(!bookmark_name)
380 continue; /* avoid crash */
381 if(rb->strcmp(fname, bookmark_name))
382 {
383 /* go back and clean up after strtok */
384 for(int i = 0; i < len - 1; ++i)
385 if(!line[i])
386 line[i] = ' ';
387
388 rb->write(tmp_fd, line, len);
389 rb->fdprintf(tmp_fd, "\n");
390 }
391 }
392 rb->close(bookmark_fd);
393 }
394 rb->fdprintf(tmp_fd, "%ld %d %d %s\n", line_offs, word_num, wpm, fname);
395 rb->close(tmp_fd);
396 rb->rename(BOOKMARK_FILE ".tmp", BOOKMARK_FILE);
397}
398
399static bool load_bookmark(const char *fname, int *wpm)
400{
401 int bookmark_fd = rb->open(BOOKMARK_FILE, O_RDONLY);
402 if(bookmark_fd >= 0)
403 {
404 while(1)
405 {
406 /* space for the filename, 2 integers, and a null */
407 char line[MAX_PATH + 1 + 10 + 1 + 10 + 1];
408 int len = rb->read_line(bookmark_fd, line, sizeof(line));
409 if(len <= 0)
410 break;
411
412 char *end;
413 char *tok = rb->strtok_r(line, " ", &end);
414 if(!tok)
415 continue;
416 off_t offs = rb->atoi(tok);
417
418 tok = rb->strtok_r(NULL, " ", &end);
419 if(!tok)
420 continue;
421 int word = rb->atoi(tok);
422
423 tok = rb->strtok_r(NULL, " ", &end);
424 if(!tok)
425 continue;
426 *wpm = rb->atoi(tok);
427 if(*wpm < MIN_WPM)
428 *wpm = MIN_WPM;
429 if(*wpm > MAX_WPM)
430 *wpm = MAX_WPM;
431
432 char *bookmark_name = rb->strtok_r(NULL, "", &end);
433
434 if(!bookmark_name)
435 continue;
436
437 if(!rb->strcmp(fname, bookmark_name))
438 {
439 rb->lseek(fd, offs, SEEK_SET);
440 for(int i = 0; i < word; ++i)
441 get_next_word();
442 rb->close(bookmark_fd);
443 return true;
444 }
445 }
446 rb->close(bookmark_fd);
447 }
448 return false;
449}
450
451static void new_font(const char *path)
452{
453 if(custom_font != FONT_UI)
454 rb->font_unload(custom_font);
455 custom_font = rb->font_load(path);
456 if(custom_font < 0)
457 custom_font = FONT_UI;
458}
459
460static void save_font(const char *path)
461{
462 int font_fd = rb->open(CONFIG_FILE, O_WRONLY | O_TRUNC | O_CREAT, 0666);
463 rb->write(font_fd, path, rb->strlen(path));
464 rb->close(font_fd);
465}
466
467static char font_buf[MAX_PATH + 1];
468
469static void load_font(void)
470{
471 int font_fd = rb->open(CONFIG_FILE, O_RDONLY);
472 if(font_fd < 0)
473 return;
474 int len = rb->read(font_fd, font_buf, MAX_PATH);
475 font_buf[len] = '\0';
476 rb->close(font_fd);
477 new_font(font_buf);
478}
479
480static void font_menu(void)
481{
482 /* taken from text_viewer */
483 struct browse_context browse;
484 char font[MAX_PATH], name[MAX_FILENAME+10];
485
486 rb->snprintf(name, sizeof(name), "%s.fnt", rb->global_settings->font_file);
487 rb->browse_context_init(&browse, SHOW_FONT,
488 BROWSE_SELECTONLY|BROWSE_NO_CONTEXT_MENU,
489 "Font", Icon_Menu_setting, FONT_DIR, name);
490
491 browse.buf = font;
492 browse.bufsize = sizeof(font);
493
494 rb->rockbox_browse(&browse);
495
496 if (browse.flags & BROWSE_SELECTED)
497 {
498 new_font(font);
499 save_font(font);
500 }
501}
502
503static bool confirm_restart(void)
504{
505 const struct text_message prompt = { (const char*[]) {"Are you sure?", "This will erase your current position."}, 2};
506 enum yesno_res response = rb->gui_syncyesno_run(&prompt, NULL, NULL);
507 if(response == YESNO_NO)
508 return false;
509 else
510 return true;
511}
512
513static int config_menu(void)
514{
515 MENUITEM_STRINGLIST(menu, "Speedread Menu", NULL,
516 "Resume Reading",
517 "Restart from Beginning",
518 "Change Font",
519 "Quit");
520 int rc = 0;
521 int sel = 0;
522 while(!rc)
523 {
524 switch(rb->do_menu(&menu, &sel, NULL, false))
525 {
526 case 0:
527 rc = 1;
528 break;
529 case 1:
530 if(fd >= 0 && confirm_restart())
531 {
532 rb->lseek(fd, begin_offs, SEEK_SET);
533 line_len = -1;
534 get_next_word();
535 rc = 1;
536 }
537 break;
538 case 2:
539 font_menu();
540 break;
541 case 3:
542 rc = 2;
543 break;
544 default:
545 break;
546 }
547 }
548 return rc - 1;
549}
550
551enum { SKIP = -1, FINISH = -2 };
552
553static int poll_input(int *wpm, long *clear, const char *fname, off_t file_size)
554{
555 switch(get_useraction())
556 {
557 case FASTER:
558 if(*wpm + WPM_INCREMENT <= MAX_WPM)
559 *wpm += WPM_INCREMENT;
560 rb->splashf(0, "%d wpm", *wpm);
561 *clear = *rb->current_tick + HZ;
562 break;
563 case SLOWER:
564 if(*wpm - WPM_INCREMENT >= MIN_WPM)
565 *wpm -= WPM_INCREMENT;
566 rb->splashf(0, "%d wpm", *wpm);
567 *clear = *rb->current_tick + HZ;
568 break;
569 case FFWD:
570 if(fd >= 0)
571 {
572 off_t base_offs = rb->lseek(fd, 0, SEEK_CUR);
573 off_t offs = 0;
574
575 do {
576 offs += SEEK_INTERVAL;
577 if(offs >= 1000 * SEEK_INTERVAL)
578 offs += 199 * SEEK_INTERVAL;
579 else if(offs >= 100 * SEEK_INTERVAL)
580 offs += 99 * SEEK_INTERVAL;
581 else if(offs >= 10 * SEEK_INTERVAL)
582 offs += 9 * SEEK_INTERVAL;
583 rb->splashf(0, "%ld/%ld bytes", offs + base_offs, file_size);
584 rb->sleep(HZ/20);
585 } while(get_useraction() == FFWD && offs + base_offs < file_size && offs + base_offs >= 0);
586
587 *clear = *rb->current_tick + HZ;
588
589 rb->lseek(fd, offs, SEEK_CUR);
590
591 /* discard the next word (or more likely, portion of a word) */
592 line_len = -1;
593 get_next_word();
594
595 return SKIP;
596 }
597 break;
598 case BACK:
599 if(fd >= 0)
600 {
601 off_t base_offs = rb->lseek(fd, 0, SEEK_CUR);
602 off_t offs = 0;
603
604 do {
605 offs -= SEEK_INTERVAL;
606 if(offs <= -1000 * SEEK_INTERVAL)
607 offs -= 199 * SEEK_INTERVAL;
608 else if(offs <= -100 * SEEK_INTERVAL)
609 offs -= 99 * SEEK_INTERVAL;
610 else if(offs <= -10 * SEEK_INTERVAL)
611 offs -= 9 * SEEK_INTERVAL;
612 rb->splashf(0, "%ld/%ld bytes", offs + base_offs, file_size);
613 rb->sleep(HZ/20);
614 } while(get_useraction() == FFWD && offs + base_offs < file_size && offs + base_offs >= 0);
615
616 *clear = *rb->current_tick + HZ;
617
618 rb->lseek(fd, offs, SEEK_CUR);
619
620 /* discard the next word (or more likely, portion of a word) */
621 line_len = -1;
622 get_next_word();
623
624 return SKIP;
625 }
626 break;
627 case PAUSE:
628 case QUIT:
629 if(config_menu())
630 {
631 save_bookmark(fname, *wpm);
632 return FINISH;
633 }
634 else
635 {
636 init_drawing();
637 begin_anim();
638 }
639 break;
640 case NOTHING:
641 default:
642 break;
643 }
644 return 0;
645}
646
647enum plugin_status plugin_start(const void *param)
648{
649 const char *fname = param;
650
651 off_t file_size = 0;
652
653 load_font();
654
655 bool loaded = false;
656
657 int wpm = DEF_WPM;
658
659 if(fname)
660 {
661 fd = rb->open_utf8(fname, O_RDONLY);
662
663 begin_offs = rb->lseek(fd, 0, SEEK_CUR); /* skip BOM */
664 file_size = rb->lseek(fd, 0, SEEK_END);
665 rb->lseek(fd, begin_offs, SEEK_SET);
666
667 loaded = load_bookmark(fname, &wpm);
668 }
669
670 init_drawing();
671
672 long clear = -1;
673 if(loaded)
674 {
675 rb->splash(0, "Loaded bookmark.");
676 clear = *rb->current_tick + HZ;
677 }
678
679 begin_anim();
680
681 /* main loop */
682 while(1)
683 {
684 switch(poll_input(&wpm, &clear, fname, file_size))
685 {
686 case SKIP:
687 continue;
688 case FINISH:
689 goto done;
690 default:
691 break;
692 }
693
694 const char *word = get_next_word();
695 if(!word)
696 break;
697 bool want_full_update = false;
698 if(TIME_AFTER(*rb->current_tick, clear) && clear != -1)
699 {
700 clear = -1;
701 rb->lcd_clear_display();
702 want_full_update = true;
703 }
704 long interval = render_screen(word, wpm);
705
706 long frame_done = *rb->current_tick + interval;
707
708 if(want_full_update)
709 rb->lcd_update();
710
711 while(!TIME_AFTER(*rb->current_tick, frame_done))
712 {
713 switch(poll_input(&wpm, &clear, fname, file_size))
714 {
715 case SKIP:
716 goto next_word;
717 case FINISH:
718 goto done;
719 default:
720 break;
721 }
722 rb->yield();
723 }
724 next_word:
725 ;
726 }
727
728done:
729 rb->close(fd);
730
731 return PLUGIN_OK;
732}