From 18b86edc5bff8aebc09b7bb71b7ecb1603837398 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 5 Apr 2018 09:40:11 +0530 Subject: [PATCH] Browser viewer: Allow showing the time left in the current chapter/book in the header and footer areas of the book. To use go to the preferences of the browser viewer and customize the headers and footers to display the time left. Note that time left in chapter only works correctly if chapters are in separate HTML files in the book. --- src/calibre/srv/TODO.rst | 2 -- src/pyj/read_book/iframe.pyj | 2 +- src/pyj/read_book/paged_mode.pyj | 3 ++- src/pyj/read_book/prefs/head_foot.pyj | 33 +++++++++++++++++++++++---- src/pyj/read_book/timers.pyj | 11 ++++++--- src/pyj/read_book/view.pyj | 20 ++++++++++++---- 6 files changed, 56 insertions(+), 15 deletions(-) diff --git a/src/calibre/srv/TODO.rst b/src/calibre/srv/TODO.rst index 521d862835..db28113032 100644 --- a/src/calibre/srv/TODO.rst +++ b/src/calibre/srv/TODO.rst @@ -14,8 +14,6 @@ New features for the in-browser viewer - Allow loading fonts from the computer running calibre and using them for reading. -- Add a time left for chapter/book footer. - New features for the server generally --------------------------------------- diff --git a/src/pyj/read_book/iframe.pyj b/src/pyj/read_book/iframe.pyj index 6ade378da0..9d12f35fb4 100644 --- a/src/pyj/read_book/iframe.pyj +++ b/src/pyj/read_book/iframe.pyj @@ -273,7 +273,7 @@ class IframeBoss: if cfi != self.last_cfi: self.last_cfi = cfi self.send_message('update_cfi', cfi=cfi, replace_history=self.replace_history_on_next_cfi_update, - progress_frac=self.calculate_progress_frac(current_name, index)) + progress_frac=self.calculate_progress_frac(current_name, index), file_progress_frac=progress_frac()) self.replace_history_on_next_cfi_update = True def update_toc_position(self): diff --git a/src/pyj/read_book/paged_mode.pyj b/src/pyj/read_book/paged_mode.pyj index 22a15e0972..874c58fd13 100644 --- a/src/pyj/read_book/paged_mode.pyj +++ b/src/pyj/read_book/paged_mode.pyj @@ -466,7 +466,8 @@ def scroll_by_page(backward, by_screen): pos = previous_col_location() if backward else next_col_location() pages = 1 if pos is -1: - get_boss().report_human_scroll() + # dont report human scroll since we dont know if a full page was + # scrolled or not get_boss().send_message('next_spine_item', previous=backward) else: if not backward: diff --git a/src/pyj/read_book/prefs/head_foot.pyj b/src/pyj/read_book/prefs/head_foot.pyj index 5623191ef8..01811ad1a9 100644 --- a/src/pyj/read_book/prefs/head_foot.pyj +++ b/src/pyj/read_book/prefs/head_foot.pyj @@ -34,6 +34,9 @@ def create_item(region, label): opt(_('Current section'), 'section'), sep(), opt(_('Progress'), 'progress'), + opt(_('Time to read book'), 'time-book'), + opt(_('Time to read chapter'), 'time-chapter'), + opt(_('Time to read chapter and book'), 'time-chapter-book'), opt(_('Clock'), 'clock'), )) ) @@ -103,14 +106,26 @@ def commit_head_foot(onchange, container): if window.Intl?.DateTimeFormat: - date_formatter = window.Intl.DateTimeFormat(undefined, {'hour':'numeric', 'minute':'numeric'}) + time_formatter = window.Intl.DateTimeFormat(undefined, {'hour':'numeric', 'minute':'numeric'}) else: - date_formatter = {'format': def(date): + time_formatter = {'format': def(date): return '{}:{}'.format(date.getHours(), date.getMinutes()) } -def render_head_foot(div, which, region, progress_frac, metadata, current_toc_node, current_toc_toplevel_node): +def format_time_left(seconds): + hours, minutes = divmod(int(seconds / 60), 60) + if hours <= 0: + if minutes < 1: + return _('almost done') + minutes = minutes + return _('{} mins').format(minutes) + if not minutes: + return _('{} hours').format(hours) + return _('{} h {} mins').format(hours, minutes) + + +def render_head_foot(div, which, region, progress_frac, metadata, current_toc_node, current_toc_toplevel_node, book_time, chapter_time): template = get_session_data().get(which) or {} field = template[region] or 'empty' interface_data = get_interface_data() @@ -128,7 +143,7 @@ def render_head_foot(div, which, region, progress_frac, metadata, current_toc_no ival = fmt_sidx(ival, use_roman=interface_data.use_roman_numerals_for_series_number) text = _('{0} of {1}').format(ival, metadata.series) elif field is 'clock': - text = date_formatter.format(Date()) + text = time_formatter.format(Date()) has_clock = True elif field is 'section': text = current_toc_node.title if current_toc_node else '' @@ -136,6 +151,16 @@ def render_head_foot(div, which, region, progress_frac, metadata, current_toc_no text = current_toc_toplevel_node.title if current_toc_toplevel_node else '' if not text: text = current_toc_node.title if current_toc_node else '' + elif field.startswith('time-'): + if book_time is None or chapter_time is None: + text = _('Calculating...') + else: + if field is 'time-book': + text = format_time_left(book_time) + elif field is 'time-chapter': + text = format_time_left(chapter_time) + else: + text = '{} ({})'.format(format_time_left(chapter_time), format_time_left(book_time)) if text is not div.textContent: div.textContent = text div.style.display = 'block' if text else 'none' diff --git a/src/pyj/read_book/timers.pyj b/src/pyj/read_book/timers.pyj index 4897421417..c41b294486 100644 --- a/src/pyj/read_book/timers.pyj +++ b/src/pyj/read_book/timers.pyj @@ -31,8 +31,8 @@ class Timers: self.average = avg sq = 0 for v'var i = 0; i < rlen; i++': - x = rates[i] - sq += (x - avg) * (x - avg) + x = rates[i] - avg + sq += x * x self.stddev = Math.sqrt(sq / (rlen - 1)) else: self.average = self.stddev = 0 @@ -42,7 +42,7 @@ class Timers: self.last_scroll_at = now = window.performance.now() if last_scroll_at is None: return - time_since_last_scroll = now - last_scroll_at + time_since_last_scroll = (now - last_scroll_at) / 1000 if time_since_last_scroll <= 0 or time_since_last_scroll >= 300: return if is_large_scroll and time_since_last_scroll < 2: @@ -54,3 +54,8 @@ class Timers: self.rates.shift() self.rates.push(rate) self.calculate() + + def time_for(self, length): + if length >= 0 and self.rates.length >= THRESHOLD and self.average > 0: + return length / self.average + return None diff --git a/src/pyj/read_book/view.pyj b/src/pyj/read_book/view.pyj index b3c0b06ef2..a57f5833b8 100644 --- a/src/pyj/read_book/view.pyj +++ b/src/pyj/read_book/view.pyj @@ -119,7 +119,7 @@ class View: self.ui = ui self.timers = Timers() self.loaded_resources = {} - self.current_progress_frac = 0 + self.current_progress_frac = self.current_file_progress_frac = 0 self.current_toc_node = self.current_toc_toplevel_node = None self.clock_timer_id = 0 sd = get_session_data() @@ -504,6 +504,7 @@ class View: self.ui.db.update_last_read_time(self.book) lrd = {'device':get_device_uuid(), 'cfi':data.cfi, 'pos_frac':data.progress_frac} self.current_progress_frac = data.progress_frac + self.current_file_progress_frac = data.file_progress_frac self.update_header_footer() key = self.book.key if username: @@ -517,13 +518,24 @@ class View: sd = get_session_data() has_clock = False + if self.book?.manifest: + book_length = self.book.manifest.total_length or 0 + name = self.currently_showing.name + chapter_length = self.book.manifest.files[name]?.length or 0 + else: + book_length = chapter_length = 0 + book_length *= max(0, 1 - self.current_progress_frac) + chapter_length *= max(0, 1 - self.current_file_progress_frac) + book_time = self.timers.time_for(book_length) + chapter_time = self.timers.time_for(chapter_length) + def render_template(div, sz_attr, name): nonlocal has_clock if sd.get(sz_attr, 20) > 5: mi = self.book.metadata - texta, hca = render_head_foot(div.firstChild, name, 'left', self.current_progress_frac, mi, self.current_toc_node, self.current_toc_toplevel_node) - textb, hcb = render_head_foot(div.firstChild.nextSibling, name, 'middle', self.current_progress_frac, mi, self.current_toc_node, self.current_toc_toplevel_node) - textc, hcc = render_head_foot(div.lastChild, name, 'right', self.current_progress_frac, mi, self.current_toc_node, self.current_toc_toplevel_node) + texta, hca = render_head_foot(div.firstChild, name, 'left', self.current_progress_frac, mi, self.current_toc_node, self.current_toc_toplevel_node, book_time, chapter_time) + textb, hcb = render_head_foot(div.firstChild.nextSibling, name, 'middle', self.current_progress_frac, mi, self.current_toc_node, self.current_toc_toplevel_node, book_time, chapter_time) + textc, hcc = render_head_foot(div.lastChild, name, 'right', self.current_progress_frac, mi, self.current_toc_node, self.current_toc_toplevel_node, book_time, chapter_time) has_clock = hca or hcb or hcc if textc and not textb and not texta: # Want right-aligned