mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-08 10:44:09 -04:00
Server: Editing notes with image insertion basically works
This commit is contained in:
parent
a941d66031
commit
ec86d24535
@ -4,11 +4,14 @@
|
|||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
|
__copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
|
|
||||||
|
import base64
|
||||||
import errno
|
import errno
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
from json import load as load_json_file
|
||||||
from threading import Lock
|
from threading import Lock
|
||||||
|
|
||||||
from calibre import fit_image, guess_type, sanitize_file_name
|
from calibre import fit_image, guess_type, sanitize_file_name
|
||||||
@ -22,7 +25,7 @@ from calibre.ebooks.metadata import authors_to_string
|
|||||||
from calibre.ebooks.metadata.meta import set_metadata
|
from calibre.ebooks.metadata.meta import set_metadata
|
||||||
from calibre.ebooks.metadata.opf2 import metadata_to_opf
|
from calibre.ebooks.metadata.opf2 import metadata_to_opf
|
||||||
from calibre.library.save_to_disk import find_plugboard
|
from calibre.library.save_to_disk import find_plugboard
|
||||||
from calibre.srv.errors import BookNotFound, HTTPNotFound
|
from calibre.srv.errors import BookNotFound, HTTPBadRequest, HTTPNotFound
|
||||||
from calibre.srv.routes import endpoint, json
|
from calibre.srv.routes import endpoint, json
|
||||||
from calibre.srv.utils import get_db, get_use_roman, http_date
|
from calibre.srv.utils import get_db, get_use_roman, http_date
|
||||||
from calibre.utils.config_base import tweaks
|
from calibre.utils.config_base import tweaks
|
||||||
@ -355,7 +358,14 @@ def get(ctx, rd, what, book_id, library_id):
|
|||||||
raise HTTPNotFound(f'No {what.lower()} format for the book {book_id!r}')
|
raise HTTPNotFound(f'No {what.lower()} format for the book {book_id!r}')
|
||||||
|
|
||||||
|
|
||||||
@endpoint('/get-note/{field}/{item_id}/{library_id=None}')
|
def resource_hash_to_url(ctx, scheme, digest, library_id):
|
||||||
|
kw = {'scheme': scheme, 'digest': digest}
|
||||||
|
if library_id:
|
||||||
|
kw['library_id'] = library_id
|
||||||
|
return ctx.url_for('/get-note-resource', **kw)
|
||||||
|
|
||||||
|
|
||||||
|
@endpoint('/get-note/{field}/{item_id}/{library_id=None}', types={'item_id': int})
|
||||||
def get_note(ctx, rd, field, item_id, library_id):
|
def get_note(ctx, rd, field, item_id, library_id):
|
||||||
db = get_db(ctx, rd, library_id)
|
db = get_db(ctx, rd, library_id)
|
||||||
if db is None:
|
if db is None:
|
||||||
@ -374,10 +384,7 @@ def get_note(ctx, rd, field, item_id, library_id):
|
|||||||
pat = re.compile(rf'{RESOURCE_URL_SCHEME}://({{}})'.format('|'.join(map(r, resources))))
|
pat = re.compile(rf'{RESOURCE_URL_SCHEME}://({{}})'.format('|'.join(map(r, resources))))
|
||||||
def sub(m):
|
def sub(m):
|
||||||
s, d = m.group(1).split('/', 1)
|
s, d = m.group(1).split('/', 1)
|
||||||
kw = {'scheme': s, 'digest': d}
|
return resource_hash_to_url(ctx, s, d, library_id)
|
||||||
if library_id:
|
|
||||||
kw['library_id'] = library_id
|
|
||||||
return ctx.url_for('/get-note-resource', **kw)
|
|
||||||
note_data['doc'] = pat.sub(sub, html)
|
note_data['doc'] = pat.sub(sub, html)
|
||||||
rd.outheaders['Content-Type'] = 'text/html; charset=UTF-8'
|
rd.outheaders['Content-Type'] = 'text/html; charset=UTF-8'
|
||||||
rd.outheaders['Last-Modified'] = http_date(note_data['mtime'])
|
rd.outheaders['Last-Modified'] = http_date(note_data['mtime'])
|
||||||
@ -398,3 +405,50 @@ def get_note_resource(ctx, rd, scheme, digest, library_id):
|
|||||||
fname_for_content_disposition(name), fname_for_content_disposition(name, as_encoded_unicode=True))
|
fname_for_content_disposition(name), fname_for_content_disposition(name, as_encoded_unicode=True))
|
||||||
rd.outheaders['Last-Modified'] = http_date(d['mtime'])
|
rd.outheaders['Last-Modified'] = http_date(d['mtime'])
|
||||||
return d['data']
|
return d['data']
|
||||||
|
|
||||||
|
|
||||||
|
@endpoint('/set-note/{field}/{item_id}/{library_id=None}', needs_db_write=True, methods={'POST'}, types={'item_id': int})
|
||||||
|
def set_note(ctx, rd, field, item_id, library_id):
|
||||||
|
db = get_db(ctx, rd, library_id)
|
||||||
|
if db is None:
|
||||||
|
raise HTTPNotFound(f'Library {library_id} not found')
|
||||||
|
try:
|
||||||
|
data = load_json_file(rd.request_body_file)
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
raise Exception('note data must be a dict')
|
||||||
|
html, searchable_text, images = data['html'], data['searchable_text'], data['images']
|
||||||
|
except Exception as err:
|
||||||
|
raise HTTPBadRequest(f'Invalid query: {err}')
|
||||||
|
srv_replacements = {}
|
||||||
|
db_replacements = {}
|
||||||
|
resources = []
|
||||||
|
res_pat = re.compile(r'get-note-resource/([a-zA-Z0-9]+)/([a-zA-Z0-9]+)')
|
||||||
|
for key, img in images.items():
|
||||||
|
try:
|
||||||
|
is_new_image = img['data'].startswith('data:')
|
||||||
|
if is_new_image:
|
||||||
|
d = img['data'].encode('ascii')
|
||||||
|
idx = d.index(b',')
|
||||||
|
d = memoryview(d)[idx:]
|
||||||
|
img_data = base64.standard_b64decode(d)
|
||||||
|
fname = img['filename']
|
||||||
|
else:
|
||||||
|
m = res_pat.search(img['data'])
|
||||||
|
scheme, digest = m.group(1), m.group(2)
|
||||||
|
resources.append(f'{scheme}:{digest}')
|
||||||
|
except Exception as err:
|
||||||
|
raise HTTPBadRequest(f'Invalid query: {err}')
|
||||||
|
if is_new_image:
|
||||||
|
chash = db.add_notes_resource(img_data, fname)
|
||||||
|
scheme, digest = chash.split(':', 1)
|
||||||
|
resources.append(chash)
|
||||||
|
srv_replacements[key] = resource_hash_to_url(ctx, scheme, digest, library_id)
|
||||||
|
db_replacements[key] = f'{RESOURCE_URL_SCHEME}://{scheme}/{digest}'
|
||||||
|
db_html = srv_html = html
|
||||||
|
if db_replacements:
|
||||||
|
db_html = re.sub('|'.join(map(re.escape, db_replacements)), lambda m: db_replacements[m.group()], html)
|
||||||
|
if srv_replacements:
|
||||||
|
srv_html = re.sub('|'.join(map(re.escape, srv_replacements)), lambda m: srv_replacements[m.group()], html)
|
||||||
|
db.set_notes_for(field, item_id, db_html, searchable_text, resources, True)
|
||||||
|
rd.outheaders['Content-Type'] = 'text/html; charset=UTF-8'
|
||||||
|
return srv_html
|
||||||
|
@ -419,21 +419,16 @@ class CommentsEditorBoss:
|
|||||||
document.body.lastChild.innerHTML = data.html
|
document.body.lastChild.innerHTML = data.html
|
||||||
|
|
||||||
def get_html(self, data):
|
def get_html(self, data):
|
||||||
c = document.body.lastChild
|
c = document.body.lastChild.cloneNode(True)
|
||||||
images = v'{}'
|
images = v'{}'
|
||||||
for img in c.getElementByTagName('img'):
|
if data.extract_images:
|
||||||
if img.src and img.src.startsWith('data:'):
|
for img in c.getElementsByTagName('img'):
|
||||||
key = short_uuid4()
|
if img.src:
|
||||||
images[key] = {'data': img.src, 'filename': img.dataset.filename}
|
key = short_uuid4()
|
||||||
img.src = key
|
images[key] = {'data': img.src, 'filename': img.dataset.filename}
|
||||||
v'delete img.dataset.filename'
|
img.src = key
|
||||||
self.comm.send_message('html', html=c.innerHTML, extra_data={'images': images})
|
v'delete img.dataset.filename'
|
||||||
for img in c.getElementByTagName('img'):
|
self.comm.send_message('html', html=c.innerHTML, extra_data={'images': images, 'text': c.innerText})
|
||||||
if img.src:
|
|
||||||
d = images[img.src]
|
|
||||||
if d:
|
|
||||||
img.src = d.data
|
|
||||||
img.dataset.filename = d.filename
|
|
||||||
|
|
||||||
def exec_command(self, data):
|
def exec_command(self, data):
|
||||||
document.execCommand(data.name, False, data.value)
|
document.execCommand(data.name, False, data.value)
|
||||||
@ -469,6 +464,7 @@ class Editor:
|
|||||||
self.id = iframe.id
|
self.id = iframe.id
|
||||||
self.ready = False
|
self.ready = False
|
||||||
self.pending_set_html = None
|
self.pending_set_html = None
|
||||||
|
self.pending_get_html = v'[]'
|
||||||
self.get_html_callbacks = v'[]'
|
self.get_html_callbacks = v'[]'
|
||||||
self.iframe_obj = iframe
|
self.iframe_obj = iframe
|
||||||
|
|
||||||
@ -496,8 +492,9 @@ class Editor:
|
|||||||
if self.pending_set_html is not None:
|
if self.pending_set_html is not None:
|
||||||
self.set_html(self.pending_set_html)
|
self.set_html(self.pending_set_html)
|
||||||
self.pending_set_html = None
|
self.pending_set_html = None
|
||||||
if self.get_html_callbacks.length:
|
if self.pending_get_html.length > 0:
|
||||||
self.get_html(self.get_html_callback)
|
for x in self.pending_get_html:
|
||||||
|
self.get_html(x.proceed, x.extract_images)
|
||||||
|
|
||||||
def set_html(self, html):
|
def set_html(self, html):
|
||||||
if not self.ready:
|
if not self.ready:
|
||||||
@ -506,15 +503,17 @@ class Editor:
|
|||||||
rgba = get_color_as_rgba('window-foreground')
|
rgba = get_color_as_rgba('window-foreground')
|
||||||
self.iframe_wrapper.send_message('set_html', html=html, color_scheme=color_scheme(), color=f'rgba({rgba[0]},{rgba[1]},{rgba[2]},{rgba[3]})')
|
self.iframe_wrapper.send_message('set_html', html=html, color_scheme=color_scheme(), color=f'rgba({rgba[0]},{rgba[1]},{rgba[2]},{rgba[3]})')
|
||||||
|
|
||||||
def get_html(self, proceed):
|
def get_html(self, proceed, extract_images):
|
||||||
self.get_html_callbacks.push(proceed)
|
self.get_html_callbacks.push(proceed)
|
||||||
if self.ready:
|
if self.ready:
|
||||||
self.iframe_wrapper.send_message('get_html')
|
self.iframe_wrapper.send_message('get_html', extract_images=v'!!extract_images')
|
||||||
|
else:
|
||||||
|
self.pending_get_html.push({'proceed': proceed, 'extract_images': v'!!extract_images'})
|
||||||
|
|
||||||
def on_html_received(self, data):
|
def on_html_received(self, data):
|
||||||
if self.get_html_callbacks.length:
|
if self.get_html_callbacks.length:
|
||||||
for f in self.get_html_callbacks:
|
for f in self.get_html_callbacks:
|
||||||
f(data.html, data.img_urls)
|
f(data.html, data.extra_data)
|
||||||
self.get_html_callbacks = v'[]'
|
self.get_html_callbacks = v'[]'
|
||||||
|
|
||||||
def exec_command(self, name, value=None):
|
def exec_command(self, name, value=None):
|
||||||
@ -607,11 +606,11 @@ def set_comments_html(container, html):
|
|||||||
editor.set_html(html or '')
|
editor.set_html(html or '')
|
||||||
|
|
||||||
|
|
||||||
def get_comments_html(container, proceed):
|
def get_comments_html(container, proceed, extract_images):
|
||||||
iframe = container.querySelector('iframe')
|
iframe = container.querySelector('iframe')
|
||||||
eid = iframe.getAttribute('id')
|
eid = iframe.getAttribute('id')
|
||||||
editor = registry[eid]
|
editor = registry[eid]
|
||||||
editor.get_html(proceed)
|
editor.get_html(proceed, extract_images)
|
||||||
|
|
||||||
|
|
||||||
def develop(container):
|
def develop(container):
|
||||||
|
@ -13,6 +13,7 @@ from book_list.router import back, home, show_note
|
|||||||
from book_list.top_bar import add_button, create_top_bar
|
from book_list.top_bar import add_button, create_top_bar
|
||||||
from book_list.ui import set_panel_handler
|
from book_list.ui import set_panel_handler
|
||||||
from gettext import gettext as _
|
from gettext import gettext as _
|
||||||
|
from modals import ajax_send_progress_dialog, error_dialog
|
||||||
from utils import parse_url_params, safe_set_inner_html, sandboxed_html
|
from utils import parse_url_params, safe_set_inner_html, sandboxed_html
|
||||||
|
|
||||||
|
|
||||||
@ -144,11 +145,29 @@ def save(container_id):
|
|||||||
c = document.getElementById(container_id)
|
c = document.getElementById(container_id)
|
||||||
if not c:
|
if not c:
|
||||||
return
|
return
|
||||||
get_comments_html(c, def(html, extra_data):
|
q = parse_url_params()
|
||||||
print(html)
|
url = 'set-note/' + encodeURIComponent(q.field) + '/' + encodeURIComponent(q.item_id)
|
||||||
console.log(extra_data)
|
if q.library_id:
|
||||||
print('TODO: Implement saving')
|
url += '/' + encodeURIComponent(q.library_id)
|
||||||
)
|
close_action = get_close_action()[0]
|
||||||
|
|
||||||
|
def on_complete(end_type, xhr, evt):
|
||||||
|
nonlocal current_note_markup
|
||||||
|
if end_type is 'abort':
|
||||||
|
close_action()
|
||||||
|
return
|
||||||
|
if end_type is not 'load':
|
||||||
|
error_dialog(_('Failed to send notes to server'), _(
|
||||||
|
'Updating the notes for: {} failed.').format(q.item), xhr.error_html)
|
||||||
|
return
|
||||||
|
q.html = xhr.responseText
|
||||||
|
current_note_markup = q
|
||||||
|
close_action()
|
||||||
|
|
||||||
|
def on_got_html(html, extra_data):
|
||||||
|
ajax_send_progress_dialog(url, {'html': html, 'searchable_text': extra_data.text, 'images': extra_data.images}, on_complete, _('Sending note data to calibre server...'))
|
||||||
|
|
||||||
|
get_comments_html(c, on_got_html, True)
|
||||||
|
|
||||||
|
|
||||||
def init_edit(container_id):
|
def init_edit(container_id):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user