Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e732b3b614 | ||
|
|
69f8767be5 | ||
|
|
6cbba6d170 | ||
|
|
888ab4dc8f | ||
|
|
753c19e700 | ||
|
|
4f9390332a | ||
|
|
ffddd85800 | ||
|
|
652b0f20e0 | ||
|
|
598f08b916 | ||
|
|
55cedba693 | ||
|
|
10caf68d8b | ||
|
|
f4ed816f6e | ||
|
|
ba77f41325 | ||
|
|
ea567fcbf0 | ||
|
|
edb05abcbe | ||
|
|
46811219ca | ||
|
|
0aab0cac73 | ||
|
|
3257a36892 | ||
|
|
f10b680b0e | ||
|
|
c2b1950cd5 | ||
|
|
00c8ff8ac4 | ||
|
|
0ca6e86177 | ||
|
|
ab17992ebb | ||
|
|
570371cc82 | ||
|
|
674b47bdbd | ||
|
|
8cff413c4d | ||
|
|
fd744af75d | ||
|
|
6208d5e264 | ||
|
|
42dc36cc10 | ||
|
|
c23d35db4a | ||
|
|
fca580505c | ||
|
|
19da54a7ae | ||
|
|
9c87f0fbde | ||
|
|
e21f943712 | ||
|
|
4bb553f51d | ||
|
|
0615637f58 | ||
|
|
a6c55deac6 | ||
|
|
2de8df7e0d | ||
|
|
4c1d7a9f6b | ||
|
|
9489905602 | ||
|
|
5e7c0c9d34 | ||
|
|
b5da0df42a | ||
|
|
0959f84fd5 | ||
|
|
84777319d7 | ||
|
|
d85bef6c38 | ||
|
|
8ad9f4e3b7 | ||
|
|
cde3888e17 | ||
|
|
387678a771 | ||
|
|
c451daad3c | ||
|
|
224915bba1 | ||
|
|
d598054195 | ||
|
|
088778969d | ||
|
|
0ed4d81da2 | ||
|
|
9478325c23 | ||
|
|
7c715f34dc | ||
|
|
00686ec1f2 | ||
|
|
83d331c89b | ||
|
|
f6338a9fda | ||
|
|
73efef17bd | ||
|
|
74725b68b0 | ||
|
|
efbdd0b6e3 | ||
|
|
1134f54be4 | ||
|
|
2d4ca23d0c | ||
|
|
6157f5027c | ||
|
|
5e48a64b15 | ||
|
|
36a7ff19bc | ||
|
|
1b4f0d0967 | ||
|
|
181117f916 | ||
|
|
32741be6c8 | ||
|
|
f64fe5d835 | ||
|
|
0db93a6036 | ||
|
|
a4bf028537 | ||
|
|
47625a616d | ||
|
|
87635a5ace | ||
|
|
a9713bd497 | ||
|
|
8cef72b847 | ||
|
|
fbed304df8 | ||
|
|
e3bf369ad6 | ||
|
|
d229f71151 | ||
|
|
3aef161cb8 | ||
|
|
cabcace3f0 | ||
|
|
424e493946 | ||
|
|
848302f69e | ||
|
|
8bdd95fc45 | ||
|
|
9c7a834cec | ||
|
|
51a2f36966 | ||
|
|
b64645e5f5 | ||
|
|
bed8957eae | ||
|
|
0ce8c19e88 | ||
|
|
5a1f3d8eec | ||
|
|
315584690d | ||
|
|
f14faff308 | ||
|
|
6f782c5687 | ||
|
|
7f7fc884c2 | ||
|
|
92084bab71 | ||
|
|
f00471baf5 | ||
|
|
9ed0774d19 | ||
|
|
eb20db55a4 | ||
|
|
a91724edb0 | ||
|
|
5ad6b7fa64 | ||
|
|
c1be944a48 | ||
|
|
e2c3c08d65 | ||
|
|
4fece545e4 | ||
|
|
876a11dc58 | ||
|
|
486914905e | ||
|
|
d47af66499 | ||
|
|
832ec80f1a | ||
|
|
85d1aab6a8 | ||
|
|
8db0d004b6 | ||
|
|
c13eac91c8 | ||
|
|
cf3b619c73 | ||
|
|
269ab0e04e | ||
|
|
47168f5ce9 | ||
|
|
73d7306627 | ||
|
|
dd1cbb1018 | ||
|
|
91fdf57b46 | ||
|
|
d77d682ee0 | ||
|
|
e2eed3d311 | ||
|
|
54a47cfc18 | ||
|
|
b5dac9e1a7 | ||
|
|
b00abd283a | ||
|
|
4821b2b34c | ||
|
|
0e31be5357 | ||
|
|
dc19d40db1 | ||
|
|
02b5409b95 | ||
|
|
63cebcd849 | ||
|
|
99e91e622e | ||
|
|
36ef81dd91 | ||
|
|
2700b4f0d1 | ||
|
|
d73f9fdd6f | ||
|
|
be89c25a90 | ||
|
|
c5e9526fc8 | ||
|
|
a99960e6c5 | ||
|
|
8105f54ebb | ||
|
|
d01010812b | ||
|
|
02fbef3dba | ||
|
|
eb8b0096a1 | ||
|
|
38b0af9ea2 | ||
|
|
83b5d0f22b | ||
|
|
38c58bd7c9 | ||
|
|
6276806c0e | ||
|
|
97a8fc661f | ||
|
|
0569c644a3 | ||
|
|
3565e5f068 | ||
|
|
8b8bc1b688 | ||
|
|
ff9dabadf1 | ||
|
|
a75d0caa02 | ||
|
|
eb872865a5 | ||
|
|
8031fff5af | ||
|
|
ac1ae66246 | ||
|
|
310443afc5 | ||
|
|
25c9aba469 | ||
|
|
3a3c8ad2a4 | ||
|
|
10816c7f6d | ||
|
|
adcfd8afe8 | ||
|
|
74a386a284 | ||
|
|
fceca5cc08 | ||
|
|
13b515d3ab | ||
|
|
d2015dd998 | ||
|
|
3959a8eddd | ||
|
|
ca63d343bd | ||
|
|
ecc6e00bfc | ||
|
|
c1114cd786 | ||
|
|
cf01bd7ad4 | ||
|
|
cf1b3bb37f | ||
|
|
70fefa2f11 | ||
|
|
80df8b1fa6 | ||
|
|
cac33165dc | ||
|
|
bf2ab66362 | ||
|
|
5e14b4bf41 | ||
|
|
a6fdd8ab36 | ||
|
|
d091b9d8b9 | ||
|
|
2eb7c0d3f0 | ||
|
|
54eb30e2b9 | ||
|
|
47f44d71cf | ||
|
|
654095054f | ||
|
|
8544702bb2 | ||
|
|
a3b5d2eac7 | ||
|
|
bba7812a07 | ||
|
|
458d0d1e3d | ||
|
|
6d6d5ac097 | ||
|
|
560e8e132d | ||
|
|
3433b62864 | ||
|
|
d8fbf967b3 | ||
|
|
902368efa7 | ||
|
|
c5571fc359 | ||
|
|
132c17151a | ||
|
|
1ce1aee35e | ||
|
|
4dc8948199 | ||
|
|
7db6481704 | ||
|
|
6b11d0b4f3 | ||
|
|
c30d550ee1 | ||
|
|
c902fe1c5a | ||
|
|
a6fe639e2f | ||
|
|
e1bce5d5fe | ||
|
|
5057e68b84 | ||
|
|
f88e776b1f | ||
|
|
957c906e53 | ||
|
|
cfda7f109a | ||
|
|
9fcd636533 | ||
|
|
0e50794d9b | ||
|
|
714c19f558 | ||
|
|
c2d2db913e | ||
|
|
e3537235ca | ||
|
|
dc5663f21f | ||
|
|
dcc091e354 | ||
|
|
4ad747decf | ||
|
|
0d07e445de | ||
|
|
9e7efb187b | ||
|
|
d480c55bcb | ||
|
|
0ff1feaef0 | ||
|
|
0bfe6de62a | ||
|
|
b632f1b9a2 | ||
|
|
9aab6d90f7 | ||
|
|
5cd914f007 | ||
|
|
3a899afcce | ||
|
|
7418c2c0b4 | ||
|
|
e86833a2a9 | ||
|
|
f3a4df1759 | ||
|
|
8938598aab | ||
|
|
7472cdf43f | ||
|
|
4e245def8f | ||
|
|
d32ad1d5ca | ||
|
|
7029fa8b60 | ||
|
|
8c98b91a25 | ||
|
|
feac864e73 | ||
|
|
d75adfe85e | ||
|
|
dbdaa0884e | ||
|
|
1b1b7455d2 | ||
|
|
daaf7337ab | ||
|
|
d08acdc3cc | ||
|
|
fd88dd2383 | ||
|
|
bcb72d780f | ||
|
|
2f6322b0ee | ||
|
|
dbf104787c | ||
|
|
bf31e675db | ||
|
|
7e24890991 | ||
|
|
4cf4c4dfe6 | ||
|
|
e531bc0f0c | ||
|
|
f78a2367a2 | ||
|
|
928ed10c2e | ||
|
|
32d120bd39 | ||
|
|
be5247a2ef | ||
|
|
14bb01010d | ||
|
|
0afba50953 | ||
|
|
24fdadbeb6 | ||
|
|
08527ae3ce | ||
|
|
89e9958222 | ||
|
|
54d9d33e53 | ||
|
|
338441f116 | ||
|
|
31380f2af9 | ||
|
|
7e5d8978ae | ||
|
|
fe78222142 | ||
|
|
de3f883992 | ||
|
|
2afce66486 | ||
|
|
2ae80d3007 | ||
|
|
e31763d388 | ||
|
|
bee6a3593d | ||
|
|
a335dd75a1 | ||
|
|
96fb2c1439 | ||
|
|
9def910924 | ||
|
|
ab3d4e4f21 | ||
|
|
ecda717bed | ||
|
|
4ed0633edf | ||
|
|
34fec0ed97 | ||
|
|
ca9fc74541 | ||
|
|
d9a2a7a1e8 | ||
|
|
d0d9985370 |
@@ -143,6 +143,7 @@ in your Docker run/compose file. Omit this variable for a lightweight image.
|
|||||||
- **Common Issues**:
|
- **Common Issues**:
|
||||||
- If you experience issues starting the application, check the log files located in the `logs` directory for error messages.
|
- If you experience issues starting the application, check the log files located in the `logs` directory for error messages.
|
||||||
- If eBooks fail to load, verify that the `Location of Calibre database` is correctly set and that the database file is accessible.
|
- If eBooks fail to load, verify that the `Location of Calibre database` is correctly set and that the database file is accessible.
|
||||||
|
- You need to enable uploads under `Basic settings` for this option to appear
|
||||||
|
|
||||||
- **Configuration Errors**: Ensure that your Calibre database is compatible and properly formatted. Refer to the Calibre documentation for guidance on maintaining the database.
|
- **Configuration Errors**: Ensure that your Calibre database is compatible and properly formatted. Refer to the Calibre documentation for guidance on maintaining the database.
|
||||||
|
|
||||||
|
|||||||
@@ -42,9 +42,26 @@ def hide_console_windows():
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
if os.name == "nt":
|
# Only hide the console window on Windows when there is no interactive
|
||||||
|
# console attached. This preserves the ability to use Ctrl+C from a
|
||||||
|
# terminal (stdin is a tty) to stop the server.
|
||||||
|
if os.name == "nt" and not sys.stdin.isatty():
|
||||||
hide_console_windows()
|
hide_console_windows()
|
||||||
|
|
||||||
|
try:
|
||||||
main()
|
main()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
# Allow stopping the server with Ctrl+C from an interactive console, after reboot of calibre-web
|
||||||
|
# (initally the SIGINT signal CTRL+C is catched in server.py via the signal callback).
|
||||||
|
try:
|
||||||
|
# Try to perform a graceful stop if web_server is available.
|
||||||
|
# Import here to avoid import-time side-effects when not needed.
|
||||||
|
from cps import web_server
|
||||||
|
web_server.stop()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
print('\nCalibre-Web: received interrupt, shutting down')
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+9
-1
@@ -26,6 +26,7 @@ import os
|
|||||||
import mimetypes
|
import mimetypes
|
||||||
|
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
|
from flask.sessions import SecureCookieSessionInterface
|
||||||
from .MyLoginManager import MyLoginManager
|
from .MyLoginManager import MyLoginManager
|
||||||
from flask_principal import Principal
|
from flask_principal import Principal
|
||||||
|
|
||||||
@@ -110,12 +111,19 @@ web_server = WebServer()
|
|||||||
updater_thread = Updater()
|
updater_thread = Updater()
|
||||||
|
|
||||||
if limiter_present:
|
if limiter_present:
|
||||||
limiter = Limiter(key_func=True, headers_enabled=True, auto_check=False, swallow_errors=False)
|
limiter = Limiter(key_func=True, headers_enabled=True, in_memory_fallback_enabled=True, default_limits=[],
|
||||||
|
swallow_errors=True)
|
||||||
else:
|
else:
|
||||||
limiter = None
|
limiter = None
|
||||||
|
|
||||||
|
class ScriptNameSessionInterface(SecureCookieSessionInterface):
|
||||||
|
def get_cookie_path(self, app):
|
||||||
|
# Called once per response, after request context exists
|
||||||
|
return app.wsgi_app.script_name.rstrip("/") or "/"
|
||||||
|
|
||||||
|
|
||||||
def create_app():
|
def create_app():
|
||||||
|
app.session_interface = ScriptNameSessionInterface()
|
||||||
if csrf:
|
if csrf:
|
||||||
csrf.init_app(app)
|
csrf.init_app(app)
|
||||||
|
|
||||||
|
|||||||
+19
-10
@@ -40,7 +40,7 @@ from flask_babel import gettext as _
|
|||||||
from flask_babel import get_locale, format_time, format_datetime, format_timedelta
|
from flask_babel import get_locale, format_time, format_datetime, format_timedelta
|
||||||
from sqlalchemy import and_
|
from sqlalchemy import and_
|
||||||
from sqlalchemy.orm.attributes import flag_modified
|
from sqlalchemy.orm.attributes import flag_modified
|
||||||
from sqlalchemy.exc import IntegrityError, OperationalError, InvalidRequestError
|
from sqlalchemy.exc import IntegrityError, OperationalError, InvalidRequestError, ArgumentError
|
||||||
from sqlalchemy.sql.expression import func, or_, text
|
from sqlalchemy.sql.expression import func, or_, text
|
||||||
|
|
||||||
from . import constants, logger, helper, services, cli_param
|
from . import constants, logger, helper, services, cli_param
|
||||||
@@ -349,8 +349,9 @@ def list_users():
|
|||||||
if sort not in ub.User.__table__.columns.keys():
|
if sort not in ub.User.__table__.columns.keys():
|
||||||
sort = "id"
|
sort = "id"
|
||||||
order = request.args.get("order", "").lower()
|
order = request.args.get("order", "").lower()
|
||||||
|
|
||||||
if sort != "state" and order:
|
if sort != "state" and order:
|
||||||
|
if not order in ["asc", "desc"]:
|
||||||
|
order = "asc"
|
||||||
order = text(sort + " " + order)
|
order = text(sort + " " + order)
|
||||||
elif not state:
|
elif not state:
|
||||||
order = ub.User.id.asc()
|
order = ub.User.id.asc()
|
||||||
@@ -386,13 +387,12 @@ def list_users():
|
|||||||
@user_login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def delete_user():
|
def delete_user():
|
||||||
user_ids = request.form.to_dict(flat=False)
|
user_ids = request.get_json().get("userid")
|
||||||
users = None
|
|
||||||
message = ""
|
message = ""
|
||||||
if "userid[]" in user_ids:
|
try:
|
||||||
users = ub.session.query(ub.User).filter(ub.User.id.in_(user_ids['userid[]'])).all()
|
users = ub.session.query(ub.User).filter(ub.User.id.in_(user_ids)).all()
|
||||||
elif "userid" in user_ids:
|
except (ArgumentError):
|
||||||
users = ub.session.query(ub.User).filter(ub.User.id == user_ids['userid'][0]).all()
|
users = None
|
||||||
count = 0
|
count = 0
|
||||||
errors = list()
|
errors = list()
|
||||||
success = list()
|
success = list()
|
||||||
@@ -408,10 +408,10 @@ def delete_user():
|
|||||||
errors.append({'type': "danger", 'message': str(ex)})
|
errors.append({'type': "danger", 'message': str(ex)})
|
||||||
|
|
||||||
if count == 1:
|
if count == 1:
|
||||||
log.info("User {} deleted".format(user_ids))
|
log.info("User {} deleted".format(user_ids[0]))
|
||||||
success = [{'type': "success", 'message': message}]
|
success = [{'type': "success", 'message': message}]
|
||||||
elif count > 1:
|
elif count > 1:
|
||||||
log.info("Users {} deleted".format(user_ids))
|
log.info("Users {} deleted".format(", ".join([str(user_id) for user_id in user_ids])))
|
||||||
success = [{'type': "success", 'message': _("{} users deleted successfully").format(count)}]
|
success = [{'type': "success", 'message': _("{} users deleted successfully").format(count)}]
|
||||||
success.extend(errors)
|
success.extend(errors)
|
||||||
return make_response(jsonify(success))
|
return make_response(jsonify(success))
|
||||||
@@ -618,6 +618,8 @@ def load_dialogtexts(element_id):
|
|||||||
texts["main"] = _('Do you really want to delete this domain?')
|
texts["main"] = _('Do you really want to delete this domain?')
|
||||||
elif element_id == "btndeluser":
|
elif element_id == "btndeluser":
|
||||||
texts["main"] = _('Do you really want to delete this user?')
|
texts["main"] = _('Do you really want to delete this user?')
|
||||||
|
elif element_id == "btndelbook":
|
||||||
|
texts["main"] = _('Do you really want to delete this book?')
|
||||||
elif element_id == "delete_shelf":
|
elif element_id == "delete_shelf":
|
||||||
texts["main"] = _('Are you sure you want to delete this shelf?')
|
texts["main"] = _('Are you sure you want to delete this shelf?')
|
||||||
elif element_id == "select_locale":
|
elif element_id == "select_locale":
|
||||||
@@ -626,6 +628,10 @@ def load_dialogtexts(element_id):
|
|||||||
texts["main"] = _('Are you sure you want to change visible book languages for selected user(s)?')
|
texts["main"] = _('Are you sure you want to change visible book languages for selected user(s)?')
|
||||||
elif element_id == "role":
|
elif element_id == "role":
|
||||||
texts["main"] = _('Are you sure you want to change the selected role for the selected user(s)?')
|
texts["main"] = _('Are you sure you want to change the selected role for the selected user(s)?')
|
||||||
|
elif element_id == "archive_books":
|
||||||
|
texts["main"] = _('Are you sure you want to change the archive status for the selected book(s)?')
|
||||||
|
elif element_id == "read_books":
|
||||||
|
texts["main"] = _('Are you sure you want to change the read status for the selected book(s)?')
|
||||||
elif element_id == "restrictions":
|
elif element_id == "restrictions":
|
||||||
texts["main"] = _('Are you sure you want to change the selected restrictions for the selected user(s)?')
|
texts["main"] = _('Are you sure you want to change the selected restrictions for the selected user(s)?')
|
||||||
elif element_id == "sidebar_view":
|
elif element_id == "sidebar_view":
|
||||||
@@ -1833,6 +1839,9 @@ def _configuration_update_helper():
|
|||||||
services.goodreads_support.connect(config.config_goodreads_api_key,
|
services.goodreads_support.connect(config.config_goodreads_api_key,
|
||||||
config.config_use_goodreads)
|
config.config_use_goodreads)
|
||||||
|
|
||||||
|
# Google Books API configuration
|
||||||
|
reboot_required |=_config_string(to_save, "config_googlebooks_api_key")
|
||||||
|
|
||||||
_config_int(to_save, "config_updatechannel")
|
_config_int(to_save, "config_updatechannel")
|
||||||
|
|
||||||
# Reverse proxy login configuration
|
# Reverse proxy login configuration
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
from cps.pagination import Pagination
|
|
||||||
from flask import Blueprint
|
from flask import Blueprint
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
from flask_babel import get_locale
|
from flask_babel import get_locale
|
||||||
|
|||||||
+7
-2
@@ -35,8 +35,13 @@ def clean_string(unsafe_text, book_id=0):
|
|||||||
try:
|
try:
|
||||||
if bleach:
|
if bleach:
|
||||||
allowed_tags = list(ALLOWED_TAGS)
|
allowed_tags = list(ALLOWED_TAGS)
|
||||||
allowed_tags.extend(["p", "span", "div", "pre", "br", "h1", "h2", "h3", "h4", "h5", "h6"])
|
allowed_tags.extend(["p", "span", "div", "pre", "br", "h1", "h2", "h3", "h4", "h5", "h6", "img"])
|
||||||
safe_text = clean_html(unsafe_text, tags=set(allowed_tags))
|
allowed_attributes = {
|
||||||
|
"*": ["class", "style"],
|
||||||
|
"a": ["href", "title", "rel"],
|
||||||
|
"img": ["src", "alt", "title", "width", "height"],
|
||||||
|
}
|
||||||
|
safe_text = clean_html(unsafe_text, tags=set(allowed_tags), attributes=allowed_attributes)
|
||||||
else:
|
else:
|
||||||
safe_text = clean_html(unsafe_text)
|
safe_text = clean_html(unsafe_text)
|
||||||
except ParserError as e:
|
except ParserError as e:
|
||||||
|
|||||||
+4
-4
@@ -65,7 +65,7 @@ def _extract_cover_from_archive(original_file_extension, tmp_file_name, rar_exec
|
|||||||
cover_data = extension = None
|
cover_data = extension = None
|
||||||
if original_file_extension.upper() == '.CBZ':
|
if original_file_extension.upper() == '.CBZ':
|
||||||
cf = zipfile.ZipFile(tmp_file_name)
|
cf = zipfile.ZipFile(tmp_file_name)
|
||||||
for name in cf.namelist():
|
for name in sorted(cf.namelist()):
|
||||||
ext = os.path.splitext(name)
|
ext = os.path.splitext(name)
|
||||||
if len(ext) > 1:
|
if len(ext) > 1:
|
||||||
extension = ext[1].lower()
|
extension = ext[1].lower()
|
||||||
@@ -74,7 +74,7 @@ def _extract_cover_from_archive(original_file_extension, tmp_file_name, rar_exec
|
|||||||
break
|
break
|
||||||
elif original_file_extension.upper() == '.CBT':
|
elif original_file_extension.upper() == '.CBT':
|
||||||
cf = tarfile.TarFile(tmp_file_name)
|
cf = tarfile.TarFile(tmp_file_name)
|
||||||
for name in cf.getnames():
|
for name in sorted(cf.getnames()):
|
||||||
ext = os.path.splitext(name)
|
ext = os.path.splitext(name)
|
||||||
if len(ext) > 1:
|
if len(ext) > 1:
|
||||||
extension = ext[1].lower()
|
extension = ext[1].lower()
|
||||||
@@ -85,7 +85,7 @@ def _extract_cover_from_archive(original_file_extension, tmp_file_name, rar_exec
|
|||||||
try:
|
try:
|
||||||
rarfile.UNRAR_TOOL = rar_executable
|
rarfile.UNRAR_TOOL = rar_executable
|
||||||
cf = rarfile.RarFile(tmp_file_name)
|
cf = rarfile.RarFile(tmp_file_name)
|
||||||
for name in cf.namelist():
|
for name in sorted(cf.namelist()):
|
||||||
ext = os.path.splitext(name)
|
ext = os.path.splitext(name)
|
||||||
if len(ext) > 1:
|
if len(ext) > 1:
|
||||||
extension = ext[1].lower()
|
extension = ext[1].lower()
|
||||||
@@ -96,7 +96,7 @@ def _extract_cover_from_archive(original_file_extension, tmp_file_name, rar_exec
|
|||||||
log.error('Rarfile failed with error: {}'.format(ex))
|
log.error('Rarfile failed with error: {}'.format(ex))
|
||||||
elif original_file_extension.upper() == '.CB7' and use_7zip:
|
elif original_file_extension.upper() == '.CB7' and use_7zip:
|
||||||
cf = py7zr.SevenZipFile(tmp_file_name)
|
cf = py7zr.SevenZipFile(tmp_file_name)
|
||||||
for name in cf.getnames():
|
for name in sorted(cf.getnames()):
|
||||||
ext = os.path.splitext(name)
|
ext = os.path.splitext(name)
|
||||||
if len(ext) > 1:
|
if len(ext) > 1:
|
||||||
extension = ext[1].lower()
|
extension = ext[1].lower()
|
||||||
|
|||||||
+8
-4
@@ -84,8 +84,8 @@ class _Settings(_Base):
|
|||||||
config_authors_max = Column(Integer, default=0)
|
config_authors_max = Column(Integer, default=0)
|
||||||
config_read_column = Column(Integer, default=0)
|
config_read_column = Column(Integer, default=0)
|
||||||
config_title_regex = Column(String,
|
config_title_regex = Column(String,
|
||||||
default=r'^(A|The|An|Der|Die|Das|Den|Ein|Eine'
|
default=r"^(A|The|An|Der|Die|Das|Den|Ein|Eine"
|
||||||
r'|Einen|Dem|Des|Einem|Eines|Le|La|Les|L\'|Un|Une)\s+')
|
r"|Einen|Dem|Des|Einem|Eines|Le|La|Les|L'|Un|Une)(\s+|(?<='))")
|
||||||
config_theme = Column(Integer, default=0)
|
config_theme = Column(Integer, default=0)
|
||||||
|
|
||||||
config_log_level = Column(SmallInteger, default=logger.DEFAULT_LOG_LEVEL)
|
config_log_level = Column(SmallInteger, default=logger.DEFAULT_LOG_LEVEL)
|
||||||
@@ -117,6 +117,7 @@ class _Settings(_Base):
|
|||||||
|
|
||||||
config_use_goodreads = Column(Boolean, default=False)
|
config_use_goodreads = Column(Boolean, default=False)
|
||||||
config_goodreads_api_key = Column(String)
|
config_goodreads_api_key = Column(String)
|
||||||
|
config_googlebooks_api_key = Column(String, default='')
|
||||||
config_register_email = Column(Boolean, default=False)
|
config_register_email = Column(Boolean, default=False)
|
||||||
config_login_type = Column(Integer, default=0)
|
config_login_type = Column(Integer, default=0)
|
||||||
|
|
||||||
@@ -325,7 +326,9 @@ class ConfigSQL(object):
|
|||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
storage = {}
|
storage = {}
|
||||||
for k, v in self.__dict__.items():
|
for k, v in self.__dict__.items():
|
||||||
if k[0] != '_' and not k.endswith("_e") and not k == "cli":
|
if k[0] != '_' and not k.endswith("_e") and not k == "cli" \
|
||||||
|
and 'api' not in k.lower() and 'token' not in k.lower() \
|
||||||
|
and 'secret' not in k.lower():
|
||||||
storage[k] = v
|
storage[k] = v
|
||||||
return storage
|
return storage
|
||||||
|
|
||||||
@@ -402,7 +405,7 @@ class ConfigSQL(object):
|
|||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def get_book_path(self):
|
def get_book_path(self):
|
||||||
return self.config_calibre_split_dir if self.config_calibre_split_dir else self.config_calibre_dir
|
return self.config_calibre_split_dir if self.config_calibre_split else self.config_calibre_dir
|
||||||
|
|
||||||
def store_calibre_uuid(self, calibre_db, Library_table):
|
def store_calibre_uuid(self, calibre_db, Library_table):
|
||||||
from . import app
|
from . import app
|
||||||
@@ -582,6 +585,7 @@ def get_encryption_key(key_path):
|
|||||||
try:
|
try:
|
||||||
with open(key_file, "wb") as f:
|
with open(key_file, "wb") as f:
|
||||||
f.write(key)
|
f.write(key)
|
||||||
|
os.chmod(key_file, 0o600)
|
||||||
except PermissionError as e:
|
except PermissionError as e:
|
||||||
error = e
|
error = e
|
||||||
return key, error
|
return key, error
|
||||||
|
|||||||
+6
-3
@@ -37,9 +37,12 @@ STATIC_DIR = os.path.join(BASE_DIR, 'cps', 'static')
|
|||||||
TEMPLATES_DIR = os.path.join(BASE_DIR, 'cps', 'templates')
|
TEMPLATES_DIR = os.path.join(BASE_DIR, 'cps', 'templates')
|
||||||
TRANSLATIONS_DIR = os.path.join(BASE_DIR, 'cps', 'translations')
|
TRANSLATIONS_DIR = os.path.join(BASE_DIR, 'cps', 'translations')
|
||||||
|
|
||||||
# Cache dir - use CACHE_DIR environment variable, otherwise use the default directory: cps/cache
|
# Cache dir - use CACHE_DIRECTORY environment variable, otherwise CACHE_DIR (for backwards compatibility),
|
||||||
|
# otherwise use the default directory: cps/cache
|
||||||
DEFAULT_CACHE_DIR = os.path.join(BASE_DIR, 'cps', 'cache')
|
DEFAULT_CACHE_DIR = os.path.join(BASE_DIR, 'cps', 'cache')
|
||||||
CACHE_DIR = os.environ.get('CACHE_DIR', DEFAULT_CACHE_DIR)
|
CACHE_DIRECTORY = os.environ.get(
|
||||||
|
'CACHE_DIRECTORY', os.environ.get('CACHE_DIR', DEFAULT_CACHE_DIR)
|
||||||
|
)
|
||||||
|
|
||||||
if HOME_CONFIG:
|
if HOME_CONFIG:
|
||||||
home_dir = os.path.join(os.path.expanduser("~"), ".calibre-web")
|
home_dir = os.path.join(os.path.expanduser("~"), ".calibre-web")
|
||||||
@@ -172,7 +175,7 @@ BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, d
|
|||||||
'series_id, languages, publisher, pubdate, identifiers')
|
'series_id, languages, publisher, pubdate, identifiers')
|
||||||
|
|
||||||
# python build process likes to have x.y.zbw -> b for beta and w a counting number
|
# python build process likes to have x.y.zbw -> b for beta and w a counting number
|
||||||
STABLE_VERSION = '0.6.25'
|
STABLE_VERSION = '0.6.27b'
|
||||||
|
|
||||||
NIGHTLY_VERSION = dict()
|
NIGHTLY_VERSION = dict()
|
||||||
NIGHTLY_VERSION[0] = '$Format:%H$'
|
NIGHTLY_VERSION[0] = '$Format:%H$'
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ from flask import abort
|
|||||||
from flask import current_app
|
from flask import current_app
|
||||||
from flask import flash
|
from flask import flash
|
||||||
from flask import g
|
from flask import g
|
||||||
from flask import has_app_context
|
|
||||||
from flask import redirect
|
from flask import redirect
|
||||||
from flask import request
|
from flask import request
|
||||||
from flask import session
|
from flask import session
|
||||||
@@ -469,7 +468,7 @@ class LoginManager:
|
|||||||
config = current_app.config
|
config = current_app.config
|
||||||
cookie_name = config.get("REMEMBER_COOKIE_NAME", COOKIE_NAME)
|
cookie_name = config.get("REMEMBER_COOKIE_NAME", COOKIE_NAME)
|
||||||
domain = config.get("REMEMBER_COOKIE_DOMAIN")
|
domain = config.get("REMEMBER_COOKIE_DOMAIN")
|
||||||
path = config.get("REMEMBER_COOKIE_PATH", "/")
|
path = config.get("REMEMBER_COOKIE_PATH", current_app.wsgi_app.script_name)
|
||||||
|
|
||||||
secure = config.get("REMEMBER_COOKIE_SECURE", COOKIE_SECURE)
|
secure = config.get("REMEMBER_COOKIE_SECURE", COOKIE_SECURE)
|
||||||
httponly = config.get("REMEMBER_COOKIE_HTTPONLY", COOKIE_HTTPONLY)
|
httponly = config.get("REMEMBER_COOKIE_HTTPONLY", COOKIE_HTTPONLY)
|
||||||
@@ -520,36 +519,5 @@ class LoginManager:
|
|||||||
config = current_app.config
|
config = current_app.config
|
||||||
cookie_name = config.get("REMEMBER_COOKIE_NAME", COOKIE_NAME)
|
cookie_name = config.get("REMEMBER_COOKIE_NAME", COOKIE_NAME)
|
||||||
domain = config.get("REMEMBER_COOKIE_DOMAIN")
|
domain = config.get("REMEMBER_COOKIE_DOMAIN")
|
||||||
path = config.get("REMEMBER_COOKIE_PATH", "/")
|
path = config.get("REMEMBER_COOKIE_PATH", current_app.wsgi_app.script_name)
|
||||||
response.delete_cookie(cookie_name, domain=domain, path=path)
|
response.delete_cookie(cookie_name, domain=domain, path=path)
|
||||||
|
|
||||||
@property
|
|
||||||
def _login_disabled(self):
|
|
||||||
"""Legacy property, use app.config['LOGIN_DISABLED'] instead."""
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
warnings.warn(
|
|
||||||
"'_login_disabled' is deprecated and will be removed in"
|
|
||||||
" Flask-Login 0.7. Use 'LOGIN_DISABLED' in 'app.config'"
|
|
||||||
" instead.",
|
|
||||||
DeprecationWarning,
|
|
||||||
stacklevel=2,
|
|
||||||
)
|
|
||||||
|
|
||||||
if has_app_context():
|
|
||||||
return current_app.config.get("LOGIN_DISABLED", False)
|
|
||||||
return False
|
|
||||||
|
|
||||||
@_login_disabled.setter
|
|
||||||
def _login_disabled(self, newvalue):
|
|
||||||
"""Legacy property setter, use app.config['LOGIN_DISABLED'] instead."""
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
warnings.warn(
|
|
||||||
"'_login_disabled' is deprecated and will be removed in"
|
|
||||||
" Flask-Login 0.7. Use 'LOGIN_DISABLED' in 'app.config'"
|
|
||||||
" instead.",
|
|
||||||
DeprecationWarning,
|
|
||||||
stacklevel=2,
|
|
||||||
)
|
|
||||||
current_app.config["LOGIN_DISABLED"] = newvalue
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ from sqlite3 import OperationalError as sqliteOperationalError
|
|||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
from sqlalchemy import Table, Column, ForeignKey, CheckConstraint
|
from sqlalchemy import Table, Column, ForeignKey, CheckConstraint
|
||||||
from sqlalchemy import String, Integer, Boolean, TIMESTAMP, Float
|
from sqlalchemy import String, Integer, Boolean, TIMESTAMP, Float
|
||||||
from sqlalchemy.orm import relationship, sessionmaker, scoped_session
|
from sqlalchemy.orm import relationship, sessionmaker, scoped_session, selectinload
|
||||||
from sqlalchemy.orm.collections import InstrumentedList
|
from sqlalchemy.orm.collections import InstrumentedList
|
||||||
from sqlalchemy.ext.declarative import DeclarativeMeta
|
from sqlalchemy.ext.declarative import DeclarativeMeta
|
||||||
from sqlalchemy.exc import OperationalError
|
from sqlalchemy.exc import OperationalError
|
||||||
@@ -416,8 +416,8 @@ class Books(Base):
|
|||||||
path = Column(String, default="", nullable=False)
|
path = Column(String, default="", nullable=False)
|
||||||
has_cover = Column(Integer, default=0)
|
has_cover = Column(Integer, default=0)
|
||||||
uuid = Column(String)
|
uuid = Column(String)
|
||||||
isbn = Column(String(collation='NOCASE'), default="")
|
# isbn = Column(String(collation='NOCASE'), default="")
|
||||||
flags = Column(Integer, nullable=False, default=1)
|
# flags = Column(Integer, nullable=False, default=1)
|
||||||
|
|
||||||
authors = relationship(Authors, secondary=books_authors_link, backref='books')
|
authors = relationship(Authors, secondary=books_authors_link, backref='books')
|
||||||
tags = relationship(Tags, secondary=books_tags_link, backref='books', order_by="Tags.name")
|
tags = relationship(Tags, secondary=books_tags_link, backref='books', order_by="Tags.name")
|
||||||
@@ -449,7 +449,14 @@ class Books(Base):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def atom_timestamp(self):
|
def atom_timestamp(self):
|
||||||
return self.timestamp.strftime('%Y-%m-%dT%H:%M:%S+00:00') or ''
|
# OPDS atom:updated is defined as "the most recent instant in time
|
||||||
|
# when the entry was modified". Books.timestamp is the date added and
|
||||||
|
# never changes after import, so metadata and cover edits were
|
||||||
|
# invisible to OPDS sync clients. Use last_modified, which Calibre
|
||||||
|
# updates on every metadata or cover change; fall back to timestamp
|
||||||
|
# only if last_modified happens to be missing.
|
||||||
|
t = self.last_modified or self.timestamp
|
||||||
|
return t.strftime('%Y-%m-%dT%H:%M:%S+00:00') if t else ''
|
||||||
|
|
||||||
|
|
||||||
class CustomColumns(Base):
|
class CustomColumns(Base):
|
||||||
@@ -640,8 +647,8 @@ class CalibreDB:
|
|||||||
connect_args={'check_same_thread': False},
|
connect_args={'check_same_thread': False},
|
||||||
poolclass=StaticPool)
|
poolclass=StaticPool)
|
||||||
with check_engine.begin() as connection:
|
with check_engine.begin() as connection:
|
||||||
connection.execute(text("attach database '{}' as calibre;".format(dbpath)))
|
connection.execute(text("attach database '{}' as calibre;".format(dbpath.replace("'", "''"))))
|
||||||
connection.execute(text("attach database '{}' as app_settings;".format(app_db_path)))
|
connection.execute(text("attach database '{}' as app_settings;".format(app_db_path.replace("'", "''"))))
|
||||||
local_session = scoped_session(sessionmaker())
|
local_session = scoped_session(sessionmaker())
|
||||||
local_session.configure(bind=connection)
|
local_session.configure(bind=connection)
|
||||||
database_uuid = local_session().query(Library_Id).one_or_none()
|
database_uuid = local_session().query(Library_Id).one_or_none()
|
||||||
@@ -694,8 +701,8 @@ class CalibreDB:
|
|||||||
poolclass=StaticPool)
|
poolclass=StaticPool)
|
||||||
with engine.begin() as connection:
|
with engine.begin() as connection:
|
||||||
connection.execute(text('PRAGMA cache_size = 10000;'))
|
connection.execute(text('PRAGMA cache_size = 10000;'))
|
||||||
connection.execute(text("attach database '{}' as calibre;".format(dbpath)))
|
connection.execute(text("attach database '{}' as calibre;".format(dbpath.replace("'", "''"))))
|
||||||
connection.execute(text("attach database '{}' as app_settings;".format(app_db_path)))
|
connection.execute(text("attach database '{}' as app_settings;".format(app_db_path.replace("'", "''"))))
|
||||||
|
|
||||||
conn = engine.connect()
|
conn = engine.connect()
|
||||||
# conn.text_factory = lambda b: b.decode(errors = 'ignore') possible fix for #1302
|
# conn.text_factory = lambda b: b.decode(errors = 'ignore') possible fix for #1302
|
||||||
@@ -746,7 +753,8 @@ class CalibreDB:
|
|||||||
.filter(self.common_filters(allow_show_archived)).first())
|
.filter(self.common_filters(allow_show_archived)).first())
|
||||||
|
|
||||||
def get_book_by_uuid(self, book_uuid):
|
def get_book_by_uuid(self, book_uuid):
|
||||||
return self.session.query(Books).filter(Books.uuid == book_uuid).first()
|
return self.session.query(Books).filter(Books.uuid == book_uuid). \
|
||||||
|
filter(self.common_filters()).first()
|
||||||
|
|
||||||
def get_book_format(self, book_id, file_format):
|
def get_book_format(self, book_id, file_format):
|
||||||
return self.session.query(Data).filter(Data.book == book_id).filter(Data.format == file_format).first()
|
return self.session.query(Data).filter(Data.book == book_id).filter(Data.format == file_format).first()
|
||||||
@@ -901,29 +909,36 @@ class CalibreDB:
|
|||||||
for entry in entries:
|
for entry in entries:
|
||||||
if combined:
|
if combined:
|
||||||
sort_authors = entry.Books.author_sort.split('&')
|
sort_authors = entry.Books.author_sort.split('&')
|
||||||
ids = [a.id for a in entry.Books.authors]
|
authors_list = entry.Books.authors
|
||||||
|
|
||||||
else:
|
else:
|
||||||
sort_authors = entry.author_sort.split('&')
|
sort_authors = entry.author_sort.split('&')
|
||||||
ids = [a.id for a in entry.authors]
|
authors_list = entry.authors
|
||||||
authors_ordered = list()
|
|
||||||
# error = False
|
# Create dictionary for O(1) lookup instead of nested loops
|
||||||
|
authors_by_sort = {}
|
||||||
|
authors_by_id = {}
|
||||||
|
for author in authors_list:
|
||||||
|
authors_by_sort[author.sort] = author
|
||||||
|
authors_by_id[author.id] = author
|
||||||
|
|
||||||
|
authors_ordered = []
|
||||||
|
ids_remaining = set(authors_by_id.keys())
|
||||||
|
|
||||||
|
# Order authors based on sort field using dictionary lookup
|
||||||
for auth in sort_authors:
|
for auth in sort_authors:
|
||||||
auth = strip_whitespaces(auth)
|
auth = strip_whitespaces(auth)
|
||||||
results = self.session.query(Authors).filter(Authors.sort == auth).all()
|
if auth in authors_by_sort:
|
||||||
# ToDo: How to handle not found author name
|
author = authors_by_sort[auth]
|
||||||
if not len(results):
|
authors_ordered.append(author)
|
||||||
book_id = entry.id if isinstance(entry, Books) else entry[0].id
|
ids_remaining.discard(author.id)
|
||||||
log.error("Author '{}' of book {} not found to display name in right order".format(auth, book_id))
|
else:
|
||||||
# error = True
|
# This can happen if author_sort has stale data or formatting issues
|
||||||
break
|
book_id = entry.id if isinstance(entry, Books) else (entry.Books.id if combined else entry.id)
|
||||||
for r in results:
|
log.warning("Author '{}' of book {} not found in author list, skipping in sort order".format(auth, book_id))
|
||||||
if r.id in ids:
|
|
||||||
authors_ordered.append(r)
|
# Add any remaining authors not in sort order
|
||||||
ids.remove(r.id)
|
for author_id in ids_remaining:
|
||||||
for author_id in ids:
|
authors_ordered.append(authors_by_id[author_id])
|
||||||
result = self.session.query(Authors).filter(Authors.id == author_id).first()
|
|
||||||
authors_ordered.append(result)
|
|
||||||
|
|
||||||
if list_return:
|
if list_return:
|
||||||
if combined:
|
if combined:
|
||||||
@@ -937,16 +952,13 @@ class CalibreDB:
|
|||||||
def get_typeahead(self, database, query, replace=('', ''), tag_filter=true()):
|
def get_typeahead(self, database, query, replace=('', ''), tag_filter=true()):
|
||||||
query = query or ''
|
query = query or ''
|
||||||
self.create_functions()
|
self.create_functions()
|
||||||
# self.session.connection().connection.connection.create_function("lower", 1, lcase)
|
|
||||||
entries = self.session.query(database).filter(tag_filter). \
|
entries = self.session.query(database).filter(tag_filter). \
|
||||||
filter(func.lower(database.name).ilike("%" + query + "%")).all()
|
filter(func.lower(database.name).ilike("%" + query + "%")).all()
|
||||||
# json_dumps = json.dumps([dict(name=escape(r.name.replace(*replace))) for r in entries])
|
|
||||||
json_dumps = json.dumps([dict(name=r.name.replace(*replace)) for r in entries])
|
json_dumps = json.dumps([dict(name=r.name.replace(*replace)) for r in entries])
|
||||||
return json_dumps
|
return json_dumps
|
||||||
|
|
||||||
def check_exists_book(self, authr, title):
|
def check_exists_book(self, authr, title):
|
||||||
self.create_functions()
|
self.create_functions()
|
||||||
# self.session.connection().connection.connection.create_function("lower", 1, lcase)
|
|
||||||
q = list()
|
q = list()
|
||||||
author_terms = re.split(r'\s*&\s*', authr)
|
author_terms = re.split(r'\s*&\s*', authr)
|
||||||
for author_term in author_terms:
|
for author_term in author_terms:
|
||||||
@@ -956,36 +968,93 @@ class CalibreDB:
|
|||||||
.filter(and_(Books.authors.any(and_(*q)), func.lower(Books.title).ilike("%" + title + "%"))).first()
|
.filter(and_(Books.authors.any(and_(*q)), func.lower(Books.title).ilike("%" + title + "%"))).first()
|
||||||
|
|
||||||
def search_query(self, term, config, *join):
|
def search_query(self, term, config, *join):
|
||||||
strip_whitespaces(term).lower()
|
term = strip_whitespaces(term).lower()
|
||||||
self.create_functions()
|
self.create_functions()
|
||||||
# self.session.connection().connection.connection.create_function("lower", 1, lcase)
|
|
||||||
q = list()
|
|
||||||
author_terms = re.split("[, ]+", term)
|
|
||||||
for author_term in author_terms:
|
|
||||||
q.append(Books.authors.any(func.lower(Authors.name).ilike("%" + author_term + "%")))
|
|
||||||
query = self.generate_linked_query(config.config_read_column, Books)
|
|
||||||
if len(join) == 6:
|
|
||||||
query = query.outerjoin(join[0], join[1]).outerjoin(join[2]).outerjoin(join[3], join[4]).outerjoin(join[5])
|
|
||||||
if len(join) == 3:
|
|
||||||
query = query.outerjoin(join[0], join[1]).outerjoin(join[2])
|
|
||||||
elif len(join) == 2:
|
|
||||||
query = query.outerjoin(join[0], join[1])
|
|
||||||
elif len(join) == 1:
|
|
||||||
query = query.outerjoin(join[0])
|
|
||||||
|
|
||||||
|
# Try FTS5 search first for better performance
|
||||||
|
fts_ids = None
|
||||||
|
# Check if FTS5 table exists before attempting search
|
||||||
|
if not hasattr(self, '_fts_available'):
|
||||||
|
try:
|
||||||
|
result = self.session.execute(
|
||||||
|
text("SELECT name FROM sqlite_master WHERE type='table' AND name='books_fts'")
|
||||||
|
).fetchone()
|
||||||
|
self._fts_available = result is not None
|
||||||
|
except Exception:
|
||||||
|
self._fts_available = False
|
||||||
|
|
||||||
|
if self._fts_available:
|
||||||
|
try:
|
||||||
|
# Escape FTS5 special characters to prevent query errors
|
||||||
|
term_fts = term.replace('"', '""')
|
||||||
|
# Wrap in quotes for phrase matching and better accuracy
|
||||||
|
fts_results = self.session.execute(
|
||||||
|
text("SELECT DISTINCT rowid FROM books_fts WHERE books_fts MATCH :term"),
|
||||||
|
{"term": f'"{term_fts}"'}
|
||||||
|
).fetchall()
|
||||||
|
if fts_results:
|
||||||
|
fts_ids = [r[0] for r in fts_results]
|
||||||
|
except Exception as ex:
|
||||||
|
# FTS5 query failed, fall back to traditional search
|
||||||
|
log.debug("FTS5 search failed for term '{}', using fallback: {}".format(term, ex))
|
||||||
|
|
||||||
|
# Build base query with optimized joins
|
||||||
|
base_query = self.generate_linked_query(config.config_read_column, Books)
|
||||||
|
base_query = base_query.filter(self.common_filters(True))
|
||||||
|
|
||||||
|
# Apply eager loading for authors to avoid N+1 queries
|
||||||
|
base_query = base_query.options(selectinload(Books.authors))
|
||||||
|
|
||||||
|
if len(join) == 6:
|
||||||
|
base_query = base_query.outerjoin(join[0], join[1]).outerjoin(join[2]).outerjoin(join[3], join[4]).outerjoin(join[5])
|
||||||
|
if len(join) == 3:
|
||||||
|
base_query = base_query.outerjoin(join[0], join[1]).outerjoin(join[2])
|
||||||
|
elif len(join) == 2:
|
||||||
|
base_query = base_query.outerjoin(join[0], join[1])
|
||||||
|
elif len(join) == 1:
|
||||||
|
base_query = base_query.outerjoin(join[0])
|
||||||
|
|
||||||
|
# If FTS5 found results, use those IDs
|
||||||
|
if fts_ids:
|
||||||
|
return base_query.filter(Books.id.in_(fts_ids))
|
||||||
|
|
||||||
|
# Fallback to traditional search with optimized subqueries
|
||||||
|
author_terms = re.split("[, ]+", term)
|
||||||
|
|
||||||
|
# Use subquery for authors to avoid expensive .any() with OR
|
||||||
|
author_subquery = self.session.query(books_authors_link.c.book).join(
|
||||||
|
Authors, books_authors_link.c.author == Authors.id
|
||||||
|
)
|
||||||
|
author_filters = []
|
||||||
|
for author_term in author_terms:
|
||||||
|
author_filters.append(func.lower(Authors.name).ilike("%" + author_term + "%"))
|
||||||
|
if author_filters:
|
||||||
|
author_subquery = author_subquery.filter(and_(*author_filters))
|
||||||
|
|
||||||
|
# Build optimized filter expressions
|
||||||
cc = self.get_cc_columns(config, filter_config_custom_read=True)
|
cc = self.get_cc_columns(config, filter_config_custom_read=True)
|
||||||
filter_expression = [Books.tags.any(func.lower(Tags.name).ilike("%" + term + "%")),
|
filter_expression = [
|
||||||
Books.series.any(func.lower(Series.name).ilike("%" + term + "%")),
|
Books.id.in_(self.session.query(books_tags_link.c.book).join(
|
||||||
Books.authors.any(and_(*q)),
|
Tags, books_tags_link.c.tag == Tags.id
|
||||||
Books.publishers.any(func.lower(Publishers.name).ilike("%" + term + "%")),
|
).filter(func.lower(Tags.name).ilike("%" + term + "%"))),
|
||||||
func.lower(Books.title).ilike("%" + term + "%")]
|
Books.id.in_(self.session.query(books_series_link.c.book).join(
|
||||||
|
Series, books_series_link.c.series == Series.id
|
||||||
|
).filter(func.lower(Series.name).ilike("%" + term + "%"))),
|
||||||
|
Books.id.in_(author_subquery),
|
||||||
|
Books.id.in_(self.session.query(books_publishers_link.c.book).join(
|
||||||
|
Publishers, books_publishers_link.c.publisher == Publishers.id
|
||||||
|
).filter(func.lower(Publishers.name).ilike("%" + term + "%"))),
|
||||||
|
func.lower(Books.title).ilike("%" + term + "%")
|
||||||
|
]
|
||||||
|
|
||||||
for c in cc:
|
for c in cc:
|
||||||
if c.datatype not in ["datetime", "rating", "bool", "int", "float"]:
|
if c.datatype not in ["datetime", "rating", "bool", "int", "float"]:
|
||||||
filter_expression.append(
|
filter_expression.append(
|
||||||
getattr(Books,
|
getattr(Books,
|
||||||
'custom_column_' + str(c.id)).any(
|
'custom_column_' + str(c.id)).any(
|
||||||
func.lower(cc_classes[c.id].value).ilike("%" + term + "%")))
|
func.lower(cc_classes[c.id].value).ilike("%" + term + "%")))
|
||||||
return query.filter(self.common_filters(True)).filter(or_(*filter_expression))
|
|
||||||
|
return base_query.filter(or_(*filter_expression))
|
||||||
|
|
||||||
def get_cc_columns(self, config, filter_config_custom_read=False):
|
def get_cc_columns(self, config, filter_config_custom_read=False):
|
||||||
tmp_cc = self.session.query(CustomColumns).filter(CustomColumns.datatype.notin_(cc_exceptions)).all()
|
tmp_cc = self.session.query(CustomColumns).filter(CustomColumns.datatype.notin_(cc_exceptions)).all()
|
||||||
@@ -1007,18 +1076,32 @@ class CalibreDB:
|
|||||||
def get_search_results(self, term, config, offset=None, order=None, limit=None, *join):
|
def get_search_results(self, term, config, offset=None, order=None, limit=None, *join):
|
||||||
order = order[0] if order else [Books.sort]
|
order = order[0] if order else [Books.sort]
|
||||||
pagination = None
|
pagination = None
|
||||||
result = self.search_query(term, config, *join).order_by(*order).all()
|
|
||||||
result_count = len(result)
|
|
||||||
if offset is not None and limit is not None:
|
if offset is not None and limit is not None:
|
||||||
offset = int(offset)
|
offset = int(offset)
|
||||||
limit_all = offset + int(limit)
|
limit_int = int(limit)
|
||||||
pagination = Pagination((offset / (int(limit)) + 1), limit, result_count)
|
|
||||||
|
# Use LIMIT+1 pattern to estimate total count without expensive count()
|
||||||
|
query = self.search_query(term, config, *join).order_by(*order)
|
||||||
|
result = query.limit(offset + limit_int + 1).all()
|
||||||
|
|
||||||
|
# Check if there are more results
|
||||||
|
has_more = len(result) > (offset + limit_int)
|
||||||
|
if has_more:
|
||||||
|
result_count = offset + limit_int + 1 # Estimate: at least this many
|
||||||
else:
|
else:
|
||||||
offset = 0
|
result_count = len(result)
|
||||||
limit_all = result_count
|
|
||||||
|
# Extract the page of results
|
||||||
|
result = result[offset:offset + limit_int]
|
||||||
|
pagination = Pagination((offset / limit_int + 1), limit_int, result_count)
|
||||||
|
else:
|
||||||
|
# No pagination, fetch all results
|
||||||
|
result = self.search_query(term, config, *join).order_by(*order).all()
|
||||||
|
result_count = len(result)
|
||||||
|
|
||||||
ub.store_combo_ids(result)
|
ub.store_combo_ids(result)
|
||||||
entries = self.order_authors(result[offset:limit_all], list_return=True, combined=True)
|
entries = self.order_authors(result, list_return=True, combined=True)
|
||||||
|
|
||||||
return entries, result_count, pagination
|
return entries, result_count, pagination
|
||||||
|
|
||||||
@@ -1043,7 +1126,7 @@ class CalibreDB:
|
|||||||
.filter(self.common_filters())
|
.filter(self.common_filters())
|
||||||
.count())
|
.count())
|
||||||
if no_lang_count:
|
if no_lang_count:
|
||||||
tags.append([Category(_("None"), "none"), no_lang_count])
|
tags.append([Category(_("None"), "None", "none"), no_lang_count])
|
||||||
return sorted(tags, key=lambda x: x[0].name.lower(), reverse=reverse_order)
|
return sorted(tags, key=lambda x: x[0].name.lower(), reverse=reverse_order)
|
||||||
else:
|
else:
|
||||||
if not languages:
|
if not languages:
|
||||||
@@ -1097,14 +1180,25 @@ def lcase(s):
|
|||||||
return s.lower()
|
return s.lower()
|
||||||
|
|
||||||
|
|
||||||
|
def title_sort(title, config):
|
||||||
|
# calibre sort stuff
|
||||||
|
title_pat = re.compile(config.config_title_regex, re.IGNORECASE)
|
||||||
|
match = title_pat.search(title)
|
||||||
|
if match:
|
||||||
|
prep = match.group(1)
|
||||||
|
title = title[len(prep):] + ', ' + prep
|
||||||
|
return strip_whitespaces(title)
|
||||||
|
|
||||||
class Category:
|
class Category:
|
||||||
name = None
|
name = None
|
||||||
|
sort = None
|
||||||
id = None
|
id = None
|
||||||
count = None
|
count = None
|
||||||
rating = None
|
rating = None
|
||||||
|
|
||||||
def __init__(self, name, cat_id, rating=None):
|
def __init__(self, name, cat_id, rating=None):
|
||||||
self.name = name
|
self.name = name
|
||||||
|
self.sort = name
|
||||||
self.id = cat_id
|
self.id = cat_id
|
||||||
self.rating = rating
|
self.rating = rating
|
||||||
self.count = 1
|
self.count = 1
|
||||||
|
|||||||
+302
-84
@@ -73,17 +73,18 @@ def edit_required(f):
|
|||||||
return inner
|
return inner
|
||||||
|
|
||||||
|
|
||||||
@editbook.route("/ajax/delete/<int:book_id>", methods=["POST"])
|
@editbook.route("/ajax/deletebook", methods=["POST"])
|
||||||
@user_login_required
|
@user_login_required
|
||||||
def delete_book_from_details(book_id):
|
def delete_books_ajax():
|
||||||
return delete_book_from_table(book_id, "", True)
|
book_ids = request.get_json().get("bookid")
|
||||||
|
return check_delete_book(book_ids, "", True)
|
||||||
|
|
||||||
|
|
||||||
@editbook.route("/delete/<int:book_id>", defaults={'book_format': ""}, methods=["POST"])
|
@editbook.route("/delete/<int:book_id>", defaults={'book_format': ""}, methods=["POST"])
|
||||||
@editbook.route("/delete/<int:book_id>/<string:book_format>", methods=["POST"])
|
@editbook.route("/delete/<int:book_id>/<string:book_format>", methods=["POST"])
|
||||||
@user_login_required
|
@user_login_required
|
||||||
def delete_book_ajax(book_id, book_format):
|
def delete_book(book_id, book_format):
|
||||||
return delete_book_from_table(book_id, book_format, False, request.form.to_dict().get('location', ""))
|
return check_delete_book(book_id, book_format, False, request.form.to_dict().get('location', ""))
|
||||||
|
|
||||||
|
|
||||||
@editbook.route("/admin/book/<int:book_id>", methods=['GET'])
|
@editbook.route("/admin/book/<int:book_id>", methods=['GET'])
|
||||||
@@ -161,7 +162,7 @@ def upload():
|
|||||||
return make_response(jsonify(resp))
|
return make_response(jsonify(resp))
|
||||||
else:
|
else:
|
||||||
resp = {"location": url_for('web.show_book', book_id=book_id)}
|
resp = {"location": url_for('web.show_book', book_id=book_id)}
|
||||||
return make_response(jsonify(resp))
|
return Response(json.dumps(resp), mimetype='application/json')
|
||||||
except (OperationalError, IntegrityError, StaleDataError) as e:
|
except (OperationalError, IntegrityError, StaleDataError) as e:
|
||||||
calibre_db.session.rollback()
|
calibre_db.session.rollback()
|
||||||
log.error_or_exception("Database error: {}".format(e))
|
log.error_or_exception("Database error: {}".format(e))
|
||||||
@@ -213,95 +214,239 @@ def table_get_custom_enum(c_id):
|
|||||||
@login_required_if_no_ano
|
@login_required_if_no_ano
|
||||||
@edit_required
|
@edit_required
|
||||||
def edit_list_book(param):
|
def edit_list_book(param):
|
||||||
vals = request.form.to_dict()
|
vals = request.get_json()
|
||||||
book = calibre_db.get_book(vals['pk'])
|
multi = vals.get('multi', False) == "True"
|
||||||
|
ret_value = edit_book_param(param, vals, multi)
|
||||||
|
if isinstance(ret_value, dict):
|
||||||
|
return jsonify(ret_value)
|
||||||
|
else:
|
||||||
|
return ret_value
|
||||||
|
|
||||||
|
@editbook.route("/ajax/editselectedbooks", methods=['POST'])
|
||||||
|
@login_required_if_no_ano
|
||||||
|
@edit_required
|
||||||
|
def edit_selected_books():
|
||||||
|
d = request.get_json()
|
||||||
|
selections = d.get('selections')
|
||||||
|
title = d.get('title')
|
||||||
|
title_sort = d.get('title_sort')
|
||||||
|
author_sort = d.get('author_sort')
|
||||||
|
authors = d.get('authors')
|
||||||
|
categories = d.get('categories')
|
||||||
|
series = d.get('series')
|
||||||
|
languages = d.get('languages')
|
||||||
|
publishers = d.get('publishers')
|
||||||
|
comments = d.get('comments')
|
||||||
|
|
||||||
|
if not (
|
||||||
|
title or title_sort or authors or categories or series or languages or publishers or comments) or not selections:
|
||||||
|
return _("Parameter not found"), 400
|
||||||
|
vals = {
|
||||||
|
"pk": selections,
|
||||||
|
"value": None,
|
||||||
|
"checkA": d.get('checkA'),
|
||||||
|
"checkT": d.get('checkT'),
|
||||||
|
}
|
||||||
|
res = list()
|
||||||
|
if title:
|
||||||
|
vals['value'] = title
|
||||||
|
out = edit_book_param('title', vals, True)
|
||||||
|
if out[0].get('success') != True:
|
||||||
|
res.extend(out)
|
||||||
|
if title_sort:
|
||||||
|
vals['value'] = title_sort
|
||||||
|
out = edit_book_param('sort', vals, True)
|
||||||
|
if out[0].get('success') != True:
|
||||||
|
res.extend(out)
|
||||||
|
if author_sort:
|
||||||
|
vals['value'] = author_sort
|
||||||
|
out = edit_book_param('author_sort', vals, True)
|
||||||
|
if out[0].get('success') != True:
|
||||||
|
res.extend(out)
|
||||||
|
if authors:
|
||||||
|
vals['value'] = authors
|
||||||
|
out = edit_book_param('authors', vals, True)
|
||||||
|
if out[0].get('success') != True:
|
||||||
|
res.extend(out)
|
||||||
|
if categories:
|
||||||
|
vals['value'] = categories
|
||||||
|
out = edit_book_param('tags', vals, True)
|
||||||
|
if out[0].get('success') != True:
|
||||||
|
res.extend(out)
|
||||||
|
if series:
|
||||||
|
vals['value'] = series
|
||||||
|
out = edit_book_param('series', vals, True)
|
||||||
|
if out[0].get('success') != True:
|
||||||
|
res.extend(out)
|
||||||
|
if languages:
|
||||||
|
vals['value'] = languages
|
||||||
|
out = edit_book_param('languages', vals, True)
|
||||||
|
if out[0].get('success') != True:
|
||||||
|
res.extend(out)
|
||||||
|
if publishers:
|
||||||
|
vals['value'] = publishers
|
||||||
|
out = edit_book_param('publishers', vals, True)
|
||||||
|
if out[0].get('success') != True:
|
||||||
|
res.extend(out)
|
||||||
|
if comments:
|
||||||
|
vals['value'] = comments
|
||||||
|
out = edit_book_param('comments', vals, True)
|
||||||
|
if out[0].get('success') != True:
|
||||||
|
res.extend(out)
|
||||||
|
if len(res) == 0:
|
||||||
|
return jsonify([{'success': True, "msg": _("Changes successfully applied")}])
|
||||||
|
else:
|
||||||
|
return jsonify(res)
|
||||||
|
|
||||||
|
# Separated from /editbooks so that /editselectedbooks can also use this
|
||||||
|
#
|
||||||
|
# param: the property of the book to be changed
|
||||||
|
# vals - JSON Object:
|
||||||
|
# {
|
||||||
|
# 'pk': "the book id",
|
||||||
|
# 'value': "changes value of param to what's passed here"
|
||||||
|
# 'checkA': "Optional. Used to check if autosort author is enabled. Assumed as true if not passed"
|
||||||
|
# 'checkT': "Optional. Used to check if autotitle author is enabled. Assumed as true if not passed"
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
@login_required_if_no_ano
|
||||||
|
@edit_required
|
||||||
|
def edit_book_param(param, vals, multi=False):
|
||||||
|
elements = vals.get('pk',[])
|
||||||
|
if vals.get('value', None) is None:
|
||||||
|
return {'success':False, 'msg':_("Value is missing on request")}
|
||||||
|
if not elements or len(elements) > 1 and multi == False:
|
||||||
|
return {"success":False, "msg":_("Oops! Selected book is unavailable. File does not exist or is not accessible")}
|
||||||
|
ret = {}
|
||||||
|
out = list()
|
||||||
|
for elem in elements:
|
||||||
|
book = calibre_db.get_book(elem)
|
||||||
|
if not book:
|
||||||
|
ret = {"success": False,
|
||||||
|
"msg": _("Oops! Selected book is unavailable. File does not exist or is not accessible")}
|
||||||
|
if multi:
|
||||||
|
out.append(ret)
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
return ret
|
||||||
calibre_db.create_functions(config)
|
calibre_db.create_functions(config)
|
||||||
sort_param = ""
|
sort_param = ""
|
||||||
ret = ""
|
|
||||||
try:
|
try:
|
||||||
if param == 'series_index':
|
if param == 'series_index':
|
||||||
edit_book_series_index(vals['value'], book)
|
edit_book_series_index(vals['value'], book)
|
||||||
ret = make_response(jsonify(success=True, newValue=book.series_index))
|
ret = {"success":True,
|
||||||
|
"newValue":book.series_index}
|
||||||
elif param == 'tags':
|
elif param == 'tags':
|
||||||
edit_book_tags(vals['value'], book)
|
edit_book_tags(vals['value'], book)
|
||||||
ret = make_response(jsonify(success=True, newValue=', '.join([tag.name for tag in book.tags])))
|
ret = {"success":True,
|
||||||
|
"newValue":', '.join([tag.name for tag in book.tags])}
|
||||||
elif param == 'series':
|
elif param == 'series':
|
||||||
edit_book_series(vals['value'], book)
|
edit_book_series(vals['value'], book)
|
||||||
ret = make_response(jsonify(success=True, newValue=', '.join([serie.name for serie in book.series])))
|
ret = {"success":True,
|
||||||
|
"newValue":', '.join([serie.name for serie in book.series])}
|
||||||
elif param == 'publishers':
|
elif param == 'publishers':
|
||||||
edit_book_publisher(vals['value'], book)
|
edit_book_publisher(vals['value'], book)
|
||||||
ret = make_response(jsonify(success=True,
|
ret = {"success":True,
|
||||||
newValue=', '.join([publisher.name for publisher in book.publishers])))
|
"newValue":', '.join([publisher.name for publisher in book.publishers])}
|
||||||
elif param == 'languages':
|
elif param == 'languages':
|
||||||
invalid = list()
|
invalid = list()
|
||||||
edit_book_languages(vals['value'], book, invalid=invalid)
|
edit_book_languages(vals['value'], book, invalid=invalid)
|
||||||
if invalid:
|
if invalid:
|
||||||
ret = make_response(jsonify(success=False,
|
ret = {"success": False, "msg": 'Invalid languages in request: {}'.format(','.join(invalid))}
|
||||||
msg='Invalid languages in request: {}'.format(','.join(invalid))))
|
if multi:
|
||||||
|
out.append(ret)
|
||||||
else:
|
else:
|
||||||
lang_names = list()
|
lang_names = list()
|
||||||
for lang in book.languages:
|
for lang in book.languages:
|
||||||
lang_names.append(isoLanguages.get_language_name(get_locale(), lang.lang_code))
|
lang_names.append(isoLanguages.get_language_name(get_locale(), lang.lang_code))
|
||||||
ret = make_response(jsonify(success=True, newValue=', '.join(lang_names)))
|
ret = {"success":True,
|
||||||
|
"newValue":', '.join(lang_names)}
|
||||||
elif param == 'author_sort':
|
elif param == 'author_sort':
|
||||||
book.author_sort = vals['value']
|
book.author_sort = vals['value']
|
||||||
ret = make_response(jsonify(success=True, newValue=book.author_sort))
|
ret = {"success":True,
|
||||||
|
"newValue":book.author_sort}
|
||||||
elif param == 'title':
|
elif param == 'title':
|
||||||
sort_param = book.sort
|
sort_param = book.sort
|
||||||
if handle_title_on_edit(book, vals.get('value', "")):
|
if handle_title_on_edit(book, vals.get('value', "")):
|
||||||
rename_error = helper.update_dir_structure(book.id, config.get_book_path())
|
rename_error = helper.update_dir_structure(book.id, config.get_book_path())
|
||||||
if not rename_error:
|
if not rename_error:
|
||||||
ret = make_response(jsonify(success=True, newValue=book.title))
|
calibre_db.session.commit()
|
||||||
|
ret = {"success":True,
|
||||||
|
"newValue":book.title}
|
||||||
else:
|
else:
|
||||||
ret = make_response(jsonify(success=False, msg=rename_error))
|
calibre_db.session.rollback()
|
||||||
|
ret = {"success":False, "msg":rename_error}
|
||||||
|
if multi:
|
||||||
|
out.append(ret)
|
||||||
elif param == 'sort':
|
elif param == 'sort':
|
||||||
book.sort = vals['value']
|
book.sort = vals['value']
|
||||||
ret = make_response(jsonify(success=True,newValue=book.sort))
|
ret = {"success":True,
|
||||||
|
"newValue":book.sort}
|
||||||
elif param == 'comments':
|
elif param == 'comments':
|
||||||
edit_book_comments(vals['value'], book)
|
edit_book_comments(vals['value'], book)
|
||||||
ret = make_response(jsonify(success=True, newValue=book.comments[0].text))
|
ret = {"success":True,
|
||||||
|
"newValue":book.comments[0].text}
|
||||||
elif param == 'authors':
|
elif param == 'authors':
|
||||||
input_authors, __ = handle_author_on_edit(book, vals['value'], vals.get('checkA', None) == "true")
|
input_authors, __ = handle_author_on_edit(book, vals['value'], vals.get('checkA', None) == True)
|
||||||
rename_error = helper.update_dir_structure(book.id, config.get_book_path(), input_authors[0])
|
rename_error = helper.update_dir_structure(book.id, config.get_book_path(), input_authors[0])
|
||||||
if not rename_error:
|
if not rename_error:
|
||||||
ret = make_response(jsonify(
|
calibre_db.session.commit()
|
||||||
success=True,
|
ret = {"success":True,
|
||||||
newValue=' & '.join([author.replace('|', ',') for author in input_authors])))
|
"newValue":' & '.join([author.replace('|', ',') for author in input_authors])}
|
||||||
else:
|
else:
|
||||||
ret = make_response(jsonify(success=False, msg=rename_error))
|
calibre_db.session.rollback()
|
||||||
|
ret = {"success":False, "msg":rename_error}
|
||||||
|
if multi:
|
||||||
|
out.append(ret)
|
||||||
elif param == 'is_archived':
|
elif param == 'is_archived':
|
||||||
is_archived = change_archived_books(book.id, vals['value'] == "True",
|
is_archived = change_archived_books(book.id, vals['value'] == "True",
|
||||||
message="Book {} archive bit set to: {}".format(book.id, vals['value']))
|
message="Book {} archive bit set to: {}".format(book.id,
|
||||||
|
vals['value']))
|
||||||
if is_archived:
|
if is_archived:
|
||||||
kobo_sync_status.remove_synced_book(book.id)
|
kobo_sync_status.remove_synced_book(book.id)
|
||||||
return ""
|
continue
|
||||||
elif param == 'read_status':
|
elif param == 'read_status':
|
||||||
ret = helper.edit_book_read_status(book.id, vals['value'] == "True")
|
error = helper.edit_book_read_status(book.id, vals['value'] == "True")
|
||||||
if ret:
|
if error:
|
||||||
return ret, 400
|
if multi:
|
||||||
|
out.append({"success":False, "msg":error})
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
return error, 400
|
||||||
|
continue
|
||||||
elif param.startswith("custom_column_"):
|
elif param.startswith("custom_column_"):
|
||||||
new_val = dict()
|
new_val = dict()
|
||||||
new_val[param] = vals['value']
|
new_val[param] = vals['value']
|
||||||
edit_single_cc_data(book.id, book, param[14:], new_val)
|
edit_single_cc_data(book.id, book, param[14:], new_val)
|
||||||
# ToDo: Very hacky find better solution
|
# ToDo: Very hacky find better solution
|
||||||
if vals['value'] in ["True", "False"]:
|
if vals['value'] in ["True", "False"]:
|
||||||
ret = ""
|
ret = {}
|
||||||
else:
|
else:
|
||||||
ret = make_response(jsonify(success=True, newValue=vals['value']))
|
ret = {"success":True, "newValue":vals['value']}
|
||||||
else:
|
else:
|
||||||
|
if multi:
|
||||||
|
out.append({"success":False, "msg":_("Parameter not found")})
|
||||||
|
continue
|
||||||
return _("Parameter not found"), 400
|
return _("Parameter not found"), 400
|
||||||
book.last_modified = datetime.now(timezone.utc)
|
book.last_modified = datetime.now(timezone.utc)
|
||||||
|
|
||||||
calibre_db.session.commit()
|
calibre_db.session.commit()
|
||||||
# revert change for sort if automatic fields link is deactivated
|
# revert change for sort if automatic fields link is deactivated
|
||||||
if param == 'title' and vals.get('checkT') == "false":
|
if param == 'title' and vals.get('checkT') == False:
|
||||||
book.sort = sort_param
|
book.sort = sort_param
|
||||||
calibre_db.session.commit()
|
calibre_db.session.commit()
|
||||||
except (OperationalError, IntegrityError, StaleDataError) as e:
|
except (OperationalError, IntegrityError, StaleDataError, AttributeError) as e:
|
||||||
calibre_db.session.rollback()
|
calibre_db.session.rollback()
|
||||||
log.error_or_exception("Database error: {}".format(e))
|
log.error_or_exception("Database error: {}".format(e))
|
||||||
ret = make_response(jsonify(success=False,
|
ret = {"success":False, "msg":'Database error: {}'.format(e.orig if hasattr(e, "orig") else e)}
|
||||||
msg='Database error: {}'.format(e.orig if hasattr(e, "orig") else e)))
|
if multi:
|
||||||
|
out.append(ret)
|
||||||
|
if multi:
|
||||||
|
if len(out) > 0:
|
||||||
|
return out
|
||||||
|
else:
|
||||||
|
return [ret]
|
||||||
|
else:
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
@@ -337,6 +482,55 @@ def simulate_merge_list_book():
|
|||||||
return make_response(jsonify({'to': to_book, 'from': from_book}))
|
return make_response(jsonify({'to': to_book, 'from': from_book}))
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
@editbook.route("/ajax/displayselectedbooks", methods=['POST'])
|
||||||
|
@user_login_required
|
||||||
|
@edit_required
|
||||||
|
def display_selected_books():
|
||||||
|
vals = request.get_json().get('selections')
|
||||||
|
books = []
|
||||||
|
if vals:
|
||||||
|
for book_id in vals:
|
||||||
|
books.append(calibre_db.get_book(book_id).title)
|
||||||
|
return json.dumps({'books': books})
|
||||||
|
return ""
|
||||||
|
|
||||||
|
@editbook.route("/ajax/archiveselectedbooks", methods=['POST'])
|
||||||
|
@login_required_if_no_ano
|
||||||
|
@edit_required
|
||||||
|
def archive_selected_books():
|
||||||
|
vals = request.get_json().get('selections')
|
||||||
|
state = request.get_json().get('archive')
|
||||||
|
if vals:
|
||||||
|
for book_id in vals:
|
||||||
|
is_archived = change_archived_books(book_id, state,
|
||||||
|
message="Book {} archive bit set to: {}".format(book_id, state))
|
||||||
|
if is_archived:
|
||||||
|
kobo_sync_status.remove_synced_book(book_id)
|
||||||
|
return json.dumps({'success': True})
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
@editbook.route("/ajax/readselectedbooks", methods=['POST'])
|
||||||
|
@user_login_required
|
||||||
|
@edit_required
|
||||||
|
def read_selected_books():
|
||||||
|
vals = request.get_json().get('selections')
|
||||||
|
markAsRead = request.get_json().get('markAsRead')
|
||||||
|
if vals:
|
||||||
|
try:
|
||||||
|
for book_id in vals:
|
||||||
|
ret = helper.edit_book_read_status(book_id, markAsRead)
|
||||||
|
|
||||||
|
except (OperationalError, IntegrityError, StaleDataError) as e:
|
||||||
|
calibre_db.session.rollback()
|
||||||
|
log.error_or_exception("Database error: {}".format(e))
|
||||||
|
ret = Response(json.dumps({'success': False,
|
||||||
|
'msg': 'Database error: {}'.format(e.orig if hasattr(e, "orig") else e)}),
|
||||||
|
mimetype='application/json')
|
||||||
|
|
||||||
|
return json.dumps({'success': True})
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
@editbook.route("/ajax/mergebooks", methods=['POST'])
|
@editbook.route("/ajax/mergebooks", methods=['POST'])
|
||||||
@user_login_required
|
@user_login_required
|
||||||
@@ -371,7 +565,7 @@ def merge_list_book():
|
|||||||
element.format,
|
element.format,
|
||||||
element.uncompressed_size,
|
element.uncompressed_size,
|
||||||
to_name))
|
to_name))
|
||||||
delete_book_from_table(from_book.id, "", True)
|
check_delete_book([from_book.id], "", True)
|
||||||
return make_response(jsonify(success=True))
|
return make_response(jsonify(success=True))
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
@@ -648,8 +842,9 @@ def prepare_authors(authr, calibre_path, gdrive=False):
|
|||||||
all_new_name = helper.get_valid_filename(one_book.title, chars=42) + ' - ' \
|
all_new_name = helper.get_valid_filename(one_book.title, chars=42) + ' - ' \
|
||||||
+ helper.get_valid_filename(renamed_author.name, chars=42)
|
+ helper.get_valid_filename(renamed_author.name, chars=42)
|
||||||
# change location in database to new author/title path
|
# change location in database to new author/title path
|
||||||
helper.rename_all_files_on_change(one_book, new_path, new_path, all_new_name, gdrive)
|
error = helper.rename_all_files_on_change(one_book, new_path, new_path, all_new_name, gdrive)
|
||||||
|
if error:
|
||||||
|
flash(error)
|
||||||
return input_authors
|
return input_authors
|
||||||
|
|
||||||
|
|
||||||
@@ -841,50 +1036,56 @@ def delete_whole_book(book_id, book):
|
|||||||
calibre_db.session.query(db.Books).filter(db.Books.id == book_id).delete()
|
calibre_db.session.query(db.Books).filter(db.Books.id == book_id).delete()
|
||||||
|
|
||||||
|
|
||||||
def render_delete_book_result(book_format, json_response, warning, book_id, location=""):
|
def render_delete_book_result(book_format, book_id, location=""):
|
||||||
if book_format:
|
if book_format:
|
||||||
if json_response:
|
|
||||||
return jsonify([warning, {"location": url_for("edit-book.show_edit_book", book_id=book_id),
|
|
||||||
"type": "success",
|
|
||||||
"format": book_format,
|
|
||||||
"message": _('Book Format Successfully Deleted')}])
|
|
||||||
else:
|
|
||||||
flash(_('Book Format Successfully Deleted'), category="success")
|
flash(_('Book Format Successfully Deleted'), category="success")
|
||||||
return redirect(url_for('edit-book.show_edit_book', book_id=book_id))
|
return redirect(url_for('edit-book.show_edit_book', book_id=book_id))
|
||||||
else:
|
|
||||||
if json_response:
|
|
||||||
return jsonify([warning, {"location": get_redirect_location(location, "web.index"),
|
|
||||||
"type": "success",
|
|
||||||
"format": book_format,
|
|
||||||
"message": _('Book Successfully Deleted')}])
|
|
||||||
else:
|
else:
|
||||||
flash(_('Book Successfully Deleted'), category="success")
|
flash(_('Book Successfully Deleted'), category="success")
|
||||||
return redirect(get_redirect_location(location, "web.index"))
|
return redirect(get_redirect_location(location, "web.index"))
|
||||||
|
|
||||||
|
|
||||||
def delete_book_from_table(book_id, book_format, json_response, location=""):
|
def check_delete_book(book_id, book_format, json_response, location=""):
|
||||||
warning = {}
|
|
||||||
if current_user.role_delete_books():
|
if current_user.role_delete_books():
|
||||||
|
if json_response:
|
||||||
|
# if json response is set, it's possible to delete more than one book, but never a format is deleted
|
||||||
|
res = list()
|
||||||
|
for b in book_id:
|
||||||
|
ret = delete_book_from_table(b)
|
||||||
|
if ret:
|
||||||
|
res.extend([ret])
|
||||||
|
if len(res) == 0:
|
||||||
|
return [{"location": get_redirect_location(location, "web.index"),
|
||||||
|
"type": "success",
|
||||||
|
"format": "",
|
||||||
|
"message": _('Book Successfully Deleted')}]
|
||||||
|
return jsonify(res)
|
||||||
|
else:
|
||||||
|
return delete_book_from_UI(book_id, book_format, location)
|
||||||
|
message = _("You are missing permissions to delete books")
|
||||||
|
if json_response:
|
||||||
|
try:
|
||||||
|
return jsonify({"location": url_for("edit-book.show_edit_book", book_id=int(book_id)),
|
||||||
|
"type": "danger",
|
||||||
|
"format": "",
|
||||||
|
"message": message})
|
||||||
|
except TypeError as e:
|
||||||
|
return jsonify({"location": url_for("web.index"), "type": "danger", "format": "",
|
||||||
|
"message": str(e)})
|
||||||
|
else:
|
||||||
|
flash(message, category="error")
|
||||||
|
return redirect(url_for('edit-book.show_edit_book', book_id=book_id))
|
||||||
|
|
||||||
|
|
||||||
|
def delete_book_from_UI(book_id, book_format, location=""):
|
||||||
book = calibre_db.get_book(book_id)
|
book = calibre_db.get_book(book_id)
|
||||||
if book:
|
if book:
|
||||||
try:
|
try:
|
||||||
result, error = helper.delete_book(book, config.get_book_path(), book_format=book_format.upper())
|
result, error = helper.delete_book(book, config.get_book_path(), book_format=book_format.upper())
|
||||||
if not result:
|
if not result:
|
||||||
if json_response:
|
|
||||||
return jsonify([{"location": url_for("edit-book.show_edit_book", book_id=book_id),
|
|
||||||
"type": "danger",
|
|
||||||
"format": "",
|
|
||||||
"message": error}])
|
|
||||||
else:
|
|
||||||
flash(error, category="error")
|
flash(error, category="error")
|
||||||
return redirect(url_for('edit-book.show_edit_book', book_id=book_id))
|
return redirect(url_for('edit-book.show_edit_book', book_id=book_id))
|
||||||
if error:
|
if error:
|
||||||
if json_response:
|
|
||||||
warning = {"location": url_for("edit-book.show_edit_book", book_id=book_id),
|
|
||||||
"type": "warning",
|
|
||||||
"format": "",
|
|
||||||
"message": error}
|
|
||||||
else:
|
|
||||||
flash(error, category="warning")
|
flash(error, category="warning")
|
||||||
if not book_format:
|
if not book_format:
|
||||||
delete_whole_book(book_id, book)
|
delete_whole_book(book_id, book)
|
||||||
@@ -897,28 +1098,45 @@ def delete_book_from_table(book_id, book_format, json_response, location=""):
|
|||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
log.error_or_exception(ex)
|
log.error_or_exception(ex)
|
||||||
calibre_db.session.rollback()
|
calibre_db.session.rollback()
|
||||||
if json_response:
|
|
||||||
return jsonify([{"location": url_for("edit-book.show_edit_book", book_id=book_id),
|
|
||||||
"type": "danger",
|
|
||||||
"format": "",
|
|
||||||
"message": ex}])
|
|
||||||
else:
|
|
||||||
flash(str(ex), category="error")
|
flash(str(ex), category="error")
|
||||||
return redirect(url_for('edit-book.show_edit_book', book_id=book_id))
|
return redirect(url_for('edit-book.show_edit_book', book_id=book_id))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# book not found
|
# book not found
|
||||||
log.error('Book with id "%s" could not be deleted: not found', book_id)
|
log.error('Book with id "%s" could not be deleted: not found', book_id)
|
||||||
return render_delete_book_result(book_format, json_response, warning, book_id, location)
|
return render_delete_book_result(book_format, book_id, location)
|
||||||
message = _("You are missing permissions to delete books")
|
|
||||||
if json_response:
|
|
||||||
return jsonify({"location": url_for("edit-book.show_edit_book", book_id=book_id),
|
def delete_book_from_table(book_id):
|
||||||
|
book = calibre_db.get_book(book_id)
|
||||||
|
if book:
|
||||||
|
try:
|
||||||
|
result, error = helper.delete_book(book, config.get_book_path(), book_format="")
|
||||||
|
if not result:
|
||||||
|
return {"location": url_for("edit-book.show_edit_book", book_id=book_id),
|
||||||
"type": "danger",
|
"type": "danger",
|
||||||
"format": "",
|
"format": "",
|
||||||
"message": message})
|
"message": error}
|
||||||
|
delete_whole_book(book_id, book)
|
||||||
|
calibre_db.session.commit()
|
||||||
|
if error:
|
||||||
|
return {"location": url_for("edit-book.show_edit_book", book_id=book_id),
|
||||||
|
"type": "warning",
|
||||||
|
"format": "",
|
||||||
|
"message": error}
|
||||||
|
except Exception as ex:
|
||||||
|
log.error_or_exception(ex)
|
||||||
|
calibre_db.session.rollback()
|
||||||
|
return {"location": url_for("edit-book.show_edit_book", book_id=book_id),
|
||||||
|
"type": "danger",
|
||||||
|
"format": "",
|
||||||
|
"message": ex}
|
||||||
else:
|
else:
|
||||||
flash(message, category="error")
|
# book not found
|
||||||
return redirect(url_for('edit-book.show_edit_book', book_id=book_id))
|
log.error('Book with id "%s" could not be deleted: not found', book_id)
|
||||||
|
return {"location": url_for("edit-book.show_edit_book", book_id=book_id),
|
||||||
|
"type": "danger",
|
||||||
|
"format": "",
|
||||||
|
"message": _('Book with id "{}" could not be deleted: not found'.format(book_id))}
|
||||||
|
|
||||||
|
|
||||||
def render_edit_book(book_id):
|
def render_edit_book(book_id):
|
||||||
@@ -1393,7 +1611,7 @@ def add_objects(db_book_object, db_object, db_session, db_type, add_elements):
|
|||||||
if db_type == 'author':
|
if db_type == 'author':
|
||||||
new_element = db_object(add_element, helper.get_sorted_author(add_element.replace('|', ',')))
|
new_element = db_object(add_element, helper.get_sorted_author(add_element.replace('|', ',')))
|
||||||
elif db_type == 'series':
|
elif db_type == 'series':
|
||||||
new_element = db_object(add_element, add_element)
|
new_element = db_object(add_element, db.title_sort(add_element, config))
|
||||||
elif db_type == 'custom':
|
elif db_type == 'custom':
|
||||||
new_element = db_object(value=add_element)
|
new_element = db_object(value=add_element)
|
||||||
elif db_type == 'publisher':
|
elif db_type == 'publisher':
|
||||||
@@ -1424,7 +1642,7 @@ def create_objects_for_addition(db_element, add_element, db_type):
|
|||||||
elif db_type == 'series':
|
elif db_type == 'series':
|
||||||
if db_element.name != add_element:
|
if db_element.name != add_element:
|
||||||
db_element.name = add_element
|
db_element.name = add_element
|
||||||
db_element.sort = add_element
|
db_element.sort = db.title_sort(add_element, config)
|
||||||
elif db_type == 'author':
|
elif db_type == 'author':
|
||||||
if db_element.name != add_element:
|
if db_element.name != add_element:
|
||||||
db_element.name = add_element
|
db_element.name = add_element
|
||||||
|
|||||||
+2
-1
@@ -36,7 +36,8 @@ def do_calibre_export(book_id, book_format):
|
|||||||
if config.config_calibre_split:
|
if config.config_calibre_split:
|
||||||
my_env['CALIBRE_OVERRIDE_DATABASE_PATH'] = os.path.join(config.config_calibre_dir, "metadata.db")
|
my_env['CALIBRE_OVERRIDE_DATABASE_PATH'] = os.path.join(config.config_calibre_dir, "metadata.db")
|
||||||
library_path = config.get_book_path()
|
library_path = config.get_book_path()
|
||||||
opf_command = [calibredb_binarypath, 'export', '--dont-write-opf', '--with-library', library_path,
|
opf_command = [calibredb_binarypath, 'export', '--dont-write-opf', '--dont-save-cover',
|
||||||
|
'--with-library', library_path,
|
||||||
'--to-dir', tmp_dir, '--formats', book_format, "--template", "{}".format(temp_file_name),
|
'--to-dir', tmp_dir, '--formats', book_format, "--template", "{}".format(temp_file_name),
|
||||||
str(book_id)]
|
str(book_id)]
|
||||||
p = process_open(opf_command, quotes, my_env)
|
p = process_open(opf_command, quotes, my_env)
|
||||||
|
|||||||
+1
-1
@@ -175,7 +175,7 @@ def parse_epub_cover(ns, tree, epub_zip, cover_path, tmp_file_path):
|
|||||||
for cs in cover_section:
|
for cs in cover_section:
|
||||||
if cs.endswith('.xhtml') or cs.endswith('.html'):
|
if cs.endswith('.xhtml') or cs.endswith('.html'):
|
||||||
markup = epub_zip.read(os.path.join(cover_path, cs))
|
markup = epub_zip.read(os.path.join(cover_path, cs))
|
||||||
markup_tree = etree.fromstring(markup)
|
markup_tree = etree.fromstring(markup, parser=etree.XMLParser(resolve_entities=False, no_network=True))
|
||||||
# no matter xhtml or html with no namespace
|
# no matter xhtml or html with no namespace
|
||||||
img_src = markup_tree.xpath("//*[local-name() = 'img']/@src")
|
img_src = markup_tree.xpath("//*[local-name() = 'img']/@src")
|
||||||
# Alternative image source
|
# Alternative image source
|
||||||
|
|||||||
+6
-2
@@ -53,16 +53,20 @@ def updateEpub(src, dest, filename, data, ):
|
|||||||
zf.writestr(filename, data)
|
zf.writestr(filename, data)
|
||||||
|
|
||||||
|
|
||||||
|
# Safe parser: disable entity resolution and network access to prevent XXE attacks
|
||||||
|
_safe_parser = etree.XMLParser(resolve_entities=False, no_network=True)
|
||||||
|
|
||||||
|
|
||||||
def get_content_opf(file_path, ns=None):
|
def get_content_opf(file_path, ns=None):
|
||||||
if ns is None:
|
if ns is None:
|
||||||
ns = default_ns
|
ns = default_ns
|
||||||
epubZip = zipfile.ZipFile(file_path)
|
epubZip = zipfile.ZipFile(file_path)
|
||||||
txt = epubZip.read('META-INF/container.xml')
|
txt = epubZip.read('META-INF/container.xml')
|
||||||
tree = etree.fromstring(txt)
|
tree = etree.fromstring(txt, parser=_safe_parser)
|
||||||
cf_name = tree.xpath('n:rootfiles/n:rootfile/@full-path', namespaces=ns)[0]
|
cf_name = tree.xpath('n:rootfiles/n:rootfile/@full-path', namespaces=ns)[0]
|
||||||
cf = epubZip.read(cf_name)
|
cf = epubZip.read(cf_name)
|
||||||
|
|
||||||
return etree.fromstring(cf), cf_name
|
return etree.fromstring(cf, parser=_safe_parser), cf_name
|
||||||
|
|
||||||
|
|
||||||
def create_new_metadata_backup(book, custom_columns, export_language, translated_cover_name, lang_type=3):
|
def create_new_metadata_backup(book, custom_columns, export_language, translated_cover_name, lang_type=3):
|
||||||
|
|||||||
+37
-4
@@ -18,21 +18,29 @@
|
|||||||
|
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
from flask import render_template
|
from flask import render_template, request, flash, make_response
|
||||||
|
from flask_limiter import RateLimitExceeded
|
||||||
|
from flask_babel import gettext as _
|
||||||
from werkzeug.exceptions import default_exceptions
|
from werkzeug.exceptions import default_exceptions
|
||||||
|
|
||||||
|
from .cw_login import current_user
|
||||||
try:
|
try:
|
||||||
from werkzeug.exceptions import FailedDependency
|
from werkzeug.exceptions import FailedDependency
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from werkzeug.exceptions import UnprocessableEntity as FailedDependency
|
from werkzeug.exceptions import UnprocessableEntity as FailedDependency
|
||||||
|
|
||||||
from . import config, app, logger, services
|
from . import config, app, logger, services
|
||||||
|
from .render_template import render_title_template
|
||||||
|
from .web import render_login
|
||||||
|
from .usermanagement import auth
|
||||||
|
from cps.string_helper import strip_whitespaces
|
||||||
|
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
|
|
||||||
# custom error page
|
# custom error page
|
||||||
|
|
||||||
def error_http(error):
|
def error_http(error):
|
||||||
|
headers = {'WWW-Authenticate': 'Basic realm="calibre-web"'} if error.code == 401 else {}
|
||||||
return render_template('http_error.html',
|
return render_template('http_error.html',
|
||||||
error_code="Error {0}".format(error.code),
|
error_code="Error {0}".format(error.code),
|
||||||
error_name=error.name,
|
error_name=error.name,
|
||||||
@@ -40,7 +48,7 @@ def error_http(error):
|
|||||||
goto_admin=False,
|
goto_admin=False,
|
||||||
unconfigured=not config.db_configured,
|
unconfigured=not config.db_configured,
|
||||||
instance=config.config_calibre_web_title
|
instance=config.config_calibre_web_title
|
||||||
), error.code
|
), error.code, headers
|
||||||
|
|
||||||
|
|
||||||
def internal_error(error):
|
def internal_error(error):
|
||||||
@@ -56,6 +64,13 @@ def internal_error(error):
|
|||||||
error_stack="",
|
error_stack="",
|
||||||
instance=config.config_calibre_web_title
|
instance=config.config_calibre_web_title
|
||||||
), 500
|
), 500
|
||||||
|
log.error("500 Internal Server Error: %s", traceback.format_exc())
|
||||||
|
error_stack = ""
|
||||||
|
try:
|
||||||
|
if current_user.is_authenticated and current_user.role_admin():
|
||||||
|
error_stack = traceback.format_exc().split("\n")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
return render_template('http_error.html',
|
return render_template('http_error.html',
|
||||||
error_code="500 Internal Server Error",
|
error_code="500 Internal Server Error",
|
||||||
error_name='The server encountered an internal error and was unable to complete your '
|
error_name='The server encountered an internal error and was unable to complete your '
|
||||||
@@ -63,7 +78,7 @@ def internal_error(error):
|
|||||||
issue=True,
|
issue=True,
|
||||||
goto_admin=False,
|
goto_admin=False,
|
||||||
unconfigured=False,
|
unconfigured=False,
|
||||||
error_stack=traceback.format_exc().split("\n"),
|
error_stack=error_stack,
|
||||||
instance=config.config_calibre_web_title
|
instance=config.config_calibre_web_title
|
||||||
), 500
|
), 500
|
||||||
|
|
||||||
@@ -84,3 +99,21 @@ def init_errorhandler():
|
|||||||
log.debug('LDAP server not accessible while trying to login to opds feed')
|
log.debug('LDAP server not accessible while trying to login to opds feed')
|
||||||
return error_http(FailedDependency())
|
return error_http(FailedDependency())
|
||||||
|
|
||||||
|
|
||||||
|
@app.errorhandler(RateLimitExceeded)
|
||||||
|
def handle_rate_limit(__):
|
||||||
|
log.error("Rate limit exceeded {}".format(request.endpoint))
|
||||||
|
if "register" in request.endpoint:
|
||||||
|
flash(_(u"Please wait one minute to register next user"), category="error")
|
||||||
|
return render_title_template('register.html', config=config, title=_("Register"), page="register")
|
||||||
|
elif "login" in request.endpoint:
|
||||||
|
form = request.form.to_dict()
|
||||||
|
username = strip_whitespaces(form.get('username', "")).lower().replace("\n", "").replace("\r", "")
|
||||||
|
flash(_("Please wait one minute before next login"), category="error")
|
||||||
|
return render_login(username, form.get("password", ""))
|
||||||
|
elif "opds" in request.endpoint:
|
||||||
|
return auth.auth_error_callback(429)
|
||||||
|
else:
|
||||||
|
return make_response('', 429)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+4
-1
@@ -20,6 +20,9 @@ from lxml import etree
|
|||||||
|
|
||||||
from .constants import BookMeta
|
from .constants import BookMeta
|
||||||
|
|
||||||
|
# Safe parser: disable entity resolution and network access to prevent XXE attacks
|
||||||
|
_safe_parser = etree.XMLParser(resolve_entities=False, no_network=True)
|
||||||
|
|
||||||
|
|
||||||
def get_fb2_info(tmp_file_path, original_file_extension):
|
def get_fb2_info(tmp_file_path, original_file_extension):
|
||||||
|
|
||||||
@@ -29,7 +32,7 @@ def get_fb2_info(tmp_file_path, original_file_extension):
|
|||||||
}
|
}
|
||||||
|
|
||||||
fb2_file = open(tmp_file_path, encoding="utf-8")
|
fb2_file = open(tmp_file_path, encoding="utf-8")
|
||||||
tree = etree.fromstring(fb2_file.read().encode())
|
tree = etree.fromstring(fb2_file.read().encode(), parser=_safe_parser)
|
||||||
|
|
||||||
authors = tree.xpath('/fb:FictionBook/fb:description/fb:title-info/fb:author', namespaces=ns)
|
authors = tree.xpath('/fb:FictionBook/fb:description/fb:title-info/fb:author', namespaces=ns)
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from . import logger
|
from . import logger
|
||||||
from .constants import CACHE_DIR
|
from .constants import CACHE_DIRECTORY
|
||||||
from os import makedirs, remove
|
from os import makedirs, remove
|
||||||
from os.path import isdir, isfile, join
|
from os.path import isdir, isfile, join
|
||||||
from shutil import rmtree
|
from shutil import rmtree
|
||||||
@@ -25,7 +25,7 @@ from shutil import rmtree
|
|||||||
|
|
||||||
class FileSystem:
|
class FileSystem:
|
||||||
_instance = None
|
_instance = None
|
||||||
_cache_dir = CACHE_DIR
|
_cache_dir = CACHE_DIRECTORY
|
||||||
|
|
||||||
def __new__(cls):
|
def __new__(cls):
|
||||||
if cls._instance is None:
|
if cls._instance is None:
|
||||||
|
|||||||
+1
-1
@@ -140,7 +140,7 @@ try:
|
|||||||
if response:
|
if response:
|
||||||
dbpath = os.path.join(config.config_calibre_dir, "metadata.db").encode()
|
dbpath = os.path.join(config.config_calibre_dir, "metadata.db").encode()
|
||||||
if not response['deleted'] and response['file']['title'] == 'metadata.db' \
|
if not response['deleted'] and response['file']['title'] == 'metadata.db' \
|
||||||
and response['file']['md5Checksum'] != hashlib.md5(dbpath): # nosec
|
and response['file']['md5Checksum'] != hashlib.md5(dbpath).hexdigest(): # nosec
|
||||||
tmp_dir = get_temp_dir()
|
tmp_dir = get_temp_dir()
|
||||||
|
|
||||||
log.info('Database file updated')
|
log.info('Database file updated')
|
||||||
|
|||||||
+36
-36
@@ -30,7 +30,7 @@ import requests
|
|||||||
import unidecode
|
import unidecode
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from flask import send_from_directory, make_response, abort, url_for, Response, request
|
from flask import send_from_directory, make_response, abort, url_for, Response, request, after_this_request
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
from flask_babel import lazy_gettext as N_
|
from flask_babel import lazy_gettext as N_
|
||||||
from flask_babel import get_locale
|
from flask_babel import get_locale
|
||||||
@@ -307,18 +307,16 @@ def edit_book_read_status(book_id, read_status=None):
|
|||||||
if not config.config_read_column:
|
if not config.config_read_column:
|
||||||
book = ub.session.query(ub.ReadBook).filter(and_(ub.ReadBook.user_id == int(current_user.id),
|
book = ub.session.query(ub.ReadBook).filter(and_(ub.ReadBook.user_id == int(current_user.id),
|
||||||
ub.ReadBook.book_id == book_id)).first()
|
ub.ReadBook.book_id == book_id)).first()
|
||||||
if book:
|
if not book:
|
||||||
|
read_book = ub.ReadBook(user_id=current_user.id, book_id=book_id)
|
||||||
|
book = read_book
|
||||||
if read_status is None:
|
if read_status is None:
|
||||||
if book.read_status == ub.ReadBook.STATUS_FINISHED:
|
if book.read_status == ub.ReadBook.STATUS_FINISHED:
|
||||||
book.read_status = ub.ReadBook.STATUS_UNREAD
|
book.read_status = ub.ReadBook.STATUS_UNREAD
|
||||||
else:
|
else:
|
||||||
book.read_status = ub.ReadBook.STATUS_FINISHED
|
book.read_status = ub.ReadBook.STATUS_FINISHED
|
||||||
else:
|
else:
|
||||||
book.read_status = ub.ReadBook.STATUS_FINISHED if read_status else ub.ReadBook.STATUS_UNREAD
|
book.read_status = ub.ReadBook.STATUS_FINISHED if read_status == True else ub.ReadBook.STATUS_UNREAD
|
||||||
else:
|
|
||||||
read_book = ub.ReadBook(user_id=current_user.id, book_id=book_id)
|
|
||||||
read_book.read_status = ub.ReadBook.STATUS_FINISHED
|
|
||||||
book = read_book
|
|
||||||
if not book.kobo_reading_state:
|
if not book.kobo_reading_state:
|
||||||
kobo_reading_state = ub.KoboReadingState(user_id=current_user.id, book_id=book_id)
|
kobo_reading_state = ub.KoboReadingState(user_id=current_user.id, book_id=book_id)
|
||||||
kobo_reading_state.current_bookmark = ub.KoboBookmark()
|
kobo_reading_state.current_bookmark = ub.KoboBookmark()
|
||||||
@@ -329,7 +327,7 @@ def edit_book_read_status(book_id, read_status=None):
|
|||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
calibre_db.create_functions(config)
|
calibre_db.create_functions(config)
|
||||||
book = calibre_db.get_filtered_book(book_id)
|
book = calibre_db.get_filtered_book(book_id, True)
|
||||||
book_read_status = getattr(book, 'custom_column_' + str(config.config_read_column))
|
book_read_status = getattr(book, 'custom_column_' + str(config.config_read_column))
|
||||||
if len(book_read_status):
|
if len(book_read_status):
|
||||||
if read_status is None:
|
if read_status is None:
|
||||||
@@ -396,10 +394,16 @@ def delete_book_file(book, calibrepath, book_format=None):
|
|||||||
def rename_all_files_on_change(one_book, new_path, old_path, all_new_name, gdrive=False):
|
def rename_all_files_on_change(one_book, new_path, old_path, all_new_name, gdrive=False):
|
||||||
for file_format in one_book.data:
|
for file_format in one_book.data:
|
||||||
if not gdrive:
|
if not gdrive:
|
||||||
|
try:
|
||||||
if not os.path.exists(new_path):
|
if not os.path.exists(new_path):
|
||||||
os.makedirs(new_path)
|
os.makedirs(new_path)
|
||||||
shutil.move(os.path.join(old_path, file_format.name + '.' + file_format.format.lower()),
|
shutil.move(os.path.join(old_path, file_format.name + '.' + file_format.format.lower()),
|
||||||
os.path.join(new_path, all_new_name + '.' + file_format.format.lower()))
|
os.path.join(new_path, all_new_name + '.' + file_format.format.lower()))
|
||||||
|
except (PermissionError, FileNotFoundError) as ex:
|
||||||
|
log.error("Moving book-id %s folder %s failed: %s", one_book.id, new_path, ex)
|
||||||
|
return _("Moving book path of Book %(book_id)s to: '%(src)s' failed with error: %(error)s",
|
||||||
|
book_id=one_book.id, src=new_path, error=str(ex))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
g_file = gd.getFileFromEbooksFolder(old_path,
|
g_file = gd.getFileFromEbooksFolder(old_path,
|
||||||
file_format.name + '.' + file_format.format.lower())
|
file_format.name + '.' + file_format.format.lower())
|
||||||
@@ -412,6 +416,7 @@ def rename_all_files_on_change(one_book, new_path, old_path, all_new_name, gdriv
|
|||||||
|
|
||||||
# change name in Database
|
# change name in Database
|
||||||
file_format.name = all_new_name
|
file_format.name = all_new_name
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def rename_author_path(first_author, old_author_dir, renamed_author, calibre_path="", gdrive=False):
|
def rename_author_path(first_author, old_author_dir, renamed_author, calibre_path="", gdrive=False):
|
||||||
@@ -466,14 +471,15 @@ def update_dir_structure_file(book_id, calibre_path, original_filepath, new_auth
|
|||||||
db_filename,
|
db_filename,
|
||||||
original_filepath,
|
original_filepath,
|
||||||
path)
|
path)
|
||||||
|
if not error:
|
||||||
new_path = os.path.join(calibre_path, new_author_dir, new_title_dir).replace('\\', '/')
|
new_path = os.path.join(calibre_path, new_author_dir, new_title_dir).replace('\\', '/')
|
||||||
all_new_name = get_valid_filename(local_book.title, chars=42) + ' - ' \
|
all_new_name = get_valid_filename(local_book.title, chars=42) + ' - ' \
|
||||||
+ get_valid_filename(new_author, chars=42)
|
+ get_valid_filename(new_author, chars=42)
|
||||||
# Book folder already moved, only files need to be renamed
|
# Book folder already moved, only files need to be renamed
|
||||||
rename_all_files_on_change(local_book, new_path, new_path, all_new_name)
|
renameerror = rename_all_files_on_change(local_book, new_path, new_path, all_new_name)
|
||||||
|
|
||||||
if error:
|
if error or renameerror:
|
||||||
return error
|
return error or renameerror
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@@ -517,7 +523,7 @@ def update_dir_structure_gdrive(book_id, first_author):
|
|||||||
if titledir != new_titledir or authordir != new_authordir :
|
if titledir != new_titledir or authordir != new_authordir :
|
||||||
all_new_name = get_valid_filename(book.title, chars=42) + ' - ' \
|
all_new_name = get_valid_filename(book.title, chars=42) + ' - ' \
|
||||||
+ get_valid_filename(new_authordir, chars=42)
|
+ get_valid_filename(new_authordir, chars=42)
|
||||||
rename_all_files_on_change(book, book.path, book.path, all_new_name, gdrive=True) # todo: Move filenames on gdrive
|
return rename_all_files_on_change(book, book.path, book.path, all_new_name, gdrive=True) # todo: Move filenames on gdrive
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@@ -553,33 +559,13 @@ def move_files_on_change(calibre_path, new_author_dir, new_titledir, localbook,
|
|||||||
log.error("Deleting authorpath for book %s failed: %s", localbook.id, ex)
|
log.error("Deleting authorpath for book %s failed: %s", localbook.id, ex)
|
||||||
# change location in database to new author/title path
|
# change location in database to new author/title path
|
||||||
localbook.path = os.path.join(new_author_dir, new_titledir).replace('\\', '/')
|
localbook.path = os.path.join(new_author_dir, new_titledir).replace('\\', '/')
|
||||||
except OSError as ex:
|
except (OSError, FileNotFoundError) as ex:
|
||||||
log.error_or_exception("Rename title from {} to {} failed with error: {}".format(path, new_path, ex))
|
log.error_or_exception("Rename title from {} to {} failed with error: {}".format(path, new_path, ex))
|
||||||
return _("Rename title from: '%(src)s' to '%(dest)s' failed with error: %(error)s",
|
return _("Rename title from: '%(src)s' to '%(dest)s' failed with error: %(error)s",
|
||||||
src=path, dest=new_path, error=str(ex))
|
src=path, dest=new_path, error=str(ex))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def rename_files_on_change(first_author,
|
|
||||||
renamed_author,
|
|
||||||
local_book,
|
|
||||||
original_filepath="",
|
|
||||||
path="",
|
|
||||||
calibre_path="",
|
|
||||||
gdrive=False):
|
|
||||||
# Rename all files from old names to new names
|
|
||||||
#try:
|
|
||||||
#clean_author_database(renamed_author, calibre_path, gdrive=gdrive)
|
|
||||||
#if first_author and first_author not in renamed_author:
|
|
||||||
# clean_author_database([first_author], calibre_path, local_book, gdrive)
|
|
||||||
#if not gdrive and not renamed_author and not original_filepath and len(os.listdir(os.path.dirname(path))) == 0:
|
|
||||||
# shutil.rmtree(os.path.dirname(path))
|
|
||||||
#except (OSError, FileNotFoundError) as ex:
|
|
||||||
# log.error_or_exception("Error in rename file in path {}".format(ex))
|
|
||||||
# return _("Error in rename file in path: {}".format(str(ex)))
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def delete_book_gdrive(book, book_format):
|
def delete_book_gdrive(book, book_format):
|
||||||
error = None
|
error = None
|
||||||
if book_format:
|
if book_format:
|
||||||
@@ -970,6 +956,17 @@ def do_download_file(book, book_format, client, data, headers):
|
|||||||
else:
|
else:
|
||||||
download_name = book_name
|
download_name = book_name
|
||||||
|
|
||||||
|
# Clean up staged copies in /tmp/calibre_web after the response is sent
|
||||||
|
# (kepubify / calibre-export branches) so the temp dir does not grow unbounded.
|
||||||
|
if filename == get_temp_dir():
|
||||||
|
_tmp_path = os.path.join(filename, download_name + "." + book_format)
|
||||||
|
@after_this_request
|
||||||
|
def _cleanup_staged_download(resp):
|
||||||
|
try:
|
||||||
|
os.remove(_tmp_path)
|
||||||
|
except OSError as ex:
|
||||||
|
log.warning('Failed to remove staged download %s: %s', _tmp_path, ex)
|
||||||
|
return resp
|
||||||
response = make_response(send_from_directory(filename, download_name + "." + book_format))
|
response = make_response(send_from_directory(filename, download_name + "." + book_format))
|
||||||
# ToDo Check headers parameter
|
# ToDo Check headers parameter
|
||||||
for element in headers:
|
for element in headers:
|
||||||
@@ -1105,14 +1102,17 @@ def get_download_link(book_id, book_format, client):
|
|||||||
file_name = book.title
|
file_name = book.title
|
||||||
if len(book.authors) > 0:
|
if len(book.authors) > 0:
|
||||||
file_name = file_name + ' - ' + book.authors[0].name
|
file_name = file_name + ' - ' + book.authors[0].name
|
||||||
if client == "kindle":
|
original_name = file_name
|
||||||
file_name = get_valid_filename(file_name, replace_whitespace=False, force_unidecode=True)
|
file_name = get_valid_filename(file_name, replace_whitespace=False, force_unidecode=True)
|
||||||
|
if client == "kindle":
|
||||||
|
quoted_file_name = file_name
|
||||||
else:
|
else:
|
||||||
file_name = quote(get_valid_filename(file_name, replace_whitespace=False))
|
native_name = get_valid_filename(original_name, replace_whitespace=False, force_unidecode=False)
|
||||||
|
quoted_file_name = quote(native_name)
|
||||||
headers = Headers()
|
headers = Headers()
|
||||||
headers["Content-Type"] = mimetypes.types_map.get('.' + book_format, "application/octet-stream")
|
headers["Content-Type"] = mimetypes.types_map.get('.' + book_format, "application/octet-stream")
|
||||||
headers["Content-Disposition"] = ('attachment; filename="{}.{}"; filename*=UTF-8\'\'{}.{}').format(
|
headers["Content-Disposition"] = ('attachment; filename="{}.{}"; filename*=UTF-8\'\'{}.{}').format(
|
||||||
file_name, book_format, file_name, book_format)
|
file_name, book_format, quoted_file_name, book_format)
|
||||||
return do_download_file(book, book_format, client, data1, headers)
|
return do_download_file(book, book_format, client, data1, headers)
|
||||||
else:
|
else:
|
||||||
log.error("Book id {} not found for downloading".format(book_id))
|
log.error("Book id {} not found for downloading".format(book_id))
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ from uuid import uuid4
|
|||||||
from flask import Blueprint, request, url_for, g
|
from flask import Blueprint, request, url_for, g
|
||||||
from flask_babel import format_date
|
from flask_babel import format_date
|
||||||
from .cw_login import current_user
|
from .cw_login import current_user
|
||||||
|
from .clean_html import clean_string as html_clean_string
|
||||||
|
|
||||||
from . import constants, logger
|
from . import constants, logger
|
||||||
|
|
||||||
@@ -181,3 +182,9 @@ def contains_music(book_formats):
|
|||||||
if format.format.lower() in g.constants.EXTENSIONS_AUDIO:
|
if format.format.lower() in g.constants.EXTENSIONS_AUDIO:
|
||||||
result = True
|
result = True
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
@jinjia.app_template_filter('clean_string')
|
||||||
|
def clean_string(unsafe_text):
|
||||||
|
return html_clean_string(unsafe_text)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+50
-14
@@ -25,6 +25,7 @@ import zipfile
|
|||||||
from time import gmtime, strftime
|
from time import gmtime, strftime
|
||||||
import json
|
import json
|
||||||
from urllib.parse import unquote
|
from urllib.parse import unquote
|
||||||
|
import requests
|
||||||
|
|
||||||
from flask import (
|
from flask import (
|
||||||
Blueprint,
|
Blueprint,
|
||||||
@@ -41,13 +42,11 @@ from werkzeug.datastructures import Headers
|
|||||||
from sqlalchemy import func
|
from sqlalchemy import func
|
||||||
from sqlalchemy.sql.expression import and_, or_
|
from sqlalchemy.sql.expression import and_, or_
|
||||||
from sqlalchemy.exc import StatementError
|
from sqlalchemy.exc import StatementError
|
||||||
from sqlalchemy.sql import select
|
|
||||||
import requests
|
|
||||||
|
|
||||||
from . import config, logger, kobo_auth, db, calibre_db, helper, shelf as shelf_lib, ub, csrf, kobo_sync_status
|
from . import config, logger, kobo_auth, db, calibre_db, helper, shelf as shelf_lib, ub, csrf, kobo_sync_status
|
||||||
from . import isoLanguages
|
from . import isoLanguages, limiter
|
||||||
from .epub import get_epub_layout
|
from .epub import get_epub_layout
|
||||||
from .constants import COVER_THUMBNAIL_SMALL, COVER_THUMBNAIL_MEDIUM, COVER_THUMBNAIL_LARGE
|
from .constants import COVER_THUMBNAIL_SMALL, COVER_THUMBNAIL_MEDIUM, COVER_THUMBNAIL_LARGE, BASE_DIR
|
||||||
from .helper import get_download_link
|
from .helper import get_download_link
|
||||||
from .services import SyncToken as SyncToken
|
from .services import SyncToken as SyncToken
|
||||||
from .web import download_required
|
from .web import download_required
|
||||||
@@ -141,7 +140,6 @@ def convert_to_kobo_timestamp_string(timestamp):
|
|||||||
|
|
||||||
@kobo.route("/v1/library/sync")
|
@kobo.route("/v1/library/sync")
|
||||||
@requires_kobo_auth
|
@requires_kobo_auth
|
||||||
# @download_required
|
|
||||||
def HandleSyncRequest():
|
def HandleSyncRequest():
|
||||||
if not current_user.role_download():
|
if not current_user.role_download():
|
||||||
log.info("Users need download permissions for syncing library to Kobo reader")
|
log.info("Users need download permissions for syncing library to Kobo reader")
|
||||||
@@ -369,7 +367,7 @@ def get_download_url_for_book(book_id, book_format):
|
|||||||
)
|
)
|
||||||
return url_for(
|
return url_for(
|
||||||
"kobo.download_book",
|
"kobo.download_book",
|
||||||
auth_token=kobo_auth.get_auth_token(),
|
auth_token=get_auth_token(),
|
||||||
book_id=book_id,
|
book_id=book_id,
|
||||||
book_format=book_format.lower(),
|
book_format=book_format.lower(),
|
||||||
_external=True,
|
_external=True,
|
||||||
@@ -504,6 +502,7 @@ def get_metadata(book):
|
|||||||
return metadata
|
return metadata
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@csrf.exempt
|
@csrf.exempt
|
||||||
@kobo.route("/v1/library/tags", methods=["POST", "DELETE"])
|
@kobo.route("/v1/library/tags", methods=["POST", "DELETE"])
|
||||||
@requires_kobo_auth
|
@requires_kobo_auth
|
||||||
@@ -812,6 +811,8 @@ def HandleStateRequest(book_uuid):
|
|||||||
|
|
||||||
ub.session.merge(kobo_reading_state)
|
ub.session.merge(kobo_reading_state)
|
||||||
ub.session_commit()
|
ub.session_commit()
|
||||||
|
update_results_response["LastModified"] = convert_to_kobo_timestamp_string(kobo_reading_state.last_modified)
|
||||||
|
update_results_response["PriorityTimestamp"] = convert_to_kobo_timestamp_string(kobo_reading_state.priority_timestamp)
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"RequestResult": "Success",
|
"RequestResult": "Success",
|
||||||
"UpdateResults": [update_results_response],
|
"UpdateResults": [update_results_response],
|
||||||
@@ -888,14 +889,21 @@ def get_statistics_response(statistics):
|
|||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
|
||||||
|
def _clean_progress(value):
|
||||||
|
"""Return progress as int if it's a whole number, preserving Kobo device expectations."""
|
||||||
|
if value is not None and value == int(value):
|
||||||
|
return int(value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
def get_current_bookmark_response(current_bookmark):
|
def get_current_bookmark_response(current_bookmark):
|
||||||
resp = {
|
resp = {
|
||||||
"LastModified": convert_to_kobo_timestamp_string(current_bookmark.last_modified),
|
"LastModified": convert_to_kobo_timestamp_string(current_bookmark.last_modified),
|
||||||
}
|
}
|
||||||
if current_bookmark.progress_percent:
|
if current_bookmark.progress_percent is not None:
|
||||||
resp["ProgressPercent"] = current_bookmark.progress_percent
|
resp["ProgressPercent"] = _clean_progress(current_bookmark.progress_percent)
|
||||||
if current_bookmark.content_source_progress_percent:
|
if current_bookmark.content_source_progress_percent is not None:
|
||||||
resp["ContentSourceProgressPercent"] = current_bookmark.content_source_progress_percent
|
resp["ContentSourceProgressPercent"] = _clean_progress(current_bookmark.content_source_progress_percent)
|
||||||
if current_bookmark.location_value:
|
if current_bookmark.location_value:
|
||||||
resp["Location"] = {
|
resp["Location"] = {
|
||||||
"Value": current_bookmark.location_value,
|
"Value": current_bookmark.location_value,
|
||||||
@@ -963,6 +971,7 @@ def HandleBookDeletionRequest(book_uuid):
|
|||||||
@kobo.route("/v1/library/<dummy>", methods=["DELETE", "GET", "POST"])
|
@kobo.route("/v1/library/<dummy>", methods=["DELETE", "GET", "POST"])
|
||||||
@kobo.route("/v1/library/<dummy>/preview", methods=["POST"])
|
@kobo.route("/v1/library/<dummy>/preview", methods=["POST"])
|
||||||
def HandleUnimplementedRequest(dummy=None):
|
def HandleUnimplementedRequest(dummy=None):
|
||||||
|
[limiter.limiter.clear(limit.limit, *limit.request_args) for limit in limiter.current_limits]
|
||||||
log.debug("Unimplemented Library Request received: %s (request is forwarded to kobo if configured)",
|
log.debug("Unimplemented Library Request received: %s (request is forwarded to kobo if configured)",
|
||||||
request.base_url)
|
request.base_url)
|
||||||
return redirect_or_proxy_request()
|
return redirect_or_proxy_request()
|
||||||
@@ -977,6 +986,9 @@ def HandleUnimplementedRequest(dummy=None):
|
|||||||
@kobo.route("/v1/analytics/<dummy>", methods=["GET", "POST"])
|
@kobo.route("/v1/analytics/<dummy>", methods=["GET", "POST"])
|
||||||
@kobo.route("/v1/assets", methods=["GET"])
|
@kobo.route("/v1/assets", methods=["GET"])
|
||||||
def HandleUserRequest(dummy=None):
|
def HandleUserRequest(dummy=None):
|
||||||
|
[limiter.limiter.clear(limit.limit, *limit.request_args) for limit in limiter.current_limits]
|
||||||
|
log.error("Key: {}".format(limiter.current_limit.key))
|
||||||
|
log.error("Remaining: {}".format(limiter.current_limit.remaining))
|
||||||
log.debug("Unimplemented User Request received: %s (request is forwarded to kobo if configured)", request.base_url)
|
log.debug("Unimplemented User Request received: %s (request is forwarded to kobo if configured)", request.base_url)
|
||||||
return redirect_or_proxy_request()
|
return redirect_or_proxy_request()
|
||||||
|
|
||||||
@@ -984,6 +996,7 @@ def HandleUserRequest(dummy=None):
|
|||||||
@csrf.exempt
|
@csrf.exempt
|
||||||
@kobo.route("/v1/user/loyalty/benefits", methods=["GET"])
|
@kobo.route("/v1/user/loyalty/benefits", methods=["GET"])
|
||||||
def handle_benefits():
|
def handle_benefits():
|
||||||
|
[limiter.limiter.clear(limit.limit, *limit.request_args) for limit in limiter.current_limits]
|
||||||
if config.config_kobo_proxy:
|
if config.config_kobo_proxy:
|
||||||
return redirect_or_proxy_request()
|
return redirect_or_proxy_request()
|
||||||
else:
|
else:
|
||||||
@@ -993,6 +1006,7 @@ def handle_benefits():
|
|||||||
@csrf.exempt
|
@csrf.exempt
|
||||||
@kobo.route("/v1/analytics/gettests", methods=["GET", "POST"])
|
@kobo.route("/v1/analytics/gettests", methods=["GET", "POST"])
|
||||||
def handle_getests():
|
def handle_getests():
|
||||||
|
[limiter.limiter.clear(limit.limit, *limit.request_args) for limit in limiter.current_limits]
|
||||||
if config.config_kobo_proxy:
|
if config.config_kobo_proxy:
|
||||||
return redirect_or_proxy_request()
|
return redirect_or_proxy_request()
|
||||||
else:
|
else:
|
||||||
@@ -1014,9 +1028,12 @@ def handle_getests():
|
|||||||
@kobo.route("/v1/products/dailydeal", methods=["GET", "POST"])
|
@kobo.route("/v1/products/dailydeal", methods=["GET", "POST"])
|
||||||
@kobo.route("/v1/products/deals", methods=["GET", "POST"])
|
@kobo.route("/v1/products/deals", methods=["GET", "POST"])
|
||||||
@kobo.route("/v1/products", methods=["GET", "POST"])
|
@kobo.route("/v1/products", methods=["GET", "POST"])
|
||||||
|
@kobo.route("/v1/products/<path:dummy>", methods=["GET", "POST"])
|
||||||
|
@kobo.route("/v1/products/<path:dummy>/", methods=["GET", "POST"])
|
||||||
@kobo.route("/v1/affiliate", methods=["GET", "POST"])
|
@kobo.route("/v1/affiliate", methods=["GET", "POST"])
|
||||||
@kobo.route("/v1/deals", methods=["GET", "POST"])
|
@kobo.route("/v1/deals", methods=["GET", "POST"])
|
||||||
def HandleProductsRequest(dummy=None):
|
def HandleProductsRequest(dummy=None):
|
||||||
|
[limiter.limiter.clear(limit.limit, *limit.request_args) for limit in limiter.current_limits]
|
||||||
log.debug("Unimplemented Products Request received: %s (request is forwarded to kobo if configured)",
|
log.debug("Unimplemented Products Request received: %s (request is forwarded to kobo if configured)",
|
||||||
request.base_url)
|
request.base_url)
|
||||||
return redirect_or_proxy_request()
|
return redirect_or_proxy_request()
|
||||||
@@ -1042,9 +1059,12 @@ def make_calibre_web_auth_response():
|
|||||||
|
|
||||||
|
|
||||||
@csrf.exempt
|
@csrf.exempt
|
||||||
|
@kobo.route("/v1/auth/refresh", methods=["POST"])
|
||||||
@kobo.route("/v1/auth/device", methods=["POST"])
|
@kobo.route("/v1/auth/device", methods=["POST"])
|
||||||
@requires_kobo_auth
|
@requires_kobo_auth
|
||||||
def HandleAuthRequest():
|
def HandleAuthRequest():
|
||||||
|
log.error(limiter.current_limit)
|
||||||
|
log.error(limiter.current_limit)
|
||||||
log.debug('Kobo Auth request')
|
log.debug('Kobo Auth request')
|
||||||
if config.config_kobo_proxy:
|
if config.config_kobo_proxy:
|
||||||
try:
|
try:
|
||||||
@@ -1086,7 +1106,7 @@ def HandleInitRequest():
|
|||||||
kobo_resources["image_host"] = calibre_web_url
|
kobo_resources["image_host"] = calibre_web_url
|
||||||
kobo_resources["image_url_quality_template"] = unquote(calibre_web_url +
|
kobo_resources["image_url_quality_template"] = unquote(calibre_web_url +
|
||||||
url_for("kobo.HandleCoverImageRequest",
|
url_for("kobo.HandleCoverImageRequest",
|
||||||
auth_token=kobo_auth.get_auth_token(),
|
auth_token=get_auth_token(),
|
||||||
book_uuid="{ImageId}",
|
book_uuid="{ImageId}",
|
||||||
width="{width}",
|
width="{width}",
|
||||||
height="{height}",
|
height="{height}",
|
||||||
@@ -1094,15 +1114,17 @@ def HandleInitRequest():
|
|||||||
isGreyscale='isGreyscale'))
|
isGreyscale='isGreyscale'))
|
||||||
kobo_resources["image_url_template"] = unquote(calibre_web_url +
|
kobo_resources["image_url_template"] = unquote(calibre_web_url +
|
||||||
url_for("kobo.HandleCoverImageRequest",
|
url_for("kobo.HandleCoverImageRequest",
|
||||||
auth_token=kobo_auth.get_auth_token(),
|
auth_token=get_auth_token(),
|
||||||
book_uuid="{ImageId}",
|
book_uuid="{ImageId}",
|
||||||
width="{width}",
|
width="{width}",
|
||||||
height="{height}",
|
height="{height}",
|
||||||
isGreyscale='false'))
|
isGreyscale='false'))
|
||||||
|
kobo_resources["library_sync"] = calibre_web_url + url_for("kobo.HandleSyncRequest",
|
||||||
|
auth_token=kobo_auth.get_auth_token())
|
||||||
else:
|
else:
|
||||||
kobo_resources["image_host"] = url_for("web.index", _external=True).strip("/")
|
kobo_resources["image_host"] = url_for("web.index", _external=True).strip("/")
|
||||||
kobo_resources["image_url_quality_template"] = unquote(url_for("kobo.HandleCoverImageRequest",
|
kobo_resources["image_url_quality_template"] = unquote(url_for("kobo.HandleCoverImageRequest",
|
||||||
auth_token=kobo_auth.get_auth_token(),
|
auth_token=get_auth_token(),
|
||||||
book_uuid="{ImageId}",
|
book_uuid="{ImageId}",
|
||||||
width="{width}",
|
width="{width}",
|
||||||
height="{height}",
|
height="{height}",
|
||||||
@@ -1110,12 +1132,15 @@ def HandleInitRequest():
|
|||||||
isGreyscale='isGreyscale',
|
isGreyscale='isGreyscale',
|
||||||
_external=True))
|
_external=True))
|
||||||
kobo_resources["image_url_template"] = unquote(url_for("kobo.HandleCoverImageRequest",
|
kobo_resources["image_url_template"] = unquote(url_for("kobo.HandleCoverImageRequest",
|
||||||
auth_token=kobo_auth.get_auth_token(),
|
auth_token=get_auth_token(),
|
||||||
book_uuid="{ImageId}",
|
book_uuid="{ImageId}",
|
||||||
width="{width}",
|
width="{width}",
|
||||||
height="{height}",
|
height="{height}",
|
||||||
isGreyscale='false',
|
isGreyscale='false',
|
||||||
_external=True))
|
_external=True))
|
||||||
|
kobo_resources["library_sync"] = url_for("kobo.HandleSyncRequest",
|
||||||
|
auth_token=kobo_auth.get_auth_token(),
|
||||||
|
_external=True)
|
||||||
|
|
||||||
response = make_response(jsonify({"Resources": kobo_resources}))
|
response = make_response(jsonify({"Resources": kobo_resources}))
|
||||||
response.headers["x-kobo-apitoken"] = "e30="
|
response.headers["x-kobo-apitoken"] = "e30="
|
||||||
@@ -1131,6 +1156,14 @@ def download_book(book_id, book_format):
|
|||||||
|
|
||||||
|
|
||||||
def NATIVE_KOBO_RESOURCES():
|
def NATIVE_KOBO_RESOURCES():
|
||||||
|
kobo_file = os.path.join(BASE_DIR, "kobo_resources.txt")
|
||||||
|
try:
|
||||||
|
if os.path.isfile(kobo_file):
|
||||||
|
with open(kobo_file, "r") as f:
|
||||||
|
lines = f.read()
|
||||||
|
return json.loads(lines)
|
||||||
|
except Exception as e:
|
||||||
|
log.error(e)
|
||||||
return {
|
return {
|
||||||
"account_page": "https://www.kobo.com/account/settings",
|
"account_page": "https://www.kobo.com/account/settings",
|
||||||
"account_page_rakuten": "https://my.rakuten.co.jp/",
|
"account_page_rakuten": "https://my.rakuten.co.jp/",
|
||||||
@@ -1201,6 +1234,9 @@ def NATIVE_KOBO_RESOURCES():
|
|||||||
"image_host": "//cdn.kobo.com/book-images/",
|
"image_host": "//cdn.kobo.com/book-images/",
|
||||||
"image_url_quality_template": "https://cdn.kobo.com/book-images/{ImageId}/{Width}/{Height}/{Quality}/{IsGreyscale}/image.jpg",
|
"image_url_quality_template": "https://cdn.kobo.com/book-images/{ImageId}/{Width}/{Height}/{Quality}/{IsGreyscale}/image.jpg",
|
||||||
"image_url_template": "https://cdn.kobo.com/book-images/{ImageId}/{Width}/{Height}/false/image.jpg",
|
"image_url_template": "https://cdn.kobo.com/book-images/{ImageId}/{Width}/{Height}/false/image.jpg",
|
||||||
|
"instapaper_enabled": "True",
|
||||||
|
"instapaper_env_url": "https://www.instapaper.com/api/kobo",
|
||||||
|
"instapaper_link_account_start": "https://authorize.kobo.com/{region}/{language}/linkinstapaper",
|
||||||
"kobo_audiobooks_credit_redemption": "False",
|
"kobo_audiobooks_credit_redemption": "False",
|
||||||
"kobo_audiobooks_enabled": "True",
|
"kobo_audiobooks_enabled": "True",
|
||||||
"kobo_audiobooks_orange_deal_enabled": "False",
|
"kobo_audiobooks_orange_deal_enabled": "False",
|
||||||
|
|||||||
+5
-9
@@ -67,7 +67,6 @@ from functools import wraps
|
|||||||
from flask import g, Blueprint, abort, request
|
from flask import g, Blueprint, abort, request
|
||||||
from .cw_login import login_user, current_user
|
from .cw_login import login_user, current_user
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
from flask_limiter import RateLimitExceeded
|
|
||||||
|
|
||||||
from . import logger, config, calibre_db, db, helper, ub, lm, limiter
|
from . import logger, config, calibre_db, db, helper, ub, lm, limiter
|
||||||
from .render_template import render_title_template
|
from .render_template import render_title_template
|
||||||
@@ -82,6 +81,8 @@ kobo_auth = Blueprint("kobo_auth", __name__, url_prefix="/kobo_auth")
|
|||||||
@kobo_auth.route("/generate_auth_token/<int:user_id>")
|
@kobo_auth.route("/generate_auth_token/<int:user_id>")
|
||||||
@user_login_required
|
@user_login_required
|
||||||
def generate_auth_token(user_id):
|
def generate_auth_token(user_id):
|
||||||
|
if current_user.id != user_id and not current_user.role_admin():
|
||||||
|
abort(403)
|
||||||
warning = False
|
warning = False
|
||||||
host_list = request.host.rsplit(':')
|
host_list = request.host.rsplit(':')
|
||||||
if len(host_list) == 1:
|
if len(host_list) == 1:
|
||||||
@@ -124,6 +125,8 @@ def generate_auth_token(user_id):
|
|||||||
@kobo_auth.route("/deleteauthtoken/<int:user_id>", methods=["POST"])
|
@kobo_auth.route("/deleteauthtoken/<int:user_id>", methods=["POST"])
|
||||||
@user_login_required
|
@user_login_required
|
||||||
def delete_auth_token(user_id):
|
def delete_auth_token(user_id):
|
||||||
|
if current_user.id != user_id and not current_user.role_admin():
|
||||||
|
abort(403)
|
||||||
# Invalidate any previously generated Kobo Auth token for this user
|
# Invalidate any previously generated Kobo Auth token for this user
|
||||||
ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.user_id == user_id)\
|
ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.user_id == user_id)\
|
||||||
.filter(ub.RemoteAuthToken.token_type==1).delete()
|
.filter(ub.RemoteAuthToken.token_type==1).delete()
|
||||||
@@ -154,13 +157,6 @@ def requires_kobo_auth(f):
|
|||||||
def inner(*args, **kwargs):
|
def inner(*args, **kwargs):
|
||||||
auth_token = get_auth_token()
|
auth_token = get_auth_token()
|
||||||
if auth_token is not None:
|
if auth_token is not None:
|
||||||
try:
|
|
||||||
limiter.check()
|
|
||||||
except RateLimitExceeded:
|
|
||||||
return abort(429)
|
|
||||||
except (ConnectionError, Exception) as e:
|
|
||||||
log.error("Connection error to limiter backend: %s", e)
|
|
||||||
return abort(429)
|
|
||||||
user = (
|
user = (
|
||||||
ub.session.query(ub.User)
|
ub.session.query(ub.User)
|
||||||
.join(ub.RemoteAuthToken)
|
.join(ub.RemoteAuthToken)
|
||||||
@@ -169,7 +165,7 @@ def requires_kobo_auth(f):
|
|||||||
)
|
)
|
||||||
if user is not None:
|
if user is not None:
|
||||||
login_user(user)
|
login_user(user)
|
||||||
[limiter.limiter.storage.clear(k.key) for k in limiter.current_limits]
|
[limiter.limiter.clear(limit.limit, *limit.request_args) for limit in limiter.current_limits]
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
log.debug("Received Kobo request without a recognizable auth token.")
|
log.debug("Received Kobo request without a recognizable auth token.")
|
||||||
return abort(401)
|
return abort(401)
|
||||||
|
|||||||
@@ -51,13 +51,15 @@ def remove_synced_book(book_id, all=False, session=None):
|
|||||||
ub.session_commit(_session=session)
|
ub.session_commit(_session=session)
|
||||||
|
|
||||||
|
|
||||||
|
# If state == none, it will toggle the archive state of the passed book_id.
|
||||||
|
# state = true archives it, state = false unarchives it
|
||||||
def change_archived_books(book_id, state=None, message=None):
|
def change_archived_books(book_id, state=None, message=None):
|
||||||
archived_book = ub.session.query(ub.ArchivedBook).filter(and_(ub.ArchivedBook.user_id == int(current_user.id),
|
archived_book = ub.session.query(ub.ArchivedBook).filter(and_(ub.ArchivedBook.user_id == int(current_user.id),
|
||||||
ub.ArchivedBook.book_id == book_id)).first()
|
ub.ArchivedBook.book_id == book_id)).first()
|
||||||
if not archived_book:
|
if not archived_book: # and (state == True or state == None):
|
||||||
archived_book = ub.ArchivedBook(user_id=current_user.id, book_id=book_id)
|
archived_book = ub.ArchivedBook(user_id=current_user.id, book_id=book_id)
|
||||||
|
|
||||||
archived_book.is_archived = state if state else not archived_book.is_archived
|
archived_book.is_archived = state if state != None else not archived_book.is_archived
|
||||||
archived_book.last_modified = datetime.now(timezone.utc) # toDo. Check utc timestamp
|
archived_book.last_modified = datetime.now(timezone.utc) # toDo. Check utc timestamp
|
||||||
|
|
||||||
ub.session.merge(archived_book)
|
ub.session.merge(archived_book)
|
||||||
|
|||||||
+3
-3
@@ -24,7 +24,7 @@ from flask import request
|
|||||||
|
|
||||||
|
|
||||||
def request_username():
|
def request_username():
|
||||||
return request.authorization.username
|
return request.authorization.username if request.authorization else ""
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
@@ -66,8 +66,8 @@ def main():
|
|||||||
app.register_blueprint(tasks)
|
app.register_blueprint(tasks)
|
||||||
app.register_blueprint(web)
|
app.register_blueprint(web)
|
||||||
app.register_blueprint(basic)
|
app.register_blueprint(basic)
|
||||||
app.register_blueprint(opds)
|
|
||||||
limiter.limit("3/minute", key_func=request_username)(opds)
|
limiter.limit("3/minute", key_func=request_username)(opds)
|
||||||
|
app.register_blueprint(opds)
|
||||||
app.register_blueprint(jinjia)
|
app.register_blueprint(jinjia)
|
||||||
app.register_blueprint(about)
|
app.register_blueprint(about)
|
||||||
app.register_blueprint(shelf)
|
app.register_blueprint(shelf)
|
||||||
@@ -77,9 +77,9 @@ def main():
|
|||||||
app.register_blueprint(gdrive)
|
app.register_blueprint(gdrive)
|
||||||
app.register_blueprint(editbook)
|
app.register_blueprint(editbook)
|
||||||
if kobo_available:
|
if kobo_available:
|
||||||
|
limiter.limit("3/minute", key_func=get_remote_address)(kobo)
|
||||||
app.register_blueprint(kobo)
|
app.register_blueprint(kobo)
|
||||||
app.register_blueprint(kobo_auth)
|
app.register_blueprint(kobo_auth)
|
||||||
limiter.limit("3/minute", key_func=get_remote_address)(kobo)
|
|
||||||
if oauth_available:
|
if oauth_available:
|
||||||
app.register_blueprint(oauth)
|
app.register_blueprint(oauth)
|
||||||
success = web_server.start()
|
success = web_server.start()
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ class ComicVine(Metadata):
|
|||||||
result.raise_for_status()
|
result.raise_for_status()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.warning(e)
|
log.warning(e)
|
||||||
return None
|
return []
|
||||||
for result in result.json()["results"]:
|
for result in result.json()["results"]:
|
||||||
match = self._parse_search_result(
|
match = self._parse_search_result(
|
||||||
result=result, generic_cover=generic_cover, locale=locale
|
result=result, generic_cover=generic_cover, locale=locale
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ class Douban(Metadata):
|
|||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.warning(e)
|
log.warning(e)
|
||||||
return None
|
return []
|
||||||
|
|
||||||
match = MetaRecord(
|
match = MetaRecord(
|
||||||
id=id,
|
id=id,
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ from datetime import datetime
|
|||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from cps import logger
|
from cps import logger, config
|
||||||
from cps.isoLanguages import get_lang3, get_language_name
|
from cps.isoLanguages import get_lang3, get_language_name
|
||||||
from cps.services.Metadata import MetaRecord, MetaSourceInfo, Metadata
|
from cps.services.Metadata import MetaRecord, MetaSourceInfo, Metadata
|
||||||
|
|
||||||
@@ -31,13 +31,14 @@ log = logger.create()
|
|||||||
|
|
||||||
|
|
||||||
class Google(Metadata):
|
class Google(Metadata):
|
||||||
__name__ = "Google"
|
__name__ = "Google Books"
|
||||||
__id__ = "google"
|
__id__ = "google"
|
||||||
DESCRIPTION = "Google Books"
|
DESCRIPTION = "Google Books"
|
||||||
META_URL = "https://books.google.com/"
|
META_URL = "https://books.google.com/"
|
||||||
BOOK_URL = "https://books.google.com/books?id="
|
BOOK_URL = "https://books.google.com/books?id="
|
||||||
SEARCH_URL = "https://www.googleapis.com/books/v1/volumes?q="
|
SEARCH_URL = "https://www.googleapis.com/books/v1/volumes?q="
|
||||||
ISBN_TYPE = "ISBN_13"
|
ISBN_TYPE = "ISBN_13"
|
||||||
|
API_KEY = "&key=" + config.config_googlebooks_api_key
|
||||||
|
|
||||||
def search(
|
def search(
|
||||||
self, query: str, generic_cover: str = "", locale: str = "en"
|
self, query: str, generic_cover: str = "", locale: str = "en"
|
||||||
@@ -50,7 +51,7 @@ class Google(Metadata):
|
|||||||
tokens = [quote(t.encode("utf-8")) for t in title_tokens]
|
tokens = [quote(t.encode("utf-8")) for t in title_tokens]
|
||||||
query = "+".join(tokens)
|
query = "+".join(tokens)
|
||||||
try:
|
try:
|
||||||
results = requests.get(Google.SEARCH_URL + query)
|
results = requests.get(Google.SEARCH_URL + query + Google.API_KEY)
|
||||||
results.raise_for_status()
|
results.raise_for_status()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.warning(e)
|
log.warning(e)
|
||||||
|
|||||||
@@ -134,6 +134,14 @@ def bind_oauth_or_register(provider_id, provider_user_id, redirect_url, provider
|
|||||||
oauth_entry = query.first()
|
oauth_entry = query.first()
|
||||||
# already bind with user, just login
|
# already bind with user, just login
|
||||||
if oauth_entry.user:
|
if oauth_entry.user:
|
||||||
|
# If a user is already logged in and it's a different account, reject the link
|
||||||
|
# to prevent account takeover via shared OAuth identities
|
||||||
|
if current_user and current_user.is_authenticated and oauth_entry.user_id != current_user.id:
|
||||||
|
flash(_("This %(oauth)s account is already linked to a different user",
|
||||||
|
oauth=provider_name), category="error")
|
||||||
|
log.warning("User %s tried to link OAuth account already bound to user %s",
|
||||||
|
current_user.id, oauth_entry.user_id)
|
||||||
|
return redirect(url_for('web.profile'))
|
||||||
login_user(oauth_entry.user)
|
login_user(oauth_entry.user)
|
||||||
log.debug("You are now logged in as: '%s'", oauth_entry.user.name)
|
log.debug("You are now logged in as: '%s'", oauth_entry.user.name)
|
||||||
flash(_("Success! You are now logged in as: %(nickname)s", nickname=oauth_entry.user.name),
|
flash(_("Success! You are now logged in as: %(nickname)s", nickname=oauth_entry.user.name),
|
||||||
|
|||||||
+45
-24
@@ -21,7 +21,6 @@
|
|||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
# import json
|
|
||||||
from urllib.parse import unquote_plus
|
from urllib.parse import unquote_plus
|
||||||
|
|
||||||
from flask import Blueprint, request, render_template, make_response, abort, g, jsonify
|
from flask import Blueprint, request, render_template, make_response, abort, g, jsonify
|
||||||
@@ -88,8 +87,8 @@ def feed_letter_books(book_id):
|
|||||||
letter,
|
letter,
|
||||||
[db.Books.sort],
|
[db.Books.sort],
|
||||||
True, config.config_read_column)
|
True, config.config_read_column)
|
||||||
|
cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
|
||||||
return render_xml_template('feed.xml', entries=entries, pagination=pagination)
|
return render_xml_template('feed.xml', entries=entries, pagination=pagination, cc=cc)
|
||||||
|
|
||||||
|
|
||||||
@opds.route("/opds/new")
|
@opds.route("/opds/new")
|
||||||
@@ -101,7 +100,8 @@ def feed_new():
|
|||||||
entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), 0,
|
entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), 0,
|
||||||
db.Books, True, [db.Books.timestamp.desc()],
|
db.Books, True, [db.Books.timestamp.desc()],
|
||||||
True, config.config_read_column)
|
True, config.config_read_column)
|
||||||
return render_xml_template('feed.xml', entries=entries, pagination=pagination)
|
cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
|
||||||
|
return render_xml_template('feed.xml', entries=entries, pagination=pagination, cc=cc)
|
||||||
|
|
||||||
|
|
||||||
@opds.route("/opds/discover")
|
@opds.route("/opds/discover")
|
||||||
@@ -112,7 +112,8 @@ def feed_discover():
|
|||||||
query = calibre_db.generate_linked_query(config.config_read_column, db.Books)
|
query = calibre_db.generate_linked_query(config.config_read_column, db.Books)
|
||||||
entries = query.filter(calibre_db.common_filters()).order_by(func.random()).limit(config.config_books_per_page)
|
entries = query.filter(calibre_db.common_filters()).order_by(func.random()).limit(config.config_books_per_page)
|
||||||
pagination = Pagination(1, config.config_books_per_page, int(config.config_books_per_page))
|
pagination = Pagination(1, config.config_books_per_page, int(config.config_books_per_page))
|
||||||
return render_xml_template('feed.xml', entries=entries, pagination=pagination)
|
cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
|
||||||
|
return render_xml_template('feed.xml', entries=entries, pagination=pagination, cc=cc)
|
||||||
|
|
||||||
|
|
||||||
@opds.route("/opds/rated")
|
@opds.route("/opds/rated")
|
||||||
@@ -125,7 +126,8 @@ def feed_best_rated():
|
|||||||
db.Books, db.Books.ratings.any(db.Ratings.rating > 9),
|
db.Books, db.Books.ratings.any(db.Ratings.rating > 9),
|
||||||
[db.Books.timestamp.desc()],
|
[db.Books.timestamp.desc()],
|
||||||
True, config.config_read_column)
|
True, config.config_read_column)
|
||||||
return render_xml_template('feed.xml', entries=entries, pagination=pagination)
|
cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
|
||||||
|
return render_xml_template('feed.xml', entries=entries, pagination=pagination, cc=cc)
|
||||||
|
|
||||||
|
|
||||||
@opds.route("/opds/hot")
|
@opds.route("/opds/hot")
|
||||||
@@ -149,7 +151,8 @@ def feed_hot():
|
|||||||
num_books = entries.__len__()
|
num_books = entries.__len__()
|
||||||
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1),
|
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1),
|
||||||
config.config_books_per_page, num_books)
|
config.config_books_per_page, num_books)
|
||||||
return render_xml_template('feed.xml', entries=entries, pagination=pagination)
|
cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
|
||||||
|
return render_xml_template('feed.xml', entries=entries, pagination=pagination, cc=cc)
|
||||||
|
|
||||||
|
|
||||||
@opds.route("/opds/author")
|
@opds.route("/opds/author")
|
||||||
@@ -174,7 +177,8 @@ def feed_letter_author(book_id):
|
|||||||
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
|
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
|
||||||
entries.count())
|
entries.count())
|
||||||
entries = entries.limit(config.config_books_per_page).offset(off).all()
|
entries = entries.limit(config.config_books_per_page).offset(off).all()
|
||||||
return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_author', pagination=pagination)
|
cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
|
||||||
|
return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_author', pagination=pagination, cc=cc)
|
||||||
|
|
||||||
|
|
||||||
@opds.route("/opds/author/<int:book_id>")
|
@opds.route("/opds/author/<int:book_id>")
|
||||||
@@ -197,7 +201,8 @@ def feed_publisherindex():
|
|||||||
.limit(config.config_books_per_page).offset(off)
|
.limit(config.config_books_per_page).offset(off)
|
||||||
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
|
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
|
||||||
len(calibre_db.session.query(db.Publishers).all()))
|
len(calibre_db.session.query(db.Publishers).all()))
|
||||||
return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_publisher', pagination=pagination)
|
cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
|
||||||
|
return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_publisher', pagination=pagination, cc=cc)
|
||||||
|
|
||||||
|
|
||||||
@opds.route("/opds/publisher/<int:book_id>")
|
@opds.route("/opds/publisher/<int:book_id>")
|
||||||
@@ -230,7 +235,8 @@ def feed_letter_category(book_id):
|
|||||||
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
|
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
|
||||||
entries.count())
|
entries.count())
|
||||||
entries = entries.offset(off).limit(config.config_books_per_page).all()
|
entries = entries.offset(off).limit(config.config_books_per_page).all()
|
||||||
return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_category', pagination=pagination)
|
cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
|
||||||
|
return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_category', pagination=pagination, cc=cc)
|
||||||
|
|
||||||
|
|
||||||
@opds.route("/opds/category/<int:book_id>")
|
@opds.route("/opds/category/<int:book_id>")
|
||||||
@@ -263,7 +269,8 @@ def feed_letter_series(book_id):
|
|||||||
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
|
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
|
||||||
entries.count())
|
entries.count())
|
||||||
entries = entries.offset(off).limit(config.config_books_per_page).all()
|
entries = entries.offset(off).limit(config.config_books_per_page).all()
|
||||||
return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_series', pagination=pagination)
|
cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
|
||||||
|
return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_series', pagination=pagination, cc=cc)
|
||||||
|
|
||||||
|
|
||||||
@opds.route("/opds/series/<int:book_id>")
|
@opds.route("/opds/series/<int:book_id>")
|
||||||
@@ -275,7 +282,8 @@ def feed_series(book_id):
|
|||||||
db.Books.series.any(db.Series.id == book_id),
|
db.Books.series.any(db.Series.id == book_id),
|
||||||
[db.Books.series_index],
|
[db.Books.series_index],
|
||||||
True, config.config_read_column)
|
True, config.config_read_column)
|
||||||
return render_xml_template('feed.xml', entries=entries, pagination=pagination)
|
cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
|
||||||
|
return render_xml_template('feed.xml', entries=entries, pagination=pagination, cc=cc)
|
||||||
|
|
||||||
|
|
||||||
@opds.route("/opds/ratings")
|
@opds.route("/opds/ratings")
|
||||||
@@ -297,7 +305,8 @@ def feed_ratingindex():
|
|||||||
element = list()
|
element = list()
|
||||||
for entry in entries:
|
for entry in entries:
|
||||||
element.append(FeedObject(entry[0].id, _("{} Stars").format(entry.name)))
|
element.append(FeedObject(entry[0].id, _("{} Stars").format(entry.name)))
|
||||||
return render_xml_template('feed.xml', listelements=element, folder='opds.feed_ratings', pagination=pagination)
|
cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
|
||||||
|
return render_xml_template('feed.xml', listelements=element, folder='opds.feed_ratings', pagination=pagination, cc=cc)
|
||||||
|
|
||||||
|
|
||||||
@opds.route("/opds/ratings/<book_id>")
|
@opds.route("/opds/ratings/<book_id>")
|
||||||
@@ -321,7 +330,8 @@ def feed_formatindex():
|
|||||||
element = list()
|
element = list()
|
||||||
for entry in entries:
|
for entry in entries:
|
||||||
element.append(FeedObject(entry.format, entry.format))
|
element.append(FeedObject(entry.format, entry.format))
|
||||||
return render_xml_template('feed.xml', listelements=element, folder='opds.feed_format', pagination=pagination)
|
cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
|
||||||
|
return render_xml_template('feed.xml', listelements=element, folder='opds.feed_format', pagination=pagination, cc=cc)
|
||||||
|
|
||||||
|
|
||||||
@opds.route("/opds/formats/<book_id>")
|
@opds.route("/opds/formats/<book_id>")
|
||||||
@@ -333,7 +343,8 @@ def feed_format(book_id):
|
|||||||
db.Books.data.any(db.Data.format == book_id.upper()),
|
db.Books.data.any(db.Data.format == book_id.upper()),
|
||||||
[db.Books.timestamp.desc()],
|
[db.Books.timestamp.desc()],
|
||||||
True, config.config_read_column)
|
True, config.config_read_column)
|
||||||
return render_xml_template('feed.xml', entries=entries, pagination=pagination)
|
cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
|
||||||
|
return render_xml_template('feed.xml', entries=entries, pagination=pagination, cc=cc)
|
||||||
|
|
||||||
|
|
||||||
@opds.route("/opds/language")
|
@opds.route("/opds/language")
|
||||||
@@ -351,7 +362,8 @@ def feed_languagesindex():
|
|||||||
languages[0].name = isoLanguages.get_language_name(get_locale(), languages[0].lang_code)
|
languages[0].name = isoLanguages.get_language_name(get_locale(), languages[0].lang_code)
|
||||||
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
|
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
|
||||||
len(languages))
|
len(languages))
|
||||||
return render_xml_template('feed.xml', listelements=languages, folder='opds.feed_languages', pagination=pagination)
|
cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
|
||||||
|
return render_xml_template('feed.xml', listelements=languages, folder='opds.feed_languages', pagination=pagination, cc=cc)
|
||||||
|
|
||||||
|
|
||||||
@opds.route("/opds/language/<int:book_id>")
|
@opds.route("/opds/language/<int:book_id>")
|
||||||
@@ -363,7 +375,8 @@ def feed_languages(book_id):
|
|||||||
db.Books.languages.any(db.Languages.id == book_id),
|
db.Books.languages.any(db.Languages.id == book_id),
|
||||||
[db.Books.timestamp.desc()],
|
[db.Books.timestamp.desc()],
|
||||||
True, config.config_read_column)
|
True, config.config_read_column)
|
||||||
return render_xml_template('feed.xml', entries=entries, pagination=pagination)
|
cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
|
||||||
|
return render_xml_template('feed.xml', entries=entries, pagination=pagination, cc=cc)
|
||||||
|
|
||||||
|
|
||||||
@opds.route("/opds/shelfindex")
|
@opds.route("/opds/shelfindex")
|
||||||
@@ -377,7 +390,8 @@ def feed_shelfindex():
|
|||||||
number = len(shelf)
|
number = len(shelf)
|
||||||
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
|
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
|
||||||
number)
|
number)
|
||||||
return render_xml_template('feed.xml', listelements=shelf, folder='opds.feed_shelf', pagination=pagination)
|
cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
|
||||||
|
return render_xml_template('feed.xml', listelements=shelf, folder='opds.feed_shelf', pagination=pagination, cc=cc)
|
||||||
|
|
||||||
|
|
||||||
@opds.route("/opds/shelf/<int:book_id>")
|
@opds.route("/opds/shelf/<int:book_id>")
|
||||||
@@ -417,7 +431,8 @@ def feed_shelf(book_id):
|
|||||||
except (OperationalError, InvalidRequestError) as e:
|
except (OperationalError, InvalidRequestError) as e:
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
log.error_or_exception("Settings Database error: {}".format(e))
|
log.error_or_exception("Settings Database error: {}".format(e))
|
||||||
return render_xml_template('feed.xml', entries=result, pagination=pagination)
|
cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
|
||||||
|
return render_xml_template('feed.xml', entries=result, pagination=pagination, cc=cc)
|
||||||
|
|
||||||
|
|
||||||
@opds.route("/opds/download/<book_id>/<book_format>/")
|
@opds.route("/opds/download/<book_id>/<book_format>/")
|
||||||
@@ -470,7 +485,8 @@ def feed_read_books():
|
|||||||
return abort(403)
|
return abort(403)
|
||||||
off = request.args.get("offset") or 0
|
off = request.args.get("offset") or 0
|
||||||
result, pagination = render_read_books(int(off) / (int(config.config_books_per_page)) + 1, True, True)
|
result, pagination = render_read_books(int(off) / (int(config.config_books_per_page)) + 1, True, True)
|
||||||
return render_xml_template('feed.xml', entries=result, pagination=pagination)
|
cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
|
||||||
|
return render_xml_template('feed.xml', entries=result, pagination=pagination, cc=cc)
|
||||||
|
|
||||||
|
|
||||||
@opds.route("/opds/unreadbooks")
|
@opds.route("/opds/unreadbooks")
|
||||||
@@ -480,7 +496,8 @@ def feed_unread_books():
|
|||||||
return abort(403)
|
return abort(403)
|
||||||
off = request.args.get("offset") or 0
|
off = request.args.get("offset") or 0
|
||||||
result, pagination = render_read_books(int(off) / (int(config.config_books_per_page)) + 1, False, True)
|
result, pagination = render_read_books(int(off) / (int(config.config_books_per_page)) + 1, False, True)
|
||||||
return render_xml_template('feed.xml', entries=result, pagination=pagination)
|
cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
|
||||||
|
return render_xml_template('feed.xml', entries=result, pagination=pagination, cc=cc)
|
||||||
|
|
||||||
|
|
||||||
class FeedObject:
|
class FeedObject:
|
||||||
@@ -502,7 +519,8 @@ def feed_search(term):
|
|||||||
entries, __, ___ = calibre_db.get_search_results(term, config=config)
|
entries, __, ___ = calibre_db.get_search_results(term, config=config)
|
||||||
entries_count = len(entries) if len(entries) > 0 else 1
|
entries_count = len(entries) if len(entries) > 0 else 1
|
||||||
pagination = Pagination(1, entries_count, entries_count)
|
pagination = Pagination(1, entries_count, entries_count)
|
||||||
return render_xml_template('feed.xml', searchterm=term, entries=entries, pagination=pagination)
|
cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
|
||||||
|
return render_xml_template('feed.xml', searchterm=term, entries=entries, pagination=pagination, cc=cc)
|
||||||
else:
|
else:
|
||||||
return render_xml_template('feed.xml', searchterm="")
|
return render_xml_template('feed.xml', searchterm="")
|
||||||
|
|
||||||
@@ -524,7 +542,8 @@ def render_xml_dataset(data_table, book_id):
|
|||||||
getattr(db.Books, data_table.__tablename__).any(data_table.id == book_id),
|
getattr(db.Books, data_table.__tablename__).any(data_table.id == book_id),
|
||||||
[db.Books.timestamp.desc()],
|
[db.Books.timestamp.desc()],
|
||||||
True, config.config_read_column)
|
True, config.config_read_column)
|
||||||
return render_xml_template('feed.xml', entries=entries, pagination=pagination)
|
cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
|
||||||
|
return render_xml_template('feed.xml', entries=entries, pagination=pagination, cc=cc)
|
||||||
|
|
||||||
|
|
||||||
def render_element_index(database_column, linked_table, folder):
|
def render_element_index(database_column, linked_table, folder):
|
||||||
@@ -545,7 +564,9 @@ def render_element_index(database_column, linked_table, folder):
|
|||||||
elements.append({'id': entry.id, 'name': entry.id})
|
elements.append({'id': entry.id, 'name': entry.id})
|
||||||
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
|
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
|
||||||
len(entries) + 1)
|
len(entries) + 1)
|
||||||
|
cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
|
||||||
return render_xml_template('feed.xml',
|
return render_xml_template('feed.xml',
|
||||||
letterelements=elements,
|
letterelements=elements,
|
||||||
folder=folder,
|
folder=folder,
|
||||||
pagination=pagination)
|
pagination=pagination,
|
||||||
|
cc=cc)
|
||||||
|
|||||||
+1
-1
@@ -29,7 +29,7 @@
|
|||||||
|
|
||||||
from urllib.parse import urlparse, urljoin
|
from urllib.parse import urlparse, urljoin
|
||||||
|
|
||||||
from flask import request, url_for, redirect, current_app
|
from flask import request, url_for, current_app
|
||||||
|
|
||||||
|
|
||||||
def is_safe_url(target):
|
def is_safe_url(target):
|
||||||
|
|||||||
+11
-11
@@ -47,7 +47,7 @@ def get_sidebar_config(kwargs=None):
|
|||||||
if current_user.role_admin():
|
if current_user.role_admin():
|
||||||
sidebar.append({"glyph": "glyphicon-download", "text": _('Downloaded Books'), "link": 'web.download_list',
|
sidebar.append({"glyph": "glyphicon-download", "text": _('Downloaded Books'), "link": 'web.download_list',
|
||||||
"id": "download", "visibility": constants.SIDEBAR_DOWNLOAD, 'public': (not current_user.is_anonymous),
|
"id": "download", "visibility": constants.SIDEBAR_DOWNLOAD, 'public': (not current_user.is_anonymous),
|
||||||
"page": "download", "show_text": _('Show Downloaded Books'),
|
"page": "download", "show_text": _('Show Downloaded Books'), "no_param":True,
|
||||||
"config_show": content})
|
"config_show": content})
|
||||||
else:
|
else:
|
||||||
sidebar.append({"glyph": "glyphicon-download", "text": _('Downloaded Books'), "link": 'web.books_list',
|
sidebar.append({"glyph": "glyphicon-download", "text": _('Downloaded Books'), "link": 'web.books_list',
|
||||||
@@ -69,37 +69,37 @@ def get_sidebar_config(kwargs=None):
|
|||||||
"visibility": constants.SIDEBAR_RANDOM, 'public': True, "page": "discover",
|
"visibility": constants.SIDEBAR_RANDOM, 'public': True, "page": "discover",
|
||||||
"show_text": _('Show Random Books'), "config_show": True})
|
"show_text": _('Show Random Books'), "config_show": True})
|
||||||
sidebar.append({"glyph": "glyphicon-inbox", "text": _('Categories'), "link": 'web.category_list', "id": "cat",
|
sidebar.append({"glyph": "glyphicon-inbox", "text": _('Categories'), "link": 'web.category_list', "id": "cat",
|
||||||
"visibility": constants.SIDEBAR_CATEGORY, 'public': True, "page": "category",
|
"visibility": constants.SIDEBAR_CATEGORY, 'public': True, "page": "category", "no_param":True,
|
||||||
"show_text": _('Show Category Section'), "config_show": True})
|
"show_text": _('Show Category Section'), "config_show": True})
|
||||||
sidebar.append({"glyph": "glyphicon-bookmark", "text": _('Series'), "link": 'web.series_list', "id": "serie",
|
sidebar.append({"glyph": "glyphicon-bookmark", "text": _('Series'), "link": 'web.series_list', "id": "serie",
|
||||||
"visibility": constants.SIDEBAR_SERIES, 'public': True, "page": "series",
|
"visibility": constants.SIDEBAR_SERIES, 'public': True, "page": "series", "no_param":True,
|
||||||
"show_text": _('Show Series Section'), "config_show": True})
|
"show_text": _('Show Series Section'), "config_show": True})
|
||||||
sidebar.append({"glyph": "glyphicon-user", "text": _('Authors'), "link": 'web.author_list', "id": "author",
|
sidebar.append({"glyph": "glyphicon-user", "text": _('Authors'), "link": 'web.author_list', "id": "author",
|
||||||
"visibility": constants.SIDEBAR_AUTHOR, 'public': True, "page": "author",
|
"visibility": constants.SIDEBAR_AUTHOR, 'public': True, "page": "author", "no_param":True,
|
||||||
"show_text": _('Show Author Section'), "config_show": True})
|
"show_text": _('Show Author Section'), "config_show": True})
|
||||||
sidebar.append(
|
sidebar.append(
|
||||||
{"glyph": "glyphicon-text-size", "text": _('Publishers'), "link": 'web.publisher_list', "id": "publisher",
|
{"glyph": "glyphicon-text-size", "text": _('Publishers'), "link": 'web.publisher_list', "id": "publisher",
|
||||||
"visibility": constants.SIDEBAR_PUBLISHER, 'public': True, "page": "publisher",
|
"visibility": constants.SIDEBAR_PUBLISHER, 'public': True, "page": "publisher", "no_param":True,
|
||||||
"show_text": _('Show Publisher Section'), "config_show":True})
|
"show_text": _('Show Publisher Section'), "config_show":True})
|
||||||
sidebar.append({"glyph": "glyphicon-flag", "text": _('Languages'), "link": 'web.language_overview', "id": "lang",
|
sidebar.append({"glyph": "glyphicon-flag", "text": _('Languages'), "link": 'web.language_overview', "id": "lang",
|
||||||
"visibility": constants.SIDEBAR_LANGUAGE, 'public': (current_user.filter_language() == 'all'),
|
"visibility": constants.SIDEBAR_LANGUAGE, 'public': (current_user.filter_language() == 'all'),
|
||||||
"page": "language",
|
"page": "language", "no_param":True,
|
||||||
"show_text": _('Show Language Section'), "config_show": True})
|
"show_text": _('Show Language Section'), "config_show": True})
|
||||||
sidebar.append({"glyph": "glyphicon-star-empty", "text": _('Ratings'), "link": 'web.ratings_list', "id": "rate",
|
sidebar.append({"glyph": "glyphicon-star-empty", "text": _('Ratings'), "link": 'web.ratings_list', "id": "rate",
|
||||||
"visibility": constants.SIDEBAR_RATING, 'public': True,
|
"visibility": constants.SIDEBAR_RATING, 'public': True, "no_param":True,
|
||||||
"page": "rating", "show_text": _('Show Ratings Section'), "config_show": True})
|
"page": "rating", "show_text": _('Show Ratings Section'), "config_show": True})
|
||||||
sidebar.append({"glyph": "glyphicon-file", "text": _('File formats'), "link": 'web.formats_list', "id": "format",
|
sidebar.append({"glyph": "glyphicon-file", "text": _('File formats'), "link": 'web.formats_list', "id": "format",
|
||||||
"visibility": constants.SIDEBAR_FORMAT, 'public': True,
|
"visibility": constants.SIDEBAR_FORMAT, 'public': True, "no_param":True,
|
||||||
"page": "format", "show_text": _('Show File Formats Section'), "config_show": True})
|
"page": "format", "show_text": _('Show File Formats Section'), "config_show": True})
|
||||||
sidebar.append(
|
sidebar.append(
|
||||||
{"glyph": "glyphicon-trash", "text": _('Archived Books'), "link": 'web.books_list', "id": "archived",
|
{"glyph": "glyphicon-folder-open", "text": _('Archived Books'), "link": 'web.books_list', "id": "archived",
|
||||||
"visibility": constants.SIDEBAR_ARCHIVED, 'public': (not current_user.is_anonymous), "page": "archived",
|
"visibility": constants.SIDEBAR_ARCHIVED, 'public': (not current_user.is_anonymous), "page": "archived",
|
||||||
"show_text": _('Show Archived Books'), "config_show": content})
|
"show_text": _('Show Archived Books'), "config_show": content})
|
||||||
if not simple:
|
if not simple:
|
||||||
sidebar.append(
|
sidebar.append(
|
||||||
{"glyph": "glyphicon-th-list", "text": _('Books List'), "link": 'web.books_table', "id": "list",
|
{"glyph": "glyphicon-th-list", "text": _('Books List'), "link": 'web.books_table', "id": "list",
|
||||||
"visibility": constants.SIDEBAR_LIST, 'public': (not current_user.is_anonymous), "page": "list",
|
"visibility": constants.SIDEBAR_LIST, 'public': (not current_user.is_anonymous),
|
||||||
"show_text": _('Show Books List'), "config_show": content})
|
"show_text": _('Show Books List'), "config_show": content, "no_param":True})
|
||||||
g.shelves_access = ub.session.query(ub.Shelf).filter(
|
g.shelves_access = ub.session.query(ub.Shelf).filter(
|
||||||
or_(ub.Shelf.is_public == 1, ub.Shelf.user_id == current_user.id)).order_by(ub.Shelf.name).all()
|
or_(ub.Shelf.is_public == 1, ub.Shelf.user_id == current_user.id)).order_by(ub.Shelf.name).all()
|
||||||
|
|
||||||
|
|||||||
@@ -61,11 +61,13 @@ class ReverseProxied(object):
|
|||||||
|
|
||||||
def __call__(self, environ, start_response):
|
def __call__(self, environ, start_response):
|
||||||
self.proxied = False
|
self.proxied = False
|
||||||
|
self.script_name = "/"
|
||||||
script_name = environ.get('HTTP_X_SCRIPT_NAME', '')
|
script_name = environ.get('HTTP_X_SCRIPT_NAME', '')
|
||||||
if script_name:
|
if script_name:
|
||||||
self.proxied = True
|
self.proxied = True
|
||||||
environ['SCRIPT_NAME'] = script_name
|
environ['SCRIPT_NAME'] = script_name
|
||||||
path_info = environ.get('PATH_INFO', '')
|
path_info = environ.get('PATH_INFO', '')
|
||||||
|
self.script_name = script_name
|
||||||
if path_info and path_info.startswith(script_name):
|
if path_info and path_info.startswith(script_name):
|
||||||
environ['PATH_INFO'] = path_info[len(script_name):]
|
environ['PATH_INFO'] = path_info[len(script_name):]
|
||||||
|
|
||||||
|
|||||||
+19
-5
@@ -177,11 +177,24 @@ def adv_search_serie(q, include_series_inputs, exclude_series_inputs):
|
|||||||
q = q.filter(not_(db.Books.series.any(db.Series.id == serie)))
|
q = q.filter(not_(db.Books.series.any(db.Series.id == serie)))
|
||||||
return q
|
return q
|
||||||
|
|
||||||
|
|
||||||
def adv_search_shelf(q, include_shelf_inputs, exclude_shelf_inputs):
|
def adv_search_shelf(q, include_shelf_inputs, exclude_shelf_inputs):
|
||||||
q = q.outerjoin(ub.BookShelf, db.Books.id == ub.BookShelf.book_id)\
|
if len(include_shelf_inputs):
|
||||||
.filter(or_(ub.BookShelf.shelf == None, ub.BookShelf.shelf.notin_(exclude_shelf_inputs)))
|
in_shelf = (
|
||||||
if len(include_shelf_inputs) > 0:
|
calibre_db.session.query(ub.BookShelf.id)
|
||||||
q = q.filter(ub.BookShelf.shelf.in_(include_shelf_inputs))
|
.filter(ub.BookShelf.book_id == db.Books.id)
|
||||||
|
.filter(ub.BookShelf.shelf.in_(include_shelf_inputs))
|
||||||
|
.exists()
|
||||||
|
)
|
||||||
|
q = q.filter(in_shelf)
|
||||||
|
if len(exclude_shelf_inputs):
|
||||||
|
ex_shelf = (
|
||||||
|
calibre_db.session.query(ub.BookShelf.id)
|
||||||
|
.filter(ub.BookShelf.book_id == db.Books.id)
|
||||||
|
.filter(ub.BookShelf.shelf.in_(exclude_shelf_inputs))
|
||||||
|
.exists()
|
||||||
|
)
|
||||||
|
q = q.filter(~ex_shelf)
|
||||||
return q
|
return q
|
||||||
|
|
||||||
def extend_search_term(searchterm,
|
def extend_search_term(searchterm,
|
||||||
@@ -245,7 +258,6 @@ def render_adv_search_results(term, offset=None, order=None, limit=None):
|
|||||||
|
|
||||||
cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
|
cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
|
||||||
calibre_db.create_functions()
|
calibre_db.create_functions()
|
||||||
# calibre_db.session.connection().connection.connection.create_function("lower", 1, db.lcase)
|
|
||||||
query = calibre_db.generate_linked_query(config.config_read_column, db.Books)
|
query = calibre_db.generate_linked_query(config.config_read_column, db.Books)
|
||||||
q = query.outerjoin(db.books_series_link, db.Books.id == db.books_series_link.c.book)\
|
q = query.outerjoin(db.books_series_link, db.Books.id == db.books_series_link.c.book)\
|
||||||
.outerjoin(db.Series)\
|
.outerjoin(db.Series)\
|
||||||
@@ -341,10 +353,12 @@ def render_adv_search_results(term, offset=None, order=None, limit=None):
|
|||||||
q = adv_search_ratings(q, rating_high, rating_low)
|
q = adv_search_ratings(q, rating_high, rating_low)
|
||||||
|
|
||||||
if description:
|
if description:
|
||||||
|
pass
|
||||||
q = q.filter(db.Books.comments.any(func.lower(db.Comments.text).ilike("%" + description + "%")))
|
q = q.filter(db.Books.comments.any(func.lower(db.Comments.text).ilike("%" + description + "%")))
|
||||||
|
|
||||||
# search custom columns
|
# search custom columns
|
||||||
try:
|
try:
|
||||||
|
pass
|
||||||
q = adv_search_custom_columns(cc, term, q)
|
q = adv_search_custom_columns(cc, term, q)
|
||||||
except AttributeError as ex:
|
except AttributeError as ex:
|
||||||
log.debug_or_exception(ex)
|
log.debug_or_exception(ex)
|
||||||
|
|||||||
@@ -135,5 +135,5 @@ def metadata_search():
|
|||||||
if active.get(c.__id__, True)
|
if active.get(c.__id__, True)
|
||||||
}
|
}
|
||||||
for future in concurrent.futures.as_completed(meta):
|
for future in concurrent.futures.as_completed(meta):
|
||||||
data.extend([asdict(x) for x in future.result() if x])
|
data.extend([asdict(x) for x in (future.result() or []) if x])
|
||||||
return make_response(jsonify(data))
|
return make_response(jsonify(data))
|
||||||
|
|||||||
+15
-1
@@ -41,7 +41,7 @@ except ImportError:
|
|||||||
VERSION = 'Tornado ' + _version
|
VERSION = 'Tornado ' + _version
|
||||||
_GEVENT = False
|
_GEVENT = False
|
||||||
|
|
||||||
from . import logger
|
from . import logger, constants
|
||||||
|
|
||||||
|
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
@@ -216,6 +216,13 @@ class WebServer(object):
|
|||||||
try:
|
try:
|
||||||
sock, output = self._make_gevent_listener()
|
sock, output = self._make_gevent_listener()
|
||||||
log.info('Starting Gevent server on %s', output)
|
log.info('Starting Gevent server on %s', output)
|
||||||
|
try:
|
||||||
|
# Also print to stdout so interactive terminals show a clear success message
|
||||||
|
if constants.APP_MODE not in ['development', 'test']:
|
||||||
|
print(f"Calibre-Web: server started on {output}")
|
||||||
|
except Exception:
|
||||||
|
print(f"Calibre-Web: error {output}")
|
||||||
|
pass
|
||||||
self.wsgiserver = WSGIServer(sock, self.app, log=self.access_logger, handler_class=MyWSGIHandler,
|
self.wsgiserver = WSGIServer(sock, self.app, log=self.access_logger, handler_class=MyWSGIHandler,
|
||||||
error_log=log,
|
error_log=log,
|
||||||
spawn=Pool(), **ssl_args)
|
spawn=Pool(), **ssl_args)
|
||||||
@@ -266,6 +273,13 @@ class WebServer(object):
|
|||||||
output = _readable_listen_address(self.listen_address, self.listen_port)
|
output = _readable_listen_address(self.listen_address, self.listen_port)
|
||||||
http_server.listen(self.listen_port, self.listen_address)
|
http_server.listen(self.listen_port, self.listen_address)
|
||||||
log.info('Starting Tornado server on %s', output)
|
log.info('Starting Tornado server on %s', output)
|
||||||
|
# Also print to stdout so interactive terminals show a clear success message
|
||||||
|
try:
|
||||||
|
if constants.APP_MODE not in ['development', 'test']:
|
||||||
|
print(f"Calibre-Web: server started on {output}")
|
||||||
|
except Exception:
|
||||||
|
print(f"Calibre-Web: error {output}")
|
||||||
|
pass
|
||||||
|
|
||||||
self.wsgiserver = IOLoop.current()
|
self.wsgiserver = IOLoop.current()
|
||||||
self.wsgiserver.start()
|
self.wsgiserver.start()
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import requests
|
|||||||
|
|
||||||
from goodreads.client import GoodreadsClient
|
from goodreads.client import GoodreadsClient
|
||||||
from goodreads.request import GoodreadsRequest
|
from goodreads.request import GoodreadsRequest
|
||||||
import xmltodict
|
from lxml import etree
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import Levenshtein
|
import Levenshtein
|
||||||
@@ -33,6 +33,39 @@ from .. import logger
|
|||||||
from ..clean_html import clean_string
|
from ..clean_html import clean_string
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def etree_to_dict(t):
|
||||||
|
"""
|
||||||
|
Convert lxml ElementTree to a nested dict (similar to xmltodict).
|
||||||
|
"""
|
||||||
|
d = {t.tag: {} if t.attrib else None}
|
||||||
|
children = list(t)
|
||||||
|
|
||||||
|
if children:
|
||||||
|
dd = {}
|
||||||
|
for dc in map(etree_to_dict, children):
|
||||||
|
for k, v in dc.items():
|
||||||
|
if k in dd:
|
||||||
|
if not isinstance(dd[k], list):
|
||||||
|
dd[k] = [dd[k]]
|
||||||
|
dd[k].append(v)
|
||||||
|
else:
|
||||||
|
dd[k] = v
|
||||||
|
d = {t.tag: dd}
|
||||||
|
|
||||||
|
if t.attrib:
|
||||||
|
d[t.tag].update(('@' + k, v) for k, v in t.attrib.items())
|
||||||
|
|
||||||
|
text = (t.text or '').strip()
|
||||||
|
if text:
|
||||||
|
if children or t.attrib:
|
||||||
|
d[t.tag]['#text'] = text
|
||||||
|
else:
|
||||||
|
d[t.tag] = text
|
||||||
|
|
||||||
|
return d
|
||||||
|
|
||||||
class my_GoodreadsClient(GoodreadsClient):
|
class my_GoodreadsClient(GoodreadsClient):
|
||||||
|
|
||||||
def request(self, *args, **kwargs):
|
def request(self, *args, **kwargs):
|
||||||
@@ -59,7 +92,9 @@ class my_GoodreadsRequest(GoodreadsRequest):
|
|||||||
if resp.status_code != 200:
|
if resp.status_code != 200:
|
||||||
raise GoodreadsRequestException(resp.reason, self.path)
|
raise GoodreadsRequestException(resp.reason, self.path)
|
||||||
if self.req_format == 'xml':
|
if self.req_format == 'xml':
|
||||||
data_dict = xmltodict.parse(resp.content)
|
root = etree.fromstring(resp.content, parser=etree.XMLParser(resolve_entities=False, no_network=True))
|
||||||
|
data_dict = etree_to_dict(root)
|
||||||
|
|
||||||
return data_dict['GoodreadsResponse']
|
return data_dict['GoodreadsResponse']
|
||||||
else:
|
else:
|
||||||
raise Exception("Invalid format")
|
raise Exception("Invalid format")
|
||||||
|
|||||||
@@ -31,6 +31,16 @@ except ImportError:
|
|||||||
log = logger.create()
|
log = logger.create()
|
||||||
|
|
||||||
|
|
||||||
|
def _escape_ldap_filter(s):
|
||||||
|
"""Escape special characters for safe use in LDAP filter strings (RFC 4515)."""
|
||||||
|
s = s.replace('\\', '\\5c')
|
||||||
|
s = s.replace('*', '\\2a')
|
||||||
|
s = s.replace('(', '\\28')
|
||||||
|
s = s.replace(')', '\\29')
|
||||||
|
s = s.replace('\x00', '\\00')
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
class LDAPLogger(object):
|
class LDAPLogger(object):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -148,9 +158,11 @@ def bind_user(username, password):
|
|||||||
|
|
||||||
:returns: True if login succeeded, False if login failed, None if server unavailable.
|
:returns: True if login succeeded, False if login failed, None if server unavailable.
|
||||||
'''
|
'''
|
||||||
|
# Escape LDAP special characters to prevent LDAP injection in search filters
|
||||||
|
safe_username = _escape_ldap_filter(username)
|
||||||
try:
|
try:
|
||||||
if _ldap.get_object_details(username):
|
if _ldap.get_object_details(safe_username):
|
||||||
result = _ldap.bind_user(username, password)
|
result = _ldap.bind_user(safe_username, password)
|
||||||
log.debug("LDAP login '%s': %r", username, result)
|
log.debug("LDAP login '%s': %r", username, result)
|
||||||
return result is not None, None
|
return result is not None, None
|
||||||
return None, None # User not found
|
return None, None # User not found
|
||||||
|
|||||||
@@ -235,7 +235,7 @@ class CalibreTask:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def dead(self):
|
def dead(self):
|
||||||
"""Determines whether this task can be garbage collected
|
"""Determines whether or not this task can be garbage collected
|
||||||
|
|
||||||
We have a separate dictating this because there may be certain tasks that want to override this
|
We have a separate dictating this because there may be certain tasks that want to override this
|
||||||
"""
|
"""
|
||||||
|
|||||||
+50
-3
@@ -102,6 +102,52 @@ def add_to_shelf(shelf_id, book_id):
|
|||||||
return "", 204
|
return "", 204
|
||||||
|
|
||||||
|
|
||||||
|
@shelf.route("/shelf/massremove/<int:shelf_id>", methods=["POST"])
|
||||||
|
@user_login_required
|
||||||
|
def search_from_shelf(shelf_id):
|
||||||
|
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
||||||
|
if shelf is None:
|
||||||
|
log.error("Invalid shelf specified: {}".format(shelf_id))
|
||||||
|
flash(_("Invalid shelf specified"), category="error")
|
||||||
|
return redirect(url_for('web.index'))
|
||||||
|
|
||||||
|
if not check_shelf_edit_permissions(shelf):
|
||||||
|
log.warning("You are not allowed to remove a book from the shelf".format(shelf.name))
|
||||||
|
flash(_("You are not allowed to remove a book from the shelf"), category="error")
|
||||||
|
return redirect(url_for('web.index'))
|
||||||
|
|
||||||
|
if current_user.id in ub.searched_ids and ub.searched_ids[current_user.id]:
|
||||||
|
books_from_shelf = list()
|
||||||
|
books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).all()
|
||||||
|
if books_in_shelf:
|
||||||
|
book_ids = [book_id.book_id for book_id in books_in_shelf]
|
||||||
|
for searchid in ub.searched_ids[current_user.id]:
|
||||||
|
if searchid in book_ids:
|
||||||
|
books_from_shelf.append(searchid)
|
||||||
|
else:
|
||||||
|
log.error("No Books are part of {}".format(shelf.name))
|
||||||
|
flash(_("No Books are part of the shelf: %(name)s", name=shelf.name), category="error")
|
||||||
|
return redirect(url_for('web.index'))
|
||||||
|
|
||||||
|
# maxOrder = ub.session.query(func.max(ub.BookShelf.order)).filter(ub.BookShelf.shelf == shelf_id).first()[0] or 0
|
||||||
|
|
||||||
|
for book in books_from_shelf:
|
||||||
|
ub.session.delete(ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).filter(
|
||||||
|
ub.BookShelf.book_id == book).first())
|
||||||
|
shelf.last_modified = datetime.now(timezone.utc)
|
||||||
|
try:
|
||||||
|
ub.session.commit()
|
||||||
|
flash(_("Books have been removed from shelf: %(sname)s", sname=shelf.name), category="success")
|
||||||
|
except (OperationalError, InvalidRequestError) as e:
|
||||||
|
ub.session.rollback()
|
||||||
|
log.error_or_exception("Settings Database error: {}".format(e))
|
||||||
|
flash(_("Oops! Database Error: %(error)s.", error=e.orig), category="error")
|
||||||
|
else:
|
||||||
|
log.error("Could not remove books from shelf: {}".format(shelf.name))
|
||||||
|
flash(_("Could not remove books from shelf: %(sname)s", sname=shelf.name), category="error")
|
||||||
|
return redirect(url_for('web.index'))
|
||||||
|
|
||||||
|
|
||||||
@shelf.route("/shelf/massadd/<int:shelf_id>", methods=["POST"])
|
@shelf.route("/shelf/massadd/<int:shelf_id>", methods=["POST"])
|
||||||
@user_login_required
|
@user_login_required
|
||||||
def search_to_shelf(shelf_id):
|
def search_to_shelf(shelf_id):
|
||||||
@@ -120,9 +166,7 @@ def search_to_shelf(shelf_id):
|
|||||||
books_for_shelf = list()
|
books_for_shelf = list()
|
||||||
books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).all()
|
books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).all()
|
||||||
if books_in_shelf:
|
if books_in_shelf:
|
||||||
book_ids = list()
|
book_ids = [book_id.book_id for book_id in books_in_shelf]
|
||||||
for book_id in books_in_shelf:
|
|
||||||
book_ids.append(book_id.book_id)
|
|
||||||
for searchid in ub.searched_ids[current_user.id]:
|
for searchid in ub.searched_ids[current_user.id]:
|
||||||
if searchid not in book_ids:
|
if searchid not in book_ids:
|
||||||
books_for_shelf.append(searchid)
|
books_for_shelf.append(searchid)
|
||||||
@@ -264,6 +308,9 @@ def order_shelf(shelf_id):
|
|||||||
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
||||||
if shelf and check_shelf_view_permissions(shelf):
|
if shelf and check_shelf_view_permissions(shelf):
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
|
if not check_shelf_edit_permissions(shelf):
|
||||||
|
flash(_("Sorry you are not allowed to edit this shelf"), category="error")
|
||||||
|
return redirect(url_for('web.index'))
|
||||||
to_save = request.form.to_dict()
|
to_save = request.form.to_dict()
|
||||||
books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).order_by(
|
books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).order_by(
|
||||||
ub.BookShelf.order.asc()).all()
|
ub.BookShelf.order.asc()).all()
|
||||||
|
|||||||
+88
-44
@@ -2018,7 +2018,7 @@ body > div.container-fluid > div > div.col-sm-10 > div.pagination > .page-previo
|
|||||||
display: none
|
display: none
|
||||||
}
|
}
|
||||||
|
|
||||||
body.authorlist > div.container-fluid > div > div.col-sm-10 > div.container > div.col-xs-12, body.catlist > div.container-fluid > div > div.col-sm-10 > div.container > div.col-xs-12, body.langlist > div.container-fluid > div > div.col-sm-10 > div.container > div.col-sm-6, body.serieslist > div.container-fluid > div > div.col-sm-10 > div.container > div.col-sm-6 {
|
body.authorlist > div.container-fluid > div > div.col-sm-10 > div.container1 > div.col-xs-12, body.downloadlist > div.container-fluid > div > div.col-sm-10 > div.container1 > div.col-xs-12, body.catlist > div.container-fluid > div > div.col-sm-10 > div.container1 > div.col-xs-12, body.langlist > div.container-fluid > div > div.col-sm-10 > div.container1 > div.col-sm-6, body.serieslist > div.container-fluid > div > div.col-sm-10 > div.container1 > div.col-sm-6 {
|
||||||
width: 100%
|
width: 100%
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2042,7 +2042,7 @@ body.serieslist > div.container-fluid > div > div.col-sm-10:before {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body > div.container-fluid > div > div.col-sm-10 > div.container {
|
body > div.container-fluid > div > div.col-sm-10 > div.container1 {
|
||||||
width: calc(80% - 30px);
|
width: calc(80% - 30px);
|
||||||
margin: 120px 0 30px 20%;
|
margin: 120px 0 30px 20%;
|
||||||
border-top: 2px solid rgba(0, 0, 0, .3);
|
border-top: 2px solid rgba(0, 0, 0, .3);
|
||||||
@@ -2051,19 +2051,19 @@ body > div.container-fluid > div > div.col-sm-10 > div.container {
|
|||||||
max-width: calc(80% - 30px)
|
max-width: calc(80% - 30px)
|
||||||
}
|
}
|
||||||
|
|
||||||
body > div.container-fluid > div > div.col-sm-10 > div.container > div {
|
body > div.container-fluid > div > div.col-sm-10 > div.container1 > div {
|
||||||
padding: 0
|
padding: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
body > div.container-fluid > div > div.col-sm-10 > div.container > div > .row {
|
body > div.container-fluid > div > div.col-sm-10 > div.container1 > div > .row {
|
||||||
padding: 4px 0
|
padding: 4px 0
|
||||||
}
|
}
|
||||||
|
|
||||||
body > div.container-fluid > div > div.col-sm-10 > div.container > div > .row:nth-of-type(even) {
|
body > div.container-fluid > div > div.col-sm-10 > div.container1 > div > .row:nth-of-type(even) {
|
||||||
background: hsla(0, 0%, 100%, .02)
|
background: hsla(0, 0%, 100%, .02)
|
||||||
}
|
}
|
||||||
|
|
||||||
body > div.container-fluid > div > div.col-sm-10 > div.container > div > .row > .col-xs-6 {
|
body > div.container-fluid > div > div.col-sm-10 > div.container1 > div > .row > .col-xs-6 {
|
||||||
width: calc(91.666% - 30px);
|
width: calc(91.666% - 30px);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
-o-text-overflow: ellipsis;
|
-o-text-overflow: ellipsis;
|
||||||
@@ -2072,7 +2072,7 @@ body > div.container-fluid > div > div.col-sm-10 > div.container > div > .row >
|
|||||||
padding-left: 60px
|
padding-left: 60px
|
||||||
}
|
}
|
||||||
|
|
||||||
body > div.container-fluid > div > div.col-sm-10 > div.container > div > .row > .col-xs-6 > a {
|
body > div.container-fluid > div > div.col-sm-10 > div.container1 > div > .row > .col-xs-6 > a {
|
||||||
-o-text-overflow: ellipsis;
|
-o-text-overflow: ellipsis;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -2082,7 +2082,7 @@ body > div.container-fluid > div > div.col-sm-10 > div.container > div > .row >
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
body > div.container-fluid > div > div.col-sm-10 > div.discover > div.container {
|
body > div.container-fluid > div > div.col-sm-10 > div.discover > div.container1 {
|
||||||
width: calc(80% - 30px);
|
width: calc(80% - 30px);
|
||||||
margin: 120px 0 30px 20%;
|
margin: 120px 0 30px 20%;
|
||||||
border-top: 2px solid rgba(0, 0, 0, .3);
|
border-top: 2px solid rgba(0, 0, 0, .3);
|
||||||
@@ -2091,19 +2091,19 @@ body > div.container-fluid > div > div.col-sm-10 > div.discover > div.container
|
|||||||
max-width: calc(80% - 30px)
|
max-width: calc(80% - 30px)
|
||||||
}
|
}
|
||||||
|
|
||||||
body > div.container-fluid > div > div.col-sm-10 > div.discover > div.container > div {
|
body > div.container-fluid > div > div.col-sm-10 > div.discover > div.container1 > div {
|
||||||
padding: 0
|
padding: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
body > div.container-fluid > div > div.col-sm-10 > div.discover > div.container > div > .row {
|
body > div.container-fluid > div > div.col-sm-10 > div.discover > div.container1 > div > .row {
|
||||||
padding: 4px 0
|
padding: 4px 0
|
||||||
}
|
}
|
||||||
|
|
||||||
body > div.container-fluid > div > div.col-sm-10 > div.discover > div.container > div > .row:nth-of-type(even) {
|
body > div.container-fluid > div > div.col-sm-10 > div.discover > div.container1 > div > .row:nth-of-type(even) {
|
||||||
background: hsla(0, 0%, 100%, .02)
|
background: hsla(0, 0%, 100%, .02)
|
||||||
}
|
}
|
||||||
|
|
||||||
body > div.container-fluid > div > div.col-sm-10 > div.discover > div.container > div > .row > .col-xs-6 {
|
body > div.container-fluid > div > div.col-sm-10 > div.discover > div.container1 > div > .row > .col-xs-6 {
|
||||||
width: calc(91.666% - 30px);
|
width: calc(91.666% - 30px);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
-o-text-overflow: ellipsis;
|
-o-text-overflow: ellipsis;
|
||||||
@@ -2112,7 +2112,7 @@ body > div.container-fluid > div > div.col-sm-10 > div.discover > div.container
|
|||||||
padding-left: 60px
|
padding-left: 60px
|
||||||
}
|
}
|
||||||
|
|
||||||
body > div.container-fluid > div > div.col-sm-10 > div.discover > div.container > div > .row > .col-xs-6 > a {
|
body > div.container-fluid > div > div.col-sm-10 > div.discover > div.container1 > div > .row > .col-xs-6 > a {
|
||||||
-o-text-overflow: ellipsis;
|
-o-text-overflow: ellipsis;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -2146,7 +2146,6 @@ body.catlist > div.container-fluid > div.row-fluid > div.col-sm-10:before {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
body.authorlist > div.container-fluid > div.row-fluid > div.col-sm-10:before, body.langlist > div.container-fluid > div > div.col-sm-10:before {
|
body.authorlist > div.container-fluid > div.row-fluid > div.col-sm-10:before, body.langlist > div.container-fluid > div > div.col-sm-10:before {
|
||||||
font-family: 'Glyphicons Halflings', serif;
|
font-family: 'Glyphicons Halflings', serif;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
@@ -2172,10 +2171,12 @@ body.authorlist > div.container-fluid > div.row-fluid > div.col-sm-10:before {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body.langlist > div.container-fluid > div > div.col-sm-10:before {
|
body.langlist > div.container-fluid > div > div.col-sm-10:before {
|
||||||
content: "\e034"
|
content: "\e034";
|
||||||
|
top: 240px;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.authorlist > div.container-fluid > div > div.col-sm-10 > div.container:before, body.catlist > div.container-fluid > div > div.col-sm-10 > div.container:before, body.langlist > div.container-fluid > div > div.col-sm-10 > div.container:before, body.me > div.container-fluid > div > div.col-sm-10 > div.discover:before, body.serieslist > div.container-fluid > div > div.col-sm-10 > div.container:before {
|
|
||||||
|
body.authorlist > div.container-fluid > div > div.col-sm-10 > div.container1:before, body.downloadlist > div.container-fluid > div > div.col-sm-10 > div.container1:before, body.catlist > div.container-fluid > div > div.col-sm-10 > div.container1:before, body.langlist > div.container-fluid > div > div.col-sm-10 > div.container1:before, body.me > div.container-fluid > div > div.col-sm-10 > div.discover:before, body.serieslist > div.container-fluid > div > div.col-sm-10 > div.container1:before {
|
||||||
top: 60px;
|
top: 60px;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
color: #eee;
|
color: #eee;
|
||||||
@@ -2186,63 +2187,104 @@ body.authorlist > div.container-fluid > div > div.col-sm-10 > div.container:befo
|
|||||||
font-weight: 400
|
font-weight: 400
|
||||||
}
|
}
|
||||||
|
|
||||||
body.authorlist > div.container-fluid > div > div.col-sm-10 > div.container:before {
|
body.authorlist > div.container-fluid > div > div.col-sm-10 > div.container1:before {
|
||||||
content: "Authors";
|
content: "Authors";
|
||||||
top: 115px;
|
top: 115px;
|
||||||
|
display: table;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 992px) {
|
@media screen and (max-width: 992px) {
|
||||||
body.authorlist > div.container-fluid > div > div.col-sm-10 > div.container:before {
|
body.authorlist > div.container-fluid > div > div.col-sm-10 > div.container1:before {
|
||||||
top: 60px;
|
top: 60px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body.catlist > div.container-fluid > div > div.col-sm-10 > div.container:before {
|
@media screen and (max-width: 992px) {
|
||||||
|
body.downloadlist > div.container-fluid > div > div.col-sm-10 > div.container1:before {
|
||||||
|
top: 60px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body.downloadlist > div.container-fluid > div.row-fluid > div.col-sm-10:before {
|
||||||
|
font-family: Glyphicons Halflings, serif;
|
||||||
|
content: "\E026";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1;
|
||||||
|
font-size: 6vw;
|
||||||
|
position: fixed;
|
||||||
|
left: 240px;
|
||||||
|
top: 240px;
|
||||||
|
width: calc(20% - 55px);
|
||||||
|
text-align: center
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media screen and (max-width: 992px) {
|
||||||
|
body.downloadlist > div.container-fluid > div.row-fluid > div.col-sm-10:before {
|
||||||
|
top: 180px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body.downloadlist > div.container-fluid > div > div.col-sm-10 > div.container1:before {
|
||||||
|
content: "Downloads";
|
||||||
|
top: 115px;
|
||||||
|
display: table;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
body.catlist > div.container-fluid > div > div.col-sm-10 > div.container1:before {
|
||||||
content: "Categories";
|
content: "Categories";
|
||||||
top: 115px;
|
top: 115px;
|
||||||
|
display: table;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 992px) {
|
@media screen and (max-width: 992px) {
|
||||||
body.catlist > div.container-fluid > div > div.col-sm-10 > div.container:before {
|
body.catlist > div.container-fluid > div > div.col-sm-10 > div.container1:before {
|
||||||
top: 60px;
|
top: 60px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body.serieslist > div.container-fluid > div > div.col-sm-10 > div.container:before {
|
body.serieslist > div.container-fluid > div > div.col-sm-10 > div.container1:before {
|
||||||
content: "Series";
|
content: "Series";
|
||||||
top: 115px;
|
top: 115px;
|
||||||
|
display: table;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 992px) {
|
@media screen and (max-width: 992px) {
|
||||||
body.serieslist > div.container-fluid > div > div.col-sm-10 > div.container:before {
|
body.serieslist > div.container-fluid > div > div.col-sm-10 > div.container1:before {
|
||||||
top: 60px;
|
top: 60px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body.ratingslist > div.container-fluid > div > div.col-sm-10 > div.container:before {
|
body.ratingslist > div.container-fluid > div > div.col-sm-10 > div.container1:before {
|
||||||
content: "File formats";
|
content: "File formats";
|
||||||
top: 115px;
|
top: 115px;
|
||||||
|
display: table;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 992px) {
|
@media screen and (max-width: 992px) {
|
||||||
body.ratingslist > div.container-fluid > div > div.col-sm-10 > div.container:before {
|
body.ratingslist > div.container-fluid > div > div.col-sm-10 > div.container1:before {
|
||||||
top: 60px;
|
top: 60px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body.formatslist > div.container-fluid > div > div.col-sm-10 > div.container:before {
|
body.formatslist > div.container-fluid > div > div.col-sm-10 > div.container1:before {
|
||||||
content: "File formats";
|
content: "File formats";
|
||||||
top: 115px;
|
top: 115px;
|
||||||
|
display: table;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 992px) {
|
@media screen and (max-width: 992px) {
|
||||||
body.formatslist > div.container-fluid > div > div.col-sm-10 > div.container:before {
|
body.formatslist > div.container-fluid > div > div.col-sm-10 > div.container1:before {
|
||||||
top: 60px;
|
top: 60px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body.langlist > div.container-fluid > div > div.col-sm-10 > div.container:before {
|
body.langlist > div.container-fluid > div > div.col-sm-10 > div.container1:before {
|
||||||
content: "Languages"
|
content: "Languages";
|
||||||
|
top: 115px;
|
||||||
|
display: table;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.advsearch > div.container-fluid > div > div.col-sm-10 > div.col-md-10.col-lg-6 {
|
body.advsearch > div.container-fluid > div > div.col-sm-10 > div.col-md-10.col-lg-6 {
|
||||||
@@ -6421,7 +6463,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
|
|||||||
display: none !important
|
display: none !important
|
||||||
}
|
}
|
||||||
|
|
||||||
.shelforder > .container-fluid > .row-fluid > .col-sm-10 > div.col-sm-6.col-lg-6.col-xs-6 > h2:before, body.advsearch > div.container-fluid > div > div.col-sm-10:before, body.authorlist > div.container-fluid > div > div.col-sm-10 > div.container:before, body.catlist > div.container-fluid > div > div.col-sm-10 > div.container:before, body.langlist > div.container-fluid > div > div.col-sm-10 > div.container:before, body.login .navbar > .container-fluid > .navbar-header > button.navbar-toggle, body.serieslist > div.container-fluid > div > div.col-sm-10 > div.container:before {
|
.shelforder > .container-fluid > .row-fluid > .col-sm-10 > div.col-sm-6.col-lg-6.col-xs-6 > h2:before, body.advsearch > div.container-fluid > div > div.col-sm-10:before, body.authorlist > div.container-fluid > div > div.col-sm-10 > div.container1:before, body.catlist > div.container-fluid > div > div.col-sm-10 > div.container1:before, body.langlist > div.container-fluid > div > div.col-sm-10 > div.container1:before, body.downloadlist > div.container-fluid > div > div.col-sm-10 > div.container1:before, body.login .navbar > .container-fluid > .navbar-header > button.navbar-toggle, body.serieslist > div.container-fluid > div > div.col-sm-10 > div.container1:before {
|
||||||
display: none
|
display: none
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6527,7 +6569,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
|
|||||||
background-size: 30px
|
background-size: 30px
|
||||||
}
|
}
|
||||||
|
|
||||||
body.advanced_search > div.container-fluid > div > div.col-sm-10 > div.col-sm-8:before, body.author:not(.authorlist) .undefined-img:before, body.authorlist > div.container-fluid > div.row-fluid > div.col-sm-10:before, body.catlist > div.container-fluid > div.row-fluid > div.col-sm-10:before, body.langlist > div.container-fluid > div > div.col-sm-10:before, body.ratingslist > div.container-fluid > div > div.col-sm-10:before, body.serieslist > div.container-fluid > div > div.col-sm-10:before, body.shelforder > div.container-fluid > div.row-fluid > div.col-sm-10:before {
|
body.advanced_search > div.container-fluid > div > div.col-sm-10 > div.col-sm-8:before, body.author:not(.authorlist) .undefined-img:before, body.authorlist > div.container-fluid > div.row-fluid > div.col-sm-10:before, body.catlist > div.container-fluid > div.row-fluid > div.col-sm-10:before, body.downloadlist > div.container-fluid > div.row-fluid > div.col-sm-10:before, body.langlist > div.container-fluid > div > div.col-sm-10:before, body.ratingslist > div.container-fluid > div > div.col-sm-10:before, body.serieslist > div.container-fluid > div > div.col-sm-10:before, body.shelforder > div.container-fluid > div.row-fluid > div.col-sm-10:before {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 62px;
|
top: 62px;
|
||||||
right: 23px;
|
right: 23px;
|
||||||
@@ -6547,7 +6589,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
|
|||||||
border-top: 1px solid hsla(0, 0%, 78%, .15)
|
border-top: 1px solid hsla(0, 0%, 78%, .15)
|
||||||
}
|
}
|
||||||
|
|
||||||
body > div.container-fluid > div > div.col-sm-10 > div.container {
|
body > div.container-fluid > div > div.col-sm-10 > div.container1 {
|
||||||
left: 0;
|
left: 0;
|
||||||
width: auto;
|
width: auto;
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
@@ -6674,7 +6716,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
|
|||||||
margin-bottom: 5px
|
margin-bottom: 5px
|
||||||
}
|
}
|
||||||
|
|
||||||
body > div.container-fluid > div > div.col-sm-10 > div.container > div > .row {
|
body > div.container-fluid > div > div.col-sm-10 > div.container1 > div > .row {
|
||||||
padding: 0
|
padding: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7441,11 +7483,11 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
|
|||||||
text-decoration: none
|
text-decoration: none
|
||||||
}
|
}
|
||||||
|
|
||||||
body > div.container-fluid > div > div.col-sm-10 > div.container > div > .row:nth-of-type(even) {
|
body > div.container-fluid > div > div.col-sm-10 > div.container1 > div > .row:nth-of-type(even) {
|
||||||
background: 0 0
|
background: 0 0
|
||||||
}
|
}
|
||||||
|
|
||||||
body > div.container-fluid > div > div.col-sm-10 > div.container > div > .row {
|
body > div.container-fluid > div > div.col-sm-10 > div.container1 > div > .row {
|
||||||
border-bottom: 1px solid hsla(0, 0%, 78%, .15)
|
border-bottom: 1px solid hsla(0, 0%, 78%, .15)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7600,7 +7642,7 @@ body.formatslist > div.container-fluid > div > div.col-sm-10:before {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body.publisherlist > div.container-fluid > div > div.col-sm-10 > div.container:before {
|
body.publisherlist > div.container-fluid > div > div.col-sm-10 > div.container1:before {
|
||||||
content: "Publishers";
|
content: "Publishers";
|
||||||
top: 115px;
|
top: 115px;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
@@ -7608,17 +7650,18 @@ body.publisherlist > div.container-fluid > div > div.col-sm-10 > div.container:b
|
|||||||
line-height: 1;
|
line-height: 1;
|
||||||
padding-left: 15px;
|
padding-left: 15px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
display: table;
|
||||||
font-family: Open Sans Bold, Helvetica Neue, Helvetica, Arial, sans-serif;
|
font-family: Open Sans Bold, Helvetica Neue, Helvetica, Arial, sans-serif;
|
||||||
font-weight: 400
|
font-weight: 400
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 992px) {
|
@media screen and (max-width: 992px) {
|
||||||
body.publisherlist > div.container-fluid > div > div.col-sm-10 > div.container:before {
|
body.publisherlist > div.container-fluid > div > div.col-sm-10 > div.container1:before {
|
||||||
top: 60px;
|
top: 60px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body.ratingslist > div.container-fluid > div > div.col-sm-10 > div.container:before {
|
body.ratingslist > div.container-fluid > div > div.col-sm-10 > div.container1:before {
|
||||||
content: "Ratings";
|
content: "Ratings";
|
||||||
top: 115px;
|
top: 115px;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
@@ -7626,23 +7669,24 @@ body.ratingslist > div.container-fluid > div > div.col-sm-10 > div.container:bef
|
|||||||
line-height: 1;
|
line-height: 1;
|
||||||
padding-left: 15px;
|
padding-left: 15px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
display: table;
|
||||||
font-family: Open Sans Bold, Helvetica Neue, Helvetica, Arial, sans-serif;
|
font-family: Open Sans Bold, Helvetica Neue, Helvetica, Arial, sans-serif;
|
||||||
font-weight: 400
|
font-weight: 400
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 992px) {
|
@media screen and (max-width: 992px) {
|
||||||
body.ratingslist > div.container-fluid > div > div.col-sm-10 > div.container:before {
|
body.ratingslist > div.container-fluid > div > div.col-sm-10 > div.container1:before {
|
||||||
top: 60px;
|
top: 60px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 768px) {
|
@media screen and (max-width: 768px) {
|
||||||
body.ratingslist > div.container-fluid > div > div.col-sm-10 > div.container:before {
|
body.ratingslist > div.container-fluid > div > div.col-sm-10 > div.container1:before {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body.formatslist > div.container-fluid > div > div.col-sm-10 > div.container:before {
|
body.formatslist > div.container-fluid > div > div.col-sm-10 > div.container1:before {
|
||||||
content: "File formats";
|
content: "File formats";
|
||||||
top: 115px;
|
top: 115px;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
@@ -7655,18 +7699,18 @@ body.formatslist > div.container-fluid > div > div.col-sm-10 > div.container:bef
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 992px) {
|
@media screen and (max-width: 992px) {
|
||||||
body.formatslist > div.container-fluid > div > div.col-sm-10 > div.container:before {
|
body.formatslist > div.container-fluid > div > div.col-sm-10 > div.container1:before {
|
||||||
top: 60px;
|
top: 60px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 768px) {
|
@media screen and (max-width: 768px) {
|
||||||
body.formatslist > div.container-fluid > div > div.col-sm-10 > div.container:before {
|
body.formatslist > div.container-fluid > div > div.col-sm-10 > div.container1:before {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body.publisherlist > div.container-fluid > div > div.col-sm-10 > div.container > div.col-sm-6 {
|
body.publisherlist > div.container-fluid > div > div.col-sm-10 > div.container1 > div.col-sm-6 {
|
||||||
width: 100%
|
width: 100%
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7705,7 +7749,7 @@ body.publisherlist > div.container-fluid > div > div.col-sm-10 > div.container >
|
|||||||
color: hsla(0, 0%, 100%, .7)
|
color: hsla(0, 0%, 100%, .7)
|
||||||
}
|
}
|
||||||
|
|
||||||
body.publisherlist > div.container-fluid > div > div.col-sm-10 > div.container:before {
|
body.publisherlist > div.container-fluid > div > div.col-sm-10 > div.container1:before {
|
||||||
display: none !important
|
display: none !important
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,3 +26,14 @@ body.serieslist.grid-view div.container-fluid > div > div.col-sm-10::before {
|
|||||||
input.datepicker {color: transparent}
|
input.datepicker {color: transparent}
|
||||||
input.datepicker:focus {color: transparent}
|
input.datepicker:focus {color: transparent}
|
||||||
input.datepicker:focus + input {color: #555}
|
input.datepicker:focus + input {color: #555}
|
||||||
|
|
||||||
|
.col-sm-3.col-lg-2.col-xs-6.book.session {
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 767px) {
|
||||||
|
.row-fluid > .col-sm-2 {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,6 +13,11 @@
|
|||||||
color: #000;
|
color: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.amberTheme {
|
||||||
|
background: #e8bf5e;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
.blackTheme {
|
.blackTheme {
|
||||||
background: #000;
|
background: #000;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
|||||||
+166
-80
@@ -1,11 +1,10 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'fontello';
|
font-family: "fontello";
|
||||||
src: url('fonts/fontello.eot?60518104');
|
src: url("fonts/fontello.eot?60518104");
|
||||||
src:
|
src: url("fonts/fontello.eot?60518104#iefix") format("embedded-opentype"),
|
||||||
url('fonts/fontello.eot?60518104#iefix') format('embedded-opentype'),
|
url("fonts/fontello.woff?60518104") format("woff"),
|
||||||
url('fonts/fontello.woff?60518104') format('woff'),
|
url("fonts/fontello.ttf?60518104") format("truetype"),
|
||||||
url('fonts/fontello.ttf?60518104') format('truetype'),
|
url("fonts/fontello.svg?60518104#fontello") format("svg");
|
||||||
url('fonts/fontello.svg?60518104#fontello') format('svg');
|
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
@@ -48,7 +47,7 @@ body {
|
|||||||
-webkit-transition: opacity 0.5s;
|
-webkit-transition: opacity 0.5s;
|
||||||
-moz-transition: opacity 0.5s;
|
-moz-transition: opacity 0.5s;
|
||||||
transition: opacity 0.5s;
|
transition: opacity 0.5s;
|
||||||
z-index: 10;
|
z-index: 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
#titlebar:hover {
|
#titlebar:hover {
|
||||||
@@ -93,14 +92,18 @@ body {
|
|||||||
box-shadow: inset 0 0 6px rgba(155, 155, 155, 0.8);
|
box-shadow: inset 0 0 6px rgba(155, 155, 155, 0.8);
|
||||||
}
|
}
|
||||||
|
|
||||||
#book-title { font-weight: 600; }
|
#book-title {
|
||||||
#title-seperator { display: none; }
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
#title-seperator {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
#viewer {
|
#viewer {
|
||||||
width: 80%;
|
width: 80%;
|
||||||
height: 80%;
|
height: 80%;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
z-index: 2;
|
z-index: 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
@@ -136,6 +139,7 @@ body {
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
padding-top: 160px;
|
padding-top: 160px;
|
||||||
padding-bottom: 160px;
|
padding-bottom: 160px;
|
||||||
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
.arrow:hover {
|
.arrow:hover {
|
||||||
@@ -200,10 +204,16 @@ body {
|
|||||||
max-width: 80%;
|
max-width: 80%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#title-controls { float: right; }
|
#title-controls {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
#panels a::before { visibility: visible; }
|
#panels a::before {
|
||||||
#panels a:hover { color: #aaa; }
|
visibility: visible;
|
||||||
|
}
|
||||||
|
#panels a:hover {
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
.list_item.currentChapter > a,
|
.list_item.currentChapter > a,
|
||||||
.list_item a:hover {
|
.list_item a:hover {
|
||||||
@@ -237,8 +247,12 @@ body {
|
|||||||
margin-top: -1px;
|
margin-top: -1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
input::-webkit-input-placeholder { color: #454545; }
|
input::-webkit-input-placeholder {
|
||||||
input:-moz-placeholder { color: #454545; }
|
color: #454545;
|
||||||
|
}
|
||||||
|
input:-moz-placeholder {
|
||||||
|
color: #454545;
|
||||||
|
}
|
||||||
|
|
||||||
#divider {
|
#divider {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -378,14 +392,14 @@ input:-moz-placeholder { color: #454545; }
|
|||||||
}
|
}
|
||||||
|
|
||||||
.toc_toggle::before {
|
.toc_toggle::before {
|
||||||
content: '▸';
|
content: "▸";
|
||||||
color: #fff;
|
color: #fff;
|
||||||
margin-right: -4px;
|
margin-right: -4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.currentChapter > .toc_toggle::before,
|
.currentChapter > .toc_toggle::before,
|
||||||
.openChapter > .toc_toggle::before {
|
.openChapter > .toc_toggle::before {
|
||||||
content: '▾';
|
content: "▾";
|
||||||
}
|
}
|
||||||
|
|
||||||
.view {
|
.view {
|
||||||
@@ -499,21 +513,33 @@ input:-moz-placeholder { color: #454545; }
|
|||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#settingsPanel .xsmall { font-size: x-small; }
|
#settingsPanel .xsmall {
|
||||||
#settingsPanel .small { font-size: small; }
|
font-size: x-small;
|
||||||
#settingsPanel .medium { font-size: medium; }
|
}
|
||||||
#settingsPanel .large { font-size: large; }
|
#settingsPanel .small {
|
||||||
#settingsPanel .xlarge { font-size: x-large; }
|
font-size: small;
|
||||||
|
}
|
||||||
|
#settingsPanel .medium {
|
||||||
|
font-size: medium;
|
||||||
|
}
|
||||||
|
#settingsPanel .large {
|
||||||
|
font-size: large;
|
||||||
|
}
|
||||||
|
#settingsPanel .xlarge {
|
||||||
|
font-size: x-large;
|
||||||
|
}
|
||||||
|
|
||||||
.highlight { background-color: yellow; }
|
.highlight {
|
||||||
|
background-color: yellow;
|
||||||
|
}
|
||||||
|
|
||||||
.modal {
|
.modal {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
width: 100vw;
|
width: 97vw;
|
||||||
height: auto;
|
height: 90dvh;
|
||||||
max-width: 630px;
|
max-width: 630px;
|
||||||
z-index: 2000;
|
z-index: 2000;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
@@ -539,7 +565,7 @@ input:-moz-placeholder { color: #454545; }
|
|||||||
}
|
}
|
||||||
|
|
||||||
.md-show ~ .overlay {
|
.md-show ~ .overlay {
|
||||||
opacity: 1;
|
/* opacity: 1; */
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -554,7 +580,7 @@ input:-moz-placeholder { color: #454545; }
|
|||||||
}
|
}
|
||||||
|
|
||||||
.md-content > div {
|
.md-content > div {
|
||||||
padding: 15px 40px 30px;
|
padding: 10px 20px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -581,6 +607,7 @@ input:-moz-placeholder { color: #454545; }
|
|||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.md-content .themes button.lightTheme,
|
||||||
.md-content .themes button.whiteTheme {
|
.md-content .themes button.whiteTheme {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
color: black;
|
color: black;
|
||||||
@@ -591,6 +618,11 @@ input:-moz-placeholder { color: #454545; }
|
|||||||
color: black;
|
color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.md-content .themes button.amberTheme {
|
||||||
|
background-color: #e8bf5e;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
.md-content .themes button.blackTheme {
|
.md-content .themes button.blackTheme {
|
||||||
background-color: black;
|
background-color: black;
|
||||||
color: white;
|
color: white;
|
||||||
@@ -638,31 +670,47 @@ input:-moz-placeholder { color: #454545; }
|
|||||||
|
|
||||||
@media only screen and (max-width: 900px) {
|
@media only screen and (max-width: 900px) {
|
||||||
#viewer {
|
#viewer {
|
||||||
width: 60%;
|
width: 100%;
|
||||||
margin-left: 20%;
|
height: calc(100dvh - 99px);
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#viewer iframe {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#prev {
|
#prev {
|
||||||
left: 20px;
|
left: 20px;
|
||||||
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
#next {
|
#next {
|
||||||
right: 20px;
|
right: 20px;
|
||||||
|
z-index: 10;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 550px) {
|
@media only screen and (max-width: 550px) {
|
||||||
#viewer {
|
#viewer {
|
||||||
width: 80%;
|
width: 100%;
|
||||||
margin-left: 10%;
|
height: calc(100dvh - 99px);
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#viewer iframe {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#prev {
|
#prev {
|
||||||
left: 0;
|
left: 0;
|
||||||
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
#next {
|
#next {
|
||||||
right: 0;
|
right: 0;
|
||||||
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
.arrow {
|
.arrow {
|
||||||
@@ -712,11 +760,7 @@ input:-moz-placeholder { color: #454545; }
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen
|
@media only screen and (min-device-width: 768px) and (max-device-width: 1024px) and (orientation: landscape) /* and (-webkit-min-device-pixel-ratio: 2) */ {
|
||||||
and (min-device-width: 768px)
|
|
||||||
and (max-device-width: 1024px)
|
|
||||||
and (orientation: landscape)
|
|
||||||
/* and (-webkit-min-device-pixel-ratio: 2)*/ {
|
|
||||||
#viewer {
|
#viewer {
|
||||||
width: 80%;
|
width: 80%;
|
||||||
margin-left: 10%;
|
margin-left: 10%;
|
||||||
@@ -739,47 +783,52 @@ input:-moz-placeholder { color: #454545; }
|
|||||||
/* For iPhone 6\6s portrait layouts only */
|
/* For iPhone 6\6s portrait layouts only */
|
||||||
@media only screen and (min-device-width: 375px) and (max-device-width: 667px) and (orientation: portrait) {
|
@media only screen and (min-device-width: 375px) and (max-device-width: 667px) and (orientation: portrait) {
|
||||||
#viewer {
|
#viewer {
|
||||||
width: 300px;
|
width: 100%;
|
||||||
height: 480px;
|
height: calc(100dvh - 99px);
|
||||||
}
|
}
|
||||||
|
|
||||||
#viewer iframe {
|
#viewer iframe {
|
||||||
width: 300px;
|
width: 100%;
|
||||||
height: 480px;
|
height: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* For iPhone 6\6s landscape layouts only */
|
/* For iPhone 6\6s landscape layouts only */
|
||||||
@media only screen and (min-device-width: 375px) and (max-device-width: 667px) and (orientation: landscape) {
|
@media only screen and (min-device-width: 375px) and (max-device-width: 667px) and (orientation: landscape) {
|
||||||
#viewer {
|
#viewer {
|
||||||
width: 450px;
|
width: 100%;
|
||||||
height: 300px;
|
height: calc(100dvh - 99px);
|
||||||
}
|
}
|
||||||
|
|
||||||
#viewer iframe {
|
#viewer iframe {
|
||||||
width: 450px;
|
width: 100%;
|
||||||
height: 300px;
|
height: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* For iPhone portrait layouts only */
|
/* For iPhone portrait layouts only */
|
||||||
@media only screen and (max-device-width: 374px) and (orientation: portrait) {
|
@media only screen and (max-device-width: 374px) and (orientation: portrait) {
|
||||||
#viewer {
|
#viewer {
|
||||||
width: 256px;
|
width: 100%;
|
||||||
height: 432px;
|
height: calc(100dvh - 99px);
|
||||||
}
|
}
|
||||||
|
|
||||||
#viewer iframe {
|
#viewer iframe {
|
||||||
width: 256px;
|
width: 100%;
|
||||||
height: 432px;
|
height: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* For iPhone landscape layouts only */
|
/* For iPhone landscape layouts only */
|
||||||
@media only screen and (max-device-width: 374px) and (orientation: landscape) {
|
@media only screen and (max-device-width: 374px) and (orientation: landscape) {
|
||||||
|
#viewer {
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100dvh - 99px);
|
||||||
|
}
|
||||||
|
|
||||||
#viewer iframe {
|
#viewer iframe {
|
||||||
width: 256px;
|
width: 100%;
|
||||||
height: 124px;
|
height: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -803,32 +852,69 @@ input:-moz-placeholder { color: #454545; }
|
|||||||
font-size: 112%;
|
font-size: 112%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-search::before { content: '\e807'; } /* '' */
|
.icon-search::before {
|
||||||
.icon-resize-full-1::before { content: '\e804'; } /* '' */
|
content: "\e807";
|
||||||
.icon-cancel-circled2::before { content: '\e80f'; } /* '' */
|
} /* '' */
|
||||||
.icon-link::before { content: '\e80d'; } /* '' */
|
.icon-resize-full-1::before {
|
||||||
.icon-bookmark::before { content: '\e805'; } /* '' */
|
content: "\e804";
|
||||||
.icon-bookmark-empty::before { content: '\e806'; } /* '' */
|
} /* '' */
|
||||||
.icon-download-cloud::before { content: '\e811'; } /* '' */
|
.icon-cancel-circled2::before {
|
||||||
.icon-edit::before { content: '\e814'; } /* '' */
|
content: "\e80f";
|
||||||
.icon-menu::before { content: '\e802'; } /* '' */
|
} /* '' */
|
||||||
.icon-cog::before { content: '\e813'; } /* '' */
|
.icon-link::before {
|
||||||
.icon-resize-full::before { content: '\e812'; } /* '' */
|
content: "\e80d";
|
||||||
.icon-cancel-circled::before { content: '\e80e'; } /* '' */
|
} /* '' */
|
||||||
.icon-up-dir::before { content: '\e80c'; } /* '' */
|
.icon-bookmark::before {
|
||||||
.icon-right-dir::before { content: '\e80b'; } /* '' */
|
content: "\e805";
|
||||||
.icon-angle-right::before { content: '\e809'; } /* '' */
|
} /* '' */
|
||||||
.icon-angle-down::before { content: '\e80a'; } /* '' */
|
.icon-bookmark-empty::before {
|
||||||
.icon-right::before { content: '\e815'; } /* '' */
|
content: "\e806";
|
||||||
.icon-list-1::before { content: '\e803'; } /* '' */
|
} /* '' */
|
||||||
.icon-list-numbered::before { content: '\e801'; } /* '' */
|
.icon-download-cloud::before {
|
||||||
.icon-columns::before { content: '\e810'; } /* '' */
|
content: "\e811";
|
||||||
.icon-list::before { content: '\e800'; } /* '' */
|
} /* '' */
|
||||||
.icon-resize-small::before { content: '\e808'; } /* '' */
|
.icon-edit::before {
|
||||||
|
content: "\e814";
|
||||||
#progress{
|
} /* '' */
|
||||||
right: 4rem;
|
.icon-menu::before {
|
||||||
bottom: 4rem;
|
content: "\e802";
|
||||||
width: fit-content;
|
} /* '' */
|
||||||
position: absolute;
|
.icon-cog::before {
|
||||||
}
|
content: "\e813";
|
||||||
|
} /* '' */
|
||||||
|
.icon-resize-full::before {
|
||||||
|
content: "\e812";
|
||||||
|
} /* '' */
|
||||||
|
.icon-cancel-circled::before {
|
||||||
|
content: "\e80e";
|
||||||
|
} /* '' */
|
||||||
|
.icon-up-dir::before {
|
||||||
|
content: "\e80c";
|
||||||
|
} /* '' */
|
||||||
|
.icon-right-dir::before {
|
||||||
|
content: "\e80b";
|
||||||
|
} /* '' */
|
||||||
|
.icon-angle-right::before {
|
||||||
|
content: "\e809";
|
||||||
|
} /* '' */
|
||||||
|
.icon-angle-down::before {
|
||||||
|
content: "\e80a";
|
||||||
|
} /* '' */
|
||||||
|
.icon-right::before {
|
||||||
|
content: "\e815";
|
||||||
|
} /* '' */
|
||||||
|
.icon-list-1::before {
|
||||||
|
content: "\e803";
|
||||||
|
} /* '' */
|
||||||
|
.icon-list-numbered::before {
|
||||||
|
content: "\e801";
|
||||||
|
} /* '' */
|
||||||
|
.icon-columns::before {
|
||||||
|
content: "\e810";
|
||||||
|
} /* '' */
|
||||||
|
.icon-list::before {
|
||||||
|
content: "\e800";
|
||||||
|
} /* '' */
|
||||||
|
.icon-resize-small::before {
|
||||||
|
content: "\e808";
|
||||||
|
} /* '' */
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
box-shadow: 0px 15px 40px #7E6D5766;
|
box-shadow: 0px 15px 40px #7e6d5766;
|
||||||
}
|
}
|
||||||
.slider label {
|
.slider label {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
@@ -29,8 +29,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
font-size: 20px;
|
font-size: 14px;
|
||||||
font-weight: 400;
|
font-weight: 600;
|
||||||
font-family: Open Sans;
|
font-family: Open Sans;
|
||||||
padding-right: 10px;
|
padding-right: 10px;
|
||||||
color: white;
|
color: white;
|
||||||
@@ -45,3 +45,37 @@
|
|||||||
margin-right: 1%;
|
margin-right: 1%;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#main,
|
||||||
|
#viewer,
|
||||||
|
#sidebar,
|
||||||
|
#tocView {
|
||||||
|
touch-action: pan-y;
|
||||||
|
}
|
||||||
|
|
||||||
|
.read-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 12px;
|
||||||
|
}
|
||||||
|
.read-footer #pages-count {
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
left: 12px;
|
||||||
|
bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#back-button {
|
||||||
|
width: 50px !important;
|
||||||
|
right: 50px;
|
||||||
|
// position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
#progress {
|
||||||
|
right: 4rem;
|
||||||
|
bottom: 2px;
|
||||||
|
width: fit-content;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
#add-to-shelves.dropdown-menu, #remove-from-shelves.dropdown-menu {
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.tooltip.bottom .tooltip-inner {
|
.tooltip.bottom .tooltip-inner {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
@@ -429,6 +433,9 @@ div.log {
|
|||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.badge-sm{
|
||||||
|
font-size: 70%;
|
||||||
|
}
|
||||||
#detailcover { cursor:zoom-in; }
|
#detailcover { cursor:zoom-in; }
|
||||||
#detailcover:-webkit-full-screen { cursor:zoom-out; border: 0; }
|
#detailcover:-webkit-full-screen { cursor:zoom-out; border: 0; }
|
||||||
#detailcover:-moz-full-screen { cursor:zoom-out; border: 0; }
|
#detailcover:-moz-full-screen { cursor:zoom-out; border: 0; }
|
||||||
@@ -438,3 +445,43 @@ div.log {
|
|||||||
.error-list {
|
.error-list {
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body > div.container-fluid > div.row-fluid > div.col-sm-10 {
|
||||||
|
max-height: calc(100vh - 111px);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
body > div.container-fluid > div.row-fluid > div.col-sm-2 {
|
||||||
|
max-height: calc(100vh - 111px);
|
||||||
|
overflow-y: auto;
|
||||||
|
/* hide scrollbar in IE, Edge */
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
|
||||||
|
/* hide scrollbar in Firefox */
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* hide scrollbar in WebKit browsers (Chrome, Safari, Opera) */
|
||||||
|
body > div.container-fluid > div.row-fluid > div.col-sm-2::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollable-col {
|
||||||
|
/* keep vertical scrolling */
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
/* hide scrollbar in IE, Edge */
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
|
||||||
|
/* hide scrollbar in Firefox */
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* hide scrollbar in WebKit browsers (Chrome, Safari, Opera) */
|
||||||
|
.scrollable-col::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body > div.container-fluid {
|
||||||
|
padding-right: 0px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -103,11 +103,11 @@ $(function () {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$("#meta-info").html("<p class=\"text-danger\">" + msg.no_result + "!</p>" + $("#meta-info")[0].innerHTML)
|
$("#meta-info").html("<p class=\"text-danger\">" + msg.no_result + "</p>" + $("#meta-info")[0].innerHTML)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: function error() {
|
error: function error() {
|
||||||
$("#meta-info").html("<p class=\"text-danger\">" + msg.search_error + "!</p>" + $("#meta-info")[0].innerHTML);
|
$("#meta-info").html("<p class=\"text-danger\">" + msg.search_error + "</p>" + $("#meta-info")[0].innerHTML);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+2
File diff suppressed because one or more lines are too long
+3
File diff suppressed because one or more lines are too long
@@ -229,10 +229,12 @@ $("#delete_confirm").click(function(event) {
|
|||||||
postButton(event, getPath() + "/delete/" + deleteId + "/" + bookFormat);
|
postButton(event, getPath() + "/delete/" + deleteId + "/" + bookFormat);
|
||||||
} else {
|
} else {
|
||||||
if (ajaxResponse) {
|
if (ajaxResponse) {
|
||||||
path = getPath() + "/ajax/delete/" + deleteId;
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
|
url: getPath() + "/ajax/deletebook",
|
||||||
method: "post",
|
method: "post",
|
||||||
url: path,
|
contentType: "application/json; charset=utf-8",
|
||||||
|
dataType: "json",
|
||||||
|
data: JSON.stringify({"bookid": [deleteId]}),
|
||||||
timeout: 900,
|
timeout: 900,
|
||||||
success:function(data) {
|
success:function(data) {
|
||||||
data.forEach(function(item) {
|
data.forEach(function(item) {
|
||||||
@@ -408,7 +410,7 @@ $(function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// fix for infinite scroll on CaliBlur Theme (#981)
|
// fix for infinite scroll on CaliBlur Theme (#981)
|
||||||
if ($("body").hasClass("blur")) {
|
|
||||||
$(".col-sm-10").bind("scroll", function () {
|
$(".col-sm-10").bind("scroll", function () {
|
||||||
if (
|
if (
|
||||||
$(this).scrollTop() + $(this).innerHeight() >=
|
$(this).scrollTop() + $(this).innerHeight() >=
|
||||||
@@ -418,7 +420,7 @@ $(function() {
|
|||||||
window.history.replaceState({}, null, $loadMore.infiniteScroll("getAbsolutePath"));
|
window.history.replaceState({}, null, $loadMore.infiniteScroll("getAbsolutePath"));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$("#restart").click(function() {
|
$("#restart").click(function() {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ var reader;
|
|||||||
|
|
||||||
reader = ePubReader(calibre.bookUrl, {
|
reader = ePubReader(calibre.bookUrl, {
|
||||||
restore: true,
|
restore: true,
|
||||||
bookmarks: calibre.bookmark ? [calibre.bookmark] : []
|
bookmarks: calibre.bookmark ? [calibre.bookmark] : [],
|
||||||
});
|
});
|
||||||
|
|
||||||
Object.keys(themes).forEach(function (theme) {
|
Object.keys(themes).forEach(function (theme) {
|
||||||
@@ -54,29 +54,92 @@ var reader;
|
|||||||
|
|
||||||
// Update progress percentage
|
// Update progress percentage
|
||||||
let progressDiv = document.getElementById("progress");
|
let progressDiv = document.getElementById("progress");
|
||||||
reader.book.ready.then((()=>{
|
// Pages counter (virtual pages via EPUB locations)
|
||||||
let locations_key = reader.book.key()+'-locations';
|
let pagesDiv = document.getElementById("pages-count");
|
||||||
|
// Honor saved visibility preference for pages counter
|
||||||
|
(function () {
|
||||||
|
try {
|
||||||
|
var pref = localStorage.getItem("calibre.reader.showPages");
|
||||||
|
var show = pref === null ? true : pref === "true";
|
||||||
|
if (pagesDiv)
|
||||||
|
pagesDiv.style.visibility = show ? "visible" : "hidden";
|
||||||
|
} catch (e) {}
|
||||||
|
})();
|
||||||
|
|
||||||
|
reader.book.ready.then(() => {
|
||||||
|
let locations_key = reader.book.key() + "-locations";
|
||||||
|
// Key to persist last-read position for this book in localStorage
|
||||||
|
let position_key = "calibre.reader.position." + reader.book.key();
|
||||||
let stored_locations = localStorage.getItem(locations_key);
|
let stored_locations = localStorage.getItem(locations_key);
|
||||||
let make_locations, save_locations;
|
let make_locations, save_locations;
|
||||||
if (stored_locations) {
|
if (stored_locations) {
|
||||||
make_locations = Promise.resolve(reader.book.locations.load(stored_locations));
|
make_locations = Promise.resolve(
|
||||||
|
reader.book.locations.load(stored_locations)
|
||||||
|
);
|
||||||
// No-op because locations are already saved
|
// No-op because locations are already saved
|
||||||
save_locations = () => {};
|
save_locations = () => {};
|
||||||
} else {
|
} else {
|
||||||
make_locations = reader.book.locations.generate();
|
make_locations = reader.book.locations.generate();
|
||||||
save_locations = () => {
|
save_locations = () => {
|
||||||
localStorage.setItem(locations_key, reader.book.locations.save());
|
localStorage.setItem(
|
||||||
|
locations_key,
|
||||||
|
reader.book.locations.save()
|
||||||
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
make_locations.then(()=>{
|
make_locations
|
||||||
reader.rendition.on('relocated', (location)=>{
|
.then(() => {
|
||||||
|
// Try to restore last position (CFI) from localStorage if present
|
||||||
|
try {
|
||||||
|
var _savedPos = localStorage.getItem(position_key);
|
||||||
|
if (_savedPos) {
|
||||||
|
try {
|
||||||
|
var _posObj = JSON.parse(_savedPos);
|
||||||
|
if (_posObj && _posObj.cfi) {
|
||||||
|
// Display the saved CFI location
|
||||||
|
try {
|
||||||
|
reader.rendition.display(_posObj.cfi);
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
reader.rendition.on("relocated", (location) => {
|
||||||
let percentage = Math.round(location.end.percentage * 100);
|
let percentage = Math.round(location.end.percentage * 100);
|
||||||
progressDiv.textContent = percentage + "%";
|
progressDiv.textContent = percentage + "%";
|
||||||
|
|
||||||
|
// Pages based on generated EPUB locations (CFI positions)
|
||||||
|
const cfi = location.start.cfi;
|
||||||
|
const current =
|
||||||
|
reader.book.locations.locationFromCfi(cfi) || 0; // 1-based index typically
|
||||||
|
const total = reader.book.locations.length() || 0;
|
||||||
|
|
||||||
|
if (total > 0) {
|
||||||
|
pagesDiv.textContent = current + "/" + total;
|
||||||
|
pagesDiv.style.visibility = "visible";
|
||||||
|
} else {
|
||||||
|
pagesDiv.textContent = "";
|
||||||
|
pagesDiv.style.visibility = "hidden";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Persist last position (CFI + percentage) to localStorage so reader can restore on next open
|
||||||
|
try {
|
||||||
|
var posObj = {
|
||||||
|
cfi: location.start.cfi,
|
||||||
|
percentage: location.start.percentage,
|
||||||
|
};
|
||||||
|
localStorage.setItem(
|
||||||
|
position_key,
|
||||||
|
JSON.stringify(posObj)
|
||||||
|
);
|
||||||
|
} catch (e) {}
|
||||||
});
|
});
|
||||||
reader.rendition.reportLocation();
|
reader.rendition.reportLocation();
|
||||||
progressDiv.style.visibility = "visible";
|
progressDiv.style.visibility = "visible";
|
||||||
}).then(save_locations);
|
})
|
||||||
}));
|
.then(save_locations);
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} action - Add or remove bookmark
|
* @param {string} action - Add or remove bookmark
|
||||||
@@ -85,11 +148,15 @@ var reader;
|
|||||||
function updateBookmark(action, location) {
|
function updateBookmark(action, location) {
|
||||||
// Remove other bookmarks (there can only be one)
|
// Remove other bookmarks (there can only be one)
|
||||||
if (action === "add") {
|
if (action === "add") {
|
||||||
this.settings.bookmarks.filter(function (bookmark) {
|
this.settings.bookmarks
|
||||||
|
.filter(function (bookmark) {
|
||||||
return bookmark && bookmark !== location;
|
return bookmark && bookmark !== location;
|
||||||
}).map(function (bookmark) {
|
})
|
||||||
|
.map(
|
||||||
|
function (bookmark) {
|
||||||
this.removeBookmark(bookmark);
|
this.removeBookmark(bookmark);
|
||||||
}.bind(this));
|
}.bind(this)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
var csrftoken = $("input[name='csrf_token']").val();
|
var csrftoken = $("input[name='csrf_token']").val();
|
||||||
@@ -98,7 +165,7 @@ var reader;
|
|||||||
$.ajax(calibre.bookmarkUrl, {
|
$.ajax(calibre.bookmarkUrl, {
|
||||||
method: "post",
|
method: "post",
|
||||||
data: { bookmark: location || "" },
|
data: { bookmark: location || "" },
|
||||||
headers: { "X-CSRFToken": csrftoken }
|
headers: { "X-CSRFToken": csrftoken },
|
||||||
}).fail(function (xhr, status, error) {
|
}).fail(function (xhr, status, error) {
|
||||||
alert(error);
|
alert(error);
|
||||||
});
|
});
|
||||||
@@ -107,4 +174,17 @@ var reader;
|
|||||||
// Default settings load
|
// Default settings load
|
||||||
const theme = localStorage.getItem("calibre.reader.theme") ?? "lightTheme";
|
const theme = localStorage.getItem("calibre.reader.theme") ?? "lightTheme";
|
||||||
selectTheme(theme);
|
selectTheme(theme);
|
||||||
|
|
||||||
|
// Restore saved font and font size after reader is ready
|
||||||
|
reader.book.ready.then(() => {
|
||||||
|
const savedFontSize = localStorage.getItem("calibre.reader.fontSize");
|
||||||
|
if (savedFontSize) {
|
||||||
|
reader.rendition.themes.fontSize(`${savedFontSize}%`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const savedFont = localStorage.getItem("calibre.reader.font");
|
||||||
|
if (savedFont && window.selectFont) {
|
||||||
|
window.selectFont(savedFont);
|
||||||
|
}
|
||||||
|
});
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
// register new event emitter locationchange that fires on urlchange
|
|
||||||
// source: https://stackoverflow.com/a/52809105/21941129
|
|
||||||
(() => {
|
|
||||||
let oldPushState = history.pushState;
|
|
||||||
history.pushState = function pushState() {
|
|
||||||
let ret = oldPushState.apply(this, arguments);
|
|
||||||
window.dispatchEvent(new Event('locationchange'));
|
|
||||||
return ret;
|
|
||||||
};
|
|
||||||
|
|
||||||
let oldReplaceState = history.replaceState;
|
|
||||||
history.replaceState = function replaceState() {
|
|
||||||
let ret = oldReplaceState.apply(this, arguments);
|
|
||||||
window.dispatchEvent(new Event('locationchange'));
|
|
||||||
return ret;
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('popstate', () => {
|
|
||||||
window.dispatchEvent(new Event('locationchange'));
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
+439
-37
@@ -81,20 +81,78 @@ $(function() {
|
|||||||
$("#merge_books").addClass("disabled");
|
$("#merge_books").addClass("disabled");
|
||||||
$("#merge_books").attr("aria-disabled", true);
|
$("#merge_books").attr("aria-disabled", true);
|
||||||
}
|
}
|
||||||
|
if (selections.length >= 1) {
|
||||||
|
$("#delete_selected_books").removeClass("disabled");
|
||||||
|
$("#delete_selected_books").attr("aria-disabled", false);
|
||||||
|
|
||||||
|
$("#archive_selected_books").removeClass("disabled");
|
||||||
|
$("#archive_selected_books").attr("aria-disabled", false);
|
||||||
|
|
||||||
|
$("#unarchive_selected_books").removeClass("disabled");
|
||||||
|
$("#unarchive_selected_books").attr("aria-disabled", false);
|
||||||
|
|
||||||
|
$("#read_selected_books").removeClass("disabled");
|
||||||
|
$("#read_selected_books").attr("aria-disabled", false);
|
||||||
|
|
||||||
|
$("#unread_selected_books").removeClass("disabled");
|
||||||
|
$("#unread_selected_books").attr("aria-disabled", false);
|
||||||
|
|
||||||
|
$("#edit_selected_books").removeClass("disabled");
|
||||||
|
$("#edit_selected_books").attr("aria-disabled", false);
|
||||||
|
} else {
|
||||||
|
$("#delete_selected_books").addClass("disabled");
|
||||||
|
$("#delete_selected_books").attr("aria-disabled", true);
|
||||||
|
|
||||||
|
$("#archive_selected_books").addClass("disabled");
|
||||||
|
$("#archive_selected_books").attr("aria-disabled", true);
|
||||||
|
|
||||||
|
$("#unarchive_selected_books").addClass("disabled");
|
||||||
|
$("#unarchive_selected_books").attr("aria-disabled", true);
|
||||||
|
|
||||||
|
$("#read_selected_books").addClass("disabled");
|
||||||
|
$("#read_selected_books").attr("aria-disabled", true);
|
||||||
|
|
||||||
|
$("#unread_selected_books").addClass("disabled");
|
||||||
|
$("#unread_selected_books").attr("aria-disabled", true);
|
||||||
|
|
||||||
|
$("#edit_selected_books").addClass("disabled");
|
||||||
|
$("#edit_selected_books").attr("aria-disabled", true);
|
||||||
|
}
|
||||||
if (selections.length < 1) {
|
if (selections.length < 1) {
|
||||||
$("#delete_selection").addClass("disabled");
|
// $("#book_delete_selection").addClass("disabled");
|
||||||
$("#delete_selection").attr("aria-disabled", true);
|
// $("#book_delete_selection").attr("aria-disabled", true);
|
||||||
$("#table_xchange").addClass("disabled");
|
$("#table_xchange").addClass("disabled");
|
||||||
$("#table_xchange").attr("aria-disabled", true);
|
$("#table_xchange").attr("aria-disabled", true);
|
||||||
} else {
|
} else {
|
||||||
$("#delete_selection").removeClass("disabled");
|
// $("#book_delete_selection").removeClass("disabled");
|
||||||
$("#delete_selection").attr("aria-disabled", false);
|
// $("#book_delete_selection").attr("aria-disabled", false);
|
||||||
$("#table_xchange").removeClass("disabled");
|
$("#table_xchange").removeClass("disabled");
|
||||||
$("#table_xchange").attr("aria-disabled", false);
|
$("#table_xchange").attr("aria-disabled", false);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
handle_header_buttons();
|
||||||
});
|
});
|
||||||
$("#delete_selection").click(function() {
|
|
||||||
|
// Small block to initialize the state of the author/title sort inputs in metadata form
|
||||||
|
{
|
||||||
|
let checkA = $('#autoupdate_authorsort').prop('checked');
|
||||||
|
$('#author_sort_input').prop('disabled', checkA);
|
||||||
|
let checkT = $('#autoupdate_titlesort').prop('checked');
|
||||||
|
$('#title_sort_input').prop('disabled', checkT);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable/enable author and title sort input in respect to auto-update title/author sort being checked on or not
|
||||||
|
$("#autoupdate_authorsort").on('change', function(event) {
|
||||||
|
let checkA = $('#autoupdate_authorsort').prop('checked');
|
||||||
|
$('#author_sort_input').prop('disabled', checkA);
|
||||||
|
})
|
||||||
|
|
||||||
|
$("#autoupdate_titlesort").on('change', function(event) {
|
||||||
|
let checkT = $('#autoupdate_titlesort').prop('checked');
|
||||||
|
$('#title_sort_input').prop('disabled', checkT);
|
||||||
|
})
|
||||||
|
/////
|
||||||
|
|
||||||
|
$("#book_delete_selection").click(function () {
|
||||||
$("#books-table").bootstrapTable("uncheckAll");
|
$("#books-table").bootstrapTable("uncheckAll");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -135,6 +193,250 @@ $(function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("#edit_selected_books").click(function(event) {
|
||||||
|
if ($(this).hasClass("disabled")) {
|
||||||
|
event.stopPropagation()
|
||||||
|
} else {
|
||||||
|
$('#edit_selected_modal').modal("show");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#edit_selected_confirm").click(function(event) {
|
||||||
|
$.ajax({
|
||||||
|
method:"post",
|
||||||
|
contentType: "application/json; charset=utf-8",
|
||||||
|
dataType: "json",
|
||||||
|
url: getPath() + "/ajax/editselectedbooks",
|
||||||
|
data: JSON.stringify({
|
||||||
|
"selections": selections,
|
||||||
|
"title": $("#title_input").val(),
|
||||||
|
"title_sort": $("#title_sort_input").val(),
|
||||||
|
"author_sort": $("#author_sort_input").val(),
|
||||||
|
"authors": $("#authors_input").val(),
|
||||||
|
"categories": $("#categories_input").val(),
|
||||||
|
"series": $("#series_input").val(),
|
||||||
|
"languages": $("#languages_input").val(),
|
||||||
|
"publishers": $("#publishers_input").val(),
|
||||||
|
"comments": $("#comments_input").val().toString(),
|
||||||
|
"checkA": $('#autoupdate_authorsort').prop('checked'),
|
||||||
|
"checkT": $('#autoupdate_titlesort').prop('checked')
|
||||||
|
}),
|
||||||
|
success: function success(data) {
|
||||||
|
let result = "";
|
||||||
|
$("#books-table").bootstrapTable("refresh");
|
||||||
|
$("#books-table").bootstrapTable("uncheckAll");
|
||||||
|
|
||||||
|
$("#title_input").val("");
|
||||||
|
$("#title_sort_input").val("");
|
||||||
|
$("#author_sort_input").val("");
|
||||||
|
$("#authors_input").val("");
|
||||||
|
$("#categories_input").val("");
|
||||||
|
$("#series_input").val("");
|
||||||
|
$("#languages_input").val("");
|
||||||
|
$("#publishers_input").val("");
|
||||||
|
$("#comments_input").val("");
|
||||||
|
|
||||||
|
$("#flash_success").remove();
|
||||||
|
$("#flash_danger").remove();
|
||||||
|
|
||||||
|
if (!jQuery.isEmptyObject(data)) {
|
||||||
|
data.forEach(function(item) {
|
||||||
|
if (item.success === true) {
|
||||||
|
result = "success";
|
||||||
|
} else {
|
||||||
|
result = "danger";
|
||||||
|
}
|
||||||
|
$(".navbar").after('<div class="row-fluid text-center">' +
|
||||||
|
'<div id="flash_' + result + '" class="alert alert-' + result + '">' + item.msg + '</div>' +
|
||||||
|
'</div>');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
$(".table.table-striped").bootstrapTable("refresh");
|
||||||
|
// handleListServerResponse(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('click', '#archive_selected_books', function(event) {
|
||||||
|
if ($(this).hasClass("disabled")) {
|
||||||
|
event.stopPropagation()
|
||||||
|
} else {
|
||||||
|
$('#archive_selected_modal').modal("show");
|
||||||
|
}
|
||||||
|
$.ajax({
|
||||||
|
method:"post",
|
||||||
|
contentType: "application/json; charset=utf-8",
|
||||||
|
dataType: "json",
|
||||||
|
url: getPath() + "/ajax/displayselectedbooks",
|
||||||
|
data: JSON.stringify({"selections":selections}),
|
||||||
|
success: function success(booTitles) {
|
||||||
|
$('#display-archive-selected-books').empty();
|
||||||
|
$.each(booTitles.books, function(i, item) {
|
||||||
|
$("<span>- " + item + "</span><p></p>").appendTo("#display-archive-selected-books");
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/*$(document).on('click', '#archive_selected_confirm', function(event) {
|
||||||
|
$.ajax({
|
||||||
|
method:"post",
|
||||||
|
contentType: "application/json; charset=utf-8",
|
||||||
|
dataType: "json",
|
||||||
|
url: getPath() + "/ajax/archiveselectedbooks",
|
||||||
|
data: JSON.stringify({"selections":selections, "archive": true}),
|
||||||
|
success: function success(booTitles) {
|
||||||
|
$("#books-table").bootstrapTable("refresh");
|
||||||
|
$("#books-table").bootstrapTable("uncheckAll");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('click', '#unarchive_selected_books', function(event) {
|
||||||
|
if ($(this).hasClass("disabled")) {
|
||||||
|
event.stopPropagation()
|
||||||
|
} else {
|
||||||
|
$('#unarchive_selected_modal').modal("show");
|
||||||
|
}
|
||||||
|
$.ajax({
|
||||||
|
method:"post",
|
||||||
|
contentType: "application/json; charset=utf-8",
|
||||||
|
dataType: "json",
|
||||||
|
url: getPath() + "/ajax/displayselectedbooks",
|
||||||
|
data: JSON.stringify({"selections":selections}),
|
||||||
|
success: function success(booTitles) {
|
||||||
|
$('#display-unarchive-selected-books').empty();
|
||||||
|
$.each(booTitles.books, function(i, item) {
|
||||||
|
$("<span>- " + item + "</span><p></p>").appendTo("#display-unarchive-selected-books");
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('click', '#unarchive_selected_confirm', function(event) {
|
||||||
|
$.ajax({
|
||||||
|
method:"post",
|
||||||
|
contentType: "application/json; charset=utf-8",
|
||||||
|
dataType: "json",
|
||||||
|
url: getPath() + "/ajax/archiveselectedbooks",
|
||||||
|
data: JSON.stringify({"selections":selections, "archive": false}),
|
||||||
|
success: function success(booTitles) {
|
||||||
|
$("#books-table").bootstrapTable("refresh");
|
||||||
|
$("#books-table").bootstrapTable("uncheckAll");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('click', '#delete_selected_books', function(event) {
|
||||||
|
if ($(this).hasClass("disabled")) {
|
||||||
|
event.stopPropagation()
|
||||||
|
} else {
|
||||||
|
$('#delete_selected_modal').modal("show");
|
||||||
|
}
|
||||||
|
$.ajax({
|
||||||
|
method:"post",
|
||||||
|
contentType: "application/json; charset=utf-8",
|
||||||
|
dataType: "json",
|
||||||
|
url: getPath() + "/ajax/displayselectedbooks",
|
||||||
|
data: JSON.stringify({"selections":selections}),
|
||||||
|
success: function success(booTitles) {
|
||||||
|
$('#display-delete-selected-books').empty();
|
||||||
|
$.each(booTitles.books, function(i, item) {
|
||||||
|
$("<span>- " + item + "</span><p></p>").appendTo("#display-delete-selected-books");
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('click', '#delete_selected_confirm', function(event) {
|
||||||
|
$.ajax({
|
||||||
|
method:"post",
|
||||||
|
contentType: "application/json; charset=utf-8",
|
||||||
|
dataType: "json",
|
||||||
|
url: getPath() + "/ajax/deleteselectedbooks",
|
||||||
|
data: JSON.stringify({"selections":selections}),
|
||||||
|
success: function success(booTitles) {
|
||||||
|
$("#books-table").bootstrapTable("refresh");
|
||||||
|
$("#books-table").bootstrapTable("uncheckAll");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('click', '#read_selected_books', function(event) {
|
||||||
|
if ($(this).hasClass("disabled")) {
|
||||||
|
event.stopPropagation()
|
||||||
|
} else {
|
||||||
|
$('#read_selected_modal').modal("show");
|
||||||
|
}
|
||||||
|
$.ajax({
|
||||||
|
method:"post",
|
||||||
|
contentType: "application/json; charset=utf-8",
|
||||||
|
dataType: "json",
|
||||||
|
url: getPath() + "/ajax/displayselectedbooks",
|
||||||
|
data: JSON.stringify({"selections":selections}),
|
||||||
|
success: function success(booTitles) {
|
||||||
|
$('#display-read-selected-books').empty();
|
||||||
|
$.each(booTitles.books, function(i, item) {
|
||||||
|
$("<span>- " + item + "</span><p></p>").appendTo("#display-read-selected-books");
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('click', '#read_selected_confirm', function(event) {
|
||||||
|
$.ajax({
|
||||||
|
method:"post",
|
||||||
|
contentType: "application/json; charset=utf-8",
|
||||||
|
dataType: "json",
|
||||||
|
url: getPath() + "/ajax/readselectedbooks",
|
||||||
|
data: JSON.stringify({"selections":selections, "markAsRead": true}),
|
||||||
|
success: function success(booTitles) {
|
||||||
|
$("#books-table").bootstrapTable("refresh");
|
||||||
|
$("#books-table").bootstrapTable("uncheckAll");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('click', '#unread_selected_books', function(event) {
|
||||||
|
if ($(this).hasClass("disabled")) {
|
||||||
|
event.stopPropagation()
|
||||||
|
} else {
|
||||||
|
$('#unread_selected_modal').modal("show");
|
||||||
|
}
|
||||||
|
$.ajax({
|
||||||
|
method:"post",
|
||||||
|
contentType: "application/json; charset=utf-8",
|
||||||
|
dataType: "json",
|
||||||
|
url: getPath() + "/ajax/displayselectedbooks",
|
||||||
|
data: JSON.stringify({"selections":selections}),
|
||||||
|
success: function success(booTitles) {
|
||||||
|
$('#display-unread-selected-books').empty();
|
||||||
|
$.each(booTitles.books, function(i, item) {
|
||||||
|
$("<span>- " + item + "</span><p></p>").appendTo("#display-unread-selected-books");
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('click', '#unread_selected_confirm', function(event) {
|
||||||
|
$.ajax({
|
||||||
|
method:"post",
|
||||||
|
contentType: "application/json; charset=utf-8",
|
||||||
|
dataType: "json",
|
||||||
|
url: getPath() + "/ajax/readselectedbooks",
|
||||||
|
data: JSON.stringify({"selections":selections, "markAsRead": false}),
|
||||||
|
success: function success(booTitles) {
|
||||||
|
$("#books-table").bootstrapTable("refresh");
|
||||||
|
$("#books-table").bootstrapTable("uncheckAll");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});*/
|
||||||
|
|
||||||
$("#table_xchange").click(function() {
|
$("#table_xchange").click(function() {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
method:"post",
|
method:"post",
|
||||||
@@ -157,6 +459,10 @@ $(function() {
|
|||||||
editable: {
|
editable: {
|
||||||
mode: "inline",
|
mode: "inline",
|
||||||
emptytext: "<span class='glyphicon glyphicon-plus'></span>",
|
emptytext: "<span class='glyphicon glyphicon-plus'></span>",
|
||||||
|
ajaxOptions: {
|
||||||
|
contentType: "application/json; charset=utf-8",
|
||||||
|
dataType: "json",
|
||||||
|
},
|
||||||
success: function (response, __) {
|
success: function (response, __) {
|
||||||
if (!response.success) return response.msg;
|
if (!response.success) return response.msg;
|
||||||
return {newValue: response.newValue};
|
return {newValue: response.newValue};
|
||||||
@@ -164,7 +470,8 @@ $(function() {
|
|||||||
params: function (params) {
|
params: function (params) {
|
||||||
params.checkA = $('#autoupdate_authorsort').prop('checked');
|
params.checkA = $('#autoupdate_authorsort').prop('checked');
|
||||||
params.checkT = $('#autoupdate_titlesort').prop('checked');
|
params.checkT = $('#autoupdate_titlesort').prop('checked');
|
||||||
return params
|
params.pk = [params.pk];
|
||||||
|
return JSON.stringify(params);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -198,7 +505,7 @@ $(function() {
|
|||||||
searchAlign: "left",
|
searchAlign: "left",
|
||||||
showSearchButton : true,
|
showSearchButton : true,
|
||||||
searchOnEnterKey: true,
|
searchOnEnterKey: true,
|
||||||
checkboxHeader: false,
|
checkboxHeader: true,
|
||||||
maintainMetaData: true,
|
maintainMetaData: true,
|
||||||
responseHandler: responseHandler,
|
responseHandler: responseHandler,
|
||||||
columns: column,
|
columns: column,
|
||||||
@@ -212,7 +519,7 @@ $(function() {
|
|||||||
$.ajax({
|
$.ajax({
|
||||||
method:"get",
|
method:"get",
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
url: window.location.pathname + "/../ajax/sort_value/" + field + "/" + row.id,
|
url: getPath() + "/ajax/sort_value/" + field + "/" + row.id,
|
||||||
success: function success(data) {
|
success: function success(data) {
|
||||||
var key = Object.keys(data)[0];
|
var key = Object.keys(data)[0];
|
||||||
$("#books-table").bootstrapTable("updateCellByUniqueId", {
|
$("#books-table").bootstrapTable("updateCellByUniqueId", {
|
||||||
@@ -224,6 +531,66 @@ $(function() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onPostBody () {
|
||||||
|
// Remove all checkboxes from Headers for showing the texts in the column selector
|
||||||
|
$('.columns [data-field]').each(function(){
|
||||||
|
var elText = $(this).next().text();
|
||||||
|
$(this).next().empty();
|
||||||
|
var index = elText.lastIndexOf('\n', elText.length - 2);
|
||||||
|
if ( index > -1) {
|
||||||
|
elText = elText.substr(index);
|
||||||
|
}
|
||||||
|
$(this).next().text(elText);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onPostHeader() {
|
||||||
|
$(".form-check").each(function () {
|
||||||
|
var item = $(this).parent();
|
||||||
|
var parent = item.parent().parent();
|
||||||
|
if (parent.prop('nodeName') === "TH") {
|
||||||
|
item.prependTo(parent);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if ($(".button_head").length) {
|
||||||
|
if (!$._data($(".button_head").get(0), "events")) {
|
||||||
|
$(".button_head").on("click", function () {
|
||||||
|
var result = $('#books-table').bootstrapTable('getSelections').map(a => a.id);
|
||||||
|
confirmDialog(
|
||||||
|
"btndelbook",
|
||||||
|
"GeneralDeleteModal",
|
||||||
|
0,
|
||||||
|
function () {
|
||||||
|
$.ajax({
|
||||||
|
method: "post",
|
||||||
|
url: getPath() + "/ajax/deletebook",
|
||||||
|
contentType: "application/json; charset=utf-8",
|
||||||
|
dataType: "json",
|
||||||
|
data: JSON.stringify({"bookid": result}),
|
||||||
|
success: function (data) {
|
||||||
|
selections = selections.filter((el) => !result.includes(el));
|
||||||
|
handleListServerResponse(data);
|
||||||
|
},
|
||||||
|
error: function (data) {
|
||||||
|
handleListServerResponse([{type: "danger", message: data.responseText}])
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($(".check_head").length) {
|
||||||
|
if (!$._data($(".check_head").get(0), "events")) {
|
||||||
|
$(".check_head").on("change", function () {
|
||||||
|
var val = $(this).data("set");
|
||||||
|
var name = $(this).data("name");
|
||||||
|
var data = $(this).data("val");
|
||||||
|
bookCheckboxHeader(val, name, data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
onColumnSwitch: function (field, checked) {
|
onColumnSwitch: function (field, checked) {
|
||||||
var visible = $("#books-table").bootstrapTable("getVisibleColumns");
|
var visible = $("#books-table").bootstrapTable("getVisibleColumns");
|
||||||
@@ -240,10 +607,16 @@ $(function() {
|
|||||||
method:"post",
|
method:"post",
|
||||||
contentType: "application/json; charset=utf-8",
|
contentType: "application/json; charset=utf-8",
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
url: window.location.pathname + "/../ajax/table_settings",
|
url: getPath() + "/ajax/table_settings",
|
||||||
data: "{" + st + "}",
|
data: "{" + st + "}",
|
||||||
});
|
});
|
||||||
|
handle_header_buttons();
|
||||||
},
|
},
|
||||||
|
onLoadSuccess: function() {
|
||||||
|
$("input:radio.check_head:checked").each(function () {
|
||||||
|
$(this).prop('checked', false);
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#domain_allow_submit").click(function(event) {
|
$("#domain_allow_submit").click(function(event) {
|
||||||
@@ -252,7 +625,7 @@ $(function() {
|
|||||||
$(this).closest("form").submit();
|
$(this).closest("form").submit();
|
||||||
$.ajax ({
|
$.ajax ({
|
||||||
method:"get",
|
method:"get",
|
||||||
url: window.location.pathname + "/../../ajax/domainlist/1",
|
url: getPath() + "/ajax/domainlist/1",
|
||||||
async: true,
|
async: true,
|
||||||
timeout: 900,
|
timeout: 900,
|
||||||
success:function(data) {
|
success:function(data) {
|
||||||
@@ -273,7 +646,7 @@ $(function() {
|
|||||||
$(this).closest("form").submit();
|
$(this).closest("form").submit();
|
||||||
$.ajax ({
|
$.ajax ({
|
||||||
method:"get",
|
method:"get",
|
||||||
url: window.location.pathname + "/../../ajax/domainlist/0",
|
url: getPath() + "/ajax/domainlist/0",
|
||||||
async: true,
|
async: true,
|
||||||
timeout: 900,
|
timeout: 900,
|
||||||
success:function(data) {
|
success:function(data) {
|
||||||
@@ -291,12 +664,12 @@ $(function() {
|
|||||||
function domainHandle(domainId) {
|
function domainHandle(domainId) {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
method:"post",
|
method:"post",
|
||||||
url: window.location.pathname + "/../../ajax/deletedomain",
|
url: getPath() + "/ajax/deletedomain",
|
||||||
data: {"domainid":domainId}
|
data: {"domainid":domainId}
|
||||||
});
|
});
|
||||||
$.ajax({
|
$.ajax({
|
||||||
method:"get",
|
method:"get",
|
||||||
url: window.location.pathname + "/../../ajax/domainlist/1",
|
url: getPath() + "/ajax/domainlist/1",
|
||||||
async: true,
|
async: true,
|
||||||
timeout: 900,
|
timeout: 900,
|
||||||
success:function(data) {
|
success:function(data) {
|
||||||
@@ -305,7 +678,7 @@ $(function() {
|
|||||||
});
|
});
|
||||||
$.ajax({
|
$.ajax({
|
||||||
method:"get",
|
method:"get",
|
||||||
url: window.location.pathname + "/../../ajax/domainlist/0",
|
url: getPath() + "/ajax/domainlist/0",
|
||||||
async: true,
|
async: true,
|
||||||
timeout: 900,
|
timeout: 900,
|
||||||
success:function(data) {
|
success:function(data) {
|
||||||
@@ -541,7 +914,7 @@ $(function() {
|
|||||||
method:"post",
|
method:"post",
|
||||||
contentType: "application/json; charset=utf-8",
|
contentType: "application/json; charset=utf-8",
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
url: window.location.pathname + "/../../ajax/user_table_settings",
|
url: getPath() + "/ajax/user_table_settings",
|
||||||
data: "{" + st + "}",
|
data: "{" + st + "}",
|
||||||
});
|
});
|
||||||
handle_header_buttons();
|
handle_header_buttons();
|
||||||
@@ -567,8 +940,8 @@ $(function() {
|
|||||||
|
|
||||||
function handle_header_buttons () {
|
function handle_header_buttons () {
|
||||||
if (selections.length < 1) {
|
if (selections.length < 1) {
|
||||||
$("#user_delete_selection").addClass("disabled");
|
$(".mass_selection").addClass("disabled");
|
||||||
$("#user_delete_selection").attr("aria-disabled", true);
|
$(".mass_selection").attr("aria-disabled", true);
|
||||||
$(".check_head").attr("aria-disabled", true);
|
$(".check_head").attr("aria-disabled", true);
|
||||||
$(".check_head").attr("disabled", true);
|
$(".check_head").attr("disabled", true);
|
||||||
$(".check_head").prop('checked', false);
|
$(".check_head").prop('checked', false);
|
||||||
@@ -580,8 +953,8 @@ function handle_header_buttons () {
|
|||||||
$(".multi_selector").attr("disabled", true);
|
$(".multi_selector").attr("disabled", true);
|
||||||
$(".header_select").attr("disabled", true);
|
$(".header_select").attr("disabled", true);
|
||||||
} else {
|
} else {
|
||||||
$("#user_delete_selection").removeClass("disabled");
|
$(".mass_selection").removeClass("disabled");
|
||||||
$("#user_delete_selection").attr("aria-disabled", false);
|
$(".mass_selection").attr("aria-disabled", false);
|
||||||
$(".check_head").attr("aria-disabled", false);
|
$(".check_head").attr("aria-disabled", false);
|
||||||
$(".check_head").removeAttr("disabled");
|
$(".check_head").removeAttr("disabled");
|
||||||
$(".button_head").attr("aria-disabled", false);
|
$(".button_head").attr("aria-disabled", false);
|
||||||
@@ -590,8 +963,10 @@ function handle_header_buttons () {
|
|||||||
$(".multi_head").removeClass("hidden");
|
$(".multi_head").removeClass("hidden");
|
||||||
$(".multi_selector").attr("aria-disabled", false);
|
$(".multi_selector").attr("aria-disabled", false);
|
||||||
$(".multi_selector").removeAttr("disabled");
|
$(".multi_selector").removeAttr("disabled");
|
||||||
$('.multi_selector').selectpicker('refresh');
|
|
||||||
$(".header_select").removeAttr("disabled");
|
$(".header_select").removeAttr("disabled");
|
||||||
|
if (typeof $.fn.selectpicker === "function") {
|
||||||
|
$('.multi_selector').selectpicker('refresh');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -760,7 +1135,7 @@ function move_header_elements() {
|
|||||||
function () {
|
function () {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
method: "post",
|
method: "post",
|
||||||
url: window.location.pathname + "/../../ajax/editlistusers/" + field,
|
url: getPath() + "/ajax/editlistusers/" + field,
|
||||||
data: {"pk": result, "value": values, "action": val},
|
data: {"pk": result, "value": values, "action": val},
|
||||||
success: function (data) {
|
success: function (data) {
|
||||||
handleListServerResponse(data);
|
handleListServerResponse(data);
|
||||||
@@ -774,7 +1149,6 @@ function move_header_elements() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$("#user_delete_selection").click(function () {
|
$("#user_delete_selection").click(function () {
|
||||||
$("#user-table").bootstrapTable("uncheckAll");
|
$("#user-table").bootstrapTable("uncheckAll");
|
||||||
});
|
});
|
||||||
@@ -805,8 +1179,10 @@ function move_header_elements() {
|
|||||||
function () {
|
function () {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
method: "post",
|
method: "post",
|
||||||
url: window.location.pathname + "/../../ajax/deleteuser",
|
url: getPath() + "/ajax/deleteuser",
|
||||||
data: {"userid": result},
|
contentType: "application/json; charset=utf-8",
|
||||||
|
dataType: "json",
|
||||||
|
data: JSON.stringify({"userid": result}),
|
||||||
success: function (data) {
|
success: function (data) {
|
||||||
selections = selections.filter((el) => !result.includes(el));
|
selections = selections.filter((el) => !result.includes(el));
|
||||||
handleListServerResponse(data);
|
handleListServerResponse(data);
|
||||||
@@ -832,7 +1208,7 @@ function handleListServerResponse (data) {
|
|||||||
'</div>');
|
'</div>');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
$("#user-table").bootstrapTable("refresh");
|
$(".table.table-striped").bootstrapTable("refresh");
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkboxChange(checkbox, userId, field, field_index) {
|
function checkboxChange(checkbox, userId, field, field_index) {
|
||||||
@@ -847,30 +1223,30 @@ function checkboxChange(checkbox, userId, field, field_index) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function BookCheckboxChange(checkbox, userId, field) {
|
function BookCheckboxChange(checkbox, bookId, field) {
|
||||||
var value = checkbox.checked ? "True" : "False";
|
var value = checkbox.checked ? "True" : "False";
|
||||||
var element = checkbox;
|
var element = checkbox;
|
||||||
$.ajax({
|
$.ajax({
|
||||||
method: "post",
|
method: "post",
|
||||||
url: getPath() + "/ajax/editbooks/" + field,
|
url: getPath() + "/ajax/editbooks/" + field,
|
||||||
data: {"pk": userId, "value": value},
|
data: JSON.stringify({"pk": [bookId], "value": value}),
|
||||||
|
contentType: "application/json; charset=utf-8",
|
||||||
|
dataType: "json",
|
||||||
error: function(data) {
|
error: function(data) {
|
||||||
element.checked = !element.checked;
|
element.checked = !element.checked;
|
||||||
handleListServerResponse([{type:"danger", message:data.responseText}])
|
handleListServerResponse([{type:"danger", message:data.responseText}])
|
||||||
},
|
},
|
||||||
success: handleListServerResponse
|
success: handleListServerResponse
|
||||||
});
|
});
|
||||||
console.log("test");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function selectHeader(element, field) {
|
function selectHeader(element, field) {
|
||||||
if (element.value !== "None") {
|
if (element.value !== "None") {
|
||||||
confirmDialog(element.id, "GeneralChangeModal", 0, function () {
|
confirmDialog(element.id, "GeneralChangeModal", 0, function () {
|
||||||
var result = $('#user-table').bootstrapTable('getSelections').map(a => a.id);
|
var result = $('#user-table').bootstrapTable('getSelections').map(a => a.id);
|
||||||
$.ajax({
|
$.ajax({
|
||||||
method: "post",
|
method: "post",
|
||||||
url: window.location.pathname + "/../../ajax/editlistusers/" + field,
|
url: getPath() + "/ajax/editlistusers/" + field,
|
||||||
data: {"pk": result, "value": element.value},
|
data: {"pk": result, "value": element.value},
|
||||||
error: function (data) {
|
error: function (data) {
|
||||||
handleListServerResponse([{type:"danger", message:data.responseText}])
|
handleListServerResponse([{type:"danger", message:data.responseText}])
|
||||||
@@ -883,12 +1259,35 @@ function selectHeader(element, field) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function bookCheckboxHeader(CheckboxState, text, field_index) {
|
||||||
|
confirmDialog(text, "GeneralChangeModal", 0, function() {
|
||||||
|
var result = $('#books-table').bootstrapTable('getSelections').map(a => a.id);
|
||||||
|
$.ajax({
|
||||||
|
method: "post",
|
||||||
|
url: getPath() + "/ajax/editbooks/" + field_index,
|
||||||
|
data: JSON.stringify({"pk": result, "field_index": field_index, "value": CheckboxState, multi: "True"}),
|
||||||
|
contentType: "application/json; charset=utf-8",
|
||||||
|
dataType: "json",
|
||||||
|
error: function (data) {
|
||||||
|
handleListServerResponse([{type:"danger", message:data.responseText}])
|
||||||
|
},
|
||||||
|
success: function (data) {
|
||||||
|
handleListServerResponse (data, true)
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},function() {
|
||||||
|
$("input:radio.check_head:checked").each(function() {
|
||||||
|
$(this).prop('checked', false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function checkboxHeader(CheckboxState, field, field_index) {
|
function checkboxHeader(CheckboxState, field, field_index) {
|
||||||
confirmDialog(field, "GeneralChangeModal", 0, function() {
|
confirmDialog(field, "GeneralChangeModal", 0, function() {
|
||||||
var result = $('#user-table').bootstrapTable('getSelections').map(a => a.id);
|
var result = $('#user-table').bootstrapTable('getSelections').map(a => a.id);
|
||||||
$.ajax({
|
$.ajax({
|
||||||
method: "post",
|
method: "post",
|
||||||
url: window.location.pathname + "/../../ajax/editlistusers/" + field,
|
url: getPath() + "/ajax/editlistusers/" + field,
|
||||||
data: {"pk": result, "field_index": field_index, "value": CheckboxState},
|
data: {"pk": result, "field_index": field_index, "value": CheckboxState},
|
||||||
error: function (data) {
|
error: function (data) {
|
||||||
handleListServerResponse([{type:"danger", message:data.responseText}])
|
handleListServerResponse([{type:"danger", message:data.responseText}])
|
||||||
@@ -912,8 +1311,10 @@ function deleteUser(a,id){
|
|||||||
function() {
|
function() {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
method:"post",
|
method:"post",
|
||||||
url: window.location.pathname + "/../../ajax/deleteuser",
|
url: getPath() + "/ajax/deleteuser",
|
||||||
data: {"userid":id},
|
contentType: "application/json; charset=utf-8",
|
||||||
|
dataType: "json",
|
||||||
|
data: JSON.stringify({"userid": [id]}),
|
||||||
success: function (data) {
|
success: function (data) {
|
||||||
userId = parseInt(id, 10);
|
userId = parseInt(id, 10);
|
||||||
selections = selections.filter(item => item !== userId);
|
selections = selections.filter(item => item !== userId);
|
||||||
@@ -940,8 +1341,10 @@ function storeLocation() {
|
|||||||
function user_handle (userId) {
|
function user_handle (userId) {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
method:"post",
|
method:"post",
|
||||||
url: window.location.pathname + "/../../ajax/deleteuser",
|
contentType: "application/json; charset=utf-8",
|
||||||
data: {"userid":userId}
|
dataType: "json",
|
||||||
|
data: JSON.stringify({"userid": [userId]}),
|
||||||
|
url: getPath() + "/ajax/deleteuser",
|
||||||
});
|
});
|
||||||
$("#user-table").bootstrapTable("refresh");
|
$("#user-table").bootstrapTable("refresh");
|
||||||
}
|
}
|
||||||
@@ -949,6 +1352,5 @@ function user_handle (userId) {
|
|||||||
function shorten_html(value, response) {
|
function shorten_html(value, response) {
|
||||||
if(value) {
|
if(value) {
|
||||||
$(this).html("[...]");
|
$(this).html("[...]");
|
||||||
// value.split('\n').slice(0, 2).join("") +
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
<h2>Details</h2>
|
<h2>Details</h2>
|
||||||
|
|
||||||
{% if entry.series|length > 0 %}
|
{% if entry.series|length > 0 %}
|
||||||
<p>{{ _("Book %(index)s of %(range)s", index=entry.series_index | formatfloat(2), range=(entry.series[0].name)|safe) }}</p>
|
<p>{{ _("Book %(index)s of %(range)s", index=entry.series_index|formatfloat(2), range=entry.series[0].name|e) }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if entry.languages|length > 0 %}
|
{% if entry.languages|length > 0 %}
|
||||||
@@ -74,7 +74,7 @@
|
|||||||
{% if entry.comments|length > 0 and entry.comments[0].text|length > 0 %}
|
{% if entry.comments|length > 0 and entry.comments[0].text|length > 0 %}
|
||||||
<div>
|
<div>
|
||||||
<h2 id="decription">{{ _('Description:') }}</h2>
|
<h2 id="decription">{{ _('Description:') }}</h2>
|
||||||
{{ entry.comments[0].text|safe }}
|
{{ entry.comments[0].text|clean_string|safe }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,6 +19,25 @@
|
|||||||
{% if sort %}data-sortable="true" {% endif %}
|
{% if sort %}data-sortable="true" {% endif %}
|
||||||
data-visible="{{visiblility.get(parameter)}}"
|
data-visible="{{visiblility.get(parameter)}}"
|
||||||
data-formatter="bookCheckboxFormatter">
|
data-formatter="bookCheckboxFormatter">
|
||||||
|
{% if parameter == "is_archived" %}
|
||||||
|
<div class="form-check">
|
||||||
|
<div>
|
||||||
|
<input type="radio" class="check_head" data-set="True" data-val="{{ parameter }}" name="options_archive_selected_books" id="false_archive_selected_books" data-name="archive_books" disabled>{{_('Archive selected books')}}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input type="radio" class="check_head" data-set="False" data-val="{{ parameter }}" name="options_unarchive_selected_books" id="true_archive_selected_books" data-name="archive_books" disabled>{{_('Unarchive selected books')}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% elif parameter == "read_status" %}
|
||||||
|
<div class="form-check">
|
||||||
|
<div>
|
||||||
|
<input type="radio" class="check_head" data-set="True" data-val="{{ parameter }}" name="options_read_selected_books" id="false_read_selected_books" data-name="read_books" disabled>{{_('Mark selected books as read')}}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input type="radio" class="check_head" data-set="False" data-val="{{ parameter }}" name="options_unread_selected_books" id="true_read_selected_books" data-name="read_books" disabled>{{_('Mark selected books as unread')}}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{{show_text}}
|
{{show_text}}
|
||||||
</th>
|
</th>
|
||||||
{%- endmacro %}
|
{%- endmacro %}
|
||||||
@@ -34,8 +53,15 @@
|
|||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
<div class="col-xs-12 col-sm-6">
|
<div class="col-xs-12 col-sm-6">
|
||||||
<div class="row form-group">
|
<div class="row form-group">
|
||||||
<div class="btn btn-default disabled" id="merge_books" aria-disabled="true">{{_('Merge selected books')}}</div>
|
<div class="btn btn-default disabled" id="merge_books" aria-disabled="true">
|
||||||
<div class="btn btn-default disabled" id="delete_selection" aria-disabled="true">{{_('Remove Selections')}}</div>
|
{{_('Merge selected books')}}
|
||||||
|
</div>
|
||||||
|
<div class="btn btn-default disabled mass_selection" id="book_delete_selection" aria-disabled="true">
|
||||||
|
{{_('Clear selections')}}
|
||||||
|
</div>
|
||||||
|
<div class="btn btn-default disabled" id="edit_selected_books" aria-disabled="true">
|
||||||
|
{{_('Edit selected books')}}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row form-group">
|
<div class="row form-group">
|
||||||
<div class="btn btn-default disabled" id="table_xchange" ><span class="glyphicon glyphicon-arrow-up"></span><span class="glyphicon glyphicon-arrow-down"></span>{{_('Exchange author and title')}}</div>
|
<div class="btn btn-default disabled" id="table_xchange" ><span class="glyphicon glyphicon-arrow-up"></span><span class="glyphicon glyphicon-arrow-down"></span>{{_('Exchange author and title')}}</div>
|
||||||
@@ -57,7 +83,7 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
{% if current_user.role_edit() %}
|
{% if current_user.role_edit() %}
|
||||||
<th data-field="state" data-checkbox="true" data-sortable="true"></th>
|
<th data-field="state" data-checkbox="true" data-visible="true" data-sortable="true"></th>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<th data-field="id" id="id" data-visible="false" data-switchable="false"></th>
|
<th data-field="id" id="id" data-visible="false" data-switchable="false"></th>
|
||||||
{{ text_table_row('title', _('Enter Title'),_('Title'), true, true) }}
|
{{ text_table_row('title', _('Enter Title'),_('Title'), true, true) }}
|
||||||
@@ -97,14 +123,23 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if current_user.role_delete_books() and current_user.role_edit()%}
|
{% if current_user.role_delete_books() and current_user.role_edit()%}
|
||||||
<th data-align="right" data-formatter="EbookActions" data-switchable="false">{{_('Delete')}}</th>
|
<th data-align="right" data-formatter="EbookActions" data-switchable="false">
|
||||||
|
<div><div class="btn btn-default button_head disabled" aria-disabled="true">
|
||||||
|
{{_('Delete selected books')}}
|
||||||
|
</div></div>
|
||||||
|
<br>
|
||||||
|
{{_('Delete')}}
|
||||||
|
</th>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block modal %}
|
{% block modal %}
|
||||||
{{ delete_book(current_user.role_delete_books()) }}
|
{{ delete_book(current_user.role_delete_books()) }}
|
||||||
|
{{ delete_confirm_modal() }}
|
||||||
|
{{ change_confirm_modal() }}
|
||||||
{% if current_user.role_edit() %}
|
{% if current_user.role_edit() %}
|
||||||
<div class="modal fade" id="mergeModal" role="dialog" aria-labelledby="metaMergeLabel">
|
<div class="modal fade" id="mergeModal" role="dialog" aria-labelledby="metaMergeLabel">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
@@ -123,15 +158,170 @@
|
|||||||
<div class="text-left" id="merge_to"></div>
|
<div class="text-left" id="merge_to"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<input id="merge_confirm" type="button" class="btn btn-danger" value="{{_('Merge')}}" name="merge_confirm" id="merge_confirm" data-dismiss="modal">
|
<input id="merge_confirm" type="button" class="btn btn-danger" value="{{_('Merge')}}" name="merge_confirm" data-dismiss="modal">
|
||||||
<button id="merge_abort" type="button" class="btn btn-default" data-dismiss="modal">{{_('Cancel')}}</button>
|
<button id="merge_abort" type="button" class="btn btn-default" data-dismiss="modal">{{_('Cancel')}}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
|
<!--div class="modal fade" id="delete_selected_modal" role="dialog" aria-labelledby="metaDeleteSelectedLabel">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header bg-danger text-center">
|
||||||
|
<span>{{_('Are you really sure?')}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p></p>
|
||||||
|
<div class="text-left">{{_('The following books will be deleted:')}}</div>
|
||||||
|
<p></p>
|
||||||
|
<div class="text-left" id="display-delete-selected-books"></div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<input id="delete_selected_confirm" type="button" class="btn btn-danger" value="{{_('Delete')}}" name="delete_selected_confirm" data-dismiss="modal">
|
||||||
|
<button id="delete_selected_abort" type="button" class="btn btn-default" data-dismiss="modal">{{_('Cancel')}}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div-->
|
||||||
|
|
||||||
|
<div class="modal fade" id="archive_selected_modal" role="dialog" aria-labelledby="metaArchiveSelectedLabel">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header bg-danger text-center">
|
||||||
|
<span>{{_('Are you really sure?')}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p></p>
|
||||||
|
<div class="text-left">{{_('The following books will be archived:')}}</div>
|
||||||
|
<p></p>
|
||||||
|
<div class="text-left" id="display-archive-selected-books"></div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<input id="archive_selected_confirm" type="button" class="btn btn-danger" value="{{_('Archive')}}" name="archive_selected_confirm" data-dismiss="modal">
|
||||||
|
<button id="archive_selected_abort" type="button" class="btn btn-default" data-dismiss="modal">{{_('Cancel')}}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal fade" id="unarchive_selected_modal" role="dialog" aria-labelledby="metaUnArchiveSelectedLabel">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header bg-danger text-center">
|
||||||
|
<span>{{_('Are you really sure?')}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p></p>
|
||||||
|
<div class="text-left">{{_('The following books will be unarchived:')}}</div>
|
||||||
|
<p></p>
|
||||||
|
<div class="text-left" id="display-unarchive-selected-books"></div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<input id="unarchive_selected_confirm" type="button" class="btn btn-danger" value="{{_('Unarchive')}}" name="unarchive_selected_confirm" data-dismiss="modal">
|
||||||
|
<button id="unarchive_selected_abort" type="button" class="btn btn-default" data-dismiss="modal">{{_('Cancel')}}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal fade" id="read_selected_modal" role="dialog" aria-labelledby="metaReadSelectedLabel">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header bg-danger text-center">
|
||||||
|
<span>{{_('Are you really sure?')}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p></p>
|
||||||
|
<div class="text-left">{{_('The following books will be marked read:')}}</div>
|
||||||
|
<p></p>
|
||||||
|
<div class="text-left" id="display-read-selected-books"></div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<input id="read_selected_confirm" type="button" class="btn btn-danger" value="{{_('Mark as read')}}" name="read_selected_confirm" data-dismiss="modal">
|
||||||
|
<button id="read_selected_abort" type="button" class="btn btn-default" data-dismiss="modal">{{_('Cancel')}}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal fade" id="unread_selected_modal" role="dialog" aria-labelledby="metaReadSelectedLabel">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header bg-danger text-center">
|
||||||
|
<span>{{_('Are you really sure?')}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p></p>
|
||||||
|
<div class="text-left">{{_('The following books will be marked unread:')}}</div>
|
||||||
|
<p></p>
|
||||||
|
<div class="text-left" id="display-unread-selected-books"></div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<input id="unread_selected_confirm" type="button" class="btn btn-danger" value="{{_('Mark as unread')}}" name="unread_selected_confirm" data-dismiss="modal">
|
||||||
|
<button id="read_selected_abort" type="button" class="btn btn-default" data-dismiss="modal">{{_('Cancel')}}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal fade" id="edit_selected_modal" role="dialog" aria-labelledby="metaEditSelectedLabel">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header bg-info text-center">
|
||||||
|
<span>{{_('Edit Metadata')}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="text-left">{{_('Edit the fields you want changed. Blank fields will be ignored:')}}</div>
|
||||||
|
<br>
|
||||||
|
Title:
|
||||||
|
<input class="form-control" id="title_input">
|
||||||
|
<p></p>
|
||||||
|
|
||||||
|
Title Sort:
|
||||||
|
<input class="form-control" id="title_sort_input">
|
||||||
|
<p></p>
|
||||||
|
|
||||||
|
Author Sort:
|
||||||
|
<input class="form-control" id="author_sort_input">
|
||||||
|
<p></p>
|
||||||
|
|
||||||
|
Authors:
|
||||||
|
<input class="form-control" id="authors_input">
|
||||||
|
<p></p>
|
||||||
|
|
||||||
|
Categories:
|
||||||
|
<input class="form-control" id="categories_input">
|
||||||
|
<p></p>
|
||||||
|
|
||||||
|
Series:
|
||||||
|
<input class="form-control" id="series_input">
|
||||||
|
<p></p>
|
||||||
|
|
||||||
|
Languages:
|
||||||
|
<input class="form-control" id="languages_input">
|
||||||
|
<p></p>
|
||||||
|
|
||||||
|
Publishers:
|
||||||
|
<input class="form-control" id="publishers_input">
|
||||||
|
<p></p>
|
||||||
|
|
||||||
|
Comments:
|
||||||
|
<input class="form-control" id="comments_input">
|
||||||
|
<p></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
<input id="edit_selected_confirm" type="button" class="btn btn-danger" value="{{_('Edit')}}" name="edit_selected_confirm" id="edit_selected_confirm" data-dismiss="modal">
|
||||||
|
<button id="edit_selected_abort" type="button" class="btn btn-default" data-dismiss="modal">{{_('Cancel')}}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block js %}
|
{% block js %}
|
||||||
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-table.min.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-table.min.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-table-locale-all.min.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-table-locale-all.min.js') }}"></script>
|
||||||
|
|||||||
@@ -163,6 +163,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="config_googlebooks_api_key">{{_('Google Books API Key')}}</label>
|
||||||
|
<input type="text" class="form-control" id="config_googlebooks_api_key" name="config_googlebooks_api_key" value="{% if config.config_googlebooks_api_key != '' %}{{ config.config_googlebooks_api_key }}{% endif %}" autocomplete="off">
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<input type="checkbox" id="config_allow_reverse_proxy_header_login" name="config_allow_reverse_proxy_header_login" data-control="reverse-proxy-login-settings" {% if config.config_allow_reverse_proxy_header_login %}checked{% endif %}>
|
<input type="checkbox" id="config_allow_reverse_proxy_header_login" name="config_allow_reverse_proxy_header_login" data-control="reverse-proxy-login-settings" {% if config.config_allow_reverse_proxy_header_login %}checked{% endif %}>
|
||||||
<label for="config_allow_reverse_proxy_header_login">{{_('Allow Reverse Proxy Authentication')}}</label>
|
<label for="config_allow_reverse_proxy_header_login">{{_('Allow Reverse Proxy Authentication')}}</label>
|
||||||
|
|||||||
@@ -232,7 +232,7 @@
|
|||||||
{% elif c.datatype == 'datetime' %}
|
{% elif c.datatype == 'datetime' %}
|
||||||
{{ column.value|formatdate }}
|
{{ column.value|formatdate }}
|
||||||
{% elif c.datatype == 'comments' %}
|
{% elif c.datatype == 'comments' %}
|
||||||
{{ column.value|safe }}
|
{{ column.value|clean_string|safe }}
|
||||||
{% elif c.datatype == 'series' %}
|
{% elif c.datatype == 'series' %}
|
||||||
{{ '%s [%s]' % (column.value, column.extra|formatfloat(2)) }}
|
{{ '%s [%s]' % (column.value, column.extra|formatfloat(2)) }}
|
||||||
{% elif c.datatype == 'text' %}
|
{% elif c.datatype == 'text' %}
|
||||||
@@ -284,7 +284,7 @@
|
|||||||
{% if entry.comments|length > 0 and entry.comments[0].text|length > 0 %}
|
{% if entry.comments|length > 0 and entry.comments[0].text|length > 0 %}
|
||||||
<div class="comments">
|
<div class="comments">
|
||||||
<h3 id="decription">{{ _('Description:') }}</h3>
|
<h3 id="decription">{{ _('Description:') }}</h3>
|
||||||
{{ entry.comments[0].text|safe }}
|
{{ entry.comments[0].text|clean_string|safe }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
@@ -294,7 +294,7 @@
|
|||||||
{% if current_user.is_authenticated %}
|
{% if current_user.is_authenticated %}
|
||||||
{% if current_user.shelf.all() or g.shelves_access %}
|
{% if current_user.shelf.all() or g.shelves_access %}
|
||||||
<div id="shelf-actions" class="btn-toolbar" role="toolbar">
|
<div id="shelf-actions" class="btn-toolbar" role="toolbar">
|
||||||
<div class="btn-group" role="group" aria-label="Add to shelves">
|
<div class="btn-group dropup" role="group" aria-label="Add to shelves">
|
||||||
<button id="add-to-shelf" type="button"
|
<button id="add-to-shelf" type="button"
|
||||||
class="btn btn-primary btn-sm dropdown-toggle" data-toggle="dropdown"
|
class="btn btn-primary btn-sm dropdown-toggle" data-toggle="dropdown"
|
||||||
aria-haspopup="true" aria-expanded="false">
|
aria-haspopup="true" aria-expanded="false">
|
||||||
|
|||||||
+51
-1
@@ -63,7 +63,57 @@
|
|||||||
term="{{tag.name}}"
|
term="{{tag.name}}"
|
||||||
label="{{tag.name}}"/>
|
label="{{tag.name}}"/>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if entry.Books.comments[0] %}<summary>{{entry.Books.comments[0].text|striptags}}</summary>{% endif %}
|
<content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml">
|
||||||
|
{% if entry.Books.ratings.__len__() > 0 %}
|
||||||
|
RATING: {% for number in range((entry.Books.ratings[0].rating/2)|int(2)) %}★{% endfor %}<br/>
|
||||||
|
{% endif %}
|
||||||
|
{% if entry.Books.tags|length > 0 %}
|
||||||
|
TAGS: {% for tag in entry.Books.tags %}{{tag.name}}{{ ", " if not loop.last else "" }}{% endfor %}<br/>
|
||||||
|
{% endif %}
|
||||||
|
{% if entry.Books.series.__len__() > 0 %}
|
||||||
|
SERIES: {{entry.Books.series[0].name}} [{{entry.Books.series_index|formatfloat(2)}}]<br/>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if cc|length > 0 %}
|
||||||
|
{% for c in cc %}
|
||||||
|
{% if entry.Books['custom_column_' ~ c.id]|length > 0 %}
|
||||||
|
{{ c.name }}:
|
||||||
|
{% for column in entry.Books['custom_column_' ~ c.id] %}
|
||||||
|
{% if c.datatype == 'rating' %}
|
||||||
|
{{ (column.value / 2)|formatfloat }}
|
||||||
|
{% else %}
|
||||||
|
{% if c.datatype == 'bool' %}
|
||||||
|
{% if column.value == true %}
|
||||||
|
✓
|
||||||
|
{% else %}
|
||||||
|
✕
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
{% if c.datatype == 'float' %}
|
||||||
|
{{ column.value|formatfloat(2) }}
|
||||||
|
{% elif c.datatype == 'datetime' %}
|
||||||
|
{{ column.value|formatdate }}
|
||||||
|
{% elif c.datatype == 'comments' %}
|
||||||
|
{{ column.value|clean_string|safe }}
|
||||||
|
{% elif c.datatype == 'series' %}
|
||||||
|
{{ '%s [%s]' % (column.value, column.extra|formatfloat(2)) }}
|
||||||
|
{% elif c.datatype == 'text' %}
|
||||||
|
{{ column.value.strip() }}{% if not loop.last %}, {% endif %}
|
||||||
|
{% else %}
|
||||||
|
{{ column.value }}
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
<br/>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if entry.Books.comments[0] %}
|
||||||
|
<p>{{entry.Books.comments[0].text}}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div></content>
|
||||||
{% if entry.Books.has_cover %}
|
{% if entry.Books.has_cover %}
|
||||||
<link type="image/jpeg" href="{{url_for('opds.feed_get_cover', book_id=entry.Books.id)}}" rel="http://opds-spec.org/image"/>
|
<link type="image/jpeg" href="{{url_for('opds.feed_get_cover', book_id=entry.Books.id)}}" rel="http://opds-spec.org/image"/>
|
||||||
<link type="image/jpeg" href="{{url_for('opds.feed_get_cover', book_id=entry.Books.id)}}" rel="http://opds-spec.org/image/thumbnail"/>
|
<link type="image/jpeg" href="{{url_for('opds.feed_get_cover', book_id=entry.Books.id)}}" rel="http://opds-spec.org/image/thumbnail"/>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
{% if g.google_site_verification|length > 0 %}
|
{% if g.google_site_verification|length > 0 %}
|
||||||
<meta name="google-site-verification" content="{{g.google_site_verification}}">
|
<meta name="google-site-verification" content="{{g.google_site_verification}}">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
{% if g.google_site_verification|length > 0 %}
|
{% if g.google_site_verification|length > 0 %}
|
||||||
<meta name="google-site-verification" content="{{g.google_site_verification}}">
|
<meta name="google-site-verification" content="{{g.google_site_verification}}">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -41,7 +41,6 @@
|
|||||||
<div class="plexBack"><a href="{{url_for('web.index')}}"></a></div>
|
<div class="plexBack"><a href="{{url_for('web.index')}}"></a></div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if current_user.is_authenticated or g.allow_anonymous %}
|
{% if current_user.is_authenticated or g.allow_anonymous %}
|
||||||
<!--# margin 0, padding 15, background color-->
|
|
||||||
<form class="navbar-form navbar-left" role="search" action="{{url_for('search.simple_search')}}" method="GET">
|
<form class="navbar-form navbar-left" role="search" action="{{url_for('search.simple_search')}}" method="GET">
|
||||||
<div class="form-group input-group input-group-sm">
|
<div class="form-group input-group input-group-sm">
|
||||||
<label for="query" class="sr-only">{{_('Search')}}</label>
|
<label for="query" class="sr-only">{{_('Search')}}</label>
|
||||||
@@ -148,13 +147,13 @@
|
|||||||
<li class="nav-head hidden-xs">{{_('Browse')}}</li>
|
<li class="nav-head hidden-xs">{{_('Browse')}}</li>
|
||||||
{% for element in sidebar %}
|
{% for element in sidebar %}
|
||||||
{% if current_user.check_visibility(element['visibility']) and element['public'] %}
|
{% if current_user.check_visibility(element['visibility']) and element['public'] %}
|
||||||
<li id="nav_{{element['id']}}" {% if page == element['page'] %}class="active"{% endif %}><a href="{{url_for(element['link'], data=element['page'], sort_param='stored')}}"><span class="glyphicon {{element['glyph']}}"></span> {{_(element['text'])}}</a></li>
|
<li id="nav_{{element['id']}}" {% if page == element['page'] %}class="active"{% endif %}><a href="{% if element['no_param'] %}{{url_for(element['link'], data=element['page'])}}{%else%}{{url_for(element['link'], data=element['page'], sort_param='stored')}}{% endif %}"><span class="glyphicon {{element['glyph']}}"></span> {{_(element['text'])}}</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if current_user.is_authenticated or g.allow_anonymous %}
|
{% if current_user.is_authenticated or g.allow_anonymous %}
|
||||||
<li class="nav-head hidden-xs public-shelves">{{_('Shelves')}}</li>
|
<li class="nav-head hidden-xs public-shelves">{{_('Shelves')}}</li>
|
||||||
{% for shelf in g.shelves_access %}
|
{% for shelf in g.shelves_access %}
|
||||||
<li><a href="{{url_for('shelf.show_shelf', shelf_id=shelf.id)}}"><span class="glyphicon glyphicon-list shelf"></span> {{shelf.name|shortentitle(40)}}{% if shelf.is_public == 1 %} {{_('(Public)')}}{% endif %}</a></li>
|
<li><a href="{{url_for('shelf.show_shelf', shelf_id=shelf.id)}}"><span class="glyphicon glyphicon-list shelf"></span> {{shelf.name|shortentitle(40)}}{% if shelf.is_public == 1 %} {{_('(Public)')}}{% endif %} <span class="badge badge-sm">{{shelf.books.all()|length}}</span></a></li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if not current_user.is_anonymous %}
|
{% if not current_user.is_anonymous %}
|
||||||
<li id="nav_createshelf" class="create-shelf"><a href="{{url_for('shelf.create_shelf')}}">{{_('Create a Shelf')}}</a></li>
|
<li id="nav_createshelf" class="create-shelf"><a href="{{url_for('shelf.create_shelf')}}">{{_('Create a Shelf')}}</a></li>
|
||||||
@@ -171,8 +170,7 @@
|
|||||||
{% if pagination and (pagination.has_next or pagination.has_prev) %}
|
{% if pagination and (pagination.has_next or pagination.has_prev) %}
|
||||||
<div class="pagination">
|
<div class="pagination">
|
||||||
{% if pagination.has_prev %}
|
{% if pagination.has_prev %}
|
||||||
<li class="page-item page-previous"><a class="page-link" aria-label="next page" href="{{ (pagination.page - 1)|url_for_other_page
|
<li class="page-item page-previous"><a class="page-link" aria-label="next page" href="{{ (pagination.page - 1)|url_for_other_page }}">« {{_('Previous')}}</a></li>
|
||||||
}}">« {{_('Previous')}}</a></li>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% for page in pagination.iter_pages() %}
|
{% for page in pagination.iter_pages() %}
|
||||||
{% if page %}
|
{% if page %}
|
||||||
@@ -186,8 +184,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if pagination.has_next %}
|
{% if pagination.has_next %}
|
||||||
<li class="page-item page-next"><a class="page-link next" aria-label="next page" href="{{ (pagination.page + 1)|url_for_other_page
|
<li class="page-item page-next"><a class="page-link next" aria-label="next page" href="{{ (pagination.page + 1)|url_for_other_page }}">{{_('Next')}} »</a></li>
|
||||||
}}">{{_('Next')}} »</a></li>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
<button class="update-view btn btn-primary" data-target="series_view" id="grid-button" data-view="grid">{{_('Grid')}}</button>
|
<button class="update-view btn btn-primary" data-target="series_view" id="grid-button" data-view="grid">{{_('Grid')}}</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="container">
|
<div class="container1">
|
||||||
<div id="list" class="col-xs-12 col-sm-6">
|
<div id="list" class="col-xs-12 col-sm-6">
|
||||||
{% for entry in entries %}
|
{% for entry in entries %}
|
||||||
{% if loop.index0 == (loop.length/2+loop.length%2)|int and loop.length > 20 %}
|
{% if loop.index0 == (loop.length/2+loop.length%2)|int and loop.length > 20 %}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<title>{{ entry.title }}</title>
|
<title>{{ entry.title }}</title>
|
||||||
<meta name="description" content="">
|
<meta name="description" content="">
|
||||||
<meta name="viewport" content="width=device-width">
|
<meta name="viewport" content="width=device-width">
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
{% if g.google_site_verification|length > 0 %}
|
{% if g.google_site_verification|length > 0 %}
|
||||||
<meta name="google-site-verification" content="{{g.google_site_verification}}">
|
<meta name="google-site-verification" content="{{g.google_site_verification}}">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -134,7 +134,7 @@
|
|||||||
{% elif c.datatype == 'datetime' %}
|
{% elif c.datatype == 'datetime' %}
|
||||||
{{ column.value|formatdate }}
|
{{ column.value|formatdate }}
|
||||||
{% elif c.datatype == 'comments' %}
|
{% elif c.datatype == 'comments' %}
|
||||||
{{column.value|safe}}
|
{{column.value|clean_string|safe}}
|
||||||
{% elif c.datatype == 'series' %}
|
{% elif c.datatype == 'series' %}
|
||||||
{{ '%s [%s]' % (column.value, column.extra|formatfloat(2)) }}
|
{{ '%s [%s]' % (column.value, column.extra|formatfloat(2)) }}
|
||||||
{% elif c.datatype == 'text' %}
|
{% elif c.datatype == 'text' %}
|
||||||
@@ -175,7 +175,7 @@
|
|||||||
{% if entry.comments|length > 0 and entry.comments[0].text|length > 0%}
|
{% if entry.comments|length > 0 and entry.comments[0].text|length > 0%}
|
||||||
<div class="comments">
|
<div class="comments">
|
||||||
<h3 id="decription">{{_('Description:')}}</h3>
|
<h3 id="decription">{{_('Description:')}}</h3>
|
||||||
{{entry.comments[0].text|safe}}
|
{{entry.comments[0].text|clean_string|safe}}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|||||||
@@ -48,8 +48,7 @@
|
|||||||
<div class="modal-body text-center">
|
<div class="modal-body text-center">
|
||||||
<p>
|
<p>
|
||||||
<span class="hidden" id="book_format">{{_('This book format will be permanently erased from database')}}</span>
|
<span class="hidden" id="book_format">{{_('This book format will be permanently erased from database')}}</span>
|
||||||
<span class="hidden" id="book_complete">{{_('This book will be permanently erased from database')}}</span>
|
<span class="hidden" id="book_complete">{{_('This book will be permanently erased from database and hard disk')}}</span>
|
||||||
<span>{{_('and hard disk')}}</span>
|
|
||||||
</p>
|
</p>
|
||||||
{% if config.config_kobo_sync %}
|
{% if config.config_kobo_sync %}
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
+296
-22
@@ -7,7 +7,7 @@
|
|||||||
<title>{{_('epub Reader')}} | {{title}}</title>
|
<title>{{_('epub Reader')}} | {{title}}</title>
|
||||||
<meta name="description" content="">
|
<meta name="description" content="">
|
||||||
<meta name="viewport" content="width=device-width, user-scalable=no">
|
<meta name="viewport" content="width=device-width, user-scalable=no">
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
{% if g.google_site_verification|length > 0 %}
|
{% if g.google_site_verification|length > 0 %}
|
||||||
<meta name="google-site-verification" content="{{g.google_site_verification}}">
|
<meta name="google-site-verification" content="{{g.google_site_verification}}">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -29,6 +29,7 @@
|
|||||||
<!--a id="show-Search" class="show_view icon-search" data-view="Search">Search</a-->
|
<!--a id="show-Search" class="show_view icon-search" data-view="Search">Search</a-->
|
||||||
<a id="show-Toc" class="show_view icon-list-1 active" data-view="Toc">TOC</a>
|
<a id="show-Toc" class="show_view icon-list-1 active" data-view="Toc">TOC</a>
|
||||||
<a id="show-Bookmarks" class="show_view icon-bookmark" data-view="Bookmarks">Bookmarks</a>
|
<a id="show-Bookmarks" class="show_view icon-bookmark" data-view="Bookmarks">Bookmarks</a>
|
||||||
|
<a id="back-button" data-view="Back to Books" href="{{ url_for('web.index') }}">Books</a>
|
||||||
<!--a id="show-Notes" class="show_view icon-edit" data-view="Notes">Notes</a-->
|
<!--a id="show-Notes" class="show_view icon-edit" data-view="Notes">Notes</a-->
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -49,7 +50,6 @@
|
|||||||
</div-->
|
</div-->
|
||||||
</div>
|
</div>
|
||||||
<div id="main">
|
<div id="main">
|
||||||
|
|
||||||
<div id="titlebar">
|
<div id="titlebar">
|
||||||
<div id="opener">
|
<div id="opener">
|
||||||
<a id="slider" class="icon-menu">Menu</a>
|
<a id="slider" class="icon-menu">Menu</a>
|
||||||
@@ -70,14 +70,19 @@
|
|||||||
<div id="prev" class="arrow">‹</div>
|
<div id="prev" class="arrow">‹</div>
|
||||||
<div id="viewer"></div>
|
<div id="viewer"></div>
|
||||||
<div id="next" class="arrow">›</div>
|
<div id="next" class="arrow">›</div>
|
||||||
|
<div class="read-footer">
|
||||||
|
<div id="pages-count"></div>
|
||||||
<div id="progress">0%</div>
|
<div id="progress">0%</div>
|
||||||
<div id="loader"><img src="{{ url_for('static', filename='img/loader.gif') }}"></div>
|
</div>
|
||||||
|
<div id="loader">
|
||||||
|
<img src="{{ url_for('static', filename='img/loader.gif') }}">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal md-effect-1" id="settings-modal">
|
<div class="modal md-effect-1" id="settings-modal">
|
||||||
<div class="md-content">
|
<div class="md-content">
|
||||||
<h3>{{_('Settings')}}</h3>
|
<h3>{{_('Settings')}}</h3>
|
||||||
<div class="form-group themes" id="themes">
|
<div class="form-group themes" id="themes">
|
||||||
{{_('Choose a theme below:')}}}<br />
|
{{_('Choose a theme below:')}}<br />
|
||||||
|
|
||||||
<!-- Hardcoded a tick in the light theme button because it is the "default" theme. Need to find a way to do this dynamically on startup-->
|
<!-- Hardcoded a tick in the light theme button because it is the "default" theme. Need to find a way to do this dynamically on startup-->
|
||||||
<button type="button" id="lightTheme" class="lightTheme" onclick="selectTheme(this.id)"><span
|
<button type="button" id="lightTheme" class="lightTheme" onclick="selectTheme(this.id)"><span
|
||||||
@@ -86,8 +91,17 @@
|
|||||||
id="darkSelected"> </span>{{_('Dark')}}</button>
|
id="darkSelected"> </span>{{_('Dark')}}</button>
|
||||||
<button type="button" id="sepiaTheme" class="sepiaTheme" onclick="selectTheme(this.id)"><span
|
<button type="button" id="sepiaTheme" class="sepiaTheme" onclick="selectTheme(this.id)"><span
|
||||||
id="sepiaSelected"> </span>{{_('Sepia')}}</button>
|
id="sepiaSelected"> </span>{{_('Sepia')}}</button>
|
||||||
|
<button type="button" id="amberTheme" class="amberTheme" onclick="selectTheme(this.id)"><span
|
||||||
|
id="amberSelected"> </span>{{_('Amber')}}</button>
|
||||||
<button type="button" id="blackTheme" class="blackTheme" onclick="selectTheme(this.id)"><span
|
<button type="button" id="blackTheme" class="blackTheme" onclick="selectTheme(this.id)"><span
|
||||||
id="blackSelected"> </span>{{_('Black')}}</button>
|
id="blackSelected"> </span>{{_('Black')}}</button>
|
||||||
|
|
||||||
|
<!-- Custom theme: color input + pickr -->
|
||||||
|
<div id="customThemeWrapper" style="display: inline-flex; align-items: center;gap: 8px;margin: 5px 8px;">Custom:
|
||||||
|
<!-- Picker-only UI: swatch acts as the picker button, and a small span shows selection tick -->
|
||||||
|
<div id="customThemeSwatch" title="Pick color" style="width: 30px; height: 30px; border: 1px solid #ccc; border-radius: 3px;cursor: pointer;"></div>
|
||||||
|
<span id="customSelected"> </span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<p>
|
<p>
|
||||||
@@ -95,10 +109,17 @@
|
|||||||
name="sidebarReflow">{{_('Reflow text when sidebars are open.')}}
|
name="sidebarReflow">{{_('Reflow text when sidebars are open.')}}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<p>
|
||||||
|
<input type="checkbox" id="showPagesCount" name="showPagesCount"/>{{_('Show pages count')}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<div class="form-group fontSizeWrapper">
|
<div class="form-group fontSizeWrapper">
|
||||||
<div class="slider">
|
<label>{{ _('Font Size') }}</label>
|
||||||
<label for="fader">{{ _('Font Sizes') }}</label>
|
<div class="font-size-controls" style="display: flex;align-items: center;gap: 10px;margin-top: 2px;">
|
||||||
<input type="range" min="75" max="200" value="100" id="fontSizeFader" step="25">
|
<button type="button" id="fontSizeDecrease" style="padding: 8px 15px; font-size: 18px; cursor: pointer">−</button>
|
||||||
|
<span id="fontSizeDisplay" style="min-width: 60px;text-align: center;font-size: 14px;font-weight: bold;">100%</span>
|
||||||
|
<button type="button" id="fontSizeIncrease" style="padding: 8px 15px; font-size: 18px; cursor: pointer">+</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="font" id="font">
|
<div class="font" id="font">
|
||||||
@@ -119,8 +140,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="overlay"></div>
|
<div class="overlay"></div>
|
||||||
<script src="{{ url_for('static', filename='js/libs/jquery.min.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/libs/jquery.min.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='js/compress/jszip_epub.min.js') }}">
|
<script src="{{ url_for('static', filename='js/compress/jszip_epub.min.js') }}"></script>
|
||||||
</script>
|
|
||||||
<script src="{{ url_for('static', filename='js/libs/epub.min.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/libs/epub.min.js') }}"></script>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
window.calibre = {
|
window.calibre = {
|
||||||
@@ -132,6 +152,14 @@
|
|||||||
useBookmarks: "{{ current_user.is_authenticated | tojson }}"
|
useBookmarks: "{{ current_user.is_authenticated | tojson }}"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// load custom theme color from localStorage (if any)
|
||||||
|
var _savedCustomThemeColor = null;
|
||||||
|
try {
|
||||||
|
_savedCustomThemeColor = localStorage.getItem(
|
||||||
|
"calibre.reader.customTheme"
|
||||||
|
);
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
window.themes = {
|
window.themes = {
|
||||||
"darkTheme": {
|
"darkTheme": {
|
||||||
"bgColor": "#202124",
|
"bgColor": "#202124",
|
||||||
@@ -148,6 +176,11 @@
|
|||||||
"css_path": "{{ url_for('static', filename='css/epub_themes.css') }}",
|
"css_path": "{{ url_for('static', filename='css/epub_themes.css') }}",
|
||||||
"title-color": "#4f4f4f"
|
"title-color": "#4f4f4f"
|
||||||
},
|
},
|
||||||
|
"amberTheme": {
|
||||||
|
"bgColor": "#e8bf5e",
|
||||||
|
"css_path": "{{ url_for('static', filename='css/epub_themes.css') }}",
|
||||||
|
"title-color": "#2f2f2f"
|
||||||
|
},
|
||||||
"blackTheme": {
|
"blackTheme": {
|
||||||
"bgColor": "black",
|
"bgColor": "black",
|
||||||
"css_path": "{{ url_for('static', filename='css/epub_themes.css') }}",
|
"css_path": "{{ url_for('static', filename='css/epub_themes.css') }}",
|
||||||
@@ -157,20 +190,46 @@
|
|||||||
|
|
||||||
function selectTheme (id) {
|
function selectTheme (id) {
|
||||||
let tickSpans = document.getElementById("themes").querySelectorAll("span");
|
let tickSpans = document.getElementById("themes").querySelectorAll("span");
|
||||||
|
|
||||||
// Remove tick mark from all theme buttons
|
|
||||||
tickSpans.forEach(function (tickSpan) {
|
tickSpans.forEach(function (tickSpan) {
|
||||||
document.getElementById(tickSpan.id).textContent = "";
|
try {
|
||||||
|
tickSpan.textContent = "";
|
||||||
|
} catch (e) {}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add tick mark to the button corresponding to the currently selected theme
|
// If the theme button exists, set its inner span to a tick. Otherwise set any span with the matching id.
|
||||||
document.getElementById(id).querySelector("span").textContent = "✓";
|
let el = document.getElementById(id);
|
||||||
|
if (el) {
|
||||||
|
let sp = el.querySelector("span");
|
||||||
|
if (sp) sp.textContent = "✓";
|
||||||
|
} else {
|
||||||
|
let spById = document.getElementById(id + "Selected") || document.getElementById("customSelected");
|
||||||
|
if (spById) spById.textContent = "✓";
|
||||||
|
}
|
||||||
|
|
||||||
// Saving theme to local storage
|
// Saving theme to local storage
|
||||||
localStorage.setItem("calibre.reader.theme", id);
|
localStorage.setItem("calibre.reader.theme", id);
|
||||||
|
|
||||||
|
// If selecting custom theme, ensure epubjs theme is registered with chosen bg color
|
||||||
|
if (id === "customTheme") {
|
||||||
|
let customColor = window.themes.customTheme.bgColor || "#ffffff";
|
||||||
|
try {
|
||||||
|
if (reader && reader.rendition && reader.rendition.themes) {
|
||||||
|
reader.rendition.themes.register("customTheme", {
|
||||||
|
body: {
|
||||||
|
background: customColor,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
reader.rendition.themes.select("customTheme");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to register/select customTheme", e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
// Apply theme to epubjs iframe
|
// Apply theme to epubjs iframe
|
||||||
|
try {
|
||||||
reader.rendition.themes.select(id);
|
reader.rendition.themes.select(id);
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
// Apply theme to rest of the page.
|
// Apply theme to rest of the page.
|
||||||
document.getElementById("main").style.backgroundColor = themes[id]["bgColor"];
|
document.getElementById("main").style.backgroundColor = themes[id]["bgColor"];
|
||||||
@@ -179,14 +238,45 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// font size settings logic
|
// font size settings logic
|
||||||
let fontSizeFader = document.getElementById('fontSizeFader');
|
let currentFontSize = 100; // default 100%
|
||||||
fontSizeFader.addEventListener("change", function () {
|
const minFontSize = 50;
|
||||||
reader.rendition.themes.fontSize(`${this.value}%`);
|
const maxFontSize = 300;
|
||||||
|
const stepSize = 5;
|
||||||
|
|
||||||
|
const fontSizeDisplay = document.getElementById("fontSizeDisplay");
|
||||||
|
const fontSizeDecrease = document.getElementById("fontSizeDecrease");
|
||||||
|
const fontSizeIncrease = document.getElementById("fontSizeIncrease");
|
||||||
|
|
||||||
|
function updateFontSize(newSize) {
|
||||||
|
if (newSize < minFontSize) newSize = minFontSize;
|
||||||
|
if (newSize > maxFontSize) newSize = maxFontSize;
|
||||||
|
|
||||||
|
currentFontSize = newSize;
|
||||||
|
fontSizeDisplay.textContent = newSize + "%";
|
||||||
|
localStorage.setItem("calibre.reader.fontSize", newSize);
|
||||||
|
if (reader && reader.rendition) {
|
||||||
|
reader.rendition.themes.fontSize(`${newSize}%`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore saved font size on load
|
||||||
|
const savedFontSize = localStorage.getItem("calibre.reader.fontSize");
|
||||||
|
if (savedFontSize) {
|
||||||
|
currentFontSize = parseInt(savedFontSize);
|
||||||
|
fontSizeDisplay.textContent = savedFontSize + "%";
|
||||||
|
}
|
||||||
|
|
||||||
|
fontSizeDecrease.addEventListener("click", function () {
|
||||||
|
updateFontSize(currentFontSize - stepSize);
|
||||||
|
});
|
||||||
|
|
||||||
|
fontSizeIncrease.addEventListener("click", function () {
|
||||||
|
updateFontSize(currentFontSize + stepSize);
|
||||||
});
|
});
|
||||||
|
|
||||||
let defaultFont;
|
let defaultFont;
|
||||||
|
|
||||||
function selectFont(id) {
|
window.selectFont = function (id) {
|
||||||
if (!defaultFont) {
|
if (!defaultFont) {
|
||||||
defaultFont = reader.rendition.getContents()[0]?.css('font-family');
|
defaultFont = reader.rendition.getContents()[0]?.css('font-family');
|
||||||
}
|
}
|
||||||
@@ -197,12 +287,15 @@
|
|||||||
}
|
}
|
||||||
document.getElementById(id).querySelector("span").textContent = "✓";
|
document.getElementById(id).querySelector("span").textContent = "✓";
|
||||||
|
|
||||||
if (id == 'default') {
|
// Save font selection to localStorage
|
||||||
|
localStorage.setItem("calibre.reader.font", id);
|
||||||
|
|
||||||
|
if (id == "default") {
|
||||||
reader.rendition.themes.font(defaultFont);
|
reader.rendition.themes.font(defaultFont);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
reader.rendition.themes.font(id);
|
reader.rendition.themes.font(id);
|
||||||
}
|
};
|
||||||
|
|
||||||
function spread(id) {
|
function spread(id) {
|
||||||
spans = document.getElementById("layout").querySelectorAll("span");
|
spans = document.getElementById("layout").querySelectorAll("span");
|
||||||
@@ -210,12 +303,193 @@
|
|||||||
spans[i].textContent = "";
|
spans[i].textContent = "";
|
||||||
}
|
}
|
||||||
document.getElementById(id).querySelector("span").textContent = "✓";
|
document.getElementById(id).querySelector("span").textContent = "✓";
|
||||||
reader.rendition.spread(id==='spread'?true:'none');
|
reader.rendition.spread(id==="spread" ? true : "none");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pages counter visibility setting
|
||||||
|
(function () {
|
||||||
|
var checkbox = document.getElementById("showPagesCount");
|
||||||
|
var pagesEl = document.getElementById("pages-count");
|
||||||
|
var key = "calibre.reader.showPages";
|
||||||
|
var saved = localStorage.getItem(key);
|
||||||
|
var show = saved === null ? true : saved === "true";
|
||||||
|
if (checkbox) checkbox.checked = show;
|
||||||
|
if (pagesEl) pagesEl.style.display = show ? "" : "none";
|
||||||
|
if (checkbox) {
|
||||||
|
checkbox.addEventListener("change", function () {
|
||||||
|
var val = checkbox.checked;
|
||||||
|
localStorage.setItem(key, String(val));
|
||||||
|
var target = document.getElementById("pages-count");
|
||||||
|
if (target) target.style.visibility = val ? "visible" : "hidden";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
// Initialize custom theme UI and Pickr once Pickr is available
|
||||||
|
(function () {
|
||||||
|
var swatch = document.getElementById("customThemeSwatch");
|
||||||
|
var saved =
|
||||||
|
window.themes &&
|
||||||
|
window.themes.customTheme &&
|
||||||
|
window.themes.customTheme.bgColor
|
||||||
|
? window.themes.customTheme.bgColor
|
||||||
|
: "#ffffff";
|
||||||
|
if (swatch) swatch.style.background = saved;
|
||||||
|
|
||||||
|
function _hexToRgb(hex) {
|
||||||
|
hex = hex.replace("#", "");
|
||||||
|
if (hex.length === 3) {
|
||||||
|
hex = hex
|
||||||
|
.split("")
|
||||||
|
.map(function (h) {
|
||||||
|
return h + h;
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
var bigint = parseInt(hex, 16);
|
||||||
|
return {
|
||||||
|
r: (bigint >> 16) & 255,
|
||||||
|
g: (bigint >> 8) & 255,
|
||||||
|
b: bigint & 255,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Better contrast decision using WCAG relative luminance and contrast ratio
|
||||||
|
// Returns true if black text is the better choice (i.e. background is light)
|
||||||
|
function _isLight(hex) {
|
||||||
|
try {
|
||||||
|
var rgb = _hexToRgb(hex);
|
||||||
|
// convert 0-255 to 0-1
|
||||||
|
var srgb = { r: rgb.r / 255, g: rgb.g / 255, b: rgb.b / 255 };
|
||||||
|
function lin(c) {
|
||||||
|
return c <= 0.03928
|
||||||
|
? c / 12.92
|
||||||
|
: Math.pow((c + 0.055) / 1.055, 2.4);
|
||||||
|
}
|
||||||
|
var R = lin(srgb.r),
|
||||||
|
G = lin(srgb.g),
|
||||||
|
B = lin(srgb.b);
|
||||||
|
var L = 0.2126 * R + 0.7152 * G + 0.0722 * B; // relative luminance
|
||||||
|
|
||||||
|
// contrast ratios against black (L=0) and white (L=1)
|
||||||
|
var contrastWithBlack = (L + 0.05) / (0.0 + 0.05);
|
||||||
|
var contrastWithWhite = (1.0 + 0.05) / (L + 0.05);
|
||||||
|
|
||||||
|
// pick the text color that gives higher contrast. If black gives >= contrast, treat bg as light
|
||||||
|
return contrastWithBlack >= contrastWithWhite;
|
||||||
|
} catch (e) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyCustomColor(hex) {
|
||||||
|
if (!hex) return;
|
||||||
|
if (hex[0] !== "#") hex = "#" + hex;
|
||||||
|
window.themes.customTheme.bgColor = hex;
|
||||||
|
try {
|
||||||
|
localStorage.setItem("calibre.reader.customTheme", hex);
|
||||||
|
} catch (e) {}
|
||||||
|
if (swatch) swatch.style.background = hex;
|
||||||
|
|
||||||
|
// Mark theme as selected
|
||||||
|
try {
|
||||||
|
localStorage.setItem("calibre.reader.theme", "customTheme");
|
||||||
|
} catch (e) {}
|
||||||
|
// Clear ticks and set custom tick
|
||||||
|
var tickSpans = document
|
||||||
|
.getElementById("themes")
|
||||||
|
.querySelectorAll("span");
|
||||||
|
tickSpans.forEach(function (ts) {
|
||||||
|
ts.textContent = "";
|
||||||
|
});
|
||||||
|
var customTick = document.getElementById("customSelected");
|
||||||
|
if (customTick) customTick.textContent = "✓";
|
||||||
|
|
||||||
|
// Compute title/text color based on contrast (black or white)
|
||||||
|
var titleColor = _isLight(hex) ? "#000000" : "#ffffff";
|
||||||
|
try {
|
||||||
|
document.getElementById("main").style.backgroundColor = hex;
|
||||||
|
document.getElementById("titlebar").style.color = titleColor;
|
||||||
|
document.getElementById("progress").style.color = titleColor;
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
// Persist title-color in themes map so selectTheme reads a consistent value
|
||||||
|
try {
|
||||||
|
window.themes.customTheme["title-color"] = titleColor;
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
// Register and select theme in epub rendition (include text color)
|
||||||
|
try {
|
||||||
|
if (reader && reader.rendition && reader.rendition.themes) {
|
||||||
|
reader.rendition.themes.register("customTheme", {
|
||||||
|
body: { background: hex, color: titleColor },
|
||||||
|
});
|
||||||
|
reader.rendition.themes.select("customTheme");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to apply custom theme to reader", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delay init until Pickr is available
|
||||||
|
function ensurePickrAndInit() {
|
||||||
|
if (window.Pickr) {
|
||||||
|
try {
|
||||||
|
var pickr = Pickr.create({
|
||||||
|
el: "#customThemeSwatch",
|
||||||
|
theme: "classic",
|
||||||
|
default: saved,
|
||||||
|
components: {
|
||||||
|
preview: true,
|
||||||
|
opacity: false,
|
||||||
|
hue: true,
|
||||||
|
interaction: {
|
||||||
|
hex: true,
|
||||||
|
input: true,
|
||||||
|
save: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Immediate apply when color changes in the picker
|
||||||
|
pickr.on("change", function (color, instance) {
|
||||||
|
try {
|
||||||
|
var hex = color.toHEXA().toString();
|
||||||
|
if (swatch) swatch.style.background = hex;
|
||||||
|
applyCustomColor(hex);
|
||||||
|
} catch (e) {}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also respond to save (some pickr configs use save)
|
||||||
|
pickr.on("save", function (color, instance) {
|
||||||
|
try {
|
||||||
|
var hex = color.toHEXA().toString();
|
||||||
|
if (swatch) swatch.style.background = hex;
|
||||||
|
applyCustomColor(hex);
|
||||||
|
pickr.hide();
|
||||||
|
} catch (e) {}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clicking swatch opens pickr
|
||||||
|
if (swatch)
|
||||||
|
swatch.addEventListener("click", function () {
|
||||||
|
pickr.show();
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Pickr init failed", e);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// wait a bit
|
||||||
|
setTimeout(ensurePickrAndInit, 150);
|
||||||
|
}
|
||||||
|
|
||||||
|
ensurePickrAndInit();
|
||||||
|
})();
|
||||||
</script>
|
</script>
|
||||||
<script src="{{ url_for('static', filename='js/libs/screenfull.min.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/libs/screenfull.min.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='js/libs/reader.min.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/libs/reader.min.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='js/reading/epub.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/reading/epub.js') }}"></script>
|
||||||
<!--script src="{{ url_for('static', filename='js/reading/locationchange-polyfill.js') }}"></script-->
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<meta name="description" content="">
|
<meta name="description" content="">
|
||||||
<title>{{_('Comic Reader')}} | {{title}}</title>
|
<title>{{_('Comic Reader')}} | {{title}}</title>
|
||||||
<meta name="viewport" content="width=device-width, user-scalable=no">
|
<meta name="viewport" content="width=device-width, user-scalable=no">
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
{% if g.google_site_verification|length > 0 %}
|
{% if g.google_site_verification|length > 0 %}
|
||||||
<meta name="google-site-verification" content="{{g.google_site_verification}}">
|
<meta name="google-site-verification" content="{{g.google_site_verification}}">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<title>{{_('DJVU Reader')}} | {{title}}</title>
|
<title>{{_('DJVU Reader')}} | {{title}}</title>
|
||||||
<meta name="description" content="">
|
<meta name="description" content="">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
{% if g.google_site_verification|length > 0 %}
|
{% if g.google_site_verification|length > 0 %}
|
||||||
<meta name="google-site-verification" content="{{g.google_site_verification}}">
|
<meta name="google-site-verification" content="{{g.google_site_verification}}">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<title>{{_('txt Reader')}} | {{title}}</title>
|
<title>{{_('txt Reader')}} | {{title}}</title>
|
||||||
<meta name="description" content="">
|
<meta name="description" content="">
|
||||||
<meta name="viewport" content="width=device-width, user-scalable=no">
|
<meta name="viewport" content="width=device-width, user-scalable=no">
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
{% if g.google_site_verification|length > 0 %}
|
{% if g.google_site_verification|length > 0 %}
|
||||||
<meta name="google-site-verification" content="{{g.google_site_verification}}">
|
<meta name="google-site-verification" content="{{g.google_site_verification}}">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -24,6 +24,20 @@
|
|||||||
{%endfor%}
|
{%endfor%}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="btn-group" role="group" aria-label="Remove from shelves">
|
||||||
|
<button id="remove-from-shelf" type="button" class="btn btn-primary btn-sm dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||||
|
<span class="glyphicon glyphicon-list"></span> {{_('Remove from shelf')}}
|
||||||
|
<span class="caret"></span>
|
||||||
|
</button>
|
||||||
|
<ul id="remove-from-shelves" class="dropdown-menu" aria-labelledby="add-to-shelf">
|
||||||
|
{% for shelf in g.shelves_access %}
|
||||||
|
{% if not shelf.is_public or current_user.role_edit_shelfs() %}
|
||||||
|
<li><a class="postAction" role="button" data-action="{{ url_for('shelf.search_from_shelf', shelf_id=shelf.id) }}"> {{shelf.name}}{% if shelf.is_public == 1 %} {{_('(Public)')}}{% endif %}</a></li>
|
||||||
|
{% endif %}
|
||||||
|
{%endfor%}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
{% if g.google_site_verification|length > 0 %}
|
{% if g.google_site_verification|length > 0 %}
|
||||||
<meta name="google-site-verification" content="{{g.google_site_verification}}">
|
<meta name="google-site-verification" content="{{g.google_site_verification}}">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -121,7 +121,7 @@
|
|||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
<div class="col-xs-12 col-sm-12">
|
<div class="col-xs-12 col-sm-12">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="btn btn-default disabled" id="user_delete_selection" aria-disabled="true">{{_('Remove Selections')}}</div>
|
<div class="btn btn-default disabled mass_selection" id="user_delete_selection" aria-disabled="true">{{_('Clear selections')}}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<table id="user-table" class="table table-no-bordered table-striped"
|
<table id="user-table" class="table table-no-bordered table-striped"
|
||||||
|
|||||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user