Merge pull request #3 from janeczku/master

Update fork
pull/1261/head
Jony 4 years ago committed by GitHub
commit c166c92685
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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]

@ -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

@ -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:

@ -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())

@ -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 list_allowed_tags(self):
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 mature_content_tags(self): def list_allowed_column_values(self):
mct = self.config_mature_content_tags.split(",") 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$'

@ -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_cover_on_failure(use_generic_cover):
if use_generic_cover:
return send_from_directory(_STATIC_DIR, "generic_cover.jpg")
else:
return None
def get_book_cover(book_id): def get_book_cover(book_id):
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).filter(common_filters()).first()
if book.has_cover: 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)

@ -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",
}

@ -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"

@ -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)

@ -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

@ -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")

@ -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);
} }
} }

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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…
Cancel
Save