From 0e63bcc709307f3935a130e42594f89abd9727e0 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 14 Mar 2018 22:53:36 +0530 Subject: [PATCH] Allow changing cover on the EM page --- src/calibre/srv/cdb.py | 30 ++++++++++++++- src/pyj/book_list/edit_metadata.pyj | 57 +++++++++++++++++++++++++++-- src/pyj/file_uploads.pyj | 16 +++++--- 3 files changed, 94 insertions(+), 9 deletions(-) diff --git a/src/calibre/srv/cdb.py b/src/calibre/srv/cdb.py index 52c8fd65fc..797488967a 100644 --- a/src/calibre/srv/cdb.py +++ b/src/calibre/srv/cdb.py @@ -5,6 +5,7 @@ from __future__ import absolute_import, division, print_function, unicode_literals import os +from base64 import standard_b64decode from functools import partial from io import BytesIO @@ -13,10 +14,11 @@ from calibre.db.cli import module_for_cmd from calibre.ebooks.metadata.meta import get_metadata from calibre.srv.changes import books_added, books_deleted, metadata from calibre.srv.errors import HTTPBadRequest, HTTPForbidden, HTTPNotFound +from calibre.srv.metadata import book_as_json from calibre.srv.routes import endpoint, json, msgpack_or_json from calibre.srv.utils import get_db, get_library_data +from calibre.utils.imghdr import what from calibre.utils.serialize import MSGPACK_MIME, json_loads, msgpack_loads -from calibre.srv.metadata import book_as_json receive_data_methods = {'GET', 'POST'} @@ -110,6 +112,17 @@ def cdb_delete_book(ctx, rd, book_ids, library_id): return {} +@endpoint('/cdb/set-cover/{book_id}/{library_id=None}', types={'book_id': int}, + needs_db_write=True, postprocess=json, methods=receive_data_methods, cache_control='no-cache') +def cdb_set_cover(ctx, rd, book_id, library_id): + db = get_db(ctx, rd, library_id) + if ctx.restriction_for(rd, db): + raise HTTPForbidden('Cannot use the add book interface with a user who has per library restrictions') + rd.request_body_file.seek(0) + dirtied = db.set_cover({book_id: rd.request_body_file}) + return tuple(dirtied) + + @endpoint('/cdb/set-fields/{book_id}/{library_id=None}', types={'book_id': int}, needs_db_write=True, postprocess=msgpack_or_json, methods=receive_data_methods, cache_control='no-cache') def cdb_set_fields(ctx, rd, book_id, library_id): @@ -130,6 +143,21 @@ def cdb_set_fields(ctx, rd, book_id, library_id): except Exception: raise HTTPBadRequest('Invalid encoded data') dirtied = set() + cdata = changes.pop('cover', False) + if cdata is not False: + if cdata is not None: + try: + cdata = standard_b64decode(cdata.split(',', 1)[-1].encode('ascii')) + except Exception: + raise HTTPBadRequest('Cover data is not valid base64 encoded data') + try: + fmt = what(None, cdata) + except Exception: + fmt = None + if fmt not in ('jpeg', 'png'): + raise HTTPBadRequest('Cover data must be either JPEG or PNG') + dirtied |= db.set_cover({book_id: cdata}) + for field, value in changes.iteritems(): dirtied |= db.set_field(field, {book_id: value}) metadata(dirtied) diff --git a/src/pyj/book_list/edit_metadata.pyj b/src/pyj/book_list/edit_metadata.pyj index b0656cf912..da815f9664 100644 --- a/src/pyj/book_list/edit_metadata.pyj +++ b/src/pyj/book_list/edit_metadata.pyj @@ -12,8 +12,8 @@ from book_list.book_details import ( report_load_failure ) from book_list.library_data import ( - book_metadata, current_library_id, field_names_for, library_data, load_status, - loaded_book_ids, set_book_metadata + book_metadata, cover_url, current_library_id, field_names_for, library_data, + load_status, loaded_book_ids, set_book_metadata ) from book_list.router import back from book_list.theme import get_color @@ -21,6 +21,9 @@ from book_list.top_bar import create_top_bar, set_title from book_list.ui import set_panel_handler, show_panel from date import UNDEFINED_DATE_ISO, format_date from dom import add_extra_css, build_rule, clear, svgicon +from file_uploads import ( + update_status_widget, upload_files_widget, upload_status_widget +) from modals import error_dialog from session import get_interface_data from utils import ( @@ -474,6 +477,29 @@ def bool_edit(container_id, book_id, field, fm, div, mi): return val_map[x] # }}} +# Cover edit {{{ + +def cover_chosen(top_container_id, book_id, container_id, files): + nonlocal has_changes + container = document.getElementById(container_id) + if not container: + return + if not files[0]: + return + file = files[0] + changes.cover = file + has_changes = True + on_close(top_container_id) +cover_chosen.counter = 0 + + +def cover_edit(container_id, book_id, field, fm, div, mi): + upload_files_widget(div, cover_chosen.bind(None, container_id, book_id), _( + 'Change the cover by selecting the cover image or drag and drop of the cover image here.'), + single_file=True, accept_extensions='png jpeg jpg') + +# }}} + def edit_field(container_id, book_id, field): nonlocal value_to_json fm = library_data.field_metadata[field] @@ -491,6 +517,8 @@ def edit_field(container_id, book_id, field): update_completions.prefix = '' if field is 'authors': multiple_line_edit(' & ', '&', container_id, book_id, field, fm, d, mi) + elif field is 'cover': + cover_edit(container_id, book_id, field, fm, d, mi) elif fm.datatype is 'series': series_edit(container_id, book_id, field, fm, d, mi) elif fm.datatype is 'datetime': @@ -710,6 +738,21 @@ def render_metadata(mi, table, container_id, book_id): # {{{ print('Failed to render metadata field: ' + field) traceback.print_exc() + current_edit_action = edit_field.bind(None, container_id, book_id, 'cover') + table.appendChild(E.tr(onclick=current_edit_action, E.td(_('Cover') + ':'), E.td())) + img = E.img( + style='max-width: 300px; max-height: 400px', + ) + if changes.cover: + r = FileReader() + r.onload = def(evt): + img.src = evt.target.result + changes.cover = evt.target.result + r.readAsDataURL(changes.cover) + v'delete changes.cover' + else: + img.src = cover_url(book_id) + table.lastChild.lastChild.appendChild(img) # }}} @@ -735,14 +778,22 @@ def changes_submitted(container_id, book_id, end_type, xhr, ev): on_close(container_id) +def on_progress(container_id, book_id, loaded, total, xhr): + container = document.getElementById(container_id) + if container and total: + update_status_widget(container, loaded, total) + + def submit_changes(container_id, book_id): c = document.getElementById(container_id) d = c.querySelector('div[data-ctype="show"]') clear(d) d.appendChild(E.div(style='margin: 1ex 1rem', _('Uploading changes to server, please wait...'))) data = {'changes': changes, 'loaded_book_ids': loaded_book_ids()} + w = upload_status_widget() + d.appendChild(w) ajax_send( - f'cdb/set-fields/{book_id}/{current_library_id()}', data, changes_submitted.bind(None, container_id, book_id)) + f'cdb/set-fields/{book_id}/{current_library_id()}', data, changes_submitted.bind(None, container_id, book_id), on_progress.bind(None, container_id, book_id)) def show_book(container_id, book_id): diff --git a/src/pyj/file_uploads.pyj b/src/pyj/file_uploads.pyj index 445d5e1d54..492d9686db 100644 --- a/src/pyj/file_uploads.pyj +++ b/src/pyj/file_uploads.pyj @@ -5,11 +5,12 @@ from __python__ import bound_methods, hash_literals from elementmaker import E from gettext import gettext as _ -from utils import safe_set_inner_html, human_readable +from dom import ensure_id +from utils import human_readable, safe_set_inner_html -def upload_files_widget(container, proceed, msg): - container_id = container.id +def upload_files_widget(container, proceed, msg, single_file=False, accept_extensions=None): + container_id = ensure_id(container, 'upload-files') def files_selected(): files = this.files @@ -19,7 +20,11 @@ def upload_files_widget(container, proceed, msg): proceed(container_id, files) msg = msg or _('Upload books by selecting the book files or drag and drop of the files here') - c = E.div(E.span(), E.input(type='file', multiple=True, style='display:none', onchange=files_selected)) + c = E.div(E.span(), E.input(type='file', style='display:none', onchange=files_selected)) + if not single_file: + c.lastChild.setAttribute('multiple', 'multiple') + if accept_extensions: + c.lastChild.setAttribute('accept', ', '.join(['.' + x for x in accept_extensions.split(' ')])) c.style.minHeight = '80vh' c.style.padding = '1rem' c.style.borderBottom = 'solid 1px currentColor' @@ -58,7 +63,8 @@ def update_status_widget(w, sent, total): def upload_status_widget(name, job_id): ans = E.div(style='padding: 1rem 1ex;', data_job='' + job_id) - ans.appendChild(E.h3(E.b(name))) + if name: + ans.appendChild(E.h3(E.b(name))) ans.appendChild(E.progress()) ans.appendChild(E.span()) return ans