commit
						c166c92685
					
				
							
								
								
									
										35
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1,35 @@ | ||||||
|  | --- | ||||||
|  | name: Bug/Problem report | ||||||
|  | about: Create a report to help us improve Calibre-Web | ||||||
|  | title: '' | ||||||
|  | labels: '' | ||||||
|  | assignees: '' | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | **Describe the bug/problem** | ||||||
|  | A clear and concise description of what the bug is. If you are asking for support, please check our [Wiki](https://github.com/janeczku/calibre-web/wiki) if your question is already answered there. | ||||||
|  | 
 | ||||||
|  | **To Reproduce** | ||||||
|  | Steps to reproduce the behavior: | ||||||
|  | 1. Go to '...' | ||||||
|  | 2. Click on '....' | ||||||
|  | 3. Scroll down to '....' | ||||||
|  | 4. See error | ||||||
|  | 
 | ||||||
|  | **Expected behavior** | ||||||
|  | A clear and concise description of what you expected to happen. | ||||||
|  | 
 | ||||||
|  | **Screenshots** | ||||||
|  | If applicable, add screenshots to help explain your problem. | ||||||
|  | 
 | ||||||
|  | **Environment (please complete the following information):** | ||||||
|  |  - OS: [e.g. Windows 10/raspian] | ||||||
|  |  - Python version [e.g. python2.7] | ||||||
|  |  - Calibre-Web version [e.g. 0.6.5 or master@16.02.20, 19:55 ]: | ||||||
|  |  - Docker container [ None/Technosoft2000/Linuxuser]: | ||||||
|  |  - Special Hardware [e.g. Rasperry Pi Zero] | ||||||
|  |  - Browser [e.g. chrome, safari] | ||||||
|  | 
 | ||||||
|  | **Additional context** | ||||||
|  | Add any other context about the problem here. [e.g. access via reverse proxy] | ||||||
							
								
								
									
										20
									
								
								.github/ISSUE_TEMPLATE/feature_request.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								.github/ISSUE_TEMPLATE/feature_request.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1,20 @@ | ||||||
|  | --- | ||||||
|  | name: Feature request | ||||||
|  | about: Suggest an idea for Calibre-Web | ||||||
|  | title: '' | ||||||
|  | labels: '' | ||||||
|  | assignees: '' | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | **Is your feature request related to a problem? Please describe.** | ||||||
|  | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] | ||||||
|  | 
 | ||||||
|  | **Describe the solution you'd like** | ||||||
|  | A clear and concise description of what you want to happen. | ||||||
|  | 
 | ||||||
|  | **Describe alternatives you've considered** | ||||||
|  | A clear and concise description of any alternative solutions or features you've considered. | ||||||
|  | 
 | ||||||
|  | **Additional context** | ||||||
|  | Add any other context or screenshots about the feature request here. | ||||||
							
								
								
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							|  | @ -21,14 +21,12 @@ vendor/ | ||||||
| # calibre-web | # calibre-web | ||||||
| *.db | *.db | ||||||
| *.log | *.log | ||||||
| config.ini |  | ||||||
| cps/static/[0-9]* |  | ||||||
| 
 | 
 | ||||||
| .idea/ | .idea/ | ||||||
| *.bak | *.bak | ||||||
| *.log.* | *.log.* | ||||||
| tags |  | ||||||
| 
 | 
 | ||||||
| settings.yaml | settings.yaml | ||||||
| gdrive_credentials | gdrive_credentials | ||||||
| client_secrets.json | client_secrets.json | ||||||
|  | 
 | ||||||
|  |  | ||||||
|  | @ -30,7 +30,7 @@ Calibre-Web is a web app providing a clean interface for browsing, reading and d | ||||||
| 
 | 
 | ||||||
| ## Quick start | ## Quick start | ||||||
| 
 | 
 | ||||||
