/*************************************************************************** * __________ __ ___. * 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 w, h; 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 */ char font[MAX_PATH], name[MAX_FILENAME+10]; rb->snprintf(name, sizeof(name), "%s.fnt", rb->global_settings->font_file); struct browse_context browse = { .dirfilter = SHOW_FONT, .flags = BROWSE_SELECTONLY | BROWSE_NO_CONTEXT_MENU, .title = rb->str(LANG_CUSTOM_FONT), .icon = Icon_Menu_setting, .root = FONT_DIR, .selected = name, .buf = font, .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; }