mirror of
https://github.com/kovidgoyal/calibre.git
synced 2026-02-25 04:30:11 -05:00
325 lines
11 KiB
Python
325 lines
11 KiB
Python
# vim:fileencoding=utf-8
|
|
# License: GPL v3 Copyright: 2023, Kovid Goyal <kovid at kovidgoyal.net>
|
|
from __python__ import bound_methods, hash_literals
|
|
|
|
from elementmaker import E
|
|
|
|
from ajax import absolute_path
|
|
from dom import add_extra_css, svgicon, unique_id
|
|
from gettext import gettext as _
|
|
from popups import MODAL_Z_INDEX
|
|
from read_book.flow_mode import FlickAnimator
|
|
from read_book.touch import TouchHandler, install_handlers
|
|
from utils import debounce
|
|
|
|
add_extra_css(def():
|
|
bg = '#333'
|
|
fg = '#ddd'
|
|
button_bg = '#333'
|
|
css = f'.calibre-image-popup {{ background-color: {bg}; color: {fg}; }}'
|
|
css += f'.calibre-image-popup a {{ padding: 0.25ex; display: inline-block; border-radius: 100%; cursor: pointer; background-color: {button_bg}; }}'
|
|
css += f'.calibre-image-popup.fit-to-window a.fit-to-window {{ color: {button_bg}; background-color: {fg}; }}'
|
|
return css
|
|
)
|
|
|
|
|
|
def fit_image(width, height, pwidth, pheight):
|
|
'''
|
|
Fit image in box of width pwidth and height pheight.
|
|
@param width: Width of image
|
|
@param height: Height of image
|
|
@param pwidth: Width of box
|
|
@param pheight: Height of box
|
|
@return: scaled, new_width, new_height. scaled is True iff new_width and/or new_height is different from width or height.
|
|
'''
|
|
scaled = height > pheight or width > pwidth
|
|
if height > pheight:
|
|
corrf = pheight / float(height)
|
|
width, height = Math.floor(corrf*width), pheight
|
|
if width > pwidth:
|
|
corrf = pwidth / float(width)
|
|
width, height = pwidth, Math.floor(corrf*height)
|
|
if height > pheight:
|
|
corrf = pheight / float(height)
|
|
width, height = Math.floor(corrf*width), pheight
|
|
|
|
return scaled, int(width), int(height)
|
|
|
|
|
|
class ImagePopup(TouchHandler):
|
|
|
|
def __init__(self):
|
|
TouchHandler.__init__(self)
|
|
self.flick_animator = fa = FlickAnimator()
|
|
fa.scroll_x = self.scroll_x
|
|
fa.scroll_y = self.scroll_y
|
|
fa.cancel_current_drag_scroll = def(): pass
|
|
self._container = None
|
|
self.container_id = unique_id('image-popup')
|
|
self.img = None
|
|
self.img_loading = False
|
|
self.img_ok = False
|
|
|
|
@property
|
|
def container(self):
|
|
if self._container is None:
|
|
self._container = E.div(
|
|
style=f'display:none; position:absolute; left:0; top: 0; z-index: {MODAL_Z_INDEX}; width: 100vw; height: 100vh; padding: 0; margin: 0; border-width: 0; box-sizing: border-box',
|
|
id=self.container_id, tabindex='0', class_='calibre-image-popup',
|
|
onkeydown=self.onkeydown,
|
|
onwheel=self.onwheel,
|
|
E.div(
|
|
style='position: fixed; top: 0; left: 0; text-align: right; width: 100%; font-size: 200%; padding: 0.25ex; box-sizing: border-box; display: flex; justify-content: space-between',
|
|
E.a(
|
|
svgicon('fit-to-screen'), title=_('Fit image to screen'), class_='fit-to-window',
|
|
onclick=self.toggle_fit_to_window,
|
|
),
|
|
E.a(
|
|
svgicon('close'), title=_('Close'),
|
|
onclick=self.hide_container
|
|
),
|
|
),
|
|
E.canvas(
|
|
width=window.innerWidth + '', height=window.innerHeight + '',
|
|
style='width: 100%; height: 100%; margin: 0; padding: 0; border-width: 0; box-sizing: border-box; display: block',
|
|
aria_label='Popup view of image',
|
|
),
|
|
)
|
|
document.body.appendChild(self._container)
|
|
install_handlers(self.canvas, self)
|
|
window.addEventListener('resize', debounce(self.resize_canvas, 250))
|
|
return self._container
|
|
|
|
def onwheel(self, ev):
|
|
ev.stopPropagation(), ev.preventDefault()
|
|
dy = ev.deltaY
|
|
dx = ev.deltaX
|
|
if ev.deltaMode is window.WheelEvent.DOM_DELTA_LINE:
|
|
dy *= 20
|
|
dx *= 20
|
|
self.scroll_y(dy)
|
|
self.scroll_x(dx)
|
|
|
|
def onkeydown(self, ev):
|
|
ev.preventDefault(), ev.stopPropagation()
|
|
if ev.key is ' ':
|
|
return self.toggle_fit_to_window()
|
|
if ev.key is 'Escape':
|
|
return self.hide_container()
|
|
if ev.key is 'ArrowDown':
|
|
return self.scroll_down((window.innerHeight - 10) if ev.getModifierState("Control") else 10)
|
|
if ev.key is 'PageDown':
|
|
return self.scroll_down(window.innerHeight-10)
|
|
if ev.key is 'ArrowUp':
|
|
return self.scroll_up((window.innerHeight - 10) if ev.getModifierState("Control") else 10)
|
|
if ev.key is 'PageUp':
|
|
return self.scroll_up(window.innerHeight-10)
|
|
if ev.key is 'ArrowLeft':
|
|
return self.scroll_left((window.innerWidth - 10) if ev.getModifierState("Control") else 10)
|
|
if ev.key is 'ArrowRight':
|
|
return self.scroll_right((window.innerWidth - 10) if ev.getModifierState("Control") else 10)
|
|
|
|
def handle_gesture(self, gesture):
|
|
self.flick_animator.stop()
|
|
if gesture.type is 'swipe':
|
|
if not self.img_ok or self.fit_to_window:
|
|
return
|
|
if gesture.points.length > 1 and not gesture.is_held:
|
|
delta = gesture.points[-2] - gesture.points[-1]
|
|
if Math.abs(delta) >= 1:
|
|
if gesture.axis is 'vertical':
|
|
self.scroll_y(delta)
|
|
else:
|
|
self.scroll_x(delta)
|
|
if not gesture.active and not gesture.is_held:
|
|
self.flick_animator.start(gesture)
|
|
if gesture.type is 'pinch' and self.img_ok:
|
|
if gesture.direction is 'in':
|
|
self.fit_to_window = True
|
|
else:
|
|
self.fit_to_window = False
|
|
self.update_canvas()
|
|
|
|
@property
|
|
def vertical_max_bounce_distance(self):
|
|
return Math.min(60, window.innerHeight / 2)
|
|
|
|
@property
|
|
def horizontal_max_bounce_distance(self):
|
|
return Math.min(60, window.innerWidth / 2)
|
|
|
|
def scroll_down(self, amt):
|
|
if self.img_ok and not self.fit_to_window:
|
|
canvas_height = self.canvas_height
|
|
if self.img.height <= canvas_height:
|
|
return
|
|
miny = canvas_height - self.img.height - self.vertical_max_bounce_distance
|
|
y = Math.max(self.y - amt, miny)
|
|
if y is not self.y:
|
|
self.y = y
|
|
self.update_canvas()
|
|
|
|
def scroll_up(self, amt):
|
|
if self.img_ok and not self.fit_to_window:
|
|
canvas_height = self.canvas_height
|
|
if self.img.height <= canvas_height:
|
|
return
|
|
y = Math.min(self.y + amt, self.vertical_max_bounce_distance)
|
|
if y is not self.y:
|
|
self.y = y
|
|
self.update_canvas()
|
|
|
|
def scroll_right(self, amt):
|
|
if self.img_ok and not self.fit_to_window:
|
|
canvas_width = self.canvas_width
|
|
if self.img.width <= canvas_width:
|
|
return
|
|
minx = canvas_width - self.img.width - self.horizontal_max_bounce_distance
|
|
x = Math.max(self.x - amt, minx)
|
|
if x is not self.x:
|
|
self.x = x
|
|
self.update_canvas()
|
|
|
|
def scroll_left(self, amt):
|
|
if self.img_ok and not self.fit_to_window:
|
|
canvas_width = self.canvas_width
|
|
if self.img.width <= canvas_width:
|
|
return
|
|
x = Math.min(self.x + amt, self.horizontal_max_bounce_distance)
|
|
if x is not self.x:
|
|
self.x = x
|
|
self.update_canvas()
|
|
|
|
def scroll_y(self, amt):
|
|
if amt < 0:
|
|
self.scroll_up(-amt)
|
|
elif amt > 0:
|
|
self.scroll_down(amt)
|
|
|
|
def scroll_x(self, amt):
|
|
if amt < 0:
|
|
self.scroll_left(-amt)
|
|
elif amt > 0:
|
|
self.scroll_right(amt)
|
|
|
|
@property
|
|
def canvas(self):
|
|
return self.container.getElementsByTagName('canvas')[0]
|
|
|
|
@property
|
|
def fit_to_window(self):
|
|
return self.container.classList.contains('fit-to-window')
|
|
|
|
@fit_to_window.setter
|
|
def fit_to_window(self, val):
|
|
c = self.container
|
|
if val:
|
|
c.classList.add('fit-to-window')
|
|
c.querySelector('a.fit-to-window').title = _('Do not fit image to window')
|
|
else:
|
|
c.classList.remove('fit-to-window')
|
|
c.querySelector('a.fit-to-window').title = _('Fit image to window')
|
|
|
|
def resize_canvas(self):
|
|
c = self.canvas
|
|
dpr = Math.max(1, window.devicePixelRatio)
|
|
c.width = Math.ceil(window.innerWidth * dpr)
|
|
c.height = Math.ceil(window.innerHeight * dpr)
|
|
c.style.width = (c.width / dpr) + 'px'
|
|
c.style.height = (c.height / dpr) + 'px'
|
|
ctx = c.getContext('2d')
|
|
ctx.setTransform(1, 0, 0, 1, 0, 0)
|
|
ctx.scale(dpr, dpr)
|
|
ctx.fillStyle = ctx.strokeStyle = '#eee'
|
|
ctx.font = '16px sans-serif'
|
|
self.update_canvas()
|
|
|
|
def show_container(self):
|
|
self.flick_animator.stop()
|
|
self.container.style.display = 'block'
|
|
|
|
def hide_container(self):
|
|
self.flick_animator.stop()
|
|
self.container.style.display = 'none'
|
|
|
|
def show_url(self, url):
|
|
self.img = Image()
|
|
self.img.addEventListener('load', self.image_loaded)
|
|
self.img.addEventListener('error', self.image_failed)
|
|
self.img_loading = True
|
|
self.x = self.y = 0
|
|
self.img_ok = False
|
|
self.fit_to_window = True
|
|
self.img.src = url
|
|
self.show_container()
|
|
self.resize_canvas()
|
|
|
|
@property
|
|
def canvas_width(self):
|
|
return self.canvas.width / Math.max(1, window.devicePixelRatio)
|
|
|
|
@property
|
|
def canvas_height(self):
|
|
return self.canvas.height / Math.max(1, window.devicePixelRatio)
|
|
|
|
def update_canvas(self):
|
|
canvas = self.canvas
|
|
ctx = canvas.getContext('2d')
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
|
canvas_width, canvas_height = self.canvas_width, self.canvas_height
|
|
|
|
def draw_centered_text(text):
|
|
tm = ctx.measureText(text)
|
|
x = Math.max(0, (canvas_width - tm.width) / 2)
|
|
y = (canvas_height - 16) / 2
|
|
ctx.fillText(text, x, y)
|
|
|
|
if self.img_loading:
|
|
draw_centered_text(_('Loading image, please wait…'))
|
|
return
|
|
if not self.img_ok:
|
|
draw_centered_text(_('Loading the image failed'))
|
|
return
|
|
|
|
def draw_full_image_fit_to_canvas():
|
|
scaled, width, height = fit_image(self.img.width, self.img.height, canvas_width, canvas_height)
|
|
scaled
|
|
x = (canvas_width - width) / 2
|
|
y = (canvas_height - height) / 2
|
|
ctx.drawImage(self.img, x, y, width, height)
|
|
|
|
if self.fit_to_window:
|
|
draw_full_image_fit_to_canvas()
|
|
return
|
|
|
|
ctx.drawImage(self.img, self.x, self.y)
|
|
|
|
def toggle_fit_to_window(self):
|
|
self.fit_to_window ^= True
|
|
self.x = self.y = 0
|
|
self.update_canvas()
|
|
|
|
def image_loaded(self):
|
|
self.img_loading = False
|
|
self.img_ok = True
|
|
self.update_canvas()
|
|
|
|
def image_failed(self):
|
|
self.img_loading = False
|
|
self.img_ok = False
|
|
self.update_canvas()
|
|
|
|
|
|
popup = None
|
|
|
|
|
|
def show_image(url):
|
|
nonlocal popup
|
|
if popup is None:
|
|
popup = ImagePopup()
|
|
popup.show_url(url)
|
|
|
|
|
|
def develop(container):
|
|
show_image(absolute_path('get/cover/1698'))
|