From 1bab5562c2498b6273916e2d0271a9deca1a97b1 Mon Sep 17 00:00:00 2001 From: Franklin Wei Date: Sat, 11 Mar 2017 17:00:47 -0500 Subject: Speed-reading plugin Partially based on `spread0r', an open-source ebook reader: https://github.com/xypiie/spread0r Similar to Spritz(TM): http://spritzinc.com Change-Id: I6aa54addd1910a83a266aea561406b6268449b67 --- apps/plugins/CATEGORIES | 1 + apps/plugins/SOURCES | 2 + apps/plugins/speedread.c | 732 ++++++++++++++++++++++++++++++++++++++++++++ apps/plugins/viewers.config | 1 + 4 files changed, 736 insertions(+) create mode 100644 apps/plugins/speedread.c (limited to 'apps') diff --git a/apps/plugins/CATEGORIES b/apps/plugins/CATEGORIES index 3fa17432f0..6308065828 100644 --- a/apps/plugins/CATEGORIES +++ b/apps/plugins/CATEGORIES @@ -148,6 +148,7 @@ solitaire,games sort,viewers spacerocks,games splitedit,apps +spritz,viewers star,games starfield,demos stats,apps diff --git a/apps/plugins/SOURCES b/apps/plugins/SOURCES index a4e372fba9..a02b9cef69 100644 --- a/apps/plugins/SOURCES +++ b/apps/plugins/SOURCES @@ -145,6 +145,7 @@ metronome.c #ifdef HAVE_LCD_BITMAP /* Not for the Archos Player */ 2048.c + /* Lua needs at least 160 KB to work in */ #if PLUGIN_BUFFER_SIZE >= 0x80000 boomshine.lua @@ -179,6 +180,7 @@ snake.c snake2.c solitaire.c sokoban.c +speedread.c star.c starfield.c vu_meter.c 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 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * $Id$ + * + * Copyright (C) 2017 Franklin Wei + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ***************************************************************************/ + +/* ideas for improvement: + * hyphenation of long words */ + +#include "plugin.h" + +#include "fixedpoint.h" + +#include "lib/helper.h" +#include "lib/pluginlib_actions.h" +#include "lib/pluginlib_exit.h" + +#define LINE_LEN 1024 +#define WORD_MAX 64 + +#define MIN_WPM 100 +#define MAX_WPM 1000 +#define DEF_WPM 250 +#define WPM_INCREMENT 25 + +/* mininum bytes to skip when seeking */ +#define SEEK_INTERVAL 100 + +#define FOCUS_X (7 * LCD_WIDTH / 20) +#define FOCUS_Y 0 + +#define FRAME_COLOR LCD_BLACK +#define BACKGROUND_COLOR LCD_WHITE /* inside frame */ + +#ifdef HAVE_LCD_COLOR +#define WORD_COLOR LCD_RGBPACK(48,48,48) +#define FOCUS_COLOR LCD_RGBPACK(204,0,0) +#define OUTSIDE_COLOR LCD_RGBPACK(128,128,128) +#define BAR_COLOR LCD_RGBPACK(230,230,230) +#else +#define WORD_COLOR LCD_BLACK +#define OUTSIDE_COLOR BACKGROUND_COLOR +#endif + +#define BOOKMARK_FILE VIEWERS_DATA_DIR "/speedread.dat" +#define CONFIG_FILE VIEWERS_DATA_DIR "/speedread.cfg" + +#define ANIM_TIME (75 * HZ / 100) + +int fd = -1; /* -1 = prescripted demo */ + +int word_num; /* which word on a line */ +off_t line_offs, begin_offs; /* offsets from the "real" beginning of the file to the current line and end of BOM */ + +int line_len = -1, custom_font = FONT_UI; + +const char *last_word = NULL; + +static const char *get_next_word(void) +{ + if(fd >= 0) + { + static char line_buf[LINE_LEN]; + static char *end = NULL; + + next_line: + + if(line_len < 0) + { + line_offs = rb->lseek(fd, 0, SEEK_CUR); + line_len = rb->read_line(fd, line_buf, LINE_LEN); + if(line_len <= 0) + return NULL; + + char *word = rb->strtok_r(line_buf, " ", &end); + + word_num = 0; + + if(!word) + goto next_line; + else + { + last_word = word; + return word; + } + } + + char *word = rb->strtok_r(NULL, " ", &end); + if(!word) + { + /* end of line */ + line_len = -1; + goto next_line; + } + ++word_num; + + last_word = word; + return word; + } + else + { + /* feed the user a quick demo */ + static const char *words[] = { "This", "plugin", "is", "for", "speed-reading", "plain", "text", "files.", + "Please", "open", "a", "plain", "text", "file", "to", "read", "by", "using", "the", "context", "menu.", + "Have", "a", "nice", "day!" }; + static unsigned idx = 0; + if(idx + 1 > ARRAYLEN(words)) + return NULL; + last_word = words[idx++]; + return last_word; + } +} + +static const char *get_last_word(void) +{ + if(last_word) + return last_word; + else + { + last_word = get_next_word(); + return last_word; + } +} + +static void cleanup(void) +{ + if(custom_font != FONT_UI) + rb->font_unload(custom_font); + backlight_use_settings(); +} + +/* returns height of drawn area */ +static int reset_drawing(long proportion) /* 16.16 fixed point, goes from 0 --> 1 over time */ +{ + int h = -1, w; + if(h < 0) + rb->lcd_getstringsize("X", &w, &h); + + /* clear word area */ + rb->lcd_set_foreground(BACKGROUND_COLOR); + rb->lcd_fillrect(0, 0, LCD_WIDTH, h * 3 / 2); + + if(proportion) + { +#ifdef HAVE_LCD_COLOR + rb->lcd_set_foreground(BAR_COLOR); +#endif + 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); + } + + rb->lcd_set_foreground(FRAME_COLOR); + + /* draw frame */ + rb->lcd_fillrect(0, h * 3 / 2, LCD_WIDTH, w / 2); + rb->lcd_fillrect(FOCUS_X - w / 4, FOCUS_Y + h * 5 / 4, w / 2, h / 2); + + rb->lcd_set_foreground(WORD_COLOR); + + return h * 3 / 2 + w / 2; +} + +static void render_word(const char *word, int focus) +{ + /* focus char first */ + char buf[5] = { 0, 0, 0, 0, 0 }; + int idx = rb->utf8seek(word, focus); + rb->memcpy(buf, word + idx, MIN(rb->utf8seek(word, focus + 1) - idx, 4)); + + int focus_w; + rb->lcd_getstringsize(buf, &focus_w, NULL); + +#ifdef HAVE_LCD_COLOR + rb->lcd_set_foreground(FOCUS_COLOR); +#endif + + rb->lcd_putsxy(FOCUS_X - focus_w / 2, FOCUS_Y, buf); + +#ifdef HAVE_LCD_COLOR + rb->lcd_set_foreground(WORD_COLOR); +#endif + + /* figure out how far left to shift */ + static char half[WORD_MAX]; + rb->strlcpy(half, word, rb->utf8seek(word, focus + 1)); + int w; + rb->lcd_getstringsize(half, &w, NULL); + + int x = FOCUS_X - focus_w / 2 - w; + + /* first half */ + rb->lcd_putsxy(x, FOCUS_Y, half); + + /* second half */ + x = FOCUS_X + focus_w / 2; + rb->lcd_putsxy(x, FOCUS_Y, word + rb->utf8seek(word, focus + 1)); +} + +static int calculate_focus(const char *word) +{ +#if 0 + int len = rb->utf8length(word); + int focus = -1; + for(int i = len / 5; i < len / 2; ++i) + { + switch(tolower(word[rb->utf8seek(word, i)])) + { + case 'a': case 'e': case 'i': case 'o': case 'u': + focus = i; + break; + default: + break; + } + } + + if(focus < 0) + focus = len / 2; + return focus; +#else + int len = rb->utf8length(word); + if(rb->utf8length(word) > 13) + return 4; + else + { + int tab[] = {0,1,1,1,1,2,2,2,2,3,3,3,3}; + return tab[len - 1]; + } +#endif +} + +static int calculate_delay(const char *word, int wpm) +{ + long base = 60 * HZ / wpm; + long timeout = base; + int len = rb->utf8length(word); + + if(len > 6) + timeout += base / 5 * (len - 6); + + if(rb->strchr(word, ',') || rb->strchr(word, '-')) + timeout += base / 2; + + if(rb->strchr(word, '.') || rb->strchr(word, '!') || rb->strchr(word, '?') || rb->strchr(word, ';')) + timeout += 3 * base / 2; + return timeout; +} + +static long render_screen(const char *word, int wpm) +{ + /* significant inspiration taken from spread0r */ + long timeout = calculate_delay(word, wpm); + int focus = calculate_focus(word); + + rb->lcd_setfont(custom_font); + + int h = reset_drawing(0); + + render_word(word, focus); + + rb->lcd_setfont(FONT_UI); + + rb->lcd_update_rect(0, 0, LCD_WIDTH, h); + return timeout; +} + +static void begin_anim(void) +{ + long start = *rb->current_tick; + long end = start + ANIM_TIME; + + const char *word = get_last_word(); + + int focus = calculate_focus(word); + + rb->lcd_setfont(custom_font); + + while(*rb->current_tick < end) + { + int h = reset_drawing(fp_div((*rb->current_tick - start) << 16, ANIM_TIME << 16, 16)); + + render_word(word, focus); + rb->lcd_update_rect(0, 0, LCD_WIDTH, h); + } + + rb->lcd_setfont(FONT_UI); +} + +static void init_drawing(void) +{ + backlight_ignore_timeout(); + atexit(cleanup); + + rb->lcd_set_background(OUTSIDE_COLOR); + rb->lcd_set_backdrop(NULL); + rb->lcd_set_drawmode(DRMODE_FG); + rb->lcd_clear_display(); + + rb->lcd_update(); +} + +enum { NOTHING = 0, SLOWER, FASTER, FFWD, BACK, PAUSE, QUIT }; + +static const struct button_mapping *plugin_contexts[] = { pla_main_ctx }; + +static int get_useraction(void) +{ + int button = pluginlib_getaction(0, plugin_contexts, ARRAYLEN(plugin_contexts)); + + switch(button) + { +#ifdef HAVE_SCROLLWHEEL + case PLA_SCROLL_FWD: + case PLA_SCROLL_FWD_REPEAT: +#else + case PLA_UP: +#endif + return FASTER; +#ifdef HAVE_SCROLLWHEEL + case PLA_SCROLL_BACK: + case PLA_SCROLL_BACK_REPEAT: +#else + case PLA_DOWN: +#endif + return SLOWER; + case PLA_SELECT: + return PAUSE; + case PLA_CANCEL: + return QUIT; + case PLA_LEFT_REPEAT: + case PLA_LEFT: + return BACK; + case PLA_RIGHT_REPEAT: + case PLA_RIGHT: + return FFWD; + default: + exit_on_usb(button); /* handle poweroff and USB events */ + return 0; + } +} + +static void save_bookmark(const char *fname, int wpm) +{ + if(!fname) + return; + rb->splash(0, "Saving..."); + /* copy every line except the one to be changed */ + int bookmark_fd = rb->open(BOOKMARK_FILE, O_RDONLY); + int tmp_fd = rb->open(BOOKMARK_FILE ".tmp", O_WRONLY | O_CREAT | O_TRUNC, 0666); + if(bookmark_fd >= 0) + { + while(1) + { + /* space for the filename, 3, integers, and a null */ + static char line[MAX_PATH + 1 + 10 + 1 + 10 + 1 + 10 + 1]; + int len = rb->read_line(bookmark_fd, line, sizeof(line)); + if(len <= 0) + break; + + char *end; + rb->strtok_r(line, " ", &end); + rb->strtok_r(NULL, " ", &end); + rb->strtok_r(NULL, " ", &end); + char *bookmark_name = rb->strtok_r(NULL, "", &end); + + if(!bookmark_name) + continue; /* avoid crash */ + if(rb->strcmp(fname, bookmark_name)) + { + /* go back and clean up after strtok */ + for(int i = 0; i < len - 1; ++i) + if(!line[i]) + line[i] = ' '; + + rb->write(tmp_fd, line, len); + rb->fdprintf(tmp_fd, "\n"); + } + } + rb->close(bookmark_fd); + } + rb->fdprintf(tmp_fd, "%ld %d %d %s\n", line_offs, word_num, wpm, fname); + rb->close(tmp_fd); + rb->rename(BOOKMARK_FILE ".tmp", BOOKMARK_FILE); +} + +static bool load_bookmark(const char *fname, int *wpm) +{ + int bookmark_fd = rb->open(BOOKMARK_FILE, O_RDONLY); + if(bookmark_fd >= 0) + { + while(1) + { + /* space for the filename, 2 integers, and a null */ + char line[MAX_PATH + 1 + 10 + 1 + 10 + 1]; + int len = rb->read_line(bookmark_fd, line, sizeof(line)); + if(len <= 0) + break; + + char *end; + char *tok = rb->strtok_r(line, " ", &end); + if(!tok) + continue; + off_t offs = rb->atoi(tok); + + tok = rb->strtok_r(NULL, " ", &end); + if(!tok) + continue; + int word = rb->atoi(tok); + + tok = rb->strtok_r(NULL, " ", &end); + if(!tok) + continue; + *wpm = rb->atoi(tok); + if(*wpm < MIN_WPM) + *wpm = MIN_WPM; + if(*wpm > MAX_WPM) + *wpm = MAX_WPM; + + char *bookmark_name = rb->strtok_r(NULL, "", &end); + + if(!bookmark_name) + continue; + + if(!rb->strcmp(fname, bookmark_name)) + { + rb->lseek(fd, offs, SEEK_SET); + for(int i = 0; i < word; ++i) + get_next_word(); + rb->close(bookmark_fd); + return true; + } + } + rb->close(bookmark_fd); + } + return false; +} + +static void new_font(const char *path) +{ + if(custom_font != FONT_UI) + rb->font_unload(custom_font); + custom_font = rb->font_load(path); + if(custom_font < 0) + custom_font = FONT_UI; +} + +static void save_font(const char *path) +{ + int font_fd = rb->open(CONFIG_FILE, O_WRONLY | O_TRUNC | O_CREAT, 0666); + rb->write(font_fd, path, rb->strlen(path)); + rb->close(font_fd); +} + +static char font_buf[MAX_PATH + 1]; + +static void load_font(void) +{ + int font_fd = rb->open(CONFIG_FILE, O_RDONLY); + if(font_fd < 0) + return; + int len = rb->read(font_fd, font_buf, MAX_PATH); + font_buf[len] = '\0'; + rb->close(font_fd); + new_font(font_buf); +} + +static void font_menu(void) +{ + /* taken from text_viewer */ + struct browse_context browse; + char font[MAX_PATH], name[MAX_FILENAME+10]; + + rb->snprintf(name, sizeof(name), "%s.fnt", rb->global_settings->font_file); + rb->browse_context_init(&browse, SHOW_FONT, + BROWSE_SELECTONLY|BROWSE_NO_CONTEXT_MENU, + "Font", Icon_Menu_setting, FONT_DIR, name); + + browse.buf = font; + browse.bufsize = sizeof(font); + + rb->rockbox_browse(&browse); + + if (browse.flags & BROWSE_SELECTED) + { + new_font(font); + save_font(font); + } +} + +static bool confirm_restart(void) +{ + const struct text_message prompt = { (const char*[]) {"Are you sure?", "This will erase your current position."}, 2}; + enum yesno_res response = rb->gui_syncyesno_run(&prompt, NULL, NULL); + if(response == YESNO_NO) + return false; + else + return true; +} + +static int config_menu(void) +{ + MENUITEM_STRINGLIST(menu, "Speedread Menu", NULL, + "Resume Reading", + "Restart from Beginning", + "Change Font", + "Quit"); + int rc = 0; + int sel = 0; + while(!rc) + { + switch(rb->do_menu(&menu, &sel, NULL, false)) + { + case 0: + rc = 1; + break; + case 1: + if(fd >= 0 && confirm_restart()) + { + rb->lseek(fd, begin_offs, SEEK_SET); + line_len = -1; + get_next_word(); + rc = 1; + } + break; + case 2: + font_menu(); + break; + case 3: + rc = 2; + break; + default: + break; + } + } + return rc - 1; +} + +enum { SKIP = -1, FINISH = -2 }; + +static int poll_input(int *wpm, long *clear, const char *fname, off_t file_size) +{ + switch(get_useraction()) + { + case FASTER: + if(*wpm + WPM_INCREMENT <= MAX_WPM) + *wpm += WPM_INCREMENT; + rb->splashf(0, "%d wpm", *wpm); + *clear = *rb->current_tick + HZ; + break; + case SLOWER: + if(*wpm - WPM_INCREMENT >= MIN_WPM) + *wpm -= WPM_INCREMENT; + rb->splashf(0, "%d wpm", *wpm); + *clear = *rb->current_tick + HZ; + break; + case FFWD: + if(fd >= 0) + { + off_t base_offs = rb->lseek(fd, 0, SEEK_CUR); + off_t offs = 0; + + do { + offs += SEEK_INTERVAL; + if(offs >= 1000 * SEEK_INTERVAL) + offs += 199 * SEEK_INTERVAL; + else if(offs >= 100 * SEEK_INTERVAL) + offs += 99 * SEEK_INTERVAL; + else if(offs >= 10 * SEEK_INTERVAL) + offs += 9 * SEEK_INTERVAL; + rb->splashf(0, "%ld/%ld bytes", offs + base_offs, file_size); + rb->sleep(HZ/20); + } while(get_useraction() == FFWD && offs + base_offs < file_size && offs + base_offs >= 0); + + *clear = *rb->current_tick + HZ; + + rb->lseek(fd, offs, SEEK_CUR); + + /* discard the next word (or more likely, portion of a word) */ + line_len = -1; + get_next_word(); + + return SKIP; + } + break; + case BACK: + if(fd >= 0) + { + off_t base_offs = rb->lseek(fd, 0, SEEK_CUR); + off_t offs = 0; + + do { + offs -= SEEK_INTERVAL; + if(offs <= -1000 * SEEK_INTERVAL) + offs -= 199 * SEEK_INTERVAL; + else if(offs <= -100 * SEEK_INTERVAL) + offs -= 99 * SEEK_INTERVAL; + else if(offs <= -10 * SEEK_INTERVAL) + offs -= 9 * SEEK_INTERVAL; + rb->splashf(0, "%ld/%ld bytes", offs + base_offs, file_size); + rb->sleep(HZ/20); + } while(get_useraction() == FFWD && offs + base_offs < file_size && offs + base_offs >= 0); + + *clear = *rb->current_tick + HZ; + + rb->lseek(fd, offs, SEEK_CUR); + + /* discard the next word (or more likely, portion of a word) */ + line_len = -1; + get_next_word(); + + return SKIP; + } + break; + case PAUSE: + case QUIT: + if(config_menu()) + { + save_bookmark(fname, *wpm); + return FINISH; + } + else + { + init_drawing(); + begin_anim(); + } + break; + case NOTHING: + default: + break; + } + return 0; +} + +enum plugin_status plugin_start(const void *param) +{ + const char *fname = param; + + off_t file_size = 0; + + load_font(); + + bool loaded = false; + + int wpm = DEF_WPM; + + if(fname) + { + fd = rb->open_utf8(fname, O_RDONLY); + + begin_offs = rb->lseek(fd, 0, SEEK_CUR); /* skip BOM */ + file_size = rb->lseek(fd, 0, SEEK_END); + rb->lseek(fd, begin_offs, SEEK_SET); + + loaded = load_bookmark(fname, &wpm); + } + + init_drawing(); + + long clear = -1; + if(loaded) + { + rb->splash(0, "Loaded bookmark."); + clear = *rb->current_tick + HZ; + } + + begin_anim(); + + /* main loop */ + while(1) + { + switch(poll_input(&wpm, &clear, fname, file_size)) + { + case SKIP: + continue; + case FINISH: + goto done; + default: + break; + } + + const char *word = get_next_word(); + if(!word) + break; + bool want_full_update = false; + if(TIME_AFTER(*rb->current_tick, clear) && clear != -1) + { + clear = -1; + rb->lcd_clear_display(); + want_full_update = true; + } + long interval = render_screen(word, wpm); + + long frame_done = *rb->current_tick + interval; + + if(want_full_update) + rb->lcd_update(); + + while(!TIME_AFTER(*rb->current_tick, frame_done)) + { + switch(poll_input(&wpm, &clear, fname, file_size)) + { + case SKIP: + goto next_word; + case FINISH: + goto done; + default: + break; + } + rb->yield(); + } + next_word: + ; + } + +done: + rb->close(fd); + + return PLUGIN_OK; +} diff --git a/apps/plugins/viewers.config b/apps/plugins/viewers.config index 9eca2dab1a..c938eeb275 100644 --- a/apps/plugins/viewers.config +++ b/apps/plugins/viewers.config @@ -2,6 +2,7 @@ ch8,viewers/chip8,0 txt,viewers/text_viewer,1 txt,apps/text_editor,2 txt,viewers/sort,- +txt,viewers/speedread,1 nfo,viewers/text_viewer,1 bmp,viewers/imageviewer,2 bmp,apps/rockpaint,11 -- cgit v1.2.3