| 1. Install dependencies by running `pip install --target vendor -r requirements.txt`. | 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) | 2. Execute the command: `python cps.py` (or `nohup python 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 | 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\ | 4. Set `Location of Calibre database` to the path of the folder where your Calibre library (metadata.db) lives, push "submit" button\ | ||||||
|  | @ -46,7 +46,7 @@ Please note that running the above install command can fail on some versions of | ||||||
| 
 | 
 | ||||||
| ## Requirements | ## Requirements | ||||||
| 
 | 
 | ||||||
| Python 2.7+, python 3.x+ | python 3.x+, (Python 2.7+) | ||||||
| 
 | 
 | ||||||
| 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: | 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: | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										11
									
								
								cps.py
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								cps.py
									
									
									
									
									
								
							|  | @ -41,6 +41,14 @@ from cps.shelf import shelf | ||||||
| from cps.admin import admi | from cps.admin import admi | ||||||
| from cps.gdrive import gdrive | from cps.gdrive import gdrive | ||||||
| from cps.editbooks import editbook | from cps.editbooks import editbook | ||||||
|  | 
 | ||||||
|  | try: | ||||||
|  |     from cps.kobo import kobo, get_kobo_activated | ||||||
|  |     from cps.kobo_auth import kobo_auth | ||||||
|  |     kobo_available = get_kobo_activated() | ||||||
|  | except ImportError: | ||||||
|  |     kobo_available = False | ||||||
|  | 
 | ||||||
| try: | try: | ||||||
|     from cps.oauth_bb import oauth |     from cps.oauth_bb import oauth | ||||||
|     oauth_available = True |     oauth_available = True | ||||||
|  | @ -58,6 +66,9 @@ def main(): | ||||||
|     app.register_blueprint(admi) |     app.register_blueprint(admi) | ||||||
|     app.register_blueprint(gdrive) |     app.register_blueprint(gdrive) | ||||||
|     app.register_blueprint(editbook) |     app.register_blueprint(editbook) | ||||||
|  |     if kobo_available: | ||||||
|  |         app.register_blueprint(kobo) | ||||||
|  |         app.register_blueprint(kobo_auth) | ||||||
|     if oauth_available: |     if oauth_available: | ||||||
|         app.register_blueprint(oauth) |         app.register_blueprint(oauth) | ||||||
|     success = web_server.start() |     success = web_server.start() | ||||||
|  |  | ||||||
|  | @ -116,14 +116,13 @@ def get_locale(): | ||||||
|         if user.nickname != 'Guest':   # if the account is the guest account bypass the config lang settings |         if user.nickname != 'Guest':   # if the account is the guest account bypass the config lang settings | ||||||
|             return user.locale |             return user.locale | ||||||
| 
 | 
 | ||||||
|     preferred = set() |     preferred = list() | ||||||
|     if request.accept_languages: |     if request.accept_languages: | ||||||
|         for x in request.accept_languages.values(): |         for x in request.accept_languages.values(): | ||||||
|             try: |             try: | ||||||
|                 preferred.add(str(LC.parse(x.replace('-', '_')))) |                 preferred.append(str(LC.parse(x.replace('-', '_')))) | ||||||
|             except (UnknownLocaleError, ValueError) as e: |             except (UnknownLocaleError, ValueError) as e: | ||||||
|                 log.warning('Could not parse locale "%s": %s', x, e) |                 log.debug('Could not parse locale "%s": %s', x, e) | ||||||
|                 # preferred.append('en') |  | ||||||
| 
 | 
 | ||||||
|     return negotiate_locale(preferred or ['en'], _BABEL_TRANSLATIONS) |     return negotiate_locale(preferred or ['en'], _BABEL_TRANSLATIONS) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -30,7 +30,7 @@ import babel, pytz, requests, sqlalchemy | ||||||
| import werkzeug, flask, flask_login, flask_principal, jinja2 | import werkzeug, flask, flask_login, flask_principal, jinja2 | ||||||
| from flask_babel import gettext as _ | from flask_babel import gettext as _ | ||||||
| 
 | 
 | ||||||
| from . import db, converter, uploader, server, isoLanguages | from . import db, converter, uploader, server, isoLanguages, constants | ||||||
| from .web import render_title_template | from .web import render_title_template | ||||||
| try: | try: | ||||||
|     from flask_login import __version__ as flask_loginVersion |     from flask_login import __version__ as flask_loginVersion | ||||||
|  | @ -49,8 +49,11 @@ about = flask.Blueprint('about', __name__) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| _VERSIONS = OrderedDict( | _VERSIONS = OrderedDict( | ||||||
|     Platform = ' '.join(platform.uname()), |     Platform = '{0[0]} {0[2]} {0[3]} {0[4]} {0[5]}'.format(platform.uname()), | ||||||
|     Python=sys.version, |     Python=sys.version, | ||||||
|  |     Calibre_Web=constants.STABLE_VERSION['version'] + ' - ' | ||||||
|  |                 + constants.NIGHTLY_VERSION[0].replace('%','%%') + ' - ' | ||||||
|  |                 + constants.NIGHTLY_VERSION[1].replace('%','%%'), | ||||||
|     WebServer=server.VERSION, |     WebServer=server.VERSION, | ||||||
|     Flask=flask.__version__, |     Flask=flask.__version__, | ||||||
|     Flask_Login=flask_loginVersion, |     Flask_Login=flask_loginVersion, | ||||||
|  | @ -67,7 +70,7 @@ _VERSIONS = OrderedDict( | ||||||
|     Unidecode = unidecode_version, |     Unidecode = unidecode_version, | ||||||
|     Flask_SimpleLDAP =  u'installed' if bool(services.ldap) else u'not installed', |     Flask_SimpleLDAP =  u'installed' if bool(services.ldap) else u'not installed', | ||||||
|     Goodreads = u'installed' if bool(services.goodreads_support) else u'not installed', |     Goodreads = u'installed' if bool(services.goodreads_support) else u'not installed', | ||||||
| 
 |     jsonschema = services.SyncToken.__version__  if bool(services.SyncToken) else u'not installed', | ||||||
| ) | ) | ||||||
| _VERSIONS.update(uploader.get_versions()) | _VERSIONS.update(uploader.get_versions()) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										291
									
								
								cps/admin.py
									
									
									
									
									
								
							
							
						
						
									
										291
									
								
								cps/admin.py
									
									
									
									
									
								
							|  | @ -1,4 +1,3 @@ | ||||||
| #!/usr/bin/env python |  | ||||||
| # -*- coding: utf-8 -*- | # -*- coding: utf-8 -*- | ||||||
| 
 | 
 | ||||||
| #  This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) | #  This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) | ||||||
|  | @ -45,7 +44,8 @@ from .web import admin_required, render_title_template, before_request, unconfig | ||||||
| 
 | 
 | ||||||
| feature_support = { | feature_support = { | ||||||
|         'ldap': False, # bool(services.ldap), |         'ldap': False, # bool(services.ldap), | ||||||
|         'goodreads': bool(services.goodreads_support) |         'goodreads': bool(services.goodreads_support), | ||||||
|  |         'kobo':  bool(services.kobo) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| # try: | # try: | ||||||
|  | @ -144,7 +144,10 @@ def configuration(): | ||||||
| def view_configuration(): | def view_configuration(): | ||||||
|     readColumn = db.session.query(db.Custom_Columns)\ |     readColumn = db.session.query(db.Custom_Columns)\ | ||||||
|             .filter(and_(db.Custom_Columns.datatype == 'bool',db.Custom_Columns.mark_for_delete == 0)).all() |             .filter(and_(db.Custom_Columns.datatype == 'bool',db.Custom_Columns.mark_for_delete == 0)).all() | ||||||
|  |     restrictColumns= 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, |     return render_title_template("config_view_edit.html", conf=config, readColumns=readColumn, | ||||||
|  |                                  restrictColumns=restrictColumns, | ||||||
|                                  title=_(u"UI Configuration"), page="uiconfig") |                                  title=_(u"UI Configuration"), page="uiconfig") | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -160,7 +163,7 @@ def update_view_configuration(): | ||||||
| 
 | 
 | ||||||
|     _config_string("config_calibre_web_title") |     _config_string("config_calibre_web_title") | ||||||
|     _config_string("config_columns_to_ignore") |     _config_string("config_columns_to_ignore") | ||||||
|     _config_string("config_mature_content_tags") |     # _config_string("config_mature_content_tags") | ||||||
|     reboot_required |= _config_string("config_title_regex") |     reboot_required |= _config_string("config_title_regex") | ||||||
| 
 | 
 | ||||||
|     _config_int("config_read_column") |     _config_int("config_read_column") | ||||||
|  | @ -168,6 +171,7 @@ def update_view_configuration(): | ||||||
|     _config_int("config_random_books") |     _config_int("config_random_books") | ||||||
|     _config_int("config_books_per_page") |     _config_int("config_books_per_page") | ||||||
|     _config_int("config_authors_max") |     _config_int("config_authors_max") | ||||||
|  |     _config_int("config_restricted_column") | ||||||
| 
 | 
 | ||||||
|     if config.config_google_drive_watch_changes_response: |     if config.config_google_drive_watch_changes_response: | ||||||
|         config.config_google_drive_watch_changes_response = json.dumps(config.config_google_drive_watch_changes_response) |         config.config_google_drive_watch_changes_response = json.dumps(config.config_google_drive_watch_changes_response) | ||||||
|  | @ -176,8 +180,6 @@ def update_view_configuration(): | ||||||
|     config.config_default_role &= ~constants.ROLE_ANONYMOUS |     config.config_default_role &= ~constants.ROLE_ANONYMOUS | ||||||
| 
 | 
 | ||||||
|     config.config_default_show = sum(int(k[5:]) for k in to_save if k.startswith('show_')) |     config.config_default_show = sum(int(k[5:]) for k in to_save if k.startswith('show_')) | ||||||
|     if "Show_mature_content" in to_save: |  | ||||||
|         config.config_default_show |= constants.MATURE_CONTENT |  | ||||||
|     if "Show_detail_random" in to_save: |     if "Show_detail_random" in to_save: | ||||||
|         config.config_default_show |= constants.DETAIL_RANDOM |         config.config_default_show |= constants.DETAIL_RANDOM | ||||||
| 
 | 
 | ||||||
|  | @ -202,7 +204,6 @@ def edit_domain(allow): | ||||||
|     # value: 'superuser!' //new value |     # value: 'superuser!' //new value | ||||||
|     vals = request.form.to_dict() |     vals = request.form.to_dict() | ||||||
|     answer = ub.session.query(ub.Registration).filter(ub.Registration.id == vals['pk']).first() |     answer = ub.session.query(ub.Registration).filter(ub.Registration.id == vals['pk']).first() | ||||||
|     # domain_name = request.args.get('domain') |  | ||||||
|     answer.domain = vals['value'].replace('*', '%').replace('?', '_').lower() |     answer.domain = vals['value'].replace('*', '%').replace('?', '_').lower() | ||||||
|     ub.session.commit() |     ub.session.commit() | ||||||
|     return "" |     return "" | ||||||
|  | @ -247,6 +248,228 @@ def list_domain(allow): | ||||||
|     response.headers["Content-Type"] = "application/json; charset=utf-8" |     response.headers["Content-Type"] = "application/json; charset=utf-8" | ||||||
|     return response |     return response | ||||||
| 
 | 
 | ||||||
|  | @admi.route("/ajax/editrestriction/<int:type>", methods=['POST']) | ||||||
|  | @login_required | ||||||
|  | @admin_required | ||||||
|  | def edit_restriction(type): | ||||||
|  |     element = request.form.to_dict() | ||||||
|  |     if element['id'].startswith('a'): | ||||||
|  |         if type == 0:  # Tags as template | ||||||
|  |             elementlist = config.list_allowed_tags() | ||||||
|  |             elementlist[int(element['id'][1:])]=element['Element'] | ||||||
|  |             config.config_allowed_tags = ','.join(elementlist) | ||||||
|  |             config.save() | ||||||
|  |         if type == 1:  # CustomC | ||||||
|  |             elementlist = config.list_allowed_column_values() | ||||||
|  |             elementlist[int(element['id'][1:])]=element['Element'] | ||||||
|  |             config.config_allowed_column_value = ','.join(elementlist) | ||||||
|  |             config.save() | ||||||
|  |         if 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() | ||||||
|  |             else: | ||||||
|  |                 usr = current_user | ||||||
|  |             elementlist = usr.list_allowed_tags() | ||||||
|  |             elementlist[int(element['id'][1:])]=element['Element'] | ||||||
|  |             usr.allowed_tags = ','.join(elementlist) | ||||||
|  |             ub.session.commit() | ||||||
|  |         if 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() | ||||||
|  |             else: | ||||||
|  |                 usr = current_user | ||||||
|  |             elementlist = usr.list_allowed_column_values() | ||||||
|  |             elementlist[int(element['id'][1:])]=element['Element'] | ||||||
|  |             usr.allowed_column_value = ','.join(elementlist) | ||||||
|  |             ub.session.commit() | ||||||
|  |     if element['id'].startswith('d'): | ||||||
|  |         if type == 0:  # Tags as template | ||||||
|  |             elementlist = config.list_denied_tags() | ||||||
|  |             elementlist[int(element['id'][1:])]=element['Element'] | ||||||
|  |             config.config_denied_tags = ','.join(elementlist) | ||||||
|  |             config.save() | ||||||
|  |         if type == 1:  # CustomC | ||||||
|  |             elementlist = config.list_denied_column_values() | ||||||
|  |             elementlist[int(element['id'][1:])]=element['Element'] | ||||||
|  |             config.config_denied_column_value = ','.join(elementlist) | ||||||
|  |             config.save() | ||||||
|  |             pass | ||||||
|  |         if 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() | ||||||
|  |             else: | ||||||
|  |                 usr = current_user | ||||||
|  |             elementlist = usr.list_denied_tags() | ||||||
|  |             elementlist[int(element['id'][1:])]=element['Element'] | ||||||
|  |             usr.denied_tags = ','.join(elementlist) | ||||||
|  |             ub.session.commit() | ||||||
|  |         if 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() | ||||||
|  |             else: | ||||||
|  |                 usr = current_user | ||||||
|  |             elementlist = usr.list_denied_column_values() | ||||||
|  |             elementlist[int(element['id'][1:])]=element['Element'] | ||||||
|  |             usr.denied_column_value = ','.join(elementlist) | ||||||
|  |             ub.session.commit() | ||||||
|  |     return "" | ||||||
|  | 
 | ||||||
|  | def restriction_addition(element, list_func): | ||||||
|  |     elementlist = list_func() | ||||||
|  |     if elementlist == ['']: | ||||||
|  |         elementlist = [] | ||||||
|  |     if not element['add_element'] in elementlist: | ||||||
|  |         elementlist += [element['add_element']] | ||||||
|  |     return ','.join(elementlist) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def restriction_deletion(element, list_func): | ||||||
|  |     elementlist = list_func() | ||||||
|  |     if element['Element'] in elementlist: | ||||||
|  |         elementlist.remove(element['Element']) | ||||||
|  |     return ','.join(elementlist) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @admi.route("/ajax/addrestriction/<int:type>", methods=['POST']) | ||||||
|  | @login_required | ||||||
|  | @admin_required | ||||||
|  | def add_restriction(type): | ||||||
|  |     element = request.form.to_dict() | ||||||
|  |     if type == 0:  # Tags as template | ||||||
|  |         if 'submit_allow' in element: | ||||||
|  |             config.config_allowed_tags = restriction_addition(element, config.list_allowed_tags) | ||||||
|  |             config.save() | ||||||
|  |         elif 'submit_deny' in element: | ||||||
|  |             config.config_denied_tags = restriction_addition(element, config.list_denied_tags) | ||||||
|  |             config.save() | ||||||
|  |     if type == 1:  # CCustom as template | ||||||
|  |         if 'submit_allow' in element: | ||||||
|  |             config.config_allowed_column_value = restriction_addition(element, config.list_denied_column_values) | ||||||
|  |             config.save() | ||||||
|  |         elif 'submit_deny' in element: | ||||||
|  |             config.config_denied_column_value = restriction_addition(element, config.list_allowed_column_values) | ||||||
|  |             config.save() | ||||||
|  |     if 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() | ||||||
|  |         else: | ||||||
|  |             usr = current_user | ||||||
|  |         if 'submit_allow' in element: | ||||||
|  |             usr.allowed_tags = restriction_addition(element, usr.list_allowed_tags) | ||||||
|  |             ub.session.commit() | ||||||
|  |         elif 'submit_deny' in element: | ||||||
|  |             usr.denied_tags = restriction_addition(element, usr.list_denied_tags) | ||||||
|  |             ub.session.commit() | ||||||
|  |     if 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() | ||||||
|  |         else: | ||||||
|  |             usr = current_user | ||||||
|  |         if 'submit_allow' in element: | ||||||
|  |             usr.allowed_column_value = restriction_addition(element, usr.list_allowed_column_values) | ||||||
|  |             ub.session.commit() | ||||||
|  |         elif 'submit_deny' in element: | ||||||
|  |             usr.denied_column_value = restriction_addition(element, usr.list_denied_column_values) | ||||||
|  |             ub.session.commit() | ||||||
|  |     return "" | ||||||
|  | 
 | ||||||
|  | @admi.route("/ajax/deleterestriction/<int:type>", methods=['POST']) | ||||||
|  | @login_required | ||||||
|  | @admin_required | ||||||
|  | def delete_restriction(type): | ||||||
|  |     element = request.form.to_dict() | ||||||
|  |     if type == 0:  # Tags as template | ||||||
|  |         if element['id'].startswith('a'): | ||||||
|  |             config.config_allowed_tags = restriction_deletion(element, config.list_allowed_tags) | ||||||
|  |             config.save() | ||||||
|  |         elif element['id'].startswith('d'): | ||||||
|  |             config.config_denied_tags = restriction_deletion(element, config.list_denied_tags) | ||||||
|  |             config.save() | ||||||
|  |     elif type == 1:  # CustomC as template | ||||||
|  |         if element['id'].startswith('a'): | ||||||
|  |             config.config_allowed_column_value = restriction_deletion(element, config.list_allowed_column_values) | ||||||
|  |             config.save() | ||||||
|  |         elif element['id'].startswith('d'): | ||||||
|  |             config.config_denied_column_value = restriction_deletion(element, config.list_denied_column_values) | ||||||
|  |             config.save() | ||||||
|  |     elif 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() | ||||||
|  |         else: | ||||||
|  |             usr = current_user | ||||||
|  |         if element['id'].startswith('a'): | ||||||
|  |             usr.allowed_tags = restriction_deletion(element, usr.list_allowed_tags) | ||||||
|  |             ub.session.commit() | ||||||
|  |         elif element['id'].startswith('d'): | ||||||
|  |             usr.denied_tags = restriction_deletion(element, usr.list_denied_tags) | ||||||
|  |             ub.session.commit() | ||||||
|  |     elif 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() | ||||||
|  |         else: | ||||||
|  |             usr = current_user | ||||||
|  |         if element['id'].startswith('a'): | ||||||
|  |             usr.allowed_column_value = restriction_deletion(element, usr.list_allowed_column_values) | ||||||
|  |             ub.session.commit() | ||||||
|  |         elif element['id'].startswith('d'): | ||||||
|  |             usr.denied_column_value = restriction_deletion(element, usr.list_denied_column_values) | ||||||
|  |             ub.session.commit() | ||||||
|  |     return "" | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | #@admi.route("/ajax/listrestriction/<int:type>/<int:user_id>", defaults={'user_id': '0'}) | ||||||
|  | @admi.route("/ajax/listrestriction/<int:type>") | ||||||
|  | @login_required | ||||||
|  | @admin_required | ||||||
|  | def list_restriction(type): | ||||||
|  |     if 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 != ''] | ||||||
|  |         json_dumps = restrict + allow | ||||||
|  |     elif 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 != ''] | ||||||
|  |         json_dumps = restrict + allow | ||||||
|  |     elif 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() | ||||||
|  |         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 != ''] | ||||||
|  |         json_dumps = restrict + allow | ||||||
|  |     elif 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() | ||||||
|  |         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 != ''] | ||||||
|  |         json_dumps = restrict + allow | ||||||
|  |     else: | ||||||
|  |         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("/config", methods=["GET", "POST"]) | ||||||
| @unconfigured | @unconfigured | ||||||
|  | @ -262,7 +485,6 @@ def _configuration_update_helper(): | ||||||
|     db_change = False |     db_change = False | ||||||
|     to_save = request.form.to_dict() |     to_save = request.form.to_dict() | ||||||
| 
 | 
 | ||||||
|     # _config_dict = lambda x: config.set_from_dictionary(to_save, x, lambda y: y['id']) |  | ||||||
|     _config_string = lambda x: config.set_from_dictionary(to_save, x, lambda y: y.strip() if y else y) |     _config_string = lambda x: config.set_from_dictionary(to_save, x, lambda y: y.strip() if y else y) | ||||||
|     _config_int = lambda x: config.set_from_dictionary(to_save, x, int) |     _config_int = lambda x: config.set_from_dictionary(to_save, x, int) | ||||||
|     _config_checkbox = lambda x: config.set_from_dictionary(to_save, x, lambda y: y == "on", False) |     _config_checkbox = lambda x: config.set_from_dictionary(to_save, x, lambda y: y == "on", False) | ||||||
|  | @ -305,6 +527,9 @@ def _configuration_update_helper(): | ||||||
|     _config_checkbox_int("config_uploading") |     _config_checkbox_int("config_uploading") | ||||||
|     _config_checkbox_int("config_anonbrowse") |     _config_checkbox_int("config_anonbrowse") | ||||||
|     _config_checkbox_int("config_public_reg") |     _config_checkbox_int("config_public_reg") | ||||||
|  |     reboot_required |= _config_checkbox_int("config_kobo_sync") | ||||||
|  |     _config_checkbox_int("config_kobo_proxy") | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
|     _config_int("config_ebookconverter") |     _config_int("config_ebookconverter") | ||||||
|     _config_string("config_calibre") |     _config_string("config_calibre") | ||||||
|  | @ -339,7 +564,7 @@ def _configuration_update_helper(): | ||||||
|     # Remote login configuration |     # Remote login configuration | ||||||
|     _config_checkbox("config_remote_login") |     _config_checkbox("config_remote_login") | ||||||
|     if not config.config_remote_login: |     if not config.config_remote_login: | ||||||
|         ub.session.query(ub.RemoteAuthToken).delete() |         ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.token_type==0).delete() | ||||||
| 
 | 
 | ||||||
|     # Goodreads configuration |     # Goodreads configuration | ||||||
|     _config_checkbox("config_use_goodreads") |     _config_checkbox("config_use_goodreads") | ||||||
|  | @ -449,10 +674,11 @@ def new_user(): | ||||||
|     content = ub.User() |     content = ub.User() | ||||||
|     languages = speaking_language() |     languages = speaking_language() | ||||||
|     translations = [LC('en')] + babel.list_translations() |     translations = [LC('en')] + babel.list_translations() | ||||||
|  |     kobo_support = feature_support['kobo'] and config.config_kobo_sync | ||||||
|     if request.method == "POST": |     if request.method == "POST": | ||||||
|         to_save = request.form.to_dict() |         to_save = request.form.to_dict() | ||||||
|         content.default_language = to_save["default_language"] |         content.default_language = to_save["default_language"] | ||||||
|         content.mature_content = "Show_mature_content" in to_save |         # content.mature_content = "Show_mature_content" in to_save | ||||||
|         content.locale = to_save.get("locale", content.locale) |         content.locale = to_save.get("locale", content.locale) | ||||||
| 
 | 
 | ||||||
|         content.sidebar_view = sum(int(key[5:]) for key in to_save if key.startswith('show_')) |         content.sidebar_view = sum(int(key[5:]) for key in to_save if key.startswith('show_')) | ||||||
|  | @ -464,7 +690,8 @@ def new_user(): | ||||||
|         if not to_save["nickname"] or not to_save["email"] or not to_save["password"]: |         if not to_save["nickname"] or not to_save["email"] or not to_save["password"]: | ||||||
|             flash(_(u"Please fill out all fields!"), category="error") |             flash(_(u"Please fill out all fields!"), category="error") | ||||||
|             return render_title_template("user_edit.html", new_user=1, content=content, translations=translations, |             return render_title_template("user_edit.html", new_user=1, content=content, translations=translations, | ||||||
|                                          registered_oauth=oauth_check, title=_(u"Add new user")) |                                          registered_oauth=oauth_check, kobo_support=kobo_support, | ||||||
|  |                                          title=_(u"Add new user")) | ||||||
|         content.password = generate_password_hash(to_save["password"]) |         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())\ |         existing_user = ub.session.query(ub.User).filter(func.lower(ub.User.nickname) == to_save["nickname"].lower())\ | ||||||
|             .first() |             .first() | ||||||
|  | @ -475,15 +702,20 @@ def new_user(): | ||||||
|             if config.config_public_reg and not check_valid_domain(to_save["email"]): |             if config.config_public_reg and not check_valid_domain(to_save["email"]): | ||||||
|                 flash(_(u"E-mail is not from valid domain"), category="error") |                 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, |                 return render_title_template("user_edit.html", new_user=1, content=content, translations=translations, | ||||||
|                                              registered_oauth=oauth_check, title=_(u"Add new user")) |                                              registered_oauth=oauth_check, kobo_support=kobo_support, | ||||||
|  |                                              title=_(u"Add new user")) | ||||||
|             else: |             else: | ||||||
|                 content.email = to_save["email"] |                 content.email = to_save["email"] | ||||||
|         else: |         else: | ||||||
|             flash(_(u"Found an existing account for this e-mail address or nickname."), category="error") |             flash(_(u"Found an existing account for this e-mail address or nickname."), category="error") | ||||||
|             return render_title_template("user_edit.html", new_user=1, content=content, translations=translations, |             return render_title_template("user_edit.html", new_user=1, content=content, translations=translations, | ||||||
|                                      languages=languages, title=_(u"Add new user"), page="newuser", |                                      languages=languages, title=_(u"Add new user"), page="newuser", | ||||||
|                                      registered_oauth=oauth_check) |                                      kobo_support=kobo_support, registered_oauth=oauth_check) | ||||||
|         try: |         try: | ||||||
|  |             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) |             ub.session.add(content) | ||||||
|             ub.session.commit() |             ub.session.commit() | ||||||
|             flash(_(u"User '%(user)s' created", user=content.nickname), category="success") |             flash(_(u"User '%(user)s' created", user=content.nickname), category="success") | ||||||
|  | @ -494,10 +726,9 @@ def new_user(): | ||||||
|     else: |     else: | ||||||
|         content.role = config.config_default_role |         content.role = config.config_default_role | ||||||
|         content.sidebar_view = config.config_default_show |         content.sidebar_view = config.config_default_show | ||||||
|         content.mature_content = bool(config.config_default_show & constants.MATURE_CONTENT) |  | ||||||
|     return render_title_template("user_edit.html", new_user=1, content=content, translations=translations, |     return render_title_template("user_edit.html", new_user=1, content=content, translations=translations, | ||||||
|                                  languages=languages, title=_(u"Add new user"), page="newuser", |                                  languages=languages, title=_(u"Add new user"), page="newuser", | ||||||
|                                  registered_oauth=oauth_check) |                                  kobo_support=kobo_support, registered_oauth=oauth_check) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @admi.route("/admin/mailsettings") | @admi.route("/admin/mailsettings") | ||||||
|  | @ -552,6 +783,7 @@ def edit_user(user_id): | ||||||
|     downloads = list() |     downloads = list() | ||||||
|     languages = speaking_language() |     languages = speaking_language() | ||||||
|     translations = babel.list_translations() + [LC('en')] |     translations = babel.list_translations() + [LC('en')] | ||||||
|  |     kobo_support = feature_support['kobo'] and config.config_kobo_sync | ||||||
|     for book in content.downloads: |     for book in content.downloads: | ||||||
|         downloadbook = db.session.query(db.Books).filter(db.Books.id == book.book_id).first() |         downloadbook = db.session.query(db.Books).filter(db.Books.id == book.book_id).first() | ||||||
|         if downloadbook: |         if downloadbook: | ||||||
|  | @ -597,8 +829,6 @@ def edit_user(user_id): | ||||||
|             else: |             else: | ||||||
|                 content.sidebar_view &= ~constants.DETAIL_RANDOM |                 content.sidebar_view &= ~constants.DETAIL_RANDOM | ||||||
| 
 | 
 | ||||||
|             content.mature_content = "Show_mature_content" in to_save |  | ||||||
| 
 |  | ||||||
|             if "default_language" in to_save: |             if "default_language" in to_save: | ||||||
|                 content.default_language = to_save["default_language"] |                 content.default_language = to_save["default_language"] | ||||||
|             if "locale" in to_save and to_save["locale"]: |             if "locale" in to_save and to_save["locale"]: | ||||||
|  | @ -610,9 +840,15 @@ def edit_user(user_id): | ||||||
|                     content.email = to_save["email"] |                     content.email = to_save["email"] | ||||||
|                 else: |                 else: | ||||||
|                     flash(_(u"Found an existing account for this e-mail address."), category="error") |                     flash(_(u"Found an existing account for this e-mail address."), category="error") | ||||||
|                     return render_title_template("user_edit.html", translations=translations, languages=languages, |                     return render_title_template("user_edit.html", | ||||||
|  |                                                  translations=translations, | ||||||
|  |                                                  languages=languages, | ||||||
|                                                  mail_configured = config.get_mail_server_configured(), |                                                  mail_configured = config.get_mail_server_configured(), | ||||||
|                                                  new_user=0, content=content, downloads=downloads, registered_oauth=oauth_check, |                                                  kobo_support=kobo_support, | ||||||
|  |                                                  new_user=0, | ||||||
|  |                                                  content=content, | ||||||
|  |                                                  downloads=downloads, | ||||||
|  |                                                  registered_oauth=oauth_check, | ||||||
|                                                  title=_(u"Edit User %(nick)s", nick=content.nickname), page="edituser") |                                                  title=_(u"Edit User %(nick)s", nick=content.nickname), page="edituser") | ||||||
|             if "nickname" in to_save and to_save["nickname"] != content.nickname: |             if "nickname" in to_save and to_save["nickname"] != content.nickname: | ||||||
|                 # Query User nickname, if not existing, change |                 # Query User nickname, if not existing, change | ||||||
|  | @ -627,6 +863,7 @@ def edit_user(user_id): | ||||||
|                                                  new_user=0, content=content, |                                                  new_user=0, content=content, | ||||||
|                                                  downloads=downloads, |                                                  downloads=downloads, | ||||||
|                                                  registered_oauth=oauth_check, |                                                  registered_oauth=oauth_check, | ||||||
|  |                                                  kobo_support=kobo_support, | ||||||
|                                                  title=_(u"Edit User %(nick)s", nick=content.nickname), |                                                  title=_(u"Edit User %(nick)s", nick=content.nickname), | ||||||
|                                                  page="edituser") |                                                  page="edituser") | ||||||
| 
 | 
 | ||||||
|  | @ -638,9 +875,15 @@ def edit_user(user_id): | ||||||
|         except IntegrityError: |         except IntegrityError: | ||||||
|             ub.session.rollback() |             ub.session.rollback() | ||||||
|             flash(_(u"An unknown error occured."), category="error") |             flash(_(u"An unknown error occured."), category="error") | ||||||
|     return render_title_template("user_edit.html", translations=translations, languages=languages, new_user=0, |     return render_title_template("user_edit.html", | ||||||
|                                  content=content, downloads=downloads, registered_oauth=oauth_check, |                                  translations=translations, | ||||||
|  |                                  languages=languages, | ||||||
|  |                                  new_user=0, | ||||||
|  |                                  content=content, | ||||||
|  |                                  downloads=downloads, | ||||||
|  |                                  registered_oauth=oauth_check, | ||||||
|                                  mail_configured=config.get_mail_server_configured(), |                                  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.nickname), page="edituser") | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -671,8 +914,12 @@ def view_logfile(): | ||||||
|     logfiles = {} |     logfiles = {} | ||||||
|     logfiles[0] = logger.get_logfile(config.config_logfile) |     logfiles[0] = logger.get_logfile(config.config_logfile) | ||||||
|     logfiles[1] = logger.get_accesslogfile(config.config_access_logfile) |     logfiles[1] = logger.get_accesslogfile(config.config_access_logfile) | ||||||
|     return render_title_template("logviewer.html",title=_(u"Logfile viewer"), accesslog_enable=config.config_access_log, |     return render_title_template("logviewer.html", | ||||||
|                                  logfiles=logfiles, page="logfile") |                                  title=_(u"Logfile viewer"), | ||||||
|  |                                  accesslog_enable=config.config_access_log, | ||||||
|  |                                  log_enable=bool(config.config_logfile != logger.LOG_TO_STDOUT), | ||||||
|  |                                  logfiles=logfiles, | ||||||
|  |                                  page="logfile") | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @admi.route("/ajax/log/<int:logtype>") | @admi.route("/ajax/log/<int:logtype>") | ||||||
|  |  | ||||||
|  | @ -1,3 +1,5 @@ | ||||||
|  | # -*- coding: utf-8 -*- | ||||||
|  | 
 | ||||||
| #   This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) | #   This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) | ||||||
| #     Copyright (C) 2016-2019 jkrehm andy29485 OzzieIsaacs | #     Copyright (C) 2016-2019 jkrehm andy29485 OzzieIsaacs | ||||||
| # | # | ||||||
|  |  | ||||||
|  | @ -43,7 +43,7 @@ parser.add_argument('-k', metavar='path', | ||||||
|                     help='path and name to SSL keyfile, e.g. /opt/test.key, works only in combination with certfile') |                     help='path and name to SSL keyfile, e.g. /opt/test.key, works only in combination with certfile') | ||||||
| parser.add_argument('-v', '--version', action='version', help='Shows version number and exits Calibre-web', | parser.add_argument('-v', '--version', action='version', help='Shows version number and exits Calibre-web', | ||||||
|                     version=version_info()) |                     version=version_info()) | ||||||
| parser.add_argument('-i', metavar='ip-adress', help='Server IP-Adress to listen') | 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('-s', metavar='user:pass', help='Sets specific username to new password') | ||||||
| args = parser.parse_args() | args = parser.parse_args() | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,4 +1,3 @@ | ||||||
| #!/usr/bin/env python |  | ||||||
| # -*- coding: utf-8 -*- | # -*- coding: utf-8 -*- | ||||||
| 
 | 
 | ||||||
| #   This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) | #   This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) | ||||||
|  |  | ||||||
|  | @ -25,7 +25,7 @@ import sys | ||||||
| from sqlalchemy import exc, Column, String, Integer, SmallInteger, Boolean | from sqlalchemy import exc, Column, String, Integer, SmallInteger, Boolean | ||||||
| from sqlalchemy.ext.declarative import declarative_base | from sqlalchemy.ext.declarative import declarative_base | ||||||
| 
 | 
 | ||||||
| from . import constants, cli, logger | from . import constants, cli, logger, ub | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| log = logger.create() | log = logger.create() | ||||||
|  | @ -68,12 +68,18 @@ class _Settings(_Base): | ||||||
|     config_anonbrowse = Column(SmallInteger, default=0) |     config_anonbrowse = Column(SmallInteger, default=0) | ||||||
|     config_public_reg = Column(SmallInteger, default=0) |     config_public_reg = Column(SmallInteger, default=0) | ||||||
|     config_remote_login = Column(Boolean, default=False) |     config_remote_login = Column(Boolean, default=False) | ||||||
| 
 |     config_kobo_sync = Column(Boolean, default=False) | ||||||
| 
 | 
 | ||||||
|     config_default_role = Column(SmallInteger, default=0) |     config_default_role = Column(SmallInteger, default=0) | ||||||
|     config_default_show = Column(SmallInteger, default=6143) |     config_default_show = Column(SmallInteger, default=6143) | ||||||
|     config_columns_to_ignore = Column(String) |     config_columns_to_ignore = Column(String) | ||||||
| 
 | 
 | ||||||
|  |     config_denied_tags = Column(String, default="") | ||||||
|  |     config_allowed_tags = Column(String, default="") | ||||||
|  |     config_restricted_column = Column(SmallInteger, default=0) | ||||||
|  |     config_denied_column_value = Column(String, default="") | ||||||
|  |     config_allowed_column_value = Column(String, default="") | ||||||
|  | 
 | ||||||
|     config_use_google_drive = Column(Boolean, default=False) |     config_use_google_drive = Column(Boolean, default=False) | ||||||
|     config_google_drive_folder = Column(String) |     config_google_drive_folder = Column(String) | ||||||
|     config_google_drive_watch_changes_response = Column(String) |     config_google_drive_watch_changes_response = Column(String) | ||||||
|  | @ -84,7 +90,8 @@ class _Settings(_Base): | ||||||
| 
 | 
 | ||||||
|     config_login_type = Column(Integer, default=0) |     config_login_type = Column(Integer, default=0) | ||||||
| 
 | 
 | ||||||
|     # config_oauth_provider = Column(Integer) |     config_kobo_proxy = Column(Boolean, default=False) | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
|     config_ldap_provider_url = Column(String, default='localhost') |     config_ldap_provider_url = Column(String, default='localhost') | ||||||
|     config_ldap_port = Column(SmallInteger, default=389) |     config_ldap_port = Column(SmallInteger, default=389) | ||||||
|  | @ -179,11 +186,20 @@ class _ConfigSQL(object): | ||||||
|     def show_detail_random(self): |     def show_detail_random(self): | ||||||
|         return self.show_element_new_user(constants.DETAIL_RANDOM) |         return self.show_element_new_user(constants.DETAIL_RANDOM) | ||||||
| 
 | 
 | ||||||
|     def show_mature_content(self): |     def list_denied_tags(self): | ||||||
|         return self.show_element_new_user(constants.MATURE_CONTENT) |         mct = self.config_denied_tags.split(",") | ||||||
|  |         return [t.strip() for t in mct] | ||||||
| 
 | 
 | ||||||
|     def mature_content_tags(self): |     def list_allowed_tags(self): | ||||||
|         mct = self.config_mature_content_tags.split(",") |         mct = self.config_allowed_tags.split(",") | ||||||
|  |         return [t.strip() for t in mct] | ||||||
|  | 
 | ||||||
|  |     def list_denied_column_values(self): | ||||||
|  |         mct = self.config_denied_column_value.split(",") | ||||||
|  |         return [t.strip() for t in mct] | ||||||
|  | 
 | ||||||
|  |     def list_allowed_column_values(self): | ||||||
|  |         mct = self.config_allowed_column_value.split(",") | ||||||
|         return [t.strip() for t in mct] |         return [t.strip() for t in mct] | ||||||
| 
 | 
 | ||||||
|     def get_log_level(self): |     def get_log_level(self): | ||||||
|  | @ -323,5 +339,12 @@ def load_configuration(session): | ||||||
|     if not session.query(_Settings).count(): |     if not session.query(_Settings).count(): | ||||||
|         session.add(_Settings()) |         session.add(_Settings()) | ||||||
|         session.commit() |         session.commit() | ||||||
| 
 |     conf = _ConfigSQL(session) | ||||||
|     return _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() | ||||||
|  |     return conf | ||||||
|  |  | ||||||
|  | @ -106,7 +106,6 @@ except ValueError: | ||||||
| del env_CALIBRE_PORT | del env_CALIBRE_PORT | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| EXTENSIONS_AUDIO    = {'mp3', 'm4a', 'm4b'} | EXTENSIONS_AUDIO    = {'mp3', 'm4a', 'm4b'} | ||||||
| EXTENSIONS_CONVERT  = {'pdf', 'epub', 'mobi', 'azw3', 'docx', 'rtf', 'fb2', 'lit', 'lrf', 'txt', 'htmlz', 'rtf', 'odt'} | EXTENSIONS_CONVERT  = {'pdf', 'epub', 'mobi', 'azw3', 'docx', 'rtf', 'fb2', 'lit', 'lrf', 'txt', 'htmlz', 'rtf', 'odt'} | ||||||
| EXTENSIONS_UPLOAD   = {'txt', 'pdf', 'epub', 'mobi', 'azw', 'azw3', 'cbr', 'cbz', 'cbt', 'djvu', 'prc', 'doc', 'docx', | EXTENSIONS_UPLOAD   = {'txt', 'pdf', 'epub', 'mobi', 'azw', 'azw3', 'cbr', 'cbz', 'cbt', 'djvu', 'prc', 'doc', 'docx', | ||||||
|  | @ -126,7 +125,7 @@ def selected_roles(dictionary): | ||||||
| BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, description, tags, series, ' | BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, description, tags, series, ' | ||||||
|                                   'series_id, languages') |                                   'series_id, languages') | ||||||
| 
 | 
 | ||||||
| STABLE_VERSION = {'version': '0.6.5 Beta'} | STABLE_VERSION = {'version': '0.6.7 Beta'} | ||||||
| 
 | 
 | ||||||
| NIGHTLY_VERSION = {} | NIGHTLY_VERSION = {} | ||||||
| NIGHTLY_VERSION[0] = '$Format:%H$' | NIGHTLY_VERSION[0] = '$Format:%H$' | ||||||
|  |  | ||||||
							
								
								
									
										15
									
								
								cps/db.py
									
									
									
									
									
								
							
							
						
						
									
										15
									
								
								cps/db.py
									
									
									
									
									
								
							|  | @ -25,13 +25,13 @@ import ast | ||||||
| 
 | 
 | ||||||
| from sqlalchemy import create_engine | from sqlalchemy import create_engine | ||||||
| from sqlalchemy import Table, Column, ForeignKey | from sqlalchemy import Table, Column, ForeignKey | ||||||
| from sqlalchemy import String, Integer, Boolean | from sqlalchemy import String, Integer, Boolean, TIMESTAMP, Float | ||||||
| from sqlalchemy.orm import relationship, sessionmaker, scoped_session | from sqlalchemy.orm import relationship, sessionmaker, scoped_session | ||||||
| from sqlalchemy.ext.declarative import declarative_base | from sqlalchemy.ext.declarative import declarative_base | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| session = None | session = None | ||||||
| cc_exceptions = ['datetime', 'comments', 'float', 'composite', 'series'] | cc_exceptions = ['datetime', 'comments', 'composite', 'series'] | ||||||
| cc_classes = {} | cc_classes = {} | ||||||
| engine = None | engine = None | ||||||
| 
 | 
 | ||||||
|  | @ -251,10 +251,10 @@ class Books(Base): | ||||||
|     title = Column(String) |     title = Column(String) | ||||||
|     sort = Column(String) |     sort = Column(String) | ||||||
|     author_sort = Column(String) |     author_sort = Column(String) | ||||||
|     timestamp = Column(String) |     timestamp = Column(TIMESTAMP) | ||||||
|     pubdate = Column(String) |     pubdate = Column(String) | ||||||
|     series_index = Column(String) |     series_index = Column(String) | ||||||
|     last_modified = Column(String) |     last_modified = Column(TIMESTAMP) | ||||||
|     path = Column(String) |     path = Column(String) | ||||||
|     has_cover = Column(Integer) |     has_cover = Column(Integer) | ||||||
|     uuid = Column(String) |     uuid = Column(String) | ||||||
|  | @ -378,6 +378,11 @@ def setup_db(config): | ||||||
|                               'id': Column(Integer, primary_key=True), |                               'id': Column(Integer, primary_key=True), | ||||||
|                               'book': Column(Integer, ForeignKey('books.id')), |                               'book': Column(Integer, ForeignKey('books.id')), | ||||||
|                               'value': Column(Integer)} |                               'value': Column(Integer)} | ||||||
|  |                 elif row.datatype == 'float': | ||||||
|  |                     ccdict = {'__tablename__': 'custom_column_' + str(row.id), | ||||||
|  |                               'id': Column(Integer, primary_key=True), | ||||||
|  |                               'book': Column(Integer, ForeignKey('books.id')), | ||||||
|  |                               'value': Column(Float)} | ||||||
|                 else: |                 else: | ||||||
|                     ccdict = {'__tablename__': 'custom_column_' + str(row.id), |                     ccdict = {'__tablename__': 'custom_column_' + str(row.id), | ||||||
|                               'id': Column(Integer, primary_key=True), |                               'id': Column(Integer, primary_key=True), | ||||||
|  | @ -385,7 +390,7 @@ def setup_db(config): | ||||||
|                 cc_classes[row.id] = type(str('Custom_Column_' + str(row.id)), (Base,), ccdict) |                 cc_classes[row.id] = type(str('Custom_Column_' + str(row.id)), (Base,), ccdict) | ||||||
| 
 | 
 | ||||||
|         for cc_id in cc_ids: |         for cc_id in cc_ids: | ||||||
|             if (cc_id[1] == 'bool') or (cc_id[1] == 'int'): |             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]], |                 setattr(Books, 'custom_column_' + str(cc_id[0]), relationship(cc_classes[cc_id[0]], | ||||||
|                                                                            primaryjoin=( |                                                                            primaryjoin=( | ||||||
|                                                                            Books.id == cc_classes[cc_id[0]].book), |                                                                            Books.id == cc_classes[cc_id[0]].book), | ||||||
|  |  | ||||||
|  | @ -175,7 +175,7 @@ def delete_book(book_id, book_format): | ||||||
|                     cc_string = "custom_column_" + str(c.id) |                     cc_string = "custom_column_" + str(c.id) | ||||||
|                     if not c.is_multiple: |                     if not c.is_multiple: | ||||||
|                         if len(getattr(book, cc_string)) > 0: |                         if len(getattr(book, cc_string)) > 0: | ||||||
|                             if c.datatype == 'bool' or c.datatype == 'integer': |                             if c.datatype == 'bool' or c.datatype == 'integer' or c.datatype == 'float': | ||||||
|                                 del_cc = getattr(book, cc_string)[0] |                                 del_cc = getattr(book, cc_string)[0] | ||||||
|                                 getattr(book, cc_string).remove(del_cc) |                                 getattr(book, cc_string).remove(del_cc) | ||||||
|                                 db.session.delete(del_cc) |                                 db.session.delete(del_cc) | ||||||
|  | @ -254,7 +254,7 @@ def edit_cc_data(book_id, book, to_save): | ||||||
|             else: |             else: | ||||||
|                 cc_db_value = None |                 cc_db_value = None | ||||||
|             if to_save[cc_string].strip(): |             if to_save[cc_string].strip(): | ||||||
|                 if c.datatype == 'int' or c.datatype == 'bool': |                 if c.datatype == 'int' or c.datatype == 'bool' or c.datatype == 'float': | ||||||
|                     if to_save[cc_string] == 'None': |                     if to_save[cc_string] == 'None': | ||||||
|                         to_save[cc_string] = None |                         to_save[cc_string] = None | ||||||
|                     elif c.datatype == 'bool': |                     elif c.datatype == 'bool': | ||||||
|  | @ -369,11 +369,11 @@ def upload_cover(request, book): | ||||||
|         requested_file = request.files['btn-upload-cover'] |         requested_file = request.files['btn-upload-cover'] | ||||||
|         # check for empty request |         # check for empty request | ||||||
|         if requested_file.filename != '': |         if requested_file.filename != '': | ||||||
|             if helper.save_cover(requested_file, book.path) is True: |             ret, message = helper.save_cover(requested_file, book.path) | ||||||
|  |             if ret is True: | ||||||
|                 return True |                 return True | ||||||
|             else: |             else: | ||||||
|                 # ToDo Message not always coorect |                 flash(message, category="error") | ||||||
|                 flash(_(u"Cover is not a supported imageformat (jpg/png/webp), can't save"), category="error") |  | ||||||
|                 return False |                 return False | ||||||
|     return None |     return None | ||||||
| 
 | 
 | ||||||
|  | @ -697,7 +697,6 @@ def upload(): | ||||||
|             # Reread book. It's important not to filter the result, as it could have language which hide it from |             # Reread book. It's important not to filter the result, as it could have language which hide it from | ||||||
|             # current users view (tags are not stored/extracted from metadata and could also be limited) |             # current users view (tags are not stored/extracted from metadata and could also be limited) | ||||||
|             book = db.session.query(db.Books).filter(db.Books.id == book_id).first() |             book = db.session.query(db.Books).filter(db.Books.id == book_id).first() | ||||||
| 
 |  | ||||||
|             # upload book to gdrive if nesseccary and add "(bookid)" to folder name |             # upload book to gdrive if nesseccary and add "(bookid)" to folder name | ||||||
|             if config.config_use_google_drive: |             if config.config_use_google_drive: | ||||||
|                 gdriveutils.updateGdriveCalibreFromLocal() |                 gdriveutils.updateGdriveCalibreFromLocal() | ||||||
|  |  | ||||||
|  | @ -1,4 +1,3 @@ | ||||||
| #!/usr/bin/env python |  | ||||||
| # -*- coding: utf-8 -*- | # -*- coding: utf-8 -*- | ||||||
| 
 | 
 | ||||||
| #  This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) | #  This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) | ||||||
|  |  | ||||||
|  | @ -1,4 +1,3 @@ | ||||||
| #!/usr/bin/env python |  | ||||||
| # -*- coding: utf-8 -*- | # -*- coding: utf-8 -*- | ||||||
| 
 | 
 | ||||||
| #  This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) | #  This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) | ||||||
|  |  | ||||||
|  | @ -1,4 +1,3 @@ | ||||||
| #!/usr/bin/env python |  | ||||||
| # -*- coding: utf-8 -*- | # -*- coding: utf-8 -*- | ||||||
| 
 | 
 | ||||||
| #  This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) | #  This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) | ||||||
|  |  | ||||||
|  | @ -1,4 +1,3 @@ | ||||||
| #!/usr/bin/env python |  | ||||||
| # -*- coding: utf-8 -*- | # -*- coding: utf-8 -*- | ||||||
| 
 | 
 | ||||||
| #  This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) | #  This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) | ||||||
|  |  | ||||||
|  | @ -448,32 +448,46 @@ def delete_book(book, calibrepath, book_format): | ||||||
|         return delete_book_file(book, calibrepath, book_format) |         return delete_book_file(book, calibrepath, book_format) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def get_book_cover(book_id): | def get_cover_on_failure(use_generic_cover): | ||||||
|     book = db.session.query(db.Books).filter(db.Books.id == book_id).first() |     if use_generic_cover: | ||||||
|     if book.has_cover: |         return send_from_directory(_STATIC_DIR, "generic_cover.jpg") | ||||||
|  |     else: | ||||||
|  |         return None | ||||||
| 
 | 
 | ||||||
|  | def get_book_cover(book_id): | ||||||
|  |     book = db.session.query(db.Books).filter(db.Books.id == book_id).filter(common_filters()).first() | ||||||
|  |     return get_book_cover_internal(book, use_generic_cover_on_failure=True) | ||||||
|  | 
 | ||||||
|  | def get_book_cover_with_uuid(book_uuid, | ||||||
|  |                    use_generic_cover_on_failure=True): | ||||||
|  |     book = db.session.query(db.Books).filter(db.Books.uuid == book_uuid).first() | ||||||
|  |     return get_book_cover_internal(book, use_generic_cover_on_failure) | ||||||
|  | 
 | ||||||
|  | def get_book_cover_internal(book, | ||||||
|  |                    use_generic_cover_on_failure): | ||||||
|  |     if book and book.has_cover: | ||||||
|         if config.config_use_google_drive: |         if config.config_use_google_drive: | ||||||
|             try: |             try: | ||||||
|                 if not gd.is_gdrive_ready(): |                 if not gd.is_gdrive_ready(): | ||||||
|                     return send_from_directory(_STATIC_DIR, "generic_cover.jpg") |                     return get_cover_on_failure(use_generic_cover_on_failure) | ||||||
|                 path=gd.get_cover_via_gdrive(book.path) |                 path=gd.get_cover_via_gdrive(book.path) | ||||||
|                 if path: |                 if path: | ||||||
|                     return redirect(path) |                     return redirect(path) | ||||||
|                 else: |                 else: | ||||||
|                     log.error('%s/cover.jpg not found on Google Drive', book.path) |                     log.error('%s/cover.jpg not found on Google Drive', book.path) | ||||||
|                     return send_from_directory(_STATIC_DIR, "generic_cover.jpg") |                     return get_cover_on_failure(use_generic_cover_on_failure) | ||||||
|             except Exception as e: |             except Exception as e: | ||||||
|                 log.exception(e) |                 log.exception(e) | ||||||
|                 # traceback.print_exc() |                 # traceback.print_exc() | ||||||
|                 return send_from_directory(_STATIC_DIR,"generic_cover.jpg") |                 return get_cover_on_failure(use_generic_cover_on_failure) | ||||||
|         else: |         else: | ||||||
|             cover_file_path = os.path.join(config.config_calibre_dir, book.path) |             cover_file_path = os.path.join(config.config_calibre_dir, book.path) | ||||||
|             if os.path.isfile(os.path.join(cover_file_path, "cover.jpg")): |             if os.path.isfile(os.path.join(cover_file_path, "cover.jpg")): | ||||||
|                 return send_from_directory(cover_file_path, "cover.jpg") |                 return send_from_directory(cover_file_path, "cover.jpg") | ||||||
|             else: |             else: | ||||||
|                 return send_from_directory(_STATIC_DIR,"generic_cover.jpg") |                 return get_cover_on_failure(use_generic_cover_on_failure) | ||||||
|     else: |     else: | ||||||
|         return send_from_directory(_STATIC_DIR,"generic_cover.jpg") |         return get_cover_on_failure(use_generic_cover_on_failure) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| # saves book cover from url | # saves book cover from url | ||||||
|  | @ -494,16 +508,16 @@ def save_cover_from_filestorage(filepath, saved_filename, img): | ||||||
|                 os.makedirs(filepath) |                 os.makedirs(filepath) | ||||||
|             except OSError: |             except OSError: | ||||||
|                 log.error(u"Failed to create path for cover") |                 log.error(u"Failed to create path for cover") | ||||||
|                 return False |                 return False, _(u"Failed to create path for cover") | ||||||
|         try: |         try: | ||||||
|             img.save(os.path.join(filepath, saved_filename)) |             img.save(os.path.join(filepath, saved_filename)) | ||||||
|         except IOError: |         except IOError: | ||||||
|             log.error(u"Cover-file is not a valid image file") |             log.error(u"Cover-file is not a valid image file") | ||||||
|             return False |             return False, _(u"Cover-file is not a valid image file") | ||||||
|         except OSError: |         except OSError: | ||||||
|             log.error(u"Failed to store cover-file") |             log.error(u"Failed to store cover-file") | ||||||
|             return False |             return False, _(u"Failed to store cover-file") | ||||||
|     return True |     return True, None | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| # saves book cover to gdrive or locally | # saves book cover to gdrive or locally | ||||||
|  | @ -513,7 +527,7 @@ def save_cover(img, book_path): | ||||||
|     if use_PIL: |     if use_PIL: | ||||||
|         if content_type not in ('image/jpeg', 'image/png', 'image/webp'): |         if content_type not in ('image/jpeg', 'image/png', 'image/webp'): | ||||||
|             log.error("Only jpg/jpeg/png/webp files are supported as coverfile") |             log.error("Only jpg/jpeg/png/webp files are supported as coverfile") | ||||||
|             return False |             return False, _("Only jpg/jpeg/png/webp files are supported as coverfile") | ||||||
|         # convert to jpg because calibre only supports jpg |         # convert to jpg because calibre only supports jpg | ||||||
|         if content_type in ('image/png', 'image/webp'): |         if content_type in ('image/png', 'image/webp'): | ||||||
|             if hasattr(img,'stream'): |             if hasattr(img,'stream'): | ||||||
|  | @ -527,17 +541,18 @@ def save_cover(img, book_path): | ||||||
|     else: |     else: | ||||||
|         if content_type not in ('image/jpeg'): |         if content_type not in ('image/jpeg'): | ||||||
|             log.error("Only jpg/jpeg files are supported as coverfile") |             log.error("Only jpg/jpeg files are supported as coverfile") | ||||||
|             return False |             return False, _("Only jpg/jpeg files are supported as coverfile") | ||||||
| 
 | 
 | ||||||
|     if config.config_use_google_drive: |     if config.config_use_google_drive: | ||||||
|         tmpDir = gettempdir() |         tmpDir = gettempdir() | ||||||
|         if save_cover_from_filestorage(tmpDir, "uploaded_cover.jpg", img) is True: |         ret, message = save_cover_from_filestorage(tmpDir, "uploaded_cover.jpg", img) | ||||||
|  |         if ret is True: | ||||||
|             gd.uploadFileToEbooksFolder(os.path.join(book_path, 'cover.jpg'), |             gd.uploadFileToEbooksFolder(os.path.join(book_path, 'cover.jpg'), | ||||||
|                                         os.path.join(tmpDir, "uploaded_cover.jpg")) |                                         os.path.join(tmpDir, "uploaded_cover.jpg")) | ||||||
|             log.info("Cover is saved on Google Drive") |             log.info("Cover is saved on Google Drive") | ||||||
|             return True |             return True, None | ||||||
|         else: |         else: | ||||||
|             return False |             return False, message | ||||||
|     else: |     else: | ||||||
|         return save_cover_from_filestorage(os.path.join(config.config_calibre_dir, book_path), "cover.jpg", img) |         return save_cover_from_filestorage(os.path.join(config.config_calibre_dir, book_path), "cover.jpg", img) | ||||||
| 
 | 
 | ||||||
|  | @ -674,20 +689,40 @@ def common_filters(): | ||||||
|         lang_filter = db.Books.languages.any(db.Languages.lang_code == current_user.filter_language()) |         lang_filter = db.Books.languages.any(db.Languages.lang_code == current_user.filter_language()) | ||||||
|     else: |     else: | ||||||
|         lang_filter = true() |         lang_filter = true() | ||||||
|     content_rating_filter = false() if current_user.mature_content else \ |     negtags_list = current_user.list_denied_tags() | ||||||
|         db.Books.tags.any(db.Tags.name.in_(config.mature_content_tags())) |     postags_list = current_user.list_allowed_tags() | ||||||
|     return and_(lang_filter, ~content_rating_filter) |     neg_content_tags_filter = false() if negtags_list == [''] else db.Books.tags.any(db.Tags.name.in_(negtags_list)) | ||||||
|  |     pos_content_tags_filter = true() if postags_list == [''] else db.Books.tags.any(db.Tags.name.in_(postags_list)) | ||||||
|  |     if config.config_restricted_column: | ||||||
|  |         pos_cc_list = current_user.allowed_column_value.split(',') | ||||||
|  |         pos_content_cc_filter = true() if pos_cc_list == [''] else \ | ||||||
|  |             getattr(db.Books, 'custom_column_' + str(config.config_restricted_column)).\ | ||||||
|  |                 any(db.cc_classes[config.config_restricted_column].value.in_(pos_cc_list)) | ||||||
|  |         neg_cc_list = current_user.denied_column_value.split(',') | ||||||
|  |         neg_content_cc_filter = false() if neg_cc_list == [''] else \ | ||||||
|  |             getattr(db.Books, 'custom_column_' + str(config.config_restricted_column)).\ | ||||||
|  |                 any(db.cc_classes[config.config_restricted_column].value.in_(neg_cc_list)) | ||||||
|  |     else: | ||||||
|  |         pos_content_cc_filter = true() | ||||||
|  |         neg_content_cc_filter = false() | ||||||
|  |     return and_(lang_filter, pos_content_tags_filter, ~neg_content_tags_filter, | ||||||
|  |                 pos_content_cc_filter, ~neg_content_cc_filter) | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| def tags_filters(): | def tags_filters(): | ||||||
|     return ~(false() if current_user.mature_content else \ |     negtags_list = current_user.list_denied_tags() | ||||||
|         db.Tags.name.in_(config.mature_content_tags())) |     postags_list = current_user.list_allowed_tags() | ||||||
|     # return db.session.query(db.Tags).filter(~content_rating_filter).order_by(db.Tags.name).all() |     neg_content_tags_filter = false() if negtags_list == [''] else db.Tags.name.in_(negtags_list) | ||||||
|  |     pos_content_tags_filter = true() if postags_list == [''] else db.Tags.name.in_(postags_list) | ||||||
|  |     return and_(pos_content_tags_filter, ~neg_content_tags_filter) | ||||||
|  |     # return ~(false()) if postags_list == [''] else db.Tags.in_(postags_list) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| # Creates for all stored languages a translated speaking name in the array for the UI | # Creates for all stored languages a translated speaking name in the array for the UI | ||||||
| def speaking_language(languages=None): | def speaking_language(languages=None): | ||||||
|     if not languages: |     if not languages: | ||||||
|         languages = db.session.query(db.Languages).all() |         languages = db.session.query(db.Languages).join(db.books_languages_link).join(db.Books).filter(common_filters())\ | ||||||
|  |         .group_by(text('books_languages_link.lang_code')).all() | ||||||
|     for lang in languages: |     for lang in languages: | ||||||
|         try: |         try: | ||||||
|             cur_l = LC.parse(lang.lang_code) |             cur_l = LC.parse(lang.lang_code) | ||||||
|  | @ -774,7 +809,7 @@ def get_cc_columns(): | ||||||
|         cc = [] |         cc = [] | ||||||
|         for col in tmpcc: |         for col in tmpcc: | ||||||
|             r = re.compile(config.config_columns_to_ignore) |             r = re.compile(config.config_columns_to_ignore) | ||||||
|             if r.match(col.label): |             if not r.match(col.name): | ||||||
|                 cc.append(col) |                 cc.append(col) | ||||||
|     else: |     else: | ||||||
|         cc = tmpcc |         cc = tmpcc | ||||||
|  | @ -784,11 +819,11 @@ def get_download_link(book_id, book_format): | ||||||
|     book_format = book_format.split(".")[0] |     book_format = book_format.split(".")[0] | ||||||
|     book = db.session.query(db.Books).filter(db.Books.id == book_id).filter(common_filters()).first() |     book = db.session.query(db.Books).filter(db.Books.id == book_id).filter(common_filters()).first() | ||||||
|     if book: |     if book: | ||||||
|         data = db.session.query(db.Data).filter(db.Data.book == book.id)\ |         data1 = db.session.query(db.Data).filter(db.Data.book == book.id)\ | ||||||
|             .filter(db.Data.format == book_format.upper()).first() |             .filter(db.Data.format == book_format.upper()).first() | ||||||
|     else: |     else: | ||||||
|         abort(404) |         abort(404) | ||||||
|     if data: |     if data1: | ||||||
|         # collect downloaded books only for registered user and not for anonymous user |         # collect downloaded books only for registered user and not for anonymous user | ||||||
|         if current_user.is_authenticated: |         if current_user.is_authenticated: | ||||||
|             ub.update_download(book_id, int(current_user.id)) |             ub.update_download(book_id, int(current_user.id)) | ||||||
|  | @ -798,9 +833,9 @@ def get_download_link(book_id, book_format): | ||||||
|         file_name = get_valid_filename(file_name) |         file_name = get_valid_filename(file_name) | ||||||
|         headers = Headers() |         headers = Headers() | ||||||
|         headers["Content-Type"] = mimetypes.types_map.get('.' + book_format, "application/octet-stream") |         headers["Content-Type"] = mimetypes.types_map.get('.' + book_format, "application/octet-stream") | ||||||
|         headers["Content-Disposition"] = "attachment; filename*=UTF-8''%s.%s" % (quote(file_name.encode('utf-8')), |         headers["Content-Disposition"] = "attachment; filename=%s.%s; filename*=UTF-8''%s.%s" % ( | ||||||
|                                                                                  book_format) |             quote(file_name.encode('utf-8')), book_format, quote(file_name.encode('utf-8')), book_format) | ||||||
|         return do_download_file(book, book_format, data, headers) |         return do_download_file(book, book_format, data1, headers) | ||||||
|     else: |     else: | ||||||
|         abort(404) |         abort(404) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,4 +1,3 @@ | ||||||
| #!/usr/bin/env python |  | ||||||
| # -*- coding: utf-8 -*- | # -*- coding: utf-8 -*- | ||||||
| 
 | 
 | ||||||
| #  This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) | #  This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) | ||||||
|  |  | ||||||
							
								
								
									
										629
									
								
								cps/kobo.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										629
									
								
								cps/kobo.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,629 @@ | ||||||
|  | #!/usr/bin/env python | ||||||
|  | # -*- coding: utf-8 -*- | ||||||
|  | 
 | ||||||
|  | #  This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) | ||||||
|  | #    Copyright (C) 2018-2019 shavitmichael, 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 <http://www.gnu.org/licenses/>. | ||||||
|  | 
 | ||||||
|  | import sys | ||||||
|  | import base64 | ||||||
|  | import os | ||||||
|  | import uuid | ||||||
|  | from datetime import datetime | ||||||
|  | from time import gmtime, strftime | ||||||
|  | try: | ||||||
|  |     from urllib import unquote | ||||||
|  | except ImportError: | ||||||
|  |     from urllib.parse import unquote | ||||||
|  | 
 | ||||||
|  | from flask import ( | ||||||
|  |     Blueprint, | ||||||
|  |     request, | ||||||
|  |     make_response, | ||||||
|  |     jsonify, | ||||||
|  |     current_app, | ||||||
|  |     url_for, | ||||||
|  |     redirect, | ||||||
|  |     abort | ||||||
|  | ) | ||||||
|  | from flask_login import login_required | ||||||
|  | from werkzeug.datastructures import Headers | ||||||
|  | from sqlalchemy import func | ||||||
|  | import requests | ||||||
|  | 
 | ||||||
|  | from . import config, logger, kobo_auth, db, helper | ||||||
|  | from .services import SyncToken as SyncToken | ||||||
|  | from .web import download_required | ||||||
|  | from .kobo_auth import requires_kobo_auth | ||||||
|  | 
 | ||||||
|  | KOBO_FORMATS = {"KEPUB": ["KEPUB"], "EPUB": ["EPUB3", "EPUB"]} | ||||||
|  | KOBO_STOREAPI_URL = "https://storeapi.kobo.com" | ||||||
|  | 
 | ||||||
|  | kobo = Blueprint("kobo", __name__, url_prefix="/kobo/<auth_token>") | ||||||
|  | kobo_auth.disable_failed_auth_redirect_for_blueprint(kobo) | ||||||
|  | kobo_auth.register_url_value_preprocessor(kobo) | ||||||
|  | 
 | ||||||
|  | log = logger.create() | ||||||
|  | 
 | ||||||
|  | def get_store_url_for_current_request(): | ||||||
|  |     # Programmatically modify the current url to point to the official Kobo store | ||||||
|  |     base, sep, request_path_with_auth_token = request.full_path.rpartition("/kobo/") | ||||||
|  |     auth_token, sep, request_path = request_path_with_auth_token.rstrip("?").partition( | ||||||
|  |         "/" | ||||||
|  |     ) | ||||||
|  |     return KOBO_STOREAPI_URL + "/" + request_path | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | CONNECTION_SPECIFIC_HEADERS = [ | ||||||
|  |     "connection", | ||||||
|  |     "content-encoding", | ||||||
|  |     "content-length", | ||||||
|  |     "transfer-encoding", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | def get_kobo_activated(): | ||||||
|  |     return config.config_kobo_sync | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def make_request_to_kobo_store(sync_token=None): | ||||||
|  |     outgoing_headers = Headers(request.headers) | ||||||
|  |     outgoing_headers.remove("Host") | ||||||
|  |     if sync_token: | ||||||
|  |         sync_token.set_kobo_store_header(outgoing_headers) | ||||||
|  | 
 | ||||||
|  |     store_response = requests.request( | ||||||
|  |         method=request.method, | ||||||
|  |         url=get_store_url_for_current_request(), | ||||||
|  |         headers=outgoing_headers, | ||||||
|  |         data=request.get_data(), | ||||||
|  |         allow_redirects=False, | ||||||
|  |         timeout=(2, 10) | ||||||
|  |     ) | ||||||
|  |     return store_response | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def redirect_or_proxy_request(): | ||||||
|  |     if config.config_kobo_proxy: | ||||||
|  |         if request.method == "GET": | ||||||
|  |             return redirect(get_store_url_for_current_request(), 307) | ||||||
|  |         if request.method == "DELETE": | ||||||
|  |             log.info('Delete Book') | ||||||
|  |             return make_response(jsonify({})) | ||||||
|  |         else: | ||||||
|  |             # The Kobo device turns other request types into GET requests on redirects, so we instead proxy to the Kobo store ourselves. | ||||||
|  |             store_response = make_request_to_kobo_store() | ||||||
|  | 
 | ||||||
|  |             response_headers = store_response.headers | ||||||
|  |             for header_key in CONNECTION_SPECIFIC_HEADERS: | ||||||
|  |                 response_headers.pop(header_key, default=None) | ||||||
|  | 
 | ||||||
|  |             return make_response( | ||||||
|  |                 store_response.content, store_response.status_code, response_headers.items() | ||||||
|  |             ) | ||||||
|  |     else: | ||||||
|  |         return make_response(jsonify({})) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @kobo.route("/v1/library/sync") | ||||||
|  | @requires_kobo_auth | ||||||
|  | @download_required | ||||||
|  | def HandleSyncRequest(): | ||||||
|  |     sync_token = SyncToken.SyncToken.from_headers(request.headers) | ||||||
|  |     log.info("Kobo library sync request received.") | ||||||
|  |     if not current_app.wsgi_app.is_proxied: | ||||||
|  |         log.debug('Kobo: Received unproxied request, changed request port to server port') | ||||||
|  | 
 | ||||||
|  |     # TODO: Limit the number of books return per sync call, and rely on the sync-continuatation header | ||||||
|  |     # instead so that the device triggers another sync. | ||||||
|  | 
 | ||||||
|  |     new_books_last_modified = sync_token.books_last_modified | ||||||
|  |     new_books_last_created = sync_token.books_last_created | ||||||
|  |     entitlements = [] | ||||||
|  | 
 | ||||||
|  |     # 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). | ||||||
|  |     db.reconnect_db(config) | ||||||
|  | 
 | ||||||
|  |     # 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 = ( | ||||||
|  |         db.session.query(db.Books) | ||||||
|  |         .join(db.Data) | ||||||
|  |         .filter(func.datetime(db.Books.last_modified) > sync_token.books_last_modified) | ||||||
|  |         .filter(db.Data.format.in_(KOBO_FORMATS)) | ||||||
|  |         .all() | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     for book in changed_entries: | ||||||
|  |         entitlement = { | ||||||
|  |             "BookEntitlement": create_book_entitlement(book), | ||||||
|  |             "BookMetadata": get_metadata(book), | ||||||
|  |             "ReadingState": reading_state(book), | ||||||
|  |         } | ||||||
|  |         if book.timestamp > sync_token.books_last_created: | ||||||
|  |             entitlements.append({"NewEntitlement": entitlement}) | ||||||
|  |         else: | ||||||
|  |             entitlements.append({"ChangedEntitlement": entitlement}) | ||||||
|  | 
 | ||||||
|  |         new_books_last_modified = max( | ||||||
|  |             book.last_modified, sync_token.books_last_modified | ||||||
|  |         ) | ||||||
|  |         new_books_last_created = max(book.timestamp, sync_token.books_last_created) | ||||||
|  | 
 | ||||||
|  |     sync_token.books_last_created = new_books_last_created | ||||||
|  |     sync_token.books_last_modified = new_books_last_modified | ||||||
|  | 
 | ||||||
|  |     if config.config_kobo_proxy: | ||||||
|  |         return generate_sync_response(request, sync_token, entitlements) | ||||||
|  | 
 | ||||||
|  |     return make_response(jsonify(entitlements)) | ||||||
|  |     # Missing feature: Detect server-side book deletions. | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def generate_sync_response(request, sync_token, entitlements): | ||||||
|  |     extra_headers = {} | ||||||
|  |     if config.config_kobo_proxy: | ||||||
|  |         # Merge in sync results from the official Kobo store. | ||||||
|  |         try: | ||||||
|  |             store_response = make_request_to_kobo_store(sync_token) | ||||||
|  | 
 | ||||||
|  |             store_entitlements = store_response.json() | ||||||
|  |             entitlements += store_entitlements | ||||||
|  |             sync_token.merge_from_store_response(store_response) | ||||||
|  |             extra_headers["x-kobo-sync"] = store_response.headers.get("x-kobo-sync") | ||||||
|  |             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)) | ||||||
|  |     sync_token.to_headers(extra_headers) | ||||||
|  | 
 | ||||||
|  |     response = make_response(jsonify(entitlements), extra_headers) | ||||||
|  | 
 | ||||||
|  |     return response | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @kobo.route("/v1/library/<book_uuid>/metadata") | ||||||
|  | @requires_kobo_auth | ||||||
|  | @download_required | ||||||
|  | def HandleMetadataRequest(book_uuid): | ||||||
|  |     if not current_app.wsgi_app.is_proxied: | ||||||
|  |         log.debug('Kobo: Received unproxied request, changed request port to server port') | ||||||
|  |     log.info("Kobo library metadata request received for book %s" % book_uuid) | ||||||
|  |     book = db.session.query(db.Books).filter(db.Books.uuid == book_uuid).first() | ||||||
|  |     if not book or not book.data: | ||||||
|  |         log.info(u"Book %s not found in database", book_uuid) | ||||||
|  |         return redirect_or_proxy_request() | ||||||
|  | 
 | ||||||
|  |     metadata = get_metadata(book) | ||||||
|  |     return jsonify([metadata]) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def get_download_url_for_book(book, book_format): | ||||||
|  |     if not current_app.wsgi_app.is_proxied: | ||||||
|  |         if ':' in request.host and not request.host.endswith(']') : | ||||||
|  |             host = "".join(request.host.split(':')[:-1]) | ||||||
|  |         else: | ||||||
|  |             host = request.host | ||||||
|  |         return "{url_scheme}://{url_base}:{url_port}/download/{book_id}/{book_format}".format( | ||||||
|  |             url_scheme=request.scheme, | ||||||
|  |             url_base=host, | ||||||
|  |             url_port=config.config_port, | ||||||
|  |             book_id=book.id, | ||||||
|  |             book_format=book_format.lower() | ||||||
|  |         ) | ||||||
|  |     return url_for( | ||||||
|  |         "web.download_link", | ||||||
|  |         book_id=book.id, | ||||||
|  |         book_format=book_format.lower(), | ||||||
|  |         _external=True, | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def create_book_entitlement(book): | ||||||
|  |     book_uuid = book.uuid | ||||||
|  |     return { | ||||||
|  |         "Accessibility": "Full", | ||||||
|  |         "ActivePeriod": {"From": current_time(),}, | ||||||
|  |         "Created": book.timestamp.strftime("%Y-%m-%dT%H:%M:%SZ"), | ||||||
|  |         "CrossRevisionId": book_uuid, | ||||||
|  |         "Id": book_uuid, | ||||||
|  |         "IsHiddenFromArchive": False, | ||||||
|  |         "IsLocked": False, | ||||||
|  |         # Setting this to true removes from the device. | ||||||
|  |         "IsRemoved": False, | ||||||
|  |         "LastModified": book.last_modified.strftime("%Y-%m-%dT%H:%M:%SZ"), | ||||||
|  |         "OriginCategory": "Imported", | ||||||
|  |         "RevisionId": book_uuid, | ||||||
|  |         "Status": "Active", | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def current_time(): | ||||||
|  |     return strftime("%Y-%m-%dT%H:%M:%SZ", gmtime()) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def get_description(book): | ||||||
|  |     if not book.comments: | ||||||
|  |         return None | ||||||
|  |     return book.comments[0].text | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # TODO handle multiple authors | ||||||
|  | def get_author(book): | ||||||
|  |     if not book.authors: | ||||||
|  |         return None | ||||||
|  |     return book.authors[0].name | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def get_publisher(book): | ||||||
|  |     if not book.publishers: | ||||||
|  |         return None | ||||||
|  |     return book.publishers[0].name | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def get_series(book): | ||||||
|  |     if not book.series: | ||||||
|  |         return None | ||||||
|  |     return book.series[0].name | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def get_metadata(book): | ||||||
|  |     download_urls = [] | ||||||
|  |     for book_data in book.data: | ||||||
|  |         if book_data.format not in KOBO_FORMATS: | ||||||
|  |             continue | ||||||
|  |         for kobo_format in KOBO_FORMATS[book_data.format]: | ||||||
|  |             # log.debug('Id: %s, Format: %s' % (book.id, kobo_format)) | ||||||
|  |             download_urls.append( | ||||||
|  |                 { | ||||||
|  |                     "Format": kobo_format, | ||||||
|  |                     "Size": book_data.uncompressed_size, | ||||||
|  |                     "Url": get_download_url_for_book(book, book_data.format), | ||||||
|  |                     # The Kobo forma accepts platforms: (Generic, Android) | ||||||
|  |                     "Platform": "Generic", | ||||||
|  |                     # "DrmType": "None", # Not required | ||||||
|  |                 } | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|  |     book_uuid = book.uuid | ||||||
|  |     metadata = { | ||||||
|  |         "Categories": ["00000000-0000-0000-0000-000000000001",], | ||||||
|  |         "Contributors": get_author(book), | ||||||
|  |         "CoverImageId": book_uuid, | ||||||
|  |         "CrossRevisionId": book_uuid, | ||||||
|  |         "CurrentDisplayPrice": {"CurrencyCode": "USD", "TotalAmount": 0}, | ||||||
|  |         "CurrentLoveDisplayPrice": {"TotalAmount": 0}, | ||||||
|  |         "Description": get_description(book), | ||||||
|  |         "DownloadUrls": download_urls, | ||||||
|  |         "EntitlementId": book_uuid, | ||||||
|  |         "ExternalIds": [], | ||||||
|  |         "Genre": "00000000-0000-0000-0000-000000000001", | ||||||
|  |         "IsEligibleForKoboLove": False, | ||||||
|  |         "IsInternetArchive": False, | ||||||
|  |         "IsPreOrder": False, | ||||||
|  |         "IsSocialEnabled": True, | ||||||
|  |         "Language": "en", | ||||||
|  |         "PhoneticPronunciations": {}, | ||||||
|  |         "PublicationDate": book.pubdate, | ||||||
|  |         "Publisher": {"Imprint": "", "Name": get_publisher(book),}, | ||||||
|  |         "RevisionId": book_uuid, | ||||||
|  |         "Title": book.title, | ||||||
|  |         "WorkId": book_uuid, | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if get_series(book): | ||||||
|  |         if sys.version_info < (3, 0): | ||||||
|  |             name = get_series(book).encode("utf-8") | ||||||
|  |         else: | ||||||
|  |             name = get_series(book) | ||||||
|  |         metadata["Series"] = { | ||||||
|  |             "Name": get_series(book), | ||||||
|  |             "Number": book.series_index, | ||||||
|  |             "NumberFloat": float(book.series_index), | ||||||
|  |             # Get a deterministic id based on the series name. | ||||||
|  |             "Id": uuid.uuid3(uuid.NAMESPACE_DNS, name), | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |     return metadata | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def reading_state(book): | ||||||
|  |     # TODO: Implement | ||||||
|  |     reading_state = { | ||||||
|  |         # "StatusInfo": { | ||||||
|  |         #     "LastModified": get_single_cc_value(book, "lastreadtimestamp"), | ||||||
|  |         #     "Status": get_single_cc_value(book, "reading_status"), | ||||||
|  |         # } | ||||||
|  |         # TODO: CurrentBookmark, Location | ||||||
|  |     } | ||||||
|  |     return reading_state | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @kobo.route("/<book_uuid>/image.jpg") | ||||||
|  | @requires_kobo_auth | ||||||
|  | def HandleCoverImageRequest(book_uuid): | ||||||
|  |     book_cover = helper.get_book_cover_with_uuid( | ||||||
|  |         book_uuid, use_generic_cover_on_failure=False | ||||||
|  |     ) | ||||||
|  |     if not book_cover: | ||||||
|  |         if config.config_kobo_proxy: | ||||||
|  |             log.debug("Cover for unknown book: %s proxied to kobo" % book_uuid) | ||||||
|  |             return redirect(get_store_url_for_current_request(), 307) | ||||||
|  |         else: | ||||||
|  |             log.debug("Cover for unknown book: %s requested" % book_uuid) | ||||||
|  |             return redirect_or_proxy_request() | ||||||
|  |     log.debug("Cover request received for book %s" % book_uuid) | ||||||
|  |     return book_cover | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @kobo.route("") | ||||||
|  | def TopLevelEndpoint(): | ||||||
|  |     return make_response(jsonify({})) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # TODO: Implement the following routes | ||||||
|  | @kobo.route("/v1/library/<dummy>", methods=["DELETE", "GET"]) | ||||||
|  | @kobo.route("/v1/library/<book_uuid>/state", methods=["PUT"]) | ||||||
|  | @kobo.route("/v1/library/tags", methods=["POST"]) | ||||||
|  | @kobo.route("/v1/library/tags/<shelf_name>", methods=["POST"]) | ||||||
|  | @kobo.route("/v1/library/tags/<tag_id>", methods=["DELETE"]) | ||||||
|  | def HandleUnimplementedRequest(dummy=None, book_uuid=None, shelf_name=None, tag_id=None): | ||||||
|  |     log.debug("Unimplemented Library Request received: %s", request.base_url) | ||||||
|  |     return redirect_or_proxy_request() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # TODO: Implement the following routes | ||||||
|  | @kobo.route("/v1/user/loyalty/<dummy>", methods=["GET", "POST"]) | ||||||
|  | @kobo.route("/v1/user/profile", methods=["GET", "POST"]) | ||||||
|  | @kobo.route("/v1/user/wishlist", methods=["GET", "POST"]) | ||||||
|  | @kobo.route("/v1/user/recommendations", methods=["GET", "POST"]) | ||||||
|  | @kobo.route("/v1/analytics/<dummy>", methods=["GET", "POST"]) | ||||||
|  | def HandleUserRequest(dummy=None): | ||||||
|  |     log.debug("Unimplemented User Request received: %s", request.base_url) | ||||||
|  |     return redirect_or_proxy_request() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @kobo.route("/v1/products/<dummy>/prices", methods=["GET", "POST"]) | ||||||
|  | @kobo.route("/v1/products/<dummy>/recommendations", methods=["GET", "POST"]) | ||||||
|  | @kobo.route("/v1/products/<dummy>/nextread", methods=["GET", "POST"]) | ||||||
|  | @kobo.route("/v1/products/<dummy>/reviews", methods=["GET", "POST"]) | ||||||
|  | @kobo.route("/v1/products/books/<dummy>", methods=["GET", "POST"]) | ||||||
|  | @kobo.route("/v1/products/dailydeal", methods=["GET", "POST"]) | ||||||
|  | @kobo.route("/v1/products", methods=["GET", "POST"]) | ||||||
|  | def HandleProductsRequest(dummy=None): | ||||||
|  |     log.debug("Unimplemented Products Request received: %s", request.base_url) | ||||||
|  |     return redirect_or_proxy_request() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @kobo.app_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) | ||||||
|  |     log.debug("Unknown Request received: %s", request.base_url) | ||||||
|  |     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. | ||||||
|  |     content = request.get_json() | ||||||
|  |     AccessToken = base64.b64encode(os.urandom(24)).decode('utf-8') | ||||||
|  |     RefreshToken = base64.b64encode(os.urandom(24)).decode('utf-8') | ||||||
|  |     return  make_response( | ||||||
|  |         jsonify( | ||||||
|  |             { | ||||||
|  |                 "AccessToken": AccessToken, | ||||||
|  |                 "RefreshToken": RefreshToken, | ||||||
|  |                 "TokenType": "Bearer", | ||||||
|  |                 "TrackingId": str(uuid.uuid4()), | ||||||
|  |                 "UserKey": content['UserKey'], | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @kobo.route("/v1/auth/device", methods=["POST"]) | ||||||
|  | @requires_kobo_auth | ||||||
|  | def HandleAuthRequest(): | ||||||
|  |     log.debug('Kobo Auth request') | ||||||
|  |     if config.config_kobo_proxy: | ||||||
|  |         try: | ||||||
|  |             return redirect_or_proxy_request() | ||||||
|  |         except: | ||||||
|  |             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() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def make_calibre_web_init_response(calibre_web_url): | ||||||
|  |         resources = NATIVE_KOBO_RESOURCES(calibre_web_url) | ||||||
|  |         response = make_response(jsonify({"Resources": resources})) | ||||||
|  |         response.headers["x-kobo-apitoken"] = "e30=" | ||||||
|  |         return response | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @kobo.route("/v1/initialization") | ||||||
|  | @requires_kobo_auth | ||||||
|  | def HandleInitRequest(): | ||||||
|  |     log.info('Init') | ||||||
|  | 
 | ||||||
|  |     if not current_app.wsgi_app.is_proxied: | ||||||
|  |         log.debug('Kobo: Received unproxied request, changed request port to server port') | ||||||
|  |         if ':' in request.host and not request.host.endswith(']'): | ||||||
|  |             host = "".join(request.host.split(':')[:-1]) | ||||||
|  |         else: | ||||||
|  |             host = request.host | ||||||
|  |         calibre_web_url = "{url_scheme}://{url_base}:{url_port}".format( | ||||||
|  |             url_scheme=request.scheme, | ||||||
|  |             url_base=host, | ||||||
|  |             url_port=config.config_port | ||||||
|  |         ) | ||||||
|  |     else: | ||||||
|  |         calibre_web_url = url_for("web.index", _external=True).strip("/") | ||||||
|  | 
 | ||||||
|  |     if config.config_kobo_proxy: | ||||||
|  |         try: | ||||||
|  |             store_response = make_request_to_kobo_store() | ||||||
|  | 
 | ||||||
|  |             store_response_json = store_response.json() | ||||||
|  |             if "Resources" in store_response_json: | ||||||
|  |                 kobo_resources = store_response_json["Resources"] | ||||||
|  |                 # calibre_web_url = url_for("web.index", _external=True).strip("/") | ||||||
|  |                 kobo_resources["image_host"] = calibre_web_url | ||||||
|  |                 kobo_resources["image_url_quality_template"] = unquote(calibre_web_url + url_for("kobo.HandleCoverImageRequest", | ||||||
|  |                     auth_token = kobo_auth.get_auth_token(), | ||||||
|  |                     book_uuid="{ImageId}")) | ||||||
|  |                 kobo_resources["image_url_template"] = unquote(calibre_web_url + url_for("kobo.HandleCoverImageRequest", | ||||||
|  |                     auth_token = kobo_auth.get_auth_token(), | ||||||
|  |                     book_uuid="{ImageId}")) | ||||||
|  | 
 | ||||||
|  |             return make_response(store_response_json, store_response.status_code) | ||||||
|  |         except: | ||||||
|  |             log.error("Failed to receive or parse response from Kobo's init endpoint. Falling back to un-proxied mode.") | ||||||
|  | 
 | ||||||
|  |     return make_calibre_web_init_response(calibre_web_url) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def NATIVE_KOBO_RESOURCES(calibre_web_url): | ||||||
|  |     return { | ||||||
|  |         "account_page": "https://secure.kobobooks.com/profile", | ||||||
|  |         "account_page_rakuten": "https://my.rakuten.co.jp/", | ||||||
|  |         "add_entitlement": "https://storeapi.kobo.com/v1/library/{RevisionIds}", | ||||||
|  |         "affiliaterequest": "https://storeapi.kobo.com/v1/affiliate", | ||||||
|  |         "audiobook_subscription_orange_deal_inclusion_url": "https://authorize.kobo.com/inclusion", | ||||||
|  |         "authorproduct_recommendations": "https://storeapi.kobo.com/v1/products/books/authors/recommendations", | ||||||
|  |         "autocomplete": "https://storeapi.kobo.com/v1/products/autocomplete", | ||||||
|  |         "blackstone_header": {"key": "x-amz-request-payer", "value": "requester"}, | ||||||
|  |         "book": "https://storeapi.kobo.com/v1/products/books/{ProductId}", | ||||||
|  |         "book_detail_page": "https://store.kobobooks.com/{culture}/ebook/{slug}", | ||||||
|  |         "book_detail_page_rakuten": "http://books.rakuten.co.jp/rk/{crossrevisionid}", | ||||||
|  |         "book_landing_page": "https://store.kobobooks.com/ebooks", | ||||||
|  |         "book_subscription": "https://storeapi.kobo.com/v1/products/books/subscriptions", | ||||||
|  |         "categories": "https://storeapi.kobo.com/v1/categories", | ||||||
|  |         "categories_page": "https://store.kobobooks.com/ebooks/categories", | ||||||
|  |         "category": "https://storeapi.kobo.com/v1/categories/{CategoryId}", | ||||||
|  |         "category_featured_lists": "https://storeapi.kobo.com/v1/categories/{CategoryId}/featured", | ||||||
|  |         "category_products": "https://storeapi.kobo.com/v1/categories/{CategoryId}/products", | ||||||
|  |         "checkout_borrowed_book": "https://storeapi.kobo.com/v1/library/borrow", | ||||||
|  |         "configuration_data": "https://storeapi.kobo.com/v1/configuration", | ||||||
|  |         "content_access_book": "https://storeapi.kobo.com/v1/products/books/{ProductId}/access", | ||||||
|  |         "customer_care_live_chat": "https://v2.zopim.com/widget/livechat.html?key=Y6gwUmnu4OATxN3Tli4Av9bYN319BTdO", | ||||||
|  |         "daily_deal": "https://storeapi.kobo.com/v1/products/dailydeal", | ||||||
|  |         "deals": "https://storeapi.kobo.com/v1/deals", | ||||||
|  |         "delete_entitlement": "https://storeapi.kobo.com/v1/library/{Ids}", | ||||||
|  |         "delete_tag": "https://storeapi.kobo.com/v1/library/tags/{TagId}", | ||||||
|  |         "delete_tag_items": "https://storeapi.kobo.com/v1/library/tags/{TagId}/items/delete", | ||||||
|  |         "device_auth": "https://storeapi.kobo.com/v1/auth/device", | ||||||
|  |         "device_refresh": "https://storeapi.kobo.com/v1/auth/refresh", | ||||||
|  |         "dictionary_host": "https://kbdownload1-a.akamaihd.net", | ||||||
|  |         "discovery_host": "https://discovery.kobobooks.com", | ||||||
|  |         "eula_page": "https://www.kobo.com/termsofuse?style=onestore", | ||||||
|  |         "exchange_auth": "https://storeapi.kobo.com/v1/auth/exchange", | ||||||
|  |         "external_book": "https://storeapi.kobo.com/v1/products/books/external/{Ids}", | ||||||
|  |         "facebook_sso_page": "https://authorize.kobo.com/signin/provider/Facebook/login?returnUrl=http://store.kobobooks.com/", | ||||||
|  |         "featured_list": "https://storeapi.kobo.com/v1/products/featured/{FeaturedListId}", | ||||||
|  |         "featured_lists": "https://storeapi.kobo.com/v1/products/featured", | ||||||
|  |         "free_books_page": { | ||||||
|  |             "EN": "https://www.kobo.com/{region}/{language}/p/free-ebooks", | ||||||
|  |             "FR": "https://www.kobo.com/{region}/{language}/p/livres-gratuits", | ||||||
|  |             "IT": "https://www.kobo.com/{region}/{language}/p/libri-gratuiti", | ||||||
|  |             "NL": "https://www.kobo.com/{region}/{language}/List/bekijk-het-overzicht-van-gratis-ebooks/QpkkVWnUw8sxmgjSlCbJRg", | ||||||
|  |             "PT": "https://www.kobo.com/{region}/{language}/p/livros-gratis", | ||||||
|  |         }, | ||||||
|  |         "fte_feedback": "https://storeapi.kobo.com/v1/products/ftefeedback", | ||||||
|  |         "get_tests_request": "https://storeapi.kobo.com/v1/analytics/gettests", | ||||||
|  |         "giftcard_epd_redeem_url": "https://www.kobo.com/{storefront}/{language}/redeem-ereader", | ||||||
|  |         "giftcard_redeem_url": "https://www.kobo.com/{storefront}/{language}/redeem", | ||||||
|  |         "help_page": "http://www.kobo.com/help", | ||||||
|  |         "image_host": calibre_web_url, | ||||||
|  |         "image_url_quality_template": unquote(calibre_web_url + url_for("kobo.HandleCoverImageRequest", | ||||||
|  |                 auth_token = kobo_auth.get_auth_token(), | ||||||
|  |                 book_uuid="{ImageId}")), | ||||||
|  |         "image_url_template":  unquote(calibre_web_url + url_for("kobo.HandleCoverImageRequest", | ||||||
|  |                 auth_token = kobo_auth.get_auth_token(), | ||||||
|  |                 book_uuid="{ImageId}")), | ||||||
|  |         "kobo_audiobooks_enabled": "False", | ||||||
|  |         "kobo_audiobooks_orange_deal_enabled": "False", | ||||||
|  |         "kobo_audiobooks_subscriptions_enabled": "False", | ||||||
|  |         "kobo_nativeborrow_enabled": "True", | ||||||
|  |         "kobo_onestorelibrary_enabled": "False", | ||||||
|  |         "kobo_redeem_enabled": "True", | ||||||
|  |         "kobo_shelfie_enabled": "False", | ||||||
|  |         "kobo_subscriptions_enabled": "False", | ||||||
|  |         "kobo_superpoints_enabled": "False", | ||||||
|  |         "kobo_wishlist_enabled": "True", | ||||||
|  |         "library_book": "https://storeapi.kobo.com/v1/user/library/books/{LibraryItemId}", | ||||||
|  |         "library_items": "https://storeapi.kobo.com/v1/user/library", | ||||||
|  |         "library_metadata": "https://storeapi.kobo.com/v1/library/{Ids}/metadata", | ||||||
|  |         "library_prices": "https://storeapi.kobo.com/v1/user/library/previews/prices", | ||||||
|  |         "library_stack": "https://storeapi.kobo.com/v1/user/library/stacks/{LibraryItemId}", | ||||||
|  |         "library_sync": "https://storeapi.kobo.com/v1/library/sync", | ||||||
|  |         "love_dashboard_page": "https://store.kobobooks.com/{culture}/kobosuperpoints", | ||||||
|  |         "love_points_redemption_page": "https://store.kobobooks.com/{culture}/KoboSuperPointsRedemption?productId={ProductId}", | ||||||
|  |         "magazine_landing_page": "https://store.kobobooks.com/emagazines", | ||||||
|  |         "notifications_registration_issue": "https://storeapi.kobo.com/v1/notifications/registration", | ||||||
|  |         "oauth_host": "https://oauth.kobo.com", | ||||||
|  |         "overdrive_account": "https://auth.overdrive.com/account", | ||||||
|  |         "overdrive_library": "https://{libraryKey}.auth.overdrive.com/library", | ||||||
|  |         "overdrive_library_finder_host": "https://libraryfinder.api.overdrive.com", | ||||||
|  |         "overdrive_thunder_host": "https://thunder.api.overdrive.com", | ||||||
|  |         "password_retrieval_page": "https://www.kobobooks.com/passwordretrieval.html", | ||||||
|  |         "post_analytics_event": "https://storeapi.kobo.com/v1/analytics/event", | ||||||
|  |         "privacy_page": "https://www.kobo.com/privacypolicy?style=onestore", | ||||||
|  |         "product_nextread": "https://storeapi.kobo.com/v1/products/{ProductIds}/nextread", | ||||||
|  |         "product_prices": "https://storeapi.kobo.com/v1/products/{ProductIds}/prices", | ||||||
|  |         "product_recommendations": "https://storeapi.kobo.com/v1/products/{ProductId}/recommendations", | ||||||
|  |         "product_reviews": "https://storeapi.kobo.com/v1/products/{ProductIds}/reviews", | ||||||
|  |         "products": "https://storeapi.kobo.com/v1/products", | ||||||
|  |         "provider_external_sign_in_page": "https://authorize.kobo.com/ExternalSignIn/{providerName}?returnUrl=http://store.kobobooks.com/", | ||||||
|  |         "purchase_buy": "https://www.kobo.com/checkout/createpurchase/", | ||||||
|  |         "purchase_buy_templated": "https://www.kobo.com/{culture}/checkout/createpurchase/{ProductId}", | ||||||
|  |         "quickbuy_checkout": "https://storeapi.kobo.com/v1/store/quickbuy/{PurchaseId}/checkout", | ||||||
|  |         "quickbuy_create": "https://storeapi.kobo.com/v1/store/quickbuy/purchase", | ||||||
|  |         "rating": "https://storeapi.kobo.com/v1/products/{ProductId}/rating/{Rating}", | ||||||
|  |         "reading_state": "https://storeapi.kobo.com/v1/library/{Ids}/state", | ||||||
|  |         "redeem_interstitial_page": "https://store.kobobooks.com", | ||||||
|  |         "registration_page": "https://authorize.kobo.com/signup?returnUrl=http://store.kobobooks.com/", | ||||||
|  |         "related_items": "https://storeapi.kobo.com/v1/products/{Id}/related", | ||||||
|  |         "remaining_book_series": "https://storeapi.kobo.com/v1/products/books/series/{SeriesId}", | ||||||
|  |         "rename_tag": "https://storeapi.kobo.com/v1/library/tags/{TagId}", | ||||||
|  |         "review": "https://storeapi.kobo.com/v1/products/reviews/{ReviewId}", | ||||||
|  |         "review_sentiment": "https://storeapi.kobo.com/v1/products/reviews/{ReviewId}/sentiment/{Sentiment}", | ||||||
|  |         "shelfie_recommendations": "https://storeapi.kobo.com/v1/user/recommendations/shelfie", | ||||||
|  |         "sign_in_page": "https://authorize.kobo.com/signin?returnUrl=http://store.kobobooks.com/", | ||||||
|  |         "social_authorization_host": "https://social.kobobooks.com:8443", | ||||||
|  |         "social_host": "https://social.kobobooks.com", | ||||||
|  |         "stacks_host_productId": "https://store.kobobooks.com/collections/byproductid/", | ||||||
|  |         "store_home": "www.kobo.com/{region}/{language}", | ||||||
|  |         "store_host": "store.kobobooks.com", | ||||||
|  |         "store_newreleases": "https://store.kobobooks.com/{culture}/List/new-releases/961XUjtsU0qxkFItWOutGA", | ||||||
|  |         "store_search": "https://store.kobobooks.com/{culture}/Search?Query={query}", | ||||||
|  |         "store_top50": "https://store.kobobooks.com/{culture}/ebooks/Top", | ||||||
|  |         "tag_items": "https://storeapi.kobo.com/v1/library/tags/{TagId}/Items", | ||||||
|  |         "tags": "https://storeapi.kobo.com/v1/library/tags", | ||||||
|  |         "taste_profile": "https://storeapi.kobo.com/v1/products/tasteprofile", | ||||||
|  |         "update_accessibility_to_preview": "https://storeapi.kobo.com/v1/library/{EntitlementIds}/preview", | ||||||
|  |         "use_one_store": "False", | ||||||
|  |         "user_loyalty_benefits": "https://storeapi.kobo.com/v1/user/loyalty/benefits", | ||||||
|  |         "user_platform": "https://storeapi.kobo.com/v1/user/platform", | ||||||
|  |         "user_profile": "https://storeapi.kobo.com/v1/user/profile", | ||||||
|  |         "user_ratings": "https://storeapi.kobo.com/v1/user/ratings", | ||||||
|  |         "user_recommendations": "https://storeapi.kobo.com/v1/user/recommendations", | ||||||
|  |         "user_reviews": "https://storeapi.kobo.com/v1/user/reviews", | ||||||
|  |         "user_wishlist": "https://storeapi.kobo.com/v1/user/wishlist", | ||||||
|  |         "userguide_host": "https://kbdownload1-a.akamaihd.net", | ||||||
|  |         "wishlist_page": "https://store.kobobooks.com/{region}/{language}/account/wishlist", | ||||||
|  |     } | ||||||
							
								
								
									
										165
									
								
								cps/kobo_auth.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										165
									
								
								cps/kobo_auth.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,165 @@ | ||||||
|  | #!/usr/bin/env python | ||||||
|  | # -*- coding: utf-8 -*- | ||||||
|  | 
 | ||||||
|  | #  This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) | ||||||
|  | #    Copyright (C) 2018-2019 shavitmichael, 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 <http://www.gnu.org/licenses/>. | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | """This module is used to control authentication/authorization of Kobo sync requests. | ||||||
|  | This module also includes research notes into the auth protocol used by Kobo devices. | ||||||
|  | 
 | ||||||
|  | Log-in: | ||||||
|  | When first booting a Kobo device the user must sign into a Kobo (or affiliate) account. | ||||||
|  | Upon successful sign-in, the user is redirected to | ||||||
|  |     https://auth.kobobooks.com/CrossDomainSignIn?id=<some id> | ||||||
|  | which serves the following response: | ||||||
|  |     <script type='text/javascript'> | ||||||
|  |         location.href='kobo://UserAuthenticated?userId=<redacted>&userKey<redacted>&email=<redacted>&returnUrl=https%3a%2f%2fwww.kobo.com'; | ||||||
|  |     </script> | ||||||
|  | And triggers the insertion of a userKey into the device's User table. | ||||||
|  | 
 | ||||||
|  | Together, the device's DeviceId and UserKey act as an *irrevocable* authentication | ||||||
|  | token to most (if not all) Kobo APIs. In fact, in most cases only the UserKey is | ||||||
|  | required to authorize the API call. | ||||||
|  | 
 | ||||||
|  | Changing Kobo password *does not* invalidate user keys! This is apparently a known | ||||||
|  | issue for a few years now https://www.mobileread.com/forums/showpost.php?p=3476851&postcount=13 | ||||||
|  | (although this poster hypothesised that Kobo could blacklist a DeviceId, many endpoints | ||||||
|  | will still grant access given the userkey.) | ||||||
|  | 
 | ||||||
|  | Official Kobo Store Api authorization: | ||||||
|  | * For most of the endpoints we care about (sync, metadata, tags, etc), the userKey is | ||||||
|  | passed in the x-kobo-userkey header, and is sufficient to authorize the API call. | ||||||
|  | * Some endpoints (e.g: AnnotationService) instead make use of Bearer tokens pass through | ||||||
|  | an authorization header. To get a BearerToken, the device makes a POST request to the | ||||||
|  | v1/auth/device endpoint with the secret UserKey and the device's DeviceId. | ||||||
|  | * The book download endpoint passes an auth token as a URL param instead of a header. | ||||||
|  | 
 | ||||||
|  | Our implementation: | ||||||
|  | We pretty much ignore all of the above. To authenticate the user, we generate a random | ||||||
|  | and unique token that they append to the CalibreWeb Url when setting up the api_store | ||||||
|  | setting on the device. | ||||||
|  | Thus, every request from the device to the api_store will hit CalibreWeb with the | ||||||
|  | auth_token in the url (e.g: https://mylibrary.com/<auth_token>/v1/library/sync). | ||||||
|  | In addition, once authenticated we also set the login cookie on the response that will | ||||||
|  | be sent back for the duration of the session to authorize subsequent API calls (in | ||||||
|  | particular calls to non-Kobo specific endpoints such as the CalibreWeb book download). | ||||||
|  | """ | ||||||
|  | 
 | ||||||
|  | from binascii import hexlify | ||||||
|  | from datetime import datetime | ||||||
|  | from os import urandom | ||||||
|  | import os | ||||||
|  | 
 | ||||||
|  | from flask import g, Blueprint, url_for, abort, request | ||||||
|  | from flask_login import login_user, login_required | ||||||
|  | from flask_babel import gettext as _ | ||||||
|  | 
 | ||||||
|  | from . import logger, ub, lm | ||||||
|  | from .web import render_title_template | ||||||
|  | 
 | ||||||
|  | try: | ||||||
|  |     from functools import wraps | ||||||
|  | except ImportError: | ||||||
|  |     pass  # We're not using Python 3 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | log = logger.create() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def register_url_value_preprocessor(kobo): | ||||||
|  |     @kobo.url_value_preprocessor | ||||||
|  |     def pop_auth_token(endpoint, values): | ||||||
|  |         g.auth_token = values.pop("auth_token") | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def disable_failed_auth_redirect_for_blueprint(bp): | ||||||
|  |     lm.blueprint_login_views[bp.name] = None | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def get_auth_token(): | ||||||
|  |     if "auth_token" in g: | ||||||
|  |         return g.get("auth_token") | ||||||
|  |     else: | ||||||
|  |         return None | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def requires_kobo_auth(f): | ||||||
|  |     @wraps(f) | ||||||
|  |     def inner(*args, **kwargs): | ||||||
|  |         auth_token = get_auth_token() | ||||||
|  |         if auth_token is not None: | ||||||
|  |             user = ( | ||||||
|  |                 ub.session.query(ub.User) | ||||||
|  |                 .join(ub.RemoteAuthToken) | ||||||
|  |                 .filter(ub.RemoteAuthToken.auth_token == auth_token).filter(ub.RemoteAuthToken.token_type==1) | ||||||
|  |                 .first() | ||||||
|  |             ) | ||||||
|  |             if user is not None: | ||||||
|  |                 login_user(user) | ||||||
|  |                 return f(*args, **kwargs) | ||||||
|  |             log.debug("Received Kobo request without a recognizable auth token.") | ||||||
|  |             return abort(401) | ||||||
|  |     return inner | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | kobo_auth = Blueprint("kobo_auth", __name__, url_prefix="/kobo_auth") | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @kobo_auth.route("/generate_auth_token/<int:user_id>") | ||||||
|  | @login_required | ||||||
|  | def generate_auth_token(user_id): | ||||||
|  |     host = ':'.join(request.host.rsplit(':')[0:-1]) | ||||||
|  |     if host.startswith('127.') or host.lower() == 'localhost' or host.startswith('[::ffff:7f'): | ||||||
|  |         warning = _('PLease access calibre-web from non localhost to get valid api_endpoint for kobo device') | ||||||
|  |         return render_title_template( | ||||||
|  |             "generate_kobo_auth_url.html", | ||||||
|  |             title=_(u"Kobo Setup"), | ||||||
|  |             warning = warning | ||||||
|  |         ) | ||||||
|  |     else: | ||||||
|  |         # Invalidate any prevously generated Kobo Auth token for this user. | ||||||
|  |         auth_token = ub.session.query(ub.RemoteAuthToken).filter( | ||||||
|  |             ub.RemoteAuthToken.user_id == user_id | ||||||
|  |         ).filter(ub.RemoteAuthToken.token_type==1).first() | ||||||
|  | 
 | ||||||
|  |         if not auth_token: | ||||||
|  |             auth_token = ub.RemoteAuthToken() | ||||||
|  |             auth_token.user_id = user_id | ||||||
|  |             auth_token.expiration = datetime.max | ||||||
|  |             auth_token.auth_token = (hexlify(urandom(16))).decode("utf-8") | ||||||
|  |             auth_token.token_type = 1 | ||||||
|  | 
 | ||||||
|  |             ub.session.add(auth_token) | ||||||
|  |             ub.session.commit() | ||||||
|  |         return render_title_template( | ||||||
|  |             "generate_kobo_auth_url.html", | ||||||
|  |             title=_(u"Kobo Setup"), | ||||||
|  |             kobo_auth_url=url_for( | ||||||
|  |                 "kobo.TopLevelEndpoint", auth_token=auth_token.auth_token, _external=True | ||||||
|  |             ), | ||||||
|  |             warning = False | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @kobo_auth.route("/deleteauthtoken/<int:user_id>") | ||||||
|  | @login_required | ||||||
|  | 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 "" | ||||||
|  | @ -1,4 +1,3 @@ | ||||||
| #!/usr/bin/env python |  | ||||||
| # -*- coding: utf-8 -*- | # -*- coding: utf-8 -*- | ||||||
| 
 | 
 | ||||||
| #  This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) | #  This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) | ||||||
|  |  | ||||||
|  | @ -1,4 +1,3 @@ | ||||||
| #!/usr/bin/env python |  | ||||||
| # -*- coding: utf-8 -*- | # -*- coding: utf-8 -*- | ||||||
| 
 | 
 | ||||||
| #  This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) | #  This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) | ||||||
|  | @ -50,7 +49,7 @@ def oauth_required(f): | ||||||
|     def inner(*args, **kwargs): |     def inner(*args, **kwargs): | ||||||
|         if config.config_login_type == constants.LOGIN_OAUTH: |         if config.config_login_type == constants.LOGIN_OAUTH: | ||||||
|             return f(*args, **kwargs) |             return f(*args, **kwargs) | ||||||
|         if request.is_xhr: |         if request.headers.get('X-Requested-With') == 'XMLHttpRequest': | ||||||
|             data = {'status': 'error', 'message': 'Not Found'} |             data = {'status': 'error', 'message': 'Not Found'} | ||||||
|             response = make_response(json.dumps(data, ensure_ascii=False)) |             response = make_response(json.dumps(data, ensure_ascii=False)) | ||||||
|             response.headers["Content-Type"] = "application/json; charset=utf-8" |             response.headers["Content-Type"] = "application/json; charset=utf-8" | ||||||
|  |  | ||||||
							
								
								
									
										60
									
								
								cps/opds.py
									
									
									
									
									
								
							
							
						
						
									
										60
									
								
								cps/opds.py
									
									
									
									
									
								
							|  | @ -1,4 +1,3 @@ | ||||||
| #!/usr/bin/env python |  | ||||||
| # -*- coding: utf-8 -*- | # -*- coding: utf-8 -*- | ||||||
| 
 | 
 | ||||||
| #  This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) | #  This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) | ||||||
|  | @ -57,6 +56,20 @@ def requires_basic_auth_if_no_ano(f): | ||||||
|     return decorated |     return decorated | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | class FeedObject(): | ||||||
|  |     def __init__(self,rating_id , rating_name): | ||||||
|  |         self.rating_id = rating_id | ||||||
|  |         self.rating_name = rating_name | ||||||
|  | 
 | ||||||
|  |     @property | ||||||
|  |     def id(self): | ||||||
|  |         return self.rating_id | ||||||
|  | 
 | ||||||
|  |     @property | ||||||
|  |     def name(self): | ||||||
|  |         return self.rating_name | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| @opds.route("/opds/") | @opds.route("/opds/") | ||||||
| @opds.route("/opds") | @opds.route("/opds") | ||||||
| @requires_basic_auth_if_no_ano | @requires_basic_auth_if_no_ano | ||||||
|  | @ -215,6 +228,31 @@ def feed_series(book_id): | ||||||
|                     db.Books, db.Books.series.any(db.Series.id == book_id), [db.Books.series_index]) |                     db.Books, db.Books.series.any(db.Series.id == book_id), [db.Books.series_index]) | ||||||
|     return render_xml_template('feed.xml', entries=entries, pagination=pagination) |     return render_xml_template('feed.xml', entries=entries, pagination=pagination) | ||||||
| 
 | 
 | ||||||
|  | @opds.route("/opds/ratings") | ||||||
|  | @requires_basic_auth_if_no_ano | ||||||
|  | def feed_ratingindex(): | ||||||
|  |     off = request.args.get("offset") or 0 | ||||||
|  |     entries = db.session.query(db.Ratings, func.count('books_ratings_link.book').label('count'), | ||||||
|  |                                (db.Ratings.rating / 2).label('name')) \ | ||||||
|  |         .join(db.books_ratings_link).join(db.Books).filter(common_filters()) \ | ||||||
|  |         .group_by(text('books_ratings_link.rating')).order_by(db.Ratings.rating).all() | ||||||
|  | 
 | ||||||
|  |     pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, | ||||||
|  |                             len(entries)) | ||||||
|  |     element = list() | ||||||
|  |     for entry in entries: | ||||||
|  |         element.append(FeedObject(entry[0].id, "{} Stars".format(entry.name))) | ||||||
|  |     return render_xml_template('feed.xml', listelements=element, folder='opds.feed_ratings', pagination=pagination) | ||||||
|  | 
 | ||||||
|  | @opds.route("/opds/ratings/<book_id>") | ||||||
|  | @requires_basic_auth_if_no_ano | ||||||
|  | def feed_ratings(book_id): | ||||||
|  |     off = request.args.get("offset") or 0 | ||||||
|  |     entries, __, pagination = fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), | ||||||
|  |                     db.Books, db.Books.ratings.any(db.Ratings.id == book_id),[db.Books.timestamp.desc()]) | ||||||
|  |     return render_xml_template('feed.xml', entries=entries, pagination=pagination) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| @opds.route("/opds/formats") | @opds.route("/opds/formats") | ||||||
| @requires_basic_auth_if_no_ano | @requires_basic_auth_if_no_ano | ||||||
| def feed_formatindex(): | def feed_formatindex(): | ||||||
|  | @ -223,10 +261,11 @@ def feed_formatindex(): | ||||||
|         .group_by(db.Data.format).order_by(db.Data.format).all() |         .group_by(db.Data.format).order_by(db.Data.format).all() | ||||||
|     pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, |     pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, | ||||||
|                             len(entries)) |                             len(entries)) | ||||||
|  | 
 | ||||||
|  |     element = list() | ||||||
|     for entry in entries: |     for entry in entries: | ||||||
|         entry.name = entry.format |         element.append(FeedObject(entry.format, entry.format)) | ||||||
|         entry.id = entry.format |     return render_xml_template('feed.xml', listelements=element, folder='opds.feed_format', pagination=pagination) | ||||||
|     return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_format', pagination=pagination) |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @opds.route("/opds/formats/<book_id>") | @opds.route("/opds/formats/<book_id>") | ||||||
|  | @ -266,16 +305,9 @@ def feed_languages(book_id): | ||||||
|     off = request.args.get("offset") or 0 |     off = request.args.get("offset") or 0 | ||||||
|     entries, __, pagination = fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), |     entries, __, pagination = fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), | ||||||
|                     db.Books, db.Books.languages.any(db.Languages.id == book_id), [db.Books.timestamp.desc()]) |                     db.Books, db.Books.languages.any(db.Languages.id == book_id), [db.Books.timestamp.desc()]) | ||||||
|     '''for entry in entries: |  | ||||||
|         for index in range(0, len(entry.languages)): |  | ||||||
|             try: |  | ||||||
|                 entry.languages[index].language_name = LC.parse(entry.languages[index].lang_code).get_language_name( |  | ||||||
|                     get_locale()) |  | ||||||
|             except UnknownLocaleError: |  | ||||||
|                 entry.languages[index].language_name = _( |  | ||||||
|                     isoLanguages.get(part3=entry.languages[index].lang_code).name)''' |  | ||||||
|     return render_xml_template('feed.xml', entries=entries, pagination=pagination) |     return render_xml_template('feed.xml', entries=entries, pagination=pagination) | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| @opds.route("/opds/shelfindex", defaults={'public': 0}) | @opds.route("/opds/shelfindex", defaults={'public': 0}) | ||||||
| @opds.route("/opds/shelfindex/<string:public>") | @opds.route("/opds/shelfindex/<string:public>") | ||||||
| @requires_basic_auth_if_no_ano | @requires_basic_auth_if_no_ano | ||||||
|  | @ -320,11 +352,11 @@ def feed_shelf(book_id): | ||||||
| @requires_basic_auth_if_no_ano | @requires_basic_auth_if_no_ano | ||||||
| @download_required | @download_required | ||||||
| def opds_download_link(book_id, book_format): | def opds_download_link(book_id, book_format): | ||||||
|     return get_download_link(book_id,book_format) |     return get_download_link(book_id,book_format.lower()) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @opds.route("/ajax/book/<string:uuid>/<library>") | @opds.route("/ajax/book/<string:uuid>/<library>") | ||||||
| @opds.route("/ajax/book/<string:uuid>") | @opds.route("/ajax/book/<string:uuid>",defaults={'library': ""}) | ||||||
| @requires_basic_auth_if_no_ano | @requires_basic_auth_if_no_ano | ||||||
| def get_metadata_calibre_companion(uuid, library): | def get_metadata_calibre_companion(uuid, library): | ||||||
|     entry = db.session.query(db.Books).filter(db.Books.uuid.like("%" + uuid + "%")).first() |     entry = db.session.query(db.Books).filter(db.Books.uuid.like("%" + uuid + "%")).first() | ||||||
|  |  | ||||||
|  | @ -1,4 +1,3 @@ | ||||||
| #!/usr/bin/env python |  | ||||||
| # -*- coding: utf-8 -*- | # -*- coding: utf-8 -*- | ||||||
| 
 | 
 | ||||||
| #  This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) | #  This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) | ||||||
|  |  | ||||||
|  | @ -1,4 +1,3 @@ | ||||||
| #!/usr/bin/env python |  | ||||||
| # -*- coding: utf-8 -*- | # -*- coding: utf-8 -*- | ||||||
| 
 | 
 | ||||||
| # Flask License | # Flask License | ||||||
|  |  | ||||||
|  | @ -1,4 +1,3 @@ | ||||||
| #!/usr/bin/env python |  | ||||||
| # -*- coding: utf-8 -*- | # -*- coding: utf-8 -*- | ||||||
| 
 | 
 | ||||||
| #  Flask License | #  Flask License | ||||||
|  | @ -60,10 +59,13 @@ class ReverseProxied(object): | ||||||
| 
 | 
 | ||||||
|     def __init__(self, application): |     def __init__(self, application): | ||||||
|         self.app = application |         self.app = application | ||||||
|  |         self.proxied = False | ||||||
| 
 | 
 | ||||||
|     def __call__(self, environ, start_response): |     def __call__(self, environ, start_response): | ||||||
|  |         self.proxied = False | ||||||
|         script_name = environ.get('HTTP_X_SCRIPT_NAME', '') |         script_name = environ.get('HTTP_X_SCRIPT_NAME', '') | ||||||
|         if script_name: |         if script_name: | ||||||
|  |             self.proxied = True | ||||||
|             environ['SCRIPT_NAME'] = script_name |             environ['SCRIPT_NAME'] = script_name | ||||||
|             path_info = environ.get('PATH_INFO', '') |             path_info = environ.get('PATH_INFO', '') | ||||||
|             if path_info and path_info.startswith(script_name): |             if path_info and path_info.startswith(script_name): | ||||||
|  | @ -76,3 +78,7 @@ class ReverseProxied(object): | ||||||
|         if servr: |         if servr: | ||||||
|             environ['HTTP_HOST'] = servr |             environ['HTTP_HOST'] = servr | ||||||
|         return self.app(environ, start_response) |         return self.app(environ, start_response) | ||||||
|  | 
 | ||||||
|  |     @property | ||||||
|  |     def is_proxied(self): | ||||||
|  |         return self.proxied | ||||||
|  |  | ||||||
|  | @ -146,7 +146,7 @@ class WebServer(object): | ||||||
|                 self.unix_socket_file = None |                 self.unix_socket_file = None | ||||||
| 
 | 
 | ||||||
|     def _start_tornado(self): |     def _start_tornado(self): | ||||||
|         if os.name == 'nt': |         if os.name == 'nt' and sys.version_info > (3, 7): | ||||||
|             import asyncio |             import asyncio | ||||||
|             asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) |             asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) | ||||||
|         log.info('Starting Tornado server on %s', _readable_listen_address(self.listen_address, self.listen_port)) |         log.info('Starting Tornado server on %s', _readable_listen_address(self.listen_address, self.listen_port)) | ||||||
|  | @ -156,7 +156,7 @@ class WebServer(object): | ||||||
|                                  max_buffer_size=209700000, |                                  max_buffer_size=209700000, | ||||||
|                                  ssl_options=self.ssl_args) |                                  ssl_options=self.ssl_args) | ||||||
|         http_server.listen(self.listen_port, self.listen_address) |         http_server.listen(self.listen_port, self.listen_address) | ||||||
|         self.wsgiserver = IOLoop.instance() |         self.wsgiserver = IOLoop.current() | ||||||
|         self.wsgiserver.start() |         self.wsgiserver.start() | ||||||
|         # wait for stop signal |         # wait for stop signal | ||||||
|         self.wsgiserver.close(True) |         self.wsgiserver.close(True) | ||||||
|  | @ -177,6 +177,8 @@ class WebServer(object): | ||||||
| 
 | 
 | ||||||
|         if not self.restart: |         if not self.restart: | ||||||
|             log.info("Performing shutdown of Calibre-Web") |             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 |             return True | ||||||
| 
 | 
 | ||||||
|         log.info("Performing restart of Calibre-Web") |         log.info("Performing restart of Calibre-Web") | ||||||
|  | @ -197,4 +199,4 @@ class WebServer(object): | ||||||
|             if _GEVENT: |             if _GEVENT: | ||||||
|                 self.wsgiserver.close() |                 self.wsgiserver.close() | ||||||
|             else: |             else: | ||||||
|                 self.wsgiserver.add_callback(self.wsgiserver.stop) |                 self.wsgiserver.add_callback_from_signal(self.wsgiserver.stop) | ||||||
|  |  | ||||||
							
								
								
									
										148
									
								
								cps/services/SyncToken.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										148
									
								
								cps/services/SyncToken.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,148 @@ | ||||||
|  | #!/usr/bin/env python | ||||||
|  | # -*- coding: utf-8 -*- | ||||||
|  | 
 | ||||||
|  | #  This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) | ||||||
|  | #    Copyright (C) 2018-2019 shavitmichael, 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 <http://www.gnu.org/licenses/>. | ||||||
|  | 
 | ||||||
|  | import sys | ||||||
|  | from base64 import b64decode, b64encode | ||||||
|  | from jsonschema import validate, exceptions, __version__ | ||||||
|  | from datetime import datetime | ||||||
|  | try: | ||||||
|  |     from urllib import unquote | ||||||
|  | except ImportError: | ||||||
|  |     from urllib.parse import unquote | ||||||
|  | 
 | ||||||
|  | from flask import json | ||||||
|  | from .. import logger as log | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def b64encode_json(json_data): | ||||||
|  |     if sys.version_info < (3, 0): | ||||||
|  |         return b64encode(json.dumps(json_data)) | ||||||
|  |     else: | ||||||
|  |         return b64encode(json.dumps(json_data).encode()) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # Python3 has a timestamp() method we could be calling, however it's not avaiable in python2. | ||||||
|  | def to_epoch_timestamp(datetime_object): | ||||||
|  |     return (datetime_object - datetime(1970, 1, 1)).total_seconds() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class SyncToken(): | ||||||
|  |     """ The SyncToken is used to persist state accross requests. | ||||||
|  |     When serialized over the response headers, the Kobo device will propagate the token onto following requests to the service. | ||||||
|  |     As an example use-case, the SyncToken is used to detect books that have been added to the library since the last time the device synced to the server. | ||||||
|  | 
 | ||||||
|  |     Attributes: | ||||||
|  |         books_last_created: Datetime representing the newest book that the device knows about. | ||||||
|  |         books_last_modified: Datetime representing the last modified book that the device knows about. | ||||||
|  |     """ | ||||||
|  | 
 | ||||||
|  |     SYNC_TOKEN_HEADER = "x-kobo-synctoken" | ||||||
|  |     VERSION = "1-0-0" | ||||||
|  |     MIN_VERSION = "1-0-0" | ||||||
|  | 
 | ||||||
|  |     token_schema = { | ||||||
|  |         "type": "object", | ||||||
|  |         "properties": {"version": {"type": "string"}, "data": {"type": "object"},}, | ||||||
|  |     } | ||||||
|  |     # This Schema doesn't contain enough information to detect and propagate book deletions from Calibre to the device. | ||||||
|  |     # A potential solution might be to keep a list of all known book uuids in the token, and look for any missing from the db. | ||||||
|  |     data_schema_v1 = { | ||||||
|  |         "type": "object", | ||||||
|  |         "properties": { | ||||||
|  |             "raw_kobo_store_token": {"type": "string"}, | ||||||
|  |             "books_last_modified": {"type": "string"}, | ||||||
|  |             "books_last_created": {"type": "string"}, | ||||||
|  |         }, | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     def __init__( | ||||||
|  |         self, | ||||||
|  |         raw_kobo_store_token="", | ||||||
|  |         books_last_created=datetime.min, | ||||||
|  |         books_last_modified=datetime.min, | ||||||
|  |     ): | ||||||
|  |         self.raw_kobo_store_token = raw_kobo_store_token | ||||||
|  |         self.books_last_created = books_last_created | ||||||
|  |         self.books_last_modified = books_last_modified | ||||||
|  | 
 | ||||||
|  |     @staticmethod | ||||||
|  |     def from_headers(headers): | ||||||
|  |         sync_token_header = headers.get(SyncToken.SYNC_TOKEN_HEADER, "") | ||||||
|  |         if sync_token_header == "": | ||||||
|  |             return SyncToken() | ||||||
|  | 
 | ||||||
|  |         # On the first sync from a Kobo device, we may receive the SyncToken | ||||||
|  |         # from the official Kobo store. Without digging too deep into it, that | ||||||
|  |         # token is of the form [b64encoded blob].[b64encoded blob 2] | ||||||
|  |         if "." in sync_token_header: | ||||||
|  |             return SyncToken(raw_kobo_store_token=sync_token_header) | ||||||
|  | 
 | ||||||
|  |         try: | ||||||
|  |             sync_token_json = json.loads( | ||||||
|  |                 b64decode(sync_token_header + "=" * (-len(sync_token_header) % 4)) | ||||||
|  |             ) | ||||||
|  |             validate(sync_token_json, SyncToken.token_schema) | ||||||
|  |             if sync_token_json["version"] < SyncToken.MIN_VERSION: | ||||||
|  |                 raise ValueError | ||||||
|  | 
 | ||||||
|  |             data_json = sync_token_json["data"] | ||||||
|  |             validate(sync_token_json, SyncToken.data_schema_v1) | ||||||
|  |         except (exceptions.ValidationError, ValueError): | ||||||
|  |             log.error("Sync token contents do not follow the expected json schema.") | ||||||
|  |             return SyncToken() | ||||||
|  | 
 | ||||||
|  |         raw_kobo_store_token = data_json["raw_kobo_store_token"] | ||||||
|  |         try: | ||||||
|  |             books_last_modified = datetime.utcfromtimestamp( | ||||||
|  |                 data_json["books_last_modified"] | ||||||
|  |             ) | ||||||
|  |             books_last_created = datetime.utcfromtimestamp( | ||||||
|  |                 data_json["books_last_created"] | ||||||
|  |             ) | ||||||
|  |         except TypeError: | ||||||
|  |             log.error("SyncToken timestamps don't parse to a datetime.") | ||||||
|  |             return SyncToken(raw_kobo_store_token=raw_kobo_store_token) | ||||||
|  | 
 | ||||||
|  |         return SyncToken( | ||||||
|  |             raw_kobo_store_token=raw_kobo_store_token, | ||||||
|  |             books_last_created=books_last_created, | ||||||
|  |             books_last_modified=books_last_modified, | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |     def set_kobo_store_header(self, store_headers): | ||||||
|  |         store_headers.set(SyncToken.SYNC_TOKEN_HEADER, self.raw_kobo_store_token) | ||||||
|  | 
 | ||||||
|  |     def merge_from_store_response(self, store_response): | ||||||
|  |         self.raw_kobo_store_token = store_response.headers.get( | ||||||
|  |             SyncToken.SYNC_TOKEN_HEADER, "" | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |     def to_headers(self, headers): | ||||||
|  |         headers[SyncToken.SYNC_TOKEN_HEADER] = self.build_sync_token() | ||||||
|  | 
 | ||||||
|  |     def build_sync_token(self): | ||||||
|  |         token = { | ||||||
|  |             "version": SyncToken.VERSION, | ||||||
|  |             "data": { | ||||||
|  |                 "raw_kobo_store_token": self.raw_kobo_store_token, | ||||||
|  |                 "books_last_modified": to_epoch_timestamp(self.books_last_modified), | ||||||
|  |                 "books_last_created": to_epoch_timestamp(self.books_last_created), | ||||||
|  |             }, | ||||||
|  |         } | ||||||
|  |         return b64encode_json(token) | ||||||
|  | @ -35,4 +35,10 @@ except ImportError as err: | ||||||
|     log.debug("cannot import simpleldap, logging in with ldap will not work: %s", err) |     log.debug("cannot import simpleldap, logging in with ldap will not work: %s", err) | ||||||
|     ldap = None |     ldap = None | ||||||
| 
 | 
 | ||||||
| 
 | try: | ||||||
|  |     from . import SyncToken as SyncToken | ||||||
|  |     kobo = True | ||||||
|  | except ImportError as err: | ||||||
|  |     log.debug("cannot import SyncToken, syncing books with Kobo Devices will not work: %s", err) | ||||||
|  |     kobo = None | ||||||
|  |     SyncToken = None | ||||||
|  |  | ||||||
							
								
								
									
										54
									
								
								cps/shelf.py
									
									
									
									
									
								
							
							
						
						
									
										54
									
								
								cps/shelf.py
									
									
									
									
									
								
							|  | @ -1,4 +1,3 @@ | ||||||
| #!/usr/bin/env python |  | ||||||
| # -*- coding: utf-8 -*- | # -*- coding: utf-8 -*- | ||||||
| 
 | 
 | ||||||
| #  This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) | #  This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) | ||||||
|  | @ -40,17 +39,18 @@ log = logger.create() | ||||||
| @shelf.route("/shelf/add/<int:shelf_id>/<int:book_id>") | @shelf.route("/shelf/add/<int:shelf_id>/<int:book_id>") | ||||||
| @login_required | @login_required | ||||||
| def add_to_shelf(shelf_id, book_id): | def add_to_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() |     shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first() | ||||||
|     if shelf is None: |     if shelf is None: | ||||||
|         log.error("Invalid shelf specified: %s", shelf_id) |         log.error("Invalid shelf specified: %s", shelf_id) | ||||||
|         if not request.is_xhr: |         if not xhr: | ||||||
|             flash(_(u"Invalid shelf specified"), category="error") |             flash(_(u"Invalid shelf specified"), category="error") | ||||||
|             return redirect(url_for('web.index')) |             return redirect(url_for('web.index')) | ||||||
|         return "Invalid shelf specified", 400 |         return "Invalid shelf specified", 400 | ||||||
| 
 | 
 | ||||||
|     if not shelf.is_public and not shelf.user_id == int(current_user.id): |     if not shelf.is_public and not shelf.user_id == int(current_user.id): | ||||||
|         log.error("User %s not allowed to add a book to %s", current_user, shelf) |         log.error("User %s not allowed to add a book to %s", current_user, shelf) | ||||||
|         if not request.is_xhr: |         if not xhr: | ||||||
|             flash(_(u"Sorry you are not allowed to add a book to the the shelf: %(shelfname)s", shelfname=shelf.name), |             flash(_(u"Sorry you are not allowed to add a book to the the shelf: %(shelfname)s", shelfname=shelf.name), | ||||||
|                   category="error") |                   category="error") | ||||||
|             return redirect(url_for('web.index')) |             return redirect(url_for('web.index')) | ||||||
|  | @ -58,7 +58,7 @@ def add_to_shelf(shelf_id, book_id): | ||||||
| 
 | 
 | ||||||
|     if shelf.is_public and not current_user.role_edit_shelfs(): |     if shelf.is_public and not current_user.role_edit_shelfs(): | ||||||
|         log.info("User %s not allowed to edit public shelves", current_user) |         log.info("User %s not allowed to edit public shelves", current_user) | ||||||
|         if not request.is_xhr: |         if not xhr: | ||||||
|             flash(_(u"You are not allowed to edit public shelves"), category="error") |             flash(_(u"You are not allowed to edit public shelves"), category="error") | ||||||
|             return redirect(url_for('web.index')) |             return redirect(url_for('web.index')) | ||||||
|         return "User is not allowed to edit public shelves", 403 |         return "User is not allowed to edit public shelves", 403 | ||||||
|  | @ -67,7 +67,7 @@ def add_to_shelf(shelf_id, book_id): | ||||||
|                                           ub.BookShelf.book_id == book_id).first() |                                           ub.BookShelf.book_id == book_id).first() | ||||||
|     if book_in_shelf: |     if book_in_shelf: | ||||||
|         log.error("Book %s is already part of %s", book_id, shelf) |         log.error("Book %s is already part of %s", book_id, shelf) | ||||||
|         if not request.is_xhr: |         if not xhr: | ||||||
|             flash(_(u"Book is already part of the shelf: %(shelfname)s", shelfname=shelf.name), category="error") |             flash(_(u"Book is already part of the shelf: %(shelfname)s", shelfname=shelf.name), category="error") | ||||||
|             return redirect(url_for('web.index')) |             return redirect(url_for('web.index')) | ||||||
|         return "Book is already part of the shelf: %s" % shelf.name, 400 |         return "Book is already part of the shelf: %s" % shelf.name, 400 | ||||||
|  | @ -81,7 +81,7 @@ def add_to_shelf(shelf_id, book_id): | ||||||
|     ins = ub.BookShelf(shelf=shelf.id, book_id=book_id, order=maxOrder + 1) |     ins = ub.BookShelf(shelf=shelf.id, book_id=book_id, order=maxOrder + 1) | ||||||
|     ub.session.add(ins) |     ub.session.add(ins) | ||||||
|     ub.session.commit() |     ub.session.commit() | ||||||
|     if not request.is_xhr: |     if not xhr: | ||||||
|         flash(_(u"Book has been added to shelf: %(sname)s", sname=shelf.name), category="success") |         flash(_(u"Book has been added to shelf: %(sname)s", sname=shelf.name), category="success") | ||||||
|         if "HTTP_REFERER" in request.environ: |         if "HTTP_REFERER" in request.environ: | ||||||
|             return redirect(request.environ["HTTP_REFERER"]) |             return redirect(request.environ["HTTP_REFERER"]) | ||||||
|  | @ -147,10 +147,11 @@ def search_to_shelf(shelf_id): | ||||||
| @shelf.route("/shelf/remove/<int:shelf_id>/<int:book_id>") | @shelf.route("/shelf/remove/<int:shelf_id>/<int:book_id>") | ||||||
| @login_required | @login_required | ||||||
| def remove_from_shelf(shelf_id, book_id): | 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() |     shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first() | ||||||
|     if shelf is None: |     if shelf is None: | ||||||
|         log.error("Invalid shelf specified: %s", shelf_id) |         log.error("Invalid shelf specified: %s", shelf_id) | ||||||
|         if not request.is_xhr: |         if not xhr: | ||||||
|             return redirect(url_for('web.index')) |             return redirect(url_for('web.index')) | ||||||
|         return "Invalid shelf specified", 400 |         return "Invalid shelf specified", 400 | ||||||
| 
 | 
 | ||||||
|  | @ -169,20 +170,23 @@ def remove_from_shelf(shelf_id, book_id): | ||||||
| 
 | 
 | ||||||
|         if book_shelf is None: |         if book_shelf is None: | ||||||
|             log.error("Book %s already removed from %s", book_id, shelf) |             log.error("Book %s already removed from %s", book_id, shelf) | ||||||
|             if not request.is_xhr: |             if not xhr: | ||||||
|                 return redirect(url_for('web.index')) |                 return redirect(url_for('web.index')) | ||||||
|             return "Book already removed from shelf", 410 |             return "Book already removed from shelf", 410 | ||||||
| 
 | 
 | ||||||
|         ub.session.delete(book_shelf) |         ub.session.delete(book_shelf) | ||||||
|         ub.session.commit() |         ub.session.commit() | ||||||
| 
 | 
 | ||||||
|         if not request.is_xhr: |         if not xhr: | ||||||
|             flash(_(u"Book has been removed from shelf: %(sname)s", sname=shelf.name), category="success") |             flash(_(u"Book has been removed from shelf: %(sname)s", sname=shelf.name), category="success") | ||||||
|             return redirect(request.environ["HTTP_REFERER"]) |             if "HTTP_REFERER" in request.environ: | ||||||
|  |                 return redirect(request.environ["HTTP_REFERER"]) | ||||||
|  |             else: | ||||||
|  |                 return redirect(url_for('web.index')) | ||||||
|         return "", 204 |         return "", 204 | ||||||
|     else: |     else: | ||||||
|         log.error("User %s not allowed to remove a book from %s", current_user, shelf) |         log.error("User %s not allowed to remove a book from %s", current_user, shelf) | ||||||
|         if not request.is_xhr: |         if not xhr: | ||||||
|             flash(_(u"Sorry you are not allowed to remove a book from this shelf: %(sname)s", sname=shelf.name), |             flash(_(u"Sorry you are not allowed to remove a book from this shelf: %(sname)s", sname=shelf.name), | ||||||
|                   category="error") |                   category="error") | ||||||
|             return redirect(url_for('web.index')) |             return redirect(url_for('web.index')) | ||||||
|  | @ -284,8 +288,16 @@ def show_shelf(shelf_type, shelf_id): | ||||||
| 
 | 
 | ||||||
|         books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id)\ |         books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id)\ | ||||||
|             .order_by(ub.BookShelf.order.asc()).all() |             .order_by(ub.BookShelf.order.asc()).all() | ||||||
|         books_list = [ b.book_id for b in books_in_shelf] |         for book in books_in_shelf: | ||||||
|         result = db.session.query(db.Books).filter(db.Books.id.in_(books_list)).filter(common_filters()).all() |             cur_book = db.session.query(db.Books).filter(db.Books.id == book.book_id).filter(common_filters()).first() | ||||||
|  |             if cur_book: | ||||||
|  |                 result.append(cur_book) | ||||||
|  |             else: | ||||||
|  |                 cur_book = db.session.query(db.Books).filter(db.Books.id == book.book_id).first() | ||||||
|  |                 if not cur_book: | ||||||
|  |                     log.info('Not existing book %s in %s deleted', book.book_id, shelf) | ||||||
|  |                     ub.session.query(ub.BookShelf).filter(ub.BookShelf.book_id == book.book_id).delete() | ||||||
|  |                     ub.session.commit() | ||||||
|         return render_title_template(page, entries=result, title=_(u"Shelf: '%(name)s'", name=shelf.name), |         return render_title_template(page, entries=result, title=_(u"Shelf: '%(name)s'", name=shelf.name), | ||||||
|                                  shelf=shelf, page="shelf") |                                  shelf=shelf, page="shelf") | ||||||
|     else: |     else: | ||||||
|  | @ -317,8 +329,20 @@ def order_shelf(shelf_id): | ||||||
|     if shelf: |     if shelf: | ||||||
|         books_in_shelf2 = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id) \ |         books_in_shelf2 = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id) \ | ||||||
|             .order_by(ub.BookShelf.order.asc()).all() |             .order_by(ub.BookShelf.order.asc()).all() | ||||||
|         books_list = [ b.book_id for b in books_in_shelf2] |         for book in books_in_shelf2: | ||||||
|         result = db.session.query(db.Books).filter(db.Books.id.in_(books_list)).filter(common_filters()).all() |             cur_book = db.session.query(db.Books).filter(db.Books.id == book.book_id).filter(common_filters()).first() | ||||||
|  |             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 = db.session.query(db.Books).filter(db.Books.id == book.book_id).first() | ||||||
|  |                 result.append({'title':_('Hidden Book'), | ||||||
|  |                                'id':cur_book.id, | ||||||
|  |                                'author':[], | ||||||
|  |                                'series':[]}) | ||||||
|     return render_title_template('shelf_order.html', entries=result, |     return render_title_template('shelf_order.html', entries=result, | ||||||
|                                  title=_(u"Change order of Shelf: '%(name)s'", name=shelf.name), |                                  title=_(u"Change order of Shelf: '%(name)s'", name=shelf.name), | ||||||
|                                  shelf=shelf, page="shelforder") |                                  shelf=shelf, page="shelforder") | ||||||
|  |  | ||||||
							
								
								
									
										116
									
								
								cps/static/css/libs/viewer.css
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										116
									
								
								cps/static/css/libs/viewer.css
									
									
									
									
										vendored
									
									
								
							|  | @ -230,36 +230,46 @@ | ||||||
|   z-index: 200; |   z-index: 200; | ||||||
|   max-width: 20em; |   max-width: 20em; | ||||||
|   background-color: #FFFF99; |   background-color: #FFFF99; | ||||||
|   box-shadow: 0px 2px 5px #333; |   box-shadow: 0px 2px 5px #888; | ||||||
|   border-radius: 2px; |   border-radius: 2px; | ||||||
|   padding: 0.6em; |   padding: 6px; | ||||||
|   margin-left: 5px; |   margin-left: 5px; | ||||||
|   cursor: pointer; |   cursor: pointer; | ||||||
|   font: message-box; |   font: message-box; | ||||||
|  |   font-size: 9px; | ||||||
|   word-wrap: break-word; |   word-wrap: break-word; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .annotationLayer .popup > * { | ||||||
|  |   font-size: 9px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .annotationLayer .popup h1 { | .annotationLayer .popup h1 { | ||||||
|   font-size: 1em; |   display: inline-block; | ||||||
|   border-bottom: 1px solid #000000; | } | ||||||
|   margin: 0; | 
 | ||||||
|   padding-bottom: 0.2em; | .annotationLayer .popup span { | ||||||
|  |   display: inline-block; | ||||||
|  |   margin-left: 5px; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .annotationLayer .popup p { | .annotationLayer .popup p { | ||||||
|   margin: 0; |   border-top: 1px solid #333; | ||||||
|   padding-top: 0.2em; |   margin-top: 2px; | ||||||
|  |   padding-top: 2px; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .annotationLayer .highlightAnnotation, | .annotationLayer .highlightAnnotation, | ||||||
| .annotationLayer .underlineAnnotation, | .annotationLayer .underlineAnnotation, | ||||||
| .annotationLayer .squigglyAnnotation, | .annotationLayer .squigglyAnnotation, | ||||||
| .annotationLayer .strikeoutAnnotation, | .annotationLayer .strikeoutAnnotation, | ||||||
|  | .annotationLayer .freeTextAnnotation, | ||||||
| .annotationLayer .lineAnnotation svg line, | .annotationLayer .lineAnnotation svg line, | ||||||
| .annotationLayer .squareAnnotation svg rect, | .annotationLayer .squareAnnotation svg rect, | ||||||
| .annotationLayer .circleAnnotation svg ellipse, | .annotationLayer .circleAnnotation svg ellipse, | ||||||
| .annotationLayer .polylineAnnotation svg polyline, | .annotationLayer .polylineAnnotation svg polyline, | ||||||
| .annotationLayer .polygonAnnotation svg polygon, | .annotationLayer .polygonAnnotation svg polygon, | ||||||
|  | .annotationLayer .caretAnnotation, | ||||||
| .annotationLayer .inkAnnotation svg polyline, | .annotationLayer .inkAnnotation svg polyline, | ||||||
| .annotationLayer .stampAnnotation, | .annotationLayer .stampAnnotation, | ||||||
| .annotationLayer .fileAttachmentAnnotation { | .annotationLayer .fileAttachmentAnnotation { | ||||||
|  | @ -279,8 +289,9 @@ | ||||||
|   overflow: visible; |   overflow: visible; | ||||||
|   border: 9px solid transparent; |   border: 9px solid transparent; | ||||||
|   background-clip: content-box; |   background-clip: content-box; | ||||||
|   -o-border-image: url(images/shadow.png) 9 9 repeat; |   -webkit-border-image: url(images/shadow.png) 9 9 repeat; | ||||||
|      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; |   background-color: white; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -543,15 +554,20 @@ select { | ||||||
|   z-index: 100; |   z-index: 100; | ||||||
|   border-top: 1px solid #333; |   border-top: 1px solid #333; | ||||||
| 
 | 
 | ||||||
|   transition-duration: 200ms; |   -webkit-transition-duration: 200ms; | ||||||
|   transition-timing-function: ease; | 
 | ||||||
|  |           transition-duration: 200ms; | ||||||
|  |   -webkit-transition-timing-function: ease; | ||||||
|  |           transition-timing-function: ease; | ||||||
| } | } | ||||||
| html[dir='ltr'] #sidebarContainer { | html[dir='ltr'] #sidebarContainer { | ||||||
|  |   -webkit-transition-property: left; | ||||||
|   transition-property: left; |   transition-property: left; | ||||||
|   left: -200px; |   left: -200px; | ||||||
|   left: calc(-1 * var(--sidebar-width)); |   left: calc(-1 * var(--sidebar-width)); | ||||||
| } | } | ||||||
| html[dir='rtl'] #sidebarContainer { | html[dir='rtl'] #sidebarContainer { | ||||||
|  |   -webkit-transition-property: right; | ||||||
|   transition-property: right; |   transition-property: right; | ||||||
|   right: -200px; |   right: -200px; | ||||||
|   right: calc(-1 * var(--sidebar-width)); |   right: calc(-1 * var(--sidebar-width)); | ||||||
|  | @ -563,7 +579,8 @@ html[dir='rtl'] #sidebarContainer { | ||||||
| 
 | 
 | ||||||
| #outerContainer.sidebarResizing #sidebarContainer { | #outerContainer.sidebarResizing #sidebarContainer { | ||||||
|   /* Improve responsiveness and avoid visual glitches when the sidebar is resized. */ |   /* Improve responsiveness and avoid visual glitches when the sidebar is resized. */ | ||||||
|   transition-duration: 0s; |   -webkit-transition-duration: 0s; | ||||||
|  |           transition-duration: 0s; | ||||||
|   /* Prevent e.g. the thumbnails being selected when the sidebar is resized. */ |   /* Prevent e.g. the thumbnails being selected when the sidebar is resized. */ | ||||||
|   -webkit-user-select: none; |   -webkit-user-select: none; | ||||||
|      -moz-user-select: none; |      -moz-user-select: none; | ||||||
|  | @ -620,8 +637,10 @@ html[dir='rtl'] #sidebarContent { | ||||||
|   outline: none; |   outline: none; | ||||||
| } | } | ||||||
| #viewerContainer:not(.pdfPresentationMode) { | #viewerContainer:not(.pdfPresentationMode) { | ||||||
|   transition-duration: 200ms; |   -webkit-transition-duration: 200ms; | ||||||
|   transition-timing-function: ease; |           transition-duration: 200ms; | ||||||
|  |   -webkit-transition-timing-function: ease; | ||||||
|  |           transition-timing-function: ease; | ||||||
| } | } | ||||||
| html[dir='ltr'] #viewerContainer { | html[dir='ltr'] #viewerContainer { | ||||||
|   box-shadow: inset 1px 0 0 hsla(0,0%,100%,.05); |   box-shadow: inset 1px 0 0 hsla(0,0%,100%,.05); | ||||||
|  | @ -632,15 +651,18 @@ html[dir='rtl'] #viewerContainer { | ||||||
| 
 | 
 | ||||||
| #outerContainer.sidebarResizing #viewerContainer { | #outerContainer.sidebarResizing #viewerContainer { | ||||||
|   /* Improve responsiveness and avoid visual glitches when the sidebar is resized. */ |   /* Improve responsiveness and avoid visual glitches when the sidebar is resized. */ | ||||||
|   transition-duration: 0s; |   -webkit-transition-duration: 0s; | ||||||
|  |           transition-duration: 0s; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| html[dir='ltr'] #outerContainer.sidebarOpen #viewerContainer:not(.pdfPresentationMode) { | html[dir='ltr'] #outerContainer.sidebarOpen #viewerContainer:not(.pdfPresentationMode) { | ||||||
|  |   -webkit-transition-property: left; | ||||||
|   transition-property: left; |   transition-property: left; | ||||||
|   left: 200px; |   left: 200px; | ||||||
|   left: var(--sidebar-width); |   left: var(--sidebar-width); | ||||||
| } | } | ||||||
| html[dir='rtl'] #outerContainer.sidebarOpen #viewerContainer:not(.pdfPresentationMode) { | html[dir='rtl'] #outerContainer.sidebarOpen #viewerContainer:not(.pdfPresentationMode) { | ||||||
|  |   -webkit-transition-property: right; | ||||||
|   transition-property: right; |   transition-property: right; | ||||||
|   right: 200px; |   right: 200px; | ||||||
|   right: var(--sidebar-width); |   right: var(--sidebar-width); | ||||||
|  | @ -662,6 +684,8 @@ html[dir='rtl'] #outerContainer.sidebarOpen #viewerContainer:not(.pdfPresentatio | ||||||
|   width: 100%; |   width: 100%; | ||||||
|   height: 32px; |   height: 32px; | ||||||
|   background-color: #424242; /* fallback */ |   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), |   background-image: url(images/texture.png), | ||||||
|                     linear-gradient(hsla(0,0%,30%,.99), hsla(0,0%,25%,.95)); |                     linear-gradient(hsla(0,0%,30%,.99), hsla(0,0%,25%,.95)); | ||||||
| } | } | ||||||
|  | @ -697,6 +721,8 @@ html[dir='rtl'] #sidebarResizer { | ||||||
|   position: relative; |   position: relative; | ||||||
|   height: 32px; |   height: 32px; | ||||||
|   background-color: #474747; /* fallback */ |   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), |   background-image: url(images/texture.png), | ||||||
|                     linear-gradient(hsla(0,0%,32%,.99), hsla(0,0%,27%,.95)); |                     linear-gradient(hsla(0,0%,32%,.99), hsla(0,0%,27%,.95)); | ||||||
| } | } | ||||||
|  | @ -733,6 +759,7 @@ html[dir='rtl'] #toolbarContainer, .findbar, .secondaryToolbar { | ||||||
|   height: 100%; |   height: 100%; | ||||||
|   background-color: #ddd; |   background-color: #ddd; | ||||||
|   overflow: hidden; |   overflow: hidden; | ||||||
|  |   -webkit-transition: width 200ms; | ||||||
|   transition: width 200ms; |   transition: width 200ms; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -748,6 +775,7 @@ html[dir='rtl'] #toolbarContainer, .findbar, .secondaryToolbar { | ||||||
| 
 | 
 | ||||||
| #loadingBar .progress.indeterminate { | #loadingBar .progress.indeterminate { | ||||||
|   background-color: #999; |   background-color: #999; | ||||||
|  |   -webkit-transition: none; | ||||||
|   transition: none; |   transition: none; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -815,6 +843,9 @@ html[dir='rtl'] .findbar { | ||||||
| #findInput::-webkit-input-placeholder { | #findInput::-webkit-input-placeholder { | ||||||
|   color: hsl(0, 0%, 75%); |   color: hsl(0, 0%, 75%); | ||||||
| } | } | ||||||
|  | #findInput::-moz-placeholder { | ||||||
|  |   font-style: italic; | ||||||
|  | } | ||||||
| #findInput:-ms-input-placeholder { | #findInput:-ms-input-placeholder { | ||||||
|   font-style: italic; |   font-style: italic; | ||||||
| } | } | ||||||
|  | @ -1006,6 +1037,7 @@ html[dir='rtl'] .splitToolbarButton > .toolbarButton { | ||||||
| .splitToolbarButton.toggled > .toolbarButton, | .splitToolbarButton.toggled > .toolbarButton, | ||||||
| .toolbarButton.textButton { | .toolbarButton.textButton { | ||||||
|   background-color: hsla(0,0%,0%,.12); |   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-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0)); | ||||||
|   background-clip: padding-box; |   background-clip: padding-box; | ||||||
|   border: 1px solid hsla(0,0%,0%,.35); |   border: 1px solid hsla(0,0%,0%,.35); | ||||||
|  | @ -1013,9 +1045,12 @@ html[dir='rtl'] .splitToolbarButton > .toolbarButton { | ||||||
|   box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset, |   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%,100%,.15) inset, | ||||||
|               0 1px 0 hsla(0,0%,100%,.05); |               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; |   transition-property: background-color, border-color, box-shadow; | ||||||
|   transition-duration: 150ms; |   -webkit-transition-duration: 150ms; | ||||||
|   transition-timing-function: ease; |           transition-duration: 150ms; | ||||||
|  |   -webkit-transition-timing-function: ease; | ||||||
|  |           transition-timing-function: ease; | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
| .splitToolbarButton > .toolbarButton:hover, | .splitToolbarButton > .toolbarButton:hover, | ||||||
|  | @ -1072,9 +1107,12 @@ html[dir='rtl'] .splitToolbarButtonSeparator { | ||||||
|   padding: 12px 0; |   padding: 12px 0; | ||||||
|   margin: 1px 0; |   margin: 1px 0; | ||||||
|   box-shadow: 0 0 0 1px hsla(0,0%,100%,.03); |   box-shadow: 0 0 0 1px hsla(0,0%,100%,.03); | ||||||
|  |   -webkit-transition-property: padding; | ||||||
|   transition-property: padding; |   transition-property: padding; | ||||||
|   transition-duration: 10ms; |   -webkit-transition-duration: 10ms; | ||||||
|   transition-timing-function: ease; |           transition-duration: 10ms; | ||||||
|  |   -webkit-transition-timing-function: ease; | ||||||
|  |           transition-timing-function: ease; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .toolbarButton, | .toolbarButton, | ||||||
|  | @ -1094,9 +1132,12 @@ html[dir='rtl'] .splitToolbarButtonSeparator { | ||||||
|           user-select: none; |           user-select: none; | ||||||
|   /* Opera does not support user-select, use <... unselectable="on"> instead */ |   /* Opera does not support user-select, use <... unselectable="on"> instead */ | ||||||
|   cursor: default; |   cursor: default; | ||||||
|  |   -webkit-transition-property: background-color, border-color, box-shadow; | ||||||
|   transition-property: background-color, border-color, box-shadow; |   transition-property: background-color, border-color, box-shadow; | ||||||
|   transition-duration: 150ms; |   -webkit-transition-duration: 150ms; | ||||||
|   transition-timing-function: ease; |           transition-duration: 150ms; | ||||||
|  |   -webkit-transition-timing-function: ease; | ||||||
|  |           transition-timing-function: ease; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| html[dir='ltr'] .toolbarButton, | html[dir='ltr'] .toolbarButton, | ||||||
|  | @ -1117,6 +1158,7 @@ html[dir='rtl'] .dropdownToolbarButton { | ||||||
| .secondaryToolbarButton:hover, | .secondaryToolbarButton:hover, | ||||||
| .secondaryToolbarButton:focus { | .secondaryToolbarButton:focus { | ||||||
|   background-color: hsla(0,0%,0%,.12); |   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-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0)); | ||||||
|   background-clip: padding-box; |   background-clip: padding-box; | ||||||
|   border: 1px solid hsla(0,0%,0%,.35); |   border: 1px solid hsla(0,0%,0%,.35); | ||||||
|  | @ -1131,28 +1173,36 @@ html[dir='rtl'] .dropdownToolbarButton { | ||||||
| .dropdownToolbarButton:hover:active, | .dropdownToolbarButton:hover:active, | ||||||
| .secondaryToolbarButton:hover:active { | .secondaryToolbarButton:hover:active { | ||||||
|   background-color: hsla(0,0%,0%,.2); |   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)); |   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); |   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, |   box-shadow: 0 1px 1px hsla(0,0%,0%,.1) inset, | ||||||
|               0 0 1px hsla(0,0%,0%,.2) inset, |               0 0 1px hsla(0,0%,0%,.2) inset, | ||||||
|               0 1px 0 hsla(0,0%,100%,.05); |               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; |   transition-property: background-color, border-color, box-shadow; | ||||||
|   transition-duration: 10ms; |   -webkit-transition-duration: 10ms; | ||||||
|   transition-timing-function: linear; |           transition-duration: 10ms; | ||||||
|  |   -webkit-transition-timing-function: linear; | ||||||
|  |           transition-timing-function: linear; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .toolbarButton.toggled, | .toolbarButton.toggled, | ||||||
| .splitToolbarButton.toggled > .toolbarButton.toggled, | .splitToolbarButton.toggled > .toolbarButton.toggled, | ||||||
| .secondaryToolbarButton.toggled { | .secondaryToolbarButton.toggled { | ||||||
|   background-color: hsla(0,0%,0%,.3); |   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)); |   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); |   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, |   box-shadow: 0 1px 1px hsla(0,0%,0%,.1) inset, | ||||||
|               0 0 1px hsla(0,0%,0%,.2) inset, |               0 0 1px hsla(0,0%,0%,.2) inset, | ||||||
|               0 1px 0 hsla(0,0%,100%,.05); |               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; |   transition-property: background-color, border-color, box-shadow; | ||||||
|   transition-duration: 10ms; |   -webkit-transition-duration: 10ms; | ||||||
|   transition-timing-function: linear; |           transition-duration: 10ms; | ||||||
|  |   -webkit-transition-timing-function: linear; | ||||||
|  |           transition-timing-function: linear; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .toolbarButton.toggled:hover:active, | .toolbarButton.toggled:hover:active, | ||||||
|  | @ -1493,6 +1543,7 @@ html[dir='rtl'] .verticalToolbarSeparator { | ||||||
|   border: 1px solid transparent; |   border: 1px solid transparent; | ||||||
|   border-radius: 2px; |   border-radius: 2px; | ||||||
|   background-color: hsla(0,0%,100%,.09); |   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-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0)); | ||||||
|   background-clip: padding-box; |   background-clip: padding-box; | ||||||
|   border: 1px solid hsla(0,0%,0%,.35); |   border: 1px solid hsla(0,0%,0%,.35); | ||||||
|  | @ -1503,9 +1554,12 @@ html[dir='rtl'] .verticalToolbarSeparator { | ||||||
|   font-size: 12px; |   font-size: 12px; | ||||||
|   line-height: 14px; |   line-height: 14px; | ||||||
|   outline-style: none; |   outline-style: none; | ||||||
|  |   -webkit-transition-property: background-color, border-color, box-shadow; | ||||||
|   transition-property: background-color, border-color, box-shadow; |   transition-property: background-color, border-color, box-shadow; | ||||||
|   transition-duration: 150ms; |   -webkit-transition-duration: 150ms; | ||||||
|   transition-timing-function: ease; |           transition-duration: 150ms; | ||||||
|  |   -webkit-transition-timing-function: ease; | ||||||
|  |           transition-timing-function: ease; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .toolbarField[type=checkbox] { | .toolbarField[type=checkbox] { | ||||||
|  | @ -1619,6 +1673,7 @@ a:focus > .thumbnail > .thumbnailSelectionRing > .thumbnailImage, | ||||||
| a:focus > .thumbnail > .thumbnailSelectionRing, | a:focus > .thumbnail > .thumbnailSelectionRing, | ||||||
| .thumbnail:hover > .thumbnailSelectionRing { | .thumbnail:hover > .thumbnailSelectionRing { | ||||||
|   background-color: hsla(0,0%,100%,.15); |   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)); |   background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0)); | ||||||
|   background-clip: padding-box; |   background-clip: padding-box; | ||||||
|   box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset, |   box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset, | ||||||
|  | @ -1634,6 +1689,7 @@ a:focus > .thumbnail > .thumbnailSelectionRing, | ||||||
| 
 | 
 | ||||||
| .thumbnail.selected > .thumbnailSelectionRing { | .thumbnail.selected > .thumbnailSelectionRing { | ||||||
|   background-color: hsla(0,0%,100%,.3); |   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-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0)); | ||||||
|   background-clip: padding-box; |   background-clip: padding-box; | ||||||
|   box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset, |   box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset, | ||||||
|  | @ -1755,6 +1811,7 @@ html[dir='rtl'] .outlineItemToggler::before { | ||||||
| .outlineItem > a:hover, | .outlineItem > a:hover, | ||||||
| .attachmentsItem > button:hover { | .attachmentsItem > button:hover { | ||||||
|   background-color: hsla(0,0%,100%,.02); |   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)); |   background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0)); | ||||||
|   background-clip: padding-box; |   background-clip: padding-box; | ||||||
|   box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset, |   box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset, | ||||||
|  | @ -1766,6 +1823,7 @@ html[dir='rtl'] .outlineItemToggler::before { | ||||||
| 
 | 
 | ||||||
| .outlineItem.selected { | .outlineItem.selected { | ||||||
|   background-color: hsla(0,0%,100%,.08); |   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)); |   background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0)); | ||||||
|   background-clip: padding-box; |   background-clip: padding-box; | ||||||
|   box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset, |   box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset, | ||||||
|  | @ -1850,6 +1908,8 @@ html[dir='rtl'] .outlineItemToggler::before { | ||||||
|   font-size: 12px; |   font-size: 12px; | ||||||
|   line-height: 14px; |   line-height: 14px; | ||||||
|   background-color: #474747; /* fallback */ |   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), |   background-image: url(images/texture.png), | ||||||
|                     linear-gradient(hsla(0,0%,32%,.99), hsla(0,0%,27%,.95)); |                     linear-gradient(hsla(0,0%,32%,.99), hsla(0,0%,27%,.95)); | ||||||
|   box-shadow: inset 1px 0 0 hsla(0,0%,100%,.08), |   box-shadow: inset 1px 0 0 hsla(0,0%,100%,.08), | ||||||
|  |  | ||||||
|  | @ -26,7 +26,7 @@ html.http-error { | ||||||
| 
 | 
 | ||||||
| body{background:#f2f2f2}body h2{font-weight:normal;color:#444} | body{background:#f2f2f2}body h2{font-weight:normal;color:#444} | ||||||
| body { margin-bottom: 40px;} | body { margin-bottom: 40px;} | ||||||
| a{color: #45b29d}a:hover{color: #444;} | a{color: #45b29d} /*a:hover{color: #444;}*/ | ||||||
| .navigation .nav-head{text-transform:uppercase;color:#999;margin:20px 0}.navigation .nav-head:nth-child(1n+2){border-top:1px solid #ccc;padding-top:20px} | .navigation .nav-head{text-transform:uppercase;color:#999;margin:20px 0}.navigation .nav-head:nth-child(1n+2){border-top:1px solid #ccc;padding-top:20px} | ||||||
| .navigation li a{color:#444;text-decoration:none;display:block;padding:10px}.navigation li a:hover{background:rgba(153,153,153,0.4);border-radius:5px} | .navigation li a{color:#444;text-decoration:none;display:block;padding:10px}.navigation li a:hover{background:rgba(153,153,153,0.4);border-radius:5px} | ||||||
| .navigation li a span{margin-right:10px} | .navigation li a span{margin-right:10px} | ||||||
|  | @ -65,6 +65,10 @@ span.glyphicon.glyphicon-tags {padding-right: 5px;color: #999;vertical-align: te | ||||||
| .navbar-default .navbar-toggle {border-color: #000;} | .navbar-default .navbar-toggle {border-color: #000;} | ||||||
| .cover { margin-bottom: 10px;} | .cover { margin-bottom: 10px;} | ||||||
| .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; top: 0; right: 0; min-width: 100%; min-height: 100%; font-size: 100px; text-align: right; filter: alpha(opacity=0); opacity: 0; outline: none; background: white; cursor: inherit; display: block;} | .btn-file input[type=file] {position: absolute; top: 0; right: 0; min-width: 100%; min-height: 100%; font-size: 100px; text-align: right; filter: alpha(opacity=0); opacity: 0; outline: none; background: white; cursor: inherit; display: block;} | ||||||
|  | @ -78,6 +82,12 @@ span.glyphicon.glyphicon-tags {padding-right: 5px;color: #999;vertical-align: te | ||||||
| .spinner {margin:0 41%;} | .spinner {margin:0 41%;} | ||||||
| .spinner2 {margin:0 41%;} | .spinner2 {margin:0 41%;} | ||||||
| 
 | 
 | ||||||
|  | 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;} | .block-label {display: block;} | ||||||
| .fake-input {position: absolute; pointer-events: none; top: 0;} | .fake-input {position: absolute; pointer-events: none; top: 0;} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -40,7 +40,8 @@ function alphanumCase(a, b) { | ||||||
| 
 | 
 | ||||||
|         while (i = (j = t.charAt(x++)).charCodeAt(0)) { |         while (i = (j = t.charAt(x++)).charCodeAt(0)) { | ||||||
|             var m = (i === 46 || (i >= 48 && i <= 57)); |             var m = (i === 46 || (i >= 48 && i <= 57)); | ||||||
|             if (m !== n) { |             // Compare has to be with != otherwise fails
 | ||||||
|  |             if (m != n) { | ||||||
|                 tz[++y] = ""; |                 tz[++y] = ""; | ||||||
|                 n = m; |                 n = m; | ||||||
|             } |             } | ||||||
|  | @ -55,7 +56,8 @@ function alphanumCase(a, b) { | ||||||
|     for (var x = 0; aa[x] && bb[x]; x++) { |     for (var x = 0; aa[x] && bb[x]; x++) { | ||||||
|         if (aa[x] !== bb[x]) { |         if (aa[x] !== bb[x]) { | ||||||
|             var c = Number(aa[x]), d = Number(bb[x]); |             var c = Number(aa[x]), d = Number(bb[x]); | ||||||
|             if (c === aa[x] && d === bb[x]) { |             // Compare has to be with == otherwise fails
 | ||||||
|  |             if (c == aa[x] && d == bb[x]) { | ||||||
|                 return c - d; |                 return c - d; | ||||||
|             } else { |             } else { | ||||||
|                 return (aa[x] > bb[x]) ? 1 : -1; |                 return (aa[x] > bb[x]) ? 1 : -1; | ||||||
|  |  | ||||||
|  | @ -159,10 +159,12 @@ if ( $( 'body.book' ).length > 0 ) { | ||||||
|   real_custom_column = $( '.real_custom_columns' ); |   real_custom_column = $( '.real_custom_columns' ); | ||||||
|     // $( '.real_custom_columns' ).remove();
 |     // $( '.real_custom_columns' ).remove();
 | ||||||
|     $.each(real_custom_column, function(i, val) { |     $.each(real_custom_column, function(i, val) { | ||||||
|         real_cc = $(this).text().split( ':' ); |         var split = $(this).text().split( ':' ); | ||||||
|  |         real_cc_key = split.shift(); | ||||||
|  |         real_cc_value = split.join(':'); | ||||||
|         $( this ).text(""); |         $( this ).text(""); | ||||||
|         if (real_cc.length > 1) { |         if (real_cc_value != "") { | ||||||
|             $( this ).append( '<span>' + real_cc[0] + '</span><span>' + real_cc[1] + '</span>' ); |             $( this ).append( '<span>' + real_cc_key + '</span><span>' + real_cc_value + '</span>' ); | ||||||
|         } |         } | ||||||
|     }); |     }); | ||||||
|   //$( '.real_custom_columns:nth-child(3)' ).text(function() {
 |   //$( '.real_custom_columns:nth-child(3)' ).text(function() {
 | ||||||
|  |  | ||||||
|  | @ -15,13 +15,15 @@ | ||||||
|  *  along with this program. If not, see <http://www.gnu.org/licenses/>.
 |  *  along with this program. If not, see <http://www.gnu.org/licenses/>.
 | ||||||
|  */ |  */ | ||||||
| /* | /* | ||||||
|  * Get Metadata from Douban Books api and Google Books api |  * Get Metadata from Douban Books api and Google Books api and ComicVine | ||||||
|  * Google Books api document: https://developers.google.com/books/docs/v1/using
 |  * Google Books api document: https://developers.google.com/books/docs/v1/using
 | ||||||
|  * Douban Books api document: https://developers.douban.com/wiki/?title=book_v2 (Chinese Only)
 |  * Douban Books api document: https://developers.douban.com/wiki/?title=book_v2 (Chinese Only)
 | ||||||
|  |  * ComicVine api document: https://comicvine.gamespot.com/api/documentation
 | ||||||
| */ | */ | ||||||
| /* global _, i18nMsg, tinymce */  | /* global _, i18nMsg, tinymce */ | ||||||
| var dbResults = []; | var dbResults = []; | ||||||
| var ggResults = []; | var ggResults = []; | ||||||
|  | var cvResults = []; | ||||||
| 
 | 
 | ||||||
| $(function () { | $(function () { | ||||||
|     var msg = i18nMsg; |     var msg = i18nMsg; | ||||||
|  | @ -33,6 +35,10 @@ $(function () { | ||||||
|     var ggSearch = "/books/v1/volumes"; |     var ggSearch = "/books/v1/volumes"; | ||||||
|     var ggDone = false; |     var ggDone = false; | ||||||
| 
 | 
 | ||||||
|  |     var comicvine = "https://comicvine.gamespot.com"; | ||||||
|  |     var cvSearch = "/api/search/"; | ||||||
|  |     var cvDone = false; | ||||||
|  | 
 | ||||||
|     var showFlag = 0; |     var showFlag = 0; | ||||||
| 
 | 
 | ||||||
|     var templates = { |     var templates = { | ||||||
|  | @ -48,16 +54,17 @@ $(function () { | ||||||
|             if ($.inArray(el, uniqueTags) === -1) uniqueTags.push(el); |             if ($.inArray(el, uniqueTags) === -1) uniqueTags.push(el); | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         $("#bookAuthor").val(book.authors); |         var ampSeparatedAuthors = (book.authors || []).join(" & "); | ||||||
|  |         $("#bookAuthor").val(ampSeparatedAuthors); | ||||||
|         $("#book_title").val(book.title); |         $("#book_title").val(book.title); | ||||||
|         $("#tags").val(uniqueTags.join(",")); |         $("#tags").val(uniqueTags.join(",")); | ||||||
|         $("#rating").data("rating").setValue(Math.round(book.rating)); |         $("#rating").data("rating").setValue(Math.round(book.rating)); | ||||||
|         $(".cover img").attr("src", book.cover); |         $(".cover img").attr("src", book.cover); | ||||||
|         $("#cover_url").val(book.cover); |         $("#cover_url").val(book.cover); | ||||||
|         $("#pubdate").val(book.publishedDate); |         $("#pubdate").val(book.publishedDate); | ||||||
|         $("#publisher").val(book.publisher) |         $("#publisher").val(book.publisher); | ||||||
|         if (book.series != undefined) { |         if (typeof book.series !== "undefined") { | ||||||
|             $("#series").val(book.series) |             $("#series").val(book.series); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -72,16 +79,18 @@ $(function () { | ||||||
|         } |         } | ||||||
|         function formatDate (date) { |         function formatDate (date) { | ||||||
|             var d = new Date(date), |             var d = new Date(date), | ||||||
|                 month = '' + (d.getMonth() + 1), |                 month = "" + (d.getMonth() + 1), | ||||||
|                 day = '' + d.getDate(), |                 day = "" + d.getDate(), | ||||||
|                 year = d.getFullYear(); |                 year = d.getFullYear(); | ||||||
|          | 
 | ||||||
|             if (month.length < 2)  |             if (month.length < 2) { | ||||||
|                 month = '0' + month; |                 month = "0" + month; | ||||||
|             if (day.length < 2)  |             } | ||||||
|                 day = '0' + day; |             if (day.length < 2) { | ||||||
|          |                 day = "0" + day; | ||||||
|             return [year, month, day].join('-'); |             } | ||||||
|  | 
 | ||||||
|  |             return [year, month, day].join("-"); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (ggDone && ggResults.length > 0) { |         if (ggDone && ggResults.length > 0) { | ||||||
|  | @ -116,16 +125,17 @@ $(function () { | ||||||
|         } |         } | ||||||
|         if (dbDone && dbResults.length > 0) { |         if (dbDone && dbResults.length > 0) { | ||||||
|             dbResults.forEach(function(result) { |             dbResults.forEach(function(result) { | ||||||
|                 if (result.series){ |                 var seriesTitle = ""; | ||||||
|                     var series_title = result.series.title |                 if (result.series) { | ||||||
|  |                     seriesTitle = result.series.title; | ||||||
|                 } |                 } | ||||||
|                 var date_fomers = result.pubdate.split("-") |                 var dateFomers = result.pubdate.split("-"); | ||||||
|                 var publishedYear = parseInt(date_fomers[0]) |                 var publishedYear = parseInt(dateFomers[0]); | ||||||
|                 var publishedMonth = parseInt(date_fomers[1]) |                 var publishedMonth = parseInt(dateFomers[1]); | ||||||
|                 var publishedDate = new Date(publishedYear, publishedMonth-1, 1) |                 var publishedDate = new Date(publishedYear, publishedMonth - 1, 1); | ||||||
|  | 
 | ||||||
|  |                 publishedDate = formatDate(publishedDate); | ||||||
| 
 | 
 | ||||||
|                 publishedDate = formatDate(publishedDate) |  | ||||||
|                 |  | ||||||
|                 var book = { |                 var book = { | ||||||
|                     id: result.id, |                     id: result.id, | ||||||
|                     title: result.title, |                     title: result.title, | ||||||
|  | @ -137,7 +147,7 @@ $(function () { | ||||||
|                         return tag.title.toLowerCase().replace(/,/g, "_"); |                         return tag.title.toLowerCase().replace(/,/g, "_"); | ||||||
|                     }), |                     }), | ||||||
|                     rating: result.rating.average || 0, |                     rating: result.rating.average || 0, | ||||||
|                     series: series_title || "", |                     series: seriesTitle || "", | ||||||
|                     cover: result.image, |                     cover: result.image, | ||||||
|                     url: "https://book.douban.com/subject/" + result.id, |                     url: "https://book.douban.com/subject/" + result.id, | ||||||
|                     source: { |                     source: { | ||||||
|  | @ -160,6 +170,52 @@ $(function () { | ||||||
|             }); |             }); | ||||||
|             dbDone = false; |             dbDone = false; | ||||||
|         } |         } | ||||||
|  |         if (cvDone && cvResults.length > 0) { | ||||||
|  |             cvResults.forEach(function(result) { | ||||||
|  |                 var seriesTitle = ""; | ||||||
|  |                 if (result.volume.name) { | ||||||
|  |                     seriesTitle = result.volume.name; | ||||||
|  |                 } | ||||||
|  |                 var dateFomers = ""; | ||||||
|  |                 if (result.store_date) { | ||||||
|  |                     dateFomers = result.store_date.split("-"); | ||||||
|  |                 }else{ | ||||||
|  |                     dateFomers = result.date_added.split("-"); | ||||||
|  |                 } | ||||||
|  |                 var publishedYear = parseInt(dateFomers[0]); | ||||||
|  |                 var publishedMonth = parseInt(dateFomers[1]); | ||||||
|  |                 var publishedDate = new Date(publishedYear, publishedMonth - 1, 1); | ||||||
|  |                  | ||||||
|  |                 publishedDate = formatDate(publishedDate); | ||||||
|  | 
 | ||||||
|  |                 var book = { | ||||||
|  |                     id: result.id, | ||||||
|  |                     title: seriesTitle + ' #' +('00' + result.issue_number).slice(-3) + ' - ' + result.name, | ||||||
|  |                     authors: result.author || [], | ||||||
|  |                     description: result.description, | ||||||
|  |                     publisher: "", | ||||||
|  |                     publishedDate: publishedDate || "", | ||||||
|  |                     tags: ['Comics', seriesTitle], | ||||||
|  |                     rating: 0, | ||||||
|  |                     series: seriesTitle || "", | ||||||
|  |                     cover: result.image.original_url, | ||||||
|  |                     url: result.site_detail_url, | ||||||
|  |                     source: { | ||||||
|  |                         id: "comicvine", | ||||||
|  |                         description: "ComicVine Books", | ||||||
|  |                         url: "https://comicvine.gamespot.com/" | ||||||
|  |                     } | ||||||
|  |                 }; | ||||||
|  | 
 | ||||||
|  |                 var $book = $(templates.bookResult(book)); | ||||||
|  |                 $book.find("img").on("click", function () { | ||||||
|  |                     populateForm(book); | ||||||
|  |                 }); | ||||||
|  | 
 | ||||||
|  |                 $("#book-list").append($book); | ||||||
|  |             }); | ||||||
|  |             cvDone = false; | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     function ggSearchBook (title) { |     function ggSearchBook (title) { | ||||||
|  | @ -183,7 +239,7 @@ $(function () { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     function dbSearchBook (title) { |     function dbSearchBook (title) { | ||||||
|         apikey="0df993c66c0c636e29ecbb5344252a4a" |         var apikey = "0df993c66c0c636e29ecbb5344252a4a"; | ||||||
|         $.ajax({ |         $.ajax({ | ||||||
|             url: douban + dbSearch + "?apikey=" + apikey + "&q=" + title + "&fields=all&count=10", |             url: douban + dbSearch + "?apikey=" + apikey + "&q=" + title + "&fields=all&count=10", | ||||||
|             type: "GET", |             type: "GET", | ||||||
|  | @ -193,7 +249,7 @@ $(function () { | ||||||
|                 dbResults = data.books; |                 dbResults = data.books; | ||||||
|             }, |             }, | ||||||
|             error: function error() { |             error: function error() { | ||||||
|                 $("#meta-info").html("<p class=\"text-danger\">" + msg.search_error + "!</p>"+ $("#meta-info")[0].innerHTML) |                 $("#meta-info").html("<p class=\"text-danger\">" + msg.search_error + "!</p>" + $("#meta-info")[0].innerHTML); | ||||||
|             }, |             }, | ||||||
|             complete: function complete() { |             complete: function complete() { | ||||||
|                 dbDone = true; |                 dbDone = true; | ||||||
|  | @ -203,12 +259,35 @@ $(function () { | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     function cvSearchBook (title) { | ||||||
|  |         var apikey = "57558043c53943d5d1e96a9ad425b0eb85532ee6"; | ||||||
|  |         title = encodeURIComponent(title); | ||||||
|  |         $.ajax({ | ||||||
|  |             url: comicvine + cvSearch + "?api_key=" + apikey + "&resources=issue&query=" + title + "&sort=name:desc&format=jsonp", | ||||||
|  |             type: "GET", | ||||||
|  |             dataType: "jsonp", | ||||||
|  |             jsonp: "json_callback", | ||||||
|  |             success: function success(data) { | ||||||
|  |                 cvResults = data.results; | ||||||
|  |             }, | ||||||
|  |             error: function error() { | ||||||
|  |                 $("#meta-info").html("<p class=\"text-danger\">" + msg.search_error + "!</p>" + $("#meta-info")[0].innerHTML); | ||||||
|  |             }, | ||||||
|  |             complete: function complete() { | ||||||
|  |                 cvDone = true; | ||||||
|  |                 showResult(); | ||||||
|  |                 $("#show-comics").trigger("change"); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     function doSearch (keyword) { |     function doSearch (keyword) { | ||||||
|         showFlag = 0; |         showFlag = 0; | ||||||
|         $("#meta-info").text(msg.loading); |         $("#meta-info").text(msg.loading); | ||||||
|         if (keyword) { |         if (keyword) { | ||||||
|             dbSearchBook(keyword); |             dbSearchBook(keyword); | ||||||
|             ggSearchBook(keyword); |             ggSearchBook(keyword); | ||||||
|  |             cvSearchBook(keyword); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										4
									
								
								cps/static/js/libs/Sortable.min.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								cps/static/js/libs/Sortable.min.js
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										5819
									
								
								cps/static/js/libs/pdf.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5819
									
								
								cps/static/js/libs/pdf.js
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										4961
									
								
								cps/static/js/libs/pdf.worker.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4961
									
								
								cps/static/js/libs/pdf.worker.js
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										1763
									
								
								cps/static/js/libs/viewer.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1763
									
								
								cps/static/js/libs/viewer.js
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							|  | @ -228,6 +228,41 @@ $(function() { | ||||||
|             $(this).find(".modal-body").html("..."); |             $(this).find(".modal-body").html("..."); | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|  |     $("#modal_kobo_token") | ||||||
|  |         .on("show.bs.modal", function(e) { | ||||||
|  |             var $modalBody = $(this).find(".modal-body"); | ||||||
|  | 
 | ||||||
|  |             // Prevent static assets from loading multiple times
 | ||||||
|  |             var useCache = function(options) { | ||||||
|  |                 options.async = true; | ||||||
|  |                 options.cache = true; | ||||||
|  |             }; | ||||||
|  |             preFilters.add(useCache); | ||||||
|  | 
 | ||||||
|  |             $.get(e.relatedTarget.href).done(function(content) { | ||||||
|  |                 $modalBody.html(content); | ||||||
|  |                 preFilters.remove(useCache); | ||||||
|  |             }); | ||||||
|  |         }) | ||||||
|  |         .on("hidden.bs.modal", function() { | ||||||
|  |             $(this).find(".modal-body").html("..."); | ||||||
|  |              $("#config_delete_kobo_token").show(); | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |     $("#btndeletetoken").click(function() { | ||||||
|  |         //get data-id attribute of the clicked element
 | ||||||
|  |         var pathname = document.getElementsByTagName("script"), src = pathname[pathname.length-1].src; | ||||||
|  |         var path = src.substring(0,src.lastIndexOf("/")); | ||||||
|  |         // var domainId = $(this).value("domainId");
 | ||||||
|  |         $.ajax({ | ||||||
|  |             method:"get", | ||||||
|  |             url: path + "/../../kobo_auth/deleteauthtoken/" + this.value, | ||||||
|  |         }); | ||||||
|  |         $("#modalDeleteToken").modal("hide"); | ||||||
|  |         $("#config_delete_kobo_token").hide(); | ||||||
|  | 
 | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|     $(window).resize(function() { |     $(window).resize(function() { | ||||||
|         $(".discover .row").isotope("layout"); |         $(".discover .row").isotope("layout"); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|  | @ -29,7 +29,7 @@ function sendData(path) { | ||||||
|     var maxElements; |     var maxElements; | ||||||
|     var tmp = []; |     var tmp = []; | ||||||
| 
 | 
 | ||||||
|     elements = Sortable.utils.find(sortTrue, "div"); |     elements = $(".list-group-item"); | ||||||
|     maxElements = elements.length; |     maxElements = elements.length; | ||||||
| 
 | 
 | ||||||
|     var form = document.createElement("form"); |     var form = document.createElement("form"); | ||||||
|  |  | ||||||
|  | @ -93,6 +93,116 @@ $(function() { | ||||||
|         var domainId = $(e.relatedTarget).data("domain-id"); |         var domainId = $(e.relatedTarget).data("domain-id"); | ||||||
|         $(e.currentTarget).find("#btndeletedomain").data("domainId", domainId); |         $(e.currentTarget).find("#btndeletedomain").data("domainId", domainId); | ||||||
|     }); |     }); | ||||||
|  | 
 | ||||||
|  |     $('#restrictModal').on('hidden.bs.modal', function () { | ||||||
|  |         // Destroy table and remove hooks for buttons
 | ||||||
|  |         $("#restrict-elements-table").unbind(); | ||||||
|  |         $('#restrict-elements-table').bootstrapTable('destroy'); | ||||||
|  |         $("[id^=submit_]").unbind(); | ||||||
|  |         $('#h1').addClass('hidden'); | ||||||
|  |         $('#h2').addClass('hidden'); | ||||||
|  |         $('#h3').addClass('hidden'); | ||||||
|  |         $('#h4').addClass('hidden'); | ||||||
|  |     }); | ||||||
|  |     function startTable(type){ | ||||||
|  |         var pathname = document.getElementsByTagName("script"), src = pathname[pathname.length-1].src; | ||||||
|  |         var path = src.substring(0,src.lastIndexOf("/")); | ||||||
|  |         $("#restrict-elements-table").bootstrapTable({ | ||||||
|  |             formatNoMatches: function () { | ||||||
|  |                 return ""; | ||||||
|  |             }, | ||||||
|  |             url: path + "/../../ajax/listrestriction/" + type, | ||||||
|  |             rowStyle: function(row, index) { | ||||||
|  |                 console.log('Reihe :' + row + ' Index :'+ index); | ||||||
|  |                 if (row.id.charAt(0) == 'a') { | ||||||
|  |                     return {classes: 'bg-primary'} | ||||||
|  |                 } | ||||||
|  |                 else { | ||||||
|  |                     return {classes: 'bg-dark-danger'} | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|  |             onClickCell: function (field, value, row, $element) { | ||||||
|  |                 if(field == 3){ | ||||||
|  |                     console.log("element") | ||||||
|  |                     $.ajax ({ | ||||||
|  |                         type: 'Post', | ||||||
|  |                         data: 'id=' + row.id + '&type=' + row.type + "&Element=" + row.Element, | ||||||
|  |                         url: path + "/../../ajax/deleterestriction/" + type, | ||||||
|  |                         async: true, | ||||||
|  |                         timeout: 900, | ||||||
|  |                         success:function(data) { | ||||||
|  |                             $.ajax({ | ||||||
|  |                                 method:"get", | ||||||
|  |                                 url: path + "/../../ajax/listrestriction/"+type, | ||||||
|  |                                 async: true, | ||||||
|  |                                 timeout: 900, | ||||||
|  |                                 success:function(data) { | ||||||
|  |                                     $("#restrict-elements-table").bootstrapTable("load", data); | ||||||
|  |                                 } | ||||||
|  |                             }); | ||||||
|  |                         } | ||||||
|  |                     }); | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|  |             striped: false | ||||||
|  |         }); | ||||||
|  |         $("#restrict-elements-table").removeClass('table-hover'); | ||||||
|  |         $("#restrict-elements-table").on('editable-save.bs.table', function (e, field, row, old, $el) { | ||||||
|  |             console.log("Hallo"); | ||||||
|  |             $.ajax({ | ||||||
|  |                 url: path + "/../../ajax/editrestriction/"+type, | ||||||
|  |                 type: 'Post', | ||||||
|  |                 data: row //$(this).closest("form").serialize() + "&" + $(this)[0].name + "=",
 | ||||||
|  |             }); | ||||||
|  |         }); | ||||||
|  |         $("[id^=submit_]").click(function(event) { | ||||||
|  |             // event.stopPropagation();
 | ||||||
|  |             // event.preventDefault();
 | ||||||
|  |             $(this)[0].blur(); | ||||||
|  |             console.log($(this)[0].name); | ||||||
|  |             $.ajax({ | ||||||
|  |                 url: path + "/../../ajax/addrestriction/"+type, | ||||||
|  |                 type: 'Post', | ||||||
|  |                 data: $(this).closest("form").serialize() + "&" + $(this)[0].name + "=", | ||||||
|  |                 success: function () { | ||||||
|  |                 $.ajax ({ | ||||||
|  |                     method:"get", | ||||||
|  |                     url: path + "/../../ajax/listrestriction/"+type, | ||||||
|  |                     async: true, | ||||||
|  |                     timeout: 900, | ||||||
|  |                     success:function(data) { | ||||||
|  |                         $("#restrict-elements-table").bootstrapTable("load", data); | ||||||
|  |                        } | ||||||
|  |                     }); | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|  |             return; | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |     $('#get_column_values').on('click',function() | ||||||
|  |     { | ||||||
|  |         startTable(1); | ||||||
|  |         $('#h2').removeClass('hidden'); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     $('#get_tags').on('click',function() | ||||||
|  |     { | ||||||
|  |         startTable(0); | ||||||
|  |         $('#h1').removeClass('hidden'); | ||||||
|  |     }); | ||||||
|  |     $('#get_user_column_values').on('click',function() | ||||||
|  |     { | ||||||
|  |         startTable(3); | ||||||
|  |         $('#h4').removeClass('hidden'); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     $('#get_user_tags').on('click',function() | ||||||
|  |     { | ||||||
|  |         startTable(2); | ||||||
|  |         $(this)[0].blur(); | ||||||
|  |         $('#h3').removeClass('hidden'); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| /* Function for deleting domain restrictions */ | /* Function for deleting domain restrictions */ | ||||||
|  | @ -104,3 +214,12 @@ function TableActions (value, row, index) { | ||||||
|         "</a>" |         "</a>" | ||||||
|     ].join(""); |     ].join(""); | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | /* Function for deleting domain restrictions */ | ||||||
|  | function RestrictionActions (value, row, index) { | ||||||
|  |     return [ | ||||||
|  |         "<div class=\"danger remove\" data-restriction-id=\"" + row.id + "\" title=\"Remove\">", | ||||||
|  |         "<i class=\"glyphicon glyphicon-trash\"></i>", | ||||||
|  |         "</div>" | ||||||
|  |     ].join(""); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -226,6 +226,10 @@ invalid_file_error=ملف PDF تالف أو غير صحيح. | ||||||
| missing_file_error=ملف PDF غير موجود. | missing_file_error=ملف PDF غير موجود. | ||||||
| unexpected_response_error=استجابة خادوم غير متوقعة. | unexpected_response_error=استجابة خادوم غير متوقعة. | ||||||
| 
 | 
 | ||||||
|  | # LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be | ||||||
|  | # replaced by the modification date, and time, of the annotation. | ||||||
|  | annotation_date_string={{date}}، {{time}} | ||||||
|  | 
 | ||||||
| # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | ||||||
| # "{{type}}" will be replaced with an annotation type from a list defined in | # "{{type}}" will be replaced with an annotation type from a list defined in | ||||||
| # the PDF spec (32000-1:2008 Table 169 – Annotation types). | # the PDF spec (32000-1:2008 Table 169 – Annotation types). | ||||||
|  |  | ||||||
|  | @ -226,6 +226,10 @@ invalid_file_error=Няспраўны або пашкоджаны файл PDF. | ||||||
| missing_file_error=Адсутны файл PDF. | missing_file_error=Адсутны файл PDF. | ||||||
| unexpected_response_error=Нечаканы адказ сервера. | unexpected_response_error=Нечаканы адказ сервера. | ||||||
| 
 | 
 | ||||||
|  | # LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be | ||||||
|  | # replaced by the modification date, and time, of the annotation. | ||||||
|  | annotation_date_string={{date}}, {{time}} | ||||||
|  | 
 | ||||||
| # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | ||||||
| # "{{type}}" will be replaced with an annotation type from a list defined in | # "{{type}}" will be replaced with an annotation type from a list defined in | ||||||
| # the PDF spec (32000-1:2008 Table 169 – Annotation types). | # the PDF spec (32000-1:2008 Table 169 – Annotation types). | ||||||
|  |  | ||||||
|  | @ -226,6 +226,10 @@ invalid_file_error=Restr PDF didalvoudek pe kontronet. | ||||||
| missing_file_error=Restr PDF o vankout. | missing_file_error=Restr PDF o vankout. | ||||||
| unexpected_response_error=Respont dic'hortoz a-berzh an dafariad | unexpected_response_error=Respont dic'hortoz a-berzh an dafariad | ||||||
| 
 | 
 | ||||||
|  | # LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be | ||||||
|  | # replaced by the modification date, and time, of the annotation. | ||||||
|  | annotation_date_string={{date}}, {{time}} | ||||||
|  | 
 | ||||||
| # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | ||||||
| # "{{type}}" will be replaced with an annotation type from a list defined in | # "{{type}}" will be replaced with an annotation type from a list defined in | ||||||
| # the PDF spec (32000-1:2008 Table 169 – Annotation types). | # the PDF spec (32000-1:2008 Table 169 – Annotation types). | ||||||
|  |  | ||||||
|  | @ -226,6 +226,10 @@ invalid_file_error=Man oke ta o yujtajinäq ri PDF yakb'äl. | ||||||
| missing_file_error=Man xilitäj ta ri PDF yakb'äl. | missing_file_error=Man xilitäj ta ri PDF yakb'äl. | ||||||
| unexpected_response_error=Man oyob'en ta tz'olin rutzij ruk'u'x samaj. | unexpected_response_error=Man oyob'en ta tz'olin rutzij ruk'u'x samaj. | ||||||
| 
 | 
 | ||||||
|  | # LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be | ||||||
|  | # replaced by the modification date, and time, of the annotation. | ||||||
|  | annotation_date_string={{date}}, {{time}} | ||||||
|  | 
 | ||||||
| # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | ||||||
| # "{{type}}" will be replaced with an annotation type from a list defined in | # "{{type}}" will be replaced with an annotation type from a list defined in | ||||||
| # the PDF spec (32000-1:2008 Table 169 – Annotation types). | # the PDF spec (32000-1:2008 Table 169 – Annotation types). | ||||||
|  |  | ||||||
|  | @ -226,6 +226,10 @@ invalid_file_error=Neplatný nebo chybný soubor PDF. | ||||||
| missing_file_error=Chybí soubor PDF. | missing_file_error=Chybí soubor PDF. | ||||||
| unexpected_response_error=Neočekávaná odpověď serveru. | unexpected_response_error=Neočekávaná odpověď serveru. | ||||||
| 
 | 
 | ||||||
|  | # LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be | ||||||
|  | # replaced by the modification date, and time, of the annotation. | ||||||
|  | annotation_date_string={{date}}, {{time}} | ||||||
|  | 
 | ||||||
| # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | ||||||
| # "{{type}}" will be replaced with an annotation type from a list defined in | # "{{type}}" will be replaced with an annotation type from a list defined in | ||||||
| # the PDF spec (32000-1:2008 Table 169 – Annotation types). | # the PDF spec (32000-1:2008 Table 169 – Annotation types). | ||||||
|  |  | ||||||
|  | @ -226,6 +226,10 @@ invalid_file_error=Ffeil PDF annilys neu llwgr. | ||||||
| missing_file_error=Ffeil PDF coll. | missing_file_error=Ffeil PDF coll. | ||||||
| unexpected_response_error=Ymateb annisgwyl gan y gweinydd. | unexpected_response_error=Ymateb annisgwyl gan y gweinydd. | ||||||
| 
 | 
 | ||||||
|  | # LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be | ||||||
|  | # replaced by the modification date, and time, of the annotation. | ||||||
|  | annotation_date_string={{date}}, {{time}} | ||||||
|  | 
 | ||||||
| # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | ||||||
| # "{{type}}" will be replaced with an annotation type from a list defined in | # "{{type}}" will be replaced with an annotation type from a list defined in | ||||||
| # the PDF spec (32000-1:2008 Table 169 – Annotation types). | # the PDF spec (32000-1:2008 Table 169 – Annotation types). | ||||||
|  |  | ||||||
|  | @ -226,6 +226,10 @@ invalid_file_error=PDF-filen er ugyldig eller ødelagt. | ||||||
| missing_file_error=Manglende PDF-fil. | missing_file_error=Manglende PDF-fil. | ||||||
| unexpected_response_error=Uventet svar fra serveren. | unexpected_response_error=Uventet svar fra serveren. | ||||||
| 
 | 
 | ||||||
|  | # LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be | ||||||
|  | # replaced by the modification date, and time, of the annotation. | ||||||
|  | annotation_date_string={{date}}, {{time}} | ||||||
|  | 
 | ||||||
| # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | ||||||
| # "{{type}}" will be replaced with an annotation type from a list defined in | # "{{type}}" will be replaced with an annotation type from a list defined in | ||||||
| # the PDF spec (32000-1:2008 Table 169 – Annotation types). | # the PDF spec (32000-1:2008 Table 169 – Annotation types). | ||||||
|  |  | ||||||
|  | @ -226,6 +226,10 @@ invalid_file_error=Ungültige oder beschädigte PDF-Datei | ||||||
| missing_file_error=Fehlende PDF-Datei | missing_file_error=Fehlende PDF-Datei | ||||||
| unexpected_response_error=Unerwartete Antwort des Servers | unexpected_response_error=Unerwartete Antwort des Servers | ||||||
| 
 | 
 | ||||||
|  | # LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be | ||||||
|  | # replaced by the modification date, and time, of the annotation. | ||||||
|  | annotation_date_string={{date}}, {{time}} | ||||||
|  | 
 | ||||||
| # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | ||||||
| # "{{type}}" will be replaced with an annotation type from a list defined in | # "{{type}}" will be replaced with an annotation type from a list defined in | ||||||
| # the PDF spec (32000-1:2008 Table 169 – Annotation types). | # the PDF spec (32000-1:2008 Table 169 – Annotation types). | ||||||
|  |  | ||||||
|  | @ -226,6 +226,10 @@ invalid_file_error=Μη έγκυρο ή κατεστραμμένο αρχείο | ||||||
| missing_file_error=Λείπει αρχείο PDF. | missing_file_error=Λείπει αρχείο PDF. | ||||||
| unexpected_response_error=Μη αναμενόμενη απόκριση από το διακομιστή. | unexpected_response_error=Μη αναμενόμενη απόκριση από το διακομιστή. | ||||||
| 
 | 
 | ||||||
|  | # LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be | ||||||
|  | # replaced by the modification date, and time, of the annotation. | ||||||
|  | annotation_date_string={{date}}, {{time}} | ||||||
|  | 
 | ||||||
| # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | ||||||
| # "{{type}}" will be replaced with an annotation type from a list defined in | # "{{type}}" will be replaced with an annotation type from a list defined in | ||||||
| # the PDF spec (32000-1:2008 Table 169 – Annotation types). | # the PDF spec (32000-1:2008 Table 169 – Annotation types). | ||||||
|  |  | ||||||
|  | @ -226,6 +226,10 @@ invalid_file_error=Invalid or corrupted PDF file. | ||||||
| missing_file_error=Missing PDF file. | missing_file_error=Missing PDF file. | ||||||
| unexpected_response_error=Unexpected server response. | unexpected_response_error=Unexpected server response. | ||||||
| 
 | 
 | ||||||
|  | # LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be | ||||||
|  | # replaced by the modification date, and time, of the annotation. | ||||||
|  | annotation_date_string={{date}}, {{time}} | ||||||
|  | 
 | ||||||
| # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | ||||||
| # "{{type}}" will be replaced with an annotation type from a list defined in | # "{{type}}" will be replaced with an annotation type from a list defined in | ||||||
| # the PDF spec (32000-1:2008 Table 169 – Annotation types). | # the PDF spec (32000-1:2008 Table 169 – Annotation types). | ||||||
|  |  | ||||||
|  | @ -226,6 +226,10 @@ invalid_file_error=Invalid or corrupted PDF file. | ||||||
| missing_file_error=Missing PDF file. | missing_file_error=Missing PDF file. | ||||||
| unexpected_response_error=Unexpected server response. | unexpected_response_error=Unexpected server response. | ||||||
| 
 | 
 | ||||||
|  | # LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be | ||||||
|  | # replaced by the modification date, and time, of the annotation. | ||||||
|  | annotation_date_string={{date}}, {{time}} | ||||||
|  | 
 | ||||||
| # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | ||||||
| # "{{type}}" will be replaced with an annotation type from a list defined in | # "{{type}}" will be replaced with an annotation type from a list defined in | ||||||
| # the PDF spec (32000-1:2008 Table 169 – Annotation types). | # the PDF spec (32000-1:2008 Table 169 – Annotation types). | ||||||
|  |  | ||||||
|  | @ -226,6 +226,10 @@ invalid_file_error=Invalid or corrupted PDF file. | ||||||
| missing_file_error=Missing PDF file. | missing_file_error=Missing PDF file. | ||||||
| unexpected_response_error=Unexpected server response. | unexpected_response_error=Unexpected server response. | ||||||
| 
 | 
 | ||||||
|  | # LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be | ||||||
|  | # replaced by the modification date, and time, of the annotation. | ||||||
|  | annotation_date_string={{date}}, {{time}} | ||||||
|  | 
 | ||||||
| # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | ||||||
| # "{{type}}" will be replaced with an annotation type from a list defined in | # "{{type}}" will be replaced with an annotation type from a list defined in | ||||||
| # the PDF spec (32000-1:2008 Table 169 – Annotation types). | # the PDF spec (32000-1:2008 Table 169 – Annotation types). | ||||||
|  |  | ||||||
|  | @ -226,6 +226,10 @@ invalid_file_error=Nevalida aŭ difektita PDF dosiero. | ||||||
| missing_file_error=Mankas dosiero PDF. | missing_file_error=Mankas dosiero PDF. | ||||||
| unexpected_response_error=Neatendita respondo de servilo. | unexpected_response_error=Neatendita respondo de servilo. | ||||||
| 
 | 
 | ||||||
|  | # LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be | ||||||
|  | # replaced by the modification date, and time, of the annotation. | ||||||
|  | annotation_date_string={{date}}, {{time}} | ||||||
|  | 
 | ||||||
| # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | ||||||
| # "{{type}}" will be replaced with an annotation type from a list defined in | # "{{type}}" will be replaced with an annotation type from a list defined in | ||||||
| # the PDF spec (32000-1:2008 Table 169 – Annotation types). | # the PDF spec (32000-1:2008 Table 169 – Annotation types). | ||||||
|  |  | ||||||
|  | @ -226,6 +226,10 @@ invalid_file_error=Archivo PDF no válido o cocrrupto. | ||||||
| missing_file_error=Archivo PDF faltante. | missing_file_error=Archivo PDF faltante. | ||||||
| unexpected_response_error=Respuesta del servidor inesperada. | unexpected_response_error=Respuesta del servidor inesperada. | ||||||
| 
 | 
 | ||||||
|  | # LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be | ||||||
|  | # replaced by the modification date, and time, of the annotation. | ||||||
|  | annotation_date_string={{date}}, {{time}} | ||||||
|  | 
 | ||||||
| # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | ||||||
| # "{{type}}" will be replaced with an annotation type from a list defined in | # "{{type}}" will be replaced with an annotation type from a list defined in | ||||||
| # the PDF spec (32000-1:2008 Table 169 – Annotation types). | # the PDF spec (32000-1:2008 Table 169 – Annotation types). | ||||||
|  |  | ||||||
|  | @ -226,6 +226,10 @@ invalid_file_error=Archivo PDF inválido o corrupto. | ||||||
| missing_file_error=Falta el archivo PDF. | missing_file_error=Falta el archivo PDF. | ||||||
| unexpected_response_error=Respuesta del servidor inesperada. | unexpected_response_error=Respuesta del servidor inesperada. | ||||||
| 
 | 
 | ||||||
|  | # LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be | ||||||
|  | # replaced by the modification date, and time, of the annotation. | ||||||
|  | annotation_date_string={{date}}, {{time}} | ||||||
|  | 
 | ||||||
| # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | ||||||
| # "{{type}}" will be replaced with an annotation type from a list defined in | # "{{type}}" will be replaced with an annotation type from a list defined in | ||||||
| # the PDF spec (32000-1:2008 Table 169 – Annotation types). | # the PDF spec (32000-1:2008 Table 169 – Annotation types). | ||||||
|  |  | ||||||
|  | @ -226,6 +226,10 @@ invalid_file_error=Fichero PDF no válido o corrupto. | ||||||
| missing_file_error=No hay fichero PDF. | missing_file_error=No hay fichero PDF. | ||||||
| unexpected_response_error=Respuesta inesperada del servidor. | unexpected_response_error=Respuesta inesperada del servidor. | ||||||
| 
 | 
 | ||||||
|  | # LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be | ||||||
|  | # replaced by the modification date, and time, of the annotation. | ||||||
|  | annotation_date_string={{date}}, {{time}} | ||||||
|  | 
 | ||||||
| # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | ||||||
| # "{{type}}" will be replaced with an annotation type from a list defined in | # "{{type}}" will be replaced with an annotation type from a list defined in | ||||||
| # the PDF spec (32000-1:2008 Table 169 – Annotation types). | # the PDF spec (32000-1:2008 Table 169 – Annotation types). | ||||||
|  |  | ||||||
|  | @ -226,6 +226,10 @@ invalid_file_error=Vigane või rikutud PDF-fail. | ||||||
| missing_file_error=PDF-fail puudub. | missing_file_error=PDF-fail puudub. | ||||||
| unexpected_response_error=Ootamatu vastus serverilt. | unexpected_response_error=Ootamatu vastus serverilt. | ||||||
| 
 | 
 | ||||||
|  | # LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be | ||||||
|  | # replaced by the modification date, and time, of the annotation. | ||||||
|  | annotation_date_string={{date}} {{time}} | ||||||
|  | 
 | ||||||
| # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | ||||||
| # "{{type}}" will be replaced with an annotation type from a list defined in | # "{{type}}" will be replaced with an annotation type from a list defined in | ||||||
| # the PDF spec (32000-1:2008 Table 169 – Annotation types). | # the PDF spec (32000-1:2008 Table 169 – Annotation types). | ||||||
|  |  | ||||||
|  | @ -226,6 +226,10 @@ invalid_file_error=PDF fitxategi baliogabe edo hondatua. | ||||||
| missing_file_error=PDF fitxategia falta da. | missing_file_error=PDF fitxategia falta da. | ||||||
| unexpected_response_error=Espero gabeko zerbitzariaren erantzuna. | unexpected_response_error=Espero gabeko zerbitzariaren erantzuna. | ||||||
| 
 | 
 | ||||||
|  | # LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be | ||||||
|  | # replaced by the modification date, and time, of the annotation. | ||||||
|  | annotation_date_string={{date}}, {{time}} | ||||||
|  | 
 | ||||||
| # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | ||||||
| # "{{type}}" will be replaced with an annotation type from a list defined in | # "{{type}}" will be replaced with an annotation type from a list defined in | ||||||
| # the PDF spec (32000-1:2008 Table 169 – Annotation types). | # the PDF spec (32000-1:2008 Table 169 – Annotation types). | ||||||
|  |  | ||||||
|  | @ -226,6 +226,10 @@ invalid_file_error=Virheellinen tai vioittunut PDF-tiedosto. | ||||||
| missing_file_error=Puuttuva PDF-tiedosto. | missing_file_error=Puuttuva PDF-tiedosto. | ||||||
| unexpected_response_error=Odottamaton vastaus palvelimelta. | unexpected_response_error=Odottamaton vastaus palvelimelta. | ||||||
| 
 | 
 | ||||||
|  | # LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be | ||||||
|  | # replaced by the modification date, and time, of the annotation. | ||||||
|  | annotation_date_string={{date}}, {{time}} | ||||||
|  | 
 | ||||||
| # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | ||||||
| # "{{type}}" will be replaced with an annotation type from a list defined in | # "{{type}}" will be replaced with an annotation type from a list defined in | ||||||
| # the PDF spec (32000-1:2008 Table 169 – Annotation types). | # the PDF spec (32000-1:2008 Table 169 – Annotation types). | ||||||
|  |  | ||||||
|  | @ -226,6 +226,10 @@ invalid_file_error=Fichier PDF invalide ou corrompu. | ||||||
| missing_file_error=Fichier PDF manquant. | missing_file_error=Fichier PDF manquant. | ||||||
| unexpected_response_error=Réponse inattendue du serveur. | unexpected_response_error=Réponse inattendue du serveur. | ||||||
| 
 | 
 | ||||||
|  | # LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be | ||||||
|  | # replaced by the modification date, and time, of the annotation. | ||||||
|  | annotation_date_string={{date}} à {{time}} | ||||||
|  | 
 | ||||||
| # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | ||||||
| # "{{type}}" will be replaced with an annotation type from a list defined in | # "{{type}}" will be replaced with an annotation type from a list defined in | ||||||
| # the PDF spec (32000-1:2008 Table 169 – Annotation types). | # the PDF spec (32000-1:2008 Table 169 – Annotation types). | ||||||
|  |  | ||||||
|  | @ -226,6 +226,10 @@ invalid_file_error=Ynfalide of korruptearre PDF-bestân. | ||||||
| missing_file_error=PDF-bestân ûntbrekt. | missing_file_error=PDF-bestân ûntbrekt. | ||||||
| unexpected_response_error=Unferwacht serverantwurd. | unexpected_response_error=Unferwacht serverantwurd. | ||||||
| 
 | 
 | ||||||
|  | # LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be | ||||||
|  | # replaced by the modification date, and time, of the annotation. | ||||||
|  | annotation_date_string={{date}}, {{time}} | ||||||
|  | 
 | ||||||
| # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | ||||||
| # "{{type}}" will be replaced with an annotation type from a list defined in | # "{{type}}" will be replaced with an annotation type from a list defined in | ||||||
| # the PDF spec (32000-1:2008 Table 169 – Annotation types). | # the PDF spec (32000-1:2008 Table 169 – Annotation types). | ||||||
|  |  | ||||||
|  | @ -226,6 +226,10 @@ invalid_file_error=PDF marandurenda ndoikóiva térã ivaipyréva. | ||||||
| missing_file_error=Ndaipóri PDF marandurenda | missing_file_error=Ndaipóri PDF marandurenda | ||||||
| unexpected_response_error=Mohendahavusu mbohovái ñeha'arõ'ỹva. | unexpected_response_error=Mohendahavusu mbohovái ñeha'arõ'ỹva. | ||||||
| 
 | 
 | ||||||
|  | # LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be | ||||||
|  | # replaced by the modification date, and time, of the annotation. | ||||||
|  | annotation_date_string={{date}}, {{time}} | ||||||
|  | 
 | ||||||
| # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | ||||||
| # "{{type}}" will be replaced with an annotation type from a list defined in | # "{{type}}" will be replaced with an annotation type from a list defined in | ||||||
| # the PDF spec (32000-1:2008 Table 169 – Annotation types). | # the PDF spec (32000-1:2008 Table 169 – Annotation types). | ||||||
|  |  | ||||||
|  | @ -173,6 +173,7 @@ find_reached_bottom=הגיע לסוף הדף, ממשיך מלמעלה | ||||||
| # "{{current}}" and "{{total}}" will be replaced by a number representing the | # "{{current}}" and "{{total}}" will be replaced by a number representing the | ||||||
| # index of the currently active find result, respectively a number representing | # index of the currently active find result, respectively a number representing | ||||||
| # the total number of matches in the document. | # the total number of matches in the document. | ||||||
|  | find_match_count={[ plural(total) ]} | ||||||
| find_match_count[one]=תוצאה {{current}} מתוך {{total}} | find_match_count[one]=תוצאה {{current}} מתוך {{total}} | ||||||
| find_match_count[two]={{current}} מתוך {{total}} תוצאות | find_match_count[two]={{current}} מתוך {{total}} תוצאות | ||||||
| find_match_count[few]={{current}} מתוך {{total}} תוצאות | find_match_count[few]={{current}} מתוך {{total}} תוצאות | ||||||
|  | @ -181,13 +182,14 @@ find_match_count[other]={{current}} מתוך {{total}} תוצאות | ||||||
| # LOCALIZATION NOTE (find_match_count_limit): The supported plural forms are | # LOCALIZATION NOTE (find_match_count_limit): The supported plural forms are | ||||||
| # [zero|one|two|few|many|other], with [other] as the default value. | # [zero|one|two|few|many|other], with [other] as the default value. | ||||||
| # "{{limit}}" will be replaced by a numerical value. | # "{{limit}}" will be replaced by a numerical value. | ||||||
|  | find_match_count_limit={[ plural(limit) ]} | ||||||
| find_match_count_limit[zero]=יותר מ־{{limit}} תוצאות | find_match_count_limit[zero]=יותר מ־{{limit}} תוצאות | ||||||
| find_match_count_limit[one]=יותר מתוצאה אחת | find_match_count_limit[one]=יותר מתוצאה אחת | ||||||
| find_match_count_limit[two]=יותר מ־{{limit}} תוצאות | find_match_count_limit[two]=יותר מ־{{limit}} תוצאות | ||||||
| find_match_count_limit[few]=יותר מ־{{limit}} תוצאות | find_match_count_limit[few]=יותר מ־{{limit}} תוצאות | ||||||
| find_match_count_limit[many]=יותר מ־{{limit}} תוצאות | find_match_count_limit[many]=יותר מ־{{limit}} תוצאות | ||||||
| find_match_count_limit[other]=יותר מ־{{limit}} תוצאות | find_match_count_limit[other]=יותר מ־{{limit}} תוצאות | ||||||
| find_not_found=ביטוי לא נמצא | find_not_found=הביטוי לא נמצא | ||||||
| 
 | 
 | ||||||
| # Error panel labels | # Error panel labels | ||||||
| error_more_info=מידע נוסף | error_more_info=מידע נוסף | ||||||
|  | @ -224,6 +226,10 @@ invalid_file_error=קובץ PDF פגום או לא תקין. | ||||||
| missing_file_error=קובץ PDF חסר. | missing_file_error=קובץ PDF חסר. | ||||||
| unexpected_response_error=תגובת שרת לא צפויה. | unexpected_response_error=תגובת שרת לא צפויה. | ||||||
| 
 | 
 | ||||||
|  | # LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be | ||||||
|  | # replaced by the modification date, and time, of the annotation. | ||||||
|  | annotation_date_string={{date}}, {{time}} | ||||||
|  | 
 | ||||||
| # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | ||||||
| # "{{type}}" will be replaced with an annotation type from a list defined in | # "{{type}}" will be replaced with an annotation type from a list defined in | ||||||
| # the PDF spec (32000-1:2008 Table 169 – Annotation types). | # the PDF spec (32000-1:2008 Table 169 – Annotation types). | ||||||
|  |  | ||||||
|  | @ -208,6 +208,10 @@ invalid_file_error=अमान्य या भ्रष्ट PDF फ़ाइ | ||||||
| missing_file_error=\u0020अनुपस्थित PDF फ़ाइल. | missing_file_error=\u0020अनुपस्थित PDF फ़ाइल. | ||||||
| unexpected_response_error=अप्रत्याशित सर्वर प्रतिक्रिया. | unexpected_response_error=अप्रत्याशित सर्वर प्रतिक्रिया. | ||||||
| 
 | 
 | ||||||
|  | # LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be | ||||||
|  | # replaced by the modification date, and time, of the annotation. | ||||||
|  | annotation_date_string={{date}}, {{time}} | ||||||
|  | 
 | ||||||
| # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | ||||||
| # "{{type}}" will be replaced with an annotation type from a list defined in | # "{{type}}" will be replaced with an annotation type from a list defined in | ||||||
| # the PDF spec (32000-1:2008 Table 169 – Annotation types). | # the PDF spec (32000-1:2008 Table 169 – Annotation types). | ||||||
|  |  | ||||||
|  | @ -65,7 +65,19 @@ cursor_text_select_tool_label=Alat za označavanje teksta | ||||||
| cursor_hand_tool.title=Omogući ručni alat | cursor_hand_tool.title=Omogući ručni alat | ||||||
| cursor_hand_tool_label=Ručni alat | cursor_hand_tool_label=Ručni alat | ||||||
| 
 | 
 | ||||||
|  | scroll_vertical.title=Koristi okomito pomicanje | ||||||
|  | scroll_vertical_label=Okomito pomicanje | ||||||
|  | scroll_horizontal.title=Koristi vodoravno pomicanje | ||||||
|  | scroll_horizontal_label=Vodoravno pomicanje | ||||||
|  | scroll_wrapped.title=Koristi omotano pomicanje | ||||||
|  | scroll_wrapped_label=Omotano pomicanje | ||||||
| 
 | 
 | ||||||
|  | spread_none.title=Ne pridružuj razmake stranica | ||||||
|  | spread_none_label=Bez razmaka | ||||||
|  | spread_odd.title=Pridruži razmake stranica počinjući od neparnih stranica | ||||||
|  | spread_odd_label=Neparni razmaci | ||||||
|  | spread_even.title=Pridruži razmake stranica počinjući od parnih stranica | ||||||
|  | spread_even_label=Parni razmaci | ||||||
| 
 | 
 | ||||||
| # Document properties dialog box | # Document properties dialog box | ||||||
| document_properties.title=Svojstva dokumenta... | document_properties.title=Svojstva dokumenta... | ||||||
|  | @ -91,8 +103,15 @@ document_properties_creator=Stvaratelj: | ||||||
| document_properties_producer=PDF stvaratelj: | document_properties_producer=PDF stvaratelj: | ||||||
| document_properties_version=PDF inačica: | document_properties_version=PDF inačica: | ||||||
| document_properties_page_count=Broj stranica: | document_properties_page_count=Broj stranica: | ||||||
|  | document_properties_page_size=Dimenzije stranice: | ||||||
|  | document_properties_page_size_unit_inches=in | ||||||
|  | document_properties_page_size_unit_millimeters=mm | ||||||
|  | document_properties_page_size_orientation_portrait=portret | ||||||
|  | document_properties_page_size_orientation_landscape=pejzaž | ||||||
| document_properties_page_size_name_a3=A3 | document_properties_page_size_name_a3=A3 | ||||||
| document_properties_page_size_name_a4=A4 | document_properties_page_size_name_a4=A4 | ||||||
|  | document_properties_page_size_name_letter=Pismo | ||||||
|  | document_properties_page_size_name_legal=Pravno | ||||||
| # LOCALIZATION NOTE (document_properties_page_size_dimension_string): | # LOCALIZATION NOTE (document_properties_page_size_dimension_string): | ||||||
| # "{{width}}", "{{height}}", {{unit}}, and {{orientation}} will be replaced by | # "{{width}}", "{{height}}", {{unit}}, and {{orientation}} will be replaced by | ||||||
| # the size, respectively their unit of measurement and orientation, of the (current) page. | # the size, respectively their unit of measurement and orientation, of the (current) page. | ||||||
|  | @ -103,6 +122,7 @@ document_properties_page_size_dimension_string={{width}} × {{height}} {{unit}} | ||||||
| document_properties_page_size_dimension_name_string={{width}} × {{height}} {{unit}} ({{name}}, {{orientation}}) | document_properties_page_size_dimension_name_string={{width}} × {{height}} {{unit}} ({{name}}, {{orientation}}) | ||||||
| # LOCALIZATION NOTE (document_properties_linearized): The linearization status of | # LOCALIZATION NOTE (document_properties_linearized): The linearization status of | ||||||
| # the document; usually called "Fast Web View" in English locales of Adobe software. | # the document; usually called "Fast Web View" in English locales of Adobe software. | ||||||
|  | document_properties_linearized=Brzi web pregled: | ||||||
| document_properties_linearized_yes=Da | document_properties_linearized_yes=Da | ||||||
| document_properties_linearized_no=Ne | document_properties_linearized_no=Ne | ||||||
| document_properties_close=Zatvori | document_properties_close=Zatvori | ||||||
|  | @ -145,6 +165,7 @@ find_next.title=Pronađi iduće javljanje ovog izraza | ||||||
| find_next_label=Sljedeće | find_next_label=Sljedeće | ||||||
| find_highlight=Istankni sve | find_highlight=Istankni sve | ||||||
| find_match_case_label=Slučaj podudaranja | find_match_case_label=Slučaj podudaranja | ||||||
|  | find_entire_word_label=Cijele riječi | ||||||
| find_reached_top=Dosegnut vrh dokumenta, nastavak od dna | find_reached_top=Dosegnut vrh dokumenta, nastavak od dna | ||||||
| find_reached_bottom=Dosegnut vrh dokumenta, nastavak od vrha | find_reached_bottom=Dosegnut vrh dokumenta, nastavak od vrha | ||||||
| # LOCALIZATION NOTE (find_match_count): The supported plural forms are | # LOCALIZATION NOTE (find_match_count): The supported plural forms are | ||||||
|  | @ -152,9 +173,22 @@ find_reached_bottom=Dosegnut vrh dokumenta, nastavak od vrha | ||||||
| # "{{current}}" and "{{total}}" will be replaced by a number representing the | # "{{current}}" and "{{total}}" will be replaced by a number representing the | ||||||
| # index of the currently active find result, respectively a number representing | # index of the currently active find result, respectively a number representing | ||||||
| # the total number of matches in the document. | # the total number of matches in the document. | ||||||
|  | find_match_count={[ plural(total) ]} | ||||||
|  | find_match_count[one]={{current}} od {{total}} se podudara | ||||||
|  | find_match_count[two]={{current}} od {{total}} se podudara | ||||||
|  | find_match_count[few]={{current}} od {{total}} se podudara | ||||||
|  | find_match_count[many]={{current}} od {{total}} se podudara | ||||||
|  | find_match_count[other]={{current}} od {{total}} se podudara | ||||||
| # LOCALIZATION NOTE (find_match_count_limit): The supported plural forms are | # LOCALIZATION NOTE (find_match_count_limit): The supported plural forms are | ||||||
| # [zero|one|two|few|many|other], with [other] as the default value. | # [zero|one|two|few|many|other], with [other] as the default value. | ||||||
| # "{{limit}}" will be replaced by a numerical value. | # "{{limit}}" will be replaced by a numerical value. | ||||||
|  | find_match_count_limit={[ plural(limit) ]} | ||||||
|  | find_match_count_limit[zero]=Više od {{limit}} podudaranja | ||||||
|  | find_match_count_limit[one]=Više od {{limit}} podudaranja | ||||||
|  | find_match_count_limit[two]=Više od {{limit}} podudaranja | ||||||
|  | find_match_count_limit[few]=Više od {{limit}} podudaranja | ||||||
|  | find_match_count_limit[many]=Više od {{limit}} podudaranja | ||||||
|  | find_match_count_limit[other]=Više od {{limit}} podudaranja | ||||||
| find_not_found=Izraz nije pronađen | find_not_found=Izraz nije pronađen | ||||||
| 
 | 
 | ||||||
| # Error panel labels | # Error panel labels | ||||||
|  | @ -192,6 +226,10 @@ invalid_file_error=Kriva ili oštećena PDF datoteka. | ||||||
| missing_file_error=Nedostaje PDF datoteka. | missing_file_error=Nedostaje PDF datoteka. | ||||||
| unexpected_response_error=Neočekivani odgovor poslužitelja. | unexpected_response_error=Neočekivani odgovor poslužitelja. | ||||||
| 
 | 
 | ||||||
|  | # LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be | ||||||
|  | # replaced by the modification date, and time, of the annotation. | ||||||
|  | annotation_date_string={{date}}, {{time}} | ||||||
|  | 
 | ||||||
| # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | ||||||
| # "{{type}}" will be replaced with an annotation type from a list defined in | # "{{type}}" will be replaced with an annotation type from a list defined in | ||||||
| # the PDF spec (32000-1:2008 Table 169 – Annotation types). | # the PDF spec (32000-1:2008 Table 169 – Annotation types). | ||||||
|  |  | ||||||
|  | @ -226,6 +226,10 @@ invalid_file_error=Njepłaćiwa abo wobškodźena PDF-dataja. | ||||||
| missing_file_error=Falowaca PDF-dataja. | missing_file_error=Falowaca PDF-dataja. | ||||||
| unexpected_response_error=Njewočakowana serwerowa wotmołwa. | unexpected_response_error=Njewočakowana serwerowa wotmołwa. | ||||||
| 
 | 
 | ||||||
|  | # LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be | ||||||
|  | # replaced by the modification date, and time, of the annotation. | ||||||
|  | annotation_date_string={{date}}, {{time}} | ||||||
|  | 
 | ||||||
| # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | ||||||
| # "{{type}}" will be replaced with an annotation type from a list defined in | # "{{type}}" will be replaced with an annotation type from a list defined in | ||||||
| # the PDF spec (32000-1:2008 Table 169 – Annotation types). | # the PDF spec (32000-1:2008 Table 169 – Annotation types). | ||||||
|  |  | ||||||
|  | @ -226,6 +226,10 @@ invalid_file_error=Érvénytelen vagy sérült PDF fájl. | ||||||
| missing_file_error=Hiányzó PDF fájl. | missing_file_error=Hiányzó PDF fájl. | ||||||
| unexpected_response_error=Váratlan kiszolgálóválasz. | unexpected_response_error=Váratlan kiszolgálóválasz. | ||||||
| 
 | 
 | ||||||
|  | # LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be | ||||||
|  | # replaced by the modification date, and time, of the annotation. | ||||||
|  | annotation_date_string={{date}}, {{time}} | ||||||
|  | 
 | ||||||
| # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | ||||||
| # "{{type}}" will be replaced with an annotation type from a list defined in | # "{{type}}" will be replaced with an annotation type from a list defined in | ||||||
| # the PDF spec (32000-1:2008 Table 169 – Annotation types). | # the PDF spec (32000-1:2008 Table 169 – Annotation types). | ||||||
|  |  | ||||||
|  | @ -226,6 +226,10 @@ invalid_file_error=File PDF corrumpite o non valide. | ||||||
| missing_file_error=File PDF mancante. | missing_file_error=File PDF mancante. | ||||||
| unexpected_response_error=Responsa del servitor inexpectate. | unexpected_response_error=Responsa del servitor inexpectate. | ||||||
| 
 | 
 | ||||||
|  | # LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be | ||||||
|  | # replaced by the modification date, and time, of the annotation. | ||||||
|  | annotation_date_string={{date}}, {{time}} | ||||||
|  | 
 | ||||||
| # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | ||||||
| # "{{type}}" will be replaced with an annotation type from a list defined in | # "{{type}}" will be replaced with an annotation type from a list defined in | ||||||
| # the PDF spec (32000-1:2008 Table 169 – Annotation types). | # the PDF spec (32000-1:2008 Table 169 – Annotation types). | ||||||
|  |  | ||||||
|  | @ -226,6 +226,10 @@ invalid_file_error=Berkas PDF tidak valid atau rusak. | ||||||
| missing_file_error=Berkas PDF tidak ada. | missing_file_error=Berkas PDF tidak ada. | ||||||
| unexpected_response_error=Balasan server yang tidak diharapkan. | unexpected_response_error=Balasan server yang tidak diharapkan. | ||||||
| 
 | 
 | ||||||
|  | # LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be | ||||||
|  | # replaced by the modification date, and time, of the annotation. | ||||||
|  | annotation_date_string={{date}}, {{time}} | ||||||
|  | 
 | ||||||
| # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | ||||||
| # "{{type}}" will be replaced with an annotation type from a list defined in | # "{{type}}" will be replaced with an annotation type from a list defined in | ||||||
| # the PDF spec (32000-1:2008 Table 169 – Annotation types). | # the PDF spec (32000-1:2008 Table 169 – Annotation types). | ||||||
|  |  | ||||||
|  | @ -65,7 +65,17 @@ cursor_text_select_tool_label=Textavalsáhald | ||||||
| cursor_hand_tool.title=Virkja handarverkfæri | cursor_hand_tool.title=Virkja handarverkfæri | ||||||
| cursor_hand_tool_label=Handarverkfæri | cursor_hand_tool_label=Handarverkfæri | ||||||
| 
 | 
 | ||||||
|  | scroll_vertical.title=Nota lóðrétt skrun | ||||||
|  | scroll_vertical_label=Lóðrétt skrun | ||||||
|  | scroll_horizontal.title=Nota lárétt skrun | ||||||
|  | scroll_horizontal_label=Lárétt skrun | ||||||
| 
 | 
 | ||||||
|  | spread_none.title=Ekki taka þátt í dreifingu síðna | ||||||
|  | spread_none_label=Engin dreifing | ||||||
|  | spread_odd.title=Taka þátt í dreifingu síðna með oddatölum | ||||||
|  | spread_odd_label=Oddatöludreifing | ||||||
|  | spread_even.title=Taktu þátt í dreifingu síðna með jöfnuntölum | ||||||
|  | spread_even_label=Jafnatöludreifing | ||||||
| 
 | 
 | ||||||
| # Document properties dialog box | # Document properties dialog box | ||||||
| document_properties.title=Eiginleikar skjals… | document_properties.title=Eiginleikar skjals… | ||||||
|  | @ -161,10 +171,21 @@ find_reached_bottom=Náði enda skjals, held áfram efst | ||||||
| # index of the currently active find result, respectively a number representing | # index of the currently active find result, respectively a number representing | ||||||
| # the total number of matches in the document. | # the total number of matches in the document. | ||||||
| find_match_count={[ plural(total) ]} | find_match_count={[ plural(total) ]} | ||||||
|  | find_match_count[one]={{current}} af {{total}} niðurstöðu | ||||||
|  | find_match_count[two]={{current}} af {{total}} niðurstöðum | ||||||
|  | find_match_count[few]={{current}} af {{total}} niðurstöðum | ||||||
|  | find_match_count[many]={{current}} af {{total}} niðurstöðum | ||||||
|  | find_match_count[other]={{current}} af {{total}} niðurstöðum | ||||||
| # LOCALIZATION NOTE (find_match_count_limit): The supported plural forms are | # LOCALIZATION NOTE (find_match_count_limit): The supported plural forms are | ||||||
| # [zero|one|two|few|many|other], with [other] as the default value. | # [zero|one|two|few|many|other], with [other] as the default value. | ||||||
| # "{{limit}}" will be replaced by a numerical value. | # "{{limit}}" will be replaced by a numerical value. | ||||||
| find_match_count_limit={[ plural(limit) ]} | find_match_count_limit={[ plural(limit) ]} | ||||||
|  | find_match_count_limit[zero]=Fleiri en {{limit}} niðurstöður | ||||||
|  | find_match_count_limit[one]=Fleiri en {{limit}} niðurstaða | ||||||
|  | find_match_count_limit[two]=Fleiri en {{limit}} niðurstöður | ||||||
|  | find_match_count_limit[few]=Fleiri en {{limit}} niðurstöður | ||||||
|  | find_match_count_limit[many]=Fleiri en {{limit}} niðurstöður | ||||||
|  | find_match_count_limit[other]=Fleiri en {{limit}} niðurstöður | ||||||
| find_not_found=Fann ekki orðið | find_not_found=Fann ekki orðið | ||||||
| 
 | 
 | ||||||
| # Error panel labels | # Error panel labels | ||||||
|  |  | ||||||
|  | @ -146,6 +146,7 @@ loading_error = Si è verificato un errore durante il caricamento del PDF. | ||||||
| invalid_file_error = File PDF non valido o danneggiato. | invalid_file_error = File PDF non valido o danneggiato. | ||||||
| missing_file_error = File PDF non disponibile. | missing_file_error = File PDF non disponibile. | ||||||
| unexpected_response_error = Risposta imprevista del server | unexpected_response_error = Risposta imprevista del server | ||||||
|  | annotation_date_string = {{date}}, {{time}} | ||||||
| text_annotation_type.alt = [Annotazione: {{type}}] | text_annotation_type.alt = [Annotazione: {{type}}] | ||||||
| password_label = Inserire la password per aprire questo file PDF. | password_label = Inserire la password per aprire questo file PDF. | ||||||
| password_invalid = Password non corretta. Riprovare. | password_invalid = Password non corretta. Riprovare. | ||||||
|  |  | ||||||
|  | @ -226,6 +226,10 @@ invalid_file_error=無効または破損した PDF ファイル。 | ||||||
| missing_file_error=PDF ファイルが見つかりません。 | missing_file_error=PDF ファイルが見つかりません。 | ||||||
| unexpected_response_error=サーバーから予期せぬ応答がありました。 | unexpected_response_error=サーバーから予期せぬ応答がありました。 | ||||||
| 
 | 
 | ||||||
|  | # LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be | ||||||
|  | # replaced by the modification date, and time, of the annotation. | ||||||
|  | annotation_date_string={{date}}, {{time}} | ||||||
|  | 
 | ||||||
| # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | ||||||
| # "{{type}}" will be replaced with an annotation type from a list defined in | # "{{type}}" will be replaced with an annotation type from a list defined in | ||||||
| # the PDF spec (32000-1:2008 Table 169 – Annotation types). | # the PDF spec (32000-1:2008 Table 169 – Annotation types). | ||||||
|  |  | ||||||
|  | @ -100,8 +100,8 @@ document_properties_modification_date=ჩასწორების თარ | ||||||
| # will be replaced by the creation/modification date, and time, of the PDF file. | # will be replaced by the creation/modification date, and time, of the PDF file. | ||||||
| document_properties_date_string={{date}}, {{time}} | document_properties_date_string={{date}}, {{time}} | ||||||
| document_properties_creator=გამომშვები: | document_properties_creator=გამომშვები: | ||||||
| document_properties_producer=PDF გამომშვები: | document_properties_producer=PDF-გამომშვები: | ||||||
| document_properties_version=PDF ვერსია: | document_properties_version=PDF-ვერსია: | ||||||
| document_properties_page_count=გვერდების რაოდენობა: | document_properties_page_count=გვერდების რაოდენობა: | ||||||
| document_properties_page_size=გვერდის ზომა: | document_properties_page_size=გვერდის ზომა: | ||||||
| document_properties_page_size_unit_inches=დუიმი | document_properties_page_size_unit_inches=დუიმი | ||||||
|  | @ -122,7 +122,7 @@ document_properties_page_size_dimension_string={{width}} × {{height}} {{unit}} | ||||||
| document_properties_page_size_dimension_name_string={{width}} × {{height}} {{unit}} ({{name}}, {{orientation}}) | document_properties_page_size_dimension_name_string={{width}} × {{height}} {{unit}} ({{name}}, {{orientation}}) | ||||||
| # LOCALIZATION NOTE (document_properties_linearized): The linearization status of | # LOCALIZATION NOTE (document_properties_linearized): The linearization status of | ||||||
| # the document; usually called "Fast Web View" in English locales of Adobe software. | # the document; usually called "Fast Web View" in English locales of Adobe software. | ||||||
| document_properties_linearized=Fast Web View: | document_properties_linearized=სწრაფი შეთვალიერება: | ||||||
| document_properties_linearized_yes=დიახ | document_properties_linearized_yes=დიახ | ||||||
| document_properties_linearized_no=არა | document_properties_linearized_no=არა | ||||||
| document_properties_close=დახურვა | document_properties_close=დახურვა | ||||||
|  | @ -154,7 +154,7 @@ findbar_label=ძიება | ||||||
| thumb_page_title=გვერდი {{page}} | thumb_page_title=გვერდი {{page}} | ||||||
| # LOCALIZATION NOTE (thumb_page_canvas): "{{page}}" will be replaced by the page | # LOCALIZATION NOTE (thumb_page_canvas): "{{page}}" will be replaced by the page | ||||||
| # number. | # number. | ||||||
| thumb_page_canvas=გვერდის ესკიზი {{page}} | thumb_page_canvas=გვერდის შეთვალიერება {{page}} | ||||||
| 
 | 
 | ||||||
| # Find panel button title and messages | # Find panel button title and messages | ||||||
| find_input.title=ძიება | find_input.title=ძიება | ||||||
|  | @ -221,22 +221,26 @@ page_scale_percent={{scale}}% | ||||||
| 
 | 
 | ||||||
| # Loading indicator messages | # Loading indicator messages | ||||||
| loading_error_indicator=შეცდომა | loading_error_indicator=შეცდომა | ||||||
| loading_error=შეცდომა, PDF ფაილის ჩატვირთვისას. | loading_error=შეცდომა, PDF-ფაილის ჩატვირთვისას. | ||||||
| invalid_file_error=არამართებული ან დაზიანებული PDF ფაილი. | invalid_file_error=არამართებული ან დაზიანებული PDF-ფაილი. | ||||||
| missing_file_error=ნაკლული PDF ფაილი. | missing_file_error=ნაკლული PDF-ფაილი. | ||||||
| unexpected_response_error=სერვერის მოულოდნელი პასუხი. | unexpected_response_error=სერვერის მოულოდნელი პასუხი. | ||||||
| 
 | 
 | ||||||
|  | # LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be | ||||||
|  | # replaced by the modification date, and time, of the annotation. | ||||||
|  | annotation_date_string={{date}}, {{time}} | ||||||
|  | 
 | ||||||
| # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | ||||||
| # "{{type}}" will be replaced with an annotation type from a list defined in | # "{{type}}" will be replaced with an annotation type from a list defined in | ||||||
| # the PDF spec (32000-1:2008 Table 169 – Annotation types). | # the PDF spec (32000-1:2008 Table 169 – Annotation types). | ||||||
| # Some common types are e.g.: "Check", "Text", "Comment", "Note" | # Some common types are e.g.: "Check", "Text", "Comment", "Note" | ||||||
| text_annotation_type.alt=[{{type}} შენიშვნა] | text_annotation_type.alt=[{{type}} შენიშვნა] | ||||||
| password_label=შეიყვანეთ პაროლი PDF ფაილის გასახსნელად. | password_label=შეიყვანეთ პაროლი PDF-ფაილის გასახსნელად. | ||||||
| password_invalid=არასწორი პაროლი. გთხოვთ, სცადოთ ხელახლა. | password_invalid=არასწორი პაროლი. გთხოვთ, სცადოთ ხელახლა. | ||||||
| password_ok=კარგი | password_ok=კარგი | ||||||
| password_cancel=გაუქმება | password_cancel=გაუქმება | ||||||
| 
 | 
 | ||||||
| printing_not_supported=გაფრთხილება: ამობეჭდვა ამ ბრაუზერში არაა სრულად მხარდაჭერილი. | printing_not_supported=გაფრთხილება: ამობეჭდვა ამ ბრაუზერში არაა სრულად მხარდაჭერილი. | ||||||
| printing_not_ready=გაფრთხილება: PDF სრულად ჩატვირთული არაა, ამობეჭდვის დასაწყებად. | printing_not_ready=გაფრთხილება: PDF სრულად ჩატვირთული არაა, ამობეჭდვის დასაწყებად. | ||||||
| web_fonts_disabled=ვებშრიფტები გამორთულია: ჩაშენებული PDF შრიფტების გამოყენება ვერ ხერხდება. | web_fonts_disabled=ვებშრიფტები გამორთულია: ჩაშენებული PDF-შრიფტების გამოყენება ვერ ხერხდება. | ||||||
| document_colors_not_allowed=PDF დოკუმენტებს არ აქვს საკუთარი ფერების გამოყენების ნებართვა: ბრაუზერში გამორთულია “გვერდებისთვის საკუთარი ფერების გამოყენების უფლება”. | document_colors_not_allowed=PDF-დოკუმენტებს არ აქვს საკუთარი ფერების გამოყენების ნებართვა: ბრაუზერში გამორთულია “გვერდებისთვის საკუთარი ფერების გამოყენების უფლება”. | ||||||
|  |  | ||||||
|  | @ -226,6 +226,10 @@ invalid_file_error=Afaylu PDF arameɣtu neɣ yexṣeṛ. | ||||||
| missing_file_error=Ulac afaylu PDF. | missing_file_error=Ulac afaylu PDF. | ||||||
| unexpected_response_error=Aqeddac yerra-d yir tiririt ur nettwaṛǧi ara. | unexpected_response_error=Aqeddac yerra-d yir tiririt ur nettwaṛǧi ara. | ||||||
| 
 | 
 | ||||||
|  | # LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be | ||||||
|  | # replaced by the modification date, and time, of the annotation. | ||||||
|  | annotation_date_string={{date}}, {{time}} | ||||||
|  | 
 | ||||||
| # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | ||||||
| # "{{type}}" will be replaced with an annotation type from a list defined in | # "{{type}}" will be replaced with an annotation type from a list defined in | ||||||
| # the PDF spec (32000-1:2008 Table 169 – Annotation types). | # the PDF spec (32000-1:2008 Table 169 – Annotation types). | ||||||
|  |  | ||||||
|  | @ -226,6 +226,10 @@ invalid_file_error=Зақымдалған немесе қате PDF файл. | ||||||
| missing_file_error=PDF файлы жоқ. | missing_file_error=PDF файлы жоқ. | ||||||
| unexpected_response_error=Сервердің күтпеген жауабы. | unexpected_response_error=Сервердің күтпеген жауабы. | ||||||
| 
 | 
 | ||||||
|  | # LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be | ||||||
|  | # replaced by the modification date, and time, of the annotation. | ||||||
|  | annotation_date_string={{date}}, {{time}} | ||||||
|  | 
 | ||||||
| # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | ||||||
| # "{{type}}" will be replaced with an annotation type from a list defined in | # "{{type}}" will be replaced with an annotation type from a list defined in | ||||||
| # the PDF spec (32000-1:2008 Table 169 – Annotation types). | # the PDF spec (32000-1:2008 Table 169 – Annotation types). | ||||||
|  |  | ||||||
|  | @ -26,23 +26,23 @@ of_pages=전체 {{pagesCount}} | ||||||
| # LOCALIZATION NOTE (page_of_pages): "{{pageNumber}}" and "{{pagesCount}}" | # LOCALIZATION NOTE (page_of_pages): "{{pageNumber}}" and "{{pagesCount}}" | ||||||
| # will be replaced by a number representing the currently visible page, | # will be replaced by a number representing the currently visible page, | ||||||
| # respectively a number representing the total number of pages in the document. | # respectively a number representing the total number of pages in the document. | ||||||
| page_of_pages=({{pagesCount}} 중 {{pageNumber}}) | page_of_pages=({{pageNumber}} / {{pagesCount}}) | ||||||
| 
 | 
 | ||||||
| zoom_out.title=축소 | zoom_out.title=축소 | ||||||
| zoom_out_label=축소 | zoom_out_label=축소 | ||||||
| zoom_in.title=확대 | zoom_in.title=확대 | ||||||
| zoom_in_label=확대 | zoom_in_label=확대 | ||||||
| zoom.title=크기 | zoom.title=확대/축소 | ||||||
| presentation_mode.title=발표 모드로 전환 | presentation_mode.title=프레젠테이션 모드로 전환 | ||||||
| presentation_mode_label=발표 모드 | presentation_mode_label=프레젠테이션 모드 | ||||||
| open_file.title=파일 열기 | open_file.title=파일 열기 | ||||||
| open_file_label=열기 | open_file_label=열기 | ||||||
| print.title=인쇄 | print.title=인쇄 | ||||||
| print_label=인쇄 | print_label=인쇄 | ||||||
| download.title=다운로드 | download.title=다운로드 | ||||||
| download_label=다운로드 | download_label=다운로드 | ||||||
| bookmark.title=지금 보이는 그대로 (복사하거나 새 창에 열기) | bookmark.title=현재 뷰 (복사하거나 새 창에 열기) | ||||||
| bookmark_label=지금 보이는 그대로 | bookmark_label=현재 뷰 | ||||||
| 
 | 
 | ||||||
| # Secondary toolbar and context menu | # Secondary toolbar and context menu | ||||||
| tools.title=도구 | tools.title=도구 | ||||||
|  | @ -83,7 +83,7 @@ spread_even_label=짝수 펼쳐짐 | ||||||
| document_properties.title=문서 속성… | document_properties.title=문서 속성… | ||||||
| document_properties_label=문서 속성… | document_properties_label=문서 속성… | ||||||
| document_properties_file_name=파일 이름: | document_properties_file_name=파일 이름: | ||||||
| document_properties_file_size=파일 사이즈: | document_properties_file_size=파일 크기: | ||||||
| # LOCALIZATION NOTE (document_properties_kb): "{{size_kb}}" and "{{size_b}}" | # LOCALIZATION NOTE (document_properties_kb): "{{size_kb}}" and "{{size_b}}" | ||||||
| # will be replaced by the PDF file size in kilobytes, respectively in bytes. | # will be replaced by the PDF file size in kilobytes, respectively in bytes. | ||||||
| document_properties_kb={{size_kb}} KB ({{size_b}}바이트) | document_properties_kb={{size_kb}} KB ({{size_b}}바이트) | ||||||
|  | @ -91,18 +91,18 @@ document_properties_kb={{size_kb}} KB ({{size_b}}바이트) | ||||||
| # will be replaced by the PDF file size in megabytes, respectively in bytes. | # will be replaced by the PDF file size in megabytes, respectively in bytes. | ||||||
| document_properties_mb={{size_mb}} MB ({{size_b}}바이트) | document_properties_mb={{size_mb}} MB ({{size_b}}바이트) | ||||||
| document_properties_title=제목: | document_properties_title=제목: | ||||||
| document_properties_author=저자: | document_properties_author=작성자: | ||||||
| document_properties_subject=주제: | document_properties_subject=주제: | ||||||
| document_properties_keywords=키워드: | document_properties_keywords=키워드: | ||||||
| document_properties_creation_date=생성일: | document_properties_creation_date=작성 날짜: | ||||||
| document_properties_modification_date=수정일: | document_properties_modification_date=수정 날짜: | ||||||
| # LOCALIZATION NOTE (document_properties_date_string): "{{date}}" and "{{time}}" | # LOCALIZATION NOTE (document_properties_date_string): "{{date}}" and "{{time}}" | ||||||
| # will be replaced by the creation/modification date, and time, of the PDF file. | # will be replaced by the creation/modification date, and time, of the PDF file. | ||||||
| document_properties_date_string={{date}}, {{time}} | document_properties_date_string={{date}}, {{time}} | ||||||
| document_properties_creator=생성자: | document_properties_creator=작성 프로그램: | ||||||
| document_properties_producer=PDF 생성기: | document_properties_producer=PDF 변환 소프트웨어: | ||||||
| document_properties_version=PDF 버전: | document_properties_version=PDF 버전: | ||||||
| document_properties_page_count=총 페이지: | document_properties_page_count=페이지 수: | ||||||
| document_properties_page_size=페이지 크기: | document_properties_page_size=페이지 크기: | ||||||
| document_properties_page_size_unit_inches=in | document_properties_page_size_unit_inches=in | ||||||
| document_properties_page_size_unit_millimeters=mm | document_properties_page_size_unit_millimeters=mm | ||||||
|  | @ -127,7 +127,7 @@ document_properties_linearized_yes=예 | ||||||
| document_properties_linearized_no=아니오 | document_properties_linearized_no=아니오 | ||||||
| document_properties_close=닫기 | document_properties_close=닫기 | ||||||
| 
 | 
 | ||||||
| print_progress_message=문서 출력 준비중… | print_progress_message=인쇄 문서 준비중… | ||||||
| # LOCALIZATION NOTE (print_progress_percent): "{{progress}}" will be replaced by | # LOCALIZATION NOTE (print_progress_percent): "{{progress}}" will be replaced by | ||||||
| # a numerical per cent value. | # a numerical per cent value. | ||||||
| print_progress_percent={{progress}}% | print_progress_percent={{progress}}% | ||||||
|  | @ -151,10 +151,10 @@ findbar_label=검색 | ||||||
| # Thumbnails panel item (tooltip and alt text for images) | # Thumbnails panel item (tooltip and alt text for images) | ||||||
| # LOCALIZATION NOTE (thumb_page_title): "{{page}}" will be replaced by the page | # LOCALIZATION NOTE (thumb_page_title): "{{page}}" will be replaced by the page | ||||||
| # number. | # number. | ||||||
| thumb_page_title={{page}}쪽 | thumb_page_title={{page}} 페이지 | ||||||
| # LOCALIZATION NOTE (thumb_page_canvas): "{{page}}" will be replaced by the page | # LOCALIZATION NOTE (thumb_page_canvas): "{{page}}" will be replaced by the page | ||||||
| # number. | # number. | ||||||
| thumb_page_canvas={{page}}쪽 미리보기 | thumb_page_canvas={{page}} 페이지 미리보기 | ||||||
| 
 | 
 | ||||||
| # Find panel button title and messages | # Find panel button title and messages | ||||||
| find_input.title=찾기 | find_input.title=찾기 | ||||||
|  | @ -164,7 +164,7 @@ find_previous_label=이전 | ||||||
| find_next.title=지정 문자열에 일치하는 다음 부분을 검색 | find_next.title=지정 문자열에 일치하는 다음 부분을 검색 | ||||||
| find_next_label=다음 | find_next_label=다음 | ||||||
| find_highlight=모두 강조 표시 | find_highlight=모두 강조 표시 | ||||||
| find_match_case_label=대문자/소문자 구별 | find_match_case_label=대/소문자 구분 | ||||||
| find_entire_word_label=전체 단어 | find_entire_word_label=전체 단어 | ||||||
| find_reached_top=문서 처음까지 검색하고 끝으로 돌아와 검색했습니다. | find_reached_top=문서 처음까지 검색하고 끝으로 돌아와 검색했습니다. | ||||||
| find_reached_bottom=문서 끝까지 검색하고 앞으로 돌아와 검색했습니다. | find_reached_bottom=문서 끝까지 검색하고 앞으로 돌아와 검색했습니다. | ||||||
|  | @ -208,12 +208,12 @@ error_stack=스택: {{stack}} | ||||||
| error_file=파일: {{file}} | error_file=파일: {{file}} | ||||||
| # LOCALIZATION NOTE (error_line): "{{line}}" will be replaced with a line number | # LOCALIZATION NOTE (error_line): "{{line}}" will be replaced with a line number | ||||||
| error_line=줄 번호: {{line}} | error_line=줄 번호: {{line}} | ||||||
| rendering_error=페이지를 렌더링하다 오류가 났습니다. | rendering_error=페이지를 렌더링하는 중 오류가 발생했습니다. | ||||||
| 
 | 
 | ||||||
| # Predefined zoom values | # Predefined zoom values | ||||||
| page_scale_width=페이지 너비에 맞춤 | page_scale_width=페이지 너비에 맞춤 | ||||||
| page_scale_fit=페이지에 맞춤 | page_scale_fit=페이지에 맞춤 | ||||||
| page_scale_auto=알아서 맞춤 | page_scale_auto=자동 맞춤 | ||||||
| page_scale_actual=실제 크기에 맞춤 | page_scale_actual=실제 크기에 맞춤 | ||||||
| # LOCALIZATION NOTE (page_scale_percent): "{{scale}}" will be replaced by a | # LOCALIZATION NOTE (page_scale_percent): "{{scale}}" will be replaced by a | ||||||
| # numerical scale value. | # numerical scale value. | ||||||
|  | @ -221,22 +221,26 @@ page_scale_percent={{scale}}% | ||||||
| 
 | 
 | ||||||
| # Loading indicator messages | # Loading indicator messages | ||||||
| loading_error_indicator=오류 | loading_error_indicator=오류 | ||||||
| loading_error=PDF를 읽는 중 오류가 생겼습니다. | loading_error=PDF를 로드하는 중 오류가 발생했습니다. | ||||||
| invalid_file_error=유효하지 않거나 파손된 PDF 파일 | invalid_file_error=유효하지 않거나 파손된 PDF 파일 | ||||||
| missing_file_error=PDF 파일이 없습니다. | missing_file_error=PDF 파일이 없습니다. | ||||||
| unexpected_response_error=알 수 없는 서버 응답입니다. | unexpected_response_error=예상치 못한 서버 응답입니다. | ||||||
|  | 
 | ||||||
|  | # LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be | ||||||
|  | # replaced by the modification date, and time, of the annotation. | ||||||
|  | annotation_date_string={{date}} {{time}} | ||||||
| 
 | 
 | ||||||
| # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | ||||||
| # "{{type}}" will be replaced with an annotation type from a list defined in | # "{{type}}" will be replaced with an annotation type from a list defined in | ||||||
| # the PDF spec (32000-1:2008 Table 169 – Annotation types). | # the PDF spec (32000-1:2008 Table 169 – Annotation types). | ||||||
| # Some common types are e.g.: "Check", "Text", "Comment", "Note" | # Some common types are e.g.: "Check", "Text", "Comment", "Note" | ||||||
| text_annotation_type.alt=[{{type}} 주석] | text_annotation_type.alt=[{{type}} 주석] | ||||||
| password_label=이 PDF 파일을 열 수 있는 암호를 입력하십시오. | password_label=이 PDF 파일을 열 수 있는 비밀번호를 입력하십시오. | ||||||
| password_invalid=잘못된 암호입니다. 다시 시도해 주십시오. | password_invalid=잘못된 비밀번호입니다. 다시 시도해 주십시오. | ||||||
| password_ok=확인 | password_ok=확인 | ||||||
| password_cancel=취소 | password_cancel=취소 | ||||||
| 
 | 
 | ||||||
| printing_not_supported=경고: 이 브라우저는 인쇄를 완전히 지원하지 않습니다. | printing_not_supported=경고: 이 브라우저는 인쇄를 완전히 지원하지 않습니다. | ||||||
| printing_not_ready=경고: 이 PDF를 인쇄를 할 수 있을 정도로 읽어들이지 못했습니다. | printing_not_ready=경고: 이 PDF를 인쇄를 할 수 있을 정도로 읽어들이지 못했습니다. | ||||||
| web_fonts_disabled=웹 폰트가 꺼져있음: 내장된 PDF 글꼴을 쓸 수 없습니다. | web_fonts_disabled=웹 폰트가 비활성화됨: 내장된 PDF 글꼴을 사용할 수 없습니다. | ||||||
| document_colors_not_allowed=PDF 문서의 색상을 쓰지 못하게 되어 있음: '웹 페이지 자체 색상 사용 허용'이 브라우저에서 꺼져 있습니다. | document_colors_not_allowed=PDF 문서의 자체 색상 허용 안됨: “페이지 자체 색상 허용”이 브라우저에서 비활성화 되어 있습니다. | ||||||
|  |  | ||||||
|  | @ -45,8 +45,8 @@ bookmark.title=Vixon corente (còpia ò arvi inte 'n neuvo barcon) | ||||||
| bookmark_label=Vixon corente | bookmark_label=Vixon corente | ||||||
| 
 | 
 | ||||||
| # Secondary toolbar and context menu | # Secondary toolbar and context menu | ||||||
| tools.title=Strumenti | tools.title=Atressi | ||||||
| tools_label=Strumenti | tools_label=Atressi | ||||||
| first_page.title=Vanni a-a primma pagina | first_page.title=Vanni a-a primma pagina | ||||||
| first_page.label=Vanni a-a primma pagina | first_page.label=Vanni a-a primma pagina | ||||||
| first_page_label=Vanni a-a primma pagina | first_page_label=Vanni a-a primma pagina | ||||||
|  | @ -82,8 +82,8 @@ spread_even_label=Difuxon pari | ||||||
| # Document properties dialog box | # Document properties dialog box | ||||||
| document_properties.title=Propietæ do documento… | document_properties.title=Propietæ do documento… | ||||||
| document_properties_label=Propietæ do documento… | document_properties_label=Propietæ do documento… | ||||||
| document_properties_file_name=Nomme file: | document_properties_file_name=Nomme schedaio: | ||||||
| document_properties_file_size=Dimenscion file: | document_properties_file_size=Dimenscion schedaio: | ||||||
| # LOCALIZATION NOTE (document_properties_kb): "{{size_kb}}" and "{{size_b}}" | # LOCALIZATION NOTE (document_properties_kb): "{{size_kb}}" and "{{size_b}}" | ||||||
| # will be replaced by the PDF file size in kilobytes, respectively in bytes. | # will be replaced by the PDF file size in kilobytes, respectively in bytes. | ||||||
| document_properties_kb={{size_kb}} kB ({{size_b}} byte) | document_properties_kb={{size_kb}} kB ({{size_b}} byte) | ||||||
|  | @ -205,7 +205,7 @@ error_message=Mesaggio: {{message}} | ||||||
| # trace. | # trace. | ||||||
| error_stack=Stack: {{stack}} | error_stack=Stack: {{stack}} | ||||||
| # LOCALIZATION NOTE (error_file): "{{file}}" will be replaced with a filename | # LOCALIZATION NOTE (error_file): "{{file}}" will be replaced with a filename | ||||||
| error_file=File: {{file}} | error_file=Schedaio: {{file}} | ||||||
| # LOCALIZATION NOTE (error_line): "{{line}}" will be replaced with a line number | # LOCALIZATION NOTE (error_line): "{{line}}" will be replaced with a line number | ||||||
| error_line=Linia: {{line}} | error_line=Linia: {{line}} | ||||||
| rendering_error=Gh'é stæto 'n'erô itno rendering da pagina. | rendering_error=Gh'é stæto 'n'erô itno rendering da pagina. | ||||||
|  | @ -222,8 +222,8 @@ page_scale_percent={{scale}}% | ||||||
| # Loading indicator messages | # Loading indicator messages | ||||||
| loading_error_indicator=Erô | loading_error_indicator=Erô | ||||||
| loading_error=S'é verificou 'n'erô itno caregamento do PDF. | loading_error=S'é verificou 'n'erô itno caregamento do PDF. | ||||||
| invalid_file_error=O file PDF o l'é no valido ò aroinou. | invalid_file_error=O schedaio PDF o l'é no valido ò aroinou. | ||||||
| missing_file_error=O file PDF o no gh'é. | missing_file_error=O schedaio PDF o no gh'é. | ||||||
| unexpected_response_error=Risposta inprevista do-u server | unexpected_response_error=Risposta inprevista do-u server | ||||||
| 
 | 
 | ||||||
| # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | ||||||
|  | @ -231,7 +231,7 @@ unexpected_response_error=Risposta inprevista do-u server | ||||||
| # the PDF spec (32000-1:2008 Table 169 – Annotation types). | # the PDF spec (32000-1:2008 Table 169 – Annotation types). | ||||||
| # Some common types are e.g.: "Check", "Text", "Comment", "Note" | # Some common types are e.g.: "Check", "Text", "Comment", "Note" | ||||||
| text_annotation_type.alt=[Anotaçion: {{type}}] | text_annotation_type.alt=[Anotaçion: {{type}}] | ||||||
| password_label=Dimme a paròlla segreta pe arvî sto file PDF. | password_label=Dimme a paròlla segreta pe arvî sto schedaio PDF. | ||||||
| password_invalid=Paròlla segreta sbalia. Preuva torna. | password_invalid=Paròlla segreta sbalia. Preuva torna. | ||||||
| password_ok=Va ben | password_ok=Va ben | ||||||
| password_cancel=Anulla | password_cancel=Anulla | ||||||
|  |  | ||||||
|  | @ -226,6 +226,10 @@ invalid_file_error=Tai nėra PDF failas arba jis yra sugadintas. | ||||||
| missing_file_error=PDF failas nerastas. | missing_file_error=PDF failas nerastas. | ||||||
| unexpected_response_error=Netikėtas serverio atsakas. | unexpected_response_error=Netikėtas serverio atsakas. | ||||||
| 
 | 
 | ||||||
|  | # LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be | ||||||
|  | # replaced by the modification date, and time, of the annotation. | ||||||
|  | annotation_date_string={{date}}, {{time}} | ||||||
|  | 
 | ||||||
| # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | ||||||
| # "{{type}}" will be replaced with an annotation type from a list defined in | # "{{type}}" will be replaced with an annotation type from a list defined in | ||||||
| # the PDF spec (32000-1:2008 Table 169 – Annotation types). | # the PDF spec (32000-1:2008 Table 169 – Annotation types). | ||||||
|  |  | ||||||
|  | @ -65,6 +65,10 @@ cursor_text_select_tool_label=मजकूर निवड साधन | ||||||
| cursor_hand_tool.title=हात साधन कार्यान्वित करा | cursor_hand_tool.title=हात साधन कार्यान्वित करा | ||||||
| cursor_hand_tool_label=हस्त साधन | cursor_hand_tool_label=हस्त साधन | ||||||
| 
 | 
 | ||||||
|  | scroll_vertical.title=अनुलंब स्क्रोलिंग वापरा | ||||||
|  | scroll_vertical_label=अनुलंब स्क्रोलिंग | ||||||
|  | scroll_horizontal.title=क्षैतिज स्क्रोलिंग वापरा | ||||||
|  | scroll_horizontal_label=क्षैतिज स्क्रोलिंग | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| # Document properties dialog box | # Document properties dialog box | ||||||
|  | @ -95,6 +99,7 @@ document_properties_page_size=पृष्ठ आकार: | ||||||
| document_properties_page_size_unit_inches=इंच | document_properties_page_size_unit_inches=इंच | ||||||
| document_properties_page_size_unit_millimeters=मीमी | document_properties_page_size_unit_millimeters=मीमी | ||||||
| document_properties_page_size_orientation_portrait=उभी मांडणी | document_properties_page_size_orientation_portrait=उभी मांडणी | ||||||
|  | document_properties_page_size_orientation_landscape=आडवे | ||||||
| document_properties_page_size_name_a3=A3 | document_properties_page_size_name_a3=A3 | ||||||
| document_properties_page_size_name_a4=A4 | document_properties_page_size_name_a4=A4 | ||||||
| document_properties_page_size_name_letter=Letter | document_properties_page_size_name_letter=Letter | ||||||
|  | @ -109,6 +114,7 @@ document_properties_page_size_dimension_string={{width}} × {{height}} {{unit}} | ||||||
| document_properties_page_size_dimension_name_string={{width}} × {{height}} {{unit}} ({{name}}, {{orientation}}) | document_properties_page_size_dimension_name_string={{width}} × {{height}} {{unit}} ({{name}}, {{orientation}}) | ||||||
| # LOCALIZATION NOTE (document_properties_linearized): The linearization status of | # LOCALIZATION NOTE (document_properties_linearized): The linearization status of | ||||||
| # the document; usually called "Fast Web View" in English locales of Adobe software. | # the document; usually called "Fast Web View" in English locales of Adobe software. | ||||||
|  | document_properties_linearized=जलद वेब दृष्य: | ||||||
| document_properties_linearized_yes=हो | document_properties_linearized_yes=हो | ||||||
| document_properties_linearized_no=नाही | document_properties_linearized_no=नाही | ||||||
| document_properties_close=बंद करा | document_properties_close=बंद करा | ||||||
|  | @ -151,8 +157,23 @@ find_next.title=वाकप्रयोगची पुढील घटना  | ||||||
| find_next_label=पुढील | find_next_label=पुढील | ||||||
| find_highlight=सर्व ठळक करा | find_highlight=सर्व ठळक करा | ||||||
| find_match_case_label=आकार जुळवा | find_match_case_label=आकार जुळवा | ||||||
|  | find_entire_word_label=संपूर्ण शब्द | ||||||
| find_reached_top=दस्तऐवजाच्या शीर्षकास पोहचले, तळपासून पुढे | find_reached_top=दस्तऐवजाच्या शीर्षकास पोहचले, तळपासून पुढे | ||||||
| find_reached_bottom=दस्तऐवजाच्या तळाला पोहचले, शीर्षकापासून पुढे | find_reached_bottom=दस्तऐवजाच्या तळाला पोहचले, शीर्षकापासून पुढे | ||||||
|  | # LOCALIZATION NOTE (find_match_count): The supported plural forms are | ||||||
|  | # [one|two|few|many|other], with [other] as the default value. | ||||||
|  | # "{{current}}" and "{{total}}" will be replaced by a number representing the | ||||||
|  | # index of the currently active find result, respectively a number representing | ||||||
|  | # the total number of matches in the document. | ||||||
|  | find_match_count={[ plural(total) ]} | ||||||
|  | # LOCALIZATION NOTE (find_match_count_limit): The supported plural forms are | ||||||
|  | # [zero|one|two|few|many|other], with [other] as the default value. | ||||||
|  | # "{{limit}}" will be replaced by a numerical value. | ||||||
|  | find_match_count_limit[zero]={{limit}} पेक्षा अधिक जुळण्या | ||||||
|  | find_match_count_limit[two]={{limit}} पेक्षा अधिक जुळण्या | ||||||
|  | find_match_count_limit[few]={{limit}} पेक्षा अधिक जुळण्या | ||||||
|  | find_match_count_limit[many]={{limit}} पेक्षा अधिक जुळण्या | ||||||
|  | find_match_count_limit[other]={{limit}} पेक्षा अधिक जुळण्या | ||||||
| find_not_found=वाकप्रयोग आढळले नाही | find_not_found=वाकप्रयोग आढळले नाही | ||||||
| 
 | 
 | ||||||
| # Error panel labels | # Error panel labels | ||||||
|  |  | ||||||
|  | @ -226,6 +226,10 @@ invalid_file_error=Ugyldig eller skadet PDF-fil. | ||||||
| missing_file_error=Manglende PDF-fil. | missing_file_error=Manglende PDF-fil. | ||||||
| unexpected_response_error=Uventet serverrespons. | unexpected_response_error=Uventet serverrespons. | ||||||
| 
 | 
 | ||||||
|  | # LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be | ||||||
|  | # replaced by the modification date, and time, of the annotation. | ||||||
|  | annotation_date_string={{date}}, {{time}} | ||||||
|  | 
 | ||||||
| # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | ||||||
| # "{{type}}" will be replaced with an annotation type from a list defined in | # "{{type}}" will be replaced with an annotation type from a list defined in | ||||||
| # the PDF spec (32000-1:2008 Table 169 – Annotation types). | # the PDF spec (32000-1:2008 Table 169 – Annotation types). | ||||||
|  |  | ||||||
|  | @ -226,6 +226,10 @@ invalid_file_error=Ongeldig of beschadigd PDF-bestand. | ||||||
| missing_file_error=PDF-bestand ontbreekt.  | missing_file_error=PDF-bestand ontbreekt.  | ||||||
| unexpected_response_error=Onverwacht serverantwoord. | unexpected_response_error=Onverwacht serverantwoord. | ||||||
| 
 | 
 | ||||||
|  | # LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be | ||||||
|  | # replaced by the modification date, and time, of the annotation. | ||||||
|  | annotation_date_string={{date}}, {{time}} | ||||||
|  | 
 | ||||||
| # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | ||||||
| # "{{type}}" will be replaced with an annotation type from a list defined in | # "{{type}}" will be replaced with an annotation type from a list defined in | ||||||
| # the PDF spec (32000-1:2008 Table 169 – Annotation types). | # the PDF spec (32000-1:2008 Table 169 – Annotation types). | ||||||
|  |  | ||||||
|  | @ -226,6 +226,10 @@ invalid_file_error=Ugyldig eller korrupt PDF-fil. | ||||||
| missing_file_error=Manglande PDF-fil. | missing_file_error=Manglande PDF-fil. | ||||||
| unexpected_response_error=Uventa tenarrespons. | unexpected_response_error=Uventa tenarrespons. | ||||||
| 
 | 
 | ||||||
|  | # LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be | ||||||
|  | # replaced by the modification date, and time, of the annotation. | ||||||
|  | annotation_date_string={{date}} {{time}} | ||||||
|  | 
 | ||||||
| # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | ||||||
| # "{{type}}" will be replaced with an annotation type from a list defined in | # "{{type}}" will be replaced with an annotation type from a list defined in | ||||||
| # the PDF spec (32000-1:2008 Table 169 – Annotation types). | # the PDF spec (32000-1:2008 Table 169 – Annotation types). | ||||||
|  |  | ||||||
|  | @ -168,10 +168,21 @@ find_reached_bottom=ਦਸਤਾਵੇਜ਼ ਦੇ ਅੰਤ ਉੱਤੇ ਆ ਗ | ||||||
| # index of the currently active find result, respectively a number representing | # index of the currently active find result, respectively a number representing | ||||||
| # the total number of matches in the document. | # the total number of matches in the document. | ||||||
| find_match_count={[ plural(total) ]} | find_match_count={[ plural(total) ]} | ||||||
|  | find_match_count[one]={{total}} ਵਿੱਚੋਂ {{current}} ਮੇਲ | ||||||
|  | find_match_count[two]={{total}} ਵਿੱਚੋਂ {{current}} ਮੇਲ | ||||||
|  | find_match_count[few]={{total}} ਵਿੱਚੋਂ {{current}} ਮੇਲ | ||||||
|  | find_match_count[many]={{total}} ਵਿੱਚੋਂ {{current}} ਮੇਲ | ||||||
|  | find_match_count[other]={{total}} ਵਿੱਚੋਂ {{current}} ਮੇਲ | ||||||
| # LOCALIZATION NOTE (find_match_count_limit): The supported plural forms are | # LOCALIZATION NOTE (find_match_count_limit): The supported plural forms are | ||||||
| # [zero|one|two|few|many|other], with [other] as the default value. | # [zero|one|two|few|many|other], with [other] as the default value. | ||||||
| # "{{limit}}" will be replaced by a numerical value. | # "{{limit}}" will be replaced by a numerical value. | ||||||
| find_match_count_limit={[ plural(limit) ]} | find_match_count_limit={[ plural(limit) ]} | ||||||
|  | find_match_count_limit[zero]={{limit}} ਮੇਲਾਂ ਤੋਂ ਵੱਧ | ||||||
|  | find_match_count_limit[one]={{limit}} ਮੇਲ ਤੋਂ ਵੱਧ | ||||||
|  | find_match_count_limit[two]={{limit}} ਮੇਲਾਂ ਤੋਂ ਵੱਧ | ||||||
|  | find_match_count_limit[few]={{limit}} ਮੇਲਾਂ ਤੋਂ ਵੱਧ | ||||||
|  | find_match_count_limit[many]={{limit}} ਮੇਲਾਂ ਤੋਂ ਵੱਧ | ||||||
|  | find_match_count_limit[other]={{limit}} ਮੇਲਾਂ ਤੋਂ ਵੱਧ | ||||||
| find_not_found=ਵਾਕ ਨਹੀਂ ਲੱਭਿਆ | find_not_found=ਵਾਕ ਨਹੀਂ ਲੱਭਿਆ | ||||||
| 
 | 
 | ||||||
| # Error panel labels | # Error panel labels | ||||||
|  |  | ||||||
|  | @ -12,13 +12,20 @@ | ||||||
| # See the License for the specific language governing permissions and | # See the License for the specific language governing permissions and | ||||||
| # limitations under the License. | # limitations under the License. | ||||||
| 
 | 
 | ||||||
|  | # Main toolbar buttons (tooltips and alt text for images) | ||||||
| previous.title=Poprzednia strona | previous.title=Poprzednia strona | ||||||
| previous_label=Poprzednia | previous_label=Poprzednia | ||||||
| next.title=Następna strona | next.title=Następna strona | ||||||
| next_label=Następna | next_label=Następna | ||||||
| 
 | 
 | ||||||
| page.title==Strona: | # LOCALIZATION NOTE (page.title): The tooltip for the pageNumber input. | ||||||
|  | page.title=Strona | ||||||
|  | # LOCALIZATION NOTE (of_pages): "{{pagesCount}}" will be replaced by a number | ||||||
|  | # representing the total number of pages in the document. | ||||||
| of_pages=z {{pagesCount}} | of_pages=z {{pagesCount}} | ||||||
|  | # LOCALIZATION NOTE (page_of_pages): "{{pageNumber}}" and "{{pagesCount}}" | ||||||
|  | # will be replaced by a number representing the currently visible page, | ||||||
|  | # respectively a number representing the total number of pages in the document. | ||||||
| page_of_pages=({{pageNumber}} z {{pagesCount}}) | page_of_pages=({{pageNumber}} z {{pagesCount}}) | ||||||
| 
 | 
 | ||||||
| zoom_out.title=Pomniejszenie | zoom_out.title=Pomniejszenie | ||||||
|  | @ -37,6 +44,7 @@ download_label=Pobierz | ||||||
| bookmark.title=Bieżąca pozycja (skopiuj lub otwórz jako odnośnik w nowym oknie) | bookmark.title=Bieżąca pozycja (skopiuj lub otwórz jako odnośnik w nowym oknie) | ||||||
| bookmark_label=Bieżąca pozycja | bookmark_label=Bieżąca pozycja | ||||||
| 
 | 
 | ||||||
|  | # Secondary toolbar and context menu | ||||||
| tools.title=Narzędzia | tools.title=Narzędzia | ||||||
| tools_label=Narzędzia | tools_label=Narzędzia | ||||||
| first_page.title=Przechodzenie do pierwszej strony | first_page.title=Przechodzenie do pierwszej strony | ||||||
|  | @ -59,30 +67,37 @@ cursor_hand_tool_label=Narzędzie rączka | ||||||
| 
 | 
 | ||||||
| scroll_vertical.title=Przewijaj dokument w pionie | scroll_vertical.title=Przewijaj dokument w pionie | ||||||
| scroll_vertical_label=Przewijanie pionowe | scroll_vertical_label=Przewijanie pionowe | ||||||
| scroll_horizontal_label=Przewijanie poziome |  | ||||||
| scroll_horizontal.title=Przewijaj dokument w poziomie | scroll_horizontal.title=Przewijaj dokument w poziomie | ||||||
| scroll_wrapped_label=Widok dwóch stron | scroll_horizontal_label=Przewijanie poziome | ||||||
| scroll_wrapped.title=Strony dokumentu wyświetlaj i przewijaj w kolumnach | scroll_wrapped.title=Strony dokumentu wyświetlaj i przewijaj w kolumnach | ||||||
|  | scroll_wrapped_label=Widok dwóch stron | ||||||
| 
 | 
 | ||||||
| spread_none_label=Brak kolumn |  | ||||||
| spread_none.title=Nie ustawiaj stron obok siebie | spread_none.title=Nie ustawiaj stron obok siebie | ||||||
| spread_odd_label=Nieparzyste po lewej | spread_none_label=Brak kolumn | ||||||
| spread_odd.title=Strony nieparzyste ustawiaj na lewo od parzystych | spread_odd.title=Strony nieparzyste ustawiaj na lewo od parzystych | ||||||
| spread_even_label=Parzyste po lewej | spread_odd_label=Nieparzyste po lewej | ||||||
| spread_even.title=Strony parzyste ustawiaj na lewo od nieparzystych | spread_even.title=Strony parzyste ustawiaj na lewo od nieparzystych | ||||||
|  | spread_even_label=Parzyste po lewej | ||||||
| 
 | 
 | ||||||
|  | # Document properties dialog box | ||||||
| document_properties.title=Właściwości dokumentu… | document_properties.title=Właściwości dokumentu… | ||||||
| document_properties_label=Właściwości dokumentu… | document_properties_label=Właściwości dokumentu… | ||||||
| document_properties_file_name=Nazwa pliku: | document_properties_file_name=Nazwa pliku: | ||||||
| document_properties_file_size=Rozmiar pliku: | document_properties_file_size=Rozmiar pliku: | ||||||
| document_properties_kb={{size_kb}} KB ({{size_b}} b) | # LOCALIZATION NOTE (document_properties_kb): "{{size_kb}}" and "{{size_b}}" | ||||||
| document_properties_mb={{size_mb}} MB ({{size_b}} b) | # will be replaced by the PDF file size in kilobytes, respectively in bytes. | ||||||
|  | document_properties_kb={{size_kb}} KB ({{size_b}} B) | ||||||
|  | # LOCALIZATION NOTE (document_properties_mb): "{{size_mb}}" and "{{size_b}}" | ||||||
|  | # will be replaced by the PDF file size in megabytes, respectively in bytes. | ||||||
|  | document_properties_mb={{size_mb}} MB ({{size_b}} B) | ||||||
| document_properties_title=Tytuł: | document_properties_title=Tytuł: | ||||||
| document_properties_author=Autor: | document_properties_author=Autor: | ||||||
| document_properties_subject=Temat: | document_properties_subject=Temat: | ||||||
| document_properties_keywords=Słowa kluczowe: | document_properties_keywords=Słowa kluczowe: | ||||||
| document_properties_creation_date=Data utworzenia: | document_properties_creation_date=Data utworzenia: | ||||||
| document_properties_modification_date=Data modyfikacji: | document_properties_modification_date=Data modyfikacji: | ||||||
|  | # LOCALIZATION NOTE (document_properties_date_string): "{{date}}" and "{{time}}" | ||||||
|  | # will be replaced by the creation/modification date, and time, of the PDF file. | ||||||
| document_properties_date_string={{date}}, {{time}} | document_properties_date_string={{date}}, {{time}} | ||||||
| document_properties_creator=Utworzony przez: | document_properties_creator=Utworzony przez: | ||||||
| document_properties_producer=PDF wyprodukowany przez: | document_properties_producer=PDF wyprodukowany przez: | ||||||
|  | @ -97,17 +112,30 @@ document_properties_page_size_name_a3=A3 | ||||||
| document_properties_page_size_name_a4=A4 | document_properties_page_size_name_a4=A4 | ||||||
| document_properties_page_size_name_letter=US Letter | document_properties_page_size_name_letter=US Letter | ||||||
| document_properties_page_size_name_legal=US Legal | document_properties_page_size_name_legal=US Legal | ||||||
| document_properties_page_size_dimension_string={{width}} × {{height}} {{unit}} (orientacja {{orientation}}) | # LOCALIZATION NOTE (document_properties_page_size_dimension_string): | ||||||
| document_properties_page_size_dimension_name_string={{width}} × {{height}} {{unit}} ({{name}}, orientacja {{orientation}}) | # "{{width}}", "{{height}}", {{unit}}, and {{orientation}} will be replaced by | ||||||
|  | # the size, respectively their unit of measurement and orientation, of the (current) page. | ||||||
|  | document_properties_page_size_dimension_string={{width}}×{{height}} {{unit}} (orientacja {{orientation}}) | ||||||
|  | # LOCALIZATION NOTE (document_properties_page_size_dimension_name_string): | ||||||
|  | # "{{width}}", "{{height}}", {{unit}}, {{name}}, and {{orientation}} will be replaced by | ||||||
|  | # the size, respectively their unit of measurement, name, and orientation, of the (current) page. | ||||||
|  | document_properties_page_size_dimension_name_string={{width}}×{{height}} {{unit}} ({{name}}, orientacja {{orientation}}) | ||||||
|  | # LOCALIZATION NOTE (document_properties_linearized): The linearization status of | ||||||
|  | # the document; usually called "Fast Web View" in English locales of Adobe software. | ||||||
| document_properties_linearized=Szybki podgląd w Internecie: | document_properties_linearized=Szybki podgląd w Internecie: | ||||||
| document_properties_linearized_yes=tak | document_properties_linearized_yes=tak | ||||||
| document_properties_linearized_no=nie | document_properties_linearized_no=nie | ||||||
| document_properties_close=Zamknij | document_properties_close=Zamknij | ||||||
| 
 | 
 | ||||||
| print_progress_message=Przygotowywanie dokumentu do druku… | print_progress_message=Przygotowywanie dokumentu do druku… | ||||||
|  | # LOCALIZATION NOTE (print_progress_percent): "{{progress}}" will be replaced by | ||||||
|  | # a numerical per cent value. | ||||||
| print_progress_percent={{progress}}% | print_progress_percent={{progress}}% | ||||||
| print_progress_close=Anuluj | print_progress_close=Anuluj | ||||||
| 
 | 
 | ||||||
|  | # Tooltips and alt text for side panel toolbar buttons | ||||||
|  | # (the _label strings are alt text for the buttons, the .title strings are | ||||||
|  | # tooltips) | ||||||
| toggle_sidebar.title=Przełączanie panelu bocznego | toggle_sidebar.title=Przełączanie panelu bocznego | ||||||
| toggle_sidebar_notification.title=Przełączanie panelu bocznego (dokument zawiera konspekt/załączniki) | toggle_sidebar_notification.title=Przełączanie panelu bocznego (dokument zawiera konspekt/załączniki) | ||||||
| toggle_sidebar_label=Przełącz panel boczny | toggle_sidebar_label=Przełącz panel boczny | ||||||
|  | @ -120,26 +148,40 @@ thumbs_label=Miniaturki | ||||||
| findbar.title=Znajdź w dokumencie | findbar.title=Znajdź w dokumencie | ||||||
| findbar_label=Znajdź | findbar_label=Znajdź | ||||||
| 
 | 
 | ||||||
|  | # Thumbnails panel item (tooltip and alt text for images) | ||||||
|  | # LOCALIZATION NOTE (thumb_page_title): "{{page}}" will be replaced by the page | ||||||
|  | # number. | ||||||
| thumb_page_title=Strona {{page}} | thumb_page_title=Strona {{page}} | ||||||
|  | # LOCALIZATION NOTE (thumb_page_canvas): "{{page}}" will be replaced by the page | ||||||
|  | # number. | ||||||
| thumb_page_canvas=Miniaturka strony {{page}} | thumb_page_canvas=Miniaturka strony {{page}} | ||||||
| 
 | 
 | ||||||
|  | # Find panel button title and messages | ||||||
| find_input.title=Wyszukiwanie | find_input.title=Wyszukiwanie | ||||||
| find_input.placeholder=Szukaj w dokumencie… | find_input.placeholder=Znajdź w dokumencie… | ||||||
| find_previous.title=Znajdź poprzednie wystąpienie tekstu | find_previous.title=Znajdź poprzednie wystąpienie tekstu | ||||||
| find_previous_label=Poprzednie | find_previous_label=Poprzednie | ||||||
| find_next.title=Znajdź następne wystąpienie tekstu | find_next.title=Znajdź następne wystąpienie tekstu | ||||||
| find_next_label=Następne | find_next_label=Następne | ||||||
| find_highlight=Podświetl wszystkie | find_highlight=Wyróżnianie wszystkich | ||||||
| find_match_case_label=Rozróżnianie wielkości liter | find_match_case_label=Rozróżnianie wielkości liter | ||||||
| find_entire_word_label=Całe słowa | find_entire_word_label=Całe słowa | ||||||
| find_reached_top=Początek dokumentu. Wyszukiwanie od końca. | find_reached_top=Początek dokumentu. Wyszukiwanie od końca. | ||||||
| find_reached_bottom=Koniec dokumentu. Wyszukiwanie od początku. | find_reached_bottom=Koniec dokumentu. Wyszukiwanie od początku. | ||||||
|  | # LOCALIZATION NOTE (find_match_count): The supported plural forms are | ||||||
|  | # [one|two|few|many|other], with [other] as the default value. | ||||||
|  | # "{{current}}" and "{{total}}" will be replaced by a number representing the | ||||||
|  | # index of the currently active find result, respectively a number representing | ||||||
|  | # the total number of matches in the document. | ||||||
| find_match_count={[ plural(total) ]} | find_match_count={[ plural(total) ]} | ||||||
| find_match_count[one]=Pierwsze z {{total}} trafień | find_match_count[one]=Pierwsze z {{total}} trafień | ||||||
| find_match_count[two]=Drugie z {{total}} trafień | find_match_count[two]=Drugie z {{total}} trafień | ||||||
| find_match_count[few]={{current}}. z {{total}} trafień | find_match_count[few]={{current}}. z {{total}} trafień | ||||||
| find_match_count[many]={{current}}. z {{total}} trafień | find_match_count[many]={{current}}. z {{total}} trafień | ||||||
| find_match_count[other]={{current}}. z {{total}} trafień | find_match_count[other]={{current}}. z {{total}} trafień | ||||||
|  | # LOCALIZATION NOTE (find_match_count_limit): The supported plural forms are | ||||||
|  | # [zero|one|two|few|many|other], with [other] as the default value. | ||||||
|  | # "{{limit}}" will be replaced by a numerical value. | ||||||
| find_match_count_limit={[ plural(limit) ]} | find_match_count_limit={[ plural(limit) ]} | ||||||
| find_match_count_limit[zero]=Brak trafień. | find_match_count_limit[zero]=Brak trafień. | ||||||
| find_match_count_limit[one]=Więcej niż jedno trafienie. | find_match_count_limit[one]=Więcej niż jedno trafienie. | ||||||
|  | @ -149,28 +191,49 @@ find_match_count_limit[many]=Więcej niż {{limit}} trafień. | ||||||
| find_match_count_limit[other]=Więcej niż {{limit}} trafień. | find_match_count_limit[other]=Więcej niż {{limit}} trafień. | ||||||
| find_not_found=Nie znaleziono tekstu | find_not_found=Nie znaleziono tekstu | ||||||
| 
 | 
 | ||||||
|  | # Error panel labels | ||||||
| error_more_info=Więcej informacji | error_more_info=Więcej informacji | ||||||
| error_less_info=Mniej informacji | error_less_info=Mniej informacji | ||||||
| error_close=Zamknij | error_close=Zamknij | ||||||
|  | # LOCALIZATION NOTE (error_version_info): "{{version}}" and "{{build}}" will be | ||||||
|  | # replaced by the PDF.JS version and build ID. | ||||||
| error_version_info=PDF.js v{{version}} (kompilacja: {{build}}) | error_version_info=PDF.js v{{version}} (kompilacja: {{build}}) | ||||||
|  | # LOCALIZATION NOTE (error_message): "{{message}}" will be replaced by an | ||||||
|  | # english string describing the error. | ||||||
| error_message=Wiadomość: {{message}} | error_message=Wiadomość: {{message}} | ||||||
|  | # LOCALIZATION NOTE (error_stack): "{{stack}}" will be replaced with a stack | ||||||
|  | # trace. | ||||||
| error_stack=Stos: {{stack}} | error_stack=Stos: {{stack}} | ||||||
|  | # LOCALIZATION NOTE (error_file): "{{file}}" will be replaced with a filename | ||||||
| error_file=Plik: {{file}} | error_file=Plik: {{file}} | ||||||
|  | # LOCALIZATION NOTE (error_line): "{{line}}" will be replaced with a line number | ||||||
| error_line=Wiersz: {{line}} | error_line=Wiersz: {{line}} | ||||||
| rendering_error=Podczas renderowania strony wystąpił błąd. | rendering_error=Podczas renderowania strony wystąpił błąd. | ||||||
| 
 | 
 | ||||||
|  | # Predefined zoom values | ||||||
| page_scale_width=Szerokość strony | page_scale_width=Szerokość strony | ||||||
| page_scale_fit=Dopasowanie strony | page_scale_fit=Dopasowanie strony | ||||||
| page_scale_auto=Skala automatyczna | page_scale_auto=Skala automatyczna | ||||||
| page_scale_actual=Rozmiar rzeczywisty | page_scale_actual=Rozmiar rzeczywisty | ||||||
|  | # LOCALIZATION NOTE (page_scale_percent): "{{scale}}" will be replaced by a | ||||||
|  | # numerical scale value. | ||||||
| page_scale_percent={{scale}}% | page_scale_percent={{scale}}% | ||||||
| 
 | 
 | ||||||
|  | # Loading indicator messages | ||||||
| loading_error_indicator=Błąd | loading_error_indicator=Błąd | ||||||
| loading_error=Podczas wczytywania dokumentu PDF wystąpił błąd. | loading_error=Podczas wczytywania dokumentu PDF wystąpił błąd. | ||||||
| invalid_file_error=Nieprawidłowy lub uszkodzony plik PDF. | invalid_file_error=Nieprawidłowy lub uszkodzony plik PDF. | ||||||
| missing_file_error=Brak pliku PDF. | missing_file_error=Brak pliku PDF. | ||||||
| unexpected_response_error=Nieoczekiwana odpowiedź serwera. | unexpected_response_error=Nieoczekiwana odpowiedź serwera. | ||||||
| 
 | 
 | ||||||
|  | # LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be | ||||||
|  | # replaced by the modification date, and time, of the annotation. | ||||||
|  | annotation_date_string={{date}}, {{time}} | ||||||
|  | 
 | ||||||
|  | # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | ||||||
|  | # "{{type}}" will be replaced with an annotation type from a list defined in | ||||||
|  | # the PDF spec (32000-1:2008 Table 169 – Annotation types). | ||||||
|  | # Some common types are e.g.: "Check", "Text", "Comment", "Note" | ||||||
| text_annotation_type.alt=[Adnotacja: {{type}}] | text_annotation_type.alt=[Adnotacja: {{type}}] | ||||||
| password_label=Wprowadź hasło, aby otworzyć ten dokument PDF. | password_label=Wprowadź hasło, aby otworzyć ten dokument PDF. | ||||||
| password_invalid=Nieprawidłowe hasło. Proszę spróbować ponownie. | password_invalid=Nieprawidłowe hasło. Proszę spróbować ponownie. | ||||||
|  |  | ||||||
|  | @ -226,6 +226,10 @@ invalid_file_error=Arquivo PDF corrompido ou inválido. | ||||||
| missing_file_error=Arquivo PDF ausente. | missing_file_error=Arquivo PDF ausente. | ||||||
| unexpected_response_error=Resposta inesperada do servidor. | unexpected_response_error=Resposta inesperada do servidor. | ||||||
| 
 | 
 | ||||||
|  | # LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be | ||||||
|  | # replaced by the modification date, and time, of the annotation. | ||||||
|  | annotation_date_string={{date}}, {{time}} | ||||||
|  | 
 | ||||||
| # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | ||||||
| # "{{type}}" will be replaced with an annotation type from a list defined in | # "{{type}}" will be replaced with an annotation type from a list defined in | ||||||
| # the PDF spec (32000-1:2008 Table 169 – Annotation types). | # the PDF spec (32000-1:2008 Table 169 – Annotation types). | ||||||
|  | @ -238,5 +242,5 @@ password_cancel=Cancelar | ||||||
| 
 | 
 | ||||||
| printing_not_supported=Aviso: a impressão não é totalmente suportada neste navegador. | printing_not_supported=Aviso: a impressão não é totalmente suportada neste navegador. | ||||||
| printing_not_ready=Aviso: o PDF não está totalmente carregado para impressão. | printing_not_ready=Aviso: o PDF não está totalmente carregado para impressão. | ||||||
| web_fonts_disabled=As fontes web estão desabilitadas: não foi possível usar fontes incorporadas do PDF. | web_fonts_disabled=As fontes web estão desativadas: não foi possível usar fontes incorporadas do PDF. | ||||||
| document_colors_not_allowed=Os documentos em PDF não estão autorizados a usar suas próprias cores: “Permitir que as páginas escolham suas próprias cores” está desabilitado no navegador. | document_colors_not_allowed=Documentos PDF não estão autorizados a usar as próprias cores: a opção “Permitir que as páginas escolham suas próprias cores” está desativada no navegador. | ||||||
|  |  | ||||||
|  | @ -140,7 +140,7 @@ toggle_sidebar.title=Alternar barra lateral | ||||||
| toggle_sidebar_notification.title=Alternar barra lateral (documento contém contorno/anexos) | toggle_sidebar_notification.title=Alternar barra lateral (documento contém contorno/anexos) | ||||||
| toggle_sidebar_label=Alternar barra lateral | toggle_sidebar_label=Alternar barra lateral | ||||||
| document_outline.title=Mostrar esquema do documento (duplo clique para expandir/colapsar todos os itens) | document_outline.title=Mostrar esquema do documento (duplo clique para expandir/colapsar todos os itens) | ||||||
| document_outline_label=Estrutura do documento | document_outline_label=Esquema do documento | ||||||
| attachments.title=Mostrar anexos | attachments.title=Mostrar anexos | ||||||
| attachments_label=Anexos | attachments_label=Anexos | ||||||
| thumbs.title=Mostrar miniaturas | thumbs.title=Mostrar miniaturas | ||||||
|  | @ -226,6 +226,10 @@ invalid_file_error=Ficheiro PDF inválido ou danificado. | ||||||
| missing_file_error=Ficheiro PDF inexistente. | missing_file_error=Ficheiro PDF inexistente. | ||||||
| unexpected_response_error=Resposta inesperada do servidor. | unexpected_response_error=Resposta inesperada do servidor. | ||||||
| 
 | 
 | ||||||
|  | # LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be | ||||||
|  | # replaced by the modification date, and time, of the annotation. | ||||||
|  | annotation_date_string={{date}}, {{time}} | ||||||
|  | 
 | ||||||
| # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | ||||||
| # "{{type}}" will be replaced with an annotation type from a list defined in | # "{{type}}" will be replaced with an annotation type from a list defined in | ||||||
| # the PDF spec (32000-1:2008 Table 169 – Annotation types). | # the PDF spec (32000-1:2008 Table 169 – Annotation types). | ||||||
|  |  | ||||||
|  | @ -83,7 +83,7 @@ spread_even_label=Broșare pagini pare | ||||||
| document_properties.title=Proprietățile documentului… | document_properties.title=Proprietățile documentului… | ||||||
| document_properties_label=Proprietățile documentului… | document_properties_label=Proprietățile documentului… | ||||||
| document_properties_file_name=Numele fișierului: | document_properties_file_name=Numele fișierului: | ||||||
| document_properties_file_size=Dimensiunea fișierului: | document_properties_file_size=Mărimea fișierului: | ||||||
| # LOCALIZATION NOTE (document_properties_kb): "{{size_kb}}" and "{{size_b}}" | # LOCALIZATION NOTE (document_properties_kb): "{{size_kb}}" and "{{size_b}}" | ||||||
| # will be replaced by the PDF file size in kilobytes, respectively in bytes. | # will be replaced by the PDF file size in kilobytes, respectively in bytes. | ||||||
| document_properties_kb={{size_kb}} KB ({{size_b}} byți) | document_properties_kb={{size_kb}} KB ({{size_b}} byți) | ||||||
|  | @ -103,7 +103,7 @@ document_properties_creator=Autor: | ||||||
| document_properties_producer=Producător PDF: | document_properties_producer=Producător PDF: | ||||||
| document_properties_version=Versiune PDF: | document_properties_version=Versiune PDF: | ||||||
| document_properties_page_count=Număr de pagini: | document_properties_page_count=Număr de pagini: | ||||||
| document_properties_page_size=Dimensiunea paginii: | document_properties_page_size=Mărimea paginii: | ||||||
| document_properties_page_size_unit_inches=in | document_properties_page_size_unit_inches=in | ||||||
| document_properties_page_size_unit_millimeters=mm | document_properties_page_size_unit_millimeters=mm | ||||||
| document_properties_page_size_orientation_portrait=portret | document_properties_page_size_orientation_portrait=portret | ||||||
|  | @ -214,7 +214,7 @@ rendering_error=A intervenit o eroare la randarea paginii. | ||||||
| page_scale_width=Lățimea paginii | page_scale_width=Lățimea paginii | ||||||
| page_scale_fit=Potrivire la pagină | page_scale_fit=Potrivire la pagină | ||||||
| page_scale_auto=Zoom automat | page_scale_auto=Zoom automat | ||||||
| page_scale_actual=Dimensiune reală | page_scale_actual=Mărime reală | ||||||
| # LOCALIZATION NOTE (page_scale_percent): "{{scale}}" will be replaced by a | # LOCALIZATION NOTE (page_scale_percent): "{{scale}}" will be replaced by a | ||||||
| # numerical scale value. | # numerical scale value. | ||||||
| page_scale_percent={{scale}}% | page_scale_percent={{scale}}% | ||||||
|  | @ -226,6 +226,10 @@ invalid_file_error=Fișier PDF nevalid sau corupt. | ||||||
| missing_file_error=Fișier PDF lipsă. | missing_file_error=Fișier PDF lipsă. | ||||||
| unexpected_response_error=Răspuns neașteptat de la server. | unexpected_response_error=Răspuns neașteptat de la server. | ||||||
| 
 | 
 | ||||||
|  | # LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be | ||||||
|  | # replaced by the modification date, and time, of the annotation. | ||||||
|  | annotation_date_string={{date}}, {{time}} | ||||||
|  | 
 | ||||||
| # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | ||||||
| # "{{type}}" will be replaced with an annotation type from a list defined in | # "{{type}}" will be replaced with an annotation type from a list defined in | ||||||
| # the PDF spec (32000-1:2008 Table 169 – Annotation types). | # the PDF spec (32000-1:2008 Table 169 – Annotation types). | ||||||
|  |  | ||||||
|  | @ -226,6 +226,10 @@ invalid_file_error=Некорректный или повреждённый PDF- | ||||||
| missing_file_error=PDF-файл отсутствует. | missing_file_error=PDF-файл отсутствует. | ||||||
| unexpected_response_error=Неожиданный ответ сервера. | unexpected_response_error=Неожиданный ответ сервера. | ||||||
| 
 | 
 | ||||||
|  | # LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be | ||||||
|  | # replaced by the modification date, and time, of the annotation. | ||||||
|  | annotation_date_string={{date}}, {{time}} | ||||||
|  | 
 | ||||||
| # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | ||||||
| # "{{type}}" will be replaced with an annotation type from a list defined in | # "{{type}}" will be replaced with an annotation type from a list defined in | ||||||
| # the PDF spec (32000-1:2008 Table 169 – Annotation types). | # the PDF spec (32000-1:2008 Table 169 – Annotation types). | ||||||
|  |  | ||||||
|  | @ -58,6 +58,9 @@ page_rotate_ccw.title=වාමාවර්තව භ්රමණය | ||||||
| page_rotate_ccw.label=වාමාවර්තව භ්රමණය | page_rotate_ccw.label=වාමාවර්තව භ්රමණය | ||||||
| page_rotate_ccw_label=වාමාවර්තව භ්රමණය | page_rotate_ccw_label=වාමාවර්තව භ්රමණය | ||||||
| 
 | 
 | ||||||
|  | cursor_hand_tool_label=අත් මෙවලම | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| # Document properties dialog box | # Document properties dialog box | ||||||
| document_properties.title=ලේඛන වත්කම්... | document_properties.title=ලේඛන වත්කම්... | ||||||
|  | @ -83,11 +86,32 @@ document_properties_creator=නිර්මාපක: | ||||||
| document_properties_producer=PDF නිශ්පාදක: | document_properties_producer=PDF නිශ්පාදක: | ||||||
| document_properties_version=PDF නිකුතුව: | document_properties_version=PDF නිකුතුව: | ||||||
| document_properties_page_count=පිටු ගණන: | document_properties_page_count=පිටු ගණන: | ||||||
|  | document_properties_page_size=පිටුවේ විශාලත්වය: | ||||||
|  | document_properties_page_size_unit_inches=අඟල් | ||||||
|  | document_properties_page_size_unit_millimeters=මිමි | ||||||
|  | document_properties_page_size_orientation_portrait=සිරස් | ||||||
|  | document_properties_page_size_orientation_landscape=තිරස් | ||||||
|  | document_properties_page_size_name_a3=A3 | ||||||
|  | document_properties_page_size_name_a4=A4 | ||||||
|  | # LOCALIZATION NOTE (document_properties_page_size_dimension_string): | ||||||
|  | # "{{width}}", "{{height}}", {{unit}}, and {{orientation}} will be replaced by | ||||||
|  | # the size, respectively their unit of measurement and orientation, of the (current) page. | ||||||
|  | document_properties_page_size_dimension_string={{width}} × {{height}} {{unit}} ({{orientation}}) | ||||||
|  | # LOCALIZATION NOTE (document_properties_page_size_dimension_name_string): | ||||||
|  | # "{{width}}", "{{height}}", {{unit}}, {{name}}, and {{orientation}} will be replaced by | ||||||
|  | # the size, respectively their unit of measurement, name, and orientation, of the (current) page. | ||||||
|  | document_properties_page_size_dimension_name_string={{width}}×{{height}}{{unit}}{{name}}{{orientation}} | ||||||
|  | # LOCALIZATION NOTE (document_properties_linearized): The linearization status of | ||||||
|  | # the document; usually called "Fast Web View" in English locales of Adobe software. | ||||||
|  | document_properties_linearized=වේගවත් ජාල දසුන: | ||||||
|  | document_properties_linearized_yes=ඔව් | ||||||
|  | document_properties_linearized_no=නැහැ | ||||||
| document_properties_close=වසන්න | document_properties_close=වසන්න | ||||||
| 
 | 
 | ||||||
| print_progress_message=ලේඛනය මුද්රණය සඳහා සූදානම් කරමින්… | print_progress_message=ලේඛනය මුද්රණය සඳහා සූදානම් කරමින්… | ||||||
| # LOCALIZATION NOTE (print_progress_percent): "{{progress}}" will be replaced by | # LOCALIZATION NOTE (print_progress_percent): "{{progress}}" will be replaced by | ||||||
| # a numerical per cent value. | # a numerical per cent value. | ||||||
|  | print_progress_percent={{progress}}% | ||||||
| print_progress_close=අවලංගු කරන්න | print_progress_close=අවලංගු කරන්න | ||||||
| 
 | 
 | ||||||
| # Tooltips and alt text for side panel toolbar buttons | # Tooltips and alt text for side panel toolbar buttons | ||||||
|  | @ -95,6 +119,7 @@ print_progress_close=අවලංගු කරන්න | ||||||
| # tooltips) | # tooltips) | ||||||
| toggle_sidebar.title=පැති තීරුවට මාරුවන්න | toggle_sidebar.title=පැති තීරුවට මාරුවන්න | ||||||
| toggle_sidebar_label=පැති තීරුවට මාරුවන්න | toggle_sidebar_label=පැති තීරුවට මාරුවන්න | ||||||
|  | document_outline_label=ලේඛනයේ පිට මායිම | ||||||
| attachments.title=ඇමිණුම් පෙන්වන්න | attachments.title=ඇමිණුම් පෙන්වන්න | ||||||
| attachments_label=ඇමිණුම් | attachments_label=ඇමිණුම් | ||||||
| thumbs.title=සිඟිති රූ පෙන්වන්න | thumbs.title=සිඟිති රූ පෙන්වන්න | ||||||
|  | @ -111,14 +136,25 @@ thumb_page_title=පිටුව {{page}} | ||||||
| thumb_page_canvas=පිටුවෙ සිඟිත රූව {{page}} | thumb_page_canvas=පිටුවෙ සිඟිත රූව {{page}} | ||||||
| 
 | 
 | ||||||
| # Find panel button title and messages | # Find panel button title and messages | ||||||
|  | find_input.title=සොයන්න | ||||||
| find_previous.title=මේ වාක්ය ඛණ්ඩය මීට පෙර යෙදුණු ස්ථානය සොයන්න | find_previous.title=මේ වාක්ය ඛණ්ඩය මීට පෙර යෙදුණු ස්ථානය සොයන්න | ||||||
| find_previous_label=පෙර: | find_previous_label=පෙර: | ||||||
| find_next.title=මේ වාක්ය ඛණ්ඩය මීළඟට යෙදෙන ස්ථානය සොයන්න | find_next.title=මේ වාක්ය ඛණ්ඩය මීළඟට යෙදෙන ස්ථානය සොයන්න | ||||||
| find_next_label=මීළඟ | find_next_label=මීළඟ | ||||||
| find_highlight=සියල්ල උද්දීපනය | find_highlight=සියල්ල උද්දීපනය | ||||||
| find_match_case_label=අකුරු ගළපන්න | find_match_case_label=අකුරු ගළපන්න | ||||||
|  | find_entire_word_label=සම්පූර්ණ වචන | ||||||
| find_reached_top=පිටුවේ ඉහළ කෙළවරට ලගාවිය, පහළ සිට ඉදිරියට යමින් | find_reached_top=පිටුවේ ඉහළ කෙළවරට ලගාවිය, පහළ සිට ඉදිරියට යමින් | ||||||
| find_reached_bottom=පිටුවේ පහළ කෙළවරට ලගාවිය, ඉහළ සිට ඉදිරියට යමින් | find_reached_bottom=පිටුවේ පහළ කෙළවරට ලගාවිය, ඉහළ සිට ඉදිරියට යමින් | ||||||
|  | # LOCALIZATION NOTE (find_match_count): The supported plural forms are | ||||||
|  | # [one|two|few|many|other], with [other] as the default value. | ||||||
|  | # "{{current}}" and "{{total}}" will be replaced by a number representing the | ||||||
|  | # index of the currently active find result, respectively a number representing | ||||||
|  | # the total number of matches in the document. | ||||||
|  | # LOCALIZATION NOTE (find_match_count_limit): The supported plural forms are | ||||||
|  | # [zero|one|two|few|many|other], with [other] as the default value. | ||||||
|  | # "{{limit}}" will be replaced by a numerical value. | ||||||
|  | find_match_count_limit[zero]=ගැලපුම් {{limit}} ට වඩා | ||||||
| find_not_found=ඔබ සෙව් වචන හමු නොවීය | find_not_found=ඔබ සෙව් වචන හමු නොවීය | ||||||
| 
 | 
 | ||||||
| # Error panel labels | # Error panel labels | ||||||
|  |  | ||||||
|  | @ -226,6 +226,10 @@ invalid_file_error=Neplatný alebo poškodený súbor PDF. | ||||||
| missing_file_error=Chýbajúci súbor PDF. | missing_file_error=Chýbajúci súbor PDF. | ||||||
| unexpected_response_error=Neočakávaná odpoveď zo servera. | unexpected_response_error=Neočakávaná odpoveď zo servera. | ||||||
| 
 | 
 | ||||||
|  | # LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be | ||||||
|  | # replaced by the modification date, and time, of the annotation. | ||||||
|  | annotation_date_string={{date}}, {{time}} | ||||||
|  | 
 | ||||||
| # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | ||||||
| # "{{type}}" will be replaced with an annotation type from a list defined in | # "{{type}}" will be replaced with an annotation type from a list defined in | ||||||
| # the PDF spec (32000-1:2008 Table 169 – Annotation types). | # the PDF spec (32000-1:2008 Table 169 – Annotation types). | ||||||
|  |  | ||||||
|  | @ -53,12 +53,12 @@ first_page_label=Pojdi na prvo stran | ||||||
| last_page.title=Pojdi na zadnjo stran | last_page.title=Pojdi na zadnjo stran | ||||||
| last_page.label=Pojdi na zadnjo stran | last_page.label=Pojdi na zadnjo stran | ||||||
| last_page_label=Pojdi na zadnjo stran | last_page_label=Pojdi na zadnjo stran | ||||||
| page_rotate_cw.title=Zavrti v smeri urninega kazalca | page_rotate_cw.title=Zavrti v smeri urnega kazalca | ||||||
| page_rotate_cw.label=Zavrti v smeri urninega kazalca | page_rotate_cw.label=Zavrti v smeri urnega kazalca | ||||||
| page_rotate_cw_label=Zavrti v smeri urninega kazalca | page_rotate_cw_label=Zavrti v smeri urnega kazalca | ||||||
| page_rotate_ccw.title=Zavrti v nasprotni smeri urninega kazalca | page_rotate_ccw.title=Zavrti v nasprotni smeri urnega kazalca | ||||||
| page_rotate_ccw.label=Zavrti v nasprotni smeri urninega kazalca | page_rotate_ccw.label=Zavrti v nasprotni smeri urnega kazalca | ||||||
| page_rotate_ccw_label=Zavrti v nasprotni smeri urninega kazalca | page_rotate_ccw_label=Zavrti v nasprotni smeri urnega kazalca | ||||||
| 
 | 
 | ||||||
| cursor_text_select_tool.title=Omogoči orodje za izbor besedila | cursor_text_select_tool.title=Omogoči orodje za izbor besedila | ||||||
| cursor_text_select_tool_label=Orodje za izbor besedila | cursor_text_select_tool_label=Orodje za izbor besedila | ||||||
|  | @ -226,6 +226,10 @@ invalid_file_error=Neveljavna ali pokvarjena datoteka PDF. | ||||||
| missing_file_error=Ni datoteke PDF. | missing_file_error=Ni datoteke PDF. | ||||||
| unexpected_response_error=Nepričakovan odgovor strežnika. | unexpected_response_error=Nepričakovan odgovor strežnika. | ||||||
| 
 | 
 | ||||||
|  | # LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be | ||||||
|  | # replaced by the modification date, and time, of the annotation. | ||||||
|  | annotation_date_string={{date}}, {{time}} | ||||||
|  | 
 | ||||||
| # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | ||||||
| # "{{type}}" will be replaced with an annotation type from a list defined in | # "{{type}}" will be replaced with an annotation type from a list defined in | ||||||
| # the PDF spec (32000-1:2008 Table 169 – Annotation types). | # the PDF spec (32000-1:2008 Table 169 – Annotation types). | ||||||
|  |  | ||||||
|  | @ -219,6 +219,10 @@ invalid_file_error=Kartelë PDF e pavlefshme ose e dëmtuar. | ||||||
| missing_file_error=Kartelë PDF që mungon. | missing_file_error=Kartelë PDF që mungon. | ||||||
| unexpected_response_error=Përgjigje shërbyesi e papritur. | unexpected_response_error=Përgjigje shërbyesi e papritur. | ||||||
| 
 | 
 | ||||||
|  | # LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be | ||||||
|  | # replaced by the modification date, and time, of the annotation. | ||||||
|  | annotation_date_string={{date}}, {{time}} | ||||||
|  | 
 | ||||||
| # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | ||||||
| # "{{type}}" will be replaced with an annotation type from a list defined in | # "{{type}}" will be replaced with an annotation type from a list defined in | ||||||
| # the PDF spec (32000-1:2008 Table 169 – Annotation types). | # the PDF spec (32000-1:2008 Table 169 – Annotation types). | ||||||
|  |  | ||||||
|  | @ -226,6 +226,10 @@ invalid_file_error=Ogiltig eller korrupt PDF-fil. | ||||||
| missing_file_error=Saknad PDF-fil. | missing_file_error=Saknad PDF-fil. | ||||||
| unexpected_response_error=Oväntat svar från servern. | unexpected_response_error=Oväntat svar från servern. | ||||||
| 
 | 
 | ||||||
|  | # LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be | ||||||
|  | # replaced by the modification date, and time, of the annotation. | ||||||
|  | annotation_date_string={{date}} {{time}} | ||||||
|  | 
 | ||||||
| # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | ||||||
| # "{{type}}" will be replaced with an annotation type from a list defined in | # "{{type}}" will be replaced with an annotation type from a list defined in | ||||||
| # the PDF spec (32000-1:2008 Table 169 – Annotation types). | # the PDF spec (32000-1:2008 Table 169 – Annotation types). | ||||||
|  |  | ||||||
|  | @ -202,6 +202,10 @@ invalid_file_error=చెల్లని లేదా పాడైన PDF ఫై | ||||||
| missing_file_error=దొరకని PDF ఫైలు. | missing_file_error=దొరకని PDF ఫైలు. | ||||||
| unexpected_response_error=అనుకోని సర్వర్ స్పందన. | unexpected_response_error=అనుకోని సర్వర్ స్పందన. | ||||||
| 
 | 
 | ||||||
|  | # LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be | ||||||
|  | # replaced by the modification date, and time, of the annotation. | ||||||
|  | annotation_date_string={{date}}, {{time}} | ||||||
|  | 
 | ||||||
| # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | ||||||
| # "{{type}}" will be replaced with an annotation type from a list defined in | # "{{type}}" will be replaced with an annotation type from a list defined in | ||||||
| # the PDF spec (32000-1:2008 Table 169 – Annotation types). | # the PDF spec (32000-1:2008 Table 169 – Annotation types). | ||||||
|  |  | ||||||
|  | @ -226,6 +226,10 @@ invalid_file_error=ไฟล์ PDF ไม่ถูกต้องหรือ | ||||||
| missing_file_error=ไฟล์ PDF หายไป | missing_file_error=ไฟล์ PDF หายไป | ||||||
| unexpected_response_error=การตอบสนองของเซิร์ฟเวอร์ที่ไม่คาดคิด | unexpected_response_error=การตอบสนองของเซิร์ฟเวอร์ที่ไม่คาดคิด | ||||||
| 
 | 
 | ||||||
|  | # LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be | ||||||
|  | # replaced by the modification date, and time, of the annotation. | ||||||
|  | annotation_date_string={{date}}, {{time}} | ||||||
|  | 
 | ||||||
| # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | # LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. | ||||||
| # "{{type}}" will be replaced with an annotation type from a list defined in | # "{{type}}" will be replaced with an annotation type from a list defined in | ||||||
| # the PDF spec (32000-1:2008 Table 169 – Annotation types). | # the PDF spec (32000-1:2008 Table 169 – Annotation types). | ||||||
|  |  | ||||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
		Reference in New Issue
	
	Block a user