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'
|
||||
__copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
|
||||
import base64
|
||||
import errno
|
||||
import os
|
||||
import re
|
||||
from contextlib import suppress
|
||||
from functools import partial
|
||||
from io import BytesIO
|
||||
from json import load as load_json_file
|
||||
from threading import Lock
|
||||
|
||||
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.opf2 import metadata_to_opf
|
||||
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.utils import get_db, get_use_roman, http_date
|
||||
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}')
|
||||
|
||||
|
||||
@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):
|
||||
db = get_db(ctx, rd, library_id)
|
||||
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))))
|
||||
def sub(m):
|
||||
s, d = m.group(1).split('/', 1)
|
||||
kw = {'scheme': s, 'digest': d}
|
||||
if library_id:
|
||||
kw['library_id'] = library_id
|
||||
return ctx.url_for('/get-note-resource', **kw)
|
||||
return resource_hash_to_url(ctx, s, d, library_id)
|
||||
note_data['doc'] = pat.sub(sub, html)
|
||||
rd.outheaders['Content-Type'] = 'text/html; charset=UTF-8'
|
||||
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))
|
||||
rd.outheaders['Last-Modified'] = http_date(d['mtime'])
|
||||
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
|
||||
|
||||
def get_html(self, data):
|
||||
c = document.body.lastChild
|
||||
c = document.body.lastChild.cloneNode(True)
|
||||
images = v'{}'
|
||||
for img in c.getElementByTagName('img'):
|
||||
if img.src and img.src.startsWith('data:'):
|
||||
key = short_uuid4()
|
||||
images[key] = {'data': img.src, 'filename': img.dataset.filename}
|
||||
img.src = key
|
||||
v'delete img.dataset.filename'
|
||||
self.comm.send_message('html', html=c.innerHTML, extra_data={'images': images})
|
||||
for img in c.getElementByTagName('img'):
|
||||
if img.src:
|
||||
d = images[img.src]
|
||||
if d:
|
||||
img.src = d.data
|
||||
img.dataset.filename = d.filename
|
||||
if data.extract_images:
|
||||
for img in c.getElementsByTagName('img'):
|
||||
if img.src:
|
||||
key = short_uuid4()
|
||||
images[key] = {'data': img.src, 'filename': img.dataset.filename}
|
||||
img.src = key
|
||||
v'delete img.dataset.filename'
|
||||
self.comm.send_message('html', html=c.innerHTML, extra_data={'images': images, 'text': c.innerText})
|
||||
|
||||
def exec_command(self, data):
|
||||
document.execCommand(data.name, False, data.value)
|
||||
@ -469,6 +464,7 @@ class Editor:
|
||||
self.id = iframe.id
|
||||
self.ready = False
|
||||
self.pending_set_html = None
|
||||
self.pending_get_html = v'[]'
|
||||
self.get_html_callbacks = v'[]'
|
||||
self.iframe_obj = iframe
|
||||
|
||||
@ -496,8 +492,9 @@ class Editor:
|
||||
if self.pending_set_html is not None:
|
||||
self.set_html(self.pending_set_html)
|
||||
self.pending_set_html = None
|
||||
if self.get_html_callbacks.length:
|
||||
self.get_html(self.get_html_callback)
|
||||
if self.pending_get_html.length > 0:
|
||||
for x in self.pending_get_html:
|
||||
self.get_html(x.proceed, x.extract_images)
|
||||
|
||||
def set_html(self, html):
|
||||
if not self.ready:
|
||||
@ -506,15 +503,17 @@ class Editor:
|
||||
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]})')
|
||||
|
||||
def get_html(self, proceed):
|
||||
def get_html(self, proceed, extract_images):
|
||||
self.get_html_callbacks.push(proceed)
|
||||
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):
|
||||
if self.get_html_callbacks.length:
|
||||
for f in self.get_html_callbacks:
|
||||
f(data.html, data.img_urls)
|
||||
f(data.html, data.extra_data)
|
||||
self.get_html_callbacks = v'[]'
|
||||
|
||||
def exec_command(self, name, value=None):
|
||||
@ -607,11 +606,11 @@ def set_comments_html(container, html):
|
||||
editor.set_html(html or '')
|
||||
|
||||
|
||||
def get_comments_html(container, proceed):
|
||||
def get_comments_html(container, proceed, extract_images):
|
||||
iframe = container.querySelector('iframe')
|
||||
eid = iframe.getAttribute('id')
|
||||
editor = registry[eid]
|
||||
editor.get_html(proceed)
|
||||
editor.get_html(proceed, extract_images)
|
||||
|
||||
|
||||
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.ui import set_panel_handler
|
||||
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
|
||||
|
||||
|
||||
@ -144,11 +145,29 @@ def save(container_id):
|
||||
c = document.getElementById(container_id)
|
||||
if not c:
|
||||
return
|
||||
get_comments_html(c, def(html, extra_data):
|
||||
print(html)
|
||||
console.log(extra_data)
|
||||
print('TODO: Implement saving')
|
||||
)
|
||||
q = parse_url_params()
|
||||
url = 'set-note/' + encodeURIComponent(q.field) + '/' + encodeURIComponent(q.item_id)
|
||||
if q.library_id:
|
||||
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):
|
||||
|
Loading…
x
Reference in New Issue
Block a user