mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Content server: Implement deleting of books via the web interface
This commit is contained in:
parent
ce22a0da00
commit
27a062cb19
@ -11,6 +11,7 @@ import sys
|
||||
from calibre import as_unicode
|
||||
from calibre.constants import is_running_from_develop, isosx, iswindows, plugins
|
||||
from calibre.db.legacy import LibraryDatabase
|
||||
from calibre.db.delete_service import shutdown as shutdown_delete_service
|
||||
from calibre.srv.bonjour import BonJour
|
||||
from calibre.srv.handler import Handler
|
||||
from calibre.srv.http_response import create_http_handler
|
||||
@ -220,4 +221,7 @@ def main(args=sys.argv):
|
||||
# Needed for dynamic cover generation, which uses Qt for drawing
|
||||
from calibre.gui2 import ensure_app, load_builtin_fonts
|
||||
ensure_app(), load_builtin_fonts()
|
||||
server.serve_forever()
|
||||
try:
|
||||
server.serve_forever()
|
||||
finally:
|
||||
shutdown_delete_service()
|
||||
|
@ -82,6 +82,17 @@ def list_duplicate_book(container, container_id, job_id, data, file):
|
||||
container.appendChild(b)
|
||||
|
||||
|
||||
def write_access_error(msg, xhr):
|
||||
html = msg + '<br>'
|
||||
if xhr.status is 403:
|
||||
un = get_interface_data().username
|
||||
if un:
|
||||
html += _('You are not allowed to make changes to the library') + '<hr>'
|
||||
else:
|
||||
html += _('You must be logged in to make changes to the library') + '<hr>'
|
||||
return html + '<br>' + xhr.error_html
|
||||
|
||||
|
||||
def on_complete(container_id, job_id, end_type, xhr, ev):
|
||||
idx = state.transfers.indexOf(xhr)
|
||||
if idx > -1:
|
||||
@ -100,14 +111,8 @@ def on_complete(container_id, job_id, end_type, xhr, ev):
|
||||
elif end_type is 'abort':
|
||||
return
|
||||
else:
|
||||
html = _('Failed to upload the file: {}').format(this.name) + '<br>'
|
||||
if xhr.status is 403:
|
||||
un = get_interface_data().username
|
||||
if un:
|
||||
html += _('You are not allowed to make changes to the library') + '<hr>'
|
||||
else:
|
||||
html += _('You must be logged in to make changes to the library') + '<hr>'
|
||||
safe_set_inner_html(container, html + '<br>' + xhr.error_html)
|
||||
html = write_access_error(_('Failed to upload the file: {}').format(this.name), xhr)
|
||||
safe_set_inner_html(container, html)
|
||||
|
||||
|
||||
def fake_send(container_id, job_id):
|
||||
@ -123,7 +128,7 @@ def send_file(file, container_id, job_id, add_duplicates):
|
||||
lid = loaded_books_query().library_id
|
||||
ad = 'y' if add_duplicates else 'n'
|
||||
xhr = ajax_send_file(
|
||||
f'/cdb/add-book/{job_id}/{ad}/{encodeURIComponent(file.name)}/{lid}',
|
||||
f'cdb/add-book/{job_id}/{ad}/{encodeURIComponent(file.name)}/{lid}',
|
||||
file, on_complete.bind(file, container_id, job_id), on_progress.bind(None, container_id, job_id))
|
||||
state.transfers.push(xhr)
|
||||
return xhr
|
||||
|
@ -7,6 +7,7 @@ from elementmaker import E
|
||||
from gettext import gettext as _
|
||||
|
||||
from ajax import ajax, encode_query_component
|
||||
from book_list.delete_book import start_delete_book
|
||||
from book_list.globals import get_session_data
|
||||
from book_list.item_list import create_item, create_item_list
|
||||
from book_list.library_data import (
|
||||
@ -15,7 +16,7 @@ from book_list.library_data import (
|
||||
)
|
||||
from book_list.router import back, home, open_book
|
||||
from book_list.theme import get_color, get_font_size
|
||||
from book_list.top_bar import add_button, create_top_bar, set_title, clear_buttons
|
||||
from book_list.top_bar import add_button, clear_buttons, create_top_bar, set_title
|
||||
from book_list.ui import query_as_href, set_panel_handler, show_panel
|
||||
from book_list.views import search_query_for
|
||||
from date import format_date
|
||||
@ -415,6 +416,7 @@ def render_book(container_id, book_id):
|
||||
if not c:
|
||||
return
|
||||
metadata = book_metadata(book_id)
|
||||
render_book.title = metadata.title
|
||||
set_title(c, metadata.title)
|
||||
authors = metadata.authors.join(' & ') if metadata.authors else _('Unknown')
|
||||
alt = _('{} by {}').format(metadata.title, authors)
|
||||
@ -453,6 +455,7 @@ def add_top_bar_buttons(container_id):
|
||||
container = document.getElementById(container_id)
|
||||
if container:
|
||||
clear_buttons(container)
|
||||
add_button(container, 'trash', action=delete_book, tooltip=_('Delete this book'))
|
||||
book_id = parse_url_params().book_id
|
||||
if book_id is '0':
|
||||
add_button(container, 'random', def(): fetch_metadata(container_id, 0);)
|
||||
@ -630,6 +633,10 @@ def search_internet(container_id):
|
||||
|
||||
))
|
||||
|
||||
|
||||
def delete_book():
|
||||
start_delete_book(current_library_id(), render_book.book_id, render_book.title or _('Unknown'))
|
||||
|
||||
set_panel_handler('book_details', init)
|
||||
set_panel_handler('book_details^more_actions', create_more_actions_panel)
|
||||
set_panel_handler('book_details^search_internet', search_internet)
|
||||
|
110
src/pyj/book_list/delete_book.pyj
Normal file
110
src/pyj/book_list/delete_book.pyj
Normal file
@ -0,0 +1,110 @@
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
from __python__ import bound_methods, hash_literals
|
||||
|
||||
from elementmaker import E
|
||||
from gettext import gettext as _
|
||||
|
||||
from ajax import ajax
|
||||
from book_list.add import write_access_error
|
||||
from book_list.library_data import book_after, force_refresh_on_next_load
|
||||
from book_list.router import back
|
||||
from book_list.ui import show_panel
|
||||
from dom import clear
|
||||
from modals import create_custom_dialog
|
||||
from read_book.db import get_db
|
||||
from utils import safe_set_inner_html
|
||||
from widgets import create_button
|
||||
|
||||
|
||||
def delete_from_cache(library_id, book_id, title):
|
||||
db = get_db()
|
||||
db.delete_books_matching(library_id, book_id)
|
||||
|
||||
|
||||
def do_delete_from_library(parent, close_modal, library_id, book_id, title):
|
||||
parent.appendChild(E.div(_('Deleting {0} from library, please wait...').format(title)))
|
||||
|
||||
def oncomplete(end_type, xhr, ev):
|
||||
if end_type is 'load' or end_type is 'abort':
|
||||
close_modal()
|
||||
if end_type is 'load':
|
||||
force_refresh_on_next_load()
|
||||
next_book_id = book_after(book_id)
|
||||
if next_book_id:
|
||||
show_panel('book_details', {'book_id': str(next_book_id), 'library_id': library_id}, True)
|
||||
else:
|
||||
back()
|
||||
return
|
||||
clear(parent)
|
||||
msg = E.div()
|
||||
safe_set_inner_html(msg, write_access_error(_('Failed to delete {0}, with error:').format(title), xhr))
|
||||
parent.appendChild(E.div(
|
||||
msg,
|
||||
E.div(class_='button-box',
|
||||
create_button(_('Close'), None, close_modal, True)
|
||||
)
|
||||
))
|
||||
|
||||
ajax(f'cdb/delete-books/{book_id}/{library_id}', oncomplete, method='POST').send()
|
||||
|
||||
|
||||
def confirm_delete_from_library(library_id, book_id, title):
|
||||
create_custom_dialog(_('Are you sure?'), def(parent, close_modal):
|
||||
|
||||
def action(doit):
|
||||
if doit:
|
||||
clear(parent)
|
||||
do_delete_from_library(parent, close_modal, library_id, book_id, title)
|
||||
else:
|
||||
close_modal()
|
||||
|
||||
msg = _('This will <b>permanently delete</b> <i>{0}</i> from your calibre library. Are you sure?').format(title)
|
||||
m = E.div()
|
||||
safe_set_inner_html(m, msg)
|
||||
parent.appendChild(E.div(
|
||||
m,
|
||||
E.div(class_='button-box',
|
||||
create_button(_('OK'), None, action.bind(None, True)),
|
||||
'\xa0',
|
||||
create_button(_('Cancel'), None, action.bind(None, False), highlight=True),
|
||||
)
|
||||
))
|
||||
)
|
||||
|
||||
|
||||
def choose_which_delete(library_id, book_id, title):
|
||||
|
||||
create_custom_dialog(_('Delete from?'), def(parent, close_modal):
|
||||
|
||||
def action(from_cache, from_library):
|
||||
close_modal()
|
||||
if from_cache:
|
||||
delete_from_cache(library_id, book_id, title)
|
||||
if from_library:
|
||||
confirm_delete_from_library(library_id, book_id, title)
|
||||
|
||||
|
||||
parent.appendChild(E.div(
|
||||
E.div(_('{0} is available both in the browser cache for offline reading and in'
|
||||
' your calibre library. Where do you want to delete it from?').format(title)),
|
||||
E.div(class_='button-box',
|
||||
create_button(_('Cache'), None, action.bind(None, True, False)),
|
||||
'\xa0',
|
||||
create_button(_('Library and cache'), None, action.bind(None, True, True)),
|
||||
)
|
||||
))
|
||||
)
|
||||
|
||||
|
||||
def delete_book_stage2(library_id, book_id, title, has_offline_copies):
|
||||
if has_offline_copies:
|
||||
choose_which_delete(library_id, book_id, title)
|
||||
else:
|
||||
confirm_delete_from_library(library_id, book_id, title)
|
||||
|
||||
|
||||
def start_delete_book(library_id, book_id, title):
|
||||
book_id = int(book_id)
|
||||
db = get_db()
|
||||
db.has_book_matching(library_id, book_id, delete_book_stage2.bind(None, library_id, book_id, title))
|
@ -97,6 +97,16 @@ def current_book_ids():
|
||||
return library_data.previous_book_ids.concat(library_data.search_result.book_ids)
|
||||
|
||||
|
||||
def book_after(book_id):
|
||||
ids = current_book_ids()
|
||||
idx = ids.indexOf(int(book_id))
|
||||
if idx > -1:
|
||||
if idx < ids.length - 1:
|
||||
return ids[idx + 1]
|
||||
if idx > 0: # wrap around
|
||||
return ids[0]
|
||||
|
||||
|
||||
def on_data_loaded(end_type, xhr, ev):
|
||||
load_status.current_fetch = None
|
||||
def bad_load(msg):
|
||||
|
@ -348,6 +348,25 @@ class DB:
|
||||
if cursor:
|
||||
cursor.continue()
|
||||
|
||||
def has_book_matching(self, library_id, book_id, proceed):
|
||||
# Should really be using a multiEntry index to avoid iterating over all
|
||||
# books in JS, but since the number of stored books is not that large,
|
||||
# I can't be bothered.
|
||||
c = self.idb.transaction(['books'], 'readonly').objectStore('books').index('recent_index').openCursor(None, 'prev')
|
||||
c.onerror = def(event):
|
||||
proceed(False)
|
||||
book_id = int(book_id)
|
||||
c.onsuccess = def (ev):
|
||||
cursor = ev.target.result
|
||||
if cursor:
|
||||
book = cursor.value
|
||||
if book.key[0] is library_id and book.key[1] is book_id:
|
||||
proceed(True)
|
||||
return
|
||||
cursor.continue()
|
||||
else:
|
||||
proceed(False)
|
||||
|
||||
def delete_book(self, book, proceed):
|
||||
c = self.idb.transaction(['books', 'files'], 'readwrite')
|
||||
files = c.objectStore('files')
|
||||
@ -366,6 +385,31 @@ class DB:
|
||||
books.delete(book.key)
|
||||
next_step()
|
||||
|
||||
def delete_books_matching(self, library_id, book_id, proceed):
|
||||
c = self.idb.transaction(['books'], 'readonly').objectStore('books').index('recent_index').openCursor(None, 'prev')
|
||||
c.onerror = def(event):
|
||||
pass
|
||||
book_id = int(book_id)
|
||||
matches = v'[]'
|
||||
|
||||
def delete_all():
|
||||
if matches.length:
|
||||
book = matches.pop()
|
||||
self.delete_book(book, delete_all)
|
||||
else:
|
||||
if proceed:
|
||||
proceed()
|
||||
|
||||
c.onsuccess = def (ev):
|
||||
cursor = ev.target.result
|
||||
if cursor:
|
||||
book = cursor.value
|
||||
if book.key[0] is library_id and book.key[1] is book_id:
|
||||
matches.push(book)
|
||||
cursor.continue()
|
||||
else:
|
||||
delete_all()
|
||||
|
||||
|
||||
def get_db(callback, show_read_book_error):
|
||||
if not get_db.ans:
|
||||
|
Loading…
x
Reference in New Issue
Block a user