diff --git a/.gitignore b/.gitignore index 903cfd36..14da8a03 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ __pycache__/ # Distribution / packaging .Python +.python-version env/ venv/ eggs/ @@ -31,4 +32,4 @@ cps/cache settings.yaml gdrive_credentials client_secrets.json - +gmail.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ce2bd780..c6006ad1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -41,6 +41,6 @@ Open a new GitHub pull request with the patch. Ensure the PR description clearly In case your code enhances features of Calibre-Web: Create your pull request for the development branch if your enhancement consists of more than some lines of code in a local section of Calibre-Webs code. This makes it easier to test it and check all implication before it's made public. -Please check if your code runs on Python 2.7 (still necessary in 2020) and mainly on python 3. If possible and the feature is related to operating system functions, try to check it on Windows and Linux. -Calibre-Web is automatically tested on Linux in combination with python 3.7. The code for testing is in a [separate repo](https://github.com/OzzieIsaacs/calibre-web-test) on Github. It uses unit tests and performs real system tests with selenium; it would be great if you could consider also writing some tests. +Please check if your code runs with python 3, python 2 is no longer supported. If possible and the feature is related to operating system functions, try to check it on Windows and Linux. +Calibre-Web is automatically tested on Linux in combination with python 3.8. The code for testing is in a [separate repo](https://github.com/OzzieIsaacs/calibre-web-test) on Github. It uses unit tests and performs real system tests with selenium; it would be great if you could consider also writing some tests. A static code analysis is done by Codacy, but it's partly broken and doesn't run automatically. You could check your code with ESLint before contributing, a configuration file can be found in the projects root folder. diff --git a/README.md b/README.md index 5a796efb..0fc6447d 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,13 @@ Calibre-Web is a web app providing a clean interface for browsing, reading and downloading eBooks using an existing [Calibre](https://calibre-ebook.com) database. +[![GitHub License](https://img.shields.io/github/license/janeczku/calibre-web?style=flat-square)](https://github.com/janeczku/calibre-web/blob/master/LICENSE) +[![GitHub commit activity](https://img.shields.io/github/commit-activity/w/janeczku/calibre-web?logo=github&style=flat-square&label=commits)]() +[![GitHub all releases](https://img.shields.io/github/downloads/janeczku/calibre-web/total?logo=github&style=flat-square)](https://github.com/janeczku/calibre-web/releases) +[![PyPI](https://img.shields.io/pypi/v/calibreweb?logo=pypi&logoColor=fff&style=flat-square)](https://pypi.org/project/calibreweb/) +[![PyPI - Downloads](https://img.shields.io/pypi/dm/calibreweb?logo=pypi&logoColor=fff&style=flat-square)](https://pypi.org/project/calibreweb/) +[![Discord](https://img.shields.io/discord/838810113564344381?label=Discord&logo=discord&style=flat-square)](https://discord.gg/h2VsJ2NEfB) + *This software is a fork of [library](https://github.com/mutschler/calibreserver) and licensed under the GPL v3 License.* ![Main screen](https://github.com/janeczku/calibre-web/wiki/images/main_screen.png) @@ -12,7 +19,7 @@ Calibre-Web is a web app providing a clean interface for browsing, reading and d - full graphical setup - User management with fine-grained per-user permissions - Admin interface -- User Interface in czech, dutch, english, finnish, french, german, greek, hungarian, italian, japanese, khmer, polish, russian, simplified chinese, spanish, swedish, turkish, ukrainian +- User Interface in brazilian, czech, dutch, english, finnish, french, german, greek, hungarian, italian, japanese, khmer, polish, russian, simplified chinese, spanish, swedish, turkish, ukrainian - OPDS feed for eBook reader apps - Filter and search by titles, authors, tags, series and language - Create a custom book collection (shelves) @@ -32,12 +39,19 @@ Calibre-Web is a web app providing a clean interface for browsing, reading and d ## Quick start +#### Install via pip +1. Install calibre web via pip with the command `pip install calibreweb` (Depending on your OS and or distro the command could also be `pip3`). +2. Optional features can also be installed via pip, please refer to [this page](https://github.com/janeczku/calibre-web/wiki/Dependencies-in-Calibre-Web-Linux-Windows) for details +3. Calibre-Web can be started afterwards by typing `cps` or `python3 -m cps` + +#### Manual installation 1. Install dependencies by running `pip3 install --target vendor -r requirements.txt` (python3.x). Alternativly set up a python virtual environment. 2. Execute the command: `python3 cps.py` (or `nohup python3 cps.py` - recommended if you want to exit the terminal window) -3. Point your browser to `http://localhost:8083` or `http://localhost:8083/opds` for the OPDS catalog -4. Set `Location of Calibre database` to the path of the folder where your Calibre library (metadata.db) lives, push "submit" button\ - Optionally a Google Drive can be used to host the calibre library [-> Using Google Drive integration](https://github.com/janeczku/calibre-web/wiki/Configuration#using-google-drive-integration) -5. Go to Login page + +Point your browser to `http://localhost:8083` or `http://localhost:8083/opds` for the OPDS catalog +Set `Location of Calibre database` to the path of the folder where your Calibre library (metadata.db) lives, push "submit" button\ +Optionally a Google Drive can be used to host the calibre library [-> Using Google Drive integration](https://github.com/janeczku/calibre-web/wiki/Configuration#using-google-drive-integration) +Go to Login page **Default admin login:**\ *Username:* admin\ @@ -48,7 +62,7 @@ Please note that running the above install command can fail on some versions of ## Requirements -python 3.x+ +python 3.5+ Optionally, to enable on-the-fly conversion from one ebook format to another when using the send-to-kindle feature, or during editing of ebooks metadata: @@ -80,7 +94,9 @@ Pre-built Docker images are available in these Docker Hub repositories: + The "path to convertertool" should be set to `/usr/bin/ebook-convert` + The "path to unrar" should be set to `/usr/bin/unrar` -# Wiki +# Contact + +Just reach us out on [Discord](https://discord.gg/h2VsJ2NEfB) For further information, How To's and FAQ please check the [Wiki](https://github.com/janeczku/calibre-web/wiki) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..2f36fac8 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,5 @@ +# Security Policy + +## Reporting a Vulnerability + +Please report security issues to ozzie.fernandez.isaacs@googlemail.com diff --git a/cps.py b/cps.py index 19ca89b8..82137bbc 100755 --- a/cps.py +++ b/cps.py @@ -42,6 +42,7 @@ from cps.admin import admi from cps.gdrive import gdrive from cps.editbooks import editbook from cps.remotelogin import remotelogin +from cps.search_metadata import meta from cps.error_handler import init_errorhandler from cps.schedule import register_jobs @@ -71,7 +72,7 @@ def main(): app.register_blueprint(shelf) app.register_blueprint(admi) app.register_blueprint(remotelogin) - # if config.config_use_google_drive: + app.register_blueprint(meta) app.register_blueprint(gdrive) app.register_blueprint(editbook) if kobo_available: diff --git a/cps/__init__.py b/cps/__init__.py index 30029428..838da5b4 100644 --- a/cps/__init__.py +++ b/cps/__init__.py @@ -37,6 +37,11 @@ from . import config_sql, logger, cache_buster, cli, ub, db from .reverseproxy import ReverseProxied from .server import WebServer +try: + import lxml + lxml_present = True +except ImportError: + lxml_present = False mimetypes.init() mimetypes.add_type('application/xhtml+xml', '.xhtml') @@ -83,11 +88,23 @@ log = logger.create() from . import services -db.CalibreDB.setup_db(config, cli.settingspath) +db.CalibreDB.update_config(config) +db.CalibreDB.setup_db(config.config_calibre_dir, cli.settingspath) + calibre_db = db.CalibreDB() def create_app(): + if sys.version_info < (3, 0): + log.info( + '*** Python2 is EOL since end of 2019, this version of Calibre-Web is no longer supporting Python2, please update your installation to Python3 ***') + print( + '*** Python2 is EOL since end of 2019, this version of Calibre-Web is no longer supporting Python2, please update your installation to Python3 ***') + sys.exit(5) + if not lxml_present: + log.info('*** "lxml" is needed for calibre-web to run. Please install it using pip: "pip install lxml" ***') + print('*** "lxml" is needed for calibre-web to run. Please install it using pip: "pip install lxml" ***') + sys.exit(6) app.wsgi_app = ReverseProxied(app.wsgi_app) # For python2 convert path to unicode if sys.version_info < (3, 0): @@ -97,11 +114,9 @@ def create_app(): if os.environ.get('FLASK_DEBUG'): cache_buster.init_cache_busting(app) + cache_buster.init_cache_busting(app) log.info('Starting Calibre Web...') - if sys.version_info < (3, 0): - log.info('Python2 is EOL since end of 2019, this version of Calibre-Web supporting Python2 please consider upgrading to Python3') - print('Python2 is EOL since end of 2019, this version of Calibre-Web supporting Python2 please consider upgrading to Python3') Principal(app) lm.init_app(app) app.secret_key = os.getenv('SECRET_KEY', config_sql.get_flask_session_key(ub.session)) @@ -126,9 +141,8 @@ def create_app(): def get_locale(): # if a user is logged in, use the locale from the user settings user = getattr(g, 'user', None) - # user = None if user is not None and hasattr(user, "locale"): - if user.nickname != 'Guest': # if the account is the guest account bypass the config lang settings + if user.name != 'Guest': # if the account is the guest account bypass the config lang settings return user.locale preferred = list() @@ -147,6 +161,7 @@ def get_timezone(): user = getattr(g, 'user', None) return user.timezone if user else None + from .updater import Updater updater_thread = Updater() updater_thread.start() diff --git a/cps/about.py b/cps/about.py index 10e68e69..66c0ef40 100644 --- a/cps/about.py +++ b/cps/about.py @@ -54,6 +54,12 @@ try: except ImportError: greenlet_Version = None +try: + from scholarly import scholarly + scholarly_version = _(u'installed') +except ImportError: + scholarly_version = _(u'not installed') + from . import services about = flask.Blueprint('about', __name__) @@ -79,6 +85,7 @@ _VERSIONS = OrderedDict( iso639=isoLanguages.__version__, pytz=pytz.__version__, Unidecode = unidecode_version, + Scholarly = scholarly_version, Flask_SimpleLDAP = u'installed' if bool(services.ldap) else None, python_LDAP = services.ldapVersion if bool(services.ldapVersion) else None, Goodreads = u'installed' if bool(services.goodreads_support) else None, diff --git a/cps/admin.py b/cps/admin.py index 30449d0c..32d66a6f 100644 --- a/cps/admin.py +++ b/cps/admin.py @@ -31,21 +31,23 @@ from datetime import datetime, timedelta from babel import Locale as LC from babel.dates import format_datetime -from flask import Blueprint, flash, redirect, url_for, abort, request, make_response, send_from_directory, g +from flask import Blueprint, flash, redirect, url_for, abort, request, make_response, send_from_directory, g, Response from flask_login import login_required, current_user, logout_user, confirm_login from flask_babel import gettext as _ +from flask import session as flask_session from sqlalchemy import and_ from sqlalchemy.orm.attributes import flag_modified from sqlalchemy.exc import IntegrityError, OperationalError, InvalidRequestError -from sqlalchemy.sql.expression import func, or_ +from sqlalchemy.sql.expression import func, or_, text from . import constants, logger, helper, services, isoLanguages, fs from .cli import filepicker from . import db, calibre_db, ub, web_server, get_locale, config, updater_thread, babel, gdriveutils -from .helper import check_valid_domain, send_test_mail, reset_password, generate_password_hash +from .helper import check_valid_domain, send_test_mail, reset_password, generate_password_hash, check_email, \ + valid_email, check_username from .gdriveutils import is_gdrive_ready, gdrive_support from .render_template import render_title_template, get_sidebar_config -from . import debug_info +from . import debug_info, _BABEL_TRANSLATIONS try: from functools import wraps @@ -58,7 +60,8 @@ feature_support = { 'ldap': bool(services.ldap), 'goodreads': bool(services.goodreads_support), 'kobo': bool(services.kobo), - 'updater': constants.UPDATER_AVAILABLE + 'updater': constants.UPDATER_AVAILABLE, + 'gmail': bool(services.gmail) } try: @@ -95,23 +98,13 @@ def admin_required(f): return inner -def unconfigured(f): - """ - Checks if calibre-web instance is not configured - """ - @wraps(f) - def inner(*args, **kwargs): - if not config.db_configured: - return f(*args, **kwargs) - abort(403) - - return inner - - @admi.before_app_request def before_request(): + # make remember me function work if current_user.is_authenticated: confirm_login() + if not ub.check_user_session(current_user.id, flask_session.get('_id')) and 'opds' not in request.path: + logout_user() g.constants = constants g.user = current_user g.allow_registration = config.config_public_reg @@ -122,10 +115,14 @@ def before_request(): 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() if '/static/' not in request.path and not config.db_configured and \ - request.endpoint not in ('admin.basic_configuration', - 'login', - 'admin.config_pathchooser'): - return redirect(url_for('admin.basic_configuration')) + request.endpoint not in ('admin.ajax_db_config', + 'admin.simulatedbchange', + 'admin.db_configuration', + 'web.login', + 'web.logout', + 'admin.load_dialogtexts', + 'admin.ajax_pathchooser'): + return redirect(url_for('admin.db_configuration')) @admi.route("/admin") @@ -150,7 +147,7 @@ def shutdown(): else: showtext['text'] = _(u'Performing shutdown of server, please close window') # stop gevent/tornado server - web_server.stop(task == 0) + web_server.stop(task==0) return json.dumps(showtext) if task == 2: @@ -209,16 +206,46 @@ def admin(): feature_support=feature_support, kobo_support=kobo_support, title=_(u"Admin page"), page="admin") +@admi.route("/admin/dbconfig", methods=["GET", "POST"]) +@login_required +@admin_required +def db_configuration(): + if request.method == "POST": + return _db_configuration_update_helper() + return _db_configuration_result() -@admi.route("/admin/config", methods=["GET", "POST"]) + +@admi.route("/admin/config", methods=["GET"]) @login_required @admin_required def configuration(): - if request.method == "POST": - return _configuration_update_helper(True) - return _configuration_result() + return render_title_template("config_edit.html", + config=config, + provider=oauthblueprints, + feature_support=feature_support, + title=_(u"Basic Configuration"), page="config") +@admi.route("/admin/ajaxconfig", methods=["POST"]) +@login_required +@admin_required +def ajax_config(): + return _configuration_update_helper() + + +@admi.route("/admin/ajaxdbconfig", methods=["POST"]) +@login_required +@admin_required +def ajax_db_config(): + return _db_configuration_update_helper() + + +@admi.route("/admin/alive", methods=["GET"]) +@login_required +@admin_required +def calibreweb_alive(): + return "", 200 + @admi.route("/admin/viewconfig") @login_required @admin_required @@ -239,68 +266,112 @@ def edit_user_table(): languages = calibre_db.speaking_language() translations = babel.list_translations() + [LC('en')] allUser = ub.session.query(ub.User) + tags = calibre_db.session.query(db.Tags)\ + .join(db.books_tags_link)\ + .join(db.Books)\ + .filter(calibre_db.common_filters()) \ + .group_by(text('books_tags_link.tag'))\ + .order_by(db.Tags.name).all() + if config.config_restricted_column: + custom_values = calibre_db.session.query(db.cc_classes[config.config_restricted_column]).all() + else: + custom_values = [] if not config.config_anonbrowse: allUser = allUser.filter(ub.User.role.op('&')(constants.ROLE_ANONYMOUS) != constants.ROLE_ANONYMOUS) - + kobo_support = feature_support['kobo'] and config.config_kobo_sync return render_title_template("user_table.html", users=allUser.all(), + tags=tags, + custom_values=custom_values, translations=translations, languages=languages, visiblility=visibility, all_roles=constants.ALL_ROLES, + kobo_support=kobo_support, sidebar_settings=constants.sidebar_settings, title=_(u"Edit Users"), page="usertable") + @admi.route("/ajax/listusers") @login_required @admin_required def list_users(): - off = request.args.get("offset") or 0 - limit = request.args.get("limit") or 40 + off = int(request.args.get("offset") or 0) + limit = int(request.args.get("limit") or 10) search = request.args.get("search") + sort = request.args.get("sort", "id") + order = request.args.get("order", "").lower() + state = None + if sort == "state": + state = json.loads(request.args.get("state", "[]")) + + if sort != "state" and order: + order = text(sort + " " + order) + elif not state: + order = ub.User.id.asc() all_user = ub.session.query(ub.User) if not config.config_anonbrowse: all_user = all_user.filter(ub.User.role.op('&')(constants.ROLE_ANONYMOUS) != constants.ROLE_ANONYMOUS) - total_count = all_user.count() + + total_count = filtered_count = all_user.count() + if search: - users = all_user.filter(or_(func.lower(ub.User.nickname).ilike("%" + search + "%"), + all_user = all_user.filter(or_(func.lower(ub.User.name).ilike("%" + search + "%"), func.lower(ub.User.kindle_mail).ilike("%" + search + "%"), - func.lower(ub.User.email).ilike("%" + search + "%")))\ - .offset(off).limit(limit).all() - filtered_count = len(users) + func.lower(ub.User.email).ilike("%" + search + "%"))) + if state: + users = calibre_db.get_checkbox_sorted(all_user.all(), state, off, limit, request.args.get("order", "").lower()) else: - users = all_user.offset(off).limit(limit).all() - filtered_count = total_count + users = all_user.order_by(order).offset(off).limit(limit).all() + if search: + filtered_count = len(users) for user in users: - # set readable locale - #try: - # user.local = LC.parse(user.locale).get_language_name(get_locale()) - #except UnknownLocaleError: - # # This should not happen - # user.local = _(isoLanguages.get(part1=user.locale).name) - # Set default language if user.default_language == "all": - user.default = _("all") + user.default = _("All") else: user.default = LC.parse(user.default_language).get_language_name(get_locale()) table_entries = {'totalNotFiltered': total_count, 'total': filtered_count, "rows": users} - js_list = json.dumps(table_entries, cls=db.AlchemyEncoder) - response = make_response(js_list) response.headers["Content-Type"] = "application/json; charset=utf-8" return response -@admi.route("/ajax/deleteuser") +@admi.route("/ajax/deleteuser", methods=['POST']) @login_required @admin_required def delete_user(): - # ToDo User delete check also not last one - return "" + user_ids = request.form.to_dict(flat=False) + users = None + if "userid[]" in user_ids: + users = ub.session.query(ub.User).filter(ub.User.id.in_(user_ids['userid[]'])).all() + elif "userid" in user_ids: + users = ub.session.query(ub.User).filter(ub.User.id == user_ids['userid'][0]).all() + count = 0 + errors = list() + success = list() + if not users: + log.error("User not found") + return Response(json.dumps({'type': "danger", 'message': _("User not found")}), mimetype='application/json') + for user in users: + try: + message = _delete_user(user) + count += 1 + except Exception as ex: + log.error(ex) + errors.append({'type': "danger", 'message': str(ex)}) + + if count == 1: + log.info("User {} deleted".format(user_ids)) + success = [{'type': "success", 'message': message}] + elif count > 1: + log.info("Users {} deleted".format(user_ids)) + success = [{'type': "success", 'message': _("{} users deleted successfully").format(count)}] + success.extend(errors) + return Response(json.dumps(success), mimetype='application/json') @admi.route("/ajax/getlocale") @login_required @@ -310,7 +381,7 @@ def table_get_locale(): ret = list() current_locale = get_locale() for loc in locale: - ret.append({'value':str(loc),'text':loc.get_language_name(current_locale)}) + ret.append({'value': str(loc), 'text': loc.get_language_name(current_locale)}) return json.dumps(ret) @@ -320,9 +391,9 @@ def table_get_locale(): def table_get_default_lang(): languages = calibre_db.speaking_language() ret = list() - ret.append({'value':'all','text':_('Show All')}) + ret.append({'value': 'all', 'text': _('Show All')}) for lang in languages: - ret.append({'value':lang.lang_code,'text': lang.name}) + ret.append({'value': lang.lang_code, 'text': lang.name}) return json.dumps(ret) @@ -341,51 +412,92 @@ def edit_list_user(param): if "pk[]" in vals: users = all_user.filter(ub.User.id.in_(vals['pk[]'])).all() else: - return "" + return _("Malformed request"), 400 if 'field_index' in vals: vals['field_index'] = vals['field_index'][0] if 'value' in vals: vals['value'] = vals['value'][0] - else: - return "" + elif not ('value[]' in vals): + return _("Malformed request"), 400 for user in users: - if param =='nickname': - if not ub.session.query(ub.User).filter(ub.User.nickname == vals['value']).scalar(): - user.nickname = vals['value'] + try: + if param in ['denied_tags', 'allowed_tags', 'allowed_column_value', 'denied_column_value']: + if 'value[]' in vals: + setattr(user, param, prepare_tags(user, vals['action'][0], param, vals['value[]'])) + else: + setattr(user, param, vals['value'].strip()) else: - log.error(u"This username is already taken") - return _(u"This username is already taken"), 400 - elif param =='email': - existing_email = ub.session.query(ub.User).filter(ub.User.email == vals['value'].lower()).first() - if not existing_email: - user.email = vals['value'] - else: - log.error(u"Found an existing account for this e-mail address.") - return _(u"Found an existing account for this e-mail address."), 400 - elif param =='kindle_mail': - user.kindle_mail = vals['value'] - elif param == 'role': - if vals['value'] == 'true': - user.role |= int(vals['field_index']) - else: - user.role &= ~int(vals['field_index']) - elif param == 'sidebar_view': - if vals['value'] == 'true': - user.sidebar_view |= int(vals['field_index']) - else: - user.sidebar_view &= ~int(vals['field_index']) - elif param == 'denied_tags': - user.denied_tags = vals['value'] - elif param == 'allowed_tags': - user.allowed_tags = vals['value'] - elif param == 'allowed_column_value': - user.allowed_column_value = vals['value'] - elif param == 'denied_column_value': - user.denied_column_value = vals['value'] - elif param == 'locale': - user.locale = vals['value'] - elif param == 'default_language': - user.default_language = vals['value'] + vals['value'] = vals['value'].strip() + if param == 'name': + if user.name == "Guest": + raise Exception(_("Guest Name can't be changed")) + user.name = check_username(vals['value']) + elif param =='email': + user.email = check_email(vals['value']) + elif param =='kobo_only_shelves_sync': + user.kobo_only_shelves_sync = int(vals['value'] == 'true') + elif param == 'kindle_mail': + user.kindle_mail = valid_email(vals['value']) if vals['value'] else "" + elif param.endswith('role'): + value = int(vals['field_index']) + if user.name == "Guest" and value in \ + [constants.ROLE_ADMIN, constants.ROLE_PASSWD, constants.ROLE_EDIT_SHELFS]: + raise Exception(_("Guest can't have this role")) + # check for valid value, last on checks for power of 2 value + if value > 0 and value <= constants.ROLE_VIEWER and (value & value-1 == 0 or value == 1): + if vals['value'] == 'true': + user.role |= value + elif vals['value'] == 'false': + if value == constants.ROLE_ADMIN: + if not ub.session.query(ub.User).\ + filter(ub.User.role.op('&')(constants.ROLE_ADMIN) == constants.ROLE_ADMIN, + ub.User.id != user.id).count(): + return Response( + json.dumps([{'type': "danger", + 'message':_(u"No admin user remaining, can't remove admin role", + nick=user.name)}]), mimetype='application/json') + user.role &= ~value + else: + raise Exception(_("Value has to be true or false")) + else: + raise Exception(_("Invalid role")) + elif param.startswith('sidebar'): + value = int(vals['field_index']) + if user.name == "Guest" and value == constants.SIDEBAR_READ_AND_UNREAD: + raise Exception(_("Guest can't have this view")) + # check for valid value, last on checks for power of 2 value + if value > 0 and value <= constants.SIDEBAR_LIST and (value & value-1 == 0 or value == 1): + if vals['value'] == 'true': + user.sidebar_view |= value + elif vals['value'] == 'false': + user.sidebar_view &= ~value + else: + raise Exception(_("Value has to be true or false")) + else: + raise Exception(_("Invalid view")) + elif param == 'locale': + if user.name == "Guest": + raise Exception(_("Guest's Locale is determined automatically and can't be set")) + if vals['value'] in _BABEL_TRANSLATIONS: + user.locale = vals['value'] + else: + raise Exception(_("No Valid Locale Given")) + elif param == 'default_language': + languages = calibre_db.session.query(db.Languages) \ + .join(db.books_languages_link) \ + .join(db.Books) \ + .filter(calibre_db.common_filters()) \ + .group_by(text('books_languages_link.lang_code')).all() + lang_codes = [lang.lang_code for lang in languages] + ["all"] + if vals['value'] in lang_codes: + user.default_language = vals['value'] + else: + raise Exception(_("No Valid Book Language Given")) + else: + return _("Parameter not found"), 400 + except Exception as ex: + log.debug_or_exception(ex) + return str(ex), 400 ub.session_commit() return "" @@ -394,7 +506,6 @@ def edit_list_user(param): @login_required @admin_required def update_table_settings(): - # ToDo: Save table settings current_user.view_settings['useredit'] = json.loads(request.data) try: try: @@ -403,10 +514,25 @@ def update_table_settings(): pass ub.session.commit() except (InvalidRequestError, OperationalError): - log.error("Invalid request received: %r ", request, ) + log.error("Invalid request received: {}".format(request)) return "Invalid request", 400 return "" +def check_valid_read_column(column): + if column != "0": + if not calibre_db.session.query(db.Custom_Columns).filter(db.Custom_Columns.id == column) \ + .filter(and_(db.Custom_Columns.datatype == 'bool', db.Custom_Columns.mark_for_delete == 0)).all(): + return False + return True + +def check_valid_restricted_column(column): + if column != "0": + if not calibre_db.session.query(db.Custom_Columns).filter(db.Custom_Columns.id == column) \ + .filter(and_(db.Custom_Columns.datatype == 'text', db.Custom_Columns.mark_for_delete == 0)).all(): + return False + return True + + @admi.route("/admin/viewconfig", methods=["POST"]) @login_required @@ -414,20 +540,31 @@ def update_table_settings(): def update_view_configuration(): to_save = request.form.to_dict() - _config_string = lambda x: config.set_from_dictionary(to_save, x, lambda y: y.strip() if y else y) - _config_int = lambda x: config.set_from_dictionary(to_save, x, int) + # _config_string = lambda x: config.set_from_dictionary(to_save, x, lambda y: y.strip() if y else y) + # _config_int = lambda x: config.set_from_dictionary(to_save, x, int) - _config_string("config_calibre_web_title") - _config_string("config_columns_to_ignore") - if _config_string("config_title_regex"): + _config_string(to_save, "config_calibre_web_title") + _config_string(to_save, "config_columns_to_ignore") + if _config_string(to_save, "config_title_regex"): calibre_db.update_title_sort(config) - _config_int("config_read_column") - _config_int("config_theme") - _config_int("config_random_books") - _config_int("config_books_per_page") - _config_int("config_authors_max") - _config_int("config_restricted_column") + if not check_valid_read_column(to_save.get("config_read_column", "0")): + flash(_(u"Invalid Read Column"), category="error") + log.debug("Invalid Read column") + return view_configuration() + _config_int(to_save, "config_read_column") + + if not check_valid_restricted_column(to_save.get("config_restricted_column", "0")): + flash(_(u"Invalid Restricted Column"), category="error") + log.debug("Invalid Restricted Column") + return view_configuration() + _config_int(to_save, "config_restricted_column") + + _config_int(to_save, "config_theme") + _config_int(to_save, "config_random_books") + _config_int(to_save, "config_books_per_page") + _config_int(to_save, "config_authors_max") + config.config_default_role = constants.selected_roles(to_save) config.config_default_role &= ~constants.ROLE_ANONYMOUS @@ -438,15 +575,16 @@ def update_view_configuration(): config.save() flash(_(u"Calibre-Web configuration updated"), category="success") + log.debug("Calibre-Web configuration updated") before_request() return view_configuration() -@admi.route("/ajax/loaddialogtexts/") +@admi.route("/ajax/loaddialogtexts/", methods=['POST']) @login_required def load_dialogtexts(element_id): - texts = {"header": "", "main": ""} + texts = {"header": "", "main": "", "valid": 1} if element_id == "config_delete_kobo_token": texts["main"] = _('Do you really want to delete the Kobo Token?') elif element_id == "btndeletedomain": @@ -461,8 +599,14 @@ def load_dialogtexts(element_id): texts["main"] = _('Are you sure you want to change visible book languages for selected user(s)?') elif element_id == "role": texts["main"] = _('Are you sure you want to change the selected role for the selected user(s)?') + elif element_id == "restrictions": + texts["main"] = _('Are you sure you want to change the selected restrictions for the selected user(s)?') elif element_id == "sidebar_view": texts["main"] = _('Are you sure you want to change the selected visibility restrictions for the selected user(s)?') + elif element_id == "kobo_only_shelves_sync": + texts["main"] = _('Are you sure you want to change shelf sync behavior for the selected user(s)?') + elif element_id == "db_submit": + texts["main"] = _('Are you sure you want to change Calibre library location?') return json.dumps(texts) @@ -549,7 +693,7 @@ def edit_restriction(res_type, user_id): elementlist = usr.list_allowed_tags() elementlist[int(element['id'][1:])] = element['Element'] usr.allowed_tags = ','.join(elementlist) - ub.session_commit("Changed allowed tags of user {} to {}".format(usr.nickname, usr.allowed_tags)) + ub.session_commit("Changed allowed tags of user {} to {}".format(usr.name, usr.allowed_tags)) if res_type == 3: # CColumn per user if isinstance(user_id, int): usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first() @@ -558,7 +702,7 @@ def edit_restriction(res_type, user_id): elementlist = usr.list_allowed_column_values() elementlist[int(element['id'][1:])] = element['Element'] usr.allowed_column_value = ','.join(elementlist) - ub.session_commit("Changed allowed columns of user {} to {}".format(usr.nickname, usr.allowed_column_value)) + ub.session_commit("Changed allowed columns of user {} to {}".format(usr.name, usr.allowed_column_value)) if element['id'].startswith('d'): if res_type == 0: # Tags as template elementlist = config.list_denied_tags() @@ -578,7 +722,7 @@ def edit_restriction(res_type, user_id): elementlist = usr.list_denied_tags() elementlist[int(element['id'][1:])] = element['Element'] usr.denied_tags = ','.join(elementlist) - ub.session_commit("Changed denied tags of user {} to {}".format(usr.nickname, usr.denied_tags)) + ub.session_commit("Changed denied tags of user {} to {}".format(usr.name, usr.denied_tags)) if res_type == 3: # CColumn per user if isinstance(user_id, int): usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first() @@ -587,7 +731,7 @@ def edit_restriction(res_type, user_id): elementlist = usr.list_denied_column_values() elementlist[int(element['id'][1:])] = element['Element'] usr.denied_column_value = ','.join(elementlist) - ub.session_commit("Changed denied columns of user {} to {}".format(usr.nickname, usr.denied_column_value)) + ub.session_commit("Changed denied columns of user {} to {}".format(usr.name, usr.denied_column_value)) return "" @@ -607,6 +751,26 @@ def restriction_deletion(element, list_func): return ','.join(elementlist) +def prepare_tags(user, action, tags_name, id_list): + if "tags" in tags_name: + tags = calibre_db.session.query(db.Tags).filter(db.Tags.id.in_(id_list)).all() + if not tags: + raise Exception(_("Tag not found")) + new_tags_list = [x.name for x in tags] + else: + tags = calibre_db.session.query(db.cc_classes[config.config_restricted_column])\ + .filter(db.cc_classes[config.config_restricted_column].id.in_(id_list)).all() + new_tags_list = [x.value for x in tags] + saved_tags_list = user.__dict__[tags_name].split(",") if len(user.__dict__[tags_name]) else [] + if action == "remove": + saved_tags_list = [x for x in saved_tags_list if x not in new_tags_list] + elif action == "add": + saved_tags_list.extend(x for x in new_tags_list if x not in saved_tags_list) + else: + raise Exception(_("Invalid Action")) + return ",".join(saved_tags_list) + + @admi.route("/ajax/addrestriction/", defaults={"user_id": 0}, methods=['POST']) @admi.route("/ajax/addrestriction//", methods=['POST']) @login_required @@ -634,10 +798,10 @@ def add_restriction(res_type, user_id): usr = current_user if 'submit_allow' in element: usr.allowed_tags = restriction_addition(element, usr.list_allowed_tags) - ub.session_commit("Changed allowed tags of user {} to {}".format(usr.nickname, usr.list_allowed_tags)) + ub.session_commit("Changed allowed tags of user {} to {}".format(usr.name, usr.list_allowed_tags())) elif 'submit_deny' in element: usr.denied_tags = restriction_addition(element, usr.list_denied_tags) - ub.session_commit("Changed denied tags of user {} to {}".format(usr.nickname, usr.list_denied_tags)) + ub.session_commit("Changed denied tags of user {} to {}".format(usr.name, usr.list_denied_tags())) if res_type == 3: # CustomC per user if isinstance(user_id, int): usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first() @@ -645,12 +809,12 @@ def add_restriction(res_type, user_id): usr = current_user if 'submit_allow' in element: usr.allowed_column_value = restriction_addition(element, usr.list_allowed_column_values) - ub.session_commit("Changed allowed columns of user {} to {}".format(usr.nickname, - usr.list_allowed_column_values)) + ub.session_commit("Changed allowed columns of user {} to {}".format(usr.name, + usr.list_allowed_column_values())) elif 'submit_deny' in element: usr.denied_column_value = restriction_addition(element, usr.list_denied_column_values) - ub.session_commit("Changed denied columns of user {} to {}".format(usr.nickname, - usr.list_denied_column_values)) + ub.session_commit("Changed denied columns of user {} to {}".format(usr.name, + usr.list_denied_column_values())) return "" @@ -681,10 +845,10 @@ def delete_restriction(res_type, user_id): usr = current_user if element['id'].startswith('a'): usr.allowed_tags = restriction_deletion(element, usr.list_allowed_tags) - ub.session_commit("Deleted allowed tags of user {}: {}".format(usr.nickname, usr.list_allowed_tags)) + ub.session_commit("Deleted allowed tags of user {}: {}".format(usr.name, usr.list_allowed_tags)) elif element['id'].startswith('d'): usr.denied_tags = restriction_deletion(element, usr.list_denied_tags) - ub.session_commit("Deleted denied tags of user {}: {}".format(usr.nickname, usr.list_allowed_tags)) + ub.session_commit("Deleted denied tags of user {}: {}".format(usr.name, usr.list_allowed_tags)) elif res_type == 3: # Columns per user if isinstance(user_id, int): usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first() @@ -692,12 +856,12 @@ def delete_restriction(res_type, user_id): usr = current_user if element['id'].startswith('a'): usr.allowed_column_value = restriction_deletion(element, usr.list_allowed_column_values) - ub.session_commit("Deleted allowed columns of user {}: {}".format(usr.nickname, + ub.session_commit("Deleted allowed columns of user {}: {}".format(usr.name, usr.list_allowed_column_values)) elif element['id'].startswith('d'): usr.denied_column_value = restriction_deletion(element, usr.list_denied_column_values) - ub.session_commit("Deleted denied columns of user {}: {}".format(usr.nickname, + ub.session_commit("Deleted denied columns of user {}: {}".format(usr.name, usr.list_denied_column_values)) return "" @@ -742,19 +906,11 @@ def list_restriction(res_type, user_id): else: json_dumps = "" js = json.dumps(json_dumps) - response = make_response(js.replace("'", '"')) + response = make_response(js) #.replace("'", '"') response.headers["Content-Type"] = "application/json; charset=utf-8" return response -@admi.route("/basicconfig/pathchooser/") -@unconfigured -def config_pathchooser(): - if filepicker: - return pathchooser() - abort(403) - - @admi.route("/ajax/pathchooser/") @login_required @admin_required @@ -803,7 +959,6 @@ def pathchooser(): folders = [] files = [] - # locale = get_locale() for f in folders: try: data = {"name": f, "fullpath": os.path.join(cwd, f)} @@ -844,15 +999,6 @@ def pathchooser(): return json.dumps(context) -@admi.route("/basicconfig", methods=["GET", "POST"]) -@unconfigured -def basic_configuration(): - logout_user() - if request.method == "POST": - return _configuration_update_helper(configured=filepicker) - return _configuration_result(configured=filepicker) - - def _config_int(to_save, x, func=int): return config.set_from_dictionary(to_save, x, func) @@ -870,24 +1016,31 @@ def _config_string(to_save, x): def _configuration_gdrive_helper(to_save): - if not os.path.isfile(gdriveutils.SETTINGS_YAML): - config.config_use_google_drive = False + gdrive_error = None + if to_save.get("config_use_google_drive"): + gdrive_secrets = {} - gdrive_secrets = {} - gdrive_error = gdriveutils.get_error_text(gdrive_secrets) - if "config_use_google_drive" in to_save and not config.config_use_google_drive and not gdrive_error: - with open(gdriveutils.CLIENT_SECRETS, 'r') as settings: - gdrive_secrets = json.load(settings)['web'] - if not gdrive_secrets: - return _configuration_result(_('client_secrets.json Is Not Configured For Web Application')) - gdriveutils.update_settings( - gdrive_secrets['client_id'], - gdrive_secrets['client_secret'], - gdrive_secrets['redirect_uris'][0] - ) + if not os.path.isfile(gdriveutils.SETTINGS_YAML): + config.config_use_google_drive = False + + if gdrive_support: + gdrive_error = gdriveutils.get_error_text(gdrive_secrets) + if "config_use_google_drive" in to_save and not config.config_use_google_drive and not gdrive_error: + with open(gdriveutils.CLIENT_SECRETS, 'r') as settings: + gdrive_secrets = json.load(settings)['web'] + if not gdrive_secrets: + return _configuration_result(_('client_secrets.json Is Not Configured For Web Application')) + gdriveutils.update_settings( + gdrive_secrets['client_id'], + gdrive_secrets['client_secret'], + gdrive_secrets['redirect_uris'][0] + ) # always show google drive settings, but in case of error deny support - config.config_use_google_drive = (not gdrive_error) and ("config_use_google_drive" in to_save) + new_gdrive_value = (not gdrive_error) and ("config_use_google_drive" in to_save) + if config.config_use_google_drive and not new_gdrive_value: + config.config_google_drive_watch_changes_response = {} + config.config_use_google_drive = new_gdrive_value if _config_string(to_save, "config_google_drive_folder"): gdriveutils.deleteDatabaseOnChange() return gdrive_error @@ -915,23 +1068,23 @@ def _configuration_oauth_helper(to_save): return reboot_required -def _configuration_logfile_helper(to_save, gdrive_error): +def _configuration_logfile_helper(to_save): reboot_required = False reboot_required |= _config_int(to_save, "config_log_level") reboot_required |= _config_string(to_save, "config_logfile") if not logger.is_valid_logfile(config.config_logfile): return reboot_required, \ - _configuration_result(_('Logfile Location is not Valid, Please Enter Correct Path'), gdrive_error) + _configuration_result(_('Logfile Location is not Valid, Please Enter Correct Path')) reboot_required |= _config_checkbox_int(to_save, "config_access_log") reboot_required |= _config_string(to_save, "config_access_logfile") if not logger.is_valid_logfile(config.config_access_logfile): return reboot_required, \ - _configuration_result(_('Access Logfile Location is not Valid, Please Enter Correct Path'), gdrive_error) + _configuration_result(_('Access Logfile Location is not Valid, Please Enter Correct Path')) return reboot_required, None -def _configuration_ldap_helper(to_save, gdrive_error): +def _configuration_ldap_helper(to_save): reboot_required = False reboot_required |= _config_string(to_save, "config_ldap_provider_url") reboot_required |= _config_int(to_save, "config_ldap_port") @@ -948,7 +1101,7 @@ def _configuration_ldap_helper(to_save, gdrive_error): reboot_required |= _config_string(to_save, "config_ldap_cert_path") reboot_required |= _config_string(to_save, "config_ldap_key_path") _config_string(to_save, "config_ldap_group_name") - if "config_ldap_serv_password" in to_save and to_save["config_ldap_serv_password"] != "": + if to_save.get("config_ldap_serv_password", "") != "": reboot_required |= 1 config.set_from_dictionary(to_save, "config_ldap_serv_password", base64.b64encode, encode='UTF-8') config.save() @@ -958,44 +1111,37 @@ def _configuration_ldap_helper(to_save, gdrive_error): or not config.config_ldap_dn \ or not config.config_ldap_user_object: return reboot_required, _configuration_result(_('Please Enter a LDAP Provider, ' - 'Port, DN and User Object Identifier'), gdrive_error) + 'Port, DN and User Object Identifier')) if config.config_ldap_authentication > constants.LDAP_AUTH_ANONYMOUS: if config.config_ldap_authentication > constants.LDAP_AUTH_UNAUTHENTICATE: if not config.config_ldap_serv_username or not bool(config.config_ldap_serv_password): - return reboot_required, _configuration_result('Please Enter a LDAP Service Account and Password', - gdrive_error) + return reboot_required, _configuration_result(_('Please Enter a LDAP Service Account and Password')) else: if not config.config_ldap_serv_username: - return reboot_required, _configuration_result('Please Enter a LDAP Service Account', gdrive_error) + return reboot_required, _configuration_result(_('Please Enter a LDAP Service Account')) if config.config_ldap_group_object_filter: if config.config_ldap_group_object_filter.count("%s") != 1: return reboot_required, \ - _configuration_result(_('LDAP Group Object Filter Needs to Have One "%s" Format Identifier'), - gdrive_error) + _configuration_result(_('LDAP Group Object Filter Needs to Have One "%s" Format Identifier')) if config.config_ldap_group_object_filter.count("(") != config.config_ldap_group_object_filter.count(")"): - return reboot_required, _configuration_result(_('LDAP Group Object Filter Has Unmatched Parenthesis'), - gdrive_error) + return reboot_required, _configuration_result(_('LDAP Group Object Filter Has Unmatched Parenthesis')) if config.config_ldap_user_object.count("%s") != 1: return reboot_required, \ - _configuration_result(_('LDAP User Object Filter needs to Have One "%s" Format Identifier'), - gdrive_error) + _configuration_result(_('LDAP User Object Filter needs to Have One "%s" Format Identifier')) if config.config_ldap_user_object.count("(") != config.config_ldap_user_object.count(")"): - return reboot_required, _configuration_result(_('LDAP User Object Filter Has Unmatched Parenthesis'), - gdrive_error) + return reboot_required, _configuration_result(_('LDAP User Object Filter Has Unmatched Parenthesis')) - if to_save["ldap_import_user_filter"] == '0': + if to_save.get("ldap_import_user_filter") == '0': config.config_ldap_member_user_object = "" else: if config.config_ldap_member_user_object.count("%s") != 1: return reboot_required, \ - _configuration_result(_('LDAP Member User Filter needs to Have One "%s" Format Identifier'), - gdrive_error) + _configuration_result(_('LDAP Member User Filter needs to Have One "%s" Format Identifier')) if config.config_ldap_member_user_object.count("(") != config.config_ldap_member_user_object.count(")"): - return reboot_required, _configuration_result(_('LDAP Member User Filter Has Unmatched Parenthesis'), - gdrive_error) + return reboot_required, _configuration_result(_('LDAP Member User Filter Has Unmatched Parenthesis')) if config.config_ldap_cacert_path or config.config_ldap_cert_path or config.config_ldap_key_path: if not (os.path.isfile(config.config_ldap_cacert_path) and @@ -1003,13 +1149,31 @@ def _configuration_ldap_helper(to_save, gdrive_error): os.path.isfile(config.config_ldap_key_path)): return reboot_required, \ _configuration_result(_('LDAP CACertificate, Certificate or Key Location is not Valid, ' - 'Please Enter Correct Path'), - gdrive_error) + 'Please Enter Correct Path')) return reboot_required, None -def _configuration_update_helper(configured): - reboot_required = False +@admi.route("/ajax/simulatedbchange", methods=['POST']) +@login_required +@admin_required +def simulatedbchange(): + db_change, db_valid = _db_simulate_change() + return Response(json.dumps({"change": db_change, "valid": db_valid}), mimetype='application/json') + + +def _db_simulate_change(): + param = request.form.to_dict() + to_save = {} + to_save['config_calibre_dir'] = re.sub(r'[\\/]metadata\.db$', + '', + param['config_calibre_dir'], + flags=re.IGNORECASE).strip() + db_change = config.config_calibre_dir != to_save["config_calibre_dir"] and config.config_calibre_dir + db_valid = calibre_db.check_valid_db(to_save["config_calibre_dir"], ub.app_DB_path) + return db_change, db_valid + + +def _db_configuration_update_helper(): db_change = False to_save = request.form.to_dict() gdrive_error = None @@ -1019,26 +1183,50 @@ def _configuration_update_helper(configured): to_save['config_calibre_dir'], flags=re.IGNORECASE) try: - db_change |= _config_string(to_save, "config_calibre_dir") + db_change, db_valid = _db_simulate_change() # gdrive_error drive setup gdrive_error = _configuration_gdrive_helper(to_save) + except (OperationalError, InvalidRequestError): + ub.session.rollback() + log.error("Settings DB is not Writeable") + _db_configuration_result(_("Settings DB is not Writeable"), gdrive_error) + try: + metadata_db = os.path.join(to_save['config_calibre_dir'], "metadata.db") + if config.config_use_google_drive and is_gdrive_ready() and not os.path.exists(metadata_db): + gdriveutils.downloadFile(None, "metadata.db", metadata_db) + db_change = True + except Exception as ex: + return _db_configuration_result('{}'.format(ex), gdrive_error) + if db_change or not db_valid or not config.db_configured: + if not calibre_db.setup_db(to_save['config_calibre_dir'], ub.app_DB_path): + return _db_configuration_result(_('DB Location is not Valid, Please Enter Correct Path'), + gdrive_error) + _config_string(to_save, "config_calibre_dir") + calibre_db.update_config(config) + if not os.access(os.path.join(config.config_calibre_dir, "metadata.db"), os.W_OK): + flash(_(u"DB is not Writeable"), category="warning") + # warning = {'type': "warning", 'message': _(u"DB is not Writeable")} + config.save() + return _db_configuration_result(None, gdrive_error) + +def _configuration_update_helper(): + reboot_required = False + to_save = request.form.to_dict() + try: reboot_required |= _config_int(to_save, "config_port") reboot_required |= _config_string(to_save, "config_keyfile") if config.config_keyfile and not os.path.isfile(config.config_keyfile): - return _configuration_result(_('Keyfile Location is not Valid, Please Enter Correct Path'), - gdrive_error, - configured) + return _configuration_result(_('Keyfile Location is not Valid, Please Enter Correct Path')) reboot_required |= _config_string(to_save, "config_certfile") if config.config_certfile and not os.path.isfile(config.config_certfile): - return _configuration_result(_('Certfile Location is not Valid, Please Enter Correct Path'), - gdrive_error, - configured) + return _configuration_result(_('Certfile Location is not Valid, Please Enter Correct Path')) _config_checkbox_int(to_save, "config_uploading") + _config_checkbox_int(to_save, "config_unicode_filename") # Reboot on config_anonbrowse with enabled ldap, as decoraters are changed in this case reboot_required |= (_config_checkbox_int(to_save, "config_anonbrowse") and config.config_login_type == constants.LOGIN_LDAP) @@ -1060,15 +1248,14 @@ def _configuration_update_helper(configured): reboot_required |= _config_int(to_save, "config_login_type") - # LDAP configurator, + # LDAP configurator if config.config_login_type == constants.LOGIN_LDAP: - reboot, message = _configuration_ldap_helper(to_save, gdrive_error) + reboot, message = _configuration_ldap_helper(to_save) if message: return message reboot_required |= reboot # Remote login configuration - _config_checkbox(to_save, "config_remote_login") if not config.config_remote_login: ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.token_type == 0).delete() @@ -1092,7 +1279,7 @@ def _configuration_update_helper(configured): if config.config_login_type == constants.LOGIN_OAUTH: reboot_required |= _configuration_oauth_helper(to_save) - reboot, message = _configuration_logfile_helper(to_save, gdrive_error) + reboot, message = _configuration_logfile_helper(to_save) if message: return message reboot_required |= reboot @@ -1101,70 +1288,61 @@ def _configuration_update_helper(configured): if "config_rarfile_location" in to_save: unrar_status = helper.check_unrar(config.config_rarfile_location) if unrar_status: - return _configuration_result(unrar_status, gdrive_error, configured) + return _configuration_result(unrar_status) except (OperationalError, InvalidRequestError): ub.session.rollback() - _configuration_result(_(u"Settings DB is not Writeable"), gdrive_error, configured) - - try: - metadata_db = os.path.join(config.config_calibre_dir, "metadata.db") - if config.config_use_google_drive and is_gdrive_ready() and not os.path.exists(metadata_db): - gdriveutils.downloadFile(None, "metadata.db", metadata_db) - db_change = True - except Exception as e: - return _configuration_result('%s' % e, gdrive_error, configured) - - if db_change: - if not calibre_db.setup_db(config, ub.app_DB_path): - return _configuration_result(_('DB Location is not Valid, Please Enter Correct Path'), - gdrive_error, - configured) - if not os.access(os.path.join(config.config_calibre_dir, "metadata.db"), os.W_OK): - flash(_(u"DB is not Writeable"), category="warning") + log.error("Settings DB is not Writeable") + _configuration_result(_("Settings DB is not Writeable")) config.save() - flash(_(u"Calibre-Web configuration updated"), category="success") if reboot_required: web_server.stop(True) - return _configuration_result(None, gdrive_error, configured) + return _configuration_result(None, reboot_required) + +def _configuration_result(error_flash=None, reboot=False): + resp = {} + if error_flash: + log.error(error_flash) + config.load() + resp['result'] = [{'type': "danger", 'message': error_flash}] + else: + resp['result'] = [{'type': "success", 'message':_(u"Calibre-Web configuration updated")}] + resp['reboot'] = reboot + resp['config_upload']= config.config_upload_formats + return Response(json.dumps(resp), mimetype='application/json') -def _configuration_result(error_flash=None, gdrive_error=None, configured=True): +def _db_configuration_result(error_flash=None, gdrive_error=None): gdrive_authenticate = not is_gdrive_ready() gdrivefolders = [] - if gdrive_error is None: + if not gdrive_error and config.config_use_google_drive: gdrive_error = gdriveutils.get_error_text() - if gdrive_error: + if gdrive_error and gdrive_support: + log.error(gdrive_error) gdrive_error = _(gdrive_error) + flash(gdrive_error, category="error") else: - # if config.config_use_google_drive and\ if not gdrive_authenticate and gdrive_support: gdrivefolders = gdriveutils.listRootFolders() - - show_back_button = current_user.is_authenticated - show_login_button = config.db_configured and not current_user.is_authenticated if error_flash: + log.error(error_flash) config.load() flash(error_flash, category="error") - show_login_button = False + elif request.method == "POST" and not gdrive_error: + flash(_("Database Settings updated"), category="success") - return render_title_template("config_edit.html", + return render_title_template("config_db.html", config=config, - provider=oauthblueprints, - show_back_button=show_back_button, - show_login_button=show_login_button, show_authenticate_google_drive=gdrive_authenticate, - filepicker=configured, gdriveError=gdrive_error, gdrivefolders=gdrivefolders, feature_support=feature_support, - title=_(u"Basic Configuration"), page="config") + title=_(u"Database Configuration"), page="dbconfig") def _handle_new_user(to_save, content, languages, translations, kobo_support): content.default_language = to_save["default_language"] - # content.mature_content = "Show_mature_content" in to_save content.locale = to_save.get("locale", content.locale) content.sidebar_view = sum(int(key[5:]) for key in to_save if key.startswith('show_')) @@ -1172,28 +1350,21 @@ def _handle_new_user(to_save, content, languages, translations, kobo_support): content.sidebar_view |= constants.DETAIL_RANDOM content.role = constants.selected_roles(to_save) - - if not to_save["nickname"] or not to_save["email"] or not to_save["password"]: - flash(_(u"Please fill out all fields!"), category="error") - return render_title_template("user_edit.html", new_user=1, content=content, translations=translations, - registered_oauth=oauth_check, kobo_support=kobo_support, - title=_(u"Add new user")) content.password = generate_password_hash(to_save["password"]) - existing_user = ub.session.query(ub.User).filter(func.lower(ub.User.nickname) == to_save["nickname"].lower()) \ - .first() - existing_email = ub.session.query(ub.User).filter(ub.User.email == to_save["email"].lower()) \ - .first() - if not existing_user and not existing_email: - content.nickname = to_save["nickname"] - if config.config_public_reg and not check_valid_domain(to_save["email"]): - flash(_(u"E-mail is not from valid domain"), category="error") - return render_title_template("user_edit.html", new_user=1, content=content, translations=translations, - registered_oauth=oauth_check, kobo_support=kobo_support, - title=_(u"Add new user")) - else: - content.email = to_save["email"] - else: - flash(_(u"Found an existing account for this e-mail address or nickname."), category="error") + try: + if not to_save["name"] or not to_save["email"] or not to_save["password"]: + log.info("Missing entries on new user") + raise Exception(_(u"Please fill out all fields!")) + content.email = check_email(to_save["email"]) + # Query User name, if not existing, change + content.name = check_username(to_save["name"]) + if to_save.get("kindle_mail"): + content.kindle_mail = valid_email(to_save["kindle_mail"]) + if config.config_public_reg and not check_valid_domain(content.email): + log.info("E-mail: {} for new user is not from valid domain".format(content.email)) + raise Exception(_(u"E-mail is not from valid domain")) + except Exception as ex: + flash(str(ex), category="error") return render_title_template("user_edit.html", new_user=1, content=content, translations=translations, languages=languages, title=_(u"Add new user"), page="newuser", kobo_support=kobo_support, registered_oauth=oauth_check) @@ -1202,36 +1373,60 @@ def _handle_new_user(to_save, content, languages, translations, kobo_support): content.denied_tags = config.config_denied_tags content.allowed_column_value = config.config_allowed_column_value content.denied_column_value = config.config_denied_column_value + # No default value for kobo sync shelf setting + content.kobo_only_shelves_sync = to_save.get("kobo_only_shelves_sync", 0) == "on" ub.session.add(content) ub.session.commit() - flash(_(u"User '%(user)s' created", user=content.nickname), category="success") + flash(_(u"User '%(user)s' created", user=content.name), category="success") + log.debug("User {} created".format(content.name)) return redirect(url_for('admin.admin')) except IntegrityError: ub.session.rollback() - flash(_(u"Found an existing account for this e-mail address or nickname."), category="error") + log.error("Found an existing account for {} or {}".format(content.name, content.email)) + flash(_("Found an existing account for this e-mail address or name."), category="error") except OperationalError: ub.session.rollback() - flash(_(u"Settings DB is not Writeable"), category="error") + log.error("Settings DB is not Writeable") + flash(_("Settings DB is not Writeable"), category="error") + +def _delete_user(content): + if ub.session.query(ub.User).filter(ub.User.role.op('&')(constants.ROLE_ADMIN) == constants.ROLE_ADMIN, + ub.User.id != content.id).count(): + if content.name != "Guest": + # Delete all books in shelfs belonging to user, all shelfs of user, downloadstat of user, read status + # and user itself + ub.session.query(ub.ReadBook).filter(content.id == ub.ReadBook.user_id).delete() + ub.session.query(ub.Downloads).filter(content.id == ub.Downloads.user_id).delete() + for us in ub.session.query(ub.Shelf).filter(content.id == ub.Shelf.user_id): + ub.session.query(ub.BookShelf).filter(us.id == ub.BookShelf.shelf).delete() + ub.session.query(ub.Shelf).filter(content.id == ub.Shelf.user_id).delete() + ub.session.query(ub.User).filter(ub.User.id == content.id).delete() + ub.session_commit() + log.info(u"User {} deleted".format(content.name)) + return(_(u"User '%(nick)s' deleted", nick=content.name)) + else: + log.warning(_(u"Can't delete Guest User")) + raise Exception(_(u"Can't delete Guest User")) + else: + log.warning(u"No admin user remaining, can't delete user") + raise Exception(_(u"No admin user remaining, can't delete user")) def _handle_edit_user(to_save, content, languages, translations, kobo_support): - if "delete" in to_save: - if ub.session.query(ub.User).filter(ub.User.role.op('&')(constants.ROLE_ADMIN) == constants.ROLE_ADMIN, - ub.User.id != content.id).count(): - ub.session.query(ub.User).filter(ub.User.id == content.id).delete() - ub.session_commit() - flash(_(u"User '%(nick)s' deleted", nick=content.nickname), category="success") - return redirect(url_for('admin.admin')) - else: - flash(_(u"No admin user remaining, can't delete user", nick=content.nickname), category="error") - return redirect(url_for('admin.admin')) + if to_save.get("delete"): + try: + flash(_delete_user(content), category="success") + except Exception as ex: + log.error(ex) + flash(str(ex), category="error") + return redirect(url_for('admin.admin')) else: if not ub.session.query(ub.User).filter(ub.User.role.op('&')(constants.ROLE_ADMIN) == constants.ROLE_ADMIN, ub.User.id != content.id).count() and 'admin_role' not in to_save: - flash(_(u"No admin user remaining, can't remove admin role", nick=content.nickname), category="error") + log.warning("No admin user remaining, can't remove admin role from {}".format(content.name)) + flash(_("No admin user remaining, can't remove admin role"), category="error") return redirect(url_for('admin.admin')) - - if "password" in to_save and to_save["password"]: + if to_save.get("password"): content.password = generate_password_hash(to_save["password"]) anonymous = content.is_anonymous content.role = constants.selected_roles(to_save) @@ -1249,58 +1444,52 @@ def _handle_edit_user(to_save, content, languages, translations, kobo_support): elif value not in val and content.check_visibility(value): content.sidebar_view &= ~value - if "Show_detail_random" in to_save: + if to_save.get("Show_detail_random"): content.sidebar_view |= constants.DETAIL_RANDOM else: content.sidebar_view &= ~constants.DETAIL_RANDOM - if "default_language" in to_save: - content.default_language = to_save["default_language"] - if "locale" in to_save and to_save["locale"]: - content.locale = to_save["locale"] - if to_save["email"] and to_save["email"] != content.email: - existing_email = ub.session.query(ub.User).filter(ub.User.email == to_save["email"].lower()) \ - .first() - if not existing_email: - content.email = to_save["email"] - else: - flash(_(u"Found an existing account for this e-mail address."), category="error") - return render_title_template("user_edit.html", - translations=translations, - languages=languages, - mail_configured=config.get_mail_server_configured(), - kobo_support=kobo_support, - new_user=0, - content=content, - registered_oauth=oauth_check, - title=_(u"Edit User %(nick)s", nick=content.nickname), page="edituser") - if "nickname" in to_save and to_save["nickname"] != content.nickname: - # Query User nickname, if not existing, change - if not ub.session.query(ub.User).filter(ub.User.nickname == to_save["nickname"]).scalar(): - content.nickname = to_save["nickname"] - else: - flash(_(u"This username is already taken"), category="error") - return render_title_template("user_edit.html", - translations=translations, - languages=languages, - mail_configured=config.get_mail_server_configured(), - new_user=0, content=content, - registered_oauth=oauth_check, - kobo_support=kobo_support, - title=_(u"Edit User %(nick)s", nick=content.nickname), - page="edituser") + content.kobo_only_shelves_sync = int(to_save.get("kobo_only_shelves_sync") == "on") or 0 - if "kindle_mail" in to_save and to_save["kindle_mail"] != content.kindle_mail: - content.kindle_mail = to_save["kindle_mail"] + if to_save.get("default_language"): + content.default_language = to_save["default_language"] + if to_save.get("locale"): + content.locale = to_save["locale"] + try: + if to_save.get("email", content.email) != content.email: + content.email = check_email(to_save["email"]) + # Query User name, if not existing, change + if to_save.get("name", content.name) != content.name: + if to_save.get("name") == "Guest": + raise Exception(_("Guest Name can't be changed")) + content.name = check_username(to_save["name"]) + if to_save.get("kindle_mail") != content.kindle_mail: + content.kindle_mail = valid_email(to_save["kindle_mail"]) if to_save["kindle_mail"] else "" + except Exception as ex: + log.error(ex) + flash(str(ex), category="error") + return render_title_template("user_edit.html", + translations=translations, + languages=languages, + mail_configured=config.get_mail_server_configured(), + kobo_support=kobo_support, + new_user=0, + content=content, + registered_oauth=oauth_check, + title=_(u"Edit User %(nick)s", nick=content.name), + page="edituser") try: ub.session_commit() - flash(_(u"User '%(nick)s' updated", nick=content.nickname), category="success") - except IntegrityError: + flash(_(u"User '%(nick)s' updated", nick=content.name), category="success") + except IntegrityError as ex: ub.session.rollback() - flash(_(u"An unknown error occured."), category="error") + log.error("An unknown error occurred while changing user: {}".format(str(ex))) + flash(_(u"An unknown error occurred. Please try again later."), category="error") except OperationalError: ub.session.rollback() - flash(_(u"Settings DB is not Writeable"), category="error") + log.error("Settings DB is not Writeable") + flash(_("Settings DB is not Writeable"), category="error") + return "" @admi.route("/admin/user/new", methods=["GET", "POST"]) @@ -1328,7 +1517,7 @@ def new_user(): def edit_mailsettings(): content = config.get_mail_settings() return render_title_template("email_edit.html", content=content, title=_(u"Edit E-mail Server Settings"), - page="mailset") + page="mailset", feature_support=feature_support) @admi.route("/admin/mailsettings", methods=["POST"]) @@ -1336,28 +1525,44 @@ def edit_mailsettings(): @admin_required def update_mailsettings(): to_save = request.form.to_dict() - # log.debug("update_mailsettings %r", to_save) + _config_int(to_save, "mail_server_type") + if to_save.get("invalidate"): + config.mail_gmail_token = {} + try: + flag_modified(config, "mail_gmail_token") + except AttributeError: + pass + elif to_save.get("gmail"): + try: + config.mail_gmail_token = services.gmail.setup_gmail(config.mail_gmail_token) + flash(_(u"Gmail Account Verification Successful"), category="success") + except Exception as ex: + flash(str(ex), category="error") + log.error(ex) + return edit_mailsettings() - _config_string(to_save, "mail_server") - _config_int(to_save, "mail_port") - _config_int(to_save, "mail_use_ssl") - _config_string(to_save, "mail_login") - _config_string(to_save, "mail_password") - _config_string(to_save, "mail_from") - _config_int(to_save, "mail_size", lambda y: int(y)*1024*1024) + else: + _config_string(to_save, "mail_server") + _config_int(to_save, "mail_port") + _config_int(to_save, "mail_use_ssl") + _config_string(to_save, "mail_login") + _config_string(to_save, "mail_password") + _config_string(to_save, "mail_from") + _config_int(to_save, "mail_size", lambda y: int(y)*1024*1024) try: config.save() except (OperationalError, InvalidRequestError): ub.session.rollback() - flash(_(u"Settings DB is not Writeable"), category="error") + log.error("Settings DB is not Writeable") + flash(_("Settings DB is not Writeable"), category="error") return edit_mailsettings() if to_save.get("test"): if current_user.email: - result = send_test_mail(current_user.email, current_user.nickname) + result = send_test_mail(current_user.email, current_user.name) if result is None: - flash(_(u"Test e-mail successfully send to %(kindlemail)s", kindlemail=current_user.email), - category="success") + flash(_(u"Test e-mail queued for sending to %(email)s, please check Tasks for result", + email=current_user.email), category="info") else: flash(_(u"There was an error sending the Test e-mail: %(res)s", res=result), category="error") else: @@ -1373,7 +1578,7 @@ def update_mailsettings(): @admin_required def edit_user(user_id): content = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first() # type: ub.User - if not content or (not config.config_anonbrowse and content.nickname == "Guest"): + if not content or (not config.config_anonbrowse and content.name == "Guest"): flash(_(u"User not found"), category="error") return redirect(url_for('admin.admin')) languages = calibre_db.speaking_language() @@ -1381,7 +1586,9 @@ def edit_user(user_id): kobo_support = feature_support['kobo'] and config.config_kobo_sync if request.method == "POST": to_save = request.form.to_dict() - _handle_edit_user(to_save, content, languages, translations, kobo_support) + resp = _handle_edit_user(to_save, content, languages, translations, kobo_support) + if resp: + return resp return render_title_template("user_edit.html", translations=translations, languages=languages, @@ -1390,7 +1597,8 @@ def edit_user(user_id): registered_oauth=oauth_check, mail_configured=config.get_mail_server_configured(), kobo_support=kobo_support, - title=_(u"Edit User %(nick)s", nick=content.nickname), page="edituser") + title=_(u"Edit User %(nick)s", nick=content.name), + page="edituser") @admi.route("/admin/resetpassword/") @@ -1495,7 +1703,8 @@ def get_updater_status(): "9": _(u'Update failed:') + u' ' + _(u'Connection error'), "10": _(u'Update failed:') + u' ' + _(u'Timeout while establishing connection'), "11": _(u'Update failed:') + u' ' + _(u'General error'), - "12": _(u'Update failed:') + u' ' + _(u'Update File Could Not be Saved in Temp Dir') + "12": _(u'Update failed:') + u' ' + _(u'Update file could not be saved in temp dir'), + "13": _(u'Update failed:') + u' ' + _(u'Files could not be replaced during update') } status['text'] = text updater_thread.status = 0 @@ -1512,6 +1721,60 @@ def get_updater_status(): return '' +def ldap_import_create_user(user, user_data): + user_login_field = extract_dynamic_field_from_filter(user, config.config_ldap_user_object) + + try: + username = user_data[user_login_field][0].decode('utf-8') + except KeyError as ex: + log.error("Failed to extract LDAP user: %s - %s", user, ex) + message = _(u'Failed to extract at least One LDAP User') + return 0, message + + # check for duplicate username + if ub.session.query(ub.User).filter(func.lower(ub.User.name) == username.lower()).first(): + # if ub.session.query(ub.User).filter(ub.User.name == username).first(): + log.warning("LDAP User %s Already in Database", user_data) + return 0, None + + kindlemail = '' + if 'mail' in user_data: + useremail = user_data['mail'][0].decode('utf-8') + if len(user_data['mail']) > 1: + kindlemail = user_data['mail'][1].decode('utf-8') + + else: + log.debug('No Mail Field Found in LDAP Response') + useremail = username + '@email.com' + + try: + # check for duplicate email + useremail = check_email(useremail) + except Exception as ex: + log.warning("LDAP Email Error: {}, {}".format(user_data, ex)) + return 0, None + content = ub.User() + content.name = username + content.password = '' # dummy password which will be replaced by ldap one + content.email = useremail + content.kindle_mail = kindlemail + content.role = config.config_default_role + content.sidebar_view = config.config_default_show + content.allowed_tags = config.config_allowed_tags + content.denied_tags = config.config_denied_tags + content.allowed_column_value = config.config_allowed_column_value + content.denied_column_value = config.config_denied_column_value + ub.session.add(content) + try: + ub.session.commit() + return 1, None # increase no of users + except Exception as ex: + log.warning("Failed to create LDAP user: %s - %s", user, ex) + ub.session.rollback() + message = _(u'Failed to Create at Least One LDAP User') + return 0, message + + @admi.route('/import_ldap_users') @login_required @admin_required @@ -1539,59 +1802,23 @@ def import_ldap_users(): query_filter = config.config_ldap_user_object try: user_identifier = extract_user_identifier(user, query_filter) - except Exception as e: - log.warning(e) + except Exception as ex: + log.warning(ex) continue else: user_identifier = user query_filter = None try: user_data = services.ldap.get_object_details(user=user_identifier, query_filter=query_filter) - except AttributeError as e: - log.debug_or_exception(e) + except AttributeError as ex: + log.debug_or_exception(ex) continue if user_data: - user_login_field = extract_dynamic_field_from_filter(user, config.config_ldap_user_object) - - username = user_data[user_login_field][0].decode('utf-8') - # check for duplicate username - if ub.session.query(ub.User).filter(func.lower(ub.User.nickname) == username.lower()).first(): - # if ub.session.query(ub.User).filter(ub.User.nickname == username).first(): - log.warning("LDAP User %s Already in Database", user_data) - continue - - kindlemail = '' - if 'mail' in user_data: - useremail = user_data['mail'][0].decode('utf-8') - if len(user_data['mail']) > 1: - kindlemail = user_data['mail'][1].decode('utf-8') - + user_count, message = ldap_import_create_user(user, user_data) + if message: + showtext['text'] = message else: - log.debug('No Mail Field Found in LDAP Response') - useremail = username + '@email.com' - # check for duplicate email - if ub.session.query(ub.User).filter(func.lower(ub.User.email) == useremail.lower()).first(): - log.warning("LDAP Email %s Already in Database", user_data) - continue - content = ub.User() - content.nickname = username - content.password = '' # dummy password which will be replaced by ldap one - content.email = useremail - content.kindle_mail = kindlemail - content.role = config.config_default_role - content.sidebar_view = config.config_default_show - content.allowed_tags = config.config_allowed_tags - content.denied_tags = config.config_denied_tags - content.allowed_column_value = config.config_allowed_column_value - content.denied_column_value = config.config_denied_column_value - ub.session.add(content) - try: - ub.session.commit() - imported += 1 - except Exception as e: - log.warning("Failed to create LDAP user: %s - %s", user, e) - ub.session.rollback() - showtext['text'] = _(u'Failed to Create at Least One LDAP User') + imported += user_count else: log.warning("LDAP User: %s Not Found", user) showtext['text'] = _(u'At Least One LDAP User Not Found in Database') @@ -1601,7 +1828,7 @@ def import_ldap_users(): def extract_user_data_from_field(user, field): - match = re.search(field + r"=([\d\s\w-]+)", user, re.IGNORECASE | re.UNICODE) + match = re.search(field + r"=([\.\d\s\w-]+)", user, re.IGNORECASE | re.UNICODE) if match: return match.group(1) else: diff --git a/cps/cli.py b/cps/cli.py index 07b719d2..3bb08c1f 100644 --- a/cps/cli.py +++ b/cps/cli.py @@ -45,7 +45,7 @@ parser.add_argument('-v', '--version', action='version', help='Shows version num version=version_info()) parser.add_argument('-i', metavar='ip-address', help='Server IP-Address to listen') parser.add_argument('-s', metavar='user:pass', help='Sets specific username to new password') -parser.add_argument('-f', action='store_true', help='Enables filepicker in unconfigured mode') +parser.add_argument('-f', action='store_true', help='Flag is depreciated and will be removed in next version') args = parser.parse_args() if sys.version_info < (3, 0): @@ -71,7 +71,7 @@ if args.c: if os.path.isfile(args.c): certfilepath = args.c else: - print("Certfilepath is invalid. Exiting...") + print("Certfile path is invalid. Exiting...") sys.exit(1) if args.c == "": @@ -81,7 +81,7 @@ if args.k: if os.path.isfile(args.k): keyfilepath = args.k else: - print("Keyfilepath is invalid. Exiting...") + print("Keyfile path is invalid. Exiting...") sys.exit(1) if (args.k and not args.c) or (not args.k and args.c): @@ -91,29 +91,29 @@ if (args.k and not args.c) or (not args.k and args.c): if args.k == "": keyfilepath = "" -# handle and check ipadress argument -ipadress = args.i or None -if ipadress: +# handle and check ip address argument +ip_address = args.i or None +if ip_address: try: # try to parse the given ip address with socket if hasattr(socket, 'inet_pton'): - if ':' in ipadress: - socket.inet_pton(socket.AF_INET6, ipadress) + if ':' in ip_address: + socket.inet_pton(socket.AF_INET6, ip_address) else: - socket.inet_pton(socket.AF_INET, ipadress) + socket.inet_pton(socket.AF_INET, ip_address) else: # on windows python < 3.4, inet_pton is not available # inet_atom only handles IPv4 addresses - socket.inet_aton(ipadress) + socket.inet_aton(ip_address) except socket.error as err: - print(ipadress, ':', err) + print(ip_address, ':', err) sys.exit(1) # handle and check user password argument user_credentials = args.s or None if user_credentials and ":" not in user_credentials: - print("No valid username:password format") + print("No valid 'username:password' format") sys.exit(3) -# Handles enableing of filepicker -filepicker = args.f or None +if args.f: + print("Warning: -f flag is depreciated and will be removed in next version") diff --git a/cps/comic.py b/cps/comic.py index c1f1fd63..462c11f0 100644 --- a/cps/comic.py +++ b/cps/comic.py @@ -105,8 +105,8 @@ def _extract_Cover_from_archive(original_file_extension, tmp_file_name, rarExecu if extension in COVER_EXTENSIONS: cover_data = cf.read(name) break - except Exception as e: - log.debug('Rarfile failed with error: %s', e) + except Exception as ex: + log.debug('Rarfile failed with error: %s', ex) return cover_data diff --git a/cps/config_sql.py b/cps/config_sql.py index 78e6c9c9..88107f9b 100644 --- a/cps/config_sql.py +++ b/cps/config_sql.py @@ -20,16 +20,18 @@ from __future__ import division, print_function, unicode_literals import os import sys +import json -from sqlalchemy import exc, Column, String, Integer, SmallInteger, Boolean, BLOB, JSON +from sqlalchemy import Column, String, Integer, SmallInteger, Boolean, BLOB, JSON from sqlalchemy.exc import OperationalError +from sqlalchemy.sql.expression import text try: # Compatibility with sqlalchemy 2.0 from sqlalchemy.orm import declarative_base except ImportError: from sqlalchemy.ext.declarative import declarative_base -from . import constants, cli, logger, ub +from . import constants, cli, logger log = logger.create() @@ -39,7 +41,7 @@ class _Flask_Settings(_Base): __tablename__ = 'flask_settings' id = Column(Integer, primary_key=True) - flask_session_key = Column(BLOB, default="") + flask_session_key = Column(BLOB, default=b"") def __init__(self, key): self.flask_session_key = key @@ -58,6 +60,8 @@ class _Settings(_Base): mail_password = Column(String, default='mypassword') mail_from = Column(String, default='automailer ') mail_size = Column(Integer, default=25*1024*1024) + mail_server_type = Column(SmallInteger, default=0) + mail_gmail_token = Column(JSON, default={}) config_calibre_dir = Column(String) config_port = Column(Integer, default=constants.DEFAULT_PORT) @@ -129,6 +133,7 @@ class _Settings(_Base): config_calibre = Column(String) config_rarfile_location = Column(String, default=None) config_upload_formats = Column(String, default=','.join(constants.EXTENSIONS_UPLOAD)) + config_unicode_filename =Column(Boolean, default=False) config_updatechannel = Column(Integer, default=constants.UPDATE_STABLE) @@ -188,7 +193,7 @@ class _ConfigSQL(object): @staticmethod def get_config_ipaddress(): - return cli.ipadress or "" + return cli.ip_address or "" def _has_role(self, role_flag): return constants.has_flag(self.config_default_role, role_flag) @@ -246,18 +251,18 @@ class _ConfigSQL(object): return {k:v for k, v in self.__dict__.items() if k.startswith('mail_')} def get_mail_server_configured(self): - return not bool(self.mail_server == constants.DEFAULT_MAIL_SERVER) + return bool((self.mail_server != constants.DEFAULT_MAIL_SERVER and self.mail_server_type == 0) + or (self.mail_gmail_token != {} and self.mail_server_type == 1)) def set_from_dictionary(self, dictionary, field, convertor=None, default=None, encode=None): - '''Possibly updates a field of this object. + """Possibly updates a field of this object. The new value, if present, is grabbed from the given dictionary, and optionally passed through a convertor. :returns: `True` if the field has changed value - ''' + """ new_value = dictionary.get(field, default) if new_value is None: - # log.debug("_ConfigSQL set_from_dictionary field '%s' not found", field) return False if field not in self.__dict__: @@ -274,7 +279,6 @@ class _ConfigSQL(object): if current_value == new_value: return False - # log.debug("_ConfigSQL set_from_dictionary '%s' = %r (was %r)", field, new_value, current_value) setattr(self, field, new_value) return True @@ -305,8 +309,11 @@ class _ConfigSQL(object): have_metadata_db = os.path.isfile(db_file) self.db_configured = have_metadata_db constants.EXTENSIONS_UPLOAD = [x.lstrip().rstrip().lower() for x in self.config_upload_formats.split(',')] - # pylint: disable=access-member-before-definition - logfile = logger.setup(self.config_logfile, self.config_log_level) + if os.environ.get('FLASK_DEBUG'): + logfile = logger.setup(logger.LOG_TO_STDOUT, logger.logging.DEBUG) + else: + # pylint: disable=access-member-before-definition + logfile = logger.setup(self.config_logfile, self.config_log_level) if logfile != self.config_logfile: log.warning("Log path %s not valid, falling back to default", self.config_logfile) self.config_logfile = logfile @@ -341,7 +348,7 @@ class _ConfigSQL(object): log.error(error) log.warning("invalidating configuration") self.db_configured = False - self.config_calibre_dir = None + # self.config_calibre_dir = None self.save() @@ -352,7 +359,7 @@ def _migrate_table(session, orm_class): if column_name[0] != '_': try: session.query(column).first() - except exc.OperationalError as err: + except OperationalError as err: log.debug("%s: %s", column_name, err.args[0]) if column.default is not None: if sys.version_info < (3, 0): @@ -362,16 +369,23 @@ def _migrate_table(session, orm_class): column_default = "" else: if isinstance(column.default.arg, bool): - column_default = ("DEFAULT %r" % int(column.default.arg)) + column_default = "DEFAULT {}".format(int(column.default.arg)) else: - column_default = ("DEFAULT %r" % column.default.arg) - alter_table = "ALTER TABLE %s ADD COLUMN `%s` %s %s" % (orm_class.__tablename__, + column_default = "DEFAULT `{}`".format(column.default.arg) + if isinstance(column.type, JSON): + column_type = "JSON" + else: + column_type = column.type + alter_table = text("ALTER TABLE %s ADD COLUMN `%s` %s %s" % (orm_class.__tablename__, column_name, - column.type, - column_default) + column_type, + column_default)) log.debug(alter_table) session.execute(alter_table) changed = True + except json.decoder.JSONDecodeError as e: + log.error("Database corrupt column: {}".format(column_name)) + log.debug(e) if changed: try: @@ -430,12 +444,12 @@ def load_configuration(session): session.commit() conf = _ConfigSQL(session) # Migrate from global restrictions to user based restrictions - if bool(conf.config_default_show & constants.MATURE_CONTENT) and conf.config_denied_tags == "": - conf.config_denied_tags = conf.config_mature_content_tags - conf.save() - session.query(ub.User).filter(ub.User.mature_content != True). \ - update({"denied_tags": conf.config_mature_content_tags}, synchronize_session=False) - session.commit() + #if bool(conf.config_default_show & constants.MATURE_CONTENT) and conf.config_denied_tags == "": + # conf.config_denied_tags = conf.config_mature_content_tags + # conf.save() + # session.query(ub.User).filter(ub.User.mature_content != True). \ + # update({"denied_tags": conf.config_mature_content_tags}, synchronize_session=False) + # session.commit() return conf def get_flask_session_key(session): diff --git a/cps/constants.py b/cps/constants.py index 0eb94709..1e44796a 100644 --- a/cps/constants.py +++ b/cps/constants.py @@ -20,6 +20,9 @@ from __future__ import division, print_function, unicode_literals import sys import os from collections import namedtuple +from sqlalchemy import __version__ as sql_version + +sqlalchemy_version2 = ([int(x) for x in sql_version.split('.')] >= [2,0,0]) # if installed via pip this variable is set to true (empty file with name .HOMEDIR present) HOME_CONFIG = os.path.isfile(os.path.join(os.path.dirname(os.path.abspath(__file__)), '.HOMEDIR')) @@ -155,7 +158,7 @@ def selected_roles(dictionary): BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, description, tags, series, ' 'series_id, languages, publisher') -STABLE_VERSION = {'version': '0.6.12 Beta'} +STABLE_VERSION = {'version': '0.6.13 Beta'} NIGHTLY_VERSION = {} NIGHTLY_VERSION[0] = '$Format:%H$' diff --git a/cps/converter.py b/cps/converter.py index 2ff73666..6b0f22e4 100644 --- a/cps/converter.py +++ b/cps/converter.py @@ -39,9 +39,9 @@ def _get_command_version(path, pattern, argument=None): if argument: command.append(argument) try: - for line in process_wait(command): - if re.search(pattern, line): - return line + match = process_wait(command, pattern=pattern) + if isinstance(match, re.Match): + return match.string except Exception as ex: log.warning("%s: %s", path, ex) return _EXECUTION_ERROR diff --git a/cps/db.py b/cps/db.py index 883eec05..296db7a4 100644 --- a/cps/db.py +++ b/cps/db.py @@ -31,6 +31,7 @@ from sqlalchemy import String, Integer, Boolean, TIMESTAMP, Float from sqlalchemy.orm import relationship, sessionmaker, scoped_session from sqlalchemy.orm.collections import InstrumentedList from sqlalchemy.ext.declarative import DeclarativeMeta +from sqlalchemy.exc import OperationalError try: # Compatibility with sqlalchemy 2.0 from sqlalchemy.orm import declarative_base @@ -43,6 +44,7 @@ from flask_login import current_user from babel import Locale as LC from babel.core import UnknownLocaleError from flask_babel import gettext as _ +from flask import flash from . import logger, ub, isoLanguages from .pagination import Pagination @@ -57,7 +59,7 @@ except ImportError: log = logger.create() -cc_exceptions = ['datetime', 'comments', 'composite', 'series'] +cc_exceptions = ['composite', 'series'] cc_classes = {} Base = declarative_base() @@ -120,6 +122,8 @@ class Identifiers(Base): return u"Douban" elif format_type == "goodreads": return u"Goodreads" + elif format_type == "babelio": + return u"Babelio" elif format_type == "google": return u"Google Books" elif format_type == "kobo": @@ -147,6 +151,8 @@ class Identifiers(Base): return u"https://dx.doi.org/{0}".format(self.val) elif format_type == "goodreads": return u"https://www.goodreads.com/book/show/{0}".format(self.val) + elif format_type == "babelio": + return u"https://www.babelio.com/livres/titre/{0}".format(self.val) elif format_type == "douban": return u"https://book.douban.com/subject/{0}".format(self.val) elif format_type == "google": @@ -331,7 +337,6 @@ class Books(Base): has_cover = Column(Integer, default=0) uuid = Column(String) isbn = Column(String(collation='NOCASE'), default="") - # Iccn = Column(String(collation='NOCASE'), default="") flags = Column(Integer, nullable=False, default=1) authors = relationship('Authors', secondary=books_authors_link, backref='books') @@ -393,7 +398,7 @@ class AlchemyEncoder(json.JSONEncoder): if isinstance(o.__class__, DeclarativeMeta): # an SQLAlchemy class fields = {} - for field in [x for x in dir(o) if not x.startswith('_') and x != 'metadata']: + for field in [x for x in dir(o) if not x.startswith('_') and x != 'metadata' and x!="password"]: if field == 'books': continue data = o.__getattribute__(field) @@ -442,26 +447,121 @@ class CalibreDB(): self.instances.add(self) - def initSession(self, expire_on_commit=True): self.session = self.session_factory() self.session.expire_on_commit = expire_on_commit self.update_title_sort(self.config) @classmethod - def setup_db(cls, config, app_db_path): + def setup_db_cc_classes(self, cc): + cc_ids = [] + books_custom_column_links = {} + for row in cc: + if row.datatype not in cc_exceptions: + if row.datatype == 'series': + dicttable = {'__tablename__': 'books_custom_column_' + str(row.id) + '_link', + 'id': Column(Integer, primary_key=True), + 'book': Column(Integer, ForeignKey('books.id'), + primary_key=True), + 'map_value': Column('value', Integer, + ForeignKey('custom_column_' + + str(row.id) + '.id'), + primary_key=True), + 'extra': Column(Float), + 'asoc': relationship('custom_column_' + str(row.id), uselist=False), + 'value': association_proxy('asoc', 'value') + } + books_custom_column_links[row.id] = type(str('books_custom_column_' + str(row.id) + '_link'), + (Base,), dicttable) + if row.datatype in ['rating', 'text', 'enumeration']: + books_custom_column_links[row.id] = Table('books_custom_column_' + str(row.id) + '_link', + Base.metadata, + Column('book', Integer, ForeignKey('books.id'), + primary_key=True), + Column('value', Integer, + ForeignKey('custom_column_' + + str(row.id) + '.id'), + primary_key=True) + ) + cc_ids.append([row.id, row.datatype]) + + ccdict = {'__tablename__': 'custom_column_' + str(row.id), + 'id': Column(Integer, primary_key=True)} + if row.datatype == 'float': + ccdict['value'] = Column(Float) + elif row.datatype == 'int': + ccdict['value'] = Column(Integer) + elif row.datatype == 'datetime': + ccdict['value'] = Column(TIMESTAMP) + elif row.datatype == 'bool': + ccdict['value'] = Column(Boolean) + else: + ccdict['value'] = Column(String) + if row.datatype in ['float', 'int', 'bool', 'datetime', 'comments']: + ccdict['book'] = Column(Integer, ForeignKey('books.id')) + cc_classes[row.id] = type(str('custom_column_' + str(row.id)), (Base,), ccdict) + + for cc_id in cc_ids: + if cc_id[1] in ['bool', 'int', 'float', 'datetime', 'comments']: + setattr(Books, + 'custom_column_' + str(cc_id[0]), + relationship(cc_classes[cc_id[0]], + primaryjoin=( + Books.id == cc_classes[cc_id[0]].book), + backref='books')) + elif cc_id[1] == 'series': + setattr(Books, + 'custom_column_' + str(cc_id[0]), + relationship(books_custom_column_links[cc_id[0]], + backref='books')) + else: + setattr(Books, + 'custom_column_' + str(cc_id[0]), + relationship(cc_classes[cc_id[0]], + secondary=books_custom_column_links[cc_id[0]], + backref='books')) + + return cc_classes + + @classmethod + def check_valid_db(cls, config_calibre_dir, app_db_path): + if not config_calibre_dir: + return False + dbpath = os.path.join(config_calibre_dir, "metadata.db") + if not os.path.exists(dbpath): + return False + try: + check_engine = create_engine('sqlite://', + echo=False, + isolation_level="SERIALIZABLE", + connect_args={'check_same_thread': False}, + poolclass=StaticPool) + with check_engine.begin() as connection: + connection.execute(text("attach database '{}' as calibre;".format(dbpath))) + connection.execute(text("attach database '{}' as app_settings;".format(app_db_path))) + check_engine.connect() + except Exception: + return False + return True + + @classmethod + def update_config(cls, config): cls.config = config + + @classmethod + def setup_db(cls, config_calibre_dir, app_db_path): + # cls.config = config cls.dispose() # toDo: if db changed -> delete shelfs, delete download books, delete read boks, kobo sync?? - if not config.config_calibre_dir: - config.invalidate() + if not config_calibre_dir: + cls.config.invalidate() return False - dbpath = os.path.join(config.config_calibre_dir, "metadata.db") + dbpath = os.path.join(config_calibre_dir, "metadata.db") if not os.path.exists(dbpath): - config.invalidate() + cls.config.invalidate() return False try: @@ -476,79 +576,18 @@ class CalibreDB(): conn = cls.engine.connect() # conn.text_factory = lambda b: b.decode(errors = 'ignore') possible fix for #1302 - except Exception as e: - config.invalidate(e) + except Exception as ex: + cls.config.invalidate(ex) return False - config.db_configured = True + cls.config.db_configured = True if not cc_classes: - cc = conn.execute(text("SELECT id, datatype FROM custom_columns")) - - cc_ids = [] - books_custom_column_links = {} - for row in cc: - if row.datatype not in cc_exceptions: - if row.datatype == 'series': - dicttable = {'__tablename__': 'books_custom_column_' + str(row.id) + '_link', - 'id': Column(Integer, primary_key=True), - 'book': Column(Integer, ForeignKey('books.id'), - primary_key=True), - 'map_value': Column('value', Integer, - ForeignKey('custom_column_' + - str(row.id) + '.id'), - primary_key=True), - 'extra': Column(Float), - 'asoc': relationship('custom_column_' + str(row.id), uselist=False), - 'value': association_proxy('asoc', 'value') - } - books_custom_column_links[row.id] = type(str('books_custom_column_' + str(row.id) + '_link'), - (Base,), dicttable) - else: - books_custom_column_links[row.id] = Table('books_custom_column_' + str(row.id) + '_link', - Base.metadata, - Column('book', Integer, ForeignKey('books.id'), - primary_key=True), - Column('value', Integer, - ForeignKey('custom_column_' + - str(row.id) + '.id'), - primary_key=True) - ) - cc_ids.append([row.id, row.datatype]) - - ccdict = {'__tablename__': 'custom_column_' + str(row.id), - 'id': Column(Integer, primary_key=True)} - if row.datatype == 'float': - ccdict['value'] = Column(Float) - elif row.datatype == 'int': - ccdict['value'] = Column(Integer) - elif row.datatype == 'bool': - ccdict['value'] = Column(Boolean) - else: - ccdict['value'] = Column(String) - if row.datatype in ['float', 'int', 'bool']: - ccdict['book'] = Column(Integer, ForeignKey('books.id')) - cc_classes[row.id] = type(str('custom_column_' + str(row.id)), (Base,), ccdict) - - for cc_id in cc_ids: - if (cc_id[1] == 'bool') or (cc_id[1] == 'int') or (cc_id[1] == 'float'): - setattr(Books, - 'custom_column_' + str(cc_id[0]), - relationship(cc_classes[cc_id[0]], - primaryjoin=( - Books.id == cc_classes[cc_id[0]].book), - backref='books')) - elif (cc_id[1] == 'series'): - setattr(Books, - 'custom_column_' + str(cc_id[0]), - relationship(books_custom_column_links[cc_id[0]], - backref='books')) - else: - setattr(Books, - 'custom_column_' + str(cc_id[0]), - relationship(cc_classes[cc_id[0]], - secondary=books_custom_column_links[cc_id[0]], - backref='books')) + try: + cc = conn.execute(text("SELECT id, datatype FROM custom_columns")) + cls.setup_db_cc_classes(cc) + except OperationalError as e: + log.debug_or_exception(e) cls.session_factory = scoped_session(sessionmaker(autocommit=False, autoflush=True, @@ -595,20 +634,46 @@ class CalibreDB(): neg_content_tags_filter = false() if negtags_list == [''] else Books.tags.any(Tags.name.in_(negtags_list)) pos_content_tags_filter = true() if postags_list == [''] else Books.tags.any(Tags.name.in_(postags_list)) if self.config.config_restricted_column: - pos_cc_list = current_user.allowed_column_value.split(',') - pos_content_cc_filter = true() if pos_cc_list == [''] else \ - getattr(Books, 'custom_column_' + str(self.config.config_restricted_column)). \ - any(cc_classes[self.config.config_restricted_column].value.in_(pos_cc_list)) - neg_cc_list = current_user.denied_column_value.split(',') - neg_content_cc_filter = false() if neg_cc_list == [''] else \ - getattr(Books, 'custom_column_' + str(self.config.config_restricted_column)). \ - any(cc_classes[self.config.config_restricted_column].value.in_(neg_cc_list)) + try: + pos_cc_list = current_user.allowed_column_value.split(',') + pos_content_cc_filter = true() if pos_cc_list == [''] else \ + getattr(Books, 'custom_column_' + str(self.config.config_restricted_column)). \ + any(cc_classes[self.config.config_restricted_column].value.in_(pos_cc_list)) + neg_cc_list = current_user.denied_column_value.split(',') + neg_content_cc_filter = false() if neg_cc_list == [''] else \ + getattr(Books, 'custom_column_' + str(self.config.config_restricted_column)). \ + any(cc_classes[self.config.config_restricted_column].value.in_(neg_cc_list)) + except (KeyError, AttributeError): + pos_content_cc_filter = false() + neg_content_cc_filter = true() + log.error(u"Custom Column No.%d is not existing in calibre database", + self.config.config_restricted_column) + flash(_("Custom Column No.%(column)d is not existing in calibre database", + column=self.config.config_restricted_column), + category="error") + else: pos_content_cc_filter = true() neg_content_cc_filter = false() return and_(lang_filter, pos_content_tags_filter, ~neg_content_tags_filter, pos_content_cc_filter, ~neg_content_cc_filter, archived_filter) + @staticmethod + def get_checkbox_sorted(inputlist, state, offset, limit, order): + outcome = list() + elementlist = {ele.id: ele for ele in inputlist} + for entry in state: + try: + outcome.append(elementlist[entry]) + except KeyError: + pass + del elementlist[entry] + for entry in elementlist: + outcome.append(elementlist[entry]) + if order == "asc": + outcome.reverse() + return outcome[offset:offset + limit] + # Fill indexpage with all requested data from database def fill_indexpage(self, page, pagesize, database, db_filter, order, *join): return self.fill_indexpage_with_archived_books(page, pagesize, database, db_filter, order, False, *join) @@ -626,10 +691,18 @@ class CalibreDB(): randm = false() off = int(int(pagesize) * (page - 1)) query = self.session.query(database) + 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) == 5: + query = query.outerjoin(join[0], join[1]).outerjoin(join[2]).outerjoin(join[3], join[4]) + if len(join) == 4: + query = query.outerjoin(join[0], join[1]).outerjoin(join[2]).outerjoin(join[3]) if len(join) == 3: - query = query.join(join[0], join[1]).join(join[2], isouter=True) + query = query.outerjoin(join[0], join[1]).outerjoin(join[2]) elif len(join) == 2: - query = query.join(join[0], join[1], isouter=True) + query = query.outerjoin(join[0], join[1]) + elif len(join) == 1: + query = query.outerjoin(join[0]) query = query.filter(db_filter)\ .filter(self.common_filters(allow_show_archived)) entries = list() @@ -638,8 +711,8 @@ class CalibreDB(): pagination = Pagination(page, pagesize, len(query.all())) entries = query.order_by(*order).offset(off).limit(pagesize).all() - except Exception as e: - log.debug_or_exception(e) + except Exception as ex: + log.debug_or_exception(ex) #for book in entries: # book = self.order_authors(book) return entries, randm, pagination @@ -681,23 +754,35 @@ class CalibreDB(): return self.session.query(Books) \ .filter(and_(Books.authors.any(and_(*q)), func.lower(Books.title).ilike("%" + title + "%"))).first() - # read search results from calibre-database and return it (function is used for feed and simple search - def get_search_results(self, term, offset=None, order=None, limit=None): - order = order or [Books.sort] - pagination = None + def search_query(self, term, *join): term.strip().lower() self.session.connection().connection.connection.create_function("lower", 1, lcase) q = list() authorterms = re.split("[, ]+", term) for authorterm in authorterms: q.append(Books.authors.any(func.lower(Authors.name).ilike("%" + authorterm + "%"))) - result = self.session.query(Books).filter(self.common_filters(True)).filter( + query = self.session.query(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]) + return query.filter(self.common_filters(True)).filter( or_(Books.tags.any(func.lower(Tags.name).ilike("%" + term + "%")), Books.series.any(func.lower(Series.name).ilike("%" + term + "%")), Books.authors.any(and_(*q)), Books.publishers.any(func.lower(Publishers.name).ilike("%" + term + "%")), func.lower(Books.title).ilike("%" + term + "%") - )).order_by(*order).all() + )) + + # read search results from calibre-database and return it (function is used for feed and simple search + def get_search_results(self, term, offset=None, order=None, limit=None, *join): + order = order or [Books.sort] + pagination = None + result = self.search_query(term, *join).order_by(*order).all() result_count = len(result) if offset != None and limit != None: offset = int(offset) @@ -777,13 +862,14 @@ class CalibreDB(): def reconnect_db(self, config, app_db_path): self.dispose() self.engine.dispose() - self.setup_db(config, app_db_path) + self.setup_db(config.config_calibre_dir, app_db_path) + self.update_config(config) def lcase(s): try: return unidecode.unidecode(s.lower()) - except Exception as e: + except Exception as ex: log = logger.create() - log.debug_or_exception(e) + log.debug_or_exception(ex) return s.lower() diff --git a/cps/debug_info.py b/cps/debug_info.py index 8f0cdeee..dd5e858e 100644 --- a/cps/debug_info.py +++ b/cps/debug_info.py @@ -22,14 +22,10 @@ import glob import zipfile import json from io import BytesIO -try: - from StringIO import StringIO -except ImportError: - from io import StringIO import os -from flask import send_file +from flask import send_file, __version__ from . import logger, config from .about import collect_stats @@ -38,14 +34,20 @@ log = logger.create() def assemble_logfiles(file_name): log_list = sorted(glob.glob(file_name + '*'), reverse=True) - wfd = StringIO() + wfd = BytesIO() for f in log_list: - with open(f, 'r') as fd: + with open(f, 'rb') as fd: shutil.copyfileobj(fd, wfd) wfd.seek(0) - return send_file(wfd, - as_attachment=True, - attachment_filename=os.path.basename(file_name)) + if int(__version__.split('.')[0]) < 2: + return send_file(wfd, + as_attachment=True, + attachment_filename=os.path.basename(file_name)) + else: + return send_file(wfd, + as_attachment=True, + download_name=os.path.basename(file_name)) + def send_debug(): file_list = glob.glob(logger.get_logfile(config.config_logfile) + '*') @@ -60,6 +62,11 @@ def send_debug(): for fp in file_list: zf.write(fp, os.path.basename(fp)) memory_zip.seek(0) - return send_file(memory_zip, - as_attachment=True, - attachment_filename="Calibre-Web-debug-pack.zip") + if int(__version__.split('.')[0]) < 2: + return send_file(memory_zip, + as_attachment=True, + attachment_filename="Calibre-Web-debug-pack.zip") + else: + return send_file(memory_zip, + as_attachment=True, + download_name="Calibre-Web-debug-pack.zip") diff --git a/cps/editbooks.py b/cps/editbooks.py index b7f496d0..1bbb099b 100644 --- a/cps/editbooks.py +++ b/cps/editbooks.py @@ -26,7 +26,22 @@ from datetime import datetime import json from shutil import copyfile from uuid import uuid4 +from markupsafe import escape +try: + from lxml.html.clean import clean_html +except ImportError: + pass + +# Improve this to check if scholarly is available in a global way, like other pythonic libraries +try: + from scholarly import scholarly + have_scholar = True +except ImportError: + have_scholar = False + +from babel import Locale as LC +from babel.core import UnknownLocaleError from flask import Blueprint, request, flash, redirect, url_for, abort, Markup, Response from flask_babel import gettext as _ from flask_login import current_user, login_required @@ -46,6 +61,8 @@ except ImportError: pass # We're not using Python 3 + + editbook = Blueprint('editbook', __name__) log = logger.create() @@ -68,17 +85,7 @@ def edit_required(f): return inner - -# Modifies different Database objects, first check if elements have to be added to database, than check -# if elements have to be deleted, because they are no longer used -def modify_database_object(input_elements, db_book_object, db_object, db_session, db_type): - # passing input_elements not as a list may lead to undesired results - if not isinstance(input_elements, list): - raise TypeError(str(input_elements) + " should be passed as a list") - changed = False - input_elements = [x for x in input_elements if x != ''] - # we have all input element (authors, series, tags) names now - # 1. search for elements to remove +def search_objects_remove(db_book_object, db_type, input_elements): del_elements = [] for c_elements in db_book_object: found = False @@ -96,7 +103,10 @@ def modify_database_object(input_elements, db_book_object, db_object, db_session # if the element was not found in the new list, add it to remove list if not found: del_elements.append(c_elements) - # 2. search for elements that need to be added + return del_elements + + +def search_objects_add(db_book_object, db_type, input_elements): add_elements = [] for inp_element in input_elements: found = False @@ -112,64 +122,96 @@ def modify_database_object(input_elements, db_book_object, db_object, db_session break if not found: add_elements.append(inp_element) - # if there are elements to remove, we remove them now + return add_elements + + +def remove_objects(db_book_object, db_session, del_elements): + changed = False if len(del_elements) > 0: for del_element in del_elements: db_book_object.remove(del_element) changed = True if len(del_element.books) == 0: db_session.delete(del_element) + return changed + +def add_objects(db_book_object, db_object, db_session, db_type, add_elements): + changed = False + if db_type == 'languages': + db_filter = db_object.lang_code + elif db_type == 'custom': + db_filter = db_object.value + else: + db_filter = db_object.name + for add_element in add_elements: + # check if a element with that name exists + db_element = db_session.query(db_object).filter(db_filter == add_element).first() + # if no element is found add it + # if new_element is None: + if db_type == 'author': + new_element = db_object(add_element, helper.get_sorted_author(add_element.replace('|', ',')), "") + elif db_type == 'series': + new_element = db_object(add_element, add_element) + elif db_type == 'custom': + new_element = db_object(value=add_element) + elif db_type == 'publisher': + new_element = db_object(add_element, None) + else: # db_type should be tag or language + new_element = db_object(add_element) + if db_element is None: + changed = True + db_session.add(new_element) + db_book_object.append(new_element) + else: + db_element = create_objects_for_addition(db_element, add_element, db_type) + changed = True + # add element to book + changed = True + db_book_object.append(db_element) + return changed + + +def create_objects_for_addition(db_element, add_element, db_type): + if db_type == 'custom': + if db_element.value != add_element: + db_element.value = add_element # ToDo: Before new_element, but this is not plausible + elif db_type == 'languages': + if db_element.lang_code != add_element: + db_element.lang_code = add_element + elif db_type == 'series': + if db_element.name != add_element: + db_element.name = add_element + db_element.sort = add_element + elif db_type == 'author': + if db_element.name != add_element: + db_element.name = add_element + db_element.sort = add_element.replace('|', ',') + elif db_type == 'publisher': + if db_element.name != add_element: + db_element.name = add_element + db_element.sort = None + elif db_element.name != add_element: + db_element.name = add_element + return db_element + + +# Modifies different Database objects, first check if elements if elements have to be deleted, +# because they are no longer used, than check if elements have to be added to database +def modify_database_object(input_elements, db_book_object, db_object, db_session, db_type): + # passing input_elements not as a list may lead to undesired results + if not isinstance(input_elements, list): + raise TypeError(str(input_elements) + " should be passed as a list") + input_elements = [x for x in input_elements if x != ''] + # we have all input element (authors, series, tags) names now + # 1. search for elements to remove + del_elements = search_objects_remove(db_book_object, db_type, input_elements) + # 2. search for elements that need to be added + add_elements = search_objects_add(db_book_object, db_type, input_elements) + # if there are elements to remove, we remove them now + changed = remove_objects(db_book_object, db_session, del_elements) # if there are elements to add, we add them now! if len(add_elements) > 0: - if db_type == 'languages': - db_filter = db_object.lang_code - elif db_type == 'custom': - db_filter = db_object.value - else: - db_filter = db_object.name - for add_element in add_elements: - # check if a element with that name exists - db_element = db_session.query(db_object).filter(db_filter == add_element).first() - # if no element is found add it - # if new_element is None: - if db_type == 'author': - new_element = db_object(add_element, helper.get_sorted_author(add_element.replace('|', ',')), "") - elif db_type == 'series': - new_element = db_object(add_element, add_element) - elif db_type == 'custom': - new_element = db_object(value=add_element) - elif db_type == 'publisher': - new_element = db_object(add_element, None) - else: # db_type should be tag or language - new_element = db_object(add_element) - if db_element is None: - changed = True - db_session.add(new_element) - db_book_object.append(new_element) - else: - if db_type == 'custom': - if db_element.value != add_element: - new_element.value = add_element - elif db_type == 'languages': - if db_element.lang_code != add_element: - db_element.lang_code = add_element - elif db_type == 'series': - if db_element.name != add_element: - db_element.name = add_element - db_element.sort = add_element - elif db_type == 'author': - if db_element.name != add_element: - db_element.name = add_element - db_element.sort = add_element.replace('|', ',') - elif db_type == 'publisher': - if db_element.name != add_element: - db_element.name = add_element - db_element.sort = None - elif db_element.name != add_element: - db_element.name = add_element - # add element to book - changed = True - db_book_object.append(db_element) + changed |= add_objects(db_book_object, db_object, db_session, db_type, add_elements) return changed @@ -202,14 +244,14 @@ def modify_identifiers(input_identifiers, db_identifiers, db_session): @editbook.route("/ajax/delete/") @login_required def delete_book_from_details(book_id): - return Response(delete_book(book_id,"", True), mimetype='application/json') + return Response(delete_book(book_id, "", True), mimetype='application/json') @editbook.route("/delete/", defaults={'book_format': ""}) @editbook.route("/delete//") @login_required def delete_book_ajax(book_id, book_format): - return delete_book(book_id,book_format, False) + return delete_book(book_id, book_format, False) def delete_whole_book(book_id, book): @@ -288,19 +330,19 @@ def delete_book(book_id, book_format, jsonResponse): result, error = helper.delete_book(book, config.config_calibre_dir, book_format=book_format.upper()) if not result: if jsonResponse: - return json.dumps({"location": url_for("editbook.edit_book"), - "type": "alert", + return json.dumps([{"location": url_for("editbook.edit_book", book_id=book_id), + "type": "danger", "format": "", - "error": error}), + "message": error}]) else: flash(error, category="error") return redirect(url_for('editbook.edit_book', book_id=book_id)) if error: if jsonResponse: - warning = {"location": url_for("editbook.edit_book"), + warning = {"location": url_for("editbook.edit_book", book_id=book_id), "type": "warning", "format": "", - "error": error} + "message": error} else: flash(error, category="warning") if not book_format: @@ -309,9 +351,18 @@ def delete_book(book_id, book_format, jsonResponse): calibre_db.session.query(db.Data).filter(db.Data.book == book.id).\ filter(db.Data.format == book_format).delete() calibre_db.session.commit() - except Exception as e: - log.debug_or_exception(e) + except Exception as ex: + log.debug_or_exception(ex) calibre_db.session.rollback() + if jsonResponse: + return json.dumps([{"location": url_for("editbook.edit_book", book_id=book_id), + "type": "danger", + "format": "", + "message": ex}]) + else: + flash(str(ex), category="error") + return redirect(url_for('editbook.edit_book', book_id=book_id)) + else: # book not found log.error('Book with id "%s" could not be deleted: not found', book_id) @@ -322,7 +373,7 @@ def render_edit_book(book_id): cc = calibre_db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all() book = calibre_db.get_filtered_book(book_id, allow_show_archived=True) if not book: - flash(_(u"Error opening eBook. File does not exist or file is not accessible"), category="error") + flash(_(u"Oops! Selected book title is unavailable. File does not exist or is not accessible"), category="error") return redirect(url_for("web.index")) for lang in book.languages: @@ -403,6 +454,9 @@ def edit_book_series_index(series_index, book): # Add default series_index to book modif_date = False series_index = series_index or '1' + if not series_index.replace('.', '', 1).isdigit(): + flash(_("%(seriesindex)s is not a valid number, skipping", seriesindex=series_index), category="warning") + return False if book.series_index != series_index: book.series_index = series_index modif_date = True @@ -411,6 +465,8 @@ def edit_book_series_index(series_index, book): # Handle book comments/description def edit_book_comments(comments, book): modif_date = False + if comments: + comments = clean_html(comments) if len(book.comments): if book.comments[0].text != comments: book.comments[0].text = comments @@ -422,7 +478,7 @@ def edit_book_comments(comments, book): return modif_date -def edit_book_languages(languages, book, upload=False): +def edit_book_languages(languages, book, upload=False, invalid=None): input_languages = languages.split(',') unknown_languages = [] if not upload: @@ -431,7 +487,10 @@ def edit_book_languages(languages, book, upload=False): input_l = isoLanguages.get_valid_language_codes(get_locale(), input_languages, unknown_languages) for l in unknown_languages: log.error('%s is not a valid language', l) - flash(_(u"%(langname)s is not a valid language", langname=l), category="warning") + if isinstance(invalid, list): + invalid.append(l) + else: + flash(_(u"%(langname)s is not a valid language", langname=l), category="warning") # ToDo: Not working correct if upload and len(input_l) == 1: # If the language of the file is excluded from the users view, it's not imported, to allow the user to view @@ -456,12 +515,21 @@ def edit_book_publisher(publishers, book): return changed -def edit_cc_data_number(book_id, book, c, to_save, cc_db_value, cc_string): +def edit_cc_data_value(book_id, book, c, to_save, cc_db_value, cc_string): changed = False if to_save[cc_string] == 'None': to_save[cc_string] = None elif c.datatype == 'bool': to_save[cc_string] = 1 if to_save[cc_string] == 'True' else 0 + elif c.datatype == 'comments': + to_save[cc_string] = Markup(to_save[cc_string]).unescape() + if to_save[cc_string]: + to_save[cc_string] = clean_html(to_save[cc_string]) + elif c.datatype == 'datetime': + try: + to_save[cc_string] = datetime.strptime(to_save[cc_string], "%Y-%m-%d") + except ValueError: + to_save[cc_string] = db.Books.DEFAULT_PUBDATE if to_save[cc_string] != cc_db_value: if cc_db_value is not None: @@ -520,8 +588,8 @@ def edit_cc_data(book_id, book, to_save): else: cc_db_value = None if to_save[cc_string].strip(): - if c.datatype == 'int' or c.datatype == 'bool' or c.datatype == 'float': - changed, to_save = edit_cc_data_number(book_id, book, c, to_save, cc_db_value, cc_string) + if c.datatype in ['int', 'bool', 'float', "datetime", "comments"]: + changed, to_save = edit_cc_data_value(book_id, book, c, to_save, cc_db_value, cc_string) else: changed, to_save = edit_cc_data_string(book, c, to_save, cc_db_value, cc_string) else: @@ -596,9 +664,9 @@ def upload_single_file(request, book, book_id): return redirect(url_for('web.show_book', book_id=book.id)) # Queue uploader info - uploadText=_(u"File format %(ext)s added to %(book)s", ext=file_ext.upper(), book=book.title) - WorkerThread.add(current_user.nickname, TaskUpload( - "" + uploadText + "")) + link = '{}'.format(url_for('web.show_book', book_id=book.id), escape(book.title)) + uploadText=_(u"File format %(ext)s added to %(book)s", ext=file_ext.upper(), book=link) + WorkerThread.add(current_user.name, TaskUpload(uploadText)) return uploader.process( saved_filename, *os.path.splitext(requested_file.filename), @@ -622,6 +690,46 @@ def upload_cover(request, book): return None +def handle_title_on_edit(book, book_title): + # handle book title + book_title = book_title.rstrip().strip() + if book.title != book_title: + if book_title == '': + book_title = _(u'Unknown') + book.title = book_title + return True + return False + + +def handle_author_on_edit(book, author_name, update_stored=True): + # handle author(s) + input_authors = author_name.split('&') + input_authors = list(map(lambda it: it.strip().replace(',', '|'), input_authors)) + # Remove duplicates in authors list + input_authors = helper.uniq(input_authors) + # we have all author names now + if input_authors == ['']: + input_authors = [_(u'Unknown')] # prevent empty Author + + change = modify_database_object(input_authors, book.authors, db.Authors, calibre_db.session, 'author') + + # Search for each author if author is in database, if not, author name and sorted author name is generated new + # everything then is assembled for sorted author field in database + sort_authors_list = list() + for inp in input_authors: + stored_author = calibre_db.session.query(db.Authors).filter(db.Authors.name == inp).first() + if not stored_author: + stored_author = helper.get_sorted_author(inp) + else: + stored_author = stored_author.sort + sort_authors_list.append(helper.get_sorted_author(stored_author)) + sort_authors = ' & '.join(sort_authors_list) + if book.author_sort != sort_authors and update_stored: + book.author_sort = sort_authors + change = True + return input_authors, change + + @editbook.route("/admin/book/", methods=['GET', 'POST']) @login_required_if_no_ano @edit_required @@ -639,12 +747,11 @@ def edit_book(book_id): if request.method != 'POST': return render_edit_book(book_id) - book = calibre_db.get_filtered_book(book_id, allow_show_archived=True) # Book not found if not book: - flash(_(u"Error opening eBook. File does not exist or file is not accessible"), category="error") + flash(_(u"Oops! Selected book title is unavailable. File does not exist or is not accessible"), category="error") return redirect(url_for("web.index")) meta = upload_single_file(request, book, book_id) @@ -657,41 +764,14 @@ def edit_book(book_id): # Update book edited_books_id = None - #handle book title - if book.title != to_save["book_title"].rstrip().strip(): - if to_save["book_title"] == '': - to_save["book_title"] = _(u'Unknown') - book.title = to_save["book_title"].rstrip().strip() + # handle book title + title_change = handle_title_on_edit(book, to_save["book_title"]) + + input_authors, authorchange = handle_author_on_edit(book, to_save["author_name"]) + if authorchange or title_change: edited_books_id = book.id modif_date = True - # handle author(s) - input_authors = to_save["author_name"].split('&') - input_authors = list(map(lambda it: it.strip().replace(',', '|'), input_authors)) - # Remove duplicates in authors list - input_authors = helper.uniq(input_authors) - # we have all author names now - if input_authors == ['']: - input_authors = [_(u'Unknown')] # prevent empty Author - - modif_date |= modify_database_object(input_authors, book.authors, db.Authors, calibre_db.session, 'author') - - # Search for each author if author is in database, if not, authorname and sorted authorname is generated new - # everything then is assembled for sorted author field in database - sort_authors_list = list() - for inp in input_authors: - stored_author = calibre_db.session.query(db.Authors).filter(db.Authors.name == inp).first() - if not stored_author: - stored_author = helper.get_sorted_author(inp) - else: - stored_author = stored_author.sort - sort_authors_list.append(helper.get_sorted_author(stored_author)) - sort_authors = ' & '.join(sort_authors_list) - if book.author_sort != sort_authors: - edited_books_id = book.id - book.author_sort = sort_authors - modif_date = True - if config.config_use_google_drive: gdriveutils.updateGdriveCalibreFromLocal() @@ -717,10 +797,8 @@ def edit_book(book_id): # Add default series_index to book modif_date |= edit_book_series_index(to_save["series_index"], book) - # Handle book comments/description - modif_date |= edit_book_comments(to_save["description"], book) - + modif_date |= edit_book_comments(Markup(to_save['description']).unescape(), book) # Handle identifiers input_identifiers = identifier_list(to_save, book) modification, warning = modify_identifiers(input_identifiers, book.identifiers, calibre_db.session) @@ -729,9 +807,16 @@ def edit_book(book_id): modif_date |= modification # Handle book tags modif_date |= edit_book_tags(to_save['tags'], book) - # Handle book series modif_date |= edit_book_series(to_save["series"], book) + # handle book publisher + modif_date |= edit_book_publisher(to_save['publisher'], book) + # handle book languages + modif_date |= edit_book_languages(to_save['languages'], book) + # handle book ratings + modif_date |= edit_book_ratings(to_save, book) + # handle cc data + modif_date |= edit_cc_data(book_id, book, to_save) if to_save["pubdate"]: try: @@ -741,18 +826,6 @@ def edit_book(book_id): else: book.pubdate = db.Books.DEFAULT_PUBDATE - # handle book publisher - modif_date |= edit_book_publisher(to_save['publisher'], book) - - # handle book languages - modif_date |= edit_book_languages(to_save['languages'], book) - - # handle book ratings - modif_date |= edit_book_ratings(to_save, book) - - # handle cc data - modif_date |= edit_cc_data(book_id, book, to_save) - if modif_date: book.last_modified = datetime.utcnow() calibre_db.session.merge(book) @@ -768,8 +841,8 @@ def edit_book(book_id): calibre_db.session.rollback() flash(error, category="error") return render_edit_book(book_id) - except Exception as e: - log.debug_or_exception(e) + except Exception as ex: + log.debug_or_exception(ex) calibre_db.session.rollback() flash(_("Error editing book, please check logfile for details"), category="error") return redirect(url_for('web.show_book', book_id=book.id)) @@ -885,6 +958,48 @@ def create_book_on_upload(modif_date, meta): calibre_db.session.flush() return db_book, input_authors, title_dir +def file_handling_on_upload(requested_file): + # check if file extension is correct + if '.' in requested_file.filename: + file_ext = requested_file.filename.rsplit('.', 1)[-1].lower() + if file_ext not in constants.EXTENSIONS_UPLOAD and '' not in constants.EXTENSIONS_UPLOAD: + flash( + _("File extension '%(ext)s' is not allowed to be uploaded to this server", + ext=file_ext), category="error") + return None, Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json') + else: + flash(_('File to be uploaded must have an extension'), category="error") + return None, Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json') + + # extract metadata from file + try: + meta = uploader.upload(requested_file, config.config_rarfile_location) + except (IOError, OSError): + log.error("File %s could not saved to temp dir", requested_file.filename) + flash(_(u"File %(filename)s could not saved to temp dir", + filename=requested_file.filename), category="error") + return None, Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json') + return meta, None + + +def move_coverfile(meta, db_book): + # move cover to final directory, including book id + if meta.cover: + coverfile = meta.cover + else: + coverfile = os.path.join(constants.STATIC_DIR, 'generic_cover.jpg') + new_coverpath = os.path.join(config.config_calibre_dir, db_book.path, "cover.jpg") + try: + copyfile(coverfile, new_coverpath) + if meta.cover: + os.unlink(meta.cover) + except OSError as e: + log.error("Failed to move cover file %s: %s", new_coverpath, e) + flash(_(u"Failed to Move Cover File %(file)s: %(error)s", file=new_coverpath, + error=e), + category="error") + + @editbook.route("/upload", methods=["GET", "POST"]) @login_required_if_no_ano @upload_required @@ -899,30 +1014,13 @@ def upload(): calibre_db.update_title_sort(config) calibre_db.session.connection().connection.connection.create_function('uuid4', 0, lambda: str(uuid4())) - # check if file extension is correct - if '.' in requested_file.filename: - file_ext = requested_file.filename.rsplit('.', 1)[-1].lower() - if file_ext not in constants.EXTENSIONS_UPLOAD and '' not in constants.EXTENSIONS_UPLOAD: - flash( - _("File extension '%(ext)s' is not allowed to be uploaded to this server", - ext=file_ext), category="error") - return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json') - else: - flash(_('File to be uploaded must have an extension'), category="error") - return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json') - - # extract metadata from file - try: - meta = uploader.upload(requested_file, config.config_rarfile_location) - except (IOError, OSError): - log.error("File %s could not saved to temp dir", requested_file.filename) - flash(_(u"File %(filename)s could not saved to temp dir", - filename= requested_file.filename), category="error") - return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json') + meta, error = file_handling_on_upload(requested_file) + if error: + return error db_book, input_authors, title_dir = create_book_on_upload(modif_date, meta) - # Comments needs book id therfore only possible after flush + # Comments needs book id therefore only possible after flush modif_date |= edit_book_comments(Markup(meta.description).unescape(), db_book) book_id = db_book.id @@ -932,23 +1030,9 @@ def upload(): config.config_calibre_dir, input_authors[0], meta.file_path, - title_dir + meta.extension) + title_dir + meta.extension.lower()) - # move cover to final directory, including book id - if meta.cover: - coverfile = meta.cover - else: - coverfile = os.path.join(constants.STATIC_DIR, 'generic_cover.jpg') - new_coverpath = os.path.join(config.config_calibre_dir, db_book.path, "cover.jpg") - try: - copyfile(coverfile, new_coverpath) - if meta.cover: - os.unlink(meta.cover) - except OSError as e: - log.error("Failed to move cover file %s: %s", new_coverpath, e) - flash(_(u"Failed to Move Cover File %(file)s: %(error)s", file=new_coverpath, - error=e), - category="error") + move_coverfile(meta, db_book) # save data to database, reread data calibre_db.session.commit() @@ -957,9 +1041,9 @@ def upload(): gdriveutils.updateGdriveCalibreFromLocal() if error: flash(error, category="error") - uploadText=_(u"File %(file)s uploaded", file=title) - WorkerThread.add(current_user.nickname, TaskUpload( - "" + uploadText + "")) + link = '{}'.format(url_for('web.show_book', book_id=book_id), escape(title)) + uploadText = _(u"File %(file)s uploaded", file=link) + WorkerThread.add(current_user.name, TaskUpload(uploadText)) if len(request.files.getlist("btn-upload")) < 2: if current_user.role_edit() or current_user.role_admin(): @@ -988,7 +1072,7 @@ def convert_bookformat(book_id): log.info('converting: book id: %s from: %s to: %s', book_id, book_format_from, book_format_to) rtn = helper.convert_book_format(book_id, config.config_calibre_dir, book_format_from.upper(), - book_format_to.upper(), current_user.nickname) + book_format_to.upper(), current_user.name) if rtn is None: flash(_(u"Book successfully queued for converting to %(book_format)s", @@ -998,61 +1082,110 @@ def convert_bookformat(book_id): flash(_(u"There was an error converting this book: %(res)s", res=rtn), category="error") return redirect(url_for('editbook.edit_book', book_id=book_id)) +@editbook.route("/scholarsearch/",methods=['GET']) +@login_required_if_no_ano +@edit_required +def scholar_search(query): + if have_scholar: + scholar_gen = scholarly.search_pubs(' '.join(query.split('+'))) + i=0 + result = [] + for publication in scholar_gen: + del publication['source'] + result.append(publication) + i+=1 + if(i>=10): + break + return Response(json.dumps(result),mimetype='application/json') + else: + return "[]" + @editbook.route("/ajax/editbooks/", methods=['POST']) @login_required_if_no_ano @edit_required def edit_list_book(param): vals = request.form.to_dict() book = calibre_db.get_book(vals['pk']) + ret = "" if param =='series_index': edit_book_series_index(vals['value'], book) + ret = Response(json.dumps({'success': True, 'newValue': book.series_index}), mimetype='application/json') elif param =='tags': edit_book_tags(vals['value'], book) + ret = Response(json.dumps({'success': True, 'newValue': ', '.join([tag.name for tag in book.tags])}), + mimetype='application/json') elif param =='series': edit_book_series(vals['value'], book) + ret = Response(json.dumps({'success': True, 'newValue': ', '.join([serie.name for serie in book.series])}), + mimetype='application/json') elif param =='publishers': - vals['publisher'] = vals['value'] - edit_book_publisher(vals, book) + edit_book_publisher(vals['value'], book) + ret = Response(json.dumps({'success': True, + 'newValue': ', '.join([publisher.name for publisher in book.publishers])}), + mimetype='application/json') elif param =='languages': - edit_book_languages(vals['value'], book) + invalid = list() + edit_book_languages(vals['value'], book, invalid=invalid) + if invalid: + ret = Response(json.dumps({'success': False, + 'msg': 'Invalid languages in request: {}'.format(','.join(invalid))}), + mimetype='application/json') + else: + lang_names = list() + for lang in book.languages: + try: + lang_names.append(LC.parse(lang.lang_code).get_language_name(get_locale())) + except UnknownLocaleError: + lang_names.append(_(isoLanguages.get(part3=lang.lang_code).name)) + ret = Response(json.dumps({'success': True, 'newValue': ', '.join(lang_names)}), + mimetype='application/json') elif param =='author_sort': book.author_sort = vals['value'] - elif param =='title': - book.title = vals['value'] + ret = Response(json.dumps({'success': True, 'newValue': book.author_sort}), + mimetype='application/json') + elif param == 'title': + sort = book.sort + handle_title_on_edit(book, vals.get('value', "")) helper.update_dir_stucture(book.id, config.config_calibre_dir) + ret = Response(json.dumps({'success': True, 'newValue': book.title}), + mimetype='application/json') elif param =='sort': book.sort = vals['value'] - # ToDo: edit books + ret = Response(json.dumps({'success': True, 'newValue': book.sort}), + mimetype='application/json') elif param =='authors': - input_authors = vals['value'].split('&') - input_authors = list(map(lambda it: it.strip().replace(',', '|'), input_authors)) - modify_database_object(input_authors, book.authors, db.Authors, calibre_db.session, 'author') - sort_authors_list = list() - for inp in input_authors: - stored_author = calibre_db.session.query(db.Authors).filter(db.Authors.name == inp).first() - if not stored_author: - stored_author = helper.get_sorted_author(inp) - else: - stored_author = stored_author.sort - sort_authors_list.append(helper.get_sorted_author(stored_author)) - sort_authors = ' & '.join(sort_authors_list) - if book.author_sort != sort_authors: - book.author_sort = sort_authors + input_authors, __ = handle_author_on_edit(book, vals['value'], vals.get('checkA', None) == "true") helper.update_dir_stucture(book.id, config.config_calibre_dir, input_authors[0]) + ret = Response(json.dumps({'success': True, + 'newValue': ' & '.join([author.replace('|',',') for author in input_authors])}), + mimetype='application/json') book.last_modified = datetime.utcnow() - calibre_db.session.commit() - return "" + try: + calibre_db.session.commit() + # revert change for sort if automatic fields link is deactivated + if param == 'title' and vals.get('checkT') == "false": + book.sort = sort + calibre_db.session.commit() + except (OperationalError, IntegrityError) as e: + calibre_db.session.rollback() + log.error("Database error: %s", e) + return ret + @editbook.route("/ajax/sort_value//") @login_required def get_sorted_entry(field, bookid): - if field == 'title' or field == 'authors': + if field in ['title', 'authors', 'sort', 'author_sort']: book = calibre_db.get_filtered_book(bookid) if book: if field == 'title': return json.dumps({'sort': book.sort}) elif field == 'authors': return json.dumps({'author_sort': book.author_sort}) + if field == 'sort': + return json.dumps({'sort': book.title}) + if field == 'author_sort': + return json.dumps({'author_sort': book.author}) return "" @@ -1104,6 +1237,46 @@ def merge_list_book(): element.format, element.uncompressed_size, to_name)) - delete_book(from_book.id,"", True) # json_resp = + delete_book(from_book.id,"", True) return json.dumps({'success': True}) return "" + +@editbook.route("/ajax/xchange", methods=['POST']) +@login_required +@edit_required +def table_xchange_author_title(): + vals = request.get_json().get('xchange') + if vals: + for val in vals: + modif_date = False + book = calibre_db.get_book(val) + authors = book.title + entries = calibre_db.order_authors(book) + author_names = [] + for authr in entries.authors: + author_names.append(authr.name.replace('|', ',')) + + title_change = handle_title_on_edit(book, " ".join(author_names)) + input_authors, authorchange = handle_author_on_edit(book, authors) + if authorchange or title_change: + edited_books_id = book.id + modif_date = True + + if config.config_use_google_drive: + gdriveutils.updateGdriveCalibreFromLocal() + + if edited_books_id: + helper.update_dir_stucture(edited_books_id, config.config_calibre_dir, input_authors[0]) + if modif_date: + book.last_modified = datetime.utcnow() + try: + calibre_db.session.commit() + except (OperationalError, IntegrityError) as e: + calibre_db.session.rollback() + log.error("Database error: %s", e) + return json.dumps({'success': False}) + + if config.config_use_google_drive: + gdriveutils.updateGdriveCalibreFromLocal() + return json.dumps({'success': True}) + return "" diff --git a/cps/epub.py b/cps/epub.py index 5833c2aa..998dbfa6 100644 --- a/cps/epub.py +++ b/cps/epub.py @@ -87,18 +87,29 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension): lang = epub_metadata['language'].split('-', 1)[0].lower() epub_metadata['language'] = isoLanguages.get_lang3(lang) - series = tree.xpath("/pkg:package/pkg:metadata/pkg:meta[@name='calibre:series']/@content", namespaces=ns) - if len(series) > 0: - epub_metadata['series'] = series[0] - else: - epub_metadata['series'] = '' + epub_metadata = parse_epbub_series(ns, tree, epub_metadata) - series_id = tree.xpath("/pkg:package/pkg:metadata/pkg:meta[@name='calibre:series_index']/@content", namespaces=ns) - if len(series_id) > 0: - epub_metadata['series_id'] = series_id[0] - else: - epub_metadata['series_id'] = '1' + coverfile = parse_ebpub_cover(ns, tree, epubZip, coverpath, tmp_file_path) + if not epub_metadata['title']: + title = original_file_name + else: + title = epub_metadata['title'] + + return BookMeta( + file_path=tmp_file_path, + extension=original_file_extension, + title=title.encode('utf-8').decode('utf-8'), + author=epub_metadata['creator'].encode('utf-8').decode('utf-8'), + cover=coverfile, + description=epub_metadata['description'], + tags=epub_metadata['subject'].encode('utf-8').decode('utf-8'), + series=epub_metadata['series'].encode('utf-8').decode('utf-8'), + series_id=epub_metadata['series_id'].encode('utf-8').decode('utf-8'), + languages=epub_metadata['language'], + publisher="") + +def parse_ebpub_cover(ns, tree, epubZip, coverpath, tmp_file_path): coversection = tree.xpath("/pkg:package/pkg:manifest/pkg:item[@id='cover-image']/@href", namespaces=ns) coverfile = None if len(coversection) > 0: @@ -126,21 +137,18 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension): coverfile = extractCover(epubZip, filename, "", tmp_file_path) else: coverfile = extractCover(epubZip, coversection[0], coverpath, tmp_file_path) + return coverfile - if not epub_metadata['title']: - title = original_file_name +def parse_epbub_series(ns, tree, epub_metadata): + series = tree.xpath("/pkg:package/pkg:metadata/pkg:meta[@name='calibre:series']/@content", namespaces=ns) + if len(series) > 0: + epub_metadata['series'] = series[0] else: - title = epub_metadata['title'] + epub_metadata['series'] = '' - return BookMeta( - file_path=tmp_file_path, - extension=original_file_extension, - title=title.encode('utf-8').decode('utf-8'), - author=epub_metadata['creator'].encode('utf-8').decode('utf-8'), - cover=coverfile, - description=epub_metadata['description'], - tags=epub_metadata['subject'].encode('utf-8').decode('utf-8'), - series=epub_metadata['series'].encode('utf-8').decode('utf-8'), - series_id=epub_metadata['series_id'].encode('utf-8').decode('utf-8'), - languages=epub_metadata['language'], - publisher="") + series_id = tree.xpath("/pkg:package/pkg:metadata/pkg:meta[@name='calibre:series_index']/@content", namespaces=ns) + if len(series_id) > 0: + epub_metadata['series_id'] = series_id[0] + else: + epub_metadata['series_id'] = '1' + return epub_metadata diff --git a/cps/error_handler.py b/cps/error_handler.py index 373a1434..37b7500e 100644 --- a/cps/error_handler.py +++ b/cps/error_handler.py @@ -35,6 +35,7 @@ def error_http(error): error_code="Error {0}".format(error.code), error_name=error.name, issue=False, + unconfigured=not config.db_configured, instance=config.config_calibre_web_title ), error.code @@ -44,6 +45,7 @@ def internal_error(error): error_code="Internal Server Error", error_name=str(error), issue=True, + unconfigured=False, error_stack=traceback.format_exc().split("\n"), instance=config.config_calibre_web_title ), 500 diff --git a/cps/fb2.py b/cps/fb2.py index d7b03d5b..af4a29a7 100644 --- a/cps/fb2.py +++ b/cps/fb2.py @@ -29,7 +29,7 @@ def get_fb2_info(tmp_file_path, original_file_extension): 'l': 'http://www.w3.org/1999/xlink', } - fb2_file = open(tmp_file_path) + fb2_file = open(tmp_file_path, encoding="utf-8") tree = etree.fromstring(fb2_file.read().encode()) authors = tree.xpath('/fb:FictionBook/fb:description/fb:title-info/fb:author', namespaces=ns) diff --git a/cps/gdrive.py b/cps/gdrive.py index 158a5a4f..c0764015 100644 --- a/cps/gdrive.py +++ b/cps/gdrive.py @@ -74,7 +74,7 @@ def google_drive_callback(): f.write(credentials.to_json()) except (ValueError, AttributeError) as error: log.error(error) - return redirect(url_for('admin.configuration')) + return redirect(url_for('admin.db_configuration')) @gdrive.route("/watch/subscribe") @@ -99,7 +99,7 @@ def watch_gdrive(): else: flash(reason['message'], category="error") - return redirect(url_for('admin.configuration')) + return redirect(url_for('admin.db_configuration')) @gdrive.route("/watch/revoke") @@ -115,7 +115,7 @@ def revoke_watch_gdrive(): pass config.config_google_drive_watch_changes_response = {} config.save() - return redirect(url_for('admin.configuration')) + return redirect(url_for('admin.db_configuration')) @gdrive.route("/watch/callback", methods=['GET', 'POST']) @@ -155,6 +155,6 @@ def on_received_watch_confirmation(): # prevent error on windows, as os.rename does on existing files, also allow cross hdd move move(os.path.join(tmp_dir, "tmp_metadata.db"), dbpath) calibre_db.reconnect_db(config, ub.app_DB_path) - except Exception as e: - log.debug_or_exception(e) + except Exception as ex: + log.debug_or_exception(ex) return '' diff --git a/cps/gdriveutils.py b/cps/gdriveutils.py index c7a95ca5..c63b0393 100644 --- a/cps/gdriveutils.py +++ b/cps/gdriveutils.py @@ -34,6 +34,7 @@ try: except ImportError: from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.exc import OperationalError, InvalidRequestError +from sqlalchemy.sql.expression import text try: from apiclient import errors @@ -168,7 +169,7 @@ class PermissionAdded(Base): def migrate(): if not engine.dialect.has_table(engine.connect(), "permissions_added"): PermissionAdded.__table__.create(bind = engine) - for sql in session.execute("select sql from sqlite_master where type='table'"): + for sql in session.execute(text("select sql from sqlite_master where type='table'")): if 'CREATE TABLE gdrive_ids' in sql[0]: currUniqueConstraint = 'UNIQUE (gdrive_id)' if currUniqueConstraint in sql[0]: @@ -202,8 +203,8 @@ def getDrive(drive=None, gauth=None): gauth.Refresh() except RefreshError as e: log.error("Google Drive error: %s", e) - except Exception as e: - log.debug_or_exception(e) + except Exception as ex: + log.debug_or_exception(ex) else: # Initialize the saved creds gauth.Authorize() @@ -221,7 +222,7 @@ def listRootFolders(): drive = getDrive(Gdrive.Instance().drive) folder = "'root' in parents and mimeType = 'application/vnd.google-apps.folder' and trashed = false" fileList = drive.ListFile({'q': folder}).GetList() - except (ServerNotFoundError, ssl.SSLError) as e: + except (ServerNotFoundError, ssl.SSLError, RefreshError) as e: log.info("GDrive Error %s" % e) fileList = [] return fileList @@ -257,7 +258,12 @@ def getEbooksFolderId(drive=None): log.error('Error gDrive, root ID not found') gDriveId.path = '/' session.merge(gDriveId) - session.commit() + try: + session.commit() + except OperationalError as ex: + log.error("gdrive.db DB is not Writeable") + log.debug('Database error: %s', ex) + session.rollback() return gDriveId.gdrive_id @@ -272,37 +278,42 @@ def getFile(pathId, fileName, drive): def getFolderId(path, drive): # drive = getDrive(drive) - currentFolderId = getEbooksFolderId(drive) - sqlCheckPath = path if path[-1] == '/' else path + '/' - storedPathName = session.query(GdriveId).filter(GdriveId.path == sqlCheckPath).first() + try: + currentFolderId = getEbooksFolderId(drive) + sqlCheckPath = path if path[-1] == '/' else path + '/' + storedPathName = session.query(GdriveId).filter(GdriveId.path == sqlCheckPath).first() - if not storedPathName: - dbChange = False - s = path.split('/') - for i, x in enumerate(s): - if len(x) > 0: - currentPath = "/".join(s[:i+1]) - if currentPath[-1] != '/': - currentPath = currentPath + '/' - storedPathName = session.query(GdriveId).filter(GdriveId.path == currentPath).first() - if storedPathName: - currentFolderId = storedPathName.gdrive_id - else: - currentFolder = getFolderInFolder(currentFolderId, x, drive) - if currentFolder: - gDriveId = GdriveId() - gDriveId.gdrive_id = currentFolder['id'] - gDriveId.path = currentPath - session.merge(gDriveId) - dbChange = True - currentFolderId = currentFolder['id'] + if not storedPathName: + dbChange = False + s = path.split('/') + for i, x in enumerate(s): + if len(x) > 0: + currentPath = "/".join(s[:i+1]) + if currentPath[-1] != '/': + currentPath = currentPath + '/' + storedPathName = session.query(GdriveId).filter(GdriveId.path == currentPath).first() + if storedPathName: + currentFolderId = storedPathName.gdrive_id else: - currentFolderId = None - break - if dbChange: - session.commit() - else: - currentFolderId = storedPathName.gdrive_id + currentFolder = getFolderInFolder(currentFolderId, x, drive) + if currentFolder: + gDriveId = GdriveId() + gDriveId.gdrive_id = currentFolder['id'] + gDriveId.path = currentPath + session.merge(gDriveId) + dbChange = True + currentFolderId = currentFolder['id'] + else: + currentFolderId = None + break + if dbChange: + session.commit() + else: + currentFolderId = storedPathName.gdrive_id + except OperationalError as ex: + log.error("gdrive.db DB is not Writeable") + log.debug('Database error: %s', ex) + session.rollback() return currentFolderId @@ -346,7 +357,7 @@ def moveGdriveFolderRemote(origin_file, target_folder): addParents=gFileTargetDir['id'], removeParents=previous_parents, fields='id, parents').execute() - # if previous_parents has no childs anymore, delete original fileparent + # if previous_parents has no children anymore, delete original fileparent if len(children['items']) == 1: deleteDatabaseEntry(previous_parents) drive.auth.service.files().delete(fileId=previous_parents).execute() @@ -497,8 +508,8 @@ def getChangeById (drive, change_id): except (errors.HttpError) as error: log.error(error) return None - except Exception as e: - log.error(e) + except Exception as ex: + log.error(ex) return None @@ -507,9 +518,10 @@ def deleteDatabaseOnChange(): try: session.query(GdriveId).delete() session.commit() - except (OperationalError, InvalidRequestError): + except (OperationalError, InvalidRequestError) as ex: session.rollback() - log.info(u"GDrive DB is not Writeable") + log.debug('Database error: %s', ex) + log.error(u"GDrive DB is not Writeable") def updateGdriveCalibreFromLocal(): @@ -524,13 +536,23 @@ def updateDatabaseOnEdit(ID,newPath): storedPathName = session.query(GdriveId).filter(GdriveId.gdrive_id == ID).first() if storedPathName: storedPathName.path = sqlCheckPath - session.commit() + try: + session.commit() + except OperationalError as ex: + log.error("gdrive.db DB is not Writeable") + log.debug('Database error: %s', ex) + session.rollback() # Deletes the hashes in database of deleted book def deleteDatabaseEntry(ID): session.query(GdriveId).filter(GdriveId.gdrive_id == ID).delete() - session.commit() + try: + session.commit() + except OperationalError as ex: + log.error("gdrive.db DB is not Writeable") + log.debug('Database error: %s', ex) + session.rollback() # Gets cover file from gdrive @@ -547,7 +569,12 @@ def get_cover_via_gdrive(cover_path): permissionAdded = PermissionAdded() permissionAdded.gdrive_id = df['id'] session.add(permissionAdded) - session.commit() + try: + session.commit() + except OperationalError as ex: + log.error("gdrive.db DB is not Writeable") + log.debug('Database error: %s', ex) + session.rollback() return df.metadata.get('webContentLink') else: return None diff --git a/cps/helper.py b/cps/helper.py index e3c79dea..758d2531 100644 --- a/cps/helper.py +++ b/cps/helper.py @@ -35,9 +35,10 @@ from babel.units import format_unit from flask import send_from_directory, make_response, redirect, abort, url_for from flask_babel import gettext as _ from flask_login import current_user -from sqlalchemy.sql.expression import true, false, and_, text +from sqlalchemy.sql.expression import true, false, and_, text, func from werkzeug.datastructures import Headers from werkzeug.security import generate_password_hash +from markupsafe import escape try: from urllib.parse import quote @@ -98,10 +99,11 @@ def convert_book_format(book_id, calibrepath, old_book_format, new_book_format, settings['body'] = _(u'This e-mail has been sent via Calibre-Web.') else: settings = dict() - txt = (u"%s -> %s: %s" % ( - old_book_format, - new_book_format, - "" + book.title + "")) + link = '{}'.format(url_for('web.show_book', book_id=book.id), escape(book.title)) # prevent xss + txt = u"{} -> {}: {}".format( + old_book_format.upper(), + new_book_format.upper(), + link) settings['old_book_format'] = old_book_format settings['new_book_format'] = new_book_format WorkerThread.add(user_id, TaskConvert(file_path, book.id, txt, settings, kindle_mail, user_id)) @@ -215,9 +217,11 @@ def send_mail(book_id, book_format, convert, kindle_mail, calibrepath, user_id): for entry in iter(book.data): if entry.format.upper() == book_format.upper(): converted_file_name = entry.name + '.' + book_format.lower() + link = '{}'.format(url_for('web.show_book', book_id=book_id), escape(book.title)) + EmailText = _(u"%(book)s send to Kindle", book=link) WorkerThread.add(user_id, TaskEmail(_(u"Send to Kindle"), book.path, converted_file_name, config.get_mail_settings(), kindle_mail, - _(u"E-mail: %(book)s", book=book.title), _(u'This e-mail has been sent via Calibre-Web.'))) + EmailText, _(u'This e-mail has been sent via Calibre-Web.'))) return return _(u"The requested file could not be read. Maybe wrong permissions?") @@ -231,16 +235,14 @@ def get_valid_filename(value, replace_whitespace=True): value = value[:-1]+u'_' value = value.replace("/", "_").replace(":", "_").strip('\0') if use_unidecode: - value = (unidecode.unidecode(value)) + if not config.config_unicode_filename: + value = (unidecode.unidecode(value)) else: value = value.replace(u'§', u'SS') value = value.replace(u'ß', u'ss') value = unicodedata.normalize('NFKD', value) re_slugify = re.compile(r'[\W\s-]', re.UNICODE) - if isinstance(value, str): # Python3 str, Python2 unicode - value = re_slugify.sub('', value) - else: - value = unicode(re_slugify.sub('', value)) + value = re_slugify.sub('', value) if replace_whitespace: # *+:\"/<>? are replaced by _ value = re.sub(r'[*+:\\\"/<>?]+', u'_', value, flags=re.U) @@ -249,10 +251,7 @@ def get_valid_filename(value, replace_whitespace=True): value = value[:128].strip() if not value: raise ValueError("Filename cannot be empty") - if sys.version_info.major == 3: - return value - else: - return value.decode('utf-8') + return value def split_authors(values): @@ -330,11 +329,12 @@ def delete_book_file(book, calibrepath, book_format=None): except (IOError, OSError) as e: log.error("Deleting authorpath for book %s failed: %s", book.id, e) return True, None - else: - log.error("Deleting book %s failed, book path not valid: %s", book.id, book.path) - return True, _("Deleting book %(id)s, book path not valid: %(path)s", - id=book.id, - path=book.path) + + log.error("Deleting book %s from database only, book path in database not valid: %s", + book.id, book.path) + return True, _("Deleting book %(id)s from database only, book path in database not valid: %(path)s", + id=book.id, + path=book.path) # Moves files in file storage during author/title rename, or from temp dir to file storage @@ -383,7 +383,7 @@ def update_dir_structure_file(book_id, calibrepath, first_author, orignal_filepa # os.unlink(os.path.normcase(os.path.join(dir_name, file))) # change location in database to new author/title path localbook.path = os.path.join(new_authordir, new_titledir).replace('\\','/') - except OSError as ex: + except (OSError) as ex: log.error("Rename title from: %s to %s: %s", path, new_path, ex) log.debug(ex, exc_info=True) return _("Rename title from: '%(src)s' to '%(dest)s' failed with error: %(error)s", @@ -398,7 +398,7 @@ def update_dir_structure_file(book_id, calibrepath, first_author, orignal_filepa file_format.name = new_name if not orignal_filepath and len(os.listdir(os.path.dirname(path))) == 0: shutil.rmtree(os.path.dirname(path)) - except OSError as ex: + except (OSError) as ex: log.error("Rename file in path %s to %s: %s", new_path, new_name, ex) log.debug(ex, exc_info=True) return _("Rename file in path '%(src)s' to '%(dest)s' failed with error: %(error)s", @@ -481,8 +481,8 @@ def reset_password(user_id): password = generate_random_password() existing_user.password = generate_password_hash(password) ub.session.commit() - send_registration_mail(existing_user.email, existing_user.nickname, password, True) - return 1, existing_user.nickname + send_registration_mail(existing_user.email, existing_user.name, password, True) + return 1, existing_user.name except Exception: ub.session.rollback() return 0, None @@ -499,11 +499,37 @@ def generate_random_password(): def uniq(inpt): output = [] + inpt = [ " ".join(inp.split()) for inp in inpt] for x in inpt: if x not in output: output.append(x) return output +def check_email(email): + email = valid_email(email) + if ub.session.query(ub.User).filter(func.lower(ub.User.email) == email.lower()).first(): + log.error(u"Found an existing account for this e-mail address") + raise Exception(_(u"Found an existing account for this e-mail address")) + return email + + +def check_username(username): + username = username.strip() + if ub.session.query(ub.User).filter(func.lower(ub.User.name) == username.lower()).scalar(): + log.error(u"This username is already taken") + raise Exception (_(u"This username is already taken")) + return username + + +def valid_email(email): + email = email.strip() + # Regex according to https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/email#validation + if not re.search(r"^[\w.!#$%&'*+\\/=?^_`{|}~-]+@[\w](?:[\w-]{0,61}[\w])?(?:\.[\w](?:[\w-]{0,61}[\w])?)*$", + email): + log.error(u"Invalid e-mail address format") + raise Exception(_(u"Invalid e-mail address format")) + return email + # ################################# External interface ################################# @@ -740,6 +766,7 @@ def do_download_file(book, book_format, client, data, headers): # ToDo Check headers parameter for element in headers: response.headers[element[0]] = element[1] + log.info('Downloading file: {}'.format(os.path.join(filename, data.name + "." + book_format))) return response ################################## @@ -756,12 +783,11 @@ def check_unrar(unrarLocation): if sys.version_info < (3, 0): unrarLocation = unrarLocation.encode(sys.getfilesystemencoding()) unrarLocation = [unrarLocation] - for lines in process_wait(unrarLocation): - value = re.search('UNRAR (.*) freeware', lines, re.IGNORECASE) - if value: - version = value.group(1) - log.debug("unrar version %s", version) - break + value = process_wait(unrarLocation, pattern='UNRAR (.*) freeware') + if value: + version = value.group(1) + log.debug("unrar version %s", version) + except (OSError, UnicodeDecodeError) as err: log.debug_or_exception(err) return _('Error excecuting UnRar') @@ -779,7 +805,6 @@ def json_serial(obj): 'seconds': obj.seconds, 'microseconds': obj.microseconds, } - # return obj.isoformat() raise TypeError("Type %s not serializable" % type(obj)) @@ -804,7 +829,7 @@ def format_runtime(runtime): def render_task_status(tasklist): renderedtasklist = list() for __, user, __, task in tasklist: - if user == current_user.nickname or current_user.role_admin(): + if user == current_user.name or current_user.role_admin(): ret = {} if task.start_time: ret['starttime'] = format_datetime(task.start_time, format='short', locale=get_locale()) @@ -825,7 +850,7 @@ def render_task_status(tasklist): ret['taskMessage'] = "{}: {}".format(_(task.name), task.message) ret['progress'] = "{} %".format(int(task.progress * 100)) - ret['user'] = user + ret['user'] = escape(user) # prevent xss renderedtasklist.append(ret) return renderedtasklist @@ -842,8 +867,8 @@ def tags_filters(): # checks if domain is in database (including wildcards) # example SELECT * FROM @TABLE WHERE 'abcdefg' LIKE Name; # from https://code.luasoftware.com/tutorials/flask/execute-raw-sql-in-flask-sqlalchemy/ +# in all calls the email address is checked for validity def check_valid_domain(domain_text): - # domain_text = domain_text.split('@', 1)[-1].lower() sql = "SELECT * FROM registration WHERE (:domain LIKE domain and allow = 1);" result = ub.session.query(ub.Registration).from_statement(text(sql)).params(domain=domain_text).all() if not len(result): @@ -877,6 +902,7 @@ def get_download_link(book_id, book_format, client): if book: data1 = calibre_db.get_book_format(book.id, book_format.upper()) else: + log.error("Book id {} not found for downloading".format(book_id)) abort(404) if data1: # collect downloaded books only for registered user and not for anonymous user @@ -884,8 +910,8 @@ def get_download_link(book_id, book_format, client): ub.update_download(book_id, int(current_user.id)) file_name = book.title if len(book.authors) > 0: - file_name = book.authors[0].name + '_' + file_name - file_name = get_valid_filename(file_name) + file_name = file_name + ' - ' + book.authors[0].name + file_name = get_valid_filename(file_name, replace_whitespace=False) headers = Headers() headers["Content-Type"] = mimetypes.types_map.get('.' + book_format, "application/octet-stream") headers["Content-Disposition"] = "attachment; filename=%s.%s; filename*=UTF-8''%s.%s" % ( diff --git a/cps/isoLanguages.py b/cps/isoLanguages.py index e447a623..35d9f0a7 100644 --- a/cps/isoLanguages.py +++ b/cps/isoLanguages.py @@ -63,7 +63,7 @@ def get_language_codes(locale, language_names, remainder=None): if v in language_names: lang.append(k) language_names.remove(v) - if remainder is not None: + if remainder is not None and language_names: remainder.extend(language_names) return lang diff --git a/cps/iso_language_names.py b/cps/iso_language_names.py index 98368449..73e6f326 100644 --- a/cps/iso_language_names.py +++ b/cps/iso_language_names.py @@ -5007,6 +5007,379 @@ LANGUAGE_NAMES = { "zxx": "brak kontekstu jÄ™zykowego", "zza": "zazaki" }, + "pt_BR": { + "abk": "Abcázio", + "ace": "Achém", + "ach": "Acoli", + "ada": "Adangme", + "ady": "Adyghe", + "aar": "Afar", + "afh": "Afrihili", + "afr": "Africânder", + "ain": "Ainu (Japão)", + "aka": "Akan", + "akk": "Acadiano", + "sqi": "Albanês", + "ale": "Aleúte", + "amh": "Amárico", + "anp": "Angika", + "ara": "Arabic", + "arg": "Aragonese", + "arp": "Arapaho", + "arw": "Arawak", + "hye": "Armênio", + "asm": "Assamese", + "ast": "Asturian", + "ava": "Avaric", + "ave": "Avestan", + "awa": "Awadhi", + "aym": "Aymara", + "aze": "Azerbaijano", + "ban": "Balinês", + "bal": "Balúchi", + "bam": "Bambara", + "bas": "Basa (Cameroon)", + "bak": "Bashkir", + "eus": "Basque", + "bej": "Beja", + "bel": "Belarusian", + "bem": "Bemba (Zambia)", + "ben": "Bengali", + "bho": "Bhojpuri", + "bik": "Bikol", + "byn": "Bilin", + "bin": "Bini", + "bis": "Bislama", + "zbl": "Blissymbols", + "bos": "Bosnian", + "bra": "Braj", + "bre": "Bretão", + "bug": "Buginese", + "bul": "Búlgaro", + "bua": "Buriat", + "mya": "Birmanês", + "cad": "Caddo", + "cat": "Catalão", + "ceb": "Cebuano", + "chg": "Chagatai", + "cha": "Chamorro", + "che": "Chechen", + "chr": "Cheroqui", + "chy": "Cheyenne", + "chb": "Chibcha", + "zho": "Chinês", + "chn": "Chinook jargon", + "chp": "Chipewyan", + "cho": "Choctaw", + "chk": "Chuukese", + "chv": "Chuvash", + "cop": "Coptic", + "cor": "Cornish", + "cos": "Corsican", + "cre": "Cree", + "mus": "Creek", + "hrv": "Croata", + "ces": "Czech", + "dak": "Dacota", + "dan": "Danish", + "dar": "Dargwa", + "del": "Delaware", + "div": "Dhivehi", + "din": "Dinka", + "doi": "Dogri (macrolanguage)", + "dgr": "Dogrib", + "dua": "Duala", + "nld": "Holandês", + "dyu": "Dyula", + "dzo": "Dzongkha", + "efi": "Efik", + "egy": "Egyptian (Ancient)", + "eka": "Ekajuk", + "elx": "Elamite", + "eng": "Inglês", + "myv": "Erzya", + "epo": "Esperanto", + "est": "Estónio", + "ewe": "Ewe", + "ewo": "Ewondo", + "fan": "Fang (Equatorial Guinea)", + "fat": "Fanti", + "fao": "Faroese", + "fij": "Fijian", + "fil": "Filipino", + "fin": "Finlandês", + "fon": "Fon", + "fra": "Francês", + "fur": "Friuliano", + "ful": "Fulah", + "gaa": "Ga", + "glg": "Galician", + "lug": "Ganda", + "gay": "Gayo", + "gba": "Gbaya (Central African Republic)", + "gez": "Geez", + "kat": "Georgiano", + "deu": "Alemão", + "gil": "Gilbertês", + "gon": "Gondi", + "gor": "Gorontalo", + "got": "Gótico", + "grb": "Grebo", + "grn": "Guarani", + "guj": "Guzerate", + "gwi": "Gwichʼin", + "hai": "Haida", + "hau": "Hauçá", + "haw": "Havaiano", + "heb": "Hebraico", + "her": "Herero", + "hil": "Hiligaynon", + "hin": "Hindi", + "hmo": "Hiri Motu", + "hit": "Hitita", + "hmn": "Hmong", + "hun": "Húngaro", + "hup": "Hupa", + "iba": "Iban", + "isl": "Islandês", + "ido": "Ido", + "ibo": "Igbo", + "ilo": "Ilocano", + "ind": "Indonésio", + "inh": "Ingush", + "ina": "Interlingua (International Auxiliary Language Association)", + "ile": "Interlingue", + "iku": "Inuktitut", + "ipk": "Inupiaq", + "gle": "Irlandês", + "ita": "Italiano", + "jpn": "Japanese", + "jav": "Javanês", + "jrb": "Judeo-Arabic", + "jpr": "Judeo-Persian", + "kbd": "Kabardian", + "kab": "Kabyle", + "kac": "Kachin", + "kal": "Kalaallisut", + "xal": "Kalmyk", + "kam": "Kamba (Quênia)", + "kan": "Canarês", + "kau": "Kanuri", + "kaa": "Kara-Kalpak", + "krc": "Karachay-Balkar", + "krl": "Karelian", + "kas": "Kashmiri", + "csb": "Kashubian", + "kaw": "Kawi", + "kaz": "Cazaque", + "kha": "Khasi", + "kho": "Khotanese", + "kik": "Quicuio", + "kmb": "Quimbundo", + "kin": "Kinyarwanda", + "kir": "Quirguiz", + "tlh": "Klingon", + "kom": "Komi", + "kon": "Quicongo", + "kok": "Konkani (macrolanguage)", + "kor": "Coreano", + "kos": "Kosraean", + "kpe": "Kpelle", + "kua": "Kuanyama", + "kum": "Kumyk", + "kur": "Kurdish", + "kru": "Kurukh", + "kut": "Kutenai", + "lad": "Ladino", + "lah": "Lahnda", + "lam": "Lamba", + "lao": "Laosiano", + "lat": "Latin", + "lav": "Letão", + "lez": "Lezghian", + "lim": "Limburgan", + "lin": "Lingala", + "lit": "Lituano", + "jbo": "Lojban", + "loz": "Lozi", + "lub": "Luba-Catanga", + "lua": "Luba-Lulua", + "lui": "Luiseno", + "smj": "Lule Sami", + "lun": "Lunda", + "luo": "Luo (Kenya and Tanzania)", + "lus": "Lushai", + "ltz": "Luxembourgish", + "mkd": "Macedónio", + "mad": "Madurese", + "mag": "Magahi", + "mai": "Maithili", + "mak": "Makasar", + "mlg": "Malgaxe", + "msa": "Malay (macrolanguage)", + "mal": "Malayalam", + "mlt": "Maltese", + "mnc": "Manchu", + "mdr": "Mandar", + "man": "Mandinga", + "mni": "Manipuri", + "glv": "Manx", + "mri": "Maori", + "arn": "Mapudungun", + "mar": "Marata", + "chm": "Mari (Russia)", + "mah": "Marshallese", + "mwr": "Marwari", + "mas": "Masai", + "men": "Mende (Sierra Leone)", + "mic": "Mi'kmaq", + "min": "Minangkabau", + "mwl": "Mirandês", + "moh": "Mohawk", + "mdf": "Mocsa", + "lol": "Mongo", + "mon": "Mongolian", + "mos": "Mossi", + "mul": "Múltiplos idiomas", + "nqo": "N'Ko", + "nau": "Nauruano", + "nav": "Navajo", + "ndo": "Ndonga", + "nap": "Neapolitan", + "nia": "Nias", + "niu": "Niueano", + "zxx": "Sem conteúdo linguistico", + "nog": "Nogai", + "nor": "Norueguês", + "nob": "Norueguês, Dano", + "nno": "Norueguês, Novo", + "nym": "Nyamwezi", + "nya": "Nyanja", + "nyn": "Nyankole", + "nyo": "Nyoro", + "nzi": "Nzima", + "oci": "Occitan (post 1500)", + "oji": "Ojibwa", + "orm": "Oromo", + "osa": "Osage", + "oss": "Ossetian", + "pal": "Pálavi", + "pau": "Palauano", + "pli": "Pali", + "pam": "Pampanga", + "pag": "Pangasinense", + "pan": "Panjabi", + "pap": "Papiamento", + "fas": "Persian", + "phn": "Fenício", + "pon": "Pohnpeian", + "pol": "Polaco", + "por": "Português", + "pus": "Pushto", + "que": "Quíchua", + "raj": "Rajastani", + "rap": "Rapanui", + "ron": "Romeno", + "roh": "Romansh", + "rom": "Romany", + "run": "Rundi", + "rus": "Russo", + "smo": "Samoan", + "sad": "Sandawe", + "sag": "Sango", + "san": "Sanskrit", + "sat": "Santali", + "srd": "Sardinian", + "sas": "Sasak", + "sco": "Scots", + "sel": "Selkup", + "srp": "Sérvio", + "srr": "Serere", + "shn": "Shan", + "sna": "Shona", + "scn": "Sicilian", + "sid": "Sidamo", + "bla": "Siksika", + "snd": "Sindi", + "sin": "Cingalês", + "den": "Slave (Athapascan)", + "slk": "Eslovaco", + "slv": "Esloveno", + "sog": "Sogdian", + "som": "Somali", + "snk": "Soninke", + "spa": "Espanhol", + "srn": "Sranan Tongo", + "suk": "Sukuma", + "sux": "Sumerian", + "sun": "Sudanês", + "sus": "Sosso", + "swa": "Swahili (macrolanguage)", + "ssw": "Swati", + "swe": "Sueco", + "syr": "Siríaco", + "tgl": "Tagaloge", + "tah": "Tahitian", + "tgk": "Tajik", + "tmh": "Tamaxeque", + "tam": "Tamil", + "tat": "Tatar", + "tel": "Telugu", + "ter": "Tereno", + "tet": "Tétum", + "tha": "Tailandês", + "bod": "Tibetano", + "tig": "Tigre", + "tir": "Tigrinya", + "tem": "Timne", + "tiv": "Tiv", + "tli": "Tlingit", + "tpi": "Tok Pisin", + "tkl": "Toquelauano", + "tog": "Toganês (Nyasa)", + "ton": "Tonga (ilhas tonga)", + "tsi": "Tsimshian", + "tso": "Tsonga", + "tsn": "Tswana", + "tum": "Tumbuka", + "tur": "Turco", + "tuk": "Turcomano", + "tvl": "Tuvaluano", + "tyv": "Tuvinian", + "twi": "Twi", + "udm": "Udmurt", + "uga": "Ugarítico", + "uig": "Uighur", + "ukr": "Ucraniano", + "umb": "Umbundu", + "mis": "Idiomas sem código", + "und": "Não identificável", + "urd": "Urdu", + "uzb": "Usbeque", + "vai": "Vai", + "ven": "Venda", + "vie": "Vietnamita", + "vol": "Volapük", + "vot": "Votic", + "wln": "Walloon", + "war": "Waray (Philippines)", + "was": "Washo", + "cym": "Galês", + "wal": "Wolaytta", + "wol": "Uolofe", + "xho": "Xosa", + "sah": "Iacuto", + "yao": "Iao", + "yap": "Yapese", + "yid": "Ãdiche", + "yor": "Iorubá", + "zap": "Zapoteca", + "zza": "Zaza", + "zen": "Zenaga", + "zha": "Zhuang", + "zul": "Zulu", + "zun": "Zuni" + }, "ru": { "aar": "Ðфар", "abk": "ÐбхазÑкий", diff --git a/cps/jinjia.py b/cps/jinjia.py index b2479adc..ac6d3d33 100644 --- a/cps/jinjia.py +++ b/cps/jinjia.py @@ -31,7 +31,7 @@ from babel.dates import format_date from flask import Blueprint, request, url_for from flask_babel import get_locale from flask_login import current_user - +from markupsafe import escape from . import logger @@ -82,7 +82,7 @@ def formatdate_filter(val): except AttributeError as e: log.error('Babel error: %s, Current user locale: %s, Current User: %s', e, current_user.locale, - current_user.nickname + current_user.name ) return val @@ -113,21 +113,25 @@ def yesno(value, yes, no): @jinjia.app_template_filter('formatfloat') def formatfloat(value, decimals=1): - formatedstring = '%d' % value - if (value % 1) != 0: - formatedstring = ('%s.%d' % (formatedstring, (value % 1) * 10**decimals)).rstrip('0') - return formatedstring + value = 0 if not value else value + return ('{0:.' + str(decimals) + 'f}').format(value).rstrip('0').rstrip('.') @jinjia.app_template_filter('formatseriesindex') def formatseriesindex_filter(series_index): if series_index: - if int(series_index) - series_index == 0: - return int(series_index) - else: + try: + if int(series_index) - series_index == 0: + return int(series_index) + else: + return series_index + except ValueError: return series_index return 0 +@jinjia.app_template_filter('escapedlink') +def escapedlink_filter(url, text): + return "{}".format(url, escape(text)) @jinjia.app_template_filter('uuidfilter') def uuidfilter(var): diff --git a/cps/kobo.py b/cps/kobo.py index a9dd8865..6952a692 100644 --- a/cps/kobo.py +++ b/cps/kobo.py @@ -42,11 +42,13 @@ from flask import ( from flask_login import current_user from werkzeug.datastructures import Headers from sqlalchemy import func -from sqlalchemy.sql.expression import and_ +from sqlalchemy.sql.expression import and_, or_ 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 +from .constants import sqlalchemy_version2 from .helper import get_download_link from .services import SyncToken as SyncToken from .web import download_required @@ -81,6 +83,7 @@ CONNECTION_SPECIFIC_HEADERS = [ "transfer-encoding", ] + def get_kobo_activated(): return config.config_kobo_sync @@ -135,6 +138,7 @@ def convert_to_kobo_timestamp_string(timestamp): def HandleSyncRequest(): sync_token = SyncToken.SyncToken.from_headers(request.headers) log.info("Kobo library sync request received.") + log.debug("SyncToken: {}".format(sync_token)) if not current_app.wsgi_app.is_proxied: log.debug('Kobo: Received unproxied request, changed request port to external server port') @@ -151,33 +155,60 @@ def HandleSyncRequest(): # in case of external changes (e.g: adding a book through Calibre). calibre_db.reconnect_db(config, ub.app_DB_path) - if sync_token.books_last_id > -1: - changed_entries = ( - calibre_db.session.query(db.Books, ub.ArchivedBook.last_modified, ub.ArchivedBook.is_archived) - .join(db.Data).outerjoin(ub.ArchivedBook, db.Books.id == ub.ArchivedBook.book_id) - .filter(db.Books.last_modified >= sync_token.books_last_modified) - .filter(db.Books.id>sync_token.books_last_id) - .filter(db.Data.format.in_(KOBO_FORMATS)) - .order_by(db.Books.last_modified) - .order_by(db.Books.id) - .limit(SYNC_ITEM_LIMIT) + only_kobo_shelves = current_user.kobo_only_shelves_sync + + if only_kobo_shelves: + if sqlalchemy_version2: + changed_entries = select(db.Books, + ub.ArchivedBook.last_modified, + ub.BookShelf.date_added, + ub.ArchivedBook.is_archived) + else: + changed_entries = calibre_db.session.query(db.Books, + ub.ArchivedBook.last_modified, + ub.BookShelf.date_added, + ub.ArchivedBook.is_archived) + changed_entries = (changed_entries + .join(db.Data).outerjoin(ub.ArchivedBook, db.Books.id == ub.ArchivedBook.book_id) + .filter(or_(db.Books.last_modified > sync_token.books_last_modified, + ub.BookShelf.date_added > sync_token.books_last_modified)) + .filter(db.Data.format.in_(KOBO_FORMATS)).filter(calibre_db.common_filters()) + .order_by(db.Books.id) + .order_by(ub.ArchivedBook.last_modified) + .join(ub.BookShelf, db.Books.id == ub.BookShelf.book_id) + .join(ub.Shelf) + .filter(ub.Shelf.user_id == current_user.id) + .filter(ub.Shelf.kobo_sync) + .distinct() ) else: - changed_entries = ( - calibre_db.session.query(db.Books, ub.ArchivedBook.last_modified, ub.ArchivedBook.is_archived) - .join(db.Data).outerjoin(ub.ArchivedBook, db.Books.id == ub.ArchivedBook.book_id) - .filter(db.Books.last_modified > sync_token.books_last_modified) - .filter(db.Data.format.in_(KOBO_FORMATS)) - .order_by(db.Books.last_modified) - .order_by(db.Books.id) - .limit(SYNC_ITEM_LIMIT) + if sqlalchemy_version2: + changed_entries = select(db.Books, ub.ArchivedBook.last_modified, ub.ArchivedBook.is_archived) + else: + changed_entries = calibre_db.session.query(db.Books, + ub.ArchivedBook.last_modified, + ub.ArchivedBook.is_archived) + changed_entries = (changed_entries + .join(db.Data).outerjoin(ub.ArchivedBook, db.Books.id == ub.ArchivedBook.book_id) + .filter(db.Books.last_modified > sync_token.books_last_modified) + .filter(calibre_db.common_filters()) + .filter(db.Data.format.in_(KOBO_FORMATS)) + .order_by(db.Books.last_modified) + .order_by(db.Books.id) ) + if sync_token.books_last_id > -1: + changed_entries = changed_entries.filter(db.Books.id > sync_token.books_last_id) + reading_states_in_new_entitlements = [] - for book in changed_entries: + if sqlalchemy_version2: + books = calibre_db.session.execute(changed_entries.limit(SYNC_ITEM_LIMIT)) + else: + books = changed_entries.limit(SYNC_ITEM_LIMIT) + for book in books: formats = [data.format for data in book.Books.data] if not 'KEPUB' in formats and config.config_kepubifypath and 'EPUB' in formats: - helper.convert_book_format(book.Books.id, config.config_calibre_dir, 'EPUB', 'KEPUB', current_user.nickname) + helper.convert_book_format(book.Books.id, config.config_calibre_dir, 'EPUB', 'KEPUB', current_user.name) kobo_reading_state = get_or_create_reading_state(book.Books.id) entitlement = { @@ -190,7 +221,14 @@ def HandleSyncRequest(): new_reading_state_last_modified = max(new_reading_state_last_modified, kobo_reading_state.last_modified) reading_states_in_new_entitlements.append(book.Books.id) - if book.Books.timestamp > sync_token.books_last_created: + ts_created = book.Books.timestamp + + try: + ts_created = max(ts_created, book.date_added) + except AttributeError: + pass + + if ts_created > sync_token.books_last_created: sync_results.append({"NewEntitlement": entitlement}) else: sync_results.append({"ChangedEntitlement": entitlement}) @@ -198,35 +236,59 @@ def HandleSyncRequest(): new_books_last_modified = max( book.Books.last_modified, new_books_last_modified ) - new_books_last_created = max(book.Books.timestamp, new_books_last_created) + try: + new_books_last_modified = max( + new_books_last_modified, book.date_added + ) + except AttributeError: + pass - max_change = (changed_entries - .from_self() - .filter(ub.ArchivedBook.is_archived) - .order_by(func.datetime(ub.ArchivedBook.last_modified).desc()) - .first() - ) - if max_change: - max_change = max_change.last_modified + new_books_last_created = max(ts_created, new_books_last_created) + + if sqlalchemy_version2: + max_change = calibre_db.session.execute(changed_entries + .filter(ub.ArchivedBook.is_archived) + .order_by(func.datetime(ub.ArchivedBook.last_modified).desc()))\ + .columns(db.Books).first() else: - max_change = new_archived_last_modified + max_change = changed_entries.from_self().filter(ub.ArchivedBook.is_archived) \ + .order_by(func.datetime(ub.ArchivedBook.last_modified).desc()).first() + + max_change = max_change.last_modified if max_change else new_archived_last_modified + new_archived_last_modified = max(new_archived_last_modified, max_change) # no. of books returned - book_count = changed_entries.count() - - # last entry: - if book_count: - books_last_id = changed_entries.all()[-1].Books.id or -1 + if sqlalchemy_version2: + entries = calibre_db.session.execute(changed_entries).all() + book_count = len(entries) else: - books_last_id = -1 + entries = changed_entries.all() + book_count = changed_entries.count() + # last entry: + books_last_id = entries[-1].Books.id or -1 if book_count else -1 # generate reading state data - changed_reading_states = ( - ub.session.query(ub.KoboReadingState) - .filter(and_(func.datetime(ub.KoboReadingState.last_modified) > sync_token.reading_state_last_modified, - ub.KoboReadingState.user_id == current_user.id, - ub.KoboReadingState.book_id.notin_(reading_states_in_new_entitlements)))) + changed_reading_states = ub.session.query(ub.KoboReadingState) + + if only_kobo_shelves: + changed_reading_states = changed_reading_states.join(ub.BookShelf, + ub.KoboReadingState.book_id == ub.BookShelf.book_id)\ + .join(ub.Shelf)\ + .filter(current_user.id == ub.Shelf.user_id)\ + .filter(ub.Shelf.kobo_sync, + or_( + func.datetime(ub.KoboReadingState.last_modified) > sync_token.reading_state_last_modified, + func.datetime(ub.BookShelf.date_added) > sync_token.books_last_modified + )).distinct() + else: + changed_reading_states = changed_reading_states.filter( + func.datetime(ub.KoboReadingState.last_modified) > sync_token.reading_state_last_modified) + + changed_reading_states = changed_reading_states.filter( + and_(ub.KoboReadingState.user_id == current_user.id, + ub.KoboReadingState.book_id.notin_(reading_states_in_new_entitlements))) + for kobo_reading_state in changed_reading_states.all(): book = calibre_db.session.query(db.Books).filter(db.Books.id == kobo_reading_state.book_id).one_or_none() if book: @@ -237,7 +299,7 @@ def HandleSyncRequest(): }) new_reading_state_last_modified = max(new_reading_state_last_modified, kobo_reading_state.last_modified) - sync_shelves(sync_token, sync_results) + sync_shelves(sync_token, sync_results, only_kobo_shelves) sync_token.books_last_created = new_books_last_created sync_token.books_last_modified = new_books_last_modified @@ -262,12 +324,13 @@ def generate_sync_response(sync_token, sync_results, set_cont=False): extra_headers["x-kobo-sync-mode"] = store_response.headers.get("x-kobo-sync-mode") extra_headers["x-kobo-recent-reads"] = store_response.headers.get("x-kobo-recent-reads") - except Exception as e: - log.error("Failed to receive or parse response from Kobo's sync endpoint: " + str(e)) + except Exception as ex: + log.error("Failed to receive or parse response from Kobo's sync endpoint: {}".format(ex)) if set_cont: extra_headers["x-kobo-sync"] = "continue" sync_token.to_headers(extra_headers) + log.debug("Kobo Sync Content: {}".format(sync_results)) response = make_response(jsonify(sync_results), extra_headers) return response @@ -305,7 +368,8 @@ def get_download_url_for_book(book, book_format): book_format=book_format.lower() ) return url_for( - "web.download_link", + "kobo.download_book", + auth_token=kobo_auth.get_auth_token(), book_id=book.id, book_format=book_format.lower(), _external=True, @@ -391,7 +455,7 @@ def get_metadata(book): book_uuid = book.uuid metadata = { - "Categories": ["00000000-0000-0000-0000-000000000001",], + "Categories": ["00000000-0000-0000-0000-000000000001", ], # "Contributors": get_author(book), "CoverImageId": book_uuid, "CrossRevisionId": book_uuid, @@ -598,13 +662,14 @@ def HandleTagRemoveItem(tag_id): # Add new, changed, or deleted shelves to the sync_results. # Note: Public shelves that aren't owned by the user aren't supported. -def sync_shelves(sync_token, sync_results): +def sync_shelves(sync_token, sync_results, only_kobo_shelves=False): new_tags_last_modified = sync_token.tags_last_modified - for shelf in ub.session.query(ub.ShelfArchive).filter(func.datetime(ub.ShelfArchive.last_modified) > sync_token.tags_last_modified, - ub.ShelfArchive.user_id == current_user.id): + for shelf in ub.session.query(ub.ShelfArchive).filter( + func.datetime(ub.ShelfArchive.last_modified) > sync_token.tags_last_modified, + ub.ShelfArchive.user_id == current_user.id + ): new_tags_last_modified = max(shelf.last_modified, new_tags_last_modified) - sync_results.append({ "DeletedTag": { "Tag": { @@ -614,8 +679,40 @@ def sync_shelves(sync_token, sync_results): } }) - for shelf in ub.session.query(ub.Shelf).filter(func.datetime(ub.Shelf.last_modified) > sync_token.tags_last_modified, - ub.Shelf.user_id == current_user.id): + extra_filters = [] + if only_kobo_shelves: + for shelf in ub.session.query(ub.Shelf).filter( + func.datetime(ub.Shelf.last_modified) > sync_token.tags_last_modified, + ub.Shelf.user_id == current_user.id, + not ub.Shelf.kobo_sync + ): + sync_results.append({ + "DeletedTag": { + "Tag": { + "Id": shelf.uuid, + "LastModified": convert_to_kobo_timestamp_string(shelf.last_modified) + } + } + }) + extra_filters.append(ub.Shelf.kobo_sync) + + if sqlalchemy_version2: + shelflist = ub.session.execute(select(ub.Shelf).outerjoin(ub.BookShelf).filter( + or_(func.datetime(ub.Shelf.last_modified) > sync_token.tags_last_modified, + func.datetime(ub.BookShelf.date_added) > sync_token.tags_last_modified), + ub.Shelf.user_id == current_user.id, + *extra_filters + ).distinct().order_by(func.datetime(ub.Shelf.last_modified).asc())).columns(ub.Shelf) + else: + shelflist = ub.session.query(ub.Shelf).outerjoin(ub.BookShelf).filter( + or_(func.datetime(ub.Shelf.last_modified) > sync_token.tags_last_modified, + func.datetime(ub.BookShelf.date_added) > sync_token.tags_last_modified), + ub.Shelf.user_id == current_user.id, + *extra_filters + ).distinct().order_by(func.datetime(ub.Shelf.last_modified).asc()) + + + for shelf in shelflist: if not shelf_lib.check_shelf_view_permissions(shelf): continue diff --git a/cps/kobo_auth.py b/cps/kobo_auth.py index 23f60fe2..a51095c8 100644 --- a/cps/kobo_auth.py +++ b/cps/kobo_auth.py @@ -155,7 +155,7 @@ def generate_auth_token(user_id): for book in books: formats = [data.format for data in book.data] if not 'KEPUB' in formats and config.config_kepubifypath and 'EPUB' in formats: - helper.convert_book_format(book.id, config.config_calibre_dir, 'EPUB', 'KEPUB', current_user.nickname) + helper.convert_book_format(book.id, config.config_calibre_dir, 'EPUB', 'KEPUB', current_user.name) return render_title_template( "generate_kobo_auth_url.html", diff --git a/cps/logger.py b/cps/logger.py index b204de31..e2747f53 100644 --- a/cps/logger.py +++ b/cps/logger.py @@ -62,11 +62,11 @@ class _Logger(logging.Logger): def debug_no_auth(self, message, *args, **kwargs): + message = message.strip("\r\n") if message.startswith("send: AUTH"): - self.debug(message[:16], stacklevel=2, *args, **kwargs) + self.debug(message[:16], *args, **kwargs) else: - self.debug(message, stacklevel=2, *args, **kwargs) - + self.debug(message, *args, **kwargs) def get(name=None): @@ -153,11 +153,11 @@ def setup(log_file, log_level=None): file_handler.baseFilename = log_file else: try: - file_handler = RotatingFileHandler(log_file, maxBytes=50000, backupCount=2, encoding='utf-8') + file_handler = RotatingFileHandler(log_file, maxBytes=100000, backupCount=2, encoding='utf-8') except IOError: if log_file == DEFAULT_LOG_FILE: raise - file_handler = RotatingFileHandler(DEFAULT_LOG_FILE, maxBytes=50000, backupCount=2, encoding='utf-8') + file_handler = RotatingFileHandler(DEFAULT_LOG_FILE, maxBytes=100000, backupCount=2, encoding='utf-8') log_file = "" file_handler.setFormatter(FORMATTER) diff --git a/cps/metadata_provider/comicvine.py b/cps/metadata_provider/comicvine.py new file mode 100644 index 00000000..8f496608 --- /dev/null +++ b/cps/metadata_provider/comicvine.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- + +# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) +# Copyright (C) 2021 OzzieIsaacs +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# ComicVine api document: https://comicvine.gamespot.com/api/documentation + +import requests +from cps.services.Metadata import Metadata + + +class ComicVine(Metadata): + __name__ = "ComicVine" + __id__ = "comicvine" + + def search(self, query, __): + val = list() + apikey = "57558043c53943d5d1e96a9ad425b0eb85532ee6" + if self.active: + headers = { + 'User-Agent': 'Not Evil Browser' + } + + result = requests.get("https://comicvine.gamespot.com/api/search?api_key=" + + apikey + "&resources=issue&query=" + query + "&sort=name:desc&format=json", headers=headers) + for r in result.json()['results']: + seriesTitle = r['volume'].get('name', "") + if r.get('store_date'): + dateFomers = r.get('store_date') + else: + dateFomers = r.get('date_added') + v = dict() + v['id'] = r['id'] + v['title'] = seriesTitle + " #" + r.get('issue_number', "0") + " - " + ( r.get('name', "") or "") + v['authors'] = r.get('authors', []) + v['description'] = r.get('description', "") + v['publisher'] = "" + v['publishedDate'] = dateFomers + v['tags'] = ["Comics", seriesTitle] + v['rating'] = 0 + v['series'] = seriesTitle + v['cover'] = r['image'].get('original_url') + v['source'] = { + "id": self.__id__, + "description": "ComicVine Books", + "link": "https://comicvine.gamespot.com/" + } + v['url'] = r.get('site_detail_url', "") + val.append(v) + return val + + diff --git a/cps/metadata_provider/google.py b/cps/metadata_provider/google.py new file mode 100644 index 00000000..f3d02d8e --- /dev/null +++ b/cps/metadata_provider/google.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- + +# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) +# Copyright (C) 2021 OzzieIsaacs +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# Google Books api document: https://developers.google.com/books/docs/v1/using + + +import requests +from cps.services.Metadata import Metadata + +class Google(Metadata): + __name__ = "Google" + __id__ = "google" + + def search(self, query, __): + if self.active: + val = list() + result = requests.get("https://www.googleapis.com/books/v1/volumes?q="+query.replace(" ","+")) + for r in result.json()['items']: + v = dict() + v['id'] = r['id'] + v['title'] = r['volumeInfo']['title'] + v['authors'] = r['volumeInfo'].get('authors', []) + v['description'] = r['volumeInfo'].get('description', "") + v['publisher'] = r['volumeInfo'].get('publisher', "") + v['publishedDate'] = r['volumeInfo'].get('publishedDate', "") + v['tags'] = r['volumeInfo'].get('categories', []) + v['rating'] = r['volumeInfo'].get('averageRating', 0) + if r['volumeInfo'].get('imageLinks'): + v['cover'] = r['volumeInfo']['imageLinks']['thumbnail'].replace("http://", "https://") + else: + v['cover'] = "/../../../static/generic_cover.jpg" + v['source'] = { + "id": self.__id__, + "description": "Google Books", + "link": "https://books.google.com/"} + v['url'] = "https://books.google.com/books?id=" + r['id'] + val.append(v) + return val + + diff --git a/cps/metadata_provider/scholar.py b/cps/metadata_provider/scholar.py new file mode 100644 index 00000000..6e13c768 --- /dev/null +++ b/cps/metadata_provider/scholar.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- + +# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) +# Copyright (C) 2021 OzzieIsaacs +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from scholarly import scholarly + +from cps.services.Metadata import Metadata + + +class scholar(Metadata): + __name__ = "Google Scholar" + __id__ = "googlescholar" + + def search(self, query, generic_cover=""): + val = list() + if self.active: + scholar_gen = scholarly.search_pubs(' '.join(query.split('+'))) + i = 0 + for publication in scholar_gen: + v = dict() + v['id'] = "1234" # publication['bib'].get('title') + v['title'] = publication['bib'].get('title') + v['authors'] = publication['bib'].get('author', []) + v['description'] = publication['bib'].get('abstract', "") + v['publisher'] = publication['bib'].get('venue', "") + if publication['bib'].get('pub_year'): + v['publishedDate'] = publication['bib'].get('pub_year')+"-01-01" + else: + v['publishedDate'] = "" + v['tags'] = "" + v['ratings'] = 0 + v['series'] = "" + v['cover'] = generic_cover + v['url'] = publication.get('pub_url') or publication.get('eprint_url') or "", + v['source'] = { + "id": self.__id__, + "description": "Google Scholar", + "link": "https://scholar.google.com/" + } + val.append(v) + i += 1 + if (i >= 10): + break + return val + + + diff --git a/cps/oauth_bb.py b/cps/oauth_bb.py index 1fd7c9b1..c8cc2e3e 100644 --- a/cps/oauth_bb.py +++ b/cps/oauth_bb.py @@ -30,6 +30,7 @@ from flask_babel import gettext as _ from flask_dance.consumer import oauth_authorized, oauth_error from flask_dance.contrib.github import make_github_blueprint, github from flask_dance.contrib.google import make_google_blueprint, google +from oauthlib.oauth2 import TokenExpiredError, InvalidGrantError from flask_login import login_user, current_user, login_required from sqlalchemy.orm.exc import NoResultFound @@ -42,6 +43,7 @@ except NameError: oauth_check = {} +oauthblueprints = [] oauth = Blueprint('oauth', __name__) log = logger.create() @@ -87,7 +89,7 @@ def register_user_with_oauth(user=None): except NoResultFound: # no found, return error return - ub.session_commit("User {} with OAuth for provider {} registered".format(user.nickname, oauth_key)) + ub.session_commit("User {} with OAuth for provider {} registered".format(user.name, oauth_key)) def logout_oauth_user(): @@ -133,8 +135,8 @@ def bind_oauth_or_register(provider_id, provider_user_id, redirect_url, provider # already bind with user, just login if oauth_entry.user: login_user(oauth_entry.user) - log.debug(u"You are now logged in as: '%s'", oauth_entry.user.nickname) - flash(_(u"you are now logged in as: '%(nickname)s'", nickname= oauth_entry.user.nickname), + log.debug(u"You are now logged in as: '%s'", oauth_entry.user.name) + flash(_(u"you are now logged in as: '%(nickname)s'", nickname= oauth_entry.user.name), category="success") return redirect(url_for('web.index')) else: @@ -145,9 +147,10 @@ def bind_oauth_or_register(provider_id, provider_user_id, redirect_url, provider ub.session.add(oauth_entry) ub.session.commit() flash(_(u"Link to %(oauth)s Succeeded", oauth=provider_name), category="success") + log.info("Link to {} Succeeded".format(provider_name)) return redirect(url_for('web.profile')) - except Exception as e: - log.debug_or_exception(e) + except Exception as ex: + log.debug_or_exception(ex) ub.session.rollback() else: flash(_(u"Login failed, No User Linked With OAuth Account"), category="error") @@ -193,8 +196,9 @@ def unlink_oauth(provider): ub.session.commit() logout_oauth_user() flash(_(u"Unlink to %(oauth)s Succeeded", oauth=oauth_check[provider]), category="success") - except Exception as e: - log.debug_or_exception(e) + log.info("Unlink to {} Succeeded".format(oauth_check[provider])) + except Exception as ex: + log.debug_or_exception(ex) ub.session.rollback() flash(_(u"Unlink to %(oauth)s Failed", oauth=oauth_check[provider]), category="error") except NoResultFound: @@ -203,7 +207,6 @@ def unlink_oauth(provider): return redirect(url_for('web.profile')) def generate_oauth_blueprints(): - oauthblueprints = [] if not ub.session.query(ub.OAuthProvider).count(): for provider in ("github", "google"): oauthProvider = ub.OAuthProvider() @@ -257,11 +260,13 @@ if ub.oauth_support: def github_logged_in(blueprint, token): if not token: flash(_(u"Failed to log in with GitHub."), category="error") + log.error("Failed to log in with GitHub") return False resp = blueprint.session.get("/user") if not resp.ok: flash(_(u"Failed to fetch user info from GitHub."), category="error") + log.error("Failed to fetch user info from GitHub") return False github_info = resp.json() @@ -273,11 +278,13 @@ if ub.oauth_support: def google_logged_in(blueprint, token): if not token: flash(_(u"Failed to log in with Google."), category="error") + log.error("Failed to log in with Google") return False resp = blueprint.session.get("/oauth2/v2/userinfo") if not resp.ok: flash(_(u"Failed to fetch user info from Google."), category="error") + log.error("Failed to fetch user info from Google") return False google_info = resp.json() @@ -299,39 +306,6 @@ if ub.oauth_support: ) # ToDo: Translate flash(msg, category="error") - - @oauth.route('/link/github') - @oauth_required - def github_login(): - if not github.authorized: - return redirect(url_for('github.login')) - account_info = github.get('/user') - if account_info.ok: - account_info_json = account_info.json() - return bind_oauth_or_register(oauthblueprints[0]['id'], account_info_json['id'], 'github.login', 'github') - flash(_(u"GitHub Oauth error, please retry later."), category="error") - return redirect(url_for('web.login')) - - - @oauth.route('/unlink/github', methods=["GET"]) - @login_required - def github_login_unlink(): - return unlink_oauth(oauthblueprints[0]['id']) - - - @oauth.route('/link/google') - @oauth_required - def google_login(): - if not google.authorized: - return redirect(url_for("google.login")) - resp = google.get("/oauth2/v2/userinfo") - if resp.ok: - account_info_json = resp.json() - return bind_oauth_or_register(oauthblueprints[1]['id'], account_info_json['id'], 'google.login', 'google') - flash(_(u"Google Oauth error, please retry later."), category="error") - return redirect(url_for('web.login')) - - @oauth_error.connect_via(oauthblueprints[1]['blueprint']) def google_error(blueprint, error, error_description=None, error_uri=None): msg = ( @@ -346,7 +320,49 @@ if ub.oauth_support: flash(msg, category="error") - @oauth.route('/unlink/google', methods=["GET"]) - @login_required - def google_login_unlink(): - return unlink_oauth(oauthblueprints[1]['id']) +@oauth.route('/link/github') +@oauth_required +def github_login(): + if not github.authorized: + return redirect(url_for('github.login')) + try: + account_info = github.get('/user') + if account_info.ok: + account_info_json = account_info.json() + return bind_oauth_or_register(oauthblueprints[0]['id'], account_info_json['id'], 'github.login', 'github') + flash(_(u"GitHub Oauth error, please retry later."), category="error") + log.error("GitHub Oauth error, please retry later") + except (InvalidGrantError, TokenExpiredError) as e: + flash(_(u"GitHub Oauth error: {}").format(e), category="error") + log.error(e) + return redirect(url_for('web.login')) + + +@oauth.route('/unlink/github', methods=["GET"]) +@login_required +def github_login_unlink(): + return unlink_oauth(oauthblueprints[0]['id']) + + +@oauth.route('/link/google') +@oauth_required +def google_login(): + if not google.authorized: + return redirect(url_for("google.login")) + try: + resp = google.get("/oauth2/v2/userinfo") + if resp.ok: + account_info_json = resp.json() + return bind_oauth_or_register(oauthblueprints[1]['id'], account_info_json['id'], 'google.login', 'google') + flash(_(u"Google Oauth error, please retry later."), category="error") + log.error("Google Oauth error, please retry later") + except (InvalidGrantError, TokenExpiredError) as e: + flash(_(u"Google Oauth error: {}").format(e), category="error") + log.error(e) + return redirect(url_for('web.login')) + + +@oauth.route('/unlink/google', methods=["GET"]) +@login_required +def google_login_unlink(): + return unlink_oauth(oauthblueprints[1]['id']) diff --git a/cps/opds.py b/cps/opds.py index c66ee836..e444302a 100644 --- a/cps/opds.py +++ b/cps/opds.py @@ -27,7 +27,7 @@ from functools import wraps from flask import Blueprint, request, render_template, Response, g, make_response, abort from flask_login import current_user -from sqlalchemy.sql.expression import func, text, or_, and_ +from sqlalchemy.sql.expression import func, text, or_, and_, true from werkzeug.security import check_password_hash from . import constants, logger, config, db, calibre_db, ub, services, get_locale, isoLanguages @@ -94,7 +94,45 @@ def feed_cc_search(query): @opds.route("/opds/search", methods=["GET"]) @requires_basic_auth_if_no_ano def feed_normal_search(): - return feed_search(request.args.get("query").strip()) + return feed_search(request.args.get("query", "").strip()) + + +@opds.route("/opds/books") +@requires_basic_auth_if_no_ano +def feed_booksindex(): + shift = 0 + off = int(request.args.get("offset") or 0) + entries = calibre_db.session.query(func.upper(func.substr(db.Books.sort, 1, 1)).label('id'))\ + .filter(calibre_db.common_filters()).group_by(func.upper(func.substr(db.Books.sort, 1, 1))).all() + + elements = [] + if off == 0: + elements.append({'id': "00", 'name':_("All")}) + shift = 1 + for entry in entries[ + off + shift - 1: + int(off + int(config.config_books_per_page) - shift)]: + elements.append({'id': entry.id, 'name': entry.id}) + + pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, + len(entries) + 1) + return render_xml_template('feed.xml', + letterelements=elements, + folder='opds.feed_letter_books', + pagination=pagination) + + +@opds.route("/opds/books/letter/") +@requires_basic_auth_if_no_ano +def feed_letter_books(book_id): + off = request.args.get("offset") or 0 + letter = true() if book_id == "00" else func.upper(db.Books.sort).startswith(book_id) + entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), 0, + db.Books, + letter, + [db.Books.sort]) + + return render_xml_template('feed.xml', entries=entries, pagination=pagination) @opds.route("/opds/new") @@ -150,14 +188,41 @@ def feed_hot(): @opds.route("/opds/author") @requires_basic_auth_if_no_ano def feed_authorindex(): - off = request.args.get("offset") or 0 - entries = calibre_db.session.query(db.Authors).join(db.books_authors_link).join(db.Books)\ - .filter(calibre_db.common_filters())\ - .group_by(text('books_authors_link.author'))\ - .order_by(db.Authors.sort).limit(config.config_books_per_page)\ - .offset(off) + shift = 0 + off = int(request.args.get("offset") or 0) + entries = calibre_db.session.query(func.upper(func.substr(db.Authors.sort, 1, 1)).label('id'))\ + .join(db.books_authors_link).join(db.Books).filter(calibre_db.common_filters())\ + .group_by(func.upper(func.substr(db.Authors.sort, 1, 1))).all() + + elements = [] + if off == 0: + elements.append({'id': "00", 'name':_("All")}) + shift = 1 + for entry in entries[ + off + shift - 1: + int(off + int(config.config_books_per_page) - shift)]: + elements.append({'id': entry.id, 'name': entry.id}) + pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, - len(calibre_db.session.query(db.Authors).all())) + len(entries) + 1) + return render_xml_template('feed.xml', + letterelements=elements, + folder='opds.feed_letter_author', + pagination=pagination) + + +@opds.route("/opds/author/letter/") +@requires_basic_auth_if_no_ano +def feed_letter_author(book_id): + off = request.args.get("offset") or 0 + letter = true() if book_id == "00" else func.upper(db.Authors.sort).startswith(book_id) + entries = calibre_db.session.query(db.Authors).join(db.books_authors_link).join(db.Books)\ + .filter(calibre_db.common_filters()).filter(letter)\ + .group_by(text('books_authors_link.author'))\ + .order_by(db.Authors.sort) + pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, + entries.count()) + 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) @@ -201,17 +266,41 @@ def feed_publisher(book_id): @opds.route("/opds/category") @requires_basic_auth_if_no_ano def feed_categoryindex(): + shift = 0 + off = int(request.args.get("offset") or 0) + entries = calibre_db.session.query(func.upper(func.substr(db.Tags.name, 1, 1)).label('id'))\ + .join(db.books_tags_link).join(db.Books).filter(calibre_db.common_filters())\ + .group_by(func.upper(func.substr(db.Tags.name, 1, 1))).all() + elements = [] + if off == 0: + elements.append({'id': "00", 'name':_("All")}) + shift = 1 + for entry in entries[ + off + shift - 1: + int(off + int(config.config_books_per_page) - shift)]: + elements.append({'id': entry.id, 'name': entry.id}) + + pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, + len(entries) + 1) + return render_xml_template('feed.xml', + letterelements=elements, + folder='opds.feed_letter_category', + pagination=pagination) + +@opds.route("/opds/category/letter/") +@requires_basic_auth_if_no_ano +def feed_letter_category(book_id): off = request.args.get("offset") or 0 + letter = true() if book_id == "00" else func.upper(db.Tags.name).startswith(book_id) entries = calibre_db.session.query(db.Tags)\ .join(db.books_tags_link)\ .join(db.Books)\ - .filter(calibre_db.common_filters())\ + .filter(calibre_db.common_filters()).filter(letter)\ .group_by(text('books_tags_link.tag'))\ - .order_by(db.Tags.name)\ - .offset(off)\ - .limit(config.config_books_per_page) + .order_by(db.Tags.name) pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, - len(calibre_db.session.query(db.Tags).all())) + entries.count()) + 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) @@ -229,16 +318,40 @@ def feed_category(book_id): @opds.route("/opds/series") @requires_basic_auth_if_no_ano def feed_seriesindex(): + shift = 0 + off = int(request.args.get("offset") or 0) + entries = calibre_db.session.query(func.upper(func.substr(db.Series.sort, 1, 1)).label('id'))\ + .join(db.books_series_link).join(db.Books).filter(calibre_db.common_filters())\ + .group_by(func.upper(func.substr(db.Series.sort, 1, 1))).all() + elements = [] + if off == 0: + elements.append({'id': "00", 'name':_("All")}) + shift = 1 + for entry in entries[ + off + shift - 1: + int(off + int(config.config_books_per_page) - shift)]: + elements.append({'id': entry.id, 'name': entry.id}) + pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, + len(entries) + 1) + return render_xml_template('feed.xml', + letterelements=elements, + folder='opds.feed_letter_series', + pagination=pagination) + +@opds.route("/opds/series/letter/") +@requires_basic_auth_if_no_ano +def feed_letter_series(book_id): off = request.args.get("offset") or 0 + letter = true() if book_id == "00" else func.upper(db.Series.sort).startswith(book_id) entries = calibre_db.session.query(db.Series)\ .join(db.books_series_link)\ .join(db.Books)\ - .filter(calibre_db.common_filters())\ + .filter(calibre_db.common_filters()).filter(letter)\ .group_by(text('books_series_link.series'))\ - .order_by(db.Series.sort)\ - .offset(off).all() + .order_by(db.Series.sort) pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, - len(calibre_db.session.query(db.Series).all())) + entries.count()) + 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) @@ -269,7 +382,7 @@ def feed_ratingindex(): len(entries)) element = list() 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) @@ -428,13 +541,13 @@ def check_auth(username, password): username = username.encode('windows-1252') except UnicodeEncodeError: username = username.encode('utf-8') - user = ub.session.query(ub.User).filter(func.lower(ub.User.nickname) == + user = ub.session.query(ub.User).filter(func.lower(ub.User.name) == username.decode('utf-8').lower()).first() if bool(user and check_password_hash(str(user.password), password)): return True else: - ipAdress = request.headers.get('X-Forwarded-For', request.remote_addr) - log.warning('OPDS Login failed for user "%s" IP-address: %s', username.decode('utf-8'), ipAdress) + ip_Address = request.headers.get('X-Forwarded-For', request.remote_addr) + log.warning('OPDS Login failed for user "%s" IP-address: %s', username.decode('utf-8'), ip_Address) return False diff --git a/cps/remotelogin.py b/cps/remotelogin.py index d9e7388f..a9994f09 100644 --- a/cps/remotelogin.py +++ b/cps/remotelogin.py @@ -62,7 +62,7 @@ def remote_login(): ub.session_commit() verify_url = url_for('remotelogin.verify_token', token=auth_token.auth_token, _external=true) log.debug(u"Remot Login request with token: %s", auth_token.auth_token) - return render_title_template('remote_login.html', title=_(u"login"), token=auth_token.auth_token, + return render_title_template('remote_login.html', title=_(u"Login"), token=auth_token.auth_token, verify_url=verify_url, page="remotelogin") @@ -126,11 +126,11 @@ def token_verified(): login_user(user) ub.session.delete(auth_token) - ub.session_commit("User {} logged in via remotelogin, token deleted".format(user.nickname)) + ub.session_commit("User {} logged in via remotelogin, token deleted".format(user.name)) data['status'] = 'success' log.debug(u"Remote Login for userid %s succeded", user.id) - flash(_(u"you are now logged in as: '%(nickname)s'", nickname=user.nickname), category="success") + flash(_(u"you are now logged in as: '%(nickname)s'", nickname=user.name), category="success") response = make_response(json.dumps(data, ensure_ascii=False)) response.headers["Content-Type"] = "application/json; charset=utf-8" diff --git a/cps/render_template.py b/cps/render_template.py index fdd8abb7..7cd341ea 100644 --- a/cps/render_template.py +++ b/cps/render_template.py @@ -42,10 +42,16 @@ def get_sidebar_config(kwargs=None): sidebar.append({"glyph": "glyphicon-fire", "text": _('Hot Books'), "link": 'web.books_list', "id": "hot", "visibility": constants.SIDEBAR_HOT, 'public': True, "page": "hot", "show_text": _('Show Hot Books'), "config_show": True}) - sidebar.append({"glyph": "glyphicon-download", "text": _('Downloaded Books'), "link": 'web.books_list', - "id": "download", "visibility": constants.SIDEBAR_DOWNLOAD, 'public': (not g.user.is_anonymous), - "page": "download", "show_text": _('Show Downloaded Books'), - "config_show": content}) + if current_user.role_admin(): + sidebar.append({"glyph": "glyphicon-download", "text": _('Downloaded Books'), "link": 'web.download_list', + "id": "download", "visibility": constants.SIDEBAR_DOWNLOAD, 'public': (not g.user.is_anonymous), + "page": "download", "show_text": _('Show Downloaded Books'), + "config_show": content}) + else: + sidebar.append({"glyph": "glyphicon-download", "text": _('Downloaded Books'), "link": 'web.books_list', + "id": "download", "visibility": constants.SIDEBAR_DOWNLOAD, 'public': (not g.user.is_anonymous), + "page": "download", "show_text": _('Show Downloaded Books'), + "config_show": content}) sidebar.append( {"glyph": "glyphicon-star", "text": _('Top Rated Books'), "link": 'web.books_list', "id": "rated", "visibility": constants.SIDEBAR_BEST_RATED, 'public': True, "page": "rated", @@ -59,7 +65,7 @@ def get_sidebar_config(kwargs=None): "show_text": _('Show unread'), "config_show": False}) sidebar.append({"glyph": "glyphicon-random", "text": _('Discover'), "link": 'web.books_list', "id": "rand", "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", "visibility": constants.SIDEBAR_CATEGORY, 'public': True, "page": "category", "show_text": _('Show category selection'), "config_show": True}) diff --git a/cps/search_metadata.py b/cps/search_metadata.py new file mode 100644 index 00000000..72e77cdd --- /dev/null +++ b/cps/search_metadata.py @@ -0,0 +1,118 @@ +# -*- coding: utf-8 -*- + +# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) +# Copyright (C) 2021 OzzieIsaacs +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from __future__ import division, print_function, unicode_literals +import os +import json +import importlib +import sys +import inspect +import datetime +import concurrent.futures + +from flask import Blueprint, request, Response, url_for +from flask_login import current_user +from flask_login import login_required +from sqlalchemy.orm.attributes import flag_modified +from sqlalchemy.exc import OperationalError, InvalidRequestError + +from . import constants, logger, ub +from cps.services.Metadata import Metadata + + +meta = Blueprint('metadata', __name__) + +log = logger.create() + +new_list = list() +meta_dir = os.path.join(constants.BASE_DIR, "cps", "metadata_provider") +modules = os.listdir(os.path.join(constants.BASE_DIR, "cps", "metadata_provider")) +for f in modules: + if os.path.isfile(os.path.join(meta_dir, f)) and not f.endswith('__init__.py'): + a = os.path.basename(f)[:-3] + try: + importlib.import_module("cps.metadata_provider." + a) + new_list.append(a) + except ImportError: + log.error("Import error for metadata source: {}".format(a)) + pass + +def list_classes(provider_list): + classes = list() + for element in provider_list: + for name, obj in inspect.getmembers(sys.modules["cps.metadata_provider." + element]): + if inspect.isclass(obj) and name != "Metadata" and issubclass(obj, Metadata): + classes.append(obj()) + return classes + +cl = list_classes(new_list) + +@meta.route("/metadata/provider") +@login_required +def metadata_provider(): + active = current_user.view_settings.get('metadata', {}) + provider = list() + for c in cl: + ac = active.get(c.__id__, True) + provider.append({"name": c.__name__, "active": ac, "initial": ac, "id": c.__id__}) + return Response(json.dumps(provider), mimetype='application/json') + +@meta.route("/metadata/provider", methods=['POST']) +@meta.route("/metadata/provider/", methods=['POST']) +@login_required +def metadata_change_active_provider(prov_name): + new_state = request.get_json() + active = current_user.view_settings.get('metadata', {}) + active[new_state['id']] = new_state['value'] + current_user.view_settings['metadata'] = active + try: + try: + flag_modified(current_user, "view_settings") + except AttributeError: + pass + ub.session.commit() + except (InvalidRequestError, OperationalError): + log.error("Invalid request received: {}".format(request)) + return "Invalid request", 400 + if "initial" in new_state and prov_name: + for c in cl: + if c.__id__ == prov_name: + data = c.search(new_state.get('query', "")) + break + return Response(json.dumps(data), mimetype='application/json') + return "" + +@meta.route("/metadata/search", methods=['POST']) +@login_required +def metadata_search(): + query = request.form.to_dict().get('query') + data = list() + active = current_user.view_settings.get('metadata', {}) + if query: + static_cover = url_for('static', filename='generic_cover.jpg') + with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor: + meta = {executor.submit(c.search, query, static_cover): c for c in cl if active.get(c.__id__, True)} + for future in concurrent.futures.as_completed(meta): + data.extend(future.result()) + return Response(json.dumps(data), mimetype='application/json') + + + + + + diff --git a/cps/services/Metadata.py b/cps/services/Metadata.py new file mode 100644 index 00000000..d6e4e7d5 --- /dev/null +++ b/cps/services/Metadata.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- + +# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) +# Copyright (C) 2021 OzzieIsaacs +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +class Metadata(): + __name__ = "Generic" + + def __init__(self): + self.active = True + + def set_status(self, state): + self.active = state diff --git a/cps/services/SyncToken.py b/cps/services/SyncToken.py index b54d8d95..cc67542c 100644 --- a/cps/services/SyncToken.py +++ b/cps/services/SyncToken.py @@ -183,3 +183,12 @@ class SyncToken: }, } return b64encode_json(token) + + def __str__(self): + return "{},{},{},{},{},{},{}".format(self.raw_kobo_store_token, + self.books_last_created, + self.books_last_modified, + self.archive_last_modified, + self.reading_state_last_modified, + self.tags_last_modified, + self.books_last_id) diff --git a/cps/services/__init__.py b/cps/services/__init__.py index 17f1f529..e6e5954c 100644 --- a/cps/services/__init__.py +++ b/cps/services/__init__.py @@ -45,3 +45,9 @@ except ImportError as err: log.debug("Cannot import SyncToken, syncing books with Kobo Devices will not work: %s", err) kobo = None SyncToken = None + +try: + from . import gmail +except ImportError as err: + log.debug("Cannot import gmail, sending books via Gmail Oauth2 Verification will not work: %s", err) + gmail = None diff --git a/cps/services/gmail.py b/cps/services/gmail.py new file mode 100644 index 00000000..baada1f8 --- /dev/null +++ b/cps/services/gmail.py @@ -0,0 +1,83 @@ +from __future__ import print_function +import os.path +from google_auth_oauthlib.flow import InstalledAppFlow +from google.auth.transport.requests import Request +from googleapiclient.discovery import build +from google.oauth2.credentials import Credentials + +from datetime import datetime +import base64 +from flask_babel import gettext as _ +from ..constants import BASE_DIR +from .. import logger + + +log = logger.create() + +SCOPES = ['openid', 'https://www.googleapis.com/auth/gmail.send', 'https://www.googleapis.com/auth/userinfo.email'] + +def setup_gmail(token): + # If there are no (valid) credentials available, let the user log in. + creds = None + if "token" in token: + creds = Credentials( + token=token['token'], + refresh_token=token['refresh_token'], + token_uri=token['token_uri'], + client_id=token['client_id'], + client_secret=token['client_secret'], + scopes=token['scopes'], + ) + creds.expiry = datetime.fromisoformat(token['expiry']) + + if not creds or not creds.valid: + # don't forget to dump one more time after the refresh + # also, some file-locking routines wouldn't be needless + if creds and creds.expired and creds.refresh_token: + creds.refresh(Request()) + else: + cred_file = os.path.join(BASE_DIR, 'gmail.json') + if not os.path.exists(cred_file): + raise Exception(_("Found no valid gmail.json file with OAuth information")) + flow = InstalledAppFlow.from_client_secrets_file( + os.path.join(BASE_DIR, 'gmail.json'), SCOPES) + creds = flow.run_local_server(port=0) + user_info = get_user_info(creds) + return { + 'token': creds.token, + 'refresh_token': creds.refresh_token, + 'token_uri': creds.token_uri, + 'client_id': creds.client_id, + 'client_secret': creds.client_secret, + 'scopes': creds.scopes, + 'expiry': creds.expiry.isoformat(), + 'email': user_info + } + return {} + +def get_user_info(credentials): + user_info_service = build(serviceName='oauth2', version='v2',credentials=credentials) + user_info = user_info_service.userinfo().get().execute() + return user_info.get('email', "") + +def send_messsage(token, msg): + log.debug("Start sending e-mail via Gmail") + creds = Credentials( + token=token['token'], + refresh_token=token['refresh_token'], + token_uri=token['token_uri'], + client_id=token['client_id'], + client_secret=token['client_secret'], + scopes=token['scopes'], + ) + creds.expiry = datetime.fromisoformat(token['expiry']) + if creds and creds.expired and creds.refresh_token: + creds.refresh(Request()) + service = build('gmail', 'v1', credentials=creds) + message_as_bytes = msg.as_bytes() # the message should converted from string to bytes. + message_as_base64 = base64.urlsafe_b64encode(message_as_bytes) # encode in base64 (printable letters coding) + raw = message_as_base64.decode() # convert to something JSON serializable + body = {'raw': raw} + + (service.users().messages().send(userId='me', body=body).execute()) + log.debug("E-mail send successfully via Gmail") diff --git a/cps/services/worker.py b/cps/services/worker.py index 2b6816db..97068f74 100644 --- a/cps/services/worker.py +++ b/cps/services/worker.py @@ -69,6 +69,7 @@ class WorkerThread(threading.Thread): def add(cls, user, task): ins = cls.getInstance() ins.num += 1 + log.debug("Add Task for user: {}: {}".format(user, task)) ins.queue.put(QueuedTask( num=ins.num, user=user, @@ -164,9 +165,9 @@ class CalibreTask: # catch any unhandled exceptions in a task and automatically fail it try: self.run(*args) - except Exception as e: - self._handleError(str(e)) - log.debug_or_exception(e) + except Exception as ex: + self._handleError(str(ex)) + log.debug_or_exception(ex) self.end_time = datetime.now() @@ -209,10 +210,13 @@ class CalibreTask: # By default, we're good to clean a task if it's "Done" return self.stat in (STAT_FINISH_SUCCESS, STAT_FAIL) - @progress.setter - def progress(self, x): - # todo: throw error if outside of [0,1] - self._progress = x + '''@progress.setter + def progress(self, x): + if x > 1: + x = 1 + if x < 0: + x = 0 + self._progress = x''' @property def self_cleanup(self): diff --git a/cps/shelf.py b/cps/shelf.py index 7b00c32b..d232e850 100644 --- a/cps/shelf.py +++ b/cps/shelf.py @@ -21,20 +21,20 @@ # along with this program. If not, see . from __future__ import division, print_function, unicode_literals -from datetime import datetime + import sys +from datetime import datetime -from flask import Blueprint, request, flash, redirect, url_for +from flask import Blueprint, flash, redirect, request, url_for from flask_babel import gettext as _ -from flask_login import login_required, current_user +from flask_login import current_user, login_required +from sqlalchemy.exc import InvalidRequestError, OperationalError from sqlalchemy.sql.expression import func, true -from sqlalchemy.exc import OperationalError, InvalidRequestError -from . import logger, ub, calibre_db, db +from . import calibre_db, config, db, logger, ub from .render_template import render_title_template from .usermanagement import login_required_if_no_ano - shelf = Blueprint('shelf', __name__) log = logger.create() @@ -72,10 +72,9 @@ def add_to_shelf(shelf_id, book_id): if not check_shelf_edit_permissions(shelf): if not xhr: - flash(_(u"Sorry you are not allowed to add a book to the the shelf: %(shelfname)s", shelfname=shelf.name), - category="error") + flash(_(u"Sorry you are not allowed to add a book to that shelf"), category="error") return redirect(url_for('web.index')) - return "Sorry you are not allowed to add a book to the the shelf: %s" % shelf.name, 403 + return "Sorry you are not allowed to add a book to the that shelf", 403 book_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id, ub.BookShelf.book_id == book_id).first() @@ -99,12 +98,14 @@ def add_to_shelf(shelf_id, book_id): ub.session.commit() except (OperationalError, InvalidRequestError): ub.session.rollback() + log.error("Settings DB is not Writeable") flash(_(u"Settings DB is not Writeable"), category="error") if "HTTP_REFERER" in request.environ: return redirect(request.environ["HTTP_REFERER"]) else: return redirect(url_for('web.index')) if not xhr: + log.debug("Book has been added to shelf: {}".format(shelf.name)) flash(_(u"Book has been added to shelf: %(sname)s", sname=shelf.name), category="success") if "HTTP_REFERER" in request.environ: return redirect(request.environ["HTTP_REFERER"]) @@ -123,6 +124,7 @@ def search_to_shelf(shelf_id): return redirect(url_for('web.index')) if not check_shelf_edit_permissions(shelf): + log.warning("You are not allowed to add a book to the the shelf: {}".format(shelf.name)) flash(_(u"You are not allowed to add a book to the the shelf: %(name)s", name=shelf.name), category="error") return redirect(url_for('web.index')) @@ -140,7 +142,7 @@ def search_to_shelf(shelf_id): books_for_shelf = ub.searched_ids[current_user.id] if not books_for_shelf: - log.error("Books are already part of %s", shelf.name) + log.error("Books are already part of {}".format(shelf.name)) flash(_(u"Books are already part of the shelf: %(name)s", name=shelf.name), category="error") return redirect(url_for('web.index')) @@ -156,8 +158,10 @@ def search_to_shelf(shelf_id): flash(_(u"Books have been added to shelf: %(sname)s", sname=shelf.name), category="success") except (OperationalError, InvalidRequestError): ub.session.rollback() - flash(_(u"Settings DB is not Writeable"), category="error") + log.error("Settings DB is not Writeable") + flash(_("Settings DB is not Writeable"), category="error") else: + log.error("Could not add books to shelf: {}".format(shelf.name)) flash(_(u"Could not add books to shelf: %(sname)s", sname=shelf.name), category="error") return redirect(url_for('web.index')) @@ -168,7 +172,7 @@ def remove_from_shelf(shelf_id, book_id): xhr = request.headers.get('X-Requested-With') == 'XMLHttpRequest' shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first() if shelf is None: - log.error("Invalid shelf specified: %s", shelf_id) + log.error("Invalid shelf specified: {}".format(shelf_id)) if not xhr: return redirect(url_for('web.index')) return "Invalid shelf specified", 400 @@ -197,7 +201,8 @@ def remove_from_shelf(shelf_id, book_id): ub.session.commit() except (OperationalError, InvalidRequestError): ub.session.rollback() - flash(_(u"Settings DB is not Writeable"), category="error") + log.error("Settings DB is not Writeable") + flash(_("Settings DB is not Writeable"), category="error") if "HTTP_REFERER" in request.environ: return redirect(request.environ["HTTP_REFERER"]) else: @@ -211,6 +216,7 @@ def remove_from_shelf(shelf_id, book_id): return "", 204 else: if not xhr: + log.warning("You are not allowed to remove a book from shelf: {}".format(shelf.name)) flash(_(u"Sorry you are not allowed to remove a book from this shelf: %(sname)s", sname=shelf.name), category="error") return redirect(url_for('web.index')) @@ -221,74 +227,86 @@ def remove_from_shelf(shelf_id, book_id): @login_required def create_shelf(): shelf = ub.Shelf() - return create_edit_shelf(shelf, title=_(u"Create a Shelf"), page="shelfcreate") + return create_edit_shelf(shelf, page_title=_(u"Create a Shelf"), page="shelfcreate") @shelf.route("/shelf/edit/", methods=["GET", "POST"]) @login_required def edit_shelf(shelf_id): shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first() - return create_edit_shelf(shelf, title=_(u"Edit a shelf"), page="shelfedit", shelf_id=shelf_id) + if not check_shelf_edit_permissions(shelf): + flash(_(u"Sorry you are not allowed to edit this shelf"), category="error") + return redirect(url_for('web.index')) + return create_edit_shelf(shelf, page_title=_(u"Edit a shelf"), page="shelfedit", shelf_id=shelf_id) # if shelf ID is set, we are editing a shelf -def create_edit_shelf(shelf, title, page, shelf_id=False): +def create_edit_shelf(shelf, page_title, page, shelf_id=False): + sync_only_selected_shelves = current_user.kobo_only_shelves_sync + # calibre_db.session.query(ub.Shelf).filter(ub.Shelf.user_id == current_user.id).filter(ub.Shelf.kobo_sync).count() if request.method == "POST": to_save = request.form.to_dict() - if "is_public" in to_save: - shelf.is_public = 1 - else: - shelf.is_public = 0 - if check_shelf_is_unique(shelf, to_save, shelf_id): - shelf.name = to_save["title"] - # shelf.last_modified = datetime.utcnow() + shelf.is_public = 1 if to_save.get("is_public") else 0 + if config.config_kobo_sync: + shelf.kobo_sync = True if to_save.get("kobo_sync") else False + shelf_title = to_save.get("title", "") + if check_shelf_is_unique(shelf, shelf_title, shelf_id): + shelf.name = shelf_title if not shelf_id: shelf.user_id = int(current_user.id) ub.session.add(shelf) shelf_action = "created" - flash_text = _(u"Shelf %(title)s created", title=to_save["title"]) + flash_text = _(u"Shelf %(title)s created", title=shelf_title) else: shelf_action = "changed" - flash_text = _(u"Shelf %(title)s changed", title=to_save["title"]) + flash_text = _(u"Shelf %(title)s changed", title=shelf_title) try: ub.session.commit() - log.info(u"Shelf {} {}".format(to_save["title"], shelf_action)) + log.info(u"Shelf {} {}".format(shelf_title, shelf_action)) flash(flash_text, category="success") return redirect(url_for('shelf.show_shelf', shelf_id=shelf.id)) - except (OperationalError, InvalidRequestError) as e: + except (OperationalError, InvalidRequestError) as ex: ub.session.rollback() - log.debug_or_exception(e) - flash(_(u"Settings DB is not Writeable"), category="error") - except Exception as e: + log.debug_or_exception(ex) + log.error("Settings DB is not Writeable") + flash(_("Settings DB is not Writeable"), category="error") + except Exception as ex: ub.session.rollback() - log.debug_or_exception(e) + log.debug_or_exception(ex) flash(_(u"There was an error"), category="error") - return render_title_template('shelf_edit.html', shelf=shelf, title=title, page=page) + return render_title_template('shelf_edit.html', + shelf=shelf, + title=page_title, + page=page, + kobo_sync_enabled=config.config_kobo_sync, + sync_only_selected_shelves=sync_only_selected_shelves) -def check_shelf_is_unique(shelf, to_save, shelf_id=False): +def check_shelf_is_unique(shelf, title, shelf_id=False): if shelf_id: ident = ub.Shelf.id != shelf_id else: ident = true() if shelf.is_public == 1: is_shelf_name_unique = ub.session.query(ub.Shelf) \ - .filter((ub.Shelf.name == to_save["title"]) & (ub.Shelf.is_public == 1)) \ + .filter((ub.Shelf.name == title) & (ub.Shelf.is_public == 1)) \ .filter(ident) \ .first() is None if not is_shelf_name_unique: - flash(_(u"A public shelf with the name '%(title)s' already exists.", title=to_save["title"]), + log.error("A public shelf with the name '{}' already exists.".format(title)) + flash(_(u"A public shelf with the name '%(title)s' already exists.", title=title), category="error") else: is_shelf_name_unique = ub.session.query(ub.Shelf) \ - .filter((ub.Shelf.name == to_save["title"]) & (ub.Shelf.is_public == 0) & + .filter((ub.Shelf.name == title) & (ub.Shelf.is_public == 0) & (ub.Shelf.user_id == int(current_user.id))) \ .filter(ident) \ .first() is None if not is_shelf_name_unique: - flash(_(u"A private shelf with the name '%(title)s' already exists.", title=to_save["title"]), + log.error("A private shelf with the name '{}' already exists.".format(title)) + flash(_(u"A private shelf with the name '%(title)s' already exists.", title=title), category="error") return is_shelf_name_unique @@ -311,7 +329,8 @@ def delete_shelf(shelf_id): delete_shelf_helper(cur_shelf) except InvalidRequestError: ub.session.rollback() - flash(_(u"Settings DB is not Writeable"), category="error") + log.error("Settings DB is not Writeable") + flash(_("Settings DB is not Writeable"), category="error") return redirect(url_for('web.index')) @@ -345,13 +364,14 @@ def order_shelf(shelf_id): ub.session.commit() except (OperationalError, InvalidRequestError): ub.session.rollback() - flash(_(u"Settings DB is not Writeable"), category="error") + log.error("Settings DB is not Writeable") + flash(_("Settings DB is not Writeable"), category="error") shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first() result = list() if shelf and check_shelf_view_permissions(shelf): - result = calibre_db.session.query(db.Books)\ - .join(ub.BookShelf,ub.BookShelf.book_id == db.Books.id , isouter=True) \ + result = calibre_db.session.query(db.Books) \ + .join(ub.BookShelf, ub.BookShelf.book_id == db.Books.id, isouter=True) \ .add_columns(calibre_db.common_filters().label("visible")) \ .filter(ub.BookShelf.shelf == shelf_id).order_by(ub.BookShelf.order.asc()).all() return render_title_template('shelf_order.html', entries=result, @@ -360,7 +380,9 @@ def order_shelf(shelf_id): def change_shelf_order(shelf_id, order): - result = calibre_db.session.query(db.Books).join(ub.BookShelf,ub.BookShelf.book_id == db.Books.id)\ + result = calibre_db.session.query(db.Books).outerjoin(db.books_series_link, + db.Books.id == db.books_series_link.c.book)\ + .outerjoin(db.Series).join(ub.BookShelf, ub.BookShelf.book_id == db.Books.id) \ .filter(ub.BookShelf.shelf == shelf_id).order_by(*order).all() for index, entry in enumerate(result): book = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id) \ @@ -390,9 +412,11 @@ def render_show_shelf(shelf_type, shelf_id, page_no, sort_param): if sort_param == 'old': change_shelf_order(shelf_id, [db.Books.timestamp]) if sort_param == 'authaz': - change_shelf_order(shelf_id, [db.Books.author_sort.asc()]) + change_shelf_order(shelf_id, [db.Books.author_sort.asc(), db.Series.name, db.Books.series_index]) if sort_param == 'authza': - change_shelf_order(shelf_id, [db.Books.author_sort.desc()]) + change_shelf_order(shelf_id, [db.Books.author_sort.desc(), + db.Series.name.desc(), + db.Books.series_index.desc()]) page = "shelf.html" pagesize = 0 else: @@ -400,13 +424,13 @@ def render_show_shelf(shelf_type, shelf_id, page_no, sort_param): page = 'shelfdown.html' result, __, pagination = calibre_db.fill_indexpage(page_no, pagesize, - db.Books, - ub.BookShelf.shelf == shelf_id, - [ub.BookShelf.order.asc()], - ub.BookShelf, ub.BookShelf.book_id == db.Books.id) + db.Books, + ub.BookShelf.shelf == shelf_id, + [ub.BookShelf.order.asc()], + ub.BookShelf, ub.BookShelf.book_id == db.Books.id) # delete chelf entries where book is not existent anymore, can happen if book is deleted outside calibre-web - wrong_entries = calibre_db.session.query(ub.BookShelf)\ - .join(db.Books, ub.BookShelf.book_id == db.Books.id, isouter=True)\ + wrong_entries = calibre_db.session.query(ub.BookShelf) \ + .join(db.Books, ub.BookShelf.book_id == db.Books.id, isouter=True) \ .filter(db.Books.id == None).all() for entry in wrong_entries: log.info('Not existing book {} in {} deleted'.format(entry.book_id, shelf)) @@ -415,7 +439,8 @@ def render_show_shelf(shelf_type, shelf_id, page_no, sort_param): ub.session.commit() except (OperationalError, InvalidRequestError): ub.session.rollback() - flash(_(u"Settings DB is not Writeable"), category="error") + log.error("Settings DB is not Writeable") + flash(_("Settings DB is not Writeable"), category="error") return render_title_template(page, entries=result, diff --git a/cps/static/cmaps/78-EUC-H.bcmap b/cps/static/cmaps/78-EUC-H.bcmap new file mode 100644 index 00000000..2655fc70 Binary files /dev/null and b/cps/static/cmaps/78-EUC-H.bcmap differ diff --git a/cps/static/cmaps/78-EUC-V.bcmap b/cps/static/cmaps/78-EUC-V.bcmap new file mode 100644 index 00000000..f1ed8538 Binary files /dev/null and b/cps/static/cmaps/78-EUC-V.bcmap differ diff --git a/cps/static/cmaps/78-H.bcmap b/cps/static/cmaps/78-H.bcmap new file mode 100644 index 00000000..39e89d33 Binary files /dev/null and b/cps/static/cmaps/78-H.bcmap differ diff --git a/cps/static/cmaps/78-RKSJ-H.bcmap b/cps/static/cmaps/78-RKSJ-H.bcmap new file mode 100644 index 00000000..e4167cb5 Binary files /dev/null and b/cps/static/cmaps/78-RKSJ-H.bcmap differ diff --git a/cps/static/cmaps/78-RKSJ-V.bcmap b/cps/static/cmaps/78-RKSJ-V.bcmap new file mode 100644 index 00000000..50b1646e Binary files /dev/null and b/cps/static/cmaps/78-RKSJ-V.bcmap differ diff --git a/cps/static/cmaps/78-V.bcmap b/cps/static/cmaps/78-V.bcmap new file mode 100644 index 00000000..d7af99b5 Binary files /dev/null and b/cps/static/cmaps/78-V.bcmap differ diff --git a/cps/static/cmaps/78ms-RKSJ-H.bcmap b/cps/static/cmaps/78ms-RKSJ-H.bcmap new file mode 100644 index 00000000..37077d01 Binary files /dev/null and b/cps/static/cmaps/78ms-RKSJ-H.bcmap differ diff --git a/cps/static/cmaps/78ms-RKSJ-V.bcmap b/cps/static/cmaps/78ms-RKSJ-V.bcmap new file mode 100644 index 00000000..acf23231 Binary files /dev/null and b/cps/static/cmaps/78ms-RKSJ-V.bcmap differ diff --git a/cps/static/cmaps/83pv-RKSJ-H.bcmap b/cps/static/cmaps/83pv-RKSJ-H.bcmap new file mode 100644 index 00000000..2359bc52 Binary files /dev/null and b/cps/static/cmaps/83pv-RKSJ-H.bcmap differ diff --git a/cps/static/cmaps/90ms-RKSJ-H.bcmap b/cps/static/cmaps/90ms-RKSJ-H.bcmap new file mode 100644 index 00000000..af829382 Binary files /dev/null and b/cps/static/cmaps/90ms-RKSJ-H.bcmap differ diff --git a/cps/static/cmaps/90ms-RKSJ-V.bcmap b/cps/static/cmaps/90ms-RKSJ-V.bcmap new file mode 100644 index 00000000..780549de Binary files /dev/null and b/cps/static/cmaps/90ms-RKSJ-V.bcmap differ diff --git a/cps/static/cmaps/90msp-RKSJ-H.bcmap b/cps/static/cmaps/90msp-RKSJ-H.bcmap new file mode 100644 index 00000000..bfd3119c Binary files /dev/null and b/cps/static/cmaps/90msp-RKSJ-H.bcmap differ diff --git a/cps/static/cmaps/90msp-RKSJ-V.bcmap b/cps/static/cmaps/90msp-RKSJ-V.bcmap new file mode 100644 index 00000000..25ef14ab Binary files /dev/null and b/cps/static/cmaps/90msp-RKSJ-V.bcmap differ diff --git a/cps/static/cmaps/90pv-RKSJ-H.bcmap b/cps/static/cmaps/90pv-RKSJ-H.bcmap new file mode 100644 index 00000000..02f713bb Binary files /dev/null and b/cps/static/cmaps/90pv-RKSJ-H.bcmap differ diff --git a/cps/static/cmaps/90pv-RKSJ-V.bcmap b/cps/static/cmaps/90pv-RKSJ-V.bcmap new file mode 100644 index 00000000..d08e0cc5 Binary files /dev/null and b/cps/static/cmaps/90pv-RKSJ-V.bcmap differ diff --git a/cps/static/cmaps/Add-H.bcmap b/cps/static/cmaps/Add-H.bcmap new file mode 100644 index 00000000..59442aca Binary files /dev/null and b/cps/static/cmaps/Add-H.bcmap differ diff --git a/cps/static/cmaps/Add-RKSJ-H.bcmap b/cps/static/cmaps/Add-RKSJ-H.bcmap new file mode 100644 index 00000000..a3065e44 Binary files /dev/null and b/cps/static/cmaps/Add-RKSJ-H.bcmap differ diff --git a/cps/static/cmaps/Add-RKSJ-V.bcmap b/cps/static/cmaps/Add-RKSJ-V.bcmap new file mode 100644 index 00000000..040014cf Binary files /dev/null and b/cps/static/cmaps/Add-RKSJ-V.bcmap differ diff --git a/cps/static/cmaps/Add-V.bcmap b/cps/static/cmaps/Add-V.bcmap new file mode 100644 index 00000000..2f816d32 Binary files /dev/null and b/cps/static/cmaps/Add-V.bcmap differ diff --git a/cps/static/cmaps/Adobe-CNS1-0.bcmap b/cps/static/cmaps/Adobe-CNS1-0.bcmap new file mode 100644 index 00000000..88ec04af Binary files /dev/null and b/cps/static/cmaps/Adobe-CNS1-0.bcmap differ diff --git a/cps/static/cmaps/Adobe-CNS1-1.bcmap b/cps/static/cmaps/Adobe-CNS1-1.bcmap new file mode 100644 index 00000000..03a50147 Binary files /dev/null and b/cps/static/cmaps/Adobe-CNS1-1.bcmap differ diff --git a/cps/static/cmaps/Adobe-CNS1-2.bcmap b/cps/static/cmaps/Adobe-CNS1-2.bcmap new file mode 100644 index 00000000..2aa95141 Binary files /dev/null and b/cps/static/cmaps/Adobe-CNS1-2.bcmap differ diff --git a/cps/static/cmaps/Adobe-CNS1-3.bcmap b/cps/static/cmaps/Adobe-CNS1-3.bcmap new file mode 100644 index 00000000..86d8b8c7 Binary files /dev/null and b/cps/static/cmaps/Adobe-CNS1-3.bcmap differ diff --git a/cps/static/cmaps/Adobe-CNS1-4.bcmap b/cps/static/cmaps/Adobe-CNS1-4.bcmap new file mode 100644 index 00000000..f50fc6c1 Binary files /dev/null and b/cps/static/cmaps/Adobe-CNS1-4.bcmap differ diff --git a/cps/static/cmaps/Adobe-CNS1-5.bcmap b/cps/static/cmaps/Adobe-CNS1-5.bcmap new file mode 100644 index 00000000..6caf4a83 Binary files /dev/null and b/cps/static/cmaps/Adobe-CNS1-5.bcmap differ diff --git a/cps/static/cmaps/Adobe-CNS1-6.bcmap b/cps/static/cmaps/Adobe-CNS1-6.bcmap new file mode 100644 index 00000000..b77fb070 Binary files /dev/null and b/cps/static/cmaps/Adobe-CNS1-6.bcmap differ diff --git a/cps/static/cmaps/Adobe-CNS1-UCS2.bcmap b/cps/static/cmaps/Adobe-CNS1-UCS2.bcmap new file mode 100644 index 00000000..69d79a2c Binary files /dev/null and b/cps/static/cmaps/Adobe-CNS1-UCS2.bcmap differ diff --git a/cps/static/cmaps/Adobe-GB1-0.bcmap b/cps/static/cmaps/Adobe-GB1-0.bcmap new file mode 100644 index 00000000..36101083 Binary files /dev/null and b/cps/static/cmaps/Adobe-GB1-0.bcmap differ diff --git a/cps/static/cmaps/Adobe-GB1-1.bcmap b/cps/static/cmaps/Adobe-GB1-1.bcmap new file mode 100644 index 00000000..707bb106 Binary files /dev/null and b/cps/static/cmaps/Adobe-GB1-1.bcmap differ diff --git a/cps/static/cmaps/Adobe-GB1-2.bcmap b/cps/static/cmaps/Adobe-GB1-2.bcmap new file mode 100644 index 00000000..f7648cc3 Binary files /dev/null and b/cps/static/cmaps/Adobe-GB1-2.bcmap differ diff --git a/cps/static/cmaps/Adobe-GB1-3.bcmap b/cps/static/cmaps/Adobe-GB1-3.bcmap new file mode 100644 index 00000000..85214589 Binary files /dev/null and b/cps/static/cmaps/Adobe-GB1-3.bcmap differ diff --git a/cps/static/cmaps/Adobe-GB1-4.bcmap b/cps/static/cmaps/Adobe-GB1-4.bcmap new file mode 100644 index 00000000..e40c63ab Binary files /dev/null and b/cps/static/cmaps/Adobe-GB1-4.bcmap differ diff --git a/cps/static/cmaps/Adobe-GB1-5.bcmap b/cps/static/cmaps/Adobe-GB1-5.bcmap new file mode 100644 index 00000000..d7623b50 Binary files /dev/null and b/cps/static/cmaps/Adobe-GB1-5.bcmap differ diff --git a/cps/static/cmaps/Adobe-GB1-UCS2.bcmap b/cps/static/cmaps/Adobe-GB1-UCS2.bcmap new file mode 100644 index 00000000..75865259 Binary files /dev/null and b/cps/static/cmaps/Adobe-GB1-UCS2.bcmap differ diff --git a/cps/static/cmaps/Adobe-Japan1-0.bcmap b/cps/static/cmaps/Adobe-Japan1-0.bcmap new file mode 100644 index 00000000..f0e94ec1 Binary files /dev/null and b/cps/static/cmaps/Adobe-Japan1-0.bcmap differ diff --git a/cps/static/cmaps/Adobe-Japan1-1.bcmap b/cps/static/cmaps/Adobe-Japan1-1.bcmap new file mode 100644 index 00000000..dad42c5a Binary files /dev/null and b/cps/static/cmaps/Adobe-Japan1-1.bcmap differ diff --git a/cps/static/cmaps/Adobe-Japan1-2.bcmap b/cps/static/cmaps/Adobe-Japan1-2.bcmap new file mode 100644 index 00000000..090819a0 Binary files /dev/null and b/cps/static/cmaps/Adobe-Japan1-2.bcmap differ diff --git a/cps/static/cmaps/Adobe-Japan1-3.bcmap b/cps/static/cmaps/Adobe-Japan1-3.bcmap new file mode 100644 index 00000000..087dfc15 Binary files /dev/null and b/cps/static/cmaps/Adobe-Japan1-3.bcmap differ diff --git a/cps/static/cmaps/Adobe-Japan1-4.bcmap b/cps/static/cmaps/Adobe-Japan1-4.bcmap new file mode 100644 index 00000000..46aa9bff Binary files /dev/null and b/cps/static/cmaps/Adobe-Japan1-4.bcmap differ diff --git a/cps/static/cmaps/Adobe-Japan1-5.bcmap b/cps/static/cmaps/Adobe-Japan1-5.bcmap new file mode 100644 index 00000000..5b4b65cc Binary files /dev/null and b/cps/static/cmaps/Adobe-Japan1-5.bcmap differ diff --git a/cps/static/cmaps/Adobe-Japan1-6.bcmap b/cps/static/cmaps/Adobe-Japan1-6.bcmap new file mode 100644 index 00000000..e77d699a Binary files /dev/null and b/cps/static/cmaps/Adobe-Japan1-6.bcmap differ diff --git a/cps/static/cmaps/Adobe-Japan1-UCS2.bcmap b/cps/static/cmaps/Adobe-Japan1-UCS2.bcmap new file mode 100644 index 00000000..128a1410 Binary files /dev/null and b/cps/static/cmaps/Adobe-Japan1-UCS2.bcmap differ diff --git a/cps/static/cmaps/Adobe-Korea1-0.bcmap b/cps/static/cmaps/Adobe-Korea1-0.bcmap new file mode 100644 index 00000000..cef1a998 Binary files /dev/null and b/cps/static/cmaps/Adobe-Korea1-0.bcmap differ diff --git a/cps/static/cmaps/Adobe-Korea1-1.bcmap b/cps/static/cmaps/Adobe-Korea1-1.bcmap new file mode 100644 index 00000000..11ffa36d Binary files /dev/null and b/cps/static/cmaps/Adobe-Korea1-1.bcmap differ diff --git a/cps/static/cmaps/Adobe-Korea1-2.bcmap b/cps/static/cmaps/Adobe-Korea1-2.bcmap new file mode 100644 index 00000000..3172308c Binary files /dev/null and b/cps/static/cmaps/Adobe-Korea1-2.bcmap differ diff --git a/cps/static/cmaps/Adobe-Korea1-UCS2.bcmap b/cps/static/cmaps/Adobe-Korea1-UCS2.bcmap new file mode 100644 index 00000000..f3371c0c Binary files /dev/null and b/cps/static/cmaps/Adobe-Korea1-UCS2.bcmap differ diff --git a/cps/static/cmaps/B5-H.bcmap b/cps/static/cmaps/B5-H.bcmap new file mode 100644 index 00000000..beb4d228 Binary files /dev/null and b/cps/static/cmaps/B5-H.bcmap differ diff --git a/cps/static/cmaps/B5-V.bcmap b/cps/static/cmaps/B5-V.bcmap new file mode 100644 index 00000000..2d4f87d5 Binary files /dev/null and b/cps/static/cmaps/B5-V.bcmap differ diff --git a/cps/static/cmaps/B5pc-H.bcmap b/cps/static/cmaps/B5pc-H.bcmap new file mode 100644 index 00000000..ce001316 Binary files /dev/null and b/cps/static/cmaps/B5pc-H.bcmap differ diff --git a/cps/static/cmaps/B5pc-V.bcmap b/cps/static/cmaps/B5pc-V.bcmap new file mode 100644 index 00000000..73b99ff2 Binary files /dev/null and b/cps/static/cmaps/B5pc-V.bcmap differ diff --git a/cps/static/cmaps/CNS-EUC-H.bcmap b/cps/static/cmaps/CNS-EUC-H.bcmap new file mode 100644 index 00000000..61d1d0cb Binary files /dev/null and b/cps/static/cmaps/CNS-EUC-H.bcmap differ diff --git a/cps/static/cmaps/CNS-EUC-V.bcmap b/cps/static/cmaps/CNS-EUC-V.bcmap new file mode 100644 index 00000000..1a393a51 Binary files /dev/null and b/cps/static/cmaps/CNS-EUC-V.bcmap differ diff --git a/cps/static/cmaps/CNS1-H.bcmap b/cps/static/cmaps/CNS1-H.bcmap new file mode 100644 index 00000000..f738e218 Binary files /dev/null and b/cps/static/cmaps/CNS1-H.bcmap differ diff --git a/cps/static/cmaps/CNS1-V.bcmap b/cps/static/cmaps/CNS1-V.bcmap new file mode 100644 index 00000000..9c3169f0 Binary files /dev/null and b/cps/static/cmaps/CNS1-V.bcmap differ diff --git a/cps/static/cmaps/CNS2-H.bcmap b/cps/static/cmaps/CNS2-H.bcmap new file mode 100644 index 00000000..c89b3527 Binary files /dev/null and b/cps/static/cmaps/CNS2-H.bcmap differ diff --git a/cps/static/cmaps/CNS2-V.bcmap b/cps/static/cmaps/CNS2-V.bcmap new file mode 100644 index 00000000..7588cec8 --- /dev/null +++ b/cps/static/cmaps/CNS2-V.bcmap @@ -0,0 +1,3 @@ +àRCopyright 1990-2009 Adobe Systems Incorporated. +All rights reserved. +See ./LICENSEáCNS2-H \ No newline at end of file diff --git a/cps/static/cmaps/ETHK-B5-H.bcmap b/cps/static/cmaps/ETHK-B5-H.bcmap new file mode 100644 index 00000000..cb29415d Binary files /dev/null and b/cps/static/cmaps/ETHK-B5-H.bcmap differ diff --git a/cps/static/cmaps/ETHK-B5-V.bcmap b/cps/static/cmaps/ETHK-B5-V.bcmap new file mode 100644 index 00000000..f09aec63 Binary files /dev/null and b/cps/static/cmaps/ETHK-B5-V.bcmap differ diff --git a/cps/static/cmaps/ETen-B5-H.bcmap b/cps/static/cmaps/ETen-B5-H.bcmap new file mode 100644 index 00000000..c2d77462 Binary files /dev/null and b/cps/static/cmaps/ETen-B5-H.bcmap differ diff --git a/cps/static/cmaps/ETen-B5-V.bcmap b/cps/static/cmaps/ETen-B5-V.bcmap new file mode 100644 index 00000000..89bff159 Binary files /dev/null and b/cps/static/cmaps/ETen-B5-V.bcmap differ diff --git a/cps/static/cmaps/ETenms-B5-H.bcmap b/cps/static/cmaps/ETenms-B5-H.bcmap new file mode 100644 index 00000000..a7d69db5 --- /dev/null +++ b/cps/static/cmaps/ETenms-B5-H.bcmap @@ -0,0 +1,3 @@ +àRCopyright 1990-2009 Adobe Systems Incorporated. +All rights reserved. +See ./LICENSEá ETen-B5-H` ^ \ No newline at end of file diff --git a/cps/static/cmaps/ETenms-B5-V.bcmap b/cps/static/cmaps/ETenms-B5-V.bcmap new file mode 100644 index 00000000..adc5d618 Binary files /dev/null and b/cps/static/cmaps/ETenms-B5-V.bcmap differ diff --git a/cps/static/cmaps/EUC-H.bcmap b/cps/static/cmaps/EUC-H.bcmap new file mode 100644 index 00000000..e92ea5b3 Binary files /dev/null and b/cps/static/cmaps/EUC-H.bcmap differ diff --git a/cps/static/cmaps/EUC-V.bcmap b/cps/static/cmaps/EUC-V.bcmap new file mode 100644 index 00000000..7a7c1832 Binary files /dev/null and b/cps/static/cmaps/EUC-V.bcmap differ diff --git a/cps/static/cmaps/Ext-H.bcmap b/cps/static/cmaps/Ext-H.bcmap new file mode 100644 index 00000000..3b5cde44 Binary files /dev/null and b/cps/static/cmaps/Ext-H.bcmap differ diff --git a/cps/static/cmaps/Ext-RKSJ-H.bcmap b/cps/static/cmaps/Ext-RKSJ-H.bcmap new file mode 100644 index 00000000..ea4d2d97 Binary files /dev/null and b/cps/static/cmaps/Ext-RKSJ-H.bcmap differ diff --git a/cps/static/cmaps/Ext-RKSJ-V.bcmap b/cps/static/cmaps/Ext-RKSJ-V.bcmap new file mode 100644 index 00000000..3457c277 Binary files /dev/null and b/cps/static/cmaps/Ext-RKSJ-V.bcmap differ diff --git a/cps/static/cmaps/Ext-V.bcmap b/cps/static/cmaps/Ext-V.bcmap new file mode 100644 index 00000000..4999ca40 Binary files /dev/null and b/cps/static/cmaps/Ext-V.bcmap differ diff --git a/cps/static/cmaps/GB-EUC-H.bcmap b/cps/static/cmaps/GB-EUC-H.bcmap new file mode 100644 index 00000000..e39908b9 Binary files /dev/null and b/cps/static/cmaps/GB-EUC-H.bcmap differ diff --git a/cps/static/cmaps/GB-EUC-V.bcmap b/cps/static/cmaps/GB-EUC-V.bcmap new file mode 100644 index 00000000..d5be5446 Binary files /dev/null and b/cps/static/cmaps/GB-EUC-V.bcmap differ diff --git a/cps/static/cmaps/GB-H.bcmap b/cps/static/cmaps/GB-H.bcmap new file mode 100644 index 00000000..39189c54 --- /dev/null +++ b/cps/static/cmaps/GB-H.bcmap @@ -0,0 +1,4 @@ +àRCopyright 1990-2009 Adobe Systems Incorporated. +All rights reserved. +See ./LICENSE!!º]aX!!]`21> p z$]‚"R‚d-Uƒ7*„ 4„%+ „Z „{/…%…<9K…b1]†."‡ ‰`]‡,"]ˆ +"]ˆh"]‰F"]Š$"]‹"]‹`"]Œ>"]"]z"]ŽX"]6"]"]r"]‘P"]’."]“ "]“j"]”H"]•&"]–"]–b"]—@"]˜"]˜|"]™Z"]š8"]›"]›t"]œR"]0"]ž"]žl"]ŸJ"] ("]¡"]¡d"]¢B"]£ "X£~']¤W"]¥5"]¦"]¦q"]§O"]¨-"]© "]©i"]ªG"]«%"]¬"]¬a"]­?"]®"]®{"]¯Y"]°7"]±"]±s"]²Q"]³/"]´ "]´k"]µI"]¶'"]·"]·c"]¸A"]¹"]¹}"]º["]»9 \ No newline at end of file diff --git a/cps/static/cmaps/GB-V.bcmap b/cps/static/cmaps/GB-V.bcmap new file mode 100644 index 00000000..31083451 Binary files /dev/null and b/cps/static/cmaps/GB-V.bcmap differ diff --git a/cps/static/cmaps/GBK-EUC-H.bcmap b/cps/static/cmaps/GBK-EUC-H.bcmap new file mode 100644 index 00000000..05fff7e8 Binary files /dev/null and b/cps/static/cmaps/GBK-EUC-H.bcmap differ diff --git a/cps/static/cmaps/GBK-EUC-V.bcmap b/cps/static/cmaps/GBK-EUC-V.bcmap new file mode 100644 index 00000000..0cdf6bed Binary files /dev/null and b/cps/static/cmaps/GBK-EUC-V.bcmap differ diff --git a/cps/static/cmaps/GBK2K-H.bcmap b/cps/static/cmaps/GBK2K-H.bcmap new file mode 100644 index 00000000..46f6ba59 Binary files /dev/null and b/cps/static/cmaps/GBK2K-H.bcmap differ diff --git a/cps/static/cmaps/GBK2K-V.bcmap b/cps/static/cmaps/GBK2K-V.bcmap new file mode 100644 index 00000000..d9a94798 Binary files /dev/null and b/cps/static/cmaps/GBK2K-V.bcmap differ diff --git a/cps/static/cmaps/GBKp-EUC-H.bcmap b/cps/static/cmaps/GBKp-EUC-H.bcmap new file mode 100644 index 00000000..5cb0af68 Binary files /dev/null and b/cps/static/cmaps/GBKp-EUC-H.bcmap differ diff --git a/cps/static/cmaps/GBKp-EUC-V.bcmap b/cps/static/cmaps/GBKp-EUC-V.bcmap new file mode 100644 index 00000000..bca93b8e Binary files /dev/null and b/cps/static/cmaps/GBKp-EUC-V.bcmap differ diff --git a/cps/static/cmaps/GBT-EUC-H.bcmap b/cps/static/cmaps/GBT-EUC-H.bcmap new file mode 100644 index 00000000..4b4e2d32 Binary files /dev/null and b/cps/static/cmaps/GBT-EUC-H.bcmap differ diff --git a/cps/static/cmaps/GBT-EUC-V.bcmap b/cps/static/cmaps/GBT-EUC-V.bcmap new file mode 100644 index 00000000..38f70669 Binary files /dev/null and b/cps/static/cmaps/GBT-EUC-V.bcmap differ diff --git a/cps/static/cmaps/GBT-H.bcmap b/cps/static/cmaps/GBT-H.bcmap new file mode 100644 index 00000000..8437ac33 Binary files /dev/null and b/cps/static/cmaps/GBT-H.bcmap differ diff --git a/cps/static/cmaps/GBT-V.bcmap b/cps/static/cmaps/GBT-V.bcmap new file mode 100644 index 00000000..697ab4a8 Binary files /dev/null and b/cps/static/cmaps/GBT-V.bcmap differ diff --git a/cps/static/cmaps/GBTpc-EUC-H.bcmap b/cps/static/cmaps/GBTpc-EUC-H.bcmap new file mode 100644 index 00000000..f6e50e89 Binary files /dev/null and b/cps/static/cmaps/GBTpc-EUC-H.bcmap differ diff --git a/cps/static/cmaps/GBTpc-EUC-V.bcmap b/cps/static/cmaps/GBTpc-EUC-V.bcmap new file mode 100644 index 00000000..6c0d71a2 Binary files /dev/null and b/cps/static/cmaps/GBTpc-EUC-V.bcmap differ diff --git a/cps/static/cmaps/GBpc-EUC-H.bcmap b/cps/static/cmaps/GBpc-EUC-H.bcmap new file mode 100644 index 00000000..c9edf67c Binary files /dev/null and b/cps/static/cmaps/GBpc-EUC-H.bcmap differ diff --git a/cps/static/cmaps/GBpc-EUC-V.bcmap b/cps/static/cmaps/GBpc-EUC-V.bcmap new file mode 100644 index 00000000..31450c97 Binary files /dev/null and b/cps/static/cmaps/GBpc-EUC-V.bcmap differ diff --git a/cps/static/cmaps/H.bcmap b/cps/static/cmaps/H.bcmap new file mode 100644 index 00000000..7b24ea46 Binary files /dev/null and b/cps/static/cmaps/H.bcmap differ diff --git a/cps/static/cmaps/HKdla-B5-H.bcmap b/cps/static/cmaps/HKdla-B5-H.bcmap new file mode 100644 index 00000000..7d30c050 Binary files /dev/null and b/cps/static/cmaps/HKdla-B5-H.bcmap differ diff --git a/cps/static/cmaps/HKdla-B5-V.bcmap b/cps/static/cmaps/HKdla-B5-V.bcmap new file mode 100644 index 00000000..78946940 Binary files /dev/null and b/cps/static/cmaps/HKdla-B5-V.bcmap differ diff --git a/cps/static/cmaps/HKdlb-B5-H.bcmap b/cps/static/cmaps/HKdlb-B5-H.bcmap new file mode 100644 index 00000000..d829a231 Binary files /dev/null and b/cps/static/cmaps/HKdlb-B5-H.bcmap differ diff --git a/cps/static/cmaps/HKdlb-B5-V.bcmap b/cps/static/cmaps/HKdlb-B5-V.bcmap new file mode 100644 index 00000000..2b572b50 Binary files /dev/null and b/cps/static/cmaps/HKdlb-B5-V.bcmap differ diff --git a/cps/static/cmaps/HKgccs-B5-H.bcmap b/cps/static/cmaps/HKgccs-B5-H.bcmap new file mode 100644 index 00000000..971a4f23 Binary files /dev/null and b/cps/static/cmaps/HKgccs-B5-H.bcmap differ diff --git a/cps/static/cmaps/HKgccs-B5-V.bcmap b/cps/static/cmaps/HKgccs-B5-V.bcmap new file mode 100644 index 00000000..d353ca25 Binary files /dev/null and b/cps/static/cmaps/HKgccs-B5-V.bcmap differ diff --git a/cps/static/cmaps/HKm314-B5-H.bcmap b/cps/static/cmaps/HKm314-B5-H.bcmap new file mode 100644 index 00000000..576dc011 Binary files /dev/null and b/cps/static/cmaps/HKm314-B5-H.bcmap differ diff --git a/cps/static/cmaps/HKm314-B5-V.bcmap b/cps/static/cmaps/HKm314-B5-V.bcmap new file mode 100644 index 00000000..0e96d0e2 Binary files /dev/null and b/cps/static/cmaps/HKm314-B5-V.bcmap differ diff --git a/cps/static/cmaps/HKm471-B5-H.bcmap b/cps/static/cmaps/HKm471-B5-H.bcmap new file mode 100644 index 00000000..11d170c7 Binary files /dev/null and b/cps/static/cmaps/HKm471-B5-H.bcmap differ diff --git a/cps/static/cmaps/HKm471-B5-V.bcmap b/cps/static/cmaps/HKm471-B5-V.bcmap new file mode 100644 index 00000000..54959bf9 Binary files /dev/null and b/cps/static/cmaps/HKm471-B5-V.bcmap differ diff --git a/cps/static/cmaps/HKscs-B5-H.bcmap b/cps/static/cmaps/HKscs-B5-H.bcmap new file mode 100644 index 00000000..6ef7857a Binary files /dev/null and b/cps/static/cmaps/HKscs-B5-H.bcmap differ diff --git a/cps/static/cmaps/HKscs-B5-V.bcmap b/cps/static/cmaps/HKscs-B5-V.bcmap new file mode 100644 index 00000000..1fb2fa2a Binary files /dev/null and b/cps/static/cmaps/HKscs-B5-V.bcmap differ diff --git a/cps/static/cmaps/Hankaku.bcmap b/cps/static/cmaps/Hankaku.bcmap new file mode 100644 index 00000000..4b8ec7fc Binary files /dev/null and b/cps/static/cmaps/Hankaku.bcmap differ diff --git a/cps/static/cmaps/Hiragana.bcmap b/cps/static/cmaps/Hiragana.bcmap new file mode 100644 index 00000000..17e983e7 Binary files /dev/null and b/cps/static/cmaps/Hiragana.bcmap differ diff --git a/cps/static/cmaps/KSC-EUC-H.bcmap b/cps/static/cmaps/KSC-EUC-H.bcmap new file mode 100644 index 00000000..a45c65f0 Binary files /dev/null and b/cps/static/cmaps/KSC-EUC-H.bcmap differ diff --git a/cps/static/cmaps/KSC-EUC-V.bcmap b/cps/static/cmaps/KSC-EUC-V.bcmap new file mode 100644 index 00000000..0e7b21f0 Binary files /dev/null and b/cps/static/cmaps/KSC-EUC-V.bcmap differ diff --git a/cps/static/cmaps/KSC-H.bcmap b/cps/static/cmaps/KSC-H.bcmap new file mode 100644 index 00000000..b9b22b67 Binary files /dev/null and b/cps/static/cmaps/KSC-H.bcmap differ diff --git a/cps/static/cmaps/KSC-Johab-H.bcmap b/cps/static/cmaps/KSC-Johab-H.bcmap new file mode 100644 index 00000000..2531ffcf Binary files /dev/null and b/cps/static/cmaps/KSC-Johab-H.bcmap differ diff --git a/cps/static/cmaps/KSC-Johab-V.bcmap b/cps/static/cmaps/KSC-Johab-V.bcmap new file mode 100644 index 00000000..367ceb22 Binary files /dev/null and b/cps/static/cmaps/KSC-Johab-V.bcmap differ diff --git a/cps/static/cmaps/KSC-V.bcmap b/cps/static/cmaps/KSC-V.bcmap new file mode 100644 index 00000000..6ae2f0b6 Binary files /dev/null and b/cps/static/cmaps/KSC-V.bcmap differ diff --git a/cps/static/cmaps/KSCms-UHC-H.bcmap b/cps/static/cmaps/KSCms-UHC-H.bcmap new file mode 100644 index 00000000..a8d4240e Binary files /dev/null and b/cps/static/cmaps/KSCms-UHC-H.bcmap differ diff --git a/cps/static/cmaps/KSCms-UHC-HW-H.bcmap b/cps/static/cmaps/KSCms-UHC-HW-H.bcmap new file mode 100644 index 00000000..8b4ae18f Binary files /dev/null and b/cps/static/cmaps/KSCms-UHC-HW-H.bcmap differ diff --git a/cps/static/cmaps/KSCms-UHC-HW-V.bcmap b/cps/static/cmaps/KSCms-UHC-HW-V.bcmap new file mode 100644 index 00000000..b655dbcf Binary files /dev/null and b/cps/static/cmaps/KSCms-UHC-HW-V.bcmap differ diff --git a/cps/static/cmaps/KSCms-UHC-V.bcmap b/cps/static/cmaps/KSCms-UHC-V.bcmap new file mode 100644 index 00000000..21f97f65 Binary files /dev/null and b/cps/static/cmaps/KSCms-UHC-V.bcmap differ diff --git a/cps/static/cmaps/KSCpc-EUC-H.bcmap b/cps/static/cmaps/KSCpc-EUC-H.bcmap new file mode 100644 index 00000000..e06f361e Binary files /dev/null and b/cps/static/cmaps/KSCpc-EUC-H.bcmap differ diff --git a/cps/static/cmaps/KSCpc-EUC-V.bcmap b/cps/static/cmaps/KSCpc-EUC-V.bcmap new file mode 100644 index 00000000..f3c9113f Binary files /dev/null and b/cps/static/cmaps/KSCpc-EUC-V.bcmap differ diff --git a/cps/static/cmaps/Katakana.bcmap b/cps/static/cmaps/Katakana.bcmap new file mode 100644 index 00000000..524303c4 Binary files /dev/null and b/cps/static/cmaps/Katakana.bcmap differ diff --git a/cps/static/cmaps/LICENSE b/cps/static/cmaps/LICENSE new file mode 100644 index 00000000..b1ad168a --- /dev/null +++ b/cps/static/cmaps/LICENSE @@ -0,0 +1,36 @@ +%%Copyright: ----------------------------------------------------------- +%%Copyright: Copyright 1990-2009 Adobe Systems Incorporated. +%%Copyright: All rights reserved. +%%Copyright: +%%Copyright: Redistribution and use in source and binary forms, with or +%%Copyright: without modification, are permitted provided that the +%%Copyright: following conditions are met: +%%Copyright: +%%Copyright: Redistributions of source code must retain the above +%%Copyright: copyright notice, this list of conditions and the following +%%Copyright: disclaimer. +%%Copyright: +%%Copyright: Redistributions in binary form must reproduce the above +%%Copyright: copyright notice, this list of conditions and the following +%%Copyright: disclaimer in the documentation and/or other materials +%%Copyright: provided with the distribution. +%%Copyright: +%%Copyright: Neither the name of Adobe Systems Incorporated nor the names +%%Copyright: of its contributors may be used to endorse or promote +%%Copyright: products derived from this software without specific prior +%%Copyright: written permission. +%%Copyright: +%%Copyright: THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +%%Copyright: CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +%%Copyright: INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +%%Copyright: MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +%%Copyright: DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +%%Copyright: CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +%%Copyright: SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +%%Copyright: NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +%%Copyright: LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +%%Copyright: HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +%%Copyright: CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +%%Copyright: OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +%%Copyright: SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +%%Copyright: ----------------------------------------------------------- diff --git a/cps/static/cmaps/NWP-H.bcmap b/cps/static/cmaps/NWP-H.bcmap new file mode 100644 index 00000000..afc5e4b0 Binary files /dev/null and b/cps/static/cmaps/NWP-H.bcmap differ diff --git a/cps/static/cmaps/NWP-V.bcmap b/cps/static/cmaps/NWP-V.bcmap new file mode 100644 index 00000000..bb5785e3 Binary files /dev/null and b/cps/static/cmaps/NWP-V.bcmap differ diff --git a/cps/static/cmaps/RKSJ-H.bcmap b/cps/static/cmaps/RKSJ-H.bcmap new file mode 100644 index 00000000..fb8d298e Binary files /dev/null and b/cps/static/cmaps/RKSJ-H.bcmap differ diff --git a/cps/static/cmaps/RKSJ-V.bcmap b/cps/static/cmaps/RKSJ-V.bcmap new file mode 100644 index 00000000..a2555a6c Binary files /dev/null and b/cps/static/cmaps/RKSJ-V.bcmap differ diff --git a/cps/static/cmaps/Roman.bcmap b/cps/static/cmaps/Roman.bcmap new file mode 100644 index 00000000..f896dcf1 Binary files /dev/null and b/cps/static/cmaps/Roman.bcmap differ diff --git a/cps/static/cmaps/UniCNS-UCS2-H.bcmap b/cps/static/cmaps/UniCNS-UCS2-H.bcmap new file mode 100644 index 00000000..d5db27c5 Binary files /dev/null and b/cps/static/cmaps/UniCNS-UCS2-H.bcmap differ diff --git a/cps/static/cmaps/UniCNS-UCS2-V.bcmap b/cps/static/cmaps/UniCNS-UCS2-V.bcmap new file mode 100644 index 00000000..1dc9b7a2 Binary files /dev/null and b/cps/static/cmaps/UniCNS-UCS2-V.bcmap differ diff --git a/cps/static/cmaps/UniCNS-UTF16-H.bcmap b/cps/static/cmaps/UniCNS-UTF16-H.bcmap new file mode 100644 index 00000000..961afefb Binary files /dev/null and b/cps/static/cmaps/UniCNS-UTF16-H.bcmap differ diff --git a/cps/static/cmaps/UniCNS-UTF16-V.bcmap b/cps/static/cmaps/UniCNS-UTF16-V.bcmap new file mode 100644 index 00000000..df0cffe8 Binary files /dev/null and b/cps/static/cmaps/UniCNS-UTF16-V.bcmap differ diff --git a/cps/static/cmaps/UniCNS-UTF32-H.bcmap b/cps/static/cmaps/UniCNS-UTF32-H.bcmap new file mode 100644 index 00000000..1ab18a14 Binary files /dev/null and b/cps/static/cmaps/UniCNS-UTF32-H.bcmap differ diff --git a/cps/static/cmaps/UniCNS-UTF32-V.bcmap b/cps/static/cmaps/UniCNS-UTF32-V.bcmap new file mode 100644 index 00000000..ad14662e Binary files /dev/null and b/cps/static/cmaps/UniCNS-UTF32-V.bcmap differ diff --git a/cps/static/cmaps/UniCNS-UTF8-H.bcmap b/cps/static/cmaps/UniCNS-UTF8-H.bcmap new file mode 100644 index 00000000..83c6bd7c Binary files /dev/null and b/cps/static/cmaps/UniCNS-UTF8-H.bcmap differ diff --git a/cps/static/cmaps/UniCNS-UTF8-V.bcmap b/cps/static/cmaps/UniCNS-UTF8-V.bcmap new file mode 100644 index 00000000..22a27e4d Binary files /dev/null and b/cps/static/cmaps/UniCNS-UTF8-V.bcmap differ diff --git a/cps/static/cmaps/UniGB-UCS2-H.bcmap b/cps/static/cmaps/UniGB-UCS2-H.bcmap new file mode 100644 index 00000000..5bd6228c Binary files /dev/null and b/cps/static/cmaps/UniGB-UCS2-H.bcmap differ diff --git a/cps/static/cmaps/UniGB-UCS2-V.bcmap b/cps/static/cmaps/UniGB-UCS2-V.bcmap new file mode 100644 index 00000000..53c534b7 Binary files /dev/null and b/cps/static/cmaps/UniGB-UCS2-V.bcmap differ diff --git a/cps/static/cmaps/UniGB-UTF16-H.bcmap b/cps/static/cmaps/UniGB-UTF16-H.bcmap new file mode 100644 index 00000000..b95045b4 Binary files /dev/null and b/cps/static/cmaps/UniGB-UTF16-H.bcmap differ diff --git a/cps/static/cmaps/UniGB-UTF16-V.bcmap b/cps/static/cmaps/UniGB-UTF16-V.bcmap new file mode 100644 index 00000000..51f023e0 Binary files /dev/null and b/cps/static/cmaps/UniGB-UTF16-V.bcmap differ diff --git a/cps/static/cmaps/UniGB-UTF32-H.bcmap b/cps/static/cmaps/UniGB-UTF32-H.bcmap new file mode 100644 index 00000000..f0dbd14f Binary files /dev/null and b/cps/static/cmaps/UniGB-UTF32-H.bcmap differ diff --git a/cps/static/cmaps/UniGB-UTF32-V.bcmap b/cps/static/cmaps/UniGB-UTF32-V.bcmap new file mode 100644 index 00000000..ce9c30a9 Binary files /dev/null and b/cps/static/cmaps/UniGB-UTF32-V.bcmap differ diff --git a/cps/static/cmaps/UniGB-UTF8-H.bcmap b/cps/static/cmaps/UniGB-UTF8-H.bcmap new file mode 100644 index 00000000..982ca462 Binary files /dev/null and b/cps/static/cmaps/UniGB-UTF8-H.bcmap differ diff --git a/cps/static/cmaps/UniGB-UTF8-V.bcmap b/cps/static/cmaps/UniGB-UTF8-V.bcmap new file mode 100644 index 00000000..f78020dd Binary files /dev/null and b/cps/static/cmaps/UniGB-UTF8-V.bcmap differ diff --git a/cps/static/cmaps/UniJIS-UCS2-H.bcmap b/cps/static/cmaps/UniJIS-UCS2-H.bcmap new file mode 100644 index 00000000..7daf56af Binary files /dev/null and b/cps/static/cmaps/UniJIS-UCS2-H.bcmap differ diff --git a/cps/static/cmaps/UniJIS-UCS2-HW-H.bcmap b/cps/static/cmaps/UniJIS-UCS2-HW-H.bcmap new file mode 100644 index 00000000..ac9975c5 Binary files /dev/null and b/cps/static/cmaps/UniJIS-UCS2-HW-H.bcmap differ diff --git a/cps/static/cmaps/UniJIS-UCS2-HW-V.bcmap b/cps/static/cmaps/UniJIS-UCS2-HW-V.bcmap new file mode 100644 index 00000000..3da0a1c6 Binary files /dev/null and b/cps/static/cmaps/UniJIS-UCS2-HW-V.bcmap differ diff --git a/cps/static/cmaps/UniJIS-UCS2-V.bcmap b/cps/static/cmaps/UniJIS-UCS2-V.bcmap new file mode 100644 index 00000000..c50b9ddf Binary files /dev/null and b/cps/static/cmaps/UniJIS-UCS2-V.bcmap differ diff --git a/cps/static/cmaps/UniJIS-UTF16-H.bcmap b/cps/static/cmaps/UniJIS-UTF16-H.bcmap new file mode 100644 index 00000000..67613446 Binary files /dev/null and b/cps/static/cmaps/UniJIS-UTF16-H.bcmap differ diff --git a/cps/static/cmaps/UniJIS-UTF16-V.bcmap b/cps/static/cmaps/UniJIS-UTF16-V.bcmap new file mode 100644 index 00000000..70bf90c0 Binary files /dev/null and b/cps/static/cmaps/UniJIS-UTF16-V.bcmap differ diff --git a/cps/static/cmaps/UniJIS-UTF32-H.bcmap b/cps/static/cmaps/UniJIS-UTF32-H.bcmap new file mode 100644 index 00000000..7a83d53a Binary files /dev/null and b/cps/static/cmaps/UniJIS-UTF32-H.bcmap differ diff --git a/cps/static/cmaps/UniJIS-UTF32-V.bcmap b/cps/static/cmaps/UniJIS-UTF32-V.bcmap new file mode 100644 index 00000000..7a871353 Binary files /dev/null and b/cps/static/cmaps/UniJIS-UTF32-V.bcmap differ diff --git a/cps/static/cmaps/UniJIS-UTF8-H.bcmap b/cps/static/cmaps/UniJIS-UTF8-H.bcmap new file mode 100644 index 00000000..9f0334ca Binary files /dev/null and b/cps/static/cmaps/UniJIS-UTF8-H.bcmap differ diff --git a/cps/static/cmaps/UniJIS-UTF8-V.bcmap b/cps/static/cmaps/UniJIS-UTF8-V.bcmap new file mode 100644 index 00000000..808a94f0 Binary files /dev/null and b/cps/static/cmaps/UniJIS-UTF8-V.bcmap differ diff --git a/cps/static/cmaps/UniJIS2004-UTF16-H.bcmap b/cps/static/cmaps/UniJIS2004-UTF16-H.bcmap new file mode 100644 index 00000000..d768bf81 Binary files /dev/null and b/cps/static/cmaps/UniJIS2004-UTF16-H.bcmap differ diff --git a/cps/static/cmaps/UniJIS2004-UTF16-V.bcmap b/cps/static/cmaps/UniJIS2004-UTF16-V.bcmap new file mode 100644 index 00000000..3d5bf6fb Binary files /dev/null and b/cps/static/cmaps/UniJIS2004-UTF16-V.bcmap differ diff --git a/cps/static/cmaps/UniJIS2004-UTF32-H.bcmap b/cps/static/cmaps/UniJIS2004-UTF32-H.bcmap new file mode 100644 index 00000000..09eee10d Binary files /dev/null and b/cps/static/cmaps/UniJIS2004-UTF32-H.bcmap differ diff --git a/cps/static/cmaps/UniJIS2004-UTF32-V.bcmap b/cps/static/cmaps/UniJIS2004-UTF32-V.bcmap new file mode 100644 index 00000000..6c546001 Binary files /dev/null and b/cps/static/cmaps/UniJIS2004-UTF32-V.bcmap differ diff --git a/cps/static/cmaps/UniJIS2004-UTF8-H.bcmap b/cps/static/cmaps/UniJIS2004-UTF8-H.bcmap new file mode 100644 index 00000000..1b1a64f5 Binary files /dev/null and b/cps/static/cmaps/UniJIS2004-UTF8-H.bcmap differ diff --git a/cps/static/cmaps/UniJIS2004-UTF8-V.bcmap b/cps/static/cmaps/UniJIS2004-UTF8-V.bcmap new file mode 100644 index 00000000..994aa9ef Binary files /dev/null and b/cps/static/cmaps/UniJIS2004-UTF8-V.bcmap differ diff --git a/cps/static/cmaps/UniJISPro-UCS2-HW-V.bcmap b/cps/static/cmaps/UniJISPro-UCS2-HW-V.bcmap new file mode 100644 index 00000000..643f921b Binary files /dev/null and b/cps/static/cmaps/UniJISPro-UCS2-HW-V.bcmap differ diff --git a/cps/static/cmaps/UniJISPro-UCS2-V.bcmap b/cps/static/cmaps/UniJISPro-UCS2-V.bcmap new file mode 100644 index 00000000..c148f67f Binary files /dev/null and b/cps/static/cmaps/UniJISPro-UCS2-V.bcmap differ diff --git a/cps/static/cmaps/UniJISPro-UTF8-V.bcmap b/cps/static/cmaps/UniJISPro-UTF8-V.bcmap new file mode 100644 index 00000000..1849d809 Binary files /dev/null and b/cps/static/cmaps/UniJISPro-UTF8-V.bcmap differ diff --git a/cps/static/cmaps/UniJISX0213-UTF32-H.bcmap b/cps/static/cmaps/UniJISX0213-UTF32-H.bcmap new file mode 100644 index 00000000..a83a677c Binary files /dev/null and b/cps/static/cmaps/UniJISX0213-UTF32-H.bcmap differ diff --git a/cps/static/cmaps/UniJISX0213-UTF32-V.bcmap b/cps/static/cmaps/UniJISX0213-UTF32-V.bcmap new file mode 100644 index 00000000..f527248a Binary files /dev/null and b/cps/static/cmaps/UniJISX0213-UTF32-V.bcmap differ diff --git a/cps/static/cmaps/UniJISX02132004-UTF32-H.bcmap b/cps/static/cmaps/UniJISX02132004-UTF32-H.bcmap new file mode 100644 index 00000000..e1a988dc Binary files /dev/null and b/cps/static/cmaps/UniJISX02132004-UTF32-H.bcmap differ diff --git a/cps/static/cmaps/UniJISX02132004-UTF32-V.bcmap b/cps/static/cmaps/UniJISX02132004-UTF32-V.bcmap new file mode 100644 index 00000000..47e054a9 Binary files /dev/null and b/cps/static/cmaps/UniJISX02132004-UTF32-V.bcmap differ diff --git a/cps/static/cmaps/UniKS-UCS2-H.bcmap b/cps/static/cmaps/UniKS-UCS2-H.bcmap new file mode 100644 index 00000000..b5b94852 Binary files /dev/null and b/cps/static/cmaps/UniKS-UCS2-H.bcmap differ diff --git a/cps/static/cmaps/UniKS-UCS2-V.bcmap b/cps/static/cmaps/UniKS-UCS2-V.bcmap new file mode 100644 index 00000000..026adcaa Binary files /dev/null and b/cps/static/cmaps/UniKS-UCS2-V.bcmap differ diff --git a/cps/static/cmaps/UniKS-UTF16-H.bcmap b/cps/static/cmaps/UniKS-UTF16-H.bcmap new file mode 100644 index 00000000..fd4e66e8 Binary files /dev/null and b/cps/static/cmaps/UniKS-UTF16-H.bcmap differ diff --git a/cps/static/cmaps/UniKS-UTF16-V.bcmap b/cps/static/cmaps/UniKS-UTF16-V.bcmap new file mode 100644 index 00000000..075efb70 Binary files /dev/null and b/cps/static/cmaps/UniKS-UTF16-V.bcmap differ diff --git a/cps/static/cmaps/UniKS-UTF32-H.bcmap b/cps/static/cmaps/UniKS-UTF32-H.bcmap new file mode 100644 index 00000000..769d2142 Binary files /dev/null and b/cps/static/cmaps/UniKS-UTF32-H.bcmap differ diff --git a/cps/static/cmaps/UniKS-UTF32-V.bcmap b/cps/static/cmaps/UniKS-UTF32-V.bcmap new file mode 100644 index 00000000..bdab208b Binary files /dev/null and b/cps/static/cmaps/UniKS-UTF32-V.bcmap differ diff --git a/cps/static/cmaps/UniKS-UTF8-H.bcmap b/cps/static/cmaps/UniKS-UTF8-H.bcmap new file mode 100644 index 00000000..6ff8674a Binary files /dev/null and b/cps/static/cmaps/UniKS-UTF8-H.bcmap differ diff --git a/cps/static/cmaps/UniKS-UTF8-V.bcmap b/cps/static/cmaps/UniKS-UTF8-V.bcmap new file mode 100644 index 00000000..8dfa76a5 Binary files /dev/null and b/cps/static/cmaps/UniKS-UTF8-V.bcmap differ diff --git a/cps/static/cmaps/V.bcmap b/cps/static/cmaps/V.bcmap new file mode 100644 index 00000000..fdec9906 Binary files /dev/null and b/cps/static/cmaps/V.bcmap differ diff --git a/cps/static/cmaps/WP-Symbol.bcmap b/cps/static/cmaps/WP-Symbol.bcmap new file mode 100644 index 00000000..46729bbf Binary files /dev/null and b/cps/static/cmaps/WP-Symbol.bcmap differ diff --git a/cps/static/css/caliBlur.css b/cps/static/css/caliBlur.css index aa747c0b..b4fa6045 100644 --- a/cps/static/css/caliBlur.css +++ b/cps/static/css/caliBlur.css @@ -577,10 +577,6 @@ body.shelforder > div.container-fluid > div.row-fluid > div.col-sm-10:before { color: hsla(0, 0%, 100%, .7) } -div.btn-group[role=group][aria-label="Download, send to Kindle, reading"] > .downloadBtn { - border-left: 2px solid rgba(0, 0, 0, .15) -} - div[aria-label="Edit/Delete book"] > .btn { width: 50px; height: 60px; @@ -1502,7 +1498,7 @@ body > div.container-fluid > div.row-fluid > div.col-sm-10 > div.discover > form } #books > .cover > a:hover, #books_rand > .cover > a:hover, .book.isotope-item > .cover > a:hover, body > div.container-fluid > div.row-fluid > div.col-sm-10 > div.discover > form > div.col-sm-12 > div.col-sm-12 > div.col-sm-2 > a:hover { - outline: solid var(--color-secondary); + /* outline: solid var(--color-secondary); */ font-size: 50px; -o-transition: outline 0s; transition: outline 0s; @@ -2936,8 +2932,9 @@ body > div.container-fluid > div > div.col-sm-10 > div > div > div.col-sm-3.col- } #bookDetailsModal > .modal-dialog.modal-lg > .modal-content > .modal-body > div > div > div > div.col-sm-3.col-lg-3.col-xs-5 > div.cover, body > div.container-fluid > div > div.col-sm-10 > div > div > div.col-sm-3.col-lg-3.col-xs-5 > div.cover { - margin: 0; + margin: auto; width: 100%; + max-width: 200px; } #bookDetailsModal > .modal-dialog.modal-lg > .modal-content > .modal-body > div > div > div > div.col-sm-3.col-lg-3.col-xs-5 > div.cover > img, body > div.container-fluid > div > div.col-sm-10 > div > div > div.col-sm-3.col-lg-3.col-xs-5 > div.cover > img { @@ -3136,7 +3133,7 @@ div.btn-group[role=group][aria-label="Download, send to Kindle, reading"] > div. float: left } -#add-to-shelf, #btnGroupDrop1, #read-in-browser, #sendbtn, .book-meta .btn-toolbar > .btn-group > .btn-group:nth-child(1) > a:first-of-type, .book-meta .btn-toolbar > .btn-group > .btn-warning, .btn-toolbar > .btn-group > #btnGroupDrop2, .btn-toolbar > .btn-group > .btn-group > #btnGroupDrop2 { +#add-to-shelf, #btnGroupDrop1, #readbtn, #sendbtn, .book-meta .btn-toolbar > .btn-group > .btn-group:nth-child(1) > a:first-of-type, .book-meta .btn-toolbar > .btn-group > .btn-warning, .btn-toolbar > .btn-group > #btnGroupDrop2, .btn-toolbar > .btn-group > .btn-group > #btnGroupDrop2 { background: 0 0; color: transparent; width: 50px; @@ -3150,11 +3147,11 @@ div.btn-group[role=group][aria-label="Download, send to Kindle, reading"] > div. padding-bottom: 5px } -#add-to-shelf > span, #btnGroupDrop1 > span, #read-in-browser > span, #sendbtn > span, .book-meta .btn-toolbar > .btn-group > .btn-group:nth-child(1) > a:first-of-type > span, .book-meta .btn-toolbar > .btn-group > .btn-warning > span, .btn-toolbar > .btn-group > #btnGroupDrop2 > span, .btn-toolbar > .btn-group > .btn-group > #btnGroupDrop2 > span { +#add-to-shelf > span, #btnGroupDrop1 > span, #readbtn > span, #sendbtn > span, .book-meta .btn-toolbar > .btn-group > .btn-group:nth-child(1) > a:first-of-type > span, .book-meta .btn-toolbar > .btn-group > .btn-warning > span, .btn-toolbar > .btn-group > #btnGroupDrop2 > span, .btn-toolbar > .btn-group > .btn-group > #btnGroupDrop2 > span { color: hsla(0, 0%, 100%, .7) } -#add-to-shelf:hover span, #btnGroupDrop1:hover > span, #read-in-browser:hover > span, #sendbtn:hover > span, .book-meta .btn-toolbar > .btn-group > .btn-group:nth-child(1) > a:first-of-type:hover > span, .book-meta .btn-toolbar > .btn-group > .btn-warning:hover > span, .btn-toolbar > .btn-group > #btnGroupDrop2:hover > span, .btn-toolbar > .btn-group > .btn-group > #btnGroupDrop2:hover > span { +#add-to-shelf:hover span, #btnGroupDrop1:hover > span, #readbtn:hover > span, #sendbtn:hover > span, .book-meta .btn-toolbar > .btn-group > .btn-group:nth-child(1) > a:first-of-type:hover > span, .book-meta .btn-toolbar > .btn-group > .btn-warning:hover > span, .btn-toolbar > .btn-group > #btnGroupDrop2:hover > span, .btn-toolbar > .btn-group > .btn-group > #btnGroupDrop2:hover > span { color: #fff } @@ -3164,13 +3161,13 @@ div.btn-group[role=group][aria-label="Download, send to Kindle, reading"] > div. font-size: 18px } -#sendbtn > span, .book-meta .btn-toolbar > .btn-group > .btn-group:nth-child(1) > a:first-of-type > span, .book-meta .btn-toolbar > .btn-group > .btn-warning > span.glyphicon-edit { +#sendbtn > span, #readbtn > span, .book-meta .btn-toolbar > .btn-group > .btn-group:nth-child(1) > a:first-of-type > span, .book-meta .btn-toolbar > .btn-group > .btn-warning > span.glyphicon-edit { font-size: 16px; line-height: 54px; width: 100% } -#read-in-browser > span.glyphicon-eye-open:before, .btn-toolbar > .btn-group > .btn-group > #btnGroupDrop2 > span.glyphicon-eye-open:before { +#readbtn > span.glyphicon-book:before, .btn-toolbar > .btn-group > .btn-group > #btnGroupDrop2 > span.glyphicon-book:before { content: "\e352"; font-family: Glyphicons Regular, serif; font-size: 18px; @@ -3293,7 +3290,12 @@ div.btn-group[role=group][aria-label="Download, send to Kindle, reading"] .dropd -ms-transform-origin: center top; transform-origin: center top; border: 0; - left: 0 !important + left: 0 !important; + overflow-y: auto; +} +#add-to-shelves { + max-height: calc(100% - 120px); + overflow-y: auto; } .dropdown-menu > li > a { @@ -5162,11 +5164,11 @@ body.login > div.navbar.navbar-default.navbar-static-top > div > div.navbar-head right: 5px } -#read-in-browser[aria-expanded=true], #shelf-actions > .btn-group.open, .downloadBtn.open, .profileDrop[aria-expanded=true] { +#shelf-actions > .btn-group.open, .downloadBtn.open, .profileDrop[aria-expanded=true] { pointer-events: none } -#btnGroupDrop1[aria-expanded=true] > span, #read-in-browser[aria-expanded=true] > span, #shelf-actions > .btn-group.open > #add-to-shelf > span { +#btnGroupDrop1[aria-expanded=true] > span, #shelf-actions > .btn-group.open > #add-to-shelf > span { color: #fff } @@ -6540,7 +6542,9 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div. height: 150px; margin-bottom: 0 } - + .container-fluid .book .cover img { + width: 100px !important; + } #books .cover img, #books_rand .cover img, .book.isotope-item .cover img { width: 100px !important } @@ -7535,14 +7539,14 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div. margin-left: 8px } -#read-in-browser > span.caret, .btn-toolbar > .btn-group > #btnGroupDrop2 > span.caret, .btn-toolbar > .btn-group > .btn-group > #btnGroupDrop2 > span.caret { +.btn-toolbar > .btn-group > #btnGroupDrop2 > span.caret, .btn-toolbar > .btn-group > .btn-group > #btnGroupDrop2 > span.caret { position: absolute; margin-left: 0; left: 33px; top: 28px } -#read-in-browser > span.glyphicon-eye-open:before, .btn-toolbar > .btn-group > .btn-group > #btnGroupDrop2 > span.glyphicon-eye-open:before { +.btn-toolbar > .btn-group > .btn-group > #btnGroupDrop2 > span.glyphicon-eye-open:before { margin-left: 8px } @@ -7943,7 +7947,7 @@ body.mailset > div.container-fluid > div > div.col-sm-10 > div.discover .glyphic } } -#sendbtn2 { +#sendbtn2, #read-in-browser { background: 0 0; color: transparent; width: 50px; @@ -7953,15 +7957,15 @@ body.mailset > div.container-fluid > div > div.col-sm-10 > div.discover .glyphic padding: 0 } -#sendbtn2 > span { +#sendbtn2 > span, #read-in-browser > span { color: hsla(0, 0%, 100%, .7) } -#sendbtn2 > span.glyphicon-send:before { +#sendbtn2 > span.glyphicon-send:before, #read-in-browser > span.glyphicon-book:before { margin-left: 8px } -#sendbtn2 > span.caret { +#sendbtn2> span.caret, #read-in-browser > span.caret { position: absolute; margin-left: 0; left: 33px; @@ -7969,11 +7973,11 @@ body.mailset > div.container-fluid > div > div.col-sm-10 > div.discover .glyphic padding-bottom: 5px } -#sendbtn2:focus span, #sendbtn2:hover span { +#sendbtn2:focus span, #sendbtn2:hover span, #read-in-browser:focus span, #read-in-browser:hover span { color: #fff } -#sendbtn2[aria-expanded=true] { +#sendbtn2[aria-expanded=true], #read-in-browser[aria-expanded=true] { pointer-events: none } diff --git a/cps/static/css/kthoom.css b/cps/static/css/kthoom.css index 233cfe94..9565cd30 100644 --- a/cps/static/css/kthoom.css +++ b/cps/static/css/kthoom.css @@ -84,15 +84,24 @@ body { #progress .bar-load, #progress .bar-read { display: flex; - align-items: flex-end; - justify-content: flex-end; position: absolute; top: 0; - left: 0; bottom: 0; transition: width 150ms ease-in-out; } +#progress .from-left { + left: 0; + align-items: flex-end; + justify-content: flex-end; +} + +#progress .from-right { + right: 0; + align-items: flex-start; + justify-content: flex-start; +} + #progress .bar-load { color: #000; background-color: #ccc; @@ -218,3 +227,14 @@ th { .dark-theme .overlay { background-color: rgba(0, 0, 0, 0.8); } + +/* Hide scrollbar for Chrome, Safari and Opera */ +.disabled-scrollbar::-webkit-scrollbar { + display: none; +} + +/* Hide scrollbar for IE, Edge and Firefox */ +.disabled-scrollbar { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ +} \ No newline at end of file diff --git a/cps/static/css/libs/bootstrap-table.min.css b/cps/static/css/libs/bootstrap-table.min.css index 8afaf62b..0fa2968e 100644 --- a/cps/static/css/libs/bootstrap-table.min.css +++ b/cps/static/css/libs/bootstrap-table.min.css @@ -1,10 +1,10 @@ /** * bootstrap-table - An extended table to integration with some of the most widely used CSS frameworks. (Supports Bootstrap, Semantic UI, Bulma, Material Design, Foundation) * - * @version v1.18.2 + * @version v1.18.3 * @homepage https://bootstrap-table.com * @author wenzhixin (http://wenzhixin.net.cn/) * @license MIT */ -.bootstrap-table .fixed-table-toolbar::after{content:"";display:block;clear:both}.bootstrap-table .fixed-table-toolbar .bs-bars,.bootstrap-table .fixed-table-toolbar .columns,.bootstrap-table .fixed-table-toolbar .search{position:relative;margin-top:10px;margin-bottom:10px}.bootstrap-table .fixed-table-toolbar .columns .btn-group>.btn-group{display:inline-block;margin-left:-1px!important}.bootstrap-table .fixed-table-toolbar .columns .btn-group>.btn-group>.btn{border-radius:0}.bootstrap-table .fixed-table-toolbar .columns .btn-group>.btn-group:first-child>.btn{border-top-left-radius:4px;border-bottom-left-radius:4px}.bootstrap-table .fixed-table-toolbar .columns .btn-group>.btn-group:last-child>.btn{border-top-right-radius:4px;border-bottom-right-radius:4px}.bootstrap-table .fixed-table-toolbar .columns .dropdown-menu{text-align:left;max-height:300px;overflow:auto;-ms-overflow-style:scrollbar;z-index:1001}.bootstrap-table .fixed-table-toolbar .columns label{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.428571429}.bootstrap-table .fixed-table-toolbar .columns-left{margin-right:5px}.bootstrap-table .fixed-table-toolbar .columns-right{margin-left:5px}.bootstrap-table .fixed-table-toolbar .pull-right .dropdown-menu{right:0;left:auto}.bootstrap-table .fixed-table-container{position:relative;clear:both}.bootstrap-table .fixed-table-container .table{width:100%;margin-bottom:0!important}.bootstrap-table .fixed-table-container .table td,.bootstrap-table .fixed-table-container .table th{vertical-align:middle;box-sizing:border-box}.bootstrap-table .fixed-table-container .table thead th{vertical-align:bottom;padding:0;margin:0}.bootstrap-table .fixed-table-container .table thead th:focus{outline:0 solid transparent}.bootstrap-table .fixed-table-container .table thead th.detail{width:30px}.bootstrap-table .fixed-table-container .table thead th .th-inner{padding:.75rem;vertical-align:bottom;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.bootstrap-table .fixed-table-container .table thead th .sortable{cursor:pointer;background-position:right;background-repeat:no-repeat;padding-right:30px!important}.bootstrap-table .fixed-table-container .table thead th .both{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAQAAADYWf5HAAAAkElEQVQoz7X QMQ5AQBCF4dWQSJxC5wwax1Cq1e7BAdxD5SL+Tq/QCM1oNiJidwox0355mXnG/DrEtIQ6azioNZQxI0ykPhTQIwhCR+BmBYtlK7kLJYwWCcJA9M4qdrZrd8pPjZWPtOqdRQy320YSV17OatFC4euts6z39GYMKRPCTKY9UnPQ6P+GtMRfGtPnBCiqhAeJPmkqAAAAAElFTkSuQmCC")}.bootstrap-table .fixed-table-container .table thead th .asc{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAByUDbMAAAAZ0lEQVQ4y2NgGLKgquEuFxBPAGI2ahhWCsS/gDibUoO0gPgxEP8H4ttArEyuQYxAPBdqEAxPBImTY5gjEL9DM+wTENuQahAvEO9DMwiGdwAxOymGJQLxTyD+jgWDxCMZRsEoGAVoAADeemwtPcZI2wAAAABJRU5ErkJggg==)}.bootstrap-table .fixed-table-container .table thead th .desc{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAByUDbMAAAAZUlEQVQ4y2NgGAWjYBSggaqGu5FA/BOIv2PBIPFEUgxjB+IdQPwfC94HxLykus4GiD+hGfQOiB3J8SojEE9EM2wuSJzcsFMG4ttQgx4DsRalkZENxL+AuJQaMcsGxBOAmGvopk8AVz1sLZgg0bsAAAAASUVORK5CYII=)}.bootstrap-table .fixed-table-container .table tbody tr.selected td{background-color:rgba(0,0,0,.075)}.bootstrap-table .fixed-table-container .table tbody tr.no-records-found td{text-align:center}.bootstrap-table .fixed-table-container .table tbody tr .card-view{display:flex}.bootstrap-table .fixed-table-container .table tbody tr .card-view .card-view-title{font-weight:700;display:inline-block;min-width:30%;text-align:left!important}.bootstrap-table .fixed-table-container .table tbody tr .card-view .card-view-value{width:100%}.bootstrap-table .fixed-table-container .table .bs-checkbox{text-align:center}.bootstrap-table .fixed-table-container .table .bs-checkbox label{margin-bottom:0}.bootstrap-table .fixed-table-container .table .bs-checkbox label input[type=checkbox],.bootstrap-table .fixed-table-container .table .bs-checkbox label input[type=radio]{margin:0 auto!important}.bootstrap-table .fixed-table-container .table.table-sm .th-inner{padding:.3rem}.bootstrap-table .fixed-table-container.fixed-height:not(.has-footer){border-bottom:1px solid #dee2e6}.bootstrap-table .fixed-table-container.fixed-height.has-card-view{border-top:1px solid #dee2e6;border-bottom:1px solid #dee2e6}.bootstrap-table .fixed-table-container.fixed-height .fixed-table-border{border-left:1px solid #dee2e6;border-right:1px solid #dee2e6}.bootstrap-table .fixed-table-container.fixed-height .table thead th{border-bottom:1px solid #dee2e6}.bootstrap-table .fixed-table-container.fixed-height .table-dark thead th{border-bottom:1px solid #32383e}.bootstrap-table .fixed-table-container .fixed-table-header{overflow:hidden}.bootstrap-table .fixed-table-container .fixed-table-body{overflow-x:auto;overflow-y:auto;height:100%}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading{align-items:center;background:#fff;display:flex;justify-content:center;position:absolute;bottom:0;width:100%;z-index:1000;transition:visibility 0s,opacity .15s ease-in-out;opacity:0;visibility:hidden}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.open{visibility:visible;opacity:1}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap{align-items:baseline;display:flex;justify-content:center}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .loading-text{margin-right:6px}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap{align-items:center;display:flex;justify-content:center}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-dot,.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap::after,.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap::before{content:"";animation-duration:1.5s;animation-iteration-count:infinite;animation-name:LOADING;background:#212529;border-radius:50%;display:block;height:5px;margin:0 4px;opacity:0;width:5px}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-dot{animation-delay:.3s}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap::after{animation-delay:.6s}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark{background:#212529}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark .animation-dot,.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark .animation-wrap::after,.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark .animation-wrap::before{background:#fff}.bootstrap-table .fixed-table-container .fixed-table-footer{overflow:hidden}.bootstrap-table .fixed-table-pagination::after{content:"";display:block;clear:both}.bootstrap-table .fixed-table-pagination>.pagination,.bootstrap-table .fixed-table-pagination>.pagination-detail{margin-top:10px;margin-bottom:10px}.bootstrap-table .fixed-table-pagination>.pagination-detail .pagination-info{line-height:34px;margin-right:5px}.bootstrap-table .fixed-table-pagination>.pagination-detail .page-list{display:inline-block}.bootstrap-table .fixed-table-pagination>.pagination-detail .page-list .btn-group{position:relative;display:inline-block;vertical-align:middle}.bootstrap-table .fixed-table-pagination>.pagination-detail .page-list .btn-group .dropdown-menu{margin-bottom:0}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination{margin:0}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination li.page-intermediate a{color:#c8c8c8}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination li.page-intermediate a::before{content:'\2B05'}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination li.page-intermediate a::after{content:'\27A1'}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination li.disabled a{pointer-events:none;cursor:default}.bootstrap-table.fullscreen{position:fixed;top:0;left:0;z-index:1050;width:100%!important;background:#fff;height:calc(100vh);overflow-y:scroll}.bootstrap-table.bootstrap4 .pagination-lg .page-link,.bootstrap-table.bootstrap5 .pagination-lg .page-link{padding:.5rem 1rem}.bootstrap-table.bootstrap5 .float-left{float:left}.bootstrap-table.bootstrap5 .float-right{float:right}div.fixed-table-scroll-inner{width:100%;height:200px}div.fixed-table-scroll-outer{top:0;left:0;visibility:hidden;width:200px;height:150px;overflow:hidden}@keyframes LOADING{0%{opacity:0}50%{opacity:1}to{opacity:0}} \ No newline at end of file +.bootstrap-table .fixed-table-toolbar::after{content:"";display:block;clear:both}.bootstrap-table .fixed-table-toolbar .bs-bars,.bootstrap-table .fixed-table-toolbar .columns,.bootstrap-table .fixed-table-toolbar .search{position:relative;margin-top:10px;margin-bottom:10px}.bootstrap-table .fixed-table-toolbar .columns .btn-group>.btn-group{display:inline-block;margin-left:-1px!important}.bootstrap-table .fixed-table-toolbar .columns .btn-group>.btn-group>.btn{border-radius:0}.bootstrap-table .fixed-table-toolbar .columns .btn-group>.btn-group:first-child>.btn{border-top-left-radius:4px;border-bottom-left-radius:4px}.bootstrap-table .fixed-table-toolbar .columns .btn-group>.btn-group:last-child>.btn{border-top-right-radius:4px;border-bottom-right-radius:4px}.bootstrap-table .fixed-table-toolbar .columns .dropdown-menu{text-align:left;max-height:300px;overflow:auto;-ms-overflow-style:scrollbar;z-index:1001}.bootstrap-table .fixed-table-toolbar .columns label{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.428571429}.bootstrap-table .fixed-table-toolbar .columns-left{margin-right:5px}.bootstrap-table .fixed-table-toolbar .columns-right{margin-left:5px}.bootstrap-table .fixed-table-toolbar .pull-right .dropdown-menu{right:0;left:auto}.bootstrap-table .fixed-table-container{position:relative;clear:both}.bootstrap-table .fixed-table-container .table{width:100%;margin-bottom:0!important}.bootstrap-table .fixed-table-container .table td,.bootstrap-table .fixed-table-container .table th{vertical-align:middle;box-sizing:border-box}.bootstrap-table .fixed-table-container .table thead th{vertical-align:bottom;padding:0;margin:0}.bootstrap-table .fixed-table-container .table thead th:focus{outline:0 solid transparent}.bootstrap-table .fixed-table-container .table thead th.detail{width:30px}.bootstrap-table .fixed-table-container .table thead th .th-inner{padding:.75rem;vertical-align:bottom;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.bootstrap-table .fixed-table-container .table thead th .sortable{cursor:pointer;background-position:right;background-repeat:no-repeat;padding-right:30px!important}.bootstrap-table .fixed-table-container .table thead th .both{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAQAAADYWf5HAAAAkElEQVQoz7X QMQ5AQBCF4dWQSJxC5wwax1Cq1e7BAdxD5SL+Tq/QCM1oNiJidwox0355mXnG/DrEtIQ6azioNZQxI0ykPhTQIwhCR+BmBYtlK7kLJYwWCcJA9M4qdrZrd8pPjZWPtOqdRQy320YSV17OatFC4euts6z39GYMKRPCTKY9UnPQ6P+GtMRfGtPnBCiqhAeJPmkqAAAAAElFTkSuQmCC")}.bootstrap-table .fixed-table-container .table thead th .asc{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAByUDbMAAAAZ0lEQVQ4y2NgGLKgquEuFxBPAGI2ahhWCsS/gDibUoO0gPgxEP8H4ttArEyuQYxAPBdqEAxPBImTY5gjEL9DM+wTENuQahAvEO9DMwiGdwAxOymGJQLxTyD+jgWDxCMZRsEoGAVoAADeemwtPcZI2wAAAABJRU5ErkJggg==")}.bootstrap-table .fixed-table-container .table thead th .desc{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAByUDbMAAAAZUlEQVQ4y2NgGAWjYBSggaqGu5FA/BOIv2PBIPFEUgxjB+IdQPwfC94HxLykus4GiD+hGfQOiB3J8SojEE9EM2wuSJzcsFMG4ttQgx4DsRalkZENxL+AuJQaMcsGxBOAmGvopk8AVz1sLZgg0bsAAAAASUVORK5CYII= ")}.bootstrap-table .fixed-table-container .table tbody tr.selected td{background-color:rgba(0,0,0,.075)}.bootstrap-table .fixed-table-container .table tbody tr.no-records-found td{text-align:center}.bootstrap-table .fixed-table-container .table tbody tr .card-view{display:flex}.bootstrap-table .fixed-table-container .table tbody tr .card-view .card-view-title{font-weight:700;display:inline-block;min-width:30%;width:auto!important;text-align:left!important}.bootstrap-table .fixed-table-container .table tbody tr .card-view .card-view-value{width:100%!important}.bootstrap-table .fixed-table-container .table .bs-checkbox{text-align:center}.bootstrap-table .fixed-table-container .table .bs-checkbox label{margin-bottom:0}.bootstrap-table .fixed-table-container .table .bs-checkbox label input[type=checkbox],.bootstrap-table .fixed-table-container .table .bs-checkbox label input[type=radio]{margin:0 auto!important}.bootstrap-table .fixed-table-container .table.table-sm .th-inner{padding:.3rem}.bootstrap-table .fixed-table-container.fixed-height:not(.has-footer){border-bottom:1px solid #dee2e6}.bootstrap-table .fixed-table-container.fixed-height.has-card-view{border-top:1px solid #dee2e6;border-bottom:1px solid #dee2e6}.bootstrap-table .fixed-table-container.fixed-height .fixed-table-border{border-left:1px solid #dee2e6;border-right:1px solid #dee2e6}.bootstrap-table .fixed-table-container.fixed-height .table thead th{border-bottom:1px solid #dee2e6}.bootstrap-table .fixed-table-container.fixed-height .table-dark thead th{border-bottom:1px solid #32383e}.bootstrap-table .fixed-table-container .fixed-table-header{overflow:hidden}.bootstrap-table .fixed-table-container .fixed-table-body{overflow-x:auto;overflow-y:auto;height:100%}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading{align-items:center;background:#fff;display:flex;justify-content:center;position:absolute;bottom:0;width:100%;z-index:1000;transition:visibility 0s,opacity .15s ease-in-out;opacity:0;visibility:hidden}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.open{visibility:visible;opacity:1}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap{align-items:baseline;display:flex;justify-content:center}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .loading-text{margin-right:6px}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap{align-items:center;display:flex;justify-content:center}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-dot,.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap::after,.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap::before{content:"";animation-duration:1.5s;animation-iteration-count:infinite;animation-name:LOADING;background:#212529;border-radius:50%;display:block;height:5px;margin:0 4px;opacity:0;width:5px}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-dot{animation-delay:.3s}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap::after{animation-delay:.6s}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark{background:#212529}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark .animation-dot,.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark .animation-wrap::after,.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark .animation-wrap::before{background:#fff}.bootstrap-table .fixed-table-container .fixed-table-footer{overflow:hidden}.bootstrap-table .fixed-table-pagination::after{content:"";display:block;clear:both}.bootstrap-table .fixed-table-pagination>.pagination,.bootstrap-table .fixed-table-pagination>.pagination-detail{margin-top:10px;margin-bottom:10px}.bootstrap-table .fixed-table-pagination>.pagination-detail .pagination-info{line-height:34px;margin-right:5px}.bootstrap-table .fixed-table-pagination>.pagination-detail .page-list{display:inline-block}.bootstrap-table .fixed-table-pagination>.pagination-detail .page-list .btn-group{position:relative;display:inline-block;vertical-align:middle}.bootstrap-table .fixed-table-pagination>.pagination-detail .page-list .btn-group .dropdown-menu{margin-bottom:0}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination{margin:0}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination li.page-intermediate a{color:#c8c8c8}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination li.page-intermediate a::before{content:'\2B05'}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination li.page-intermediate a::after{content:'\27A1'}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination li.disabled a{pointer-events:none;cursor:default}.bootstrap-table.fullscreen{position:fixed;top:0;left:0;z-index:1050;width:100%!important;background:#fff;height:calc(100vh);overflow-y:scroll}.bootstrap-table.bootstrap4 .pagination-lg .page-link,.bootstrap-table.bootstrap5 .pagination-lg .page-link{padding:.5rem 1rem}.bootstrap-table.bootstrap5 .float-left{float:left}.bootstrap-table.bootstrap5 .float-right{float:right}div.fixed-table-scroll-inner{width:100%;height:200px}div.fixed-table-scroll-outer{top:0;left:0;visibility:hidden;width:200px;height:150px;overflow:hidden}@keyframes LOADING{0%{opacity:0}50%{opacity:1}to{opacity:0}} \ No newline at end of file diff --git a/cps/static/css/libs/images/findbarButton-next-dark.svg b/cps/static/css/libs/images/findbarButton-next-dark.svg new file mode 100644 index 00000000..80df70bc --- /dev/null +++ b/cps/static/css/libs/images/findbarButton-next-dark.svg @@ -0,0 +1,6 @@ + + \ No newline at end of file diff --git a/cps/static/css/libs/images/findbarButton-next-rtl.png b/cps/static/css/libs/images/findbarButton-next-rtl.png deleted file mode 100644 index bef02743..00000000 Binary files a/cps/static/css/libs/images/findbarButton-next-rtl.png and /dev/null differ diff --git a/cps/static/css/libs/images/findbarButton-next-rtl@2x.png b/cps/static/css/libs/images/findbarButton-next-rtl@2x.png deleted file mode 100644 index 1da6dc94..00000000 Binary files a/cps/static/css/libs/images/findbarButton-next-rtl@2x.png and /dev/null differ diff --git a/cps/static/css/libs/images/findbarButton-next.png b/cps/static/css/libs/images/findbarButton-next.png deleted file mode 100644 index de1d0fc9..00000000 Binary files a/cps/static/css/libs/images/findbarButton-next.png and /dev/null differ diff --git a/cps/static/css/libs/images/findbarButton-next.svg b/cps/static/css/libs/images/findbarButton-next.svg new file mode 100644 index 00000000..a81eb029 --- /dev/null +++ b/cps/static/css/libs/images/findbarButton-next.svg @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/cps/static/css/libs/images/findbarButton-next@2x.png b/cps/static/css/libs/images/findbarButton-next@2x.png deleted file mode 100644 index 0250307c..00000000 Binary files a/cps/static/css/libs/images/findbarButton-next@2x.png and /dev/null differ diff --git a/cps/static/css/libs/images/findbarButton-previous-dark.svg b/cps/static/css/libs/images/findbarButton-previous-dark.svg new file mode 100644 index 00000000..d304a9b8 --- /dev/null +++ b/cps/static/css/libs/images/findbarButton-previous-dark.svg @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/cps/static/css/libs/images/findbarButton-previous-rtl.png b/cps/static/css/libs/images/findbarButton-previous-rtl.png deleted file mode 100644 index de1d0fc9..00000000 Binary files a/cps/static/css/libs/images/findbarButton-previous-rtl.png and /dev/null differ diff --git a/cps/static/css/libs/images/findbarButton-previous-rtl@2x.png b/cps/static/css/libs/images/findbarButton-previous-rtl@2x.png deleted file mode 100644 index 0250307c..00000000 Binary files a/cps/static/css/libs/images/findbarButton-previous-rtl@2x.png and /dev/null differ diff --git a/cps/static/css/libs/images/findbarButton-previous.png b/cps/static/css/libs/images/findbarButton-previous.png deleted file mode 100644 index bef02743..00000000 Binary files a/cps/static/css/libs/images/findbarButton-previous.png and /dev/null differ diff --git a/cps/static/css/libs/images/findbarButton-previous.svg b/cps/static/css/libs/images/findbarButton-previous.svg new file mode 100644 index 00000000..5fd70322 --- /dev/null +++ b/cps/static/css/libs/images/findbarButton-previous.svg @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/cps/static/css/libs/images/findbarButton-previous@2x.png b/cps/static/css/libs/images/findbarButton-previous@2x.png deleted file mode 100644 index 1da6dc94..00000000 Binary files a/cps/static/css/libs/images/findbarButton-previous@2x.png and /dev/null differ diff --git a/cps/static/css/libs/images/loading-dark.svg b/cps/static/css/libs/images/loading-dark.svg new file mode 100644 index 00000000..fa5269b1 --- /dev/null +++ b/cps/static/css/libs/images/loading-dark.svg @@ -0,0 +1,24 @@ + \ No newline at end of file diff --git a/cps/static/css/libs/images/loading-small.png b/cps/static/css/libs/images/loading-small.png deleted file mode 100644 index 8831a805..00000000 Binary files a/cps/static/css/libs/images/loading-small.png and /dev/null differ diff --git a/cps/static/css/libs/images/loading-small@2x.png b/cps/static/css/libs/images/loading-small@2x.png deleted file mode 100644 index b25b4452..00000000 Binary files a/cps/static/css/libs/images/loading-small@2x.png and /dev/null differ diff --git a/cps/static/css/libs/images/loading.svg b/cps/static/css/libs/images/loading.svg new file mode 100644 index 00000000..0a15ff68 --- /dev/null +++ b/cps/static/css/libs/images/loading.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/cps/static/css/libs/images/secondaryToolbarButton-documentProperties-dark.svg b/cps/static/css/libs/images/secondaryToolbarButton-documentProperties-dark.svg new file mode 100644 index 00000000..306e628d --- /dev/null +++ b/cps/static/css/libs/images/secondaryToolbarButton-documentProperties-dark.svg @@ -0,0 +1,16 @@ + + + + + + + + + \ No newline at end of file diff --git a/cps/static/css/libs/images/secondaryToolbarButton-documentProperties.png b/cps/static/css/libs/images/secondaryToolbarButton-documentProperties.png deleted file mode 100644 index 40925e25..00000000 Binary files a/cps/static/css/libs/images/secondaryToolbarButton-documentProperties.png and /dev/null differ diff --git a/cps/static/css/libs/images/secondaryToolbarButton-documentProperties.svg b/cps/static/css/libs/images/secondaryToolbarButton-documentProperties.svg new file mode 100644 index 00000000..6bd55cda --- /dev/null +++ b/cps/static/css/libs/images/secondaryToolbarButton-documentProperties.svg @@ -0,0 +1,15 @@ + + + + + + + + + \ No newline at end of file diff --git a/cps/static/css/libs/images/secondaryToolbarButton-documentProperties@2x.png b/cps/static/css/libs/images/secondaryToolbarButton-documentProperties@2x.png deleted file mode 100644 index adb240ea..00000000 Binary files a/cps/static/css/libs/images/secondaryToolbarButton-documentProperties@2x.png and /dev/null differ diff --git a/cps/static/css/libs/images/secondaryToolbarButton-firstPage-dark.svg b/cps/static/css/libs/images/secondaryToolbarButton-firstPage-dark.svg new file mode 100644 index 00000000..c13ff867 --- /dev/null +++ b/cps/static/css/libs/images/secondaryToolbarButton-firstPage-dark.svg @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/cps/static/css/libs/images/secondaryToolbarButton-firstPage.png b/cps/static/css/libs/images/secondaryToolbarButton-firstPage.png deleted file mode 100644 index e68846aa..00000000 Binary files a/cps/static/css/libs/images/secondaryToolbarButton-firstPage.png and /dev/null differ diff --git a/cps/static/css/libs/images/secondaryToolbarButton-firstPage.svg b/cps/static/css/libs/images/secondaryToolbarButton-firstPage.svg new file mode 100644 index 00000000..2fa0fa6d --- /dev/null +++ b/cps/static/css/libs/images/secondaryToolbarButton-firstPage.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/cps/static/css/libs/images/secondaryToolbarButton-firstPage@2x.png b/cps/static/css/libs/images/secondaryToolbarButton-firstPage@2x.png deleted file mode 100644 index 3ad8af51..00000000 Binary files a/cps/static/css/libs/images/secondaryToolbarButton-firstPage@2x.png and /dev/null differ diff --git a/cps/static/css/libs/images/secondaryToolbarButton-handTool-dark.svg b/cps/static/css/libs/images/secondaryToolbarButton-handTool-dark.svg new file mode 100644 index 00000000..834d8b0d --- /dev/null +++ b/cps/static/css/libs/images/secondaryToolbarButton-handTool-dark.svg @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/cps/static/css/libs/images/secondaryToolbarButton-handTool.png b/cps/static/css/libs/images/secondaryToolbarButton-handTool.png deleted file mode 100644 index cb85a841..00000000 Binary files a/cps/static/css/libs/images/secondaryToolbarButton-handTool.png and /dev/null differ diff --git a/cps/static/css/libs/images/secondaryToolbarButton-handTool.svg b/cps/static/css/libs/images/secondaryToolbarButton-handTool.svg new file mode 100644 index 00000000..3d038fab --- /dev/null +++ b/cps/static/css/libs/images/secondaryToolbarButton-handTool.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/cps/static/css/libs/images/secondaryToolbarButton-handTool@2x.png b/cps/static/css/libs/images/secondaryToolbarButton-handTool@2x.png deleted file mode 100644 index 5c13f77f..00000000 Binary files a/cps/static/css/libs/images/secondaryToolbarButton-handTool@2x.png and /dev/null differ diff --git a/cps/static/css/libs/images/secondaryToolbarButton-lastPage-dark.svg b/cps/static/css/libs/images/secondaryToolbarButton-lastPage-dark.svg new file mode 100644 index 00000000..8633e420 --- /dev/null +++ b/cps/static/css/libs/images/secondaryToolbarButton-lastPage-dark.svg @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/cps/static/css/libs/images/secondaryToolbarButton-lastPage.png b/cps/static/css/libs/images/secondaryToolbarButton-lastPage.png deleted file mode 100644 index be763e0c..00000000 Binary files a/cps/static/css/libs/images/secondaryToolbarButton-lastPage.png and /dev/null differ diff --git a/cps/static/css/libs/images/secondaryToolbarButton-lastPage.svg b/cps/static/css/libs/images/secondaryToolbarButton-lastPage.svg new file mode 100644 index 00000000..53fa9a6d --- /dev/null +++ b/cps/static/css/libs/images/secondaryToolbarButton-lastPage.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/cps/static/css/libs/images/secondaryToolbarButton-lastPage@2x.png b/cps/static/css/libs/images/secondaryToolbarButton-lastPage@2x.png deleted file mode 100644 index 8570984f..00000000 Binary files a/cps/static/css/libs/images/secondaryToolbarButton-lastPage@2x.png and /dev/null differ diff --git a/cps/static/css/libs/images/secondaryToolbarButton-rotateCcw-dark.svg b/cps/static/css/libs/images/secondaryToolbarButton-rotateCcw-dark.svg new file mode 100644 index 00000000..1a92f802 --- /dev/null +++ b/cps/static/css/libs/images/secondaryToolbarButton-rotateCcw-dark.svg @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/cps/static/css/libs/images/secondaryToolbarButton-rotateCcw.png b/cps/static/css/libs/images/secondaryToolbarButton-rotateCcw.png deleted file mode 100644 index 675d6da2..00000000 Binary files a/cps/static/css/libs/images/secondaryToolbarButton-rotateCcw.png and /dev/null differ diff --git a/cps/static/css/libs/images/secondaryToolbarButton-rotateCcw.svg b/cps/static/css/libs/images/secondaryToolbarButton-rotateCcw.svg new file mode 100644 index 00000000..c71ea8e8 --- /dev/null +++ b/cps/static/css/libs/images/secondaryToolbarButton-rotateCcw.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/cps/static/css/libs/images/secondaryToolbarButton-rotateCcw@2x.png b/cps/static/css/libs/images/secondaryToolbarButton-rotateCcw@2x.png deleted file mode 100644 index b9e74312..00000000 Binary files a/cps/static/css/libs/images/secondaryToolbarButton-rotateCcw@2x.png and /dev/null differ diff --git a/cps/static/css/libs/images/secondaryToolbarButton-rotateCw-dark.svg b/cps/static/css/libs/images/secondaryToolbarButton-rotateCw-dark.svg new file mode 100644 index 00000000..2a4ef738 --- /dev/null +++ b/cps/static/css/libs/images/secondaryToolbarButton-rotateCw-dark.svg @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/cps/static/css/libs/images/secondaryToolbarButton-rotateCw.png b/cps/static/css/libs/images/secondaryToolbarButton-rotateCw.png deleted file mode 100644 index e1c75988..00000000 Binary files a/cps/static/css/libs/images/secondaryToolbarButton-rotateCw.png and /dev/null differ diff --git a/cps/static/css/libs/images/secondaryToolbarButton-rotateCw.svg b/cps/static/css/libs/images/secondaryToolbarButton-rotateCw.svg new file mode 100644 index 00000000..e1e19e73 --- /dev/null +++ b/cps/static/css/libs/images/secondaryToolbarButton-rotateCw.svg @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/cps/static/css/libs/images/secondaryToolbarButton-rotateCw@2x.png b/cps/static/css/libs/images/secondaryToolbarButton-rotateCw@2x.png deleted file mode 100644 index cb257b41..00000000 Binary files a/cps/static/css/libs/images/secondaryToolbarButton-rotateCw@2x.png and /dev/null differ diff --git a/cps/static/css/libs/images/secondaryToolbarButton-scrollHorizontal-dark.svg b/cps/static/css/libs/images/secondaryToolbarButton-scrollHorizontal-dark.svg new file mode 100644 index 00000000..337f85ef --- /dev/null +++ b/cps/static/css/libs/images/secondaryToolbarButton-scrollHorizontal-dark.svg @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/cps/static/css/libs/images/secondaryToolbarButton-scrollHorizontal.png b/cps/static/css/libs/images/secondaryToolbarButton-scrollHorizontal.png deleted file mode 100644 index cb702fc4..00000000 Binary files a/cps/static/css/libs/images/secondaryToolbarButton-scrollHorizontal.png and /dev/null differ diff --git a/cps/static/css/libs/images/secondaryToolbarButton-scrollHorizontal.svg b/cps/static/css/libs/images/secondaryToolbarButton-scrollHorizontal.svg new file mode 100644 index 00000000..8693eec3 --- /dev/null +++ b/cps/static/css/libs/images/secondaryToolbarButton-scrollHorizontal.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/cps/static/css/libs/images/secondaryToolbarButton-scrollHorizontal@2x.png b/cps/static/css/libs/images/secondaryToolbarButton-scrollHorizontal@2x.png deleted file mode 100644 index 7f05289b..00000000 Binary files a/cps/static/css/libs/images/secondaryToolbarButton-scrollHorizontal@2x.png and /dev/null differ diff --git a/cps/static/css/libs/images/secondaryToolbarButton-scrollVertical-dark.svg b/cps/static/css/libs/images/secondaryToolbarButton-scrollVertical-dark.svg new file mode 100644 index 00000000..41bdd8f1 --- /dev/null +++ b/cps/static/css/libs/images/secondaryToolbarButton-scrollVertical-dark.svg @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/cps/static/css/libs/images/secondaryToolbarButton-scrollVertical.png b/cps/static/css/libs/images/secondaryToolbarButton-scrollVertical.png deleted file mode 100644 index 0b8427a1..00000000 Binary files a/cps/static/css/libs/images/secondaryToolbarButton-scrollVertical.png and /dev/null differ diff --git a/cps/static/css/libs/images/secondaryToolbarButton-scrollVertical.svg b/cps/static/css/libs/images/secondaryToolbarButton-scrollVertical.svg new file mode 100644 index 00000000..ee1cf22f --- /dev/null +++ b/cps/static/css/libs/images/secondaryToolbarButton-scrollVertical.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/cps/static/css/libs/images/secondaryToolbarButton-scrollVertical@2x.png b/cps/static/css/libs/images/secondaryToolbarButton-scrollVertical@2x.png deleted file mode 100644 index 72ab55eb..00000000 Binary files a/cps/static/css/libs/images/secondaryToolbarButton-scrollVertical@2x.png and /dev/null differ diff --git a/cps/static/css/libs/images/secondaryToolbarButton-scrollWrapped-dark.svg b/cps/static/css/libs/images/secondaryToolbarButton-scrollWrapped-dark.svg new file mode 100644 index 00000000..cd50526f --- /dev/null +++ b/cps/static/css/libs/images/secondaryToolbarButton-scrollWrapped-dark.svg @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/cps/static/css/libs/images/secondaryToolbarButton-scrollWrapped.png b/cps/static/css/libs/images/secondaryToolbarButton-scrollWrapped.png deleted file mode 100644 index 165fc8bc..00000000 Binary files a/cps/static/css/libs/images/secondaryToolbarButton-scrollWrapped.png and /dev/null differ diff --git a/cps/static/css/libs/images/secondaryToolbarButton-scrollWrapped.svg b/cps/static/css/libs/images/secondaryToolbarButton-scrollWrapped.svg new file mode 100644 index 00000000..804e7469 --- /dev/null +++ b/cps/static/css/libs/images/secondaryToolbarButton-scrollWrapped.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/cps/static/css/libs/images/secondaryToolbarButton-scrollWrapped@2x.png b/cps/static/css/libs/images/secondaryToolbarButton-scrollWrapped@2x.png deleted file mode 100644 index 42461411..00000000 Binary files a/cps/static/css/libs/images/secondaryToolbarButton-scrollWrapped@2x.png and /dev/null differ diff --git a/cps/static/css/libs/images/secondaryToolbarButton-selectTool-dark.svg b/cps/static/css/libs/images/secondaryToolbarButton-selectTool-dark.svg new file mode 100644 index 00000000..7a95098a --- /dev/null +++ b/cps/static/css/libs/images/secondaryToolbarButton-selectTool-dark.svg @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/cps/static/css/libs/images/secondaryToolbarButton-selectTool.png b/cps/static/css/libs/images/secondaryToolbarButton-selectTool.png deleted file mode 100644 index 25520a6f..00000000 Binary files a/cps/static/css/libs/images/secondaryToolbarButton-selectTool.png and /dev/null differ diff --git a/cps/static/css/libs/images/secondaryToolbarButton-selectTool.svg b/cps/static/css/libs/images/secondaryToolbarButton-selectTool.svg new file mode 100644 index 00000000..43e97894 --- /dev/null +++ b/cps/static/css/libs/images/secondaryToolbarButton-selectTool.svg @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/cps/static/css/libs/images/secondaryToolbarButton-selectTool@2x.png b/cps/static/css/libs/images/secondaryToolbarButton-selectTool@2x.png deleted file mode 100644 index a58aaef4..00000000 Binary files a/cps/static/css/libs/images/secondaryToolbarButton-selectTool@2x.png and /dev/null differ diff --git a/cps/static/css/libs/images/secondaryToolbarButton-spreadEven-dark.svg b/cps/static/css/libs/images/secondaryToolbarButton-spreadEven-dark.svg new file mode 100644 index 00000000..0c9586ed --- /dev/null +++ b/cps/static/css/libs/images/secondaryToolbarButton-spreadEven-dark.svg @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/cps/static/css/libs/images/secondaryToolbarButton-spreadEven.png b/cps/static/css/libs/images/secondaryToolbarButton-spreadEven.png deleted file mode 100644 index 3fa07e70..00000000 Binary files a/cps/static/css/libs/images/secondaryToolbarButton-spreadEven.png and /dev/null differ diff --git a/cps/static/css/libs/images/secondaryToolbarButton-spreadEven.svg b/cps/static/css/libs/images/secondaryToolbarButton-spreadEven.svg new file mode 100644 index 00000000..ddec5e68 --- /dev/null +++ b/cps/static/css/libs/images/secondaryToolbarButton-spreadEven.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/cps/static/css/libs/images/secondaryToolbarButton-spreadEven@2x.png b/cps/static/css/libs/images/secondaryToolbarButton-spreadEven@2x.png deleted file mode 100644 index 32e5033d..00000000 Binary files a/cps/static/css/libs/images/secondaryToolbarButton-spreadEven@2x.png and /dev/null differ diff --git a/cps/static/css/libs/images/secondaryToolbarButton-spreadNone-dark.svg b/cps/static/css/libs/images/secondaryToolbarButton-spreadNone-dark.svg new file mode 100644 index 00000000..75e1b985 --- /dev/null +++ b/cps/static/css/libs/images/secondaryToolbarButton-spreadNone-dark.svg @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/cps/static/css/libs/images/secondaryToolbarButton-spreadNone.png b/cps/static/css/libs/images/secondaryToolbarButton-spreadNone.png deleted file mode 100644 index 16114735..00000000 Binary files a/cps/static/css/libs/images/secondaryToolbarButton-spreadNone.png and /dev/null differ diff --git a/cps/static/css/libs/images/secondaryToolbarButton-spreadNone.svg b/cps/static/css/libs/images/secondaryToolbarButton-spreadNone.svg new file mode 100644 index 00000000..63318c56 --- /dev/null +++ b/cps/static/css/libs/images/secondaryToolbarButton-spreadNone.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/cps/static/css/libs/images/secondaryToolbarButton-spreadNone@2x.png b/cps/static/css/libs/images/secondaryToolbarButton-spreadNone@2x.png deleted file mode 100644 index 8e51cf3b..00000000 Binary files a/cps/static/css/libs/images/secondaryToolbarButton-spreadNone@2x.png and /dev/null differ diff --git a/cps/static/css/libs/images/secondaryToolbarButton-spreadOdd-dark.svg b/cps/static/css/libs/images/secondaryToolbarButton-spreadOdd-dark.svg new file mode 100644 index 00000000..8dff9598 --- /dev/null +++ b/cps/static/css/libs/images/secondaryToolbarButton-spreadOdd-dark.svg @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/cps/static/css/libs/images/secondaryToolbarButton-spreadOdd.png b/cps/static/css/libs/images/secondaryToolbarButton-spreadOdd.png deleted file mode 100644 index 5126313a..00000000 Binary files a/cps/static/css/libs/images/secondaryToolbarButton-spreadOdd.png and /dev/null differ diff --git a/cps/static/css/libs/images/secondaryToolbarButton-spreadOdd.svg b/cps/static/css/libs/images/secondaryToolbarButton-spreadOdd.svg new file mode 100644 index 00000000..29909e9f --- /dev/null +++ b/cps/static/css/libs/images/secondaryToolbarButton-spreadOdd.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/cps/static/css/libs/images/secondaryToolbarButton-spreadOdd@2x.png b/cps/static/css/libs/images/secondaryToolbarButton-spreadOdd@2x.png deleted file mode 100644 index 5996b74d..00000000 Binary files a/cps/static/css/libs/images/secondaryToolbarButton-spreadOdd@2x.png and /dev/null differ diff --git a/cps/static/css/libs/images/shadow.png b/cps/static/css/libs/images/shadow.png index 31d3bdb1..a00061ac 100644 Binary files a/cps/static/css/libs/images/shadow.png and b/cps/static/css/libs/images/shadow.png differ diff --git a/cps/static/css/libs/images/texture.png b/cps/static/css/libs/images/texture.png deleted file mode 100644 index 12bae83a..00000000 Binary files a/cps/static/css/libs/images/texture.png and /dev/null differ diff --git a/cps/static/css/libs/images/toolbarButton-bookmark-dark.svg b/cps/static/css/libs/images/toolbarButton-bookmark-dark.svg new file mode 100644 index 00000000..7bf33297 --- /dev/null +++ b/cps/static/css/libs/images/toolbarButton-bookmark-dark.svg @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/cps/static/css/libs/images/toolbarButton-bookmark.png b/cps/static/css/libs/images/toolbarButton-bookmark.png deleted file mode 100644 index a187be6c..00000000 Binary files a/cps/static/css/libs/images/toolbarButton-bookmark.png and /dev/null differ diff --git a/cps/static/css/libs/images/toolbarButton-bookmark.svg b/cps/static/css/libs/images/toolbarButton-bookmark.svg new file mode 100644 index 00000000..79d39b09 --- /dev/null +++ b/cps/static/css/libs/images/toolbarButton-bookmark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/cps/static/css/libs/images/toolbarButton-bookmark@2x.png b/cps/static/css/libs/images/toolbarButton-bookmark@2x.png deleted file mode 100644 index 4efbaa67..00000000 Binary files a/cps/static/css/libs/images/toolbarButton-bookmark@2x.png and /dev/null differ diff --git a/cps/static/css/libs/images/toolbarButton-download-dark.svg b/cps/static/css/libs/images/toolbarButton-download-dark.svg new file mode 100644 index 00000000..d2a92e5d --- /dev/null +++ b/cps/static/css/libs/images/toolbarButton-download-dark.svg @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/cps/static/css/libs/images/toolbarButton-download.png b/cps/static/css/libs/images/toolbarButton-download.png deleted file mode 100644 index eaab35f0..00000000 Binary files a/cps/static/css/libs/images/toolbarButton-download.png and /dev/null differ diff --git a/cps/static/css/libs/images/toolbarButton-download.svg b/cps/static/css/libs/images/toolbarButton-download.svg new file mode 100644 index 00000000..2cdb5db3 --- /dev/null +++ b/cps/static/css/libs/images/toolbarButton-download.svg @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/cps/static/css/libs/images/toolbarButton-download@2x.png b/cps/static/css/libs/images/toolbarButton-download@2x.png deleted file mode 100644 index 896face4..00000000 Binary files a/cps/static/css/libs/images/toolbarButton-download@2x.png and /dev/null differ diff --git a/cps/static/css/libs/images/toolbarButton-menuArrow-dark.svg b/cps/static/css/libs/images/toolbarButton-menuArrow-dark.svg new file mode 100644 index 00000000..eb7f50e6 --- /dev/null +++ b/cps/static/css/libs/images/toolbarButton-menuArrow-dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/cps/static/css/libs/images/toolbarButton-menuArrow.svg b/cps/static/css/libs/images/toolbarButton-menuArrow.svg new file mode 100644 index 00000000..46e41e18 --- /dev/null +++ b/cps/static/css/libs/images/toolbarButton-menuArrow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/cps/static/css/libs/images/toolbarButton-menuArrows.png b/cps/static/css/libs/images/toolbarButton-menuArrows.png deleted file mode 100644 index e50ca4ee..00000000 Binary files a/cps/static/css/libs/images/toolbarButton-menuArrows.png and /dev/null differ diff --git a/cps/static/css/libs/images/toolbarButton-menuArrows@2x.png b/cps/static/css/libs/images/toolbarButton-menuArrows@2x.png deleted file mode 100644 index f7570bc0..00000000 Binary files a/cps/static/css/libs/images/toolbarButton-menuArrows@2x.png and /dev/null differ diff --git a/cps/static/css/libs/images/toolbarButton-openFile-dark.svg b/cps/static/css/libs/images/toolbarButton-openFile-dark.svg new file mode 100644 index 00000000..0bd612f0 --- /dev/null +++ b/cps/static/css/libs/images/toolbarButton-openFile-dark.svg @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/cps/static/css/libs/images/toolbarButton-openFile.png b/cps/static/css/libs/images/toolbarButton-openFile.png deleted file mode 100644 index b5cf1bd0..00000000 Binary files a/cps/static/css/libs/images/toolbarButton-openFile.png and /dev/null differ diff --git a/cps/static/css/libs/images/toolbarButton-openFile.svg b/cps/static/css/libs/images/toolbarButton-openFile.svg new file mode 100644 index 00000000..cb35980f --- /dev/null +++ b/cps/static/css/libs/images/toolbarButton-openFile.svg @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/cps/static/css/libs/images/toolbarButton-openFile@2x.png b/cps/static/css/libs/images/toolbarButton-openFile@2x.png deleted file mode 100644 index 91ab7659..00000000 Binary files a/cps/static/css/libs/images/toolbarButton-openFile@2x.png and /dev/null differ diff --git a/cps/static/css/libs/images/toolbarButton-pageDown-dark.svg b/cps/static/css/libs/images/toolbarButton-pageDown-dark.svg new file mode 100644 index 00000000..c2ca60c8 --- /dev/null +++ b/cps/static/css/libs/images/toolbarButton-pageDown-dark.svg @@ -0,0 +1,8 @@ + + \ No newline at end of file diff --git a/cps/static/css/libs/images/toolbarButton-pageDown-rtl.png b/cps/static/css/libs/images/toolbarButton-pageDown-rtl.png deleted file mode 100644 index 1957f79a..00000000 Binary files a/cps/static/css/libs/images/toolbarButton-pageDown-rtl.png and /dev/null differ diff --git a/cps/static/css/libs/images/toolbarButton-pageDown-rtl@2x.png b/cps/static/css/libs/images/toolbarButton-pageDown-rtl@2x.png deleted file mode 100644 index 16ebcb8e..00000000 Binary files a/cps/static/css/libs/images/toolbarButton-pageDown-rtl@2x.png and /dev/null differ diff --git a/cps/static/css/libs/images/toolbarButton-pageDown.png b/cps/static/css/libs/images/toolbarButton-pageDown.png deleted file mode 100644 index 8219ecf8..00000000 Binary files a/cps/static/css/libs/images/toolbarButton-pageDown.png and /dev/null differ diff --git a/cps/static/css/libs/images/toolbarButton-pageDown.svg b/cps/static/css/libs/images/toolbarButton-pageDown.svg new file mode 100644 index 00000000..c5d8b0f3 --- /dev/null +++ b/cps/static/css/libs/images/toolbarButton-pageDown.svg @@ -0,0 +1,7 @@ + + \ No newline at end of file diff --git a/cps/static/css/libs/images/toolbarButton-pageDown@2x.png b/cps/static/css/libs/images/toolbarButton-pageDown@2x.png deleted file mode 100644 index 758c01d8..00000000 Binary files a/cps/static/css/libs/images/toolbarButton-pageDown@2x.png and /dev/null differ diff --git a/cps/static/css/libs/images/toolbarButton-pageUp-dark.svg b/cps/static/css/libs/images/toolbarButton-pageUp-dark.svg new file mode 100644 index 00000000..dddc4ab2 --- /dev/null +++ b/cps/static/css/libs/images/toolbarButton-pageUp-dark.svg @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/cps/static/css/libs/images/toolbarButton-pageUp-rtl.png b/cps/static/css/libs/images/toolbarButton-pageUp-rtl.png deleted file mode 100644 index 98e7ce48..00000000 Binary files a/cps/static/css/libs/images/toolbarButton-pageUp-rtl.png and /dev/null differ diff --git a/cps/static/css/libs/images/toolbarButton-pageUp-rtl@2x.png b/cps/static/css/libs/images/toolbarButton-pageUp-rtl@2x.png deleted file mode 100644 index a01b0238..00000000 Binary files a/cps/static/css/libs/images/toolbarButton-pageUp-rtl@2x.png and /dev/null differ diff --git a/cps/static/css/libs/images/toolbarButton-pageUp.png b/cps/static/css/libs/images/toolbarButton-pageUp.png deleted file mode 100644 index fb9daa33..00000000 Binary files a/cps/static/css/libs/images/toolbarButton-pageUp.png and /dev/null differ diff --git a/cps/static/css/libs/images/toolbarButton-pageUp.svg b/cps/static/css/libs/images/toolbarButton-pageUp.svg new file mode 100644 index 00000000..aa0160ab --- /dev/null +++ b/cps/static/css/libs/images/toolbarButton-pageUp.svg @@ -0,0 +1,12 @@ + + + + + \ No newline at end of file diff --git a/cps/static/css/libs/images/toolbarButton-pageUp@2x.png b/cps/static/css/libs/images/toolbarButton-pageUp@2x.png deleted file mode 100644 index a5cfd755..00000000 Binary files a/cps/static/css/libs/images/toolbarButton-pageUp@2x.png and /dev/null differ diff --git a/cps/static/css/libs/images/toolbarButton-presentationMode-dark.svg b/cps/static/css/libs/images/toolbarButton-presentationMode-dark.svg new file mode 100644 index 00000000..13fa9dc7 --- /dev/null +++ b/cps/static/css/libs/images/toolbarButton-presentationMode-dark.svg @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/cps/static/css/libs/images/toolbarButton-presentationMode.png b/cps/static/css/libs/images/toolbarButton-presentationMode.png deleted file mode 100644 index 3ac21244..00000000 Binary files a/cps/static/css/libs/images/toolbarButton-presentationMode.png and /dev/null differ diff --git a/cps/static/css/libs/images/toolbarButton-presentationMode.svg b/cps/static/css/libs/images/toolbarButton-presentationMode.svg new file mode 100644 index 00000000..3f1f832e --- /dev/null +++ b/cps/static/css/libs/images/toolbarButton-presentationMode.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/cps/static/css/libs/images/toolbarButton-presentationMode@2x.png b/cps/static/css/libs/images/toolbarButton-presentationMode@2x.png deleted file mode 100644 index cada9e79..00000000 Binary files a/cps/static/css/libs/images/toolbarButton-presentationMode@2x.png and /dev/null differ diff --git a/cps/static/css/libs/images/toolbarButton-print-dark.svg b/cps/static/css/libs/images/toolbarButton-print-dark.svg new file mode 100644 index 00000000..ad37022f --- /dev/null +++ b/cps/static/css/libs/images/toolbarButton-print-dark.svg @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/cps/static/css/libs/images/toolbarButton-print.png b/cps/static/css/libs/images/toolbarButton-print.png deleted file mode 100644 index 51275e54..00000000 Binary files a/cps/static/css/libs/images/toolbarButton-print.png and /dev/null differ diff --git a/cps/static/css/libs/images/toolbarButton-print.svg b/cps/static/css/libs/images/toolbarButton-print.svg new file mode 100644 index 00000000..d521c9ad --- /dev/null +++ b/cps/static/css/libs/images/toolbarButton-print.svg @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/cps/static/css/libs/images/toolbarButton-print@2x.png b/cps/static/css/libs/images/toolbarButton-print@2x.png deleted file mode 100644 index 53d18daf..00000000 Binary files a/cps/static/css/libs/images/toolbarButton-print@2x.png and /dev/null differ diff --git a/cps/static/css/libs/images/toolbarButton-search-dark.svg b/cps/static/css/libs/images/toolbarButton-search-dark.svg new file mode 100644 index 00000000..cec8a420 --- /dev/null +++ b/cps/static/css/libs/images/toolbarButton-search-dark.svg @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/cps/static/css/libs/images/toolbarButton-search.png b/cps/static/css/libs/images/toolbarButton-search.png deleted file mode 100644 index f9b75579..00000000 Binary files a/cps/static/css/libs/images/toolbarButton-search.png and /dev/null differ diff --git a/cps/static/css/libs/images/toolbarButton-search.svg b/cps/static/css/libs/images/toolbarButton-search.svg new file mode 100644 index 00000000..28b7774e --- /dev/null +++ b/cps/static/css/libs/images/toolbarButton-search.svg @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/cps/static/css/libs/images/toolbarButton-search@2x.png b/cps/static/css/libs/images/toolbarButton-search@2x.png deleted file mode 100644 index 456b1332..00000000 Binary files a/cps/static/css/libs/images/toolbarButton-search@2x.png and /dev/null differ diff --git a/cps/static/css/libs/images/toolbarButton-secondaryToolbarToggle-dark.svg b/cps/static/css/libs/images/toolbarButton-secondaryToolbarToggle-dark.svg new file mode 100644 index 00000000..0160c07c --- /dev/null +++ b/cps/static/css/libs/images/toolbarButton-secondaryToolbarToggle-dark.svg @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/cps/static/css/libs/images/toolbarButton-secondaryToolbarToggle-rtl.png b/cps/static/css/libs/images/toolbarButton-secondaryToolbarToggle-rtl.png deleted file mode 100644 index 84370952..00000000 Binary files a/cps/static/css/libs/images/toolbarButton-secondaryToolbarToggle-rtl.png and /dev/null differ diff --git a/cps/static/css/libs/images/toolbarButton-secondaryToolbarToggle-rtl@2x.png b/cps/static/css/libs/images/toolbarButton-secondaryToolbarToggle-rtl@2x.png deleted file mode 100644 index 9d9bfa4f..00000000 Binary files a/cps/static/css/libs/images/toolbarButton-secondaryToolbarToggle-rtl@2x.png and /dev/null differ diff --git a/cps/static/css/libs/images/toolbarButton-secondaryToolbarToggle.png b/cps/static/css/libs/images/toolbarButton-secondaryToolbarToggle.png deleted file mode 100644 index 1f90f83d..00000000 Binary files a/cps/static/css/libs/images/toolbarButton-secondaryToolbarToggle.png and /dev/null differ diff --git a/cps/static/css/libs/images/toolbarButton-secondaryToolbarToggle.svg b/cps/static/css/libs/images/toolbarButton-secondaryToolbarToggle.svg new file mode 100644 index 00000000..dbef2380 --- /dev/null +++ b/cps/static/css/libs/images/toolbarButton-secondaryToolbarToggle.svg @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/cps/static/css/libs/images/toolbarButton-secondaryToolbarToggle@2x.png b/cps/static/css/libs/images/toolbarButton-secondaryToolbarToggle@2x.png deleted file mode 100644 index b066fe5c..00000000 Binary files a/cps/static/css/libs/images/toolbarButton-secondaryToolbarToggle@2x.png and /dev/null differ diff --git a/cps/static/css/libs/images/toolbarButton-sidebarToggle-dark.svg b/cps/static/css/libs/images/toolbarButton-sidebarToggle-dark.svg new file mode 100644 index 00000000..0118e41a --- /dev/null +++ b/cps/static/css/libs/images/toolbarButton-sidebarToggle-dark.svg @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/cps/static/css/libs/images/toolbarButton-sidebarToggle-rtl.png b/cps/static/css/libs/images/toolbarButton-sidebarToggle-rtl.png deleted file mode 100644 index 6f85ec06..00000000 Binary files a/cps/static/css/libs/images/toolbarButton-sidebarToggle-rtl.png and /dev/null differ diff --git a/cps/static/css/libs/images/toolbarButton-sidebarToggle-rtl@2x.png b/cps/static/css/libs/images/toolbarButton-sidebarToggle-rtl@2x.png deleted file mode 100644 index 291e0067..00000000 Binary files a/cps/static/css/libs/images/toolbarButton-sidebarToggle-rtl@2x.png and /dev/null differ diff --git a/cps/static/css/libs/images/toolbarButton-sidebarToggle.png b/cps/static/css/libs/images/toolbarButton-sidebarToggle.png deleted file mode 100644 index 025dc904..00000000 Binary files a/cps/static/css/libs/images/toolbarButton-sidebarToggle.png and /dev/null differ diff --git a/cps/static/css/libs/images/toolbarButton-sidebarToggle.svg b/cps/static/css/libs/images/toolbarButton-sidebarToggle.svg new file mode 100644 index 00000000..691c41cb --- /dev/null +++ b/cps/static/css/libs/images/toolbarButton-sidebarToggle.svg @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/cps/static/css/libs/images/toolbarButton-sidebarToggle@2x.png b/cps/static/css/libs/images/toolbarButton-sidebarToggle@2x.png deleted file mode 100644 index 7f834df9..00000000 Binary files a/cps/static/css/libs/images/toolbarButton-sidebarToggle@2x.png and /dev/null differ diff --git a/cps/static/css/libs/images/toolbarButton-viewAttachments-dark.svg b/cps/static/css/libs/images/toolbarButton-viewAttachments-dark.svg new file mode 100644 index 00000000..c9714fde --- /dev/null +++ b/cps/static/css/libs/images/toolbarButton-viewAttachments-dark.svg @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/cps/static/css/libs/images/toolbarButton-viewAttachments.png b/cps/static/css/libs/images/toolbarButton-viewAttachments.png deleted file mode 100644 index fcd0b268..00000000 Binary files a/cps/static/css/libs/images/toolbarButton-viewAttachments.png and /dev/null differ diff --git a/cps/static/css/libs/images/toolbarButton-viewAttachments.svg b/cps/static/css/libs/images/toolbarButton-viewAttachments.svg new file mode 100644 index 00000000..e914ec08 --- /dev/null +++ b/cps/static/css/libs/images/toolbarButton-viewAttachments.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/cps/static/css/libs/images/toolbarButton-viewAttachments@2x.png b/cps/static/css/libs/images/toolbarButton-viewAttachments@2x.png deleted file mode 100644 index 4a5e2b8a..00000000 Binary files a/cps/static/css/libs/images/toolbarButton-viewAttachments@2x.png and /dev/null differ diff --git a/cps/static/css/libs/images/toolbarButton-viewLayers-dark.svg b/cps/static/css/libs/images/toolbarButton-viewLayers-dark.svg new file mode 100644 index 00000000..76b042a9 --- /dev/null +++ b/cps/static/css/libs/images/toolbarButton-viewLayers-dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/cps/static/css/libs/images/toolbarButton-viewLayers.svg b/cps/static/css/libs/images/toolbarButton-viewLayers.svg new file mode 100644 index 00000000..e8687b77 --- /dev/null +++ b/cps/static/css/libs/images/toolbarButton-viewLayers.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/cps/static/css/libs/images/toolbarButton-viewOutline-dark.svg b/cps/static/css/libs/images/toolbarButton-viewOutline-dark.svg new file mode 100644 index 00000000..1704d961 --- /dev/null +++ b/cps/static/css/libs/images/toolbarButton-viewOutline-dark.svg @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/cps/static/css/libs/images/toolbarButton-viewOutline-rtl.png b/cps/static/css/libs/images/toolbarButton-viewOutline-rtl.png deleted file mode 100644 index aaa94302..00000000 Binary files a/cps/static/css/libs/images/toolbarButton-viewOutline-rtl.png and /dev/null differ diff --git a/cps/static/css/libs/images/toolbarButton-viewOutline-rtl@2x.png b/cps/static/css/libs/images/toolbarButton-viewOutline-rtl@2x.png deleted file mode 100644 index 3410f70d..00000000 Binary files a/cps/static/css/libs/images/toolbarButton-viewOutline-rtl@2x.png and /dev/null differ diff --git a/cps/static/css/libs/images/toolbarButton-viewOutline.png b/cps/static/css/libs/images/toolbarButton-viewOutline.png deleted file mode 100644 index 976365a5..00000000 Binary files a/cps/static/css/libs/images/toolbarButton-viewOutline.png and /dev/null differ diff --git a/cps/static/css/libs/images/toolbarButton-viewOutline.svg b/cps/static/css/libs/images/toolbarButton-viewOutline.svg new file mode 100644 index 00000000..030c28df --- /dev/null +++ b/cps/static/css/libs/images/toolbarButton-viewOutline.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/cps/static/css/libs/images/toolbarButton-viewOutline@2x.png b/cps/static/css/libs/images/toolbarButton-viewOutline@2x.png deleted file mode 100644 index b6a197fd..00000000 Binary files a/cps/static/css/libs/images/toolbarButton-viewOutline@2x.png and /dev/null differ diff --git a/cps/static/css/libs/images/toolbarButton-viewThumbnail-dark.svg b/cps/static/css/libs/images/toolbarButton-viewThumbnail-dark.svg new file mode 100644 index 00000000..17c55f7b --- /dev/null +++ b/cps/static/css/libs/images/toolbarButton-viewThumbnail-dark.svg @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/cps/static/css/libs/images/toolbarButton-viewThumbnail.png b/cps/static/css/libs/images/toolbarButton-viewThumbnail.png deleted file mode 100644 index 584ba558..00000000 Binary files a/cps/static/css/libs/images/toolbarButton-viewThumbnail.png and /dev/null differ diff --git a/cps/static/css/libs/images/toolbarButton-viewThumbnail.svg b/cps/static/css/libs/images/toolbarButton-viewThumbnail.svg new file mode 100644 index 00000000..b997ec49 --- /dev/null +++ b/cps/static/css/libs/images/toolbarButton-viewThumbnail.svg @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/cps/static/css/libs/images/toolbarButton-viewThumbnail@2x.png b/cps/static/css/libs/images/toolbarButton-viewThumbnail@2x.png deleted file mode 100644 index a0208b41..00000000 Binary files a/cps/static/css/libs/images/toolbarButton-viewThumbnail@2x.png and /dev/null differ diff --git a/cps/static/css/libs/images/toolbarButton-zoomIn-dark.svg b/cps/static/css/libs/images/toolbarButton-zoomIn-dark.svg new file mode 100644 index 00000000..9b615541 --- /dev/null +++ b/cps/static/css/libs/images/toolbarButton-zoomIn-dark.svg @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/cps/static/css/libs/images/toolbarButton-zoomIn.png b/cps/static/css/libs/images/toolbarButton-zoomIn.png deleted file mode 100644 index 513d081b..00000000 Binary files a/cps/static/css/libs/images/toolbarButton-zoomIn.png and /dev/null differ diff --git a/cps/static/css/libs/images/toolbarButton-zoomIn.svg b/cps/static/css/libs/images/toolbarButton-zoomIn.svg new file mode 100644 index 00000000..480d2cef --- /dev/null +++ b/cps/static/css/libs/images/toolbarButton-zoomIn.svg @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/cps/static/css/libs/images/toolbarButton-zoomIn@2x.png b/cps/static/css/libs/images/toolbarButton-zoomIn@2x.png deleted file mode 100644 index d5d49d5f..00000000 Binary files a/cps/static/css/libs/images/toolbarButton-zoomIn@2x.png and /dev/null differ diff --git a/cps/static/css/libs/images/toolbarButton-zoomOut-dark.svg b/cps/static/css/libs/images/toolbarButton-zoomOut-dark.svg new file mode 100644 index 00000000..0fb3594d --- /dev/null +++ b/cps/static/css/libs/images/toolbarButton-zoomOut-dark.svg @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/cps/static/css/libs/images/toolbarButton-zoomOut.png b/cps/static/css/libs/images/toolbarButton-zoomOut.png deleted file mode 100644 index 156c26b9..00000000 Binary files a/cps/static/css/libs/images/toolbarButton-zoomOut.png and /dev/null differ diff --git a/cps/static/css/libs/images/toolbarButton-zoomOut.svg b/cps/static/css/libs/images/toolbarButton-zoomOut.svg new file mode 100644 index 00000000..527f5210 --- /dev/null +++ b/cps/static/css/libs/images/toolbarButton-zoomOut.svg @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/cps/static/css/libs/images/toolbarButton-zoomOut@2x.png b/cps/static/css/libs/images/toolbarButton-zoomOut@2x.png deleted file mode 100644 index 959e1919..00000000 Binary files a/cps/static/css/libs/images/toolbarButton-zoomOut@2x.png and /dev/null differ diff --git a/cps/static/css/libs/images/treeitem-collapsed-dark.svg b/cps/static/css/libs/images/treeitem-collapsed-dark.svg new file mode 100644 index 00000000..1fb65516 --- /dev/null +++ b/cps/static/css/libs/images/treeitem-collapsed-dark.svg @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/cps/static/css/libs/images/treeitem-collapsed-rtl.png b/cps/static/css/libs/images/treeitem-collapsed-rtl.png deleted file mode 100644 index 0496b357..00000000 Binary files a/cps/static/css/libs/images/treeitem-collapsed-rtl.png and /dev/null differ diff --git a/cps/static/css/libs/images/treeitem-collapsed-rtl@2x.png b/cps/static/css/libs/images/treeitem-collapsed-rtl@2x.png deleted file mode 100644 index 6ad9ebcd..00000000 Binary files a/cps/static/css/libs/images/treeitem-collapsed-rtl@2x.png and /dev/null differ diff --git a/cps/static/css/libs/images/treeitem-collapsed.png b/cps/static/css/libs/images/treeitem-collapsed.png deleted file mode 100644 index 06d4d376..00000000 Binary files a/cps/static/css/libs/images/treeitem-collapsed.png and /dev/null differ diff --git a/cps/static/css/libs/images/treeitem-collapsed.svg b/cps/static/css/libs/images/treeitem-collapsed.svg new file mode 100644 index 00000000..831cddfc --- /dev/null +++ b/cps/static/css/libs/images/treeitem-collapsed.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/cps/static/css/libs/images/treeitem-collapsed@2x.png b/cps/static/css/libs/images/treeitem-collapsed@2x.png deleted file mode 100644 index eec1e58c..00000000 Binary files a/cps/static/css/libs/images/treeitem-collapsed@2x.png and /dev/null differ diff --git a/cps/static/css/libs/images/treeitem-expanded-dark.svg b/cps/static/css/libs/images/treeitem-expanded-dark.svg new file mode 100644 index 00000000..695b0aa6 --- /dev/null +++ b/cps/static/css/libs/images/treeitem-expanded-dark.svg @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/cps/static/css/libs/images/treeitem-expanded.png b/cps/static/css/libs/images/treeitem-expanded.png deleted file mode 100644 index c8d55735..00000000 Binary files a/cps/static/css/libs/images/treeitem-expanded.png and /dev/null differ diff --git a/cps/static/css/libs/images/treeitem-expanded.svg b/cps/static/css/libs/images/treeitem-expanded.svg new file mode 100644 index 00000000..2d45f0c8 --- /dev/null +++ b/cps/static/css/libs/images/treeitem-expanded.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/cps/static/css/libs/images/treeitem-expanded@2x.png b/cps/static/css/libs/images/treeitem-expanded@2x.png deleted file mode 100644 index 3b3b6103..00000000 Binary files a/cps/static/css/libs/images/treeitem-expanded@2x.png and /dev/null differ diff --git a/cps/static/css/libs/viewer.css b/cps/static/css/libs/viewer.css index 5835b309..cd4781bf 100644 --- a/cps/static/css/libs/viewer.css +++ b/cps/static/css/libs/viewer.css @@ -21,7 +21,7 @@ bottom: 0; overflow: hidden; opacity: 0.2; - line-height: 1.0; + line-height: 1; } .textLayer > span { @@ -29,15 +29,13 @@ position: absolute; white-space: pre; cursor: text; - -webkit-transform-origin: 0% 0%; - transform-origin: 0% 0%; + transform-origin: 0% 0%; } .textLayer .highlight { margin: -1px; padding: 1px; - - background-color: rgb(180, 0, 170); + background-color: rgba(180, 0, 170, 1); border-radius: 4px; } @@ -54,12 +52,16 @@ } .textLayer .highlight.selected { - background-color: rgb(0, 100, 0); + background-color: rgba(0, 100, 0, 1); } -.textLayer ::-moz-selection { background: rgb(0,0,255); } +.textLayer ::-moz-selection { + background: rgba(0, 0, 255, 1); +} -.textLayer ::selection { background: rgb(0,0,255); } +.textLayer ::selection { + background: rgba(0, 0, 255, 1); +} .textLayer .endOfContent { display: block; @@ -98,8 +100,8 @@ .annotationLayer .linkAnnotation > a:hover, .annotationLayer .buttonWidgetAnnotation.pushButton > a:hover { opacity: 0.2; - background: #ff0; - box-shadow: 0px 2px 10px #ff0; + background: rgba(255, 255, 0, 1); + box-shadow: 0px 2px 10px rgba(255, 255, 0, 1); } .annotationLayer .textAnnotation img { @@ -152,7 +154,7 @@ .annotationLayer .choiceWidgetAnnotation select:hover, .annotationLayer .buttonWidgetAnnotation.checkBox input:hover, .annotationLayer .buttonWidgetAnnotation.radioButton input:hover { - border: 1px solid #000; + border: 1px solid rgba(0, 0, 0, 1); } .annotationLayer .textWidgetAnnotation input:focus, @@ -165,8 +167,8 @@ .annotationLayer .buttonWidgetAnnotation.checkBox input:checked:before, .annotationLayer .buttonWidgetAnnotation.checkBox input:checked:after, .annotationLayer .buttonWidgetAnnotation.radioButton input:checked:before { - background-color: #000; - content: ''; + background-color: rgba(0, 0, 0, 1); + content: ""; display: block; position: absolute; } @@ -179,13 +181,11 @@ } .annotationLayer .buttonWidgetAnnotation.checkBox input:checked:before { - -webkit-transform: rotate(45deg); - transform: rotate(45deg); + transform: rotate(45deg); } .annotationLayer .buttonWidgetAnnotation.checkBox input:checked:after { - -webkit-transform: rotate(-45deg); - transform: rotate(-45deg); + transform: rotate(-45deg); } .annotationLayer .buttonWidgetAnnotation.radioButton input:checked:before { @@ -229,8 +229,8 @@ position: absolute; z-index: 200; max-width: 20em; - background-color: #FFFF99; - box-shadow: 0px 2px 5px #888; + background-color: rgba(255, 255, 153, 1); + box-shadow: 0px 2px 5px rgba(136, 136, 136, 1); border-radius: 2px; padding: 6px; margin-left: 5px; @@ -254,7 +254,7 @@ } .annotationLayer .popup p { - border-top: 1px solid #333; + border-top: 1px solid rgba(51, 51, 51, 1); margin-top: 2px; padding-top: 2px; } @@ -289,10 +289,9 @@ overflow: visible; border: 9px solid transparent; background-clip: content-box; - -webkit-border-image: url(images/shadow.png) 9 9 repeat; - -o-border-image: url(images/shadow.png) 9 9 repeat; - border-image: url(images/shadow.png) 9 9 repeat; - background-color: white; + -o-border-image: url(images/shadow.png) 9 9 repeat; + border-image: url(images/shadow.png) 9 9 repeat; + background-color: rgba(255, 255, 255, 1); } .pdfViewer.removePageBorders .page { @@ -309,13 +308,16 @@ border: none; } -.pdfViewer.scrollHorizontal, .pdfViewer.scrollWrapped, .spread { +.pdfViewer.scrollHorizontal, +.pdfViewer.scrollWrapped, +.spread { margin-left: 3.5px; margin-right: 3.5px; text-align: center; } -.pdfViewer.scrollHorizontal, .spread { +.pdfViewer.scrollHorizontal, +.spread { white-space: nowrap; } @@ -365,7 +367,7 @@ top: 0; right: 0; bottom: 0; - background: url('images/loading-icon.gif') center no-repeat; + background: url("images/loading-icon.gif") center no-repeat; } .pdfPresentationMode .pdfViewer { @@ -405,6 +407,115 @@ :root { --sidebar-width: 200px; + --sidebar-transition-duration: 200ms; + --sidebar-transition-timing-function: ease; + + --toolbar-icon-opacity: 0.7; + --doorhanger-icon-opacity: 0.9; + + --main-color: rgba(12, 12, 13, 1); + --body-bg-color: rgba(237, 237, 240, 1); + --errorWrapper-bg-color: rgba(255, 74, 74, 1); + --progressBar-color: rgba(10, 132, 255, 1); + --progressBar-indeterminate-bg-color: rgba(221, 221, 222, 1); + --progressBar-indeterminate-blend-color: rgba(116, 177, 239, 1); + --scrollbar-color: auto; + --scrollbar-bg-color: auto; + + --sidebar-bg-color: rgba(245, 246, 247, 1); + --toolbar-bg-color: rgba(249, 249, 250, 1); + --toolbar-border-color: rgba(204, 204, 204, 1); + --button-hover-color: rgba(221, 222, 223, 1); + --toggled-btn-bg-color: rgba(0, 0, 0, 0.3); + --dropdown-btn-bg-color: rgba(215, 215, 219, 1); + --separator-color: rgba(0, 0, 0, 0.3); + --field-color: rgba(6, 6, 6, 1); + --field-bg-color: rgba(255, 255, 255, 1); + --field-border-color: rgba(187, 187, 188, 1); + --findbar-nextprevious-btn-bg-color: rgba(227, 228, 230, 1); + --outline-color: rgba(0, 0, 0, 0.8); + --outline-hover-color: rgba(0, 0, 0, 0.9); + --outline-active-color: rgba(0, 0, 0, 0.08); + --outline-active-bg-color: rgba(0, 0, 0, 1); + --sidebaritem-bg-color: rgba(0, 0, 0, 0.15); + --doorhanger-bg-color: rgba(255, 255, 255, 1); + --doorhanger-border-color: rgba(12, 12, 13, 0.2); + --doorhanger-hover-color: rgba(237, 237, 237, 1); + --doorhanger-separator-color: rgba(222, 222, 222, 1); + --overlay-button-bg-color: rgba(12, 12, 13, 0.1); + --overlay-button-hover-color: rgba(12, 12, 13, 0.3); + + /*--loading-icon: url(images/loading.svg); + --treeitem-expanded-icon: url(images/treeitem-expanded.svg); + --treeitem-collapsed-icon: url(images/treeitem-collapsed.svg); + --toolbarButton-menuArrow-icon: url(images/toolbarButton-menuArrow.svg); + --toolbarButton-sidebarToggle-icon: url(images/toolbarButton-sidebarToggle.svg); + --toolbarButton-secondaryToolbarToggle-icon: url(images/toolbarButton-secondaryToolbarToggle.svg); + --toolbarButton-pageUp-icon: url(images/toolbarButton-pageUp.svg); + --toolbarButton-pageDown-icon: url(images/toolbarButton-pageDown.svg); + --toolbarButton-zoomOut-icon: url(images/toolbarButton-zoomOut.svg); + --toolbarButton-zoomIn-icon: url(images/toolbarButton-zoomIn.svg); + --toolbarButton-presentationMode-icon: url(images/toolbarButton-presentationMode.svg); + --toolbarButton-print-icon: url(images/toolbarButton-print.svg); + --toolbarButton-openFile-icon: url(images/toolbarButton-openFile.svg); + --toolbarButton-download-icon: url(images/toolbarButton-download.svg); + --toolbarButton-bookmark-icon: url(images/toolbarButton-bookmark.svg); + --toolbarButton-viewThumbnail-icon: url(images/toolbarButton-viewThumbnail.svg); + --toolbarButton-viewOutline-icon: url(images/toolbarButton-viewOutline.svg); + --toolbarButton-viewAttachments-icon: url(images/toolbarButton-viewAttachments.svg); + --toolbarButton-viewLayers-icon: url(images/toolbarButton-viewLayers.svg); + --toolbarButton-search-icon: url(images/toolbarButton-search.svg); + --findbarButton-previous-icon: url(images/findbarButton-previous.svg); + --findbarButton-next-icon: url(images/findbarButton-next.svg); + --secondaryToolbarButton-firstPage-icon: url(images/secondaryToolbarButton-firstPage.svg); + --secondaryToolbarButton-lastPage-icon: url(images/secondaryToolbarButton-lastPage.svg); + --secondaryToolbarButton-rotateCcw-icon: url(images/secondaryToolbarButton-rotateCcw.svg); + --secondaryToolbarButton-rotateCw-icon: url(images/secondaryToolbarButton-rotateCw.svg); + --secondaryToolbarButton-selectTool-icon: url(images/secondaryToolbarButton-selectTool.svg); + --secondaryToolbarButton-handTool-icon: url(images/secondaryToolbarButton-handTool.svg); + --secondaryToolbarButton-scrollVertical-icon: url(images/secondaryToolbarButton-scrollVertical.svg); + --secondaryToolbarButton-scrollHorizontal-icon: url(images/secondaryToolbarButton-scrollHorizontal.svg); + --secondaryToolbarButton-scrollWrapped-icon: url(images/secondaryToolbarButton-scrollWrapped.svg); + --secondaryToolbarButton-spreadNone-icon: url(images/secondaryToolbarButton-spreadNone.svg); + --secondaryToolbarButton-spreadOdd-icon: url(images/secondaryToolbarButton-spreadOdd.svg); + --secondaryToolbarButton-spreadEven-icon: url(images/secondaryToolbarButton-spreadEven.svg); + --secondaryToolbarButton-documentProperties-icon: url(images/secondaryToolbarButton-documentProperties.svg);*/ +} + +@media (prefers-color-scheme: dark) { + :root { + --main-color: rgba(249, 249, 250, 1); + --body-bg-color: rgba(42, 42, 46, 1); + --errorWrapper-bg-color: rgba(199, 17, 17, 1); + --progressBar-color: rgba(0, 96, 223, 1); + --progressBar-indeterminate-bg-color: rgba(40, 40, 43, 1); + --progressBar-indeterminate-blend-color: rgba(20, 68, 133, 1); + --scrollbar-color: rgba(121, 121, 123, 1); + --scrollbar-bg-color: rgba(35, 35, 39, 1); + + --sidebar-bg-color: rgba(50, 50, 52, 1); + --toolbar-bg-color: rgba(56, 56, 61, 1); + --toolbar-border-color: rgba(12, 12, 13, 1); + --button-hover-color: rgba(102, 102, 103, 1); + --toggled-btn-bg-color: rgba(0, 0, 0, 0.3); + --dropdown-btn-bg-color: rgba(74, 74, 79, 1); + --separator-color: rgba(0, 0, 0, 0.3); + --field-color: rgba(250, 250, 250, 1); + --field-bg-color: rgba(64, 64, 68, 1); + --field-border-color: rgba(115, 115, 115, 1); + --findbar-nextprevious-btn-bg-color: rgba(89, 89, 89, 1); + --outline-color: rgba(255, 255, 255, 0.8); + --outline-hover-color: rgba(255, 255, 255, 0.9); + --outline-active-color: rgba(255, 255, 255, 0.08); + --outline-active-bg-color: rgba(255, 255, 255, 1); + --sidebaritem-bg-color: rgba(255, 255, 255, 0.15); + --doorhanger-bg-color: rgba(74, 74, 79, 1); + --doorhanger-border-color: rgba(39, 39, 43, 1); + --doorhanger-hover-color: rgba(93, 94, 98, 1); + --doorhanger-separator-color: rgba(92, 92, 97, 1); + --overlay-button-bg-color: rgba(92, 92, 97, 1); + --overlay-button-hover-color: rgba(115, 115, 115, 1); + } } * { @@ -422,16 +533,172 @@ html { body { height: 100%; width: 100%; - background-color: #404040; - background-image: url(images/texture.png); + background-color: rgba(237, 237, 240, 1); + background-color: var(--body-bg-color); +} + +@media (prefers-color-scheme: dark) { + + body { + background-color: rgba(42, 42, 46, 1); + background-color: var(--body-bg-color); + } +} + +body { + font: message-box; + outline: none; + scrollbar-color: auto auto; + scrollbar-color: var(--scrollbar-color) var(--scrollbar-bg-color); +} + +@media (prefers-color-scheme: dark) { + + body { + scrollbar-color: rgba(121, 121, 123, 1) rgba(35, 35, 39, 1); + scrollbar-color: var(--scrollbar-color) var(--scrollbar-bg-color); + } +} + +@media (prefers-color-scheme: dark) { + + body { + scrollbar-color: rgba(121, 121, 123, 1) rgba(35, 35, 39, 1); + scrollbar-color: var(--scrollbar-color) var(--scrollbar-bg-color); + } +} + +@media (prefers-color-scheme: dark) { + + body { + scrollbar-color: rgba(121, 121, 123, 1) rgba(35, 35, 39, 1); + scrollbar-color: var(--scrollbar-color) var(--scrollbar-bg-color); + } +} + +@media (prefers-color-scheme: dark) { + + body { + scrollbar-color: rgba(121, 121, 123, 1) rgba(35, 35, 39, 1); + scrollbar-color: var(--scrollbar-color) var(--scrollbar-bg-color); + } +} + +input { + font: message-box; + outline: none; + scrollbar-color: auto auto; + scrollbar-color: var(--scrollbar-color) var(--scrollbar-bg-color); +} + +@media (prefers-color-scheme: dark) { + + input { + scrollbar-color: rgba(121, 121, 123, 1) rgba(35, 35, 39, 1); + scrollbar-color: var(--scrollbar-color) var(--scrollbar-bg-color); + } +} + +@media (prefers-color-scheme: dark) { + + input { + scrollbar-color: rgba(121, 121, 123, 1) rgba(35, 35, 39, 1); + scrollbar-color: var(--scrollbar-color) var(--scrollbar-bg-color); + } +} + +@media (prefers-color-scheme: dark) { + + input { + scrollbar-color: rgba(121, 121, 123, 1) rgba(35, 35, 39, 1); + scrollbar-color: var(--scrollbar-color) var(--scrollbar-bg-color); + } +} + +@media (prefers-color-scheme: dark) { + + input { + scrollbar-color: rgba(121, 121, 123, 1) rgba(35, 35, 39, 1); + scrollbar-color: var(--scrollbar-color) var(--scrollbar-bg-color); + } +} + +button { + font: message-box; + outline: none; + scrollbar-color: auto auto; + scrollbar-color: var(--scrollbar-color) var(--scrollbar-bg-color); +} + +@media (prefers-color-scheme: dark) { + + button { + scrollbar-color: rgba(121, 121, 123, 1) rgba(35, 35, 39, 1); + scrollbar-color: var(--scrollbar-color) var(--scrollbar-bg-color); + } +} + +@media (prefers-color-scheme: dark) { + + button { + scrollbar-color: rgba(121, 121, 123, 1) rgba(35, 35, 39, 1); + scrollbar-color: var(--scrollbar-color) var(--scrollbar-bg-color); + } +} + +@media (prefers-color-scheme: dark) { + + button { + scrollbar-color: rgba(121, 121, 123, 1) rgba(35, 35, 39, 1); + scrollbar-color: var(--scrollbar-color) var(--scrollbar-bg-color); + } +} + +@media (prefers-color-scheme: dark) { + + button { + scrollbar-color: rgba(121, 121, 123, 1) rgba(35, 35, 39, 1); + scrollbar-color: var(--scrollbar-color) var(--scrollbar-bg-color); + } } -body, -input, -button, select { font: message-box; outline: none; + scrollbar-color: auto auto; + scrollbar-color: var(--scrollbar-color) var(--scrollbar-bg-color); +} + +@media (prefers-color-scheme: dark) { + + select { + scrollbar-color: rgba(121, 121, 123, 1) rgba(35, 35, 39, 1); + scrollbar-color: var(--scrollbar-color) var(--scrollbar-bg-color); + } +} + +@media (prefers-color-scheme: dark) { + + select { + scrollbar-color: rgba(121, 121, 123, 1) rgba(35, 35, 39, 1); + scrollbar-color: var(--scrollbar-color) var(--scrollbar-bg-color); + } +} + +@media (prefers-color-scheme: dark) { + + select { + scrollbar-color: rgba(121, 121, 123, 1) rgba(35, 35, 39, 1); + scrollbar-color: var(--scrollbar-color) var(--scrollbar-bg-color); + } +} + +@media (prefers-color-scheme: dark) { + + select { + scrollbar-color: rgba(121, 121, 123, 1) rgba(35, 35, 39, 1); + scrollbar-color: var(--scrollbar-color) var(--scrollbar-bg-color); + } } .hidden { @@ -441,19 +708,27 @@ select { display: none !important; } +.pdfViewer.enablePermissions .textLayer > span { + -webkit-user-select: none !important; + -moz-user-select: none !important; + -ms-user-select: none !important; + user-select: none !important; + cursor: not-allowed; +} + #viewerContainer.pdfPresentationMode:-ms-fullscreen { top: 0px !important; overflow: hidden !important; } #viewerContainer.pdfPresentationMode:-ms-fullscreen::-ms-backdrop { - background-color: #000; + background-color: rgba(0, 0, 0, 1); } #viewerContainer.pdfPresentationMode:-webkit-full-screen { top: 0px; - border-top: 2px solid transparent; - background-color: #000; + border-top: 2px solid rgba(0, 0, 0, 0); + background-color: rgba(0, 0, 0, 1); width: 100%; height: 100%; overflow: hidden; @@ -464,8 +739,8 @@ select { #viewerContainer.pdfPresentationMode:-moz-full-screen { top: 0px; - border-top: 2px solid transparent; - background-color: #000; + border-top: 2px solid rgba(0, 0, 0, 0); + background-color: rgba(0, 0, 0, 1); width: 100%; height: 100%; overflow: hidden; @@ -476,8 +751,8 @@ select { #viewerContainer.pdfPresentationMode:-ms-fullscreen { top: 0px; - border-top: 2px solid transparent; - background-color: #000; + border-top: 2px solid rgba(0, 0, 0, 0); + background-color: rgba(0, 0, 0, 1); width: 100%; height: 100%; overflow: hidden; @@ -488,8 +763,8 @@ select { #viewerContainer.pdfPresentationMode:fullscreen { top: 0px; - border-top: 2px solid transparent; - background-color: #000; + border-top: 2px solid rgba(0, 0, 0, 0); + background-color: rgba(0, 0, 0, 1); width: 100%; height: 100%; overflow: hidden; @@ -547,30 +822,25 @@ select { position: absolute; top: 32px; bottom: 0; - width: 200px; /* Here, and elsewhere below, keep the constant value for compatibility - with older browsers that lack support for CSS variables. */ + width: 200px; width: var(--sidebar-width); visibility: hidden; z-index: 100; - border-top: 1px solid #333; - - -webkit-transition-duration: 200ms; - - transition-duration: 200ms; - -webkit-transition-timing-function: ease; - transition-timing-function: ease; + border-top: 1px solid rgba(51, 51, 51, 1); + transition-duration: 200ms; + transition-duration: var(--sidebar-transition-duration); + transition-timing-function: ease; + transition-timing-function: var(--sidebar-transition-timing-function); } -html[dir='ltr'] #sidebarContainer { - -webkit-transition-property: left; +html[dir="ltr"] #sidebarContainer { transition-property: left; left: -200px; - left: calc(-1 * var(--sidebar-width)); + left: calc(0px - var(--sidebar-width)); } -html[dir='rtl'] #sidebarContainer { - -webkit-transition-property: right; +html[dir="rtl"] #sidebarContainer { transition-property: right; right: -200px; - right: calc(-1 * var(--sidebar-width)); + right: calc(0px - var(--sidebar-width)); } .loadingInProgress #sidebarContainer { @@ -579,8 +849,7 @@ html[dir='rtl'] #sidebarContainer { #outerContainer.sidebarResizing #sidebarContainer { /* Improve responsiveness and avoid visual glitches when the sidebar is resized. */ - -webkit-transition-duration: 0s; - transition-duration: 0s; + transition-duration: 0s; /* Prevent e.g. the thumbnails being selected when the sidebar is resized. */ -webkit-user-select: none; -moz-user-select: none; @@ -592,10 +861,10 @@ html[dir='rtl'] #sidebarContainer { #outerContainer.sidebarOpen #sidebarContainer { visibility: visible; } -html[dir='ltr'] #outerContainer.sidebarOpen #sidebarContainer { +html[dir="ltr"] #outerContainer.sidebarOpen #sidebarContainer { left: 0px; } -html[dir='rtl'] #outerContainer.sidebarOpen #sidebarContainer { +html[dir="rtl"] #outerContainer.sidebarOpen #sidebarContainer { right: 0px; } @@ -615,15 +884,15 @@ html[dir='rtl'] #outerContainer.sidebarOpen #sidebarContainer { -webkit-overflow-scrolling: touch; position: absolute; width: 100%; - background-color: hsla(0,0%,0%,.1); + background-color: rgba(0, 0, 0, 0.1); } -html[dir='ltr'] #sidebarContent { +html[dir="ltr"] #sidebarContent { left: 0; - box-shadow: inset -1px 0 0 hsla(0,0%,0%,.25); + box-shadow: inset -1px 0 0 rgba(0, 0, 0, 0.25); } -html[dir='rtl'] #sidebarContent { +html[dir="rtl"] #sidebarContent { right: 0; - box-shadow: inset 1px 0 0 hsla(0,0%,0%,.25); + box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.25); } #viewerContainer { @@ -637,32 +906,27 @@ html[dir='rtl'] #sidebarContent { outline: none; } #viewerContainer:not(.pdfPresentationMode) { - -webkit-transition-duration: 200ms; - transition-duration: 200ms; - -webkit-transition-timing-function: ease; - transition-timing-function: ease; -} -html[dir='ltr'] #viewerContainer { - box-shadow: inset 1px 0 0 hsla(0,0%,100%,.05); -} -html[dir='rtl'] #viewerContainer { - box-shadow: inset -1px 0 0 hsla(0,0%,100%,.05); + transition-duration: 200ms; + transition-duration: var(--sidebar-transition-duration); + transition-timing-function: ease; + transition-timing-function: var(--sidebar-transition-timing-function); } #outerContainer.sidebarResizing #viewerContainer { /* Improve responsiveness and avoid visual glitches when the sidebar is resized. */ - -webkit-transition-duration: 0s; - transition-duration: 0s; + transition-duration: 0s; } -html[dir='ltr'] #outerContainer.sidebarOpen #viewerContainer:not(.pdfPresentationMode) { - -webkit-transition-property: left; +html[dir="ltr"] + #outerContainer.sidebarOpen + #viewerContainer:not(.pdfPresentationMode) { transition-property: left; left: 200px; left: var(--sidebar-width); } -html[dir='rtl'] #outerContainer.sidebarOpen #viewerContainer:not(.pdfPresentationMode) { - -webkit-transition-property: right; +html[dir="rtl"] + #outerContainer.sidebarOpen + #viewerContainer:not(.pdfPresentationMode) { transition-property: right; right: 200px; right: var(--sidebar-width); @@ -683,23 +947,31 @@ html[dir='rtl'] #outerContainer.sidebarOpen #viewerContainer:not(.pdfPresentatio #toolbarSidebar { width: 100%; height: 32px; - background-color: #424242; /* fallback */ - background-image: url(images/texture.png), - -webkit-gradient(linear, left top, left bottom, from(hsla(0,0%,30%,.99)), to(hsla(0,0%,25%,.95))); - background-image: url(images/texture.png), - linear-gradient(hsla(0,0%,30%,.99), hsla(0,0%,25%,.95)); + background-color: rgba(245, 246, 247, 1); + background-color: var(--sidebar-bg-color); } -html[dir='ltr'] #toolbarSidebar { - box-shadow: inset -1px 0 0 rgba(0, 0, 0, 0.25), - inset 0 -1px 0 hsla(0,0%,100%,.05), - 0 1px 0 hsla(0,0%,0%,.15), - 0 0 1px hsla(0,0%,0%,.1); + +@media (prefers-color-scheme: dark) { + + #toolbarSidebar { + background-color: rgba(50, 50, 52, 1); + background-color: var(--sidebar-bg-color); + } } -html[dir='rtl'] #toolbarSidebar { - box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.25), - inset 0 1px 0 hsla(0,0%,100%,.05), - 0 1px 0 hsla(0,0%,0%,.15), - 0 0 1px hsla(0,0%,0%,.1); +html[dir="ltr"] #toolbarSidebar { + box-shadow: inset -1px 0 0 rgba(0, 0, 0, 0.25), 0 1px 0 rgba(0, 0, 0, 0.15), + 0 0 1px rgba(0, 0, 0, 0.1); +} +html[dir="rtl"] #toolbarSidebar { + box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.25), 0 1px 0 rgba(0, 0, 0, 0.15), + 0 0 1px rgba(0, 0, 0, 0.1); +} + +html[dir="ltr"] #toolbarSidebar .toolbarButton { + margin-right: 2px !important; +} +html[dir="rtl"] #toolbarSidebar .toolbarButton { + margin-left: 2px !important; } #sidebarResizer { @@ -710,33 +982,122 @@ html[dir='rtl'] #toolbarSidebar { z-index: 200; cursor: ew-resize; } -html[dir='ltr'] #sidebarResizer { +html[dir="ltr"] #sidebarResizer { right: -6px; } -html[dir='rtl'] #sidebarResizer { +html[dir="rtl"] #sidebarResizer { left: -6px; } -#toolbarContainer, .findbar, .secondaryToolbar { +#toolbarContainer { position: relative; height: 32px; - background-color: #474747; /* fallback */ - background-image: url(images/texture.png), - -webkit-gradient(linear, left top, left bottom, from(hsla(0,0%,32%,.99)), to(hsla(0,0%,27%,.95))); - background-image: url(images/texture.png), - linear-gradient(hsla(0,0%,32%,.99), hsla(0,0%,27%,.95)); + background-color: rgba(249, 249, 250, 1); + background-color: var(--toolbar-bg-color); } -html[dir='ltr'] #toolbarContainer, .findbar, .secondaryToolbar { - box-shadow: inset 0 1px 1px hsla(0,0%,0%,.15), - inset 0 -1px 0 hsla(0,0%,100%,.05), - 0 1px 0 hsla(0,0%,0%,.15), - 0 1px 1px hsla(0,0%,0%,.1); + +@media (prefers-color-scheme: dark) { + + #toolbarContainer { + background-color: rgba(56, 56, 61, 1); + background-color: var(--toolbar-bg-color); + } } -html[dir='rtl'] #toolbarContainer, .findbar, .secondaryToolbar { - box-shadow: inset 0 1px 1px hsla(0,0%,0%,.15), - inset 0 -1px 0 hsla(0,0%,100%,.05), - 0 1px 0 hsla(0,0%,0%,.15), - 0 1px 1px hsla(0,0%,0%,.1); + +.findbar { + position: relative; + height: 32px; + background-color: rgba(249, 249, 250, 1); + background-color: var(--toolbar-bg-color); +} + +@media (prefers-color-scheme: dark) { + + .findbar { + background-color: rgba(56, 56, 61, 1); + background-color: var(--toolbar-bg-color); + } +} + +.secondaryToolbar { + position: relative; + height: 32px; + background-color: rgba(249, 249, 250, 1); + background-color: var(--toolbar-bg-color); +} + +@media (prefers-color-scheme: dark) { + + .secondaryToolbar { + background-color: rgba(56, 56, 61, 1); + background-color: var(--toolbar-bg-color); + } +} +html[dir="ltr"] #toolbarContainer { + box-shadow: 0 1px 0 rgba(204, 204, 204, 1); + box-shadow: 0 1px 0 var(--toolbar-border-color); +} +@media (prefers-color-scheme: dark) { + + html[dir="ltr"] #toolbarContainer { + box-shadow: 0 1px 0 rgba(12, 12, 13, 1); + box-shadow: 0 1px 0 var(--toolbar-border-color); + } +} +.findbar { + box-shadow: 0 1px 0 rgba(204, 204, 204, 1); + box-shadow: 0 1px 0 var(--toolbar-border-color); +} +@media (prefers-color-scheme: dark) { + + .findbar { + box-shadow: 0 1px 0 rgba(12, 12, 13, 1); + box-shadow: 0 1px 0 var(--toolbar-border-color); + } +} +.secondaryToolbar { + box-shadow: 0 1px 0 rgba(204, 204, 204, 1); + box-shadow: 0 1px 0 var(--toolbar-border-color); +} +@media (prefers-color-scheme: dark) { + + .secondaryToolbar { + box-shadow: 0 1px 0 rgba(12, 12, 13, 1); + box-shadow: 0 1px 0 var(--toolbar-border-color); + } +} +html[dir="rtl"] #toolbarContainer { + box-shadow: 0 1px 0 rgba(204, 204, 204, 1); + box-shadow: 0 1px 0 var(--toolbar-border-color); +} +@media (prefers-color-scheme: dark) { + + html[dir="rtl"] #toolbarContainer { + box-shadow: 0 1px 0 rgba(12, 12, 13, 1); + box-shadow: 0 1px 0 var(--toolbar-border-color); + } +} +.findbar { + box-shadow: 0 1px 0 rgba(204, 204, 204, 1); + box-shadow: 0 1px 0 var(--toolbar-border-color); +} +@media (prefers-color-scheme: dark) { + + .findbar { + box-shadow: 0 1px 0 rgba(12, 12, 13, 1); + box-shadow: 0 1px 0 var(--toolbar-border-color); + } +} +.secondaryToolbar { + box-shadow: 0 1px 0 rgba(204, 204, 204, 1); + box-shadow: 0 1px 0 var(--toolbar-border-color); +} +@media (prefers-color-scheme: dark) { + + .secondaryToolbar { + box-shadow: 0 1px 0 rgba(12, 12, 13, 1); + box-shadow: 0 1px 0 var(--toolbar-border-color); + } } #toolbarViewer { @@ -747,8 +1108,26 @@ html[dir='rtl'] #toolbarContainer, .findbar, .secondaryToolbar { position: relative; width: 100%; height: 4px; - background-color: #333; - border-bottom: 1px solid #333; + background-color: rgba(237, 237, 240, 1); + background-color: var(--body-bg-color); + border-bottom: 1px solid rgba(204, 204, 204, 1); + border-bottom: 1px solid var(--toolbar-border-color); +} + +@media (prefers-color-scheme: dark) { + + #loadingBar { + border-bottom: 1px solid rgba(12, 12, 13, 1); + border-bottom: 1px solid var(--toolbar-border-color); + } +} + +@media (prefers-color-scheme: dark) { + + #loadingBar { + background-color: rgba(42, 42, 46, 1); + background-color: var(--body-bg-color); + } } #loadingBar .progress { @@ -757,54 +1136,306 @@ html[dir='rtl'] #toolbarContainer, .findbar, .secondaryToolbar { left: 0; width: 0%; height: 100%; - background-color: #ddd; + background-color: rgba(10, 132, 255, 1); + background-color: var(--progressBar-color); overflow: hidden; - -webkit-transition: width 200ms; transition: width 200ms; } +@media (prefers-color-scheme: dark) { + + #loadingBar .progress { + background-color: rgba(0, 96, 223, 1); + background-color: var(--progressBar-color); + } +} + @-webkit-keyframes progressIndeterminate { - 0% { left: -142px; } - 100% { left: 0; } + 0% { + left: -142px; + } + 100% { + left: 0; + } } @keyframes progressIndeterminate { - 0% { left: -142px; } - 100% { left: 0; } + 0% { + left: -142px; + } + 100% { + left: 0; + } } #loadingBar .progress.indeterminate { - background-color: #999; - -webkit-transition: none; + background-color: rgba(221, 221, 222, 1); + background-color: var(--progressBar-indeterminate-bg-color); transition: none; } +@media (prefers-color-scheme: dark) { + + #loadingBar .progress.indeterminate { + background-color: rgba(40, 40, 43, 1); + background-color: var(--progressBar-indeterminate-bg-color); + } +} + #loadingBar .progress.indeterminate .glimmer { position: absolute; top: 0; left: 0; height: 100%; width: calc(100% + 150px); - - background: repeating-linear-gradient(135deg, - #bbb 0, #999 5px, - #999 45px, #ddd 55px, - #ddd 95px, #bbb 100px); - - -webkit-animation: progressIndeterminate 950ms linear infinite; - - animation: progressIndeterminate 950ms linear infinite; + background: repeating-linear-gradient( + 135deg, + rgba(116, 177, 239, 1) 0, + rgba(221, 221, 222, 1) 5px, + rgba(221, 221, 222, 1) 45px, + rgba(10, 132, 255, 1) 55px, + rgba(10, 132, 255, 1) 95px, + rgba(116, 177, 239, 1) 100px + ); + background: repeating-linear-gradient( + 135deg, + var(--progressBar-indeterminate-blend-color) 0, + var(--progressBar-indeterminate-bg-color) 5px, + var(--progressBar-indeterminate-bg-color) 45px, + var(--progressBar-color) 55px, + var(--progressBar-color) 95px, + var(--progressBar-indeterminate-blend-color) 100px + ); + -webkit-animation: progressIndeterminate 1s linear infinite; + animation: progressIndeterminate 1s linear infinite; } -.findbar, .secondaryToolbar { +@media (prefers-color-scheme: dark) { + + #loadingBar .progress.indeterminate .glimmer { + background: repeating-linear-gradient( + 135deg, + rgba(20, 68, 133, 1) 0, + rgba(40, 40, 43, 1) 5px, + rgba(40, 40, 43, 1) 45px, + rgba(0, 96, 223, 1) 55px, + rgba(0, 96, 223, 1) 95px, + rgba(20, 68, 133, 1) 100px + ); + background: repeating-linear-gradient( + 135deg, + var(--progressBar-indeterminate-blend-color) 0, + var(--progressBar-indeterminate-bg-color) 5px, + var(--progressBar-indeterminate-bg-color) 45px, + var(--progressBar-color) 55px, + var(--progressBar-color) 95px, + var(--progressBar-indeterminate-blend-color) 100px + ); + } +} + +@media (prefers-color-scheme: dark) { + + #loadingBar .progress.indeterminate .glimmer { + background: repeating-linear-gradient( + 135deg, + rgba(20, 68, 133, 1) 0, + rgba(40, 40, 43, 1) 5px, + rgba(40, 40, 43, 1) 45px, + rgba(0, 96, 223, 1) 55px, + rgba(0, 96, 223, 1) 95px, + rgba(20, 68, 133, 1) 100px + ); + background: repeating-linear-gradient( + 135deg, + var(--progressBar-indeterminate-blend-color) 0, + var(--progressBar-indeterminate-bg-color) 5px, + var(--progressBar-indeterminate-bg-color) 45px, + var(--progressBar-color) 55px, + var(--progressBar-color) 95px, + var(--progressBar-indeterminate-blend-color) 100px + ); + } +} + +@media (prefers-color-scheme: dark) { + + #loadingBar .progress.indeterminate .glimmer { + background: repeating-linear-gradient( + 135deg, + rgba(20, 68, 133, 1) 0, + rgba(40, 40, 43, 1) 5px, + rgba(40, 40, 43, 1) 45px, + rgba(0, 96, 223, 1) 55px, + rgba(0, 96, 223, 1) 95px, + rgba(20, 68, 133, 1) 100px + ); + background: repeating-linear-gradient( + 135deg, + var(--progressBar-indeterminate-blend-color) 0, + var(--progressBar-indeterminate-bg-color) 5px, + var(--progressBar-indeterminate-bg-color) 45px, + var(--progressBar-color) 55px, + var(--progressBar-color) 95px, + var(--progressBar-indeterminate-blend-color) 100px + ); + } +} + +@media (prefers-color-scheme: dark) { + + #loadingBar .progress.indeterminate .glimmer { + background: repeating-linear-gradient( + 135deg, + rgba(20, 68, 133, 1) 0, + rgba(40, 40, 43, 1) 5px, + rgba(40, 40, 43, 1) 45px, + rgba(0, 96, 223, 1) 55px, + rgba(0, 96, 223, 1) 95px, + rgba(20, 68, 133, 1) 100px + ); + background: repeating-linear-gradient( + 135deg, + var(--progressBar-indeterminate-blend-color) 0, + var(--progressBar-indeterminate-bg-color) 5px, + var(--progressBar-indeterminate-bg-color) 45px, + var(--progressBar-color) 55px, + var(--progressBar-color) 95px, + var(--progressBar-indeterminate-blend-color) 100px + ); + } +} + +@media (prefers-color-scheme: dark) { + + #loadingBar .progress.indeterminate .glimmer { + background: repeating-linear-gradient( + 135deg, + rgba(20, 68, 133, 1) 0, + rgba(40, 40, 43, 1) 5px, + rgba(40, 40, 43, 1) 45px, + rgba(0, 96, 223, 1) 55px, + rgba(0, 96, 223, 1) 95px, + rgba(20, 68, 133, 1) 100px + ); + background: repeating-linear-gradient( + 135deg, + var(--progressBar-indeterminate-blend-color) 0, + var(--progressBar-indeterminate-bg-color) 5px, + var(--progressBar-indeterminate-bg-color) 45px, + var(--progressBar-color) 55px, + var(--progressBar-color) 95px, + var(--progressBar-indeterminate-blend-color) 100px + ); + } +} + +@media (prefers-color-scheme: dark) { + + #loadingBar .progress.indeterminate .glimmer { + background: repeating-linear-gradient( + 135deg, + rgba(20, 68, 133, 1) 0, + rgba(40, 40, 43, 1) 5px, + rgba(40, 40, 43, 1) 45px, + rgba(0, 96, 223, 1) 55px, + rgba(0, 96, 223, 1) 95px, + rgba(20, 68, 133, 1) 100px + ); + background: repeating-linear-gradient( + 135deg, + var(--progressBar-indeterminate-blend-color) 0, + var(--progressBar-indeterminate-bg-color) 5px, + var(--progressBar-indeterminate-bg-color) 45px, + var(--progressBar-color) 55px, + var(--progressBar-color) 95px, + var(--progressBar-indeterminate-blend-color) 100px + ); + } +} + +@media (prefers-color-scheme: dark) { + + #loadingBar .progress.indeterminate .glimmer { + background: repeating-linear-gradient( + 135deg, + rgba(20, 68, 133, 1) 0, + rgba(40, 40, 43, 1) 5px, + rgba(40, 40, 43, 1) 45px, + rgba(0, 96, 223, 1) 55px, + rgba(0, 96, 223, 1) 95px, + rgba(20, 68, 133, 1) 100px + ); + background: repeating-linear-gradient( + 135deg, + var(--progressBar-indeterminate-blend-color) 0, + var(--progressBar-indeterminate-bg-color) 5px, + var(--progressBar-indeterminate-bg-color) 45px, + var(--progressBar-color) 55px, + var(--progressBar-color) 95px, + var(--progressBar-indeterminate-blend-color) 100px + ); + } +} + +@media (prefers-color-scheme: dark) { + + #loadingBar .progress.indeterminate .glimmer { + background: repeating-linear-gradient( + 135deg, + rgba(20, 68, 133, 1) 0, + rgba(40, 40, 43, 1) 5px, + rgba(40, 40, 43, 1) 45px, + rgba(0, 96, 223, 1) 55px, + rgba(0, 96, 223, 1) 95px, + rgba(20, 68, 133, 1) 100px + ); + background: repeating-linear-gradient( + 135deg, + var(--progressBar-indeterminate-blend-color) 0, + var(--progressBar-indeterminate-bg-color) 5px, + var(--progressBar-indeterminate-bg-color) 45px, + var(--progressBar-color) 55px, + var(--progressBar-color) 95px, + var(--progressBar-indeterminate-blend-color) 100px + ); + } +} + +@media (prefers-color-scheme: dark) { + + #loadingBar .progress.indeterminate .glimmer { + background: repeating-linear-gradient( + 135deg, + rgba(20, 68, 133, 1) 0, + rgba(40, 40, 43, 1) 5px, + rgba(40, 40, 43, 1) 45px, + rgba(0, 96, 223, 1) 55px, + rgba(0, 96, 223, 1) 95px, + rgba(20, 68, 133, 1) 100px + ); + background: repeating-linear-gradient( + 135deg, + var(--progressBar-indeterminate-blend-color) 0, + var(--progressBar-indeterminate-bg-color) 5px, + var(--progressBar-indeterminate-bg-color) 45px, + var(--progressBar-color) 55px, + var(--progressBar-color) 95px, + var(--progressBar-indeterminate-blend-color) 100px + ); + } +} + +.findbar, +.secondaryToolbar { top: 32px; position: absolute; z-index: 10000; height: auto; min-width: 16px; - padding: 0px 6px 0px 6px; + padding: 0px 4px 0px 4px; margin: 4px 2px 4px 2px; - color: hsl(0,0%,85%); + color: rgba(217, 217, 217, 1); font-size: 12px; line-height: 14px; text-align: left; @@ -813,6 +1444,16 @@ html[dir='rtl'] #toolbarContainer, .findbar, .secondaryToolbar { .findbar { min-width: 300px; + background-color: rgba(249, 249, 250, 1); + background-color: var(--toolbar-bg-color); +} + +@media (prefers-color-scheme: dark) { + + .findbar { + background-color: rgba(56, 56, 61, 1); + background-color: var(--toolbar-bg-color); + } } .findbar > div { height: 32px; @@ -823,11 +1464,114 @@ html[dir='rtl'] #toolbarContainer, .findbar, .secondaryToolbar { .findbar.wrapContainers > div#findbarMessageContainer { height: auto; } -html[dir='ltr'] .findbar { - left: 68px; +html[dir="ltr"] .findbar { + left: 64px; } -html[dir='rtl'] .findbar { - right: 68px; +html[dir="rtl"] .findbar { + right: 64px; +} + +html[dir="ltr"] .findbar .splitToolbarButton { + margin-left: 0px; + margin-top: 3px; +} + +html[dir="rtl"] .findbar .splitToolbarButton { + margin-right: 0px; + margin-top: 3px; +} + +.findbar .splitToolbarButton .findNext { + width: 29px; +} + +html[dir="ltr"] .findbar .splitToolbarButton .findNext { + border-right: 1px solid rgba(187, 187, 188, 1); + border-right: 1px solid var(--field-border-color); +} + +@media (prefers-color-scheme: dark) { + + html[dir="ltr"] .findbar .splitToolbarButton .findNext { + border-right: 1px solid rgba(115, 115, 115, 1); + border-right: 1px solid var(--field-border-color); + } +} + +html[dir="rtl"] .findbar .splitToolbarButton .findNext { + border-left: 1px solid rgba(187, 187, 188, 1); + border-left: 1px solid var(--field-border-color); +} + +@media (prefers-color-scheme: dark) { + + html[dir="rtl"] .findbar .splitToolbarButton .findNext { + border-left: 1px solid rgba(115, 115, 115, 1); + border-left: 1px solid var(--field-border-color); + } +} + +.findbar .splitToolbarButton .toolbarButton { + background-color: rgba(227, 228, 230, 1); + background-color: var(--findbar-nextprevious-btn-bg-color); + border-radius: 0px; + height: 26px; + border-top: 1px solid rgba(187, 187, 188, 1); + border-top: 1px solid var(--field-border-color); + border-bottom: 1px solid rgba(187, 187, 188, 1); + border-bottom: 1px solid var(--field-border-color); +} + +@media (prefers-color-scheme: dark) { + + .findbar .splitToolbarButton .toolbarButton { + border-bottom: 1px solid rgba(115, 115, 115, 1); + border-bottom: 1px solid var(--field-border-color); + } +} + +@media (prefers-color-scheme: dark) { + + .findbar .splitToolbarButton .toolbarButton { + border-top: 1px solid rgba(115, 115, 115, 1); + border-top: 1px solid var(--field-border-color); + } +} + +@media (prefers-color-scheme: dark) { + + .findbar .splitToolbarButton .toolbarButton { + background-color: rgba(89, 89, 89, 1); + background-color: var(--findbar-nextprevious-btn-bg-color); + } +} + +.findbar .splitToolbarButton .toolbarButton::before { + top: 5px; +} + +html[dir="ltr"] .findbar .splitToolbarButton > .findPrevious { + border-radius: 0; +} +html[dir="ltr"] .findbar .splitToolbarButton > .findNext { + border-bottom-left-radius: 0; + border-bottom-right-radius: 2px; + border-top-left-radius: 0; + border-top-right-radius: 2px; +} + +html[dir="rtl"] .findbar .splitToolbarButton > .findPrevious { + border-radius: 0; +} +html[dir="rtl"] .findbar .splitToolbarButton > .findNext { + border-bottom-left-radius: 2px; + border-bottom-right-radius: 0; + border-top-left-radius: 2px; + border-top-right-radius: 0; +} + +.findbar input[type="checkbox"] { + pointer-events: none; } .findbar label { @@ -837,47 +1581,114 @@ html[dir='rtl'] .findbar { user-select: none; } +.findbar label:hover { + background-color: rgba(221, 222, 223, 1); + background-color: var(--button-hover-color); +} + +@media (prefers-color-scheme: dark) { + + .findbar label:hover { + background-color: rgba(102, 102, 103, 1); + background-color: var(--button-hover-color); + } +} + +.findbar input:focus + label { + background-color: rgba(221, 222, 223, 1); + background-color: var(--button-hover-color); +} + +@media (prefers-color-scheme: dark) { + + .findbar input:focus + label { + background-color: rgba(102, 102, 103, 1); + background-color: var(--button-hover-color); + } +} + +html[dir="ltr"] #findInput { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +html[dir="rtl"] #findInput { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.findbar .toolbarField[type="checkbox"]:checked + .toolbarLabel { + background-color: rgba(0, 0, 0, 0.3) !important; + background-color: var(--toggled-btn-bg-color) !important; +} + +@media (prefers-color-scheme: dark) { + + .findbar .toolbarField[type="checkbox"]:checked + .toolbarLabel { + background-color: rgba(0, 0, 0, 0.3) !important; + background-color: var(--toggled-btn-bg-color) !important; + } +} + #findInput { width: 200px; } #findInput::-webkit-input-placeholder { - color: hsl(0, 0%, 75%); + color: rgba(191, 191, 191, 1); } #findInput::-moz-placeholder { - font-style: italic; + font-style: normal; } #findInput:-ms-input-placeholder { - font-style: italic; + font-style: normal; } #findInput::-ms-input-placeholder { - font-style: italic; + font-style: normal; } #findInput::placeholder { - font-style: italic; + font-style: normal; } #findInput[data-status="pending"] { - background-image: url(images/loading-small.png); + background-image: url(images/loading.svg); + /*background-image: var(--loading-icon);*/ background-repeat: no-repeat; - background-position: right; + background-position: 98%; } -html[dir='rtl'] #findInput[data-status="pending"] { - background-position: left; +@media (prefers-color-scheme: dark) { + + #findInput[data-status="pending"] { + background-image: url(images/loading-dark.svg); + /*background-image: var(--loading-icon);*/ + } +} +html[dir="rtl"] #findInput[data-status="pending"] { + background-position: 3px; } .secondaryToolbar { - padding: 6px; + padding: 6px 0 10px 0; height: auto; z-index: 30000; + background-color: rgba(255, 255, 255, 1); + background-color: var(--doorhanger-bg-color); } -html[dir='ltr'] .secondaryToolbar { + +@media (prefers-color-scheme: dark) { + + .secondaryToolbar { + background-color: rgba(74, 74, 79, 1); + background-color: var(--doorhanger-bg-color); + } +} +html[dir="ltr"] .secondaryToolbar { right: 4px; } -html[dir='rtl'] .secondaryToolbar { +html[dir="rtl"] .secondaryToolbar { left: 4px; } #secondaryToolbarButtonContainer { - max-width: 200px; + max-width: 220px; max-height: 400px; overflow-y: auto; -webkit-overflow-scrolling: touch; @@ -889,16 +1700,47 @@ html[dir='rtl'] .secondaryToolbar { display: none !important; } -.doorHanger, -.doorHangerRight { - border: 1px solid hsla(0,0%,0%,.5); +.doorHanger { border-radius: 2px; - box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3); + box-shadow: 0 1px 5px rgba(12, 12, 13, 0.2), + 0 0 0 1px rgba(12, 12, 13, 0.2); + box-shadow: 0 1px 5px var(--doorhanger-border-color), + 0 0 0 1px var(--doorhanger-border-color); } -.doorHanger:after, .doorHanger:before, -.doorHangerRight:after, .doorHangerRight:before { + +@media (prefers-color-scheme: dark) { + + .doorHanger { + box-shadow: 0 1px 5px rgba(39, 39, 43, 1), + 0 0 0 1px rgba(39, 39, 43, 1); + box-shadow: 0 1px 5px var(--doorhanger-border-color), + 0 0 0 1px var(--doorhanger-border-color); + } +} + +.doorHangerRight { + border-radius: 2px; + box-shadow: 0 1px 5px rgba(12, 12, 13, 0.2), + 0 0 0 1px rgba(12, 12, 13, 0.2); + box-shadow: 0 1px 5px var(--doorhanger-border-color), + 0 0 0 1px var(--doorhanger-border-color); +} + +@media (prefers-color-scheme: dark) { + + .doorHangerRight { + box-shadow: 0 1px 5px rgba(39, 39, 43, 1), + 0 0 0 1px rgba(39, 39, 43, 1); + box-shadow: 0 1px 5px var(--doorhanger-border-color), + 0 0 0 1px var(--doorhanger-border-color); + } +} +.doorHanger:after, +.doorHanger:before, +.doorHangerRight:after, +.doorHangerRight:before { bottom: 100%; - border: solid transparent; + border: solid rgba(0, 0, 0, 0); content: " "; height: 0; width: 0; @@ -907,102 +1749,141 @@ html[dir='rtl'] .secondaryToolbar { } .doorHanger:after, .doorHangerRight:after { - border-bottom-color: hsla(0,0%,32%,.99); border-width: 8px; } -.doorHanger:before, -.doorHangerRight:before { - border-bottom-color: hsla(0,0%,0%,.5); +.doorHanger:after { + border-bottom-color: rgba(249, 249, 250, 1); + border-bottom-color: var(--toolbar-bg-color); +} +@media (prefers-color-scheme: dark) { + + .doorHanger:after { + border-bottom-color: rgba(56, 56, 61, 1); + border-bottom-color: var(--toolbar-bg-color); + } +} +.doorHangerRight:after { + border-bottom-color: rgba(255, 255, 255, 1); + border-bottom-color: var(--doorhanger-bg-color); +} +@media (prefers-color-scheme: dark) { + + .doorHangerRight:after { + border-bottom-color: rgba(74, 74, 79, 1); + border-bottom-color: var(--doorhanger-bg-color); + } +} +.doorHanger:before { + border-bottom-color: rgba(12, 12, 13, 0.2); + border-bottom-color: var(--doorhanger-border-color); border-width: 9px; } +@media (prefers-color-scheme: dark) { -html[dir='ltr'] .doorHanger:after, -html[dir='rtl'] .doorHangerRight:after { - left: 13px; + .doorHanger:before { + border-bottom-color: rgba(39, 39, 43, 1); + border-bottom-color: var(--doorhanger-border-color); + } +} +.doorHangerRight:before { + border-bottom-color: rgba(12, 12, 13, 0.2); + border-bottom-color: var(--doorhanger-border-color); + border-width: 9px; +} +@media (prefers-color-scheme: dark) { + + .doorHangerRight:before { + border-bottom-color: rgba(39, 39, 43, 1); + border-bottom-color: var(--doorhanger-border-color); + } +} + +html[dir="ltr"] .doorHanger:after, +html[dir="rtl"] .doorHangerRight:after { + left: 10px; margin-left: -8px; } -html[dir='ltr'] .doorHanger:before, -html[dir='rtl'] .doorHangerRight:before { - left: 13px; +html[dir="ltr"] .doorHanger:before, +html[dir="rtl"] .doorHangerRight:before { + left: 10px; margin-left: -9px; } -html[dir='rtl'] .doorHanger:after, -html[dir='ltr'] .doorHangerRight:after { - right: 13px; +html[dir="rtl"] .doorHanger:after, +html[dir="ltr"] .doorHangerRight:after { + right: 10px; margin-right: -8px; } -html[dir='rtl'] .doorHanger:before, -html[dir='ltr'] .doorHangerRight:before { - right: 13px; +html[dir="rtl"] .doorHanger:before, +html[dir="ltr"] .doorHangerRight:before { + right: 10px; margin-right: -9px; } #findResultsCount { - background-color: hsl(0, 0%, 85%); - color: hsl(0, 0%, 32%); + background-color: rgba(217, 217, 217, 1); + color: rgba(82, 82, 82, 1); text-align: center; padding: 3px 4px; + margin: 5px; } #findMsg { - font-style: italic; - color: #A6B7D0; + color: rgba(251, 0, 0, 1); } #findMsg:empty { display: none; } #findInput.notFound { - background-color: rgb(255, 102, 102); + background-color: rgba(255, 102, 102, 1); } #toolbarViewerMiddle { position: absolute; left: 50%; - -webkit-transform: translateX(-50%); - transform: translateX(-50%); + transform: translateX(-50%); } -html[dir='ltr'] #toolbarViewerLeft, -html[dir='rtl'] #toolbarViewerRight { +html[dir="ltr"] #toolbarViewerLeft, +html[dir="rtl"] #toolbarViewerRight { float: left; } -html[dir='ltr'] #toolbarViewerRight, -html[dir='rtl'] #toolbarViewerLeft { +html[dir="ltr"] #toolbarViewerRight, +html[dir="rtl"] #toolbarViewerLeft { float: right; } -html[dir='ltr'] #toolbarViewerLeft > *, -html[dir='ltr'] #toolbarViewerMiddle > *, -html[dir='ltr'] #toolbarViewerRight > *, -html[dir='ltr'] .findbar * { +html[dir="ltr"] #toolbarViewerLeft > *, +html[dir="ltr"] #toolbarViewerMiddle > *, +html[dir="ltr"] #toolbarViewerRight > *, +html[dir="ltr"] .findbar * { position: relative; float: left; } -html[dir='rtl'] #toolbarViewerLeft > *, -html[dir='rtl'] #toolbarViewerMiddle > *, -html[dir='rtl'] #toolbarViewerRight > *, -html[dir='rtl'] .findbar * { +html[dir="rtl"] #toolbarViewerLeft > *, +html[dir="rtl"] #toolbarViewerMiddle > *, +html[dir="rtl"] #toolbarViewerRight > *, +html[dir="rtl"] .findbar * { position: relative; float: right; } -html[dir='ltr'] .splitToolbarButton { - margin: 3px 2px 4px 0; +html[dir="ltr"] .splitToolbarButton { + margin: 2px 2px 0; display: inline-block; } -html[dir='rtl'] .splitToolbarButton { - margin: 3px 0 4px 2px; +html[dir="rtl"] .splitToolbarButton { + margin: 2px 2px 0; display: inline-block; } -html[dir='ltr'] .splitToolbarButton > .toolbarButton { - border-radius: 0; +html[dir="ltr"] .splitToolbarButton > .toolbarButton { + border-radius: 2px; float: left; } -html[dir='rtl'] .splitToolbarButton > .toolbarButton { - border-radius: 0; +html[dir="rtl"] .splitToolbarButton > .toolbarButton { + border-radius: 2px; float: right; } @@ -1011,8 +1892,45 @@ html[dir='rtl'] .splitToolbarButton > .toolbarButton { .overlayButton { border: 0 none; background: none; - width: 32px; - height: 25px; + width: 28px; + height: 28px; +} +.overlayButton { + background-color: rgba(12, 12, 13, 0.1); + background-color: var(--overlay-button-bg-color); +} +@media (prefers-color-scheme: dark) { + + .overlayButton { + background-color: rgba(92, 92, 97, 1); + background-color: var(--overlay-button-bg-color); + } +} + +.overlayButton:hover { + background-color: rgba(12, 12, 13, 0.3); + background-color: var(--overlay-button-hover-color); +} + +@media (prefers-color-scheme: dark) { + + .overlayButton:hover { + background-color: rgba(115, 115, 115, 1); + background-color: var(--overlay-button-hover-color); + } +} + +.overlayButton:focus { + background-color: rgba(12, 12, 13, 0.3); + background-color: var(--overlay-button-hover-color); +} + +@media (prefers-color-scheme: dark) { + + .overlayButton:focus { + background-color: rgba(115, 115, 115, 1); + background-color: var(--overlay-button-hover-color); + } } .toolbarButton > span { @@ -1025,105 +1943,141 @@ html[dir='rtl'] .splitToolbarButton > .toolbarButton { .toolbarButton[disabled], .secondaryToolbarButton[disabled], .overlayButton[disabled] { - opacity: .5; + opacity: 0.5; } .splitToolbarButton.toggled .toolbarButton { margin: 0; } -.splitToolbarButton:hover > .toolbarButton, -.splitToolbarButton:focus > .toolbarButton, -.splitToolbarButton.toggled > .toolbarButton, -.toolbarButton.textButton { - background-color: hsla(0,0%,0%,.12); - background-image: -webkit-gradient(linear, left top, left bottom, from(hsla(0,0%,100%,.05)), to(hsla(0,0%,100%,0))); - background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0)); - background-clip: padding-box; - border: 1px solid hsla(0,0%,0%,.35); - border-color: hsla(0,0%,0%,.32) hsla(0,0%,0%,.38) hsla(0,0%,0%,.42); - box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset, - 0 0 1px hsla(0,0%,100%,.15) inset, - 0 1px 0 hsla(0,0%,100%,.05); - -webkit-transition-property: background-color, border-color, box-shadow; - transition-property: background-color, border-color, box-shadow; - -webkit-transition-duration: 150ms; - transition-duration: 150ms; - -webkit-transition-timing-function: ease; - transition-timing-function: ease; - -} -.splitToolbarButton > .toolbarButton:hover, -.splitToolbarButton > .toolbarButton:focus, -.dropdownToolbarButton:hover, -.overlayButton:hover, -.overlayButton:focus, -.toolbarButton.textButton:hover, -.toolbarButton.textButton:focus { - background-color: hsla(0,0%,0%,.2); - box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset, - 0 0 1px hsla(0,0%,100%,.15) inset, - 0 0 1px hsla(0,0%,0%,.05); +.splitToolbarButton > .toolbarButton:hover { + background-color: rgba(221, 222, 223, 1); + background-color: var(--button-hover-color); z-index: 199; } + +@media (prefers-color-scheme: dark) { + + .splitToolbarButton > .toolbarButton:hover { + background-color: rgba(102, 102, 103, 1); + background-color: var(--button-hover-color); + } +} + +.splitToolbarButton > .toolbarButton:focus { + background-color: rgba(221, 222, 223, 1); + background-color: var(--button-hover-color); + z-index: 199; +} + +@media (prefers-color-scheme: dark) { + + .splitToolbarButton > .toolbarButton:focus { + background-color: rgba(102, 102, 103, 1); + background-color: var(--button-hover-color); + } +} + +.dropdownToolbarButton:hover { + background-color: rgba(221, 222, 223, 1); + background-color: var(--button-hover-color); + z-index: 199; +} + +@media (prefers-color-scheme: dark) { + + .dropdownToolbarButton:hover { + background-color: rgba(102, 102, 103, 1); + background-color: var(--button-hover-color); + } +} + +.toolbarButton.textButton:hover { + background-color: rgba(221, 222, 223, 1); + background-color: var(--button-hover-color); + z-index: 199; +} + +@media (prefers-color-scheme: dark) { + + .toolbarButton.textButton:hover { + background-color: rgba(102, 102, 103, 1); + background-color: var(--button-hover-color); + } +} + +.toolbarButton.textButton:focus { + background-color: rgba(221, 222, 223, 1); + background-color: var(--button-hover-color); + z-index: 199; +} + +@media (prefers-color-scheme: dark) { + + .toolbarButton.textButton:focus { + background-color: rgba(102, 102, 103, 1); + background-color: var(--button-hover-color); + } +} .splitToolbarButton > .toolbarButton { position: relative; } -html[dir='ltr'] .splitToolbarButton > .toolbarButton:first-child, -html[dir='rtl'] .splitToolbarButton > .toolbarButton:last-child { +html[dir="ltr"] .splitToolbarButton > .toolbarButton:first-child, +html[dir="rtl"] .splitToolbarButton > .toolbarButton:last-child { position: relative; margin: 0; - margin-right: -1px; - border-top-left-radius: 2px; - border-bottom-left-radius: 2px; - border-right-color: transparent; } -html[dir='ltr'] .splitToolbarButton > .toolbarButton:last-child, -html[dir='rtl'] .splitToolbarButton > .toolbarButton:first-child { +html[dir="ltr"] .splitToolbarButton > .toolbarButton:last-child, +html[dir="rtl"] .splitToolbarButton > .toolbarButton:first-child { position: relative; margin: 0; - margin-left: -1px; - border-top-right-radius: 2px; - border-bottom-right-radius: 2px; - border-left-color: transparent; } .splitToolbarButtonSeparator { - padding: 8px 0; + padding: 10px 0; width: 1px; - background-color: hsla(0,0%,0%,.5); + background-color: rgba(0, 0, 0, 0.3); + background-color: var(--separator-color); z-index: 99; - box-shadow: 0 0 0 1px hsla(0,0%,100%,.08); display: inline-block; - margin: 5px 0; + margin: 4px 0; } -html[dir='ltr'] .splitToolbarButtonSeparator { - float: left; -} -html[dir='rtl'] .splitToolbarButtonSeparator { - float: right; -} -.splitToolbarButton:hover > .splitToolbarButtonSeparator, -.splitToolbarButton.toggled > .splitToolbarButtonSeparator { - padding: 12px 0; - margin: 1px 0; - box-shadow: 0 0 0 1px hsla(0,0%,100%,.03); - -webkit-transition-property: padding; - transition-property: padding; - -webkit-transition-duration: 10ms; - transition-duration: 10ms; - -webkit-transition-timing-function: ease; - transition-timing-function: ease; +@media (prefers-color-scheme: dark) { + + .splitToolbarButtonSeparator { + background-color: rgba(0, 0, 0, 0.3); + background-color: var(--separator-color); + } } -.toolbarButton, -.dropdownToolbarButton, -.secondaryToolbarButton, -.overlayButton { +.findbar .splitToolbarButtonSeparator { + background-color: rgba(187, 187, 188, 1); + background-color: var(--field-border-color); + margin: 0; + padding: 13px 0; +} + +@media (prefers-color-scheme: dark) { + + .findbar .splitToolbarButtonSeparator { + background-color: rgba(115, 115, 115, 1); + background-color: var(--field-border-color); + } +} + +html[dir="ltr"] .splitToolbarButtonSeparator { + float: left; +} +html[dir="rtl"] .splitToolbarButtonSeparator { + float: right; +} + +.toolbarButton { min-width: 16px; padding: 2px 6px 0; - border: 1px solid transparent; + border: none; border-radius: 2px; - color: hsla(0,0%,100%,.8); + color: rgba(12, 12, 13, 1); + color: var(--main-color); font-size: 12px; line-height: 14px; -webkit-user-select: none; @@ -1132,115 +2086,314 @@ html[dir='rtl'] .splitToolbarButtonSeparator { user-select: none; /* Opera does not support user-select, use <... unselectable="on"> instead */ cursor: default; - -webkit-transition-property: background-color, border-color, box-shadow; - transition-property: background-color, border-color, box-shadow; - -webkit-transition-duration: 150ms; - transition-duration: 150ms; - -webkit-transition-timing-function: ease; - transition-timing-function: ease; + box-sizing: border-box; } -html[dir='ltr'] .toolbarButton, -html[dir='ltr'] .overlayButton, -html[dir='ltr'] .dropdownToolbarButton { - margin: 3px 2px 4px 0; -} -html[dir='rtl'] .toolbarButton, -html[dir='rtl'] .overlayButton, -html[dir='rtl'] .dropdownToolbarButton { - margin: 3px 0 4px 2px; +@media (prefers-color-scheme: dark) { + + .toolbarButton { + color: rgba(249, 249, 250, 1); + color: var(--main-color); + } } -.toolbarButton:hover, -.toolbarButton:focus, -.dropdownToolbarButton, -.overlayButton, -.secondaryToolbarButton:hover, +.dropdownToolbarButton { + min-width: 16px; + padding: 2px 6px 0; + border: none; + border-radius: 2px; + color: rgba(12, 12, 13, 1); + color: var(--main-color); + font-size: 12px; + line-height: 14px; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + /* Opera does not support user-select, use <... unselectable="on"> instead */ + cursor: default; + box-sizing: border-box; +} + +@media (prefers-color-scheme: dark) { + + .dropdownToolbarButton { + color: rgba(249, 249, 250, 1); + color: var(--main-color); + } +} + +.secondaryToolbarButton { + min-width: 16px; + padding: 2px 6px 0; + border: none; + border-radius: 2px; + color: rgba(12, 12, 13, 1); + color: var(--main-color); + font-size: 12px; + line-height: 14px; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + /* Opera does not support user-select, use <... unselectable="on"> instead */ + cursor: default; + box-sizing: border-box; +} + +@media (prefers-color-scheme: dark) { + + .secondaryToolbarButton { + color: rgba(249, 249, 250, 1); + color: var(--main-color); + } +} + +.overlayButton { + min-width: 16px; + padding: 2px 6px 0; + border: none; + border-radius: 2px; + color: rgba(12, 12, 13, 1); + color: var(--main-color); + font-size: 12px; + line-height: 14px; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + /* Opera does not support user-select, use <... unselectable="on"> instead */ + cursor: default; + box-sizing: border-box; +} + +@media (prefers-color-scheme: dark) { + + .overlayButton { + color: rgba(249, 249, 250, 1); + color: var(--main-color); + } +} + +html[dir="ltr"] .toolbarButton, +html[dir="ltr"] .overlayButton, +html[dir="ltr"] .dropdownToolbarButton { + margin: 2px 1px; +} +html[dir="rtl"] .toolbarButton, +html[dir="rtl"] .overlayButton, +html[dir="rtl"] .dropdownToolbarButton { + margin: 2px 1px; +} + +html[dir="ltr"] #toolbarViewerLeft > .toolbarButton:first-child, +html[dir="rtl"] #toolbarViewerRight > .toolbarButton:last-child { + margin-left: 2px; +} + +html[dir="ltr"] #toolbarViewerRight > .toolbarButton:last-child, +html[dir="rtl"] #toolbarViewerLeft > .toolbarButton:first-child { + margin-right: 2px; +} +.toolbarButton:hover { + background-color: rgba(221, 222, 223, 1); + background-color: var(--button-hover-color); +} +@media (prefers-color-scheme: dark) { + + .toolbarButton:hover { + background-color: rgba(102, 102, 103, 1); + background-color: var(--button-hover-color); + } +} +.toolbarButton:focus { + background-color: rgba(221, 222, 223, 1); + background-color: var(--button-hover-color); +} +@media (prefers-color-scheme: dark) { + + .toolbarButton:focus { + background-color: rgba(102, 102, 103, 1); + background-color: var(--button-hover-color); + } +} +.secondaryToolbarButton:hover { + background-color: rgba(237, 237, 237, 1); + background-color: var(--doorhanger-hover-color); +} +@media (prefers-color-scheme: dark) { + + .secondaryToolbarButton:hover { + background-color: rgba(93, 94, 98, 1); + background-color: var(--doorhanger-hover-color); + } +} .secondaryToolbarButton:focus { - background-color: hsla(0,0%,0%,.12); - background-image: -webkit-gradient(linear, left top, left bottom, from(hsla(0,0%,100%,.05)), to(hsla(0,0%,100%,0))); - background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0)); - background-clip: padding-box; - border: 1px solid hsla(0,0%,0%,.35); - border-color: hsla(0,0%,0%,.32) hsla(0,0%,0%,.38) hsla(0,0%,0%,.42); - box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset, - 0 0 1px hsla(0,0%,100%,.15) inset, - 0 1px 0 hsla(0,0%,100%,.05); + background-color: rgba(237, 237, 237, 1); + background-color: var(--doorhanger-hover-color); +} +@media (prefers-color-scheme: dark) { + + .secondaryToolbarButton:focus { + background-color: rgba(93, 94, 98, 1); + background-color: var(--doorhanger-hover-color); + } } -.toolbarButton:hover:active, -.overlayButton:hover:active, -.dropdownToolbarButton:hover:active, -.secondaryToolbarButton:hover:active { - background-color: hsla(0,0%,0%,.2); - background-image: -webkit-gradient(linear, left top, left bottom, from(hsla(0,0%,100%,.05)), to(hsla(0,0%,100%,0))); - background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0)); - border-color: hsla(0,0%,0%,.35) hsla(0,0%,0%,.4) hsla(0,0%,0%,.45); - box-shadow: 0 1px 1px hsla(0,0%,0%,.1) inset, - 0 0 1px hsla(0,0%,0%,.2) inset, - 0 1px 0 hsla(0,0%,100%,.05); - -webkit-transition-property: background-color, border-color, box-shadow; - transition-property: background-color, border-color, box-shadow; - -webkit-transition-duration: 10ms; - transition-duration: 10ms; - -webkit-transition-timing-function: linear; - transition-timing-function: linear; +.toolbarButton.toggled { + background-color: rgba(0, 0, 0, 0.3); + background-color: var(--toggled-btn-bg-color); +} + +@media (prefers-color-scheme: dark) { + + .toolbarButton.toggled { + background-color: rgba(0, 0, 0, 0.3); + background-color: var(--toggled-btn-bg-color); + } +} + +.splitToolbarButton.toggled > .toolbarButton.toggled { + background-color: rgba(0, 0, 0, 0.3); + background-color: var(--toggled-btn-bg-color); +} + +@media (prefers-color-scheme: dark) { + + .splitToolbarButton.toggled > .toolbarButton.toggled { + background-color: rgba(0, 0, 0, 0.3); + background-color: var(--toggled-btn-bg-color); + } } -.toolbarButton.toggled, -.splitToolbarButton.toggled > .toolbarButton.toggled, .secondaryToolbarButton.toggled { - background-color: hsla(0,0%,0%,.3); - background-image: -webkit-gradient(linear, left top, left bottom, from(hsla(0,0%,100%,.05)), to(hsla(0,0%,100%,0))); - background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0)); - border-color: hsla(0,0%,0%,.4) hsla(0,0%,0%,.45) hsla(0,0%,0%,.5); - box-shadow: 0 1px 1px hsla(0,0%,0%,.1) inset, - 0 0 1px hsla(0,0%,0%,.2) inset, - 0 1px 0 hsla(0,0%,100%,.05); - -webkit-transition-property: background-color, border-color, box-shadow; - transition-property: background-color, border-color, box-shadow; - -webkit-transition-duration: 10ms; - transition-duration: 10ms; - -webkit-transition-timing-function: linear; - transition-timing-function: linear; + background-color: rgba(0, 0, 0, 0.3); + background-color: var(--toggled-btn-bg-color); +} + +@media (prefers-color-scheme: dark) { + + .secondaryToolbarButton.toggled { + background-color: rgba(0, 0, 0, 0.3); + background-color: var(--toggled-btn-bg-color); + } } .toolbarButton.toggled:hover:active, .splitToolbarButton.toggled > .toolbarButton.toggled:hover:active, .secondaryToolbarButton.toggled:hover:active { - background-color: hsla(0,0%,0%,.4); - border-color: hsla(0,0%,0%,.4) hsla(0,0%,0%,.5) hsla(0,0%,0%,.55); - box-shadow: 0 1px 1px hsla(0,0%,0%,.2) inset, - 0 0 1px hsla(0,0%,0%,.3) inset, - 0 1px 0 hsla(0,0%,100%,.05); + background-color: rgba(0, 0, 0, 0.4); } .dropdownToolbarButton { - width: 120px; - max-width: 120px; + width: 140px; padding: 0; overflow: hidden; - background: url(images/toolbarButton-menuArrows.png) no-repeat; + background-color: rgba(215, 215, 219, 1); + background-color: var(--dropdown-btn-bg-color); + margin-top: 2px !important; } -html[dir='ltr'] .dropdownToolbarButton { - background-position: 95%; + +@media (prefers-color-scheme: dark) { + + .dropdownToolbarButton { + background-color: rgba(74, 74, 79, 1); + background-color: var(--dropdown-btn-bg-color); + } } -html[dir='rtl'] .dropdownToolbarButton { - background-position: 5%; +.dropdownToolbarButton::after { + position: absolute; + display: inline-block; + top: 6px; + content: url(images/toolbarButton-menuArrow.svg); + /*content: var(--toolbarButton-menuArrow-icon);*/ + pointer-events: none; + max-width: 16px; +} +@media (prefers-color-scheme: dark) { + + .dropdownToolbarButton::after { + content: url(images/toolbarButton-menuArrow-dark.svg); + /*content: var(--toolbarButton-menuArrow-icon);*/ + } +} +html[dir="ltr"] .dropdownToolbarButton::after { + right: 7px; +} +html[dir="rtl"] .dropdownToolbarButton::after { + left: 7px; } .dropdownToolbarButton > select { - min-width: 140px; + width: 162px; + height: 28px; font-size: 12px; - color: hsl(0,0%,95%); + color: rgba(12, 12, 13, 1); + color: var(--main-color); margin: 0; - padding: 3px 2px 2px; + padding: 1px 0 2px; border: none; - background: rgba(0,0,0,0); /* Opera does not support 'transparent' ' + $("#metadata_provider").append($provider_button); + }); + }, + }); + } + + $(document).on("change", ".pill", function () { + var element = $(this); + var id = element.data("control"); + var initial = element.data("initial"); + var val = element.prop('checked'); + var params = {id : id, value: val}; + if (!initial) { + params['initial'] = initial; + params['query'] = keyword; + } + $.ajax({ + method:"post", + contentType: "application/json; charset=utf-8", + dataType: "json", + url: getPath() + "/metadata/provider/" + id, + data: JSON.stringify(params), + success: function success(data) { + element.data("initial", "true"); + data.forEach(function(book) { + var $book = $(templates.bookResult(book)); + $book.find("img").on("click", function () { + populateForm(book); + }); + $("#book-list").append($book); + }); + } + }); + }); + $("#meta-search").on("submit", function (e) { e.preventDefault(); - var keyword = $("#keyword").val(); - if (keyword) { - doSearch(keyword); - } + keyword = $("#keyword").val(); + $('.pill').each(function(){ + // console.log($(this).data('control')); + $(this).data("initial", $(this).prop('checked')); + // console.log($(this).data('initial')); + }); + doSearch(keyword); }); $("#get_meta").click(function () { + populate_provider(); var bookTitle = $("#book_title").val(); - if (bookTitle) { - $("#keyword").val(bookTitle); - doSearch(bookTitle); - } + $("#keyword").val(bookTitle); + keyword = bookTitle; + doSearch(bookTitle); + }); + $("#metaModal").on("show.bs.modal", function(e) { + $(e.relatedTarget).one('focus', function (e) { + $(this).blur(); + }); }); - }); diff --git a/cps/static/js/io/bitstream.js b/cps/static/js/io/bitstream.js deleted file mode 100644 index af1cad99..00000000 --- a/cps/static/js/io/bitstream.js +++ /dev/null @@ -1,237 +0,0 @@ -/* - * bitstream.js - * - * Provides readers for bitstreams. - * - * Licensed under the MIT License - * - * Copyright(c) 2011 Google Inc. - * Copyright(c) 2011 antimatter15 - */ - -/* global bitjs, Uint8Array */ - -var bitjs = bitjs || {}; -bitjs.io = bitjs.io || {}; - -(function() { - - // mask for getting the Nth bit (zero-based) - bitjs.BIT = [0x01, 0x02, 0x04, 0x08, - 0x10, 0x20, 0x40, 0x80, - 0x100, 0x200, 0x400, 0x800, - 0x1000, 0x2000, 0x4000, 0x8000 - ]; - - // mask for getting N number of bits (0-8) - var BITMASK = [0, 0x01, 0x03, 0x07, 0x0F, 0x1F, 0x3F, 0x7F, 0xFF]; - - - /** - * This bit stream peeks and consumes bits out of a binary stream. - * - * @param {ArrayBuffer} ab An ArrayBuffer object or a Uint8Array. - * @param {boolean} rtl Whether the stream reads bits from the byte starting - * from bit 7 to 0 (true) or bit 0 to 7 (false). - * @param {Number} optOffset The offset into the ArrayBuffer - * @param {Number} optLength The length of this BitStream - */ - bitjs.io.BitStream = function(ab, rtl, optOffset, optLength) { - if (!ab || !ab.toString || ab.toString() !== "[object ArrayBuffer]") { - throw "Error! BitArray constructed with an invalid ArrayBuffer object"; - } - - var offset = optOffset || 0; - var length = optLength || ab.byteLength; - this.bytes = new Uint8Array(ab, offset, length); - this.bytePtr = 0; // tracks which byte we are on - this.bitPtr = 0; // tracks which bit we are on (can have values 0 through 7) - this.peekBits = rtl ? this.peekBitsRtl : this.peekBitsLtr; - }; - - - /** - * byte0 byte1 byte2 byte3 - * 7......0 | 7......0 | 7......0 | 7......0 - * - * The bit pointer starts at bit0 of byte0 and moves left until it reaches - * bit7 of byte0, then jumps to bit0 of byte1, etc. - * @param {number} n The number of bits to peek. - * @param {boolean=} movePointers Whether to move the pointer, defaults false. - * @return {number} The peeked bits, as an unsigned number. - */ - bitjs.io.BitStream.prototype.peekBitsLtr = function(n, movePointers) { - if (n <= 0 || typeof n !== typeof 1) { - return 0; - } - - var movePointers = movePointers || false, - bytePtr = this.bytePtr, - bitPtr = this.bitPtr, - result = 0, - bitsIn = 0, - bytes = this.bytes; - - // keep going until we have no more bits left to peek at - // TODO: Consider putting all bits from bytes we will need into a variable and then - // shifting/masking it to just extract the bits we want. - // This could be considerably faster when reading more than 3 or 4 bits at a time. - while (n > 0) { - if (bytePtr >= bytes.length) { - throw "Error! Overflowed the bit stream! n=" + n + ", bytePtr=" + bytePtr + ", bytes.length=" + - bytes.length + ", bitPtr=" + bitPtr; - // return -1; - } - - var numBitsLeftInThisByte = (8 - bitPtr); - var mask; - if (n >= numBitsLeftInThisByte) { - mask = (BITMASK[numBitsLeftInThisByte] << bitPtr); - result |= (((bytes[bytePtr] & mask) >> bitPtr) << bitsIn); - - bytePtr++; - bitPtr = 0; - bitsIn += numBitsLeftInThisByte; - n -= numBitsLeftInThisByte; - } else { - mask = (BITMASK[n] << bitPtr); - result |= (((bytes[bytePtr] & mask) >> bitPtr) << bitsIn); - - bitPtr += n; - bitsIn += n; - n = 0; - } - } - - if (movePointers) { - this.bitPtr = bitPtr; - this.bytePtr = bytePtr; - } - - return result; - }; - - - /** - * byte0 byte1 byte2 byte3 - * 7......0 | 7......0 | 7......0 | 7......0 - * - * The bit pointer starts at bit7 of byte0 and moves right until it reaches - * bit0 of byte0, then goes to bit7 of byte1, etc. - * @param {number} n The number of bits to peek. - * @param {boolean=} movePointers Whether to move the pointer, defaults false. - * @return {number} The peeked bits, as an unsigned number. - */ - bitjs.io.BitStream.prototype.peekBitsRtl = function(n, movePointers) { - if (n <= 0 || typeof n !== typeof 1) { - return 0; - } - - var movePointers = movePointers || false, - bytePtr = this.bytePtr, - bitPtr = this.bitPtr, - result = 0, - bytes = this.bytes; - - // keep going until we have no more bits left to peek at - // TODO: Consider putting all bits from bytes we will need into a variable and then - // shifting/masking it to just extract the bits we want. - // This could be considerably faster when reading more than 3 or 4 bits at a time. - while (n > 0) { - - if (bytePtr >= bytes.length) { - throw "Error! Overflowed the bit stream! n=" + n + ", bytePtr=" + bytePtr + ", bytes.length=" + - bytes.length + ", bitPtr=" + bitPtr; - // return -1; - } - - var numBitsLeftInThisByte = (8 - bitPtr); - if (n >= numBitsLeftInThisByte) { - result <<= numBitsLeftInThisByte; - result |= (BITMASK[numBitsLeftInThisByte] & bytes[bytePtr]); - bytePtr++; - bitPtr = 0; - n -= numBitsLeftInThisByte; - } else { - result <<= n; - result |= ((bytes[bytePtr] & (BITMASK[n] << (8 - n - bitPtr))) >> (8 - n - bitPtr)); - - bitPtr += n; - n = 0; - } - } - - if (movePointers) { - this.bitPtr = bitPtr; - this.bytePtr = bytePtr; - } - - return result; - }; - - - /** - * Peek at 16 bits from current position in the buffer. - * Bit at (bytePtr,bitPtr) has the highest position in returning data. - * Taken from getbits.hpp in unrar. - * TODO: Move this out of BitStream and into unrar. - */ - bitjs.io.BitStream.prototype.getBits = function() { - return (((((this.bytes[this.bytePtr] & 0xff) << 16) + - ((this.bytes[this.bytePtr + 1] & 0xff) << 8) + - ((this.bytes[this.bytePtr + 2] & 0xff))) >>> (8 - this.bitPtr)) & 0xffff); - }; - - - /** - * Reads n bits out of the stream, consuming them (moving the bit pointer). - * @param {number} n The number of bits to read. - * @return {number} The read bits, as an unsigned number. - */ - bitjs.io.BitStream.prototype.readBits = function(n) { - return this.peekBits(n, true); - }; - - - /** - * This returns n bytes as a sub-array, advancing the pointer if movePointers - * is true. Only use this for uncompressed blocks as this throws away remaining - * bits in the current byte. - * @param {number} n The number of bytes to peek. - * @param {boolean=} movePointers Whether to move the pointer, defaults false. - * @return {Uint8Array} The subarray. - */ - bitjs.io.BitStream.prototype.peekBytes = function(n, movePointers) { - if (n <= 0 || typeof n !== typeof 1) { - return 0; - } - - // from http://tools.ietf.org/html/rfc1951#page-11 - // "Any bits of input up to the next byte boundary are ignored." - while (this.bitPtr !== 0) { - this.readBits(1); - } - - var movePointers = movePointers || false; - var bytePtr = this.bytePtr; - // bitPtr = this.bitPtr; - - var result = this.bytes.subarray(bytePtr, bytePtr + n); - - if (movePointers) { - this.bytePtr += n; - } - - return result; - }; - - - /** - * @param {number} n The number of bytes to read. - * @return {Uint8Array} The subarray. - */ - bitjs.io.BitStream.prototype.readBytes = function(n) { - return this.peekBytes(n, true); - }; - -})(); diff --git a/cps/static/js/io/bytebuffer.js b/cps/static/js/io/bytebuffer.js deleted file mode 100644 index 2d55a76f..00000000 --- a/cps/static/js/io/bytebuffer.js +++ /dev/null @@ -1,124 +0,0 @@ -/* - * bytestream.js - * - * Provides a writer for bytes. - * - * Licensed under the MIT License - * - * Copyright(c) 2011 Google Inc. - * Copyright(c) 2011 antimatter15 - */ - -/* global bitjs, Uint8Array */ - -var bitjs = bitjs || {}; -bitjs.io = bitjs.io || {}; - -(function() { - - - /** - * A write-only Byte buffer which uses a Uint8 Typed Array as a backing store. - * @param {number} numBytes The number of bytes to allocate. - * @constructor - */ - bitjs.io.ByteBuffer = function(numBytes) { - if (typeof numBytes !== typeof 1 || numBytes <= 0) { - throw "Error! ByteBuffer initialized with '" + numBytes + "'"; - } - this.data = new Uint8Array(numBytes); - this.ptr = 0; - }; - - - /** - * @param {number} b The byte to insert. - */ - bitjs.io.ByteBuffer.prototype.insertByte = function(b) { - // TODO: throw if byte is invalid? - this.data[this.ptr++] = b; - }; - - - /** - * @param {Array.|Uint8Array|Int8Array} bytes The bytes to insert. - */ - bitjs.io.ByteBuffer.prototype.insertBytes = function(bytes) { - // TODO: throw if bytes is invalid? - this.data.set(bytes, this.ptr); - this.ptr += bytes.length; - }; - - - /** - * Writes an unsigned number into the next n bytes. If the number is too large - * to fit into n bytes or is negative, an error is thrown. - * @param {number} num The unsigned number to write. - * @param {number} numBytes The number of bytes to write the number into. - */ - bitjs.io.ByteBuffer.prototype.writeNumber = function(num, numBytes) { - if (numBytes < 1) { - throw "Trying to write into too few bytes: " + numBytes; - } - if (num < 0) { - throw "Trying to write a negative number (" + num + - ") as an unsigned number to an ArrayBuffer"; - } - if (num > (Math.pow(2, numBytes * 8) - 1)) { - throw "Trying to write " + num + " into only " + numBytes + " bytes"; - } - - // Roll 8-bits at a time into an array of bytes. - var bytes = []; - while (numBytes-- > 0) { - var eightBits = num & 255; - bytes.push(eightBits); - num >>= 8; - } - - this.insertBytes(bytes); - }; - - - /** - * Writes a signed number into the next n bytes. If the number is too large - * to fit into n bytes, an error is thrown. - * @param {number} num The signed number to write. - * @param {number} numBytes The number of bytes to write the number into. - */ - bitjs.io.ByteBuffer.prototype.writeSignedNumber = function(num, numBytes) { - if (numBytes < 1) { - throw "Trying to write into too few bytes: " + numBytes; - } - - var HALF = Math.pow(2, (numBytes * 8) - 1); - if (num >= HALF || num < -HALF) { - throw "Trying to write " + num + " into only " + numBytes + " bytes"; - } - - // Roll 8-bits at a time into an array of bytes. - var bytes = []; - while (numBytes-- > 0) { - var eightBits = num & 255; - bytes.push(eightBits); - num >>= 8; - } - - this.insertBytes(bytes); - }; - - - /** - * @param {string} str The ASCII string to write. - */ - bitjs.io.ByteBuffer.prototype.writeASCIIString = function(str) { - for (var i = 0; i < str.length; ++i) { - var curByte = str.charCodeAt(i); - if (curByte < 0 || curByte > 255) { - throw "Trying to write a non-ASCII string!"; - } - this.insertByte(curByte); - } - }; - -})(); diff --git a/cps/static/js/io/bytestream.js b/cps/static/js/io/bytestream.js deleted file mode 100644 index 9372f648..00000000 --- a/cps/static/js/io/bytestream.js +++ /dev/null @@ -1,195 +0,0 @@ -/* - * bytestream.js - * - * Provides readers for byte streams. - * - * Licensed under the MIT License - * - * Copyright(c) 2011 Google Inc. - * Copyright(c) 2011 antimatter15 - */ - -/* global bitjs, Uint8Array */ - -var bitjs = bitjs || {}; -bitjs.io = bitjs.io || {}; - -(function() { - - - /** - * This object allows you to peek and consume bytes as numbers and strings - * out of an ArrayBuffer. In this buffer, everything must be byte-aligned. - * - * @param {ArrayBuffer} ab The ArrayBuffer object. - * @param {number=} optOffset The offset into the ArrayBuffer - * @param {number=} optLength The length of this BitStream - * @constructor - */ - bitjs.io.ByteStream = function(ab, optOffset, optLength) { - var offset = optOffset || 0; - var length = optLength || ab.byteLength; - this.bytes = new Uint8Array(ab, offset, length); - this.ptr = 0; - }; - - - /** - * Peeks at the next n bytes as an unsigned number but does not advance the - * pointer - * TODO: This apparently cannot read more than 4 bytes as a number? - * @param {number} n The number of bytes to peek at. - * @return {number} The n bytes interpreted as an unsigned number. - */ - bitjs.io.ByteStream.prototype.peekNumber = function(n) { - // TODO: return error if n would go past the end of the stream? - if (n <= 0 || typeof n !== typeof 1) { - return -1; - } - - var result = 0; - // read from last byte to first byte and roll them in - var curByte = this.ptr + n - 1; - while (curByte >= this.ptr) { - result <<= 8; - result |= this.bytes[curByte]; - --curByte; - } - return result; - }; - - - /** - * Returns the next n bytes as an unsigned number (or -1 on error) - * and advances the stream pointer n bytes. - * @param {number} n The number of bytes to read. - * @return {number} The n bytes interpreted as an unsigned number. - */ - bitjs.io.ByteStream.prototype.readNumber = function(n) { - var num = this.peekNumber(n); - this.ptr += n; - return num; - }; - - - /** - * Returns the next n bytes as a signed number but does not advance the - * pointer. - * @param {number} n The number of bytes to read. - * @return {number} The bytes interpreted as a signed number. - */ - bitjs.io.ByteStream.prototype.peekSignedNumber = function(n) { - var num = this.peekNumber(n); - var HALF = Math.pow(2, (n * 8) - 1); - var FULL = HALF * 2; - - if (num >= HALF) num -= FULL; - - return num; - }; - - - /** - * Returns the next n bytes as a signed number and advances the stream pointer. - * @param {number} n The number of bytes to read. - * @return {number} The bytes interpreted as a signed number. - */ - bitjs.io.ByteStream.prototype.readSignedNumber = function(n) { - var num = this.peekSignedNumber(n); - this.ptr += n; - return num; - }; - - - /** - * ToDo: Returns the next n bytes as a signed number and advances the stream pointer. - * @param {number} n The number of bytes to read. - * @return {number} The bytes interpreted as a signed number. - */ - bitjs.io.ByteStream.prototype.movePointer = function(n) { - this.ptr += n; - // end of buffer reached - if ((this.bytes.byteLength - this.ptr) < 0 ) { - this.ptr = this.bytes.byteLength; - } - } - - /** - * ToDo: Returns the next n bytes as a signed number and advances the stream pointer. - * @param {number} n The number of bytes to read. - * @return {number} The bytes interpreted as a signed number. - */ - bitjs.io.ByteStream.prototype.moveTo = function(n) { - if ( n < 0 ) { - n = 0; - } - this.ptr = n; - // end of buffer reached - if ((this.bytes.byteLength - this.ptr) < 0 ) { - this.ptr = this.bytes.byteLength; - } - } - - /** - * This returns n bytes as a sub-array, advancing the pointer if movePointers - * is true. - * @param {number} n The number of bytes to read. - * @param {boolean} movePointers Whether to move the pointers. - * @return {Uint8Array} The subarray. - */ - bitjs.io.ByteStream.prototype.peekBytes = function(n, movePointers) { - if (n <= 0 || typeof n !== typeof 1) { - return null; - } - - var result = this.bytes.subarray(this.ptr, this.ptr + n); - - if (movePointers) { - this.ptr += n; - } - - return result; - }; - - - /** - * Reads the next n bytes as a sub-array. - * @param {number} n The number of bytes to read. - * @return {Uint8Array} The subarray. - */ - bitjs.io.ByteStream.prototype.readBytes = function(n) { - return this.peekBytes(n, true); - }; - - - /** - * Peeks at the next n bytes as a string but does not advance the pointer. - * @param {number} n The number of bytes to peek at. - * @return {string} The next n bytes as a string. - */ - bitjs.io.ByteStream.prototype.peekString = function(n) { - if (n <= 0 || typeof n !== typeof 1) { - return ""; - } - - var result = ""; - for (var p = this.ptr, end = this.ptr + n; p < end; ++p) { - result += String.fromCharCode(this.bytes[p]); - } - return result; - }; - - - /** - * Returns the next n bytes as an ASCII string and advances the stream pointer - * n bytes. - * @param {number} n The number of bytes to read. - * @return {string} The next n bytes as a string. - */ - bitjs.io.ByteStream.prototype.readString = function(n) { - var strToReturn = this.peekString(n); - this.ptr += n; - return strToReturn; - }; - -})(); diff --git a/cps/static/js/kthoom.js b/cps/static/js/kthoom.js index 56038fc6..76e6a2e4 100644 --- a/cps/static/js/kthoom.js +++ b/cps/static/js/kthoom.js @@ -15,7 +15,7 @@ * Typed Arrays: http://www.khronos.org/registry/typedarray/specs/latest/#6 */ -/* global screenfull, bitjs, Uint8Array, opera */ +/* global screenfull, bitjs, Uint8Array, opera, loadArchiveFormats, archiveOpenFile */ /* exported init, event */ @@ -69,7 +69,9 @@ var settings = { rotateTimes: 0, fitMode: kthoom.Key.B, theme: "light", - direction: 0 // 0 = Left to Right, 1 = Right to Left + direction: 0, // 0 = Left to Right, 1 = Right to Left + nextPage: 0, // 0 = Reset to Top, 1 = Remember Position + scrollbar: 1 // 0 = Hide Scrollbar, 1 = Show Scrollbar }; kthoom.saveSettings = function() { @@ -102,9 +104,8 @@ kthoom.setSettings = function() { }; var createURLFromArray = function(array, mimeType) { - var offset = array.byteOffset; + var offset = 0; // array.byteOffset; var len = array.byteLength; - // var url; var blob; if (mimeType === "image/xml+svg") { @@ -164,90 +165,61 @@ kthoom.ImageFile = function(file) { } if ( this.mimeType !== undefined) { this.dataURI = createURLFromArray(file.fileData, this.mimeType); - this.data = file; } }; - function initProgressClick() { $("#progress").click(function(e) { - var page = Math.max(1, Math.ceil((e.offsetX / $(this).width()) * totalImages)) - 1; - currentImage = page; + var offset = $(this).offset(); + var x = e.pageX - offset.left; + var rate = settings.direction === 0 ? x / $(this).width() : 1 - x / $(this).width(); + currentImage = Math.max(1, Math.ceil(rate * totalImages)) - 1; updatePage(); }); } function loadFromArrayBuffer(ab) { - var start = (new Date).getTime(); - var h = new Uint8Array(ab, 0, 10); - var pathToBitJS = "../../static/js/archive/"; var lastCompletion = 0; - if (h[0] === 0x52 && h[1] === 0x61 && h[2] === 0x72 && h[3] === 0x21) { //Rar! - unarchiver = new bitjs.archive.Unrarrer(ab, pathToBitJS); - } else if (h[0] === 80 && h[1] === 75) { //PK (Zip) - unarchiver = new bitjs.archive.Unzipper(ab, pathToBitJS); - } else if (h[0] === 255 && h[1] === 216) { // JPEG - // ToDo: check - updateProgress(100); - lastCompletion = 100; - return; - } else { // Try with tar - unarchiver = new bitjs.archive.Untarrer(ab, pathToBitJS); - } - // Listen for UnarchiveEvents. - if (unarchiver) { - unarchiver.addEventListener(bitjs.archive.UnarchiveEvent.Type.PROGRESS, - function(e) { - var percentage = e.currentBytesUnarchived / e.totalUncompressedBytesInArchive; - if (totalImages === 0) { - totalImages = e.totalFilesInArchive; - } - updateProgress(percentage * 100); - lastCompletion = percentage * 100; - }); - unarchiver.addEventListener(bitjs.archive.UnarchiveEvent.Type.INFO, - function(e) { - // console.log(e.msg); // Enable debug output here - }); - unarchiver.addEventListener(bitjs.archive.UnarchiveEvent.Type.EXTRACT, - function(e) { - // convert DecompressedFile into a bunch of ImageFiles - if (e.unarchivedFile) { - var f = e.unarchivedFile; - // add any new pages based on the filename - if (imageFilenames.indexOf(f.filename) === -1) { - var test = new kthoom.ImageFile(f); - if ( test.mimeType !== undefined) { - imageFilenames.push(f.filename); - imageFiles.push(test); - // add thumbnails to the TOC list - $("#thumbnails").append( - "
  • " + - "" + + loadArchiveFormats(['rar', 'zip', 'tar'], function() { + // Open the file as an archive + archiveOpenFile(ab, function (archive) { + if (archive) { + totalImages = archive.entries.length + console.info('Uncompressing ' + archive.archive_type + ' ...'); + archive.entries.forEach(function(e, i) { + updateProgress( (i + 1)/ totalImages * 100); + if (e.is_file) { + e.readData(function(d) { + // add any new pages based on the filename + if (imageFilenames.indexOf(e.name) === -1) { + let data = {filename: e.name, fileData: d}; + var test = new kthoom.ImageFile(data); + if (test.mimeType !== undefined) { + imageFilenames.push(e.name); + imageFiles.push(test); + // add thumbnails to the TOC list + $("#thumbnails").append( + "
  • " + + "" + "" + "" + imageFiles.length + "" + - "" + - "
  • " - ); - // display first page if we haven't yet - if (imageFiles.length === currentImage + 1) { - updatePage(lastCompletion); + "" + + "" + ); + // display first page if we haven't yet + if (imageFiles.length === currentImage + 1) { + updatePage(lastCompletion); + } + } else { + totalImages--; + } } - } else { - totalImages--; - } + }); } - } - }); - unarchiver.addEventListener(bitjs.archive.UnarchiveEvent.Type.FINISH, - function() { - var diff = ((new Date).getTime() - start) / 1000; - console.log("Unarchiving done in " + diff + "s"); - }); - unarchiver.start(); - } else { - alert("Some error"); - } + }); + } + }); + }); } function scrollTocToActive() { @@ -279,12 +251,29 @@ function updatePage() { } $("body").toggleClass("dark-theme", settings.theme === "dark"); + $("#mainContent").toggleClass("disabled-scrollbar", settings.scrollbar === 0); kthoom.setSettings(); kthoom.saveSettings(); } function updateProgress(loadPercentage) { + if (settings.direction === 0) { + $("#progress .bar-read") + .removeClass("from-right") + .addClass("from-left"); + $("#progress .bar-load") + .removeClass("from-right") + .addClass("from-left"); + } else { + $("#progress .bar-read") + .removeClass("from-left") + .addClass("from-right"); + $("#progress .bar-load") + .removeClass("from-left") + .addClass("from-right"); + } + // Set the load/unzip progress if it's passed in if (loadPercentage) { $("#progress .bar-load").css({ width: loadPercentage + "%" }); @@ -295,7 +284,6 @@ function updateProgress(loadPercentage) { .find(".load").text(""); } } - // Set page progress bar $("#progress .bar-read").css({ width: totalImages === 0 ? 0 : Math.round((currentImage + 1) / totalImages * 100) + "%"}); } @@ -420,6 +408,9 @@ function showPrevPage() { currentImage++; } else { updatePage(); + if (settings.nextPage === 0) { + $("#mainContent").scrollTop(0); + } } } @@ -430,6 +421,9 @@ function showNextPage() { currentImage--; } else { updatePage(); + if (settings.nextPage === 0) { + $("#mainContent").scrollTop(0); + } } } @@ -525,19 +519,14 @@ function keyHandler(evt) { updateScale(false); break; case kthoom.Key.SPACE: - var container = $("#mainContent"); - var atTop = container.scrollTop() === 0; - var atBottom = container.scrollTop() >= container[0].scrollHeight - container.height(); - - if (evt.shiftKey && atTop) { + if (evt.shiftKey) { evt.preventDefault(); // If it's Shift + Space and the container is at the top of the page - showLeftPage(); - } else if (!evt.shiftKey && atBottom) { + showPrevPage(); + } else { evt.preventDefault(); // If you're at the bottom of the page and you only pressed space - showRightPage(); - container.scrollTop(0); + showNextPage(); } break; default: @@ -546,33 +535,11 @@ function keyHandler(evt) { } } -/*function ImageLoadCallback() { - var jso = this.response; - // Unable to decompress file, or no response from server - if (jso === null) { - setImage("error"); - } else { - // IE 11 sometimes sees the response as a string - if (typeof jso !== "object") { - jso = JSON.parse(jso); - } - - if (jso.page !== jso.last) { - this.open("GET", this.fileid + "/" + (jso.page + 1)); - this.addEventListener("load", ImageLoadCallback); - this.send(); - } - - loadFromArrayBuffer(jso); - } -}*/ function init(filename) { var request = new XMLHttpRequest(); request.open("GET", filename); request.responseType = "arraybuffer"; - request.setRequestHeader("X-Test", "test1"); - request.setRequestHeader("X-Test", "test2"); - request.addEventListener("load", function(event) { + request.addEventListener("load", function() { if (request.status >= 200 && request.status < 300) { loadFromArrayBuffer(request.response); } else { @@ -632,6 +599,9 @@ function init(filename) { $("#thumbnails").on("click", "a", function() { currentImage = $(this).data("page") - 1; updatePage(); + if (settings.nextPage === 0) { + $("#mainContent").scrollTop(0); + } }); // Fullscreen mode diff --git a/cps/static/js/libs/Sortable.min.js b/cps/static/js/libs/Sortable.min.js index eba06149..4fe7f0c3 100644 --- a/cps/static/js/libs/Sortable.min.js +++ b/cps/static/js/libs/Sortable.min.js @@ -1,2 +1,2 @@ -/*! Sortable 1.10.2 - MIT | git://github.com/SortableJS/Sortable.git */ -!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t=t||self).Sortable=e()}(this,function(){"use strict";function o(t){return(o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function a(){return(a=Object.assign||function(t){for(var e=1;e"===e[0]&&(e=e.substring(1)),t)try{if(t.matches)return t.matches(e);if(t.msMatchesSelector)return t.msMatchesSelector(e);if(t.webkitMatchesSelector)return t.webkitMatchesSelector(e)}catch(t){return!1}return!1}}function P(t,e,n,o){if(t){n=n||document;do{if(null!=e&&(">"===e[0]?t.parentNode===n&&h(t,e):h(t,e))||o&&t===n)return t;if(t===n)break}while(t=(i=t).host&&i!==document&&i.host.nodeType?i.host:i.parentNode)}var i;return null}var f,p=/\s+/g;function k(t,e,n){if(t&&e)if(t.classList)t.classList[n?"add":"remove"](e);else{var o=(" "+t.className+" ").replace(p," ").replace(" "+e+" "," ");t.className=(o+(n?" "+e:"")).replace(p," ")}}function R(t,e,n){var o=t&&t.style;if(o){if(void 0===n)return document.defaultView&&document.defaultView.getComputedStyle?n=document.defaultView.getComputedStyle(t,""):t.currentStyle&&(n=t.currentStyle),void 0===e?n:n[e];e in o||-1!==e.indexOf("webkit")||(e="-webkit-"+e),o[e]=n+("string"==typeof n?"":"px")}}function v(t,e){var n="";if("string"==typeof t)n=t;else do{var o=R(t,"transform");o&&"none"!==o&&(n=o+" "+n)}while(!e&&(t=t.parentNode));var i=window.DOMMatrix||window.WebKitCSSMatrix||window.CSSMatrix||window.MSCSSMatrix;return i&&new i(n)}function g(t,e,n){if(t){var o=t.getElementsByTagName(e),i=0,r=o.length;if(n)for(;i=e.left-n&&r<=e.right+n,i=a>=e.top-n&&a<=e.bottom+n;return n&&o&&i?l=t:void 0}}),l}((t=t.touches?t.touches[0]:t).clientX,t.clientY);if(e){var n={};for(var o in t)t.hasOwnProperty(o)&&(n[o]=t[o]);n.target=n.rootEl=e,n.preventDefault=void 0,n.stopPropagation=void 0,e[j]._onDragOver(n)}}}function kt(t){z&&z.parentNode[j]._isOutsideThisEl(t.target)}function Rt(t,e){if(!t||!t.nodeType||1!==t.nodeType)throw"Sortable: `el` must be an HTMLElement, not ".concat({}.toString.call(t));this.el=t,this.options=e=a({},e),t[j]=this;var n={group:null,sort:!0,disabled:!1,store:null,handle:null,draggable:/^[uo]l$/i.test(t.nodeName)?">li":">*",swapThreshold:1,invertSwap:!1,invertedSwapThreshold:null,removeCloneOnHide:!0,direction:function(){return Ot(t,this.options)},ghostClass:"sortable-ghost",chosenClass:"sortable-chosen",dragClass:"sortable-drag",ignore:"a, img",filter:null,preventOnFilter:!0,animation:0,easing:null,setData:function(t,e){t.setData("Text",e.textContent)},dropBubble:!1,dragoverBubble:!1,dataIdAttr:"data-id",delay:0,delayOnTouchOnly:!1,touchStartThreshold:(Number.parseInt?Number:window).parseInt(window.devicePixelRatio,10)||1,forceFallback:!1,fallbackClass:"sortable-fallback",fallbackOnBody:!1,fallbackTolerance:0,fallbackOffset:{x:0,y:0},supportPointer:!1!==Rt.supportPointer&&"PointerEvent"in window,emptyInsertThreshold:5};for(var o in O.initializePlugins(this,t,n),n)o in e||(e[o]=n[o]);for(var i in At(e),this)"_"===i.charAt(0)&&"function"==typeof this[i]&&(this[i]=this[i].bind(this));this.nativeDraggable=!e.forceFallback&&xt,this.nativeDraggable&&(this.options.touchStartThreshold=1),e.supportPointer?u(t,"pointerdown",this._onTapStart):(u(t,"mousedown",this._onTapStart),u(t,"touchstart",this._onTapStart)),this.nativeDraggable&&(u(t,"dragover",this),u(t,"dragenter",this)),bt.push(this.el),e.store&&e.store.get&&this.sort(e.store.get(this)||[]),a(this,T())}function Xt(t,e,n,o,i,r,a,l){var s,c,u=t[j],d=u.options.onMove;return!window.CustomEvent||w||E?(s=document.createEvent("Event")).initEvent("move",!0,!0):s=new CustomEvent("move",{bubbles:!0,cancelable:!0}),s.to=e,s.from=t,s.dragged=n,s.draggedRect=o,s.related=i||e,s.relatedRect=r||X(e),s.willInsertAfter=l,s.originalEvent=a,t.dispatchEvent(s),d&&(c=d.call(u,s,a)),c}function Yt(t){t.draggable=!1}function Bt(){Dt=!1}function Ft(t){for(var e=t.tagName+t.className+t.src+t.href+t.textContent,n=e.length,o=0;n--;)o+=e.charCodeAt(n);return o.toString(36)}function Ht(t){return setTimeout(t,0)}function Lt(t){return clearTimeout(t)}Rt.prototype={constructor:Rt,_isOutsideThisEl:function(t){this.el.contains(t)||t===this.el||(ht=null)},_getDirection:function(t,e){return"function"==typeof this.options.direction?this.options.direction.call(this,t,e,z):this.options.direction},_onTapStart:function(e){if(e.cancelable){var n=this,o=this.el,t=this.options,i=t.preventOnFilter,r=e.type,a=e.touches&&e.touches[0]||e.pointerType&&"touch"===e.pointerType&&e,l=(a||e).target,s=e.target.shadowRoot&&(e.path&&e.path[0]||e.composedPath&&e.composedPath()[0])||l,c=t.filter;if(function(t){St.length=0;var e=t.getElementsByTagName("input"),n=e.length;for(;n--;){var o=e[n];o.checked&&St.push(o)}}(o),!z&&!(/mousedown|pointerdown/.test(r)&&0!==e.button||t.disabled||s.isContentEditable||(l=P(l,t.draggable,o,!1))&&l.animated||Z===l)){if(J=F(l),et=F(l,t.draggable),"function"==typeof c){if(c.call(this,e,l,this))return W({sortable:n,rootEl:s,name:"filter",targetEl:l,toEl:o,fromEl:o}),K("filter",n,{evt:e}),void(i&&e.cancelable&&e.preventDefault())}else if(c&&(c=c.split(",").some(function(t){if(t=P(s,t.trim(),o,!1))return W({sortable:n,rootEl:t,name:"filter",targetEl:l,fromEl:o,toEl:o}),K("filter",n,{evt:e}),!0})))return void(i&&e.cancelable&&e.preventDefault());t.handle&&!P(s,t.handle,o,!1)||this._prepareDragStart(e,a,l)}}},_prepareDragStart:function(t,e,n){var o,i=this,r=i.el,a=i.options,l=r.ownerDocument;if(n&&!z&&n.parentNode===r){var s=X(n);if(q=r,G=(z=n).parentNode,V=z.nextSibling,Z=n,ot=a.group,rt={target:Rt.dragged=z,clientX:(e||t).clientX,clientY:(e||t).clientY},ct=rt.clientX-s.left,ut=rt.clientY-s.top,this._lastX=(e||t).clientX,this._lastY=(e||t).clientY,z.style["will-change"]="all",o=function(){K("delayEnded",i,{evt:t}),Rt.eventCanceled?i._onDrop():(i._disableDelayedDragEvents(),!c&&i.nativeDraggable&&(z.draggable=!0),i._triggerDragStart(t,e),W({sortable:i,name:"choose",originalEvent:t}),k(z,a.chosenClass,!0))},a.ignore.split(",").forEach(function(t){g(z,t.trim(),Yt)}),u(l,"dragover",Pt),u(l,"mousemove",Pt),u(l,"touchmove",Pt),u(l,"mouseup",i._onDrop),u(l,"touchend",i._onDrop),u(l,"touchcancel",i._onDrop),c&&this.nativeDraggable&&(this.options.touchStartThreshold=4,z.draggable=!0),K("delayStart",this,{evt:t}),!a.delay||a.delayOnTouchOnly&&!e||this.nativeDraggable&&(E||w))o();else{if(Rt.eventCanceled)return void this._onDrop();u(l,"mouseup",i._disableDelayedDrag),u(l,"touchend",i._disableDelayedDrag),u(l,"touchcancel",i._disableDelayedDrag),u(l,"mousemove",i._delayedDragTouchMoveHandler),u(l,"touchmove",i._delayedDragTouchMoveHandler),a.supportPointer&&u(l,"pointermove",i._delayedDragTouchMoveHandler),i._dragStartTimer=setTimeout(o,a.delay)}}},_delayedDragTouchMoveHandler:function(t){var e=t.touches?t.touches[0]:t;Math.max(Math.abs(e.clientX-this._lastX),Math.abs(e.clientY-this._lastY))>=Math.floor(this.options.touchStartThreshold/(this.nativeDraggable&&window.devicePixelRatio||1))&&this._disableDelayedDrag()},_disableDelayedDrag:function(){z&&Yt(z),clearTimeout(this._dragStartTimer),this._disableDelayedDragEvents()},_disableDelayedDragEvents:function(){var t=this.el.ownerDocument;d(t,"mouseup",this._disableDelayedDrag),d(t,"touchend",this._disableDelayedDrag),d(t,"touchcancel",this._disableDelayedDrag),d(t,"mousemove",this._delayedDragTouchMoveHandler),d(t,"touchmove",this._delayedDragTouchMoveHandler),d(t,"pointermove",this._delayedDragTouchMoveHandler)},_triggerDragStart:function(t,e){e=e||"touch"==t.pointerType&&t,!this.nativeDraggable||e?this.options.supportPointer?u(document,"pointermove",this._onTouchMove):u(document,e?"touchmove":"mousemove",this._onTouchMove):(u(z,"dragend",this),u(q,"dragstart",this._onDragStart));try{document.selection?Ht(function(){document.selection.empty()}):window.getSelection().removeAllRanges()}catch(t){}},_dragStarted:function(t,e){if(vt=!1,q&&z){K("dragStarted",this,{evt:e}),this.nativeDraggable&&u(document,"dragover",kt);var n=this.options;t||k(z,n.dragClass,!1),k(z,n.ghostClass,!0),Rt.active=this,t&&this._appendGhost(),W({sortable:this,name:"start",originalEvent:e})}else this._nulling()},_emulateDragOver:function(){if(at){this._lastX=at.clientX,this._lastY=at.clientY,Nt();for(var t=document.elementFromPoint(at.clientX,at.clientY),e=t;t&&t.shadowRoot&&(t=t.shadowRoot.elementFromPoint(at.clientX,at.clientY))!==e;)e=t;if(z.parentNode[j]._isOutsideThisEl(t),e)do{if(e[j]){if(e[j]._onDragOver({clientX:at.clientX,clientY:at.clientY,target:t,rootEl:e})&&!this.options.dragoverBubble)break}t=e}while(e=e.parentNode);It()}},_onTouchMove:function(t){if(rt){var e=this.options,n=e.fallbackTolerance,o=e.fallbackOffset,i=t.touches?t.touches[0]:t,r=U&&v(U,!0),a=U&&r&&r.a,l=U&&r&&r.d,s=Ct&>&&b(gt),c=(i.clientX-rt.clientX+o.x)/(a||1)+(s?s[0]-Et[0]:0)/(a||1),u=(i.clientY-rt.clientY+o.y)/(l||1)+(s?s[1]-Et[1]:0)/(l||1);if(!Rt.active&&!vt){if(n&&Math.max(Math.abs(i.clientX-this._lastX),Math.abs(i.clientY-this._lastY))o.right+10||t.clientX<=o.right&&t.clientY>o.bottom&&t.clientX>=o.left:t.clientX>o.right&&t.clientY>o.top||t.clientX<=o.right&&t.clientY>o.bottom+10}(n,a,this)&&!g.animated){if(g===z)return A(!1);if(g&&l===n.target&&(s=g),s&&(i=X(s)),!1!==Xt(q,l,z,o,s,i,n,!!s))return O(),l.appendChild(z),G=l,N(),A(!0)}else if(s.parentNode===l){i=X(s);var v,m,b,y=z.parentNode!==l,w=!function(t,e,n){var o=n?t.left:t.top,i=n?t.right:t.bottom,r=n?t.width:t.height,a=n?e.left:e.top,l=n?e.right:e.bottom,s=n?e.width:e.height;return o===a||i===l||o+r/2===a+s/2}(z.animated&&z.toRect||o,s.animated&&s.toRect||i,a),E=a?"top":"left",D=Y(s,"top","top")||Y(z,"top","top"),S=D?D.scrollTop:void 0;if(ht!==s&&(m=i[E],yt=!1,wt=!w&&e.invertSwap||y),0!==(v=function(t,e,n,o,i,r,a,l){var s=o?t.clientY:t.clientX,c=o?n.height:n.width,u=o?n.top:n.left,d=o?n.bottom:n.right,h=!1;if(!a)if(l&&pt"===e[0]&&(e=e.substring(1)),t)try{if(t.matches)return t.matches(e);if(t.msMatchesSelector)return t.msMatchesSelector(e);if(t.webkitMatchesSelector)return t.webkitMatchesSelector(e)}catch(t){return!1}return!1}}function P(t,e,n,o){if(t){n=n||document;do{if(null!=e&&(">"===e[0]?t.parentNode===n&&h(t,e):h(t,e))||o&&t===n)return t;if(t===n)break}while(t=(i=t).host&&i!==document&&i.host.nodeType?i.host:i.parentNode)}var i;return null}var f,p=/\s+/g;function k(t,e,n){if(t&&e)if(t.classList)t.classList[n?"add":"remove"](e);else{var o=(" "+t.className+" ").replace(p," ").replace(" "+e+" "," ");t.className=(o+(n?" "+e:"")).replace(p," ")}}function R(t,e,n){var o=t&&t.style;if(o){if(void 0===n)return document.defaultView&&document.defaultView.getComputedStyle?n=document.defaultView.getComputedStyle(t,""):t.currentStyle&&(n=t.currentStyle),void 0===e?n:n[e];e in o||-1!==e.indexOf("webkit")||(e="-webkit-"+e),o[e]=n+("string"==typeof n?"":"px")}}function v(t,e){var n="";if("string"==typeof t)n=t;else do{var o=R(t,"transform");o&&"none"!==o&&(n=o+" "+n)}while(!e&&(t=t.parentNode));var i=window.DOMMatrix||window.WebKitCSSMatrix||window.CSSMatrix||window.MSCSSMatrix;return i&&new i(n)}function g(t,e,n){if(t){var o=t.getElementsByTagName(e),i=0,r=o.length;if(n)for(;i=e.left-n&&r<=e.right+n,i=a>=e.top-n&&a<=e.bottom+n;return n&&o&&i?l=t:void 0}}),l}((t=t.touches?t.touches[0]:t).clientX,t.clientY);if(e){var n={};for(var o in t)t.hasOwnProperty(o)&&(n[o]=t[o]);n.target=n.rootEl=e,n.preventDefault=void 0,n.stopPropagation=void 0,e[j]._onDragOver(n)}}}function kt(t){z&&z.parentNode[j]._isOutsideThisEl(t.target)}function Rt(t,e){if(!t||!t.nodeType||1!==t.nodeType)throw"Sortable: `el` must be an HTMLElement, not ".concat({}.toString.call(t));this.el=t,this.options=e=a({},e),t[j]=this;var n={group:null,sort:!0,disabled:!1,store:null,handle:null,draggable:/^[uo]l$/i.test(t.nodeName)?">li":">*",swapThreshold:1,invertSwap:!1,invertedSwapThreshold:null,removeCloneOnHide:!0,direction:function(){return Ot(t,this.options)},ghostClass:"sortable-ghost",chosenClass:"sortable-chosen",dragClass:"sortable-drag",ignore:"a, img",filter:null,preventOnFilter:!0,animation:0,easing:null,setData:function(t,e){t.setData("Text",e.textContent)},dropBubble:!1,dragoverBubble:!1,dataIdAttr:"data-id",delay:0,delayOnTouchOnly:!1,touchStartThreshold:(Number.parseInt?Number:window).parseInt(window.devicePixelRatio,10)||1,forceFallback:!1,fallbackClass:"sortable-fallback",fallbackOnBody:!1,fallbackTolerance:0,fallbackOffset:{x:0,y:0},supportPointer:!1!==Rt.supportPointer&&"PointerEvent"in window&&!u,emptyInsertThreshold:5};for(var o in O.initializePlugins(this,t,n),n)o in e||(e[o]=n[o]);for(var i in Nt(e),this)"_"===i.charAt(0)&&"function"==typeof this[i]&&(this[i]=this[i].bind(this));this.nativeDraggable=!e.forceFallback&&xt,this.nativeDraggable&&(this.options.touchStartThreshold=1),e.supportPointer?d(t,"pointerdown",this._onTapStart):(d(t,"mousedown",this._onTapStart),d(t,"touchstart",this._onTapStart)),this.nativeDraggable&&(d(t,"dragover",this),d(t,"dragenter",this)),bt.push(this.el),e.store&&e.store.get&&this.sort(e.store.get(this)||[]),a(this,T())}function Xt(t,e,n,o,i,r,a,l){var s,c,u=t[j],d=u.options.onMove;return!window.CustomEvent||w||E?(s=document.createEvent("Event")).initEvent("move",!0,!0):s=new CustomEvent("move",{bubbles:!0,cancelable:!0}),s.to=e,s.from=t,s.dragged=n,s.draggedRect=o,s.related=i||e,s.relatedRect=r||X(e),s.willInsertAfter=l,s.originalEvent=a,t.dispatchEvent(s),d&&(c=d.call(u,s,a)),c}function Yt(t){t.draggable=!1}function Bt(){Dt=!1}function Ft(t){for(var e=t.tagName+t.className+t.src+t.href+t.textContent,n=e.length,o=0;n--;)o+=e.charCodeAt(n);return o.toString(36)}function Ht(t){return setTimeout(t,0)}function Lt(t){return clearTimeout(t)}Rt.prototype={constructor:Rt,_isOutsideThisEl:function(t){this.el.contains(t)||t===this.el||(ht=null)},_getDirection:function(t,e){return"function"==typeof this.options.direction?this.options.direction.call(this,t,e,z):this.options.direction},_onTapStart:function(e){if(e.cancelable){var n=this,o=this.el,t=this.options,i=t.preventOnFilter,r=e.type,a=e.touches&&e.touches[0]||e.pointerType&&"touch"===e.pointerType&&e,l=(a||e).target,s=e.target.shadowRoot&&(e.path&&e.path[0]||e.composedPath&&e.composedPath()[0])||l,c=t.filter;if(function(t){St.length=0;var e=t.getElementsByTagName("input"),n=e.length;for(;n--;){var o=e[n];o.checked&&St.push(o)}}(o),!z&&!(/mousedown|pointerdown/.test(r)&&0!==e.button||t.disabled)&&!s.isContentEditable&&(this.nativeDraggable||!u||!l||"SELECT"!==l.tagName.toUpperCase())&&!((l=P(l,t.draggable,o,!1))&&l.animated||Z===l)){if(J=F(l),et=F(l,t.draggable),"function"==typeof c){if(c.call(this,e,l,this))return W({sortable:n,rootEl:s,name:"filter",targetEl:l,toEl:o,fromEl:o}),K("filter",n,{evt:e}),void(i&&e.cancelable&&e.preventDefault())}else if(c&&(c=c.split(",").some(function(t){if(t=P(s,t.trim(),o,!1))return W({sortable:n,rootEl:t,name:"filter",targetEl:l,fromEl:o,toEl:o}),K("filter",n,{evt:e}),!0})))return void(i&&e.cancelable&&e.preventDefault());t.handle&&!P(s,t.handle,o,!1)||this._prepareDragStart(e,a,l)}}},_prepareDragStart:function(t,e,n){var o,i=this,r=i.el,a=i.options,l=r.ownerDocument;if(n&&!z&&n.parentNode===r){var s=X(n);if(q=r,G=(z=n).parentNode,V=z.nextSibling,Z=n,ot=a.group,rt={target:Rt.dragged=z,clientX:(e||t).clientX,clientY:(e||t).clientY},ct=rt.clientX-s.left,ut=rt.clientY-s.top,this._lastX=(e||t).clientX,this._lastY=(e||t).clientY,z.style["will-change"]="all",o=function(){K("delayEnded",i,{evt:t}),Rt.eventCanceled?i._onDrop():(i._disableDelayedDragEvents(),!c&&i.nativeDraggable&&(z.draggable=!0),i._triggerDragStart(t,e),W({sortable:i,name:"choose",originalEvent:t}),k(z,a.chosenClass,!0))},a.ignore.split(",").forEach(function(t){g(z,t.trim(),Yt)}),d(l,"dragover",Pt),d(l,"mousemove",Pt),d(l,"touchmove",Pt),d(l,"mouseup",i._onDrop),d(l,"touchend",i._onDrop),d(l,"touchcancel",i._onDrop),c&&this.nativeDraggable&&(this.options.touchStartThreshold=4,z.draggable=!0),K("delayStart",this,{evt:t}),!a.delay||a.delayOnTouchOnly&&!e||this.nativeDraggable&&(E||w))o();else{if(Rt.eventCanceled)return void this._onDrop();d(l,"mouseup",i._disableDelayedDrag),d(l,"touchend",i._disableDelayedDrag),d(l,"touchcancel",i._disableDelayedDrag),d(l,"mousemove",i._delayedDragTouchMoveHandler),d(l,"touchmove",i._delayedDragTouchMoveHandler),a.supportPointer&&d(l,"pointermove",i._delayedDragTouchMoveHandler),i._dragStartTimer=setTimeout(o,a.delay)}}},_delayedDragTouchMoveHandler:function(t){var e=t.touches?t.touches[0]:t;Math.max(Math.abs(e.clientX-this._lastX),Math.abs(e.clientY-this._lastY))>=Math.floor(this.options.touchStartThreshold/(this.nativeDraggable&&window.devicePixelRatio||1))&&this._disableDelayedDrag()},_disableDelayedDrag:function(){z&&Yt(z),clearTimeout(this._dragStartTimer),this._disableDelayedDragEvents()},_disableDelayedDragEvents:function(){var t=this.el.ownerDocument;s(t,"mouseup",this._disableDelayedDrag),s(t,"touchend",this._disableDelayedDrag),s(t,"touchcancel",this._disableDelayedDrag),s(t,"mousemove",this._delayedDragTouchMoveHandler),s(t,"touchmove",this._delayedDragTouchMoveHandler),s(t,"pointermove",this._delayedDragTouchMoveHandler)},_triggerDragStart:function(t,e){e=e||"touch"==t.pointerType&&t,!this.nativeDraggable||e?this.options.supportPointer?d(document,"pointermove",this._onTouchMove):d(document,e?"touchmove":"mousemove",this._onTouchMove):(d(z,"dragend",this),d(q,"dragstart",this._onDragStart));try{document.selection?Ht(function(){document.selection.empty()}):window.getSelection().removeAllRanges()}catch(t){}},_dragStarted:function(t,e){if(vt=!1,q&&z){K("dragStarted",this,{evt:e}),this.nativeDraggable&&d(document,"dragover",kt);var n=this.options;t||k(z,n.dragClass,!1),k(z,n.ghostClass,!0),Rt.active=this,t&&this._appendGhost(),W({sortable:this,name:"start",originalEvent:e})}else this._nulling()},_emulateDragOver:function(){if(at){this._lastX=at.clientX,this._lastY=at.clientY,At();for(var t=document.elementFromPoint(at.clientX,at.clientY),e=t;t&&t.shadowRoot&&(t=t.shadowRoot.elementFromPoint(at.clientX,at.clientY))!==e;)e=t;if(z.parentNode[j]._isOutsideThisEl(t),e)do{if(e[j]){if(e[j]._onDragOver({clientX:at.clientX,clientY:at.clientY,target:t,rootEl:e})&&!this.options.dragoverBubble)break}t=e}while(e=e.parentNode);It()}},_onTouchMove:function(t){if(rt){var e=this.options,n=e.fallbackTolerance,o=e.fallbackOffset,i=t.touches?t.touches[0]:t,r=U&&v(U,!0),a=U&&r&&r.a,l=U&&r&&r.d,s=Ct&>&&b(gt),c=(i.clientX-rt.clientX+o.x)/(a||1)+(s?s[0]-Et[0]:0)/(a||1),u=(i.clientY-rt.clientY+o.y)/(l||1)+(s?s[1]-Et[1]:0)/(l||1);if(!Rt.active&&!vt){if(n&&Math.max(Math.abs(i.clientX-this._lastX),Math.abs(i.clientY-this._lastY))o.right+10||t.clientX<=o.right&&t.clientY>o.bottom&&t.clientX>=o.left:t.clientX>o.right&&t.clientY>o.top||t.clientX<=o.right&&t.clientY>o.bottom+10}(n,a,this)&&!g.animated){if(g===z)return N(!1);if(g&&l===n.target&&(s=g),s&&(i=X(s)),!1!==Xt(q,l,z,o,s,i,n,!!s))return O(),l.appendChild(z),G=l,A(),N(!0)}else if(s.parentNode===l){i=X(s);var v,m,b,y=z.parentNode!==l,w=!function(t,e,n){var o=n?t.left:t.top,i=n?t.right:t.bottom,r=n?t.width:t.height,a=n?e.left:e.top,l=n?e.right:e.bottom,s=n?e.width:e.height;return o===a||i===l||o+r/2===a+s/2}(z.animated&&z.toRect||o,s.animated&&s.toRect||i,a),E=a?"top":"left",D=Y(s,"top","top")||Y(z,"top","top"),S=D?D.scrollTop:void 0;if(ht!==s&&(m=i[E],yt=!1,wt=!w&&e.invertSwap||y),0!==(v=function(t,e,n,o,i,r,a,l){var s=o?t.clientY:t.clientX,c=o?n.height:n.width,u=o?n.top:n.left,d=o?n.bottom:n.right,h=!1;if(!a)if(l&&pt Modified to eliminate some bugs by Ozzieisaacs (don't update without appling patches again) * @homepage https://bootstrap-table.com * @author wenzhixin (http://wenzhixin.net.cn/) * @license MIT */ -!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(require("jquery")):"function"==typeof define&&define.amd?define(["jquery"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).jQuery)}(this,(function(t){"use strict";function e(t){return t&&"object"==typeof t&&"default"in t?t:{default:t}}var n=e(t),r="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{};function o(t,e){return t(e={exports:{}},e.exports),e.exports}var i=function(t){return t&&t.Math==Math&&t},a=i("object"==typeof globalThis&&globalThis)||i("object"==typeof window&&window)||i("object"==typeof self&&self)||i("object"==typeof r&&r)||function(){return this}()||Function("return this")(),u=function(t){try{return!!t()}catch(t){return!0}},c=!u((function(){return 7!=Object.defineProperty({},1,{get:function(){return 7}})[1]})),f={}.propertyIsEnumerable,l=Object.getOwnPropertyDescriptor,s={f:l&&!f.call({1:2},1)?function(t){var e=l(this,t);return!!e&&e.enumerable}:f},d=function(t,e){return{enumerable:!(1&t),configurable:!(2&t),writable:!(4&t),value:e}},p={}.toString,v=function(t){return p.call(t).slice(8,-1)},h="".split,y=u((function(){return!Object("z").propertyIsEnumerable(0)}))?function(t){return"String"==v(t)?h.call(t,""):Object(t)}:Object,b=function(t){if(null==t)throw TypeError("Can't call method on "+t);return t},g=function(t){return y(b(t))},m=function(t){return"object"==typeof t?null!==t:"function"==typeof t},x=function(t,e){if(!m(t))return t;var n,r;if(e&&"function"==typeof(n=t.toString)&&!m(r=n.call(t)))return r;if("function"==typeof(n=t.valueOf)&&!m(r=n.call(t)))return r;if(!e&&"function"==typeof(n=t.toString)&&!m(r=n.call(t)))return r;throw TypeError("Can't convert object to primitive value")},E={}.hasOwnProperty,S=function(t,e){return E.call(t,e)},w=a.document,O=m(w)&&m(w.createElement),j=function(t){return O?w.createElement(t):{}},T=!c&&!u((function(){return 7!=Object.defineProperty(j("div"),"a",{get:function(){return 7}}).a})),A=Object.getOwnPropertyDescriptor,I={f:c?A:function(t,e){if(t=g(t),e=x(e,!0),T)try{return A(t,e)}catch(t){}if(S(t,e))return d(!s.f.call(t,e),t[e])}},P=function(t){if(!m(t))throw TypeError(String(t)+" is not an object");return t},R=Object.defineProperty,_={f:c?R:function(t,e,n){if(P(t),e=x(e,!0),P(n),T)try{return R(t,e,n)}catch(t){}if("get"in n||"set"in n)throw TypeError("Accessors not supported");return"value"in n&&(t[e]=n.value),t}},C=c?function(t,e,n){return _.f(t,e,d(1,n))}:function(t,e,n){return t[e]=n,t},D=function(t,e){try{C(a,t,e)}catch(n){a[t]=e}return e},k="__core-js_shared__",F=a[k]||D(k,{}),M=Function.toString;"function"!=typeof F.inspectSource&&(F.inspectSource=function(t){return M.call(t)});var $,U,N,L=F.inspectSource,B=a.WeakMap,q="function"==typeof B&&/native code/.test(L(B)),V=o((function(t){(t.exports=function(t,e){return F[t]||(F[t]=void 0!==e?e:{})})("versions",[]).push({version:"3.8.1",mode:"global",copyright:"© 2020 Denis Pushkarev (zloirock.ru)"})})),K=0,G=Math.random(),H=function(t){return"Symbol("+String(void 0===t?"":t)+")_"+(++K+G).toString(36)},W=V("keys"),z=function(t){return W[t]||(W[t]=H(t))},X={},Y=a.WeakMap;if(q){var Q=F.state||(F.state=new Y),Z=Q.get,J=Q.has,tt=Q.set;$=function(t,e){return e.facade=t,tt.call(Q,t,e),e},U=function(t){return Z.call(Q,t)||{}},N=function(t){return J.call(Q,t)}}else{var et=z("state");X[et]=!0,$=function(t,e){return e.facade=t,C(t,et,e),e},U=function(t){return S(t,et)?t[et]:{}},N=function(t){return S(t,et)}}var nt,rt,ot={set:$,get:U,has:N,enforce:function(t){return N(t)?U(t):$(t,{})},getterFor:function(t){return function(e){var n;if(!m(e)||(n=U(e)).type!==t)throw TypeError("Incompatible receiver, "+t+" required");return n}}},it=o((function(t){var e=ot.get,n=ot.enforce,r=String(String).split("String");(t.exports=function(t,e,o,i){var u,c=!!i&&!!i.unsafe,f=!!i&&!!i.enumerable,l=!!i&&!!i.noTargetGet;"function"==typeof o&&("string"!=typeof e||S(o,"name")||C(o,"name",e),(u=n(o)).source||(u.source=r.join("string"==typeof e?e:""))),t!==a?(c?!l&&t[e]&&(f=!0):delete t[e],f?t[e]=o:C(t,e,o)):f?t[e]=o:D(e,o)})(Function.prototype,"toString",(function(){return"function"==typeof this&&e(this).source||L(this)}))})),at=a,ut=function(t){return"function"==typeof t?t:void 0},ct=function(t,e){return arguments.length<2?ut(at[t])||ut(a[t]):at[t]&&at[t][e]||a[t]&&a[t][e]},ft=Math.ceil,lt=Math.floor,st=function(t){return isNaN(t=+t)?0:(t>0?lt:ft)(t)},dt=Math.min,pt=function(t){return t>0?dt(st(t),9007199254740991):0},vt=Math.max,ht=Math.min,yt=function(t){return function(e,n,r){var o,i=g(e),a=pt(i.length),u=function(t,e){var n=st(t);return n<0?vt(n+e,0):ht(n,e)}(r,a);if(t&&n!=n){for(;a>u;)if((o=i[u++])!=o)return!0}else for(;a>u;u++)if((t||u in i)&&i[u]===n)return t||u||0;return!t&&-1}},bt={includes:yt(!0),indexOf:yt(!1)},gt=bt.indexOf,mt=function(t,e){var n,r=g(t),o=0,i=[];for(n in r)!S(X,n)&&S(r,n)&&i.push(n);for(;e.length>o;)S(r,n=e[o++])&&(~gt(i,n)||i.push(n));return i},xt=["constructor","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","valueOf"],Et=xt.concat("length","prototype"),St={f:Object.getOwnPropertyNames||function(t){return mt(t,Et)}},wt={f:Object.getOwnPropertySymbols},Ot=ct("Reflect","ownKeys")||function(t){var e=St.f(P(t)),n=wt.f;return n?e.concat(n(t)):e},jt=function(t,e){for(var n=Ot(e),r=_.f,o=I.f,i=0;i=74)&&(nt=Ht.match(/Chrome\/(\d+)/))&&(rt=nt[1]);var Yt,Qt=rt&&+rt,Zt=Vt("species"),Jt=Vt("isConcatSpreadable"),te=9007199254740991,ee="Maximum allowed index exceeded",ne=Qt>=51||!u((function(){var t=[];return t[Jt]=!1,t.concat()[0]!==t})),re=(Yt="concat",Qt>=51||!u((function(){var t=[];return(t.constructor={})[Zt]=function(){return{foo:1}},1!==t[Yt](Boolean).foo}))),oe=function(t){if(!m(t))return!1;var e=t[Jt];return void 0!==e?!!e:Ft(t)};kt({target:"Array",proto:!0,forced:!ne||!re},{concat:function(t){var e,n,r,o,i,a=Mt(this),u=Gt(a,0),c=0;for(e=-1,r=arguments.length;ete)throw TypeError(ee);for(n=0;n=te)throw TypeError(ee);$t(u,c++,i)}return u.length=c,u}});var ie,ae=function(t,e,n){if(function(t){if("function"!=typeof t)throw TypeError(String(t)+" is not a function")}(t),void 0===e)return t;switch(n){case 0:return function(){return t.call(e)};case 1:return function(n){return t.call(e,n)};case 2:return function(n,r){return t.call(e,n,r)};case 3:return function(n,r,o){return t.call(e,n,r,o)}}return function(){return t.apply(e,arguments)}},ue=[].push,ce=function(t){var e=1==t,n=2==t,r=3==t,o=4==t,i=6==t,a=7==t,u=5==t||i;return function(c,f,l,s){for(var d,p,v=Mt(c),h=y(v),b=ae(f,l,3),g=pt(h.length),m=0,x=s||Gt,E=e?x(c,g):n||a?x(c,0):void 0;g>m;m++)if((u||m in h)&&(p=b(d=h[m],m,v),t))if(e)E[m]=p;else if(p)switch(t){case 3:return!0;case 5:return d;case 6:return m;case 2:ue.call(E,d)}else switch(t){case 4:return!1;case 7:ue.call(E,d)}return i?-1:r||o?o:E}},fe={forEach:ce(0),map:ce(1),filter:ce(2),some:ce(3),every:ce(4),find:ce(5),findIndex:ce(6),filterOut:ce(7)},le=Object.keys||function(t){return mt(t,xt)},se=c?Object.defineProperties:function(t,e){P(t);for(var n,r=le(e),o=r.length,i=0;o>i;)_.f(t,n=r[i++],e[n]);return t},de=ct("document","documentElement"),pe=z("IE_PROTO"),ve=function(){},he=function(t){return" {% endif %} + {% endblock %} {% block header %} diff --git a/cps/templates/book_table.html b/cps/templates/book_table.html index ecd840b5..bbcd0ed6 100644 --- a/cps/templates/book_table.html +++ b/cps/templates/book_table.html @@ -1,7 +1,9 @@ {% extends "layout.html" %} -{% macro text_table_row(parameter, edit_text, show_text, validate) -%} -{{_(title)}}
    -
    -
    {{_('Merge selected books')}}
    +
    +
    {{_('Merge selected books')}}
    {{_('Remove Selections')}}
    +
    +
    {{_('Exchange author and title')}}
    +
    -
    -
    +
    +
    -
    - - +
    + +
    @@ -43,17 +48,17 @@ {% endif %} - {{ text_table_row('title', _('Enter Title'),_('Title'), true) }} - {{ text_table_row('sort', _('Enter Title Sort'),_('Title Sort'), false) }} - {{ text_table_row('author_sort', _('Enter Author Sort'),_('Author Sort'), false) }} - {{ text_table_row('authors', _('Enter Authors'),_('Authors'), true) }} - {{ text_table_row('tags', _('Enter Categories'),_('Categories'), false) }} - {{ text_table_row('series', _('Enter Series'),_('Series'), false) }} - {{_('Series Index')}} - {{ text_table_row('languages', _('Enter Languages'),_('Languages'), false) }} + {{ text_table_row('title', _('Enter Title'),_('Title'), true, true) }} + {{ text_table_row('sort', _('Enter Title Sort'),_('Title Sort'), false, true) }} + {{ text_table_row('author_sort', _('Enter Author Sort'),_('Author Sort'), false, true) }} + {{ text_table_row('authors', _('Enter Authors'),_('Authors'), true, true) }} + {{ text_table_row('tags', _('Enter Categories'),_('Categories'), false, true) }} + {{ text_table_row('series', _('Enter Series'),_('Series'), false, true) }} + {{_('Series Index')}} + {{ text_table_row('languages', _('Enter Languages'),_('Languages'), false, true) }} - {{ text_table_row('publishers', _('Enter Publishers'),_('Publishers'), false) }} - {% if g.user.role_edit() %} + {{ text_table_row('publishers', _('Enter Publishers'),_('Publishers'), false, true) }} + {% if g.user.role_delete_books() and g.user.role_edit()%} {{_('Delete')}} {% endif %} @@ -73,11 +78,11 @@

    {{_('Books with Title will be merged from:')}}

    -
    +

    {{_('Into Book with Title:')}}

    -
    +