diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 35314511..bfb4688d 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -31,7 +31,7 @@ If applicable, add screenshots to help explain your problem. - OS: [e.g. Windows 10/Raspberry Pi OS] - Python version: [e.g. python2.7] - Calibre-Web version: [e.g. 0.6.8 or 087c4c59 (git rev-parse --short HEAD)]: - - Docker container: [None/Technosoft2000/Linuxuser]: + - Docker container: [None/Technosoft2000/LinuxServer]: - Special Hardware: [e.g. Rasperry Pi Zero] - Browser: [e.g. Chrome 83.0.4103.97, Safari 13.3.7, Firefox 68.0.1 ESR] diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..3ba13e0c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false diff --git a/.gitignore b/.gitignore index f06dcd44..cbf29ce0 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ env/ venv/ eggs/ dist/ +executable/ build/ vendor/ .eggs/ @@ -29,4 +30,4 @@ vendor/ settings.yaml gdrive_credentials client_secrets.json - +gmail.json diff --git a/README.md b/README.md index 4ee209f8..4a3db631 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,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) @@ -22,7 +22,7 @@ Calibre-Web is a web app providing a clean interface for browsing, reading and d - Support for public user registration - Send eBooks to Kindle devices with the click of a button - Sync your Kobo devices through Calibre-Web with your Calibre library -- Support for reading eBooks directly in the browser (.txt, .epub, .pdf, .cbr, .cbt, .cbz) +- Support for reading eBooks directly in the browser (.txt, .epub, .pdf, .cbr, .cbt, .cbz, .djvu) - Upload new books in many formats, including audio formats (.mp3, .m4a, .m4b) - Support for Calibre Custom Columns - Ability to hide content based on categories and Custom Column content per user @@ -32,8 +32,8 @@ Calibre-Web is a web app providing a clean interface for browsing, reading and d ## Quick start -1. Install dependencies by running `pip3 install --target vendor -r requirements.txt` (python3.x) or `pip install --target vendor -r requirements.txt` (python2.7). -2. Execute the command: `python cps.py` (or `nohup python cps.py` - recommended if you want to exit the terminal window) +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) @@ -48,7 +48,7 @@ Please note that running the above install command can fail on some versions of ## Requirements -python 3.x+, (Python 2.7+) +python 3.x+ 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: diff --git a/cps.py b/cps.py index 20aa9cac..737b0d97 100755 --- a/cps.py +++ b/cps.py @@ -31,7 +31,7 @@ else: sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'vendor')) -from cps import create_app, config +from cps import create_app from cps import web_server from cps.opds import opds from cps.web import web @@ -41,6 +41,8 @@ from cps.shelf import shelf from cps.admin import admi from cps.gdrive import gdrive from cps.editbooks import editbook +from cps.remotelogin import remotelogin +from cps.error_handler import init_errorhandler try: from cps.kobo import kobo, get_kobo_activated @@ -58,14 +60,18 @@ except ImportError: def main(): app = create_app() + + init_errorhandler() + app.register_blueprint(web) app.register_blueprint(opds) app.register_blueprint(jinjia) app.register_blueprint(about) app.register_blueprint(shelf) app.register_blueprint(admi) - if config.config_use_google_drive: - app.register_blueprint(gdrive) + app.register_blueprint(remotelogin) + # if config.config_use_google_drive: + app.register_blueprint(gdrive) app.register_blueprint(editbook) if kobo_available: app.register_blueprint(kobo) diff --git a/cps/__init__.py b/cps/__init__.py index 03945b57..f6bb0cf7 100644 --- a/cps/__init__.py +++ b/cps/__init__.py @@ -45,6 +45,7 @@ mimetypes.add_type('application/fb2+zip', '.fb2') mimetypes.add_type('application/x-mobipocket-ebook', '.mobi') mimetypes.add_type('application/x-mobipocket-ebook', '.prc') mimetypes.add_type('application/vnd.amazon.ebook', '.azw') +mimetypes.add_type('application/x-mobi8-ebook', '.azw3') mimetypes.add_type('application/x-cbr', '.cbr') mimetypes.add_type('application/x-cbz', '.cbz') mimetypes.add_type('application/x-cbt', '.cbt') @@ -94,9 +95,13 @@ def create_app(): app.root_path = app.root_path.decode('utf-8') app.instance_path = app.instance_path.decode('utf-8') - cache_buster.init_cache_busting(app) + if os.environ.get('FLASK_DEBUG'): + 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 is no longer supporting Python2 please consider upgrading to Python3') + print('Python2 is EOL since end of 2019, this version of Calibre-Web is no longer 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)) @@ -122,7 +127,7 @@ def get_locale(): 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() diff --git a/cps/about.py b/cps/about.py index f9c58738..10e68e69 100644 --- a/cps/about.py +++ b/cps/about.py @@ -31,12 +31,13 @@ import werkzeug, flask, flask_login, flask_principal, jinja2 from flask_babel import gettext as _ from . import db, calibre_db, converter, uploader, server, isoLanguages, constants -from .web import render_title_template +from .render_template import render_title_template try: from flask_login import __version__ as flask_loginVersion except ImportError: from flask_login.__about__ import __version__ as flask_loginVersion try: + # pylint: disable=unused-import import unidecode # _() necessary to make babel aware of string for translation unidecode_version = _(u'installed') @@ -48,6 +49,11 @@ try: except ImportError: flask_danceVersion = None +try: + from greenlet import __version__ as greenlet_Version +except ImportError: + greenlet_Version = None + from . import services about = flask.Blueprint('about', __name__) @@ -77,7 +83,8 @@ _VERSIONS = OrderedDict( python_LDAP = services.ldapVersion if bool(services.ldapVersion) else None, Goodreads = u'installed' if bool(services.goodreads_support) else None, jsonschema = services.SyncToken.__version__ if bool(services.SyncToken) else None, - flask_dance = flask_danceVersion + flask_dance = flask_danceVersion, + greenlet = greenlet_Version ) _VERSIONS.update(uploader.get_versions()) diff --git a/cps/admin.py b/cps/admin.py index 531d855d..55ab41ad 100644 --- a/cps/admin.py +++ b/cps/admin.py @@ -5,7 +5,7 @@ # andy29485, idalin, Kyosfonica, wuqi, Kennyl, lemmsh, # falgh1, grunjol, csitko, ytils, xybydy, trasba, vrabe, # ruben-herold, marblepebble, JackED42, SiphonSquirrel, -# apetresc, nanu-c, mutschler +# apetresc, nanu-c, mutschler, GammaC0de, vuolter # # 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 @@ -26,34 +26,46 @@ import re import base64 import json import time +import operator 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 -from flask_login import login_required, current_user, logout_user +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 sqlalchemy import and_ +from sqlalchemy.orm.attributes import flag_modified from sqlalchemy.exc import IntegrityError, OperationalError, InvalidRequestError -from sqlalchemy.sql.expression import func +from sqlalchemy.sql.expression import func, or_, text +# from sqlalchemy.func import field from . import constants, logger, helper, services +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 .web import admin_required, render_title_template, before_request, unconfigured +from .render_template import render_title_template, get_sidebar_config from . import debug_info +try: + from functools import wraps +except ImportError: + pass # We're not using Python 3 + log = logger.create() feature_support = { 'ldap': bool(services.ldap), 'goodreads': bool(services.goodreads_support), - 'kobo': bool(services.kobo) + 'kobo': bool(services.kobo), + 'updater': constants.UPDATER_AVAILABLE, + 'gmail': bool(services.gmail) } try: - import rarfile + import rarfile # pylint: disable=unused-import feature_support['rar'] = True except (ImportError, SyntaxError): feature_support['rar'] = False @@ -72,6 +84,52 @@ feature_support['gdrive'] = gdrive_support admi = Blueprint('admin', __name__) +def admin_required(f): + """ + Checks if current_user.role == 1 + """ + + @wraps(f) + def inner(*args, **kwargs): + if current_user.role_admin(): + return f(*args, **kwargs) + abort(403) + + 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(): + if current_user.is_authenticated: + confirm_login() + g.constants = constants + g.user = current_user + g.allow_registration = config.config_public_reg + g.allow_anonymous = config.config_anonbrowse + g.allow_upload = config.config_uploading + g.current_theme = config.config_theme + g.config_authors_max = config.config_authors_max + 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')) + @admi.route("/admin") @login_required @@ -95,7 +153,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: @@ -143,7 +201,7 @@ def admin(): @admin_required def configuration(): if request.method == "POST": - return _configuration_update_helper() + return _configuration_update_helper(True) return _configuration_result() @@ -151,14 +209,241 @@ def configuration(): @login_required @admin_required def view_configuration(): - readColumn = calibre_db.session.query(db.Custom_Columns)\ - .filter(and_(db.Custom_Columns.datatype == 'bool', db.Custom_Columns.mark_for_delete == 0)).all() - restrictColumns= calibre_db.session.query(db.Custom_Columns)\ - .filter(and_(db.Custom_Columns.datatype == 'text', db.Custom_Columns.mark_for_delete == 0)).all() - return render_title_template("config_view_edit.html", conf=config, readColumns=readColumn, - restrictColumns=restrictColumns, + read_column = calibre_db.session.query(db.Custom_Columns)\ + .filter(and_(db.Custom_Columns.datatype == 'bool', db.Custom_Columns.mark_for_delete == 0)).all() + restrict_columns = calibre_db.session.query(db.Custom_Columns)\ + .filter(and_(db.Custom_Columns.datatype == 'text', db.Custom_Columns.mark_for_delete == 0)).all() + return render_title_template("config_view_edit.html", conf=config, readColumns=read_column, + restrictColumns=restrict_columns, title=_(u"UI Configuration"), page="uiconfig") +@admi.route("/admin/usertable") +@login_required +@admin_required +def edit_user_table(): + visibility = current_user.view_settings.get('useredit', {}) + 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) + + 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, + sidebar_settings=constants.sidebar_settings, + title=_(u"Edit Users"), + page="usertable") + + +@admi.route("/ajax/listusers") +@login_required +@admin_required +def list_users(): + 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 = filtered_count = all_user.count() + + if 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 + "%"))) + if state: + users = calibre_db.get_checkbox_sorted(all_user.all(), state, off, limit, request.args.get("order", "").lower()) + else: + users = all_user.order_by(order).offset(off).limit(limit).all() + if search: + filtered_count = len(users) + + for user in users: + if user.default_language == "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", methods=['POST']) +@login_required +@admin_required +def delete_user(): + user_ids = request.form.to_dict(flat=False) + 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 +@admin_required +def table_get_locale(): + locale = babel.list_translations() + [LC('en')] + ret = list() + current_locale = get_locale() + for loc in locale: + ret.append({'value': str(loc), 'text': loc.get_language_name(current_locale)}) + return json.dumps(ret) + + +@admi.route("/ajax/getdefaultlanguage") +@login_required +@admin_required +def table_get_default_lang(): + languages = calibre_db.speaking_language() + ret = list() + ret.append({'value': 'all', 'text': _('Show All')}) + for lang in languages: + ret.append({'value': lang.lang_code, 'text': lang.name}) + return json.dumps(ret) + + +@admi.route("/ajax/editlistusers/", methods=['POST']) +@login_required +@admin_required +def edit_list_user(param): + vals = request.form.to_dict(flat=False) + 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) + # only one user is posted + if "pk" in vals: + users = [all_user.filter(ub.User.id == vals['pk'][0]).one_or_none()] + else: + if "pk[]" in vals: + users = all_user.filter(ub.User.id.in_(vals['pk[]'])).all() + else: + return "" + if 'field_index' in vals: + vals['field_index'] = vals['field_index'][0] + if 'value' in vals: + vals['value'] = vals['value'][0] + elif not ('value[]' in vals): + return "" + for user in users: + 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: + 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 == 'kindle_mail': + user.kindle_mail = valid_email(vals['value']) if vals['value'] else "" + elif param.endswith('role'): + if user.name == "Guest" and int(vals['field_index']) in \ + [constants.ROLE_ADMIN, constants.ROLE_PASSWD, constants.ROLE_EDIT_SHELFS]: + raise Exception(_("Guest can't have this role")) + if vals['value'] == 'true': + user.role |= int(vals['field_index']) + else: + if int(vals['field_index']) == 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 &= ~int(vals['field_index']) + elif param.startswith('sidebar'): + if user.name == "Guest" and int(vals['field_index']) == constants.SIDEBAR_READ_AND_UNREAD: + raise Exception(_("Guest can't have this view")) + if vals['value'] == 'true': + user.sidebar_view |= int(vals['field_index']) + else: + user.sidebar_view &= ~int(vals['field_index']) + elif param == 'locale': + if user.name == "Guest": + raise Exception(_("Guest's Locale is determined automatically and can't be set")) + user.locale = vals['value'] + elif param == 'default_language': + user.default_language = vals['value'] + except Exception as ex: + log.debug_or_exception(ex) + return str(ex), 400 + ub.session_commit() + return "" + + +@admi.route("/ajax/user_table_settings", methods=['POST']) +@login_required +@admin_required +def update_table_settings(): + current_user.view_settings['useredit'] = json.loads(request.data) + 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 + return "" + @admi.route("/admin/viewconfig", methods=["POST"]) @login_required @@ -190,11 +475,37 @@ 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/") +@login_required +def load_dialogtexts(element_id): + texts = {"header": "", "main": ""} + if element_id == "config_delete_kobo_token": + texts["main"] = _('Do you really want to delete the Kobo Token?') + elif element_id == "btndeletedomain": + texts["main"] = _('Do you really want to delete this domain?') + elif element_id == "btndeluser": + texts["main"] = _('Do you really want to delete this user?') + elif element_id == "delete_shelf": + texts["main"] = _('Are you sure you want to delete this shelf?') + elif element_id == "select_locale": + texts["main"] = _('Are you sure you want to change locales of selected user(s)?') + elif element_id == "select_default_language": + 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)?') + return json.dumps(texts) + + @admi.route("/ajax/editdomain/", methods=['POST']) @login_required @admin_required @@ -206,8 +517,7 @@ def edit_domain(allow): vals = request.form.to_dict() answer = ub.session.query(ub.Registration).filter(ub.Registration.id == vals['pk']).first() answer.domain = vals['value'].replace('*', '%').replace('?', '_').lower() - ub.session.commit() - return "" + return ub.session_commit("Registering Domains edited {}".format(answer.domain)) @admi.route("/ajax/adddomain/", methods=['POST']) @@ -220,7 +530,7 @@ def add_domain(allow): if not check: new_domain = ub.Registration(domain=domain_name, allow=allow) ub.session.add(new_domain) - ub.session.commit() + ub.session_commit("Registering Domains added {}".format(domain_name)) return "" @@ -228,14 +538,17 @@ def add_domain(allow): @login_required @admin_required def delete_domain(): - domain_id = request.form.to_dict()['domainid'].replace('*', '%').replace('?', '_').lower() - ub.session.query(ub.Registration).filter(ub.Registration.id == domain_id).delete() - ub.session.commit() - # If last domain was deleted, add all domains by default - if not ub.session.query(ub.Registration).filter(ub.Registration.allow==1).count(): - new_domain = ub.Registration(domain="%.%",allow=1) - ub.session.add(new_domain) - ub.session.commit() + try: + domain_id = request.form.to_dict()['domainid'].replace('*', '%').replace('?', '_').lower() + ub.session.query(ub.Registration).filter(ub.Registration.id == domain_id).delete() + ub.session_commit("Registering Domains deleted {}".format(domain_id)) + # If last domain was deleted, add all domains by default + if not ub.session.query(ub.Registration).filter(ub.Registration.allow == 1).count(): + new_domain = ub.Registration(domain="%.%", allow=1) + ub.session.add(new_domain) + ub.session_commit("Last Registering Domain deleted, added *.* as default") + except KeyError: + pass return "" @@ -250,75 +563,74 @@ def list_domain(allow): response.headers["Content-Type"] = "application/json; charset=utf-8" return response -@admi.route("/ajax/editrestriction/", methods=['POST']) + +@admi.route("/ajax/editrestriction/", defaults={"user_id": 0}, methods=['POST']) +@admi.route("/ajax/editrestriction//", methods=['POST']) @login_required @admin_required -def edit_restriction(res_type): +def edit_restriction(res_type, user_id): element = request.form.to_dict() if element['id'].startswith('a'): if res_type == 0: # Tags as template elementlist = config.list_allowed_tags() - elementlist[int(element['id'][1:])]=element['Element'] + elementlist[int(element['id'][1:])] = element['Element'] config.config_allowed_tags = ','.join(elementlist) config.save() if res_type == 1: # CustomC elementlist = config.list_allowed_column_values() - elementlist[int(element['id'][1:])]=element['Element'] + elementlist[int(element['id'][1:])] = element['Element'] config.config_allowed_column_value = ','.join(elementlist) config.save() if res_type == 2: # Tags per user - usr_id = os.path.split(request.referrer)[-1] - if usr_id.isdigit() == True: - usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first() + if isinstance(user_id, int): + usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first() else: usr = current_user elementlist = usr.list_allowed_tags() - elementlist[int(element['id'][1:])]=element['Element'] + elementlist[int(element['id'][1:])] = element['Element'] usr.allowed_tags = ','.join(elementlist) - ub.session.commit() + ub.session_commit("Changed allowed tags of user {} to {}".format(usr.name, usr.allowed_tags)) if res_type == 3: # CColumn per user - usr_id = os.path.split(request.referrer)[-1] - if usr_id.isdigit() == True: - usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first() + if isinstance(user_id, int): + usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first() else: usr = current_user elementlist = usr.list_allowed_column_values() - elementlist[int(element['id'][1:])]=element['Element'] + elementlist[int(element['id'][1:])] = element['Element'] usr.allowed_column_value = ','.join(elementlist) - ub.session.commit() + 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() - elementlist[int(element['id'][1:])]=element['Element'] + elementlist[int(element['id'][1:])] = element['Element'] config.config_denied_tags = ','.join(elementlist) config.save() if res_type == 1: # CustomC elementlist = config.list_denied_column_values() - elementlist[int(element['id'][1:])]=element['Element'] + elementlist[int(element['id'][1:])] = element['Element'] config.config_denied_column_value = ','.join(elementlist) config.save() if res_type == 2: # Tags per user - usr_id = os.path.split(request.referrer)[-1] - if usr_id.isdigit() == True: - usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first() + if isinstance(user_id, int): + usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first() else: usr = current_user elementlist = usr.list_denied_tags() - elementlist[int(element['id'][1:])]=element['Element'] + elementlist[int(element['id'][1:])] = element['Element'] usr.denied_tags = ','.join(elementlist) - ub.session.commit() + ub.session_commit("Changed denied tags of user {} to {}".format(usr.name, usr.denied_tags)) if res_type == 3: # CColumn per user - usr_id = os.path.split(request.referrer)[-1] - if usr_id.isdigit() == True: - usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first() + if isinstance(user_id, int): + usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first() else: usr = current_user elementlist = usr.list_denied_column_values() - elementlist[int(element['id'][1:])]=element['Element'] + elementlist[int(element['id'][1:])] = element['Element'] usr.denied_column_value = ','.join(elementlist) - ub.session.commit() + ub.session_commit("Changed denied columns of user {} to {}".format(usr.name, usr.denied_column_value)) return "" + def restriction_addition(element, list_func): elementlist = list_func() if elementlist == ['']: @@ -335,10 +647,27 @@ def restriction_deletion(element, list_func): return ','.join(elementlist) -@admi.route("/ajax/addrestriction/", methods=['POST']) +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() + 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] + else: + saved_tags_list.extend(x for x in new_tags_list if x not in saved_tags_list) + return ",".join(saved_tags_list) + + +@admi.route("/ajax/addrestriction/", defaults={"user_id": 0}, methods=['POST']) +@admi.route("/ajax/addrestriction//", methods=['POST']) @login_required @admin_required -def add_restriction(res_type): +def add_restriction(res_type, user_id): element = request.form.to_dict() if res_type == 0: # Tags as template if 'submit_allow' in element: @@ -355,35 +684,37 @@ def add_restriction(res_type): config.config_denied_column_value = restriction_addition(element, config.list_allowed_column_values) config.save() if res_type == 2: # Tags per user - usr_id = os.path.split(request.referrer)[-1] - if usr_id.isdigit() == True: - usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first() + if isinstance(user_id, int): + usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first() else: usr = current_user if 'submit_allow' in element: usr.allowed_tags = restriction_addition(element, usr.list_allowed_tags) - ub.session.commit() + 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() + ub.session_commit("Changed denied tags of user {} to {}".format(usr.name, usr.list_denied_tags)) if res_type == 3: # CustomC per user - usr_id = os.path.split(request.referrer)[-1] - if usr_id.isdigit() == True: - usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first() + if isinstance(user_id, int): + usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first() else: usr = current_user if 'submit_allow' in element: usr.allowed_column_value = restriction_addition(element, usr.list_allowed_column_values) - ub.session.commit() + 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() + ub.session_commit("Changed denied columns of user {} to {}".format(usr.name, + usr.list_denied_column_values)) return "" -@admi.route("/ajax/deleterestriction/", methods=['POST']) + +@admi.route("/ajax/deleterestriction/", defaults={"user_id": 0}, methods=['POST']) +@admi.route("/ajax/deleterestriction//", methods=['POST']) @login_required @admin_required -def delete_restriction(res_type): +def delete_restriction(res_type, user_id): element = request.form.to_dict() if res_type == 0: # Tags as template if element['id'].startswith('a'): @@ -400,85 +731,182 @@ def delete_restriction(res_type): config.config_denied_column_value = restriction_deletion(element, config.list_denied_column_values) config.save() elif res_type == 2: # Tags per user - usr_id = os.path.split(request.referrer)[-1] - if usr_id.isdigit() == True: - usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first() + if isinstance(user_id, int): + usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first() else: usr = current_user if element['id'].startswith('a'): usr.allowed_tags = restriction_deletion(element, usr.list_allowed_tags) - ub.session.commit() + 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() + ub.session_commit("Deleted denied tags of user {}: {}".format(usr.name, usr.list_allowed_tags)) elif res_type == 3: # Columns per user - usr_id = os.path.split(request.referrer)[-1] - if usr_id.isdigit() == True: # select current user if admins are editing their own rights - usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first() + if isinstance(user_id, int): + usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first() else: usr = current_user if element['id'].startswith('a'): usr.allowed_column_value = restriction_deletion(element, usr.list_allowed_column_values) - ub.session.commit() + 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() + ub.session_commit("Deleted denied columns of user {}: {}".format(usr.name, + usr.list_denied_column_values)) return "" -@admi.route("/ajax/listrestriction/") +@admi.route("/ajax/listrestriction/", defaults={"user_id": 0}) +@admi.route("/ajax/listrestriction//") @login_required @admin_required -def list_restriction(res_type): +def list_restriction(res_type, user_id): if res_type == 0: # Tags as template restrict = [{'Element': x, 'type':_('Deny'), 'id': 'd'+str(i) } - for i,x in enumerate(config.list_denied_tags()) if x != '' ] - allow = [{'Element': x, 'type':_('Allow'), 'id': 'a'+str(i) } - for i,x in enumerate(config.list_allowed_tags()) if x != ''] + for i,x in enumerate(config.list_denied_tags()) if x != ''] + allow = [{'Element': x, 'type': _('Allow'), 'id': 'a'+str(i)} + for i, x in enumerate(config.list_allowed_tags()) if x != ''] json_dumps = restrict + allow elif res_type == 1: # CustomC as template - restrict = [{'Element': x, 'type':_('Deny'), 'id': 'd'+str(i) } - for i,x in enumerate(config.list_denied_column_values()) if x != '' ] - allow = [{'Element': x, 'type':_('Allow'), 'id': 'a'+str(i) } - for i,x in enumerate(config.list_allowed_column_values()) if x != ''] + restrict = [{'Element': x, 'type': _('Deny'), 'id': 'd'+str(i)} + for i, x in enumerate(config.list_denied_column_values()) if x != ''] + allow = [{'Element': x, 'type': _('Allow'), 'id': 'a'+str(i)} + for i, x in enumerate(config.list_allowed_column_values()) if x != ''] json_dumps = restrict + allow elif res_type == 2: # Tags per user - usr_id = os.path.split(request.referrer)[-1] - if usr_id.isdigit() == True: - usr = ub.session.query(ub.User).filter(ub.User.id == usr_id).first() + if isinstance(user_id, int): + usr = ub.session.query(ub.User).filter(ub.User.id == user_id).first() else: usr = current_user - restrict = [{'Element': x, 'type':_('Deny'), 'id': 'd'+str(i) } - for i,x in enumerate(usr.list_denied_tags()) if x != '' ] - allow = [{'Element': x, 'type':_('Allow'), 'id': 'a'+str(i) } - for i,x in enumerate(usr.list_allowed_tags()) if x != ''] + restrict = [{'Element': x, 'type': _('Deny'), 'id': 'd'+str(i)} + for i, x in enumerate(usr.list_denied_tags()) if x != ''] + allow = [{'Element': x, 'type': _('Allow'), 'id': 'a'+str(i)} + for i, x in enumerate(usr.list_allowed_tags()) if x != ''] json_dumps = restrict + allow elif res_type == 3: # CustomC per user - usr_id = os.path.split(request.referrer)[-1] - if usr_id.isdigit() == True: - usr = ub.session.query(ub.User).filter(ub.User.id==usr_id).first() + if isinstance(user_id, int): + usr = ub.session.query(ub.User).filter(ub.User.id == user_id).first() else: usr = current_user - restrict = [{'Element': x, 'type':_('Deny'), 'id': 'd'+str(i) } - for i,x in enumerate(usr.list_denied_column_values()) if x != '' ] - allow = [{'Element': x, 'type':_('Allow'), 'id': 'a'+str(i) } - for i,x in enumerate(usr.list_allowed_column_values()) if x != ''] + restrict = [{'Element': x, 'type': _('Deny'), 'id': 'd'+str(i)} + for i, x in enumerate(usr.list_denied_column_values()) if x != ''] + allow = [{'Element': x, 'type': _('Allow'), 'id': 'a'+str(i)} + for i, x in enumerate(usr.list_allowed_column_values()) if x != ''] json_dumps = restrict + allow else: - json_dumps="" + json_dumps = "" js = json.dumps(json_dumps) response = make_response(js.replace("'", '"')) response.headers["Content-Type"] = "application/json; charset=utf-8" return response -@admi.route("/config", methods=["GET", "POST"]) +@admi.route("/basicconfig/pathchooser/") +@unconfigured +def config_pathchooser(): + if filepicker: + return pathchooser() + abort(403) + + +@admi.route("/ajax/pathchooser/") +@login_required +@admin_required +def ajax_pathchooser(): + return pathchooser() + + +def pathchooser(): + browse_for = "folder" + folder_only = request.args.get('folder', False) == "true" + file_filter = request.args.get('filter', "") + path = os.path.normpath(request.args.get('path', "")) + + if os.path.isfile(path): + oldfile = path + path = os.path.dirname(path) + else: + oldfile = "" + + absolute = False + + if os.path.isdir(path): + # if os.path.isabs(path): + cwd = os.path.realpath(path) + absolute = True + # else: + # cwd = os.path.relpath(path) + else: + cwd = os.getcwd() + + cwd = os.path.normpath(os.path.realpath(cwd)) + parentdir = os.path.dirname(cwd) + if not absolute: + if os.path.realpath(cwd) == os.path.realpath("/"): + cwd = os.path.relpath(cwd) + else: + cwd = os.path.relpath(cwd) + os.path.sep + parentdir = os.path.relpath(parentdir) + os.path.sep + + if os.path.realpath(cwd) == os.path.realpath("/"): + parentdir = "" + + try: + folders = os.listdir(cwd) + except Exception: + folders = [] + + files = [] + for f in folders: + try: + data = {"name": f, "fullpath": os.path.join(cwd, f)} + data["sort"] = data["fullpath"].lower() + except Exception: + continue + + if os.path.isfile(os.path.join(cwd, f)): + if folder_only: + continue + if file_filter != "" and file_filter != f: + continue + data["type"] = "file" + data["size"] = os.path.getsize(os.path.join(cwd, f)) + + power = 0 + while (data["size"] >> 10) > 0.3: + power += 1 + data["size"] >>= 10 + units = ("", "K", "M", "G", "T") + data["size"] = str(data["size"]) + " " + units[power] + "Byte" + else: + data["type"] = "dir" + data["size"] = "" + + files.append(data) + + files = sorted(files, key=operator.itemgetter("type", "sort")) + + context = { + "cwd": cwd, + "files": files, + "parentdir": parentdir, + "type": browse_for, + "oldfile": oldfile, + "absolute": absolute, + } + return json.dumps(context) + + +@admi.route("/basicconfig", methods=["GET", "POST"]) @unconfigured def basic_configuration(): logout_user() if request.method == "POST": - return _configuration_update_helper() - return _configuration_result() + log.debug("Basic Configuration send") + return _configuration_update_helper(configured=filepicker) + return _configuration_result(configured=filepicker) def _config_int(to_save, x, func=int): @@ -502,8 +930,8 @@ def _configuration_gdrive_helper(to_save): config.config_use_google_drive = False gdrive_secrets = {} - gdriveError = gdriveutils.get_error_text(gdrive_secrets) - if "config_use_google_drive" in to_save and not config.config_use_google_drive and not gdriveError: + 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: @@ -515,10 +943,11 @@ def _configuration_gdrive_helper(to_save): ) # always show google drive settings, but in case of error deny support - config.config_use_google_drive = (not gdriveError) and ("config_use_google_drive" in to_save) + config.config_use_google_drive = (not gdrive_error) and ("config_use_google_drive" in to_save) if _config_string(to_save, "config_google_drive_folder"): gdriveutils.deleteDatabaseOnChange() - return gdriveError + return gdrive_error + def _configuration_oauth_helper(to_save): active_oauths = 0 @@ -541,22 +970,24 @@ def _configuration_oauth_helper(to_save): "active": element["active"]}) return reboot_required -def _configuration_logfile_helper(to_save, gdriveError): + +def _configuration_logfile_helper(to_save, gdrive_error): 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'), gdriveError) + _configuration_result(_('Logfile Location is not Valid, Please Enter Correct Path'), gdrive_error) 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'), gdriveError) + _configuration_result(_('Access Logfile Location is not Valid, Please Enter Correct Path'), gdrive_error) return reboot_required, None -def _configuration_ldap_helper(to_save, gdriveError): + +def _configuration_ldap_helper(to_save, gdrive_error): reboot_required = False reboot_required |= _config_string(to_save, "config_ldap_provider_url") reboot_required |= _config_int(to_save, "config_ldap_port") @@ -573,7 +1004,7 @@ def _configuration_ldap_helper(to_save, gdriveError): 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() @@ -583,44 +1014,44 @@ def _configuration_ldap_helper(to_save, gdriveError): 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'), gdriveError) + 'Port, DN and User Object Identifier'), gdrive_error) 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', - gdriveError) + gdrive_error) else: if not config.config_ldap_serv_username: - return reboot_required, _configuration_result('Please Enter a LDAP Service Account', gdriveError) + return reboot_required, _configuration_result('Please Enter a LDAP Service Account', gdrive_error) 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'), - gdriveError) + gdrive_error) 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'), - gdriveError) + gdrive_error) 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'), - gdriveError) + gdrive_error) 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'), - gdriveError) + gdrive_error) - 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'), - gdriveError) + gdrive_error) 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'), - gdriveError) + gdrive_error) 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 @@ -629,39 +1060,43 @@ def _configuration_ldap_helper(to_save, gdriveError): return reboot_required, \ _configuration_result(_('LDAP CACertificate, Certificate or Key Location is not Valid, ' 'Please Enter Correct Path'), - gdriveError) + gdrive_error) return reboot_required, None -def _configuration_update_helper(): +def _configuration_update_helper(configured): reboot_required = False db_change = False to_save = request.form.to_dict() - gdriveError = None + gdrive_error = None - to_save['config_calibre_dir'] = re.sub('[\\/]metadata\.db$', + to_save['config_calibre_dir'] = re.sub(r'[\\/]metadata\.db$', '', to_save['config_calibre_dir'], flags=re.IGNORECASE) try: db_change |= _config_string(to_save, "config_calibre_dir") - # Google drive setup - gdriveError = _configuration_gdrive_helper(to_save) + # gdrive_error drive setup + gdrive_error = _configuration_gdrive_helper(to_save) 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'), gdriveError) + return _configuration_result(_('Keyfile Location is not Valid, Please Enter Correct Path'), + gdrive_error, + configured) 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'), gdriveError) + return _configuration_result(_('Certfile Location is not Valid, Please Enter Correct Path'), + gdrive_error, + configured) _config_checkbox_int(to_save, "config_uploading") # Reboot on config_anonbrowse with enabled ldap, as decoraters are changed in this case - reboot_required |= (_config_checkbox_int(to_save, "config_anonbrowse") + reboot_required |= (_config_checkbox_int(to_save, "config_anonbrowse") and config.config_login_type == constants.LOGIN_LDAP) _config_checkbox_int(to_save, "config_public_reg") _config_checkbox_int(to_save, "config_register_email") @@ -681,9 +1116,9 @@ def _configuration_update_helper(): 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, gdriveError) + reboot, message = _configuration_ldap_helper(to_save, gdrive_error) if message: return message reboot_required |= reboot @@ -692,7 +1127,7 @@ def _configuration_update_helper(): _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() + ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.token_type == 0).delete() # Goodreads configuration _config_checkbox(to_save, "config_use_goodreads") @@ -713,7 +1148,7 @@ def _configuration_update_helper(): if config.config_login_type == constants.LOGIN_OAUTH: reboot_required |= _configuration_oauth_helper(to_save) - reboot, message = _configuration_logfile_helper(to_save, gdriveError) + reboot, message = _configuration_logfile_helper(to_save, gdrive_error) if message: return message reboot_required |= reboot @@ -722,22 +1157,25 @@ def _configuration_update_helper(): if "config_rarfile_location" in to_save: unrar_status = helper.check_unrar(config.config_rarfile_location) if unrar_status: - return _configuration_result(unrar_status, gdriveError) + return _configuration_result(unrar_status, gdrive_error, configured) except (OperationalError, InvalidRequestError): ub.session.rollback() - _configuration_result(_(u"Settings DB is not Writeable"), gdriveError) + log.error("Settings DB is not Writeable") + _configuration_result(_("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, gdriveError) + except Exception as ex: + return _configuration_result('%s' % ex, 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'), gdriveError) + 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") @@ -746,16 +1184,17 @@ def _configuration_update_helper(): if reboot_required: web_server.stop(True) - return _configuration_result(None, gdriveError) + return _configuration_result(None, gdrive_error, configured) -def _configuration_result(error_flash=None, gdriveError=None): +def _configuration_result(error_flash=None, gdrive_error=None, configured=True): gdrive_authenticate = not is_gdrive_ready() gdrivefolders = [] - if gdriveError is None: - gdriveError = gdriveutils.get_error_text() - if gdriveError: - gdriveError = _(gdriveError) + if gdrive_error is None: + gdrive_error = gdriveutils.get_error_text() + if gdrive_error: + log.error(gdrive_error) + gdrive_error = _(gdrive_error) else: # if config.config_use_google_drive and\ if not gdrive_authenticate and gdrive_support: @@ -764,20 +1203,26 @@ def _configuration_result(error_flash=None, gdriveError=None): 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 - return render_title_template("config_edit.html", config=config, provider=oauthblueprints, - show_back_button=show_back_button, show_login_button=show_login_button, + return render_title_template("config_edit.html", + config=config, + provider=oauthblueprints, + show_back_button=show_back_button, + show_login_button=show_login_button, show_authenticate_google_drive=gdrive_authenticate, - gdriveError=gdriveError, gdrivefolders=gdrivefolders, feature_support=feature_support, + filepicker=configured, + gdriveError=gdrive_error, + gdrivefolders=gdrivefolders, + feature_support=feature_support, title=_(u"Basic Configuration"), page="config") -def _handle_new_user(to_save, content,languages, translations, kobo_support): +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_')) @@ -785,28 +1230,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) @@ -817,35 +1255,49 @@ def _handle_new_user(to_save, content,languages, translations, kobo_support): content.denied_column_value = config.config_denied_column_value 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 _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(): +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": 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')) + ub.session_commit() + log.info(u"User {} deleted".format(content.name)) + return(_(u"User '%(nick)s' deleted", nick=content.name)) else: - flash(_(u"No admin user remaining, can't delete user", nick=content.nickname), category="error") - return redirect(url_for('admin.admin')) + 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 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 \ - not 'admin_role' in to_save: - flash(_(u"No admin user remaining, can't remove admin role", nick=content.nickname), category="error") + ub.User.id != content.id).count() and 'admin_role' not in to_save: + 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) @@ -855,66 +1307,58 @@ def _handle_edit_user(to_save, content,languages, translations, kobo_support): content.role &= ~constants.ROLE_ANONYMOUS val = [int(k[5:]) for k in to_save if k.startswith('show_')] - sidebar = ub.get_sidebar_config() + sidebar = get_sidebar_config() for element in sidebar: value = element['visibility'] if value in val and not content.check_visibility(value): content.sidebar_view |= value - elif not value in val and content.check_visibility(value): + 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: + if to_save.get("default_language"): content.default_language = to_save["default_language"] - if "locale" in to_save and to_save["locale"]: + if to_save.get("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") - - if "kindle_mail" in to_save and to_save["kindle_mail"] != content.kindle_mail: - content.kindle_mail = to_save["kindle_mail"] + 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: + ub.session_commit() + 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."), 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"]) @@ -942,7 +1386,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"]) @@ -950,28 +1394,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"G-Mail 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: @@ -987,7 +1447,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() @@ -995,7 +1455,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, @@ -1004,7 +1466,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/") @@ -1029,9 +1492,8 @@ def reset_user_password(user_id): @login_required @admin_required def view_logfile(): - logfiles = {} - logfiles[0] = logger.get_logfile(config.config_logfile) - logfiles[1] = logger.get_accesslogfile(config.config_access_logfile) + logfiles = {0: logger.get_logfile(config.config_logfile), + 1: logger.get_accesslogfile(config.config_access_logfile)} return render_title_template("logviewer.html", title=_(u"Logfile viewer"), accesslog_enable=config.config_access_log, @@ -1055,6 +1517,7 @@ def send_logfile(logtype): else: return "" + @admi.route("/admin/logdownload/") @login_required @admin_required @@ -1081,8 +1544,11 @@ def download_debug(): @login_required @admin_required def get_update_status(): - log.info(u"Update status requested") - return updater_thread.get_available_updates(request.method, locale=get_locale()) + if feature_support['updater']: + log.info(u"Update status requested") + return updater_thread.get_available_updates(request.method, locale=get_locale()) + else: + return '' @admi.route("/get_updater_status", methods=['GET', 'POST']) @@ -1090,32 +1556,155 @@ def get_update_status(): @admin_required def get_updater_status(): status = {} - if request.method == "POST": - commit = request.form.to_dict() - if "start" in commit and commit['start'] == 'True': - text = { - "1": _(u'Requesting update package'), - "2": _(u'Downloading update package'), - "3": _(u'Unzipping update package'), - "4": _(u'Replacing files'), - "5": _(u'Database connections are closed'), - "6": _(u'Stopping server'), - "7": _(u'Update finished, please press okay and reload page'), - "8": _(u'Update failed:') + u' ' + _(u'HTTP Error'), - "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') - } - status['text'] = text - updater_thread.status = 0 - updater_thread.resume() - status['status'] = updater_thread.get_update_status() - elif request.method == "GET": + if feature_support['updater']: + if request.method == "POST": + commit = request.form.to_dict() + if "start" in commit and commit['start'] == 'True': + text = { + "1": _(u'Requesting update package'), + "2": _(u'Downloading update package'), + "3": _(u'Unzipping update package'), + "4": _(u'Replacing files'), + "5": _(u'Database connections are closed'), + "6": _(u'Stopping server'), + "7": _(u'Update finished, please press okay and reload page'), + "8": _(u'Update failed:') + u' ' + _(u'HTTP Error'), + "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') + } + status['text'] = text + updater_thread.status = 0 + updater_thread.resume() + status['status'] = updater_thread.get_update_status() + elif request.method == "GET": + try: + status['status'] = updater_thread.get_update_status() + if status['status'] == -1: + status['status'] = 7 + except Exception: + status['status'] = 11 + return json.dumps(status) + return '' + + +def ldap_import_create_user(user, 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.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 +def import_ldap_users(): + showtext = {} + try: + new_users = services.ldap.get_group_members(config.config_ldap_group_name) + except (services.ldap.LDAPException, TypeError, AttributeError, KeyError) as e: + log.debug_or_exception(e) + showtext['text'] = _(u'Error: %(ldaperror)s', ldaperror=e) + return json.dumps(showtext) + if not new_users: + log.debug('LDAP empty response') + showtext['text'] = _(u'Error: No user returned in response of LDAP server') + return json.dumps(showtext) + + imported = 0 + for username in new_users: + user = username.decode('utf-8') + if '=' in user: + # if member object field is empty take user object as filter + if config.config_ldap_member_user_object: + query_filter = config.config_ldap_member_user_object + else: + query_filter = config.config_ldap_user_object + try: + user_identifier = extract_user_identifier(user, query_filter) + except Exception as ex: + log.warning(ex) + continue + else: + user_identifier = user + query_filter = None try: - status['status'] = updater_thread.get_update_status() - if status['status'] == -1: - status['status'] = 7 - except Exception: - status['status'] = 11 - return json.dumps(status) + user_data = services.ldap.get_object_details(user=user_identifier, query_filter=query_filter) + except AttributeError as ex: + log.debug_or_exception(ex) + continue + if user_data: + user_count, message = ldap_import_create_user(user, user_data) + if message: + showtext['text'] = message + else: + imported += user_count + else: + log.warning("LDAP User: %s Not Found", user) + showtext['text'] = _(u'At Least One LDAP User Not Found in Database') + if not showtext: + showtext['text'] = _(u'{} User Successfully Imported'.format(imported)) + return json.dumps(showtext) + + +def extract_user_data_from_field(user, field): + match = re.search(field + r"=([\d\s\w-]+)", user, re.IGNORECASE | re.UNICODE) + if match: + return match.group(1) + else: + raise Exception("Could Not Parse LDAP User: {}".format(user)) + + +def extract_dynamic_field_from_filter(user, filtr): + match = re.search("([a-zA-Z0-9-]+)=%s", filtr, re.IGNORECASE | re.UNICODE) + if match: + return match.group(1) + else: + raise Exception("Could Not Parse LDAP Userfield: {}", user) + + +def extract_user_identifier(user, filtr): + dynamic_field = extract_dynamic_field_from_filter(user, filtr) + return extract_user_data_from_field(user, dynamic_field) diff --git a/cps/cache_buster.py b/cps/cache_buster.py index 99614dfa..8c521fe1 100644 --- a/cps/cache_buster.py +++ b/cps/cache_buster.py @@ -49,7 +49,7 @@ def init_cache_busting(app): # compute version component rooted_filename = os.path.join(dirpath, filename) with open(rooted_filename, 'rb') as f: - file_hash = hashlib.md5(f.read()).hexdigest()[:7] + file_hash = hashlib.md5(f.read()).hexdigest()[:7] # nosec # save version to tables file_path = rooted_filename.replace(static_folder, "") @@ -64,6 +64,7 @@ def init_cache_busting(app): return filename.split("?", 1)[0] @app.url_defaults + # pylint: disable=unused-variable def reverse_to_cache_busted_url(endpoint, values): """ Make `url_for` produce busted filenames when using the 'static' endpoint. diff --git a/cps/cli.py b/cps/cli.py index c94cb89d..d7dc596b 100644 --- a/cps/cli.py +++ b/cps/cli.py @@ -45,6 +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') args = parser.parse_args() if sys.version_info < (3, 0): @@ -70,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 == "": @@ -80,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): @@ -90,23 +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_password = args.s or None +user_credentials = args.s or None +if user_credentials and ":" not in user_credentials: + print("No valid 'username:password' format") + sys.exit(3) + +# Handles enabling of filepicker +filepicker = args.f or None diff --git a/cps/comic.py b/cps/comic.py index 7e3c7d47..462c11f0 100644 --- a/cps/comic.py +++ b/cps/comic.py @@ -18,21 +18,21 @@ from __future__ import division, print_function, unicode_literals import os -import io from . import logger, isoLanguages from .constants import BookMeta -try: - from PIL import Image as PILImage - use_PIL = True -except ImportError as e: - use_PIL = False - log = logger.create() +try: + from wand.image import Image + use_IM = True +except (ImportError, RuntimeError) as e: + use_IM = False + + try: from comicapi.comicarchive import ComicArchive, MetaDataStyle use_comic_meta = True @@ -52,25 +52,63 @@ except (ImportError, LookupError) as e: use_rarfile = False use_comic_meta = False +NO_JPEG_EXTENSIONS = ['.png', '.webp', '.bmp'] +COVER_EXTENSIONS = ['.png', '.webp', '.bmp', '.jpg', '.jpeg'] + def _cover_processing(tmp_file_name, img, extension): - if use_PIL: + tmp_cover_name = os.path.join(os.path.dirname(tmp_file_name), 'cover.jpg') + if use_IM: # convert to jpg because calibre only supports jpg - if extension in ('.png', '.webp'): - imgc = PILImage.open(io.BytesIO(img)) - im = imgc.convert('RGB') - tmp_bytesio = io.BytesIO() - im.save(tmp_bytesio, format='JPEG') - img = tmp_bytesio.getvalue() + if extension in NO_JPEG_EXTENSIONS: + with Image(filename=tmp_file_name) as imgc: + imgc.format = 'jpeg' + imgc.transform_colorspace('rgb') + imgc.save(tmp_cover_name) + return tmp_cover_name if not img: return None - tmp_cover_name = os.path.join(os.path.dirname(tmp_file_name), 'cover.jpg') with open(tmp_cover_name, 'wb') as f: f.write(img) return tmp_cover_name +def _extract_Cover_from_archive(original_file_extension, tmp_file_name, rarExecutable): + cover_data = None + if original_file_extension.upper() == '.CBZ': + cf = zipfile.ZipFile(tmp_file_name) + for name in cf.namelist(): + ext = os.path.splitext(name) + if len(ext) > 1: + extension = ext[1].lower() + if extension in COVER_EXTENSIONS: + cover_data = cf.read(name) + break + elif original_file_extension.upper() == '.CBT': + cf = tarfile.TarFile(tmp_file_name) + for name in cf.getnames(): + ext = os.path.splitext(name) + if len(ext) > 1: + extension = ext[1].lower() + if extension in COVER_EXTENSIONS: + cover_data = cf.extractfile(name).read() + break + elif original_file_extension.upper() == '.CBR' and use_rarfile: + try: + rarfile.UNRAR_TOOL = rarExecutable + cf = rarfile.RarFile(tmp_file_name) + for name in cf.getnames(): + ext = os.path.splitext(name) + if len(ext) > 1: + extension = ext[1].lower() + if extension in COVER_EXTENSIONS: + cover_data = cf.read(name) + break + except Exception as ex: + log.debug('Rarfile failed with error: %s', ex) + return cover_data + def _extractCover(tmp_file_name, original_file_extension, rarExecutable): cover_data = extension = None @@ -80,41 +118,11 @@ def _extractCover(tmp_file_name, original_file_extension, rarExecutable): ext = os.path.splitext(name) if len(ext) > 1: extension = ext[1].lower() - if extension in ('.jpg', '.jpeg', '.png', '.webp'): + if extension in COVER_EXTENSIONS: cover_data = archive.getPage(index) break else: - if original_file_extension.upper() == '.CBZ': - cf = zipfile.ZipFile(tmp_file_name) - for name in cf.namelist(): - ext = os.path.splitext(name) - if len(ext) > 1: - extension = ext[1].lower() - if extension in ('.jpg', '.jpeg', '.png', '.webp'): - cover_data = cf.read(name) - break - elif original_file_extension.upper() == '.CBT': - cf = tarfile.TarFile(tmp_file_name) - for name in cf.getnames(): - ext = os.path.splitext(name) - if len(ext) > 1: - extension = ext[1].lower() - if extension in ('.jpg', '.jpeg', '.png', '.webp'): - cover_data = cf.extractfile(name).read() - break - elif original_file_extension.upper() == '.CBR' and use_rarfile: - try: - rarfile.UNRAR_TOOL = rarExecutable - cf = rarfile.RarFile(tmp_file_name) - for name in cf.getnames(): - ext = os.path.splitext(name) - if len(ext) > 1: - extension = ext[1].lower() - if extension in ('.jpg', '.jpeg', '.png', '.webp'): - cover_data = cf.read(name) - break - except Exception as e: - log.debug('Rarfile failed with error: %s', e) + cover_data = _extract_Cover_from_archive(original_file_extension, tmp_file_name, rarExecutable) return _cover_processing(tmp_file_name, cover_data, extension) @@ -139,13 +147,15 @@ def get_comic_info(tmp_file_path, original_file_name, original_file_extension, r file_path=tmp_file_path, extension=original_file_extension, title=loadedMetadata.title or original_file_name, - author=" & ".join([credit["person"] for credit in loadedMetadata.credits if credit["role"] == "Writer"]) or u'Unknown', + author=" & ".join([credit["person"] + for credit in loadedMetadata.credits if credit["role"] == "Writer"]) or u'Unknown', cover=_extractCover(tmp_file_path, original_file_extension, rarExecutable), description=loadedMetadata.comments or "", tags="", series=loadedMetadata.series or "", series_id=loadedMetadata.issue or "", - languages=loadedMetadata.language) + languages=loadedMetadata.language, + publisher="") return BookMeta( file_path=tmp_file_path, @@ -157,4 +167,5 @@ def get_comic_info(tmp_file_path, original_file_name, original_file_extension, r tags="", series="", series_id="", - languages="") + languages="", + publisher="") diff --git a/cps/config_sql.py b/cps/config_sql.py index 877ad1c2..ef90aee4 100644 --- a/cps/config_sql.py +++ b/cps/config_sql.py @@ -20,11 +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.ext.declarative import declarative_base +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() @@ -34,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 @@ -53,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) @@ -65,7 +74,7 @@ class _Settings(_Base): config_random_books = Column(Integer, default=4) config_authors_max = Column(Integer, default=0) config_read_column = Column(Integer, default=0) - config_title_regex = Column(String, default=u'^(A|The|An|Der|Die|Das|Den|Ein|Eine|Einen|Dem|Des|Einem|Eines)\s+') + config_title_regex = Column(String, default=r'^(A|The|An|Der|Die|Das|Den|Ein|Eine|Einen|Dem|Des|Einem|Eines)\s+') config_mature_content_tags = Column(String, default='') config_theme = Column(Integer, default=0) @@ -145,15 +154,16 @@ class _ConfigSQL(object): self.load() change = False - if self.config_converterpath == None: + if self.config_converterpath == None: # pylint: disable=access-member-before-definition change = True self.config_converterpath = autodetect_calibre_binary() - if self.config_kepubifypath == None: + if self.config_kepubifypath == None: # pylint: disable=access-member-before-definition + change = True self.config_kepubifypath = autodetect_kepubify_binary() - if self.config_rarfile_location == None: + if self.config_rarfile_location == None: # pylint: disable=access-member-before-definition change = True self.config_rarfile_location = autodetect_unrar_binary() if change: @@ -180,8 +190,9 @@ class _ConfigSQL(object): return None return self.config_keyfile - def get_config_ipaddress(self): - return cli.ipadress or "" + @staticmethod + def get_config_ipaddress(): + return cli.ip_address or "" def _has_role(self, role_flag): return constants.has_flag(self.config_default_role, role_flag) @@ -239,18 +250,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__: @@ -267,10 +278,17 @@ 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 + def toDict(self): + storage = {} + for k, v in self.__dict__.items(): + if k[0] != '_' and not k.endswith("password") and not k.endswith("secret"): + storage[k] = v + return storage + + def load(self): '''Load all configuration values from the underlying storage.''' s = self._read_from_storage() # type: _Settings @@ -290,12 +308,20 @@ 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(',')] - 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 self._session.merge(s) - self._session.commit() + try: + self._session.commit() + except OperationalError as e: + log.error('Database error: %s', e) + self._session.rollback() def save(self): '''Apply all configuration values to the underlying storage.''' @@ -309,7 +335,11 @@ class _ConfigSQL(object): log.debug("_ConfigSQL updating storage") self._session.merge(s) - self._session.commit() + try: + self._session.commit() + except OperationalError as e: + log.error('Database error: %s', e) + self._session.rollback() self.load() def invalidate(self, error=None): @@ -328,7 +358,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): @@ -338,19 +368,29 @@ 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: - session.commit() + try: + session.commit() + except OperationalError: + session.rollback() def autodetect_calibre_binary(): @@ -403,12 +443,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 c1bcbe59..e9c26cb1 100644 --- a/cps/constants.py +++ b/cps/constants.py @@ -21,7 +21,11 @@ import sys import os from collections import namedtuple -HOME_CONFIG = False +# 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')) + +#In executables updater is not available, so variable is set to False there +UPDATER_AVAILABLE = True # Base dir is parent of current file, necessary if called from different folder if sys.version_info < (3, 0): @@ -40,7 +44,7 @@ if HOME_CONFIG: os.makedirs(home_dir) CONFIG_DIR = os.environ.get('CALIBRE_DBPATH', home_dir) else: - CONFIG_DIR = os.environ.get('CALIBRE_DBPATH', BASE_DIR) + CONFIG_DIR = os.environ.get('CALIBRE_DBPATH', BASE_DIR) ROLE_USER = 0 << 0 @@ -84,6 +88,26 @@ SIDEBAR_ARCHIVED = 1 << 15 SIDEBAR_DOWNLOAD = 1 << 16 SIDEBAR_LIST = 1 << 17 +sidebar_settings = { + "detail_random": DETAIL_RANDOM, + "sidebar_language": SIDEBAR_LANGUAGE, + "sidebar_series": SIDEBAR_SERIES, + "sidebar_category": SIDEBAR_CATEGORY, + "sidebar_random": SIDEBAR_RANDOM, + "sidebar_author": SIDEBAR_AUTHOR, + "sidebar_best_rated": SIDEBAR_BEST_RATED, + "sidebar_read_and_unread": SIDEBAR_READ_AND_UNREAD, + "sidebar_recent": SIDEBAR_RECENT, + "sidebar_sorted": SIDEBAR_SORTED, + "sidebar_publisher": SIDEBAR_PUBLISHER, + "sidebar_rating": SIDEBAR_RATING, + "sidebar_format": SIDEBAR_FORMAT, + "sidebar_archived": SIDEBAR_ARCHIVED, + "sidebar_download": SIDEBAR_DOWNLOAD, + "sidebar_list": SIDEBAR_LIST, + } + + ADMIN_USER_ROLES = sum(r for r in ALL_ROLES.values()) & ~ROLE_ANONYMOUS ADMIN_USER_SIDEBAR = (SIDEBAR_LIST << 1) - 1 @@ -102,7 +126,7 @@ LDAP_AUTH_SIMPLE = 0 DEFAULT_MAIL_SERVER = "mail.example.org" -DEFAULT_PASSWORD = "admin123" +DEFAULT_PASSWORD = "admin123" # nosec DEFAULT_PORT = 8083 env_CALIBRE_PORT = os.environ.get("CALIBRE_PORT", DEFAULT_PORT) try: @@ -128,9 +152,9 @@ def selected_roles(dictionary): # :rtype: BookMeta BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, description, tags, series, ' - 'series_id, languages') + 'series_id, languages, publisher') -STABLE_VERSION = {'version': '0.6.10 Beta'} +STABLE_VERSION = {'version': '0.6.12 Beta'} NIGHTLY_VERSION = {} NIGHTLY_VERSION[0] = '$Format:%H$' diff --git a/cps/db.py b/cps/db.py index f648a794..39adcd4b 100644 --- a/cps/db.py +++ b/cps/db.py @@ -30,11 +30,17 @@ from sqlalchemy import Table, Column, ForeignKey, CheckConstraint 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 declarative_base, DeclarativeMeta +from sqlalchemy.ext.declarative import DeclarativeMeta +from sqlalchemy.exc import OperationalError +try: + # Compatibility with sqlalchemy 2.0 + from sqlalchemy.orm import declarative_base +except ImportError: + from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.pool import StaticPool -from flask_login import current_user from sqlalchemy.sql.expression import and_, true, false, text, func, or_ from sqlalchemy.ext.associationproxy import association_proxy +from flask_login import current_user from babel import Locale as LC from babel.core import UnknownLocaleError from flask_babel import gettext as _ @@ -50,6 +56,8 @@ try: except ImportError: use_unidecode = False +log = logger.create() + cc_exceptions = ['datetime', 'comments', 'composite', 'series'] cc_classes = {} @@ -113,6 +121,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": @@ -140,6 +150,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": @@ -154,10 +166,8 @@ class Identifiers(Base): return u"https://portal.issn.org/resource/ISSN/{0}".format(self.val) elif format_type == "isfdb": return u"http://www.isfdb.org/cgi-bin/pl.cgi?{0}".format(self.val) - elif format_type == "url": - return u"{0}".format(self.val) else: - return u"" + return u"{0}".format(self.val) class Comments(Base): @@ -326,7 +336,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') @@ -384,14 +393,14 @@ class Custom_Columns(Base): class AlchemyEncoder(json.JSONEncoder): - def default(self, obj): - if isinstance(obj.__class__, DeclarativeMeta): + def default(self, o): + if isinstance(o.__class__, DeclarativeMeta): # an SQLAlchemy class fields = {} - for field in [x for x in dir(obj) 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 = obj.__getattribute__(field) + data = o.__getattribute__(field) try: if isinstance(data, str): data = data.replace("'", "\'") @@ -402,18 +411,21 @@ class AlchemyEncoder(json.JSONEncoder): el.append(ele.get()) else: el.append(json.dumps(ele, cls=AlchemyEncoder)) - data = ",".join(el) + if field == 'authors': + data = " & ".join(el) + else: + data = ",".join(el) if data == '[]': data = "" else: json.dumps(data) fields[field] = data - except: + except Exception: fields[field] = "" # a json-encodable dict return fields - return json.JSONEncoder.default(self, obj) + return json.JSONEncoder.default(self, o) class CalibreDB(): @@ -425,25 +437,96 @@ class CalibreDB(): # instances alive once they reach the end of their respective scopes instances = WeakSet() - def __init__(self): + def __init__(self, expire_on_commit=True): """ Initialize a new CalibreDB session """ self.session = None if self._init: - self.initSession() + self.initSession(expire_on_commit) self.instances.add(self) - - def initSession(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_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) + 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')) + + return cc_classes + @classmethod def setup_db(cls, config, 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() return False @@ -459,84 +542,24 @@ class CalibreDB(): isolation_level="SERIALIZABLE", connect_args={'check_same_thread': False}, poolclass=StaticPool) - cls.engine.execute("attach database '{}' as calibre;".format(dbpath)) - cls.engine.execute("attach database '{}' as app_settings;".format(app_db_path)) + with cls.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))) 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: + config.invalidate(ex) return False config.db_configured = True if not cc_classes: - cc = conn.execute("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("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, @@ -557,8 +580,8 @@ class CalibreDB(): def get_book_by_uuid(self, book_uuid): return self.session.query(Books).filter(Books.uuid == book_uuid).first() - def get_book_format(self, book_id, format): - return self.session.query(Data).filter(Data.book == book_id).filter(Data.format == format).first() + def get_book_format(self, book_id, file_format): + return self.session.query(Data).filter(Data.book == book_id).filter(Data.format == file_format).first() # Language and content filters for displaying in the UI def common_filters(self, allow_show_archived=False): @@ -597,6 +620,22 @@ class CalibreDB(): 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) @@ -608,19 +647,29 @@ class CalibreDB(): randm = self.session.query(Books) \ .filter(self.common_filters(allow_show_archived)) \ .order_by(func.random()) \ - .limit(self.config.config_random_books) + .limit(self.config.config_random_books).all() else: randm = false() off = int(int(pagesize) * (page - 1)) - query = self.session.query(database) \ - .join(*join, isouter=True) \ - .filter(db_filter) \ + query = self.session.query(database) + 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]) + query = query.filter(db_filter)\ .filter(self.common_filters(allow_show_archived)) - pagination = Pagination(page, pagesize, - len(query.all())) - entries = query.order_by(*order).offset(off).limit(pagesize).all() - for book in entries: - book = self.order_authors(book) + entries = list() + pagination = list() + try: + pagination = Pagination(page, pagesize, + len(query.all())) + entries = query.order_by(*order).offset(off).limit(pagesize).all() + except Exception as ex: + log.debug_or_exception(ex) + #for book in entries: + # book = self.order_authors(book) return entries, randm, pagination # Orders all Authors in the list according to authors sort @@ -660,23 +709,33 @@ 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) == 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) @@ -731,7 +790,7 @@ class CalibreDB(): if old_session: try: old_session.close() - except: + except Exception: pass if old_session.bind: try: @@ -762,7 +821,7 @@ class CalibreDB(): def lcase(s): try: return unidecode.unidecode(s.lower()) - except Exception as e: + except Exception as ex: log = logger.create() - log.exception(e) + log.debug_or_exception(ex) return s.lower() diff --git a/cps/debug_info.py b/cps/debug_info.py index 75ef3bb8..8f0cdeee 100644 --- a/cps/debug_info.py +++ b/cps/debug_info.py @@ -21,7 +21,12 @@ import shutil import glob import zipfile import json -import io +from io import BytesIO +try: + from StringIO import StringIO +except ImportError: + from io import StringIO + import os from flask import send_file @@ -32,11 +37,12 @@ from .about import collect_stats log = logger.create() def assemble_logfiles(file_name): - log_list = glob.glob(file_name + '*') - wfd = io.StringIO() + log_list = sorted(glob.glob(file_name + '*'), reverse=True) + wfd = StringIO() for f in log_list: with open(f, 'r') as fd: shutil.copyfileobj(fd, wfd) + wfd.seek(0) return send_file(wfd, as_attachment=True, attachment_filename=os.path.basename(file_name)) @@ -44,8 +50,12 @@ def assemble_logfiles(file_name): def send_debug(): file_list = glob.glob(logger.get_logfile(config.config_logfile) + '*') file_list.extend(glob.glob(logger.get_accesslogfile(config.config_access_logfile) + '*')) - memory_zip = io.BytesIO() + for element in [logger.LOG_TO_STDOUT, logger.LOG_TO_STDERR]: + if element in file_list: + file_list.remove(element) + memory_zip = BytesIO() with zipfile.ZipFile(memory_zip, 'w', compression=zipfile.ZIP_DEFLATED) as zf: + zf.writestr('settings.txt', json.dumps(config.toDict())) zf.writestr('libs.txt', json.dumps(collect_stats())) for fp in file_list: zf.write(fp, os.path.basename(fp)) diff --git a/cps/editbooks.py b/cps/editbooks.py index 7d207ed3..45e0f9fe 100644 --- a/cps/editbooks.py +++ b/cps/editbooks.py @@ -27,33 +27,50 @@ import json from shutil import copyfile from uuid import uuid4 +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 -from sqlalchemy.exc import OperationalError - +from sqlalchemy.exc import OperationalError, IntegrityError +from sqlite3 import OperationalError as sqliteOperationalError from . import constants, logger, isoLanguages, gdriveutils, uploader, helper from . import config, get_locale, ub, db from . import calibre_db from .services.worker import WorkerThread from .tasks.upload import TaskUpload -from .web import login_required_if_no_ano, render_title_template, edit_required, upload_required +from .render_template import render_title_template +from .usermanagement import login_required_if_no_ano + +try: + from functools import wraps +except ImportError: + pass # We're not using Python 3 editbook = Blueprint('editbook', __name__) log = logger.create() -# 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 upload_required(f): + @wraps(f) + def inner(*args, **kwargs): + if current_user.role_upload() or current_user.role_admin(): + return f(*args, **kwargs) + abort(403) + + return inner + +def edit_required(f): + @wraps(f) + def inner(*args, **kwargs): + if current_user.role_edit() or current_user.role_admin(): + return f(*args, **kwargs) + abort(403) + + return inner + +def search_objects_remove(db_book_object, db_type, input_elements): del_elements = [] for c_elements in db_book_object: found = False @@ -71,7 +88,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 @@ -87,64 +107,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 @@ -177,93 +229,63 @@ 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_book(book_id, book_format, jsonResponse): - warning = {} - if current_user.role_delete_books(): - book = calibre_db.get_book(book_id) - if book: - try: - 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", - "format": "", - "error": 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"), - "type": "warning", - "format": "", - "error": error} - else: - flash(error, category="warning") - if not book_format: - # delete book from Shelfs, Downloads, Read list - ub.session.query(ub.BookShelf).filter(ub.BookShelf.book_id == book_id).delete() - ub.session.query(ub.ReadBook).filter(ub.ReadBook.book_id == book_id).delete() - ub.delete_download(book_id) - ub.session.commit() - # check if only this book links to: - # author, language, series, tags, custom columns - modify_database_object([u''], book.authors, db.Authors, calibre_db.session, 'author') - modify_database_object([u''], book.tags, db.Tags, calibre_db.session, 'tags') - modify_database_object([u''], book.series, db.Series, calibre_db.session, 'series') - modify_database_object([u''], book.languages, db.Languages, calibre_db.session, 'languages') - modify_database_object([u''], book.publishers, db.Publishers, calibre_db.session, 'publishers') +def delete_whole_book(book_id, book): + # delete book from Shelfs, Downloads, Read list + ub.session.query(ub.BookShelf).filter(ub.BookShelf.book_id == book_id).delete() + ub.session.query(ub.ReadBook).filter(ub.ReadBook.book_id == book_id).delete() + ub.delete_download(book_id) + ub.session_commit() - cc = calibre_db.session.query(db.Custom_Columns).\ - filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all() - for c in cc: - cc_string = "custom_column_" + str(c.id) - if not c.is_multiple: - if len(getattr(book, cc_string)) > 0: - if c.datatype == 'bool' or c.datatype == 'integer' or c.datatype == 'float': - del_cc = getattr(book, cc_string)[0] - getattr(book, cc_string).remove(del_cc) - log.debug('remove ' + str(c.id)) - calibre_db.session.delete(del_cc) - calibre_db.session.commit() - elif c.datatype == 'rating': - del_cc = getattr(book, cc_string)[0] - getattr(book, cc_string).remove(del_cc) - if len(del_cc.books) == 0: - log.debug('remove ' + str(c.id)) - calibre_db.session.delete(del_cc) - calibre_db.session.commit() - else: - del_cc = getattr(book, cc_string)[0] - getattr(book, cc_string).remove(del_cc) - log.debug('remove ' + str(c.id)) - calibre_db.session.delete(del_cc) - calibre_db.session.commit() - else: - modify_database_object([u''], getattr(book, cc_string), db.cc_classes[c.id], - calibre_db.session, 'custom') - calibre_db.session.query(db.Books).filter(db.Books.id == book_id).delete() + # check if only this book links to: + # author, language, series, tags, custom columns + modify_database_object([u''], book.authors, db.Authors, calibre_db.session, 'author') + modify_database_object([u''], book.tags, db.Tags, calibre_db.session, 'tags') + modify_database_object([u''], book.series, db.Series, calibre_db.session, 'series') + modify_database_object([u''], book.languages, db.Languages, calibre_db.session, 'languages') + modify_database_object([u''], book.publishers, db.Publishers, calibre_db.session, 'publishers') + + cc = calibre_db.session.query(db.Custom_Columns). \ + filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all() + for c in cc: + cc_string = "custom_column_" + str(c.id) + if not c.is_multiple: + if len(getattr(book, cc_string)) > 0: + if c.datatype == 'bool' or c.datatype == 'integer' or c.datatype == 'float': + del_cc = getattr(book, cc_string)[0] + getattr(book, cc_string).remove(del_cc) + log.debug('remove ' + str(c.id)) + calibre_db.session.delete(del_cc) + calibre_db.session.commit() + elif c.datatype == 'rating': + del_cc = getattr(book, cc_string)[0] + getattr(book, cc_string).remove(del_cc) + if len(del_cc.books) == 0: + log.debug('remove ' + str(c.id)) + calibre_db.session.delete(del_cc) + calibre_db.session.commit() else: - 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.exception(e) - calibre_db.session.rollback() + del_cc = getattr(book, cc_string)[0] + getattr(book, cc_string).remove(del_cc) + log.debug('remove ' + str(c.id)) + calibre_db.session.delete(del_cc) + calibre_db.session.commit() else: - # book not found - log.error('Book with id "%s" could not be deleted: not found', book_id) + modify_database_object([u''], getattr(book, cc_string), db.cc_classes[c.id], + calibre_db.session, 'custom') + calibre_db.session.query(db.Books).filter(db.Books.id == book_id).delete() + + +def render_delete_book_result(book_format, jsonResponse, warning, book_id): if book_format: if jsonResponse: return json.dumps([warning, {"location": url_for("editbook.edit_book", book_id=book_id), @@ -284,10 +306,57 @@ def delete_book(book_id, book_format, jsonResponse): return redirect(url_for('web.index')) +def delete_book(book_id, book_format, jsonResponse): + warning = {} + if current_user.role_delete_books(): + book = calibre_db.get_book(book_id) + if book: + try: + 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", book_id=book_id), + "type": "danger", + "format": "", + "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", book_id=book_id), + "type": "warning", + "format": "", + "message": error} + else: + flash(error, category="warning") + if not book_format: + delete_whole_book(book_id, book) + else: + 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 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) + return render_delete_book_result(book_format, jsonResponse, warning, book_id) + + def render_edit_book(book_id): - calibre_db.update_title_sort(config) 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) + 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") return redirect(url_for("web.index")) @@ -389,7 +458,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: @@ -398,7 +467,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 @@ -411,10 +483,10 @@ def edit_book_languages(languages, book, upload=False): return modify_database_object(input_l, book.languages, db.Languages, calibre_db.session, 'languages') -def edit_book_publisher(to_save, book): +def edit_book_publisher(publishers, book): changed = False - if to_save["publisher"]: - publisher = to_save["publisher"].rstrip().strip() + if publishers: + publisher = publishers.rstrip().strip() if len(book.publishers) == 0 or (len(book.publishers) > 0 and publisher != book.publishers[0].name): changed |= modify_database_object([publisher], book.publishers, db.Publishers, calibre_db.session, 'publisher') @@ -423,6 +495,59 @@ def edit_book_publisher(to_save, book): return changed +def edit_cc_data_number(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 + + if to_save[cc_string] != cc_db_value: + if cc_db_value is not None: + if to_save[cc_string] is not None: + setattr(getattr(book, cc_string)[0], 'value', to_save[cc_string]) + changed = True + else: + del_cc = getattr(book, cc_string)[0] + getattr(book, cc_string).remove(del_cc) + calibre_db.session.delete(del_cc) + changed = True + else: + cc_class = db.cc_classes[c.id] + new_cc = cc_class(value=to_save[cc_string], book=book_id) + calibre_db.session.add(new_cc) + changed = True + return changed, to_save + + +def edit_cc_data_string(book, c, to_save, cc_db_value, cc_string): + changed = False + if c.datatype == 'rating': + to_save[cc_string] = str(int(float(to_save[cc_string]) * 2)) + if to_save[cc_string].strip() != cc_db_value: + if cc_db_value is not None: + # remove old cc_val + del_cc = getattr(book, cc_string)[0] + getattr(book, cc_string).remove(del_cc) + if len(del_cc.books) == 0: + calibre_db.session.delete(del_cc) + changed = True + cc_class = db.cc_classes[c.id] + new_cc = calibre_db.session.query(cc_class).filter( + cc_class.value == to_save[cc_string].strip()).first() + # if no cc val is found add it + if new_cc is None: + new_cc = cc_class(value=to_save[cc_string].strip()) + calibre_db.session.add(new_cc) + changed = True + calibre_db.session.flush() + new_cc = calibre_db.session.query(cc_class).filter( + cc_class.value == to_save[cc_string].strip()).first() + # add cc value to book + getattr(book, cc_string).append(new_cc) + return changed, to_save + + def edit_cc_data(book_id, book, to_save): changed = False cc = calibre_db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all() @@ -435,51 +560,9 @@ def edit_cc_data(book_id, book, to_save): cc_db_value = None if to_save[cc_string].strip(): if c.datatype == 'int' or c.datatype == 'bool' or c.datatype == 'float': - 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 - - if to_save[cc_string] != cc_db_value: - if cc_db_value is not None: - if to_save[cc_string] is not None: - setattr(getattr(book, cc_string)[0], 'value', to_save[cc_string]) - changed = True - else: - del_cc = getattr(book, cc_string)[0] - getattr(book, cc_string).remove(del_cc) - calibre_db.session.delete(del_cc) - changed = True - else: - cc_class = db.cc_classes[c.id] - new_cc = cc_class(value=to_save[cc_string], book=book_id) - calibre_db.session.add(new_cc) - changed = True - + changed, to_save = edit_cc_data_number(book_id, book, c, to_save, cc_db_value, cc_string) else: - if c.datatype == 'rating': - to_save[cc_string] = str(int(float(to_save[cc_string]) * 2)) - if to_save[cc_string].strip() != cc_db_value: - if cc_db_value is not None: - # remove old cc_val - del_cc = getattr(book, cc_string)[0] - getattr(book, cc_string).remove(del_cc) - if len(del_cc.books) == 0: - calibre_db.session.delete(del_cc) - changed = True - cc_class = db.cc_classes[c.id] - new_cc = calibre_db.session.query(cc_class).filter( - cc_class.value == to_save[cc_string].strip()).first() - # if no cc val is found add it - if new_cc is None: - new_cc = cc_class(value=to_save[cc_string].strip()) - calibre_db.session.add(new_cc) - changed = True - calibre_db.session.flush() - new_cc = calibre_db.session.query(cc_class).filter( - cc_class.value == to_save[cc_string].strip()).first() - # add cc value to book - getattr(book, cc_string).append(new_cc) + changed, to_save = edit_cc_data_string(book, c, to_save, cc_db_value, cc_string) else: if cc_db_value is not None: # remove old cc_val @@ -545,7 +628,7 @@ def upload_single_file(request, book, book_id): calibre_db.session.add(db_format) calibre_db.session.commit() calibre_db.update_title_sort(config) - except OperationalError as e: + except (OperationalError, IntegrityError) as e: calibre_db.session.rollback() log.error('Database error: %s', e) flash(_(u"Database error: %(error)s.", error=e), category="error") @@ -553,7 +636,7 @@ def upload_single_file(request, book, 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( + WorkerThread.add(current_user.name, TaskUpload( "" + uploadText + "")) return uploader.process( @@ -577,17 +660,63 @@ 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 def edit_book(book_id): modif_date = False + + # create the function for sorting... + try: + calibre_db.update_title_sort(config) + except sqliteOperationalError as e: + log.debug_or_exception(e) + calibre_db.session.rollback() + # Show form if request.method != 'POST': return render_edit_book(book_id) - # create the function for sorting... - calibre_db.update_title_sort(config) book = calibre_db.get_filtered_book(book_id, allow_show_archived=True) # Book not found @@ -605,41 +734,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() @@ -664,10 +766,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) - # Handle identifiers input_identifiers = identifier_list(to_save, book) modification, warning = modify_identifiers(input_identifiers, book.identifiers, calibre_db.session) @@ -676,9 +776,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: @@ -688,18 +795,6 @@ def edit_book(book_id): else: book.pubdate = db.Books.DEFAULT_PUBDATE - # handle book publisher - modif_date |= edit_book_publisher(to_save, 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) @@ -715,8 +810,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.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)) @@ -735,6 +830,7 @@ def merge_metadata(to_save, meta): to_save["description"] = to_save["description"] or Markup( getattr(meta, 'description', '')).unescape() + def identifier_list(to_save, book): """Generate a list of Identifiers from form information""" id_type_prefix = 'identifier-type-' @@ -749,6 +845,130 @@ def identifier_list(to_save, book): result.append(db.Identifiers(to_save[val_key], type_value, book.id)) return result + +def prepare_authors_on_upload(title, authr): + if title != _(u'Unknown') and authr != _(u'Unknown'): + entry = calibre_db.check_exists_book(authr, title) + if entry: + log.info("Uploaded book probably exists in library") + flash(_(u"Uploaded book probably exists in the library, consider to change before upload new: ") + + Markup(render_title_template('book_exists_flash.html', entry=entry)), category="warning") + + # handle authors + input_authors = authr.split('&') + # handle_authors(input_authors) + 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 + + sort_authors_list = list() + db_author = None + for inp in input_authors: + stored_author = calibre_db.session.query(db.Authors).filter(db.Authors.name == inp).first() + if not stored_author: + if not db_author: + db_author = db.Authors(inp, helper.get_sorted_author(inp), "") + calibre_db.session.add(db_author) + calibre_db.session.commit() + sort_author = helper.get_sorted_author(inp) + else: + if not db_author: + db_author = stored_author + sort_author = stored_author.sort + sort_authors_list.append(sort_author) + sort_authors = ' & '.join(sort_authors_list) + return sort_authors, input_authors, db_author + + +def create_book_on_upload(modif_date, meta): + title = meta.title + authr = meta.author + sort_authors, input_authors, db_author = prepare_authors_on_upload(title, authr) + + title_dir = helper.get_valid_filename(title) + author_dir = helper.get_valid_filename(db_author.name) + + # combine path and normalize path from windows systems + path = os.path.join(author_dir, title_dir).replace('\\', '/') + + # Calibre adds books with utc as timezone + db_book = db.Books(title, "", sort_authors, datetime.utcnow(), datetime(101, 1, 1), + '1', datetime.utcnow(), path, meta.cover, db_author, [], "") + + modif_date |= modify_database_object(input_authors, db_book.authors, db.Authors, calibre_db.session, + 'author') + + # Add series_index to book + modif_date |= edit_book_series_index(meta.series_id, db_book) + + # add languages + modif_date |= edit_book_languages(meta.languages, db_book, upload=True) + + # handle tags + modif_date |= edit_book_tags(meta.tags, db_book) + + # handle publisher + modif_date |= edit_book_publisher(meta.publisher, db_book) + + # handle series + modif_date |= edit_book_series(meta.series, db_book) + + # Add file to book + file_size = os.path.getsize(meta.file_path) + db_data = db.Data(db_book, meta.extension.upper()[1:], file_size, title_dir) + db_book.data.append(db_data) + calibre_db.session.add(db_book) + + # flush content, get db_book.id available + 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 @@ -763,98 +983,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') + meta, error = file_handling_on_upload(requested_file) + if error: + return error - # 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') - title = meta.title - authr = meta.author + db_book, input_authors, title_dir = create_book_on_upload(modif_date, meta) - if title != _(u'Unknown') and authr != _(u'Unknown'): - entry = calibre_db.check_exists_book(authr, title) - if entry: - log.info("Uploaded book probably exists in library") - flash(_(u"Uploaded book probably exists in the library, consider to change before upload new: ") - + Markup(render_title_template('book_exists_flash.html', entry=entry)), category="warning") - - # handle authors - input_authors = authr.split('&') - # handle_authors(input_authors) - 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 - - sort_authors_list=list() - db_author = None - for inp in input_authors: - stored_author = calibre_db.session.query(db.Authors).filter(db.Authors.name == inp).first() - if not stored_author: - if not db_author: - db_author = db.Authors(inp, helper.get_sorted_author(inp), "") - calibre_db.session.add(db_author) - calibre_db.session.commit() - sort_author = helper.get_sorted_author(inp) - else: - if not db_author: - db_author = stored_author - sort_author = stored_author.sort - sort_authors_list.append(sort_author) - sort_authors = ' & '.join(sort_authors_list) - - title_dir = helper.get_valid_filename(title) - author_dir = helper.get_valid_filename(db_author.name) - - # combine path and normalize path from windows systems - path = os.path.join(author_dir, title_dir).replace('\\', '/') - # Calibre adds books with utc as timezone - db_book = db.Books(title, "", sort_authors, datetime.utcnow(), datetime(101, 1, 1), - '1', datetime.utcnow(), path, meta.cover, db_author, [], "") - - modif_date |= modify_database_object(input_authors, db_book.authors, db.Authors, calibre_db.session, - 'author') - - # Add series_index to book - modif_date |= edit_book_series_index(meta.series_id, db_book) - - # add languages - modif_date |= edit_book_languages(meta.languages, db_book, upload=True) - - # handle tags - modif_date |= edit_book_tags(meta.tags, db_book) - - # handle series - modif_date |= edit_book_series(meta.series, db_book) - - # Add file to book - file_size = os.path.getsize(meta.file_path) - db_data = db.Data(db_book, meta.extension.upper()[1:], file_size, title_dir) - db_book.data.append(db_data) - calibre_db.session.add(db_book) - - # flush content, get db_book.id available - calibre_db.session.flush() - - # 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 @@ -866,21 +1001,7 @@ def upload(): meta.file_path, title_dir + meta.extension) - # 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() @@ -890,7 +1011,7 @@ def upload(): if error: flash(error, category="error") uploadText=_(u"File %(file)s uploaded", file=title) - WorkerThread.add(current_user.nickname, TaskUpload( + WorkerThread.add(current_user.name, TaskUpload( "" + uploadText + "")) if len(request.files.getlist("btn-upload")) < 2: @@ -900,7 +1021,7 @@ def upload(): else: resp = {"location": url_for('web.show_book', book_id=book_id)} return Response(json.dumps(resp), mimetype='application/json') - except OperationalError as e: + except (OperationalError, IntegrityError) as e: calibre_db.session.rollback() log.error("Database error: %s", e) flash(_(u"Database error: %(error)s.", error=e), category="error") @@ -920,7 +1041,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", @@ -930,61 +1051,89 @@ 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("/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 "" + # 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() + 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 "" @@ -1036,6 +1185,6 @@ 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 "" diff --git a/cps/epub.py b/cps/epub.py index 583e4eda..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,20 +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']) + 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 new file mode 100644 index 00000000..373a1434 --- /dev/null +++ b/cps/error_handler.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- + +# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) +# Copyright (C) 2018-2020 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 . + +import traceback +from flask import render_template +from werkzeug.exceptions import default_exceptions +try: + from werkzeug.exceptions import FailedDependency +except ImportError: + from werkzeug.exceptions import UnprocessableEntity as FailedDependency + +from . import config, app, logger, services + + +log = logger.create() + +# custom error page +def error_http(error): + return render_template('http_error.html', + error_code="Error {0}".format(error.code), + error_name=error.name, + issue=False, + instance=config.config_calibre_web_title + ), error.code + + +def internal_error(error): + return render_template('http_error.html', + error_code="Internal Server Error", + error_name=str(error), + issue=True, + error_stack=traceback.format_exc().split("\n"), + instance=config.config_calibre_web_title + ), 500 + +def init_errorhandler(): + # http error handling + for ex in default_exceptions: + if ex < 500: + app.register_error_handler(ex, error_http) + elif ex == 500: + app.register_error_handler(ex, internal_error) + + + if services.ldap: + # Only way of catching the LDAPException upon logging in with LDAP server down + @app.errorhandler(services.ldap.LDAPException) + # pylint: disable=unused-variable + def handle_exception(e): + log.debug('LDAP server not accessible while trying to login to opds feed') + return error_http(FailedDependency()) + diff --git a/cps/fb2.py b/cps/fb2.py index bdb3d1d5..d7b03d5b 100644 --- a/cps/fb2.py +++ b/cps/fb2.py @@ -30,51 +30,52 @@ def get_fb2_info(tmp_file_path, original_file_extension): } fb2_file = open(tmp_file_path) - tree = etree.fromstring(fb2_file.read()) + tree = etree.fromstring(fb2_file.read().encode()) authors = tree.xpath('/fb:FictionBook/fb:description/fb:title-info/fb:author', namespaces=ns) def get_author(element): last_name = element.xpath('fb:last-name/text()', namespaces=ns) if len(last_name): - last_name = last_name[0].encode('utf-8') + last_name = last_name[0] else: last_name = u'' middle_name = element.xpath('fb:middle-name/text()', namespaces=ns) if len(middle_name): - middle_name = middle_name[0].encode('utf-8') + middle_name = middle_name[0] else: middle_name = u'' first_name = element.xpath('fb:first-name/text()', namespaces=ns) if len(first_name): - first_name = first_name[0].encode('utf-8') + first_name = first_name[0] else: first_name = u'' - return (first_name.decode('utf-8') + u' ' - + middle_name.decode('utf-8') + u' ' - + last_name.decode('utf-8')).encode('utf-8') + return (first_name + u' ' + + middle_name + u' ' + + last_name) author = str(", ".join(map(get_author, authors))) title = tree.xpath('/fb:FictionBook/fb:description/fb:title-info/fb:book-title/text()', namespaces=ns) if len(title): - title = str(title[0].encode('utf-8')) + title = str(title[0]) else: title = u'' description = tree.xpath('/fb:FictionBook/fb:description/fb:publish-info/fb:book-name/text()', namespaces=ns) if len(description): - description = str(description[0].encode('utf-8')) + description = str(description[0]) else: description = u'' return BookMeta( file_path=tmp_file_path, extension=original_file_extension, - title=title.decode('utf-8'), - author=author.decode('utf-8'), + title=title, + author=author, cover=None, - description=description.decode('utf-8'), + description=description, tags="", series="", series_id="", - languages="") + languages="", + publisher="") diff --git a/cps/gdrive.py b/cps/gdrive.py index 90b83e64..6d6dfd07 100644 --- a/cps/gdrive.py +++ b/cps/gdrive.py @@ -35,9 +35,9 @@ from flask_babel import gettext as _ from flask_login import login_required from . import logger, gdriveutils, config, ub, calibre_db -from .web import admin_required +from .admin import admin_required -gdrive = Blueprint('gdrive', __name__) +gdrive = Blueprint('gdrive', __name__, url_prefix='/gdrive') log = logger.create() try: @@ -47,10 +47,10 @@ except ImportError as err: current_milli_time = lambda: int(round(time() * 1000)) -gdrive_watch_callback_token = 'target=calibreweb-watch_files' +gdrive_watch_callback_token = 'target=calibreweb-watch_files' #nosec -@gdrive.route("/gdrive/authenticate") +@gdrive.route("/authenticate") @login_required @admin_required def authenticate_google_drive(): @@ -63,7 +63,7 @@ def authenticate_google_drive(): return redirect(authUrl) -@gdrive.route("/gdrive/callback") +@gdrive.route("/callback") def google_drive_callback(): auth_code = request.args.get('code') if not auth_code: @@ -77,18 +77,14 @@ def google_drive_callback(): return redirect(url_for('admin.configuration')) -@gdrive.route("/gdrive/watch/subscribe") +@gdrive.route("/watch/subscribe") @login_required @admin_required def watch_gdrive(): if not config.config_google_drive_watch_changes_response: with open(gdriveutils.CLIENT_SECRETS, 'r') as settings: filedata = json.load(settings) - if filedata['web']['redirect_uris'][0].endswith('/'): - filedata['web']['redirect_uris'][0] = filedata['web']['redirect_uris'][0][:-((len('/gdrive/callback')+1))] - else: - filedata['web']['redirect_uris'][0] = filedata['web']['redirect_uris'][0][:-(len('/gdrive/callback'))] - address = '%s/gdrive/watch/callback' % filedata['web']['redirect_uris'][0] + address = filedata['web']['redirect_uris'][0].rstrip('/').replace('/gdrive/callback', '/gdrive/watch/callback') notification_id = str(uuid4()) try: result = gdriveutils.watchChange(gdriveutils.Gdrive.Instance().drive, notification_id, @@ -98,14 +94,15 @@ def watch_gdrive(): except HttpError as e: reason=json.loads(e.content)['error']['errors'][0] if reason['reason'] == u'push.webhookUrlUnauthorized': - flash(_(u'Callback domain is not verified, please follow steps to verify domain in google developer console'), category="error") + flash(_(u'Callback domain is not verified, ' + u'please follow steps to verify domain in google developer console'), category="error") else: flash(reason['message'], category="error") return redirect(url_for('admin.configuration')) -@gdrive.route("/gdrive/watch/revoke") +@gdrive.route("/watch/revoke") @login_required @admin_required def revoke_watch_gdrive(): @@ -121,14 +118,14 @@ def revoke_watch_gdrive(): return redirect(url_for('admin.configuration')) -@gdrive.route("/gdrive/watch/callback", methods=['GET', 'POST']) +@gdrive.route("/watch/callback", methods=['GET', 'POST']) def on_received_watch_confirmation(): if not config.config_google_drive_watch_changes_response: return '' if request.headers.get('X-Goog-Channel-Token') != gdrive_watch_callback_token \ or request.headers.get('X-Goog-Resource-State') != 'change' \ or not request.data: - return '' # redirect(url_for('admin.configuration')) + return '' log.debug('%r', request.headers) log.debug('%r', request.data) @@ -145,16 +142,19 @@ def on_received_watch_confirmation(): else: dbpath = os.path.join(config.config_calibre_dir, "metadata.db").encode() if not response['deleted'] and response['file']['title'] == 'metadata.db' \ - and response['file']['md5Checksum'] != hashlib.md5(dbpath): - tmpDir = tempfile.gettempdir() + and response['file']['md5Checksum'] != hashlib.md5(dbpath): # nosec + tmp_dir = os.path.join(tempfile.gettempdir(), 'calibre_web') + if not os.path.isdir(tmp_dir): + os.mkdir(tmp_dir) + log.info('Database file updated') - copyfile(dbpath, os.path.join(tmpDir, "metadata.db_" + str(current_milli_time()))) + copyfile(dbpath, os.path.join(tmp_dir, "metadata.db_" + str(current_milli_time()))) log.info('Backing up existing and downloading updated metadata.db') - gdriveutils.downloadFile(None, "metadata.db", os.path.join(tmpDir, "tmp_metadata.db")) + gdriveutils.downloadFile(None, "metadata.db", os.path.join(tmp_dir, "tmp_metadata.db")) log.info('Setting up new DB') - # prevent error on windows, as os.rename does on exisiting files - move(os.path.join(tmpDir, "tmp_metadata.db"), dbpath) + # 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.exception(e) + except Exception as ex: + log.debug_or_exception(ex) return '' diff --git a/cps/gdriveutils.py b/cps/gdriveutils.py index 3e00c9af..13f83bd5 100644 --- a/cps/gdriveutils.py +++ b/cps/gdriveutils.py @@ -28,20 +28,33 @@ from sqlalchemy import create_engine from sqlalchemy import Column, UniqueConstraint from sqlalchemy import String, Integer from sqlalchemy.orm import sessionmaker, scoped_session -from sqlalchemy.ext.declarative import declarative_base +try: + # Compatibility with sqlalchemy 2.0 + from sqlalchemy.orm import declarative_base +except ImportError: + from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.exc import OperationalError, InvalidRequestError try: - from pydrive.auth import GoogleAuth - from pydrive.drive import GoogleDrive - from pydrive.auth import RefreshError from apiclient import errors from httplib2 import ServerNotFoundError - gdrive_support = True importError = None -except ImportError as err: - importError = err + gdrive_support = True +except ImportError as e: + importError = e gdrive_support = False +try: + from pydrive2.auth import GoogleAuth + from pydrive2.drive import GoogleDrive + from pydrive2.auth import RefreshError +except ImportError as err: + try: + from pydrive.auth import GoogleAuth + from pydrive.drive import GoogleDrive + from pydrive.auth import RefreshError + except ImportError as err: + importError = err + gdrive_support = False from . import logger, cli, config from .constants import CONFIG_DIR as _CONFIG_DIR @@ -91,7 +104,7 @@ class Singleton: except AttributeError: self._instance = self._decorated() return self._instance - except ImportError as e: + except (ImportError, NameError) as e: log.debug(e) return None @@ -189,8 +202,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.exception(e) + except Exception as ex: + log.debug_or_exception(ex) else: # Initialize the saved creds gauth.Authorize() @@ -208,7 +221,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 as e: + except (ServerNotFoundError, ssl.SSLError) as e: log.info("GDrive Error %s" % e) fileList = [] return fileList @@ -244,7 +257,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 @@ -259,37 +277,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 @@ -333,7 +356,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() @@ -385,7 +408,8 @@ def uploadFileToEbooksFolder(destFile, f): if len(existingFiles) > 0: driveFile = existingFiles[0] else: - driveFile = drive.CreateFile({'title': x, 'parents': [{"kind": "drive#fileLink", 'id': parent['id']}],}) + driveFile = drive.CreateFile({'title': x, + 'parents': [{"kind": "drive#fileLink", 'id': parent['id']}], }) driveFile.SetContentFile(f) driveFile.Upload() else: @@ -483,8 +507,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 @@ -493,9 +517,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(): @@ -510,13 +535,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 @@ -533,7 +568,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 @@ -547,21 +587,24 @@ def partial(total_byte_len, part_size_limit): return s # downloads files in chunks from gdrive -def do_gdrive_download(df, headers): +def do_gdrive_download(df, headers, convert_encoding=False): total_size = int(df.metadata.get('fileSize')) download_url = df.metadata.get('downloadUrl') s = partial(total_size, 1024 * 1024) # I'm downloading BIG files, so 100M chunk size is fine for me - def stream(): + def stream(convert_encoding): for byte in s: headers = {"Range": 'bytes=%s-%s' % (byte[0], byte[1])} resp, content = df.auth.Get_Http_Object().request(download_url, headers=headers) if resp.status == 206: + if convert_encoding: + result = chardet.detect(content) + content = content.decode(result['encoding']).encode('utf-8') yield content else: log.warning('An error occurred: %s', resp) return - return Response(stream_with_context(stream()), headers=headers) + return Response(stream_with_context(stream(convert_encoding)), headers=headers) _SETTINGS_YAML_TEMPLATE = """ diff --git a/cps/helper.py b/cps/helper.py index 9845df97..8495687c 100644 --- a/cps/helper.py +++ b/cps/helper.py @@ -24,10 +24,7 @@ import io import mimetypes import re import shutil -import glob import time -import zipfile -import json import unicodedata from datetime import datetime, timedelta from tempfile import gettempdir @@ -35,10 +32,10 @@ from tempfile import gettempdir import requests from babel.dates import format_datetime from babel.units import format_unit -from flask import send_from_directory, make_response, redirect, abort, url_for, send_file +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 @@ -53,13 +50,6 @@ try: except ImportError: use_unidecode = False -try: - from PIL import Image as PILImage - from PIL import UnidentifiedImageError - use_PIL = True -except ImportError: - use_PIL = False - from . import calibre_db from .tasks.convert import TaskConvert from . import logger, config, get_locale, db, ub @@ -69,9 +59,17 @@ from .subproc_wrapper import process_wait from .services.worker import WorkerThread, STAT_WAITING, STAT_FAIL, STAT_STARTED, STAT_FINISH_SUCCESS from .tasks.mail import TaskEmail - log = logger.create() +try: + from wand.image import Image + from wand.exceptions import MissingDelegateError + use_IM = True +except (ImportError, RuntimeError) as e: + log.debug('Cannot import Image, generating covers from non jpg files will not work: %s', e) + use_IM = False + MissingDelegateError = BaseException + # Convert existing book entry to new format def convert_book_format(book_id, calibrepath, old_book_format, new_book_format, user_id, kindle_mail=None): @@ -112,21 +110,21 @@ def convert_book_format(book_id, calibrepath, old_book_format, new_book_format, def send_test_mail(kindle_mail, user_name): WorkerThread.add(user_name, TaskEmail(_(u'Calibre-Web test e-mail'), None, None, config.get_mail_settings(), kindle_mail, _(u"Test e-mail"), - _(u'This e-mail has been sent via Calibre-Web.'))) + _(u'This e-mail has been sent via Calibre-Web.'))) return # Send registration email or password reset email, depending on parameter resend (False means welcome email) def send_registration_mail(e_mail, user_name, default_password, resend=False): - text = "Hello %s!\r\n" % user_name + txt = "Hello %s!\r\n" % user_name if not resend: - text += "Your new account at Calibre-Web has been created. Thanks for joining us!\r\n" - text += "Please log in to your account using the following informations:\r\n" - text += "User name: %s\r\n" % user_name - text += "Password: %s\r\n" % default_password - text += "Don't forget to change your password after first login.\r\n" - text += "Sincerely\r\n\r\n" - text += "Your Calibre-Web team" + txt += "Your new account at Calibre-Web has been created. Thanks for joining us!\r\n" + txt += "Please log in to your account using the following informations:\r\n" + txt += "User name: %s\r\n" % user_name + txt += "Password: %s\r\n" % default_password + txt += "Don't forget to change your password after first login.\r\n" + txt += "Sincerely\r\n\r\n" + txt += "Your Calibre-Web team" WorkerThread.add(None, TaskEmail( subject=_(u'Get Started with Calibre-Web'), filepath=None, @@ -134,64 +132,52 @@ def send_registration_mail(e_mail, user_name, default_password, resend=False): settings=config.get_mail_settings(), recipient=e_mail, taskMessage=_(u"Registration e-mail for user: %(name)s", name=user_name), - text=text + text=txt )) - return +def check_send_to_kindle_with_converter(formats): + bookformats = list() + if 'EPUB' in formats and 'MOBI' not in formats: + bookformats.append({'format': 'Mobi', + 'convert': 1, + 'text': _('Convert %(orig)s to %(format)s and send to Kindle', + orig='Epub', + format='Mobi')}) + if 'AZW3' in formats and not 'MOBI' in formats: + bookformats.append({'format': 'Mobi', + 'convert': 2, + 'text': _('Convert %(orig)s to %(format)s and send to Kindle', + orig='Azw3', + format='Mobi')}) + return bookformats + + def check_send_to_kindle(entry): """ returns all available book formats for sending to Kindle """ + formats = list() + bookformats = list() if len(entry.data): - bookformats = list() - if not config.config_converterpath: - # no converter - only for mobi and pdf formats - for ele in iter(entry.data): - if ele.uncompressed_size < config.mail_size: - if 'MOBI' in ele.format: - bookformats.append({'format': 'Mobi', - 'convert': 0, - 'text': _('Send %(format)s to Kindle', format='Mobi')}) - if 'PDF' in ele.format: - bookformats.append({'format': 'Pdf', - 'convert': 0, - 'text': _('Send %(format)s to Kindle', format='Pdf')}) - if 'AZW' in ele.format: - bookformats.append({'format': 'Azw', - 'convert': 0, - 'text': _('Send %(format)s to Kindle', format='Azw')}) - else: - formats = list() - for ele in iter(entry.data): - if ele.uncompressed_size < config.mail_size: - formats.append(ele.format) - if 'MOBI' in formats: - bookformats.append({'format': 'Mobi', - 'convert': 0, - 'text': _('Send %(format)s to Kindle', format='Mobi')}) - if 'AZW' in formats: - bookformats.append({'format': 'Azw', - 'convert': 0, - 'text': _('Send %(format)s to Kindle', format='Azw')}) - if 'PDF' in formats: - bookformats.append({'format': 'Pdf', - 'convert': 0, - 'text': _('Send %(format)s to Kindle', format='Pdf')}) - if config.config_converterpath: - if 'EPUB' in formats and not 'MOBI' in formats: - bookformats.append({'format': 'Mobi', - 'convert':1, - 'text': _('Convert %(orig)s to %(format)s and send to Kindle', - orig='Epub', - format='Mobi')}) - if 'AZW3' in formats and not 'MOBI' in formats: - bookformats.append({'format': 'Mobi', - 'convert': 2, - 'text': _('Convert %(orig)s to %(format)s and send to Kindle', - orig='Azw3', - format='Mobi')}) + for ele in iter(entry.data): + if ele.uncompressed_size < config.mail_size: + formats.append(ele.format) + if 'MOBI' in formats: + bookformats.append({'format': 'Mobi', + 'convert': 0, + 'text': _('Send %(format)s to Kindle', format='Mobi')}) + if 'PDF' in formats: + bookformats.append({'format': 'Pdf', + 'convert': 0, + 'text': _('Send %(format)s to Kindle', format='Pdf')}) + if 'AZW' in formats: + bookformats.append({'format': 'Azw', + 'convert': 0, + 'text': _('Send %(format)s to Kindle', format='Azw')}) + if config.config_converterpath: + bookformats.extend(check_send_to_kindle_with_converter(formats)) return bookformats else: log.error(u'Cannot find book entry %d', entry.id) @@ -201,7 +187,7 @@ def check_send_to_kindle(entry): # Check if a reader is existing for any of the book formats, if not, return empty list, otherwise return # list with supported formats def check_read_formats(entry): - EXTENSIONS_READER = {'TXT', 'PDF', 'EPUB', 'CBZ', 'CBT', 'CBR'} + EXTENSIONS_READER = {'TXT', 'PDF', 'EPUB', 'CBZ', 'CBT', 'CBR', 'DJVU'} bookformats = list() if len(entry.data): for ele in iter(entry.data): @@ -494,8 +480,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 @@ -512,11 +498,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 ################################# @@ -564,9 +576,8 @@ def get_book_cover_internal(book, use_generic_cover_on_failure): else: log.error('%s/cover.jpg not found on Google Drive', book.path) return get_cover_on_failure(use_generic_cover_on_failure) - except Exception as e: - log.exception(e) - # traceback.print_exc() + except Exception as ex: + log.debug_or_exception(ex) return get_cover_on_failure(use_generic_cover_on_failure) else: cover_file_path = os.path.join(config.config_calibre_dir, book.path) @@ -589,29 +600,35 @@ def save_cover_from_url(url, book_path): requests.exceptions.Timeout) as ex: log.info(u'Cover Download Error %s', ex) return False, _("Error Downloading Cover") - except UnidentifiedImageError as ex: + except MissingDelegateError as ex: log.info(u'File Format Error %s', ex) return False, _("Cover Format Error") def save_cover_from_filestorage(filepath, saved_filename, img): - if hasattr(img, '_content'): - f = open(os.path.join(filepath, saved_filename), "wb") - f.write(img._content) - f.close() - else: - # check if file path exists, otherwise create it, copy file to calibre path and delete temp file - if not os.path.exists(filepath): - try: - os.makedirs(filepath) - except OSError: - log.error(u"Failed to create path for cover") - return False, _(u"Failed to create path for cover") + # check if file path exists, otherwise create it, copy file to calibre path and delete temp file + if not os.path.exists(filepath): try: - img.save(os.path.join(filepath, saved_filename)) - except (IOError, OSError): - log.error(u"Cover-file is not a valid image file, or could not be stored") - return False, _(u"Cover-file is not a valid image file, or could not be stored") + os.makedirs(filepath) + except OSError: + log.error(u"Failed to create path for cover") + return False, _(u"Failed to create path for cover") + try: + # upload of jgp file without wand + if isinstance(img, requests.Response): + with open(os.path.join(filepath, saved_filename), 'wb') as f: + f.write(img.content) + else: + if hasattr(img, "metadata"): + # upload of jpg/png... via url + img.save(filename=os.path.join(filepath, saved_filename)) + img.close() + else: + # upload of jpg/png... from hdd + img.save(os.path.join(filepath, saved_filename)) + except (IOError, OSError): + log.error(u"Cover-file is not a valid image file, or could not be stored") + return False, _(u"Cover-file is not a valid image file, or could not be stored") return True, None @@ -619,31 +636,33 @@ def save_cover_from_filestorage(filepath, saved_filename, img): def save_cover(img, book_path): content_type = img.headers.get('content-type') - if use_PIL: - if content_type not in ('image/jpeg', 'image/png', 'image/webp'): - log.error("Only jpg/jpeg/png/webp files are supported as coverfile") - return False, _("Only jpg/jpeg/png/webp files are supported as coverfile") + if use_IM: + if content_type not in ('image/jpeg', 'image/png', 'image/webp', 'image/bmp'): + log.error("Only jpg/jpeg/png/webp/bmp files are supported as coverfile") + return False, _("Only jpg/jpeg/png/webp/bmp files are supported as coverfile") # convert to jpg because calibre only supports jpg - if content_type in ('image/png', 'image/webp'): + if content_type != 'image/jpg': if hasattr(img, 'stream'): - imgc = PILImage.open(img.stream) + imgc = Image(blob=img.stream) else: - imgc = PILImage.open(io.BytesIO(img.content)) - im = imgc.convert('RGB') - tmp_bytesio = io.BytesIO() - im.save(tmp_bytesio, format='JPEG') - img._content = tmp_bytesio.getvalue() + imgc = Image(blob=io.BytesIO(img.content)) + imgc.format = 'jpeg' + imgc.transform_colorspace("rgb") + img = imgc else: if content_type not in 'image/jpeg': log.error("Only jpg/jpeg files are supported as coverfile") return False, _("Only jpg/jpeg files are supported as coverfile") if config.config_use_google_drive: - tmpDir = gettempdir() - ret, message = save_cover_from_filestorage(tmpDir, "uploaded_cover.jpg", img) + tmp_dir = os.path.join(gettempdir(), 'calibre_web') + + if not os.path.isdir(tmp_dir): + os.mkdir(tmp_dir) + ret, message = save_cover_from_filestorage(tmp_dir, "uploaded_cover.jpg", img) if ret is True: - gd.uploadFileToEbooksFolder(os.path.join(book_path, 'cover.jpg'), - os.path.join(tmpDir, "uploaded_cover.jpg")) + gd.uploadFileToEbooksFolder(os.path.join(book_path, 'cover.jpg').replace("\\","/"), + os.path.join(tmp_dir, "uploaded_cover.jpg")) log.info("Cover is saved on Google Drive") return True, None else: @@ -674,6 +693,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 ################################## @@ -697,7 +717,7 @@ def check_unrar(unrarLocation): log.debug("unrar version %s", version) break except (OSError, UnicodeDecodeError) as err: - log.exception(err) + log.debug_or_exception(err) return _('Error excecuting UnRar') @@ -713,7 +733,6 @@ def json_serial(obj): 'seconds': obj.seconds, 'microseconds': obj.microseconds, } - # return obj.isoformat() raise TypeError("Type %s not serializable" % type(obj)) @@ -737,8 +756,8 @@ def format_runtime(runtime): # helper function to apply localize status information in tasklist entries def render_task_status(tasklist): renderedtasklist = list() - for num, user, added, task in tasklist: - if user == current_user.nickname or current_user.role_admin(): + for __, user, __, task in tasklist: + 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()) @@ -776,8 +795,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): @@ -811,6 +830,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 @@ -827,4 +847,3 @@ def get_download_link(book_id, book_format, client): return do_download_file(book, book_format, client, data1, headers) else: abort(404) - diff --git a/cps/isoLanguages.py b/cps/isoLanguages.py index 4c0aefc3..35d9f0a7 100644 --- a/cps/isoLanguages.py +++ b/cps/isoLanguages.py @@ -57,27 +57,30 @@ def get_language_name(locale, lang_code): def get_language_codes(locale, language_names, remainder=None): language_names = set(x.strip().lower() for x in language_names if x) - languages = list() + lang = list() for k, v in get_language_names(locale).items(): v = v.lower() if v in language_names: - languages.append(k) + 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 languages + return lang + + def get_valid_language_codes(locale, language_names, remainder=None): - languages = list() + lang = list() if "" in language_names: language_names.remove("") - for k, v in get_language_names(locale).items(): + for k, __ in get_language_names(locale).items(): if k in language_names: - languages.append(k) + lang.append(k) language_names.remove(k) if remainder is not None and len(language_names): remainder.extend(language_names) - return languages + return lang + def get_lang3(lang): try: diff --git a/cps/iso_language_names.py b/cps/iso_language_names.py index 52e50667..73e6f326 100644 --- a/cps/iso_language_names.py +++ b/cps/iso_language_names.py @@ -14,425 +14,845 @@ from __future__ import unicode_literals # map iso639 language codes to language names, translated LANGUAGE_NAMES = { - "pl": { - "aar": "afarski", - "abk": "abchaski", - "ace": "aczineski", - "ach": "aczoli", - "ada": "adangme", - "ady": "adygejski", - "afh": "afrihili", - "afr": "afrykanerski", - "ain": "ajnoski (Japonia)", - "aka": "akan", - "akk": "akadyjski", - "ale": "aleucki", - "alt": "ałtajski południowy", - "amh": "amharski", - "ang": "Staroangielski (ok. 450-1100)", + "cs": { + "aar": "afarština", + "abk": "abchazajština", + "ace": "atěžština", + "ach": "ačoli (luoština)", + "ada": "adangmeština", + "ady": "adyghe", + "afh": "afrihilijština", + "afr": "afrikánština", + "ain": "ainu (Japonsko)", + "aka": "akanština", + "akk": "akkadština", + "ale": "aleutština", + "alt": "altajština; jižní", + "amh": "Amharština", + "ang": "Angličtina; stará (asi 450-1100)", "anp": "angika", - "ara": "arabski", - "arc": "aramejski oficjalny (700-300 p.n.e.)", - "arg": "aragoński", - "arn": "araukański", + "ara": "arabština", + "arc": "aramejština; oficiální (700-300 př. n. l.)", + "arg": "aragonská španělština", + "arn": "mapudungun", "arp": "arapaho", - "arw": "arawak", - "asm": "asamski", - "ast": "asturyjski", - "ava": "awarski", - "ave": "awestyjski", - "awa": "awadhi", - "aym": "ajmara", - "aze": "azerski", - "bak": "baszkirski", - "bal": "baluczi", - "bam": "bambara", - "ban": "balijski", + "arw": "arawacké jazyky", + "asm": "ásámština", + "ast": "asturština", + "ava": "avarština", + "ave": "avestština", + "awa": "avadhština (avadhí)", + "aym": "aymarština", + "aze": "azerbajdžánština", + "bak": "Baskirština", + "bal": "balúčština", + "bam": "bambarština", + "ban": "balijština", "bas": "basa (Kamerun)", - "bej": "bedża", - "bel": "białoruski", - "bem": "bemba (Zambia)", - "ben": "bengalski", - "bho": "bhodźpuri", - "bik": "bikol", - "bin": "edo", - "bis": "bislama", + "bej": "bedža", + "bel": "běloruština", + "bem": "bemba (Zambie)", + "ben": "bengálština", + "bho": "bhódžpurština", + "bik": "bikolština", + "bin": "bini", + "bis": "bislamština", "bla": "siksika", - "bod": "tybetański", - "bos": "bośniacki", - "bra": "bradź", - "bre": "bretoński", - "bua": "buriacki", - "bug": "bugijski", - "bul": "bułgarski", - "byn": "blin", - "cad": "kaddo", - "car": "karaibski galibi", - "cat": "kataloński", - "ceb": "cebuański", - "ces": "czeski", - "cha": "czamorro", - "chb": "czibcza", - "che": "czeczeński", - "chg": "czagatajski", - "chk": "chuuk", - "chm": "maryjski (Rosja)", - "chn": "żargon chinoocki", - "cho": "czoktaw", - "chp": "chipewyan", - "chr": "czerokeski", - "chu": "starosłowiański", - "chv": "czuwaski", - "chy": "czejeński", - "cop": "koptyjski", - "cor": "kornijski", - "cos": "korsykański", - "cre": "kri", - "crh": "krymskotatarski", - "csb": "kaszubski", - "cym": "walijski", - "dak": "dakota", - "dan": "duński", - "dar": "dargwijski", - "del": "delaware", - "den": "slavey (atapaskański)", - "deu": "niemiecki", + "bod": "tibetština", + "bos": "bosenština", + "bra": "bradžština", + "bre": "bretonština", + "bua": "burjatština", + "bug": "bugiština", + "bul": "bulharština", + "byn": "bilin", + "cad": "caddo", + "car": "carib; Galibi", + "cat": "katalánština", + "ceb": "cebuánština", + "ces": "Čeština", + "cha": "čamoro", + "chb": "čibča", + "che": "čečenština", + "chg": "Chagatai", + "chk": "čukčtina", + "chm": "Mari (Russia)", + "chn": "činuk pidžin", + "cho": "choctawština", + "chp": "čipeva", + "chr": "čerokézština", + "chu": "Slavonic; Old", + "chv": "čuvaština", + "chy": "čejenština", + "cop": "koptština", + "cor": "kornština", + "cos": "korsičtina", + "cre": "krí", + "crh": "Turkish; Crimean", + "csb": "kašubština", + "cym": "velština", + "dak": "dakotština", + "dan": "dánština", + "dar": "dargwa", + "del": "delawarština", + "den": "atabaské jazyky", + "deu": "Němčina", "dgr": "dogrib", - "din": "dinka", - "div": "malediwski; divehi", - "doi": "dogri (makrojęzyk)", - "dsb": "dolnołużycki", - "dua": "duala", - "dum": "holenderski średniowieczny (ok. 1050-1350)", - "dyu": "diula", - "dzo": "dzongka", + "din": "dinkština", + "div": "Dhivehi", + "doi": "Dogri (macrolanguage)", + "dsb": "Sorbian; Lower", + "dua": "dualština", + "dum": "Dutch; Middle (ca. 1050-1350)", + "dyu": "djula", + "dzo": "Bhútánština", "efi": "efik", - "egy": "egipski (starożytny)", + "egy": "egyptština (starověká)", "eka": "ekajuk", - "ell": "grecki współczesny (1453-)", - "elx": "elamicki", - "eng": "Angielski", - "enm": "angielski średniowieczny (1100-1500)", + "ell": "řečtina; moderní (1453-)", + "elx": "elamština", + "eng": "Angličtina", + "enm": "Angličtina; středověká (1100-1500)", "epo": "esperanto", - "est": "estoński", - "eus": "baskijski", - "ewe": "ewe", + "est": "estonština", + "eus": "baskičtina", + "ewe": "eweština", "ewo": "ewondo", - "fan": "fang (Gwinea Równikowa)", - "fao": "farerski", - "fas": "perski", - "fat": "fanti", - "fij": "fidżyjski", - "fil": "pilipino", - "fin": "fiński", - "fon": "fon", - "fra": "francuski", - "frm": "francuski średniowieczny (ok. 1400-1600)", - "fro": "starofrancuski (842-ok. 1400)", - "frr": "północnofryzyjski", - "frs": "wschodniofryzyjski", - "fry": "zachodniofryzyjski", - "ful": "fulani", - "fur": "friulski", + "fan": "Fang (Equatorial Guinea)", + "fao": "faerština", + "fas": "perština", + "fat": "fantiština", + "fij": "Fidži", + "fil": "Filipino", + "fin": "finština", + "fon": "fonština", + "fra": "francouzština", + "frm": "French; Middle (ca. 1400-1600)", + "fro": "French; Old (842-ca. 1400)", + "frr": "Frisian; Northern", + "frs": "Frisian; Eastern", + "fry": "Frisian; Western", + "ful": "fulahština", + "fur": "furlanština", "gaa": "ga", "gay": "gayo", - "gba": "gbaya (Republika Środkowoafrykańska)", - "gez": "gyyz", - "gil": "gilbertański", - "gla": "szkocki gaelicki", - "gle": "irlandzki", - "glg": "galicyjski", - "glv": "manx", - "gmh": "średnio-wysoko-niemiecki (ok. 1050-1500)", - "goh": "staro-wysoko-niemiecki (ok. 750-1050)", - "gon": "gondi", + "gba": "Gbaya (Central African Republic)", + "gez": "etiopština", + "gil": "kiribatština", + "gla": "Gaelic; Scottish", + "gle": "irština", + "glg": "Galician", + "glv": "manština", + "gmh": "German; Middle High (ca. 1050-1500)", + "goh": "German; Old High (ca. 750-1050)", + "gon": "góndština", "gor": "gorontalo", - "got": "gocki", + "got": "gótština", "grb": "grebo", - "grc": "grecki starożytny (do 1453)", - "grn": "guarani", - "gsw": "niemiecki szwajcarski", - "guj": "gudźarati", - "gwi": "gwichʼin", + "grc": "řečtina; starověká (do 1453)", + "grn": "Guaranština", + "gsw": "German; Swiss", + "guj": "Gudžarátština", + "gwi": "Gwichʼin", "hai": "haida", - "hat": "kreolski haitański", - "hau": "hausa", - "haw": "hawajski", - "heb": "hebrajski", + "hat": "Creole; Haitian", + "hau": "Hausa", + "haw": "havajština", + "heb": "hebrejština", "her": "herero", - "hil": "hiligajnon", - "hin": "hindi", - "hit": "hetycki", - "hmn": "hmong", + "hil": "hiligayonština", + "hin": "hindština", + "hit": "chetitština", + "hmn": "hmongština", "hmo": "hiri motu", - "hrv": "chorwacki", - "hsb": "górnołużycki", - "hun": "węgierski", + "hrv": "chorvatština", + "hsb": "Sorbian; Upper", + "hun": "maďarština", "hup": "hupa", - "hye": "ormiański", - "iba": "ibanag", - "ibo": "ibo", + "hye": "arménština", + "iba": "iban", + "ibo": "igbo", "ido": "ido", - "iii": "syczuański", - "iku": "inuktitut", - "ile": "interlingue", - "ilo": "ilokano", - "ina": "interlingua (Międzynarodowe Stowarzyszenie Języka Pomocniczego)", - "ind": "indonezyjski", - "inh": "inguski", - "ipk": "inupiaq", - "isl": "islandzki", - "ita": "włoski", - "jav": "jawajski", + "iii": "Yi; Sichuan", + "iku": "Inuktitutština", + "ile": "Interlingue", + "ilo": "ilokánština", + "ina": "Interlingua (Mezinárodní pomocná jazyková asociace)", + "ind": "indonézština", + "inh": "inguština", + "ipk": "Inupiakština", + "isl": "islandština", + "ita": "italština", + "jav": "jávština", "jbo": "lojban", - "jpn": "japoński", - "jpr": "judeo-perski", - "jrb": "judeoarabski", - "kaa": "karakałpacki", - "kab": "kabylski", - "kac": "kaczin", - "kal": "kalaallisut", - "kam": "kamba (Kenia)", - "kan": "kannada", - "kas": "kaszmirski", - "kat": "gruziński", + "jpn": "japonština", + "jpr": "judeo-perština", + "jrb": "judeo-arabština", + "kaa": "karakalpačtina", + "kab": "kabulí", + "kac": "kačjinština", + "kal": "Kalaallisut", + "kam": "Kamba (Kenya)", + "kan": "Kannadština", + "kas": "kašmírština", + "kat": "Gruzínština", "kau": "kanuri", "kaw": "kawi", - "kaz": "kazaski", - "kbd": "kabardyjski", - "kha": "khasi", - "khm": "środkowokhmerski", - "kho": "chotański", - "kik": "kikiju", - "kin": "ruanda", - "kir": "kirgiski", - "kmb": "kimbundu", - "kok": "konkani (makrojęzyk)", - "kom": "komi", - "kon": "kongo", - "kor": "koreański", - "kos": "kosrae", + "kaz": "Kazachština", + "kbd": "kabardština", + "kha": "Khasi", + "khm": "Khmer; Central", + "kho": "chotánština", + "kik": "Kikuyu", + "kin": "Kinyarwandština", + "kir": "Kirgizština", + "kmb": "kimbundština", + "kok": "Konkani (macrolanguage)", + "kom": "komijština", + "kon": "Kongo", + "kor": "korejština", + "kos": "kosrajština", "kpe": "kpelle", - "krc": "karaczajsko-bałkarski", - "krl": "karelski", + "krc": "karachay-balkarština", + "krl": "karelština", "kru": "kurukh", - "kua": "kwanyama", - "kum": "kumycki", - "kur": "kurdyjski", + "kua": "Kuanyama", + "kum": "kumyčtina", + "kur": "kurdština", "kut": "kutenai", "lad": "ladino", "lah": "lahnda", - "lam": "lamba", - "lao": "laotański", - "lat": "łaciński", - "lav": "łotewski", - "lez": "lezgiński", - "lim": "limburgijski", - "lin": "lingala", - "lit": "litewski", - "lol": "mongo", - "loz": "lozi", - "ltz": "luksemburski", - "lua": "luba-lulua", - "lub": "luba-katanga", - "lug": "luganda", - "lui": "luiseno", - "lun": "lunda", - "luo": "luo (Kenia i Tanzania)", - "lus": "lushai", - "mad": "madurajski", - "mag": "magahi", - "mah": "marshalski", - "mai": "maithili", - "mak": "makasar", - "mal": "malajalam", - "man": "mandingo", - "mar": "marathi", - "mas": "masajski", - "mdf": "moksza", - "mdr": "mandar", - "men": "mende (Sierra Leone)", - "mga": "irlandzki średniowieczny (900-1200)", - "mic": "micmac", + "lam": "lambština", + "lao": "Laoština", + "lat": "latina", + "lav": "Latvian", + "lez": "lezgiština", + "lim": "Limburgan", + "lin": "Ngalština", + "lit": "litevština", + "lol": "mongština", + "loz": "lozština", + "ltz": "Luxembourgish", + "lua": "luba-luluaština", + "lub": "lubu-katanžština", + "lug": "Ganda", + "lui": "luiseňo", + "lun": "lundština", + "luo": "luoština (Keňa a Tanzanie)", + "lus": "lušáí", + "mad": "madurština", + "mag": "magahština", + "mah": "Marshallese", + "mai": "maithilština", + "mak": "makasarština", + "mal": "Malabarština", + "man": "mandingština", + "mar": "maráthština", + "mas": "masajština", + "mdf": "moksha", + "mdr": "mandarínština", + "men": "Mende (Sierra Leone)", + "mga": "irština; středověká (900-1200)", + "mic": "Mi'kmaq", "min": "minangkabau", - "mis": "języki niezakodowane", - "mkd": "macedoński", - "mlg": "malgaski", - "mlt": "maltański", - "mnc": "mandżurski", - "mni": "manipuri", + "mis": "Uncoded languages", + "mkd": "makedonština", + "mlg": "Malgaština", + "mlt": "maltézština", + "mnc": "manchu", + "mni": "manipurština", "moh": "mohawk", - "mon": "mongolski", - "mos": "mossi", - "mri": "maoryski", - "msa": "malajski (makrojęzyk)", - "mul": "wiele języków", - "mus": "krik", - "mwl": "mirandyjski", - "mwr": "marwari", - "mya": "birmański", - "myv": "erzja", - "nap": "neapolitański", - "nau": "nauruański", - "nav": "navaho", - "nbl": "ndebele południowy", - "nde": "ndebele północny", - "ndo": "ndonga", + "mon": "mongolština", + "mos": "mosi", + "mri": "maorština", + "msa": "Malay (macrolanguage)", + "mul": "násobné jazyky", + "mus": "krík", + "mwl": "mirandština", + "mwr": "márvárština", + "mya": "Barmština", + "myv": "erzya", + "nap": "neapolština", + "nau": "naurština", + "nav": "navažština", + "nbl": "Ndebele; South", + "nde": "Ndebele; North", + "ndo": "ndondština", "nds": "German; Low", - "nep": "nepalski", - "new": "newarski", + "nep": "nepálština", + "new": "Bhasa; Nepal", "nia": "nias", "niu": "niue", - "nld": "holenderski", - "nno": "norweski Nynorsk", - "nob": "norweski Bokmål", - "nog": "nogajski", - "non": "staronordyjski", - "nor": "norweski", - "nqo": "n’ko", - "nso": "sotho północny", - "nwc": "newarski klasyczny", - "nya": "njandża", - "nym": "nyamwezi", - "nyn": "nyankole", - "nyo": "nyoro", - "nzi": "nzema", - "oci": "okcytański (po 1500)", - "oji": "odżibwe", + "nld": "holandština", + "nno": "Norwegian Nynorsk", + "nob": "Norwegian Bokmål", + "nog": "nogai", + "non": "norština; stará", + "nor": "norština", + "nqo": "N'Ko", + "nso": "Sotho; Northern", + "nwc": "Newari; Old", + "nya": "Nyanja", + "nym": "ňamwežština", + "nyn": "nyankolština", + "nyo": "Nyoro", + "nzi": "nzima", + "oci": "Occitan (post 1500)", + "oji": "Ojibwa", "ori": "orija", - "orm": "oromo", - "osa": "osage", - "oss": "osetyjski", - "ota": "turecki otomański (1500-1928)", - "pag": "pangasino", - "pal": "pahlawi", - "pam": "pampango", - "pan": "pendżabski", + "orm": "Oromo (Afan)", + "osa": "osagština", + "oss": "Ossetian", + "ota": "turečtina; osmanská (1500-1928)", + "pag": "pangsinan", + "pal": "pahlaví", + "pam": "pampangau", + "pan": "Panjabi", "pap": "papiamento", - "pau": "palau", - "peo": "staroperski (ok. 600-400 p.n.e)", - "phn": "fenicki", - "pli": "pali", - "pol": "Polski", - "pon": "pohnpei", - "por": "portugalski", - "pro": "prowansalski średniowieczny (do 1500)", - "pus": "paszto", - "que": "keczua", - "raj": "radźasthani", - "rap": "rapanui", - "rar": "maoryski Wysp Cooka", - "roh": "retoromański", - "rom": "romski", - "ron": "rumuński", - "run": "rundi", - "rup": "arumuński", - "rus": "rosyjski", - "sad": "sandawe", - "sag": "sango", - "sah": "jakucki", - "sam": "samarytański aramejski", - "san": "sanskryt", - "sas": "sasak", - "sat": "santali", - "scn": "sycylijski", - "sco": "scots", - "sel": "selkupski", - "sga": "staroirlandzki (do 900)", - "shn": "szan", + "pau": "palauština", + "peo": "Persian; Old (ca. 600-400 B.C.)", + "phn": "Slovinština", + "pli": "páli", + "pol": "Polština", + "pon": "pohnpeiština", + "por": "portugalština", + "pro": "provensálština; stará (do 1500)", + "pus": "pašto", + "que": "kečuánština", + "raj": "rádžasthánština", + "rap": "rapanuiština", + "rar": "Maori; Cook Islands", + "roh": "Romansh", + "rom": "římština", + "ron": "rumunština", + "run": "Kirundi", + "rup": "Romanian; Macedo-", + "rus": "Ruština", + "sad": "sandawština", + "sag": "sangoština", + "sah": "jakutština", + "sam": "Aramaic; Samaritan", + "san": "sanskrt", + "sas": "sačtina", + "sat": "santálí", + "scn": "sicilština", + "sco": "skotština", + "sel": "selkupština", + "sga": "irština; stará (do 900)", + "shn": "šanština", "sid": "sidamo", - "sin": "syngaleski", - "slk": "słowacki", - "slv": "słoweński", - "sma": "południowolapoński", - "sme": "północnolapoński", - "smj": "lapoński lule", - "smn": "lapoński inari", - "smo": "samoański", - "sms": "lapoński skolt", - "sna": "shona", - "snd": "sindhi", - "snk": "soninke", - "sog": "sogdiański", - "som": "somalijski", - "sot": "sotho południowy", - "spa": "hiszpański", - "sqi": "albański", - "srd": "sardyński", - "srn": "sranan tongo", - "srp": "serbski", - "srr": "serer", - "ssw": "suazi", + "sin": "Sinhálština", + "slk": "Slovenština", + "slv": "slovinština", + "sma": "Sami; Southern", + "sme": "Sami; Northern", + "smj": "lule sami", + "smn": "Sami; Inari", + "smo": "Samoyština", + "sms": "Sami; Skolt", + "sna": "šonština", + "snd": "sindhština", + "snk": "sonikština", + "sog": "sogdijština", + "som": "somálština", + "sot": "sotština; jižní", + "spa": "španělština", + "sqi": "albánština", + "srd": "sardinština", + "srn": "Sranan Tongo", + "srp": "srbština", + "srr": "Serer", + "ssw": "Siswatština", "suk": "sukuma", - "sun": "sundajski", + "sun": "Sundanština", "sus": "susu", - "sux": "sumeryjski", - "swa": "suahili (makrojęzyk)", - "swe": "szwedzki", - "syc": "syryjski klasyczny", - "syr": "syryjski", - "tah": "tahitański", - "tam": "tamilski", - "tat": "tatarski", - "tel": "telugu", + "sux": "sumerština", + "swa": "svahilština (makrojazyk)", + "swe": "švédština", + "syc": "Syriac; Classical", + "syr": "syrština", + "tah": "tahitština", + "tam": "Tamilština", + "tat": "tatarština", + "tel": "Telugu", "tem": "temne", "ter": "tereno", - "tet": "tetum", - "tgk": "tadżycki", - "tgl": "tagalski", - "tha": "tajski", - "tig": "tigre", - "tir": "tigrinia", - "tiv": "tiw", - "tkl": "tokelau", - "tlh": "klingoński", + "tet": "tetumština", + "tgk": "Tádžičtina", + "tgl": "Tagalog", + "tha": "thajština", + "tig": "tigrejština", + "tir": "Tigrinijština", + "tiv": "tivština", + "tkl": "tokelauština", + "tlh": "Klingon", "tli": "tlingit", - "tmh": "tuareski", - "tog": "tongański (Nyasa)", - "ton": "tongański (Wyspy Tonga)", + "tmh": "Tamashek", + "tog": "tongština (nyasa)", + "ton": "Tonga", "tpi": "tok pisin", - "tsi": "tsimszian", - "tsn": "tswana", - "tso": "tsonga", - "tuk": "turkmeński", - "tum": "tumbuka", - "tur": "turecki", - "tvl": "tuvalu", - "twi": "twi", - "tyv": "tuwiński", - "udm": "udmurcki", - "uga": "ugarycki", - "uig": "ujgurski", - "ukr": "ukraiński", + "tsi": "tsimshijské jazyky", + "tsn": "Setswanština", + "tso": "Tsonga", + "tuk": "turkmenistánština", + "tum": "tumbukština", + "tur": "turečtina", + "tvl": "tuvalština", + "twi": "ťwiština", + "tyv": "tuvština", + "udm": "udmurtština", + "uga": "ugaritština", + "uig": "Uighurština", + "ukr": "ukrajinština", "umb": "umbundu", - "und": "nieokreślony", - "urd": "urdu", - "uzb": "uzbecki", - "vai": "wai", - "ven": "venda", - "vie": "wietnamski", - "vol": "wolapik", - "vot": "wotycki", - "wal": "walamo", - "war": "warajski (Filipiny)", + "und": "neurčitý", + "urd": "urdština", + "uzb": "uzbekistánština", + "vai": "vai", + "ven": "vendština", + "vie": "vietnamština", + "vol": "volapük", + "vot": "votiatština", + "wal": "Wolaytta", + "war": "Waray (Philippines)", "was": "washo", - "wln": "waloński", - "wol": "wolof", - "xal": "kałmucki", - "xho": "xhosa", - "yao": "yao", - "yap": "japski", - "yid": "jidysz", - "yor": "joruba", - "zap": "zapotecki", - "zbl": "bliss", + "wln": "valonština", + "wol": "volofština", + "xal": "Kalmyk", + "xho": "xhoština", + "yao": "jaoština", + "yap": "japština", + "yid": "Jidiš", + "yor": "jorubština", + "zap": "Zapotec", + "zbl": "Blissymbols", "zen": "zenaga", - "zha": "zhuang", - "zho": "chiński", - "zul": "zuluski", - "zun": "zuni", - "zxx": "brak kontekstu językowego", - "zza": "zazaki" + "zha": "Zhuang", + "zho": "čínština", + "zul": "Zulu", + "zun": "zunijština", + "zxx": "bez lingvistického obsahu", + "zza": "zaza" + }, + "de": { + "aar": "Danakil-Sprache", + "abk": "Abchasisch", + "ace": "Aceh-Sprache", + "ach": "Acholi-Sprache", + "ada": "Adangme-Sprache", + "ady": "Adygisch", + "afh": "Afrihili", + "afr": "Afrikaans", + "ain": "Ainu-Sprache (Japan)", + "aka": "Akan-Sprache", + "akk": "Akkadisch", + "ale": "Aleutisch", + "alt": "Altaisch; Süd", + "amh": "Amharisch", + "ang": "Englisch; Alt (ca. 450-1100)", + "anp": "Anga-Sprache", + "ara": "Arabisch", + "arc": "Aramäisch", + "arg": "Aragonesisch", + "arn": "Mapudungun", + "arp": "Arapaho", + "arw": "Arawakisch", + "asm": "Assamesisch", + "ast": "Asturisch", + "ava": "Awarisch", + "ave": "Avestisch", + "awa": "Awadhi", + "aym": "Aymara", + "aze": "Aserbaidschanisch", + "bak": "Baschkirisch", + "bal": "Belutschisch", + "bam": "Bambara", + "ban": "Balinesisch", + "bas": "Basa (Kamerun)", + "bej": "Bedja (Bedauye)", + "bel": "Weißrussisch", + "bem": "Bemba (Sambia)", + "ben": "Bengalisch", + "bho": "Bhojpuri", + "bik": "Bikol", + "bin": "Bini", + "bis": "Bislama", + "bla": "Blackfoot", + "bod": "Tibetisch", + "bos": "Bosnisch", + "bra": "Braj-Bhakha", + "bre": "Bretonisch", + "bua": "Burjatisch", + "bug": "Buginesisch", + "bul": "Bulgarisch", + "byn": "Bilin", + "cad": "Caddo", + "car": "Karibisch; Galíbi", + "cat": "Katalanisch", + "ceb": "Cebuano", + "ces": "Tschechisch", + "cha": "Chamorro", + "chb": "Chibcha", + "che": "Tschetschenisch", + "chg": "Tschagataisch", + "chk": "Trukesisch", + "chm": "Mari (Russland)", + "chn": "Chinook", + "cho": "Choctaw", + "chp": "Chipewyan", + "chr": "Cherokee", + "chu": "Altkirchenslawisch", + "chv": "Tschuwaschisch", + "chy": "Cheyenne", + "cop": "Koptisch", + "cor": "Kornisch", + "cos": "Korsisch", + "cre": "Cree", + "crh": "Türkisch; Krimtatarisch", + "csb": "Kaschubisch", + "cym": "Walisisch", + "dak": "Dakota", + "dan": "Dänisch", + "dar": "Darginisch", + "del": "Delaware", + "den": "Slave (Athapaskisch)", + "deu": "Deutsch", + "dgr": "Dogrib", + "din": "Dinka", + "div": "Dhivehi", + "doi": "Dogri (Makrosprache)", + "dsb": "Sorbisch; Nieder", + "dua": "Duala", + "dum": "Niederländisch; Mittel (ca. 1050-1350)", + "dyu": "Dyula", + "dzo": "Dzongkha", + "efi": "Efik", + "egy": "Ägyptisch (Historisch)", + "eka": "Ekajuk", + "ell": "Neugriechisch (ab 1453)", + "elx": "Elamisch", + "eng": "Englisch", + "enm": "Mittelenglisch", + "epo": "Esperanto", + "est": "Estnisch", + "eus": "Baskisch", + "ewe": "Ewe-Sprache", + "ewo": "Ewondo", + "fan": "Fang (Äquatorial-Guinea)", + "fao": "Färöisch", + "fas": "Persisch", + "fat": "Fanti", + "fij": "Fidschianisch", + "fil": "Filipino", + "fin": "Finnisch", + "fon": "Fon", + "fra": "Französisch", + "frm": "Französisch; Mittel (ca. 1400 - 1600)", + "fro": "Französisch; Alt (842 - ca. 1400)", + "frr": "Friesisch; Nord", + "frs": "Friesisch; Ost", + "fry": "Friesisch; West", + "ful": "Ful", + "fur": "Friaulisch", + "gaa": "Ga", + "gay": "Gayo", + "gba": "Gbaya (Zentralafrikanische Republik)", + "gez": "Altäthiopisch", + "gil": "Gilbertesisch", + "gla": "Gälisch; Schottisch", + "gle": "Irisch", + "glg": "Galicisch", + "glv": "Manx", + "gmh": "Mittelhochdeutsch (ca. 1050-1500)", + "goh": "Althochdeutsch (ca. 750-1050)", + "gon": "Gondi", + "gor": "Gorontalesisch", + "got": "Gotisch", + "grb": "Grebo", + "grc": "Altgriechisch (bis 1453)", + "grn": "Guaraní", + "gsw": "Schweizerdeutsch", + "guj": "Gujarati", + "gwi": "Kutchin", + "hai": "Haida", + "hat": "Kreolisch; Haitisch", + "hau": "Haussa", + "haw": "Hawaiianisch", + "heb": "Hebräisch", + "her": "Herero", + "hil": "Hiligaynon", + "hin": "Hindi", + "hit": "Hethitisch", + "hmn": "Miao-Sprachen", + "hmo": "Hiri-Motu", + "hrv": "Kroatisch", + "hsb": "Obersorbisch", + "hun": "Ungarisch", + "hup": "Hupa", + "hye": "Armenisch", + "iba": "Iban", + "ibo": "Ibo", + "ido": "Ido", + "iii": "Yi; Sichuan", + "iku": "Inuktitut", + "ile": "Interlingue", + "ilo": "Ilokano", + "ina": "Interlingua (Internationale Hilfssprachen-Vereinigung)", + "ind": "Indonesisch", + "inh": "Inguschisch", + "ipk": "Inupiaq", + "isl": "Isländisch", + "ita": "Italienisch", + "jav": "Javanisch", + "jbo": "Lojban", + "jpn": "Japanisch", + "jpr": "Jüdisch-Persisch", + "jrb": "Jüdisch-Arabisch", + "kaa": "Karakalpakisch", + "kab": "Kabylisch", + "kac": "Kachinisch", + "kal": "Kalaallisut (Grönländisch)", + "kam": "Kamba (Kenia)", + "kan": "Kannada", + "kas": "Kaschmirisch", + "kat": "Georgisch", + "kau": "Kanuri", + "kaw": "Kawi; Altjavanisch", + "kaz": "Kasachisch", + "kbd": "Kabardisch", + "kha": "Khasi-Sprache", + "khm": "Khmer; Zentral", + "kho": "Sakisch", + "kik": "Kikuyu", + "kin": "Rwanda", + "kir": "Kirgisisch", + "kmb": "Mbundu; Kimbundu", + "kok": "Konkani (Makrosprache)", + "kom": "Komi", + "kon": "Kongo", + "kor": "Koreanisch", + "kos": "Kosraeanisch", + "kpe": "Kpelle", + "krc": "Karachay-Balkar", + "krl": "Karenisch", + "kru": "Kurukh", + "kua": "Kwanyama", + "kum": "Kumükisch", + "kur": "Kurdisch", + "kut": "Kutenai", + "lad": "Judenspanisch", + "lah": "Lahnda", + "lam": "Banjari; Lamba", + "lao": "Laotisch", + "lat": "Lateinisch", + "lav": "Lettisch", + "lez": "Lesgisch", + "lim": "Limburgisch", + "lin": "Lingala", + "lit": "Litauisch", + "lol": "Mongo", + "loz": "Rotse", + "ltz": "Luxemburgisch", + "lua": "Luba-Lulua", + "lub": "Luba-Katanga", + "lug": "Ganda", + "lui": "Luiseno", + "lun": "Lunda", + "luo": "Luo (Kenia und Tansania)", + "lus": "Lushai", + "mad": "Maduresisch", + "mag": "Khotta", + "mah": "Marshallesisch", + "mai": "Maithili", + "mak": "Makassarisch", + "mal": "Malayalam", + "man": "Mande; Mandigo; Malinke", + "mar": "Marathi", + "mas": "Massai", + "mdf": "Moksha", + "mdr": "Mandaresisch", + "men": "Mende (Sierra Leone)", + "mga": "Mittelirisch (900-1200)", + "mic": "Mikmak", + "min": "Minangkabau", + "mis": "Nichtklassifizierte Sprachen", + "mkd": "Makedonisch", + "mlg": "Madegassisch", + "mlt": "Maltesisch", + "mnc": "Manchu; Mandschurisch", + "mni": "Meithei-Sprache", + "moh": "Mohawk", + "mon": "Mongolisch", + "mos": "Mossi", + "mri": "Maori", + "msa": "Malaiisch (Makrosprache)", + "mul": "Mehrsprachig; Polyglott", + "mus": "Muskogee", + "mwl": "Mirandesisch", + "mwr": "Marwari", + "mya": "Burmesisch", + "myv": "Erzya", + "nap": "Neapolitanisch", + "nau": "Nauruanisch", + "nav": "Navajo", + "nbl": "Ndebele (Süd)", + "nde": "Ndebele (Nord)", + "ndo": "Ndonga", + "nds": "Plattdeutsch", + "nep": "Nepali", + "new": "Bhasa; Nepalesisch", + "nia": "Nias", + "niu": "Niue", + "nld": "Niederländisch", + "nno": "Nynorsk (Norwegen)", + "nob": "Norwegisch-Bokmål", + "nog": "Nogai", + "non": "Altnordisch", + "nor": "Norwegisch", + "nqo": "N'Ko", + "nso": "Sotho; Nord", + "nwc": "Newari; Alt", + "nya": "Nyanja", + "nym": "Nyamwezi", + "nyn": "Nyankole", + "nyo": "Nyoro", + "nzi": "Nzima", + "oci": "Okzitanisch (nach 1500)", + "oji": "Ojibwa", + "ori": "Orija", + "orm": "Oromo", + "osa": "Osage", + "oss": "Ossetisch", + "ota": "Ottomanisch (Osmanisch/Türkisch) (1500-1928)", + "pag": "Pangasinan", + "pal": "Mittelpersisch", + "pam": "Pampanggan", + "pan": "Panjabi", + "pap": "Papiamento", + "pau": "Palau", + "peo": "Persisch; Alt (ca. 600-400 v.Chr.)", + "phn": "Phönikisch", + "pli": "Pali", + "pol": "Polnisch", + "pon": "Ponapeanisch", + "por": "Portugiesisch", + "pro": "Altokzitanisch; Altprovenzalisch (bis 1500)", + "pus": "Paschtu; Afghanisch", + "que": "Ketschua", + "raj": "Rajasthani", + "rap": "Osterinsel-Sprache; Rapanui", + "rar": "Maori; Cook-Inseln", + "roh": "Bündnerromanisch", + "rom": "Romani; Zigeunersprache", + "ron": "Rumänisch", + "run": "Rundi", + "rup": "Rumänisch; Mezedonisch", + "rus": "Russisch", + "sad": "Sandawe", + "sag": "Sango", + "sah": "Jakutisch", + "sam": "Aramäisch; Samaritanisch", + "san": "Sanskrit", + "sas": "Sassak", + "sat": "Santali", + "scn": "Sizilianisch", + "sco": "Schottisch", + "sel": "Selkupisch", + "sga": "Altirisch (bis 900)", + "shn": "Schan", + "sid": "Sidamo", + "sin": "Singhalesisch", + "slk": "Slowakisch", + "slv": "Slowenisch", + "sma": "Sami; Süd", + "sme": "Nordsamisch", + "smj": "Samisch (Lule)", + "smn": "Samisch; Inari", + "smo": "Samoanisch", + "sms": "Samisch; Skolt", + "sna": "Schona", + "snd": "Sindhi", + "snk": "Soninke", + "sog": "Sogdisch", + "som": "Somali", + "sot": "Sotho (Süd)", + "spa": "Spanisch; Kastilianisch", + "sqi": "Albanisch", + "srd": "Sardisch", + "srn": "Sranan Tongo", + "srp": "Serbisch", + "srr": "Serer", + "ssw": "Swazi", + "suk": "Sukuma", + "sun": "Sundanesisch", + "sus": "Susu", + "sux": "Sumerisch", + "swa": "Swahili (Makrosprache)", + "swe": "Schwedisch", + "syc": "Syrisch; Klassisch", + "syr": "Syrisch", + "tah": "Tahitisch", + "tam": "Tamilisch", + "tat": "Tatarisch", + "tel": "Telugu", + "tem": "Temne", + "ter": "Tereno", + "tet": "Tetum", + "tgk": "Tadschikisch", + "tgl": "Tagalog", + "tha": "Thailändisch", + "tig": "Tigre", + "tir": "Tigrinja", + "tiv": "Tiv", + "tkl": "Tokelauanisch", + "tlh": "Klingonisch", + "tli": "Tlingit", + "tmh": "Tamaseq", + "tog": "Tonga (Nyasa)", + "ton": "Tonga (Tonga-Inseln)", + "tpi": "Neumelanesisch; Pidgin", + "tsi": "Tsimshian", + "tsn": "Tswana", + "tso": "Tsonga", + "tuk": "Turkmenisch", + "tum": "Tumbuka", + "tur": "Türkisch", + "tvl": "Elliceanisch", + "twi": "Twi", + "tyv": "Tuwinisch", + "udm": "Udmurt", + "uga": "Ugaritisch", + "uig": "Uigurisch", + "ukr": "Ukrainisch", + "umb": "Mbundu; Umbundu", + "und": "Unbestimmbar", + "urd": "Urdu", + "uzb": "Usbekisch", + "vai": "Vai", + "ven": "Venda", + "vie": "Vietnamesisch", + "vol": "Volapük", + "vot": "Wotisch", + "wal": "Wolaytta", + "war": "Waray (Philippinen)", + "was": "Washo", + "wln": "Wallonisch", + "wol": "Wolof", + "xal": "Kalmükisch", + "xho": "Xhosa", + "yao": "Yao", + "yap": "Yapesisch", + "yid": "Jiddisch", + "yor": "Joruba", + "zap": "Zapotekisch", + "zbl": "Bliss-Symbole", + "zen": "Zenaga", + "zha": "Zhuang", + "zho": "Chinesisch", + "zul": "Zulu", + "zun": "Zuni", + "zxx": "Kein sprachlicher Inhalt", + "zza": "Zaza" }, "el": { "abk": "Αμπχαζιανά", @@ -807,798 +1227,845 @@ LANGUAGE_NAMES = { "zul": "Zulu", "zun": "Zuni" }, - "nl": { - "aar": "Afar; Hamitisch", - "abk": "Abchazisch", - "ace": "Achinees", + "es": { + "aar": "Afar", + "abk": "Abjasio", + "ace": "Achenés", "ach": "Acholi", "ada": "Adangme", - "ady": "Adyghe", + "ady": "Adigué", "afh": "Afrihili", - "afr": "Afrikaans", - "ain": "Ainu (Japan)", - "aka": "Akaans", - "akk": "Akkadiaans", - "ale": "Aleut", - "alt": "Altajs; zuidelijk", - "amh": "Amhaars; Amharisch", - "ang": "Engels; oud (ca. 450-1100)", + "afr": "Afrikáans", + "ain": "Ainu (Japón)", + "aka": "Acano", + "akk": "Acadio", + "ale": "Aleutiano", + "alt": "Altai meridional", + "amh": "Amhárico", + "ang": "Inglés antiguo (ca. 450-1100)", "anp": "Angika", - "ara": "Arabisch", - "arc": "Aramees; officieel (700-300 B.C.)", - "arg": "Aragonees", + "ara": "Árabe", + "arc": "Arameo oficial (700-300 A. C.)", + "arg": "Aragonés", "arn": "Mapudungun", - "arp": "Arapaho", - "arw": "Arawak", - "asm": "Assamees; Assami", - "ast": "Asturisch", - "ava": "Avaars; Awari", - "ave": "Avestisch", + "arp": "Arapajó", + "arw": "Arahuaco", + "asm": "Asamés", + "ast": "Asturiano", + "ava": "Ávaro", + "ave": "Avéstico", "awa": "Awadhi", - "aym": "Aymara", - "aze": "Azerbeidzjaans", - "bak": "Basjkiers; Basjkirisch", - "bal": "Balutsji; Baluchi", + "aym": "Aimara", + "aze": "Azerí", + "bak": "Bashkir", + "bal": "Baluchi", "bam": "Bambara", - "ban": "Balinees", - "bas": "Basa (Kameroen)", - "bej": "Beja", - "bel": "Wit-Russisch; Belarussisch", + "ban": "Balinés", + "bas": "Basa (Camerún)", + "bej": "Beya", + "bel": "Bielorruso", "bem": "Bemba (Zambia)", - "ben": "Bengaals", - "bho": "Bhojpuri", - "bik": "Bikol", - "bin": "Bini; Edo", + "ben": "Bengalí", + "bho": "Bopurí", + "bik": "Bicolano", + "bin": "Bini", "bis": "Bislama", - "bla": "Siksika", - "bod": "Tibetaans", - "bos": "Bosnisch", + "bla": "Siksiká", + "bod": "Tibetano", + "bos": "Bosnio", "bra": "Braj", - "bre": "Bretons; Bretoens", - "bua": "Boeriaats", - "bug": "Buginees", - "bul": "Bulgaars", + "bre": "Bretón", + "bua": "Buriato", + "bug": "Buginés", + "bul": "Búlgaro", "byn": "Bilin", "cad": "Caddo", - "car": "Caribische talen", - "cat": "Catalaans", + "car": "Caribe galibí", + "cat": "Catalán", "ceb": "Cebuano", - "ces": "Tsjechisch", + "ces": "Checo", "cha": "Chamorro", - "chb": "Tsjibtsja", - "che": "Tsjetsjeens", + "chb": "Muisca", + "che": "Checheno", "chg": "Chagatai", - "chk": "Chukees", - "chm": "Mari (Rusland)", - "chn": "Chinook-jargon", + "chk": "Chuukés", + "chm": "Mari (Rusia)", + "chn": "Jerga chinook", "cho": "Choctaw", - "chp": "Chipewyaans", - "chr": "Cherokee", - "chu": "Slavisch; oud (kerk)", - "chv": "Tsjoevasjisch", + "chp": "Chipewyan", + "chr": "Cheroqui", + "chu": "Eslavo antiguo", + "chv": "Chuvasio", "chy": "Cheyenne", - "cop": "Koptisch", - "cor": "Cornisch", - "cos": "Corsicaans", + "cop": "Copto", + "cor": "Córnico", + "cos": "Corso", "cre": "Cree", - "crh": "Turks; Crimean", - "csb": "Kasjoebiaans", - "cym": "Welsh", + "crh": "Tártaro de Crimea", + "csb": "Casubio", + "cym": "Galés", "dak": "Dakota", - "dan": "Deens", + "dan": "Danés", "dar": "Dargwa", "del": "Delaware", - "den": "Slavisch (Athapascaans)", - "deu": "Duits", + "den": "Slave (atabascano)", + "deu": "Alemán", "dgr": "Dogrib", "din": "Dinka", - "div": "Divehi", - "doi": "Dogri", - "dsb": "Sorbisch; lager", + "div": "Dhivehi", + "doi": "Dogri (macrolengua)", + "dsb": "Bajo sorabo", "dua": "Duala", - "dum": "Nederlands; middel (ca. 1050-1350)", - "dyu": "Dyula", + "dum": "Neerlandés medio (ca. 1050-1350)", + "dyu": "Diula", "dzo": "Dzongkha", - "efi": "Efikisch", - "egy": "Egyptisch (antiek)", + "efi": "Efik", + "egy": "Egipcio (antiguo)", "eka": "Ekajuk", - "ell": "Grieks; Modern (1453-)", - "elx": "Elamitisch", - "eng": "Engels", - "enm": "Engels; middel (1100-1500)", + "ell": "Griego moderno (1453-)", + "elx": "Elamita", + "eng": "Inglés", + "enm": "Inglés medio (1100-1500)", "epo": "Esperanto", - "est": "Estlands", - "eus": "Baskisch", + "est": "Estonio", + "eus": "Vasco", "ewe": "Ewe", "ewo": "Ewondo", - "fan": "Fang", - "fao": "Faeröers", - "fas": "Perzisch", + "fan": "Fang (Guinea Ecuatorial)", + "fao": "Feroés", + "fas": "Persa", "fat": "Fanti", - "fij": "Fijisch", - "fil": "Filipijns", - "fin": "Fins", + "fij": "Fiyiano", + "fil": "Filipino", + "fin": "Finés", "fon": "Fon", - "fra": "Frans", - "frm": "Frans; middel (ca. 1400-1600)", - "fro": "Frans; oud (842-ca. 1400)", - "frr": "Fries; noordelijk (Duitsland)", - "frs": "Fries; oostelijk (Duitsland)", - "fry": "Fries", - "ful": "Fulah", - "fur": "Friulisch", + "fra": "Francés", + "frm": "Francés medio (ca. 1400-1600)", + "fro": "Francés antiguo (842-ca. 1400)", + "frr": "Frisón septentrional", + "frs": "Frisón oriental", + "fry": "Frisón occidental", + "ful": "Fula", + "fur": "Friulano", "gaa": "Ga", "gay": "Gayo", - "gba": "Gbaya (Centraal Afrikaanse Republiek)", + "gba": "Gbaya (República Centroafricana)", "gez": "Ge'ez", - "gil": "Gilbertees", - "gla": "Keltisch; schots", - "gle": "Iers", - "glg": "Galiciaans", - "glv": "Manx", - "gmh": "Duits; middel hoog (ca. 1050-1500)", - "goh": "Duits; oud hoog (ca. 750-1050)", + "gil": "Gilbertés", + "gla": "Gaélico escocés", + "gle": "Irlandés", + "glg": "Gallego", + "glv": "Manés", + "gmh": "Alto alemán medio (ca. 1050-1500)", + "goh": "Alto alemán antiguo (ca. 750-1050)", "gon": "Gondi", "gor": "Gorontalo", - "got": "Gothisch", + "got": "Gótico", "grb": "Grebo", - "grc": "Grieks; antiek (tot 1453)", - "grn": "Guarani", - "gsw": "Duits; Zwitserland", - "guj": "Gujarati", + "grc": "Griego antiguo (hasta 1453)", + "grn": "Guaraní", + "gsw": "Alemán suizo", + "guj": "Guyaratí", "gwi": "Gwichʼin", "hai": "Haida", - "hat": "Creools; Haïtiaans", + "hat": "Criollo haitiano", "hau": "Hausa", - "haw": "Hawaiiaans", - "heb": "Hebreeuws", + "haw": "Hawaiano", + "heb": "Hebreo", "her": "Herero", - "hil": "Hiligainoons", + "hil": "Hiligainón", "hin": "Hindi", - "hit": "Hittitisch", + "hit": "Hitita", "hmn": "Hmong", - "hmo": "Hiri Motu", - "hrv": "Kroatisch", - "hsb": "Servisch; hoger", - "hun": "Hongaars", + "hmo": "Hiri motu", + "hrv": "Croata", + "hsb": "Alto sorabo", + "hun": "Húngaro", "hup": "Hupa", - "hye": "Armeens", - "iba": "Ibaans", + "hye": "Armenio", + "iba": "Iban", "ibo": "Igbo", "ido": "Ido", - "iii": "Yi; Sichuan - Nuosu", + "iii": "Yi de Sichuan", "iku": "Inuktitut", - "ile": "Interlingue", - "ilo": "Iloko", - "ina": "Interlingua (International Auxiliary Language Association)", - "ind": "Indonesisch", - "inh": "Ingoesjetisch", - "ipk": "Inupiak", - "isl": "IJslands", - "ita": "Italiaans", - "jav": "Javaans", - "jbo": "Lojbaans", - "jpn": "Japans", - "jpr": "Joods-Perzisch", - "jrb": "Joods-Arabisch", - "kaa": "Kara-Kalpak", - "kab": "Kabyle", - "kac": "Katsjin", - "kal": "Groenlands", - "kam": "Kamba (Kenya)", - "kan": "Kannada; Kanara; Kanarees", - "kas": "Kashmiri", - "kat": "Georgisch", - "kau": "Kanuri", - "kaw": "Kawi", - "kaz": "Kazachs", - "kbd": "Kabardisch; Tsjerkessisch", - "kha": "Khasi", - "khm": "Khmer, Cambodjaans", - "kho": "Khotanees", - "kik": "Kikuyu", - "kin": "Kinyarwanda", - "kir": "Kirgizisch", - "kmb": "Kimbundu", - "kok": "Konkani", - "kom": "Komi", - "kon": "Kikongo", - "kor": "Koreaans", - "kos": "Kosraeaans", - "kpe": "Kpelle", - "krc": "Karatsjay-Balkar", - "krl": "Karelisch", - "kru": "Kurukh", - "kua": "Kuanyama", - "kum": "Kumyk", - "kur": "Koerdisch", - "kut": "Kutenaïsch", - "lad": "Ladino", - "lah": "Lahnda", - "lam": "Lamba", - "lao": "Laotiaans", - "lat": "Latijn", - "lav": "Lets", - "lez": "Lezghiaans", - "lim": "Limburgs", - "lin": "Lingala", - "lit": "Litouws", - "lol": "Mongo", - "loz": "Lozi", - "ltz": "Luxemburgs", - "lua": "Luba-Lulua", - "lub": "Luba-Katanga", - "lug": "Luganda", - "lui": "Luiseno", - "lun": "Lunda", - "luo": "Luo (Kenia en Tanzania)", - "lus": "Lushai", - "mad": "Madurees", - "mag": "Magahisch", - "mah": "Marshallees", - "mai": "Maithili", - "mak": "Makasar", - "mal": "Malayalam", - "man": "Mandingo", - "mar": "Marathi", - "mas": "Masai", - "mdf": "Moksja", - "mdr": "Mandars", - "men": "Mende", - "mga": "Iers; middel (900-1200)", - "mic": "Mi'kmaq; Micmac", - "min": "Minangkabau", - "mis": "Niet-gecodeerde talen", - "mkd": "Macedonisch", - "mlg": "Malagassisch", - "mlt": "Maltees", - "mnc": "Manchu", - "mni": "Manipuri", - "moh": "Mohawk", - "mon": "Mongools", - "mos": "Mossisch", - "mri": "Maori", - "msa": "Maleis", - "mul": "Meerdere talen", - "mus": "Creek", - "mwl": "Mirandees", - "mwr": "Marwari", - "mya": "Burmees", - "myv": "Erzya", - "nap": "Napolitaans", - "nau": "Nauruaans", - "nav": "Navajo", - "nbl": "Ndebele; zuid", - "nde": "Ndebele; noord", - "ndo": "Ndonga", - "nds": "Duits; Laag", - "nep": "Nepalees", - "new": "Newari; Nepal", - "nia": "Nias", - "niu": "Niueaans", - "nld": "Nederlands", - "nno": "Noors; Nynorsk", - "nob": "Noors; Bokmål", - "nog": "Nogai", - "non": "Noors; oud", - "nor": "Noors", - "nqo": "N'Ko", - "nso": "Pedi; Sepedi; Noord-Sothotisch", - "nwc": "Newari; Klassiek Nepal", - "nya": "Nyanja", - "nym": "Nyamwezi", - "nyn": "Nyankools", - "nyo": "Nyoro", - "nzi": "Nzima", - "oci": "Occitaans (na 1500)", - "oji": "Ojibwa", - "ori": "Oriya", - "orm": "Oromo", - "osa": "Osaags", - "oss": "Ossetisch", - "ota": "Turks; ottomaans (1500-1928)", - "pag": "Pangasinaans", - "pal": "Pehlevi", - "pam": "Pampanga", - "pan": "Punjabi", - "pap": "Papiamento", - "pau": "Palauaans", - "peo": "Perzisch; oud (ca. 600-400 B.C.)", - "phn": "Foenisisch", - "pli": "Pali", - "pol": "Pools", - "pon": "Pohnpeiaans", - "por": "Portugees", - "pro": "Provençaals; oud (tot 1500)", - "pus": "Poesjto", - "que": "Quechua", - "raj": "Rajasthani", - "rap": "Rapanui", - "rar": "Rarotongan; Cookeilanden Maori", - "roh": "Reto-Romaans", - "rom": "Romani", - "ron": "Roemeens", - "run": "Rundi", - "rup": "Roemeens; Macedo-", - "rus": "Russisch", - "sad": "Sandawe", - "sag": "Sangho", - "sah": "Jakoets", - "sam": "Aramees; Samaritaans", - "san": "Sanskriet", - "sas": "Sasaaks", - "sat": "Santali", - "scn": "Siciliaans", - "sco": "Schots", - "sel": "Sulkoeps", - "sga": "Iers; oud (tot 900)", - "shn": "Sjaans", - "sid": "Sidamo", - "sin": "Sinhala", - "slk": "Slowaaks", - "slv": "Sloveens", - "sma": "Samisch; zuid, Laps; zuid", - "sme": "Samisch; noord, Laps; noord", - "smj": "Lule Sami", - "smn": "Sami; Inari, Laps; Inari", - "smo": "Samoaans", - "sms": "Sami; Skolt, Laps; Skolt", - "sna": "Shona", - "snd": "Sindhi", - "snk": "Soninke", - "sog": "Sogdiaans", - "som": "Somalisch", - "sot": "Sothaans; zuidelijk", - "spa": "Spaans", - "sqi": "Albanees", - "srd": "Sardinisch", - "srn": "Sranan Tongo", - "srp": "Servisch", - "srr": "Serer", - "ssw": "Swati", - "suk": "Sukuma", - "sun": "Soendanees; Sundanees", - "sus": "Susu", - "sux": "Sumerisch", - "swa": "Swahili", - "swe": "Zweeds", - "syc": "Syriac; Klassiek", - "syr": "Syrisch", - "tah": "Tahitisch", - "tam": "Tamil", - "tat": "Tataars", - "tel": "Telugu", - "tem": "Timne", - "ter": "Tereno", - "tet": "Tetum", - "tgk": "Tadzjieks", - "tgl": "Tagalog", - "tha": "Thai", - "tig": "Tigre", - "tir": "Tigrinya", - "tiv": "Tiv", - "tkl": "Tokelau", - "tlh": "Klingon; tlhIngan-Hol", - "tli": "Tlingit", - "tmh": "Tamasjek", - "tog": "Tonga (Nyasa)", - "ton": "Tonga (Tonga-eilanden)", - "tpi": "Tok Pisin", - "tsi": "Tsimsjiaans", - "tsn": "Tswana", - "tso": "Tsonga", - "tuk": "Turkmeens", - "tum": "Tumbuka", - "tur": "Turks", - "tvl": "Tuvalu", - "twi": "Twi", - "tyv": "Tuviniaans", - "udm": "Udmurts", - "uga": "Ugaritisch", - "uig": "Oeigoers; Oejgoers", - "ukr": "Oekraïens", - "umb": "Umbundu", - "und": "Onbepaald", - "urd": "Urdu", - "uzb": "Oezbeeks", - "vai": "Vai", - "ven": "Venda", - "vie": "Vietnamees", - "vol": "Volapük", - "vot": "Votisch", - "wal": "Walamo", - "war": "Waray (Filipijns)", - "was": "Wasjo", - "wln": "Waals", - "wol": "Wolof", - "xal": "Kalmyk", - "xho": "Xhosa", - "yao": "Yao", - "yap": "Yapees", - "yid": "Jiddisch", - "yor": "Yoruba", - "zap": "Zapotec", - "zbl": "Blissymbolen", - "zen": "Zenaga", - "zha": "Zhuang, Tsjoeang", - "zho": "Chinees", - "zul": "Zoeloe", - "zun": "Zuni", - "zxx": "Geen linguïstische inhoud", - "zza": "Zaza" - }, - "tr": { - "abk": "Abhazca", - "ace": "Achinese", - "ach": "Acoli", - "ada": "Adangme", - "ady": "Adyghe", - "aar": "Afar", - "afh": "Afrihili", - "afr": "Afrikanca", - "ain": "Ainu (Japonca)", - "aka": "Akanca (Afrika dili)", - "akk": "Akatça", - "sqi": "Albanian", - "ale": "Alaskaca", - "amh": "Etiyopyaca", - "anp": "Angika", - "ara": "Arapça", - "arg": "Aragonca (İspanya)", - "arp": "Arapaho (Kuzey Amerika yerlileri)", - "arw": "Arawak (Surinam)", - "hye": "Ermenice", - "asm": "Assamese (Hindistan)", - "ast": "Asturyasca", - "ava": "Avarca", - "ave": "Avestan (Eski İran)", - "awa": "Awadhi (Hindistan)", - "aym": "Aymara (Güney Amerika)", - "aze": "Azerice", - "ban": "Balice (Bali adaları)", - "bal": "Belucice (İran)", - "bam": "Bambara (Mali)", - "bas": "Basa (Kamerun)", - "bak": "Başkırca", - "eus": "Baskça", - "bej": "Beja (Eritre; Sudan)", - "bel": "Beyaz Rusça", - "bem": "Bemba (Zambia)", - "ben": "Bengalce", - "bho": "Bhojpuri (Hindistan)", - "bik": "Bikol (Filipinler)", - "byn": "Bilin", - "bin": "Bini (Afrika)", - "bis": "Bislama (Vanuatu; Kuzey Pasifik)", - "zbl": "Blis Sembolleri", - "bos": "Boşnakça", - "bra": "Braj (Hindistan)", - "bre": "Bretonca", - "bug": "Buginese (Endonezya)", - "bul": "Bulgarca", - "bua": "Buriat (Moğolistan)", - "mya": "Burmaca", - "cad": "Caddo (Kuzey Amerika yerlileri)", - "cat": "Katalanca", - "ceb": "Cebuano (Filipinler)", - "chg": "Çağatayca", - "cha": "Chamorro (Guam adaları)", - "che": "Çeçence", - "chr": "Cherokee (Kuzey Amerika yerlileri)", - "chy": "Cheyenne (kuzey Amerika yerlileri)", - "chb": "Chibcha (Kolombiya)", - "zho": "Çince", - "chn": "Chinook lehçesi (Kuzey Batı Amerika kıyıları)", - "chp": "Chipewyan (Kuzey Amerika yerlileri)", - "cho": "Choctaw (Kuzey Amerika yerlileri)", - "chk": "Chuukese", - "chv": "Çuvaş (Türkçe)", - "cop": "Kıptice (Eski Mısır)", - "cor": "Cornish (Kelt)", - "cos": "Korsikaca", - "cre": "Cree (Kuzey Amerika yerlileri)", - "mus": "Creek", - "hrv": "Hırvatça", - "ces": "Çekçe", - "dak": "Dakota (Kuzey Amerika yerlileri)", - "dan": "Danimarkaca; Danca", - "dar": "Dargwa (Dağıstan)", - "del": "Delaware (Kuzey Amerika yerlileri)", - "div": "Dhivehi", - "din": "Dinka (Sudan)", - "doi": "Dogri (makro dili)", - "dgr": "Dogrib (Kanada)", - "dua": "Duala (Afrika)", - "nld": "Flâmanca (Hollanda dili)", - "dyu": "Dyula (Burkina Faso; Mali)", - "dzo": "Dzongkha (Butan)", - "efi": "Efik (Afrika)", - "egy": "Mısırca (Eski)", - "eka": "Ekajuk (Afrika)", - "elx": "Elamca", - "eng": "İngilizce", - "myv": "Erzya dili", - "epo": "Esperanto", - "est": "Estonca", - "ewe": "Ewe (Afrika)", - "ewo": "Ewondo (Afrika)", - "fan": "Fang (Ekvatoryal Guinea)", - "fat": "Fanti (Afrika)", - "fao": "Faroece", - "fij": "Fiji dili", - "fil": "Filipince", - "fin": "Fince", - "fon": "Fon (Benin)", - "fra": "Fransızca", - "fur": "Friulian (İtalya)", - "ful": "Fulah (Afrika)", - "gaa": "Ganaca", - "glg": "Galce", - "lug": "Ganda Dili", - "gay": "Gayo (Sumatra)", - "gba": "Gbaya (Orta Afrika Cumhuriyeti)", - "gez": "Geez (Etiyopya)", - "kat": "Gürcüce", - "deu": "Almanca", - "gil": "Kiribati dili", - "gon": "Gondi (Hindistan)", - "gor": "Gorontalo (Endonezya)", - "got": "Gotik", - "grb": "Grebo (Liberya)", - "grn": "Guarani (Paraguay)", - "guj": "Gucaratça", - "gwi": "Gwichʼin", - "hai": "Haida (Kuzey Amerika yerlileri)", - "hau": "Hausa Dili", - "haw": "Havai Dili", - "heb": "İbranice", - "her": "Herero Dili", - "hil": "Hiligaynon", - "hin": "Hintçe", - "hmo": "Hiri Motu", - "hit": "Hititçe", - "hmn": "Hmong", - "hun": "Macarca", - "hup": "Hupa", - "iba": "Iban", - "isl": "İzlandaca", - "ido": "Ido Dili", - "ibo": "Igbo Dili", - "ilo": "Iloko", - "ind": "Endonezyaca", - "inh": "İnguşca", - "ina": "Interlingua (Uluslararası Yardımcı Dil Kurumu)", - "ile": "Interlingue", - "iku": "Inuktitut", - "ipk": "Inupiak Dili", - "gle": "İrlandaca", - "ita": "İtalyanca", - "jpn": "Japonca", - "jav": "Cava Dili", - "jrb": "Yahudi-Arapçası", - "jpr": "Yahudi-Farsça", - "kbd": "Kabardian", - "kab": "Kabyle", + "ile": "Interlingüe", + "ilo": "Ilocano", + "ina": "Interlingua", + "ind": "Indonesio", + "inh": "Ingusetio", + "ipk": "Iñupiaq", + "isl": "Islandés", + "ita": "Italiano", + "jav": "Javanés", + "jbo": "Lojban", + "jpn": "Japonés", + "jpr": "Judeo-persa", + "jrb": "Judeo-árabe", + "kaa": "Karakalpako", + "kab": "Cabilio", "kac": "Kachin", "kal": "Kalaallisut", - "xal": "Kalmyk", - "kam": "Kamba (Kenya)", - "kan": "Kannada", - "kau": "Kanuri Dili", - "kaa": "Kara-Kalpak", - "krc": "Karachay-Balkar", - "krl": "Karelian", - "kas": "Keşmirce", - "csb": "Kashubian (Lehçe diyalekti)", + "kam": "Kamba (Kenia)", + "kan": "Canarés", + "kas": "Cachemir", + "kat": "Georgiano", + "kau": "Kanuri", "kaw": "Kawi", - "kaz": "Kazakça", + "kaz": "Kazajo", + "kbd": "Cabardiano", "kha": "Khasi", - "kho": "Khotanese", - "kik": "Kikuyu Dili", - "kmb": "Kimbundu", + "khm": "Jemer central", + "kho": "Khotanés", + "kik": "Kikuyu", "kin": "Kinyarwanda", - "kir": "Kırgızca", - "tlh": "Klingon", - "kom": "Komi Dili", - "kon": "Kongo Dili", - "kok": "Konkani (makro dil)", - "kor": "Korece", - "kos": "Kosraean", + "kir": "Kirguís", + "kmb": "Kimbundu", + "kok": "Konkaní (macrolengua)", + "kom": "Komi", + "kon": "Kikongo", + "kor": "Coreano", + "kos": "Kosraeano", "kpe": "Kpelle", - "kua": "Kuanyama Dili", - "kum": "Kumyk", - "kur": "Kürtçe", + "krc": "Karachayo-bálkaro", + "krl": "Carelio", "kru": "Kurukh", + "kua": "Kuanyama", + "kum": "Cumuco", + "kur": "Curdo", "kut": "Kutenai", "lad": "Ladino", "lah": "Lahnda", "lam": "Lamba", - "lao": "Laos Dili", - "lat": "Latince", - "lav": "Letonca", - "lez": "Lezghian", - "lim": "Liburg Dili", - "lin": "Lingala Dili", - "lit": "Litvanyaca", - "jbo": "Lojban dili", - "loz": "Lozi", - "lub": "Luba Katanga Dili", - "lua": "Luba-Lulua", - "lui": "Luiseno", - "smj": "Lule Sami", - "lun": "Lunda", - "luo": "Luo (Kenya ve Tanzanya)", - "lus": "Lushai", - "ltz": "Lüksemburg Dili", - "mkd": "Makedonca", - "mad": "Madurese", - "mag": "Magahi", - "mai": "Maithili dili", - "mak": "Makasar", - "mlg": "Madagaskar Dili", - "msa": "Malay (makro dili)", - "mal": "Malayalam", - "mlt": "Maltaca", - "mnc": "Manchu", - "mdr": "Mandar", - "man": "Mandingo", - "mni": "Manipuri dili", - "glv": "Manx (Galler)", - "mri": "Maori Dili", - "arn": "Mapudungun", - "mar": "Marathi", - "chm": "Mari (Rusya)", - "mah": "Marshall Dili", - "mwr": "Marwari", - "mas": "Masai", - "men": "Mende (Sierra Leone)", - "mic": "Mi'kmak", - "min": "Minangkabau", - "mwl": "Mirandese", - "moh": "Mohawk", - "mdf": "Moşka", + "lao": "Laosiano", + "lat": "Latín", + "lav": "Letón", + "lez": "Lezguino", + "lim": "Limburgan", + "lin": "Lingala", + "lit": "Lituano", "lol": "Mongo", - "mon": "Moğol Dili", + "loz": "Lozi", + "ltz": "Luxemburgués", + "lua": "Chiluba", + "lub": "Kiluba", + "lug": "Luganda", + "lui": "Luiseño", + "lun": "Lunda", + "luo": "Luo (Kenia y Tanzania)", + "lus": "Mizo", + "mad": "Madurés", + "mag": "Magahi", + "mah": "Marshalés", + "mai": "Maithili", + "mak": "Macasar", + "mal": "Malayalam", + "man": "Mandingo", + "mar": "Maratí", + "mas": "Masai", + "mdf": "Moksha", + "mdr": "Mandar", + "men": "Mende (Sierra Leona)", + "mga": "Irlandés medio (900-1200)", + "mic": "Mi'kmaq", + "min": "Minangkabau", + "mis": "Idiomas sin codificar", + "mkd": "Macedonio", + "mlg": "Malgache", + "mlt": "Maltés", + "mnc": "Manchú", + "mni": "Meitei", + "moh": "Mohawk", + "mon": "Mongol", "mos": "Mossi", - "mul": "Çoklu diller", - "nqo": "N'Ko", - "nau": "Nauru", - "nav": "Navajo Dili", - "ndo": "Ndonga Dili", - "nap": "Neapolitan", + "mri": "Maorí", + "msa": "Malayo (macrolengua)", + "mul": "Idiomas múltiples", + "mus": "Creek", + "mwl": "Mirandés", + "mwr": "Marwari", + "mya": "Birmano", + "myv": "Erzya", + "nap": "Napolitano", + "nau": "Nauruano", + "nav": "Navajo", + "nbl": "Ndebele meridional", + "nde": "Ndebele septentrional", + "ndo": "Ndonga", + "nds": "Bajo alemán", + "nep": "Nepalí", + "new": "Bhasa; Nepalés", "nia": "Nias", - "niu": "Niuean", - "zxx": "Hiçbir dil içeriği yok", - "nog": "Nogai", - "nor": "Norveçce", - "nob": "Norveççe Bokmal", - "nno": "Norveççe Nynorsk", - "nym": "Nyamwezi", + "niu": "Niuano", + "nld": "Neerlandés", + "nno": "Noruego nynorsk", + "nob": "Noruego bokmål", + "nog": "Nogayo", + "non": "Noruego antiguo", + "nor": "Noruego", + "nqo": "N'ko", + "nso": "Sepedi", + "nwc": "Newarí antiguo", "nya": "Nyanja", + "nym": "Nyamwezi", "nyn": "Nyankole", "nyo": "Nyoro", - "nzi": "Nzima", - "oci": "Oksitanca (1500 sonrası)", - "oji": "Ojibwa Dili", - "orm": "Oromo Dili", + "nzi": "Nzema", + "oci": "Occitano (posterior a 1500)", + "oji": "Ojibwa", + "ori": "Oriya", + "orm": "Oromo", "osa": "Osage", - "oss": "Osetya Dili", - "pal": "Pehlevi", - "pau": "Palauan", - "pli": "Pali Dili", - "pam": "Pampanga", - "pag": "Pangasinan", - "pan": "Pencabi Dili", + "oss": "Osetio", + "ota": "Turco otomano (1500-1928)", + "pag": "Pangasinense", + "pal": "Pahlavi", + "pam": "Pampango", + "pan": "Panyabí", "pap": "Papiamento", - "fas": "Farsça", - "phn": "Fenikçe", - "pon": "Pohnpeian", - "pol": "Polonyaca", - "por": "Portekizce", - "pus": "Pushto", + "pau": "Palauano", + "peo": "Persa antiguo (ca. 600-400 A. C.)", + "phn": "Fenicio", + "pli": "Pali", + "pol": "Polaco", + "pon": "Pohnpeiano", + "por": "Portugués", + "pro": "Provenzal antiguo (hasta 1500)", + "pus": "Pastún", "que": "Quechua", - "raj": "Rajasthani", + "raj": "Rajastaní", "rap": "Rapanui", - "ron": "Rumence", - "roh": "Romanca", - "rom": "Çingene Dili", + "rar": "Maorí; Islas Cook", + "roh": "Romanche", + "rom": "Romaní", + "ron": "Rumano", "run": "Kirundi", - "rus": "Rusça", - "smo": "Samoa Dili", - "sad": "Sandawe", - "sag": "Sangho", - "san": "Sanskritçe", - "sat": "Santali dili", - "srd": "Sardinya", + "rup": "Rumano; Macedo-", + "rus": "Ruso", + "sad": "Sandavés", + "sag": "Sango", + "sah": "Yakuto", + "sam": "Arameo samaritano", + "san": "Sánscrito", "sas": "Sasak", - "sco": "İskoç lehçesi", + "sat": "Santalí", + "scn": "Siciliano", + "sco": "Escocés (germánico)", "sel": "Selkup", - "srp": "Sırpça", - "srr": "Serer", + "sga": "Irlandés antiguo (hasta 900)", "shn": "Shan", - "sna": "Shona", - "scn": "Sicilyalı", "sid": "Sidamo", - "bla": "Siksika (Kuzey Amerika yerlileri)", + "sin": "Cingalés", + "slk": "Eslovaco", + "slv": "Esloveno", + "sma": "Sami meridional", + "sme": "Sami septentrional", + "smj": "Sami de Lule", + "smn": "Sami de Inari", + "smo": "Samoano", + "sms": "Sami de Skolt", + "sna": "Shona", "snd": "Sindhi", - "sin": "Sinhala Dili", - "den": "Slave (Athapascan; Kuzey Amerika yerlileri)", - "slk": "Slovakça", - "slv": "Slovence", - "sog": "Sogdian", - "som": "Somali Dili", - "snk": "Soninke", - "spa": "İspanyolca", - "srn": "Sranan Tongo", + "snk": "Soninké", + "sog": "Sogdiano", + "som": "Somalí", + "sot": "Sesotho", + "spa": "Español", + "sqi": "Albanés", + "srd": "Sardo", + "srn": "Sranan tongo", + "srp": "Serbio", + "srr": "Serer", + "ssw": "Swazi", "suk": "Sukuma", - "sux": "Sümerce", - "sun": "Sudan Dili", + "sun": "Sundanés", "sus": "Susu", - "swa": "Swahili (makro dil)", - "ssw": "Siswati", - "swe": "İsveçce", - "syr": "Süryanice", - "tgl": "Tagalog", - "tah": "Tahitice", - "tgk": "Tacikçe", - "tmh": "Tamashek", - "tam": "Tamilce", - "tat": "Tatarca", - "tel": "Telugu", + "sux": "Sumerio", + "swa": "Suajili (macrolengua)", + "swe": "Sueco", + "syc": "Siríaco clásico", + "syr": "Siríaco", + "tah": "Tahitiano", + "tam": "Tamil", + "tat": "Tártaro", + "tel": "Telugú", + "tem": "Temné", "ter": "Tereno", - "tet": "Tetum", - "tha": "Taylandça", - "bod": "Tibetçe", - "tig": "Tigre", - "tir": "Tigrinya Dili", - "tem": "Timne", + "tet": "Tetun", + "tgk": "Tayiko", + "tgl": "Tagalo", + "tha": "Tailandés", + "tig": "Tigré", + "tir": "Tigriña", "tiv": "Tiv", + "tkl": "Tokelauano", + "tlh": "Klingon", "tli": "Tlingit", - "tpi": "Tok Pisin", - "tkl": "Tokelau", - "tog": "Tonga (Nyasa)", - "ton": "Tonga", + "tmh": "Targuí", + "tog": "Tonga (Malaui)", + "ton": "Tonga (Islas Tonga)", + "tpi": "Tok pisin", "tsi": "Tsimshian", + "tsn": "Setsuana", "tso": "Tsonga", - "tsn": "Setswana", + "tuk": "Turcomano", "tum": "Tumbuka", - "tur": "Türkçe", - "tuk": "Türkmence", - "tvl": "Tuvalu", - "tyv": "Tuvinian", + "tur": "Turco", + "tvl": "Tuvaluano", "twi": "Twi", + "tyv": "Tuvano", "udm": "Udmurt", - "uga": "Ugarit Çivi Yazısı", - "uig": "Uygurca", - "ukr": "Ukraynaca", + "uga": "Ugarítico", + "uig": "Uiguro", + "ukr": "Ucraniano", "umb": "Umbundu", - "mis": "Şifresiz diller", - "und": "Belirlenemeyen", - "urd": "Urduca", - "uzb": "Özbekçe", + "und": "Indeterminado", + "urd": "Urdu", + "uzb": "Uzbeko", "vai": "Vai", - "ven": "Venda Dili", - "vie": "Vietnamca", + "ven": "Venda", + "vie": "Vietnamita", "vol": "Volapük", - "vot": "Votic", - "wln": "Valonca", - "war": "Waray (Filipinler)", - "was": "Vasho", - "cym": "Gal Dili", + "vot": "Vótico", "wal": "Wolaytta", - "wol": "Wolof", + "war": "Waray (Filipinas)", + "was": "Washo", + "wln": "Valón", + "wol": "Wólof", + "xal": "Calmuco", "xho": "Xhosa", - "sah": "Yakut", "yao": "Yao", - "yap": "Yapese", - "yid": "Yidiş", + "yap": "Yapés", + "yid": "Yidis", "yor": "Yoruba", - "zap": "Zapotec", - "zza": "Zaza", + "zap": "Zapoteco", + "zbl": "Símbolos de Bliss", "zen": "Zenaga", - "zha": "Zuang Dili", - "zul": "Zulu", - "zun": "Zuni" + "zha": "Chuang", + "zho": "Chino", + "zul": "Zulú", + "zun": "Zuñi", + "zxx": "Sin contenido lingüístico", + "zza": "Zaza" + }, + "fi": { + "aar": "afar", + "abk": "abhaasi", + "ace": "aceh", + "ach": "atšoli", + "ada": "adangme", + "ady": "adyge", + "afh": "afrihili", + "afr": "afrikaans", + "ain": "Ainu (Japan)", + "aka": "Akan", + "akk": "akkadi", + "ale": "aleutti", + "alt": "Altai; Southern", + "amh": "amhara", + "ang": "English; Old (ca. 450-1100)", + "anp": "angika", + "ara": "arabia", + "arc": "Aramaic; Official (700-300 BCE)", + "arg": "aragonia", + "arn": "Mapudungun", + "arp": "arapaho", + "arw": "arawak", + "asm": "asami", + "ast": "asturia", + "ava": "avaari", + "ave": "avestan", + "awa": "awadhi", + "aym": "Aymara", + "aze": "azeri", + "bak": "baškiiri", + "bal": "belutši", + "bam": "Bambara", + "ban": "bali", + "bas": "Basa (Cameroon)", + "bej": "beja", + "bel": "valkovenäjä", + "bem": "Bemba (Zambia)", + "ben": "bengali", + "bho": "bhojpuri", + "bik": "bikol", + "bin": "bini", + "bis": "bislama", + "bla": "mustajalka (siksika)", + "bod": "tiibetti", + "bos": "bosnia", + "bra": "bradž", + "bre": "bretoni", + "bua": "burjaatti", + "bug": "bugi", + "bul": "bulgaria", + "byn": "Bilin", + "cad": "caddo", + "car": "Carib; Galibi", + "cat": "katalaani", + "ceb": "cebuano", + "ces": "tšekki", + "cha": "chamorro", + "chb": "chibcha", + "che": "tšetšeeni", + "chg": "Chagatai", + "chk": "chuuk", + "chm": "mari (Venäjä)", + "chn": "chinook-jargon", + "cho": "choctaw", + "chp": "chipewyan", + "chr": "cherokee", + "chu": "Slavonic; Old", + "chv": "tšuvassi", + "chy": "cheyenne", + "cop": "kopti", + "cor": "korni", + "cos": "korsika", + "cre": "cree", + "crh": "krimintataari", + "csb": "kašubi", + "cym": "kymri", + "dak": "dakota", + "dan": "tanska", + "dar": "dargva", + "del": "delaware", + "den": "athapaski-slavi", + "deu": "saksa", + "dgr": "dogrib", + "din": "Dinka", + "div": "Dhivehi", + "doi": "Dogri (macrolanguage)", + "dsb": "alasorbi", + "dua": "duala", + "dum": "Dutch; Middle (ca. 1050-1350)", + "dyu": "dyula", + "dzo": "dzongkha", + "efi": "efik", + "egy": "muinaisegypti", + "eka": "ekajuk", + "ell": "nykykreikka", + "elx": "elami", + "eng": "englanti", + "enm": "keskienglanti", + "epo": "esperanto", + "est": "viro", + "eus": "baski", + "ewe": "ewe", + "ewo": "ewondo", + "fan": "Fang (Equatorial Guinea)", + "fao": "fääri", + "fas": "persia", + "fat": "fanti", + "fij": "fidži", + "fil": "filipino", + "fin": "suomi", + "fon": "fon", + "fra": "ranska", + "frm": "French; Middle (ca. 1400-1600)", + "fro": "French; Old (842-ca. 1400)", + "frr": "Frisian; Northern", + "frs": "Frisian; Eastern", + "fry": "Frisian; Western", + "ful": "fulani", + "fur": "friuli", + "gaa": "gã", + "gay": "gayo", + "gba": "Gbaya (Central African Republic)", + "gez": "ge'ez", + "gil": "kiribati", + "gla": "Gaelic; Scottish", + "gle": "iiri", + "glg": "galicia", + "glv": "manksi", + "gmh": "German; Middle High (ca. 1050-1500)", + "goh": "German; Old High (ca. 750-1050)", + "gon": "gondi", + "gor": "gorontalo", + "got": "gootti", + "grb": "grebo", + "grc": "muinaiskreikka", + "grn": "guarani", + "gsw": "German; Swiss", + "guj": "gujarati", + "gwi": "Gwichʼin", + "hai": "haida", + "hat": "Creole; Haitian", + "hau": "hausa", + "haw": "havaiji", + "heb": "heprea", + "her": "herero", + "hil": "hiligaynon", + "hin": "hindi", + "hit": "heetti", + "hmn": "hmong", + "hmo": "hiri-motu", + "hrv": "kroatia", + "hsb": "Sorbian; Upper", + "hun": "unkari", + "hup": "hupa", + "hye": "armenia", + "iba": "Iban", + "ibo": "igbo", + "ido": "ido", + "iii": "Yi; Sichuan", + "iku": "inuktitut", + "ile": "interlingue", + "ilo": "iloko", + "ina": "interlingua", + "ind": "indonesia", + "inh": "inguuši", + "ipk": "iñupiaq", + "isl": "islanti", + "ita": "italia", + "jav": "jaava", + "jbo": "lojban", + "jpn": "japani", + "jpr": "juutalaispersia", + "jrb": "juutalaisarabia", + "kaa": "karakalpakki", + "kab": "kabyyli", + "kac": "kachin", + "kal": "grönlanti", + "kam": "Kamba (Kenya)", + "kan": "kannada", + "kas": "kashmiri", + "kat": "georgia", + "kau": "kanuri", + "kaw": "kavi", + "kaz": "kazakki", + "kbd": "kabardi", + "kha": "Khasi", + "khm": "Khmer; Central", + "kho": "khotani", + "kik": "kikuyu", + "kin": "ruanda", + "kir": "kirgiisi", + "kmb": "kimbundu", + "kok": "Konkani (macrolanguage)", + "kom": "komi", + "kon": "Kongo", + "kor": "korea", + "kos": "kosrae", + "kpe": "kpelle", + "krc": "karatšai-balkaari", + "krl": "karjala", + "kru": "kurukh", + "kua": "kuanjama", + "kum": "kumykki", + "kur": "kurdi", + "kut": "kutenai", + "lad": "ladino", + "lah": "lahnda", + "lam": "lamba", + "lao": "lao", + "lat": "latina", + "lav": "latvia", + "lez": "lezgi", + "lim": "Limburgan", + "lin": "lingala", + "lit": "liettua", + "lol": "mongo", + "loz": "lozi", + "ltz": "Luxemburg", + "lua": "luba (Lulua)", + "lub": "luba (Katanga)", + "lug": "Ganda", + "lui": "luiseño", + "lun": "lunda", + "luo": "luo", + "lus": "lushai", + "mad": "madura", + "mag": "magahi", + "mah": "Marshallese", + "mai": "maithili", + "mak": "Makasar", + "mal": "malayalam", + "man": "mandingo", + "mar": "marathi", + "mas": "masai", + "mdf": "mokša", + "mdr": "mandar", + "men": "Mende (Sierra Leone)", + "mga": "keski-iiri", + "mic": "Mi'kmaq", + "min": "minangkabau", + "mis": "Uncoded languages", + "mkd": "makedonia", + "mlg": "malagassi", + "mlt": "malta", + "mnc": "mantšu", + "mni": "manipuri", + "moh": "mohawk", + "mon": "Mongolian", + "mos": "mossi", + "mri": "maori", + "msa": "Malay (macrolanguage)", + "mul": "monia kieliä", + "mus": "muskogi", + "mwl": "mirandês", + "mwr": "marwari", + "mya": "burma", + "myv": "ersä", + "nap": "napoli", + "nau": "nauru", + "nav": "Navajo", + "nbl": "ndebele; eteländebele", + "nde": "ndebele; pohjoisndebele", + "ndo": "ndonga", + "nds": "German; Low", + "nep": "nepali", + "new": "Bhasa; Nepal", + "nia": "nias", + "niu": "niue", + "nld": "hollanti", + "nno": "norja (uusnorja)", + "nob": "Norwegian Bokmål", + "nog": "nogai", + "non": "muinaisnorja", + "nor": "norja", + "nqo": "N'Ko", + "nso": "Sotho; Northern", + "nwc": "Newari; Old", + "nya": "Nyanja", + "nym": "nyamwezi", + "nyn": "nyankole", + "nyo": "Nyoro", + "nzi": "nzima", + "oci": "Occitan (post 1500)", + "oji": "Ojibwa", + "ori": "oriya", + "orm": "oromo", + "osa": "osage", + "oss": "Ossetian", + "ota": "osmaninturkki", + "pag": "pangasinan", + "pal": "pahlavi", + "pam": "pampanga", + "pan": "Panjabi", + "pap": "papiamentu", + "pau": "palau", + "peo": "Persian; Old (ca. 600-400 B.C.)", + "phn": "foinikia", + "pli": "Pali", + "pol": "puola", + "pon": "pohnpei", + "por": "portugali", + "pro": "muinaisprovensaali", + "pus": "pašto", + "que": "Quechua", + "raj": "rajasthani", + "rap": "rapanui", + "rar": "Maori; Cook Islands", + "roh": "Romansh", + "rom": "romani", + "ron": "romania", + "run": "rundi", + "rup": "Romanian; Macedo-", + "rus": "venäjä", + "sad": "sandawe", + "sag": "Sango", + "sah": "jakuutti", + "sam": "Aramaic; Samaritan", + "san": "sanskrit", + "sas": "Sasak", + "sat": "santali", + "scn": "sisilia", + "sco": "skotti", + "sel": "selkuppi", + "sga": "muinaisiiri", + "shn": "shan", + "sid": "sidamo", + "sin": "Sinhala", + "slk": "slovakki", + "slv": "sloveeni", + "sma": "eteläsaame", + "sme": "pohjoissaame", + "smj": "luulajansaame", + "smn": "inarinsaame", + "smo": "samoa", + "sms": "koltansaami", + "sna": "shona", + "snd": "sindhi", + "snk": "soninke", + "sog": "sogdi", + "som": "somali", + "sot": "eteläsotho", + "spa": "espanja", + "sqi": "albania", + "srd": "sardi", + "srn": "Sranan Tongo", + "srp": "serbia", + "srr": "Serer", + "ssw": "swazi", + "suk": "sukuma", + "sun": "sunda", + "sus": "susu", + "sux": "sumeri", + "swa": "Swahili (macrolanguage)", + "swe": "ruotsi", + "syc": "Syriac; Classical", + "syr": "syyria", + "tah": "tahiti", + "tam": "tamili", + "tat": "tataari", + "tel": "Telugu", + "tem": "temne", + "ter": "tereno", + "tet": "tetum", + "tgk": "tadžikki", + "tgl": "tagalog", + "tha": "thai", + "tig": "tigre", + "tir": "tigrinya", + "tiv": "tiv", + "tkl": "tokelau", + "tlh": "Klingon", + "tli": "tlinglit", + "tmh": "Tamashek", + "tog": "Malawin tonga", + "ton": "Tongan tonga", + "tpi": "tok-pisin", + "tsi": "tsimshian", + "tsn": "tswana", + "tso": "tsonga", + "tuk": "turkmeeni", + "tum": "tumbuka", + "tur": "turkki", + "tvl": "tuvalu", + "twi": "twi", + "tyv": "tuva", + "udm": "udmurtti", + "uga": "ugarit", + "uig": "uiguuri", + "ukr": "ukraina", + "umb": "umbundu", + "und": "määrittämätön", + "urd": "urdu", + "uzb": "uzbekki", + "vai": "vai", + "ven": "venda", + "vie": "vietnam", + "vol": "volapük", + "vot": "vatja", + "wal": "Wolaytta", + "war": "Waray (Philippines)", + "was": "washo", + "wln": "valloni", + "wol": "wolof", + "xal": "Kalmyk", + "xho": "xhosa", + "yao": "mien", + "yap": "yap", + "yid": "jiddiš", + "yor": "yoruba", + "zap": "Zapotec", + "zbl": "Blissymbols", + "zen": "zenaga", + "zha": "Zhuang", + "zho": "kiina", + "zul": "zulu", + "zun": "zuni", + "zxx": "No linguistic content", + "zza": "Zaza" }, "fr": { "aar": "afar", @@ -2440,1266 +2907,6 @@ LANGUAGE_NAMES = { "zxx": "No linguistic content", "zza": "Zaza" }, - "ru": { - "aar": "Афар", - "abk": "Абхазский", - "ace": "Ачехский", - "ach": "Ачоли", - "ada": "Адангме", - "ady": "Адыгейский", - "afh": "Африхили", - "afr": "Африкаанс", - "ain": "Ainu (Japan)", - "aka": "Акан", - "akk": "Аккадский", - "ale": "Алеутский", - "alt": "Altai; Southern", - "amh": "Амхарский (Амаринья)", - "ang": "English; Old (ca. 450-1100)", - "anp": "Анжика", - "ara": "Арабский", - "arc": "Арамейский; Официальный", - "arg": "Арагонский", - "arn": "Mapudungun", - "arp": "Арапахо", - "arw": "Аравакский", - "asm": "Ассамский", - "ast": "Астурийский", - "ava": "Аварский", - "ave": "Авестийский", - "awa": "Авадхи", - "aym": "Аймара", - "aze": "Азербайджанский", - "bak": "Башкирский", - "bal": "Baluchi", - "bam": "Бамбара", - "ban": "Балийский", - "bas": "Баса (Камерун)", - "bej": "Беджа", - "bel": "Белорусский", - "bem": "Бемба (Замбия)", - "ben": "Бенгальский", - "bho": "Бходжпури", - "bik": "Бикольский", - "bin": "Бини", - "bis": "Бислама", - "bla": "Сиксика", - "bod": "Тибетский", - "bos": "Боснийский", - "bra": "Браун", - "bre": "Бретонский", - "bua": "Бурятский", - "bug": "Бугийский", - "bul": "Болгарский", - "byn": "Bilin", - "cad": "Каддо", - "car": "Carib; Galibi", - "cat": "Каталанский", - "ceb": "Себуано", - "ces": "Чешский", - "cha": "Чаморро", - "chb": "Чибча", - "che": "Чеченский", - "chg": "Чагатайский", - "chk": "Трукский", - "chm": "Марийский (Россия)", - "chn": "Чинук жаргон", - "cho": "Чоктав", - "chp": "Чипевианский", - "chr": "Чероки", - "chu": "Slavonic; Old", - "chv": "Чувашский", - "chy": "Чейенн", - "cop": "Коптский", - "cor": "Корнский", - "cos": "Корсиканский", - "cre": "Кри", - "crh": "Turkish; Crimean", - "csb": "Кашубианский", - "cym": "Уэльский (Валлийский)", - "dak": "Дакота", - "dan": "Датский", - "dar": "Даргва", - "del": "Делаварский", - "den": "Атапачские языки", - "deu": "Немецкий", - "dgr": "Догриб", - "din": "Динка", - "div": "Dhivehi", - "doi": "Dogri (macrolanguage)", - "dsb": "Sorbian; Lower", - "dua": "Дуала", - "dum": "Dutch; Middle (ca. 1050-1350)", - "dyu": "Диула (Дьюла)", - "dzo": "Дзонг-кэ", - "efi": "Эфик", - "egy": "Древнеегипетский", - "eka": "Экаджук", - "ell": "Новогреческий (с 1453)", - "elx": "Эламский", - "eng": "Английский", - "enm": "Среднеанглийский (1100-1500)", - "epo": "Эсперанто", - "est": "Эстонский", - "eus": "Баскский", - "ewe": "Эве", - "ewo": "Эвондо", - "fan": "Fang (Equatorial Guinea)", - "fao": "Фарерский", - "fas": "Персидский", - "fat": "Фанти", - "fij": "Фиджийский", - "fil": "Filipino", - "fin": "Финский", - "fon": "Фон", - "fra": "Французский", - "frm": "French; Middle (ca. 1400-1600)", - "fro": "French; Old (842-ca. 1400)", - "frr": "Frisian; Northern", - "frs": "Frisian; Eastern", - "fry": "Frisian; Western", - "ful": "Фулах", - "fur": "Фриулианский", - "gaa": "Га", - "gay": "Гайо", - "gba": "Gbaya (Central African Republic)", - "gez": "Геэз", - "gil": "Гильбертский", - "gla": "Gaelic; Scottish", - "gle": "Ирландский", - "glg": "Galician", - "glv": "Мэнкский", - "gmh": "German; Middle High (ca. 1050-1500)", - "goh": "German; Old High (ca. 750-1050)", - "gon": "Гонди", - "gor": "Горонтало", - "got": "Готский", - "grb": "Гребо", - "grc": "Древнегреческий (по 1453)", - "grn": "Гуарани", - "gsw": "German; Swiss", - "guj": "Гуджарати", - "gwi": "Gwichʼin", - "hai": "Хайда", - "hat": "Creole; Haitian", - "hau": "Хауса", - "haw": "Гавайский", - "heb": "Иврит", - "her": "Гереро", - "hil": "Хилигайнон", - "hin": "Хинди", - "hit": "Хиттит", - "hmn": "Хмонг", - "hmo": "Хири Моту", - "hrv": "Хорватский", - "hsb": "Sorbian; Upper", - "hun": "Венгерский", - "hup": "Хупа", - "hye": "Армянский", - "iba": "Ибанский", - "ibo": "Игбо", - "ido": "Идо", - "iii": "Yi; Sichuan", - "iku": "Инуктитут", - "ile": "Интерлингве", - "ilo": "Илоко", - "ina": "Интерлингва (Ассоциация международного вспомогательного языка)", - "ind": "Индонезийский", - "inh": "Ингушский", - "ipk": "Инулиак", - "isl": "Исландский", - "ita": "Итальянский", - "jav": "Яванский", - "jbo": "Лоджбан", - "jpn": "Японский", - "jpr": "Еврейско-персидский", - "jrb": "Еврейско-арабский", - "kaa": "Каракалпакский", - "kab": "Кабильский", - "kac": "Качинский", - "kal": "Kalaallisut", - "kam": "Kamba (Kenya)", - "kan": "Каннада", - "kas": "Кашмири", - "kat": "Грузинский", - "kau": "Канури", - "kaw": "Кави", - "kaz": "Казахский", - "kbd": "Кабардинский", - "kha": "Кхаси", - "khm": "Khmer; Central", - "kho": "Хотанский", - "kik": "Кикуйю", - "kin": "Киньяруанда", - "kir": "Киргизский", - "kmb": "Кимбунду", - "kok": "Konkani (macrolanguage)", - "kom": "Коми", - "kon": "Конго", - "kor": "Корейский", - "kos": "Косраинский", - "kpe": "Кпелле", - "krc": "Карачаево-балкарский", - "krl": "Карельский", - "kru": "Курух", - "kua": "Киньяма", - "kum": "Кумыкский", - "kur": "Курдский", - "kut": "Кутенаи", - "lad": "Ладино", - "lah": "Лахнда", - "lam": "Ламба", - "lao": "Лаосский", - "lat": "Латинский", - "lav": "Латвийский", - "lez": "Лезгинский", - "lim": "Limburgan", - "lin": "Лингала", - "lit": "Литовский", - "lol": "Монго", - "loz": "Лози", - "ltz": "Luxembourgish", - "lua": "Луба-Лулуа", - "lub": "Луба-Катанга", - "lug": "Ганда", - "lui": "Луисеньо", - "lun": "Лунда", - "luo": "Луо (Кения и Танзания)", - "lus": "Лушай", - "mad": "Мадурский", - "mag": "Магахи", - "mah": "Marshallese", - "mai": "Майтхили", - "mak": "Макассарский", - "mal": "Малаялам", - "man": "Мандинго", - "mar": "Маратхи", - "mas": "Масаи", - "mdf": "Мокшанский", - "mdr": "Мандарский", - "men": "Mende (Sierra Leone)", - "mga": "Среднеирландский (900-1200)", - "mic": "Mi'kmaq", - "min": "Минангкабау", - "mis": "Uncoded languages", - "mkd": "Македонский", - "mlg": "Малагаси", - "mlt": "Мальтийский", - "mnc": "Манчу", - "mni": "Манипури", - "moh": "Мохаук", - "mon": "Монгольский", - "mos": "Моей", - "mri": "Маори", - "msa": "Malay (macrolanguage)", - "mul": "Разных семей языки", - "mus": "Крик", - "mwl": "Мирандские", - "mwr": "Марвари", - "mya": "Бирманский", - "myv": "Эрзянский", - "nap": "Неаполитанский", - "nau": "Науру", - "nav": "Navajo", - "nbl": "Ндебеле южный", - "nde": "Ндебеле северный", - "ndo": "Ндунга", - "nds": "German; Low", - "nep": "Непальский", - "new": "Bhasa; Nepal", - "nia": "Ниас", - "niu": "Ниуэ", - "nld": "Нидерландский", - "nno": "Норвежский Нюнорск", - "nob": "Norwegian Bokmål", - "nog": "Ногайский", - "non": "Старонорвежский", - "nor": "Норвежский", - "nqo": "Н'ко", - "nso": "Sotho; Northern", - "nwc": "Newari; Old", - "nya": "Nyanja", - "nym": "Ньямвези", - "nyn": "Ньянколе", - "nyo": "Ньоро", - "nzi": "Нзима", - "oci": "Occitan (post 1500)", - "oji": "Оджибва", - "ori": "Ория", - "orm": "Оромо", - "osa": "Оседжи", - "oss": "Ossetian", - "ota": "Турецкий; Отомангский (1500-1928)", - "pag": "Пангасинан", - "pal": "Пехлевийский", - "pam": "Пампанга", - "pan": "Panjabi", - "pap": "Папьяменто", - "pau": "Палау", - "peo": "Persian; Old (ca. 600-400 B.C.)", - "phn": "Финикийский", - "pli": "Пали", - "pol": "Польский", - "pon": "Фонпейский", - "por": "Португальский", - "pro": "Старопровансальский (по 1500)", - "pus": "Пушту", - "que": "Кечуа", - "raj": "Раджастхани", - "rap": "Рапаню", - "rar": "Maori; Cook Islands", - "roh": "Romansh", - "rom": "Цыганский", - "ron": "Румынский", - "run": "Рунди", - "rup": "Romanian; Macedo-", - "rus": "Русский", - "sad": "Сандаве", - "sag": "Санго", - "sah": "Якутский", - "sam": "Aramaic; Samaritan", - "san": "Санскрит", - "sas": "Сасакский", - "sat": "Сантали", - "scn": "Сицилийский", - "sco": "Шотландский", - "sel": "Селкапский", - "sga": "Староирландский (по 900)", - "shn": "Шанский", - "sid": "Сидама", - "sin": "Сингальский", - "slk": "Словацкий", - "slv": "Словенский", - "sma": "Sami; Southern", - "sme": "Sami; Northern", - "smj": "Люле-саамский", - "smn": "Sami; Inari", - "smo": "Самоанский", - "sms": "Sami; Skolt", - "sna": "Шона", - "snd": "Синдхи", - "snk": "Сонинк", - "sog": "Согдийский", - "som": "Сомали", - "sot": "Сото Южный", - "spa": "Испанский", - "sqi": "Албанский", - "srd": "Сардинский", - "srn": "Sranan Tongo", - "srp": "Сербский", - "srr": "Серер", - "ssw": "Свати", - "suk": "Сукума", - "sun": "Сунданский", - "sus": "Сусу", - "sux": "Шумерский", - "swa": "Swahili (macrolanguage)", - "swe": "Шведский", - "syc": "Syriac; Classical", - "syr": "Сирийский", - "tah": "Таитянский", - "tam": "Тамильский", - "tat": "Татарский", - "tel": "Телугу", - "tem": "Темне", - "ter": "Терено", - "tet": "Тетумский", - "tgk": "Таджикский", - "tgl": "Тагалог", - "tha": "Таи", - "tig": "Тигре", - "tir": "Тигринья", - "tiv": "Тив", - "tkl": "Токелау", - "tlh": "Klingon", - "tli": "Тлингит", - "tmh": "Тамашек", - "tog": "Тонга (Ньяса)", - "ton": "Тонга (острова Тонга)", - "tpi": "Ток Писин", - "tsi": "Цимшиан", - "tsn": "Тсвана", - "tso": "Тсонга", - "tuk": "Туркменский", - "tum": "Тумбука", - "tur": "Турецкий", - "tvl": "Тувалу", - "twi": "Тви", - "tyv": "Тувинский", - "udm": "Удмуртский", - "uga": "Угаритский", - "uig": "Уйгурский", - "ukr": "Украинский", - "umb": "Умбунду", - "und": "Неидентифицированный", - "urd": "Урду", - "uzb": "Узбекский", - "vai": "Ваи", - "ven": "Венда", - "vie": "Вьетнамский", - "vol": "Волапюк", - "vot": "Вотик", - "wal": "Wolaytta", - "war": "Waray (Philippines)", - "was": "Вашо", - "wln": "Валлун", - "wol": "Волоф", - "xal": "Kalmyk", - "xho": "Коса", - "yao": "Яо", - "yap": "Яапийский", - "yid": "Идиш", - "yor": "Йоруба", - "zap": "Сапотекский", - "zbl": "Blissymbols", - "zen": "Зенагский", - "zha": "Чжуанский", - "zho": "Китайский", - "zul": "Зулусский", - "zun": "Зуньи", - "zxx": "Нет языкового содержимого", - "zza": "Зазаки" - }, - "zh_Hans_CN": { - "aar": "阿法尔语", - "abk": "阿布哈兹语", - "ace": "亚齐语", - "ach": "阿乔利语", - "ada": "阿当梅语", - "ady": "阿迪格语", - "afh": "阿弗里希利语", - "afr": "南非荷兰语", - "ain": "阿伊努语(日本)", - "aka": "阿坎语", - "akk": "阿卡德语", - "ale": "阿留申语", - "alt": "阿尔泰语(南)", - "amh": "阿姆哈拉语", - "ang": "英语(上古,约 450-1100)", - "anp": "安吉卡语", - "ara": "阿拉伯语", - "arc": "阿拉米语(官方,公元前 700-300)", - "arg": "阿拉贡语", - "arn": "阿劳坎语", - "arp": "阿拉帕霍语", - "arw": "阿拉瓦克语", - "asm": "阿萨姆语", - "ast": "阿斯图里亚斯语", - "ava": "阿瓦尔语", - "ave": "阿维斯陀语", - "awa": "阿瓦德语", - "aym": "艾马拉语", - "aze": "阿塞拜疆语", - "bak": "巴什基尔语", - "bal": "俾路支语", - "bam": "班巴拉语", - "ban": "巴厘语", - "bas": "巴萨语(喀麦隆)", - "bej": "贝扎语", - "bel": "白俄罗斯语", - "bem": "本巴语(赞比亚)", - "ben": "孟加拉语", - "bho": "博杰普尔语", - "bik": "比科尔语", - "bin": "比尼语", - "bis": "比斯拉马语", - "bla": "西克西卡语", - "bod": "藏语", - "bos": "波斯尼亚语", - "bra": "布拉吉语", - "bre": "布列塔尼语", - "bua": "布里亚特语", - "bug": "布吉语", - "bul": "保加利亚语", - "byn": "比林语", - "cad": "卡多语", - "car": "加勒比语", - "cat": "加泰罗尼亚语", - "ceb": "宿务语", - "ces": "捷克语", - "cha": "查莫罗语", - "chb": "奇布查语", - "che": "车臣语", - "chg": "察合台语", - "chk": "丘克语", - "chm": "马里语(俄罗斯)", - "chn": "奇努克混合语", - "cho": "乔克托语", - "chp": "奇佩维安语", - "chr": "切罗基语", - "chu": "斯拉夫语(古教会)", - "chv": "楚瓦什语", - "chy": "夏延语", - "cop": "科普特语", - "cor": "康沃尔语", - "cos": "科西嘉语", - "cre": "克里语", - "crh": "鞑靼语(克里米亚)", - "csb": "卡舒比语", - "cym": "威尔士语", - "dak": "达科他语", - "dan": "丹麦语", - "dar": "达尔格瓦语", - "del": "特拉华语", - "den": "史拉维语(阿沙巴斯甘)", - "deu": "德语", - "dgr": "多格里布语", - "din": "丁卡语", - "div": "迪维希语", - "doi": "多格拉语", - "dsb": "索布语(下)", - "dua": "杜亚拉语", - "dum": "荷兰语(中古,约 1050-1350)", - "dyu": "迪尤拉语", - "dzo": "宗喀语", - "efi": "埃菲克语", - "egy": "埃及语(古)", - "eka": "埃克丘克语", - "ell": "希腊语(现代,1453-)", - "elx": "埃兰语", - "eng": "英语", - "enm": "英语(中古,1100-1500)", - "epo": "世界语", - "est": "爱沙尼亚语", - "eus": "巴斯克语", - "ewe": "埃维语", - "ewo": "埃翁多语", - "fan": "芳语(赤道几内亚)", - "fao": "法罗语", - "fas": "波斯语", - "fat": "芳蒂语", - "fij": "斐济语", - "fil": "菲律宾语", - "fin": "芬兰语", - "fon": "丰语", - "fra": "法语", - "frm": "法语(中古,约 1400-1600)", - "fro": "法语(上古,842-约 1400)", - "frr": "弗里西语(北)", - "frs": "弗里西亚语(东)", - "fry": "弗里西亚语(西)", - "ful": "富拉语", - "fur": "弗留利语", - "gaa": "加语", - "gay": "卡约语", - "gba": "巴亚语(中非共和国)", - "gez": "吉兹语", - "gil": "吉尔伯特语", - "gla": "盖尔语(苏格兰)", - "gle": "爱尔兰语", - "glg": "加利西亚语", - "glv": "马恩岛语", - "gmh": "德语(中古高地,约 1050-1500)", - "goh": "德语(上古高地,约 750-1050)", - "gon": "贡德语", - "gor": "哥伦打洛语", - "got": "哥特语", - "grb": "格列博语", - "grc": "希腊语(古典,直到 1453)", - "grn": "瓜拉尼语", - "gsw": "德语(瑞士)", - "guj": "古吉拉特语", - "gwi": "库臣语", - "hai": "海达语", - "hat": "克里奥尔语(海地)", - "hau": "豪萨语", - "haw": "夏威夷语", - "heb": "希伯来语", - "her": "赫雷罗语", - "hil": "希利盖农语", - "hin": "印地语", - "hit": "赫梯语", - "hmn": "苗语", - "hmo": "希里莫图语", - "hrv": "克罗地亚语", - "hsb": "索布语(上)", - "hun": "匈牙利语", - "hup": "胡帕语", - "hye": "亚美尼亚语", - "iba": "伊班语", - "ibo": "伊博语", - "ido": "伊多语", - "iii": "彝语(四川)", - "iku": "伊努伊特语", - "ile": "国际语(西方)", - "ilo": "伊洛卡诺语", - "ina": "国际语", - "ind": "印尼语", - "inh": "印古什语", - "ipk": "依努庇克语", - "isl": "冰岛语", - "ita": "意大利语", - "jav": "爪哇语", - "jbo": "逻辑语", - "jpn": "日语", - "jpr": "犹太-波斯语", - "jrb": "犹太-阿拉伯语", - "kaa": "卡拉卡尔帕克语", - "kab": "卡布列语", - "kac": "景颇语", - "kal": "格陵兰语", - "kam": "坎巴语(肯尼亚)", - "kan": "卡纳达语", - "kas": "克什米尔语", - "kat": "格鲁吉亚语", - "kau": "卡努里语", - "kaw": "卡威语", - "kaz": "哈萨克语", - "kbd": "卡巴尔达语", - "kha": "卡西语", - "khm": "高棉语", - "kho": "和田语", - "kik": "基库尤语", - "kin": "基尼阿万达语", - "kir": "吉尔吉斯语", - "kmb": "金本杜语", - "kok": "孔卡尼语", - "kom": "科米语", - "kon": "刚果语", - "kor": "朝鲜语", - "kos": "科斯拉伊语", - "kpe": "克佩勒语", - "krc": "卡拉恰伊-巴尔卡尔语", - "krl": "卡累利阿语", - "kru": "库卢克语", - "kua": "宽亚玛语", - "kum": "库梅克语", - "kur": "库尔德语", - "kut": "库特内语", - "lad": "拉迪诺语", - "lah": "拉亨达语", - "lam": "兰巴语", - "lao": "老挝语", - "lat": "拉丁语", - "lav": "拉脱维亚语", - "lez": "列兹金语", - "lim": "林堡语", - "lin": "林加拉语", - "lit": "立陶宛语", - "lol": "芒戈语", - "loz": "洛齐语", - "ltz": "卢森堡语", - "lua": "卢巴-卢拉语", - "lub": "卢巴-加丹加语", - "lug": "干达语", - "lui": "卢伊塞诺语", - "lun": "隆达语", - "luo": "卢奥语(肯尼亚和坦桑尼亚)", - "lus": "卢萨语", - "mad": "马都拉语", - "mag": "摩揭陀语", - "mah": "马绍尔语", - "mai": "米德勒语", - "mak": "望加锡语", - "mal": "马拉雅拉姆语", - "man": "曼丁哥语", - "mar": "马拉地语", - "mas": "马萨伊语", - "mdf": "莫克沙语", - "mdr": "曼达语", - "men": "门德语(塞拉利昂)", - "mga": "爱尔兰语(中古,900-1200)", - "mic": "米克马克语", - "min": "米南卡保语", - "mis": "未被编码的语言", - "mkd": "马其顿语", - "mlg": "马达加斯加语", - "mlt": "马耳他语", - "mnc": "满语", - "mni": "曼尼普尔语", - "moh": "莫霍克语", - "mon": "蒙古语", - "mos": "莫西语", - "mri": "毛利语", - "msa": "马来语族", - "mul": "多种语言", - "mus": "克里克语", - "mwl": "米兰德斯语", - "mwr": "马尔瓦利语", - "mya": "缅甸语", - "myv": "厄尔兹亚语", - "nap": "拿坡里语", - "nau": "瑙鲁语", - "nav": "纳瓦霍语", - "nbl": "恩德贝勒语(南)", - "nde": "恩德贝勒语(北)", - "ndo": "恩敦加语", - "nds": "撒克逊语(低地)", - "nep": "尼泊尔语", - "new": "尼瓦尔语", - "nia": "尼亚斯语", - "niu": "纽埃语", - "nld": "荷兰语", - "nno": "新挪威语", - "nob": "挪威布克莫尔语", - "nog": "诺盖语", - "non": "诺尔斯语(古)", - "nor": "挪威语", - "nqo": "西非书面语言字母", - "nso": "索托语(北)", - "nwc": "尼瓦尔语(古典)", - "nya": "尼扬贾语", - "nym": "尼扬韦齐语", - "nyn": "尼扬科勒语", - "nyo": "尼奥罗语", - "nzi": "恩济马语", - "oci": "奥克西唐语(1500 后)", - "oji": "奥吉布瓦语", - "ori": "奥利亚语", - "orm": "奥罗莫语", - "osa": "奥萨格语", - "oss": "奥塞梯语", - "ota": "土耳其语(奥斯曼,1500-1928)", - "pag": "邦阿西楠语", - "pal": "钵罗钵语", - "pam": "邦板牙语", - "pan": "旁遮普语", - "pap": "帕皮亚门托语", - "pau": "帕劳语", - "peo": "波斯语(古,公元前约 600-400)", - "phn": "腓尼基语", - "pli": "巴利语", - "pol": "波兰语", - "pon": "波纳佩语", - "por": "葡萄牙语", - "pro": "普罗旺斯语(古,至 1500)", - "pus": "普什图语", - "que": "克丘亚语", - "raj": "拉贾斯坦语", - "rap": "拉帕努伊语", - "rar": "拉罗汤加语", - "roh": "罗曼什语", - "rom": "罗姆语", - "ron": "罗马尼亚语", - "run": "基隆迪语", - "rup": "阿罗马尼亚语", - "rus": "俄语", - "sad": "桑达韦语", - "sag": "桑戈语", - "sah": "雅库特语", - "sam": "阿拉米语(萨马利亚)", - "san": "梵语", - "sas": "萨萨克语", - "sat": "桑塔利语", - "scn": "西西里语", - "sco": "苏格兰语", - "sel": "塞尔库普语", - "sga": "爱尔兰语(古,至 900)", - "shn": "掸语", - "sid": "锡达莫语", - "sin": "僧加罗语", - "slk": "斯洛伐克语", - "slv": "斯洛文尼亚语", - "sma": "萨米语(南)", - "sme": "萨米语(北)", - "smj": "律勒欧-萨米语", - "smn": "伊纳里-萨米语", - "smo": "萨摩亚语", - "sms": "斯科特-萨米语", - "sna": "修纳语", - "snd": "信德语", - "snk": "索宁克语", - "sog": "粟特语", - "som": "索马里语", - "sot": "索托语(南)", - "spa": "西班牙语", - "sqi": "阿尔巴尼亚语", - "srd": "撒丁语", - "srn": "苏里南汤加语", - "srp": "塞尔维亚语", - "srr": "塞雷尔语", - "ssw": "斯瓦特语", - "suk": "苏库马语", - "sun": "巽他语", - "sus": "苏苏语", - "sux": "苏美尔语", - "swa": "斯瓦希里语族", - "swe": "瑞典语", - "syc": "叙利亚语(古典)", - "syr": "古叙利亚语", - "tah": "塔希提语", - "tam": "泰米尔语", - "tat": "塔塔尔语", - "tel": "泰卢固语", - "tem": "滕内语", - "ter": "特列纳语", - "tet": "特塔姆语", - "tgk": "塔吉克语", - "tgl": "塔加洛语", - "tha": "泰语", - "tig": "提格雷语", - "tir": "提格里尼亚语", - "tiv": "蒂夫语", - "tkl": "托克劳语", - "tlh": "克林贡语", - "tli": "特林吉特语", - "tmh": "塔马舍克语", - "tog": "汤加语 (尼亚萨)", - "ton": "汤加语(汤加岛)", - "tpi": "托克皮辛语", - "tsi": "钦西安语", - "tsn": "茨瓦纳语", - "tso": "聪加语", - "tuk": "土库曼语", - "tum": "奇图姆布卡语", - "tur": "土耳其语", - "tvl": "图瓦卢语", - "twi": "契维语", - "tyv": "图瓦语", - "udm": "乌德穆尔特语", - "uga": "乌加里特语", - "uig": "维吾尔语", - "ukr": "乌克兰语", - "umb": "翁本杜语", - "und": "未确定的语言", - "urd": "乌尔都语", - "uzb": "乌兹别克语", - "vai": "瓦伊语", - "ven": "文达语", - "vie": "越南语", - "vol": "沃拉普克语", - "vot": "沃提克语", - "wal": "瓦拉莫语", - "war": "瓦赖语(菲律宾)", - "was": "瓦肖语", - "wln": "瓦龙语", - "wol": "沃洛夫语", - "xal": "卡尔梅克语", - "xho": "科萨语", - "yao": "瑶语", - "yap": "雅浦语", - "yid": "依地语", - "yor": "约鲁巴语", - "zap": "萨波特克语", - "zbl": "布利斯符号", - "zen": "哲纳加语", - "zha": "壮语", - "zho": "中文", - "zul": "祖鲁语", - "zun": "祖尼语", - "zxx": "No linguistic content", - "zza": "扎扎其语" - }, - "fi": { - "aar": "afar", - "abk": "abhaasi", - "ace": "aceh", - "ach": "atšoli", - "ada": "adangme", - "ady": "adyge", - "afh": "afrihili", - "afr": "afrikaans", - "ain": "Ainu (Japan)", - "aka": "Akan", - "akk": "akkadi", - "ale": "aleutti", - "alt": "Altai; Southern", - "amh": "amhara", - "ang": "English; Old (ca. 450-1100)", - "anp": "angika", - "ara": "arabia", - "arc": "Aramaic; Official (700-300 BCE)", - "arg": "aragonia", - "arn": "Mapudungun", - "arp": "arapaho", - "arw": "arawak", - "asm": "asami", - "ast": "asturia", - "ava": "avaari", - "ave": "avestan", - "awa": "awadhi", - "aym": "Aymara", - "aze": "azeri", - "bak": "baškiiri", - "bal": "belutši", - "bam": "Bambara", - "ban": "bali", - "bas": "Basa (Cameroon)", - "bej": "beja", - "bel": "valkovenäjä", - "bem": "Bemba (Zambia)", - "ben": "bengali", - "bho": "bhojpuri", - "bik": "bikol", - "bin": "bini", - "bis": "bislama", - "bla": "mustajalka (siksika)", - "bod": "tiibetti", - "bos": "bosnia", - "bra": "bradž", - "bre": "bretoni", - "bua": "burjaatti", - "bug": "bugi", - "bul": "bulgaria", - "byn": "Bilin", - "cad": "caddo", - "car": "Carib; Galibi", - "cat": "katalaani", - "ceb": "cebuano", - "ces": "tšekki", - "cha": "chamorro", - "chb": "chibcha", - "che": "tšetšeeni", - "chg": "Chagatai", - "chk": "chuuk", - "chm": "mari (Venäjä)", - "chn": "chinook-jargon", - "cho": "choctaw", - "chp": "chipewyan", - "chr": "cherokee", - "chu": "Slavonic; Old", - "chv": "tšuvassi", - "chy": "cheyenne", - "cop": "kopti", - "cor": "korni", - "cos": "korsika", - "cre": "cree", - "crh": "krimintataari", - "csb": "kašubi", - "cym": "kymri", - "dak": "dakota", - "dan": "tanska", - "dar": "dargva", - "del": "delaware", - "den": "athapaski-slavi", - "deu": "saksa", - "dgr": "dogrib", - "din": "Dinka", - "div": "Dhivehi", - "doi": "Dogri (macrolanguage)", - "dsb": "alasorbi", - "dua": "duala", - "dum": "Dutch; Middle (ca. 1050-1350)", - "dyu": "dyula", - "dzo": "dzongkha", - "efi": "efik", - "egy": "muinaisegypti", - "eka": "ekajuk", - "ell": "nykykreikka", - "elx": "elami", - "eng": "englanti", - "enm": "keskienglanti", - "epo": "esperanto", - "est": "viro", - "eus": "baski", - "ewe": "ewe", - "ewo": "ewondo", - "fan": "Fang (Equatorial Guinea)", - "fao": "fääri", - "fas": "persia", - "fat": "fanti", - "fij": "fidži", - "fil": "filipino", - "fin": "suomi", - "fon": "fon", - "fra": "ranska", - "frm": "French; Middle (ca. 1400-1600)", - "fro": "French; Old (842-ca. 1400)", - "frr": "Frisian; Northern", - "frs": "Frisian; Eastern", - "fry": "Frisian; Western", - "ful": "fulani", - "fur": "friuli", - "gaa": "gã", - "gay": "gayo", - "gba": "Gbaya (Central African Republic)", - "gez": "ge'ez", - "gil": "kiribati", - "gla": "Gaelic; Scottish", - "gle": "iiri", - "glg": "galicia", - "glv": "manksi", - "gmh": "German; Middle High (ca. 1050-1500)", - "goh": "German; Old High (ca. 750-1050)", - "gon": "gondi", - "gor": "gorontalo", - "got": "gootti", - "grb": "grebo", - "grc": "muinaiskreikka", - "grn": "guarani", - "gsw": "German; Swiss", - "guj": "gujarati", - "gwi": "Gwichʼin", - "hai": "haida", - "hat": "Creole; Haitian", - "hau": "hausa", - "haw": "havaiji", - "heb": "heprea", - "her": "herero", - "hil": "hiligaynon", - "hin": "hindi", - "hit": "heetti", - "hmn": "hmong", - "hmo": "hiri-motu", - "hrv": "kroatia", - "hsb": "Sorbian; Upper", - "hun": "unkari", - "hup": "hupa", - "hye": "armenia", - "iba": "Iban", - "ibo": "igbo", - "ido": "ido", - "iii": "Yi; Sichuan", - "iku": "inuktitut", - "ile": "interlingue", - "ilo": "iloko", - "ina": "interlingua", - "ind": "indonesia", - "inh": "inguuši", - "ipk": "iñupiaq", - "isl": "islanti", - "ita": "italia", - "jav": "jaava", - "jbo": "lojban", - "jpn": "japani", - "jpr": "juutalaispersia", - "jrb": "juutalaisarabia", - "kaa": "karakalpakki", - "kab": "kabyyli", - "kac": "kachin", - "kal": "grönlanti", - "kam": "Kamba (Kenya)", - "kan": "kannada", - "kas": "kashmiri", - "kat": "georgia", - "kau": "kanuri", - "kaw": "kavi", - "kaz": "kazakki", - "kbd": "kabardi", - "kha": "Khasi", - "khm": "Khmer; Central", - "kho": "khotani", - "kik": "kikuyu", - "kin": "ruanda", - "kir": "kirgiisi", - "kmb": "kimbundu", - "kok": "Konkani (macrolanguage)", - "kom": "komi", - "kon": "Kongo", - "kor": "korea", - "kos": "kosrae", - "kpe": "kpelle", - "krc": "karatšai-balkaari", - "krl": "karjala", - "kru": "kurukh", - "kua": "kuanjama", - "kum": "kumykki", - "kur": "kurdi", - "kut": "kutenai", - "lad": "ladino", - "lah": "lahnda", - "lam": "lamba", - "lao": "lao", - "lat": "latina", - "lav": "latvia", - "lez": "lezgi", - "lim": "Limburgan", - "lin": "lingala", - "lit": "liettua", - "lol": "mongo", - "loz": "lozi", - "ltz": "Luxemburg", - "lua": "luba (Lulua)", - "lub": "luba (Katanga)", - "lug": "Ganda", - "lui": "luiseño", - "lun": "lunda", - "luo": "luo", - "lus": "lushai", - "mad": "madura", - "mag": "magahi", - "mah": "Marshallese", - "mai": "maithili", - "mak": "Makasar", - "mal": "malayalam", - "man": "mandingo", - "mar": "marathi", - "mas": "masai", - "mdf": "mokša", - "mdr": "mandar", - "men": "Mende (Sierra Leone)", - "mga": "keski-iiri", - "mic": "Mi'kmaq", - "min": "minangkabau", - "mis": "Uncoded languages", - "mkd": "makedonia", - "mlg": "malagassi", - "mlt": "malta", - "mnc": "mantšu", - "mni": "manipuri", - "moh": "mohawk", - "mon": "Mongolian", - "mos": "mossi", - "mri": "maori", - "msa": "Malay (macrolanguage)", - "mul": "monia kieliä", - "mus": "muskogi", - "mwl": "mirandês", - "mwr": "marwari", - "mya": "burma", - "myv": "ersä", - "nap": "napoli", - "nau": "nauru", - "nav": "Navajo", - "nbl": "ndebele; eteländebele", - "nde": "ndebele; pohjoisndebele", - "ndo": "ndonga", - "nds": "German; Low", - "nep": "nepali", - "new": "Bhasa; Nepal", - "nia": "nias", - "niu": "niue", - "nld": "hollanti", - "nno": "norja (uusnorja)", - "nob": "Norwegian Bokmål", - "nog": "nogai", - "non": "muinaisnorja", - "nor": "norja", - "nqo": "N'Ko", - "nso": "Sotho; Northern", - "nwc": "Newari; Old", - "nya": "Nyanja", - "nym": "nyamwezi", - "nyn": "nyankole", - "nyo": "Nyoro", - "nzi": "nzima", - "oci": "Occitan (post 1500)", - "oji": "Ojibwa", - "ori": "oriya", - "orm": "oromo", - "osa": "osage", - "oss": "Ossetian", - "ota": "osmaninturkki", - "pag": "pangasinan", - "pal": "pahlavi", - "pam": "pampanga", - "pan": "Panjabi", - "pap": "papiamentu", - "pau": "palau", - "peo": "Persian; Old (ca. 600-400 B.C.)", - "phn": "foinikia", - "pli": "Pali", - "pol": "puola", - "pon": "pohnpei", - "por": "portugali", - "pro": "muinaisprovensaali", - "pus": "pašto", - "que": "Quechua", - "raj": "rajasthani", - "rap": "rapanui", - "rar": "Maori; Cook Islands", - "roh": "Romansh", - "rom": "romani", - "ron": "romania", - "run": "rundi", - "rup": "Romanian; Macedo-", - "rus": "venäjä", - "sad": "sandawe", - "sag": "Sango", - "sah": "jakuutti", - "sam": "Aramaic; Samaritan", - "san": "sanskrit", - "sas": "Sasak", - "sat": "santali", - "scn": "sisilia", - "sco": "skotti", - "sel": "selkuppi", - "sga": "muinaisiiri", - "shn": "shan", - "sid": "sidamo", - "sin": "Sinhala", - "slk": "slovakki", - "slv": "sloveeni", - "sma": "eteläsaame", - "sme": "pohjoissaame", - "smj": "luulajansaame", - "smn": "inarinsaame", - "smo": "samoa", - "sms": "koltansaami", - "sna": "shona", - "snd": "sindhi", - "snk": "soninke", - "sog": "sogdi", - "som": "somali", - "sot": "eteläsotho", - "spa": "espanja", - "sqi": "albania", - "srd": "sardi", - "srn": "Sranan Tongo", - "srp": "serbia", - "srr": "Serer", - "ssw": "swazi", - "suk": "sukuma", - "sun": "sunda", - "sus": "susu", - "sux": "sumeri", - "swa": "Swahili (macrolanguage)", - "swe": "ruotsi", - "syc": "Syriac; Classical", - "syr": "syyria", - "tah": "tahiti", - "tam": "tamili", - "tat": "tataari", - "tel": "Telugu", - "tem": "temne", - "ter": "tereno", - "tet": "tetum", - "tgk": "tadžikki", - "tgl": "tagalog", - "tha": "thai", - "tig": "tigre", - "tir": "tigrinya", - "tiv": "tiv", - "tkl": "tokelau", - "tlh": "Klingon", - "tli": "tlinglit", - "tmh": "Tamashek", - "tog": "Malawin tonga", - "ton": "Tongan tonga", - "tpi": "tok-pisin", - "tsi": "tsimshian", - "tsn": "tswana", - "tso": "tsonga", - "tuk": "turkmeeni", - "tum": "tumbuka", - "tur": "turkki", - "tvl": "tuvalu", - "twi": "twi", - "tyv": "tuva", - "udm": "udmurtti", - "uga": "ugarit", - "uig": "uiguuri", - "ukr": "ukraina", - "umb": "umbundu", - "und": "määrittämätön", - "urd": "urdu", - "uzb": "uzbekki", - "vai": "vai", - "ven": "venda", - "vie": "vietnam", - "vol": "volapük", - "vot": "vatja", - "wal": "Wolaytta", - "war": "Waray (Philippines)", - "was": "washo", - "wln": "valloni", - "wol": "wolof", - "xal": "Kalmyk", - "xho": "xhosa", - "yao": "mien", - "yap": "yap", - "yid": "jiddiš", - "yor": "yoruba", - "zap": "Zapotec", - "zbl": "Blissymbols", - "zen": "zenaga", - "zha": "Zhuang", - "zho": "kiina", - "zul": "zulu", - "zun": "zuni", - "zxx": "No linguistic content", - "zza": "Zaza" - }, "it": { "aar": "Afar", "abk": "Abkhazian", @@ -4120,846 +3327,6 @@ LANGUAGE_NAMES = { "zxx": "Nessun contenuto linguistico", "zza": "Zaza" }, - "uk": { - "aar": "афар", - "abk": "абхазька", - "ace": "ачеська", - "ach": "ачолі", - "ada": "адангме", - "ady": "адигейська", - "afh": "афрингілі", - "afr": "африкаанс", - "ain": "айнська (Японія)", - "aka": "акан", - "akk": "аккадська", - "ale": "алеутська", - "alt": "алтайська (південна)", - "amh": "амхарська", - "ang": "давньоанглійська (бл. 450-1100)", - "anp": "ангіка", - "ara": "арабська", - "arc": "арамейська (офіційна; 700-300 до нашої ери)", - "arg": "арагонська", - "arn": "арауканська", - "arp": "арапахо", - "arw": "аравакська", - "asm": "ассамська", - "ast": "астурійська", - "ava": "аварська", - "ave": "авестанська", - "awa": "авадхі", - "aym": "аймарська", - "aze": "азербайджанська", - "bak": "башкирська", - "bal": "белуджійська", - "bam": "бамбара", - "ban": "балійська", - "bas": "баса (Камерун)", - "bej": "бежа", - "bel": "білоруська", - "bem": "бемба (Замбія)", - "ben": "бенгальська", - "bho": "бходжпурі", - "bik": "бікольська", - "bin": "біні", - "bis": "біслама", - "bla": "сісіка", - "bod": "тибетська", - "bos": "боснійська", - "bra": "брай", - "bre": "бретонська", - "bua": "бурятська", - "bug": "бугійська", - "bul": "болгарська", - "byn": "білін", - "cad": "каддо", - "car": "карибська (галібі)", - "cat": "каталонська", - "ceb": "себуано", - "ces": "чеська", - "cha": "чаморо", - "chb": "чибча", - "che": "чеченська", - "chg": "чагатайська", - "chk": "чуукська", - "chm": "марійська (Росія)", - "chn": "чинук; жаргон", - "cho": "чоктау", - "chp": "чипев’ян", - "chr": "черокі", - "chu": "давньослов’янська", - "chv": "чуваська", - "chy": "шаєнн", - "cop": "коптська", - "cor": "корнійська", - "cos": "корсиканська", - "cre": "крі", - "crh": "турецька (кримська)", - "csb": "кашубська", - "cym": "валійська", - "dak": "дакота", - "dan": "данська", - "dar": "даргва", - "del": "делаварська", - "den": "слейві (атабаська)", - "deu": "німецька", - "dgr": "догріб", - "din": "дінка", - "div": "мальдивська", - "doi": "догрі (макромова)", - "dsb": "нижньолужицька", - "dua": "дуала", - "dum": "середньовічна голландська (бл. 1050-1350)", - "dyu": "діула", - "dzo": "дзонг-ке", - "efi": "ефік", - "egy": "давньоєгипетська", - "eka": "екаджук", - "ell": "грецька (з 1453)", - "elx": "еламська", - "eng": "англійська", - "enm": "середньоанглійська (1100-1500)", - "epo": "есперанто", - "est": "естонська", - "eus": "баскська", - "ewe": "еве", - "ewo": "евондо", - "fan": "фанг (Екваторіальна Гвінея)", - "fao": "фарерська", - "fas": "перська", - "fat": "фанті", - "fij": "фіджійська", - "fil": "філіппінська", - "fin": "фінська", - "fon": "фон", - "fra": "французька", - "frm": "середньофранцузька (бл. 1400-1600)", - "fro": "давньофранцузька (842-бл. 1400)", - "frr": "фризька (північна)", - "frs": "фризька (східна)", - "fry": "фризька (західна)", - "ful": "фулах", - "fur": "фріульська", - "gaa": "га", - "gay": "гайо", - "gba": "гбая (Центральноафриканська Республіка)", - "gez": "гііз", - "gil": "гільбертська", - "gla": "гаельська (Шотландія)", - "gle": "ірландська", - "glg": "галісійська", - "glv": "манкс", - "gmh": "середньоверхньонімецька (бл. 1050-1500)", - "goh": "давньосередньонімецька (бл. 750-1050)", - "gon": "гонді", - "gor": "горонтало", - "got": "готська", - "grb": "гребо", - "grc": "давньогрецька (до 1453)", - "grn": "гуарані", - "gsw": "німецька (Швейцарія)", - "guj": "гуджараті", - "gwi": "гвічин", - "hai": "хайда", - "hat": "креольська (гаїтянська)", - "hau": "хауса", - "haw": "гавайська", - "heb": "іврит", - "her": "гереро", - "hil": "хілігайнон", - "hin": "хінді", - "hit": "хетська", - "hmn": "хмонг", - "hmo": "хірімоту", - "hrv": "хорватська", - "hsb": "верхньолужицька", - "hun": "угорська", - "hup": "хупа", - "hye": "вірменська", - "iba": "ібанська", - "ibo": "ігбо", - "ido": "ідо", - "iii": "ї (Сичуань)", - "iku": "інуктітут", - "ile": "окциденталь", - "ilo": "ілоко", - "ina": "інтерлінгва (Асоціація міжнародної допоміжної мови)", - "ind": "індонезійська", - "inh": "інгушська", - "ipk": "інупіак", - "isl": "ісландська", - "ita": "італійська", - "jav": "яванська", - "jbo": "ложбан", - "jpn": "японська", - "jpr": "єврейсько-перська", - "jrb": "єврейсько-арабська", - "kaa": "каракалпацька", - "kab": "кабильська", - "kac": "качин", - "kal": "калаалісут", - "kam": "камба (Кенія)", - "kan": "каннада", - "kas": "кашмірська", - "kat": "грузинська", - "kau": "канурі", - "kaw": "каві", - "kaz": "казахська", - "kbd": "кабардінська", - "kha": "кхасі", - "khm": "кхмерська (центральна)", - "kho": "хотаносакська", - "kik": "кікуйю", - "kin": "кіньяруанда", - "kir": "киргизька", - "kmb": "кімбунду", - "kok": "конкані (макромова)", - "kom": "комі", - "kon": "конго", - "kor": "корейська", - "kos": "косрейська", - "kpe": "кпелле", - "krc": "карачаєво-балкарська", - "krl": "карельська", - "kru": "курух", - "kua": "куаньяма", - "kum": "кумикська", - "kur": "курдська", - "kut": "кутенай", - "lad": "ладіно", - "lah": "лахнда", - "lam": "ламба", - "lao": "лаоська", - "lat": "латинська", - "lav": "латиська", - "lez": "лезгінська", - "lim": "лімбурганська", - "lin": "лінгала", - "lit": "литовська", - "lol": "монго", - "loz": "лозі", - "ltz": "люксембурзька", - "lua": "луба-лулуа", - "lub": "луба-катанга", - "lug": "ганда", - "lui": "луйсеньо", - "lun": "лунда", - "luo": "луо (Кенія і Танзанія)", - "lus": "лушай", - "mad": "мадурська", - "mag": "магахі", - "mah": "маршальська", - "mai": "майтхілі", - "mak": "макасарська", - "mal": "малаялам", - "man": "мандінго", - "mar": "мараті", - "mas": "масаї", - "mdf": "мокшанська", - "mdr": "мандарська", - "men": "менде (Сьєрра-Леоне)", - "mga": "середньоірландська (900-1200)", - "mic": "мікмак", - "min": "мінангкабау", - "mis": "мови без коду", - "mkd": "македонська", - "mlg": "малагасійська", - "mlt": "мальтійська", - "mnc": "манчжурська", - "mni": "маніпурська", - "moh": "мохаук", - "mon": "монгольська", - "mos": "мосі", - "mri": "маорійська", - "msa": "малайська (макромова)", - "mul": "мови; що належать до декількох родин", - "mus": "крікська", - "mwl": "мірандська", - "mwr": "марварі", - "mya": "бірманська", - "myv": "ерзянська", - "nap": "неаполітанська", - "nau": "науру", - "nav": "навахо", - "nbl": "південна ндебеле", - "nde": "північна ндебеле", - "ndo": "ндонга", - "nds": "нижньонімецька", - "nep": "непальська", - "new": "бхаса (Непал)", - "nia": "ніасійська", - "niu": "ніуе", - "nld": "голландська", - "nno": "норвезька нюноршк", - "nob": "норвезька букмол", - "nog": "ногайська", - "non": "давньонорвезька", - "nor": "норвезька", - "nqo": "н’ко", - "nso": "сото; північне", - "nwc": "неварі (давня)", - "nya": "ньянджа", - "nym": "ньямвезі", - "nyn": "ньянколе", - "nyo": "ньоро", - "nzi": "нзіма", - "oci": "оксітанська (після 1500)", - "oji": "оджибва", - "ori": "орія", - "orm": "оромо", - "osa": "оседжі", - "oss": "осетинська", - "ota": "оттоманська турецька (1500-1928)", - "pag": "пангасінан", - "pal": "пехлевійська", - "pam": "пампанга", - "pan": "пенджабі", - "pap": "папьяменто", - "pau": "палау", - "peo": "давньоперська (бл. 600-400 до н.е.)", - "phn": "фінікійська", - "pli": "палі", - "pol": "польська", - "pon": "понапе", - "por": "португальська", - "pro": "провансальська (давня; до 1500 року)", - "pus": "пуштунська", - "que": "кечуа", - "raj": "раджастхані", - "rap": "рапануї", - "rar": "маорійська (острови Кука)", - "roh": "ретророманська", - "rom": "ромська", - "ron": "румунська", - "run": "рунді", - "rup": "македоно-румунська", - "rus": "російська", - "sad": "сандаве", - "sag": "санго", - "sah": "якутська", - "sam": "арамейська (самаритянська)", - "san": "санскрит", - "sas": "сасакська", - "sat": "санталі", - "scn": "сицилійська", - "sco": "шотландська", - "sel": "селькупська", - "sga": "давньоірландська (до 900)", - "shn": "шан", - "sid": "сидама", - "sin": "сингалійська", - "slk": "словацька", - "slv": "словенська", - "sma": "саамська (південна)", - "sme": "саамська (північна)", - "smj": "лулесаамська", - "smn": "саамська (інарі)", - "smo": "самоанська", - "sms": "саамська (сколт)", - "sna": "шона", - "snd": "синдхі", - "snk": "сонікійська", - "sog": "согдійська", - "som": "сомалійська", - "sot": "сото; південна", - "spa": "іспанська", - "sqi": "албанська", - "srd": "сардинська", - "srn": "сранан-тонго", - "srp": "сербська", - "srr": "серер", - "ssw": "свазі", - "suk": "сукума", - "sun": "сунданська", - "sus": "сусу", - "sux": "шумерська", - "swa": "суахілі (макромова)", - "swe": "шведська", - "syc": "сирійська (класична)", - "syr": "сирійська", - "tah": "таїтянська", - "tam": "тамільська", - "tat": "татарська", - "tel": "телугу", - "tem": "тімне", - "ter": "терено", - "tet": "тетум", - "tgk": "таджицька", - "tgl": "тагалог", - "tha": "таїландська", - "tig": "тігре", - "tir": "тигринійська", - "tiv": "тиві", - "tkl": "токелау", - "tlh": "клінгонська", - "tli": "тлінгіт", - "tmh": "тамашек", - "tog": "тонга (ньяса)", - "ton": "тонга (острови Тонга)", - "tpi": "ток-пісін", - "tsi": "цимшіан", - "tsn": "тсвана", - "tso": "цонга", - "tuk": "туркменська", - "tum": "тумбука", - "tur": "турецька", - "tvl": "тувалу", - "twi": "тві", - "tyv": "тувінська", - "udm": "удмурдська", - "uga": "угаритська", - "uig": "уйгурська", - "ukr": "українська", - "umb": "умбунду", - "und": "невизначена", - "urd": "урду", - "uzb": "узбецька", - "vai": "вай", - "ven": "венда", - "vie": "в'єтнамська", - "vol": "волапюк", - "vot": "водська", - "wal": "волайтта", - "war": "варай (Філіппіни)", - "was": "вашо", - "wln": "валлонська", - "wol": "волоф", - "xal": "калмицька", - "xho": "хоза", - "yao": "яо", - "yap": "япська", - "yid": "ідиш", - "yor": "йоруба", - "zap": "сапотецька", - "zbl": "бліссимволіка", - "zen": "зеназька", - "zha": "чжуань", - "zho": "китайська", - "zul": "зулуська", - "zun": "зуні", - "zxx": "немає мовних даних", - "zza": "заза" - }, - "de": { - "aar": "Danakil-Sprache", - "abk": "Abchasisch", - "ace": "Aceh-Sprache", - "ach": "Acholi-Sprache", - "ada": "Adangme-Sprache", - "ady": "Adygisch", - "afh": "Afrihili", - "afr": "Afrikaans", - "ain": "Ainu-Sprache (Japan)", - "aka": "Akan-Sprache", - "akk": "Akkadisch", - "ale": "Aleutisch", - "alt": "Altaisch; Süd", - "amh": "Amharisch", - "ang": "Englisch; Alt (ca. 450-1100)", - "anp": "Anga-Sprache", - "ara": "Arabisch", - "arc": "Aramäisch", - "arg": "Aragonesisch", - "arn": "Mapudungun", - "arp": "Arapaho", - "arw": "Arawakisch", - "asm": "Assamesisch", - "ast": "Asturisch", - "ava": "Awarisch", - "ave": "Avestisch", - "awa": "Awadhi", - "aym": "Aymara", - "aze": "Aserbaidschanisch", - "bak": "Baschkirisch", - "bal": "Belutschisch", - "bam": "Bambara", - "ban": "Balinesisch", - "bas": "Basa (Kamerun)", - "bej": "Bedja (Bedauye)", - "bel": "Weißrussisch", - "bem": "Bemba (Sambia)", - "ben": "Bengalisch", - "bho": "Bhojpuri", - "bik": "Bikol", - "bin": "Bini", - "bis": "Bislama", - "bla": "Blackfoot", - "bod": "Tibetisch", - "bos": "Bosnisch", - "bra": "Braj-Bhakha", - "bre": "Bretonisch", - "bua": "Burjatisch", - "bug": "Buginesisch", - "bul": "Bulgarisch", - "byn": "Bilin", - "cad": "Caddo", - "car": "Karibisch; Galíbi", - "cat": "Katalanisch", - "ceb": "Cebuano", - "ces": "Tschechisch", - "cha": "Chamorro", - "chb": "Chibcha", - "che": "Tschetschenisch", - "chg": "Tschagataisch", - "chk": "Trukesisch", - "chm": "Mari (Russland)", - "chn": "Chinook", - "cho": "Choctaw", - "chp": "Chipewyan", - "chr": "Cherokee", - "chu": "Altkirchenslawisch", - "chv": "Tschuwaschisch", - "chy": "Cheyenne", - "cop": "Koptisch", - "cor": "Kornisch", - "cos": "Korsisch", - "cre": "Cree", - "crh": "Türkisch; Krimtatarisch", - "csb": "Kaschubisch", - "cym": "Walisisch", - "dak": "Dakota", - "dan": "Dänisch", - "dar": "Darginisch", - "del": "Delaware", - "den": "Slave (Athapaskisch)", - "deu": "Deutsch", - "dgr": "Dogrib", - "din": "Dinka", - "div": "Dhivehi", - "doi": "Dogri (Makrosprache)", - "dsb": "Sorbisch; Nieder", - "dua": "Duala", - "dum": "Niederländisch; Mittel (ca. 1050-1350)", - "dyu": "Dyula", - "dzo": "Dzongkha", - "efi": "Efik", - "egy": "Ägyptisch (Historisch)", - "eka": "Ekajuk", - "ell": "Neugriechisch (ab 1453)", - "elx": "Elamisch", - "eng": "Englisch", - "enm": "Mittelenglisch", - "epo": "Esperanto", - "est": "Estnisch", - "eus": "Baskisch", - "ewe": "Ewe-Sprache", - "ewo": "Ewondo", - "fan": "Fang (Äquatorial-Guinea)", - "fao": "Färöisch", - "fas": "Persisch", - "fat": "Fanti", - "fij": "Fidschianisch", - "fil": "Filipino", - "fin": "Finnisch", - "fon": "Fon", - "fra": "Französisch", - "frm": "Französisch; Mittel (ca. 1400 - 1600)", - "fro": "Französisch; Alt (842 - ca. 1400)", - "frr": "Friesisch; Nord", - "frs": "Friesisch; Ost", - "fry": "Friesisch; West", - "ful": "Ful", - "fur": "Friaulisch", - "gaa": "Ga", - "gay": "Gayo", - "gba": "Gbaya (Zentralafrikanische Republik)", - "gez": "Altäthiopisch", - "gil": "Gilbertesisch", - "gla": "Gälisch; Schottisch", - "gle": "Irisch", - "glg": "Galicisch", - "glv": "Manx", - "gmh": "Mittelhochdeutsch (ca. 1050-1500)", - "goh": "Althochdeutsch (ca. 750-1050)", - "gon": "Gondi", - "gor": "Gorontalesisch", - "got": "Gotisch", - "grb": "Grebo", - "grc": "Altgriechisch (bis 1453)", - "grn": "Guaraní", - "gsw": "Schweizerdeutsch", - "guj": "Gujarati", - "gwi": "Kutchin", - "hai": "Haida", - "hat": "Kreolisch; Haitisch", - "hau": "Haussa", - "haw": "Hawaiianisch", - "heb": "Hebräisch", - "her": "Herero", - "hil": "Hiligaynon", - "hin": "Hindi", - "hit": "Hethitisch", - "hmn": "Miao-Sprachen", - "hmo": "Hiri-Motu", - "hrv": "Kroatisch", - "hsb": "Obersorbisch", - "hun": "Ungarisch", - "hup": "Hupa", - "hye": "Armenisch", - "iba": "Iban", - "ibo": "Ibo", - "ido": "Ido", - "iii": "Yi; Sichuan", - "iku": "Inuktitut", - "ile": "Interlingue", - "ilo": "Ilokano", - "ina": "Interlingua (Internationale Hilfssprachen-Vereinigung)", - "ind": "Indonesisch", - "inh": "Inguschisch", - "ipk": "Inupiaq", - "isl": "Isländisch", - "ita": "Italienisch", - "jav": "Javanisch", - "jbo": "Lojban", - "jpn": "Japanisch", - "jpr": "Jüdisch-Persisch", - "jrb": "Jüdisch-Arabisch", - "kaa": "Karakalpakisch", - "kab": "Kabylisch", - "kac": "Kachinisch", - "kal": "Kalaallisut (Grönländisch)", - "kam": "Kamba (Kenia)", - "kan": "Kannada", - "kas": "Kaschmirisch", - "kat": "Georgisch", - "kau": "Kanuri", - "kaw": "Kawi; Altjavanisch", - "kaz": "Kasachisch", - "kbd": "Kabardisch", - "kha": "Khasi-Sprache", - "khm": "Khmer; Zentral", - "kho": "Sakisch", - "kik": "Kikuyu", - "kin": "Rwanda", - "kir": "Kirgisisch", - "kmb": "Mbundu; Kimbundu", - "kok": "Konkani (Makrosprache)", - "kom": "Komi", - "kon": "Kongo", - "kor": "Koreanisch", - "kos": "Kosraeanisch", - "kpe": "Kpelle", - "krc": "Karachay-Balkar", - "krl": "Karenisch", - "kru": "Kurukh", - "kua": "Kwanyama", - "kum": "Kumükisch", - "kur": "Kurdisch", - "kut": "Kutenai", - "lad": "Judenspanisch", - "lah": "Lahnda", - "lam": "Banjari; Lamba", - "lao": "Laotisch", - "lat": "Lateinisch", - "lav": "Lettisch", - "lez": "Lesgisch", - "lim": "Limburgisch", - "lin": "Lingala", - "lit": "Litauisch", - "lol": "Mongo", - "loz": "Rotse", - "ltz": "Luxemburgisch", - "lua": "Luba-Lulua", - "lub": "Luba-Katanga", - "lug": "Ganda", - "lui": "Luiseno", - "lun": "Lunda", - "luo": "Luo (Kenia und Tansania)", - "lus": "Lushai", - "mad": "Maduresisch", - "mag": "Khotta", - "mah": "Marshallesisch", - "mai": "Maithili", - "mak": "Makassarisch", - "mal": "Malayalam", - "man": "Mande; Mandigo; Malinke", - "mar": "Marathi", - "mas": "Massai", - "mdf": "Moksha", - "mdr": "Mandaresisch", - "men": "Mende (Sierra Leone)", - "mga": "Mittelirisch (900-1200)", - "mic": "Mikmak", - "min": "Minangkabau", - "mis": "Nichtklassifizierte Sprachen", - "mkd": "Makedonisch", - "mlg": "Madegassisch", - "mlt": "Maltesisch", - "mnc": "Manchu; Mandschurisch", - "mni": "Meithei-Sprache", - "moh": "Mohawk", - "mon": "Mongolisch", - "mos": "Mossi", - "mri": "Maori", - "msa": "Malaiisch (Makrosprache)", - "mul": "Mehrsprachig; Polyglott", - "mus": "Muskogee", - "mwl": "Mirandesisch", - "mwr": "Marwari", - "mya": "Burmesisch", - "myv": "Erzya", - "nap": "Neapolitanisch", - "nau": "Nauruanisch", - "nav": "Navajo", - "nbl": "Ndebele (Süd)", - "nde": "Ndebele (Nord)", - "ndo": "Ndonga", - "nds": "Plattdeutsch", - "nep": "Nepali", - "new": "Bhasa; Nepalesisch", - "nia": "Nias", - "niu": "Niue", - "nld": "Niederländisch", - "nno": "Nynorsk (Norwegen)", - "nob": "Norwegisch-Bokmål", - "nog": "Nogai", - "non": "Altnordisch", - "nor": "Norwegisch", - "nqo": "N'Ko", - "nso": "Sotho; Nord", - "nwc": "Newari; Alt", - "nya": "Nyanja", - "nym": "Nyamwezi", - "nyn": "Nyankole", - "nyo": "Nyoro", - "nzi": "Nzima", - "oci": "Okzitanisch (nach 1500)", - "oji": "Ojibwa", - "ori": "Orija", - "orm": "Oromo", - "osa": "Osage", - "oss": "Ossetisch", - "ota": "Ottomanisch (Osmanisch/Türkisch) (1500-1928)", - "pag": "Pangasinan", - "pal": "Mittelpersisch", - "pam": "Pampanggan", - "pan": "Panjabi", - "pap": "Papiamento", - "pau": "Palau", - "peo": "Persisch; Alt (ca. 600-400 v.Chr.)", - "phn": "Phönikisch", - "pli": "Pali", - "pol": "Polnisch", - "pon": "Ponapeanisch", - "por": "Portugiesisch", - "pro": "Altokzitanisch; Altprovenzalisch (bis 1500)", - "pus": "Paschtu; Afghanisch", - "que": "Ketschua", - "raj": "Rajasthani", - "rap": "Osterinsel-Sprache; Rapanui", - "rar": "Maori; Cook-Inseln", - "roh": "Bündnerromanisch", - "rom": "Romani; Zigeunersprache", - "ron": "Rumänisch", - "run": "Rundi", - "rup": "Rumänisch; Mezedonisch", - "rus": "Russisch", - "sad": "Sandawe", - "sag": "Sango", - "sah": "Jakutisch", - "sam": "Aramäisch; Samaritanisch", - "san": "Sanskrit", - "sas": "Sassak", - "sat": "Santali", - "scn": "Sizilianisch", - "sco": "Schottisch", - "sel": "Selkupisch", - "sga": "Altirisch (bis 900)", - "shn": "Schan", - "sid": "Sidamo", - "sin": "Singhalesisch", - "slk": "Slowakisch", - "slv": "Slowenisch", - "sma": "Sami; Süd", - "sme": "Nordsamisch", - "smj": "Samisch (Lule)", - "smn": "Samisch; Inari", - "smo": "Samoanisch", - "sms": "Samisch; Skolt", - "sna": "Schona", - "snd": "Sindhi", - "snk": "Soninke", - "sog": "Sogdisch", - "som": "Somali", - "sot": "Sotho (Süd)", - "spa": "Spanisch; Kastilianisch", - "sqi": "Albanisch", - "srd": "Sardisch", - "srn": "Sranan Tongo", - "srp": "Serbisch", - "srr": "Serer", - "ssw": "Swazi", - "suk": "Sukuma", - "sun": "Sundanesisch", - "sus": "Susu", - "sux": "Sumerisch", - "swa": "Swahili (Makrosprache)", - "swe": "Schwedisch", - "syc": "Syrisch; Klassisch", - "syr": "Syrisch", - "tah": "Tahitisch", - "tam": "Tamilisch", - "tat": "Tatarisch", - "tel": "Telugu", - "tem": "Temne", - "ter": "Tereno", - "tet": "Tetum", - "tgk": "Tadschikisch", - "tgl": "Tagalog", - "tha": "Thailändisch", - "tig": "Tigre", - "tir": "Tigrinja", - "tiv": "Tiv", - "tkl": "Tokelauanisch", - "tlh": "Klingonisch", - "tli": "Tlingit", - "tmh": "Tamaseq", - "tog": "Tonga (Nyasa)", - "ton": "Tonga (Tonga-Inseln)", - "tpi": "Neumelanesisch; Pidgin", - "tsi": "Tsimshian", - "tsn": "Tswana", - "tso": "Tsonga", - "tuk": "Turkmenisch", - "tum": "Tumbuka", - "tur": "Türkisch", - "tvl": "Elliceanisch", - "twi": "Twi", - "tyv": "Tuwinisch", - "udm": "Udmurt", - "uga": "Ugaritisch", - "uig": "Uigurisch", - "ukr": "Ukrainisch", - "umb": "Mbundu; Umbundu", - "und": "Unbestimmbar", - "urd": "Urdu", - "uzb": "Usbekisch", - "vai": "Vai", - "ven": "Venda", - "vie": "Vietnamesisch", - "vol": "Volapük", - "vot": "Wotisch", - "wal": "Wolaytta", - "war": "Waray (Philippinen)", - "was": "Washo", - "wln": "Wallonisch", - "wol": "Wolof", - "xal": "Kalmükisch", - "xho": "Xhosa", - "yao": "Yao", - "yap": "Yapesisch", - "yid": "Jiddisch", - "yor": "Joruba", - "zap": "Zapotekisch", - "zbl": "Bliss-Symbole", - "zen": "Zenaga", - "zha": "Zhuang", - "zho": "Chinesisch", - "zul": "Zulu", - "zun": "Zuni", - "zxx": "Kein sprachlicher Inhalt", - "zza": "Zaza" - }, "ja": { "aar": "アファル語", "abk": "アブハジア語", @@ -5800,6 +4167,1639 @@ LANGUAGE_NAMES = { "zxx": "No linguistic content", "zza": "Zaza" }, + "nl": { + "aar": "Afar; Hamitisch", + "abk": "Abchazisch", + "ace": "Achinees", + "ach": "Acholi", + "ada": "Adangme", + "ady": "Adyghe", + "afh": "Afrihili", + "afr": "Afrikaans", + "ain": "Ainu (Japan)", + "aka": "Akaans", + "akk": "Akkadiaans", + "ale": "Aleut", + "alt": "Altajs; zuidelijk", + "amh": "Amhaars; Amharisch", + "ang": "Engels; oud (ca. 450-1100)", + "anp": "Angika", + "ara": "Arabisch", + "arc": "Aramees; officieel (700-300 B.C.)", + "arg": "Aragonees", + "arn": "Mapudungun", + "arp": "Arapaho", + "arw": "Arawak", + "asm": "Assamees; Assami", + "ast": "Asturisch", + "ava": "Avaars; Awari", + "ave": "Avestisch", + "awa": "Awadhi", + "aym": "Aymara", + "aze": "Azerbeidzjaans", + "bak": "Basjkiers; Basjkirisch", + "bal": "Balutsji; Baluchi", + "bam": "Bambara", + "ban": "Balinees", + "bas": "Basa (Kameroen)", + "bej": "Beja", + "bel": "Wit-Russisch; Belarussisch", + "bem": "Bemba (Zambia)", + "ben": "Bengaals", + "bho": "Bhojpuri", + "bik": "Bikol", + "bin": "Bini; Edo", + "bis": "Bislama", + "bla": "Siksika", + "bod": "Tibetaans", + "bos": "Bosnisch", + "bra": "Braj", + "bre": "Bretons; Bretoens", + "bua": "Boeriaats", + "bug": "Buginees", + "bul": "Bulgaars", + "byn": "Bilin", + "cad": "Caddo", + "car": "Caribische talen", + "cat": "Catalaans", + "ceb": "Cebuano", + "ces": "Tsjechisch", + "cha": "Chamorro", + "chb": "Tsjibtsja", + "che": "Tsjetsjeens", + "chg": "Chagatai", + "chk": "Chukees", + "chm": "Mari (Rusland)", + "chn": "Chinook-jargon", + "cho": "Choctaw", + "chp": "Chipewyaans", + "chr": "Cherokee", + "chu": "Slavisch; oud (kerk)", + "chv": "Tsjoevasjisch", + "chy": "Cheyenne", + "cop": "Koptisch", + "cor": "Cornisch", + "cos": "Corsicaans", + "cre": "Cree", + "crh": "Turks; Crimean", + "csb": "Kasjoebiaans", + "cym": "Welsh", + "dak": "Dakota", + "dan": "Deens", + "dar": "Dargwa", + "del": "Delaware", + "den": "Slavisch (Athapascaans)", + "deu": "Duits", + "dgr": "Dogrib", + "din": "Dinka", + "div": "Divehi", + "doi": "Dogri", + "dsb": "Sorbisch; lager", + "dua": "Duala", + "dum": "Nederlands; middel (ca. 1050-1350)", + "dyu": "Dyula", + "dzo": "Dzongkha", + "efi": "Efikisch", + "egy": "Egyptisch (antiek)", + "eka": "Ekajuk", + "ell": "Grieks; Modern (1453-)", + "elx": "Elamitisch", + "eng": "Engels", + "enm": "Engels; middel (1100-1500)", + "epo": "Esperanto", + "est": "Estlands", + "eus": "Baskisch", + "ewe": "Ewe", + "ewo": "Ewondo", + "fan": "Fang", + "fao": "Faeröers", + "fas": "Perzisch", + "fat": "Fanti", + "fij": "Fijisch", + "fil": "Filipijns", + "fin": "Fins", + "fon": "Fon", + "fra": "Frans", + "frm": "Frans; middel (ca. 1400-1600)", + "fro": "Frans; oud (842-ca. 1400)", + "frr": "Fries; noordelijk (Duitsland)", + "frs": "Fries; oostelijk (Duitsland)", + "fry": "Fries", + "ful": "Fulah", + "fur": "Friulisch", + "gaa": "Ga", + "gay": "Gayo", + "gba": "Gbaya (Centraal Afrikaanse Republiek)", + "gez": "Ge'ez", + "gil": "Gilbertees", + "gla": "Keltisch; schots", + "gle": "Iers", + "glg": "Galiciaans", + "glv": "Manx", + "gmh": "Duits; middel hoog (ca. 1050-1500)", + "goh": "Duits; oud hoog (ca. 750-1050)", + "gon": "Gondi", + "gor": "Gorontalo", + "got": "Gothisch", + "grb": "Grebo", + "grc": "Grieks; antiek (tot 1453)", + "grn": "Guarani", + "gsw": "Duits; Zwitserland", + "guj": "Gujarati", + "gwi": "Gwichʼin", + "hai": "Haida", + "hat": "Creools; Haïtiaans", + "hau": "Hausa", + "haw": "Hawaiiaans", + "heb": "Hebreeuws", + "her": "Herero", + "hil": "Hiligainoons", + "hin": "Hindi", + "hit": "Hittitisch", + "hmn": "Hmong", + "hmo": "Hiri Motu", + "hrv": "Kroatisch", + "hsb": "Servisch; hoger", + "hun": "Hongaars", + "hup": "Hupa", + "hye": "Armeens", + "iba": "Ibaans", + "ibo": "Igbo", + "ido": "Ido", + "iii": "Yi; Sichuan - Nuosu", + "iku": "Inuktitut", + "ile": "Interlingue", + "ilo": "Iloko", + "ina": "Interlingua (International Auxiliary Language Association)", + "ind": "Indonesisch", + "inh": "Ingoesjetisch", + "ipk": "Inupiak", + "isl": "IJslands", + "ita": "Italiaans", + "jav": "Javaans", + "jbo": "Lojbaans", + "jpn": "Japans", + "jpr": "Joods-Perzisch", + "jrb": "Joods-Arabisch", + "kaa": "Kara-Kalpak", + "kab": "Kabyle", + "kac": "Katsjin", + "kal": "Groenlands", + "kam": "Kamba (Kenya)", + "kan": "Kannada; Kanara; Kanarees", + "kas": "Kashmiri", + "kat": "Georgisch", + "kau": "Kanuri", + "kaw": "Kawi", + "kaz": "Kazachs", + "kbd": "Kabardisch; Tsjerkessisch", + "kha": "Khasi", + "khm": "Khmer, Cambodjaans", + "kho": "Khotanees", + "kik": "Kikuyu", + "kin": "Kinyarwanda", + "kir": "Kirgizisch", + "kmb": "Kimbundu", + "kok": "Konkani", + "kom": "Komi", + "kon": "Kikongo", + "kor": "Koreaans", + "kos": "Kosraeaans", + "kpe": "Kpelle", + "krc": "Karatsjay-Balkar", + "krl": "Karelisch", + "kru": "Kurukh", + "kua": "Kuanyama", + "kum": "Kumyk", + "kur": "Koerdisch", + "kut": "Kutenaïsch", + "lad": "Ladino", + "lah": "Lahnda", + "lam": "Lamba", + "lao": "Laotiaans", + "lat": "Latijn", + "lav": "Lets", + "lez": "Lezghiaans", + "lim": "Limburgs", + "lin": "Lingala", + "lit": "Litouws", + "lol": "Mongo", + "loz": "Lozi", + "ltz": "Luxemburgs", + "lua": "Luba-Lulua", + "lub": "Luba-Katanga", + "lug": "Luganda", + "lui": "Luiseno", + "lun": "Lunda", + "luo": "Luo (Kenia en Tanzania)", + "lus": "Lushai", + "mad": "Madurees", + "mag": "Magahisch", + "mah": "Marshallees", + "mai": "Maithili", + "mak": "Makasar", + "mal": "Malayalam", + "man": "Mandingo", + "mar": "Marathi", + "mas": "Masai", + "mdf": "Moksja", + "mdr": "Mandars", + "men": "Mende", + "mga": "Iers; middel (900-1200)", + "mic": "Mi'kmaq; Micmac", + "min": "Minangkabau", + "mis": "Niet-gecodeerde talen", + "mkd": "Macedonisch", + "mlg": "Malagassisch", + "mlt": "Maltees", + "mnc": "Manchu", + "mni": "Manipuri", + "moh": "Mohawk", + "mon": "Mongools", + "mos": "Mossisch", + "mri": "Maori", + "msa": "Maleis", + "mul": "Meerdere talen", + "mus": "Creek", + "mwl": "Mirandees", + "mwr": "Marwari", + "mya": "Burmees", + "myv": "Erzya", + "nap": "Napolitaans", + "nau": "Nauruaans", + "nav": "Navajo", + "nbl": "Ndebele; zuid", + "nde": "Ndebele; noord", + "ndo": "Ndonga", + "nds": "Duits; Laag", + "nep": "Nepalees", + "new": "Newari; Nepal", + "nia": "Nias", + "niu": "Niueaans", + "nld": "Nederlands", + "nno": "Noors; Nynorsk", + "nob": "Noors; Bokmål", + "nog": "Nogai", + "non": "Noors; oud", + "nor": "Noors", + "nqo": "N'Ko", + "nso": "Pedi; Sepedi; Noord-Sothotisch", + "nwc": "Newari; Klassiek Nepal", + "nya": "Nyanja", + "nym": "Nyamwezi", + "nyn": "Nyankools", + "nyo": "Nyoro", + "nzi": "Nzima", + "oci": "Occitaans (na 1500)", + "oji": "Ojibwa", + "ori": "Oriya", + "orm": "Oromo", + "osa": "Osaags", + "oss": "Ossetisch", + "ota": "Turks; ottomaans (1500-1928)", + "pag": "Pangasinaans", + "pal": "Pehlevi", + "pam": "Pampanga", + "pan": "Punjabi", + "pap": "Papiamento", + "pau": "Palauaans", + "peo": "Perzisch; oud (ca. 600-400 B.C.)", + "phn": "Foenisisch", + "pli": "Pali", + "pol": "Pools", + "pon": "Pohnpeiaans", + "por": "Portugees", + "pro": "Provençaals; oud (tot 1500)", + "pus": "Poesjto", + "que": "Quechua", + "raj": "Rajasthani", + "rap": "Rapanui", + "rar": "Rarotongan; Cookeilanden Maori", + "roh": "Reto-Romaans", + "rom": "Romani", + "ron": "Roemeens", + "run": "Rundi", + "rup": "Roemeens; Macedo-", + "rus": "Russisch", + "sad": "Sandawe", + "sag": "Sangho", + "sah": "Jakoets", + "sam": "Aramees; Samaritaans", + "san": "Sanskriet", + "sas": "Sasaaks", + "sat": "Santali", + "scn": "Siciliaans", + "sco": "Schots", + "sel": "Sulkoeps", + "sga": "Iers; oud (tot 900)", + "shn": "Sjaans", + "sid": "Sidamo", + "sin": "Sinhala", + "slk": "Slowaaks", + "slv": "Sloveens", + "sma": "Samisch; zuid, Laps; zuid", + "sme": "Samisch; noord, Laps; noord", + "smj": "Lule Sami", + "smn": "Sami; Inari, Laps; Inari", + "smo": "Samoaans", + "sms": "Sami; Skolt, Laps; Skolt", + "sna": "Shona", + "snd": "Sindhi", + "snk": "Soninke", + "sog": "Sogdiaans", + "som": "Somalisch", + "sot": "Sothaans; zuidelijk", + "spa": "Spaans", + "sqi": "Albanees", + "srd": "Sardinisch", + "srn": "Sranan Tongo", + "srp": "Servisch", + "srr": "Serer", + "ssw": "Swati", + "suk": "Sukuma", + "sun": "Soendanees; Sundanees", + "sus": "Susu", + "sux": "Sumerisch", + "swa": "Swahili", + "swe": "Zweeds", + "syc": "Syriac; Klassiek", + "syr": "Syrisch", + "tah": "Tahitisch", + "tam": "Tamil", + "tat": "Tataars", + "tel": "Telugu", + "tem": "Timne", + "ter": "Tereno", + "tet": "Tetum", + "tgk": "Tadzjieks", + "tgl": "Tagalog", + "tha": "Thai", + "tig": "Tigre", + "tir": "Tigrinya", + "tiv": "Tiv", + "tkl": "Tokelau", + "tlh": "Klingon; tlhIngan-Hol", + "tli": "Tlingit", + "tmh": "Tamasjek", + "tog": "Tonga (Nyasa)", + "ton": "Tonga (Tonga-eilanden)", + "tpi": "Tok Pisin", + "tsi": "Tsimsjiaans", + "tsn": "Tswana", + "tso": "Tsonga", + "tuk": "Turkmeens", + "tum": "Tumbuka", + "tur": "Turks", + "tvl": "Tuvalu", + "twi": "Twi", + "tyv": "Tuviniaans", + "udm": "Udmurts", + "uga": "Ugaritisch", + "uig": "Oeigoers; Oejgoers", + "ukr": "Oekraïens", + "umb": "Umbundu", + "und": "Onbepaald", + "urd": "Urdu", + "uzb": "Oezbeeks", + "vai": "Vai", + "ven": "Venda", + "vie": "Vietnamees", + "vol": "Volapük", + "vot": "Votisch", + "wal": "Walamo", + "war": "Waray (Filipijns)", + "was": "Wasjo", + "wln": "Waals", + "wol": "Wolof", + "xal": "Kalmyk", + "xho": "Xhosa", + "yao": "Yao", + "yap": "Yapees", + "yid": "Jiddisch", + "yor": "Yoruba", + "zap": "Zapotec", + "zbl": "Blissymbolen", + "zen": "Zenaga", + "zha": "Zhuang, Tsjoeang", + "zho": "Chinees", + "zul": "Zoeloe", + "zun": "Zuni", + "zxx": "Geen linguïstische inhoud", + "zza": "Zaza" + }, + "pl": { + "aar": "afarski", + "abk": "abchaski", + "ace": "aczineski", + "ach": "aczoli", + "ada": "adangme", + "ady": "adygejski", + "afh": "afrihili", + "afr": "afrykanerski", + "ain": "ajnoski (Japonia)", + "aka": "akan", + "akk": "akadyjski", + "ale": "aleucki", + "alt": "ałtajski południowy", + "amh": "amharski", + "ang": "Staroangielski (ok. 450-1100)", + "anp": "angika", + "ara": "arabski", + "arc": "aramejski oficjalny (700-300 p.n.e.)", + "arg": "aragoński", + "arn": "araukański", + "arp": "arapaho", + "arw": "arawak", + "asm": "asamski", + "ast": "asturyjski", + "ava": "awarski", + "ave": "awestyjski", + "awa": "awadhi", + "aym": "ajmara", + "aze": "azerski", + "bak": "baszkirski", + "bal": "baluczi", + "bam": "bambara", + "ban": "balijski", + "bas": "basa (Kamerun)", + "bej": "bedża", + "bel": "białoruski", + "bem": "bemba (Zambia)", + "ben": "bengalski", + "bho": "bhodźpuri", + "bik": "bikol", + "bin": "edo", + "bis": "bislama", + "bla": "siksika", + "bod": "tybetański", + "bos": "bośniacki", + "bra": "bradź", + "bre": "bretoński", + "bua": "buriacki", + "bug": "bugijski", + "bul": "bułgarski", + "byn": "blin", + "cad": "kaddo", + "car": "karaibski galibi", + "cat": "kataloński", + "ceb": "cebuański", + "ces": "czeski", + "cha": "czamorro", + "chb": "czibcza", + "che": "czeczeński", + "chg": "czagatajski", + "chk": "chuuk", + "chm": "maryjski (Rosja)", + "chn": "żargon chinoocki", + "cho": "czoktaw", + "chp": "chipewyan", + "chr": "czerokeski", + "chu": "starosłowiański", + "chv": "czuwaski", + "chy": "czejeński", + "cop": "koptyjski", + "cor": "kornijski", + "cos": "korsykański", + "cre": "kri", + "crh": "krymskotatarski", + "csb": "kaszubski", + "cym": "walijski", + "dak": "dakota", + "dan": "duński", + "dar": "dargwijski", + "del": "delaware", + "den": "slavey (atapaskański)", + "deu": "niemiecki", + "dgr": "dogrib", + "din": "dinka", + "div": "malediwski; divehi", + "doi": "dogri (makrojęzyk)", + "dsb": "dolnołużycki", + "dua": "duala", + "dum": "holenderski średniowieczny (ok. 1050-1350)", + "dyu": "diula", + "dzo": "dzongka", + "efi": "efik", + "egy": "egipski (starożytny)", + "eka": "ekajuk", + "ell": "grecki współczesny (1453-)", + "elx": "elamicki", + "eng": "Angielski", + "enm": "angielski średniowieczny (1100-1500)", + "epo": "esperanto", + "est": "estoński", + "eus": "baskijski", + "ewe": "ewe", + "ewo": "ewondo", + "fan": "fang (Gwinea Równikowa)", + "fao": "farerski", + "fas": "perski", + "fat": "fanti", + "fij": "fidżyjski", + "fil": "pilipino", + "fin": "fiński", + "fon": "fon", + "fra": "francuski", + "frm": "francuski średniowieczny (ok. 1400-1600)", + "fro": "starofrancuski (842-ok. 1400)", + "frr": "północnofryzyjski", + "frs": "wschodniofryzyjski", + "fry": "zachodniofryzyjski", + "ful": "fulani", + "fur": "friulski", + "gaa": "ga", + "gay": "gayo", + "gba": "gbaya (Republika Środkowoafrykańska)", + "gez": "gyyz", + "gil": "gilbertański", + "gla": "szkocki gaelicki", + "gle": "irlandzki", + "glg": "galicyjski", + "glv": "manx", + "gmh": "średnio-wysoko-niemiecki (ok. 1050-1500)", + "goh": "staro-wysoko-niemiecki (ok. 750-1050)", + "gon": "gondi", + "gor": "gorontalo", + "got": "gocki", + "grb": "grebo", + "grc": "grecki starożytny (do 1453)", + "grn": "guarani", + "gsw": "niemiecki szwajcarski", + "guj": "gudźarati", + "gwi": "gwichʼin", + "hai": "haida", + "hat": "kreolski haitański", + "hau": "hausa", + "haw": "hawajski", + "heb": "hebrajski", + "her": "herero", + "hil": "hiligajnon", + "hin": "hindi", + "hit": "hetycki", + "hmn": "hmong", + "hmo": "hiri motu", + "hrv": "chorwacki", + "hsb": "górnołużycki", + "hun": "węgierski", + "hup": "hupa", + "hye": "ormiański", + "iba": "ibanag", + "ibo": "ibo", + "ido": "ido", + "iii": "syczuański", + "iku": "inuktitut", + "ile": "interlingue", + "ilo": "ilokano", + "ina": "interlingua (Międzynarodowe Stowarzyszenie Języka Pomocniczego)", + "ind": "indonezyjski", + "inh": "inguski", + "ipk": "inupiaq", + "isl": "islandzki", + "ita": "włoski", + "jav": "jawajski", + "jbo": "lojban", + "jpn": "japoński", + "jpr": "judeo-perski", + "jrb": "judeoarabski", + "kaa": "karakałpacki", + "kab": "kabylski", + "kac": "kaczin", + "kal": "kalaallisut", + "kam": "kamba (Kenia)", + "kan": "kannada", + "kas": "kaszmirski", + "kat": "gruziński", + "kau": "kanuri", + "kaw": "kawi", + "kaz": "kazaski", + "kbd": "kabardyjski", + "kha": "khasi", + "khm": "środkowokhmerski", + "kho": "chotański", + "kik": "kikiju", + "kin": "ruanda", + "kir": "kirgiski", + "kmb": "kimbundu", + "kok": "konkani (makrojęzyk)", + "kom": "komi", + "kon": "kongo", + "kor": "koreański", + "kos": "kosrae", + "kpe": "kpelle", + "krc": "karaczajsko-bałkarski", + "krl": "karelski", + "kru": "kurukh", + "kua": "kwanyama", + "kum": "kumycki", + "kur": "kurdyjski", + "kut": "kutenai", + "lad": "ladino", + "lah": "lahnda", + "lam": "lamba", + "lao": "laotański", + "lat": "łaciński", + "lav": "łotewski", + "lez": "lezgiński", + "lim": "limburgijski", + "lin": "lingala", + "lit": "litewski", + "lol": "mongo", + "loz": "lozi", + "ltz": "luksemburski", + "lua": "luba-lulua", + "lub": "luba-katanga", + "lug": "luganda", + "lui": "luiseno", + "lun": "lunda", + "luo": "luo (Kenia i Tanzania)", + "lus": "lushai", + "mad": "madurajski", + "mag": "magahi", + "mah": "marshalski", + "mai": "maithili", + "mak": "makasar", + "mal": "malajalam", + "man": "mandingo", + "mar": "marathi", + "mas": "masajski", + "mdf": "moksza", + "mdr": "mandar", + "men": "mende (Sierra Leone)", + "mga": "irlandzki średniowieczny (900-1200)", + "mic": "micmac", + "min": "minangkabau", + "mis": "języki niezakodowane", + "mkd": "macedoński", + "mlg": "malgaski", + "mlt": "maltański", + "mnc": "mandżurski", + "mni": "manipuri", + "moh": "mohawk", + "mon": "mongolski", + "mos": "mossi", + "mri": "maoryski", + "msa": "malajski (makrojęzyk)", + "mul": "wiele języków", + "mus": "krik", + "mwl": "mirandyjski", + "mwr": "marwari", + "mya": "birmański", + "myv": "erzja", + "nap": "neapolitański", + "nau": "nauruański", + "nav": "navaho", + "nbl": "ndebele południowy", + "nde": "ndebele północny", + "ndo": "ndonga", + "nds": "German; Low", + "nep": "nepalski", + "new": "newarski", + "nia": "nias", + "niu": "niue", + "nld": "holenderski", + "nno": "norweski Nynorsk", + "nob": "norweski Bokmål", + "nog": "nogajski", + "non": "staronordyjski", + "nor": "norweski", + "nqo": "n’ko", + "nso": "sotho północny", + "nwc": "newarski klasyczny", + "nya": "njandża", + "nym": "nyamwezi", + "nyn": "nyankole", + "nyo": "nyoro", + "nzi": "nzema", + "oci": "okcytański (po 1500)", + "oji": "odżibwe", + "ori": "orija", + "orm": "oromo", + "osa": "osage", + "oss": "osetyjski", + "ota": "turecki otomański (1500-1928)", + "pag": "pangasino", + "pal": "pahlawi", + "pam": "pampango", + "pan": "pendżabski", + "pap": "papiamento", + "pau": "palau", + "peo": "staroperski (ok. 600-400 p.n.e)", + "phn": "fenicki", + "pli": "pali", + "pol": "Polski", + "pon": "pohnpei", + "por": "portugalski", + "pro": "prowansalski średniowieczny (do 1500)", + "pus": "paszto", + "que": "keczua", + "raj": "radźasthani", + "rap": "rapanui", + "rar": "maoryski Wysp Cooka", + "roh": "retoromański", + "rom": "romski", + "ron": "rumuński", + "run": "rundi", + "rup": "arumuński", + "rus": "rosyjski", + "sad": "sandawe", + "sag": "sango", + "sah": "jakucki", + "sam": "samarytański aramejski", + "san": "sanskryt", + "sas": "sasak", + "sat": "santali", + "scn": "sycylijski", + "sco": "scots", + "sel": "selkupski", + "sga": "staroirlandzki (do 900)", + "shn": "szan", + "sid": "sidamo", + "sin": "syngaleski", + "slk": "słowacki", + "slv": "słoweński", + "sma": "południowolapoński", + "sme": "północnolapoński", + "smj": "lapoński lule", + "smn": "lapoński inari", + "smo": "samoański", + "sms": "lapoński skolt", + "sna": "shona", + "snd": "sindhi", + "snk": "soninke", + "sog": "sogdiański", + "som": "somalijski", + "sot": "sotho południowy", + "spa": "hiszpański", + "sqi": "albański", + "srd": "sardyński", + "srn": "sranan tongo", + "srp": "serbski", + "srr": "serer", + "ssw": "suazi", + "suk": "sukuma", + "sun": "sundajski", + "sus": "susu", + "sux": "sumeryjski", + "swa": "suahili (makrojęzyk)", + "swe": "szwedzki", + "syc": "syryjski klasyczny", + "syr": "syryjski", + "tah": "tahitański", + "tam": "tamilski", + "tat": "tatarski", + "tel": "telugu", + "tem": "temne", + "ter": "tereno", + "tet": "tetum", + "tgk": "tadżycki", + "tgl": "tagalski", + "tha": "tajski", + "tig": "tigre", + "tir": "tigrinia", + "tiv": "tiw", + "tkl": "tokelau", + "tlh": "klingoński", + "tli": "tlingit", + "tmh": "tuareski", + "tog": "tongański (Nyasa)", + "ton": "tongański (Wyspy Tonga)", + "tpi": "tok pisin", + "tsi": "tsimszian", + "tsn": "tswana", + "tso": "tsonga", + "tuk": "turkmeński", + "tum": "tumbuka", + "tur": "turecki", + "tvl": "tuvalu", + "twi": "twi", + "tyv": "tuwiński", + "udm": "udmurcki", + "uga": "ugarycki", + "uig": "ujgurski", + "ukr": "ukraiński", + "umb": "umbundu", + "und": "nieokreślony", + "urd": "urdu", + "uzb": "uzbecki", + "vai": "wai", + "ven": "venda", + "vie": "wietnamski", + "vol": "wolapik", + "vot": "wotycki", + "wal": "walamo", + "war": "warajski (Filipiny)", + "was": "washo", + "wln": "waloński", + "wol": "wolof", + "xal": "kałmucki", + "xho": "xhosa", + "yao": "yao", + "yap": "japski", + "yid": "jidysz", + "yor": "joruba", + "zap": "zapotecki", + "zbl": "bliss", + "zen": "zenaga", + "zha": "zhuang", + "zho": "chiński", + "zul": "zuluski", + "zun": "zuni", + "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": "Абхазский", + "ace": "Ачехский", + "ach": "Ачоли", + "ada": "Адангме", + "ady": "Адыгейский", + "afh": "Африхили", + "afr": "Африкаанс", + "ain": "Ainu (Japan)", + "aka": "Акан", + "akk": "Аккадский", + "ale": "Алеутский", + "alt": "Altai; Southern", + "amh": "Амхарский (Амаринья)", + "ang": "English; Old (ca. 450-1100)", + "anp": "Анжика", + "ara": "Арабский", + "arc": "Арамейский; Официальный", + "arg": "Арагонский", + "arn": "Mapudungun", + "arp": "Арапахо", + "arw": "Аравакский", + "asm": "Ассамский", + "ast": "Астурийский", + "ava": "Аварский", + "ave": "Авестийский", + "awa": "Авадхи", + "aym": "Аймара", + "aze": "Азербайджанский", + "bak": "Башкирский", + "bal": "Baluchi", + "bam": "Бамбара", + "ban": "Балийский", + "bas": "Баса (Камерун)", + "bej": "Беджа", + "bel": "Белорусский", + "bem": "Бемба (Замбия)", + "ben": "Бенгальский", + "bho": "Бходжпури", + "bik": "Бикольский", + "bin": "Бини", + "bis": "Бислама", + "bla": "Сиксика", + "bod": "Тибетский", + "bos": "Боснийский", + "bra": "Браун", + "bre": "Бретонский", + "bua": "Бурятский", + "bug": "Бугийский", + "bul": "Болгарский", + "byn": "Bilin", + "cad": "Каддо", + "car": "Carib; Galibi", + "cat": "Каталанский", + "ceb": "Себуано", + "ces": "Чешский", + "cha": "Чаморро", + "chb": "Чибча", + "che": "Чеченский", + "chg": "Чагатайский", + "chk": "Трукский", + "chm": "Марийский (Россия)", + "chn": "Чинук жаргон", + "cho": "Чоктав", + "chp": "Чипевианский", + "chr": "Чероки", + "chu": "Slavonic; Old", + "chv": "Чувашский", + "chy": "Чейенн", + "cop": "Коптский", + "cor": "Корнский", + "cos": "Корсиканский", + "cre": "Кри", + "crh": "Turkish; Crimean", + "csb": "Кашубианский", + "cym": "Уэльский (Валлийский)", + "dak": "Дакота", + "dan": "Датский", + "dar": "Даргва", + "del": "Делаварский", + "den": "Атапачские языки", + "deu": "Немецкий", + "dgr": "Догриб", + "din": "Динка", + "div": "Dhivehi", + "doi": "Dogri (macrolanguage)", + "dsb": "Sorbian; Lower", + "dua": "Дуала", + "dum": "Dutch; Middle (ca. 1050-1350)", + "dyu": "Диула (Дьюла)", + "dzo": "Дзонг-кэ", + "efi": "Эфик", + "egy": "Древнеегипетский", + "eka": "Экаджук", + "ell": "Новогреческий (с 1453)", + "elx": "Эламский", + "eng": "Английский", + "enm": "Среднеанглийский (1100-1500)", + "epo": "Эсперанто", + "est": "Эстонский", + "eus": "Баскский", + "ewe": "Эве", + "ewo": "Эвондо", + "fan": "Fang (Equatorial Guinea)", + "fao": "Фарерский", + "fas": "Персидский", + "fat": "Фанти", + "fij": "Фиджийский", + "fil": "Filipino", + "fin": "Финский", + "fon": "Фон", + "fra": "Французский", + "frm": "French; Middle (ca. 1400-1600)", + "fro": "French; Old (842-ca. 1400)", + "frr": "Frisian; Northern", + "frs": "Frisian; Eastern", + "fry": "Frisian; Western", + "ful": "Фулах", + "fur": "Фриулианский", + "gaa": "Га", + "gay": "Гайо", + "gba": "Gbaya (Central African Republic)", + "gez": "Геэз", + "gil": "Гильбертский", + "gla": "Gaelic; Scottish", + "gle": "Ирландский", + "glg": "Galician", + "glv": "Мэнкский", + "gmh": "German; Middle High (ca. 1050-1500)", + "goh": "German; Old High (ca. 750-1050)", + "gon": "Гонди", + "gor": "Горонтало", + "got": "Готский", + "grb": "Гребо", + "grc": "Древнегреческий (по 1453)", + "grn": "Гуарани", + "gsw": "German; Swiss", + "guj": "Гуджарати", + "gwi": "Gwichʼin", + "hai": "Хайда", + "hat": "Creole; Haitian", + "hau": "Хауса", + "haw": "Гавайский", + "heb": "Иврит", + "her": "Гереро", + "hil": "Хилигайнон", + "hin": "Хинди", + "hit": "Хиттит", + "hmn": "Хмонг", + "hmo": "Хири Моту", + "hrv": "Хорватский", + "hsb": "Sorbian; Upper", + "hun": "Венгерский", + "hup": "Хупа", + "hye": "Армянский", + "iba": "Ибанский", + "ibo": "Игбо", + "ido": "Идо", + "iii": "Yi; Sichuan", + "iku": "Инуктитут", + "ile": "Интерлингве", + "ilo": "Илоко", + "ina": "Интерлингва (Ассоциация международного вспомогательного языка)", + "ind": "Индонезийский", + "inh": "Ингушский", + "ipk": "Инулиак", + "isl": "Исландский", + "ita": "Итальянский", + "jav": "Яванский", + "jbo": "Лоджбан", + "jpn": "Японский", + "jpr": "Еврейско-персидский", + "jrb": "Еврейско-арабский", + "kaa": "Каракалпакский", + "kab": "Кабильский", + "kac": "Качинский", + "kal": "Kalaallisut", + "kam": "Kamba (Kenya)", + "kan": "Каннада", + "kas": "Кашмири", + "kat": "Грузинский", + "kau": "Канури", + "kaw": "Кави", + "kaz": "Казахский", + "kbd": "Кабардинский", + "kha": "Кхаси", + "khm": "Khmer; Central", + "kho": "Хотанский", + "kik": "Кикуйю", + "kin": "Киньяруанда", + "kir": "Киргизский", + "kmb": "Кимбунду", + "kok": "Konkani (macrolanguage)", + "kom": "Коми", + "kon": "Конго", + "kor": "Корейский", + "kos": "Косраинский", + "kpe": "Кпелле", + "krc": "Карачаево-балкарский", + "krl": "Карельский", + "kru": "Курух", + "kua": "Киньяма", + "kum": "Кумыкский", + "kur": "Курдский", + "kut": "Кутенаи", + "lad": "Ладино", + "lah": "Лахнда", + "lam": "Ламба", + "lao": "Лаосский", + "lat": "Латинский", + "lav": "Латвийский", + "lez": "Лезгинский", + "lim": "Limburgan", + "lin": "Лингала", + "lit": "Литовский", + "lol": "Монго", + "loz": "Лози", + "ltz": "Luxembourgish", + "lua": "Луба-Лулуа", + "lub": "Луба-Катанга", + "lug": "Ганда", + "lui": "Луисеньо", + "lun": "Лунда", + "luo": "Луо (Кения и Танзания)", + "lus": "Лушай", + "mad": "Мадурский", + "mag": "Магахи", + "mah": "Marshallese", + "mai": "Майтхили", + "mak": "Макассарский", + "mal": "Малаялам", + "man": "Мандинго", + "mar": "Маратхи", + "mas": "Масаи", + "mdf": "Мокшанский", + "mdr": "Мандарский", + "men": "Mende (Sierra Leone)", + "mga": "Среднеирландский (900-1200)", + "mic": "Mi'kmaq", + "min": "Минангкабау", + "mis": "Uncoded languages", + "mkd": "Македонский", + "mlg": "Малагаси", + "mlt": "Мальтийский", + "mnc": "Манчу", + "mni": "Манипури", + "moh": "Мохаук", + "mon": "Монгольский", + "mos": "Моей", + "mri": "Маори", + "msa": "Malay (macrolanguage)", + "mul": "Разных семей языки", + "mus": "Крик", + "mwl": "Мирандские", + "mwr": "Марвари", + "mya": "Бирманский", + "myv": "Эрзянский", + "nap": "Неаполитанский", + "nau": "Науру", + "nav": "Navajo", + "nbl": "Ндебеле южный", + "nde": "Ндебеле северный", + "ndo": "Ндунга", + "nds": "German; Low", + "nep": "Непальский", + "new": "Bhasa; Nepal", + "nia": "Ниас", + "niu": "Ниуэ", + "nld": "Нидерландский", + "nno": "Норвежский Нюнорск", + "nob": "Norwegian Bokmål", + "nog": "Ногайский", + "non": "Старонорвежский", + "nor": "Норвежский", + "nqo": "Н'ко", + "nso": "Sotho; Northern", + "nwc": "Newari; Old", + "nya": "Nyanja", + "nym": "Ньямвези", + "nyn": "Ньянколе", + "nyo": "Ньоро", + "nzi": "Нзима", + "oci": "Occitan (post 1500)", + "oji": "Оджибва", + "ori": "Ория", + "orm": "Оромо", + "osa": "Оседжи", + "oss": "Ossetian", + "ota": "Турецкий; Отомангский (1500-1928)", + "pag": "Пангасинан", + "pal": "Пехлевийский", + "pam": "Пампанга", + "pan": "Panjabi", + "pap": "Папьяменто", + "pau": "Палау", + "peo": "Persian; Old (ca. 600-400 B.C.)", + "phn": "Финикийский", + "pli": "Пали", + "pol": "Польский", + "pon": "Фонпейский", + "por": "Португальский", + "pro": "Старопровансальский (по 1500)", + "pus": "Пушту", + "que": "Кечуа", + "raj": "Раджастхани", + "rap": "Рапаню", + "rar": "Maori; Cook Islands", + "roh": "Romansh", + "rom": "Цыганский", + "ron": "Румынский", + "run": "Рунди", + "rup": "Romanian; Macedo-", + "rus": "Русский", + "sad": "Сандаве", + "sag": "Санго", + "sah": "Якутский", + "sam": "Aramaic; Samaritan", + "san": "Санскрит", + "sas": "Сасакский", + "sat": "Сантали", + "scn": "Сицилийский", + "sco": "Шотландский", + "sel": "Селкапский", + "sga": "Староирландский (по 900)", + "shn": "Шанский", + "sid": "Сидама", + "sin": "Сингальский", + "slk": "Словацкий", + "slv": "Словенский", + "sma": "Sami; Southern", + "sme": "Sami; Northern", + "smj": "Люле-саамский", + "smn": "Sami; Inari", + "smo": "Самоанский", + "sms": "Sami; Skolt", + "sna": "Шона", + "snd": "Синдхи", + "snk": "Сонинк", + "sog": "Согдийский", + "som": "Сомали", + "sot": "Сото Южный", + "spa": "Испанский", + "sqi": "Албанский", + "srd": "Сардинский", + "srn": "Sranan Tongo", + "srp": "Сербский", + "srr": "Серер", + "ssw": "Свати", + "suk": "Сукума", + "sun": "Сунданский", + "sus": "Сусу", + "sux": "Шумерский", + "swa": "Swahili (macrolanguage)", + "swe": "Шведский", + "syc": "Syriac; Classical", + "syr": "Сирийский", + "tah": "Таитянский", + "tam": "Тамильский", + "tat": "Татарский", + "tel": "Телугу", + "tem": "Темне", + "ter": "Терено", + "tet": "Тетумский", + "tgk": "Таджикский", + "tgl": "Тагалог", + "tha": "Таи", + "tig": "Тигре", + "tir": "Тигринья", + "tiv": "Тив", + "tkl": "Токелау", + "tlh": "Klingon", + "tli": "Тлингит", + "tmh": "Тамашек", + "tog": "Тонга (Ньяса)", + "ton": "Тонга (острова Тонга)", + "tpi": "Ток Писин", + "tsi": "Цимшиан", + "tsn": "Тсвана", + "tso": "Тсонга", + "tuk": "Туркменский", + "tum": "Тумбука", + "tur": "Турецкий", + "tvl": "Тувалу", + "twi": "Тви", + "tyv": "Тувинский", + "udm": "Удмуртский", + "uga": "Угаритский", + "uig": "Уйгурский", + "ukr": "Украинский", + "umb": "Умбунду", + "und": "Неидентифицированный", + "urd": "Урду", + "uzb": "Узбекский", + "vai": "Ваи", + "ven": "Венда", + "vie": "Вьетнамский", + "vol": "Волапюк", + "vot": "Вотик", + "wal": "Wolaytta", + "war": "Waray (Philippines)", + "was": "Вашо", + "wln": "Валлун", + "wol": "Волоф", + "xal": "Kalmyk", + "xho": "Коса", + "yao": "Яо", + "yap": "Яапийский", + "yid": "Идиш", + "yor": "Йоруба", + "zap": "Сапотекский", + "zbl": "Blissymbols", + "zen": "Зенагский", + "zha": "Чжуанский", + "zho": "Китайский", + "zul": "Зулусский", + "zun": "Зуньи", + "zxx": "Нет языкового содержимого", + "zza": "Зазаки" + }, "sv": { "aar": "Afar", "abk": "Abchaziska", @@ -6220,845 +6220,1218 @@ LANGUAGE_NAMES = { "zxx": "No linguistic content", "zza": "Zaza" }, - "cs": { - "aar": "afarština", - "abk": "abchazajština", - "ace": "atěžština", - "ach": "ačoli (luoština)", - "ada": "adangmeština", - "ady": "adyghe", - "afh": "afrihilijština", - "afr": "afrikánština", - "ain": "ainu (Japonsko)", - "aka": "akanština", - "akk": "akkadština", - "ale": "aleutština", - "alt": "altajština; jižní", - "amh": "Amharština", - "ang": "Angličtina; stará (asi 450-1100)", - "anp": "angika", - "ara": "arabština", - "arc": "aramejština; oficiální (700-300 př. n. l.)", - "arg": "aragonská španělština", - "arn": "mapudungun", - "arp": "arapaho", - "arw": "arawacké jazyky", - "asm": "ásámština", - "ast": "asturština", - "ava": "avarština", - "ave": "avestština", - "awa": "avadhština (avadhí)", - "aym": "aymarština", - "aze": "azerbajdžánština", - "bak": "Baskirština", - "bal": "balúčština", - "bam": "bambarština", - "ban": "balijština", - "bas": "basa (Kamerun)", - "bej": "bedža", - "bel": "běloruština", - "bem": "bemba (Zambie)", - "ben": "bengálština", - "bho": "bhódžpurština", - "bik": "bikolština", - "bin": "bini", - "bis": "bislamština", - "bla": "siksika", - "bod": "tibetština", - "bos": "bosenština", - "bra": "bradžština", - "bre": "bretonština", - "bua": "burjatština", - "bug": "bugiština", - "bul": "bulharština", - "byn": "bilin", - "cad": "caddo", - "car": "carib; Galibi", - "cat": "katalánština", - "ceb": "cebuánština", - "ces": "Čeština", - "cha": "čamoro", - "chb": "čibča", - "che": "čečenština", - "chg": "Chagatai", - "chk": "čukčtina", - "chm": "Mari (Russia)", - "chn": "činuk pidžin", - "cho": "choctawština", - "chp": "čipeva", - "chr": "čerokézština", - "chu": "Slavonic; Old", - "chv": "čuvaština", - "chy": "čejenština", - "cop": "koptština", - "cor": "kornština", - "cos": "korsičtina", - "cre": "krí", - "crh": "Turkish; Crimean", - "csb": "kašubština", - "cym": "velština", - "dak": "dakotština", - "dan": "dánština", - "dar": "dargwa", - "del": "delawarština", - "den": "atabaské jazyky", - "deu": "Němčina", - "dgr": "dogrib", - "din": "dinkština", - "div": "Dhivehi", - "doi": "Dogri (macrolanguage)", - "dsb": "Sorbian; Lower", - "dua": "dualština", - "dum": "Dutch; Middle (ca. 1050-1350)", - "dyu": "djula", - "dzo": "Bhútánština", - "efi": "efik", - "egy": "egyptština (starověká)", - "eka": "ekajuk", - "ell": "řečtina; moderní (1453-)", - "elx": "elamština", - "eng": "Angličtina", - "enm": "Angličtina; středověká (1100-1500)", - "epo": "esperanto", - "est": "estonština", - "eus": "baskičtina", - "ewe": "eweština", - "ewo": "ewondo", - "fan": "Fang (Equatorial Guinea)", - "fao": "faerština", - "fas": "perština", - "fat": "fantiština", - "fij": "Fidži", - "fil": "Filipino", - "fin": "finština", - "fon": "fonština", - "fra": "francouzština", - "frm": "French; Middle (ca. 1400-1600)", - "fro": "French; Old (842-ca. 1400)", - "frr": "Frisian; Northern", - "frs": "Frisian; Eastern", - "fry": "Frisian; Western", - "ful": "fulahština", - "fur": "furlanština", - "gaa": "ga", - "gay": "gayo", - "gba": "Gbaya (Central African Republic)", - "gez": "etiopština", - "gil": "kiribatština", - "gla": "Gaelic; Scottish", - "gle": "irština", - "glg": "Galician", - "glv": "manština", - "gmh": "German; Middle High (ca. 1050-1500)", - "goh": "German; Old High (ca. 750-1050)", - "gon": "góndština", - "gor": "gorontalo", - "got": "gótština", - "grb": "grebo", - "grc": "řečtina; starověká (do 1453)", - "grn": "Guaranština", - "gsw": "German; Swiss", - "guj": "Gudžarátština", - "gwi": "Gwichʼin", - "hai": "haida", - "hat": "Creole; Haitian", - "hau": "Hausa", - "haw": "havajština", - "heb": "hebrejština", - "her": "herero", - "hil": "hiligayonština", - "hin": "hindština", - "hit": "chetitština", - "hmn": "hmongština", - "hmo": "hiri motu", - "hrv": "chorvatština", - "hsb": "Sorbian; Upper", - "hun": "maďarština", - "hup": "hupa", - "hye": "arménština", - "iba": "iban", - "ibo": "igbo", - "ido": "ido", - "iii": "Yi; Sichuan", - "iku": "Inuktitutština", - "ile": "Interlingue", - "ilo": "ilokánština", - "ina": "Interlingua (Mezinárodní pomocná jazyková asociace)", - "ind": "indonézština", - "inh": "inguština", - "ipk": "Inupiakština", - "isl": "islandština", - "ita": "italština", - "jav": "jávština", - "jbo": "lojban", - "jpn": "japonština", - "jpr": "judeo-perština", - "jrb": "judeo-arabština", - "kaa": "karakalpačtina", - "kab": "kabulí", - "kac": "kačjinština", - "kal": "Kalaallisut", - "kam": "Kamba (Kenya)", - "kan": "Kannadština", - "kas": "kašmírština", - "kat": "Gruzínština", - "kau": "kanuri", - "kaw": "kawi", - "kaz": "Kazachština", - "kbd": "kabardština", - "kha": "Khasi", - "khm": "Khmer; Central", - "kho": "chotánština", - "kik": "Kikuyu", - "kin": "Kinyarwandština", - "kir": "Kirgizština", - "kmb": "kimbundština", - "kok": "Konkani (macrolanguage)", - "kom": "komijština", - "kon": "Kongo", - "kor": "korejština", - "kos": "kosrajština", - "kpe": "kpelle", - "krc": "karachay-balkarština", - "krl": "karelština", - "kru": "kurukh", - "kua": "Kuanyama", - "kum": "kumyčtina", - "kur": "kurdština", - "kut": "kutenai", - "lad": "ladino", - "lah": "lahnda", - "lam": "lambština", - "lao": "Laoština", - "lat": "latina", - "lav": "Latvian", - "lez": "lezgiština", - "lim": "Limburgan", - "lin": "Ngalština", - "lit": "litevština", - "lol": "mongština", - "loz": "lozština", - "ltz": "Luxembourgish", - "lua": "luba-luluaština", - "lub": "lubu-katanžština", - "lug": "Ganda", - "lui": "luiseňo", - "lun": "lundština", - "luo": "luoština (Keňa a Tanzanie)", - "lus": "lušáí", - "mad": "madurština", - "mag": "magahština", - "mah": "Marshallese", - "mai": "maithilština", - "mak": "makasarština", - "mal": "Malabarština", - "man": "mandingština", - "mar": "maráthština", - "mas": "masajština", - "mdf": "moksha", - "mdr": "mandarínština", - "men": "Mende (Sierra Leone)", - "mga": "irština; středověká (900-1200)", - "mic": "Mi'kmaq", - "min": "minangkabau", - "mis": "Uncoded languages", - "mkd": "makedonština", - "mlg": "Malgaština", - "mlt": "maltézština", - "mnc": "manchu", - "mni": "manipurština", - "moh": "mohawk", - "mon": "mongolština", - "mos": "mosi", - "mri": "maorština", - "msa": "Malay (macrolanguage)", - "mul": "násobné jazyky", - "mus": "krík", - "mwl": "mirandština", - "mwr": "márvárština", - "mya": "Barmština", - "myv": "erzya", - "nap": "neapolština", - "nau": "naurština", - "nav": "navažština", - "nbl": "Ndebele; South", - "nde": "Ndebele; North", - "ndo": "ndondština", - "nds": "German; Low", - "nep": "nepálština", - "new": "Bhasa; Nepal", - "nia": "nias", - "niu": "niue", - "nld": "holandština", - "nno": "Norwegian Nynorsk", - "nob": "Norwegian Bokmål", - "nog": "nogai", - "non": "norština; stará", - "nor": "norština", - "nqo": "N'Ko", - "nso": "Sotho; Northern", - "nwc": "Newari; Old", - "nya": "Nyanja", - "nym": "ňamwežština", - "nyn": "nyankolština", - "nyo": "Nyoro", - "nzi": "nzima", - "oci": "Occitan (post 1500)", - "oji": "Ojibwa", - "ori": "orija", - "orm": "Oromo (Afan)", - "osa": "osagština", - "oss": "Ossetian", - "ota": "turečtina; osmanská (1500-1928)", - "pag": "pangsinan", - "pal": "pahlaví", - "pam": "pampangau", - "pan": "Panjabi", - "pap": "papiamento", - "pau": "palauština", - "peo": "Persian; Old (ca. 600-400 B.C.)", - "phn": "Slovinština", - "pli": "páli", - "pol": "Polština", - "pon": "pohnpeiština", - "por": "portugalština", - "pro": "provensálština; stará (do 1500)", - "pus": "pašto", - "que": "kečuánština", - "raj": "rádžasthánština", - "rap": "rapanuiština", - "rar": "Maori; Cook Islands", - "roh": "Romansh", - "rom": "římština", - "ron": "rumunština", - "run": "Kirundi", - "rup": "Romanian; Macedo-", - "rus": "Ruština", - "sad": "sandawština", - "sag": "sangoština", - "sah": "jakutština", - "sam": "Aramaic; Samaritan", - "san": "sanskrt", - "sas": "sačtina", - "sat": "santálí", - "scn": "sicilština", - "sco": "skotština", - "sel": "selkupština", - "sga": "irština; stará (do 900)", - "shn": "šanština", - "sid": "sidamo", - "sin": "Sinhálština", - "slk": "Slovenština", - "slv": "slovinština", - "sma": "Sami; Southern", - "sme": "Sami; Northern", - "smj": "lule sami", - "smn": "Sami; Inari", - "smo": "Samoyština", - "sms": "Sami; Skolt", - "sna": "šonština", - "snd": "sindhština", - "snk": "sonikština", - "sog": "sogdijština", - "som": "somálština", - "sot": "sotština; jižní", - "spa": "španělština", - "sqi": "albánština", - "srd": "sardinština", - "srn": "Sranan Tongo", - "srp": "srbština", - "srr": "Serer", - "ssw": "Siswatština", - "suk": "sukuma", - "sun": "Sundanština", - "sus": "susu", - "sux": "sumerština", - "swa": "svahilština (makrojazyk)", - "swe": "švédština", - "syc": "Syriac; Classical", - "syr": "syrština", - "tah": "tahitština", - "tam": "Tamilština", - "tat": "tatarština", - "tel": "Telugu", - "tem": "temne", - "ter": "tereno", - "tet": "tetumština", - "tgk": "Tádžičtina", - "tgl": "Tagalog", - "tha": "thajština", - "tig": "tigrejština", - "tir": "Tigrinijština", - "tiv": "tivština", - "tkl": "tokelauština", - "tlh": "Klingon", - "tli": "tlingit", - "tmh": "Tamashek", - "tog": "tongština (nyasa)", - "ton": "Tonga", - "tpi": "tok pisin", - "tsi": "tsimshijské jazyky", - "tsn": "Setswanština", - "tso": "Tsonga", - "tuk": "turkmenistánština", - "tum": "tumbukština", - "tur": "turečtina", - "tvl": "tuvalština", - "twi": "ťwiština", - "tyv": "tuvština", - "udm": "udmurtština", - "uga": "ugaritština", - "uig": "Uighurština", - "ukr": "ukrajinština", - "umb": "umbundu", - "und": "neurčitý", - "urd": "urdština", - "uzb": "uzbekistánština", - "vai": "vai", - "ven": "vendština", - "vie": "vietnamština", - "vol": "volapük", - "vot": "votiatština", - "wal": "Wolaytta", - "war": "Waray (Philippines)", - "was": "washo", - "wln": "valonština", - "wol": "volofština", - "xal": "Kalmyk", - "xho": "xhoština", - "yao": "jaoština", - "yap": "japština", - "yid": "Jidiš", - "yor": "jorubština", - "zap": "Zapotec", - "zbl": "Blissymbols", - "zen": "zenaga", - "zha": "Zhuang", - "zho": "čínština", - "zul": "Zulu", - "zun": "zunijština", - "zxx": "bez lingvistického obsahu", - "zza": "zaza" - }, - "es": { - "aar": "Afar", - "abk": "Abkhazian", + "tr": { + "abk": "Abhazca", "ace": "Achinese", "ach": "Acoli", "ada": "Adangme", "ady": "Adyghe", + "aar": "Afar", "afh": "Afrihili", - "afr": "Afrikaans", - "ain": "Ainu (Japan)", - "aka": "Akan", - "akk": "Akkadian", - "ale": "Aleut", - "alt": "Altai; Southern", - "amh": "Amharic", - "ang": "English; Old (ca. 450-1100)", + "afr": "Afrikanca", + "ain": "Ainu (Japonca)", + "aka": "Akanca (Afrika dili)", + "akk": "Akatça", + "sqi": "Albanian", + "ale": "Alaskaca", + "amh": "Etiyopyaca", "anp": "Angika", - "ara": "Arabic", - "arc": "Aramaic; Official (700-300 BCE)", - "arg": "Aragonese", - "arn": "Mapudungun", - "arp": "Arapaho", - "arw": "Arawak", - "asm": "Assamese", - "ast": "Asturian", - "ava": "Avaric", - "ave": "Avestan", - "awa": "Awadhi", - "aym": "Aymara", - "aze": "Azerbaijani", - "bak": "Bashkir", - "bal": "Baluchi", - "bam": "Bambara", - "ban": "Balinese", - "bas": "Basa (Cameroon)", - "bej": "Beja", - "bel": "Belarusian", + "ara": "Arapça", + "arg": "Aragonca (İspanya)", + "arp": "Arapaho (Kuzey Amerika yerlileri)", + "arw": "Arawak (Surinam)", + "hye": "Ermenice", + "asm": "Assamese (Hindistan)", + "ast": "Asturyasca", + "ava": "Avarca", + "ave": "Avestan (Eski İran)", + "awa": "Awadhi (Hindistan)", + "aym": "Aymara (Güney Amerika)", + "aze": "Azerice", + "ban": "Balice (Bali adaları)", + "bal": "Belucice (İran)", + "bam": "Bambara (Mali)", + "bas": "Basa (Kamerun)", + "bak": "Başkırca", + "eus": "Baskça", + "bej": "Beja (Eritre; Sudan)", + "bel": "Beyaz Rusça", "bem": "Bemba (Zambia)", - "ben": "Bengali", - "bho": "Bhojpuri", - "bik": "Bikol", - "bin": "Bini", - "bis": "Bislama", - "bla": "Siksika", - "bod": "Tibetan", - "bos": "Bosnian", - "bra": "Braj", - "bre": "Breton", - "bua": "Buriat", - "bug": "Buginese", - "bul": "Bulgarian", + "ben": "Bengalce", + "bho": "Bhojpuri (Hindistan)", + "bik": "Bikol (Filipinler)", "byn": "Bilin", - "cad": "Caddo", - "car": "Carib; Galibi", - "cat": "Catalan", - "ceb": "Cebuano", - "ces": "Czech", - "cha": "Chamorro", - "chb": "Chibcha", - "che": "Chechen", - "chg": "Chagatai", + "bin": "Bini (Afrika)", + "bis": "Bislama (Vanuatu; Kuzey Pasifik)", + "zbl": "Blis Sembolleri", + "bos": "Boşnakça", + "bra": "Braj (Hindistan)", + "bre": "Bretonca", + "bug": "Buginese (Endonezya)", + "bul": "Bulgarca", + "bua": "Buriat (Moğolistan)", + "mya": "Burmaca", + "cad": "Caddo (Kuzey Amerika yerlileri)", + "cat": "Katalanca", + "ceb": "Cebuano (Filipinler)", + "chg": "Çağatayca", + "cha": "Chamorro (Guam adaları)", + "che": "Çeçence", + "chr": "Cherokee (Kuzey Amerika yerlileri)", + "chy": "Cheyenne (kuzey Amerika yerlileri)", + "chb": "Chibcha (Kolombiya)", + "zho": "Çince", + "chn": "Chinook lehçesi (Kuzey Batı Amerika kıyıları)", + "chp": "Chipewyan (Kuzey Amerika yerlileri)", + "cho": "Choctaw (Kuzey Amerika yerlileri)", "chk": "Chuukese", - "chm": "Mari (Russia)", - "chn": "Chinook jargon", - "cho": "Choctaw", - "chp": "Chipewyan", - "chr": "Cherokee", - "chu": "Slavonic; Old", - "chv": "Chuvash", - "chy": "Cheyenne", - "cop": "Coptic", - "cor": "Cornish", - "cos": "Corsican", - "cre": "Cree", - "crh": "Turkish; Crimean", - "csb": "Kashubian", - "cym": "Welsh", - "dak": "Dakota", - "dan": "Danish", - "dar": "Dargwa", - "del": "Delaware", - "den": "Slave (Athapascan)", - "deu": "German", - "dgr": "Dogrib", - "din": "Dinka", + "chv": "Çuvaş (Türkçe)", + "cop": "Kıptice (Eski Mısır)", + "cor": "Cornish (Kelt)", + "cos": "Korsikaca", + "cre": "Cree (Kuzey Amerika yerlileri)", + "mus": "Creek", + "hrv": "Hırvatça", + "ces": "Çekçe", + "dak": "Dakota (Kuzey Amerika yerlileri)", + "dan": "Danimarkaca; Danca", + "dar": "Dargwa (Dağıstan)", + "del": "Delaware (Kuzey Amerika yerlileri)", "div": "Dhivehi", - "doi": "Dogri (macrolanguage)", - "dsb": "Sorbian; Lower", - "dua": "Duala", - "dum": "Dutch; Middle (ca. 1050-1350)", - "dyu": "Dyula", - "dzo": "Dzongkha", - "efi": "Efik", - "egy": "Egyptian (Ancient)", - "eka": "Ekajuk", - "ell": "Greek; Modern (1453-)", - "elx": "Elamite", - "eng": "English", - "enm": "English; Middle (1100-1500)", + "din": "Dinka (Sudan)", + "doi": "Dogri (makro dili)", + "dgr": "Dogrib (Kanada)", + "dua": "Duala (Afrika)", + "nld": "Flâmanca (Hollanda dili)", + "dyu": "Dyula (Burkina Faso; Mali)", + "dzo": "Dzongkha (Butan)", + "efi": "Efik (Afrika)", + "egy": "Mısırca (Eski)", + "eka": "Ekajuk (Afrika)", + "elx": "Elamca", + "eng": "İngilizce", + "myv": "Erzya dili", "epo": "Esperanto", - "est": "Estonian", - "eus": "Basque", - "ewe": "Ewe", - "ewo": "Ewondo", - "fan": "Fang (Equatorial Guinea)", - "fao": "Faroese", - "fas": "Persian", - "fat": "Fanti", - "fij": "Fijian", - "fil": "Filipino", - "fin": "Finnish", - "fon": "Fon", - "fra": "French", - "frm": "French; Middle (ca. 1400-1600)", - "fro": "French; Old (842-ca. 1400)", - "frr": "Frisian; Northern", - "frs": "Frisian; Eastern", - "fry": "Frisian; Western", - "ful": "Fulah", - "fur": "Friulian", - "gaa": "Ga", - "gay": "Gayo", - "gba": "Gbaya (Central African Republic)", - "gez": "Geez", - "gil": "Gilbertese", - "gla": "Gaelic; Scottish", - "gle": "Irish", - "glg": "Galician", - "glv": "Manx", - "gmh": "German; Middle High (ca. 1050-1500)", - "goh": "German; Old High (ca. 750-1050)", - "gon": "Gondi", - "gor": "Gorontalo", - "got": "Gothic", - "grb": "Grebo", - "grc": "Greek; Ancient (to 1453)", - "grn": "Guarani", - "gsw": "German; Swiss", - "guj": "Gujarati", + "est": "Estonca", + "ewe": "Ewe (Afrika)", + "ewo": "Ewondo (Afrika)", + "fan": "Fang (Ekvatoryal Guinea)", + "fat": "Fanti (Afrika)", + "fao": "Faroece", + "fij": "Fiji dili", + "fil": "Filipince", + "fin": "Fince", + "fon": "Fon (Benin)", + "fra": "Fransızca", + "fur": "Friulian (İtalya)", + "ful": "Fulah (Afrika)", + "gaa": "Ganaca", + "glg": "Galce", + "lug": "Ganda Dili", + "gay": "Gayo (Sumatra)", + "gba": "Gbaya (Orta Afrika Cumhuriyeti)", + "gez": "Geez (Etiyopya)", + "kat": "Gürcüce", + "deu": "Almanca", + "gil": "Kiribati dili", + "gon": "Gondi (Hindistan)", + "gor": "Gorontalo (Endonezya)", + "got": "Gotik", + "grb": "Grebo (Liberya)", + "grn": "Guarani (Paraguay)", + "guj": "Gucaratça", "gwi": "Gwichʼin", - "hai": "Haida", - "hat": "Creole; Haitian", - "hau": "Hausa", - "haw": "Hawaiian", - "heb": "Hebrew", - "her": "Herero", + "hai": "Haida (Kuzey Amerika yerlileri)", + "hau": "Hausa Dili", + "haw": "Havai Dili", + "heb": "İbranice", + "her": "Herero Dili", "hil": "Hiligaynon", - "hin": "Hindi", - "hit": "Hittite", - "hmn": "Hmong", + "hin": "Hintçe", "hmo": "Hiri Motu", - "hrv": "Croatian", - "hsb": "Sorbian; Upper", - "hun": "Hungarian", + "hit": "Hititçe", + "hmn": "Hmong", + "hun": "Macarca", "hup": "Hupa", - "hye": "Armenian", "iba": "Iban", - "ibo": "Igbo", - "ido": "Ido", - "iii": "Yi; Sichuan", - "iku": "Inuktitut", - "ile": "Interlingue", + "isl": "İzlandaca", + "ido": "Ido Dili", + "ibo": "Igbo Dili", "ilo": "Iloko", - "ina": "Interlingua (International Auxiliary Language Association)", - "ind": "Indonesian", - "inh": "Ingush", - "ipk": "Inupiaq", - "isl": "Icelandic", - "ita": "Italian", - "jav": "Javanese", - "jbo": "Lojban", - "jpn": "Japanese", - "jpr": "Judeo-Persian", - "jrb": "Judeo-Arabic", - "kaa": "Kara-Kalpak", + "ind": "Endonezyaca", + "inh": "İnguşca", + "ina": "Interlingua (Uluslararası Yardımcı Dil Kurumu)", + "ile": "Interlingue", + "iku": "Inuktitut", + "ipk": "Inupiak Dili", + "gle": "İrlandaca", + "ita": "İtalyanca", + "jpn": "Japonca", + "jav": "Cava Dili", + "jrb": "Yahudi-Arapçası", + "jpr": "Yahudi-Farsça", + "kbd": "Kabardian", "kab": "Kabyle", "kac": "Kachin", "kal": "Kalaallisut", + "xal": "Kalmyk", "kam": "Kamba (Kenya)", "kan": "Kannada", - "kas": "Kashmiri", - "kat": "Georgian", - "kau": "Kanuri", - "kaw": "Kawi", - "kaz": "Kazakh", - "kbd": "Kabardian", - "kha": "Khasi", - "khm": "Khmer; Central", - "kho": "Khotanese", - "kik": "Kikuyu", - "kin": "Kinyarwanda", - "kir": "Kirghiz", - "kmb": "Kimbundu", - "kok": "Konkani (macrolanguage)", - "kom": "Komi", - "kon": "Kongo", - "kor": "Korean", - "kos": "Kosraean", - "kpe": "Kpelle", + "kau": "Kanuri Dili", + "kaa": "Kara-Kalpak", "krc": "Karachay-Balkar", "krl": "Karelian", - "kru": "Kurukh", - "kua": "Kuanyama", + "kas": "Keşmirce", + "csb": "Kashubian (Lehçe diyalekti)", + "kaw": "Kawi", + "kaz": "Kazakça", + "kha": "Khasi", + "kho": "Khotanese", + "kik": "Kikuyu Dili", + "kmb": "Kimbundu", + "kin": "Kinyarwanda", + "kir": "Kırgızca", + "tlh": "Klingon", + "kom": "Komi Dili", + "kon": "Kongo Dili", + "kok": "Konkani (makro dil)", + "kor": "Korece", + "kos": "Kosraean", + "kpe": "Kpelle", + "kua": "Kuanyama Dili", "kum": "Kumyk", - "kur": "Kurdish", + "kur": "Kürtçe", + "kru": "Kurukh", "kut": "Kutenai", "lad": "Ladino", "lah": "Lahnda", "lam": "Lamba", - "lao": "Lao", - "lat": "Latin", - "lav": "Latvian", + "lao": "Laos Dili", + "lat": "Latince", + "lav": "Letonca", "lez": "Lezghian", - "lim": "Limburgan", - "lin": "Lingala", - "lit": "Lithuanian", - "lol": "Mongo", + "lim": "Liburg Dili", + "lin": "Lingala Dili", + "lit": "Litvanyaca", + "jbo": "Lojban dili", "loz": "Lozi", - "ltz": "Luxembourgish", + "lub": "Luba Katanga Dili", "lua": "Luba-Lulua", - "lub": "Luba-Katanga", - "lug": "Ganda", "lui": "Luiseno", + "smj": "Lule Sami", "lun": "Lunda", - "luo": "Luo (Kenya and Tanzania)", + "luo": "Luo (Kenya ve Tanzanya)", "lus": "Lushai", + "ltz": "Lüksemburg Dili", + "mkd": "Makedonca", "mad": "Madurese", "mag": "Magahi", - "mah": "Marshallese", - "mai": "Maithili", + "mai": "Maithili dili", "mak": "Makasar", + "mlg": "Madagaskar Dili", + "msa": "Malay (makro dili)", "mal": "Malayalam", - "man": "Mandingo", - "mar": "Marathi", - "mas": "Masai", - "mdf": "Moksha", - "mdr": "Mandar", - "men": "Mende (Sierra Leone)", - "mga": "Irish; Middle (900-1200)", - "mic": "Mi'kmaq", - "min": "Minangkabau", - "mis": "Uncoded languages", - "mkd": "Macedonian", - "mlg": "Malagasy", - "mlt": "Maltese", + "mlt": "Maltaca", "mnc": "Manchu", - "mni": "Manipuri", - "moh": "Mohawk", - "mon": "Mongolian", - "mos": "Mossi", - "mri": "Maori", - "msa": "Malay (macrolanguage)", - "mul": "Multiple languages", - "mus": "Creek", - "mwl": "Mirandese", + "mdr": "Mandar", + "man": "Mandingo", + "mni": "Manipuri dili", + "glv": "Manx (Galler)", + "mri": "Maori Dili", + "arn": "Mapudungun", + "mar": "Marathi", + "chm": "Mari (Rusya)", + "mah": "Marshall Dili", "mwr": "Marwari", - "mya": "Burmese", - "myv": "Erzya", - "nap": "Neapolitan", + "mas": "Masai", + "men": "Mende (Sierra Leone)", + "mic": "Mi'kmak", + "min": "Minangkabau", + "mwl": "Mirandese", + "moh": "Mohawk", + "mdf": "Moşka", + "lol": "Mongo", + "mon": "Moğol Dili", + "mos": "Mossi", + "mul": "Çoklu diller", + "nqo": "N'Ko", "nau": "Nauru", - "nav": "Navajo", - "nbl": "Ndebele; South", - "nde": "Ndebele; North", - "ndo": "Ndonga", - "nds": "German; Low", - "nep": "Nepali", - "new": "Bhasa; Nepal", + "nav": "Navajo Dili", + "ndo": "Ndonga Dili", + "nap": "Neapolitan", "nia": "Nias", "niu": "Niuean", - "nld": "Dutch", - "nno": "Norwegian Nynorsk", - "nob": "Norwegian Bokmål", + "zxx": "Hiçbir dil içeriği yok", "nog": "Nogai", - "non": "Norse; Old", - "nor": "Norwegian", - "nqo": "N'Ko", - "nso": "Sotho; Northern", - "nwc": "Newari; Old", - "nya": "Nyanja", + "nor": "Norveçce", + "nob": "Norveççe Bokmal", + "nno": "Norveççe Nynorsk", "nym": "Nyamwezi", + "nya": "Nyanja", "nyn": "Nyankole", "nyo": "Nyoro", "nzi": "Nzima", - "oci": "Occitan (post 1500)", - "oji": "Ojibwa", - "ori": "Oriya", - "orm": "Oromo", + "oci": "Oksitanca (1500 sonrası)", + "oji": "Ojibwa Dili", + "orm": "Oromo Dili", "osa": "Osage", - "oss": "Ossetian", - "ota": "Turkish; Ottoman (1500-1928)", - "pag": "Pangasinan", - "pal": "Pahlavi", - "pam": "Pampanga", - "pan": "Panjabi", - "pap": "Papiamento", + "oss": "Osetya Dili", + "pal": "Pehlevi", "pau": "Palauan", - "peo": "Persian; Old (ca. 600-400 B.C.)", - "phn": "Phoenician", - "pli": "Pali", - "pol": "Polish", + "pli": "Pali Dili", + "pam": "Pampanga", + "pag": "Pangasinan", + "pan": "Pencabi Dili", + "pap": "Papiamento", + "fas": "Farsça", + "phn": "Fenikçe", "pon": "Pohnpeian", - "por": "Portuguese", - "pro": "Provençal; Old (to 1500)", - "pus": "Pashto", + "pol": "Polonyaca", + "por": "Portekizce", + "pus": "Pushto", "que": "Quechua", "raj": "Rajasthani", "rap": "Rapanui", - "rar": "Maori; Cook Islands", - "roh": "Romansh", - "rom": "Romany", - "ron": "Romanian", - "run": "Rundi", - "rup": "Romanian; Macedo-", - "rus": "Russian", + "ron": "Rumence", + "roh": "Romanca", + "rom": "Çingene Dili", + "run": "Kirundi", + "rus": "Rusça", + "smo": "Samoa Dili", "sad": "Sandawe", - "sag": "Sango", - "sah": "Yakut", - "sam": "Aramaic; Samaritan", - "san": "Sanskrit", + "sag": "Sangho", + "san": "Sanskritçe", + "sat": "Santali dili", + "srd": "Sardinya", "sas": "Sasak", - "sat": "Santali", - "scn": "Sicilian", - "sco": "Scots", + "sco": "İskoç lehçesi", "sel": "Selkup", - "sga": "Irish; Old (to 900)", - "shn": "Shan", - "sid": "Sidamo", - "sin": "Sinhala", - "slk": "Slovak", - "slv": "Slovenian", - "sma": "Sami; Southern", - "sme": "Sami; Northern", - "smj": "Lule Sami", - "smn": "Sami; Inari", - "smo": "Samoan", - "sms": "Sami; Skolt", - "sna": "Shona", - "snd": "Sindhi", - "snk": "Soninke", - "sog": "Sogdian", - "som": "Somali", - "sot": "Sotho; Southern", - "spa": "Spanish", - "sqi": "Albanian", - "srd": "Sardinian", - "srn": "Sranan Tongo", - "srp": "Serbian", + "srp": "Sırpça", "srr": "Serer", - "ssw": "Swati", + "shn": "Shan", + "sna": "Shona", + "scn": "Sicilyalı", + "sid": "Sidamo", + "bla": "Siksika (Kuzey Amerika yerlileri)", + "snd": "Sindhi", + "sin": "Sinhala Dili", + "den": "Slave (Athapascan; Kuzey Amerika yerlileri)", + "slk": "Slovakça", + "slv": "Slovence", + "sog": "Sogdian", + "som": "Somali Dili", + "snk": "Soninke", + "spa": "İspanyolca", + "srn": "Sranan Tongo", "suk": "Sukuma", - "sun": "Sundanese", + "sux": "Sümerce", + "sun": "Sudan Dili", "sus": "Susu", - "sux": "Sumerian", - "swa": "Swahili (macrolanguage)", - "swe": "Swedish", - "syc": "Syriac; Classical", - "syr": "Syriac", - "tah": "Tahitian", - "tam": "Tamil", - "tat": "Tatar", + "swa": "Swahili (makro dil)", + "ssw": "Siswati", + "swe": "İsveçce", + "syr": "Süryanice", + "tgl": "Tagalog", + "tah": "Tahitice", + "tgk": "Tacikçe", + "tmh": "Tamashek", + "tam": "Tamilce", + "tat": "Tatarca", "tel": "Telugu", - "tem": "Timne", "ter": "Tereno", "tet": "Tetum", - "tgk": "Tajik", - "tgl": "Tagalog", - "tha": "Thai", + "tha": "Taylandça", + "bod": "Tibetçe", "tig": "Tigre", - "tir": "Tigrinya", + "tir": "Tigrinya Dili", + "tem": "Timne", "tiv": "Tiv", - "tkl": "Tokelau", - "tlh": "Klingon", "tli": "Tlingit", - "tmh": "Tamashek", - "tog": "Tonga (Nyasa)", - "ton": "Tonga (Tonga Islands)", "tpi": "Tok Pisin", + "tkl": "Tokelau", + "tog": "Tonga (Nyasa)", + "ton": "Tonga", "tsi": "Tsimshian", - "tsn": "Tswana", "tso": "Tsonga", - "tuk": "Turkmen", + "tsn": "Setswana", "tum": "Tumbuka", - "tur": "Turkish", + "tur": "Türkçe", + "tuk": "Türkmence", "tvl": "Tuvalu", - "twi": "Twi", "tyv": "Tuvinian", + "twi": "Twi", "udm": "Udmurt", - "uga": "Ugaritic", - "uig": "Uighur", - "ukr": "Ukrainian", + "uga": "Ugarit Çivi Yazısı", + "uig": "Uygurca", + "ukr": "Ukraynaca", "umb": "Umbundu", - "und": "Undetermined", - "urd": "Urdu", - "uzb": "Uzbek", + "mis": "Şifresiz diller", + "und": "Belirlenemeyen", + "urd": "Urduca", + "uzb": "Özbekçe", "vai": "Vai", - "ven": "Venda", - "vie": "Vietnamese", + "ven": "Venda Dili", + "vie": "Vietnamca", "vol": "Volapük", "vot": "Votic", + "wln": "Valonca", + "war": "Waray (Filipinler)", + "was": "Vasho", + "cym": "Gal Dili", "wal": "Wolaytta", - "war": "Waray (Philippines)", - "was": "Washo", - "wln": "Walloon", "wol": "Wolof", - "xal": "Kalmyk", "xho": "Xhosa", + "sah": "Yakut", "yao": "Yao", "yap": "Yapese", - "yid": "Yiddish", + "yid": "Yidiş", "yor": "Yoruba", "zap": "Zapotec", - "zbl": "Blissymbols", + "zza": "Zaza", "zen": "Zenaga", - "zha": "Zhuang", - "zho": "Chinese", + "zha": "Zuang Dili", "zul": "Zulu", - "zun": "Zuni", + "zun": "Zuni" + }, + "uk": { + "aar": "афар", + "abk": "абхазька", + "ace": "ачеська", + "ach": "ачолі", + "ada": "адангме", + "ady": "адигейська", + "afh": "афрингілі", + "afr": "африкаанс", + "ain": "айнська (Японія)", + "aka": "акан", + "akk": "аккадська", + "ale": "алеутська", + "alt": "алтайська (південна)", + "amh": "амхарська", + "ang": "давньоанглійська (бл. 450-1100)", + "anp": "ангіка", + "ara": "арабська", + "arc": "арамейська (офіційна; 700-300 до нашої ери)", + "arg": "арагонська", + "arn": "арауканська", + "arp": "арапахо", + "arw": "аравакська", + "asm": "ассамська", + "ast": "астурійська", + "ava": "аварська", + "ave": "авестанська", + "awa": "авадхі", + "aym": "аймарська", + "aze": "азербайджанська", + "bak": "башкирська", + "bal": "белуджійська", + "bam": "бамбара", + "ban": "балійська", + "bas": "баса (Камерун)", + "bej": "бежа", + "bel": "білоруська", + "bem": "бемба (Замбія)", + "ben": "бенгальська", + "bho": "бходжпурі", + "bik": "бікольська", + "bin": "біні", + "bis": "біслама", + "bla": "сісіка", + "bod": "тибетська", + "bos": "боснійська", + "bra": "брай", + "bre": "бретонська", + "bua": "бурятська", + "bug": "бугійська", + "bul": "болгарська", + "byn": "білін", + "cad": "каддо", + "car": "карибська (галібі)", + "cat": "каталонська", + "ceb": "себуано", + "ces": "чеська", + "cha": "чаморо", + "chb": "чибча", + "che": "чеченська", + "chg": "чагатайська", + "chk": "чуукська", + "chm": "марійська (Росія)", + "chn": "чинук; жаргон", + "cho": "чоктау", + "chp": "чипев’ян", + "chr": "черокі", + "chu": "давньослов’янська", + "chv": "чуваська", + "chy": "шаєнн", + "cop": "коптська", + "cor": "корнійська", + "cos": "корсиканська", + "cre": "крі", + "crh": "турецька (кримська)", + "csb": "кашубська", + "cym": "валійська", + "dak": "дакота", + "dan": "данська", + "dar": "даргва", + "del": "делаварська", + "den": "слейві (атабаська)", + "deu": "німецька", + "dgr": "догріб", + "din": "дінка", + "div": "мальдивська", + "doi": "догрі (макромова)", + "dsb": "нижньолужицька", + "dua": "дуала", + "dum": "середньовічна голландська (бл. 1050-1350)", + "dyu": "діула", + "dzo": "дзонг-ке", + "efi": "ефік", + "egy": "давньоєгипетська", + "eka": "екаджук", + "ell": "грецька (з 1453)", + "elx": "еламська", + "eng": "англійська", + "enm": "середньоанглійська (1100-1500)", + "epo": "есперанто", + "est": "естонська", + "eus": "баскська", + "ewe": "еве", + "ewo": "евондо", + "fan": "фанг (Екваторіальна Гвінея)", + "fao": "фарерська", + "fas": "перська", + "fat": "фанті", + "fij": "фіджійська", + "fil": "філіппінська", + "fin": "фінська", + "fon": "фон", + "fra": "французька", + "frm": "середньофранцузька (бл. 1400-1600)", + "fro": "давньофранцузька (842-бл. 1400)", + "frr": "фризька (північна)", + "frs": "фризька (східна)", + "fry": "фризька (західна)", + "ful": "фулах", + "fur": "фріульська", + "gaa": "га", + "gay": "гайо", + "gba": "гбая (Центральноафриканська Республіка)", + "gez": "гііз", + "gil": "гільбертська", + "gla": "гаельська (Шотландія)", + "gle": "ірландська", + "glg": "галісійська", + "glv": "манкс", + "gmh": "середньоверхньонімецька (бл. 1050-1500)", + "goh": "давньосередньонімецька (бл. 750-1050)", + "gon": "гонді", + "gor": "горонтало", + "got": "готська", + "grb": "гребо", + "grc": "давньогрецька (до 1453)", + "grn": "гуарані", + "gsw": "німецька (Швейцарія)", + "guj": "гуджараті", + "gwi": "гвічин", + "hai": "хайда", + "hat": "креольська (гаїтянська)", + "hau": "хауса", + "haw": "гавайська", + "heb": "іврит", + "her": "гереро", + "hil": "хілігайнон", + "hin": "хінді", + "hit": "хетська", + "hmn": "хмонг", + "hmo": "хірімоту", + "hrv": "хорватська", + "hsb": "верхньолужицька", + "hun": "угорська", + "hup": "хупа", + "hye": "вірменська", + "iba": "ібанська", + "ibo": "ігбо", + "ido": "ідо", + "iii": "ї (Сичуань)", + "iku": "інуктітут", + "ile": "окциденталь", + "ilo": "ілоко", + "ina": "інтерлінгва (Асоціація міжнародної допоміжної мови)", + "ind": "індонезійська", + "inh": "інгушська", + "ipk": "інупіак", + "isl": "ісландська", + "ita": "італійська", + "jav": "яванська", + "jbo": "ложбан", + "jpn": "японська", + "jpr": "єврейсько-перська", + "jrb": "єврейсько-арабська", + "kaa": "каракалпацька", + "kab": "кабильська", + "kac": "качин", + "kal": "калаалісут", + "kam": "камба (Кенія)", + "kan": "каннада", + "kas": "кашмірська", + "kat": "грузинська", + "kau": "канурі", + "kaw": "каві", + "kaz": "казахська", + "kbd": "кабардінська", + "kha": "кхасі", + "khm": "кхмерська (центральна)", + "kho": "хотаносакська", + "kik": "кікуйю", + "kin": "кіньяруанда", + "kir": "киргизька", + "kmb": "кімбунду", + "kok": "конкані (макромова)", + "kom": "комі", + "kon": "конго", + "kor": "корейська", + "kos": "косрейська", + "kpe": "кпелле", + "krc": "карачаєво-балкарська", + "krl": "карельська", + "kru": "курух", + "kua": "куаньяма", + "kum": "кумикська", + "kur": "курдська", + "kut": "кутенай", + "lad": "ладіно", + "lah": "лахнда", + "lam": "ламба", + "lao": "лаоська", + "lat": "латинська", + "lav": "латиська", + "lez": "лезгінська", + "lim": "лімбурганська", + "lin": "лінгала", + "lit": "литовська", + "lol": "монго", + "loz": "лозі", + "ltz": "люксембурзька", + "lua": "луба-лулуа", + "lub": "луба-катанга", + "lug": "ганда", + "lui": "луйсеньо", + "lun": "лунда", + "luo": "луо (Кенія і Танзанія)", + "lus": "лушай", + "mad": "мадурська", + "mag": "магахі", + "mah": "маршальська", + "mai": "майтхілі", + "mak": "макасарська", + "mal": "малаялам", + "man": "мандінго", + "mar": "мараті", + "mas": "масаї", + "mdf": "мокшанська", + "mdr": "мандарська", + "men": "менде (Сьєрра-Леоне)", + "mga": "середньоірландська (900-1200)", + "mic": "мікмак", + "min": "мінангкабау", + "mis": "мови без коду", + "mkd": "македонська", + "mlg": "малагасійська", + "mlt": "мальтійська", + "mnc": "манчжурська", + "mni": "маніпурська", + "moh": "мохаук", + "mon": "монгольська", + "mos": "мосі", + "mri": "маорійська", + "msa": "малайська (макромова)", + "mul": "мови; що належать до декількох родин", + "mus": "крікська", + "mwl": "мірандська", + "mwr": "марварі", + "mya": "бірманська", + "myv": "ерзянська", + "nap": "неаполітанська", + "nau": "науру", + "nav": "навахо", + "nbl": "південна ндебеле", + "nde": "північна ндебеле", + "ndo": "ндонга", + "nds": "нижньонімецька", + "nep": "непальська", + "new": "бхаса (Непал)", + "nia": "ніасійська", + "niu": "ніуе", + "nld": "голландська", + "nno": "норвезька нюноршк", + "nob": "норвезька букмол", + "nog": "ногайська", + "non": "давньонорвезька", + "nor": "норвезька", + "nqo": "н’ко", + "nso": "сото; північне", + "nwc": "неварі (давня)", + "nya": "ньянджа", + "nym": "ньямвезі", + "nyn": "ньянколе", + "nyo": "ньоро", + "nzi": "нзіма", + "oci": "оксітанська (після 1500)", + "oji": "оджибва", + "ori": "орія", + "orm": "оромо", + "osa": "оседжі", + "oss": "осетинська", + "ota": "оттоманська турецька (1500-1928)", + "pag": "пангасінан", + "pal": "пехлевійська", + "pam": "пампанга", + "pan": "пенджабі", + "pap": "папьяменто", + "pau": "палау", + "peo": "давньоперська (бл. 600-400 до н.е.)", + "phn": "фінікійська", + "pli": "палі", + "pol": "польська", + "pon": "понапе", + "por": "португальська", + "pro": "провансальська (давня; до 1500 року)", + "pus": "пуштунська", + "que": "кечуа", + "raj": "раджастхані", + "rap": "рапануї", + "rar": "маорійська (острови Кука)", + "roh": "ретророманська", + "rom": "ромська", + "ron": "румунська", + "run": "рунді", + "rup": "македоно-румунська", + "rus": "російська", + "sad": "сандаве", + "sag": "санго", + "sah": "якутська", + "sam": "арамейська (самаритянська)", + "san": "санскрит", + "sas": "сасакська", + "sat": "санталі", + "scn": "сицилійська", + "sco": "шотландська", + "sel": "селькупська", + "sga": "давньоірландська (до 900)", + "shn": "шан", + "sid": "сидама", + "sin": "сингалійська", + "slk": "словацька", + "slv": "словенська", + "sma": "саамська (південна)", + "sme": "саамська (північна)", + "smj": "лулесаамська", + "smn": "саамська (інарі)", + "smo": "самоанська", + "sms": "саамська (сколт)", + "sna": "шона", + "snd": "синдхі", + "snk": "сонікійська", + "sog": "согдійська", + "som": "сомалійська", + "sot": "сото; південна", + "spa": "іспанська", + "sqi": "албанська", + "srd": "сардинська", + "srn": "сранан-тонго", + "srp": "сербська", + "srr": "серер", + "ssw": "свазі", + "suk": "сукума", + "sun": "сунданська", + "sus": "сусу", + "sux": "шумерська", + "swa": "суахілі (макромова)", + "swe": "шведська", + "syc": "сирійська (класична)", + "syr": "сирійська", + "tah": "таїтянська", + "tam": "тамільська", + "tat": "татарська", + "tel": "телугу", + "tem": "тімне", + "ter": "терено", + "tet": "тетум", + "tgk": "таджицька", + "tgl": "тагалог", + "tha": "таїландська", + "tig": "тігре", + "tir": "тигринійська", + "tiv": "тиві", + "tkl": "токелау", + "tlh": "клінгонська", + "tli": "тлінгіт", + "tmh": "тамашек", + "tog": "тонга (ньяса)", + "ton": "тонга (острови Тонга)", + "tpi": "ток-пісін", + "tsi": "цимшіан", + "tsn": "тсвана", + "tso": "цонга", + "tuk": "туркменська", + "tum": "тумбука", + "tur": "турецька", + "tvl": "тувалу", + "twi": "тві", + "tyv": "тувінська", + "udm": "удмурдська", + "uga": "угаритська", + "uig": "уйгурська", + "ukr": "українська", + "umb": "умбунду", + "und": "невизначена", + "urd": "урду", + "uzb": "узбецька", + "vai": "вай", + "ven": "венда", + "vie": "в'єтнамська", + "vol": "волапюк", + "vot": "водська", + "wal": "волайтта", + "war": "варай (Філіппіни)", + "was": "вашо", + "wln": "валлонська", + "wol": "волоф", + "xal": "калмицька", + "xho": "хоза", + "yao": "яо", + "yap": "япська", + "yid": "ідиш", + "yor": "йоруба", + "zap": "сапотецька", + "zbl": "бліссимволіка", + "zen": "зеназька", + "zha": "чжуань", + "zho": "китайська", + "zul": "зулуська", + "zun": "зуні", + "zxx": "немає мовних даних", + "zza": "заза" + }, + "zh_Hans_CN": { + "aar": "阿法尔语", + "abk": "阿布哈兹语", + "ace": "亚齐语", + "ach": "阿乔利语", + "ada": "阿当梅语", + "ady": "阿迪格语", + "afh": "阿弗里希利语", + "afr": "南非荷兰语", + "ain": "阿伊努语(日本)", + "aka": "阿坎语", + "akk": "阿卡德语", + "ale": "阿留申语", + "alt": "阿尔泰语(南)", + "amh": "阿姆哈拉语", + "ang": "英语(上古,约 450-1100)", + "anp": "安吉卡语", + "ara": "阿拉伯语", + "arc": "阿拉米语(官方,公元前 700-300)", + "arg": "阿拉贡语", + "arn": "阿劳坎语", + "arp": "阿拉帕霍语", + "arw": "阿拉瓦克语", + "asm": "阿萨姆语", + "ast": "阿斯图里亚斯语", + "ava": "阿瓦尔语", + "ave": "阿维斯陀语", + "awa": "阿瓦德语", + "aym": "艾马拉语", + "aze": "阿塞拜疆语", + "bak": "巴什基尔语", + "bal": "俾路支语", + "bam": "班巴拉语", + "ban": "巴厘语", + "bas": "巴萨语(喀麦隆)", + "bej": "贝扎语", + "bel": "白俄罗斯语", + "bem": "本巴语(赞比亚)", + "ben": "孟加拉语", + "bho": "博杰普尔语", + "bik": "比科尔语", + "bin": "比尼语", + "bis": "比斯拉马语", + "bla": "西克西卡语", + "bod": "藏语", + "bos": "波斯尼亚语", + "bra": "布拉吉语", + "bre": "布列塔尼语", + "bua": "布里亚特语", + "bug": "布吉语", + "bul": "保加利亚语", + "byn": "比林语", + "cad": "卡多语", + "car": "加勒比语", + "cat": "加泰罗尼亚语", + "ceb": "宿务语", + "ces": "捷克语", + "cha": "查莫罗语", + "chb": "奇布查语", + "che": "车臣语", + "chg": "察合台语", + "chk": "丘克语", + "chm": "马里语(俄罗斯)", + "chn": "奇努克混合语", + "cho": "乔克托语", + "chp": "奇佩维安语", + "chr": "切罗基语", + "chu": "斯拉夫语(古教会)", + "chv": "楚瓦什语", + "chy": "夏延语", + "cop": "科普特语", + "cor": "康沃尔语", + "cos": "科西嘉语", + "cre": "克里语", + "crh": "鞑靼语(克里米亚)", + "csb": "卡舒比语", + "cym": "威尔士语", + "dak": "达科他语", + "dan": "丹麦语", + "dar": "达尔格瓦语", + "del": "特拉华语", + "den": "史拉维语(阿沙巴斯甘)", + "deu": "德语", + "dgr": "多格里布语", + "din": "丁卡语", + "div": "迪维希语", + "doi": "多格拉语", + "dsb": "索布语(下)", + "dua": "杜亚拉语", + "dum": "荷兰语(中古,约 1050-1350)", + "dyu": "迪尤拉语", + "dzo": "宗喀语", + "efi": "埃菲克语", + "egy": "埃及语(古)", + "eka": "埃克丘克语", + "ell": "希腊语(现代,1453-)", + "elx": "埃兰语", + "eng": "英语", + "enm": "英语(中古,1100-1500)", + "epo": "世界语", + "est": "爱沙尼亚语", + "eus": "巴斯克语", + "ewe": "埃维语", + "ewo": "埃翁多语", + "fan": "芳语(赤道几内亚)", + "fao": "法罗语", + "fas": "波斯语", + "fat": "芳蒂语", + "fij": "斐济语", + "fil": "菲律宾语", + "fin": "芬兰语", + "fon": "丰语", + "fra": "法语", + "frm": "法语(中古,约 1400-1600)", + "fro": "法语(上古,842-约 1400)", + "frr": "弗里西语(北)", + "frs": "弗里西亚语(东)", + "fry": "弗里西亚语(西)", + "ful": "富拉语", + "fur": "弗留利语", + "gaa": "加语", + "gay": "卡约语", + "gba": "巴亚语(中非共和国)", + "gez": "吉兹语", + "gil": "吉尔伯特语", + "gla": "盖尔语(苏格兰)", + "gle": "爱尔兰语", + "glg": "加利西亚语", + "glv": "马恩岛语", + "gmh": "德语(中古高地,约 1050-1500)", + "goh": "德语(上古高地,约 750-1050)", + "gon": "贡德语", + "gor": "哥伦打洛语", + "got": "哥特语", + "grb": "格列博语", + "grc": "希腊语(古典,直到 1453)", + "grn": "瓜拉尼语", + "gsw": "德语(瑞士)", + "guj": "古吉拉特语", + "gwi": "库臣语", + "hai": "海达语", + "hat": "克里奥尔语(海地)", + "hau": "豪萨语", + "haw": "夏威夷语", + "heb": "希伯来语", + "her": "赫雷罗语", + "hil": "希利盖农语", + "hin": "印地语", + "hit": "赫梯语", + "hmn": "苗语", + "hmo": "希里莫图语", + "hrv": "克罗地亚语", + "hsb": "索布语(上)", + "hun": "匈牙利语", + "hup": "胡帕语", + "hye": "亚美尼亚语", + "iba": "伊班语", + "ibo": "伊博语", + "ido": "伊多语", + "iii": "彝语(四川)", + "iku": "伊努伊特语", + "ile": "国际语(西方)", + "ilo": "伊洛卡诺语", + "ina": "国际语", + "ind": "印尼语", + "inh": "印古什语", + "ipk": "依努庇克语", + "isl": "冰岛语", + "ita": "意大利语", + "jav": "爪哇语", + "jbo": "逻辑语", + "jpn": "日语", + "jpr": "犹太-波斯语", + "jrb": "犹太-阿拉伯语", + "kaa": "卡拉卡尔帕克语", + "kab": "卡布列语", + "kac": "景颇语", + "kal": "格陵兰语", + "kam": "坎巴语(肯尼亚)", + "kan": "卡纳达语", + "kas": "克什米尔语", + "kat": "格鲁吉亚语", + "kau": "卡努里语", + "kaw": "卡威语", + "kaz": "哈萨克语", + "kbd": "卡巴尔达语", + "kha": "卡西语", + "khm": "高棉语", + "kho": "和田语", + "kik": "基库尤语", + "kin": "基尼阿万达语", + "kir": "吉尔吉斯语", + "kmb": "金本杜语", + "kok": "孔卡尼语", + "kom": "科米语", + "kon": "刚果语", + "kor": "朝鲜语", + "kos": "科斯拉伊语", + "kpe": "克佩勒语", + "krc": "卡拉恰伊-巴尔卡尔语", + "krl": "卡累利阿语", + "kru": "库卢克语", + "kua": "宽亚玛语", + "kum": "库梅克语", + "kur": "库尔德语", + "kut": "库特内语", + "lad": "拉迪诺语", + "lah": "拉亨达语", + "lam": "兰巴语", + "lao": "老挝语", + "lat": "拉丁语", + "lav": "拉脱维亚语", + "lez": "列兹金语", + "lim": "林堡语", + "lin": "林加拉语", + "lit": "立陶宛语", + "lol": "芒戈语", + "loz": "洛齐语", + "ltz": "卢森堡语", + "lua": "卢巴-卢拉语", + "lub": "卢巴-加丹加语", + "lug": "干达语", + "lui": "卢伊塞诺语", + "lun": "隆达语", + "luo": "卢奥语(肯尼亚和坦桑尼亚)", + "lus": "卢萨语", + "mad": "马都拉语", + "mag": "摩揭陀语", + "mah": "马绍尔语", + "mai": "米德勒语", + "mak": "望加锡语", + "mal": "马拉雅拉姆语", + "man": "曼丁哥语", + "mar": "马拉地语", + "mas": "马萨伊语", + "mdf": "莫克沙语", + "mdr": "曼达语", + "men": "门德语(塞拉利昂)", + "mga": "爱尔兰语(中古,900-1200)", + "mic": "米克马克语", + "min": "米南卡保语", + "mis": "未被编码的语言", + "mkd": "马其顿语", + "mlg": "马达加斯加语", + "mlt": "马耳他语", + "mnc": "满语", + "mni": "曼尼普尔语", + "moh": "莫霍克语", + "mon": "蒙古语", + "mos": "莫西语", + "mri": "毛利语", + "msa": "马来语族", + "mul": "多种语言", + "mus": "克里克语", + "mwl": "米兰德斯语", + "mwr": "马尔瓦利语", + "mya": "缅甸语", + "myv": "厄尔兹亚语", + "nap": "拿坡里语", + "nau": "瑙鲁语", + "nav": "纳瓦霍语", + "nbl": "恩德贝勒语(南)", + "nde": "恩德贝勒语(北)", + "ndo": "恩敦加语", + "nds": "撒克逊语(低地)", + "nep": "尼泊尔语", + "new": "尼瓦尔语", + "nia": "尼亚斯语", + "niu": "纽埃语", + "nld": "荷兰语", + "nno": "新挪威语", + "nob": "挪威布克莫尔语", + "nog": "诺盖语", + "non": "诺尔斯语(古)", + "nor": "挪威语", + "nqo": "西非书面语言字母", + "nso": "索托语(北)", + "nwc": "尼瓦尔语(古典)", + "nya": "尼扬贾语", + "nym": "尼扬韦齐语", + "nyn": "尼扬科勒语", + "nyo": "尼奥罗语", + "nzi": "恩济马语", + "oci": "奥克西唐语(1500 后)", + "oji": "奥吉布瓦语", + "ori": "奥利亚语", + "orm": "奥罗莫语", + "osa": "奥萨格语", + "oss": "奥塞梯语", + "ota": "土耳其语(奥斯曼,1500-1928)", + "pag": "邦阿西楠语", + "pal": "钵罗钵语", + "pam": "邦板牙语", + "pan": "旁遮普语", + "pap": "帕皮亚门托语", + "pau": "帕劳语", + "peo": "波斯语(古,公元前约 600-400)", + "phn": "腓尼基语", + "pli": "巴利语", + "pol": "波兰语", + "pon": "波纳佩语", + "por": "葡萄牙语", + "pro": "普罗旺斯语(古,至 1500)", + "pus": "普什图语", + "que": "克丘亚语", + "raj": "拉贾斯坦语", + "rap": "拉帕努伊语", + "rar": "拉罗汤加语", + "roh": "罗曼什语", + "rom": "罗姆语", + "ron": "罗马尼亚语", + "run": "基隆迪语", + "rup": "阿罗马尼亚语", + "rus": "俄语", + "sad": "桑达韦语", + "sag": "桑戈语", + "sah": "雅库特语", + "sam": "阿拉米语(萨马利亚)", + "san": "梵语", + "sas": "萨萨克语", + "sat": "桑塔利语", + "scn": "西西里语", + "sco": "苏格兰语", + "sel": "塞尔库普语", + "sga": "爱尔兰语(古,至 900)", + "shn": "掸语", + "sid": "锡达莫语", + "sin": "僧加罗语", + "slk": "斯洛伐克语", + "slv": "斯洛文尼亚语", + "sma": "萨米语(南)", + "sme": "萨米语(北)", + "smj": "律勒欧-萨米语", + "smn": "伊纳里-萨米语", + "smo": "萨摩亚语", + "sms": "斯科特-萨米语", + "sna": "修纳语", + "snd": "信德语", + "snk": "索宁克语", + "sog": "粟特语", + "som": "索马里语", + "sot": "索托语(南)", + "spa": "西班牙语", + "sqi": "阿尔巴尼亚语", + "srd": "撒丁语", + "srn": "苏里南汤加语", + "srp": "塞尔维亚语", + "srr": "塞雷尔语", + "ssw": "斯瓦特语", + "suk": "苏库马语", + "sun": "巽他语", + "sus": "苏苏语", + "sux": "苏美尔语", + "swa": "斯瓦希里语族", + "swe": "瑞典语", + "syc": "叙利亚语(古典)", + "syr": "古叙利亚语", + "tah": "塔希提语", + "tam": "泰米尔语", + "tat": "塔塔尔语", + "tel": "泰卢固语", + "tem": "滕内语", + "ter": "特列纳语", + "tet": "特塔姆语", + "tgk": "塔吉克语", + "tgl": "塔加洛语", + "tha": "泰语", + "tig": "提格雷语", + "tir": "提格里尼亚语", + "tiv": "蒂夫语", + "tkl": "托克劳语", + "tlh": "克林贡语", + "tli": "特林吉特语", + "tmh": "塔马舍克语", + "tog": "汤加语 (尼亚萨)", + "ton": "汤加语(汤加岛)", + "tpi": "托克皮辛语", + "tsi": "钦西安语", + "tsn": "茨瓦纳语", + "tso": "聪加语", + "tuk": "土库曼语", + "tum": "奇图姆布卡语", + "tur": "土耳其语", + "tvl": "图瓦卢语", + "twi": "契维语", + "tyv": "图瓦语", + "udm": "乌德穆尔特语", + "uga": "乌加里特语", + "uig": "维吾尔语", + "ukr": "乌克兰语", + "umb": "翁本杜语", + "und": "未确定的语言", + "urd": "乌尔都语", + "uzb": "乌兹别克语", + "vai": "瓦伊语", + "ven": "文达语", + "vie": "越南语", + "vol": "沃拉普克语", + "vot": "沃提克语", + "wal": "瓦拉莫语", + "war": "瓦赖语(菲律宾)", + "was": "瓦肖语", + "wln": "瓦龙语", + "wol": "沃洛夫语", + "xal": "卡尔梅克语", + "xho": "科萨语", + "yao": "瑶语", + "yap": "雅浦语", + "yid": "依地语", + "yor": "约鲁巴语", + "zap": "萨波特克语", + "zbl": "布利斯符号", + "zen": "哲纳加语", + "zha": "壮语", + "zho": "中文", + "zul": "祖鲁语", + "zun": "祖尼语", "zxx": "No linguistic content", - "zza": "Zaza" + "zza": "扎扎其语" }, "en": { "aar": "Afar", diff --git a/cps/jinjia.py b/cps/jinjia.py index 688d1fba..f37dfb49 100644 --- a/cps/jinjia.py +++ b/cps/jinjia.py @@ -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 diff --git a/cps/kobo.py b/cps/kobo.py index a9c0f936..8988ef3f 100644 --- a/cps/kobo.py +++ b/cps/kobo.py @@ -42,7 +42,7 @@ from flask import ( from flask_login import current_user from werkzeug.datastructures import Headers from sqlalchemy import func -from sqlalchemy.sql.expression import and_, or_ +from sqlalchemy.sql.expression import and_ from sqlalchemy.exc import StatementError import requests @@ -56,6 +56,8 @@ KOBO_FORMATS = {"KEPUB": ["KEPUB"], "EPUB": ["EPUB3", "EPUB"]} KOBO_STOREAPI_URL = "https://storeapi.kobo.com" KOBO_IMAGEHOST_URL = "https://kbimages1-a.akamaihd.net" +SYNC_ITEM_LIMIT = 100 + kobo = Blueprint("kobo", __name__, url_prefix="/kobo/") kobo_auth.disable_failed_auth_redirect_for_blueprint(kobo) kobo_auth.register_url_value_preprocessor(kobo) @@ -142,68 +144,84 @@ def HandleSyncRequest(): new_books_last_modified = sync_token.books_last_modified new_books_last_created = sync_token.books_last_created new_reading_state_last_modified = sync_token.reading_state_last_modified + new_archived_last_modified = datetime.datetime.min sync_results = [] # We reload the book database so that the user get's a fresh view of the library # in case of external changes (e.g: adding a book through Calibre). calibre_db.reconnect_db(config, ub.app_DB_path) - archived_books = ( - ub.session.query(ub.ArchivedBook) - .filter(ub.ArchivedBook.user_id == int(current_user.id)) - .all() - ) - - # We join-in books that have had their Archived bit recently modified in order to either: - # * Restore them to the user's device. - # * Delete them from the user's device. - # (Ideally we would use a join for this logic, however cross-database joins don't look trivial in SqlAlchemy.) - recently_restored_or_archived_books = [] - archived_book_ids = {} - new_archived_last_modified = datetime.datetime.min - for archived_book in archived_books: - if archived_book.last_modified > sync_token.archive_last_modified: - recently_restored_or_archived_books.append(archived_book.book_id) - if archived_book.is_archived: - archived_book_ids[archived_book.book_id] = True - new_archived_last_modified = max( - new_archived_last_modified, archived_book.last_modified) - - # sqlite gives unexpected results when performing the last_modified comparison without the datetime cast. - # It looks like it's treating the db.Books.last_modified field as a string and may fail - # the comparison because of the +00:00 suffix. - changed_entries = ( - calibre_db.session.query(db.Books) - .join(db.Data) - .filter(or_(func.datetime(db.Books.last_modified) > sync_token.books_last_modified, - db.Books.id.in_(recently_restored_or_archived_books))) - .filter(db.Data.format.in_(KOBO_FORMATS)) - .all() - ) + 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) + ) + 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) + ) reading_states_in_new_entitlements = [] for book in changed_entries: - kobo_reading_state = get_or_create_reading_state(book.id) + 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.name) + + kobo_reading_state = get_or_create_reading_state(book.Books.id) entitlement = { - "BookEntitlement": create_book_entitlement(book, archived=(book.id in archived_book_ids)), - "BookMetadata": get_metadata(book), + "BookEntitlement": create_book_entitlement(book.Books, archived=(book.is_archived == True)), + "BookMetadata": get_metadata(book.Books), } if kobo_reading_state.last_modified > sync_token.reading_state_last_modified: - entitlement["ReadingState"] = get_kobo_reading_state_response(book, kobo_reading_state) + entitlement["ReadingState"] = get_kobo_reading_state_response(book.Books, kobo_reading_state) new_reading_state_last_modified = max(new_reading_state_last_modified, kobo_reading_state.last_modified) - reading_states_in_new_entitlements.append(book.id) + reading_states_in_new_entitlements.append(book.Books.id) - if book.timestamp > sync_token.books_last_created: + if book.Books.timestamp > sync_token.books_last_created: sync_results.append({"NewEntitlement": entitlement}) else: sync_results.append({"ChangedEntitlement": entitlement}) new_books_last_modified = max( - book.last_modified, new_books_last_modified + book.Books.last_modified, new_books_last_modified ) - new_books_last_created = max(book.timestamp, new_books_last_created) + new_books_last_created = max(book.Books.timestamp, new_books_last_created) + 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 + else: + max_change = 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 + else: + books_last_id = -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, @@ -225,11 +243,12 @@ def HandleSyncRequest(): sync_token.books_last_modified = new_books_last_modified sync_token.archive_last_modified = new_archived_last_modified sync_token.reading_state_last_modified = new_reading_state_last_modified + sync_token.books_last_id = books_last_id - return generate_sync_response(sync_token, sync_results) + return generate_sync_response(sync_token, sync_results, book_count) -def generate_sync_response(sync_token, sync_results): +def generate_sync_response(sync_token, sync_results, set_cont=False): extra_headers = {} if config.config_kobo_proxy: # Merge in sync results from the official Kobo store. @@ -243,8 +262,10 @@ def generate_sync_response(sync_token, sync_results): 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) response = make_response(jsonify(sync_results), extra_headers) @@ -284,7 +305,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, @@ -443,8 +465,7 @@ def HandleTagCreate(): items_unknown_to_calibre = add_items_to_shelf(items, shelf) if items_unknown_to_calibre: log.debug("Received request to add unknown books to a collection. Silently ignoring items.") - ub.session.commit() - + ub.session_commit() return make_response(jsonify(str(shelf.uuid)), 201) @@ -476,7 +497,7 @@ def HandleTagUpdate(tag_id): shelf.name = name ub.session.merge(shelf) - ub.session.commit() + ub.session_commit() return make_response(' ', 200) @@ -528,8 +549,7 @@ def HandleTagAddItem(tag_id): log.debug("Received request to add an unknown book to a collection. Silently ignoring item.") ub.session.merge(shelf) - ub.session.commit() - + ub.session_commit() return make_response('', 201) @@ -569,7 +589,7 @@ def HandleTagRemoveItem(tag_id): shelf.books.filter(ub.BookShelf.book_id == book.id).delete() except KeyError: items_unknown_to_calibre.append(item) - ub.session.commit() + ub.session_commit() if items_unknown_to_calibre: log.debug("Received request to remove an unknown book to a collecition. Silently ignoring item.") @@ -615,7 +635,7 @@ def sync_shelves(sync_token, sync_results): "ChangedTag": tag }) sync_token.tags_last_modified = new_tags_last_modified - ub.session.commit() + ub.session_commit() # Creates a Kobo "Tag" object from a ub.Shelf object @@ -696,7 +716,7 @@ def HandleStateRequest(book_uuid): abort(400, description="Malformed request data is missing 'ReadingStates' key") ub.session.merge(kobo_reading_state) - ub.session.commit() + ub.session_commit() return jsonify({ "RequestResult": "Success", "UpdateResults": [update_results_response], @@ -734,7 +754,7 @@ def get_or_create_reading_state(book_id): kobo_reading_state.statistics = ub.KoboStatistics() book_read.kobo_reading_state = kobo_reading_state ub.session.add(book_read) - ub.session.commit() + ub.session_commit() return book_read.kobo_reading_state @@ -837,8 +857,7 @@ def HandleBookDeletionRequest(book_uuid): archived_book.last_modified = datetime.datetime.utcnow() ub.session.merge(archived_book) - ub.session.commit() - + ub.session_commit() return ("", 204) @@ -874,17 +893,6 @@ def HandleProductsRequest(dummy=None): return redirect_or_proxy_request() -'''@kobo.errorhandler(404) -def handle_404(err): - # This handler acts as a catch-all for endpoints that we don't have an interest in - # implementing (e.g: v1/analytics/gettests, v1/user/recommendations, etc) - if err: - print('404') - return jsonify(error=str(err)), 404 - log.debug("Unknown Request received: %s, method: %s, data: %s", request.base_url, request.method, request.data) - return redirect_or_proxy_request()''' - - def make_calibre_web_auth_response(): # As described in kobo_auth.py, CalibreWeb doesn't make use practical use of this auth/device API call for # authentation (nor for authorization). We return a dummy response just to keep the device happy. @@ -911,7 +919,7 @@ def HandleAuthRequest(): if config.config_kobo_proxy: try: return redirect_or_proxy_request() - except: + except Exception: log.error("Failed to receive or parse response from Kobo's auth endpoint. Falling back to un-proxied mode.") return make_calibre_web_auth_response() @@ -928,7 +936,7 @@ def HandleInitRequest(): store_response_json = store_response.json() if "Resources" in store_response_json: kobo_resources = store_response_json["Resources"] - except: + except Exception: log.error("Failed to receive or parse response from Kobo's init endpoint. Falling back to un-proxied mode.") if not kobo_resources: kobo_resources = NATIVE_KOBO_RESOURCES() @@ -989,7 +997,6 @@ def HandleInitRequest(): @requires_kobo_auth @download_required def download_book(book_id, book_format): - return get_download_link(book_id, book_format, "kobo") diff --git a/cps/kobo_auth.py b/cps/kobo_auth.py index 0f6cd174..a51095c8 100644 --- a/cps/kobo_auth.py +++ b/cps/kobo_auth.py @@ -64,11 +64,11 @@ from datetime import datetime from os import urandom from flask import g, Blueprint, url_for, abort, request -from flask_login import login_user, login_required +from flask_login import login_user, current_user, login_required from flask_babel import gettext as _ -from . import logger, ub, lm -from .web import render_title_template +from . import logger, config, calibre_db, db, helper, ub, lm +from .render_template import render_title_template try: from functools import wraps @@ -81,6 +81,7 @@ log = logger.create() def register_url_value_preprocessor(kobo): @kobo.url_value_preprocessor + # pylint: disable=unused-variable def pop_auth_token(__, values): g.auth_token = values.pop("auth_token") @@ -147,7 +148,15 @@ def generate_auth_token(user_id): auth_token.token_type = 1 ub.session.add(auth_token) - ub.session.commit() + ub.session_commit() + + books = calibre_db.session.query(db.Books).join(db.Data).all() + + 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.name) + return render_title_template( "generate_kobo_auth_url.html", title=_(u"Kobo Setup"), @@ -164,5 +173,5 @@ def delete_auth_token(user_id): # Invalidate any prevously generated Kobo Auth token for this user. ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.user_id == user_id)\ .filter(ub.RemoteAuthToken.token_type==1).delete() - ub.session.commit() - return "" + + return ub.session_commit() diff --git a/cps/logger.py b/cps/logger.py index 7cc0f4d9..e2747f53 100644 --- a/cps/logger.py +++ b/cps/logger.py @@ -41,10 +41,37 @@ logging.addLevelName(logging.WARNING, "WARN") logging.addLevelName(logging.CRITICAL, "CRIT") +class _Logger(logging.Logger): + + def debug_or_exception(self, message, *args, **kwargs): + if sys.version_info > (3, 7): + if is_debug_enabled(): + self.exception(message, stacklevel=2, *args, **kwargs) + else: + self.error(message, stacklevel=2, *args, **kwargs) + elif sys.version_info > (3, 0): + if is_debug_enabled(): + self.exception(message, stack_info=True, *args, **kwargs) + else: + self.error(message, *args, **kwargs) + else: + if is_debug_enabled(): + self.exception(message, *args, **kwargs) + else: + self.error(message, *args, **kwargs) + + + def debug_no_auth(self, message, *args, **kwargs): + message = message.strip("\r\n") + if message.startswith("send: AUTH"): + self.debug(message[:16], *args, **kwargs) + else: + self.debug(message, *args, **kwargs) + + def get(name=None): return logging.getLogger(name) - def create(): parent_frame = inspect.stack(0)[1] if hasattr(parent_frame, 'frame'): @@ -54,7 +81,6 @@ def create(): parent_module = inspect.getmodule(parent_frame) return get(parent_module.__name__) - def is_debug_enabled(): return logging.root.level <= logging.DEBUG @@ -99,6 +125,7 @@ def setup(log_file, log_level=None): May be called multiple times. ''' log_level = log_level or DEFAULT_LOG_LEVEL + logging.setLoggerClass(_Logger) logging.getLogger(__package__).setLevel(log_level) r = logging.root @@ -126,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/oauth.py b/cps/oauth.py index 67ef2703..a8995180 100644 --- a/cps/oauth.py +++ b/cps/oauth.py @@ -19,7 +19,6 @@ from __future__ import division, print_function, unicode_literals from flask import session - try: from flask_dance.consumer.backend.sqla import SQLAlchemyBackend, first, _get_real_user from sqlalchemy.orm.exc import NoResultFound @@ -34,134 +33,131 @@ except ImportError: except ImportError: pass -try: - class OAuthBackend(SQLAlchemyBackend): - """ - Stores and retrieves OAuth tokens using a relational database through - the `SQLAlchemy`_ ORM. - .. _SQLAlchemy: https://www.sqlalchemy.org/ - """ - def __init__(self, model, session, provider_id, - user=None, user_id=None, user_required=None, anon_user=None, - cache=None): - self.provider_id = provider_id - super(OAuthBackend, self).__init__(model, session, user, user_id, user_required, anon_user, cache) +class OAuthBackend(SQLAlchemyBackend): + """ + Stores and retrieves OAuth tokens using a relational database through + the `SQLAlchemy`_ ORM. - def get(self, blueprint, user=None, user_id=None): - if self.provider_id + '_oauth_token' in session and session[self.provider_id + '_oauth_token'] != '': - return session[self.provider_id + '_oauth_token'] - # check cache - cache_key = self.make_cache_key(blueprint=blueprint, user=user, user_id=user_id) - token = self.cache.get(cache_key) - if token: - return token - - # if not cached, make database queries - query = ( - self.session.query(self.model) - .filter_by(provider=self.provider_id) - ) - uid = first([user_id, self.user_id, blueprint.config.get("user_id")]) - u = first(_get_real_user(ref, self.anon_user) - for ref in (user, self.user, blueprint.config.get("user"))) - - use_provider_user_id = False - if self.provider_id + '_oauth_user_id' in session and session[self.provider_id + '_oauth_user_id'] != '': - query = query.filter_by(provider_user_id=session[self.provider_id + '_oauth_user_id']) - use_provider_user_id = True - - if self.user_required and not u and not uid and not use_provider_user_id: - # raise ValueError("Cannot get OAuth token without an associated user") - return None - # check for user ID - if hasattr(self.model, "user_id") and uid: - query = query.filter_by(user_id=uid) - # check for user (relationship property) - elif hasattr(self.model, "user") and u: - query = query.filter_by(user=u) - # if we have the property, but not value, filter by None - elif hasattr(self.model, "user_id"): - query = query.filter_by(user_id=None) - # run query - try: - token = query.one().token - except NoResultFound: - token = None - - # cache the result - self.cache.set(cache_key, token) + .. _SQLAlchemy: https://www.sqlalchemy.org/ + """ + def __init__(self, model, session, provider_id, + user=None, user_id=None, user_required=None, anon_user=None, + cache=None): + self.provider_id = provider_id + super(OAuthBackend, self).__init__(model, session, user, user_id, user_required, anon_user, cache) + def get(self, blueprint, user=None, user_id=None): + if self.provider_id + '_oauth_token' in session and session[self.provider_id + '_oauth_token'] != '': + return session[self.provider_id + '_oauth_token'] + # check cache + cache_key = self.make_cache_key(blueprint=blueprint, user=user, user_id=user_id) + token = self.cache.get(cache_key) + if token: return token - def set(self, blueprint, token, user=None, user_id=None): - uid = first([user_id, self.user_id, blueprint.config.get("user_id")]) - u = first(_get_real_user(ref, self.anon_user) - for ref in (user, self.user, blueprint.config.get("user"))) + # if not cached, make database queries + query = ( + self.session.query(self.model) + .filter_by(provider=self.provider_id) + ) + uid = first([user_id, self.user_id, blueprint.config.get("user_id")]) + u = first(_get_real_user(ref, self.anon_user) + for ref in (user, self.user, blueprint.config.get("user"))) - if self.user_required and not u and not uid: - raise ValueError("Cannot set OAuth token without an associated user") + use_provider_user_id = False + if self.provider_id + '_oauth_user_id' in session and session[self.provider_id + '_oauth_user_id'] != '': + query = query.filter_by(provider_user_id=session[self.provider_id + '_oauth_user_id']) + use_provider_user_id = True - # if there was an existing model, delete it - existing_query = ( - self.session.query(self.model) - .filter_by(provider=self.provider_id) - ) - # check for user ID - has_user_id = hasattr(self.model, "user_id") - if has_user_id and uid: - existing_query = existing_query.filter_by(user_id=uid) - # check for user (relationship property) - has_user = hasattr(self.model, "user") - if has_user and u: - existing_query = existing_query.filter_by(user=u) - # queue up delete query -- won't be run until commit() - existing_query.delete() - # create a new model for this token - kwargs = { - "provider": self.provider_id, - "token": token, - } - if has_user_id and uid: - kwargs["user_id"] = uid - if has_user and u: - kwargs["user"] = u - self.session.add(self.model(**kwargs)) - # commit to delete and add simultaneously - self.session.commit() - # invalidate cache - self.cache.delete(self.make_cache_key( - blueprint=blueprint, user=user, user_id=user_id - )) + if self.user_required and not u and not uid and not use_provider_user_id: + # raise ValueError("Cannot get OAuth token without an associated user") + return None + # check for user ID + if hasattr(self.model, "user_id") and uid: + query = query.filter_by(user_id=uid) + # check for user (relationship property) + elif hasattr(self.model, "user") and u: + query = query.filter_by(user=u) + # if we have the property, but not value, filter by None + elif hasattr(self.model, "user_id"): + query = query.filter_by(user_id=None) + # run query + try: + token = query.one().token + except NoResultFound: + token = None - def delete(self, blueprint, user=None, user_id=None): - query = ( - self.session.query(self.model) - .filter_by(provider=self.provider_id) - ) - uid = first([user_id, self.user_id, blueprint.config.get("user_id")]) - u = first(_get_real_user(ref, self.anon_user) - for ref in (user, self.user, blueprint.config.get("user"))) + # cache the result + self.cache.set(cache_key, token) - if self.user_required and not u and not uid: - raise ValueError("Cannot delete OAuth token without an associated user") + return token - # check for user ID - if hasattr(self.model, "user_id") and uid: - query = query.filter_by(user_id=uid) - # check for user (relationship property) - elif hasattr(self.model, "user") and u: - query = query.filter_by(user=u) - # if we have the property, but not value, filter by None - elif hasattr(self.model, "user_id"): - query = query.filter_by(user_id=None) - # run query - query.delete() - self.session.commit() - # invalidate cache - self.cache.delete(self.make_cache_key( - blueprint=blueprint, user=user, user_id=user_id, - )) + def set(self, blueprint, token, user=None, user_id=None): + uid = first([user_id, self.user_id, blueprint.config.get("user_id")]) + u = first(_get_real_user(ref, self.anon_user) + for ref in (user, self.user, blueprint.config.get("user"))) -except Exception: - pass + if self.user_required and not u and not uid: + raise ValueError("Cannot set OAuth token without an associated user") + + # if there was an existing model, delete it + existing_query = ( + self.session.query(self.model) + .filter_by(provider=self.provider_id) + ) + # check for user ID + has_user_id = hasattr(self.model, "user_id") + if has_user_id and uid: + existing_query = existing_query.filter_by(user_id=uid) + # check for user (relationship property) + has_user = hasattr(self.model, "user") + if has_user and u: + existing_query = existing_query.filter_by(user=u) + # queue up delete query -- won't be run until commit() + existing_query.delete() + # create a new model for this token + kwargs = { + "provider": self.provider_id, + "token": token, + } + if has_user_id and uid: + kwargs["user_id"] = uid + if has_user and u: + kwargs["user"] = u + self.session.add(self.model(**kwargs)) + # commit to delete and add simultaneously + self.session.commit() + # invalidate cache + self.cache.delete(self.make_cache_key( + blueprint=blueprint, user=user, user_id=user_id + )) + + def delete(self, blueprint, user=None, user_id=None): + query = ( + self.session.query(self.model) + .filter_by(provider=self.provider_id) + ) + uid = first([user_id, self.user_id, blueprint.config.get("user_id")]) + u = first(_get_real_user(ref, self.anon_user) + for ref in (user, self.user, blueprint.config.get("user"))) + + if self.user_required and not u and not uid: + raise ValueError("Cannot delete OAuth token without an associated user") + + # check for user ID + if hasattr(self.model, "user_id") and uid: + query = query.filter_by(user_id=uid) + # check for user (relationship property) + elif hasattr(self.model, "user") and u: + query = query.filter_by(user=u) + # if we have the property, but not value, filter by None + elif hasattr(self.model, "user_id"): + query = query.filter_by(user_id=None) + # run query + query.delete() + self.session.commit() + # invalidate cache + self.cache.delete(self.make_cache_key( + blueprint=blueprint, user=user, user_id=user_id, + )) diff --git a/cps/oauth_bb.py b/cps/oauth_bb.py index 4d489cdd..c8cc2e3e 100644 --- a/cps/oauth_bb.py +++ b/cps/oauth_bb.py @@ -30,15 +30,20 @@ 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 flask_login import login_user, current_user +from oauthlib.oauth2 import TokenExpiredError, InvalidGrantError +from flask_login import login_user, current_user, login_required from sqlalchemy.orm.exc import NoResultFound from . import constants, logger, config, app, ub -from .web import login_required -from .oauth import OAuthBackend, backend_resultcode + +try: + from .oauth import OAuthBackend, backend_resultcode +except NameError: + pass oauth_check = {} +oauthblueprints = [] oauth = Blueprint('oauth', __name__) log = logger.create() @@ -84,11 +89,7 @@ def register_user_with_oauth(user=None): except NoResultFound: # no found, return error return - try: - ub.session.commit() - except Exception as e: - log.exception(e) - ub.session.rollback() + ub.session_commit("User {} with OAuth for provider {} registered".format(user.name, oauth_key)) def logout_oauth_user(): @@ -97,19 +98,122 @@ def logout_oauth_user(): session.pop(str(oauth_key) + '_oauth_user_id') -if ub.oauth_support: - oauthblueprints = [] +def oauth_update_token(provider_id, token, provider_user_id): + session[provider_id + "_oauth_user_id"] = provider_user_id + session[provider_id + "_oauth_token"] = token + + # Find this OAuth token in the database, or create it + query = ub.session.query(ub.OAuth).filter_by( + provider=provider_id, + provider_user_id=provider_user_id, + ) + try: + oauth_entry = query.one() + # update token + oauth_entry.token = token + except NoResultFound: + oauth_entry = ub.OAuth( + provider=provider_id, + provider_user_id=provider_user_id, + token=token, + ) + ub.session.add(oauth_entry) + ub.session_commit() + + # Disable Flask-Dance's default behavior for saving the OAuth token + # Value differrs depending on flask-dance version + return backend_resultcode + + +def bind_oauth_or_register(provider_id, provider_user_id, redirect_url, provider_name): + query = ub.session.query(ub.OAuth).filter_by( + provider=provider_id, + provider_user_id=provider_user_id, + ) + try: + oauth_entry = query.first() + # 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.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: + # bind to current user + if current_user and current_user.is_authenticated: + oauth_entry.user = current_user + try: + 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 ex: + log.debug_or_exception(ex) + ub.session.rollback() + else: + flash(_(u"Login failed, No User Linked With OAuth Account"), category="error") + log.info('Login failed, No User Linked With OAuth Account') + return redirect(url_for('web.login')) + # return redirect(url_for('web.login')) + # if config.config_public_reg: + # return redirect(url_for('web.register')) + # else: + # flash(_(u"Public registration is not enabled"), category="error") + # return redirect(url_for(redirect_url)) + except (NoResultFound, AttributeError): + return redirect(url_for(redirect_url)) + + +def get_oauth_status(): + status = [] + query = ub.session.query(ub.OAuth).filter_by( + user_id=current_user.id, + ) + try: + oauths = query.all() + for oauth_entry in oauths: + status.append(int(oauth_entry.provider)) + return status + except NoResultFound: + return None + + +def unlink_oauth(provider): + if request.host_url + 'me' != request.referrer: + pass + query = ub.session.query(ub.OAuth).filter_by( + provider=provider, + user_id=current_user.id, + ) + try: + oauth_entry = query.one() + if current_user and current_user.is_authenticated: + oauth_entry.user = current_user + try: + ub.session.delete(oauth_entry) + ub.session.commit() + logout_oauth_user() + flash(_(u"Unlink to %(oauth)s Succeeded", oauth=oauth_check[provider]), category="success") + 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: + log.warning("oauth %s for user %d not found", provider, current_user.id) + flash(_(u"Not Linked to %(oauth)s", oauth=provider), category="error") + return redirect(url_for('web.profile')) + +def generate_oauth_blueprints(): if not ub.session.query(ub.OAuthProvider).count(): - oauthProvider = ub.OAuthProvider() - oauthProvider.provider_name = "github" - oauthProvider.active = False - ub.session.add(oauthProvider) - ub.session.commit() - oauthProvider = ub.OAuthProvider() - oauthProvider.provider_name = "google" - oauthProvider.active = False - ub.session.add(oauthProvider) - ub.session.commit() + for provider in ("github", "google"): + oauthProvider = ub.OAuthProvider() + oauthProvider.provider_name = provider + oauthProvider.active = False + ub.session.add(oauthProvider) + ub.session_commit("{} Blueprint Created".format(provider)) oauth_ids = ub.session.query(ub.OAuthProvider).all() ele1 = dict(provider_name='github', @@ -146,17 +250,23 @@ if ub.oauth_support: app.register_blueprint(blueprint, url_prefix="/login") if element['active']: register_oauth_blueprint(element['id'], element['provider_name']) + return oauthblueprints +if ub.oauth_support: + oauthblueprints = generate_oauth_blueprints() + @oauth_authorized.connect_via(oauthblueprints[0]['blueprint']) 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() @@ -168,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() @@ -180,117 +292,6 @@ if ub.oauth_support: return oauth_update_token(str(oauthblueprints[1]['id']), token, google_user_id) - def oauth_update_token(provider_id, token, provider_user_id): - session[provider_id + "_oauth_user_id"] = provider_user_id - session[provider_id + "_oauth_token"] = token - - # Find this OAuth token in the database, or create it - query = ub.session.query(ub.OAuth).filter_by( - provider=provider_id, - provider_user_id=provider_user_id, - ) - try: - oauth_entry = query.one() - # update token - oauth_entry.token = token - except NoResultFound: - oauth_entry = ub.OAuth( - provider=provider_id, - provider_user_id=provider_user_id, - token=token, - ) - try: - ub.session.add(oauth_entry) - ub.session.commit() - except Exception as e: - log.exception(e) - ub.session.rollback() - - # Disable Flask-Dance's default behavior for saving the OAuth token - # Value differrs depending on flask-dance version - return backend_resultcode - - - def bind_oauth_or_register(provider_id, provider_user_id, redirect_url, provider_name): - query = ub.session.query(ub.OAuth).filter_by( - provider=provider_id, - provider_user_id=provider_user_id, - ) - try: - oauth_entry = query.first() - # 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), - category="success") - return redirect(url_for('web.index')) - else: - # bind to current user - if current_user and current_user.is_authenticated: - oauth_entry.user = current_user - try: - ub.session.add(oauth_entry) - ub.session.commit() - flash(_(u"Link to %(oauth)s Succeeded", oauth=provider_name), category="success") - return redirect(url_for('web.profile')) - except Exception as e: - log.exception(e) - ub.session.rollback() - else: - flash(_(u"Login failed, No User Linked With OAuth Account"), category="error") - log.info('Login failed, No User Linked With OAuth Account') - return redirect(url_for('web.login')) - # return redirect(url_for('web.login')) - # if config.config_public_reg: - # return redirect(url_for('web.register')) - # else: - # flash(_(u"Public registration is not enabled"), category="error") - # return redirect(url_for(redirect_url)) - except (NoResultFound, AttributeError): - return redirect(url_for(redirect_url)) - - - def get_oauth_status(): - status = [] - query = ub.session.query(ub.OAuth).filter_by( - user_id=current_user.id, - ) - try: - oauths = query.all() - for oauth_entry in oauths: - status.append(int(oauth_entry.provider)) - return status - except NoResultFound: - return None - - - def unlink_oauth(provider): - if request.host_url + 'me' != request.referrer: - pass - query = ub.session.query(ub.OAuth).filter_by( - provider=provider, - user_id=current_user.id, - ) - try: - oauth_entry = query.one() - if current_user and current_user.is_authenticated: - oauth_entry.user = current_user - try: - ub.session.delete(oauth_entry) - ub.session.commit() - logout_oauth_user() - flash(_(u"Unlink to %(oauth)s Succeeded", oauth=oauth_check[provider]), category="success") - except Exception as e: - log.exception(e) - ub.session.rollback() - flash(_(u"Unlink to %(oauth)s Failed", oauth=oauth_check[provider]), category="error") - except NoResultFound: - log.warning("oauth %s for user %d not found", provider, current_user.id) - flash(_(u"Not Linked to %(oauth)s", oauth=provider), category="error") - return redirect(url_for('web.profile')) - - # notify on OAuth provider error @oauth_error.connect_via(oauthblueprints[0]['blueprint']) def github_error(blueprint, error, error_description=None, error_uri=None): @@ -305,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 = ( @@ -352,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 05e9c68c..e444302a 100644 --- a/cps/opds.py +++ b/cps/opds.py @@ -27,13 +27,14 @@ 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 from .helper import get_download_link, get_book_cover from .pagination import Pagination -from .web import render_read_books, download_required, load_user_from_request +from .web import render_read_books +from .usermanagement import load_user_from_request from flask_babel import gettext as _ from babel import Locale as LC from babel.core import UnknownLocaleError @@ -93,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") @@ -149,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) @@ -200,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) @@ -228,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) @@ -268,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) @@ -427,9 +541,14 @@ 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() - return bool(user and check_password_hash(str(user.password), password)) + if bool(user and check_password_hash(str(user.password), password)): + return True + else: + 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 def authenticate(): diff --git a/cps/remotelogin.py b/cps/remotelogin.py new file mode 100644 index 00000000..47d10c20 --- /dev/null +++ b/cps/remotelogin.py @@ -0,0 +1,138 @@ +# -*- coding: utf-8 -*- + +# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) +# Copyright (C) 2018-2019 OzzieIsaacs, cervinko, jkrehm, bodybybuddha, ok11, +# andy29485, idalin, Kyosfonica, wuqi, Kennyl, lemmsh, +# falgh1, grunjol, csitko, ytils, xybydy, trasba, vrabe, +# ruben-herold, marblepebble, JackED42, SiphonSquirrel, +# apetresc, nanu-c, mutschler +# +# 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 . + +import json +from datetime import datetime + +from flask import Blueprint, request, make_response, abort, url_for, flash, redirect +from flask_login import login_required, current_user, login_user +from flask_babel import gettext as _ +from sqlalchemy.sql.expression import true + +from . import config, logger, ub +from .render_template import render_title_template + +try: + from functools import wraps +except ImportError: + pass # We're not using Python 3 + +remotelogin = Blueprint('remotelogin', __name__) +log = logger.create() + + +def remote_login_required(f): + @wraps(f) + def inner(*args, **kwargs): + if config.config_remote_login: + return f(*args, **kwargs) + if request.headers.get('X-Requested-With') == 'XMLHttpRequest': + data = {'status': 'error', 'message': 'Forbidden'} + response = make_response(json.dumps(data, ensure_ascii=False)) + response.headers["Content-Type"] = "application/json; charset=utf-8" + return response, 403 + abort(403) + + return inner + +@remotelogin.route('/remote/login') +@remote_login_required +def remote_login(): + auth_token = ub.RemoteAuthToken() + ub.session.add(auth_token) + 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, + verify_url=verify_url, page="remotelogin") + + +@remotelogin.route('/verify/') +@remote_login_required +@login_required +def verify_token(token): + auth_token = ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.auth_token == token).first() + + # Token not found + if auth_token is None: + flash(_(u"Token not found"), category="error") + log.error(u"Remote Login token not found") + return redirect(url_for('web.index')) + + # Token expired + elif datetime.now() > auth_token.expiration: + ub.session.delete(auth_token) + ub.session_commit() + + flash(_(u"Token has expired"), category="error") + log.error(u"Remote Login token expired") + return redirect(url_for('web.index')) + + # Update token with user information + auth_token.user_id = current_user.id + auth_token.verified = True + ub.session_commit() + + flash(_(u"Success! Please return to your device"), category="success") + log.debug(u"Remote Login token for userid %s verified", auth_token.user_id) + return redirect(url_for('web.index')) + + +@remotelogin.route('/ajax/verify_token', methods=['POST']) +@remote_login_required +def token_verified(): + token = request.form['token'] + auth_token = ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.auth_token == token).first() + + data = {} + + # Token not found + if auth_token is None: + data['status'] = 'error' + data['message'] = _(u"Token not found") + + # Token expired + elif datetime.now() > auth_token.expiration: + ub.session.delete(auth_token) + ub.session_commit() + + data['status'] = 'error' + data['message'] = _(u"Token has expired") + + elif not auth_token.verified: + data['status'] = 'not_verified' + + else: + user = ub.session.query(ub.User).filter(ub.User.id == auth_token.user_id).first() + login_user(user) + + ub.session.delete(auth_token) + 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.name), category="success") + + response = make_response(json.dumps(data, ensure_ascii=False)) + response.headers["Content-Type"] = "application/json; charset=utf-8" + + return response diff --git a/cps/render_template.py b/cps/render_template.py new file mode 100644 index 00000000..51e4db95 --- /dev/null +++ b/cps/render_template.py @@ -0,0 +1,122 @@ +# -*- coding: utf-8 -*- + +# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) +# Copyright (C) 2018-2020 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 flask import render_template +from flask_babel import gettext as _ +from flask import g +from werkzeug.local import LocalProxy +from flask_login import current_user + +from . import config, constants, ub, logger, db, calibre_db +from .ub import User + + +log = logger.create() + +def get_sidebar_config(kwargs=None): + kwargs = kwargs or [] + if 'content' in kwargs: + content = kwargs['content'] + content = isinstance(content, (User, LocalProxy)) and not content.role_anonymous() + else: + content = 'conf' in kwargs + sidebar = list() + sidebar.append({"glyph": "glyphicon-book", "text": _('Books'), "link": 'web.index', "id": "new", + "visibility": constants.SIDEBAR_RECENT, 'public': True, "page": "root", + "show_text": _('Show recent books'), "config_show":False}) + 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}) + 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", + "show_text": _('Show Top Rated Books'), "config_show": True}) + sidebar.append({"glyph": "glyphicon-eye-open", "text": _('Read Books'), "link": 'web.books_list', "id": "read", + "visibility": constants.SIDEBAR_READ_AND_UNREAD, 'public': (not g.user.is_anonymous), + "page": "read", "show_text": _('Show read and unread'), "config_show": content}) + sidebar.append( + {"glyph": "glyphicon-eye-close", "text": _('Unread Books'), "link": 'web.books_list', "id": "unread", + "visibility": constants.SIDEBAR_READ_AND_UNREAD, 'public': (not g.user.is_anonymous), "page": "unread", + "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}) + 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}) + sidebar.append({"glyph": "glyphicon-bookmark", "text": _('Series'), "link": 'web.series_list', "id": "serie", + "visibility": constants.SIDEBAR_SERIES, 'public': True, "page": "series", + "show_text": _('Show series selection'), "config_show": True}) + sidebar.append({"glyph": "glyphicon-user", "text": _('Authors'), "link": 'web.author_list', "id": "author", + "visibility": constants.SIDEBAR_AUTHOR, 'public': True, "page": "author", + "show_text": _('Show author selection'), "config_show": True}) + sidebar.append( + {"glyph": "glyphicon-text-size", "text": _('Publishers'), "link": 'web.publisher_list', "id": "publisher", + "visibility": constants.SIDEBAR_PUBLISHER, 'public': True, "page": "publisher", + "show_text": _('Show publisher selection'), "config_show":True}) + sidebar.append({"glyph": "glyphicon-flag", "text": _('Languages'), "link": 'web.language_overview', "id": "lang", + "visibility": constants.SIDEBAR_LANGUAGE, 'public': (g.user.filter_language() == 'all'), + "page": "language", + "show_text": _('Show language selection'), "config_show": True}) + sidebar.append({"glyph": "glyphicon-star-empty", "text": _('Ratings'), "link": 'web.ratings_list', "id": "rate", + "visibility": constants.SIDEBAR_RATING, 'public': True, + "page": "rating", "show_text": _('Show ratings selection'), "config_show": True}) + sidebar.append({"glyph": "glyphicon-file", "text": _('File formats'), "link": 'web.formats_list', "id": "format", + "visibility": constants.SIDEBAR_FORMAT, 'public': True, + "page": "format", "show_text": _('Show file formats selection'), "config_show": True}) + sidebar.append( + {"glyph": "glyphicon-trash", "text": _('Archived Books'), "link": 'web.books_list', "id": "archived", + "visibility": constants.SIDEBAR_ARCHIVED, 'public': (not g.user.is_anonymous), "page": "archived", + "show_text": _('Show archived books'), "config_show": content}) + sidebar.append( + {"glyph": "glyphicon-th-list", "text": _('Books List'), "link": 'web.books_table', "id": "list", + "visibility": constants.SIDEBAR_LIST, 'public': (not g.user.is_anonymous), "page": "list", + "show_text": _('Show Books List'), "config_show": content}) + + return sidebar + +def get_readbooks_ids(): + if not config.config_read_column: + readBooks = ub.session.query(ub.ReadBook).filter(ub.ReadBook.user_id == int(current_user.id))\ + .filter(ub.ReadBook.read_status == ub.ReadBook.STATUS_FINISHED).all() + return frozenset([x.book_id for x in readBooks]) + else: + try: + readBooks = calibre_db.session.query(db.cc_classes[config.config_read_column])\ + .filter(db.cc_classes[config.config_read_column].value == True).all() + return frozenset([x.book for x in readBooks]) + except (KeyError, AttributeError): + log.error("Custom Column No.%d is not existing in calibre database", config.config_read_column) + return [] + +# Returns the template for rendering and includes the instance name +def render_title_template(*args, **kwargs): + sidebar = get_sidebar_config(kwargs) + return render_template(instance=config.config_calibre_web_title, sidebar=sidebar, + accept=constants.EXTENSIONS_UPLOAD, read_book_ids=get_readbooks_ids(), + *args, **kwargs) diff --git a/cps/server.py b/cps/server.py index b2d9c1b0..9b79f77d 100644 --- a/cps/server.py +++ b/cps/server.py @@ -22,6 +22,7 @@ import os import errno import signal import socket +import subprocess # nosec try: from gevent.pywsgi import WSGIServer @@ -136,6 +137,64 @@ class WebServer(object): return sock, _readable_listen_address(*address) + @staticmethod + def _get_args_for_reloading(): + """Determine how the script was executed, and return the args needed + to execute it again in a new process. + Code from https://github.com/pyload/pyload. Author GammaC0de, voulter + """ + rv = [sys.executable] + py_script = sys.argv[0] + args = sys.argv[1:] + # Need to look at main module to determine how it was executed. + __main__ = sys.modules["__main__"] + + # The value of __package__ indicates how Python was called. It may + # not exist if a setuptools script is installed as an egg. It may be + # set incorrectly for entry points created with pip on Windows. + if getattr(__main__, "__package__", None) is None or ( + os.name == "nt" + and __main__.__package__ == "" + and not os.path.exists(py_script) + and os.path.exists("{}.exe".format(py_script)) + ): + # Executed a file, like "python app.py". + py_script = os.path.abspath(py_script) + + if os.name == "nt": + # Windows entry points have ".exe" extension and should be + # called directly. + if not os.path.exists(py_script) and os.path.exists("{}.exe".format(py_script)): + py_script += ".exe" + + if ( + os.path.splitext(sys.executable)[1] == ".exe" + and os.path.splitext(py_script)[1] == ".exe" + ): + rv.pop(0) + + rv.append(py_script) + else: + # Executed a module, like "python -m module". + if sys.argv[0] == "-m": + args = sys.argv + else: + if os.path.isfile(py_script): + # Rewritten by Python from "-m script" to "/path/to/script.py". + py_module = __main__.__package__ + name = os.path.splitext(os.path.basename(py_script))[0] + + if name != "__main__": + py_module += ".{}".format(name) + else: + # Incorrectly rewritten by pydevd debugger from "-m script" to "script". + py_module = py_script + + rv.extend(("-m", py_module.lstrip("."))) + + rv.extend(args) + return rv + def _start_gevent(self): ssl_args = self.ssl_args or {} @@ -192,18 +251,16 @@ class WebServer(object): finally: self.wsgiserver = None + # prevent irritating log of pending tasks message from asyncio + logger.get('asyncio').setLevel(logger.logging.CRITICAL) + if not self.restart: log.info("Performing shutdown of Calibre-Web") - # prevent irritiating log of pending tasks message from asyncio - logger.get('asyncio').setLevel(logger.logging.CRITICAL) return True log.info("Performing restart of Calibre-Web") - arguments = list(sys.argv) - arguments.insert(0, sys.executable) - if os.name == 'nt': - arguments = ["\"%s\"" % a for a in arguments] - os.execv(sys.executable, arguments) + args = self._get_args_for_reloading() + subprocess.call(args, close_fds=True) # nosec return True def _killServer(self, __, ___): diff --git a/cps/services/SyncToken.py b/cps/services/SyncToken.py index f6db960b..b54d8d95 100644 --- a/cps/services/SyncToken.py +++ b/cps/services/SyncToken.py @@ -22,6 +22,7 @@ from base64 import b64decode, b64encode from jsonschema import validate, exceptions, __version__ from datetime import datetime try: + # pylint: disable=unused-import from urllib import unquote except ImportError: from urllib.parse import unquote @@ -64,7 +65,7 @@ class SyncToken: books_last_modified: Datetime representing the last modified book that the device knows about. """ - SYNC_TOKEN_HEADER = "x-kobo-synctoken" + SYNC_TOKEN_HEADER = "x-kobo-synctoken" # nosec VERSION = "1-1-0" LAST_MODIFIED_ADDED_VERSION = "1-1-0" MIN_VERSION = "1-0-0" @@ -85,6 +86,7 @@ class SyncToken: "archive_last_modified": {"type": "string"}, "reading_state_last_modified": {"type": "string"}, "tags_last_modified": {"type": "string"}, + "books_last_id": {"type": "integer", "optional": True} }, } @@ -96,18 +98,20 @@ class SyncToken: archive_last_modified=datetime.min, reading_state_last_modified=datetime.min, tags_last_modified=datetime.min, - ): + books_last_id=-1 + ): # nosec self.raw_kobo_store_token = raw_kobo_store_token self.books_last_created = books_last_created self.books_last_modified = books_last_modified self.archive_last_modified = archive_last_modified self.reading_state_last_modified = reading_state_last_modified self.tags_last_modified = tags_last_modified + self.books_last_id = books_last_id @staticmethod def from_headers(headers): sync_token_header = headers.get(SyncToken.SYNC_TOKEN_HEADER, "") - if sync_token_header == "": + if sync_token_header == "": # nosec return SyncToken() # On the first sync from a Kobo device, we may receive the SyncToken @@ -137,9 +141,12 @@ class SyncToken: archive_last_modified = get_datetime_from_json(data_json, "archive_last_modified") reading_state_last_modified = get_datetime_from_json(data_json, "reading_state_last_modified") tags_last_modified = get_datetime_from_json(data_json, "tags_last_modified") + books_last_id = data_json["books_last_id"] except TypeError: log.error("SyncToken timestamps don't parse to a datetime.") return SyncToken(raw_kobo_store_token=raw_kobo_store_token) + except KeyError: + books_last_id = -1 return SyncToken( raw_kobo_store_token=raw_kobo_store_token, @@ -147,7 +154,8 @@ class SyncToken: books_last_modified=books_last_modified, archive_last_modified=archive_last_modified, reading_state_last_modified=reading_state_last_modified, - tags_last_modified=tags_last_modified + tags_last_modified=tags_last_modified, + books_last_id=books_last_id ) def set_kobo_store_header(self, store_headers): @@ -170,7 +178,8 @@ class SyncToken: "books_last_created": to_epoch_timestamp(self.books_last_created), "archive_last_modified": to_epoch_timestamp(self.archive_last_modified), "reading_state_last_modified": to_epoch_timestamp(self.reading_state_last_modified), - "tags_last_modified": to_epoch_timestamp(self.tags_last_modified) + "tags_last_modified": to_epoch_timestamp(self.tags_last_modified), + "books_last_id":self.books_last_id }, } return b64encode_json(token) 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..9380121a --- /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 email 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("Email send successfully via Gmail") diff --git a/cps/services/worker.py b/cps/services/worker.py index c2ea594c..1baf25fe 100644 --- a/cps/services/worker.py +++ b/cps/services/worker.py @@ -45,7 +45,7 @@ class ImprovedQueue(queue.Queue): with self.mutex: return list(self.queue) -#Class for all worker tasks in the background +# Class for all worker tasks in the background class WorkerThread(threading.Thread): _instance = None @@ -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, @@ -110,7 +111,7 @@ class WorkerThread(threading.Thread): # We don't use a daemon here because we don't want the tasks to just be abruptly halted, leading to # possible file / database corruption item = self.queue.get(timeout=1) - except queue.Empty as ex: + except queue.Empty: time.sleep(1) continue @@ -159,9 +160,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.exception(e) + except Exception as ex: + self._handleError(str(ex)) + log.debug_or_exception(ex) self.end_time = datetime.now() @@ -210,7 +211,6 @@ class CalibreTask: self._progress = x def _handleError(self, error_message): - log.exception(error_message) self.stat = STAT_FAIL self.progress = 1 self.error = error_message diff --git a/cps/shelf.py b/cps/shelf.py index ea7f1eeb..a58e6d5c 100644 --- a/cps/shelf.py +++ b/cps/shelf.py @@ -22,15 +22,17 @@ from __future__ import division, print_function, unicode_literals from datetime import datetime +import sys from flask import Blueprint, request, flash, redirect, url_for from flask_babel import gettext as _ from flask_login import login_required, current_user -from sqlalchemy.sql.expression import func +from sqlalchemy.sql.expression import func, true from sqlalchemy.exc import OperationalError, InvalidRequestError -from . import logger, ub, calibre_db -from .web import login_required_if_no_ano, render_title_template +from . import logger, ub, calibre_db, db +from .render_template import render_title_template +from .usermanagement import login_required_if_no_ano shelf = Blueprint('shelf', __name__) @@ -97,12 +99,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"]) @@ -121,6 +125,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')) @@ -138,18 +143,14 @@ 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) + 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')) - maxOrder = ub.session.query(func.max(ub.BookShelf.order)).filter(ub.BookShelf.shelf == shelf_id).first() - if maxOrder[0] is None: - maxOrder = 0 - else: - maxOrder = maxOrder[0] + maxOrder = ub.session.query(func.max(ub.BookShelf.order)).filter(ub.BookShelf.shelf == shelf_id).first()[0] or 0 for book in books_for_shelf: - maxOrder = maxOrder + 1 + maxOrder += 1 shelf.books.append(ub.BookShelf(shelf=shelf.id, book_id=book, order=maxOrder)) shelf.last_modified = datetime.utcnow() try: @@ -158,8 +159,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')) @@ -170,7 +173,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 @@ -199,7 +202,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: @@ -213,6 +217,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')) @@ -223,96 +228,79 @@ def remove_from_shelf(shelf_id, book_id): @login_required def create_shelf(): shelf = ub.Shelf() - if request.method == "POST": - to_save = request.form.to_dict() - if "is_public" in to_save: - shelf.is_public = 1 - shelf.name = to_save["title"] - shelf.user_id = int(current_user.id) - - is_shelf_name_unique = False - 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)) \ - .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"]), - category="error") - else: - is_shelf_name_unique = ub.session.query(ub.Shelf) \ - .filter((ub.Shelf.name == to_save["title"]) & (ub.Shelf.is_public == 0) & - (ub.Shelf.user_id == int(current_user.id)))\ - .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"]), - category="error") - - if is_shelf_name_unique: - try: - ub.session.add(shelf) - ub.session.commit() - flash(_(u"Shelf %(title)s created", title=to_save["title"]), category="success") - return redirect(url_for('shelf.show_shelf', shelf_id=shelf.id)) - except (OperationalError, InvalidRequestError): - ub.session.rollback() - flash(_(u"Settings DB is not Writeable"), category="error") - except Exception: - ub.session.rollback() - flash(_(u"There was an error"), category="error") - return render_title_template('shelf_edit.html', shelf=shelf, title=_(u"Create a Shelf"), page="shelfcreate") - else: - return render_title_template('shelf_edit.html', shelf=shelf, title=_(u"Create a Shelf"), page="shelfcreate") + return create_edit_shelf(shelf, 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 shelf ID is set, we are editing a shelf +def create_edit_shelf(shelf, title, page, shelf_id=False): if request.method == "POST": to_save = request.form.to_dict() - - is_shelf_name_unique = False - 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.id != shelf_id) \ - .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"]), - category="error") + if "is_public" in to_save: + shelf.is_public = 1 else: - is_shelf_name_unique = ub.session.query(ub.Shelf) \ - .filter((ub.Shelf.name == to_save["title"]) & (ub.Shelf.is_public == 0) & - (ub.Shelf.user_id == int(current_user.id)))\ - .filter(ub.Shelf.id != shelf_id)\ - .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"]), - category="error") - - if is_shelf_name_unique: + shelf.is_public = 0 + if check_shelf_is_unique(shelf, to_save, shelf_id): shelf.name = to_save["title"] - shelf.last_modified = datetime.utcnow() - if "is_public" in to_save: - shelf.is_public = 1 + # shelf.last_modified = datetime.utcnow() + 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"]) else: - shelf.is_public = 0 + shelf_action = "changed" + flash_text = _(u"Shelf %(title)s changed", title=to_save["title"]) try: ub.session.commit() - flash(_(u"Shelf %(title)s changed", title=to_save["title"]), category="success") - except (OperationalError, InvalidRequestError): + log.info(u"Shelf {} {}".format(to_save["title"], shelf_action)) + flash(flash_text, category="success") + return redirect(url_for('shelf.show_shelf', shelf_id=shelf.id)) + except (OperationalError, InvalidRequestError) as ex: ub.session.rollback() - flash(_(u"Settings DB is not Writeable"), category="error") - except Exception: + 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(ex) flash(_(u"There was an error"), category="error") - return render_title_template('shelf_edit.html', shelf=shelf, title=_(u"Edit a shelf"), page="shelfedit") + return render_title_template('shelf_edit.html', shelf=shelf, title=title, page=page) + + +def check_shelf_is_unique(shelf, to_save, shelf_id=False): + if shelf_id: + ident = ub.Shelf.id != shelf_id else: - return render_title_template('shelf_edit.html', shelf=shelf, title=_(u"Edit a shelf"), page="shelfedit") + 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(ident) \ + .first() is None + + if not is_shelf_name_unique: + log.error("A public shelf with the name '{}' already exists.".format(to_save["title"])) + flash(_(u"A public shelf with the name '%(title)s' already exists.", title=to_save["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) & + (ub.Shelf.user_id == int(current_user.id))) \ + .filter(ident) \ + .first() is None + + if not is_shelf_name_unique: + log.error("A private shelf with the name '{}' already exists.".format(to_save["title"])) + flash(_(u"A private shelf with the name '%(title)s' already exists.", title=to_save["title"]), + category="error") + return is_shelf_name_unique def delete_shelf_helper(cur_shelf): @@ -322,9 +310,7 @@ def delete_shelf_helper(cur_shelf): ub.session.delete(cur_shelf) ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).delete() ub.session.add(ub.ShelfArchive(uuid=cur_shelf.uuid, user_id=cur_shelf.user_id)) - ub.session.commit() - log.info("successfully deleted %s", cur_shelf) - + ub.session_commit("successfully deleted Shelf {}".format(cur_shelf.name)) @shelf.route("/shelf/delete/") @@ -333,44 +319,25 @@ def delete_shelf(shelf_id): cur_shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first() try: delete_shelf_helper(cur_shelf) - except (OperationalError, InvalidRequestError): + 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')) -@shelf.route("/shelf/", defaults={'shelf_type': 1}) -@shelf.route("/shelf//") +@shelf.route("/simpleshelf/") @login_required_if_no_ano -def show_shelf(shelf_type, shelf_id): - shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first() +def show_simpleshelf(shelf_id): + return render_show_shelf(2, shelf_id, 1, None) - result = list() - # user is allowed to access shelf - if shelf and check_shelf_view_permissions(shelf): - page = "shelf.html" if shelf_type == 1 else 'shelfdown.html' - books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id)\ - .order_by(ub.BookShelf.order.asc()).all() - for book in books_in_shelf: - cur_book = calibre_db.get_filtered_book(book.book_id) - if cur_book: - result.append(cur_book) - else: - cur_book = calibre_db.get_book(book.book_id) - if not cur_book: - log.info('Not existing book %s in %s deleted', book.book_id, shelf) - try: - ub.session.query(ub.BookShelf).filter(ub.BookShelf.book_id == book.book_id).delete() - ub.session.commit() - except (OperationalError, InvalidRequestError): - ub.session.rollback() - flash(_(u"Settings DB is not Writeable"), category="error") - return render_title_template(page, entries=result, title=_(u"Shelf: '%(name)s'", name=shelf.name), - shelf=shelf, page="shelf") - else: - flash(_(u"Error opening shelf. Shelf does not exist or is not accessible"), category="error") - return redirect(url_for("web.index")) +@shelf.route("/shelf/", defaults={"sort_param": "order", 'page': 1}) +@shelf.route("/shelf//", defaults={'page': 1}) +@shelf.route("/shelf///") +@login_required_if_no_ano +def show_shelf(shelf_id, sort_param, page): + return render_show_shelf(1, shelf_id, page, sort_param) @shelf.route("/shelf/order/", methods=["GET", "POST"]) @@ -389,27 +356,86 @@ 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): - books_in_shelf2 = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id) \ - .order_by(ub.BookShelf.order.asc()).all() - for book in books_in_shelf2: - cur_book = calibre_db.get_filtered_book(book.book_id) - if cur_book: - result.append({'title': cur_book.title, - 'id': cur_book.id, - 'author': cur_book.authors, - 'series': cur_book.series, - 'series_index': cur_book.series_index}) - else: - cur_book = calibre_db.get_book(book.book_id) - result.append({'title': _('Hidden Book'), - 'id': cur_book.id, - 'author': [], - 'series': []}) + 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, title=_(u"Change order of Shelf: '%(name)s'", name=shelf.name), shelf=shelf, page="shelforder") + + +def change_shelf_order(shelf_id, order): + result = calibre_db.session.query(db.Books).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) \ + .filter(ub.BookShelf.book_id == entry.id).first() + book.order = index + ub.session_commit("Shelf-id:{} - Order changed".format(shelf_id)) + + +def render_show_shelf(shelf_type, shelf_id, page_no, sort_param): + shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first() + + # check user is allowed to access shelf + if shelf and check_shelf_view_permissions(shelf): + + if shelf_type == 1: + # order = [ub.BookShelf.order.asc()] + if sort_param == 'pubnew': + change_shelf_order(shelf_id, [db.Books.pubdate.desc()]) + if sort_param == 'pubold': + change_shelf_order(shelf_id, [db.Books.pubdate]) + if sort_param == 'abc': + change_shelf_order(shelf_id, [db.Books.sort]) + if sort_param == 'zyx': + change_shelf_order(shelf_id, [db.Books.sort.desc()]) + if sort_param == 'new': + change_shelf_order(shelf_id, [db.Books.timestamp.desc()]) + 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()]) + if sort_param == 'authza': + change_shelf_order(shelf_id, [db.Books.author_sort.desc()]) + page = "shelf.html" + pagesize = 0 + else: + pagesize = sys.maxsize + 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) + # 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)\ + .filter(db.Books.id == None).all() + for entry in wrong_entries: + log.info('Not existing book {} in {} deleted'.format(entry.book_id, shelf)) + try: + ub.session.query(ub.BookShelf).filter(ub.BookShelf.book_id == entry.book_id).delete() + ub.session.commit() + except (OperationalError, InvalidRequestError): + ub.session.rollback() + log.error("Settings DB is not Writeable") + flash(_("Settings DB is not Writeable"), category="error") + + return render_title_template(page, + entries=result, + pagination=pagination, + title=_(u"Shelf: '%(name)s'", name=shelf.name), + shelf=shelf, + page="shelf") + else: + flash(_(u"Error opening shelf. Shelf does not exist or is not accessible"), category="error") + return redirect(url_for("web.index")) diff --git a/cps/static/css/caliBlur.css b/cps/static/css/caliBlur.css index 383f7e37..e2f8445d 100644 --- a/cps/static/css/caliBlur.css +++ b/cps/static/css/caliBlur.css @@ -240,7 +240,7 @@ body.blur .row-fluid .col-sm-10 { .col-sm-10 .book-meta > div.btn-toolbar:after { content: ''; - direction: block; + direction: ltr; position: fixed; top: 120px; right: 0; @@ -398,20 +398,17 @@ body.blur .row-fluid .col-sm-10 { .shelforder #sortTrue > div:hover { background-color: hsla(0, 0%, 100%, .06) !important; - cursor: move; cursor: grab; - cursor: -webkit-grab; color: #eee } .shelforder #sortTrue > div:active { cursor: grabbing; - cursor: -webkit-grabbing } .shelforder #sortTrue > div:before { content: "\EA53"; - font-family: plex-icons-new; + font-family: plex-icons-new, serif; margin-right: 30px; margin-left: 15px; vertical-align: bottom; @@ -446,7 +443,7 @@ body.blur .row-fluid .col-sm-10 { body.shelforder > div.container-fluid > div.row-fluid > div.col-sm-10:before { content: "\e155"; - font-family: 'Glyphicons Halflings'; + font-family: 'Glyphicons Halflings', serif; font-style: normal; font-weight: 400; line-height: 1; @@ -494,7 +491,7 @@ body.shelforder > div.container-fluid > div.row-fluid > div.col-sm-10:before { } #have_read_cb + label:before, #have_read_cb:checked + label:before { - font-family: 'Glyphicons Halflings'; + font-family: 'Glyphicons Halflings', serif; font-size: 16px; height: 40px; width: 60px; @@ -550,13 +547,12 @@ body.shelforder > div.container-fluid > div.row-fluid > div.col-sm-10:before { height: 60px; width: 50px; cursor: pointer; - margin: 0; display: inline-block; - margin-top: -4px; + margin: -4px 0 0; } #archived_cb + label:before, #archived_cb:checked + label:before { - font-family: 'Glyphicons Halflings'; + font-family: 'Glyphicons Halflings', serif; font-size: 16px; height: 40px; width: 60px; @@ -581,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; @@ -618,7 +610,7 @@ div[aria-label="Edit/Delete book"] > .btn > span { div[aria-label="Edit/Delete book"] > .btn > span:before { content: "\EA5d"; - font-family: plex-icons; + font-family: plex-icons, serif; font-size: 20px; padding: 16px 15px; display: inline-block; @@ -641,7 +633,7 @@ div[aria-label="Edit/Delete book"] > .btn > span:hover { width: 225px; max-width: 225px; position: relative !important; - left: auto !important; + left: auto !important; top: auto !important; -webkit-transform: none !important; -ms-transform: none !important; @@ -760,7 +752,7 @@ body > div.navbar.navbar-default.navbar-static-top > div > div.navbar-header > a .home-btn { color: hsla(0, 0%, 100%, .7); - line-height: 34.29px; + line-height: 34px; margin: 0; padding: 0; position: absolute; @@ -770,7 +762,7 @@ body > div.navbar.navbar-default.navbar-static-top > div > div.navbar-header > a .home-btn > a { color: rgba(255, 255, 255, .7); - font-family: plex-icons-new; + font-family: plex-icons-new, serif; line-height: 60px; position: relative; text-align: center; @@ -800,7 +792,7 @@ body > div.navbar.navbar-default.navbar-static-top > div > div.home-btn > a:hove .glyphicon-search:before { content: "\EA4F"; - font-family: plex-icons + font-family: plex-icons, serif } #nav_about:after, .profileDrop > span:after, .profileDrop > span:before { @@ -833,7 +825,7 @@ body:not(.read-frame) { overflow: hidden; margin: 0; /* scroll bar fix for firefox */ - scrollbar-color: hsla(0, 0%, 100%, .2) transparent; + scrollbar-color: hsla(0, 0%, 100%, .2) transparent; scrollbar-width: thin; } @@ -966,7 +958,7 @@ body > div.navbar.navbar-default.navbar-static-top > div > form.search-focus > d #form-upload .form-group .btn:before { content: "\e043"; - font-family: 'Glyphicons Halflings'; + font-family: 'Glyphicons Halflings', serif; font-style: normal; font-weight: 400; line-height: 1; @@ -991,7 +983,7 @@ body > div.navbar.navbar-default.navbar-static-top > div > form.search-focus > d #form-upload .form-group .btn:after { content: "\EA13"; position: absolute; - font-family: plex-icons-new; + font-family: plex-icons-new, serif; font-size: 8px; background: #3c444a; color: hsla(0, 0%, 100%, .7); @@ -1019,7 +1011,7 @@ body > div.navbar.navbar-default.navbar-static-top > div > form.search-focus > d text-transform: none; font-weight: 400; font-style: normal; - font-family: plex-icons-new; + font-family: plex-icons-new, serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; line-height: 1; @@ -1075,7 +1067,7 @@ body > div.navbar.navbar-default.navbar-static-top > div > form.search-focus > d body > div.navbar.navbar-default.navbar-static-top > div > form > div > span > button:before { content: "\EA32"; - font-family: plex-icons-new; + font-family: plex-icons-new, serif; color: #eee; background: #555; font-size: 10px; @@ -1097,7 +1089,7 @@ body > div.navbar.navbar-default.navbar-static-top > div > form > div > span > b body > div.navbar.navbar-default.navbar-static-top > div > form:before { content: "\EA4F"; display: block; - font-family: plex-icons; + font-family: plex-icons, serif; position: absolute; color: #eee; font-weight: 400; @@ -1120,7 +1112,7 @@ body > div.navbar.navbar-default.navbar-static-top > div > form:before { body > div.navbar.navbar-default.navbar-static-top > div > form.search-focus > div > span.input-group-btn:before { content: "\EA4F"; display: block; - font-family: plex-icons; + font-family: plex-icons, serif; position: absolute; left: -298px; top: 8px; @@ -1193,7 +1185,7 @@ body > div.navbar.navbar-default.navbar-static-top > div > div.navbar-collapse.c body > div.navbar.navbar-default.navbar-static-top > div > div.navbar-collapse.collapse > ul > li > #top_admin > .glyphicon-dashboard::before { content: "\EA31"; - font-family: plex-icons-new; + font-family: plex-icons-new, serif; font-size: 20px } @@ -1272,7 +1264,7 @@ body > div.container-fluid > div > div.col-sm-10 > div > form > a:hover { user-select: none } -.navigation li, .navigation li:not(ul>li) { +.navigation li, .navigation li:not(ul > li) { border-radius: 0 4px 4px 0 } @@ -1352,32 +1344,32 @@ body > div.container-fluid > div > div.col-sm-10 > div > form > a:hover { #nav_hot .glyphicon-fire::before { content: "\1F525"; - font-family: glyphicons regular + font-family: glyphicons regular, serif } .glyphicon-star:before { content: "\EA10"; - font-family: plex-icons-new + font-family: plex-icons-new, serif } #nav_rand .glyphicon-random::before { content: "\EA44"; - font-family: plex-icons-new + font-family: plex-icons-new, serif } .glyphicon-list::before { content: "\EA4D"; - font-family: plex-icons-new + font-family: plex-icons-new, serif } #nav_about .glyphicon-info-sign::before { content: "\EA26"; - font-family: plex-icons-new + font-family: plex-icons-new, serif } #nav_cat .glyphicon-inbox::before, .glyphicon-tags::before { content: "\E067"; - font-family: Glyphicons Regular; + font-family: Glyphicons Regular, serif; margin-left: 2px } @@ -1423,7 +1415,7 @@ body > div.container-fluid > div > div.col-sm-10 > div > form > a:hover { .navigation .create-shelf a:before { content: "\EA13"; - font-family: plex-icons-new; + font-family: plex-icons-new, serif; font-size: 100%; padding-right: 10px; vertical-align: middle @@ -1473,7 +1465,7 @@ body > div.container-fluid > div > div.col-sm-10 > div > form > a:hover { #books > .cover > a:before, #books_rand > .cover > a:before, .book.isotope-item > .cover > a:before, 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:before { content: "\e352"; - font-family: Glyphicons Regular; + font-family: Glyphicons Regular, serif; background: var(--color-secondary); border-radius: 50%; font-weight: 400; @@ -1521,8 +1513,8 @@ body > div.container-fluid > div.row-fluid > div.col-sm-10 > div.discover > form top: 0; left: 0; opacity: 0; - background: -webkit-radial-gradient(50% 50%, farthest-corner, rgba(50, 50, 50, .5) 50%, #323232 100%); - background: -o-radial-gradient(50% 50%, farthest-corner, rgba(50, 50, 50, .5) 50%, #323232 100%); + background: -webkit-radial-gradient(farthest-corner at 50% 50%, rgba(50, 50, 50, .5) 50%, #323232 100%); + background: -o-radial-gradient(farthest-corner at 50% 50%, rgba(50, 50, 50, .5) 50%, #323232 100%); background: radial-gradient(farthest-corner at 50% 50%, rgba(50, 50, 50, .5) 50%, #323232 100%); z-index: -9 } @@ -1562,8 +1554,8 @@ body > div.container-fluid > div.row-fluid > div.col-sm-10 > div.discover > form top: 0; left: 0; opacity: 0; - background: -webkit-radial-gradient(50% 50%, farthest-corner, rgba(50, 50, 50, .5) 50%, #323232 100%); - background: -o-radial-gradient(50% 50%, farthest-corner, rgba(50, 50, 50, .5) 50%, #323232 100%); + background: -webkit-radial-gradient(farthest-corner at 50% 50%, rgba(50, 50, 50, .5) 50%, #323232 100%); + background: -o-radial-gradient(farthest-corner at 50% 50%, rgba(50, 50, 50, .5) 50%, #323232 100%); background: radial-gradient(farthest-corner at 50% 50%, rgba(50, 50, 50, .5) 50%, #323232 100%) } @@ -1739,7 +1731,7 @@ body > div.container-fluid > div.row-fluid > div.col-sm-10 { body.me > div.container-fluid > div.row-fluid > div.col-sm-10:before { content: ''; - font-family: 'Glyphicons Halflings'; + font-family: 'Glyphicons Halflings', serif; font-style: normal; font-weight: 400; font-size: 6vw; @@ -1787,6 +1779,12 @@ body > div.container-fluid > div > div.col-sm-10 > div.discover { margin-top: 0 } +.container-fluid .book .meta .series { + /* font-weight: 400; */ + /* font-size: 12px; */ + color: hsla(0, 0%, 100%, .45); +} + .container-fluid .book .meta > p { -o-text-overflow: ellipsis; text-overflow: ellipsis; @@ -1947,7 +1945,7 @@ body > div.container-fluid > div > div.col-sm-10 > div.pagination .page-next > a body > div.container-fluid > div > div.col-sm-10 > div.pagination .page-previous > a { top: 0; - font-family: plex-icons-new; + font-family: plex-icons-new, serif; font-weight: 100; -webkit-font-smoothing: antialiased; line-height: 60px; @@ -2026,7 +2024,7 @@ body.authorlist > div.container-fluid > div > div.col-sm-10 > div.container > di body.serieslist > div.container-fluid > div > div.col-sm-10:before { content: "\e044"; - font-family: 'Glyphicons Halflings'; + font-family: 'Glyphicons Halflings', serif; font-style: normal; font-weight: 400; line-height: 1; @@ -2123,15 +2121,14 @@ body > div.container-fluid > div > div.col-sm-10 > div.discover > div.container transition: all 0s } - -.book-meta > .bookinfo > .tags .btn-info, .well > form > .btn { +.well > form > .btn { vertical-align: middle; -o-transition: background-color .2s, color .2s } body.catlist > div.container-fluid > div.row-fluid > div.col-sm-10:before { content: "\E067"; - font-family: Glyphicons Regular; + font-family: Glyphicons Regular, serif; font-style: normal; font-weight: 400; line-height: 1; @@ -2151,7 +2148,7 @@ body.catlist > div.container-fluid > div.row-fluid > div.col-sm-10:before { body.authorlist > div.container-fluid > div.row-fluid > div.col-sm-10:before, body.langlist > div.container-fluid > div > div.col-sm-10:before { - font-family: 'Glyphicons Halflings'; + font-family: 'Glyphicons Halflings', serif; font-style: normal; font-weight: 400; line-height: 1; @@ -2250,7 +2247,7 @@ body.langlist > div.container-fluid > div > div.col-sm-10 > div.container:before body.advsearch > div.container-fluid > div > div.col-sm-10 > div.col-md-10.col-lg-6 { padding: 15px 10px 15px 40px; -} +} @media screen and (max-width: 992px) { body.advsearch > div.container-fluid > div > div.col-sm-10 > div.col-md-10.col-lg-6 { @@ -2492,7 +2489,6 @@ body > div.container-fluid > div > div.col-sm-10 > div.col-sm-8 > form > .btn.bt } textarea { - resize: none; resize: vertical } @@ -2838,7 +2834,7 @@ body > div.container-fluid > div.row-fluid > div.col-sm-10 > div.col-sm-8 > form body.advanced_search > div.container-fluid > div > div.col-sm-10 > div.col-sm-8:before { content: "\EA4F"; - font-family: plex-icons; + font-family: plex-icons, serif; font-style: normal; font-weight: 400; line-height: 1; @@ -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 { @@ -2962,46 +2959,35 @@ body > div.container-fluid > div > div.col-sm-10 > div > div > div.col-sm-3.col- margin-top: 24px } -.book-meta > .bookinfo > .publishers > span:first-of-type, .book-meta > .bookinfo > .publishing-date > span:first-of-type { +.book-meta > .bookinfo > .languages > span:first-of-type, +.book-meta > .bookinfo > .publishers > span:first-of-type, +.book-meta > .bookinfo > .publishing-date > span:first-of-type, +.real_custom_columns > span:first-of-type { color: hsla(0, 0%, 100%, .45); text-transform: uppercase; - font-family: Open Sans Bold, Helvetica Neue, Helvetica, Arial, sans-serif + font-family: Open Sans Bold, Helvetica Neue, Helvetica, Arial, sans-serif; + width: 200px; + display: inline-block } -.book-meta > .bookinfo > .publishers > span:last-of-type, .book-meta > .bookinfo > .publishing-date > span:last-of-type { +.book-meta > .bookinfo > .languages > span:last-of-type, +.book-meta > .bookinfo > .publishers > span:last-of-type, +.book-meta > .bookinfo > .publishing-date > span:last-of-type, +.real_custom_columns > span:last-of-type { font-family: Open Sans Semibold, Helvetica Neue, Helvetica, Arial, sans-serif; color: #fff; font-size: 15px; -webkit-font-smoothing: antialiased } -.book-meta > .bookinfo > .publishers > span:last-of-type { - padding-left: 90px +.book-meta > .bookinfo > .languages > span > a, +.book-meta > .bookinfo > .publishers > span > a, +.book-meta > .bookinfo > .publishing-date > span > a, +.real_custom_columns > span > a { + color: #fff } -.real_custom_columns > span:last-of-type { - padding-left: 90px -} - -.book-meta > .bookinfo > .publishing-date > span:last-of-type { - padding-left: 90px -} - -.book-meta > .bookinfo > .languages > span:first-of-type { - color: hsla(0, 0%, 100%, .45); - text-transform: uppercase; - font-family: Open Sans Bold, Helvetica Neue, Helvetica, Arial, sans-serif -} - -.book-meta > .bookinfo > .languages > span:last-of-type { - font-size: 15px; - font-family: Open Sans Semibold, Helvetica Neue, Helvetica, Arial, sans-serif; - -webkit-font-smoothing: antialiased; - color: #fff; - padding-left: 85px -} - -.book-meta > .bookinfo > .tags .btn-info, .book-meta > h2, body.book .author { +.book-meta > h2, body.book .author { font-family: Open Sans Bold, Helvetica Neue, Helvetica, Arial, sans-serif } @@ -3082,34 +3068,10 @@ body.book .author { background-color: rgba(0, 0, 0, .3) } -.book-meta > .bookinfo > .identifiers > p > .btn-success, .book-meta > .bookinfo > .tags .btn-info { - overflow: hidden; - text-align: center; - white-space: nowrap; - margin: 2px 3px 0 0; - padding: 0 10px -} - -.book-meta > .bookinfo > .tags .btn-info { - background-color: rgba(0, 0, 0, .15); - color: hsla(0, 0%, 100%, .7); - font-size: 13px; - display: inline-block; - border-radius: 4px; - -webkit-transition: background-color .2s, color .2s; - transition: background-color .2s, color .2s; - text-transform: none -} - .dropdown-menu, .tooltip.in { -webkit-transition: opacity .15s ease-out, -webkit-transform .15s cubic-bezier(.6, .4, .2, 1.4) } -.book-meta > .bookinfo > .tags .btn-info:hover { - color: #fff; - text-decoration: underline -} - .book-meta > .bookinfo > .identifiers, .book-meta > .bookinfo > .tags { padding-left: 40px; margin: 10px 0 @@ -3180,6 +3142,10 @@ div.btn-group[role=group][aria-label="Download, send to Kindle, reading"] > div. overflow: hidden; padding: 0 } +#readbtn { + height: 100%; + padding: 16px; +} #add-to-shelf > span.caret, #btnGroupDrop1 > span.caret, #read-in-browser > span.caret, .btn-toolbar > .btn-group > #btnGroupDrop2 > span.caret, .btn-toolbar > .btn-group > .btn-group > #btnGroupDrop2 > span.caret { padding-bottom: 5px @@ -3195,7 +3161,7 @@ div.btn-group[role=group][aria-label="Download, send to Kindle, reading"] > div. #add-to-shelf > span.glyphicon.glyphicon-list:before { content: "\EA59"; - font-family: plex-icons; + font-family: plex-icons, serif; font-size: 18px } @@ -3207,7 +3173,7 @@ div.btn-group[role=group][aria-label="Download, send to Kindle, reading"] > div. #read-in-browser > span.glyphicon-eye-open:before, .btn-toolbar > .btn-group > .btn-group > #btnGroupDrop2 > span.glyphicon-eye-open:before { content: "\e352"; - font-family: Glyphicons Regular; + font-family: Glyphicons Regular, serif; font-size: 18px; padding-right: 5px } @@ -3219,7 +3185,7 @@ div.btn-group[role=group][aria-label="Download, send to Kindle, reading"] > div. #btnGroupDrop1 > span.glyphicon-download:before { font-size: 20px; content: "\ea66"; - font-family: plex-icons + font-family: plex-icons, serif } .col-sm-10 .book-meta > div.btn-toolbar { @@ -3323,7 +3289,6 @@ div.btn-group[role=group][aria-label="Download, send to Kindle, reading"] .dropd -webkit-box-shadow: 0 4px 10px rgba(0, 0, 0, .35); box-shadow: 0 4px 10px rgba(0, 0, 0, .35); -o-transition: opacity .15s ease-out, transform .15s cubic-bezier(.6, .4, .2, 1.4); - transition: opacity .15s ease-out, transform .15s cubic-bezier(.6, .4, .2, 1.4); transition: opacity .15s ease-out, transform .15s cubic-bezier(.6, .4, .2, 1.4), -webkit-transform .15s cubic-bezier(.6, .4, .2, 1.4); -webkit-transform-origin: center top; -ms-transform-origin: center top; @@ -3351,7 +3316,8 @@ div.btn-group[role=group][aria-label="Download, send to Kindle, reading"] .dropd box-shadow: none } -.book-meta > .bookinfo > .identifiers > p > .btn-success { +.book-meta > .bookinfo .btn-info, +.book-meta > .bookinfo .btn-success { background-color: rgba(0, 0, 0, .15); color: hsla(0, 0%, 100%, .7); font-size: 13px; @@ -3365,11 +3331,21 @@ div.btn-group[role=group][aria-label="Download, send to Kindle, reading"] .dropd text-transform: none } -.book-meta > .bookinfo > .identifiers > p > .btn-success:hover { +.book-meta > .bookinfo .btn-info:hover, +.book-meta > .bookinfo .btn-success:hover { color: #fff; text-decoration: underline } +.book-meta > .bookinfo .btn-info, +.book-meta > .bookinfo .btn-success { + overflow: hidden; + text-align: center; + white-space: nowrap; + margin: 2px 3px 0 0; + padding: 0 10px +} + #bookDetailsModal .book-meta { color: hsla(0, 0%, 100%, .7); height: calc(100% - 120px); @@ -3441,7 +3417,7 @@ body > div.container-fluid > div > div.col-sm-10 > div.discover > .btn-primary:l .book-meta > div.more-stuff > .btn-toolbar > .btn-group[aria-label="Remove from shelves"] > a > .glyphicon-remove:before { content: "\ea64"; - font-family: plex-icons + font-family: plex-icons, serif } body > div.container-fluid > div.row-fluid > div.col-sm-10 > div.discover > form > .col-sm-6 { @@ -3555,7 +3531,7 @@ body.shelf > div.container-fluid > div > div.col-sm-10 > div.discover > .shelf-b body.shelf > div.container-fluid > div > div.col-sm-10 > div.discover > .shelf-btn-group > [data-target="#DeleteShelfDialog"]:before { content: "\EA6D"; - font-family: plex-icons-new; + font-family: plex-icons-new, serif; position: absolute; color: hsla(0, 0%, 100%, .7); font-size: 20px; @@ -3585,7 +3561,7 @@ body.shelf > div.container-fluid > div > div.col-sm-10 > div.discover > .shelf-b body.shelf > div.container-fluid > div > div.col-sm-10 > div.discover > .shelf-btn-group > [href*=edit]:before { content: "\EA5d"; - font-family: plex-icons; + font-family: plex-icons, serif; position: absolute; color: hsla(0, 0%, 100%, .7); font-size: 20px; @@ -3615,7 +3591,7 @@ body.shelf > div.container-fluid > div > div.col-sm-10 > div.discover > .shelf-b body.shelf > div.container-fluid > div > div.col-sm-10 > div.discover > .shelf-btn-group > [href*=order]:before { content: "\E409"; - font-family: Glyphicons Regular; + font-family: Glyphicons Regular, serif; position: absolute; color: hsla(0, 0%, 100%, .7); font-size: 20px; @@ -3752,7 +3728,7 @@ body > div.navbar.navbar-default.navbar-static-top > div > div.navbar-header > a .plexBack > a { color: rgba(255, 255, 255, .7); - font-family: plex-icons-new; + font-family: plex-icons-new, serif; -webkit-font-variant-ligatures: normal; font-variant-ligatures: normal; line-height: 60px; @@ -3864,11 +3840,9 @@ body.login > div.container-fluid > div.row-fluid > div.col-sm-10::before, body.l -webkit-transform: translateY(-50%); -ms-transform: translateY(-50%); transform: translateY(-50%); - border-style: solid; vertical-align: middle; -webkit-transition: border .2s, -webkit-transform .4s; -o-transition: border .2s, transform .4s; - transition: border .2s, transform .4s; transition: border .2s, transform .4s, -webkit-transform .4s; margin: 9px 6px } @@ -3887,11 +3861,9 @@ body.login > div.container-fluid > div.row-fluid > div.col-sm-10::before, body.l -webkit-transform: translateY(-50%); -ms-transform: translateY(-50%); transform: translateY(-50%); - border-style: solid; vertical-align: middle; -webkit-transition: border .2s, -webkit-transform .4s; -o-transition: border .2s, transform .4s; - transition: border .2s, transform .4s; transition: border .2s, transform .4s, -webkit-transform .4s; margin: 12px 6px } @@ -3971,7 +3943,7 @@ body.author img.bg-blur[src=undefined] { body.author:not(.authorlist) .undefined-img:before { content: "\e008"; - font-family: 'Glyphicons Halflings'; + font-family: 'Glyphicons Halflings', serif; font-style: normal; font-weight: 400; line-height: 1; @@ -4120,7 +4092,7 @@ body.shelf.modal-open > .container-fluid { font-size: 18px; color: #999; display: inline-block; - font-family: Glyphicons Regular; + font-family: Glyphicons Regular, serif; font-style: normal; font-weight: 400 } @@ -4221,7 +4193,7 @@ body.shelf.modal-open > .container-fluid { #remove-from-shelves > .btn > span:before { content: "\EA52"; - font-family: plex-icons-new; + font-family: plex-icons-new, serif; color: transparent; padding-left: 5px } @@ -4233,7 +4205,7 @@ body.shelf.modal-open > .container-fluid { #remove-from-shelves > a:first-of-type:before { content: "\EA4D"; - font-family: plex-icons-new; + font-family: plex-icons-new, serif; position: absolute; color: hsla(0, 0%, 100%, .45); font-style: normal; @@ -4273,7 +4245,7 @@ body.shelf.modal-open > .container-fluid { content: "\E208"; padding-right: 10px; display: block; - font-family: Glyphicons Regular; + font-family: Glyphicons Regular, serif; font-style: normal; font-weight: 400; position: absolute; @@ -4284,7 +4256,6 @@ body.shelf.modal-open > .container-fluid { opacity: .5; -webkit-transition: -webkit-transform .3s ease-out; -o-transition: transform .3s ease-out; - transition: transform .3s ease-out; transition: transform .3s ease-out, -webkit-transform .3s ease-out; -webkit-transform: translate(0, -60px); -ms-transform: translate(0, -60px); @@ -4344,7 +4315,7 @@ body.advanced_search > div.container-fluid > div > div.col-sm-10 > div.col-sm-8 .glyphicon-remove:before { content: "\EA52"; - font-family: plex-icons-new; + font-family: plex-icons-new, serif; font-weight: 400 } @@ -4430,7 +4401,7 @@ body.advanced_search > div.container-fluid > div.row-fluid > div.col-sm-10 > div body:not(.blur) #nav_new:before { content: "\EA4F"; - font-family: plex-icons; + font-family: plex-icons, serif; font-style: normal; font-weight: 400; line-height: 1; @@ -4456,7 +4427,7 @@ body.advanced_search > div.container-fluid > div.row-fluid > div.col-sm-10 > div color: hsla(0, 0%, 100%, .7); cursor: pointer; display: block; - font-family: plex-icons-new; + font-family: plex-icons-new, serif; font-size: 20px; font-stretch: 100%; font-style: normal; @@ -4552,12 +4523,12 @@ body.admin > div.container-fluid > div > div.col-sm-10 > div.container-fluid > d } body.admin > div.container-fluid > div > div.col-sm-10 > div.container-fluid > div.row > div.col .table > tbody > tr > td, body.admin > div.container-fluid > div > div.col-sm-10 > div.container-fluid > div.row > div.col .table > tbody > tr > th, body.admin > div.container-fluid > div > div.col-sm-10 > div.discover > .table > tbody > tr > td, body.admin > div.container-fluid > div > div.col-sm-10 > div.discover > .table > tbody > tr > th { - border: collapse + border: collapse; } body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10::before, body.newuser.admin > div.container-fluid > div.row-fluid > div.col-sm-10::before { content: ''; - font-family: 'Glyphicons Halflings'; + font-family: 'Glyphicons Halflings', serif; font-style: normal; font-weight: 400; font-size: 6vw; @@ -4661,7 +4632,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div. content: "\e352"; display: inline-block; position: absolute; - font-family: Glyphicons Regular; + font-family: Glyphicons Regular, serif; background: var(--color-secondary); color: #fff; border-radius: 50%; @@ -4699,8 +4670,8 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div. top: 0; left: 0; opacity: 0; - background: -webkit-radial-gradient(50% 50%, farthest-corner, rgba(50, 50, 50, .5) 50%, #323232 100%); - background: -o-radial-gradient(50% 50%, farthest-corner, rgba(50, 50, 50, .5) 50%, #323232 100%); + background: -webkit-radial-gradient(farthest-corner at 50% 50%, rgba(50, 50, 50, .5) 50%, #323232 100%); + background: -o-radial-gradient(farthest-corner at 50% 50%, rgba(50, 50, 50, .5) 50%, #323232 100%); background: radial-gradient(farthest-corner at 50% 50%, rgba(50, 50, 50, .5) 50%, #323232 100%) } @@ -4752,7 +4723,7 @@ body.admin td > a:hover { .glyphicon-ok::before { content: "\EA55"; - font-family: plex-icons-new; + font-family: plex-icons-new, serif; font-weight: 400 } @@ -4821,7 +4792,7 @@ body:not(.blur):not(.login):not(.me):not(.author):not(.editbook):not(.upload):no background-position: center center, center center, center center !important; background-size: auto, auto, cover !important; -webkit-background-size: auto, auto, cover !important; - -moz-background-size: autom, auto, cover !important; + -moz-background-size: auto, auto, cover !important; -o-background-size: auto, auto, cover !important; width: 100%; height: 60px; @@ -4887,7 +4858,6 @@ body.read:not(.blur) a[href*=readbooks] { .tooltip.in { opacity: 1; -o-transition: opacity .15s ease-out, transform .15s cubic-bezier(.6, .4, .2, 1.4); - transition: opacity .15s ease-out, transform .15s cubic-bezier(.6, .4, .2, 1.4); transition: opacity .15s ease-out, transform .15s cubic-bezier(.6, .4, .2, 1.4), -webkit-transform .15s cubic-bezier(.6, .4, .2, 1.4); -webkit-transform: translate(0) scale(1); -ms-transform: translate(0) scale(1); @@ -4987,7 +4957,7 @@ body.editbook > div.container-fluid > div.row-fluid > div.col-sm-10 > form > div body.editbook > div.container-fluid > div.row-fluid > div.col-sm-10 > form > div.col-sm-3 > div.text-center > #delete:before, body.upload > div.container-fluid > div.row-fluid > div.col-sm-10 > form > div.col-sm-3 > div.text-center > #delete:before { content: "\EA6D"; - font-family: plex-icons-new; + font-family: plex-icons-new, serif; font-size: 18px; color: hsla(0, 0%, 100%, .7) } @@ -5072,7 +5042,7 @@ body.tasks > div.container-fluid > div > div.col-sm-10 > div.discover > div.boot body.tasks > div.container-fluid > div > div.col-sm-10 > div.discover > div.bootstrap-table > div.fixed-table-container > div.fixed-table-body > #table > thead > tr > th > .th-inner.asc:after { content: "\EA58"; - font-family: plex-icons-new; + font-family: plex-icons-new, serif; font-weight: 400; right: 20px; position: absolute @@ -5080,7 +5050,7 @@ body.tasks > div.container-fluid > div > div.col-sm-10 > div.discover > div.boot body.tasks > div.container-fluid > div > div.col-sm-10 > div.discover > div.bootstrap-table > div.fixed-table-container > div.fixed-table-body > #table > thead > tr > th > .th-inner.desc:after { content: "\EA57"; - font-family: plex-icons-new; + font-family: plex-icons-new, serif; font-weight: 400; right: 20px; position: absolute @@ -5143,7 +5113,7 @@ body.tasks > div.container-fluid > div > div.col-sm-10 > div.discover > div.boot .epub-back:before { content: "\EA1C"; - font-family: plex-icons-new; + font-family: plex-icons-new, serif; font-weight: 400; color: #4f4f4f; position: absolute; @@ -5306,7 +5276,7 @@ body.editbook > div.container-fluid > div.row-fluid > div.col-sm-10 > div.col-sm body.editbook > div.container-fluid > div.row-fluid > div.col-sm-10 > div.col-sm-3 > div.text-center > #delete:before, body.upload > div.container-fluid > div.row-fluid > div.col-sm-10 > div.col-sm-3 > div.text-center > #delete:before { content: "\EA6D"; - font-family: plex-icons-new; + font-family: plex-icons-new, serif; font-size: 18px; color: hsla(0, 0%, 100%, .7); vertical-align: super @@ -5466,7 +5436,7 @@ body.editbook > div.container-fluid > div.row-fluid > div.col-sm-10 > div.col-sm #main-nav + #scnd-nav .create-shelf a:before { content: "\EA13"; - font-family: plex-icons-new; + font-family: plex-icons-new, serif; font-size: 100%; padding-right: 10px; vertical-align: middle @@ -5511,7 +5481,7 @@ body.admin.modal-open .navbar { content: "\E208"; padding-right: 10px; display: block; - font-family: Glyphicons Regular; + font-family: Glyphicons Regular, serif; font-style: normal; font-weight: 400; position: absolute; @@ -5522,7 +5492,6 @@ body.admin.modal-open .navbar { opacity: .5; -webkit-transition: -webkit-transform .3s ease-out; -o-transition: transform .3s ease-out; - transition: transform .3s ease-out; transition: transform .3s ease-out, -webkit-transform .3s ease-out; -webkit-transform: translate(0, -60px); -ms-transform: translate(0, -60px); @@ -5576,22 +5545,22 @@ body.admin.modal-open .navbar { #RestartDialog > .modal-dialog > .modal-content > .modal-header:before { content: "\EA4F"; - font-family: plex-icons-new + font-family: plex-icons-new, serif } #ShutdownDialog > .modal-dialog > .modal-content > .modal-header:before { content: "\E064"; - font-family: glyphicons regular + font-family: glyphicons regular, serif } #StatusDialog > .modal-dialog > .modal-content > .modal-header:before { content: "\EA15"; - font-family: plex-icons-new + font-family: plex-icons-new, serif } #deleteModal > .modal-dialog > .modal-content > .modal-header:before { content: "\EA6D"; - font-family: plex-icons-new + font-family: plex-icons-new, serif } #RestartDialog > .modal-dialog > .modal-content > .modal-header:after { @@ -5982,7 +5951,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div. .home-btn { height: 48px; - line-height: 28.29px; + line-height: 28px; right: 10px; left: auto } @@ -5994,7 +5963,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div. .plexBack { height: 48px; - line-height: 28.29px; + line-height: 28px; left: 48px; display: none } @@ -6073,7 +6042,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div. body > div.navbar.navbar-default.navbar-static-top > div > form.search-focus > div > span.input-group-btn:before { content: "\EA33"; display: block; - font-family: plex-icons-new; + font-family: plex-icons-new, serif; position: fixed; left: 0; top: 0; @@ -6225,7 +6194,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div. #form-upload .form-group .btn:before { content: "\e043"; - font-family: 'Glyphicons Halflings'; + font-family: 'Glyphicons Halflings', serif; line-height: 1; -webkit-font-smoothing: antialiased; color: #fff; @@ -6243,7 +6212,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div. #form-upload .form-group .btn:after { content: "\EA13"; position: absolute; - font-family: plex-icons-new; + font-family: plex-icons-new, serif; font-size: 8px; background: #3c444a; color: #fff; @@ -6296,7 +6265,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div. } #top_admin, #top_tasks { - padding: 11.5px 15px; + padding: 12px 15px; font-size: 13px; line-height: 1.71428571; overflow: hidden @@ -6305,7 +6274,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div. #top_admin > .glyphicon, #top_tasks > .glyphicon-tasks { position: relative; top: 0; - font-family: 'Glyphicons Halflings'; + font-family: 'Glyphicons Halflings', serif; line-height: 1; border-radius: 0; background: 0 0; @@ -6324,7 +6293,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div. #top_tasks > .glyphicon-tasks::before, body > div.navbar.navbar-default.navbar-static-top > div > div.navbar-collapse.collapse > ul > li > #top_admin > .glyphicon-dashboard::before { text-transform: none; - font-family: plex-icons-new; + font-family: plex-icons-new, serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-rendering: optimizeLegibility; @@ -6649,7 +6618,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div. .author > .container-fluid > .row-fluid > .col-sm-10 > h2:after { content: "\e008"; - font-family: 'Glyphicons Halflings'; + font-family: 'Glyphicons Halflings', serif; font-style: normal; font-weight: 400; line-height: 1; @@ -6854,7 +6823,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div. color: hsla(0, 0%, 100%, .7); cursor: pointer; display: block; - font-family: plex-icons-new; + font-family: plex-icons-new, serif; font-size: 20px; font-stretch: 100%; font-style: normal; @@ -6983,16 +6952,12 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div. margin: 45px } - .book-meta > .bookinfo > .publishing-date > span:last-of-type { - padding-left: 25px - } - - .book-meta > .bookinfo > .publishers > span:last-of-type { - padding-left: 70px - } - - .book-meta > .bookinfo > .languages > span:last-of-type { - padding-left: 65px + .book-meta > .bookinfo > .languages > span:first-of-type, + .book-meta > .bookinfo > .publishers > span:first-of-type, + .book-meta > .bookinfo > .publishing-date > span:first-of-type, + .real_custom_columns > span:first-of-type { + width: 50%; + max-width: 200px; } .book-meta > .bookinfo .publishers { @@ -7025,11 +6990,9 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div. -webkit-transform: translateY(-50%); -ms-transform: translateY(-50%); transform: translateY(-50%); - border-style: solid; vertical-align: middle; -webkit-transition: border .2s, -webkit-transform .4s; -o-transition: border .2s, transform .4s; - transition: border .2s, transform .4s; transition: border .2s, transform .4s, -webkit-transform .4s; margin: 12px 6px } @@ -7048,18 +7011,16 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div. -webkit-transform: translateY(-50%); -ms-transform: translateY(-50%); transform: translateY(-50%); - border-style: solid; vertical-align: middle; -webkit-transition: border .2s, -webkit-transform .4s; -o-transition: border .2s, transform .4s; - transition: border .2s, transform .4s; transition: border .2s, transform .4s, -webkit-transform .4s; margin: 9px 6px } body.author:not(.authorlist) .blur-wrapper:before, body.author > .container-fluid > .row-fluid > .col-sm-10 > h2:after { content: "\e008"; - font-family: 'Glyphicons Halflings'; + font-family: 'Glyphicons Halflings', serif; font-weight: 400; z-index: 9; line-height: 1; @@ -7390,7 +7351,6 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div. transform: translate3d(0, 0, 0); -webkit-transition: -webkit-transform .5s; -o-transition: transform .5s; - transition: transform .5s; transition: transform .5s, -webkit-transform .5s; z-index: 99 } @@ -7405,7 +7365,6 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div. transform: translate3d(-240px, 0, 0); -webkit-transition: -webkit-transform .5s; -o-transition: transform .5s; - transition: transform .5s; transition: transform .5s, -webkit-transform .5s; top: 0; margin: 0; @@ -7444,7 +7403,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div. text-align: center; min-width: 40px; pointer-events: none; - color: # + // color: # } .col-xs-12 > .row > .col-xs-10 { @@ -7555,7 +7514,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div. body.publisherlist > div.container-fluid > div > div.col-sm-10:before { content: "\e241"; - font-family: 'Glyphicons Halflings'; + font-family: 'Glyphicons Halflings', serif; font-style: normal; font-weight: 400; line-height: 1; @@ -7575,7 +7534,7 @@ body.publisherlist > div.container-fluid > div > div.col-sm-10:before { body.ratingslist > div.container-fluid > div > div.col-sm-10:before { content: "\e007"; - font-family: 'Glyphicons Halflings'; + font-family: 'Glyphicons Halflings', serif; font-style: normal; font-weight: 400; line-height: 1; @@ -7601,7 +7560,7 @@ body.ratingslist > div.container-fluid > div > div.col-sm-10:before { body.formatslist > div.container-fluid > div > div.col-sm-10:before { content: "\e022"; - font-family: 'Glyphicons Halflings'; + font-family: 'Glyphicons Halflings', serif; font-style: normal; font-weight: 400; line-height: 1; @@ -7776,7 +7735,7 @@ body.mailset > div.container-fluid > div > div.col-sm-10 > div.discover .editabl body.mailset > div.container-fluid > div > div.col-sm-10 > div.discover .glyphicon-trash:before { content: "\EA6D"; - font-family: plex-icons-new + font-family: plex-icons-new, serif } #DeleteDomain { @@ -7799,7 +7758,7 @@ body.mailset > div.container-fluid > div > div.col-sm-10 > div.discover .glyphic content: "\E208"; padding-right: 10px; display: block; - font-family: Glyphicons Regular; + font-family: Glyphicons Regular, serif; font-style: normal; font-weight: 400; position: absolute; @@ -7810,7 +7769,6 @@ body.mailset > div.container-fluid > div > div.col-sm-10 > div.discover .glyphic opacity: .5; -webkit-transition: -webkit-transform .3s ease-out; -o-transition: transform .3s ease-out; - transition: transform .3s ease-out; transition: transform .3s ease-out, -webkit-transform .3s ease-out; -webkit-transform: translate(0, -60px); -ms-transform: translate(0, -60px); @@ -7849,7 +7807,7 @@ body.mailset > div.container-fluid > div > div.col-sm-10 > div.discover .glyphic #DeleteDomain > .modal-dialog > .modal-content > .modal-header:before { content: "\EA6D"; - font-family: plex-icons-new; + font-family: plex-icons-new, serif; padding-right: 10px; font-size: 18px; color: #999; diff --git a/cps/static/css/caliBlur_override.css b/cps/static/css/caliBlur_override.css index 7f940212..4c8b6cb0 100644 --- a/cps/static/css/caliBlur_override.css +++ b/cps/static/css/caliBlur_override.css @@ -1,17 +1,24 @@ -body.serieslist.grid-view div.container-fluid>div>div.col-sm-10:before{ - display: none; +body.serieslist.grid-view div.container-fluid > div > div.col-sm-10::before { + display: none; } -.cover .badge{ - position: absolute; - top: 0; - left: 0; - background-color: #cc7b19; - border-radius: 0; - padding: 0 8px; - box-shadow: 0 0 4px rgba(0,0,0,.6); - line-height: 24px; +.cover .badge { + position: absolute; + top: 0; + left: 0; + color: #fff; + background-color: #cc7b19; + border-radius: 0; + padding: 0 8px; + box-shadow: 0 0 4px rgba(0, 0, 0, 0.6); + line-height: 24px; } -.cover{ - box-shadow: 0 0 4px rgba(0,0,0,.6); + +.cover { + box-shadow: 0 0 4px rgba(0, 0, 0, 0.6); +} + +.cover .read { + padding: 0 0; + line-height: 15px; } diff --git a/cps/static/css/img/clear.png b/cps/static/css/img/clear.png new file mode 100644 index 00000000..580b52a5 Binary files /dev/null and b/cps/static/css/img/clear.png differ diff --git a/cps/static/css/img/loading.gif b/cps/static/css/img/loading.gif new file mode 100644 index 00000000..5b33f7e5 Binary files /dev/null and b/cps/static/css/img/loading.gif differ diff --git a/cps/static/css/kthoom.css b/cps/static/css/kthoom.css index cc38740b..267a2a84 100644 --- a/cps/static/css/kthoom.css +++ b/cps/static/css/kthoom.css @@ -33,7 +33,6 @@ body { position: relative; cursor: pointer; padding: 4px; - transition: all 0.2s ease; } @@ -45,7 +44,7 @@ body { #sidebar a.active, #sidebar a.active img + span { - background-color: #45B29D; + background-color: #45b29d; } #sidebar li img { @@ -85,21 +84,30 @@ 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; } -#progress .bar-read { +#progress .bar-read { color: #fff; background-color: #45b29d; } diff --git a/cps/static/css/libs/bootstrap-select.min.css b/cps/static/css/libs/bootstrap-select.min.css new file mode 100644 index 00000000..59708ed5 --- /dev/null +++ b/cps/static/css/libs/bootstrap-select.min.css @@ -0,0 +1,6 @@ +/*! + * Bootstrap-select v1.13.14 (https://developer.snapappointments.com/bootstrap-select) + * + * Copyright 2012-2020 SnapAppointments, LLC + * Licensed under MIT (https://github.com/snapappointments/bootstrap-select/blob/master/LICENSE) + */@-webkit-keyframes bs-notify-fadeOut{0%{opacity:.9}100%{opacity:0}}@-o-keyframes bs-notify-fadeOut{0%{opacity:.9}100%{opacity:0}}@keyframes bs-notify-fadeOut{0%{opacity:.9}100%{opacity:0}}.bootstrap-select>select.bs-select-hidden,select.bs-select-hidden,select.selectpicker{display:none!important}.bootstrap-select{width:220px\0;vertical-align:middle}.bootstrap-select>.dropdown-toggle{position:relative;width:100%;text-align:right;white-space:nowrap;display:-webkit-inline-box;display:-webkit-inline-flex;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between}.bootstrap-select>.dropdown-toggle:after{margin-top:-1px}.bootstrap-select>.dropdown-toggle.bs-placeholder,.bootstrap-select>.dropdown-toggle.bs-placeholder:active,.bootstrap-select>.dropdown-toggle.bs-placeholder:focus,.bootstrap-select>.dropdown-toggle.bs-placeholder:hover{color:#999}.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-danger,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-danger:active,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-danger:focus,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-danger:hover,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-dark,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-dark:active,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-dark:focus,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-dark:hover,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-info,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-info:active,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-info:focus,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-info:hover,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-primary,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-primary:active,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-primary:focus,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-primary:hover,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-secondary,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-secondary:active,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-secondary:focus,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-secondary:hover,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-success,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-success:active,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-success:focus,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-success:hover{color:rgba(255,255,255,.5)}.bootstrap-select>select{position:absolute!important;bottom:0;left:50%;display:block!important;width:.5px!important;height:100%!important;padding:0!important;opacity:0!important;border:none;z-index:0!important}.bootstrap-select>select.mobile-device{top:0;left:0;display:block!important;width:100%!important;z-index:2!important}.bootstrap-select.is-invalid .dropdown-toggle,.error .bootstrap-select .dropdown-toggle,.has-error .bootstrap-select .dropdown-toggle,.was-validated .bootstrap-select select:invalid+.dropdown-toggle{border-color:#b94a48}.bootstrap-select.is-valid .dropdown-toggle,.was-validated .bootstrap-select select:valid+.dropdown-toggle{border-color:#28a745}.bootstrap-select.fit-width{width:auto!important}.bootstrap-select:not([class*=col-]):not([class*=form-control]):not(.input-group-btn){width:220px}.bootstrap-select .dropdown-toggle:focus,.bootstrap-select>select.mobile-device:focus+.dropdown-toggle{outline:thin dotted #333!important;outline:5px auto -webkit-focus-ring-color!important;outline-offset:-2px}.bootstrap-select.form-control{margin-bottom:0;padding:0;border:none;height:auto}:not(.input-group)>.bootstrap-select.form-control:not([class*=col-]){width:100%}.bootstrap-select.form-control.input-group-btn{float:none;z-index:auto}.form-inline .bootstrap-select,.form-inline .bootstrap-select.form-control:not([class*=col-]){width:auto}.bootstrap-select:not(.input-group-btn),.bootstrap-select[class*=col-]{float:none;display:inline-block;margin-left:0}.bootstrap-select.dropdown-menu-right,.bootstrap-select[class*=col-].dropdown-menu-right,.row .bootstrap-select[class*=col-].dropdown-menu-right{float:right}.form-group .bootstrap-select,.form-horizontal .bootstrap-select,.form-inline .bootstrap-select{margin-bottom:0}.form-group-lg .bootstrap-select.form-control,.form-group-sm .bootstrap-select.form-control{padding:0}.form-group-lg .bootstrap-select.form-control .dropdown-toggle,.form-group-sm .bootstrap-select.form-control .dropdown-toggle{height:100%;font-size:inherit;line-height:inherit;border-radius:inherit}.bootstrap-select.form-control-lg .dropdown-toggle,.bootstrap-select.form-control-sm .dropdown-toggle{font-size:inherit;line-height:inherit;border-radius:inherit}.bootstrap-select.form-control-sm .dropdown-toggle{padding:.25rem .5rem}.bootstrap-select.form-control-lg .dropdown-toggle{padding:.5rem 1rem}.form-inline .bootstrap-select .form-control{width:100%}.bootstrap-select.disabled,.bootstrap-select>.disabled{cursor:not-allowed}.bootstrap-select.disabled:focus,.bootstrap-select>.disabled:focus{outline:0!important}.bootstrap-select.bs-container{position:absolute;top:0;left:0;height:0!important;padding:0!important}.bootstrap-select.bs-container .dropdown-menu{z-index:1060}.bootstrap-select .dropdown-toggle .filter-option{position:static;top:0;left:0;float:left;height:100%;width:100%;text-align:left;overflow:hidden;-webkit-box-flex:0;-webkit-flex:0 1 auto;-ms-flex:0 1 auto;flex:0 1 auto}.bs3.bootstrap-select .dropdown-toggle .filter-option{padding-right:inherit}.input-group .bs3-has-addon.bootstrap-select .dropdown-toggle .filter-option{position:absolute;padding-top:inherit;padding-bottom:inherit;padding-left:inherit;float:none}.input-group .bs3-has-addon.bootstrap-select .dropdown-toggle .filter-option .filter-option-inner{padding-right:inherit}.bootstrap-select .dropdown-toggle .filter-option-inner-inner{overflow:hidden}.bootstrap-select .dropdown-toggle .filter-expand{width:0!important;float:left;opacity:0!important;overflow:hidden}.bootstrap-select .dropdown-toggle .caret{position:absolute;top:50%;right:12px;margin-top:-2px;vertical-align:middle}.input-group .bootstrap-select.form-control .dropdown-toggle{border-radius:inherit}.bootstrap-select[class*=col-] .dropdown-toggle{width:100%}.bootstrap-select .dropdown-menu{min-width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.bootstrap-select .dropdown-menu>.inner:focus{outline:0!important}.bootstrap-select .dropdown-menu.inner{position:static;float:none;border:0;padding:0;margin:0;border-radius:0;-webkit-box-shadow:none;box-shadow:none}.bootstrap-select .dropdown-menu li{position:relative}.bootstrap-select .dropdown-menu li.active small{color:rgba(255,255,255,.5)!important}.bootstrap-select .dropdown-menu li.disabled a{cursor:not-allowed}.bootstrap-select .dropdown-menu li a{cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.bootstrap-select .dropdown-menu li a.opt{position:relative;padding-left:2.25em}.bootstrap-select .dropdown-menu li a span.check-mark{display:none}.bootstrap-select .dropdown-menu li a span.text{display:inline-block}.bootstrap-select .dropdown-menu li small{padding-left:.5em}.bootstrap-select .dropdown-menu .notify{position:absolute;bottom:5px;width:96%;margin:0 2%;min-height:26px;padding:3px 5px;background:#f5f5f5;border:1px solid #e3e3e3;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05);pointer-events:none;opacity:.9;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.bootstrap-select .dropdown-menu .notify.fadeOut{-webkit-animation:.3s linear 750ms forwards bs-notify-fadeOut;-o-animation:.3s linear 750ms forwards bs-notify-fadeOut;animation:.3s linear 750ms forwards bs-notify-fadeOut}.bootstrap-select .no-results{padding:3px;background:#f5f5f5;margin:0 5px;white-space:nowrap}.bootstrap-select.fit-width .dropdown-toggle .filter-option{position:static;display:inline;padding:0}.bootstrap-select.fit-width .dropdown-toggle .filter-option-inner,.bootstrap-select.fit-width .dropdown-toggle .filter-option-inner-inner{display:inline}.bootstrap-select.fit-width .dropdown-toggle .bs-caret:before{content:'\00a0'}.bootstrap-select.fit-width .dropdown-toggle .caret{position:static;top:auto;margin-top:-1px}.bootstrap-select.show-tick .dropdown-menu .selected span.check-mark{position:absolute;display:inline-block;right:15px;top:5px}.bootstrap-select.show-tick .dropdown-menu li a span.text{margin-right:34px}.bootstrap-select .bs-ok-default:after{content:'';display:block;width:.5em;height:1em;border-style:solid;border-width:0 .26em .26em 0;-webkit-transform:rotate(45deg);-ms-transform:rotate(45deg);-o-transform:rotate(45deg);transform:rotate(45deg)}.bootstrap-select.show-menu-arrow.open>.dropdown-toggle,.bootstrap-select.show-menu-arrow.show>.dropdown-toggle{z-index:1061}.bootstrap-select.show-menu-arrow .dropdown-toggle .filter-option:before{content:'';border-left:7px solid transparent;border-right:7px solid transparent;border-bottom:7px solid rgba(204,204,204,.2);position:absolute;bottom:-4px;left:9px;display:none}.bootstrap-select.show-menu-arrow .dropdown-toggle .filter-option:after{content:'';border-left:6px solid transparent;border-right:6px solid transparent;border-bottom:6px solid #fff;position:absolute;bottom:-4px;left:10px;display:none}.bootstrap-select.show-menu-arrow.dropup .dropdown-toggle .filter-option:before{bottom:auto;top:-4px;border-top:7px solid rgba(204,204,204,.2);border-bottom:0}.bootstrap-select.show-menu-arrow.dropup .dropdown-toggle .filter-option:after{bottom:auto;top:-4px;border-top:6px solid #fff;border-bottom:0}.bootstrap-select.show-menu-arrow.pull-right .dropdown-toggle .filter-option:before{right:12px;left:auto}.bootstrap-select.show-menu-arrow.pull-right .dropdown-toggle .filter-option:after{right:13px;left:auto}.bootstrap-select.show-menu-arrow.open>.dropdown-toggle .filter-option:after,.bootstrap-select.show-menu-arrow.open>.dropdown-toggle .filter-option:before,.bootstrap-select.show-menu-arrow.show>.dropdown-toggle .filter-option:after,.bootstrap-select.show-menu-arrow.show>.dropdown-toggle .filter-option:before{display:block}.bs-actionsbox,.bs-donebutton,.bs-searchbox{padding:4px 8px}.bs-actionsbox{width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.bs-actionsbox .btn-group button{width:50%}.bs-donebutton{float:left;width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.bs-donebutton .btn-group button{width:100%}.bs-searchbox+.bs-actionsbox{padding:0 8px 4px}.bs-searchbox .form-control{margin-bottom:0;width:100%;float:none} \ 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 72a8f74f..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.16.0 + * @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 .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:none;justify-content:center;position:absolute;bottom:0;width:100%;z-index:1000}.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{font-size:2rem;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 a{padding:6px 12px;line-height:1.428571429}.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}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..605cad57 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,151 @@ :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); + + --loading-icon: url(images/loading-dark.svg); + --treeitem-expanded-icon: url(images/treeitem-expanded-dark.svg); + --treeitem-collapsed-icon: url(images/treeitem-collapsed-dark.svg); + --toolbarButton-menuArrow-icon: url(images/toolbarButton-menuArrow-dark.svg); + --toolbarButton-sidebarToggle-icon: url(images/toolbarButton-sidebarToggle-dark.svg); + --toolbarButton-secondaryToolbarToggle-icon: url(images/toolbarButton-secondaryToolbarToggle-dark.svg); + --toolbarButton-pageUp-icon: url(images/toolbarButton-pageUp-dark.svg); + --toolbarButton-pageDown-icon: url(images/toolbarButton-pageDown-dark.svg); + --toolbarButton-zoomOut-icon: url(images/toolbarButton-zoomOut-dark.svg); + --toolbarButton-zoomIn-icon: url(images/toolbarButton-zoomIn-dark.svg); + --toolbarButton-presentationMode-icon: url(images/toolbarButton-presentationMode-dark.svg); + --toolbarButton-print-icon: url(images/toolbarButton-print-dark.svg); + --toolbarButton-openFile-icon: url(images/toolbarButton-openFile-dark.svg); + --toolbarButton-download-icon: url(images/toolbarButton-download-dark.svg); + --toolbarButton-bookmark-icon: url(images/toolbarButton-bookmark-dark.svg); + --toolbarButton-viewThumbnail-icon: url(images/toolbarButton-viewThumbnail-dark.svg); + --toolbarButton-viewOutline-icon: url(images/toolbarButton-viewOutline-dark.svg); + --toolbarButton-viewAttachments-icon: url(images/toolbarButton-viewAttachments-dark.svg); + --toolbarButton-viewLayers-icon: url(images/toolbarButton-viewLayers-dark.svg); + --toolbarButton-search-icon: url(images/toolbarButton-search-dark.svg); + --findbarButton-previous-icon: url(images/findbarButton-previous-dark.svg); + --findbarButton-next-icon: url(images/findbarButton-next-dark.svg); + --secondaryToolbarButton-firstPage-icon: url(images/secondaryToolbarButton-firstPage-dark.svg); + --secondaryToolbarButton-lastPage-icon: url(images/secondaryToolbarButton-lastPage-dark.svg); + --secondaryToolbarButton-rotateCcw-icon: url(images/secondaryToolbarButton-rotateCcw-dark.svg); + --secondaryToolbarButton-rotateCw-icon: url(images/secondaryToolbarButton-rotateCw-dark.svg); + --secondaryToolbarButton-selectTool-icon: url(images/secondaryToolbarButton-selectTool-dark.svg); + --secondaryToolbarButton-handTool-icon: url(images/secondaryToolbarButton-handTool-dark.svg); + --secondaryToolbarButton-scrollVertical-icon: url(images/secondaryToolbarButton-scrollVertical-dark.svg); + --secondaryToolbarButton-scrollHorizontal-icon: url(images/secondaryToolbarButton-scrollHorizontal-dark.svg); + --secondaryToolbarButton-scrollWrapped-icon: url(images/secondaryToolbarButton-scrollWrapped-dark.svg); + --secondaryToolbarButton-spreadNone-icon: url(images/secondaryToolbarButton-spreadNone-dark.svg); + --secondaryToolbarButton-spreadOdd-icon: url(images/secondaryToolbarButton-spreadOdd-dark.svg); + --secondaryToolbarButton-spreadEven-icon: url(images/secondaryToolbarButton-spreadEven-dark.svg); + --secondaryToolbarButton-documentProperties-icon: url(images/secondaryToolbarButton-documentProperties-dark.svg); + } } * { @@ -422,16 +569,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 +744,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 +775,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 +787,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 +799,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 +858,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 +885,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 +897,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 +920,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 +942,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 +983,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 +1018,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 +1144,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 +1172,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 +1480,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 +1500,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 +1617,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 +1736,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 +1785,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 +1928,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 +1979,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 +2122,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' background */ + background-color: rgba(215, 215, 219, 1); + background-color: var(--dropdown-btn-bg-color); +} + +@media (prefers-color-scheme: dark) { + + .dropdownToolbarButton > select { + background-color: rgba(74, 74, 79, 1); + background-color: var(--dropdown-btn-bg-color); + } +} + +@media (prefers-color-scheme: dark) { + + .dropdownToolbarButton > select { + color: rgba(249, 249, 250, 1); + color: var(--main-color); + } +} +html[dir="ltr"] .dropdownToolbarButton > select { + padding-left: 4px; +} +html[dir="rtl"] .dropdownToolbarButton > select { + padding-right: 4px; +} +.dropdownToolbarButton > select:hover { + background-color: rgba(221, 222, 223, 1); + background-color: var(--button-hover-color); +} +@media (prefers-color-scheme: dark) { + + .dropdownToolbarButton > select:hover { + background-color: rgba(102, 102, 103, 1); + background-color: var(--button-hover-color); + } +} + +.dropdownToolbarButton > select:focus { + background-color: rgba(221, 222, 223, 1); + background-color: var(--button-hover-color); +} + +@media (prefers-color-scheme: dark) { + + .dropdownToolbarButton > select:focus { + background-color: rgba(102, 102, 103, 1); + background-color: var(--button-hover-color); + } } .dropdownToolbarButton > select > option { - background: hsl(0,0%,24%); + background: rgba(255, 255, 255, 1); + background: var(--doorhanger-bg-color); +} + +@media (prefers-color-scheme: dark) { + + .dropdownToolbarButton > select > option { + background: rgba(74, 74, 79, 1); + background: var(--doorhanger-bg-color); + } } #customScaleOption { @@ -1248,20 +2437,7 @@ html[dir='rtl'] .dropdownToolbarButton { } #pageWidthOption { - border-bottom: 1px rgba(255, 255, 255, .5) solid; -} - -html[dir='ltr'] .splitToolbarButton:first-child, -html[dir='ltr'] .toolbarButton:first-child, -html[dir='rtl'] .splitToolbarButton:last-child, -html[dir='rtl'] .toolbarButton:last-child { - margin-left: 4px; -} -html[dir='ltr'] .splitToolbarButton:last-child, -html[dir='ltr'] .toolbarButton:last-child, -html[dir='rtl'] .splitToolbarButton:first-child, -html[dir='rtl'] .toolbarButton:first-child { - margin-right: 4px; + border-bottom: 1px rgba(255, 255, 255, 0.5) solid; } .toolbarButtonSpacer { @@ -1270,145 +2446,377 @@ html[dir='rtl'] .toolbarButton:first-child { height: 1px; } -html[dir='ltr'] #findPrevious { - margin-left: 3px; +html[dir="ltr"] #findPrevious { + margin-left: 0; } -html[dir='ltr'] #findNext { +html[dir="ltr"] #findNext { margin-right: 3px; } -html[dir='rtl'] #findPrevious { - margin-right: 3px; +html[dir="rtl"] #findPrevious { + margin-right: 0; } -html[dir='rtl'] #findNext { +html[dir="rtl"] #findNext { margin-left: 3px; } +.toolbarButton::before { + opacity: 0.7; + opacity: var(--toolbar-icon-opacity); + top: 6px; +} + +.secondaryToolbarButton::before { + opacity: 0.9; + opacity: var(--doorhanger-icon-opacity); + top: 5px; +} + .toolbarButton::before, .secondaryToolbarButton::before { /* All matching images have a size of 16x16 - * All relevant containers have a size of 32x25 */ + * All relevant containers have a size of 28x28 */ position: absolute; display: inline-block; - top: 4px; - left: 7px; + left: 6px; + max-width: 16px; } html[dir="ltr"] .secondaryToolbarButton::before { - left: 4px; + left: 12px; } html[dir="rtl"] .secondaryToolbarButton::before { - right: 4px; + right: 12px; } -html[dir='ltr'] .toolbarButton#sidebarToggle::before { - content: url(images/toolbarButton-sidebarToggle.png); -} -html[dir='rtl'] .toolbarButton#sidebarToggle::before { - content: url(images/toolbarButton-sidebarToggle-rtl.png); +.toolbarButton#sidebarToggle::before { + content: url(images/toolbarButton-sidebarToggle.svg); + content: var(--toolbarButton-sidebarToggle-icon); } -html[dir='ltr'] .toolbarButton#secondaryToolbarToggle::before { - content: url(images/toolbarButton-secondaryToolbarToggle.png); +@media (prefers-color-scheme: dark) { + + .toolbarButton#sidebarToggle::before { + content: url(images/toolbarButton-sidebarToggle-dark.svg); + content: var(--toolbarButton-sidebarToggle-icon); + } } -html[dir='rtl'] .toolbarButton#secondaryToolbarToggle::before { - content: url(images/toolbarButton-secondaryToolbarToggle-rtl.png); +html[dir="rtl"] .toolbarButton#sidebarToggle::before { + transform: scaleX(-1); } -html[dir='ltr'] .toolbarButton.findPrevious::before { - content: url(images/findbarButton-previous.png); -} -html[dir='rtl'] .toolbarButton.findPrevious::before { - content: url(images/findbarButton-previous-rtl.png); +.toolbarButton#secondaryToolbarToggle::before { + content: url(images/toolbarButton-secondaryToolbarToggle.svg); + content: var(--toolbarButton-secondaryToolbarToggle-icon); } -html[dir='ltr'] .toolbarButton.findNext::before { - content: url(images/findbarButton-next.png); +@media (prefers-color-scheme: dark) { + + .toolbarButton#secondaryToolbarToggle::before { + content: url(images/toolbarButton-secondaryToolbarToggle-dark.svg); + content: var(--toolbarButton-secondaryToolbarToggle-icon); + } } -html[dir='rtl'] .toolbarButton.findNext::before { - content: url(images/findbarButton-next-rtl.png); +html[dir="rtl"] .toolbarButton#secondaryToolbarToggle::before { + transform: scaleX(-1); } -html[dir='ltr'] .toolbarButton.pageUp::before { - content: url(images/toolbarButton-pageUp.png); -} -html[dir='rtl'] .toolbarButton.pageUp::before { - content: url(images/toolbarButton-pageUp-rtl.png); +.toolbarButton.findPrevious::before { + content: url(images/findbarButton-previous.svg); + content: var(--findbarButton-previous-icon); } -html[dir='ltr'] .toolbarButton.pageDown::before { - content: url(images/toolbarButton-pageDown.png); +@media (prefers-color-scheme: dark) { + + .toolbarButton.findPrevious::before { + content: url(images/findbarButton-previous-dark.svg); + content: var(--findbarButton-previous-icon); + } } -html[dir='rtl'] .toolbarButton.pageDown::before { - content: url(images/toolbarButton-pageDown-rtl.png); +html[dir="rtl"] .toolbarButton.findPrevious::before { + transform: scaleX(-1); +} + +.toolbarButton.findNext::before { + content: url(images/findbarButton-next.svg); + content: var(--findbarButton-next-icon); +} + +@media (prefers-color-scheme: dark) { + + .toolbarButton.findNext::before { + content: url(images/findbarButton-next-dark.svg); + content: var(--findbarButton-next-icon); + } +} +html[dir="rtl"] .toolbarButton.findNext::before { + transform: scaleX(-1); +} + +.toolbarButton.pageUp::before { + content: url(images/toolbarButton-pageUp.svg); + content: var(--toolbarButton-pageUp-icon); +} + +@media (prefers-color-scheme: dark) { + + .toolbarButton.pageUp::before { + content: url(images/toolbarButton-pageUp-dark.svg); + content: var(--toolbarButton-pageUp-icon); + } +} +html[dir="rtl"] .toolbarButton.pageUp::before { + transform: scaleX(-1); +} + +.toolbarButton.pageDown::before { + content: url(images/toolbarButton-pageDown.svg); + content: var(--toolbarButton-pageDown-icon); +} + +@media (prefers-color-scheme: dark) { + + .toolbarButton.pageDown::before { + content: url(images/toolbarButton-pageDown-dark.svg); + content: var(--toolbarButton-pageDown-icon); + } +} +html[dir="rtl"] .toolbarButton.pageDown::before { + transform: scaleX(-1); } .toolbarButton.zoomOut::before { - content: url(images/toolbarButton-zoomOut.png); + content: url(images/toolbarButton-zoomOut.svg); + content: var(--toolbarButton-zoomOut-icon); +} + +@media (prefers-color-scheme: dark) { + + .toolbarButton.zoomOut::before { + content: url(images/toolbarButton-zoomOut-dark.svg); + content: var(--toolbarButton-zoomOut-icon); + } } .toolbarButton.zoomIn::before { - content: url(images/toolbarButton-zoomIn.png); + content: url(images/toolbarButton-zoomIn.svg); + content: var(--toolbarButton-zoomIn-icon); +} + +@media (prefers-color-scheme: dark) { + + .toolbarButton.zoomIn::before { + content: url(images/toolbarButton-zoomIn-dark.svg); + content: var(--toolbarButton-zoomIn-icon); + } +} + +.toolbarButton.presentationMode::before { + content: url(images/toolbarButton-presentationMode.svg); + content: var(--toolbarButton-presentationMode-icon); +} + +@media (prefers-color-scheme: dark) { + + .toolbarButton.presentationMode::before { + content: url(images/toolbarButton-presentationMode-dark.svg); + content: var(--toolbarButton-presentationMode-icon); + } } -.toolbarButton.presentationMode::before, .secondaryToolbarButton.presentationMode::before { - content: url(images/toolbarButton-presentationMode.png); + content: url(images/toolbarButton-presentationMode.svg); + content: var(--toolbarButton-presentationMode-icon); +} + +@media (prefers-color-scheme: dark) { + + .secondaryToolbarButton.presentationMode::before { + content: url(images/toolbarButton-presentationMode-dark.svg); + content: var(--toolbarButton-presentationMode-icon); + } +} + +.toolbarButton.print::before { + content: url(images/toolbarButton-print.svg); + content: var(--toolbarButton-print-icon); +} + +@media (prefers-color-scheme: dark) { + + .toolbarButton.print::before { + content: url(images/toolbarButton-print-dark.svg); + content: var(--toolbarButton-print-icon); + } } -.toolbarButton.print::before, .secondaryToolbarButton.print::before { - content: url(images/toolbarButton-print.png); + content: url(images/toolbarButton-print.svg); + content: var(--toolbarButton-print-icon); +} + +@media (prefers-color-scheme: dark) { + + .secondaryToolbarButton.print::before { + content: url(images/toolbarButton-print-dark.svg); + content: var(--toolbarButton-print-icon); + } +} + +.toolbarButton.openFile::before { + content: url(images/toolbarButton-openFile.svg); + content: var(--toolbarButton-openFile-icon); +} + +@media (prefers-color-scheme: dark) { + + .toolbarButton.openFile::before { + content: url(images/toolbarButton-openFile-dark.svg); + content: var(--toolbarButton-openFile-icon); + } } -.toolbarButton.openFile::before, .secondaryToolbarButton.openFile::before { - content: url(images/toolbarButton-openFile.png); + content: url(images/toolbarButton-openFile.svg); + content: var(--toolbarButton-openFile-icon); +} + +@media (prefers-color-scheme: dark) { + + .secondaryToolbarButton.openFile::before { + content: url(images/toolbarButton-openFile-dark.svg); + content: var(--toolbarButton-openFile-icon); + } +} + +.toolbarButton.download::before { + content: url(images/toolbarButton-download.svg); + content: var(--toolbarButton-download-icon); +} + +@media (prefers-color-scheme: dark) { + + .toolbarButton.download::before { + content: url(images/toolbarButton-download-dark.svg); + content: var(--toolbarButton-download-icon); + } } -.toolbarButton.download::before, .secondaryToolbarButton.download::before { - content: url(images/toolbarButton-download.png); + content: url(images/toolbarButton-download.svg); + content: var(--toolbarButton-download-icon); +} + +@media (prefers-color-scheme: dark) { + + .secondaryToolbarButton.download::before { + content: url(images/toolbarButton-download-dark.svg); + content: var(--toolbarButton-download-icon); + } } -.toolbarButton.bookmark, .secondaryToolbarButton.bookmark { - box-sizing: border-box; - outline: none; - padding-top: 4px; + padding-top: 6px; text-decoration: none; } -.secondaryToolbarButton.bookmark { - padding-top: 5px; -} -.bookmark[href='#'] { - opacity: .5; +.bookmark[href="#"] { + opacity: 0.5; pointer-events: none; } -.toolbarButton.bookmark::before, +.toolbarButton.bookmark::before { + content: url(images/toolbarButton-bookmark.svg); + content: var(--toolbarButton-bookmark-icon); +} + +@media (prefers-color-scheme: dark) { + + .toolbarButton.bookmark::before { + content: url(images/toolbarButton-bookmark-dark.svg); + content: var(--toolbarButton-bookmark-icon); + } +} + .secondaryToolbarButton.bookmark::before { - content: url(images/toolbarButton-bookmark.png); + content: url(images/toolbarButton-bookmark.svg); + content: var(--toolbarButton-bookmark-icon); +} + +@media (prefers-color-scheme: dark) { + + .secondaryToolbarButton.bookmark::before { + content: url(images/toolbarButton-bookmark-dark.svg); + content: var(--toolbarButton-bookmark-icon); + } } #viewThumbnail.toolbarButton::before { - content: url(images/toolbarButton-viewThumbnail.png); + content: url(images/toolbarButton-viewThumbnail.svg); + content: var(--toolbarButton-viewThumbnail-icon); } -html[dir="ltr"] #viewOutline.toolbarButton::before { - content: url(images/toolbarButton-viewOutline.png); +@media (prefers-color-scheme: dark) { + + #viewThumbnail.toolbarButton::before { + content: url(images/toolbarButton-viewThumbnail-dark.svg); + content: var(--toolbarButton-viewThumbnail-icon); + } +} + +#viewOutline.toolbarButton::before { + content: url(images/toolbarButton-viewOutline.svg); + content: var(--toolbarButton-viewOutline-icon); +} + +@media (prefers-color-scheme: dark) { + + #viewOutline.toolbarButton::before { + content: url(images/toolbarButton-viewOutline-dark.svg); + content: var(--toolbarButton-viewOutline-icon); + } } html[dir="rtl"] #viewOutline.toolbarButton::before { - content: url(images/toolbarButton-viewOutline-rtl.png); + transform: scaleX(-1); } #viewAttachments.toolbarButton::before { - content: url(images/toolbarButton-viewAttachments.png); + content: url(images/toolbarButton-viewAttachments.svg); + content: var(--toolbarButton-viewAttachments-icon); +} + +@media (prefers-color-scheme: dark) { + + #viewAttachments.toolbarButton::before { + content: url(images/toolbarButton-viewAttachments-dark.svg); + content: var(--toolbarButton-viewAttachments-icon); + } +} + +#viewLayers.toolbarButton::before { + content: url(images/toolbarButton-viewLayers.svg); + content: var(--toolbarButton-viewLayers-icon); +} + +@media (prefers-color-scheme: dark) { + + #viewLayers.toolbarButton::before { + content: url(images/toolbarButton-viewLayers-dark.svg); + content: var(--toolbarButton-viewLayers-icon); + } } #viewFind.toolbarButton::before { - content: url(images/toolbarButton-search.png); + content: url(images/toolbarButton-search.svg); + content: var(--toolbarButton-search-icon); +} + +@media (prefers-color-scheme: dark) { + + #viewFind.toolbarButton::before { + content: url(images/toolbarButton-search-dark.svg); + content: var(--toolbarButton-search-icon); + } } .toolbarButton.pdfSidebarNotification::after { @@ -1416,43 +2824,39 @@ html[dir="rtl"] #viewOutline.toolbarButton::before { display: inline-block; top: 1px; /* Create a filled circle, with a diameter of 9 pixels, using only CSS: */ - content: ''; - background-color: #70DB55; + content: ""; + background-color: rgba(112, 219, 85, 1); height: 9px; width: 9px; border-radius: 50%; } -html[dir='ltr'] .toolbarButton.pdfSidebarNotification::after { +html[dir="ltr"] .toolbarButton.pdfSidebarNotification::after { left: 17px; } -html[dir='rtl'] .toolbarButton.pdfSidebarNotification::after { +html[dir="rtl"] .toolbarButton.pdfSidebarNotification::after { right: 17px; } .secondaryToolbarButton { position: relative; - margin: 0 0 4px 0; - padding: 3px 0 1px 0; + margin: 0; + padding: 0 0 1px 0; height: auto; - min-height: 25px; + min-height: 26px; width: auto; min-width: 100%; white-space: normal; + border-radius: 0; + box-sizing: border-box; } html[dir="ltr"] .secondaryToolbarButton { - padding-left: 24px; + padding-left: 36px; text-align: left; } html[dir="rtl"] .secondaryToolbarButton { - padding-right: 24px; + padding-right: 36px; text-align: right; } -html[dir="ltr"] .secondaryToolbarButton.bookmark { - padding-left: 27px; -} -html[dir="rtl"] .secondaryToolbarButton.bookmark { - padding-right: 27px; -} html[dir="ltr"] .secondaryToolbarButton > span { padding-right: 4px; @@ -1462,109 +2866,269 @@ html[dir="rtl"] .secondaryToolbarButton > span { } .secondaryToolbarButton.firstPage::before { - content: url(images/secondaryToolbarButton-firstPage.png); + content: url(images/secondaryToolbarButton-firstPage.svg); + content: var(--secondaryToolbarButton-firstPage-icon); +} + +@media (prefers-color-scheme: dark) { + + .secondaryToolbarButton.firstPage::before { + content: url(images/secondaryToolbarButton-firstPage-dark.svg); + content: var(--secondaryToolbarButton-firstPage-icon); + } } .secondaryToolbarButton.lastPage::before { - content: url(images/secondaryToolbarButton-lastPage.png); + content: url(images/secondaryToolbarButton-lastPage.svg); + content: var(--secondaryToolbarButton-lastPage-icon); +} + +@media (prefers-color-scheme: dark) { + + .secondaryToolbarButton.lastPage::before { + content: url(images/secondaryToolbarButton-lastPage-dark.svg); + content: var(--secondaryToolbarButton-lastPage-icon); + } } .secondaryToolbarButton.rotateCcw::before { - content: url(images/secondaryToolbarButton-rotateCcw.png); + content: url(images/secondaryToolbarButton-rotateCcw.svg); + content: var(--secondaryToolbarButton-rotateCcw-icon); +} + +@media (prefers-color-scheme: dark) { + + .secondaryToolbarButton.rotateCcw::before { + content: url(images/secondaryToolbarButton-rotateCcw-dark.svg); + content: var(--secondaryToolbarButton-rotateCcw-icon); + } } .secondaryToolbarButton.rotateCw::before { - content: url(images/secondaryToolbarButton-rotateCw.png); + content: url(images/secondaryToolbarButton-rotateCw.svg); + content: var(--secondaryToolbarButton-rotateCw-icon); +} + +@media (prefers-color-scheme: dark) { + + .secondaryToolbarButton.rotateCw::before { + content: url(images/secondaryToolbarButton-rotateCw-dark.svg); + content: var(--secondaryToolbarButton-rotateCw-icon); + } } .secondaryToolbarButton.selectTool::before { - content: url(images/secondaryToolbarButton-selectTool.png); + content: url(images/secondaryToolbarButton-selectTool.svg); + content: var(--secondaryToolbarButton-selectTool-icon); +} + +@media (prefers-color-scheme: dark) { + + .secondaryToolbarButton.selectTool::before { + content: url(images/secondaryToolbarButton-selectTool-dark.svg); + content: var(--secondaryToolbarButton-selectTool-icon); + } } .secondaryToolbarButton.handTool::before { - content: url(images/secondaryToolbarButton-handTool.png); + content: url(images/secondaryToolbarButton-handTool.svg); + content: var(--secondaryToolbarButton-handTool-icon); +} + +@media (prefers-color-scheme: dark) { + + .secondaryToolbarButton.handTool::before { + content: url(images/secondaryToolbarButton-handTool-dark.svg); + content: var(--secondaryToolbarButton-handTool-icon); + } } .secondaryToolbarButton.scrollVertical::before { - content: url(images/secondaryToolbarButton-scrollVertical.png); + content: url(images/secondaryToolbarButton-scrollVertical.svg); + content: var(--secondaryToolbarButton-scrollVertical-icon); +} + +@media (prefers-color-scheme: dark) { + + .secondaryToolbarButton.scrollVertical::before { + content: url(images/secondaryToolbarButton-scrollVertical-dark.svg); + content: var(--secondaryToolbarButton-scrollVertical-icon); + } } .secondaryToolbarButton.scrollHorizontal::before { - content: url(images/secondaryToolbarButton-scrollHorizontal.png); + content: url(images/secondaryToolbarButton-scrollHorizontal.svg); + content: var(--secondaryToolbarButton-scrollHorizontal-icon); +} + +@media (prefers-color-scheme: dark) { + + .secondaryToolbarButton.scrollHorizontal::before { + content: url(images/secondaryToolbarButton-scrollHorizontal-dark.svg); + content: var(--secondaryToolbarButton-scrollHorizontal-icon); + } } .secondaryToolbarButton.scrollWrapped::before { - content: url(images/secondaryToolbarButton-scrollWrapped.png); + content: url(images/secondaryToolbarButton-scrollWrapped.svg); + content: var(--secondaryToolbarButton-scrollWrapped-icon); +} + +@media (prefers-color-scheme: dark) { + + .secondaryToolbarButton.scrollWrapped::before { + content: url(images/secondaryToolbarButton-scrollWrapped-dark.svg); + content: var(--secondaryToolbarButton-scrollWrapped-icon); + } } .secondaryToolbarButton.spreadNone::before { - content: url(images/secondaryToolbarButton-spreadNone.png); + content: url(images/secondaryToolbarButton-spreadNone.svg); + content: var(--secondaryToolbarButton-spreadNone-icon); +} + +@media (prefers-color-scheme: dark) { + + .secondaryToolbarButton.spreadNone::before { + content: url(images/secondaryToolbarButton-spreadNone-dark.svg); + content: var(--secondaryToolbarButton-spreadNone-icon); + } } .secondaryToolbarButton.spreadOdd::before { - content: url(images/secondaryToolbarButton-spreadOdd.png); + content: url(images/secondaryToolbarButton-spreadOdd.svg); + content: var(--secondaryToolbarButton-spreadOdd-icon); +} + +@media (prefers-color-scheme: dark) { + + .secondaryToolbarButton.spreadOdd::before { + content: url(images/secondaryToolbarButton-spreadOdd-dark.svg); + content: var(--secondaryToolbarButton-spreadOdd-icon); + } } .secondaryToolbarButton.spreadEven::before { - content: url(images/secondaryToolbarButton-spreadEven.png); + content: url(images/secondaryToolbarButton-spreadEven.svg); + content: var(--secondaryToolbarButton-spreadEven-icon); +} + +@media (prefers-color-scheme: dark) { + + .secondaryToolbarButton.spreadEven::before { + content: url(images/secondaryToolbarButton-spreadEven-dark.svg); + content: var(--secondaryToolbarButton-spreadEven-icon); + } } .secondaryToolbarButton.documentProperties::before { - content: url(images/secondaryToolbarButton-documentProperties.png); + content: url(images/secondaryToolbarButton-documentProperties.svg); + content: var(--secondaryToolbarButton-documentProperties-icon); +} + +@media (prefers-color-scheme: dark) { + + .secondaryToolbarButton.documentProperties::before { + content: url(images/secondaryToolbarButton-documentProperties-dark.svg); + content: var(--secondaryToolbarButton-documentProperties-icon); + } } .verticalToolbarSeparator { display: block; - padding: 8px 0; - margin: 8px 4px; + padding: 11px 0; + margin: 5px 2px; width: 1px; - background-color: hsla(0,0%,0%,.5); - box-shadow: 0 0 0 1px hsla(0,0%,100%,.08); + background-color: rgba(0, 0, 0, 0.3); + background-color: var(--separator-color); } -html[dir='ltr'] .verticalToolbarSeparator { + +@media (prefers-color-scheme: dark) { + + .verticalToolbarSeparator { + background-color: rgba(0, 0, 0, 0.3); + background-color: var(--separator-color); + } +} +html[dir="ltr"] .verticalToolbarSeparator { margin-left: 2px; } -html[dir='rtl'] .verticalToolbarSeparator { +html[dir="rtl"] .verticalToolbarSeparator { margin-right: 2px; } .horizontalToolbarSeparator { display: block; - margin: 0 0 4px 0; + margin: 6px 0 5px 0; height: 1px; width: 100%; - background-color: hsla(0,0%,0%,.5); - box-shadow: 0 0 0 1px hsla(0,0%,100%,.08); + border-top: 1px solid rgba(222, 222, 222, 1); + border-top: 1px solid var(--doorhanger-separator-color); +} + +@media (prefers-color-scheme: dark) { + + .horizontalToolbarSeparator { + border-top: 1px solid rgba(92, 92, 97, 1); + border-top: 1px solid var(--doorhanger-separator-color); + } } .toolbarField { - padding: 3px 6px; - margin: 4px 0 4px 0; - border: 1px solid transparent; + padding: 4px 7px; + margin: 3px 0 3px 0; border-radius: 2px; - background-color: hsla(0,0%,100%,.09); - 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-color: rgba(255, 255, 255, 1); + background-color: var(--field-bg-color); 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%,0%,.05) inset, - 0 1px 0 hsla(0,0%,100%,.05); - color: hsl(0,0%,95%); + border-width: 1px; + border-style: solid; + border-color: rgba(187, 187, 188, 1); + border-color: var(--field-border-color); + box-shadow: none; + color: rgba(6, 6, 6, 1); + color: var(--field-color); font-size: 12px; - line-height: 14px; + line-height: 16px; outline-style: none; - -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; } -.toolbarField[type=checkbox] { - display: inline-block; - margin: 8px 0px; +@media (prefers-color-scheme: dark) { + + .toolbarField { + color: rgba(250, 250, 250, 1); + color: var(--field-color); + } +} + +@media (prefers-color-scheme: dark) { + + .toolbarField { + border-color: rgba(115, 115, 115, 1); + border-color: var(--field-border-color); + } +} + +@media (prefers-color-scheme: dark) { + + .toolbarField { + background-color: rgba(64, 64, 68, 1); + background-color: var(--field-bg-color); + } +} + +.toolbarField[type="checkbox"] { + opacity: 0; + position: absolute !important; + left: 0; +} + +html[dir="ltr"] .toolbarField[type="checkbox"] { + margin: 10px 0 3px 7px; +} + +html[dir="rtl"] .toolbarField[type="checkbox"] { + margin: 10px 7px 3px 0; } .toolbarField.pageNumber { @@ -1575,34 +3139,38 @@ html[dir='rtl'] .verticalToolbarSeparator { } .toolbarField.pageNumber.visiblePageIsLoading { - background-image: url(images/loading-small.png); + background-image: url(images/loading.svg); + background-image: var(--loading-icon); background-repeat: no-repeat; - background-position: 1px; + background-position: 3px; +} + +@media (prefers-color-scheme: dark) { + + .toolbarField.pageNumber.visiblePageIsLoading { + background-image: url(images/loading-dark.svg); + background-image: var(--loading-icon); + } } .toolbarField.pageNumber::-webkit-inner-spin-button, .toolbarField.pageNumber::-webkit-outer-spin-button { - -webkit-appearance: none; - margin: 0; -} - -.toolbarField:hover { - background-color: hsla(0,0%,100%,.11); - border-color: hsla(0,0%,0%,.4) hsla(0,0%,0%,.43) hsla(0,0%,0%,.45); + -webkit-appearance: none; + margin: 0; } .toolbarField:focus { - background-color: hsla(0,0%,100%,.15); - border-color: hsla(204,100%,65%,.8) hsla(204,100%,65%,.85) hsla(204,100%,65%,.9); + border-color: #0a84ff; } .toolbarLabel { min-width: 16px; - padding: 3px 6px 3px 2px; - margin: 4px 2px 4px 0; - border: 1px solid transparent; + padding: 6px; + margin: 2px; + border: 1px solid rgba(0, 0, 0, 0); border-radius: 2px; - color: hsl(0,0%,85%); + color: rgba(12, 12, 13, 1); + color: var(--main-color); font-size: 12px; line-height: 14px; text-align: left; @@ -1613,6 +3181,21 @@ html[dir='rtl'] .verticalToolbarSeparator { cursor: default; } +@media (prefers-color-scheme: dark) { + + .toolbarLabel { + color: rgba(249, 249, 250, 1); + color: var(--main-color); + } +} + +html[dir="ltr"] #numPages.toolbarLabel { + padding-left: 2px; +} +html[dir="rtl"] #numPages.toolbarLabel { + padding-right: 2px; +} + #thumbnailView { position: absolute; width: calc(100% - 60px); @@ -1631,10 +3214,10 @@ html[dir='rtl'] .verticalToolbarSeparator { .thumbnail { margin: 0 10px 5px 10px; } -html[dir='ltr'] .thumbnail { +html[dir="ltr"] .thumbnail { float: left; } -html[dir='rtl'] .thumbnail { +html[dir="rtl"] .thumbnail { float: right; } @@ -1647,16 +3230,16 @@ html[dir='rtl'] .thumbnail { } .thumbnail:not([data-loaded]) { - border: 1px dashed rgba(255, 255, 255, 0.5); + border: 1px dashed rgba(132, 132, 132, 1); margin: -1px 9px 4px 9px; } .thumbnailImage { - border: 1px solid transparent; + border: 1px solid rgba(0, 0, 0, 0); box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.5), 0 2px 8px rgba(0, 0, 0, 0.3); opacity: 0.8; z-index: 99; - background-color: white; + background-color: rgba(255, 255, 255, 1); background-clip: content-box; } @@ -1667,43 +3250,66 @@ html[dir='rtl'] .thumbnail { a:focus > .thumbnail > .thumbnailSelectionRing > .thumbnailImage, .thumbnail:hover > .thumbnailSelectionRing > .thumbnailImage { - opacity: .9; + opacity: 0.9; } -a:focus > .thumbnail > .thumbnailSelectionRing, -.thumbnail:hover > .thumbnailSelectionRing { - background-color: hsla(0,0%,100%,.15); - 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)); +a:focus > .thumbnail > .thumbnailSelectionRing { + background-color: rgba(0, 0, 0, 0.15); + background-color: var(--sidebaritem-bg-color); background-clip: padding-box; - box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset, - 0 0 1px hsla(0,0%,100%,.2) inset, - 0 0 1px hsla(0,0%,0%,.2); - color: hsla(0,0%,100%,.9); + color: rgba(255, 255, 255, 0.9); +} + +@media (prefers-color-scheme: dark) { + + a:focus > .thumbnail > .thumbnailSelectionRing { + background-color: rgba(255, 255, 255, 0.15); + background-color: var(--sidebaritem-bg-color); + } +} + +.thumbnail:hover > .thumbnailSelectionRing { + background-color: rgba(0, 0, 0, 0.15); + background-color: var(--sidebaritem-bg-color); + background-clip: padding-box; + color: rgba(255, 255, 255, 0.9); +} + +@media (prefers-color-scheme: dark) { + + .thumbnail:hover > .thumbnailSelectionRing { + background-color: rgba(255, 255, 255, 0.15); + background-color: var(--sidebaritem-bg-color); + } } .thumbnail.selected > .thumbnailSelectionRing > .thumbnailImage { - box-shadow: 0 0 0 1px hsla(0,0%,0%,.5); opacity: 1; } .thumbnail.selected > .thumbnailSelectionRing { - background-color: hsla(0,0%,100%,.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)); + background-color: rgba(0, 0, 0, 0.15); + background-color: var(--sidebaritem-bg-color); background-clip: padding-box; - box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset, - 0 0 1px hsla(0,0%,100%,.1) inset, - 0 0 1px hsla(0,0%,0%,.2); - color: hsla(0,0%,100%,1); + color: rgba(255, 255, 255, 1); +} + +@media (prefers-color-scheme: dark) { + + .thumbnail.selected > .thumbnailSelectionRing { + background-color: rgba(255, 255, 255, 0.15); + background-color: var(--sidebaritem-bg-color); + } } #outlineView, -#attachmentsView { +#attachmentsView, +#layersView { position: absolute; width: calc(100% - 8px); top: 0; bottom: 0; + padding: 4px 4px 0; overflow: auto; -webkit-overflow-scrolling: touch; -webkit-user-select: none; @@ -1712,34 +3318,27 @@ a:focus > .thumbnail > .thumbnailSelectionRing, user-select: none; } -#outlineView { - padding: 4px 4px 0; -} -#attachmentsView { - padding: 3px 4px 0; -} - -html[dir='ltr'] .outlineWithDeepNesting > .outlineItem, -html[dir='ltr'] .outlineItem > .outlineItems { +html[dir="ltr"] .treeWithDeepNesting > .treeItem, +html[dir="ltr"] .treeItem > .treeItems { margin-left: 20px; } -html[dir='rtl'] .outlineWithDeepNesting > .outlineItem, -html[dir='rtl'] .outlineItem > .outlineItems { +html[dir="rtl"] .treeWithDeepNesting > .treeItem, +html[dir="rtl"] .treeItem > .treeItems { margin-right: 20px; } -.outlineItem > a, -.attachmentsItem > button { +.treeItem > a { text-decoration: none; display: inline-block; min-width: 95%; - min-width: calc(100% - 4px); /* Subtract the right padding (left, in RTL mode) - of the container. */ + /* Subtract the right padding (left, in RTL mode) of the container: */ + min-width: calc(100% - 4px); height: auto; margin-bottom: 1px; border-radius: 2px; - color: hsla(0,0%,100%,.8); + color: rgba(0, 0, 0, 0.8); + color: var(--outline-color); font-size: 13px; line-height: 15px; -webkit-user-select: none; @@ -1747,94 +3346,211 @@ html[dir='rtl'] .outlineItem > .outlineItems { -ms-user-select: none; user-select: none; white-space: normal; -} - -.attachmentsItem > button { - border: 0 none; - background: none; cursor: pointer; - width: 100%; } -html[dir='ltr'] .outlineItem > a { +@media (prefers-color-scheme: dark) { + + .treeItem > a { + color: rgba(255, 255, 255, 0.8); + color: var(--outline-color); + } +} +html[dir="ltr"] .treeItem > a { padding: 2px 0 5px 4px; } -html[dir='ltr'] .attachmentsItem > button { - padding: 2px 0 3px 7px; - text-align: left; -} - -html[dir='rtl'] .outlineItem > a { +html[dir="rtl"] .treeItem > a { padding: 2px 4px 5px 0; } -html[dir='rtl'] .attachmentsItem > button { - padding: 2px 7px 3px 0; - text-align: right; + +#layersView .treeItem > a > * { + cursor: pointer; +} +html[dir="ltr"] #layersView .treeItem > a > label { + padding-left: 4px; +} +html[dir="rtl"] #layersView .treesItem > a > label { + padding-right: 4px; } -.outlineItemToggler { +.treeItemToggler { position: relative; height: 0; width: 0; - color: hsla(0,0%,100%,.5); + color: rgba(255, 255, 255, 0.5); } -.outlineItemToggler::before { - content: url(images/treeitem-expanded.png); +.treeItemToggler::before { + content: url(images/treeitem-expanded.svg); + content: var(--treeitem-expanded-icon); display: inline-block; position: absolute; + max-width: 16px; } -html[dir='ltr'] .outlineItemToggler.outlineItemsHidden::before { - content: url(images/treeitem-collapsed.png); +@media (prefers-color-scheme: dark) { + + .treeItemToggler::before { + content: url(images/treeitem-expanded-dark.svg); + content: var(--treeitem-expanded-icon); + } } -html[dir='rtl'] .outlineItemToggler.outlineItemsHidden::before { - content: url(images/treeitem-collapsed-rtl.png); +.treeItemToggler.treeItemsHidden::before { + content: url(images/treeitem-collapsed.svg); + content: var(--treeitem-collapsed-icon); + max-width: 16px; } -.outlineItemToggler.outlineItemsHidden ~ .outlineItems { +@media (prefers-color-scheme: dark) { + + .treeItemToggler.treeItemsHidden::before { + content: url(images/treeitem-collapsed-dark.svg); + content: var(--treeitem-collapsed-icon); + } +} +html[dir="rtl"] .treeItemToggler.treeItemsHidden::before { + transform: scaleX(-1); +} +.treeItemToggler.treeItemsHidden ~ .treeItems { display: none; } -html[dir='ltr'] .outlineItemToggler { +html[dir="ltr"] .treeItemToggler { float: left; } -html[dir='rtl'] .outlineItemToggler { +html[dir="rtl"] .treeItemToggler { float: right; } -html[dir='ltr'] .outlineItemToggler::before { +html[dir="ltr"] .treeItemToggler::before { right: 4px; } -html[dir='rtl'] .outlineItemToggler::before { +html[dir="rtl"] .treeItemToggler::before { left: 4px; } -.outlineItemToggler:hover, -.outlineItemToggler:hover + a, -.outlineItemToggler:hover ~ .outlineItems, -.outlineItem > a:hover, -.attachmentsItem > button:hover { - background-color: hsla(0,0%,100%,.02); - 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)); +.treeItemToggler:hover { + background-color: rgba(0, 0, 0, 0.15); + background-color: var(--sidebaritem-bg-color); background-clip: padding-box; - box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset, - 0 0 1px hsla(0,0%,100%,.2) inset, - 0 0 1px hsla(0,0%,0%,.2); border-radius: 2px; - color: hsla(0,0%,100%,.9); + color: rgba(0, 0, 0, 0.9); + color: var(--outline-hover-color); } -.outlineItem.selected { - background-color: hsla(0,0%,100%,.08); - 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)); +@media (prefers-color-scheme: dark) { + + .treeItemToggler:hover { + color: rgba(255, 255, 255, 0.9); + color: var(--outline-hover-color); + } +} + +@media (prefers-color-scheme: dark) { + + .treeItemToggler:hover { + background-color: rgba(255, 255, 255, 0.15); + background-color: var(--sidebaritem-bg-color); + } +} + +.treeItemToggler:hover + a { + background-color: rgba(0, 0, 0, 0.15); + background-color: var(--sidebaritem-bg-color); background-clip: padding-box; - box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset, - 0 0 1px hsla(0,0%,100%,.1) inset, - 0 0 1px hsla(0,0%,0%,.2); - color: hsla(0,0%,100%,1); + border-radius: 2px; + color: rgba(0, 0, 0, 0.9); + color: var(--outline-hover-color); +} + +@media (prefers-color-scheme: dark) { + + .treeItemToggler:hover + a { + color: rgba(255, 255, 255, 0.9); + color: var(--outline-hover-color); + } +} + +@media (prefers-color-scheme: dark) { + + .treeItemToggler:hover + a { + background-color: rgba(255, 255, 255, 0.15); + background-color: var(--sidebaritem-bg-color); + } +} + +.treeItemToggler:hover ~ .treeItems { + background-color: rgba(0, 0, 0, 0.15); + background-color: var(--sidebaritem-bg-color); + background-clip: padding-box; + border-radius: 2px; + color: rgba(0, 0, 0, 0.9); + color: var(--outline-hover-color); +} + +@media (prefers-color-scheme: dark) { + + .treeItemToggler:hover ~ .treeItems { + color: rgba(255, 255, 255, 0.9); + color: var(--outline-hover-color); + } +} + +@media (prefers-color-scheme: dark) { + + .treeItemToggler:hover ~ .treeItems { + background-color: rgba(255, 255, 255, 0.15); + background-color: var(--sidebaritem-bg-color); + } +} + +.treeItem > a:hover { + background-color: rgba(0, 0, 0, 0.15); + background-color: var(--sidebaritem-bg-color); + background-clip: padding-box; + border-radius: 2px; + color: rgba(0, 0, 0, 0.9); + color: var(--outline-hover-color); +} + +@media (prefers-color-scheme: dark) { + + .treeItem > a:hover { + color: rgba(255, 255, 255, 0.9); + color: var(--outline-hover-color); + } +} + +@media (prefers-color-scheme: dark) { + + .treeItem > a:hover { + background-color: rgba(255, 255, 255, 0.15); + background-color: var(--sidebaritem-bg-color); + } +} + +.treeItem.selected { + background-color: rgba(0, 0, 0, 1); + background-color: var(--outline-active-bg-color); + background-clip: padding-box; + color: rgba(0, 0, 0, 0.08); + color: var(--outline-active-color); +} + +@media (prefers-color-scheme: dark) { + + .treeItem.selected { + color: rgba(255, 255, 255, 0.08); + color: var(--outline-active-color); + } +} + +@media (prefers-color-scheme: dark) { + + .treeItem.selected { + background-color: rgba(255, 255, 255, 1); + background-color: var(--outline-active-bg-color); + } } .noResults { font-size: 12px; - color: hsla(0,0%,100%,.8); + color: rgba(255, 255, 255, 0.8); font-style: italic; cursor: default; } @@ -1842,18 +3558,39 @@ html[dir='rtl'] .outlineItemToggler::before { /* TODO: file FF bug to support ::-moz-selection:window-inactive so we can override the opaque grey background when the window is inactive; see https://bugzilla.mozilla.org/show_bug.cgi?id=706209 */ -::-moz-selection { background: rgba(0,0,255,0.3); } -::selection { background: rgba(0,0,255,0.3); } +::-moz-selection { + background: rgba(0, 0, 255, 0.3); +} +::selection { + background: rgba(0, 0, 255, 0.3); +} #errorWrapper { - background: none repeat scroll 0 0 #FF5555; - color: white; + background: none repeat scroll 0 0 rgba(255, 74, 74, 1); + background: none repeat scroll 0 0 var(--errorWrapper-bg-color); + color: rgba(12, 12, 13, 1); + color: var(--main-color); left: 0; position: absolute; right: 0; z-index: 1000; - padding: 3px; - font-size: 0.8em; + padding: 3px 6px; +} + +@media (prefers-color-scheme: dark) { + + #errorWrapper { + color: rgba(249, 249, 250, 1); + color: var(--main-color); + } +} + +@media (prefers-color-scheme: dark) { + + #errorWrapper { + background: none repeat scroll 0 0 rgba(199, 17, 17, 1); + background: none repeat scroll 0 0 var(--errorWrapper-bg-color); + } } .loadingInProgress #errorWrapper { top: 37px; @@ -1868,17 +3605,45 @@ html[dir='rtl'] .outlineItemToggler::before { } #errorMoreInfo { - background-color: #FFFFFF; - color: black; + background-color: rgba(255, 255, 255, 1); + background-color: var(--field-bg-color); + color: rgba(6, 6, 6, 1); + color: var(--field-color); + border: 1px solid rgba(187, 187, 188, 1); + border: 1px solid var(--field-border-color); padding: 3px; margin: 3px; width: 98%; } +@media (prefers-color-scheme: dark) { + + #errorMoreInfo { + border: 1px solid rgba(115, 115, 115, 1); + border: 1px solid var(--field-border-color); + } +} + +@media (prefers-color-scheme: dark) { + + #errorMoreInfo { + color: rgba(250, 250, 250, 1); + color: var(--field-color); + } +} + +@media (prefers-color-scheme: dark) { + + #errorMoreInfo { + background-color: rgba(64, 64, 68, 1); + background-color: var(--field-bg-color); + } +} + .overlayButton { width: auto; margin: 3px 4px 2px 4px !important; - padding: 2px 6px 3px 6px; + padding: 2px 11px 2px 11px; } #overlayContainer { @@ -1886,7 +3651,7 @@ html[dir='rtl'] .outlineItemToggler::before { position: absolute; width: 100%; height: 100%; - background-color: hsla(0,0%,0%,.2); + background-color: rgba(0, 0, 0, 0.2); z-index: 40000; } #overlayContainer > * { @@ -1904,24 +3669,33 @@ html[dir='rtl'] .outlineItemToggler::before { display: inline-block; padding: 15px; border-spacing: 4px; - color: hsl(0,0%,85%); + color: rgba(12, 12, 13, 1); + color: var(--main-color); font-size: 12px; line-height: 14px; - 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)); - box-shadow: inset 1px 0 0 hsla(0,0%,100%,.08), - 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); - border: 1px solid hsla(0,0%,0%,.5); + background-color: rgba(255, 255, 255, 1); + background-color: var(--doorhanger-bg-color); + border: 1px solid rgba(0, 0, 0, 0.5); border-radius: 4px; box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3); } +@media (prefers-color-scheme: dark) { + + #overlayContainer > .container > .dialog { + background-color: rgba(74, 74, 79, 1); + background-color: var(--doorhanger-bg-color); + } +} + +@media (prefers-color-scheme: dark) { + + #overlayContainer > .container > .dialog { + color: rgba(249, 249, 250, 1); + color: var(--main-color); + } +} + .dialog > .row { display: table-row; } @@ -1939,8 +3713,16 @@ html[dir='rtl'] .outlineItemToggler::before { margin: 4px 0 4px 0; height: 1px; width: 100%; - background-color: hsla(0,0%,0%,.5); - box-shadow: 0 0 0 1px hsla(0,0%,100%,.08); + background-color: rgba(0, 0, 0, 0.3); + background-color: var(--separator-color); +} + +@media (prefers-color-scheme: dark) { + + .dialog .separator { + background-color: rgba(0, 0, 0, 0.3); + background-color: var(--separator-color); + } } .dialog .buttonRow { @@ -1949,7 +3731,7 @@ html[dir='rtl'] .outlineItemToggler::before { } .dialog :link { - color: white; + color: rgba(255, 255, 255, 1); } #passwordOverlay > .dialog { @@ -1965,10 +3747,10 @@ html[dir='rtl'] .outlineItemToggler::before { #documentPropertiesOverlay .row > * { min-width: 100px; } -html[dir='ltr'] #documentPropertiesOverlay .row > * { +html[dir="ltr"] #documentPropertiesOverlay .row > * { text-align: left; } -html[dir='rtl'] #documentPropertiesOverlay .row > * { +html[dir="rtl"] #documentPropertiesOverlay .row > * { text-align: right; } #documentPropertiesOverlay .row > span { @@ -1988,8 +3770,8 @@ html[dir='rtl'] #documentPropertiesOverlay .row > * { } .fileInput { - background: white; - color: black; + background: rgba(255, 255, 255, 1); + color: rgba(0, 0, 0, 1); margin-top: 5px; visibility: hidden; position: fixed; @@ -1998,8 +3780,8 @@ html[dir='rtl'] #documentPropertiesOverlay .row > * { } #PDFBug { - background: none repeat scroll 0 0 white; - border: 1px solid #666666; + background: none repeat scroll 0 0 rgba(255, 255, 255, 1); + border: 1px solid rgba(102, 102, 102, 1); position: fixed; top: 32px; right: 0; @@ -2009,9 +3791,9 @@ html[dir='rtl'] #documentPropertiesOverlay .row > * { width: 300px; } #PDFBug .controls { - background:#EEEEEE; - border-bottom: 1px solid #666666; - padding: 3px; + background: rgba(238, 238, 238, 1); + border-bottom: 1px solid rgba(102, 102, 102, 1); + padding: 3px; } #PDFBug .panels { bottom: 0; @@ -2022,15 +3804,18 @@ html[dir='rtl'] #documentPropertiesOverlay .row > * { right: 0; top: 27px; } +#PDFBug .panels > div { + padding: 5px; +} #PDFBug button.active { font-weight: bold; } .debuggerShowText { - background: none repeat scroll 0 0 yellow; - color: blue; + background: none repeat scroll 0 0 rgba(255, 255, 0, 1); + color: rgba(0, 0, 255, 1); } .debuggerHideText:hover { - background: none repeat scroll 0 0 yellow; + background: none repeat scroll 0 0 rgba(255, 255, 0, 1); } #PDFBug .stats { font-family: courier; @@ -2038,18 +3823,18 @@ html[dir='rtl'] #documentPropertiesOverlay .row > * { white-space: pre; } #PDFBug .stats .title { - font-weight: bold; + font-weight: bold; } #PDFBug table { font-size: 10px; } #viewer.textLayer-visible .textLayer { - opacity: 1.0; + opacity: 1; } #viewer.textLayer-visible .canvasWrapper { - background-color: rgb(128,255,128); + background-color: rgba(128, 255, 128, 1); } #viewer.textLayer-visible .canvasWrapper canvas { @@ -2058,19 +3843,19 @@ html[dir='rtl'] #documentPropertiesOverlay .row > * { #viewer.textLayer-visible .textLayer > span { background-color: rgba(255, 255, 0, 0.1); - color: black; + color: rgba(0, 0, 0, 1); border: solid 1px rgba(255, 0, 0, 0.5); box-sizing: border-box; } #viewer.textLayer-hover .textLayer > span:hover { - background-color: white; - color: black; + background-color: rgba(255, 255, 255, 1); + color: rgba(0, 0, 0, 1); } #viewer.textLayer-shadow .textLayer > span { - background-color: rgba(255,255,255, .6); - color: black; + background-color: rgba(255, 255, 255, 0.6); + color: rgba(0, 0, 0, 1); } .grab-to-pan-grab { @@ -2078,7 +3863,8 @@ html[dir='rtl'] #documentPropertiesOverlay .row > * { cursor: -webkit-grab !important; cursor: grab !important; } -.grab-to-pan-grab *:not(input):not(textarea):not(button):not(select):not(:link) { +.grab-to-pan-grab + *:not(input):not(textarea):not(button):not(select):not(:link) { cursor: inherit !important; } .grab-to-pan-grab:active, @@ -2086,9 +3872,8 @@ html[dir='rtl'] #documentPropertiesOverlay .row > * { cursor: url("images/grabbing.cur"), move !important; cursor: -webkit-grabbing !important; cursor: grabbing !important; - position: fixed; - background: transparent; + background: rgba(0, 0, 0, 0); display: block; top: 0; left: 0; @@ -2106,224 +3891,29 @@ html[dir='rtl'] #documentPropertiesOverlay .row > * { display: none; } -@media screen and (-webkit-min-device-pixel-ratio: 1.1), screen and (min-resolution: 1.1dppx) { - /* Rules for Retina screens */ - .toolbarButton::before { - -webkit-transform: scale(0.5); - transform: scale(0.5); - top: -5px; - } - - .secondaryToolbarButton::before { - -webkit-transform: scale(0.5); - transform: scale(0.5); - top: -4px; - } - - html[dir='ltr'] .toolbarButton::before, - html[dir='rtl'] .toolbarButton::before { - left: -1px; - } - - html[dir='ltr'] .secondaryToolbarButton::before { - left: -2px; - } - html[dir='rtl'] .secondaryToolbarButton::before { - left: 186px; - } - - .toolbarField.pageNumber.visiblePageIsLoading, - #findInput[data-status="pending"] { - background-image: url(images/loading-small@2x.png); - background-size: 16px 17px; - } - - .dropdownToolbarButton { - background: url(images/toolbarButton-menuArrows@2x.png) no-repeat; - background-size: 7px 16px; - } - - html[dir='ltr'] .toolbarButton#sidebarToggle::before { - content: url(images/toolbarButton-sidebarToggle@2x.png); - } - html[dir='rtl'] .toolbarButton#sidebarToggle::before { - content: url(images/toolbarButton-sidebarToggle-rtl@2x.png); - } - - html[dir='ltr'] .toolbarButton#secondaryToolbarToggle::before { - content: url(images/toolbarButton-secondaryToolbarToggle@2x.png); - } - html[dir='rtl'] .toolbarButton#secondaryToolbarToggle::before { - content: url(images/toolbarButton-secondaryToolbarToggle-rtl@2x.png); - } - - html[dir='ltr'] .toolbarButton.findPrevious::before { - content: url(images/findbarButton-previous@2x.png); - } - html[dir='rtl'] .toolbarButton.findPrevious::before { - content: url(images/findbarButton-previous-rtl@2x.png); - } - - html[dir='ltr'] .toolbarButton.findNext::before { - content: url(images/findbarButton-next@2x.png); - } - html[dir='rtl'] .toolbarButton.findNext::before { - content: url(images/findbarButton-next-rtl@2x.png); - } - - html[dir='ltr'] .toolbarButton.pageUp::before { - content: url(images/toolbarButton-pageUp@2x.png); - } - html[dir='rtl'] .toolbarButton.pageUp::before { - content: url(images/toolbarButton-pageUp-rtl@2x.png); - } - - html[dir='ltr'] .toolbarButton.pageDown::before { - content: url(images/toolbarButton-pageDown@2x.png); - } - html[dir='rtl'] .toolbarButton.pageDown::before { - content: url(images/toolbarButton-pageDown-rtl@2x.png); - } - - .toolbarButton.zoomIn::before { - content: url(images/toolbarButton-zoomIn@2x.png); - } - - .toolbarButton.zoomOut::before { - content: url(images/toolbarButton-zoomOut@2x.png); - } - - .toolbarButton.presentationMode::before, - .secondaryToolbarButton.presentationMode::before { - content: url(images/toolbarButton-presentationMode@2x.png); - } - - .toolbarButton.print::before, - .secondaryToolbarButton.print::before { - content: url(images/toolbarButton-print@2x.png); - } - - .toolbarButton.openFile::before, - .secondaryToolbarButton.openFile::before { - content: url(images/toolbarButton-openFile@2x.png); - } - - .toolbarButton.download::before, - .secondaryToolbarButton.download::before { - content: url(images/toolbarButton-download@2x.png); - } - - .toolbarButton.bookmark::before, - .secondaryToolbarButton.bookmark::before { - content: url(images/toolbarButton-bookmark@2x.png); - } - - #viewThumbnail.toolbarButton::before { - content: url(images/toolbarButton-viewThumbnail@2x.png); - } - - html[dir="ltr"] #viewOutline.toolbarButton::before { - content: url(images/toolbarButton-viewOutline@2x.png); - } - html[dir="rtl"] #viewOutline.toolbarButton::before { - content: url(images/toolbarButton-viewOutline-rtl@2x.png); - } - - #viewAttachments.toolbarButton::before { - content: url(images/toolbarButton-viewAttachments@2x.png); - } - - #viewFind.toolbarButton::before { - content: url(images/toolbarButton-search@2x.png); - } - - .secondaryToolbarButton.firstPage::before { - content: url(images/secondaryToolbarButton-firstPage@2x.png); - } - - .secondaryToolbarButton.lastPage::before { - content: url(images/secondaryToolbarButton-lastPage@2x.png); - } - - .secondaryToolbarButton.rotateCcw::before { - content: url(images/secondaryToolbarButton-rotateCcw@2x.png); - } - - .secondaryToolbarButton.rotateCw::before { - content: url(images/secondaryToolbarButton-rotateCw@2x.png); - } - - .secondaryToolbarButton.selectTool::before { - content: url(images/secondaryToolbarButton-selectTool@2x.png); - } - - .secondaryToolbarButton.handTool::before { - content: url(images/secondaryToolbarButton-handTool@2x.png); - } - - .secondaryToolbarButton.scrollVertical::before { - content: url(images/secondaryToolbarButton-scrollVertical@2x.png); - } - - .secondaryToolbarButton.scrollHorizontal::before { - content: url(images/secondaryToolbarButton-scrollHorizontal@2x.png); - } - - .secondaryToolbarButton.scrollWrapped::before { - content: url(images/secondaryToolbarButton-scrollWrapped@2x.png); - } - - .secondaryToolbarButton.spreadNone::before { - content: url(images/secondaryToolbarButton-spreadNone@2x.png); - } - - .secondaryToolbarButton.spreadOdd::before { - content: url(images/secondaryToolbarButton-spreadOdd@2x.png); - } - - .secondaryToolbarButton.spreadEven::before { - content: url(images/secondaryToolbarButton-spreadEven@2x.png); - } - - .secondaryToolbarButton.documentProperties::before { - content: url(images/secondaryToolbarButton-documentProperties@2x.png); - } - - .outlineItemToggler::before { - -webkit-transform: scale(0.5); - transform: scale(0.5); - top: -1px; - content: url(images/treeitem-expanded@2x.png); - } - html[dir='ltr'] .outlineItemToggler.outlineItemsHidden::before { - content: url(images/treeitem-collapsed@2x.png); - } - html[dir='rtl'] .outlineItemToggler.outlineItemsHidden::before { - content: url(images/treeitem-collapsed-rtl@2x.png); - } - html[dir='ltr'] .outlineItemToggler::before { - right: 0; - } - html[dir='rtl'] .outlineItemToggler::before { - left: 0; - } -} - @media print { /* General rules for printing. */ body { - background: transparent none; + background: rgba(0, 0, 0, 0) none; } /* Rules for browsers that don't support mozPrintCallback. */ - #sidebarContainer, #secondaryToolbar, .toolbar, #loadingBox, #errorWrapper, .textLayer { + #sidebarContainer, + #secondaryToolbar, + .toolbar, + #loadingBox, + #errorWrapper, + .textLayer { display: none; } #viewerContainer { overflow: visible; } - #mainContainer, #viewerContainer, .page, .page canvas { + #mainContainer, + #viewerContainer, + .page, + .page canvas { position: static; padding: 0; margin: 0; @@ -2335,7 +3925,7 @@ html[dir='rtl'] #documentPropertiesOverlay .row > * { border: none; box-shadow: none; background-clip: content-box; - background-color: white; + background-color: rgba(255, 255, 255, 1); } .page[data-loaded] { @@ -2369,6 +3959,7 @@ html[dir='rtl'] #documentPropertiesOverlay .row > * { } #printContainer canvas, #printContainer img { + direction: ltr; display: block; } } @@ -2385,20 +3976,19 @@ html[dir='rtl'] #documentPropertiesOverlay .row > * { margin: auto; left: auto; position: inherit; - -webkit-transform: none; - transform: none; + transform: none; } } @media all and (max-width: 840px) { #sidebarContent { - background-color: hsla(0,0%,0%,.7); + background-color: rgba(0, 0, 0, 0.7); } - html[dir='ltr'] #outerContainer.sidebarOpen #viewerContainer { + html[dir="ltr"] #outerContainer.sidebarOpen #viewerContainer { left: 0px !important; } - html[dir='rtl'] #outerContainer.sidebarOpen #viewerContainer { + html[dir="rtl"] #outerContainer.sidebarOpen #viewerContainer { right: 0px !important; } @@ -2431,7 +4021,8 @@ html[dir='rtl'] #documentPropertiesOverlay .row > * { } @media all and (max-width: 640px) { - .hiddenSmallView, .hiddenSmallView * { + .hiddenSmallView, + .hiddenSmallView * { display: none; } .visibleSmallView { @@ -2440,11 +4031,11 @@ html[dir='rtl'] #documentPropertiesOverlay .row > * { .toolbarButtonSpacer { width: 0; } - html[dir='ltr'] .findbar { - left: 38px; + html[dir="ltr"] .findbar { + left: 34px; } - html[dir='rtl'] .findbar { - right: 38px; + html[dir="rtl"] .findbar { + right: 34px; } } diff --git a/cps/static/css/listen.css b/cps/static/css/listen.css index a69af72e..9e1d3bb4 100644 --- a/cps/static/css/listen.css +++ b/cps/static/css/listen.css @@ -66,19 +66,12 @@ body { right: 40px; } -xmp, -pre, -plaintext { - display: block; - font-family: -moz-fixed; - white-space: pre; - margin: 1em 0; -} - pre { + display: block; + margin: 1em 0; white-space: pre-wrap; word-wrap: break-word; - font-family: -moz-fixed; + font-family: -moz-fixed, sans-serif; column-count: 2; -webkit-columns: 2; -moz-columns: 2; diff --git a/cps/static/css/main.css b/cps/static/css/main.css index 08506bc2..adbfbfdf 100644 --- a/cps/static/css/main.css +++ b/cps/static/css/main.css @@ -15,6 +15,10 @@ body { overflow: hidden; } +.myselect { + overflow: visible !important; +} + #main { position: absolute; width: 100%; @@ -25,10 +29,9 @@ body { overflow: hidden; -webkit-transition: -webkit-transform 0.4s, width 0.2s; -moz-transition: -webkit-transform 0.4s, width 0.2s; - -ms-transition: -webkit-transform 0.4s, width 0.2s; + transition: -webkit-transform 0.4s, width 0.2s; -moz-box-shadow: inset 0 0 50px rgba(0, 0, 0, 0.1); -webkit-box-shadow: inset 0 0 50px rgba(0, 0, 0, 0.1); - -ms-box-shadow: inset 0 0 50px rgba(0, 0, 0, 0.1); box-shadow: inset 0 0 50px rgba(0, 0, 0, 0.1); } @@ -36,7 +39,6 @@ body { height: 8%; min-height: 20px; padding: 10px; - /* margin: 0 50px 0 50px; */ position: relative; color: #4f4f4f; font-weight: 100; @@ -45,7 +47,7 @@ body { text-align: center; -webkit-transition: opacity 0.5s; -moz-transition: opacity 0.5s; - -ms-transition: opacity 0.5s; + transition: opacity 0.5s; z-index: 10; } @@ -74,12 +76,21 @@ body { padding: 3px; } +#panels a { + visibility: hidden; + width: 18px; + height: 20px; + overflow: hidden; + display: inline-block; + color: #ccc; + margin-left: 6px; +} + #titlebar a:active { opacity: 1; color: rgba(0, 0, 0, 0.6); -moz-box-shadow: inset 0 0 6px rgba(155, 155, 155, 0.8); -webkit-box-shadow: inset 0 0 6px rgba(155, 155, 155, 0.8); - -ms-box-shadow: inset 0 0 6px rgba(155, 155, 155, 0.8); box-shadow: inset 0 0 6px rgba(155, 155, 155, 0.8); } @@ -116,12 +127,11 @@ body { top: 50%; margin-top: -192px; font-size: 64px; - color: #E2E2E2; + color: #e2e2e2; font-family: arial, sans-serif; font-weight: bold; cursor: pointer; -webkit-user-select: none; - -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; @@ -147,16 +157,10 @@ body { height: 100%; -webkit-transition: -webkit-transform 0.5s; -moz-transition: -moz-transform 0.5s; - -ms-transition: -moz-transform 0.5s; + transition: -moz-transform 0.5s; overflow: hidden; } -#sidebar.open { - /* left: 0; */ - /* -webkit-transform: translate(0, 0); - -moz-transform: translate(0, 0); */ -} - #main.closed { /* left: 300px; */ -webkit-transform: translate(300px, 0); @@ -183,7 +187,6 @@ body { height: 14px; -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.6); -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.6); - -ms-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.6); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.6); } @@ -200,19 +203,24 @@ body { #title-controls { float: right; } -#panels a { - visibility: hidden; - width: 18px; - height: 20px; - overflow: hidden; - display: inline-block; - color: #ccc; - margin-left: 6px; -} - #panels a::before { visibility: visible; } #panels a:hover { color: #aaa; } +.list_item.currentChapter > a, +.list_item a:hover { + color: #f1f1f1; +} + +.list_item a { + color: #aaa; + text-decoration: none; +} + +#searchResults a { + color: #aaa; + text-decoration: none; +} + #panels a:active { color: #aaa; margin: 1px 0 -1px 6px; @@ -232,7 +240,6 @@ body { input::-webkit-input-placeholder { color: #454545; } input:-moz-placeholder { color: #454545; } -input:-ms-placeholder { color: #454545; } #divider { position: absolute; @@ -243,7 +250,7 @@ input:-ms-placeholder { color: #454545; } left: 50%; margin-left: -1px; top: 10%; - opacity: .15; + opacity: 0.15; box-shadow: -2px 0 15px rgba(0, 0, 0, 1); display: none; } @@ -268,24 +275,35 @@ input:-ms-placeholder { color: #454545; } width: 25%; height: 100%; visibility: hidden; - -webkit-transition: visibility 0 ease 0.5s; - -moz-transition: visibility 0 ease 0.5s; - -ms-transition: visibility 0 ease 0.5s; + -webkit-transition: visibility 0s ease 0.5s; + -moz-transition: visibility 0s ease 0.5s; + transition: visibility 0s ease 0.5s; } #sidebar.open #tocView, #sidebar.open #bookmarksView { overflow-y: auto; visibility: visible; - -webkit-transition: visibility 0 ease 0; - -moz-transition: visibility 0 ease 0; - -ms-transition: visibility 0 ease 0; + -webkit-transition: visibility 0s ease 0s; + -moz-transition: visibility 0s ease 0s; + transition: visibility 0s ease 0s; } #sidebar.open #tocView { display: block; } +.list_item ul { + padding-left: 10px; + margin-top: 8px; + display: none; +} + +.list_item.currentChapter > ul, +.list_item.openChapter > ul { + display: block; +} + #tocView > ul, #bookmarksView > ul { margin-top: 15px; @@ -296,22 +314,41 @@ input:-ms-placeholder { color: #454545; } #tocView li, #bookmarksView li { - margin-bottom:10px; + margin-bottom: 10px; width: 225px; font-family: Georgia, "Times New Roman", Times, serif; list-style: none; text-transform: capitalize; } -#tocView li:active, -#tocView li.currentChapter -{ +.md-content > div ul li { + padding: 5px 0; +} + +#settingsPanel li { + font-size: 1em; + color: #f1f1f1; +} + +#searchResults li { + margin-bottom: 10px; + width: 225px; + font-family: Georgia, "Times New Roman", Times, serif; list-style: none; } -.list_item a { - color: #aaa; - text-decoration: none; +#notes li { + color: #eee; + font-size: 12px; + width: 240px; + border-top: 1px #fff solid; + padding-top: 6px; + margin-bottom: 6px; +} + +#tocView li:active, +#tocView li.currentChapter { + list-style: none; } .list_item a.chapter { @@ -322,27 +359,11 @@ input:-ms-placeholder { color: #454545; } font-size: 0.8em; } -.list_item.currentChapter > a, -.list_item a:hover { - color: #f1f1f1 -} - /* #tocView li.openChapter > a, */ .list_item a:hover { color: #e2e2e2; } -.list_item ul { - padding-left:10px; - margin-top: 8px; - display: none; -} - -.list_item.currentChapter > ul, -.list_item.openChapter > ul { - display: block; -} - #tocView.hidden { display: none; } @@ -357,14 +378,14 @@ input:-ms-placeholder { color: #454545; } user-select: none; } -.toc_toggle:before { +.toc_toggle::before { content: '▸'; color: #fff; margin-right: -4px; } -.currentChapter > .toc_toggle:before, -.openChapter > .toc_toggle:before { +.currentChapter > .toc_toggle::before, +.openChapter > .toc_toggle::before { content: '▾'; } @@ -382,18 +403,6 @@ input:-ms-placeholder { color: #454545; } display: block; } -#searchResults li { - margin-bottom:10px; - width: 225px; - font-family: Georgia, "Times New Roman", Times, serif; - list-style: none; -} - -#searchResults a { - color: #aaa; - text-decoration: none; -} - #searchResults p { text-decoration: none; font-size: 12px; @@ -405,10 +414,21 @@ input:-ms-placeholder { color: #454545; } color: #000; } +.md-content > div p { + margin: 0; + padding: 10px 0; +} + #searchResults li > p { color: #aaa; } +#notes li a { + color: #fff; + display: inline-block; + margin-left: 6px; +} + #searchResults li a:hover { color: #e2e2e2; } @@ -419,22 +439,7 @@ input:-ms-placeholder { color: #454545; } } #notes { - padding: 0 0 0 34px; -} - -#notes li { - color: #eee; - font-size: 12px; - width: 240px; - border-top: 1px #fff solid; - padding-top: 6px; - margin-bottom: 6px; -} - -#notes li a { - color: #fff; - display: inline-block; - margin-left: 6px; + padding: 0 0 0 34px; } #notes li a:hover { @@ -454,8 +459,9 @@ input:-ms-placeholder { color: #454545; } border-radius: 5px; } -#note-text[disabled], #note-text[disabled="disabled"]{ - opacity: 0.5; +#note-text[disabled], +#note-text[disabled="disabled"] { + opacity: 0.5; } #note-anchor { @@ -467,6 +473,22 @@ input:-ms-placeholder { color: #454545; } display: none; } +.md-content h3 { + margin: 0; + padding: 6px; + text-align: center; + font-size: 22px; + font-weight: 300; + opacity: 0.8; + background: rgba(0, 0, 0, 0.1); + border-radius: 3px 3px 0 0; +} + +.md-content > div ul { + margin: 0; + padding: 0 0 30px 20px; +} + #settingsPanel h3 { color: #f1f1f1; font-family: Georgia, "Times New Roman", Times, serif; @@ -478,32 +500,24 @@ input:-ms-placeholder { color: #454545; } list-style-type: none; } -#settingsPanel li { - font-size: 1em; - color: #f1f1f1; -} +#settingsPanel .xsmall { font-size: x-small; } +#settingsPanel .small { font-size: small; } +#settingsPanel .medium { font-size: medium; } +#settingsPanel .large { font-size: large; } +#settingsPanel .xlarge { font-size: x-large; } -#settingsPanel .xsmall { font-size: x-small; } -#settingsPanel .small { font-size: small; } -#settingsPanel .medium { font-size: medium; } -#settingsPanel .large { font-size: large; } -#settingsPanel .xlarge { font-size: x-large; } - -.highlight { background-color: yellow } +.highlight { background-color: yellow; } .modal { position: fixed; top: 50%; left: 50%; - width: 50%; width: 630px; - height: auto; z-index: 2000; visibility: hidden; margin-left: -320px; margin-top: -160px; - } .overlay { @@ -518,17 +532,16 @@ input:-ms-placeholder { color: #454545; } background: rgba(255, 255, 255, 0.8); -webkit-transition: all 0.3s; -moz-transition: all 0.3s; - -ms-transition: all 0.3s; transition: all 0.3s; } .md-show { - visibility: visible; + visibility: visible; } .md-show ~ .overlay { - opacity: 1; - visibility: visible; + opacity: 1; + visibility: visible; } /* Content styles */ @@ -541,17 +554,6 @@ input:-ms-placeholder { color: #454545; } height: 320px; } -.md-content h3 { - margin: 0; - padding: 6px; - text-align: center; - font-size: 22px; - font-weight: 300; - opacity: 0.8; - background: rgba(0, 0, 0, 0.1); - border-radius: 3px 3px 0 0; -} - .md-content > div { padding: 15px 40px 30px; margin: 0; @@ -559,20 +561,6 @@ input:-ms-placeholder { color: #454545; } font-size: 14px; } -.md-content > div p { - margin: 0; - padding: 10px 0; -} - -.md-content > div ul { - margin: 0; - padding: 0 0 30px 20px; -} - -.md-content > div ul li { - padding: 5px 0; -} - .md-content button { display: block; margin: 0 auto; @@ -588,7 +576,6 @@ input:-ms-placeholder { color: #454545; } opacity: 0; -webkit-transition: all 0.3s; -moz-transition: all 0.3s; - -ms-transition: all 0.3s; transition: all 0.3s; } @@ -601,7 +588,6 @@ input:-ms-placeholder { color: #454545; } } .md-content > .closer { - font-size: 18px; position: absolute; right: 0; top: 0; @@ -610,7 +596,7 @@ input:-ms-placeholder { color: #454545; } } @media only screen and (max-width: 1040px) and (orientation: portrait) { - #viewer{ + #viewer { width: 80%; margin-left: 10%; } @@ -622,7 +608,7 @@ input:-ms-placeholder { color: #454545; } } @media only screen and (max-width: 900px) { - #viewer{ + #viewer { width: 60%; margin-left: 20%; } @@ -637,7 +623,7 @@ input:-ms-placeholder { color: #454545; } } @media only screen and (max-width: 550px) { - #viewer{ + #viewer { width: 80%; margin-left: 10%; } @@ -661,9 +647,9 @@ input:-ms-placeholder { color: #454545; } -webkit-transform: translate(0, 0); -moz-transform: translate(0, 0); -ms-transform: translate(0, 0); - -webkit-transition: -webkit-transform .3s; - -moz-transition: -moz-transform .3s; - -ms-transition: -moz-transform .3s; + -webkit-transition: -webkit-transform 0.3s; + -moz-transition: -moz-transform 0.3s; + transition: -moz-transform 0.3s; } #main.closed { @@ -672,11 +658,6 @@ input:-ms-placeholder { color: #454545; } -ms-transform: translate(260px, 0); } - #titlebar { - /* font-size: 16px; */ - /* margin: 0 50px 0 50px; */ - } - #metainfo { font-size: 10px; } @@ -689,130 +670,129 @@ input:-ms-placeholder { color: #454545; } font-size: 12px; } - #tocView > ul{ + #tocView > ul { padding-left: 10px; } } - /* For iPad portrait layouts only */ @media only screen and (min-device-width: 481px) and (max-device-width: 1024px) and (orientation: portrait) { - #viewer iframe { - width: 460px; - height: 740px; - } + #viewer iframe { + width: 460px; + height: 740px; + } } - /*For iPad landscape layouts only *//* -@media only screen and (min-device-width: 481px) and (max-device-width: 1024px) and (orientation: landscape) { - #viewer iframe { - width: 460px; - height: 415px; - } -}*/ @media only screen -and (min-device-width : 768px) -and (max-device-width : 1024px) -and (orientation : landscape) -/*and (-webkit-min-device-pixel-ratio: 2)*/ { - #viewer{ + and (min-device-width: 768px) + and (max-device-width: 1024px) + and (orientation: landscape) + /* and (-webkit-min-device-pixel-ratio: 2)*/ { + #viewer { width: 80%; margin-left: 10%; } + #divider, #divider.show { display: none; } } - /*For iPad landscape layouts only */ +/* For iPad landscape layouts only */ @media only screen and (min-device-width: 481px) and (max-device-width: 1024px) and (orientation: landscape) { - #viewer iframe { - width: 960px; - height: 515px; - } + #viewer iframe { + width: 960px; + height: 515px; + } } /* For iPhone 6\6s portrait layouts only */ -@media only screen and (min-device-width : 375px) and (max-device-width : 667px) and (orientation: portrait) { - #viewer { - width: 300px; - height: 480px; - } - #viewer iframe { - width: 300px; - height: 480px; - } +@media only screen and (min-device-width: 375px) and (max-device-width: 667px) and (orientation: portrait) { + #viewer { + width: 300px; + height: 480px; + } + + #viewer iframe { + width: 300px; + height: 480px; + } } /* For iPhone 6\6s landscape layouts only */ -@media only screen and (min-device-width : 375px) and (max-device-width : 667px) and (orientation: landscape) { - #viewer { - width: 450px; - height: 300px; - } - #viewer iframe { - width: 450px; - height: 300px; - } +@media only screen and (min-device-width: 375px) and (max-device-width: 667px) and (orientation: landscape) { + #viewer { + width: 450px; + height: 300px; + } + + #viewer iframe { + width: 450px; + height: 300px; + } } /* For iPhone portrait layouts only */ @media only screen and (max-device-width: 374px) and (orientation: portrait) { - #viewer { - width: 256px; - height: 432px; - } - #viewer iframe { - width: 256px; - height: 432px; - } + #viewer { + width: 256px; + height: 432px; + } + + #viewer iframe { + width: 256px; + height: 432px; + } } /* For iPhone landscape layouts only */ @media only screen and (max-device-width: 374px) and (orientation: landscape) { - #viewer iframe { - width: 256px; - height: 124px; - } + #viewer iframe { + width: 256px; + height: 124px; + } } -[class^="icon-"]:before, [class*=" icon-"]:before { - font-family: "fontello"; - font-style: normal; - font-weight: normal; - speak: none; - display: inline-block; - text-decoration: inherit; - width: 1em; - margin-right: 0.2em; - text-align: center; - /* For safety - reset parent styles, that can break glyph codes*/ - font-variant: normal; - text-transform: none; - /* you can be more comfortable with increased icons size */ - font-size: 112%; +[class^="icon-"]::before, +[class*=" icon-"]::before { + font-family: "fontello", serif; + font-style: normal; + font-weight: normal; + speak: none; + display: inline-block; + text-decoration: inherit; + width: 1em; + margin-right: 0.2em; + text-align: center; + + /* For safety - reset parent styles, that can break glyph codes */ + font-variant: normal; + text-transform: none; + + /* you can be more comfortable with increased icons size */ + font-size: 112%; } -.icon-search:before { content: '\e807'; } /* '' */ -.icon-resize-full-1:before { content: '\e804'; } /* '' */ -.icon-cancel-circled2:before { content: '\e80f'; } /* '' */ -.icon-link:before { content: '\e80d'; } /* '' */ -.icon-bookmark:before { content: '\e805'; } /* '' */ -.icon-bookmark-empty:before { content: '\e806'; } /* '' */ -.icon-download-cloud:before { content: '\e811'; } /* '' */ -.icon-edit:before { content: '\e814'; } /* '' */ -.icon-menu:before { content: '\e802'; } /* '' */ -.icon-cog:before { content: '\e813'; } /* '' */ -.icon-resize-full:before { content: '\e812'; } /* '' */ -.icon-cancel-circled:before { content: '\e80e'; } /* '' */ -.icon-up-dir:before { content: '\e80c'; } /* '' */ -.icon-right-dir:before { content: '\e80b'; } /* '' */ -.icon-angle-right:before { content: '\e809'; } /* '' */ -.icon-angle-down:before { content: '\e80a'; } /* '' */ -.icon-right:before { content: '\e815'; } /* '' */ -.icon-list-1:before { content: '\e803'; } /* '' */ -.icon-list-numbered:before { content: '\e801'; } /* '' */ -.icon-columns:before { content: '\e810'; } /* '' */ -.icon-list:before { content: '\e800'; } /* '' */ -.icon-resize-small:before { content: '\e808'; } /* '' */ +.icon-search::before { content: '\e807'; } /* '' */ +.icon-resize-full-1::before { content: '\e804'; } /* '' */ +.icon-cancel-circled2::before { content: '\e80f'; } /* '' */ +.icon-link::before { content: '\e80d'; } /* '' */ +.icon-bookmark::before { content: '\e805'; } /* '' */ +.icon-bookmark-empty::before { content: '\e806'; } /* '' */ +.icon-download-cloud::before { content: '\e811'; } /* '' */ +.icon-edit::before { content: '\e814'; } /* '' */ +.icon-menu::before { content: '\e802'; } /* '' */ +.icon-cog::before { content: '\e813'; } /* '' */ +.icon-resize-full::before { content: '\e812'; } /* '' */ +.icon-cancel-circled::before { content: '\e80e'; } /* '' */ +.icon-up-dir::before { content: '\e80c'; } /* '' */ +.icon-right-dir::before { content: '\e80b'; } /* '' */ +.icon-angle-right::before { content: '\e809'; } /* '' */ +.icon-angle-down::before { content: '\e80a'; } /* '' */ +.icon-right::before { content: '\e815'; } /* '' */ +.icon-list-1::before { content: '\e803'; } /* '' */ +.icon-list-numbered::before { content: '\e801'; } /* '' */ +.icon-columns::before { content: '\e810'; } /* '' */ +.icon-list::before { content: '\e800'; } /* '' */ +.icon-resize-small::before { content: '\e808'; } /* '' */ diff --git a/cps/static/css/style.css b/cps/static/css/style.css index 47b0c4cb..f1d38901 100644 --- a/cps/static/css/style.css +++ b/cps/static/css/style.css @@ -1,7 +1,7 @@ .tooltip.bottom .tooltip-inner { font-size: 13px; - font-family: Open Sans Semibold,Helvetica Neue,Helvetica,Arial,sans-serif; + font-family: Open Sans Semibold, Helvetica Neue, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; padding: 3px 10px; @@ -28,6 +28,11 @@ html.http-error { height: 100%; } +body { + background: #f2f2f2; + margin-bottom: 40px; +} + .http-error body { margin: 0; height: 100%; @@ -41,32 +46,40 @@ html.http-error { text-align: center; } -body { - background: #f2f2f2; - margin-bottom: 40px; -} - body h2 { font-weight: normal; - color:#444; + color: #444; } -a, .danger,.book-remove, .editable-empty, .editable-empty:hover { color: #45b29d; } - +a, +.danger, +.book-remove, +.editable-empty, +.editable-empty:hover { color: #45b29d; } .book-remove:hover { color: #23527c; } - +.user-remove:hover { color: #23527c; } .btn-default a { color: #444; } +.panel-title > a { text-decoration: none; } + +.navigation li a { + color: #444; + text-decoration: none; + display: block; + padding: 10px; +} .btn-default a:hover { - color: #45b29d; - text-decoration: None; + color: #45b29d; + text-decoration: None; } .btn-default:hover { - color: #45b29d; + color: #45b29d; } -.editable-click, a.editable-click, a.editable-click:hover { border-bottom: None; } +.editable-click, +a.editable-click, +a.editable-click:hover { border-bottom: None; } .navigation .nav-head { text-transform: uppercase; @@ -79,11 +92,17 @@ a, .danger,.book-remove, .editable-empty, .editable-empty:hover { color: #45b29d padding-top: 20px; } -.navigation li a { - color: #444; +.book-meta .tags a { display: inline; } +table .bg-primary a { color: #fff; } +table .bg-dark-danger a { color: #fff; } +.book-meta .identifiers a { display: inline; } + +.navigation .create-shelf a { + color: #fff; + background: #45b29d; + padding: 10px 20px; + border-radius: 5px; text-decoration: none; - display: block; - padding: 10px; } .navigation li a:hover { @@ -99,14 +118,6 @@ a, .danger,.book-remove, .editable-empty, .editable-empty:hover { color: #45b29d text-align: center; } -.navigation .create-shelf a { - color: #fff; - background: #45b29d; - padding: 10px 20px; - border-radius: 5px; - text-decoration: none; -} - .row.display-flex { display: flex; flex-wrap: wrap; @@ -116,34 +127,59 @@ a, .danger,.book-remove, .editable-empty, .editable-empty:hover { color: #45b29d display: block; max-width: 100%; height: auto; + max-height: 100%; } -.container-fluid .discover{ margin-bottom: 50px; } +.container-fluid .discover { margin-bottom: 50px; } .container-fluid .new-books { border-top: 1px solid #ccc; } .container-fluid .new-books h2 { margin: 50px 0 0 0; } + .container-fluid .book { margin-top: 20px; display: flex; flex-direction: column; } +.cover { margin-bottom: 10px; } .container-fluid .book .cover { height: 225px; position: relative; } -.container-fluid .book .cover img { +.author-link img { + display: block; + height: 100%; +} + +.container-fluid .book .cover span.img { + bottom: 0; + height: 100%; + position: absolute; +} +.author-bio img { margin: 0 1em 1em 0; } + +.container-fluid .single .cover img { border: 1px solid #fff; box-sizing: border-box; + -webkit-box-shadow: 0 5px 8px -6px #777; + -moz-box-shadow: 0 5px 8px -6px #777; + box-shadow: 0 5px 8px -6px #777; +} + +.container-fluid .book .cover span img { + position: relative; + top: 0; + left: 0; height: 100%; - bottom: 0; - position: absolute; + border: 1px solid #fff; + box-sizing: border-box; -webkit-box-shadow: 0 5px 8px -6px #777; -moz-box-shadow: 0 5px 8px -6px #777; box-shadow: 0 5px 8px -6px #777; } .container-fluid .book .meta { margin-top: 10px; } +.media-body p { text-align: justify; } .container-fluid .book .meta p { margin: 0; } .container-fluid .book .meta .title { @@ -166,9 +202,10 @@ a, .danger,.book-remove, .editable-empty, .editable-empty:hover { color: #45b29d .container-fluid .book .meta .rating { margin-top: 5px; } .rating .glyphicon-star-empty { color: #444; } .rating .glyphicon-star.good { color: #444; } -.rating-clear .glyphicon-remove { color: #333 } +.rating-clear .glyphicon-remove { color: #333; } -.container-fluid .author .author-hidden, .container-fluid .author .author-hidden-divider { display: none; } +.container-fluid .author .author-hidden, +.container-fluid .author .author-hidden-divider { display: none; } .navbar-brand { font-family: 'Grand Hotel', cursive; @@ -182,7 +219,7 @@ a, .danger,.book-remove, .editable-empty, .editable-empty:hover { color: #45b29d border-top: 1px solid #ccc; } -.more-stuff>li { margin-bottom: 10px; } +.more-stuff > li { margin-bottom: 10px; } .navbar-collapse.in .navbar-nav { margin: 0; } span.glyphicon.glyphicon-tags { @@ -192,34 +229,38 @@ span.glyphicon.glyphicon-tags { } .book-meta { padding-bottom: 20px; } -.book-meta .tags a { display: inline; } -.book-meta .identifiers a { display: inline; } -.container-fluid .single .cover img { - border: 1px solid #fff; - box-sizing: border-box; - -webkit-box-shadow: 0 5px 8px -6px #777; - -moz-box-shadow: 0 5px 8px -6px #777; - box-shadow: 0 5px 8px -6px #777; +.navbar-default .navbar-toggle .icon-bar { background-color: #000; } +.navbar-default .navbar-toggle { border-color: #000; } + +.cover .badge { + position: absolute; + top: 2px; + left: 2px; + color: #000; + border-radius: 10px; + background-color: #fff; } -.navbar-default .navbar-toggle .icon-bar {background-color: #000; } -.navbar-default .navbar-toggle {border-color: #000; } -.cover { margin-bottom: 10px; } -.cover .badge{ - position: absolute; - top: 2px; - left: 2px; - background-color: #777; +.cover .read { + left: auto; + right: 2px; + width: 17px; + height: 17px; + display: inline-block; + padding: 2px; } -.cover-height { max-height: 100px;} +.cover-height { max-height: 100px; } .col-sm-2 a .cover-small { margin: 5px; max-height: 200px; } -.btn-file {position: relative; overflow: hidden;} +.btn-file { + position: relative; + overflow: hidden; +} .btn-file input[type=file] { position: absolute; @@ -237,24 +278,60 @@ span.glyphicon.glyphicon-tags { display: block; } -.btn-toolbar .btn,.discover .btn { margin-bottom: 5px; } -.button-link {color: #fff; } -.btn-primary:hover, .btn-primary:focus, .btn-primary:active, .btn-primary.active, .open .dropdown-toggle.btn-primary{ background-color: #1C5484; } -.btn-primary.disabled, .btn-primary[disabled], fieldset[disabled] .btn-primary, .btn-primary.disabled:hover, .btn-primary[disabled]:hover, fieldset[disabled] .btn-primary:hover, .btn-primary.disabled:focus, .btn-primary[disabled]:focus, fieldset[disabled] .btn-primary:focus, .btn-primary.disabled:active, .btn-primary[disabled]:active, fieldset[disabled] .btn-primary:active, .btn-primary.disabled.active, .btn-primary[disabled].active, fieldset[disabled] .btn-primary.active { background-color: #89B9E2; } -.btn-toolbar>.btn+.btn, .btn-toolbar>.btn-group+.btn, .btn-toolbar>.btn+.btn-group, .btn-toolbar>.btn-group+.btn-group { margin-left: 0px; } -.panel-body {background-color: #f5f5f5; } -.spinner {margin: 0 41%; } -.spinner2 {margin: 0 41%; } -.intend-form { margin-left:20px; } -table .bg-dark-danger {background-color: #d9534f; color: #fff; } -table .bg-dark-danger a {color: #fff; } -table .bg-dark-danger:hover {background-color: #c9302c; } -table .bg-primary:hover {background-color: #1C5484; } -table .bg-primary a {color: #fff; } -.block-label {display: block;} -.fake-input {position: absolute; pointer-events: none; top: 0; } +.btn-toolbar .btn, +.discover .btn { margin-bottom: 5px; } +.button-link { color: #fff; } -input.pill { position: absolute; opacity: 0; } +.btn-primary:hover, +.btn-primary:focus, +.btn-primary:active, +.btn-primary.active, +.open .dropdown-toggle.btn-primary { background-color: #1c5484; } + +.btn-primary.disabled, +.btn-primary[disabled], +fieldset[disabled] .btn-primary, +.btn-primary.disabled:hover, +.btn-primary[disabled]:hover, +fieldset[disabled] .btn-primary:hover, +.btn-primary.disabled:focus, +.btn-primary[disabled]:focus, +fieldset[disabled] .btn-primary:focus, +.btn-primary.disabled:active, +.btn-primary[disabled]:active, +fieldset[disabled] .btn-primary:active, +.btn-primary.disabled.active, +.btn-primary[disabled].active, +fieldset[disabled] .btn-primary.active { background-color: #89b9e2; } + +.btn-toolbar > .btn + .btn, +.btn-toolbar > .btn-group + .btn, +.btn-toolbar > .btn + .btn-group, +.btn-toolbar > .btn-group + .btn-group { margin-left: 0; } + +.panel-body { background-color: #f5f5f5; } +.spinner { margin: 0 41%; } +.spinner2 { margin: 0 41%; } +.intend-form { margin-left: 20px; } + +table .bg-dark-danger { + background-color: #d9534f; + color: #fff; +} +table .bg-dark-danger:hover { background-color: #c9302c; } +table .bg-primary:hover { background-color: #1c5484; } +.block-label { display: block; } + +.fake-input { + position: absolute; + pointer-events: none; + top: 0; +} + +input.pill { + position: absolute; + opacity: 0; +} input.pill + label { border: 2px solid #45b29d; @@ -276,12 +353,18 @@ input.pill:checked + label { input.pill:not(:checked) + label .glyphicon { display: none; } -.author-bio img { margin: 0 1em 1em 0; } -.author-link { display: inline-block; margin-top: 10px; width: 100px; } -.author-link img { display: block; height: 100%; } -#remove-from-shelves .btn, #shelf-action-errors { margin-left: 5px; } +.author-link { + display: inline-block; + margin-top: 10px; + width: 100px; +} -.tags_click, .serie_click, .language_click { margin-right: 5px; } +#remove-from-shelves .btn, +#shelf-action-errors { margin-left: 5px; } + +.tags_click, +.serie_click, +.language_click { margin-right: 5px; } #meta-info { height: 600px; @@ -289,7 +372,6 @@ input.pill:not(:checked) + label .glyphicon { display: none; } } .media-list { padding-right: 15px; } -.media-body p { text-align: justify; } #meta-info img { max-height: 150px; @@ -302,20 +384,20 @@ input.pill:not(:checked) + label .glyphicon { display: none; } #btn-upload-format { display: none; } .upload-cover-input-text { display: initial; } #btn-upload-cover { display: none; } -.panel-title > a { text-decoration: none; } + .editable-buttons { - display:inline-block; + display: inline-block; margin-left: 7px; } -.editable-input { display:inline-block; } +.editable-input { display: inline-block; } .editable-cancel { - margin-bottom: 0px !important; + margin-bottom: 0 !important; margin-left: 7px !important; } -.editable-submit { margin-bottom: 0px !important; } +.editable-submit { margin-bottom: 0 !important; } .filterheader { margin-bottom: 20px; } .errorlink { margin-top: 20px; } .emailconfig { margin-top: 10px; } @@ -326,7 +408,7 @@ input.pill:not(:checked) + label .glyphicon { display: none; } } div.log { - font-family: Courier New; + font-family: Courier New, serif; font-size: 12px; box-sizing: border-box; height: 700px; @@ -335,4 +417,3 @@ div.log { white-space: nowrap; padding: 0.5em; } - diff --git a/cps/static/js/archive/archive.js b/cps/static/js/archive/archive.js index 06c05624..cb76321f 100644 --- a/cps/static/js/archive/archive.js +++ b/cps/static/js/archive/archive.js @@ -411,19 +411,6 @@ bitjs.archive = bitjs.archive || {}; return "unrar.js"; }; - /** - * Unrarrer5 - * @extends {bitjs.archive.Unarchiver} - * @constructor - */ - bitjs.archive.Unrarrer5 = function(arrayBuffer, optPathToBitJS) { - bitjs.base(this, arrayBuffer, optPathToBitJS); - }; - bitjs.inherits(bitjs.archive.Unrarrer5, bitjs.archive.Unarchiver); - bitjs.archive.Unrarrer5.prototype.getScriptFileName = function() { - return "unrar5.js"; - }; - /** * Untarrer * @extends {bitjs.archive.Unarchiver} diff --git a/cps/static/js/archive/unrar.js b/cps/static/js/archive/unrar.js index 3e2a45af..fadb791e 100644 --- a/cps/static/js/archive/unrar.js +++ b/cps/static/js/archive/unrar.js @@ -14,10 +14,10 @@ /* global VM_FIXEDGLOBALSIZE, VM_GLOBALMEMSIZE, MAXWINMASK, VM_GLOBALMEMADDR, MAXWINSIZE */ // This file expects to be invoked as a Worker (see onmessage below). -/*importScripts("../io/bitstream.js"); +importScripts("../io/bitstream.js"); importScripts("../io/bytebuffer.js"); importScripts("archive.js"); -importScripts("rarvm.js");*/ +importScripts("rarvm.js"); // Progress variables. var currentFilename = ""; @@ -29,21 +29,19 @@ var totalFilesInArchive = 0; // Helper functions. var info = function(str) { - console.log(str); - // postMessage(new bitjs.archive.UnarchiveInfoEvent(str)); + postMessage(new bitjs.archive.UnarchiveInfoEvent(str)); }; var err = function(str) { - console.log(str); - // postMessage(new bitjs.archive.UnarchiveErrorEvent(str)); + postMessage(new bitjs.archive.UnarchiveErrorEvent(str)); }; var postProgress = function() { - /*postMessage(new bitjs.archive.UnarchiveProgressEvent( + postMessage(new bitjs.archive.UnarchiveProgressEvent( currentFilename, currentFileNumber, currentBytesUnarchivedInFile, currentBytesUnarchived, totalUncompressedBytesInArchive, - totalFilesInArchive));*/ + totalFilesInArchive)); }; // shows a byte value as its hex representation @@ -1300,7 +1298,7 @@ var unrar = function(arrayBuffer) { totalUncompressedBytesInArchive = 0; totalFilesInArchive = 0; - //postMessage(new bitjs.archive.UnarchiveStartEvent()); + postMessage(new bitjs.archive.UnarchiveStartEvent()); var bstream = new bitjs.io.BitStream(arrayBuffer, false /* rtl */); var header = new RarVolumeHeader(bstream); @@ -1350,7 +1348,7 @@ var unrar = function(arrayBuffer) { localfile.unrar(); if (localfile.isValid) { - // postMessage(new bitjs.archive.UnarchiveExtractEvent(localfile)); + postMessage(new bitjs.archive.UnarchiveExtractEvent(localfile)); postProgress(); } } @@ -1360,7 +1358,7 @@ var unrar = function(arrayBuffer) { } else { err("Invalid RAR file"); } - // postMessage(new bitjs.archive.UnarchiveFinishEvent()); + postMessage(new bitjs.archive.UnarchiveFinishEvent()); }; // event.data.file has the ArrayBuffer. diff --git a/cps/static/js/caliBlur.js b/cps/static/js/caliBlur.js index 73e1c320..45166834 100644 --- a/cps/static/js/caliBlur.js +++ b/cps/static/js/caliBlur.js @@ -145,44 +145,16 @@ if ($("body.book").length > 0) { $(".blur-wrapper") .prepend(''); - // Fix-up book detail headings - publisher = $(".publishers p span").text().split(":"); - $(".publishers p span").remove(); - $.each(publisher, function (i, val) { - $(".publishers").append("" + publisher[i] + ""); - }); - $(".publishers span:nth-child(3)").text(function () { - return $(this).text().replace(/^\s+|^\t+|\t+|\s+$/g, ""); - }); - - // Fix-up book custom colums headings - // real_custom_column = $( '.real_custom_columns' ).text().split( ':' ); - real_custom_column = $(".real_custom_columns"); - // $( ".real_custom_columns" ).remove(); - $.each(real_custom_column, function (i, val) { - var split = $(this).text().split(":"); - real_cc_key = split.shift(); - real_cc_value = split.join(":"); - $(this).text(""); - if (real_cc_value != "") { - $(this).append("" + real_cc_key + "" + real_cc_value + ""); + // Metadata Fields - Publishers, Published, Languages and Custom + $('.publishers, .publishing-date, .real_custom_columns, .languages').each(function () { + var splitText = $(this).text().split(':'); + var label = splitText.shift().trim(); + var value = splitText.join(':').trim(); + // Preserve Links + if ($(this).find('a').length) { + value = $(this).find('a').first().removeClass(); } - }); - //$( '.real_custom_columns:nth-child(3)' ).text(function() { - //return $(this).text().replace(/^\s+|^\t+|\t+|\s+$/g, ""); - //}); - - published = $(".publishing-date p") - .text().split(": "); - $(".publishing-date p").remove(); - $.each(published, function (i, val) { - $(".publishing-date").append("" + published[i] + ""); - }); - - languages = $(".languages p span").text().split(": "); - $(".languages p span").remove(); - $.each(languages, function (i, val) { - $(".languages").append("" + languages[i] + ""); + $(this).html('' + label + '').find('span').last().append(value); }); $(".book-meta h2:first").clone() @@ -246,11 +218,6 @@ if ($("body.book").length > 0) { $("#add-to-shelves").toggle(); }); - // Fix formatting error on book detail languages - if (!$(".book-meta > .bookinfo > .languages > span:last-of-type").text().startsWith(" ")) { - $(".book-meta > .bookinfo > .languages > span:last-of-type").prepend(" "); - } - //Work to reposition dropdowns. Does not currently solve for //screen resizing function dropdownToggle() { @@ -436,6 +403,39 @@ $("div.comments").readmore({ // End of Global Work // /////////////////////////////// +// Advanced Search Results +if($("body.advsearch").length > 0) { + $("#loader + .container-fluid") + .prepend(""); + $("#add-to-shelves").insertBefore(".blur-wrapper"); + $('div[aria-label="Add to shelves"]').click(function () { + $("#add-to-shelves").toggle(); + }); + $('#add-to-shelf').height("40px"); + function dropdownToggle() { + topPos = $("#add-to-shelf").offset().top-20; + if ($('div[aria-label="Add to shelves"]').length > 0) { + + position = $('div[aria-label="Add to shelves"]').offset().left + + if (position + $("#add-to-shelves").width() > $(window).width()) { + positionOff = position + $("#add-to-shelves").width() - $(window).width(); + adsPosition = position - positionOff - 5 + $("#add-to-shelves").attr("style", "left: " + adsPosition + "px !important; right: auto; top: " + topPos + "px"); + } else { + $("#add-to-shelves").attr("style", "left: " + position + "px !important; right: auto; top: " + topPos + "px"); + } + } + } + + dropdownToggle(); + + $(window).on("resize", function () { + dropdownToggle(); + }); + +} + // Author Page Background Blur if ($("body.author").length > 0) { cover = $(".author-bio img").attr("src"); @@ -710,7 +710,7 @@ $(".navbar-collapse.collapse.in").before('') // Get rid of leading white space recentlyAdded = $("#nav_new a:contains('Recently')").text().trim(); $("#nav_new a:contains('Recently')").contents().filter(function () { - return this.nodeType == 3 + return this.nodeType === 3 }).each(function () { this.textContent = this.textContent.replace(" Recently Added", recentlyAdded); }); diff --git a/cps/static/js/edit_books.js b/cps/static/js/edit_books.js index 35515aa1..8cedf688 100644 --- a/cps/static/js/edit_books.js +++ b/cps/static/js/edit_books.js @@ -1,7 +1,7 @@ /** * Created by SpeedProg on 05.04.2015. */ -/* global Bloodhound, language, Modernizr, tinymce */ +/* global Bloodhound, language, Modernizr, tinymce, getPath */ if ($("#description").length) { tinymce.init({ @@ -78,10 +78,10 @@ function prefixedSource(prefix, query, cb, bhAdapter) { }); } -function getPath() { +/*function getPath() { var jsFileLocation = $("script[src*=edit_books]").attr("src"); // the js file path return jsFileLocation.substr(0, jsFileLocation.search("/static/js/edit_books.js")); // the js folder path -} +}*/ var authors = new Bloodhound({ name: "authors", @@ -249,18 +249,26 @@ promisePublishers.done(function() { ); }); -$("#search").on("change input.typeahead:selected", function() { +$("#search").on("change input.typeahead:selected", function(event) { + if (event.target.type === "search" && event.target.tagName === "INPUT") { + return; + } var form = $("form").serialize(); $.getJSON( getPath() + "/get_matching_tags", form, function( data ) { $(".tags_click").each(function() { - if ($.inArray(parseInt($(this).children("input").first().val(), 10), data.tags) === -1 ) { - if (!($(this).hasClass("active"))) { - $(this).addClass("disabled"); + if ($.inArray(parseInt($(this).val(), 10), data.tags) === -1) { + if (!$(this).prop("selected")) { + $(this).prop("disabled", true); } } else { - $(this).removeClass("disabled"); + $(this).prop("disabled", false); } }); + $("#include_tag option:selected").each(function () { + $("#exclude_tag").find("[value=" + $(this).val() + "]").prop("disabled", true); + }); + $("#include_tag").selectpicker("refresh"); + $("#exclude_tag").selectpicker("refresh"); }); }); diff --git a/cps/static/js/filter_grid.js b/cps/static/js/filter_grid.js index 362c6bfa..623ffdc1 100644 --- a/cps/static/js/filter_grid.js +++ b/cps/static/js/filter_grid.js @@ -15,6 +15,8 @@ * along with this program. If not, see . */ +var direction = $("#asc").data('order'); // 0=Descending order; 1= ascending order + var $list = $("#list").isotope({ itemSelector: ".book", layoutMode: "fitRows", @@ -24,6 +26,9 @@ var $list = $("#list").isotope({ }); $("#desc").click(function() { + if (direction === 0) { + return; + } var page = $(this).data("id"); $.ajax({ method:"post", @@ -36,10 +41,12 @@ $("#desc").click(function() { sortBy: "name", sortAscending: true }); - return; }); $("#asc").click(function() { + if (direction === 1) { + return; + } var page = $(this).data("id"); $.ajax({ method:"post", @@ -52,19 +59,20 @@ $("#asc").click(function() { sortBy: "name", sortAscending: false }); - return; }); $("#all").click(function() { // go through all elements and make them visible $list.isotope({ filter: function() { return true; - } }) + } + }); }); $(".char").click(function() { var character = this.innerText; $list.isotope({ filter: function() { - return this.attributes["data-id"].value.charAt(0).toUpperCase() == character; - } }) + return this.attributes["data-id"].value.charAt(0).toUpperCase() === character; + } + }); }); diff --git a/cps/static/js/filter_list.js b/cps/static/js/filter_list.js index 676ff47b..b8f79f4e 100644 --- a/cps/static/js/filter_list.js +++ b/cps/static/js/filter_list.js @@ -15,20 +15,13 @@ * along with this program. If not, see . */ -var direction = 0; // Descending order +var direction = $("#asc").data('order'); // 0=Descending order; 1= ascending order var sort = 0; // Show sorted entries $("#sort_name").click(function() { - var class_name = $("h1").attr('Class') + "_sort_name"; + var className = $("h1").attr("Class") + "_sort_name"; var obj = {}; - obj[class_name] = sort; - /*$.ajax({ - method:"post", - contentType: "application/json; charset=utf-8", - dataType: "json", - url: window.location.pathname + "/../../ajax/view", - data: JSON.stringify({obj}), - });*/ + obj[className] = sort; var count = 0; var index = 0; @@ -95,7 +88,7 @@ $("#desc").click(function() { // Find count of middle element var count = $(".row:visible").length; if (count > 20) { - middle = parseInt(count / 2) + (count % 2); + middle = parseInt(count / 2, 10) + (count % 2); //var middle = parseInt(count / 2) + (count % 2); // search for the middle of all visible elements @@ -142,7 +135,7 @@ $("#asc").click(function() { // Find count of middle element var count = $(".row:visible").length; if (count > 20) { - var middle = parseInt(count / 2) + (count % 2); + var middle = parseInt(count / 2, 10) + (count % 2); //var middle = parseInt(count / 2) + (count % 2); // search for the middle of all visible elements diff --git a/cps/static/js/get_meta.js b/cps/static/js/get_meta.js index d3e0eb46..04c1d270 100644 --- a/cps/static/js/get_meta.js +++ b/cps/static/js/get_meta.js @@ -138,8 +138,8 @@ $(function () { seriesTitle = result.series.title; } var dateFomers = result.pubdate.split("-"); - var publishedYear = parseInt(dateFomers[0]); - var publishedMonth = parseInt(dateFomers[1]); + var publishedYear = parseInt(dateFomers[0], 10); + var publishedMonth = parseInt(dateFomers[1], 10); var publishedDate = new Date(publishedYear, publishedMonth - 1, 1); publishedDate = formatDate(publishedDate); @@ -194,8 +194,8 @@ $(function () { } else { dateFomers = result.date_added.split("-"); } - var publishedYear = parseInt(dateFomers[0]); - var publishedMonth = parseInt(dateFomers[1]); + var publishedYear = parseInt(dateFomers[0], 10); + var publishedMonth = parseInt(dateFomers[1], 10); var publishedDate = new Date(publishedYear, publishedMonth - 1, 1); publishedDate = formatDate(publishedDate); diff --git a/cps/static/js/kthoom.js b/cps/static/js/kthoom.js index bbb3fead..f6c1e4d7 100644 --- a/cps/static/js/kthoom.js +++ b/cps/static/js/kthoom.js @@ -141,9 +141,27 @@ var createURLFromArray = function(array, mimeType) { kthoom.ImageFile = function(file) { this.filename = file.filename; var fileExtension = file.filename.split(".").pop().toLowerCase(); - this.mimeType = fileExtension === "png" ? "image/png" : - (fileExtension === "jpg" || fileExtension === "jpeg") ? "image/jpeg" : - fileExtension === "gif" ? "image/gif" : fileExtension === "svg" ? "image/xml+svg" : undefined; + switch (fileExtension) { + case "jpg": + case "jpeg": + this.mimeType = "image/jpeg"; + break; + case "png": + this.mimeType = "image/png"; + break; + case "gif": + this.mimeType = "image/gif"; + break; + case "svg": + this.mimeType = "image/svg+xml"; + break; + case "webp": + this.mimeType = "image/webp"; + break; + default: + this.mimeType = undefined; + break; + } if ( this.mimeType !== undefined) { this.dataURI = createURLFromArray(file.fileData, this.mimeType); this.data = file; @@ -153,7 +171,10 @@ kthoom.ImageFile = function(file) { function initProgressClick() { $("#progress").click(function(e) { - var page = Math.max(1, Math.ceil((e.offsetX / $(this).width()) * totalImages)) - 1; + var offset = $(this).offset(); + var x = e.pageX - offset.left; + var rate = settings.direction === 0 ? x / $(this).width() : 1 - x / $(this).width(); + var page = Math.max(1, Math.ceil(rate * totalImages)) - 1; currentImage = page; updatePage(); }); @@ -162,15 +183,10 @@ function initProgressClick() { function loadFromArrayBuffer(ab) { var start = (new Date).getTime(); var h = new Uint8Array(ab, 0, 10); - unrar5(ab); var pathToBitJS = "../../static/js/archive/"; var lastCompletion = 0; - /*if (h[0] === 0x52 && h[1] === 0x61 && h[2] === 0x72 && h[3] === 0x21) { //Rar! - if (h[7] === 0x01) { - unarchiver = new bitjs.archive.Unrarrer(ab, pathToBitJS); - } else { - unarchiver = new bitjs.archive.Unrarrer5(ab, pathToBitJS); - } + 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 @@ -234,7 +250,7 @@ function loadFromArrayBuffer(ab) { unarchiver.start(); } else { alert("Some error"); - }*/ + } } function scrollTocToActive() { @@ -272,6 +288,22 @@ function updatePage() { } 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 + "%" }); @@ -336,7 +368,7 @@ function setImage(url) { $("#mainText").innerHTML(""); }; xhr.send(null); - } else if (!/(jpg|jpeg|png|gif)$/.test(imageFiles[currentImage].filename) && imageFiles[currentImage].data.uncompressedSize < 10 * 1024) { + } else if (!/(jpg|jpeg|png|gif|webp)$/.test(imageFiles[currentImage].filename) && imageFiles[currentImage].data.uncompressedSize < 10 * 1024) { xhr.open("GET", url, true); xhr.onload = function() { $("#mainText").css("display", ""); @@ -513,18 +545,17 @@ function keyHandler(evt) { break; case kthoom.Key.SPACE: var container = $("#mainContent"); - var atTop = container.scrollTop() === 0; - var atBottom = container.scrollTop() >= container[0].scrollHeight - container.height(); + // 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: @@ -640,6 +671,14 @@ function init(filename) { // Focus the scrollable area so that keyboard scrolling work as expected $("#mainContent").focus(); + $("#mainContent").swipe( { + swipeRight:function() { + showLeftPage(); + }, + swipeLeft:function() { + showRightPage(); + }, + }); $("#mainImage").click(function(evt) { // Firefox does not support offsetX/Y so we have to manually calculate // where the user clicked in the image. diff --git a/cps/static/js/libs/bootstrap-datepicker/locales/bootstrap-datepicker.pt_BR.min.js b/cps/static/js/libs/bootstrap-datepicker/locales/bootstrap-datepicker.pt_BR.min.js new file mode 100644 index 00000000..2d3f8afd --- /dev/null +++ b/cps/static/js/libs/bootstrap-datepicker/locales/bootstrap-datepicker.pt_BR.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates["pt-BR"]={days:["Domingo","Segunda","Terça","Quarta","Quinta","Sexta","Sábado"],daysShort:["Dom","Seg","Ter","Qua","Qui","Sex","Sáb"],daysMin:["Do","Se","Te","Qu","Qu","Se","Sa"],months:["Janeiro","Fevereiro","Março","Abril","Maio","Junho","Julho","Agosto","Setembro","Outubro","Novembro","Dezembro"],monthsShort:["Jan","Fev","Mar","Abr","Mai","Jun","Jul","Ago","Set","Out","Nov","Dez"],today:"Hoje",monthsTitle:"Meses",clear:"Limpar",format:"dd/mm/yyyy"}}(jQuery); \ No newline at end of file diff --git a/cps/static/js/libs/bootstrap-select.min.js b/cps/static/js/libs/bootstrap-select.min.js new file mode 100644 index 00000000..92e3a32e --- /dev/null +++ b/cps/static/js/libs/bootstrap-select.min.js @@ -0,0 +1,9 @@ +/*! + * Bootstrap-select v1.13.14 (https://developer.snapappointments.com/bootstrap-select) + * + * Copyright 2012-2020 SnapAppointments, LLC + * Licensed under MIT (https://github.com/snapappointments/bootstrap-select/blob/master/LICENSE) + */ + +!function(e,t){void 0===e&&void 0!==window&&(e=window),"function"==typeof define&&define.amd?define(["jquery"],function(e){return t(e)}):"object"==typeof module&&module.exports?module.exports=t(require("jquery")):t(e.jQuery)}(this,function(e){!function(z){"use strict";var d=["sanitize","whiteList","sanitizeFn"],r=["background","cite","href","itemtype","longdesc","poster","src","xlink:href"],e={"*":["class","dir","id","lang","role","tabindex","style",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],div:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},l=/^(?:(?:https?|mailto|ftp|tel|file):|[^&:/?#]*(?:[/?#]|$))/gi,a=/^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[a-z0-9+/]+=*$/i;function v(e,t){var i=e.nodeName.toLowerCase();if(-1!==z.inArray(i,t))return-1===z.inArray(i,r)||Boolean(e.nodeValue.match(l)||e.nodeValue.match(a));for(var s=z(t).filter(function(e,t){return t instanceof RegExp}),n=0,o=s.length;n]+>/g,"")),s&&(a=w(a)),a=a.toUpperCase(),o="contains"===i?0<=a.indexOf(t):a.startsWith(t)))break}return o}function L(e){return parseInt(e,10)||0}z.fn.triggerNative=function(e){var t,i=this[0];i.dispatchEvent?(u?t=new Event(e,{bubbles:!0}):(t=document.createEvent("Event")).initEvent(e,!0,!1),i.dispatchEvent(t)):i.fireEvent?((t=document.createEventObject()).eventType=e,i.fireEvent("on"+e,t)):this.trigger(e)};var f={"\xc0":"A","\xc1":"A","\xc2":"A","\xc3":"A","\xc4":"A","\xc5":"A","\xe0":"a","\xe1":"a","\xe2":"a","\xe3":"a","\xe4":"a","\xe5":"a","\xc7":"C","\xe7":"c","\xd0":"D","\xf0":"d","\xc8":"E","\xc9":"E","\xca":"E","\xcb":"E","\xe8":"e","\xe9":"e","\xea":"e","\xeb":"e","\xcc":"I","\xcd":"I","\xce":"I","\xcf":"I","\xec":"i","\xed":"i","\xee":"i","\xef":"i","\xd1":"N","\xf1":"n","\xd2":"O","\xd3":"O","\xd4":"O","\xd5":"O","\xd6":"O","\xd8":"O","\xf2":"o","\xf3":"o","\xf4":"o","\xf5":"o","\xf6":"o","\xf8":"o","\xd9":"U","\xda":"U","\xdb":"U","\xdc":"U","\xf9":"u","\xfa":"u","\xfb":"u","\xfc":"u","\xdd":"Y","\xfd":"y","\xff":"y","\xc6":"Ae","\xe6":"ae","\xde":"Th","\xfe":"th","\xdf":"ss","\u0100":"A","\u0102":"A","\u0104":"A","\u0101":"a","\u0103":"a","\u0105":"a","\u0106":"C","\u0108":"C","\u010a":"C","\u010c":"C","\u0107":"c","\u0109":"c","\u010b":"c","\u010d":"c","\u010e":"D","\u0110":"D","\u010f":"d","\u0111":"d","\u0112":"E","\u0114":"E","\u0116":"E","\u0118":"E","\u011a":"E","\u0113":"e","\u0115":"e","\u0117":"e","\u0119":"e","\u011b":"e","\u011c":"G","\u011e":"G","\u0120":"G","\u0122":"G","\u011d":"g","\u011f":"g","\u0121":"g","\u0123":"g","\u0124":"H","\u0126":"H","\u0125":"h","\u0127":"h","\u0128":"I","\u012a":"I","\u012c":"I","\u012e":"I","\u0130":"I","\u0129":"i","\u012b":"i","\u012d":"i","\u012f":"i","\u0131":"i","\u0134":"J","\u0135":"j","\u0136":"K","\u0137":"k","\u0138":"k","\u0139":"L","\u013b":"L","\u013d":"L","\u013f":"L","\u0141":"L","\u013a":"l","\u013c":"l","\u013e":"l","\u0140":"l","\u0142":"l","\u0143":"N","\u0145":"N","\u0147":"N","\u014a":"N","\u0144":"n","\u0146":"n","\u0148":"n","\u014b":"n","\u014c":"O","\u014e":"O","\u0150":"O","\u014d":"o","\u014f":"o","\u0151":"o","\u0154":"R","\u0156":"R","\u0158":"R","\u0155":"r","\u0157":"r","\u0159":"r","\u015a":"S","\u015c":"S","\u015e":"S","\u0160":"S","\u015b":"s","\u015d":"s","\u015f":"s","\u0161":"s","\u0162":"T","\u0164":"T","\u0166":"T","\u0163":"t","\u0165":"t","\u0167":"t","\u0168":"U","\u016a":"U","\u016c":"U","\u016e":"U","\u0170":"U","\u0172":"U","\u0169":"u","\u016b":"u","\u016d":"u","\u016f":"u","\u0171":"u","\u0173":"u","\u0174":"W","\u0175":"w","\u0176":"Y","\u0177":"y","\u0178":"Y","\u0179":"Z","\u017b":"Z","\u017d":"Z","\u017a":"z","\u017c":"z","\u017e":"z","\u0132":"IJ","\u0133":"ij","\u0152":"Oe","\u0153":"oe","\u0149":"'n","\u017f":"s"},m=/[\xc0-\xd6\xd8-\xf6\xf8-\xff\u0100-\u017f]/g,g=RegExp("[\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff\\u1ab0-\\u1aff\\u1dc0-\\u1dff]","g");function b(e){return f[e]}function w(e){return(e=e.toString())&&e.replace(m,b).replace(g,"")}var I,x,y,$,S=(I={"&":"&","<":"<",">":">",'"':""","'":"'","`":"`"},x="(?:"+Object.keys(I).join("|")+")",y=RegExp(x),$=RegExp(x,"g"),function(e){return e=null==e?"":""+e,y.test(e)?e.replace($,E):e});function E(e){return I[e]}var C={32:" ",48:"0",49:"1",50:"2",51:"3",52:"4",53:"5",54:"6",55:"7",56:"8",57:"9",59:";",65:"A",66:"B",67:"C",68:"D",69:"E",70:"F",71:"G",72:"H",73:"I",74:"J",75:"K",76:"L",77:"M",78:"N",79:"O",80:"P",81:"Q",82:"R",83:"S",84:"T",85:"U",86:"V",87:"W",88:"X",89:"Y",90:"Z",96:"0",97:"1",98:"2",99:"3",100:"4",101:"5",102:"6",103:"7",104:"8",105:"9"},N=27,D=13,H=32,W=9,B=38,M=40,R={success:!1,major:"3"};try{R.full=(z.fn.dropdown.Constructor.VERSION||"").split(" ")[0].split("."),R.major=R.full[0],R.success=!0}catch(e){}var U=0,j=".bs.select",V={DISABLED:"disabled",DIVIDER:"divider",SHOW:"open",DROPUP:"dropup",MENU:"dropdown-menu",MENURIGHT:"dropdown-menu-right",MENULEFT:"dropdown-menu-left",BUTTONCLASS:"btn-default",POPOVERHEADER:"popover-title",ICONBASE:"glyphicon",TICKICON:"glyphicon-ok"},F={MENU:"."+V.MENU},_={span:document.createElement("span"),i:document.createElement("i"),subtext:document.createElement("small"),a:document.createElement("a"),li:document.createElement("li"),whitespace:document.createTextNode("\xa0"),fragment:document.createDocumentFragment()};_.a.setAttribute("role","option"),"4"===R.major&&(_.a.className="dropdown-item"),_.subtext.className="text-muted",_.text=_.span.cloneNode(!1),_.text.className="text",_.checkMark=_.span.cloneNode(!1);var G=new RegExp(B+"|"+M),q=new RegExp("^"+W+"$|"+N),K={li:function(e,t,i){var s=_.li.cloneNode(!1);return e&&(1===e.nodeType||11===e.nodeType?s.appendChild(e):s.innerHTML=e),void 0!==t&&""!==t&&(s.className=t),null!=i&&s.classList.add("optgroup-"+i),s},a:function(e,t,i){var s=_.a.cloneNode(!0);return e&&(11===e.nodeType?s.appendChild(e):s.insertAdjacentHTML("beforeend",e)),void 0!==t&&""!==t&&s.classList.add.apply(s.classList,t.split(" ")),i&&s.setAttribute("style",i),s},text:function(e,t){var i,s,n=_.text.cloneNode(!1);if(e.content)n.innerHTML=e.content;else{if(n.textContent=e.text,e.icon){var o=_.whitespace.cloneNode(!1);(s=(!0===t?_.i:_.span).cloneNode(!1)).className=this.options.iconBase+" "+e.icon,_.fragment.appendChild(s),_.fragment.appendChild(o)}e.subtext&&((i=_.subtext.cloneNode(!1)).textContent=e.subtext,n.appendChild(i))}if(!0===t)for(;0'},maxOptions:!1,mobile:!1,selectOnTab:!1,dropdownAlignRight:!1,windowPadding:0,virtualScroll:600,display:!1,sanitize:!0,sanitizeFn:null,whiteList:e},Y.prototype={constructor:Y,init:function(){var i=this,e=this.$element.attr("id");U++,this.selectId="bs-select-"+U,this.$element[0].classList.add("bs-select-hidden"),this.multiple=this.$element.prop("multiple"),this.autofocus=this.$element.prop("autofocus"),this.$element[0].classList.contains("show-tick")&&(this.options.showTick=!0),this.$newElement=this.createDropdown(),this.buildData(),this.$element.after(this.$newElement).prependTo(this.$newElement),this.$button=this.$newElement.children("button"),this.$menu=this.$newElement.children(F.MENU),this.$menuInner=this.$menu.children(".inner"),this.$searchbox=this.$menu.find("input"),this.$element[0].classList.remove("bs-select-hidden"),!0===this.options.dropdownAlignRight&&this.$menu[0].classList.add(V.MENURIGHT),void 0!==e&&this.$button.attr("data-id",e),this.checkDisabled(),this.clickListener(),this.options.liveSearch?(this.liveSearchListener(),this.focusedParent=this.$searchbox[0]):this.focusedParent=this.$menuInner[0],this.setStyle(),this.render(),this.setWidth(),this.options.container?this.selectPosition():this.$element.on("hide"+j,function(){if(i.isVirtual()){var e=i.$menuInner[0],t=e.firstChild.cloneNode(!1);e.replaceChild(t,e.firstChild),e.scrollTop=0}}),this.$menu.data("this",this),this.$newElement.data("this",this),this.options.mobile&&this.mobile(),this.$newElement.on({"hide.bs.dropdown":function(e){i.$element.trigger("hide"+j,e)},"hidden.bs.dropdown":function(e){i.$element.trigger("hidden"+j,e)},"show.bs.dropdown":function(e){i.$element.trigger("show"+j,e)},"shown.bs.dropdown":function(e){i.$element.trigger("shown"+j,e)}}),i.$element[0].hasAttribute("required")&&this.$element.on("invalid"+j,function(){i.$button[0].classList.add("bs-invalid"),i.$element.on("shown"+j+".invalid",function(){i.$element.val(i.$element.val()).off("shown"+j+".invalid")}).on("rendered"+j,function(){this.validity.valid&&i.$button[0].classList.remove("bs-invalid"),i.$element.off("rendered"+j)}),i.$button.on("blur"+j,function(){i.$element.trigger("focus").trigger("blur"),i.$button.off("blur"+j)})}),setTimeout(function(){i.buildList(),i.$element.trigger("loaded"+j)})},createDropdown:function(){var e=this.multiple||this.options.showTick?" show-tick":"",t=this.multiple?' aria-multiselectable="true"':"",i="",s=this.autofocus?" autofocus":"";R.major<4&&this.$element.parent().hasClass("input-group")&&(i=" input-group-btn");var n,o="",r="",l="",a="";return this.options.header&&(o='×'+this.options.header+""),this.options.liveSearch&&(r=''),this.multiple&&this.options.actionsBox&&(l=''+this.options.selectAllText+''+this.options.deselectAllText+""),this.multiple&&this.options.doneButton&&(a=''+this.options.doneButtonText+""),n=' '+("4"===R.major?"":''+this.options.template.caret+"")+''+o+r+l+''+a+"",z(n)},setPositionData:function(){this.selectpicker.view.canHighlight=[];for(var e=this.selectpicker.view.size=0;e=this.options.virtualScroll||!0===this.options.virtualScroll},createView:function(A,e,t){var L,N,D=this,i=0,H=[];if(this.selectpicker.isSearching=A,this.selectpicker.current=A?this.selectpicker.search:this.selectpicker.main,this.setPositionData(),e)if(t)i=this.$menuInner[0].scrollTop;else if(!D.multiple){var s=D.$element[0],n=(s.options[s.selectedIndex]||{}).liIndex;if("number"==typeof n&&!1!==D.options.size){var o=D.selectpicker.main.data[n],r=o&&o.position;r&&(i=r-(D.sizeInfo.menuInnerHeight+D.sizeInfo.liHeight)/2)}}function l(e,t){var i,s,n,o,r,l,a,c,d=D.selectpicker.current.elements.length,h=[],p=!0,u=D.isVirtual();D.selectpicker.view.scrollTop=e,i=Math.ceil(D.sizeInfo.menuInnerHeight/D.sizeInfo.liHeight*1.5),s=Math.round(d/i)||1;for(var f=0;fd-1?0:D.selectpicker.current.data[d-1].position-D.selectpicker.current.data[D.selectpicker.view.position1-1].position,b.firstChild.style.marginTop=v+"px",b.firstChild.style.marginBottom=g+"px"):(b.firstChild.style.marginTop=0,b.firstChild.style.marginBottom=0),b.firstChild.appendChild(w),!0===u&&D.sizeInfo.hasScrollBar){var C=b.firstChild.offsetWidth;if(t&&CD.sizeInfo.selectWidth)b.firstChild.style.minWidth=D.sizeInfo.menuInnerInnerWidth+"px";else if(C>D.sizeInfo.menuInnerInnerWidth){D.$menu[0].style.minWidth=0;var O=b.firstChild.offsetWidth;O>D.sizeInfo.menuInnerInnerWidth&&(D.sizeInfo.menuInnerInnerWidth=O,b.firstChild.style.minWidth=D.sizeInfo.menuInnerInnerWidth+"px"),D.$menu[0].style.minWidth=""}}}if(D.prevActiveIndex=D.activeIndex,D.options.liveSearch){if(A&&t){var z,T=0;D.selectpicker.view.canHighlight[T]||(T=1+D.selectpicker.view.canHighlight.slice(1).indexOf(!0)),z=D.selectpicker.view.visibleElements[T],D.defocusItem(D.selectpicker.view.currentActive),D.activeIndex=(D.selectpicker.current.data[T]||{}).index,D.focusItem(z)}}else D.$menuInner.trigger("focus")}l(i,!0),this.$menuInner.off("scroll.createView").on("scroll.createView",function(e,t){D.noScroll||l(this.scrollTop,t),D.noScroll=!1}),z(window).off("resize"+j+"."+this.selectId+".createView").on("resize"+j+"."+this.selectId+".createView",function(){D.$newElement.hasClass(V.SHOW)&&l(D.$menuInner[0].scrollTop)})},focusItem:function(e,t,i){if(e){t=t||this.selectpicker.main.data[this.activeIndex];var s=e.firstChild;s&&(s.setAttribute("aria-setsize",this.selectpicker.view.size),s.setAttribute("aria-posinset",t.posinset),!0!==i&&(this.focusedParent.setAttribute("aria-activedescendant",s.id),e.classList.add("active"),s.classList.add("active")))}},defocusItem:function(e){e&&(e.classList.remove("active"),e.firstChild&&e.firstChild.classList.remove("active"))},setPlaceholder:function(){var e=!1;if(this.options.title&&!this.multiple){this.selectpicker.view.titleOption||(this.selectpicker.view.titleOption=document.createElement("option")),e=!0;var t=this.$element[0],i=!1,s=!this.selectpicker.view.titleOption.parentNode;if(s)this.selectpicker.view.titleOption.className="bs-title-option",this.selectpicker.view.titleOption.value="",i=void 0===z(t.options[t.selectedIndex]).attr("selected")&&void 0===this.$element.data("selected");!s&&0===this.selectpicker.view.titleOption.index||t.insertBefore(this.selectpicker.view.titleOption,t.firstChild),i&&(t.selectedIndex=0)}return e},buildData:function(){var p=':not([hidden]):not([data-hidden="true"])',u=[],f=0,e=this.setPlaceholder()?1:0;this.options.hideDisabled&&(p+=":not(:disabled)");var t=this.$element[0].querySelectorAll("select > *"+p);function m(e){var t=u[u.length-1];t&&"divider"===t.type&&(t.optID||e.optID)||((e=e||{}).type="divider",u.push(e))}function v(e,t){if((t=t||{}).divider="true"===e.getAttribute("data-divider"),t.divider)m({optID:t.optID});else{var i=u.length,s=e.style.cssText,n=s?S(s):"",o=(e.className||"")+(t.optgroupClass||"");t.optID&&(o="opt "+o),t.optionClass=o.trim(),t.inlineStyle=n,t.text=e.textContent,t.content=e.getAttribute("data-content"),t.tokens=e.getAttribute("data-tokens"),t.subtext=e.getAttribute("data-subtext"),t.icon=e.getAttribute("data-icon"),e.liIndex=i,t.display=t.content||t.text,t.type="option",t.index=i,t.option=e,t.selected=!!e.selected,t.disabled=t.disabled||!!e.disabled,u.push(t)}}function i(e,t){var i=t[e],s=t[e-1],n=t[e+1],o=i.querySelectorAll("option"+p);if(o.length){var r,l,a={display:S(i.label),subtext:i.getAttribute("data-subtext"),icon:i.getAttribute("data-icon"),type:"optgroup-label",optgroupClass:" "+(i.className||"")};f++,s&&m({optID:f}),a.optID=f,u.push(a);for(var c=0,d=o.length;c li")},render:function(){var e,t=this,i=this.$element[0],s=this.setPlaceholder()&&0===i.selectedIndex,n=O(i,this.options.hideDisabled),o=n.length,r=this.$button[0],l=r.querySelector(".filter-option-inner-inner"),a=document.createTextNode(this.options.multipleSeparator),c=_.fragment.cloneNode(!1),d=!1;if(r.classList.toggle("bs-placeholder",t.multiple?!o:!T(i,n)),this.tabIndex(),"static"===this.options.selectedTextFormat)c=K.text.call(this,{text:this.options.title},!0);else if(!1===(this.multiple&&-1!==this.options.selectedTextFormat.indexOf("count")&&1")).length&&o>e[1]||1===e.length&&2<=o))){if(!s){for(var h=0;h option"+m+", optgroup"+m+" option"+m).length,g="function"==typeof this.options.countSelectedText?this.options.countSelectedText(o,v):this.options.countSelectedText;c=K.text.call(this,{text:g.replace("{0}",o.toString()).replace("{1}",v.toString())},!0)}if(null==this.options.title&&(this.options.title=this.$element.attr("title")),c.childNodes.length||(c=K.text.call(this,{text:void 0!==this.options.title?this.options.title:this.options.noneSelectedText},!0)),r.title=c.textContent.replace(/<[^>]*>?/g,"").trim(),this.options.sanitize&&d&&P([c],t.options.whiteList,t.options.sanitizeFn),l.innerHTML="",l.appendChild(c),R.major<4&&this.$newElement[0].classList.contains("bs3-has-addon")){var b=r.querySelector(".filter-expand"),w=l.cloneNode(!0);w.className="filter-expand",b?r.replaceChild(w,b):r.appendChild(w)}this.$element.trigger("rendered"+j)},setStyle:function(e,t){var i,s=this.$button[0],n=this.$newElement[0],o=this.options.style.trim();this.$element.attr("class")&&this.$newElement.addClass(this.$element.attr("class").replace(/selectpicker|mobile-device|bs-select-hidden|validate\[.*\]/gi,"")),R.major<4&&(n.classList.add("bs3"),n.parentNode.classList.contains("input-group")&&(n.previousElementSibling||n.nextElementSibling)&&(n.previousElementSibling||n.nextElementSibling).classList.contains("input-group-addon")&&n.classList.add("bs3-has-addon")),i=e?e.trim():o,"add"==t?i&&s.classList.add.apply(s.classList,i.split(" ")):"remove"==t?i&&s.classList.remove.apply(s.classList,i.split(" ")):(o&&s.classList.remove.apply(s.classList,o.split(" ")),i&&s.classList.add.apply(s.classList,i.split(" ")))},liHeight:function(e){if(e||!1!==this.options.size&&!Object.keys(this.sizeInfo).length){var t=document.createElement("div"),i=document.createElement("div"),s=document.createElement("div"),n=document.createElement("ul"),o=document.createElement("li"),r=document.createElement("li"),l=document.createElement("li"),a=document.createElement("a"),c=document.createElement("span"),d=this.options.header&&0this.sizeInfo.menuExtras.vert&&l+this.sizeInfo.menuExtras.vert+50>this.sizeInfo.selectOffsetBot,!0===this.selectpicker.isSearching&&(a=this.selectpicker.dropup),this.$newElement.toggleClass(V.DROPUP,a),this.selectpicker.dropup=a),"auto"===this.options.size)n=3this.options.size){for(var b=0;bthis.sizeInfo.menuInnerHeight&&(this.sizeInfo.hasScrollBar=!0,this.sizeInfo.totalMenuWidth=this.sizeInfo.menuWidth+this.sizeInfo.scrollBarWidth),"auto"===this.options.dropdownAlignRight&&this.$menu.toggleClass(V.MENURIGHT,this.sizeInfo.selectOffsetLeft>this.sizeInfo.selectOffsetRight&&this.sizeInfo.selectOffsetRightthis.options.size&&i.off("resize"+j+"."+this.selectId+".setMenuSize scroll"+j+"."+this.selectId+".setMenuSize")}this.createView(!1,!0,e)},setWidth:function(){var i=this;"auto"===this.options.width?requestAnimationFrame(function(){i.$menu.css("min-width","0"),i.$element.on("loaded"+j,function(){i.liHeight(),i.setMenuSize();var e=i.$newElement.clone().appendTo("body"),t=e.css("width","auto").children("button").outerWidth();e.remove(),i.sizeInfo.selectWidth=Math.max(i.sizeInfo.totalMenuWidth,t),i.$newElement.css("width",i.sizeInfo.selectWidth+"px")})}):"fit"===this.options.width?(this.$menu.css("min-width",""),this.$newElement.css("width","").addClass("fit-width")):this.options.width?(this.$menu.css("min-width",""),this.$newElement.css("width",this.options.width)):(this.$menu.css("min-width",""),this.$newElement.css("width","")),this.$newElement.hasClass("fit-width")&&"fit"!==this.options.width&&this.$newElement[0].classList.remove("fit-width")},selectPosition:function(){this.$bsContainer=z('');function e(e){var t={},i=r.options.display||!!z.fn.dropdown.Constructor.Default&&z.fn.dropdown.Constructor.Default.display;r.$bsContainer.addClass(e.attr("class").replace(/form-control|fit-width/gi,"")).toggleClass(V.DROPUP,e.hasClass(V.DROPUP)),s=e.offset(),l.is("body")?n={top:0,left:0}:((n=l.offset()).top+=parseInt(l.css("borderTopWidth"))-l.scrollTop(),n.left+=parseInt(l.css("borderLeftWidth"))-l.scrollLeft()),o=e.hasClass(V.DROPUP)?0:e[0].offsetHeight,(R.major<4||"static"===i)&&(t.top=s.top-n.top+o,t.left=s.left-n.left),t.width=e[0].offsetWidth,r.$bsContainer.css(t)}var s,n,o,r=this,l=z(this.options.container);this.$button.on("click.bs.dropdown.data-api",function(){r.isDisabled()||(e(r.$newElement),r.$bsContainer.appendTo(r.options.container).toggleClass(V.SHOW,!r.$button.hasClass(V.SHOW)).append(r.$menu))}),z(window).off("resize"+j+"."+this.selectId+" scroll"+j+"."+this.selectId).on("resize"+j+"."+this.selectId+" scroll"+j+"."+this.selectId,function(){r.$newElement.hasClass(V.SHOW)&&e(r.$newElement)}),this.$element.on("hide"+j,function(){r.$menu.data("height",r.$menu.height()),r.$bsContainer.detach()})},setOptionStatus:function(e){var t=this;if(t.noScroll=!1,t.selectpicker.view.visibleElements&&t.selectpicker.view.visibleElements.length)for(var i=0;i');y[2]&&($=$.replace("{var}",y[2][1"+$+"")),d=!1,C.$element.trigger("maxReached"+j)),g&&w&&(E.append(z(""+S+"")),d=!1,C.$element.trigger("maxReachedGrp"+j)),setTimeout(function(){C.setSelected(r,!1)},10),E[0].classList.add("fadeOut"),setTimeout(function(){E.remove()},1050)}}}else c&&(c.selected=!1),h.selected=!0,C.setSelected(r,!0);!C.multiple||C.multiple&&1===C.options.maxOptions?C.$button.trigger("focus"):C.options.liveSearch&&C.$searchbox.trigger("focus"),d&&(!C.multiple&&a===s.selectedIndex||(A=[h.index,p.prop("selected"),l],C.$element.triggerNative("change")))}}),this.$menu.on("click","li."+V.DISABLED+" a, ."+V.POPOVERHEADER+", ."+V.POPOVERHEADER+" :not(.close)",function(e){e.currentTarget==this&&(e.preventDefault(),e.stopPropagation(),C.options.liveSearch&&!z(e.target).hasClass("close")?C.$searchbox.trigger("focus"):C.$button.trigger("focus"))}),this.$menuInner.on("click",".divider, .dropdown-header",function(e){e.preventDefault(),e.stopPropagation(),C.options.liveSearch?C.$searchbox.trigger("focus"):C.$button.trigger("focus")}),this.$menu.on("click","."+V.POPOVERHEADER+" .close",function(){C.$button.trigger("click")}),this.$searchbox.on("click",function(e){e.stopPropagation()}),this.$menu.on("click",".actions-btn",function(e){C.options.liveSearch?C.$searchbox.trigger("focus"):C.$button.trigger("focus"),e.preventDefault(),e.stopPropagation(),z(this).hasClass("bs-select-all")?C.selectAll():C.deselectAll()}),this.$element.on("change"+j,function(){C.render(),C.$element.trigger("changed"+j,A),A=null}).on("focus"+j,function(){C.options.mobile||C.$button.trigger("focus")})},liveSearchListener:function(){var u=this,f=document.createElement("li");this.$button.on("click.bs.dropdown.data-api",function(){u.$searchbox.val()&&u.$searchbox.val("")}),this.$searchbox.on("click.bs.dropdown.data-api focus.bs.dropdown.data-api touchend.bs.dropdown.data-api",function(e){e.stopPropagation()}),this.$searchbox.on("input propertychange",function(){var e=u.$searchbox.val();if(u.selectpicker.search.elements=[],u.selectpicker.search.data=[],e){var t=[],i=e.toUpperCase(),s={},n=[],o=u._searchStyle(),r=u.options.liveSearchNormalize;r&&(i=w(i));for(var l=0;l=a.selectpicker.view.canHighlight.length&&(t=0),a.selectpicker.view.canHighlight[t+f]||(t=t+1+a.selectpicker.view.canHighlight.slice(t+f+1).indexOf(!0))),e.preventDefault();var m=f+t;e.which===B?0===f&&t===c.length-1?(a.$menuInner[0].scrollTop=a.$menuInner[0].scrollHeight,m=a.selectpicker.current.elements.length-1):d=(o=(n=a.selectpicker.current.data[m]).position-n.height)u+a.sizeInfo.menuInnerHeight),s=a.selectpicker.main.elements[v],a.activeIndex=b[x],a.focusItem(s),s&&s.firstChild.focus(),d&&(a.$menuInner[0].scrollTop=o),r.trigger("focus")}}i&&(e.which===H&&!a.selectpicker.keydown.keyHistory||e.which===D||e.which===W&&a.options.selectOnTab)&&(e.which!==H&&e.preventDefault(),a.options.liveSearch&&e.which===H||(a.$menuInner.find(".active a").trigger("click",!0),r.trigger("focus"),a.options.liveSearch||(e.preventDefault(),z(document).data("spaceSelect",!0))))}},mobile:function(){this.$element[0].classList.add("mobile-device")},refresh:function(){var e=z.extend({},this.options,this.$element.data());this.options=e,this.checkDisabled(),this.setStyle(),this.render(),this.buildData(),this.buildList(),this.setWidth(),this.setSize(!0),this.$element.trigger("refreshed"+j)},hide:function(){this.$newElement.hide()},show:function(){this.$newElement.show()},remove:function(){this.$newElement.remove(),this.$element.remove()},destroy:function(){this.$newElement.before(this.$element).remove(),this.$bsContainer?this.$bsContainer.remove():this.$menu.remove(),this.$element.off(j).removeData("selectpicker").removeClass("bs-select-hidden selectpicker"),z(window).off(j+"."+this.selectId)}};var J=z.fn.selectpicker;z.fn.selectpicker=Z,z.fn.selectpicker.Constructor=Y,z.fn.selectpicker.noConflict=function(){return z.fn.selectpicker=J,this};var Q=z.fn.dropdown.Constructor._dataApiKeydownHandler||z.fn.dropdown.Constructor.prototype.keydown;z(document).off("keydown.bs.dropdown.data-api").on("keydown.bs.dropdown.data-api",':not(.bootstrap-select) > [data-toggle="dropdown"]',Q).on("keydown.bs.dropdown.data-api",":not(.bootstrap-select) > .dropdown-menu",Q).on("keydown"+j,'.bootstrap-select [data-toggle="dropdown"], .bootstrap-select [role="listbox"], .bootstrap-select .bs-searchbox input',Y.prototype.keydown).on("focusin.modal",'.bootstrap-select [data-toggle="dropdown"], .bootstrap-select [role="listbox"], .bootstrap-select .bs-searchbox input',function(e){e.stopPropagation()}),z(window).on("load"+j+".data-api",function(){z(".selectpicker").each(function(){var e=z(this);Z.call(e,e.data())})})}(e)}); +//# sourceMappingURL=bootstrap-select.min.js.map \ No newline at end of file diff --git a/cps/static/js/libs/bootstrap-select/defaults-cs.min.js b/cps/static/js/libs/bootstrap-select/defaults-cs.min.js new file mode 100644 index 00000000..be309a10 --- /dev/null +++ b/cps/static/js/libs/bootstrap-select/defaults-cs.min.js @@ -0,0 +1,8 @@ +/*! + * Bootstrap-select v1.13.14 (https://developer.snapappointments.com/bootstrap-select) + * + * Copyright 2012-2020 SnapAppointments, LLC + * Licensed under MIT (https://github.com/snapappointments/bootstrap-select/blob/master/LICENSE) + */ + +!function(e,n){void 0===e&&void 0!==window&&(e=window),"function"==typeof define&&define.amd?define(["jquery"],function(e){return n(e)}):"object"==typeof module&&module.exports?module.exports=n(require("jquery")):n(e.jQuery)}(this,function(e){e.fn.selectpicker.defaults={noneSelectedText:"Vyberte ze seznamu",noneResultsText:"Pro hled\xe1n\xed {0} nebyly nalezeny \u017e\xe1dn\xe9 v\xfdsledky",countSelectedText:"Vybran\xe9 {0} z {1}",maxOptionsText:["Limit p\u0159ekro\u010den ({n} {var} max)","Limit skupiny p\u0159ekro\u010den ({n} {var} max)",["polo\u017eek","polo\u017eka"]],multipleSeparator:", ",selectAllText:"Vybrat v\u0161e",deselectAllText:"Zru\u0161it v\xfdb\u011br"}}); \ No newline at end of file diff --git a/cps/static/js/libs/bootstrap-select/defaults-de.min.js b/cps/static/js/libs/bootstrap-select/defaults-de.min.js new file mode 100644 index 00000000..e625440b --- /dev/null +++ b/cps/static/js/libs/bootstrap-select/defaults-de.min.js @@ -0,0 +1,8 @@ +/*! + * Bootstrap-select v1.13.14 (https://developer.snapappointments.com/bootstrap-select) + * + * Copyright 2012-2020 SnapAppointments, LLC + * Licensed under MIT (https://github.com/snapappointments/bootstrap-select/blob/master/LICENSE) + */ + +!function(e,t){void 0===e&&void 0!==window&&(e=window),"function"==typeof define&&define.amd?define(["jquery"],function(e){return t(e)}):"object"==typeof module&&module.exports?module.exports=t(require("jquery")):t(e.jQuery)}(this,function(e){e.fn.selectpicker.defaults={noneSelectedText:"Bitte w\xe4hlen...",noneResultsText:"Keine Ergebnisse f\xfcr {0}",countSelectedText:function(e,t){return 1==e?"{0} Element ausgew\xe4hlt":"{0} Elemente ausgew\xe4hlt"},maxOptionsText:function(e,t){return[1==e?"Limit erreicht ({n} Element max.)":"Limit erreicht ({n} Elemente max.)",1==t?"Gruppen-Limit erreicht ({n} Element max.)":"Gruppen-Limit erreicht ({n} Elemente max.)"]},selectAllText:"Alles ausw\xe4hlen",deselectAllText:"Nichts ausw\xe4hlen",multipleSeparator:", "}}); \ No newline at end of file diff --git a/cps/static/js/libs/bootstrap-select/defaults-es.min.js b/cps/static/js/libs/bootstrap-select/defaults-es.min.js new file mode 100644 index 00000000..25efec39 --- /dev/null +++ b/cps/static/js/libs/bootstrap-select/defaults-es.min.js @@ -0,0 +1,8 @@ +/*! + * Bootstrap-select v1.13.14 (https://developer.snapappointments.com/bootstrap-select) + * + * Copyright 2012-2020 SnapAppointments, LLC + * Licensed under MIT (https://github.com/snapappointments/bootstrap-select/blob/master/LICENSE) + */ + +!function(e,o){void 0===e&&void 0!==window&&(e=window),"function"==typeof define&&define.amd?define(["jquery"],function(e){return o(e)}):"object"==typeof module&&module.exports?module.exports=o(require("jquery")):o(e.jQuery)}(this,function(e){e.fn.selectpicker.defaults={noneSelectedText:"No hay selecci\xf3n",noneResultsText:"No hay resultados {0}",countSelectedText:"Seleccionados {0} de {1}",maxOptionsText:["L\xedmite alcanzado ({n} {var} max)","L\xedmite del grupo alcanzado({n} {var} max)",["elementos","element"]],multipleSeparator:", ",selectAllText:"Seleccionar Todos",deselectAllText:"Desmarcar Todos"}}); \ No newline at end of file diff --git a/cps/static/js/libs/bootstrap-select/defaults-fi.min.js b/cps/static/js/libs/bootstrap-select/defaults-fi.min.js new file mode 100644 index 00000000..bee14048 --- /dev/null +++ b/cps/static/js/libs/bootstrap-select/defaults-fi.min.js @@ -0,0 +1,8 @@ +/*! + * Bootstrap-select v1.13.14 (https://developer.snapappointments.com/bootstrap-select) + * + * Copyright 2012-2020 SnapAppointments, LLC + * Licensed under MIT (https://github.com/snapappointments/bootstrap-select/blob/master/LICENSE) + */ + +!function(e,t){void 0===e&&void 0!==window&&(e=window),"function"==typeof define&&define.amd?define(["jquery"],function(e){return t(e)}):"object"==typeof module&&module.exports?module.exports=t(require("jquery")):t(e.jQuery)}(this,function(e){e.fn.selectpicker.defaults={noneSelectedText:"Ei valintoja",noneResultsText:"Ei hakutuloksia {0}",countSelectedText:function(e,t){return 1==e?"{0} valittu":"{0} valitut"},maxOptionsText:function(e,t){return["Valintojen maksimim\xe4\xe4r\xe4 ({n} saavutettu)","Ryhm\xe4n maksimim\xe4\xe4r\xe4 ({n} saavutettu)"]},selectAllText:"Valitse kaikki",deselectAllText:"Poista kaikki",multipleSeparator:", "}}); \ No newline at end of file diff --git a/cps/static/js/libs/bootstrap-select/defaults-fr.min.js b/cps/static/js/libs/bootstrap-select/defaults-fr.min.js new file mode 100644 index 00000000..d8931590 --- /dev/null +++ b/cps/static/js/libs/bootstrap-select/defaults-fr.min.js @@ -0,0 +1,8 @@ +/*! + * Bootstrap-select v1.13.14 (https://developer.snapappointments.com/bootstrap-select) + * + * Copyright 2012-2020 SnapAppointments, LLC + * Licensed under MIT (https://github.com/snapappointments/bootstrap-select/blob/master/LICENSE) + */ + +!function(e,t){void 0===e&&void 0!==window&&(e=window),"function"==typeof define&&define.amd?define(["jquery"],function(e){return t(e)}):"object"==typeof module&&module.exports?module.exports=t(require("jquery")):t(e.jQuery)}(this,function(e){e.fn.selectpicker.defaults={noneSelectedText:"Aucune s\xe9lection",noneResultsText:"Aucun r\xe9sultat pour {0}",countSelectedText:function(e,t){return 1 (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=t||self).jQuery)}(this,(function(t){"use strict";t=t&&t.hasOwnProperty("default")?t.default:t;var e="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{};function n(t,e){return t(e={exports:{}},e.exports),e.exports}var r=function(t){return t&&t.Math==Math&&t},o=r("object"==typeof globalThis&&globalThis)||r("object"==typeof window&&window)||r("object"==typeof self&&self)||r("object"==typeof e&&e)||Function("return this")(),i=function(t){try{return!!t()}catch(t){return!0}},a=!i((function(){return 7!=Object.defineProperty({},"a",{get:function(){return 7}}).a})),c={}.propertyIsEnumerable,u=Object.getOwnPropertyDescriptor,f={f:u&&!c.call({1:2},1)?function(t){var e=u(this,t);return!!e&&e.enumerable}:c},l=function(t,e){return{enumerable:!(1&t),configurable:!(2&t),writable:!(4&t),value:e}},s={}.toString,d=function(t){return s.call(t).slice(8,-1)},p="".split,h=i((function(){return!Object("z").propertyIsEnumerable(0)}))?function(t){return"String"==d(t)?p.call(t,""):Object(t)}:Object,v=function(t){if(null==t)throw TypeError("Can't call method on "+t);return t},y=function(t){return h(v(t))},g=function(t){return"object"==typeof t?null!==t:"function"==typeof t},b=function(t,e){if(!g(t))return t;var n,r;if(e&&"function"==typeof(n=t.toString)&&!g(r=n.call(t)))return r;if("function"==typeof(n=t.valueOf)&&!g(r=n.call(t)))return r;if(!e&&"function"==typeof(n=t.toString)&&!g(r=n.call(t)))return r;throw TypeError("Can't convert object to primitive value")},m={}.hasOwnProperty,x=function(t,e){return m.call(t,e)},O=o.document,w=g(O)&&g(O.createElement),E=function(t){return w?O.createElement(t):{}},j=!a&&!i((function(){return 7!=Object.defineProperty(E("div"),"a",{get:function(){return 7}}).a})),S=Object.getOwnPropertyDescriptor,T={f:a?S:function(t,e){if(t=y(t),e=b(e,!0),j)try{return S(t,e)}catch(t){}if(x(t,e))return l(!f.f.call(t,e),t[e])}},P=function(t){if(!g(t))throw TypeError(String(t)+" is not an object");return t},A=Object.defineProperty,_={f:a?A:function(t,e,n){if(P(t),e=b(e,!0),P(n),j)try{return A(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}},I=a?function(t,e,n){return _.f(t,e,l(1,n))}:function(t,e,n){return t[e]=n,t},R=function(t,e){try{I(o,t,e)}catch(n){o[t]=e}return e},C=o["__core-js_shared__"]||R("__core-js_shared__",{}),k=Function.toString;"function"!=typeof C.inspectSource&&(C.inspectSource=function(t){return k.call(t)});var M,$,F,D=C.inspectSource,N=o.WeakMap,q="function"==typeof N&&/native code/.test(D(N)),B=n((function(t){(t.exports=function(t,e){return C[t]||(C[t]=void 0!==e?e:{})})("versions",[]).push({version:"3.6.0",mode:"global",copyright:"© 2019 Denis Pushkarev (zloirock.ru)"})})),L=0,K=Math.random(),V=function(t){return"Symbol("+String(void 0===t?"":t)+")_"+(++L+K).toString(36)},U=B("keys"),W=function(t){return U[t]||(U[t]=V(t))},z={},Y=o.WeakMap;if(q){var G=new Y,H=G.get,Q=G.has,X=G.set;M=function(t,e){return X.call(G,t,e),e},$=function(t){return H.call(G,t)||{}},F=function(t){return Q.call(G,t)}}else{var Z=W("state");z[Z]=!0,M=function(t,e){return I(t,Z,e),e},$=function(t){return x(t,Z)?t[Z]:{}},F=function(t){return x(t,Z)}}var J,tt,et={set:M,get:$,has:F,enforce:function(t){return F(t)?$(t):M(t,{})},getterFor:function(t){return function(e){var n;if(!g(e)||(n=$(e)).type!==t)throw TypeError("Incompatible receiver, "+t+" required");return n}}},nt=n((function(t){var e=et.get,n=et.enforce,r=String(String).split("String");(t.exports=function(t,e,i,a){var c=!!a&&!!a.unsafe,u=!!a&&!!a.enumerable,f=!!a&&!!a.noTargetGet;"function"==typeof i&&("string"!=typeof e||x(i,"name")||I(i,"name",e),n(i).source=r.join("string"==typeof e?e:"")),t!==o?(c?!f&&t[e]&&(u=!0):delete t[e],u?t[e]=i:I(t,e,i)):u?t[e]=i:R(e,i)})(Function.prototype,"toString",(function(){return"function"==typeof this&&e(this).source||D(this)}))})),rt=o,ot=function(t){return"function"==typeof t?t:void 0},it=function(t,e){return arguments.length<2?ot(rt[t])||ot(o[t]):rt[t]&&rt[t][e]||o[t]&&o[t][e]},at=Math.ceil,ct=Math.floor,ut=function(t){return isNaN(t=+t)?0:(t>0?ct:at)(t)},ft=Math.min,lt=function(t){return t>0?ft(ut(t),9007199254740991):0},st=Math.max,dt=Math.min,pt=function(t){return function(e,n,r){var o,i=y(e),a=lt(i.length),c=function(t,e){var n=ut(t);return n<0?st(n+e,0):dt(n,e)}(r,a);if(t&&n!=n){for(;a>c;)if((o=i[c++])!=o)return!0}else for(;a>c;c++)if((t||c in i)&&i[c]===n)return t||c||0;return!t&&-1}},ht={includes:pt(!0),indexOf:pt(!1)},vt=ht.indexOf,yt=function(t,e){var n,r=y(t),o=0,i=[];for(n in r)!x(z,n)&&x(r,n)&&i.push(n);for(;e.length>o;)x(r,n=e[o++])&&(~vt(i,n)||i.push(n));return i},gt=["constructor","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","valueOf"],bt=gt.concat("length","prototype"),mt={f:Object.getOwnPropertyNames||function(t){return yt(t,bt)}},xt={f:Object.getOwnPropertySymbols},Ot=it("Reflect","ownKeys")||function(t){var e=mt.f(P(t)),n=xt.f;return n?e.concat(n(t)):e},wt=function(t,e){for(var n=Ot(e),r=_.f,o=T.f,i=0;i=74)&&(J=Vt.match(/Chrome\/(\d+)/))&&(tt=J[1]);var Yt,Gt=tt&&+tt,Ht=Bt("species"),Qt=Bt("isConcatSpreadable"),Xt=Gt>=51||!i((function(){var t=[];return t[Qt]=!1,t.concat()[0]!==t})),Zt=(Yt="concat",Gt>=51||!i((function(){var t=[];return(t.constructor={})[Ht]=function(){return{foo:1}},1!==t[Yt](Boolean).foo}))),Jt=function(t){if(!g(t))return!1;var e=t[Qt];return void 0!==e?!!e:Ct(t)};Rt({target:"Array",proto:!0,forced:!Xt||!Zt},{concat:function(t){var e,n,r,o,i,a=kt(this),c=Kt(a,0),u=0;for(e=-1,r=arguments.length;e9007199254740991)throw TypeError("Maximum allowed index exceeded");for(n=0;n=9007199254740991)throw TypeError("Maximum allowed index exceeded");Mt(c,u++,i)}return c.length=u,c}});var te,ee=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)}},ne=[].push,re=function(t){var e=1==t,n=2==t,r=3==t,o=4==t,i=6==t,a=5==t||i;return function(c,u,f,l){for(var s,d,p=kt(c),v=h(p),y=ee(u,f,3),g=lt(v.length),b=0,m=l||Kt,x=e?m(c,g):n?m(c,0):void 0;g>b;b++)if((a||b in v)&&(d=y(s=v[b],b,p),t))if(e)x[b]=d;else if(d)switch(t){case 3:return!0;case 5:return s;case 6:return b;case 2:ne.call(x,s)}else if(o)return!1;return i?-1:r||o?o:x}},oe={forEach:re(0),map:re(1),filter:re(2),some:re(3),every:re(4),find:re(5),findIndex:re(6)},ie=Object.keys||function(t){return yt(t,gt)},ae=a?Object.defineProperties:function(t,e){P(t);for(var n,r=ie(e),o=r.length,i=0;o>i;)_.f(t,n=r[i++],e[n]);return t},ce=it("document","documentElement"),ue=W("IE_PROTO"),fe=function(){},le=function(t){return"