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.
This commit is contained in:
Kovid Goyal 2018-04-05 09:40:11 +05:30
parent 575422bab2
commit 18b86edc5b
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
6 changed files with 56 additions and 15 deletions

View File

@ -14,8 +14,6 @@ New features for the in-browser viewer
- Allow loading fonts from the computer running calibre and using them - Allow loading fonts from the computer running calibre and using them
for reading. for reading.
- Add a time left for chapter/book footer.
New features for the server generally New features for the server generally
--------------------------------------- ---------------------------------------

View File

@ -273,7 +273,7 @@ class IframeBoss:
if cfi != self.last_cfi: if cfi != self.last_cfi:
self.last_cfi = cfi self.last_cfi = cfi
self.send_message('update_cfi', cfi=cfi, replace_history=self.replace_history_on_next_cfi_update, 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 self.replace_history_on_next_cfi_update = True
def update_toc_position(self): def update_toc_position(self):

View File

@ -466,7 +466,8 @@ def scroll_by_page(backward, by_screen):
pos = previous_col_location() if backward else next_col_location() pos = previous_col_location() if backward else next_col_location()
pages = 1 pages = 1
if pos is -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) get_boss().send_message('next_spine_item', previous=backward)
else: else:
if not backward: if not backward:

View File

@ -34,6 +34,9 @@ def create_item(region, label):
opt(_('Current section'), 'section'), opt(_('Current section'), 'section'),
sep(), sep(),
opt(_('Progress'), 'progress'), 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'), opt(_('Clock'), 'clock'),
)) ))
) )
@ -103,14 +106,26 @@ def commit_head_foot(onchange, container):
if window.Intl?.DateTimeFormat: 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: else:
date_formatter = {'format': def(date): time_formatter = {'format': def(date):
return '{}:{}'.format(date.getHours(), date.getMinutes()) 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 {} template = get_session_data().get(which) or {}
field = template[region] or 'empty' field = template[region] or 'empty'
interface_data = get_interface_data() 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) ival = fmt_sidx(ival, use_roman=interface_data.use_roman_numerals_for_series_number)
text = _('{0} of {1}').format(ival, metadata.series) text = _('{0} of {1}').format(ival, metadata.series)
elif field is 'clock': elif field is 'clock':
text = date_formatter.format(Date()) text = time_formatter.format(Date())
has_clock = True has_clock = True
elif field is 'section': elif field is 'section':
text = current_toc_node.title if current_toc_node else '' 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 '' text = current_toc_toplevel_node.title if current_toc_toplevel_node else ''
if not text: if not text:
text = current_toc_node.title if current_toc_node else '' 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: if text is not div.textContent:
div.textContent = text div.textContent = text
div.style.display = 'block' if text else 'none' div.style.display = 'block' if text else 'none'

View File

@ -31,8 +31,8 @@ class Timers:
self.average = avg self.average = avg
sq = 0 sq = 0
for v'var i = 0; i < rlen; i++': for v'var i = 0; i < rlen; i++':
x = rates[i] x = rates[i] - avg
sq += (x - avg) * (x - avg) sq += x * x
self.stddev = Math.sqrt(sq / (rlen - 1)) self.stddev = Math.sqrt(sq / (rlen - 1))
else: else:
self.average = self.stddev = 0 self.average = self.stddev = 0
@ -42,7 +42,7 @@ class Timers:
self.last_scroll_at = now = window.performance.now() self.last_scroll_at = now = window.performance.now()
if last_scroll_at is None: if last_scroll_at is None:
return 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: if time_since_last_scroll <= 0 or time_since_last_scroll >= 300:
return return
if is_large_scroll and time_since_last_scroll < 2: if is_large_scroll and time_since_last_scroll < 2:
@ -54,3 +54,8 @@ class Timers:
self.rates.shift() self.rates.shift()
self.rates.push(rate) self.rates.push(rate)
self.calculate() 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

View File

@ -119,7 +119,7 @@ class View:
self.ui = ui self.ui = ui
self.timers = Timers() self.timers = Timers()
self.loaded_resources = {} 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.current_toc_node = self.current_toc_toplevel_node = None
self.clock_timer_id = 0 self.clock_timer_id = 0
sd = get_session_data() sd = get_session_data()
@ -504,6 +504,7 @@ class View:
self.ui.db.update_last_read_time(self.book) self.ui.db.update_last_read_time(self.book)
lrd = {'device':get_device_uuid(), 'cfi':data.cfi, 'pos_frac':data.progress_frac} lrd = {'device':get_device_uuid(), 'cfi':data.cfi, 'pos_frac':data.progress_frac}
self.current_progress_frac = data.progress_frac self.current_progress_frac = data.progress_frac
self.current_file_progress_frac = data.file_progress_frac
self.update_header_footer() self.update_header_footer()
key = self.book.key key = self.book.key
if username: if username:
@ -517,13 +518,24 @@ class View:
sd = get_session_data() sd = get_session_data()
has_clock = False 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): def render_template(div, sz_attr, name):
nonlocal has_clock nonlocal has_clock
if sd.get(sz_attr, 20) > 5: if sd.get(sz_attr, 20) > 5:
mi = self.book.metadata 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) 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) 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) 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 has_clock = hca or hcb or hcc
if textc and not textb and not texta: if textc and not textb and not texta:
# Want right-aligned # Want right-aligned